@forgeax/engine-rhi-debug 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +139 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/bootstrap-integration.test.d.ts +2 -0
- package/dist/__tests__/bootstrap-integration.test.d.ts.map +1 -0
- package/dist/__tests__/consumer-inventory.unit.test.d.ts +2 -0
- package/dist/__tests__/consumer-inventory.unit.test.d.ts.map +1 -0
- package/dist/__tests__/copy-src-usage-validity.dawn.test.d.ts +15 -0
- package/dist/__tests__/copy-src-usage-validity.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/coverage-invariant.test-d.d.ts +2 -0
- package/dist/__tests__/coverage-invariant.test-d.d.ts.map +1 -0
- package/dist/__tests__/coverage-invariant.unit.test.d.ts +2 -0
- package/dist/__tests__/coverage-invariant.unit.test.d.ts.map +1 -0
- package/dist/__tests__/e2e.browser.test.d.ts +2 -0
- package/dist/__tests__/e2e.browser.test.d.ts.map +1 -0
- package/dist/__tests__/error-code-narrowing.test-d.d.ts +2 -0
- package/dist/__tests__/error-code-narrowing.test-d.d.ts.map +1 -0
- package/dist/__tests__/errors-v7.test-d.d.ts +2 -0
- package/dist/__tests__/errors-v7.test-d.d.ts.map +1 -0
- package/dist/__tests__/errors-v7.unit.test.d.ts +2 -0
- package/dist/__tests__/errors-v7.unit.test.d.ts.map +1 -0
- package/dist/__tests__/errors.unit.test.d.ts +2 -0
- package/dist/__tests__/errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/event-semantics-coverage.unit.test.d.ts +2 -0
- package/dist/__tests__/event-semantics-coverage.unit.test.d.ts.map +1 -0
- package/dist/__tests__/format-info.unit.test.d.ts +2 -0
- package/dist/__tests__/format-info.unit.test.d.ts.map +1 -0
- package/dist/__tests__/frame-model-parity.test-d.d.ts +2 -0
- package/dist/__tests__/frame-model-parity.test-d.d.ts.map +1 -0
- package/dist/__tests__/frame-model-parity.unit.test.d.ts +2 -0
- package/dist/__tests__/frame-model-parity.unit.test.d.ts.map +1 -0
- package/dist/__tests__/guard-gates.test.d.ts +2 -0
- package/dist/__tests__/guard-gates.test.d.ts.map +1 -0
- package/dist/__tests__/public-surface.integration.test.d.ts +2 -0
- package/dist/__tests__/public-surface.integration.test.d.ts.map +1 -0
- package/dist/__tests__/readback-fault-cleanup.unit.test.d.ts +2 -0
- package/dist/__tests__/readback-fault-cleanup.unit.test.d.ts.map +1 -0
- package/dist/__tests__/readback-format-matrix.dawn.test.d.ts +2 -0
- package/dist/__tests__/readback-format-matrix.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/readback-format-matrix.unit.test.d.ts +2 -0
- package/dist/__tests__/readback-format-matrix.unit.test.d.ts.map +1 -0
- package/dist/__tests__/recorder-bootstrap.unit.test.d.ts +2 -0
- package/dist/__tests__/recorder-bootstrap.unit.test.d.ts.map +1 -0
- package/dist/__tests__/recorder-budget.unit.test.d.ts +2 -0
- package/dist/__tests__/recorder-budget.unit.test.d.ts.map +1 -0
- package/dist/__tests__/recorder-fail-fast.unit.test.d.ts +2 -0
- package/dist/__tests__/recorder-fail-fast.unit.test.d.ts.map +1 -0
- package/dist/__tests__/recorder-lifecycle.unit.test.d.ts +2 -0
- package/dist/__tests__/recorder-lifecycle.unit.test.d.ts.map +1 -0
- package/dist/__tests__/recorder-push-event.unit.test.d.ts +2 -0
- package/dist/__tests__/recorder-push-event.unit.test.d.ts.map +1 -0
- package/dist/__tests__/recorder-session.integration.test.d.ts +2 -0
- package/dist/__tests__/recorder-session.integration.test.d.ts.map +1 -0
- package/dist/__tests__/recorder-session.unit.test.d.ts +2 -0
- package/dist/__tests__/recorder-session.unit.test.d.ts.map +1 -0
- package/dist/__tests__/recorder-steady-frame.dawn.test.d.ts +2 -0
- package/dist/__tests__/recorder-steady-frame.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/replay-fail-fast.unit.test.d.ts +2 -0
- package/dist/__tests__/replay-fail-fast.unit.test.d.ts.map +1 -0
- package/dist/__tests__/replay-session.dawn.test.d.ts +2 -0
- package/dist/__tests__/replay-session.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/replay-session.test-d.d.ts +2 -0
- package/dist/__tests__/replay-session.test-d.d.ts.map +1 -0
- package/dist/__tests__/replay-session.unit.test.d.ts +2 -0
- package/dist/__tests__/replay-session.unit.test.d.ts.map +1 -0
- package/dist/__tests__/resolve-texture-descriptor.unit.test.d.ts +2 -0
- package/dist/__tests__/resolve-texture-descriptor.unit.test.d.ts.map +1 -0
- package/dist/__tests__/resource-lifecycle.unit.test.d.ts +2 -0
- package/dist/__tests__/resource-lifecycle.unit.test.d.ts.map +1 -0
- package/dist/__tests__/rhi-debug-fresh-replay.dawn.test.d.ts +2 -0
- package/dist/__tests__/rhi-debug-fresh-replay.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/tape-index.unit.test.d.ts +2 -0
- package/dist/__tests__/tape-index.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tape-v7-invalid.unit.test.d.ts +2 -0
- package/dist/__tests__/tape-v7-invalid.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tape-v7-roundtrip.unit.test.d.ts +2 -0
- package/dist/__tests__/tape-v7-roundtrip.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tape-v7-version.unit.test.d.ts +2 -0
- package/dist/__tests__/tape-v7-version.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tree-shake.unit.test.d.ts +2 -0
- package/dist/__tests__/tree-shake.unit.test.d.ts.map +1 -0
- package/dist/__tests__/types.test.d.ts +2 -0
- package/dist/__tests__/types.test.d.ts.map +1 -0
- package/dist/browser.d.ts +45 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.mjs +83 -0
- package/dist/browser.mjs.map +1 -0
- package/dist/errors.d.ts +85 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/frame-model.d.ts +126 -0
- package/dist/frame-model.d.ts.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +5810 -0
- package/dist/index.mjs.map +1 -0
- package/dist/protocol/codec.d.ts +6 -0
- package/dist/protocol/codec.d.ts.map +1 -0
- package/dist/protocol/event-semantics.d.ts +15 -0
- package/dist/protocol/event-semantics.d.ts.map +1 -0
- package/dist/protocol/tape-index.d.ts +30 -0
- package/dist/protocol/tape-index.d.ts.map +1 -0
- package/dist/protocol/types.d.ts +44 -0
- package/dist/protocol/types.d.ts.map +1 -0
- package/dist/protocol/validation.d.ts +7 -0
- package/dist/protocol/validation.d.ts.map +1 -0
- package/dist/readback.d.ts +156 -0
- package/dist/readback.d.ts.map +1 -0
- package/dist/recorder/assemble.d.ts +11 -0
- package/dist/recorder/assemble.d.ts.map +1 -0
- package/dist/recorder/closure.d.ts +39 -0
- package/dist/recorder/closure.d.ts.map +1 -0
- package/dist/recorder/core.d.ts +296 -0
- package/dist/recorder/core.d.ts.map +1 -0
- package/dist/recorder/device.d.ts +4 -0
- package/dist/recorder/device.d.ts.map +1 -0
- package/dist/recorder/encoder.d.ts +5 -0
- package/dist/recorder/encoder.d.ts.map +1 -0
- package/dist/recorder/lifecycle.d.ts +20 -0
- package/dist/recorder/lifecycle.d.ts.map +1 -0
- package/dist/recorder/pass.d.ts +6 -0
- package/dist/recorder/pass.d.ts.map +1 -0
- package/dist/recorder/proxy.d.ts +19 -0
- package/dist/recorder/proxy.d.ts.map +1 -0
- package/dist/recorder/queue.d.ts +4 -0
- package/dist/recorder/queue.d.ts.map +1 -0
- package/dist/recorder/resource-registry.d.ts +15 -0
- package/dist/recorder/resource-registry.d.ts.map +1 -0
- package/dist/recorder/session.d.ts +25 -0
- package/dist/recorder/session.d.ts.map +1 -0
- package/dist/recorder/shader.d.ts +3 -0
- package/dist/recorder/shader.d.ts.map +1 -0
- package/dist/recorder/snapshot.d.ts +10 -0
- package/dist/recorder/snapshot.d.ts.map +1 -0
- package/dist/recorder/wrap.d.ts +4 -0
- package/dist/recorder/wrap.d.ts.map +1 -0
- package/dist/recorder.d.ts +7 -0
- package/dist/recorder.d.ts.map +1 -0
- package/dist/replay/execute-support.d.ts +33 -0
- package/dist/replay/execute-support.d.ts.map +1 -0
- package/dist/replay/execute.d.ts +18 -0
- package/dist/replay/execute.d.ts.map +1 -0
- package/dist/replay/readback.d.ts +25 -0
- package/dist/replay/readback.d.ts.map +1 -0
- package/dist/replay/resources.d.ts +57 -0
- package/dist/replay/resources.d.ts.map +1 -0
- package/dist/replay/session.d.ts +32 -0
- package/dist/replay/session.d.ts.map +1 -0
- package/dist/replay/texture-format.d.ts +21 -0
- package/dist/replay/texture-format.d.ts.map +1 -0
- package/dist/texel-decode.d.ts +34 -0
- package/dist/texel-decode.d.ts.map +1 -0
- package/dist/texel-layout.d.ts +76 -0
- package/dist/texel-layout.d.ts.map +1 -0
- package/dist/types.d.ts +704 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +72 -0
- package/src/__tests__/bootstrap-integration.test.ts +268 -0
- package/src/__tests__/consumer-inventory.unit.test.ts +106 -0
- package/src/__tests__/copy-src-usage-validity.dawn.test.ts +117 -0
- package/src/__tests__/coverage-invariant.test-d.ts +115 -0
- package/src/__tests__/coverage-invariant.unit.test.ts +45 -0
- package/src/__tests__/e2e.browser.test.ts +234 -0
- package/src/__tests__/error-code-narrowing.test-d.ts +32 -0
- package/src/__tests__/errors-v7.test-d.ts +19 -0
- package/src/__tests__/errors-v7.unit.test.ts +15 -0
- package/src/__tests__/errors.unit.test.ts +83 -0
- package/src/__tests__/event-semantics-coverage.unit.test.ts +22 -0
- package/src/__tests__/format-info.unit.test.ts +85 -0
- package/src/__tests__/frame-model-parity.test-d.ts +8 -0
- package/src/__tests__/frame-model-parity.unit.test.ts +38 -0
- package/src/__tests__/guard-gates.test.ts +144 -0
- package/src/__tests__/public-surface.integration.test.ts +30 -0
- package/src/__tests__/readback-fault-cleanup.unit.test.ts +66 -0
- package/src/__tests__/readback-format-matrix.dawn.test.ts +155 -0
- package/src/__tests__/readback-format-matrix.unit.test.ts +96 -0
- package/src/__tests__/recorder-bootstrap.unit.test.ts +302 -0
- package/src/__tests__/recorder-budget.unit.test.ts +42 -0
- package/src/__tests__/recorder-fail-fast.unit.test.ts +434 -0
- package/src/__tests__/recorder-lifecycle.unit.test.ts +42 -0
- package/src/__tests__/recorder-push-event.unit.test.ts +573 -0
- package/src/__tests__/recorder-session.integration.test.ts +32 -0
- package/src/__tests__/recorder-session.unit.test.ts +63 -0
- package/src/__tests__/recorder-steady-frame.dawn.test.ts +52 -0
- package/src/__tests__/replay-fail-fast.unit.test.ts +143 -0
- package/src/__tests__/replay-session.dawn.test.ts +217 -0
- package/src/__tests__/replay-session.test-d.ts +21 -0
- package/src/__tests__/replay-session.unit.test.ts +155 -0
- package/src/__tests__/resolve-texture-descriptor.unit.test.ts +144 -0
- package/src/__tests__/resource-lifecycle.unit.test.ts +114 -0
- package/src/__tests__/rhi-debug-fresh-replay.dawn.test.ts +176 -0
- package/src/__tests__/tape-index.unit.test.ts +53 -0
- package/src/__tests__/tape-v7-invalid.unit.test.ts +45 -0
- package/src/__tests__/tape-v7-roundtrip.unit.test.ts +66 -0
- package/src/__tests__/tape-v7-version.unit.test.ts +13 -0
- package/src/__tests__/tree-shake.unit.test.ts +127 -0
- package/src/__tests__/types.test.ts +257 -0
- package/src/browser.ts +146 -0
- package/src/errors.ts +173 -0
- package/src/frame-model.ts +455 -0
- package/src/index.ts +85 -0
- package/src/pako.d.ts +8 -0
- package/src/protocol/codec.ts +220 -0
- package/src/protocol/event-semantics.ts +180 -0
- package/src/protocol/tape-index.ts +120 -0
- package/src/protocol/types.ts +60 -0
- package/src/protocol/validation.ts +173 -0
- package/src/readback.ts +804 -0
- package/src/recorder/assemble.ts +192 -0
- package/src/recorder/closure.ts +408 -0
- package/src/recorder/core.ts +587 -0
- package/src/recorder/device.ts +456 -0
- package/src/recorder/encoder.ts +234 -0
- package/src/recorder/lifecycle.ts +847 -0
- package/src/recorder/pass.ts +241 -0
- package/src/recorder/proxy.ts +59 -0
- package/src/recorder/queue.ts +109 -0
- package/src/recorder/resource-registry.ts +53 -0
- package/src/recorder/session.ts +152 -0
- package/src/recorder/shader.ts +47 -0
- package/src/recorder/snapshot.ts +38 -0
- package/src/recorder/wrap.ts +157 -0
- package/src/recorder.ts +15 -0
- package/src/replay/execute-support.ts +243 -0
- package/src/replay/execute.ts +870 -0
- package/src/replay/readback.ts +583 -0
- package/src/replay/resources.ts +154 -0
- package/src/replay/session.ts +464 -0
- package/src/replay/texture-format.ts +99 -0
- package/src/texel-decode.ts +255 -0
- package/src/texel-layout.ts +324 -0
- package/src/types.ts +864 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { buildFrameModel } from '../frame-model';
|
|
3
|
+
import type { Tape } from '../protocol/types';
|
|
4
|
+
|
|
5
|
+
describe('FrameModel parity', () => {
|
|
6
|
+
it('is JSON-safe and derives work entries from the same index', () => {
|
|
7
|
+
const tape: Tape = {
|
|
8
|
+
header: { formatVersion: 7, rhiCaps: {}, eventCount: 2, blobCount: 0 },
|
|
9
|
+
bootstrap: [],
|
|
10
|
+
events: [
|
|
11
|
+
{
|
|
12
|
+
kind: 'beginRenderPass',
|
|
13
|
+
passHandleId: 'pass:1',
|
|
14
|
+
cmdHandleId: 'cmd:1',
|
|
15
|
+
desc: { colorAttachments: [] },
|
|
16
|
+
colorAttachmentViewHandleIds: [],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
kind: 'draw',
|
|
20
|
+
passHandleId: 'pass:1',
|
|
21
|
+
vertexCount: 3,
|
|
22
|
+
instanceCount: 1,
|
|
23
|
+
firstVertex: 0,
|
|
24
|
+
firstInstance: 0,
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
blobs: [],
|
|
28
|
+
};
|
|
29
|
+
const model = buildFrameModel(tape);
|
|
30
|
+
expect(model.works[0]).toMatchObject({
|
|
31
|
+
workIndex: 0,
|
|
32
|
+
eventIndex: 1,
|
|
33
|
+
passIndex: 0,
|
|
34
|
+
kind: 'draw',
|
|
35
|
+
});
|
|
36
|
+
expect(JSON.stringify(model)).not.toContain('Map');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// @forgeax/engine-rhi-debug/src/__tests__/guard-gates.test.ts
|
|
2
|
+
//
|
|
3
|
+
// Build-time codebase guard assertions using filesystem reads. These act as canaries: any future change that adds a
|
|
4
|
+
// flag-drift point or a second error owner turns this test red.
|
|
5
|
+
//
|
|
6
|
+
// AC-07: full-repo grep zero-hit for --runId / --ws-url (flag drift).
|
|
7
|
+
// AC-08 partial: import.meta.hot usage in create-app.ts is inside the
|
|
8
|
+
// rhiDebugFlag === '1' guard block.
|
|
9
|
+
//
|
|
10
|
+
// t10; requirements AC-07/AC-08/AC-09; plan-strategy §2 D-8.
|
|
11
|
+
|
|
12
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { describe, expect, it } from 'vitest';
|
|
16
|
+
|
|
17
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const ENGINE_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
|
19
|
+
|
|
20
|
+
function flagDriftHits(root: string): string[] {
|
|
21
|
+
const hits: string[] = [];
|
|
22
|
+
const visit = (directory: string): void => {
|
|
23
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
24
|
+
if (entry.isDirectory()) {
|
|
25
|
+
if (entry.name === 'dist' || entry.name === 'node_modules') continue;
|
|
26
|
+
visit(path.join(directory, entry.name));
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (!entry.isFile() || (!entry.name.endsWith('.ts') && !entry.name.endsWith('.mjs'))) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (entry.name === 'guard-gates.test.ts') continue;
|
|
33
|
+
const absolute = path.join(directory, entry.name);
|
|
34
|
+
const source = readFileSync(absolute, 'utf8');
|
|
35
|
+
if (source.includes('--runId') || source.includes('--ws-url')) {
|
|
36
|
+
hits.push(path.relative(ENGINE_ROOT, absolute));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
visit(root);
|
|
41
|
+
return hits.sort();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('AC-07: flag drift grep gate', () => {
|
|
45
|
+
it('zero hits for --runId / --ws-url across apps/ packages/ (excluding dist, node_modules, self)', () => {
|
|
46
|
+
expect([
|
|
47
|
+
...flagDriftHits(path.join(ENGINE_ROOT, 'apps')),
|
|
48
|
+
...flagDriftHits(path.join(ENGINE_ROOT, 'packages')),
|
|
49
|
+
]).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('RHI-debug error owner gate', () => {
|
|
54
|
+
it('keeps the v7 error union as the only error owner', () => {
|
|
55
|
+
const errorsPath = path.resolve(__dirname, '..', '..', 'src', 'errors.ts');
|
|
56
|
+
const content = readFileSync(errorsPath, 'utf-8');
|
|
57
|
+
expect(content).toContain('export type RhiDebugErrorCode =');
|
|
58
|
+
expect(content).not.toMatch(/\bDebugErrorCode\b/);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('W92: readback staging usage owner', () => {
|
|
63
|
+
it('keeps one COPY_DST | MAP_READ owner for all three readback paths', () => {
|
|
64
|
+
const readbackPath = path.resolve(__dirname, '..', '..', 'src', 'readback.ts');
|
|
65
|
+
const content = readFileSync(readbackPath, 'utf-8');
|
|
66
|
+
expect(content.match(/const COPY_DST_MAP_READ = 9/g)).toHaveLength(1);
|
|
67
|
+
expect(content.match(/usage: COPY_DST_MAP_READ/g)).toHaveLength(3);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('AC-08 partial: import.meta.hot in rhiDebugFlag guard', () => {
|
|
72
|
+
it('all hotMeta.hot / import.meta.hot code references are inside rhiDebugFlag guard block', () => {
|
|
73
|
+
const createAppPath = path.resolve(
|
|
74
|
+
__dirname,
|
|
75
|
+
'..',
|
|
76
|
+
'..',
|
|
77
|
+
'..',
|
|
78
|
+
'..',
|
|
79
|
+
'packages',
|
|
80
|
+
'app',
|
|
81
|
+
'src',
|
|
82
|
+
'create-app.ts',
|
|
83
|
+
);
|
|
84
|
+
const content = readFileSync(createAppPath, 'utf-8');
|
|
85
|
+
const lines = content.split('\n');
|
|
86
|
+
|
|
87
|
+
const guardConditionIdx = lines.findIndex((l) => l.includes("rhiDebugFlag === '1'"));
|
|
88
|
+
expect(guardConditionIdx).not.toBe(-1);
|
|
89
|
+
let guardOpenIdx = -1;
|
|
90
|
+
for (let i = guardConditionIdx; i >= 0; i--) {
|
|
91
|
+
if (lines[i]?.includes('if (')) {
|
|
92
|
+
guardOpenIdx = i;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
expect(guardOpenIdx).not.toBe(-1);
|
|
97
|
+
|
|
98
|
+
const guardBraceIdx = lines.findIndex(
|
|
99
|
+
(l, index) => index >= guardConditionIdx && l.includes('{'),
|
|
100
|
+
);
|
|
101
|
+
expect(guardBraceIdx).not.toBe(-1);
|
|
102
|
+
|
|
103
|
+
// Find the matching '}' at the same indent level as the 'if' statement.
|
|
104
|
+
// Avoid counting braces inside template literals or strings by matching
|
|
105
|
+
// the exact indent prefix.
|
|
106
|
+
const guardLine = lines[guardBraceIdx];
|
|
107
|
+
if (guardLine === undefined) throw new Error('unreachable: guardBraceIdx verified above');
|
|
108
|
+
const indentMatch = guardLine.match(/^(\s*)/);
|
|
109
|
+
if (indentMatch === null)
|
|
110
|
+
throw new Error('unreachable: every line matches the whitespace regex');
|
|
111
|
+
const indent = indentMatch[1];
|
|
112
|
+
let guardCloseIdx = -1;
|
|
113
|
+
for (let i = guardBraceIdx + 1; i < lines.length; i++) {
|
|
114
|
+
if (lines[i] === `${indent}}`) {
|
|
115
|
+
guardCloseIdx = i;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
expect(guardCloseIdx).not.toBe(-1);
|
|
120
|
+
|
|
121
|
+
// Verify all hotMeta / import.meta.hot references in non-comment lines
|
|
122
|
+
// fall within the guard block.
|
|
123
|
+
for (let i = 0; i < lines.length; i++) {
|
|
124
|
+
const line = lines[i];
|
|
125
|
+
if (line === undefined) break;
|
|
126
|
+
const trimmed = line.trimStart();
|
|
127
|
+
// Skip comment-only lines.
|
|
128
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*')) continue;
|
|
129
|
+
if (
|
|
130
|
+
trimmed.includes('hotMeta') ||
|
|
131
|
+
(trimmed.includes('import.meta') && trimmed.includes('hot'))
|
|
132
|
+
) {
|
|
133
|
+
expect(
|
|
134
|
+
i,
|
|
135
|
+
`import.meta.hot reference at line ${i + 1} is outside rhiDebugFlag guard (guard: ${guardOpenIdx + 1}-${guardCloseIdx + 1})`,
|
|
136
|
+
).toBeGreaterThanOrEqual(guardOpenIdx);
|
|
137
|
+
expect(
|
|
138
|
+
i,
|
|
139
|
+
`import.meta.hot reference at line ${i + 1} is outside rhiDebugFlag guard (guard: ${guardOpenIdx + 1}-${guardCloseIdx + 1})`,
|
|
140
|
+
).toBeLessThanOrEqual(guardCloseIdx);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import * as root from '../index';
|
|
5
|
+
|
|
6
|
+
const packageJsonPath = resolve(import.meta.dirname, '../../package.json');
|
|
7
|
+
|
|
8
|
+
describe('v7 public surface', () => {
|
|
9
|
+
it('exposes protocol, index, model, and closed error roots', () => {
|
|
10
|
+
expect(root.decodeTape).toBeTypeOf('function');
|
|
11
|
+
expect(root.encodeTape).toBeTypeOf('function');
|
|
12
|
+
expect(root.buildTapeIndex).toBeTypeOf('function');
|
|
13
|
+
expect(root.buildFrameModel).toBeTypeOf('function');
|
|
14
|
+
expect(root.createRhiDebugError).toBeTypeOf('function');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('declares only the v7 root and browser entry', () => {
|
|
18
|
+
const manifest = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as {
|
|
19
|
+
exports: Record<string, unknown>;
|
|
20
|
+
};
|
|
21
|
+
expect(manifest.exports['.']).toBeDefined();
|
|
22
|
+
expect(manifest.exports['./browser']).toMatchObject({ import: './dist/browser.mjs' });
|
|
23
|
+
expect(manifest.exports['./frame-model']).toBeUndefined();
|
|
24
|
+
expect(manifest.exports['./resource-lifecycle']).toBeUndefined();
|
|
25
|
+
expect(manifest.exports['./dev-routes']).toBeUndefined();
|
|
26
|
+
expect(manifest.exports['./capture-browser']).toBeUndefined();
|
|
27
|
+
expect(manifest.exports['./cli']).toBeUndefined();
|
|
28
|
+
expect(Object.keys(manifest.exports).sort()).toEqual(['.', './browser', './package.json']);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { RhiDevice } from '@forgeax/engine-rhi';
|
|
2
|
+
import { createShaderModule, rhi } from '@forgeax/engine-rhi-null';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import type { BootstrapResource, Tape } from '../protocol/types';
|
|
5
|
+
import type { ReplayBackend } from '../replay/session';
|
|
6
|
+
import { openReplay } from '../replay/session';
|
|
7
|
+
|
|
8
|
+
const buffer: BootstrapResource = {
|
|
9
|
+
handleId: 'buffer:readback',
|
|
10
|
+
kind: 'buffer',
|
|
11
|
+
create: {
|
|
12
|
+
kind: 'createBuffer',
|
|
13
|
+
handleId: 'buffer:readback',
|
|
14
|
+
desc: { size: 32, usage: 8 },
|
|
15
|
+
},
|
|
16
|
+
initialData: [],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const tape: Tape = {
|
|
20
|
+
header: { formatVersion: 7, rhiCaps: {}, eventCount: 0, blobCount: 0 },
|
|
21
|
+
bootstrap: [buffer],
|
|
22
|
+
events: [],
|
|
23
|
+
blobs: [],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
async function backendWithDestroySpy(): Promise<{
|
|
27
|
+
backend: ReplayBackend;
|
|
28
|
+
destroy: ReturnType<typeof vi.fn>;
|
|
29
|
+
}> {
|
|
30
|
+
const adapter = await rhi.requestAdapter();
|
|
31
|
+
if (!adapter.ok) throw new Error(adapter.error.hint);
|
|
32
|
+
const result = await adapter.value.requestDevice();
|
|
33
|
+
if (!result.ok) throw new Error(result.error.hint);
|
|
34
|
+
const base = result.value;
|
|
35
|
+
const destroy = vi.fn((value) => base.destroyBuffer(value));
|
|
36
|
+
const device = Object.create(base) as RhiDevice;
|
|
37
|
+
Object.defineProperty(device, 'destroyBuffer', { configurable: true, value: destroy });
|
|
38
|
+
return { backend: { device, createShaderModule }, destroy };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('Replay readback cleanup', () => {
|
|
42
|
+
it('cleans staging resources when map/readback fails', async () => {
|
|
43
|
+
const { backend, destroy } = await backendWithDestroySpy();
|
|
44
|
+
const replay = await openReplay(tape, backend);
|
|
45
|
+
expect(replay.ok).toBe(true);
|
|
46
|
+
if (!replay.ok) return;
|
|
47
|
+
|
|
48
|
+
const readback = await replay.value.readResource('buffer:readback', { offset: 4, size: 8 });
|
|
49
|
+
expect(readback.ok).toBe(false);
|
|
50
|
+
if (!readback.ok) expect(readback.error.code).toBe('readback-failed');
|
|
51
|
+
expect(destroy).toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('rejects a range that escapes the recorded buffer before GPU work', async () => {
|
|
55
|
+
const replay = await openReplay(
|
|
56
|
+
tape,
|
|
57
|
+
await backendWithDestroySpy().then((value) => value.backend),
|
|
58
|
+
);
|
|
59
|
+
expect(replay.ok).toBe(true);
|
|
60
|
+
if (!replay.ok) return;
|
|
61
|
+
|
|
62
|
+
const readback = await replay.value.readResource('buffer:readback', { offset: 24, size: 16 });
|
|
63
|
+
expect(readback.ok).toBe(false);
|
|
64
|
+
if (!readback.ok) expect(readback.error.code).toBe('readback-failed');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { RhiInstance } from '@forgeax/engine-rhi';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import type { BootstrapResource, Tape } from '../protocol/types';
|
|
4
|
+
import type { ReplayBackend } from '../replay/session';
|
|
5
|
+
import { openReplay } from '../replay/session';
|
|
6
|
+
|
|
7
|
+
interface DawnPack {
|
|
8
|
+
readonly rhi: RhiInstance;
|
|
9
|
+
readonly createShaderModule: ReplayBackend['createShaderModule'];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function loadDawn(): Promise<DawnPack> {
|
|
13
|
+
return (await import('@forgeax/engine-rhi-webgpu')) as unknown as DawnPack;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const SKIP_DAWN = process.env.FORGEAX_SKIP_DAWN === '1';
|
|
17
|
+
|
|
18
|
+
function colorResource(): BootstrapResource {
|
|
19
|
+
return {
|
|
20
|
+
handleId: 'texture:color',
|
|
21
|
+
kind: 'texture',
|
|
22
|
+
create: {
|
|
23
|
+
kind: 'createTexture',
|
|
24
|
+
handleId: 'texture:color',
|
|
25
|
+
desc: {
|
|
26
|
+
size: { width: 4, height: 4, depthOrArrayLayers: 1 },
|
|
27
|
+
format: 'rgba8unorm',
|
|
28
|
+
dimension: '2d',
|
|
29
|
+
mipLevelCount: 1,
|
|
30
|
+
sampleCount: 1,
|
|
31
|
+
usage: 1,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
initialData: [],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function depthResource(
|
|
39
|
+
handleId: string,
|
|
40
|
+
format: 'depth24plus' | 'depth24plus-stencil8',
|
|
41
|
+
): BootstrapResource {
|
|
42
|
+
return {
|
|
43
|
+
handleId,
|
|
44
|
+
kind: 'texture',
|
|
45
|
+
create: {
|
|
46
|
+
kind: 'createTexture',
|
|
47
|
+
handleId,
|
|
48
|
+
desc: {
|
|
49
|
+
size: { width: 4, height: 4, depthOrArrayLayers: 1 },
|
|
50
|
+
format,
|
|
51
|
+
dimension: '2d',
|
|
52
|
+
mipLevelCount: 1,
|
|
53
|
+
sampleCount: 1,
|
|
54
|
+
usage: 0x15,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
initialData: [],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe.skipIf(SKIP_DAWN)('Dawn replay-owned readback matrix', () => {
|
|
62
|
+
it('reads a color subresource on a fresh Dawn device', async () => {
|
|
63
|
+
const pack = await loadDawn();
|
|
64
|
+
const adapter = await pack.rhi.requestAdapter();
|
|
65
|
+
expect(adapter.ok).toBe(true);
|
|
66
|
+
if (!adapter.ok) throw new Error(`Dawn admission failed: ${adapter.error.code}`);
|
|
67
|
+
const device = await adapter.value.requestDevice();
|
|
68
|
+
expect(device.ok).toBe(true);
|
|
69
|
+
if (!device.ok) throw new Error(`Dawn device admission failed: ${device.error.code}`);
|
|
70
|
+
|
|
71
|
+
const tape: Tape = {
|
|
72
|
+
header: { formatVersion: 7, rhiCaps: {}, eventCount: 0, blobCount: 0 },
|
|
73
|
+
bootstrap: [colorResource()],
|
|
74
|
+
events: [],
|
|
75
|
+
blobs: [],
|
|
76
|
+
};
|
|
77
|
+
const replay = await openReplay(tape, {
|
|
78
|
+
device: device.value,
|
|
79
|
+
createShaderModule: pack.createShaderModule,
|
|
80
|
+
});
|
|
81
|
+
expect(replay.ok).toBe(true);
|
|
82
|
+
if (!replay.ok) throw new Error(replay.error.hint);
|
|
83
|
+
const pixels = await replay.value.readResource('texture:color', {
|
|
84
|
+
mipLevel: 0,
|
|
85
|
+
arrayLayer: 0,
|
|
86
|
+
aspect: 'all',
|
|
87
|
+
});
|
|
88
|
+
expect(pixels.ok).toBe(true);
|
|
89
|
+
if (!pixels.ok) throw new Error(pixels.error.hint);
|
|
90
|
+
expect(pixels.value.bytes.byteLength).toBe(4 * 4 * 4);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('reads depth24plus through the replay-owned depth-to-color blit', async () => {
|
|
94
|
+
const pack = await loadDawn();
|
|
95
|
+
const adapter = await pack.rhi.requestAdapter();
|
|
96
|
+
expect(adapter.ok).toBe(true);
|
|
97
|
+
if (!adapter.ok) throw new Error(`Dawn admission failed: ${adapter.error.code}`);
|
|
98
|
+
const device = await adapter.value.requestDevice();
|
|
99
|
+
expect(device.ok).toBe(true);
|
|
100
|
+
if (!device.ok) throw new Error(`Dawn device admission failed: ${device.error.code}`);
|
|
101
|
+
|
|
102
|
+
const replay = await openReplay(
|
|
103
|
+
{
|
|
104
|
+
header: { formatVersion: 7, rhiCaps: {}, eventCount: 0, blobCount: 0 },
|
|
105
|
+
bootstrap: [depthResource('texture:depth24plus', 'depth24plus')],
|
|
106
|
+
events: [],
|
|
107
|
+
blobs: [],
|
|
108
|
+
},
|
|
109
|
+
{ device: device.value, createShaderModule: pack.createShaderModule },
|
|
110
|
+
);
|
|
111
|
+
expect(replay.ok).toBe(true);
|
|
112
|
+
if (!replay.ok) throw new Error(replay.error.hint);
|
|
113
|
+
const pixels = await replay.value.readResource('texture:depth24plus', {
|
|
114
|
+
mipLevel: 0,
|
|
115
|
+
arrayLayer: 0,
|
|
116
|
+
aspect: 'depth-only',
|
|
117
|
+
});
|
|
118
|
+
expect(pixels.ok).toBe(true);
|
|
119
|
+
if (!pixels.ok) throw new Error(pixels.error.hint);
|
|
120
|
+
expect(pixels.value.bytes.byteLength).toBe(4 * 4 * 4);
|
|
121
|
+
expect([...new Float32Array(pixels.value.bytes.buffer)]).toHaveLength(16);
|
|
122
|
+
await replay.value.dispose();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('reads the explicit stencil aspect on depth24plus-stencil8', async () => {
|
|
126
|
+
const pack = await loadDawn();
|
|
127
|
+
const adapter = await pack.rhi.requestAdapter();
|
|
128
|
+
expect(adapter.ok).toBe(true);
|
|
129
|
+
if (!adapter.ok) throw new Error(`Dawn admission failed: ${adapter.error.code}`);
|
|
130
|
+
const device = await adapter.value.requestDevice();
|
|
131
|
+
expect(device.ok).toBe(true);
|
|
132
|
+
if (!device.ok) throw new Error(`Dawn device admission failed: ${device.error.code}`);
|
|
133
|
+
|
|
134
|
+
const replay = await openReplay(
|
|
135
|
+
{
|
|
136
|
+
header: { formatVersion: 7, rhiCaps: {}, eventCount: 0, blobCount: 0 },
|
|
137
|
+
bootstrap: [depthResource('texture:stencil', 'depth24plus-stencil8')],
|
|
138
|
+
events: [],
|
|
139
|
+
blobs: [],
|
|
140
|
+
},
|
|
141
|
+
{ device: device.value, createShaderModule: pack.createShaderModule },
|
|
142
|
+
);
|
|
143
|
+
expect(replay.ok).toBe(true);
|
|
144
|
+
if (!replay.ok) throw new Error(replay.error.hint);
|
|
145
|
+
const pixels = await replay.value.readResource('texture:stencil', {
|
|
146
|
+
mipLevel: 0,
|
|
147
|
+
arrayLayer: 0,
|
|
148
|
+
aspect: 'stencil-only',
|
|
149
|
+
});
|
|
150
|
+
expect(pixels.ok).toBe(true);
|
|
151
|
+
if (!pixels.ok) throw new Error(pixels.error.hint);
|
|
152
|
+
expect(pixels.value.bytes.byteLength).toBe(4 * 4);
|
|
153
|
+
await replay.value.dispose();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createShaderModule, rhi } from '@forgeax/engine-rhi-null';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import type { BootstrapResource, Tape } from '../protocol/types';
|
|
4
|
+
import type { TextureSubresource } from '../replay/session';
|
|
5
|
+
import { openReplay } from '../replay/session';
|
|
6
|
+
import { getTextureReadbackPlan } from '../replay/texture-format';
|
|
7
|
+
|
|
8
|
+
function textureResource(
|
|
9
|
+
handleId: string,
|
|
10
|
+
format: string,
|
|
11
|
+
dimension: string,
|
|
12
|
+
layers = 1,
|
|
13
|
+
): BootstrapResource {
|
|
14
|
+
return {
|
|
15
|
+
handleId,
|
|
16
|
+
kind: 'texture',
|
|
17
|
+
create: {
|
|
18
|
+
kind: 'createTexture',
|
|
19
|
+
handleId,
|
|
20
|
+
desc: {
|
|
21
|
+
size: { width: 4, height: 4, depthOrArrayLayers: layers },
|
|
22
|
+
format,
|
|
23
|
+
dimension,
|
|
24
|
+
mipLevelCount: 1,
|
|
25
|
+
sampleCount: 1,
|
|
26
|
+
usage: 1,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
initialData: [],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function tape(bootstrap: readonly BootstrapResource[]): Tape {
|
|
34
|
+
return {
|
|
35
|
+
header: { formatVersion: 7, rhiCaps: {}, eventCount: 0, blobCount: 0 },
|
|
36
|
+
bootstrap,
|
|
37
|
+
events: [],
|
|
38
|
+
blobs: [],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function nullBackend() {
|
|
43
|
+
const adapter = await rhi.requestAdapter();
|
|
44
|
+
if (!adapter.ok) throw new Error(adapter.error.hint);
|
|
45
|
+
const device = await adapter.value.requestDevice();
|
|
46
|
+
if (!device.ok) throw new Error(device.error.hint);
|
|
47
|
+
return { device: device.value, createShaderModule };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('Replay readback format matrix', () => {
|
|
51
|
+
it.each([
|
|
52
|
+
['rgba8unorm', '2d'],
|
|
53
|
+
['depth32float', '2d'],
|
|
54
|
+
['depth24plus', '2d'],
|
|
55
|
+
['depth24plus-stencil8', '2d-array'],
|
|
56
|
+
['rgba8unorm', 'cube'],
|
|
57
|
+
])('keeps %s/%s in the supported matrix', (format, dimension) => {
|
|
58
|
+
const plan = getTextureReadbackPlan({ format, dimension });
|
|
59
|
+
expect(plan.supported).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it.each([
|
|
63
|
+
['bc7-rgba-unorm', '2d'],
|
|
64
|
+
['rgba8unorm', '3d'],
|
|
65
|
+
])('returns an explicit unsupported plan for %s/%s', (format, dimension) => {
|
|
66
|
+
const plan = getTextureReadbackPlan({ format, dimension });
|
|
67
|
+
expect(plan.supported).toBe(false);
|
|
68
|
+
if (!plan.supported) expect(plan.reason.length).toBeGreaterThan(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('returns descriptor-aware readback-unsupported without allocating staging resources', async () => {
|
|
72
|
+
const result = await openReplay(
|
|
73
|
+
tape([textureResource('texture:compressed', 'bc7-rgba-unorm', '2d')]),
|
|
74
|
+
await nullBackend(),
|
|
75
|
+
);
|
|
76
|
+
expect(result.ok).toBe(true);
|
|
77
|
+
if (!result.ok) return;
|
|
78
|
+
|
|
79
|
+
const readback = await result.value.readResource('texture:compressed');
|
|
80
|
+
expect(readback.ok).toBe(false);
|
|
81
|
+
if (!readback.ok && readback.error.code === 'readback-unsupported') {
|
|
82
|
+
expect(readback.error.code).toBe('readback-unsupported');
|
|
83
|
+
expect(readback.error.detail?.format).toBe('bc7-rgba-unorm');
|
|
84
|
+
expect(readback.error.detail?.resourceId).toBe('texture:compressed');
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('keeps texture subresource coordinates in the replay-owned request shape', () => {
|
|
89
|
+
const subresource: TextureSubresource = {
|
|
90
|
+
mipLevel: 1,
|
|
91
|
+
arrayLayer: 2,
|
|
92
|
+
aspect: 'stencil-only',
|
|
93
|
+
};
|
|
94
|
+
expect(subresource).toEqual({ mipLevel: 1, arrayLayer: 2, aspect: 'stencil-only' });
|
|
95
|
+
});
|
|
96
|
+
});
|