@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,151 @@
1
+ // m3-3: tree-shake grep gate — verify FORGEAX_ENGINE_RHI_DEBUG=0 bundle does NOT
2
+ // contain 'engine-rhi-debug' string (AC-03).
3
+ //
4
+ // This test uses a static grep on the hello-cube production bundle. The
5
+ // coverage job materializes that cold bundle before running package tests, so
6
+ // an absent dist is a real gate failure rather than an implicit pass.
7
+
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { describe, expect, it } from 'vitest';
12
+
13
+ function findDistFiles(rootDir: string): string[] {
14
+ const results: string[] = [];
15
+ function walk(dir: string): void {
16
+ let entries: fs.Dirent[];
17
+ try {
18
+ entries = fs.readdirSync(dir, { withFileTypes: true });
19
+ } catch {
20
+ return;
21
+ }
22
+ for (const e of entries) {
23
+ const full = path.join(dir, e.name);
24
+ if (e.isDirectory()) {
25
+ if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
26
+ walk(full);
27
+ } else if (
28
+ (e.name.endsWith('.mjs') || e.name.endsWith('.js')) &&
29
+ full.includes('/dist/assets/')
30
+ ) {
31
+ results.push(full);
32
+ }
33
+ }
34
+ }
35
+ walk(rootDir);
36
+ return results;
37
+ }
38
+
39
+ // I-14 fix-up (round 1 implement-review): the prior shape silently returned
40
+ // when no dist bundles existed (cold worktree), making the gate look green
41
+ // without a real grep. The explicit coverage prebuild now makes the absence
42
+ // assertion below actionable.
43
+
44
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
45
+ const ENGINE_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
46
+ const COLD_DIST_ROOT = path.resolve(ENGINE_ROOT, 'apps', 'hello', 'cube', 'dist', 'assets');
47
+ const DIST_FILES = findDistFiles(COLD_DIST_ROOT);
48
+
49
+ describe('tree-shake grep gate (AC-03)', () => {
50
+ it('hello-cube cold production dist exists and does not contain engine-rhi-debug string', () => {
51
+ expect(DIST_FILES.length).toBeGreaterThan(0);
52
+ // Grep each .mjs file for the forbidden string.
53
+ const violations: string[] = [];
54
+ for (const fp of DIST_FILES) {
55
+ let content: string;
56
+ try {
57
+ content = fs.readFileSync(fp, 'utf-8');
58
+ } catch {
59
+ continue;
60
+ }
61
+ if (content.includes('engine-rhi-debug')) {
62
+ violations.push(fp);
63
+ }
64
+ }
65
+
66
+ // All demo dist bundles must be clean.
67
+ expect(violations).toEqual([]);
68
+ });
69
+ });
70
+
71
+ // Browser entry node-identifier grep (AC-16).
72
+ //
73
+ // The browser entry and its bundled import closure must carry zero Node-only
74
+ // dependencies. The root remains realm-neutral and is tested separately.
75
+
76
+ const BROWSER_DIST = path.resolve(__dirname, '..', '..', 'dist', 'browser.mjs');
77
+ const BROWSER_DIST_EXISTS = fs.existsSync(BROWSER_DIST);
78
+
79
+ const BARREL_SRC = path.resolve(__dirname, '..', 'index.ts');
80
+
81
+ describe('browser entry isolation (AC-16)', () => {
82
+ it.skipIf(!BROWSER_DIST_EXISTS)(
83
+ 'browser.mjs + import closure contain no fs / pngjs / ws identifiers',
84
+ () => {
85
+ const content = fs.readFileSync(BROWSER_DIST, 'utf-8');
86
+ // Match Node-builtin imports + the two Node-only deps by their import
87
+ // shapes. recorder-core / tape-format are inlined by tsup, so a hit here
88
+ // means the isolation broke (a node-tainted module crept into the closure).
89
+ const forbidden: RegExp[] = [
90
+ /\bnode:fs\b/,
91
+ /\bnode:path\b/,
92
+ /\bnode:crypto\b/,
93
+ /from\s+['"]fs['"]/,
94
+ /from\s+['"]path['"]/,
95
+ /\bpngjs\b/,
96
+ /from\s+['"]ws['"]/,
97
+ /require\(\s*['"]ws['"]\s*\)/,
98
+ ];
99
+ const hits = forbidden.filter((re) => re.test(content)).map((re) => re.source);
100
+ expect(hits).toEqual([]);
101
+ },
102
+ );
103
+
104
+ it('barrel index.ts does not re-export browser transport symbols', () => {
105
+ const barrel = fs.readFileSync(BARREL_SRC, 'utf-8');
106
+ const leaked = ['captureAndUpload', 'uploadTape'].filter((sym) => barrel.includes(sym));
107
+ expect(leaked).toEqual([]);
108
+ });
109
+ });
110
+
111
+ describe('retained core dependency boundary (OOS-5)', () => {
112
+ it('does not include viewer or host dependency identifiers in core production sources', () => {
113
+ const sourceFiles = [
114
+ 'index.ts',
115
+ 'frame-model.ts',
116
+ 'protocol/codec.ts',
117
+ 'replay/session.ts',
118
+ ].map((file) => path.resolve(__dirname, '..', file));
119
+ const forbidden = [
120
+ '@codemirror/',
121
+ 'dockview',
122
+ '@forgeax/engine-naga',
123
+ "from 'react'",
124
+ 'node:fs',
125
+ 'node:path',
126
+ ];
127
+ const hits = sourceFiles.flatMap((file) => {
128
+ const source = fs.readFileSync(file, 'utf8');
129
+ return forbidden.filter((token) => source.includes(token)).map((token) => `${file}:${token}`);
130
+ });
131
+ expect(hits).toEqual([]);
132
+ });
133
+ });
134
+
135
+ describe('v7 cold model path', () => {
136
+ it('keeps protocol and model sources free of host-only imports', () => {
137
+ const sourcePaths = [
138
+ path.resolve(__dirname, '..', 'protocol', 'codec.ts'),
139
+ path.resolve(__dirname, '..', 'protocol', 'validation.ts'),
140
+ path.resolve(__dirname, '..', 'protocol', 'event-semantics.ts'),
141
+ path.resolve(__dirname, '..', 'protocol', 'tape-index.ts'),
142
+ path.resolve(__dirname, '..', 'frame-model.ts'),
143
+ ];
144
+ const forbidden = /^\s*(?:import|export).*\b(?:node:|pngjs|webgpu|vite|rhi-webgpu|rhi-wgpu)\b/m;
145
+ const hits = sourcePaths.flatMap((sourcePath) => {
146
+ const source = fs.readFileSync(sourcePath, 'utf8');
147
+ return forbidden.test(source) ? [sourcePath] : [];
148
+ });
149
+ expect(hits).toEqual([]);
150
+ });
151
+ });
@@ -0,0 +1,257 @@
1
+ // Unit — RhiCallEvent + Tape + work inspection type validation.
2
+ //
3
+ // Verifies that ~5 RhiCallEvent kinds can be constructed at the type level,
4
+ // that they are JSON-serializable (no native GPU objects in the shape),
5
+ // and that Tape / work inspection / RhiCapsRecorded types compile.
6
+
7
+ import { describe, expect, expectTypeOf, it } from 'vitest';
8
+ import type {
9
+ InspectBindingEntry,
10
+ InspectReport,
11
+ RhiCallEvent,
12
+ RhiCallEventBeginRenderPass,
13
+ RhiCallEventCreateBuffer,
14
+ RhiCallEventDraw,
15
+ RhiCallEventDrawIndexed,
16
+ RhiCallEventSetBindGroup,
17
+ RhiCallEventSetPipeline,
18
+ RhiCallEventWriteBuffer,
19
+ RhiCapsRecorded,
20
+ Tape,
21
+ } from '../types';
22
+
23
+ describe('RhiCallEvent — closed union constructibility', () => {
24
+ it('createBuffer event shape', () => {
25
+ const ev: RhiCallEventCreateBuffer = {
26
+ kind: 'createBuffer',
27
+ handleId: 'buf-1',
28
+ desc: {
29
+ size: 256,
30
+ usage: 0,
31
+ mappedAtCreation: false,
32
+ },
33
+ };
34
+ expectTypeOf(ev).toMatchTypeOf<RhiCallEvent>();
35
+ // JSON serializable check
36
+ const json = JSON.stringify(ev);
37
+ expect(json).toContain('createBuffer');
38
+ });
39
+
40
+ it('writeBuffer event shape', () => {
41
+ const ev: RhiCallEventWriteBuffer = {
42
+ kind: 'writeBuffer',
43
+ handleId: 'buf-1',
44
+ bufferOffset: 0,
45
+ dataHash: 'abc123',
46
+ size: 256,
47
+ };
48
+ expectTypeOf(ev).toMatchTypeOf<RhiCallEvent>();
49
+ const json = JSON.stringify(ev);
50
+ expect(json).toContain('writeBuffer');
51
+ });
52
+
53
+ it('beginRenderPass event shape', () => {
54
+ const ev: RhiCallEventBeginRenderPass = {
55
+ kind: 'beginRenderPass',
56
+ cmdHandleId: 'cmd:1',
57
+ passHandleId: 'pass:1',
58
+ desc: {
59
+ colorAttachments: [],
60
+ },
61
+ colorAttachmentViewHandleIds: [],
62
+ };
63
+ expectTypeOf(ev).toMatchTypeOf<RhiCallEvent>();
64
+ // No native GPUTextureView in the shape — AC-10
65
+ const json = JSON.stringify(ev);
66
+ expect(json).not.toContain('[object');
67
+ });
68
+
69
+ it('setPipeline + draw event chain', () => {
70
+ const setP: RhiCallEventSetPipeline = {
71
+ kind: 'setPipeline',
72
+ passHandleId: 'pass:1',
73
+ pipelineHandleId: 'pipeline:1',
74
+ };
75
+ const draw: RhiCallEventDraw = {
76
+ kind: 'draw',
77
+ passHandleId: 'pass:1',
78
+ vertexCount: 3,
79
+ instanceCount: 1,
80
+ firstVertex: 0,
81
+ firstInstance: 0,
82
+ };
83
+ const indexedDraw: RhiCallEventDrawIndexed = {
84
+ kind: 'drawIndexed',
85
+ passHandleId: 'pass:1',
86
+ indexCount: 36,
87
+ instanceCount: 1,
88
+ firstIndex: 0,
89
+ baseVertex: 0,
90
+ firstInstance: 0,
91
+ };
92
+ expectTypeOf(setP).toMatchTypeOf<RhiCallEvent>();
93
+ expectTypeOf(draw).toMatchTypeOf<RhiCallEvent>();
94
+ expectTypeOf(indexedDraw).toMatchTypeOf<RhiCallEvent>();
95
+ });
96
+
97
+ it('setBindGroup event shape', () => {
98
+ const ev: RhiCallEventSetBindGroup = {
99
+ kind: 'setBindGroup',
100
+ passHandleId: 'pass:1',
101
+ index: 0,
102
+ bindGroupHandleId: 'bg:1',
103
+ dynamicOffsets: [0, 256],
104
+ };
105
+ expectTypeOf(ev).toMatchTypeOf<RhiCallEvent>();
106
+ });
107
+
108
+ it('closed union rejects non-existent kind (type-level)', () => {
109
+ // Verify RhiCallEvent is a discriminated union with ~40 kinds.
110
+ // The kind field is the discriminant — all union members have kind.
111
+ expectTypeOf<RhiCallEvent>().toHaveProperty('kind');
112
+ // The kind discriminator should match RHI method names
113
+ expectTypeOf<RhiCallEventCreateBuffer['kind']>().toMatchTypeOf<'createBuffer'>();
114
+ });
115
+ });
116
+
117
+ describe('Tape — constructibility', () => {
118
+ it('minimal tape compiles', () => {
119
+ const tape: Tape = {
120
+ formatVersion: 2,
121
+ rhiCapsRecorded: {
122
+ canvasFormat: 'bgra8unorm',
123
+ rgba16floatRenderable: true,
124
+ float32Filterable: false,
125
+ textureCompressionBc: true,
126
+ textureCompressionEtc2: false,
127
+ textureCompressionAstc: false,
128
+ storageBuffer: true,
129
+ timestampQuery: false,
130
+ },
131
+ events: [
132
+ {
133
+ kind: 'frameMark',
134
+ frameIdx: 0,
135
+ },
136
+ ],
137
+ blobPool: new Map(),
138
+ };
139
+ expectTypeOf(tape).toMatchTypeOf<Tape>();
140
+ expect(tape.formatVersion).toBe(2);
141
+ expect(tape.events).toHaveLength(1);
142
+ });
143
+
144
+ it('tape with events + blobPool compiles', () => {
145
+ const buf = new ArrayBuffer(16);
146
+ const tape: Tape = {
147
+ formatVersion: 2,
148
+ rhiCapsRecorded: {
149
+ canvasFormat: 'bgra8unorm',
150
+ rgba16floatRenderable: false,
151
+ float32Filterable: false,
152
+ textureCompressionBc: false,
153
+ textureCompressionEtc2: false,
154
+ textureCompressionAstc: false,
155
+ storageBuffer: true,
156
+ timestampQuery: false,
157
+ },
158
+ events: [
159
+ { kind: 'frameMark', frameIdx: 0 },
160
+ {
161
+ kind: 'createBuffer',
162
+ handleId: 'buf-1',
163
+ desc: { size: 16, usage: 0, mappedAtCreation: false },
164
+ },
165
+ {
166
+ kind: 'writeBuffer',
167
+ handleId: 'buf-1',
168
+ bufferOffset: 0,
169
+ dataHash: 'a1b2c3',
170
+ size: 16,
171
+ },
172
+ ],
173
+ blobPool: new Map([['a1b2c3', buf]]),
174
+ };
175
+ expect(tape.formatVersion).toBe(2);
176
+ expect(tape.blobPool.get('a1b2c3')).toBe(buf);
177
+ });
178
+ });
179
+
180
+ describe('RhiCapsRecorded — constructibility', () => {
181
+ it('minimal caps', () => {
182
+ const caps: RhiCapsRecorded = {
183
+ canvasFormat: 'bgra8unorm',
184
+ rgba16floatRenderable: false,
185
+ float32Filterable: false,
186
+ textureCompressionBc: false,
187
+ textureCompressionEtc2: false,
188
+ textureCompressionAstc: false,
189
+ storageBuffer: false,
190
+ timestampQuery: false,
191
+ };
192
+ expect(caps.canvasFormat).toBe('bgra8unorm');
193
+ expect(caps.rgba16floatRenderable).toBe(false);
194
+ });
195
+ });
196
+
197
+ describe('InspectReport — constructibility', () => {
198
+ it('full report without rt compiles', () => {
199
+ const report: InspectReport = {
200
+ frameIdx: 0,
201
+ workIndex: 5,
202
+ passIdx: 0,
203
+ bindings: [],
204
+ drawCall: {
205
+ pipelineKind: 'render',
206
+ pipelineHandleId: 'pipeline:1',
207
+ },
208
+ };
209
+ expectTypeOf(report).toMatchTypeOf<InspectReport>();
210
+ expect(report.frameIdx).toBe(0);
211
+ expect(report.workIndex).toBe(5);
212
+ expect(report.rt).toBeUndefined();
213
+ });
214
+
215
+ it('report with rt path as string (not base64)', () => {
216
+ const report: InspectReport = {
217
+ frameIdx: 0,
218
+ workIndex: 42,
219
+ passIdx: 1,
220
+ bindings: [],
221
+ drawCall: {
222
+ pipelineKind: 'compute',
223
+ pipelineHandleId: 'pipeline:2',
224
+ dispatchX: 16,
225
+ dispatchY: 1,
226
+ dispatchZ: 1,
227
+ },
228
+ rt: '.forgeax-debug/test-run/inspect/d0042-rt0.png',
229
+ };
230
+ // AC-19: rt is a path string, not base64 data URI
231
+ expect(report.rt).toContain('png');
232
+ expect(report.rt).not.toContain('base64');
233
+ expect(report.rt).not.toContain('data:');
234
+ });
235
+ });
236
+
237
+ describe('InspectBindingEntry — constructibility', () => {
238
+ it('buffer kind', () => {
239
+ const entry: InspectBindingEntry = {
240
+ groupIndex: 0,
241
+ entryIndex: 0,
242
+ handleId: 'buf-3',
243
+ kind: 'buffer',
244
+ };
245
+ expect(entry.kind).toBe('buffer');
246
+ });
247
+
248
+ it('texture kind', () => {
249
+ const entry: InspectBindingEntry = {
250
+ groupIndex: 1,
251
+ entryIndex: 2,
252
+ handleId: 'texture:5',
253
+ kind: 'texture',
254
+ };
255
+ expect(entry.kind).toBe('texture');
256
+ });
257
+ });
package/src/browser.ts ADDED
@@ -0,0 +1,146 @@
1
+ // @forgeax/engine-rhi-debug/browser -- browser-only capture transport.
2
+
3
+ import { err, ok, type Result } from '@forgeax/engine-types';
4
+ import type { RhiDebugError } from './errors';
5
+ import type { CaptureFrameOptions, EncodedTape, RecorderAttachment } from './recorder/session';
6
+
7
+ export interface BrowserCaptureProvider {
8
+ captureFrame(options?: CaptureFrameOptions): Promise<Result<EncodedTape, RhiDebugError>>;
9
+ }
10
+
11
+ export interface BrowserCaptureOptions {
12
+ readonly endpoint?: string;
13
+ readonly runId?: string;
14
+ readonly capture?: CaptureFrameOptions;
15
+ readonly signal?: AbortSignal;
16
+ }
17
+
18
+ export interface BrowserArtifactRef {
19
+ readonly kind: 'rhi-tape';
20
+ readonly digest: string;
21
+ readonly path?: string;
22
+ }
23
+
24
+ export type BrowserCaptureError =
25
+ | {
26
+ readonly code: 'browser-capture-transport-unavailable';
27
+ readonly expected: string;
28
+ readonly hint: string;
29
+ readonly detail: { readonly endpoint: string };
30
+ }
31
+ | {
32
+ readonly code: 'browser-capture-upload-failed';
33
+ readonly expected: string;
34
+ readonly hint: string;
35
+ readonly detail: { readonly endpoint: string; readonly status: number };
36
+ }
37
+ | {
38
+ readonly code: 'browser-capture-response-invalid';
39
+ readonly expected: string;
40
+ readonly hint: string;
41
+ readonly detail: { readonly endpoint: string; readonly cause: string };
42
+ };
43
+
44
+ export function browserCaptureProvider(attachment: RecorderAttachment): BrowserCaptureProvider {
45
+ return attachment;
46
+ }
47
+
48
+ export async function captureAndUpload(
49
+ provider: BrowserCaptureProvider,
50
+ options: BrowserCaptureOptions = {},
51
+ ): Promise<Result<BrowserArtifactRef, RhiDebugError | BrowserCaptureError>> {
52
+ const captureOptions = mergeCaptureOptions(options.capture, options.signal);
53
+ const captured = await provider.captureFrame(captureOptions);
54
+ if (!captured.ok) return captured;
55
+ return uploadTape(captured.value, options);
56
+ }
57
+
58
+ export async function uploadTape(
59
+ tape: Pick<EncodedTape, 'bytes' | 'digest'>,
60
+ options: BrowserCaptureOptions = {},
61
+ ): Promise<Result<BrowserArtifactRef, BrowserCaptureError>> {
62
+ const endpoint = options.endpoint ?? '/__forgeax-debug/tape';
63
+ const runId = options.runId ?? createRunId();
64
+ const url = `${endpoint}?runId=${encodeURIComponent(runId)}`;
65
+ let response: Response;
66
+ try {
67
+ response = await fetch(url, {
68
+ method: 'POST',
69
+ headers: { 'content-type': 'application/x-forgeax-rhitape' },
70
+ body: copyBytes(tape.bytes),
71
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
72
+ });
73
+ } catch {
74
+ return err({
75
+ code: 'browser-capture-transport-unavailable',
76
+ expected: 'the configured RHI tape endpoint to accept a browser upload',
77
+ hint: 'start the Vite RHI-debug transport or provide a reachable endpoint',
78
+ detail: { endpoint },
79
+ });
80
+ }
81
+
82
+ if (!response.ok) {
83
+ return err({
84
+ code: 'browser-capture-upload-failed',
85
+ expected: 'the RHI tape endpoint to accept the validated artifact',
86
+ hint: 'inspect the endpoint response and retry one capture',
87
+ detail: { endpoint, status: response.status },
88
+ });
89
+ }
90
+
91
+ let value: unknown;
92
+ try {
93
+ value = await response.json();
94
+ } catch (cause) {
95
+ return err({
96
+ code: 'browser-capture-response-invalid',
97
+ expected: 'the RHI tape endpoint to return a JSON artifact reference',
98
+ hint: 'check the dev transport response body before retrying',
99
+ detail: { endpoint, cause: String(cause) },
100
+ });
101
+ }
102
+ if (!isArtifactRef(value)) {
103
+ return err({
104
+ code: 'browser-capture-response-invalid',
105
+ expected: 'the RHI tape endpoint to return a rhi-tape artifact reference',
106
+ hint: 'check the endpoint contract and capture again',
107
+ detail: { endpoint, cause: 'response is not a rhi-tape artifact reference' },
108
+ });
109
+ }
110
+ return ok(value);
111
+ }
112
+
113
+ function mergeCaptureOptions(
114
+ capture: CaptureFrameOptions | undefined,
115
+ signal: AbortSignal | undefined,
116
+ ): CaptureFrameOptions | undefined {
117
+ if (capture === undefined && signal === undefined) return undefined;
118
+ return { ...capture, ...(signal === undefined ? {} : { signal }) };
119
+ }
120
+
121
+ function createRunId(): string {
122
+ const randomUUID = globalThis.crypto?.randomUUID;
123
+ return randomUUID === undefined
124
+ ? `rhi-capture-${Date.now().toString(36)}`
125
+ : `rhi-capture-${randomUUID()}`;
126
+ }
127
+
128
+ function copyBytes(bytes: Uint8Array): ArrayBuffer {
129
+ const copy = new ArrayBuffer(bytes.byteLength);
130
+ new Uint8Array(copy).set(bytes);
131
+ return copy;
132
+ }
133
+
134
+ function isArtifactRef(value: unknown): value is BrowserArtifactRef {
135
+ if (value === null || typeof value !== 'object') return false;
136
+ const candidate = value as {
137
+ readonly kind?: unknown;
138
+ readonly digest?: unknown;
139
+ readonly path?: unknown;
140
+ };
141
+ return (
142
+ candidate.kind === 'rhi-tape' &&
143
+ typeof candidate.digest === 'string' &&
144
+ (candidate.path === undefined || typeof candidate.path === 'string')
145
+ );
146
+ }