@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
|
@@ -349,7 +349,7 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
349
349
|
});
|
|
350
350
|
});
|
|
351
351
|
|
|
352
|
-
describe('t2 - AC-15 ImageErrorCode exhaustive switch (new
|
|
352
|
+
describe('t2 - AC-15 ImageErrorCode exhaustive switch (new members)', () => {
|
|
353
353
|
it('ImageErrorCode includes image-hdr-decode-failed', () => {
|
|
354
354
|
const code: ImageErrorCode = 'image-hdr-decode-failed';
|
|
355
355
|
expect(code).toBe('image-hdr-decode-failed');
|
|
@@ -378,10 +378,17 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
378
378
|
return 'atlas size exceeded';
|
|
379
379
|
case 'atlas-region-mismatch':
|
|
380
380
|
return 'atlas region mismatch';
|
|
381
|
+
// feat-20260829-deep-agent-game-feedback-resolution M5 —
|
|
382
|
+
// PixelSurface authoring errors remain part of the closed image
|
|
383
|
+
// error vocabulary and must stay covered by every exhaustive
|
|
384
|
+
// consumer switch.
|
|
385
|
+
case 'image-surface-invalid':
|
|
386
|
+
return 'surface invalid';
|
|
381
387
|
}
|
|
382
388
|
}
|
|
383
389
|
expect(exhaustive('image-decode-failed')).toBe('decode failed');
|
|
384
390
|
expect(exhaustive('image-hdr-decode-failed')).toBe('hdr decode failed');
|
|
391
|
+
expect(exhaustive('image-surface-invalid')).toBe('surface invalid');
|
|
385
392
|
});
|
|
386
393
|
});
|
|
387
394
|
});
|
|
@@ -420,6 +427,8 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
420
427
|
return 'cleanup failed';
|
|
421
428
|
case 'scene-data-unavailable':
|
|
422
429
|
return 'scene data unavailable';
|
|
430
|
+
case 'standard-profile-invalid':
|
|
431
|
+
return 'standard profile invalid';
|
|
423
432
|
case 'frame-receipt-stale':
|
|
424
433
|
return 'frame receipt stale';
|
|
425
434
|
case 'renderer-contract-failed':
|
|
@@ -464,14 +473,24 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
464
473
|
return 'mesh ssbo capacity exceeded';
|
|
465
474
|
case 'mesh-ssbo-ceiling-reached':
|
|
466
475
|
return 'mesh ssbo ceiling reached';
|
|
467
|
-
case '
|
|
468
|
-
return '
|
|
469
|
-
case '
|
|
470
|
-
return '
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
case '
|
|
474
|
-
return '
|
|
476
|
+
case 'render-target-descriptor-invalid':
|
|
477
|
+
return 'render target descriptor invalid';
|
|
478
|
+
case 'render-target-capability-missing':
|
|
479
|
+
return 'render target capability missing';
|
|
480
|
+
case 'render-target-state-invalid':
|
|
481
|
+
return 'render target state invalid';
|
|
482
|
+
case 'render-target-operation-failed':
|
|
483
|
+
return 'render target operation failed';
|
|
484
|
+
case 'reflection-probe-budget-exceeded':
|
|
485
|
+
return 'reflection probe budget exceeded';
|
|
486
|
+
case 'render-intent-invalid':
|
|
487
|
+
return 'render intent invalid';
|
|
488
|
+
case 'standard-light-budget-exceeded':
|
|
489
|
+
return 'standard light budget exceeded';
|
|
490
|
+
case 'standard-cluster-index-overflow':
|
|
491
|
+
return 'standard cluster index overflow';
|
|
492
|
+
case 'standard-cluster-transport-unavailable':
|
|
493
|
+
return 'standard cluster transport unavailable';
|
|
475
494
|
// feat-20260611-fox-skinning-vertex-attribute-chain M4 / w17 (D-5):
|
|
476
495
|
// bidirectional Skin <-> pbr-skin material mismatch detected at extract.
|
|
477
496
|
case 'skin-material-mismatch':
|
|
@@ -528,6 +547,8 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
528
547
|
return 'points lines budget exceeded';
|
|
529
548
|
case 'points-lines-prepare-failed':
|
|
530
549
|
return 'points lines prepare failed';
|
|
550
|
+
case 'light-resource-unavailable':
|
|
551
|
+
return 'light resource unavailable';
|
|
531
552
|
case 'transmission-capability-missing':
|
|
532
553
|
return 'transmission capability missing';
|
|
533
554
|
case 'projector-binding-failed':
|
|
@@ -551,16 +572,15 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
551
572
|
});
|
|
552
573
|
|
|
553
574
|
/*
|
|
554
|
-
*
|
|
555
|
-
* RuntimeErrorCode +3 members — exhaustive narrow test.
|
|
575
|
+
* Standard lighting closed error codes — exhaustive narrow test.
|
|
556
576
|
*
|
|
557
|
-
* AC-07: three
|
|
577
|
+
* AC-07: three Standard lighting codes are members of the closed render union.
|
|
558
578
|
* Each error code narrows without a default branch in switch (err.code).
|
|
559
579
|
*/
|
|
560
|
-
describe('
|
|
561
|
-
it('
|
|
562
|
-
const code: RuntimeLayerErrorCode = '
|
|
563
|
-
expect(code).toBe('
|
|
580
|
+
describe('Standard lighting error members', () => {
|
|
581
|
+
it('standard-cluster-transport-unavailable is a valid RenderErrorCode', () => {
|
|
582
|
+
const code: RuntimeLayerErrorCode = 'standard-cluster-transport-unavailable';
|
|
583
|
+
expect(code).toBe('standard-cluster-transport-unavailable');
|
|
564
584
|
});
|
|
565
585
|
|
|
566
586
|
it('exhaustive switch covers all 3 new members alongside existing members', () => {
|
|
@@ -591,6 +611,7 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
591
611
|
case 'cleanup-failed':
|
|
592
612
|
return 'ok';
|
|
593
613
|
case 'scene-data-unavailable':
|
|
614
|
+
case 'standard-profile-invalid':
|
|
594
615
|
return 'ok';
|
|
595
616
|
case 'frame-receipt-stale':
|
|
596
617
|
return 'ok';
|
|
@@ -636,9 +657,9 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
636
657
|
return 'ok';
|
|
637
658
|
case 'mesh-ssbo-ceiling-reached':
|
|
638
659
|
return 'ok';
|
|
639
|
-
case '
|
|
660
|
+
case 'standard-light-budget-exceeded':
|
|
640
661
|
return 'ok';
|
|
641
|
-
case '
|
|
662
|
+
case 'standard-cluster-index-overflow':
|
|
642
663
|
return 'ok';
|
|
643
664
|
case 'skin-material-mismatch':
|
|
644
665
|
return 'ok';
|
|
@@ -650,7 +671,19 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
650
671
|
return 'ok';
|
|
651
672
|
case 'joint-entity-dangling':
|
|
652
673
|
return 'ok';
|
|
653
|
-
case '
|
|
674
|
+
case 'standard-cluster-transport-unavailable':
|
|
675
|
+
return 'ok';
|
|
676
|
+
case 'render-target-descriptor-invalid':
|
|
677
|
+
return 'ok';
|
|
678
|
+
case 'render-target-capability-missing':
|
|
679
|
+
return 'ok';
|
|
680
|
+
case 'render-target-state-invalid':
|
|
681
|
+
return 'ok';
|
|
682
|
+
case 'render-target-operation-failed':
|
|
683
|
+
return 'ok';
|
|
684
|
+
case 'reflection-probe-budget-exceeded':
|
|
685
|
+
return 'ok';
|
|
686
|
+
case 'render-intent-invalid':
|
|
654
687
|
return 'ok';
|
|
655
688
|
case 'point-shadow-atlas-uninitialized':
|
|
656
689
|
return 'ok';
|
|
@@ -688,6 +721,7 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
688
721
|
return 'ok';
|
|
689
722
|
case 'points-lines-prepare-failed':
|
|
690
723
|
return 'ok';
|
|
724
|
+
case 'light-resource-unavailable':
|
|
691
725
|
case 'transmission-capability-missing':
|
|
692
726
|
return 'ok';
|
|
693
727
|
case 'projector-binding-failed':
|
|
@@ -702,7 +736,7 @@ type RuntimeLayerError = RenderError | AssetRuntimeError | SkinError;
|
|
|
702
736
|
return 'ok';
|
|
703
737
|
}
|
|
704
738
|
}
|
|
705
|
-
expect(exhaustive('
|
|
739
|
+
expect(exhaustive('standard-cluster-transport-unavailable')).toBe('ok');
|
|
706
740
|
expect(exhaustive('point-shadow-atlas-uninitialized')).toBe('ok');
|
|
707
741
|
expect(exhaustive('point-shadow-atlas-bounds-violation')).toBe('ok');
|
|
708
742
|
});
|
|
@@ -161,62 +161,39 @@ function simulateMerge(
|
|
|
161
161
|
// AC-04: lights — point[]/spot[] concat; directional first-hit in worlds[] order; directionalCount sum
|
|
162
162
|
const point: PointLightSnapshot[] = [];
|
|
163
163
|
const spot: SpotLightSnapshot[] = [];
|
|
164
|
+
const rect: ExtractedLights['rect'][number][] = [];
|
|
164
165
|
let directional: DirectionalLightSnapshot | undefined;
|
|
165
166
|
let directionalCount = 0;
|
|
166
167
|
for (const f of frames) {
|
|
167
168
|
for (const p of f.lights.point) point.push(p);
|
|
168
169
|
for (const s of f.lights.spot) spot.push(s);
|
|
170
|
+
for (const r of f.lights.rect) rect.push(r);
|
|
169
171
|
if (directional === undefined && f.lights.directional !== undefined) {
|
|
170
172
|
directional = f.lights.directional;
|
|
171
173
|
}
|
|
172
174
|
directionalCount += f.lights.directionalCount;
|
|
173
175
|
}
|
|
176
|
+
const firstDirectionalLights = frames.find(
|
|
177
|
+
(frame) => frame.lights.directional !== undefined,
|
|
178
|
+
)?.lights;
|
|
174
179
|
const lights: ExtractedLights = {
|
|
175
180
|
directional,
|
|
176
181
|
directionalCount,
|
|
177
182
|
point,
|
|
178
183
|
spot,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
? frames.find((f) => f.lights.directional !== undefined)?.lights.cascadeCount
|
|
190
|
-
: undefined,
|
|
191
|
-
cascadeBlend:
|
|
192
|
-
directional !== undefined
|
|
193
|
-
? frames.find((f) => f.lights.directional !== undefined)?.lights.cascadeBlend
|
|
194
|
-
: undefined,
|
|
195
|
-
shadowMapSize:
|
|
196
|
-
directional !== undefined
|
|
197
|
-
? frames.find((f) => f.lights.directional !== undefined)?.lights.shadowMapSize
|
|
198
|
-
: undefined,
|
|
199
|
-
depthBias:
|
|
200
|
-
directional !== undefined
|
|
201
|
-
? frames.find((f) => f.lights.directional !== undefined)?.lights.depthBias
|
|
202
|
-
: undefined,
|
|
203
|
-
normalBias:
|
|
204
|
-
directional !== undefined
|
|
205
|
-
? frames.find((f) => f.lights.directional !== undefined)?.lights.normalBias
|
|
206
|
-
: undefined,
|
|
207
|
-
pcfKernelSize:
|
|
208
|
-
directional !== undefined
|
|
209
|
-
? frames.find((f) => f.lights.directional !== undefined)?.lights.pcfKernelSize
|
|
210
|
-
: undefined,
|
|
184
|
+
rect,
|
|
185
|
+
lightViewProj: firstDirectionalLights?.lightViewProj,
|
|
186
|
+
splitPlanes: firstDirectionalLights?.splitPlanes,
|
|
187
|
+
cascadeCount: firstDirectionalLights?.cascadeCount,
|
|
188
|
+
cascadeBlend: firstDirectionalLights?.cascadeBlend,
|
|
189
|
+
shadowMapSize: firstDirectionalLights?.shadowMapSize,
|
|
190
|
+
depthBias: firstDirectionalLights?.depthBias,
|
|
191
|
+
normalBias: firstDirectionalLights?.normalBias,
|
|
192
|
+
directionalShadowQuality: firstDirectionalLights?.directionalShadowQuality,
|
|
193
|
+
directionalShadowError: firstDirectionalLights?.directionalShadowError,
|
|
211
194
|
pointShadow: frames.flatMap((f) => f.lights.pointShadow),
|
|
212
|
-
directionalCsmConfig:
|
|
213
|
-
|
|
214
|
-
? frames.find((f) => f.lights.directional !== undefined)?.lights.directionalCsmConfig
|
|
215
|
-
: undefined,
|
|
216
|
-
directionalCsmDirection:
|
|
217
|
-
directional !== undefined
|
|
218
|
-
? frames.find((f) => f.lights.directional !== undefined)?.lights.directionalCsmDirection
|
|
219
|
-
: undefined,
|
|
195
|
+
directionalCsmConfig: firstDirectionalLights?.directionalCsmConfig,
|
|
196
|
+
directionalCsmDirection: firstDirectionalLights?.directionalCsmDirection,
|
|
220
197
|
};
|
|
221
198
|
|
|
222
199
|
// AC-05: singletons from owner world only
|
|
@@ -433,6 +410,36 @@ describe('extractFrames merge semantics (m2-t1, AC-04/05/06)', () => {
|
|
|
433
410
|
expect(frameB.lights.directionalCount).toBe(1);
|
|
434
411
|
});
|
|
435
412
|
|
|
413
|
+
it('AC-04: first directional owner error is preserved across the merge', () => {
|
|
414
|
+
const worldA = new World();
|
|
415
|
+
worldA
|
|
416
|
+
.spawn(
|
|
417
|
+
{ component: Transform, data: identityTransform() },
|
|
418
|
+
{
|
|
419
|
+
component: DirectionalLight,
|
|
420
|
+
data: {
|
|
421
|
+
direction: [0, -1, 0],
|
|
422
|
+
shadowFilter: 4,
|
|
423
|
+
shadowAngularRadius: 999,
|
|
424
|
+
maxPenumbraTexels: 32,
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
)
|
|
428
|
+
.unwrap();
|
|
429
|
+
|
|
430
|
+
const worldB = makeWorldWithDirectionalLight();
|
|
431
|
+
const frameA = extractFrame(worldA, prepareExtractContext(worldA));
|
|
432
|
+
const frameB = extractFrame(worldB, prepareExtractContext(worldB));
|
|
433
|
+
|
|
434
|
+
expect(frameA.lights.directionalShadowError?.code).toBe('shadow-invalid-config');
|
|
435
|
+
expect(frameB.lights.directionalShadowError).toBeUndefined();
|
|
436
|
+
const merged = simulateMerge([frameA, frameB], 0);
|
|
437
|
+
expect(merged.lights.directionalShadowError?.detail).toMatchObject({
|
|
438
|
+
field: 'shadowAngularRadius',
|
|
439
|
+
actual: 999,
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
436
443
|
// ── AC-05: singleton resources from owner only ─────────────────────────────
|
|
437
444
|
|
|
438
445
|
it('AC-05: skylight is taken from owner world only, non-owner ignored', () => {
|
|
@@ -624,6 +631,9 @@ describe('extractFrames cross-world directional CSM (bug-20260710)', () => {
|
|
|
624
631
|
}
|
|
625
632
|
// splitPlanes reach the component shadowDistance on the last cascade.
|
|
626
633
|
expect(frame.lights.splitPlanes).toBeDefined();
|
|
634
|
+
expect(frame.lights.splitPlanes).toHaveLength(16);
|
|
635
|
+
expect(frame.lights.splitPlanes?.[1]).toBeGreaterThan(0);
|
|
636
|
+
expect(frame.lights.splitPlanes?.[2]).toBeGreaterThan(0);
|
|
627
637
|
});
|
|
628
638
|
|
|
629
639
|
it('single world (camera + light together) still yields non-zero lightViewProj', () => {
|
|
@@ -652,5 +662,6 @@ describe('extractFrames cross-world directional CSM (bug-20260710)', () => {
|
|
|
652
662
|
for (let i = 0; i < (frame.lights.cascadeCount ?? 0); i++) {
|
|
653
663
|
expect(isNonZeroMat(lvp?.[i])).toBe(true);
|
|
654
664
|
}
|
|
665
|
+
expect(frame.lights.splitPlanes).toHaveLength(16);
|
|
655
666
|
});
|
|
656
667
|
});
|
|
@@ -276,7 +276,7 @@ describe('render-system-extract Skin / pbr-skin mismatch (AC-07 / w19)', () => {
|
|
|
276
276
|
expect(frame.renderables).toHaveLength(0);
|
|
277
277
|
});
|
|
278
278
|
|
|
279
|
-
it('(c) Skin + pbr-skin material
|
|
279
|
+
it('(c) Skin + pbr-skin material without a palette allocator -> renderable and dispatch skipped', () => {
|
|
280
280
|
const world = new World();
|
|
281
281
|
const assets = makeAssetRegistry();
|
|
282
282
|
const meshHandle = registerSkinnedMesh(world);
|
|
@@ -288,9 +288,13 @@ describe('render-system-extract Skin / pbr-skin mismatch (AC-07 / w19)', () => {
|
|
|
288
288
|
|
|
289
289
|
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
290
290
|
|
|
291
|
-
extractFrame(world, prepareExtractContext(world, { assets }));
|
|
291
|
+
const frame = extractFrame(world, prepareExtractContext(world, { assets }));
|
|
292
292
|
|
|
293
293
|
expect(errorSpy).not.toHaveBeenCalled();
|
|
294
|
+
expect(frame.renderables).toHaveLength(0);
|
|
295
|
+
expect(frame.dispatch.some((entry) => entry.materialShaderId === 'forgeax::pbr-skin')).toBe(
|
|
296
|
+
false,
|
|
297
|
+
);
|
|
294
298
|
});
|
|
295
299
|
|
|
296
300
|
it('(d) D-5 continue semantics: bad-skin entity skipped does NOT abort sibling extract', () => {
|
|
@@ -25,4 +25,17 @@ describe('frame targets shadow removal', () => {
|
|
|
25
25
|
|
|
26
26
|
expect(resolveShadowMapSize(internals as never, lights as never)).toBe(1024);
|
|
27
27
|
});
|
|
28
|
+
|
|
29
|
+
it('does not resolve a Directional atlas for the disabled topology sentinel', () => {
|
|
30
|
+
const internals = {
|
|
31
|
+
device: { limits: { maxTextureDimension2D: 2048 } },
|
|
32
|
+
};
|
|
33
|
+
const lights = {
|
|
34
|
+
directional: 'disabled',
|
|
35
|
+
shadowMapSize: 2048,
|
|
36
|
+
cascadeCount: undefined,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
expect(resolveShadowMapSize(internals as never, lights as never)).toBeUndefined();
|
|
40
|
+
});
|
|
28
41
|
});
|
|
@@ -2390,20 +2390,17 @@ function drawPublished(renderer: RendererType, world: WorldType) {
|
|
|
2390
2390
|
spies.drawIndexed.mockClear();
|
|
2391
2391
|
drawPublished(renderer, world as WorldType);
|
|
2392
2392
|
|
|
2393
|
-
//
|
|
2394
|
-
//
|
|
2393
|
+
// Mesh indexed draws carry the Instances cardinality. Fullscreen graph
|
|
2394
|
+
// draws are a separate topology concern and intentionally use one
|
|
2395
|
+
// instance; do not mix those two contracts in one equality assertion.
|
|
2395
2396
|
const indexedCalls = spies.drawIndexed.mock.calls as Array<
|
|
2396
2397
|
[number, number, number, number, number]
|
|
2397
2398
|
>;
|
|
2398
2399
|
const drawCalls = spies.draw.mock.calls as Array<[number, number, number, number]>;
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
for (const ic of allInstanceCounts) {
|
|
2404
|
-
expect(ic).toBe(first);
|
|
2405
|
-
}
|
|
2406
|
-
}
|
|
2400
|
+
expect(indexedCalls.length).toBeGreaterThan(0);
|
|
2401
|
+
expect(indexedCalls.map((call) => call[1])).toEqual(expect.arrayContaining([4]));
|
|
2402
|
+
expect(indexedCalls.every((call) => call[1] === 4)).toBe(true);
|
|
2403
|
+
expect(drawCalls.every((call) => call[1] === 1)).toBe(true);
|
|
2407
2404
|
expect(errors).toEqual([]);
|
|
2408
2405
|
});
|
|
2409
2406
|
|
|
@@ -24,7 +24,14 @@ import {
|
|
|
24
24
|
TileLayer,
|
|
25
25
|
Tilemap,
|
|
26
26
|
} from '@forgeax/engine-render/authoring';
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
ChildOf,
|
|
29
|
+
Children,
|
|
30
|
+
GlobalTransform,
|
|
31
|
+
MorphWeights,
|
|
32
|
+
Name,
|
|
33
|
+
Transform,
|
|
34
|
+
} from '@forgeax/engine-scene';
|
|
28
35
|
|
|
29
36
|
const RUNTIME_COMPONENTS: readonly Component[] = [
|
|
30
37
|
Camera,
|
|
@@ -51,6 +58,7 @@ const RUNTIME_COMPONENTS: readonly Component[] = [
|
|
|
51
58
|
TileLayer,
|
|
52
59
|
Tilemap,
|
|
53
60
|
Transform,
|
|
61
|
+
GlobalTransform,
|
|
54
62
|
VideoPlayer,
|
|
55
63
|
Visibility,
|
|
56
64
|
];
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import type { Component, World } from '@forgeax/engine-ecs';
|
|
2
2
|
import { SceneInstance } from '@forgeax/engine-render';
|
|
3
|
-
import { ChildOf, Children, Transform } from '@forgeax/engine-scene';
|
|
3
|
+
import { ChildOf, Children, GlobalTransform, Transform } from '@forgeax/engine-scene';
|
|
4
4
|
|
|
5
5
|
/** Register the scene vocabulary explicitly for an isolated test World. */
|
|
6
6
|
export function registerSceneComponents(world: World, extras: readonly Component[] = []): void {
|
|
7
|
-
for (const component of [
|
|
7
|
+
for (const component of [
|
|
8
|
+
SceneInstance,
|
|
9
|
+
ChildOf,
|
|
10
|
+
Children,
|
|
11
|
+
Transform,
|
|
12
|
+
GlobalTransform,
|
|
13
|
+
...extras,
|
|
14
|
+
]) {
|
|
8
15
|
world.components.register(component).unwrap();
|
|
9
16
|
}
|
|
10
17
|
}
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// lock the four light GPU pack paths' byte output so the collapse is proven
|
|
8
8
|
// byte-neutral end to end:
|
|
9
9
|
// 1. View UBO (DirectionalLight direction/color lanes) -> writeViewUbo
|
|
10
|
-
// 2. PointLightBuffer (color lanes) ->
|
|
11
|
-
// 3. SpotLightBuffer (direction + color lanes) ->
|
|
10
|
+
// 2. PointLightBuffer (color lanes) -> packDirectLightSlot
|
|
11
|
+
// 3. SpotLightBuffer (direction + color lanes) -> packDirectLightSlot
|
|
12
12
|
// 4. Skylight uniform/ambient (color destructure) -> SkylightSnapshot
|
|
13
13
|
// (Camera clear pass is M3, not here.)
|
|
14
14
|
|
|
@@ -16,7 +16,7 @@ import { vec3 } from '@forgeax/engine-math';
|
|
|
16
16
|
import type { Buffer, RhiQueue } from '@forgeax/engine-rhi';
|
|
17
17
|
import { ok } from '@forgeax/engine-rhi';
|
|
18
18
|
import { describe, expect, it } from 'vitest';
|
|
19
|
-
import {
|
|
19
|
+
import { packDirectLightSlot } from '../../../render/src/light-buffer-layout';
|
|
20
20
|
import { writeViewUbo } from '../../../render/src/record/view-ubo';
|
|
21
21
|
import type { CameraSnapshot } from '../../../render/src/render-contract';
|
|
22
22
|
import type {
|
|
@@ -57,13 +57,14 @@ const emptyLights: ExtractedLights = {
|
|
|
57
57
|
directionalCount: 0,
|
|
58
58
|
point: [],
|
|
59
59
|
spot: [],
|
|
60
|
+
directionalShadowError: undefined,
|
|
60
61
|
lightViewProj: undefined,
|
|
61
62
|
splitPlanes: undefined,
|
|
62
63
|
cascadeCount: undefined,
|
|
63
64
|
cascadeBlend: undefined,
|
|
64
65
|
depthBias: undefined,
|
|
65
66
|
normalBias: undefined,
|
|
66
|
-
|
|
67
|
+
directionalShadowQuality: undefined,
|
|
67
68
|
} as unknown as ExtractedLights;
|
|
68
69
|
|
|
69
70
|
/** Capture the single writeViewUbo payload as a Float32Array. */
|
|
@@ -102,7 +103,7 @@ describe('w7 -- light GPU pack byte-neutral (AC-11)', () => {
|
|
|
102
103
|
expect(payload[22]).toBeCloseTo(0.7, 5);
|
|
103
104
|
});
|
|
104
105
|
|
|
105
|
-
it('
|
|
106
|
+
it('packDirectLightSlot writes Point color into slots 4..6 byte-for-byte (80 B)', () => {
|
|
106
107
|
const snap: PointLightSnapshot = {
|
|
107
108
|
kind: 'point',
|
|
108
109
|
position: vec3.create(1.5, -2.25, 0.125),
|
|
@@ -110,14 +111,14 @@ describe('w7 -- light GPU pack byte-neutral (AC-11)', () => {
|
|
|
110
111
|
intensity: 2,
|
|
111
112
|
invRangeSquared: 0.04,
|
|
112
113
|
};
|
|
113
|
-
const out =
|
|
114
|
-
expect(out.byteLength).toBe(
|
|
114
|
+
const out = packDirectLightSlot(snap);
|
|
115
|
+
expect(out.byteLength).toBe(80);
|
|
115
116
|
expect(out[4]).toBeCloseTo(0.4, 6);
|
|
116
117
|
expect(out[5]).toBeCloseTo(0.5, 6);
|
|
117
118
|
expect(out[6]).toBeCloseTo(0.6, 6);
|
|
118
119
|
});
|
|
119
120
|
|
|
120
|
-
it('
|
|
121
|
+
it('packDirectLightSlot writes Spot color slots 4..6 + direction slots 8..10 byte-for-byte (80 B)', () => {
|
|
121
122
|
const snap: SpotLightSnapshot = {
|
|
122
123
|
kind: 'spot',
|
|
123
124
|
position: vec3.create(0, 5, 0),
|
|
@@ -134,9 +135,7 @@ describe('w7 -- light GPU pack byte-neutral (AC-11)', () => {
|
|
|
134
135
|
farPlane: 50,
|
|
135
136
|
shadowAtlasTile: -1,
|
|
136
137
|
};
|
|
137
|
-
const out =
|
|
138
|
-
// SpotLight carries projector receiver controls and the shadow intensity
|
|
139
|
-
// tail in the current std430 payload.
|
|
138
|
+
const out = packDirectLightSlot(snap);
|
|
140
139
|
expect(out.byteLength).toBe(80);
|
|
141
140
|
expect(out[4]).toBeCloseTo(0.3, 6);
|
|
142
141
|
expect(out[5]).toBeCloseTo(0.4, 6);
|
|
@@ -21,11 +21,18 @@
|
|
|
21
21
|
// covers any .test-d.ts file in any package).
|
|
22
22
|
|
|
23
23
|
import { expectTypeOf, test } from 'vitest';
|
|
24
|
+
import type { DirectionalShadowQuality } from '../../../render/src/components/directional-shadow-filter';
|
|
25
|
+
import type { LightValidationError } from '../../../render/src/components/light-helpers';
|
|
26
|
+
import type {
|
|
27
|
+
DirectLightSlotKind,
|
|
28
|
+
DirectLightSnapshot,
|
|
29
|
+
} from '../../../render/src/light-buffer-layout';
|
|
24
30
|
import type {
|
|
25
31
|
DirectionalLightSnapshot,
|
|
26
32
|
ExtractedLights,
|
|
27
33
|
LightSnapshot,
|
|
28
34
|
PointLightSnapshot,
|
|
35
|
+
RectAreaDirectLightSnapshot,
|
|
29
36
|
SpotLightSnapshot,
|
|
30
37
|
} from '../../../render/src/render-system-extract';
|
|
31
38
|
|
|
@@ -44,6 +51,8 @@ function consumeSnapshot(s: LightSnapshot): number {
|
|
|
44
51
|
return s.invRangeSquared;
|
|
45
52
|
case 'spot':
|
|
46
53
|
return s.cosInner + s.cosOuter;
|
|
54
|
+
case 'rect-area':
|
|
55
|
+
return s.halfWidth + s.halfHeight;
|
|
47
56
|
default:
|
|
48
57
|
return assertNever(s);
|
|
49
58
|
}
|
|
@@ -51,7 +60,7 @@ function consumeSnapshot(s: LightSnapshot): number {
|
|
|
51
60
|
|
|
52
61
|
test('LightSnapshot is a discriminated union on kind', () => {
|
|
53
62
|
expectTypeOf<LightSnapshot>().toEqualTypeOf<
|
|
54
|
-
DirectionalLightSnapshot | PointLightSnapshot | SpotLightSnapshot
|
|
63
|
+
DirectionalLightSnapshot | PointLightSnapshot | SpotLightSnapshot | RectAreaDirectLightSnapshot
|
|
55
64
|
>();
|
|
56
65
|
// Smoke that the consumer compiles (run-time noop; compile-time exhaustive).
|
|
57
66
|
void consumeSnapshot;
|
|
@@ -81,4 +90,29 @@ test('ExtractedLights three-bucket shape', () => {
|
|
|
81
90
|
expectTypeOf<ExtractedLights['directionalCount']>().toEqualTypeOf<number>();
|
|
82
91
|
expectTypeOf<ExtractedLights['point']>().toEqualTypeOf<readonly PointLightSnapshot[]>();
|
|
83
92
|
expectTypeOf<ExtractedLights['spot']>().toEqualTypeOf<readonly SpotLightSnapshot[]>();
|
|
93
|
+
expectTypeOf<ExtractedLights['rect']>().toEqualTypeOf<readonly RectAreaDirectLightSnapshot[]>();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
function consumeDirectLight(snapshot: DirectLightSnapshot): number {
|
|
97
|
+
switch (snapshot.kind) {
|
|
98
|
+
case 'point':
|
|
99
|
+
return snapshot.invRangeSquared;
|
|
100
|
+
case 'spot':
|
|
101
|
+
return snapshot.cosInner + snapshot.cosOuter;
|
|
102
|
+
case 'rect-area':
|
|
103
|
+
return snapshot.halfWidth + snapshot.halfHeight;
|
|
104
|
+
default:
|
|
105
|
+
return assertNever(snapshot);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
test('DirectLightSlot kind is closed and includes only finite direct lights', () => {
|
|
110
|
+
expectTypeOf<DirectLightSlotKind>().toEqualTypeOf<'point' | 'spot' | 'rect-area'>();
|
|
111
|
+
void consumeDirectLight;
|
|
112
|
+
expectTypeOf<ExtractedLights['directionalShadowQuality']>().toEqualTypeOf<
|
|
113
|
+
DirectionalShadowQuality | undefined
|
|
114
|
+
>();
|
|
115
|
+
expectTypeOf<ExtractedLights['directionalShadowError']>().toEqualTypeOf<
|
|
116
|
+
LightValidationError | undefined
|
|
117
|
+
>();
|
|
84
118
|
});
|