@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,38 @@
1
+ import { err, ok, type Result } from '@forgeax/engine-types';
2
+ import { createRhiDebugError, type RhiDebugError } from '../errors';
3
+ import type { DebugRhiInstance } from '../recorder';
4
+ import type { ResourceRegistry } from './resource-registry';
5
+
6
+ export interface SnapshotOptions {
7
+ readonly snapshotTimeoutMs: number;
8
+ readonly byteBudget: number;
9
+ }
10
+
11
+ export async function snapshotFrame(
12
+ recorder: DebugRhiInstance,
13
+ registry: ResourceRegistry,
14
+ options: SnapshotOptions,
15
+ ): Promise<Result<void, RhiDebugError>> {
16
+ const estimatedBytes = registry.estimateSnapshotBytes();
17
+ const requiredBytes = Math.max(1, estimatedBytes);
18
+ if (requiredBytes > options.byteBudget) {
19
+ return err(
20
+ createRhiDebugError('capture-snapshot-failed', {
21
+ stage: 'snapshot',
22
+ cause: `snapshot byte budget ${options.byteBudget} is smaller than the ${requiredBytes}-byte capture minimum`,
23
+ }),
24
+ );
25
+ }
26
+ try {
27
+ const result = await recorder.snapshotAllLiveResources(options.snapshotTimeoutMs);
28
+ if (result.ok) return ok(undefined);
29
+ return err(result.error);
30
+ } catch (cause) {
31
+ return err(
32
+ createRhiDebugError('capture-snapshot-failed', {
33
+ stage: 'snapshot',
34
+ cause: String(cause),
35
+ }),
36
+ );
37
+ }
38
+ }
@@ -0,0 +1,157 @@
1
+ // @forgeax/engine-rhi-debug/src/recorder/wrap -- RHI instance assembly.
2
+
3
+ /// <reference types="@webgpu/types" />
4
+
5
+ import type {
6
+ RequestAdapterOptions,
7
+ RequestDeviceOptions,
8
+ Result,
9
+ RhiAdapter,
10
+ RhiDevice,
11
+ RhiInstance,
12
+ ShaderModule,
13
+ } from '@forgeax/engine-rhi';
14
+ import { ok as makeOk } from '@forgeax/engine-types';
15
+ import type { HandleId, RhiCallEvent } from '../types';
16
+ import {
17
+ type DebugRhiInstance,
18
+ pushEvent,
19
+ type RecorderInternal,
20
+ RecorderState,
21
+ registerHandle,
22
+ } from './core';
23
+ import { createDeviceProxy } from './device';
24
+ import { createRecorderLifecycle } from './lifecycle';
25
+
26
+ export function wrap(instance: RhiInstance): DebugRhiInstance {
27
+ const s: RecorderInternal = {
28
+ state: RecorderState.Idle,
29
+ requestedFrames: 0,
30
+ recordedFrames: 0,
31
+ events: [],
32
+ blobPool: new Map(),
33
+ handleMap: new WeakMap(),
34
+ textureViewHandleMap: new WeakMap(),
35
+ bootstrapCreates: new Map(),
36
+ snapshotSeededHandles: new Set(),
37
+ snapshotGeneration: 0,
38
+ snapshotProgress: undefined,
39
+ descriptorTable: new Map(),
40
+ _skipRecord: false,
41
+ frameIdx: 0,
42
+ bootstrap: true,
43
+ recordedCaps: undefined,
44
+ valid: true,
45
+ capturedDevice: undefined,
46
+ };
47
+
48
+ const lifecycle = createRecorderLifecycle(s);
49
+ const {
50
+ arm,
51
+ onFrameEnd,
52
+ getTape,
53
+ getState,
54
+ getEvents,
55
+ getBlobPool,
56
+ transitionToError,
57
+ disposeError,
58
+ snapshotResource,
59
+ snapshotAllLiveResources,
60
+ } = lifecycle;
61
+
62
+ const debugInst: DebugRhiInstance = {
63
+ arm,
64
+ onFrameEnd,
65
+ getTape,
66
+ getState,
67
+ getEvents,
68
+ getBlobPool,
69
+ transitionToError,
70
+ disposeError,
71
+ snapshotResource,
72
+ snapshotAllLiveResources,
73
+ pushExternalEvent(event: RhiCallEvent): void {
74
+ pushEvent(s, event);
75
+ },
76
+ registerShaderModule(handle: ShaderModule, handleId: HandleId): void {
77
+ s.handleMap.set(handle as object, handleId);
78
+ },
79
+ pushExternalCreateEvent(handle: object, kind: string, event: RhiCallEvent): HandleId {
80
+ const hId = registerHandle(s, handle, kind, event);
81
+ pushEvent(s, event);
82
+ return hId;
83
+ },
84
+ resetForDeviceLoss(): void {
85
+ // A lost GPU device invalidates every opaque handle and every descriptor
86
+ // entry. Keep the recorder usable for the renderer's subsequent rebuild:
87
+ // resource and shader creation calls during recovery repopulate these
88
+ // registries against the fresh device before the next capture arms.
89
+ s.state = RecorderState.Idle;
90
+ s.requestedFrames = 0;
91
+ s.recordedFrames = 0;
92
+ s.events = [];
93
+ s.blobPool = new Map();
94
+ s.handleMap = new WeakMap();
95
+ s.textureViewHandleMap = new WeakMap();
96
+ s.bootstrapCreates = new Map();
97
+ s.descriptorTable = new Map();
98
+ s.snapshotSeededHandles = new Set();
99
+ s.snapshotProgress = undefined;
100
+ s.frameIdx = 0;
101
+ s.bootstrap = true;
102
+ s.recordedCaps = undefined;
103
+ s.valid = true;
104
+ s.capturedDevice = undefined;
105
+ },
106
+ valid(): boolean {
107
+ return s.valid;
108
+ },
109
+ bootstrapCreatesSize(): number {
110
+ return s.bootstrapCreates.size;
111
+ },
112
+ bootstrapEvents(): readonly RhiCallEvent[] {
113
+ return Array.from(s.bootstrapCreates.values());
114
+ },
115
+ descriptorTable() {
116
+ return s.descriptorTable;
117
+ },
118
+
119
+ async requestAdapter(
120
+ opts?: RequestAdapterOptions | undefined,
121
+ compatibleSurface?: HTMLCanvasElement | OffscreenCanvas | undefined,
122
+ ) {
123
+ const res = await instance.requestAdapter(opts, compatibleSurface);
124
+ if (!res.ok) return res;
125
+
126
+ const realAdapter = res.value;
127
+ const proxyAdapter: RhiAdapter = {
128
+ features: realAdapter.features,
129
+ limits: realAdapter.limits,
130
+ async requestDevice(devOpts?: RequestDeviceOptions | undefined) {
131
+ const devRes = await realAdapter.requestDevice(devOpts);
132
+ if (!devRes.ok) return devRes;
133
+ // Keep the proxied device available for recorder-owned readback.
134
+ const proxied = createDeviceProxy(s, devRes.value);
135
+ s.capturedDevice = proxied;
136
+ s.recordedCaps = {
137
+ // RhiDevice does not own a canvas, so retain the existing tape
138
+ // default for this informational field while deriving every
139
+ // device-backed capability from the captured device.
140
+ canvasFormat: 'bgra8unorm' as GPUTextureFormat,
141
+ rgba16floatRenderable: proxied.caps.rgba16floatRenderable,
142
+ float32Filterable: proxied.caps.float32Filterable,
143
+ textureCompressionBc: proxied.caps.textureCompressionBc,
144
+ textureCompressionEtc2: proxied.caps.textureCompressionEtc2,
145
+ textureCompressionAstc: proxied.caps.textureCompressionAstc,
146
+ storageBuffer: proxied.caps.storageBuffer,
147
+ timestampQuery: proxied.caps.timestampQuery,
148
+ };
149
+ return makeOk(proxied) as Result<RhiDevice, import('@forgeax/engine-rhi').RhiError>;
150
+ },
151
+ };
152
+ return makeOk(proxyAdapter) as Result<RhiAdapter, import('@forgeax/engine-rhi').RhiError>;
153
+ },
154
+ };
155
+
156
+ return debugInst;
157
+ }
@@ -0,0 +1,15 @@
1
+ // @forgeax/engine-rhi-debug/src/recorder -- split recorder public seam.
2
+
3
+ export type { CreateShaderModuleFn, DebugRhiInstance } from './recorder/core';
4
+ export { PER_EVENT_OVERHEAD, TAPE_FORMAT_VERSION } from './recorder/core';
5
+ export type {
6
+ CaptureFrameOptions,
7
+ EncodedTape,
8
+ RecordableBackend,
9
+ RecorderAttachment,
10
+ RecorderBackend,
11
+ RecorderOptions,
12
+ } from './recorder/session';
13
+ export { attachRecorder } from './recorder/session';
14
+ export { wrapCreateShaderModule } from './recorder/shader';
15
+ export { wrap } from './recorder/wrap';
@@ -0,0 +1,38 @@
1
+ import type { Tape } from '../protocol/types';
2
+
3
+ const RECORDED_CAPABILITY_FEATURES = [
4
+ ['timestampQuery', 'timestamp-query'],
5
+ ['textureCompressionBc', 'texture-compression-bc'],
6
+ ['textureCompressionEtc2', 'texture-compression-etc2'],
7
+ ['textureCompressionAstc', 'texture-compression-astc'],
8
+ ['firstInstanceIndirect', 'indirect-first-instance'],
9
+ ['float32Filterable', 'float32-filterable'],
10
+ ['rg11b10ufloatRenderable', 'rg11b10ufloat-renderable'],
11
+ ] as const satisfies readonly (readonly [string, GPUFeatureName])[];
12
+
13
+ /**
14
+ * Build the strongest fresh WebGPU device the current adapter can provide for
15
+ * a recorded tape. Optional features must be enabled, not merely advertised,
16
+ * and replay receives concrete adapter limits instead of lower device defaults.
17
+ */
18
+ export function replayDeviceRequest(
19
+ tape: Tape,
20
+ adapterFeatures: ReadonlySet<GPUFeatureName>,
21
+ adapterLimits: Readonly<Record<string, number>>,
22
+ ): GPUDeviceDescriptor {
23
+ const requiredFeatures = RECORDED_CAPABILITY_FEATURES.filter(
24
+ ([capability, feature]) =>
25
+ tape.header.rhiCaps[capability] === true && adapterFeatures.has(feature),
26
+ ).map(([, feature]) => feature);
27
+ const limitEntries = Object.entries(adapterLimits).filter(
28
+ ([, value]) => Number.isFinite(value) && value >= 0,
29
+ );
30
+ const request: GPUDeviceDescriptor = {};
31
+ if (requiredFeatures.length > 0) request.requiredFeatures = requiredFeatures;
32
+ if (limitEntries.length > 0) {
33
+ request.requiredLimits = Object.fromEntries(limitEntries) as NonNullable<
34
+ GPUDeviceDescriptor['requiredLimits']
35
+ >;
36
+ }
37
+ return request;
38
+ }
@@ -0,0 +1,243 @@
1
+ // @forgeax/engine-rhi-debug/src/replay/execute-support -- replay event helpers.
2
+
3
+ import type {
4
+ Buffer,
5
+ RhiCommandEncoder,
6
+ RhiComputePassEncoder,
7
+ RhiRenderPassEncoder,
8
+ } from '@forgeax/engine-rhi';
9
+ import { err, ok, type Result } from '@forgeax/engine-types';
10
+ import { createRhiDebugError, type RhiDebugError } from '../errors';
11
+ import type { RhiCallEvent, Tape } from '../protocol/types';
12
+ import type { ReplayExecutionContext } from './execute';
13
+ import type { ReplayResource } from './resources';
14
+
15
+ type ReplayPass = RhiRenderPassEncoder & RhiComputePassEncoder;
16
+
17
+ export function eventRecord(event: RhiCallEvent): Record<string, unknown> {
18
+ return JSON.parse(JSON.stringify(event));
19
+ }
20
+
21
+ export function eventFailure(
22
+ eventIndex: number,
23
+ event: RhiCallEvent,
24
+ stage: 'lookup' | 'create' | 'write' | 'encode' | 'finish' | 'submit',
25
+ cause: unknown,
26
+ ): Result<never, RhiDebugError> {
27
+ return err(
28
+ createRhiDebugError('replay-event-failed', {
29
+ eventIndex,
30
+ kind: event.kind,
31
+ stage,
32
+ cause: cause instanceof Error ? cause.message : String(cause),
33
+ }),
34
+ );
35
+ }
36
+
37
+ export function blob(
38
+ tape: Tape,
39
+ hash: string,
40
+ event: RhiCallEvent,
41
+ eventIndex: number,
42
+ ): Result<Uint8Array, RhiDebugError> {
43
+ const found = tape.blobs.find((candidate) => candidate.hash === hash);
44
+ return found === undefined
45
+ ? eventFailure(eventIndex, event, 'lookup', `blob ${hash} is missing`)
46
+ : ok(found.bytes);
47
+ }
48
+
49
+ export function clearBuffer(
50
+ context: ReplayExecutionContext,
51
+ event: Extract<RhiCallEvent, { kind: 'clearBuffer' }>,
52
+ eventIndex: number,
53
+ ): Result<void, RhiDebugError> {
54
+ const encoder = requireResource<RhiCommandEncoder>(
55
+ context,
56
+ event.cmdHandleId,
57
+ 'encoder',
58
+ eventIndex,
59
+ event.kind,
60
+ 'command',
61
+ );
62
+ const buffer = requireResource<Buffer>(context, event.handleId, 'buffer', eventIndex, event.kind);
63
+ if (!encoder.ok) return encoder;
64
+ if (!buffer.ok) return buffer;
65
+ encoder.value.clearBuffer(buffer.value, event.offset, event.size);
66
+ return ok(undefined);
67
+ }
68
+
69
+ export function commandEncoderCall(
70
+ context: ReplayExecutionContext,
71
+ event: Extract<RhiCallEvent, { kind: 'pushDebugGroup' | 'popDebugGroup' | 'insertDebugMarker' }>,
72
+ eventIndex: number,
73
+ call: (encoder: RhiCommandEncoder) => void,
74
+ ): Result<void, RhiDebugError> {
75
+ const encoder = requireResource<RhiCommandEncoder>(
76
+ context,
77
+ event.cmdHandleId,
78
+ 'encoder',
79
+ eventIndex,
80
+ event.kind,
81
+ 'command',
82
+ );
83
+ if (!encoder.ok) return encoder;
84
+ call(encoder.value);
85
+ return ok(undefined);
86
+ }
87
+
88
+ export function finish(
89
+ context: ReplayExecutionContext,
90
+ event: Extract<RhiCallEvent, { kind: 'finish' }>,
91
+ eventIndex: number,
92
+ ): Result<void, RhiDebugError> {
93
+ const encoder = requireResource<RhiCommandEncoder>(
94
+ context,
95
+ event.cmdHandleId,
96
+ 'encoder',
97
+ eventIndex,
98
+ event.kind,
99
+ 'command',
100
+ );
101
+ if (!encoder.ok) return encoder;
102
+ const result = encoder.value.finish();
103
+ if (!result.ok) return eventFailure(eventIndex, event, 'finish', result.error);
104
+ return context.table.set(
105
+ event.cmdHandleId,
106
+ { kind: 'encoder', role: 'command-buffer', value: result.value },
107
+ eventRecord(event),
108
+ );
109
+ }
110
+
111
+ export function endPass(
112
+ context: ReplayExecutionContext,
113
+ event: Extract<RhiCallEvent, { kind: 'endRenderPass' | 'endComputePass' }>,
114
+ eventIndex: number,
115
+ role: 'render-pass' | 'compute-pass',
116
+ ): Result<void, RhiDebugError> {
117
+ const pass = requireResource<RhiRenderPassEncoder | RhiComputePassEncoder>(
118
+ context,
119
+ event.passHandleId,
120
+ 'encoder',
121
+ eventIndex,
122
+ event.kind,
123
+ role,
124
+ );
125
+ if (!pass.ok) return pass;
126
+ pass.value.end();
127
+ context.table.delete(event.passHandleId);
128
+ return ok(undefined);
129
+ }
130
+
131
+ export function seedInitialData(
132
+ context: ReplayExecutionContext,
133
+ event: Extract<RhiCallEvent, { kind: 'initialData' }>,
134
+ eventIndex: number,
135
+ ): Result<void, RhiDebugError> {
136
+ const entry = context.table.get(event.handleId);
137
+ const bytes = blob(context.tape, event.dataHash, event, eventIndex);
138
+ if (!bytes.ok) return bytes;
139
+ if (entry?.resource.kind === 'buffer') {
140
+ const result = context.queue.writeBuffer(entry.resource.value, 0, bytes.value);
141
+ return result.ok ? ok(undefined) : eventFailure(eventIndex, event, 'write', result.error);
142
+ }
143
+ return eventFailure(
144
+ eventIndex,
145
+ event,
146
+ 'lookup',
147
+ 'initialData currently requires a buffer resource',
148
+ );
149
+ }
150
+
151
+ export function requireResource<T>(
152
+ context: ReplayExecutionContext,
153
+ resourceId: string,
154
+ kind: ReplayResource['kind'] | string,
155
+ eventIndex: number,
156
+ eventKind: RhiCallEvent['kind'],
157
+ role?: string,
158
+ ): Result<T, RhiDebugError> {
159
+ const entry = context.table.get(resourceId);
160
+ if (
161
+ entry === undefined ||
162
+ entry.resource.kind !== kind ||
163
+ (role !== undefined && entry.resource.role !== role)
164
+ ) {
165
+ return missingResource(
166
+ eventIndex,
167
+ { kind: eventKind } as RhiCallEvent,
168
+ resourceId,
169
+ `${kind}${role === undefined ? '' : `/${role}`}`,
170
+ );
171
+ }
172
+ return ok(entry.resource.value as T);
173
+ }
174
+
175
+ export function requireReplayResource(
176
+ context: ReplayExecutionContext,
177
+ resourceId: string,
178
+ kind: ReplayResource['kind'] | string,
179
+ eventIndex: number,
180
+ eventKind: RhiCallEvent['kind'],
181
+ role?: string,
182
+ ): Result<ReplayResource, RhiDebugError> {
183
+ const entry = context.table.get(resourceId);
184
+ if (
185
+ entry === undefined ||
186
+ entry.resource.kind !== kind ||
187
+ (role !== undefined && entry.resource.role !== role)
188
+ ) {
189
+ return missingResource(
190
+ eventIndex,
191
+ { kind: eventKind } as RhiCallEvent,
192
+ resourceId,
193
+ `${kind}${role === undefined ? '' : `/${role}`}`,
194
+ );
195
+ }
196
+ return ok(entry.resource);
197
+ }
198
+
199
+ export function passCall(
200
+ context: ReplayExecutionContext,
201
+ event: RhiCallEvent,
202
+ eventIndex: number,
203
+ role: 'render-pass' | 'compute-pass' | 'pass',
204
+ call: (pass: ReplayPass) => Result<void, RhiDebugError>,
205
+ ): Result<void, RhiDebugError> {
206
+ const passId = 'passHandleId' in event ? event.passHandleId : undefined;
207
+ if (passId === undefined)
208
+ return eventFailure(eventIndex, event, 'lookup', 'pass handle is missing');
209
+ const entry = context.table.get(passId);
210
+ const valid =
211
+ entry?.resource.kind === 'encoder' && (role === 'pass' || entry.resource.role === role);
212
+ if (!valid || entry === undefined || entry.resource.kind !== 'encoder') {
213
+ return missingResource(eventIndex, event, passId, role);
214
+ }
215
+ return call(entry.resource.value as ReplayPass);
216
+ }
217
+
218
+ export function pipelineError<T>(
219
+ result: Result<T, RhiDebugError>,
220
+ event: RhiCallEvent,
221
+ eventIndex: number,
222
+ ): Result<never, RhiDebugError> {
223
+ return result.ok
224
+ ? eventFailure(eventIndex, event, 'lookup', 'resource has the wrong role')
225
+ : result;
226
+ }
227
+
228
+ export function missingResource(
229
+ eventIndex: number,
230
+ event: RhiCallEvent,
231
+ resourceId: string,
232
+ expected: string,
233
+ ): Result<never, RhiDebugError> {
234
+ return eventFailure(eventIndex, event, 'lookup', `resource ${resourceId} is not a ${expected}`);
235
+ }
236
+
237
+ export function unsupportedEvent(
238
+ eventIndex: number,
239
+ event: RhiCallEvent,
240
+ cause: string,
241
+ ): Result<never, RhiDebugError> {
242
+ return eventFailure(eventIndex, event, 'lookup', cause);
243
+ }