@forgeax/engine-runtime 0.1.20 → 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__/lights-preservation.fixup.test.d.ts +2 -0
- package/dist/__tests__/lights-preservation.fixup.test.d.ts.map +1 -0
- package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -1
- package/dist/createRenderer.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +2 -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 +1 -1
- 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__/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 +9 -1
- package/src/__tests__/dawn/instances-shadow.dawn.test.ts +15 -2
- package/src/__tests__/errors.unit.test.ts +51 -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__/frame-targets-shadow-regression.unit.test.ts +13 -0
- package/src/__tests__/geometry.unit.test.ts +7 -10
- package/src/__tests__/light-gpu-byte-neutral.unit.test.ts +2 -1
- 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 -2235
- package/src/__tests__/pbr-pipeline.unit.test.ts +6 -1
- package/src/__tests__/pipeline.unit.test.ts +48 -202
- package/src/__tests__/point-light-shadow.browser.test.ts +208 -83
- package/src/__tests__/point-light-shadow.unit.test.ts +61 -4
- package/src/__tests__/render-error-exhaustive.test-d.ts +63 -9
- package/src/__tests__/render-system-mega.test.ts +30 -41
- package/src/__tests__/render-system-record-multi-material-textureview.test.ts +4 -1
- package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +4 -1
- 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 +85 -20
- package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +3 -1
- package/src/__tests__/skylight-lazy-projection.test.ts +8 -5
- 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 +44 -31
- package/src/__tests__/view-ubo-layout.browser.test.ts +20 -10
- package/src/createRenderer.ts +1 -0
- package/src/index.ts +1 -2
|
@@ -179,7 +179,7 @@ describe('buildBindGroupLayoutDescriptor — pbr-pipeline 6 sites byte-equiv', (
|
|
|
179
179
|
});
|
|
180
180
|
|
|
181
181
|
describe('pbr-skin-mesh-array', () => {
|
|
182
|
-
it('storageBuffer=true:
|
|
182
|
+
it('storageBuffer=true: 3 entries, all dynamic offset', () => {
|
|
183
183
|
const spec = makeSpec();
|
|
184
184
|
const out = buildBindGroupLayoutDescriptor(spec, {
|
|
185
185
|
kind: 'pbr-skin-mesh-array',
|
|
@@ -198,6 +198,11 @@ describe('buildBindGroupLayoutDescriptor — pbr-pipeline 6 sites byte-equiv', (
|
|
|
198
198
|
visibility: 0x1,
|
|
199
199
|
buffer: { type: 'read-only-storage', hasDynamicOffset: true },
|
|
200
200
|
},
|
|
201
|
+
{
|
|
202
|
+
binding: 2,
|
|
203
|
+
visibility: 0x1,
|
|
204
|
+
buffer: { type: 'read-only-storage', hasDynamicOffset: true },
|
|
205
|
+
},
|
|
201
206
|
],
|
|
202
207
|
});
|
|
203
208
|
});
|
|
@@ -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, '');
|
|
@@ -4934,40 +4931,49 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4934
4931
|
}
|
|
4935
4932
|
|
|
4936
4933
|
{
|
|
4937
|
-
// --- from
|
|
4938
|
-
describe('
|
|
4934
|
+
// --- from standard-profile-invalid.test.ts ---
|
|
4935
|
+
describe('standard-profile-invalid', () => {
|
|
4939
4936
|
describe('validateClusterGrid', () => {
|
|
4940
4937
|
// AC-23 scenario (a): x === 0
|
|
4941
|
-
it('rejects x=0 with code
|
|
4938
|
+
it('rejects x=0 with code standard-profile-invalid and hint contains "positive integer"', () => {
|
|
4942
4939
|
const result = validateClusterGrid({ x: 0, y: 9, z: 24 });
|
|
4943
4940
|
expect(result.ok).toBe(false);
|
|
4944
4941
|
if (!result.ok) {
|
|
4945
|
-
expect(result.error.code).toBe('
|
|
4942
|
+
expect(result.error.code).toBe('standard-profile-invalid');
|
|
4946
4943
|
expect(result.error.hint).toContain('positive integer');
|
|
4947
4944
|
expect(result.error.expected).toContain('[1, 64]');
|
|
4948
|
-
expect(result.error.detail).toEqual({
|
|
4945
|
+
expect(result.error.detail).toEqual({
|
|
4946
|
+
field: 'clusterGrid',
|
|
4947
|
+
actual: { x: 0, y: 9, z: 24 },
|
|
4948
|
+
});
|
|
4949
4949
|
}
|
|
4950
4950
|
});
|
|
4951
4951
|
|
|
4952
4952
|
// AC-23 scenario (b): non-integer x=1.5
|
|
4953
|
-
it('rejects x=1.5 (non-integer) with code
|
|
4953
|
+
it('rejects x=1.5 (non-integer) with code standard-profile-invalid', () => {
|
|
4954
4954
|
const result = validateClusterGrid({ x: 1.5, y: 9, z: 24 });
|
|
4955
4955
|
expect(result.ok).toBe(false);
|
|
4956
4956
|
if (!result.ok) {
|
|
4957
|
-
expect(result.error.code).toBe('
|
|
4957
|
+
expect(result.error.code).toBe('standard-profile-invalid');
|
|
4958
4958
|
expect(result.error.hint).toContain('positive integer');
|
|
4959
|
-
expect(result.error.detail).toEqual({
|
|
4959
|
+
expect(result.error.detail).toEqual({
|
|
4960
|
+
field: 'clusterGrid',
|
|
4961
|
+
actual: { x: 1.5, y: 9, z: 24 },
|
|
4962
|
+
});
|
|
4960
4963
|
}
|
|
4961
4964
|
});
|
|
4962
4965
|
|
|
4963
4966
|
// AC-23 scenario (c): y === -1
|
|
4964
|
-
it('rejects y=-1 with code
|
|
4967
|
+
it('rejects y=-1 with code standard-profile-invalid', () => {
|
|
4965
4968
|
const result = validateClusterGrid({ x: 16, y: -1, z: 24 });
|
|
4966
4969
|
expect(result.ok).toBe(false);
|
|
4967
4970
|
if (!result.ok) {
|
|
4968
|
-
expect(result.error.code).toBe('
|
|
4971
|
+
expect(result.error.code).toBe('standard-profile-invalid');
|
|
4969
4972
|
expect(result.error.hint).toContain('positive integer');
|
|
4970
|
-
expect(result.error.detail).toEqual({
|
|
4973
|
+
expect(result.error.detail).toEqual({
|
|
4974
|
+
field: 'clusterGrid',
|
|
4975
|
+
actual: { x: 16, y: -1, z: 24 },
|
|
4976
|
+
});
|
|
4971
4977
|
}
|
|
4972
4978
|
});
|
|
4973
4979
|
|
|
@@ -4976,10 +4982,13 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4976
4982
|
const result = validateClusterGrid({ x: 16, y: 9, z: 65 });
|
|
4977
4983
|
expect(result.ok).toBe(false);
|
|
4978
4984
|
if (!result.ok) {
|
|
4979
|
-
expect(result.error.code).toBe('
|
|
4985
|
+
expect(result.error.code).toBe('standard-profile-invalid');
|
|
4980
4986
|
expect(result.error.hint).toContain('[1, 64]');
|
|
4981
4987
|
expect(result.error.expected).toContain('[1, 64]');
|
|
4982
|
-
expect(result.error.detail).toEqual({
|
|
4988
|
+
expect(result.error.detail).toEqual({
|
|
4989
|
+
field: 'clusterGrid',
|
|
4990
|
+
actual: { x: 16, y: 9, z: 65 },
|
|
4991
|
+
});
|
|
4983
4992
|
}
|
|
4984
4993
|
});
|
|
4985
4994
|
|
|
@@ -5001,21 +5010,24 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
5001
5010
|
});
|
|
5002
5011
|
});
|
|
5003
5012
|
|
|
5004
|
-
describe('
|
|
5013
|
+
describe('StandardProfileInvalidError shape', () => {
|
|
5005
5014
|
// AC-23: charter P3 triple-set .code / .hint / .expected + .detail.{x,y,z}
|
|
5006
5015
|
it('carries charter P3 triple-set with detail', () => {
|
|
5007
|
-
const err = new
|
|
5008
|
-
|
|
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');
|
|
5009
5021
|
expect(err.hint).toContain('positive integer');
|
|
5010
5022
|
expect(err.expected).toContain('[1, 64]');
|
|
5011
|
-
expect(err.detail).toEqual({ x: 0, y: 9, z: 24 });
|
|
5023
|
+
expect(err.detail).toEqual({ field: 'clusterGrid', actual: { x: 0, y: 9, z: 24 } });
|
|
5012
5024
|
});
|
|
5013
5025
|
});
|
|
5014
5026
|
});
|
|
5015
5027
|
}
|
|
5016
5028
|
|
|
5017
5029
|
{
|
|
5018
|
-
// --- from
|
|
5030
|
+
// --- from standard-cluster-index-overflow-once.test.ts ---
|
|
5019
5031
|
function makeIdentityMat4(): Mat4 {
|
|
5020
5032
|
return mat4.identity(mat4.create());
|
|
5021
5033
|
}
|
|
@@ -5076,7 +5088,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
5076
5088
|
});
|
|
5077
5089
|
});
|
|
5078
5090
|
|
|
5079
|
-
// ──
|
|
5091
|
+
// ── Standard cluster index-overflow RuntimeError class shape ─────────────────
|
|
5080
5092
|
|
|
5081
5093
|
// ── AC-03 guardrail: URP LIGHT_ARRAY_MAX_SLOTS=4 + 5 PointLights ─────────────
|
|
5082
5094
|
//
|
|
@@ -5102,185 +5114,19 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
5102
5114
|
});
|
|
5103
5115
|
}
|
|
5104
5116
|
|
|
5105
|
-
{
|
|
5106
|
-
// --- from hdrp-light-budget.test.ts ---
|
|
5107
|
-
|
|
5108
|
-
// ── Budget gate unit logic (the pure gate w23 will embed) ─────────────────────
|
|
5109
|
-
|
|
5110
|
-
const HDRP_BUDGET = 256;
|
|
5111
|
-
|
|
5112
|
-
function enforceLightBudget(
|
|
5113
|
-
totalLightCount: number,
|
|
5114
|
-
oncePerFrameFired: Set<string>,
|
|
5115
|
-
fire: (code: string, errors: unknown[]) => void,
|
|
5116
|
-
): number {
|
|
5117
|
-
if (totalLightCount <= HDRP_BUDGET) {
|
|
5118
|
-
return totalLightCount;
|
|
5119
|
-
}
|
|
5120
|
-
// Truncate to budget.
|
|
5121
|
-
const truncated = HDRP_BUDGET;
|
|
5122
|
-
if (!oncePerFrameFired.has('hdrp-light-budget-exceeded')) {
|
|
5123
|
-
oncePerFrameFired.add('hdrp-light-budget-exceeded');
|
|
5124
|
-
fire('hdrp-light-budget-exceeded', [totalLightCount, HDRP_BUDGET]);
|
|
5125
|
-
}
|
|
5126
|
-
return truncated;
|
|
5127
|
-
}
|
|
5128
|
-
|
|
5129
|
-
describe('HDRP light budget enforcement gate (AC-06/AC-07)', () => {
|
|
5130
|
-
it('256 lights: no fire, returns 256 (AC-06 boundary)', () => {
|
|
5131
|
-
const fired = new Set<string>();
|
|
5132
|
-
const errors: Array<{ code: string; actual: number; budget: number }> = [];
|
|
5133
|
-
const fireFn = (_code: string, args: unknown[]) => {
|
|
5134
|
-
errors.push({
|
|
5135
|
-
code: 'hdrp-light-budget-exceeded',
|
|
5136
|
-
actual: args[0] as number,
|
|
5137
|
-
budget: args[1] as number,
|
|
5138
|
-
});
|
|
5139
|
-
};
|
|
5140
|
-
|
|
5141
|
-
const result = enforceLightBudget(256, fired, fireFn);
|
|
5142
|
-
expect(result).toBe(256);
|
|
5143
|
-
expect(fired.size).toBe(0);
|
|
5144
|
-
expect(errors.length).toBe(0);
|
|
5145
|
-
});
|
|
5146
|
-
|
|
5147
|
-
it('257 lights: fires once, returns 256 (AC-07 truncation)', () => {
|
|
5148
|
-
const fired = new Set<string>();
|
|
5149
|
-
const errors: Array<{ code: string; actual: number; budget: number }> = [];
|
|
5150
|
-
const fireFn = (_code: string, args: unknown[]) => {
|
|
5151
|
-
errors.push({
|
|
5152
|
-
code: 'hdrp-light-budget-exceeded',
|
|
5153
|
-
actual: args[0] as number,
|
|
5154
|
-
budget: args[1] as number,
|
|
5155
|
-
});
|
|
5156
|
-
};
|
|
5157
|
-
|
|
5158
|
-
const result = enforceLightBudget(257, fired, fireFn);
|
|
5159
|
-
expect(result).toBe(256);
|
|
5160
|
-
expect(fired.size).toBe(1);
|
|
5161
|
-
expect(fired.has('hdrp-light-budget-exceeded')).toBe(true);
|
|
5162
|
-
expect(errors.length).toBe(1);
|
|
5163
|
-
expect(errors[0]?.actual).toBe(257);
|
|
5164
|
-
expect(errors[0]?.budget).toBe(256);
|
|
5165
|
-
});
|
|
5166
|
-
|
|
5167
|
-
it('same-frame subsequent overflow suppressed (warn-once-per-frame-bucket)', () => {
|
|
5168
|
-
// Simulate frame already having fired the budget error.
|
|
5169
|
-
const fired = new Set<string>(['hdrp-light-budget-exceeded']);
|
|
5170
|
-
const errors: Array<{ code: string; actual: number; budget: number }> = [];
|
|
5171
|
-
const fireFn = (_code: string, args: unknown[]) => {
|
|
5172
|
-
errors.push({
|
|
5173
|
-
code: 'hdrp-light-budget-exceeded',
|
|
5174
|
-
actual: args[0] as number,
|
|
5175
|
-
budget: args[1] as number,
|
|
5176
|
-
});
|
|
5177
|
-
};
|
|
5178
|
-
|
|
5179
|
-
// Even with 300 lights on the same frame, it should not fire again.
|
|
5180
|
-
const result = enforceLightBudget(300, fired, fireFn);
|
|
5181
|
-
expect(result).toBe(256);
|
|
5182
|
-
expect(errors.length).toBe(0);
|
|
5183
|
-
});
|
|
5184
|
-
|
|
5185
|
-
it('second frame fires again (fresh bucket)', () => {
|
|
5186
|
-
// New frame: empty Set.
|
|
5187
|
-
const fired = new Set<string>();
|
|
5188
|
-
const errors: Array<{ code: string; actual: number; budget: number }> = [];
|
|
5189
|
-
const fireFn = (_code: string, args: unknown[]) => {
|
|
5190
|
-
errors.push({
|
|
5191
|
-
code: 'hdrp-light-budget-exceeded',
|
|
5192
|
-
actual: args[0] as number,
|
|
5193
|
-
budget: args[1] as number,
|
|
5194
|
-
});
|
|
5195
|
-
};
|
|
5196
|
-
|
|
5197
|
-
const result = enforceLightBudget(258, fired, fireFn);
|
|
5198
|
-
expect(result).toBe(256);
|
|
5199
|
-
expect(errors.length).toBe(1);
|
|
5200
|
-
expect(errors[0]?.actual).toBe(258);
|
|
5201
|
-
expect(errors[0]?.budget).toBe(256);
|
|
5202
|
-
});
|
|
5203
|
-
|
|
5204
|
-
it('second frame with exactly 256 does not fire (fresh bucket, under budget)', () => {
|
|
5205
|
-
const fired = new Set<string>();
|
|
5206
|
-
const errors: Array<{ code: string; actual: number; budget: number }> = [];
|
|
5207
|
-
const fireFn = (_code: string, args: unknown[]) => {
|
|
5208
|
-
errors.push({
|
|
5209
|
-
code: 'hdrp-light-budget-exceeded',
|
|
5210
|
-
actual: args[0] as number,
|
|
5211
|
-
budget: args[1] as number,
|
|
5212
|
-
});
|
|
5213
|
-
};
|
|
5214
|
-
|
|
5215
|
-
const result = enforceLightBudget(256, fired, fireFn);
|
|
5216
|
-
expect(result).toBe(256);
|
|
5217
|
-
expect(errors.length).toBe(0);
|
|
5218
|
-
});
|
|
5219
|
-
|
|
5220
|
-
it('0 lights returns 0 (empty scene, no fire)', () => {
|
|
5221
|
-
const fired = new Set<string>();
|
|
5222
|
-
const errors: Array<{ code: string; actual: number; budget: number }> = [];
|
|
5223
|
-
const fireFn = (_code: string, args: unknown[]) => {
|
|
5224
|
-
errors.push({
|
|
5225
|
-
code: 'hdrp-light-budget-exceeded',
|
|
5226
|
-
actual: args[0] as number,
|
|
5227
|
-
budget: args[1] as number,
|
|
5228
|
-
});
|
|
5229
|
-
};
|
|
5230
|
-
|
|
5231
|
-
const result = enforceLightBudget(0, fired, fireFn);
|
|
5232
|
-
expect(result).toBe(0);
|
|
5233
|
-
expect(errors.length).toBe(0);
|
|
5234
|
-
});
|
|
5235
|
-
|
|
5236
|
-
it('HDRP_BUDGET constant must be 256 (D-scope anchor)', () => {
|
|
5237
|
-
expect(HDRP_BUDGET).toBe(256);
|
|
5238
|
-
});
|
|
5239
|
-
});
|
|
5240
|
-
|
|
5241
|
-
// ── Integration pattern: once-per-frame Set complements existing hdrpOncePerFrameFired ──
|
|
5242
|
-
|
|
5243
|
-
describe('hdrpOncePerFrameFired Set dedup semantics', () => {
|
|
5244
|
-
it('empty set allows first fire', () => {
|
|
5245
|
-
const fired = new Set<string>();
|
|
5246
|
-
expect(fired.has('hdrp-light-budget-exceeded')).toBe(false);
|
|
5247
|
-
});
|
|
5248
|
-
|
|
5249
|
-
it('set with entry suppresses subsequent fires on the same code', () => {
|
|
5250
|
-
const fired = new Set<string>(['hdrp-light-budget-exceeded']);
|
|
5251
|
-
expect(fired.has('hdrp-light-budget-exceeded')).toBe(true);
|
|
5252
|
-
});
|
|
5253
|
-
|
|
5254
|
-
it('hdrp-light-budget-exceeded is independent from hdrp-index-list-overflow', () => {
|
|
5255
|
-
const fired = new Set<string>(['hdrp-index-list-overflow']);
|
|
5256
|
-
// Budget-exceeded should still fire even if index-list-overflow already fired this frame.
|
|
5257
|
-
expect(fired.has('hdrp-light-budget-exceeded')).toBe(false);
|
|
5258
|
-
expect(fired.has('hdrp-index-list-overflow')).toBe(true);
|
|
5259
|
-
});
|
|
5260
|
-
|
|
5261
|
-
it('clearing the set (new frame) allows re-fire', () => {
|
|
5262
|
-
const fired = new Set<string>(['hdrp-light-budget-exceeded']);
|
|
5263
|
-
fired.clear();
|
|
5264
|
-
expect(fired.has('hdrp-light-budget-exceeded')).toBe(false);
|
|
5265
|
-
// The recordFrame entry clears hdrpOncePerFrameFired when frameNumber increments.
|
|
5266
|
-
// This test verifies the clear() semantics match the expected new-frame behaviour.
|
|
5267
|
-
});
|
|
5268
|
-
});
|
|
5269
|
-
}
|
|
5270
|
-
|
|
5271
5117
|
{
|
|
5272
5118
|
// --- from hdrp-pipeline-asset-config.test.ts ---
|
|
5273
5119
|
describe('hdrp pipeline asset config', () => {
|
|
5274
5120
|
it('creates a RenderPipelineAsset with HDRP pipelineId and clusterGrid config', () => {
|
|
5275
5121
|
const asset: RenderPipelineAsset = {
|
|
5276
5122
|
kind: 'render-pipeline',
|
|
5277
|
-
pipelineId: 'forgeax::
|
|
5123
|
+
pipelineId: 'forgeax::standard',
|
|
5278
5124
|
config: {
|
|
5279
5125
|
clusterGrid: { x: 16, y: 9, z: 24 },
|
|
5280
5126
|
},
|
|
5281
5127
|
};
|
|
5282
5128
|
|
|
5283
|
-
expect(asset.pipelineId).toBe('forgeax::
|
|
5129
|
+
expect(asset.pipelineId).toBe('forgeax::standard');
|
|
5284
5130
|
expect(asset.kind).toBe('render-pipeline');
|
|
5285
5131
|
expect(asset.config?.clusterGrid).toBeDefined();
|
|
5286
5132
|
expect(asset.config?.clusterGrid?.x).toBe(16);
|
|
@@ -5291,17 +5137,17 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
5291
5137
|
it('creates a RenderPipelineAsset with URP pipelineId and no clusterGrid', () => {
|
|
5292
5138
|
const asset: RenderPipelineAsset = {
|
|
5293
5139
|
kind: 'render-pipeline',
|
|
5294
|
-
pipelineId: 'forgeax::
|
|
5140
|
+
pipelineId: 'forgeax::standard',
|
|
5295
5141
|
};
|
|
5296
5142
|
|
|
5297
|
-
expect(asset.pipelineId).toBe('forgeax::
|
|
5143
|
+
expect(asset.pipelineId).toBe('forgeax::standard');
|
|
5298
5144
|
expect(asset.config?.clusterGrid).toBeUndefined();
|
|
5299
5145
|
});
|
|
5300
5146
|
|
|
5301
5147
|
it('passCount field is still supported alongside clusterGrid', () => {
|
|
5302
5148
|
const asset: RenderPipelineAsset = {
|
|
5303
5149
|
kind: 'render-pipeline',
|
|
5304
|
-
pipelineId: 'forgeax::
|
|
5150
|
+
pipelineId: 'forgeax::standard',
|
|
5305
5151
|
config: {
|
|
5306
5152
|
passCount: 3,
|
|
5307
5153
|
clusterGrid: { x: 16, y: 9, z: 24 },
|