@forgeax/engine-rhi 0.0.0-dev.8d955ade1c79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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__/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 +1758 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +87 -0
- package/dist/index.mjs.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 +284 -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 +108 -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 +2276 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// RhiBuffer mapAsync / mapState surface + MappedBuffer brand method form.
|
|
2
|
+
//
|
|
3
|
+
// K-1 decision (plan-strategy §2): mapAsync mode parameter is the raw
|
|
4
|
+
// GPUMapModeFlags bitmask (NOT a closed union 'read' | 'write'). spec mirror
|
|
5
|
+
// stance (research §4.2 + W3C WebGPU §buffer-mapping spec anchor).
|
|
6
|
+
//
|
|
7
|
+
// D-P2 #6 (feat-20260511-rhi-spec-realign-aggressive M1): mapAsync resolves
|
|
8
|
+
// to `Promise<Result<MappedBuffer, RhiError>>`; `getMappedRange` and `unmap`
|
|
9
|
+
// migrated from `Buffer` -> `MappedBuffer` method form (this: MappedBuffer
|
|
10
|
+
// implicit guard). The previous top-level Buffer.getMappedRange / Buffer.unmap
|
|
11
|
+
// are removed.
|
|
12
|
+
//
|
|
13
|
+
// Charter: proposition 1 (progressive disclosure - one read shows the buffer
|
|
14
|
+
// surface) + proposition 3 (machine-readable union > prose) + proposition 4
|
|
15
|
+
// (explicit failure - state mismatch surfaces TS2339 at the call site, not
|
|
16
|
+
// runtime panic).
|
|
17
|
+
//
|
|
18
|
+
// Anchors: requirements AC-11 (MappedBuffer brand) + research §4.1 / §4.2 /
|
|
19
|
+
// §4.4; plan-strategy §7.1 + D-P2 #6; plan-decisions OQ-5 (unmap +
|
|
20
|
+
// re-mapAsync re-attach path).
|
|
21
|
+
|
|
22
|
+
/// <reference types="@webgpu/types" />
|
|
23
|
+
|
|
24
|
+
import { describe, expectTypeOf, it } from 'vitest';
|
|
25
|
+
import type { Buffer, MappedBuffer, Result, RhiError } from '../index';
|
|
26
|
+
|
|
27
|
+
/** Strict structural equality helper. */
|
|
28
|
+
type Equals<X, Y> =
|
|
29
|
+
(<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? true : false;
|
|
30
|
+
|
|
31
|
+
describe('Buffer mapAsync surface + MappedBuffer brand methods', () => {
|
|
32
|
+
it('mapAsync(mode: GPUMapModeFlags, offset?, size?) returns Promise<Result<MappedBuffer, RhiError>>', () => {
|
|
33
|
+
type Method = Buffer['mapAsync'];
|
|
34
|
+
expectTypeOf<Method>().toBeFunction();
|
|
35
|
+
type Expected = (
|
|
36
|
+
mode: GPUMapModeFlags,
|
|
37
|
+
offset?: number | undefined,
|
|
38
|
+
size?: number | undefined,
|
|
39
|
+
) => Promise<Result<MappedBuffer, RhiError>>;
|
|
40
|
+
expectTypeOf<Expected>().toMatchTypeOf<Method>();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('mapAsync mode parameter is the raw GPUMapModeFlags bitmask (K-1 decision)', () => {
|
|
44
|
+
type ModeArg = Parameters<Buffer['mapAsync']>[0];
|
|
45
|
+
expectTypeOf<ModeArg>().toEqualTypeOf<GPUMapModeFlags>();
|
|
46
|
+
type ClosedUnion = 'read' | 'write';
|
|
47
|
+
type IsClosedUnion = ModeArg extends ClosedUnion ? true : false;
|
|
48
|
+
expectTypeOf<IsClosedUnion>().toEqualTypeOf<false>();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('MappedBuffer.getMappedRange returns Result<ArrayBuffer, RhiError>', () => {
|
|
52
|
+
type Method = MappedBuffer['getMappedRange'];
|
|
53
|
+
expectTypeOf<Method>().toBeFunction();
|
|
54
|
+
type Expected = (
|
|
55
|
+
offset?: number | undefined,
|
|
56
|
+
size?: number | undefined,
|
|
57
|
+
) => Result<ArrayBuffer, RhiError>;
|
|
58
|
+
expectTypeOf<Expected>().toMatchTypeOf<Method>();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('MappedBuffer.unmap returns void (spec silent no-op)', () => {
|
|
62
|
+
type Method = MappedBuffer['unmap'];
|
|
63
|
+
expectTypeOf<Method>().toBeFunction();
|
|
64
|
+
type Expected = () => void;
|
|
65
|
+
expectTypeOf<Expected>().toMatchTypeOf<Method>();
|
|
66
|
+
type IsResultShape =
|
|
67
|
+
ReturnType<MappedBuffer['unmap']> extends Result<unknown, unknown> ? true : false;
|
|
68
|
+
expectTypeOf<IsResultShape>().toEqualTypeOf<false>();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("mapState getter exposes the closed union 'unmapped' | 'pending' | 'mapped'", () => {
|
|
72
|
+
expectTypeOf<Buffer['mapState']>().toEqualTypeOf<'unmapped' | 'pending' | 'mapped'>();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('mapState writes are rejected at compile time (readonly)', () => {
|
|
76
|
+
type Writable<T> = { -readonly [K in keyof T]: T[K] };
|
|
77
|
+
type IsReadonlyMapState = Equals<Buffer, Writable<Buffer>>;
|
|
78
|
+
expectTypeOf<IsReadonlyMapState>().toEqualTypeOf<false>();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('MappedBuffer is a structural superset of Buffer (D-P2 #6 brand)', () => {
|
|
82
|
+
expectTypeOf<MappedBuffer>().toMatchTypeOf<Buffer>();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -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
|
+
});
|