@forgeax/engine-rhi 0.0.0-dev.8d955ade1c79

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 (77) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +234 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/async-form.test-d.d.ts +2 -0
  5. package/dist/__tests__/async-form.test-d.d.ts.map +1 -0
  6. package/dist/__tests__/binding-resource.test-d.d.ts +2 -0
  7. package/dist/__tests__/binding-resource.test-d.d.ts.map +1 -0
  8. package/dist/__tests__/buffer-mapping.test-d.d.ts +2 -0
  9. package/dist/__tests__/buffer-mapping.test-d.d.ts.map +1 -0
  10. package/dist/__tests__/canvasContext.test-d.d.ts +2 -0
  11. package/dist/__tests__/canvasContext.test-d.d.ts.map +1 -0
  12. package/dist/__tests__/caps-three-way.unit.test.d.ts +2 -0
  13. package/dist/__tests__/caps-three-way.unit.test.d.ts.map +1 -0
  14. package/dist/__tests__/caps.test-d.d.ts +2 -0
  15. package/dist/__tests__/caps.test-d.d.ts.map +1 -0
  16. package/dist/__tests__/command-encoder-spec-alignment.test-d.d.ts +2 -0
  17. package/dist/__tests__/command-encoder-spec-alignment.test-d.d.ts.map +1 -0
  18. package/dist/__tests__/createComputePipeline.test-d.d.ts +2 -0
  19. package/dist/__tests__/createComputePipeline.test-d.d.ts.map +1 -0
  20. package/dist/__tests__/createQuerySet.test-d.d.ts +2 -0
  21. package/dist/__tests__/createQuerySet.test-d.d.ts.map +1 -0
  22. package/dist/__tests__/createTextureView.test-d.d.ts +2 -0
  23. package/dist/__tests__/createTextureView.test-d.d.ts.map +1 -0
  24. package/dist/__tests__/descriptor-mirror.test-d.d.ts +2 -0
  25. package/dist/__tests__/descriptor-mirror.test-d.d.ts.map +1 -0
  26. package/dist/__tests__/draw-validation-errors.test.d.ts +2 -0
  27. package/dist/__tests__/draw-validation-errors.test.d.ts.map +1 -0
  28. package/dist/__tests__/errors-instancing.unit.test.d.ts +2 -0
  29. package/dist/__tests__/errors-instancing.unit.test.d.ts.map +1 -0
  30. package/dist/__tests__/errors.test-d.d.ts +2 -0
  31. package/dist/__tests__/errors.test-d.d.ts.map +1 -0
  32. package/dist/__tests__/limit-exceeded-detail.test-d.d.ts +2 -0
  33. package/dist/__tests__/limit-exceeded-detail.test-d.d.ts.map +1 -0
  34. package/dist/__tests__/mapped-buffer.test-d.d.ts +2 -0
  35. package/dist/__tests__/mapped-buffer.test-d.d.ts.map +1 -0
  36. package/dist/__tests__/opaque-handle.test-d.d.ts +2 -0
  37. package/dist/__tests__/opaque-handle.test-d.d.ts.map +1 -0
  38. package/dist/__tests__/owner-out-of-range-role.test.d.ts +2 -0
  39. package/dist/__tests__/owner-out-of-range-role.test.d.ts.map +1 -0
  40. package/dist/__tests__/queue-writes.test-d.d.ts +2 -0
  41. package/dist/__tests__/queue-writes.test-d.d.ts.map +1 -0
  42. package/dist/__tests__/render-pass-encoder-spec-alignment.test-d.d.ts +2 -0
  43. package/dist/__tests__/render-pass-encoder-spec-alignment.test-d.d.ts.map +1 -0
  44. package/dist/__tests__/requestAdapter.test-d.d.ts +2 -0
  45. package/dist/__tests__/requestAdapter.test-d.d.ts.map +1 -0
  46. package/dist/errors.d.ts +315 -0
  47. package/dist/errors.d.ts.map +1 -0
  48. package/dist/errors.mjs +56 -0
  49. package/dist/errors.mjs.map +1 -0
  50. package/dist/index.d.ts +1758 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.mjs +87 -0
  53. package/dist/index.mjs.map +1 -0
  54. package/package.json +61 -0
  55. package/src/__tests__/async-form.test-d.ts +40 -0
  56. package/src/__tests__/binding-resource.test-d.ts +66 -0
  57. package/src/__tests__/buffer-mapping.test-d.ts +84 -0
  58. package/src/__tests__/canvasContext.test-d.ts +106 -0
  59. package/src/__tests__/caps-three-way.unit.test.ts +144 -0
  60. package/src/__tests__/caps.test-d.ts +83 -0
  61. package/src/__tests__/command-encoder-spec-alignment.test-d.ts +117 -0
  62. package/src/__tests__/createComputePipeline.test-d.ts +149 -0
  63. package/src/__tests__/createQuerySet.test-d.ts +81 -0
  64. package/src/__tests__/createTextureView.test-d.ts +127 -0
  65. package/src/__tests__/descriptor-mirror.test-d.ts +284 -0
  66. package/src/__tests__/draw-validation-errors.test.ts +155 -0
  67. package/src/__tests__/errors-instancing.unit.test.ts +167 -0
  68. package/src/__tests__/errors.test-d.ts +199 -0
  69. package/src/__tests__/limit-exceeded-detail.test-d.ts +33 -0
  70. package/src/__tests__/mapped-buffer.test-d.ts +65 -0
  71. package/src/__tests__/opaque-handle.test-d.ts +121 -0
  72. package/src/__tests__/owner-out-of-range-role.test.ts +84 -0
  73. package/src/__tests__/queue-writes.test-d.ts +108 -0
  74. package/src/__tests__/render-pass-encoder-spec-alignment.test-d.ts +202 -0
  75. package/src/__tests__/requestAdapter.test-d.ts +123 -0
  76. package/src/errors.ts +504 -0
  77. package/src/index.ts +2276 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createComputePipeline.test-d.d.ts","sourceRoot":"","sources":["../../src/__tests__/createComputePipeline.test-d.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=createQuerySet.test-d.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createQuerySet.test-d.d.ts","sourceRoot":"","sources":["../../src/__tests__/createQuerySet.test-d.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=createTextureView.test-d.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createTextureView.test-d.d.ts","sourceRoot":"","sources":["../../src/__tests__/createTextureView.test-d.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=descriptor-mirror.test-d.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"descriptor-mirror.test-d.d.ts","sourceRoot":"","sources":["../../src/__tests__/descriptor-mirror.test-d.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=draw-validation-errors.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draw-validation-errors.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/draw-validation-errors.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=errors-instancing.unit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors-instancing.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/errors-instancing.unit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=errors.test-d.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.test-d.d.ts","sourceRoot":"","sources":["../../src/__tests__/errors.test-d.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=limit-exceeded-detail.test-d.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"limit-exceeded-detail.test-d.d.ts","sourceRoot":"","sources":["../../src/__tests__/limit-exceeded-detail.test-d.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=mapped-buffer.test-d.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mapped-buffer.test-d.d.ts","sourceRoot":"","sources":["../../src/__tests__/mapped-buffer.test-d.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=opaque-handle.test-d.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opaque-handle.test-d.d.ts","sourceRoot":"","sources":["../../src/__tests__/opaque-handle.test-d.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=owner-out-of-range-role.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"owner-out-of-range-role.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/owner-out-of-range-role.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=queue-writes.test-d.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue-writes.test-d.d.ts","sourceRoot":"","sources":["../../src/__tests__/queue-writes.test-d.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=render-pass-encoder-spec-alignment.test-d.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-pass-encoder-spec-alignment.test-d.d.ts","sourceRoot":"","sources":["../../src/__tests__/render-pass-encoder-spec-alignment.test-d.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=requestAdapter.test-d.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"requestAdapter.test-d.d.ts","sourceRoot":"","sources":["../../src/__tests__/requestAdapter.test-d.ts"],"names":[],"mappings":""}
@@ -0,0 +1,315 @@
1
+ import { type Result } from '@forgeax/engine-types';
2
+ /**
3
+ * Closed RhiErrorCode union. `switch` exhaustive checks need no default
4
+ * fallback - tsc strict mode guards union completeness (charter proposition 4
5
+ * + proposition 3: machine-readable union > prose).
6
+ *
7
+ *
8
+ * The executable union below is the machine-readable source of truth for
9
+ * the 23 members. The package README owns the AI-facing trigger and recovery
10
+ * table; keeping that projection in one documentation owner avoids a second
11
+ * member ledger drifting from the union.
12
+ *
13
+ * @example AI-user exhaustive switch on the 4 command/queue members (no default fallback)
14
+ * ```ts
15
+ * import type { RhiError, RhiErrorCode } from '@forgeax/engine-rhi';
16
+ *
17
+ * function recover(code: RhiErrorCode): string {
18
+ * switch (code) {
19
+ * // ... 6 baseline members elided ...
20
+ * case 'command-encoder-finished': return 'recreate encoder via device.createCommandEncoder()';
21
+ * case 'render-pass-not-ended': return 'call pass.end() before next beginRenderPass()';
22
+ * case 'queue-submit-failed': return 'audit buffer/pipeline lifetimes before submit';
23
+ * case 'queue-write-buffer-out-of-bounds': return 'realign offset and re-check buffer.size';
24
+ * default: return 'baseline path';
25
+ * }
26
+ * }
27
+ * ```
28
+ */
29
+ export type RhiErrorCode = 'adapter-unavailable' | 'feature-not-enabled' | 'limit-exceeded' | 'shader-compile-failed' | 'rhi-not-available' | 'webgpu-runtime-error' | 'command-encoder-finished' | 'render-pass-not-ended' | 'queue-submit-failed' | 'queue-write-buffer-out-of-bounds' | 'render-system-no-camera' | 'render-system-multi-camera' | 'render-system-multi-light' | 'asset-not-registered' | 'device-lost' | 'oom' | 'internal-error' | 'hierarchy-broken' | 'destroy-after-destroy' | 'rhi-descriptor-invalid' | 'instancing-exceeds-uniform-cap' | 'render-system-empty-worlds' | 'render-system-owner-out-of-range';
30
+ /**
31
+ * Detail structure exclusive to the `shader-compile-failed` path.
32
+ *
33
+ * `compilerMessages` directly forwards the 6 standardized fields of
34
+ * `GPUCompilationMessage` from `@webgpu/types` v0.1.69 (`message` / `type` /
35
+ * `lineNum` / `linePos` / `offset` / `length`); research F-3 finding;
36
+ * plan-decisions OQ-P2 locks full-field forwarding.
37
+ *
38
+ * @see {@link GPUCompilationMessage}
39
+ */
40
+ export interface RhiShaderCompileDetail {
41
+ readonly compilerMessages: readonly GPUCompilationMessage[];
42
+ }
43
+ /**
44
+ * Detail structure exclusive to the `asset-not-registered` path (D-S6).
45
+ *
46
+ * `assetHandle` carries the offending u32 handle the caller passed via
47
+ * `MeshFilter.assetHandle`; AI users access it through property access
48
+ * (`err.detail.assetHandle`) rather than parsing the message string
49
+ * (charter proposition 4 + F-3 contract surface).
50
+ */
51
+ export interface RhiAssetNotRegisteredDetail {
52
+ readonly assetHandle: number;
53
+ }
54
+ /**
55
+ * Detail structure exclusive to the `webgpu-runtime-error` path (D-S8).
56
+ *
57
+ * `error` carries the underlying exception object so AI users can inspect the
58
+ * root cause (`.code` / `.expected` / `.hint` for `RhiError` paths, or
59
+ * `.code` + `.message` for non-RhiError falls) without parsing the
60
+ * RhiError.message field. Optional: the K-9 silent-skip fan-out root path
61
+ * may emit `webgpu-runtime-error` without `.detail` when the underlying
62
+ * exception is unavailable.
63
+ *
64
+ * feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M4 / T-M4-02:
65
+ * `error` field type widened from `string` to `RhiError | { code: string;
66
+ * message: string }` so downstream `switch (err.code)` handlers can narrow
67
+ * the inner error (`.code` / `.expected` / `.hint`) without an `as` cast.
68
+ */
69
+ export interface RhiWebgpuRuntimeDetail {
70
+ readonly error: RhiError | {
71
+ code: string;
72
+ message: string;
73
+ name?: string;
74
+ };
75
+ }
76
+ /**
77
+ * Detail structure exclusive to the `limit-exceeded` path.
78
+ *
79
+ * `maxStorageBufferBindingSize` carries the device-reported storage cap
80
+ * (`device.limits.maxStorageBufferBindingSize`); `requestedBytes`
81
+ * carries the byte count the caller attempted to allocate. AI users
82
+ * access these through typed property access (`err.detail.maxStorageBufferBindingSize`
83
+ * / `err.detail.requestedBytes`) rather than parsing the message string
84
+ * — charter proposition 4 structured-error consumption path; `err.hint`
85
+ * is for human eyeballs only.
86
+ *
87
+ * Single live emit point: the RenderSystem record stage per-entity
88
+ * instance buffer upload path
89
+ * (`packages/runtime/src/render-system-record.ts`). The 18-member
90
+ * `RhiErrorCode` union is unchanged (`'limit-exceeded'` discriminant
91
+ * preserved); evolution major rename + replace of the discriminated
92
+ * `detail` shape per AGENTS.md Change stance + plan-strategy D-3.
93
+ *
94
+ * Migration history:
95
+ * - feat-20260513-instanced-mesh M5: detail reshape from
96
+ * `{ renderableCount, limit }` to `{ maxStorageBufferBindingSize,
97
+ * requestedBytes }`. Emit point at the time was
98
+ * `AssetRegistry.createInstancedBuffer`.
99
+ * - feat-20260514-ecs-children-instances-managed-buffer-array M3 / w15:
100
+ * `AssetRegistry.createInstancedBuffer` deleted alongside the
101
+ * `InstancedBufferAsset` POD; emit point migrated to the record
102
+ * stage upload path (`requestedBytes` now equals
103
+ * `Instances.transforms.byteLength` per Instances-bearing entity).
104
+ */
105
+ export interface LimitExceededDetail {
106
+ readonly maxStorageBufferBindingSize: number;
107
+ readonly requestedBytes: number;
108
+ }
109
+ /**
110
+ * Detail structure exclusive to the `'render-system-multi-light'` path
111
+ * (feat-20260519-light-casters-point-spot-pbr M3 / w20 + plan-strategy
112
+ * section 8 (3) (b)).
113
+ *
114
+ * Emitted by the RenderSystem record stage when first-slice cap exceedance
115
+ * is detected: `type` discriminates the offending bucket
116
+ * (`'directional'` for N>1 / `'point'` or `'spot'` for N>4); `got`
117
+ * carries the observed entity count so AI users can branch via property
118
+ * access (`err.detail.type === 'point' && err.detail.got > 4`) rather
119
+ * than parsing the message string (charter proposition 4 + F-3 contract
120
+ * surface).
121
+ *
122
+ * Single live emit point: the RenderSystem record stage three-bucket
123
+ * fail-fast (`packages/runtime/src/render-system-record.ts`). Minor
124
+ * additive evolution per AGENTS.md error model evolution contract.
125
+ */
126
+ export interface RhiMultiLightDetail {
127
+ readonly type: 'directional' | 'point' | 'spot';
128
+ readonly got: number;
129
+ }
130
+ /**
131
+ * Detail structure exclusive to the `'instancing-exceeds-uniform-cap'` path
132
+ * (feat-20260622-chunk-gpu-instancing-sprite-tilemap M2 / w10 +
133
+ * plan-strategy 2 D-2 + research N-1).
134
+ *
135
+ * Emitted by the record-stage fold dispatch loop
136
+ * (`packages/runtime/src/render-system-record.ts`) when
137
+ * `caps.storageBuffer === false` AND a fold bucket carries more than
138
+ * `limit` instances. The engine fires the error AND falls the offending
139
+ * bucket back to per-entity drawIndexed via the same exit the mode-gate
140
+ * bypass uses (plan-strategy D-9 "shared fallback exit"), so the frame
141
+ * is still visually correct (no identity-collapse / black screen) and
142
+ * the cap event surfaces structurally for AI users to observe.
143
+ *
144
+ * Fields:
145
+ * - `requested` — the offending bucket's instance count
146
+ * (`FoldBucket.bucketSize`); always strictly greater than `limit` at
147
+ * emit time (the cap-check helper guards `requested > limit`).
148
+ * - `limit` — the literal 128. The value is locked at the type level
149
+ * because the cap is structurally tied to the WebGL2 minimum 16384 B
150
+ * UBO size (128 * 64 B mat4 stride = 8192 B leaves headroom for the
151
+ * per-frame material UBO slice — research N-1). A future cap change
152
+ * would be a major evolution, not a runtime knob.
153
+ * - `scope` — closed `'sprite' | 'tilemap-chunk'` discriminator that
154
+ * pinpoints the dispatch site (sprite-pass entry that came directly
155
+ * from a user-spawned Sprite vs one derived by
156
+ * `tilemap-chunk-extract-system`). AI users branch on `.scope` to
157
+ * decide whether to shrink the sprite batch size or the tilemap
158
+ * chunk size.
159
+ *
160
+ * AI-user consumption (charter P3 + plan-strategy 8.3):
161
+ * ```ts
162
+ * if (err.code === 'instancing-exceeds-uniform-cap') {
163
+ * const d = err.detail as RhiInstancingExceedsUniformCapDetail;
164
+ * if (d.scope === 'sprite') shrinkSpriteBatchSize(d.requested);
165
+ * else if (d.scope === 'tilemap-chunk') shrinkTileChunkSize(d.requested);
166
+ * }
167
+ * ```
168
+ * — never parse `err.message`. The discriminated `detail` field is the
169
+ * surface; the human-readable `err.message` is for logs only.
170
+ */
171
+ export interface RhiInstancingExceedsUniformCapDetail {
172
+ readonly requested: number;
173
+ readonly limit: 128;
174
+ readonly scope: 'sprite' | 'tilemap-chunk';
175
+ }
176
+ /**
177
+ * Detail structure exclusive to the `'render-system-owner-out-of-range'` path
178
+ * (feat-20260708-composited-multi-world-rendering M3 / D-5).
179
+ *
180
+ * Emitted by `renderer.draw(worlds, { cameraOwner, resourceOwner })` when one
181
+ * owner index is not valid for `worlds` (`owner < 0` or `owner >= worlds.length`).
182
+ * An out-of-range index cannot resolve, so the frame is skipped before extract.
183
+ *
184
+ * Fields:
185
+ * - `role` — WHICH of the two draw-owner indices was out of range
186
+ * (feat-20260709-editor-world-partition M1 / w7). `draw(worlds, {
187
+ * cameraOwner, resourceOwner })` carries two independent indices; `role`
188
+ * tells the AI user whether the camera-source index (`'camera'`) or the
189
+ * singleton-resource index (`'resource'`) is the offender, so the fix is
190
+ * unambiguous from the text channel (no new error code — D-3 keeps 0 net
191
+ * new codes; the discriminator lives in `.detail`). When both indices are
192
+ * out of range the first offender is reported: `cameraOwner` is validated
193
+ * before `resourceOwner`, so `role === 'camera'`.
194
+ * - `owner` — the offending index the caller passed (the `role` index's
195
+ * value).
196
+ * - `worldCount` — `worlds.length` at call time (the valid range is
197
+ * `0 .. worldCount - 1`).
198
+ *
199
+ * AI users branch via property access (`err.detail.role` / `err.detail.owner` /
200
+ * `err.detail.worldCount`) after narrowing on `.code`, rather than parsing the
201
+ * message string (charter P3 structured-failure surface).
202
+ *
203
+ * The sibling `'render-system-empty-worlds'` path carries no `.detail` — an
204
+ * empty array is fully described by `.code`, and the entry check short-circuits
205
+ * to that code before the owner-range check runs (the two codes are
206
+ * non-exclusive).
207
+ */
208
+ export interface RhiOwnerOutOfRangeDetail {
209
+ readonly role: 'camera' | 'resource';
210
+ readonly owner: number;
211
+ readonly worldCount: number;
212
+ }
213
+ /**
214
+ * Tagged union of `.detail` shapes carried by structured errors.
215
+ *
216
+ * Entries:
217
+ * - `RhiShaderCompileDetail` (carries `compilerMessages`) - emitted on the
218
+ * `'shader-compile-failed'` path.
219
+ * - `RhiAssetNotRegisteredDetail` (carries `assetHandle`) - emitted on the
220
+ * `'asset-not-registered'` path (D-S6).
221
+ * - `RhiWebgpuRuntimeDetail` (carries `error: RhiError | { code, message }`) - optionally emitted
222
+ * on the `'webgpu-runtime-error'` path when a captured `Error.message` is
223
+ * available (D-S8).
224
+ * - `LimitExceededDetail` (carries `maxStorageBufferBindingSize` +
225
+ * `requestedBytes`) - emitted on the `'limit-exceeded'` path when
226
+ * the RenderSystem record stage's per-entity Instances upload
227
+ * exceeds `device.limits.maxStorageBufferBindingSize`
228
+ * (feat-20260514-ecs-children-instances-managed-buffer-array M3 / w15;
229
+ * emit point migrated from the deleted
230
+ * `AssetRegistry.createInstancedBuffer` factory).
231
+ *
232
+ * The other 15 paths leave `.detail = undefined` (charter proposition 4
233
+ * baseline).
234
+ */
235
+ export type RhiErrorDetail = RhiShaderCompileDetail | RhiAssetNotRegisteredDetail | RhiWebgpuRuntimeDetail | LimitExceededDetail | RhiMultiLightDetail | RhiInstancingExceedsUniformCapDetail | RhiOwnerOutOfRangeDetail;
236
+ /**
237
+ * Structured RHI error.
238
+ *
239
+ * Three readonly fields aligned with AGENTS.md "Errors are structured":
240
+ * - `.code` - closed union member (L1 key signal).
241
+ * - `.expected` - expected-state description (L2 detail).
242
+ * - `.hint` - actionable recovery guidance (L2 detail; charter proposition 3:
243
+ * machine-readable hint > prose).
244
+ *
245
+ * `.detail` is populated on four paths:
246
+ * - `code === 'shader-compile-failed'` -> `RhiShaderCompileDetail`
247
+ * - `code === 'asset-not-registered'` -> `RhiAssetNotRegisteredDetail`
248
+ * - `code === 'webgpu-runtime-error'` -> `RhiWebgpuRuntimeDetail` (optional)
249
+ * - `code === 'limit-exceeded'` -> `LimitExceededDetail`
250
+ * (feat-20260513-instanced-mesh M5 reshape; carries
251
+ * `maxStorageBufferBindingSize` + `requestedBytes`)
252
+ *
253
+ * The other 15 paths leave `.detail = undefined` (charter proposition 4
254
+ * baseline).
255
+ *
256
+ * Note: `RhiErrorDetail` is currently a flat tagged union without a
257
+ * `code` discriminant field on each variant; AI users perform typed
258
+ * narrowing via outer `switch (err.code)` then a one-time `as` cast on
259
+ * `err.detail` per the documented variant. Full discriminated-union
260
+ * refactor (each variant carrying its own `code` literal field) is left
261
+ * to `feat-future-rhi-error-detail-discriminant` spinoff.
262
+ */
263
+ export declare class RhiError extends Error {
264
+ readonly code: RhiErrorCode;
265
+ readonly expected: string;
266
+ readonly hint: string;
267
+ readonly detail: RhiErrorDetail | undefined;
268
+ constructor(args: {
269
+ code: RhiErrorCode;
270
+ expected: string;
271
+ hint: string;
272
+ detail?: RhiErrorDetail | undefined;
273
+ });
274
+ }
275
+ /**
276
+ * The two split draw-owner indices carried by
277
+ * `draw(worlds, { cameraOwner, resourceOwner })`
278
+ * (feat-20260709-editor-world-partition M1 / w6). `cameraOwner` selects the
279
+ * world whose cameras are surfaced; `resourceOwner` selects the world whose
280
+ * skylight / skybox / postProcessParams are surfaced. Declared here (World-free
281
+ * primitives) so the validator and the `RhiOwnerOutOfRangeDetail.role`
282
+ * discriminator live in one SSOT package (architecture-principles §1).
283
+ */
284
+ export interface DrawOwnerSplit {
285
+ readonly cameraOwner: number;
286
+ readonly resourceOwner: number;
287
+ }
288
+ /**
289
+ * Validate `renderer.draw(worlds, { cameraOwner, resourceOwner })` arguments at
290
+ * the draw entry (feat-20260708 M3 / D-5, extended by
291
+ * feat-20260709-editor-world-partition M1 / w6-w7).
292
+ *
293
+ * The validator takes primitives (`worldCount = worlds.length`, plus the owner
294
+ * index/indices) — no `World`, no math — so it lives in `@forgeax/engine-rhi`
295
+ * alongside the `RhiErrorCode` members it emits (architecture-principles §1
296
+ * SSOT). The runtime `createRenderer` draw entry calls it before any extract; a
297
+ * non-`ok` result skips the frame with a structured error (charter P3), never a
298
+ * silent no-op.
299
+ *
300
+ * Checks, in order (D-5 + w6):
301
+ * 1. `worldCount === 0` -> `'render-system-empty-worlds'` (no `.detail`).
302
+ * 2. `cameraOwner` is not a valid index -> `'render-system-owner-out-of-range'`
303
+ * with `.detail = { role: 'camera', owner: cameraOwner, worldCount }`.
304
+ * 3. `resourceOwner` is not a valid index -> `'render-system-owner-out-of-range'`
305
+ * with `.detail = { role: 'resource', owner: resourceOwner, worldCount }`.
306
+ * The `Number.isInteger` guard rejects a `NaN` / fractional / undefined-coerced
307
+ * index a JS caller could pass despite the compile-time requirement. When both
308
+ * indices are out of range the FIRST offender wins: `cameraOwner` is checked
309
+ * before `resourceOwner`, so `role === 'camera'` (D-3 / w3 contract).
310
+ *
311
+ * The empty-worlds guard short-circuits before either owner-range check.
312
+ */
313
+ export declare function validateDrawArgs(worldCount: number, owner: DrawOwnerSplit): Result<void, RhiError>;
314
+ export { err, ok, type Result, type ResultErr, type ResultOk, } from '@forgeax/engine-types';
315
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ import { err, ok } from '@forgeax/engine-types';
2
+ export { err, ok } from '@forgeax/engine-types';
3
+
4
+ // src/errors.ts
5
+ var RhiError = class extends Error {
6
+ code;
7
+ expected;
8
+ hint;
9
+ detail;
10
+ constructor(args) {
11
+ super(`[RhiError ${args.code}] expected: ${args.expected}; hint: ${args.hint}`);
12
+ this.name = "RhiError";
13
+ this.code = args.code;
14
+ this.expected = args.expected;
15
+ this.hint = args.hint;
16
+ this.detail = args.detail;
17
+ }
18
+ };
19
+ function validateDrawArgs(worldCount, owner) {
20
+ if (worldCount === 0) {
21
+ return err(
22
+ new RhiError({
23
+ code: "render-system-empty-worlds",
24
+ expected: "worlds array has at least one world",
25
+ hint: "pass at least one world: draw([world], { cameraOwner: 0, resourceOwner: 0 })"
26
+ })
27
+ );
28
+ }
29
+ const { cameraOwner, resourceOwner } = owner;
30
+ const outOfRange = (index) => !Number.isInteger(index) || index < 0 || index >= worldCount;
31
+ if (outOfRange(cameraOwner)) {
32
+ return err(
33
+ new RhiError({
34
+ code: "render-system-owner-out-of-range",
35
+ expected: "cameraOwner is an index into worlds (0 <= cameraOwner < worlds.length)",
36
+ hint: "cameraOwner must be in 0..worlds.length-1; the cameraOwner world supplies the surfaced cameras",
37
+ detail: { role: "camera", owner: cameraOwner, worldCount }
38
+ })
39
+ );
40
+ }
41
+ if (outOfRange(resourceOwner)) {
42
+ return err(
43
+ new RhiError({
44
+ code: "render-system-owner-out-of-range",
45
+ expected: "resourceOwner is an index into worlds (0 <= resourceOwner < worlds.length)",
46
+ hint: "resourceOwner must be in 0..worlds.length-1; the resourceOwner world supplies skylight/skybox/postProcess",
47
+ detail: { role: "resource", owner: resourceOwner, worldCount }
48
+ })
49
+ );
50
+ }
51
+ return ok(void 0);
52
+ }
53
+
54
+ export { RhiError, validateDrawArgs };
55
+ //# sourceMappingURL=errors.mjs.map
56
+ //# sourceMappingURL=errors.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts"],"names":[],"mappings":";;;;AAuYO,IAAM,QAAA,GAAN,cAAuB,KAAA,CAAM;AAAA,EACzB,IAAA;AAAA,EACA,QAAA;AAAA,EACA,IAAA;AAAA,EACA,MAAA;AAAA,EAET,YAAY,IAAA,EAKT;AACD,IAAA,KAAA,CAAM,CAAA,UAAA,EAAa,KAAK,IAAI,CAAA,YAAA,EAAe,KAAK,QAAQ,CAAA,QAAA,EAAW,IAAA,CAAK,IAAI,CAAA,CAAE,CAAA;AAC9E,IAAA,IAAA,CAAK,IAAA,GAAO,UAAA;AACZ,IAAA,IAAA,CAAK,OAAO,IAAA,CAAK,IAAA;AACjB,IAAA,IAAA,CAAK,WAAW,IAAA,CAAK,QAAA;AACrB,IAAA,IAAA,CAAK,OAAO,IAAA,CAAK,IAAA;AACjB,IAAA,IAAA,CAAK,SAAS,IAAA,CAAK,MAAA;AAAA,EACrB;AACF;AAyCO,SAAS,gBAAA,CACd,YACA,KAAA,EACwB;AACxB,EAAA,IAAI,eAAe,CAAA,EAAG;AACpB,IAAA,OAAO,GAAA;AAAA,MACL,IAAI,QAAA,CAAS;AAAA,QACX,IAAA,EAAM,4BAAA;AAAA,QACN,QAAA,EAAU,qCAAA;AAAA,QACV,IAAA,EAAM;AAAA,OACP;AAAA,KACH;AAAA,EACF;AACA,EAAA,MAAM,EAAE,WAAA,EAAa,aAAA,EAAc,GAAI,KAAA;AACvC,EAAA,MAAM,UAAA,GAAa,CAAC,KAAA,KAClB,CAAC,MAAA,CAAO,UAAU,KAAK,CAAA,IAAK,KAAA,GAAQ,CAAA,IAAK,KAAA,IAAS,UAAA;AAGpD,EAAA,IAAI,UAAA,CAAW,WAAW,CAAA,EAAG;AAC3B,IAAA,OAAO,GAAA;AAAA,MACL,IAAI,QAAA,CAAS;AAAA,QACX,IAAA,EAAM,kCAAA;AAAA,QACN,QAAA,EAAU,wEAAA;AAAA,QACV,IAAA,EAAM,gGAAA;AAAA,QACN,QAAQ,EAAE,IAAA,EAAM,QAAA,EAAU,KAAA,EAAO,aAAa,UAAA;AAAW,OAC1D;AAAA,KACH;AAAA,EACF;AACA,EAAA,IAAI,UAAA,CAAW,aAAa,CAAA,EAAG;AAC7B,IAAA,OAAO,GAAA;AAAA,MACL,IAAI,QAAA,CAAS;AAAA,QACX,IAAA,EAAM,kCAAA;AAAA,QACN,QAAA,EAAU,4EAAA;AAAA,QACV,IAAA,EAAM,2GAAA;AAAA,QACN,QAAQ,EAAE,IAAA,EAAM,UAAA,EAAY,KAAA,EAAO,eAAe,UAAA;AAAW,OAC9D;AAAA,KACH;AAAA,EACF;AACA,EAAA,OAAO,GAAG,MAAS,CAAA;AACrB","file":"errors.mjs","sourcesContent":["// @forgeax/engine-rhi/src/errors - RhiError + closed RhiErrorCode union + Result<T, E>.\n//\n// Shape:\n// - RhiErrorCode = closed union 23 members (charter P3: closed-union\n// exhaustive switch needs no default fallback; tsc strict mode guards\n// completeness). Extended from 6 to 10 in feat-20260508-rhi-surface-completion\n// w7 (D-S3): added 'command-encoder-finished' / 'render-pass-not-ended' /\n// 'queue-submit-failed' / 'queue-write-buffer-out-of-bounds'.\n// Extended from 10 to 14 in feat-20260509-ecs-render-bridge-mvp w6 (D-S7):\n// added 'render-system-no-camera' / 'render-system-multi-camera' /\n// 'render-system-multi-light' / 'asset-not-registered'.\n// Extended from 14 to 17 in feat-20260511-rhi-spec-realign-aggressive w6\n// (D-P4 + R-02 §2.1 W3C spec 22.2 subtypes): added 'device-lost' / 'oom' /\n// 'internal-error' so the onError fan-out can disambiguate spec error\n// subclasses without falling back to the bucket 'webgpu-runtime-error'.\n// Extended from 17 to 18 in feat-20260511-asset-system-v1 w4\n// (D-P2 + requirements §9 row 8 + AC-04 + AC-21): added\n// 'hierarchy-broken' for `propagateTransforms` stale ChildOf ref fail-fast\n// (ChildOf component references a destroyed entity); same\n// render-system / schedule semantic domain as\n// 'render-system-multi-camera' / 'render-system-no-camera'. Minor add-only\n// per AGENTS.md evolution contract (no reorder / rename / deprecate).\n// Extended from 18 to 19 in feat-20260612-rhi-destroy-renderer-dispose-gpu-\n// lifecycle M1 (D-6 + D-7 + AC-02 / AC-03): added 'destroy-after-destroy'\n// for second `destroyBuffer` / `destroyTexture` on the same handle. The\n// shim layer (rhi-webgpu + rhi-wgpu) tracks per-handle `destroyed: boolean`\n// in WeakMap-backed meta and fail-fasts the second call rather than\n// forwarding it to the underlying GPU (research F-1 wgpu wasm `destroy()`\n// is idempotent void; F-8 WebGPU spec is also idempotent void; D-7 prefers\n// fail-fast over silent idempotency because double-destroy is almost always\n// a lifecycle bug). Minor add-only per AGENTS.md evolution contract.\n// Extended from 19 to 20 in feat-20260619-wasm-fault-isolation M3 w7:\n// added 'rhi-descriptor-invalid' for `createRenderPipeline` (and other\n// create* entries) descriptor parse failures surfaced through the wgpu-wasm\n// backend (Rust `#[wasm_bindgen(catch)]` Err). The prefix-based\n// classification (D-1 / D-2) routes wasm exceptions with the stable marker\n// `[wgpu-wasm] failed to parse` to this code; exceptions without the prefix\n// remain in 'webgpu-runtime-error'. Semantics: descriptor parse failure =\n// caller bug (malformed descriptor data passed from TS), distinct from\n// 'webgpu-runtime-error' = runtime condition (valid descriptor rejected by\n// wgpu backend). Minor add-only per AGENTS.md evolution contract.\n// Extended from 20 to 21 in feat-20260622-chunk-gpu-instancing-sprite-\n// tilemap M2 w10 (D-2 + AC-05 + research N-1): added\n// 'instancing-exceeds-uniform-cap' for the WebGL2 uniform-fallback path\n// when a record-stage fold bucket carries more than 128 instances\n// (128 = MAX_UNIFORM_INSTANCES; 128 * 64B = 8192B comfortably fits the\n// WebGL2 minimum 16384B UBO size, leaving headroom for the per-frame\n// material UBO slice — research N-1 implements the locked value). The\n// record-stage dispatch site fires the error AND falls the offending\n// bucket back to per-entity drawIndexed (the same exit the mode-gate\n// bypass uses — plan-strategy D-9 \"shared fallback exit\"). Semantics:\n// distinct from 'limit-exceeded' (byte-cap against\n// maxStorageBufferBindingSize) — this code targets the per-bucket\n// instance-count cap, which is a backend-capability ceiling rather than\n// an allocation-size ceiling. AI users branch on .code first then read\n// detail.requested / .limit / .scope through property access (charter\n// P3 + plan-strategy 8.3 actionable hint). Minor add-only\n// per AGENTS.md evolution contract.\n// Extended from 21 to 23 in feat-20260708-composited-multi-world-rendering\n// M3 (D-5): added 'render-system-empty-worlds' + 'render-system-owner-out-of-\n// range' for draw(worlds, { cameraOwner, resourceOwner }) entry validation. The\n// owner-out-of-range path exposes .detail = RhiOwnerOutOfRangeDetail\n// ({ role, owner, worldCount } after feat-20260709-editor-world-partition\n// M1 / w7: role ∈ {'camera','resource'} names which of the two split draw\n// owners is out of range); empty-worlds carries no .detail. The pure\n// validateDrawArgs(worldCount, { cameraOwner, resourceOwner })\n// helper (World-free primitives) emits both and is consumed by the runtime\n// createRenderer draw entry (the codes' SSOT stays in rhi). Checks run\n// empty-worlds -> cameraOwner -> resourceOwner; the first out-of-range owner\n// wins (role='camera' when both offend). Add-only (no new code, 0 net Δ per\n// D-3) per AGENTS.md evolution contract.\n// - RhiError class has readonly .code / .expected / .hint three-field surface\n// (AGENTS.md \"Errors are structured\" / D-5); the 'shader-compile-failed' path\n// exposes .detail = RhiShaderCompileDetail (compilerMessages array);\n// the 'asset-not-registered' path exposes .detail = RhiAssetNotRegisteredDetail\n// ({ assetHandle: number }, D-S6); the 'webgpu-runtime-error' path optionally\n// exposes .detail = RhiWebgpuRuntimeDetail ({ error: RhiError | fallback }, D-S8) for\n// RenderSystem internal exception fan-out; the 'limit-exceeded' path\n// exposes .detail = LimitExceededDetail ({ maxStorageBufferBindingSize,\n// requestedBytes }, feat-20260513-instanced-mesh M5 reshape from legacy\n// { renderableCount, limit }); the other 15 paths leave\n// .detail = undefined per charter proposition 4 baseline.\n// - Result<T, E> = binary tag union ('ok' / 'err'), per AGENTS.md \"Errors are\n// structured\" convention.\n//\n// Related: requirements AC AC-10 + MVP-1.7 + AC-RSC-07 + hard-constraint 8 +\n// AI User Affordances; plan-strategy 2 S-6 (types/rhi single source) +\n// 7.3 error-info table; plan-decisions OQ-P2 (forward all 6 fields of\n// GPUCompilationMessage); D-S3 (4 command/queue members) + D-S6 / D-S7\n// / D-S8 (4 RenderSystem / AssetRegistry members + .detail structure).\n\n/// <reference types=\"@webgpu/types\" />\n\nimport { err, ok, type Result } from '@forgeax/engine-types';\n\n/**\n * Closed RhiErrorCode union. `switch` exhaustive checks need no default\n * fallback - tsc strict mode guards union completeness (charter proposition 4\n * + proposition 3: machine-readable union > prose).\n *\n *\n * The executable union below is the machine-readable source of truth for\n * the 23 members. The package README owns the AI-facing trigger and recovery\n * table; keeping that projection in one documentation owner avoids a second\n * member ledger drifting from the union.\n *\n * @example AI-user exhaustive switch on the 4 command/queue members (no default fallback)\n * ```ts\n * import type { RhiError, RhiErrorCode } from '@forgeax/engine-rhi';\n *\n * function recover(code: RhiErrorCode): string {\n * switch (code) {\n * // ... 6 baseline members elided ...\n * case 'command-encoder-finished': return 'recreate encoder via device.createCommandEncoder()';\n * case 'render-pass-not-ended': return 'call pass.end() before next beginRenderPass()';\n * case 'queue-submit-failed': return 'audit buffer/pipeline lifetimes before submit';\n * case 'queue-write-buffer-out-of-bounds': return 'realign offset and re-check buffer.size';\n * default: return 'baseline path';\n * }\n * }\n * ```\n */\nexport type RhiErrorCode =\n | 'adapter-unavailable'\n | 'feature-not-enabled'\n | 'limit-exceeded'\n | 'shader-compile-failed'\n | 'rhi-not-available'\n | 'webgpu-runtime-error'\n | 'command-encoder-finished'\n | 'render-pass-not-ended'\n | 'queue-submit-failed'\n | 'queue-write-buffer-out-of-bounds'\n | 'render-system-no-camera'\n | 'render-system-multi-camera'\n | 'render-system-multi-light'\n | 'asset-not-registered'\n | 'device-lost'\n | 'oom'\n | 'internal-error'\n | 'hierarchy-broken'\n | 'destroy-after-destroy'\n | 'rhi-descriptor-invalid'\n | 'instancing-exceeds-uniform-cap'\n | 'render-system-empty-worlds'\n | 'render-system-owner-out-of-range';\n\n/**\n * Detail structure exclusive to the `shader-compile-failed` path.\n *\n * `compilerMessages` directly forwards the 6 standardized fields of\n * `GPUCompilationMessage` from `@webgpu/types` v0.1.69 (`message` / `type` /\n * `lineNum` / `linePos` / `offset` / `length`); research F-3 finding;\n * plan-decisions OQ-P2 locks full-field forwarding.\n *\n * @see {@link GPUCompilationMessage}\n */\nexport interface RhiShaderCompileDetail {\n readonly compilerMessages: readonly GPUCompilationMessage[];\n}\n\n/**\n * Detail structure exclusive to the `asset-not-registered` path (D-S6).\n *\n * `assetHandle` carries the offending u32 handle the caller passed via\n * `MeshFilter.assetHandle`; AI users access it through property access\n * (`err.detail.assetHandle`) rather than parsing the message string\n * (charter proposition 4 + F-3 contract surface).\n */\nexport interface RhiAssetNotRegisteredDetail {\n readonly assetHandle: number;\n}\n\n/**\n * Detail structure exclusive to the `webgpu-runtime-error` path (D-S8).\n *\n * `error` carries the underlying exception object so AI users can inspect the\n * root cause (`.code` / `.expected` / `.hint` for `RhiError` paths, or\n * `.code` + `.message` for non-RhiError falls) without parsing the\n * RhiError.message field. Optional: the K-9 silent-skip fan-out root path\n * may emit `webgpu-runtime-error` without `.detail` when the underlying\n * exception is unavailable.\n *\n * feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M4 / T-M4-02:\n * `error` field type widened from `string` to `RhiError | { code: string;\n * message: string }` so downstream `switch (err.code)` handlers can narrow\n * the inner error (`.code` / `.expected` / `.hint`) without an `as` cast.\n */\nexport interface RhiWebgpuRuntimeDetail {\n readonly error: RhiError | { code: string; message: string; name?: string };\n}\n\n/**\n * Detail structure exclusive to the `limit-exceeded` path.\n *\n * `maxStorageBufferBindingSize` carries the device-reported storage cap\n * (`device.limits.maxStorageBufferBindingSize`); `requestedBytes`\n * carries the byte count the caller attempted to allocate. AI users\n * access these through typed property access (`err.detail.maxStorageBufferBindingSize`\n * / `err.detail.requestedBytes`) rather than parsing the message string\n * — charter proposition 4 structured-error consumption path; `err.hint`\n * is for human eyeballs only.\n *\n * Single live emit point: the RenderSystem record stage per-entity\n * instance buffer upload path\n * (`packages/runtime/src/render-system-record.ts`). The 18-member\n * `RhiErrorCode` union is unchanged (`'limit-exceeded'` discriminant\n * preserved); evolution major rename + replace of the discriminated\n * `detail` shape per AGENTS.md Change stance + plan-strategy D-3.\n *\n * Migration history:\n * - feat-20260513-instanced-mesh M5: detail reshape from\n * `{ renderableCount, limit }` to `{ maxStorageBufferBindingSize,\n * requestedBytes }`. Emit point at the time was\n * `AssetRegistry.createInstancedBuffer`.\n * - feat-20260514-ecs-children-instances-managed-buffer-array M3 / w15:\n * `AssetRegistry.createInstancedBuffer` deleted alongside the\n * `InstancedBufferAsset` POD; emit point migrated to the record\n * stage upload path (`requestedBytes` now equals\n * `Instances.transforms.byteLength` per Instances-bearing entity).\n */\nexport interface LimitExceededDetail {\n readonly maxStorageBufferBindingSize: number;\n readonly requestedBytes: number;\n}\n\n/**\n * Detail structure exclusive to the `'render-system-multi-light'` path\n * (feat-20260519-light-casters-point-spot-pbr M3 / w20 + plan-strategy\n * section 8 (3) (b)).\n *\n * Emitted by the RenderSystem record stage when first-slice cap exceedance\n * is detected: `type` discriminates the offending bucket\n * (`'directional'` for N>1 / `'point'` or `'spot'` for N>4); `got`\n * carries the observed entity count so AI users can branch via property\n * access (`err.detail.type === 'point' && err.detail.got > 4`) rather\n * than parsing the message string (charter proposition 4 + F-3 contract\n * surface).\n *\n * Single live emit point: the RenderSystem record stage three-bucket\n * fail-fast (`packages/runtime/src/render-system-record.ts`). Minor\n * additive evolution per AGENTS.md error model evolution contract.\n */\nexport interface RhiMultiLightDetail {\n readonly type: 'directional' | 'point' | 'spot';\n readonly got: number;\n}\n\n/**\n * Detail structure exclusive to the `'instancing-exceeds-uniform-cap'` path\n * (feat-20260622-chunk-gpu-instancing-sprite-tilemap M2 / w10 +\n * plan-strategy 2 D-2 + research N-1).\n *\n * Emitted by the record-stage fold dispatch loop\n * (`packages/runtime/src/render-system-record.ts`) when\n * `caps.storageBuffer === false` AND a fold bucket carries more than\n * `limit` instances. The engine fires the error AND falls the offending\n * bucket back to per-entity drawIndexed via the same exit the mode-gate\n * bypass uses (plan-strategy D-9 \"shared fallback exit\"), so the frame\n * is still visually correct (no identity-collapse / black screen) and\n * the cap event surfaces structurally for AI users to observe.\n *\n * Fields:\n * - `requested` — the offending bucket's instance count\n * (`FoldBucket.bucketSize`); always strictly greater than `limit` at\n * emit time (the cap-check helper guards `requested > limit`).\n * - `limit` — the literal 128. The value is locked at the type level\n * because the cap is structurally tied to the WebGL2 minimum 16384 B\n * UBO size (128 * 64 B mat4 stride = 8192 B leaves headroom for the\n * per-frame material UBO slice — research N-1). A future cap change\n * would be a major evolution, not a runtime knob.\n * - `scope` — closed `'sprite' | 'tilemap-chunk'` discriminator that\n * pinpoints the dispatch site (sprite-pass entry that came directly\n * from a user-spawned Sprite vs one derived by\n * `tilemap-chunk-extract-system`). AI users branch on `.scope` to\n * decide whether to shrink the sprite batch size or the tilemap\n * chunk size.\n *\n * AI-user consumption (charter P3 + plan-strategy 8.3):\n * ```ts\n * if (err.code === 'instancing-exceeds-uniform-cap') {\n * const d = err.detail as RhiInstancingExceedsUniformCapDetail;\n * if (d.scope === 'sprite') shrinkSpriteBatchSize(d.requested);\n * else if (d.scope === 'tilemap-chunk') shrinkTileChunkSize(d.requested);\n * }\n * ```\n * — never parse `err.message`. The discriminated `detail` field is the\n * surface; the human-readable `err.message` is for logs only.\n */\nexport interface RhiInstancingExceedsUniformCapDetail {\n readonly requested: number;\n readonly limit: 128;\n readonly scope: 'sprite' | 'tilemap-chunk';\n}\n\n/**\n * Detail structure exclusive to the `'render-system-owner-out-of-range'` path\n * (feat-20260708-composited-multi-world-rendering M3 / D-5).\n *\n * Emitted by `renderer.draw(worlds, { cameraOwner, resourceOwner })` when one\n * owner index is not valid for `worlds` (`owner < 0` or `owner >= worlds.length`).\n * An out-of-range index cannot resolve, so the frame is skipped before extract.\n *\n * Fields:\n * - `role` — WHICH of the two draw-owner indices was out of range\n * (feat-20260709-editor-world-partition M1 / w7). `draw(worlds, {\n * cameraOwner, resourceOwner })` carries two independent indices; `role`\n * tells the AI user whether the camera-source index (`'camera'`) or the\n * singleton-resource index (`'resource'`) is the offender, so the fix is\n * unambiguous from the text channel (no new error code — D-3 keeps 0 net\n * new codes; the discriminator lives in `.detail`). When both indices are\n * out of range the first offender is reported: `cameraOwner` is validated\n * before `resourceOwner`, so `role === 'camera'`.\n * - `owner` — the offending index the caller passed (the `role` index's\n * value).\n * - `worldCount` — `worlds.length` at call time (the valid range is\n * `0 .. worldCount - 1`).\n *\n * AI users branch via property access (`err.detail.role` / `err.detail.owner` /\n * `err.detail.worldCount`) after narrowing on `.code`, rather than parsing the\n * message string (charter P3 structured-failure surface).\n *\n * The sibling `'render-system-empty-worlds'` path carries no `.detail` — an\n * empty array is fully described by `.code`, and the entry check short-circuits\n * to that code before the owner-range check runs (the two codes are\n * non-exclusive).\n */\nexport interface RhiOwnerOutOfRangeDetail {\n readonly role: 'camera' | 'resource';\n readonly owner: number;\n readonly worldCount: number;\n}\n\n/**\n * Tagged union of `.detail` shapes carried by structured errors.\n *\n * Entries:\n * - `RhiShaderCompileDetail` (carries `compilerMessages`) - emitted on the\n * `'shader-compile-failed'` path.\n * - `RhiAssetNotRegisteredDetail` (carries `assetHandle`) - emitted on the\n * `'asset-not-registered'` path (D-S6).\n * - `RhiWebgpuRuntimeDetail` (carries `error: RhiError | { code, message }`) - optionally emitted\n * on the `'webgpu-runtime-error'` path when a captured `Error.message` is\n * available (D-S8).\n * - `LimitExceededDetail` (carries `maxStorageBufferBindingSize` +\n * `requestedBytes`) - emitted on the `'limit-exceeded'` path when\n * the RenderSystem record stage's per-entity Instances upload\n * exceeds `device.limits.maxStorageBufferBindingSize`\n * (feat-20260514-ecs-children-instances-managed-buffer-array M3 / w15;\n * emit point migrated from the deleted\n * `AssetRegistry.createInstancedBuffer` factory).\n *\n * The other 15 paths leave `.detail = undefined` (charter proposition 4\n * baseline).\n */\nexport type RhiErrorDetail =\n | RhiShaderCompileDetail\n | RhiAssetNotRegisteredDetail\n | RhiWebgpuRuntimeDetail\n | LimitExceededDetail\n | RhiMultiLightDetail\n | RhiInstancingExceedsUniformCapDetail\n | RhiOwnerOutOfRangeDetail;\n\n/**\n * Structured RHI error.\n *\n * Three readonly fields aligned with AGENTS.md \"Errors are structured\":\n * - `.code` - closed union member (L1 key signal).\n * - `.expected` - expected-state description (L2 detail).\n * - `.hint` - actionable recovery guidance (L2 detail; charter proposition 3:\n * machine-readable hint > prose).\n *\n * `.detail` is populated on four paths:\n * - `code === 'shader-compile-failed'` -> `RhiShaderCompileDetail`\n * - `code === 'asset-not-registered'` -> `RhiAssetNotRegisteredDetail`\n * - `code === 'webgpu-runtime-error'` -> `RhiWebgpuRuntimeDetail` (optional)\n * - `code === 'limit-exceeded'` -> `LimitExceededDetail`\n * (feat-20260513-instanced-mesh M5 reshape; carries\n * `maxStorageBufferBindingSize` + `requestedBytes`)\n *\n * The other 15 paths leave `.detail = undefined` (charter proposition 4\n * baseline).\n *\n * Note: `RhiErrorDetail` is currently a flat tagged union without a\n * `code` discriminant field on each variant; AI users perform typed\n * narrowing via outer `switch (err.code)` then a one-time `as` cast on\n * `err.detail` per the documented variant. Full discriminated-union\n * refactor (each variant carrying its own `code` literal field) is left\n * to `feat-future-rhi-error-detail-discriminant` spinoff.\n */\nexport class RhiError extends Error {\n readonly code: RhiErrorCode;\n readonly expected: string;\n readonly hint: string;\n readonly detail: RhiErrorDetail | undefined;\n\n constructor(args: {\n code: RhiErrorCode;\n expected: string;\n hint: string;\n detail?: RhiErrorDetail | undefined;\n }) {\n super(`[RhiError ${args.code}] expected: ${args.expected}; hint: ${args.hint}`);\n this.name = 'RhiError';\n this.code = args.code;\n this.expected = args.expected;\n this.hint = args.hint;\n this.detail = args.detail;\n }\n}\n\n/**\n * The two split draw-owner indices carried by\n * `draw(worlds, { cameraOwner, resourceOwner })`\n * (feat-20260709-editor-world-partition M1 / w6). `cameraOwner` selects the\n * world whose cameras are surfaced; `resourceOwner` selects the world whose\n * skylight / skybox / postProcessParams are surfaced. Declared here (World-free\n * primitives) so the validator and the `RhiOwnerOutOfRangeDetail.role`\n * discriminator live in one SSOT package (architecture-principles §1).\n */\nexport interface DrawOwnerSplit {\n readonly cameraOwner: number;\n readonly resourceOwner: number;\n}\n\n/**\n * Validate `renderer.draw(worlds, { cameraOwner, resourceOwner })` arguments at\n * the draw entry (feat-20260708 M3 / D-5, extended by\n * feat-20260709-editor-world-partition M1 / w6-w7).\n *\n * The validator takes primitives (`worldCount = worlds.length`, plus the owner\n * index/indices) — no `World`, no math — so it lives in `@forgeax/engine-rhi`\n * alongside the `RhiErrorCode` members it emits (architecture-principles §1\n * SSOT). The runtime `createRenderer` draw entry calls it before any extract; a\n * non-`ok` result skips the frame with a structured error (charter P3), never a\n * silent no-op.\n *\n * Checks, in order (D-5 + w6):\n * 1. `worldCount === 0` -> `'render-system-empty-worlds'` (no `.detail`).\n * 2. `cameraOwner` is not a valid index -> `'render-system-owner-out-of-range'`\n * with `.detail = { role: 'camera', owner: cameraOwner, worldCount }`.\n * 3. `resourceOwner` is not a valid index -> `'render-system-owner-out-of-range'`\n * with `.detail = { role: 'resource', owner: resourceOwner, worldCount }`.\n * The `Number.isInteger` guard rejects a `NaN` / fractional / undefined-coerced\n * index a JS caller could pass despite the compile-time requirement. When both\n * indices are out of range the FIRST offender wins: `cameraOwner` is checked\n * before `resourceOwner`, so `role === 'camera'` (D-3 / w3 contract).\n *\n * The empty-worlds guard short-circuits before either owner-range check.\n */\nexport function validateDrawArgs(\n worldCount: number,\n owner: DrawOwnerSplit,\n): Result<void, RhiError> {\n if (worldCount === 0) {\n return err(\n new RhiError({\n code: 'render-system-empty-worlds',\n expected: 'worlds array has at least one world',\n hint: 'pass at least one world: draw([world], { cameraOwner: 0, resourceOwner: 0 })',\n }),\n );\n }\n const { cameraOwner, resourceOwner } = owner;\n const outOfRange = (index: number): boolean =>\n !Number.isInteger(index) || index < 0 || index >= worldCount;\n // cameraOwner is validated first: it is the first offender when both indices\n // are out of range (w3 contract). role names which index the AI user fixes.\n if (outOfRange(cameraOwner)) {\n return err(\n new RhiError({\n code: 'render-system-owner-out-of-range',\n expected: 'cameraOwner is an index into worlds (0 <= cameraOwner < worlds.length)',\n hint: 'cameraOwner must be in 0..worlds.length-1; the cameraOwner world supplies the surfaced cameras',\n detail: { role: 'camera', owner: cameraOwner, worldCount },\n }),\n );\n }\n if (outOfRange(resourceOwner)) {\n return err(\n new RhiError({\n code: 'render-system-owner-out-of-range',\n expected: 'resourceOwner is an index into worlds (0 <= resourceOwner < worlds.length)',\n hint: 'resourceOwner must be in 0..worlds.length-1; the resourceOwner world supplies skylight/skybox/postProcess',\n detail: { role: 'resource', owner: resourceOwner, worldCount },\n }),\n );\n }\n return ok(undefined);\n}\n\n// Result<T, E> + ok / err live in `@forgeax/engine-types` (tweak-20260612-result-\n// into-types). They were duplicated here (\"byte-for-byte aligned\" by prose) and\n// in packages/ecs/src/result.ts; SSOT consolidated upstream. The barrel here\n// re-exports them so existing `import { err, ok, Result, ResultOk, ResultErr }\n// from '@forgeax/engine-rhi'` consumers stay unchanged.\nexport {\n err,\n ok,\n type Result,\n type ResultErr,\n type ResultOk,\n} from '@forgeax/engine-types';\n"]}