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

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