@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,154 @@
1
+ import type {
2
+ BindGroup,
3
+ BindGroupLayout,
4
+ Buffer,
5
+ CommandBuffer,
6
+ ComputePipeline,
7
+ PipelineLayout,
8
+ RenderPipeline,
9
+ RhiCommandEncoder,
10
+ RhiComputePassEncoder,
11
+ RhiDevice,
12
+ RhiRenderPassEncoder,
13
+ Sampler,
14
+ ShaderModule,
15
+ Texture,
16
+ TextureView,
17
+ } from '@forgeax/engine-rhi';
18
+ import { err, ok, type Result } from '@forgeax/engine-types';
19
+ import { createRhiDebugError, type RhiDebugError } from '../errors';
20
+ import type { ResourceKind } from '../protocol/types';
21
+
22
+ export type ReplayResource = {
23
+ readonly role?: string;
24
+ } & (
25
+ | { readonly kind: 'buffer'; readonly value: Buffer }
26
+ | { readonly kind: 'texture'; readonly value: Texture }
27
+ | { readonly kind: 'texture-view'; readonly value: TextureView }
28
+ | { readonly kind: 'sampler'; readonly value: Sampler }
29
+ | { readonly kind: 'shader-module'; readonly value: ShaderModule }
30
+ | {
31
+ readonly kind: 'pipeline';
32
+ readonly role: 'render' | 'compute';
33
+ readonly value: RenderPipeline | ComputePipeline;
34
+ }
35
+ | {
36
+ readonly kind: 'binding';
37
+ readonly role: 'bind-group' | 'bind-group-layout' | 'pipeline-layout';
38
+ readonly value: BindGroup | BindGroupLayout | PipelineLayout;
39
+ }
40
+ | {
41
+ readonly kind: 'encoder';
42
+ readonly role: 'command' | 'command-buffer' | 'render-pass' | 'compute-pass';
43
+ readonly value:
44
+ | RhiCommandEncoder
45
+ | RhiRenderPassEncoder
46
+ | RhiComputePassEncoder
47
+ | CommandBuffer;
48
+ }
49
+ );
50
+
51
+ export interface ResourceTableEntry {
52
+ readonly resourceId: string;
53
+ readonly generation: number;
54
+ readonly resource: ReplayResource;
55
+ readonly descriptor: Record<string, unknown> | undefined;
56
+ }
57
+
58
+ export class ResourceTable {
59
+ private readonly entries = new Map<string, ResourceTableEntry>();
60
+ private disposed = false;
61
+ private currentGeneration: number;
62
+
63
+ constructor(
64
+ private readonly device: RhiDevice,
65
+ generation = 0,
66
+ ) {
67
+ this.currentGeneration = generation;
68
+ }
69
+
70
+ get generation(): number {
71
+ return this.currentGeneration;
72
+ }
73
+
74
+ get(resourceId: string): ResourceTableEntry | undefined {
75
+ return this.entries.get(resourceId);
76
+ }
77
+
78
+ set(
79
+ resourceId: string,
80
+ resource: ReplayResource,
81
+ descriptor?: Record<string, unknown> | undefined,
82
+ ): Result<void, RhiDebugError> {
83
+ if (this.disposed) {
84
+ return err(
85
+ createRhiDebugError('replay-position-invalid', {
86
+ requested: this.generation,
87
+ available: -1,
88
+ }),
89
+ );
90
+ }
91
+ this.entries.set(resourceId, {
92
+ resourceId,
93
+ generation: this.generation,
94
+ resource,
95
+ descriptor,
96
+ });
97
+ return ok(undefined);
98
+ }
99
+
100
+ delete(resourceId: string): void {
101
+ this.entries.delete(resourceId);
102
+ }
103
+
104
+ values(): IterableIterator<ResourceTableEntry> {
105
+ return this.entries.values();
106
+ }
107
+
108
+ reset(): Result<void, RhiDebugError> {
109
+ const result = this.releaseAll();
110
+ if (!result.ok) return result;
111
+ this.entries.clear();
112
+ this.currentGeneration += 1;
113
+ return ok(undefined);
114
+ }
115
+
116
+ dispose(): Result<void, RhiDebugError> {
117
+ if (this.disposed) return ok(undefined);
118
+ const result = this.releaseAll();
119
+ this.entries.clear();
120
+ this.disposed = true;
121
+ return result;
122
+ }
123
+
124
+ private releaseAll(): Result<void, RhiDebugError> {
125
+ let firstFailure: Result<never, RhiDebugError> | undefined;
126
+ for (const entry of this.entries.values()) {
127
+ if (entry.resource.kind === 'buffer') {
128
+ const result = this.device.destroyBuffer(entry.resource.value);
129
+ if (!result.ok && firstFailure === undefined)
130
+ firstFailure = resourceDisposeFailure(entry.resourceId, result.error.code);
131
+ } else if (entry.resource.kind === 'texture') {
132
+ const result = this.device.destroyTexture(entry.resource.value);
133
+ if (!result.ok && firstFailure === undefined)
134
+ firstFailure = resourceDisposeFailure(entry.resourceId, result.error.code);
135
+ }
136
+ }
137
+ return firstFailure ?? ok(undefined);
138
+ }
139
+ }
140
+
141
+ function resourceDisposeFailure(resourceId: string, cause: string): Result<never, RhiDebugError> {
142
+ return err(
143
+ createRhiDebugError('replay-event-failed', {
144
+ eventIndex: -1,
145
+ kind: `dispose:${resourceId}`,
146
+ stage: 'create',
147
+ cause,
148
+ }),
149
+ );
150
+ }
151
+
152
+ export function isResourceKind(resource: ReplayResource, kind: ResourceKind): boolean {
153
+ return resource.kind === kind;
154
+ }
@@ -0,0 +1,509 @@
1
+ import type {
2
+ RhiCommandEncoder,
3
+ RhiComputePassEncoder,
4
+ RhiDevice,
5
+ RhiRenderPassEncoder,
6
+ } from '@forgeax/engine-rhi';
7
+ import { err, ok, type Result } from '@forgeax/engine-types';
8
+ import { createRhiDebugError, type RhiDebugError } from '../errors';
9
+ import { buildFrameModel, type WorkBinding, type WorkPipeline } from '../frame-model';
10
+ import { buildTapeIndex, type TapeIndex, type TapeWorkEntry } from '../protocol/tape-index';
11
+ import type { BootstrapResource, RhiCallEvent, Tape } from '../protocol/types';
12
+ import type { CreateShaderModuleFn } from '../recorder';
13
+ import { computeTextureLayout, projectTextureExtent } from '../texel-layout';
14
+ import { eventFailure, executeEvent, type ReplayExecutionContext } from './execute';
15
+ import {
16
+ type BufferReadbackRange,
17
+ type ReplayReadbackResult,
18
+ readReplayResource,
19
+ } from './readback';
20
+ import { ResourceTable } from './resources';
21
+
22
+ export interface ReplayBackend {
23
+ readonly device: RhiDevice;
24
+ readonly createShaderModule: CreateShaderModuleFn;
25
+ }
26
+
27
+ export interface TextureSubresource {
28
+ readonly mipLevel: number;
29
+ readonly arrayLayer: number;
30
+ readonly aspect?: 'all' | 'depth-only' | 'stencil-only';
31
+ }
32
+
33
+ export type ReadbackSubresource = TextureSubresource | BufferReadbackRange;
34
+
35
+ export type InspectField = 'bindings' | 'pipeline' | 'pixels';
36
+
37
+ export interface WorkInspection {
38
+ readonly workIndex: number;
39
+ readonly eventIndex: number;
40
+ readonly passIndex: number;
41
+ readonly attachment: ReplayReadbackResult | undefined;
42
+ readonly pipeline?: WorkPipeline;
43
+ readonly bindings?: readonly WorkBinding[];
44
+ readonly vertexBuffers?: readonly {
45
+ readonly slot: number;
46
+ readonly bufferHandleId: string;
47
+ readonly offset: number;
48
+ readonly size: number | null;
49
+ }[];
50
+ readonly indexBuffer?: {
51
+ readonly bufferHandleId: string;
52
+ readonly format: string;
53
+ readonly offset: number;
54
+ readonly size: number | null;
55
+ } | null;
56
+ readonly shaders?: WorkPipeline['shaders'];
57
+ readonly resourceIds?: readonly string[];
58
+ }
59
+
60
+ export interface ReplaySession {
61
+ readonly generation: number;
62
+ inspectWork(
63
+ workIndex: number,
64
+ fields?: readonly InspectField[],
65
+ signal?: AbortSignal,
66
+ ): Promise<Result<WorkInspection, RhiDebugError>>;
67
+ readResource(
68
+ resourceId: string,
69
+ subresource?: ReadbackSubresource,
70
+ signal?: AbortSignal,
71
+ ): Promise<Result<ReplayReadbackResult, RhiDebugError>>;
72
+ dispose(): Promise<Result<void, RhiDebugError>>;
73
+ }
74
+
75
+ export async function openReplay(
76
+ tape: Tape,
77
+ backend: ReplayBackend,
78
+ ): Promise<Result<ReplaySession, RhiDebugError>> {
79
+ if (tape.header.formatVersion !== 7) {
80
+ return err(
81
+ createRhiDebugError('tape-version-unsupported', {
82
+ foundVersion: tape.header.formatVersion,
83
+ expectedVersion: 7,
84
+ }),
85
+ );
86
+ }
87
+ const capabilityFailure = checkCapabilities(tape, backend.device);
88
+ if (capabilityFailure !== undefined) return err(capabilityFailure);
89
+
90
+ const index = buildTapeIndex(tape);
91
+ const model = buildFrameModel(tape);
92
+ const table = new ResourceTable(backend.device, 0);
93
+ let disposed = false;
94
+ let prepared = false;
95
+
96
+ const context: ReplayExecutionContext = {
97
+ device: backend.device,
98
+ queue: backend.device.queue,
99
+ tape,
100
+ table,
101
+ createShaderModule: backend.createShaderModule,
102
+ };
103
+
104
+ const reset = async (): Promise<Result<void, RhiDebugError>> => {
105
+ const cleared = table.reset();
106
+ if (!cleared.ok) return cleared;
107
+ prepared = false;
108
+ return ok(undefined);
109
+ };
110
+
111
+ const prepare = async (): Promise<Result<void, RhiDebugError>> => {
112
+ if (prepared) return ok(undefined);
113
+ for (let index = 0; index < tape.bootstrap.length; index++) {
114
+ const resource = tape.bootstrap[index];
115
+ if (resource === undefined) continue;
116
+ const created = await executeBootstrapResource(context, resource, index);
117
+ if (!created.ok) return created;
118
+ const seeded = await seedBootstrapResource(context, resource, index);
119
+ if (!seeded.ok) return seeded;
120
+ }
121
+ prepared = true;
122
+ return ok(undefined);
123
+ };
124
+
125
+ const session: ReplaySession = {
126
+ get generation() {
127
+ return table.generation;
128
+ },
129
+ async inspectWork(workIndex, fields, signal) {
130
+ if (disposed) return positionError(workIndex, index.works.length);
131
+ if (signal?.aborted) return positionError(workIndex, index.works.length);
132
+ const work = index.works[workIndex];
133
+ const modelWork = model.works[workIndex];
134
+ if (work === undefined) return positionError(workIndex, index.works.length);
135
+ if (modelWork === undefined) return positionError(workIndex, index.works.length);
136
+ const cleared = await reset();
137
+ if (!cleared.ok) return cleared;
138
+ const bootstrapped = await prepare();
139
+ if (!bootstrapped.ok) return bootstrapped;
140
+ const replayed = await replayThroughWork(context, index, work, signal);
141
+ if (!replayed.ok) return replayed;
142
+ const attachment = fields?.includes('pixels')
143
+ ? await readWorkAttachment(context, index, work)
144
+ : undefined;
145
+ if (attachment !== undefined && !attachment.ok) return attachment;
146
+ const selectedAttachment =
147
+ attachment?.ok === true
148
+ ? {
149
+ ...attachment.value,
150
+ provenance: {
151
+ ...attachment.value.provenance,
152
+ selectedWorkIndex: work.workIndex,
153
+ },
154
+ }
155
+ : undefined;
156
+ const baseInspection = {
157
+ workIndex: work.workIndex,
158
+ eventIndex: work.eventIndex,
159
+ passIndex: work.passIndex,
160
+ attachment: selectedAttachment,
161
+ };
162
+ return ok({
163
+ ...baseInspection,
164
+ ...(fields?.includes('pipeline') ? { pipeline: modelWork.pipeline } : {}),
165
+ ...(fields?.includes('bindings')
166
+ ? {
167
+ bindings: modelWork.bindings,
168
+ vertexBuffers: modelWork.vertexBuffers,
169
+ indexBuffer: modelWork.indexBuffer,
170
+ shaders: modelWork.pipeline.shaders,
171
+ resourceIds: modelWork.bindings
172
+ .map((binding) => binding.resourceId)
173
+ .filter((resourceId): resourceId is string => resourceId !== null),
174
+ }
175
+ : {}),
176
+ });
177
+ },
178
+ async readResource(resourceId, subresource, signal) {
179
+ if (disposed) {
180
+ return err(
181
+ createRhiDebugError('replay-position-invalid', {
182
+ requested: -1,
183
+ available: 0,
184
+ }),
185
+ );
186
+ }
187
+ if (signal?.aborted) {
188
+ return err(
189
+ createRhiDebugError('readback-failed', {
190
+ stage: 'readback',
191
+ cause: 'readback was aborted',
192
+ }),
193
+ );
194
+ }
195
+ const cleared = await reset();
196
+ if (!cleared.ok) return cleared;
197
+ const bootstrapped = await prepare();
198
+ if (!bootstrapped.ok) return bootstrapped;
199
+ return readReplayResource(
200
+ backend.device,
201
+ table,
202
+ resourceId,
203
+ subresource,
204
+ backend.createShaderModule,
205
+ );
206
+ },
207
+ async dispose() {
208
+ if (disposed) return ok(undefined);
209
+ const result = table.dispose();
210
+ disposed = true;
211
+ prepared = false;
212
+ return result;
213
+ },
214
+ };
215
+
216
+ return ok(session);
217
+ }
218
+
219
+ function checkCapabilities(tape: Tape, device: RhiDevice): RhiDebugError | undefined {
220
+ const recorded = tape.header.rhiCaps;
221
+ const missing = Object.entries(recorded).filter(([key, value]) => {
222
+ if (value !== true) return false;
223
+ const target = device.caps[key as keyof typeof device.caps];
224
+ return target !== true;
225
+ });
226
+ if (missing.length === 0) return undefined;
227
+ return createRhiDebugError('replay-capability-mismatch', {
228
+ stage: 'replay',
229
+ cause: `missing capabilities: ${missing.map(([key]) => key).join(', ')}`,
230
+ });
231
+ }
232
+
233
+ async function executeBootstrapResource(
234
+ context: ReplayExecutionContext,
235
+ resource: BootstrapResource,
236
+ bootstrapIndex: number,
237
+ ): Promise<Result<void, RhiDebugError>> {
238
+ const event: RhiCallEvent = JSON.parse(JSON.stringify(resource.create));
239
+ const result = await executeEvent(context, event, -bootstrapIndex - 1);
240
+ return result;
241
+ }
242
+
243
+ async function seedBootstrapResource(
244
+ context: ReplayExecutionContext,
245
+ resource: BootstrapResource,
246
+ bootstrapIndex: number,
247
+ ): Promise<Result<void, RhiDebugError>> {
248
+ if (resource.initialData.length === 0) return ok(undefined);
249
+ const entry = context.table.get(resource.handleId);
250
+ const event: RhiCallEvent = JSON.parse(JSON.stringify(resource.create));
251
+ if (entry?.resource.kind === 'texture' && event.kind === 'createTexture') {
252
+ return seedTextureInitialData(context, entry.resource.value, event, resource, bootstrapIndex);
253
+ }
254
+ if (entry?.resource.kind !== 'buffer') {
255
+ return eventFailure(
256
+ -bootstrapIndex - 1,
257
+ event,
258
+ 'lookup',
259
+ 'bootstrap initialData requires a buffer or color texture resource',
260
+ );
261
+ }
262
+ for (const slice of resource.initialData) {
263
+ const blob = context.tape.blobs.find((candidate) => candidate.hash === slice.hash);
264
+ if (blob === undefined) {
265
+ return eventFailure(-bootstrapIndex - 1, event, 'lookup', `blob ${slice.hash} is missing`);
266
+ }
267
+ const end = slice.byteOffset + slice.byteLength;
268
+ if (slice.byteOffset < 0 || slice.byteLength < 0 || end > blob.bytes.byteLength) {
269
+ return eventFailure(
270
+ -bootstrapIndex - 1,
271
+ event,
272
+ 'lookup',
273
+ `blob ${slice.hash} does not contain initialData slice [${slice.byteOffset}, ${end})`,
274
+ );
275
+ }
276
+ const bytes = blob.bytes.slice(slice.byteOffset, slice.byteOffset + slice.byteLength);
277
+ const written = context.queue.writeBuffer(entry.resource.value, 0, bytes);
278
+ if (!written.ok) {
279
+ return eventFailure(-bootstrapIndex - 1, event, 'write', written.error);
280
+ }
281
+ }
282
+ return ok(undefined);
283
+ }
284
+
285
+ function seedTextureInitialData(
286
+ context: ReplayExecutionContext,
287
+ texture: import('@forgeax/engine-rhi').Texture,
288
+ event: Extract<RhiCallEvent, { kind: 'createTexture' }>,
289
+ resource: BootstrapResource,
290
+ bootstrapIndex: number,
291
+ ): Result<void, RhiDebugError> {
292
+ const extent = projectTextureExtent(event.desc.size);
293
+ const layout = computeTextureLayout(
294
+ event.desc.format,
295
+ extent.width,
296
+ extent.height,
297
+ extent.layerCount,
298
+ event.desc.mipLevelCount ?? 1,
299
+ );
300
+ if (layout === undefined) {
301
+ return eventFailure(
302
+ -bootstrapIndex - 1,
303
+ event,
304
+ 'lookup',
305
+ `texture format '${event.desc.format}' has no known bootstrap byte layout`,
306
+ );
307
+ }
308
+ for (const slice of resource.initialData) {
309
+ const blob = context.tape.blobs.find((candidate) => candidate.hash === slice.hash);
310
+ if (blob === undefined) {
311
+ return eventFailure(-bootstrapIndex - 1, event, 'lookup', `blob ${slice.hash} is missing`);
312
+ }
313
+ const end = slice.byteOffset + slice.byteLength;
314
+ if (slice.byteOffset < 0 || slice.byteLength < 0 || end > blob.bytes.byteLength) {
315
+ return eventFailure(
316
+ -bootstrapIndex - 1,
317
+ event,
318
+ 'lookup',
319
+ `blob ${slice.hash} does not contain initialData slice [${slice.byteOffset}, ${end})`,
320
+ );
321
+ }
322
+ const bytes = blob.bytes.subarray(slice.byteOffset, end);
323
+ if (bytes.byteLength !== layout.totalBytes) {
324
+ return eventFailure(
325
+ -bootstrapIndex - 1,
326
+ event,
327
+ 'lookup',
328
+ `texture initialData has ${bytes.byteLength} bytes; expected ${layout.totalBytes}`,
329
+ );
330
+ }
331
+ for (const subresource of layout.slices) {
332
+ const rowBytes = Math.ceil(subresource.width / layout.blockWidth) * layout.bytesPerBlock;
333
+ const rowCount = Math.ceil(subresource.height / layout.blockHeight);
334
+ const subresourceBytes = bytes.subarray(
335
+ subresource.byteOffset,
336
+ subresource.byteOffset + subresource.byteLength,
337
+ );
338
+ const written = context.queue.writeTexture(
339
+ {
340
+ texture,
341
+ mipLevel: subresource.mip,
342
+ origin: { x: 0, y: 0, z: subresource.layer },
343
+ aspect: 'all',
344
+ } as never,
345
+ subresourceBytes,
346
+ { offset: 0, bytesPerRow: rowBytes, rowsPerImage: rowCount },
347
+ {
348
+ width: Math.ceil(subresource.width / layout.blockWidth) * layout.blockWidth,
349
+ height: Math.ceil(subresource.height / layout.blockHeight) * layout.blockHeight,
350
+ depthOrArrayLayers: 1,
351
+ },
352
+ );
353
+ if (!written.ok) {
354
+ return eventFailure(-bootstrapIndex - 1, event, 'write', written.error);
355
+ }
356
+ }
357
+ }
358
+ return ok(undefined);
359
+ }
360
+
361
+ async function replayThroughWork(
362
+ context: ReplayExecutionContext,
363
+ index: TapeIndex,
364
+ work: TapeWorkEntry,
365
+ signal?: AbortSignal,
366
+ ): Promise<Result<void, RhiDebugError>> {
367
+ for (let eventIndex = 0; eventIndex <= work.eventIndex; eventIndex++) {
368
+ const event = context.tape.events[eventIndex];
369
+ if (event === undefined) break;
370
+ if (signal?.aborted) {
371
+ return eventFailure(eventIndex, event, 'lookup', 'inspectWork was aborted');
372
+ }
373
+ const result = await executeEvent(context, event, eventIndex);
374
+ if (!result.ok) return result;
375
+ }
376
+ return finalizeWorkPass(context, index, work);
377
+ }
378
+
379
+ async function finalizeWorkPass(
380
+ context: ReplayExecutionContext,
381
+ index: TapeIndex,
382
+ work: TapeWorkEntry,
383
+ ): Promise<Result<void, RhiDebugError>> {
384
+ const pass = index.passes.find((candidate) => candidate.passIndex === work.passIndex);
385
+ if (pass === undefined) return ok(undefined);
386
+ const begin = context.tape.events[pass.beginEventIndex];
387
+ if (begin?.kind !== 'beginRenderPass' && begin?.kind !== 'beginComputePass') return ok(undefined);
388
+ const entry = context.table.get(begin.passHandleId);
389
+ if (
390
+ entry?.resource.kind !== 'encoder' ||
391
+ (entry.resource.role !== 'render-pass' && entry.resource.role !== 'compute-pass')
392
+ ) {
393
+ return eventFailure(
394
+ pass.beginEventIndex,
395
+ begin,
396
+ 'lookup',
397
+ `pass ${begin.passHandleId} is not open`,
398
+ );
399
+ }
400
+ try {
401
+ (entry.resource.value as RhiRenderPassEncoder | RhiComputePassEncoder).end();
402
+ } catch (cause) {
403
+ return eventFailure(pass.endEventIndex ?? work.eventIndex, begin, 'encode', cause);
404
+ }
405
+ context.table.delete(begin.passHandleId);
406
+ const finishEventIndex = findNextEvent(
407
+ context.tape.events,
408
+ pass.beginEventIndex,
409
+ 'finish',
410
+ begin.cmdHandleId,
411
+ );
412
+ const encoder = context.table.get(begin.cmdHandleId);
413
+ if (encoder?.resource.kind !== 'encoder' || encoder.resource.role !== 'command') {
414
+ return eventFailure(
415
+ finishEventIndex ?? work.eventIndex,
416
+ begin,
417
+ 'lookup',
418
+ `encoder ${begin.cmdHandleId} is not available`,
419
+ );
420
+ }
421
+ const finished = (encoder.resource.value as RhiCommandEncoder).finish();
422
+ const finishEvent = context.tape.events[finishEventIndex ?? work.eventIndex] ?? begin;
423
+ if (!finished.ok)
424
+ return eventFailure(finishEventIndex ?? work.eventIndex, finishEvent, 'finish', finished.error);
425
+ context.table.set(begin.cmdHandleId, {
426
+ kind: 'encoder',
427
+ role: 'command-buffer',
428
+ value: finished.value,
429
+ });
430
+ const submitEventIndex = findNextEvent(
431
+ context.tape.events,
432
+ finishEventIndex ?? pass.beginEventIndex,
433
+ 'submit',
434
+ begin.cmdHandleId,
435
+ );
436
+ const submitted = context.queue.submit([finished.value]);
437
+ const submitEvent = context.tape.events[submitEventIndex ?? work.eventIndex] ?? begin;
438
+ if (!submitted.ok)
439
+ return eventFailure(
440
+ submitEventIndex ?? work.eventIndex,
441
+ submitEvent,
442
+ 'submit',
443
+ submitted.error,
444
+ );
445
+ await context.queue.onSubmittedWorkDone();
446
+ return ok(undefined);
447
+ }
448
+
449
+ function findNextEvent(
450
+ events: readonly RhiCallEvent[],
451
+ start: number,
452
+ kind: 'finish' | 'submit',
453
+ commandId: string,
454
+ ): number | undefined {
455
+ for (let index = start + 1; index < events.length; index++) {
456
+ const event = events[index];
457
+ if (event?.kind === 'finish' && kind === 'finish' && event.cmdHandleId === commandId)
458
+ return index;
459
+ if (event?.kind === 'submit' && kind === 'submit' && event.cmdHandleIds.includes(commandId))
460
+ return index;
461
+ }
462
+ return undefined;
463
+ }
464
+
465
+ function positionError(requested: number, available: number): Result<never, RhiDebugError> {
466
+ return err(
467
+ createRhiDebugError('replay-position-invalid', {
468
+ requested,
469
+ available,
470
+ }),
471
+ );
472
+ }
473
+
474
+ async function readWorkAttachment(
475
+ context: ReplayExecutionContext,
476
+ index: TapeIndex,
477
+ work: TapeWorkEntry,
478
+ ): Promise<Result<ReplayReadbackResult, RhiDebugError>> {
479
+ const pass = index.passes.find((candidate) => candidate.passIndex === work.passIndex);
480
+ const begin = pass === undefined ? undefined : context.tape.events[pass.beginEventIndex];
481
+ if (begin?.kind !== 'beginRenderPass') {
482
+ return err(
483
+ createRhiDebugError('readback-unsupported', {
484
+ stage: 'readback',
485
+ reason: 'work has no color attachment',
486
+ }),
487
+ );
488
+ }
489
+ const viewId = begin.colorAttachmentViewHandleIds.find(
490
+ (candidate): candidate is string => candidate !== undefined,
491
+ );
492
+ if (viewId === undefined) {
493
+ return err(
494
+ createRhiDebugError('readback-unsupported', {
495
+ stage: 'readback',
496
+ reason: 'render pass has no readable color attachment view',
497
+ }),
498
+ );
499
+ }
500
+ return readReplayResource(
501
+ context.device,
502
+ context.table,
503
+ viewId,
504
+ undefined,
505
+ context.createShaderModule,
506
+ );
507
+ }
508
+
509
+ export type { BufferReadbackRange, ReplayReadbackResult } from './readback';