@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,106 @@
|
|
|
1
|
+
// w19 - RhiSurface + RhiCanvasContext type-level contract test (TDD red).
|
|
2
|
+
//
|
|
3
|
+
// Locks M3 CanvasContext surface (plan-tasks w19 / requirements IN-4 / AC-04;
|
|
4
|
+
// plan-strategy K-4):
|
|
5
|
+
// RhiSurface — abstraction wrapping HTMLCanvasElement / OffscreenCanvas
|
|
6
|
+
// RhiCanvasContext.configure(desc): Result<void, RhiError>
|
|
7
|
+
// RhiCanvasContext.unconfigure(): void (spec literal void return)
|
|
8
|
+
// RhiCanvasContext.getConfiguration(): CanvasConfiguration | undefined
|
|
9
|
+
// RhiCanvasContext.getCurrentTexture(): Result<Texture, RhiError> (K-4)
|
|
10
|
+
//
|
|
11
|
+
// CanvasConfiguration = Pick<GPUCanvasConfiguration,
|
|
12
|
+
// 'device' | 'format' | 'usage' | 'viewFormats' | 'colorSpace' |
|
|
13
|
+
// 'toneMapping' | 'alphaMode'> (7 fields per spec §3.2)
|
|
14
|
+
//
|
|
15
|
+
// Red expected: tsc -b fails with TS2305 / TS2339 (missing RhiSurface /
|
|
16
|
+
// RhiCanvasContext / CanvasConfiguration). Turns green after w21 ships.
|
|
17
|
+
//
|
|
18
|
+
// K-4 decision: getCurrentTexture returns Result<Texture, RhiError> (NOT
|
|
19
|
+
// TextureView) — spec literal alignment; AI users go two-step:
|
|
20
|
+
// const tex = (canvasContext.getCurrentTexture()).unwrap();
|
|
21
|
+
// const view = device.createTextureView(tex, {}).unwrap();
|
|
22
|
+
//
|
|
23
|
+
// Anchors: requirements §IN-4 / §AC-04; research §3.1 4 methods + §3.2 7
|
|
24
|
+
// fields; plan-strategy §2 K-4 + §6 M3 + K-10.
|
|
25
|
+
|
|
26
|
+
import { describe, expectTypeOf, it } from 'vitest';
|
|
27
|
+
import type {
|
|
28
|
+
CanvasConfiguration,
|
|
29
|
+
Result,
|
|
30
|
+
RhiCanvasContext,
|
|
31
|
+
RhiError,
|
|
32
|
+
RhiSurface,
|
|
33
|
+
Texture,
|
|
34
|
+
} from '../index';
|
|
35
|
+
|
|
36
|
+
describe('w19 type-level - RhiSurface abstraction', () => {
|
|
37
|
+
it('RhiSurface is an abstraction over canvas surface (brand-only opaque)', () => {
|
|
38
|
+
// RhiSurface wraps HTMLCanvasElement / OffscreenCanvas; the abstraction
|
|
39
|
+
// is opaque from the AI-user perspective (spec couples context to canvas).
|
|
40
|
+
const s = {} as RhiSurface;
|
|
41
|
+
// @ts-expect-error MVP: RhiSurface is opaque; raw fields not exposed.
|
|
42
|
+
s.getContext;
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('w19 type-level - CanvasConfiguration field set === Pick<GPUCanvasConfiguration, ...> (research §3.2 7 fields)', () => {
|
|
47
|
+
it('has exactly the 7 spec keys (device / format / usage / viewFormats / colorSpace / toneMapping / alphaMode)', () => {
|
|
48
|
+
type ExpectedKeys =
|
|
49
|
+
| 'device'
|
|
50
|
+
| 'format'
|
|
51
|
+
| 'usage'
|
|
52
|
+
| 'viewFormats'
|
|
53
|
+
| 'colorSpace'
|
|
54
|
+
| 'toneMapping'
|
|
55
|
+
| 'alphaMode';
|
|
56
|
+
expectTypeOf<keyof CanvasConfiguration>().toEqualTypeOf<ExpectedKeys>();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('format is a GPUTextureFormat (forgeax form preserves spec field type)', () => {
|
|
60
|
+
type FormatField = NonNullable<CanvasConfiguration['format']>;
|
|
61
|
+
type SpecFormat = NonNullable<GPUCanvasConfiguration['format']>;
|
|
62
|
+
expectTypeOf<FormatField>().toEqualTypeOf<SpecFormat>();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('w19 type-level - RhiCanvasContext.configure signature', () => {
|
|
67
|
+
it('takes CanvasConfiguration and returns Result<void, RhiError>', () => {
|
|
68
|
+
type Sig = RhiCanvasContext['configure'];
|
|
69
|
+
type Ret = ReturnType<Sig>;
|
|
70
|
+
expectTypeOf<Ret>().toEqualTypeOf<Result<void, RhiError>>();
|
|
71
|
+
type Params = Parameters<Sig>;
|
|
72
|
+
expectTypeOf<Params>().toEqualTypeOf<[desc: CanvasConfiguration]>();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('w19 type-level - RhiCanvasContext.unconfigure signature (spec void return)', () => {
|
|
77
|
+
it('takes no arguments and returns void (spec literal alignment)', () => {
|
|
78
|
+
type Sig = RhiCanvasContext['unconfigure'];
|
|
79
|
+
type Ret = ReturnType<Sig>;
|
|
80
|
+
expectTypeOf<Ret>().toEqualTypeOf<void>();
|
|
81
|
+
type Params = Parameters<Sig>;
|
|
82
|
+
expectTypeOf<Params>().toEqualTypeOf<[]>();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('w19 type-level - RhiCanvasContext.getConfiguration signature', () => {
|
|
87
|
+
it('returns CanvasConfiguration | undefined (feature-detection entry)', () => {
|
|
88
|
+
type Sig = RhiCanvasContext['getConfiguration'];
|
|
89
|
+
type Ret = ReturnType<Sig>;
|
|
90
|
+
expectTypeOf<Ret>().toEqualTypeOf<CanvasConfiguration | undefined>();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('w19 type-level - RhiCanvasContext.getCurrentTexture signature (K-4)', () => {
|
|
95
|
+
it('returns Result<Texture, RhiError> (K-4: Texture brand, NOT TextureView)', () => {
|
|
96
|
+
type Sig = RhiCanvasContext['getCurrentTexture'];
|
|
97
|
+
type Ret = ReturnType<Sig>;
|
|
98
|
+
expectTypeOf<Ret>().toEqualTypeOf<Result<Texture, RhiError>>();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('takes no arguments', () => {
|
|
102
|
+
type Sig = RhiCanvasContext['getCurrentTexture'];
|
|
103
|
+
type Params = Parameters<Sig>;
|
|
104
|
+
expectTypeOf<Params>().toEqualTypeOf<[]>();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// caps-three-way.unit.test.ts — M4 w23 (green after w25).
|
|
2
|
+
//
|
|
3
|
+
// Verifies the three-way texture compression cap contract on RhiCaps.
|
|
4
|
+
// D-8: caps fields are derived from GPUAdapter.features Set via has(...).
|
|
5
|
+
//
|
|
6
|
+
// Scenarios:
|
|
7
|
+
// (a) bc-only → textureCompressionBc=true, etc2/astc=false
|
|
8
|
+
// (b) etc2+astc → etc2/astc true, bc false
|
|
9
|
+
// (c) no compression → three-way all false
|
|
10
|
+
// (d) rhi-null → three-way all false (AC-06)
|
|
11
|
+
|
|
12
|
+
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
13
|
+
import type { RhiCaps } from '../index';
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Type-level: RhiCaps has the three fields as readonly boolean
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
describe('RhiCaps — three-way texture compression (w25)', () => {
|
|
20
|
+
it('textureCompressionBc is readonly boolean', () => {
|
|
21
|
+
expectTypeOf<RhiCaps['textureCompressionBc']>().toEqualTypeOf<boolean>();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('textureCompressionEtc2 is readonly boolean', () => {
|
|
25
|
+
expectTypeOf<RhiCaps['textureCompressionEtc2']>().toEqualTypeOf<boolean>();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('textureCompressionAstc is readonly boolean', () => {
|
|
29
|
+
expectTypeOf<RhiCaps['textureCompressionAstc']>().toEqualTypeOf<boolean>();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('textureCompressionBc/Etc2/Astc are readonly', () => {
|
|
33
|
+
type Check = {
|
|
34
|
+
readonly textureCompressionBc: boolean;
|
|
35
|
+
readonly textureCompressionEtc2: boolean;
|
|
36
|
+
readonly textureCompressionAstc: boolean;
|
|
37
|
+
};
|
|
38
|
+
expectTypeOf<
|
|
39
|
+
Pick<RhiCaps, 'textureCompressionBc' | 'textureCompressionEtc2' | 'textureCompressionAstc'>
|
|
40
|
+
>().toEqualTypeOf<Check>();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('three fields are camelCase per D-8', () => {
|
|
44
|
+
const keys = [
|
|
45
|
+
'textureCompressionBc',
|
|
46
|
+
'textureCompressionEtc2',
|
|
47
|
+
'textureCompressionAstc',
|
|
48
|
+
] as const;
|
|
49
|
+
for (const k of keys) {
|
|
50
|
+
expect(k).toMatch(/^textureCompression[A-Z]/);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Scenario tests: mock the Derive-from-features contract
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
/** Simulates Derive: given a Set of GPUFeatureName strings, produce the three caps. */
|
|
60
|
+
function deriveThreeWayCaps(features: ReadonlySet<string>): {
|
|
61
|
+
textureCompressionBc: boolean;
|
|
62
|
+
textureCompressionEtc2: boolean;
|
|
63
|
+
textureCompressionAstc: boolean;
|
|
64
|
+
} {
|
|
65
|
+
return {
|
|
66
|
+
textureCompressionBc: features.has('texture-compression-bc'),
|
|
67
|
+
textureCompressionEtc2: features.has('texture-compression-etc2'),
|
|
68
|
+
textureCompressionAstc: features.has('texture-compression-astc'),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe('RhiCaps three-way — derive from features Set (Derive principle, D-8)', () => {
|
|
73
|
+
it('(a) bc-only: textureCompressionBc=true, etc2/astc=false', () => {
|
|
74
|
+
const caps = deriveThreeWayCaps(new Set(['texture-compression-bc']));
|
|
75
|
+
expect(caps.textureCompressionBc).toBe(true);
|
|
76
|
+
expect(caps.textureCompressionEtc2).toBe(false);
|
|
77
|
+
expect(caps.textureCompressionAstc).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('(b) etc2-only: textureCompressionEtc2=true, bc/astc=false', () => {
|
|
81
|
+
const caps = deriveThreeWayCaps(new Set(['texture-compression-etc2']));
|
|
82
|
+
expect(caps.textureCompressionBc).toBe(false);
|
|
83
|
+
expect(caps.textureCompressionEtc2).toBe(true);
|
|
84
|
+
expect(caps.textureCompressionAstc).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('(b-2) astc-only: textureCompressionAstc=true, bc/etc2=false', () => {
|
|
88
|
+
const caps = deriveThreeWayCaps(new Set(['texture-compression-astc']));
|
|
89
|
+
expect(caps.textureCompressionBc).toBe(false);
|
|
90
|
+
expect(caps.textureCompressionEtc2).toBe(false);
|
|
91
|
+
expect(caps.textureCompressionAstc).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('(c) no compression features: three-way all false', () => {
|
|
95
|
+
const caps = deriveThreeWayCaps(new Set(['timestamp-query']));
|
|
96
|
+
expect(caps.textureCompressionBc).toBe(false);
|
|
97
|
+
expect(caps.textureCompressionEtc2).toBe(false);
|
|
98
|
+
expect(caps.textureCompressionAstc).toBe(false);
|
|
99
|
+
// Also works with empty set
|
|
100
|
+
const capsEmpty = deriveThreeWayCaps(new Set());
|
|
101
|
+
expect(capsEmpty.textureCompressionBc).toBe(false);
|
|
102
|
+
expect(capsEmpty.textureCompressionEtc2).toBe(false);
|
|
103
|
+
expect(capsEmpty.textureCompressionAstc).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('(d) rhi-null contract: three-way all false (AC-06)', () => {
|
|
107
|
+
// rhi-null backend has no real GPU; its caps must truthfully report false.
|
|
108
|
+
// Verified here as the contract; actual null device verification is in w26.
|
|
109
|
+
const caps = deriveThreeWayCaps(new Set());
|
|
110
|
+
expect(caps.textureCompressionBc).toBe(false);
|
|
111
|
+
expect(caps.textureCompressionEtc2).toBe(false);
|
|
112
|
+
expect(caps.textureCompressionAstc).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('bc+etc2+astc co-exist: all three true (e.g. Apple Silicon dual HW)', () => {
|
|
116
|
+
const caps = deriveThreeWayCaps(
|
|
117
|
+
new Set(['texture-compression-bc', 'texture-compression-etc2', 'texture-compression-astc']),
|
|
118
|
+
);
|
|
119
|
+
expect(caps.textureCompressionBc).toBe(true);
|
|
120
|
+
expect(caps.textureCompressionEtc2).toBe(true);
|
|
121
|
+
expect(caps.textureCompressionAstc).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ============================================================================
|
|
126
|
+
// Breaking: old textureCompression must be absent from RhiCaps after w25
|
|
127
|
+
// ============================================================================
|
|
128
|
+
|
|
129
|
+
describe('RhiCaps — old field removed (breaking, no compat alias)', () => {
|
|
130
|
+
it('old textureCompression single boolean is no longer a key of RhiCaps', () => {
|
|
131
|
+
// Type-level: 'textureCompression' extends keyof RhiCaps? must be false.
|
|
132
|
+
type HasOldTextureCompression = 'textureCompression' extends keyof RhiCaps ? true : false;
|
|
133
|
+
expectTypeOf<HasOldTextureCompression>().toEqualTypeOf<false>();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('old name is not among the three-way keys', () => {
|
|
137
|
+
const threeWayKeys = [
|
|
138
|
+
'textureCompressionBc',
|
|
139
|
+
'textureCompressionEtc2',
|
|
140
|
+
'textureCompressionAstc',
|
|
141
|
+
] as const;
|
|
142
|
+
expect(threeWayKeys).not.toContain('textureCompression');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Type-level — RhiCaps capability layer field-set + readonly boolean shape.
|
|
2
|
+
//
|
|
3
|
+
// Baseline fields (round 1 + rhi-resource-creation + M4 three-way caps):
|
|
4
|
+
// 1) compute / 2) timestampQuery / 3) indirectDrawing
|
|
5
|
+
// 4-6) textureCompressionBc / textureCompressionEtc2 / textureCompressionAstc
|
|
6
|
+
// (M4 breaking change: single textureCompression boolean replaced with
|
|
7
|
+
// three-way per-compression-format caps, derived from adapter.features)
|
|
8
|
+
// 7) multiDrawIndirect (reserved) / 8) pushConstants (reserved) /
|
|
9
|
+
// 9) textureBindingArray (reserved)
|
|
10
|
+
//
|
|
11
|
+
// Extended in feat-20260511-rhi-spec-realign-aggressive w2 (red) ->
|
|
12
|
+
// w7 (green) per plan-strategy D-P3 + requirements AC-05 + research R-03 §3.1
|
|
13
|
+
// 4-field mapping matrix:
|
|
14
|
+
// 10) samplerAliasing — spec mandatory; both backends true
|
|
15
|
+
// 11) firstInstanceIndirect — gated by 'indirect-first-instance' feature
|
|
16
|
+
// 12) storageBuffer — limits.maxStorageBuffersPerShaderStage > 0
|
|
17
|
+
// 13) storageTexture — limits.maxStorageTexturesPerShaderStage > 0
|
|
18
|
+
//
|
|
19
|
+
// 3 reserved fields (#7/#8/#9) JSDoc wording amended in w8 from
|
|
20
|
+
// `@reserved-for-wgpu-wasm` to `@reserved-for-wgpu-native-only` (AC-06): on
|
|
21
|
+
// the browser path both rhi-webgpu and rhi-wgpu wasm always return false; only
|
|
22
|
+
// wgpu native (Tauri / native runtime) flips them.
|
|
23
|
+
//
|
|
24
|
+
// charter mapping: proposition 4 (explicit failure — caps.X = false is a
|
|
25
|
+
// signal, not an exception) + proposition 5 (consistent abstraction —
|
|
26
|
+
// browsers + native share one cap surface, differences are discoverable not
|
|
27
|
+
// hidden).
|
|
28
|
+
|
|
29
|
+
import { describe, expectTypeOf, it } from 'vitest';
|
|
30
|
+
import type { RhiCaps } from '../index';
|
|
31
|
+
|
|
32
|
+
describe('RhiCaps — field shape', () => {
|
|
33
|
+
it('contains baseline boolean fields', () => {
|
|
34
|
+
expectTypeOf<RhiCaps['compute']>().toEqualTypeOf<boolean>();
|
|
35
|
+
expectTypeOf<RhiCaps['timestampQuery']>().toEqualTypeOf<boolean>();
|
|
36
|
+
expectTypeOf<RhiCaps['indirectDrawing']>().toEqualTypeOf<boolean>();
|
|
37
|
+
expectTypeOf<RhiCaps['textureCompressionBc']>().toEqualTypeOf<boolean>();
|
|
38
|
+
expectTypeOf<RhiCaps['textureCompressionEtc2']>().toEqualTypeOf<boolean>();
|
|
39
|
+
expectTypeOf<RhiCaps['textureCompressionAstc']>().toEqualTypeOf<boolean>();
|
|
40
|
+
expectTypeOf<RhiCaps['multiDrawIndirect']>().toEqualTypeOf<boolean>();
|
|
41
|
+
expectTypeOf<RhiCaps['pushConstants']>().toEqualTypeOf<boolean>();
|
|
42
|
+
expectTypeOf<RhiCaps['textureBindingArray']>().toEqualTypeOf<boolean>();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('contains D-P3 boolean fields (samplerAliasing / firstInstanceIndirect / storageBuffer / storageTexture)', () => {
|
|
46
|
+
expectTypeOf<RhiCaps['samplerAliasing']>().toEqualTypeOf<boolean>();
|
|
47
|
+
expectTypeOf<RhiCaps['firstInstanceIndirect']>().toEqualTypeOf<boolean>();
|
|
48
|
+
expectTypeOf<RhiCaps['storageBuffer']>().toEqualTypeOf<boolean>();
|
|
49
|
+
expectTypeOf<RhiCaps['storageTexture']>().toEqualTypeOf<boolean>();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// feat-20260612-hdrp-deferred-shading-learn-render-5-8 M1 / w2:
|
|
53
|
+
// RhiCaps.maxColorAttachments field existence + type = number.
|
|
54
|
+
it('contains maxColorAttachments: number field', () => {
|
|
55
|
+
expectTypeOf<RhiCaps['maxColorAttachments']>().toEqualTypeOf<number>();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('maxColorAttachments is readonly', () => {
|
|
59
|
+
type Check = { readonly maxColorAttachments: number };
|
|
60
|
+
expectTypeOf<Pick<RhiCaps, 'maxColorAttachments'>>().toEqualTypeOf<Check>();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('all fields are readonly', () => {
|
|
64
|
+
type ReadonlyKeys = {
|
|
65
|
+
readonly compute: boolean;
|
|
66
|
+
readonly timestampQuery: boolean;
|
|
67
|
+
readonly timestampPeriodNanoseconds: number | null;
|
|
68
|
+
readonly indirectDrawing: boolean;
|
|
69
|
+
readonly textureCompressionBc: boolean;
|
|
70
|
+
readonly textureCompressionEtc2: boolean;
|
|
71
|
+
readonly textureCompressionAstc: boolean;
|
|
72
|
+
readonly multiDrawIndirect: boolean;
|
|
73
|
+
readonly pushConstants: boolean;
|
|
74
|
+
readonly textureBindingArray: boolean;
|
|
75
|
+
readonly samplerAliasing: boolean;
|
|
76
|
+
readonly firstInstanceIndirect: boolean;
|
|
77
|
+
readonly storageBuffer: boolean;
|
|
78
|
+
readonly storageTexture: boolean;
|
|
79
|
+
readonly maxColorAttachments: number;
|
|
80
|
+
};
|
|
81
|
+
expectTypeOf<RhiCaps>().toMatchTypeOf<ReadonlyKeys>();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// w2 type-level - RhiCommandEncoder spec method-set alignment with
|
|
2
|
+
// `@webgpu/types` GPUCommandEncoder (research F-1 / plan-strategy D-S4).
|
|
3
|
+
//
|
|
4
|
+
// Asserts (will be RED until w3 lands the impl):
|
|
5
|
+
// - 12 spec methods (9 direct + 3 GPUDebugCommandsMixin) exist on RhiCommandEncoder
|
|
6
|
+
// - `copyBufferToBuffer` exposes both spec overloads (3-arg shorthand + 5-arg full form)
|
|
7
|
+
// - `finish()` returns Result<CommandBuffer, RhiError>
|
|
8
|
+
// - method NAMES align byte-for-byte with `@webgpu/types` GPUCommandEncoder
|
|
9
|
+
//
|
|
10
|
+
// Charter mapping: proposition 1 (progressive disclosure) + proposition 3
|
|
11
|
+
// (machine-readable union > prose) + proposition 5 (consistent abstraction).
|
|
12
|
+
|
|
13
|
+
import { describe, expectTypeOf, it } from 'vitest';
|
|
14
|
+
import type {
|
|
15
|
+
Buffer,
|
|
16
|
+
CommandBuffer,
|
|
17
|
+
QuerySet,
|
|
18
|
+
Result,
|
|
19
|
+
RhiCommandEncoder,
|
|
20
|
+
RhiError,
|
|
21
|
+
} from '../index';
|
|
22
|
+
|
|
23
|
+
describe('w2 - RhiCommandEncoder spec method set (12 methods)', () => {
|
|
24
|
+
it('exposes beginRenderPass(desc): RhiRenderPassEncoder', () => {
|
|
25
|
+
expectTypeOf<RhiCommandEncoder['beginRenderPass']>().toBeFunction();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('exposes beginComputePass(desc?): RhiComputePassEncoder', () => {
|
|
29
|
+
expectTypeOf<RhiCommandEncoder['beginComputePass']>().toBeFunction();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('exposes copyBufferToBuffer (5-arg full form per spec)', () => {
|
|
33
|
+
// 5-arg overload must be callable (research F-1 finding):
|
|
34
|
+
// copyBufferToBuffer(src, srcOffset, dst, dstOffset, size): void
|
|
35
|
+
type FullForm = (
|
|
36
|
+
source: Buffer,
|
|
37
|
+
sourceOffset: number,
|
|
38
|
+
destination: Buffer,
|
|
39
|
+
destinationOffset: number,
|
|
40
|
+
size: number,
|
|
41
|
+
) => void;
|
|
42
|
+
type CallSignature5 = RhiCommandEncoder['copyBufferToBuffer'] extends FullForm ? true : false;
|
|
43
|
+
expectTypeOf<CallSignature5>().toEqualTypeOf<true>();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('exposes copyBufferToBuffer (3-arg shorthand overload per spec)', () => {
|
|
47
|
+
// 3-arg overload (size optional):
|
|
48
|
+
// copyBufferToBuffer(src, dst, size?): void
|
|
49
|
+
type Shorthand = (source: Buffer, destination: Buffer, size?: number | undefined) => void;
|
|
50
|
+
type CallSignature3 = RhiCommandEncoder['copyBufferToBuffer'] extends Shorthand ? true : false;
|
|
51
|
+
expectTypeOf<CallSignature3>().toEqualTypeOf<true>();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('exposes copyBufferToTexture(source, destination, copySize)', () => {
|
|
55
|
+
expectTypeOf<RhiCommandEncoder['copyBufferToTexture']>().toBeFunction();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('exposes copyTextureToBuffer(source, destination, copySize)', () => {
|
|
59
|
+
expectTypeOf<RhiCommandEncoder['copyTextureToBuffer']>().toBeFunction();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('exposes copyTextureToTexture(source, destination, copySize)', () => {
|
|
63
|
+
expectTypeOf<RhiCommandEncoder['copyTextureToTexture']>().toBeFunction();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('exposes clearBuffer(buffer, offset?, size?)', () => {
|
|
67
|
+
type Method = RhiCommandEncoder['clearBuffer'];
|
|
68
|
+
expectTypeOf<Method>().toBeFunction();
|
|
69
|
+
type Expected = (
|
|
70
|
+
buffer: Buffer,
|
|
71
|
+
offset?: number | undefined,
|
|
72
|
+
size?: number | undefined,
|
|
73
|
+
) => void;
|
|
74
|
+
expectTypeOf<Expected>().toMatchTypeOf<Method>();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('exposes resolveQuerySet (capability-gated placeholder per D-S4)', () => {
|
|
78
|
+
type Method = RhiCommandEncoder['resolveQuerySet'];
|
|
79
|
+
expectTypeOf<Method>().toBeFunction();
|
|
80
|
+
type Expected = (
|
|
81
|
+
querySet: QuerySet,
|
|
82
|
+
firstQuery: number,
|
|
83
|
+
queryCount: number,
|
|
84
|
+
destination: Buffer,
|
|
85
|
+
destinationOffset: number,
|
|
86
|
+
) => Result<void, RhiError>;
|
|
87
|
+
expectTypeOf<Expected>().toMatchTypeOf<Method>();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('exposes pushDebugGroup(label) (GPUDebugCommandsMixin)', () => {
|
|
91
|
+
expectTypeOf<RhiCommandEncoder['pushDebugGroup']>().toBeFunction();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('exposes popDebugGroup() (GPUDebugCommandsMixin)', () => {
|
|
95
|
+
expectTypeOf<RhiCommandEncoder['popDebugGroup']>().toBeFunction();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('exposes insertDebugMarker(label) (GPUDebugCommandsMixin)', () => {
|
|
99
|
+
expectTypeOf<RhiCommandEncoder['insertDebugMarker']>().toBeFunction();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('finish() returns Result<CommandBuffer, RhiError>', () => {
|
|
103
|
+
expectTypeOf<RhiCommandEncoder['finish']>().returns.toEqualTypeOf<
|
|
104
|
+
Result<CommandBuffer, RhiError>
|
|
105
|
+
>();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// w38 (M5 / K-3): writeTimestamp on the CommandEncoder entry only.
|
|
109
|
+
// CPE/RPE inside-pass timestamp writes are deferred to
|
|
110
|
+
// feat-future-rhi-perf-timestamp-pass per K-3 decision.
|
|
111
|
+
it('exposes writeTimestamp(querySet, queryIndex): void (K-3 CommandEncoder-only)', () => {
|
|
112
|
+
type Method = RhiCommandEncoder['writeTimestamp'];
|
|
113
|
+
expectTypeOf<Method>().toBeFunction();
|
|
114
|
+
type Expected = (querySet: QuerySet, queryIndex: number) => void;
|
|
115
|
+
expectTypeOf<Expected>().toMatchTypeOf<Method>();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// w07 - RhiDevice.createComputePipeline type-level contract test (TDD red).
|
|
2
|
+
//
|
|
3
|
+
// Locks M1 createComputePipeline surface (plan-tasks w07 / requirements IN-1):
|
|
4
|
+
// RhiDevice.createComputePipeline(desc: ComputePipelineDescriptor)
|
|
5
|
+
// -> Result<ComputePipeline, RhiError>
|
|
6
|
+
//
|
|
7
|
+
// Field shape (research §1.2 IDL):
|
|
8
|
+
// ComputePipelineDescriptor = ExplicitUndefined<
|
|
9
|
+
// Pick<GPUComputePipelineDescriptor, 'label' | 'layout' | 'compute'>
|
|
10
|
+
// > with the spec polymorphism `(GPUAutoLayoutMode or GPUPipelineLayout)`
|
|
11
|
+
// tightened to the forgeax union `'auto' | PipelineLayout` (D-S5 pattern),
|
|
12
|
+
// and `compute.module` tightened to the forgeax `ShaderModule` opaque handle.
|
|
13
|
+
//
|
|
14
|
+
// Red expected: tsc -b fails with TS2724 (missing ComputePipelineDescriptor)
|
|
15
|
+
// + TS2339 (createComputePipeline missing on RhiDevice). Turns green after
|
|
16
|
+
// w09 ships interface + shim.
|
|
17
|
+
//
|
|
18
|
+
// Anchors: requirements §IN-1 / §AC-01; research §1.2 IDL + 'auto' vs
|
|
19
|
+
// explicit PipelineLayout double form; plan-strategy §4.2 + K-10.
|
|
20
|
+
|
|
21
|
+
import { describe, expectTypeOf, it } from 'vitest';
|
|
22
|
+
import type {
|
|
23
|
+
ComputePipeline,
|
|
24
|
+
ComputePipelineDescriptor,
|
|
25
|
+
PipelineLayout,
|
|
26
|
+
Result,
|
|
27
|
+
RhiDevice,
|
|
28
|
+
RhiError,
|
|
29
|
+
ShaderModule,
|
|
30
|
+
} from '../index';
|
|
31
|
+
|
|
32
|
+
/** Strip undefined from an optional field; bridges forgeax `?: T | undefined`
|
|
33
|
+
* and spec `?: T` while comparing value types. */
|
|
34
|
+
type ValueOf<T, K extends keyof T> = NonNullable<T[K]>;
|
|
35
|
+
|
|
36
|
+
describe('w07 type-level - ComputePipelineDescriptor field set === Pick<GPUComputePipelineDescriptor, ...>', () => {
|
|
37
|
+
it('has exactly the keys label / layout / compute', () => {
|
|
38
|
+
type ExpectedKeys = 'label' | 'layout' | 'compute';
|
|
39
|
+
expectTypeOf<keyof ComputePipelineDescriptor>().toEqualTypeOf<ExpectedKeys>();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('label field type aligns with spec (string | undefined)', () => {
|
|
43
|
+
type LabelForgeaX = NonNullable<ComputePipelineDescriptor['label']>;
|
|
44
|
+
type LabelSpec = NonNullable<GPUComputePipelineDescriptor['label']>;
|
|
45
|
+
expectTypeOf<LabelForgeaX>().toEqualTypeOf<LabelSpec>();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("w07 type-level - layout union 'auto' | PipelineLayout", () => {
|
|
50
|
+
it("layout: 'auto' literal is a legal assignment", () => {
|
|
51
|
+
const psm = {} as ShaderModule;
|
|
52
|
+
const _auto: ComputePipelineDescriptor = {
|
|
53
|
+
layout: 'auto',
|
|
54
|
+
compute: { module: psm },
|
|
55
|
+
};
|
|
56
|
+
void _auto;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('layout: PipelineLayout brand is a legal assignment', () => {
|
|
60
|
+
const psm = {} as ShaderModule;
|
|
61
|
+
const pl = {} as PipelineLayout;
|
|
62
|
+
const _explicit: ComputePipelineDescriptor = {
|
|
63
|
+
layout: pl,
|
|
64
|
+
compute: { module: psm },
|
|
65
|
+
};
|
|
66
|
+
void _explicit;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("layout type is exactly 'auto' | PipelineLayout (no spec polymorphism leak)", () => {
|
|
70
|
+
type Layout = ComputePipelineDescriptor['layout'];
|
|
71
|
+
expectTypeOf<Layout>().toEqualTypeOf<'auto' | PipelineLayout>();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('w07 type-level - compute nested dictionary mirrors GPUProgrammableStage', () => {
|
|
76
|
+
it('compute.module is the forgeax ShaderModule opaque handle (not GPUShaderModule)', () => {
|
|
77
|
+
type ModuleForgeaX = ComputePipelineDescriptor['compute']['module'];
|
|
78
|
+
expectTypeOf<ModuleForgeaX>().toEqualTypeOf<ShaderModule>();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('compute.entryPoint is optional string | undefined', () => {
|
|
82
|
+
const psm = {} as ShaderModule;
|
|
83
|
+
const _withEntry: ComputePipelineDescriptor = {
|
|
84
|
+
layout: 'auto',
|
|
85
|
+
compute: { module: psm, entryPoint: 'cs_main' },
|
|
86
|
+
};
|
|
87
|
+
const _withoutEntry: ComputePipelineDescriptor = {
|
|
88
|
+
layout: 'auto',
|
|
89
|
+
compute: { module: psm },
|
|
90
|
+
};
|
|
91
|
+
const _explicitUndef: ComputePipelineDescriptor = {
|
|
92
|
+
layout: 'auto',
|
|
93
|
+
compute: { module: psm, entryPoint: undefined },
|
|
94
|
+
};
|
|
95
|
+
void _withEntry;
|
|
96
|
+
void _withoutEntry;
|
|
97
|
+
void _explicitUndef;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('compute.constants is optional Record<string, number>', () => {
|
|
101
|
+
const psm = {} as ShaderModule;
|
|
102
|
+
const _withConsts: ComputePipelineDescriptor = {
|
|
103
|
+
layout: 'auto',
|
|
104
|
+
compute: { module: psm, constants: { foo: 1.0, bar: 2.5 } },
|
|
105
|
+
};
|
|
106
|
+
void _withConsts;
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('w07 type-level - RhiDevice.createComputePipeline signature', () => {
|
|
111
|
+
it('returns Result<ComputePipeline, RhiError>', () => {
|
|
112
|
+
type Sig = RhiDevice['createComputePipeline'];
|
|
113
|
+
type Ret = ReturnType<Sig>;
|
|
114
|
+
expectTypeOf<Ret>().toEqualTypeOf<Result<ComputePipeline, RhiError>>();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('takes a ComputePipelineDescriptor as the sole parameter', () => {
|
|
118
|
+
type Sig = RhiDevice['createComputePipeline'];
|
|
119
|
+
type Params = Parameters<Sig>;
|
|
120
|
+
expectTypeOf<Params>().toEqualTypeOf<[ComputePipelineDescriptor]>();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('w07 type-level - ComputePipeline opaque handle does not expose raw GPU fields', () => {
|
|
125
|
+
it('ComputePipeline is brand-only (no .gpuComputePipeline access)', () => {
|
|
126
|
+
const h = {} as ComputePipeline;
|
|
127
|
+
// @ts-expect-error MVP-1.3: ComputePipeline is opaque; raw fields not exposed.
|
|
128
|
+
h.gpuComputePipeline;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('S-7 optional shape: label uses `?: T | undefined`', () => {
|
|
132
|
+
const psm = {} as ShaderModule;
|
|
133
|
+
const _omitted: ComputePipelineDescriptor = {
|
|
134
|
+
layout: 'auto',
|
|
135
|
+
compute: { module: psm },
|
|
136
|
+
};
|
|
137
|
+
const _explicit: ComputePipelineDescriptor = {
|
|
138
|
+
label: undefined,
|
|
139
|
+
layout: 'auto',
|
|
140
|
+
compute: { module: psm },
|
|
141
|
+
};
|
|
142
|
+
void _omitted;
|
|
143
|
+
void _explicit;
|
|
144
|
+
type LabelType = ComputePipelineDescriptor['label'];
|
|
145
|
+
expectTypeOf<NonNullable<LabelType>>().toEqualTypeOf<
|
|
146
|
+
ValueOf<GPUComputePipelineDescriptor, 'label'>
|
|
147
|
+
>();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// w10 - RhiDevice.createQuerySet type-level contract test (TDD red).
|
|
2
|
+
//
|
|
3
|
+
// Locks M1 createQuerySet surface (plan-tasks w10 / requirements IN-1):
|
|
4
|
+
// RhiDevice.createQuerySet(desc: QuerySetDescriptor)
|
|
5
|
+
// -> Result<QuerySet, RhiError>
|
|
6
|
+
//
|
|
7
|
+
// Field shape (research §1.3 IDL):
|
|
8
|
+
// QuerySetDescriptor = ExplicitUndefined<
|
|
9
|
+
// Pick<GPUQuerySetDescriptor, 'label' | 'type' | 'count'>
|
|
10
|
+
// >
|
|
11
|
+
// type: 'occlusion' | 'timestamp' (closed union, GPUQueryType).
|
|
12
|
+
//
|
|
13
|
+
// Red expected: tsc -b fails with TS2305 (missing QuerySetDescriptor) +
|
|
14
|
+
// TS2339 (createQuerySet missing on RhiDevice). Turns green after w12 ships.
|
|
15
|
+
//
|
|
16
|
+
// Anchors: requirements §IN-1 / §AC-01; research §1.3 IDL + Pick set;
|
|
17
|
+
// plan-strategy §4.2 + K-10.
|
|
18
|
+
|
|
19
|
+
import { describe, expectTypeOf, it } from 'vitest';
|
|
20
|
+
import type { QuerySet, QuerySetDescriptor, Result, RhiDevice, RhiError } from '../index';
|
|
21
|
+
|
|
22
|
+
/** Strip undefined from an optional field; bridges forgeax `?: T | undefined`
|
|
23
|
+
* and spec `?: T` while comparing value types. */
|
|
24
|
+
type ValueOf<T, K extends keyof T> = NonNullable<T[K]>;
|
|
25
|
+
|
|
26
|
+
describe('w10 type-level - QuerySetDescriptor field set === Pick<GPUQuerySetDescriptor, ...>', () => {
|
|
27
|
+
it('has exactly the keys label / type / count', () => {
|
|
28
|
+
type ExpectedKeys = 'label' | 'type' | 'count';
|
|
29
|
+
expectTypeOf<keyof QuerySetDescriptor>().toEqualTypeOf<ExpectedKeys>();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('label field type aligns with spec (string | undefined)', () => {
|
|
33
|
+
type LabelForgeaX = NonNullable<QuerySetDescriptor['label']>;
|
|
34
|
+
type LabelSpec = NonNullable<GPUQuerySetDescriptor['label']>;
|
|
35
|
+
expectTypeOf<LabelForgeaX>().toEqualTypeOf<LabelSpec>();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("type field is a closed union 'occlusion' | 'timestamp' (GPUQueryType)", () => {
|
|
39
|
+
type TypeForgeaX = ValueOf<QuerySetDescriptor, 'type'>;
|
|
40
|
+
expectTypeOf<TypeForgeaX>().toEqualTypeOf<ValueOf<GPUQuerySetDescriptor, 'type'>>();
|
|
41
|
+
// Sentinel assignment: both literal members are accepted.
|
|
42
|
+
const _occ: QuerySetDescriptor = { type: 'occlusion', count: 4 };
|
|
43
|
+
const _ts: QuerySetDescriptor = { type: 'timestamp', count: 4 };
|
|
44
|
+
void _occ;
|
|
45
|
+
void _ts;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('count field is GPUSize32 (number)', () => {
|
|
49
|
+
type CountForgeaX = ValueOf<QuerySetDescriptor, 'count'>;
|
|
50
|
+
expectTypeOf<CountForgeaX>().toEqualTypeOf<ValueOf<GPUQuerySetDescriptor, 'count'>>();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('S-7 optional shape: label uses `?: T | undefined`', () => {
|
|
54
|
+
const _omitted: QuerySetDescriptor = { type: 'occlusion', count: 0 };
|
|
55
|
+
const _explicit: QuerySetDescriptor = { label: undefined, type: 'occlusion', count: 0 };
|
|
56
|
+
void _omitted;
|
|
57
|
+
void _explicit;
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('w10 type-level - RhiDevice.createQuerySet signature', () => {
|
|
62
|
+
it('returns Result<QuerySet, RhiError>', () => {
|
|
63
|
+
type Sig = RhiDevice['createQuerySet'];
|
|
64
|
+
type Ret = ReturnType<Sig>;
|
|
65
|
+
expectTypeOf<Ret>().toEqualTypeOf<Result<QuerySet, RhiError>>();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('takes a QuerySetDescriptor as the sole parameter', () => {
|
|
69
|
+
type Sig = RhiDevice['createQuerySet'];
|
|
70
|
+
type Params = Parameters<Sig>;
|
|
71
|
+
expectTypeOf<Params>().toEqualTypeOf<[QuerySetDescriptor]>();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('w10 type-level - QuerySet opaque handle does not expose raw GPU fields', () => {
|
|
76
|
+
it('QuerySet is brand-only (no .gpuQuerySet access)', () => {
|
|
77
|
+
const h = {} as QuerySet;
|
|
78
|
+
// @ts-expect-error MVP-1.3: QuerySet is opaque; raw fields not exposed.
|
|
79
|
+
h.gpuQuerySet;
|
|
80
|
+
});
|
|
81
|
+
});
|