@forgeax/engine-rhi-debug 0.1.6 → 0.1.7

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.
Files changed (41) hide show
  1. package/README.md +39 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/readback-matrix-fixture.d.ts +32 -0
  4. package/dist/__tests__/readback-matrix-fixture.d.ts.map +1 -0
  5. package/dist/frame-model.d.ts +84 -52
  6. package/dist/frame-model.d.ts.map +1 -1
  7. package/dist/index.d.ts +3 -2
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.mjs +395 -40
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/replay/device-request.d.ts +8 -0
  12. package/dist/replay/device-request.d.ts.map +1 -0
  13. package/dist/replay/readback.d.ts +7 -0
  14. package/dist/replay/readback.d.ts.map +1 -1
  15. package/dist/replay/session.d.ts +17 -0
  16. package/dist/replay/session.d.ts.map +1 -1
  17. package/dist/texel-decode.d.ts +5 -4
  18. package/dist/texel-decode.d.ts.map +1 -1
  19. package/package.json +7 -7
  20. package/src/__tests__/consumer-inventory.unit.test.ts +49 -1
  21. package/src/__tests__/e2e.browser.test.ts +6 -1
  22. package/src/__tests__/frame-model-parity.test-d.ts +13 -1
  23. package/src/__tests__/frame-model-parity.unit.test.ts +262 -15
  24. package/src/__tests__/guard-gates.test.ts +176 -1
  25. package/src/__tests__/public-surface.integration.test.ts +8 -0
  26. package/src/__tests__/readback-format-matrix.dawn.test.ts +226 -3
  27. package/src/__tests__/readback-format-matrix.unit.test.ts +25 -1
  28. package/src/__tests__/readback-matrix-fixture.ts +20 -0
  29. package/src/__tests__/replay-fail-fast.unit.test.ts +12 -0
  30. package/src/__tests__/replay-session.dawn.test.ts +3 -0
  31. package/src/__tests__/replay-session.test-d.ts +7 -1
  32. package/src/__tests__/replay-session.unit.test.ts +16 -0
  33. package/src/__tests__/rhi-debug-fresh-replay.dawn.test.ts +2 -0
  34. package/src/__tests__/tape-index.unit.test.ts +3 -0
  35. package/src/__tests__/tree-shake.unit.test.ts +24 -0
  36. package/src/frame-model.ts +381 -83
  37. package/src/index.ts +17 -10
  38. package/src/replay/device-request.ts +38 -0
  39. package/src/replay/readback.ts +106 -18
  40. package/src/replay/session.ts +47 -2
  41. package/src/texel-decode.ts +37 -3
@@ -0,0 +1,8 @@
1
+ import type { Tape } from '../protocol/types';
2
+ /**
3
+ * Build the strongest fresh WebGPU device the current adapter can provide for
4
+ * a recorded tape. Optional features must be enabled, not merely advertised,
5
+ * and replay receives concrete adapter limits instead of lower device defaults.
6
+ */
7
+ export declare function replayDeviceRequest(tape: Tape, adapterFeatures: ReadonlySet<GPUFeatureName>, adapterLimits: Readonly<Record<string, number>>): GPUDeviceDescriptor;
8
+ //# sourceMappingURL=device-request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-request.d.ts","sourceRoot":"","sources":["../../src/replay/device-request.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAY9C;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,IAAI,EACV,eAAe,EAAE,WAAW,CAAC,cAAc,CAAC,EAC5C,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAC9C,mBAAmB,CAgBrB"}
@@ -13,6 +13,12 @@ export interface TextureReadbackSubresource {
13
13
  readonly aspect?: 'all' | 'depth-only' | 'stencil-only';
14
14
  }
15
15
  export type ReplayReadbackRequest = BufferReadbackRange | TextureReadbackSubresource;
16
+ export interface ReplayReadbackProvenance {
17
+ readonly generation: number;
18
+ readonly resourceId: string;
19
+ readonly subresource: ReplayReadbackRequest | null;
20
+ readonly selectedWorkIndex?: number;
21
+ }
16
22
  export interface ReplayReadbackResult {
17
23
  readonly resourceId: string;
18
24
  readonly kind: 'buffer' | 'texture';
@@ -20,6 +26,7 @@ export interface ReplayReadbackResult {
20
26
  readonly width?: number;
21
27
  readonly height?: number;
22
28
  readonly bytes: Uint8Array;
29
+ readonly provenance: ReplayReadbackProvenance;
23
30
  }
24
31
  export declare function readReplayResource(device: RhiDevice, table: ResourceTable, resourceId: string, subresource: ReplayReadbackRequest | undefined, createShaderModule: CreateShaderModuleFn): Promise<Result<ReplayReadbackResult, RhiDebugError>>;
25
32
  //# sourceMappingURL=readback.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"readback.d.ts","sourceRoot":"","sources":["../../src/replay/readback.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAIV,SAAS,EAEV,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AACpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAAsB,MAAM,aAAa,CAAC;AAWrE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,YAAY,GAAG,cAAc,CAAC;CACzD;AAED,MAAM,MAAM,qBAAqB,GAAG,mBAAmB,GAAG,0BAA0B,CAAC;AAErF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;CAC5B;AAED,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,aAAa,EACpB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,qBAAqB,GAAG,SAAS,EAC9C,kBAAkB,EAAE,oBAAoB,GACvC,OAAO,CAAC,MAAM,CAAC,oBAAoB,EAAE,aAAa,CAAC,CAAC,CAgBtD"}
1
+ {"version":3,"file":"readback.d.ts","sourceRoot":"","sources":["../../src/replay/readback.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAIV,SAAS,EAEV,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AACpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAAsB,MAAM,aAAa,CAAC;AAWrE,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,YAAY,GAAG,cAAc,CAAC;CACzD;AAED,MAAM,MAAM,qBAAqB,GAAG,mBAAmB,GAAG,0BAA0B,CAAC;AAErF,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACnD,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,wBAAwB,CAAC;CAC/C;AAID,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,aAAa,EACpB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,qBAAqB,GAAG,SAAS,EAC9C,kBAAkB,EAAE,oBAAoB,GACvC,OAAO,CAAC,MAAM,CAAC,oBAAoB,EAAE,aAAa,CAAC,CAAC,CAoCtD"}
@@ -1,6 +1,7 @@
1
1
  import type { RhiDevice } from '@forgeax/engine-rhi';
2
2
  import { type Result } from '@forgeax/engine-types';
3
3
  import { type RhiDebugError } from '../errors';
4
+ import { type WorkBinding, type WorkPipeline } from '../frame-model';
4
5
  import type { Tape } from '../protocol/types';
5
6
  import type { CreateShaderModuleFn } from '../recorder';
6
7
  import { type BufferReadbackRange, type ReplayReadbackResult } from './readback';
@@ -20,6 +21,22 @@ export interface WorkInspection {
20
21
  readonly eventIndex: number;
21
22
  readonly passIndex: number;
22
23
  readonly attachment: ReplayReadbackResult | undefined;
24
+ readonly pipeline?: WorkPipeline;
25
+ readonly bindings?: readonly WorkBinding[];
26
+ readonly vertexBuffers?: readonly {
27
+ readonly slot: number;
28
+ readonly bufferHandleId: string;
29
+ readonly offset: number;
30
+ readonly size: number | null;
31
+ }[];
32
+ readonly indexBuffer?: {
33
+ readonly bufferHandleId: string;
34
+ readonly format: string;
35
+ readonly offset: number;
36
+ readonly size: number | null;
37
+ } | null;
38
+ readonly shaders?: WorkPipeline['shaders'];
39
+ readonly resourceIds?: readonly string[];
23
40
  }
24
41
  export interface ReplaySession {
25
42
  readonly generation: number;
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/replay/session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,SAAS,EAEV,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAEpE,OAAO,KAAK,EAAmC,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAC/E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAGxD,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EAE1B,MAAM,YAAY,CAAC;AAGpB,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,kBAAkB,EAAE,oBAAoB,CAAC;CACnD;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,YAAY,GAAG,cAAc,CAAC;CACzD;AAED,MAAM,MAAM,mBAAmB,GAAG,kBAAkB,GAAG,mBAAmB,CAAC;AAE3E,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,UAAU,GAAG,QAAQ,CAAC;AAE9D,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,oBAAoB,GAAG,SAAS,CAAC;CACvD;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,WAAW,CACT,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,SAAS,YAAY,EAAE,EAChC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,MAAM,CAAC,cAAc,EAAE,aAAa,CAAC,CAAC,CAAC;IAClD,YAAY,CACV,UAAU,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,mBAAmB,EACjC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,MAAM,CAAC,oBAAoB,EAAE,aAAa,CAAC,CAAC,CAAC;IACxD,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC;CACjD;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC,CA+G/C;AAoSD,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/replay/session.ts"],"names":[],"mappings":"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"}
@@ -1,16 +1,17 @@
1
1
  /** Decode a single IEEE half-float (16-bit) bit pattern to a JS number. */
2
2
  export declare function halfToFloat(h: number): number;
3
3
  /**
4
- * Decode tight raw GPU bytes of an uncompressed color texture into RGBA8 the
5
- * canvas can paint via putImageData. Returns null when the format has no
6
- * {@link formatInfo} entry (compressed / depth / unknown -> caller falls back).
4
+ * Decode tight raw GPU bytes of a supported color or depth/stencil plane into
5
+ * RGBA8 the canvas can paint via putImageData. Returns null when the format has
6
+ * no {@link formatInfo} entry or the requested aspect is invalid.
7
7
  *
8
8
  * @param bytes - Tight readback bytes (no row padding), length = w*h*bytesPerTexel.
9
9
  * @param format - The texture's real format string.
10
10
  * @param width - Texture width in pixels.
11
11
  * @param height - Texture height in pixels.
12
+ * @param aspect - Explicit plane selected by readback for depth/stencil formats.
12
13
  */
13
- export declare function decodeToRgba8(bytes: Uint8Array, format: string, width: number, height: number): Uint8ClampedArray<ArrayBuffer> | null;
14
+ export declare function decodeToRgba8(bytes: Uint8Array, format: string, width: number, height: number, aspect?: 'all' | 'depth-only' | 'stencil-only'): Uint8ClampedArray<ArrayBuffer> | null;
14
15
  /**
15
16
  * Decode a single texel from tight raw GPU bytes into raw float RGBA values
16
17
  * without the display clamp (D-4: raw byte bypass for HDR fidelity).
@@ -1 +1 @@
1
- {"version":3,"file":"texel-decode.d.ts","sourceRoot":"","sources":["../src/texel-decode.ts"],"names":[],"mappings":"AAqBA,2EAA2E;AAC3E,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAe7C;AA8GD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,iBAAiB,CAAC,WAAW,CAAC,GAAG,IAAI,CAoCvC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CA4BzC"}
1
+ {"version":3,"file":"texel-decode.d.ts","sourceRoot":"","sources":["../src/texel-decode.ts"],"names":[],"mappings":"AAqBA,2EAA2E;AAC3E,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAe7C;AA8GD;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,KAAK,GAAG,YAAY,GAAG,cAAsB,GACpD,iBAAiB,CAAC,WAAW,CAAC,GAAG,IAAI,CAuCvC;AA+BD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CA4BzC"}
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@forgeax/engine-rhi-debug",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
7
7
  "sideEffects": false,
8
- "description": "Single-file v7 RHI tape capture, deterministic fresh-backend replay, and structured offline inspection for forgeax-engine.",
8
+ "description": "Single-file v7 RHI tape capture, FrameModel projection, deterministic fresh-backend replay, and structured AI-readable inspection for forgeax-engine.",
9
9
  "exports": {
10
10
  ".": {
11
11
  "types": "./dist/index.d.ts",
@@ -26,19 +26,19 @@
26
26
  "LICENSE"
27
27
  ],
28
28
  "dependencies": {
29
- "@forgeax/engine-rhi": "0.1.6",
30
- "@forgeax/engine-types": "0.1.6",
29
+ "@forgeax/engine-rhi": "0.1.7",
30
+ "@forgeax/engine-types": "0.1.7",
31
31
  "@noble/hashes": "^2.2.0",
32
32
  "pako": "^1.0.11"
33
33
  },
34
34
  "devDependencies": {
35
- "@forgeax/engine-rhi-null": "0.1.6",
35
+ "@forgeax/engine-rhi-null": "0.1.7",
36
36
  "@types/node": "^20.14.0",
37
37
  "@webgpu/types": "^0.1.71"
38
38
  },
39
39
  "peerDependencies": {
40
- "@forgeax/engine-rhi-webgpu": "0.1.6",
41
- "@forgeax/engine-rhi-wgpu": "0.1.6"
40
+ "@forgeax/engine-rhi-webgpu": "0.1.7",
41
+ "@forgeax/engine-rhi-wgpu": "0.1.7"
42
42
  },
43
43
  "forgeax": {
44
44
  "metrics": {
@@ -1,6 +1,6 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  // @perf-budget-skip: intentional repository-wide inventory subprocess gate.
3
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join, resolve } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
@@ -8,6 +8,17 @@ import { describe, expect, it } from 'vitest';
8
8
 
9
9
  const repoRoot = resolve(fileURLToPath(new URL('../../../../', import.meta.url)));
10
10
  const inventoryScript = join(repoRoot, 'apps/shared/scripts/rhi-debug-consumer-inventory.mjs');
11
+ const oosInventoryScript = join(repoRoot, 'apps/shared/scripts/rhi-debug-oos-inventory.mjs');
12
+
13
+ const deletedFieldConsumers = [
14
+ 'apps/shared/scripts/rhi-debug-browser-admission.mjs',
15
+ 'apps/shared/scripts/rhi-debug-verify.mjs',
16
+ 'apps/learn-render/4.advanced-opengl/5.framebuffers/scripts/smoke-browser-live.mjs',
17
+ 'apps/rhi-debug-viewer/src/viewer-model.ts',
18
+ 'apps/rhi-debug-viewer/scripts/smoke-browser.mjs',
19
+ 'apps/rhi-debug-viewer/scripts/smoke-browser-no-webgpu.mjs',
20
+ 'packages/rhi-debug/src/__tests__/e2e.browser.test.ts',
21
+ ];
11
22
 
12
23
  type ChannelReport = {
13
24
  totalMatches: number;
@@ -40,6 +51,33 @@ function runInventory(root: string): { status: number | null; report: InventoryR
40
51
  }
41
52
 
42
53
  describe('rhi-debug consumer inventory', () => {
54
+ it('reports all nine OOS boundaries and retained owners as machine-readable pass rows', () => {
55
+ const result = spawnSync(process.execPath, [oosInventoryScript, '--json', '--assert-pass'], {
56
+ cwd: repoRoot,
57
+ encoding: 'utf8',
58
+ });
59
+ expect(result.status).toBe(0);
60
+ const report = JSON.parse(result.stdout) as {
61
+ status: string;
62
+ oos: readonly { id: string; status: string }[];
63
+ retainedCore: readonly { present: boolean }[];
64
+ };
65
+ expect(report.status).toBe('pass');
66
+ expect(report.oos.map((entry) => entry.id)).toEqual([
67
+ 'OOS-1',
68
+ 'OOS-2',
69
+ 'OOS-3',
70
+ 'OOS-4',
71
+ 'OOS-5',
72
+ 'OOS-6',
73
+ 'OOS-7',
74
+ 'OOS-8',
75
+ 'OOS-9',
76
+ ]);
77
+ expect(report.oos.every((entry) => entry.status === 'pass')).toBe(true);
78
+ expect(report.retainedCore.every((entry) => entry.present)).toBe(true);
79
+ });
80
+
43
81
  it('enumerates TS, erased-script, and JSON/schema consumer channels', () => {
44
82
  const fixture = mkdtempSync(join(tmpdir(), 'forgeax-rhi-debug-consumers-'));
45
83
  try {
@@ -103,4 +141,14 @@ describe('rhi-debug consumer inventory', () => {
103
141
  ),
104
142
  );
105
143
  });
144
+
145
+ it('finds no deleted FrameModel fields across the three consumer channels', () => {
146
+ const deleted =
147
+ /(?:\bmodel\.(?:tree|draws|meta|resourceTable|resourceEntries|totalDraws)|buildTapeIndex)/;
148
+ const matches = deletedFieldConsumers.flatMap((relativePath) => {
149
+ const source = readFileSync(join(repoRoot, relativePath), 'utf8');
150
+ return deleted.test(source) ? [relativePath] : [];
151
+ });
152
+ expect(matches).toEqual([]);
153
+ });
106
154
  });
@@ -180,7 +180,9 @@ describe.skipIf(!GPU_AVAILABLE)('M4 browser capture and replay gate', () => {
180
180
  const model = buildFrameModel(captured.tape);
181
181
  expect(captured.tape.header.formatVersion).toBe(7);
182
182
  expect(captured.tape.events.length).toBeGreaterThan(0);
183
- expect(model.meta.totalDraws).toBeGreaterThan(0);
183
+ expect(model.works.some((work) => work.kind === 'draw' || work.kind === 'drawIndexed')).toBe(
184
+ true,
185
+ );
184
186
  expect(captured.tape.events.some((event) => event.kind === 'setBindGroup')).toBe(true);
185
187
  expect(model.works.length).toBeGreaterThan(0);
186
188
 
@@ -199,6 +201,9 @@ describe.skipIf(!GPU_AVAILABLE)('M4 browser capture and replay gate', () => {
199
201
  expect(work.attachment?.kind).toBe('texture');
200
202
  expect(work.attachment?.width).toBe(SIZE);
201
203
  expect(work.attachment?.height).toBe(SIZE);
204
+ expect(work.attachment?.provenance.selectedWorkIndex).toBe(0);
205
+ expect(work.attachment?.provenance.resourceId).toBeDefined();
206
+ expect(work.attachment?.provenance.subresource).toBeNull();
202
207
  expect(hasVisibleRgb(work.attachment?.bytes ?? new Uint8Array())).toBe(true);
203
208
  await session.dispose();
204
209
  }, 60_000);
@@ -1,7 +1,19 @@
1
1
  import { expectTypeOf, it } from 'vitest';
2
2
  import type { FrameModel } from '../frame-model';
3
3
 
4
- it('exposes stable workIndex fields', () => {
4
+ type HasKey<T, K extends PropertyKey> = K extends keyof T ? true : false;
5
+ type IsReadonlyMap<T> = T extends ReadonlyMap<unknown, unknown> ? true : false;
6
+
7
+ it('exposes only the canonical JSON-safe model shape', () => {
8
+ expectTypeOf<HasKey<FrameModel, 'commands'>>().toEqualTypeOf<true>();
9
+ expectTypeOf<HasKey<FrameModel, 'passes'>>().toEqualTypeOf<true>();
10
+ expectTypeOf<HasKey<FrameModel, 'resources'>>().toEqualTypeOf<true>();
11
+ expectTypeOf<HasKey<FrameModel, 'works'>>().toEqualTypeOf<true>();
12
+ expectTypeOf<HasKey<FrameModel, 'tree'>>().toEqualTypeOf<false>();
13
+ expectTypeOf<HasKey<FrameModel, 'draws'>>().toEqualTypeOf<false>();
14
+ expectTypeOf<HasKey<FrameModel, 'meta'>>().toEqualTypeOf<false>();
15
+ expectTypeOf<HasKey<FrameModel, 'resourceLifecycle'>>().toEqualTypeOf<true>();
16
+ expectTypeOf<IsReadonlyMap<FrameModel['resources']>>().toEqualTypeOf<false>();
5
17
  expectTypeOf<FrameModel['works'][number]['workIndex']>().toEqualTypeOf<number>();
6
18
  expectTypeOf<FrameModel['works'][number]['eventIndex']>().toEqualTypeOf<number>();
7
19
  expectTypeOf<FrameModel['works'][number]['passIndex']>().toEqualTypeOf<number>();
@@ -2,37 +2,284 @@ import { describe, expect, it } from 'vitest';
2
2
  import { buildFrameModel } from '../frame-model';
3
3
  import type { Tape } from '../protocol/types';
4
4
 
5
- describe('FrameModel parity', () => {
6
- it('is JSON-safe and derives work entries from the same index', () => {
5
+ function makeTape(): Tape {
6
+ return {
7
+ header: { formatVersion: 7, rhiCaps: {}, eventCount: 15, blobCount: 0 },
8
+ bootstrap: [
9
+ {
10
+ handleId: 'buffer-1',
11
+ kind: 'buffer',
12
+ create: { kind: 'createBuffer', handleId: 'buffer-1', desc: { size: 64, usage: 4 } },
13
+ initialData: [],
14
+ },
15
+ {
16
+ handleId: 'shader:1',
17
+ kind: 'shader-module',
18
+ create: {
19
+ kind: 'createShaderModule',
20
+ handleId: 'shader:1',
21
+ wgslCode: '@vertex fn vs_main() -> @builtin(position) vec4f { return vec4f(0.0); }',
22
+ },
23
+ initialData: [],
24
+ },
25
+ ],
26
+ events: [
27
+ {
28
+ kind: 'beginRenderPass',
29
+ passHandleId: 'pass:render',
30
+ cmdHandleId: 'encoder:1',
31
+ desc: { colorAttachments: [] },
32
+ colorAttachmentViewHandleIds: [],
33
+ },
34
+ { kind: 'pushDebugGroup', cmdHandleId: 'encoder:1', groupLabel: 'render group' },
35
+ { kind: 'passPushDebugGroup', passHandleId: 'pass:render', groupLabel: 'nested group' },
36
+ { kind: 'insertDebugMarker', cmdHandleId: 'encoder:1', markerLabel: 'before draws' },
37
+ {
38
+ kind: 'draw',
39
+ passHandleId: 'pass:render',
40
+ vertexCount: 3,
41
+ instanceCount: 1,
42
+ firstVertex: 0,
43
+ firstInstance: 0,
44
+ },
45
+ {
46
+ kind: 'drawIndexed',
47
+ passHandleId: 'pass:render',
48
+ indexCount: 3,
49
+ instanceCount: 1,
50
+ firstIndex: 0,
51
+ baseVertex: 0,
52
+ firstInstance: 0,
53
+ },
54
+ {
55
+ kind: 'drawIndirect',
56
+ passHandleId: 'pass:render',
57
+ indirectBufferHandleId: 'buffer-1',
58
+ indirectOffset: 0,
59
+ },
60
+ {
61
+ kind: 'drawIndexedIndirect',
62
+ passHandleId: 'pass:render',
63
+ indirectBufferHandleId: 'buffer-1',
64
+ indirectOffset: 16,
65
+ },
66
+ { kind: 'passPopDebugGroup', passHandleId: 'pass:render' },
67
+ { kind: 'popDebugGroup', cmdHandleId: 'encoder:1' },
68
+ { kind: 'endRenderPass', passHandleId: 'pass:render' },
69
+ {
70
+ kind: 'beginComputePass',
71
+ passHandleId: 'pass:compute',
72
+ cmdHandleId: 'encoder:1',
73
+ desc: {},
74
+ },
75
+ { kind: 'dispatchWorkgroups', passHandleId: 'pass:compute', x: 2, y: 3, z: 1 },
76
+ {
77
+ kind: 'dispatchWorkgroupsIndirect',
78
+ passHandleId: 'pass:compute',
79
+ indirectBufferHandleId: 'buffer-1',
80
+ indirectOffset: 32,
81
+ },
82
+ { kind: 'endComputePass', passHandleId: 'pass:compute' },
83
+ ],
84
+ blobs: [],
85
+ };
86
+ }
87
+
88
+ describe('FrameModel canonical projection', () => {
89
+ it('exposes one JSON-safe projection with six globally indexed work kinds', () => {
90
+ const model = buildFrameModel(makeTape());
91
+ const roundTrip = JSON.parse(JSON.stringify(model)) as typeof model;
92
+
93
+ expect(roundTrip).toEqual(model);
94
+ expect(Object.keys(model)).toEqual([
95
+ 'commands',
96
+ 'passes',
97
+ 'resources',
98
+ 'resourceLifecycle',
99
+ 'works',
100
+ ]);
101
+ expect(
102
+ model.works.map((work) => [work.workIndex, work.eventIndex, work.passIndex, work.kind]),
103
+ ).toEqual([
104
+ [0, 4, 0, 'draw'],
105
+ [1, 5, 0, 'drawIndexed'],
106
+ [2, 6, 0, 'drawIndirect'],
107
+ [3, 7, 0, 'drawIndexedIndirect'],
108
+ [4, 12, 1, 'dispatchWorkgroups'],
109
+ [5, 13, 1, 'dispatchWorkgroupsIndirect'],
110
+ ]);
111
+ expect(model.commands[1]).toMatchObject({
112
+ eventIndex: 1,
113
+ passIndex: 0,
114
+ group: ['render group'],
115
+ });
116
+ expect(model.commands[3]).toMatchObject({ eventIndex: 3, marker: 'before draws' });
117
+ expect(model.passes.map((pass) => pass.workIndices)).toEqual([
118
+ [0, 1, 2, 3],
119
+ [4, 5],
120
+ ]);
121
+ expect(model.resources).toEqual(
122
+ expect.arrayContaining([
123
+ expect.objectContaining({ resourceId: 'buffer-1', kind: 'buffer' }),
124
+ expect.objectContaining({ resourceId: 'shader:1', kind: 'shader-module' }),
125
+ ]),
126
+ );
127
+ expect(model.works.every((work) => work.eventIndex >= 0 && work.passIndex >= 0)).toBe(true);
128
+ });
129
+
130
+ it('fails closed instead of exposing empty legacy projections or Map values', () => {
131
+ const model = buildFrameModel(makeTape()) as unknown as Record<string, unknown>;
132
+ for (const field of ['tree', 'draws', 'meta']) {
133
+ expect(model, `legacy field ${field} must be absent`).not.toHaveProperty(field);
134
+ }
135
+ expect(model).not.toHaveProperty('totalDraws');
136
+ expect(model.resources).toBeInstanceOf(Array);
137
+ expect(JSON.stringify(model)).not.toContain('ReadonlyMap');
138
+ });
139
+
140
+ it('resolves pipeline, shader, and bind-group facts from the bootstrap closure', () => {
7
141
  const tape: Tape = {
8
- header: { formatVersion: 7, rhiCaps: {}, eventCount: 2, blobCount: 0 },
9
- bootstrap: [],
142
+ header: { formatVersion: 7, rhiCaps: {}, eventCount: 5, blobCount: 0 },
143
+ bootstrap: [
144
+ {
145
+ handleId: 'texture:sample',
146
+ kind: 'texture',
147
+ create: {
148
+ kind: 'createTexture',
149
+ handleId: 'texture:sample',
150
+ desc: { size: [2, 2, 1], format: 'rgba8unorm', usage: 4 },
151
+ },
152
+ initialData: [],
153
+ },
154
+ {
155
+ handleId: 'view:sample',
156
+ kind: 'texture-view',
157
+ create: {
158
+ kind: 'createTextureView',
159
+ sourceHandleId: 'texture:sample',
160
+ resultHandleId: 'view:sample',
161
+ desc: {},
162
+ },
163
+ initialData: [],
164
+ },
165
+ {
166
+ handleId: 'shader:vertex',
167
+ kind: 'shader-module',
168
+ create: {
169
+ kind: 'createShaderModule',
170
+ handleId: 'shader:vertex',
171
+ wgslCode: '@vertex fn main() -> @builtin(position) vec4f { return vec4f(); }',
172
+ },
173
+ initialData: [],
174
+ },
175
+ {
176
+ handleId: 'shader:fragment',
177
+ kind: 'shader-module',
178
+ create: {
179
+ kind: 'createShaderModule',
180
+ handleId: 'shader:fragment',
181
+ wgslCode: '@fragment fn main() -> @location(0) vec4f { return vec4f(1.0); }',
182
+ },
183
+ initialData: [],
184
+ },
185
+ {
186
+ handleId: 'bgl:main',
187
+ kind: 'binding',
188
+ create: {
189
+ kind: 'createBindGroupLayout',
190
+ handleId: 'bgl:main',
191
+ desc: { entries: [] },
192
+ },
193
+ initialData: [],
194
+ },
195
+ {
196
+ handleId: 'bindGroup:main',
197
+ kind: 'binding',
198
+ create: {
199
+ kind: 'createBindGroup',
200
+ handleId: 'bindGroup:main',
201
+ layoutHandleId: 'bgl:main',
202
+ entries: [{ binding: 3, resourceKind: 'textureView' }],
203
+ resourceHandleIds: ['view:sample'],
204
+ },
205
+ initialData: [],
206
+ },
207
+ {
208
+ handleId: 'pipelineLayout:main',
209
+ kind: 'binding',
210
+ create: {
211
+ kind: 'createPipelineLayout',
212
+ handleId: 'pipelineLayout:main',
213
+ bglHandleIds: ['bgl:main'],
214
+ },
215
+ initialData: [],
216
+ },
217
+ {
218
+ handleId: 'pipeline:main',
219
+ kind: 'pipeline',
220
+ create: {
221
+ kind: 'createRenderPipeline',
222
+ handleId: 'pipeline:main',
223
+ desc: {
224
+ vertex: { entryPoint: 'main', buffers: [] },
225
+ fragment: { entryPoint: 'main', targets: [{ format: 'rgba8unorm' }] },
226
+ },
227
+ layoutHandleId: 'pipelineLayout:main',
228
+ vertexShaderModuleHandleId: 'shader:vertex',
229
+ fragmentShaderModuleHandleId: 'shader:fragment',
230
+ },
231
+ initialData: [],
232
+ },
233
+ ],
10
234
  events: [
11
235
  {
12
236
  kind: 'beginRenderPass',
13
- passHandleId: 'pass:1',
14
- cmdHandleId: 'cmd:1',
237
+ passHandleId: 'pass:render',
238
+ cmdHandleId: 'encoder:1',
15
239
  desc: { colorAttachments: [] },
16
- colorAttachmentViewHandleIds: [],
240
+ colorAttachmentViewHandleIds: ['view:sample'],
241
+ },
242
+ {
243
+ kind: 'setPipeline',
244
+ passHandleId: 'pass:render',
245
+ pipelineHandleId: 'pipeline:main',
246
+ },
247
+ {
248
+ kind: 'setBindGroup',
249
+ passHandleId: 'pass:render',
250
+ index: 0,
251
+ bindGroupHandleId: 'bindGroup:main',
17
252
  },
18
253
  {
19
254
  kind: 'draw',
20
- passHandleId: 'pass:1',
255
+ passHandleId: 'pass:render',
21
256
  vertexCount: 3,
22
257
  instanceCount: 1,
23
258
  firstVertex: 0,
24
259
  firstInstance: 0,
25
260
  },
261
+ { kind: 'endRenderPass', passHandleId: 'pass:render' },
26
262
  ],
27
263
  blobs: [],
28
264
  };
29
- const model = buildFrameModel(tape);
30
- expect(model.works[0]).toMatchObject({
31
- workIndex: 0,
32
- eventIndex: 1,
33
- passIndex: 0,
34
- kind: 'draw',
265
+
266
+ const work = buildFrameModel(tape).works[0];
267
+ expect(work?.pipeline).toMatchObject({
268
+ status: 'available',
269
+ pipelineHandleId: 'pipeline:main',
35
270
  });
36
- expect(JSON.stringify(model)).not.toContain('Map');
271
+ expect(work?.pipeline.shaders.map((shader) => shader.moduleHandleId)).toEqual([
272
+ 'shader:vertex',
273
+ 'shader:fragment',
274
+ ]);
275
+ expect(work?.bindings).toEqual([
276
+ expect.objectContaining({
277
+ groupIndex: 0,
278
+ binding: 3,
279
+ bindGroupId: 'bindGroup:main',
280
+ resourceId: 'view:sample',
281
+ resourceKind: 'textureView',
282
+ }),
283
+ ]);
37
284
  });
38
285
  });