@forgeax/engine-rhi-webgpu 0.1.20 → 0.1.23
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 +15 -7
- package/dist/__tests__/r32float-capability-generation.integration.test.d.ts +2 -0
- package/dist/__tests__/r32float-capability-generation.integration.test.d.ts.map +1 -0
- package/dist/__tests__/r32float-format-capability.browser.test.d.ts +2 -0
- package/dist/__tests__/r32float-format-capability.browser.test.d.ts.map +1 -0
- package/dist/__tests__/r32float-format-capability.dawn.test.d.ts +2 -0
- package/dist/__tests__/r32float-format-capability.dawn.test.d.ts.map +1 -0
- package/dist/device.d.ts.map +1 -1
- package/dist/index.mjs +159 -43
- package/dist/index.mjs.map +1 -1
- package/dist/internal/r32float-capability.d.ts +6 -0
- package/dist/internal/r32float-capability.d.ts.map +1 -0
- 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 +170 -3
- package/src/__tests__/r32float-capability-generation.integration.test.ts +35 -0
- package/src/__tests__/r32float-format-capability.browser.test.ts +22 -0
- package/src/__tests__/r32float-format-capability.dawn.test.ts +31 -0
- package/src/__tests__/rhi-webgpu.unit.test.ts +73 -101
- package/src/device.ts +49 -54
- package/src/internal/__tests__/timestamp-query.unit.test.ts +110 -78
- package/src/internal/r32float-capability.ts +153 -0
- package/src/internal/timestamp-query.ts +0 -27
- package/dist/.tsbuildinfo +0 -1
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type Result, RhiError, type RhiTextureFormatCapabilityReceipt } from '@forgeax/engine-rhi';
|
|
2
|
+
type RawDevice = GPUDevice;
|
|
3
|
+
/** Execute the complete real-device r32float profile without exposing raw handles. */
|
|
4
|
+
export declare function probeR32FloatCapability(rawDevice: RawDevice, deviceGeneration: number): Promise<Result<RhiTextureFormatCapabilityReceipt, RhiError>>;
|
|
5
|
+
export {};
|
|
6
|
+
//# sourceMappingURL=r32float-capability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"r32float-capability.d.ts","sourceRoot":"","sources":["../../src/internal/r32float-capability.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,MAAM,EACX,QAAQ,EACR,KAAK,iCAAiC,EAGvC,MAAM,qBAAqB,CAAC;AAS7B,KAAK,SAAS,GAAG,SAAS,CAAC;AAE3B,sFAAsF;AACtF,wBAAsB,uBAAuB,CAC3C,SAAS,EAAE,SAAS,EACpB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,MAAM,CAAC,iCAAiC,EAAE,QAAQ,CAAC,CAAC,CA+H9D"}
|
|
@@ -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.23",
|
|
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.23",
|
|
26
|
+
"@forgeax/engine-types": "0.1.23",
|
|
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();
|
|
@@ -1277,6 +1276,20 @@ describe('w3 dawn-real-gpu timestamp admission path', () => {
|
|
|
1277
1276
|
if (!deviceResult.ok) return;
|
|
1278
1277
|
const device = deviceResult.value;
|
|
1279
1278
|
|
|
1279
|
+
const { createShaderModule } = await import('../index');
|
|
1280
|
+
const shaderResult = await createShaderModule(device, {
|
|
1281
|
+
code: '@compute @workgroup_size(1) fn timestamp_probe() {}',
|
|
1282
|
+
});
|
|
1283
|
+
expect(shaderResult.ok).toBe(true);
|
|
1284
|
+
if (!shaderResult.ok) return;
|
|
1285
|
+
const pipelineResult = device.createComputePipeline({
|
|
1286
|
+
label: 'w3-timestamp-compute-pipeline',
|
|
1287
|
+
layout: 'auto',
|
|
1288
|
+
compute: { module: shaderResult.value, entryPoint: 'timestamp_probe' },
|
|
1289
|
+
});
|
|
1290
|
+
expect(pipelineResult.ok).toBe(true);
|
|
1291
|
+
if (!pipelineResult.ok) return;
|
|
1292
|
+
|
|
1280
1293
|
const querySetResult = device.createQuerySet({ type: 'timestamp', count: 2 });
|
|
1281
1294
|
expect(querySetResult.ok).toBe(true);
|
|
1282
1295
|
if (!querySetResult.ok) return;
|
|
@@ -1311,6 +1324,8 @@ describe('w3 dawn-real-gpu timestamp admission path', () => {
|
|
|
1311
1324
|
endOfPassWriteIndex: 1,
|
|
1312
1325
|
},
|
|
1313
1326
|
});
|
|
1327
|
+
pass.setPipeline(pipelineResult.value);
|
|
1328
|
+
pass.dispatchWorkgroups(1);
|
|
1314
1329
|
pass.end();
|
|
1315
1330
|
} catch (error) {
|
|
1316
1331
|
const refusal = error as { code?: string; expected?: string; hint?: string };
|
|
@@ -1382,6 +1397,158 @@ describe('w3 dawn-real-gpu timestamp admission path', () => {
|
|
|
1382
1397
|
expect(end).toBeGreaterThan(begin);
|
|
1383
1398
|
mappedResult.value.unmap();
|
|
1384
1399
|
});
|
|
1400
|
+
|
|
1401
|
+
it('runs render-pass timestampWrites -> resolve -> submit -> readback or records a structured refusal', async () => {
|
|
1402
|
+
const selector = 'standard' as const;
|
|
1403
|
+
const source = 'packages/rhi-webgpu/src/__tests__/dawn-real-gpu.dawn.test.ts';
|
|
1404
|
+
const adapterResult = await rhi.requestAdapter();
|
|
1405
|
+
expect(adapterResult.ok).toBe(true);
|
|
1406
|
+
if (!adapterResult.ok) return;
|
|
1407
|
+
|
|
1408
|
+
if (!adapterResult.value.features.has('timestamp-query')) {
|
|
1409
|
+
reportTimestampEvidence({
|
|
1410
|
+
carrier: 'dawn-node',
|
|
1411
|
+
selector,
|
|
1412
|
+
source,
|
|
1413
|
+
acceptedGpu: 0,
|
|
1414
|
+
refusalCode: 'timestamp-query-unsupported',
|
|
1415
|
+
expected: "adapter.features.has('timestamp-query')",
|
|
1416
|
+
hint: 'request a real Dawn device with the timestamp-query feature enabled',
|
|
1417
|
+
});
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
const deviceResult = await adapterResult.value.requestDevice({
|
|
1422
|
+
requiredFeatures: ['timestamp-query'],
|
|
1423
|
+
});
|
|
1424
|
+
expect(deviceResult.ok).toBe(true);
|
|
1425
|
+
if (!deviceResult.ok) return;
|
|
1426
|
+
const device = deviceResult.value;
|
|
1427
|
+
|
|
1428
|
+
const querySetResult = device.createQuerySet({ type: 'timestamp', count: 2 });
|
|
1429
|
+
expect(querySetResult.ok).toBe(true);
|
|
1430
|
+
if (!querySetResult.ok) return;
|
|
1431
|
+
|
|
1432
|
+
const targetResult = device.createTexture({
|
|
1433
|
+
label: 'w3-timestamp-raster-target',
|
|
1434
|
+
size: { width: 1, height: 1, depthOrArrayLayers: 1 },
|
|
1435
|
+
format: 'rgba8unorm',
|
|
1436
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
1437
|
+
});
|
|
1438
|
+
expect(targetResult.ok).toBe(true);
|
|
1439
|
+
if (!targetResult.ok) return;
|
|
1440
|
+
const viewResult = device.createTextureView(targetResult.value, {});
|
|
1441
|
+
expect(viewResult.ok).toBe(true);
|
|
1442
|
+
if (!viewResult.ok) return;
|
|
1443
|
+
|
|
1444
|
+
const resolveResult = device.createBuffer({
|
|
1445
|
+
label: 'w3-timestamp-raster-resolve',
|
|
1446
|
+
size: 256,
|
|
1447
|
+
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC,
|
|
1448
|
+
});
|
|
1449
|
+
expect(resolveResult.ok).toBe(true);
|
|
1450
|
+
if (!resolveResult.ok) return;
|
|
1451
|
+
|
|
1452
|
+
const readbackResult = device.createBuffer({
|
|
1453
|
+
label: 'w3-timestamp-raster-readback',
|
|
1454
|
+
size: 256,
|
|
1455
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
|
|
1456
|
+
});
|
|
1457
|
+
expect(readbackResult.ok).toBe(true);
|
|
1458
|
+
if (!readbackResult.ok) return;
|
|
1459
|
+
|
|
1460
|
+
const encoderResult = device.createCommandEncoder({ label: 'w3-timestamp-raster-encoder' });
|
|
1461
|
+
expect(encoderResult.ok).toBe(true);
|
|
1462
|
+
if (!encoderResult.ok) return;
|
|
1463
|
+
const encoder = encoderResult.value;
|
|
1464
|
+
|
|
1465
|
+
try {
|
|
1466
|
+
const pass = encoder.beginRenderPass({
|
|
1467
|
+
label: 'timestamp-raster',
|
|
1468
|
+
colorAttachments: [
|
|
1469
|
+
{
|
|
1470
|
+
view: viewResult.value,
|
|
1471
|
+
loadOp: 'clear',
|
|
1472
|
+
storeOp: 'store',
|
|
1473
|
+
},
|
|
1474
|
+
],
|
|
1475
|
+
timestampWrites: {
|
|
1476
|
+
querySet: querySetResult.value,
|
|
1477
|
+
beginningOfPassWriteIndex: 0,
|
|
1478
|
+
endOfPassWriteIndex: 1,
|
|
1479
|
+
},
|
|
1480
|
+
});
|
|
1481
|
+
pass.end();
|
|
1482
|
+
} catch (error) {
|
|
1483
|
+
const refusal = error as { code?: string; expected?: string; hint?: string };
|
|
1484
|
+
reportTimestampEvidence({
|
|
1485
|
+
carrier: 'dawn-node',
|
|
1486
|
+
selector,
|
|
1487
|
+
source,
|
|
1488
|
+
acceptedGpu: 0,
|
|
1489
|
+
refusalCode: 'timestamp-write-unavailable',
|
|
1490
|
+
expected:
|
|
1491
|
+
refusal.expected ??
|
|
1492
|
+
'GPURenderPassDescriptor.timestampWrites to be accepted by the real render pass',
|
|
1493
|
+
hint: refusal.hint ?? String(error),
|
|
1494
|
+
});
|
|
1495
|
+
expect(refusal.code).toBe('webgpu-runtime-error');
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
const resolveQueriesResult = encoder.resolveQuerySet(
|
|
1500
|
+
querySetResult.value,
|
|
1501
|
+
0,
|
|
1502
|
+
2,
|
|
1503
|
+
resolveResult.value,
|
|
1504
|
+
0,
|
|
1505
|
+
);
|
|
1506
|
+
expect(resolveQueriesResult.ok).toBe(true);
|
|
1507
|
+
if (!resolveQueriesResult.ok) return;
|
|
1508
|
+
encoder.copyBufferToBuffer(resolveResult.value, 0, readbackResult.value, 0, 16);
|
|
1509
|
+
|
|
1510
|
+
const finishResult = encoder.finish();
|
|
1511
|
+
expect(finishResult.ok).toBe(true);
|
|
1512
|
+
if (!finishResult.ok) return;
|
|
1513
|
+
const submitResult = device.queue.submit([finishResult.value]);
|
|
1514
|
+
expect(submitResult.ok).toBe(true);
|
|
1515
|
+
if (!submitResult.ok) return;
|
|
1516
|
+
await device.queue.onSubmittedWorkDone();
|
|
1517
|
+
|
|
1518
|
+
const mappedResult = await readbackResult.value.mapAsync(GPUMapMode.READ);
|
|
1519
|
+
expect(mappedResult.ok).toBe(true);
|
|
1520
|
+
if (!mappedResult.ok) return;
|
|
1521
|
+
const rangeResult = mappedResult.value.getMappedRange(0, 16);
|
|
1522
|
+
expect(rangeResult.ok).toBe(true);
|
|
1523
|
+
if (!rangeResult.ok) return;
|
|
1524
|
+
const ticks = new BigUint64Array(rangeResult.value);
|
|
1525
|
+
const begin = ticks[0];
|
|
1526
|
+
const end = ticks[1];
|
|
1527
|
+
if (begin === undefined || end === undefined || end <= begin) {
|
|
1528
|
+
reportTimestampEvidence({
|
|
1529
|
+
carrier: 'dawn-node',
|
|
1530
|
+
selector,
|
|
1531
|
+
source,
|
|
1532
|
+
acceptedGpu: 0,
|
|
1533
|
+
refusalCode: 'timestamp-write-unavailable',
|
|
1534
|
+
expected: 'end > begin',
|
|
1535
|
+
hint: 'timestamp readback did not produce a positive GPU interval for a render pass',
|
|
1536
|
+
observed: { begin: begin?.toString() ?? null, end: end?.toString() ?? null },
|
|
1537
|
+
});
|
|
1538
|
+
mappedResult.value.unmap();
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
reportTimestampEvidence({
|
|
1543
|
+
carrier: 'dawn-node',
|
|
1544
|
+
selector,
|
|
1545
|
+
source,
|
|
1546
|
+
acceptedGpu: 1,
|
|
1547
|
+
ticks: { begin: begin.toString(), end: end.toString() },
|
|
1548
|
+
});
|
|
1549
|
+
expect(end).toBeGreaterThan(begin);
|
|
1550
|
+
mappedResult.value.unmap();
|
|
1551
|
+
});
|
|
1385
1552
|
});
|
|
1386
1553
|
|
|
1387
1554
|
describe('w36 (M5) - dawn-real-gpu RhiQueue.writeTexture real-path', () => {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { rhi } from '../index';
|
|
3
|
+
|
|
4
|
+
const hasGpu = globalThis.navigator?.gpu !== undefined;
|
|
5
|
+
|
|
6
|
+
describe.skipIf(!hasGpu)('WebGPU r32float probe generation budget', () => {
|
|
7
|
+
it('reuses one receipt within a generation and probes a replacement', async () => {
|
|
8
|
+
const adapterResult = await rhi.requestAdapter();
|
|
9
|
+
expect(adapterResult.ok).toBe(true);
|
|
10
|
+
if (!adapterResult.ok) return;
|
|
11
|
+
const firstDeviceResult = await adapterResult.value.requestDevice();
|
|
12
|
+
expect(firstDeviceResult.ok).toBe(true);
|
|
13
|
+
if (!firstDeviceResult.ok) return;
|
|
14
|
+
|
|
15
|
+
const first = firstDeviceResult.value;
|
|
16
|
+
const firstProbe = first.probeTextureFormatCapability();
|
|
17
|
+
expect(first.probeTextureFormatCapability()).toBe(firstProbe);
|
|
18
|
+
const firstReceipt = await firstProbe;
|
|
19
|
+
expect(firstReceipt.ok).toBe(true);
|
|
20
|
+
if (!firstReceipt.ok) return;
|
|
21
|
+
expect(firstReceipt.value.probeExecutions).toBe(1);
|
|
22
|
+
|
|
23
|
+
const replacementAdapterResult = await rhi.requestAdapter();
|
|
24
|
+
expect(replacementAdapterResult.ok).toBe(true);
|
|
25
|
+
if (!replacementAdapterResult.ok) return;
|
|
26
|
+
const replacementResult = await replacementAdapterResult.value.requestDevice();
|
|
27
|
+
expect(replacementResult.ok).toBe(true);
|
|
28
|
+
if (!replacementResult.ok) return;
|
|
29
|
+
const replacementReceipt = await replacementResult.value.probeTextureFormatCapability();
|
|
30
|
+
expect(replacementReceipt.ok).toBe(true);
|
|
31
|
+
if (!replacementReceipt.ok) return;
|
|
32
|
+
expect(replacementReceipt.value.deviceGeneration).not.toBe(firstReceipt.value.deviceGeneration);
|
|
33
|
+
expect(replacementReceipt.value.probeExecutions).toBe(1);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { rhi } from '../index';
|
|
3
|
+
|
|
4
|
+
describe('WebGPU r32float format profile', () => {
|
|
5
|
+
it('executes every profile stage and returns readback evidence', async () => {
|
|
6
|
+
const adapterResult = await rhi.requestAdapter();
|
|
7
|
+
expect(adapterResult.ok).toBe(true);
|
|
8
|
+
if (!adapterResult.ok) return;
|
|
9
|
+
const deviceResult = await adapterResult.value.requestDevice();
|
|
10
|
+
expect(deviceResult.ok).toBe(true);
|
|
11
|
+
if (!deviceResult.ok) return;
|
|
12
|
+
|
|
13
|
+
const receiptResult = await deviceResult.value.probeTextureFormatCapability();
|
|
14
|
+
expect(receiptResult.ok).toBe(true);
|
|
15
|
+
if (!receiptResult.ok) return;
|
|
16
|
+
expect(receiptResult.value.profile).toBe('r32float-mip-sampled-storage');
|
|
17
|
+
expect(receiptResult.value.verdict).toBe('admitted');
|
|
18
|
+
expect(receiptResult.value.evidence).toBe('real');
|
|
19
|
+
expect(receiptResult.value.stages).toHaveLength(8);
|
|
20
|
+
expect(receiptResult.value.readback?.byteLength).toBeGreaterThan(0);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { rhi } from '../index';
|
|
3
|
+
|
|
4
|
+
describe('Dawn r32float format profile', () => {
|
|
5
|
+
it('executes the independent full chain through completion and readback', async () => {
|
|
6
|
+
const adapterResult = await rhi.requestAdapter();
|
|
7
|
+
expect(adapterResult.ok).toBe(true);
|
|
8
|
+
if (!adapterResult.ok) return;
|
|
9
|
+
const deviceResult = await adapterResult.value.requestDevice();
|
|
10
|
+
expect(deviceResult.ok).toBe(true);
|
|
11
|
+
if (!deviceResult.ok) return;
|
|
12
|
+
|
|
13
|
+
const receiptResult = await deviceResult.value.probeTextureFormatCapability();
|
|
14
|
+
expect(receiptResult.ok).toBe(true);
|
|
15
|
+
if (!receiptResult.ok) return;
|
|
16
|
+
expect(receiptResult.value.profile).toBe('r32float-mip-sampled-storage');
|
|
17
|
+
expect(receiptResult.value.verdict).toBe('admitted');
|
|
18
|
+
expect(receiptResult.value.evidence).toBe('real');
|
|
19
|
+
expect(receiptResult.value.stages.map((stage) => stage.stage)).toEqual([
|
|
20
|
+
'texture-create',
|
|
21
|
+
'mip-view',
|
|
22
|
+
'sampled-storage-bind-group',
|
|
23
|
+
'pipeline-bind',
|
|
24
|
+
'finish',
|
|
25
|
+
'submit',
|
|
26
|
+
'completion',
|
|
27
|
+
'readback',
|
|
28
|
+
]);
|
|
29
|
+
expect(receiptResult.value.readback?.values.length).toBeGreaterThan(0);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -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,42 +452,13 @@ import { createMockGpu, type MockCapture, makeShaderError } from './__mocks__/gp
|
|
|
384
452
|
expect(device.destroyBuffer(readbackResult.value).ok).toBe(true);
|
|
385
453
|
});
|
|
386
454
|
});
|
|
387
|
-
|
|
388
455
|
// ---------------------------------------------------------------------------
|
|
389
|
-
// w38 (M5
|
|
456
|
+
// w38 (M5) - pass descriptor timestampWrites forwarding.
|
|
390
457
|
// ---------------------------------------------------------------------------
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
|
|
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
|
-
|
|
458
|
+
// Current Dawn rejects the obsolete command-encoder writeTimestamp method.
|
|
459
|
+
// Timestamp ownership therefore enters through the real render/compute pass
|
|
460
|
+
// descriptor, where the shim maps the opaque QuerySet to its raw handle.
|
|
461
|
+
describe('w38 (M5) - pass descriptor timestampWrites', () => {
|
|
423
462
|
it('maps an opaque QuerySet in compute-pass timestampWrites to the raw descriptor', async () => {
|
|
424
463
|
const gpu = createMockGpu();
|
|
425
464
|
const adapter = await gpu.requestAdapter();
|
|
@@ -476,73 +515,6 @@ import { createMockGpu, type MockCapture, makeShaderError } from './__mocks__/gp
|
|
|
476
515
|
expect(rawQuerySet).toBeDefined();
|
|
477
516
|
expect(captured?.timestampWrites?.querySet).toBe(rawQuerySet);
|
|
478
517
|
});
|
|
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
518
|
});
|
|
547
519
|
}
|
|
548
520
|
|