@forgeax/engine-runtime 0.1.20 → 0.1.23
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 +105 -3
- 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/register-runtime-components.d.ts.map +1 -1
- package/dist/__tests__/helpers/register-scene-components.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__/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 +4 -2
- package/dist/index.mjs.map +1 -1
- package/dist/sprite-param-values.d.ts +1 -1
- package/package.json +30 -30
- package/src/__tests__/active-camera.unit.test.ts +6 -8
- package/src/__tests__/animation-graph-asset-union.test-d.ts +2 -0
- package/src/__tests__/asset-brand-and-name-retirement.test.ts +4 -3
- package/src/__tests__/asset-registry-scene.test-d.ts +1 -0
- package/src/__tests__/asset-registry.recursive.spec.ts +1 -1
- package/src/__tests__/asset.unit.test.ts +5 -2
- package/src/__tests__/clamp-to-last.e2e.browser.test.ts +2 -2
- package/src/__tests__/collect-transient-roundtrip.test.ts +14 -3
- package/src/__tests__/components.test-d.ts +19 -9
- package/src/__tests__/components.unit.test.ts +36 -26
- 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 +54 -20
- package/src/__tests__/extract-frames-merge.test.ts +51 -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__/helpers/register-runtime-components.ts +9 -1
- package/src/__tests__/helpers/register-scene-components.ts +9 -2
- package/src/__tests__/light-gpu-byte-neutral.unit.test.ts +10 -11
- package/src/__tests__/light-snapshot.test-d.ts +35 -1
- package/src/__tests__/lights-preservation.fixup.test.ts +125 -0
- package/src/__tests__/lights.unit.test.ts +223 -308
- package/src/__tests__/loader-name-survival.test.ts +8 -9
- package/src/__tests__/package-promotion.test.ts +7 -11
- package/src/__tests__/pbr-pipeline.unit.test.ts +22 -17
- package/src/__tests__/pipeline.unit.test.ts +68 -226
- package/src/__tests__/point-light-shadow.browser.test.ts +209 -83
- package/src/__tests__/point-light-shadow.unit.test.ts +24 -47
- package/src/__tests__/propagate-transforms.dawn.test.ts +15 -16
- package/src/__tests__/record-worldid-isolation.test.ts +16 -32
- package/src/__tests__/render-error-exhaustive.test-d.ts +73 -9
- package/src/__tests__/render-system-extract.test.ts +5 -5
- package/src/__tests__/render-system-mega.test.ts +30 -41
- package/src/__tests__/render-system-record-multi-material-textureview.test.ts +5 -1
- package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +5 -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 -5
- package/src/__tests__/resolveName-unified-entry.test.ts +6 -6
- package/src/__tests__/resolveName.test.ts +14 -2
- package/src/__tests__/shadow-csm-atlas.test.ts +41 -0
- package/src/__tests__/shadow-csm-cascade-loadop.test.ts +1 -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-tile-consistency.test.ts +1 -0
- package/src/__tests__/shadow-csm-ubo.test.ts +43 -45
- package/src/__tests__/shadow-csm-viewport.browser.test.ts +29 -0
- package/src/__tests__/shadow-fields-observable.dawn.test.ts +100 -24
- package/src/__tests__/skin-extract-getarrayview-count.unit.test.ts +1 -1
- package/src/__tests__/skin-parented-double-transform.test.ts +5 -5
- package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +4 -7
- package/src/__tests__/skylight-lazy-projection.test.ts +8 -5
- package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +11 -13
- package/src/__tests__/ssao-bgl.test.ts +12 -17
- package/src/__tests__/ssao-passes.test.ts +19 -2
- package/src/__tests__/systems.unit.test.ts +69 -205
- package/src/__tests__/template-game-default-api-contract.test.ts +45 -23
- package/src/__tests__/tilemap-chunk-extract.propagate-order.test.ts +10 -4
- package/src/__tests__/transform-hierarchy-pixel-diff.dawn.test.ts +2 -2
- package/src/__tests__/view-ubo-layout.browser.test.ts +20 -10
- package/src/collect-scene-asset.ts +1 -1
- package/src/components/__tests__/transform-vec-schema.test.ts +2 -2
- package/src/createRenderer.ts +2 -0
- package/src/index.ts +1 -2
- package/src/sprite-param-values.ts +1 -1
- package/dist/.tsbuildinfo +0 -1
|
@@ -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
|
|
@@ -29,11 +29,18 @@ import { validateDirectionalLightData } from '../../../render/src/components/lig
|
|
|
29
29
|
import { constructRuntimeRendererHost } from '../renderer-host';
|
|
30
30
|
import { drawPublished } from './draw-published';
|
|
31
31
|
|
|
32
|
-
const
|
|
33
|
-
|
|
32
|
+
const LIGHTWEIGHT_DAWN = process.env.FORGEAX_DAWN_LIGHTWEIGHT === '1';
|
|
33
|
+
// The CI observability lane only needs enough settled frames to prove the
|
|
34
|
+
// field-owned pixel/UBO deltas. Keep the full 300-frame, 256x256 window for
|
|
35
|
+
// local and nightly runs, while the isolated PR process uses a bounded 60-frame
|
|
36
|
+
// 128x128 target to avoid retaining a large lavapipe command backlog.
|
|
37
|
+
const SHADOW_FIELDS_RENDER_FRAMES = LIGHTWEIGHT_DAWN ? 60 : 300;
|
|
38
|
+
const WIDTH = LIGHTWEIGHT_DAWN ? 128 : 256;
|
|
39
|
+
const HEIGHT = LIGHTWEIGHT_DAWN ? 128 : 256;
|
|
34
40
|
const BPR = Math.ceil((WIDTH * 4) / 256) * 256;
|
|
35
41
|
|
|
36
|
-
// Each falsifier performs four fresh
|
|
42
|
+
// Each falsifier performs four fresh Dawn readbacks at the active target size
|
|
43
|
+
// (256x256 by default, 128x128 in the lightweight CI lane). Windows' Dawn
|
|
37
44
|
// prebuild can exceed Vitest's 90-second bound for that sequence while still
|
|
38
45
|
// completing well inside the nightly platform job's 45-minute budget.
|
|
39
46
|
const SHADOW_FIELDS_FALSIFIER_TIMEOUT_MS = 300_000;
|
|
@@ -44,6 +51,59 @@ const ENGINE_MANIFEST = await (async () => {
|
|
|
44
51
|
})();
|
|
45
52
|
const ENGINE_MANIFEST_URL = `data:application/json,${encodeURIComponent(JSON.stringify(ENGINE_MANIFEST))}`;
|
|
46
53
|
|
|
54
|
+
describe('M3 Dawn candidate admission', () => {
|
|
55
|
+
it('requires accepted production candidate to preserve the requested PCSS profile', async () => {
|
|
56
|
+
const module = (await import('../../../render/src/render-pipeline')) as Record<string, unknown>;
|
|
57
|
+
const resolver = module.resolveDirectionalShadowBackendAdmission;
|
|
58
|
+
expect(typeof resolver).toBe('function');
|
|
59
|
+
const result = (
|
|
60
|
+
resolver as (input: {
|
|
61
|
+
backendKind: 'webgpu';
|
|
62
|
+
requested: 'pcssMedium' | 'pcssHigh';
|
|
63
|
+
candidate: 'accepted' | 'failed';
|
|
64
|
+
}) => { effective: string; status: string; pixelEvidence: string }
|
|
65
|
+
)({ backendKind: 'webgpu', requested: 'pcssHigh', candidate: 'accepted' });
|
|
66
|
+
expect(result).toMatchObject({
|
|
67
|
+
effective: 'pcssHigh',
|
|
68
|
+
status: 'accepted',
|
|
69
|
+
pixelEvidence: 'available',
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('requires the same bounded Directional inspection projection used by Dawn', async () => {
|
|
74
|
+
const module = (await import(
|
|
75
|
+
'../../../render/src/assembly/directional-shadow-inspection'
|
|
76
|
+
)) as Record<string, unknown>;
|
|
77
|
+
const project = module.projectDirectionalShadowInspection;
|
|
78
|
+
expect(typeof project).toBe('function');
|
|
79
|
+
const result = (project as (input: Record<string, unknown>) => Record<string, unknown>)({
|
|
80
|
+
admission: {
|
|
81
|
+
requested: 'pcssMedium',
|
|
82
|
+
effective: 'pcssMedium',
|
|
83
|
+
status: 'accepted',
|
|
84
|
+
pixelEvidence: 'available',
|
|
85
|
+
},
|
|
86
|
+
cascadeCount: 4,
|
|
87
|
+
mapSize: 1024,
|
|
88
|
+
atlasBytes: 16_777_216,
|
|
89
|
+
writerPasses: 4,
|
|
90
|
+
blockerTaps: 8,
|
|
91
|
+
filterTapUpperBound: 16,
|
|
92
|
+
seamTapUpperBound: 48,
|
|
93
|
+
deviceGeneration: 1,
|
|
94
|
+
graphGeneration: 3,
|
|
95
|
+
});
|
|
96
|
+
expect(result).toMatchObject({
|
|
97
|
+
requested: 'pcssMedium',
|
|
98
|
+
effective: 'pcssMedium',
|
|
99
|
+
blockerTaps: 8,
|
|
100
|
+
filterTapUpperBound: 16,
|
|
101
|
+
seamTapUpperBound: 48,
|
|
102
|
+
});
|
|
103
|
+
expect(Object.keys(result)).not.toContain('perPixel');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
47
107
|
// Helper types
|
|
48
108
|
interface WriteCapture {
|
|
49
109
|
f32: Float32Array;
|
|
@@ -177,7 +237,9 @@ function spawnScene(
|
|
|
177
237
|
depthBias,
|
|
178
238
|
normalBias,
|
|
179
239
|
shadowDistance: 50,
|
|
180
|
-
|
|
240
|
+
shadowFilter: pcf === 1 ? 1 : pcf === 3 ? 2 : 3,
|
|
241
|
+
shadowAngularRadius: 0.00465,
|
|
242
|
+
maxPenumbraTexels: 32,
|
|
181
243
|
},
|
|
182
244
|
});
|
|
183
245
|
world.spawn(
|
|
@@ -300,9 +362,13 @@ async function renderConfig(
|
|
|
300
362
|
const w = new World();
|
|
301
363
|
spawnScene(w, castShadow, depthBias, normalBias, pcf, mapSize);
|
|
302
364
|
let de = 0;
|
|
303
|
-
for (let i = 0; i <
|
|
365
|
+
for (let i = 0; i < SHADOW_FIELDS_RENDER_FRAMES; i++) {
|
|
304
366
|
const r = drawPublished(renderer, w);
|
|
305
367
|
if (!r.ok) de++;
|
|
368
|
+
// Keep the native Dawn carrier paced like a real frame loop. Without a
|
|
369
|
+
// bounded drain, a long shadow-map leg can retain an unbounded lavapipe
|
|
370
|
+
// command/resource backlog before the final fence below.
|
|
371
|
+
if (i % 16 === 15) await dev.queue.onSubmittedWorkDone();
|
|
306
372
|
}
|
|
307
373
|
if (de > 0) throw new Error(`draw errors: ${de}`);
|
|
308
374
|
// rt is created lazily by the canvas configure()/getCurrentTexture path during
|
|
@@ -437,15 +503,15 @@ async function renderConfigWithSpy(
|
|
|
437
503
|
|
|
438
504
|
describe('M6 shadow fields observability', () => {
|
|
439
505
|
// --- Gate A: structural WGSL check ---
|
|
440
|
-
it('composed default-standard-pbr WGSL reads view.depthBias, view.normalBias,
|
|
506
|
+
it('composed default-standard-pbr WGSL reads view.depthBias, view.normalBias, and directionalShadowFilter', () => {
|
|
441
507
|
const wgsl = composedStandardPbr();
|
|
442
508
|
expect(wgsl).toContain('depthBias');
|
|
443
509
|
expect(wgsl).toContain('normalBias');
|
|
444
|
-
expect(wgsl).toContain('
|
|
510
|
+
expect(wgsl).toContain('directionalShadowFilter');
|
|
445
511
|
});
|
|
446
512
|
|
|
447
|
-
// --- Gate B: UBO spy — slots [126]=depthBias, [127]=normalBias, [128]=
|
|
448
|
-
it('View UBO slots [126]=depthBias, [127]=normalBias, [128]=
|
|
513
|
+
// --- Gate B: UBO spy — slots [126]=depthBias, [127]=normalBias, [128]=filter profile ---
|
|
514
|
+
it('View UBO slots [126]=depthBias, [127]=normalBias, [128]=filter profile', async () => {
|
|
449
515
|
if (typeof globalThis.navigator?.gpu?.requestAdapter !== 'function') return;
|
|
450
516
|
const { captured } = await renderConfigWithSpy(true, 0.123, 0.456, 3);
|
|
451
517
|
const vw = captured.filter((c) => c.f32.length >= 148);
|
|
@@ -455,22 +521,22 @@ describe('M6 shadow fields observability', () => {
|
|
|
455
521
|
const f32 = last.f32;
|
|
456
522
|
expect(Math.abs((f32[126] ?? Number.NaN) - 0.123)).toBeLessThan(0.001);
|
|
457
523
|
expect(Math.abs((f32[127] ?? Number.NaN) - 0.456)).toBeLessThan(0.001);
|
|
458
|
-
expect(f32[128]).toBe(
|
|
524
|
+
expect(f32[128]).toBe(2);
|
|
459
525
|
}, 60000);
|
|
460
526
|
|
|
461
|
-
// --- Gate D:
|
|
462
|
-
it('
|
|
527
|
+
// --- Gate D: pcf5 maps to the accepted profile carrier ---
|
|
528
|
+
it('pcf5 maps to profile 3 in View UBO[128]', async () => {
|
|
463
529
|
if (typeof globalThis.navigator?.gpu?.requestAdapter !== 'function') return;
|
|
464
|
-
const { captured } = await renderConfigWithSpy(true, 0.005, 0.05,
|
|
530
|
+
const { captured } = await renderConfigWithSpy(true, 0.005, 0.05, 5);
|
|
465
531
|
const vw9 = captured.filter((c) => c.f32.length >= 148);
|
|
466
532
|
expect(vw9.length).toBeGreaterThan(0);
|
|
467
533
|
const last9 = vw9[vw9.length - 1];
|
|
468
534
|
if (last9 === undefined) throw new Error('no view UBO write captured');
|
|
469
|
-
expect(last9.f32[128]).toBe(
|
|
535
|
+
expect(last9.f32[128]).toBe(3);
|
|
470
536
|
}, 60000);
|
|
471
537
|
|
|
472
|
-
// --- Gate D:
|
|
473
|
-
it('validate() accepts
|
|
538
|
+
// --- Gate D: Directional filter validation ---
|
|
539
|
+
it('validate() accepts pcf5 and rejects an unknown directional filter', () => {
|
|
474
540
|
const valid = validateDirectionalLightData({
|
|
475
541
|
direction: [0, -1, 0],
|
|
476
542
|
castShadow: true,
|
|
@@ -479,7 +545,7 @@ describe('M6 shadow fields observability', () => {
|
|
|
479
545
|
cascadeBlend: 0.2,
|
|
480
546
|
mapSize: 2048,
|
|
481
547
|
shadowDistance: 50,
|
|
482
|
-
|
|
548
|
+
shadowFilter: 3,
|
|
483
549
|
});
|
|
484
550
|
expect(valid.ok).toBe(true);
|
|
485
551
|
|
|
@@ -491,7 +557,7 @@ describe('M6 shadow fields observability', () => {
|
|
|
491
557
|
cascadeBlend: 0.2,
|
|
492
558
|
mapSize: 2048,
|
|
493
559
|
shadowDistance: 50,
|
|
494
|
-
|
|
560
|
+
shadowFilter: 99,
|
|
495
561
|
});
|
|
496
562
|
expect(even.ok).toBe(false);
|
|
497
563
|
if (!even.ok) expect(even.error.code).toBe('shadow-invalid-config');
|
|
@@ -504,7 +570,7 @@ describe('M6 shadow fields observability', () => {
|
|
|
504
570
|
cascadeBlend: 0.2,
|
|
505
571
|
mapSize: 2048,
|
|
506
572
|
shadowDistance: 50,
|
|
507
|
-
|
|
573
|
+
shadowFilter: 0,
|
|
508
574
|
});
|
|
509
575
|
expect(zero.ok).toBe(false);
|
|
510
576
|
if (!zero.ok) expect(zero.error.code).toBe('shadow-invalid-config');
|
|
@@ -519,6 +585,16 @@ describe('M6 shadow fields observability', () => {
|
|
|
519
585
|
expect(d.diff).toBeGreaterThan(0);
|
|
520
586
|
}, 60000);
|
|
521
587
|
|
|
588
|
+
it('castShadow=false emits a zero directional cascade count in the View UBO', async () => {
|
|
589
|
+
if (typeof globalThis.navigator?.gpu?.requestAdapter !== 'function') return;
|
|
590
|
+
const { captured } = await renderConfigWithSpy(false, 0.005, 0.05, 3);
|
|
591
|
+
const viewWrites = captured.filter((capture) => capture.f32.length >= 148);
|
|
592
|
+
expect(viewWrites.length).toBeGreaterThan(0);
|
|
593
|
+
const last = viewWrites[viewWrites.length - 1];
|
|
594
|
+
if (last === undefined) throw new Error('no view UBO write captured');
|
|
595
|
+
expect(last.f32[124]).toBe(0);
|
|
596
|
+
}, 60000);
|
|
597
|
+
|
|
522
598
|
// --- Gate C: pixel A/B — depthBias 0.0 vs 0.5 ---
|
|
523
599
|
it('pixel A/B: depthBias 0.0 vs 0.5', async () => {
|
|
524
600
|
if (typeof globalThis.navigator?.gpu?.requestAdapter !== 'function') return;
|
|
@@ -537,8 +613,8 @@ describe('M6 shadow fields observability', () => {
|
|
|
537
613
|
expect(d.diff).toBeGreaterThan(0);
|
|
538
614
|
}, 60000);
|
|
539
615
|
|
|
540
|
-
// --- Gate C: pixel A/B —
|
|
541
|
-
it('pixel A/B:
|
|
616
|
+
// --- Gate C: pixel A/B — directional filter profile 1 vs 3 ---
|
|
617
|
+
it('pixel A/B: directional filter profile 1 vs 3', async () => {
|
|
542
618
|
if (typeof globalThis.navigator?.gpu?.requestAdapter !== 'function') return;
|
|
543
619
|
const pxA = await renderConfig(true, 0.005, 0.05, 1);
|
|
544
620
|
const pxB = await renderConfig(true, 0.005, 0.05, 5);
|
|
@@ -624,7 +700,7 @@ describe('M6 shadow fields observability', () => {
|
|
|
624
700
|
);
|
|
625
701
|
|
|
626
702
|
it(
|
|
627
|
-
'falsification:
|
|
703
|
+
'falsification: directional filter varied differs, held does not',
|
|
628
704
|
async () => {
|
|
629
705
|
if (typeof globalThis.navigator?.gpu?.requestAdapter !== 'function') return;
|
|
630
706
|
const varied = diff(
|
|
@@ -15,7 +15,7 @@ const extractPath = (() => {
|
|
|
15
15
|
describe('_getArrayView count (AC-03 gate)', () => {
|
|
16
16
|
it('keeps skin and instance hot fields on column views', () => {
|
|
17
17
|
const src = readFileSync(extractPath, 'utf8');
|
|
18
|
-
expect(src).toContain("_getArrayView(jointEntity,
|
|
18
|
+
expect(src).toContain("_getArrayView(jointEntity, GlobalTransform, 'world')");
|
|
19
19
|
expect(src).toContain("_getArrayView(entity, Instances, 'transforms')");
|
|
20
20
|
expect(src).toContain("_getArrayView(entity, SpriteInstances, 'transforms')");
|
|
21
21
|
expect(src).toContain("_getArrayView(entity, SpriteInstances, 'regions')");
|
|
@@ -34,7 +34,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
34
34
|
import { World } from '@forgeax/engine-ecs';
|
|
35
35
|
import type { Mat4, Vec3 } from '@forgeax/engine-math';
|
|
36
36
|
import { mat4, vec3 } from '@forgeax/engine-math';
|
|
37
|
-
import { ChildOf, propagateTransforms, Transform } from '@forgeax/engine-scene';
|
|
37
|
+
import { ChildOf, GlobalTransform, propagateTransforms, Transform } from '@forgeax/engine-scene';
|
|
38
38
|
import { Skin } from '@forgeax/engine-skinning';
|
|
39
39
|
import { describe, expect, it } from 'vitest';
|
|
40
40
|
|
|
@@ -125,13 +125,13 @@ describe('skin parented double transform (AC-01)', () => {
|
|
|
125
125
|
const r = propagateTransforms(world);
|
|
126
126
|
expect(r.ok).toBe(true);
|
|
127
127
|
|
|
128
|
-
// Read
|
|
129
|
-
const st = world.get(skinEntity,
|
|
128
|
+
// Read derived GlobalTransform.world for skin entity and joint entity.
|
|
129
|
+
const st = world.get(skinEntity, GlobalTransform);
|
|
130
130
|
expect(st.ok).toBe(true);
|
|
131
131
|
if (!st.ok) return;
|
|
132
132
|
const meshWorldMat = mat4FromFloat32Array(st.value.world as unknown as Float32Array);
|
|
133
133
|
|
|
134
|
-
const jt = world.get(jointEntity,
|
|
134
|
+
const jt = world.get(jointEntity, GlobalTransform);
|
|
135
135
|
expect(jt.ok).toBe(true);
|
|
136
136
|
if (!jt.ok) return;
|
|
137
137
|
const jointWorldMat = mat4FromFloat32Array(jt.value.world as unknown as Float32Array);
|
|
@@ -202,7 +202,7 @@ describe('skin parented double transform (AC-01)', () => {
|
|
|
202
202
|
const r = propagateTransforms(world);
|
|
203
203
|
expect(r.ok).toBe(true);
|
|
204
204
|
|
|
205
|
-
const jt = world.get(jointEntity,
|
|
205
|
+
const jt = world.get(jointEntity, GlobalTransform);
|
|
206
206
|
expect(jt.ok).toBe(true);
|
|
207
207
|
if (!jt.ok) return;
|
|
208
208
|
const jointWorldMat = mat4FromFloat32Array(jt.value.world as unknown as Float32Array);
|
|
@@ -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
|
|
|
@@ -216,12 +218,7 @@ function spawnMixedScene(world: World, castShadow: boolean): void {
|
|
|
216
218
|
};
|
|
217
219
|
// Entity order is intentional: static A -> skinned B -> static C.
|
|
218
220
|
spawnStaticCaster(-3);
|
|
219
|
-
const joint = world
|
|
220
|
-
.spawn({
|
|
221
|
-
component: Transform,
|
|
222
|
-
data: identityTransform([0, 7, 5]),
|
|
223
|
-
})
|
|
224
|
-
.unwrap();
|
|
221
|
+
const joint = world.spawn({ component: Transform, data: identityTransform([0, 7, 5]) }).unwrap();
|
|
225
222
|
world.spawn(
|
|
226
223
|
{ component: Transform, data: identityTransform() },
|
|
227
224
|
{ component: MeshFilter, data: { assetHandle: skinMesh } },
|
|
@@ -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).
|
|
@@ -46,22 +46,22 @@ function makeSpec(shaderId: string): PipelineSpec {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
describe('sprite-lit BGL byte-identical to sprite (AC-07, w4/w5 close)', () => {
|
|
49
|
-
describe('pbr-view BGL (
|
|
50
|
-
it('sprite-lit vs sprite share the same
|
|
49
|
+
describe('pbr-view BGL (Cluster lights live in group(2))', () => {
|
|
50
|
+
it('sprite-lit vs sprite share the same 10-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, {
|
|
54
54
|
kind: 'pbr-view',
|
|
55
|
-
caps: { storageBuffer: true },
|
|
55
|
+
caps: { storageBuffer: true, extendedLighting: false },
|
|
56
56
|
});
|
|
57
57
|
const spriteLit = buildBindGroupLayoutDescriptor(spriteLitSpec, {
|
|
58
58
|
kind: 'pbr-view',
|
|
59
|
-
caps: { storageBuffer: true },
|
|
59
|
+
caps: { storageBuffer: true, extendedLighting: false },
|
|
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
63
|
// optional SpotLight projector texture and sampler on capable devices.
|
|
64
|
-
expect(spriteLit.entries.length).toBe(
|
|
64
|
+
expect(spriteLit.entries.length).toBe(10);
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
it('sprite-lit vs sprite share the same 10-entry pbr-view BGL under uniform fallback caps (AC-10)', () => {
|
|
@@ -69,25 +69,23 @@ describe('sprite-lit BGL byte-identical to sprite (AC-07, w4/w5 close)', () => {
|
|
|
69
69
|
const spriteLitSpec = makeSpec('forgeax::sprite-lit');
|
|
70
70
|
const sprite = buildBindGroupLayoutDescriptor(spriteSpec, {
|
|
71
71
|
kind: 'pbr-view',
|
|
72
|
-
caps: { storageBuffer: false },
|
|
72
|
+
caps: { storageBuffer: false, extendedLighting: false },
|
|
73
73
|
});
|
|
74
74
|
const spriteLit = buildBindGroupLayoutDescriptor(spriteLitSpec, {
|
|
75
75
|
kind: 'pbr-view',
|
|
76
|
-
caps: { storageBuffer: false },
|
|
76
|
+
caps: { storageBuffer: false, extendedLighting: false },
|
|
77
77
|
});
|
|
78
78
|
expect(JSON.stringify(spriteLit)).toBe(JSON.stringify(sprite));
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
it('
|
|
81
|
+
it('does not expose point/spot light bindings on storageBuffer=true', () => {
|
|
82
82
|
const entries = buildPbrViewBglEntries({ storageBuffer: true });
|
|
83
|
-
expect(entries
|
|
84
|
-
expect(entries[2]?.buffer?.type).toBe('read-only-storage');
|
|
83
|
+
expect(entries.some((entry) => entry.binding === 1 || entry.binding === 2)).toBe(false);
|
|
85
84
|
});
|
|
86
85
|
|
|
87
|
-
it('
|
|
86
|
+
it('does not expose point/spot light bindings on storageBuffer=false', () => {
|
|
88
87
|
const entries = buildPbrViewBglEntries({ storageBuffer: false });
|
|
89
|
-
expect(entries
|
|
90
|
-
expect(entries[2]?.buffer?.type).toBe('uniform');
|
|
88
|
+
expect(entries.some((entry) => entry.binding === 1 || entry.binding === 2)).toBe(false);
|
|
91
89
|
});
|
|
92
90
|
});
|
|
93
91
|
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
getOrCreateSsaoFallbackTexture,
|
|
40
40
|
packClusterUniform,
|
|
41
41
|
} from '../../../render/src/hdrp-buffers';
|
|
42
|
+
import { BYTES_PER_DIRECT_LIGHT_SLOT } from '../../../render/src/light-buffer-layout';
|
|
42
43
|
import { createHdrpBindGroupLayoutDescriptor } from '../../../render/src/pipeline-spec';
|
|
43
44
|
import type { RenderSystemRuntime } from '../../../render/src/render-system';
|
|
44
45
|
|
|
@@ -131,9 +132,9 @@ function makeMockRuntime(capsOverride: Partial<RhiCaps> = {}): {
|
|
|
131
132
|
|
|
132
133
|
function fakeHdrpBuffers(): HdrpBuffers {
|
|
133
134
|
return {
|
|
134
|
-
|
|
135
|
+
device: {} as HdrpBuffers['device'],
|
|
135
136
|
lightDataBuffer: mockBuffer('hdrp-light-data'),
|
|
136
|
-
lightDataBytes:
|
|
137
|
+
lightDataBytes: 256 * BYTES_PER_DIRECT_LIGHT_SLOT,
|
|
137
138
|
clusterGridBuffer: mockBuffer('hdrp-cluster-grid'),
|
|
138
139
|
clusterGridBytes: 1024,
|
|
139
140
|
lightIndexListBuffer: mockBuffer('hdrp-light-index-list'),
|
|
@@ -150,15 +151,9 @@ function fakeHdrpBuffers(): HdrpBuffers {
|
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
describe('HDRP GPU membership producer bind-group contract', () => {
|
|
153
|
-
it('preserves the 256-light
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
);
|
|
157
|
-
const storagePayload = new Uint32Array(
|
|
158
|
-
packClusterUniform({ x: 16, y: 9, z: 24 }, 0.1, 50, 0, 256, 256),
|
|
159
|
-
);
|
|
160
|
-
expect(uniformPayload[3]).toBe(128);
|
|
161
|
-
expect(storagePayload[3]).toBe(256);
|
|
154
|
+
it('preserves the full 256-light Standard count in the cluster UBO', () => {
|
|
155
|
+
const payload = new Uint32Array(packClusterUniform({ x: 16, y: 9, z: 24 }, 0.1, 50, 0, 256));
|
|
156
|
+
expect(payload[3]).toBe(256);
|
|
162
157
|
});
|
|
163
158
|
|
|
164
159
|
it('keeps enabled SSAO intensity in the zero-punctual-light uniform path', () => {
|
|
@@ -209,12 +204,12 @@ describe('HDRP GPU membership producer bind-group contract', () => {
|
|
|
209
204
|
|
|
210
205
|
describe('w28 — HDRP unified BGL SSAO entries (binding 7/8)', () => {
|
|
211
206
|
it('descriptor entries.length === 7 (5 cluster + 2 ssao)', () => {
|
|
212
|
-
const desc = createHdrpBindGroupLayoutDescriptor(
|
|
207
|
+
const desc = createHdrpBindGroupLayoutDescriptor();
|
|
213
208
|
expect(desc.entries?.length).toBe(7);
|
|
214
209
|
});
|
|
215
210
|
|
|
216
211
|
it('binding 7 is texture_2d<f32> with FRAGMENT visibility (ssaoBlurred)', () => {
|
|
217
|
-
const desc = createHdrpBindGroupLayoutDescriptor(
|
|
212
|
+
const desc = createHdrpBindGroupLayoutDescriptor();
|
|
218
213
|
const b7 = desc.entries?.find((e) => e.binding === 7);
|
|
219
214
|
expect(b7).toBeDefined();
|
|
220
215
|
expect(b7?.visibility).toBe(FRAGMENT_VISIBILITY);
|
|
@@ -223,7 +218,7 @@ describe('w28 — HDRP unified BGL SSAO entries (binding 7/8)', () => {
|
|
|
223
218
|
});
|
|
224
219
|
|
|
225
220
|
it('binding 8 is sampler with FRAGMENT visibility (ssaoSampler)', () => {
|
|
226
|
-
const desc = createHdrpBindGroupLayoutDescriptor(
|
|
221
|
+
const desc = createHdrpBindGroupLayoutDescriptor();
|
|
227
222
|
const b8 = desc.entries?.find((e) => e.binding === 8);
|
|
228
223
|
expect(b8).toBeDefined();
|
|
229
224
|
expect(b8?.visibility).toBe(FRAGMENT_VISIBILITY);
|
|
@@ -231,13 +226,13 @@ describe('w28 — HDRP unified BGL SSAO entries (binding 7/8)', () => {
|
|
|
231
226
|
});
|
|
232
227
|
|
|
233
228
|
it('binding 9 is absent (scope-amend-webgl2-ubo: intensity folded into binding 6)', () => {
|
|
234
|
-
const desc = createHdrpBindGroupLayoutDescriptor(
|
|
229
|
+
const desc = createHdrpBindGroupLayoutDescriptor();
|
|
235
230
|
const bindings = desc.entries?.map((e) => e.binding) ?? [];
|
|
236
231
|
expect(bindings).not.toContain(9);
|
|
237
232
|
});
|
|
238
233
|
|
|
239
234
|
it('existing bindings 0/3/4/5/6 remain unchanged', () => {
|
|
240
|
-
const desc = createHdrpBindGroupLayoutDescriptor(
|
|
235
|
+
const desc = createHdrpBindGroupLayoutDescriptor();
|
|
241
236
|
const b0 = desc.entries?.find((e) => e.binding === 0);
|
|
242
237
|
expect(b0?.buffer?.hasDynamicOffset).toBe(true);
|
|
243
238
|
for (const b of [3, 4, 5, 6]) {
|
|
@@ -247,7 +242,7 @@ describe('w28 — HDRP unified BGL SSAO entries (binding 7/8)', () => {
|
|
|
247
242
|
});
|
|
248
243
|
|
|
249
244
|
it('all bindings exactly { 0, 3, 4, 5, 6, 7, 8 }', () => {
|
|
250
|
-
const desc = createHdrpBindGroupLayoutDescriptor(
|
|
245
|
+
const desc = createHdrpBindGroupLayoutDescriptor();
|
|
251
246
|
const bindings = new Set(desc.entries?.map((e) => e.binding));
|
|
252
247
|
expect(bindings).toEqual(new Set([0, 3, 4, 5, 6, 7, 8]));
|
|
253
248
|
});
|
|
@@ -63,7 +63,14 @@ interface SpyCtx {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
function makeDispatchSpyCtx(
|
|
66
|
-
opts: {
|
|
66
|
+
opts: {
|
|
67
|
+
ssaoCalcPipeline?: unknown;
|
|
68
|
+
ssaoBlurPipeline?: unknown;
|
|
69
|
+
ssaoBgl?: unknown;
|
|
70
|
+
fxaaPipeline?: unknown;
|
|
71
|
+
fxaaBindGroupLayout?: unknown;
|
|
72
|
+
fxaaSampler?: unknown;
|
|
73
|
+
} = {},
|
|
67
74
|
): SpyCtx {
|
|
68
75
|
const spy: DispatchSpy = {
|
|
69
76
|
beginRenderPassCalls: [],
|
|
@@ -138,10 +145,12 @@ function makeDispatchSpyCtx(
|
|
|
138
145
|
removeEventListener: vi.fn(),
|
|
139
146
|
};
|
|
140
147
|
|
|
148
|
+
const fxaaParamsBuffer = { __label: 'fxaa-params' };
|
|
141
149
|
const runtime = {
|
|
142
150
|
device,
|
|
143
151
|
errorRegistry,
|
|
144
152
|
shaderCache: { get: vi.fn() },
|
|
153
|
+
getPostProcessParamsBuffer: vi.fn(() => fxaaParamsBuffer),
|
|
145
154
|
} as unknown as RenderSystemRuntime;
|
|
146
155
|
|
|
147
156
|
// Pre-seed ssao-buffers cache so the record closure resolves a stable
|
|
@@ -162,6 +171,13 @@ function makeDispatchSpyCtx(
|
|
|
162
171
|
ssaoFilteringSampler: null,
|
|
163
172
|
ssaoDepthSampler: null,
|
|
164
173
|
ssaoFallbackRawView: null,
|
|
174
|
+
// FXAA is part of the same Standard post chain. Keep the fixture's
|
|
175
|
+
// fullscreen resources complete so recordAll exercises the real FXAA
|
|
176
|
+
// bind-group path instead of returning early at its capability gate.
|
|
177
|
+
fxaaPipeline: 'fxaaPipeline' in opts ? opts.fxaaPipeline : { __label: 'fxaa-pipeline' },
|
|
178
|
+
fxaaBindGroupLayout:
|
|
179
|
+
'fxaaBindGroupLayout' in opts ? opts.fxaaBindGroupLayout : { __label: 'fxaa-bgl' },
|
|
180
|
+
fxaaSampler: 'fxaaSampler' in opts ? opts.fxaaSampler : { __label: 'fxaa-sampler' },
|
|
165
181
|
},
|
|
166
182
|
};
|
|
167
183
|
|
|
@@ -215,7 +231,6 @@ function makeDispatchSpyCtx(
|
|
|
215
231
|
meshBindGroup: null,
|
|
216
232
|
frameState: {
|
|
217
233
|
perFrameGraph: mockPerFrameGraph,
|
|
218
|
-
isHdrpActive: true,
|
|
219
234
|
installedPipelineConfig: { ssao: { enabled: true, intensity: 1.0 } },
|
|
220
235
|
// Post-process bind group identity cache (bloom / fxaa / ssao). Keyed on
|
|
221
236
|
// the graph-resolved views so resize rebuilds automatically.
|
|
@@ -673,6 +688,7 @@ function makePostProcessRecordCtx(): {
|
|
|
673
688
|
draw: vi.fn(),
|
|
674
689
|
end: vi.fn(),
|
|
675
690
|
};
|
|
691
|
+
const fxaaParamsBuffer = { label: 'fxaa-params' };
|
|
676
692
|
const device = {
|
|
677
693
|
createBindGroup: vi.fn((desc: PostProcessBindGroupCreate) => {
|
|
678
694
|
bindGroupCreates.push(desc);
|
|
@@ -711,6 +727,7 @@ function makePostProcessRecordCtx(): {
|
|
|
711
727
|
runtime: {
|
|
712
728
|
device: { ...device, caps: { storageBuffer: true } },
|
|
713
729
|
errorRegistry: { fire: vi.fn() },
|
|
730
|
+
getPostProcessParamsBuffer: vi.fn(() => fxaaParamsBuffer),
|
|
714
731
|
},
|
|
715
732
|
store: { getCubemapGpuView: vi.fn() },
|
|
716
733
|
encoder: {
|