@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.
- package/README.md +3 -3
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/__mocks__/gpu-device.d.ts +0 -7
- package/dist/__tests__/__mocks__/gpu-device.d.ts.map +1 -1
- package/dist/device.d.ts +1 -1
- package/dist/device.d.ts.map +1 -1
- package/dist/index.mjs +112 -99
- package/dist/index.mjs.map +1 -1
- package/dist/internal/timestamp-query.d.ts +0 -24
- package/dist/internal/timestamp-query.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/__mocks__/gpu-device.ts +1 -10
- package/src/__tests__/dawn-real-gpu.dawn.test.ts +0 -43
- package/src/__tests__/rhi-webgpu.unit.test.ts +7 -41
- package/src/device.ts +164 -36
- package/src/internal/timestamp-query.ts +2 -110
- package/dist/__tests__/queue-write-range.browser.test.d.ts +0 -2
- package/dist/__tests__/queue-write-range.browser.test.d.ts.map +0 -1
- package/dist/__tests__/rgba16float-live-probe.browser.test.d.ts +0 -2
- package/dist/__tests__/rgba16float-live-probe.browser.test.d.ts.map +0 -1
- package/dist/__tests__/rgba16float-live-probe.d.ts +0 -12
- package/dist/__tests__/rgba16float-live-probe.d.ts.map +0 -1
- package/dist/__tests__/rgba16float-live-probe.dawn.test.d.ts +0 -2
- package/dist/__tests__/rgba16float-live-probe.dawn.test.d.ts.map +0 -1
- package/src/__tests__/queue-write-range.browser.test.ts +0 -60
- package/src/__tests__/rgba16float-live-probe.browser.test.ts +0 -13
- package/src/__tests__/rgba16float-live-probe.dawn.test.ts +0 -15
- package/src/__tests__/rgba16float-live-probe.ts +0 -44
|
@@ -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
|
-
|
|
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 +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 +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 +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
|
-
}
|