@forgeax/engine-rhi-debug 0.1.6 → 0.1.7
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 +39 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/readback-matrix-fixture.d.ts +32 -0
- package/dist/__tests__/readback-matrix-fixture.d.ts.map +1 -0
- package/dist/frame-model.d.ts +84 -52
- package/dist/frame-model.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +395 -40
- package/dist/index.mjs.map +1 -1
- package/dist/replay/device-request.d.ts +8 -0
- package/dist/replay/device-request.d.ts.map +1 -0
- package/dist/replay/readback.d.ts +7 -0
- package/dist/replay/readback.d.ts.map +1 -1
- package/dist/replay/session.d.ts +17 -0
- package/dist/replay/session.d.ts.map +1 -1
- package/dist/texel-decode.d.ts +5 -4
- package/dist/texel-decode.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/__tests__/consumer-inventory.unit.test.ts +49 -1
- package/src/__tests__/e2e.browser.test.ts +6 -1
- package/src/__tests__/frame-model-parity.test-d.ts +13 -1
- package/src/__tests__/frame-model-parity.unit.test.ts +262 -15
- package/src/__tests__/guard-gates.test.ts +176 -1
- package/src/__tests__/public-surface.integration.test.ts +8 -0
- package/src/__tests__/readback-format-matrix.dawn.test.ts +226 -3
- package/src/__tests__/readback-format-matrix.unit.test.ts +25 -1
- package/src/__tests__/readback-matrix-fixture.ts +20 -0
- package/src/__tests__/replay-fail-fast.unit.test.ts +12 -0
- package/src/__tests__/replay-session.dawn.test.ts +3 -0
- package/src/__tests__/replay-session.test-d.ts +7 -1
- package/src/__tests__/replay-session.unit.test.ts +16 -0
- package/src/__tests__/rhi-debug-fresh-replay.dawn.test.ts +2 -0
- package/src/__tests__/tape-index.unit.test.ts +3 -0
- package/src/__tests__/tree-shake.unit.test.ts +24 -0
- package/src/frame-model.ts +381 -83
- package/src/index.ts +17 -10
- package/src/replay/device-request.ts +38 -0
- package/src/replay/readback.ts +106 -18
- package/src/replay/session.ts +47 -2
- package/src/texel-decode.ts +37 -3
package/src/frame-model.ts
CHANGED
|
@@ -2,117 +2,415 @@
|
|
|
2
2
|
|
|
3
3
|
/// <reference types="@webgpu/types" />
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
EVENT_SEMANTICS,
|
|
7
|
+
type EventCategory,
|
|
8
|
+
isWorkEvent,
|
|
9
|
+
resourceKindForEvent,
|
|
10
|
+
} from './protocol/event-semantics';
|
|
11
|
+
import type { TapeWorkEntry } from './protocol/tape-index';
|
|
7
12
|
import { buildTapeIndex } from './protocol/tape-index';
|
|
8
13
|
import type { Tape as V7Tape } from './protocol/types';
|
|
9
14
|
import { computeTextureLayout } from './texel-layout';
|
|
10
|
-
import type {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
readonly
|
|
23
|
-
readonly
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
| 'dispatchWorkgroupsIndirect'
|
|
28
|
-
| 'drawIndirect'
|
|
29
|
-
| 'drawIndexedIndirect';
|
|
15
|
+
import type { HandleId, RhiCallEvent } from './types';
|
|
16
|
+
|
|
17
|
+
type JsonPrimitive = string | number | boolean | null;
|
|
18
|
+
export type JsonValue =
|
|
19
|
+
| JsonPrimitive
|
|
20
|
+
| readonly JsonValue[]
|
|
21
|
+
| { readonly [key: string]: JsonValue };
|
|
22
|
+
|
|
23
|
+
export interface CommandEntry {
|
|
24
|
+
readonly eventIndex: number;
|
|
25
|
+
readonly passIndex: number;
|
|
26
|
+
readonly kind: string;
|
|
27
|
+
readonly category: EventCategory;
|
|
28
|
+
readonly isWork: boolean;
|
|
29
|
+
readonly params: JsonValue;
|
|
30
|
+
readonly group: readonly string[];
|
|
31
|
+
readonly marker?: string;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
export interface
|
|
34
|
+
export interface FramePass {
|
|
35
|
+
readonly passIndex: number;
|
|
33
36
|
readonly kind: 'render' | 'compute';
|
|
34
|
-
readonly
|
|
35
|
-
readonly
|
|
37
|
+
readonly beginEventIndex: number;
|
|
38
|
+
readonly endEventIndex: number | null;
|
|
39
|
+
readonly workIndices: readonly number[];
|
|
40
|
+
readonly commandIndices: readonly number[];
|
|
41
|
+
colorAttachmentViewHandleIds: readonly string[];
|
|
42
|
+
depthStencilViewHandleId: string | null;
|
|
36
43
|
}
|
|
37
44
|
|
|
38
|
-
export interface
|
|
39
|
-
readonly
|
|
40
|
-
readonly
|
|
41
|
-
readonly
|
|
42
|
-
readonly drawCall: InspectDrawCall;
|
|
43
|
-
readonly colorAttachmentHandleId: string | undefined;
|
|
44
|
-
readonly pipelineState: DrawPipelineState;
|
|
45
|
-
readonly vertexBuffers: ReadonlyMap<
|
|
46
|
-
number,
|
|
47
|
-
{ readonly handleId: HandleId; readonly offset: number; readonly size: number }
|
|
48
|
-
>;
|
|
49
|
-
readonly indexBuffer?: { handleId: HandleId; format: GPUIndexFormat; offset: number } | undefined;
|
|
50
|
-
readonly depthStencil: DrawDepthStencil;
|
|
45
|
+
export interface ResourceConsumer {
|
|
46
|
+
readonly eventIndex: number;
|
|
47
|
+
readonly workIndex: number | null;
|
|
48
|
+
readonly access: 'read' | 'write';
|
|
51
49
|
}
|
|
52
50
|
|
|
53
|
-
export interface
|
|
54
|
-
readonly
|
|
55
|
-
readonly
|
|
51
|
+
export interface ResourceEntry {
|
|
52
|
+
readonly resourceId: string;
|
|
53
|
+
readonly kind: string;
|
|
54
|
+
readonly origin: 'bootstrap' | 'frame';
|
|
55
|
+
readonly createEventIndex: number | null;
|
|
56
|
+
readonly destroyEventIndex: number | null;
|
|
57
|
+
readonly descriptor: JsonValue | null;
|
|
58
|
+
consumers: readonly ResourceConsumer[];
|
|
59
|
+
readonly lifecycle: {
|
|
60
|
+
readonly state: 'live' | 'destroyed' | 'unavailable';
|
|
61
|
+
readonly byteEstimate: ResourceByteEstimate | null;
|
|
62
|
+
};
|
|
56
63
|
}
|
|
57
64
|
|
|
58
|
-
export interface
|
|
59
|
-
readonly
|
|
60
|
-
readonly
|
|
61
|
-
readonly
|
|
62
|
-
readonly
|
|
63
|
-
readonly
|
|
64
|
-
readonly
|
|
65
|
+
export interface WorkBinding {
|
|
66
|
+
readonly groupIndex: number;
|
|
67
|
+
readonly binding: number;
|
|
68
|
+
readonly bindGroupId: string;
|
|
69
|
+
readonly resourceId: string | null;
|
|
70
|
+
readonly resourceKind: string | null;
|
|
71
|
+
readonly bufferOffset: number | null;
|
|
72
|
+
readonly bufferSize: number | null;
|
|
65
73
|
}
|
|
66
74
|
|
|
67
|
-
export interface
|
|
68
|
-
readonly
|
|
69
|
-
readonly
|
|
70
|
-
readonly
|
|
75
|
+
export interface WorkPipeline {
|
|
76
|
+
readonly status: 'available' | 'unavailable';
|
|
77
|
+
readonly pipelineHandleId?: string;
|
|
78
|
+
readonly kind?: 'render' | 'compute';
|
|
79
|
+
readonly descriptor?: JsonValue;
|
|
80
|
+
readonly shaders: readonly {
|
|
81
|
+
readonly stage: 'vertex' | 'fragment' | 'compute';
|
|
82
|
+
readonly moduleHandleId: string;
|
|
83
|
+
readonly entryPoint: string | null;
|
|
84
|
+
readonly source: string | null;
|
|
85
|
+
}[];
|
|
86
|
+
readonly reason?: 'pipeline-not-bound' | 'descriptor-unavailable';
|
|
71
87
|
}
|
|
72
88
|
|
|
73
|
-
export interface WorkEntry {
|
|
74
|
-
readonly
|
|
75
|
-
readonly
|
|
76
|
-
readonly
|
|
77
|
-
readonly
|
|
89
|
+
export interface WorkEntry extends TapeWorkEntry {
|
|
90
|
+
readonly commandIndex: number;
|
|
91
|
+
readonly drawCall: JsonValue;
|
|
92
|
+
readonly pipeline: WorkPipeline;
|
|
93
|
+
readonly bindings: readonly WorkBinding[];
|
|
94
|
+
readonly vertexBuffers: readonly {
|
|
95
|
+
readonly slot: number;
|
|
96
|
+
readonly bufferHandleId: string;
|
|
97
|
+
readonly offset: number;
|
|
98
|
+
readonly size: number | null;
|
|
99
|
+
}[];
|
|
100
|
+
readonly indexBuffer: {
|
|
101
|
+
readonly bufferHandleId: string;
|
|
102
|
+
readonly format: string;
|
|
103
|
+
readonly offset: number;
|
|
104
|
+
readonly size: number | null;
|
|
105
|
+
} | null;
|
|
106
|
+
readonly attachments: {
|
|
107
|
+
readonly colorViewHandleIds: readonly string[];
|
|
108
|
+
readonly depthStencilViewHandleId: string | null;
|
|
109
|
+
} | null;
|
|
78
110
|
}
|
|
79
111
|
|
|
80
112
|
export interface FrameModel {
|
|
81
|
-
readonly tree: readonly PassNode[];
|
|
82
|
-
readonly draws: readonly DrawEntry[];
|
|
83
|
-
readonly meta: FrameModelMeta;
|
|
84
113
|
readonly commands: readonly CommandEntry[];
|
|
85
|
-
readonly
|
|
114
|
+
readonly passes: readonly FramePass[];
|
|
115
|
+
readonly resources: readonly ResourceEntry[];
|
|
86
116
|
readonly resourceLifecycle: ResourceLifecycleSummary;
|
|
87
117
|
readonly works: readonly WorkEntry[];
|
|
88
|
-
|
|
89
|
-
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function jsonValue(value: unknown): JsonValue {
|
|
121
|
+
if (
|
|
122
|
+
value === null ||
|
|
123
|
+
typeof value === 'string' ||
|
|
124
|
+
typeof value === 'number' ||
|
|
125
|
+
typeof value === 'boolean'
|
|
126
|
+
)
|
|
127
|
+
return value;
|
|
128
|
+
if (value === undefined) return null;
|
|
129
|
+
if (Array.isArray(value)) return value.map(jsonValue);
|
|
130
|
+
if (ArrayBuffer.isView(value))
|
|
131
|
+
return Array.from(value as unknown as ArrayLike<unknown>, jsonValue);
|
|
132
|
+
if (typeof value === 'object') {
|
|
133
|
+
const output: Record<string, JsonValue> = {};
|
|
134
|
+
for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
|
|
135
|
+
if (child !== undefined) output[key] = jsonValue(child);
|
|
136
|
+
}
|
|
137
|
+
return output;
|
|
138
|
+
}
|
|
139
|
+
return String(value);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function eventDescriptor(event: RhiCallEvent): JsonValue {
|
|
143
|
+
return jsonValue(event);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function workPipeline(
|
|
147
|
+
pipelineId: string | undefined,
|
|
148
|
+
pipelineEvents: ReadonlyMap<string, RhiCallEvent>,
|
|
149
|
+
shaderEvents: ReadonlyMap<string, RhiCallEvent>,
|
|
150
|
+
): WorkPipeline {
|
|
151
|
+
if (pipelineId === undefined)
|
|
152
|
+
return { status: 'unavailable', shaders: [], reason: 'pipeline-not-bound' };
|
|
153
|
+
const event = pipelineEvents.get(pipelineId);
|
|
154
|
+
if (event === undefined)
|
|
155
|
+
return { status: 'unavailable', shaders: [], reason: 'descriptor-unavailable' };
|
|
156
|
+
const shaders: WorkPipeline['shaders'][number][] = [];
|
|
157
|
+
const shaderRefs =
|
|
158
|
+
event.kind === 'createRenderPipeline'
|
|
159
|
+
? [
|
|
160
|
+
['vertex', event.vertexShaderModuleHandleId, event.desc.vertex?.entryPoint],
|
|
161
|
+
['fragment', event.fragmentShaderModuleHandleId, event.desc.fragment?.entryPoint],
|
|
162
|
+
]
|
|
163
|
+
: event.kind === 'createComputePipeline'
|
|
164
|
+
? [['compute', event.computeShaderModuleHandleId, event.desc.compute.entryPoint]]
|
|
165
|
+
: [];
|
|
166
|
+
for (const [stage, moduleId, entryPoint] of shaderRefs) {
|
|
167
|
+
if (typeof moduleId !== 'string') continue;
|
|
168
|
+
const shader = shaderEvents.get(moduleId);
|
|
169
|
+
shaders.push({
|
|
170
|
+
stage: stage as 'vertex' | 'fragment' | 'compute',
|
|
171
|
+
moduleHandleId: moduleId,
|
|
172
|
+
entryPoint: typeof entryPoint === 'string' ? entryPoint : null,
|
|
173
|
+
source: shader?.kind === 'createShaderModule' ? shader.wgslCode : null,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
status: 'available',
|
|
178
|
+
pipelineHandleId: pipelineId,
|
|
179
|
+
kind: event.kind === 'createRenderPipeline' ? 'render' : 'compute',
|
|
180
|
+
descriptor: eventDescriptor(event),
|
|
181
|
+
shaders,
|
|
182
|
+
};
|
|
90
183
|
}
|
|
91
184
|
|
|
92
185
|
export function buildFrameModel(tape: V7Tape): FrameModel {
|
|
93
186
|
const index = buildTapeIndex(tape);
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
187
|
+
const events = tape.events;
|
|
188
|
+
const passCommandIndices = index.passes.map(() => [] as number[]);
|
|
189
|
+
const commands: CommandEntry[] = [];
|
|
190
|
+
const groupPath: string[] = [];
|
|
191
|
+
const currentPassByEvent = index.passIndexByEvent;
|
|
192
|
+
const passAttachmentByIndex = new Map<number, FramePass>();
|
|
193
|
+
|
|
194
|
+
for (const pass of index.passes) {
|
|
195
|
+
passAttachmentByIndex.set(pass.passIndex, {
|
|
196
|
+
passIndex: pass.passIndex,
|
|
197
|
+
kind: pass.kind,
|
|
198
|
+
beginEventIndex: pass.beginEventIndex,
|
|
199
|
+
endEventIndex: pass.endEventIndex ?? null,
|
|
200
|
+
workIndices: pass.workIndices,
|
|
201
|
+
commandIndices: [],
|
|
202
|
+
colorAttachmentViewHandleIds: [],
|
|
203
|
+
depthStencilViewHandleId: null,
|
|
204
|
+
});
|
|
205
|
+
const begin = events[pass.beginEventIndex];
|
|
206
|
+
if (begin?.kind === 'beginRenderPass') {
|
|
207
|
+
const attachment = passAttachmentByIndex.get(pass.passIndex);
|
|
208
|
+
if (attachment !== undefined) {
|
|
209
|
+
attachment.colorAttachmentViewHandleIds = begin.colorAttachmentViewHandleIds.filter(
|
|
210
|
+
(id): id is string => typeof id === 'string',
|
|
211
|
+
);
|
|
212
|
+
attachment.depthStencilViewHandleId = begin.depthStencilViewHandleId ?? null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (const [eventIndex, event] of events.entries()) {
|
|
218
|
+
const passIndex = currentPassByEvent[eventIndex] ?? -1;
|
|
219
|
+
const isPush = event.kind === 'pushDebugGroup' || event.kind === 'passPushDebugGroup';
|
|
220
|
+
const isPop = event.kind === 'popDebugGroup' || event.kind === 'passPopDebugGroup';
|
|
221
|
+
const group = isPush ? [...groupPath, event.groupLabel] : [...groupPath];
|
|
222
|
+
const marker =
|
|
223
|
+
event.kind === 'insertDebugMarker' || event.kind === 'passInsertDebugMarker'
|
|
224
|
+
? event.markerLabel
|
|
225
|
+
: undefined;
|
|
226
|
+
const commandIndex = commands.length;
|
|
227
|
+
commands.push({
|
|
228
|
+
eventIndex,
|
|
229
|
+
passIndex,
|
|
107
230
|
kind: event.kind,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
231
|
+
category: EVENT_SEMANTICS[event.kind].category,
|
|
232
|
+
isWork: isWorkEvent(event.kind),
|
|
233
|
+
params: eventDescriptor(event),
|
|
234
|
+
group,
|
|
235
|
+
...(marker === undefined ? {} : { marker }),
|
|
236
|
+
});
|
|
237
|
+
if (passIndex >= 0) passCommandIndices[passIndex]?.push(commandIndex);
|
|
238
|
+
if (isPush) groupPath.push(event.groupLabel);
|
|
239
|
+
if (isPop) groupPath.pop();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const pipelineEvents = new Map<string, RhiCallEvent>();
|
|
243
|
+
const shaderEvents = new Map<string, RhiCallEvent>();
|
|
244
|
+
const bindGroups = new Map<string, Extract<RhiCallEvent, { kind: 'createBindGroup' }>>();
|
|
245
|
+
const resourceRecords = new Map<string, ResourceEntry>();
|
|
246
|
+
const workByEvent = new Map<number, number>();
|
|
247
|
+
for (const work of index.works) workByEvent.set(work.eventIndex, work.workIndex);
|
|
248
|
+
|
|
249
|
+
for (const bootstrap of tape.bootstrap) {
|
|
250
|
+
// Strict v7 decoding validates each bootstrap create record against the same
|
|
251
|
+
// RhiCallEvent union before FrameModel construction.
|
|
252
|
+
const create = bootstrap.create as unknown as RhiCallEvent;
|
|
253
|
+
if (create.kind === 'createRenderPipeline' || create.kind === 'createComputePipeline')
|
|
254
|
+
pipelineEvents.set(create.handleId, create);
|
|
255
|
+
if (create.kind === 'createShaderModule') shaderEvents.set(create.handleId, create);
|
|
256
|
+
if (create.kind === 'createBindGroup') bindGroups.set(create.handleId, create);
|
|
257
|
+
resourceRecords.set(bootstrap.handleId, {
|
|
258
|
+
resourceId: bootstrap.handleId,
|
|
259
|
+
kind: bootstrap.kind,
|
|
260
|
+
origin: 'bootstrap',
|
|
261
|
+
createEventIndex: null,
|
|
262
|
+
destroyEventIndex: null,
|
|
263
|
+
descriptor: jsonValue(bootstrap.create),
|
|
264
|
+
consumers: [],
|
|
265
|
+
lifecycle: { state: 'unavailable', byteEstimate: null },
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
for (const [eventIndex, event] of events.entries()) {
|
|
269
|
+
if (event.kind === 'createRenderPipeline' || event.kind === 'createComputePipeline')
|
|
270
|
+
pipelineEvents.set(event.handleId, event);
|
|
271
|
+
if (event.kind === 'createShaderModule') shaderEvents.set(event.handleId, event);
|
|
272
|
+
if (event.kind === 'createBindGroup') bindGroups.set(event.handleId, event);
|
|
273
|
+
const kind = resourceKindForEvent(event.kind);
|
|
274
|
+
const handleId =
|
|
275
|
+
kind === undefined
|
|
276
|
+
? undefined
|
|
277
|
+
: event.kind === 'createTextureView'
|
|
278
|
+
? event.resultHandleId
|
|
279
|
+
: event.kind === 'createCommandEncoder'
|
|
280
|
+
? event.cmdHandleId
|
|
281
|
+
: 'handleId' in event
|
|
282
|
+
? event.handleId
|
|
283
|
+
: undefined;
|
|
284
|
+
if (kind !== undefined && handleId !== undefined) {
|
|
285
|
+
resourceRecords.set(handleId, {
|
|
286
|
+
resourceId: handleId,
|
|
287
|
+
kind,
|
|
288
|
+
origin: 'frame',
|
|
289
|
+
createEventIndex: eventIndex,
|
|
290
|
+
destroyEventIndex: null,
|
|
291
|
+
descriptor: eventDescriptor(event),
|
|
292
|
+
consumers: [],
|
|
293
|
+
lifecycle: { state: 'unavailable', byteEstimate: null },
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const lifecycle = buildResourceLifecycle(events);
|
|
299
|
+
for (const record of lifecycle.resources) {
|
|
300
|
+
const resource = resourceRecords.get(record.handleId);
|
|
301
|
+
if (resource === undefined) continue;
|
|
302
|
+
resourceRecords.set(record.handleId, {
|
|
303
|
+
...resource,
|
|
304
|
+
destroyEventIndex: record.destroyedEventIndex ?? null,
|
|
305
|
+
lifecycle: { state: record.state, byteEstimate: record.byteEstimate },
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
for (const [eventIndex, event] of events.entries()) {
|
|
309
|
+
const workIndex = workByEvent.get(eventIndex) ?? null;
|
|
310
|
+
for (const resourceId of EVENT_SEMANTICS[event.kind].read(event)) {
|
|
311
|
+
const resource = resourceRecords.get(resourceId);
|
|
312
|
+
if (resource !== undefined)
|
|
313
|
+
resource.consumers = [...resource.consumers, { eventIndex, workIndex, access: 'read' }];
|
|
314
|
+
}
|
|
315
|
+
for (const resourceId of EVENT_SEMANTICS[event.kind].written(event)) {
|
|
316
|
+
const resource = resourceRecords.get(resourceId);
|
|
317
|
+
if (resource !== undefined)
|
|
318
|
+
resource.consumers = [...resource.consumers, { eventIndex, workIndex, access: 'write' }];
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const works: WorkEntry[] = [];
|
|
323
|
+
const currentPipeline = new Map<string, string>();
|
|
324
|
+
const currentVertexBuffers = new Map<
|
|
325
|
+
string,
|
|
326
|
+
Map<number, { bufferHandleId: string; offset: number; size: number | null }>
|
|
327
|
+
>();
|
|
328
|
+
const currentIndexBuffers = new Map<string, WorkEntry['indexBuffer']>();
|
|
329
|
+
const currentBindGroups = new Map<string, Map<number, string>>();
|
|
330
|
+
const workByEventEntry = new Map(index.works.map((work) => [work.eventIndex, work]));
|
|
331
|
+
for (const [eventIndex, event] of events.entries()) {
|
|
332
|
+
if (event === undefined) continue;
|
|
333
|
+
const passHandleId = 'passHandleId' in event ? event.passHandleId : '';
|
|
334
|
+
if (event.kind === 'setPipeline' || event.kind === 'setComputePipeline')
|
|
335
|
+
currentPipeline.set(passHandleId, event.pipelineHandleId);
|
|
336
|
+
if (event.kind === 'setVertexBuffer') {
|
|
337
|
+
const buffers = currentVertexBuffers.get(passHandleId) ?? new Map();
|
|
338
|
+
buffers.set(event.slot, {
|
|
339
|
+
bufferHandleId: event.bufferHandleId,
|
|
340
|
+
offset: event.offset ?? 0,
|
|
341
|
+
size: event.size ?? null,
|
|
342
|
+
});
|
|
343
|
+
currentVertexBuffers.set(passHandleId, buffers);
|
|
344
|
+
}
|
|
345
|
+
if (event.kind === 'setIndexBuffer')
|
|
346
|
+
currentIndexBuffers.set(passHandleId, {
|
|
347
|
+
bufferHandleId: event.bufferHandleId,
|
|
348
|
+
format: event.format,
|
|
349
|
+
offset: event.offset ?? 0,
|
|
350
|
+
size: event.size ?? null,
|
|
351
|
+
});
|
|
352
|
+
if (event.kind === 'setBindGroup') {
|
|
353
|
+
const groups = currentBindGroups.get(passHandleId) ?? new Map();
|
|
354
|
+
groups.set(event.index, event.bindGroupHandleId);
|
|
355
|
+
currentBindGroups.set(passHandleId, groups);
|
|
356
|
+
}
|
|
357
|
+
if (!isWorkEvent(event.kind)) continue;
|
|
358
|
+
const work = workByEventEntry.get(eventIndex);
|
|
359
|
+
if (work === undefined) continue;
|
|
360
|
+
const pipelineId = currentPipeline.get(passHandleId);
|
|
361
|
+
const groups = currentBindGroups.get(passHandleId) ?? new Map();
|
|
362
|
+
const bindings: WorkBinding[] = [];
|
|
363
|
+
for (const [groupIndex, bindGroupId] of groups) {
|
|
364
|
+
const bindGroup = bindGroups.get(bindGroupId);
|
|
365
|
+
for (const [entryIndex, resourceId] of bindGroup?.resourceHandleIds.entries() ?? []) {
|
|
366
|
+
const entry = bindGroup?.entries[entryIndex];
|
|
367
|
+
bindings.push({
|
|
368
|
+
groupIndex,
|
|
369
|
+
binding: entry?.binding ?? entryIndex,
|
|
370
|
+
bindGroupId,
|
|
371
|
+
resourceId: resourceId ?? null,
|
|
372
|
+
resourceKind: entry?.resourceKind ?? null,
|
|
373
|
+
bufferOffset: entry?.bufferOffset ?? null,
|
|
374
|
+
bufferSize: entry?.bufferSize ?? null,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const attachment = passAttachmentByIndex.get(work.passIndex);
|
|
379
|
+
works.push({
|
|
380
|
+
...work,
|
|
381
|
+
commandIndex: commands.findIndex((command) => command.eventIndex === work.eventIndex),
|
|
382
|
+
drawCall: eventDescriptor(event),
|
|
383
|
+
pipeline: workPipeline(pipelineId, pipelineEvents, shaderEvents),
|
|
384
|
+
bindings,
|
|
385
|
+
vertexBuffers: [...(currentVertexBuffers.get(passHandleId)?.entries() ?? [])].map(
|
|
386
|
+
([slot, buffer]) => ({ slot, ...buffer }),
|
|
387
|
+
),
|
|
388
|
+
indexBuffer: currentIndexBuffers.get(passHandleId) ?? null,
|
|
389
|
+
attachments:
|
|
390
|
+
attachment === undefined
|
|
391
|
+
? null
|
|
392
|
+
: {
|
|
393
|
+
colorViewHandleIds: attachment.colorAttachmentViewHandleIds,
|
|
394
|
+
depthStencilViewHandleId: attachment.depthStencilViewHandleId,
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const passes = index.passes.map((pass) => ({
|
|
400
|
+
...pass,
|
|
401
|
+
endEventIndex: pass.endEventIndex ?? null,
|
|
402
|
+
commandIndices: passCommandIndices[pass.passIndex] ?? [],
|
|
403
|
+
colorAttachmentViewHandleIds:
|
|
404
|
+
passAttachmentByIndex.get(pass.passIndex)?.colorAttachmentViewHandleIds ?? [],
|
|
405
|
+
depthStencilViewHandleId:
|
|
406
|
+
passAttachmentByIndex.get(pass.passIndex)?.depthStencilViewHandleId ?? null,
|
|
407
|
+
}));
|
|
408
|
+
return {
|
|
409
|
+
commands,
|
|
410
|
+
passes,
|
|
411
|
+
resources: [...resourceRecords.values()],
|
|
412
|
+
resourceLifecycle: lifecycle,
|
|
113
413
|
works,
|
|
114
|
-
passes: index.passes,
|
|
115
|
-
resourceEntries: index.resources,
|
|
116
414
|
};
|
|
117
415
|
}
|
|
118
416
|
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @forgeax/engine-rhi-debug/src/index.ts -- v7 public contract.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// fresh
|
|
3
|
+
// AI cold-start path: one .rhitape ArtifactRef, one FrameModel workIndex, and one
|
|
4
|
+
// fresh ReplaySession. Consumers should use structured Result errors and keep
|
|
5
5
|
// browser, Node, and backend ownership at their host boundaries.
|
|
6
6
|
|
|
7
7
|
export {
|
|
@@ -13,17 +13,23 @@ export {
|
|
|
13
13
|
} from './errors';
|
|
14
14
|
export type {
|
|
15
15
|
CommandEntry,
|
|
16
|
-
CreateDescriptor,
|
|
17
|
-
DrawDepthStencil,
|
|
18
|
-
DrawEntry,
|
|
19
|
-
DrawPipelineState,
|
|
20
16
|
FrameModel,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
FramePass,
|
|
18
|
+
JsonValue,
|
|
19
|
+
ResourceConsumer,
|
|
20
|
+
ResourceEntry,
|
|
21
|
+
WorkBinding,
|
|
24
22
|
WorkEntry,
|
|
23
|
+
WorkPipeline,
|
|
24
|
+
} from './frame-model';
|
|
25
|
+
export {
|
|
26
|
+
buildFrameModel,
|
|
27
|
+
buildResourceLifecycle,
|
|
28
|
+
type ResourceByteEstimate,
|
|
29
|
+
type ResourceKind,
|
|
30
|
+
type ResourceLifecycleEntry,
|
|
31
|
+
type ResourceLifecycleSummary,
|
|
25
32
|
} from './frame-model';
|
|
26
|
-
export { buildFrameModel } from './frame-model';
|
|
27
33
|
export { decodeTape, encodeTape } from './protocol/codec';
|
|
28
34
|
export type { EventCategory, EventSemantics } from './protocol/event-semantics';
|
|
29
35
|
export {
|
|
@@ -66,6 +72,7 @@ export {
|
|
|
66
72
|
type RecorderBackend,
|
|
67
73
|
type RecorderOptions,
|
|
68
74
|
} from './recorder/session';
|
|
75
|
+
export { replayDeviceRequest } from './replay/device-request';
|
|
69
76
|
export {
|
|
70
77
|
type InspectField,
|
|
71
78
|
openReplay,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Tape } from '../protocol/types';
|
|
2
|
+
|
|
3
|
+
const RECORDED_CAPABILITY_FEATURES = [
|
|
4
|
+
['timestampQuery', 'timestamp-query'],
|
|
5
|
+
['textureCompressionBc', 'texture-compression-bc'],
|
|
6
|
+
['textureCompressionEtc2', 'texture-compression-etc2'],
|
|
7
|
+
['textureCompressionAstc', 'texture-compression-astc'],
|
|
8
|
+
['firstInstanceIndirect', 'indirect-first-instance'],
|
|
9
|
+
['float32Filterable', 'float32-filterable'],
|
|
10
|
+
['rg11b10ufloatRenderable', 'rg11b10ufloat-renderable'],
|
|
11
|
+
] as const satisfies readonly (readonly [string, GPUFeatureName])[];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build the strongest fresh WebGPU device the current adapter can provide for
|
|
15
|
+
* a recorded tape. Optional features must be enabled, not merely advertised,
|
|
16
|
+
* and replay receives concrete adapter limits instead of lower device defaults.
|
|
17
|
+
*/
|
|
18
|
+
export function replayDeviceRequest(
|
|
19
|
+
tape: Tape,
|
|
20
|
+
adapterFeatures: ReadonlySet<GPUFeatureName>,
|
|
21
|
+
adapterLimits: Readonly<Record<string, number>>,
|
|
22
|
+
): GPUDeviceDescriptor {
|
|
23
|
+
const requiredFeatures = RECORDED_CAPABILITY_FEATURES.filter(
|
|
24
|
+
([capability, feature]) =>
|
|
25
|
+
tape.header.rhiCaps[capability] === true && adapterFeatures.has(feature),
|
|
26
|
+
).map(([, feature]) => feature);
|
|
27
|
+
const limitEntries = Object.entries(adapterLimits).filter(
|
|
28
|
+
([, value]) => Number.isFinite(value) && value >= 0,
|
|
29
|
+
);
|
|
30
|
+
const request: GPUDeviceDescriptor = {};
|
|
31
|
+
if (requiredFeatures.length > 0) request.requiredFeatures = requiredFeatures;
|
|
32
|
+
if (limitEntries.length > 0) {
|
|
33
|
+
request.requiredLimits = Object.fromEntries(limitEntries) as NonNullable<
|
|
34
|
+
GPUDeviceDescriptor['requiredLimits']
|
|
35
|
+
>;
|
|
36
|
+
}
|
|
37
|
+
return request;
|
|
38
|
+
}
|