@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
|
@@ -13,8 +13,74 @@
|
|
|
13
13
|
|
|
14
14
|
import { createRenderer, EngineEnvironmentError } from '@forgeax/engine-runtime';
|
|
15
15
|
import { describe, expect, it } from 'vitest';
|
|
16
|
+
import { projectDirectionalShadowInspection } from '../../../render/src/assembly/directional-shadow-inspection';
|
|
17
|
+
import { resolveDirectionalShadowBackendAdmission } from '../../../render/src/render-pipeline';
|
|
16
18
|
|
|
17
19
|
describe('createRenderer without navigator.gpu (browser-no-webgpu)', () => {
|
|
20
|
+
it('requires the shared backend admission truth table for WebGL2 fallback and capable failure', async () => {
|
|
21
|
+
const module = (await import('../../../render/src/render-pipeline')) as Record<string, unknown>;
|
|
22
|
+
const resolver = module.resolveDirectionalShadowBackendAdmission;
|
|
23
|
+
expect(typeof resolver).toBe('function');
|
|
24
|
+
const resolve = resolver as (input: {
|
|
25
|
+
backendKind: 'webgpu' | 'wgpu-webgl2';
|
|
26
|
+
requested: 'pcssMedium' | 'pcssHigh';
|
|
27
|
+
candidate: 'accepted' | 'failed';
|
|
28
|
+
lastKnownGood?: 'pcf3' | 'pcf5';
|
|
29
|
+
}) => {
|
|
30
|
+
effective: string;
|
|
31
|
+
status: string;
|
|
32
|
+
fallbackReason?: string;
|
|
33
|
+
};
|
|
34
|
+
expect(
|
|
35
|
+
resolve({ backendKind: 'wgpu-webgl2', requested: 'pcssMedium', candidate: 'accepted' }),
|
|
36
|
+
).toMatchObject({
|
|
37
|
+
effective: 'pcf3',
|
|
38
|
+
status: 'fallback',
|
|
39
|
+
fallbackReason: 'webgl2-unsupported',
|
|
40
|
+
});
|
|
41
|
+
expect(
|
|
42
|
+
resolve({
|
|
43
|
+
backendKind: 'webgpu',
|
|
44
|
+
requested: 'pcssHigh',
|
|
45
|
+
candidate: 'failed',
|
|
46
|
+
lastKnownGood: 'pcf3',
|
|
47
|
+
}),
|
|
48
|
+
).toMatchObject({ effective: 'pcf3', status: 'rejected', fallbackReason: 'candidate-failed' });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('keeps capable candidate failure explicit while retaining the LKG inspection facts', () => {
|
|
52
|
+
const inspection = projectDirectionalShadowInspection({
|
|
53
|
+
admission: resolveDirectionalShadowBackendAdmission({
|
|
54
|
+
backendKind: 'webgpu',
|
|
55
|
+
requested: 'pcssHigh',
|
|
56
|
+
candidate: 'failed',
|
|
57
|
+
lastKnownGood: 'pcf3',
|
|
58
|
+
}),
|
|
59
|
+
cascadeCount: 4,
|
|
60
|
+
mapSize: 2048,
|
|
61
|
+
atlasBytes: 67_108_864,
|
|
62
|
+
writerPasses: 4,
|
|
63
|
+
blockerTaps: 0,
|
|
64
|
+
filterTapUpperBound: 9,
|
|
65
|
+
seamTapUpperBound: 9,
|
|
66
|
+
deviceGeneration: 2,
|
|
67
|
+
graphGeneration: 6,
|
|
68
|
+
error: {
|
|
69
|
+
code: 'shadow-candidate-failed',
|
|
70
|
+
expected: 'the capable backend candidate compiles and validates',
|
|
71
|
+
hint: 'fix the authoring input, then retry or recover',
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
expect(inspection).toMatchObject({
|
|
75
|
+
effective: 'pcf3',
|
|
76
|
+
status: 'rejected',
|
|
77
|
+
fallbackReason: 'candidate-failed',
|
|
78
|
+
lastKnownGood: true,
|
|
79
|
+
graphGeneration: 6,
|
|
80
|
+
error: { hint: expect.stringContaining('retry') },
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
18
84
|
it('navigator.gpu is absent or non-functional in this browser environment', async () => {
|
|
19
85
|
const nav = globalThis.navigator as { gpu?: GPU };
|
|
20
86
|
if (nav.gpu === undefined) return;
|
|
@@ -62,6 +62,12 @@ const BUFFER_USAGE_MAP_READ = 0x0001;
|
|
|
62
62
|
const BUFFER_USAGE_COPY_DST = 0x0008;
|
|
63
63
|
const MAP_MODE_READ = 0x0001;
|
|
64
64
|
|
|
65
|
+
// Windows Dawn first-touch construction for the two renderer/readback probes
|
|
66
|
+
// can exceed Vitest's project-wide 30-second timeout while still completing
|
|
67
|
+
// well inside the nightly platform budget. Keep this owner-specific gate
|
|
68
|
+
// bounded so a valid cold-start probe is not retried as a false failure.
|
|
69
|
+
const SPRITE_NINESLICE_DAWN_TEST_TIMEOUT_MS = 120_000;
|
|
70
|
+
|
|
65
71
|
// Distinctive 4x4 texture: corners RED (cells (0,0)/(3,0)/(0,3)/(3,3)),
|
|
66
72
|
// remaining cells BLUE. With slices=[0.25,0.25,0.25,0.25] in atlas-uv space
|
|
67
73
|
// the 4 corners line up with the 4 corner texels -- making the central band
|
|
@@ -89,11 +95,10 @@ function makeCornerCheckerTexture(): TextureAsset {
|
|
|
89
95
|
}
|
|
90
96
|
return {
|
|
91
97
|
kind: 'texture',
|
|
92
|
-
width: side,
|
|
93
|
-
height: side,
|
|
98
|
+
shape: { viewDimension: '2d', extent: { width: side, height: side } },
|
|
94
99
|
format: 'rgba8unorm-srgb',
|
|
95
100
|
colorSpace: 'srgb',
|
|
96
|
-
|
|
101
|
+
mips: { kind: 'none' },
|
|
97
102
|
data: bytes,
|
|
98
103
|
};
|
|
99
104
|
}
|
|
@@ -326,7 +331,9 @@ function midBandDistance(a: CapturedFrame, b: CapturedFrame): number {
|
|
|
326
331
|
}
|
|
327
332
|
|
|
328
333
|
describe('feat-20260527-sprite-nineslice w14 dawn smoke (stretch vs tile divergence)', () => {
|
|
329
|
-
it('stretch + tile sprite frames diverge in the mid-band after w15 (red until w15 lands)',
|
|
334
|
+
it('stretch + tile sprite frames diverge in the mid-band after w15 (red until w15 lands)', {
|
|
335
|
+
timeout: SPRITE_NINESLICE_DAWN_TEST_TIMEOUT_MS,
|
|
336
|
+
}, async () => {
|
|
330
337
|
// Asymmetric scale: x=4 vs y=1. With slices=[0.25,0.25,0.25,0.25] and the
|
|
331
338
|
// 4x4 corner-checker texture, the post-w15 9-slice path keeps corner
|
|
332
339
|
// zones at fixed world dimensions while the legacy pre-w15 path stretches
|
|
@@ -55,6 +55,17 @@ const MAP_MODE_READ = 0x0001;
|
|
|
55
55
|
const INSTANCE_COUNT = 5;
|
|
56
56
|
const SPACING = 2.5;
|
|
57
57
|
|
|
58
|
+
const SHADOW_FILTER = (() => {
|
|
59
|
+
const value = (
|
|
60
|
+
globalThis as unknown as { process?: { env?: Record<string, string | undefined> } }
|
|
61
|
+
).process?.env?.FORGEAX_SHADOW_FILTER;
|
|
62
|
+
if (value === 'pcssMedium') return 4;
|
|
63
|
+
if (value === 'pcssHigh') return 5;
|
|
64
|
+
if (value === 'pcf1') return 1;
|
|
65
|
+
if (value === 'pcf5') return 3;
|
|
66
|
+
return 2;
|
|
67
|
+
})();
|
|
68
|
+
|
|
58
69
|
function buildTranslationGrid(): Float32Array {
|
|
59
70
|
const out = new Float32Array(INSTANCE_COUNT * 16);
|
|
60
71
|
const half = ((INSTANCE_COUNT - 1) * SPACING) / 2;
|
|
@@ -263,7 +274,9 @@ describe('w10 -- shadow instanced dawn smoke (AC-05 behavioral)', () => {
|
|
|
263
274
|
depthBias: 0.005,
|
|
264
275
|
normalBias: 0.05,
|
|
265
276
|
shadowDistance: 50,
|
|
266
|
-
|
|
277
|
+
shadowFilter: SHADOW_FILTER,
|
|
278
|
+
shadowAngularRadius: 0.00465,
|
|
279
|
+
maxPenumbraTexels: 32,
|
|
267
280
|
},
|
|
268
281
|
});
|
|
269
282
|
|
|
@@ -285,7 +298,7 @@ describe('w10 -- shadow instanced dawn smoke (AC-05 behavioral)', () => {
|
|
|
285
298
|
{ component: Instances, data: { transforms: instanceTransforms } },
|
|
286
299
|
);
|
|
287
300
|
|
|
288
|
-
// Render 300 frames for temporal stability (PCF shadow, light accumulation)
|
|
301
|
+
// Render 300 frames for temporal stability (PCF or PCSS shadow, light accumulation)
|
|
289
302
|
const attachment = renderer.attach(world);
|
|
290
303
|
expect(attachment.ok).toBe(true);
|
|
291
304
|
if (!attachment.ok) throw attachment.error;
|
|
@@ -99,12 +99,11 @@ function textureBytes(color: readonly [number, number, number, number]): Uint8Ar
|
|
|
99
99
|
function makeTexture(color: readonly [number, number, number, number]): TextureAsset {
|
|
100
100
|
return {
|
|
101
101
|
kind: 'texture',
|
|
102
|
-
width: 1,
|
|
103
|
-
height: 1,
|
|
102
|
+
shape: { viewDimension: '2d', extent: { width: 1, height: 1 } },
|
|
104
103
|
format: 'rgba8unorm-srgb',
|
|
105
104
|
data: textureBytes(color),
|
|
106
105
|
colorSpace: 'srgb',
|
|
107
|
-
|
|
106
|
+
mips: { kind: 'none' },
|
|
108
107
|
};
|
|
109
108
|
}
|
|
110
109
|
|
|
@@ -42,12 +42,11 @@ function makeTexture(
|
|
|
42
42
|
): TextureAsset {
|
|
43
43
|
return {
|
|
44
44
|
kind: 'texture',
|
|
45
|
-
width,
|
|
46
|
-
height,
|
|
45
|
+
shape: { viewDimension: '2d', extent: { width, height } },
|
|
47
46
|
format,
|
|
48
47
|
data: new Uint8Array(width * height * 4),
|
|
49
48
|
colorSpace,
|
|
50
|
-
mipmap,
|
|
49
|
+
mips: mipmap ? { kind: 'generate' } : { kind: 'none' },
|
|
51
50
|
};
|
|
52
51
|
}
|
|
53
52
|
|
|
@@ -60,12 +60,11 @@ function makeChequerTexture(primaryR: number, primaryG: number, primaryB: number
|
|
|
60
60
|
}
|
|
61
61
|
return {
|
|
62
62
|
kind: 'texture',
|
|
63
|
-
width: side,
|
|
64
|
-
height: side,
|
|
63
|
+
shape: { viewDimension: '2d', extent: { width: side, height: side } },
|
|
65
64
|
format: 'rgba8unorm',
|
|
66
65
|
data: bytes,
|
|
67
66
|
colorSpace: 'srgb',
|
|
68
|
-
|
|
67
|
+
mips: { kind: 'none' },
|
|
69
68
|
};
|
|
70
69
|
}
|
|
71
70
|
|
|
@@ -126,11 +126,10 @@ describe('feat-20260527-sprite-nineslice w13 dawn smoke (HANDLE_NINESLICE_QUAD b
|
|
|
126
126
|
}
|
|
127
127
|
const texAsset: TextureAsset = {
|
|
128
128
|
kind: 'texture',
|
|
129
|
-
width: 4,
|
|
130
|
-
height: 4,
|
|
129
|
+
shape: { viewDimension: '2d', extent: { width: 4, height: 4 } },
|
|
131
130
|
format: 'rgba8unorm-srgb',
|
|
132
131
|
colorSpace: 'srgb',
|
|
133
|
-
|
|
132
|
+
mips: { kind: 'none' },
|
|
134
133
|
data: pixels,
|
|
135
134
|
};
|
|
136
135
|
const texHandle = world.allocSharedRef<'TextureAsset', TextureAsset>('TextureAsset', texAsset);
|
|
@@ -72,12 +72,11 @@ function makeTexture(
|
|
|
72
72
|
): TextureAsset {
|
|
73
73
|
return {
|
|
74
74
|
kind: 'texture',
|
|
75
|
-
width,
|
|
76
|
-
height,
|
|
75
|
+
shape: { viewDimension: '2d', extent: { width, height } },
|
|
77
76
|
format,
|
|
78
77
|
data: new Uint8Array(width * height * 4),
|
|
79
78
|
colorSpace,
|
|
80
|
-
mipmap,
|
|
79
|
+
mips: mipmap ? { kind: 'generate' } : { kind: 'none' },
|
|
81
80
|
};
|
|
82
81
|
}
|
|
83
82
|
|
|
@@ -182,12 +181,11 @@ describe('AC-04: uploadTexture non-256-aligned width (real GPU path)', () => {
|
|
|
182
181
|
|
|
183
182
|
const tex: TextureAsset = {
|
|
184
183
|
kind: 'texture',
|
|
185
|
-
width,
|
|
186
|
-
height,
|
|
184
|
+
shape: { viewDimension: '2d', extent: { width, height } },
|
|
187
185
|
format: 'rgba8unorm',
|
|
188
186
|
data: buf,
|
|
189
187
|
colorSpace: 'linear',
|
|
190
|
-
|
|
188
|
+
mips: { kind: 'none' },
|
|
191
189
|
};
|
|
192
190
|
|
|
193
191
|
const handle = world.allocSharedRef('TextureAsset', tex);
|
|
@@ -87,6 +87,17 @@ const builtinSchemas: ReadonlyArray<{ id: string; schema: readonly ParamSchemaEn
|
|
|
87
87
|
];
|
|
88
88
|
|
|
89
89
|
describe('derive(schema) integration over 5 built-in shader families (M3 w11)', () => {
|
|
90
|
+
it('derives array and volume texture schemas without a second binding path', () => {
|
|
91
|
+
for (const schema of [
|
|
92
|
+
[{ name: 'layers', type: 'texture2d_array' as const }],
|
|
93
|
+
[{ name: 'volume', type: 'texture3d' as const }],
|
|
94
|
+
]) {
|
|
95
|
+
const out = derive(schema);
|
|
96
|
+
expect(out.bglEntries.map((entry) => entry.binding)).toEqual([0, 1, 2]);
|
|
97
|
+
expect(out.resourceBindings[1]?.name).toBe(schema[0]?.name);
|
|
98
|
+
expect(out.textureFieldNames).toContain(schema[0]?.name);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
90
101
|
it('(a) every built-in shader derives a well-formed BGL with consistent userRegionBindingEnd', () => {
|
|
91
102
|
for (const { id, schema } of builtinSchemas) {
|
|
92
103
|
const out = derive(schema);
|
|
@@ -145,12 +145,14 @@ describe.skipIf(!hasGpu)('equirect renderable-source integration (feat-20260707
|
|
|
145
145
|
// POD mirrors that 2D surface.
|
|
146
146
|
const source = {
|
|
147
147
|
kind: 'texture' as const,
|
|
148
|
-
width: asset.width,
|
|
149
|
-
height: asset.height,
|
|
148
|
+
shape: { viewDimension: '2d' as const, extent: { width: asset.width, height: asset.height } },
|
|
150
149
|
format: asset.format,
|
|
151
|
-
data:
|
|
150
|
+
data:
|
|
151
|
+
asset.data instanceof Uint8ClampedArray
|
|
152
|
+
? new Uint8Array(asset.data.buffer, asset.data.byteOffset, asset.data.byteLength)
|
|
153
|
+
: asset.data,
|
|
152
154
|
colorSpace: asset.colorSpace,
|
|
153
|
-
|
|
155
|
+
mips: { kind: 'none' as const },
|
|
154
156
|
};
|
|
155
157
|
const res = deriveRenderDataCubemap(source);
|
|
156
158
|
expect(res.ok).toBe(true);
|
|
@@ -349,7 +349,7 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
349
349
|
});
|
|
350
350
|
});
|
|
351
351
|
|
|
352
|
-
describe('t2 - AC-15 ImageErrorCode exhaustive switch (new
|
|
352
|
+
describe('t2 - AC-15 ImageErrorCode exhaustive switch (new members)', () => {
|
|
353
353
|
it('ImageErrorCode includes image-hdr-decode-failed', () => {
|
|
354
354
|
const code: ImageErrorCode = 'image-hdr-decode-failed';
|
|
355
355
|
expect(code).toBe('image-hdr-decode-failed');
|
|
@@ -378,10 +378,17 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
378
378
|
return 'atlas size exceeded';
|
|
379
379
|
case 'atlas-region-mismatch':
|
|
380
380
|
return 'atlas region mismatch';
|
|
381
|
+
// feat-20260829-deep-agent-game-feedback-resolution M5 —
|
|
382
|
+
// PixelSurface authoring errors remain part of the closed image
|
|
383
|
+
// error vocabulary and must stay covered by every exhaustive
|
|
384
|
+
// consumer switch.
|
|
385
|
+
case 'image-surface-invalid':
|
|
386
|
+
return 'surface invalid';
|
|
381
387
|
}
|
|
382
388
|
}
|
|
383
389
|
expect(exhaustive('image-decode-failed')).toBe('decode failed');
|
|
384
390
|
expect(exhaustive('image-hdr-decode-failed')).toBe('hdr decode failed');
|
|
391
|
+
expect(exhaustive('image-surface-invalid')).toBe('surface invalid');
|
|
385
392
|
});
|
|
386
393
|
});
|
|
387
394
|
});
|
|
@@ -420,6 +427,8 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
420
427
|
return 'cleanup failed';
|
|
421
428
|
case 'scene-data-unavailable':
|
|
422
429
|
return 'scene data unavailable';
|
|
430
|
+
case 'standard-profile-invalid':
|
|
431
|
+
return 'standard profile invalid';
|
|
423
432
|
case 'frame-receipt-stale':
|
|
424
433
|
return 'frame receipt stale';
|
|
425
434
|
case 'renderer-contract-failed':
|
|
@@ -464,14 +473,24 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
464
473
|
return 'mesh ssbo capacity exceeded';
|
|
465
474
|
case 'mesh-ssbo-ceiling-reached':
|
|
466
475
|
return 'mesh ssbo ceiling reached';
|
|
467
|
-
case '
|
|
468
|
-
return '
|
|
469
|
-
case '
|
|
470
|
-
return '
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
case '
|
|
474
|
-
return '
|
|
476
|
+
case 'render-target-descriptor-invalid':
|
|
477
|
+
return 'render target descriptor invalid';
|
|
478
|
+
case 'render-target-capability-missing':
|
|
479
|
+
return 'render target capability missing';
|
|
480
|
+
case 'render-target-state-invalid':
|
|
481
|
+
return 'render target state invalid';
|
|
482
|
+
case 'render-target-operation-failed':
|
|
483
|
+
return 'render target operation failed';
|
|
484
|
+
case 'reflection-probe-budget-exceeded':
|
|
485
|
+
return 'reflection probe budget exceeded';
|
|
486
|
+
case 'render-intent-invalid':
|
|
487
|
+
return 'render intent invalid';
|
|
488
|
+
case 'standard-light-budget-exceeded':
|
|
489
|
+
return 'standard light budget exceeded';
|
|
490
|
+
case 'standard-cluster-index-overflow':
|
|
491
|
+
return 'standard cluster index overflow';
|
|
492
|
+
case 'standard-cluster-transport-unavailable':
|
|
493
|
+
return 'standard cluster transport unavailable';
|
|
475
494
|
// feat-20260611-fox-skinning-vertex-attribute-chain M4 / w17 (D-5):
|
|
476
495
|
// bidirectional Skin <-> pbr-skin material mismatch detected at extract.
|
|
477
496
|
case 'skin-material-mismatch':
|
|
@@ -530,6 +549,16 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
530
549
|
return 'points lines prepare failed';
|
|
531
550
|
case 'transmission-capability-missing':
|
|
532
551
|
return 'transmission capability missing';
|
|
552
|
+
case 'projector-binding-failed':
|
|
553
|
+
return 'projector binding failed';
|
|
554
|
+
case 'volume-owner-conflict':
|
|
555
|
+
return 'volume owner conflict';
|
|
556
|
+
case 'volume-density-shape-mismatch':
|
|
557
|
+
return 'volume density shape mismatch';
|
|
558
|
+
case 'volume-invalid-bounds':
|
|
559
|
+
return 'volume invalid bounds';
|
|
560
|
+
case 'volume-invalid-parameters':
|
|
561
|
+
return 'volume invalid parameters';
|
|
533
562
|
}
|
|
534
563
|
}
|
|
535
564
|
expect(exhaustive('equirect-projection-failed')).toBe('equirect projection failed');
|
|
@@ -541,16 +570,15 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
541
570
|
});
|
|
542
571
|
|
|
543
572
|
/*
|
|
544
|
-
*
|
|
545
|
-
* RuntimeErrorCode +3 members — exhaustive narrow test.
|
|
573
|
+
* Standard lighting closed error codes — exhaustive narrow test.
|
|
546
574
|
*
|
|
547
|
-
* AC-07: three
|
|
575
|
+
* AC-07: three Standard lighting codes are members of the closed render union.
|
|
548
576
|
* Each error code narrows without a default branch in switch (err.code).
|
|
549
577
|
*/
|
|
550
|
-
describe('
|
|
551
|
-
it('
|
|
552
|
-
const code: RuntimeLayerErrorCode = '
|
|
553
|
-
expect(code).toBe('
|
|
578
|
+
describe('Standard lighting error members', () => {
|
|
579
|
+
it('standard-cluster-transport-unavailable is a valid RenderErrorCode', () => {
|
|
580
|
+
const code: RuntimeLayerErrorCode = 'standard-cluster-transport-unavailable';
|
|
581
|
+
expect(code).toBe('standard-cluster-transport-unavailable');
|
|
554
582
|
});
|
|
555
583
|
|
|
556
584
|
it('exhaustive switch covers all 3 new members alongside existing members', () => {
|
|
@@ -581,6 +609,7 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
581
609
|
case 'cleanup-failed':
|
|
582
610
|
return 'ok';
|
|
583
611
|
case 'scene-data-unavailable':
|
|
612
|
+
case 'standard-profile-invalid':
|
|
584
613
|
return 'ok';
|
|
585
614
|
case 'frame-receipt-stale':
|
|
586
615
|
return 'ok';
|
|
@@ -626,9 +655,9 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
626
655
|
return 'ok';
|
|
627
656
|
case 'mesh-ssbo-ceiling-reached':
|
|
628
657
|
return 'ok';
|
|
629
|
-
case '
|
|
658
|
+
case 'standard-light-budget-exceeded':
|
|
630
659
|
return 'ok';
|
|
631
|
-
case '
|
|
660
|
+
case 'standard-cluster-index-overflow':
|
|
632
661
|
return 'ok';
|
|
633
662
|
case 'skin-material-mismatch':
|
|
634
663
|
return 'ok';
|
|
@@ -640,7 +669,19 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
640
669
|
return 'ok';
|
|
641
670
|
case 'joint-entity-dangling':
|
|
642
671
|
return 'ok';
|
|
643
|
-
case '
|
|
672
|
+
case 'standard-cluster-transport-unavailable':
|
|
673
|
+
return 'ok';
|
|
674
|
+
case 'render-target-descriptor-invalid':
|
|
675
|
+
return 'ok';
|
|
676
|
+
case 'render-target-capability-missing':
|
|
677
|
+
return 'ok';
|
|
678
|
+
case 'render-target-state-invalid':
|
|
679
|
+
return 'ok';
|
|
680
|
+
case 'render-target-operation-failed':
|
|
681
|
+
return 'ok';
|
|
682
|
+
case 'reflection-probe-budget-exceeded':
|
|
683
|
+
return 'ok';
|
|
684
|
+
case 'render-intent-invalid':
|
|
644
685
|
return 'ok';
|
|
645
686
|
case 'point-shadow-atlas-uninitialized':
|
|
646
687
|
return 'ok';
|
|
@@ -680,9 +721,19 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
680
721
|
return 'ok';
|
|
681
722
|
case 'transmission-capability-missing':
|
|
682
723
|
return 'ok';
|
|
724
|
+
case 'projector-binding-failed':
|
|
725
|
+
return 'ok';
|
|
726
|
+
case 'volume-owner-conflict':
|
|
727
|
+
return 'ok';
|
|
728
|
+
case 'volume-density-shape-mismatch':
|
|
729
|
+
return 'ok';
|
|
730
|
+
case 'volume-invalid-bounds':
|
|
731
|
+
return 'ok';
|
|
732
|
+
case 'volume-invalid-parameters':
|
|
733
|
+
return 'ok';
|
|
683
734
|
}
|
|
684
735
|
}
|
|
685
|
-
expect(exhaustive('
|
|
736
|
+
expect(exhaustive('standard-cluster-transport-unavailable')).toBe('ok');
|
|
686
737
|
expect(exhaustive('point-shadow-atlas-uninitialized')).toBe('ok');
|
|
687
738
|
expect(exhaustive('point-shadow-atlas-bounds-violation')).toBe('ok');
|
|
688
739
|
});
|
|
@@ -171,52 +171,26 @@ function simulateMerge(
|
|
|
171
171
|
}
|
|
172
172
|
directionalCount += f.lights.directionalCount;
|
|
173
173
|
}
|
|
174
|
+
const firstDirectionalLights = frames.find(
|
|
175
|
+
(frame) => frame.lights.directional !== undefined,
|
|
176
|
+
)?.lights;
|
|
174
177
|
const lights: ExtractedLights = {
|
|
175
178
|
directional,
|
|
176
179
|
directionalCount,
|
|
177
180
|
point,
|
|
178
181
|
spot,
|
|
179
|
-
lightViewProj:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
directional !== undefined
|
|
189
|
-
? frames.find((f) => f.lights.directional !== undefined)?.lights.cascadeCount
|
|
190
|
-
: undefined,
|
|
191
|
-
cascadeBlend:
|
|
192
|
-
directional !== undefined
|
|
193
|
-
? frames.find((f) => f.lights.directional !== undefined)?.lights.cascadeBlend
|
|
194
|
-
: undefined,
|
|
195
|
-
shadowMapSize:
|
|
196
|
-
directional !== undefined
|
|
197
|
-
? frames.find((f) => f.lights.directional !== undefined)?.lights.shadowMapSize
|
|
198
|
-
: undefined,
|
|
199
|
-
depthBias:
|
|
200
|
-
directional !== undefined
|
|
201
|
-
? frames.find((f) => f.lights.directional !== undefined)?.lights.depthBias
|
|
202
|
-
: undefined,
|
|
203
|
-
normalBias:
|
|
204
|
-
directional !== undefined
|
|
205
|
-
? frames.find((f) => f.lights.directional !== undefined)?.lights.normalBias
|
|
206
|
-
: undefined,
|
|
207
|
-
pcfKernelSize:
|
|
208
|
-
directional !== undefined
|
|
209
|
-
? frames.find((f) => f.lights.directional !== undefined)?.lights.pcfKernelSize
|
|
210
|
-
: undefined,
|
|
182
|
+
lightViewProj: firstDirectionalLights?.lightViewProj,
|
|
183
|
+
splitPlanes: firstDirectionalLights?.splitPlanes,
|
|
184
|
+
cascadeCount: firstDirectionalLights?.cascadeCount,
|
|
185
|
+
cascadeBlend: firstDirectionalLights?.cascadeBlend,
|
|
186
|
+
shadowMapSize: firstDirectionalLights?.shadowMapSize,
|
|
187
|
+
depthBias: firstDirectionalLights?.depthBias,
|
|
188
|
+
normalBias: firstDirectionalLights?.normalBias,
|
|
189
|
+
directionalShadowQuality: firstDirectionalLights?.directionalShadowQuality,
|
|
190
|
+
directionalShadowError: firstDirectionalLights?.directionalShadowError,
|
|
211
191
|
pointShadow: frames.flatMap((f) => f.lights.pointShadow),
|
|
212
|
-
directionalCsmConfig:
|
|
213
|
-
|
|
214
|
-
? frames.find((f) => f.lights.directional !== undefined)?.lights.directionalCsmConfig
|
|
215
|
-
: undefined,
|
|
216
|
-
directionalCsmDirection:
|
|
217
|
-
directional !== undefined
|
|
218
|
-
? frames.find((f) => f.lights.directional !== undefined)?.lights.directionalCsmDirection
|
|
219
|
-
: undefined,
|
|
192
|
+
directionalCsmConfig: firstDirectionalLights?.directionalCsmConfig,
|
|
193
|
+
directionalCsmDirection: firstDirectionalLights?.directionalCsmDirection,
|
|
220
194
|
};
|
|
221
195
|
|
|
222
196
|
// AC-05: singletons from owner world only
|
|
@@ -433,6 +407,36 @@ describe('extractFrames merge semantics (m2-t1, AC-04/05/06)', () => {
|
|
|
433
407
|
expect(frameB.lights.directionalCount).toBe(1);
|
|
434
408
|
});
|
|
435
409
|
|
|
410
|
+
it('AC-04: first directional owner error is preserved across the merge', () => {
|
|
411
|
+
const worldA = new World();
|
|
412
|
+
worldA
|
|
413
|
+
.spawn(
|
|
414
|
+
{ component: Transform, data: identityTransform() },
|
|
415
|
+
{
|
|
416
|
+
component: DirectionalLight,
|
|
417
|
+
data: {
|
|
418
|
+
direction: [0, -1, 0],
|
|
419
|
+
shadowFilter: 4,
|
|
420
|
+
shadowAngularRadius: 999,
|
|
421
|
+
maxPenumbraTexels: 32,
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
)
|
|
425
|
+
.unwrap();
|
|
426
|
+
|
|
427
|
+
const worldB = makeWorldWithDirectionalLight();
|
|
428
|
+
const frameA = extractFrame(worldA, prepareExtractContext(worldA));
|
|
429
|
+
const frameB = extractFrame(worldB, prepareExtractContext(worldB));
|
|
430
|
+
|
|
431
|
+
expect(frameA.lights.directionalShadowError?.code).toBe('shadow-invalid-config');
|
|
432
|
+
expect(frameB.lights.directionalShadowError).toBeUndefined();
|
|
433
|
+
const merged = simulateMerge([frameA, frameB], 0);
|
|
434
|
+
expect(merged.lights.directionalShadowError?.detail).toMatchObject({
|
|
435
|
+
field: 'shadowAngularRadius',
|
|
436
|
+
actual: 999,
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
436
440
|
// ── AC-05: singleton resources from owner only ─────────────────────────────
|
|
437
441
|
|
|
438
442
|
it('AC-05: skylight is taken from owner world only, non-owner ignored', () => {
|
|
@@ -624,6 +628,9 @@ describe('extractFrames cross-world directional CSM (bug-20260710)', () => {
|
|
|
624
628
|
}
|
|
625
629
|
// splitPlanes reach the component shadowDistance on the last cascade.
|
|
626
630
|
expect(frame.lights.splitPlanes).toBeDefined();
|
|
631
|
+
expect(frame.lights.splitPlanes).toHaveLength(16);
|
|
632
|
+
expect(frame.lights.splitPlanes?.[1]).toBeGreaterThan(0);
|
|
633
|
+
expect(frame.lights.splitPlanes?.[2]).toBeGreaterThan(0);
|
|
627
634
|
});
|
|
628
635
|
|
|
629
636
|
it('single world (camera + light together) still yields non-zero lightViewProj', () => {
|
|
@@ -652,5 +659,6 @@ describe('extractFrames cross-world directional CSM (bug-20260710)', () => {
|
|
|
652
659
|
for (let i = 0; i < (frame.lights.cascadeCount ?? 0); i++) {
|
|
653
660
|
expect(isNonZeroMat(lvp?.[i])).toBe(true);
|
|
654
661
|
}
|
|
662
|
+
expect(frame.lights.splitPlanes).toHaveLength(16);
|
|
655
663
|
});
|
|
656
664
|
});
|
|
@@ -276,7 +276,7 @@ describe('render-system-extract Skin / pbr-skin mismatch (AC-07 / w19)', () => {
|
|
|
276
276
|
expect(frame.renderables).toHaveLength(0);
|
|
277
277
|
});
|
|
278
278
|
|
|
279
|
-
it('(c) Skin + pbr-skin material
|
|
279
|
+
it('(c) Skin + pbr-skin material without a palette allocator -> renderable and dispatch skipped', () => {
|
|
280
280
|
const world = new World();
|
|
281
281
|
const assets = makeAssetRegistry();
|
|
282
282
|
const meshHandle = registerSkinnedMesh(world);
|
|
@@ -288,9 +288,13 @@ describe('render-system-extract Skin / pbr-skin mismatch (AC-07 / w19)', () => {
|
|
|
288
288
|
|
|
289
289
|
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
290
290
|
|
|
291
|
-
extractFrame(world, prepareExtractContext(world, { assets }));
|
|
291
|
+
const frame = extractFrame(world, prepareExtractContext(world, { assets }));
|
|
292
292
|
|
|
293
293
|
expect(errorSpy).not.toHaveBeenCalled();
|
|
294
|
+
expect(frame.renderables).toHaveLength(0);
|
|
295
|
+
expect(frame.dispatch.some((entry) => entry.materialShaderId === 'forgeax::pbr-skin')).toBe(
|
|
296
|
+
false,
|
|
297
|
+
);
|
|
294
298
|
});
|
|
295
299
|
|
|
296
300
|
it('(d) D-5 continue semantics: bad-skin entity skipped does NOT abort sibling extract', () => {
|
|
@@ -216,10 +216,10 @@ describe('w16: 4-path compressed-fixture loading', () => {
|
|
|
216
216
|
guid: GUID_TEX,
|
|
217
217
|
kind: 'texture',
|
|
218
218
|
payload: {
|
|
219
|
-
width: 4,
|
|
220
|
-
height: 4,
|
|
219
|
+
shape: { viewDimension: '2d', extent: { width: 4, height: 4 } },
|
|
221
220
|
format: 'rgba8unorm',
|
|
222
221
|
colorSpace: 'srgb',
|
|
222
|
+
mips: { kind: 'none' },
|
|
223
223
|
},
|
|
224
224
|
refs: [],
|
|
225
225
|
artifacts: {
|
|
@@ -25,4 +25,17 @@ describe('frame targets shadow removal', () => {
|
|
|
25
25
|
|
|
26
26
|
expect(resolveShadowMapSize(internals as never, lights as never)).toBe(1024);
|
|
27
27
|
});
|
|
28
|
+
|
|
29
|
+
it('does not resolve a Directional atlas for the disabled topology sentinel', () => {
|
|
30
|
+
const internals = {
|
|
31
|
+
device: { limits: { maxTextureDimension2D: 2048 } },
|
|
32
|
+
};
|
|
33
|
+
const lights = {
|
|
34
|
+
directional: 'disabled',
|
|
35
|
+
shadowMapSize: 2048,
|
|
36
|
+
cascadeCount: undefined,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
expect(resolveShadowMapSize(internals as never, lights as never)).toBeUndefined();
|
|
40
|
+
});
|
|
28
41
|
});
|
|
@@ -2390,20 +2390,17 @@ function drawPublished(renderer: RendererType, world: WorldType) {
|
|
|
2390
2390
|
spies.drawIndexed.mockClear();
|
|
2391
2391
|
drawPublished(renderer, world as WorldType);
|
|
2392
2392
|
|
|
2393
|
-
//
|
|
2394
|
-
//
|
|
2393
|
+
// Mesh indexed draws carry the Instances cardinality. Fullscreen graph
|
|
2394
|
+
// draws are a separate topology concern and intentionally use one
|
|
2395
|
+
// instance; do not mix those two contracts in one equality assertion.
|
|
2395
2396
|
const indexedCalls = spies.drawIndexed.mock.calls as Array<
|
|
2396
2397
|
[number, number, number, number, number]
|
|
2397
2398
|
>;
|
|
2398
2399
|
const drawCalls = spies.draw.mock.calls as Array<[number, number, number, number]>;
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
for (const ic of allInstanceCounts) {
|
|
2404
|
-
expect(ic).toBe(first);
|
|
2405
|
-
}
|
|
2406
|
-
}
|
|
2400
|
+
expect(indexedCalls.length).toBeGreaterThan(0);
|
|
2401
|
+
expect(indexedCalls.map((call) => call[1])).toEqual(expect.arrayContaining([4]));
|
|
2402
|
+
expect(indexedCalls.every((call) => call[1] === 4)).toBe(true);
|
|
2403
|
+
expect(drawCalls.every((call) => call[1] === 1)).toBe(true);
|
|
2407
2404
|
expect(errors).toEqual([]);
|
|
2408
2405
|
});
|
|
2409
2406
|
|