@forgeax/engine-runtime 0.1.28 → 0.1.29
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 +98 -0
- package/dist/collect-scene-asset.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +480 -15
- package/dist/index.mjs.map +1 -1
- package/dist/scene-utils/mount-override-fold.d.ts +3 -1
- package/dist/scene-utils/mount-override-fold.d.ts.map +1 -1
- package/package.json +37 -32
- package/src/__tests__/animation-graph-collect-roundtrip.test.ts +4 -2
- package/src/__tests__/asset-registry-aabb.cases.unit.test.ts +5 -4
- package/src/__tests__/asset-registry-guid-reverse.test.ts +22 -27
- package/src/__tests__/asset-registry-mounts.test.ts +174 -734
- package/src/__tests__/asset-registry-scene.cases.unit.test.ts +9 -18
- package/src/__tests__/asset-registry.recursive.spec.ts +23 -29
- package/src/__tests__/camera-ortho.cases.unit.test.ts +12 -3
- package/src/__tests__/collect-scene-asset.test.ts +96 -113
- package/src/__tests__/collect-scene-mount-collapse.test.ts +64 -66
- package/src/__tests__/collect-transient-children-absent.test.ts +11 -17
- package/src/__tests__/collect-transient-roundtrip.test.ts +12 -13
- package/src/__tests__/collect-transient-skip.test.ts +20 -27
- package/src/__tests__/components.test-d.ts +17 -1
- package/src/__tests__/components.unit.test.ts +75 -27
- package/src/__tests__/composite-skybox-cross-world.browser.test.ts +4 -2
- package/src/__tests__/dawn/instances-per-instance-pbr.dawn.test.ts +8 -4
- package/src/__tests__/dawn/instances-shadow.dawn.test.ts +4 -1
- package/src/__tests__/dawn/instances-uniform-fallback.dawn.test.ts +66 -92
- package/src/__tests__/dawn/material-cooked-fixture.dawn.test.ts +200 -4
- package/src/__tests__/errors.unit.test.ts +13 -0
- package/src/__tests__/feature-depth-material.dawn.test.ts +23 -0
- package/src/__tests__/feature-depth-material.fixture.ts +473 -0
- package/src/__tests__/fullscreen-post-process-pass.dawn.test.ts +14 -14
- package/src/__tests__/geometry.unit.test.ts +42 -43
- package/src/__tests__/gpu-resource-release.cases.unit.test.ts +16 -9
- package/src/__tests__/handle-migration.test.ts +0 -1
- package/src/__tests__/instances-renderer-lifecycle.integration.test.ts +20 -14
- package/src/__tests__/instances.test-d.ts +13 -52
- package/src/__tests__/instantiate-mount-deferred-wiring.test.ts +42 -80
- package/src/__tests__/lazy-catalog.cases.unit.test.ts +17 -3
- package/src/__tests__/loader-registry.cases.unit.test.ts +4 -4
- package/src/__tests__/material-publication.browser.test.ts +29 -0
- package/src/__tests__/material-publication.commands.ts +23 -0
- package/src/__tests__/material-publication.dawn.test.ts +24 -0
- package/src/__tests__/material-publication.fixture.ts +349 -0
- package/src/__tests__/material-publication.server.ts +109 -0
- package/src/__tests__/parse-scene-payload-refs.cases.unit.test.ts +73 -96
- package/src/__tests__/pbr-pipeline.unit.test.ts +25 -11
- package/src/__tests__/point-light-shadow.browser.test.ts +8 -2
- package/src/__tests__/point-light-shadow.unit.test.ts +37 -8
- package/src/__tests__/record-all-topology.cases.unit.test.ts +52 -0
- package/src/__tests__/record-draw-branch.cases.unit.test.ts +1 -0
- package/src/__tests__/record-worldid-isolation.test.ts +47 -28
- package/src/__tests__/render-error-exhaustive.test-d.ts +24 -0
- package/src/__tests__/render-system-extract-dispatch-ordering.test.ts +15 -4
- package/src/__tests__/render-system-extract.test.ts +61 -0
- package/src/__tests__/render-system-extract.unit.test.ts +21 -7
- package/src/__tests__/render-system-fold-bucket.unit.test.ts +33 -1
- package/src/__tests__/render-system-fold-material.unit.test.ts +32 -6
- package/src/__tests__/render-system-fold-modegate.unit.test.ts +26 -4
- package/src/__tests__/render-system-fold.dawn.test.ts +4 -1
- package/src/__tests__/render-system-mega.test.ts +50 -62
- package/src/__tests__/renderer-device-loss-provider-feasibility.dawn.test.ts +0 -1
- package/src/__tests__/renderer-device-loss-recovery.dawn.test.ts +0 -3
- package/src/__tests__/renderer-draw-world.cases.unit.test.ts +16 -513
- package/src/__tests__/resolve-scene-guids.cases.unit.test.ts +48 -43
- package/src/__tests__/rhi-null-command-flow.unit.test.ts +9 -7
- package/src/__tests__/roots-to-scene-asset.test.ts +27 -28
- package/src/__tests__/roundtrip-primitive.test.ts +24 -23
- package/src/__tests__/scene-equirect-roundtrip.test.ts +5 -16
- package/src/__tests__/scene-instantiate-cycle-runtime.test.ts +13 -29
- package/src/__tests__/scene-mount-fixed-point.test.ts +104 -227
- package/src/__tests__/scene-mount-roundtrip.test.ts +95 -961
- package/src/__tests__/scene-mount-wrapper-roundtrip.test.ts +36 -182
- package/src/__tests__/scene-sugar-instantiate.test.ts +20 -15
- package/src/__tests__/serialize-mounts-roundtrip.test.ts +108 -492
- package/src/__tests__/serialize-scene-asset.test.ts +36 -50
- package/src/__tests__/shadow-caster-empty-schema.test.ts +3 -1
- package/src/__tests__/shadow-fields-observable.dawn.test.ts +34 -19
- package/src/__tests__/shadow-mesh-ssbo-order.test.ts +5 -12
- package/src/__tests__/skin-extract-getarrayview-count.unit.test.ts +6 -4
- package/src/__tests__/skin-m2-extract-happy.cases.unit.test.ts +50 -2
- package/src/__tests__/skin-m2-extract-joint-error.cases.unit.test.ts +50 -2
- package/src/__tests__/skin-m2-extract-null-assets.cases.unit.test.ts +50 -2
- package/src/__tests__/skin-m2-extract-skeleton-error.cases.unit.test.ts +50 -2
- package/src/__tests__/skin-palette-extract.cases.unit.test.ts +2 -0
- package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +93 -48
- package/src/__tests__/skylight-pipeline-layout.cases.unit.test.ts +6 -2
- package/src/__tests__/sparse-tag-scene-roundtrip.test.ts +1 -1
- package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +11 -3
- package/src/__tests__/template-game-default-api-contract.test.ts +21 -21
- package/src/__tests__/tonemap-hdr-target.cases.unit.test.ts +8 -5
- package/src/__tests__/tonemap-pipeline-split.cases.unit.test.ts +3 -0
- package/src/__tests__/transient-fields-collect.test.ts +4 -4
- package/src/__tests__/transient-runtime-behavior.test.ts +9 -9
- package/src/__tests__/vfx-depth-material.dawn.test.ts +42 -0
- package/src/__tests__/vfx-mesh-lighting.browser.test.ts +16 -0
- package/src/__tests__/vfx-mesh-lighting.commands.ts +14 -0
- package/src/__tests__/vfx-mesh-lighting.dawn.test.ts +16 -0
- package/src/__tests__/vfx-mesh-lighting.fixture.ts +883 -0
- package/src/__tests__/wave1-rendering-materials.browser.test.ts +617 -0
- package/src/__tests__/wave1-rendering-p0.browser.test.ts +751 -0
- package/src/__tests__/wave1-rendering-recovery.browser.test.ts +666 -0
- package/src/__tests__/wave1-shadow-diagnostic.browser.test.ts +582 -0
- package/src/__tests__/world-scene-despawn.test.ts +8 -8
- package/src/__tests__/world-scene-instantiate.test.ts +23 -29
- package/src/__tests__/world-scene-nested.test.ts +90 -163
- package/src/__tests__/world-scene-override.test.ts +17 -16
- package/src/__tests__/zero-compression-zero-load.integration.test.ts +1 -0
- package/src/collect-scene-asset.ts +637 -19
- package/src/components/__tests__/sprite-animation.test-d.ts +2 -2
- package/src/index.ts +1 -0
- package/src/scene-utils/mount-override-fold.ts +5 -14
- package/src/__tests__/node-shims.d.ts +0 -31
|
@@ -436,22 +436,24 @@ type __MergedKeep =
|
|
|
436
436
|
const fn = accessParseScenePayload(reg);
|
|
437
437
|
const refs = [GUID_A, GUID_B];
|
|
438
438
|
const payload = {
|
|
439
|
-
entities:
|
|
440
|
-
{
|
|
441
|
-
localId: 0,
|
|
439
|
+
entities: {
|
|
440
|
+
'entity-0': {
|
|
442
441
|
components: {
|
|
443
442
|
MeshFilter: { assetHandle: 0 },
|
|
444
443
|
MeshRenderer: { materials: [1] },
|
|
445
444
|
},
|
|
446
445
|
},
|
|
447
|
-
|
|
446
|
+
},
|
|
448
447
|
};
|
|
449
448
|
const asset = fn('scene', payload, refs);
|
|
450
449
|
expect(asset).toBeDefined();
|
|
451
450
|
if (!asset) return;
|
|
452
451
|
expect(asset.kind).toBe('scene');
|
|
453
|
-
expect(asset.entities.length).toBe(1);
|
|
454
|
-
const comp = asset.entities[0]?.components as Record<
|
|
452
|
+
expect(Object.keys(asset.entities).length).toBe(1);
|
|
453
|
+
const comp = asset.entities['entity-0']?.components as Record<
|
|
454
|
+
string,
|
|
455
|
+
Record<string, unknown>
|
|
456
|
+
>;
|
|
455
457
|
expect(comp.MeshFilter?.assetHandle).toBe(GUID_A); // refs[0]
|
|
456
458
|
expect(comp.MeshRenderer?.materials).toEqual([GUID_B]); // refs[1]
|
|
457
459
|
});
|
|
@@ -461,17 +463,15 @@ type __MergedKeep =
|
|
|
461
463
|
const fn = accessParseScenePayload(reg);
|
|
462
464
|
const refs = [GUID_A];
|
|
463
465
|
const payload = {
|
|
464
|
-
entities:
|
|
465
|
-
{
|
|
466
|
-
localId: 0,
|
|
467
|
-
components: { MeshFilter: { assetHandle: 0 } },
|
|
468
|
-
},
|
|
469
|
-
],
|
|
466
|
+
entities: { 'entity-0': { components: { MeshFilter: { assetHandle: 0 } } } },
|
|
470
467
|
};
|
|
471
468
|
const asset = fn('scene', payload, refs);
|
|
472
469
|
expect(asset).toBeDefined();
|
|
473
470
|
if (!asset) return;
|
|
474
|
-
const comp = asset.entities[0]?.components as Record<
|
|
471
|
+
const comp = asset.entities['entity-0']?.components as Record<
|
|
472
|
+
string,
|
|
473
|
+
Record<string, unknown>
|
|
474
|
+
>;
|
|
475
475
|
expect(comp.MeshFilter?.assetHandle).toBe(GUID_A);
|
|
476
476
|
});
|
|
477
477
|
|
|
@@ -480,20 +480,22 @@ type __MergedKeep =
|
|
|
480
480
|
const fn = accessParseScenePayload(reg);
|
|
481
481
|
const refs = [GUID_A];
|
|
482
482
|
const payload = {
|
|
483
|
-
entities:
|
|
484
|
-
{
|
|
485
|
-
localId: 0,
|
|
483
|
+
entities: {
|
|
484
|
+
'entity-0': {
|
|
486
485
|
components: {
|
|
487
486
|
Transform: { pos: [1.5, 2.7, -3.2] },
|
|
488
487
|
DirectionalLight: { color: 'white', intensity: 0.8 },
|
|
489
488
|
},
|
|
490
489
|
},
|
|
491
|
-
|
|
490
|
+
},
|
|
492
491
|
};
|
|
493
492
|
const asset = fn('scene', payload, refs);
|
|
494
493
|
expect(asset).toBeDefined();
|
|
495
494
|
if (!asset) return;
|
|
496
|
-
const comp = asset.entities[0]?.components as Record<
|
|
495
|
+
const comp = asset.entities['entity-0']?.components as Record<
|
|
496
|
+
string,
|
|
497
|
+
Record<string, unknown>
|
|
498
|
+
>;
|
|
497
499
|
// Non-integer float values are not refs indices — passed through unchanged.
|
|
498
500
|
expect(comp.Transform?.pos).toEqual([1.5, 2.7, -3.2]);
|
|
499
501
|
expect(comp.DirectionalLight?.color).toBe('white');
|
|
@@ -505,23 +507,25 @@ type __MergedKeep =
|
|
|
505
507
|
const fn = accessParseScenePayload(reg);
|
|
506
508
|
const refs = [GUID_A];
|
|
507
509
|
const payload = {
|
|
508
|
-
entities:
|
|
509
|
-
{
|
|
510
|
-
localId: 0,
|
|
510
|
+
entities: {
|
|
511
|
+
'entity-0': {
|
|
511
512
|
components: {
|
|
512
|
-
ChildOf: { parent: 0 },
|
|
513
|
+
ChildOf: { parent: 'entity-0' },
|
|
513
514
|
Transform: { pos: [0, 0, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
|
|
514
515
|
MeshFilter: { assetHandle: 0 },
|
|
515
516
|
},
|
|
516
517
|
},
|
|
517
|
-
|
|
518
|
+
},
|
|
518
519
|
};
|
|
519
520
|
const asset = fn('scene', payload, refs);
|
|
520
521
|
expect(asset).toBeDefined();
|
|
521
522
|
if (!asset) return;
|
|
522
|
-
const comp = asset.entities[0]?.components as Record<
|
|
523
|
+
const comp = asset.entities['entity-0']?.components as Record<
|
|
524
|
+
string,
|
|
525
|
+
Record<string, unknown>
|
|
526
|
+
>;
|
|
523
527
|
// Non-handle integer fields are kept as-is (M1-fixup F-1)
|
|
524
|
-
expect(comp.ChildOf?.parent).toBe(0);
|
|
528
|
+
expect(comp.ChildOf?.parent).toBe('entity-0');
|
|
525
529
|
expect(comp.Transform?.pos).toEqual([0, 0, 0]);
|
|
526
530
|
expect(comp.Transform?.quat).toEqual([0, 0, 0, 1]);
|
|
527
531
|
expect(comp.Transform?.scale).toEqual([1, 1, 1]);
|
|
@@ -534,28 +538,28 @@ type __MergedKeep =
|
|
|
534
538
|
const fn = accessParseScenePayload(reg);
|
|
535
539
|
const refs = [GUID_A, GUID_B];
|
|
536
540
|
const payload = {
|
|
537
|
-
entities:
|
|
538
|
-
{
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
{
|
|
543
|
-
localId: 1,
|
|
544
|
-
components: { MeshFilter: { assetHandle: 1 } },
|
|
545
|
-
},
|
|
546
|
-
{
|
|
547
|
-
localId: 2,
|
|
548
|
-
components: { MeshRenderer: { materials: [0] } },
|
|
549
|
-
},
|
|
550
|
-
],
|
|
541
|
+
entities: {
|
|
542
|
+
'entity-0': { components: { MeshFilter: { assetHandle: 0 } } },
|
|
543
|
+
'entity-1': { components: { MeshFilter: { assetHandle: 1 } } },
|
|
544
|
+
'entity-2': { components: { MeshRenderer: { materials: [0] } } },
|
|
545
|
+
},
|
|
551
546
|
};
|
|
552
547
|
const asset = fn('scene', payload, refs);
|
|
553
548
|
expect(asset).toBeDefined();
|
|
554
549
|
if (!asset) return;
|
|
555
|
-
expect(asset.entities.length).toBe(3);
|
|
556
|
-
const comp0 = asset.entities[0]?.components as Record<
|
|
557
|
-
|
|
558
|
-
|
|
550
|
+
expect(Object.keys(asset.entities).length).toBe(3);
|
|
551
|
+
const comp0 = asset.entities['entity-0']?.components as Record<
|
|
552
|
+
string,
|
|
553
|
+
Record<string, unknown>
|
|
554
|
+
>;
|
|
555
|
+
const comp1 = asset.entities['entity-1']?.components as Record<
|
|
556
|
+
string,
|
|
557
|
+
Record<string, unknown>
|
|
558
|
+
>;
|
|
559
|
+
const comp2 = asset.entities['entity-2']?.components as Record<
|
|
560
|
+
string,
|
|
561
|
+
Record<string, unknown>
|
|
562
|
+
>;
|
|
559
563
|
expect(comp0.MeshFilter?.assetHandle).toBe(GUID_A);
|
|
560
564
|
expect(comp1.MeshFilter?.assetHandle).toBe(GUID_B);
|
|
561
565
|
expect(comp2.MeshRenderer?.materials).toEqual([GUID_A]);
|
|
@@ -568,12 +572,7 @@ type __MergedKeep =
|
|
|
568
572
|
const fn = accessParseScenePayload(reg);
|
|
569
573
|
const refs = [GUID_A]; // length=1, valid indices: 0 only
|
|
570
574
|
const payload = {
|
|
571
|
-
entities:
|
|
572
|
-
{
|
|
573
|
-
localId: 0,
|
|
574
|
-
components: { MeshFilter: { assetHandle: 1 } }, // N=1 >= refs.length
|
|
575
|
-
},
|
|
576
|
-
],
|
|
575
|
+
entities: { 'entity-0': { components: { MeshFilter: { assetHandle: 1 } } } },
|
|
577
576
|
};
|
|
578
577
|
const result = fn('scene', payload, refs);
|
|
579
578
|
expect(result).toBeUndefined();
|
|
@@ -584,12 +583,7 @@ type __MergedKeep =
|
|
|
584
583
|
const fn = accessParseScenePayload(reg);
|
|
585
584
|
const refs: string[] = [];
|
|
586
585
|
const payload = {
|
|
587
|
-
entities:
|
|
588
|
-
{
|
|
589
|
-
localId: 0,
|
|
590
|
-
components: { MeshFilter: { assetHandle: 0 } }, // N=0 >= refs.length (0 >= 0)
|
|
591
|
-
},
|
|
592
|
-
],
|
|
586
|
+
entities: { 'entity-0': { components: { MeshFilter: { assetHandle: 0 } } } },
|
|
593
587
|
};
|
|
594
588
|
const result = fn('scene', payload, refs);
|
|
595
589
|
expect(result).toBeUndefined();
|
|
@@ -600,16 +594,10 @@ type __MergedKeep =
|
|
|
600
594
|
const fn = accessParseScenePayload(reg);
|
|
601
595
|
const refs = [GUID_A]; // length=1
|
|
602
596
|
const payload = {
|
|
603
|
-
entities:
|
|
604
|
-
{
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
},
|
|
608
|
-
{
|
|
609
|
-
localId: 1,
|
|
610
|
-
components: { MeshFilter: { assetHandle: 0 } }, // second node: valid, but should not be reached
|
|
611
|
-
},
|
|
612
|
-
],
|
|
597
|
+
entities: {
|
|
598
|
+
'entity-0': { components: { MeshFilter: { assetHandle: 1 } } },
|
|
599
|
+
'entity-1': { components: { MeshFilter: { assetHandle: 0 } } },
|
|
600
|
+
},
|
|
613
601
|
};
|
|
614
602
|
// parseScenePayload returns undefined on first error, so result is undefined
|
|
615
603
|
// regardless of the second node's validity
|
|
@@ -622,12 +610,7 @@ type __MergedKeep =
|
|
|
622
610
|
const fn = accessParseScenePayload(reg);
|
|
623
611
|
const refs = [GUID_A];
|
|
624
612
|
const payload = {
|
|
625
|
-
entities:
|
|
626
|
-
{
|
|
627
|
-
localId: 0,
|
|
628
|
-
components: { MeshFilter: { assetHandle: -1 } },
|
|
629
|
-
},
|
|
630
|
-
],
|
|
613
|
+
entities: { 'entity-0': { components: { MeshFilter: { assetHandle: -1 } } } },
|
|
631
614
|
};
|
|
632
615
|
const result = fn('scene', payload, refs);
|
|
633
616
|
expect(result).toBeUndefined();
|
|
@@ -637,21 +620,23 @@ type __MergedKeep =
|
|
|
637
620
|
const reg = new AssetRegistry(makeMockShaderRegistry());
|
|
638
621
|
const fn = accessParseScenePayload(reg);
|
|
639
622
|
const payload = {
|
|
640
|
-
entities:
|
|
641
|
-
{
|
|
642
|
-
localId: 0,
|
|
623
|
+
entities: {
|
|
624
|
+
'entity-0': {
|
|
643
625
|
components: {
|
|
644
626
|
MeshFilter: { assetHandle: 42 },
|
|
645
627
|
MeshRenderer: { materials: [1024] },
|
|
646
628
|
},
|
|
647
629
|
},
|
|
648
|
-
|
|
630
|
+
},
|
|
649
631
|
};
|
|
650
632
|
// No refs passed -- old call signature, numbers should pass through
|
|
651
633
|
const asset = fn('scene', payload);
|
|
652
634
|
expect(asset).toBeDefined();
|
|
653
635
|
if (!asset) return;
|
|
654
|
-
const comp = asset.entities[0]?.components as Record<
|
|
636
|
+
const comp = asset.entities['entity-0']?.components as Record<
|
|
637
|
+
string,
|
|
638
|
+
Record<string, unknown>
|
|
639
|
+
>;
|
|
655
640
|
expect(comp.MeshFilter?.assetHandle).toBe(42);
|
|
656
641
|
expect(comp.MeshRenderer?.materials).toEqual([1024]);
|
|
657
642
|
});
|
|
@@ -661,17 +646,19 @@ type __MergedKeep =
|
|
|
661
646
|
const fn = accessParseScenePayload(reg);
|
|
662
647
|
const refs = [GUID_A];
|
|
663
648
|
const payload = {
|
|
664
|
-
entities:
|
|
665
|
-
{
|
|
666
|
-
localId: 0,
|
|
649
|
+
entities: {
|
|
650
|
+
'entity-0': {
|
|
667
651
|
components: { Transform: { pos: [0, 0, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] } },
|
|
668
652
|
},
|
|
669
|
-
|
|
653
|
+
},
|
|
670
654
|
};
|
|
671
655
|
const asset = fn('scene', payload, refs);
|
|
672
656
|
expect(asset).toBeDefined();
|
|
673
657
|
if (!asset) return;
|
|
674
|
-
const comp = asset.entities[0]?.components as Record<
|
|
658
|
+
const comp = asset.entities['entity-0']?.components as Record<
|
|
659
|
+
string,
|
|
660
|
+
Record<string, unknown>
|
|
661
|
+
>;
|
|
675
662
|
// M1-fixup F-1: non-handle integer fields (Transform) are preserved as-is.
|
|
676
663
|
// Only fields in the HANDLE_FIELD_NAMES allowlist get refs resolution.
|
|
677
664
|
expect(comp.Transform?.pos).toEqual([0, 0, 0]);
|
|
@@ -690,12 +677,7 @@ type __MergedKeep =
|
|
|
690
677
|
// parseAssetPayload returns { ok: false, error }, then fetchPackFile
|
|
691
678
|
// wraps it as asset-parse-failed.
|
|
692
679
|
const badPayload = {
|
|
693
|
-
entities:
|
|
694
|
-
{
|
|
695
|
-
localId: 0,
|
|
696
|
-
components: { MeshFilter: { assetHandle: 5 } }, // refs length is 3, 5 is out of bounds
|
|
697
|
-
},
|
|
698
|
-
],
|
|
680
|
+
entities: { 'entity-0': { components: { MeshFilter: { assetHandle: 5 } } } },
|
|
699
681
|
};
|
|
700
682
|
const packEntry = {
|
|
701
683
|
guid: '00000000-0000-7000-8000-000000000099',
|
|
@@ -724,7 +706,7 @@ type __MergedKeep =
|
|
|
724
706
|
// the LoaderOutput { ok: false, error } arm (no instance-slot side effect).
|
|
725
707
|
const result = internal.parseAssetPayload('scene', badPayload, packEntry.refs) as {
|
|
726
708
|
ok: false;
|
|
727
|
-
error: { index: number; refsLength: number };
|
|
709
|
+
error: { entityKey: string; index: number; refsLength: number };
|
|
728
710
|
};
|
|
729
711
|
expect(result.ok).toBe(false);
|
|
730
712
|
expect(result.error.index).toBe(5);
|
|
@@ -735,12 +717,7 @@ type __MergedKeep =
|
|
|
735
717
|
// biome-ignore lint/suspicious/noExplicitAny: private method access
|
|
736
718
|
const internal = reg as any;
|
|
737
719
|
const badPayload = {
|
|
738
|
-
entities:
|
|
739
|
-
{
|
|
740
|
-
localId: 42,
|
|
741
|
-
components: { MeshFilter: { assetHandle: 5 } },
|
|
742
|
-
},
|
|
743
|
-
],
|
|
720
|
+
entities: { 'entity-42': { components: { MeshFilter: { assetHandle: 5 } } } },
|
|
744
721
|
};
|
|
745
722
|
const refs = ['guid-a', 'guid-b']; // length=2, 5 is out of bounds
|
|
746
723
|
// F21: the structured error is the return value's `error` field, not a
|
|
@@ -748,7 +725,7 @@ type __MergedKeep =
|
|
|
748
725
|
const result = internal.parseAssetPayload('scene', badPayload, refs) as {
|
|
749
726
|
ok: false;
|
|
750
727
|
error: {
|
|
751
|
-
|
|
728
|
+
entityKey: string;
|
|
752
729
|
component: string;
|
|
753
730
|
field: string;
|
|
754
731
|
index: number;
|
|
@@ -757,7 +734,7 @@ type __MergedKeep =
|
|
|
757
734
|
};
|
|
758
735
|
expect(result.ok).toBe(false);
|
|
759
736
|
const err = result.error;
|
|
760
|
-
expect(err.
|
|
737
|
+
expect(err.entityKey).toBe('entity-42');
|
|
761
738
|
expect(err.component).toBe('MeshFilter');
|
|
762
739
|
expect(err.field).toBe('assetHandle');
|
|
763
740
|
expect(err.index).toBe(5);
|
|
@@ -102,7 +102,7 @@ describe('buildBindGroupLayoutDescriptor — pbr-pipeline 6 sites byte-equiv', (
|
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
describe('pbr-material-merged', () => {
|
|
105
|
-
it('
|
|
105
|
+
it('25 entries: user-region 15 + ibl 7 + transmission 2 + scene material 1', () => {
|
|
106
106
|
const spec = makeSpec();
|
|
107
107
|
const out = buildBindGroupLayoutDescriptor(spec, {
|
|
108
108
|
kind: 'pbr-material-merged',
|
|
@@ -114,10 +114,18 @@ describe('buildBindGroupLayoutDescriptor — pbr-pipeline 6 sites byte-equiv', (
|
|
|
114
114
|
const afterIbl = [...userRegion, ...appendInjection(userRegion, 'ibl')];
|
|
115
115
|
const expected = {
|
|
116
116
|
label: 'pbr-material-skylight-bgl',
|
|
117
|
-
entries: [
|
|
117
|
+
entries: [
|
|
118
|
+
...afterIbl,
|
|
119
|
+
...appendInjection(afterIbl, 'transmission'),
|
|
120
|
+
{
|
|
121
|
+
binding: 46,
|
|
122
|
+
visibility: 0x1 | 0x2,
|
|
123
|
+
buffer: { type: 'read-only-storage', hasDynamicOffset: false },
|
|
124
|
+
},
|
|
125
|
+
],
|
|
118
126
|
};
|
|
119
127
|
expect(out).toEqual(expected);
|
|
120
|
-
expect(out.entries.length).toBe(
|
|
128
|
+
expect(out.entries.length).toBe(25);
|
|
121
129
|
});
|
|
122
130
|
|
|
123
131
|
it('adds only authored physical map pairs after the canonical injection chain', () => {
|
|
@@ -131,10 +139,7 @@ describe('buildBindGroupLayoutDescriptor — pbr-pipeline 6 sites byte-equiv', (
|
|
|
131
139
|
kind: 'pbr-material-merged',
|
|
132
140
|
materialParamSchema: schema,
|
|
133
141
|
});
|
|
134
|
-
expect(out.entries.map((entry) => entry.binding)).toEqual([
|
|
135
|
-
...Array.from({ length: 28 }, (_, index) => index),
|
|
136
|
-
]);
|
|
137
|
-
expect(out.entries.slice(24).map((entry) => entry.binding)).toEqual([24, 25, 26, 27]);
|
|
142
|
+
expect(out.entries.slice(24).map((entry) => entry.binding)).toEqual([26, 27, 30, 31, 46]);
|
|
138
143
|
});
|
|
139
144
|
});
|
|
140
145
|
|
|
@@ -168,7 +173,7 @@ describe('buildBindGroupLayoutDescriptor — pbr-pipeline 6 sites byte-equiv', (
|
|
|
168
173
|
});
|
|
169
174
|
|
|
170
175
|
describe('pbr-instances', () => {
|
|
171
|
-
it('storageBuffer=true:
|
|
176
|
+
it('storageBuffer=true: one vertex-visible read-only-storage entry', () => {
|
|
172
177
|
const spec = makeSpec();
|
|
173
178
|
const out = buildBindGroupLayoutDescriptor(spec, {
|
|
174
179
|
kind: 'pbr-instances',
|
|
@@ -186,13 +191,22 @@ describe('buildBindGroupLayoutDescriptor — pbr-pipeline 6 sites byte-equiv', (
|
|
|
186
191
|
});
|
|
187
192
|
});
|
|
188
193
|
|
|
189
|
-
it('storageBuffer=false: falls back to uniform', () => {
|
|
194
|
+
it('storageBuffer=false: one entry falls back to uniform', () => {
|
|
190
195
|
const spec = makeSpec();
|
|
191
196
|
const out = buildBindGroupLayoutDescriptor(spec, {
|
|
192
197
|
kind: 'pbr-instances',
|
|
193
198
|
caps: { storageBuffer: false },
|
|
194
199
|
});
|
|
195
|
-
expect(out
|
|
200
|
+
expect(out).toEqual({
|
|
201
|
+
label: 'pbr-instances-bgl',
|
|
202
|
+
entries: [
|
|
203
|
+
{
|
|
204
|
+
binding: 0,
|
|
205
|
+
visibility: 0x3,
|
|
206
|
+
buffer: { type: 'uniform', hasDynamicOffset: false },
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
});
|
|
196
210
|
});
|
|
197
211
|
});
|
|
198
212
|
|
|
@@ -208,7 +222,7 @@ describe('buildBindGroupLayoutDescriptor — pbr-pipeline 6 sites byte-equiv', (
|
|
|
208
222
|
entries: [
|
|
209
223
|
{
|
|
210
224
|
binding: 0,
|
|
211
|
-
visibility:
|
|
225
|
+
visibility: 0x3,
|
|
212
226
|
buffer: { type: 'read-only-storage', hasDynamicOffset: true },
|
|
213
227
|
},
|
|
214
228
|
{
|
|
@@ -232,7 +232,11 @@ function spawnPointShadowScene(world: World, withShadow: boolean): void {
|
|
|
232
232
|
.unwrap();
|
|
233
233
|
world
|
|
234
234
|
.spawn(
|
|
235
|
-
|
|
235
|
+
// A receiver wall sits behind the cube so the point light's occlusion
|
|
236
|
+
// lands on a camera-visible surface. The old floor-only witness could
|
|
237
|
+
// miss the shadow entirely at the fixed camera pose and make a valid
|
|
238
|
+
// shadow pass look identical to the no-shadow baseline.
|
|
239
|
+
{ component: Transform, data: { pos: [0, 0, -0.65], scale: [5, 5, 0.1] } },
|
|
236
240
|
{ component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
|
|
237
241
|
{ component: MeshRenderer, data: { materials: [material] } },
|
|
238
242
|
)
|
|
@@ -246,7 +250,9 @@ function spawnPointShadowScene(world: World, withShadow: boolean): void {
|
|
|
246
250
|
world
|
|
247
251
|
.spawn({
|
|
248
252
|
component: DirectionalLight,
|
|
249
|
-
|
|
253
|
+
// Keep the point light as the only direct source; a directional fill
|
|
254
|
+
// would mask a broken point-shadow factor in this pixel witness.
|
|
255
|
+
data: { direction: [-0.4, -0.8, -1], intensity: 0, castShadow: false },
|
|
250
256
|
})
|
|
251
257
|
.unwrap();
|
|
252
258
|
const point = [1.8, 2.6, 2.2] as const;
|
|
@@ -270,18 +270,49 @@ function spawnValidatedPointShadow(
|
|
|
270
270
|
}
|
|
271
271
|
});
|
|
272
272
|
|
|
273
|
-
it('
|
|
273
|
+
it('projects off-axis casters to the texel addressed by a raw cube direction on all six faces', () => {
|
|
274
|
+
const position = vec3.create(2, 3, 4);
|
|
275
|
+
const shadowMatrices = buildPointShadowMatrices(position, 0.1, 25);
|
|
276
|
+
// WebGPU cube face table: each direction addresses UV=(0.25,0.75).
|
|
277
|
+
// Independent of either camera/matrix helper, and not on a face axis.
|
|
278
|
+
const directions = [
|
|
279
|
+
[2, -1, 1],
|
|
280
|
+
[-2, -1, -1],
|
|
281
|
+
[-1, 2, 1],
|
|
282
|
+
[-1, -2, -1],
|
|
283
|
+
[-1, -1, 2],
|
|
284
|
+
[1, -1, -2],
|
|
285
|
+
] as const;
|
|
286
|
+
for (const [index, matrix] of shadowMatrices.entries()) {
|
|
287
|
+
const direction = directions[index];
|
|
288
|
+
if (direction === undefined) throw new Error('missing cube direction');
|
|
289
|
+
const point = [
|
|
290
|
+
(position[0] ?? 0) + direction[0],
|
|
291
|
+
(position[1] ?? 0) + direction[1],
|
|
292
|
+
(position[2] ?? 0) + direction[2],
|
|
293
|
+
1,
|
|
294
|
+
];
|
|
295
|
+
const clip = [0, 1, 2, 3].map((row) =>
|
|
296
|
+
point.reduce((sum, value, column) => sum + value * (matrix[column * 4 + row] ?? 0), 0),
|
|
297
|
+
);
|
|
298
|
+
const w = clip[3] ?? 1;
|
|
299
|
+
expect(((clip[0] ?? 0) / w) * 0.5 + 0.5).toBeCloseTo(0.25, 6);
|
|
300
|
+
expect(0.5 - ((clip[1] ?? 0) / w) * 0.5).toBeCloseTo(0.75, 6);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('cube +X projects world -X far plane to NDC z=1', () => {
|
|
274
305
|
// Reference: WebGPU [0, 1] NDC. far point on the face axis maps to ndc_z=1.
|
|
275
306
|
const ms = buildPointShadowMatrices(vec3.create(0, 0, 0), 0.1, 10);
|
|
276
307
|
const faceM = ms[0] ?? new Float32Array(16);
|
|
277
|
-
// A point at (10, 0, 0) —
|
|
308
|
+
// A point at (+10, 0, 0) — far distance along cube +X — should project
|
|
278
309
|
// to NDC z = 1 (WebGPU convention, far plane).
|
|
279
310
|
const p = projectPoint(faceM, 10, 0, 0);
|
|
280
311
|
const ndcZ = p.z / p.w;
|
|
281
312
|
expect(ndcZ).toBeCloseTo(1.0, 5);
|
|
282
313
|
});
|
|
283
314
|
|
|
284
|
-
it('
|
|
315
|
+
it('cube +X projects world -X near plane to NDC z=0', () => {
|
|
285
316
|
const ms = buildPointShadowMatrices(vec3.create(0, 0, 0), 0.1, 10);
|
|
286
317
|
const faceM = ms[0] ?? new Float32Array(16);
|
|
287
318
|
const p = projectPoint(faceM, 0.1, 0, 0);
|
|
@@ -291,8 +322,7 @@ function spawnValidatedPointShadow(
|
|
|
291
322
|
|
|
292
323
|
it('AC-03: each face projects its forward axis point at far depth to NDC z=1', () => {
|
|
293
324
|
const ms = buildPointShadowMatrices(vec3.create(0, 0, 0), 0.1, 10);
|
|
294
|
-
//
|
|
295
|
-
// space) should map to NDC z = 1.
|
|
325
|
+
// Directions are in the negated-world cube basis used for sampling.
|
|
296
326
|
const lookDirs: ReadonlyArray<readonly [number, number, number]> = [
|
|
297
327
|
[10, 0, 0], // +X
|
|
298
328
|
[-10, 0, 0], // -X
|
|
@@ -330,7 +360,7 @@ function spawnValidatedPointShadow(
|
|
|
330
360
|
});
|
|
331
361
|
|
|
332
362
|
it('AC-03: WebGPU [0,1] NDC depth — out-of-frustum mid-distance maps to ndc_z in (0, 1)', () => {
|
|
333
|
-
// Pick a mid-point along +X
|
|
363
|
+
// Pick a mid-point along cube +X (world +X).
|
|
334
364
|
const ms = buildPointShadowMatrices(vec3.create(0, 0, 0), 0.1, 10);
|
|
335
365
|
const faceM = ms[0] ?? new Float32Array(16);
|
|
336
366
|
const p = projectPoint(faceM, 5, 0, 0);
|
|
@@ -343,8 +373,7 @@ function spawnValidatedPointShadow(
|
|
|
343
373
|
it('AC-03: lightPos translation — origin reference point at (lightPos + far*lookDir) maps to ndc_z=1', () => {
|
|
344
374
|
const lightPos = vec3.create(2, 3, 4);
|
|
345
375
|
const ms = buildPointShadowMatrices(lightPos, 0.1, 10);
|
|
346
|
-
//
|
|
347
|
-
// plane along the +X face look direction.
|
|
376
|
+
// Cube +X: world point lightPos + (10, 0, 0) is on the far plane.
|
|
348
377
|
// biome-ignore lint/style/noNonNullAssertion: ms.length === 6
|
|
349
378
|
const faceM = ms[0]!;
|
|
350
379
|
const p = projectPoint(faceM, 12, 3, 4);
|
|
@@ -604,5 +604,57 @@ type __MergedKeep =
|
|
|
604
604
|
expect(errors).toEqual([]);
|
|
605
605
|
});
|
|
606
606
|
}
|
|
607
|
+
|
|
608
|
+
it('vertex-only mesh uses draw() without an index buffer', async () => {
|
|
609
|
+
const spies = makePassSpies();
|
|
610
|
+
const { renderer } = await setupRenderer(spies);
|
|
611
|
+
const errors: string[] = [];
|
|
612
|
+
renderer.onError((e) => errors.push(e.code));
|
|
613
|
+
|
|
614
|
+
const world = await spawnScene(renderer, meshForTopology('line-list'));
|
|
615
|
+
if (!(renderer as unknown as RendererType).attach(world as WorldType).ok) {
|
|
616
|
+
throw new Error('World attachment failed');
|
|
617
|
+
}
|
|
618
|
+
(world as WorldType).update().unwrap();
|
|
619
|
+
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
620
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
621
|
+
spies.draw.mockClear();
|
|
622
|
+
spies.drawIndexed.mockClear();
|
|
623
|
+
spies.setIndexBuffer.mockClear();
|
|
624
|
+
spies.setVertexBuffer.mockClear();
|
|
625
|
+
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
626
|
+
|
|
627
|
+
expect(spies.draw).toHaveBeenCalled();
|
|
628
|
+
expect(spies.drawIndexed).not.toHaveBeenCalled();
|
|
629
|
+
expect(spies.setIndexBuffer).not.toHaveBeenCalled();
|
|
630
|
+
expect(spies.setVertexBuffer).toHaveBeenCalled();
|
|
631
|
+
expect(spies.draw.mock.calls[0]?.[0]).toBe(3);
|
|
632
|
+
expect(errors).toEqual([]);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('indexed mesh uses setIndexBuffer() and drawIndexed()', async () => {
|
|
636
|
+
const spies = makePassSpies();
|
|
637
|
+
const { renderer } = await setupRenderer(spies);
|
|
638
|
+
const errors: string[] = [];
|
|
639
|
+
renderer.onError((e) => errors.push(e.code));
|
|
640
|
+
|
|
641
|
+
const world = await spawnScene(renderer, meshForTopology('triangle-list'));
|
|
642
|
+
if (!(renderer as unknown as RendererType).attach(world as WorldType).ok) {
|
|
643
|
+
throw new Error('World attachment failed');
|
|
644
|
+
}
|
|
645
|
+
(world as WorldType).update().unwrap();
|
|
646
|
+
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
647
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
648
|
+
spies.draw.mockClear();
|
|
649
|
+
spies.drawIndexed.mockClear();
|
|
650
|
+
spies.setIndexBuffer.mockClear();
|
|
651
|
+
spies.setVertexBuffer.mockClear();
|
|
652
|
+
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
653
|
+
|
|
654
|
+
expect(spies.setIndexBuffer).toHaveBeenCalled();
|
|
655
|
+
expect(spies.drawIndexed).toHaveBeenCalled();
|
|
656
|
+
expect(spies.setVertexBuffer).toHaveBeenCalled();
|
|
657
|
+
expect(errors).toEqual([]);
|
|
658
|
+
});
|
|
607
659
|
});
|
|
608
660
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @perf-budget-skip: intentional full mock-GPU renderer setup for indexed/non-indexed draw dispatch.
|
|
1
2
|
// @ts-nocheck — merged file: cross-source node:fs/node:path imports outside @types/node coverage in runtime tsconfig
|
|
2
3
|
// Consolidated by feat-20260609-test-pool-startup-reduction-merge-tiny-test-files
|
|
3
4
|
// biome-ignore-all lint/complexity/noUselessLoneBlockStatements: block-scope isolation between merged source files (consolidation paradigm)
|