@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,144 @@
1
+ // resolve-texture-descriptor.unit.test.ts — SSOT "tape handle -> texture descriptor".
2
+ //
3
+ // resolveTextureDescriptor is the one resolver both the RT path (resolveAttachmentSize /
4
+ // readbackDrawRt) and the viewer's depth + bound-texture preview paths share. It walks
5
+ // createTextureView resultHandleId -> sourceHandleId -> createTexture, falling back to the
6
+ // id itself for direct texture handles, and reads size from the raw createTexture event
7
+ // (The raw event remains the authoritative source for texture dimensions.)
8
+
9
+ import { describe, expect, it } from 'vitest';
10
+ import { resolveAttachmentSize, resolveTextureDescriptor } from '../readback';
11
+ import type { RhiCallEvent } from '../types';
12
+
13
+ describe('resolveTextureDescriptor', () => {
14
+ it('resolves a view handle through its source createTexture (size from event)', () => {
15
+ const events: RhiCallEvent[] = [
16
+ {
17
+ kind: 'createTexture',
18
+ handleId: 'tex-src',
19
+ desc: { size: [256, 128, 1], format: 'rgba8unorm', usage: 0x14 },
20
+ } as RhiCallEvent,
21
+ {
22
+ kind: 'createTextureView',
23
+ sourceHandleId: 'tex-src',
24
+ resultHandleId: 'view-1',
25
+ desc: { dimension: '2d' },
26
+ } as RhiCallEvent,
27
+ ];
28
+ expect(resolveTextureDescriptor(events, 'view-1')).toEqual({
29
+ handleId: 'tex-src',
30
+ width: 256,
31
+ height: 128,
32
+ format: 'rgba8unorm',
33
+ dimension: '2d',
34
+ arrayLayers: 1,
35
+ });
36
+ });
37
+
38
+ it('resolves a direct texture handle when no view event exists', () => {
39
+ const events: RhiCallEvent[] = [
40
+ {
41
+ kind: 'createTexture',
42
+ handleId: 'tex-1',
43
+ desc: { size: { width: 512, height: 512 }, format: 'bgra8unorm', usage: 0x14 },
44
+ } as RhiCallEvent,
45
+ ];
46
+ expect(resolveTextureDescriptor(events, 'tex-1')).toEqual({
47
+ handleId: 'tex-1',
48
+ width: 512,
49
+ height: 512,
50
+ format: 'bgra8unorm',
51
+ dimension: '2d',
52
+ arrayLayers: 1,
53
+ });
54
+ });
55
+
56
+ it('captures the view dimension (cube) over the texture dimension', () => {
57
+ const events: RhiCallEvent[] = [
58
+ {
59
+ kind: 'createTexture',
60
+ handleId: 'cube-src',
61
+ desc: { size: [64, 64, 6], format: 'rgba8unorm', dimension: '2d', usage: 0x14 },
62
+ } as RhiCallEvent,
63
+ {
64
+ kind: 'createTextureView',
65
+ sourceHandleId: 'cube-src',
66
+ resultHandleId: 'cube-view',
67
+ desc: { dimension: 'cube' },
68
+ } as RhiCallEvent,
69
+ ];
70
+ expect(resolveTextureDescriptor(events, 'cube-view')?.dimension).toBe('cube');
71
+ });
72
+
73
+ it('defaults height to width for a square [w] size array', () => {
74
+ const events: RhiCallEvent[] = [
75
+ {
76
+ kind: 'createTexture',
77
+ handleId: 'tex-sq',
78
+ desc: { size: [320], format: 'rgba8unorm', usage: 0x14 },
79
+ } as RhiCallEvent,
80
+ ];
81
+ const desc = resolveTextureDescriptor(events, 'tex-sq');
82
+ expect(desc?.width).toBe(320);
83
+ expect(desc?.height).toBe(320);
84
+ });
85
+
86
+ it('extracts arrayLayers (depthOrArrayLayers) from an array-size texture', () => {
87
+ const events: RhiCallEvent[] = [
88
+ {
89
+ kind: 'createTexture',
90
+ handleId: 'atlas',
91
+ // cube-array shadow atlas: 512x512, 6 faces x 4 layers = 24 array layers.
92
+ desc: { size: [512, 512, 24], format: 'depth32float', dimension: '2d', usage: 0x14 },
93
+ } as RhiCallEvent,
94
+ ];
95
+ expect(resolveTextureDescriptor(events, 'atlas')?.arrayLayers).toBe(24);
96
+ });
97
+
98
+ it('extracts arrayLayers from an object-size texture (depthOrArrayLayers)', () => {
99
+ const events: RhiCallEvent[] = [
100
+ {
101
+ kind: 'createTexture',
102
+ handleId: 'arr',
103
+ desc: {
104
+ size: { width: 256, height: 256, depthOrArrayLayers: 4 },
105
+ format: 'rgba8unorm',
106
+ usage: 0x14,
107
+ },
108
+ } as RhiCallEvent,
109
+ ];
110
+ expect(resolveTextureDescriptor(events, 'arr')?.arrayLayers).toBe(4);
111
+ });
112
+
113
+ it('defaults arrayLayers to 1 for a plain 2D texture', () => {
114
+ const events: RhiCallEvent[] = [
115
+ {
116
+ kind: 'createTexture',
117
+ handleId: 'plain',
118
+ desc: { size: { width: 128, height: 128 }, format: 'rgba8unorm', usage: 0x14 },
119
+ } as RhiCallEvent,
120
+ ];
121
+ expect(resolveTextureDescriptor(events, 'plain')?.arrayLayers).toBe(1);
122
+ });
123
+
124
+ it('returns null when no createTexture declares the resolved handle', () => {
125
+ expect(resolveTextureDescriptor([], 'missing')).toBeNull();
126
+ });
127
+ });
128
+
129
+ describe('resolveAttachmentSize (thin wrapper)', () => {
130
+ it('returns the resolved dimensions', () => {
131
+ const events: RhiCallEvent[] = [
132
+ {
133
+ kind: 'createTexture',
134
+ handleId: 'rt',
135
+ desc: { size: [800, 600, 1], format: 'rgba8unorm', usage: 0x14 },
136
+ } as RhiCallEvent,
137
+ ];
138
+ expect(resolveAttachmentSize(events, 'rt')).toEqual({ width: 800, height: 600 });
139
+ });
140
+
141
+ it('falls back to 512x512 when unresolved', () => {
142
+ expect(resolveAttachmentSize([], 'nope')).toEqual({ width: 512, height: 512 });
143
+ });
144
+ });
@@ -0,0 +1,114 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildFrameModel, buildResourceLifecycle } from '../frame-model';
3
+ import { decodeTape, encodeTape } from '../protocol/codec';
4
+ import type { RhiCallEvent, Tape } from '../protocol/types';
5
+
6
+ const events: readonly RhiCallEvent[] = [
7
+ { kind: 'createBuffer', handleId: 'buf:1', desc: { size: 64, usage: 4 } },
8
+ {
9
+ kind: 'createTexture',
10
+ handleId: 'tex:1',
11
+ desc: {
12
+ size: { width: 4, height: 4, depthOrArrayLayers: 2 },
13
+ format: 'rgba16float',
14
+ mipLevelCount: 2,
15
+ usage: 1,
16
+ },
17
+ },
18
+ {
19
+ kind: 'createTexture',
20
+ handleId: 'depth:1',
21
+ desc: { size: { width: 4, height: 4 }, format: 'depth24plus', usage: 4 },
22
+ },
23
+ { kind: 'createTextureView', sourceHandleId: 'tex:1', resultHandleId: 'view:1', desc: {} },
24
+ { kind: 'destroyBuffer', handleId: 'buf:1' },
25
+ ];
26
+
27
+ function makeTape(tapeEvents: readonly RhiCallEvent[] = events): Tape {
28
+ return {
29
+ header: { formatVersion: 7, rhiCaps: {}, eventCount: tapeEvents.length, blobCount: 0 },
30
+ bootstrap: [],
31
+ events: tapeEvents,
32
+ blobs: [],
33
+ };
34
+ }
35
+
36
+ describe('resource lifecycle attribution', () => {
37
+ it('joins create/destroy events and keeps unavailable bytes explicit', () => {
38
+ const report = buildResourceLifecycle(events);
39
+
40
+ expect(report.scope).toBe('captured-tape-resource-closure');
41
+ expect(report.counts).toEqual({
42
+ created: 4,
43
+ destroyed: 1,
44
+ live: 3,
45
+ destroyEvents: 1,
46
+ unknownDestroyEvents: 0,
47
+ });
48
+ expect(report.bytes).toEqual({
49
+ knownCreated: 384,
50
+ knownDestroyed: 64,
51
+ knownLive: 320,
52
+ unavailableCreated: 2,
53
+ unavailableDestroyed: 0,
54
+ unavailableLive: 2,
55
+ });
56
+ expect(report.availability).toEqual({
57
+ destroy: 'observed-buffer-texture',
58
+ retire: 'unavailable',
59
+ driverAllocation: 'unavailable',
60
+ });
61
+ expect(report.resources.find((resource) => resource.handleId === 'buf:1')).toMatchObject({
62
+ kind: 'buffer',
63
+ state: 'destroyed',
64
+ byteEstimate: { status: 'known', bytes: 64, basis: 'buffer-descriptor' },
65
+ });
66
+ expect(
67
+ report.resources.find((resource) => resource.handleId === 'depth:1')?.byteEstimate,
68
+ ).toEqual({
69
+ status: 'unavailable',
70
+ reason: 'unsupported-texture-format',
71
+ });
72
+ });
73
+
74
+ it('is part of the same FrameModel used by the public summary', () => {
75
+ const model = buildFrameModel(makeTape());
76
+ expect(model.resourceLifecycle.counts.live).toBe(3);
77
+ expect(model.resourceLifecycle.resources).toHaveLength(4);
78
+ });
79
+
80
+ it('round-trips destroy events in the v7 tape format', () => {
81
+ const encoded = encodeTape(makeTape());
82
+ expect(encoded.ok).toBe(true);
83
+ if (!encoded.ok) return;
84
+ const decoded = decodeTape(encoded.value);
85
+ expect(decoded.ok).toBe(true);
86
+ if (decoded.ok)
87
+ expect(decoded.value.events.at(-1)).toEqual({ kind: 'destroyBuffer', handleId: 'buf:1' });
88
+ });
89
+
90
+ it('keeps swapchain textures out of engine-owned attribution', () => {
91
+ const report = buildResourceLifecycle([
92
+ {
93
+ kind: 'createTexture',
94
+ handleId: 'swapchain:1',
95
+ origin: 'swapchain',
96
+ desc: {
97
+ size: { width: 1280, height: 720, depthOrArrayLayers: 1 },
98
+ format: 'bgra8unorm',
99
+ usage: 1,
100
+ },
101
+ },
102
+ {
103
+ kind: 'createTextureView',
104
+ sourceHandleId: 'swapchain:1',
105
+ resultHandleId: 'view:1',
106
+ desc: {},
107
+ },
108
+ ]);
109
+
110
+ expect(report.originBreakdown.engine.created).toBe(0);
111
+ expect(report.originBreakdown.swapchain.created).toBe(2);
112
+ expect(report.resources.every((resource) => resource.origin === 'swapchain')).toBe(true);
113
+ });
114
+ });
@@ -0,0 +1,178 @@
1
+ /// <reference types="@webgpu/types" />
2
+
3
+ import type { RhiDevice, RhiInstance } from '@forgeax/engine-rhi';
4
+ import { describe, expect, it } from 'vitest';
5
+ import { decodeTape } from '../protocol/codec';
6
+ import { readbackTexturePixels } from '../readback';
7
+ import {
8
+ type CreateShaderModuleFn,
9
+ type DebugRhiInstance,
10
+ wrap,
11
+ wrapCreateShaderModule,
12
+ } from '../recorder';
13
+ import { assembleTape } from '../recorder/assemble';
14
+ import { openReplay, type ReplayBackend } from '../replay/session';
15
+
16
+ interface DawnPack {
17
+ readonly rhi: RhiInstance;
18
+ readonly createShaderModule: CreateShaderModuleFn;
19
+ }
20
+
21
+ const SKIP_DAWN = process.env.FORGEAX_SKIP_DAWN === '1';
22
+ const WIDTH = 32;
23
+ const HEIGHT = 32;
24
+
25
+ function normalizedPixelDelta(left: Uint8Array, right: Uint8Array): number {
26
+ if (left.byteLength !== right.byteLength || left.byteLength === 0) return 1;
27
+ let total = 0;
28
+ for (let index = 0; index < left.byteLength; index += 1) {
29
+ total += Math.abs((left[index] ?? 0) - (right[index] ?? 0));
30
+ }
31
+ return total / (left.byteLength * 255);
32
+ }
33
+
34
+ const VERTEX_SHADER = `
35
+ @vertex
36
+ fn main(@builtin(vertex_index) vertexIndex: u32) -> @builtin(position) vec4<f32> {
37
+ var positions = array<vec2<f32>, 3>(vec2<f32>(0.0, 0.7), vec2<f32>(-0.7, -0.7), vec2<f32>(0.7, -0.7));
38
+ return vec4<f32>(positions[vertexIndex], 0.0, 1.0);
39
+ }`;
40
+
41
+ const FRAGMENT_SHADER = `
42
+ @fragment
43
+ fn main() -> @location(0) vec4<f32> {
44
+ return vec4<f32>(0.0, 0.8, 0.2, 1.0);
45
+ }`;
46
+
47
+ async function loadDawn(): Promise<DawnPack> {
48
+ return (await import('@forgeax/engine-rhi-webgpu')) as unknown as DawnPack;
49
+ }
50
+
51
+ async function makeRecording(pack: DawnPack): Promise<{
52
+ readonly recorder: DebugRhiInstance;
53
+ readonly device: RhiDevice;
54
+ readonly texture: import('@forgeax/engine-rhi').Texture;
55
+ }> {
56
+ const recorder = wrap(pack.rhi);
57
+ const createShaderModule = wrapCreateShaderModule(pack.createShaderModule, recorder);
58
+ const adapter = await recorder.requestAdapter();
59
+ expect(adapter.ok).toBe(true);
60
+ if (!adapter.ok) throw new Error(`Dawn admission failed: ${adapter.error.code}`);
61
+ const deviceResult = await adapter.value.requestDevice();
62
+ expect(deviceResult.ok).toBe(true);
63
+ if (!deviceResult.ok) throw new Error(`Dawn device admission failed: ${deviceResult.error.code}`);
64
+ const device = deviceResult.value;
65
+ const rawDevice =
66
+ (device as RhiDevice & { readonly _realDevice?: RhiDevice })._realDevice ?? device;
67
+ const armed = recorder.arm(1);
68
+ expect(armed.ok).toBe(true);
69
+ if (!armed.ok) throw new Error(armed.error.hint);
70
+
71
+ const textureResult = device.createTexture({
72
+ size: { width: WIDTH, height: HEIGHT, depthOrArrayLayers: 1 },
73
+ format: 'rgba8unorm',
74
+ usage: 0x11,
75
+ });
76
+ expect(textureResult.ok).toBe(true);
77
+ if (!textureResult.ok) throw new Error(textureResult.error.hint);
78
+ const texture = textureResult.value;
79
+ const viewResult = device.createTextureView(texture, {});
80
+ expect(viewResult.ok).toBe(true);
81
+ if (!viewResult.ok) throw new Error(viewResult.error.hint);
82
+ const vertex = await createShaderModule(rawDevice, { code: VERTEX_SHADER });
83
+ expect(vertex.ok).toBe(true);
84
+ if (!vertex.ok) throw new Error(vertex.error.hint);
85
+ const fragment = await createShaderModule(rawDevice, { code: FRAGMENT_SHADER });
86
+ expect(fragment.ok).toBe(true);
87
+ if (!fragment.ok) throw new Error(fragment.error.hint);
88
+ const bindGroupLayout = device.createBindGroupLayout({ entries: [] });
89
+ expect(bindGroupLayout.ok).toBe(true);
90
+ if (!bindGroupLayout.ok) throw new Error(bindGroupLayout.error.hint);
91
+ const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout.value] });
92
+ expect(pipelineLayout.ok).toBe(true);
93
+ if (!pipelineLayout.ok) throw new Error(pipelineLayout.error.hint);
94
+ const pipeline = device.createRenderPipeline({
95
+ layout: pipelineLayout.value,
96
+ vertex: { module: vertex.value, entryPoint: 'main', buffers: [] },
97
+ fragment: { module: fragment.value, entryPoint: 'main', targets: [{ format: 'rgba8unorm' }] },
98
+ primitive: { topology: 'triangle-list' },
99
+ } as never);
100
+ expect(pipeline.ok).toBe(true);
101
+ if (!pipeline.ok) throw new Error(pipeline.error.hint);
102
+ const encoderResult = device.createCommandEncoder({});
103
+ expect(encoderResult.ok).toBe(true);
104
+ if (!encoderResult.ok) throw new Error(encoderResult.error.hint);
105
+ const pass = encoderResult.value.beginRenderPass({
106
+ colorAttachments: [
107
+ {
108
+ view: viewResult.value,
109
+ clearValue: { r: 0, g: 0, b: 0, a: 1 },
110
+ loadOp: 'clear',
111
+ storeOp: 'store',
112
+ },
113
+ ],
114
+ } as never);
115
+ pass.setPipeline(pipeline.value);
116
+ pass.draw(3, 1, 0, 0);
117
+ pass.end();
118
+ const command = encoderResult.value.finish();
119
+ expect(command.ok).toBe(true);
120
+ if (!command.ok) throw new Error(command.error.hint);
121
+ const submitted = device.queue.submit([command.value]);
122
+ expect(submitted.ok).toBe(true);
123
+ if (!submitted.ok) throw new Error(submitted.error.hint);
124
+ await device.queue.onSubmittedWorkDone();
125
+ return { recorder, device, texture };
126
+ }
127
+
128
+ describe.skipIf(SKIP_DAWN)('RHI debug v7 fresh-device evidence', () => {
129
+ it('records, encodes, strictly decodes, and replays pixels on a fresh Dawn device', async () => {
130
+ const pack = await loadDawn();
131
+ const recording = await makeRecording(pack);
132
+ const baseline = await readbackTexturePixels(
133
+ recording.device,
134
+ recording.texture,
135
+ WIDTH,
136
+ HEIGHT,
137
+ );
138
+ recording.recorder.onFrameEnd();
139
+ const assembled = assembleTape(recording.recorder);
140
+ expect(assembled.ok).toBe(true);
141
+ if (!assembled.ok) throw new Error(assembled.error.hint);
142
+ const decoded = decodeTape(assembled.value.bytes);
143
+ expect(decoded.ok).toBe(true);
144
+ if (!decoded.ok) throw new Error(decoded.error.hint);
145
+ expect(decoded.value.header.formatVersion).toBe(7);
146
+ expect(decoded.value.events.some((event) => event.kind === 'draw')).toBe(true);
147
+
148
+ const replayAdapter = await pack.rhi.requestAdapter();
149
+ expect(replayAdapter.ok).toBe(true);
150
+ if (!replayAdapter.ok)
151
+ throw new Error(`fresh Dawn adapter failed: ${replayAdapter.error.code}`);
152
+ const replayDevice = await replayAdapter.value.requestDevice();
153
+ expect(replayDevice.ok).toBe(true);
154
+ if (!replayDevice.ok) throw new Error(`fresh Dawn device failed: ${replayDevice.error.code}`);
155
+ const backend: ReplayBackend = {
156
+ device: replayDevice.value,
157
+ createShaderModule: pack.createShaderModule,
158
+ };
159
+ const session = await openReplay(decoded.value, backend);
160
+ expect(session.ok).toBe(true);
161
+ if (!session.ok) throw new Error(session.error.hint);
162
+ const inspection = await session.value.inspectWork(0, ['pixels']);
163
+ expect(
164
+ inspection.ok,
165
+ inspection.ok
166
+ ? undefined
167
+ : `${inspection.error.code}: ${JSON.stringify(inspection.error.detail)}`,
168
+ ).toBe(true);
169
+ if (!inspection.ok) throw new Error(inspection.error.hint);
170
+ const replayPixels = inspection.value.attachment?.bytes;
171
+ expect(replayPixels).toBeDefined();
172
+ if (replayPixels === undefined) throw new Error('fresh replay did not return attachment bytes');
173
+ expect(inspection.value.attachment?.provenance.selectedWorkIndex).toBe(0);
174
+ expect(inspection.value.attachment?.provenance.resourceId).toBeDefined();
175
+ expect(normalizedPixelDelta(baseline, replayPixels)).toBeLessThanOrEqual(0.01);
176
+ expect((await session.value.dispose()).ok).toBe(true);
177
+ }, 60_000);
178
+ });
@@ -0,0 +1,56 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildTapeIndex } from '../protocol/tape-index';
3
+ import type { Tape } from '../protocol/types';
4
+
5
+ const tape: Tape = {
6
+ header: { formatVersion: 7, rhiCaps: {}, eventCount: 5, blobCount: 0 },
7
+ bootstrap: [],
8
+ events: [
9
+ {
10
+ kind: 'beginRenderPass',
11
+ passHandleId: 'pass:1',
12
+ cmdHandleId: 'cmd:1',
13
+ desc: { colorAttachments: [] },
14
+ colorAttachmentViewHandleIds: [],
15
+ },
16
+ {
17
+ kind: 'draw',
18
+ passHandleId: 'pass:1',
19
+ vertexCount: 3,
20
+ instanceCount: 1,
21
+ firstVertex: 0,
22
+ firstInstance: 0,
23
+ },
24
+ {
25
+ kind: 'drawIndexedIndirect',
26
+ passHandleId: 'pass:1',
27
+ indirectBufferHandleId: 'buf:1',
28
+ indirectOffset: 0,
29
+ },
30
+ { kind: 'endRenderPass', passHandleId: 'pass:1' },
31
+ { kind: 'beginComputePass', passHandleId: 'pass:2', cmdHandleId: 'cmd:1', desc: {} },
32
+ { kind: 'dispatchWorkgroups', passHandleId: 'pass:2', x: 1, y: 1, z: 1 },
33
+ ],
34
+ blobs: [],
35
+ };
36
+
37
+ describe('TapeIndex', () => {
38
+ it('assigns one global workIndex and preserves event/pass positions', () => {
39
+ const index = buildTapeIndex(tape);
40
+ expect(index.works.map((work) => [work.workIndex, work.eventIndex, work.passIndex])).toEqual([
41
+ [0, 1, 0],
42
+ [1, 2, 0],
43
+ [2, 5, 1],
44
+ ]);
45
+ expect(index.passes).toHaveLength(2);
46
+ expect(index.eventKinds).toContain('dispatchWorkgroups');
47
+ expect(index.passIndexByEvent[1]).toBe(0);
48
+ expect(index.passIndexByEvent[5]).toBe(1);
49
+ });
50
+
51
+ it('does not invent work for an empty pass', () => {
52
+ const index = buildTapeIndex({ ...tape, events: tape.events.slice(0, 1) });
53
+ expect(index.works).toHaveLength(0);
54
+ expect(index.passes[0]?.workIndices).toEqual([]);
55
+ });
56
+ });
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { decodeTape, encodeTape } from '../protocol/codec';
3
+ import type { Tape } from '../protocol/types';
4
+
5
+ const tape: Tape = {
6
+ header: { formatVersion: 7, rhiCaps: {}, eventCount: 0, blobCount: 0 },
7
+ bootstrap: [],
8
+ events: [],
9
+ blobs: [],
10
+ };
11
+
12
+ function encodedTape(): Uint8Array {
13
+ const result = encodeTape(tape);
14
+ if (!result.ok) throw new Error('fixture encoding failed');
15
+ return result.value;
16
+ }
17
+
18
+ describe('v7 tape rejection boundary', () => {
19
+ it.each([
20
+ ['truncated', (bytes: Uint8Array) => bytes.slice(0, bytes.length - 1)],
21
+ ['bad magic', (bytes: Uint8Array) => new Uint8Array([0, ...bytes.slice(1)])],
22
+ [
23
+ 'bad digest',
24
+ (bytes: Uint8Array) => {
25
+ const copy = bytes.slice();
26
+ copy[copy.length - 1] = (copy[copy.length - 1] ?? 0) ^ 0xff;
27
+ return copy;
28
+ },
29
+ ],
30
+ ])('%s fails with a structured tape error before replay', (_name, mutate) => {
31
+ const result = decodeTape(mutate(encodedTape()));
32
+ expect(result.ok).toBe(false);
33
+ if (result.ok) return;
34
+ expect(result.error.code).toBe('tape-invalid');
35
+ expect(result.error.expected).toContain('v7');
36
+ expect(result.error.hint.length).toBeGreaterThan(0);
37
+ });
38
+
39
+ it('rejects missing event fields and lifecycle violations', () => {
40
+ const invalid = new TextEncoder().encode(JSON.stringify({ formatVersion: 7 }));
41
+ const result = decodeTape(invalid);
42
+ expect(result.ok).toBe(false);
43
+ if (!result.ok) expect(result.error.code).toBe('tape-invalid');
44
+ });
45
+ });
@@ -0,0 +1,66 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { decodeTape, encodeTape } from '../protocol/codec';
3
+ import type { Tape } from '../protocol/types';
4
+
5
+ const caps = {
6
+ compute: true,
7
+ indirectDrawing: true,
8
+ storageBuffer: true,
9
+ };
10
+
11
+ function makeTape(compression: 'none' | 'gzip'): Tape {
12
+ return {
13
+ header: {
14
+ formatVersion: 7,
15
+ rhiCaps: caps,
16
+ eventCount: 1,
17
+ blobCount: 1,
18
+ },
19
+ bootstrap: [{ handleId: 'buf:1', kind: 'buffer', create: {}, initialData: [] }],
20
+ events: [
21
+ {
22
+ kind: 'writeBuffer',
23
+ handleId: 'buf:1',
24
+ bufferOffset: 0,
25
+ dataHash: 'sha256:payload',
26
+ size: 4,
27
+ },
28
+ ],
29
+ blobs: [
30
+ {
31
+ hash: 'sha256:payload',
32
+ bytes: new Uint8Array([1, 2, 3, 4]),
33
+ compression,
34
+ },
35
+ ],
36
+ };
37
+ }
38
+
39
+ describe('v7 tape container', () => {
40
+ it.each(['none', 'gzip'] as const)('round-trips %s with canonical bytes', (compression) => {
41
+ const first = encodeTape(makeTape(compression));
42
+ expect(first.ok).toBe(true);
43
+ if (!first.ok) return;
44
+
45
+ const decoded = decodeTape(first.value);
46
+ expect(decoded.ok).toBe(true);
47
+ if (!decoded.ok) return;
48
+
49
+ const second = encodeTape(decoded.value);
50
+ expect(second.ok).toBe(true);
51
+ if (!second.ok) return;
52
+ expect(Array.from(second.value)).toEqual(Array.from(first.value));
53
+ expect(decoded.value.blobs[0]?.bytes).toEqual(new Uint8Array([1, 2, 3, 4]));
54
+ });
55
+
56
+ it('preserves typed array blob bytes and digest identity', () => {
57
+ const encoded = encodeTape(makeTape('none'));
58
+ expect(encoded.ok).toBe(true);
59
+ if (!encoded.ok) return;
60
+ const decoded = decodeTape(encoded.value);
61
+ expect(decoded.ok).toBe(true);
62
+ if (!decoded.ok) return;
63
+ expect(decoded.value.blobs[0]?.bytes.constructor).toBe(Uint8Array);
64
+ expect(decoded.value.blobs[0]?.hash).toBe('sha256:payload');
65
+ });
66
+ });
@@ -0,0 +1,13 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { decodeTape } from '../protocol/codec';
3
+
4
+ describe('v7 tape version boundary', () => {
5
+ it.each([2, 3, 4, 5, 6])('rejects legacy version %s without conversion', (version) => {
6
+ const bytes = new TextEncoder().encode(JSON.stringify({ formatVersion: version }));
7
+ const result = decodeTape(bytes);
8
+ expect(result.ok).toBe(false);
9
+ if (result.ok) return;
10
+ expect(result.error.code).toBe('tape-version-unsupported');
11
+ expect(result.error.hint).toContain('source revision');
12
+ });
13
+ });