@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.
Files changed (77) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +234 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/async-form.test-d.d.ts +2 -0
  5. package/dist/__tests__/async-form.test-d.d.ts.map +1 -0
  6. package/dist/__tests__/binding-resource.test-d.d.ts +2 -0
  7. package/dist/__tests__/binding-resource.test-d.d.ts.map +1 -0
  8. package/dist/__tests__/buffer-mapping.test-d.d.ts +2 -0
  9. package/dist/__tests__/buffer-mapping.test-d.d.ts.map +1 -0
  10. package/dist/__tests__/canvasContext.test-d.d.ts +2 -0
  11. package/dist/__tests__/canvasContext.test-d.d.ts.map +1 -0
  12. package/dist/__tests__/caps-three-way.unit.test.d.ts +2 -0
  13. package/dist/__tests__/caps-three-way.unit.test.d.ts.map +1 -0
  14. package/dist/__tests__/caps.test-d.d.ts +2 -0
  15. package/dist/__tests__/caps.test-d.d.ts.map +1 -0
  16. package/dist/__tests__/command-encoder-spec-alignment.test-d.d.ts +2 -0
  17. package/dist/__tests__/command-encoder-spec-alignment.test-d.d.ts.map +1 -0
  18. package/dist/__tests__/createComputePipeline.test-d.d.ts +2 -0
  19. package/dist/__tests__/createComputePipeline.test-d.d.ts.map +1 -0
  20. package/dist/__tests__/createQuerySet.test-d.d.ts +2 -0
  21. package/dist/__tests__/createQuerySet.test-d.d.ts.map +1 -0
  22. package/dist/__tests__/createTextureView.test-d.d.ts +2 -0
  23. package/dist/__tests__/createTextureView.test-d.d.ts.map +1 -0
  24. package/dist/__tests__/descriptor-mirror.test-d.d.ts +2 -0
  25. package/dist/__tests__/descriptor-mirror.test-d.d.ts.map +1 -0
  26. package/dist/__tests__/draw-validation-errors.test.d.ts +2 -0
  27. package/dist/__tests__/draw-validation-errors.test.d.ts.map +1 -0
  28. package/dist/__tests__/errors-instancing.unit.test.d.ts +2 -0
  29. package/dist/__tests__/errors-instancing.unit.test.d.ts.map +1 -0
  30. package/dist/__tests__/errors.test-d.d.ts +2 -0
  31. package/dist/__tests__/errors.test-d.d.ts.map +1 -0
  32. package/dist/__tests__/limit-exceeded-detail.test-d.d.ts +2 -0
  33. package/dist/__tests__/limit-exceeded-detail.test-d.d.ts.map +1 -0
  34. package/dist/__tests__/mapped-buffer.test-d.d.ts +2 -0
  35. package/dist/__tests__/mapped-buffer.test-d.d.ts.map +1 -0
  36. package/dist/__tests__/opaque-handle.test-d.d.ts +2 -0
  37. package/dist/__tests__/opaque-handle.test-d.d.ts.map +1 -0
  38. package/dist/__tests__/owner-out-of-range-role.test.d.ts +2 -0
  39. package/dist/__tests__/owner-out-of-range-role.test.d.ts.map +1 -0
  40. package/dist/__tests__/queue-writes.test-d.d.ts +2 -0
  41. package/dist/__tests__/queue-writes.test-d.d.ts.map +1 -0
  42. package/dist/__tests__/render-pass-encoder-spec-alignment.test-d.d.ts +2 -0
  43. package/dist/__tests__/render-pass-encoder-spec-alignment.test-d.d.ts.map +1 -0
  44. package/dist/__tests__/requestAdapter.test-d.d.ts +2 -0
  45. package/dist/__tests__/requestAdapter.test-d.d.ts.map +1 -0
  46. package/dist/errors.d.ts +315 -0
  47. package/dist/errors.d.ts.map +1 -0
  48. package/dist/errors.mjs +56 -0
  49. package/dist/errors.mjs.map +1 -0
  50. package/dist/index.d.ts +1758 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.mjs +87 -0
  53. package/dist/index.mjs.map +1 -0
  54. package/package.json +61 -0
  55. package/src/__tests__/async-form.test-d.ts +40 -0
  56. package/src/__tests__/binding-resource.test-d.ts +66 -0
  57. package/src/__tests__/buffer-mapping.test-d.ts +84 -0
  58. package/src/__tests__/canvasContext.test-d.ts +106 -0
  59. package/src/__tests__/caps-three-way.unit.test.ts +144 -0
  60. package/src/__tests__/caps.test-d.ts +83 -0
  61. package/src/__tests__/command-encoder-spec-alignment.test-d.ts +117 -0
  62. package/src/__tests__/createComputePipeline.test-d.ts +149 -0
  63. package/src/__tests__/createQuerySet.test-d.ts +81 -0
  64. package/src/__tests__/createTextureView.test-d.ts +127 -0
  65. package/src/__tests__/descriptor-mirror.test-d.ts +284 -0
  66. package/src/__tests__/draw-validation-errors.test.ts +155 -0
  67. package/src/__tests__/errors-instancing.unit.test.ts +167 -0
  68. package/src/__tests__/errors.test-d.ts +199 -0
  69. package/src/__tests__/limit-exceeded-detail.test-d.ts +33 -0
  70. package/src/__tests__/mapped-buffer.test-d.ts +65 -0
  71. package/src/__tests__/opaque-handle.test-d.ts +121 -0
  72. package/src/__tests__/owner-out-of-range-role.test.ts +84 -0
  73. package/src/__tests__/queue-writes.test-d.ts +108 -0
  74. package/src/__tests__/render-pass-encoder-spec-alignment.test-d.ts +202 -0
  75. package/src/__tests__/requestAdapter.test-d.ts +123 -0
  76. package/src/errors.ts +504 -0
  77. package/src/index.ts +2276 -0
@@ -0,0 +1,155 @@
1
+ // feat-20260708-composited-multi-world-rendering M3 / m3-t1 — AC-10 two new
2
+ // RhiErrorCode members + the pure draw-args validation helper.
3
+ //
4
+ // Plan-strategy 2 D-5: renderer.draw(worlds, { cameraOwner, resourceOwner }) validates its inputs at
5
+ // the entry (before any extract) and returns Result.err on:
6
+ // - empty worlds array -> 'render-system-empty-worlds'
7
+ // - owner index out of range -> 'render-system-owner-out-of-range'
8
+ // (.detail = { owner, worldCount })
9
+ //
10
+ // The two codes are NOT mutually exclusive: an empty array with an out-of-range
11
+ // owner surfaces the empty-worlds code first (entry check short-circuits before
12
+ // the owner-range check). requirements AC-10: illegal input is a structured,
13
+ // property-accessible failure (.code / .hint / .detail) — never silent.
14
+ //
15
+ // Why the validation lives in @forgeax/engine-rhi (World-free primitives, not
16
+ // @forgeax/engine-runtime): rhi owns the RhiErrorCode closed union (SSOT), and
17
+ // this package depends only on @forgeax/engine-types — it cannot import the
18
+ // runtime Renderer. `validateDrawArgs(worldCount, owner)` takes plain numbers
19
+ // (no World, no math), so it belongs with the codes it emits; createRenderer's
20
+ // draw entry calls it (D-5 "validate at the draw entry"). This keeps the code
21
+ // + validation + test in one package (architecture-principles §1 SSOT + §3
22
+ // Schema as Contract: the validator is the machine-checkable contract).
23
+ //
24
+ // AC-01 single path: only the explicit camera/resource owner form exists; there
25
+ // is no draw(world) overload. AC-02's owner-required compile-time narrowing is
26
+ // verified at the real hello-triangle callsite (m3-t2), not here.
27
+
28
+ import { describe, expect, expectTypeOf, it } from 'vitest';
29
+ import {
30
+ RhiError,
31
+ type RhiErrorCode,
32
+ type RhiErrorDetail,
33
+ type RhiOwnerOutOfRangeDetail,
34
+ validateDrawArgs,
35
+ } from '../errors';
36
+
37
+ describe('M3 / m3-t1 — two new RhiErrorCode members', () => {
38
+ it("contains 'render-system-empty-worlds' in the closed RhiErrorCode union", () => {
39
+ expectTypeOf<'render-system-empty-worlds'>().toMatchTypeOf<RhiErrorCode>();
40
+ });
41
+
42
+ it("contains 'render-system-owner-out-of-range' in the closed RhiErrorCode union", () => {
43
+ expectTypeOf<'render-system-owner-out-of-range'>().toMatchTypeOf<RhiErrorCode>();
44
+ });
45
+
46
+ it('the two new members are distinct from each other', () => {
47
+ type Empty = Extract<RhiErrorCode, 'render-system-empty-worlds'>;
48
+ type Oob = Extract<RhiErrorCode, 'render-system-owner-out-of-range'>;
49
+ expectTypeOf<Empty>().not.toEqualTypeOf<Oob>();
50
+ });
51
+
52
+ it('exhaustive switch reaches both new members without a default arm', () => {
53
+ // A narrow switch over just the two new members: tsc proves both literals
54
+ // are members of the union (otherwise the case labels would not compile);
55
+ // the runtime assertion proves the mapping is reachable.
56
+ function describeNew(
57
+ code: 'render-system-empty-worlds' | 'render-system-owner-out-of-range',
58
+ ): string {
59
+ switch (code) {
60
+ case 'render-system-empty-worlds':
61
+ return 'empty';
62
+ case 'render-system-owner-out-of-range':
63
+ return 'oob';
64
+ }
65
+ }
66
+ expect(describeNew('render-system-empty-worlds')).toBe('empty');
67
+ expect(describeNew('render-system-owner-out-of-range')).toBe('oob');
68
+ });
69
+ });
70
+
71
+ describe('M3 / m3-t1 — RhiOwnerOutOfRangeDetail shape', () => {
72
+ // feat-20260709-editor-world-partition M1 / w7: the detail grew a `role`
73
+ // discriminator when the single draw owner was split into cameraOwner +
74
+ // resourceOwner (plan-strategy §2 D-3; new SSOT contract in
75
+ // owner-out-of-range-role.test.ts). This precursor shape assertion is
76
+ // migrated to the two-index form — no new error code, only `.detail` grows.
77
+ it('detail equals { role, owner: number, worldCount: number }', () => {
78
+ expectTypeOf<RhiOwnerOutOfRangeDetail>().toEqualTypeOf<{
79
+ readonly role: 'camera' | 'resource';
80
+ readonly owner: number;
81
+ readonly worldCount: number;
82
+ }>();
83
+ });
84
+
85
+ it('detail is a member of the RhiErrorDetail discriminated union', () => {
86
+ const detail: RhiOwnerOutOfRangeDetail = { role: 'resource', owner: 2, worldCount: 1 };
87
+ const widened: RhiErrorDetail = detail;
88
+ expectTypeOf(widened).toMatchTypeOf<RhiErrorDetail>();
89
+ });
90
+ });
91
+
92
+ describe('M3 / m3-t1 — validateDrawArgs (D-5 draw entry validation)', () => {
93
+ it('empty worlds -> err render-system-empty-worlds with a non-empty hint', () => {
94
+ const r = validateDrawArgs(0, { cameraOwner: 0, resourceOwner: 0 });
95
+ expect(r.ok).toBe(false);
96
+ if (!r.ok) {
97
+ expect(r.error.code).toBe('render-system-empty-worlds');
98
+ expect(r.error.hint.length).toBeGreaterThan(0);
99
+ // AI users recover from the hint text (charter P3 actionable hint).
100
+ expect(r.error.hint).toContain('at least one world');
101
+ // empty-worlds carries no detail (the failure is fully described by .code).
102
+ expect(r.error.detail).toBeUndefined();
103
+ }
104
+ });
105
+
106
+ it('owner out of range (owner >= worldCount) -> err with detail { owner, worldCount }', () => {
107
+ const r = validateDrawArgs(1, { cameraOwner: 1, resourceOwner: 0 });
108
+ expect(r.ok).toBe(false);
109
+ if (!r.ok) {
110
+ expect(r.error.code).toBe('render-system-owner-out-of-range');
111
+ expect(r.error.hint.length).toBeGreaterThan(0);
112
+ expect(r.error.hint).toContain('0..worlds.length-1');
113
+ // .detail is reachable via property access after narrowing on .code.
114
+ const d = r.error.detail as RhiOwnerOutOfRangeDetail;
115
+ expect(d.owner).toBe(1);
116
+ expect(d.worldCount).toBe(1);
117
+ }
118
+ });
119
+
120
+ it('owner out of range (owner < 0) -> err render-system-owner-out-of-range', () => {
121
+ const r = validateDrawArgs(2, { cameraOwner: -1, resourceOwner: 0 });
122
+ expect(r.ok).toBe(false);
123
+ if (!r.ok) {
124
+ expect(r.error.code).toBe('render-system-owner-out-of-range');
125
+ const d = r.error.detail as RhiOwnerOutOfRangeDetail;
126
+ expect(d.owner).toBe(-1);
127
+ expect(d.worldCount).toBe(2);
128
+ }
129
+ });
130
+
131
+ it('two codes are non-exclusive: empty array + out-of-range owner short-circuits to empty-worlds', () => {
132
+ // Entry check order: the empty-worlds guard runs first, so an empty array
133
+ // with a nonsense owner (5) never reaches the owner-range branch (D-5).
134
+ const r = validateDrawArgs(0, { cameraOwner: 5, resourceOwner: 5 });
135
+ expect(r.ok).toBe(false);
136
+ if (!r.ok) {
137
+ expect(r.error.code).toBe('render-system-empty-worlds');
138
+ }
139
+ });
140
+
141
+ it('valid args (owner within [0, worldCount)) -> ok', () => {
142
+ expect(validateDrawArgs(1, { cameraOwner: 0, resourceOwner: 0 }).ok).toBe(true);
143
+ expect(validateDrawArgs(3, { cameraOwner: 0, resourceOwner: 0 }).ok).toBe(true);
144
+ expect(validateDrawArgs(3, { cameraOwner: 2, resourceOwner: 2 }).ok).toBe(true);
145
+ });
146
+
147
+ it('returned errors are RhiError instances (structured, not thrown strings)', () => {
148
+ const r = validateDrawArgs(1, { cameraOwner: 9, resourceOwner: 0 });
149
+ expect(r.ok).toBe(false);
150
+ if (!r.ok) {
151
+ expect(r.error).toBeInstanceOf(RhiError);
152
+ expect(r.error.expected.length).toBeGreaterThan(0);
153
+ }
154
+ });
155
+ });
@@ -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
+ });