@forgeax/engine-rhi-webgpu 0.1.23 → 0.1.25
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/dist/device.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +30 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/rhi-webgpu.unit.test.ts +71 -0
- package/src/device.ts +4 -0
- package/src/index.ts +44 -3
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.25",
|
|
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.25",
|
|
26
|
+
"@forgeax/engine-types": "0.1.25",
|
|
27
27
|
"@webgpu/types": "^0.1.71"
|
|
28
28
|
},
|
|
29
29
|
"forgeax": {
|
|
@@ -563,6 +563,77 @@ import { createMockGpu, type MockCapture, makeShaderError } from './__mocks__/gp
|
|
|
563
563
|
}
|
|
564
564
|
|
|
565
565
|
describe('MVP-1.1 runtime — verbatim pass-through of the 5 descriptors', () => {
|
|
566
|
+
afterEach(() => vi.unstubAllGlobals());
|
|
567
|
+
|
|
568
|
+
it('normalizes omitted dynamic Dawn limits from the adapter without mutating the descriptor', async () => {
|
|
569
|
+
const gpu = createMockGpu();
|
|
570
|
+
const descriptor: GPUDeviceDescriptor = { label: 'dynamic-limit-probe' };
|
|
571
|
+
const r = await requestDevice({ gpu, deviceDescriptor: descriptor });
|
|
572
|
+
expect(r.ok).toBe(true);
|
|
573
|
+
expect(descriptor).toEqual({ label: 'dynamic-limit-probe' });
|
|
574
|
+
|
|
575
|
+
const ev = lastOf(gpu.__captured, 'requestDevice');
|
|
576
|
+
expect(ev.options).toMatchObject({
|
|
577
|
+
label: 'dynamic-limit-probe',
|
|
578
|
+
requiredLimits: {
|
|
579
|
+
maxDynamicUniformBuffersPerPipelineLayout: 8,
|
|
580
|
+
maxDynamicStorageBuffersPerPipelineLayout: 4,
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('preserves explicit dynamic limits while filling only the omitted adapter-backed limit', async () => {
|
|
586
|
+
const gpu = createMockGpu();
|
|
587
|
+
const requiredLimits = { maxDynamicUniformBuffersPerPipelineLayout: 2 };
|
|
588
|
+
const r = await requestDevice({ gpu, deviceDescriptor: { requiredLimits } });
|
|
589
|
+
expect(r.ok).toBe(true);
|
|
590
|
+
|
|
591
|
+
const ev = lastOf(gpu.__captured, 'requestDevice');
|
|
592
|
+
expect(ev.options?.requiredLimits).toEqual({
|
|
593
|
+
maxDynamicUniformBuffersPerPipelineLayout: 2,
|
|
594
|
+
maxDynamicStorageBuffersPerPipelineLayout: 4,
|
|
595
|
+
});
|
|
596
|
+
expect(requiredLimits).toEqual({ maxDynamicUniformBuffersPerPipelineLayout: 2 });
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it('clamps oversized dynamic limits to the adapter without mutating the descriptor', async () => {
|
|
600
|
+
const gpu = createMockGpu();
|
|
601
|
+
const requiredLimits = {
|
|
602
|
+
maxDynamicUniformBuffersPerPipelineLayout: 1_000_000,
|
|
603
|
+
maxDynamicStorageBuffersPerPipelineLayout: 1_000_000,
|
|
604
|
+
};
|
|
605
|
+
const r = await requestDevice({ gpu, deviceDescriptor: { requiredLimits } });
|
|
606
|
+
expect(r.ok).toBe(true);
|
|
607
|
+
|
|
608
|
+
const ev = lastOf(gpu.__captured, 'requestDevice');
|
|
609
|
+
expect(ev.options?.requiredLimits).toEqual({
|
|
610
|
+
maxDynamicUniformBuffersPerPipelineLayout: 8,
|
|
611
|
+
maxDynamicStorageBuffersPerPipelineLayout: 4,
|
|
612
|
+
});
|
|
613
|
+
expect(requiredLimits).toEqual({
|
|
614
|
+
maxDynamicUniformBuffersPerPipelineLayout: 1_000_000,
|
|
615
|
+
maxDynamicStorageBuffersPerPipelineLayout: 1_000_000,
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('applies the same normalization on the public adapter two-step path', async () => {
|
|
620
|
+
const gpu = createMockGpu();
|
|
621
|
+
vi.stubGlobal('navigator', { gpu });
|
|
622
|
+
const adapterResult = await requestAdapter();
|
|
623
|
+
expect(adapterResult.ok).toBe(true);
|
|
624
|
+
if (!adapterResult.ok) return;
|
|
625
|
+
|
|
626
|
+
const deviceResult = await adapterResult.value.requestDevice({
|
|
627
|
+
requiredLimits: { maxDynamicStorageBuffersPerPipelineLayout: 1 },
|
|
628
|
+
});
|
|
629
|
+
expect(deviceResult.ok).toBe(true);
|
|
630
|
+
const ev = lastOf(gpu.__captured, 'requestDevice');
|
|
631
|
+
expect(ev.options?.requiredLimits).toEqual({
|
|
632
|
+
maxDynamicUniformBuffersPerPipelineLayout: 8,
|
|
633
|
+
maxDynamicStorageBuffersPerPipelineLayout: 1,
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
|
|
566
637
|
it('BufferDescriptor passes through size / usage / label / mappedAtCreation', async () => {
|
|
567
638
|
const gpu = createMockGpu();
|
|
568
639
|
const r = await requestDevice({ gpu });
|
package/src/device.ts
CHANGED
|
@@ -1525,6 +1525,10 @@ export function makeRhiDevice(rawDevice: GPUDevice): {
|
|
|
1525
1525
|
return probe;
|
|
1526
1526
|
},
|
|
1527
1527
|
queue,
|
|
1528
|
+
// The raw backend owns the only real loss-injection seam. Keep this
|
|
1529
|
+
// Promise as a transparent observation boundary; a test provider must
|
|
1530
|
+
// operate on the backend before this shim and must never be added to the
|
|
1531
|
+
// public RHI surface.
|
|
1528
1532
|
lost: rawDevice.lost as unknown as Promise<{
|
|
1529
1533
|
readonly reason: 'destroyed' | 'unknown';
|
|
1530
1534
|
readonly message: string;
|
package/src/index.ts
CHANGED
|
@@ -76,12 +76,51 @@ export interface GpuLike {
|
|
|
76
76
|
requestAdapter(options?: GPURequestAdapterOptions | undefined): Promise<GpuAdapterLike | null>;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
/** The `GPUAdapter` subset accepted at the provider seam —
|
|
79
|
+
/** The `GPUAdapter` subset accepted at the provider seam — capability limits plus `requestDevice`. */
|
|
80
80
|
export interface GpuAdapterLike {
|
|
81
|
+
readonly limits?: GPUSupportedLimits | undefined;
|
|
81
82
|
// forgeax-async-whitelist: dom-native — spec `GPUAdapter.requestDevice()` raw entry
|
|
82
83
|
requestDevice(descriptor?: GPUDeviceDescriptor | undefined): Promise<GpuDeviceLike>;
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
const DYNAMIC_DEVICE_LIMIT_KEYS = [
|
|
87
|
+
'maxDynamicUniformBuffersPerPipelineLayout',
|
|
88
|
+
'maxDynamicStorageBuffersPerPipelineLayout',
|
|
89
|
+
] as const;
|
|
90
|
+
const SYNTHETIC_DAWN_DYNAMIC_LIMIT_DEFAULT = 1_000_000;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Keep browser-native Dawn from synthesizing the 1,000,000 dynamic-buffer
|
|
94
|
+
* defaults or clamping Dawn's synthetic 1,000,000 request. The adapter's
|
|
95
|
+
* reported values are the only safe values for omitted/defaulted limits: they
|
|
96
|
+
* are finite, supported by this adapter, and do not mutate the caller's
|
|
97
|
+
* descriptor.
|
|
98
|
+
*/
|
|
99
|
+
function normalizeDeviceDescriptor(
|
|
100
|
+
adapter: { readonly limits?: GPUSupportedLimits | undefined },
|
|
101
|
+
descriptor?: GPUDeviceDescriptor | undefined,
|
|
102
|
+
): GPUDeviceDescriptor | undefined {
|
|
103
|
+
const adapterLimits = adapter.limits;
|
|
104
|
+
if (adapterLimits === undefined || adapterLimits === null) return descriptor;
|
|
105
|
+
|
|
106
|
+
const requiredLimits = { ...(descriptor?.requiredLimits ?? {}) };
|
|
107
|
+
let changed = false;
|
|
108
|
+
for (const key of DYNAMIC_DEVICE_LIMIT_KEYS) {
|
|
109
|
+
const value = adapterLimits[key];
|
|
110
|
+
if (!Number.isFinite(value) || value < 1) continue;
|
|
111
|
+
const requested = requiredLimits[key];
|
|
112
|
+
if (
|
|
113
|
+
requested !== undefined &&
|
|
114
|
+
(requested <= value || requested !== SYNTHETIC_DAWN_DYNAMIC_LIMIT_DEFAULT)
|
|
115
|
+
) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
requiredLimits[key] = value;
|
|
119
|
+
changed = true;
|
|
120
|
+
}
|
|
121
|
+
return changed ? { ...(descriptor ?? {}), requiredLimits } : descriptor;
|
|
122
|
+
}
|
|
123
|
+
|
|
85
124
|
/**
|
|
86
125
|
* The `GPUDevice` subset accepted at the provider seam — exactly the fields the
|
|
87
126
|
* shim actually touches (the 5 descriptor `createX` calls + features / limits /
|
|
@@ -196,7 +235,9 @@ export async function requestDevice(
|
|
|
196
235
|
|
|
197
236
|
let rawDevice: GpuDeviceLike;
|
|
198
237
|
try {
|
|
199
|
-
rawDevice = await adapter.requestDevice(
|
|
238
|
+
rawDevice = await adapter.requestDevice(
|
|
239
|
+
normalizeDeviceDescriptor(adapter, opts.deviceDescriptor),
|
|
240
|
+
);
|
|
200
241
|
} catch (e) {
|
|
201
242
|
return classifyRequestDeviceError(e);
|
|
202
243
|
}
|
|
@@ -393,7 +434,7 @@ function makeRhiAdapter(rawAdapter: {
|
|
|
393
434
|
let rawDevice: GpuDeviceLike;
|
|
394
435
|
try {
|
|
395
436
|
rawDevice = (await rawAdapter.requestDevice(
|
|
396
|
-
opts as GPUDeviceDescriptor | undefined,
|
|
437
|
+
normalizeDeviceDescriptor(rawAdapter, opts as GPUDeviceDescriptor | undefined),
|
|
397
438
|
)) as GpuDeviceLike;
|
|
398
439
|
} catch (e) {
|
|
399
440
|
return classifyRequestDeviceError(e);
|