@forgeax/engine-rhi 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +234 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/async-form.test-d.d.ts +2 -0
  5. package/dist/__tests__/async-form.test-d.d.ts.map +1 -0
  6. package/dist/__tests__/binding-resource.test-d.d.ts +2 -0
  7. package/dist/__tests__/binding-resource.test-d.d.ts.map +1 -0
  8. package/dist/__tests__/buffer-mapping.test-d.d.ts +2 -0
  9. package/dist/__tests__/buffer-mapping.test-d.d.ts.map +1 -0
  10. package/dist/__tests__/canvasContext.test-d.d.ts +2 -0
  11. package/dist/__tests__/canvasContext.test-d.d.ts.map +1 -0
  12. package/dist/__tests__/caps-three-way.unit.test.d.ts +2 -0
  13. package/dist/__tests__/caps-three-way.unit.test.d.ts.map +1 -0
  14. package/dist/__tests__/caps.test-d.d.ts +2 -0
  15. package/dist/__tests__/caps.test-d.d.ts.map +1 -0
  16. package/dist/__tests__/command-encoder-spec-alignment.test-d.d.ts +2 -0
  17. package/dist/__tests__/command-encoder-spec-alignment.test-d.d.ts.map +1 -0
  18. package/dist/__tests__/createComputePipeline.test-d.d.ts +2 -0
  19. package/dist/__tests__/createComputePipeline.test-d.d.ts.map +1 -0
  20. package/dist/__tests__/createQuerySet.test-d.d.ts +2 -0
  21. package/dist/__tests__/createQuerySet.test-d.d.ts.map +1 -0
  22. package/dist/__tests__/createTextureView.test-d.d.ts +2 -0
  23. package/dist/__tests__/createTextureView.test-d.d.ts.map +1 -0
  24. package/dist/__tests__/descriptor-mirror.test-d.d.ts +2 -0
  25. package/dist/__tests__/descriptor-mirror.test-d.d.ts.map +1 -0
  26. package/dist/__tests__/draw-validation-errors.test.d.ts +2 -0
  27. package/dist/__tests__/draw-validation-errors.test.d.ts.map +1 -0
  28. package/dist/__tests__/errors-instancing.unit.test.d.ts +2 -0
  29. package/dist/__tests__/errors-instancing.unit.test.d.ts.map +1 -0
  30. package/dist/__tests__/errors.test-d.d.ts +2 -0
  31. package/dist/__tests__/errors.test-d.d.ts.map +1 -0
  32. package/dist/__tests__/limit-exceeded-detail.test-d.d.ts +2 -0
  33. package/dist/__tests__/limit-exceeded-detail.test-d.d.ts.map +1 -0
  34. package/dist/__tests__/mapped-buffer.test-d.d.ts +2 -0
  35. package/dist/__tests__/mapped-buffer.test-d.d.ts.map +1 -0
  36. package/dist/__tests__/opaque-handle.test-d.d.ts +2 -0
  37. package/dist/__tests__/opaque-handle.test-d.d.ts.map +1 -0
  38. package/dist/__tests__/owner-out-of-range-role.test.d.ts +2 -0
  39. package/dist/__tests__/owner-out-of-range-role.test.d.ts.map +1 -0
  40. package/dist/__tests__/queue-writes.test-d.d.ts +2 -0
  41. package/dist/__tests__/queue-writes.test-d.d.ts.map +1 -0
  42. package/dist/__tests__/queue-writes.unit.test.d.ts +2 -0
  43. package/dist/__tests__/queue-writes.unit.test.d.ts.map +1 -0
  44. package/dist/__tests__/render-pass-encoder-spec-alignment.test-d.d.ts +2 -0
  45. package/dist/__tests__/render-pass-encoder-spec-alignment.test-d.d.ts.map +1 -0
  46. package/dist/__tests__/requestAdapter.test-d.d.ts +2 -0
  47. package/dist/__tests__/requestAdapter.test-d.d.ts.map +1 -0
  48. package/dist/errors.d.ts +315 -0
  49. package/dist/errors.d.ts.map +1 -0
  50. package/dist/errors.mjs +56 -0
  51. package/dist/errors.mjs.map +1 -0
  52. package/dist/index.d.ts +1749 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.mjs +256 -0
  55. package/dist/index.mjs.map +1 -0
  56. package/dist/queue-writes.d.ts +20 -0
  57. package/dist/queue-writes.d.ts.map +1 -0
  58. package/package.json +61 -0
  59. package/src/__tests__/async-form.test-d.ts +40 -0
  60. package/src/__tests__/binding-resource.test-d.ts +66 -0
  61. package/src/__tests__/buffer-mapping.test-d.ts +84 -0
  62. package/src/__tests__/canvasContext.test-d.ts +106 -0
  63. package/src/__tests__/caps-three-way.unit.test.ts +144 -0
  64. package/src/__tests__/caps.test-d.ts +83 -0
  65. package/src/__tests__/command-encoder-spec-alignment.test-d.ts +117 -0
  66. package/src/__tests__/createComputePipeline.test-d.ts +149 -0
  67. package/src/__tests__/createQuerySet.test-d.ts +81 -0
  68. package/src/__tests__/createTextureView.test-d.ts +127 -0
  69. package/src/__tests__/descriptor-mirror.test-d.ts +267 -0
  70. package/src/__tests__/draw-validation-errors.test.ts +155 -0
  71. package/src/__tests__/errors-instancing.unit.test.ts +167 -0
  72. package/src/__tests__/errors.test-d.ts +199 -0
  73. package/src/__tests__/limit-exceeded-detail.test-d.ts +33 -0
  74. package/src/__tests__/mapped-buffer.test-d.ts +65 -0
  75. package/src/__tests__/opaque-handle.test-d.ts +121 -0
  76. package/src/__tests__/owner-out-of-range-role.test.ts +84 -0
  77. package/src/__tests__/queue-writes.test-d.ts +107 -0
  78. package/src/__tests__/queue-writes.unit.test.ts +30 -0
  79. package/src/__tests__/render-pass-encoder-spec-alignment.test-d.ts +202 -0
  80. package/src/__tests__/requestAdapter.test-d.ts +123 -0
  81. package/src/errors.ts +504 -0
  82. package/src/index.ts +2398 -0
  83. package/src/queue-writes.ts +62 -0
@@ -0,0 +1,127 @@
1
+ // w03 - RhiDevice.createTextureView type-level contract test (TDD red).
2
+ //
3
+ // Locks M1 createTextureView surface (plan-tasks w03 / requirements IN-1):
4
+ // RhiDevice.createTextureView(texture: Texture, desc: TextureViewDescriptor)
5
+ // -> Result<TextureView, RhiError>
6
+ //
7
+ // Field shape: TextureViewDescriptor = ExplicitUndefined<
8
+ // Pick<GPUTextureViewDescriptor,
9
+ // 'format' | 'dimension' | 'usage' | 'aspect' | 'baseMipLevel' |
10
+ // 'mipLevelCount' | 'baseArrayLayer' | 'arrayLayerCount' | 'label'
11
+ // >
12
+ // >
13
+ //
14
+ // Red expected state: this file fails tsc -b until w06 ships
15
+ // (1) the `TextureViewDescriptor` type alias on `@forgeax/engine-rhi`,
16
+ // (2) the `RhiDevice.createTextureView` method signature.
17
+ // Green expected state (after w06 commit): all expectTypeOf assertions pass.
18
+ //
19
+ // Anchors: requirements §IN-1 / §AC-01 / §AC-07(a); research §1.1 9 fields
20
+ // (excl swizzle feature-gate); plan-strategy §4.2 type layer + K-10;
21
+ // AI User Charter proposition 1 progressive disclosure +
22
+ // proposition 5 consistent abstraction (single createX entry).
23
+
24
+ import { describe, expectTypeOf, it } from 'vitest';
25
+ import type {
26
+ Result,
27
+ RhiDevice,
28
+ RhiError,
29
+ Texture,
30
+ TextureView,
31
+ TextureViewDescriptor,
32
+ } from '../index';
33
+
34
+ /** Strip undefined from an optional field; bridges forgeax `?: T | undefined`
35
+ * and spec `?: T` while comparing value types. */
36
+ type ValueOf<T, K extends keyof T> = NonNullable<T[K]>;
37
+
38
+ describe('w03 type-level - TextureViewDescriptor field set === Pick<GPUTextureViewDescriptor, ...> (research §1.1 9 fields, swizzle excluded)', () => {
39
+ it('has exactly the 9 spec keys (label / format / dimension / usage / aspect / baseMipLevel / mipLevelCount / baseArrayLayer / arrayLayerCount)', () => {
40
+ type ExpectedKeys =
41
+ | 'label'
42
+ | 'format'
43
+ | 'dimension'
44
+ | 'usage'
45
+ | 'aspect'
46
+ | 'baseMipLevel'
47
+ | 'mipLevelCount'
48
+ | 'baseArrayLayer'
49
+ | 'arrayLayerCount';
50
+ expectTypeOf<keyof TextureViewDescriptor>().toEqualTypeOf<ExpectedKeys>();
51
+ });
52
+
53
+ it('format / dimension / usage value types align with spec', () => {
54
+ expectTypeOf<ValueOf<TextureViewDescriptor, 'format'>>().toEqualTypeOf<
55
+ ValueOf<GPUTextureViewDescriptor, 'format'>
56
+ >();
57
+ expectTypeOf<ValueOf<TextureViewDescriptor, 'dimension'>>().toEqualTypeOf<
58
+ ValueOf<GPUTextureViewDescriptor, 'dimension'>
59
+ >();
60
+ expectTypeOf<ValueOf<TextureViewDescriptor, 'usage'>>().toEqualTypeOf<
61
+ ValueOf<GPUTextureViewDescriptor, 'usage'>
62
+ >();
63
+ });
64
+
65
+ it('aspect / baseMipLevel / mipLevelCount / baseArrayLayer / arrayLayerCount value types align with spec', () => {
66
+ expectTypeOf<ValueOf<TextureViewDescriptor, 'aspect'>>().toEqualTypeOf<
67
+ ValueOf<GPUTextureViewDescriptor, 'aspect'>
68
+ >();
69
+ expectTypeOf<ValueOf<TextureViewDescriptor, 'baseMipLevel'>>().toEqualTypeOf<
70
+ ValueOf<GPUTextureViewDescriptor, 'baseMipLevel'>
71
+ >();
72
+ expectTypeOf<ValueOf<TextureViewDescriptor, 'mipLevelCount'>>().toEqualTypeOf<
73
+ ValueOf<GPUTextureViewDescriptor, 'mipLevelCount'>
74
+ >();
75
+ expectTypeOf<ValueOf<TextureViewDescriptor, 'baseArrayLayer'>>().toEqualTypeOf<
76
+ ValueOf<GPUTextureViewDescriptor, 'baseArrayLayer'>
77
+ >();
78
+ expectTypeOf<ValueOf<TextureViewDescriptor, 'arrayLayerCount'>>().toEqualTypeOf<
79
+ ValueOf<GPUTextureViewDescriptor, 'arrayLayerCount'>
80
+ >();
81
+ });
82
+
83
+ it('S-7 optional shape: every field uses `?: T | undefined` (omitted vs explicit-undefined both legal)', () => {
84
+ const _empty: TextureViewDescriptor = {};
85
+ const _explicit: TextureViewDescriptor = {
86
+ label: undefined,
87
+ format: undefined,
88
+ dimension: undefined,
89
+ usage: undefined,
90
+ aspect: undefined,
91
+ baseMipLevel: undefined,
92
+ mipLevelCount: undefined,
93
+ baseArrayLayer: undefined,
94
+ arrayLayerCount: undefined,
95
+ };
96
+ void _empty;
97
+ void _explicit;
98
+ });
99
+
100
+ it('swizzle is NOT exposed (research §1.1 OOS-MVP feature-gated)', () => {
101
+ type Keys = keyof TextureViewDescriptor;
102
+ type HasSwizzle = 'swizzle' extends Keys ? true : false;
103
+ expectTypeOf<HasSwizzle>().toEqualTypeOf<false>();
104
+ });
105
+ });
106
+
107
+ describe('w03 type-level - RhiDevice.createTextureView signature', () => {
108
+ it('returns Result<TextureView, RhiError>', () => {
109
+ type Sig = RhiDevice['createTextureView'];
110
+ type Ret = ReturnType<Sig>;
111
+ expectTypeOf<Ret>().toEqualTypeOf<Result<TextureView, RhiError>>();
112
+ });
113
+
114
+ it('takes (texture: Texture, desc: TextureViewDescriptor) as parameters', () => {
115
+ type Sig = RhiDevice['createTextureView'];
116
+ type Params = Parameters<Sig>;
117
+ expectTypeOf<Params>().toEqualTypeOf<[Texture, TextureViewDescriptor]>();
118
+ });
119
+ });
120
+
121
+ describe('w03 type-level - TextureView opaque handle does not expose raw GPU fields', () => {
122
+ it('TextureView is brand-only (no .gpuTextureView access)', () => {
123
+ const h = {} as TextureView;
124
+ // @ts-expect-error MVP-1.3: TextureView is opaque; gpuTextureView is not exposed.
125
+ h.gpuTextureView;
126
+ });
127
+ });
@@ -0,0 +1,267 @@
1
+ // MVP-1.1 - 5 core descriptors mirror @webgpu/types (type-level red -> green).
2
+ // Extended in feat-20260508-rhi-surface-completion w8 (D-S5) with 4 new
3
+ // descriptors: CommandEncoderDescriptor / RenderPassColorAttachment /
4
+ // RenderPassDepthStencilAttachment / RenderPassDescriptor.
5
+ //
6
+ // Mirror policy (decision S-7 + research F-3):
7
+ // - Field NAMES align byte-for-byte with spec: keyof forgeax === locked key
8
+ // list === keyof Pick<spec, ...>.
9
+ // - Field VALUE types (NonNullable) align with spec: forgeax[K] minus undefined
10
+ // === spec[K] minus undefined.
11
+ // - Optional shape: forgeax writes `?: T | undefined` (decision S-7); spec
12
+ // v0.1.69 writes `?: T` (ecosystem upstream pending migration); the
13
+ // ExplicitUndefined mapped type bridges them, allowing
14
+ // `{ label: undefined }` writes (charter proposition 4: explicit failure /
15
+ // distinguish missing vs explicit-undefined).
16
+ //
17
+ // view narrow Path X (feat-20260510-rhi-resource-creation M2 / breakage point
18
+ // #1): the `view` field on RenderPassColorAttachment /
19
+ // RenderPassDepthStencilAttachment is NARROWED to the forgeax `TextureView`
20
+ // brand (instead of the spec polymorphism `GPUTexture | GPUTextureView`).
21
+ // This breaks field-type alignment but preserves field-name alignment (R12
22
+ // lint scope). Charter proposition 5 (consistent abstraction) over
23
+ // proposition 1 (strict spec shape) so AI users land on the single
24
+ // constructable brand: `device.createTextureView(tex, {}).unwrap()`. The
25
+ // earlier D-S5 tightening to `Texture` was retired with M1 of this closure.
26
+ //
27
+ // Related: requirements AC MVP-1.1 (5 descriptors + field-name byte-for-byte) +
28
+ // AC-RSC-08 (4 new descriptors + view tightening); plan-strategy 4.3
29
+ // key tests #1; 7 propositions 4 / 5; research F-1 / F-3 / F-7;
30
+ // plan-strategy 2 D-S3 / D-S5 / D-S7.
31
+
32
+ import { describe, expectTypeOf, it } from 'vitest';
33
+ import type {
34
+ BindGroupLayoutDescriptor,
35
+ BufferDescriptor,
36
+ CommandEncoderDescriptor,
37
+ RenderPassColorAttachment,
38
+ RenderPassDepthStencilAttachment,
39
+ RenderPassDescriptor,
40
+ RenderPassTimestampWrites,
41
+ RenderPipelineDescriptor,
42
+ SamplerDescriptor,
43
+ TextureDescriptor,
44
+ TextureView,
45
+ } from '../index';
46
+
47
+ /** Strip undefined from an optional field; bridges forgeax `?: T | undefined`
48
+ * and spec `?: T` while comparing value types. */
49
+ type ValueOf<T, K extends keyof T> = NonNullable<T[K]>;
50
+
51
+ describe('MVP-1.1 - 5 descriptors mirror @webgpu/types', () => {
52
+ it('BufferDescriptor field set === Pick<GPUBufferDescriptor, ...>', () => {
53
+ type BufferKeys = 'label' | 'size' | 'usage' | 'mappedAtCreation';
54
+ expectTypeOf<keyof BufferDescriptor>().toEqualTypeOf<BufferKeys>();
55
+ expectTypeOf<ValueOf<BufferDescriptor, 'label'>>().toEqualTypeOf<
56
+ ValueOf<GPUBufferDescriptor, 'label'>
57
+ >();
58
+ expectTypeOf<ValueOf<BufferDescriptor, 'size'>>().toEqualTypeOf<
59
+ ValueOf<GPUBufferDescriptor, 'size'>
60
+ >();
61
+ expectTypeOf<ValueOf<BufferDescriptor, 'usage'>>().toEqualTypeOf<
62
+ ValueOf<GPUBufferDescriptor, 'usage'>
63
+ >();
64
+ expectTypeOf<ValueOf<BufferDescriptor, 'mappedAtCreation'>>().toEqualTypeOf<
65
+ ValueOf<GPUBufferDescriptor, 'mappedAtCreation'>
66
+ >();
67
+ // S-7: writing `{ label: undefined }` is legal under exactOptionalPropertyTypes.
68
+ const b: BufferDescriptor = { size: 16, usage: 0, label: undefined };
69
+ expectTypeOf(b).toMatchTypeOf<BufferDescriptor>();
70
+ });
71
+
72
+ it('TextureDescriptor field set === Pick<GPUTextureDescriptor, ...> (incl R8 Compatibility Mode)', () => {
73
+ type TextureKeys =
74
+ | 'label'
75
+ | 'size'
76
+ | 'mipLevelCount'
77
+ | 'sampleCount'
78
+ | 'dimension'
79
+ | 'format'
80
+ | 'usage'
81
+ | 'viewFormats'
82
+ | 'textureBindingViewDimension';
83
+ expectTypeOf<keyof TextureDescriptor>().toEqualTypeOf<TextureKeys>();
84
+ expectTypeOf<ValueOf<TextureDescriptor, 'format'>>().toEqualTypeOf<
85
+ ValueOf<GPUTextureDescriptor, 'format'>
86
+ >();
87
+ expectTypeOf<ValueOf<TextureDescriptor, 'usage'>>().toEqualTypeOf<
88
+ ValueOf<GPUTextureDescriptor, 'usage'>
89
+ >();
90
+ expectTypeOf<ValueOf<TextureDescriptor, 'textureBindingViewDimension'>>().toEqualTypeOf<
91
+ ValueOf<GPUTextureDescriptor, 'textureBindingViewDimension'>
92
+ >();
93
+ // Explicit `undefined` writes are legal (R8 Compatibility Mode follow-on).
94
+ const t: TextureDescriptor = {
95
+ size: [128, 128],
96
+ format: 'rgba8unorm',
97
+ usage: 0,
98
+ textureBindingViewDimension: undefined,
99
+ };
100
+ expectTypeOf(t).toMatchTypeOf<TextureDescriptor>();
101
+ });
102
+
103
+ it('SamplerDescriptor field set === Pick<GPUSamplerDescriptor, ...>', () => {
104
+ type SamplerKeys =
105
+ | 'label'
106
+ | 'addressModeU'
107
+ | 'addressModeV'
108
+ | 'addressModeW'
109
+ | 'magFilter'
110
+ | 'minFilter'
111
+ | 'mipmapFilter'
112
+ | 'lodMinClamp'
113
+ | 'lodMaxClamp'
114
+ | 'compare'
115
+ | 'maxAnisotropy';
116
+ expectTypeOf<keyof SamplerDescriptor>().toEqualTypeOf<SamplerKeys>();
117
+ expectTypeOf<ValueOf<SamplerDescriptor, 'compare'>>().toEqualTypeOf<
118
+ ValueOf<GPUSamplerDescriptor, 'compare'>
119
+ >();
120
+ expectTypeOf<ValueOf<SamplerDescriptor, 'magFilter'>>().toEqualTypeOf<
121
+ ValueOf<GPUSamplerDescriptor, 'magFilter'>
122
+ >();
123
+ });
124
+
125
+ it('BindGroupLayoutDescriptor field set === Pick<GPUBindGroupLayoutDescriptor, ...>', () => {
126
+ type BglKeys = 'label' | 'entries';
127
+ expectTypeOf<keyof BindGroupLayoutDescriptor>().toEqualTypeOf<BglKeys>();
128
+ expectTypeOf<ValueOf<BindGroupLayoutDescriptor, 'entries'>>().toEqualTypeOf<
129
+ ValueOf<GPUBindGroupLayoutDescriptor, 'entries'>
130
+ >();
131
+ });
132
+
133
+ it('RenderPipelineDescriptor field set === Pick<GPURenderPipelineDescriptor, ...>', () => {
134
+ type RppKeys =
135
+ | 'label'
136
+ | 'layout'
137
+ | 'vertex'
138
+ | 'primitive'
139
+ | 'depthStencil'
140
+ | 'multisample'
141
+ | 'fragment';
142
+ expectTypeOf<keyof RenderPipelineDescriptor>().toEqualTypeOf<RppKeys>();
143
+ expectTypeOf<ValueOf<RenderPipelineDescriptor, 'vertex'>>().toEqualTypeOf<
144
+ ValueOf<GPURenderPipelineDescriptor, 'vertex'>
145
+ >();
146
+ expectTypeOf<ValueOf<RenderPipelineDescriptor, 'fragment'>>().toEqualTypeOf<
147
+ ValueOf<GPURenderPipelineDescriptor, 'fragment'>
148
+ >();
149
+ expectTypeOf<ValueOf<RenderPipelineDescriptor, 'layout'>>().toEqualTypeOf<
150
+ ValueOf<GPURenderPipelineDescriptor, 'layout'>
151
+ >();
152
+ });
153
+ });
154
+
155
+ // w8: 4 new descriptors (D-S5) - field NAMES align byte-for-byte with spec;
156
+ // the `view` field TYPE is tightened to `Texture` (spec has
157
+ // `GPUTexture | GPUTextureView`).
158
+ describe('w8 - 4 new descriptors mirror @webgpu/types (D-S5)', () => {
159
+ it('CommandEncoderDescriptor field set === Pick<GPUCommandEncoderDescriptor, label>', () => {
160
+ type CmdKeys = 'label';
161
+ expectTypeOf<keyof CommandEncoderDescriptor>().toEqualTypeOf<CmdKeys>();
162
+ expectTypeOf<ValueOf<CommandEncoderDescriptor, 'label'>>().toEqualTypeOf<
163
+ ValueOf<GPUCommandEncoderDescriptor, 'label'>
164
+ >();
165
+ const desc: CommandEncoderDescriptor = { label: 'frame-encoder' };
166
+ expectTypeOf(desc).toMatchTypeOf<CommandEncoderDescriptor>();
167
+ });
168
+
169
+ it('RenderPassColorAttachment field set === Pick + view aligned to TextureView (view narrow Path X)', () => {
170
+ type AttKeys = 'view' | 'depthSlice' | 'resolveTarget' | 'clearValue' | 'loadOp' | 'storeOp';
171
+ expectTypeOf<keyof RenderPassColorAttachment>().toEqualTypeOf<AttKeys>();
172
+ // view is the forgeax TextureView handle, NOT spec polymorphism.
173
+ // (feat-20260510-rhi-resource-creation M2 view narrow Path X / breakage
174
+ // point #1: the earlier D-S5 temporary tightening to Texture was
175
+ // retired once M1 shipped RhiDevice.createTextureView.)
176
+ expectTypeOf<RenderPassColorAttachment['view']>().toEqualTypeOf<TextureView>();
177
+ // resolveTarget shares the same view narrow alignment.
178
+ expectTypeOf<
179
+ NonNullable<RenderPassColorAttachment['resolveTarget']>
180
+ >().toEqualTypeOf<TextureView>();
181
+ // Other fields keep spec types.
182
+ expectTypeOf<ValueOf<RenderPassColorAttachment, 'loadOp'>>().toEqualTypeOf<
183
+ ValueOf<GPURenderPassColorAttachment, 'loadOp'>
184
+ >();
185
+ expectTypeOf<ValueOf<RenderPassColorAttachment, 'storeOp'>>().toEqualTypeOf<
186
+ ValueOf<GPURenderPassColorAttachment, 'storeOp'>
187
+ >();
188
+ expectTypeOf<ValueOf<RenderPassColorAttachment, 'clearValue'>>().toEqualTypeOf<
189
+ ValueOf<GPURenderPassColorAttachment, 'clearValue'>
190
+ >();
191
+ });
192
+
193
+ it('RenderPassDepthStencilAttachment field set === Pick + view aligned to TextureView (view narrow Path X)', () => {
194
+ type DsKeys =
195
+ | 'view'
196
+ | 'depthClearValue'
197
+ | 'depthLoadOp'
198
+ | 'depthStoreOp'
199
+ | 'depthReadOnly'
200
+ | 'stencilClearValue'
201
+ | 'stencilLoadOp'
202
+ | 'stencilStoreOp'
203
+ | 'stencilReadOnly';
204
+ expectTypeOf<keyof RenderPassDepthStencilAttachment>().toEqualTypeOf<DsKeys>();
205
+ expectTypeOf<RenderPassDepthStencilAttachment['view']>().toEqualTypeOf<TextureView>();
206
+ expectTypeOf<ValueOf<RenderPassDepthStencilAttachment, 'depthClearValue'>>().toEqualTypeOf<
207
+ ValueOf<GPURenderPassDepthStencilAttachment, 'depthClearValue'>
208
+ >();
209
+ expectTypeOf<ValueOf<RenderPassDepthStencilAttachment, 'depthLoadOp'>>().toEqualTypeOf<
210
+ ValueOf<GPURenderPassDepthStencilAttachment, 'depthLoadOp'>
211
+ >();
212
+ });
213
+
214
+ // w40 (M5) - BufferDescriptor.mappedAtCreation mirror extension. Pairs
215
+ // with K-7 (mappedAtCreation passthrough fix) by locking the type-level
216
+ // contract: forgeax field is `boolean | undefined` (forgeax
217
+ // ExplicitUndefined<...>) and the underlying value type matches
218
+ // @webgpu/types.GPUBufferDescriptor.mappedAtCreation byte-for-byte.
219
+ // Anchors: requirements §IN-5 F-4 / AC-05; research §4.3 +
220
+ // plan-strategy §4.2 type layer + K-10.
221
+ it('BufferDescriptor.mappedAtCreation field is `boolean | undefined` and matches GPUBufferDescriptor.mappedAtCreation', () => {
222
+ // Field is part of the Pick<> set (already covered above; re-anchor here
223
+ // so a future Pick<> edit that drops the field is caught by w40 too).
224
+ type HasMappedAtCreation = 'mappedAtCreation' extends keyof BufferDescriptor ? true : false;
225
+ expectTypeOf<HasMappedAtCreation>().toEqualTypeOf<true>();
226
+ // Underlying value type aligns with the spec.
227
+ expectTypeOf<ValueOf<BufferDescriptor, 'mappedAtCreation'>>().toEqualTypeOf<
228
+ ValueOf<GPUBufferDescriptor, 'mappedAtCreation'>
229
+ >();
230
+ // ExplicitUndefined<...> shape: the forgeax form accepts `boolean |
231
+ // undefined` writes (S-7 distinguishes missing vs explicit-undefined).
232
+ const m1: BufferDescriptor = { size: 16, usage: 0, mappedAtCreation: true };
233
+ expectTypeOf(m1).toMatchTypeOf<BufferDescriptor>();
234
+ const m2: BufferDescriptor = { size: 16, usage: 0, mappedAtCreation: false };
235
+ expectTypeOf(m2).toMatchTypeOf<BufferDescriptor>();
236
+ const m3: BufferDescriptor = { size: 16, usage: 0, mappedAtCreation: undefined };
237
+ expectTypeOf(m3).toMatchTypeOf<BufferDescriptor>();
238
+ const m4: BufferDescriptor = { size: 16, usage: 0 };
239
+ expectTypeOf(m4).toMatchTypeOf<BufferDescriptor>();
240
+ // Anti-form: passing a non-boolean must NOT type-check (commented out;
241
+ // uncommenting must turn tsc red, not throw at runtime):
242
+ // const bad: BufferDescriptor = { size: 16, usage: 0, mappedAtCreation: 1 as never };
243
+ });
244
+
245
+ it('RenderPassDescriptor field set === Pick<GPURenderPassDescriptor, ...> (label inherited)', () => {
246
+ type RpKeys =
247
+ | 'label'
248
+ | 'colorAttachments'
249
+ | 'depthStencilAttachment'
250
+ | 'occlusionQuerySet'
251
+ | 'timestampWrites'
252
+ | 'maxDrawCount';
253
+ expectTypeOf<keyof RenderPassDescriptor>().toEqualTypeOf<RpKeys>();
254
+ // depthStencilAttachment uses the tightened type (not spec).
255
+ expectTypeOf<
256
+ NonNullable<RenderPassDescriptor['depthStencilAttachment']>
257
+ >().toEqualTypeOf<RenderPassDepthStencilAttachment>();
258
+ // timestampWrites uses the forgeax QuerySet brand; maxDrawCount keeps the
259
+ // spec type.
260
+ expectTypeOf<
261
+ ValueOf<RenderPassDescriptor, 'timestampWrites'>
262
+ >().toEqualTypeOf<RenderPassTimestampWrites>();
263
+ expectTypeOf<ValueOf<RenderPassDescriptor, 'maxDrawCount'>>().toEqualTypeOf<
264
+ ValueOf<GPURenderPassDescriptor, 'maxDrawCount'>
265
+ >();
266
+ });
267
+ });
@@ -0,0 +1,155 @@
1
+ // feat-20260708-composited-multi-world-rendering M3 / m3-t1 — AC-10 two new
2
+ // RhiErrorCode members + the pure draw-args validation helper.
3
+ //
4
+ // Plan-strategy 2 D-5: renderer.draw(worlds, { cameraOwner, resourceOwner }) validates its inputs at
5
+ // the entry (before any extract) and returns Result.err on:
6
+ // - empty worlds array -> 'render-system-empty-worlds'
7
+ // - owner index out of range -> 'render-system-owner-out-of-range'
8
+ // (.detail = { owner, worldCount })
9
+ //
10
+ // The two codes are NOT mutually exclusive: an empty array with an out-of-range
11
+ // owner surfaces the empty-worlds code first (entry check short-circuits before
12
+ // the owner-range check). requirements AC-10: illegal input is a structured,
13
+ // property-accessible failure (.code / .hint / .detail) — never silent.
14
+ //
15
+ // Why the validation lives in @forgeax/engine-rhi (World-free primitives, not
16
+ // @forgeax/engine-runtime): rhi owns the RhiErrorCode closed union (SSOT), and
17
+ // this package depends only on @forgeax/engine-types — it cannot import the
18
+ // runtime Renderer. `validateDrawArgs(worldCount, owner)` takes plain numbers
19
+ // (no World, no math), so it belongs with the codes it emits; createRenderer's
20
+ // draw entry calls it (D-5 "validate at the draw entry"). This keeps the code
21
+ // + validation + test in one package (architecture-principles §1 SSOT + §3
22
+ // Schema as Contract: the validator is the machine-checkable contract).
23
+ //
24
+ // AC-01 single path: only the explicit camera/resource owner form exists; there
25
+ // is no draw(world) overload. AC-02's owner-required compile-time narrowing is
26
+ // verified at the real hello-triangle callsite (m3-t2), not here.
27
+
28
+ import { describe, expect, expectTypeOf, it } from 'vitest';
29
+ import {
30
+ RhiError,
31
+ type RhiErrorCode,
32
+ type RhiErrorDetail,
33
+ type RhiOwnerOutOfRangeDetail,
34
+ validateDrawArgs,
35
+ } from '../errors';
36
+
37
+ describe('M3 / m3-t1 — two new RhiErrorCode members', () => {
38
+ it("contains 'render-system-empty-worlds' in the closed RhiErrorCode union", () => {
39
+ expectTypeOf<'render-system-empty-worlds'>().toMatchTypeOf<RhiErrorCode>();
40
+ });
41
+
42
+ it("contains 'render-system-owner-out-of-range' in the closed RhiErrorCode union", () => {
43
+ expectTypeOf<'render-system-owner-out-of-range'>().toMatchTypeOf<RhiErrorCode>();
44
+ });
45
+
46
+ it('the two new members are distinct from each other', () => {
47
+ type Empty = Extract<RhiErrorCode, 'render-system-empty-worlds'>;
48
+ type Oob = Extract<RhiErrorCode, 'render-system-owner-out-of-range'>;
49
+ expectTypeOf<Empty>().not.toEqualTypeOf<Oob>();
50
+ });
51
+
52
+ it('exhaustive switch reaches both new members without a default arm', () => {
53
+ // A narrow switch over just the two new members: tsc proves both literals
54
+ // are members of the union (otherwise the case labels would not compile);
55
+ // the runtime assertion proves the mapping is reachable.
56
+ function describeNew(
57
+ code: 'render-system-empty-worlds' | 'render-system-owner-out-of-range',
58
+ ): string {
59
+ switch (code) {
60
+ case 'render-system-empty-worlds':
61
+ return 'empty';
62
+ case 'render-system-owner-out-of-range':
63
+ return 'oob';
64
+ }
65
+ }
66
+ expect(describeNew('render-system-empty-worlds')).toBe('empty');
67
+ expect(describeNew('render-system-owner-out-of-range')).toBe('oob');
68
+ });
69
+ });
70
+
71
+ describe('M3 / m3-t1 — RhiOwnerOutOfRangeDetail shape', () => {
72
+ // feat-20260709-editor-world-partition M1 / w7: the detail grew a `role`
73
+ // discriminator when the single draw owner was split into cameraOwner +
74
+ // resourceOwner (plan-strategy §2 D-3; new SSOT contract in
75
+ // owner-out-of-range-role.test.ts). This precursor shape assertion is
76
+ // migrated to the two-index form — no new error code, only `.detail` grows.
77
+ it('detail equals { role, owner: number, worldCount: number }', () => {
78
+ expectTypeOf<RhiOwnerOutOfRangeDetail>().toEqualTypeOf<{
79
+ readonly role: 'camera' | 'resource';
80
+ readonly owner: number;
81
+ readonly worldCount: number;
82
+ }>();
83
+ });
84
+
85
+ it('detail is a member of the RhiErrorDetail discriminated union', () => {
86
+ const detail: RhiOwnerOutOfRangeDetail = { role: 'resource', owner: 2, worldCount: 1 };
87
+ const widened: RhiErrorDetail = detail;
88
+ expectTypeOf(widened).toMatchTypeOf<RhiErrorDetail>();
89
+ });
90
+ });
91
+
92
+ describe('M3 / m3-t1 — validateDrawArgs (D-5 draw entry validation)', () => {
93
+ it('empty worlds -> err render-system-empty-worlds with a non-empty hint', () => {
94
+ const r = validateDrawArgs(0, { cameraOwner: 0, resourceOwner: 0 });
95
+ expect(r.ok).toBe(false);
96
+ if (!r.ok) {
97
+ expect(r.error.code).toBe('render-system-empty-worlds');
98
+ expect(r.error.hint.length).toBeGreaterThan(0);
99
+ // AI users recover from the hint text (charter P3 actionable hint).
100
+ expect(r.error.hint).toContain('at least one world');
101
+ // empty-worlds carries no detail (the failure is fully described by .code).
102
+ expect(r.error.detail).toBeUndefined();
103
+ }
104
+ });
105
+
106
+ it('owner out of range (owner >= worldCount) -> err with detail { owner, worldCount }', () => {
107
+ const r = validateDrawArgs(1, { cameraOwner: 1, resourceOwner: 0 });
108
+ expect(r.ok).toBe(false);
109
+ if (!r.ok) {
110
+ expect(r.error.code).toBe('render-system-owner-out-of-range');
111
+ expect(r.error.hint.length).toBeGreaterThan(0);
112
+ expect(r.error.hint).toContain('0..worlds.length-1');
113
+ // .detail is reachable via property access after narrowing on .code.
114
+ const d = r.error.detail as RhiOwnerOutOfRangeDetail;
115
+ expect(d.owner).toBe(1);
116
+ expect(d.worldCount).toBe(1);
117
+ }
118
+ });
119
+
120
+ it('owner out of range (owner < 0) -> err render-system-owner-out-of-range', () => {
121
+ const r = validateDrawArgs(2, { cameraOwner: -1, resourceOwner: 0 });
122
+ expect(r.ok).toBe(false);
123
+ if (!r.ok) {
124
+ expect(r.error.code).toBe('render-system-owner-out-of-range');
125
+ const d = r.error.detail as RhiOwnerOutOfRangeDetail;
126
+ expect(d.owner).toBe(-1);
127
+ expect(d.worldCount).toBe(2);
128
+ }
129
+ });
130
+
131
+ it('two codes are non-exclusive: empty array + out-of-range owner short-circuits to empty-worlds', () => {
132
+ // Entry check order: the empty-worlds guard runs first, so an empty array
133
+ // with a nonsense owner (5) never reaches the owner-range branch (D-5).
134
+ const r = validateDrawArgs(0, { cameraOwner: 5, resourceOwner: 5 });
135
+ expect(r.ok).toBe(false);
136
+ if (!r.ok) {
137
+ expect(r.error.code).toBe('render-system-empty-worlds');
138
+ }
139
+ });
140
+
141
+ it('valid args (owner within [0, worldCount)) -> ok', () => {
142
+ expect(validateDrawArgs(1, { cameraOwner: 0, resourceOwner: 0 }).ok).toBe(true);
143
+ expect(validateDrawArgs(3, { cameraOwner: 0, resourceOwner: 0 }).ok).toBe(true);
144
+ expect(validateDrawArgs(3, { cameraOwner: 2, resourceOwner: 2 }).ok).toBe(true);
145
+ });
146
+
147
+ it('returned errors are RhiError instances (structured, not thrown strings)', () => {
148
+ const r = validateDrawArgs(1, { cameraOwner: 9, resourceOwner: 0 });
149
+ expect(r.ok).toBe(false);
150
+ if (!r.ok) {
151
+ expect(r.error).toBeInstanceOf(RhiError);
152
+ expect(r.error.expected.length).toBeGreaterThan(0);
153
+ }
154
+ });
155
+ });