@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.
- package/README.md +102 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/create-renderer-gpu-pass-timing.unit.test.d.ts +2 -0
- package/dist/__tests__/create-renderer-gpu-pass-timing.unit.test.d.ts.map +1 -0
- package/dist/__tests__/helpers/tilemap-assets.d.ts.map +1 -1
- package/dist/__tests__/lights-preservation.fixup.test.d.ts +2 -0
- package/dist/__tests__/lights-preservation.fixup.test.d.ts.map +1 -0
- package/dist/__tests__/material-texture-dimension-mismatch.unit.test.d.ts +2 -0
- package/dist/__tests__/material-texture-dimension-mismatch.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -1
- package/dist/__tests__/texture-dimensions.browser.test.d.ts +2 -0
- package/dist/__tests__/texture-dimensions.browser.test.d.ts.map +1 -0
- package/dist/__tests__/volumetric-fog-record-recovery.integration.test.d.ts +2 -0
- package/dist/__tests__/volumetric-fog-record-recovery.integration.test.d.ts.map +1 -0
- package/dist/createRenderer.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +3 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +30 -30
- package/src/__tests__/active-camera.unit.test.ts +6 -8
- package/src/__tests__/asset-registry.recursive.spec.ts +13 -19
- package/src/__tests__/asset.unit.test.ts +28 -25
- package/src/__tests__/basis-catalog-dispatch.integration.test.ts +10 -9
- package/src/__tests__/clamp-to-last.e2e.browser.test.ts +2 -2
- package/src/__tests__/components.test-d.ts +10 -3
- package/src/__tests__/components.unit.test.ts +13 -7
- package/src/__tests__/compressed-texture-physical-extent.integration.test.ts +12 -10
- package/src/__tests__/create-renderer-assembly.integration.test.ts +24 -0
- package/src/__tests__/create-renderer-gpu-pass-timing.unit.test.ts +32 -0
- package/src/__tests__/createRenderer.browser-no-webgpu.test.ts +66 -0
- package/src/__tests__/dawn/hello-sprite-nineslice-section.dawn.test.ts +11 -4
- package/src/__tests__/dawn/instances-shadow.dawn.test.ts +15 -2
- package/src/__tests__/dawn/material-alpha.dawn.test.ts +2 -3
- package/src/__tests__/dawn/mipmap-srgb-linear.dawn.test.ts +2 -3
- package/src/__tests__/dawn/per-entity-material-texture-isolation.dawn.test.ts +2 -3
- package/src/__tests__/dawn/sprite-nineslice-mesh-bind.dawn.test.ts +2 -3
- package/src/__tests__/dawn/upload-texture.dawn.test.ts +4 -6
- package/src/__tests__/derive-bgl-integration.test.ts +11 -0
- package/src/__tests__/equirect-bc6h.integration.test.ts +6 -4
- package/src/__tests__/errors.unit.test.ts +71 -20
- package/src/__tests__/extract-frames-merge.test.ts +48 -40
- package/src/__tests__/extract-skin-mismatch.unit.test.ts +6 -2
- package/src/__tests__/four-path-compression.integration.test.ts +2 -2
- package/src/__tests__/frame-targets-shadow-regression.unit.test.ts +13 -0
- package/src/__tests__/geometry.unit.test.ts +7 -10
- package/src/__tests__/gpu-resource-cow-survivor.test.ts +2 -3
- package/src/__tests__/helpers/tilemap-assets.ts +3 -4
- package/src/__tests__/light-gpu-byte-neutral.unit.test.ts +6 -3
- package/src/__tests__/light-snapshot.test-d.ts +8 -0
- package/src/__tests__/lights-preservation.fixup.test.ts +127 -0
- package/src/__tests__/lights.unit.test.ts +75 -2214
- package/src/__tests__/loadbyguid-texture-intern.unit.test.ts +3 -4
- package/src/__tests__/loader-name-survival.test.ts +2 -3
- package/src/__tests__/material-texture-dimension-mismatch.unit.test.ts +18 -0
- package/src/__tests__/material-texture-uv-scale.unit.test.ts +10 -2
- package/src/__tests__/mip-gate.unit.test.ts +11 -8
- package/src/__tests__/msaa-sprite-pixel-diff.dawn.test.ts +2 -3
- package/src/__tests__/new-kind-refs-contract.test.ts +2 -3
- package/src/__tests__/pbr-pipeline.unit.test.ts +29 -5
- package/src/__tests__/pipeline.unit.test.ts +66 -221
- package/src/__tests__/point-light-shadow.browser.test.ts +208 -83
- package/src/__tests__/point-light-shadow.unit.test.ts +67 -9
- package/src/__tests__/render-error-exhaustive.test-d.ts +92 -9
- package/src/__tests__/render-system-extract.test.ts +4 -6
- package/src/__tests__/render-system-mega.test.ts +30 -41
- package/src/__tests__/render-system-record-multi-material-textureview.test.ts +8 -7
- package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +6 -4
- package/src/__tests__/render-system-skin-bg.unit.test.ts +14 -9
- package/src/__tests__/renderer-recover.unit.test.ts +8 -0
- package/src/__tests__/renderer-surface.unit.test.ts +229 -3
- package/src/__tests__/shadow-csm-atlas.test.ts +41 -0
- package/src/__tests__/shadow-csm-component.test.ts +31 -24
- package/src/__tests__/shadow-csm-extract.test.ts +156 -47
- package/src/__tests__/shadow-csm-runtime-vary.dawn.test.ts +72 -0
- package/src/__tests__/shadow-csm-shader.dawn.test.ts +19 -0
- package/src/__tests__/shadow-csm-ubo.test.ts +41 -45
- package/src/__tests__/shadow-csm-viewport.browser.test.ts +29 -0
- package/src/__tests__/shadow-fields-observable.dawn.test.ts +140 -58
- package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +6 -2
- package/src/__tests__/skylight-lazy-projection.test.ts +8 -5
- package/src/__tests__/sprite-cullmode-flip.dawn.test.ts +4 -6
- package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +4 -2
- package/src/__tests__/ssao-bgl.test.ts +10 -16
- package/src/__tests__/ssao-passes.test.ts +19 -2
- package/src/__tests__/systems.unit.test.ts +59 -33
- package/src/__tests__/texture-dimensions.browser.test.ts +127 -0
- package/src/__tests__/transcode-fallback.unit.test.ts +5 -1
- package/src/__tests__/view-ubo-layout.browser.test.ts +20 -10
- package/src/__tests__/volumetric-fog-record-recovery.integration.test.ts +180 -0
- package/src/createRenderer.ts +4 -0
- 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__/
|
|
35
|
-
// - packages/runtime/src/__tests__/
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
4939
|
-
describe('
|
|
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
|
|
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('
|
|
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({
|
|
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
|
|
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('
|
|
4957
|
+
expect(result.error.code).toBe('standard-profile-invalid');
|
|
4959
4958
|
expect(result.error.hint).toContain('positive integer');
|
|
4960
|
-
expect(result.error.detail).toEqual({
|
|
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
|
|
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('
|
|
4971
|
+
expect(result.error.code).toBe('standard-profile-invalid');
|
|
4970
4972
|
expect(result.error.hint).toContain('positive integer');
|
|
4971
|
-
expect(result.error.detail).toEqual({
|
|
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('
|
|
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({
|
|
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('
|
|
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
|
|
5009
|
-
|
|
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
|
|
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
|
-
// ──
|
|
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
|
|
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
|
-
//
|
|
5100
|
-
//
|
|
5101
|
-
expect(SPOT_LIGHT_STD430_BYTES).toBe(
|
|
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::
|
|
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::
|
|
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::
|
|
5140
|
+
pipelineId: 'forgeax::standard',
|
|
5296
5141
|
};
|
|
5297
5142
|
|
|
5298
|
-
expect(asset.pipelineId).toBe('forgeax::
|
|
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::
|
|
5150
|
+
pipelineId: 'forgeax::standard',
|
|
5306
5151
|
config: {
|
|
5307
5152
|
passCount: 3,
|
|
5308
5153
|
clusterGrid: { x: 16, y: 9, z: 24 },
|