@forgeax/engine-rhi 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +234 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/async-form.test-d.d.ts +2 -0
- package/dist/__tests__/async-form.test-d.d.ts.map +1 -0
- package/dist/__tests__/binding-resource.test-d.d.ts +2 -0
- package/dist/__tests__/binding-resource.test-d.d.ts.map +1 -0
- package/dist/__tests__/buffer-mapping.test-d.d.ts +2 -0
- package/dist/__tests__/buffer-mapping.test-d.d.ts.map +1 -0
- package/dist/__tests__/canvasContext.test-d.d.ts +2 -0
- package/dist/__tests__/canvasContext.test-d.d.ts.map +1 -0
- package/dist/__tests__/caps-three-way.unit.test.d.ts +2 -0
- package/dist/__tests__/caps-three-way.unit.test.d.ts.map +1 -0
- package/dist/__tests__/caps.test-d.d.ts +2 -0
- package/dist/__tests__/caps.test-d.d.ts.map +1 -0
- package/dist/__tests__/command-encoder-spec-alignment.test-d.d.ts +2 -0
- package/dist/__tests__/command-encoder-spec-alignment.test-d.d.ts.map +1 -0
- package/dist/__tests__/createComputePipeline.test-d.d.ts +2 -0
- package/dist/__tests__/createComputePipeline.test-d.d.ts.map +1 -0
- package/dist/__tests__/createQuerySet.test-d.d.ts +2 -0
- package/dist/__tests__/createQuerySet.test-d.d.ts.map +1 -0
- package/dist/__tests__/createTextureView.test-d.d.ts +2 -0
- package/dist/__tests__/createTextureView.test-d.d.ts.map +1 -0
- package/dist/__tests__/descriptor-mirror.test-d.d.ts +2 -0
- package/dist/__tests__/descriptor-mirror.test-d.d.ts.map +1 -0
- package/dist/__tests__/draw-validation-errors.test.d.ts +2 -0
- package/dist/__tests__/draw-validation-errors.test.d.ts.map +1 -0
- package/dist/__tests__/errors-instancing.unit.test.d.ts +2 -0
- package/dist/__tests__/errors-instancing.unit.test.d.ts.map +1 -0
- package/dist/__tests__/errors.test-d.d.ts +2 -0
- package/dist/__tests__/errors.test-d.d.ts.map +1 -0
- package/dist/__tests__/limit-exceeded-detail.test-d.d.ts +2 -0
- package/dist/__tests__/limit-exceeded-detail.test-d.d.ts.map +1 -0
- package/dist/__tests__/mapped-buffer.test-d.d.ts +2 -0
- package/dist/__tests__/mapped-buffer.test-d.d.ts.map +1 -0
- package/dist/__tests__/opaque-handle.test-d.d.ts +2 -0
- package/dist/__tests__/opaque-handle.test-d.d.ts.map +1 -0
- package/dist/__tests__/owner-out-of-range-role.test.d.ts +2 -0
- package/dist/__tests__/owner-out-of-range-role.test.d.ts.map +1 -0
- package/dist/__tests__/queue-writes.test-d.d.ts +2 -0
- package/dist/__tests__/queue-writes.test-d.d.ts.map +1 -0
- package/dist/__tests__/queue-writes.unit.test.d.ts +2 -0
- package/dist/__tests__/queue-writes.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-pass-encoder-spec-alignment.test-d.d.ts +2 -0
- package/dist/__tests__/render-pass-encoder-spec-alignment.test-d.d.ts.map +1 -0
- package/dist/__tests__/requestAdapter.test-d.d.ts +2 -0
- package/dist/__tests__/requestAdapter.test-d.d.ts.map +1 -0
- package/dist/errors.d.ts +315 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.mjs +56 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.d.ts +1749 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +256 -0
- package/dist/index.mjs.map +1 -0
- package/dist/queue-writes.d.ts +20 -0
- package/dist/queue-writes.d.ts.map +1 -0
- package/package.json +61 -0
- package/src/__tests__/async-form.test-d.ts +40 -0
- package/src/__tests__/binding-resource.test-d.ts +66 -0
- package/src/__tests__/buffer-mapping.test-d.ts +84 -0
- package/src/__tests__/canvasContext.test-d.ts +106 -0
- package/src/__tests__/caps-three-way.unit.test.ts +144 -0
- package/src/__tests__/caps.test-d.ts +83 -0
- package/src/__tests__/command-encoder-spec-alignment.test-d.ts +117 -0
- package/src/__tests__/createComputePipeline.test-d.ts +149 -0
- package/src/__tests__/createQuerySet.test-d.ts +81 -0
- package/src/__tests__/createTextureView.test-d.ts +127 -0
- package/src/__tests__/descriptor-mirror.test-d.ts +267 -0
- package/src/__tests__/draw-validation-errors.test.ts +155 -0
- package/src/__tests__/errors-instancing.unit.test.ts +167 -0
- package/src/__tests__/errors.test-d.ts +199 -0
- package/src/__tests__/limit-exceeded-detail.test-d.ts +33 -0
- package/src/__tests__/mapped-buffer.test-d.ts +65 -0
- package/src/__tests__/opaque-handle.test-d.ts +121 -0
- package/src/__tests__/owner-out-of-range-role.test.ts +84 -0
- package/src/__tests__/queue-writes.test-d.ts +107 -0
- package/src/__tests__/queue-writes.unit.test.ts +30 -0
- package/src/__tests__/render-pass-encoder-spec-alignment.test-d.ts +202 -0
- package/src/__tests__/requestAdapter.test-d.ts +123 -0
- package/src/errors.ts +504 -0
- package/src/index.ts +2398 -0
- package/src/queue-writes.ts +62 -0
|
@@ -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
|
+
});
|