@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,192 @@
1
+ import { err, ok, type Result } from '@forgeax/engine-types';
2
+ import { sha256 } from '@noble/hashes/sha2.js';
3
+ import { bytesToHex } from '@noble/hashes/utils.js';
4
+ import { createRhiDebugError, type RhiDebugError } from '../errors';
5
+ import { encodeTape } from '../protocol/codec';
6
+ import type { BootstrapResource, Tape as V7Tape } from '../protocol/types';
7
+ import type { DebugRhiInstance } from '../recorder';
8
+ import type { HandleId, Tape as LegacyTape, RhiCallEvent } from '../types';
9
+
10
+ export interface EncodedTape {
11
+ readonly bytes: Uint8Array;
12
+ readonly digest: string;
13
+ readonly tape: V7Tape;
14
+ }
15
+
16
+ export function assembleTape(recorder: DebugRhiInstance): Result<EncodedTape, RhiDebugError> {
17
+ const legacy = recorder.getTape();
18
+ if (legacy === undefined) {
19
+ return err(
20
+ createRhiDebugError('capture-snapshot-failed', {
21
+ stage: 'snapshot',
22
+ cause: 'the recorder finalized without a frame event',
23
+ }),
24
+ );
25
+ }
26
+ if ('code' in legacy) {
27
+ return err(legacy);
28
+ }
29
+ const tape = toV7Tape(legacy, new Set(recorder.bootstrapEvents()));
30
+ const encoded = encodeTape(tape);
31
+ if (!encoded.ok) return err(encoded.error);
32
+ return ok({
33
+ bytes: encoded.value,
34
+ digest: `sha256:${bytesToHex(sha256(encoded.value))}`,
35
+ tape,
36
+ });
37
+ }
38
+
39
+ function toV7Tape(legacy: LegacyTape, bootstrapEvents: ReadonlySet<RhiCallEvent>): V7Tape {
40
+ const firstFrame = legacy.events.findIndex((event) => event.kind === 'frameMark');
41
+ const boundary = firstFrame < 0 ? legacy.events.length : firstFrame;
42
+ const bootstrap: BootstrapResource[] = [];
43
+ const bootstrapIds = new Set<HandleId>();
44
+ const initialData = new Map<HandleId, string[]>();
45
+ for (const event of legacy.events.slice(0, boundary)) {
46
+ if (event.kind === 'initialData') {
47
+ const hashes = initialData.get(event.handleId) ?? [];
48
+ hashes.push(event.dataHash);
49
+ initialData.set(event.handleId, hashes);
50
+ continue;
51
+ }
52
+ if (!bootstrapEvents.has(event)) continue;
53
+ const handleId = createdHandleId(event);
54
+ const kind = resourceKind(event);
55
+ if (handleId === undefined || kind === undefined || bootstrapIds.has(handleId)) continue;
56
+ bootstrapIds.add(handleId);
57
+ bootstrap.push({
58
+ handleId,
59
+ kind,
60
+ create: toJsonRecord(event),
61
+ initialData: [],
62
+ });
63
+ }
64
+ const allBlobs = Array.from(legacy.blobPool, ([hash, data]) => ({
65
+ hash,
66
+ bytes: new Uint8Array(data),
67
+ compression: 'none' as const,
68
+ }));
69
+ const referenced = collectReferencedHandleIds(legacy.events, bootstrapEvents);
70
+ const closure = collectBootstrapClosure(referenced, bootstrap);
71
+ const keptBootstrap = bootstrap
72
+ .filter((resource) => closure.has(resource.handleId))
73
+ .map((resource) => {
74
+ const hashes = initialData.get(resource.handleId) ?? [];
75
+ const slices = hashes.flatMap((hash) => {
76
+ const blob = legacy.blobPool.get(hash);
77
+ return blob === undefined ? [] : [{ hash, byteOffset: 0, byteLength: blob.byteLength }];
78
+ });
79
+ return { ...resource, initialData: slices };
80
+ });
81
+ const events = legacy.events
82
+ .filter((event) => event.kind !== 'initialData' && !bootstrapEvents.has(event))
83
+ .map(toJsonSafe);
84
+ const keptHashes = new Set(
85
+ keptBootstrap.flatMap((resource) => resource.initialData.map((slice) => slice.hash)),
86
+ );
87
+ for (const event of events) {
88
+ if ('dataHash' in event && typeof event.dataHash === 'string') keptHashes.add(event.dataHash);
89
+ }
90
+ const blobs = allBlobs.filter((blob) => keptHashes.has(blob.hash));
91
+ return {
92
+ header: {
93
+ formatVersion: 7,
94
+ rhiCaps: { ...legacy.rhiCapsRecorded },
95
+ eventCount: events.length,
96
+ blobCount: blobs.length,
97
+ },
98
+ bootstrap: keptBootstrap,
99
+ events,
100
+ blobs,
101
+ };
102
+ }
103
+
104
+ function collectReferencedHandleIds(
105
+ events: readonly RhiCallEvent[],
106
+ bootstrapEvents: ReadonlySet<RhiCallEvent>,
107
+ ): Set<HandleId> {
108
+ const ids = new Set<HandleId>();
109
+ for (const event of events) {
110
+ if (event.kind === 'initialData' || bootstrapEvents.has(event)) continue;
111
+ collectHandleStrings(event, ids);
112
+ }
113
+ return ids;
114
+ }
115
+
116
+ function collectHandleStrings(value: unknown, ids: Set<HandleId>): void {
117
+ if (typeof value === 'string' && /^[a-zA-Z][a-zA-Z-]*:\S+$/.test(value)) {
118
+ ids.add(value);
119
+ return;
120
+ }
121
+ if (Array.isArray(value)) {
122
+ for (const entry of value) collectHandleStrings(entry, ids);
123
+ return;
124
+ }
125
+ if (value !== null && typeof value === 'object') {
126
+ for (const entry of Object.values(value)) collectHandleStrings(entry, ids);
127
+ }
128
+ }
129
+
130
+ function collectBootstrapClosure(
131
+ referenced: ReadonlySet<HandleId>,
132
+ resources: readonly BootstrapResource[],
133
+ ): Set<HandleId> {
134
+ const byId = new Map(resources.map((resource) => [resource.handleId, resource]));
135
+ const closure = new Set<HandleId>();
136
+ const pending = [...referenced];
137
+ while (pending.length > 0) {
138
+ const handleId = pending.pop();
139
+ if (handleId === undefined || closure.has(handleId)) continue;
140
+ const resource = byId.get(handleId);
141
+ if (resource === undefined) continue;
142
+ closure.add(handleId);
143
+ const dependencies = new Set<HandleId>();
144
+ collectHandleStrings(resource.create, dependencies);
145
+ pending.push(...dependencies);
146
+ }
147
+ return closure;
148
+ }
149
+
150
+ function toJsonSafe(event: RhiCallEvent): RhiCallEvent {
151
+ return JSON.parse(JSON.stringify(event)) as RhiCallEvent;
152
+ }
153
+
154
+ function toJsonRecord(event: RhiCallEvent): Record<string, unknown> {
155
+ return JSON.parse(JSON.stringify(event));
156
+ }
157
+
158
+ function createdHandleId(event: RhiCallEvent): HandleId | undefined {
159
+ if (event.kind === 'createTextureView') return event.resultHandleId;
160
+ if (event.kind === 'createCommandEncoder') return event.cmdHandleId;
161
+ if (event.kind === 'beginRenderPass' || event.kind === 'beginComputePass') {
162
+ return event.passHandleId;
163
+ }
164
+ if (event.kind.startsWith('create') && 'handleId' in event) return event.handleId;
165
+ return undefined;
166
+ }
167
+
168
+ function resourceKind(event: RhiCallEvent): BootstrapResource['kind'] | undefined {
169
+ switch (event.kind) {
170
+ case 'createBuffer':
171
+ return 'buffer';
172
+ case 'createTexture':
173
+ return 'texture';
174
+ case 'createTextureView':
175
+ return 'texture-view';
176
+ case 'createSampler':
177
+ return 'sampler';
178
+ case 'createShaderModule':
179
+ return 'shader-module';
180
+ case 'createRenderPipeline':
181
+ case 'createComputePipeline':
182
+ return 'pipeline';
183
+ case 'createBindGroup':
184
+ case 'createBindGroupLayout':
185
+ case 'createPipelineLayout':
186
+ return 'binding';
187
+ case 'createCommandEncoder':
188
+ return 'encoder';
189
+ default:
190
+ return undefined;
191
+ }
192
+ }
@@ -0,0 +1,408 @@
1
+ // @forgeax/engine-rhi-debug/src/recorder/closure -- transitive handle closure owner.
2
+
3
+ import type { HandleId, RhiCallEvent } from '../types';
4
+
5
+ export function _collectFrameReferencedHandleIds(events: readonly RhiCallEvent[]): Set<HandleId> {
6
+ const refs = new Set<HandleId>();
7
+ for (const e of events) {
8
+ switch (e.kind) {
9
+ case 'writeBuffer':
10
+ case 'clearBuffer':
11
+ // initialData seeds a pre-arm resource's bytes; its handleId must be
12
+ // prefix-pulled so the resource's create* event lands in the bootstrap
13
+ // closure (otherwise the tape references a handle with no create event ->
14
+ // tape-invalid on deserialize).
15
+ case 'initialData': {
16
+ const we = e as { handleId: HandleId };
17
+ refs.add(we.handleId);
18
+ break;
19
+ }
20
+ case 'setVertexBuffer': {
21
+ const we = e as { passHandleId: HandleId; bufferHandleId: HandleId };
22
+ refs.add(we.passHandleId);
23
+ refs.add(we.bufferHandleId);
24
+ break;
25
+ }
26
+ case 'setIndexBuffer': {
27
+ const we = e as { passHandleId: HandleId; bufferHandleId: HandleId };
28
+ refs.add(we.passHandleId);
29
+ refs.add(we.bufferHandleId);
30
+ break;
31
+ }
32
+ case 'setPipeline': {
33
+ const we = e as { passHandleId: HandleId; pipelineHandleId: HandleId };
34
+ refs.add(we.passHandleId);
35
+ refs.add(we.pipelineHandleId);
36
+ break;
37
+ }
38
+ case 'setComputePipeline': {
39
+ const we = e as { passHandleId: HandleId; pipelineHandleId: HandleId };
40
+ refs.add(we.passHandleId);
41
+ refs.add(we.pipelineHandleId);
42
+ break;
43
+ }
44
+ case 'setBindGroup': {
45
+ const we = e as { passHandleId: HandleId; bindGroupHandleId: HandleId };
46
+ refs.add(we.passHandleId);
47
+ refs.add(we.bindGroupHandleId);
48
+ break;
49
+ }
50
+ case 'draw': {
51
+ const we = e as { passHandleId: HandleId };
52
+ refs.add(we.passHandleId);
53
+ break;
54
+ }
55
+ case 'drawIndexed': {
56
+ const we = e as { passHandleId: HandleId };
57
+ refs.add(we.passHandleId);
58
+ break;
59
+ }
60
+ case 'setViewport': {
61
+ const we = e as { passHandleId: HandleId };
62
+ refs.add(we.passHandleId);
63
+ break;
64
+ }
65
+ case 'setScissorRect': {
66
+ const we = e as { passHandleId: HandleId };
67
+ refs.add(we.passHandleId);
68
+ break;
69
+ }
70
+ case 'setBlendConstant': {
71
+ const we = e as { passHandleId: HandleId };
72
+ refs.add(we.passHandleId);
73
+ break;
74
+ }
75
+ case 'setStencilReference': {
76
+ const we = e as { passHandleId: HandleId };
77
+ refs.add(we.passHandleId);
78
+ break;
79
+ }
80
+ case 'drawIndirect': {
81
+ const we = e as { passHandleId: HandleId; indirectBufferHandleId: HandleId };
82
+ refs.add(we.passHandleId);
83
+ refs.add(we.indirectBufferHandleId);
84
+ break;
85
+ }
86
+ case 'drawIndexedIndirect': {
87
+ const we = e as { passHandleId: HandleId; indirectBufferHandleId: HandleId };
88
+ refs.add(we.passHandleId);
89
+ refs.add(we.indirectBufferHandleId);
90
+ break;
91
+ }
92
+ case 'passPushDebugGroup': {
93
+ const we = e as { passHandleId: HandleId };
94
+ refs.add(we.passHandleId);
95
+ break;
96
+ }
97
+ case 'passPopDebugGroup': {
98
+ const we = e as { passHandleId: HandleId };
99
+ refs.add(we.passHandleId);
100
+ break;
101
+ }
102
+ case 'passInsertDebugMarker': {
103
+ const we = e as { passHandleId: HandleId };
104
+ refs.add(we.passHandleId);
105
+ break;
106
+ }
107
+ case 'endRenderPass': {
108
+ const we = e as { passHandleId: HandleId };
109
+ refs.add(we.passHandleId);
110
+ break;
111
+ }
112
+ case 'dispatchWorkgroups': {
113
+ const we = e as { passHandleId: HandleId };
114
+ refs.add(we.passHandleId);
115
+ break;
116
+ }
117
+ case 'dispatchWorkgroupsIndirect': {
118
+ const we = e as { passHandleId: HandleId; indirectBufferHandleId: HandleId };
119
+ refs.add(we.passHandleId);
120
+ refs.add(we.indirectBufferHandleId);
121
+ break;
122
+ }
123
+ case 'endComputePass': {
124
+ const we = e as { passHandleId: HandleId };
125
+ refs.add(we.passHandleId);
126
+ break;
127
+ }
128
+ case 'submit': {
129
+ const we = e as { cmdHandleIds: readonly HandleId[] };
130
+ for (const id of we.cmdHandleIds) refs.add(id);
131
+ break;
132
+ }
133
+ case 'beginRenderPass': {
134
+ const we = e as {
135
+ cmdHandleId: HandleId;
136
+ colorAttachmentViewHandleIds: readonly (HandleId | undefined)[];
137
+ depthStencilViewHandleId?: HandleId;
138
+ };
139
+ refs.add(we.cmdHandleId);
140
+ for (const vhId of we.colorAttachmentViewHandleIds) {
141
+ if (vhId !== undefined) refs.add(vhId);
142
+ }
143
+ if (we.depthStencilViewHandleId !== undefined) refs.add(we.depthStencilViewHandleId);
144
+ break;
145
+ }
146
+ case 'beginComputePass': {
147
+ const we = e as { cmdHandleId: HandleId };
148
+ refs.add(we.cmdHandleId);
149
+ break;
150
+ }
151
+ case 'finish': {
152
+ const we = e as { cmdHandleId: HandleId };
153
+ refs.add(we.cmdHandleId);
154
+ break;
155
+ }
156
+ case 'pushDebugGroup':
157
+ case 'popDebugGroup':
158
+ case 'insertDebugMarker': {
159
+ const we = e as { cmdHandleId: HandleId };
160
+ refs.add(we.cmdHandleId);
161
+ break;
162
+ }
163
+ case 'writeTexture': {
164
+ const we = e as { destination: { textureHandleId: HandleId } };
165
+ refs.add(we.destination.textureHandleId);
166
+ break;
167
+ }
168
+ case 'copyExternalImageToTexture': {
169
+ const we = e as { destination: { textureHandleId: HandleId } };
170
+ refs.add(we.destination.textureHandleId);
171
+ break;
172
+ }
173
+ case 'copyBufferToBuffer': {
174
+ const we = e as { sourceHandleId: HandleId; destinationHandleId: HandleId };
175
+ refs.add(we.sourceHandleId);
176
+ refs.add(we.destinationHandleId);
177
+ break;
178
+ }
179
+ case 'copyBufferToTexture': {
180
+ const we = e as {
181
+ source: { bufferHandleId: HandleId };
182
+ destination: { textureHandleId: HandleId };
183
+ };
184
+ refs.add(we.source.bufferHandleId);
185
+ refs.add(we.destination.textureHandleId);
186
+ break;
187
+ }
188
+ case 'copyTextureToBuffer': {
189
+ const we = e as {
190
+ source: { textureHandleId: HandleId };
191
+ destination: { bufferHandleId: HandleId };
192
+ };
193
+ refs.add(we.source.textureHandleId);
194
+ refs.add(we.destination.bufferHandleId);
195
+ break;
196
+ }
197
+ case 'copyTextureToTexture': {
198
+ const we = e as {
199
+ source: { textureHandleId: HandleId };
200
+ destination: { textureHandleId: HandleId };
201
+ };
202
+ refs.add(we.source.textureHandleId);
203
+ refs.add(we.destination.textureHandleId);
204
+ break;
205
+ }
206
+ case 'createBindGroup':
207
+ case 'createPipelineLayout':
208
+ case 'createRenderPipeline':
209
+ case 'createComputePipeline':
210
+ case 'createTextureView': {
211
+ // An in-frame-created resource may reference a PRE-ARM resource via its
212
+ // backward edges (e.g. a composite/FXAA bind group built mid-frame that
213
+ // samples a scratch TextureView created at setup; or an in-frame
214
+ // createTextureView of a pre-arm texture). Those pre-arm handles are
215
+ // reachable ONLY through this create* event's backward refs — no usage
216
+ // event names them directly — so without collecting them here they never
217
+ // become prefix seeds and the tape deserializes as non-self-contained
218
+ // (tape-invalid). Collect the edges; getTape's prefixSeedIds
219
+ // filter then drops entries that are themselves in-frame declared, leaving
220
+ // only the genuinely pre-arm dependencies to seed the bootstrap closure.
221
+ for (const ref of _getCreateEventReferencedHandleIds(e)) refs.add(ref);
222
+ break;
223
+ }
224
+ case 'destroyBuffer':
225
+ case 'destroyTexture':
226
+ refs.add(e.handleId);
227
+ break;
228
+ case 'frameMark':
229
+ case 'createBuffer':
230
+ case 'createTexture':
231
+ case 'createSampler':
232
+ case 'createBindGroupLayout':
233
+ case 'createShaderModule':
234
+ case 'createCommandEncoder':
235
+ // Leaf declaration events — no backward references to collect.
236
+ break;
237
+ default: {
238
+ // Exhaustiveness guard: if a new RhiCallEvent member is added to the
239
+ // union without a corresponding handle-collection case, tsc fails here.
240
+ // This prevents silent omission of handle references (tape-invalid).
241
+ const _exhaustive: never = e;
242
+ void _exhaustive;
243
+ break;
244
+ }
245
+ }
246
+ }
247
+ return refs;
248
+ }
249
+
250
+ /**
251
+ * @internal
252
+ * Return handleIds referenced by a create* event for transitive closure traversal.
253
+ *
254
+ * The edge set follows D-3 (plan-strategy 2):
255
+ * - createBindGroup → layoutHandleId + resourceHandleIds
256
+ * - createPipelineLayout → bglHandleIds
257
+ * - createRenderPipeline → layoutHandleId (if != 'layout:auto') + vertex/fragmentShaderModuleHandleId (R-1)
258
+ * - createComputePipeline → layoutHandleId (if != 'layout:auto') + computeShaderModuleHandleId (R-1)
259
+ * - createTextureView → sourceHandleId
260
+ * Leaf resources (buffer / texture / sampler / BGL / shaderModule) return empty.
261
+ */
262
+ export function _getCreateEventReferencedHandleIds(event: RhiCallEvent): HandleId[] {
263
+ switch (event.kind) {
264
+ case 'createBindGroup': {
265
+ const e = event as { layoutHandleId: HandleId; resourceHandleIds: readonly HandleId[] };
266
+ return [e.layoutHandleId, ...e.resourceHandleIds];
267
+ }
268
+ case 'createPipelineLayout': {
269
+ const e = event as { bglHandleIds: readonly HandleId[] };
270
+ return [...e.bglHandleIds];
271
+ }
272
+ case 'createRenderPipeline': {
273
+ const e = event as {
274
+ layoutHandleId: HandleId;
275
+ vertexShaderModuleHandleId?: HandleId;
276
+ fragmentShaderModuleHandleId?: HandleId;
277
+ };
278
+ const refs: HandleId[] = [];
279
+ if (e.layoutHandleId !== 'layout:auto') refs.push(e.layoutHandleId);
280
+ if (e.vertexShaderModuleHandleId !== undefined) refs.push(e.vertexShaderModuleHandleId);
281
+ if (e.fragmentShaderModuleHandleId !== undefined) refs.push(e.fragmentShaderModuleHandleId);
282
+ return refs;
283
+ }
284
+ case 'createComputePipeline': {
285
+ const e = event as { layoutHandleId: HandleId; computeShaderModuleHandleId?: HandleId };
286
+ const refs: HandleId[] = [];
287
+ if (e.layoutHandleId !== 'layout:auto') refs.push(e.layoutHandleId);
288
+ if (e.computeShaderModuleHandleId !== undefined) refs.push(e.computeShaderModuleHandleId);
289
+ return refs;
290
+ }
291
+ case 'createTextureView': {
292
+ const e = event as { sourceHandleId: HandleId };
293
+ return [e.sourceHandleId];
294
+ }
295
+ case 'createBuffer':
296
+ case 'createTexture':
297
+ case 'createSampler':
298
+ case 'createBindGroupLayout':
299
+ case 'createShaderModule':
300
+ case 'createCommandEncoder':
301
+ return [];
302
+ default:
303
+ return [];
304
+ }
305
+ }
306
+
307
+ /**
308
+ * @internal
309
+ * Compute the transitive closure of handleIds from bootstrapCreates.
310
+ *
311
+ * Starting from the given seed set, recursively walks all referenced handleIds
312
+ * via _getCreateEventReferencedHandleIds. Returns the set of all handleIds
313
+ * whose create events must be included in the tape prefix for self-containment.
314
+ *
315
+ * If a referenced handleId is not found in bootstrapCreates, returns
316
+ * `null` for that id — the caller should produce a structured tape error.
317
+ */
318
+ export function _computeClosure(
319
+ seedHandleIds: Set<HandleId>,
320
+ bootstrapCreates: Map<HandleId, RhiCallEvent>,
321
+ inFrameHandleIds: Set<HandleId>,
322
+ ): { closure: Set<HandleId>; missing: HandleId | null } {
323
+ const closure = new Set(seedHandleIds);
324
+ const queue = [...seedHandleIds];
325
+
326
+ while (queue.length > 0) {
327
+ const current: HandleId | undefined = queue.shift();
328
+ if (current === undefined) break;
329
+ const createEvent = bootstrapCreates.get(current);
330
+ if (createEvent === undefined) {
331
+ // The handle is not in bootstrapCreates. If it is declared in
332
+ // s.events (e.g. swapchain textures from getCurrentTexture), treat it
333
+ // as a leaf — no further expansion needed.
334
+ if (inFrameHandleIds.has(current)) continue;
335
+ return { closure, missing: current };
336
+ }
337
+ const edges = _getCreateEventReferencedHandleIds(createEvent);
338
+ for (const target of edges) {
339
+ if (!closure.has(target)) {
340
+ closure.add(target);
341
+ queue.push(target);
342
+ }
343
+ }
344
+ }
345
+ return { closure, missing: null };
346
+ }
347
+
348
+ /**
349
+ * @internal
350
+ * Topologically sort the closure set so that dependencies appear before dependents.
351
+ *
352
+ * Builds a dep-graph: if event A references handleId of event B, then B must
353
+ * appear before A. Uses Kahn's algorithm.
354
+ */
355
+ export function _topoSortClosure(
356
+ closure: Set<HandleId>,
357
+ bootstrapCreates: Map<HandleId, RhiCallEvent>,
358
+ ): RhiCallEvent[] {
359
+ const inDegree = new Map<HandleId, number>();
360
+ const dependents = new Map<HandleId, HandleId[]>();
361
+
362
+ for (const hId of closure) {
363
+ inDegree.set(hId, 0);
364
+ dependents.set(hId, []);
365
+ }
366
+
367
+ for (const hId of closure) {
368
+ const event = bootstrapCreates.get(hId);
369
+ if (event === undefined) continue;
370
+ const edges = _getCreateEventReferencedHandleIds(event);
371
+ for (const target of edges) {
372
+ if (closure.has(target)) {
373
+ // hId depends on target
374
+ const current = dependents.get(target);
375
+ if (current !== undefined) current.push(hId);
376
+ inDegree.set(hId, (inDegree.get(hId) ?? 0) + 1);
377
+ }
378
+ }
379
+ }
380
+
381
+ const queue: HandleId[] = [];
382
+ for (const [hId, deg] of inDegree) {
383
+ if (deg === 0) queue.push(hId);
384
+ }
385
+
386
+ const sorted: RhiCallEvent[] = [];
387
+ while (queue.length > 0) {
388
+ const current: HandleId | undefined = queue.shift();
389
+ if (current === undefined) break;
390
+ const event = bootstrapCreates.get(current);
391
+ if (event !== undefined) sorted.push(event);
392
+
393
+ const deps = dependents.get(current);
394
+ if (deps !== undefined) {
395
+ for (const dep of deps) {
396
+ const newDeg = (inDegree.get(dep) ?? 1) - 1;
397
+ inDegree.set(dep, newDeg);
398
+ if (newDeg === 0) queue.push(dep);
399
+ }
400
+ }
401
+ }
402
+
403
+ return sorted;
404
+ }
405
+
406
+ // ============================================================================
407
+ // DebugRhiInstance — public interface
408
+ // ============================================================================