@forgeax/engine-rhi-webgpu 0.1.3 → 0.1.6

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.
@@ -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
- }