@forgeax/engine-runtime 0.1.20 → 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 (86) hide show
  1. package/README.md +105 -3
  2. package/dist/__tests__/create-renderer-gpu-pass-timing.unit.test.d.ts +2 -0
  3. package/dist/__tests__/create-renderer-gpu-pass-timing.unit.test.d.ts.map +1 -0
  4. package/dist/__tests__/helpers/register-runtime-components.d.ts.map +1 -1
  5. package/dist/__tests__/helpers/register-scene-components.d.ts.map +1 -1
  6. package/dist/__tests__/lights-preservation.fixup.test.d.ts +2 -0
  7. package/dist/__tests__/lights-preservation.fixup.test.d.ts.map +1 -0
  8. package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -1
  9. package/dist/createRenderer.d.ts.map +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.mjs +4 -2
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/sprite-param-values.d.ts +1 -1
  14. package/package.json +30 -30
  15. package/src/__tests__/active-camera.unit.test.ts +6 -8
  16. package/src/__tests__/animation-graph-asset-union.test-d.ts +2 -0
  17. package/src/__tests__/asset-brand-and-name-retirement.test.ts +4 -3
  18. package/src/__tests__/asset-registry-scene.test-d.ts +1 -0
  19. package/src/__tests__/asset-registry.recursive.spec.ts +1 -1
  20. package/src/__tests__/asset.unit.test.ts +5 -2
  21. package/src/__tests__/clamp-to-last.e2e.browser.test.ts +2 -2
  22. package/src/__tests__/collect-transient-roundtrip.test.ts +14 -3
  23. package/src/__tests__/components.test-d.ts +19 -9
  24. package/src/__tests__/components.unit.test.ts +36 -26
  25. package/src/__tests__/create-renderer-assembly.integration.test.ts +24 -0
  26. package/src/__tests__/create-renderer-gpu-pass-timing.unit.test.ts +32 -0
  27. package/src/__tests__/createRenderer.browser-no-webgpu.test.ts +66 -0
  28. package/src/__tests__/dawn/hello-sprite-nineslice-section.dawn.test.ts +9 -1
  29. package/src/__tests__/dawn/instances-shadow.dawn.test.ts +15 -2
  30. package/src/__tests__/errors.unit.test.ts +54 -20
  31. package/src/__tests__/extract-frames-merge.test.ts +51 -40
  32. package/src/__tests__/extract-skin-mismatch.unit.test.ts +6 -2
  33. package/src/__tests__/frame-targets-shadow-regression.unit.test.ts +13 -0
  34. package/src/__tests__/geometry.unit.test.ts +7 -10
  35. package/src/__tests__/helpers/register-runtime-components.ts +9 -1
  36. package/src/__tests__/helpers/register-scene-components.ts +9 -2
  37. package/src/__tests__/light-gpu-byte-neutral.unit.test.ts +10 -11
  38. package/src/__tests__/light-snapshot.test-d.ts +35 -1
  39. package/src/__tests__/lights-preservation.fixup.test.ts +125 -0
  40. package/src/__tests__/lights.unit.test.ts +223 -308
  41. package/src/__tests__/loader-name-survival.test.ts +8 -9
  42. package/src/__tests__/package-promotion.test.ts +7 -11
  43. package/src/__tests__/pbr-pipeline.unit.test.ts +22 -17
  44. package/src/__tests__/pipeline.unit.test.ts +68 -226
  45. package/src/__tests__/point-light-shadow.browser.test.ts +209 -83
  46. package/src/__tests__/point-light-shadow.unit.test.ts +24 -47
  47. package/src/__tests__/propagate-transforms.dawn.test.ts +15 -16
  48. package/src/__tests__/record-worldid-isolation.test.ts +16 -32
  49. package/src/__tests__/render-error-exhaustive.test-d.ts +73 -9
  50. package/src/__tests__/render-system-extract.test.ts +5 -5
  51. package/src/__tests__/render-system-mega.test.ts +30 -41
  52. package/src/__tests__/render-system-record-multi-material-textureview.test.ts +5 -1
  53. package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +5 -1
  54. package/src/__tests__/render-system-skin-bg.unit.test.ts +14 -9
  55. package/src/__tests__/renderer-recover.unit.test.ts +8 -0
  56. package/src/__tests__/renderer-surface.unit.test.ts +229 -5
  57. package/src/__tests__/resolveName-unified-entry.test.ts +6 -6
  58. package/src/__tests__/resolveName.test.ts +14 -2
  59. package/src/__tests__/shadow-csm-atlas.test.ts +41 -0
  60. package/src/__tests__/shadow-csm-cascade-loadop.test.ts +1 -0
  61. package/src/__tests__/shadow-csm-component.test.ts +31 -24
  62. package/src/__tests__/shadow-csm-extract.test.ts +156 -47
  63. package/src/__tests__/shadow-csm-runtime-vary.dawn.test.ts +72 -0
  64. package/src/__tests__/shadow-csm-shader.dawn.test.ts +19 -0
  65. package/src/__tests__/shadow-csm-tile-consistency.test.ts +1 -0
  66. package/src/__tests__/shadow-csm-ubo.test.ts +43 -45
  67. package/src/__tests__/shadow-csm-viewport.browser.test.ts +29 -0
  68. package/src/__tests__/shadow-fields-observable.dawn.test.ts +100 -24
  69. package/src/__tests__/skin-extract-getarrayview-count.unit.test.ts +1 -1
  70. package/src/__tests__/skin-parented-double-transform.test.ts +5 -5
  71. package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +4 -7
  72. package/src/__tests__/skylight-lazy-projection.test.ts +8 -5
  73. package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +11 -13
  74. package/src/__tests__/ssao-bgl.test.ts +12 -17
  75. package/src/__tests__/ssao-passes.test.ts +19 -2
  76. package/src/__tests__/systems.unit.test.ts +69 -205
  77. package/src/__tests__/template-game-default-api-contract.test.ts +45 -23
  78. package/src/__tests__/tilemap-chunk-extract.propagate-order.test.ts +10 -4
  79. package/src/__tests__/transform-hierarchy-pixel-diff.dawn.test.ts +2 -2
  80. package/src/__tests__/view-ubo-layout.browser.test.ts +20 -10
  81. package/src/collect-scene-asset.ts +1 -1
  82. package/src/components/__tests__/transform-vec-schema.test.ts +2 -2
  83. package/src/createRenderer.ts +2 -0
  84. package/src/index.ts +1 -2
  85. package/src/sprite-param-values.ts +1 -1
  86. package/dist/.tsbuildinfo +0 -1
@@ -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,
@@ -448,9 +450,9 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
448
450
  ];
449
451
  }
450
452
 
451
- describe('Camera schema (19 fields: 17 f32 + clearColor array + autoAspect bool after w9 + tonemap-mvp + fxaa + bloom + clearColor + aspect-sync extensions)', () => {
452
- it('Camera.schema has 19 fields (17 f32 + clearColor array + autoAspect bool: perspective quartet + projection + ortho quartet + tonemap trio + antialias + bloom quartet + clearColor + autoAspect)', () => {
453
- expect(Object.keys(Camera.fields).length).toBe(20);
453
+ describe('Camera schema (21 fields: 17 f32 + clearColor array + autoAspect bool + target after render-target extension)', () => {
454
+ it('Camera.schema has 21 fields (17 f32 + clearColor array + autoAspect bool + target: perspective quartet + projection + ortho quartet + tonemap trio + antialias + bloom quartet + clearColor + autoAspect)', () => {
455
+ expect(Object.keys(Camera.fields).length).toBe(21);
454
456
  expect(componentFieldType(Camera, 'fov')).toBe('f32');
455
457
  expect(componentFieldType(Camera, 'aspect')).toBe('f32');
456
458
  expect(componentFieldType(Camera, 'near')).toBe('f32');
@@ -476,6 +478,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
476
478
  // feat-20260617-host-engine-contract-and-video-cutscene / M3: aspect-sync
477
479
  // opt-out flag (bool column tier).
478
480
  expect(componentFieldType(Camera, 'autoAspect')).toBe('bool');
481
+ expect(componentFieldType(Camera, 'target')).toBe('shared<RenderTarget>');
479
482
  });
480
483
  });
481
484
 
@@ -574,6 +577,8 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
574
577
  directionalCount: 0,
575
578
  point: [],
576
579
  spot: [],
580
+ rect: [],
581
+ directionalShadowError: undefined,
577
582
  lightSpaceMatrix: undefined,
578
583
  shadowMapSize: undefined,
579
584
  pointShadow: [],
@@ -688,8 +693,6 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
688
693
  unlitPipelineHdr: null,
689
694
  shadowFallbackTextureView: { __label: 'shadow-fallback-view' },
690
695
  skylightFallback: null,
691
- pointLightsBuffer: { __label: 'point-lights-buf' },
692
- spotLightsBuffer: { __label: 'spot-lights-buf' },
693
696
  pbrPipelineLayout: { __label: 'pbr-pipeline-layout' },
694
697
  defaultNormalTextureView: { __label: 'default-normal-view' },
695
698
  perPassResources: {
@@ -767,14 +770,13 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
767
770
  temporalFrameInput: undefined,
768
771
  compiledFrameGraph: null,
769
772
  compiledFrameGraphTopologyKey: null,
773
+ compiledFrameGraphGeneration: 0,
770
774
  retiredCompiledFrameGraphs: new Set(),
771
775
  instanceBuffers: new Map(),
772
776
  transientInstanceBuffers: [],
773
777
  warnedZeroLightStandard: false,
774
778
  warnedShadowDisabled: false,
775
779
  warnedMultiLightDirectional: false,
776
- warnedMultiLightPoint: false,
777
- warnedMultiLightSpot: false,
778
780
  warnedSkyboxTonemapNone: false,
779
781
  warnedMissingBaseColorTextureHandles: new Set<number>(),
780
782
  warnedNineSliceScaleEntities: new Set<number>(),
@@ -787,8 +789,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
787
789
  installedPipelineHandle: 0,
788
790
  activePipeline: urpPipeline,
789
791
  installedPipelineConfig: undefined,
790
- isHdrpActive: false,
791
- hdrpOncePerFrameFired: new Set(),
792
+ standardOncePerFrameFired: new Set(),
792
793
  },
793
794
  { unlit: 0 },
794
795
  { createBindGroup: 0, keys: [] },
@@ -824,14 +825,13 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
824
825
  temporalFrameInput: undefined,
825
826
  compiledFrameGraph: null,
826
827
  compiledFrameGraphTopologyKey: null,
828
+ compiledFrameGraphGeneration: 0,
827
829
  retiredCompiledFrameGraphs: new Set(),
828
830
  instanceBuffers: new Map(),
829
831
  transientInstanceBuffers: [],
830
832
  warnedZeroLightStandard: false,
831
833
  warnedShadowDisabled: false,
832
834
  warnedMultiLightDirectional: false,
833
- warnedMultiLightPoint: false,
834
- warnedMultiLightSpot: false,
835
835
  warnedSkyboxTonemapNone: false,
836
836
  warnedMissingBaseColorTextureHandles: new Set<number>(),
837
837
  warnedNineSliceScaleEntities: new Set<number>(),
@@ -844,8 +844,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
844
844
  installedPipelineHandle: 0,
845
845
  activePipeline: urpPipeline,
846
846
  installedPipelineConfig: undefined,
847
- isHdrpActive: false,
848
- hdrpOncePerFrameFired: new Set(),
847
+ standardOncePerFrameFired: new Set(),
849
848
  },
850
849
  { unlit: 0 },
851
850
  { createBindGroup: 0, keys: [] },
@@ -914,8 +913,9 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
914
913
  // --- from fxaa-pipeline.test.ts ---
915
914
 
916
915
  describe('feat-20260528-fxaa-post-processing M2 w8: FXAA pipeline construction (success path)', () => {
917
- it('D-2: FXAA BGL has exactly 2 entries: @binding(0) texture_2d<f32> + @binding(1) sampler, no UBO', () => {
918
- // plan-strategy D-2 specifies 2-entry BGL with no UBO entry.
916
+ it('D-2: FXAA BGL has exactly 3 entries: texture + sampler + dither policy UBO', () => {
917
+ // FXAA owns a per-pass output-policy UBO so pipeline asset config can
918
+ // disable dither without rebuilding the shader or graph topology.
919
919
  // The sampler entry uses { type: 'filtering' } (linear sampling).
920
920
  const bglEntries = [
921
921
  {
@@ -928,15 +928,21 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
928
928
  visibility: 2, // GPU_SHADER_STAGE_FRAGMENT = 0x2
929
929
  sampler: { type: 'filtering' },
930
930
  },
931
+ {
932
+ binding: 2,
933
+ visibility: 2,
934
+ buffer: { type: 'uniform' },
935
+ },
931
936
  ];
932
- expect(bglEntries).toHaveLength(2);
937
+ expect(bglEntries).toHaveLength(3);
933
938
  expect(bglEntries[0]?.binding).toBe(0);
934
939
  expect(bglEntries[0]?.texture).toBeDefined();
935
940
  expect(bglEntries[1]?.binding).toBe(1);
936
941
  expect(bglEntries[1]?.sampler).toBeDefined();
937
- // D-2 explicitly: no UBO entry (no buffer-type binding).
942
+ expect(bglEntries[2]?.binding).toBe(2);
943
+ expect(bglEntries[2]?.buffer).toEqual({ type: 'uniform' });
938
944
  const hasBuffer = bglEntries.some((e) => 'buffer' in (e as Record<string, unknown>));
939
- expect(hasBuffer).toBe(false);
945
+ expect(hasBuffer).toBe(true);
940
946
  });
941
947
 
942
948
  it('D-3: FXAA pipeline target format = swap-chain storage format (non-srgb, helper Channel 2 truth)', () => {
@@ -1309,8 +1315,6 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
1309
1315
  function makeFrameState() {
1310
1316
  return {
1311
1317
  warnedMultiLightDirectional: false,
1312
- warnedMultiLightPoint: false,
1313
- warnedMultiLightSpot: false,
1314
1318
  };
1315
1319
  }
1316
1320
 
@@ -1379,151 +1383,6 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
1379
1383
  }
1380
1384
  });
1381
1385
  });
1382
-
1383
- describe('warnMultiLightPoint (M3 / AC-04 / AC-05 (c))', () => {
1384
- let warnSpy: ReturnType<typeof vi.spyOn>;
1385
-
1386
- beforeEach(() => {
1387
- warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
1388
- });
1389
-
1390
- afterEach(() => {
1391
- warnSpy.mockRestore();
1392
- });
1393
-
1394
- it('first hit fires console.warn (N>4)', () => {
1395
- const fs = makeFrameState();
1396
- warnMultiLightPoint(fs, 5);
1397
- expect(warnSpy).toHaveBeenCalledTimes(1);
1398
- expect(fs.warnedMultiLightPoint).toBe(true);
1399
- const call = warnSpy.mock.calls[0];
1400
- expect(call[1]).toMatchObject({
1401
- code: 'render-system-multi-light',
1402
- expected: 'at most 4 point',
1403
- detail: { type: 'point', got: 5 },
1404
- });
1405
- });
1406
-
1407
- it('no-op when pointCount <= 4', () => {
1408
- const fs = makeFrameState();
1409
- warnMultiLightPoint(fs, 4);
1410
- warnMultiLightPoint(fs, 0);
1411
- expect(warnSpy).toHaveBeenCalledTimes(0);
1412
- expect(fs.warnedMultiLightPoint).toBe(false);
1413
- });
1414
-
1415
- it('warn-once across 6 frames', () => {
1416
- const fs = makeFrameState();
1417
- warnMultiLightPoint(fs, 5);
1418
- expect(warnSpy).toHaveBeenCalledTimes(1);
1419
- for (let i = 0; i < 5; i++) {
1420
- warnMultiLightPoint(fs, 5);
1421
- }
1422
- expect(warnSpy).toHaveBeenCalledTimes(1);
1423
- });
1424
-
1425
- it('production gate suppresses output but flips latch', () => {
1426
- const fs = makeFrameState();
1427
- warnMultiLightPoint(fs, 5, { env: { NODE_ENV: 'production' } });
1428
- expect(warnSpy).toHaveBeenCalledTimes(0);
1429
- expect(fs.warnedMultiLightPoint).toBe(true);
1430
- });
1431
- });
1432
-
1433
- describe('warnMultiLightSpot (M3 / AC-04 / AC-05 (c))', () => {
1434
- let warnSpy: ReturnType<typeof vi.spyOn>;
1435
-
1436
- beforeEach(() => {
1437
- warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
1438
- });
1439
-
1440
- afterEach(() => {
1441
- warnSpy.mockRestore();
1442
- });
1443
-
1444
- it('first hit fires console.warn (N>4)', () => {
1445
- const fs = makeFrameState();
1446
- warnMultiLightSpot(fs, 5);
1447
- expect(warnSpy).toHaveBeenCalledTimes(1);
1448
- expect(fs.warnedMultiLightSpot).toBe(true);
1449
- const call = warnSpy.mock.calls[0];
1450
- expect(call[1]).toMatchObject({
1451
- code: 'render-system-multi-light',
1452
- expected: 'at most 4 spot',
1453
- detail: { type: 'spot', got: 5 },
1454
- });
1455
- });
1456
-
1457
- it('no-op when spotCount <= 4', () => {
1458
- const fs = makeFrameState();
1459
- warnMultiLightSpot(fs, 4);
1460
- warnMultiLightSpot(fs, 0);
1461
- expect(warnSpy).toHaveBeenCalledTimes(0);
1462
- expect(fs.warnedMultiLightSpot).toBe(false);
1463
- });
1464
-
1465
- it('warn-once across 6 frames', () => {
1466
- const fs = makeFrameState();
1467
- warnMultiLightSpot(fs, 5);
1468
- expect(warnSpy).toHaveBeenCalledTimes(1);
1469
- for (let i = 0; i < 5; i++) {
1470
- warnMultiLightSpot(fs, 5);
1471
- }
1472
- expect(warnSpy).toHaveBeenCalledTimes(1);
1473
- });
1474
-
1475
- it('production gate suppresses output but flips latch', () => {
1476
- const fs = makeFrameState();
1477
- warnMultiLightSpot(fs, 5, { env: { NODE_ENV: 'production' } });
1478
- expect(warnSpy).toHaveBeenCalledTimes(0);
1479
- expect(fs.warnedMultiLightSpot).toBe(true);
1480
- });
1481
- });
1482
-
1483
- describe('independent latch isolation (M3 / AC-04 / user Step 5.a)', () => {
1484
- let warnSpy: ReturnType<typeof vi.spyOn>;
1485
-
1486
- beforeEach(() => {
1487
- warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
1488
- });
1489
-
1490
- afterEach(() => {
1491
- warnSpy.mockRestore();
1492
- });
1493
-
1494
- it('firing directional does not silence point', () => {
1495
- const fs = makeFrameState();
1496
- warnMultiLightDirectional(fs, 2);
1497
- expect(fs.warnedMultiLightDirectional).toBe(true);
1498
- expect(fs.warnedMultiLightPoint).toBe(false);
1499
- expect(fs.warnedMultiLightSpot).toBe(false);
1500
- expect(warnSpy).toHaveBeenCalledTimes(1);
1501
- warnMultiLightPoint(fs, 5);
1502
- expect(warnSpy).toHaveBeenCalledTimes(2);
1503
- expect(fs.warnedMultiLightPoint).toBe(true);
1504
- });
1505
-
1506
- it('firing point does not silence spot', () => {
1507
- const fs = makeFrameState();
1508
- warnMultiLightPoint(fs, 5);
1509
- expect(fs.warnedMultiLightPoint).toBe(true);
1510
- expect(fs.warnedMultiLightSpot).toBe(false);
1511
- expect(warnSpy).toHaveBeenCalledTimes(1);
1512
- warnMultiLightSpot(fs, 5);
1513
- expect(warnSpy).toHaveBeenCalledTimes(2);
1514
- expect(fs.warnedMultiLightSpot).toBe(true);
1515
- });
1516
-
1517
- it('pre-set directional latch does not prevent point/spot first fire', () => {
1518
- const fs = makeFrameState();
1519
- fs.warnedMultiLightDirectional = true;
1520
- warnMultiLightPoint(fs, 5);
1521
- warnMultiLightSpot(fs, 5);
1522
- expect(warnSpy).toHaveBeenCalledTimes(2);
1523
- expect(fs.warnedMultiLightPoint).toBe(true);
1524
- expect(fs.warnedMultiLightSpot).toBe(true);
1525
- });
1526
- });
1527
1386
  }
1528
1387
 
1529
1388
  {
@@ -1694,6 +1553,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
1694
1553
 
1695
1554
  async function importComponents(): Promise<{
1696
1555
  Transform: unknown;
1556
+ GlobalTransform: unknown;
1697
1557
  MeshFilter: unknown;
1698
1558
  MeshRenderer: unknown;
1699
1559
  Camera: unknown;
@@ -1920,10 +1780,10 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
1920
1780
  return 'mesh-ssbo-capacity';
1921
1781
  case 'mesh-ssbo-ceiling-reached':
1922
1782
  return 'mesh-ssbo-ceiling';
1923
- case 'hdrp-light-budget-exceeded':
1924
- return 'hdrp-budget';
1925
- case 'hdrp-index-list-overflow':
1926
- return 'hdrp-overflow';
1783
+ case 'standard-light-budget-exceeded':
1784
+ return 'standard-budget';
1785
+ case 'standard-cluster-index-overflow':
1786
+ return 'standard-overflow';
1927
1787
  }
1928
1788
  }
1929
1789
  expect(exhaustive('skin-joint-count-exceeded')).toBe('joint-count');
@@ -2761,8 +2621,10 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
2761
2621
  const result = buildPbrPipelineLayouts(device as any, STORAGE_CAPS);
2762
2622
  expect(result.bindGroupLayouts).toHaveLength(4);
2763
2623
  // And the captured pipeline layout descriptor matches.
2764
- expect(device.capturedPipelineLayouts).toHaveLength(1);
2765
- const layout = device.capturedPipelineLayouts[0];
2624
+ expect(device.capturedPipelineLayouts).toHaveLength(2);
2625
+ const layout = device.capturedPipelineLayouts.find(
2626
+ (candidate) => candidate.label === 'pbr-pl',
2627
+ );
2766
2628
  expect(layout?.bindGroupLayouts).toHaveLength(4);
2767
2629
  expect(layout?.label).toBe('pbr-pl');
2768
2630
  });
@@ -3186,6 +3048,8 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
3186
3048
  directionalCount: 0,
3187
3049
  point: [],
3188
3050
  spot: [],
3051
+ rect: [],
3052
+ directionalShadowError: undefined,
3189
3053
  lightSpaceMatrix: undefined,
3190
3054
  shadowMapSize: undefined,
3191
3055
  pointShadow: [],
@@ -3368,11 +3232,12 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
3368
3232
  }
3369
3233
 
3370
3234
  function makePipelineState(
3371
- internals: { _fakes: Record<string, unknown> },
3235
+ internals: { _fakes: Record<string, unknown>; device?: unknown },
3372
3236
  hdrAlreadyAllocated: boolean,
3373
3237
  ): unknown {
3374
3238
  const f = internals._fakes;
3375
3239
  return {
3240
+ device: internals.device,
3376
3241
  meshes: new Map(),
3377
3242
  format: 'bgra8unorm',
3378
3243
  colorAttachmentFormat: 'bgra8unorm-srgb',
@@ -3461,14 +3326,13 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
3461
3326
  temporalFrameInput: undefined,
3462
3327
  compiledFrameGraph: null,
3463
3328
  compiledFrameGraphTopologyKey: null,
3329
+ compiledFrameGraphGeneration: 0,
3464
3330
  retiredCompiledFrameGraphs: new Set(),
3465
3331
  instanceBuffers: new Map(),
3466
3332
  transientInstanceBuffers: [],
3467
3333
  warnedZeroLightStandard: false,
3468
3334
  warnedShadowDisabled: false,
3469
3335
  warnedMultiLightDirectional: false,
3470
- warnedMultiLightPoint: false,
3471
- warnedMultiLightSpot: false,
3472
3336
  warnedSkyboxTonemapNone: false,
3473
3337
  warnedMissingBaseColorTextureHandles: new Set<number>(),
3474
3338
  warnedNineSliceScaleEntities: new Set<number>(),
@@ -3481,8 +3345,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
3481
3345
  installedPipelineHandle: 0,
3482
3346
  activePipeline: urpPipeline,
3483
3347
  installedPipelineConfig: undefined,
3484
- isHdrpActive: false,
3485
- hdrpOncePerFrameFired: new Set(),
3348
+ standardOncePerFrameFired: new Set(),
3486
3349
  },
3487
3350
  { unlit: 0 },
3488
3351
  { createBindGroup: 0, keys: [] },
@@ -3582,13 +3445,12 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
3582
3445
  temporalFrameInput: undefined,
3583
3446
  compiledFrameGraph: null,
3584
3447
  compiledFrameGraphTopologyKey: null,
3448
+ compiledFrameGraphGeneration: 0,
3585
3449
  retiredCompiledFrameGraphs: new Set(),
3586
3450
  instanceBuffers,
3587
3451
  warnedZeroLightStandard: false,
3588
3452
  warnedShadowDisabled: false,
3589
3453
  warnedMultiLightDirectional: false,
3590
- warnedMultiLightPoint: false,
3591
- warnedMultiLightSpot: false,
3592
3454
  warnedSkyboxTonemapNone: false,
3593
3455
  warnedMissingBaseColorTextureHandles: new Set<number>(),
3594
3456
  warnedNineSliceScaleEntities: new Set<number>(),
@@ -3601,8 +3463,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
3601
3463
  installedPipelineHandle: 0,
3602
3464
  activePipeline: urpPipeline,
3603
3465
  installedPipelineConfig: undefined,
3604
- isHdrpActive: false,
3605
- hdrpOncePerFrameFired: new Set(),
3466
+ standardOncePerFrameFired: new Set(),
3606
3467
  },
3607
3468
  { unlit: 0 },
3608
3469
  { createBindGroup: 0, keys: [] },
@@ -3657,13 +3518,12 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
3657
3518
  temporalFrameInput: undefined,
3658
3519
  compiledFrameGraph: null,
3659
3520
  compiledFrameGraphTopologyKey: null,
3521
+ compiledFrameGraphGeneration: 0,
3660
3522
  retiredCompiledFrameGraphs: new Set(),
3661
3523
  instanceBuffers,
3662
3524
  warnedZeroLightStandard: false,
3663
3525
  warnedShadowDisabled: false,
3664
3526
  warnedMultiLightDirectional: false,
3665
- warnedMultiLightPoint: false,
3666
- warnedMultiLightSpot: false,
3667
3527
  warnedSkyboxTonemapNone: false,
3668
3528
  warnedMissingBaseColorTextureHandles: new Set<number>(),
3669
3529
  warnedNineSliceScaleEntities: new Set<number>(),
@@ -3676,8 +3536,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
3676
3536
  installedPipelineHandle: 0,
3677
3537
  activePipeline: urpPipeline,
3678
3538
  installedPipelineConfig: undefined,
3679
- isHdrpActive: false,
3680
- hdrpOncePerFrameFired: new Set(),
3539
+ standardOncePerFrameFired: new Set(),
3681
3540
  },
3682
3541
  { unlit: 0 },
3683
3542
  { createBindGroup: 0, keys: [] },
@@ -5137,7 +4996,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
5137
4996
  }
5138
4997
 
5139
4998
  function readWorld(world: World, entity: EntityHandle): Float32Array {
5140
- return world.get(entity, Transform).unwrap().world as Float32Array;
4999
+ return world.get(entity, GlobalTransform).unwrap().world as Float32Array;
5141
5000
  }
5142
5001
 
5143
5002
  function expectMatClose(actual: Float32Array, expected: Float32Array): void {
@@ -5146,7 +5005,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
5146
5005
  }
5147
5006
  }
5148
5007
 
5149
- describe('w7 - propagate writes Transform.world mat4 (AC-05 / AC-06)', () => {
5008
+ describe('w7 - propagate writes GlobalTransform.world mat4 (AC-05 / AC-06)', () => {
5150
5009
  it('root: world == compose(local.TRS) element-wise epsilon<=1e-5 (AC-06)', () => {
5151
5010
  const world = new World();
5152
5011
  const local = trs({ pos: [3, 4, 5], scale: [2, 2, 2] });
@@ -5539,17 +5398,17 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
5539
5398
  });
5540
5399
 
5541
5400
  // feat-20260601 w12/w13: skin joint world matrices now flow from the resolved
5542
- // `Transform.world` column array view (a raw 16-float `Float32Array`, written
5401
+ // `GlobalTransform.world` column array view (a raw 16-float `Float32Array`, written
5543
5402
  // by propagateTransforms) straight into the allocator's `readonly Mat4[]`
5544
5403
  // jointWorlds parameter -- zero recompose from decomposed TRS. The allocator
5545
- // contract is unchanged; this guards that a bare Transform.world-shaped
5404
+ // contract is unchanged; this guards that a bare GlobalTransform.world-shaped
5546
5405
  // Float32Array premultiplies against the IBM exactly like a `mat4.create()`.
5547
- 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', () => {
5548
5407
  const { device, written } = mockDevice();
5549
5408
  const alloc = createSkinPaletteAllocator(device, MAX_BINDING);
5550
- // 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.
5551
5410
  // Here: translation (0, 3, 0) -- the shape a `world._getArrayView(joint,
5552
- // Transform, 'world')` read returns after propagate.
5411
+ // GlobalTransform, 'world')` read returns after propagate.
5553
5412
  const jointWorldView = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 3, 0, 1]);
5554
5413
  const ibm = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
5555
5414
  const slice = alloc.allocateSlice(1);
@@ -7370,6 +7229,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
7370
7229
 
7371
7230
  async function importComponents(): Promise<{
7372
7231
  Transform: unknown;
7232
+ GlobalTransform: unknown;
7373
7233
  MeshFilter: unknown;
7374
7234
  MeshRenderer: unknown;
7375
7235
  Camera: unknown;
@@ -7687,6 +7547,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
7687
7547
 
7688
7548
  async function importComponents(): Promise<{
7689
7549
  Transform: unknown;
7550
+ GlobalTransform: unknown;
7690
7551
  MeshFilter: unknown;
7691
7552
  MeshRenderer: unknown;
7692
7553
  Camera: unknown;
@@ -8336,10 +8197,10 @@ type ExtractFramesWithPipeline = (
8336
8197
  }
8337
8198
 
8338
8199
  // ── feat-20260612-skin-palette-per-frame-upload M3 / m3-1: record-stage ──
8339
- // dynOffset[1] truth-value + skin BG cache miss=1 + hit=N-1 stats counter.
8200
+ // dynOffset[1]/[2] truth-value + skin BG cache miss=1 + hit=N-1 stats counter.
8340
8201
  //
8341
8202
  // Why this block exists:
8342
- // M3 lifts the record-stage `group2DynamicOffsets` second slot from a
8203
+ // M3 lifts the record-stage `group2DynamicOffsets` palette slots from a
8343
8204
  // hard-coded `0` (PR #353 stub) to the per-entity `entry.source.skin
8344
8205
  // .byteOffset` cursor that M2 m2-6 wired into the extract output. The
8345
8206
  // acceptanceCheck calls for the literal text `group2DynamicOffsets[1]
@@ -8351,8 +8212,8 @@ type ExtractFramesWithPipeline = (
8351
8212
  // What the assertions cover:
8352
8213
  // (a) The test-local offset oracle mirrors the production main-pass
8353
8214
  // geometry contract. With skinByteOffset !== undefined the returned
8354
- // tuple's [1] slot equals the supplied byteOffset (the contract:
8355
- // `group2DynamicOffsets[1] === byteOffset`); with skinByteOffset ===
8215
+ // tuple's [1] and [2] slots equal the supplied byteOffset (the
8216
+ // temporal contract); with skinByteOffset ===
8356
8217
  // undefined it returns a length-1 tuple (non-skin paths preserved).
8357
8218
  // (b) N=3 sequential lookups
8358
8219
  // against the same `(meshStorageBuffer, skinPaletteAllocator
@@ -8379,7 +8240,9 @@ type ExtractFramesWithPipeline = (
8379
8240
  skinByteOffset: number | undefined,
8380
8241
  ): readonly number[] => {
8381
8242
  const meshOffset = meshSlotIdx * MESH_PER_ENTITY_STRIDE;
8382
- return skinByteOffset === undefined ? [meshOffset] : [meshOffset, skinByteOffset];
8243
+ return skinByteOffset === undefined
8244
+ ? [meshOffset]
8245
+ : [meshOffset, skinByteOffset, skinByteOffset];
8383
8246
  };
8384
8247
 
8385
8248
  // Skin path: second slot must equal the per-entity byteOffset cursor
@@ -8388,12 +8251,12 @@ type ExtractFramesWithPipeline = (
8388
8251
  // (first allocateSlice), cursor=256 (second 1-joint slice), cursor
8389
8252
  // =16320 (the worst-case 255-joint slice end).
8390
8253
  const skinAt0 = computeSkinGroup2DynOffsets(0, 0);
8391
- expect(skinAt0).toHaveLength(2);
8254
+ expect(skinAt0).toHaveLength(3);
8392
8255
  // group2DynamicOffsets[1] === byteOffset (literal assertion text per acceptanceCheck)
8393
8256
  expect(skinAt0[1]).toBe(0);
8394
8257
 
8395
8258
  const skinAt256 = computeSkinGroup2DynOffsets(1, 256);
8396
- expect(skinAt256).toHaveLength(2);
8259
+ expect(skinAt256).toHaveLength(3);
8397
8260
  // group2DynamicOffsets[1] === byteOffset
8398
8261
  expect(skinAt256[1]).toBe(256);
8399
8262
  // mesh slot continues to follow the 256-byte stride: meshSlotIdx * 256.
@@ -8401,7 +8264,7 @@ type ExtractFramesWithPipeline = (
8401
8264
 
8402
8265
  const byteOffsetWorst = 254 * 256;
8403
8266
  const skinAtEnd = computeSkinGroup2DynOffsets(2, byteOffsetWorst);
8404
- expect(skinAtEnd).toHaveLength(2);
8267
+ expect(skinAtEnd).toHaveLength(3);
8405
8268
  // group2DynamicOffsets[1] === byteOffset
8406
8269
  expect(skinAtEnd[1]).toBe(byteOffsetWorst);
8407
8270
  expect(skinAtEnd[0]).toBe(2 * 256);
@@ -8764,6 +8627,7 @@ type ExtractFramesWithPipeline = (
8764
8627
 
8765
8628
  interface IntegrationComponents {
8766
8629
  Transform: unknown;
8630
+ GlobalTransform: unknown;
8767
8631
  MeshFilter: unknown;
8768
8632
  MeshRenderer: unknown;
8769
8633
  Camera: unknown;