@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/replay/execute.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
RhiDevice,
|
|
11
11
|
RhiError,
|
|
12
12
|
RhiQueue,
|
|
13
|
+
RhiRenderPipelineOps,
|
|
13
14
|
Texture,
|
|
14
15
|
} from '@forgeax/engine-rhi';
|
|
15
16
|
import { ok, type Result } from '@forgeax/engine-types';
|
|
@@ -70,10 +71,20 @@ export async function executeEvent(
|
|
|
70
71
|
kind: 'texture',
|
|
71
72
|
},
|
|
72
73
|
);
|
|
74
|
+
case 'createQuerySet':
|
|
75
|
+
return createResource(
|
|
76
|
+
context,
|
|
77
|
+
eventIndex,
|
|
78
|
+
event,
|
|
79
|
+
context.device.createQuerySet(event.desc),
|
|
80
|
+
{ kind: 'query-set' },
|
|
81
|
+
);
|
|
73
82
|
case 'destroyBuffer':
|
|
74
83
|
return destroyResource(context, eventIndex, event, 'buffer');
|
|
75
84
|
case 'destroyTexture':
|
|
76
85
|
return destroyResource(context, eventIndex, event, 'texture');
|
|
86
|
+
case 'destroyQuerySet':
|
|
87
|
+
return destroyQuerySet(context, event, eventIndex);
|
|
77
88
|
case 'createTextureView': {
|
|
78
89
|
const texture = requireResource<Texture>(
|
|
79
90
|
context,
|
|
@@ -110,6 +121,28 @@ export async function executeEvent(
|
|
|
110
121
|
context.device.createBindGroupLayout(event.desc as never),
|
|
111
122
|
{ kind: 'binding', role: 'bind-group-layout' },
|
|
112
123
|
);
|
|
124
|
+
case 'getBindGroupLayout': {
|
|
125
|
+
const pipeline = requireReplayResource(
|
|
126
|
+
context,
|
|
127
|
+
event.pipelineHandleId,
|
|
128
|
+
'pipeline',
|
|
129
|
+
eventIndex,
|
|
130
|
+
event.kind,
|
|
131
|
+
);
|
|
132
|
+
if (!pipeline.ok) return pipeline;
|
|
133
|
+
return createResource(
|
|
134
|
+
context,
|
|
135
|
+
eventIndex,
|
|
136
|
+
event,
|
|
137
|
+
ok(
|
|
138
|
+
(
|
|
139
|
+
pipeline.value.value as import('@forgeax/engine-rhi').RenderPipeline &
|
|
140
|
+
RhiRenderPipelineOps
|
|
141
|
+
).getBindGroupLayout(event.index),
|
|
142
|
+
),
|
|
143
|
+
{ kind: 'binding', role: 'bind-group-layout' },
|
|
144
|
+
);
|
|
145
|
+
}
|
|
113
146
|
case 'createBindGroup':
|
|
114
147
|
return createBindGroup(context, event, eventIndex);
|
|
115
148
|
case 'createPipelineLayout':
|
|
@@ -141,6 +174,22 @@ export async function executeEvent(
|
|
|
141
174
|
return beginRenderPass(context, event, eventIndex);
|
|
142
175
|
case 'beginComputePass':
|
|
143
176
|
return beginComputePass(context, event, eventIndex);
|
|
177
|
+
case 'resolveQuerySet':
|
|
178
|
+
return resolveQuerySet(context, event, eventIndex);
|
|
179
|
+
case 'beginOcclusionQuery':
|
|
180
|
+
return passCall(context, event, eventIndex, 'render-pass', (pass) => {
|
|
181
|
+
const result = pass.beginOcclusionQuery(event.queryIndex);
|
|
182
|
+
return result.ok
|
|
183
|
+
? ok(undefined)
|
|
184
|
+
: eventFailure(eventIndex, event, 'encode', result.error);
|
|
185
|
+
});
|
|
186
|
+
case 'endOcclusionQuery':
|
|
187
|
+
return passCall(context, event, eventIndex, 'render-pass', (pass) => {
|
|
188
|
+
const result = pass.endOcclusionQuery();
|
|
189
|
+
return result.ok
|
|
190
|
+
? ok(undefined)
|
|
191
|
+
: eventFailure(eventIndex, event, 'encode', result.error);
|
|
192
|
+
});
|
|
144
193
|
case 'copyBufferToBuffer':
|
|
145
194
|
return copyBufferToBuffer(context, event, eventIndex);
|
|
146
195
|
case 'copyBufferToTexture':
|
|
@@ -386,6 +435,60 @@ function destroyResource(
|
|
|
386
435
|
return ok(undefined);
|
|
387
436
|
}
|
|
388
437
|
|
|
438
|
+
function destroyQuerySet(
|
|
439
|
+
context: ReplayExecutionContext,
|
|
440
|
+
event: Extract<RhiCallEvent, { kind: 'destroyQuerySet' }>,
|
|
441
|
+
eventIndex: number,
|
|
442
|
+
): Result<void, RhiDebugError> {
|
|
443
|
+
const entry = context.table.get(event.handleId);
|
|
444
|
+
if (entry?.resource.kind !== 'query-set')
|
|
445
|
+
return missingResource(eventIndex, event, event.handleId, 'query-set');
|
|
446
|
+
const result = context.device.destroyQuerySet(entry.resource.value);
|
|
447
|
+
if (!result.ok) return eventFailure(eventIndex, event, 'create', result.error);
|
|
448
|
+
context.table.delete(event.handleId);
|
|
449
|
+
return ok(undefined);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function resolveQuerySet(
|
|
453
|
+
context: ReplayExecutionContext,
|
|
454
|
+
event: Extract<RhiCallEvent, { kind: 'resolveQuerySet' }>,
|
|
455
|
+
eventIndex: number,
|
|
456
|
+
): Result<void, RhiDebugError> {
|
|
457
|
+
const encoder = requireResource<RhiCommandEncoder>(
|
|
458
|
+
context,
|
|
459
|
+
event.cmdHandleId,
|
|
460
|
+
'encoder',
|
|
461
|
+
eventIndex,
|
|
462
|
+
event.kind,
|
|
463
|
+
'command',
|
|
464
|
+
);
|
|
465
|
+
const querySet = requireResource<import('@forgeax/engine-rhi').QuerySet>(
|
|
466
|
+
context,
|
|
467
|
+
event.querySetHandleId,
|
|
468
|
+
'query-set',
|
|
469
|
+
eventIndex,
|
|
470
|
+
event.kind,
|
|
471
|
+
);
|
|
472
|
+
const destination = requireResource<Buffer>(
|
|
473
|
+
context,
|
|
474
|
+
event.destinationHandleId,
|
|
475
|
+
'buffer',
|
|
476
|
+
eventIndex,
|
|
477
|
+
event.kind,
|
|
478
|
+
);
|
|
479
|
+
if (!encoder.ok) return encoder;
|
|
480
|
+
if (!querySet.ok) return querySet;
|
|
481
|
+
if (!destination.ok) return destination;
|
|
482
|
+
const result = encoder.value.resolveQuerySet(
|
|
483
|
+
querySet.value,
|
|
484
|
+
event.firstQuery,
|
|
485
|
+
event.queryCount,
|
|
486
|
+
destination.value,
|
|
487
|
+
event.destinationOffset,
|
|
488
|
+
);
|
|
489
|
+
return result.ok ? ok(undefined) : eventFailure(eventIndex, event, 'encode', result.error);
|
|
490
|
+
}
|
|
491
|
+
|
|
389
492
|
async function createShaderModule(
|
|
390
493
|
context: ReplayExecutionContext,
|
|
391
494
|
event: Extract<RhiCallEvent, { kind: 'createShaderModule' }>,
|
|
@@ -673,8 +776,18 @@ function beginRenderPass(
|
|
|
673
776
|
event.depthStencilViewHandleId === undefined
|
|
674
777
|
? undefined
|
|
675
778
|
: context.table.get(event.depthStencilViewHandleId)?.resource;
|
|
779
|
+
const querySet =
|
|
780
|
+
event.occlusionQuerySetHandleId === undefined
|
|
781
|
+
? undefined
|
|
782
|
+
: context.table.get(event.occlusionQuerySetHandleId)?.resource;
|
|
783
|
+
if (event.occlusionQuerySetHandleId !== undefined && querySet?.kind !== 'query-set') {
|
|
784
|
+
return missingResource(eventIndex, event, event.occlusionQuerySetHandleId, 'query-set');
|
|
785
|
+
}
|
|
786
|
+
const timestamp = replayTimestampWrites(context, event, eventIndex);
|
|
787
|
+
if (!timestamp.ok) return timestamp;
|
|
676
788
|
const descriptor = {
|
|
677
789
|
...event.desc,
|
|
790
|
+
...(timestamp.value === undefined ? {} : { timestampWrites: timestamp.value }),
|
|
678
791
|
colorAttachments: colors,
|
|
679
792
|
...(depth?.kind === 'texture-view'
|
|
680
793
|
? {
|
|
@@ -684,6 +797,7 @@ function beginRenderPass(
|
|
|
684
797
|
} as RenderPassDepthStencilAttachment,
|
|
685
798
|
}
|
|
686
799
|
: {}),
|
|
800
|
+
...(querySet?.kind === 'query-set' ? { occlusionQuerySet: querySet.value } : {}),
|
|
687
801
|
};
|
|
688
802
|
const pass = encoder.value.beginRenderPass(descriptor as never);
|
|
689
803
|
return context.table.set(
|
|
@@ -693,6 +807,29 @@ function beginRenderPass(
|
|
|
693
807
|
);
|
|
694
808
|
}
|
|
695
809
|
|
|
810
|
+
function replayTimestampWrites(
|
|
811
|
+
context: ReplayExecutionContext,
|
|
812
|
+
event: Extract<RhiCallEvent, { kind: 'beginRenderPass' | 'beginComputePass' }>,
|
|
813
|
+
eventIndex: number,
|
|
814
|
+
): Result<import('@forgeax/engine-rhi').RenderPassTimestampWrites | undefined, RhiDebugError> {
|
|
815
|
+
if (event.desc?.timestampWrites === undefined) return ok(undefined);
|
|
816
|
+
const handle = event.timestampQuerySetHandleId;
|
|
817
|
+
if (handle === undefined)
|
|
818
|
+
return missingResource(eventIndex, event, 'timestamp-query-set', 'query-set');
|
|
819
|
+
const query = requireResource<import('@forgeax/engine-rhi').QuerySet>(
|
|
820
|
+
context,
|
|
821
|
+
handle,
|
|
822
|
+
'query-set',
|
|
823
|
+
eventIndex,
|
|
824
|
+
event.kind,
|
|
825
|
+
);
|
|
826
|
+
if (!query.ok) return query;
|
|
827
|
+
return ok({
|
|
828
|
+
...event.desc.timestampWrites,
|
|
829
|
+
querySet: query.value,
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
|
|
696
833
|
function beginComputePass(
|
|
697
834
|
context: ReplayExecutionContext,
|
|
698
835
|
event: Extract<RhiCallEvent, { kind: 'beginComputePass' }>,
|
|
@@ -707,7 +844,12 @@ function beginComputePass(
|
|
|
707
844
|
'command',
|
|
708
845
|
);
|
|
709
846
|
if (!encoder.ok) return encoder;
|
|
710
|
-
const
|
|
847
|
+
const timestamp = replayTimestampWrites(context, event, eventIndex);
|
|
848
|
+
if (!timestamp.ok) return timestamp;
|
|
849
|
+
const pass = encoder.value.beginComputePass({
|
|
850
|
+
...(event.desc?.label === undefined ? {} : { label: event.desc.label }),
|
|
851
|
+
...(timestamp.value === undefined ? {} : { timestampWrites: timestamp.value }),
|
|
852
|
+
});
|
|
711
853
|
return context.table.set(
|
|
712
854
|
event.passHandleId,
|
|
713
855
|
{ kind: 'encoder', role: 'compute-pass', value: pass },
|
package/src/replay/readback.ts
CHANGED
|
@@ -100,7 +100,7 @@ function resolveTextureViewSubresource(
|
|
|
100
100
|
source: ResourceTableEntry,
|
|
101
101
|
requested: ReplayReadbackRequest | undefined,
|
|
102
102
|
): Result<ReplayReadbackRequest | undefined, RhiDebugError> {
|
|
103
|
-
if (
|
|
103
|
+
if (isBufferRange(requested)) return ok(requested);
|
|
104
104
|
const sourceDescriptor = recordField(source.descriptor, 'desc');
|
|
105
105
|
const sourceSize = textureSize(sourceDescriptor?.size);
|
|
106
106
|
const sourceMipCount = numberField(sourceDescriptor, 'mipLevelCount') ?? 1;
|
|
@@ -112,8 +112,8 @@ function resolveTextureViewSubresource(
|
|
|
112
112
|
const arrayLayerCount =
|
|
113
113
|
numberField(viewDescriptor, 'arrayLayerCount') ??
|
|
114
114
|
sourceSize.depthOrArrayLayers - baseArrayLayer;
|
|
115
|
-
const localMipLevel = requested
|
|
116
|
-
const localArrayLayer = requested
|
|
115
|
+
const localMipLevel = requested?.mipLevel ?? 0;
|
|
116
|
+
const localArrayLayer = requested?.arrayLayer ?? 0;
|
|
117
117
|
if (
|
|
118
118
|
!validIndex(baseMipLevel, sourceMipCount + 1) ||
|
|
119
119
|
!validIndex(baseArrayLayer, sourceSize.depthOrArrayLayers + 1) ||
|
|
@@ -128,6 +128,11 @@ function resolveTextureViewSubresource(
|
|
|
128
128
|
...requested,
|
|
129
129
|
mipLevel: baseMipLevel + localMipLevel,
|
|
130
130
|
arrayLayer: baseArrayLayer + localArrayLayer,
|
|
131
|
+
aspect:
|
|
132
|
+
requested?.aspect ??
|
|
133
|
+
(viewDescriptor?.aspect === 'depth-only' || viewDescriptor?.aspect === 'stencil-only'
|
|
134
|
+
? viewDescriptor.aspect
|
|
135
|
+
: 'all'),
|
|
131
136
|
});
|
|
132
137
|
}
|
|
133
138
|
|
|
@@ -168,6 +173,13 @@ async function readTexture(
|
|
|
168
173
|
const format = stringField(descriptor, 'format');
|
|
169
174
|
const dimension = stringField(descriptor, 'dimension') ?? '2d';
|
|
170
175
|
if (format === undefined) return readbackFailure(`texture ${resourceId} has no recorded format`);
|
|
176
|
+
if ((numberField(descriptor, 'sampleCount') ?? 1) > 1) {
|
|
177
|
+
return readbackUnsupported(
|
|
178
|
+
resourceId,
|
|
179
|
+
format,
|
|
180
|
+
'multisampled textures require a single-sample resolve target before readback',
|
|
181
|
+
);
|
|
182
|
+
}
|
|
171
183
|
const plan = getTextureReadbackPlan({ format, dimension });
|
|
172
184
|
if (!plan.supported) {
|
|
173
185
|
return err(
|
package/src/replay/resources.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
CommandBuffer,
|
|
6
6
|
ComputePipeline,
|
|
7
7
|
PipelineLayout,
|
|
8
|
+
QuerySet,
|
|
8
9
|
RenderPipeline,
|
|
9
10
|
RhiCommandEncoder,
|
|
10
11
|
RhiComputePassEncoder,
|
|
@@ -24,6 +25,7 @@ export type ReplayResource = {
|
|
|
24
25
|
} & (
|
|
25
26
|
| { readonly kind: 'buffer'; readonly value: Buffer }
|
|
26
27
|
| { readonly kind: 'texture'; readonly value: Texture }
|
|
28
|
+
| { readonly kind: 'query-set'; readonly value: QuerySet }
|
|
27
29
|
| { readonly kind: 'texture-view'; readonly value: TextureView }
|
|
28
30
|
| { readonly kind: 'sampler'; readonly value: Sampler }
|
|
29
31
|
| { readonly kind: 'shader-module'; readonly value: ShaderModule }
|
|
@@ -132,6 +134,10 @@ export class ResourceTable {
|
|
|
132
134
|
const result = this.device.destroyTexture(entry.resource.value);
|
|
133
135
|
if (!result.ok && firstFailure === undefined)
|
|
134
136
|
firstFailure = resourceDisposeFailure(entry.resourceId, result.error.code);
|
|
137
|
+
} else if (entry.resource.kind === 'query-set') {
|
|
138
|
+
const result = this.device.destroyQuerySet(entry.resource.value);
|
|
139
|
+
if (!result.ok && firstFailure === undefined)
|
|
140
|
+
firstFailure = resourceDisposeFailure(entry.resourceId, result.error.code);
|
|
135
141
|
}
|
|
136
142
|
}
|
|
137
143
|
return firstFailure ?? ok(undefined);
|
package/src/replay/session.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
MappedBuffer,
|
|
2
3
|
RhiCommandEncoder,
|
|
3
4
|
RhiComputePassEncoder,
|
|
4
5
|
RhiDevice,
|
|
@@ -69,6 +70,18 @@ export interface ReplaySession {
|
|
|
69
70
|
subresource?: ReadbackSubresource,
|
|
70
71
|
signal?: AbortSignal,
|
|
71
72
|
): Promise<Result<ReplayReadbackResult, RhiDebugError>>;
|
|
73
|
+
/**
|
|
74
|
+
* Replay the captured stream through one indexed work item, then read the
|
|
75
|
+
* resource from that exact post-work state. Unlike readResource(), this is
|
|
76
|
+
* not a bootstrap-only read and therefore can inspect compute/storage
|
|
77
|
+
* outputs produced by the selected work without creating a second tape.
|
|
78
|
+
*/
|
|
79
|
+
readResourceAtWork(
|
|
80
|
+
resourceId: string,
|
|
81
|
+
workIndex: number,
|
|
82
|
+
subresource?: ReadbackSubresource,
|
|
83
|
+
signal?: AbortSignal,
|
|
84
|
+
): Promise<Result<ReplayReadbackResult, RhiDebugError>>;
|
|
72
85
|
dispose(): Promise<Result<void, RhiDebugError>>;
|
|
73
86
|
}
|
|
74
87
|
|
|
@@ -204,6 +217,42 @@ export async function openReplay(
|
|
|
204
217
|
backend.createShaderModule,
|
|
205
218
|
);
|
|
206
219
|
},
|
|
220
|
+
async readResourceAtWork(resourceId, workIndex, subresource, signal) {
|
|
221
|
+
if (disposed) return positionError(workIndex, index.works.length);
|
|
222
|
+
if (signal?.aborted) {
|
|
223
|
+
return err(
|
|
224
|
+
createRhiDebugError('readback-failed', {
|
|
225
|
+
stage: 'readback',
|
|
226
|
+
cause: 'readback was aborted',
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
const work = index.works[workIndex];
|
|
231
|
+
if (work === undefined || model.works[workIndex] === undefined) {
|
|
232
|
+
return positionError(workIndex, index.works.length);
|
|
233
|
+
}
|
|
234
|
+
const cleared = await reset();
|
|
235
|
+
if (!cleared.ok) return cleared;
|
|
236
|
+
const bootstrapped = await prepare();
|
|
237
|
+
if (!bootstrapped.ok) return bootstrapped;
|
|
238
|
+
const replayed = await replayThroughWork(context, index, work, signal);
|
|
239
|
+
if (!replayed.ok) return replayed;
|
|
240
|
+
const read = await readReplayResource(
|
|
241
|
+
backend.device,
|
|
242
|
+
table,
|
|
243
|
+
resourceId,
|
|
244
|
+
subresource,
|
|
245
|
+
backend.createShaderModule,
|
|
246
|
+
);
|
|
247
|
+
if (!read.ok) return read;
|
|
248
|
+
return ok({
|
|
249
|
+
...read.value,
|
|
250
|
+
provenance: {
|
|
251
|
+
...read.value.provenance,
|
|
252
|
+
selectedWorkIndex: work.workIndex,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
},
|
|
207
256
|
async dispose() {
|
|
208
257
|
if (disposed) return ok(undefined);
|
|
209
258
|
const result = table.dispose();
|
|
@@ -328,7 +377,14 @@ async function executeBootstrapResource(
|
|
|
328
377
|
bootstrapIndex: number,
|
|
329
378
|
): Promise<Result<void, RhiDebugError>> {
|
|
330
379
|
const event: RhiCallEvent = JSON.parse(JSON.stringify(resource.create));
|
|
331
|
-
|
|
380
|
+
// Captured GPU-written buffers need not allow COPY_DST. Restore their
|
|
381
|
+
// initial bytes through creation mapping, which is legal for every usage
|
|
382
|
+
// (including MAP_WRITE, where adding COPY_DST would itself be invalid).
|
|
383
|
+
const replayEvent =
|
|
384
|
+
event.kind === 'createBuffer' && resource.initialData.length > 0
|
|
385
|
+
? { ...event, desc: { ...event.desc, mappedAtCreation: true } }
|
|
386
|
+
: event;
|
|
387
|
+
const result = await executeEvent(context, replayEvent, -bootstrapIndex - 1);
|
|
332
388
|
return result;
|
|
333
389
|
}
|
|
334
390
|
|
|
@@ -351,25 +407,50 @@ async function seedBootstrapResource(
|
|
|
351
407
|
'bootstrap initialData requires a buffer or color texture resource',
|
|
352
408
|
);
|
|
353
409
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
410
|
+
// executeBootstrapResource created this buffer mapped specifically for
|
|
411
|
+
// restoration. It must be unmapped before any recorded work can consume it.
|
|
412
|
+
const mapped = entry.resource.value as MappedBuffer;
|
|
413
|
+
// RhiNull deliberately exposes opaque structural handles rather than a
|
|
414
|
+
// CPU-visible mapping implementation. Its replay bytes have no observable
|
|
415
|
+
// storage, so there is nothing to seed; real backends always provide the
|
|
416
|
+
// MappedBuffer methods below and still take the strict mapping path.
|
|
417
|
+
if (
|
|
418
|
+
typeof (mapped as Partial<MappedBuffer>).getMappedRange !== 'function' ||
|
|
419
|
+
typeof (mapped as Partial<MappedBuffer>).unmap !== 'function'
|
|
420
|
+
) {
|
|
421
|
+
if (context.device.caps.backendKind === 'null') return ok(undefined);
|
|
422
|
+
return eventFailure(
|
|
423
|
+
-bootstrapIndex - 1,
|
|
424
|
+
event,
|
|
425
|
+
'write',
|
|
426
|
+
'bootstrap buffer mapping is unavailable on the replay backend',
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
const range = mapped.getMappedRange();
|
|
431
|
+
if (!range.ok) return eventFailure(-bootstrapIndex - 1, event, 'write', range.error);
|
|
432
|
+
const target = new Uint8Array(range.value);
|
|
433
|
+
for (const slice of resource.initialData) {
|
|
434
|
+
const blob = context.tape.blobs.find((candidate) => candidate.hash === slice.hash);
|
|
435
|
+
if (blob === undefined) {
|
|
436
|
+
return eventFailure(-bootstrapIndex - 1, event, 'lookup', `blob ${slice.hash} is missing`);
|
|
437
|
+
}
|
|
438
|
+
const end = slice.byteOffset + slice.byteLength;
|
|
439
|
+
if (slice.byteOffset < 0 || slice.byteLength < 0 || end > blob.bytes.byteLength) {
|
|
440
|
+
return eventFailure(
|
|
441
|
+
-bootstrapIndex - 1,
|
|
442
|
+
event,
|
|
443
|
+
'lookup',
|
|
444
|
+
`blob ${slice.hash} does not contain initialData slice [${slice.byteOffset}, ${end})`,
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
if (slice.byteLength > target.byteLength) {
|
|
448
|
+
return eventFailure(-bootstrapIndex - 1, event, 'write', 'initialData exceeds buffer size');
|
|
449
|
+
}
|
|
450
|
+
target.set(blob.bytes.subarray(slice.byteOffset, end));
|
|
372
451
|
}
|
|
452
|
+
} finally {
|
|
453
|
+
mapped.unmap();
|
|
373
454
|
}
|
|
374
455
|
return ok(undefined);
|
|
375
456
|
}
|
|
@@ -489,12 +570,8 @@ async function finalizeWorkPass(
|
|
|
489
570
|
`pass ${begin.passHandleId} is not open`,
|
|
490
571
|
);
|
|
491
572
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
} catch (cause) {
|
|
495
|
-
return eventFailure(pass.endEventIndex ?? work.eventIndex, begin, 'encode', cause);
|
|
496
|
-
}
|
|
497
|
-
context.table.delete(begin.passHandleId);
|
|
573
|
+
const closed = closeReplayPass(context, entry, begin, pass, work.eventIndex);
|
|
574
|
+
if (!closed.ok) return closed;
|
|
498
575
|
const finishEventIndex = findNextEvent(
|
|
499
576
|
context.tape.events,
|
|
500
577
|
pass.beginEventIndex,
|
|
@@ -510,6 +587,13 @@ async function finalizeWorkPass(
|
|
|
510
587
|
`encoder ${begin.cmdHandleId} is not available`,
|
|
511
588
|
);
|
|
512
589
|
}
|
|
590
|
+
const closure = await replayCommandClosure(
|
|
591
|
+
context,
|
|
592
|
+
begin.cmdHandleId,
|
|
593
|
+
(pass.endEventIndex ?? work.eventIndex) + 1,
|
|
594
|
+
finishEventIndex ?? context.tape.events.length,
|
|
595
|
+
);
|
|
596
|
+
if (!closure.ok) return closure;
|
|
513
597
|
const finished = (encoder.resource.value as RhiCommandEncoder).finish();
|
|
514
598
|
const finishEvent = context.tape.events[finishEventIndex ?? work.eventIndex] ?? begin;
|
|
515
599
|
if (!finished.ok)
|
|
@@ -538,6 +622,131 @@ async function finalizeWorkPass(
|
|
|
538
622
|
return ok(undefined);
|
|
539
623
|
}
|
|
540
624
|
|
|
625
|
+
function closeReplayPass(
|
|
626
|
+
context: ReplayExecutionContext,
|
|
627
|
+
entry: NonNullable<ReturnType<ReplayExecutionContext['table']['get']>>,
|
|
628
|
+
begin: Extract<RhiCallEvent, { kind: 'beginRenderPass' | 'beginComputePass' }>,
|
|
629
|
+
pass: TapeIndex['passes'][number],
|
|
630
|
+
selectedEventIndex: number,
|
|
631
|
+
): Result<void, RhiDebugError> {
|
|
632
|
+
const activeQuery = activeOcclusionQuery(
|
|
633
|
+
context.tape.events,
|
|
634
|
+
begin.kind === 'beginRenderPass' ? begin.passHandleId : undefined,
|
|
635
|
+
pass.beginEventIndex,
|
|
636
|
+
selectedEventIndex,
|
|
637
|
+
);
|
|
638
|
+
if (!activeQuery.ok) return activeQuery;
|
|
639
|
+
try {
|
|
640
|
+
if (activeQuery.value !== undefined) {
|
|
641
|
+
if (entry.resource.role !== 'render-pass') {
|
|
642
|
+
return eventFailure(
|
|
643
|
+
activeQuery.value.eventIndex,
|
|
644
|
+
activeQuery.value.event,
|
|
645
|
+
'lookup',
|
|
646
|
+
'an active occlusion query belongs to a non-render pass',
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
const endedQuery = (entry.resource.value as RhiRenderPassEncoder).endOcclusionQuery();
|
|
650
|
+
if (!endedQuery.ok) {
|
|
651
|
+
return eventFailure(
|
|
652
|
+
activeQuery.value.eventIndex,
|
|
653
|
+
activeQuery.value.event,
|
|
654
|
+
'encode',
|
|
655
|
+
endedQuery.error,
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
(entry.resource.value as RhiRenderPassEncoder | RhiComputePassEncoder).end();
|
|
660
|
+
} catch (cause) {
|
|
661
|
+
return eventFailure(pass.endEventIndex ?? selectedEventIndex, begin, 'encode', cause);
|
|
662
|
+
}
|
|
663
|
+
context.table.delete(begin.passHandleId);
|
|
664
|
+
return ok(undefined);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
async function replayCommandClosure(
|
|
668
|
+
context: ReplayExecutionContext,
|
|
669
|
+
cmdHandleId: string,
|
|
670
|
+
start: number,
|
|
671
|
+
end: number,
|
|
672
|
+
): Promise<Result<void, RhiDebugError>> {
|
|
673
|
+
for (let eventIndex = start; eventIndex < end; eventIndex++) {
|
|
674
|
+
const event = context.tape.events[eventIndex];
|
|
675
|
+
if (event === undefined || !isCommandClosureEvent(event)) continue;
|
|
676
|
+
if (event.cmdHandleId !== cmdHandleId) continue;
|
|
677
|
+
const result = await executeEvent(context, event, eventIndex);
|
|
678
|
+
if (!result.ok) return result;
|
|
679
|
+
}
|
|
680
|
+
return ok(undefined);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
type ActiveOcclusionQuery = Extract<RhiCallEvent, { kind: 'beginOcclusionQuery' }>;
|
|
684
|
+
|
|
685
|
+
function activeOcclusionQuery(
|
|
686
|
+
events: readonly RhiCallEvent[],
|
|
687
|
+
passHandleId: string | undefined,
|
|
688
|
+
beginEventIndex: number,
|
|
689
|
+
selectedEventIndex: number,
|
|
690
|
+
): Result<
|
|
691
|
+
{ readonly eventIndex: number; readonly event: ActiveOcclusionQuery } | undefined,
|
|
692
|
+
RhiDebugError
|
|
693
|
+
> {
|
|
694
|
+
if (passHandleId === undefined) return ok(undefined);
|
|
695
|
+
let active: { readonly eventIndex: number; readonly event: ActiveOcclusionQuery } | undefined;
|
|
696
|
+
for (let eventIndex = beginEventIndex + 1; eventIndex <= selectedEventIndex; eventIndex++) {
|
|
697
|
+
const event = events[eventIndex];
|
|
698
|
+
if (event === undefined || !('passHandleId' in event) || event.passHandleId !== passHandleId)
|
|
699
|
+
continue;
|
|
700
|
+
if (event.kind === 'beginOcclusionQuery') {
|
|
701
|
+
if (active !== undefined) {
|
|
702
|
+
return eventFailure(
|
|
703
|
+
eventIndex,
|
|
704
|
+
event,
|
|
705
|
+
'encode',
|
|
706
|
+
'an occlusion query is already active for this render pass',
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
active = { eventIndex, event };
|
|
710
|
+
} else if (event.kind === 'endOcclusionQuery') {
|
|
711
|
+
if (active === undefined) {
|
|
712
|
+
return eventFailure(
|
|
713
|
+
eventIndex,
|
|
714
|
+
event,
|
|
715
|
+
'encode',
|
|
716
|
+
'an occlusion query ended without a matching begin',
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
active = undefined;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return ok(active);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function isCommandClosureEvent(event: RhiCallEvent): event is Extract<
|
|
726
|
+
RhiCallEvent,
|
|
727
|
+
{
|
|
728
|
+
kind:
|
|
729
|
+
| 'resolveQuerySet'
|
|
730
|
+
| 'copyBufferToBuffer'
|
|
731
|
+
| 'copyBufferToTexture'
|
|
732
|
+
| 'copyTextureToBuffer'
|
|
733
|
+
| 'copyTextureToTexture'
|
|
734
|
+
| 'clearBuffer';
|
|
735
|
+
}
|
|
736
|
+
> {
|
|
737
|
+
switch (event.kind) {
|
|
738
|
+
case 'resolveQuerySet':
|
|
739
|
+
case 'copyBufferToBuffer':
|
|
740
|
+
case 'copyBufferToTexture':
|
|
741
|
+
case 'copyTextureToBuffer':
|
|
742
|
+
case 'copyTextureToTexture':
|
|
743
|
+
case 'clearBuffer':
|
|
744
|
+
return true;
|
|
745
|
+
default:
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
541
750
|
function findNextEvent(
|
|
542
751
|
events: readonly RhiCallEvent[],
|
|
543
752
|
start: number,
|
|
@@ -578,9 +787,14 @@ async function readWorkAttachment(
|
|
|
578
787
|
}),
|
|
579
788
|
);
|
|
580
789
|
}
|
|
581
|
-
const
|
|
582
|
-
(candidate)
|
|
790
|
+
const attachmentIndex = begin.colorAttachmentViewHandleIds.findIndex(
|
|
791
|
+
(candidate) => candidate !== undefined,
|
|
583
792
|
);
|
|
793
|
+
// finalizeWorkPass has ended the pass and resolved the selected work.
|
|
794
|
+
// Keep the resolve target paired with its color slot, including sparse MRTs.
|
|
795
|
+
const viewId =
|
|
796
|
+
begin.colorAttachmentResolveTargetHandleIds?.[attachmentIndex] ??
|
|
797
|
+
begin.colorAttachmentViewHandleIds[attachmentIndex];
|
|
584
798
|
if (viewId === undefined) {
|
|
585
799
|
return err(
|
|
586
800
|
createRhiDebugError('readback-unsupported', {
|