@forgeax/engine-rhi-webgpu 0.1.4 → 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.
Files changed (33) hide show
  1. package/README.md +12 -3
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/__mocks__/gpu-device.d.ts +0 -7
  4. package/dist/__tests__/__mocks__/gpu-device.d.ts.map +1 -1
  5. package/dist/device.d.ts +1 -1
  6. package/dist/device.d.ts.map +1 -1
  7. package/dist/errors.d.ts +2 -0
  8. package/dist/errors.d.ts.map +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.mjs +144 -103
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/internal/timestamp-query.d.ts +0 -24
  13. package/dist/internal/timestamp-query.d.ts.map +1 -1
  14. package/package.json +3 -3
  15. package/src/__tests__/__mocks__/gpu-device.ts +1 -10
  16. package/src/__tests__/dawn-real-gpu.dawn.test.ts +0 -43
  17. package/src/__tests__/rhi-webgpu.unit.test.ts +32 -43
  18. package/src/device.ts +164 -36
  19. package/src/errors.ts +40 -2
  20. package/src/index.ts +8 -10
  21. package/src/internal/timestamp-query.ts +2 -110
  22. package/dist/__tests__/queue-write-range.browser.test.d.ts +0 -2
  23. package/dist/__tests__/queue-write-range.browser.test.d.ts.map +0 -1
  24. package/dist/__tests__/rgba16float-live-probe.browser.test.d.ts +0 -2
  25. package/dist/__tests__/rgba16float-live-probe.browser.test.d.ts.map +0 -1
  26. package/dist/__tests__/rgba16float-live-probe.d.ts +0 -12
  27. package/dist/__tests__/rgba16float-live-probe.d.ts.map +0 -1
  28. package/dist/__tests__/rgba16float-live-probe.dawn.test.d.ts +0 -2
  29. package/dist/__tests__/rgba16float-live-probe.dawn.test.d.ts.map +0 -1
  30. package/src/__tests__/queue-write-range.browser.test.ts +0 -60
  31. package/src/__tests__/rgba16float-live-probe.browser.test.ts +0 -13
  32. package/src/__tests__/rgba16float-live-probe.dawn.test.ts +0 -15
  33. package/src/__tests__/rgba16float-live-probe.ts +0 -44
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();
@@ -2,74 +2,6 @@
2
2
 
3
3
  import { err, ok, type Result, RhiError } from '@forgeax/engine-rhi';
4
4
 
5
- export type TimestampQueryHealthCode =
6
- | 'timestamp-query-unsupported'
7
- | 'timestamp-query-encoder-missing'
8
- | 'timestamp-query-generation-mismatch';
9
-
10
- export interface TimestampQueryHealth {
11
- readonly status: 'ready' | 'fault';
12
- readonly code: TimestampQueryHealthCode | undefined;
13
- readonly expected: string;
14
- readonly hint: string;
15
- readonly deviceGeneration: number;
16
- readonly expectedGeneration: number | undefined;
17
- }
18
-
19
- /**
20
- * Keep timestamp capability and device-generation checks structured. This is
21
- * also the health vocabulary used by recovery callers; a missing capability
22
- * or stale encoder is never reported as a fabricated timing interval.
23
- */
24
- export function timestampQueryHealth(input: {
25
- readonly capability: boolean;
26
- readonly encoderWriteTimestamp: boolean;
27
- readonly deviceGeneration: number;
28
- readonly expectedGeneration?: number | undefined;
29
- }): TimestampQueryHealth {
30
- if (!input.capability) {
31
- return {
32
- status: 'fault',
33
- code: 'timestamp-query-unsupported',
34
- expected: 'device.caps.timestampQuery is true before timestamp queries are submitted',
35
- hint: 'use the CPU timing path or recreate the renderer with timestamp-query capability',
36
- deviceGeneration: input.deviceGeneration,
37
- expectedGeneration: input.expectedGeneration,
38
- };
39
- }
40
- if (
41
- input.expectedGeneration !== undefined &&
42
- input.deviceGeneration !== input.expectedGeneration
43
- ) {
44
- return {
45
- status: 'fault',
46
- code: 'timestamp-query-generation-mismatch',
47
- expected: 'timestamp encoder generation matches the live device generation',
48
- hint: 'discard the stale encoder and rebuild timestamp resources during device recovery',
49
- deviceGeneration: input.deviceGeneration,
50
- expectedGeneration: input.expectedGeneration,
51
- };
52
- }
53
- if (!input.encoderWriteTimestamp) {
54
- return {
55
- status: 'fault',
56
- code: 'timestamp-query-encoder-missing',
57
- expected: 'underlying GPUCommandEncoder.writeTimestamp to be callable',
58
- hint: 'timestamp-query is advertised but the raw encoder has no writeTimestamp method',
59
- deviceGeneration: input.deviceGeneration,
60
- expectedGeneration: input.expectedGeneration,
61
- };
62
- }
63
- return {
64
- status: 'ready',
65
- code: undefined,
66
- expected: 'timestamp-query capability and encoder are available for this device generation',
67
- hint: 'continue with the timestamp query operation',
68
- deviceGeneration: input.deviceGeneration,
69
- expectedGeneration: input.expectedGeneration,
70
- };
71
- }
72
-
73
5
  export function resolveTimestampQueries(args: {
74
6
  rawEncoder: GPUCommandEncoder;
75
7
  rawQuerySet: GPUQuerySet;
@@ -77,28 +9,7 @@ export function resolveTimestampQueries(args: {
77
9
  queryCount: number;
78
10
  rawDestination: GPUBuffer;
79
11
  destinationOffset: number;
80
- readonly deviceGeneration?: number;
81
- readonly expectedGeneration?: number;
82
12
  }): Result<void, RhiError> {
83
- if (
84
- args.deviceGeneration !== undefined &&
85
- args.expectedGeneration !== undefined &&
86
- args.deviceGeneration !== args.expectedGeneration
87
- ) {
88
- const health = timestampQueryHealth({
89
- capability: true,
90
- encoderWriteTimestamp: true,
91
- deviceGeneration: args.deviceGeneration,
92
- expectedGeneration: args.expectedGeneration,
93
- });
94
- return err(
95
- new RhiError({
96
- code: 'webgpu-runtime-error',
97
- expected: health.expected,
98
- hint: `${health.code}: ${health.hint}`,
99
- }),
100
- );
101
- }
102
13
  try {
103
14
  args.rawEncoder.resolveQuerySet(
104
15
  args.rawQuerySet,
@@ -124,30 +35,11 @@ export function writeTimestamp(args: {
124
35
  rawEncoder: GPUCommandEncoder;
125
36
  rawQuerySet: GPUQuerySet;
126
37
  queryIndex: number;
127
- readonly deviceGeneration?: number;
128
- readonly expectedGeneration?: number;
129
38
  }): void {
130
39
  const encoderWithTimestamp = args.rawEncoder as unknown as {
131
40
  writeTimestamp?: (querySet: GPUQuerySet, queryIndex: number) => void;
132
41
  };
133
- const writeTimestamp = encoderWithTimestamp.writeTimestamp;
134
- const health = timestampQueryHealth({
135
- capability: true,
136
- encoderWriteTimestamp: typeof writeTimestamp === 'function',
137
- deviceGeneration: args.deviceGeneration ?? 0,
138
- expectedGeneration: args.expectedGeneration,
139
- });
140
- if (health.status === 'fault') {
141
- throw new RhiError({
142
- code: 'webgpu-runtime-error',
143
- expected: health.expected,
144
- hint:
145
- health.code === 'timestamp-query-encoder-missing'
146
- ? health.hint
147
- : `${health.code}: ${health.hint}`,
148
- });
149
- }
150
- if (typeof writeTimestamp !== 'function') {
42
+ if (typeof encoderWithTimestamp.writeTimestamp !== 'function') {
151
43
  throw new RhiError({
152
44
  code: 'webgpu-runtime-error',
153
45
  expected: 'underlying GPUCommandEncoder.writeTimestamp to be callable',
@@ -155,7 +47,7 @@ export function writeTimestamp(args: {
155
47
  });
156
48
  }
157
49
  try {
158
- writeTimestamp(args.rawQuerySet, args.queryIndex);
50
+ encoderWithTimestamp.writeTimestamp(args.rawQuerySet, args.queryIndex);
159
51
  } catch (error) {
160
52
  const message = error instanceof Error ? error.message : String(error);
161
53
  throw new RhiError({
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=queue-write-range.browser.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"queue-write-range.browser.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/queue-write-range.browser.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=rgba16float-live-probe.browser.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"rgba16float-live-probe.browser.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/rgba16float-live-probe.browser.test.ts"],"names":[],"mappings":""}
@@ -1,12 +0,0 @@
1
- import { type Rgba16floatRenderTargetProbe } from '@forgeax/engine-rhi';
2
- export interface Rgba16floatLiveProbeEvidence {
3
- readonly backend: string;
4
- readonly initial: Rgba16floatRenderTargetProbe;
5
- readonly loss: {
6
- readonly reason: 'destroyed' | 'unknown';
7
- readonly message: string;
8
- };
9
- readonly recovered: Rgba16floatRenderTargetProbe;
10
- }
11
- export declare function runRgba16floatLiveProbe(): Promise<Rgba16floatLiveProbeEvidence>;
12
- //# sourceMappingURL=rgba16float-live-probe.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"rgba16float-live-probe.d.ts","sourceRoot":"","sources":["../../src/__tests__/rgba16float-live-probe.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,4BAA4B,EAClC,MAAM,qBAAqB,CAAC;AAG7B,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,4BAA4B,CAAC;IAC/C,QAAQ,CAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACtF,QAAQ,CAAC,SAAS,EAAE,4BAA4B,CAAC;CAClD;AAgBD,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,4BAA4B,CAAC,CAerF"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=rgba16float-live-probe.dawn.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"rgba16float-live-probe.dawn.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/rgba16float-live-probe.dawn.test.ts"],"names":[],"mappings":""}
@@ -1,60 +0,0 @@
1
- import type { RhiDevice } from '@forgeax/engine-rhi';
2
- import { describe, expect, it } from 'vitest';
3
- import { rhi } from '../index';
4
-
5
- async function requestRhiDevice(): Promise<RhiDevice | undefined> {
6
- if (typeof navigator === 'undefined' || navigator.gpu === undefined) return undefined;
7
- const adapter = await rhi.requestAdapter();
8
- expect(adapter.ok).toBe(true);
9
- if (!adapter.ok) return undefined;
10
- const device = await adapter.value.requestDevice();
11
- expect(device.ok).toBe(true);
12
- if (!device.ok) return undefined;
13
- return device.value;
14
- }
15
-
16
- describe('browser WebGPU queue.writeBuffer typed range', () => {
17
- it('round-trips the exact selected typed-array elements', async () => {
18
- const device = await requestRhiDevice();
19
- if (device === undefined) return;
20
- const target = device.createBuffer({
21
- label: 'browser-write-range-target',
22
- size: 32,
23
- usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
24
- });
25
- const readback = device.createBuffer({
26
- label: 'browser-write-range-readback',
27
- size: 32,
28
- usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
29
- });
30
- expect(target.ok && readback.ok).toBe(true);
31
- if (!target.ok || !readback.ok) return;
32
-
33
- const write = device.queue.writeBuffer(
34
- target.value,
35
- 0,
36
- new Float32Array([10, 20, 30, 40]),
37
- 1,
38
- 2,
39
- );
40
- expect(write.ok).toBe(true);
41
- if (!write.ok) return;
42
- const encoder = device.createCommandEncoder({ label: 'browser-write-range-copy' });
43
- expect(encoder.ok).toBe(true);
44
- if (!encoder.ok) return;
45
- encoder.value.copyBufferToBuffer(target.value, 0, readback.value, 0, 32);
46
- const finish = encoder.value.finish();
47
- expect(finish.ok).toBe(true);
48
- if (!finish.ok) return;
49
- expect(device.queue.submit([finish.value]).ok).toBe(true);
50
- await device.queue.onSubmittedWorkDone();
51
- const mapped = await readback.value.mapAsync(GPUMapMode.READ);
52
- expect(mapped.ok).toBe(true);
53
- if (!mapped.ok) return;
54
- const range = mapped.value.getMappedRange();
55
- expect(range.ok).toBe(true);
56
- if (!range.ok) return;
57
- expect(Array.from(new Float32Array(range.value.slice(0)))).toEqual([20, 30, 0, 0, 0, 0, 0, 0]);
58
- mapped.value.unmap();
59
- });
60
- });
@@ -1,13 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { runRgba16floatLiveProbe } from './rgba16float-live-probe';
3
-
4
- describe('browser rgba16float live render/readback/recovery probe', () => {
5
- it('reads the packed render target on the initial and fresh recovery device', async () => {
6
- const evidence = await runRgba16floatLiveProbe();
7
- expect(evidence.backend).toBe('webgpu');
8
- expect(evidence.initial.pixel).toEqual([0.25, 0.5, 0.75, 1]);
9
- expect(evidence.loss.reason).toBe('destroyed');
10
- expect(evidence.recovered.pixel).toEqual(evidence.initial.pixel);
11
- expect(typeof evidence.initial.timestampQuery).toBe('boolean');
12
- });
13
- });
@@ -1,15 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { runRgba16floatLiveProbe } from './rgba16float-live-probe';
3
-
4
- describe('Dawn rgba16float live render/readback/recovery probe', () => {
5
- it('reads the packed render target on the initial and fresh recovery device', async () => {
6
- const evidence = await runRgba16floatLiveProbe();
7
- expect(evidence.backend).toBe('webgpu');
8
- expect(evidence.initial.pixel).toEqual([0.25, 0.5, 0.75, 1]);
9
- expect(evidence.loss.reason).toBe('destroyed');
10
- expect(evidence.recovered.pixel).toEqual(evidence.initial.pixel);
11
- expect(typeof evidence.initial.timestampQuery).toBe('boolean');
12
- // biome-ignore lint/suspicious/noConsole: emitted as live Dawn admission evidence
13
- console.log(JSON.stringify({ kind: 'rgba16float-live-probe', ...evidence }));
14
- });
15
- });
@@ -1,44 +0,0 @@
1
- import type { RhiDevice } from '@forgeax/engine-rhi';
2
- import {
3
- probeRgba16floatRenderTarget,
4
- type Rgba16floatRenderTargetProbe,
5
- } from '@forgeax/engine-rhi';
6
- import { _internal_getRawDevice, rhi } from '../index';
7
-
8
- export interface Rgba16floatLiveProbeEvidence {
9
- readonly backend: string;
10
- readonly initial: Rgba16floatRenderTargetProbe;
11
- readonly loss: { readonly reason: 'destroyed' | 'unknown'; readonly message: string };
12
- readonly recovered: Rgba16floatRenderTargetProbe;
13
- }
14
-
15
- async function probeFreshDevice(): Promise<{
16
- readonly backend: string;
17
- readonly device: RhiDevice;
18
- readonly report: Rgba16floatRenderTargetProbe;
19
- }> {
20
- const adapter = await rhi.requestAdapter();
21
- if (!adapter.ok) throw adapter.error;
22
- const device = await adapter.value.requestDevice();
23
- if (!device.ok) throw device.error;
24
- const probed = await probeRgba16floatRenderTarget(device.value);
25
- if (!probed.ok) throw probed.error;
26
- return { backend: device.value.caps.backendKind, device: device.value, report: probed.value };
27
- }
28
-
29
- export async function runRgba16floatLiveProbe(): Promise<Rgba16floatLiveProbeEvidence> {
30
- const initial = await probeFreshDevice();
31
- const rawDevice = _internal_getRawDevice(initial.device);
32
- if (rawDevice === undefined)
33
- throw new Error('live WebGPU probe requires its backend-owned device');
34
- const lost = initial.device.lost;
35
- rawDevice.destroy();
36
- const loss = await lost;
37
- const recovered = await probeFreshDevice();
38
- return {
39
- backend: initial.backend,
40
- initial: initial.report,
41
- loss,
42
- recovered: recovered.report,
43
- };
44
- }