@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,285 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildFrameModel } from '../frame-model';
3
+ import type { Tape } from '../protocol/types';
4
+
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', () => {
141
+ const tape: Tape = {
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
+ ],
234
+ events: [
235
+ {
236
+ kind: 'beginRenderPass',
237
+ passHandleId: 'pass:render',
238
+ cmdHandleId: 'encoder:1',
239
+ desc: { colorAttachments: [] },
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',
252
+ },
253
+ {
254
+ kind: 'draw',
255
+ passHandleId: 'pass:render',
256
+ vertexCount: 3,
257
+ instanceCount: 1,
258
+ firstVertex: 0,
259
+ firstInstance: 0,
260
+ },
261
+ { kind: 'endRenderPass', passHandleId: 'pass:render' },
262
+ ],
263
+ blobs: [],
264
+ };
265
+
266
+ const work = buildFrameModel(tape).works[0];
267
+ expect(work?.pipeline).toMatchObject({
268
+ status: 'available',
269
+ pipelineHandleId: 'pipeline:main',
270
+ });
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
+ ]);
284
+ });
285
+ });
@@ -0,0 +1,317 @@
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 { spawnSync } from 'node:child_process';
13
+ import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
14
+ import { tmpdir } from 'node:os';
15
+ import path from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { describe, expect, it } from 'vitest';
18
+
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ const ENGINE_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
21
+
22
+ function flagDriftHits(root: string): string[] {
23
+ const hits: string[] = [];
24
+ const visit = (directory: string): void => {
25
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
26
+ if (entry.isDirectory()) {
27
+ if (entry.name === 'dist' || entry.name === 'node_modules') continue;
28
+ visit(path.join(directory, entry.name));
29
+ continue;
30
+ }
31
+ if (!entry.isFile() || (!entry.name.endsWith('.ts') && !entry.name.endsWith('.mjs'))) {
32
+ continue;
33
+ }
34
+ if (entry.name === 'guard-gates.test.ts') continue;
35
+ const absolute = path.join(directory, entry.name);
36
+ const source = readFileSync(absolute, 'utf8');
37
+ if (source.includes('--runId') || source.includes('--ws-url')) {
38
+ hits.push(path.relative(ENGINE_ROOT, absolute));
39
+ }
40
+ }
41
+ };
42
+ visit(root);
43
+ return hits.sort();
44
+ }
45
+
46
+ describe('AC-07: flag drift grep gate', () => {
47
+ it('zero hits for --runId / --ws-url across apps/ packages/ (excluding dist, node_modules, self)', () => {
48
+ expect([
49
+ ...flagDriftHits(path.join(ENGINE_ROOT, 'apps')),
50
+ ...flagDriftHits(path.join(ENGINE_ROOT, 'packages')),
51
+ ]).toEqual([]);
52
+ });
53
+ });
54
+
55
+ describe('RHI-debug error owner gate', () => {
56
+ it('keeps the v7 error union as the only error owner', () => {
57
+ const errorsPath = path.resolve(__dirname, '..', '..', 'src', 'errors.ts');
58
+ const content = readFileSync(errorsPath, 'utf-8');
59
+ expect(content).toContain('export type RhiDebugErrorCode =');
60
+ expect(content).not.toMatch(/\bDebugErrorCode\b/);
61
+ });
62
+ });
63
+
64
+ describe('W92: readback staging usage owner', () => {
65
+ it('keeps one COPY_DST | MAP_READ owner for all three readback paths', () => {
66
+ const readbackPath = path.resolve(__dirname, '..', '..', 'src', 'readback.ts');
67
+ const content = readFileSync(readbackPath, 'utf-8');
68
+ expect(content.match(/const COPY_DST_MAP_READ = 9/g)).toHaveLength(1);
69
+ expect(content.match(/usage: COPY_DST_MAP_READ/g)).toHaveLength(3);
70
+ });
71
+ });
72
+
73
+ describe('AC-08 partial: import.meta.hot in rhiDebugFlag guard', () => {
74
+ it('all hotMeta.hot / import.meta.hot code references are inside rhiDebugFlag guard block', () => {
75
+ const createAppPath = path.resolve(
76
+ __dirname,
77
+ '..',
78
+ '..',
79
+ '..',
80
+ '..',
81
+ 'packages',
82
+ 'app',
83
+ 'src',
84
+ 'create-app.ts',
85
+ );
86
+ const content = readFileSync(createAppPath, 'utf-8');
87
+ const lines = content.split('\n');
88
+
89
+ const guardConditionIdx = lines.findIndex((l) => l.includes("rhiDebugFlag === '1'"));
90
+ expect(guardConditionIdx).not.toBe(-1);
91
+ let guardOpenIdx = -1;
92
+ for (let i = guardConditionIdx; i >= 0; i--) {
93
+ if (lines[i]?.includes('if (')) {
94
+ guardOpenIdx = i;
95
+ break;
96
+ }
97
+ }
98
+ expect(guardOpenIdx).not.toBe(-1);
99
+
100
+ const guardBraceIdx = lines.findIndex(
101
+ (l, index) => index >= guardConditionIdx && l.includes('{'),
102
+ );
103
+ expect(guardBraceIdx).not.toBe(-1);
104
+
105
+ // Find the matching '}' at the same indent level as the 'if' statement.
106
+ // Avoid counting braces inside template literals or strings by matching
107
+ // the exact indent prefix.
108
+ const guardLine = lines[guardBraceIdx];
109
+ if (guardLine === undefined) throw new Error('unreachable: guardBraceIdx verified above');
110
+ const indentMatch = guardLine.match(/^(\s*)/);
111
+ if (indentMatch === null)
112
+ throw new Error('unreachable: every line matches the whitespace regex');
113
+ const indent = indentMatch[1];
114
+ let guardCloseIdx = -1;
115
+ for (let i = guardBraceIdx + 1; i < lines.length; i++) {
116
+ if (lines[i] === `${indent}}`) {
117
+ guardCloseIdx = i;
118
+ break;
119
+ }
120
+ }
121
+ expect(guardCloseIdx).not.toBe(-1);
122
+
123
+ // Verify all hotMeta / import.meta.hot references in non-comment lines
124
+ // fall within the guard block.
125
+ for (let i = 0; i < lines.length; i++) {
126
+ const line = lines[i];
127
+ if (line === undefined) break;
128
+ const trimmed = line.trimStart();
129
+ // Skip comment-only lines.
130
+ if (trimmed.startsWith('//') || trimmed.startsWith('*')) continue;
131
+ if (
132
+ trimmed.includes('hotMeta') ||
133
+ (trimmed.includes('import.meta') && trimmed.includes('hot'))
134
+ ) {
135
+ expect(
136
+ i,
137
+ `import.meta.hot reference at line ${i + 1} is outside rhiDebugFlag guard (guard: ${guardOpenIdx + 1}-${guardCloseIdx + 1})`,
138
+ ).toBeGreaterThanOrEqual(guardOpenIdx);
139
+ expect(
140
+ i,
141
+ `import.meta.hot reference at line ${i + 1} is outside rhiDebugFlag guard (guard: ${guardOpenIdx + 1}-${guardCloseIdx + 1})`,
142
+ ).toBeLessThanOrEqual(guardCloseIdx);
143
+ }
144
+ }
145
+ });
146
+ });
147
+
148
+ describe('RHI-debug smoke roster gate', () => {
149
+ it('resolves every declared hello and learn-render smoke at 300 frames', () => {
150
+ const rosterPath = path.resolve(ENGINE_ROOT, 'scripts', 'rhi-debug-smoke-roster.mjs');
151
+ const result = spawnSync(
152
+ process.execPath,
153
+ [
154
+ rosterPath,
155
+ '--apps-root',
156
+ 'apps/hello',
157
+ '--learn-root',
158
+ 'apps/learn-render',
159
+ '--frames',
160
+ '300',
161
+ ],
162
+ { cwd: ENGINE_ROOT, encoding: 'utf8' },
163
+ );
164
+ expect(result.status).toBe(1);
165
+ const roster = JSON.parse(result.stdout) as {
166
+ status: string;
167
+ frameCount: number;
168
+ execution: { status: string; mode: string };
169
+ entries: readonly {
170
+ frames: number;
171
+ command: string;
172
+ invocation: string;
173
+ tokens: readonly string[];
174
+ }[];
175
+ unavailable: readonly { reason: string }[];
176
+ };
177
+ expect(roster.status).toBe('unavailable');
178
+ expect(roster.execution).toMatchObject({ status: 'not-executed', mode: 'declaration-only' });
179
+ expect(roster.frameCount).toBe(300);
180
+ expect(roster.entries.length).toBeGreaterThan(0);
181
+ expect(
182
+ roster.entries.every(
183
+ (entry) =>
184
+ entry.frames === 300 &&
185
+ entry.command === entry.invocation &&
186
+ entry.tokens.join(' ') === entry.invocation &&
187
+ entry.tokens[0] === 'pnpm' &&
188
+ entry.tokens[1] === '--filter',
189
+ ),
190
+ ).toBe(true);
191
+ expect(roster.entries.some((entry) => entry.invocation.includes('format-tier1 build'))).toBe(
192
+ true,
193
+ );
194
+ expect(roster.unavailable.length).toBeGreaterThan(0);
195
+ expect(roster.unavailable.every((item) => item.reason.length > 0)).toBe(true);
196
+ });
197
+
198
+ it('executes every declared command in a temporary workspace and fails closed', () => {
199
+ const fixture = mkdtempSync(path.join(tmpdir(), 'forgeax-rhi-debug-roster-'));
200
+ const appRoot = path.join(fixture, 'apps', 'hello');
201
+ const packageRoot = path.join(appRoot, 'fake-smoke');
202
+ const learnRoot = path.join(fixture, 'apps', 'learn-render');
203
+ const learnPackageRoot = path.join(learnRoot, 'fake-learn-smoke');
204
+ mkdirSync(packageRoot, { recursive: true });
205
+ mkdirSync(learnPackageRoot, { recursive: true });
206
+ writeFileSync(path.join(fixture, 'pnpm-workspace.yaml'), 'packages:\n - apps/**\n');
207
+ const manifestPath = path.join(packageRoot, 'package.json');
208
+ writeFileSync(
209
+ manifestPath,
210
+ JSON.stringify({
211
+ name: '@fake/smoke',
212
+ private: true,
213
+ scripts: { 'smoke:browser': 'node -e "console.log(\'fake browser smoke\')"' },
214
+ forgeax: { smokeInvocation: 'pnpm --filter @fake/smoke smoke:browser' },
215
+ }),
216
+ );
217
+ writeFileSync(
218
+ path.join(learnPackageRoot, 'package.json'),
219
+ JSON.stringify({
220
+ name: '@fake/learn-smoke',
221
+ private: true,
222
+ scripts: { 'smoke:browser': 'node -e "console.log(\'fake learn browser smoke\')"' },
223
+ forgeax: { smokeInvocation: 'pnpm --filter @fake/learn-smoke smoke:browser' },
224
+ }),
225
+ );
226
+ const rosterPath = path.resolve(ENGINE_ROOT, 'scripts', 'rhi-debug-smoke-roster.mjs');
227
+ try {
228
+ const passed = spawnSync(
229
+ process.execPath,
230
+ [
231
+ rosterPath,
232
+ '--apps-root',
233
+ appRoot,
234
+ '--learn-root',
235
+ learnRoot,
236
+ '--cwd',
237
+ fixture,
238
+ '--frames',
239
+ '300',
240
+ '--execute',
241
+ ],
242
+ { cwd: ENGINE_ROOT, encoding: 'utf8' },
243
+ );
244
+ expect(passed.status).toBe(0);
245
+ const passedRoster = JSON.parse(passed.stdout) as {
246
+ status: string;
247
+ execution: { status: string; mode: string; passedCount: number; failedCount: number };
248
+ entries: readonly {
249
+ status: string;
250
+ returnCode: number | null;
251
+ stdout: string;
252
+ invocation: string;
253
+ tokens: readonly string[];
254
+ backend: string;
255
+ frames: number;
256
+ }[];
257
+ };
258
+ expect(passedRoster.status).toBe('passed');
259
+ expect(passedRoster.execution).toMatchObject({
260
+ status: 'passed',
261
+ mode: 'execute',
262
+ passedCount: 2,
263
+ failedCount: 0,
264
+ });
265
+ const passedEntry = passedRoster.entries.find(
266
+ (entry) => entry.invocation === 'pnpm --filter @fake/smoke smoke:browser',
267
+ );
268
+ expect(passedEntry).toMatchObject({
269
+ status: 'passed',
270
+ returnCode: 0,
271
+ invocation: 'pnpm --filter @fake/smoke smoke:browser',
272
+ tokens: ['pnpm', '--filter', '@fake/smoke', 'smoke:browser'],
273
+ backend: 'browser',
274
+ frames: 300,
275
+ });
276
+ expect(passedEntry?.stdout).toContain('fake browser smoke');
277
+
278
+ writeFileSync(
279
+ manifestPath,
280
+ JSON.stringify({
281
+ name: '@fake/smoke',
282
+ private: true,
283
+ scripts: {
284
+ 'smoke:browser': 'node -e "console.error(\'fake failure\'); process.exit(7)"',
285
+ },
286
+ forgeax: { smokeInvocation: 'pnpm --filter @fake/smoke smoke:browser' },
287
+ }),
288
+ );
289
+ const failed = spawnSync(
290
+ process.execPath,
291
+ [
292
+ rosterPath,
293
+ '--apps-root',
294
+ appRoot,
295
+ '--learn-root',
296
+ learnRoot,
297
+ '--cwd',
298
+ fixture,
299
+ '--execute',
300
+ ],
301
+ { cwd: ENGINE_ROOT, encoding: 'utf8' },
302
+ );
303
+ expect(failed.status).toBe(1);
304
+ const failedRoster = JSON.parse(failed.stdout) as {
305
+ status: string;
306
+ execution: { status: string; failedCount: number };
307
+ entries: readonly { status: string; returnCode: number | null; stderr: string }[];
308
+ };
309
+ expect(failedRoster.status).toBe('failed');
310
+ expect(failedRoster.execution).toMatchObject({ status: 'failed', failedCount: 1 });
311
+ expect(failedRoster.entries[0]).toMatchObject({ status: 'failed', returnCode: 7 });
312
+ expect(failedRoster.entries[0]?.stderr).toContain('fake failure');
313
+ } finally {
314
+ rmSync(fixture, { recursive: true, force: true });
315
+ }
316
+ });
317
+ });
@@ -0,0 +1,38 @@
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.buildResourceLifecycle).toBeTypeOf('function');
15
+ expect(root.createRhiDebugError).toBeTypeOf('function');
16
+ });
17
+
18
+ it('declares only the v7 root and browser entry', () => {
19
+ const manifest = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as {
20
+ exports: Record<string, unknown>;
21
+ };
22
+ expect(manifest.exports['.']).toBeDefined();
23
+ expect(manifest.exports['./browser']).toMatchObject({ import: './dist/browser.mjs' });
24
+ expect(manifest.exports['./frame-model']).toBeUndefined();
25
+ expect(manifest.exports['./resource-lifecycle']).toBeUndefined();
26
+ expect(manifest.exports['./dev-routes']).toBeUndefined();
27
+ expect(manifest.exports['./capture-browser']).toBeUndefined();
28
+ expect(manifest.exports['./cli']).toBeUndefined();
29
+ expect(Object.keys(manifest.exports).sort()).toEqual(['.', './browser', './package.json']);
30
+ });
31
+
32
+ it('keeps the producer contract as the only model surface', () => {
33
+ expect(root).not.toHaveProperty('buildViewerModel');
34
+ expect(root).not.toHaveProperty('buildViewModel');
35
+ expect(root).not.toHaveProperty('captureAndUpload');
36
+ expect(root).not.toHaveProperty('uploadTape');
37
+ });
38
+ });