@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
@@ -31,9 +31,8 @@
31
31
  // - packages/runtime/src/__tests__/render-data.test.ts
32
32
  // - packages/runtime/src/__tests__/hdrp-bgl-slots.test.ts
33
33
  // - packages/runtime/src/__tests__/hdrp-caps-gate.test.ts
34
- // - packages/runtime/src/__tests__/hdrp-grid-invalid.test.ts
35
- // - packages/runtime/src/__tests__/hdrp-index-list-overflow-once.test.ts
36
- // - packages/runtime/src/__tests__/hdrp-light-budget.test.ts
34
+ // - packages/runtime/src/__tests__/standard-profile-invalid.test.ts
35
+ // - packages/runtime/src/__tests__/standard-cluster-index-overflow-once.test.ts
37
36
  // - packages/runtime/src/__tests__/hdrp-pipeline-asset-config.test.ts
38
37
  // - packages/runtime/src/__tests__/m7-hdrp-demo-shape.test.ts
39
38
  //
@@ -110,16 +109,14 @@ import {
110
109
  } from '../../../render/src/cluster-binner';
111
110
  import { GpuResidencyCache } from '../../../render/src/device/gpu-residency';
112
111
  import { createEngineMetrics } from '../../../render/src/engine-metrics';
112
+ import { StandardProfileInvalidError } from '../../../render/src/errors/render';
113
113
  import { assertStorageBufferCap } from '../../../render/src/light-buffer-layout';
114
114
  import {
115
115
  buildPbrPipelineLayouts,
116
116
  buildPbrViewBglEntries,
117
117
  type PbrCaps,
118
118
  } from '../../../render/src/pbr-pipeline';
119
- import {
120
- HdrpInstallError,
121
- validateClusterGrid,
122
- } from '../../../render/src/pipeline/standard-pipeline';
119
+ import { validateClusterGrid } from '../../../render/src/pipeline/standard-pipeline';
123
120
  import {
124
121
  buildPipelineForMaterialShader,
125
122
  type PipelineBuilderContext,
@@ -2122,13 +2119,13 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
2122
2119
  // sprite; the test locks the outcome so an inline-logic drift is caught.
2123
2120
  function resolveBootDefinesKey(
2124
2121
  msEntry: MaterialShaderManifestEntry,
2125
- opts: { readonly storageBufferCapable: boolean; readonly isHdrpActive: boolean },
2122
+ opts: { readonly storageBufferCapable: boolean; readonly clusteredLighting: boolean },
2126
2123
  ): string {
2127
2124
  const variantDefines: Record<string, boolean> = {
2128
2125
  STORAGE_BUFFER_AVAILABLE: opts.storageBufferCapable,
2129
2126
  };
2130
2127
  if (msEntry.variants.some((v) => 'CLUSTER_FORWARD_AVAILABLE' in v.defines)) {
2131
- variantDefines.CLUSTER_FORWARD_AVAILABLE = opts.isHdrpActive;
2128
+ variantDefines.CLUSTER_FORWARD_AVAILABLE = opts.clusteredLighting;
2132
2129
  }
2133
2130
  if (msEntry.variants.some((v) => 'PER_INSTANCE_REGION' in v.defines)) {
2134
2131
  variantDefines.PER_INSTANCE_REGION = false;
@@ -2145,7 +2142,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
2145
2142
  const spriteEntry = makeSpriteEntry();
2146
2143
  const definesKey = resolveBootDefinesKey(spriteEntry, {
2147
2144
  storageBufferCapable: true,
2148
- isHdrpActive: false,
2145
+ clusteredLighting: false,
2149
2146
  });
2150
2147
  expect(definesKey).toBe('PER_INSTANCE_REGION=false+STORAGE_BUFFER_AVAILABLE=true');
2151
2148
  const variant = findVariantByKey(spriteEntry, definesKey);
@@ -2162,7 +2159,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
2162
2159
  const spriteEntry = makeSpriteEntry();
2163
2160
  const definesKey = resolveBootDefinesKey(spriteEntry, {
2164
2161
  storageBufferCapable: false,
2165
- isHdrpActive: false,
2162
+ clusteredLighting: false,
2166
2163
  });
2167
2164
  expect(definesKey).toBe('PER_INSTANCE_REGION=false+STORAGE_BUFFER_AVAILABLE=false');
2168
2165
  const variant = findVariantByKey(spriteEntry, definesKey);
@@ -2197,7 +2194,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
2197
2194
  const spriteEntry = makeSpriteEntry();
2198
2195
  const bootDefinesKey = resolveBootDefinesKey(spriteEntry, {
2199
2196
  storageBufferCapable: true,
2200
- isHdrpActive: false,
2197
+ clusteredLighting: false,
2201
2198
  });
2202
2199
  const bootVariant = findVariantByKey(spriteEntry, bootDefinesKey);
2203
2200
  const spriteInstancesVariant = findVariantByKey(spriteEntry, '');
@@ -4466,12 +4463,11 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
4466
4463
  function texturePod(mipmap: boolean, format: GPUTextureFormat = 'rgba8unorm-srgb'): TextureAsset {
4467
4464
  return {
4468
4465
  kind: 'texture',
4469
- width: 2,
4470
- height: 2,
4466
+ shape: { viewDimension: '2d', extent: { width: 2, height: 2 } },
4471
4467
  format,
4472
4468
  data: new Uint8Array(2 * 2 * 4).fill(188),
4473
4469
  colorSpace: format.endsWith('-srgb') ? 'srgb' : 'linear',
4474
- mipmap,
4470
+ mips: { kind: mipmap ? 'generate' : 'none' },
4475
4471
  };
4476
4472
  }
4477
4473
 
@@ -4549,12 +4545,11 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
4549
4545
  ): TextureAsset {
4550
4546
  return {
4551
4547
  kind: 'texture',
4552
- width,
4553
- height,
4548
+ shape: { viewDimension: '2d', extent: { width, height } },
4554
4549
  format,
4555
4550
  data: new Uint8Array(width * height * 4).fill(170),
4556
4551
  colorSpace: format.endsWith('-srgb') ? 'srgb' : 'linear',
4557
- mipmap,
4552
+ mips: { kind: mipmap ? 'generate' : 'none' },
4558
4553
  };
4559
4554
  }
4560
4555
 
@@ -4566,12 +4561,11 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
4566
4561
  ): TextureAsset {
4567
4562
  return {
4568
4563
  kind: 'texture',
4569
- width,
4570
- height,
4564
+ shape: { viewDimension: '2d', extent: { width, height } },
4571
4565
  format,
4572
4566
  data: new Uint8Array(width * height * 8),
4573
4567
  colorSpace,
4574
- mipmap: false,
4568
+ mips: { kind: 'none' },
4575
4569
  };
4576
4570
  }
4577
4571
 
@@ -4658,33 +4652,35 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
4658
4652
  const source = texturePod(false, 'rgba16float');
4659
4653
  const res = deriveRenderDataTexture({
4660
4654
  ...source,
4661
- data: new Uint8Array(source.width * source.height * 8),
4655
+ data: new Uint8Array(source.shape.extent.width * source.shape.extent.height * 8),
4662
4656
  });
4663
4657
  expect(res.ok).toBe(true);
4664
4658
  if (!res.ok) return;
4665
- expect(res.value.bytesPerRow).toBe(source.width * 8);
4659
+ expect(res.value.bytesPerRow).toBe(source.shape.extent.width * 8);
4666
4660
  });
4667
4661
 
4668
4662
  it('derives row pitch from format when an rgba8 decoder retains trailing bytes', () => {
4669
4663
  const source = texturePod(false, 'rgba8unorm');
4670
4664
  const res = deriveRenderDataTexture({
4671
4665
  ...source,
4672
- data: new Uint8Array(source.width * source.height * 4 + source.width),
4666
+ data: new Uint8Array(
4667
+ source.shape.extent.width * source.shape.extent.height * 4 + source.shape.extent.width,
4668
+ ),
4673
4669
  });
4674
4670
  expect(res.ok).toBe(true);
4675
4671
  if (!res.ok) return;
4676
- expect(res.value.bytesPerRow).toBe(source.width * 4);
4672
+ expect(res.value.bytesPerRow).toBe(source.shape.extent.width * 4);
4677
4673
  });
4678
4674
 
4679
4675
  it('derives the row pitch from an rgba32float source payload', () => {
4680
4676
  const source = texturePod(false, 'rgba32float');
4681
4677
  const res = deriveRenderDataTexture({
4682
4678
  ...source,
4683
- data: new Uint8Array(source.width * source.height * 16),
4679
+ data: new Uint8Array(source.shape.extent.width * source.shape.extent.height * 16),
4684
4680
  });
4685
4681
  expect(res.ok).toBe(true);
4686
4682
  if (!res.ok) return;
4687
- expect(res.value.bytesPerRow).toBe(source.width * 16);
4683
+ expect(res.value.bytesPerRow).toBe(source.shape.extent.width * 16);
4688
4684
  });
4689
4685
 
4690
4686
  it('rejects an uncompressed payload shorter than its base mip', () => {
@@ -4935,40 +4931,49 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
4935
4931
  }
4936
4932
 
4937
4933
  {
4938
- // --- from hdrp-grid-invalid.test.ts ---
4939
- describe('hdrp-grid-invalid', () => {
4934
+ // --- from standard-profile-invalid.test.ts ---
4935
+ describe('standard-profile-invalid', () => {
4940
4936
  describe('validateClusterGrid', () => {
4941
4937
  // AC-23 scenario (a): x === 0
4942
- it('rejects x=0 with code hdrp-grid-invalid and hint contains "positive integer"', () => {
4938
+ it('rejects x=0 with code standard-profile-invalid and hint contains "positive integer"', () => {
4943
4939
  const result = validateClusterGrid({ x: 0, y: 9, z: 24 });
4944
4940
  expect(result.ok).toBe(false);
4945
4941
  if (!result.ok) {
4946
- expect(result.error.code).toBe('hdrp-grid-invalid');
4942
+ expect(result.error.code).toBe('standard-profile-invalid');
4947
4943
  expect(result.error.hint).toContain('positive integer');
4948
4944
  expect(result.error.expected).toContain('[1, 64]');
4949
- expect(result.error.detail).toEqual({ x: 0, y: 9, z: 24 });
4945
+ expect(result.error.detail).toEqual({
4946
+ field: 'clusterGrid',
4947
+ actual: { x: 0, y: 9, z: 24 },
4948
+ });
4950
4949
  }
4951
4950
  });
4952
4951
 
4953
4952
  // AC-23 scenario (b): non-integer x=1.5
4954
- it('rejects x=1.5 (non-integer) with code hdrp-grid-invalid', () => {
4953
+ it('rejects x=1.5 (non-integer) with code standard-profile-invalid', () => {
4955
4954
  const result = validateClusterGrid({ x: 1.5, y: 9, z: 24 });
4956
4955
  expect(result.ok).toBe(false);
4957
4956
  if (!result.ok) {
4958
- expect(result.error.code).toBe('hdrp-grid-invalid');
4957
+ expect(result.error.code).toBe('standard-profile-invalid');
4959
4958
  expect(result.error.hint).toContain('positive integer');
4960
- expect(result.error.detail).toEqual({ x: 1.5, y: 9, z: 24 });
4959
+ expect(result.error.detail).toEqual({
4960
+ field: 'clusterGrid',
4961
+ actual: { x: 1.5, y: 9, z: 24 },
4962
+ });
4961
4963
  }
4962
4964
  });
4963
4965
 
4964
4966
  // AC-23 scenario (c): y === -1
4965
- it('rejects y=-1 with code hdrp-grid-invalid', () => {
4967
+ it('rejects y=-1 with code standard-profile-invalid', () => {
4966
4968
  const result = validateClusterGrid({ x: 16, y: -1, z: 24 });
4967
4969
  expect(result.ok).toBe(false);
4968
4970
  if (!result.ok) {
4969
- expect(result.error.code).toBe('hdrp-grid-invalid');
4971
+ expect(result.error.code).toBe('standard-profile-invalid');
4970
4972
  expect(result.error.hint).toContain('positive integer');
4971
- expect(result.error.detail).toEqual({ x: 16, y: -1, z: 24 });
4973
+ expect(result.error.detail).toEqual({
4974
+ field: 'clusterGrid',
4975
+ actual: { x: 16, y: -1, z: 24 },
4976
+ });
4972
4977
  }
4973
4978
  });
4974
4979
 
@@ -4977,10 +4982,13 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
4977
4982
  const result = validateClusterGrid({ x: 16, y: 9, z: 65 });
4978
4983
  expect(result.ok).toBe(false);
4979
4984
  if (!result.ok) {
4980
- expect(result.error.code).toBe('hdrp-grid-invalid');
4985
+ expect(result.error.code).toBe('standard-profile-invalid');
4981
4986
  expect(result.error.hint).toContain('[1, 64]');
4982
4987
  expect(result.error.expected).toContain('[1, 64]');
4983
- expect(result.error.detail).toEqual({ x: 16, y: 9, z: 65 });
4988
+ expect(result.error.detail).toEqual({
4989
+ field: 'clusterGrid',
4990
+ actual: { x: 16, y: 9, z: 65 },
4991
+ });
4984
4992
  }
4985
4993
  });
4986
4994
 
@@ -5002,21 +5010,24 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
5002
5010
  });
5003
5011
  });
5004
5012
 
5005
- describe('HdrpInstallError shape', () => {
5013
+ describe('StandardProfileInvalidError shape', () => {
5006
5014
  // AC-23: charter P3 triple-set .code / .hint / .expected + .detail.{x,y,z}
5007
5015
  it('carries charter P3 triple-set with detail', () => {
5008
- const err = new HdrpInstallError(0, 9, 24);
5009
- expect(err.code).toBe('hdrp-grid-invalid');
5016
+ const err = new StandardProfileInvalidError('clusterGrid invalid', {
5017
+ field: 'clusterGrid',
5018
+ actual: { x: 0, y: 9, z: 24 },
5019
+ });
5020
+ expect(err.code).toBe('standard-profile-invalid');
5010
5021
  expect(err.hint).toContain('positive integer');
5011
5022
  expect(err.expected).toContain('[1, 64]');
5012
- expect(err.detail).toEqual({ x: 0, y: 9, z: 24 });
5023
+ expect(err.detail).toEqual({ field: 'clusterGrid', actual: { x: 0, y: 9, z: 24 } });
5013
5024
  });
5014
5025
  });
5015
5026
  });
5016
5027
  }
5017
5028
 
5018
5029
  {
5019
- // --- from hdrp-index-list-overflow-once.test.ts ---
5030
+ // --- from standard-cluster-index-overflow-once.test.ts ---
5020
5031
  function makeIdentityMat4(): Mat4 {
5021
5032
  return mat4.identity(mat4.create());
5022
5033
  }
@@ -5077,7 +5088,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
5077
5088
  });
5078
5089
  });
5079
5090
 
5080
- // ── hdrp-index-list-overflow RuntimeError class shape ────────────────────────
5091
+ // ── Standard cluster index-overflow RuntimeError class shape ─────────────────
5081
5092
 
5082
5093
  // ── AC-03 guardrail: URP LIGHT_ARRAY_MAX_SLOTS=4 + 5 PointLights ─────────────
5083
5094
  //
@@ -5091,180 +5102,14 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
5091
5102
  expect(LIGHT_ARRAY_MAX_SLOTS).toBe(4);
5092
5103
  });
5093
5104
 
5094
- it('POINT_LIGHT_STD430_BYTES and SPOT_LIGHT_STD430_BYTES still match URP specs', async () => {
5105
+ it('POINT_LIGHT_STD430_BYTES and SPOT_LIGHT_STD430_BYTES match the current URP payloads', async () => {
5095
5106
  const { POINT_LIGHT_STD430_BYTES, SPOT_LIGHT_STD430_BYTES } = await import(
5096
5107
  '../../../render/src/light-buffer-layout'
5097
5108
  );
5098
5109
  expect(POINT_LIGHT_STD430_BYTES).toBe(32);
5099
- // feat-20260625-spot-light-shadow-mapping M2 / w8 (D-4): SpotLight grew
5100
- // 48 -> 64 for the shadowAtlasTile i32 clip-signal lane (4th vec4 column).
5101
- expect(SPOT_LIGHT_STD430_BYTES).toBe(64);
5102
- });
5103
- });
5104
- }
5105
-
5106
- {
5107
- // --- from hdrp-light-budget.test.ts ---
5108
-
5109
- // ── Budget gate unit logic (the pure gate w23 will embed) ─────────────────────
5110
-
5111
- const HDRP_BUDGET = 256;
5112
-
5113
- function enforceLightBudget(
5114
- totalLightCount: number,
5115
- oncePerFrameFired: Set<string>,
5116
- fire: (code: string, errors: unknown[]) => void,
5117
- ): number {
5118
- if (totalLightCount <= HDRP_BUDGET) {
5119
- return totalLightCount;
5120
- }
5121
- // Truncate to budget.
5122
- const truncated = HDRP_BUDGET;
5123
- if (!oncePerFrameFired.has('hdrp-light-budget-exceeded')) {
5124
- oncePerFrameFired.add('hdrp-light-budget-exceeded');
5125
- fire('hdrp-light-budget-exceeded', [totalLightCount, HDRP_BUDGET]);
5126
- }
5127
- return truncated;
5128
- }
5129
-
5130
- describe('HDRP light budget enforcement gate (AC-06/AC-07)', () => {
5131
- it('256 lights: no fire, returns 256 (AC-06 boundary)', () => {
5132
- const fired = new Set<string>();
5133
- const errors: Array<{ code: string; actual: number; budget: number }> = [];
5134
- const fireFn = (_code: string, args: unknown[]) => {
5135
- errors.push({
5136
- code: 'hdrp-light-budget-exceeded',
5137
- actual: args[0] as number,
5138
- budget: args[1] as number,
5139
- });
5140
- };
5141
-
5142
- const result = enforceLightBudget(256, fired, fireFn);
5143
- expect(result).toBe(256);
5144
- expect(fired.size).toBe(0);
5145
- expect(errors.length).toBe(0);
5146
- });
5147
-
5148
- it('257 lights: fires once, returns 256 (AC-07 truncation)', () => {
5149
- const fired = new Set<string>();
5150
- const errors: Array<{ code: string; actual: number; budget: number }> = [];
5151
- const fireFn = (_code: string, args: unknown[]) => {
5152
- errors.push({
5153
- code: 'hdrp-light-budget-exceeded',
5154
- actual: args[0] as number,
5155
- budget: args[1] as number,
5156
- });
5157
- };
5158
-
5159
- const result = enforceLightBudget(257, fired, fireFn);
5160
- expect(result).toBe(256);
5161
- expect(fired.size).toBe(1);
5162
- expect(fired.has('hdrp-light-budget-exceeded')).toBe(true);
5163
- expect(errors.length).toBe(1);
5164
- expect(errors[0]?.actual).toBe(257);
5165
- expect(errors[0]?.budget).toBe(256);
5166
- });
5167
-
5168
- it('same-frame subsequent overflow suppressed (warn-once-per-frame-bucket)', () => {
5169
- // Simulate frame already having fired the budget error.
5170
- const fired = new Set<string>(['hdrp-light-budget-exceeded']);
5171
- const errors: Array<{ code: string; actual: number; budget: number }> = [];
5172
- const fireFn = (_code: string, args: unknown[]) => {
5173
- errors.push({
5174
- code: 'hdrp-light-budget-exceeded',
5175
- actual: args[0] as number,
5176
- budget: args[1] as number,
5177
- });
5178
- };
5179
-
5180
- // Even with 300 lights on the same frame, it should not fire again.
5181
- const result = enforceLightBudget(300, fired, fireFn);
5182
- expect(result).toBe(256);
5183
- expect(errors.length).toBe(0);
5184
- });
5185
-
5186
- it('second frame fires again (fresh bucket)', () => {
5187
- // New frame: empty Set.
5188
- const fired = new Set<string>();
5189
- const errors: Array<{ code: string; actual: number; budget: number }> = [];
5190
- const fireFn = (_code: string, args: unknown[]) => {
5191
- errors.push({
5192
- code: 'hdrp-light-budget-exceeded',
5193
- actual: args[0] as number,
5194
- budget: args[1] as number,
5195
- });
5196
- };
5197
-
5198
- const result = enforceLightBudget(258, fired, fireFn);
5199
- expect(result).toBe(256);
5200
- expect(errors.length).toBe(1);
5201
- expect(errors[0]?.actual).toBe(258);
5202
- expect(errors[0]?.budget).toBe(256);
5203
- });
5204
-
5205
- it('second frame with exactly 256 does not fire (fresh bucket, under budget)', () => {
5206
- const fired = new Set<string>();
5207
- const errors: Array<{ code: string; actual: number; budget: number }> = [];
5208
- const fireFn = (_code: string, args: unknown[]) => {
5209
- errors.push({
5210
- code: 'hdrp-light-budget-exceeded',
5211
- actual: args[0] as number,
5212
- budget: args[1] as number,
5213
- });
5214
- };
5215
-
5216
- const result = enforceLightBudget(256, fired, fireFn);
5217
- expect(result).toBe(256);
5218
- expect(errors.length).toBe(0);
5219
- });
5220
-
5221
- it('0 lights returns 0 (empty scene, no fire)', () => {
5222
- const fired = new Set<string>();
5223
- const errors: Array<{ code: string; actual: number; budget: number }> = [];
5224
- const fireFn = (_code: string, args: unknown[]) => {
5225
- errors.push({
5226
- code: 'hdrp-light-budget-exceeded',
5227
- actual: args[0] as number,
5228
- budget: args[1] as number,
5229
- });
5230
- };
5231
-
5232
- const result = enforceLightBudget(0, fired, fireFn);
5233
- expect(result).toBe(0);
5234
- expect(errors.length).toBe(0);
5235
- });
5236
-
5237
- it('HDRP_BUDGET constant must be 256 (D-scope anchor)', () => {
5238
- expect(HDRP_BUDGET).toBe(256);
5239
- });
5240
- });
5241
-
5242
- // ── Integration pattern: once-per-frame Set complements existing hdrpOncePerFrameFired ──
5243
-
5244
- describe('hdrpOncePerFrameFired Set dedup semantics', () => {
5245
- it('empty set allows first fire', () => {
5246
- const fired = new Set<string>();
5247
- expect(fired.has('hdrp-light-budget-exceeded')).toBe(false);
5248
- });
5249
-
5250
- it('set with entry suppresses subsequent fires on the same code', () => {
5251
- const fired = new Set<string>(['hdrp-light-budget-exceeded']);
5252
- expect(fired.has('hdrp-light-budget-exceeded')).toBe(true);
5253
- });
5254
-
5255
- it('hdrp-light-budget-exceeded is independent from hdrp-index-list-overflow', () => {
5256
- const fired = new Set<string>(['hdrp-index-list-overflow']);
5257
- // Budget-exceeded should still fire even if index-list-overflow already fired this frame.
5258
- expect(fired.has('hdrp-light-budget-exceeded')).toBe(false);
5259
- expect(fired.has('hdrp-index-list-overflow')).toBe(true);
5260
- });
5261
-
5262
- it('clearing the set (new frame) allows re-fire', () => {
5263
- const fired = new Set<string>(['hdrp-light-budget-exceeded']);
5264
- fired.clear();
5265
- expect(fired.has('hdrp-light-budget-exceeded')).toBe(false);
5266
- // The recordFrame entry clears hdrpOncePerFrameFired when frameNumber increments.
5267
- // This test verifies the clear() semantics match the expected new-frame behaviour.
5110
+ // SpotLight's projector receiver controls and shadow intensity occupy
5111
+ // the appended fifth vec4 column; point-light layout remains unchanged.
5112
+ expect(SPOT_LIGHT_STD430_BYTES).toBe(80);
5268
5113
  });
5269
5114
  });
5270
5115
  }
@@ -5275,13 +5120,13 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
5275
5120
  it('creates a RenderPipelineAsset with HDRP pipelineId and clusterGrid config', () => {
5276
5121
  const asset: RenderPipelineAsset = {
5277
5122
  kind: 'render-pipeline',
5278
- pipelineId: 'forgeax::hdrp',
5123
+ pipelineId: 'forgeax::standard',
5279
5124
  config: {
5280
5125
  clusterGrid: { x: 16, y: 9, z: 24 },
5281
5126
  },
5282
5127
  };
5283
5128
 
5284
- expect(asset.pipelineId).toBe('forgeax::hdrp');
5129
+ expect(asset.pipelineId).toBe('forgeax::standard');
5285
5130
  expect(asset.kind).toBe('render-pipeline');
5286
5131
  expect(asset.config?.clusterGrid).toBeDefined();
5287
5132
  expect(asset.config?.clusterGrid?.x).toBe(16);
@@ -5292,17 +5137,17 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
5292
5137
  it('creates a RenderPipelineAsset with URP pipelineId and no clusterGrid', () => {
5293
5138
  const asset: RenderPipelineAsset = {
5294
5139
  kind: 'render-pipeline',
5295
- pipelineId: 'forgeax::urp',
5140
+ pipelineId: 'forgeax::standard',
5296
5141
  };
5297
5142
 
5298
- expect(asset.pipelineId).toBe('forgeax::urp');
5143
+ expect(asset.pipelineId).toBe('forgeax::standard');
5299
5144
  expect(asset.config?.clusterGrid).toBeUndefined();
5300
5145
  });
5301
5146
 
5302
5147
  it('passCount field is still supported alongside clusterGrid', () => {
5303
5148
  const asset: RenderPipelineAsset = {
5304
5149
  kind: 'render-pipeline',
5305
- pipelineId: 'forgeax::hdrp',
5150
+ pipelineId: 'forgeax::standard',
5306
5151
  config: {
5307
5152
  passCount: 3,
5308
5153
  clusterGrid: { x: 16, y: 9, z: 24 },