@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.
Files changed (75) hide show
  1. package/README.md +32 -7
  2. package/dist/__tests__/query-set-lifecycle.unit.test.d.ts +2 -0
  3. package/dist/__tests__/query-set-lifecycle.unit.test.d.ts.map +1 -0
  4. package/dist/__tests__/query-set-replay-fixture.d.ts +39 -0
  5. package/dist/__tests__/query-set-replay-fixture.d.ts.map +1 -0
  6. package/dist/__tests__/recorder-canvas-context.unit.test.d.ts +2 -0
  7. package/dist/__tests__/recorder-canvas-context.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/timestamp-query-capture.dawn.test.d.ts +2 -0
  9. package/dist/__tests__/timestamp-query-capture.dawn.test.d.ts.map +1 -0
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.mjs +527 -58
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/protocol/event-semantics.d.ts +1 -1
  15. package/dist/protocol/event-semantics.d.ts.map +1 -1
  16. package/dist/protocol/types.d.ts +1 -1
  17. package/dist/protocol/types.d.ts.map +1 -1
  18. package/dist/recorder/closure.d.ts.map +1 -1
  19. package/dist/recorder/core.d.ts +10 -1
  20. package/dist/recorder/core.d.ts.map +1 -1
  21. package/dist/recorder/device.d.ts.map +1 -1
  22. package/dist/recorder/encoder.d.ts.map +1 -1
  23. package/dist/recorder/pass.d.ts.map +1 -1
  24. package/dist/recorder/pipeline.d.ts +5 -0
  25. package/dist/recorder/pipeline.d.ts.map +1 -0
  26. package/dist/recorder/proxy.d.ts +3 -1
  27. package/dist/recorder/proxy.d.ts.map +1 -1
  28. package/dist/recorder/session.d.ts +1 -1
  29. package/dist/recorder/session.d.ts.map +1 -1
  30. package/dist/recorder/shader.d.ts +3 -1
  31. package/dist/recorder/shader.d.ts.map +1 -1
  32. package/dist/recorder.d.ts +2 -2
  33. package/dist/recorder.d.ts.map +1 -1
  34. package/dist/replay/execute.d.ts.map +1 -1
  35. package/dist/replay/resources.d.ts +4 -1
  36. package/dist/replay/resources.d.ts.map +1 -1
  37. package/dist/replay/session.d.ts +7 -0
  38. package/dist/replay/session.d.ts.map +1 -1
  39. package/dist/types.d.ts +45 -6
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +6 -6
  42. package/src/__tests__/coverage-invariant.test-d.ts +4 -5
  43. package/src/__tests__/coverage-invariant.unit.test.ts +6 -17
  44. package/src/__tests__/e2e.browser.test.ts +50 -0
  45. package/src/__tests__/guard-gates.test.ts +1 -1
  46. package/src/__tests__/query-set-lifecycle.unit.test.ts +265 -0
  47. package/src/__tests__/query-set-replay-fixture.ts +553 -0
  48. package/src/__tests__/readback-format-matrix.dawn.test.ts +9 -2
  49. package/src/__tests__/recorder-canvas-context.unit.test.ts +67 -0
  50. package/src/__tests__/recorder-session.integration.test.ts +55 -0
  51. package/src/__tests__/recorder-session.unit.test.ts +18 -1
  52. package/src/__tests__/replay-session.dawn.test.ts +171 -0
  53. package/src/__tests__/replay-session.test-d.ts +1 -0
  54. package/src/__tests__/rhi-debug-fresh-replay.dawn.test.ts +61 -0
  55. package/src/__tests__/timestamp-query-capture.dawn.test.ts +61 -0
  56. package/src/index.ts +1 -0
  57. package/src/protocol/event-semantics.ts +21 -2
  58. package/src/protocol/types.ts +1 -0
  59. package/src/protocol/validation.ts +36 -0
  60. package/src/recorder/assemble.ts +8 -1
  61. package/src/recorder/closure.ts +21 -1
  62. package/src/recorder/core.ts +11 -0
  63. package/src/recorder/device.ts +33 -6
  64. package/src/recorder/encoder.ts +60 -6
  65. package/src/recorder/pass.ts +7 -2
  66. package/src/recorder/pipeline.ts +31 -0
  67. package/src/recorder/proxy.ts +58 -2
  68. package/src/recorder/session.ts +1 -1
  69. package/src/recorder/shader.ts +29 -22
  70. package/src/recorder.ts +6 -2
  71. package/src/replay/execute.ts +143 -1
  72. package/src/replay/readback.ts +15 -3
  73. package/src/replay/resources.ts +6 -0
  74. package/src/replay/session.ts +241 -27
  75. package/src/types.ts +61 -14
@@ -1,9 +1,64 @@
1
1
  import { createShaderModule, rhi } from '@forgeax/engine-rhi-null';
2
+ import { ok } from '@forgeax/engine-types';
2
3
  import { describe, expect, it } from 'vitest';
3
4
  import { attachRecorder, openReplay } from '../index';
4
5
  import { decodeTape } from '../protocol/codec';
5
6
 
6
7
  describe('RecorderSession real RHI consumer', () => {
8
+ it('preserves synchronous shader creation and replays pipeline-derived layouts', async () => {
9
+ const source = '@compute @workgroup_size(1) fn main() {}';
10
+ const rawDevice = (await (await rhi.requestAdapter()).unwrap().requestDevice()).unwrap();
11
+ const module = (await createShaderModule(rawDevice, { code: source })).unwrap();
12
+ let receivedDevice: unknown;
13
+ const attachment = attachRecorder({
14
+ rhi,
15
+ createShaderModule,
16
+ createShaderModuleImmediate: (device) => {
17
+ receivedDevice = device;
18
+ return ok(module);
19
+ },
20
+ }).unwrap();
21
+ const device = (
22
+ await (await attachment.backend.rhi.requestAdapter()).unwrap().requestDevice()
23
+ ).unwrap();
24
+ const factory = attachment.backend.createShaderModuleImmediate;
25
+ expect(factory).toBeTypeOf('function');
26
+ expect(
27
+ (attachment.backend.rhi as unknown as { createShaderModuleImmediate: unknown })
28
+ .createShaderModuleImmediate,
29
+ ).toBe(factory);
30
+ const shader = factory?.(device, { code: source });
31
+ expect(shader?.ok).toBe(true);
32
+ expect(receivedDevice).not.toBe(device);
33
+ if (!shader?.ok) throw new Error('synchronous shader factory unavailable');
34
+ const pipeline = device
35
+ .createComputePipeline({
36
+ layout: 'auto',
37
+ compute: { module: shader.value, entryPoint: 'main' },
38
+ })
39
+ .unwrap();
40
+ const layout = (
41
+ pipeline as typeof pipeline & import('@forgeax/engine-rhi').RhiComputePipelineOps
42
+ ).getBindGroupLayout(0);
43
+ const bindings = device.createBindGroup({ layout, entries: [] }).unwrap();
44
+ const capture = attachment.captureFrame();
45
+ (await attachment.frameBoundary()).unwrap();
46
+ const encoder = device.createCommandEncoder({}).unwrap();
47
+ const pass = encoder.beginComputePass({});
48
+ pass.setPipeline(pipeline);
49
+ pass.setBindGroup(0, bindings);
50
+ pass.dispatchWorkgroups(1);
51
+ pass.end();
52
+ device.queue.submit([encoder.finish().unwrap()]).unwrap();
53
+ (await attachment.frameBoundary()).unwrap();
54
+ const tape = decodeTape((await capture).unwrap().bytes).unwrap();
55
+ expect(tape.bootstrap.some((item) => item.create.kind === 'getBindGroupLayout')).toBe(true);
56
+ const replayDevice = (await (await rhi.requestAdapter()).unwrap().requestDevice()).unwrap();
57
+ const replay = await openReplay(tape, { device: replayDevice, createShaderModule });
58
+ expect(replay.ok).toBe(true);
59
+ await attachment.dispose();
60
+ });
61
+
7
62
  it('captures one steady frame into a strict v7 artifact', async () => {
8
63
  const attached = attachRecorder({ rhi, createShaderModule });
9
64
  expect(attached.ok).toBe(true);
@@ -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);
@@ -153,7 +153,122 @@ function triangleTape(): Tape {
153
153
  };
154
154
  }
155
155
 
156
+ function msaaTriangleTape(): Tape {
157
+ const tape = triangleTape();
158
+ const bootstrapResources = tape.bootstrap.map((resource) => {
159
+ if (resource.handleId === 'texture:rt') {
160
+ const desc = resource.create.desc as Record<string, unknown>;
161
+ return bootstrap('texture:msaa', 'texture', {
162
+ ...resource.create,
163
+ handleId: 'texture:msaa',
164
+ desc: { ...desc, sampleCount: 4, usage: 0x10 },
165
+ });
166
+ }
167
+ if (resource.handleId === 'texture-view:rt') {
168
+ return bootstrap('texture-view:msaa', 'texture-view', {
169
+ ...resource.create,
170
+ sourceHandleId: 'texture:msaa',
171
+ resultHandleId: 'texture-view:msaa',
172
+ });
173
+ }
174
+ if (resource.handleId === 'pipeline:triangle') {
175
+ const desc = resource.create.desc as Record<string, unknown>;
176
+ return bootstrap('pipeline:triangle', 'pipeline', {
177
+ ...resource.create,
178
+ desc: { ...desc, multisample: { count: 4 } },
179
+ });
180
+ }
181
+ return resource;
182
+ });
183
+ bootstrapResources.push(
184
+ bootstrap('texture:resolve', 'texture', {
185
+ kind: 'createTexture',
186
+ handleId: 'texture:resolve',
187
+ desc: {
188
+ size: { width: WIDTH, height: HEIGHT, depthOrArrayLayers: 1 },
189
+ format: 'rgba8unorm',
190
+ dimension: '2d',
191
+ mipLevelCount: 1,
192
+ sampleCount: 1,
193
+ usage: 0x11,
194
+ },
195
+ }),
196
+ bootstrap('texture-view:resolve', 'texture-view', {
197
+ kind: 'createTextureView',
198
+ sourceHandleId: 'texture:resolve',
199
+ resultHandleId: 'texture-view:resolve',
200
+ desc: {},
201
+ }),
202
+ );
203
+
204
+ const events = tape.events.map((event) => {
205
+ if (event.kind === 'beginRenderPass') {
206
+ return {
207
+ ...event,
208
+ desc: {
209
+ ...event.desc,
210
+ colorAttachments: [
211
+ {
212
+ view: null,
213
+ clearValue: { r: 0, g: 0, b: 0, a: 1 },
214
+ loadOp: 'clear',
215
+ storeOp: 'store',
216
+ },
217
+ ],
218
+ },
219
+ colorAttachmentViewHandleIds: ['texture-view:msaa'],
220
+ colorAttachmentResolveTargetHandleIds: ['texture-view:resolve'],
221
+ };
222
+ }
223
+ return event;
224
+ }) as Tape['events'];
225
+
226
+ return {
227
+ ...tape,
228
+ header: { ...tape.header, blobCount: 0 },
229
+ bootstrap: bootstrapResources,
230
+ events,
231
+ blobs: [],
232
+ };
233
+ }
234
+
156
235
  describe.skipIf(SKIP_DAWN)('ReplaySession Dawn contract', () => {
236
+ it.each([
237
+ 0x84, 0x184, 0x06,
238
+ ])('restores captured buffer bytes without COPY_DST (usage %i)', async (usage) => {
239
+ const pack = await loadDawn();
240
+ const expected = new Uint8Array([17, 31, 63, 127, 5, 9, 13, 21]);
241
+ const tape: Tape = {
242
+ header: { formatVersion: 7, rhiCaps: {}, eventCount: 0, blobCount: 1 },
243
+ bootstrap: [
244
+ bootstrap(
245
+ 'buffer:seed',
246
+ 'buffer',
247
+ {
248
+ kind: 'createBuffer',
249
+ handleId: 'buffer:seed',
250
+ desc: { size: expected.length, usage },
251
+ },
252
+ [{ hash: 'seed', byteOffset: 0, byteLength: expected.length }],
253
+ ),
254
+ ],
255
+ events: [],
256
+ blobs: [{ hash: 'seed', bytes: expected, compression: 'none' }],
257
+ };
258
+ const replay = (
259
+ await openReplay(tape, {
260
+ device: await freshDevice(pack),
261
+ createShaderModule: pack.createShaderModule,
262
+ })
263
+ ).unwrap();
264
+ try {
265
+ expect((await replay.readResource('buffer:seed')).unwrap().bytes).toEqual(expected);
266
+ expect(tape.bootstrap[0]?.create.desc).toEqual({ size: expected.length, usage });
267
+ } finally {
268
+ (await replay.dispose()).unwrap();
269
+ }
270
+ });
271
+
157
272
  it('replays a triangle and returns attachment bytes through inspectWork', async () => {
158
273
  const pack = await loadDawn();
159
274
  const tape = triangleTape();
@@ -175,6 +290,19 @@ describe.skipIf(SKIP_DAWN)('ReplaySession Dawn contract', () => {
175
290
  expect(baseline.value.attachment?.provenance.selectedWorkIndex).toBe(0);
176
291
  expect(baseline.value.attachment?.provenance.subresource).toBeNull();
177
292
 
293
+ const selected = await first.value.readResourceAtWork('texture-view:rt', 0);
294
+ expect(
295
+ selected.ok,
296
+ selected.ok ? undefined : `${selected.error.code}: ${JSON.stringify(selected.error.detail)}`,
297
+ ).toBe(true);
298
+ if (!selected.ok) throw new Error(selected.error.hint);
299
+ expect(selected.value.provenance.selectedWorkIndex).toBe(0);
300
+ expect(selected.value.bytes).toEqual(baseline.value.attachment?.bytes);
301
+ const bootstrap = await first.value.readResource('texture-view:rt');
302
+ if (!bootstrap.ok) throw new Error(bootstrap.error.hint);
303
+ expect(bootstrap.value.provenance.selectedWorkIndex).toBeUndefined();
304
+ expect(selected.value.bytes).not.toEqual(bootstrap.value.bytes);
305
+
178
306
  const second = await openReplay(tape, {
179
307
  device: await freshDevice(pack),
180
308
  createShaderModule: pack.createShaderModule,
@@ -189,6 +317,49 @@ describe.skipIf(SKIP_DAWN)('ReplaySession Dawn contract', () => {
189
317
  expect((await second.value.dispose()).ok).toBe(true);
190
318
  }, 60_000);
191
319
 
320
+ it('resolves an MSAA attachment for pixels and rejects direct MSAA readback', async () => {
321
+ const pack = await loadDawn();
322
+ const replayResult = await openReplay(msaaTriangleTape(), {
323
+ device: await freshDevice(pack),
324
+ createShaderModule: pack.createShaderModule,
325
+ });
326
+ expect(replayResult.ok).toBe(true);
327
+ if (!replayResult.ok) throw new Error(replayResult.error.hint);
328
+
329
+ try {
330
+ const first = await replayResult.value.inspectWork(0, ['pixels']);
331
+ expect(
332
+ first.ok,
333
+ first.ok ? undefined : `${first.error.code}: ${JSON.stringify(first.error.detail)}`,
334
+ ).toBe(true);
335
+ if (!first.ok) throw new Error(first.error.hint);
336
+ const attachment = first.value.attachment;
337
+ expect(attachment?.provenance.resourceId).toBe('texture-view:resolve');
338
+ expect(attachment?.provenance.selectedWorkIndex).toBe(0);
339
+ expect(attachment?.provenance.subresource).toBeNull();
340
+ const centerOffset = (Math.floor(HEIGHT / 2) * WIDTH + Math.floor(WIDTH / 2)) * 4;
341
+ expect(attachment?.bytes.slice(centerOffset, centerOffset + 4)).toEqual(
342
+ new Uint8Array([255, 0, 0, 255]),
343
+ );
344
+
345
+ const repeated = await replayResult.value.inspectWork(0, ['pixels']);
346
+ expect(repeated.ok).toBe(true);
347
+ if (!repeated.ok) throw new Error(repeated.error.hint);
348
+ expect(repeated.value.attachment?.bytes).toEqual(attachment?.bytes);
349
+ expect(repeated.value.attachment?.provenance.resourceId).toBe('texture-view:resolve');
350
+ expect(repeated.value.attachment?.provenance.selectedWorkIndex).toBe(0);
351
+ expect(repeated.value.attachment?.provenance.subresource).toBeNull();
352
+
353
+ const directMsaa = await replayResult.value.readResource('texture:msaa');
354
+ expect(directMsaa.ok).toBe(false);
355
+ if (directMsaa.ok) throw new Error('MSAA texture readback unexpectedly succeeded');
356
+ expect(directMsaa.error.code).toBe('readback-unsupported');
357
+ expect(directMsaa.error.detail).toMatchObject({ resourceId: 'texture:msaa' });
358
+ } finally {
359
+ expect((await replayResult.value.dispose()).ok).toBe(true);
360
+ }
361
+ }, 60_000);
362
+
192
363
  it('reports the first shader factory failure on a real Dawn device', async () => {
193
364
  const pack = await loadDawn();
194
365
  const device = await freshDevice(pack);
@@ -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
  });
@@ -0,0 +1,61 @@
1
+ import { rhi } from '@forgeax/engine-rhi-webgpu';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { decodeTape } from '../protocol/codec';
4
+ import { validateTape } from '../protocol/validation';
5
+ import { wrap } from '../recorder';
6
+ import { assembleTape } from '../recorder/assemble';
7
+
8
+ describe('timestamp query capture ownership', () => {
9
+ it.each([
10
+ 'render',
11
+ 'compute',
12
+ 'empty-compute',
13
+ ] as const)('retains a pre-capture QuerySet for %s passes', async (kind) => {
14
+ const recorded = wrap(rhi);
15
+ const adapter = (await recorded.requestAdapter()).unwrap();
16
+ const device = (
17
+ await adapter.requestDevice({ requiredFeatures: ['timestamp-query'] })
18
+ ).unwrap();
19
+ const querySet = device.createQuerySet({ type: 'timestamp', count: 2 }).unwrap();
20
+ const target = device
21
+ .createTexture({ size: { width: 4, height: 4 }, format: 'rgba8unorm', usage: 0x10 })
22
+ .unwrap();
23
+ const view = device.createTextureView(target, {}).unwrap();
24
+ try {
25
+ recorded.arm(1);
26
+ const encoder = device.createCommandEncoder().unwrap();
27
+ const timestampWrites = { querySet, beginningOfPassWriteIndex: 0, endOfPassWriteIndex: 1 };
28
+ if (kind === 'render')
29
+ encoder
30
+ .beginRenderPass({
31
+ colorAttachments: [{ view, loadOp: 'clear', storeOp: 'store' }],
32
+ timestampWrites,
33
+ })
34
+ .end();
35
+ else if (kind === 'compute') encoder.beginComputePass({ timestampWrites }).end();
36
+ else encoder.encodeEmptyComputePass({ timestampWrites });
37
+ device.queue.submit([encoder.finish().unwrap()]).unwrap();
38
+ await device.queue.onSubmittedWorkDone();
39
+ recorded.onFrameEnd();
40
+ const tape = recorded.getTape();
41
+ if (tape === undefined || !('events' in tape)) throw new Error('Missing captured tape');
42
+ const pass = tape.events.find(
43
+ (event) => event.kind === 'beginRenderPass' || event.kind === 'beginComputePass',
44
+ );
45
+ expect(pass).toHaveProperty('timestampQuerySetHandleId');
46
+ if (pass?.kind !== 'beginRenderPass' && pass?.kind !== 'beginComputePass')
47
+ throw new Error('Missing pass');
48
+ expect(pass.desc?.timestampWrites).toEqual({
49
+ beginningOfPassWriteIndex: 0,
50
+ endOfPassWriteIndex: 1,
51
+ });
52
+ const encoded = assembleTape(recorded).unwrap().bytes;
53
+ const decoded = decodeTape(encoded).unwrap();
54
+ expect(decoded.bootstrap.some((row) => row.kind === 'query-set')).toBe(true);
55
+ expect(validateTape(decoded).ok).toBe(true);
56
+ } finally {
57
+ device.destroyQuerySet(querySet).unwrap();
58
+ device.destroyTexture(target).unwrap();
59
+ }
60
+ });
61
+ });
package/src/index.ts CHANGED
@@ -66,6 +66,7 @@ export {
66
66
  attachRecorder,
67
67
  type CaptureFrameOptions,
68
68
  type CreateShaderModuleFn,
69
+ type CreateShaderModuleImmediateFn,
69
70
  type EncodedTape,
70
71
  type RecordableBackend,
71
72
  type RecorderAttachment,
@@ -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' || event.kind === 'destroyTexture'
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 (kind.startsWith('create') || kind.startsWith('destroy') || kind === 'initialData')
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,9 @@ function referencedHandles(event: RhiCallEvent): readonly string[] {
153
169
  'resourceHandleIds',
154
170
  'bindGroupHandleId',
155
171
  'pipelineHandleId',
172
+ 'querySetHandleId',
173
+ 'occlusionQuerySetHandleId',
174
+ 'timestampQuerySetHandleId',
156
175
  'indexBufferHandleId',
157
176
  'vertexBufferHandleId',
158
177
  ];
@@ -11,6 +11,7 @@ export type TapeBlobCompression = 'none' | 'gzip';
11
11
  export type ResourceKind =
12
12
  | 'buffer'
13
13
  | 'texture'
14
+ | 'query-set'
14
15
  | 'texture-view'
15
16
  | 'sampler'
16
17
  | 'shader-module'
@@ -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,37 @@ 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
+ 'timestampQuerySetHandleId',
177
+ 'colorAttachmentViewHandleIds',
178
+ 'colorAttachmentResolveTargetHandleIds',
179
+ 'depthStencilViewHandleId',
180
+ ]),
181
+ destroyed: [],
182
+ };
183
+ case 'beginComputePass':
184
+ return {
185
+ created: [event.passHandleId],
186
+ reads: handleRefs(event, ['cmdHandleId', 'timestampQuerySetHandleId']),
187
+ destroyed: [],
188
+ };
189
+ case 'endRenderPass':
190
+ case 'endComputePass':
191
+ return { created: [], reads: [event.passHandleId], destroyed: [event.passHandleId] };
192
+ case 'beginOcclusionQuery':
193
+ case 'endOcclusionQuery':
194
+ return { created: [], reads: handleRefs(event, ['passHandleId']), destroyed: [] };
159
195
  default:
160
196
  return { created: [], reads: [], destroyed: [] };
161
197
  }
@@ -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 (event.kind.startsWith('create') && 'handleId' in event) return event.handleId;
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':