@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.
- package/LICENSE +202 -0
- package/README.md +178 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/bootstrap-integration.test.d.ts +2 -0
- package/dist/__tests__/bootstrap-integration.test.d.ts.map +1 -0
- package/dist/__tests__/consumer-inventory.unit.test.d.ts +2 -0
- package/dist/__tests__/consumer-inventory.unit.test.d.ts.map +1 -0
- package/dist/__tests__/copy-src-usage-validity.dawn.test.d.ts +15 -0
- package/dist/__tests__/copy-src-usage-validity.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/coverage-invariant.test-d.d.ts +2 -0
- package/dist/__tests__/coverage-invariant.test-d.d.ts.map +1 -0
- package/dist/__tests__/coverage-invariant.unit.test.d.ts +2 -0
- package/dist/__tests__/coverage-invariant.unit.test.d.ts.map +1 -0
- package/dist/__tests__/e2e.browser.test.d.ts +2 -0
- package/dist/__tests__/e2e.browser.test.d.ts.map +1 -0
- package/dist/__tests__/error-code-narrowing.test-d.d.ts +2 -0
- package/dist/__tests__/error-code-narrowing.test-d.d.ts.map +1 -0
- package/dist/__tests__/errors-v7.test-d.d.ts +2 -0
- package/dist/__tests__/errors-v7.test-d.d.ts.map +1 -0
- package/dist/__tests__/errors-v7.unit.test.d.ts +2 -0
- package/dist/__tests__/errors-v7.unit.test.d.ts.map +1 -0
- package/dist/__tests__/errors.unit.test.d.ts +2 -0
- package/dist/__tests__/errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/event-semantics-coverage.unit.test.d.ts +2 -0
- package/dist/__tests__/event-semantics-coverage.unit.test.d.ts.map +1 -0
- package/dist/__tests__/format-info.unit.test.d.ts +2 -0
- package/dist/__tests__/format-info.unit.test.d.ts.map +1 -0
- package/dist/__tests__/frame-model-parity.test-d.d.ts +2 -0
- package/dist/__tests__/frame-model-parity.test-d.d.ts.map +1 -0
- package/dist/__tests__/frame-model-parity.unit.test.d.ts +2 -0
- package/dist/__tests__/frame-model-parity.unit.test.d.ts.map +1 -0
- package/dist/__tests__/guard-gates.test.d.ts +2 -0
- package/dist/__tests__/guard-gates.test.d.ts.map +1 -0
- package/dist/__tests__/public-surface.integration.test.d.ts +2 -0
- package/dist/__tests__/public-surface.integration.test.d.ts.map +1 -0
- package/dist/__tests__/readback-fault-cleanup.unit.test.d.ts +2 -0
- package/dist/__tests__/readback-fault-cleanup.unit.test.d.ts.map +1 -0
- package/dist/__tests__/readback-format-matrix.dawn.test.d.ts +2 -0
- package/dist/__tests__/readback-format-matrix.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/readback-format-matrix.unit.test.d.ts +2 -0
- package/dist/__tests__/readback-format-matrix.unit.test.d.ts.map +1 -0
- package/dist/__tests__/readback-matrix-fixture.d.ts +32 -0
- package/dist/__tests__/readback-matrix-fixture.d.ts.map +1 -0
- package/dist/__tests__/recorder-bootstrap.unit.test.d.ts +2 -0
- package/dist/__tests__/recorder-bootstrap.unit.test.d.ts.map +1 -0
- package/dist/__tests__/recorder-budget.unit.test.d.ts +2 -0
- package/dist/__tests__/recorder-budget.unit.test.d.ts.map +1 -0
- package/dist/__tests__/recorder-fail-fast.unit.test.d.ts +2 -0
- package/dist/__tests__/recorder-fail-fast.unit.test.d.ts.map +1 -0
- package/dist/__tests__/recorder-lifecycle.unit.test.d.ts +2 -0
- package/dist/__tests__/recorder-lifecycle.unit.test.d.ts.map +1 -0
- package/dist/__tests__/recorder-push-event.unit.test.d.ts +2 -0
- package/dist/__tests__/recorder-push-event.unit.test.d.ts.map +1 -0
- package/dist/__tests__/recorder-session.integration.test.d.ts +2 -0
- package/dist/__tests__/recorder-session.integration.test.d.ts.map +1 -0
- package/dist/__tests__/recorder-session.unit.test.d.ts +2 -0
- package/dist/__tests__/recorder-session.unit.test.d.ts.map +1 -0
- package/dist/__tests__/recorder-steady-frame.dawn.test.d.ts +2 -0
- package/dist/__tests__/recorder-steady-frame.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/replay-fail-fast.unit.test.d.ts +2 -0
- package/dist/__tests__/replay-fail-fast.unit.test.d.ts.map +1 -0
- package/dist/__tests__/replay-session.dawn.test.d.ts +2 -0
- package/dist/__tests__/replay-session.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/replay-session.test-d.d.ts +2 -0
- package/dist/__tests__/replay-session.test-d.d.ts.map +1 -0
- package/dist/__tests__/replay-session.unit.test.d.ts +2 -0
- package/dist/__tests__/replay-session.unit.test.d.ts.map +1 -0
- package/dist/__tests__/resolve-texture-descriptor.unit.test.d.ts +2 -0
- package/dist/__tests__/resolve-texture-descriptor.unit.test.d.ts.map +1 -0
- package/dist/__tests__/resource-lifecycle.unit.test.d.ts +2 -0
- package/dist/__tests__/resource-lifecycle.unit.test.d.ts.map +1 -0
- package/dist/__tests__/rhi-debug-fresh-replay.dawn.test.d.ts +2 -0
- package/dist/__tests__/rhi-debug-fresh-replay.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/tape-index.unit.test.d.ts +2 -0
- package/dist/__tests__/tape-index.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tape-v7-invalid.unit.test.d.ts +2 -0
- package/dist/__tests__/tape-v7-invalid.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tape-v7-roundtrip.unit.test.d.ts +2 -0
- package/dist/__tests__/tape-v7-roundtrip.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tape-v7-version.unit.test.d.ts +2 -0
- package/dist/__tests__/tape-v7-version.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tree-shake.unit.test.d.ts +2 -0
- package/dist/__tests__/tree-shake.unit.test.d.ts.map +1 -0
- package/dist/__tests__/types.test.d.ts +2 -0
- package/dist/__tests__/types.test.d.ts.map +1 -0
- package/dist/browser.d.ts +45 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.mjs +83 -0
- package/dist/browser.mjs.map +1 -0
- package/dist/errors.d.ts +85 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/frame-model.d.ts +158 -0
- package/dist/frame-model.d.ts.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +10415 -0
- package/dist/index.mjs.map +1 -0
- package/dist/protocol/codec.d.ts +6 -0
- package/dist/protocol/codec.d.ts.map +1 -0
- package/dist/protocol/event-semantics.d.ts +15 -0
- package/dist/protocol/event-semantics.d.ts.map +1 -0
- package/dist/protocol/tape-index.d.ts +30 -0
- package/dist/protocol/tape-index.d.ts.map +1 -0
- package/dist/protocol/types.d.ts +44 -0
- package/dist/protocol/types.d.ts.map +1 -0
- package/dist/protocol/validation.d.ts +7 -0
- package/dist/protocol/validation.d.ts.map +1 -0
- package/dist/readback.d.ts +156 -0
- package/dist/readback.d.ts.map +1 -0
- package/dist/recorder/assemble.d.ts +11 -0
- package/dist/recorder/assemble.d.ts.map +1 -0
- package/dist/recorder/closure.d.ts +39 -0
- package/dist/recorder/closure.d.ts.map +1 -0
- package/dist/recorder/core.d.ts +296 -0
- package/dist/recorder/core.d.ts.map +1 -0
- package/dist/recorder/device.d.ts +4 -0
- package/dist/recorder/device.d.ts.map +1 -0
- package/dist/recorder/encoder.d.ts +5 -0
- package/dist/recorder/encoder.d.ts.map +1 -0
- package/dist/recorder/lifecycle.d.ts +20 -0
- package/dist/recorder/lifecycle.d.ts.map +1 -0
- package/dist/recorder/pass.d.ts +6 -0
- package/dist/recorder/pass.d.ts.map +1 -0
- package/dist/recorder/proxy.d.ts +19 -0
- package/dist/recorder/proxy.d.ts.map +1 -0
- package/dist/recorder/queue.d.ts +4 -0
- package/dist/recorder/queue.d.ts.map +1 -0
- package/dist/recorder/resource-registry.d.ts +15 -0
- package/dist/recorder/resource-registry.d.ts.map +1 -0
- package/dist/recorder/session.d.ts +25 -0
- package/dist/recorder/session.d.ts.map +1 -0
- package/dist/recorder/shader.d.ts +3 -0
- package/dist/recorder/shader.d.ts.map +1 -0
- package/dist/recorder/snapshot.d.ts +10 -0
- package/dist/recorder/snapshot.d.ts.map +1 -0
- package/dist/recorder/wrap.d.ts +4 -0
- package/dist/recorder/wrap.d.ts.map +1 -0
- package/dist/recorder.d.ts +7 -0
- package/dist/recorder.d.ts.map +1 -0
- package/dist/replay/device-request.d.ts +8 -0
- package/dist/replay/device-request.d.ts.map +1 -0
- package/dist/replay/execute-support.d.ts +33 -0
- package/dist/replay/execute-support.d.ts.map +1 -0
- package/dist/replay/execute.d.ts +18 -0
- package/dist/replay/execute.d.ts.map +1 -0
- package/dist/replay/readback.d.ts +32 -0
- package/dist/replay/readback.d.ts.map +1 -0
- package/dist/replay/resources.d.ts +57 -0
- package/dist/replay/resources.d.ts.map +1 -0
- package/dist/replay/session.d.ts +49 -0
- package/dist/replay/session.d.ts.map +1 -0
- package/dist/replay/texture-format.d.ts +21 -0
- package/dist/replay/texture-format.d.ts.map +1 -0
- package/dist/texel-decode.d.ts +35 -0
- package/dist/texel-decode.d.ts.map +1 -0
- package/dist/texel-layout.d.ts +76 -0
- package/dist/texel-layout.d.ts.map +1 -0
- package/dist/types.d.ts +722 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +72 -0
- package/src/__tests__/bootstrap-integration.test.ts +268 -0
- package/src/__tests__/consumer-inventory.unit.test.ts +154 -0
- package/src/__tests__/copy-src-usage-validity.dawn.test.ts +117 -0
- package/src/__tests__/coverage-invariant.test-d.ts +115 -0
- package/src/__tests__/coverage-invariant.unit.test.ts +45 -0
- package/src/__tests__/e2e.browser.test.ts +239 -0
- package/src/__tests__/error-code-narrowing.test-d.ts +32 -0
- package/src/__tests__/errors-v7.test-d.ts +19 -0
- package/src/__tests__/errors-v7.unit.test.ts +15 -0
- package/src/__tests__/errors.unit.test.ts +83 -0
- package/src/__tests__/event-semantics-coverage.unit.test.ts +22 -0
- package/src/__tests__/format-info.unit.test.ts +85 -0
- package/src/__tests__/frame-model-parity.test-d.ts +20 -0
- package/src/__tests__/frame-model-parity.unit.test.ts +285 -0
- package/src/__tests__/guard-gates.test.ts +317 -0
- package/src/__tests__/public-surface.integration.test.ts +38 -0
- package/src/__tests__/readback-fault-cleanup.unit.test.ts +66 -0
- package/src/__tests__/readback-format-matrix.dawn.test.ts +378 -0
- package/src/__tests__/readback-format-matrix.unit.test.ts +120 -0
- package/src/__tests__/readback-matrix-fixture.ts +20 -0
- package/src/__tests__/recorder-bootstrap.unit.test.ts +302 -0
- package/src/__tests__/recorder-budget.unit.test.ts +42 -0
- package/src/__tests__/recorder-fail-fast.unit.test.ts +434 -0
- package/src/__tests__/recorder-lifecycle.unit.test.ts +42 -0
- package/src/__tests__/recorder-push-event.unit.test.ts +573 -0
- package/src/__tests__/recorder-session.integration.test.ts +32 -0
- package/src/__tests__/recorder-session.unit.test.ts +63 -0
- package/src/__tests__/recorder-steady-frame.dawn.test.ts +52 -0
- package/src/__tests__/replay-fail-fast.unit.test.ts +155 -0
- package/src/__tests__/replay-session.dawn.test.ts +220 -0
- package/src/__tests__/replay-session.test-d.ts +27 -0
- package/src/__tests__/replay-session.unit.test.ts +171 -0
- package/src/__tests__/resolve-texture-descriptor.unit.test.ts +144 -0
- package/src/__tests__/resource-lifecycle.unit.test.ts +114 -0
- package/src/__tests__/rhi-debug-fresh-replay.dawn.test.ts +178 -0
- package/src/__tests__/tape-index.unit.test.ts +56 -0
- package/src/__tests__/tape-v7-invalid.unit.test.ts +45 -0
- package/src/__tests__/tape-v7-roundtrip.unit.test.ts +66 -0
- package/src/__tests__/tape-v7-version.unit.test.ts +13 -0
- package/src/__tests__/tree-shake.unit.test.ts +151 -0
- package/src/__tests__/types.test.ts +257 -0
- package/src/browser.ts +146 -0
- package/src/errors.ts +173 -0
- package/src/frame-model.ts +753 -0
- package/src/index.ts +92 -0
- package/src/pako.d.ts +8 -0
- package/src/protocol/codec.ts +220 -0
- package/src/protocol/event-semantics.ts +180 -0
- package/src/protocol/tape-index.ts +120 -0
- package/src/protocol/types.ts +60 -0
- package/src/protocol/validation.ts +173 -0
- package/src/readback.ts +804 -0
- package/src/recorder/assemble.ts +192 -0
- package/src/recorder/closure.ts +408 -0
- package/src/recorder/core.ts +587 -0
- package/src/recorder/device.ts +474 -0
- package/src/recorder/encoder.ts +251 -0
- package/src/recorder/lifecycle.ts +847 -0
- package/src/recorder/pass.ts +241 -0
- package/src/recorder/proxy.ts +59 -0
- package/src/recorder/queue.ts +109 -0
- package/src/recorder/resource-registry.ts +53 -0
- package/src/recorder/session.ts +152 -0
- package/src/recorder/shader.ts +47 -0
- package/src/recorder/snapshot.ts +38 -0
- package/src/recorder/wrap.ts +157 -0
- package/src/recorder.ts +15 -0
- package/src/replay/device-request.ts +38 -0
- package/src/replay/execute-support.ts +243 -0
- package/src/replay/execute.ts +870 -0
- package/src/replay/readback.ts +671 -0
- package/src/replay/resources.ts +154 -0
- package/src/replay/session.ts +509 -0
- package/src/replay/texture-format.ts +99 -0
- package/src/texel-decode.ts +289 -0
- package/src/texel-layout.ts +324 -0
- package/src/types.ts +888 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// @forgeax/engine-rhi-debug/src/recorder/pass -- render/compute pass proxy owners.
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ComputePipeline,
|
|
5
|
+
RenderPipeline,
|
|
6
|
+
RhiComputePassEncoder,
|
|
7
|
+
RhiRenderPassEncoder,
|
|
8
|
+
} from '@forgeax/engine-rhi';
|
|
9
|
+
import type { HandleId } from '../types';
|
|
10
|
+
import type { RecorderInternal } from './core';
|
|
11
|
+
import { getHandleId, pushEvent } from './core';
|
|
12
|
+
|
|
13
|
+
export function createRenderPassProxy(
|
|
14
|
+
s: RecorderInternal,
|
|
15
|
+
realPass: RhiRenderPassEncoder,
|
|
16
|
+
passHId: HandleId,
|
|
17
|
+
): RhiRenderPassEncoder {
|
|
18
|
+
return {
|
|
19
|
+
setPipeline(pipeline: RenderPipeline) {
|
|
20
|
+
const pid = getHandleId(s, pipeline as object, 'renderPipeline');
|
|
21
|
+
pushEvent(s, { kind: 'setPipeline', passHandleId: passHId, pipelineHandleId: pid });
|
|
22
|
+
realPass.setPipeline(pipeline);
|
|
23
|
+
},
|
|
24
|
+
setVertexBuffer(slot, buffer, offset, size) {
|
|
25
|
+
const bid = getHandleId(s, buffer as object, 'buffer');
|
|
26
|
+
pushEvent(s, {
|
|
27
|
+
kind: 'setVertexBuffer',
|
|
28
|
+
passHandleId: passHId,
|
|
29
|
+
slot,
|
|
30
|
+
bufferHandleId: bid,
|
|
31
|
+
offset,
|
|
32
|
+
size,
|
|
33
|
+
});
|
|
34
|
+
realPass.setVertexBuffer(slot, buffer, offset, size);
|
|
35
|
+
},
|
|
36
|
+
setIndexBuffer(buffer, format, offset, size) {
|
|
37
|
+
const bid = getHandleId(s, buffer as object, 'buffer');
|
|
38
|
+
pushEvent(s, {
|
|
39
|
+
kind: 'setIndexBuffer',
|
|
40
|
+
passHandleId: passHId,
|
|
41
|
+
bufferHandleId: bid,
|
|
42
|
+
format,
|
|
43
|
+
offset,
|
|
44
|
+
size,
|
|
45
|
+
});
|
|
46
|
+
realPass.setIndexBuffer(buffer, format, offset, size);
|
|
47
|
+
},
|
|
48
|
+
setBindGroup(index, bindGroup, ...rest: unknown[]) {
|
|
49
|
+
const bgid = getHandleId(s, bindGroup as object, 'bindGroup');
|
|
50
|
+
let dynOffsets: readonly number[] | undefined;
|
|
51
|
+
if (rest[0] instanceof Uint32Array) {
|
|
52
|
+
dynOffsets = Array.from(rest[0] as Uint32Array);
|
|
53
|
+
(realPass.setBindGroup as (...args: unknown[]) => void)(index, bindGroup, ...rest);
|
|
54
|
+
} else {
|
|
55
|
+
const offsets = rest[0] as readonly number[] | undefined;
|
|
56
|
+
dynOffsets = offsets === undefined ? undefined : Array.from(offsets);
|
|
57
|
+
realPass.setBindGroup(index, bindGroup, offsets);
|
|
58
|
+
}
|
|
59
|
+
pushEvent(s, {
|
|
60
|
+
kind: 'setBindGroup',
|
|
61
|
+
passHandleId: passHId,
|
|
62
|
+
index,
|
|
63
|
+
bindGroupHandleId: bgid,
|
|
64
|
+
dynamicOffsets: dynOffsets,
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
draw(vertexCount, instanceCount, firstVertex, firstInstance) {
|
|
68
|
+
pushEvent(s, {
|
|
69
|
+
kind: 'draw',
|
|
70
|
+
passHandleId: passHId,
|
|
71
|
+
vertexCount,
|
|
72
|
+
instanceCount: instanceCount ?? 1,
|
|
73
|
+
firstVertex: firstVertex ?? 0,
|
|
74
|
+
firstInstance: firstInstance ?? 0,
|
|
75
|
+
});
|
|
76
|
+
realPass.draw(vertexCount, instanceCount, firstVertex, firstInstance);
|
|
77
|
+
},
|
|
78
|
+
drawIndexed(indexCount, instanceCount, firstIndex, baseVertex, firstInstance) {
|
|
79
|
+
pushEvent(s, {
|
|
80
|
+
kind: 'drawIndexed',
|
|
81
|
+
passHandleId: passHId,
|
|
82
|
+
indexCount,
|
|
83
|
+
instanceCount: instanceCount ?? 1,
|
|
84
|
+
firstIndex: firstIndex ?? 0,
|
|
85
|
+
baseVertex: baseVertex ?? 0,
|
|
86
|
+
firstInstance: firstInstance ?? 0,
|
|
87
|
+
});
|
|
88
|
+
realPass.drawIndexed(indexCount, instanceCount, firstIndex, baseVertex, firstInstance);
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
// Pass-through methods (not in v1 event set, but must not break the proxy)
|
|
92
|
+
setViewport(x, y, w, h, minDepth, maxDepth) {
|
|
93
|
+
pushEvent(s, {
|
|
94
|
+
kind: 'setViewport',
|
|
95
|
+
passHandleId: passHId,
|
|
96
|
+
x,
|
|
97
|
+
y,
|
|
98
|
+
w,
|
|
99
|
+
h,
|
|
100
|
+
minDepth: minDepth ?? 0,
|
|
101
|
+
maxDepth: maxDepth ?? 1,
|
|
102
|
+
});
|
|
103
|
+
realPass.setViewport(x, y, w, h, minDepth, maxDepth);
|
|
104
|
+
},
|
|
105
|
+
setScissorRect(x, y, w, h) {
|
|
106
|
+
pushEvent(s, {
|
|
107
|
+
kind: 'setScissorRect',
|
|
108
|
+
passHandleId: passHId,
|
|
109
|
+
x,
|
|
110
|
+
y,
|
|
111
|
+
w,
|
|
112
|
+
h,
|
|
113
|
+
});
|
|
114
|
+
realPass.setScissorRect(x, y, w, h);
|
|
115
|
+
},
|
|
116
|
+
setBlendConstant(color) {
|
|
117
|
+
pushEvent(s, {
|
|
118
|
+
kind: 'setBlendConstant',
|
|
119
|
+
passHandleId: passHId,
|
|
120
|
+
color,
|
|
121
|
+
});
|
|
122
|
+
realPass.setBlendConstant(color);
|
|
123
|
+
},
|
|
124
|
+
setStencilReference(reference) {
|
|
125
|
+
// Recorded (not a no-op pass-through): stencil pipelines compare against
|
|
126
|
+
// this dynamic reference, so without it replay defaults ref=0 and a
|
|
127
|
+
// not-equal/equal stencil test (e.g. the 4.2 stencil-testing outline
|
|
128
|
+
// pass) silently breaks -- the outline vanishes on replay.
|
|
129
|
+
pushEvent(s, {
|
|
130
|
+
kind: 'setStencilReference',
|
|
131
|
+
passHandleId: passHId,
|
|
132
|
+
reference,
|
|
133
|
+
});
|
|
134
|
+
realPass.setStencilReference(reference);
|
|
135
|
+
},
|
|
136
|
+
drawIndirect(indirectBuffer, indirectOffset) {
|
|
137
|
+
const ibId = getHandleId(s, indirectBuffer as object, 'buffer');
|
|
138
|
+
pushEvent(s, {
|
|
139
|
+
kind: 'drawIndirect',
|
|
140
|
+
passHandleId: passHId,
|
|
141
|
+
indirectBufferHandleId: ibId,
|
|
142
|
+
indirectOffset,
|
|
143
|
+
});
|
|
144
|
+
realPass.drawIndirect(indirectBuffer, indirectOffset);
|
|
145
|
+
},
|
|
146
|
+
drawIndexedIndirect(indirectBuffer, indirectOffset) {
|
|
147
|
+
const ibId = getHandleId(s, indirectBuffer as object, 'buffer');
|
|
148
|
+
pushEvent(s, {
|
|
149
|
+
kind: 'drawIndexedIndirect',
|
|
150
|
+
passHandleId: passHId,
|
|
151
|
+
indirectBufferHandleId: ibId,
|
|
152
|
+
indirectOffset,
|
|
153
|
+
});
|
|
154
|
+
realPass.drawIndexedIndirect(indirectBuffer, indirectOffset);
|
|
155
|
+
},
|
|
156
|
+
pushDebugGroup(groupLabel) {
|
|
157
|
+
pushEvent(s, {
|
|
158
|
+
kind: 'passPushDebugGroup',
|
|
159
|
+
passHandleId: passHId,
|
|
160
|
+
groupLabel,
|
|
161
|
+
});
|
|
162
|
+
realPass.pushDebugGroup(groupLabel);
|
|
163
|
+
},
|
|
164
|
+
popDebugGroup() {
|
|
165
|
+
pushEvent(s, { kind: 'passPopDebugGroup', passHandleId: passHId });
|
|
166
|
+
realPass.popDebugGroup();
|
|
167
|
+
},
|
|
168
|
+
insertDebugMarker(markerLabel) {
|
|
169
|
+
pushEvent(s, {
|
|
170
|
+
kind: 'passInsertDebugMarker',
|
|
171
|
+
passHandleId: passHId,
|
|
172
|
+
markerLabel,
|
|
173
|
+
});
|
|
174
|
+
realPass.insertDebugMarker(markerLabel);
|
|
175
|
+
},
|
|
176
|
+
executeBundles(bundles) {
|
|
177
|
+
return realPass.executeBundles(bundles);
|
|
178
|
+
},
|
|
179
|
+
beginOcclusionQuery(queryIndex) {
|
|
180
|
+
return realPass.beginOcclusionQuery(queryIndex);
|
|
181
|
+
},
|
|
182
|
+
endOcclusionQuery() {
|
|
183
|
+
return realPass.endOcclusionQuery();
|
|
184
|
+
},
|
|
185
|
+
end() {
|
|
186
|
+
pushEvent(s, { kind: 'endRenderPass', passHandleId: passHId });
|
|
187
|
+
realPass.end();
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function createComputePassProxy(
|
|
193
|
+
s: RecorderInternal,
|
|
194
|
+
realPass: RhiComputePassEncoder,
|
|
195
|
+
passHId: HandleId,
|
|
196
|
+
): RhiComputePassEncoder {
|
|
197
|
+
return {
|
|
198
|
+
setPipeline(pipeline: ComputePipeline) {
|
|
199
|
+
const pid = getHandleId(s, pipeline as object, 'computePipeline');
|
|
200
|
+
pushEvent(s, { kind: 'setComputePipeline', passHandleId: passHId, pipelineHandleId: pid });
|
|
201
|
+
realPass.setPipeline(pipeline);
|
|
202
|
+
},
|
|
203
|
+
setBindGroup(index, bindGroup, dynamicOffsets) {
|
|
204
|
+
const bgid = getHandleId(s, bindGroup as object, 'bindGroup');
|
|
205
|
+
const recordedDynamicOffsets =
|
|
206
|
+
dynamicOffsets === undefined ? undefined : Array.from(dynamicOffsets);
|
|
207
|
+
pushEvent(s, {
|
|
208
|
+
kind: 'setBindGroup',
|
|
209
|
+
passHandleId: passHId,
|
|
210
|
+
index,
|
|
211
|
+
bindGroupHandleId: bgid,
|
|
212
|
+
dynamicOffsets: recordedDynamicOffsets,
|
|
213
|
+
});
|
|
214
|
+
realPass.setBindGroup(index, bindGroup, dynamicOffsets);
|
|
215
|
+
},
|
|
216
|
+
dispatchWorkgroups(x, y, z) {
|
|
217
|
+
pushEvent(s, {
|
|
218
|
+
kind: 'dispatchWorkgroups',
|
|
219
|
+
passHandleId: passHId,
|
|
220
|
+
x,
|
|
221
|
+
y: y ?? 1,
|
|
222
|
+
z: z ?? 1,
|
|
223
|
+
});
|
|
224
|
+
realPass.dispatchWorkgroups(x, y, z);
|
|
225
|
+
},
|
|
226
|
+
dispatchWorkgroupsIndirect(indirectBuffer, indirectOffset) {
|
|
227
|
+
const bufferHandleId = getHandleId(s, indirectBuffer as object, 'buffer');
|
|
228
|
+
pushEvent(s, {
|
|
229
|
+
kind: 'dispatchWorkgroupsIndirect',
|
|
230
|
+
passHandleId: passHId,
|
|
231
|
+
indirectBufferHandleId: bufferHandleId,
|
|
232
|
+
indirectOffset,
|
|
233
|
+
});
|
|
234
|
+
realPass.dispatchWorkgroupsIndirect(indirectBuffer, indirectOffset);
|
|
235
|
+
},
|
|
236
|
+
end() {
|
|
237
|
+
pushEvent(s, { kind: 'endComputePass', passHandleId: passHId });
|
|
238
|
+
realPass.end();
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { RhiCanvasContext, RhiDevice, RhiError, RhiInstance } from '@forgeax/engine-rhi';
|
|
2
|
+
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
3
|
+
import { createRhiDebugError, type RhiDebugError } from '../errors';
|
|
4
|
+
import {
|
|
5
|
+
type CreateShaderModuleFn,
|
|
6
|
+
type DebugRhiInstance,
|
|
7
|
+
wrap,
|
|
8
|
+
wrapCreateShaderModule,
|
|
9
|
+
} from '../recorder';
|
|
10
|
+
|
|
11
|
+
export interface RecordableBackend {
|
|
12
|
+
readonly rhi: RhiInstance & {
|
|
13
|
+
readonly acquireCanvasContext?: (
|
|
14
|
+
canvas: HTMLCanvasElement | OffscreenCanvas,
|
|
15
|
+
) => Result<RhiCanvasContext, RhiError>;
|
|
16
|
+
};
|
|
17
|
+
readonly createShaderModule: CreateShaderModuleFn;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface RecorderBackend extends RecordableBackend {
|
|
21
|
+
readonly unwrapDeviceForSurface: (device: RhiDevice) => Result<RhiDevice, RhiDebugError>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RecorderProxy {
|
|
25
|
+
readonly backend: RecorderBackend;
|
|
26
|
+
readonly recorder: DebugRhiInstance;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createRecorderProxy(backend: RecordableBackend): RecorderProxy {
|
|
30
|
+
const recorder = wrap(backend.rhi);
|
|
31
|
+
const wrappedCreateShaderModule = wrapCreateShaderModule(backend.createShaderModule, recorder);
|
|
32
|
+
const acquireCanvasContext = backend.rhi.acquireCanvasContext;
|
|
33
|
+
// Runtime's explicit-RHI seam receives the wrapped singleton as one value.
|
|
34
|
+
// Keep the standalone shader factory enumerable on that value so the
|
|
35
|
+
// renderer can preserve the complete typed backend pack after capture
|
|
36
|
+
// attachment; the public RhiInstance contract remains unchanged.
|
|
37
|
+
const rhi: RecordableBackend['rhi'] & { readonly createShaderModule: CreateShaderModuleFn } = {
|
|
38
|
+
requestAdapter: recorder.requestAdapter.bind(recorder),
|
|
39
|
+
createShaderModule: wrappedCreateShaderModule,
|
|
40
|
+
...(acquireCanvasContext === undefined
|
|
41
|
+
? {}
|
|
42
|
+
: { acquireCanvasContext: acquireCanvasContext.bind(backend.rhi) }),
|
|
43
|
+
};
|
|
44
|
+
const wrappedBackend: RecorderBackend = {
|
|
45
|
+
rhi,
|
|
46
|
+
createShaderModule: wrappedCreateShaderModule,
|
|
47
|
+
unwrapDeviceForSurface(device) {
|
|
48
|
+
const raw = (device as RhiDevice & { readonly _realDevice?: RhiDevice })._realDevice;
|
|
49
|
+
if (raw !== undefined) return ok(raw);
|
|
50
|
+
return err(
|
|
51
|
+
createRhiDebugError('capture-unavailable', {
|
|
52
|
+
stage: 'capture',
|
|
53
|
+
cause: 'the recorder backend does not expose a surface device resolver',
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
return { backend: wrappedBackend, recorder };
|
|
59
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// @forgeax/engine-rhi-debug/src/recorder/queue -- queue proxy owner.
|
|
2
|
+
|
|
3
|
+
import type { Buffer, CommandBuffer, RhiQueue } from '@forgeax/engine-rhi';
|
|
4
|
+
import type { RhiCallEventWriteTexture } from '../types';
|
|
5
|
+
import type { RecorderInternal } from './core';
|
|
6
|
+
import { getHandleId, pushEvent, shouldRecord, storeBlob } from './core';
|
|
7
|
+
|
|
8
|
+
export function createQueueProxy(s: RecorderInternal, realQueue: RhiQueue): RhiQueue {
|
|
9
|
+
return {
|
|
10
|
+
writeBuffer(
|
|
11
|
+
buffer: Buffer,
|
|
12
|
+
bufferOffset: number,
|
|
13
|
+
data: ArrayBufferView | ArrayBuffer,
|
|
14
|
+
dataOffset?: number,
|
|
15
|
+
size?: number,
|
|
16
|
+
) {
|
|
17
|
+
// Idle fast-path: when not recording, skip getHandleId + slice + the
|
|
18
|
+
// storeBlob hash-and-double-copy entirely (they would only feed a
|
|
19
|
+
// pushEvent that the state gate drops, and blobPool is reset on arm()).
|
|
20
|
+
// Same-condition-as-pushEvent, so a recording call still records.
|
|
21
|
+
if (!shouldRecord(s)) {
|
|
22
|
+
return realQueue.writeBuffer(buffer, bufferOffset, data, dataOffset, size);
|
|
23
|
+
}
|
|
24
|
+
const hId = getHandleId(s, buffer as object, 'buffer');
|
|
25
|
+
const raw = ArrayBuffer.isView(data)
|
|
26
|
+
? new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
|
27
|
+
: new Uint8Array(data as ArrayBuffer);
|
|
28
|
+
const sz = size ?? raw.byteLength - (dataOffset ?? 0);
|
|
29
|
+
const slice = raw.slice(dataOffset ?? 0, (dataOffset ?? 0) + sz);
|
|
30
|
+
const dataHash = storeBlob(s, slice.buffer as ArrayBuffer);
|
|
31
|
+
|
|
32
|
+
pushEvent(s, {
|
|
33
|
+
kind: 'writeBuffer',
|
|
34
|
+
handleId: hId,
|
|
35
|
+
bufferOffset,
|
|
36
|
+
dataHash,
|
|
37
|
+
size: sz,
|
|
38
|
+
});
|
|
39
|
+
return realQueue.writeBuffer(buffer, bufferOffset, data, dataOffset, size);
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
writeTexture(destination, data, dataLayout, copySize) {
|
|
43
|
+
// Idle fast-path (see writeBuffer): skip storeBlob hash+copy when the
|
|
44
|
+
// recorded event would be dropped anyway.
|
|
45
|
+
if (!shouldRecord(s)) {
|
|
46
|
+
return realQueue.writeTexture(destination, data, dataLayout, copySize);
|
|
47
|
+
}
|
|
48
|
+
const hId = getHandleId(s, destination.texture as object, 'texture');
|
|
49
|
+
const raw = ArrayBuffer.isView(data)
|
|
50
|
+
? new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
|
|
51
|
+
: new Uint8Array(data as ArrayBuffer);
|
|
52
|
+
const dataHash = storeBlob(s, raw.buffer as ArrayBuffer);
|
|
53
|
+
|
|
54
|
+
pushEvent(s, {
|
|
55
|
+
kind: 'writeTexture',
|
|
56
|
+
destination: {
|
|
57
|
+
textureHandleId: hId,
|
|
58
|
+
mipLevel: destination.mipLevel,
|
|
59
|
+
origin: destination.origin,
|
|
60
|
+
aspect: destination.aspect,
|
|
61
|
+
},
|
|
62
|
+
dataHash,
|
|
63
|
+
dataLayout: {
|
|
64
|
+
offset: dataLayout.offset,
|
|
65
|
+
bytesPerRow: dataLayout.bytesPerRow,
|
|
66
|
+
rowsPerImage: dataLayout.rowsPerImage,
|
|
67
|
+
},
|
|
68
|
+
size: copySize,
|
|
69
|
+
} as RhiCallEventWriteTexture);
|
|
70
|
+
return realQueue.writeTexture(destination, data, dataLayout, copySize);
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
copyExternalImageToTexture(source, destination, copySize) {
|
|
74
|
+
if (!shouldRecord(s)) {
|
|
75
|
+
return realQueue.copyExternalImageToTexture(source, destination, copySize);
|
|
76
|
+
}
|
|
77
|
+
const hId = getHandleId(s, destination.texture as object, 'texture');
|
|
78
|
+
pushEvent(s, {
|
|
79
|
+
kind: 'copyExternalImageToTexture',
|
|
80
|
+
source: { origin: source.origin, flipY: source.flipY },
|
|
81
|
+
destination: {
|
|
82
|
+
textureHandleId: hId,
|
|
83
|
+
mipLevel: destination.mipLevel,
|
|
84
|
+
origin: destination.origin,
|
|
85
|
+
aspect: destination.aspect,
|
|
86
|
+
colorSpace: destination.colorSpace,
|
|
87
|
+
premultipliedAlpha: destination.premultipliedAlpha,
|
|
88
|
+
},
|
|
89
|
+
copySize,
|
|
90
|
+
});
|
|
91
|
+
return realQueue.copyExternalImageToTexture(source, destination, copySize);
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
submit(commandBuffers: readonly CommandBuffer[]) {
|
|
95
|
+
if (!shouldRecord(s)) {
|
|
96
|
+
return realQueue.submit(commandBuffers);
|
|
97
|
+
}
|
|
98
|
+
const cmdHandleIds = commandBuffers.map((cb) =>
|
|
99
|
+
getHandleId(s, cb as object, 'commandBuffer'),
|
|
100
|
+
);
|
|
101
|
+
pushEvent(s, { kind: 'submit', cmdHandleIds });
|
|
102
|
+
return realQueue.submit(commandBuffers);
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
onSubmittedWorkDone() {
|
|
106
|
+
return realQueue.onSubmittedWorkDone();
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { DebugRhiInstance } from '../recorder';
|
|
2
|
+
import type { HandleId } from '../types';
|
|
3
|
+
|
|
4
|
+
export interface ResourceCandidate {
|
|
5
|
+
readonly handleId: HandleId;
|
|
6
|
+
readonly kind: 'buffer' | 'texture';
|
|
7
|
+
readonly estimatedBytes: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class ResourceRegistry {
|
|
11
|
+
constructor(private readonly recorder: DebugRhiInstance) {}
|
|
12
|
+
|
|
13
|
+
candidates(): readonly ResourceCandidate[] {
|
|
14
|
+
return Array.from(this.recorder.descriptorTable(), ([handleId, descriptor]) => ({
|
|
15
|
+
handleId,
|
|
16
|
+
kind: descriptor.kind,
|
|
17
|
+
estimatedBytes: estimateBytes(descriptor),
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
estimateSnapshotBytes(): number {
|
|
22
|
+
return this.candidates().reduce((total, candidate) => total + candidate.estimatedBytes, 0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
clearGeneration(): void {
|
|
26
|
+
this.recorder.transitionToError();
|
|
27
|
+
this.recorder.resetForDeviceLoss();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function estimateBytes(descriptor: {
|
|
32
|
+
readonly kind: 'buffer' | 'texture';
|
|
33
|
+
readonly size?: number | GPUExtent3DStrict;
|
|
34
|
+
}): number {
|
|
35
|
+
if (descriptor.kind === 'buffer') {
|
|
36
|
+
return typeof descriptor.size === 'number' ? descriptor.size : 0;
|
|
37
|
+
}
|
|
38
|
+
if (descriptor.size === undefined || typeof descriptor.size === 'number') return 4;
|
|
39
|
+
if (Array.isArray(descriptor.size)) {
|
|
40
|
+
return Math.max(
|
|
41
|
+
1,
|
|
42
|
+
(descriptor.size[0] ?? 1) * (descriptor.size[1] ?? 1) * (descriptor.size[2] ?? 1) * 4,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (!('width' in descriptor.size)) return 4;
|
|
46
|
+
return Math.max(
|
|
47
|
+
1,
|
|
48
|
+
descriptor.size.width *
|
|
49
|
+
(descriptor.size.height ?? 1) *
|
|
50
|
+
(descriptor.size.depthOrArrayLayers ?? 1) *
|
|
51
|
+
4,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
2
|
+
import { createRhiDebugError, type RhiDebugError } from '../errors';
|
|
3
|
+
import { assembleTape, type EncodedTape } from './assemble';
|
|
4
|
+
import { createRecorderProxy, type RecordableBackend, type RecorderBackend } from './proxy';
|
|
5
|
+
import { ResourceRegistry } from './resource-registry';
|
|
6
|
+
import { snapshotFrame } from './snapshot';
|
|
7
|
+
|
|
8
|
+
export interface CaptureFrameOptions {
|
|
9
|
+
readonly snapshotTimeoutMs?: number;
|
|
10
|
+
readonly byteBudget?: number;
|
|
11
|
+
readonly signal?: AbortSignal;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RecorderOptions {
|
|
15
|
+
readonly snapshotTimeoutMs?: number;
|
|
16
|
+
readonly byteBudget?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RecorderAttachment {
|
|
20
|
+
readonly backend: RecorderBackend;
|
|
21
|
+
captureFrame(options?: CaptureFrameOptions): Promise<Result<EncodedTape, RhiDebugError>>;
|
|
22
|
+
frameBoundary(): Promise<Result<void, RhiDebugError>>;
|
|
23
|
+
deviceLost(): void;
|
|
24
|
+
dispose(): Promise<Result<void, RhiDebugError>>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type Phase = 'idle' | 'armed' | 'snapshotting' | 'recording' | 'disposed';
|
|
28
|
+
|
|
29
|
+
export function attachRecorder(
|
|
30
|
+
backend: RecordableBackend,
|
|
31
|
+
options: RecorderOptions = {},
|
|
32
|
+
): Result<RecorderAttachment, RhiDebugError> {
|
|
33
|
+
const proxy = createRecorderProxy(backend);
|
|
34
|
+
const registry = new ResourceRegistry(proxy.recorder);
|
|
35
|
+
let phase: Phase = 'idle';
|
|
36
|
+
let generation = 0;
|
|
37
|
+
let active:
|
|
38
|
+
| {
|
|
39
|
+
readonly generation: number;
|
|
40
|
+
readonly options: CaptureFrameOptions;
|
|
41
|
+
readonly resolve: (result: Result<EncodedTape, RhiDebugError>) => void;
|
|
42
|
+
readonly signal?: AbortSignal;
|
|
43
|
+
abortListener?: () => void;
|
|
44
|
+
}
|
|
45
|
+
| undefined;
|
|
46
|
+
|
|
47
|
+
const fail = <T>(
|
|
48
|
+
code: 'capture-unavailable' | 'capture-busy',
|
|
49
|
+
cause: string,
|
|
50
|
+
): Result<T, RhiDebugError> => err(createRhiDebugError(code, { stage: 'capture', cause }));
|
|
51
|
+
|
|
52
|
+
const settle = (result: Result<EncodedTape, RhiDebugError>): void => {
|
|
53
|
+
const request = active;
|
|
54
|
+
if (request === undefined) return;
|
|
55
|
+
active = undefined;
|
|
56
|
+
request.signal?.removeEventListener('abort', request.abortListener ?? (() => {}));
|
|
57
|
+
if (!result.ok && proxy.recorder.getState() === 'error') proxy.recorder.disposeError();
|
|
58
|
+
phase = phase === 'disposed' ? 'disposed' : 'idle';
|
|
59
|
+
request.resolve(result);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const abort = (): void => {
|
|
63
|
+
if (active === undefined) return;
|
|
64
|
+
proxy.recorder.transitionToError();
|
|
65
|
+
settle(fail('capture-unavailable', 'capture request was aborted'));
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const attachment: RecorderAttachment = {
|
|
69
|
+
backend: proxy.backend,
|
|
70
|
+
captureFrame(captureOptions = {}) {
|
|
71
|
+
if (phase === 'disposed')
|
|
72
|
+
return Promise.resolve(fail('capture-unavailable', 'recorder is disposed'));
|
|
73
|
+
if (active !== undefined)
|
|
74
|
+
return Promise.resolve(fail('capture-busy', 'another capture is active'));
|
|
75
|
+
const signal = captureOptions.signal;
|
|
76
|
+
if (signal?.aborted)
|
|
77
|
+
return Promise.resolve(fail('capture-unavailable', 'capture request was aborted'));
|
|
78
|
+
const arm = proxy.recorder.arm(1);
|
|
79
|
+
if (!arm.ok) return Promise.resolve(fail('capture-busy', arm.error.hint));
|
|
80
|
+
phase = 'armed';
|
|
81
|
+
generation += 1;
|
|
82
|
+
return new Promise<Result<EncodedTape, RhiDebugError>>((resolve) => {
|
|
83
|
+
const request = {
|
|
84
|
+
generation,
|
|
85
|
+
options: captureOptions,
|
|
86
|
+
resolve,
|
|
87
|
+
...(signal === undefined ? {} : { signal }),
|
|
88
|
+
} as {
|
|
89
|
+
readonly generation: number;
|
|
90
|
+
readonly options: CaptureFrameOptions;
|
|
91
|
+
readonly resolve: (result: Result<EncodedTape, RhiDebugError>) => void;
|
|
92
|
+
readonly signal?: AbortSignal;
|
|
93
|
+
abortListener?: () => void;
|
|
94
|
+
};
|
|
95
|
+
request.abortListener = abort;
|
|
96
|
+
active = request;
|
|
97
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
async frameBoundary() {
|
|
101
|
+
if (phase === 'disposed' || active === undefined) return ok(undefined);
|
|
102
|
+
const request = active;
|
|
103
|
+
if (request.generation !== generation) return ok(undefined);
|
|
104
|
+
if (phase === 'armed') {
|
|
105
|
+
phase = 'snapshotting';
|
|
106
|
+
const result = await snapshotFrame(proxy.recorder, registry, {
|
|
107
|
+
snapshotTimeoutMs:
|
|
108
|
+
request.options.snapshotTimeoutMs ?? options.snapshotTimeoutMs ?? 30_000,
|
|
109
|
+
byteBudget: request.options.byteBudget ?? options.byteBudget ?? Number.MAX_SAFE_INTEGER,
|
|
110
|
+
});
|
|
111
|
+
if (!result.ok) {
|
|
112
|
+
proxy.recorder.transitionToError();
|
|
113
|
+
settle(err(result.error));
|
|
114
|
+
return err(result.error);
|
|
115
|
+
}
|
|
116
|
+
phase = 'recording';
|
|
117
|
+
return ok(undefined);
|
|
118
|
+
}
|
|
119
|
+
if (phase !== 'recording') return ok(undefined);
|
|
120
|
+
proxy.recorder.onFrameEnd();
|
|
121
|
+
if (proxy.recorder.getState() !== 'idle') return ok(undefined);
|
|
122
|
+
const result = assembleTape(proxy.recorder);
|
|
123
|
+
settle(result);
|
|
124
|
+
return result.ok ? ok(undefined) : err(result.error);
|
|
125
|
+
},
|
|
126
|
+
deviceLost() {
|
|
127
|
+
generation += 1;
|
|
128
|
+
registry.clearGeneration();
|
|
129
|
+
if (active !== undefined) {
|
|
130
|
+
phase = 'idle';
|
|
131
|
+
settle(fail('capture-unavailable', 'device loss invalidated the active generation'));
|
|
132
|
+
} else if (phase !== 'disposed') {
|
|
133
|
+
phase = 'idle';
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
async dispose() {
|
|
137
|
+
if (phase === 'disposed') return ok(undefined);
|
|
138
|
+
if (active !== undefined) {
|
|
139
|
+
proxy.recorder.transitionToError();
|
|
140
|
+
settle(fail('capture-unavailable', 'recorder was disposed during capture'));
|
|
141
|
+
}
|
|
142
|
+
proxy.recorder.disposeError();
|
|
143
|
+
phase = 'disposed';
|
|
144
|
+
return ok(undefined);
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
return ok(attachment);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export type { CreateShaderModuleFn } from '../recorder';
|
|
151
|
+
export type { EncodedTape } from './assemble';
|
|
152
|
+
export type { RecordableBackend, RecorderBackend } from './proxy';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @forgeax/engine-rhi-debug/src/recorder/shader -- standalone shader factory proxy.
|
|
2
|
+
|
|
3
|
+
import type { Result, RhiDevice, ShaderModule } from '@forgeax/engine-rhi';
|
|
4
|
+
import type { HandleId } from '../types';
|
|
5
|
+
import type { CreateShaderModuleFn, DebugRhiInstance } from './core';
|
|
6
|
+
|
|
7
|
+
export function wrapCreateShaderModule(
|
|
8
|
+
originalFn: CreateShaderModuleFn,
|
|
9
|
+
debugInst: DebugRhiInstance,
|
|
10
|
+
): CreateShaderModuleFn {
|
|
11
|
+
return async function wrappedCreateShaderModule(
|
|
12
|
+
device: RhiDevice,
|
|
13
|
+
desc: { code: string; label?: string | undefined },
|
|
14
|
+
): Promise<Result<ShaderModule, import('@forgeax/engine-rhi').RhiError>> {
|
|
15
|
+
// The renderer threads the proxied RhiDevice (from proxyDevice()) here, but
|
|
16
|
+
// engine-rhi-webgpu's createShaderModule reverse-looks-up the GPUDevice via
|
|
17
|
+
// a WeakMap keyed on the RhiDevice that makeRhiDevice registered. The proxy
|
|
18
|
+
// is a different JS object, so WeakMap.get(proxy) is undefined and the real
|
|
19
|
+
// fn returns shader-compile-failed ("unregistered RhiDevice"). Unwrap the
|
|
20
|
+
// proxy to the registered device via the _realDevice escape hatch that
|
|
21
|
+
// proxyDevice exposes for exactly this purpose.
|
|
22
|
+
const realDevice = (device as RhiDevice & { _realDevice?: RhiDevice })._realDevice ?? device;
|
|
23
|
+
const result = await originalFn(realDevice, desc);
|
|
24
|
+
if (!result.ok) return result;
|
|
25
|
+
|
|
26
|
+
// I-12 (round 1 implement-review) fix: route the createShaderModule
|
|
27
|
+
// event through the same `pushExternalEvent` helper that wraps the
|
|
28
|
+
// internal `pushEvent` (and therefore the `_skipRecord` + state-machine
|
|
29
|
+
// guard). The previous `(events as RhiCallEvent[]).push(...)` cast
|
|
30
|
+
// bypassed those guards — recorder-internal RHI calls during shader
|
|
31
|
+
// construction would have self-polluted the tape.
|
|
32
|
+
const hId = debugInst.pushExternalCreateEvent(result.value, 'shaderModule', {
|
|
33
|
+
kind: 'createShaderModule',
|
|
34
|
+
handleId: '' as HandleId,
|
|
35
|
+
wgslCode: desc.code,
|
|
36
|
+
});
|
|
37
|
+
// Register the shader module handle in the recorder's handleMap so
|
|
38
|
+
// downstream pipeline events (createRenderPipeline / createComputePipeline)
|
|
39
|
+
// can resolve the handleId via getHandleId. Required for cross-device
|
|
40
|
+
// replay: the tape's pipeline desc carries live GPU module objects which
|
|
41
|
+
// are device-bound, so the replayer must swap them with re-created shader
|
|
42
|
+
// modules from the handleMap using the handleId.
|
|
43
|
+
debugInst.registerShaderModule(result.value, hId);
|
|
44
|
+
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
}
|