@forgeax/engine-rhi-webgpu 0.1.20 → 0.1.21
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 +7 -6
- package/dist/.tsbuildinfo +1 -1
- package/dist/device.d.ts.map +1 -1
- package/dist/index.mjs +18 -42
- package/dist/index.mjs.map +1 -1
- package/dist/internal/timestamp-query.d.ts +0 -5
- package/dist/internal/timestamp-query.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/dawn-real-gpu.dawn.test.ts +2 -3
- package/src/__tests__/rhi-webgpu.unit.test.ts +68 -160
- package/src/device.ts +26 -54
- package/src/internal/timestamp-query.ts +0 -27
- package/dist/internal/__tests__/timestamp-query.unit.test.d.ts +0 -2
- package/dist/internal/__tests__/timestamp-query.unit.test.d.ts.map +0 -1
- package/src/internal/__tests__/timestamp-query.unit.test.ts +0 -88
|
@@ -7,9 +7,4 @@ export declare function resolveTimestampQueries(args: {
|
|
|
7
7
|
rawDestination: GPUBuffer;
|
|
8
8
|
destinationOffset: number;
|
|
9
9
|
}): Result<void, RhiError>;
|
|
10
|
-
export declare function writeTimestamp(args: {
|
|
11
|
-
rawEncoder: GPUCommandEncoder;
|
|
12
|
-
rawQuerySet: GPUQuerySet;
|
|
13
|
-
queryIndex: number;
|
|
14
|
-
}): void;
|
|
15
10
|
//# sourceMappingURL=timestamp-query.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timestamp-query.d.ts","sourceRoot":"","sources":["../../src/internal/timestamp-query.ts"],"names":[],"mappings":"AAEA,OAAO,EAAW,KAAK,MAAM,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAErE,wBAAgB,uBAAuB,CAAC,IAAI,EAAE;IAC5C,UAAU,EAAE,iBAAiB,CAAC;IAC9B,WAAW,EAAE,WAAW,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,SAAS,CAAC;IAC1B,iBAAiB,EAAE,MAAM,CAAC;CAC3B,GAAG,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,CAoBzB
|
|
1
|
+
{"version":3,"file":"timestamp-query.d.ts","sourceRoot":"","sources":["../../src/internal/timestamp-query.ts"],"names":[],"mappings":"AAEA,OAAO,EAAW,KAAK,MAAM,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAErE,wBAAgB,uBAAuB,CAAC,IAAI,EAAE;IAC5C,UAAU,EAAE,iBAAiB,CAAC;IAC9B,WAAW,EAAE,WAAW,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,SAAS,CAAC;IAC1B,iBAAiB,EAAE,MAAM,CAAC;CAC3B,GAAG,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,CAoBzB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgeax/engine-rhi-webgpu",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"LICENSE"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@forgeax/engine-rhi": "0.1.
|
|
26
|
-
"@forgeax/engine-types": "0.1.
|
|
25
|
+
"@forgeax/engine-rhi": "0.1.21",
|
|
26
|
+
"@forgeax/engine-types": "0.1.21",
|
|
27
27
|
"@webgpu/types": "^0.1.71"
|
|
28
28
|
},
|
|
29
29
|
"forgeax": {
|
|
@@ -1219,9 +1219,8 @@ describe('w36 (M5) - dawn-real-gpu RhiQueue.onSubmittedWorkDone returns Promise<
|
|
|
1219
1219
|
// ---------------------------------------------------------------------------
|
|
1220
1220
|
//
|
|
1221
1221
|
// research §2.4 + dawn ComputePassDescriptor timestampWrites reference:
|
|
1222
|
-
// the real compute pass owns the beginning/end timestamp writes.
|
|
1223
|
-
//
|
|
1224
|
-
// it even when timestamp-query is advertised.
|
|
1222
|
+
// the real compute pass owns the beginning/end timestamp writes. Command-
|
|
1223
|
+
// encoder timestamp markers are intentionally absent from the RHI surface.
|
|
1225
1224
|
describe('w38 (M5 / K-3) - dawn-real-gpu compute-pass timestampWrites gate', () => {
|
|
1226
1225
|
it('reports timestamp-query refusal without treating a capability-disabled path as success', async () => {
|
|
1227
1226
|
const device = await requestRhiDevice();
|
|
@@ -219,6 +219,74 @@ import { createMockGpu, type MockCapture, makeShaderError } from './__mocks__/gp
|
|
|
219
219
|
});
|
|
220
220
|
});
|
|
221
221
|
|
|
222
|
+
describe('encodeEmptyComputePass', () => {
|
|
223
|
+
it('forwards one mirrored descriptor to one raw pass and ends that pass once', async () => {
|
|
224
|
+
const gpu = createMockGpu();
|
|
225
|
+
const adapter = await gpu.requestAdapter();
|
|
226
|
+
if (adapter === null) throw new Error('mock adapter should exist');
|
|
227
|
+
const raw = await adapter.requestDevice();
|
|
228
|
+
(raw.features as unknown as Set<GPUFeatureName>).add('timestamp-query');
|
|
229
|
+
const rawDescriptors: GPUComputePassDescriptor[] = [];
|
|
230
|
+
let beginCalls = 0;
|
|
231
|
+
let endCalls = 0;
|
|
232
|
+
const originalCreateCommandEncoder = raw.createCommandEncoder.bind(raw);
|
|
233
|
+
raw.createCommandEncoder = (descriptor) => {
|
|
234
|
+
const encoder = originalCreateCommandEncoder(descriptor);
|
|
235
|
+
const originalBeginComputePass = encoder.beginComputePass.bind(encoder);
|
|
236
|
+
encoder.beginComputePass = (passDescriptor) => {
|
|
237
|
+
beginCalls += 1;
|
|
238
|
+
if (passDescriptor !== undefined) rawDescriptors.push(passDescriptor);
|
|
239
|
+
const pass = originalBeginComputePass(passDescriptor);
|
|
240
|
+
const originalEnd = pass.end.bind(pass);
|
|
241
|
+
pass.end = () => {
|
|
242
|
+
endCalls += 1;
|
|
243
|
+
originalEnd();
|
|
244
|
+
};
|
|
245
|
+
return pass;
|
|
246
|
+
};
|
|
247
|
+
return encoder;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const device = makeRhiDevice(raw as unknown as GPUDevice).device;
|
|
251
|
+
const querySetResult = device.createQuerySet({ type: 'timestamp', count: 2 });
|
|
252
|
+
if (!querySetResult.ok) throw new Error('mock createQuerySet failed');
|
|
253
|
+
const descriptor = {
|
|
254
|
+
timestampWrites: {
|
|
255
|
+
querySet: querySetResult.value,
|
|
256
|
+
beginningOfPassWriteIndex: 0,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
const encoderResult = device.createCommandEncoder();
|
|
260
|
+
if (!encoderResult.ok) throw new Error('mock createCommandEncoder failed');
|
|
261
|
+
|
|
262
|
+
encoderResult.value.encodeEmptyComputePass(descriptor);
|
|
263
|
+
|
|
264
|
+
expect(beginCalls).toBe(1);
|
|
265
|
+
expect(endCalls).toBe(1);
|
|
266
|
+
expect(rawDescriptors).toHaveLength(1);
|
|
267
|
+
expect(rawDescriptors[0]?.timestampWrites?.querySet).toBe(querySetResult.value);
|
|
268
|
+
expect(rawDescriptors[0]?.timestampWrites?.beginningOfPassWriteIndex).toBe(0);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('keeps the finished-encoder failure channel', async () => {
|
|
272
|
+
const gpu = createMockGpu();
|
|
273
|
+
const r = await requestDevice({ gpu });
|
|
274
|
+
if (!r.ok) throw new Error('mock requestDevice should not fail');
|
|
275
|
+
const encoderResult = r.value.createCommandEncoder();
|
|
276
|
+
if (!encoderResult.ok) throw new Error('mock createCommandEncoder failed');
|
|
277
|
+
expect(encoderResult.value.finish().ok).toBe(true);
|
|
278
|
+
|
|
279
|
+
expect(() =>
|
|
280
|
+
encoderResult.value.encodeEmptyComputePass({
|
|
281
|
+
timestampWrites: {
|
|
282
|
+
querySet: {} as never,
|
|
283
|
+
endOfPassWriteIndex: 1,
|
|
284
|
+
},
|
|
285
|
+
}),
|
|
286
|
+
).toThrowError(expect.objectContaining({ code: 'command-encoder-finished' }));
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
222
290
|
// w24 - resolveQuerySet placeholder retirement red phase. Asserts:
|
|
223
291
|
// (a) destinationOffset % 256 != 0 -> webgpu-runtime-error with .expected
|
|
224
292
|
// literal 'destinationOffset % 256 == 0 (spec normative)'.
|
|
@@ -384,166 +452,6 @@ import { createMockGpu, type MockCapture, makeShaderError } from './__mocks__/gp
|
|
|
384
452
|
expect(device.destroyBuffer(readbackResult.value).ok).toBe(true);
|
|
385
453
|
});
|
|
386
454
|
});
|
|
387
|
-
|
|
388
|
-
// ---------------------------------------------------------------------------
|
|
389
|
-
// w38 (M5 / K-3) - RhiCommandEncoder.writeTimestamp gating + happy path.
|
|
390
|
-
// ---------------------------------------------------------------------------
|
|
391
|
-
//
|
|
392
|
-
// research §2.4: dawn TimestampOnCommandEncoder calls
|
|
393
|
-
// encoder.WriteTimestamp(querySet, queryIndex) directly on the command
|
|
394
|
-
// encoder; the entry is gated on the 'timestamp-query' device feature.
|
|
395
|
-
// The forgeax form is RhiCommandEncoder.writeTimestamp(querySet, queryIndex)
|
|
396
|
-
// with `void` return (spec literal alignment); when caps.timestampQuery is
|
|
397
|
-
// false the shim fans out 'feature-not-enabled' through the engine onError
|
|
398
|
-
// channel (no Result wrapper because the spec method returns void).
|
|
399
|
-
|
|
400
|
-
describe('w38 (M5 / K-3) - RhiCommandEncoder.writeTimestamp', () => {
|
|
401
|
-
it('writeTimestamp(querySet, queryIndex) is callable on a CommandEncoder when caps.timestampQuery is true', async () => {
|
|
402
|
-
const gpu = createMockGpu();
|
|
403
|
-
const r = await requestDevice({ gpu });
|
|
404
|
-
if (!r.ok) throw new Error('mock requestDevice should not fail');
|
|
405
|
-
const device = r.value as unknown as {
|
|
406
|
-
caps: { timestampQuery: boolean };
|
|
407
|
-
createQuerySet: (desc: { type: string; count: number; label?: string }) => {
|
|
408
|
-
ok: boolean;
|
|
409
|
-
value: unknown;
|
|
410
|
-
};
|
|
411
|
-
createCommandEncoder: (desc?: unknown) => {
|
|
412
|
-
ok: boolean;
|
|
413
|
-
value: { writeTimestamp?: (qs: unknown, idx: number) => void };
|
|
414
|
-
};
|
|
415
|
-
};
|
|
416
|
-
// Mock device defaults to timestampQuery=false; the gate test below
|
|
417
|
-
// covers that path. Here we assert the surface exists at the very least.
|
|
418
|
-
const encResult = device.createCommandEncoder({ label: 'w38-encoder' });
|
|
419
|
-
expect(encResult.ok).toBe(true);
|
|
420
|
-
expect(typeof encResult.value.writeTimestamp).toBe('function');
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
it('maps an opaque QuerySet in compute-pass timestampWrites to the raw descriptor', async () => {
|
|
424
|
-
const gpu = createMockGpu();
|
|
425
|
-
const adapter = await gpu.requestAdapter();
|
|
426
|
-
if (adapter === null) throw new Error('mock adapter should exist');
|
|
427
|
-
const raw = await adapter.requestDevice();
|
|
428
|
-
(raw.features as unknown as Set<GPUFeatureName>).add('timestamp-query');
|
|
429
|
-
let rawQuerySet: unknown;
|
|
430
|
-
const originalCreateQuerySet = raw.createQuerySet.bind(raw);
|
|
431
|
-
raw.createQuerySet = (descriptor) => {
|
|
432
|
-
const result = originalCreateQuerySet(descriptor);
|
|
433
|
-
rawQuerySet = result;
|
|
434
|
-
return result;
|
|
435
|
-
};
|
|
436
|
-
let captured: GPUComputePassDescriptor | undefined;
|
|
437
|
-
const originalCreateCommandEncoder = raw.createCommandEncoder.bind(raw);
|
|
438
|
-
raw.createCommandEncoder = (descriptor) => {
|
|
439
|
-
const encoder = originalCreateCommandEncoder(descriptor) as unknown as Record<
|
|
440
|
-
string,
|
|
441
|
-
unknown
|
|
442
|
-
>;
|
|
443
|
-
const originalBegin = encoder.beginComputePass as (
|
|
444
|
-
passDescriptor?: GPUComputePassDescriptor,
|
|
445
|
-
) => unknown;
|
|
446
|
-
encoder.beginComputePass = (passDescriptor?: GPUComputePassDescriptor) => {
|
|
447
|
-
captured = passDescriptor;
|
|
448
|
-
return originalBegin.call(encoder, passDescriptor);
|
|
449
|
-
};
|
|
450
|
-
return encoder as unknown as ReturnType<typeof raw.createCommandEncoder>;
|
|
451
|
-
};
|
|
452
|
-
const { device } = makeRhiDevice(raw as unknown as GPUDevice);
|
|
453
|
-
const querySet = device.createQuerySet({ type: 'timestamp', count: 2 });
|
|
454
|
-
expect(querySet.ok).toBe(true);
|
|
455
|
-
if (!querySet.ok) return;
|
|
456
|
-
const encoder = device.createCommandEncoder();
|
|
457
|
-
expect(encoder.ok).toBe(true);
|
|
458
|
-
if (!encoder.ok) return;
|
|
459
|
-
const pass = encoder.value.beginComputePass({
|
|
460
|
-
label: 'hdrp-cluster-membership',
|
|
461
|
-
timestampWrites: {
|
|
462
|
-
querySet: querySet.value,
|
|
463
|
-
beginningOfPassWriteIndex: 0,
|
|
464
|
-
endOfPassWriteIndex: 1,
|
|
465
|
-
},
|
|
466
|
-
});
|
|
467
|
-
pass.end();
|
|
468
|
-
expect(captured).toMatchObject({
|
|
469
|
-
label: 'hdrp-cluster-membership',
|
|
470
|
-
timestampWrites: {
|
|
471
|
-
querySet: querySet.value,
|
|
472
|
-
beginningOfPassWriteIndex: 0,
|
|
473
|
-
endOfPassWriteIndex: 1,
|
|
474
|
-
},
|
|
475
|
-
});
|
|
476
|
-
expect(rawQuerySet).toBeDefined();
|
|
477
|
-
expect(captured?.timestampWrites?.querySet).toBe(rawQuerySet);
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
async function timestampEncoder(
|
|
481
|
-
writeTimestamp: ((querySet: unknown, queryIndex: number) => void) | undefined,
|
|
482
|
-
) {
|
|
483
|
-
const gpu = createMockGpu();
|
|
484
|
-
const adapter = await gpu.requestAdapter();
|
|
485
|
-
if (adapter === null) throw new Error('mock adapter should exist');
|
|
486
|
-
const raw = await adapter.requestDevice();
|
|
487
|
-
const features = raw.features as unknown as Set<GPUFeatureName>;
|
|
488
|
-
features.add('timestamp-query');
|
|
489
|
-
const originalCreateCommandEncoder = raw.createCommandEncoder.bind(raw);
|
|
490
|
-
raw.createCommandEncoder = (descriptor) => {
|
|
491
|
-
const encoder = originalCreateCommandEncoder(descriptor) as unknown as Record<
|
|
492
|
-
string,
|
|
493
|
-
unknown
|
|
494
|
-
>;
|
|
495
|
-
if (writeTimestamp !== undefined) encoder.writeTimestamp = writeTimestamp;
|
|
496
|
-
return encoder as unknown as ReturnType<typeof raw.createCommandEncoder>;
|
|
497
|
-
};
|
|
498
|
-
const { device } = makeRhiDevice(raw as unknown as GPUDevice);
|
|
499
|
-
const querySet = device.createQuerySet({ type: 'timestamp', count: 2 });
|
|
500
|
-
if (!querySet.ok) throw new Error('timestamp query set should be created');
|
|
501
|
-
const encoder = device.createCommandEncoder();
|
|
502
|
-
if (!encoder.ok) throw new Error('command encoder should be created');
|
|
503
|
-
return { encoder: encoder.value, querySet: querySet.value };
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
it('forwards a callable raw writeTimestamp exactly once with the raw query set and index', async () => {
|
|
507
|
-
const calls: Array<{ querySet: unknown; queryIndex: number }> = [];
|
|
508
|
-
const { encoder, querySet } = await timestampEncoder((rawQuerySet, queryIndex) => {
|
|
509
|
-
calls.push({ querySet: rawQuerySet, queryIndex });
|
|
510
|
-
});
|
|
511
|
-
encoder.writeTimestamp(querySet, 1);
|
|
512
|
-
expect(calls).toHaveLength(1);
|
|
513
|
-
expect(calls[0]?.queryIndex).toBe(1);
|
|
514
|
-
expect(calls[0]?.querySet).toBe(querySet);
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
it('throws structured webgpu-runtime-error when a timestamp-capable raw encoder omits writeTimestamp', async () => {
|
|
518
|
-
const { encoder, querySet } = await timestampEncoder(undefined);
|
|
519
|
-
expect(() => encoder.writeTimestamp(querySet, 0)).toThrow(RhiError);
|
|
520
|
-
try {
|
|
521
|
-
encoder.writeTimestamp(querySet, 0);
|
|
522
|
-
} catch (error) {
|
|
523
|
-
expect(error).toMatchObject({
|
|
524
|
-
code: 'webgpu-runtime-error',
|
|
525
|
-
expected: 'underlying GPUCommandEncoder.writeTimestamp to be callable',
|
|
526
|
-
});
|
|
527
|
-
expect((error as RhiError).hint).toContain('timestamp-query');
|
|
528
|
-
}
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
it('throws structured webgpu-runtime-error when the raw timestamp write throws', async () => {
|
|
532
|
-
const { encoder, querySet } = await timestampEncoder(() => {
|
|
533
|
-
throw new Error('raw timestamp failure');
|
|
534
|
-
});
|
|
535
|
-
expect(() => encoder.writeTimestamp(querySet, 0)).toThrow(RhiError);
|
|
536
|
-
try {
|
|
537
|
-
encoder.writeTimestamp(querySet, 0);
|
|
538
|
-
} catch (error) {
|
|
539
|
-
expect(error).toMatchObject({
|
|
540
|
-
code: 'webgpu-runtime-error',
|
|
541
|
-
expected: 'underlying GPUCommandEncoder.writeTimestamp to succeed',
|
|
542
|
-
});
|
|
543
|
-
expect((error as RhiError).hint).toContain('raw timestamp failure');
|
|
544
|
-
}
|
|
545
|
-
});
|
|
546
|
-
});
|
|
547
455
|
}
|
|
548
456
|
|
|
549
457
|
{
|
package/src/device.ts
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
// exposes the spec Promise without a second cache.
|
|
15
15
|
//
|
|
16
16
|
// w3 / w5 / w6 (feat-20260508-rhi-surface-completion, co-commit):
|
|
17
|
-
// - createCommandEncoder + 12 RhiCommandEncoder methods
|
|
17
|
+
// - createCommandEncoder + 12 RhiCommandEncoder spec methods plus the
|
|
18
|
+
// ForgeaX encodeEmptyComputePass compound operation + 3 mixin (w3)
|
|
18
19
|
// - 17 RhiRenderPassEncoder spec stable methods + 1 setBindGroup overload +
|
|
19
20
|
// 3 placeholders (executeBundles / beginOcclusionQuery / endOcclusionQuery) (w5)
|
|
20
21
|
// - Queue.submit / writeBuffer real implementation + bounds validation (w6)
|
|
@@ -33,6 +34,7 @@ import type {
|
|
|
33
34
|
BindGroupLayout,
|
|
34
35
|
BindGroupLayoutDescriptor,
|
|
35
36
|
Buffer,
|
|
37
|
+
BufferCopyDestination,
|
|
36
38
|
BufferDescriptor,
|
|
37
39
|
CanvasConfiguration,
|
|
38
40
|
CommandBuffer,
|
|
@@ -63,6 +65,7 @@ import type {
|
|
|
63
65
|
Sampler,
|
|
64
66
|
SamplerDescriptor,
|
|
65
67
|
Texture,
|
|
68
|
+
TextureCopySource,
|
|
66
69
|
TextureDescriptor,
|
|
67
70
|
TextureView,
|
|
68
71
|
TextureViewDescriptor,
|
|
@@ -75,7 +78,7 @@ import {
|
|
|
75
78
|
queueWriteBufferOutOfBounds,
|
|
76
79
|
renderPassNotEnded,
|
|
77
80
|
} from './errors';
|
|
78
|
-
import { resolveTimestampQueries
|
|
81
|
+
import { resolveTimestampQueries } from './internal/timestamp-query';
|
|
79
82
|
|
|
80
83
|
/**
|
|
81
84
|
* Mirror forgeax `?: T | undefined` descriptor onto the spec GPUXxxDescriptor
|
|
@@ -812,11 +815,7 @@ function mirrorRenderPipelineDescriptor(
|
|
|
812
815
|
* - render-pass-not-ended is detected by tracking activePass; finish()
|
|
813
816
|
* while a pass has not been end()-ed returns the structured error.
|
|
814
817
|
*/
|
|
815
|
-
function makeCommandEncoder(
|
|
816
|
-
rawEncoder: GPUCommandEncoder,
|
|
817
|
-
caps: { readonly timestampQuery: boolean },
|
|
818
|
-
fireFeatureNotEnabled: (featureName: string, hint: string) => void,
|
|
819
|
-
): RhiCommandEncoder {
|
|
818
|
+
function makeCommandEncoder(rawEncoder: GPUCommandEncoder): RhiCommandEncoder {
|
|
820
819
|
function mirrorComputePassDescriptor(
|
|
821
820
|
desc: ComputePassDescriptor | undefined,
|
|
822
821
|
): GPUComputePassDescriptor | undefined {
|
|
@@ -890,6 +889,12 @@ function makeCommandEncoder(
|
|
|
890
889
|
};
|
|
891
890
|
return pass;
|
|
892
891
|
},
|
|
892
|
+
encodeEmptyComputePass(desc: ComputePassDescriptor): void {
|
|
893
|
+
const state = ENCODER_STATE.get(enc);
|
|
894
|
+
throwIfFinished(state);
|
|
895
|
+
const rawPass = rawEncoder.beginComputePass(mirrorComputePassDescriptor(desc));
|
|
896
|
+
rawPass.end();
|
|
897
|
+
},
|
|
893
898
|
copyBufferToBuffer(
|
|
894
899
|
source: Buffer,
|
|
895
900
|
arg2: number | Buffer,
|
|
@@ -933,19 +938,27 @@ function makeCommandEncoder(
|
|
|
933
938
|
rawEncoder.copyBufferToTexture(rawSrc, destination, copySize);
|
|
934
939
|
},
|
|
935
940
|
copyTextureToBuffer(
|
|
936
|
-
source: GPUTexelCopyTextureInfo,
|
|
937
|
-
destination: GPUTexelCopyBufferInfo,
|
|
941
|
+
source: GPUTexelCopyTextureInfo | TextureCopySource,
|
|
942
|
+
destination: GPUTexelCopyBufferInfo | BufferCopyDestination,
|
|
938
943
|
copySize: GPUExtent3DStrict,
|
|
939
944
|
): void {
|
|
940
945
|
const state = ENCODER_STATE.get(enc);
|
|
941
946
|
throwIfFinished(state);
|
|
942
|
-
const
|
|
943
|
-
|
|
947
|
+
const rawSource: GPUTexelCopyTextureInfo = {
|
|
948
|
+
texture: source.texture as unknown as GPUTexture,
|
|
949
|
+
};
|
|
950
|
+
if (source.mipLevel !== undefined) rawSource.mipLevel = source.mipLevel;
|
|
951
|
+
if (source.origin !== undefined) rawSource.origin = source.origin;
|
|
952
|
+
if (source.aspect !== undefined) rawSource.aspect = source.aspect;
|
|
953
|
+
const rawDst: GPUTexelCopyBufferInfo = {
|
|
944
954
|
buffer:
|
|
945
955
|
BUFFER_RAW_MAP.get(destination.buffer as unknown as Buffer) ??
|
|
946
956
|
(destination.buffer as unknown as GPUBuffer),
|
|
947
957
|
};
|
|
948
|
-
|
|
958
|
+
if (destination.offset !== undefined) rawDst.offset = destination.offset;
|
|
959
|
+
if (destination.bytesPerRow !== undefined) rawDst.bytesPerRow = destination.bytesPerRow;
|
|
960
|
+
if (destination.rowsPerImage !== undefined) rawDst.rowsPerImage = destination.rowsPerImage;
|
|
961
|
+
rawEncoder.copyTextureToBuffer(rawSource, rawDst, copySize);
|
|
949
962
|
},
|
|
950
963
|
copyTextureToTexture(
|
|
951
964
|
source: GPUTexelCopyTextureInfo,
|
|
@@ -1045,25 +1058,6 @@ function makeCommandEncoder(
|
|
|
1045
1058
|
insertDebugMarker(markerLabel: string): void {
|
|
1046
1059
|
rawEncoder.insertDebugMarker(markerLabel);
|
|
1047
1060
|
},
|
|
1048
|
-
writeTimestamp(querySet: QuerySet, queryIndex: number): void {
|
|
1049
|
-
// M5 / K-3 (research §2.4): timestamp-query feature gate. spec
|
|
1050
|
-
// writeTimestamp returns void; the forgeax form keeps the void shape
|
|
1051
|
-
// and fans out 'feature-not-enabled' through the engine onError
|
|
1052
|
-
// channel rather than wrapping in Result. When the capability is true,
|
|
1053
|
-
// a missing or throwing raw write is a structured runtime failure so a
|
|
1054
|
-
// Render capture cannot publish a fabricated interval.
|
|
1055
|
-
const state = ENCODER_STATE.get(enc);
|
|
1056
|
-
throwIfFinished(state);
|
|
1057
|
-
if (caps.timestampQuery !== true) {
|
|
1058
|
-
fireFeatureNotEnabled(
|
|
1059
|
-
'timestamp-query',
|
|
1060
|
-
'check device.caps.timestampQuery before calling writeTimestamp',
|
|
1061
|
-
);
|
|
1062
|
-
return;
|
|
1063
|
-
}
|
|
1064
|
-
const rawQs = QUERY_SET_RAW_MAP.get(querySet) ?? (querySet as unknown as GPUQuerySet);
|
|
1065
|
-
writeTimestamp({ rawEncoder, rawQuerySet: rawQs, queryIndex });
|
|
1066
|
-
},
|
|
1067
1061
|
finish(): Result<CommandBuffer, RhiError> {
|
|
1068
1062
|
const state = ENCODER_STATE.get(enc);
|
|
1069
1063
|
if (state === undefined) {
|
|
@@ -1915,29 +1909,7 @@ export function makeRhiDevice(rawDevice: GPUDevice): {
|
|
|
1915
1909
|
: rawDevice.createCommandEncoder(
|
|
1916
1910
|
mirror(desc, ENC_KEYS) as unknown as GPUCommandEncoderDescriptor,
|
|
1917
1911
|
);
|
|
1918
|
-
|
|
1919
|
-
// writeTimestamp can fire 'feature-not-enabled' through the engine
|
|
1920
|
-
// channel when caps.timestampQuery is false (K-3: spec writeTimestamp
|
|
1921
|
-
// returns void; the forgeax form keeps that shape).
|
|
1922
|
-
//
|
|
1923
|
-
// Round 3 fix-up F-P3-3: the forgeax RhiDevice
|
|
1924
|
-
// does not expose `onError` directly (charter proposition 5: keep
|
|
1925
|
-
// RHI math-free + listener-free), so the shim writes a structured
|
|
1926
|
-
// diagnostic to `console.error` matching the RhiError shape. The
|
|
1927
|
-
// engine layer subscribes through `Renderer.onError` and fans the
|
|
1928
|
-
// same RhiError out; this keeps the unsupported capability observable
|
|
1929
|
-
// for pure-RHI consumers (mock unit tests, dawn-real-gpu probes) that
|
|
1930
|
-
// never instantiate a Renderer.
|
|
1931
|
-
const fireFeatureNotEnabled = (featureName: string, hint: string): void => {
|
|
1932
|
-
// Diagnostic channel (a) of the K-9 double-channel pattern: default
|
|
1933
|
-
// console.error so AI consumers running headless / mock paths still
|
|
1934
|
-
// observe the unsupported capability without subscribing to a
|
|
1935
|
-
// listener. The capability-disabled entry remains non-throwing.
|
|
1936
|
-
console.error(
|
|
1937
|
-
`[RhiError feature-not-enabled] expected: device.features.has('${featureName}') === true; hint: ${hint}`,
|
|
1938
|
-
);
|
|
1939
|
-
};
|
|
1940
|
-
return ok(makeCommandEncoder(rawEnc, caps, fireFeatureNotEnabled));
|
|
1912
|
+
return ok(makeCommandEncoder(rawEnc));
|
|
1941
1913
|
},
|
|
1942
1914
|
// fix-f3: synchronous createShaderModule placeholder removed; the
|
|
1943
1915
|
// shader-compile-failed path lives in the top-level async factory
|
|
@@ -30,30 +30,3 @@ export function resolveTimestampQueries(args: {
|
|
|
30
30
|
);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
export function writeTimestamp(args: {
|
|
35
|
-
rawEncoder: GPUCommandEncoder;
|
|
36
|
-
rawQuerySet: GPUQuerySet;
|
|
37
|
-
queryIndex: number;
|
|
38
|
-
}): void {
|
|
39
|
-
const encoderWithTimestamp = args.rawEncoder as unknown as {
|
|
40
|
-
writeTimestamp?: (querySet: GPUQuerySet, queryIndex: number) => void;
|
|
41
|
-
};
|
|
42
|
-
if (typeof encoderWithTimestamp.writeTimestamp !== 'function') {
|
|
43
|
-
throw new RhiError({
|
|
44
|
-
code: 'webgpu-runtime-error',
|
|
45
|
-
expected: 'underlying GPUCommandEncoder.writeTimestamp to be callable',
|
|
46
|
-
hint: 'timestamp-query is advertised but the raw encoder has no writeTimestamp method',
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
try {
|
|
50
|
-
encoderWithTimestamp.writeTimestamp(args.rawQuerySet, args.queryIndex);
|
|
51
|
-
} catch (error) {
|
|
52
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
53
|
-
throw new RhiError({
|
|
54
|
-
code: 'webgpu-runtime-error',
|
|
55
|
-
expected: 'underlying GPUCommandEncoder.writeTimestamp to succeed',
|
|
56
|
-
hint: `writeTimestamp raised: ${message}`,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"timestamp-query.unit.test.d.ts","sourceRoot":"","sources":["../../../src/internal/__tests__/timestamp-query.unit.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { RhiError } from '@forgeax/engine-rhi';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { createMockGpu } from '../../__tests__/__mocks__/gpu-device';
|
|
4
|
-
import { makeRhiDevice } from '../../device';
|
|
5
|
-
|
|
6
|
-
async function timestampEncoder(writeTimestamp?: (querySet: unknown, queryIndex: number) => void) {
|
|
7
|
-
const gpu = createMockGpu();
|
|
8
|
-
const adapter = await gpu.requestAdapter();
|
|
9
|
-
if (adapter === null) throw new Error('mock adapter should exist');
|
|
10
|
-
const raw = await adapter.requestDevice();
|
|
11
|
-
const features = raw.features as unknown as Set<GPUFeatureName>;
|
|
12
|
-
features.add('timestamp-query');
|
|
13
|
-
const originalCreateCommandEncoder = raw.createCommandEncoder.bind(raw);
|
|
14
|
-
raw.createCommandEncoder = (descriptor) => {
|
|
15
|
-
const encoder = originalCreateCommandEncoder(descriptor) as unknown as Record<string, unknown>;
|
|
16
|
-
if (writeTimestamp !== undefined) encoder.writeTimestamp = writeTimestamp;
|
|
17
|
-
return encoder as unknown as ReturnType<typeof raw.createCommandEncoder>;
|
|
18
|
-
};
|
|
19
|
-
const { device } = makeRhiDevice(raw as unknown as GPUDevice);
|
|
20
|
-
const querySet = device.createQuerySet({ type: 'timestamp', count: 2 });
|
|
21
|
-
if (!querySet.ok) throw new Error('timestamp query set should be created');
|
|
22
|
-
const encoder = device.createCommandEncoder();
|
|
23
|
-
if (!encoder.ok) throw new Error('command encoder should be created');
|
|
24
|
-
return { encoder: encoder.value, querySet: querySet.value };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
describe('timestamp query raw write seam', () => {
|
|
28
|
-
it('forwards a callable raw writeTimestamp exactly once', async () => {
|
|
29
|
-
const calls: Array<{ querySet: unknown; queryIndex: number }> = [];
|
|
30
|
-
const { encoder, querySet } = await timestampEncoder((rawQuerySet, queryIndex) => {
|
|
31
|
-
calls.push({ querySet: rawQuerySet, queryIndex });
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
encoder.writeTimestamp(querySet, 1);
|
|
35
|
-
|
|
36
|
-
expect(calls).toHaveLength(1);
|
|
37
|
-
expect(calls[0]?.querySet).toBeDefined();
|
|
38
|
-
expect(calls[0]?.queryIndex).toBe(1);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('returns a structured refusal when capability-positive raw writeTimestamp is missing', async () => {
|
|
42
|
-
const { encoder, querySet } = await timestampEncoder();
|
|
43
|
-
|
|
44
|
-
expect(() => encoder.writeTimestamp(querySet, 0)).toThrow(RhiError);
|
|
45
|
-
try {
|
|
46
|
-
encoder.writeTimestamp(querySet, 0);
|
|
47
|
-
} catch (error) {
|
|
48
|
-
expect(error).toMatchObject({
|
|
49
|
-
code: 'webgpu-runtime-error',
|
|
50
|
-
expected: 'underlying GPUCommandEncoder.writeTimestamp to be callable',
|
|
51
|
-
});
|
|
52
|
-
expect((error as RhiError).hint).toContain('timestamp-query');
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('returns a structured refusal when raw writeTimestamp throws', async () => {
|
|
57
|
-
const { encoder, querySet } = await timestampEncoder(() => {
|
|
58
|
-
throw new Error('raw timestamp failure');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
expect(() => encoder.writeTimestamp(querySet, 0)).toThrow(RhiError);
|
|
62
|
-
try {
|
|
63
|
-
encoder.writeTimestamp(querySet, 0);
|
|
64
|
-
} catch (error) {
|
|
65
|
-
expect(error).toMatchObject({
|
|
66
|
-
code: 'webgpu-runtime-error',
|
|
67
|
-
expected: 'underlying GPUCommandEncoder.writeTimestamp to succeed',
|
|
68
|
-
});
|
|
69
|
-
expect((error as RhiError).hint).toContain('raw timestamp failure');
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('refuses timestamp writes after encoder.finish with a lifecycle error', async () => {
|
|
74
|
-
const { encoder, querySet } = await timestampEncoder(() => {});
|
|
75
|
-
const finish = encoder.finish();
|
|
76
|
-
expect(finish.ok).toBe(true);
|
|
77
|
-
|
|
78
|
-
expect(() => encoder.writeTimestamp(querySet, 0)).toThrow(RhiError);
|
|
79
|
-
try {
|
|
80
|
-
encoder.writeTimestamp(querySet, 0);
|
|
81
|
-
} catch (error) {
|
|
82
|
-
expect(error).toMatchObject({
|
|
83
|
-
code: 'command-encoder-finished',
|
|
84
|
-
expected: 'command encoder must not be finished before recording new commands',
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
});
|