@forgeax/engine-rhi-webgpu 0.1.6 → 0.1.7

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.6",
3
+ "version": "0.1.7",
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.6",
26
- "@forgeax/engine-types": "0.1.6",
25
+ "@forgeax/engine-rhi": "0.1.7",
26
+ "@forgeax/engine-types": "0.1.7",
27
27
  "@webgpu/types": "^0.1.71"
28
28
  },
29
29
  "forgeax": {
@@ -20,9 +20,9 @@ import type {
20
20
  RhiCanvasContext,
21
21
  } from '@forgeax/engine-rhi';
22
22
  import { type Result, RhiError, type RhiErrorCode } from '@forgeax/engine-rhi';
23
- import { describe, expect, it } from 'vitest';
23
+ import { afterEach, describe, expect, it, vi } from 'vitest';
24
24
  import { makeRhiDevice } from '../device';
25
- import { acquireCanvasContext, createShaderModule, requestDevice } from '../index';
25
+ import { acquireCanvasContext, createShaderModule, requestAdapter, requestDevice } from '../index';
26
26
  import { createMockGpu, type MockCapture, makeShaderError } from './__mocks__/gpu-device';
27
27
 
28
28
  {
@@ -1384,6 +1384,8 @@ import { createMockGpu, type MockCapture, makeShaderError } from './__mocks__/gp
1384
1384
  }
1385
1385
 
1386
1386
  describe('AC-10 — 4 error paths .code / .expected / .hint three-field assertions', () => {
1387
+ afterEach(() => vi.unstubAllGlobals());
1388
+
1387
1389
  it('adapter null -> code=adapter-unavailable + three non-empty string fields', async () => {
1388
1390
  const gpu = createMockGpu({ adapterNull: true });
1389
1391
  const e = unwrapErr(await requestDevice({ gpu }));
@@ -1395,6 +1397,27 @@ import { createMockGpu, type MockCapture, makeShaderError } from './__mocks__/gp
1395
1397
  expect(e.hint.length).toBeGreaterThan(0);
1396
1398
  });
1397
1399
 
1400
+ it('requestAdapter throw keeps the original failure instead of claiming adapter-unavailable', async () => {
1401
+ vi.stubGlobal('navigator', {
1402
+ gpu: {
1403
+ requestAdapter: async () => {
1404
+ throw new DOMException('WebGPU permission policy denied access', 'SecurityError');
1405
+ },
1406
+ },
1407
+ });
1408
+
1409
+ const e = unwrapErr(await requestAdapter());
1410
+ expect(e.code).toBe('webgpu-runtime-error');
1411
+ expect(e.hint).toContain('wgpu/WebGL2 fallback');
1412
+ expect(e.detail).toEqual({
1413
+ error: {
1414
+ code: 'request-adapter-threw',
1415
+ name: 'SecurityError',
1416
+ message: 'WebGPU permission policy denied access',
1417
+ },
1418
+ });
1419
+ });
1420
+
1398
1421
  it('feature not enabled -> code=feature-not-enabled + three non-empty string fields', async () => {
1399
1422
  const gpu = createMockGpu({ requestDeviceFeatureNotEnabled: true });
1400
1423
  const e = unwrapErr(await requestDevice({ gpu }));
package/src/errors.ts CHANGED
@@ -17,8 +17,46 @@ export function adapterUnavailable(): Result<never, RhiError> {
17
17
  return err(
18
18
  new RhiError({
19
19
  code: 'adapter-unavailable',
20
- expected: 'an available WebGPU adapter',
21
- hint: 'check whether the browser supports WebGPU or enable the relevant flag',
20
+ expected: 'an available browser-native WebGPU adapter',
21
+ hint: 'this only reports the browser-native WebGPU channel; ForgeaX may continue through its wgpu/WebGL2 fallback, so do not conclude that the browser or machine is unsupported unless both backend causes fail',
22
+ }),
23
+ );
24
+ }
25
+
26
+ /** `GPU.requestAdapter()` rejected instead of reporting adapter absence with `null`. */
27
+ export function requestAdapterFailed(cause: unknown): Result<never, RhiError> {
28
+ const record =
29
+ cause !== null && typeof cause === 'object'
30
+ ? (cause as { readonly name?: unknown; readonly message?: unknown })
31
+ : undefined;
32
+ const name = typeof record?.name === 'string' && record.name.length > 0 ? record.name : undefined;
33
+ let message =
34
+ typeof record?.message === 'string' && record.message.length > 0
35
+ ? record.message
36
+ : typeof cause === 'string'
37
+ ? cause
38
+ : '';
39
+ if (message.length === 0 && cause !== undefined) {
40
+ try {
41
+ const serialized = JSON.stringify(cause);
42
+ message = serialized && serialized !== '{}' ? serialized : 'unknown thrown object';
43
+ } catch {
44
+ message = 'unserializable thrown object';
45
+ }
46
+ }
47
+ if (message.length === 0) message = 'unknown requestAdapter failure';
48
+ return err(
49
+ new RhiError({
50
+ code: 'webgpu-runtime-error',
51
+ expected: 'navigator.gpu.requestAdapter() resolves with an adapter or null',
52
+ hint: 'inspect detail.error before assigning the failure to WebGPU capability; the ForgeaX runtime can still attempt its wgpu/WebGL2 fallback',
53
+ detail: {
54
+ error: {
55
+ code: 'request-adapter-threw',
56
+ ...(name === undefined ? {} : { name }),
57
+ message,
58
+ },
59
+ },
22
60
  }),
23
61
  );
24
62
  }
package/src/index.ts CHANGED
@@ -56,6 +56,7 @@ import {
56
56
  adapterUnavailable,
57
57
  featureNotEnabled,
58
58
  limitExceeded,
59
+ requestAdapterFailed,
59
60
  shaderCompileFailed,
60
61
  } from './errors';
61
62
 
@@ -398,19 +399,16 @@ export async function requestAdapter(
398
399
  if (ambient === undefined || ambient === null) {
399
400
  return adapterUnavailable();
400
401
  }
401
- // bug-20260610: `navigator.gpu.requestAdapter()` may throw rather than return
402
- // null when WebGPU is disabled at the browser level (observed on Edge with
403
- // WebGPU flag off "Failed to create WebGPU Context Provider"). Without this
404
- // try/catch, the throw escapes structurally engine `tryCreateWebGPURenderer`
405
- // catches it as outcome=throw with a raw Error (no `.code`), and the rhi-wgpu
406
- // wasm GL fallback never gets a chance to run on its own merits because the
407
- // engine sees an unstructured failure. Treat any throw as adapter-unavailable
408
- // so the structured fallback path (Channel 2 -> Channel 3) stays intact.
402
+ // `null` is the spec adapter-absence channel. A rejection is a different
403
+ // failure (permission policy, insecure context, browser/runtime fault, etc.)
404
+ // and must retain its cause instead of being rewritten as unsupported WebGPU.
405
+ // Both outcomes remain structured, so Runtime can still try its wgpu/WebGL2
406
+ // fallback without erasing the browser-native channel diagnosis.
409
407
  let adapter: unknown;
410
408
  try {
411
409
  adapter = await ambient.requestAdapter(opts as GPURequestAdapterOptions | undefined);
412
- } catch {
413
- return adapterUnavailable();
410
+ } catch (cause) {
411
+ return requestAdapterFailed(cause);
414
412
  }
415
413
  if (adapter === null) {
416
414
  return adapterUnavailable();