@forgeax/engine-rhi-webgpu 0.1.2
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/LICENSE +202 -0
- package/README.md +124 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/__mocks__/gpu-device.d.ts +233 -0
- package/dist/__tests__/__mocks__/gpu-device.d.ts.map +1 -0
- package/dist/__tests__/dawn-real-gpu.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn-real-gpu.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/queue-write-range.browser.test.d.ts +2 -0
- package/dist/__tests__/queue-write-range.browser.test.d.ts.map +1 -0
- package/dist/__tests__/rgba16float-live-probe.browser.test.d.ts +2 -0
- package/dist/__tests__/rgba16float-live-probe.browser.test.d.ts.map +1 -0
- package/dist/__tests__/rgba16float-live-probe.d.ts +12 -0
- package/dist/__tests__/rgba16float-live-probe.d.ts.map +1 -0
- package/dist/__tests__/rgba16float-live-probe.dawn.test.d.ts +2 -0
- package/dist/__tests__/rgba16float-live-probe.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/rhi-webgpu.unit.test.d.ts +2 -0
- package/dist/__tests__/rhi-webgpu.unit.test.d.ts.map +1 -0
- package/dist/device.d.ts +62 -0
- package/dist/device.d.ts.map +1 -0
- package/dist/errors.d.ts +47 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +183 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +1704 -0
- package/dist/index.mjs.map +1 -0
- package/dist/internal/__tests__/timestamp-query.unit.test.d.ts +2 -0
- package/dist/internal/__tests__/timestamp-query.unit.test.d.ts.map +1 -0
- package/dist/internal/error-translation.d.ts +16 -0
- package/dist/internal/error-translation.d.ts.map +1 -0
- package/dist/internal/timestamp-query.d.ts +39 -0
- package/dist/internal/timestamp-query.d.ts.map +1 -0
- package/package.json +58 -0
- package/src/__tests__/__mocks__/gpu-device.ts +564 -0
- package/src/__tests__/dawn-real-gpu.dawn.test.ts +1488 -0
- package/src/__tests__/queue-write-range.browser.test.ts +60 -0
- package/src/__tests__/rgba16float-live-probe.browser.test.ts +13 -0
- package/src/__tests__/rgba16float-live-probe.dawn.test.ts +15 -0
- package/src/__tests__/rgba16float-live-probe.ts +44 -0
- package/src/__tests__/rhi-webgpu.unit.test.ts +2409 -0
- package/src/device.ts +1974 -0
- package/src/errors.ts +145 -0
- package/src/index.ts +599 -0
- package/src/internal/__tests__/timestamp-query.unit.test.ts +88 -0
- package/src/internal/error-translation.ts +187 -0
- package/src/internal/timestamp-query.ts +167 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// @forgeax/engine-rhi-webgpu/src/internal/error-translation — spec GPUError +
|
|
2
|
+
// GPUDeviceLostInfo -> 17-member RhiErrorCode dispatch translator (D-P4 literal).
|
|
3
|
+
//
|
|
4
|
+
// Charter: proposition 4 (explicit failure) — every WebGPU async dispatch
|
|
5
|
+
// error reachable through onuncapturederror / device.lost has a closed-union
|
|
6
|
+
// RhiErrorCode mapping; proposition 5 (consistent abstraction) — the dual
|
|
7
|
+
// backends (rhi-webgpu / rhi-wgpu) translate the SAME spec event shape to the
|
|
8
|
+
// SAME 17-member union so engine.onError listeners see byte-for-byte aligned
|
|
9
|
+
// .code values regardless of which backend dispatched the error.
|
|
10
|
+
//
|
|
11
|
+
// Mapping rules (plan-strategy D-P4 table literal):
|
|
12
|
+
// (a) GPUValidationError — sub-pattern on the message text:
|
|
13
|
+
// - /shader|compile|wgsl/i -> 'shader-compile-failed'
|
|
14
|
+
// - /size|alignment|out of bounds/i -> 'queue-write-buffer-out-of-bounds'
|
|
15
|
+
// - /encoder.*finished|finished encoder/i -> 'command-encoder-finished'
|
|
16
|
+
// - /render pass.*not ended|not ended/i -> 'render-pass-not-ended'
|
|
17
|
+
// - /submit/i -> 'queue-submit-failed'
|
|
18
|
+
// - default -> 'limit-exceeded' (the broadest spec validation bucket
|
|
19
|
+
// reachable from validation messages; AI users still get a
|
|
20
|
+
// structured code, not 'webgpu-runtime-error' which is reserved for
|
|
21
|
+
// unrecognised types).
|
|
22
|
+
// (b) GPUOutOfMemoryError -> 'oom' (RhiErrorCode 17-member addition w6).
|
|
23
|
+
// (c) GPUInternalError -> 'internal-error' (RhiErrorCode 17-member addition w6).
|
|
24
|
+
// (d) GPUDeviceLostInfo -> 'device-lost' (RhiErrorCode 17-member addition w6).
|
|
25
|
+
// (e) Unrecognised event type -> 'webgpu-runtime-error' (catch-all bucket;
|
|
26
|
+
// AI users `switch (err.code)` still exhausts the closed union).
|
|
27
|
+
//
|
|
28
|
+
// Dual-channel responsibility boundary (D-PD4):
|
|
29
|
+
// - createX entries return Result.ok / Result.err *synchronously* — sync
|
|
30
|
+
// validation errors (size=0, missing layout, etc.) flow through this
|
|
31
|
+
// channel via the existing errors.ts factories.
|
|
32
|
+
// - device.lost + onuncapturederror channels carry *async* dispatch errors
|
|
33
|
+
// — this translator is the sole gateway from spec event -> RhiError in
|
|
34
|
+
// that async channel. The engine RhiErrorListenerRegistry fan-out happens
|
|
35
|
+
// downstream (packages/engine/src/renderer.ts).
|
|
36
|
+
//
|
|
37
|
+
// Anchors: requirements AC-02 + AC-04; research R-02 §2.1 spec / wgpu / dawn
|
|
38
|
+
// three-way fact; plan-strategy D-P4 mapping table literal; charter propositions
|
|
39
|
+
// 4 + 5; AGENTS.md "## RHI / Shader / error model contract" evolution contract
|
|
40
|
+
// minor add-only.
|
|
41
|
+
|
|
42
|
+
/// <reference types="@webgpu/types" />
|
|
43
|
+
|
|
44
|
+
import { err, type Result, RhiError } from '@forgeax/engine-rhi';
|
|
45
|
+
|
|
46
|
+
/** Translate a spec async-dispatch error event into a closed-union RhiError.
|
|
47
|
+
*
|
|
48
|
+
* This is the single gateway from `GPUUncapturedErrorEvent` / `device.lost`
|
|
49
|
+
* Promise into the 17-member `RhiErrorCode` union (charter proposition 5
|
|
50
|
+
* consistent abstraction: dual-backend dispatch tables align byte-for-byte).
|
|
51
|
+
*
|
|
52
|
+
* @note GPUValidationError dispatch defaults to the 'limit-exceeded' bucket when the device-side message does not match any narrower regex pattern; see L12-21 module-level mapping rules comment for the full sub-pattern table (5 narrower regexes + default fallback). AI users still receive a structured `.code` rather than `'webgpu-runtime-error'` (reserved for unrecognised event types only); charter proposition 4 explicit failure — every spec event maps to a real union member.
|
|
53
|
+
* @param event one of:
|
|
54
|
+
* - `GPUUncapturedErrorEvent` (carries a `.error` of `GPUValidationError |
|
|
55
|
+
* GPUOutOfMemoryError | GPUInternalError`);
|
|
56
|
+
* - `GPUDeviceLostInfo` (carries `.reason` + `.message`);
|
|
57
|
+
* - opaque (catch-all → 'webgpu-runtime-error').
|
|
58
|
+
*/
|
|
59
|
+
export function translateErrorEventToRhiError(
|
|
60
|
+
event: GPUUncapturedErrorEvent | GPUDeviceLostInfo | unknown,
|
|
61
|
+
): Result<never, RhiError> {
|
|
62
|
+
// (d) device-lost branch — GPUDeviceLostInfo shape has `.reason` enum.
|
|
63
|
+
if (
|
|
64
|
+
typeof event === 'object' &&
|
|
65
|
+
event !== null &&
|
|
66
|
+
'reason' in event &&
|
|
67
|
+
typeof (event as { reason: unknown }).reason === 'string'
|
|
68
|
+
) {
|
|
69
|
+
const info = event as GPUDeviceLostInfo;
|
|
70
|
+
return err(
|
|
71
|
+
new RhiError({
|
|
72
|
+
code: 'device-lost',
|
|
73
|
+
expected: 'device must remain alive (driver / browser must not destroy the GPUDevice)',
|
|
74
|
+
hint: `device-lost reason: ${info.reason}; message: ${info.message ?? '<empty>'}`,
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// GPUUncapturedErrorEvent branch — carries `.error` of GPUError subtype.
|
|
80
|
+
if (
|
|
81
|
+
typeof event === 'object' &&
|
|
82
|
+
event !== null &&
|
|
83
|
+
'error' in event &&
|
|
84
|
+
typeof (event as { error: unknown }).error === 'object' &&
|
|
85
|
+
(event as { error: unknown }).error !== null
|
|
86
|
+
) {
|
|
87
|
+
const error = (event as { error: unknown }).error as object;
|
|
88
|
+
const message =
|
|
89
|
+
typeof (error as { message?: unknown }).message === 'string'
|
|
90
|
+
? (error as { message: string }).message
|
|
91
|
+
: '<no message>';
|
|
92
|
+
|
|
93
|
+
// (b) GPUOutOfMemoryError → 'oom' — detected by constructor name.
|
|
94
|
+
if (error.constructor.name === 'GPUOutOfMemoryError') {
|
|
95
|
+
return err(
|
|
96
|
+
new RhiError({
|
|
97
|
+
code: 'oom',
|
|
98
|
+
expected: 'sufficient GPU memory to satisfy the allocation',
|
|
99
|
+
hint: `GPU out-of-memory: ${message}`,
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// (c) GPUInternalError → 'internal-error' — detected by constructor name.
|
|
105
|
+
if (error.constructor.name === 'GPUInternalError') {
|
|
106
|
+
return err(
|
|
107
|
+
new RhiError({
|
|
108
|
+
code: 'internal-error',
|
|
109
|
+
expected: 'driver / browser must report a recognised validation error',
|
|
110
|
+
hint: `GPU internal error: ${message}`,
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// (a) GPUValidationError → sub-pattern dispatch via message text.
|
|
116
|
+
if (error.constructor.name === 'GPUValidationError') {
|
|
117
|
+
if (/shader|compile|wgsl/i.test(message)) {
|
|
118
|
+
return err(
|
|
119
|
+
new RhiError({
|
|
120
|
+
code: 'shader-compile-failed',
|
|
121
|
+
expected: 'valid WGSL source + matching pipeline layout',
|
|
122
|
+
hint: `GPU validation: ${message}`,
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (/size|alignment|out of bounds/i.test(message)) {
|
|
127
|
+
return err(
|
|
128
|
+
new RhiError({
|
|
129
|
+
code: 'queue-write-buffer-out-of-bounds',
|
|
130
|
+
expected: 'writeBuffer offset + data.byteLength must be within buffer.size',
|
|
131
|
+
hint: `GPU validation: ${message}`,
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
if (/encoder.*finished|finished encoder/i.test(message)) {
|
|
136
|
+
return err(
|
|
137
|
+
new RhiError({
|
|
138
|
+
code: 'command-encoder-finished',
|
|
139
|
+
expected: 'command encoder must not be finished before recording new commands',
|
|
140
|
+
hint: `GPU validation: ${message}`,
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (/render pass.*not ended|pass.*not ended/i.test(message)) {
|
|
145
|
+
return err(
|
|
146
|
+
new RhiError({
|
|
147
|
+
code: 'render-pass-not-ended',
|
|
148
|
+
expected: 'previous render pass must be ended before beginning a new pass',
|
|
149
|
+
hint: `GPU validation: ${message}`,
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (/submit/i.test(message)) {
|
|
154
|
+
return err(
|
|
155
|
+
new RhiError({
|
|
156
|
+
code: 'queue-submit-failed',
|
|
157
|
+
expected: 'command buffer references must be valid at submit time',
|
|
158
|
+
hint: `GPU validation: ${message}`,
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
// Default validation bucket — limit-exceeded is the broadest reachable
|
|
163
|
+
// spec validation code (AI users still get a structured code, not the
|
|
164
|
+
// catch-all 'webgpu-runtime-error').
|
|
165
|
+
return err(
|
|
166
|
+
new RhiError({
|
|
167
|
+
code: 'limit-exceeded',
|
|
168
|
+
expected: 'descriptor field values within device limits',
|
|
169
|
+
hint: `GPU validation: ${message}`,
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// (e) Catch-all — unrecognised event type → 'webgpu-runtime-error'.
|
|
176
|
+
const repr =
|
|
177
|
+
typeof event === 'object' && event !== null && 'toString' in event
|
|
178
|
+
? String(event)
|
|
179
|
+
: '<unknown>';
|
|
180
|
+
return err(
|
|
181
|
+
new RhiError({
|
|
182
|
+
code: 'webgpu-runtime-error',
|
|
183
|
+
expected: 'spec-recognised GPUUncapturedErrorEvent or GPUDeviceLostInfo',
|
|
184
|
+
hint: `unrecognised async-dispatch event: ${repr}`,
|
|
185
|
+
}),
|
|
186
|
+
);
|
|
187
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/// <reference types="@webgpu/types" />
|
|
2
|
+
|
|
3
|
+
import { err, ok, type Result, RhiError } from '@forgeax/engine-rhi';
|
|
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
|
+
export function resolveTimestampQueries(args: {
|
|
74
|
+
rawEncoder: GPUCommandEncoder;
|
|
75
|
+
rawQuerySet: GPUQuerySet;
|
|
76
|
+
firstQuery: number;
|
|
77
|
+
queryCount: number;
|
|
78
|
+
rawDestination: GPUBuffer;
|
|
79
|
+
destinationOffset: number;
|
|
80
|
+
readonly deviceGeneration?: number;
|
|
81
|
+
readonly expectedGeneration?: number;
|
|
82
|
+
}): 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
|
+
try {
|
|
103
|
+
args.rawEncoder.resolveQuerySet(
|
|
104
|
+
args.rawQuerySet,
|
|
105
|
+
args.firstQuery,
|
|
106
|
+
args.queryCount,
|
|
107
|
+
args.rawDestination,
|
|
108
|
+
args.destinationOffset,
|
|
109
|
+
);
|
|
110
|
+
return ok(undefined);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
113
|
+
return err(
|
|
114
|
+
new RhiError({
|
|
115
|
+
code: 'webgpu-runtime-error',
|
|
116
|
+
expected: 'underlying GPUCommandEncoder.resolveQuerySet to succeed',
|
|
117
|
+
hint: `resolveQuerySet raised: ${message}`,
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function writeTimestamp(args: {
|
|
124
|
+
rawEncoder: GPUCommandEncoder;
|
|
125
|
+
rawQuerySet: GPUQuerySet;
|
|
126
|
+
queryIndex: number;
|
|
127
|
+
readonly deviceGeneration?: number;
|
|
128
|
+
readonly expectedGeneration?: number;
|
|
129
|
+
}): void {
|
|
130
|
+
const encoderWithTimestamp = args.rawEncoder as unknown as {
|
|
131
|
+
writeTimestamp?: (querySet: GPUQuerySet, queryIndex: number) => void;
|
|
132
|
+
};
|
|
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') {
|
|
151
|
+
throw new RhiError({
|
|
152
|
+
code: 'webgpu-runtime-error',
|
|
153
|
+
expected: 'underlying GPUCommandEncoder.writeTimestamp to be callable',
|
|
154
|
+
hint: 'timestamp-query is advertised but the raw encoder has no writeTimestamp method',
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
writeTimestamp(args.rawQuerySet, args.queryIndex);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
161
|
+
throw new RhiError({
|
|
162
|
+
code: 'webgpu-runtime-error',
|
|
163
|
+
expected: 'underlying GPUCommandEncoder.writeTimestamp to succeed',
|
|
164
|
+
hint: `writeTimestamp raised: ${message}`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|