@forgeax/engine-rhi-debug 0.1.28 → 0.1.30
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/README.md +32 -7
- package/dist/__tests__/query-set-lifecycle.unit.test.d.ts +2 -0
- package/dist/__tests__/query-set-lifecycle.unit.test.d.ts.map +1 -0
- package/dist/__tests__/query-set-replay-fixture.d.ts +39 -0
- package/dist/__tests__/query-set-replay-fixture.d.ts.map +1 -0
- package/dist/__tests__/recorder-canvas-context.unit.test.d.ts +2 -0
- package/dist/__tests__/recorder-canvas-context.unit.test.d.ts.map +1 -0
- package/dist/__tests__/timestamp-query-capture.dawn.test.d.ts +2 -0
- package/dist/__tests__/timestamp-query-capture.dawn.test.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +527 -58
- package/dist/index.mjs.map +1 -1
- package/dist/protocol/event-semantics.d.ts +1 -1
- package/dist/protocol/event-semantics.d.ts.map +1 -1
- package/dist/protocol/types.d.ts +1 -1
- package/dist/protocol/types.d.ts.map +1 -1
- package/dist/recorder/closure.d.ts.map +1 -1
- package/dist/recorder/core.d.ts +10 -1
- package/dist/recorder/core.d.ts.map +1 -1
- package/dist/recorder/device.d.ts.map +1 -1
- package/dist/recorder/encoder.d.ts.map +1 -1
- package/dist/recorder/pass.d.ts.map +1 -1
- package/dist/recorder/pipeline.d.ts +5 -0
- package/dist/recorder/pipeline.d.ts.map +1 -0
- package/dist/recorder/proxy.d.ts +3 -1
- package/dist/recorder/proxy.d.ts.map +1 -1
- package/dist/recorder/session.d.ts +1 -1
- package/dist/recorder/session.d.ts.map +1 -1
- package/dist/recorder/shader.d.ts +3 -1
- package/dist/recorder/shader.d.ts.map +1 -1
- package/dist/recorder.d.ts +2 -2
- package/dist/recorder.d.ts.map +1 -1
- package/dist/replay/execute.d.ts.map +1 -1
- package/dist/replay/resources.d.ts +4 -1
- package/dist/replay/resources.d.ts.map +1 -1
- package/dist/replay/session.d.ts +7 -0
- package/dist/replay/session.d.ts.map +1 -1
- package/dist/types.d.ts +45 -6
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/__tests__/coverage-invariant.test-d.ts +4 -5
- package/src/__tests__/coverage-invariant.unit.test.ts +6 -17
- package/src/__tests__/e2e.browser.test.ts +50 -0
- package/src/__tests__/guard-gates.test.ts +1 -1
- package/src/__tests__/query-set-lifecycle.unit.test.ts +265 -0
- package/src/__tests__/query-set-replay-fixture.ts +553 -0
- package/src/__tests__/readback-format-matrix.dawn.test.ts +9 -2
- package/src/__tests__/recorder-canvas-context.unit.test.ts +67 -0
- package/src/__tests__/recorder-session.integration.test.ts +55 -0
- package/src/__tests__/recorder-session.unit.test.ts +18 -1
- package/src/__tests__/replay-session.dawn.test.ts +171 -0
- package/src/__tests__/replay-session.test-d.ts +1 -0
- package/src/__tests__/rhi-debug-fresh-replay.dawn.test.ts +61 -0
- package/src/__tests__/timestamp-query-capture.dawn.test.ts +61 -0
- package/src/index.ts +1 -0
- package/src/protocol/event-semantics.ts +21 -2
- package/src/protocol/types.ts +1 -0
- package/src/protocol/validation.ts +36 -0
- package/src/recorder/assemble.ts +8 -1
- package/src/recorder/closure.ts +21 -1
- package/src/recorder/core.ts +11 -0
- package/src/recorder/device.ts +33 -6
- package/src/recorder/encoder.ts +60 -6
- package/src/recorder/pass.ts +7 -2
- package/src/recorder/pipeline.ts +31 -0
- package/src/recorder/proxy.ts +58 -2
- package/src/recorder/session.ts +1 -1
- package/src/recorder/shader.ts +29 -22
- package/src/recorder.ts +6 -2
- package/src/replay/execute.ts +143 -1
- package/src/replay/readback.ts +15 -3
- package/src/replay/resources.ts +6 -0
- package/src/replay/session.ts +241 -27
- package/src/types.ts +61 -14
package/src/recorder/closure.ts
CHANGED
|
@@ -135,17 +135,22 @@ export function _collectFrameReferencedHandleIds(events: readonly RhiCallEvent[]
|
|
|
135
135
|
cmdHandleId: HandleId;
|
|
136
136
|
colorAttachmentViewHandleIds: readonly (HandleId | undefined)[];
|
|
137
137
|
depthStencilViewHandleId?: HandleId;
|
|
138
|
+
occlusionQuerySetHandleId?: HandleId;
|
|
139
|
+
timestampQuerySetHandleId?: HandleId;
|
|
138
140
|
};
|
|
139
141
|
refs.add(we.cmdHandleId);
|
|
140
142
|
for (const vhId of we.colorAttachmentViewHandleIds) {
|
|
141
143
|
if (vhId !== undefined) refs.add(vhId);
|
|
142
144
|
}
|
|
143
145
|
if (we.depthStencilViewHandleId !== undefined) refs.add(we.depthStencilViewHandleId);
|
|
146
|
+
if (we.occlusionQuerySetHandleId !== undefined) refs.add(we.occlusionQuerySetHandleId);
|
|
147
|
+
if (we.timestampQuerySetHandleId !== undefined) refs.add(we.timestampQuerySetHandleId);
|
|
144
148
|
break;
|
|
145
149
|
}
|
|
146
150
|
case 'beginComputePass': {
|
|
147
|
-
const we = e as { cmdHandleId: HandleId };
|
|
151
|
+
const we = e as { cmdHandleId: HandleId; timestampQuerySetHandleId?: HandleId };
|
|
148
152
|
refs.add(we.cmdHandleId);
|
|
153
|
+
if (we.timestampQuerySetHandleId !== undefined) refs.add(we.timestampQuerySetHandleId);
|
|
149
154
|
break;
|
|
150
155
|
}
|
|
151
156
|
case 'finish': {
|
|
@@ -204,6 +209,7 @@ export function _collectFrameReferencedHandleIds(events: readonly RhiCallEvent[]
|
|
|
204
209
|
break;
|
|
205
210
|
}
|
|
206
211
|
case 'createBindGroup':
|
|
212
|
+
case 'getBindGroupLayout':
|
|
207
213
|
case 'createPipelineLayout':
|
|
208
214
|
case 'createRenderPipeline':
|
|
209
215
|
case 'createComputePipeline':
|
|
@@ -223,17 +229,28 @@ export function _collectFrameReferencedHandleIds(events: readonly RhiCallEvent[]
|
|
|
223
229
|
}
|
|
224
230
|
case 'destroyBuffer':
|
|
225
231
|
case 'destroyTexture':
|
|
232
|
+
case 'destroyQuerySet':
|
|
226
233
|
refs.add(e.handleId);
|
|
227
234
|
break;
|
|
228
235
|
case 'frameMark':
|
|
229
236
|
case 'createBuffer':
|
|
230
237
|
case 'createTexture':
|
|
238
|
+
case 'createQuerySet':
|
|
231
239
|
case 'createSampler':
|
|
232
240
|
case 'createBindGroupLayout':
|
|
233
241
|
case 'createShaderModule':
|
|
234
242
|
case 'createCommandEncoder':
|
|
235
243
|
// Leaf declaration events — no backward references to collect.
|
|
236
244
|
break;
|
|
245
|
+
case 'resolveQuerySet':
|
|
246
|
+
refs.add(e.cmdHandleId);
|
|
247
|
+
refs.add(e.querySetHandleId);
|
|
248
|
+
refs.add(e.destinationHandleId);
|
|
249
|
+
break;
|
|
250
|
+
case 'beginOcclusionQuery':
|
|
251
|
+
case 'endOcclusionQuery':
|
|
252
|
+
refs.add(e.passHandleId);
|
|
253
|
+
break;
|
|
237
254
|
default: {
|
|
238
255
|
// Exhaustiveness guard: if a new RhiCallEvent member is added to the
|
|
239
256
|
// union without a corresponding handle-collection case, tsc fails here.
|
|
@@ -292,8 +309,11 @@ export function _getCreateEventReferencedHandleIds(event: RhiCallEvent): HandleI
|
|
|
292
309
|
const e = event as { sourceHandleId: HandleId };
|
|
293
310
|
return [e.sourceHandleId];
|
|
294
311
|
}
|
|
312
|
+
case 'getBindGroupLayout':
|
|
313
|
+
return [(event as { pipelineHandleId: HandleId }).pipelineHandleId];
|
|
295
314
|
case 'createBuffer':
|
|
296
315
|
case 'createTexture':
|
|
316
|
+
case 'createQuerySet':
|
|
297
317
|
case 'createSampler':
|
|
298
318
|
case 'createBindGroupLayout':
|
|
299
319
|
case 'createShaderModule':
|
package/src/recorder/core.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type {
|
|
6
6
|
Result,
|
|
7
7
|
RhiDevice,
|
|
8
|
+
RhiError,
|
|
8
9
|
RhiInstance,
|
|
9
10
|
ShaderModule,
|
|
10
11
|
TextureView,
|
|
@@ -557,6 +558,16 @@ export type CreateShaderModuleFn = (
|
|
|
557
558
|
desc: { code: string; label?: string | undefined },
|
|
558
559
|
) => Promise<Result<ShaderModule, import('@forgeax/engine-rhi').RhiError>>;
|
|
559
560
|
|
|
561
|
+
/**
|
|
562
|
+
* Synchronous shader-module factory used by the render path's immediate
|
|
563
|
+
* adapter. Keep it in the recordable backend contract so attaching RHI-debug
|
|
564
|
+
* does not silently remove the renderer's first-use fast path.
|
|
565
|
+
*/
|
|
566
|
+
export type CreateShaderModuleImmediateFn = (
|
|
567
|
+
device: RhiDevice,
|
|
568
|
+
desc: { code: string; label?: string | undefined },
|
|
569
|
+
) => Result<ShaderModule, RhiError>;
|
|
570
|
+
|
|
560
571
|
export type { RecorderInternal, SnapshotProgress };
|
|
561
572
|
export {
|
|
562
573
|
addSwapchainViewFormat,
|
package/src/recorder/device.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
TEXTURE_USAGE_COPY_SRC,
|
|
41
41
|
} from './core';
|
|
42
42
|
import { createCommandEncoderProxy } from './encoder';
|
|
43
|
+
import { wrapPipeline } from './pipeline';
|
|
43
44
|
import { createQueueProxy } from './queue';
|
|
44
45
|
|
|
45
46
|
export function createDeviceProxy(s: RecorderInternal, realDevice: RhiDevice): RhiDevice {
|
|
@@ -338,9 +339,9 @@ export function createDeviceProxy(s: RecorderInternal, realDevice: RhiDevice): R
|
|
|
338
339
|
vertexShaderModuleHandleId,
|
|
339
340
|
fragmentShaderModuleHandleId,
|
|
340
341
|
};
|
|
341
|
-
registerHandle(s, res.value as object, 'renderPipeline', event);
|
|
342
|
+
const pipelineId = registerHandle(s, res.value as object, 'renderPipeline', event);
|
|
342
343
|
pushEvent(s, event);
|
|
343
|
-
return res;
|
|
344
|
+
return makeOk(wrapPipeline(s, res.value, pipelineId));
|
|
344
345
|
},
|
|
345
346
|
|
|
346
347
|
createComputePipeline(desc: ComputePipelineDescriptor) {
|
|
@@ -364,13 +365,26 @@ export function createDeviceProxy(s: RecorderInternal, realDevice: RhiDevice): R
|
|
|
364
365
|
layoutHandleId: layoutId,
|
|
365
366
|
computeShaderModuleHandleId,
|
|
366
367
|
};
|
|
367
|
-
registerHandle(s, res.value as object, 'computePipeline', event);
|
|
368
|
+
const pipelineId = registerHandle(s, res.value as object, 'computePipeline', event);
|
|
368
369
|
pushEvent(s, event);
|
|
369
|
-
return res;
|
|
370
|
+
return makeOk(wrapPipeline(s, res.value, pipelineId));
|
|
370
371
|
},
|
|
371
372
|
|
|
372
373
|
createQuerySet(desc: QuerySetDescriptor) {
|
|
373
|
-
|
|
374
|
+
const res = realDevice.createQuerySet(desc);
|
|
375
|
+
if (!res.ok) return res;
|
|
376
|
+
const event: RhiCallEvent = {
|
|
377
|
+
kind: 'createQuerySet',
|
|
378
|
+
handleId: '' as HandleId,
|
|
379
|
+
desc: {
|
|
380
|
+
...(desc.label === undefined ? {} : { label: desc.label }),
|
|
381
|
+
type: desc.type ?? 'occlusion',
|
|
382
|
+
count: desc.count ?? 0,
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
registerHandle(s, res.value as object, 'querySet', event);
|
|
386
|
+
pushEvent(s, event);
|
|
387
|
+
return res;
|
|
374
388
|
},
|
|
375
389
|
|
|
376
390
|
destroyBuffer(buf: Buffer) {
|
|
@@ -419,7 +433,20 @@ export function createDeviceProxy(s: RecorderInternal, realDevice: RhiDevice): R
|
|
|
419
433
|
},
|
|
420
434
|
|
|
421
435
|
destroyQuerySet(querySet: QuerySet) {
|
|
422
|
-
|
|
436
|
+
const hId = s.handleMap.get(querySet as object);
|
|
437
|
+
const res = realDevice.destroyQuerySet(querySet);
|
|
438
|
+
if (!res.ok) return res;
|
|
439
|
+
if (hId !== undefined) {
|
|
440
|
+
if (
|
|
441
|
+
!retainsCaptureBootstrap(s) &&
|
|
442
|
+
!s.snapshotSeededHandles.has(hId) &&
|
|
443
|
+
!hasBootstrapDependency(s, hId)
|
|
444
|
+
) {
|
|
445
|
+
s.bootstrapCreates.delete(hId);
|
|
446
|
+
}
|
|
447
|
+
pushEvent(s, { kind: 'destroyQuerySet', handleId: hId });
|
|
448
|
+
}
|
|
449
|
+
return res;
|
|
423
450
|
},
|
|
424
451
|
|
|
425
452
|
destroyTexture(tex: Texture) {
|
package/src/recorder/encoder.ts
CHANGED
|
@@ -54,6 +54,10 @@ export function createCommandEncoderProxy(
|
|
|
54
54
|
kind: 'beginRenderPass',
|
|
55
55
|
cmdHandleId: cmdHId,
|
|
56
56
|
passHandleId: passHId,
|
|
57
|
+
timestampQuerySetHandleId:
|
|
58
|
+
desc?.timestampWrites === undefined
|
|
59
|
+
? undefined
|
|
60
|
+
: getHandleId(s, desc.timestampWrites.querySet as object, 'querySet'),
|
|
57
61
|
desc: {
|
|
58
62
|
colorAttachments: Array.from(desc.colorAttachments).map((attachment) =>
|
|
59
63
|
attachment === null || attachment === undefined ? attachment : { ...attachment },
|
|
@@ -61,15 +65,23 @@ export function createCommandEncoderProxy(
|
|
|
61
65
|
...(desc.depthStencilAttachment === undefined
|
|
62
66
|
? {}
|
|
63
67
|
: { depthStencilAttachment: { ...desc.depthStencilAttachment } }),
|
|
64
|
-
...(desc.
|
|
68
|
+
...(desc.timestampWrites === undefined
|
|
65
69
|
? {}
|
|
66
|
-
: {
|
|
67
|
-
...(desc.timestampWrites === undefined ? {} : { timestampWrites: desc.timestampWrites }),
|
|
70
|
+
: { timestampWrites: recordTimestampWrites(desc.timestampWrites) }),
|
|
68
71
|
...(desc.maxDrawCount === undefined ? {} : { maxDrawCount: desc.maxDrawCount }),
|
|
69
72
|
},
|
|
70
73
|
colorAttachmentViewHandleIds,
|
|
71
74
|
colorAttachmentResolveTargetHandleIds,
|
|
72
75
|
depthStencilViewHandleId,
|
|
76
|
+
...(desc.occlusionQuerySet === undefined
|
|
77
|
+
? {}
|
|
78
|
+
: {
|
|
79
|
+
occlusionQuerySetHandleId: getHandleId(
|
|
80
|
+
s,
|
|
81
|
+
desc.occlusionQuerySet as object,
|
|
82
|
+
'querySet',
|
|
83
|
+
),
|
|
84
|
+
}),
|
|
73
85
|
});
|
|
74
86
|
const realPass = realEnc.beginRenderPass(desc);
|
|
75
87
|
return createRenderPassProxy(s, realPass, passHId);
|
|
@@ -80,7 +92,17 @@ export function createCommandEncoderProxy(
|
|
|
80
92
|
kind: 'beginComputePass',
|
|
81
93
|
cmdHandleId: cmdHId,
|
|
82
94
|
passHandleId: passHId,
|
|
83
|
-
|
|
95
|
+
timestampQuerySetHandleId:
|
|
96
|
+
desc?.timestampWrites === undefined
|
|
97
|
+
? undefined
|
|
98
|
+
: getHandleId(s, desc.timestampWrites.querySet as object, 'querySet'),
|
|
99
|
+
desc:
|
|
100
|
+
desc === undefined
|
|
101
|
+
? undefined
|
|
102
|
+
: {
|
|
103
|
+
...(desc.label === undefined ? {} : { label: desc.label }),
|
|
104
|
+
timestampWrites: recordTimestampWrites(desc.timestampWrites),
|
|
105
|
+
},
|
|
84
106
|
});
|
|
85
107
|
const realPass = realEnc.beginComputePass(desc);
|
|
86
108
|
return createComputePassProxy(s, realPass, passHId);
|
|
@@ -91,7 +113,14 @@ export function createCommandEncoderProxy(
|
|
|
91
113
|
kind: 'beginComputePass',
|
|
92
114
|
cmdHandleId: cmdHId,
|
|
93
115
|
passHandleId: passHId,
|
|
94
|
-
|
|
116
|
+
timestampQuerySetHandleId:
|
|
117
|
+
desc?.timestampWrites === undefined
|
|
118
|
+
? undefined
|
|
119
|
+
: getHandleId(s, desc.timestampWrites.querySet as object, 'querySet'),
|
|
120
|
+
desc: {
|
|
121
|
+
...(desc.label === undefined ? {} : { label: desc.label }),
|
|
122
|
+
timestampWrites: recordTimestampWrites(desc.timestampWrites),
|
|
123
|
+
},
|
|
95
124
|
});
|
|
96
125
|
realEnc.encodeEmptyComputePass(desc);
|
|
97
126
|
pushEvent(s, { kind: 'endComputePass', passHandleId: passHId });
|
|
@@ -220,13 +249,25 @@ export function createCommandEncoderProxy(
|
|
|
220
249
|
realEnc.clearBuffer(buffer, offset, size);
|
|
221
250
|
},
|
|
222
251
|
resolveQuerySet(querySet, firstQuery, queryCount, destination, destinationOffset) {
|
|
223
|
-
|
|
252
|
+
const result = realEnc.resolveQuerySet(
|
|
224
253
|
querySet,
|
|
225
254
|
firstQuery,
|
|
226
255
|
queryCount,
|
|
227
256
|
destination,
|
|
228
257
|
destinationOffset,
|
|
229
258
|
);
|
|
259
|
+
if (result.ok) {
|
|
260
|
+
pushEvent(s, {
|
|
261
|
+
kind: 'resolveQuerySet',
|
|
262
|
+
cmdHandleId: cmdHId,
|
|
263
|
+
querySetHandleId: getHandleId(s, querySet as object, 'querySet'),
|
|
264
|
+
firstQuery,
|
|
265
|
+
queryCount,
|
|
266
|
+
destinationHandleId: getHandleId(s, destination as object, 'buffer'),
|
|
267
|
+
destinationOffset,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return result;
|
|
230
271
|
},
|
|
231
272
|
pushDebugGroup(groupLabel) {
|
|
232
273
|
pushEvent(s, { kind: 'pushDebugGroup', cmdHandleId: cmdHId, groupLabel });
|
|
@@ -257,3 +298,16 @@ export function createCommandEncoderProxy(
|
|
|
257
298
|
},
|
|
258
299
|
};
|
|
259
300
|
}
|
|
301
|
+
|
|
302
|
+
function recordTimestampWrites(writes: RenderPassDescriptor['timestampWrites']) {
|
|
303
|
+
return writes === undefined
|
|
304
|
+
? undefined
|
|
305
|
+
: {
|
|
306
|
+
...(writes.beginningOfPassWriteIndex === undefined
|
|
307
|
+
? {}
|
|
308
|
+
: { beginningOfPassWriteIndex: writes.beginningOfPassWriteIndex }),
|
|
309
|
+
...(writes.endOfPassWriteIndex === undefined
|
|
310
|
+
? {}
|
|
311
|
+
: { endOfPassWriteIndex: writes.endOfPassWriteIndex }),
|
|
312
|
+
};
|
|
313
|
+
}
|
package/src/recorder/pass.ts
CHANGED
|
@@ -177,10 +177,15 @@ export function createRenderPassProxy(
|
|
|
177
177
|
return realPass.executeBundles(bundles);
|
|
178
178
|
},
|
|
179
179
|
beginOcclusionQuery(queryIndex) {
|
|
180
|
-
|
|
180
|
+
const result = realPass.beginOcclusionQuery(queryIndex);
|
|
181
|
+
if (result.ok)
|
|
182
|
+
pushEvent(s, { kind: 'beginOcclusionQuery', passHandleId: passHId, queryIndex });
|
|
183
|
+
return result;
|
|
181
184
|
},
|
|
182
185
|
endOcclusionQuery() {
|
|
183
|
-
|
|
186
|
+
const result = realPass.endOcclusionQuery();
|
|
187
|
+
if (result.ok) pushEvent(s, { kind: 'endOcclusionQuery', passHandleId: passHId });
|
|
188
|
+
return result;
|
|
184
189
|
},
|
|
185
190
|
end() {
|
|
186
191
|
pushEvent(s, { kind: 'endRenderPass', passHandleId: passHId });
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ComputePipeline, RenderPipeline, RhiRenderPipelineOps } from '@forgeax/engine-rhi';
|
|
2
|
+
import type { HandleId } from '../types';
|
|
3
|
+
import { pushEvent, type RecorderInternal, registerHandle } from './core';
|
|
4
|
+
|
|
5
|
+
export function wrapPipeline<T extends RenderPipeline | ComputePipeline>(
|
|
6
|
+
state: RecorderInternal,
|
|
7
|
+
pipeline: T,
|
|
8
|
+
pipelineHandleId: HandleId,
|
|
9
|
+
): T {
|
|
10
|
+
// Preserve native pipeline identity: renderer-owned command paths may pass
|
|
11
|
+
// the opaque handle directly to WebGPU, which rejects a JavaScript Proxy.
|
|
12
|
+
const operation = (pipeline as T & Partial<RhiRenderPipelineOps>).getBindGroupLayout;
|
|
13
|
+
if (operation === undefined) return pipeline;
|
|
14
|
+
const getLayout = operation.bind(pipeline);
|
|
15
|
+
Object.defineProperty(pipeline, 'getBindGroupLayout', {
|
|
16
|
+
configurable: true,
|
|
17
|
+
value: (index: number) => {
|
|
18
|
+
const layout = getLayout(index);
|
|
19
|
+
const event = {
|
|
20
|
+
kind: 'getBindGroupLayout' as const,
|
|
21
|
+
handleId: '' as HandleId,
|
|
22
|
+
pipelineHandleId,
|
|
23
|
+
index,
|
|
24
|
+
};
|
|
25
|
+
registerHandle(state, layout, 'bindGroupLayout', event);
|
|
26
|
+
pushEvent(state, event);
|
|
27
|
+
return layout;
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
return pipeline;
|
|
31
|
+
}
|
package/src/recorder/proxy.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { err, ok, type Result } from '@forgeax/engine-types';
|
|
|
3
3
|
import { createRhiDebugError, type RhiDebugError } from '../errors';
|
|
4
4
|
import {
|
|
5
5
|
type CreateShaderModuleFn,
|
|
6
|
+
type CreateShaderModuleImmediateFn,
|
|
6
7
|
type DebugRhiInstance,
|
|
7
8
|
wrap,
|
|
8
9
|
wrapCreateShaderModule,
|
|
@@ -15,6 +16,8 @@ export interface RecordableBackend {
|
|
|
15
16
|
) => Result<RhiCanvasContext, RhiError>;
|
|
16
17
|
};
|
|
17
18
|
readonly createShaderModule: CreateShaderModuleFn;
|
|
19
|
+
/** Optional synchronous factory retained for render's first-use path. */
|
|
20
|
+
readonly createShaderModuleImmediate?: CreateShaderModuleImmediateFn;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
export interface RecorderBackend extends RecordableBackend {
|
|
@@ -29,21 +32,74 @@ export interface RecorderProxy {
|
|
|
29
32
|
export function createRecorderProxy(backend: RecordableBackend): RecorderProxy {
|
|
30
33
|
const recorder = wrap(backend.rhi);
|
|
31
34
|
const wrappedCreateShaderModule = wrapCreateShaderModule(backend.createShaderModule, recorder);
|
|
35
|
+
const originalCreateShaderModuleImmediate = backend.createShaderModuleImmediate;
|
|
36
|
+
const wrappedCreateShaderModuleImmediate =
|
|
37
|
+
originalCreateShaderModuleImmediate === undefined
|
|
38
|
+
? undefined
|
|
39
|
+
: (device: RhiDevice, desc: { code: string; label?: string | undefined }) => {
|
|
40
|
+
const realDevice =
|
|
41
|
+
(device as RhiDevice & { readonly _realDevice?: RhiDevice })._realDevice ?? device;
|
|
42
|
+
const result = originalCreateShaderModuleImmediate(realDevice, desc);
|
|
43
|
+
if (!result.ok) return result;
|
|
44
|
+
const hId = recorder.pushExternalCreateEvent(result.value, 'shaderModule', {
|
|
45
|
+
kind: 'createShaderModule',
|
|
46
|
+
handleId: '' as import('../types').HandleId,
|
|
47
|
+
wgslCode: desc.code,
|
|
48
|
+
});
|
|
49
|
+
recorder.registerShaderModule(result.value, hId);
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
32
52
|
const acquireCanvasContext = backend.rhi.acquireCanvasContext;
|
|
33
53
|
// Runtime's explicit-RHI seam receives the wrapped singleton as one value.
|
|
34
54
|
// Keep the standalone shader factory enumerable on that value so the
|
|
35
55
|
// renderer can preserve the complete typed backend pack after capture
|
|
36
56
|
// attachment; the public RhiInstance contract remains unchanged.
|
|
37
|
-
const rhi: RecordableBackend['rhi'] & {
|
|
57
|
+
const rhi: RecordableBackend['rhi'] & {
|
|
58
|
+
readonly createShaderModule: CreateShaderModuleFn;
|
|
59
|
+
readonly createShaderModuleImmediate?: CreateShaderModuleImmediateFn;
|
|
60
|
+
} = {
|
|
38
61
|
requestAdapter: recorder.requestAdapter.bind(recorder),
|
|
39
62
|
createShaderModule: wrappedCreateShaderModule,
|
|
63
|
+
...(wrappedCreateShaderModuleImmediate === undefined
|
|
64
|
+
? {}
|
|
65
|
+
: { createShaderModuleImmediate: wrappedCreateShaderModuleImmediate }),
|
|
40
66
|
...(acquireCanvasContext === undefined
|
|
41
67
|
? {}
|
|
42
|
-
: {
|
|
68
|
+
: {
|
|
69
|
+
acquireCanvasContext(canvas) {
|
|
70
|
+
const result = acquireCanvasContext.call(backend.rhi, canvas);
|
|
71
|
+
if (!result.ok) return result;
|
|
72
|
+
const context = result.value;
|
|
73
|
+
// rhi-webgpu's canvas shim translates the forgeax RhiDevice to the
|
|
74
|
+
// native GPUDevice through its own WeakMap. The recorder device is
|
|
75
|
+
// an intentional proxy, so passing it directly makes configure()
|
|
76
|
+
// reject with a foreign-device error before the first frame. Keep
|
|
77
|
+
// the context owned by the selected backend, but unwrap only the
|
|
78
|
+
// device argument at this boundary; all resource calls continue to
|
|
79
|
+
// flow through the recorder device and remain capturable.
|
|
80
|
+
return ok({
|
|
81
|
+
...context,
|
|
82
|
+
configure(configuration) {
|
|
83
|
+
const device = (
|
|
84
|
+
configuration.device as RhiDevice & {
|
|
85
|
+
readonly _realDevice?: RhiDevice;
|
|
86
|
+
}
|
|
87
|
+
)._realDevice;
|
|
88
|
+
return context.configure({
|
|
89
|
+
...configuration,
|
|
90
|
+
...(device === undefined ? {} : { device }),
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
}),
|
|
43
96
|
};
|
|
44
97
|
const wrappedBackend: RecorderBackend = {
|
|
45
98
|
rhi,
|
|
46
99
|
createShaderModule: wrappedCreateShaderModule,
|
|
100
|
+
...(wrappedCreateShaderModuleImmediate === undefined
|
|
101
|
+
? {}
|
|
102
|
+
: { createShaderModuleImmediate: wrappedCreateShaderModuleImmediate }),
|
|
47
103
|
unwrapDeviceForSurface(device) {
|
|
48
104
|
const raw = (device as RhiDevice & { readonly _realDevice?: RhiDevice })._realDevice;
|
|
49
105
|
if (raw !== undefined) return ok(raw);
|
package/src/recorder/session.ts
CHANGED
|
@@ -147,6 +147,6 @@ export function attachRecorder(
|
|
|
147
147
|
return ok(attachment);
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
export type { CreateShaderModuleFn } from '../recorder';
|
|
150
|
+
export type { CreateShaderModuleFn, CreateShaderModuleImmediateFn } from '../recorder';
|
|
151
151
|
export type { EncodedTape } from './assemble';
|
|
152
152
|
export type { RecordableBackend, RecorderBackend } from './proxy';
|
package/src/recorder/shader.ts
CHANGED
|
@@ -2,7 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
import type { Result, RhiDevice, ShaderModule } from '@forgeax/engine-rhi';
|
|
4
4
|
import type { HandleId } from '../types';
|
|
5
|
-
import type { CreateShaderModuleFn, DebugRhiInstance } from './core';
|
|
5
|
+
import type { CreateShaderModuleFn, CreateShaderModuleImmediateFn, DebugRhiInstance } from './core';
|
|
6
|
+
|
|
7
|
+
function recordShaderModule(
|
|
8
|
+
debugInst: DebugRhiInstance,
|
|
9
|
+
result: Result<ShaderModule, import('@forgeax/engine-rhi').RhiError>,
|
|
10
|
+
desc: { code: string; label?: string | undefined },
|
|
11
|
+
): Result<ShaderModule, import('@forgeax/engine-rhi').RhiError> {
|
|
12
|
+
if (!result.ok) return result;
|
|
13
|
+
const hId = debugInst.pushExternalCreateEvent(result.value, 'shaderModule', {
|
|
14
|
+
kind: 'createShaderModule',
|
|
15
|
+
handleId: '' as HandleId,
|
|
16
|
+
wgslCode: desc.code,
|
|
17
|
+
});
|
|
18
|
+
// Register the shader module handle in the recorder's handleMap so
|
|
19
|
+
// downstream pipeline events can resolve the handleId during replay.
|
|
20
|
+
debugInst.registerShaderModule(result.value, hId);
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
6
23
|
|
|
7
24
|
export function wrapCreateShaderModule(
|
|
8
25
|
originalFn: CreateShaderModuleFn,
|
|
@@ -21,27 +38,17 @@ export function wrapCreateShaderModule(
|
|
|
21
38
|
// proxyDevice exposes for exactly this purpose.
|
|
22
39
|
const realDevice = (device as RhiDevice & { _realDevice?: RhiDevice })._realDevice ?? device;
|
|
23
40
|
const result = await originalFn(realDevice, desc);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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);
|
|
41
|
+
return recordShaderModule(debugInst, result, desc);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
/** Wrap the synchronous render-path factory while preserving tape ownership. */
|
|
46
|
+
export function wrapCreateShaderModuleImmediate(
|
|
47
|
+
originalFn: CreateShaderModuleImmediateFn,
|
|
48
|
+
debugInst: DebugRhiInstance,
|
|
49
|
+
): CreateShaderModuleImmediateFn {
|
|
50
|
+
return (device, desc) => {
|
|
51
|
+
const realDevice = (device as RhiDevice & { _realDevice?: RhiDevice })._realDevice ?? device;
|
|
52
|
+
return recordShaderModule(debugInst, originalFn(realDevice, desc), desc);
|
|
46
53
|
};
|
|
47
54
|
}
|
package/src/recorder.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// @forgeax/engine-rhi-debug/src/recorder -- split recorder public seam.
|
|
2
2
|
|
|
3
|
-
export type {
|
|
3
|
+
export type {
|
|
4
|
+
CreateShaderModuleFn,
|
|
5
|
+
CreateShaderModuleImmediateFn,
|
|
6
|
+
DebugRhiInstance,
|
|
7
|
+
} from './recorder/core';
|
|
4
8
|
export { PER_EVENT_OVERHEAD, TAPE_FORMAT_VERSION } from './recorder/core';
|
|
5
9
|
export type {
|
|
6
10
|
CaptureFrameOptions,
|
|
@@ -11,5 +15,5 @@ export type {
|
|
|
11
15
|
RecorderOptions,
|
|
12
16
|
} from './recorder/session';
|
|
13
17
|
export { attachRecorder } from './recorder/session';
|
|
14
|
-
export { wrapCreateShaderModule } from './recorder/shader';
|
|
18
|
+
export { wrapCreateShaderModule, wrapCreateShaderModuleImmediate } from './recorder/shader';
|
|
15
19
|
export { wrap } from './recorder/wrap';
|