@forgeax/engine-rhi-debug 0.1.27 → 0.1.29
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 +13 -0
- 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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +465 -53
- 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 +39 -4
- 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 +7 -0
- 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 +49 -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/index.ts +1 -0
- package/src/protocol/event-semantics.ts +20 -2
- package/src/protocol/types.ts +1 -0
- package/src/protocol/validation.ts +31 -0
- package/src/recorder/assemble.ts +8 -1
- package/src/recorder/closure.ts +17 -0
- package/src/recorder/core.ts +11 -0
- package/src/recorder/device.ts +33 -6
- package/src/recorder/encoder.ts +22 -4
- 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 +111 -0
- package/src/replay/readback.ts +8 -3
- package/src/replay/resources.ts +6 -0
- package/src/replay/session.ts +234 -25
- package/src/types.ts +53 -12
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { RhiInstance } from '@forgeax/engine-rhi';
|
|
1
|
+
import type { RhiInstance, ShaderModule } from '@forgeax/engine-rhi';
|
|
2
|
+
import { ok } from '@forgeax/engine-types';
|
|
2
3
|
import { describe, expect, it, vi } from 'vitest';
|
|
3
4
|
import { attachRecorder, type RecordableBackend } from '../recorder/session';
|
|
4
5
|
|
|
@@ -48,6 +49,22 @@ describe('RecorderSession contract', () => {
|
|
|
48
49
|
).toBe(attached.value.backend.createShaderModule);
|
|
49
50
|
});
|
|
50
51
|
|
|
52
|
+
it('preserves the optional immediate shader factory on both backend seams', () => {
|
|
53
|
+
const createShaderModuleImmediate = vi.fn(() => ok({} as ShaderModule));
|
|
54
|
+
const attached = attachRecorder({ ...backend(), createShaderModuleImmediate });
|
|
55
|
+
expect(attached.ok).toBe(true);
|
|
56
|
+
if (!attached.ok) return;
|
|
57
|
+
|
|
58
|
+
expect(attached.value.backend.createShaderModuleImmediate).toBeDefined();
|
|
59
|
+
expect(
|
|
60
|
+
(
|
|
61
|
+
attached.value.backend.rhi as unknown as {
|
|
62
|
+
createShaderModuleImmediate?: unknown;
|
|
63
|
+
}
|
|
64
|
+
).createShaderModuleImmediate,
|
|
65
|
+
).toBe(attached.value.backend.createShaderModuleImmediate);
|
|
66
|
+
});
|
|
67
|
+
|
|
51
68
|
it('resolves an abort as one terminal structured result', async () => {
|
|
52
69
|
const attached = attachRecorder(backend());
|
|
53
70
|
expect(attached.ok).toBe(true);
|
|
@@ -154,6 +154,42 @@ function triangleTape(): Tape {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
describe.skipIf(SKIP_DAWN)('ReplaySession Dawn contract', () => {
|
|
157
|
+
it.each([
|
|
158
|
+
0x84, 0x184, 0x06,
|
|
159
|
+
])('restores captured buffer bytes without COPY_DST (usage %i)', async (usage) => {
|
|
160
|
+
const pack = await loadDawn();
|
|
161
|
+
const expected = new Uint8Array([17, 31, 63, 127, 5, 9, 13, 21]);
|
|
162
|
+
const tape: Tape = {
|
|
163
|
+
header: { formatVersion: 7, rhiCaps: {}, eventCount: 0, blobCount: 1 },
|
|
164
|
+
bootstrap: [
|
|
165
|
+
bootstrap(
|
|
166
|
+
'buffer:seed',
|
|
167
|
+
'buffer',
|
|
168
|
+
{
|
|
169
|
+
kind: 'createBuffer',
|
|
170
|
+
handleId: 'buffer:seed',
|
|
171
|
+
desc: { size: expected.length, usage },
|
|
172
|
+
},
|
|
173
|
+
[{ hash: 'seed', byteOffset: 0, byteLength: expected.length }],
|
|
174
|
+
),
|
|
175
|
+
],
|
|
176
|
+
events: [],
|
|
177
|
+
blobs: [{ hash: 'seed', bytes: expected, compression: 'none' }],
|
|
178
|
+
};
|
|
179
|
+
const replay = (
|
|
180
|
+
await openReplay(tape, {
|
|
181
|
+
device: await freshDevice(pack),
|
|
182
|
+
createShaderModule: pack.createShaderModule,
|
|
183
|
+
})
|
|
184
|
+
).unwrap();
|
|
185
|
+
try {
|
|
186
|
+
expect((await replay.readResource('buffer:seed')).unwrap().bytes).toEqual(expected);
|
|
187
|
+
expect(tape.bootstrap[0]?.create.desc).toEqual({ size: expected.length, usage });
|
|
188
|
+
} finally {
|
|
189
|
+
(await replay.dispose()).unwrap();
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
157
193
|
it('replays a triangle and returns attachment bytes through inspectWork', async () => {
|
|
158
194
|
const pack = await loadDawn();
|
|
159
195
|
const tape = triangleTape();
|
|
@@ -175,6 +211,19 @@ describe.skipIf(SKIP_DAWN)('ReplaySession Dawn contract', () => {
|
|
|
175
211
|
expect(baseline.value.attachment?.provenance.selectedWorkIndex).toBe(0);
|
|
176
212
|
expect(baseline.value.attachment?.provenance.subresource).toBeNull();
|
|
177
213
|
|
|
214
|
+
const selected = await first.value.readResourceAtWork('texture-view:rt', 0);
|
|
215
|
+
expect(
|
|
216
|
+
selected.ok,
|
|
217
|
+
selected.ok ? undefined : `${selected.error.code}: ${JSON.stringify(selected.error.detail)}`,
|
|
218
|
+
).toBe(true);
|
|
219
|
+
if (!selected.ok) throw new Error(selected.error.hint);
|
|
220
|
+
expect(selected.value.provenance.selectedWorkIndex).toBe(0);
|
|
221
|
+
expect(selected.value.bytes).toEqual(baseline.value.attachment?.bytes);
|
|
222
|
+
const bootstrap = await first.value.readResource('texture-view:rt');
|
|
223
|
+
if (!bootstrap.ok) throw new Error(bootstrap.error.hint);
|
|
224
|
+
expect(bootstrap.value.provenance.selectedWorkIndex).toBeUndefined();
|
|
225
|
+
expect(selected.value.bytes).not.toEqual(bootstrap.value.bytes);
|
|
226
|
+
|
|
178
227
|
const second = await openReplay(tape, {
|
|
179
228
|
device: await freshDevice(pack),
|
|
180
229
|
createShaderModule: pack.createShaderModule,
|
|
@@ -14,6 +14,7 @@ const backend: ReplayBackend = {
|
|
|
14
14
|
|
|
15
15
|
expectTypeOf(backend.createShaderModule).toBeFunction();
|
|
16
16
|
expectTypeOf(session.inspectWork(0)).resolves.toHaveProperty('ok');
|
|
17
|
+
expectTypeOf(session.readResourceAtWork('texture:out', 0)).resolves.toHaveProperty('ok');
|
|
17
18
|
expectTypeOf(session.dispose()).resolves.toHaveProperty('ok');
|
|
18
19
|
expectTypeOf<WorkInspection['pipeline']>().not.toBeNever();
|
|
19
20
|
expectTypeOf<WorkInspection['bindings']>().toMatchTypeOf<readonly unknown[] | undefined>();
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from '../recorder';
|
|
13
13
|
import { assembleTape } from '../recorder/assemble';
|
|
14
14
|
import { openReplay, type ReplayBackend } from '../replay/session';
|
|
15
|
+
import { runQuerySetReplayFixture } from './query-set-replay-fixture';
|
|
15
16
|
|
|
16
17
|
interface DawnPack {
|
|
17
18
|
readonly rhi: RhiInstance;
|
|
@@ -175,4 +176,64 @@ describe.skipIf(SKIP_DAWN)('RHI debug v7 fresh-device evidence', () => {
|
|
|
175
176
|
expect(normalizedPixelDelta(baseline, replayPixels)).toBeLessThanOrEqual(0.01);
|
|
176
177
|
expect((await session.value.dispose()).ok).toBe(true);
|
|
177
178
|
}, 60_000);
|
|
179
|
+
|
|
180
|
+
it('captures and replays an occlusion QuerySet with an eight-byte readback', async () => {
|
|
181
|
+
const pack = await loadDawn();
|
|
182
|
+
const recorder = wrap(pack.rhi);
|
|
183
|
+
const recordingShaderModule = wrapCreateShaderModule(pack.createShaderModule, recorder);
|
|
184
|
+
const adapter = await recorder.requestAdapter();
|
|
185
|
+
expect(adapter.ok).toBe(true);
|
|
186
|
+
if (!adapter.ok) throw new Error(`QuerySet Dawn adapter failed: ${adapter.error.code}`);
|
|
187
|
+
const deviceResult = await adapter.value.requestDevice();
|
|
188
|
+
expect(deviceResult.ok).toBe(true);
|
|
189
|
+
if (!deviceResult.ok)
|
|
190
|
+
throw new Error(`QuerySet Dawn device failed: ${deviceResult.error.code}`);
|
|
191
|
+
const armed = recorder.arm(1);
|
|
192
|
+
expect(armed.ok).toBe(true);
|
|
193
|
+
if (!armed.ok) throw new Error(armed.error.hint);
|
|
194
|
+
const evidence = await runQuerySetReplayFixture({
|
|
195
|
+
runner: 'dawn',
|
|
196
|
+
device: deviceResult.value,
|
|
197
|
+
createShaderModule: recordingShaderModule,
|
|
198
|
+
replayCreateShaderModule: pack.createShaderModule,
|
|
199
|
+
finishCapture: async () => {
|
|
200
|
+
recorder.onFrameEnd();
|
|
201
|
+
const assembled = assembleTape(recorder);
|
|
202
|
+
if (!assembled.ok) throw new Error(assembled.error.hint);
|
|
203
|
+
return assembled.value;
|
|
204
|
+
},
|
|
205
|
+
createFreshDevice: async () => {
|
|
206
|
+
const freshAdapter = await pack.rhi.requestAdapter();
|
|
207
|
+
if (!freshAdapter.ok) throw new Error(freshAdapter.error.hint);
|
|
208
|
+
const freshDevice = await freshAdapter.value.requestDevice();
|
|
209
|
+
if (!freshDevice.ok) throw new Error(freshDevice.error.hint);
|
|
210
|
+
return freshDevice.value;
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
expect(evidence, JSON.stringify(evidence.errorReceipts)).toMatchObject({
|
|
214
|
+
status: 'available',
|
|
215
|
+
tapeFormatVersion: 7,
|
|
216
|
+
resolveDestinationOffset: 256,
|
|
217
|
+
originalQueryValues: expect.arrayContaining([expect.any(String), '0']),
|
|
218
|
+
originalResultHalfWords: [15360, 0, 0, 15360, 0, 0, 0, 15360],
|
|
219
|
+
freshQueryValues: ['1', '0'],
|
|
220
|
+
errorReceipts: [],
|
|
221
|
+
deviceLost: null,
|
|
222
|
+
});
|
|
223
|
+
expect(evidence.originalQueryValues[0]).not.toBe('0');
|
|
224
|
+
expect(evidence.originalColorBytes.some((byte) => byte > 0)).toBe(true);
|
|
225
|
+
expect(evidence.eventKinds).toEqual(
|
|
226
|
+
expect.arrayContaining([
|
|
227
|
+
'createQuerySet',
|
|
228
|
+
'beginRenderPass',
|
|
229
|
+
'beginOcclusionQuery',
|
|
230
|
+
'endOcclusionQuery',
|
|
231
|
+
'resolveQuerySet',
|
|
232
|
+
'submit',
|
|
233
|
+
'destroyQuerySet',
|
|
234
|
+
]),
|
|
235
|
+
);
|
|
236
|
+
expect(evidence.querySetHandleId).toBeDefined();
|
|
237
|
+
expect(evidence.resolveDestinationHandleId).toBeDefined();
|
|
238
|
+
}, 60_000);
|
|
178
239
|
});
|
package/src/index.ts
CHANGED
|
@@ -14,11 +14,14 @@ export const eventKinds = [
|
|
|
14
14
|
'frameMark',
|
|
15
15
|
'createBuffer',
|
|
16
16
|
'createTexture',
|
|
17
|
+
'createQuerySet',
|
|
17
18
|
'destroyBuffer',
|
|
18
19
|
'destroyTexture',
|
|
20
|
+
'destroyQuerySet',
|
|
19
21
|
'createTextureView',
|
|
20
22
|
'createSampler',
|
|
21
23
|
'createBindGroupLayout',
|
|
24
|
+
'getBindGroupLayout',
|
|
22
25
|
'createBindGroup',
|
|
23
26
|
'createPipelineLayout',
|
|
24
27
|
'createRenderPipeline',
|
|
@@ -30,12 +33,15 @@ export const eventKinds = [
|
|
|
30
33
|
'copyExternalImageToTexture',
|
|
31
34
|
'submit',
|
|
32
35
|
'beginRenderPass',
|
|
36
|
+
'beginOcclusionQuery',
|
|
37
|
+
'endOcclusionQuery',
|
|
33
38
|
'beginComputePass',
|
|
34
39
|
'copyBufferToBuffer',
|
|
35
40
|
'copyBufferToTexture',
|
|
36
41
|
'copyTextureToBuffer',
|
|
37
42
|
'copyTextureToTexture',
|
|
38
43
|
'clearBuffer',
|
|
44
|
+
'resolveQuerySet',
|
|
39
45
|
'pushDebugGroup',
|
|
40
46
|
'popDebugGroup',
|
|
41
47
|
'insertDebugMarker',
|
|
@@ -86,6 +92,8 @@ export function resourceKindForEvent(kind: EventKind): ResourceKind | undefined
|
|
|
86
92
|
return 'buffer';
|
|
87
93
|
case 'createTexture':
|
|
88
94
|
return 'texture';
|
|
95
|
+
case 'createQuerySet':
|
|
96
|
+
return 'query-set';
|
|
89
97
|
case 'createTextureView':
|
|
90
98
|
return 'texture-view';
|
|
91
99
|
case 'createSampler':
|
|
@@ -97,6 +105,7 @@ export function resourceKindForEvent(kind: EventKind): ResourceKind | undefined
|
|
|
97
105
|
return 'pipeline';
|
|
98
106
|
case 'createBindGroup':
|
|
99
107
|
case 'createBindGroupLayout':
|
|
108
|
+
case 'getBindGroupLayout':
|
|
100
109
|
case 'createPipelineLayout':
|
|
101
110
|
return 'binding';
|
|
102
111
|
case 'createCommandEncoder':
|
|
@@ -114,7 +123,9 @@ function semanticsFor(kind: EventKind): EventSemantics {
|
|
|
114
123
|
read: (event) => referencedHandles(event),
|
|
115
124
|
written: (event) => writtenHandles(event),
|
|
116
125
|
destroyed: (event) =>
|
|
117
|
-
event.kind === 'destroyBuffer' ||
|
|
126
|
+
event.kind === 'destroyBuffer' ||
|
|
127
|
+
event.kind === 'destroyTexture' ||
|
|
128
|
+
event.kind === 'destroyQuerySet'
|
|
118
129
|
? stringField(event, 'handleId')
|
|
119
130
|
: [],
|
|
120
131
|
};
|
|
@@ -122,7 +133,12 @@ function semanticsFor(kind: EventKind): EventSemantics {
|
|
|
122
133
|
|
|
123
134
|
function categoryFor(kind: EventKind): EventCategory {
|
|
124
135
|
if (isWorkEvent(kind)) return 'work';
|
|
125
|
-
if (
|
|
136
|
+
if (
|
|
137
|
+
kind.startsWith('create') ||
|
|
138
|
+
kind.startsWith('destroy') ||
|
|
139
|
+
kind === 'initialData' ||
|
|
140
|
+
kind === 'getBindGroupLayout'
|
|
141
|
+
)
|
|
126
142
|
return 'resource';
|
|
127
143
|
if (kind.includes('Pass')) return 'pass';
|
|
128
144
|
if (kind.startsWith('copy') || kind === 'clearBuffer' || kind.startsWith('write')) return 'copy';
|
|
@@ -153,6 +169,8 @@ function referencedHandles(event: RhiCallEvent): readonly string[] {
|
|
|
153
169
|
'resourceHandleIds',
|
|
154
170
|
'bindGroupHandleId',
|
|
155
171
|
'pipelineHandleId',
|
|
172
|
+
'querySetHandleId',
|
|
173
|
+
'occlusionQuerySetHandleId',
|
|
156
174
|
'indexBufferHandleId',
|
|
157
175
|
'vertexBufferHandleId',
|
|
158
176
|
];
|
package/src/protocol/types.ts
CHANGED
|
@@ -93,6 +93,7 @@ function isResourceKind(value: string): value is ResourceKind {
|
|
|
93
93
|
return [
|
|
94
94
|
'buffer',
|
|
95
95
|
'texture',
|
|
96
|
+
'query-set',
|
|
96
97
|
'texture-view',
|
|
97
98
|
'sampler',
|
|
98
99
|
'shader-module',
|
|
@@ -112,8 +113,10 @@ function eventResources(event: RhiCallEvent): EventResources {
|
|
|
112
113
|
switch (event.kind) {
|
|
113
114
|
case 'createBuffer':
|
|
114
115
|
case 'createTexture':
|
|
116
|
+
case 'createQuerySet':
|
|
115
117
|
case 'createSampler':
|
|
116
118
|
case 'createBindGroupLayout':
|
|
119
|
+
case 'getBindGroupLayout':
|
|
117
120
|
case 'createBindGroup':
|
|
118
121
|
case 'createPipelineLayout':
|
|
119
122
|
case 'createRenderPipeline':
|
|
@@ -123,6 +126,7 @@ function eventResources(event: RhiCallEvent): EventResources {
|
|
|
123
126
|
created: [event.handleId],
|
|
124
127
|
reads: handleRefs(event, [
|
|
125
128
|
'layoutHandleId',
|
|
129
|
+
'pipelineHandleId',
|
|
126
130
|
'vertexShaderModuleHandleId',
|
|
127
131
|
'fragmentShaderModuleHandleId',
|
|
128
132
|
'computeShaderModuleHandleId',
|
|
@@ -135,6 +139,7 @@ function eventResources(event: RhiCallEvent): EventResources {
|
|
|
135
139
|
return { created: [event.cmdHandleId], reads: [], destroyed: [] };
|
|
136
140
|
case 'destroyBuffer':
|
|
137
141
|
case 'destroyTexture':
|
|
142
|
+
case 'destroyQuerySet':
|
|
138
143
|
return { created: [], reads: [event.handleId], destroyed: [event.handleId] };
|
|
139
144
|
case 'writeBuffer':
|
|
140
145
|
case 'writeTexture':
|
|
@@ -156,6 +161,32 @@ function eventResources(event: RhiCallEvent): EventResources {
|
|
|
156
161
|
]),
|
|
157
162
|
destroyed: [],
|
|
158
163
|
};
|
|
164
|
+
case 'resolveQuerySet':
|
|
165
|
+
return {
|
|
166
|
+
created: [],
|
|
167
|
+
reads: handleRefs(event, ['cmdHandleId', 'querySetHandleId', 'destinationHandleId']),
|
|
168
|
+
destroyed: [],
|
|
169
|
+
};
|
|
170
|
+
case 'beginRenderPass':
|
|
171
|
+
return {
|
|
172
|
+
created: [event.passHandleId],
|
|
173
|
+
reads: handleRefs(event, [
|
|
174
|
+
'cmdHandleId',
|
|
175
|
+
'occlusionQuerySetHandleId',
|
|
176
|
+
'colorAttachmentViewHandleIds',
|
|
177
|
+
'colorAttachmentResolveTargetHandleIds',
|
|
178
|
+
'depthStencilViewHandleId',
|
|
179
|
+
]),
|
|
180
|
+
destroyed: [],
|
|
181
|
+
};
|
|
182
|
+
case 'beginComputePass':
|
|
183
|
+
return { created: [event.passHandleId], reads: [event.cmdHandleId], destroyed: [] };
|
|
184
|
+
case 'endRenderPass':
|
|
185
|
+
case 'endComputePass':
|
|
186
|
+
return { created: [], reads: [event.passHandleId], destroyed: [event.passHandleId] };
|
|
187
|
+
case 'beginOcclusionQuery':
|
|
188
|
+
case 'endOcclusionQuery':
|
|
189
|
+
return { created: [], reads: handleRefs(event, ['passHandleId']), destroyed: [] };
|
|
159
190
|
default:
|
|
160
191
|
return { created: [], reads: [], destroyed: [] };
|
|
161
192
|
}
|
package/src/recorder/assemble.ts
CHANGED
|
@@ -161,7 +161,11 @@ function createdHandleId(event: RhiCallEvent): HandleId | undefined {
|
|
|
161
161
|
if (event.kind === 'beginRenderPass' || event.kind === 'beginComputePass') {
|
|
162
162
|
return event.passHandleId;
|
|
163
163
|
}
|
|
164
|
-
if (
|
|
164
|
+
if (
|
|
165
|
+
(event.kind.startsWith('create') || event.kind === 'getBindGroupLayout') &&
|
|
166
|
+
'handleId' in event
|
|
167
|
+
)
|
|
168
|
+
return event.handleId;
|
|
165
169
|
return undefined;
|
|
166
170
|
}
|
|
167
171
|
|
|
@@ -171,6 +175,8 @@ function resourceKind(event: RhiCallEvent): BootstrapResource['kind'] | undefine
|
|
|
171
175
|
return 'buffer';
|
|
172
176
|
case 'createTexture':
|
|
173
177
|
return 'texture';
|
|
178
|
+
case 'createQuerySet':
|
|
179
|
+
return 'query-set';
|
|
174
180
|
case 'createTextureView':
|
|
175
181
|
return 'texture-view';
|
|
176
182
|
case 'createSampler':
|
|
@@ -182,6 +188,7 @@ function resourceKind(event: RhiCallEvent): BootstrapResource['kind'] | undefine
|
|
|
182
188
|
return 'pipeline';
|
|
183
189
|
case 'createBindGroup':
|
|
184
190
|
case 'createBindGroupLayout':
|
|
191
|
+
case 'getBindGroupLayout':
|
|
185
192
|
case 'createPipelineLayout':
|
|
186
193
|
return 'binding';
|
|
187
194
|
case 'createCommandEncoder':
|
package/src/recorder/closure.ts
CHANGED
|
@@ -135,12 +135,14 @@ export function _collectFrameReferencedHandleIds(events: readonly RhiCallEvent[]
|
|
|
135
135
|
cmdHandleId: HandleId;
|
|
136
136
|
colorAttachmentViewHandleIds: readonly (HandleId | undefined)[];
|
|
137
137
|
depthStencilViewHandleId?: HandleId;
|
|
138
|
+
occlusionQuerySetHandleId?: HandleId;
|
|
138
139
|
};
|
|
139
140
|
refs.add(we.cmdHandleId);
|
|
140
141
|
for (const vhId of we.colorAttachmentViewHandleIds) {
|
|
141
142
|
if (vhId !== undefined) refs.add(vhId);
|
|
142
143
|
}
|
|
143
144
|
if (we.depthStencilViewHandleId !== undefined) refs.add(we.depthStencilViewHandleId);
|
|
145
|
+
if (we.occlusionQuerySetHandleId !== undefined) refs.add(we.occlusionQuerySetHandleId);
|
|
144
146
|
break;
|
|
145
147
|
}
|
|
146
148
|
case 'beginComputePass': {
|
|
@@ -204,6 +206,7 @@ export function _collectFrameReferencedHandleIds(events: readonly RhiCallEvent[]
|
|
|
204
206
|
break;
|
|
205
207
|
}
|
|
206
208
|
case 'createBindGroup':
|
|
209
|
+
case 'getBindGroupLayout':
|
|
207
210
|
case 'createPipelineLayout':
|
|
208
211
|
case 'createRenderPipeline':
|
|
209
212
|
case 'createComputePipeline':
|
|
@@ -223,17 +226,28 @@ export function _collectFrameReferencedHandleIds(events: readonly RhiCallEvent[]
|
|
|
223
226
|
}
|
|
224
227
|
case 'destroyBuffer':
|
|
225
228
|
case 'destroyTexture':
|
|
229
|
+
case 'destroyQuerySet':
|
|
226
230
|
refs.add(e.handleId);
|
|
227
231
|
break;
|
|
228
232
|
case 'frameMark':
|
|
229
233
|
case 'createBuffer':
|
|
230
234
|
case 'createTexture':
|
|
235
|
+
case 'createQuerySet':
|
|
231
236
|
case 'createSampler':
|
|
232
237
|
case 'createBindGroupLayout':
|
|
233
238
|
case 'createShaderModule':
|
|
234
239
|
case 'createCommandEncoder':
|
|
235
240
|
// Leaf declaration events — no backward references to collect.
|
|
236
241
|
break;
|
|
242
|
+
case 'resolveQuerySet':
|
|
243
|
+
refs.add(e.cmdHandleId);
|
|
244
|
+
refs.add(e.querySetHandleId);
|
|
245
|
+
refs.add(e.destinationHandleId);
|
|
246
|
+
break;
|
|
247
|
+
case 'beginOcclusionQuery':
|
|
248
|
+
case 'endOcclusionQuery':
|
|
249
|
+
refs.add(e.passHandleId);
|
|
250
|
+
break;
|
|
237
251
|
default: {
|
|
238
252
|
// Exhaustiveness guard: if a new RhiCallEvent member is added to the
|
|
239
253
|
// union without a corresponding handle-collection case, tsc fails here.
|
|
@@ -292,8 +306,11 @@ export function _getCreateEventReferencedHandleIds(event: RhiCallEvent): HandleI
|
|
|
292
306
|
const e = event as { sourceHandleId: HandleId };
|
|
293
307
|
return [e.sourceHandleId];
|
|
294
308
|
}
|
|
309
|
+
case 'getBindGroupLayout':
|
|
310
|
+
return [(event as { pipelineHandleId: HandleId }).pipelineHandleId];
|
|
295
311
|
case 'createBuffer':
|
|
296
312
|
case 'createTexture':
|
|
313
|
+
case 'createQuerySet':
|
|
297
314
|
case 'createSampler':
|
|
298
315
|
case 'createBindGroupLayout':
|
|
299
316
|
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
|
@@ -61,15 +61,21 @@ export function createCommandEncoderProxy(
|
|
|
61
61
|
...(desc.depthStencilAttachment === undefined
|
|
62
62
|
? {}
|
|
63
63
|
: { depthStencilAttachment: { ...desc.depthStencilAttachment } }),
|
|
64
|
-
...(desc.occlusionQuerySet === undefined
|
|
65
|
-
? {}
|
|
66
|
-
: { occlusionQuerySet: desc.occlusionQuerySet }),
|
|
67
64
|
...(desc.timestampWrites === undefined ? {} : { timestampWrites: desc.timestampWrites }),
|
|
68
65
|
...(desc.maxDrawCount === undefined ? {} : { maxDrawCount: desc.maxDrawCount }),
|
|
69
66
|
},
|
|
70
67
|
colorAttachmentViewHandleIds,
|
|
71
68
|
colorAttachmentResolveTargetHandleIds,
|
|
72
69
|
depthStencilViewHandleId,
|
|
70
|
+
...(desc.occlusionQuerySet === undefined
|
|
71
|
+
? {}
|
|
72
|
+
: {
|
|
73
|
+
occlusionQuerySetHandleId: getHandleId(
|
|
74
|
+
s,
|
|
75
|
+
desc.occlusionQuerySet as object,
|
|
76
|
+
'querySet',
|
|
77
|
+
),
|
|
78
|
+
}),
|
|
73
79
|
});
|
|
74
80
|
const realPass = realEnc.beginRenderPass(desc);
|
|
75
81
|
return createRenderPassProxy(s, realPass, passHId);
|
|
@@ -220,13 +226,25 @@ export function createCommandEncoderProxy(
|
|
|
220
226
|
realEnc.clearBuffer(buffer, offset, size);
|
|
221
227
|
},
|
|
222
228
|
resolveQuerySet(querySet, firstQuery, queryCount, destination, destinationOffset) {
|
|
223
|
-
|
|
229
|
+
const result = realEnc.resolveQuerySet(
|
|
224
230
|
querySet,
|
|
225
231
|
firstQuery,
|
|
226
232
|
queryCount,
|
|
227
233
|
destination,
|
|
228
234
|
destinationOffset,
|
|
229
235
|
);
|
|
236
|
+
if (result.ok) {
|
|
237
|
+
pushEvent(s, {
|
|
238
|
+
kind: 'resolveQuerySet',
|
|
239
|
+
cmdHandleId: cmdHId,
|
|
240
|
+
querySetHandleId: getHandleId(s, querySet as object, 'querySet'),
|
|
241
|
+
firstQuery,
|
|
242
|
+
queryCount,
|
|
243
|
+
destinationHandleId: getHandleId(s, destination as object, 'buffer'),
|
|
244
|
+
destinationOffset,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return result;
|
|
230
248
|
},
|
|
231
249
|
pushDebugGroup(groupLabel) {
|
|
232
250
|
pushEvent(s, { kind: 'pushDebugGroup', cmdHandleId: cmdHId, groupLabel });
|
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
|
+
}
|