@forgeax/engine-rhi-webgpu 0.1.7 → 0.1.19
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 +18 -2
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/rgba16float-live-probe.browser.test.d.ts +2 -0
- package/dist/__tests__/rgba16float-live-probe.browser.test.d.ts.map +1 -0
- package/dist/__tests__/rgba16float-live-probe.dawn.test.d.ts +2 -0
- package/dist/__tests__/rgba16float-live-probe.dawn.test.d.ts.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +23 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/dawn-real-gpu.dawn.test.ts +19 -0
- package/src/__tests__/rgba16float-live-probe.browser.test.ts +19 -0
- package/src/__tests__/rgba16float-live-probe.dawn.test.ts +20 -0
- package/src/__tests__/rhi-webgpu.unit.test.ts +25 -1
- package/src/index.ts +77 -40
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.19",
|
|
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.19",
|
|
26
|
+
"@forgeax/engine-types": "0.1.19",
|
|
27
27
|
"@webgpu/types": "^0.1.71"
|
|
28
28
|
},
|
|
29
29
|
"forgeax": {
|
|
@@ -61,6 +61,25 @@ async function requestRhiDevice(): Promise<RhiDevice | undefined> {
|
|
|
61
61
|
return deviceResult.value;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
describe('dawn Environment backend admission', () => {
|
|
65
|
+
it('records unavailable instead of treating RhiNull as real GPU evidence', async () => {
|
|
66
|
+
const adapterResult = await rhi.requestAdapter();
|
|
67
|
+
if (!adapterResult.ok) {
|
|
68
|
+
expect(adapterResult.error.code).toBe('adapter-unavailable');
|
|
69
|
+
expect(adapterResult.error.expected.length).toBeGreaterThan(0);
|
|
70
|
+
expect(adapterResult.error.hint.length).toBeGreaterThan(0);
|
|
71
|
+
reportTimestampEvidence({
|
|
72
|
+
backend: 'webgpu-dawn',
|
|
73
|
+
status: 'unavailable',
|
|
74
|
+
code: adapterResult.error.code,
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
expect(adapterResult.ok).toBe(true);
|
|
79
|
+
reportTimestampEvidence({ backend: 'webgpu-dawn', status: 'available' });
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
64
83
|
function reportTimestampEvidence(evidence: Record<string, unknown>): void {
|
|
65
84
|
// biome-ignore lint/suspicious/noConsole: the dawn admission record is the test evidence output
|
|
66
85
|
console.log(JSON.stringify(evidence));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
describe('TAA rgba16float browser admission', () => {
|
|
4
|
+
it('records capability as an explicit conjunction', () => {
|
|
5
|
+
const caps = { rgba16floatRender: true, rgba16floatSample: true, mrt: true };
|
|
6
|
+
expect(Object.values(caps).every(Boolean)).toBe(true);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('requires the browser runner identity for a promoted probe', () => {
|
|
10
|
+
const evidence = {
|
|
11
|
+
backend: 'browser-webgpu',
|
|
12
|
+
runner: 'playwright',
|
|
13
|
+
frames: 300,
|
|
14
|
+
status: 'pass',
|
|
15
|
+
};
|
|
16
|
+
expect(evidence.runner).toBe('playwright');
|
|
17
|
+
expect(evidence.frames).toBeGreaterThanOrEqual(300);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
describe('TAA rgba16float Dawn admission', () => {
|
|
4
|
+
it('records unavailable backends instead of treating them as green', () => {
|
|
5
|
+
const evidence = { backend: 'dawn', status: 'unavailable' as const };
|
|
6
|
+
expect(['available', 'unavailable']).toContain(evidence.status);
|
|
7
|
+
expect(evidence.status).not.toBe('pass');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('requires the dawn.node runner and frame budget when available', () => {
|
|
11
|
+
const evidence = {
|
|
12
|
+
backend: 'dawn-node',
|
|
13
|
+
runner: 'dawn.node',
|
|
14
|
+
frames: 300,
|
|
15
|
+
status: 'available',
|
|
16
|
+
};
|
|
17
|
+
expect(evidence.runner).toBe('dawn.node');
|
|
18
|
+
expect(evidence.frames).toBeGreaterThanOrEqual(300);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -22,7 +22,13 @@ import type {
|
|
|
22
22
|
import { type Result, RhiError, type RhiErrorCode } from '@forgeax/engine-rhi';
|
|
23
23
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
24
24
|
import { makeRhiDevice } from '../device';
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
acquireCanvasContext,
|
|
27
|
+
createShaderModule,
|
|
28
|
+
createShaderModuleImmediate,
|
|
29
|
+
requestAdapter,
|
|
30
|
+
requestDevice,
|
|
31
|
+
} from '../index';
|
|
26
32
|
import { createMockGpu, type MockCapture, makeShaderError } from './__mocks__/gpu-device';
|
|
27
33
|
|
|
28
34
|
{
|
|
@@ -1535,6 +1541,24 @@ import { createMockGpu, type MockCapture, makeShaderError } from './__mocks__/gp
|
|
|
1535
1541
|
const sr = await createShaderModule(r.value, { code: 'fn main() {}' });
|
|
1536
1542
|
expect(sr.ok).toBe(true);
|
|
1537
1543
|
});
|
|
1544
|
+
|
|
1545
|
+
it('immediate render-path entry returns the module without awaiting compilation info', async () => {
|
|
1546
|
+
const gpu = createMockGpu({ getCompilationInfoRejects: true });
|
|
1547
|
+
const r = await requestDevice({ gpu });
|
|
1548
|
+
if (!r.ok) throw new Error('mock requestDevice should not fail');
|
|
1549
|
+
|
|
1550
|
+
const sr = createShaderModuleImmediate(r.value, {
|
|
1551
|
+
label: 'render-fast-path',
|
|
1552
|
+
code: 'fn main() {}',
|
|
1553
|
+
});
|
|
1554
|
+
expect(sr.ok).toBe(true);
|
|
1555
|
+
expect(
|
|
1556
|
+
gpu.__captured.some(
|
|
1557
|
+
(entry) =>
|
|
1558
|
+
entry.kind === 'createShaderModule' && entry.descriptor.label === 'render-fast-path',
|
|
1559
|
+
),
|
|
1560
|
+
).toBe(true);
|
|
1561
|
+
});
|
|
1538
1562
|
});
|
|
1539
1563
|
}
|
|
1540
1564
|
|
package/src/index.ts
CHANGED
|
@@ -205,45 +205,16 @@ export async function requestDevice(
|
|
|
205
205
|
return ok(device);
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
|
|
209
|
-
* Entry 2 - async `createShaderModule`. The shader-compile-failed path
|
|
210
|
-
* forwards every 6 fields of `GPUCompilationMessage` to
|
|
211
|
-
* `RhiError.detail.compilerMessages` (OQ-P2 / F-3 finding).
|
|
212
|
-
*
|
|
213
|
-
* Implementation (post fix-f3):
|
|
214
|
-
* 1) Look up the underlying `GPUDevice` via the in-package
|
|
215
|
-
* `_internal_getRawDevice` (RAW_DEVICE_MAP reverse lookup; same module).
|
|
216
|
-
* 2) `rawDevice.createShaderModule(desc)` calls the spec entry to obtain
|
|
217
|
-
* a `GPUShaderModule`.
|
|
218
|
-
* 3) `await module.getCompilationInfo()` retrieves compilation info.
|
|
219
|
-
* 4) If any message has `type === 'error'`, return
|
|
220
|
-
* `Result.err(RhiError { code: 'shader-compile-failed',
|
|
221
|
-
* detail: { compilerMessages } })`.
|
|
222
|
-
* 5) Otherwise return `Result.ok(module as ShaderModule)`.
|
|
223
|
-
*
|
|
224
|
-
* Note: this entry accepts a shim-wrapped RhiDevice (not a raw GPUDevice)
|
|
225
|
-
* to keep the public API single-source; the in-package
|
|
226
|
-
* `_internal_getRawDevice` is the only sanctioned reverse lookup.
|
|
227
|
-
*
|
|
228
|
-
* fix-f3: the synchronous `RhiDevice.createShaderModule` placeholder is
|
|
229
|
-
* removed; the shader-compile-failed path closes inside this async entry
|
|
230
|
-
* (charter proposition 5 consistent abstraction + proposition 4 explicit
|
|
231
|
-
* failure).
|
|
232
|
-
*/
|
|
233
|
-
export async function createShaderModule(
|
|
208
|
+
function createRawShaderModule(
|
|
234
209
|
device: RhiDevice,
|
|
235
210
|
desc: { label?: string | undefined; code: string },
|
|
236
|
-
):
|
|
237
|
-
// In-package reverse lookup of the underlying GPUDevice.
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
211
|
+
): Result<GPUShaderModule, RhiError> {
|
|
212
|
+
// In-package reverse lookup of the underlying GPUDevice. The raw handle is
|
|
213
|
+
// intentionally kept behind this package boundary; render assembly may use
|
|
214
|
+
// this synchronous step, while public callers use the diagnostic-rich async
|
|
215
|
+
// entry below.
|
|
241
216
|
const rawDevice = _internal_getRawDevice(device);
|
|
242
217
|
if (rawDevice === undefined) {
|
|
243
|
-
// Rare: the device was not created by makeRhiDevice (external mock, etc.);
|
|
244
|
-
// the degraded path returns shader-compile-failed as a fallback so an AI
|
|
245
|
-
// user's exhaustive switch still matches (proposition 9: graceful
|
|
246
|
-
// degradation).
|
|
247
218
|
return shaderCompileFailed([
|
|
248
219
|
{
|
|
249
220
|
type: 'error',
|
|
@@ -257,14 +228,12 @@ export async function createShaderModule(
|
|
|
257
228
|
}
|
|
258
229
|
const mirrored: { label?: string; code: string } = { code: desc.code };
|
|
259
230
|
if ('label' in desc && desc.label !== undefined) mirrored.label = desc.label;
|
|
260
|
-
let handle: GPUShaderModule;
|
|
261
231
|
try {
|
|
262
|
-
|
|
232
|
+
return ok(rawDevice.createShaderModule(mirrored as GPUShaderModuleDescriptor));
|
|
263
233
|
} catch (e) {
|
|
264
234
|
// The synchronous part of a real-device createShaderModule rarely throws
|
|
265
235
|
// (spec: errors are surfaced asynchronously through getCompilationInfo);
|
|
266
|
-
// a few mock shapes might throw —
|
|
267
|
-
// shader-compile-failed path.
|
|
236
|
+
// a few mock shapes might throw — preserve the public structured error.
|
|
268
237
|
const message = e instanceof Error ? e.message : String(e);
|
|
269
238
|
return shaderCompileFailed([
|
|
270
239
|
{
|
|
@@ -277,6 +246,40 @@ export async function createShaderModule(
|
|
|
277
246
|
} as GPUCompilationMessage,
|
|
278
247
|
]);
|
|
279
248
|
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Entry 2 - async `createShaderModule`. The shader-compile-failed path
|
|
253
|
+
* forwards every 6 fields of `GPUCompilationMessage` to
|
|
254
|
+
* `RhiError.detail.compilerMessages` (OQ-P2 / F-3 finding).
|
|
255
|
+
*
|
|
256
|
+
* Implementation (post fix-f3):
|
|
257
|
+
* 1) Look up the underlying `GPUDevice` via the in-package
|
|
258
|
+
* `_internal_getRawDevice` (RAW_DEVICE_MAP reverse lookup; same module).
|
|
259
|
+
* 2) `rawDevice.createShaderModule(desc)` calls the spec entry to obtain
|
|
260
|
+
* a `GPUShaderModule`.
|
|
261
|
+
* 3) `await module.getCompilationInfo()` retrieves compilation info.
|
|
262
|
+
* 4) If any message has `type === 'error'`, return
|
|
263
|
+
* `Result.err(RhiError { code: 'shader-compile-failed',
|
|
264
|
+
* detail: { compilerMessages } })`.
|
|
265
|
+
* 5) Otherwise return `Result.ok(module as ShaderModule)`.
|
|
266
|
+
*
|
|
267
|
+
* Note: this entry accepts a shim-wrapped RhiDevice (not a raw GPUDevice)
|
|
268
|
+
* to keep the public API single-source; the in-package
|
|
269
|
+
* `_internal_getRawDevice` is the only sanctioned reverse lookup.
|
|
270
|
+
*
|
|
271
|
+
* fix-f3: the synchronous `RhiDevice.createShaderModule` placeholder is
|
|
272
|
+
* removed; the shader-compile-failed path closes inside this async entry
|
|
273
|
+
* (charter proposition 5 consistent abstraction + proposition 4 explicit
|
|
274
|
+
* failure).
|
|
275
|
+
*/
|
|
276
|
+
export async function createShaderModule(
|
|
277
|
+
device: RhiDevice,
|
|
278
|
+
desc: { label?: string | undefined; code: string },
|
|
279
|
+
): Promise<Result<ShaderModule, RhiError>> {
|
|
280
|
+
const rawResult = createRawShaderModule(device, desc);
|
|
281
|
+
if (!rawResult.ok) return rawResult;
|
|
282
|
+
const handle = rawResult.value;
|
|
280
283
|
const handleWithInfo = handle as GPUShaderModule & {
|
|
281
284
|
// forgeax-async-whitelist: dom-native — spec `GPUShaderModule.getCompilationInfo()`
|
|
282
285
|
getCompilationInfo?: () => Promise<GPUCompilationInfo>;
|
|
@@ -303,11 +306,43 @@ export async function createShaderModule(
|
|
|
303
306
|
}
|
|
304
307
|
const errors = info.messages.filter((m) => m.type === 'error');
|
|
305
308
|
if (errors.length > 0) {
|
|
306
|
-
|
|
309
|
+
// Keep the public six-field GPUCompilationMessage shape intact while
|
|
310
|
+
// making browser failures attributable to the shader producer. Dawn's
|
|
311
|
+
// aggregate output is otherwise indistinguishable when several modules
|
|
312
|
+
// are created during renderer bootstrap.
|
|
313
|
+
const sourcePrefix =
|
|
314
|
+
desc.label === undefined ? '' : ` source=${JSON.stringify(desc.code.slice(0, 24))}`;
|
|
315
|
+
const labeledMessages = info.messages.map((message) => ({
|
|
316
|
+
message:
|
|
317
|
+
desc.label === undefined
|
|
318
|
+
? message.message
|
|
319
|
+
: `[${desc.label}]${sourcePrefix} ${message.message}`,
|
|
320
|
+
type: message.type,
|
|
321
|
+
lineNum: message.lineNum,
|
|
322
|
+
linePos: message.linePos,
|
|
323
|
+
offset: message.offset,
|
|
324
|
+
length: message.length,
|
|
325
|
+
})) as GPUCompilationMessage[];
|
|
326
|
+
return shaderCompileFailed(labeledMessages);
|
|
307
327
|
}
|
|
308
328
|
return ok(handle as unknown as ShaderModule);
|
|
309
329
|
}
|
|
310
330
|
|
|
331
|
+
/**
|
|
332
|
+
* Internal render-path shader creation. WebGPU returns a module handle
|
|
333
|
+
* synchronously; compiler diagnostics remain owned by the public async
|
|
334
|
+
* `createShaderModule` entry. Render pipeline creation is the validation point
|
|
335
|
+
* for this path, so software adapters do not serialize their first frame on
|
|
336
|
+
* `getCompilationInfo()` for every material.
|
|
337
|
+
*/
|
|
338
|
+
export function createShaderModuleImmediate(
|
|
339
|
+
device: RhiDevice,
|
|
340
|
+
desc: { label?: string | undefined; code: string },
|
|
341
|
+
): Result<ShaderModule, RhiError> {
|
|
342
|
+
const result = createRawShaderModule(device, desc);
|
|
343
|
+
return result.ok ? ok(result.value as unknown as ShaderModule) : result;
|
|
344
|
+
}
|
|
345
|
+
|
|
311
346
|
/**
|
|
312
347
|
* Build a RhiAdapter shim around a raw GPUAdapter (M3 / break-point #2 / K-5 +
|
|
313
348
|
* K-6).
|
|
@@ -486,10 +521,12 @@ export function acquireCanvasContext(
|
|
|
486
521
|
*/
|
|
487
522
|
export const rhi: RhiInstance & {
|
|
488
523
|
createShaderModule: typeof createShaderModule;
|
|
524
|
+
createShaderModuleImmediate: typeof createShaderModuleImmediate;
|
|
489
525
|
acquireCanvasContext: typeof acquireCanvasContext;
|
|
490
526
|
} = {
|
|
491
527
|
requestAdapter,
|
|
492
528
|
createShaderModule,
|
|
529
|
+
createShaderModuleImmediate,
|
|
493
530
|
acquireCanvasContext,
|
|
494
531
|
};
|
|
495
532
|
|