@forgeax/engine-runtime 0.1.27 → 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 -30
- 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-dynamic-geometry.browser.test.ts +326 -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
|
@@ -72,7 +72,7 @@ import {
|
|
|
72
72
|
propagateTransforms,
|
|
73
73
|
Transform,
|
|
74
74
|
} from '@forgeax/engine-scene';
|
|
75
|
-
import type { Handle,
|
|
75
|
+
import type { Handle, SceneAsset, SceneEntity } from '@forgeax/engine-types';
|
|
76
76
|
import { toShared } from '@forgeax/engine-types';
|
|
77
77
|
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
78
78
|
import { tonemapFromF32 } from '../../../render/src/components/camera';
|
|
@@ -182,9 +182,9 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
182
182
|
expect(schemaRecord.materials).toBe('array<shared<MaterialAsset>>');
|
|
183
183
|
});
|
|
184
184
|
|
|
185
|
-
it('Camera has
|
|
185
|
+
it('Camera has 29 fields including target, exposure, and color grading columns', () => {
|
|
186
186
|
expect(Camera.name).toBe('Camera');
|
|
187
|
-
expect(Object.keys(componentSchema(Camera)).length).toBe(
|
|
187
|
+
expect(Object.keys(componentSchema(Camera)).length).toBe(29);
|
|
188
188
|
expect(componentSchema(Camera).fov).toBe('f32');
|
|
189
189
|
expect(componentSchema(Camera).aspect).toBe('f32');
|
|
190
190
|
expect(componentSchema(Camera).near).toBe('f32');
|
|
@@ -215,6 +215,14 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
215
215
|
// feat-20260617-host-engine-contract-and-video-cutscene / M3: aspect-sync
|
|
216
216
|
// opt-out flag (bool column tier, not f32).
|
|
217
217
|
expect(componentSchema(Camera).autoAspect).toBe('bool');
|
|
218
|
+
expect(componentSchema(Camera).exposureMode).toBe('f32');
|
|
219
|
+
expect(componentSchema(Camera).compensationEv).toBe('f32');
|
|
220
|
+
expect(componentSchema(Camera).rangeEv).toBe('array<f32, 2>');
|
|
221
|
+
expect(componentSchema(Camera).rates).toBe('array<f32, 2>');
|
|
222
|
+
expect(componentSchema(Camera).temperature).toBe('f32');
|
|
223
|
+
expect(componentSchema(Camera).tint).toBe('f32');
|
|
224
|
+
expect(componentSchema(Camera).colorLut).toBe('shared<TextureAsset>');
|
|
225
|
+
expect(componentSchema(Camera).colorLutStrength).toBe('f32');
|
|
218
226
|
});
|
|
219
227
|
|
|
220
228
|
it('DirectionalLight has 14 fields: 3 light + castShadow bool + closed shadow quality fields', () => {
|
|
@@ -806,12 +814,16 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
806
814
|
state: { type: 'unique<SceneInstanceState>' },
|
|
807
815
|
});
|
|
808
816
|
|
|
809
|
-
function
|
|
810
|
-
return
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
817
|
+
function buildScene(nodes: readonly (SceneEntity & { readonly key: string })[]): SceneAsset {
|
|
818
|
+
return {
|
|
819
|
+
kind: 'scene',
|
|
820
|
+
entities: Object.fromEntries(
|
|
821
|
+
nodes.map(({ key, components, instance }) => [
|
|
822
|
+
key,
|
|
823
|
+
{ components, ...(instance === undefined ? {} : { instance }) },
|
|
824
|
+
]),
|
|
825
|
+
),
|
|
826
|
+
};
|
|
815
827
|
}
|
|
816
828
|
|
|
817
829
|
function registerSceneAsset(
|
|
@@ -847,7 +859,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
847
859
|
|
|
848
860
|
const nodes: SceneEntity[] = [
|
|
849
861
|
{
|
|
850
|
-
|
|
862
|
+
key: 'entity-0',
|
|
851
863
|
components: { Transform: { pos: [1.5, 0, 0] } },
|
|
852
864
|
},
|
|
853
865
|
];
|
|
@@ -878,7 +890,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
878
890
|
// asserted in the next test).
|
|
879
891
|
const nodes: SceneEntity[] = [
|
|
880
892
|
{
|
|
881
|
-
|
|
893
|
+
key: 'entity-0',
|
|
882
894
|
components: { Transform: {} },
|
|
883
895
|
},
|
|
884
896
|
];
|
|
@@ -905,7 +917,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
905
917
|
// defaults. AC-12 mandates this path is SILENT (no error code).
|
|
906
918
|
const nodes: SceneEntity[] = [
|
|
907
919
|
{
|
|
908
|
-
|
|
920
|
+
key: 'entity-0',
|
|
909
921
|
components: { Transform: {} },
|
|
910
922
|
},
|
|
911
923
|
];
|
|
@@ -935,7 +947,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
935
947
|
// - read layer: null is what the AI user observes after world.get
|
|
936
948
|
const nodes: SceneEntity[] = [
|
|
937
949
|
{
|
|
938
|
-
|
|
950
|
+
key: 'entity-0',
|
|
939
951
|
components: { TargetSlot: {} },
|
|
940
952
|
},
|
|
941
953
|
];
|
|
@@ -970,7 +982,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
970
982
|
|
|
971
983
|
const nodes: SceneEntity[] = [
|
|
972
984
|
{
|
|
973
|
-
|
|
985
|
+
key: 'entity-0',
|
|
974
986
|
components: { Transform: { pos: [1.5, 0, 0] } },
|
|
975
987
|
},
|
|
976
988
|
];
|
|
@@ -996,7 +1008,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
996
1008
|
const world = new World();
|
|
997
1009
|
world.components.register(Transform).unwrap();
|
|
998
1010
|
|
|
999
|
-
const nodes: SceneEntity[] = [{
|
|
1011
|
+
const nodes: SceneEntity[] = [{ key: 'entity-0', components: { Transform: {} } }];
|
|
1000
1012
|
const handle = registerSceneAsset(world, buildScene(nodes), [Transform]);
|
|
1001
1013
|
const r = SceneOwner.worldInstantiateScene(world, handle);
|
|
1002
1014
|
expect(r.ok).toBe(true);
|
|
@@ -1398,14 +1410,14 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
1398
1410
|
expect(pod.tonemap).toBe(TONEMAP_NONE);
|
|
1399
1411
|
});
|
|
1400
1412
|
|
|
1401
|
-
it('perspective + orthographic
|
|
1413
|
+
it('perspective + orthographic 28-field counts (28 Camera columns)', () => {
|
|
1402
1414
|
const p = perspective({ fov: 60, aspect: 4 / 3 });
|
|
1403
1415
|
const o = orthographic({ left: -1, right: 1, bottom: -1, top: 1 });
|
|
1404
|
-
// Both return exactly
|
|
1416
|
+
// Both return exactly 28 fields (including exposure and color grading columns +
|
|
1405
1417
|
// autoAspect bool column; feat-20260709 M3 collapsed the 4-scalar
|
|
1406
1418
|
// clear-color quartet into one inline array<f32,4>).
|
|
1407
|
-
expect(Object.keys(p).length).toBe(
|
|
1408
|
-
expect(Object.keys(o).length).toBe(
|
|
1419
|
+
expect(Object.keys(p).length).toBe(28);
|
|
1420
|
+
expect(Object.keys(o).length).toBe(28);
|
|
1409
1421
|
});
|
|
1410
1422
|
});
|
|
1411
1423
|
|
|
@@ -1414,7 +1426,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
1414
1426
|
// ────────────────────────────────────────────────────────────────────────────
|
|
1415
1427
|
|
|
1416
1428
|
describe('Camera.fields reflection (w14 AC-07 SSOT)', () => {
|
|
1417
|
-
it('Camera.fields has exactly
|
|
1429
|
+
it('Camera.fields has exactly 28 keys matching the Camera column set', () => {
|
|
1418
1430
|
const keys = Object.keys(Camera.fields).sort();
|
|
1419
1431
|
expect(keys).toEqual([
|
|
1420
1432
|
'antialias',
|
|
@@ -1426,15 +1438,23 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
1426
1438
|
'bloomThreshold',
|
|
1427
1439
|
'bottom',
|
|
1428
1440
|
'clearColor',
|
|
1441
|
+
'colorLut',
|
|
1442
|
+
'colorLutStrength',
|
|
1443
|
+
'compensationEv',
|
|
1429
1444
|
'exposure',
|
|
1445
|
+
'exposureMode',
|
|
1430
1446
|
'far',
|
|
1431
1447
|
'fov',
|
|
1432
1448
|
'historyVersion',
|
|
1433
1449
|
'left',
|
|
1434
1450
|
'near',
|
|
1435
1451
|
'projection',
|
|
1452
|
+
'rangeEv',
|
|
1453
|
+
'rates',
|
|
1436
1454
|
'right',
|
|
1437
1455
|
'target',
|
|
1456
|
+
'temperature',
|
|
1457
|
+
'tint',
|
|
1438
1458
|
'tonemap',
|
|
1439
1459
|
'top',
|
|
1440
1460
|
'whitePoint',
|
|
@@ -1448,11 +1468,15 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
1448
1468
|
? 'bool'
|
|
1449
1469
|
: key === 'clearColor'
|
|
1450
1470
|
? 'array<f32, 4>'
|
|
1451
|
-
: key === '
|
|
1452
|
-
? '
|
|
1453
|
-
: key === '
|
|
1454
|
-
? 'shared<
|
|
1455
|
-
: '
|
|
1471
|
+
: key === 'rangeEv' || key === 'rates'
|
|
1472
|
+
? 'array<f32, 2>'
|
|
1473
|
+
: key === 'colorLut'
|
|
1474
|
+
? 'shared<TextureAsset>'
|
|
1475
|
+
: key === 'historyVersion'
|
|
1476
|
+
? 'u32'
|
|
1477
|
+
: key === 'target'
|
|
1478
|
+
? 'shared<RenderTarget>'
|
|
1479
|
+
: 'f32';
|
|
1456
1480
|
expect(Camera.fields[key].type).toBe(expected);
|
|
1457
1481
|
}
|
|
1458
1482
|
});
|
|
@@ -1487,6 +1511,14 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
1487
1511
|
expect(Array.from(d.clearColor.default as Float32Array)).toEqual([0, 0, 0, 0]);
|
|
1488
1512
|
// aspect-sync opt-out default (feat-20260617 / M3).
|
|
1489
1513
|
expect(d.autoAspect.default).toBe(true);
|
|
1514
|
+
expect(d.exposureMode.default).toBe(0);
|
|
1515
|
+
expect(d.compensationEv.default).toBe(0);
|
|
1516
|
+
expect(Array.from(d.rangeEv.default as Float32Array)).toEqual([-8, 8]);
|
|
1517
|
+
expect(Array.from(d.rates.default as Float32Array)).toEqual([3, 1]);
|
|
1518
|
+
expect(d.temperature.default).toBe(6504);
|
|
1519
|
+
expect(d.tint.default).toBe(0);
|
|
1520
|
+
expect(d.colorLut.default).toBe(0);
|
|
1521
|
+
expect(d.colorLutStrength.default).toBe(0);
|
|
1490
1522
|
// Perspective quartet defaults intentionally absent (OOS-5).
|
|
1491
1523
|
expect(d.fov.default).toBeUndefined();
|
|
1492
1524
|
expect(d.aspect.default).toBeUndefined();
|
|
@@ -1496,7 +1528,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
1496
1528
|
});
|
|
1497
1529
|
|
|
1498
1530
|
describe('componentDefinition(Camera).defaults — frozen token defaults map (AC-07 + feat-20260528-fxaa-post-processing + feat-20260531-bloom + feat-20260608-clear-color)', () => {
|
|
1499
|
-
it('componentDefinition(Camera).defaults
|
|
1531
|
+
it('componentDefinition(Camera).defaults includes exposure and color grading defaults', () => {
|
|
1500
1532
|
expect(componentDefinition(Camera).defaults).toEqual({
|
|
1501
1533
|
projection: 0,
|
|
1502
1534
|
left: -1,
|
|
@@ -1514,6 +1546,14 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
1514
1546
|
bloomBlurRadius: 4.0,
|
|
1515
1547
|
clearColor: new Float32Array([0, 0, 0, 0]),
|
|
1516
1548
|
autoAspect: true,
|
|
1549
|
+
exposureMode: 0,
|
|
1550
|
+
compensationEv: 0,
|
|
1551
|
+
rangeEv: new Float32Array([-8, 8]),
|
|
1552
|
+
rates: new Float32Array([3, 1]),
|
|
1553
|
+
temperature: 6504,
|
|
1554
|
+
tint: 0,
|
|
1555
|
+
colorLut: 0,
|
|
1556
|
+
colorLutStrength: 0,
|
|
1517
1557
|
});
|
|
1518
1558
|
});
|
|
1519
1559
|
|
|
@@ -1722,7 +1762,7 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
1722
1762
|
expect(pod.exposure).toBeCloseTo(1.0, 6);
|
|
1723
1763
|
});
|
|
1724
1764
|
|
|
1725
|
-
it('perspective return value has all
|
|
1765
|
+
it('perspective return value has all 28 Camera fields', () => {
|
|
1726
1766
|
const pod = perspective({ fov: 60, aspect: 4 / 3 });
|
|
1727
1767
|
const keys = Object.keys(pod).sort();
|
|
1728
1768
|
expect(keys).toEqual([
|
|
@@ -1735,14 +1775,22 @@ import { extractFrame, prepareExtractContext } from '../../../render/src/render-
|
|
|
1735
1775
|
'bloomThreshold',
|
|
1736
1776
|
'bottom',
|
|
1737
1777
|
'clearColor',
|
|
1778
|
+
'colorLut',
|
|
1779
|
+
'colorLutStrength',
|
|
1780
|
+
'compensationEv',
|
|
1738
1781
|
'exposure',
|
|
1782
|
+
'exposureMode',
|
|
1739
1783
|
'far',
|
|
1740
1784
|
'fov',
|
|
1741
1785
|
'historyVersion',
|
|
1742
1786
|
'left',
|
|
1743
1787
|
'near',
|
|
1744
1788
|
'projection',
|
|
1789
|
+
'rangeEv',
|
|
1790
|
+
'rates',
|
|
1745
1791
|
'right',
|
|
1792
|
+
'temperature',
|
|
1793
|
+
'tint',
|
|
1746
1794
|
'tonemap',
|
|
1747
1795
|
'top',
|
|
1748
1796
|
'whitePoint',
|
|
@@ -62,8 +62,10 @@ if (typeof window !== 'undefined') {
|
|
|
62
62
|
|
|
63
63
|
const browserReady = typeof navigator !== 'undefined' && navigator.gpu !== undefined;
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
// This gate checks world ownership and non-black presentation, not detail.
|
|
66
|
+
// Keep all composite frames while reducing software rasterization in CI.
|
|
67
|
+
const CANVAS_W = import.meta.env.FORGEAX_BROWSER_CI_LIGHTWEIGHT === '1' ? 128 : 256;
|
|
68
|
+
const CANVAS_H = CANVAS_W;
|
|
67
69
|
const FRAMES = 8;
|
|
68
70
|
|
|
69
71
|
// Build a bright, uniform equirect (all channels = 0.8 linear) so the projected
|
|
@@ -168,6 +168,7 @@ describe('w2 -- PBR dual-state dawn smoke (AC-01 / AC-06)', () => {
|
|
|
168
168
|
const { renderer, assets } = host.value;
|
|
169
169
|
expect(renderer.inspect().state).toBe('alive');
|
|
170
170
|
expect(assets).toBeInstanceOf(AssetRegistry);
|
|
171
|
+
const collectionN = renderer.instances.create({ transforms: transformsN }).unwrap();
|
|
171
172
|
|
|
172
173
|
// Material creation (PBR standard). D-18: a shared-ref handle is per-World;
|
|
173
174
|
// each frame's World allocs the same payload to get a handle it can resolve.
|
|
@@ -207,7 +208,7 @@ describe('w2 -- PBR dual-state dawn smoke (AC-01 / AC-06)', () => {
|
|
|
207
208
|
},
|
|
208
209
|
{ component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
|
|
209
210
|
{ component: MeshRenderer, data: { materials: [matHandleN] } },
|
|
210
|
-
{ component: Instances, data: {
|
|
211
|
+
{ component: Instances, data: { collectionId: collectionN.collectionId } },
|
|
211
212
|
);
|
|
212
213
|
worldN.spawn(
|
|
213
214
|
{
|
|
@@ -270,6 +271,7 @@ describe('w2 -- PBR dual-state dawn smoke (AC-01 / AC-06)', () => {
|
|
|
270
271
|
singleTransform[5] = 1;
|
|
271
272
|
singleTransform[10] = 1;
|
|
272
273
|
singleTransform[15] = 1;
|
|
274
|
+
const collection1 = renderer.instances.create({ transforms: singleTransform }).unwrap();
|
|
273
275
|
|
|
274
276
|
const world1 = new World();
|
|
275
277
|
const matHandle1 = world1.allocSharedRef<'MaterialAsset', MaterialAsset>(
|
|
@@ -287,7 +289,7 @@ describe('w2 -- PBR dual-state dawn smoke (AC-01 / AC-06)', () => {
|
|
|
287
289
|
},
|
|
288
290
|
{ component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
|
|
289
291
|
{ component: MeshRenderer, data: { materials: [matHandle1] } },
|
|
290
|
-
{ component: Instances, data: {
|
|
292
|
+
{ component: Instances, data: { collectionId: collection1.collectionId } },
|
|
291
293
|
);
|
|
292
294
|
world1.spawn(
|
|
293
295
|
{
|
|
@@ -465,6 +467,7 @@ describe('w2 -- PBR dual-state dawn smoke (AC-01 / AC-06)', () => {
|
|
|
465
467
|
} as MaterialAsset;
|
|
466
468
|
|
|
467
469
|
const transformsN2 = buildTranslationGrid();
|
|
470
|
+
const collectionN2 = renderer2.instances.create({ transforms: transformsN2 }).unwrap();
|
|
468
471
|
|
|
469
472
|
// Frame A: N instances
|
|
470
473
|
const worldN2 = new World();
|
|
@@ -483,7 +486,7 @@ describe('w2 -- PBR dual-state dawn smoke (AC-01 / AC-06)', () => {
|
|
|
483
486
|
},
|
|
484
487
|
{ component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
|
|
485
488
|
{ component: MeshRenderer, data: { materials: [matHandleN2] } },
|
|
486
|
-
{ component: Instances, data: {
|
|
489
|
+
{ component: Instances, data: { collectionId: collectionN2.collectionId } },
|
|
487
490
|
);
|
|
488
491
|
worldN2.spawn(
|
|
489
492
|
{
|
|
@@ -545,6 +548,7 @@ describe('w2 -- PBR dual-state dawn smoke (AC-01 / AC-06)', () => {
|
|
|
545
548
|
singleTransform2[5] = 1;
|
|
546
549
|
singleTransform2[10] = 1;
|
|
547
550
|
singleTransform2[15] = 1;
|
|
551
|
+
const collection12 = renderer2.instances.create({ transforms: singleTransform2 }).unwrap();
|
|
548
552
|
|
|
549
553
|
const world12 = new World();
|
|
550
554
|
const matHandle12 = world12.allocSharedRef<'MaterialAsset', MaterialAsset>(
|
|
@@ -562,7 +566,7 @@ describe('w2 -- PBR dual-state dawn smoke (AC-01 / AC-06)', () => {
|
|
|
562
566
|
},
|
|
563
567
|
{ component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
|
|
564
568
|
{ component: MeshRenderer, data: { materials: [matHandle12] } },
|
|
565
|
-
{ component: Instances, data: {
|
|
569
|
+
{ component: Instances, data: { collectionId: collection12.collectionId } },
|
|
566
570
|
);
|
|
567
571
|
world12.spawn(
|
|
568
572
|
{
|
|
@@ -284,6 +284,9 @@ describe('w10 -- shadow instanced dawn smoke (AC-05 behavioral)', () => {
|
|
|
284
284
|
const instanceTransforms = FALSIFY_COLLAPSED
|
|
285
285
|
? buildIdentityTransforms()
|
|
286
286
|
: buildTranslationGrid();
|
|
287
|
+
const instanceCollection = renderer.instances
|
|
288
|
+
.create({ transforms: instanceTransforms })
|
|
289
|
+
.unwrap();
|
|
287
290
|
world.spawn(
|
|
288
291
|
{
|
|
289
292
|
component: Transform,
|
|
@@ -295,7 +298,7 @@ describe('w10 -- shadow instanced dawn smoke (AC-05 behavioral)', () => {
|
|
|
295
298
|
},
|
|
296
299
|
{ component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
|
|
297
300
|
{ component: MeshRenderer, data: { materials: [matHandle] } },
|
|
298
|
-
{ component: Instances, data: {
|
|
301
|
+
{ component: Instances, data: { collectionId: instanceCollection.collectionId } },
|
|
299
302
|
);
|
|
300
303
|
|
|
301
304
|
// Render 300 frames for temporal stability (PCF or PCSS shadow, light accumulation)
|
|
@@ -1,10 +1,28 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// instances-uniform-fallback.dawn.test.ts -- feat-20260604-instances-per-instance-transform-shader-group3-bin
|
|
2
|
+
// M1 / w6.
|
|
3
|
+
//
|
|
4
|
+
// AC-07: storage-buffer path + uniform-fallback path both compile and render
|
|
5
|
+
// correctly for <=128 instances.
|
|
6
|
+
//
|
|
7
|
+
// Primary path: dawn-node renders <=128 instances through the normal storage
|
|
8
|
+
// buffer path (caps.storageBuffer===true on dawn) and asserts frames are
|
|
9
|
+
// non-clear (proves the storage variant renders).
|
|
10
|
+
//
|
|
11
|
+
// Degraded path (research R-C partial): dawn-node always has
|
|
12
|
+
// caps.storageBuffer===true, so we cannot force the uniform-fallback variant
|
|
13
|
+
// at the GPU level. Instead, we verify that:
|
|
14
|
+
// (a) The uniform-fallback #else form `var<uniform> instances : array<InstanceData, 128>`
|
|
15
|
+
// is present in common.wgsl (source-level structural check).
|
|
16
|
+
// (b) The variant count stays at 2 (verified by w1 unit test).
|
|
17
|
+
// (c) A non-trivial Instances render (<=128) works on the storage path.
|
|
18
|
+
//
|
|
19
|
+
// This is the best-effort degraded path per research R-C: "if dawn cannot
|
|
20
|
+
// construct caps.storageBuffer===false, fall back to a vitest unit that
|
|
21
|
+
// asserts the #else uniform array<..,128> form compiles, with reason
|
|
22
|
+
// recorded."
|
|
5
23
|
|
|
6
24
|
import type { MaterialAsset } from '@forgeax/engine-types';
|
|
7
|
-
import {
|
|
25
|
+
import { describe, expect, it } from 'vitest';
|
|
8
26
|
import { drawPublished } from '../draw-published';
|
|
9
27
|
|
|
10
28
|
// Source-level structural check: common.wgsl must carry both the storage
|
|
@@ -27,16 +45,7 @@ const STORAGE_PATTERN =
|
|
|
27
45
|
const UNIFORM_PATTERN =
|
|
28
46
|
/@group\(3\)\s+@binding\(0\)\s+var<uniform>\s+instances\s*:\s*array<InstanceData,\s*128>/;
|
|
29
47
|
|
|
30
|
-
describe('
|
|
31
|
-
let ENGINE_MANIFEST_URL_W6: string;
|
|
32
|
-
beforeAll(async () => {
|
|
33
|
-
// Shader production is fixture setup; keep the render assertion's 30s
|
|
34
|
-
// deadline independent of a cold compiler/manifest build.
|
|
35
|
-
const { buildEngineShaderManifest } = await import('@forgeax/engine-vite-plugin-shader');
|
|
36
|
-
const manifest = await buildEngineShaderManifest();
|
|
37
|
-
ENGINE_MANIFEST_URL_W6 = `data:application/json,${encodeURIComponent(JSON.stringify(manifest))}`;
|
|
38
|
-
}, 180_000);
|
|
39
|
-
|
|
48
|
+
describe('w6 -- AC-07 storage+uniform variant test (degraded best-effort)', () => {
|
|
40
49
|
it('(a) common.wgsl declares uniform-fallback array<InstanceData,128> under #else', async () => {
|
|
41
50
|
const fsId = 'node:fs';
|
|
42
51
|
const pathId = 'node:path';
|
|
@@ -79,41 +88,39 @@ describe('World Instances storage and uniform submission', () => {
|
|
|
79
88
|
expect(commonSrc).toMatch(/localFromInstance\s*:\s*mat4x4<f32>/);
|
|
80
89
|
});
|
|
81
90
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
// (b) deg-path rationale: dawn-node always has caps.storageBuffer===true, so
|
|
92
|
+
// the uniform-fallback path cannot be exercised at the GPU level in this
|
|
93
|
+
// dawn test. The uniform-fallback variant is structurally verified via:
|
|
94
|
+
// - w1 source-level variant=2 unit test (AC-11)
|
|
95
|
+
// - w6 (a) source-level uniform array<..,128> form check above
|
|
96
|
+
// - naga_oil compiles both variants at build-time (vite-plugin-shader)
|
|
97
|
+
// Restore as a real `it()` if a dawn-side uniform-fallback path becomes
|
|
98
|
+
// reachable (feat-20260608-ci-time-cut converted from `expect(true)`).
|
|
99
|
+
it.todo('(b) deg-path rationale recorded: dawn-node always storageBuffer=true');
|
|
100
|
+
|
|
101
|
+
it('(c) storage path renders Instances (dawn smoke integration)', async () => {
|
|
87
102
|
const dawnAvailable = typeof globalThis.navigator?.gpu?.requestAdapter === 'function';
|
|
88
103
|
if (!dawnAvailable) {
|
|
89
104
|
throw new Error('dawn-node navigator.gpu not injected; vitest.setup-webgpu.ts regressed');
|
|
90
105
|
}
|
|
91
106
|
|
|
107
|
+
// Render <=128 instances on the storage path and assert non-clear output.
|
|
108
|
+
// This proves the storage-buffer variant of @group(3) instances works
|
|
109
|
+
// end-to-end on dawn.
|
|
110
|
+
|
|
92
111
|
const { World } = await import('@forgeax/engine-ecs');
|
|
93
|
-
const { rhi } = await import('@forgeax/engine-rhi-webgpu');
|
|
94
|
-
const restrictedRhi: typeof rhi = {
|
|
95
|
-
...rhi,
|
|
96
|
-
async requestAdapter(...args) {
|
|
97
|
-
const adapter = await rhi.requestAdapter(...args);
|
|
98
|
-
if (!adapter.ok) return adapter;
|
|
99
|
-
const requestDevice = adapter.value.requestDevice.bind(adapter.value);
|
|
100
|
-
adapter.value.requestDevice = async (options) => {
|
|
101
|
-
const result = await requestDevice(options);
|
|
102
|
-
if (result.ok && !storage)
|
|
103
|
-
Object.defineProperty(result.value, 'caps', {
|
|
104
|
-
value: { ...result.value.caps, storageBuffer: false, compute: false },
|
|
105
|
-
});
|
|
106
|
-
return result;
|
|
107
|
-
};
|
|
108
|
-
return adapter;
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
112
|
const { AssetRegistry, HANDLE_CUBE } = await import('@forgeax/engine-assets-runtime');
|
|
112
113
|
const { Camera, MeshRenderer } = await import('@forgeax/engine-render');
|
|
113
114
|
const { Instances } = await import('@forgeax/engine-render');
|
|
114
115
|
const { constructRuntimeRendererHost } = await import('../../renderer-host');
|
|
115
116
|
const { MeshFilter } = await import('@forgeax/engine-render');
|
|
116
117
|
const { Transform } = await import('@forgeax/engine-scene');
|
|
118
|
+
const { buildEngineShaderManifest } = await import('@forgeax/engine-vite-plugin-shader');
|
|
119
|
+
const ENGINE_MANIFEST_W6 = await buildEngineShaderManifest();
|
|
120
|
+
const ENGINE_MANIFEST_URL_W6 = `data:application/json,${encodeURIComponent(
|
|
121
|
+
JSON.stringify(ENGINE_MANIFEST_W6),
|
|
122
|
+
)}`;
|
|
123
|
+
|
|
117
124
|
const W6_WIDTH = 256;
|
|
118
125
|
const W6_HEIGHT = 256;
|
|
119
126
|
|
|
@@ -125,20 +132,15 @@ describe('World Instances storage and uniform submission', () => {
|
|
|
125
132
|
const originalRequestDevice = rawAdapter.requestDevice.bind(rawAdapter);
|
|
126
133
|
rawAdapter.requestDevice = async (desc) => {
|
|
127
134
|
const dev = await originalRequestDevice(desc);
|
|
128
|
-
if (sharedDevice === undefined)
|
|
129
|
-
sharedDevice = dev;
|
|
130
|
-
dev.pushErrorScope('validation');
|
|
131
|
-
}
|
|
135
|
+
if (sharedDevice === undefined) sharedDevice = dev;
|
|
132
136
|
return dev;
|
|
133
137
|
};
|
|
134
138
|
return rawAdapter;
|
|
135
139
|
};
|
|
136
140
|
|
|
137
141
|
let renderTarget: GPUTexture | undefined;
|
|
138
|
-
let targetFormat: GPUTextureFormat = 'rgba8unorm';
|
|
139
142
|
const ensureTarget = (device: GPUDevice, format: GPUTextureFormat): GPUTexture => {
|
|
140
143
|
if (renderTarget !== undefined) return renderTarget;
|
|
141
|
-
targetFormat = format;
|
|
142
144
|
renderTarget = device.createTexture({
|
|
143
145
|
size: { width: W6_WIDTH, height: W6_HEIGHT, depthOrArrayLayers: 1 },
|
|
144
146
|
format,
|
|
@@ -175,18 +177,15 @@ describe('World Instances storage and uniform submission', () => {
|
|
|
175
177
|
try {
|
|
176
178
|
host = await constructRuntimeRendererHost(
|
|
177
179
|
mockCanvas,
|
|
178
|
-
{
|
|
180
|
+
{},
|
|
179
181
|
{ shaderManifestUrl: ENGINE_MANIFEST_URL_W6 },
|
|
180
182
|
);
|
|
181
183
|
} finally {
|
|
182
184
|
globalThis.navigator.gpu.requestAdapter = origReq;
|
|
183
185
|
}
|
|
186
|
+
expect(host.ok).toBe(true);
|
|
184
187
|
if (!host.ok) throw host.error;
|
|
185
188
|
const { renderer, assets } = host.value;
|
|
186
|
-
const errors: string[] = [];
|
|
187
|
-
renderer.subscribe((event) => {
|
|
188
|
-
if (event.kind === 'error') errors.push(JSON.stringify(event.error));
|
|
189
|
-
});
|
|
190
189
|
expect(renderer.inspect().state).toBe('alive');
|
|
191
190
|
expect(assets).toBeInstanceOf(AssetRegistry);
|
|
192
191
|
|
|
@@ -202,8 +201,9 @@ describe('World Instances storage and uniform submission', () => {
|
|
|
202
201
|
values: { baseColor: [0.9, 0.2, 0.2, 1], metallic: 0, roughness: 0.5 },
|
|
203
202
|
} as MaterialAsset;
|
|
204
203
|
|
|
205
|
-
|
|
206
|
-
const
|
|
204
|
+
// Build ~60 instances (well under 128 uniform cap), 2D grid
|
|
205
|
+
const GRID = 8;
|
|
206
|
+
const COUNT = GRID * GRID; // 64
|
|
207
207
|
const SP = 3.0;
|
|
208
208
|
const transforms = new Float32Array(COUNT * 16);
|
|
209
209
|
let idx = 0;
|
|
@@ -220,6 +220,7 @@ describe('World Instances storage and uniform submission', () => {
|
|
|
220
220
|
idx++;
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
|
+
const instanceCollection = renderer.instances.create({ transforms }).unwrap();
|
|
223
224
|
|
|
224
225
|
const world = new World();
|
|
225
226
|
const matHandle = world.allocSharedRef<'MaterialAsset', MaterialAsset>(
|
|
@@ -237,7 +238,7 @@ describe('World Instances storage and uniform submission', () => {
|
|
|
237
238
|
},
|
|
238
239
|
{ component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
|
|
239
240
|
{ component: MeshRenderer, data: { materials: [matHandle] } },
|
|
240
|
-
{ component: Instances, data: {
|
|
241
|
+
{ component: Instances, data: { collectionId: instanceCollection.collectionId } },
|
|
241
242
|
);
|
|
242
243
|
world.spawn(
|
|
243
244
|
{
|
|
@@ -295,45 +296,18 @@ describe('World Instances storage and uniform submission', () => {
|
|
|
295
296
|
readbackBuf.unmap();
|
|
296
297
|
readbackBuf.destroy();
|
|
297
298
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
expect(redPixels).toBeGreaterThan(COUNT * 2);
|
|
314
|
-
// The last row crosses the 128-instance boundary for the 12x12 case.
|
|
315
|
-
// Verify its last matrix reaches a distinct screen location, not a
|
|
316
|
-
// duplicate of the first chunk or merely some non-clear geometry.
|
|
317
|
-
const edge = ((GRID - 1) / 2) * SP * 0.3;
|
|
318
|
-
const ndcEdge = edge / (25 * Math.tan(Math.PI / 8));
|
|
319
|
-
const lastX = Math.round(((1 + ndcEdge) * W6_WIDTH) / 2);
|
|
320
|
-
const lastY = Math.round(((1 - ndcEdge) * W6_HEIGHT) / 2);
|
|
321
|
-
let lastInstancePixels = 0;
|
|
322
|
-
for (let y = lastY - 3; y <= lastY + 3; y++) {
|
|
323
|
-
for (let x = lastX - 3; x <= lastX + 3; x++) {
|
|
324
|
-
const offset = y * bytesPerRow + x * 4;
|
|
325
|
-
const r = bytes[offset + (targetFormat.startsWith('bgra') ? 2 : 0)] ?? 0;
|
|
326
|
-
const g = bytes[offset + 1] ?? 0;
|
|
327
|
-
const b = bytes[offset + (targetFormat.startsWith('bgra') ? 0 : 2)] ?? 0;
|
|
328
|
-
if (r > g + 30 && r > b + 30) lastInstancePixels++;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
expect(lastInstancePixels).toBeGreaterThan(0);
|
|
332
|
-
if (!storage) {
|
|
333
|
-
const residency = renderer.inspect().instanceCollections[0];
|
|
334
|
-
expect(residency?.lane).toBe(COUNT > 128 ? 'chunked-uniform' : 'direct-uniform');
|
|
335
|
-
expect(residency?.uploadedBytes).toBe(0);
|
|
336
|
-
}
|
|
337
|
-
await renderer.dispose();
|
|
299
|
+
// Sample multiple positions — the 8x8 grid should fill a substantial
|
|
300
|
+
// portion of the frame. Center should NOT be clear-color.
|
|
301
|
+
const cx = W6_WIDTH >> 1;
|
|
302
|
+
const cy = W6_HEIGHT >> 1;
|
|
303
|
+
const readPixel = (px: number, py: number): [number, number, number] => {
|
|
304
|
+
const off = py * bytesPerRow + px * 4;
|
|
305
|
+
return [bytes[off + 2] ?? 0, bytes[off + 1] ?? 0, bytes[off + 0] ?? 0];
|
|
306
|
+
};
|
|
307
|
+
const [cr, cg, cb] = readPixel(cx, cy);
|
|
308
|
+
// Clear color is ~(13, 13, 20) in sRGB bytes. Center should be
|
|
309
|
+
// significantly different (red cubes).
|
|
310
|
+
const distFromClear = Math.sqrt((cr - 13) ** 2 + (cg - 13) ** 2 + (cb - 20) ** 2);
|
|
311
|
+
expect(distFromClear).toBeGreaterThan(30);
|
|
338
312
|
});
|
|
339
313
|
});
|