@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
|
@@ -5,15 +5,17 @@
|
|
|
5
5
|
// independent of runtime cascadeCount. Validates std140 byte offsets for
|
|
6
6
|
// lightViewProj[4], splitPlanes[4], cascadeCount, cascadeBlend.
|
|
7
7
|
//
|
|
8
|
-
// feat-
|
|
9
|
-
//
|
|
8
|
+
// feat-20260827-directional-csm-pcss-quality M1: the View UBO keeps its
|
|
9
|
+
// existing 960 B payload and 1024 B slot while the directional filter carrier
|
|
10
|
+
// reuses the tail pad. The directional cascade
|
|
10
11
|
// offsets [0..128] are byte-for-byte UNCHANGED; the per-spot fragment-read
|
|
11
12
|
// `spotLightViewProj` array<mat4x4<f32>,4> (256 B) folded into the tail at
|
|
12
13
|
// byte 528 (float 132), replacing the former zero tail-pad and removing the
|
|
13
14
|
// standalone @group(0) binding 9 uniform buffer that overflowed the WebGL2
|
|
14
15
|
// fallback fragment uniform-buffer budget.
|
|
15
16
|
//
|
|
16
|
-
// Layout (
|
|
17
|
+
// Layout (960 B View payload, 1024 B slot; the directional prefix remains
|
|
18
|
+
// byte-compatible with the 784 B Spot-matrix boundary):
|
|
17
19
|
// [ 0.. 16) worldViewProj mat4x4<f32> (align 16, size 64)
|
|
18
20
|
// [ 16.. 19) lightDir vec3<f32> (align 16, 12 + 4 pad)
|
|
19
21
|
// [ 20.. 22) lightColor vec3<f32> (align 16, 12 + 4 pad)
|
|
@@ -31,30 +33,27 @@
|
|
|
31
33
|
// [125] cascadeBlend f32 (align 4, size 4)
|
|
32
34
|
// [126] depthBias f32 (align 4, size 4)
|
|
33
35
|
// [127] normalBias f32 (align 4, size 4)
|
|
34
|
-
// [128]
|
|
35
|
-
// [129..131] align padding f32[3] (mat4 array align 16)
|
|
36
|
+
// [128..131] directionalShadowFilter vec4<f32> (profile, radius, penumbra, reserved)
|
|
36
37
|
// [132..195] spotLightViewProj array<mat4x4<f32>,4> (align 16, size 256)
|
|
37
38
|
|
|
38
39
|
import { readFileSync } from 'node:fs';
|
|
39
40
|
import { fileURLToPath } from 'node:url';
|
|
40
41
|
import { describe, expect, it } from 'vitest';
|
|
41
42
|
|
|
42
|
-
const VIEW_UBO_FLOAT_COUNT =
|
|
43
|
-
const VIEW_UBO_BYTES =
|
|
43
|
+
const VIEW_UBO_FLOAT_COUNT = 240;
|
|
44
|
+
const VIEW_UBO_BYTES = 960;
|
|
44
45
|
// feat-20260625 w25: spot lightViewProj array folded into the View UBO tail.
|
|
45
|
-
const OFFSET_SPOT_LIGHT_VIEW_PROJ = 132; // byte 528 (16 B-aligned after
|
|
46
|
+
const OFFSET_SPOT_LIGHT_VIEW_PROJ = 132; // byte 528 (16 B-aligned after filter carrier)
|
|
46
47
|
const SPOT_LVP_LANE_COUNT = 4;
|
|
47
48
|
const SPOT_LVP_FLOATS_PER_LANE = 16;
|
|
48
49
|
|
|
49
|
-
// feat-
|
|
50
|
+
// feat-20260827-directional-csm-pcss-quality M1:
|
|
50
51
|
// the merged DirectionalLight's shadow tail-pad slots — depthBias [126]/byte504,
|
|
51
|
-
// normalBias [127]/byte508,
|
|
52
|
-
//
|
|
53
|
-
// demos AC-14 pcfKernelSize lane moved from [126] to [128] when depthBias/normalBias
|
|
54
|
-
// merged in ahead of it.
|
|
52
|
+
// normalBias [127]/byte508, and the four-float directional filter carrier
|
|
53
|
+
// [128..131]. The Spot matrix array remains at [132..195].
|
|
55
54
|
const OFFSET_DEPTH_BIAS = 126; // byte 504
|
|
56
55
|
const OFFSET_NORMAL_BIAS = 127; // byte 508
|
|
57
|
-
const
|
|
56
|
+
const OFFSET_DIRECTIONAL_FILTER = 128; // byte 512
|
|
58
57
|
|
|
59
58
|
const F32_BYTES = 4;
|
|
60
59
|
|
|
@@ -79,30 +78,30 @@ const SPLIT_STRIDE_FLOATS = 4;
|
|
|
79
78
|
|
|
80
79
|
describe('View UBO std140 layout (w14)', () => {
|
|
81
80
|
describe('size invariants', () => {
|
|
82
|
-
it('UBO total size is
|
|
83
|
-
expect(VIEW_UBO_FLOAT_COUNT).toBe(
|
|
81
|
+
it('UBO total size is 960 B (240 f32)', () => {
|
|
82
|
+
expect(VIEW_UBO_FLOAT_COUNT).toBe(240);
|
|
84
83
|
expect(VIEW_UBO_BYTES).toBe(VIEW_UBO_FLOAT_COUNT * F32_BYTES);
|
|
85
|
-
expect(VIEW_UBO_BYTES).toBe(
|
|
84
|
+
expect(VIEW_UBO_BYTES).toBe(960);
|
|
86
85
|
});
|
|
87
86
|
|
|
88
87
|
it('UBO size is fixed — independent of cascadeCount', () => {
|
|
89
|
-
// The host always allocates
|
|
88
|
+
// The host always allocates 960 B regardless of whether cascadeCount
|
|
90
89
|
// is 1, 2, 3, or 4 at runtime. This validates AC-08.
|
|
91
90
|
const sizeForN1 = VIEW_UBO_FLOAT_COUNT * F32_BYTES; // cascadeCount=1
|
|
92
91
|
const sizeForN4 = VIEW_UBO_FLOAT_COUNT * F32_BYTES; // cascadeCount=4
|
|
93
92
|
expect(sizeForN1).toBe(sizeForN4);
|
|
94
|
-
expect(sizeForN1).toBe(
|
|
93
|
+
expect(sizeForN1).toBe(960);
|
|
95
94
|
});
|
|
96
95
|
|
|
97
96
|
it('spotLightViewProj array lands at byte 528 (float 132), 16 B-aligned, last field', () => {
|
|
98
97
|
// feat-20260625 w25: the 4-lane spot perspective matrix array folds into
|
|
99
|
-
// the View UBO tail after
|
|
98
|
+
// the View UBO tail after the directional filter carrier, 16 B-aligned
|
|
100
99
|
// so it starts at byte 528 (float 132). It is the last struct field.
|
|
101
100
|
expect(OFFSET_SPOT_LIGHT_VIEW_PROJ * F32_BYTES).toBe(528);
|
|
102
101
|
expect((OFFSET_SPOT_LIGHT_VIEW_PROJ * F32_BYTES) % 16).toBe(0);
|
|
103
102
|
const spotArrayEndFloat =
|
|
104
103
|
OFFSET_SPOT_LIGHT_VIEW_PROJ + SPOT_LVP_LANE_COUNT * SPOT_LVP_FLOATS_PER_LANE;
|
|
105
|
-
expect(spotArrayEndFloat).toBe(
|
|
104
|
+
expect(spotArrayEndFloat).toBe(196);
|
|
106
105
|
expect(spotArrayEndFloat * F32_BYTES).toBe(784);
|
|
107
106
|
});
|
|
108
107
|
});
|
|
@@ -220,15 +219,8 @@ describe('View UBO std140 layout (w14)', () => {
|
|
|
220
219
|
});
|
|
221
220
|
});
|
|
222
221
|
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
// WGSL View struct must declare the matching `pcfKernelSize : f32` immediately
|
|
226
|
-
// after cascadeBlend (single SSOT, append-at-tail, plan-strategy D-0). These
|
|
227
|
-
// are read against the real source the compiler/host use (mirrors the
|
|
228
|
-
// shadow-csm-tile-consistency antipattern guard: never re-declare and test a
|
|
229
|
-
// formula against itself). RED until M0-T-IMPL-RECORD + M0-T-IMPL-WGSL-STRUCT
|
|
230
|
-
// land; the slot was previously a zero tail-pad float (no record write existed).
|
|
231
|
-
describe('pcfKernelSize tail-pad slot wiring (M0, AC-14)', () => {
|
|
222
|
+
// M1-T05: read the real host and WGSL sources so the frozen ABI cannot drift.
|
|
223
|
+
describe('directional filter carrier tail-pad wiring (M1-T05)', () => {
|
|
232
224
|
const recordSrc = readFileSync(
|
|
233
225
|
fileURLToPath(new URL('../../../render/src/record/view-ubo.ts', import.meta.url)),
|
|
234
226
|
'utf8',
|
|
@@ -238,26 +230,25 @@ describe('pcfKernelSize tail-pad slot wiring (M0, AC-14)', () => {
|
|
|
238
230
|
'utf8',
|
|
239
231
|
);
|
|
240
232
|
|
|
241
|
-
it('shadow tail floats [126..
|
|
233
|
+
it('shadow tail floats [126..131] at fixed offsets, before the spot matrix array', () => {
|
|
242
234
|
expect(OFFSET_DEPTH_BIAS * F32_BYTES).toBe(504);
|
|
243
235
|
expect(OFFSET_NORMAL_BIAS * F32_BYTES).toBe(508);
|
|
244
|
-
expect(
|
|
236
|
+
expect(OFFSET_DIRECTIONAL_FILTER * F32_BYTES).toBe(512);
|
|
245
237
|
expect(OFFSET_DEPTH_BIAS).toBeGreaterThan(OFFSET_CASCADE_BLEND);
|
|
246
|
-
expect(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
expect(OFFSET_PCF_KERNEL_SIZE).toBeLessThan(OFFSET_SPOT_LIGHT_VIEW_PROJ);
|
|
251
|
-
expect(VIEW_UBO_FLOAT_COUNT).toBe(196);
|
|
252
|
-
expect(VIEW_UBO_BYTES).toBe(784);
|
|
238
|
+
expect(OFFSET_DIRECTIONAL_FILTER + 4).toBe(OFFSET_SPOT_LIGHT_VIEW_PROJ);
|
|
239
|
+
expect(OFFSET_DIRECTIONAL_FILTER).toBeLessThan(OFFSET_SPOT_LIGHT_VIEW_PROJ);
|
|
240
|
+
expect(VIEW_UBO_FLOAT_COUNT).toBe(240);
|
|
241
|
+
expect(VIEW_UBO_BYTES).toBe(960);
|
|
253
242
|
});
|
|
254
243
|
|
|
255
|
-
it('record writes the merged shadow tail-pad floats [126]/[127]/[128]', () => {
|
|
244
|
+
it('record writes the merged shadow tail-pad floats [126]/[127]/[128..131]', () => {
|
|
256
245
|
expect(recordSrc).toMatch(/viewPayload\[126\]\s*=\s*lights\.depthBias/);
|
|
257
246
|
expect(recordSrc).toMatch(/viewPayload\[127\]\s*=\s*lights\.normalBias/);
|
|
258
|
-
expect(recordSrc).toMatch(
|
|
259
|
-
|
|
260
|
-
);
|
|
247
|
+
expect(recordSrc).toMatch(/viewPayload\[128\]/);
|
|
248
|
+
expect(recordSrc).toMatch(/viewPayload\[129\]/);
|
|
249
|
+
expect(recordSrc).toMatch(/viewPayload\[130\]/);
|
|
250
|
+
expect(recordSrc).toMatch(/viewPayload\[131\]/);
|
|
251
|
+
expect(recordSrc).not.toMatch(/lights\.pcfKernelSize/);
|
|
261
252
|
});
|
|
262
253
|
|
|
263
254
|
it('record carries the folded spot matrices in the complete View UBO payload (w25)', () => {
|
|
@@ -269,10 +260,15 @@ describe('pcfKernelSize tail-pad slot wiring (M0, AC-14)', () => {
|
|
|
269
260
|
expect(recordSrc).toMatch(/SPOT_LVP_BASE_FLOAT\s*=\s*132/);
|
|
270
261
|
});
|
|
271
262
|
|
|
272
|
-
it('common.wgsl View struct declares
|
|
263
|
+
it('common.wgsl View struct declares the directional filter vec4 after biases', () => {
|
|
273
264
|
const m = wgslSrc.match(
|
|
274
|
-
/cascadeBlend\s*:\s*f32\s*,\s*depthBias\s*:\s*f32\s*,\s*normalBias\s*:\s*f32\s*,\s*
|
|
265
|
+
/cascadeBlend\s*:\s*f32\s*,\s*depthBias\s*:\s*f32\s*,\s*normalBias\s*:\s*f32\s*,\s*directionalShadowFilter\s*:\s*vec4<f32>\s*,/,
|
|
275
266
|
);
|
|
276
267
|
expect(m).not.toBeNull();
|
|
277
268
|
});
|
|
269
|
+
|
|
270
|
+
it('keeps one View binding and removes the retired spot matrix binding', () => {
|
|
271
|
+
expect(wgslSrc.match(/@group\(0\) @binding\(0\) var<uniform> view/g)).toHaveLength(1);
|
|
272
|
+
expect(wgslSrc).not.toMatch(/@group\(0\)\s*@binding\(9\)/);
|
|
273
|
+
});
|
|
278
274
|
});
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
// D-6: pass names shadowCascade0..3.
|
|
12
12
|
|
|
13
13
|
import { describe, expect, it } from 'vitest';
|
|
14
|
+
import { projectDirectionalShadowInspection } from '../../../render/src/assembly/directional-shadow-inspection';
|
|
15
|
+
import { resolveDirectionalShadowBackendAdmission } from '../../../render/src/render-pipeline';
|
|
14
16
|
|
|
15
17
|
const browserReady = typeof navigator !== 'undefined' && navigator.gpu !== undefined;
|
|
16
18
|
|
|
@@ -83,3 +85,30 @@ describe('CSM per-cascade viewport (w30)', () => {
|
|
|
83
85
|
});
|
|
84
86
|
});
|
|
85
87
|
});
|
|
88
|
+
|
|
89
|
+
describe('M3 Browser backend admission', () => {
|
|
90
|
+
it('declares WebGL2 PCSS fallback without changing the atlas viewport contract', () => {
|
|
91
|
+
const inspection = projectDirectionalShadowInspection({
|
|
92
|
+
admission: resolveDirectionalShadowBackendAdmission({
|
|
93
|
+
backendKind: 'wgpu-webgl2',
|
|
94
|
+
requested: 'pcssHigh',
|
|
95
|
+
candidate: 'accepted',
|
|
96
|
+
}),
|
|
97
|
+
cascadeCount: 4,
|
|
98
|
+
mapSize: 2048,
|
|
99
|
+
atlasBytes: 67_108_864,
|
|
100
|
+
writerPasses: 4,
|
|
101
|
+
blockerTaps: 0,
|
|
102
|
+
filterTapUpperBound: 25,
|
|
103
|
+
seamTapUpperBound: 25,
|
|
104
|
+
deviceGeneration: 1,
|
|
105
|
+
graphGeneration: 1,
|
|
106
|
+
});
|
|
107
|
+
expect(inspection).toMatchObject({
|
|
108
|
+
effective: 'pcf5',
|
|
109
|
+
fallbackReason: 'webgl2-unsupported',
|
|
110
|
+
cascadeCount: 4,
|
|
111
|
+
mapSize: 2048,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// shadow-fields-observable.dawn.test.ts
|
|
2
2
|
// feat-20260621-merge-directionallightshadow-into-directionallight M6
|
|
3
3
|
//
|
|
4
|
-
// Observability gate: proves depthBias, normalBias,
|
|
5
|
-
// three merged DirectionalLight shadow fields -- produce real pixel A/B diffs.
|
|
4
|
+
// Observability gate: proves depthBias, normalBias, and the directional filter
|
|
5
|
+
// carrier -- three merged DirectionalLight shadow fields -- produce real pixel A/B diffs.
|
|
6
6
|
//
|
|
7
7
|
// Root cause of prior "0 diff everywhere" failure: (a) materials without
|
|
8
8
|
// ShadowCaster pass (nothing written into shadow depth atlas), (b) floor
|
|
@@ -33,12 +33,70 @@ const WIDTH = 256;
|
|
|
33
33
|
const HEIGHT = 256;
|
|
34
34
|
const BPR = Math.ceil((WIDTH * 4) / 256) * 256;
|
|
35
35
|
|
|
36
|
+
// Each falsifier performs four fresh 256x256 Dawn readbacks. Windows' Dawn
|
|
37
|
+
// prebuild can exceed Vitest's 90-second bound for that sequence while still
|
|
38
|
+
// completing well inside the nightly platform job's 45-minute budget.
|
|
39
|
+
const SHADOW_FIELDS_FALSIFIER_TIMEOUT_MS = 300_000;
|
|
40
|
+
|
|
36
41
|
const ENGINE_MANIFEST = await (async () => {
|
|
37
42
|
const { buildEngineShaderManifest } = await import('@forgeax/engine-vite-plugin-shader');
|
|
38
43
|
return buildEngineShaderManifest();
|
|
39
44
|
})();
|
|
40
45
|
const ENGINE_MANIFEST_URL = `data:application/json,${encodeURIComponent(JSON.stringify(ENGINE_MANIFEST))}`;
|
|
41
46
|
|
|
47
|
+
describe('M3 Dawn candidate admission', () => {
|
|
48
|
+
it('requires accepted production candidate to preserve the requested PCSS profile', async () => {
|
|
49
|
+
const module = (await import('../../../render/src/render-pipeline')) as Record<string, unknown>;
|
|
50
|
+
const resolver = module.resolveDirectionalShadowBackendAdmission;
|
|
51
|
+
expect(typeof resolver).toBe('function');
|
|
52
|
+
const result = (
|
|
53
|
+
resolver as (input: {
|
|
54
|
+
backendKind: 'webgpu';
|
|
55
|
+
requested: 'pcssMedium' | 'pcssHigh';
|
|
56
|
+
candidate: 'accepted' | 'failed';
|
|
57
|
+
}) => { effective: string; status: string; pixelEvidence: string }
|
|
58
|
+
)({ backendKind: 'webgpu', requested: 'pcssHigh', candidate: 'accepted' });
|
|
59
|
+
expect(result).toMatchObject({
|
|
60
|
+
effective: 'pcssHigh',
|
|
61
|
+
status: 'accepted',
|
|
62
|
+
pixelEvidence: 'available',
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('requires the same bounded Directional inspection projection used by Dawn', async () => {
|
|
67
|
+
const module = (await import(
|
|
68
|
+
'../../../render/src/assembly/directional-shadow-inspection'
|
|
69
|
+
)) as Record<string, unknown>;
|
|
70
|
+
const project = module.projectDirectionalShadowInspection;
|
|
71
|
+
expect(typeof project).toBe('function');
|
|
72
|
+
const result = (project as (input: Record<string, unknown>) => Record<string, unknown>)({
|
|
73
|
+
admission: {
|
|
74
|
+
requested: 'pcssMedium',
|
|
75
|
+
effective: 'pcssMedium',
|
|
76
|
+
status: 'accepted',
|
|
77
|
+
pixelEvidence: 'available',
|
|
78
|
+
},
|
|
79
|
+
cascadeCount: 4,
|
|
80
|
+
mapSize: 1024,
|
|
81
|
+
atlasBytes: 16_777_216,
|
|
82
|
+
writerPasses: 4,
|
|
83
|
+
blockerTaps: 8,
|
|
84
|
+
filterTapUpperBound: 16,
|
|
85
|
+
seamTapUpperBound: 48,
|
|
86
|
+
deviceGeneration: 1,
|
|
87
|
+
graphGeneration: 3,
|
|
88
|
+
});
|
|
89
|
+
expect(result).toMatchObject({
|
|
90
|
+
requested: 'pcssMedium',
|
|
91
|
+
effective: 'pcssMedium',
|
|
92
|
+
blockerTaps: 8,
|
|
93
|
+
filterTapUpperBound: 16,
|
|
94
|
+
seamTapUpperBound: 48,
|
|
95
|
+
});
|
|
96
|
+
expect(Object.keys(result)).not.toContain('perPixel');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
42
100
|
// Helper types
|
|
43
101
|
interface WriteCapture {
|
|
44
102
|
f32: Float32Array;
|
|
@@ -172,7 +230,9 @@ function spawnScene(
|
|
|
172
230
|
depthBias,
|
|
173
231
|
normalBias,
|
|
174
232
|
shadowDistance: 50,
|
|
175
|
-
|
|
233
|
+
shadowFilter: pcf === 1 ? 1 : pcf === 3 ? 2 : 3,
|
|
234
|
+
shadowAngularRadius: 0.00465,
|
|
235
|
+
maxPenumbraTexels: 32,
|
|
176
236
|
},
|
|
177
237
|
});
|
|
178
238
|
world.spawn(
|
|
@@ -432,15 +492,15 @@ async function renderConfigWithSpy(
|
|
|
432
492
|
|
|
433
493
|
describe('M6 shadow fields observability', () => {
|
|
434
494
|
// --- Gate A: structural WGSL check ---
|
|
435
|
-
it('composed default-standard-pbr WGSL reads view.depthBias, view.normalBias,
|
|
495
|
+
it('composed default-standard-pbr WGSL reads view.depthBias, view.normalBias, and directionalShadowFilter', () => {
|
|
436
496
|
const wgsl = composedStandardPbr();
|
|
437
497
|
expect(wgsl).toContain('depthBias');
|
|
438
498
|
expect(wgsl).toContain('normalBias');
|
|
439
|
-
expect(wgsl).toContain('
|
|
499
|
+
expect(wgsl).toContain('directionalShadowFilter');
|
|
440
500
|
});
|
|
441
501
|
|
|
442
|
-
// --- Gate B: UBO spy — slots [126]=depthBias, [127]=normalBias, [128]=
|
|
443
|
-
it('View UBO slots [126]=depthBias, [127]=normalBias, [128]=
|
|
502
|
+
// --- Gate B: UBO spy — slots [126]=depthBias, [127]=normalBias, [128]=filter profile ---
|
|
503
|
+
it('View UBO slots [126]=depthBias, [127]=normalBias, [128]=filter profile', async () => {
|
|
444
504
|
if (typeof globalThis.navigator?.gpu?.requestAdapter !== 'function') return;
|
|
445
505
|
const { captured } = await renderConfigWithSpy(true, 0.123, 0.456, 3);
|
|
446
506
|
const vw = captured.filter((c) => c.f32.length >= 148);
|
|
@@ -450,22 +510,22 @@ describe('M6 shadow fields observability', () => {
|
|
|
450
510
|
const f32 = last.f32;
|
|
451
511
|
expect(Math.abs((f32[126] ?? Number.NaN) - 0.123)).toBeLessThan(0.001);
|
|
452
512
|
expect(Math.abs((f32[127] ?? Number.NaN) - 0.456)).toBeLessThan(0.001);
|
|
453
|
-
expect(f32[128]).toBe(
|
|
513
|
+
expect(f32[128]).toBe(2);
|
|
454
514
|
}, 60000);
|
|
455
515
|
|
|
456
|
-
// --- Gate D:
|
|
457
|
-
it('
|
|
516
|
+
// --- Gate D: pcf5 maps to the accepted profile carrier ---
|
|
517
|
+
it('pcf5 maps to profile 3 in View UBO[128]', async () => {
|
|
458
518
|
if (typeof globalThis.navigator?.gpu?.requestAdapter !== 'function') return;
|
|
459
|
-
const { captured } = await renderConfigWithSpy(true, 0.005, 0.05,
|
|
519
|
+
const { captured } = await renderConfigWithSpy(true, 0.005, 0.05, 5);
|
|
460
520
|
const vw9 = captured.filter((c) => c.f32.length >= 148);
|
|
461
521
|
expect(vw9.length).toBeGreaterThan(0);
|
|
462
522
|
const last9 = vw9[vw9.length - 1];
|
|
463
523
|
if (last9 === undefined) throw new Error('no view UBO write captured');
|
|
464
|
-
expect(last9.f32[128]).toBe(
|
|
524
|
+
expect(last9.f32[128]).toBe(3);
|
|
465
525
|
}, 60000);
|
|
466
526
|
|
|
467
|
-
// --- Gate D:
|
|
468
|
-
it('validate() accepts
|
|
527
|
+
// --- Gate D: Directional filter validation ---
|
|
528
|
+
it('validate() accepts pcf5 and rejects an unknown directional filter', () => {
|
|
469
529
|
const valid = validateDirectionalLightData({
|
|
470
530
|
direction: [0, -1, 0],
|
|
471
531
|
castShadow: true,
|
|
@@ -474,7 +534,7 @@ describe('M6 shadow fields observability', () => {
|
|
|
474
534
|
cascadeBlend: 0.2,
|
|
475
535
|
mapSize: 2048,
|
|
476
536
|
shadowDistance: 50,
|
|
477
|
-
|
|
537
|
+
shadowFilter: 3,
|
|
478
538
|
});
|
|
479
539
|
expect(valid.ok).toBe(true);
|
|
480
540
|
|
|
@@ -486,7 +546,7 @@ describe('M6 shadow fields observability', () => {
|
|
|
486
546
|
cascadeBlend: 0.2,
|
|
487
547
|
mapSize: 2048,
|
|
488
548
|
shadowDistance: 50,
|
|
489
|
-
|
|
549
|
+
shadowFilter: 99,
|
|
490
550
|
});
|
|
491
551
|
expect(even.ok).toBe(false);
|
|
492
552
|
if (!even.ok) expect(even.error.code).toBe('shadow-invalid-config');
|
|
@@ -499,7 +559,7 @@ describe('M6 shadow fields observability', () => {
|
|
|
499
559
|
cascadeBlend: 0.2,
|
|
500
560
|
mapSize: 2048,
|
|
501
561
|
shadowDistance: 50,
|
|
502
|
-
|
|
562
|
+
shadowFilter: 0,
|
|
503
563
|
});
|
|
504
564
|
expect(zero.ok).toBe(false);
|
|
505
565
|
if (!zero.ok) expect(zero.error.code).toBe('shadow-invalid-config');
|
|
@@ -514,6 +574,16 @@ describe('M6 shadow fields observability', () => {
|
|
|
514
574
|
expect(d.diff).toBeGreaterThan(0);
|
|
515
575
|
}, 60000);
|
|
516
576
|
|
|
577
|
+
it('castShadow=false emits a zero directional cascade count in the View UBO', async () => {
|
|
578
|
+
if (typeof globalThis.navigator?.gpu?.requestAdapter !== 'function') return;
|
|
579
|
+
const { captured } = await renderConfigWithSpy(false, 0.005, 0.05, 3);
|
|
580
|
+
const viewWrites = captured.filter((capture) => capture.f32.length >= 148);
|
|
581
|
+
expect(viewWrites.length).toBeGreaterThan(0);
|
|
582
|
+
const last = viewWrites[viewWrites.length - 1];
|
|
583
|
+
if (last === undefined) throw new Error('no view UBO write captured');
|
|
584
|
+
expect(last.f32[124]).toBe(0);
|
|
585
|
+
}, 60000);
|
|
586
|
+
|
|
517
587
|
// --- Gate C: pixel A/B — depthBias 0.0 vs 0.5 ---
|
|
518
588
|
it('pixel A/B: depthBias 0.0 vs 0.5', async () => {
|
|
519
589
|
if (typeof globalThis.navigator?.gpu?.requestAdapter !== 'function') return;
|
|
@@ -532,8 +602,8 @@ describe('M6 shadow fields observability', () => {
|
|
|
532
602
|
expect(d.diff).toBeGreaterThan(0);
|
|
533
603
|
}, 60000);
|
|
534
604
|
|
|
535
|
-
// --- Gate C: pixel A/B —
|
|
536
|
-
it('pixel A/B:
|
|
605
|
+
// --- Gate C: pixel A/B — directional filter profile 1 vs 3 ---
|
|
606
|
+
it('pixel A/B: directional filter profile 1 vs 3', async () => {
|
|
537
607
|
if (typeof globalThis.navigator?.gpu?.requestAdapter !== 'function') return;
|
|
538
608
|
const pxA = await renderConfig(true, 0.005, 0.05, 1);
|
|
539
609
|
const pxB = await renderConfig(true, 0.005, 0.05, 5);
|
|
@@ -582,45 +652,57 @@ describe('M6 shadow fields observability', () => {
|
|
|
582
652
|
// (physically removing the UBO write), comment out the matching
|
|
583
653
|
// `viewPayload[126|127|128] = ...` line in render-system-record.ts and re-run
|
|
584
654
|
// the corresponding `varied` leg — its diff collapses to 0, matching `held`.
|
|
585
|
-
it(
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
655
|
+
it(
|
|
656
|
+
'falsification: depthBias varied differs, held does not (signal attributable to the field)',
|
|
657
|
+
async () => {
|
|
658
|
+
if (typeof globalThis.navigator?.gpu?.requestAdapter !== 'function') return;
|
|
659
|
+
const varied = diff(
|
|
660
|
+
await renderConfig(true, 0.0, 0.05, 3),
|
|
661
|
+
await renderConfig(true, 0.5, 0.05, 3),
|
|
662
|
+
);
|
|
663
|
+
const held = diff(
|
|
664
|
+
await renderConfig(true, 0.0, 0.05, 3),
|
|
665
|
+
await renderConfig(true, 0.0, 0.05, 3),
|
|
666
|
+
);
|
|
667
|
+
expect(varied.diff).toBeGreaterThan(0);
|
|
668
|
+
expect(held.diff).toBe(0);
|
|
669
|
+
},
|
|
670
|
+
SHADOW_FIELDS_FALSIFIER_TIMEOUT_MS,
|
|
671
|
+
);
|
|
598
672
|
|
|
599
|
-
it(
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
673
|
+
it(
|
|
674
|
+
'falsification: normalBias varied differs, held does not',
|
|
675
|
+
async () => {
|
|
676
|
+
if (typeof globalThis.navigator?.gpu?.requestAdapter !== 'function') return;
|
|
677
|
+
const varied = diff(
|
|
678
|
+
await renderConfig(true, 0.005, 0.0, 3),
|
|
679
|
+
await renderConfig(true, 0.005, 0.5, 3),
|
|
680
|
+
);
|
|
681
|
+
const held = diff(
|
|
682
|
+
await renderConfig(true, 0.005, 0.0, 3),
|
|
683
|
+
await renderConfig(true, 0.005, 0.0, 3),
|
|
684
|
+
);
|
|
685
|
+
expect(varied.diff).toBeGreaterThan(0);
|
|
686
|
+
expect(held.diff).toBe(0);
|
|
687
|
+
},
|
|
688
|
+
SHADOW_FIELDS_FALSIFIER_TIMEOUT_MS,
|
|
689
|
+
);
|
|
612
690
|
|
|
613
|
-
it(
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
691
|
+
it(
|
|
692
|
+
'falsification: directional filter varied differs, held does not',
|
|
693
|
+
async () => {
|
|
694
|
+
if (typeof globalThis.navigator?.gpu?.requestAdapter !== 'function') return;
|
|
695
|
+
const varied = diff(
|
|
696
|
+
await renderConfig(true, 0.005, 0.05, 1),
|
|
697
|
+
await renderConfig(true, 0.005, 0.05, 5),
|
|
698
|
+
);
|
|
699
|
+
const held = diff(
|
|
700
|
+
await renderConfig(true, 0.005, 0.05, 1),
|
|
701
|
+
await renderConfig(true, 0.005, 0.05, 1),
|
|
702
|
+
);
|
|
703
|
+
expect(varied.diff).toBeGreaterThan(0);
|
|
704
|
+
expect(held.diff).toBe(0);
|
|
705
|
+
},
|
|
706
|
+
SHADOW_FIELDS_FALSIFIER_TIMEOUT_MS,
|
|
707
|
+
);
|
|
626
708
|
});
|
|
@@ -203,7 +203,9 @@ function spawnMixedScene(world: World, castShadow: boolean): void {
|
|
|
203
203
|
depthBias: 0.005,
|
|
204
204
|
normalBias: 0.05,
|
|
205
205
|
shadowDistance: 50,
|
|
206
|
-
|
|
206
|
+
shadowFilter: 2,
|
|
207
|
+
shadowAngularRadius: 0.00465,
|
|
208
|
+
maxPenumbraTexels: 32,
|
|
207
209
|
},
|
|
208
210
|
});
|
|
209
211
|
|
|
@@ -312,7 +314,9 @@ async function renderMixed(castShadow: boolean): Promise<MixedCapture> {
|
|
|
312
314
|
if (!host.ok) throw host.error;
|
|
313
315
|
renderer = host.value.renderer;
|
|
314
316
|
unsubscribe = renderer.subscribe((event) => {
|
|
315
|
-
if (event.kind === 'error')
|
|
317
|
+
if (event.kind === 'error') {
|
|
318
|
+
errors.push(event.error.code);
|
|
319
|
+
}
|
|
316
320
|
});
|
|
317
321
|
const world = new World();
|
|
318
322
|
spawnMixedScene(world, castShadow);
|
|
@@ -260,10 +260,10 @@ describe('driveLazyEquirectProjection — lazy projection state machine (M3 / w2
|
|
|
260
260
|
const world = new World();
|
|
261
261
|
const handle = catalogEquirect(world, equirectPod());
|
|
262
262
|
const reg = new RhiErrorListenerRegistry();
|
|
263
|
-
const fired: Array<{ code: string; handle: number | undefined }> = [];
|
|
263
|
+
const fired: Array<{ code: string; handle: number | undefined; hint?: string }> = [];
|
|
264
264
|
reg.add((e) => {
|
|
265
265
|
if (e.code === 'equirect-projection-failed') {
|
|
266
|
-
fired.push({ code: e.code, handle: e.detail.handle });
|
|
266
|
+
fired.push({ code: e.code, handle: e.detail.handle, hint: e.hint });
|
|
267
267
|
} else {
|
|
268
268
|
fired.push({ code: e.code, handle: undefined });
|
|
269
269
|
}
|
|
@@ -281,9 +281,12 @@ describe('driveLazyEquirectProjection — lazy projection state machine (M3 / w2
|
|
|
281
281
|
|
|
282
282
|
// Frame 2: status:'failed' observed -> fire the structured error ONCE.
|
|
283
283
|
driveLazyEquirectProjection(internals, world, frameState, handle as unknown as number);
|
|
284
|
-
expect(fired).
|
|
285
|
-
|
|
286
|
-
|
|
284
|
+
expect(fired).toHaveLength(1);
|
|
285
|
+
expect(fired[0]).toMatchObject({
|
|
286
|
+
code: 'equirect-projection-failed',
|
|
287
|
+
handle: handle as unknown as number,
|
|
288
|
+
});
|
|
289
|
+
expect(fired[0]?.hint).toContain('declare Skylight');
|
|
287
290
|
|
|
288
291
|
// Frame 3+: the latch keeps the channel quiet -- no re-fire, no retry
|
|
289
292
|
// (the store's status:'failed' short-circuit + the frameState latch).
|
|
@@ -229,12 +229,11 @@ describe('feat-20260608 M2 m2-t5: sprite pipeline cullMode "none" lets H/V flipp
|
|
|
229
229
|
const synth = buildSyntheticRgba();
|
|
230
230
|
const synthPod = {
|
|
231
231
|
kind: 'texture' as const,
|
|
232
|
-
width: synth.width,
|
|
233
|
-
height: synth.height,
|
|
232
|
+
shape: { viewDimension: '2d' as const, extent: { width: synth.width, height: synth.height } },
|
|
234
233
|
format: 'rgba8unorm-srgb' as const,
|
|
235
234
|
data: synth.data,
|
|
236
235
|
colorSpace: 'srgb' as const,
|
|
237
|
-
|
|
236
|
+
mips: { kind: 'none' as const },
|
|
238
237
|
};
|
|
239
238
|
const textureHandle = worldFlip.allocSharedRef('TextureAsset', synthPod as never);
|
|
240
239
|
const samplerHandle = worldFlip.allocSharedRef('SamplerAsset', {
|
|
@@ -382,12 +381,11 @@ describe('feat-20260608 M2 m2-t5: sprite pipeline cullMode "none" lets H/V flipp
|
|
|
382
381
|
const synth = buildSyntheticRgba();
|
|
383
382
|
const synthPod = {
|
|
384
383
|
kind: 'texture' as const,
|
|
385
|
-
width: synth.width,
|
|
386
|
-
height: synth.height,
|
|
384
|
+
shape: { viewDimension: '2d' as const, extent: { width: synth.width, height: synth.height } },
|
|
387
385
|
format: 'rgba8unorm-srgb' as const,
|
|
388
386
|
data: synth.data,
|
|
389
387
|
colorSpace: 'srgb' as const,
|
|
390
|
-
|
|
388
|
+
mips: { kind: 'none' as const },
|
|
391
389
|
};
|
|
392
390
|
const textureHandle = worldFlipBoth.allocSharedRef('TextureAsset', synthPod as never);
|
|
393
391
|
const samplerHandle = worldFlipBoth.allocSharedRef('SamplerAsset', {
|
|
@@ -47,7 +47,7 @@ function makeSpec(shaderId: string): PipelineSpec {
|
|
|
47
47
|
|
|
48
48
|
describe('sprite-lit BGL byte-identical to sprite (AC-07, w4/w5 close)', () => {
|
|
49
49
|
describe('pbr-view BGL (light buffers; binding 1+2)', () => {
|
|
50
|
-
it('sprite-lit vs sprite share the same
|
|
50
|
+
it('sprite-lit vs sprite share the same 12-entry pbr-view BGL under storage-buffer caps', () => {
|
|
51
51
|
const spriteSpec = makeSpec('forgeax::sprite');
|
|
52
52
|
const spriteLitSpec = makeSpec('forgeax::sprite-lit');
|
|
53
53
|
const sprite = buildBindGroupLayoutDescriptor(spriteSpec, {
|
|
@@ -59,7 +59,9 @@ describe('sprite-lit BGL byte-identical to sprite (AC-07, w4/w5 close)', () => {
|
|
|
59
59
|
caps: { storageBuffer: true },
|
|
60
60
|
});
|
|
61
61
|
expect(JSON.stringify(spriteLit)).toBe(JSON.stringify(sprite));
|
|
62
|
-
|
|
62
|
+
// binding 10 is the shared Points/Lines view UBO; bindings 11/12 are the
|
|
63
|
+
// optional SpotLight projector texture and sampler on capable devices.
|
|
64
|
+
expect(spriteLit.entries.length).toBe(12);
|
|
63
65
|
});
|
|
64
66
|
|
|
65
67
|
it('sprite-lit vs sprite share the same 10-entry pbr-view BGL under uniform fallback caps (AC-10)', () => {
|