@forgeax/engine-rhi-debug 0.0.0-dev.8d955ade1c79

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 (237) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +178 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/bootstrap-integration.test.d.ts +2 -0
  5. package/dist/__tests__/bootstrap-integration.test.d.ts.map +1 -0
  6. package/dist/__tests__/consumer-inventory.unit.test.d.ts +2 -0
  7. package/dist/__tests__/consumer-inventory.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/copy-src-usage-validity.dawn.test.d.ts +15 -0
  9. package/dist/__tests__/copy-src-usage-validity.dawn.test.d.ts.map +1 -0
  10. package/dist/__tests__/coverage-invariant.test-d.d.ts +2 -0
  11. package/dist/__tests__/coverage-invariant.test-d.d.ts.map +1 -0
  12. package/dist/__tests__/coverage-invariant.unit.test.d.ts +2 -0
  13. package/dist/__tests__/coverage-invariant.unit.test.d.ts.map +1 -0
  14. package/dist/__tests__/e2e.browser.test.d.ts +2 -0
  15. package/dist/__tests__/e2e.browser.test.d.ts.map +1 -0
  16. package/dist/__tests__/error-code-narrowing.test-d.d.ts +2 -0
  17. package/dist/__tests__/error-code-narrowing.test-d.d.ts.map +1 -0
  18. package/dist/__tests__/errors-v7.test-d.d.ts +2 -0
  19. package/dist/__tests__/errors-v7.test-d.d.ts.map +1 -0
  20. package/dist/__tests__/errors-v7.unit.test.d.ts +2 -0
  21. package/dist/__tests__/errors-v7.unit.test.d.ts.map +1 -0
  22. package/dist/__tests__/errors.unit.test.d.ts +2 -0
  23. package/dist/__tests__/errors.unit.test.d.ts.map +1 -0
  24. package/dist/__tests__/event-semantics-coverage.unit.test.d.ts +2 -0
  25. package/dist/__tests__/event-semantics-coverage.unit.test.d.ts.map +1 -0
  26. package/dist/__tests__/format-info.unit.test.d.ts +2 -0
  27. package/dist/__tests__/format-info.unit.test.d.ts.map +1 -0
  28. package/dist/__tests__/frame-model-parity.test-d.d.ts +2 -0
  29. package/dist/__tests__/frame-model-parity.test-d.d.ts.map +1 -0
  30. package/dist/__tests__/frame-model-parity.unit.test.d.ts +2 -0
  31. package/dist/__tests__/frame-model-parity.unit.test.d.ts.map +1 -0
  32. package/dist/__tests__/guard-gates.test.d.ts +2 -0
  33. package/dist/__tests__/guard-gates.test.d.ts.map +1 -0
  34. package/dist/__tests__/public-surface.integration.test.d.ts +2 -0
  35. package/dist/__tests__/public-surface.integration.test.d.ts.map +1 -0
  36. package/dist/__tests__/readback-fault-cleanup.unit.test.d.ts +2 -0
  37. package/dist/__tests__/readback-fault-cleanup.unit.test.d.ts.map +1 -0
  38. package/dist/__tests__/readback-format-matrix.dawn.test.d.ts +2 -0
  39. package/dist/__tests__/readback-format-matrix.dawn.test.d.ts.map +1 -0
  40. package/dist/__tests__/readback-format-matrix.unit.test.d.ts +2 -0
  41. package/dist/__tests__/readback-format-matrix.unit.test.d.ts.map +1 -0
  42. package/dist/__tests__/readback-matrix-fixture.d.ts +32 -0
  43. package/dist/__tests__/readback-matrix-fixture.d.ts.map +1 -0
  44. package/dist/__tests__/recorder-bootstrap.unit.test.d.ts +2 -0
  45. package/dist/__tests__/recorder-bootstrap.unit.test.d.ts.map +1 -0
  46. package/dist/__tests__/recorder-budget.unit.test.d.ts +2 -0
  47. package/dist/__tests__/recorder-budget.unit.test.d.ts.map +1 -0
  48. package/dist/__tests__/recorder-fail-fast.unit.test.d.ts +2 -0
  49. package/dist/__tests__/recorder-fail-fast.unit.test.d.ts.map +1 -0
  50. package/dist/__tests__/recorder-lifecycle.unit.test.d.ts +2 -0
  51. package/dist/__tests__/recorder-lifecycle.unit.test.d.ts.map +1 -0
  52. package/dist/__tests__/recorder-push-event.unit.test.d.ts +2 -0
  53. package/dist/__tests__/recorder-push-event.unit.test.d.ts.map +1 -0
  54. package/dist/__tests__/recorder-session.integration.test.d.ts +2 -0
  55. package/dist/__tests__/recorder-session.integration.test.d.ts.map +1 -0
  56. package/dist/__tests__/recorder-session.unit.test.d.ts +2 -0
  57. package/dist/__tests__/recorder-session.unit.test.d.ts.map +1 -0
  58. package/dist/__tests__/recorder-steady-frame.dawn.test.d.ts +2 -0
  59. package/dist/__tests__/recorder-steady-frame.dawn.test.d.ts.map +1 -0
  60. package/dist/__tests__/replay-fail-fast.unit.test.d.ts +2 -0
  61. package/dist/__tests__/replay-fail-fast.unit.test.d.ts.map +1 -0
  62. package/dist/__tests__/replay-session.dawn.test.d.ts +2 -0
  63. package/dist/__tests__/replay-session.dawn.test.d.ts.map +1 -0
  64. package/dist/__tests__/replay-session.test-d.d.ts +2 -0
  65. package/dist/__tests__/replay-session.test-d.d.ts.map +1 -0
  66. package/dist/__tests__/replay-session.unit.test.d.ts +2 -0
  67. package/dist/__tests__/replay-session.unit.test.d.ts.map +1 -0
  68. package/dist/__tests__/resolve-texture-descriptor.unit.test.d.ts +2 -0
  69. package/dist/__tests__/resolve-texture-descriptor.unit.test.d.ts.map +1 -0
  70. package/dist/__tests__/resource-lifecycle.unit.test.d.ts +2 -0
  71. package/dist/__tests__/resource-lifecycle.unit.test.d.ts.map +1 -0
  72. package/dist/__tests__/rhi-debug-fresh-replay.dawn.test.d.ts +2 -0
  73. package/dist/__tests__/rhi-debug-fresh-replay.dawn.test.d.ts.map +1 -0
  74. package/dist/__tests__/tape-index.unit.test.d.ts +2 -0
  75. package/dist/__tests__/tape-index.unit.test.d.ts.map +1 -0
  76. package/dist/__tests__/tape-v7-invalid.unit.test.d.ts +2 -0
  77. package/dist/__tests__/tape-v7-invalid.unit.test.d.ts.map +1 -0
  78. package/dist/__tests__/tape-v7-roundtrip.unit.test.d.ts +2 -0
  79. package/dist/__tests__/tape-v7-roundtrip.unit.test.d.ts.map +1 -0
  80. package/dist/__tests__/tape-v7-version.unit.test.d.ts +2 -0
  81. package/dist/__tests__/tape-v7-version.unit.test.d.ts.map +1 -0
  82. package/dist/__tests__/tree-shake.unit.test.d.ts +2 -0
  83. package/dist/__tests__/tree-shake.unit.test.d.ts.map +1 -0
  84. package/dist/__tests__/types.test.d.ts +2 -0
  85. package/dist/__tests__/types.test.d.ts.map +1 -0
  86. package/dist/browser.d.ts +45 -0
  87. package/dist/browser.d.ts.map +1 -0
  88. package/dist/browser.mjs +83 -0
  89. package/dist/browser.mjs.map +1 -0
  90. package/dist/errors.d.ts +85 -0
  91. package/dist/errors.d.ts.map +1 -0
  92. package/dist/frame-model.d.ts +158 -0
  93. package/dist/frame-model.d.ts.map +1 -0
  94. package/dist/index.d.ts +17 -0
  95. package/dist/index.d.ts.map +1 -0
  96. package/dist/index.mjs +10415 -0
  97. package/dist/index.mjs.map +1 -0
  98. package/dist/protocol/codec.d.ts +6 -0
  99. package/dist/protocol/codec.d.ts.map +1 -0
  100. package/dist/protocol/event-semantics.d.ts +15 -0
  101. package/dist/protocol/event-semantics.d.ts.map +1 -0
  102. package/dist/protocol/tape-index.d.ts +30 -0
  103. package/dist/protocol/tape-index.d.ts.map +1 -0
  104. package/dist/protocol/types.d.ts +44 -0
  105. package/dist/protocol/types.d.ts.map +1 -0
  106. package/dist/protocol/validation.d.ts +7 -0
  107. package/dist/protocol/validation.d.ts.map +1 -0
  108. package/dist/readback.d.ts +156 -0
  109. package/dist/readback.d.ts.map +1 -0
  110. package/dist/recorder/assemble.d.ts +11 -0
  111. package/dist/recorder/assemble.d.ts.map +1 -0
  112. package/dist/recorder/closure.d.ts +39 -0
  113. package/dist/recorder/closure.d.ts.map +1 -0
  114. package/dist/recorder/core.d.ts +296 -0
  115. package/dist/recorder/core.d.ts.map +1 -0
  116. package/dist/recorder/device.d.ts +4 -0
  117. package/dist/recorder/device.d.ts.map +1 -0
  118. package/dist/recorder/encoder.d.ts +5 -0
  119. package/dist/recorder/encoder.d.ts.map +1 -0
  120. package/dist/recorder/lifecycle.d.ts +20 -0
  121. package/dist/recorder/lifecycle.d.ts.map +1 -0
  122. package/dist/recorder/pass.d.ts +6 -0
  123. package/dist/recorder/pass.d.ts.map +1 -0
  124. package/dist/recorder/proxy.d.ts +19 -0
  125. package/dist/recorder/proxy.d.ts.map +1 -0
  126. package/dist/recorder/queue.d.ts +4 -0
  127. package/dist/recorder/queue.d.ts.map +1 -0
  128. package/dist/recorder/resource-registry.d.ts +15 -0
  129. package/dist/recorder/resource-registry.d.ts.map +1 -0
  130. package/dist/recorder/session.d.ts +25 -0
  131. package/dist/recorder/session.d.ts.map +1 -0
  132. package/dist/recorder/shader.d.ts +3 -0
  133. package/dist/recorder/shader.d.ts.map +1 -0
  134. package/dist/recorder/snapshot.d.ts +10 -0
  135. package/dist/recorder/snapshot.d.ts.map +1 -0
  136. package/dist/recorder/wrap.d.ts +4 -0
  137. package/dist/recorder/wrap.d.ts.map +1 -0
  138. package/dist/recorder.d.ts +7 -0
  139. package/dist/recorder.d.ts.map +1 -0
  140. package/dist/replay/device-request.d.ts +8 -0
  141. package/dist/replay/device-request.d.ts.map +1 -0
  142. package/dist/replay/execute-support.d.ts +33 -0
  143. package/dist/replay/execute-support.d.ts.map +1 -0
  144. package/dist/replay/execute.d.ts +18 -0
  145. package/dist/replay/execute.d.ts.map +1 -0
  146. package/dist/replay/readback.d.ts +32 -0
  147. package/dist/replay/readback.d.ts.map +1 -0
  148. package/dist/replay/resources.d.ts +57 -0
  149. package/dist/replay/resources.d.ts.map +1 -0
  150. package/dist/replay/session.d.ts +49 -0
  151. package/dist/replay/session.d.ts.map +1 -0
  152. package/dist/replay/texture-format.d.ts +21 -0
  153. package/dist/replay/texture-format.d.ts.map +1 -0
  154. package/dist/texel-decode.d.ts +35 -0
  155. package/dist/texel-decode.d.ts.map +1 -0
  156. package/dist/texel-layout.d.ts +76 -0
  157. package/dist/texel-layout.d.ts.map +1 -0
  158. package/dist/types.d.ts +722 -0
  159. package/dist/types.d.ts.map +1 -0
  160. package/package.json +72 -0
  161. package/src/__tests__/bootstrap-integration.test.ts +268 -0
  162. package/src/__tests__/consumer-inventory.unit.test.ts +154 -0
  163. package/src/__tests__/copy-src-usage-validity.dawn.test.ts +117 -0
  164. package/src/__tests__/coverage-invariant.test-d.ts +115 -0
  165. package/src/__tests__/coverage-invariant.unit.test.ts +45 -0
  166. package/src/__tests__/e2e.browser.test.ts +239 -0
  167. package/src/__tests__/error-code-narrowing.test-d.ts +32 -0
  168. package/src/__tests__/errors-v7.test-d.ts +19 -0
  169. package/src/__tests__/errors-v7.unit.test.ts +15 -0
  170. package/src/__tests__/errors.unit.test.ts +83 -0
  171. package/src/__tests__/event-semantics-coverage.unit.test.ts +22 -0
  172. package/src/__tests__/format-info.unit.test.ts +85 -0
  173. package/src/__tests__/frame-model-parity.test-d.ts +20 -0
  174. package/src/__tests__/frame-model-parity.unit.test.ts +285 -0
  175. package/src/__tests__/guard-gates.test.ts +317 -0
  176. package/src/__tests__/public-surface.integration.test.ts +38 -0
  177. package/src/__tests__/readback-fault-cleanup.unit.test.ts +66 -0
  178. package/src/__tests__/readback-format-matrix.dawn.test.ts +378 -0
  179. package/src/__tests__/readback-format-matrix.unit.test.ts +120 -0
  180. package/src/__tests__/readback-matrix-fixture.ts +20 -0
  181. package/src/__tests__/recorder-bootstrap.unit.test.ts +302 -0
  182. package/src/__tests__/recorder-budget.unit.test.ts +42 -0
  183. package/src/__tests__/recorder-fail-fast.unit.test.ts +434 -0
  184. package/src/__tests__/recorder-lifecycle.unit.test.ts +42 -0
  185. package/src/__tests__/recorder-push-event.unit.test.ts +573 -0
  186. package/src/__tests__/recorder-session.integration.test.ts +32 -0
  187. package/src/__tests__/recorder-session.unit.test.ts +63 -0
  188. package/src/__tests__/recorder-steady-frame.dawn.test.ts +52 -0
  189. package/src/__tests__/replay-fail-fast.unit.test.ts +155 -0
  190. package/src/__tests__/replay-session.dawn.test.ts +220 -0
  191. package/src/__tests__/replay-session.test-d.ts +27 -0
  192. package/src/__tests__/replay-session.unit.test.ts +171 -0
  193. package/src/__tests__/resolve-texture-descriptor.unit.test.ts +144 -0
  194. package/src/__tests__/resource-lifecycle.unit.test.ts +114 -0
  195. package/src/__tests__/rhi-debug-fresh-replay.dawn.test.ts +178 -0
  196. package/src/__tests__/tape-index.unit.test.ts +56 -0
  197. package/src/__tests__/tape-v7-invalid.unit.test.ts +45 -0
  198. package/src/__tests__/tape-v7-roundtrip.unit.test.ts +66 -0
  199. package/src/__tests__/tape-v7-version.unit.test.ts +13 -0
  200. package/src/__tests__/tree-shake.unit.test.ts +151 -0
  201. package/src/__tests__/types.test.ts +257 -0
  202. package/src/browser.ts +146 -0
  203. package/src/errors.ts +173 -0
  204. package/src/frame-model.ts +753 -0
  205. package/src/index.ts +92 -0
  206. package/src/pako.d.ts +8 -0
  207. package/src/protocol/codec.ts +220 -0
  208. package/src/protocol/event-semantics.ts +180 -0
  209. package/src/protocol/tape-index.ts +120 -0
  210. package/src/protocol/types.ts +60 -0
  211. package/src/protocol/validation.ts +173 -0
  212. package/src/readback.ts +804 -0
  213. package/src/recorder/assemble.ts +192 -0
  214. package/src/recorder/closure.ts +408 -0
  215. package/src/recorder/core.ts +587 -0
  216. package/src/recorder/device.ts +474 -0
  217. package/src/recorder/encoder.ts +251 -0
  218. package/src/recorder/lifecycle.ts +847 -0
  219. package/src/recorder/pass.ts +241 -0
  220. package/src/recorder/proxy.ts +59 -0
  221. package/src/recorder/queue.ts +109 -0
  222. package/src/recorder/resource-registry.ts +53 -0
  223. package/src/recorder/session.ts +152 -0
  224. package/src/recorder/shader.ts +47 -0
  225. package/src/recorder/snapshot.ts +38 -0
  226. package/src/recorder/wrap.ts +157 -0
  227. package/src/recorder.ts +15 -0
  228. package/src/replay/device-request.ts +38 -0
  229. package/src/replay/execute-support.ts +243 -0
  230. package/src/replay/execute.ts +870 -0
  231. package/src/replay/readback.ts +671 -0
  232. package/src/replay/resources.ts +154 -0
  233. package/src/replay/session.ts +509 -0
  234. package/src/replay/texture-format.ts +99 -0
  235. package/src/texel-decode.ts +289 -0
  236. package/src/texel-layout.ts +324 -0
  237. package/src/types.ts +888 -0
@@ -0,0 +1,434 @@
1
+ // @forgeax/engine-rhi-debug/src/__tests__/recorder-fail-fast.unit.test.ts
2
+ //
3
+ // M2 bug-20260624: B2 fail-fast convergence —
4
+ // getTape() dangling detection mirrors deserializeTape's findDanglingHandleId.
5
+ //
6
+ // AC-04: producer-side fail-fast — getTape() returns tape-invalid
7
+ // for any handle that findDanglingHandleId would reject.
8
+ // AC-23: reuses the closed v7 tape error, no new member.
9
+ // R-3: in-frame transient handles (passHandleId, cmdHandleId) not
10
+ // falsely flagged when legitimately declared.
11
+
12
+ // biome-ignore-all lint/suspicious/noExplicitAny: stub RHI mock objects at test boundary
13
+ // biome-ignore-all lint/style/noNonNullAssertion: test assertions on mock stubs guarded by expect
14
+
15
+ import { describe, expect, it, vi } from 'vitest';
16
+ import type { RhiDebugError } from '../errors';
17
+ import { type DebugRhiInstance, wrap } from '../recorder';
18
+ import type { HandleId } from '../types';
19
+
20
+ // ---------------------------------------------------------------
21
+ // Minimal Result helpers
22
+ // ---------------------------------------------------------------
23
+
24
+ function rOk<T>(value: T) {
25
+ return { ok: true as const, value };
26
+ }
27
+
28
+ function h(): any {
29
+ return {};
30
+ }
31
+
32
+ // ---------------------------------------------------------------
33
+ // Build a mock RhiInstance with a proxied device
34
+ // ---------------------------------------------------------------
35
+
36
+ function buildMockInstanceWithRealDevice(): {
37
+ inst: any;
38
+ realDeviceSpies: Record<string, ReturnType<typeof vi.fn>>;
39
+ } {
40
+ const realCreateBuffer = vi.fn((_desc: any) => rOk(h()));
41
+ const realCreateTexture = vi.fn(() => rOk(h()));
42
+ const realCreateSampler = vi.fn(() => rOk(h()));
43
+ const realCreateBindGroupLayout = vi.fn(() => rOk(h()));
44
+ const realCreateBindGroup = vi.fn(() => rOk(h()));
45
+ const realCreatePipelineLayout = vi.fn(() => rOk(h()));
46
+ const realCreateRenderPipeline = vi.fn(() => rOk(h()));
47
+ const realCreateComputePipeline = vi.fn(() => rOk(h()));
48
+
49
+ function makeCmdEncoder(): any {
50
+ const e: any = {};
51
+ const realPass = makeRenderPass();
52
+ const realCPass = makeComputePass();
53
+ e.beginRenderPass = vi.fn(() => realPass);
54
+ e.beginComputePass = vi.fn(() => realCPass);
55
+ e.copyBufferToBuffer = vi.fn();
56
+ e.copyBufferToTexture = vi.fn();
57
+ e.copyTextureToBuffer = vi.fn();
58
+ e.copyTextureToTexture = vi.fn();
59
+ e.clearBuffer = vi.fn();
60
+ e.resolveQuerySet = vi.fn(() => rOk(undefined));
61
+ e.writeTimestamp = vi.fn();
62
+ e.pushDebugGroup = vi.fn();
63
+ e.popDebugGroup = vi.fn();
64
+ e.insertDebugMarker = vi.fn();
65
+ e.finish = vi.fn(() => rOk(h()));
66
+ return e;
67
+ }
68
+
69
+ function makeRenderPass(): any {
70
+ return {
71
+ setPipeline: vi.fn(),
72
+ setVertexBuffer: vi.fn(),
73
+ setIndexBuffer: vi.fn(),
74
+ setBindGroup: vi.fn(),
75
+ draw: vi.fn(),
76
+ drawIndexed: vi.fn(),
77
+ setViewport: vi.fn(),
78
+ setScissorRect: vi.fn(),
79
+ setBlendConstant: vi.fn(),
80
+ setStencilReference: vi.fn(),
81
+ drawIndirect: vi.fn(),
82
+ drawIndexedIndirect: vi.fn(),
83
+ pushDebugGroup: vi.fn(),
84
+ popDebugGroup: vi.fn(),
85
+ insertDebugMarker: vi.fn(),
86
+ executeBundles: vi.fn(),
87
+ beginOcclusionQuery: vi.fn(),
88
+ endOcclusionQuery: vi.fn(),
89
+ end: vi.fn(),
90
+ };
91
+ }
92
+
93
+ function makeComputePass(): any {
94
+ return {
95
+ setPipeline: vi.fn(),
96
+ setBindGroup: vi.fn(),
97
+ dispatchWorkgroups: vi.fn(),
98
+ end: vi.fn(),
99
+ };
100
+ }
101
+
102
+ const realDevice = {
103
+ caps: { rgba16floatRenderable: false },
104
+ features: new Set(),
105
+ limits: {},
106
+ queue: {
107
+ writeBuffer: vi.fn(() => rOk(undefined)),
108
+ submit: vi.fn(() => rOk(undefined)),
109
+ writeTexture: vi.fn(() => rOk(undefined)),
110
+ copyExternalImageToTexture: vi.fn(() => rOk(undefined)),
111
+ onSubmittedWorkDone: vi.fn(() => Promise.resolve(undefined)),
112
+ },
113
+ lost: new Promise(() => {}),
114
+ createBuffer: realCreateBuffer,
115
+ createTexture: realCreateTexture,
116
+ createTextureView: vi.fn(() => rOk(h())),
117
+ createSampler: realCreateSampler,
118
+ createBindGroupLayout: realCreateBindGroupLayout,
119
+ createBindGroup: realCreateBindGroup,
120
+ createPipelineLayout: realCreatePipelineLayout,
121
+ createRenderPipeline: realCreateRenderPipeline,
122
+ createComputePipeline: realCreateComputePipeline,
123
+ createCommandEncoder: vi.fn(() => rOk(makeCmdEncoder())),
124
+ createQuerySet: vi.fn(() => rOk(h())),
125
+ destroyBuffer: vi.fn(),
126
+ destroyTexture: vi.fn(),
127
+ };
128
+
129
+ const adapter = {
130
+ features: new Set(),
131
+ limits: {},
132
+ requestDevice: vi.fn(() => Promise.resolve(rOk(realDevice))),
133
+ };
134
+
135
+ return {
136
+ inst: { requestAdapter: vi.fn(() => Promise.resolve(rOk(adapter))) },
137
+ realDeviceSpies: {
138
+ createBuffer: realCreateBuffer,
139
+ createTexture: realCreateTexture,
140
+ createSampler: realCreateSampler,
141
+ createBindGroupLayout: realCreateBindGroupLayout,
142
+ createBindGroup: realCreateBindGroup,
143
+ createPipelineLayout: realCreatePipelineLayout,
144
+ createRenderPipeline: realCreateRenderPipeline,
145
+ createComputePipeline: realCreateComputePipeline,
146
+ },
147
+ };
148
+ }
149
+
150
+ async function bootstrapWithRealDevice(): Promise<{
151
+ debugInst: DebugRhiInstance;
152
+ proxyDevice: any;
153
+ realDevice: any;
154
+ }> {
155
+ const { inst } = buildMockInstanceWithRealDevice();
156
+ const debugInst = wrap(inst);
157
+ const adapterRes = await debugInst.requestAdapter();
158
+ if (!adapterRes.ok) throw new Error('adapter');
159
+ const adapter = (adapterRes as any).value;
160
+ const devRes = await adapter.requestDevice();
161
+ if (!devRes.ok) throw new Error('device');
162
+ const proxyDevice = (devRes as any).value;
163
+ const realDevice = proxyDevice._realDevice;
164
+ return { debugInst, proxyDevice, realDevice };
165
+ }
166
+
167
+ // ================================================================
168
+ // B2: dangling handle detection — getTape fail-fast
169
+ // ================================================================
170
+
171
+ describe('B2: getTape dangling handle fail-fast (AC-04)', () => {
172
+ it('returns tape-invalid for orphan passHandleId in endRenderPass', async () => {
173
+ const { debugInst } = await bootstrapWithRealDevice();
174
+
175
+ // Inject an endRenderPass event with a passHandleId that was
176
+ // never declared by any beginRenderPass in the frame, and is not
177
+ // in bootstrapCreates. This simulates a tape where a pass handle
178
+ // is referenced but undeclared — findDanglingHandleId would reject
179
+ // it. Before B2 convergence, _collectFrameReferencedHandleIds
180
+ // ignores passHandleId from endRenderPass (it's not in the switch),
181
+ // so getTape() would silently produce a tape that deserializeTape
182
+ // later rejects.
183
+ debugInst.arm(1);
184
+
185
+ // Inject endRenderPass with an undeclared passHandleId directly
186
+ // into the event stream via pushExternalEvent.
187
+ const orphanPassId = 'renderPass:orphan' as HandleId;
188
+ debugInst.pushExternalEvent({
189
+ kind: 'endRenderPass',
190
+ passHandleId: orphanPassId,
191
+ } as any);
192
+
193
+ debugInst.onFrameEnd();
194
+
195
+ const tape = debugInst.getTape();
196
+ expect(tape).toHaveProperty('code', 'tape-invalid');
197
+
198
+ const error = tape as RhiDebugError;
199
+ expect(error.code).toBe('tape-invalid');
200
+ expect(error.detail).toMatchObject({ handleId: orphanPassId });
201
+ });
202
+
203
+ it('returns tape-invalid for orphan passHandleId in draw', async () => {
204
+ const { debugInst } = await bootstrapWithRealDevice();
205
+
206
+ debugInst.arm(1);
207
+
208
+ const orphanPassId = 'renderPass:orphan' as HandleId;
209
+ debugInst.pushExternalEvent({
210
+ kind: 'draw',
211
+ passHandleId: orphanPassId,
212
+ vertexCount: 3,
213
+ instanceCount: 1,
214
+ firstVertex: 0,
215
+ firstInstance: 0,
216
+ } as any);
217
+
218
+ debugInst.onFrameEnd();
219
+
220
+ const tape = debugInst.getTape();
221
+ expect(tape).toHaveProperty('code', 'tape-invalid');
222
+ const error = tape as RhiDebugError;
223
+ expect(error.code).toBe('tape-invalid');
224
+ expect(error.detail).toMatchObject({ handleId: orphanPassId });
225
+ });
226
+
227
+ it('returns tape-invalid for orphan cmdHandleId in finish', async () => {
228
+ const { debugInst } = await bootstrapWithRealDevice();
229
+
230
+ debugInst.arm(1);
231
+
232
+ const orphanCmdId = 'commandEncoder:orphan' as HandleId;
233
+ debugInst.pushExternalEvent({
234
+ kind: 'finish',
235
+ cmdHandleId: orphanCmdId,
236
+ } as any);
237
+
238
+ debugInst.onFrameEnd();
239
+
240
+ const tape = debugInst.getTape();
241
+ expect(tape).toHaveProperty('code', 'tape-invalid');
242
+ const error = tape as RhiDebugError;
243
+ expect(error.code).toBe('tape-invalid');
244
+ expect(error.detail).toMatchObject({ handleId: orphanCmdId });
245
+ });
246
+
247
+ it('returns tape-invalid for orphan cmdHandleId in submit', async () => {
248
+ const { debugInst } = await bootstrapWithRealDevice();
249
+
250
+ debugInst.arm(1);
251
+
252
+ const orphanCmdId = 'commandEncoder:orphan' as HandleId;
253
+ debugInst.pushExternalEvent({
254
+ kind: 'submit',
255
+ cmdHandleIds: [orphanCmdId],
256
+ } as any);
257
+
258
+ debugInst.onFrameEnd();
259
+
260
+ const tape = debugInst.getTape();
261
+ expect(tape).toHaveProperty('code', 'tape-invalid');
262
+ const error = tape as RhiDebugError;
263
+ expect(error.code).toBe('tape-invalid');
264
+ expect(error.detail).toMatchObject({ handleId: orphanCmdId });
265
+ });
266
+
267
+ it('returns tape-invalid for orphan cmdHandleId in beginRenderPass', async () => {
268
+ const { debugInst } = await bootstrapWithRealDevice();
269
+
270
+ debugInst.arm(1);
271
+
272
+ const orphanCmdId = 'commandEncoder:orphan' as HandleId;
273
+ debugInst.pushExternalEvent({
274
+ kind: 'beginRenderPass',
275
+ cmdHandleId: orphanCmdId,
276
+ passHandleId: 'renderPass:valid' as HandleId,
277
+ colorAttachmentViewHandleIds: [],
278
+ desc: { colorAttachments: [] },
279
+ } as any);
280
+
281
+ debugInst.onFrameEnd();
282
+
283
+ const tape = debugInst.getTape();
284
+ expect(tape).toHaveProperty('code', 'tape-invalid');
285
+ const error = tape as RhiDebugError;
286
+ expect(error.code).toBe('tape-invalid');
287
+ expect(error.detail).toMatchObject({ handleId: orphanCmdId });
288
+ });
289
+
290
+ it('returns tape-invalid for orphan handle in setPipeline', async () => {
291
+ const { debugInst } = await bootstrapWithRealDevice();
292
+
293
+ debugInst.arm(1);
294
+
295
+ // passHandleId + pipelineHandleId both undeclared.
296
+ const orphanPassId = 'renderPass:orphan' as HandleId;
297
+ debugInst.pushExternalEvent({
298
+ kind: 'setPipeline',
299
+ passHandleId: orphanPassId,
300
+ pipelineHandleId: 'renderPipeline:ghost' as HandleId,
301
+ } as any);
302
+
303
+ debugInst.onFrameEnd();
304
+
305
+ const tape = debugInst.getTape();
306
+ expect(tape).toHaveProperty('code', 'tape-invalid');
307
+ const error = tape as RhiDebugError;
308
+ expect(error.code).toBe('tape-invalid');
309
+ });
310
+
311
+ it('returns tape-invalid for orphan handle in pushDebugGroup', async () => {
312
+ const { debugInst } = await bootstrapWithRealDevice();
313
+
314
+ debugInst.arm(1);
315
+
316
+ const orphanCmdId = 'commandEncoder:orphan' as HandleId;
317
+ debugInst.pushExternalEvent({
318
+ kind: 'pushDebugGroup',
319
+ cmdHandleId: orphanCmdId,
320
+ groupLabel: 'test',
321
+ } as any);
322
+
323
+ debugInst.onFrameEnd();
324
+
325
+ const tape = debugInst.getTape();
326
+ expect(tape).toHaveProperty('code', 'tape-invalid');
327
+ const error = tape as RhiDebugError;
328
+ expect(error.code).toBe('tape-invalid');
329
+ expect(error.detail).toMatchObject({ handleId: orphanCmdId });
330
+ });
331
+ });
332
+
333
+ // ================================================================
334
+ // B2: known-good paths — not falsely flagging
335
+ // ================================================================
336
+
337
+ describe('B2: legitimate tape production (known-good paths)', () => {
338
+ it('returns a valid Tape when all references resolve through bootstrapCreates', async () => {
339
+ const { debugInst, proxyDevice } = await bootstrapWithRealDevice();
340
+
341
+ // Pre-arm resource creation.
342
+ const bufRes = proxyDevice.createBuffer({ size: 64, usage: 16 });
343
+ expect(bufRes.ok).toBe(true);
344
+ const buf = (bufRes as any).value;
345
+
346
+ debugInst.arm(1);
347
+ proxyDevice.queue.writeBuffer(buf, 0, new Uint8Array([1]));
348
+ debugInst.onFrameEnd();
349
+
350
+ const tape = debugInst.getTape();
351
+ expect(tape).not.toHaveProperty('code');
352
+ expect(tape).toHaveProperty('events');
353
+ });
354
+
355
+ it('does not flag handles declared in-frame by create* events', async () => {
356
+ const { debugInst, proxyDevice } = await bootstrapWithRealDevice();
357
+
358
+ // All resources created in-frame (after arm) — their create* events
359
+ // are part of the frame's event stream and should be self-sufficient.
360
+ debugInst.arm(1);
361
+ const bufRes = proxyDevice.createBuffer({ size: 64, usage: 16 });
362
+ expect(bufRes.ok).toBe(true);
363
+ proxyDevice.queue.writeBuffer((bufRes as any).value, 0, new Uint8Array([1]));
364
+ debugInst.onFrameEnd();
365
+
366
+ const tape = debugInst.getTape();
367
+ expect(tape).not.toHaveProperty('code');
368
+ expect(tape).toHaveProperty('events');
369
+ });
370
+ });
371
+
372
+ // ================================================================
373
+ // R-3: in-frame transient handle exemption (anti-regression)
374
+ // ================================================================
375
+
376
+ describe('B2: R-3 in-frame transient handle exemption (anti-regression)', () => {
377
+ it('does not falsely flag pass+capture chain from real proxy', async () => {
378
+ const { debugInst, proxyDevice } = await bootstrapWithRealDevice();
379
+
380
+ // Full proxy chain: createCommandEncoder → beginRenderPass → ... → end → finish → submit.
381
+ // All passHandleId and cmdHandleId are properly declared in-frame.
382
+ debugInst.arm(1);
383
+
384
+ const cmdEncRes = proxyDevice.createCommandEncoder();
385
+ expect(cmdEncRes.ok).toBe(true);
386
+ const cmdEnc = (cmdEncRes as any).value;
387
+
388
+ const pass = cmdEnc.beginRenderPass({
389
+ colorAttachments: [],
390
+ });
391
+
392
+ pass.setViewport(0, 0, 800, 600, 0, 1);
393
+ pass.setScissorRect(0, 0, 800, 600);
394
+ pass.end();
395
+
396
+ const cmdBufRes = cmdEnc.finish();
397
+ expect(cmdBufRes.ok).toBe(true);
398
+ const cmdBuf = (cmdBufRes as any).value;
399
+
400
+ proxyDevice.queue.submit([cmdBuf]);
401
+
402
+ debugInst.onFrameEnd();
403
+
404
+ const tape = debugInst.getTape();
405
+ expect(tape).not.toHaveProperty('code');
406
+ expect(tape).toHaveProperty('events');
407
+ });
408
+
409
+ it('does not falsely flag popDebugGroup/insertDebugMarker/finish referencing in-frame cmdHandleId', async () => {
410
+ const { debugInst, proxyDevice } = await bootstrapWithRealDevice();
411
+
412
+ debugInst.arm(1);
413
+
414
+ const cmdEncRes = proxyDevice.createCommandEncoder();
415
+ expect(cmdEncRes.ok).toBe(true);
416
+ const cmdEnc = (cmdEncRes as any).value;
417
+
418
+ cmdEnc.pushDebugGroup('test');
419
+ cmdEnc.popDebugGroup();
420
+ cmdEnc.insertDebugMarker('marker');
421
+
422
+ const cmdBufRes = cmdEnc.finish();
423
+ expect(cmdBufRes.ok).toBe(true);
424
+ const cmdBuf = (cmdBufRes as any).value;
425
+
426
+ proxyDevice.queue.submit([cmdBuf]);
427
+
428
+ debugInst.onFrameEnd();
429
+
430
+ const tape = debugInst.getTape();
431
+ expect(tape).not.toHaveProperty('code');
432
+ expect(tape).toHaveProperty('events');
433
+ });
434
+ });
@@ -0,0 +1,42 @@
1
+ import type { RhiInstance } from '@forgeax/engine-rhi';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { attachRecorder, type RecordableBackend } from '../recorder/session';
4
+
5
+ function backend(): RecordableBackend {
6
+ return {
7
+ rhi: {
8
+ requestAdapter: vi.fn(() => Promise.resolve({ ok: false as const, error: {} })),
9
+ } as unknown as RhiInstance,
10
+ createShaderModule: vi.fn(),
11
+ };
12
+ }
13
+
14
+ describe('RecorderSession lifecycle', () => {
15
+ it('turns device loss into a terminal failure and permits a fresh generation', async () => {
16
+ const attached = attachRecorder(backend());
17
+ expect(attached.ok).toBe(true);
18
+ if (!attached.ok) return;
19
+
20
+ const first = attached.value.captureFrame();
21
+ attached.value.deviceLost();
22
+ expect(await first).toMatchObject({ ok: false, error: { code: 'capture-unavailable' } });
23
+
24
+ const second = attached.value.captureFrame();
25
+ await attached.value.frameBoundary();
26
+ await attached.value.frameBoundary();
27
+ expect(await second).toMatchObject({ ok: true });
28
+ });
29
+
30
+ it('makes dispose idempotent and rejects later captures without throwing', async () => {
31
+ const attached = attachRecorder(backend());
32
+ expect(attached.ok).toBe(true);
33
+ if (!attached.ok) return;
34
+
35
+ expect((await attached.value.dispose()).ok).toBe(true);
36
+ expect((await attached.value.dispose()).ok).toBe(true);
37
+ expect(await attached.value.captureFrame()).toMatchObject({
38
+ ok: false,
39
+ error: { code: 'capture-unavailable' },
40
+ });
41
+ });
42
+ });