@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,241 @@
1
+ // @forgeax/engine-rhi-debug/src/recorder/pass -- render/compute pass proxy owners.
2
+
3
+ import type {
4
+ ComputePipeline,
5
+ RenderPipeline,
6
+ RhiComputePassEncoder,
7
+ RhiRenderPassEncoder,
8
+ } from '@forgeax/engine-rhi';
9
+ import type { HandleId } from '../types';
10
+ import type { RecorderInternal } from './core';
11
+ import { getHandleId, pushEvent } from './core';
12
+
13
+ export function createRenderPassProxy(
14
+ s: RecorderInternal,
15
+ realPass: RhiRenderPassEncoder,
16
+ passHId: HandleId,
17
+ ): RhiRenderPassEncoder {
18
+ return {
19
+ setPipeline(pipeline: RenderPipeline) {
20
+ const pid = getHandleId(s, pipeline as object, 'renderPipeline');
21
+ pushEvent(s, { kind: 'setPipeline', passHandleId: passHId, pipelineHandleId: pid });
22
+ realPass.setPipeline(pipeline);
23
+ },
24
+ setVertexBuffer(slot, buffer, offset, size) {
25
+ const bid = getHandleId(s, buffer as object, 'buffer');
26
+ pushEvent(s, {
27
+ kind: 'setVertexBuffer',
28
+ passHandleId: passHId,
29
+ slot,
30
+ bufferHandleId: bid,
31
+ offset,
32
+ size,
33
+ });
34
+ realPass.setVertexBuffer(slot, buffer, offset, size);
35
+ },
36
+ setIndexBuffer(buffer, format, offset, size) {
37
+ const bid = getHandleId(s, buffer as object, 'buffer');
38
+ pushEvent(s, {
39
+ kind: 'setIndexBuffer',
40
+ passHandleId: passHId,
41
+ bufferHandleId: bid,
42
+ format,
43
+ offset,
44
+ size,
45
+ });
46
+ realPass.setIndexBuffer(buffer, format, offset, size);
47
+ },
48
+ setBindGroup(index, bindGroup, ...rest: unknown[]) {
49
+ const bgid = getHandleId(s, bindGroup as object, 'bindGroup');
50
+ let dynOffsets: readonly number[] | undefined;
51
+ if (rest[0] instanceof Uint32Array) {
52
+ dynOffsets = Array.from(rest[0] as Uint32Array);
53
+ (realPass.setBindGroup as (...args: unknown[]) => void)(index, bindGroup, ...rest);
54
+ } else {
55
+ const offsets = rest[0] as readonly number[] | undefined;
56
+ dynOffsets = offsets === undefined ? undefined : Array.from(offsets);
57
+ realPass.setBindGroup(index, bindGroup, offsets);
58
+ }
59
+ pushEvent(s, {
60
+ kind: 'setBindGroup',
61
+ passHandleId: passHId,
62
+ index,
63
+ bindGroupHandleId: bgid,
64
+ dynamicOffsets: dynOffsets,
65
+ });
66
+ },
67
+ draw(vertexCount, instanceCount, firstVertex, firstInstance) {
68
+ pushEvent(s, {
69
+ kind: 'draw',
70
+ passHandleId: passHId,
71
+ vertexCount,
72
+ instanceCount: instanceCount ?? 1,
73
+ firstVertex: firstVertex ?? 0,
74
+ firstInstance: firstInstance ?? 0,
75
+ });
76
+ realPass.draw(vertexCount, instanceCount, firstVertex, firstInstance);
77
+ },
78
+ drawIndexed(indexCount, instanceCount, firstIndex, baseVertex, firstInstance) {
79
+ pushEvent(s, {
80
+ kind: 'drawIndexed',
81
+ passHandleId: passHId,
82
+ indexCount,
83
+ instanceCount: instanceCount ?? 1,
84
+ firstIndex: firstIndex ?? 0,
85
+ baseVertex: baseVertex ?? 0,
86
+ firstInstance: firstInstance ?? 0,
87
+ });
88
+ realPass.drawIndexed(indexCount, instanceCount, firstIndex, baseVertex, firstInstance);
89
+ },
90
+
91
+ // Pass-through methods (not in v1 event set, but must not break the proxy)
92
+ setViewport(x, y, w, h, minDepth, maxDepth) {
93
+ pushEvent(s, {
94
+ kind: 'setViewport',
95
+ passHandleId: passHId,
96
+ x,
97
+ y,
98
+ w,
99
+ h,
100
+ minDepth: minDepth ?? 0,
101
+ maxDepth: maxDepth ?? 1,
102
+ });
103
+ realPass.setViewport(x, y, w, h, minDepth, maxDepth);
104
+ },
105
+ setScissorRect(x, y, w, h) {
106
+ pushEvent(s, {
107
+ kind: 'setScissorRect',
108
+ passHandleId: passHId,
109
+ x,
110
+ y,
111
+ w,
112
+ h,
113
+ });
114
+ realPass.setScissorRect(x, y, w, h);
115
+ },
116
+ setBlendConstant(color) {
117
+ pushEvent(s, {
118
+ kind: 'setBlendConstant',
119
+ passHandleId: passHId,
120
+ color,
121
+ });
122
+ realPass.setBlendConstant(color);
123
+ },
124
+ setStencilReference(reference) {
125
+ // Recorded (not a no-op pass-through): stencil pipelines compare against
126
+ // this dynamic reference, so without it replay defaults ref=0 and a
127
+ // not-equal/equal stencil test (e.g. the 4.2 stencil-testing outline
128
+ // pass) silently breaks -- the outline vanishes on replay.
129
+ pushEvent(s, {
130
+ kind: 'setStencilReference',
131
+ passHandleId: passHId,
132
+ reference,
133
+ });
134
+ realPass.setStencilReference(reference);
135
+ },
136
+ drawIndirect(indirectBuffer, indirectOffset) {
137
+ const ibId = getHandleId(s, indirectBuffer as object, 'buffer');
138
+ pushEvent(s, {
139
+ kind: 'drawIndirect',
140
+ passHandleId: passHId,
141
+ indirectBufferHandleId: ibId,
142
+ indirectOffset,
143
+ });
144
+ realPass.drawIndirect(indirectBuffer, indirectOffset);
145
+ },
146
+ drawIndexedIndirect(indirectBuffer, indirectOffset) {
147
+ const ibId = getHandleId(s, indirectBuffer as object, 'buffer');
148
+ pushEvent(s, {
149
+ kind: 'drawIndexedIndirect',
150
+ passHandleId: passHId,
151
+ indirectBufferHandleId: ibId,
152
+ indirectOffset,
153
+ });
154
+ realPass.drawIndexedIndirect(indirectBuffer, indirectOffset);
155
+ },
156
+ pushDebugGroup(groupLabel) {
157
+ pushEvent(s, {
158
+ kind: 'passPushDebugGroup',
159
+ passHandleId: passHId,
160
+ groupLabel,
161
+ });
162
+ realPass.pushDebugGroup(groupLabel);
163
+ },
164
+ popDebugGroup() {
165
+ pushEvent(s, { kind: 'passPopDebugGroup', passHandleId: passHId });
166
+ realPass.popDebugGroup();
167
+ },
168
+ insertDebugMarker(markerLabel) {
169
+ pushEvent(s, {
170
+ kind: 'passInsertDebugMarker',
171
+ passHandleId: passHId,
172
+ markerLabel,
173
+ });
174
+ realPass.insertDebugMarker(markerLabel);
175
+ },
176
+ executeBundles(bundles) {
177
+ return realPass.executeBundles(bundles);
178
+ },
179
+ beginOcclusionQuery(queryIndex) {
180
+ return realPass.beginOcclusionQuery(queryIndex);
181
+ },
182
+ endOcclusionQuery() {
183
+ return realPass.endOcclusionQuery();
184
+ },
185
+ end() {
186
+ pushEvent(s, { kind: 'endRenderPass', passHandleId: passHId });
187
+ realPass.end();
188
+ },
189
+ };
190
+ }
191
+
192
+ export function createComputePassProxy(
193
+ s: RecorderInternal,
194
+ realPass: RhiComputePassEncoder,
195
+ passHId: HandleId,
196
+ ): RhiComputePassEncoder {
197
+ return {
198
+ setPipeline(pipeline: ComputePipeline) {
199
+ const pid = getHandleId(s, pipeline as object, 'computePipeline');
200
+ pushEvent(s, { kind: 'setComputePipeline', passHandleId: passHId, pipelineHandleId: pid });
201
+ realPass.setPipeline(pipeline);
202
+ },
203
+ setBindGroup(index, bindGroup, dynamicOffsets) {
204
+ const bgid = getHandleId(s, bindGroup as object, 'bindGroup');
205
+ const recordedDynamicOffsets =
206
+ dynamicOffsets === undefined ? undefined : Array.from(dynamicOffsets);
207
+ pushEvent(s, {
208
+ kind: 'setBindGroup',
209
+ passHandleId: passHId,
210
+ index,
211
+ bindGroupHandleId: bgid,
212
+ dynamicOffsets: recordedDynamicOffsets,
213
+ });
214
+ realPass.setBindGroup(index, bindGroup, dynamicOffsets);
215
+ },
216
+ dispatchWorkgroups(x, y, z) {
217
+ pushEvent(s, {
218
+ kind: 'dispatchWorkgroups',
219
+ passHandleId: passHId,
220
+ x,
221
+ y: y ?? 1,
222
+ z: z ?? 1,
223
+ });
224
+ realPass.dispatchWorkgroups(x, y, z);
225
+ },
226
+ dispatchWorkgroupsIndirect(indirectBuffer, indirectOffset) {
227
+ const bufferHandleId = getHandleId(s, indirectBuffer as object, 'buffer');
228
+ pushEvent(s, {
229
+ kind: 'dispatchWorkgroupsIndirect',
230
+ passHandleId: passHId,
231
+ indirectBufferHandleId: bufferHandleId,
232
+ indirectOffset,
233
+ });
234
+ realPass.dispatchWorkgroupsIndirect(indirectBuffer, indirectOffset);
235
+ },
236
+ end() {
237
+ pushEvent(s, { kind: 'endComputePass', passHandleId: passHId });
238
+ realPass.end();
239
+ },
240
+ };
241
+ }
@@ -0,0 +1,59 @@
1
+ import type { RhiCanvasContext, RhiDevice, RhiError, RhiInstance } from '@forgeax/engine-rhi';
2
+ import { err, ok, type Result } from '@forgeax/engine-types';
3
+ import { createRhiDebugError, type RhiDebugError } from '../errors';
4
+ import {
5
+ type CreateShaderModuleFn,
6
+ type DebugRhiInstance,
7
+ wrap,
8
+ wrapCreateShaderModule,
9
+ } from '../recorder';
10
+
11
+ export interface RecordableBackend {
12
+ readonly rhi: RhiInstance & {
13
+ readonly acquireCanvasContext?: (
14
+ canvas: HTMLCanvasElement | OffscreenCanvas,
15
+ ) => Result<RhiCanvasContext, RhiError>;
16
+ };
17
+ readonly createShaderModule: CreateShaderModuleFn;
18
+ }
19
+
20
+ export interface RecorderBackend extends RecordableBackend {
21
+ readonly unwrapDeviceForSurface: (device: RhiDevice) => Result<RhiDevice, RhiDebugError>;
22
+ }
23
+
24
+ export interface RecorderProxy {
25
+ readonly backend: RecorderBackend;
26
+ readonly recorder: DebugRhiInstance;
27
+ }
28
+
29
+ export function createRecorderProxy(backend: RecordableBackend): RecorderProxy {
30
+ const recorder = wrap(backend.rhi);
31
+ const wrappedCreateShaderModule = wrapCreateShaderModule(backend.createShaderModule, recorder);
32
+ const acquireCanvasContext = backend.rhi.acquireCanvasContext;
33
+ // Runtime's explicit-RHI seam receives the wrapped singleton as one value.
34
+ // Keep the standalone shader factory enumerable on that value so the
35
+ // renderer can preserve the complete typed backend pack after capture
36
+ // attachment; the public RhiInstance contract remains unchanged.
37
+ const rhi: RecordableBackend['rhi'] & { readonly createShaderModule: CreateShaderModuleFn } = {
38
+ requestAdapter: recorder.requestAdapter.bind(recorder),
39
+ createShaderModule: wrappedCreateShaderModule,
40
+ ...(acquireCanvasContext === undefined
41
+ ? {}
42
+ : { acquireCanvasContext: acquireCanvasContext.bind(backend.rhi) }),
43
+ };
44
+ const wrappedBackend: RecorderBackend = {
45
+ rhi,
46
+ createShaderModule: wrappedCreateShaderModule,
47
+ unwrapDeviceForSurface(device) {
48
+ const raw = (device as RhiDevice & { readonly _realDevice?: RhiDevice })._realDevice;
49
+ if (raw !== undefined) return ok(raw);
50
+ return err(
51
+ createRhiDebugError('capture-unavailable', {
52
+ stage: 'capture',
53
+ cause: 'the recorder backend does not expose a surface device resolver',
54
+ }),
55
+ );
56
+ },
57
+ };
58
+ return { backend: wrappedBackend, recorder };
59
+ }
@@ -0,0 +1,109 @@
1
+ // @forgeax/engine-rhi-debug/src/recorder/queue -- queue proxy owner.
2
+
3
+ import type { Buffer, CommandBuffer, RhiQueue } from '@forgeax/engine-rhi';
4
+ import type { RhiCallEventWriteTexture } from '../types';
5
+ import type { RecorderInternal } from './core';
6
+ import { getHandleId, pushEvent, shouldRecord, storeBlob } from './core';
7
+
8
+ export function createQueueProxy(s: RecorderInternal, realQueue: RhiQueue): RhiQueue {
9
+ return {
10
+ writeBuffer(
11
+ buffer: Buffer,
12
+ bufferOffset: number,
13
+ data: ArrayBufferView | ArrayBuffer,
14
+ dataOffset?: number,
15
+ size?: number,
16
+ ) {
17
+ // Idle fast-path: when not recording, skip getHandleId + slice + the
18
+ // storeBlob hash-and-double-copy entirely (they would only feed a
19
+ // pushEvent that the state gate drops, and blobPool is reset on arm()).
20
+ // Same-condition-as-pushEvent, so a recording call still records.
21
+ if (!shouldRecord(s)) {
22
+ return realQueue.writeBuffer(buffer, bufferOffset, data, dataOffset, size);
23
+ }
24
+ const hId = getHandleId(s, buffer as object, 'buffer');
25
+ const raw = ArrayBuffer.isView(data)
26
+ ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
27
+ : new Uint8Array(data as ArrayBuffer);
28
+ const sz = size ?? raw.byteLength - (dataOffset ?? 0);
29
+ const slice = raw.slice(dataOffset ?? 0, (dataOffset ?? 0) + sz);
30
+ const dataHash = storeBlob(s, slice.buffer as ArrayBuffer);
31
+
32
+ pushEvent(s, {
33
+ kind: 'writeBuffer',
34
+ handleId: hId,
35
+ bufferOffset,
36
+ dataHash,
37
+ size: sz,
38
+ });
39
+ return realQueue.writeBuffer(buffer, bufferOffset, data, dataOffset, size);
40
+ },
41
+
42
+ writeTexture(destination, data, dataLayout, copySize) {
43
+ // Idle fast-path (see writeBuffer): skip storeBlob hash+copy when the
44
+ // recorded event would be dropped anyway.
45
+ if (!shouldRecord(s)) {
46
+ return realQueue.writeTexture(destination, data, dataLayout, copySize);
47
+ }
48
+ const hId = getHandleId(s, destination.texture as object, 'texture');
49
+ const raw = ArrayBuffer.isView(data)
50
+ ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
51
+ : new Uint8Array(data as ArrayBuffer);
52
+ const dataHash = storeBlob(s, raw.buffer as ArrayBuffer);
53
+
54
+ pushEvent(s, {
55
+ kind: 'writeTexture',
56
+ destination: {
57
+ textureHandleId: hId,
58
+ mipLevel: destination.mipLevel,
59
+ origin: destination.origin,
60
+ aspect: destination.aspect,
61
+ },
62
+ dataHash,
63
+ dataLayout: {
64
+ offset: dataLayout.offset,
65
+ bytesPerRow: dataLayout.bytesPerRow,
66
+ rowsPerImage: dataLayout.rowsPerImage,
67
+ },
68
+ size: copySize,
69
+ } as RhiCallEventWriteTexture);
70
+ return realQueue.writeTexture(destination, data, dataLayout, copySize);
71
+ },
72
+
73
+ copyExternalImageToTexture(source, destination, copySize) {
74
+ if (!shouldRecord(s)) {
75
+ return realQueue.copyExternalImageToTexture(source, destination, copySize);
76
+ }
77
+ const hId = getHandleId(s, destination.texture as object, 'texture');
78
+ pushEvent(s, {
79
+ kind: 'copyExternalImageToTexture',
80
+ source: { origin: source.origin, flipY: source.flipY },
81
+ destination: {
82
+ textureHandleId: hId,
83
+ mipLevel: destination.mipLevel,
84
+ origin: destination.origin,
85
+ aspect: destination.aspect,
86
+ colorSpace: destination.colorSpace,
87
+ premultipliedAlpha: destination.premultipliedAlpha,
88
+ },
89
+ copySize,
90
+ });
91
+ return realQueue.copyExternalImageToTexture(source, destination, copySize);
92
+ },
93
+
94
+ submit(commandBuffers: readonly CommandBuffer[]) {
95
+ if (!shouldRecord(s)) {
96
+ return realQueue.submit(commandBuffers);
97
+ }
98
+ const cmdHandleIds = commandBuffers.map((cb) =>
99
+ getHandleId(s, cb as object, 'commandBuffer'),
100
+ );
101
+ pushEvent(s, { kind: 'submit', cmdHandleIds });
102
+ return realQueue.submit(commandBuffers);
103
+ },
104
+
105
+ onSubmittedWorkDone() {
106
+ return realQueue.onSubmittedWorkDone();
107
+ },
108
+ };
109
+ }
@@ -0,0 +1,53 @@
1
+ import type { DebugRhiInstance } from '../recorder';
2
+ import type { HandleId } from '../types';
3
+
4
+ export interface ResourceCandidate {
5
+ readonly handleId: HandleId;
6
+ readonly kind: 'buffer' | 'texture';
7
+ readonly estimatedBytes: number;
8
+ }
9
+
10
+ export class ResourceRegistry {
11
+ constructor(private readonly recorder: DebugRhiInstance) {}
12
+
13
+ candidates(): readonly ResourceCandidate[] {
14
+ return Array.from(this.recorder.descriptorTable(), ([handleId, descriptor]) => ({
15
+ handleId,
16
+ kind: descriptor.kind,
17
+ estimatedBytes: estimateBytes(descriptor),
18
+ }));
19
+ }
20
+
21
+ estimateSnapshotBytes(): number {
22
+ return this.candidates().reduce((total, candidate) => total + candidate.estimatedBytes, 0);
23
+ }
24
+
25
+ clearGeneration(): void {
26
+ this.recorder.transitionToError();
27
+ this.recorder.resetForDeviceLoss();
28
+ }
29
+ }
30
+
31
+ function estimateBytes(descriptor: {
32
+ readonly kind: 'buffer' | 'texture';
33
+ readonly size?: number | GPUExtent3DStrict;
34
+ }): number {
35
+ if (descriptor.kind === 'buffer') {
36
+ return typeof descriptor.size === 'number' ? descriptor.size : 0;
37
+ }
38
+ if (descriptor.size === undefined || typeof descriptor.size === 'number') return 4;
39
+ if (Array.isArray(descriptor.size)) {
40
+ return Math.max(
41
+ 1,
42
+ (descriptor.size[0] ?? 1) * (descriptor.size[1] ?? 1) * (descriptor.size[2] ?? 1) * 4,
43
+ );
44
+ }
45
+ if (!('width' in descriptor.size)) return 4;
46
+ return Math.max(
47
+ 1,
48
+ descriptor.size.width *
49
+ (descriptor.size.height ?? 1) *
50
+ (descriptor.size.depthOrArrayLayers ?? 1) *
51
+ 4,
52
+ );
53
+ }
@@ -0,0 +1,152 @@
1
+ import { err, ok, type Result } from '@forgeax/engine-types';
2
+ import { createRhiDebugError, type RhiDebugError } from '../errors';
3
+ import { assembleTape, type EncodedTape } from './assemble';
4
+ import { createRecorderProxy, type RecordableBackend, type RecorderBackend } from './proxy';
5
+ import { ResourceRegistry } from './resource-registry';
6
+ import { snapshotFrame } from './snapshot';
7
+
8
+ export interface CaptureFrameOptions {
9
+ readonly snapshotTimeoutMs?: number;
10
+ readonly byteBudget?: number;
11
+ readonly signal?: AbortSignal;
12
+ }
13
+
14
+ export interface RecorderOptions {
15
+ readonly snapshotTimeoutMs?: number;
16
+ readonly byteBudget?: number;
17
+ }
18
+
19
+ export interface RecorderAttachment {
20
+ readonly backend: RecorderBackend;
21
+ captureFrame(options?: CaptureFrameOptions): Promise<Result<EncodedTape, RhiDebugError>>;
22
+ frameBoundary(): Promise<Result<void, RhiDebugError>>;
23
+ deviceLost(): void;
24
+ dispose(): Promise<Result<void, RhiDebugError>>;
25
+ }
26
+
27
+ type Phase = 'idle' | 'armed' | 'snapshotting' | 'recording' | 'disposed';
28
+
29
+ export function attachRecorder(
30
+ backend: RecordableBackend,
31
+ options: RecorderOptions = {},
32
+ ): Result<RecorderAttachment, RhiDebugError> {
33
+ const proxy = createRecorderProxy(backend);
34
+ const registry = new ResourceRegistry(proxy.recorder);
35
+ let phase: Phase = 'idle';
36
+ let generation = 0;
37
+ let active:
38
+ | {
39
+ readonly generation: number;
40
+ readonly options: CaptureFrameOptions;
41
+ readonly resolve: (result: Result<EncodedTape, RhiDebugError>) => void;
42
+ readonly signal?: AbortSignal;
43
+ abortListener?: () => void;
44
+ }
45
+ | undefined;
46
+
47
+ const fail = <T>(
48
+ code: 'capture-unavailable' | 'capture-busy',
49
+ cause: string,
50
+ ): Result<T, RhiDebugError> => err(createRhiDebugError(code, { stage: 'capture', cause }));
51
+
52
+ const settle = (result: Result<EncodedTape, RhiDebugError>): void => {
53
+ const request = active;
54
+ if (request === undefined) return;
55
+ active = undefined;
56
+ request.signal?.removeEventListener('abort', request.abortListener ?? (() => {}));
57
+ if (!result.ok && proxy.recorder.getState() === 'error') proxy.recorder.disposeError();
58
+ phase = phase === 'disposed' ? 'disposed' : 'idle';
59
+ request.resolve(result);
60
+ };
61
+
62
+ const abort = (): void => {
63
+ if (active === undefined) return;
64
+ proxy.recorder.transitionToError();
65
+ settle(fail('capture-unavailable', 'capture request was aborted'));
66
+ };
67
+
68
+ const attachment: RecorderAttachment = {
69
+ backend: proxy.backend,
70
+ captureFrame(captureOptions = {}) {
71
+ if (phase === 'disposed')
72
+ return Promise.resolve(fail('capture-unavailable', 'recorder is disposed'));
73
+ if (active !== undefined)
74
+ return Promise.resolve(fail('capture-busy', 'another capture is active'));
75
+ const signal = captureOptions.signal;
76
+ if (signal?.aborted)
77
+ return Promise.resolve(fail('capture-unavailable', 'capture request was aborted'));
78
+ const arm = proxy.recorder.arm(1);
79
+ if (!arm.ok) return Promise.resolve(fail('capture-busy', arm.error.hint));
80
+ phase = 'armed';
81
+ generation += 1;
82
+ return new Promise<Result<EncodedTape, RhiDebugError>>((resolve) => {
83
+ const request = {
84
+ generation,
85
+ options: captureOptions,
86
+ resolve,
87
+ ...(signal === undefined ? {} : { signal }),
88
+ } as {
89
+ readonly generation: number;
90
+ readonly options: CaptureFrameOptions;
91
+ readonly resolve: (result: Result<EncodedTape, RhiDebugError>) => void;
92
+ readonly signal?: AbortSignal;
93
+ abortListener?: () => void;
94
+ };
95
+ request.abortListener = abort;
96
+ active = request;
97
+ signal?.addEventListener('abort', abort, { once: true });
98
+ });
99
+ },
100
+ async frameBoundary() {
101
+ if (phase === 'disposed' || active === undefined) return ok(undefined);
102
+ const request = active;
103
+ if (request.generation !== generation) return ok(undefined);
104
+ if (phase === 'armed') {
105
+ phase = 'snapshotting';
106
+ const result = await snapshotFrame(proxy.recorder, registry, {
107
+ snapshotTimeoutMs:
108
+ request.options.snapshotTimeoutMs ?? options.snapshotTimeoutMs ?? 30_000,
109
+ byteBudget: request.options.byteBudget ?? options.byteBudget ?? Number.MAX_SAFE_INTEGER,
110
+ });
111
+ if (!result.ok) {
112
+ proxy.recorder.transitionToError();
113
+ settle(err(result.error));
114
+ return err(result.error);
115
+ }
116
+ phase = 'recording';
117
+ return ok(undefined);
118
+ }
119
+ if (phase !== 'recording') return ok(undefined);
120
+ proxy.recorder.onFrameEnd();
121
+ if (proxy.recorder.getState() !== 'idle') return ok(undefined);
122
+ const result = assembleTape(proxy.recorder);
123
+ settle(result);
124
+ return result.ok ? ok(undefined) : err(result.error);
125
+ },
126
+ deviceLost() {
127
+ generation += 1;
128
+ registry.clearGeneration();
129
+ if (active !== undefined) {
130
+ phase = 'idle';
131
+ settle(fail('capture-unavailable', 'device loss invalidated the active generation'));
132
+ } else if (phase !== 'disposed') {
133
+ phase = 'idle';
134
+ }
135
+ },
136
+ async dispose() {
137
+ if (phase === 'disposed') return ok(undefined);
138
+ if (active !== undefined) {
139
+ proxy.recorder.transitionToError();
140
+ settle(fail('capture-unavailable', 'recorder was disposed during capture'));
141
+ }
142
+ proxy.recorder.disposeError();
143
+ phase = 'disposed';
144
+ return ok(undefined);
145
+ },
146
+ };
147
+ return ok(attachment);
148
+ }
149
+
150
+ export type { CreateShaderModuleFn } from '../recorder';
151
+ export type { EncodedTape } from './assemble';
152
+ export type { RecordableBackend, RecorderBackend } from './proxy';
@@ -0,0 +1,47 @@
1
+ // @forgeax/engine-rhi-debug/src/recorder/shader -- standalone shader factory proxy.
2
+
3
+ import type { Result, RhiDevice, ShaderModule } from '@forgeax/engine-rhi';
4
+ import type { HandleId } from '../types';
5
+ import type { CreateShaderModuleFn, DebugRhiInstance } from './core';
6
+
7
+ export function wrapCreateShaderModule(
8
+ originalFn: CreateShaderModuleFn,
9
+ debugInst: DebugRhiInstance,
10
+ ): CreateShaderModuleFn {
11
+ return async function wrappedCreateShaderModule(
12
+ device: RhiDevice,
13
+ desc: { code: string; label?: string | undefined },
14
+ ): Promise<Result<ShaderModule, import('@forgeax/engine-rhi').RhiError>> {
15
+ // The renderer threads the proxied RhiDevice (from proxyDevice()) here, but
16
+ // engine-rhi-webgpu's createShaderModule reverse-looks-up the GPUDevice via
17
+ // a WeakMap keyed on the RhiDevice that makeRhiDevice registered. The proxy
18
+ // is a different JS object, so WeakMap.get(proxy) is undefined and the real
19
+ // fn returns shader-compile-failed ("unregistered RhiDevice"). Unwrap the
20
+ // proxy to the registered device via the _realDevice escape hatch that
21
+ // proxyDevice exposes for exactly this purpose.
22
+ const realDevice = (device as RhiDevice & { _realDevice?: RhiDevice })._realDevice ?? device;
23
+ const result = await originalFn(realDevice, desc);
24
+ if (!result.ok) return result;
25
+
26
+ // I-12 (round 1 implement-review) fix: route the createShaderModule
27
+ // event through the same `pushExternalEvent` helper that wraps the
28
+ // internal `pushEvent` (and therefore the `_skipRecord` + state-machine
29
+ // guard). The previous `(events as RhiCallEvent[]).push(...)` cast
30
+ // bypassed those guards — recorder-internal RHI calls during shader
31
+ // construction would have self-polluted the tape.
32
+ const hId = debugInst.pushExternalCreateEvent(result.value, 'shaderModule', {
33
+ kind: 'createShaderModule',
34
+ handleId: '' as HandleId,
35
+ wgslCode: desc.code,
36
+ });
37
+ // Register the shader module handle in the recorder's handleMap so
38
+ // downstream pipeline events (createRenderPipeline / createComputePipeline)
39
+ // can resolve the handleId via getHandleId. Required for cross-device
40
+ // replay: the tape's pipeline desc carries live GPU module objects which
41
+ // are device-bound, so the replayer must swap them with re-created shader
42
+ // modules from the handleMap using the handleId.
43
+ debugInst.registerShaderModule(result.value, hId);
44
+
45
+ return result;
46
+ };
47
+ }