@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
|
@@ -7,6 +7,7 @@ import { decodeTape } from '../protocol/codec';
|
|
|
7
7
|
import type { Tape } from '../protocol/types';
|
|
8
8
|
import { attachRecorder, type RecordableBackend } from '../recorder/session';
|
|
9
9
|
import { openReplay } from '../replay/session';
|
|
10
|
+
import { runQuerySetReplayFixture } from './query-set-replay-fixture';
|
|
10
11
|
|
|
11
12
|
const GPU_AVAILABLE = typeof navigator !== 'undefined' && navigator.gpu !== undefined;
|
|
12
13
|
const SIZE = 64;
|
|
@@ -236,4 +237,53 @@ describe.skipIf(!GPU_AVAILABLE)('M4 browser capture and replay gate', () => {
|
|
|
236
237
|
expect(replay.error.code).toBeDefined();
|
|
237
238
|
}
|
|
238
239
|
}, 60_000);
|
|
240
|
+
|
|
241
|
+
it('captures and replays an occlusion QuerySet with an eight-byte readback', async () => {
|
|
242
|
+
const backend = await loadBackend();
|
|
243
|
+
const attachment = must(attachRecorder(backend), 'attachRecorder query set');
|
|
244
|
+
const adapter = must(await attachment.backend.rhi.requestAdapter(), 'query set requestAdapter');
|
|
245
|
+
const device = must(await adapter.requestDevice(), 'query set requestDevice');
|
|
246
|
+
const capture = attachment.captureFrame();
|
|
247
|
+
must(await attachment.frameBoundary(), 'query set capture snapshot boundary');
|
|
248
|
+
const evidence = await runQuerySetReplayFixture({
|
|
249
|
+
runner: 'browser',
|
|
250
|
+
device,
|
|
251
|
+
createShaderModule: attachment.backend.createShaderModule,
|
|
252
|
+
replayCreateShaderModule: backend.createShaderModule,
|
|
253
|
+
finishCapture: async () => {
|
|
254
|
+
must(await attachment.frameBoundary(), 'query set capture recording boundary');
|
|
255
|
+
return must(await capture, 'query set capture frame');
|
|
256
|
+
},
|
|
257
|
+
createFreshDevice: async () => {
|
|
258
|
+
const freshAdapter = must(await backend.rhi.requestAdapter(), 'query set fresh adapter');
|
|
259
|
+
return must(await freshAdapter.requestDevice(), 'query set fresh device');
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
await attachment.dispose();
|
|
263
|
+
expect(evidence, JSON.stringify(evidence.errorReceipts)).toMatchObject({
|
|
264
|
+
status: 'available',
|
|
265
|
+
tapeFormatVersion: 7,
|
|
266
|
+
resolveDestinationOffset: 256,
|
|
267
|
+
originalQueryValues: expect.arrayContaining([expect.any(String), '0']),
|
|
268
|
+
originalResultHalfWords: [15360, 0, 0, 15360, 0, 0, 0, 15360],
|
|
269
|
+
freshQueryValues: ['1', '0'],
|
|
270
|
+
errorReceipts: [],
|
|
271
|
+
deviceLost: null,
|
|
272
|
+
});
|
|
273
|
+
expect(evidence.originalQueryValues[0]).not.toBe('0');
|
|
274
|
+
expect(evidence.originalColorBytes.some((byte) => byte > 0)).toBe(true);
|
|
275
|
+
expect(evidence.eventKinds).toEqual(
|
|
276
|
+
expect.arrayContaining([
|
|
277
|
+
'createQuerySet',
|
|
278
|
+
'beginRenderPass',
|
|
279
|
+
'beginOcclusionQuery',
|
|
280
|
+
'endOcclusionQuery',
|
|
281
|
+
'resolveQuerySet',
|
|
282
|
+
'submit',
|
|
283
|
+
'destroyQuerySet',
|
|
284
|
+
]),
|
|
285
|
+
);
|
|
286
|
+
expect(evidence.querySetHandleId).toBeDefined();
|
|
287
|
+
expect(evidence.resolveDestinationHandleId).toBeDefined();
|
|
288
|
+
}, 60_000);
|
|
239
289
|
});
|
|
@@ -290,7 +290,7 @@ describe('RHI-debug smoke roster gate', () => {
|
|
|
290
290
|
],
|
|
291
291
|
{ cwd: ENGINE_ROOT, encoding: 'utf8' },
|
|
292
292
|
);
|
|
293
|
-
expect(passed.status).toBe(0);
|
|
293
|
+
expect(passed.status, passed.stdout + passed.stderr).toBe(0);
|
|
294
294
|
const passedRoster = JSON.parse(passed.stdout) as {
|
|
295
295
|
status: string;
|
|
296
296
|
execution: { status: string; mode: string; passedCount: number; failedCount: number };
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import type { RhiDevice } from '@forgeax/engine-rhi';
|
|
2
|
+
import { createShaderModule, rhi } from '@forgeax/engine-rhi-null';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { decodeTape, encodeTape } from '../protocol/codec';
|
|
5
|
+
import type { RhiCallEvent, Tape } from '../protocol/types';
|
|
6
|
+
import { validateTape } from '../protocol/validation';
|
|
7
|
+
import type { ReplayBackend } from '../replay/session';
|
|
8
|
+
import { openReplay } from '../replay/session';
|
|
9
|
+
|
|
10
|
+
const events: readonly RhiCallEvent[] = [
|
|
11
|
+
{ kind: 'createQuerySet', handleId: 'query-set:1', desc: { type: 'occlusion', count: 1 } },
|
|
12
|
+
{ kind: 'createBuffer', handleId: 'buffer-1', desc: { size: 8, usage: 132 } },
|
|
13
|
+
{
|
|
14
|
+
kind: 'createCommandEncoder',
|
|
15
|
+
cmdHandleId: 'encoder:1',
|
|
16
|
+
desc: { label: 'occlusion-replay' },
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
kind: 'beginRenderPass',
|
|
20
|
+
cmdHandleId: 'encoder:1',
|
|
21
|
+
passHandleId: 'render-pass:1',
|
|
22
|
+
desc: { colorAttachments: [] },
|
|
23
|
+
colorAttachmentViewHandleIds: [],
|
|
24
|
+
occlusionQuerySetHandleId: 'query-set:1',
|
|
25
|
+
},
|
|
26
|
+
{ kind: 'beginOcclusionQuery', passHandleId: 'render-pass:1', queryIndex: 0 },
|
|
27
|
+
{ kind: 'endOcclusionQuery', passHandleId: 'render-pass:1' },
|
|
28
|
+
{ kind: 'endRenderPass', passHandleId: 'render-pass:1' },
|
|
29
|
+
{
|
|
30
|
+
kind: 'resolveQuerySet',
|
|
31
|
+
cmdHandleId: 'encoder:1',
|
|
32
|
+
querySetHandleId: 'query-set:1',
|
|
33
|
+
firstQuery: 0,
|
|
34
|
+
queryCount: 1,
|
|
35
|
+
destinationHandleId: 'buffer-1',
|
|
36
|
+
destinationOffset: 0,
|
|
37
|
+
},
|
|
38
|
+
{ kind: 'finish', cmdHandleId: 'encoder:1' },
|
|
39
|
+
{ kind: 'destroyQuerySet', handleId: 'query-set:1' },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function tape(tapeEvents: readonly RhiCallEvent[] = events): Tape {
|
|
43
|
+
return {
|
|
44
|
+
header: { formatVersion: 7, rhiCaps: {}, eventCount: tapeEvents.length, blobCount: 0 },
|
|
45
|
+
bootstrap: [],
|
|
46
|
+
events: tapeEvents,
|
|
47
|
+
blobs: [],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function inspectTape(queryEvents: readonly RhiCallEvent[]): Tape {
|
|
52
|
+
const inspectEvents: readonly RhiCallEvent[] = [
|
|
53
|
+
{
|
|
54
|
+
kind: 'createQuerySet',
|
|
55
|
+
handleId: 'query-set:inspect',
|
|
56
|
+
desc: { type: 'occlusion', count: 1 },
|
|
57
|
+
},
|
|
58
|
+
{ kind: 'createCommandEncoder', cmdHandleId: 'encoder:inspect', desc: {} },
|
|
59
|
+
{
|
|
60
|
+
kind: 'beginRenderPass',
|
|
61
|
+
cmdHandleId: 'encoder:inspect',
|
|
62
|
+
passHandleId: 'render-pass:inspect',
|
|
63
|
+
desc: { colorAttachments: [] },
|
|
64
|
+
colorAttachmentViewHandleIds: [],
|
|
65
|
+
occlusionQuerySetHandleId: 'query-set:inspect',
|
|
66
|
+
},
|
|
67
|
+
...queryEvents,
|
|
68
|
+
{
|
|
69
|
+
kind: 'draw',
|
|
70
|
+
passHandleId: 'render-pass:inspect',
|
|
71
|
+
vertexCount: 3,
|
|
72
|
+
instanceCount: 1,
|
|
73
|
+
firstVertex: 0,
|
|
74
|
+
firstInstance: 0,
|
|
75
|
+
},
|
|
76
|
+
{ kind: 'finish', cmdHandleId: 'encoder:inspect' },
|
|
77
|
+
{ kind: 'submit', cmdHandleIds: ['encoder:inspect'] },
|
|
78
|
+
];
|
|
79
|
+
return tape(inspectEvents);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function nullBackend(): Promise<ReplayBackend> {
|
|
83
|
+
const adapter = await rhi.requestAdapter();
|
|
84
|
+
if (!adapter.ok) throw new Error(adapter.error.hint);
|
|
85
|
+
const device = await adapter.value.requestDevice();
|
|
86
|
+
if (!device.ok) throw new Error(device.error.hint);
|
|
87
|
+
return { device: device.value, createShaderModule };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function bootstrapBufferTape(withInitialData: boolean): Tape {
|
|
91
|
+
const bootstrap = {
|
|
92
|
+
handleId: 'buffer:bootstrap',
|
|
93
|
+
kind: 'buffer' as const,
|
|
94
|
+
create: {
|
|
95
|
+
kind: 'createBuffer',
|
|
96
|
+
handleId: 'buffer:bootstrap',
|
|
97
|
+
desc: { size: 4, usage: 0x204, mappedAtCreation: false },
|
|
98
|
+
},
|
|
99
|
+
initialData: withInitialData ? [{ hash: 'bootstrap-bytes', byteOffset: 0, byteLength: 4 }] : [],
|
|
100
|
+
};
|
|
101
|
+
const events: readonly RhiCallEvent[] = [
|
|
102
|
+
{ kind: 'createCommandEncoder', cmdHandleId: 'encoder:bootstrap', desc: {} },
|
|
103
|
+
{
|
|
104
|
+
kind: 'beginComputePass',
|
|
105
|
+
cmdHandleId: 'encoder:bootstrap',
|
|
106
|
+
passHandleId: 'pass:bootstrap',
|
|
107
|
+
desc: {},
|
|
108
|
+
},
|
|
109
|
+
{ kind: 'dispatchWorkgroups', passHandleId: 'pass:bootstrap', x: 1, y: 1, z: 1 },
|
|
110
|
+
{ kind: 'endComputePass', passHandleId: 'pass:bootstrap' },
|
|
111
|
+
{ kind: 'finish', cmdHandleId: 'encoder:bootstrap' },
|
|
112
|
+
{ kind: 'submit', cmdHandleIds: ['encoder:bootstrap'] },
|
|
113
|
+
];
|
|
114
|
+
return {
|
|
115
|
+
header: { formatVersion: 7, rhiCaps: {}, eventCount: events.length, blobCount: 1 },
|
|
116
|
+
bootstrap: [bootstrap],
|
|
117
|
+
events,
|
|
118
|
+
blobs: [
|
|
119
|
+
{
|
|
120
|
+
hash: 'bootstrap-bytes',
|
|
121
|
+
bytes: new Uint8Array([1, 2, 3, 4]),
|
|
122
|
+
compression: 'none',
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function trackingBackend(): Promise<{
|
|
129
|
+
readonly backend: ReplayBackend;
|
|
130
|
+
readonly usages: number[];
|
|
131
|
+
}> {
|
|
132
|
+
const backend = await nullBackend();
|
|
133
|
+
const usages: number[] = [];
|
|
134
|
+
const device = backend.device as RhiDevice;
|
|
135
|
+
const createBuffer = device.createBuffer.bind(device);
|
|
136
|
+
device.createBuffer = (desc) => {
|
|
137
|
+
usages.push(desc.usage ?? 0);
|
|
138
|
+
return createBuffer(desc);
|
|
139
|
+
};
|
|
140
|
+
return { backend, usages };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
describe('QuerySet occlusion tape lifecycle', () => {
|
|
144
|
+
it('round-trips create, attach, begin/end, resolve, and destroy events', () => {
|
|
145
|
+
const encoded = encodeTape(tape());
|
|
146
|
+
expect(encoded.ok).toBe(true);
|
|
147
|
+
if (!encoded.ok) return;
|
|
148
|
+
const decoded = decodeTape(encoded.value);
|
|
149
|
+
expect(decoded.ok).toBe(true);
|
|
150
|
+
if (!decoded.ok) return;
|
|
151
|
+
expect(decoded.value.events.map((event) => event.kind)).toEqual(
|
|
152
|
+
events.map((event) => event.kind),
|
|
153
|
+
);
|
|
154
|
+
expect(validateTape(decoded.value).ok).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('rejects query resolve before its QuerySet is created', () => {
|
|
158
|
+
const invalid = tape([
|
|
159
|
+
{
|
|
160
|
+
kind: 'resolveQuerySet',
|
|
161
|
+
cmdHandleId: 'encoder:1',
|
|
162
|
+
querySetHandleId: 'query-set:missing',
|
|
163
|
+
firstQuery: 0,
|
|
164
|
+
queryCount: 1,
|
|
165
|
+
destinationHandleId: 'buffer-1',
|
|
166
|
+
destinationOffset: 0,
|
|
167
|
+
},
|
|
168
|
+
]);
|
|
169
|
+
const result = validateTape(invalid);
|
|
170
|
+
expect(result.ok).toBe(false);
|
|
171
|
+
if (!result.ok) expect(result.error.code).toBe('tape-invalid');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('rejects QuerySet use after disposal', () => {
|
|
175
|
+
const invalid = tape([
|
|
176
|
+
{ kind: 'createQuerySet', handleId: 'query-set:1', desc: { type: 'occlusion', count: 1 } },
|
|
177
|
+
{ kind: 'destroyQuerySet', handleId: 'query-set:1' },
|
|
178
|
+
{ kind: 'destroyQuerySet', handleId: 'query-set:1' },
|
|
179
|
+
]);
|
|
180
|
+
const result = validateTape(invalid);
|
|
181
|
+
expect(result.ok).toBe(false);
|
|
182
|
+
if (!result.ok) expect(result.error.code).toBe('tape-invalid');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('closes an active occlusion query before finalizing a selected work pass', async () => {
|
|
186
|
+
const opened = await openReplay(
|
|
187
|
+
inspectTape([
|
|
188
|
+
{ kind: 'beginOcclusionQuery', passHandleId: 'render-pass:inspect', queryIndex: 0 },
|
|
189
|
+
]),
|
|
190
|
+
await nullBackend(),
|
|
191
|
+
);
|
|
192
|
+
expect(opened.ok).toBe(true);
|
|
193
|
+
if (!opened.ok) return;
|
|
194
|
+
const inspection = await opened.value.inspectWork(0);
|
|
195
|
+
expect(inspection.ok).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('finalizes a selected pass without inventing an occlusion query', async () => {
|
|
199
|
+
const opened = await openReplay(inspectTape([]), await nullBackend());
|
|
200
|
+
expect(opened.ok).toBe(true);
|
|
201
|
+
if (!opened.ok) return;
|
|
202
|
+
const inspection = await opened.value.inspectWork(0);
|
|
203
|
+
expect(inspection.ok).toBe(true);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('returns a structured error for a mismatched occlusion-query closure', async () => {
|
|
207
|
+
const opened = await openReplay(
|
|
208
|
+
inspectTape([{ kind: 'endOcclusionQuery', passHandleId: 'render-pass:other' }]),
|
|
209
|
+
await nullBackend(),
|
|
210
|
+
);
|
|
211
|
+
expect(opened.ok).toBe(true);
|
|
212
|
+
if (!opened.ok) return;
|
|
213
|
+
const inspection = await opened.value.inspectWork(0);
|
|
214
|
+
expect(inspection.ok).toBe(false);
|
|
215
|
+
if (!inspection.ok) expect(inspection.error.code).toBe('replay-event-failed');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('keeps the captured usage when bootstrap initialData seeds through creation mapping', async () => {
|
|
219
|
+
const tracked = await trackingBackend();
|
|
220
|
+
const opened = await openReplay(bootstrapBufferTape(true), tracked.backend);
|
|
221
|
+
expect(opened.ok).toBe(true);
|
|
222
|
+
if (!opened.ok) return;
|
|
223
|
+
expect((await opened.value.inspectWork(0)).ok).toBe(true);
|
|
224
|
+
// Creation mapping restores the bytes without queue.writeBuffer, so the
|
|
225
|
+
// replay must not mutate the captured usage with a replay-only COPY_DST.
|
|
226
|
+
expect(tracked.usages).toEqual([0x204]);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('does not add replay-only COPY_DST to an unseeded or already writable buffer', async () => {
|
|
230
|
+
const unseeded = await trackingBackend();
|
|
231
|
+
const openedUnseeded = await openReplay(bootstrapBufferTape(false), unseeded.backend);
|
|
232
|
+
expect(openedUnseeded.ok).toBe(true);
|
|
233
|
+
if (!openedUnseeded.ok) return;
|
|
234
|
+
expect((await openedUnseeded.value.inspectWork(0)).ok).toBe(true);
|
|
235
|
+
expect(unseeded.usages).toEqual([0x204]);
|
|
236
|
+
|
|
237
|
+
const alreadyWritable = await trackingBackend();
|
|
238
|
+
const tapeWithWritableBuffer = bootstrapBufferTape(true);
|
|
239
|
+
const bootstrap = tapeWithWritableBuffer.bootstrap[0];
|
|
240
|
+
if (bootstrap === undefined) return;
|
|
241
|
+
const createBufferEvent = bootstrap.create as unknown as Extract<
|
|
242
|
+
RhiCallEvent,
|
|
243
|
+
{ readonly kind: 'createBuffer' }
|
|
244
|
+
>;
|
|
245
|
+
const openedWritable = await openReplay(
|
|
246
|
+
{
|
|
247
|
+
...tapeWithWritableBuffer,
|
|
248
|
+
bootstrap: [
|
|
249
|
+
{
|
|
250
|
+
...bootstrap,
|
|
251
|
+
create: {
|
|
252
|
+
...createBufferEvent,
|
|
253
|
+
desc: { ...createBufferEvent.desc, usage: 0x20c },
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
},
|
|
258
|
+
alreadyWritable.backend,
|
|
259
|
+
);
|
|
260
|
+
expect(openedWritable.ok).toBe(true);
|
|
261
|
+
if (!openedWritable.ok) return;
|
|
262
|
+
expect((await openedWritable.value.inspectWork(0)).ok).toBe(true);
|
|
263
|
+
expect(alreadyWritable.usages).toEqual([0x20c]);
|
|
264
|
+
});
|
|
265
|
+
});
|