@forgeax/engine-runtime 0.1.21 → 0.1.23

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 (57) hide show
  1. package/README.md +3 -3
  2. package/dist/__tests__/helpers/register-runtime-components.d.ts.map +1 -1
  3. package/dist/__tests__/helpers/register-scene-components.d.ts.map +1 -1
  4. package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -1
  5. package/dist/createRenderer.d.ts.map +1 -1
  6. package/dist/index.mjs +3 -2
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/sprite-param-values.d.ts +1 -1
  9. package/package.json +30 -30
  10. package/src/__tests__/animation-graph-asset-union.test-d.ts +2 -0
  11. package/src/__tests__/asset-brand-and-name-retirement.test.ts +4 -3
  12. package/src/__tests__/asset-registry-scene.test-d.ts +1 -0
  13. package/src/__tests__/asset.unit.test.ts +5 -2
  14. package/src/__tests__/collect-transient-roundtrip.test.ts +14 -3
  15. package/src/__tests__/components.test-d.ts +9 -6
  16. package/src/__tests__/components.unit.test.ts +23 -19
  17. package/src/__tests__/errors.unit.test.ts +3 -0
  18. package/src/__tests__/extract-frames-merge.test.ts +3 -0
  19. package/src/__tests__/helpers/register-runtime-components.ts +9 -1
  20. package/src/__tests__/helpers/register-scene-components.ts +9 -2
  21. package/src/__tests__/light-gpu-byte-neutral.unit.test.ts +8 -10
  22. package/src/__tests__/light-snapshot.test-d.ts +27 -1
  23. package/src/__tests__/lights-preservation.fixup.test.ts +19 -21
  24. package/src/__tests__/lights.unit.test.ts +2076 -1
  25. package/src/__tests__/loader-name-survival.test.ts +8 -9
  26. package/src/__tests__/package-promotion.test.ts +7 -11
  27. package/src/__tests__/pbr-pipeline.unit.test.ts +16 -16
  28. package/src/__tests__/pipeline.unit.test.ts +20 -24
  29. package/src/__tests__/point-light-shadow.browser.test.ts +1 -0
  30. package/src/__tests__/point-light-shadow.unit.test.ts +18 -98
  31. package/src/__tests__/propagate-transforms.dawn.test.ts +15 -16
  32. package/src/__tests__/record-worldid-isolation.test.ts +16 -32
  33. package/src/__tests__/render-error-exhaustive.test-d.ts +10 -0
  34. package/src/__tests__/render-system-extract.test.ts +5 -5
  35. package/src/__tests__/render-system-record-multi-material-textureview.test.ts +1 -0
  36. package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +1 -0
  37. package/src/__tests__/renderer-surface.unit.test.ts +0 -2
  38. package/src/__tests__/resolveName-unified-entry.test.ts +6 -6
  39. package/src/__tests__/resolveName.test.ts +14 -2
  40. package/src/__tests__/shadow-csm-cascade-loadop.test.ts +1 -0
  41. package/src/__tests__/shadow-csm-tile-consistency.test.ts +1 -0
  42. package/src/__tests__/shadow-csm-ubo.test.ts +3 -1
  43. package/src/__tests__/shadow-fields-observable.dawn.test.ts +15 -4
  44. package/src/__tests__/skin-extract-getarrayview-count.unit.test.ts +1 -1
  45. package/src/__tests__/skin-parented-double-transform.test.ts +5 -5
  46. package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +1 -6
  47. package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +11 -13
  48. package/src/__tests__/ssao-bgl.test.ts +2 -1
  49. package/src/__tests__/systems.unit.test.ts +25 -174
  50. package/src/__tests__/template-game-default-api-contract.test.ts +45 -23
  51. package/src/__tests__/tilemap-chunk-extract.propagate-order.test.ts +10 -4
  52. package/src/__tests__/transform-hierarchy-pixel-diff.dawn.test.ts +2 -2
  53. package/src/collect-scene-asset.ts +1 -1
  54. package/src/components/__tests__/transform-vec-schema.test.ts +2 -2
  55. package/src/createRenderer.ts +1 -0
  56. package/src/sprite-param-values.ts +1 -1
  57. package/dist/.tsbuildinfo +0 -1
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // resolveName is the one runtime name-truth function and it delegates to the
4
4
  // stateless deriveAssetName pure function (D-6). Every other name consumer reads
5
- // resolveName or that same deriveAssetName -- none re-implements the XOR rule.
5
+ // resolveName or that same deriveAssetName -- none re-implements the display-name rule.
6
6
  //
7
7
  // This is a grep/fixture test, not a pure runtime assertion: it reads the source
8
8
  // trees and asserts structural unification.
@@ -27,11 +27,11 @@ const buildCatalogSrc = readFileSync(
27
27
  );
28
28
 
29
29
  describe('unified name resolution (AC-04)', () => {
30
- it('deriveAssetName is the single XOR rule definition', () => {
30
+ it('deriveAssetName is the single display-name rule definition', () => {
31
31
  expect(deriveSrc).toContain('export function deriveAssetName');
32
32
  });
33
33
 
34
- it('resolveName delegates to deriveAssetName (no inline XOR re-implementation)', () => {
34
+ it('resolveName delegates to deriveAssetName (no inline re-implementation)', () => {
35
35
  expect(registrySrc).toContain("import { deriveAssetName } from '@forgeax/engine-pack/name'");
36
36
  // resolveName returns the result of deriveAssetName, not a hand-rolled rule.
37
37
  const body = registrySrc.slice(
@@ -55,11 +55,11 @@ describe('unified name resolution (AC-04)', () => {
55
55
  });
56
56
 
57
57
  // M4 expansion (w22 landed): catalog-builder routes through deriveAssetName
58
- it('catalog-builder imports and calls deriveAssetName (no inline XOR)', () => {
58
+ it('catalog-builder imports and calls deriveAssetName (no inline display-name rule)', () => {
59
59
  expect(buildCatalogSrc).toContain("import { deriveAssetName } from './deriveAssetName.js'");
60
60
  expect(buildCatalogSrc).toContain('deriveAssetName(');
61
- // No inline re-implementation of the XOR rule (no hand-rolled basename)
62
- // The single deriveAssetName pure function handles all branches.
61
+ // No inline re-implementation (no hand-rolled basename); the single
62
+ // deriveAssetName pure function handles the display-name rule.
63
63
  expect(buildCatalogSrc.match(/deriveAssetName\(/g)?.length ?? 0).toBeGreaterThanOrEqual(2);
64
64
  });
65
65
  });
@@ -4,8 +4,8 @@
4
4
  // state directly through registerPackage (Risk-2: no pack-index fetch needed).
5
5
  //
6
6
  // Coverage:
7
- // AC-01 — single-asset package: resolveName === basename(path); the stored
8
- // Asset POD carries no `name` property (name is never on the payload).
7
+ // AC-01 — single-asset package: an explicit stored name wins; basename(path)
8
+ // remains the fallback. The Asset POD carries no `name` property.
9
9
  // AC-02 — multi-asset package: resolveName returns each entry's stored name.
10
10
 
11
11
  import { AssetRegistry } from '@forgeax/engine-assets-runtime';
@@ -42,6 +42,18 @@ describe('resolveName single-asset package (AC-01)', () => {
42
42
  expect(stored).toBeDefined();
43
43
  expect(Object.hasOwn(stored as object, 'name')).toBe(false);
44
44
  });
45
+
46
+ it('preserves an explicit authored name for a single-member pack', () => {
47
+ const reg = makeRegistry();
48
+ reg.catalog(parseGuid(SINGLE_GUID), sampler);
49
+ reg._registerPackage(
50
+ 'assets/Materials.pack.json',
51
+ [SINGLE_GUID],
52
+ new Map([[SINGLE_GUID, 'NewMaterial']]),
53
+ );
54
+
55
+ expect(reg.resolveName(SINGLE_GUID)).toBe('NewMaterial');
56
+ });
45
57
  });
46
58
 
47
59
  describe('resolveName multi-asset package (AC-02)', () => {
@@ -215,6 +215,7 @@ async function importEcs(): Promise<{ World: new () => unknown }> {
215
215
 
216
216
  async function importComponents(): Promise<{
217
217
  Transform: unknown;
218
+ GlobalTransform: unknown;
218
219
  MeshFilter: unknown;
219
220
  MeshRenderer: unknown;
220
221
  Camera: unknown;
@@ -225,6 +225,7 @@ async function importEcs(): Promise<{ World: new () => unknown }> {
225
225
 
226
226
  async function importComponents(): Promise<{
227
227
  Transform: unknown;
228
+ GlobalTransform: unknown;
228
229
  MeshFilter: unknown;
229
230
  MeshRenderer: unknown;
230
231
  Camera: unknown;
@@ -269,6 +269,8 @@ describe('directional filter carrier tail-pad wiring (M1-T05)', () => {
269
269
 
270
270
  it('keeps one View binding and removes the retired spot matrix binding', () => {
271
271
  expect(wgslSrc.match(/@group\(0\) @binding\(0\) var<uniform> view/g)).toHaveLength(1);
272
- expect(wgslSrc).not.toMatch(/@group\(0\)\s*@binding\(9\)/);
272
+ expect(wgslSrc).not.toMatch(
273
+ /@group\(0\)\s*@binding\(9\)\s*var<uniform>\s+spotLightViewProj\s*:/,
274
+ );
273
275
  });
274
276
  });
@@ -29,11 +29,18 @@ import { validateDirectionalLightData } from '../../../render/src/components/lig
29
29
  import { constructRuntimeRendererHost } from '../renderer-host';
30
30
  import { drawPublished } from './draw-published';
31
31
 
32
- const WIDTH = 256;
33
- const HEIGHT = 256;
32
+ const LIGHTWEIGHT_DAWN = process.env.FORGEAX_DAWN_LIGHTWEIGHT === '1';
33
+ // The CI observability lane only needs enough settled frames to prove the
34
+ // field-owned pixel/UBO deltas. Keep the full 300-frame, 256x256 window for
35
+ // local and nightly runs, while the isolated PR process uses a bounded 60-frame
36
+ // 128x128 target to avoid retaining a large lavapipe command backlog.
37
+ const SHADOW_FIELDS_RENDER_FRAMES = LIGHTWEIGHT_DAWN ? 60 : 300;
38
+ const WIDTH = LIGHTWEIGHT_DAWN ? 128 : 256;
39
+ const HEIGHT = LIGHTWEIGHT_DAWN ? 128 : 256;
34
40
  const BPR = Math.ceil((WIDTH * 4) / 256) * 256;
35
41
 
36
- // Each falsifier performs four fresh 256x256 Dawn readbacks. Windows' Dawn
42
+ // Each falsifier performs four fresh Dawn readbacks at the active target size
43
+ // (256x256 by default, 128x128 in the lightweight CI lane). Windows' Dawn
37
44
  // prebuild can exceed Vitest's 90-second bound for that sequence while still
38
45
  // completing well inside the nightly platform job's 45-minute budget.
39
46
  const SHADOW_FIELDS_FALSIFIER_TIMEOUT_MS = 300_000;
@@ -355,9 +362,13 @@ async function renderConfig(
355
362
  const w = new World();
356
363
  spawnScene(w, castShadow, depthBias, normalBias, pcf, mapSize);
357
364
  let de = 0;
358
- for (let i = 0; i < 300; i++) {
365
+ for (let i = 0; i < SHADOW_FIELDS_RENDER_FRAMES; i++) {
359
366
  const r = drawPublished(renderer, w);
360
367
  if (!r.ok) de++;
368
+ // Keep the native Dawn carrier paced like a real frame loop. Without a
369
+ // bounded drain, a long shadow-map leg can retain an unbounded lavapipe
370
+ // command/resource backlog before the final fence below.
371
+ if (i % 16 === 15) await dev.queue.onSubmittedWorkDone();
361
372
  }
362
373
  if (de > 0) throw new Error(`draw errors: ${de}`);
363
374
  // rt is created lazily by the canvas configure()/getCurrentTexture path during
@@ -15,7 +15,7 @@ const extractPath = (() => {
15
15
  describe('_getArrayView count (AC-03 gate)', () => {
16
16
  it('keeps skin and instance hot fields on column views', () => {
17
17
  const src = readFileSync(extractPath, 'utf8');
18
- expect(src).toContain("_getArrayView(jointEntity, Transform, 'world')");
18
+ expect(src).toContain("_getArrayView(jointEntity, GlobalTransform, 'world')");
19
19
  expect(src).toContain("_getArrayView(entity, Instances, 'transforms')");
20
20
  expect(src).toContain("_getArrayView(entity, SpriteInstances, 'transforms')");
21
21
  expect(src).toContain("_getArrayView(entity, SpriteInstances, 'regions')");
@@ -34,7 +34,7 @@ import { fileURLToPath } from 'node:url';
34
34
  import { World } from '@forgeax/engine-ecs';
35
35
  import type { Mat4, Vec3 } from '@forgeax/engine-math';
36
36
  import { mat4, vec3 } from '@forgeax/engine-math';
37
- import { ChildOf, propagateTransforms, Transform } from '@forgeax/engine-scene';
37
+ import { ChildOf, GlobalTransform, propagateTransforms, Transform } from '@forgeax/engine-scene';
38
38
  import { Skin } from '@forgeax/engine-skinning';
39
39
  import { describe, expect, it } from 'vitest';
40
40
 
@@ -125,13 +125,13 @@ describe('skin parented double transform (AC-01)', () => {
125
125
  const r = propagateTransforms(world);
126
126
  expect(r.ok).toBe(true);
127
127
 
128
- // Read Transform.world for skin entity and joint entity.
129
- const st = world.get(skinEntity, Transform);
128
+ // Read derived GlobalTransform.world for skin entity and joint entity.
129
+ const st = world.get(skinEntity, GlobalTransform);
130
130
  expect(st.ok).toBe(true);
131
131
  if (!st.ok) return;
132
132
  const meshWorldMat = mat4FromFloat32Array(st.value.world as unknown as Float32Array);
133
133
 
134
- const jt = world.get(jointEntity, Transform);
134
+ const jt = world.get(jointEntity, GlobalTransform);
135
135
  expect(jt.ok).toBe(true);
136
136
  if (!jt.ok) return;
137
137
  const jointWorldMat = mat4FromFloat32Array(jt.value.world as unknown as Float32Array);
@@ -202,7 +202,7 @@ describe('skin parented double transform (AC-01)', () => {
202
202
  const r = propagateTransforms(world);
203
203
  expect(r.ok).toBe(true);
204
204
 
205
- const jt = world.get(jointEntity, Transform);
205
+ const jt = world.get(jointEntity, GlobalTransform);
206
206
  expect(jt.ok).toBe(true);
207
207
  if (!jt.ok) return;
208
208
  const jointWorldMat = mat4FromFloat32Array(jt.value.world as unknown as Float32Array);
@@ -218,12 +218,7 @@ function spawnMixedScene(world: World, castShadow: boolean): void {
218
218
  };
219
219
  // Entity order is intentional: static A -> skinned B -> static C.
220
220
  spawnStaticCaster(-3);
221
- const joint = world
222
- .spawn({
223
- component: Transform,
224
- data: identityTransform([0, 7, 5]),
225
- })
226
- .unwrap();
221
+ const joint = world.spawn({ component: Transform, data: identityTransform([0, 7, 5]) }).unwrap();
227
222
  world.spawn(
228
223
  { component: Transform, data: identityTransform() },
229
224
  { component: MeshFilter, data: { assetHandle: skinMesh } },
@@ -46,22 +46,22 @@ function makeSpec(shaderId: string): PipelineSpec {
46
46
  }
47
47
 
48
48
  describe('sprite-lit BGL byte-identical to sprite (AC-07, w4/w5 close)', () => {
49
- describe('pbr-view BGL (light buffers; binding 1+2)', () => {
50
- it('sprite-lit vs sprite share the same 12-entry pbr-view BGL under storage-buffer caps', () => {
49
+ describe('pbr-view BGL (Cluster lights live in group(2))', () => {
50
+ it('sprite-lit vs sprite share the same 10-entry pbr-view BGL under storage-buffer caps', () => {
51
51
  const spriteSpec = makeSpec('forgeax::sprite');
52
52
  const spriteLitSpec = makeSpec('forgeax::sprite-lit');
53
53
  const sprite = buildBindGroupLayoutDescriptor(spriteSpec, {
54
54
  kind: 'pbr-view',
55
- caps: { storageBuffer: true },
55
+ caps: { storageBuffer: true, extendedLighting: false },
56
56
  });
57
57
  const spriteLit = buildBindGroupLayoutDescriptor(spriteLitSpec, {
58
58
  kind: 'pbr-view',
59
- caps: { storageBuffer: true },
59
+ caps: { storageBuffer: true, extendedLighting: false },
60
60
  });
61
61
  expect(JSON.stringify(spriteLit)).toBe(JSON.stringify(sprite));
62
62
  // binding 10 is the shared Points/Lines view UBO; bindings 11/12 are the
63
63
  // optional SpotLight projector texture and sampler on capable devices.
64
- expect(spriteLit.entries.length).toBe(12);
64
+ expect(spriteLit.entries.length).toBe(10);
65
65
  });
66
66
 
67
67
  it('sprite-lit vs sprite share the same 10-entry pbr-view BGL under uniform fallback caps (AC-10)', () => {
@@ -69,25 +69,23 @@ describe('sprite-lit BGL byte-identical to sprite (AC-07, w4/w5 close)', () => {
69
69
  const spriteLitSpec = makeSpec('forgeax::sprite-lit');
70
70
  const sprite = buildBindGroupLayoutDescriptor(spriteSpec, {
71
71
  kind: 'pbr-view',
72
- caps: { storageBuffer: false },
72
+ caps: { storageBuffer: false, extendedLighting: false },
73
73
  });
74
74
  const spriteLit = buildBindGroupLayoutDescriptor(spriteLitSpec, {
75
75
  kind: 'pbr-view',
76
- caps: { storageBuffer: false },
76
+ caps: { storageBuffer: false, extendedLighting: false },
77
77
  });
78
78
  expect(JSON.stringify(spriteLit)).toBe(JSON.stringify(sprite));
79
79
  });
80
80
 
81
- it('binding 1 + 2 are read-only-storage on storageBuffer=true (point/spot light buffers)', () => {
81
+ it('does not expose point/spot light bindings on storageBuffer=true', () => {
82
82
  const entries = buildPbrViewBglEntries({ storageBuffer: true });
83
- expect(entries[1]?.buffer?.type).toBe('read-only-storage');
84
- expect(entries[2]?.buffer?.type).toBe('read-only-storage');
83
+ expect(entries.some((entry) => entry.binding === 1 || entry.binding === 2)).toBe(false);
85
84
  });
86
85
 
87
- it('binding 1 + 2 fall back to uniform on storageBuffer=false (AC-10 fallback)', () => {
86
+ it('does not expose point/spot light bindings on storageBuffer=false', () => {
88
87
  const entries = buildPbrViewBglEntries({ storageBuffer: false });
89
- expect(entries[1]?.buffer?.type).toBe('uniform');
90
- expect(entries[2]?.buffer?.type).toBe('uniform');
88
+ expect(entries.some((entry) => entry.binding === 1 || entry.binding === 2)).toBe(false);
91
89
  });
92
90
  });
93
91
 
@@ -39,6 +39,7 @@ import {
39
39
  getOrCreateSsaoFallbackTexture,
40
40
  packClusterUniform,
41
41
  } from '../../../render/src/hdrp-buffers';
42
+ import { BYTES_PER_DIRECT_LIGHT_SLOT } from '../../../render/src/light-buffer-layout';
42
43
  import { createHdrpBindGroupLayoutDescriptor } from '../../../render/src/pipeline-spec';
43
44
  import type { RenderSystemRuntime } from '../../../render/src/render-system';
44
45
 
@@ -133,7 +134,7 @@ function fakeHdrpBuffers(): HdrpBuffers {
133
134
  return {
134
135
  device: {} as HdrpBuffers['device'],
135
136
  lightDataBuffer: mockBuffer('hdrp-light-data'),
136
- lightDataBytes: 16384,
137
+ lightDataBytes: 256 * BYTES_PER_DIRECT_LIGHT_SLOT,
137
138
  clusterGridBuffer: mockBuffer('hdrp-cluster-grid'),
138
139
  clusterGridBytes: 1024,
139
140
  lightIndexListBuffer: mockBuffer('hdrp-light-index-list'),
@@ -80,7 +80,13 @@ import {
80
80
  } from '@forgeax/engine-render';
81
81
  import type { BindGroupEntry, Buffer, Sampler, Texture, TextureView } from '@forgeax/engine-rhi';
82
82
  import { ok as rhiOk } from '@forgeax/engine-rhi';
83
- import { ChildOf, Name, propagateTransforms, Transform } from '@forgeax/engine-scene';
83
+ import {
84
+ ChildOf,
85
+ GlobalTransform,
86
+ Name,
87
+ propagateTransforms,
88
+ Transform,
89
+ } from '@forgeax/engine-scene';
84
90
  import { TONEMAP_LUMINANCE_EPSILON } from '@forgeax/engine-shader';
85
91
  import type { SkinErrorCode } from '@forgeax/engine-skinning';
86
92
  import {
@@ -206,11 +212,7 @@ import {
206
212
  } from '../../../render/src/instance-buffer-cache';
207
213
  import { standardPipeline as urpPipeline } from '../../../render/src/pipeline/standard-pipeline';
208
214
  import { ZERO_CAMERA_CLEAR_FALLBACK } from '../../../render/src/record/frame-snapshot';
209
- import {
210
- warnMultiLightDirectional,
211
- warnMultiLightPoint,
212
- warnMultiLightSpot,
213
- } from '../../../render/src/record/helpers';
215
+ import { warnMultiLightDirectional } from '../../../render/src/record/helpers';
214
216
  import type { CameraSnapshot } from '../../../render/src/render-contract';
215
217
  import {
216
218
  type ExtractedLights,
@@ -575,6 +577,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
575
577
  directionalCount: 0,
576
578
  point: [],
577
579
  spot: [],
580
+ rect: [],
578
581
  directionalShadowError: undefined,
579
582
  lightSpaceMatrix: undefined,
580
583
  shadowMapSize: undefined,
@@ -690,8 +693,6 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
690
693
  unlitPipelineHdr: null,
691
694
  shadowFallbackTextureView: { __label: 'shadow-fallback-view' },
692
695
  skylightFallback: null,
693
- pointLightsBuffer: { __label: 'point-lights-buf' },
694
- spotLightsBuffer: { __label: 'spot-lights-buf' },
695
696
  pbrPipelineLayout: { __label: 'pbr-pipeline-layout' },
696
697
  defaultNormalTextureView: { __label: 'default-normal-view' },
697
698
  perPassResources: {
@@ -776,8 +777,6 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
776
777
  warnedZeroLightStandard: false,
777
778
  warnedShadowDisabled: false,
778
779
  warnedMultiLightDirectional: false,
779
- warnedMultiLightPoint: false,
780
- warnedMultiLightSpot: false,
781
780
  warnedSkyboxTonemapNone: false,
782
781
  warnedMissingBaseColorTextureHandles: new Set<number>(),
783
782
  warnedNineSliceScaleEntities: new Set<number>(),
@@ -833,8 +832,6 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
833
832
  warnedZeroLightStandard: false,
834
833
  warnedShadowDisabled: false,
835
834
  warnedMultiLightDirectional: false,
836
- warnedMultiLightPoint: false,
837
- warnedMultiLightSpot: false,
838
835
  warnedSkyboxTonemapNone: false,
839
836
  warnedMissingBaseColorTextureHandles: new Set<number>(),
840
837
  warnedNineSliceScaleEntities: new Set<number>(),
@@ -1318,8 +1315,6 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
1318
1315
  function makeFrameState() {
1319
1316
  return {
1320
1317
  warnedMultiLightDirectional: false,
1321
- warnedMultiLightPoint: false,
1322
- warnedMultiLightSpot: false,
1323
1318
  };
1324
1319
  }
1325
1320
 
@@ -1388,151 +1383,6 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
1388
1383
  }
1389
1384
  });
1390
1385
  });
1391
-
1392
- describe('warnMultiLightPoint (M3 / AC-04 / AC-05 (c))', () => {
1393
- let warnSpy: ReturnType<typeof vi.spyOn>;
1394
-
1395
- beforeEach(() => {
1396
- warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
1397
- });
1398
-
1399
- afterEach(() => {
1400
- warnSpy.mockRestore();
1401
- });
1402
-
1403
- it('first hit fires console.warn (N>4)', () => {
1404
- const fs = makeFrameState();
1405
- warnMultiLightPoint(fs, 5);
1406
- expect(warnSpy).toHaveBeenCalledTimes(1);
1407
- expect(fs.warnedMultiLightPoint).toBe(true);
1408
- const call = warnSpy.mock.calls[0];
1409
- expect(call[1]).toMatchObject({
1410
- code: 'render-system-multi-light',
1411
- expected: 'at most 4 point',
1412
- detail: { type: 'point', got: 5 },
1413
- });
1414
- });
1415
-
1416
- it('no-op when pointCount <= 4', () => {
1417
- const fs = makeFrameState();
1418
- warnMultiLightPoint(fs, 4);
1419
- warnMultiLightPoint(fs, 0);
1420
- expect(warnSpy).toHaveBeenCalledTimes(0);
1421
- expect(fs.warnedMultiLightPoint).toBe(false);
1422
- });
1423
-
1424
- it('warn-once across 6 frames', () => {
1425
- const fs = makeFrameState();
1426
- warnMultiLightPoint(fs, 5);
1427
- expect(warnSpy).toHaveBeenCalledTimes(1);
1428
- for (let i = 0; i < 5; i++) {
1429
- warnMultiLightPoint(fs, 5);
1430
- }
1431
- expect(warnSpy).toHaveBeenCalledTimes(1);
1432
- });
1433
-
1434
- it('production gate suppresses output but flips latch', () => {
1435
- const fs = makeFrameState();
1436
- warnMultiLightPoint(fs, 5, { env: { NODE_ENV: 'production' } });
1437
- expect(warnSpy).toHaveBeenCalledTimes(0);
1438
- expect(fs.warnedMultiLightPoint).toBe(true);
1439
- });
1440
- });
1441
-
1442
- describe('warnMultiLightSpot (M3 / AC-04 / AC-05 (c))', () => {
1443
- let warnSpy: ReturnType<typeof vi.spyOn>;
1444
-
1445
- beforeEach(() => {
1446
- warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
1447
- });
1448
-
1449
- afterEach(() => {
1450
- warnSpy.mockRestore();
1451
- });
1452
-
1453
- it('first hit fires console.warn (N>4)', () => {
1454
- const fs = makeFrameState();
1455
- warnMultiLightSpot(fs, 5);
1456
- expect(warnSpy).toHaveBeenCalledTimes(1);
1457
- expect(fs.warnedMultiLightSpot).toBe(true);
1458
- const call = warnSpy.mock.calls[0];
1459
- expect(call[1]).toMatchObject({
1460
- code: 'render-system-multi-light',
1461
- expected: 'at most 4 spot',
1462
- detail: { type: 'spot', got: 5 },
1463
- });
1464
- });
1465
-
1466
- it('no-op when spotCount <= 4', () => {
1467
- const fs = makeFrameState();
1468
- warnMultiLightSpot(fs, 4);
1469
- warnMultiLightSpot(fs, 0);
1470
- expect(warnSpy).toHaveBeenCalledTimes(0);
1471
- expect(fs.warnedMultiLightSpot).toBe(false);
1472
- });
1473
-
1474
- it('warn-once across 6 frames', () => {
1475
- const fs = makeFrameState();
1476
- warnMultiLightSpot(fs, 5);
1477
- expect(warnSpy).toHaveBeenCalledTimes(1);
1478
- for (let i = 0; i < 5; i++) {
1479
- warnMultiLightSpot(fs, 5);
1480
- }
1481
- expect(warnSpy).toHaveBeenCalledTimes(1);
1482
- });
1483
-
1484
- it('production gate suppresses output but flips latch', () => {
1485
- const fs = makeFrameState();
1486
- warnMultiLightSpot(fs, 5, { env: { NODE_ENV: 'production' } });
1487
- expect(warnSpy).toHaveBeenCalledTimes(0);
1488
- expect(fs.warnedMultiLightSpot).toBe(true);
1489
- });
1490
- });
1491
-
1492
- describe('independent latch isolation (M3 / AC-04 / user Step 5.a)', () => {
1493
- let warnSpy: ReturnType<typeof vi.spyOn>;
1494
-
1495
- beforeEach(() => {
1496
- warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
1497
- });
1498
-
1499
- afterEach(() => {
1500
- warnSpy.mockRestore();
1501
- });
1502
-
1503
- it('firing directional does not silence point', () => {
1504
- const fs = makeFrameState();
1505
- warnMultiLightDirectional(fs, 2);
1506
- expect(fs.warnedMultiLightDirectional).toBe(true);
1507
- expect(fs.warnedMultiLightPoint).toBe(false);
1508
- expect(fs.warnedMultiLightSpot).toBe(false);
1509
- expect(warnSpy).toHaveBeenCalledTimes(1);
1510
- warnMultiLightPoint(fs, 5);
1511
- expect(warnSpy).toHaveBeenCalledTimes(2);
1512
- expect(fs.warnedMultiLightPoint).toBe(true);
1513
- });
1514
-
1515
- it('firing point does not silence spot', () => {
1516
- const fs = makeFrameState();
1517
- warnMultiLightPoint(fs, 5);
1518
- expect(fs.warnedMultiLightPoint).toBe(true);
1519
- expect(fs.warnedMultiLightSpot).toBe(false);
1520
- expect(warnSpy).toHaveBeenCalledTimes(1);
1521
- warnMultiLightSpot(fs, 5);
1522
- expect(warnSpy).toHaveBeenCalledTimes(2);
1523
- expect(fs.warnedMultiLightSpot).toBe(true);
1524
- });
1525
-
1526
- it('pre-set directional latch does not prevent point/spot first fire', () => {
1527
- const fs = makeFrameState();
1528
- fs.warnedMultiLightDirectional = true;
1529
- warnMultiLightPoint(fs, 5);
1530
- warnMultiLightSpot(fs, 5);
1531
- expect(warnSpy).toHaveBeenCalledTimes(2);
1532
- expect(fs.warnedMultiLightPoint).toBe(true);
1533
- expect(fs.warnedMultiLightSpot).toBe(true);
1534
- });
1535
- });
1536
1386
  }
1537
1387
 
1538
1388
  {
@@ -1703,6 +1553,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
1703
1553
 
1704
1554
  async function importComponents(): Promise<{
1705
1555
  Transform: unknown;
1556
+ GlobalTransform: unknown;
1706
1557
  MeshFilter: unknown;
1707
1558
  MeshRenderer: unknown;
1708
1559
  Camera: unknown;
@@ -2770,8 +2621,10 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
2770
2621
  const result = buildPbrPipelineLayouts(device as any, STORAGE_CAPS);
2771
2622
  expect(result.bindGroupLayouts).toHaveLength(4);
2772
2623
  // And the captured pipeline layout descriptor matches.
2773
- expect(device.capturedPipelineLayouts).toHaveLength(1);
2774
- const layout = device.capturedPipelineLayouts[0];
2624
+ expect(device.capturedPipelineLayouts).toHaveLength(2);
2625
+ const layout = device.capturedPipelineLayouts.find(
2626
+ (candidate) => candidate.label === 'pbr-pl',
2627
+ );
2775
2628
  expect(layout?.bindGroupLayouts).toHaveLength(4);
2776
2629
  expect(layout?.label).toBe('pbr-pl');
2777
2630
  });
@@ -3195,6 +3048,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
3195
3048
  directionalCount: 0,
3196
3049
  point: [],
3197
3050
  spot: [],
3051
+ rect: [],
3198
3052
  directionalShadowError: undefined,
3199
3053
  lightSpaceMatrix: undefined,
3200
3054
  shadowMapSize: undefined,
@@ -3479,8 +3333,6 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
3479
3333
  warnedZeroLightStandard: false,
3480
3334
  warnedShadowDisabled: false,
3481
3335
  warnedMultiLightDirectional: false,
3482
- warnedMultiLightPoint: false,
3483
- warnedMultiLightSpot: false,
3484
3336
  warnedSkyboxTonemapNone: false,
3485
3337
  warnedMissingBaseColorTextureHandles: new Set<number>(),
3486
3338
  warnedNineSliceScaleEntities: new Set<number>(),
@@ -3599,8 +3451,6 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
3599
3451
  warnedZeroLightStandard: false,
3600
3452
  warnedShadowDisabled: false,
3601
3453
  warnedMultiLightDirectional: false,
3602
- warnedMultiLightPoint: false,
3603
- warnedMultiLightSpot: false,
3604
3454
  warnedSkyboxTonemapNone: false,
3605
3455
  warnedMissingBaseColorTextureHandles: new Set<number>(),
3606
3456
  warnedNineSliceScaleEntities: new Set<number>(),
@@ -3674,8 +3524,6 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
3674
3524
  warnedZeroLightStandard: false,
3675
3525
  warnedShadowDisabled: false,
3676
3526
  warnedMultiLightDirectional: false,
3677
- warnedMultiLightPoint: false,
3678
- warnedMultiLightSpot: false,
3679
3527
  warnedSkyboxTonemapNone: false,
3680
3528
  warnedMissingBaseColorTextureHandles: new Set<number>(),
3681
3529
  warnedNineSliceScaleEntities: new Set<number>(),
@@ -5148,7 +4996,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
5148
4996
  }
5149
4997
 
5150
4998
  function readWorld(world: World, entity: EntityHandle): Float32Array {
5151
- return world.get(entity, Transform).unwrap().world as Float32Array;
4999
+ return world.get(entity, GlobalTransform).unwrap().world as Float32Array;
5152
5000
  }
5153
5001
 
5154
5002
  function expectMatClose(actual: Float32Array, expected: Float32Array): void {
@@ -5157,7 +5005,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
5157
5005
  }
5158
5006
  }
5159
5007
 
5160
- describe('w7 - propagate writes Transform.world mat4 (AC-05 / AC-06)', () => {
5008
+ describe('w7 - propagate writes GlobalTransform.world mat4 (AC-05 / AC-06)', () => {
5161
5009
  it('root: world == compose(local.TRS) element-wise epsilon<=1e-5 (AC-06)', () => {
5162
5010
  const world = new World();
5163
5011
  const local = trs({ pos: [3, 4, 5], scale: [2, 2, 2] });
@@ -5550,17 +5398,17 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
5550
5398
  });
5551
5399
 
5552
5400
  // feat-20260601 w12/w13: skin joint world matrices now flow from the resolved
5553
- // `Transform.world` column array view (a raw 16-float `Float32Array`, written
5401
+ // `GlobalTransform.world` column array view (a raw 16-float `Float32Array`, written
5554
5402
  // by propagateTransforms) straight into the allocator's `readonly Mat4[]`
5555
5403
  // jointWorlds parameter -- zero recompose from decomposed TRS. The allocator
5556
- // contract is unchanged; this guards that a bare Transform.world-shaped
5404
+ // contract is unchanged; this guards that a bare GlobalTransform.world-shaped
5557
5405
  // Float32Array premultiplies against the IBM exactly like a `mat4.create()`.
5558
- it('accepts a Transform.world-shaped Float32Array as a joint world matrix', () => {
5406
+ it('accepts a GlobalTransform.world-shaped Float32Array as a joint world matrix', () => {
5559
5407
  const { device, written } = mockDevice();
5560
5408
  const alloc = createSkinPaletteAllocator(device, MAX_BINDING);
5561
- // A Transform.world view is a plain 16-float column-major Float32Array.
5409
+ // A GlobalTransform.world view is a plain 16-float column-major Float32Array.
5562
5410
  // Here: translation (0, 3, 0) -- the shape a `world._getArrayView(joint,
5563
- // Transform, 'world')` read returns after propagate.
5411
+ // GlobalTransform, 'world')` read returns after propagate.
5564
5412
  const jointWorldView = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 3, 0, 1]);
5565
5413
  const ibm = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
5566
5414
  const slice = alloc.allocateSlice(1);
@@ -7381,6 +7229,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
7381
7229
 
7382
7230
  async function importComponents(): Promise<{
7383
7231
  Transform: unknown;
7232
+ GlobalTransform: unknown;
7384
7233
  MeshFilter: unknown;
7385
7234
  MeshRenderer: unknown;
7386
7235
  Camera: unknown;
@@ -7698,6 +7547,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
7698
7547
 
7699
7548
  async function importComponents(): Promise<{
7700
7549
  Transform: unknown;
7550
+ GlobalTransform: unknown;
7701
7551
  MeshFilter: unknown;
7702
7552
  MeshRenderer: unknown;
7703
7553
  Camera: unknown;
@@ -8777,6 +8627,7 @@ type ExtractFramesWithPipeline = (
8777
8627
 
8778
8628
  interface IntegrationComponents {
8779
8629
  Transform: unknown;
8630
+ GlobalTransform: unknown;
8780
8631
  MeshFilter: unknown;
8781
8632
  MeshRenderer: unknown;
8782
8633
  Camera: unknown;