@forgeax/engine-runtime 0.1.19 → 0.1.21

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 (91) hide show
  1. package/README.md +102 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/create-renderer-gpu-pass-timing.unit.test.d.ts +2 -0
  4. package/dist/__tests__/create-renderer-gpu-pass-timing.unit.test.d.ts.map +1 -0
  5. package/dist/__tests__/helpers/tilemap-assets.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__/material-texture-dimension-mismatch.unit.test.d.ts +2 -0
  9. package/dist/__tests__/material-texture-dimension-mismatch.unit.test.d.ts.map +1 -0
  10. package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -1
  11. package/dist/__tests__/texture-dimensions.browser.test.d.ts +2 -0
  12. package/dist/__tests__/texture-dimensions.browser.test.d.ts.map +1 -0
  13. package/dist/__tests__/volumetric-fog-record-recovery.integration.test.d.ts +2 -0
  14. package/dist/__tests__/volumetric-fog-record-recovery.integration.test.d.ts.map +1 -0
  15. package/dist/createRenderer.d.ts.map +1 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.mjs +3 -1
  18. package/dist/index.mjs.map +1 -1
  19. package/package.json +30 -30
  20. package/src/__tests__/active-camera.unit.test.ts +6 -8
  21. package/src/__tests__/asset-registry.recursive.spec.ts +13 -19
  22. package/src/__tests__/asset.unit.test.ts +28 -25
  23. package/src/__tests__/basis-catalog-dispatch.integration.test.ts +10 -9
  24. package/src/__tests__/clamp-to-last.e2e.browser.test.ts +2 -2
  25. package/src/__tests__/components.test-d.ts +10 -3
  26. package/src/__tests__/components.unit.test.ts +13 -7
  27. package/src/__tests__/compressed-texture-physical-extent.integration.test.ts +12 -10
  28. package/src/__tests__/create-renderer-assembly.integration.test.ts +24 -0
  29. package/src/__tests__/create-renderer-gpu-pass-timing.unit.test.ts +32 -0
  30. package/src/__tests__/createRenderer.browser-no-webgpu.test.ts +66 -0
  31. package/src/__tests__/dawn/hello-sprite-nineslice-section.dawn.test.ts +11 -4
  32. package/src/__tests__/dawn/instances-shadow.dawn.test.ts +15 -2
  33. package/src/__tests__/dawn/material-alpha.dawn.test.ts +2 -3
  34. package/src/__tests__/dawn/mipmap-srgb-linear.dawn.test.ts +2 -3
  35. package/src/__tests__/dawn/per-entity-material-texture-isolation.dawn.test.ts +2 -3
  36. package/src/__tests__/dawn/sprite-nineslice-mesh-bind.dawn.test.ts +2 -3
  37. package/src/__tests__/dawn/upload-texture.dawn.test.ts +4 -6
  38. package/src/__tests__/derive-bgl-integration.test.ts +11 -0
  39. package/src/__tests__/equirect-bc6h.integration.test.ts +6 -4
  40. package/src/__tests__/errors.unit.test.ts +71 -20
  41. package/src/__tests__/extract-frames-merge.test.ts +48 -40
  42. package/src/__tests__/extract-skin-mismatch.unit.test.ts +6 -2
  43. package/src/__tests__/four-path-compression.integration.test.ts +2 -2
  44. package/src/__tests__/frame-targets-shadow-regression.unit.test.ts +13 -0
  45. package/src/__tests__/geometry.unit.test.ts +7 -10
  46. package/src/__tests__/gpu-resource-cow-survivor.test.ts +2 -3
  47. package/src/__tests__/helpers/tilemap-assets.ts +3 -4
  48. package/src/__tests__/light-gpu-byte-neutral.unit.test.ts +6 -3
  49. package/src/__tests__/light-snapshot.test-d.ts +8 -0
  50. package/src/__tests__/lights-preservation.fixup.test.ts +127 -0
  51. package/src/__tests__/lights.unit.test.ts +75 -2214
  52. package/src/__tests__/loadbyguid-texture-intern.unit.test.ts +3 -4
  53. package/src/__tests__/loader-name-survival.test.ts +2 -3
  54. package/src/__tests__/material-texture-dimension-mismatch.unit.test.ts +18 -0
  55. package/src/__tests__/material-texture-uv-scale.unit.test.ts +10 -2
  56. package/src/__tests__/mip-gate.unit.test.ts +11 -8
  57. package/src/__tests__/msaa-sprite-pixel-diff.dawn.test.ts +2 -3
  58. package/src/__tests__/new-kind-refs-contract.test.ts +2 -3
  59. package/src/__tests__/pbr-pipeline.unit.test.ts +29 -5
  60. package/src/__tests__/pipeline.unit.test.ts +66 -221
  61. package/src/__tests__/point-light-shadow.browser.test.ts +208 -83
  62. package/src/__tests__/point-light-shadow.unit.test.ts +67 -9
  63. package/src/__tests__/render-error-exhaustive.test-d.ts +92 -9
  64. package/src/__tests__/render-system-extract.test.ts +4 -6
  65. package/src/__tests__/render-system-mega.test.ts +30 -41
  66. package/src/__tests__/render-system-record-multi-material-textureview.test.ts +8 -7
  67. package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +6 -4
  68. package/src/__tests__/render-system-skin-bg.unit.test.ts +14 -9
  69. package/src/__tests__/renderer-recover.unit.test.ts +8 -0
  70. package/src/__tests__/renderer-surface.unit.test.ts +229 -3
  71. package/src/__tests__/shadow-csm-atlas.test.ts +41 -0
  72. package/src/__tests__/shadow-csm-component.test.ts +31 -24
  73. package/src/__tests__/shadow-csm-extract.test.ts +156 -47
  74. package/src/__tests__/shadow-csm-runtime-vary.dawn.test.ts +72 -0
  75. package/src/__tests__/shadow-csm-shader.dawn.test.ts +19 -0
  76. package/src/__tests__/shadow-csm-ubo.test.ts +41 -45
  77. package/src/__tests__/shadow-csm-viewport.browser.test.ts +29 -0
  78. package/src/__tests__/shadow-fields-observable.dawn.test.ts +140 -58
  79. package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +6 -2
  80. package/src/__tests__/skylight-lazy-projection.test.ts +8 -5
  81. package/src/__tests__/sprite-cullmode-flip.dawn.test.ts +4 -6
  82. package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +4 -2
  83. package/src/__tests__/ssao-bgl.test.ts +10 -16
  84. package/src/__tests__/ssao-passes.test.ts +19 -2
  85. package/src/__tests__/systems.unit.test.ts +59 -33
  86. package/src/__tests__/texture-dimensions.browser.test.ts +127 -0
  87. package/src/__tests__/transcode-fallback.unit.test.ts +5 -1
  88. package/src/__tests__/view-ubo-layout.browser.test.ts +20 -10
  89. package/src/__tests__/volumetric-fog-record-recovery.integration.test.ts +180 -0
  90. package/src/createRenderer.ts +4 -0
  91. package/src/index.ts +1 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeax/engine-runtime",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -26,37 +26,37 @@
26
26
  "LICENSE"
27
27
  ],
28
28
  "dependencies": {
29
- "@forgeax/engine-animation": "0.1.19",
30
- "@forgeax/engine-assets-runtime": "0.1.19",
31
- "@forgeax/engine-audio-webaudio": "0.1.19",
32
- "@forgeax/engine-codec": "0.1.19",
33
- "@forgeax/engine-debug-draw": "0.1.19",
34
- "@forgeax/engine-ecs": "0.1.19",
35
- "@forgeax/engine-font": "0.1.19",
36
- "@forgeax/engine-geometry": "0.1.19",
37
- "@forgeax/engine-graphics-extras": "0.1.19",
38
- "@forgeax/engine-image": "0.1.19",
39
- "@forgeax/engine-input": "0.1.19",
40
- "@forgeax/engine-math": "0.1.19",
41
- "@forgeax/engine-pack": "0.1.19",
42
- "@forgeax/engine-profiler": "0.1.19",
43
- "@forgeax/engine-render": "0.1.19",
44
- "@forgeax/engine-render-graph": "0.1.19",
45
- "@forgeax/engine-rhi": "0.1.19",
46
- "@forgeax/engine-rhi-null": "0.1.19",
47
- "@forgeax/engine-rhi-webgpu": "0.1.19",
48
- "@forgeax/engine-rhi-wgpu": "0.1.19",
49
- "@forgeax/engine-scene": "0.1.19",
50
- "@forgeax/engine-shader": "0.1.19",
51
- "@forgeax/engine-skinning": "0.1.19",
52
- "@forgeax/engine-types": "0.1.19",
53
- "@forgeax/engine-vfx": "0.1.19"
29
+ "@forgeax/engine-animation": "0.1.21",
30
+ "@forgeax/engine-assets-runtime": "0.1.21",
31
+ "@forgeax/engine-audio-webaudio": "0.1.21",
32
+ "@forgeax/engine-codec": "0.1.21",
33
+ "@forgeax/engine-debug-draw": "0.1.21",
34
+ "@forgeax/engine-ecs": "0.1.21",
35
+ "@forgeax/engine-font": "0.1.21",
36
+ "@forgeax/engine-geometry": "0.1.21",
37
+ "@forgeax/engine-graphics-extras": "0.1.21",
38
+ "@forgeax/engine-image": "0.1.21",
39
+ "@forgeax/engine-input": "0.1.21",
40
+ "@forgeax/engine-math": "0.1.21",
41
+ "@forgeax/engine-pack": "0.1.21",
42
+ "@forgeax/engine-profiler": "0.1.21",
43
+ "@forgeax/engine-render": "0.1.21",
44
+ "@forgeax/engine-render-graph": "0.1.21",
45
+ "@forgeax/engine-rhi": "0.1.21",
46
+ "@forgeax/engine-rhi-null": "0.1.21",
47
+ "@forgeax/engine-rhi-webgpu": "0.1.21",
48
+ "@forgeax/engine-rhi-wgpu": "0.1.21",
49
+ "@forgeax/engine-scene": "0.1.21",
50
+ "@forgeax/engine-shader": "0.1.21",
51
+ "@forgeax/engine-skinning": "0.1.21",
52
+ "@forgeax/engine-types": "0.1.21",
53
+ "@forgeax/engine-vfx": "0.1.21"
54
54
  },
55
55
  "devDependencies": {
56
- "@forgeax/engine-import": "0.1.19",
57
- "@forgeax/engine-naga": "0.1.19",
58
- "@forgeax/engine-state": "0.1.19",
59
- "@forgeax/engine-vite-plugin-shader": "0.1.19",
56
+ "@forgeax/engine-import": "0.1.21",
57
+ "@forgeax/engine-naga": "0.1.21",
58
+ "@forgeax/engine-state": "0.1.21",
59
+ "@forgeax/engine-vite-plugin-shader": "0.1.21",
60
60
  "@types/node": "^20.14.0"
61
61
  },
62
62
  "forgeax": {
@@ -147,16 +147,14 @@ describe('w7 — getActiveCamera / setActiveCamera', () => {
147
147
  // ═══════════════════════════════════════════════════════════════════════════════
148
148
 
149
149
  describe('w7 — extractFrame ActiveCamera selection', () => {
150
- it('(2) no ActiveCamera + multiple cameras -> all cameras surface (first-hit fallback preserved)', () => {
150
+ it('(2) no ActiveCamera + multiple display cameras -> first display camera surfaces', () => {
151
151
  const world = new World();
152
152
  const assets = makeAssets();
153
153
  spawnCamera(world, 1.0);
154
154
  spawnCamera(world, 2.0);
155
155
  const frame = extractFrame(world, prepareExtractContext(world, { assets }));
156
- // No selection -> existing behavior: both cameras flow through; record
157
- // stage fires multi-camera diagnostic + uses first hit. We assert extract
158
- // does NOT prune when there is no ActiveCamera.
159
- expect(frame.cameras.length).toBe(2);
156
+ // No selection -> the first display camera remains the display authority.
157
+ expect(frame.cameras.length).toBe(1);
160
158
  });
161
159
 
162
160
  it('(1) ActiveCamera pointing at 2nd camera -> only that camera surfaces', () => {
@@ -171,15 +169,15 @@ describe('w7 — extractFrame ActiveCamera selection', () => {
171
169
  expect(frame.cameras[0]?.fov).toBeCloseTo(2.0, 5);
172
170
  });
173
171
 
174
- it('(3) ActiveCamera pointing at non-existent entity -> fallback to first-hit (all cameras)', () => {
172
+ it('(3) ActiveCamera pointing at non-existent entity -> fallback to first display camera', () => {
175
173
  const world = new World();
176
174
  const assets = makeAssets();
177
175
  spawnCamera(world, 1.0);
178
176
  spawnCamera(world, 2.0);
179
177
  setActiveCamera(world, 999999);
180
178
  const frame = extractFrame(world, prepareExtractContext(world, { assets }));
181
- // Unresolvable -> behave as if no ActiveCamera (do not prune).
182
- expect(frame.cameras.length).toBe(2);
179
+ // Unresolvable -> behave as if no ActiveCamera.
180
+ expect(frame.cameras.length).toBe(1);
183
181
  });
184
182
 
185
183
  it('(4) single camera + ActiveCamera pointing at it -> behavior unchanged (one camera)', () => {
@@ -374,12 +374,11 @@ describe('mesh default material ready boundary', () => {
374
374
  if (refs.length > 0) {
375
375
  reg.catalog(parseGuid(WRONG_DEFAULT_KIND_GUID), {
376
376
  kind: 'texture' as const,
377
- width: 1,
378
- height: 1,
377
+ shape: { viewDimension: '2d', extent: { width: 1, height: 1 } },
379
378
  format: 'rgba8unorm' as const,
380
379
  data: new Uint8Array([255, 255, 255, 255]),
381
380
  colorSpace: 'srgb' as const,
382
- mipmap: false,
381
+ mips: { kind: 'none' as const },
383
382
  });
384
383
  }
385
384
  const omittedDefaultGuid = 'b0000000-0000-4000-b000-000000000099';
@@ -445,12 +444,11 @@ describe('mesh default material ready boundary', () => {
445
444
  const reg = makeRegistry();
446
445
  reg.catalog(parseGuid(WRONG_DEFAULT_KIND_GUID), {
447
446
  kind: 'texture' as const,
448
- width: 1,
449
- height: 1,
447
+ shape: { viewDimension: '2d', extent: { width: 1, height: 1 } },
450
448
  format: 'rgba8unorm' as const,
451
449
  data: new Uint8Array([255, 255, 255, 255]),
452
450
  colorSpace: 'srgb' as const,
453
- mipmap: false,
451
+ mips: { kind: 'none' as const },
454
452
  });
455
453
 
456
454
  const packIndex = [
@@ -740,21 +738,19 @@ describe('AC-02 — material recursive loadByGuid', () => {
740
738
  // Pre-register textures in dev mode (fast-path compatible)
741
739
  reg.catalog(parseGuid(TEXTURE_A_GUID), {
742
740
  kind: 'texture' as const,
743
- width: 4,
744
- height: 4,
741
+ shape: { viewDimension: '2d', extent: { width: 4, height: 4 } },
745
742
  format: 'rgba8unorm' as const,
746
743
  data: new Uint8Array(64),
747
744
  colorSpace: 'srgb' as const,
748
- mipmap: false,
745
+ mips: { kind: 'none' as const },
749
746
  });
750
747
  reg.catalog(parseGuid(TEXTURE_B_GUID), {
751
748
  kind: 'texture' as const,
752
- width: 4,
753
- height: 4,
749
+ shape: { viewDimension: '2d', extent: { width: 4, height: 4 } },
754
750
  format: 'rgba8unorm' as const,
755
751
  data: new Uint8Array(64),
756
752
  colorSpace: 'srgb' as const,
757
- mipmap: false,
753
+ mips: { kind: 'none' as const },
758
754
  });
759
755
 
760
756
  const packIndex = [
@@ -900,12 +896,11 @@ describe('AC-03 — gltf-shaped scene composite (via SceneAsset, D-9)', () => {
900
896
  // Pre-register texture in dev mode.
901
897
  reg.catalog(parseGuid(TEXTURE_C_GUID), {
902
898
  kind: 'texture' as const,
903
- width: 4,
904
- height: 4,
899
+ shape: { viewDimension: '2d', extent: { width: 4, height: 4 } },
905
900
  format: 'rgba8unorm' as const,
906
901
  data: new Uint8Array(64),
907
902
  colorSpace: 'srgb' as const,
908
- mipmap: false,
903
+ mips: { kind: 'none' as const },
909
904
  });
910
905
 
911
906
  const packIndex = [
@@ -1103,12 +1098,11 @@ const LEAF_FIXTURES: readonly LeafFixture[] = [
1103
1098
  kind: 'texture',
1104
1099
  makeAsset: () => ({
1105
1100
  kind: 'texture' as const,
1106
- width: 4,
1107
- height: 4,
1101
+ shape: { viewDimension: '2d', extent: { width: 4, height: 4 } },
1108
1102
  format: 'rgba8unorm' as const,
1109
1103
  data: new Uint8Array(64),
1110
1104
  colorSpace: 'srgb' as const,
1111
- mipmap: false,
1105
+ mips: { kind: 'none' as const },
1112
1106
  }),
1113
1107
  },
1114
1108
  {
@@ -1172,7 +1166,7 @@ const LEAF_FIXTURES: readonly LeafFixture[] = [
1172
1166
  kind: 'render-pipeline',
1173
1167
  makeAsset: () => ({
1174
1168
  kind: 'render-pipeline' as const,
1175
- pipelineId: 'forgeax::urp' as const,
1169
+ pipelineId: 'forgeax::standard' as const,
1176
1170
  }),
1177
1171
  },
1178
1172
  ];
@@ -689,11 +689,10 @@ function _makeStubGPU(): unknown {
689
689
  kind: 'texture',
690
690
  payload: {
691
691
  kind: 'texture',
692
- width: 2,
693
- height: 2,
692
+ shape: { viewDimension: '2d', extent: { width: 2, height: 2 } },
694
693
  format: 'rgba8unorm',
695
694
  colorSpace: 'srgb',
696
- mipmap: false,
695
+ mips: { kind: 'none' },
697
696
  },
698
697
  refs: [],
699
698
  artifacts: {
@@ -706,7 +705,11 @@ function _makeStubGPU(): unknown {
706
705
  const out = await textureLoader.loadPack?.(input, mockCtx());
707
706
  expect(out.ok).toBe(true);
708
707
  if (out.ok) {
709
- expect(out.value).toMatchObject({ kind: 'texture', width: 2, height: 2 });
708
+ expect(out.value).toMatchObject({
709
+ kind: 'texture',
710
+ shape: { viewDimension: '2d', extent: { width: 2, height: 2 } },
711
+ mips: { kind: 'none' },
712
+ });
710
713
  }
711
714
  });
712
715
 
@@ -3414,11 +3417,10 @@ function _makeStubGPU(): unknown {
3414
3417
 
3415
3418
  const TEXTURE_METADATA = {
3416
3419
  kind: 'texture',
3417
- width: 2,
3418
- height: 2,
3420
+ shape: { viewDimension: '2d', extent: { width: 2, height: 2 } },
3419
3421
  format: 'rgba8unorm',
3420
3422
  colorSpace: 'srgb',
3421
- mipmap: false,
3423
+ mips: { kind: 'none' },
3422
3424
  } as const;
3423
3425
 
3424
3426
  function unimportedTextureRow() {
@@ -3731,10 +3733,13 @@ function _makeStubGPU(): unknown {
3731
3733
  guid: GUID_IMPORTED,
3732
3734
  kind: 'texture',
3733
3735
  payload: {
3734
- width: binWidth,
3735
- height: binHeight,
3736
+ shape: {
3737
+ viewDimension: '2d',
3738
+ extent: { width: binWidth, height: binHeight },
3739
+ },
3736
3740
  format: 'rgba16float',
3737
3741
  colorSpace: 'linear',
3742
+ mips: { kind: 'none' },
3738
3743
  },
3739
3744
  refs: [],
3740
3745
  artifacts: {
@@ -3771,8 +3776,10 @@ function _makeStubGPU(): unknown {
3771
3776
  const tex: TextureAsset = result.value;
3772
3777
  expect(tex.format).toBe('rgba16float');
3773
3778
  expect(tex.colorSpace).toBe('linear');
3774
- expect(tex.width).toBe(binWidth);
3775
- expect(tex.height).toBe(binHeight);
3779
+ expect(tex.shape).toEqual({
3780
+ viewDimension: '2d',
3781
+ extent: { width: binWidth, height: binHeight },
3782
+ });
3776
3783
  expect(tex.data.length).toBe(binWidth * binHeight * 4 * 2);
3777
3784
  });
3778
3785
  });
@@ -5544,11 +5551,10 @@ function _makeStubGPU(): unknown {
5544
5551
  kind: 'texture',
5545
5552
  payload: {
5546
5553
  kind: 'texture',
5547
- width: 4,
5548
- height: 4,
5554
+ shape: { viewDimension: '2d', extent: { width: 4, height: 4 } },
5549
5555
  format: 'rgba8unorm',
5550
5556
  colorSpace: 'linear',
5551
- mipmap: false,
5557
+ mips: { kind: 'none' },
5552
5558
  },
5553
5559
  refs: [],
5554
5560
  artifacts: {
@@ -5577,11 +5583,10 @@ function _makeStubGPU(): unknown {
5577
5583
  // loadByGuid returns the payload directly.
5578
5584
  const asset = result.value;
5579
5585
  expect(asset.kind).toBe('texture');
5580
- expect(asset.width).toBe(4);
5581
- expect(asset.height).toBe(4);
5586
+ expect(asset.shape).toEqual({ viewDimension: '2d', extent: { width: 4, height: 4 } });
5582
5587
  expect(asset.format).toBe('rgba8unorm');
5583
5588
  expect(asset.colorSpace).toBe('linear');
5584
- expect(asset.mipmap).toBe(false);
5589
+ expect(asset.mips).toEqual({ kind: 'none' });
5585
5590
  expect(asset.data.byteLength).toBe(4 * 4 * 4);
5586
5591
  expect(asset.data[0]).toBe(0);
5587
5592
  expect(asset.data[63]).toBe(63);
@@ -6040,12 +6045,11 @@ function _makeStubGPU(): unknown {
6040
6045
 
6041
6046
  return {
6042
6047
  kind: 'texture' as const,
6043
- width: 1,
6044
- height: 1,
6048
+ shape: { viewDimension: '2d' as const, extent: { width: 1, height: 1 } },
6045
6049
  format: 'rgba16float',
6046
6050
  data,
6047
6051
  colorSpace: 'linear' as const,
6048
- mipmap: false,
6052
+ mips: { kind: 'none' as const },
6049
6053
  };
6050
6054
  }
6051
6055
 
@@ -6065,8 +6069,8 @@ function _makeStubGPU(): unknown {
6065
6069
  if (got === undefined) return;
6066
6070
  expect(got.format).toBe('rgba16float');
6067
6071
  expect(got.colorSpace).toBe('linear');
6068
- expect(got.width).toBe(1);
6069
- expect(got.height).toBe(1);
6072
+ expect(got.shape).toEqual({ viewDimension: '2d', extent: { width: 1, height: 1 } });
6073
+ expect(got.mips).toEqual({ kind: 'none' });
6070
6074
  expect(got.data.length).toBe(8);
6071
6075
  });
6072
6076
 
@@ -6815,12 +6819,11 @@ function _makeStubGPU(): unknown {
6815
6819
  function makeTexture(format: GPUTextureFormat, colorSpace: 'srgb' | 'linear'): TextureAsset {
6816
6820
  return {
6817
6821
  kind: 'texture',
6818
- width: 1,
6819
- height: 1,
6822
+ shape: { viewDimension: '2d', extent: { width: 1, height: 1 } },
6820
6823
  format,
6821
6824
  data: new Uint8Array([255, 255, 255, 255]),
6822
6825
  colorSpace,
6823
- mipmap: false,
6826
+ mips: { kind: 'none' },
6824
6827
  };
6825
6828
  }
6826
6829
 
@@ -166,10 +166,10 @@ function wireFetch(
166
166
  guid: GUID_TEX,
167
167
  kind: 'texture',
168
168
  payload: {
169
- width: W,
170
- height: H,
169
+ shape: { viewDimension: '2d', extent: { width: W, height: H } },
171
170
  format: fixture.colorSpace === 'srgb' ? 'rgba8unorm-srgb' : 'rgba16float',
172
171
  colorSpace: fixture.colorSpace,
172
+ mips: { kind: 'none' },
173
173
  },
174
174
  refs: [],
175
175
  artifacts: {
@@ -238,10 +238,13 @@ describe.skipIf(!pkgBuilt)('Basis catalog dispatch round-trip (M6 fix)', () => {
238
238
  const result = await loadWith(true);
239
239
  expect(result.ok).toBe(true);
240
240
  if (!result.ok) throw new Error(`load failed: ${result.error.code}`);
241
- const tex = result.value as { kind: string; width: number; height: number; format: string };
241
+ const tex = result.value as {
242
+ kind: string;
243
+ shape: { viewDimension: string; extent: { width: number; height: number } };
244
+ format: string;
245
+ };
242
246
  expect(tex.kind).toBe('texture');
243
- expect(tex.width).toBe(W);
244
- expect(tex.height).toBe(H);
247
+ expect(tex.shape).toEqual({ viewDimension: '2d', extent: { width: W, height: H } });
245
248
  // NO_CAPS -> the transcode arm degrades to the uncompressed sRGB fallback
246
249
  // (section 8 P3), never a scheme=1 reject.
247
250
  expect(tex.format).toBe('rgba8unorm-srgb');
@@ -290,15 +293,13 @@ describe.skipIf(!pkgBuilt)('Basis catalog dispatch round-trip (M6 fix)', () => {
290
293
  if (!result.ok) continue;
291
294
  const texture = result.value as {
292
295
  kind: string;
293
- width: number;
294
- height: number;
296
+ shape: { viewDimension: string; extent: { width: number; height: number } };
295
297
  format: string;
296
298
  data: Uint8Array;
297
299
  colorSpace: string;
298
300
  };
299
301
  expect(texture.kind).toBe('texture');
300
- expect(texture.width).toBe(W);
301
- expect(texture.height).toBe(H);
302
+ expect(texture.shape).toEqual({ viewDimension: '2d', extent: { width: W, height: H } });
302
303
  expect(texture.data.byteLength).toBeGreaterThan(0);
303
304
  expect(texture.colorSpace).toBe(fixture.colorSpace);
304
305
  const expected =
@@ -61,8 +61,8 @@ const SUT_ATTRIBUTABLE_RENDER_CODES: ReadonlySet<string> = new Set([
61
61
  'vertex-storage-buffer-unavailable',
62
62
  'mesh-ssbo-capacity-exceeded',
63
63
  'mesh-ssbo-ceiling-reached',
64
- 'hdrp-light-budget-exceeded',
65
- 'hdrp-index-list-overflow',
64
+ 'standard-light-budget-exceeded',
65
+ 'standard-cluster-index-overflow',
66
66
  ]);
67
67
 
68
68
  // Suppress WebGPU teardown race: chromium fires unhandled OperationError
@@ -67,7 +67,7 @@ describe('w7 type-level - 5 component schemas yield exact data shapes via ShapeO
67
67
  expectTypeOf<{ materials: readonly [0] }>().not.toMatchTypeOf<SpawnData>();
68
68
  });
69
69
 
70
- it('Camera data shape has 20 fields (18 number + clearColor array + autoAspect boolean)', () => {
70
+ it('Camera data shape has 21 fields (18 number + clearColor array + autoAspect boolean + target)', () => {
71
71
  type Data = ShapeOf<SchemaOf<typeof Camera>>;
72
72
  expectTypeOf<keyof Data>().toEqualTypeOf<
73
73
  | 'fov'
@@ -90,6 +90,7 @@ describe('w7 type-level - 5 component schemas yield exact data shapes via ShapeO
90
90
  | 'bloomBlurRadius'
91
91
  | 'clearColor'
92
92
  | 'autoAspect'
93
+ | 'target'
93
94
  >();
94
95
  expectTypeOf<Data['fov']>().toEqualTypeOf<number>();
95
96
  expectTypeOf<Data['far']>().toEqualTypeOf<number>();
@@ -107,9 +108,10 @@ describe('w7 type-level - 5 component schemas yield exact data shapes via ShapeO
107
108
  expectTypeOf<Data['clearColor']>().toEqualTypeOf<Float32Array>();
108
109
  // feat-20260617 / M3: AC-09 -- bool column narrows to boolean, not number.
109
110
  expectTypeOf<Data['autoAspect']>().toEqualTypeOf<boolean>();
111
+ expectTypeOf<Data['target']>().toEqualTypeOf<Handle<'RenderTarget', 'shared'>>();
110
112
  });
111
113
 
112
- it('DirectionalLight data shape: 3 light fields + castShadow bool + 8 merged shadow fields', () => {
114
+ it('DirectionalLight data shape includes closed shadow quality fields', () => {
113
115
  // feat-20260621: DirectionalLightShadow merged into DirectionalLight via castShadow toggle.
114
116
  // shadowDistance replaced the nearPlane/farPlane pair (near derives from camera).
115
117
  // feat-20260709 M2: direction/color collapsed to array<f32,3> columns.
@@ -126,13 +128,18 @@ describe('w7 type-level - 5 component schemas yield exact data shapes via ShapeO
126
128
  | 'depthBias'
127
129
  | 'normalBias'
128
130
  | 'shadowDistance'
129
- | 'pcfKernelSize'
131
+ | 'shadowFilter'
132
+ | 'shadowAngularRadius'
133
+ | 'maxPenumbraTexels'
130
134
  >();
131
135
  expectTypeOf<Data['direction']>().toEqualTypeOf<Float32Array>();
132
136
  expectTypeOf<Data['color']>().toEqualTypeOf<Float32Array>();
133
137
  expectTypeOf<Data['intensity']>().toEqualTypeOf<number>();
134
138
  // bool column narrows to boolean, not number.
135
139
  expectTypeOf<Data['castShadow']>().toEqualTypeOf<boolean>();
140
+ expectTypeOf<Data['shadowFilter']>().toEqualTypeOf<number>();
141
+ expectTypeOf<Data['shadowAngularRadius']>().toEqualTypeOf<number>();
142
+ expectTypeOf<Data['maxPenumbraTexels']>().toEqualTypeOf<number>();
136
143
  });
137
144
  });
138
145
 
@@ -176,9 +176,9 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
176
176
  expect(schemaRecord.materials).toBe('array<shared<MaterialAsset>>');
177
177
  });
178
178
 
179
- it('Camera has 20 fields (17 f32 + historyVersion u32 + clearColor array<f32,4> + autoAspect bool)', () => {
179
+ it('Camera has 21 fields (17 f32 + historyVersion u32 + clearColor array<f32,4> + autoAspect bool + target)', () => {
180
180
  expect(Camera.name).toBe('Camera');
181
- expect(Object.keys(componentSchema(Camera)).length).toBe(20);
181
+ expect(Object.keys(componentSchema(Camera)).length).toBe(21);
182
182
  expect(componentSchema(Camera).fov).toBe('f32');
183
183
  expect(componentSchema(Camera).aspect).toBe('f32');
184
184
  expect(componentSchema(Camera).near).toBe('f32');
@@ -211,13 +211,13 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
211
211
  expect(componentSchema(Camera).autoAspect).toBe('bool');
212
212
  });
213
213
 
214
- it('DirectionalLight has 12 fields: 3 light + castShadow bool + 8 merged shadow f32', () => {
214
+ it('DirectionalLight has 14 fields: 3 light + castShadow bool + closed shadow quality fields', () => {
215
215
  // feat-20260621: DirectionalLightShadow merged into DirectionalLight via castShadow toggle.
216
216
  // shadowDistance replaced the nearPlane/farPlane pair (near derives from camera).
217
217
  // feat-20260709 M2: direction/color collapsed from 6 per-axis scalars to
218
218
  // two array<f32,3> columns (3 light fields: direction + color + intensity).
219
219
  expect(DirectionalLight.name).toBe('DirectionalLight');
220
- expect(Object.keys(componentSchema(DirectionalLight)).length).toBe(12);
220
+ expect(Object.keys(componentSchema(DirectionalLight)).length).toBe(14);
221
221
  // 3 light fields
222
222
  expect(componentSchema(DirectionalLight).direction).toBe('array<f32, 3>');
223
223
  expect(componentSchema(DirectionalLight).color).toBe('array<f32, 3>');
@@ -231,7 +231,10 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
231
231
  expect(componentSchema(DirectionalLight).depthBias).toBe('f32');
232
232
  expect(componentSchema(DirectionalLight).normalBias).toBe('f32');
233
233
  expect(componentSchema(DirectionalLight).shadowDistance).toBe('f32');
234
- expect(componentSchema(DirectionalLight).pcfKernelSize).toBe('f32');
234
+ expect(componentSchema(DirectionalLight).shadowFilter).toBe('enum');
235
+ expect(componentSchema(DirectionalLight).shadowAngularRadius).toBe('f32');
236
+ expect(componentSchema(DirectionalLight).maxPenumbraTexels).toBe('f32');
237
+ expect('pcfKernelSize' in componentSchema(DirectionalLight)).toBe(false);
235
238
  });
236
239
 
237
240
  it('all 5 components are frozen tokens with owner-assigned identities', () => {
@@ -1408,7 +1411,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
1408
1411
  // ────────────────────────────────────────────────────────────────────────────
1409
1412
 
1410
1413
  describe('Camera.fields reflection (w14 AC-07 SSOT)', () => {
1411
- it('Camera.fields has exactly 20 keys matching the Camera column set', () => {
1414
+ it('Camera.fields has exactly 21 keys matching the Camera column set', () => {
1412
1415
  const keys = Object.keys(Camera.fields).sort();
1413
1416
  expect(keys).toEqual([
1414
1417
  'antialias',
@@ -1428,6 +1431,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
1428
1431
  'near',
1429
1432
  'projection',
1430
1433
  'right',
1434
+ 'target',
1431
1435
  'tonemap',
1432
1436
  'top',
1433
1437
  'whitePoint',
@@ -1443,7 +1447,9 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
1443
1447
  ? 'array<f32, 4>'
1444
1448
  : key === 'historyVersion'
1445
1449
  ? 'u32'
1446
- : 'f32';
1450
+ : key === 'target'
1451
+ ? 'shared<RenderTarget>'
1452
+ : 'f32';
1447
1453
  expect(Camera.fields[key].type).toBe(expected);
1448
1454
  }
1449
1455
  });
@@ -79,13 +79,11 @@ function compressedPod(): TextureAsset {
79
79
  bytes.fill(level.level + 1, level.byteOffset, level.byteOffset + level.byteLength);
80
80
  return {
81
81
  kind: 'texture',
82
- width: 2085,
83
- height: 1573,
82
+ shape: { viewDimension: '2d', extent: { width: 2085, height: 1573 } },
84
83
  format: 'bc7-rgba-unorm',
85
84
  data: bytes,
86
85
  colorSpace: 'linear',
87
- mipmap: true,
88
- mipLevelCount: layout.length,
86
+ mips: { kind: 'packed', levelCount: layout.length },
89
87
  };
90
88
  }
91
89
 
@@ -93,13 +91,18 @@ describe('compressed texture physical extent recorded-RHI witness [w36]', () =>
93
91
  it('allocates and uploads BC7 physical extents while preserving logical asset metadata', () => {
94
92
  const calls = { create: [] as TextureCall[], writes: [] as WriteCall[] };
95
93
  const pod = compressedPod();
96
- const expected = deriveMipUploadLayout(pod.format, pod.width, pod.height, numMipLevels(pod));
94
+ const expected = deriveMipUploadLayout(
95
+ pod.format,
96
+ pod.shape.extent.width,
97
+ pod.shape.extent.height,
98
+ pod.mips.kind === 'packed' ? pod.mips.levelCount : 1,
99
+ );
97
100
 
98
101
  const result = makeStore(calls).ensureResident(toShared<'TextureAsset'>(36), pod);
99
102
  if (!result.ok) throw result.error;
100
103
 
101
- expect(pod.width).toBe(2085);
102
- expect(pod.height).toBe(1573);
104
+ expect(pod.shape.extent.width).toBe(2085);
105
+ expect(pod.shape.extent.height).toBe(1573);
103
106
  expect(calls.create).toHaveLength(1);
104
107
  expect(calls.create[0]?.size).toMatchObject({ width: 2088, height: 1576 });
105
108
  expect(calls.writes).toHaveLength(expected.length);
@@ -122,12 +125,11 @@ describe('compressed texture physical extent recorded-RHI witness [w36]', () =>
122
125
  const calls = { create: [] as TextureCall[], writes: [] as WriteCall[] };
123
126
  const pod: TextureAsset = {
124
127
  kind: 'texture',
125
- width: 17,
126
- height: 9,
128
+ shape: { viewDimension: '2d', extent: { width: 17, height: 9 } },
127
129
  format: 'rgba8unorm',
128
130
  data: new Uint8Array(17 * 9 * 4),
129
131
  colorSpace: 'linear',
130
- mipmap: false,
132
+ mips: { kind: 'none' },
131
133
  };
132
134
 
133
135
  expect(makeStore(calls).ensureResident(toShared<'TextureAsset'>(37), pod).ok).toBe(true);
@@ -36,6 +36,30 @@ describe('runtime renderer host assembly', () => {
36
36
  host.value.renderer.dispose();
37
37
  });
38
38
 
39
+ it('repeats null-device assembly and frame observation without device loss', async () => {
40
+ for (let attempt = 0; attempt < 2; attempt += 1) {
41
+ const host = await constructRendererHost(canvas(), { rhi }, { shaderManifestUrl: manifest });
42
+ expect(host.ok).toBe(true);
43
+ if (!host.ok) continue;
44
+ const world = new World();
45
+ const attached = host.value.renderer.attach(world);
46
+ expect(attached.ok).toBe(true);
47
+ if (!attached.ok) {
48
+ host.value.renderer.dispose();
49
+ continue;
50
+ }
51
+ expect(world.update().ok).toBe(true);
52
+ const frame = host.value.renderer.draw(frameRequest(attached.value));
53
+ expect(frame.ok).toBe(true);
54
+ if (frame.ok) {
55
+ expect((await host.value.renderer.observe(frame.value, { include: ['timings'] })).ok).toBe(
56
+ true,
57
+ );
58
+ }
59
+ host.value.renderer.dispose();
60
+ }
61
+ });
62
+
39
63
  it('keeps runtime construction errors structured', async () => {
40
64
  const result = await createRenderer(canvas(), { rhi: undefined });
41
65
  expect(result.ok).toBe(false);
@@ -0,0 +1,32 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import type { GpuPassTimingOptions, RendererOptions } from '@forgeax/engine-render';
4
+ import { describe, expect, it } from 'vitest';
5
+
6
+ describe('runtime GPU pass timing forwarding', () => {
7
+ it('keeps one option object and projects the exact field to Render', () => {
8
+ const gpuPassTiming = { maxPassesPerFrame: 8 } satisfies GpuPassTimingOptions;
9
+ const options = { gpuPassTiming } satisfies RendererOptions;
10
+ expect(options.gpuPassTiming).toBe(gpuPassTiming);
11
+ });
12
+
13
+ it('forwards without owning admission, sessions, handles, or CPU capture', () => {
14
+ const source = readFileSync(
15
+ fileURLToPath(new URL('../createRenderer.ts', import.meta.url)),
16
+ 'utf8',
17
+ );
18
+ expect(source).toContain('gpuPassTiming: options.gpuPassTiming');
19
+ expect(source).not.toContain('createGpuPassTimingSession');
20
+ expect(source).not.toContain('ProfileCapture');
21
+ expect(source).not.toContain('passCatalog');
22
+ });
23
+
24
+ it('keeps the no-option path free of a timing field', () => {
25
+ const source = readFileSync(
26
+ fileURLToPath(new URL('../createRenderer.ts', import.meta.url)),
27
+ 'utf8',
28
+ );
29
+ expect(source).toContain('options === undefined');
30
+ expect(source).toContain('gpuPassTiming: options.gpuPassTiming');
31
+ });
32
+ });