@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,167 @@
|
|
|
1
|
+
// feat-20260622-chunk-gpu-instancing-sprite-tilemap M2 / w8 — RhiErrorCode
|
|
2
|
+
// 21st member ('instancing-exceeds-uniform-cap') + .detail shape unit tests.
|
|
3
|
+
//
|
|
4
|
+
// Plan-strategy 2 D-2: closed RhiErrorCode add-only minor evolution; AC-05 (a)
|
|
5
|
+
// surface — RhiInstancingExceedsUniformCapDetail tagged union member with
|
|
6
|
+
// fields { requested: number, limit: 128, scope: 'sprite' | 'tilemap-chunk' }.
|
|
7
|
+
//
|
|
8
|
+
// AI-user contract (charter proposition 4 explicit failure + plan-strategy
|
|
9
|
+
// 8.3 actionable hint): consumers narrow .detail through property access
|
|
10
|
+
// (err.detail.requested / err.detail.limit / err.detail.scope) instead of
|
|
11
|
+
// parsing err.message string. The 128-instance cap originates from the
|
|
12
|
+
// uniform-fallback path's WebGL2 minimum 16384B UBO size — research N-1
|
|
13
|
+
// observed value (128 * 64B = 8192B leaves headroom for the per-frame
|
|
14
|
+
// material UBO slice). research F-4 + plan-decisions D-3 lock the limit.
|
|
15
|
+
//
|
|
16
|
+
// The runtime-shape assertions (sections 1 + 2) double as compile-time
|
|
17
|
+
// guarantees: if the union member or detail shape regress, tsc + vitest
|
|
18
|
+
// both flip red. Section 3 protects pre-existing 'limit-exceeded' detail
|
|
19
|
+
// from incidental drift (LimitExceededDetail must keep
|
|
20
|
+
// { maxStorageBufferBindingSize, requestedBytes } per
|
|
21
|
+
// feat-20260513-instanced-mesh M5 reshape).
|
|
22
|
+
|
|
23
|
+
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
24
|
+
import {
|
|
25
|
+
type LimitExceededDetail,
|
|
26
|
+
RhiError,
|
|
27
|
+
type RhiErrorCode,
|
|
28
|
+
type RhiErrorDetail,
|
|
29
|
+
type RhiInstancingExceedsUniformCapDetail,
|
|
30
|
+
} from '../errors';
|
|
31
|
+
|
|
32
|
+
describe('M2 / w8 — RhiErrorCode 21st member instancing-exceeds-uniform-cap', () => {
|
|
33
|
+
it('contains instancing-exceeds-uniform-cap in the closed RhiErrorCode union', () => {
|
|
34
|
+
expectTypeOf<'instancing-exceeds-uniform-cap'>().toMatchTypeOf<RhiErrorCode>();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('exhaustive switch over all 21 members compiles without default fallback', () => {
|
|
38
|
+
function describeCode(code: RhiErrorCode): string {
|
|
39
|
+
switch (code) {
|
|
40
|
+
case 'adapter-unavailable':
|
|
41
|
+
return 'adapter';
|
|
42
|
+
case 'feature-not-enabled':
|
|
43
|
+
return 'feature';
|
|
44
|
+
case 'limit-exceeded':
|
|
45
|
+
return 'limit';
|
|
46
|
+
case 'shader-compile-failed':
|
|
47
|
+
return 'shader';
|
|
48
|
+
case 'rhi-not-available':
|
|
49
|
+
return 'reserved';
|
|
50
|
+
case 'webgpu-runtime-error':
|
|
51
|
+
return 'webgpu-runtime';
|
|
52
|
+
case 'command-encoder-finished':
|
|
53
|
+
return 'encoder-finished';
|
|
54
|
+
case 'render-pass-not-ended':
|
|
55
|
+
return 'pass-not-ended';
|
|
56
|
+
case 'queue-submit-failed':
|
|
57
|
+
return 'queue-submit';
|
|
58
|
+
case 'queue-write-buffer-out-of-bounds':
|
|
59
|
+
return 'queue-bounds';
|
|
60
|
+
case 'render-system-no-camera':
|
|
61
|
+
return 'no-camera';
|
|
62
|
+
case 'render-system-multi-camera':
|
|
63
|
+
return 'multi-camera';
|
|
64
|
+
case 'render-system-multi-light':
|
|
65
|
+
return 'multi-light';
|
|
66
|
+
case 'asset-not-registered':
|
|
67
|
+
return 'asset-miss';
|
|
68
|
+
case 'device-lost':
|
|
69
|
+
return 'device-lost';
|
|
70
|
+
case 'oom':
|
|
71
|
+
return 'oom';
|
|
72
|
+
case 'internal-error':
|
|
73
|
+
return 'internal-error';
|
|
74
|
+
case 'hierarchy-broken':
|
|
75
|
+
return 'hierarchy-broken';
|
|
76
|
+
case 'destroy-after-destroy':
|
|
77
|
+
return 'destroy-after-destroy';
|
|
78
|
+
case 'rhi-descriptor-invalid':
|
|
79
|
+
return 'descriptor-invalid';
|
|
80
|
+
case 'instancing-exceeds-uniform-cap':
|
|
81
|
+
return 'instancing-cap';
|
|
82
|
+
case 'render-system-empty-worlds':
|
|
83
|
+
return 'empty-worlds';
|
|
84
|
+
case 'render-system-owner-out-of-range':
|
|
85
|
+
return 'owner-oob';
|
|
86
|
+
}
|
|
87
|
+
// No default — tsc strict guards: union drift here triggers compile-time red.
|
|
88
|
+
}
|
|
89
|
+
expect(describeCode('instancing-exceeds-uniform-cap')).toBe('instancing-cap');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('M2 / w8 — RhiInstancingExceedsUniformCapDetail shape', () => {
|
|
94
|
+
it('detail equals { requested: number, limit: 128, scope: sprite|tilemap-chunk }', () => {
|
|
95
|
+
expectTypeOf<RhiInstancingExceedsUniformCapDetail>().toEqualTypeOf<{
|
|
96
|
+
readonly requested: number;
|
|
97
|
+
readonly limit: 128;
|
|
98
|
+
readonly scope: 'sprite' | 'tilemap-chunk';
|
|
99
|
+
}>();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('detail is a member of the RhiErrorDetail discriminated union', () => {
|
|
103
|
+
const detail: RhiInstancingExceedsUniformCapDetail = {
|
|
104
|
+
requested: 200,
|
|
105
|
+
limit: 128,
|
|
106
|
+
scope: 'sprite',
|
|
107
|
+
};
|
|
108
|
+
const widened: RhiErrorDetail = detail;
|
|
109
|
+
expectTypeOf(widened).toMatchTypeOf<RhiErrorDetail>();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('limit field is the literal 128, not a generic number', () => {
|
|
113
|
+
// tsc will reject `limit: 256` etc. — locked to the literal 128.
|
|
114
|
+
const detail: RhiInstancingExceedsUniformCapDetail = {
|
|
115
|
+
requested: 1024,
|
|
116
|
+
limit: 128,
|
|
117
|
+
scope: 'tilemap-chunk',
|
|
118
|
+
};
|
|
119
|
+
expect(detail.limit).toBe(128);
|
|
120
|
+
expectTypeOf<RhiInstancingExceedsUniformCapDetail['limit']>().toEqualTypeOf<128>();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('scope field is the closed sprite | tilemap-chunk literal union', () => {
|
|
124
|
+
expectTypeOf<RhiInstancingExceedsUniformCapDetail['scope']>().toEqualTypeOf<
|
|
125
|
+
'sprite' | 'tilemap-chunk'
|
|
126
|
+
>();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('AI-user property-access consumption path: read .requested / .limit / .scope', () => {
|
|
130
|
+
const err = new RhiError({
|
|
131
|
+
code: 'instancing-exceeds-uniform-cap',
|
|
132
|
+
expected: 'bucket instance count <= 128 (uniform fallback cap)',
|
|
133
|
+
hint: 'reduce bucket size, switch to a WebGPU-capable backend, or accept per-cell fallback',
|
|
134
|
+
detail: { requested: 256, limit: 128, scope: 'tilemap-chunk' },
|
|
135
|
+
});
|
|
136
|
+
expect(err.code).toBe('instancing-exceeds-uniform-cap');
|
|
137
|
+
if (err.code === 'instancing-exceeds-uniform-cap') {
|
|
138
|
+
// After narrowing on .code, .detail is reachable via property access
|
|
139
|
+
// — no string parsing of err.message.
|
|
140
|
+
const d = err.detail as RhiInstancingExceedsUniformCapDetail;
|
|
141
|
+
expect(d.requested).toBe(256);
|
|
142
|
+
expect(d.limit).toBe(128);
|
|
143
|
+
expect(d.scope).toBe('tilemap-chunk');
|
|
144
|
+
}
|
|
145
|
+
expect(err.expected.length).toBeGreaterThan(0);
|
|
146
|
+
expect(err.hint.length).toBeGreaterThan(0);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('M2 / w8 — pre-existing limit-exceeded detail unchanged', () => {
|
|
151
|
+
it('LimitExceededDetail keeps { maxStorageBufferBindingSize, requestedBytes }', () => {
|
|
152
|
+
expectTypeOf<LimitExceededDetail>().toEqualTypeOf<{
|
|
153
|
+
readonly maxStorageBufferBindingSize: number;
|
|
154
|
+
readonly requestedBytes: number;
|
|
155
|
+
}>();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("'limit-exceeded' member remains in the closed union (no rename / reorder)", () => {
|
|
159
|
+
expectTypeOf<'limit-exceeded'>().toMatchTypeOf<RhiErrorCode>();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('limit-exceeded and instancing-exceeds-uniform-cap are distinct members', () => {
|
|
163
|
+
type Le = Extract<RhiErrorCode, 'limit-exceeded'>;
|
|
164
|
+
type Ic = Extract<RhiErrorCode, 'instancing-exceeds-uniform-cap'>;
|
|
165
|
+
expectTypeOf<Le>().not.toEqualTypeOf<Ic>();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// MVP-1.7 (type-level) — closed RhiErrorCode union completeness + exhaustive switch
|
|
2
|
+
// without default fallback. Round 1 baseline: 6 members; extended to 10 members in
|
|
3
|
+
// feat-20260508-rhi-surface-completion w7 (D-S3); extended to 14 members in
|
|
4
|
+
// feat-20260509-ecs-render-bridge-mvp w6 (D-S7); extended to 17 members in
|
|
5
|
+
// feat-20260511-rhi-spec-realign-aggressive w1 (red) -> w6 (green) per
|
|
6
|
+
// plan-strategy D-P4 + requirements AC-04. Three new members map W3C WebGPU
|
|
7
|
+
// 22.2 spec subtypes (device-lost / oom / internal-error) to the forgeax union.
|
|
8
|
+
// Extended to 20 members in feat-20260619-wasm-fault-isolation M3 w7:
|
|
9
|
+
// 'rhi-descriptor-invalid' for wgpu-wasm descriptor parse failures.
|
|
10
|
+
//
|
|
11
|
+
// 20 members:
|
|
12
|
+
// 1) 'adapter-unavailable' (Round 1 baseline)
|
|
13
|
+
// 2) 'feature-not-enabled' (Round 1 baseline)
|
|
14
|
+
// 3) 'limit-exceeded' (Round 1 baseline)
|
|
15
|
+
// 4) 'shader-compile-failed' (Round 1 baseline)
|
|
16
|
+
// 5) 'rhi-not-available' (Round 1 baseline; @reserved + placeholder)
|
|
17
|
+
// 6) 'webgpu-runtime-error' (verify-gpu-smoke-gate K-9 sixth member)
|
|
18
|
+
// 7) 'command-encoder-finished' (rhi-surface-completion D-S3)
|
|
19
|
+
// 8) 'render-pass-not-ended' (rhi-surface-completion D-S3)
|
|
20
|
+
// 9) 'queue-submit-failed' (rhi-surface-completion D-S3)
|
|
21
|
+
// 10) 'queue-write-buffer-out-of-bounds' (rhi-surface-completion D-S3)
|
|
22
|
+
// 11) 'render-system-no-camera' (ecs-render-bridge-mvp D-S7)
|
|
23
|
+
// 12) 'render-system-multi-camera' (ecs-render-bridge-mvp D-S7)
|
|
24
|
+
// 13) 'render-system-multi-light' (ecs-render-bridge-mvp D-S7)
|
|
25
|
+
// 14) 'asset-not-registered' (ecs-render-bridge-mvp D-S7)
|
|
26
|
+
// 15) 'device-lost' (rhi-spec-realign-aggressive D-P4 / R-02 §2.1)
|
|
27
|
+
// 16) 'oom' (rhi-spec-realign-aggressive D-P4 / R-02 §2.1)
|
|
28
|
+
// 17) 'internal-error' (rhi-spec-realign-aggressive D-P4 / R-02 §2.1)
|
|
29
|
+
// 18) 'hierarchy-broken' (asset-system-v1 w4 / D-P2 + requirements §9 row 8)
|
|
30
|
+
// 19) 'destroy-after-destroy' (feat-20260612-rhi-destroy-renderer-dispose-gpu-lifecycle M1 / D-6 + D-7)
|
|
31
|
+
// 20) 'rhi-descriptor-invalid' (feat-20260619-wasm-fault-isolation M3 w7 / D-1 + D-2 + D-8)
|
|
32
|
+
//
|
|
33
|
+
// charter mapping: proposition 4 (explicit failure via closed union) +
|
|
34
|
+
// proposition 3 (machine-readable union > prose) — switch (err.code) without
|
|
35
|
+
// default fallback; tsc strict mode rejects union drift at compile time.
|
|
36
|
+
//
|
|
37
|
+
// Related: requirements AC-04 (18-member union) + AC MVP-1.7 + AC-10 + AC-RSC-07;
|
|
38
|
+
// plan-strategy D-P4 (.expected / .hint templates) + D-P2 (hierarchy-broken) +
|
|
39
|
+
// S-6 (types/rhi single source) + 7.2 naming convention; research R-02 §2.1
|
|
40
|
+
// (W3C spec 22.2 subtypes).
|
|
41
|
+
|
|
42
|
+
import { describe, expectTypeOf, it } from 'vitest';
|
|
43
|
+
import type { RhiError, RhiErrorCode } from '../errors';
|
|
44
|
+
|
|
45
|
+
describe('MVP-1.7 — RhiErrorCode closed union 20 members', () => {
|
|
46
|
+
it('contains adapter-unavailable', () => {
|
|
47
|
+
expectTypeOf<'adapter-unavailable'>().toMatchTypeOf<RhiErrorCode>();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('contains feature-not-enabled', () => {
|
|
51
|
+
expectTypeOf<'feature-not-enabled'>().toMatchTypeOf<RhiErrorCode>();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('contains limit-exceeded', () => {
|
|
55
|
+
expectTypeOf<'limit-exceeded'>().toMatchTypeOf<RhiErrorCode>();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('contains shader-compile-failed', () => {
|
|
59
|
+
expectTypeOf<'shader-compile-failed'>().toMatchTypeOf<RhiErrorCode>();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('contains rhi-not-available (@reserved + placeholder methods)', () => {
|
|
63
|
+
expectTypeOf<'rhi-not-available'>().toMatchTypeOf<RhiErrorCode>();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('contains webgpu-runtime-error (verify-gpu-smoke-gate K-9 sixth member)', () => {
|
|
67
|
+
expectTypeOf<'webgpu-runtime-error'>().toMatchTypeOf<RhiErrorCode>();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('contains command-encoder-finished (D-S3 new member)', () => {
|
|
71
|
+
expectTypeOf<'command-encoder-finished'>().toMatchTypeOf<RhiErrorCode>();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('contains render-pass-not-ended (D-S3 new member)', () => {
|
|
75
|
+
expectTypeOf<'render-pass-not-ended'>().toMatchTypeOf<RhiErrorCode>();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('contains queue-submit-failed (D-S3 new member)', () => {
|
|
79
|
+
expectTypeOf<'queue-submit-failed'>().toMatchTypeOf<RhiErrorCode>();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('contains queue-write-buffer-out-of-bounds (D-S3 new member)', () => {
|
|
83
|
+
expectTypeOf<'queue-write-buffer-out-of-bounds'>().toMatchTypeOf<RhiErrorCode>();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('contains render-system-no-camera (D-S7 new member)', () => {
|
|
87
|
+
expectTypeOf<'render-system-no-camera'>().toMatchTypeOf<RhiErrorCode>();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('contains render-system-multi-camera (D-S7 new member)', () => {
|
|
91
|
+
expectTypeOf<'render-system-multi-camera'>().toMatchTypeOf<RhiErrorCode>();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('contains render-system-multi-light (D-S7 new member)', () => {
|
|
95
|
+
expectTypeOf<'render-system-multi-light'>().toMatchTypeOf<RhiErrorCode>();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('contains asset-not-registered (D-S7 new member)', () => {
|
|
99
|
+
expectTypeOf<'asset-not-registered'>().toMatchTypeOf<RhiErrorCode>();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('contains device-lost (D-P4 new member; spec 22.1 device lost)', () => {
|
|
103
|
+
expectTypeOf<'device-lost'>().toMatchTypeOf<RhiErrorCode>();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('contains oom (D-P4 new member; spec 22.2 GPUOutOfMemoryError subtype)', () => {
|
|
107
|
+
expectTypeOf<'oom'>().toMatchTypeOf<RhiErrorCode>();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('contains internal-error (D-P4 new member; spec 22.2 GPUInternalError subtype)', () => {
|
|
111
|
+
expectTypeOf<'internal-error'>().toMatchTypeOf<RhiErrorCode>();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('contains hierarchy-broken (w4 asset-system-v1 D-P2 new member; propagateTransforms stale ChildOf ref)', () => {
|
|
115
|
+
expectTypeOf<'hierarchy-broken'>().toMatchTypeOf<RhiErrorCode>();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('contains destroy-after-destroy (feat-20260612 M1 / D-6 + D-7; double destroy fail-fast)', () => {
|
|
119
|
+
expectTypeOf<'destroy-after-destroy'>().toMatchTypeOf<RhiErrorCode>();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('contains rhi-descriptor-invalid (feat-20260619 M3 w7; wgpu-wasm descriptor parse failure)', () => {
|
|
123
|
+
expectTypeOf<'rhi-descriptor-invalid'>().toMatchTypeOf<RhiErrorCode>();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('union remains closed: rejects non-member literal', () => {
|
|
127
|
+
// @ts-expect-error MVP-1.7: union is closed — 'not-a-real-code' is not a member.
|
|
128
|
+
const _bogus: RhiErrorCode = 'not-a-real-code';
|
|
129
|
+
void _bogus;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('exhaustive switch with no default fallback for all members (charter proposition 4)', () => {
|
|
133
|
+
function describeCode(code: RhiErrorCode): string {
|
|
134
|
+
switch (code) {
|
|
135
|
+
case 'adapter-unavailable':
|
|
136
|
+
return 'adapter';
|
|
137
|
+
case 'feature-not-enabled':
|
|
138
|
+
return 'feature';
|
|
139
|
+
case 'limit-exceeded':
|
|
140
|
+
return 'limit';
|
|
141
|
+
case 'shader-compile-failed':
|
|
142
|
+
return 'shader';
|
|
143
|
+
case 'rhi-not-available':
|
|
144
|
+
return 'reserved';
|
|
145
|
+
case 'webgpu-runtime-error':
|
|
146
|
+
return 'webgpu-runtime';
|
|
147
|
+
case 'command-encoder-finished':
|
|
148
|
+
return 'encoder-finished';
|
|
149
|
+
case 'render-pass-not-ended':
|
|
150
|
+
return 'pass-not-ended';
|
|
151
|
+
case 'queue-submit-failed':
|
|
152
|
+
return 'queue-submit';
|
|
153
|
+
case 'queue-write-buffer-out-of-bounds':
|
|
154
|
+
return 'queue-bounds';
|
|
155
|
+
case 'render-system-no-camera':
|
|
156
|
+
return 'no-camera';
|
|
157
|
+
case 'render-system-multi-camera':
|
|
158
|
+
return 'multi-camera';
|
|
159
|
+
case 'render-system-multi-light':
|
|
160
|
+
return 'multi-light';
|
|
161
|
+
case 'asset-not-registered':
|
|
162
|
+
return 'asset-miss';
|
|
163
|
+
case 'device-lost':
|
|
164
|
+
return 'device-lost';
|
|
165
|
+
case 'oom':
|
|
166
|
+
return 'oom';
|
|
167
|
+
case 'internal-error':
|
|
168
|
+
return 'internal-error';
|
|
169
|
+
case 'hierarchy-broken':
|
|
170
|
+
return 'hierarchy-broken';
|
|
171
|
+
case 'destroy-after-destroy':
|
|
172
|
+
return 'destroy-after-destroy';
|
|
173
|
+
case 'rhi-descriptor-invalid':
|
|
174
|
+
return 'descriptor-invalid';
|
|
175
|
+
case 'instancing-exceeds-uniform-cap':
|
|
176
|
+
return 'instancing-cap';
|
|
177
|
+
case 'render-system-empty-worlds':
|
|
178
|
+
return 'empty-worlds';
|
|
179
|
+
case 'render-system-owner-out-of-range':
|
|
180
|
+
return 'owner-oob';
|
|
181
|
+
}
|
|
182
|
+
// No default — TS guards: union drift here triggers compile-time red.
|
|
183
|
+
}
|
|
184
|
+
expectTypeOf(describeCode).returns.toEqualTypeOf<string>();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('RhiError.code literal type is exactly RhiErrorCode union', () => {
|
|
188
|
+
expectTypeOf<RhiError['code']>().toEqualTypeOf<RhiErrorCode>();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('RhiError has readonly code / expected / hint fields', () => {
|
|
192
|
+
type ErrShape = {
|
|
193
|
+
readonly code: RhiErrorCode;
|
|
194
|
+
readonly expected: string;
|
|
195
|
+
readonly hint: string;
|
|
196
|
+
};
|
|
197
|
+
expectTypeOf<RhiError>().toMatchTypeOf<ErrShape>();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// feat-20260513-instanced-mesh M5 (T-M5-2): LimitExceededDetail field
|
|
2
|
+
// reshape — type-level assertions (vitest typecheck layer, *.test-d.ts).
|
|
3
|
+
//
|
|
4
|
+
// Companion to `limit-exceeded-detail.test.ts` (runtime). Vitest's
|
|
5
|
+
// typecheck pass (vitest 4.x default `typecheck.include === **/*.test-d.{ts,tsx}`)
|
|
6
|
+
// runs `expectTypeOf` here against the source LimitExceededDetail type;
|
|
7
|
+
// a legacy regression that re-introduces `renderableCount` / `limit`
|
|
8
|
+
// fields would flip these red.
|
|
9
|
+
//
|
|
10
|
+
// Plan rationale: requirements AC-15 (LimitExceededDetail field reshape
|
|
11
|
+
// evolution major) + plan-strategy D-3 (rename + replace, AGENTS.md
|
|
12
|
+
// Change stance — optimal > compatible).
|
|
13
|
+
|
|
14
|
+
import { describe, expectTypeOf, it } from 'vitest';
|
|
15
|
+
import type { LimitExceededDetail, RhiErrorDetail } from '../errors';
|
|
16
|
+
|
|
17
|
+
describe('feat-20260513-instanced-mesh T-M5-2 LimitExceededDetail (type-level)', () => {
|
|
18
|
+
it('equals the new { maxStorageBufferBindingSize, requestedBytes } shape', () => {
|
|
19
|
+
expectTypeOf<LimitExceededDetail>().toEqualTypeOf<{
|
|
20
|
+
readonly maxStorageBufferBindingSize: number;
|
|
21
|
+
readonly requestedBytes: number;
|
|
22
|
+
}>();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('is a member of the RhiErrorDetail discriminated union', () => {
|
|
26
|
+
const detail: LimitExceededDetail = {
|
|
27
|
+
maxStorageBufferBindingSize: 256,
|
|
28
|
+
requestedBytes: 4096,
|
|
29
|
+
};
|
|
30
|
+
const widened: RhiErrorDetail = detail;
|
|
31
|
+
expectTypeOf(widened).toMatchTypeOf<RhiErrorDetail>();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Type-level — MappedBuffer brand + method form (this: MappedBuffer guard).
|
|
2
|
+
//
|
|
3
|
+
// Introduced in feat-20260511-rhi-spec-realign-aggressive w4 (red) -> w10
|
|
4
|
+
// (green) per requirements AC-11 + plan-strategy §7.1 MappedBuffer + D-P2
|
|
5
|
+
// break-point #6 (brand + method form merged into one row).
|
|
6
|
+
//
|
|
7
|
+
// Shape:
|
|
8
|
+
// - `MappedBuffer = Buffer & { readonly __mapped: void }`
|
|
9
|
+
// - `buffer.mapAsync(mode, ...)` returns `Promise<Result<MappedBuffer,
|
|
10
|
+
// RhiError>>` — success branch carries the branded handle the caller
|
|
11
|
+
// subsequently consumes for getMappedRange / unmap.
|
|
12
|
+
// - `MappedBuffer.getMappedRange(...)` / `MappedBuffer.unmap()` are method
|
|
13
|
+
// forms on the brand (`this: MappedBuffer` implicit guard); the previous
|
|
14
|
+
// top-level `Buffer.getMappedRange` / `Buffer.unmap` are removed.
|
|
15
|
+
//
|
|
16
|
+
// charter mapping: proposition 4 (TS compile-time guard against calling
|
|
17
|
+
// getMappedRange on an un-mapped Buffer) + proposition 5 (consistent
|
|
18
|
+
// abstraction — the brand encodes state at the type level, no runtime
|
|
19
|
+
// re-validation needed at every call site).
|
|
20
|
+
//
|
|
21
|
+
// Related Rust analogue: `wgpu::BufferSlice<'_>` ownership/borrow tracking;
|
|
22
|
+
// forgeax uses a brand instead of lifetimes because TS lacks affine types.
|
|
23
|
+
|
|
24
|
+
import { describe, expectTypeOf, it } from 'vitest';
|
|
25
|
+
import type { Buffer, MappedBuffer, Result, RhiError } from '../index';
|
|
26
|
+
|
|
27
|
+
describe('MappedBuffer brand — type-level state machine', () => {
|
|
28
|
+
it('mapAsync resolves to Promise<Result<MappedBuffer, RhiError>>', () => {
|
|
29
|
+
type MapAsyncRet = ReturnType<Buffer['mapAsync']>;
|
|
30
|
+
expectTypeOf<MapAsyncRet>().toEqualTypeOf<Promise<Result<MappedBuffer, RhiError>>>();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('MappedBuffer is a structural superset of Buffer (Buffer & { __mapped })', () => {
|
|
34
|
+
expectTypeOf<MappedBuffer>().toMatchTypeOf<Buffer>();
|
|
35
|
+
// Negative direction — plain Buffer is NOT a MappedBuffer (brand guard).
|
|
36
|
+
// @ts-expect-error TS2345 — Buffer is missing the __mapped brand.
|
|
37
|
+
const _bogus: MappedBuffer = undefined as unknown as Buffer;
|
|
38
|
+
void _bogus;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('getMappedRange is a method on MappedBuffer (not on Buffer)', () => {
|
|
42
|
+
type GMR = MappedBuffer['getMappedRange'];
|
|
43
|
+
expectTypeOf<GMR>().toMatchTypeOf<
|
|
44
|
+
(offset?: number, size?: number) => Result<ArrayBuffer, RhiError>
|
|
45
|
+
>();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('unmap is a method on MappedBuffer (not on Buffer)', () => {
|
|
49
|
+
type Unmap = MappedBuffer['unmap'];
|
|
50
|
+
expectTypeOf<Unmap>().toMatchTypeOf<() => void>();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('AI user pattern: const r = await buf.mapAsync(...); if (r.ok) mappedBuffer.getMappedRange()', async () => {
|
|
54
|
+
const buffer = undefined as unknown as Buffer;
|
|
55
|
+
const r = await buffer.mapAsync(1);
|
|
56
|
+
if (r.ok) {
|
|
57
|
+
const mappedBuffer: MappedBuffer = r.value;
|
|
58
|
+
// method-form: mappedBuffer.getMappedRange and mappedBuffer.unmap exist
|
|
59
|
+
const range = mappedBuffer.getMappedRange();
|
|
60
|
+
expectTypeOf(range).toEqualTypeOf<Result<ArrayBuffer, RhiError>>();
|
|
61
|
+
const u = mappedBuffer.unmap();
|
|
62
|
+
expectTypeOf<typeof u>().toEqualTypeOf<void>();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// MVP-1.3 — 14 opaque handle types do not surface internal GPU fields
|
|
2
|
+
// (type-level red → green).
|
|
3
|
+
//
|
|
4
|
+
// Shape: 14 typed `Id<T>` brand-only types; user attempts to access
|
|
5
|
+
// `.gpuBuffer` / `.__brand` etc. → TypeScript compile-time red signal
|
|
6
|
+
// (auto-guarded by `@ts-expect-error`).
|
|
7
|
+
//
|
|
8
|
+
// Red expected state (at the M1 t7 commit): t10 has not declared these types
|
|
9
|
+
// → `tsc -b` reports a 'no exported member' red.
|
|
10
|
+
// Green expected state (after the M1 t10 commit): the 14 handle types are
|
|
11
|
+
// exported and brand-only → all 14 `ts-expect-error` directive blocks fire
|
|
12
|
+
// (attempts to access internal fields fail).
|
|
13
|
+
//
|
|
14
|
+
// Anchors: requirements §AC MVP-1.3 + §AI User Affordances / resource handles
|
|
15
|
+
// opaque; plan-strategy §4.3 key-test-point table row 2; §7
|
|
16
|
+
// proposition 4 / proposition 5; research §F-1 + §R5 (`__brand` is
|
|
17
|
+
// the d.ts nominal-typing workaround).
|
|
18
|
+
|
|
19
|
+
import { describe, it } from 'vitest';
|
|
20
|
+
import type {
|
|
21
|
+
BindGroup,
|
|
22
|
+
BindGroupLayout,
|
|
23
|
+
Buffer,
|
|
24
|
+
CommandBuffer,
|
|
25
|
+
CommandEncoder,
|
|
26
|
+
ComputePipeline,
|
|
27
|
+
Fence,
|
|
28
|
+
PipelineLayout,
|
|
29
|
+
QuerySet,
|
|
30
|
+
RenderPipeline,
|
|
31
|
+
Sampler,
|
|
32
|
+
ShaderModule,
|
|
33
|
+
Texture,
|
|
34
|
+
TextureView,
|
|
35
|
+
} from '../index';
|
|
36
|
+
|
|
37
|
+
describe('MVP-1.3 — 14 opaque handle types reject access to internal GPU fields', () => {
|
|
38
|
+
it('Buffer rejects access to the internal GPU object', () => {
|
|
39
|
+
const h = {} as Buffer;
|
|
40
|
+
// @ts-expect-error MVP-1.3: Buffer is opaque; gpuBuffer is not exposed.
|
|
41
|
+
h.gpuBuffer;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('Texture rejects access to the internal GPU object', () => {
|
|
45
|
+
const h = {} as Texture;
|
|
46
|
+
// @ts-expect-error MVP-1.3: Texture is opaque; gpuTexture is not exposed.
|
|
47
|
+
h.gpuTexture;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('TextureView rejects access to the internal GPU object', () => {
|
|
51
|
+
const h = {} as TextureView;
|
|
52
|
+
// @ts-expect-error MVP-1.3: TextureView is opaque; gpuTextureView is not exposed.
|
|
53
|
+
h.gpuTextureView;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('Sampler rejects access to the internal GPU object', () => {
|
|
57
|
+
const h = {} as Sampler;
|
|
58
|
+
// @ts-expect-error MVP-1.3: Sampler is opaque; gpuSampler is not exposed.
|
|
59
|
+
h.gpuSampler;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('BindGroup rejects access to the internal GPU object', () => {
|
|
63
|
+
const h = {} as BindGroup;
|
|
64
|
+
// @ts-expect-error MVP-1.3: BindGroup is opaque; gpuBindGroup is not exposed.
|
|
65
|
+
h.gpuBindGroup;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('BindGroupLayout rejects access to the internal GPU object', () => {
|
|
69
|
+
const h = {} as BindGroupLayout;
|
|
70
|
+
// @ts-expect-error MVP-1.3: BindGroupLayout is opaque; gpuBindGroupLayout is not exposed.
|
|
71
|
+
h.gpuBindGroupLayout;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('PipelineLayout rejects access to the internal GPU object', () => {
|
|
75
|
+
const h = {} as PipelineLayout;
|
|
76
|
+
// @ts-expect-error MVP-1.3: PipelineLayout is opaque; gpuPipelineLayout is not exposed.
|
|
77
|
+
h.gpuPipelineLayout;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('RenderPipeline rejects access to the internal GPU object', () => {
|
|
81
|
+
const h = {} as RenderPipeline;
|
|
82
|
+
// @ts-expect-error MVP-1.3: RenderPipeline is opaque; gpuRenderPipeline is not exposed.
|
|
83
|
+
h.gpuRenderPipeline;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('ComputePipeline rejects access to the internal GPU object', () => {
|
|
87
|
+
const h = {} as ComputePipeline;
|
|
88
|
+
// @ts-expect-error MVP-1.3: ComputePipeline is opaque; gpuComputePipeline is not exposed.
|
|
89
|
+
h.gpuComputePipeline;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('ShaderModule rejects access to the internal GPU object', () => {
|
|
93
|
+
const h = {} as ShaderModule;
|
|
94
|
+
// @ts-expect-error MVP-1.3: ShaderModule is opaque; gpuShaderModule is not exposed.
|
|
95
|
+
h.gpuShaderModule;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('QuerySet rejects access to the internal GPU object', () => {
|
|
99
|
+
const h = {} as QuerySet;
|
|
100
|
+
// @ts-expect-error MVP-1.3: QuerySet is opaque; gpuQuerySet is not exposed.
|
|
101
|
+
h.gpuQuerySet;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('Fence rejects access to the internal sync object', () => {
|
|
105
|
+
const h = {} as Fence;
|
|
106
|
+
// @ts-expect-error MVP-1.3: Fence is opaque; internal sync handle is not exposed.
|
|
107
|
+
h.gpuFence;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('CommandEncoder rejects access to the internal GPU object', () => {
|
|
111
|
+
const h = {} as CommandEncoder;
|
|
112
|
+
// @ts-expect-error MVP-1.3: CommandEncoder is opaque; gpuCommandEncoder is not exposed.
|
|
113
|
+
h.gpuCommandEncoder;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('CommandBuffer rejects access to the internal GPU object', () => {
|
|
117
|
+
const h = {} as CommandBuffer;
|
|
118
|
+
// @ts-expect-error MVP-1.3: CommandBuffer is opaque; gpuCommandBuffer is not exposed.
|
|
119
|
+
h.gpuCommandBuffer;
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// feat-20260709-editor-world-partition-editorworld-super-composite / M1 / w3
|
|
2
|
+
// (RED — impl lands in w6/w7). Contract test for the `role` field added to the
|
|
3
|
+
// `render-system-owner-out-of-range` structured error detail.
|
|
4
|
+
//
|
|
5
|
+
// Today the draw entry validates a SINGLE `owner` index and, on out-of-range,
|
|
6
|
+
// returns `RhiError { code: 'render-system-owner-out-of-range', detail: {
|
|
7
|
+
// owner, worldCount } }` (errors.ts validateDrawArgs). This feature splits the
|
|
8
|
+
// single owner into `cameraOwner` + `resourceOwner`, so a validation failure
|
|
9
|
+
// must say WHICH index was out of range. w7 extends the detail to
|
|
10
|
+
// `{ role: 'camera' | 'resource', owner, worldCount }`; w6 threads two indices
|
|
11
|
+
// through the draw entry and validates cameraOwner BEFORE resourceOwner.
|
|
12
|
+
//
|
|
13
|
+
// This test pins the future contract as the SSOT for w6/w7. The rhi package is
|
|
14
|
+
// World-free (it owns the RhiErrorCode closed union, architecture-principles §1
|
|
15
|
+
// SSOT) and cannot import the runtime Renderer — so, exactly like the existing
|
|
16
|
+
// draw-validation-errors.test.ts, the contract is exercised through the pure
|
|
17
|
+
// `validateDrawArgs` primitive rather than through `renderer.draw` itself. The
|
|
18
|
+
// future primitive shape is `validateDrawArgs(worldCount, { cameraOwner,
|
|
19
|
+
// resourceOwner })`. Until w6/w7 evolve the signature + detail, these calls are
|
|
20
|
+
// a type/shape mismatch and this file is RED (test-first).
|
|
21
|
+
//
|
|
22
|
+
// No new error code is added (D-3: 0 new error codes) — only `.detail` grows a
|
|
23
|
+
// `role` discriminator.
|
|
24
|
+
//
|
|
25
|
+
// Anchors:
|
|
26
|
+
// requirements AC-08 (out-of-range/missing returns a structured error aligned
|
|
27
|
+
// with the existing `render-system-owner-out-of-range` + `.detail`)
|
|
28
|
+
// plan-strategy §3.3 interface example (err.detail === { role: 'resource',
|
|
29
|
+
// owner: 3, worldCount: 2 }) + §2 D-3
|
|
30
|
+
// research F2.3 (existing .detail = { owner, worldCount }; add role field)
|
|
31
|
+
|
|
32
|
+
import { describe, expect, it } from 'vitest';
|
|
33
|
+
import { validateDrawArgs } from '../errors';
|
|
34
|
+
|
|
35
|
+
// The role-carrying detail w7 will attach to the existing code. Declared
|
|
36
|
+
// locally so this test states the future contract independent of the
|
|
37
|
+
// (not-yet-updated) RhiOwnerOutOfRangeDetail export.
|
|
38
|
+
interface OwnerOutOfRangeRoleDetail {
|
|
39
|
+
readonly role: 'camera' | 'resource';
|
|
40
|
+
readonly owner: number;
|
|
41
|
+
readonly worldCount: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('owner-out-of-range role detail (w3, AC-08)', () => {
|
|
45
|
+
it("cameraOwner out of range -> code render-system-owner-out-of-range, detail.role === 'camera'", () => {
|
|
46
|
+
// worldCount=2, cameraOwner=99 is out of range; resourceOwner=0 is valid.
|
|
47
|
+
const r = validateDrawArgs(2, { cameraOwner: 99, resourceOwner: 0 });
|
|
48
|
+
expect(r.ok).toBe(false);
|
|
49
|
+
if (!r.ok) {
|
|
50
|
+
expect(r.error.code).toBe('render-system-owner-out-of-range');
|
|
51
|
+
const d = r.error.detail as OwnerOutOfRangeRoleDetail;
|
|
52
|
+
expect(d.role).toBe('camera');
|
|
53
|
+
expect(d.owner).toBe(99);
|
|
54
|
+
expect(d.worldCount).toBe(2);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("resourceOwner out of range -> code render-system-owner-out-of-range, detail.role === 'resource'", () => {
|
|
59
|
+
// worldCount=2, cameraOwner=0 is valid; resourceOwner=99 is out of range.
|
|
60
|
+
const r = validateDrawArgs(2, { cameraOwner: 0, resourceOwner: 99 });
|
|
61
|
+
expect(r.ok).toBe(false);
|
|
62
|
+
if (!r.ok) {
|
|
63
|
+
expect(r.error.code).toBe('render-system-owner-out-of-range');
|
|
64
|
+
const d = r.error.detail as OwnerOutOfRangeRoleDetail;
|
|
65
|
+
expect(d.role).toBe('resource');
|
|
66
|
+
expect(d.owner).toBe(99);
|
|
67
|
+
expect(d.worldCount).toBe(2);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("both indices out of range -> reports the FIRST offender (camera), detail.role === 'camera'", () => {
|
|
72
|
+
// Both out of range. D-3 / w6 orders the check cameraOwner-before-resource,
|
|
73
|
+
// so the reported role is 'camera' (the first offender) with its own index.
|
|
74
|
+
const r = validateDrawArgs(2, { cameraOwner: 7, resourceOwner: 8 });
|
|
75
|
+
expect(r.ok).toBe(false);
|
|
76
|
+
if (!r.ok) {
|
|
77
|
+
expect(r.error.code).toBe('render-system-owner-out-of-range');
|
|
78
|
+
const d = r.error.detail as OwnerOutOfRangeRoleDetail;
|
|
79
|
+
expect(d.role).toBe('camera');
|
|
80
|
+
expect(d.owner).toBe(7);
|
|
81
|
+
expect(d.worldCount).toBe(2);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|