@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.
Files changed (83) 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__/queue-writes.unit.test.d.ts +2 -0
  43. package/dist/__tests__/queue-writes.unit.test.d.ts.map +1 -0
  44. package/dist/__tests__/render-pass-encoder-spec-alignment.test-d.d.ts +2 -0
  45. package/dist/__tests__/render-pass-encoder-spec-alignment.test-d.d.ts.map +1 -0
  46. package/dist/__tests__/requestAdapter.test-d.d.ts +2 -0
  47. package/dist/__tests__/requestAdapter.test-d.d.ts.map +1 -0
  48. package/dist/errors.d.ts +315 -0
  49. package/dist/errors.d.ts.map +1 -0
  50. package/dist/errors.mjs +56 -0
  51. package/dist/errors.mjs.map +1 -0
  52. package/dist/index.d.ts +1749 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.mjs +256 -0
  55. package/dist/index.mjs.map +1 -0
  56. package/dist/queue-writes.d.ts +20 -0
  57. package/dist/queue-writes.d.ts.map +1 -0
  58. package/package.json +61 -0
  59. package/src/__tests__/async-form.test-d.ts +40 -0
  60. package/src/__tests__/binding-resource.test-d.ts +66 -0
  61. package/src/__tests__/buffer-mapping.test-d.ts +84 -0
  62. package/src/__tests__/canvasContext.test-d.ts +106 -0
  63. package/src/__tests__/caps-three-way.unit.test.ts +144 -0
  64. package/src/__tests__/caps.test-d.ts +83 -0
  65. package/src/__tests__/command-encoder-spec-alignment.test-d.ts +117 -0
  66. package/src/__tests__/createComputePipeline.test-d.ts +149 -0
  67. package/src/__tests__/createQuerySet.test-d.ts +81 -0
  68. package/src/__tests__/createTextureView.test-d.ts +127 -0
  69. package/src/__tests__/descriptor-mirror.test-d.ts +267 -0
  70. package/src/__tests__/draw-validation-errors.test.ts +155 -0
  71. package/src/__tests__/errors-instancing.unit.test.ts +167 -0
  72. package/src/__tests__/errors.test-d.ts +199 -0
  73. package/src/__tests__/limit-exceeded-detail.test-d.ts +33 -0
  74. package/src/__tests__/mapped-buffer.test-d.ts +65 -0
  75. package/src/__tests__/opaque-handle.test-d.ts +121 -0
  76. package/src/__tests__/owner-out-of-range-role.test.ts +84 -0
  77. package/src/__tests__/queue-writes.test-d.ts +107 -0
  78. package/src/__tests__/queue-writes.unit.test.ts +30 -0
  79. package/src/__tests__/render-pass-encoder-spec-alignment.test-d.ts +202 -0
  80. package/src/__tests__/requestAdapter.test-d.ts +123 -0
  81. package/src/errors.ts +504 -0
  82. package/src/index.ts +2398 -0
  83. package/src/queue-writes.ts +62 -0
package/src/index.ts ADDED
@@ -0,0 +1,2398 @@
1
+ // @forgeax/engine-rhi - pure-interface RHI surface for forgeax-engine.
2
+ //
3
+ // Iron laws: spec-aligned (descriptor field names mirror Pick<GPUXxxDescriptor, ...>
4
+ // byte-for-byte) / capability-gated / opaque handle / math-free. See README +
5
+ // AGENTS.md "## RHI / WebGPU" section.
6
+ //
7
+ // This file is the complete entry for `import * as RHI from '@forgeax/engine-rhi'`
8
+ // (charter proposition 1: progressive disclosure + plan-strategy 7.4
9
+ // discoverability "AI users see the full surface in one read").
10
+ //
11
+ // =====================================================================
12
+ // Async function form contract (D-P9 / requirements AC-12)
13
+ // =====================================================================
14
+ //
15
+ // All async functions exposed by `@forgeax/engine-rhi` (and the rhi-webgpu /
16
+ // rhi-wgpu shim packages that implement this interface) follow the
17
+ // `Promise<Result<T, RhiError>>` shape — Promise NEVER rejects, failures
18
+ // ride `Result.err` so AI users have a single error-handling idiom across
19
+ // sync + async surfaces (charter proposition 4 explicit failure +
20
+ // proposition 5 consistent abstraction).
21
+ //
22
+ // Three permitted whitelist categories may keep the bare `Promise<T>` shape;
23
+ // each exemption must be marked with a `// forgeax-async-whitelist:
24
+ // <category>` comment near the signature site so the grep gate
25
+ // (scripts/check-async-form.mjs) can pair the violation count with the
26
+ // whitelist count:
27
+ //
28
+ // (a) wasm-bindgen — outputs of wasm-pack JS shim that we cannot
29
+ // restructure at the type layer (wasm-loader edge);
30
+ // (b) dom-native — DOM native Promise passthrough such as
31
+ // `GPUDevice.lost` re-exposed verbatim;
32
+ // (c) render-loop — non-fallible internal drivers like
33
+ // requestAnimationFrame wrappers.
34
+ //
35
+ // Anything else returning `Promise<T>` without the whitelist comment is a
36
+ // CI gate red.
37
+ //
38
+ // Related: requirements AC AC-02 + MVP-1.1 / MVP-1.2 / MVP-1.3 / MVP-1.5 /
39
+ // MVP-1.7 + AC-04 (RhiErrorCode 17 members) + AC-05 (RhiCaps 11
40
+ // fields) + AC-10 (RhiBindingResource tagged union) + AC-11
41
+ // (MappedBuffer brand) + AC-12 (Promise<Result<T,E>> + whitelist)
42
+ // + hard-constraints 1 / 2 / 3 / 7 / 9; plan-strategy 1
43
+ // architecture + 2 S-1 / S-3 / S-6 / S-7 + 6 M1 + D-P9 async form;
44
+ // research F-1 (17 core descriptor surface) + F-7 (wgpu-hal 21
45
+ // associated types);
46
+ // feat-20260508-rhi-surface-completion w8 D-S5 (4 new descriptors
47
+ // with `view` field tightened to TextureView);
48
+ // feat-20260511-rhi-spec-realign-aggressive (Promise<Result<T,E>>
49
+ // contract locked at the interface layer).
50
+
51
+ /// <reference types="@webgpu/types" />
52
+
53
+ import type {
54
+ AddressMode,
55
+ CompareFunction,
56
+ FilterMode,
57
+ TextureFormat,
58
+ } from '@forgeax/engine-types';
59
+ import { err, ok, type Result, RhiError } from './errors';
60
+
61
+ // ============================================================================
62
+ // 14 opaque handles (MVP-1.3)
63
+ // ============================================================================
64
+ //
65
+ // Shape: each handle = brand-only typed Id<T>; no runtime value; attempting to
66
+ // access internal GPU fields is a tsc compile-time red signal (research R5;
67
+ // charter proposition 4 explicit failure + proposition 5 consistent abstraction).
68
+ //
69
+ // Naming: no Rhi prefix (D-9); coexists with @webgpu/types.GPU* prefix +
70
+ // wgpu::* Rust paths via module-path semantics (e.g.
71
+ // `import { Buffer } from '@forgeax/engine-rhi'` vs `GPUBuffer`).
72
+
73
+ declare const RhiBufferBrand: unique symbol;
74
+ declare const RhiMappedBufferBrand: unique symbol;
75
+
76
+ /**
77
+ * GPU buffer opaque handle (vertex / index / uniform / storage / indirect).
78
+ *
79
+ * Spec anchor: W3C WebGPU §4 Buffers / [@webgpu/types.GPUBuffer]; research
80
+ * §4.1 mapState 3-state enum + §4.2 mapAsync 8-item validation + §4.4 unmap
81
+ * detach semantics.
82
+ *
83
+ * The buffer mapping surface is added in feat-20260510-rhi-resource-creation
84
+ * M5 (K-1: raw GPUMapMode bitmask; K-2: alignment / mode-usage / detach
85
+ * faults all ride 'webgpu-runtime-error' with structured .expected / .hint);
86
+ * re-shaped in feat-20260511-rhi-spec-realign-aggressive M1 (D-P2 #6):
87
+ * - `mapAsync` resolves to `Result<MappedBuffer, RhiError>` (success branch
88
+ * carries a branded handle subsequently used for getMappedRange / unmap).
89
+ * - `getMappedRange` / `unmap` are methods on `MappedBuffer` (not Buffer);
90
+ * calling them on a plain Buffer is a TS2345 compile-time red.
91
+ *
92
+ * The forgeax form keeps the spec verb names but routes failures via Result
93
+ * (charter proposition 4 explicit failure):
94
+ * mapAsync(mode, offset?, size?): Promise<Result<MappedBuffer, RhiError>>
95
+ * readonly mapState: 'unmapped' | 'pending' | 'mapped' - getter (research
96
+ * §4.1; same closed union as GPUBufferMapState).
97
+ */
98
+ export interface Buffer {
99
+ readonly [RhiBufferBrand]: void;
100
+ /**
101
+ * Map the buffer for CPU access.
102
+ *
103
+ * Spec anchor: W3C WebGPU §gpubuffer-mapasync /
104
+ * [@webgpu/types.GPUBuffer.mapAsync]. K-1 decision: `mode` is the raw
105
+ * `GPUMapMode` bitmask (NOT a closed union 'read' | 'write') so the forgeax
106
+ * form mirrors `GPUMapMode.READ` / `GPUMapMode.WRITE` literals.
107
+ *
108
+ * D-P2 #6 (feat-20260511-rhi-spec-realign-aggressive): the success branch
109
+ * resolves to `MappedBuffer`, a brand on top of `Buffer`. AI users
110
+ * subsequently call `mapped.getMappedRange(...)` / `mapped.unmap()` on the
111
+ * branded handle; calling those methods on a plain `Buffer` is a TS2345
112
+ * compile-time signal (charter proposition 4 explicit failure encoded at
113
+ * the type layer).
114
+ *
115
+ * Failure paths (research §4.2 + plan-strategy §2 K-2):
116
+ * - mapState !== 'unmapped' (F-8 row 1) -> 'webgpu-runtime-error'.
117
+ * - offset % 8 != 0 (step 4) -> 'webgpu-runtime-error'.
118
+ * - rangeSize % 4 != 0 (step 5) -> 'webgpu-runtime-error'.
119
+ * - offset + rangeSize > size (step 6) -> 'webgpu-runtime-error'.
120
+ * - mode contains foreign bits (step 7) -> 'webgpu-runtime-error'.
121
+ * - mode is not exactly READ or WRITE (step 8) -> 'webgpu-runtime-error'.
122
+ * - mode-usage mismatch (step 9 / F-8 row 3) -> 'webgpu-runtime-error'.
123
+ *
124
+ * @example
125
+ * const r = await buffer.mapAsync(GPUMapMode.WRITE);
126
+ * if (!r.ok) {
127
+ * // route via switch (r.error.code)
128
+ * return;
129
+ * }
130
+ * const mapped: MappedBuffer = r.value;
131
+ * const range = mapped.getMappedRange();
132
+ * if (range.ok) new Uint32Array(range.value).set([1, 2, 3, 4]);
133
+ * mapped.unmap();
134
+ */
135
+ mapAsync(
136
+ mode: GPUMapModeFlags,
137
+ offset?: number | undefined,
138
+ size?: number | undefined,
139
+ ): Promise<Result<MappedBuffer, RhiError>>;
140
+ /**
141
+ * Current mapping state (read-only getter).
142
+ *
143
+ * Spec anchor: research §4.1 mapState 3-state enum; mirrors
144
+ * GPUBufferMapState. Transitions:
145
+ * - createBuffer({mappedAtCreation:true}) sets mapState='mapped'.
146
+ * - mapAsync moves 'unmapped' -> 'pending' -> 'mapped'.
147
+ * - unmap moves 'mapped' -> 'unmapped'.
148
+ */
149
+ readonly mapState: 'unmapped' | 'pending' | 'mapped';
150
+ }
151
+
152
+ /**
153
+ * Brand on top of `Buffer` indicating the mapping is currently open; only the
154
+ * `MappedBuffer` exposes `getMappedRange` / `unmap` method forms so AI users
155
+ * cannot accidentally call them on an unmapped `Buffer` (D-P2 #6).
156
+ *
157
+ * The brand is structural — runtime the `MappedBuffer` is the same JS object
158
+ * as the underlying `Buffer`; TypeScript narrows access through the
159
+ * `__mapped: void` private brand symbol.
160
+ *
161
+ * Spec anchor: W3C WebGPU §4 Buffers mapping lifecycle (research §4.1 /
162
+ * §4.4); plan-strategy §7.1 + D-P2 break-point #6 (brand + method form
163
+ * merged).
164
+ *
165
+ * @example
166
+ * const r = await buffer.mapAsync(GPUMapMode.WRITE);
167
+ * if (!r.ok) return;
168
+ * const mapped: MappedBuffer = r.value;
169
+ * mapped.getMappedRange(); // method form, this: MappedBuffer
170
+ * mapped.unmap(); // method form, this: MappedBuffer
171
+ */
172
+ export interface MappedBuffer extends Buffer {
173
+ readonly [RhiMappedBufferBrand]: void;
174
+ /**
175
+ * Return an ArrayBuffer view of the mapped range. Method form on
176
+ * `MappedBuffer` per D-P2 #6 — calling on a plain `Buffer` is TS2339.
177
+ *
178
+ * Spec anchor: W3C WebGPU §gpubuffer-getmappedrange /
179
+ * [@webgpu/types.GPUBuffer.getMappedRange].
180
+ *
181
+ * Failure paths:
182
+ * - mapState !== 'mapped' (incl after unmap, F-8 row 2 detach guard) ->
183
+ * 'webgpu-runtime-error'.
184
+ */
185
+ getMappedRange(
186
+ offset?: number | undefined,
187
+ size?: number | undefined,
188
+ ): Result<ArrayBuffer, RhiError>;
189
+ /**
190
+ * Unmap the buffer, detaching all ArrayBuffer views obtained from
191
+ * getMappedRange. Method form on `MappedBuffer` per D-P2 #6.
192
+ *
193
+ * Spec anchor: W3C WebGPU §gpubuffer-unmap /
194
+ * [@webgpu/types.GPUBuffer.unmap]. unmap() returns void per spec normative
195
+ * silent no-op (research §4.4); calling unmap on an already-unmapped buffer
196
+ * does NOT error. This is the ONE Result-shape exception in the buffer
197
+ * surface (AI User Affordances explicit listing).
198
+ *
199
+ * After unmap, the JS object continues to exist but the brand narrows
200
+ * away at the TS layer: AI users who hold a `MappedBuffer` after the
201
+ * underlying state flipped should re-`mapAsync` to obtain a fresh branded
202
+ * instance (OQ-5 plan-decisions: unmap returns void; subsequent mapAsync
203
+ * returns a new MappedBuffer brand).
204
+ */
205
+ unmap(): void;
206
+ }
207
+
208
+ declare const RhiTextureBrand: unique symbol;
209
+ /** GPU texture opaque handle (2D / 3D / cube / array). */
210
+ export interface Texture {
211
+ readonly [RhiTextureBrand]: void;
212
+ }
213
+
214
+ declare const RhiTextureViewBrand: unique symbol;
215
+ /** GPU texture view opaque handle. */
216
+ export interface TextureView {
217
+ readonly [RhiTextureViewBrand]: void;
218
+ }
219
+
220
+ declare const RhiSamplerBrand: unique symbol;
221
+ /** GPU sampler opaque handle. */
222
+ export interface Sampler {
223
+ readonly [RhiSamplerBrand]: void;
224
+ }
225
+
226
+ declare const RhiBindGroupBrand: unique symbol;
227
+ /** GPU bind group opaque handle (instantiated layout). */
228
+ export interface BindGroup {
229
+ readonly [RhiBindGroupBrand]: void;
230
+ }
231
+
232
+ declare const RhiBindGroupLayoutBrand: unique symbol;
233
+ /** GPU bind group layout opaque handle (declares binding shapes). */
234
+ export interface BindGroupLayout {
235
+ readonly [RhiBindGroupLayoutBrand]: void;
236
+ }
237
+
238
+ declare const RhiPipelineLayoutBrand: unique symbol;
239
+ /** GPU pipeline layout opaque handle (aggregates BindGroupLayouts). */
240
+ export interface PipelineLayout {
241
+ readonly [RhiPipelineLayoutBrand]: void;
242
+ }
243
+
244
+ declare const RhiRenderPipelineBrand: unique symbol;
245
+ /** GPU render pipeline opaque handle. */
246
+ export interface RenderPipeline {
247
+ readonly [RhiRenderPipelineBrand]: void;
248
+ }
249
+
250
+ declare const RhiComputePipelineBrand: unique symbol;
251
+ /** GPU compute pipeline opaque handle. */
252
+ export interface ComputePipeline {
253
+ readonly [RhiComputePipelineBrand]: void;
254
+ }
255
+
256
+ declare const RhiShaderModuleBrand: unique symbol;
257
+ /** GPU shader module opaque handle (WGSL / SPIR-V compile artifact). */
258
+ export interface ShaderModule {
259
+ readonly [RhiShaderModuleBrand]: void;
260
+ }
261
+
262
+ declare const RhiQuerySetBrand: unique symbol;
263
+ /** GPU query set opaque handle (occlusion / timestamp). */
264
+ export interface QuerySet {
265
+ readonly [RhiQuerySetBrand]: void;
266
+ }
267
+
268
+ declare const RhiFenceBrand: unique symbol;
269
+ /** GPU fence opaque handle (GPU/CPU sync barrier). */
270
+ export interface Fence {
271
+ readonly [RhiFenceBrand]: void;
272
+ }
273
+
274
+ declare const RhiCommandEncoderBrand: unique symbol;
275
+ /** GPU command encoder opaque handle (single-use). */
276
+ export interface CommandEncoder {
277
+ readonly [RhiCommandEncoderBrand]: void;
278
+ }
279
+
280
+ declare const RhiCommandBufferBrand: unique symbol;
281
+ /** GPU command buffer opaque handle (submitted to Queue). */
282
+ export interface CommandBuffer {
283
+ readonly [RhiCommandBufferBrand]: void;
284
+ }
285
+
286
+ // ============================================================================
287
+ // 5 core descriptors (MVP-1.1) - Pick<GPUXxxDescriptor, ...> field names
288
+ // align byte-for-byte with @webgpu/types
289
+ // ============================================================================
290
+ //
291
+ // Decision S-7 + research F-3: optional fields are uniformly `?: T | undefined`
292
+ // (compatible with exactOptionalPropertyTypes; explicitly accepts writers
293
+ // passing `{ x: undefined }`; charter proposition 4 explicit failure /
294
+ // distinguish missing vs explicit-undefined).
295
+ // R8 mitigation: Compatibility Mode field (textureBindingViewDimension) is
296
+ // optional follow-on.
297
+ //
298
+ // Note: @webgpu/types v0.1.69 spec uses the `?: T` simplified form; forgeax
299
+ // applies the ExplicitUndefined mapped type to `Pick<spec>` so the forgeax
300
+ // side accepts `{ label: undefined }` writes (research F-3 finding;
301
+ // ecosystem upgrade path = once upstream @webgpu/types v0.2.x ships
302
+ // `?: T | undefined` uniformly, ExplicitUndefined can be removed).
303
+
304
+ /**
305
+ * Convert `?: T` optional fields to `?: T | undefined` (decision S-7).
306
+ *
307
+ * Compatible with exactOptionalPropertyTypes: writers may pass `undefined`
308
+ * explicitly or omit the field; the M2 shim distinguishes the two via
309
+ * `'x' in src` guards (research F-3 anti-pattern 2).
310
+ */
311
+ type ExplicitUndefined<T> = { [K in keyof T]: T[K] | undefined };
312
+
313
+ /** GPU buffer descriptor. Field set strictly matches GPUBufferDescriptor;
314
+ * optional fields use `?: T | undefined`. */
315
+ export type BufferDescriptor = ExplicitUndefined<
316
+ Pick<GPUBufferDescriptor, 'label' | 'size' | 'usage' | 'mappedAtCreation'>
317
+ >;
318
+
319
+ /** GPU texture descriptor. Field set strictly matches GPUTextureDescriptor
320
+ * (incl R8 Compatibility Mode field). */
321
+ export type TextureDescriptor = ExplicitUndefined<
322
+ Pick<
323
+ GPUTextureDescriptor,
324
+ | 'label'
325
+ | 'size'
326
+ | 'mipLevelCount'
327
+ | 'sampleCount'
328
+ | 'dimension'
329
+ | 'format'
330
+ | 'usage'
331
+ | 'viewFormats'
332
+ | 'textureBindingViewDimension'
333
+ >
334
+ >;
335
+
336
+ /** GPU sampler descriptor. Field set strictly matches GPUSamplerDescriptor. */
337
+ export type SamplerDescriptor = ExplicitUndefined<
338
+ Pick<
339
+ GPUSamplerDescriptor,
340
+ | 'label'
341
+ | 'addressModeU'
342
+ | 'addressModeV'
343
+ | 'addressModeW'
344
+ | 'magFilter'
345
+ | 'minFilter'
346
+ | 'mipmapFilter'
347
+ | 'lodMinClamp'
348
+ | 'lodMaxClamp'
349
+ | 'compare'
350
+ | 'maxAnisotropy'
351
+ >
352
+ >;
353
+
354
+ /** GPU bind group layout descriptor. Field set strictly matches
355
+ * GPUBindGroupLayoutDescriptor. */
356
+ export type BindGroupLayoutDescriptor = ExplicitUndefined<
357
+ Pick<GPUBindGroupLayoutDescriptor, 'label' | 'entries'>
358
+ >;
359
+
360
+ /**
361
+ * GPU texture view descriptor (Pick<GPUTextureViewDescriptor, 9 fields>).
362
+ *
363
+ * Spec anchor: W3C WebGPU §texture-view-creation /
364
+ * [@webgpu/types.GPUTextureViewDescriptor]. Field NAMES align byte-for-byte;
365
+ * field set excludes the feature-gated `swizzle` field (research §1.1 OOS-MVP;
366
+ * a future closure can add it once `'texture-component-swizzle'` is enabled).
367
+ *
368
+ * Cross-resource validation (shim fast-path, research §1.1):
369
+ * - `format` must equal source.format OR be in source.viewFormats; otherwise
370
+ * the shim returns Result.err({ code: 'webgpu-runtime-error' }).
371
+ * - `usage` must be a subset of source.usage (bitmask); otherwise the shim
372
+ * returns the same code.
373
+ *
374
+ * @example
375
+ * const r = device.createTextureView(tex, { format: 'rgba8unorm', dimension: '2d' });
376
+ * if (!r.ok) {
377
+ * // route via switch (r.error.code)
378
+ * }
379
+ */
380
+ export type TextureViewDescriptor = ExplicitUndefined<
381
+ Pick<
382
+ GPUTextureViewDescriptor,
383
+ | 'label'
384
+ | 'format'
385
+ | 'dimension'
386
+ | 'usage'
387
+ | 'aspect'
388
+ | 'baseMipLevel'
389
+ | 'mipLevelCount'
390
+ | 'baseArrayLayer'
391
+ | 'arrayLayerCount'
392
+ >
393
+ >;
394
+
395
+ /**
396
+ * GPU compute pipeline descriptor (Pick<GPUComputePipelineDescriptor,
397
+ * 'label' | 'layout' | 'compute'>).
398
+ *
399
+ * Spec anchor: W3C WebGPU §compute-pipeline-creation /
400
+ * [@webgpu/types.GPUComputePipelineDescriptor]. Field NAMES align byte-for-byte
401
+ * with spec.
402
+ *
403
+ * `layout` is the spec union `'auto' | GPUPipelineLayout`; forgeax tightens
404
+ * the explicit form to the `PipelineLayout` opaque handle (D-S5 pattern):
405
+ * layout: 'auto' | PipelineLayout
406
+ *
407
+ * `compute` mirrors `GPUProgrammableStage` verbatim — `module` (required
408
+ * `ShaderModule` opaque handle), `entryPoint?` (optional string),
409
+ * `constants?` (optional `Record<string, number>`).
410
+ *
411
+ * Capability gate (research §1.2 NOTE; plan-strategy §4.3 boundary case row 1):
412
+ * - `caps.compute === false` -> shim returns Result.err({
413
+ * code: 'feature-not-enabled', expected: 'caps.compute === true',
414
+ * hint: 'check device.caps.compute before calling createComputePipeline'
415
+ * }). MVP WebGPU path always has caps.compute=true (spec mandate); the gate
416
+ * exists for potential future non-WebGPU backends.
417
+ *
418
+ * @example
419
+ * const r = device.createComputePipeline({
420
+ * label: 'cs',
421
+ * layout: 'auto',
422
+ * compute: { module: csModule, entryPoint: 'cs_main' },
423
+ * });
424
+ */
425
+ export type ComputePipelineDescriptor = ExplicitUndefined<
426
+ Omit<Pick<GPUComputePipelineDescriptor, 'label' | 'layout' | 'compute'>, 'layout' | 'compute'>
427
+ > & {
428
+ /**
429
+ * Either `'auto'` for user-agent BGL inference or a forgeax
430
+ * `PipelineLayout` opaque handle (D-S5 pattern: forgeax handle replaces
431
+ * spec polymorphism `(GPUAutoLayoutMode or GPUPipelineLayout)`).
432
+ */
433
+ layout: 'auto' | PipelineLayout;
434
+ /**
435
+ * The compute programmable stage. `module` is the forgeax `ShaderModule`
436
+ * opaque handle (replacing spec `GPUShaderModule`); `entryPoint?` defaults
437
+ * to the module's single compute entry; `constants?` is a record of
438
+ * pipeline-overridable constants.
439
+ */
440
+ compute: {
441
+ module: ShaderModule;
442
+ entryPoint?: string | undefined;
443
+ constants?: Record<string, number> | undefined;
444
+ };
445
+ };
446
+
447
+ /**
448
+ * GPU query set descriptor (Pick<GPUQuerySetDescriptor, 'label' | 'type' | 'count'>).
449
+ *
450
+ * Spec anchor: W3C WebGPU §queries / [@webgpu/types.GPUQuerySetDescriptor].
451
+ * Field NAMES align byte-for-byte.
452
+ *
453
+ * Hard constraints (research §1.3):
454
+ * - `count <= 4096` (spec normative). The shim fast-paths a violation to
455
+ * Result.err({ code: 'limit-exceeded',
456
+ * expected: 'count <= 4096 (spec normative)',
457
+ * hint: 'create multiple QuerySet instances if more than 4096 queries needed' }).
458
+ * - `type === 'timestamp'` requires `caps.timestampQuery === true` (the
459
+ * 'timestamp-query' feature). Otherwise the shim fast-paths to
460
+ * Result.err({ code: 'feature-not-enabled' }).
461
+ * - `count = 0` is legal (lower bound; dawn end2end test fixture).
462
+ *
463
+ * @example
464
+ * const r = device.createQuerySet({ type: 'occlusion', count: 4 });
465
+ * if (!r.ok) {
466
+ * // route via switch (r.error.code)
467
+ * }
468
+ */
469
+ export type QuerySetDescriptor = ExplicitUndefined<
470
+ Pick<GPUQuerySetDescriptor, 'label' | 'type' | 'count'>
471
+ >;
472
+
473
+ /**
474
+ * Discriminated union over the 4 BindGroup entry resource kinds (the spec
475
+ * polymorphic `GPUBindingResource` collapsed to a tagged union — charter
476
+ * proposition 4 closed-union exhaustive switch + proposition 5 consistent
477
+ * abstraction over duck-typing).
478
+ *
479
+ * Introduced in feat-20260511-rhi-spec-realign-aggressive w9 per requirements
480
+ * AC-10 + plan-strategy §7.1 + D-P2 break-point #5. AI users `switch
481
+ * (resource.kind)` is exhaustive without a default fallback; construction-side
482
+ * typos like `{ kind: 'samplre', ... }` trip TS2322 at the literal slot.
483
+ *
484
+ * Kind discriminator uses kebab-case for multi-word entries (`textureView` is
485
+ * already single-word camelCase by convention; `externalTexture` is multi-word
486
+ * camelCase for parity with the spec verb `GPUExternalTexture`).
487
+ *
488
+ * @example
489
+ * const e: RhiBindingResource = { kind: 'sampler', value: linearSampler };
490
+ * const e2: RhiBindingResource = {
491
+ * kind: 'buffer',
492
+ * value: { buffer: viewUniforms, offset: 0, size: 64 },
493
+ * };
494
+ */
495
+ export type RhiBindingResource =
496
+ | { readonly kind: 'sampler'; readonly value: Sampler }
497
+ | {
498
+ readonly kind: 'buffer';
499
+ readonly value: {
500
+ readonly buffer: Buffer;
501
+ readonly offset?: number;
502
+ readonly size?: number;
503
+ };
504
+ }
505
+ | { readonly kind: 'textureView'; readonly value: TextureView }
506
+ | { readonly kind: 'externalTexture'; readonly value: GPUExternalTexture };
507
+
508
+ /**
509
+ * BindGroup entry — one slot in a BindGroup, identified by `binding` (the
510
+ * shader binding number) and `resource` (the tagged-union `RhiBindingResource`
511
+ * — replaces the spec polymorphic `GPUBindingResource`).
512
+ *
513
+ * Field set strictly mirrors `Pick<GPUBindGroupEntry, 'binding'>`; the
514
+ * `resource` field is tightened to the forgeax `RhiBindingResource` tagged
515
+ * union per D-P2 break-point #5 (charter proposition 5 consistent abstraction:
516
+ * AI users see one canonical 4-kind switch rather than spec duck-typing).
517
+ */
518
+ export type BindGroupEntry = Pick<GPUBindGroupEntry, 'binding'> & {
519
+ resource: RhiBindingResource;
520
+ };
521
+
522
+ /**
523
+ * GPU bind group descriptor (Pick<GPUBindGroupDescriptor, 'label' | 'layout' | 'entries'>).
524
+ *
525
+ * Spec anchor: W3C WebGPU 10 Resource binding /
526
+ * [@webgpu/types.GPUBindGroupDescriptor].
527
+ *
528
+ * Introduced in feat-20260509-ecs-render-bridge-mvp (D-S1) so the RenderSystem
529
+ * can record `pass.setBindGroup(0/1/2, bg, ...)` through a single RHI surface
530
+ * (charter proposition 5 consistent abstraction; never via raw GPUDevice).
531
+ *
532
+ * The `layout` field references the forgeax `BindGroupLayout` opaque handle
533
+ * (created via `RhiDevice.createBindGroupLayout`); the `entries` array uses
534
+ * the forgeax `BindGroupEntry` shape (binding number + tagged-union
535
+ * `RhiBindingResource`) per feat-20260511-rhi-spec-realign-aggressive D-P2
536
+ * break-point #5 (was previously verbatim `GPUBindGroupEntry`).
537
+ *
538
+ * @example
539
+ * const desc: BindGroupDescriptor = {
540
+ * label: 'view-bg',
541
+ * layout: bgl,
542
+ * entries: [{ binding: 0, resource: { kind: 'buffer', value: { buffer: viewUniforms } } }],
543
+ * };
544
+ */
545
+ export type BindGroupDescriptor = ExplicitUndefined<
546
+ Omit<Pick<GPUBindGroupDescriptor, 'label' | 'layout' | 'entries'>, 'layout' | 'entries'>
547
+ > & {
548
+ /**
549
+ * BindGroupLayout opaque handle (forgeax tightening: `layout` is the
550
+ * already-shipped `BindGroupLayout` brand, not the spec
551
+ * `GPUBindGroupLayout`). Same D-S5 pattern as RenderPassColorAttachment.view
552
+ * — AI users receive a forgeax-creatable handle, never a phantom spec type.
553
+ */
554
+ layout: BindGroupLayout;
555
+ /**
556
+ * Iterable of forgeax `BindGroupEntry` (binding + tagged-union
557
+ * `RhiBindingResource`); replaces the spec polymorphic
558
+ * `iterable<GPUBindGroupEntry>` per D-P2 break-point #5.
559
+ */
560
+ entries: Iterable<BindGroupEntry>;
561
+ };
562
+
563
+ /**
564
+ * GPU pipeline layout descriptor (Pick<GPUPipelineLayoutDescriptor, 'label' | 'bindGroupLayouts'>).
565
+ *
566
+ * Spec anchor: W3C WebGPU 10.3 Pipeline layout /
567
+ * [@webgpu/types.GPUPipelineLayoutDescriptor].
568
+ *
569
+ * Introduced in feat-20260509-ecs-render-bridge-mvp (D-S1) so the
570
+ * `Renderer.ready` step 2 (PBR pipeline compile) can compose the 3
571
+ * BindGroupLayouts (view / material / mesh-array) into a single
572
+ * `PipelineLayout` for `RhiDevice.createRenderPipeline`.
573
+ *
574
+ * @example
575
+ * const desc: PipelineLayoutDescriptor = {
576
+ * label: 'pbr-pl',
577
+ * bindGroupLayouts: [viewBgl, materialBgl, meshArrayBgl],
578
+ * };
579
+ */
580
+ export type PipelineLayoutDescriptor = ExplicitUndefined<
581
+ Omit<Pick<GPUPipelineLayoutDescriptor, 'label' | 'bindGroupLayouts'>, 'bindGroupLayouts'>
582
+ > & {
583
+ /**
584
+ * Iterable of BindGroupLayout opaque handles (forgeax tightening: the
585
+ * iterable element type is the forgeax `BindGroupLayout` brand). Same D-S5
586
+ * pattern as BindGroupDescriptor.layout.
587
+ */
588
+ bindGroupLayouts: Iterable<BindGroupLayout>;
589
+ };
590
+
591
+ /** GPU render pipeline descriptor. Field set strictly matches
592
+ * GPURenderPipelineDescriptor. */
593
+ export type RenderPipelineDescriptor = ExplicitUndefined<
594
+ Pick<
595
+ GPURenderPipelineDescriptor,
596
+ 'label' | 'layout' | 'vertex' | 'primitive' | 'depthStencil' | 'multisample' | 'fragment'
597
+ >
598
+ >;
599
+
600
+ // ============================================================================
601
+ // 4 new descriptors (feat-20260508-rhi-surface-completion w8 / D-S5,
602
+ // feat-20260510-rhi-resource-creation M2 view narrow Path X)
603
+ // ============================================================================
604
+ //
605
+ // Field NAMES align byte-for-byte with @webgpu/types (Pick<GPUXxxDescriptor,
606
+ // ...> shape preserved; R12 lint enforces this). Field TYPES for `view` are
607
+ // SPEC-ALIGNED to the forgeax `TextureView` opaque handle (per
608
+ // feat-20260510-rhi-resource-creation IN-2 / AC-02 view narrow Path X;
609
+ // breakage point #1). The earlier D-S5 temporary tightening to `Texture` was
610
+ // retired once M1 of feat-20260510-rhi-resource-creation shipped
611
+ // `RhiDevice.createTextureView`, so AI users now follow the spec idiom:
612
+ // const view = device.createTextureView(texture, desc).unwrap();
613
+ // pass.beginRenderPass({ colorAttachments: [{ view, ... }] });
614
+ //
615
+ // Charter mapping: proposition 5 consistent abstraction (call site lines up
616
+ // with what `createTextureView` returns) + proposition 4 explicit failure
617
+ // (passing a Texture brand to `view` is a tsc red signal at the call site,
618
+ // not a runtime swap). The breakage point is registered in the AGENTS.md
619
+ // RHI / Shader error model contract section -> breakage point list (M7 of
620
+ // this closure).
621
+
622
+ /**
623
+ * Command encoder descriptor (Pick<GPUCommandEncoderDescriptor, 'label'>).
624
+ *
625
+ * Spec anchor: W3C WebGPU 22 GPUCommandEncoder /
626
+ * [@webgpu/types.GPUCommandEncoderDescriptor].
627
+ *
628
+ * @example
629
+ * const desc: CommandEncoderDescriptor = { label: 'frame-encoder' };
630
+ */
631
+ export type CommandEncoderDescriptor = ExplicitUndefined<
632
+ Pick<GPUCommandEncoderDescriptor, 'label'>
633
+ >;
634
+
635
+ /**
636
+ * Render-pass color attachment (Pick<GPURenderPassColorAttachment, ...> with
637
+ * `view` field aligned to the forgeax `TextureView` opaque handle, per
638
+ * feat-20260510-rhi-resource-creation M2 view narrow Path X / breakage point
639
+ * #1).
640
+ *
641
+ * Spec anchor: W3C WebGPU 22.7 Render pass /
642
+ * [@webgpu/types.GPURenderPassColorAttachment].
643
+ *
644
+ * v0.1.69 spec shape: `view: GPUTexture | GPUTextureView`. The forgeax RHI
645
+ * tightens the union to the single `TextureView` brand (the only branch the
646
+ * shim ever produces post-M1). Charter proposition 5 consistent abstraction
647
+ * (the field type matches what `RhiDevice.createTextureView` returns) +
648
+ * proposition 4 explicit failure (passing a `Texture` brand here is a tsc red
649
+ * signal; the AI user is steered to the spec idiom).
650
+ *
651
+ * **Migration**: see AGENTS.md break-point list 2026-05-10 #1
652
+ * "view: Texture -> TextureView narrow (major breaking)" for the call-site
653
+ * upgrade diff (3 narrowed fields: this `view` + `resolveTarget` +
654
+ * `RenderPassDepthStencilAttachment.view`).
655
+ *
656
+ * @example
657
+ * const view = device.createTextureView(tex, {}).unwrap();
658
+ * const att: RenderPassColorAttachment = {
659
+ * view,
660
+ * clearValue: { r: 0, g: 0, b: 0, a: 1 },
661
+ * loadOp: 'clear',
662
+ * storeOp: 'store',
663
+ * };
664
+ */
665
+ export type RenderPassColorAttachment = ExplicitUndefined<
666
+ Omit<
667
+ Pick<
668
+ GPURenderPassColorAttachment,
669
+ 'view' | 'depthSlice' | 'resolveTarget' | 'clearValue' | 'loadOp' | 'storeOp'
670
+ >,
671
+ 'view' | 'resolveTarget'
672
+ >
673
+ > & {
674
+ /**
675
+ * TextureView target of this color attachment (view narrow Path X: aligned
676
+ * to the forgeax `TextureView` brand returned by `createTextureView`; not
677
+ * the spec union `GPUTexture | GPUTextureView`).
678
+ */
679
+ view: TextureView;
680
+ /**
681
+ * Optional resolve target for multisample resolution. Same view narrow
682
+ * alignment as `view`.
683
+ */
684
+ resolveTarget?: TextureView | undefined;
685
+ };
686
+
687
+ /**
688
+ * Render-pass depth/stencil attachment (Pick<GPURenderPassDepthStencilAttachment,
689
+ * ...> with `view` field aligned to the forgeax `TextureView` opaque handle,
690
+ * per feat-20260510-rhi-resource-creation M2 view narrow Path X / breakage
691
+ * point #1).
692
+ *
693
+ * Spec anchor: W3C WebGPU 22.7 Render pass /
694
+ * [@webgpu/types.GPURenderPassDepthStencilAttachment].
695
+ *
696
+ * Same view narrow alignment as RenderPassColorAttachment.
697
+ *
698
+ * **Migration**: see AGENTS.md break-point list 2026-05-10 #1 for the
699
+ * call-site upgrade diff covering this `view` field plus the two
700
+ * `RenderPassColorAttachment` narrowed fields.
701
+ *
702
+ * @example
703
+ * const view = device.createTextureView(depthTex, {}).unwrap();
704
+ * const ds: RenderPassDepthStencilAttachment = {
705
+ * view,
706
+ * depthClearValue: 1,
707
+ * depthLoadOp: 'clear',
708
+ * depthStoreOp: 'store',
709
+ * };
710
+ */
711
+ export type RenderPassDepthStencilAttachment = ExplicitUndefined<
712
+ Omit<
713
+ Pick<
714
+ GPURenderPassDepthStencilAttachment,
715
+ | 'view'
716
+ | 'depthClearValue'
717
+ | 'depthLoadOp'
718
+ | 'depthStoreOp'
719
+ | 'depthReadOnly'
720
+ | 'stencilClearValue'
721
+ | 'stencilLoadOp'
722
+ | 'stencilStoreOp'
723
+ | 'stencilReadOnly'
724
+ >,
725
+ 'view'
726
+ >
727
+ > & {
728
+ /**
729
+ * TextureView target of this depth/stencil attachment (view narrow Path X:
730
+ * aligned to the forgeax `TextureView` brand).
731
+ */
732
+ view: TextureView;
733
+ };
734
+
735
+ /**
736
+ * Render-pass descriptor (Pick<GPURenderPassDescriptor, ...>).
737
+ *
738
+ * Spec anchor: W3C WebGPU 22.7 Render pass /
739
+ * [@webgpu/types.GPURenderPassDescriptor].
740
+ *
741
+ * `colorAttachments` element type uses the forgeax narrow
742
+ * `RenderPassColorAttachment` (with `view: TextureView`);
743
+ * `depthStencilAttachment` uses `RenderPassDepthStencilAttachment`.
744
+ *
745
+ * @example
746
+ * const view = device.createTextureView(tex, {}).unwrap();
747
+ * const desc: RenderPassDescriptor = {
748
+ * label: 'frame',
749
+ * colorAttachments: [{ view, loadOp: 'clear', storeOp: 'store',
750
+ * clearValue: { r: 0, g: 0, b: 0, a: 1 } }],
751
+ * };
752
+ */
753
+ export type RenderPassDescriptor = ExplicitUndefined<
754
+ Omit<
755
+ Pick<
756
+ GPURenderPassDescriptor,
757
+ | 'label'
758
+ | 'colorAttachments'
759
+ | 'depthStencilAttachment'
760
+ | 'occlusionQuerySet'
761
+ | 'timestampWrites'
762
+ | 'maxDrawCount'
763
+ >,
764
+ 'colorAttachments' | 'depthStencilAttachment' | 'occlusionQuerySet' | 'timestampWrites'
765
+ >
766
+ > & {
767
+ /**
768
+ * Color attachments for this render pass (forgeax narrow element type per
769
+ * view narrow Path X: each entry's `view` field is `TextureView`, not the
770
+ * spec union `GPUTexture | GPUTextureView`).
771
+ */
772
+ colorAttachments: Iterable<RenderPassColorAttachment | null | undefined>;
773
+ /** Optional depth/stencil attachment with the same view narrow alignment. */
774
+ depthStencilAttachment?: RenderPassDepthStencilAttachment | undefined;
775
+ /** Occlusion query set (capability-gated). The shim accepts a
776
+ * `QuerySet` brand created via `device.createQuerySet({ type: 'occlusion',
777
+ * count })` and pairs it with `pass.beginOcclusionQuery(idx) /
778
+ * pass.endOcclusionQuery()`; both methods now have real implementations
779
+ * (see `RhiRenderPassEncoder` below). Capability gate: read
780
+ * `device.caps.timestampQuery` ahead of `'timestamp'` query sets;
781
+ * occlusion sets are unconditionally available. */
782
+ occlusionQuerySet?: QuerySet | undefined;
783
+ /** Optional timestamp query set and the pass boundary write slots. */
784
+ timestampWrites?: RenderPassTimestampWrites | undefined;
785
+ };
786
+
787
+ /** Timestamp writes attached to a render pass descriptor. */
788
+ export interface RenderPassTimestampWrites {
789
+ querySet: QuerySet;
790
+ beginningOfPassWriteIndex?: number | undefined;
791
+ endOfPassWriteIndex?: number | undefined;
792
+ }
793
+
794
+ /** Compute-pass descriptor with forgeax-owned timestamp query handles. */
795
+ export type ComputePassDescriptor = ExplicitUndefined<
796
+ Omit<Pick<GPUComputePassDescriptor, 'label' | 'timestampWrites'>, 'timestampWrites'>
797
+ > & {
798
+ timestampWrites?: ComputePassTimestampWrites | undefined;
799
+ };
800
+
801
+ /** Timestamp writes attached to a compute pass descriptor. */
802
+ export interface ComputePassTimestampWrites {
803
+ querySet: QuerySet;
804
+ beginningOfPassWriteIndex?: number | undefined;
805
+ endOfPassWriteIndex?: number | undefined;
806
+ }
807
+
808
+ // ============================================================================
809
+ // Capabilities trio (MVP-1.2) - readonly + independent fields
810
+ // ============================================================================
811
+ //
812
+ // Charter proposition 5 (consistent abstraction / discoverable differences):
813
+ // caps (hardware probe) / features (enabled set) / limits (numeric ceilings)
814
+ // are three independent semantic layers; `caps.X = false` is an explicit
815
+ // signal, never an exception (proposition 4).
816
+
817
+ /** Hardware-probe layer: readonly boolean capability flags. */
818
+ export interface RhiCaps {
819
+ /**
820
+ * The rendering backend kind — single source of truth for backend-aware
821
+ * logic (e.g. explicit barrier insertion vs. spec-managed / GL-implicit
822
+ * sync). Closed 4-member union: every backend reports exactly one.
823
+ *
824
+ * - `'webgpu'`: browser WebGPU — spec-managed barriers, no explicit
825
+ * barrier insertion needed.
826
+ * - `'wgpu-native'`: wgpu native-desktop runtime (Tauri / native) —
827
+ * requires explicit Vulkan/Metal/DX12 barrier commands.
828
+ * - `'wgpu-webgl2'`: wgpu GLES3/WebGL2 backend — GL implicit sync, no
829
+ * explicit barrier insertion needed (equivalence group with `'webgpu'`).
830
+ * - `'null'`: headless no-op backend (`@forgeax/engine-rhi-null`) for
831
+ * structural unit tests — no GPU / DOM; records command-stream shape into
832
+ * a ledger instead of executing it. No barrier insertion needed (the
833
+ * no-op backend executes nothing); falls into the same no-barrier branch
834
+ * as `'webgpu'` / `'wgpu-webgl2'`.
835
+ *
836
+ * @note `exactOptionalPropertyTypes` requires every backend fill this
837
+ * field; a backend that omits it produces a tsc compile error.
838
+ */
839
+ readonly backendKind: 'webgpu' | 'wgpu-native' | 'wgpu-webgl2' | 'null';
840
+ /** Whether compute pipelines are supported. */
841
+ readonly compute: boolean;
842
+ /** Whether timestamp queries are supported. */
843
+ readonly timestampQuery: boolean;
844
+ /** Backend-owned nanoseconds represented by one timestamp tick, or null when unavailable. */
845
+ readonly timestampPeriodNanoseconds: number | null;
846
+ /** Whether indirect drawing is supported. */
847
+ readonly indirectDrawing: boolean;
848
+ /**
849
+ * Whether BC texture compression (BC1-BC7) is available.
850
+ *
851
+ * Derived from `adapter.features.has('texture-compression-bc')`.
852
+ * On rhi-null this is always `false` (headless has no compression hardware,
853
+ * AC-06).
854
+ */
855
+ readonly textureCompressionBc: boolean;
856
+ /**
857
+ * Whether ETC2 texture compression is available.
858
+ *
859
+ * Derived from `adapter.features.has('texture-compression-etc2')`.
860
+ * On rhi-null this is always `false` (headless has no compression hardware,
861
+ * AC-06).
862
+ */
863
+ readonly textureCompressionEtc2: boolean;
864
+ /**
865
+ * Whether ASTC texture compression is available.
866
+ *
867
+ * Derived from `adapter.features.has('texture-compression-astc')`.
868
+ * On rhi-null this is always `false` (headless has no compression hardware,
869
+ * AC-06).
870
+ */
871
+ readonly textureCompressionAstc: boolean;
872
+ /**
873
+ * Whether multi-draw indirect is available (wgpu native extension).
874
+ *
875
+ * @reserved-for-wgpu-native-only always `false` on browser backends; only
876
+ * available when the forgeax renderer runs against a wgpu native runtime
877
+ * (Tauri / native runtime, not the wasm bundle). `caps.X = false` is an
878
+ * explicit signal, never an exception (charter proposition 4 / AGENTS.md
879
+ * `RHI / WebGPU` shape rule #2 capability-gated).
880
+ */
881
+ readonly multiDrawIndirect: boolean;
882
+ /**
883
+ * Whether push constants are available (wgpu native extension).
884
+ *
885
+ * @reserved-for-wgpu-native-only always `false` on browser backends; only
886
+ * available when the forgeax renderer runs against a wgpu native runtime
887
+ * (Tauri / native runtime, not the wasm bundle). `caps.X = false` is an
888
+ * explicit signal, never an exception (charter proposition 4 / AGENTS.md
889
+ * `RHI / WebGPU` shape rule #2 capability-gated).
890
+ */
891
+ readonly pushConstants: boolean;
892
+ /**
893
+ * Whether bindless texture array is available (wgpu native extension).
894
+ *
895
+ * @reserved-for-wgpu-native-only always `false` on browser backends; only
896
+ * available when the forgeax renderer runs against a wgpu native runtime
897
+ * (Tauri / native runtime, not the wasm bundle). `caps.X = false` is an
898
+ * explicit signal, never an exception (charter proposition 4 / AGENTS.md
899
+ * `RHI / WebGPU` shape rule #2 capability-gated).
900
+ */
901
+ readonly textureBindingArray: boolean;
902
+ /**
903
+ * Whether sampler binding aliasing is supported across pipelines.
904
+ *
905
+ * @spec-anchor W3C WebGPU §10.3 Bind group layout — spec mandates that a
906
+ * sampler may alias multiple binding slots; both navigator.gpu and the
907
+ * wgpu wasm bundle satisfy this so the field is always `true` on browser
908
+ * backends.
909
+ * @note Always `true` on shipped backends (WebGPU + wgpu wasm). The field
910
+ * exists for potential future backends that lack sampler aliasing.
911
+ * @hint AI users use `caps.samplerAliasing` to gate code that creates two
912
+ * `BindGroupEntry`s pointing at the same `Sampler` across different
913
+ * layouts; `caps.X = false` is an explicit signal, never an exception
914
+ * (charter proposition 4).
915
+ */
916
+ readonly samplerAliasing: boolean;
917
+ /**
918
+ * Whether the renderer can issue indirect draws with a non-zero
919
+ * `firstInstance`.
920
+ *
921
+ * @spec-anchor W3C WebGPU §22.4 drawIndirect — the `indirect-first-instance`
922
+ * feature on `GPUAdapter.features` gates non-zero `firstInstance` in
923
+ * indirect draws; rhi-webgpu maps this to `device.features.has(
924
+ * 'indirect-first-instance')`.
925
+ * @note `false` on backends without indirect drawing support.
926
+ * @hint Most AI users never need this; the field surfaces so a renderer
927
+ * author building instanced draw batchers can gate the fast path. With
928
+ * `caps.firstInstanceIndirect === false` the renderer must pre-rebase
929
+ * instance indices in the vertex shader (charter proposition 5
930
+ * consistent abstraction over a discoverable cap difference).
931
+ */
932
+ readonly firstInstanceIndirect: boolean;
933
+ /**
934
+ * Whether storage buffer bindings are available
935
+ * (`device.limits.maxStorageBuffersPerShaderStage > 0`).
936
+ *
937
+ * @spec-anchor W3C WebGPU §3.6.2 GPUSupportedLimits.
938
+ * maxStorageBuffersPerShaderStage; `> 0` means the device supports the
939
+ * `storage` / `read-only-storage` binding types.
940
+ * @note `false` on backends without storage buffer support.
941
+ * @hint AI users gate compute / large-buffer paths on
942
+ * `caps.storageBuffer`; the per-stage numeric limit lives on
943
+ * `device.limits.maxStorageBuffersPerShaderStage` for capacity planning
944
+ * (charter proposition 4: `caps.X = false` is an explicit signal).
945
+ */
946
+ readonly storageBuffer: boolean;
947
+ /**
948
+ * Whether storage texture bindings are available
949
+ * (`device.limits.maxStorageTexturesPerShaderStage > 0`).
950
+ *
951
+ * @spec-anchor W3C WebGPU §3.6.2 GPUSupportedLimits.
952
+ * maxStorageTexturesPerShaderStage; `> 0` means the device supports the
953
+ * `write-only` / `read-write` storage texture binding types.
954
+ * @note `false` on backends without storage texture support.
955
+ * @hint AI users gate image-effects compute / postprocess paths on
956
+ * `caps.storageTexture`; the per-stage numeric limit lives on
957
+ * `device.limits.maxStorageTexturesPerShaderStage` (charter proposition
958
+ * 4: `caps.X = false` is an explicit signal).
959
+ */
960
+ readonly storageTexture: boolean;
961
+ /**
962
+ * Whether the device can create `rgba16float` textures with `RENDER_ATTACHMENT`
963
+ * usage, enabling the HDR cubemap path for IBL irradiance / specular prefilter
964
+ * and downstream HDR render-target chains.
965
+ *
966
+ * @spec-anchor W3C WebGPU $25.1 GPUTextureFormat — `rgba16float` is an
967
+ * optional texture format whose `RENDER_ATTACHMENT` capability is probed by
968
+ * attempting `createTexture({ format: 'rgba16float', usage:
969
+ * GPUTextureUsage.RENDER_ATTACHMENT, size: [1, 1, 1] })` on the live
970
+ * device; failure maps the cap to `false`.
971
+ * @note Probed at device-creation time via a synchronous `createTexture`
972
+ * call, not via `GPUAdapter.features`. The `rgba16float` format is widely
973
+ * supported but `RENDER_ATTACHMENT` with float formats is optional per spec
974
+ * so the cap reflects the concrete device, not the adapter feature list.
975
+ * @hint AI users gate IBL / HDR post-processing paths on
976
+ * `caps.rgba16floatRenderable`; when `false` the internal equirect-to-cubemap
977
+ * IBL projection (driven by declaring `Skylight{equirect}`) degrades to the
978
+ * white-cube fallback and fires `{ code: 'equirect-projection-failed' }` with
979
+ * a machine-readable `expected` field naming this cap (charter P3 structured
980
+ * failure).
981
+ */
982
+ readonly rgba16floatRenderable: boolean;
983
+ /**
984
+ * Whether the device can create `rg11b10ufloat` textures with
985
+ * `RENDER_ATTACHMENT` usage, enabling the HDR swapchain / render-target path
986
+ * with reduced bit-depth precision versus `rgba16float`.
987
+ *
988
+ * @spec-anchor W3C WebGPU $25.1 GPUTextureFormat — `rg11b10ufloat` is
989
+ * `RENDER_ATTACHMENT`-capable only when the optional feature
990
+ * `rg11b10ufloat-renderable` is enabled (W3C WebGPU $4.2). Probed by gating
991
+ * on `device.features.has('rg11b10ufloat-renderable')` first; only then
992
+ * confirmed by `createTexture({ format: 'rg11b10ufloat', usage:
993
+ * GPUTextureUsage.RENDER_ATTACHMENT, size: [1, 1, 1] })`.
994
+ * @note The format packs 11+11+10 unsigned float bits into 32 bits per pixel;
995
+ * it is a popular HDR swapchain format for engines that trade precision
996
+ * for bandwidth but its `RENDER_ATTACHMENT` capability is not universal.
997
+ * The feature gate is the authoritative answer (avoiding fan-out via
998
+ * `device.onuncapturederror` when the optional feature is absent); the
999
+ * subsequent probe handles the rare case where the feature is reported
1000
+ * but the concrete device still rejects.
1001
+ * @hint AI users can select an HDR back-buffer format by reading
1002
+ * `caps.rg11b10ufloatRenderable` before creating a
1003
+ * `GPUTextureUsage.RENDER_ATTACHMENT` texture at that format; when `false`
1004
+ * fall back to `rgba16float` (if `caps.rgba16floatRenderable` is true)
1005
+ * or an SDR format.
1006
+ */
1007
+ readonly rg11b10ufloatRenderable: boolean;
1008
+ /**
1009
+ * Whether the device supports sampling `rgba32float` textures with a
1010
+ * `filtering` sampler (linear / mipmap filtering), NOT just with a
1011
+ * `non-filtering` sampler.
1012
+ *
1013
+ * @spec-anchor W3C WebGPU $10.3 Bind group layout — a bind group layout
1014
+ * entry pairing a `filtering` sampler type with `sampleType: 'float'`
1015
+ * (matching `rgba32float`) validates only when the
1016
+ * `float32-filterable` feature is enabled. The cap probes this via
1017
+ * `device.createBindGroupLayout({ entries: [{ sampler: { type:
1018
+ * 'filtering' } }, { texture: { sampleType: 'float' } }] })` and
1019
+ * `device.createSampler({ minFilter: 'linear', magFilter: 'linear' })`;
1020
+ * failure maps the cap to `false`.
1021
+ * @note Probed by gating on `device.features.has('float32-filterable')`
1022
+ * first (the authoritative answer per spec $4.2; avoids fan-out via
1023
+ * `device.onuncapturederror` when the optional feature is absent); only
1024
+ * then confirmed by exercising the bind-group-layout. The subsequent
1025
+ * probe handles the rare case where the feature is reported but the
1026
+ * concrete device still rejects (spec ambiguity, driver quirks).
1027
+ * @hint AI users gate float32-sampled compute / post-process paths on
1028
+ * `caps.float32Filterable`; when `false` use `sampleType: 'unfilterable-
1029
+ * float'` with a `non-filtering` sampler and compute the filter kernel
1030
+ * manually in the shader, or fall back to `rgba16float` with filtering.
1031
+ */
1032
+ readonly float32Filterable: boolean;
1033
+ /**
1034
+ * Maximum number of color attachments per render pass.
1035
+ *
1036
+ * @spec-anchor W3C WebGPU $3.6.2 GPUSupportedLimits.maxColorAttachments;
1037
+ * spec minimum = 4, defaults to 8 on mainstream backends.
1038
+ * HDRP deferred pipeline requires >= 4 (3 g-buffer RT + 1 depth);
1039
+ * installPipeline checks this cap at install time and throws
1040
+ * `hdrp-deferred-caps-insufficient` on violation (charter P3).
1041
+ * @note add-only minor (feat-20260612-hdrp-deferred-shading-learn-render-5-8
1042
+ * M1 / w5); no existing field is modified.
1043
+ */
1044
+ readonly maxColorAttachments: number;
1045
+ }
1046
+
1047
+ /**
1048
+ * Enabled feature set, opaque iteration only via `has()`.
1049
+ *
1050
+ * Aligned with `GPUSupportedFeatures` shape; this empty interface intentionally
1051
+ * adds no fields — the concrete enabled set is decided at `requestDevice` time
1052
+ * and is then probed by AI users via `device.features.has('feature-name')`
1053
+ * (charter proposition 5 consistent abstraction; no implementation-detail leak,
1054
+ * no enumeration helper that would tie callers to a fixed feature list).
1055
+ *
1056
+ * @see {@link GPUSupportedFeatures}
1057
+ */
1058
+ export interface RhiFeatures extends ReadonlySet<GPUFeatureName> {
1059
+ /** Aligned with GPUSupportedFeatures shape; concrete enabled set is decided
1060
+ * at device creation time. */
1061
+ }
1062
+
1063
+ /** Numeric-limits layer aligned with GPUSupportedLimits (incl Compatibility
1064
+ * Mode follow-on fields). */
1065
+ export type RhiLimits = Readonly<GPUSupportedLimits>;
1066
+
1067
+ // ============================================================================
1068
+ // 7 main interfaces: Device / Queue / CommandEncoder / RenderPassEncoder /
1069
+ // ComputePassEncoder / RenderPipeline / ComputePipeline
1070
+ // ============================================================================
1071
+ //
1072
+ // Interface signatures accept POD + ArrayBuffer / Float32Array (math-free /
1073
+ // MVP-1.5); all fallible operations return Result<T, RhiError>
1074
+ // (D-5 + AGENTS.md error baseline).
1075
+ //
1076
+ // Note: interface names RenderPipeline / ComputePipeline / CommandEncoder
1077
+ // match opaque handle names - spec-aligned choice (per plan-strategy 7.1)
1078
+ // matching GPURenderPipeline et al; module-path semantics distinguish them.
1079
+ // M1 interfaces express "operation verb sets"; opaque handles serve as
1080
+ // return-value types.
1081
+
1082
+ // ============================================================================
1083
+ // RhiInstance + RhiAdapter (M3 break-point #2; K-5 / K-6)
1084
+ // ============================================================================
1085
+ //
1086
+ // Strict two-step path: `rhi.requestAdapter(opts) -> adapter.requestDevice(opts)`
1087
+ // mirrors wgpu (research §6.1) + Dawn (§6.2) source-level idiom. The legacy
1088
+ // top-level `rhi.requestDevice(opts)` factory is deprecated in favour of this
1089
+ // path; AGENTS.md break-point list registers the deprecation under
1090
+ // feat-20260510-rhi-resource-creation.
1091
+ //
1092
+ // K-5: `RhiAdapter.features: ReadonlySet<GPUFeatureName>` (Round 3 fix-up
1093
+ // F-P1-2: aligned with `RhiDevice.features: RhiFeatures extends
1094
+ // ReadonlySet<GPUFeatureName>` so the cross-tier surface is uniform —
1095
+ // AI users use `.has(name)` on both abstraction layers; previously a
1096
+ // `ReadonlyArray<string>` projection drifted from the spec
1097
+ // `GPUSupportedFeatures` Set shape and split the AI-user idiom).
1098
+ // `RhiAdapter.limits: Readonly<Record<string, number>>` aligns with
1099
+ // `GPUAdapter.limits` (`GPUSupportedLimits`). RhiDevice.caps stays
1100
+ // as the existing high-level boolean gate (caps.compute /
1101
+ // caps.timestampQuery etc.).
1102
+ //
1103
+ // K-6: `RhiAdapter.requestDevice` returns `Result<RhiDevice, RhiError>` (NOT a
1104
+ // `(Device, Queue)` tuple). The queue continues to be exposed via the
1105
+ // existing `RhiDevice.queue: RhiQueue` field (spec `device.queue` auto-
1106
+ // provisioned, packages/rhi/src/index.ts).
1107
+
1108
+ /**
1109
+ * RhiAdapter request options.
1110
+ *
1111
+ * Spec anchor: W3C WebGPU §3.2 `GPURequestAdapterOptions` /
1112
+ * [@webgpu/types.GPURequestAdapterOptions]. Fields pass through to the
1113
+ * underlying `navigator.gpu.requestAdapter(opts)` call.
1114
+ */
1115
+ export type RequestAdapterOptions = ExplicitUndefined<
1116
+ Pick<GPURequestAdapterOptions, 'powerPreference' | 'forceFallbackAdapter'>
1117
+ >;
1118
+
1119
+ /**
1120
+ * RhiAdapter.requestDevice options.
1121
+ *
1122
+ * Spec anchor: W3C WebGPU §3.4 `GPUDeviceDescriptor` /
1123
+ * [@webgpu/types.GPUDeviceDescriptor]. Fields pass through to the underlying
1124
+ * `adapter.requestDevice(opts)` call.
1125
+ */
1126
+ export type RequestDeviceOptions = ExplicitUndefined<
1127
+ Pick<GPUDeviceDescriptor, 'label' | 'requiredFeatures' | 'requiredLimits'>
1128
+ >;
1129
+
1130
+ /**
1131
+ * RhiInstance — entry point for adapter discovery (K-6 strict two-step path).
1132
+ *
1133
+ * Spec anchor: W3C WebGPU §3.1 `GPU` interface / [@webgpu/types.GPU]; wgpu
1134
+ * `Instance::request_adapter` (research §6.1) + Dawn
1135
+ * `InstanceBase::APIRequestAdapter` (§6.2).
1136
+ *
1137
+ * Replaces the legacy top-level `rhi.requestDevice(opts)` factory (break-
1138
+ * point #2). AI users follow the spec idiom:
1139
+ * const a = (await rhi.requestAdapter()).unwrap();
1140
+ * const d = (await a.requestDevice(opts)).unwrap();
1141
+ *
1142
+ * @example
1143
+ * const adapterResult = await rhi.requestAdapter();
1144
+ * if (!adapterResult.ok) {
1145
+ * // route via switch (adapterResult.error.code)
1146
+ * }
1147
+ */
1148
+ export interface RhiInstance {
1149
+ /**
1150
+ * Request a GPU adapter.
1151
+ *
1152
+ * Spec anchor: W3C WebGPU §3.1 `GPU.requestAdapter` /
1153
+ * [@webgpu/types.GPU.requestAdapter].
1154
+ *
1155
+ * @param opts — W3C-spec request adapter options (powerPreference,
1156
+ * forceFallbackAdapter).
1157
+ * @param compatibleSurface — non-W3C extension required by the wgpu GL
1158
+ * backend for adapter enumeration. Provided as a positional escape hatch
1159
+ * so the first parameter stays spec-aligned (plan-strategy D-5).
1160
+ * rhi-webgpu accepts and ignores this parameter (dual-impl symmetry);
1161
+ * rhi-wgpu routes it to `requestAdapterWithCanvas`.
1162
+ *
1163
+ * Failure paths (research §F-5):
1164
+ * - adapter null -> `Result.err({ code: 'adapter-unavailable' })`.
1165
+ */
1166
+ requestAdapter(
1167
+ opts?: RequestAdapterOptions | undefined,
1168
+ compatibleSurface?: HTMLCanvasElement | OffscreenCanvas | undefined,
1169
+ ): Promise<Result<RhiAdapter, RhiError>>;
1170
+ }
1171
+
1172
+ /**
1173
+ * RhiAdapter — capability-probe layer + device-creation entry (K-5 + K-6).
1174
+ *
1175
+ * Spec anchor: W3C WebGPU §3.2 `GPUAdapter` interface /
1176
+ * [@webgpu/types.GPUAdapter]; wgpu `Adapter::request_device` (research §6.1)
1177
+ * + Dawn `AdapterBase::APIRequestDevice` (§6.2).
1178
+ *
1179
+ * The `features` / `limits` fields let AI users **pre-screen** device
1180
+ * capabilities before calling `requestDevice(opts)` (charter proposition 4
1181
+ * forward-reachable: features mismatch becomes visible before spec
1182
+ * validation surfaces it).
1183
+ *
1184
+ * @example
1185
+ * if (!adapter.features.has('timestamp-query')) {
1186
+ * // skip timestamp-related code paths
1187
+ * }
1188
+ * const deviceResult = await adapter.requestDevice({
1189
+ * requiredFeatures: ['timestamp-query'],
1190
+ * });
1191
+ */
1192
+ export interface RhiAdapter {
1193
+ /**
1194
+ * Read-only feature-name set (K-5).
1195
+ *
1196
+ * Aligned with `GPUAdapter.features` projection of `GPUSupportedFeatures`
1197
+ * (a read-only Set) **and** with `RhiDevice.features` (Round 3 fix-up
1198
+ * F-P1-2: cross-tier shape uniformity — AI users use `.has(name)` on
1199
+ * both abstraction layers, no projection drift).
1200
+ *
1201
+ * F-1 ai-user-review: mutation of set entries (`features.add('x')` /
1202
+ * deletion / clear) is rejected at compile time via `ReadonlySet`;
1203
+ * charter proposition 4 explicit failure + proposition 5 consistent
1204
+ * abstraction.
1205
+ */
1206
+ readonly features: ReadonlySet<GPUFeatureName>;
1207
+ /**
1208
+ * Read-only numeric-limits map (K-5).
1209
+ *
1210
+ * Aligned with `GPUAdapter.limits` projection of `GPUSupportedLimits`. The
1211
+ * forgeax form flattens to a `Readonly<Record<string, number>>` so AI users
1212
+ * can do `adapter.limits.maxTextureDimension2D` lookups without holding the
1213
+ * spec object handle.
1214
+ *
1215
+ * F-1 ai-user-review: mutation of values (`limits.x = 0`) is rejected at
1216
+ * compile time.
1217
+ */
1218
+ readonly limits: Readonly<Record<string, number>>;
1219
+ /**
1220
+ * Request a GPU device.
1221
+ *
1222
+ * Spec anchor: W3C WebGPU §3.2 `GPUAdapter.requestDevice` /
1223
+ * [@webgpu/types.GPUAdapter.requestDevice].
1224
+ *
1225
+ * Returns `Result<RhiDevice, RhiError>` (K-6: NOT a `(Device, Queue)` tuple
1226
+ * — queue is exposed via `RhiDevice.queue`).
1227
+ *
1228
+ * Failure paths (research §F-5):
1229
+ * - feature not enabled -> `Result.err({ code: 'feature-not-enabled' })`.
1230
+ * - limit exceeded -> `Result.err({ code: 'limit-exceeded' })`.
1231
+ */
1232
+ requestDevice(opts?: RequestDeviceOptions | undefined): Promise<Result<RhiDevice, RhiError>>;
1233
+ }
1234
+
1235
+ // ============================================================================
1236
+ // RhiSurface + RhiCanvasContext (M3 / K-4)
1237
+ // ============================================================================
1238
+ //
1239
+ // Spec anchor: W3C WebGPU §3.3 GPUCanvasContext / §3.3 GPUCanvasConfiguration.
1240
+ // 4 methods + 7 fields per research §3.1 + §3.2; 4-method algorithms per
1241
+ // research §3.3. K-4 decision: getCurrentTexture returns Result<Texture,
1242
+ // RhiError> (NOT TextureView) — spec literal alignment + AI users go two-step:
1243
+ // const tex = canvasContext.getCurrentTexture().unwrap();
1244
+ // const view = device.createTextureView(tex, {}).unwrap();
1245
+
1246
+ /**
1247
+ * Canvas configuration descriptor (Pick<GPUCanvasConfiguration, 7 fields>).
1248
+ *
1249
+ * Spec anchor: W3C WebGPU §3.3 `GPUCanvasConfiguration` /
1250
+ * [@webgpu/types.GPUCanvasConfiguration].
1251
+ *
1252
+ * 7 fields (research §3.2):
1253
+ * - `device` (required): the GPUDevice for the configured context.
1254
+ * - `format` (required): one of `{'bgra8unorm', 'rgba8unorm', 'rgba16float'}`
1255
+ * (the spec normative supported context formats).
1256
+ * - `usage` (default `0x10` = RENDER_ATTACHMENT): bitmask of GPUTextureUsage
1257
+ * for the swap-chain textures.
1258
+ * - `viewFormats` (default `[]`): list of formats createView may yield;
1259
+ * **the spec sRGB-render-target idiom** uses `format='bgra8unorm'` +
1260
+ * `viewFormats=['bgra8unorm-srgb']` + `device.createTextureView` (research
1261
+ * §3.2 normative).
1262
+ * - `colorSpace` (default `'srgb'`): predefined color space for the canvas.
1263
+ * - `toneMapping` (default `{}` ≅ `{ mode: 'standard' }`): HDR tone-mapping
1264
+ * descriptor; the spec NOTE in research §3.2 says implementations
1265
+ * without tone-mapping support **omit** this from `getConfiguration()`.
1266
+ * - `alphaMode` (default `'opaque'`): canvas compositing mode.
1267
+ *
1268
+ * Field NAMES align byte-for-byte with the spec; the forgeax `?: T |
1269
+ * undefined` shape (S-7 / hard-constraint 10) lets writers omit or pass
1270
+ * `undefined` explicitly while the shim distinguishes via `'x' in src`.
1271
+ */
1272
+ export type CanvasConfiguration = ExplicitUndefined<
1273
+ Omit<
1274
+ Pick<
1275
+ GPUCanvasConfiguration,
1276
+ 'device' | 'format' | 'usage' | 'viewFormats' | 'colorSpace' | 'toneMapping' | 'alphaMode'
1277
+ >,
1278
+ 'device'
1279
+ >
1280
+ > & {
1281
+ /**
1282
+ * The forgeax RhiDevice the configured context binds to (D-S5 pattern: spec
1283
+ * `device: GPUDevice` is replaced by the forgeax brand so AI users pass the
1284
+ * device they got from `rhi.requestAdapter().requestDevice()`).
1285
+ */
1286
+ device: RhiDevice;
1287
+ };
1288
+
1289
+ declare const RhiSurfaceBrand: unique symbol;
1290
+ /**
1291
+ * RhiSurface — opaque abstraction over a canvas surface
1292
+ * (HTMLCanvasElement / OffscreenCanvas).
1293
+ *
1294
+ * Spec couples GPUCanvasContext to a canvas (research §3.1); the forgeax
1295
+ * abstraction wraps the raw GPUCanvasContext in an opaque brand. AI users
1296
+ * obtain the `RhiCanvasContext` via
1297
+ * `rhi.acquireCanvasContext(canvas)` (returns `Result<RhiCanvasContext, RhiError>`).
1298
+ *
1299
+ * Charter proposition 5 consistent abstraction: the surface brand decouples
1300
+ * AI-user code from the DOM canvas zoo (HTMLCanvasElement / OffscreenCanvas /
1301
+ * native Window).
1302
+ */
1303
+ export interface RhiSurface {
1304
+ readonly [RhiSurfaceBrand]: void;
1305
+ }
1306
+
1307
+ /**
1308
+ * RhiCanvasContext — forgeax canvas-context abstraction (M3 / K-4).
1309
+ *
1310
+ * Spec anchor: W3C WebGPU §3.3 `GPUCanvasContext` /
1311
+ * [@webgpu/types.GPUCanvasContext]. 4 methods (research §3.1) match the spec
1312
+ * names; the return types differ:
1313
+ * - `configure` returns `Result<void, RhiError>` (the spec returns void; the
1314
+ * forgeax form surfaces `webgpu-runtime-error` on format-gate / device-
1315
+ * lost paths via Result, charter proposition 4 explicit failure).
1316
+ * - `unconfigure` returns void (spec literal alignment).
1317
+ * - `getConfiguration` returns `CanvasConfiguration | undefined` (the spec
1318
+ * returns `GPUCanvasConfiguration?`; forgeax uses `undefined`).
1319
+ * - `getCurrentTexture` returns `Result<Texture, RhiError>` (K-4: Texture
1320
+ * brand, NOT TextureView; AI users go two-step
1321
+ * `device.createTextureView(canvasContext.getCurrentTexture().unwrap(), {})`).
1322
+ *
1323
+ * Lifecycle (research §3.3 [[Expire the current texture]]): currentTexture
1324
+ * **must NOT be cached across frames** — every frame must call
1325
+ * `getCurrentTexture()` afresh.
1326
+ */
1327
+ export interface RhiCanvasContext {
1328
+ /**
1329
+ * Configure the canvas context with a forgeax CanvasConfiguration.
1330
+ *
1331
+ * Returns `Result<void, RhiError>` (charter proposition 4 explicit failure).
1332
+ *
1333
+ * Failure paths (research §3.3):
1334
+ * - `format` not in supported context formats (`{'bgra8unorm',
1335
+ * 'rgba8unorm', 'rgba16float'}`) -> `'webgpu-runtime-error'` with
1336
+ * `.expected = 'one of bgra8unorm/rgba8unorm/rgba16float'`.
1337
+ * - `device` invalid | lost -> `'rhi-not-available'`.
1338
+ *
1339
+ * @example
1340
+ * const out = canvasContext.configure({
1341
+ * device,
1342
+ * format: 'bgra8unorm',
1343
+ * usage: GPUTextureUsage.RENDER_ATTACHMENT,
1344
+ * viewFormats: ['rgba8unorm-srgb'],
1345
+ * });
1346
+ * if (!out.ok) {
1347
+ * // route via switch (out.error.code)
1348
+ * }
1349
+ */
1350
+ configure(desc: CanvasConfiguration): Result<void, RhiError>;
1351
+ /**
1352
+ * Unconfigure the canvas context (spec literal void return).
1353
+ *
1354
+ * Idempotent (already-unconfigured contexts continue to be unconfigured;
1355
+ * Operation is silent).
1356
+ */
1357
+ unconfigure(): void;
1358
+ /**
1359
+ * Return the current canvas configuration, or `undefined` if the context is
1360
+ * unconfigured.
1361
+ *
1362
+ * Feature-detection entry (research §3.2 spec NOTE): when an implementation
1363
+ * does not support a configuration field (e.g. tone-mapping), the field is
1364
+ * **omitted** from the returned record (NOT defaulted) so AI users can use
1365
+ * `'toneMapping' in conf` to detect support.
1366
+ */
1367
+ getConfiguration(): CanvasConfiguration | undefined;
1368
+ /**
1369
+ * Get the current swap-chain texture (K-4: returns Texture brand, NOT
1370
+ * TextureView).
1371
+ *
1372
+ * Spec anchor: W3C WebGPU §3.3 `GPUCanvasContext.getCurrentTexture` /
1373
+ * [@webgpu/types.GPUCanvasContext.getCurrentTexture].
1374
+ *
1375
+ * Failure paths (research §3.3):
1376
+ * - context unconfigured -> `'webgpu-runtime-error'` (spec
1377
+ * `InvalidStateError` mapping).
1378
+ *
1379
+ * AI users typically pair this with `device.createTextureView` to get the
1380
+ * render-pass attachment view (charter proposition 5 consistent abstraction):
1381
+ * const tex = canvasContext.getCurrentTexture().unwrap();
1382
+ * const view = device.createTextureView(tex, {}).unwrap();
1383
+ * pass.beginRenderPass({ colorAttachments: [{ view, ... }] });
1384
+ *
1385
+ * Lifecycle (research §3.3 [[Expire the current texture]]): each frame
1386
+ * **must call this fresh**; the forgeax shim does NOT cache across frames.
1387
+ */
1388
+ getCurrentTexture(): Result<Texture, RhiError>;
1389
+ }
1390
+
1391
+ /** GPU device - sole entry point for resource creation + capability probing. */
1392
+ export interface RhiDevice {
1393
+ /** Hardware-probe layer (charter proposition 5). */
1394
+ readonly caps: RhiCaps;
1395
+ /** Enabled-features layer. */
1396
+ readonly features: RhiFeatures;
1397
+ /** Numeric-limits layer. */
1398
+ readonly limits: RhiLimits;
1399
+
1400
+ /** Create GPU buffer. */
1401
+ createBuffer(desc: BufferDescriptor): Result<Buffer, RhiError>;
1402
+ /** Create GPU texture. */
1403
+ createTexture(desc: TextureDescriptor): Result<Texture, RhiError>;
1404
+ /**
1405
+ * Destroy a GPU buffer obtained from `createBuffer`.
1406
+ *
1407
+ * Spec anchor: W3C WebGPU §gpubuffer-destroy /
1408
+ * [@webgpu/types.GPUBuffer.destroy]; wgpu wasm
1409
+ * `RhiWgpuBuffer::destroy` (research §F-1; both surfaces are idempotent
1410
+ * void at the underlying GPU).
1411
+ *
1412
+ * The forgeax form prefers fail-fast over the spec idempotent void:
1413
+ * the shim layer (rhi-webgpu / rhi-wgpu) tracks per-handle
1414
+ * `destroyed: boolean` and surfaces a second destroy as
1415
+ * `Result.err({ code: 'destroy-after-destroy' })` rather than silently
1416
+ * succeeding. Double destroy is almost always a lifecycle bug — caching
1417
+ * a stale handle, a forgotten registry slot, a race between dispose
1418
+ * paths — and surfacing it early at the call site is more useful than
1419
+ * swallowing it (plan-strategy D-7 + architecture-principles §5 Fail
1420
+ * Fast). Charter proposition 4 explicit failure.
1421
+ *
1422
+ * Failure paths:
1423
+ * - second destroy on the same handle ->
1424
+ * `Result.err({ code: 'destroy-after-destroy' })`.
1425
+ *
1426
+ * @example
1427
+ * const r = device.destroyBuffer(buf);
1428
+ * if (!r.ok) {
1429
+ * // route via switch (r.error.code)
1430
+ * }
1431
+ */
1432
+ destroyBuffer(buf: Buffer): Result<void, RhiError>;
1433
+ /**
1434
+ * Destroy a GPU query set obtained from `createQuerySet`.
1435
+ *
1436
+ * Query sets are device-owned resources even though the WebGPU surface does
1437
+ * not expose them through the Buffer/Texture families. The explicit RHI
1438
+ * seam lets Render release timestamp query sets exactly once after queue
1439
+ * completion, including failure and disposal paths.
1440
+ */
1441
+ destroyQuerySet(querySet: QuerySet): Result<void, RhiError>;
1442
+ /**
1443
+ * Destroy a GPU texture obtained from `createTexture`.
1444
+ *
1445
+ * Spec anchor: W3C WebGPU §gputexture-destroy /
1446
+ * [@webgpu/types.GPUTexture.destroy]; wgpu wasm idempotent void at the
1447
+ * underlying GPU.
1448
+ *
1449
+ * Same fail-fast contract as `destroyBuffer`: the shim layer tracks
1450
+ * per-handle `destroyed: boolean` and surfaces a second destroy as
1451
+ * `Result.err({ code: 'destroy-after-destroy' })`.
1452
+ *
1453
+ * @example
1454
+ * const r = device.destroyTexture(tex);
1455
+ * if (!r.ok) {
1456
+ * // route via switch (r.error.code)
1457
+ * }
1458
+ */
1459
+ destroyTexture(tex: Texture): Result<void, RhiError>;
1460
+ /**
1461
+ * Create a GPU texture view of an existing texture.
1462
+ *
1463
+ * Spec anchor: W3C WebGPU §texture-view-creation /
1464
+ * [@webgpu/types.GPUTexture.createView].
1465
+ *
1466
+ * Introduced in feat-20260510-rhi-resource-creation (M1). Cross-resource
1467
+ * validation is performed fast-path by the shim before forwarding to raw
1468
+ * GPUTexture.createView (research §1.1):
1469
+ * - `format` must be in `source.format ∪ source.viewFormats`; violation
1470
+ * returns Result.err({ code: 'webgpu-runtime-error' }).
1471
+ * - `usage` must be a subset of source.usage (bitmask); violation returns
1472
+ * the same code.
1473
+ *
1474
+ * @example
1475
+ * const r = device.createTextureView(tex, { format: 'rgba8unorm', dimension: '2d' });
1476
+ * if (!r.ok) {
1477
+ * // route via switch (r.error.code)
1478
+ * }
1479
+ */
1480
+ createTextureView(texture: Texture, desc: TextureViewDescriptor): Result<TextureView, RhiError>;
1481
+ /** Create sampler (spec defaults are applied by the shim). */
1482
+ createSampler(desc?: SamplerDescriptor | undefined): Result<Sampler, RhiError>;
1483
+ /** Create bind group layout. */
1484
+ createBindGroupLayout(desc: BindGroupLayoutDescriptor): Result<BindGroupLayout, RhiError>;
1485
+ /**
1486
+ * Create a bind group (instantiated layout + resource bindings).
1487
+ *
1488
+ * Spec anchor: W3C WebGPU 10 Resource binding /
1489
+ * [@webgpu/types.GPUDevice.createBindGroup].
1490
+ *
1491
+ * Introduced in feat-20260509-ecs-render-bridge-mvp (D-S1) — additive
1492
+ * extension; reuses the existing 17-member `RhiErrorCode` union
1493
+ * ('feature-not-enabled' / 'limit-exceeded' / 'webgpu-runtime-error').
1494
+ * No new error code is introduced (AGENTS.md evolution contract no-op,
1495
+ * breakage list stays empty).
1496
+ *
1497
+ * @example
1498
+ * const out = device.createBindGroup({ label: 'view-bg', layout: bgl, entries: [...] });
1499
+ * if (!out.ok) {
1500
+ * // route via switch (out.error.code)
1501
+ * }
1502
+ */
1503
+ createBindGroup(desc: BindGroupDescriptor): Result<BindGroup, RhiError>;
1504
+ /**
1505
+ * Create a pipeline layout (aggregates BindGroupLayouts).
1506
+ *
1507
+ * Spec anchor: W3C WebGPU 10.3 Pipeline layout /
1508
+ * [@webgpu/types.GPUDevice.createPipelineLayout].
1509
+ *
1510
+ * Introduced in feat-20260509-ecs-render-bridge-mvp (D-S1) — additive
1511
+ * extension; reuses the existing 17-member `RhiErrorCode` union.
1512
+ *
1513
+ * @example
1514
+ * const out = device.createPipelineLayout({ label: 'pbr-pl', bindGroupLayouts: [viewBgl, materialBgl, meshArrayBgl] });
1515
+ * if (!out.ok) {
1516
+ * // route via switch (out.error.code)
1517
+ * }
1518
+ */
1519
+ createPipelineLayout(desc: PipelineLayoutDescriptor): Result<PipelineLayout, RhiError>;
1520
+ /** Create render pipeline (synchronous path). */
1521
+ createRenderPipeline(desc: RenderPipelineDescriptor): Result<RenderPipeline, RhiError>;
1522
+ /**
1523
+ * Create a compute pipeline (synchronous path).
1524
+ *
1525
+ * Spec anchor: W3C WebGPU §compute-pipeline-creation /
1526
+ * [@webgpu/types.GPUDevice.createComputePipeline].
1527
+ *
1528
+ * Introduced in feat-20260510-rhi-resource-creation (M1). Capability gate
1529
+ * (research §1.2 + plan-strategy §4.3 boundary case row 1):
1530
+ * `caps.compute === false` -> Result.err({ code: 'feature-not-enabled' }).
1531
+ * The MVP WebGPU path always has caps.compute=true; the gate exists for
1532
+ * potential future backends that lack compute.
1533
+ *
1534
+ * @example
1535
+ * const r = device.createComputePipeline({
1536
+ * layout: 'auto',
1537
+ * compute: { module, entryPoint: 'cs_main' },
1538
+ * });
1539
+ * if (!r.ok) {
1540
+ * // route via switch (r.error.code)
1541
+ * }
1542
+ */
1543
+ createComputePipeline(desc: ComputePipelineDescriptor): Result<ComputePipeline, RhiError>;
1544
+ /**
1545
+ * Create a query set (occlusion or timestamp).
1546
+ *
1547
+ * Spec anchor: W3C WebGPU §queries / [@webgpu/types.GPUDevice.createQuerySet].
1548
+ *
1549
+ * Introduced in feat-20260510-rhi-resource-creation (M1). Hard constraints
1550
+ * (research §1.3):
1551
+ * - `count <= 4096` (spec normative); violation -> 'limit-exceeded'.
1552
+ * - `type === 'timestamp'` requires caps.timestampQuery; otherwise ->
1553
+ * 'feature-not-enabled'.
1554
+ * - `count = 0` is legal.
1555
+ *
1556
+ * @example
1557
+ * const r = device.createQuerySet({ type: 'occlusion', count: 4 });
1558
+ * if (!r.ok) {
1559
+ * // route via switch (r.error.code)
1560
+ * }
1561
+ */
1562
+ createQuerySet(desc: QuerySetDescriptor): Result<QuerySet, RhiError>;
1563
+
1564
+ /**
1565
+ * Create a command encoder.
1566
+ *
1567
+ * Spec anchor: W3C WebGPU 21.2 createCommandEncoder /
1568
+ * [@webgpu/types.GPUDevice.createCommandEncoder].
1569
+ *
1570
+ * @example
1571
+ * const encResult = device.createCommandEncoder({ label: 'frame' });
1572
+ * if (!encResult.ok) {
1573
+ * // route via switch (encResult.error.code)
1574
+ * } else {
1575
+ * const enc = encResult.value;
1576
+ * // ... record commands ...
1577
+ * }
1578
+ */
1579
+ createCommandEncoder(
1580
+ desc?: CommandEncoderDescriptor | undefined,
1581
+ ): Result<RhiCommandEncoder, RhiError>;
1582
+
1583
+ // fix-f3: synchronous createShaderModule placeholder removed - the
1584
+ // shader-compile-failed error path must go through the top-level async
1585
+ // factory `createShaderModule(device, desc)` exported from
1586
+ // `@forgeax/engine-rhi-webgpu`. A synchronous placeholder would render the
1587
+ // 'shader-compile-failed' branch unreachable in
1588
+ // `switch (err.code)` exhaustive consumers (charter proposition 5
1589
+ // consistent abstraction). See plan-strategy 7.3 error-info table
1590
+ // shader row + verify Round 1 finding F3.
1591
+
1592
+ /** Queue for command submission. */
1593
+ readonly queue: RhiQueue;
1594
+
1595
+ /**
1596
+ * Spec-style device.lost Promise (research F-4 / R2 mitigation). The engine
1597
+ * layer performs single-source subscription + dual-form fan-out without a
1598
+ * second cache. `reason` is a binary union ('destroyed' / 'unknown').
1599
+ */
1600
+ // forgeax-async-whitelist: dom-native — spec `GPUDevice.lost` Promise
1601
+ // passthrough; resolves (never rejects) per spec normative when the
1602
+ // underlying device transitions to the lost state.
1603
+ readonly lost: Promise<{ readonly reason: 'destroyed' | 'unknown'; readonly message: string }>;
1604
+ }
1605
+
1606
+ /** GPU command queue - writeBuffer / submit + M5 writeTexture /
1607
+ * copyExternalImageToTexture / onSubmittedWorkDone. */
1608
+ export interface RhiQueue {
1609
+ /**
1610
+ * Direct write to a buffer (POD + ArrayBufferView, math-free).
1611
+ *
1612
+ * For typed-array sources, dataOffset and size are element counts, matching
1613
+ * GPUQueue.writeBuffer. For ArrayBuffer and DataView sources they are byte
1614
+ * counts. Every backend and the debug tape normalize the selected source
1615
+ * bytes through resolveBufferWriteRange before validation or recording.
1616
+ */
1617
+ writeBuffer(
1618
+ buffer: Buffer,
1619
+ bufferOffset: number,
1620
+ data: ArrayBufferView | ArrayBuffer,
1621
+ dataOffset?: number | undefined,
1622
+ size?: number | undefined,
1623
+ ): Result<void, RhiError>;
1624
+ /**
1625
+ * Direct write to a texture region.
1626
+ *
1627
+ * Spec anchor: W3C WebGPU §queue-writetexture /
1628
+ * [@webgpu/types.GPUQueue.writeTexture]. Field NAMES align byte-for-byte
1629
+ * with the spec; the forgeax form returns Result<void, RhiError> instead
1630
+ * of void so AI users can route alignment failures (research §1.3 +
1631
+ * plan-strategy 2 K-2: bytesPerRow % 256 != 0 maps to
1632
+ * 'queue-write-buffer-out-of-bounds').
1633
+ *
1634
+ * @example
1635
+ * const out = device.queue.writeTexture(
1636
+ * { texture: tex, mipLevel: 0, origin: [0, 0, 0] },
1637
+ * pixels,
1638
+ * { offset: 0, bytesPerRow: 256, rowsPerImage: H },
1639
+ * { width: W, height: H, depthOrArrayLayers: 1 },
1640
+ * );
1641
+ * if (!out.ok) {
1642
+ * // route via switch (out.error.code)
1643
+ * }
1644
+ */
1645
+ writeTexture(
1646
+ destination: Pick<GPUTexelCopyTextureInfo, 'texture' | 'mipLevel' | 'origin' | 'aspect'>,
1647
+ data: ArrayBufferView | ArrayBuffer,
1648
+ dataLayout: Pick<GPUTexelCopyBufferLayout, 'offset' | 'bytesPerRow' | 'rowsPerImage'>,
1649
+ size: GPUExtent3DStrict,
1650
+ ): Result<void, RhiError>;
1651
+ /**
1652
+ * Copy an external image source (ImageBitmap / canvas / video) into a
1653
+ * GPUTexture region.
1654
+ *
1655
+ * Spec anchor: W3C WebGPU §queue-copyexternalimagetotexture /
1656
+ * [@webgpu/types.GPUQueue.copyExternalImageToTexture]. The forgeax form
1657
+ * returns Result<void, RhiError>.
1658
+ *
1659
+ * dawn-node note: dawn-node lacks HTMLCanvasElement / VideoFrame /
1660
+ * HTMLImageElement; only the ImageBitmap subset reachable from
1661
+ * createImageBitmap is exercised in dawn tests (research §7.1).
1662
+ */
1663
+ copyExternalImageToTexture(
1664
+ source: Pick<GPUCopyExternalImageSourceInfo, 'source' | 'origin' | 'flipY'>,
1665
+ destination: Pick<
1666
+ GPUCopyExternalImageDestInfo,
1667
+ 'texture' | 'mipLevel' | 'origin' | 'aspect' | 'colorSpace' | 'premultipliedAlpha'
1668
+ >,
1669
+ copySize: GPUExtent3DStrict,
1670
+ ): Result<void, RhiError>;
1671
+ /** Submit command buffers (single-use). */
1672
+ submit(commandBuffers: readonly CommandBuffer[]): Result<void, RhiError>;
1673
+ /**
1674
+ * Resolve when all currently-enqueued operations have completed.
1675
+ *
1676
+ * Spec anchor: W3C WebGPU §queue-onsubmittedworkdone /
1677
+ * [@webgpu/types.GPUQueue.onSubmittedWorkDone]. Returns
1678
+ * `Promise<undefined>` per spec normative (research §5.1: no reject path;
1679
+ * device-lost flows through `RhiDevice.lost` instead). Ordering
1680
+ * constraints (research §5.2):
1681
+ * - constraint #1 (FIFO): if p1 = q.onSubmittedWorkDone() is called
1682
+ * before p2 = q.onSubmittedWorkDone(), p1 must settle before p2.
1683
+ * - constraint #2 (mapAsync vs onSubmittedWorkDone): if p1 =
1684
+ * b.mapAsync() is called before p2 = q.onSubmittedWorkDone(), p1 must
1685
+ * settle before p2.
1686
+ *
1687
+ * @example Pattern A read-back idiom:
1688
+ * const cb = enc.finish().value;
1689
+ * queue.submit([cb]);
1690
+ * await queue.onSubmittedWorkDone();
1691
+ * await readBuf.mapAsync(GPUMapMode.READ);
1692
+ * const range = readBuf.getMappedRange().value;
1693
+ */
1694
+ // forgeax-async-whitelist: dom-native — spec `GPUQueue.onSubmittedWorkDone`
1695
+ // never rejects (research §5.1 normative); failure surfaces via
1696
+ // `RhiDevice.lost` instead.
1697
+ onSubmittedWorkDone(): Promise<undefined>;
1698
+ }
1699
+
1700
+ /** GPU command encoder - records render / compute passes + resource copies.
1701
+ *
1702
+ * Method NAMES align byte-for-byte with `@webgpu/types.GPUCommandEncoder` +
1703
+ * `GPUDebugCommandsMixin` (research F-1 / D-S4). 12 methods total:
1704
+ * - 9 direct: beginRenderPass / beginComputePass / copyBufferToBuffer /
1705
+ * copyBufferToTexture / copyTextureToBuffer / copyTextureToTexture /
1706
+ * clearBuffer / resolveQuerySet / finish
1707
+ * - 3 mixin (GPUDebugCommandsMixin): pushDebugGroup / popDebugGroup /
1708
+ * insertDebugMarker
1709
+ *
1710
+ * Lifecycle: after `finish()`, all subsequent recording calls return
1711
+ * Result.err({ code: 'command-encoder-finished' }) per D-S3 template 1
1712
+ * (where the method returns Result; void-returning methods throw the
1713
+ * structured error so AI users observe the failure consistently).
1714
+ */
1715
+ export interface RhiCommandEncoder {
1716
+ /** Begin render pass (auto-closes on end()).
1717
+ *
1718
+ * Spec anchor: [@webgpu/types.GPUCommandEncoder.beginRenderPass].
1719
+ *
1720
+ * @throws RhiError code === 'command-encoder-finished' when invoked on a finished encoder (dual-channel: spec-aligned void return throws on finished state; AI users wrap call sites with `try / catch (e: unknown) { if (e instanceof RhiError && e.code === 'command-encoder-finished') ... }`).
1721
+ * @example
1722
+ * const pass = encoder.beginRenderPass({ colorAttachments: [{ ... }] });
1723
+ */
1724
+ beginRenderPass(desc: GPURenderPassDescriptor): RhiRenderPassEncoder;
1725
+ /** Begin compute pass.
1726
+ *
1727
+ * Spec anchor: [@webgpu/types.GPUCommandEncoder.beginComputePass].
1728
+ *
1729
+ * @throws RhiError code === 'command-encoder-finished' when invoked on a finished encoder (dual-channel: see beginRenderPass for the recovery pattern).
1730
+ * @example
1731
+ * const pass = encoder.beginComputePass();
1732
+ */
1733
+ beginComputePass(desc?: ComputePassDescriptor | undefined): RhiComputePassEncoder;
1734
+ /** Copy a sub-region of a Buffer to another Buffer (5-arg full form).
1735
+ *
1736
+ * Spec anchor: [@webgpu/types.GPUCommandEncoder.copyBufferToBuffer].
1737
+ *
1738
+ * @throws RhiError code === 'command-encoder-finished' when invoked on a finished encoder (dual-channel; see beginRenderPass JSDoc for the recovery pattern). Both overloads share the same throw contract.
1739
+ * @example
1740
+ * encoder.copyBufferToBuffer(src, 0, dst, 0, 256);
1741
+ */
1742
+ copyBufferToBuffer(
1743
+ source: Buffer,
1744
+ sourceOffset: number,
1745
+ destination: Buffer,
1746
+ destinationOffset: number,
1747
+ size: number,
1748
+ ): void;
1749
+ /** Copy a Buffer to another Buffer (3-arg shorthand).
1750
+ *
1751
+ * Spec anchor: [@webgpu/types.GPUCommandEncoder.copyBufferToBuffer].
1752
+ *
1753
+ * @throws RhiError code === 'command-encoder-finished' when invoked on a finished encoder (dual-channel; see beginRenderPass JSDoc for the recovery pattern). Both overloads share the same throw contract.
1754
+ * @example
1755
+ * encoder.copyBufferToBuffer(src, dst, 256);
1756
+ */
1757
+ copyBufferToBuffer(source: Buffer, destination: Buffer, size?: number | undefined): void;
1758
+ /** Copy a Buffer sub-region to a Texture sub-region.
1759
+ *
1760
+ * Spec anchor: [@webgpu/types.GPUCommandEncoder.copyBufferToTexture].
1761
+ *
1762
+ * @throws RhiError code === 'command-encoder-finished' when invoked on a finished encoder (dual-channel; see beginRenderPass JSDoc for the recovery pattern).
1763
+ * @example
1764
+ * encoder.copyBufferToTexture(srcInfo, dstInfo, [w, h, 1]);
1765
+ */
1766
+ copyBufferToTexture(
1767
+ source: GPUTexelCopyBufferInfo,
1768
+ destination: GPUTexelCopyTextureInfo,
1769
+ copySize: GPUExtent3DStrict,
1770
+ ): void;
1771
+ /** Copy a Texture sub-region to a Buffer sub-region.
1772
+ *
1773
+ * Spec anchor: [@webgpu/types.GPUCommandEncoder.copyTextureToBuffer].
1774
+ *
1775
+ * @throws RhiError code === 'command-encoder-finished' when invoked on a finished encoder (dual-channel; see beginRenderPass JSDoc for the recovery pattern).
1776
+ * @example
1777
+ * encoder.copyTextureToBuffer(srcInfo, dstInfo, [w, h, 1]);
1778
+ */
1779
+ copyTextureToBuffer(
1780
+ source: GPUTexelCopyTextureInfo,
1781
+ destination: GPUTexelCopyBufferInfo,
1782
+ copySize: GPUExtent3DStrict,
1783
+ ): void;
1784
+ /** Copy a Texture sub-region to a Texture sub-region.
1785
+ *
1786
+ * Spec anchor: [@webgpu/types.GPUCommandEncoder.copyTextureToTexture].
1787
+ *
1788
+ * @throws RhiError code === 'command-encoder-finished' when invoked on a finished encoder (dual-channel; see beginRenderPass JSDoc for the recovery pattern).
1789
+ * @example
1790
+ * encoder.copyTextureToTexture(srcInfo, dstInfo, [w, h, 1]);
1791
+ */
1792
+ copyTextureToTexture(
1793
+ source: GPUTexelCopyTextureInfo,
1794
+ destination: GPUTexelCopyTextureInfo,
1795
+ copySize: GPUExtent3DStrict,
1796
+ ): void;
1797
+ /** Fill a Buffer sub-region with zeros.
1798
+ *
1799
+ * Spec anchor: [@webgpu/types.GPUCommandEncoder.clearBuffer].
1800
+ *
1801
+ * @throws RhiError code === 'command-encoder-finished' when invoked on a finished encoder (dual-channel; see beginRenderPass JSDoc for the recovery pattern).
1802
+ * @example
1803
+ * encoder.clearBuffer(buf, 0, 256);
1804
+ */
1805
+ clearBuffer(buffer: Buffer, offset?: number | undefined, size?: number | undefined): void;
1806
+ /** Resolve query results from a QuerySet to a Buffer.
1807
+ *
1808
+ * Real implementation (M3 / w26): writes 8-byte query results in 256-byte
1809
+ * aligned strides into `destination` starting at `destinationOffset`.
1810
+ * Validates `destination.usage & GPUBufferUsage.QUERY_RESOLVE` and the
1811
+ * 256-byte alignment up front; on misuse returns
1812
+ * `Result.err({ code: 'webgpu-runtime-error', ... })` with a structured
1813
+ * .expected / .hint pair.
1814
+ *
1815
+ * Spec anchor: [@webgpu/types.GPUCommandEncoder.resolveQuerySet].
1816
+ *
1817
+ * @example
1818
+ * const out = encoder.resolveQuerySet(qs, 0, 4, dstBuf, 0);
1819
+ * if (!out.ok) {
1820
+ * // switch (out.error.code) { case 'webgpu-runtime-error': ... }
1821
+ * }
1822
+ */
1823
+ resolveQuerySet(
1824
+ querySet: QuerySet,
1825
+ firstQuery: number,
1826
+ queryCount: number,
1827
+ destination: Buffer,
1828
+ destinationOffset: number,
1829
+ ): Result<void, RhiError>;
1830
+ /**
1831
+ * Write a u64 GPU timestamp into a timestamp QuerySet at queryIndex.
1832
+ *
1833
+ * Spec anchor: W3C WebGPU §queries / [@webgpu/types.GPUCommandEncoder].
1834
+ * dawn `TimestampOnCommandEncoder` reference (research §2.4): the entry is
1835
+ * gated on the `'timestamp-query'` device feature. K-3 decision
1836
+ * (plan-strategy §2): the forgeax form ships ONLY this CommandEncoder
1837
+ * entry; CPE/RPE inside-pass timestamp writes are deferred to
1838
+ * `feat-future-rhi-perf-timestamp-pass` (additive minor evolution).
1839
+ *
1840
+ * Return shape: `void` per spec literal alignment. When
1841
+ * `caps.timestampQuery === false` the shim fans out a structured
1842
+ * `'feature-not-enabled'` RhiError through the engine `onError` channel
1843
+ * (the forgeax form keeps the spec void return; AI users probe the gate
1844
+ * via `device.caps.timestampQuery` BEFORE calling this entry, charter
1845
+ * proposition 4 explicit failure forward-reachable).
1846
+ *
1847
+ * @throws RhiError code === 'command-encoder-finished' when invoked on a finished encoder (dual-channel; see beginRenderPass JSDoc for the recovery pattern).
1848
+ * @example
1849
+ * if (device.caps.timestampQuery) {
1850
+ * encoder.writeTimestamp(qs, 0);
1851
+ * // ... draw / dispatch ...
1852
+ * encoder.writeTimestamp(qs, 1);
1853
+ * }
1854
+ */
1855
+ writeTimestamp(querySet: QuerySet, queryIndex: number): void;
1856
+ /** Push a labelled debug group (GPUDebugCommandsMixin).
1857
+ *
1858
+ * Spec anchor: [@webgpu/types.GPUDebugCommandsMixin.pushDebugGroup].
1859
+ *
1860
+ * @note silent delegate to raw GPU encoder on finished encoder; matches W3C spec lenience (the debug-commands mixin permits a no-op pass-through after finish() so the signal stays real instead of forging a `@throws` contract; charter proposition 4).
1861
+ * @example
1862
+ * encoder.pushDebugGroup('frame-setup');
1863
+ */
1864
+ pushDebugGroup(groupLabel: string): void;
1865
+ /** Pop the most recent debug group (GPUDebugCommandsMixin).
1866
+ *
1867
+ * Spec anchor: [@webgpu/types.GPUDebugCommandsMixin.popDebugGroup].
1868
+ *
1869
+ * @note silent delegate to raw GPU encoder on finished encoder; matches W3C spec lenience (see pushDebugGroup JSDoc for the dual-channel rationale).
1870
+ * @example
1871
+ * encoder.popDebugGroup();
1872
+ */
1873
+ popDebugGroup(): void;
1874
+ /** Insert a labelled debug marker (GPUDebugCommandsMixin).
1875
+ *
1876
+ * Spec anchor: [@webgpu/types.GPUDebugCommandsMixin.insertDebugMarker].
1877
+ *
1878
+ * @note silent delegate to raw GPU encoder on finished encoder; matches W3C spec lenience (see pushDebugGroup JSDoc for the dual-channel rationale).
1879
+ * @example
1880
+ * encoder.insertDebugMarker('post-resolve');
1881
+ */
1882
+ insertDebugMarker(markerLabel: string): void;
1883
+ /** Finish recording -> CommandBuffer. After finish() any further recording
1884
+ * call returns Result.err({ code: 'command-encoder-finished' }).
1885
+ *
1886
+ * Spec anchor: [@webgpu/types.GPUCommandEncoder.finish].
1887
+ *
1888
+ * @example
1889
+ * const cb = encoder.finish();
1890
+ * if (cb.ok) device.queue.submit([cb.value]);
1891
+ */
1892
+ finish(): Result<CommandBuffer, RhiError>;
1893
+ }
1894
+
1895
+ /** GPU render pass encoder - records draw calls + state changes.
1896
+ *
1897
+ * Method NAMES align byte-for-byte with @webgpu/types.GPURenderPassEncoder +
1898
+ * GPURenderCommandsMixin + GPUBindingCommandsMixin + GPUDebugCommandsMixin
1899
+ * (research F-2 / D-S4): 17 spec stable + 1 setBindGroup overload + 1
1900
+ * remaining capability-gated placeholder (`executeBundles` returns
1901
+ * Result.err({ code: 'rhi-not-available', hint: 'see
1902
+ * feat-future-rhi-render-bundle' })). The 2 occlusion-query methods
1903
+ * (`beginOcclusionQuery` / `endOcclusionQuery`) shipped real implementations
1904
+ * in M3 (w23) backed by `RenderPassDescriptor.occlusionQuerySet`.
1905
+ *
1906
+ * `setImmediates` (PROPOSED) is intentionally NOT exposed (charter
1907
+ * proposition 4: untested features hide behind caps, not surfaces).
1908
+ *
1909
+ * Lifecycle: encoder.finish() while a pass is unfinished returns
1910
+ * Result.err({ code: 'render-pass-not-ended' }) per D-S3 template 2.
1911
+ */
1912
+ export interface RhiRenderPassEncoder {
1913
+ // ===== Existing 7 methods (Round 1 baseline) =====
1914
+ /** Set the bound render pipeline.
1915
+ *
1916
+ * Spec anchor: [@webgpu/types.GPURenderCommandsMixin.setPipeline].
1917
+ *
1918
+ * @example pass.setPipeline(pipeline);
1919
+ */
1920
+ setPipeline(pipeline: RenderPipeline): void;
1921
+ /** Set a vertex buffer.
1922
+ *
1923
+ * Spec anchor: [@webgpu/types.GPURenderCommandsMixin.setVertexBuffer].
1924
+ *
1925
+ * @example pass.setVertexBuffer(0, vbo);
1926
+ */
1927
+ setVertexBuffer(
1928
+ slot: number,
1929
+ buffer: Buffer,
1930
+ offset?: number | undefined,
1931
+ size?: number | undefined,
1932
+ ): void;
1933
+ /** Set the index buffer.
1934
+ *
1935
+ * Spec anchor: [@webgpu/types.GPURenderCommandsMixin.setIndexBuffer].
1936
+ *
1937
+ * @example pass.setIndexBuffer(ibo, 'uint32');
1938
+ */
1939
+ setIndexBuffer(
1940
+ buffer: Buffer,
1941
+ format: 'uint16' | 'uint32',
1942
+ offset?: number | undefined,
1943
+ size?: number | undefined,
1944
+ ): void;
1945
+ /** Set a bind group with optional dynamic offsets array (overload (a)).
1946
+ *
1947
+ * Spec anchor: [@webgpu/types.GPUBindingCommandsMixin.setBindGroup].
1948
+ *
1949
+ * @example pass.setBindGroup(0, bg, [0, 256]);
1950
+ */
1951
+ setBindGroup(
1952
+ index: number,
1953
+ bindGroup: BindGroup,
1954
+ dynamicOffsets?: readonly number[] | undefined,
1955
+ ): void;
1956
+ /** Set a bind group with a Uint32Array slice for dynamic offsets (overload (b)).
1957
+ *
1958
+ * Spec anchor: [@webgpu/types.GPUBindingCommandsMixin.setBindGroup].
1959
+ *
1960
+ * @example
1961
+ * pass.setBindGroup(0, bg, dynamicOffsetsData, dynamicOffsetsDataStart, dynamicOffsetsDataLength);
1962
+ */
1963
+ setBindGroup(
1964
+ index: number,
1965
+ bindGroup: BindGroup,
1966
+ dynamicOffsetsData: Uint32Array,
1967
+ dynamicOffsetsDataStart: number,
1968
+ dynamicOffsetsDataLength: number,
1969
+ ): void;
1970
+ /** Issue a draw.
1971
+ *
1972
+ * Spec anchor: [@webgpu/types.GPURenderCommandsMixin.draw].
1973
+ *
1974
+ * @example pass.draw(3);
1975
+ */
1976
+ draw(
1977
+ vertexCount: number,
1978
+ instanceCount?: number | undefined,
1979
+ firstVertex?: number | undefined,
1980
+ firstInstance?: number | undefined,
1981
+ ): void;
1982
+ /** Issue an indexed draw.
1983
+ *
1984
+ * Spec anchor: [@webgpu/types.GPURenderCommandsMixin.drawIndexed].
1985
+ *
1986
+ * @example pass.drawIndexed(36);
1987
+ */
1988
+ drawIndexed(
1989
+ indexCount: number,
1990
+ instanceCount?: number | undefined,
1991
+ firstIndex?: number | undefined,
1992
+ baseVertex?: number | undefined,
1993
+ firstInstance?: number | undefined,
1994
+ ): void;
1995
+ /** End the render pass.
1996
+ *
1997
+ * Spec anchor: [@webgpu/types.GPURenderPassEncoder.end].
1998
+ *
1999
+ * @example pass.end();
2000
+ */
2001
+ end(): void;
2002
+
2003
+ // ===== 10 new spec stable methods (D-S4) =====
2004
+ /** Set the viewport.
2005
+ *
2006
+ * Spec anchor: [@webgpu/types.GPURenderPassEncoder.setViewport].
2007
+ *
2008
+ * @example pass.setViewport(0, 0, 800, 600, 0, 1);
2009
+ */
2010
+ setViewport(x: number, y: number, w: number, h: number, minDepth: number, maxDepth: number): void;
2011
+ /** Set the scissor rect.
2012
+ *
2013
+ * Spec anchor: [@webgpu/types.GPURenderPassEncoder.setScissorRect].
2014
+ *
2015
+ * @example pass.setScissorRect(0, 0, 800, 600);
2016
+ */
2017
+ setScissorRect(x: number, y: number, w: number, h: number): void;
2018
+ /** Set the blend constant color.
2019
+ *
2020
+ * Spec anchor: [@webgpu/types.GPURenderPassEncoder.setBlendConstant].
2021
+ *
2022
+ * @example pass.setBlendConstant({ r: 1, g: 0, b: 0, a: 1 });
2023
+ */
2024
+ setBlendConstant(color: GPUColor): void;
2025
+ /** Set the stencil reference value.
2026
+ *
2027
+ * Spec anchor: [@webgpu/types.GPURenderPassEncoder.setStencilReference].
2028
+ *
2029
+ * @example pass.setStencilReference(0xff);
2030
+ */
2031
+ setStencilReference(reference: number): void;
2032
+ /** Issue an indirect draw.
2033
+ *
2034
+ * Spec anchor: [@webgpu/types.GPURenderCommandsMixin.drawIndirect].
2035
+ *
2036
+ * @example pass.drawIndirect(indirectBuf, 0);
2037
+ */
2038
+ drawIndirect(indirectBuffer: Buffer, indirectOffset: number): void;
2039
+ /** Issue an indexed indirect draw.
2040
+ *
2041
+ * Spec anchor: [@webgpu/types.GPURenderCommandsMixin.drawIndexedIndirect].
2042
+ *
2043
+ * @example pass.drawIndexedIndirect(indirectBuf, 0);
2044
+ */
2045
+ drawIndexedIndirect(indirectBuffer: Buffer, indirectOffset: number): void;
2046
+ /** Push a labelled debug group (GPUDebugCommandsMixin).
2047
+ *
2048
+ * Spec anchor: [@webgpu/types.GPUDebugCommandsMixin.pushDebugGroup].
2049
+ *
2050
+ * @note silent delegate to raw GPU encoder on finished encoder; matches W3C spec lenience (the debug-commands mixin permits a no-op pass-through after finish() so the signal stays real instead of forging a `@throws` contract; charter proposition 4).
2051
+ * @example pass.pushDebugGroup('lighting');
2052
+ */
2053
+ pushDebugGroup(groupLabel: string): void;
2054
+ /** Pop the most recent debug group (GPUDebugCommandsMixin).
2055
+ *
2056
+ * Spec anchor: [@webgpu/types.GPUDebugCommandsMixin.popDebugGroup].
2057
+ *
2058
+ * @note silent delegate to raw GPU encoder on finished encoder; matches W3C spec lenience (see pushDebugGroup JSDoc for the dual-channel rationale).
2059
+ * @example pass.popDebugGroup();
2060
+ */
2061
+ popDebugGroup(): void;
2062
+ /** Insert a labelled debug marker (GPUDebugCommandsMixin).
2063
+ *
2064
+ * Spec anchor: [@webgpu/types.GPUDebugCommandsMixin.insertDebugMarker].
2065
+ *
2066
+ * @note silent delegate to raw GPU encoder on finished encoder; matches W3C spec lenience (see pushDebugGroup JSDoc for the dual-channel rationale).
2067
+ * @example pass.insertDebugMarker('post-shadow');
2068
+ */
2069
+ insertDebugMarker(markerLabel: string): void;
2070
+
2071
+ // ===== 1 remaining placeholder + 2 real impls (M3 / w23 + w26) =====
2072
+ /** Execute render bundles. Capability-gated placeholder per D-S4: returns
2073
+ * Result.err({ code: 'rhi-not-available', hint: 'see feat-future-rhi-render-bundle' })
2074
+ * until that closure lands RenderBundle creation.
2075
+ *
2076
+ * Spec anchor: [@webgpu/types.GPURenderPassEncoder.executeBundles].
2077
+ *
2078
+ * @example
2079
+ * const out = pass.executeBundles([bundle]);
2080
+ * if (!out.ok) { ... route via switch (out.error.code) ... }
2081
+ */
2082
+ executeBundles(bundles: Iterable<unknown>): Result<void, RhiError>;
2083
+ /** Begin an occlusion query. Real implementation (M3 / w23): pairs with
2084
+ * `endOcclusionQuery()` against the `RenderPassDescriptor.occlusionQuerySet`
2085
+ * and validates the spec [[occlusion_query_active]] state machine
2086
+ * (queries cannot nest; missing occlusionQuerySet returns
2087
+ * Result.err({ code: 'webgpu-runtime-error' }) with structured
2088
+ * .expected / .hint fields).
2089
+ *
2090
+ * Spec anchor: [@webgpu/types.GPURenderPassEncoder.beginOcclusionQuery].
2091
+ *
2092
+ * @example
2093
+ * const out = pass.beginOcclusionQuery(0);
2094
+ * if (!out.ok) { ... route via switch (out.error.code) ... }
2095
+ */
2096
+ beginOcclusionQuery(queryIndex: number): Result<void, RhiError>;
2097
+ /** End an occlusion query. Real implementation (M3 / w23): finalizes the
2098
+ * matching `beginOcclusionQuery(idx)` slot; emits
2099
+ * Result.err({ code: 'render-pass-not-ended', ... }) if no active begin
2100
+ * is pending.
2101
+ *
2102
+ * Spec anchor: [@webgpu/types.GPURenderPassEncoder.endOcclusionQuery].
2103
+ *
2104
+ * @example
2105
+ * const out = pass.endOcclusionQuery();
2106
+ * if (!out.ok) { ... route via switch (out.error.code) ... }
2107
+ */
2108
+ endOcclusionQuery(): Result<void, RhiError>;
2109
+ }
2110
+
2111
+ /** GPU compute pass encoder - records dispatch calls. */
2112
+ export interface RhiComputePassEncoder {
2113
+ setPipeline(pipeline: ComputePipeline): void;
2114
+ setBindGroup(
2115
+ index: number,
2116
+ bindGroup: BindGroup,
2117
+ dynamicOffsets?: readonly number[] | undefined,
2118
+ ): void;
2119
+ dispatchWorkgroups(x: number, y?: number | undefined, z?: number | undefined): void;
2120
+ /** Dispatch dimensions read from three consecutive u32 values in an indirect buffer. */
2121
+ dispatchWorkgroupsIndirect(indirectBuffer: Buffer, indirectOffset: number): void;
2122
+ end(): void;
2123
+ }
2124
+
2125
+ /** GPU render pipeline operations - returned by RhiDevice.createRenderPipeline. */
2126
+ export interface RhiRenderPipelineOps {
2127
+ /** Get bind group layout (used for dynamic bind-group creation). */
2128
+ getBindGroupLayout(index: number): BindGroupLayout;
2129
+ }
2130
+
2131
+ /** GPU compute pipeline operations. */
2132
+ export interface RhiComputePipelineOps {
2133
+ /** Get bind group layout. */
2134
+ getBindGroupLayout(index: number): BindGroupLayout;
2135
+ }
2136
+
2137
+ // ============================================================================
2138
+ // re-export errors (charter proposition 1: single entry shows the full surface)
2139
+ // ============================================================================
2140
+
2141
+ export type {
2142
+ DrawOwnerSplit,
2143
+ LimitExceededDetail,
2144
+ Result,
2145
+ ResultErr,
2146
+ ResultOk,
2147
+ RhiAssetNotRegisteredDetail,
2148
+ RhiErrorCode,
2149
+ RhiErrorDetail,
2150
+ RhiOwnerOutOfRangeDetail,
2151
+ RhiShaderCompileDetail,
2152
+ RhiWebgpuRuntimeDetail,
2153
+ } from './errors';
2154
+ export { err, ok, RhiError, validateDrawArgs } from './errors';
2155
+
2156
+ // re-export common descriptor-related aliases for single-entry consumption.
2157
+ export type { AddressMode, CompareFunction, FilterMode, TextureFormat };
2158
+
2159
+ export interface Rgba16floatRenderTargetProbe {
2160
+ readonly format: 'rgba16float';
2161
+ readonly pixel: readonly [number, number, number, number];
2162
+ readonly timestampQuery: boolean;
2163
+ }
2164
+
2165
+ function halfToFloat(value: number): number {
2166
+ const sign = (value & 0x8000) === 0 ? 1 : -1;
2167
+ const exponent = (value >>> 10) & 0x1f;
2168
+ const mantissa = value & 0x03ff;
2169
+ if (exponent === 0) return sign * 2 ** -14 * (mantissa / 1024);
2170
+ if (exponent === 0x1f) return mantissa === 0 ? sign * Number.POSITIVE_INFINITY : Number.NaN;
2171
+ return sign * 2 ** (exponent - 15) * (1 + mantissa / 1024);
2172
+ }
2173
+
2174
+ /** Execute an rgba16float attachment clear and texture-to-buffer readback on a live device. */
2175
+ export async function probeRgba16floatRenderTarget(
2176
+ device: RhiDevice,
2177
+ ): Promise<Result<Rgba16floatRenderTargetProbe, RhiError>> {
2178
+ if (!device.caps.rgba16floatRenderable) {
2179
+ return err(
2180
+ new RhiError({
2181
+ code: 'feature-not-enabled',
2182
+ expected: 'device.caps.rgba16floatRenderable is true',
2183
+ hint: 'select a device that can render rgba16float attachments',
2184
+ }),
2185
+ );
2186
+ }
2187
+ const texture = device.createTexture({
2188
+ label: 'rgba16float-render-target-probe',
2189
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
2190
+ mipLevelCount: 1,
2191
+ sampleCount: 1,
2192
+ dimension: '2d',
2193
+ format: 'rgba16float',
2194
+ usage: 0x10 | 0x01,
2195
+ textureBindingViewDimension: undefined,
2196
+ viewFormats: [],
2197
+ });
2198
+ if (!texture.ok) return texture;
2199
+ const readback = device.createBuffer({
2200
+ label: 'rgba16float-render-target-probe-readback',
2201
+ size: 256,
2202
+ usage: 0x01 | 0x08,
2203
+ mappedAtCreation: false,
2204
+ });
2205
+ if (!readback.ok) {
2206
+ device.destroyTexture(texture.value);
2207
+ return readback;
2208
+ }
2209
+ const view = device.createTextureView(texture.value, {});
2210
+ if (!view.ok) {
2211
+ device.destroyBuffer(readback.value);
2212
+ device.destroyTexture(texture.value);
2213
+ return view;
2214
+ }
2215
+ const encoder = device.createCommandEncoder({ label: 'rgba16float-render-target-probe' });
2216
+ if (!encoder.ok) {
2217
+ device.destroyBuffer(readback.value);
2218
+ device.destroyTexture(texture.value);
2219
+ return encoder;
2220
+ }
2221
+ try {
2222
+ const pass = encoder.value.beginRenderPass({
2223
+ label: 'rgba16float-render-target-probe',
2224
+ colorAttachments: [
2225
+ {
2226
+ view: view.value as unknown as GPUTextureView,
2227
+ clearValue: { r: 0.25, g: 0.5, b: 0.75, a: 1 },
2228
+ loadOp: 'clear',
2229
+ storeOp: 'store',
2230
+ },
2231
+ ],
2232
+ });
2233
+ pass.end();
2234
+ encoder.value.copyTextureToBuffer(
2235
+ {
2236
+ texture: texture.value as unknown as GPUTexture,
2237
+ mipLevel: 0,
2238
+ origin: { x: 0, y: 0, z: 0 },
2239
+ },
2240
+ {
2241
+ buffer: readback.value as unknown as GPUBuffer,
2242
+ offset: 0,
2243
+ bytesPerRow: 256,
2244
+ rowsPerImage: 1,
2245
+ },
2246
+ { width: 1, height: 1, depthOrArrayLayers: 1 },
2247
+ );
2248
+ } catch (cause) {
2249
+ device.destroyBuffer(readback.value);
2250
+ device.destroyTexture(texture.value);
2251
+ return err(
2252
+ new RhiError({
2253
+ code: 'webgpu-runtime-error',
2254
+ expected: 'rgba16float render pass and texture-to-buffer copy encode successfully',
2255
+ hint: cause instanceof Error ? cause.message : String(cause),
2256
+ }),
2257
+ );
2258
+ }
2259
+ const command = encoder.value.finish();
2260
+ if (!command.ok) {
2261
+ device.destroyBuffer(readback.value);
2262
+ device.destroyTexture(texture.value);
2263
+ return command;
2264
+ }
2265
+ const submitted = device.queue.submit([command.value]);
2266
+ if (!submitted.ok) {
2267
+ device.destroyBuffer(readback.value);
2268
+ device.destroyTexture(texture.value);
2269
+ return submitted;
2270
+ }
2271
+ try {
2272
+ await device.queue.onSubmittedWorkDone();
2273
+ } catch (cause) {
2274
+ device.destroyBuffer(readback.value);
2275
+ device.destroyTexture(texture.value);
2276
+ return err(
2277
+ new RhiError({
2278
+ code: 'webgpu-runtime-error',
2279
+ expected: 'rgba16float probe submission completes',
2280
+ hint: cause instanceof Error ? cause.message : String(cause),
2281
+ }),
2282
+ );
2283
+ }
2284
+ const mapped = await readback.value.mapAsync(0x01);
2285
+ if (!mapped.ok) {
2286
+ device.destroyBuffer(readback.value);
2287
+ device.destroyTexture(texture.value);
2288
+ return mapped;
2289
+ }
2290
+ const range = mapped.value.getMappedRange(0, 8);
2291
+ if (!range.ok) {
2292
+ mapped.value.unmap();
2293
+ device.destroyBuffer(readback.value);
2294
+ device.destroyTexture(texture.value);
2295
+ return range;
2296
+ }
2297
+ const words = new Uint16Array(range.value.slice(0));
2298
+ const pixel = [
2299
+ halfToFloat(words[0] ?? 0),
2300
+ halfToFloat(words[1] ?? 0),
2301
+ halfToFloat(words[2] ?? 0),
2302
+ halfToFloat(words[3] ?? 0),
2303
+ ] as const;
2304
+ mapped.value.unmap();
2305
+ device.destroyBuffer(readback.value);
2306
+ device.destroyTexture(texture.value);
2307
+ return ok({ format: 'rgba16float', pixel, timestampQuery: device.caps.timestampQuery });
2308
+ }
2309
+
2310
+ // ============================================================================
2311
+ // Descriptor builder helpers (feat-20260612-point-light-shadows-urp-hdrp M0)
2312
+ // ============================================================================
2313
+ //
2314
+ // Pure functions that assemble descriptor objects for cube_array depth
2315
+ // textures, comparison samplers, and cube-face texture views. These are
2316
+ // zero-side-effect descriptor factories — the caller passes the returned
2317
+ // descriptor to `device.createTexture(desc)` etc. Separating descriptor
2318
+ // assembly from GPU calls keeps tests concise and prevents copy-paste of
2319
+ // dimension/format/usage boilerplate across dawn + browser fixtures.
2320
+
2321
+ /**
2322
+ * Build a TextureDescriptor for a cube-array depth texture suitable for
2323
+ * point-light shadow atlas (texture_depth_cube_array).
2324
+ *
2325
+ * Usage: `device.createTexture(cubeArrayDepthDescriptor(512, 4))`
2326
+ *
2327
+ * @param faceSize - width and height of each cube face in pixels (default 512)
2328
+ * @param layers - number of cube layers (= max shadow-casting point lights, default 4)
2329
+ * @param usage - texture usage flags (default RENDER_ATTACHMENT | TEXTURE_BINDING)
2330
+ */
2331
+ export function cubeArrayDepthDescriptor(
2332
+ faceSize: number = 512,
2333
+ layers: number = 4,
2334
+ usage: number = GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
2335
+ ): TextureDescriptor {
2336
+ return {
2337
+ size: { width: faceSize, height: faceSize, depthOrArrayLayers: 6 * layers },
2338
+ format: 'depth32float',
2339
+ dimension: '2d',
2340
+ usage,
2341
+ };
2342
+ }
2343
+
2344
+ /**
2345
+ * Build a SamplerDescriptor for a depth-comparison sampler used with
2346
+ * texture_depth_2d or texture_depth_cube_array.
2347
+ *
2348
+ * The returned descriptor uses clamp-to-edge addressing, linear filtering
2349
+ * (required for comparison sampling on some backends), and `compare: 'less'`.
2350
+ *
2351
+ * Usage: `device.createSampler(comparisonSamplerDescriptor())`
2352
+ */
2353
+ export function comparisonSamplerDescriptor(): SamplerDescriptor {
2354
+ return {
2355
+ addressModeU: 'clamp-to-edge',
2356
+ addressModeV: 'clamp-to-edge',
2357
+ addressModeW: 'clamp-to-edge',
2358
+ magFilter: 'linear',
2359
+ minFilter: 'linear',
2360
+ compare: 'less',
2361
+ };
2362
+ }
2363
+
2364
+ export { type BufferWriteRange, resolveBufferWriteRange } from './queue-writes';
2365
+
2366
+ /**
2367
+ * Build a TextureViewDescriptor to view a single cube layer + face as a 2D
2368
+ * depth attachment during shadow-caster rendering.
2369
+ *
2370
+ * Each shadow-caster pass renders to one face of one cube layer. This helper
2371
+ * produces the view descriptor that selects `baseArrayLayer = layerIndex * 6 + faceIndex`
2372
+ * with `arrayLayerCount = 1` and `dimension = '2d'`, which satisfies WebGPU's
2373
+ * requirement that render pass attachments are 2D views (cube views cannot be
2374
+ * bound as render targets).
2375
+ *
2376
+ * Usage:
2377
+ * ```
2378
+ * const viewDesc = cubeArrayDepthFaceView(lightIndex, faceIndex);
2379
+ * const view = device.createTextureView(atlas, viewDesc).unwrap();
2380
+ * ```
2381
+ *
2382
+ * @param layerIndex - 0-based shadow-casting light index (0..3)
2383
+ * @param faceIndex - 0-based cube face index (0..5, +X/-X/+Y/-Y/+Z/-Z per §5.3)
2384
+ */
2385
+ export function cubeArrayDepthFaceView(
2386
+ layerIndex: number,
2387
+ faceIndex: number,
2388
+ ): TextureViewDescriptor {
2389
+ return {
2390
+ format: 'depth32float',
2391
+ dimension: '2d',
2392
+ aspect: 'depth-only',
2393
+ baseArrayLayer: layerIndex * 6 + faceIndex,
2394
+ arrayLayerCount: 1,
2395
+ baseMipLevel: 0,
2396
+ mipLevelCount: 1,
2397
+ };
2398
+ }