@forgeax/engine-rhi-debug 0.1.4 → 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 +4651 -46
- package/dist/index.mjs.map +1 -1
- package/dist/recorder/device.d.ts.map +1 -1
- package/dist/recorder/encoder.d.ts.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/dist/types.d.ts +24 -6
- package/dist/types.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/recorder/device.ts +20 -2
- package/src/recorder/encoder.ts +22 -5
- 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/types.ts +30 -6
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { RhiInstance } from '@forgeax/engine-rhi';
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import type { BootstrapResource, Tape } from '../protocol/types';
|
|
3
|
+
import type { BootstrapResource, Tape, TapeBlob } from '../protocol/types';
|
|
4
4
|
import type { ReplayBackend } from '../replay/session';
|
|
5
5
|
import { openReplay } from '../replay/session';
|
|
6
|
+
import { READBACK_MATRIX_CASES } from './readback-matrix-fixture';
|
|
6
7
|
|
|
7
8
|
interface DawnPack {
|
|
8
9
|
readonly rhi: RhiInstance;
|
|
@@ -37,7 +38,7 @@ function colorResource(): BootstrapResource {
|
|
|
37
38
|
|
|
38
39
|
function depthResource(
|
|
39
40
|
handleId: string,
|
|
40
|
-
format: 'depth24plus' | 'depth24plus-stencil8',
|
|
41
|
+
format: 'depth24plus' | 'depth24plus-stencil8' | 'depth32float',
|
|
41
42
|
): BootstrapResource {
|
|
42
43
|
return {
|
|
43
44
|
handleId,
|
|
@@ -58,7 +59,47 @@ function depthResource(
|
|
|
58
59
|
};
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
function matrixTextureResource(): BootstrapResource {
|
|
63
|
+
return {
|
|
64
|
+
handleId: 'texture:matrix',
|
|
65
|
+
kind: 'texture',
|
|
66
|
+
create: {
|
|
67
|
+
kind: 'createTexture',
|
|
68
|
+
handleId: 'texture:matrix',
|
|
69
|
+
desc: {
|
|
70
|
+
size: { width: 2, height: 2, depthOrArrayLayers: 12 },
|
|
71
|
+
format: 'rgba8unorm',
|
|
72
|
+
dimension: '2d',
|
|
73
|
+
mipLevelCount: 2,
|
|
74
|
+
sampleCount: 1,
|
|
75
|
+
usage: 0x1f,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
initialData: [{ hash: 'matrix-texture', byteOffset: 0, byteLength: 240 }],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function matrixBlob(): TapeBlob {
|
|
83
|
+
return {
|
|
84
|
+
hash: 'matrix-texture',
|
|
85
|
+
bytes: Uint8Array.from({ length: 240 }, (_, index) => index),
|
|
86
|
+
compression: 'none',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
61
90
|
describe.skipIf(SKIP_DAWN)('Dawn replay-owned readback matrix', () => {
|
|
91
|
+
it('enumerates the canonical descriptor and subresource matrix', () => {
|
|
92
|
+
expect(READBACK_MATRIX_CASES).toEqual([
|
|
93
|
+
{ format: 'rgba8unorm', dimension: '2d', aspect: 'all' },
|
|
94
|
+
{ format: 'depth32float', dimension: '2d', aspect: 'depth-only' },
|
|
95
|
+
{ format: 'depth24plus', dimension: '2d', aspect: 'depth-only' },
|
|
96
|
+
{ format: 'depth24plus-stencil8', dimension: '2d', aspect: 'stencil-only' },
|
|
97
|
+
{ format: 'rgba8unorm', dimension: '2d-array', aspect: 'all' },
|
|
98
|
+
{ format: 'rgba8unorm', dimension: 'cube', aspect: 'all' },
|
|
99
|
+
{ format: 'rgba8unorm', dimension: 'cube-array', aspect: 'all' },
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
|
|
62
103
|
it('reads a color subresource on a fresh Dawn device', async () => {
|
|
63
104
|
const pack = await loadDawn();
|
|
64
105
|
const adapter = await pack.rhi.requestAdapter();
|
|
@@ -88,6 +129,164 @@ describe.skipIf(SKIP_DAWN)('Dawn replay-owned readback matrix', () => {
|
|
|
88
129
|
expect(pixels.ok).toBe(true);
|
|
89
130
|
if (!pixels.ok) throw new Error(pixels.error.hint);
|
|
90
131
|
expect(pixels.value.bytes.byteLength).toBe(4 * 4 * 4);
|
|
132
|
+
expect(pixels.value.provenance).toEqual({
|
|
133
|
+
generation: replay.value.generation,
|
|
134
|
+
resourceId: 'texture:color',
|
|
135
|
+
subresource: { mipLevel: 0, arrayLayer: 0, aspect: 'all' },
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('reads a known buffer range through the replay owner', async () => {
|
|
140
|
+
const pack = await loadDawn();
|
|
141
|
+
const adapter = await pack.rhi.requestAdapter();
|
|
142
|
+
expect(adapter.ok).toBe(true);
|
|
143
|
+
if (!adapter.ok) throw new Error(`Dawn admission failed: ${adapter.error.code}`);
|
|
144
|
+
const device = await adapter.value.requestDevice();
|
|
145
|
+
expect(device.ok).toBe(true);
|
|
146
|
+
if (!device.ok) throw new Error(`Dawn device admission failed: ${device.error.code}`);
|
|
147
|
+
const replay = await openReplay(
|
|
148
|
+
{
|
|
149
|
+
header: { formatVersion: 7, rhiCaps: {}, eventCount: 0, blobCount: 1 },
|
|
150
|
+
bootstrap: [
|
|
151
|
+
{
|
|
152
|
+
handleId: 'buffer:known',
|
|
153
|
+
kind: 'buffer',
|
|
154
|
+
create: {
|
|
155
|
+
kind: 'createBuffer',
|
|
156
|
+
handleId: 'buffer:known',
|
|
157
|
+
desc: { size: 8, usage: 0x0c },
|
|
158
|
+
},
|
|
159
|
+
initialData: [{ hash: 'buffer-known', byteOffset: 0, byteLength: 8 }],
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
events: [],
|
|
163
|
+
blobs: [
|
|
164
|
+
{
|
|
165
|
+
hash: 'buffer-known',
|
|
166
|
+
bytes: Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7]),
|
|
167
|
+
compression: 'none',
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
{ device: device.value, createShaderModule: pack.createShaderModule },
|
|
172
|
+
);
|
|
173
|
+
expect(replay.ok).toBe(true);
|
|
174
|
+
if (!replay.ok) throw new Error(replay.error.hint);
|
|
175
|
+
const result = await replay.value.readResource('buffer:known', { offset: 2, size: 4 });
|
|
176
|
+
expect(result.ok).toBe(true);
|
|
177
|
+
if (!result.ok) throw new Error(result.error.hint);
|
|
178
|
+
expect([...result.value.bytes]).toEqual([2, 3, 4, 5]);
|
|
179
|
+
expect(result.value.provenance.subresource).toEqual({ offset: 2, size: 4 });
|
|
180
|
+
await replay.value.dispose();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('reads array, cube, cube-array, and view-local mip/layer subresources', async () => {
|
|
184
|
+
const pack = await loadDawn();
|
|
185
|
+
const adapter = await pack.rhi.requestAdapter();
|
|
186
|
+
expect(adapter.ok).toBe(true);
|
|
187
|
+
if (!adapter.ok) throw new Error(`Dawn admission failed: ${adapter.error.code}`);
|
|
188
|
+
const device = await adapter.value.requestDevice();
|
|
189
|
+
expect(device.ok).toBe(true);
|
|
190
|
+
if (!device.ok) throw new Error(`Dawn device admission failed: ${device.error.code}`);
|
|
191
|
+
const view = (handleId: string, dimension: string, baseArrayLayer: number, count: number) => ({
|
|
192
|
+
handleId,
|
|
193
|
+
kind: 'texture-view' as const,
|
|
194
|
+
create: {
|
|
195
|
+
kind: 'createTextureView',
|
|
196
|
+
sourceHandleId: 'texture:matrix',
|
|
197
|
+
resultHandleId: handleId,
|
|
198
|
+
desc: {
|
|
199
|
+
dimension,
|
|
200
|
+
baseMipLevel: 1,
|
|
201
|
+
mipLevelCount: 1,
|
|
202
|
+
baseArrayLayer,
|
|
203
|
+
arrayLayerCount: count,
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
initialData: [],
|
|
207
|
+
});
|
|
208
|
+
const replay = await openReplay(
|
|
209
|
+
{
|
|
210
|
+
header: { formatVersion: 7, rhiCaps: {}, eventCount: 0, blobCount: 1 },
|
|
211
|
+
bootstrap: [
|
|
212
|
+
matrixTextureResource(),
|
|
213
|
+
view('view:array', '2d-array', 4, 2),
|
|
214
|
+
view('view:cube', 'cube', 0, 6),
|
|
215
|
+
view('view:cube-array', 'cube-array', 0, 12),
|
|
216
|
+
],
|
|
217
|
+
events: [],
|
|
218
|
+
blobs: [matrixBlob()],
|
|
219
|
+
},
|
|
220
|
+
{ device: device.value, createShaderModule: pack.createShaderModule },
|
|
221
|
+
);
|
|
222
|
+
expect(replay.ok).toBe(true);
|
|
223
|
+
if (!replay.ok) throw new Error(replay.error.hint);
|
|
224
|
+
const arrayResult = await replay.value.readResource('view:array', {
|
|
225
|
+
mipLevel: 0,
|
|
226
|
+
arrayLayer: 0,
|
|
227
|
+
aspect: 'all',
|
|
228
|
+
});
|
|
229
|
+
expect(arrayResult.ok).toBe(true);
|
|
230
|
+
if (!arrayResult.ok) throw new Error(arrayResult.error.hint);
|
|
231
|
+
expect(arrayResult.value.width).toBe(1);
|
|
232
|
+
expect(arrayResult.value.height).toBe(1);
|
|
233
|
+
expect([...arrayResult.value.bytes]).toEqual([96, 97, 98, 99]);
|
|
234
|
+
const cubeResult = await replay.value.readResource('view:cube', {
|
|
235
|
+
mipLevel: 0,
|
|
236
|
+
arrayLayer: 5,
|
|
237
|
+
aspect: 'all',
|
|
238
|
+
});
|
|
239
|
+
expect(cubeResult.ok).toBe(true);
|
|
240
|
+
if (!cubeResult.ok) throw new Error(cubeResult.error.hint);
|
|
241
|
+
expect(cubeResult.value.provenance.subresource).toEqual({
|
|
242
|
+
mipLevel: 0,
|
|
243
|
+
arrayLayer: 5,
|
|
244
|
+
aspect: 'all',
|
|
245
|
+
});
|
|
246
|
+
const cubeArrayResult = await replay.value.readResource('view:cube-array', {
|
|
247
|
+
mipLevel: 0,
|
|
248
|
+
arrayLayer: 11,
|
|
249
|
+
aspect: 'all',
|
|
250
|
+
});
|
|
251
|
+
expect(cubeArrayResult.ok).toBe(true);
|
|
252
|
+
if (!cubeArrayResult.ok) throw new Error(cubeArrayResult.error.hint);
|
|
253
|
+
expect([...cubeArrayResult.value.bytes]).toEqual([236, 237, 238, 239]);
|
|
254
|
+
await replay.value.dispose();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('reads depth32float through the direct depth aspect path', async () => {
|
|
258
|
+
const pack = await loadDawn();
|
|
259
|
+
const adapter = await pack.rhi.requestAdapter();
|
|
260
|
+
expect(adapter.ok).toBe(true);
|
|
261
|
+
if (!adapter.ok) throw new Error(`Dawn admission failed: ${adapter.error.code}`);
|
|
262
|
+
const device = await adapter.value.requestDevice();
|
|
263
|
+
expect(device.ok).toBe(true);
|
|
264
|
+
if (!device.ok) throw new Error(`Dawn device admission failed: ${device.error.code}`);
|
|
265
|
+
const replay = await openReplay(
|
|
266
|
+
{
|
|
267
|
+
header: { formatVersion: 7, rhiCaps: {}, eventCount: 0, blobCount: 0 },
|
|
268
|
+
bootstrap: [depthResource('texture:depth32', 'depth32float')],
|
|
269
|
+
events: [],
|
|
270
|
+
blobs: [],
|
|
271
|
+
},
|
|
272
|
+
{ device: device.value, createShaderModule: pack.createShaderModule },
|
|
273
|
+
);
|
|
274
|
+
expect(replay.ok).toBe(true);
|
|
275
|
+
if (!replay.ok) throw new Error(replay.error.hint);
|
|
276
|
+
const result = await replay.value.readResource('texture:depth32', {
|
|
277
|
+
mipLevel: 0,
|
|
278
|
+
arrayLayer: 0,
|
|
279
|
+
aspect: 'depth-only',
|
|
280
|
+
});
|
|
281
|
+
expect(result.ok).toBe(true);
|
|
282
|
+
if (!result.ok) throw new Error(result.error.hint);
|
|
283
|
+
expect(result.value.bytes.byteLength).toBe(4 * 4 * 4);
|
|
284
|
+
expect(result.value.provenance.subresource).toEqual({
|
|
285
|
+
mipLevel: 0,
|
|
286
|
+
arrayLayer: 0,
|
|
287
|
+
aspect: 'depth-only',
|
|
288
|
+
});
|
|
289
|
+
await replay.value.dispose();
|
|
91
290
|
});
|
|
92
291
|
|
|
93
292
|
it('reads depth24plus through the replay-owned depth-to-color blit', async () => {
|
|
@@ -119,10 +318,16 @@ describe.skipIf(SKIP_DAWN)('Dawn replay-owned readback matrix', () => {
|
|
|
119
318
|
if (!pixels.ok) throw new Error(pixels.error.hint);
|
|
120
319
|
expect(pixels.value.bytes.byteLength).toBe(4 * 4 * 4);
|
|
121
320
|
expect([...new Float32Array(pixels.value.bytes.buffer)]).toHaveLength(16);
|
|
321
|
+
expect(pixels.value.provenance.resourceId).toBe('texture:depth24plus');
|
|
322
|
+
expect(pixels.value.provenance.subresource).toEqual({
|
|
323
|
+
mipLevel: 0,
|
|
324
|
+
arrayLayer: 0,
|
|
325
|
+
aspect: 'depth-only',
|
|
326
|
+
});
|
|
122
327
|
await replay.value.dispose();
|
|
123
328
|
});
|
|
124
329
|
|
|
125
|
-
it('reads
|
|
330
|
+
it('reads both explicit planes on depth24plus-stencil8', async () => {
|
|
126
331
|
const pack = await loadDawn();
|
|
127
332
|
const adapter = await pack.rhi.requestAdapter();
|
|
128
333
|
expect(adapter.ok).toBe(true);
|
|
@@ -142,6 +347,19 @@ describe.skipIf(SKIP_DAWN)('Dawn replay-owned readback matrix', () => {
|
|
|
142
347
|
);
|
|
143
348
|
expect(replay.ok).toBe(true);
|
|
144
349
|
if (!replay.ok) throw new Error(replay.error.hint);
|
|
350
|
+
const depth = await replay.value.readResource('texture:stencil', {
|
|
351
|
+
mipLevel: 0,
|
|
352
|
+
arrayLayer: 0,
|
|
353
|
+
aspect: 'depth-only',
|
|
354
|
+
});
|
|
355
|
+
expect(depth.ok).toBe(true);
|
|
356
|
+
if (!depth.ok) throw new Error(depth.error.hint);
|
|
357
|
+
expect(depth.value.bytes.byteLength).toBe(4 * 4 * 4);
|
|
358
|
+
expect(depth.value.provenance.subresource).toEqual({
|
|
359
|
+
mipLevel: 0,
|
|
360
|
+
arrayLayer: 0,
|
|
361
|
+
aspect: 'depth-only',
|
|
362
|
+
});
|
|
145
363
|
const pixels = await replay.value.readResource('texture:stencil', {
|
|
146
364
|
mipLevel: 0,
|
|
147
365
|
arrayLayer: 0,
|
|
@@ -150,6 +368,11 @@ describe.skipIf(SKIP_DAWN)('Dawn replay-owned readback matrix', () => {
|
|
|
150
368
|
expect(pixels.ok).toBe(true);
|
|
151
369
|
if (!pixels.ok) throw new Error(pixels.error.hint);
|
|
152
370
|
expect(pixels.value.bytes.byteLength).toBe(4 * 4);
|
|
371
|
+
expect(pixels.value.provenance.subresource).toEqual({
|
|
372
|
+
mipLevel: 0,
|
|
373
|
+
arrayLayer: 0,
|
|
374
|
+
aspect: 'stencil-only',
|
|
375
|
+
});
|
|
153
376
|
await replay.value.dispose();
|
|
154
377
|
});
|
|
155
378
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createShaderModule, rhi } from '@forgeax/engine-rhi-null';
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
3
|
import type { BootstrapResource, Tape } from '../protocol/types';
|
|
4
|
+
import type { ReplayReadbackResult } from '../replay/readback';
|
|
4
5
|
import type { TextureSubresource } from '../replay/session';
|
|
5
6
|
import { openReplay } from '../replay/session';
|
|
6
7
|
import { getTextureReadbackPlan } from '../replay/texture-format';
|
|
@@ -49,10 +50,14 @@ async function nullBackend() {
|
|
|
49
50
|
|
|
50
51
|
describe('Replay readback format matrix', () => {
|
|
51
52
|
it.each([
|
|
53
|
+
['bgra8unorm', '2d'],
|
|
54
|
+
['depth24plus-stencil8', '2d'],
|
|
52
55
|
['rgba8unorm', '2d'],
|
|
56
|
+
['rgba8unorm-srgb', '2d'],
|
|
57
|
+
['rg16float', '2d'],
|
|
58
|
+
['rgba16float', '2d'],
|
|
53
59
|
['depth32float', '2d'],
|
|
54
60
|
['depth24plus', '2d'],
|
|
55
|
-
['depth24plus-stencil8', '2d-array'],
|
|
56
61
|
['rgba8unorm', 'cube'],
|
|
57
62
|
])('keeps %s/%s in the supported matrix', (format, dimension) => {
|
|
58
63
|
const plan = getTextureReadbackPlan({ format, dimension });
|
|
@@ -93,4 +98,23 @@ describe('Replay readback format matrix', () => {
|
|
|
93
98
|
};
|
|
94
99
|
expect(subresource).toEqual({ mipLevel: 1, arrayLayer: 2, aspect: 'stencil-only' });
|
|
95
100
|
});
|
|
101
|
+
|
|
102
|
+
it('requires readback results to preserve resource and subresource provenance', () => {
|
|
103
|
+
const provenance: ReplayReadbackResult['provenance'] = {
|
|
104
|
+
generation: 1,
|
|
105
|
+
resourceId: 'texture:color',
|
|
106
|
+
subresource: {
|
|
107
|
+
mipLevel: 0,
|
|
108
|
+
arrayLayer: 0,
|
|
109
|
+
aspect: 'all',
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
expect(provenance.resourceId).toBe('texture:color');
|
|
113
|
+
expect(provenance.generation).toBeGreaterThanOrEqual(0);
|
|
114
|
+
expect(provenance.subresource).toEqual({
|
|
115
|
+
mipLevel: 0,
|
|
116
|
+
arrayLayer: 0,
|
|
117
|
+
aspect: 'all',
|
|
118
|
+
});
|
|
119
|
+
});
|
|
96
120
|
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { BootstrapResource, Tape } from '../protocol/types';
|
|
2
|
+
|
|
3
|
+
export const READBACK_MATRIX_CASES = [
|
|
4
|
+
{ format: 'rgba8unorm', dimension: '2d', aspect: 'all' },
|
|
5
|
+
{ format: 'depth32float', dimension: '2d', aspect: 'depth-only' },
|
|
6
|
+
{ format: 'depth24plus', dimension: '2d', aspect: 'depth-only' },
|
|
7
|
+
{ format: 'depth24plus-stencil8', dimension: '2d', aspect: 'stencil-only' },
|
|
8
|
+
{ format: 'rgba8unorm', dimension: '2d-array', aspect: 'all' },
|
|
9
|
+
{ format: 'rgba8unorm', dimension: 'cube', aspect: 'all' },
|
|
10
|
+
{ format: 'rgba8unorm', dimension: 'cube-array', aspect: 'all' },
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
export function makeReadbackMatrixTape(resources: readonly BootstrapResource[]): Tape {
|
|
14
|
+
return {
|
|
15
|
+
header: { formatVersion: 7, rhiCaps: {}, eventCount: 0, blobCount: 0 },
|
|
16
|
+
bootstrap: resources,
|
|
17
|
+
events: [],
|
|
18
|
+
blobs: [],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -140,4 +140,16 @@ describe('ReplaySession fail-closed executor', () => {
|
|
|
140
140
|
expect(inspection.error.detail?.stage).toBe('write');
|
|
141
141
|
}
|
|
142
142
|
});
|
|
143
|
+
|
|
144
|
+
it('rejects stale generation reads after a new inspection reset', async () => {
|
|
145
|
+
const result = await openReplay(tape(), { device: await device(), createShaderModule });
|
|
146
|
+
expect(result.ok).toBe(true);
|
|
147
|
+
if (!result.ok) return;
|
|
148
|
+
const first = await result.value.inspectWork(0);
|
|
149
|
+
expect(first.ok).toBe(true);
|
|
150
|
+
const generation = result.value.generation;
|
|
151
|
+
expect(generation).toBeGreaterThan(0);
|
|
152
|
+
expect((await result.value.dispose()).ok).toBe(true);
|
|
153
|
+
expect((await result.value.dispose()).ok).toBe(true);
|
|
154
|
+
});
|
|
143
155
|
});
|
|
@@ -171,6 +171,9 @@ describe.skipIf(SKIP_DAWN)('ReplaySession Dawn contract', () => {
|
|
|
171
171
|
if (!baseline.ok) throw new Error(baseline.error.hint);
|
|
172
172
|
expect(baseline.value.attachment?.bytes.byteLength).toBe(WIDTH * HEIGHT * 4);
|
|
173
173
|
expect(baseline.value.attachment?.bytes.slice(0, 4)).toEqual(new Uint8Array([23, 23, 23, 23]));
|
|
174
|
+
expect(baseline.value.attachment?.provenance.resourceId).toBe('texture-view:rt');
|
|
175
|
+
expect(baseline.value.attachment?.provenance.selectedWorkIndex).toBe(0);
|
|
176
|
+
expect(baseline.value.attachment?.provenance.subresource).toBeNull();
|
|
174
177
|
|
|
175
178
|
const second = await openReplay(tape, {
|
|
176
179
|
device: await freshDevice(pack),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { RhiDevice } from '@forgeax/engine-rhi';
|
|
2
2
|
import { expectTypeOf } from 'vitest';
|
|
3
|
-
import type { ReplayBackend, ReplaySession } from '../replay/session';
|
|
3
|
+
import type { ReplayBackend, ReplaySession, WorkInspection } from '../replay/session';
|
|
4
4
|
|
|
5
5
|
declare const device: RhiDevice;
|
|
6
6
|
declare const session: ReplaySession;
|
|
@@ -15,6 +15,12 @@ const backend: ReplayBackend = {
|
|
|
15
15
|
expectTypeOf(backend.createShaderModule).toBeFunction();
|
|
16
16
|
expectTypeOf(session.inspectWork(0)).resolves.toHaveProperty('ok');
|
|
17
17
|
expectTypeOf(session.dispose()).resolves.toHaveProperty('ok');
|
|
18
|
+
expectTypeOf<WorkInspection['pipeline']>().not.toBeNever();
|
|
19
|
+
expectTypeOf<WorkInspection['bindings']>().toMatchTypeOf<readonly unknown[] | undefined>();
|
|
20
|
+
expectTypeOf<WorkInspection['vertexBuffers']>().toMatchTypeOf<readonly unknown[] | undefined>();
|
|
21
|
+
expectTypeOf<WorkInspection['indexBuffer']>().not.toBeNever();
|
|
22
|
+
expectTypeOf<WorkInspection['shaders']>().toMatchTypeOf<readonly unknown[] | undefined>();
|
|
23
|
+
expectTypeOf<WorkInspection['resourceIds']>().toEqualTypeOf<readonly string[] | undefined>();
|
|
18
24
|
|
|
19
25
|
// @ts-expect-error ReplayBackend must not be constructible without shader creation.
|
|
20
26
|
const missingFactory: ReplayBackend = { device };
|
|
@@ -116,6 +116,22 @@ describe('ReplaySession', () => {
|
|
|
116
116
|
expect('commitThroughDraw' in result.value).toBe(false);
|
|
117
117
|
});
|
|
118
118
|
|
|
119
|
+
it('returns selected-work facts for every inspection selector', async () => {
|
|
120
|
+
const result = await openReplay(makeWorkTape(), await makeBackend());
|
|
121
|
+
expect(result.ok).toBe(true);
|
|
122
|
+
if (!result.ok) return;
|
|
123
|
+
|
|
124
|
+
const inspection = await result.value.inspectWork(0, ['bindings', 'pipeline']);
|
|
125
|
+
expect(inspection.ok).toBe(true);
|
|
126
|
+
if (!inspection.ok) return;
|
|
127
|
+
expect(inspection.value.pipeline).toBeDefined();
|
|
128
|
+
expect(inspection.value.bindings).toBeDefined();
|
|
129
|
+
expect(inspection.value.vertexBuffers).toBeDefined();
|
|
130
|
+
expect(inspection.value.indexBuffer).toBeDefined();
|
|
131
|
+
expect(inspection.value.shaders).toBeDefined();
|
|
132
|
+
expect(inspection.value.resourceIds).toBeDefined();
|
|
133
|
+
});
|
|
134
|
+
|
|
119
135
|
it('resets internally when selecting an earlier work and rejects invalid positions', async () => {
|
|
120
136
|
const tape = makeWorkTape();
|
|
121
137
|
const withSecondWork: Tape = {
|
|
@@ -170,6 +170,8 @@ describe.skipIf(SKIP_DAWN)('RHI debug v7 fresh-device evidence', () => {
|
|
|
170
170
|
const replayPixels = inspection.value.attachment?.bytes;
|
|
171
171
|
expect(replayPixels).toBeDefined();
|
|
172
172
|
if (replayPixels === undefined) throw new Error('fresh replay did not return attachment bytes');
|
|
173
|
+
expect(inspection.value.attachment?.provenance.selectedWorkIndex).toBe(0);
|
|
174
|
+
expect(inspection.value.attachment?.provenance.resourceId).toBeDefined();
|
|
173
175
|
expect(normalizedPixelDelta(baseline, replayPixels)).toBeLessThanOrEqual(0.01);
|
|
174
176
|
expect((await session.value.dispose()).ok).toBe(true);
|
|
175
177
|
}, 60_000);
|
|
@@ -43,6 +43,9 @@ describe('TapeIndex', () => {
|
|
|
43
43
|
[2, 5, 1],
|
|
44
44
|
]);
|
|
45
45
|
expect(index.passes).toHaveLength(2);
|
|
46
|
+
expect(index.eventKinds).toContain('dispatchWorkgroups');
|
|
47
|
+
expect(index.passIndexByEvent[1]).toBe(0);
|
|
48
|
+
expect(index.passIndexByEvent[5]).toBe(1);
|
|
46
49
|
});
|
|
47
50
|
|
|
48
51
|
it('does not invent work for an empty pass', () => {
|
|
@@ -108,6 +108,30 @@ describe('browser entry isolation (AC-16)', () => {
|
|
|
108
108
|
});
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
describe('retained core dependency boundary (OOS-5)', () => {
|
|
112
|
+
it('does not include viewer or host dependency identifiers in core production sources', () => {
|
|
113
|
+
const sourceFiles = [
|
|
114
|
+
'index.ts',
|
|
115
|
+
'frame-model.ts',
|
|
116
|
+
'protocol/codec.ts',
|
|
117
|
+
'replay/session.ts',
|
|
118
|
+
].map((file) => path.resolve(__dirname, '..', file));
|
|
119
|
+
const forbidden = [
|
|
120
|
+
'@codemirror/',
|
|
121
|
+
'dockview',
|
|
122
|
+
'@forgeax/engine-naga',
|
|
123
|
+
"from 'react'",
|
|
124
|
+
'node:fs',
|
|
125
|
+
'node:path',
|
|
126
|
+
];
|
|
127
|
+
const hits = sourceFiles.flatMap((file) => {
|
|
128
|
+
const source = fs.readFileSync(file, 'utf8');
|
|
129
|
+
return forbidden.filter((token) => source.includes(token)).map((token) => `${file}:${token}`);
|
|
130
|
+
});
|
|
131
|
+
expect(hits).toEqual([]);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
111
135
|
describe('v7 cold model path', () => {
|
|
112
136
|
it('keeps protocol and model sources free of host-only imports', () => {
|
|
113
137
|
const sourcePaths = [
|