@forgeax/engine-runtime 0.1.20 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/create-renderer-gpu-pass-timing.unit.test.d.ts +2 -0
- package/dist/__tests__/create-renderer-gpu-pass-timing.unit.test.d.ts.map +1 -0
- package/dist/__tests__/lights-preservation.fixup.test.d.ts +2 -0
- package/dist/__tests__/lights-preservation.fixup.test.d.ts.map +1 -0
- package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -1
- package/dist/createRenderer.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +2 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +30 -30
- package/src/__tests__/active-camera.unit.test.ts +6 -8
- package/src/__tests__/asset-registry.recursive.spec.ts +1 -1
- package/src/__tests__/clamp-to-last.e2e.browser.test.ts +2 -2
- package/src/__tests__/components.test-d.ts +10 -3
- package/src/__tests__/components.unit.test.ts +13 -7
- package/src/__tests__/create-renderer-assembly.integration.test.ts +24 -0
- package/src/__tests__/create-renderer-gpu-pass-timing.unit.test.ts +32 -0
- package/src/__tests__/createRenderer.browser-no-webgpu.test.ts +66 -0
- package/src/__tests__/dawn/hello-sprite-nineslice-section.dawn.test.ts +9 -1
- package/src/__tests__/dawn/instances-shadow.dawn.test.ts +15 -2
- package/src/__tests__/errors.unit.test.ts +51 -20
- package/src/__tests__/extract-frames-merge.test.ts +48 -40
- package/src/__tests__/extract-skin-mismatch.unit.test.ts +6 -2
- package/src/__tests__/frame-targets-shadow-regression.unit.test.ts +13 -0
- package/src/__tests__/geometry.unit.test.ts +7 -10
- package/src/__tests__/light-gpu-byte-neutral.unit.test.ts +2 -1
- package/src/__tests__/light-snapshot.test-d.ts +8 -0
- package/src/__tests__/lights-preservation.fixup.test.ts +127 -0
- package/src/__tests__/lights.unit.test.ts +75 -2235
- package/src/__tests__/pbr-pipeline.unit.test.ts +6 -1
- package/src/__tests__/pipeline.unit.test.ts +48 -202
- package/src/__tests__/point-light-shadow.browser.test.ts +208 -83
- package/src/__tests__/point-light-shadow.unit.test.ts +61 -4
- package/src/__tests__/render-error-exhaustive.test-d.ts +63 -9
- package/src/__tests__/render-system-mega.test.ts +30 -41
- package/src/__tests__/render-system-record-multi-material-textureview.test.ts +4 -1
- package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +4 -1
- package/src/__tests__/render-system-skin-bg.unit.test.ts +14 -9
- package/src/__tests__/renderer-recover.unit.test.ts +8 -0
- package/src/__tests__/renderer-surface.unit.test.ts +229 -3
- package/src/__tests__/shadow-csm-atlas.test.ts +41 -0
- package/src/__tests__/shadow-csm-component.test.ts +31 -24
- package/src/__tests__/shadow-csm-extract.test.ts +156 -47
- package/src/__tests__/shadow-csm-runtime-vary.dawn.test.ts +72 -0
- package/src/__tests__/shadow-csm-shader.dawn.test.ts +19 -0
- package/src/__tests__/shadow-csm-ubo.test.ts +41 -45
- package/src/__tests__/shadow-csm-viewport.browser.test.ts +29 -0
- package/src/__tests__/shadow-fields-observable.dawn.test.ts +85 -20
- package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +3 -1
- package/src/__tests__/skylight-lazy-projection.test.ts +8 -5
- package/src/__tests__/ssao-bgl.test.ts +10 -16
- package/src/__tests__/ssao-passes.test.ts +19 -2
- package/src/__tests__/systems.unit.test.ts +44 -31
- package/src/__tests__/view-ubo-layout.browser.test.ts +20 -10
- package/src/createRenderer.ts +1 -0
- package/src/index.ts +1 -2
|
@@ -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
|
|
@@ -44,6 +44,59 @@ const ENGINE_MANIFEST = await (async () => {
|
|
|
44
44
|
})();
|
|
45
45
|
const ENGINE_MANIFEST_URL = `data:application/json,${encodeURIComponent(JSON.stringify(ENGINE_MANIFEST))}`;
|
|
46
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
|
+
|
|
47
100
|
// Helper types
|
|
48
101
|
interface WriteCapture {
|
|
49
102
|
f32: Float32Array;
|
|
@@ -177,7 +230,9 @@ function spawnScene(
|
|
|
177
230
|
depthBias,
|
|
178
231
|
normalBias,
|
|
179
232
|
shadowDistance: 50,
|
|
180
|
-
|
|
233
|
+
shadowFilter: pcf === 1 ? 1 : pcf === 3 ? 2 : 3,
|
|
234
|
+
shadowAngularRadius: 0.00465,
|
|
235
|
+
maxPenumbraTexels: 32,
|
|
181
236
|
},
|
|
182
237
|
});
|
|
183
238
|
world.spawn(
|
|
@@ -437,15 +492,15 @@ async function renderConfigWithSpy(
|
|
|
437
492
|
|
|
438
493
|
describe('M6 shadow fields observability', () => {
|
|
439
494
|
// --- Gate A: structural WGSL check ---
|
|
440
|
-
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', () => {
|
|
441
496
|
const wgsl = composedStandardPbr();
|
|
442
497
|
expect(wgsl).toContain('depthBias');
|
|
443
498
|
expect(wgsl).toContain('normalBias');
|
|
444
|
-
expect(wgsl).toContain('
|
|
499
|
+
expect(wgsl).toContain('directionalShadowFilter');
|
|
445
500
|
});
|
|
446
501
|
|
|
447
|
-
// --- Gate B: UBO spy — slots [126]=depthBias, [127]=normalBias, [128]=
|
|
448
|
-
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 () => {
|
|
449
504
|
if (typeof globalThis.navigator?.gpu?.requestAdapter !== 'function') return;
|
|
450
505
|
const { captured } = await renderConfigWithSpy(true, 0.123, 0.456, 3);
|
|
451
506
|
const vw = captured.filter((c) => c.f32.length >= 148);
|
|
@@ -455,22 +510,22 @@ describe('M6 shadow fields observability', () => {
|
|
|
455
510
|
const f32 = last.f32;
|
|
456
511
|
expect(Math.abs((f32[126] ?? Number.NaN) - 0.123)).toBeLessThan(0.001);
|
|
457
512
|
expect(Math.abs((f32[127] ?? Number.NaN) - 0.456)).toBeLessThan(0.001);
|
|
458
|
-
expect(f32[128]).toBe(
|
|
513
|
+
expect(f32[128]).toBe(2);
|
|
459
514
|
}, 60000);
|
|
460
515
|
|
|
461
|
-
// --- Gate D:
|
|
462
|
-
it('
|
|
516
|
+
// --- Gate D: pcf5 maps to the accepted profile carrier ---
|
|
517
|
+
it('pcf5 maps to profile 3 in View UBO[128]', async () => {
|
|
463
518
|
if (typeof globalThis.navigator?.gpu?.requestAdapter !== 'function') return;
|
|
464
|
-
const { captured } = await renderConfigWithSpy(true, 0.005, 0.05,
|
|
519
|
+
const { captured } = await renderConfigWithSpy(true, 0.005, 0.05, 5);
|
|
465
520
|
const vw9 = captured.filter((c) => c.f32.length >= 148);
|
|
466
521
|
expect(vw9.length).toBeGreaterThan(0);
|
|
467
522
|
const last9 = vw9[vw9.length - 1];
|
|
468
523
|
if (last9 === undefined) throw new Error('no view UBO write captured');
|
|
469
|
-
expect(last9.f32[128]).toBe(
|
|
524
|
+
expect(last9.f32[128]).toBe(3);
|
|
470
525
|
}, 60000);
|
|
471
526
|
|
|
472
|
-
// --- Gate D:
|
|
473
|
-
it('validate() accepts
|
|
527
|
+
// --- Gate D: Directional filter validation ---
|
|
528
|
+
it('validate() accepts pcf5 and rejects an unknown directional filter', () => {
|
|
474
529
|
const valid = validateDirectionalLightData({
|
|
475
530
|
direction: [0, -1, 0],
|
|
476
531
|
castShadow: true,
|
|
@@ -479,7 +534,7 @@ describe('M6 shadow fields observability', () => {
|
|
|
479
534
|
cascadeBlend: 0.2,
|
|
480
535
|
mapSize: 2048,
|
|
481
536
|
shadowDistance: 50,
|
|
482
|
-
|
|
537
|
+
shadowFilter: 3,
|
|
483
538
|
});
|
|
484
539
|
expect(valid.ok).toBe(true);
|
|
485
540
|
|
|
@@ -491,7 +546,7 @@ describe('M6 shadow fields observability', () => {
|
|
|
491
546
|
cascadeBlend: 0.2,
|
|
492
547
|
mapSize: 2048,
|
|
493
548
|
shadowDistance: 50,
|
|
494
|
-
|
|
549
|
+
shadowFilter: 99,
|
|
495
550
|
});
|
|
496
551
|
expect(even.ok).toBe(false);
|
|
497
552
|
if (!even.ok) expect(even.error.code).toBe('shadow-invalid-config');
|
|
@@ -504,7 +559,7 @@ describe('M6 shadow fields observability', () => {
|
|
|
504
559
|
cascadeBlend: 0.2,
|
|
505
560
|
mapSize: 2048,
|
|
506
561
|
shadowDistance: 50,
|
|
507
|
-
|
|
562
|
+
shadowFilter: 0,
|
|
508
563
|
});
|
|
509
564
|
expect(zero.ok).toBe(false);
|
|
510
565
|
if (!zero.ok) expect(zero.error.code).toBe('shadow-invalid-config');
|
|
@@ -519,6 +574,16 @@ describe('M6 shadow fields observability', () => {
|
|
|
519
574
|
expect(d.diff).toBeGreaterThan(0);
|
|
520
575
|
}, 60000);
|
|
521
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
|
+
|
|
522
587
|
// --- Gate C: pixel A/B — depthBias 0.0 vs 0.5 ---
|
|
523
588
|
it('pixel A/B: depthBias 0.0 vs 0.5', async () => {
|
|
524
589
|
if (typeof globalThis.navigator?.gpu?.requestAdapter !== 'function') return;
|
|
@@ -537,8 +602,8 @@ describe('M6 shadow fields observability', () => {
|
|
|
537
602
|
expect(d.diff).toBeGreaterThan(0);
|
|
538
603
|
}, 60000);
|
|
539
604
|
|
|
540
|
-
// --- Gate C: pixel A/B —
|
|
541
|
-
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 () => {
|
|
542
607
|
if (typeof globalThis.navigator?.gpu?.requestAdapter !== 'function') return;
|
|
543
608
|
const pxA = await renderConfig(true, 0.005, 0.05, 1);
|
|
544
609
|
const pxB = await renderConfig(true, 0.005, 0.05, 5);
|
|
@@ -624,7 +689,7 @@ describe('M6 shadow fields observability', () => {
|
|
|
624
689
|
);
|
|
625
690
|
|
|
626
691
|
it(
|
|
627
|
-
'falsification:
|
|
692
|
+
'falsification: directional filter varied differs, held does not',
|
|
628
693
|
async () => {
|
|
629
694
|
if (typeof globalThis.navigator?.gpu?.requestAdapter !== 'function') return;
|
|
630
695
|
const varied = diff(
|
|
@@ -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
|
|
|
@@ -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).
|
|
@@ -131,7 +131,7 @@ function makeMockRuntime(capsOverride: Partial<RhiCaps> = {}): {
|
|
|
131
131
|
|
|
132
132
|
function fakeHdrpBuffers(): HdrpBuffers {
|
|
133
133
|
return {
|
|
134
|
-
|
|
134
|
+
device: {} as HdrpBuffers['device'],
|
|
135
135
|
lightDataBuffer: mockBuffer('hdrp-light-data'),
|
|
136
136
|
lightDataBytes: 16384,
|
|
137
137
|
clusterGridBuffer: mockBuffer('hdrp-cluster-grid'),
|
|
@@ -150,15 +150,9 @@ function fakeHdrpBuffers(): HdrpBuffers {
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
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);
|
|
153
|
+
it('preserves the full 256-light Standard count in the cluster UBO', () => {
|
|
154
|
+
const payload = new Uint32Array(packClusterUniform({ x: 16, y: 9, z: 24 }, 0.1, 50, 0, 256));
|
|
155
|
+
expect(payload[3]).toBe(256);
|
|
162
156
|
});
|
|
163
157
|
|
|
164
158
|
it('keeps enabled SSAO intensity in the zero-punctual-light uniform path', () => {
|
|
@@ -209,12 +203,12 @@ describe('HDRP GPU membership producer bind-group contract', () => {
|
|
|
209
203
|
|
|
210
204
|
describe('w28 — HDRP unified BGL SSAO entries (binding 7/8)', () => {
|
|
211
205
|
it('descriptor entries.length === 7 (5 cluster + 2 ssao)', () => {
|
|
212
|
-
const desc = createHdrpBindGroupLayoutDescriptor(
|
|
206
|
+
const desc = createHdrpBindGroupLayoutDescriptor();
|
|
213
207
|
expect(desc.entries?.length).toBe(7);
|
|
214
208
|
});
|
|
215
209
|
|
|
216
210
|
it('binding 7 is texture_2d<f32> with FRAGMENT visibility (ssaoBlurred)', () => {
|
|
217
|
-
const desc = createHdrpBindGroupLayoutDescriptor(
|
|
211
|
+
const desc = createHdrpBindGroupLayoutDescriptor();
|
|
218
212
|
const b7 = desc.entries?.find((e) => e.binding === 7);
|
|
219
213
|
expect(b7).toBeDefined();
|
|
220
214
|
expect(b7?.visibility).toBe(FRAGMENT_VISIBILITY);
|
|
@@ -223,7 +217,7 @@ describe('w28 — HDRP unified BGL SSAO entries (binding 7/8)', () => {
|
|
|
223
217
|
});
|
|
224
218
|
|
|
225
219
|
it('binding 8 is sampler with FRAGMENT visibility (ssaoSampler)', () => {
|
|
226
|
-
const desc = createHdrpBindGroupLayoutDescriptor(
|
|
220
|
+
const desc = createHdrpBindGroupLayoutDescriptor();
|
|
227
221
|
const b8 = desc.entries?.find((e) => e.binding === 8);
|
|
228
222
|
expect(b8).toBeDefined();
|
|
229
223
|
expect(b8?.visibility).toBe(FRAGMENT_VISIBILITY);
|
|
@@ -231,13 +225,13 @@ describe('w28 — HDRP unified BGL SSAO entries (binding 7/8)', () => {
|
|
|
231
225
|
});
|
|
232
226
|
|
|
233
227
|
it('binding 9 is absent (scope-amend-webgl2-ubo: intensity folded into binding 6)', () => {
|
|
234
|
-
const desc = createHdrpBindGroupLayoutDescriptor(
|
|
228
|
+
const desc = createHdrpBindGroupLayoutDescriptor();
|
|
235
229
|
const bindings = desc.entries?.map((e) => e.binding) ?? [];
|
|
236
230
|
expect(bindings).not.toContain(9);
|
|
237
231
|
});
|
|
238
232
|
|
|
239
233
|
it('existing bindings 0/3/4/5/6 remain unchanged', () => {
|
|
240
|
-
const desc = createHdrpBindGroupLayoutDescriptor(
|
|
234
|
+
const desc = createHdrpBindGroupLayoutDescriptor();
|
|
241
235
|
const b0 = desc.entries?.find((e) => e.binding === 0);
|
|
242
236
|
expect(b0?.buffer?.hasDynamicOffset).toBe(true);
|
|
243
237
|
for (const b of [3, 4, 5, 6]) {
|
|
@@ -247,7 +241,7 @@ describe('w28 — HDRP unified BGL SSAO entries (binding 7/8)', () => {
|
|
|
247
241
|
});
|
|
248
242
|
|
|
249
243
|
it('all bindings exactly { 0, 3, 4, 5, 6, 7, 8 }', () => {
|
|
250
|
-
const desc = createHdrpBindGroupLayoutDescriptor(
|
|
244
|
+
const desc = createHdrpBindGroupLayoutDescriptor();
|
|
251
245
|
const bindings = new Set(desc.entries?.map((e) => e.binding));
|
|
252
246
|
expect(bindings).toEqual(new Set([0, 3, 4, 5, 6, 7, 8]));
|
|
253
247
|
});
|
|
@@ -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: {
|
|
@@ -448,9 +448,9 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
448
448
|
];
|
|
449
449
|
}
|
|
450
450
|
|
|
451
|
-
describe('Camera schema (
|
|
452
|
-
it('Camera.schema has
|
|
453
|
-
expect(Object.keys(Camera.fields).length).toBe(
|
|
451
|
+
describe('Camera schema (21 fields: 17 f32 + clearColor array + autoAspect bool + target after render-target extension)', () => {
|
|
452
|
+
it('Camera.schema has 21 fields (17 f32 + clearColor array + autoAspect bool + target: perspective quartet + projection + ortho quartet + tonemap trio + antialias + bloom quartet + clearColor + autoAspect)', () => {
|
|
453
|
+
expect(Object.keys(Camera.fields).length).toBe(21);
|
|
454
454
|
expect(componentFieldType(Camera, 'fov')).toBe('f32');
|
|
455
455
|
expect(componentFieldType(Camera, 'aspect')).toBe('f32');
|
|
456
456
|
expect(componentFieldType(Camera, 'near')).toBe('f32');
|
|
@@ -476,6 +476,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
476
476
|
// feat-20260617-host-engine-contract-and-video-cutscene / M3: aspect-sync
|
|
477
477
|
// opt-out flag (bool column tier).
|
|
478
478
|
expect(componentFieldType(Camera, 'autoAspect')).toBe('bool');
|
|
479
|
+
expect(componentFieldType(Camera, 'target')).toBe('shared<RenderTarget>');
|
|
479
480
|
});
|
|
480
481
|
});
|
|
481
482
|
|
|
@@ -574,6 +575,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
574
575
|
directionalCount: 0,
|
|
575
576
|
point: [],
|
|
576
577
|
spot: [],
|
|
578
|
+
directionalShadowError: undefined,
|
|
577
579
|
lightSpaceMatrix: undefined,
|
|
578
580
|
shadowMapSize: undefined,
|
|
579
581
|
pointShadow: [],
|
|
@@ -767,6 +769,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
767
769
|
temporalFrameInput: undefined,
|
|
768
770
|
compiledFrameGraph: null,
|
|
769
771
|
compiledFrameGraphTopologyKey: null,
|
|
772
|
+
compiledFrameGraphGeneration: 0,
|
|
770
773
|
retiredCompiledFrameGraphs: new Set(),
|
|
771
774
|
instanceBuffers: new Map(),
|
|
772
775
|
transientInstanceBuffers: [],
|
|
@@ -787,8 +790,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
787
790
|
installedPipelineHandle: 0,
|
|
788
791
|
activePipeline: urpPipeline,
|
|
789
792
|
installedPipelineConfig: undefined,
|
|
790
|
-
|
|
791
|
-
hdrpOncePerFrameFired: new Set(),
|
|
793
|
+
standardOncePerFrameFired: new Set(),
|
|
792
794
|
},
|
|
793
795
|
{ unlit: 0 },
|
|
794
796
|
{ createBindGroup: 0, keys: [] },
|
|
@@ -824,6 +826,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
824
826
|
temporalFrameInput: undefined,
|
|
825
827
|
compiledFrameGraph: null,
|
|
826
828
|
compiledFrameGraphTopologyKey: null,
|
|
829
|
+
compiledFrameGraphGeneration: 0,
|
|
827
830
|
retiredCompiledFrameGraphs: new Set(),
|
|
828
831
|
instanceBuffers: new Map(),
|
|
829
832
|
transientInstanceBuffers: [],
|
|
@@ -844,8 +847,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
844
847
|
installedPipelineHandle: 0,
|
|
845
848
|
activePipeline: urpPipeline,
|
|
846
849
|
installedPipelineConfig: undefined,
|
|
847
|
-
|
|
848
|
-
hdrpOncePerFrameFired: new Set(),
|
|
850
|
+
standardOncePerFrameFired: new Set(),
|
|
849
851
|
},
|
|
850
852
|
{ unlit: 0 },
|
|
851
853
|
{ createBindGroup: 0, keys: [] },
|
|
@@ -914,8 +916,9 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
914
916
|
// --- from fxaa-pipeline.test.ts ---
|
|
915
917
|
|
|
916
918
|
describe('feat-20260528-fxaa-post-processing M2 w8: FXAA pipeline construction (success path)', () => {
|
|
917
|
-
it('D-2: FXAA BGL has exactly
|
|
918
|
-
//
|
|
919
|
+
it('D-2: FXAA BGL has exactly 3 entries: texture + sampler + dither policy UBO', () => {
|
|
920
|
+
// FXAA owns a per-pass output-policy UBO so pipeline asset config can
|
|
921
|
+
// disable dither without rebuilding the shader or graph topology.
|
|
919
922
|
// The sampler entry uses { type: 'filtering' } (linear sampling).
|
|
920
923
|
const bglEntries = [
|
|
921
924
|
{
|
|
@@ -928,15 +931,21 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
928
931
|
visibility: 2, // GPU_SHADER_STAGE_FRAGMENT = 0x2
|
|
929
932
|
sampler: { type: 'filtering' },
|
|
930
933
|
},
|
|
934
|
+
{
|
|
935
|
+
binding: 2,
|
|
936
|
+
visibility: 2,
|
|
937
|
+
buffer: { type: 'uniform' },
|
|
938
|
+
},
|
|
931
939
|
];
|
|
932
|
-
expect(bglEntries).toHaveLength(
|
|
940
|
+
expect(bglEntries).toHaveLength(3);
|
|
933
941
|
expect(bglEntries[0]?.binding).toBe(0);
|
|
934
942
|
expect(bglEntries[0]?.texture).toBeDefined();
|
|
935
943
|
expect(bglEntries[1]?.binding).toBe(1);
|
|
936
944
|
expect(bglEntries[1]?.sampler).toBeDefined();
|
|
937
|
-
|
|
945
|
+
expect(bglEntries[2]?.binding).toBe(2);
|
|
946
|
+
expect(bglEntries[2]?.buffer).toEqual({ type: 'uniform' });
|
|
938
947
|
const hasBuffer = bglEntries.some((e) => 'buffer' in (e as Record<string, unknown>));
|
|
939
|
-
expect(hasBuffer).toBe(
|
|
948
|
+
expect(hasBuffer).toBe(true);
|
|
940
949
|
});
|
|
941
950
|
|
|
942
951
|
it('D-3: FXAA pipeline target format = swap-chain storage format (non-srgb, helper Channel 2 truth)', () => {
|
|
@@ -1920,10 +1929,10 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
1920
1929
|
return 'mesh-ssbo-capacity';
|
|
1921
1930
|
case 'mesh-ssbo-ceiling-reached':
|
|
1922
1931
|
return 'mesh-ssbo-ceiling';
|
|
1923
|
-
case '
|
|
1924
|
-
return '
|
|
1925
|
-
case '
|
|
1926
|
-
return '
|
|
1932
|
+
case 'standard-light-budget-exceeded':
|
|
1933
|
+
return 'standard-budget';
|
|
1934
|
+
case 'standard-cluster-index-overflow':
|
|
1935
|
+
return 'standard-overflow';
|
|
1927
1936
|
}
|
|
1928
1937
|
}
|
|
1929
1938
|
expect(exhaustive('skin-joint-count-exceeded')).toBe('joint-count');
|
|
@@ -3186,6 +3195,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
3186
3195
|
directionalCount: 0,
|
|
3187
3196
|
point: [],
|
|
3188
3197
|
spot: [],
|
|
3198
|
+
directionalShadowError: undefined,
|
|
3189
3199
|
lightSpaceMatrix: undefined,
|
|
3190
3200
|
shadowMapSize: undefined,
|
|
3191
3201
|
pointShadow: [],
|
|
@@ -3368,11 +3378,12 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
3368
3378
|
}
|
|
3369
3379
|
|
|
3370
3380
|
function makePipelineState(
|
|
3371
|
-
internals: { _fakes: Record<string, unknown
|
|
3381
|
+
internals: { _fakes: Record<string, unknown>; device?: unknown },
|
|
3372
3382
|
hdrAlreadyAllocated: boolean,
|
|
3373
3383
|
): unknown {
|
|
3374
3384
|
const f = internals._fakes;
|
|
3375
3385
|
return {
|
|
3386
|
+
device: internals.device,
|
|
3376
3387
|
meshes: new Map(),
|
|
3377
3388
|
format: 'bgra8unorm',
|
|
3378
3389
|
colorAttachmentFormat: 'bgra8unorm-srgb',
|
|
@@ -3461,6 +3472,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
3461
3472
|
temporalFrameInput: undefined,
|
|
3462
3473
|
compiledFrameGraph: null,
|
|
3463
3474
|
compiledFrameGraphTopologyKey: null,
|
|
3475
|
+
compiledFrameGraphGeneration: 0,
|
|
3464
3476
|
retiredCompiledFrameGraphs: new Set(),
|
|
3465
3477
|
instanceBuffers: new Map(),
|
|
3466
3478
|
transientInstanceBuffers: [],
|
|
@@ -3481,8 +3493,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
3481
3493
|
installedPipelineHandle: 0,
|
|
3482
3494
|
activePipeline: urpPipeline,
|
|
3483
3495
|
installedPipelineConfig: undefined,
|
|
3484
|
-
|
|
3485
|
-
hdrpOncePerFrameFired: new Set(),
|
|
3496
|
+
standardOncePerFrameFired: new Set(),
|
|
3486
3497
|
},
|
|
3487
3498
|
{ unlit: 0 },
|
|
3488
3499
|
{ createBindGroup: 0, keys: [] },
|
|
@@ -3582,6 +3593,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
3582
3593
|
temporalFrameInput: undefined,
|
|
3583
3594
|
compiledFrameGraph: null,
|
|
3584
3595
|
compiledFrameGraphTopologyKey: null,
|
|
3596
|
+
compiledFrameGraphGeneration: 0,
|
|
3585
3597
|
retiredCompiledFrameGraphs: new Set(),
|
|
3586
3598
|
instanceBuffers,
|
|
3587
3599
|
warnedZeroLightStandard: false,
|
|
@@ -3601,8 +3613,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
3601
3613
|
installedPipelineHandle: 0,
|
|
3602
3614
|
activePipeline: urpPipeline,
|
|
3603
3615
|
installedPipelineConfig: undefined,
|
|
3604
|
-
|
|
3605
|
-
hdrpOncePerFrameFired: new Set(),
|
|
3616
|
+
standardOncePerFrameFired: new Set(),
|
|
3606
3617
|
},
|
|
3607
3618
|
{ unlit: 0 },
|
|
3608
3619
|
{ createBindGroup: 0, keys: [] },
|
|
@@ -3657,6 +3668,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
3657
3668
|
temporalFrameInput: undefined,
|
|
3658
3669
|
compiledFrameGraph: null,
|
|
3659
3670
|
compiledFrameGraphTopologyKey: null,
|
|
3671
|
+
compiledFrameGraphGeneration: 0,
|
|
3660
3672
|
retiredCompiledFrameGraphs: new Set(),
|
|
3661
3673
|
instanceBuffers,
|
|
3662
3674
|
warnedZeroLightStandard: false,
|
|
@@ -3676,8 +3688,7 @@ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
|
3676
3688
|
installedPipelineHandle: 0,
|
|
3677
3689
|
activePipeline: urpPipeline,
|
|
3678
3690
|
installedPipelineConfig: undefined,
|
|
3679
|
-
|
|
3680
|
-
hdrpOncePerFrameFired: new Set(),
|
|
3691
|
+
standardOncePerFrameFired: new Set(),
|
|
3681
3692
|
},
|
|
3682
3693
|
{ unlit: 0 },
|
|
3683
3694
|
{ createBindGroup: 0, keys: [] },
|
|
@@ -8336,10 +8347,10 @@ type ExtractFramesWithPipeline = (
|
|
|
8336
8347
|
}
|
|
8337
8348
|
|
|
8338
8349
|
// ── feat-20260612-skin-palette-per-frame-upload M3 / m3-1: record-stage ──
|
|
8339
|
-
// dynOffset[1] truth-value + skin BG cache miss=1 + hit=N-1 stats counter.
|
|
8350
|
+
// dynOffset[1]/[2] truth-value + skin BG cache miss=1 + hit=N-1 stats counter.
|
|
8340
8351
|
//
|
|
8341
8352
|
// Why this block exists:
|
|
8342
|
-
// M3 lifts the record-stage `group2DynamicOffsets`
|
|
8353
|
+
// M3 lifts the record-stage `group2DynamicOffsets` palette slots from a
|
|
8343
8354
|
// hard-coded `0` (PR #353 stub) to the per-entity `entry.source.skin
|
|
8344
8355
|
// .byteOffset` cursor that M2 m2-6 wired into the extract output. The
|
|
8345
8356
|
// acceptanceCheck calls for the literal text `group2DynamicOffsets[1]
|
|
@@ -8351,8 +8362,8 @@ type ExtractFramesWithPipeline = (
|
|
|
8351
8362
|
// What the assertions cover:
|
|
8352
8363
|
// (a) The test-local offset oracle mirrors the production main-pass
|
|
8353
8364
|
// geometry contract. With skinByteOffset !== undefined the returned
|
|
8354
|
-
// tuple's [1]
|
|
8355
|
-
//
|
|
8365
|
+
// tuple's [1] and [2] slots equal the supplied byteOffset (the
|
|
8366
|
+
// temporal contract); with skinByteOffset ===
|
|
8356
8367
|
// undefined it returns a length-1 tuple (non-skin paths preserved).
|
|
8357
8368
|
// (b) N=3 sequential lookups
|
|
8358
8369
|
// against the same `(meshStorageBuffer, skinPaletteAllocator
|
|
@@ -8379,7 +8390,9 @@ type ExtractFramesWithPipeline = (
|
|
|
8379
8390
|
skinByteOffset: number | undefined,
|
|
8380
8391
|
): readonly number[] => {
|
|
8381
8392
|
const meshOffset = meshSlotIdx * MESH_PER_ENTITY_STRIDE;
|
|
8382
|
-
return skinByteOffset === undefined
|
|
8393
|
+
return skinByteOffset === undefined
|
|
8394
|
+
? [meshOffset]
|
|
8395
|
+
: [meshOffset, skinByteOffset, skinByteOffset];
|
|
8383
8396
|
};
|
|
8384
8397
|
|
|
8385
8398
|
// Skin path: second slot must equal the per-entity byteOffset cursor
|
|
@@ -8388,12 +8401,12 @@ type ExtractFramesWithPipeline = (
|
|
|
8388
8401
|
// (first allocateSlice), cursor=256 (second 1-joint slice), cursor
|
|
8389
8402
|
// =16320 (the worst-case 255-joint slice end).
|
|
8390
8403
|
const skinAt0 = computeSkinGroup2DynOffsets(0, 0);
|
|
8391
|
-
expect(skinAt0).toHaveLength(
|
|
8404
|
+
expect(skinAt0).toHaveLength(3);
|
|
8392
8405
|
// group2DynamicOffsets[1] === byteOffset (literal assertion text per acceptanceCheck)
|
|
8393
8406
|
expect(skinAt0[1]).toBe(0);
|
|
8394
8407
|
|
|
8395
8408
|
const skinAt256 = computeSkinGroup2DynOffsets(1, 256);
|
|
8396
|
-
expect(skinAt256).toHaveLength(
|
|
8409
|
+
expect(skinAt256).toHaveLength(3);
|
|
8397
8410
|
// group2DynamicOffsets[1] === byteOffset
|
|
8398
8411
|
expect(skinAt256[1]).toBe(256);
|
|
8399
8412
|
// mesh slot continues to follow the 256-byte stride: meshSlotIdx * 256.
|
|
@@ -8401,7 +8414,7 @@ type ExtractFramesWithPipeline = (
|
|
|
8401
8414
|
|
|
8402
8415
|
const byteOffsetWorst = 254 * 256;
|
|
8403
8416
|
const skinAtEnd = computeSkinGroup2DynOffsets(2, byteOffsetWorst);
|
|
8404
|
-
expect(skinAtEnd).toHaveLength(
|
|
8417
|
+
expect(skinAtEnd).toHaveLength(3);
|
|
8405
8418
|
// group2DynamicOffsets[1] === byteOffset
|
|
8406
8419
|
expect(skinAtEnd[1]).toBe(byteOffsetWorst);
|
|
8407
8420
|
expect(skinAtEnd[0]).toBe(2 * 256);
|