@forgeax/engine-rhi 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 +234 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/async-form.test-d.d.ts +2 -0
- package/dist/__tests__/async-form.test-d.d.ts.map +1 -0
- package/dist/__tests__/binding-resource.test-d.d.ts +2 -0
- package/dist/__tests__/binding-resource.test-d.d.ts.map +1 -0
- package/dist/__tests__/buffer-mapping.test-d.d.ts +2 -0
- package/dist/__tests__/buffer-mapping.test-d.d.ts.map +1 -0
- package/dist/__tests__/canvasContext.test-d.d.ts +2 -0
- package/dist/__tests__/canvasContext.test-d.d.ts.map +1 -0
- package/dist/__tests__/caps-three-way.unit.test.d.ts +2 -0
- package/dist/__tests__/caps-three-way.unit.test.d.ts.map +1 -0
- package/dist/__tests__/caps.test-d.d.ts +2 -0
- package/dist/__tests__/caps.test-d.d.ts.map +1 -0
- package/dist/__tests__/command-encoder-spec-alignment.test-d.d.ts +2 -0
- package/dist/__tests__/command-encoder-spec-alignment.test-d.d.ts.map +1 -0
- package/dist/__tests__/createComputePipeline.test-d.d.ts +2 -0
- package/dist/__tests__/createComputePipeline.test-d.d.ts.map +1 -0
- package/dist/__tests__/createQuerySet.test-d.d.ts +2 -0
- package/dist/__tests__/createQuerySet.test-d.d.ts.map +1 -0
- package/dist/__tests__/createTextureView.test-d.d.ts +2 -0
- package/dist/__tests__/createTextureView.test-d.d.ts.map +1 -0
- package/dist/__tests__/descriptor-mirror.test-d.d.ts +2 -0
- package/dist/__tests__/descriptor-mirror.test-d.d.ts.map +1 -0
- package/dist/__tests__/draw-validation-errors.test.d.ts +2 -0
- package/dist/__tests__/draw-validation-errors.test.d.ts.map +1 -0
- package/dist/__tests__/errors-instancing.unit.test.d.ts +2 -0
- package/dist/__tests__/errors-instancing.unit.test.d.ts.map +1 -0
- package/dist/__tests__/errors.test-d.d.ts +2 -0
- package/dist/__tests__/errors.test-d.d.ts.map +1 -0
- package/dist/__tests__/limit-exceeded-detail.test-d.d.ts +2 -0
- package/dist/__tests__/limit-exceeded-detail.test-d.d.ts.map +1 -0
- package/dist/__tests__/mapped-buffer.test-d.d.ts +2 -0
- package/dist/__tests__/mapped-buffer.test-d.d.ts.map +1 -0
- package/dist/__tests__/opaque-handle.test-d.d.ts +2 -0
- package/dist/__tests__/opaque-handle.test-d.d.ts.map +1 -0
- package/dist/__tests__/owner-out-of-range-role.test.d.ts +2 -0
- package/dist/__tests__/owner-out-of-range-role.test.d.ts.map +1 -0
- package/dist/__tests__/queue-writes.test-d.d.ts +2 -0
- package/dist/__tests__/queue-writes.test-d.d.ts.map +1 -0
- package/dist/__tests__/queue-writes.unit.test.d.ts +2 -0
- package/dist/__tests__/queue-writes.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-pass-encoder-spec-alignment.test-d.d.ts +2 -0
- package/dist/__tests__/render-pass-encoder-spec-alignment.test-d.d.ts.map +1 -0
- package/dist/__tests__/requestAdapter.test-d.d.ts +2 -0
- package/dist/__tests__/requestAdapter.test-d.d.ts.map +1 -0
- package/dist/errors.d.ts +315 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.mjs +56 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.d.ts +1749 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +256 -0
- package/dist/index.mjs.map +1 -0
- package/dist/queue-writes.d.ts +20 -0
- package/dist/queue-writes.d.ts.map +1 -0
- package/package.json +61 -0
- package/src/__tests__/async-form.test-d.ts +40 -0
- package/src/__tests__/binding-resource.test-d.ts +66 -0
- package/src/__tests__/buffer-mapping.test-d.ts +84 -0
- package/src/__tests__/canvasContext.test-d.ts +106 -0
- package/src/__tests__/caps-three-way.unit.test.ts +144 -0
- package/src/__tests__/caps.test-d.ts +83 -0
- package/src/__tests__/command-encoder-spec-alignment.test-d.ts +117 -0
- package/src/__tests__/createComputePipeline.test-d.ts +149 -0
- package/src/__tests__/createQuerySet.test-d.ts +81 -0
- package/src/__tests__/createTextureView.test-d.ts +127 -0
- package/src/__tests__/descriptor-mirror.test-d.ts +267 -0
- package/src/__tests__/draw-validation-errors.test.ts +155 -0
- package/src/__tests__/errors-instancing.unit.test.ts +167 -0
- package/src/__tests__/errors.test-d.ts +199 -0
- package/src/__tests__/limit-exceeded-detail.test-d.ts +33 -0
- package/src/__tests__/mapped-buffer.test-d.ts +65 -0
- package/src/__tests__/opaque-handle.test-d.ts +121 -0
- package/src/__tests__/owner-out-of-range-role.test.ts +84 -0
- package/src/__tests__/queue-writes.test-d.ts +107 -0
- package/src/__tests__/queue-writes.unit.test.ts +30 -0
- package/src/__tests__/render-pass-encoder-spec-alignment.test-d.ts +202 -0
- package/src/__tests__/requestAdapter.test-d.ts +123 -0
- package/src/errors.ts +504 -0
- package/src/index.ts +2398 -0
- package/src/queue-writes.ts +62 -0
package/src/errors.ts
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
// @forgeax/engine-rhi/src/errors - RhiError + closed RhiErrorCode union + Result<T, E>.
|
|
2
|
+
//
|
|
3
|
+
// Shape:
|
|
4
|
+
// - RhiErrorCode = closed union 23 members (charter P3: closed-union
|
|
5
|
+
// exhaustive switch needs no default fallback; tsc strict mode guards
|
|
6
|
+
// completeness). Extended from 6 to 10 in feat-20260508-rhi-surface-completion
|
|
7
|
+
// w7 (D-S3): added 'command-encoder-finished' / 'render-pass-not-ended' /
|
|
8
|
+
// 'queue-submit-failed' / 'queue-write-buffer-out-of-bounds'.
|
|
9
|
+
// Extended from 10 to 14 in feat-20260509-ecs-render-bridge-mvp w6 (D-S7):
|
|
10
|
+
// added 'render-system-no-camera' / 'render-system-multi-camera' /
|
|
11
|
+
// 'render-system-multi-light' / 'asset-not-registered'.
|
|
12
|
+
// Extended from 14 to 17 in feat-20260511-rhi-spec-realign-aggressive w6
|
|
13
|
+
// (D-P4 + R-02 §2.1 W3C spec 22.2 subtypes): added 'device-lost' / 'oom' /
|
|
14
|
+
// 'internal-error' so the onError fan-out can disambiguate spec error
|
|
15
|
+
// subclasses without falling back to the bucket 'webgpu-runtime-error'.
|
|
16
|
+
// Extended from 17 to 18 in feat-20260511-asset-system-v1 w4
|
|
17
|
+
// (D-P2 + requirements §9 row 8 + AC-04 + AC-21): added
|
|
18
|
+
// 'hierarchy-broken' for `propagateTransforms` stale ChildOf ref fail-fast
|
|
19
|
+
// (ChildOf component references a destroyed entity); same
|
|
20
|
+
// render-system / schedule semantic domain as
|
|
21
|
+
// 'render-system-multi-camera' / 'render-system-no-camera'. Minor add-only
|
|
22
|
+
// per AGENTS.md evolution contract (no reorder / rename / deprecate).
|
|
23
|
+
// Extended from 18 to 19 in feat-20260612-rhi-destroy-renderer-dispose-gpu-
|
|
24
|
+
// lifecycle M1 (D-6 + D-7 + AC-02 / AC-03): added 'destroy-after-destroy'
|
|
25
|
+
// for second `destroyBuffer` / `destroyTexture` on the same handle. The
|
|
26
|
+
// shim layer (rhi-webgpu + rhi-wgpu) tracks per-handle `destroyed: boolean`
|
|
27
|
+
// in WeakMap-backed meta and fail-fasts the second call rather than
|
|
28
|
+
// forwarding it to the underlying GPU (research F-1 wgpu wasm `destroy()`
|
|
29
|
+
// is idempotent void; F-8 WebGPU spec is also idempotent void; D-7 prefers
|
|
30
|
+
// fail-fast over silent idempotency because double-destroy is almost always
|
|
31
|
+
// a lifecycle bug). Minor add-only per AGENTS.md evolution contract.
|
|
32
|
+
// Extended from 19 to 20 in feat-20260619-wasm-fault-isolation M3 w7:
|
|
33
|
+
// added 'rhi-descriptor-invalid' for `createRenderPipeline` (and other
|
|
34
|
+
// create* entries) descriptor parse failures surfaced through the wgpu-wasm
|
|
35
|
+
// backend (Rust `#[wasm_bindgen(catch)]` Err). The prefix-based
|
|
36
|
+
// classification (D-1 / D-2) routes wasm exceptions with the stable marker
|
|
37
|
+
// `[wgpu-wasm] failed to parse` to this code; exceptions without the prefix
|
|
38
|
+
// remain in 'webgpu-runtime-error'. Semantics: descriptor parse failure =
|
|
39
|
+
// caller bug (malformed descriptor data passed from TS), distinct from
|
|
40
|
+
// 'webgpu-runtime-error' = runtime condition (valid descriptor rejected by
|
|
41
|
+
// wgpu backend). Minor add-only per AGENTS.md evolution contract.
|
|
42
|
+
// Extended from 20 to 21 in feat-20260622-chunk-gpu-instancing-sprite-
|
|
43
|
+
// tilemap M2 w10 (D-2 + AC-05 + research N-1): added
|
|
44
|
+
// 'instancing-exceeds-uniform-cap' for the WebGL2 uniform-fallback path
|
|
45
|
+
// when a record-stage fold bucket carries more than 128 instances
|
|
46
|
+
// (128 = MAX_UNIFORM_INSTANCES; 128 * 64B = 8192B comfortably fits the
|
|
47
|
+
// WebGL2 minimum 16384B UBO size, leaving headroom for the per-frame
|
|
48
|
+
// material UBO slice — research N-1 implements the locked value). The
|
|
49
|
+
// record-stage dispatch site fires the error AND falls the offending
|
|
50
|
+
// bucket back to per-entity drawIndexed (the same exit the mode-gate
|
|
51
|
+
// bypass uses — plan-strategy D-9 "shared fallback exit"). Semantics:
|
|
52
|
+
// distinct from 'limit-exceeded' (byte-cap against
|
|
53
|
+
// maxStorageBufferBindingSize) — this code targets the per-bucket
|
|
54
|
+
// instance-count cap, which is a backend-capability ceiling rather than
|
|
55
|
+
// an allocation-size ceiling. AI users branch on .code first then read
|
|
56
|
+
// detail.requested / .limit / .scope through property access (charter
|
|
57
|
+
// P3 + plan-strategy 8.3 actionable hint). Minor add-only
|
|
58
|
+
// per AGENTS.md evolution contract.
|
|
59
|
+
// Extended from 21 to 23 in feat-20260708-composited-multi-world-rendering
|
|
60
|
+
// M3 (D-5): added 'render-system-empty-worlds' + 'render-system-owner-out-of-
|
|
61
|
+
// range' for draw(worlds, { cameraOwner, resourceOwner }) entry validation. The
|
|
62
|
+
// owner-out-of-range path exposes .detail = RhiOwnerOutOfRangeDetail
|
|
63
|
+
// ({ role, owner, worldCount } after feat-20260709-editor-world-partition
|
|
64
|
+
// M1 / w7: role ∈ {'camera','resource'} names which of the two split draw
|
|
65
|
+
// owners is out of range); empty-worlds carries no .detail. The pure
|
|
66
|
+
// validateDrawArgs(worldCount, { cameraOwner, resourceOwner })
|
|
67
|
+
// helper (World-free primitives) emits both and is consumed by the runtime
|
|
68
|
+
// createRenderer draw entry (the codes' SSOT stays in rhi). Checks run
|
|
69
|
+
// empty-worlds -> cameraOwner -> resourceOwner; the first out-of-range owner
|
|
70
|
+
// wins (role='camera' when both offend). Add-only (no new code, 0 net Δ per
|
|
71
|
+
// D-3) per AGENTS.md evolution contract.
|
|
72
|
+
// - RhiError class has readonly .code / .expected / .hint three-field surface
|
|
73
|
+
// (AGENTS.md "Errors are structured" / D-5); the 'shader-compile-failed' path
|
|
74
|
+
// exposes .detail = RhiShaderCompileDetail (compilerMessages array);
|
|
75
|
+
// the 'asset-not-registered' path exposes .detail = RhiAssetNotRegisteredDetail
|
|
76
|
+
// ({ assetHandle: number }, D-S6); the 'webgpu-runtime-error' path optionally
|
|
77
|
+
// exposes .detail = RhiWebgpuRuntimeDetail ({ error: RhiError | fallback }, D-S8) for
|
|
78
|
+
// RenderSystem internal exception fan-out; the 'limit-exceeded' path
|
|
79
|
+
// exposes .detail = LimitExceededDetail ({ maxStorageBufferBindingSize,
|
|
80
|
+
// requestedBytes }, feat-20260513-instanced-mesh M5 reshape from legacy
|
|
81
|
+
// { renderableCount, limit }); the other 15 paths leave
|
|
82
|
+
// .detail = undefined per charter proposition 4 baseline.
|
|
83
|
+
// - Result<T, E> = binary tag union ('ok' / 'err'), per AGENTS.md "Errors are
|
|
84
|
+
// structured" convention.
|
|
85
|
+
//
|
|
86
|
+
// Related: requirements AC AC-10 + MVP-1.7 + AC-RSC-07 + hard-constraint 8 +
|
|
87
|
+
// AI User Affordances; plan-strategy 2 S-6 (types/rhi single source) +
|
|
88
|
+
// 7.3 error-info table; plan-decisions OQ-P2 (forward all 6 fields of
|
|
89
|
+
// GPUCompilationMessage); D-S3 (4 command/queue members) + D-S6 / D-S7
|
|
90
|
+
// / D-S8 (4 RenderSystem / AssetRegistry members + .detail structure).
|
|
91
|
+
|
|
92
|
+
/// <reference types="@webgpu/types" />
|
|
93
|
+
|
|
94
|
+
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Closed RhiErrorCode union. `switch` exhaustive checks need no default
|
|
98
|
+
* fallback - tsc strict mode guards union completeness (charter proposition 4
|
|
99
|
+
* + proposition 3: machine-readable union > prose).
|
|
100
|
+
*
|
|
101
|
+
*
|
|
102
|
+
* The executable union below is the machine-readable source of truth for
|
|
103
|
+
* the 23 members. The package README owns the AI-facing trigger and recovery
|
|
104
|
+
* table; keeping that projection in one documentation owner avoids a second
|
|
105
|
+
* member ledger drifting from the union.
|
|
106
|
+
*
|
|
107
|
+
* @example AI-user exhaustive switch on the 4 command/queue members (no default fallback)
|
|
108
|
+
* ```ts
|
|
109
|
+
* import type { RhiError, RhiErrorCode } from '@forgeax/engine-rhi';
|
|
110
|
+
*
|
|
111
|
+
* function recover(code: RhiErrorCode): string {
|
|
112
|
+
* switch (code) {
|
|
113
|
+
* // ... 6 baseline members elided ...
|
|
114
|
+
* case 'command-encoder-finished': return 'recreate encoder via device.createCommandEncoder()';
|
|
115
|
+
* case 'render-pass-not-ended': return 'call pass.end() before next beginRenderPass()';
|
|
116
|
+
* case 'queue-submit-failed': return 'audit buffer/pipeline lifetimes before submit';
|
|
117
|
+
* case 'queue-write-buffer-out-of-bounds': return 'realign offset and re-check buffer.size';
|
|
118
|
+
* default: return 'baseline path';
|
|
119
|
+
* }
|
|
120
|
+
* }
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
export type RhiErrorCode =
|
|
124
|
+
| 'adapter-unavailable'
|
|
125
|
+
| 'feature-not-enabled'
|
|
126
|
+
| 'limit-exceeded'
|
|
127
|
+
| 'shader-compile-failed'
|
|
128
|
+
| 'rhi-not-available'
|
|
129
|
+
| 'webgpu-runtime-error'
|
|
130
|
+
| 'command-encoder-finished'
|
|
131
|
+
| 'render-pass-not-ended'
|
|
132
|
+
| 'queue-submit-failed'
|
|
133
|
+
| 'queue-write-buffer-out-of-bounds'
|
|
134
|
+
| 'render-system-no-camera'
|
|
135
|
+
| 'render-system-multi-camera'
|
|
136
|
+
| 'render-system-multi-light'
|
|
137
|
+
| 'asset-not-registered'
|
|
138
|
+
| 'device-lost'
|
|
139
|
+
| 'oom'
|
|
140
|
+
| 'internal-error'
|
|
141
|
+
| 'hierarchy-broken'
|
|
142
|
+
| 'destroy-after-destroy'
|
|
143
|
+
| 'rhi-descriptor-invalid'
|
|
144
|
+
| 'instancing-exceeds-uniform-cap'
|
|
145
|
+
| 'render-system-empty-worlds'
|
|
146
|
+
| 'render-system-owner-out-of-range';
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Detail structure exclusive to the `shader-compile-failed` path.
|
|
150
|
+
*
|
|
151
|
+
* `compilerMessages` directly forwards the 6 standardized fields of
|
|
152
|
+
* `GPUCompilationMessage` from `@webgpu/types` v0.1.69 (`message` / `type` /
|
|
153
|
+
* `lineNum` / `linePos` / `offset` / `length`); research F-3 finding;
|
|
154
|
+
* plan-decisions OQ-P2 locks full-field forwarding.
|
|
155
|
+
*
|
|
156
|
+
* @see {@link GPUCompilationMessage}
|
|
157
|
+
*/
|
|
158
|
+
export interface RhiShaderCompileDetail {
|
|
159
|
+
readonly compilerMessages: readonly GPUCompilationMessage[];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Detail structure exclusive to the `asset-not-registered` path (D-S6).
|
|
164
|
+
*
|
|
165
|
+
* `assetHandle` carries the offending u32 handle the caller passed via
|
|
166
|
+
* `MeshFilter.assetHandle`; AI users access it through property access
|
|
167
|
+
* (`err.detail.assetHandle`) rather than parsing the message string
|
|
168
|
+
* (charter proposition 4 + F-3 contract surface).
|
|
169
|
+
*/
|
|
170
|
+
export interface RhiAssetNotRegisteredDetail {
|
|
171
|
+
readonly assetHandle: number;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Detail structure exclusive to the `webgpu-runtime-error` path (D-S8).
|
|
176
|
+
*
|
|
177
|
+
* `error` carries the underlying exception object so AI users can inspect the
|
|
178
|
+
* root cause (`.code` / `.expected` / `.hint` for `RhiError` paths, or
|
|
179
|
+
* `.code` + `.message` for non-RhiError falls) without parsing the
|
|
180
|
+
* RhiError.message field. Optional: the K-9 silent-skip fan-out root path
|
|
181
|
+
* may emit `webgpu-runtime-error` without `.detail` when the underlying
|
|
182
|
+
* exception is unavailable.
|
|
183
|
+
*
|
|
184
|
+
* feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M4 / T-M4-02:
|
|
185
|
+
* `error` field type widened from `string` to `RhiError | { code: string;
|
|
186
|
+
* message: string }` so downstream `switch (err.code)` handlers can narrow
|
|
187
|
+
* the inner error (`.code` / `.expected` / `.hint`) without an `as` cast.
|
|
188
|
+
*/
|
|
189
|
+
export interface RhiWebgpuRuntimeDetail {
|
|
190
|
+
readonly error: RhiError | { code: string; message: string; name?: string };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Detail structure exclusive to the `limit-exceeded` path.
|
|
195
|
+
*
|
|
196
|
+
* `maxStorageBufferBindingSize` carries the device-reported storage cap
|
|
197
|
+
* (`device.limits.maxStorageBufferBindingSize`); `requestedBytes`
|
|
198
|
+
* carries the byte count the caller attempted to allocate. AI users
|
|
199
|
+
* access these through typed property access (`err.detail.maxStorageBufferBindingSize`
|
|
200
|
+
* / `err.detail.requestedBytes`) rather than parsing the message string
|
|
201
|
+
* — charter proposition 4 structured-error consumption path; `err.hint`
|
|
202
|
+
* is for human eyeballs only.
|
|
203
|
+
*
|
|
204
|
+
* Single live emit point: the RenderSystem record stage per-entity
|
|
205
|
+
* instance buffer upload path
|
|
206
|
+
* (`packages/runtime/src/render-system-record.ts`). The 18-member
|
|
207
|
+
* `RhiErrorCode` union is unchanged (`'limit-exceeded'` discriminant
|
|
208
|
+
* preserved); evolution major rename + replace of the discriminated
|
|
209
|
+
* `detail` shape per AGENTS.md Change stance + plan-strategy D-3.
|
|
210
|
+
*
|
|
211
|
+
* Migration history:
|
|
212
|
+
* - feat-20260513-instanced-mesh M5: detail reshape from
|
|
213
|
+
* `{ renderableCount, limit }` to `{ maxStorageBufferBindingSize,
|
|
214
|
+
* requestedBytes }`. Emit point at the time was
|
|
215
|
+
* `AssetRegistry.createInstancedBuffer`.
|
|
216
|
+
* - feat-20260514-ecs-children-instances-managed-buffer-array M3 / w15:
|
|
217
|
+
* `AssetRegistry.createInstancedBuffer` deleted alongside the
|
|
218
|
+
* `InstancedBufferAsset` POD; emit point migrated to the record
|
|
219
|
+
* stage upload path (`requestedBytes` now equals
|
|
220
|
+
* `Instances.transforms.byteLength` per Instances-bearing entity).
|
|
221
|
+
*/
|
|
222
|
+
export interface LimitExceededDetail {
|
|
223
|
+
readonly maxStorageBufferBindingSize: number;
|
|
224
|
+
readonly requestedBytes: number;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Detail structure exclusive to the `'render-system-multi-light'` path
|
|
229
|
+
* (feat-20260519-light-casters-point-spot-pbr M3 / w20 + plan-strategy
|
|
230
|
+
* section 8 (3) (b)).
|
|
231
|
+
*
|
|
232
|
+
* Emitted by the RenderSystem record stage when first-slice cap exceedance
|
|
233
|
+
* is detected: `type` discriminates the offending bucket
|
|
234
|
+
* (`'directional'` for N>1 / `'point'` or `'spot'` for N>4); `got`
|
|
235
|
+
* carries the observed entity count so AI users can branch via property
|
|
236
|
+
* access (`err.detail.type === 'point' && err.detail.got > 4`) rather
|
|
237
|
+
* than parsing the message string (charter proposition 4 + F-3 contract
|
|
238
|
+
* surface).
|
|
239
|
+
*
|
|
240
|
+
* Single live emit point: the RenderSystem record stage three-bucket
|
|
241
|
+
* fail-fast (`packages/runtime/src/render-system-record.ts`). Minor
|
|
242
|
+
* additive evolution per AGENTS.md error model evolution contract.
|
|
243
|
+
*/
|
|
244
|
+
export interface RhiMultiLightDetail {
|
|
245
|
+
readonly type: 'directional' | 'point' | 'spot';
|
|
246
|
+
readonly got: number;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Detail structure exclusive to the `'instancing-exceeds-uniform-cap'` path
|
|
251
|
+
* (feat-20260622-chunk-gpu-instancing-sprite-tilemap M2 / w10 +
|
|
252
|
+
* plan-strategy 2 D-2 + research N-1).
|
|
253
|
+
*
|
|
254
|
+
* Emitted by the record-stage fold dispatch loop
|
|
255
|
+
* (`packages/runtime/src/render-system-record.ts`) when
|
|
256
|
+
* `caps.storageBuffer === false` AND a fold bucket carries more than
|
|
257
|
+
* `limit` instances. The engine fires the error AND falls the offending
|
|
258
|
+
* bucket back to per-entity drawIndexed via the same exit the mode-gate
|
|
259
|
+
* bypass uses (plan-strategy D-9 "shared fallback exit"), so the frame
|
|
260
|
+
* is still visually correct (no identity-collapse / black screen) and
|
|
261
|
+
* the cap event surfaces structurally for AI users to observe.
|
|
262
|
+
*
|
|
263
|
+
* Fields:
|
|
264
|
+
* - `requested` — the offending bucket's instance count
|
|
265
|
+
* (`FoldBucket.bucketSize`); always strictly greater than `limit` at
|
|
266
|
+
* emit time (the cap-check helper guards `requested > limit`).
|
|
267
|
+
* - `limit` — the literal 128. The value is locked at the type level
|
|
268
|
+
* because the cap is structurally tied to the WebGL2 minimum 16384 B
|
|
269
|
+
* UBO size (128 * 64 B mat4 stride = 8192 B leaves headroom for the
|
|
270
|
+
* per-frame material UBO slice — research N-1). A future cap change
|
|
271
|
+
* would be a major evolution, not a runtime knob.
|
|
272
|
+
* - `scope` — closed `'sprite' | 'tilemap-chunk'` discriminator that
|
|
273
|
+
* pinpoints the dispatch site (sprite-pass entry that came directly
|
|
274
|
+
* from a user-spawned Sprite vs one derived by
|
|
275
|
+
* `tilemap-chunk-extract-system`). AI users branch on `.scope` to
|
|
276
|
+
* decide whether to shrink the sprite batch size or the tilemap
|
|
277
|
+
* chunk size.
|
|
278
|
+
*
|
|
279
|
+
* AI-user consumption (charter P3 + plan-strategy 8.3):
|
|
280
|
+
* ```ts
|
|
281
|
+
* if (err.code === 'instancing-exceeds-uniform-cap') {
|
|
282
|
+
* const d = err.detail as RhiInstancingExceedsUniformCapDetail;
|
|
283
|
+
* if (d.scope === 'sprite') shrinkSpriteBatchSize(d.requested);
|
|
284
|
+
* else if (d.scope === 'tilemap-chunk') shrinkTileChunkSize(d.requested);
|
|
285
|
+
* }
|
|
286
|
+
* ```
|
|
287
|
+
* — never parse `err.message`. The discriminated `detail` field is the
|
|
288
|
+
* surface; the human-readable `err.message` is for logs only.
|
|
289
|
+
*/
|
|
290
|
+
export interface RhiInstancingExceedsUniformCapDetail {
|
|
291
|
+
readonly requested: number;
|
|
292
|
+
readonly limit: 128;
|
|
293
|
+
readonly scope: 'sprite' | 'tilemap-chunk';
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Detail structure exclusive to the `'render-system-owner-out-of-range'` path
|
|
298
|
+
* (feat-20260708-composited-multi-world-rendering M3 / D-5).
|
|
299
|
+
*
|
|
300
|
+
* Emitted by `renderer.draw(worlds, { cameraOwner, resourceOwner })` when one
|
|
301
|
+
* owner index is not valid for `worlds` (`owner < 0` or `owner >= worlds.length`).
|
|
302
|
+
* An out-of-range index cannot resolve, so the frame is skipped before extract.
|
|
303
|
+
*
|
|
304
|
+
* Fields:
|
|
305
|
+
* - `role` — WHICH of the two draw-owner indices was out of range
|
|
306
|
+
* (feat-20260709-editor-world-partition M1 / w7). `draw(worlds, {
|
|
307
|
+
* cameraOwner, resourceOwner })` carries two independent indices; `role`
|
|
308
|
+
* tells the AI user whether the camera-source index (`'camera'`) or the
|
|
309
|
+
* singleton-resource index (`'resource'`) is the offender, so the fix is
|
|
310
|
+
* unambiguous from the text channel (no new error code — D-3 keeps 0 net
|
|
311
|
+
* new codes; the discriminator lives in `.detail`). When both indices are
|
|
312
|
+
* out of range the first offender is reported: `cameraOwner` is validated
|
|
313
|
+
* before `resourceOwner`, so `role === 'camera'`.
|
|
314
|
+
* - `owner` — the offending index the caller passed (the `role` index's
|
|
315
|
+
* value).
|
|
316
|
+
* - `worldCount` — `worlds.length` at call time (the valid range is
|
|
317
|
+
* `0 .. worldCount - 1`).
|
|
318
|
+
*
|
|
319
|
+
* AI users branch via property access (`err.detail.role` / `err.detail.owner` /
|
|
320
|
+
* `err.detail.worldCount`) after narrowing on `.code`, rather than parsing the
|
|
321
|
+
* message string (charter P3 structured-failure surface).
|
|
322
|
+
*
|
|
323
|
+
* The sibling `'render-system-empty-worlds'` path carries no `.detail` — an
|
|
324
|
+
* empty array is fully described by `.code`, and the entry check short-circuits
|
|
325
|
+
* to that code before the owner-range check runs (the two codes are
|
|
326
|
+
* non-exclusive).
|
|
327
|
+
*/
|
|
328
|
+
export interface RhiOwnerOutOfRangeDetail {
|
|
329
|
+
readonly role: 'camera' | 'resource';
|
|
330
|
+
readonly owner: number;
|
|
331
|
+
readonly worldCount: number;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Tagged union of `.detail` shapes carried by structured errors.
|
|
336
|
+
*
|
|
337
|
+
* Entries:
|
|
338
|
+
* - `RhiShaderCompileDetail` (carries `compilerMessages`) - emitted on the
|
|
339
|
+
* `'shader-compile-failed'` path.
|
|
340
|
+
* - `RhiAssetNotRegisteredDetail` (carries `assetHandle`) - emitted on the
|
|
341
|
+
* `'asset-not-registered'` path (D-S6).
|
|
342
|
+
* - `RhiWebgpuRuntimeDetail` (carries `error: RhiError | { code, message }`) - optionally emitted
|
|
343
|
+
* on the `'webgpu-runtime-error'` path when a captured `Error.message` is
|
|
344
|
+
* available (D-S8).
|
|
345
|
+
* - `LimitExceededDetail` (carries `maxStorageBufferBindingSize` +
|
|
346
|
+
* `requestedBytes`) - emitted on the `'limit-exceeded'` path when
|
|
347
|
+
* the RenderSystem record stage's per-entity Instances upload
|
|
348
|
+
* exceeds `device.limits.maxStorageBufferBindingSize`
|
|
349
|
+
* (feat-20260514-ecs-children-instances-managed-buffer-array M3 / w15;
|
|
350
|
+
* emit point migrated from the deleted
|
|
351
|
+
* `AssetRegistry.createInstancedBuffer` factory).
|
|
352
|
+
*
|
|
353
|
+
* The other 15 paths leave `.detail = undefined` (charter proposition 4
|
|
354
|
+
* baseline).
|
|
355
|
+
*/
|
|
356
|
+
export type RhiErrorDetail =
|
|
357
|
+
| RhiShaderCompileDetail
|
|
358
|
+
| RhiAssetNotRegisteredDetail
|
|
359
|
+
| RhiWebgpuRuntimeDetail
|
|
360
|
+
| LimitExceededDetail
|
|
361
|
+
| RhiMultiLightDetail
|
|
362
|
+
| RhiInstancingExceedsUniformCapDetail
|
|
363
|
+
| RhiOwnerOutOfRangeDetail;
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Structured RHI error.
|
|
367
|
+
*
|
|
368
|
+
* Three readonly fields aligned with AGENTS.md "Errors are structured":
|
|
369
|
+
* - `.code` - closed union member (L1 key signal).
|
|
370
|
+
* - `.expected` - expected-state description (L2 detail).
|
|
371
|
+
* - `.hint` - actionable recovery guidance (L2 detail; charter proposition 3:
|
|
372
|
+
* machine-readable hint > prose).
|
|
373
|
+
*
|
|
374
|
+
* `.detail` is populated on four paths:
|
|
375
|
+
* - `code === 'shader-compile-failed'` -> `RhiShaderCompileDetail`
|
|
376
|
+
* - `code === 'asset-not-registered'` -> `RhiAssetNotRegisteredDetail`
|
|
377
|
+
* - `code === 'webgpu-runtime-error'` -> `RhiWebgpuRuntimeDetail` (optional)
|
|
378
|
+
* - `code === 'limit-exceeded'` -> `LimitExceededDetail`
|
|
379
|
+
* (feat-20260513-instanced-mesh M5 reshape; carries
|
|
380
|
+
* `maxStorageBufferBindingSize` + `requestedBytes`)
|
|
381
|
+
*
|
|
382
|
+
* The other 15 paths leave `.detail = undefined` (charter proposition 4
|
|
383
|
+
* baseline).
|
|
384
|
+
*
|
|
385
|
+
* Note: `RhiErrorDetail` is currently a flat tagged union without a
|
|
386
|
+
* `code` discriminant field on each variant; AI users perform typed
|
|
387
|
+
* narrowing via outer `switch (err.code)` then a one-time `as` cast on
|
|
388
|
+
* `err.detail` per the documented variant. Full discriminated-union
|
|
389
|
+
* refactor (each variant carrying its own `code` literal field) is left
|
|
390
|
+
* to `feat-future-rhi-error-detail-discriminant` spinoff.
|
|
391
|
+
*/
|
|
392
|
+
export class RhiError extends Error {
|
|
393
|
+
readonly code: RhiErrorCode;
|
|
394
|
+
readonly expected: string;
|
|
395
|
+
readonly hint: string;
|
|
396
|
+
readonly detail: RhiErrorDetail | undefined;
|
|
397
|
+
|
|
398
|
+
constructor(args: {
|
|
399
|
+
code: RhiErrorCode;
|
|
400
|
+
expected: string;
|
|
401
|
+
hint: string;
|
|
402
|
+
detail?: RhiErrorDetail | undefined;
|
|
403
|
+
}) {
|
|
404
|
+
super(`[RhiError ${args.code}] expected: ${args.expected}; hint: ${args.hint}`);
|
|
405
|
+
this.name = 'RhiError';
|
|
406
|
+
this.code = args.code;
|
|
407
|
+
this.expected = args.expected;
|
|
408
|
+
this.hint = args.hint;
|
|
409
|
+
this.detail = args.detail;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* The two split draw-owner indices carried by
|
|
415
|
+
* `draw(worlds, { cameraOwner, resourceOwner })`
|
|
416
|
+
* (feat-20260709-editor-world-partition M1 / w6). `cameraOwner` selects the
|
|
417
|
+
* world whose cameras are surfaced; `resourceOwner` selects the world whose
|
|
418
|
+
* skylight / skybox / postProcessParams are surfaced. Declared here (World-free
|
|
419
|
+
* primitives) so the validator and the `RhiOwnerOutOfRangeDetail.role`
|
|
420
|
+
* discriminator live in one SSOT package (architecture-principles §1).
|
|
421
|
+
*/
|
|
422
|
+
export interface DrawOwnerSplit {
|
|
423
|
+
readonly cameraOwner: number;
|
|
424
|
+
readonly resourceOwner: number;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Validate `renderer.draw(worlds, { cameraOwner, resourceOwner })` arguments at
|
|
429
|
+
* the draw entry (feat-20260708 M3 / D-5, extended by
|
|
430
|
+
* feat-20260709-editor-world-partition M1 / w6-w7).
|
|
431
|
+
*
|
|
432
|
+
* The validator takes primitives (`worldCount = worlds.length`, plus the owner
|
|
433
|
+
* index/indices) — no `World`, no math — so it lives in `@forgeax/engine-rhi`
|
|
434
|
+
* alongside the `RhiErrorCode` members it emits (architecture-principles §1
|
|
435
|
+
* SSOT). The runtime `createRenderer` draw entry calls it before any extract; a
|
|
436
|
+
* non-`ok` result skips the frame with a structured error (charter P3), never a
|
|
437
|
+
* silent no-op.
|
|
438
|
+
*
|
|
439
|
+
* Checks, in order (D-5 + w6):
|
|
440
|
+
* 1. `worldCount === 0` -> `'render-system-empty-worlds'` (no `.detail`).
|
|
441
|
+
* 2. `cameraOwner` is not a valid index -> `'render-system-owner-out-of-range'`
|
|
442
|
+
* with `.detail = { role: 'camera', owner: cameraOwner, worldCount }`.
|
|
443
|
+
* 3. `resourceOwner` is not a valid index -> `'render-system-owner-out-of-range'`
|
|
444
|
+
* with `.detail = { role: 'resource', owner: resourceOwner, worldCount }`.
|
|
445
|
+
* The `Number.isInteger` guard rejects a `NaN` / fractional / undefined-coerced
|
|
446
|
+
* index a JS caller could pass despite the compile-time requirement. When both
|
|
447
|
+
* indices are out of range the FIRST offender wins: `cameraOwner` is checked
|
|
448
|
+
* before `resourceOwner`, so `role === 'camera'` (D-3 / w3 contract).
|
|
449
|
+
*
|
|
450
|
+
* The empty-worlds guard short-circuits before either owner-range check.
|
|
451
|
+
*/
|
|
452
|
+
export function validateDrawArgs(
|
|
453
|
+
worldCount: number,
|
|
454
|
+
owner: DrawOwnerSplit,
|
|
455
|
+
): Result<void, RhiError> {
|
|
456
|
+
if (worldCount === 0) {
|
|
457
|
+
return err(
|
|
458
|
+
new RhiError({
|
|
459
|
+
code: 'render-system-empty-worlds',
|
|
460
|
+
expected: 'worlds array has at least one world',
|
|
461
|
+
hint: 'pass at least one world: draw([world], { cameraOwner: 0, resourceOwner: 0 })',
|
|
462
|
+
}),
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
const { cameraOwner, resourceOwner } = owner;
|
|
466
|
+
const outOfRange = (index: number): boolean =>
|
|
467
|
+
!Number.isInteger(index) || index < 0 || index >= worldCount;
|
|
468
|
+
// cameraOwner is validated first: it is the first offender when both indices
|
|
469
|
+
// are out of range (w3 contract). role names which index the AI user fixes.
|
|
470
|
+
if (outOfRange(cameraOwner)) {
|
|
471
|
+
return err(
|
|
472
|
+
new RhiError({
|
|
473
|
+
code: 'render-system-owner-out-of-range',
|
|
474
|
+
expected: 'cameraOwner is an index into worlds (0 <= cameraOwner < worlds.length)',
|
|
475
|
+
hint: 'cameraOwner must be in 0..worlds.length-1; the cameraOwner world supplies the surfaced cameras',
|
|
476
|
+
detail: { role: 'camera', owner: cameraOwner, worldCount },
|
|
477
|
+
}),
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
if (outOfRange(resourceOwner)) {
|
|
481
|
+
return err(
|
|
482
|
+
new RhiError({
|
|
483
|
+
code: 'render-system-owner-out-of-range',
|
|
484
|
+
expected: 'resourceOwner is an index into worlds (0 <= resourceOwner < worlds.length)',
|
|
485
|
+
hint: 'resourceOwner must be in 0..worlds.length-1; the resourceOwner world supplies skylight/skybox/postProcess',
|
|
486
|
+
detail: { role: 'resource', owner: resourceOwner, worldCount },
|
|
487
|
+
}),
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
return ok(undefined);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Result<T, E> + ok / err live in `@forgeax/engine-types` (tweak-20260612-result-
|
|
494
|
+
// into-types). They were duplicated here ("byte-for-byte aligned" by prose) and
|
|
495
|
+
// in packages/ecs/src/result.ts; SSOT consolidated upstream. The barrel here
|
|
496
|
+
// re-exports them so existing `import { err, ok, Result, ResultOk, ResultErr }
|
|
497
|
+
// from '@forgeax/engine-rhi'` consumers stay unchanged.
|
|
498
|
+
export {
|
|
499
|
+
err,
|
|
500
|
+
ok,
|
|
501
|
+
type Result,
|
|
502
|
+
type ResultErr,
|
|
503
|
+
type ResultOk,
|
|
504
|
+
} from '@forgeax/engine-types';
|