@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/engine-rhi-webgpu",
3
- "version": "0.1.7",
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.7",
26
- "@forgeax/engine-types": "0.1.7",
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 { acquireCanvasContext, createShaderModule, requestAdapter, requestDevice } from '../index';
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
- ): Promise<Result<ShaderModule, RhiError>> {
237
- // In-package reverse lookup of the underlying GPUDevice. After D-S1 the
238
- // function is renamed to `_internal_getRawDevice`; this call is in the
239
- // same package as the WeakMap registry so it is allowed by the AC-08
240
- // grep gate (the gate only restricts cross-package callers).
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
- handle = rawDevice.createShaderModule(mirrored as GPUShaderModuleDescriptor);
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 — fall back to the
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
- return shaderCompileFailed(info.messages);
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