@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
|
@@ -573,6 +573,7 @@ type __MergedKeep =
|
|
|
573
573
|
|
|
574
574
|
interface IntegrationComponents {
|
|
575
575
|
Transform: unknown;
|
|
576
|
+
GlobalTransform: unknown;
|
|
576
577
|
MeshFilter: unknown;
|
|
577
578
|
MeshRenderer: unknown;
|
|
578
579
|
Camera: unknown;
|
|
@@ -642,11 +643,15 @@ type __MergedKeep =
|
|
|
642
643
|
}
|
|
643
644
|
|
|
644
645
|
function spawnInstancedCube(
|
|
646
|
+
renderer: IntegrationRenderer,
|
|
645
647
|
world: IntegrationWorld,
|
|
646
648
|
C: IntegrationComponents,
|
|
647
649
|
instanceCount: number,
|
|
648
|
-
): unknown {
|
|
649
|
-
|
|
650
|
+
): { entity: unknown; collectionId: number } {
|
|
651
|
+
const collectionId = renderer.instances
|
|
652
|
+
.create({ transforms: new Float32Array(instanceCount * 16) })
|
|
653
|
+
.unwrap().collectionId as number;
|
|
654
|
+
const entity = world
|
|
650
655
|
.spawn(
|
|
651
656
|
{ component: C.MeshFilter, data: { assetHandle: C.HANDLE_CUBE } },
|
|
652
657
|
{ component: C.MeshRenderer, data: {} },
|
|
@@ -658,9 +663,11 @@ type __MergedKeep =
|
|
|
658
663
|
scale: [1, 1, 1],
|
|
659
664
|
},
|
|
660
665
|
},
|
|
661
|
-
{ component: C.
|
|
666
|
+
{ component: C.GlobalTransform, data: {} },
|
|
667
|
+
{ component: C.Instances, data: { collectionId } },
|
|
662
668
|
)
|
|
663
669
|
.unwrap();
|
|
670
|
+
return { entity, collectionId };
|
|
664
671
|
}
|
|
665
672
|
|
|
666
673
|
// ── w14: AC-07 B-family error strategy ──
|
|
@@ -746,7 +753,7 @@ type __MergedKeep =
|
|
|
746
753
|
const { renderer, world, C, errors } = await bootIntegrationRenderer(device);
|
|
747
754
|
|
|
748
755
|
spawnCamera(world, C);
|
|
749
|
-
const cube = spawnInstancedCube(world, C, 2);
|
|
756
|
+
const cube = spawnInstancedCube(renderer, world, C, 2);
|
|
750
757
|
|
|
751
758
|
// Frame 1: allocate the instance buffer (fingerprint = 2 instances).
|
|
752
759
|
drawPublished(renderer, world as WorldType);
|
|
@@ -754,7 +761,7 @@ type __MergedKeep =
|
|
|
754
761
|
// Frame 2: fingerprint mismatch -> F12 destroys the old buffer, whose
|
|
755
762
|
// raw .destroy() throws -> rhi-webgpu webgpu-runtime-error -> the F12
|
|
756
763
|
// production fires errorRegistry and continues to set the new buffer.
|
|
757
|
-
|
|
764
|
+
renderer.instances.replace(cube.collectionId as never, new Float32Array(3 * 16));
|
|
758
765
|
drawPublished(renderer, world as WorldType);
|
|
759
766
|
|
|
760
767
|
// Sweep continued: a fresh (larger) instance buffer was still allocated
|
|
@@ -777,7 +784,7 @@ type __MergedKeep =
|
|
|
777
784
|
const { renderer, world, C, errors } = await bootIntegrationRenderer(device);
|
|
778
785
|
|
|
779
786
|
spawnCamera(world, C);
|
|
780
|
-
const cube = spawnInstancedCube(world, C, 2);
|
|
787
|
+
const cube = spawnInstancedCube(renderer, world, C, 2);
|
|
781
788
|
|
|
782
789
|
// Frame 1: cold allocate the 2-instance buffer (256 B).
|
|
783
790
|
drawPublished(renderer, world as WorldType);
|
|
@@ -792,7 +799,7 @@ type __MergedKeep =
|
|
|
792
799
|
// 256 B buffer, then sets a fresh 384 B one. No shadow-caster shader is
|
|
793
800
|
// registered, so recordShadowPass early-exits and the main pass is the
|
|
794
801
|
// sole F12 owner this frame.
|
|
795
|
-
|
|
802
|
+
renderer.instances.replace(cube.collectionId as never, new Float32Array(3 * 16));
|
|
796
803
|
drawPublished(renderer, world as WorldType);
|
|
797
804
|
|
|
798
805
|
// The destroyed buffer is specifically the old instance buffer (STORAGE
|
|
@@ -826,13 +833,13 @@ type __MergedKeep =
|
|
|
826
833
|
mapSize: 1024,
|
|
827
834
|
},
|
|
828
835
|
});
|
|
829
|
-
const cube = spawnInstancedCube(world, C, 2);
|
|
836
|
+
const cube = spawnInstancedCube(renderer, world, C, 2);
|
|
830
837
|
|
|
831
838
|
drawPublished(renderer, world as WorldType);
|
|
832
839
|
expect(log.destroyed).toHaveLength(0);
|
|
833
840
|
const oldInstanceBuffer = latestInstanceBuffer(log, 2);
|
|
834
841
|
|
|
835
|
-
|
|
842
|
+
renderer.instances.replace(cube.collectionId as never, new Float32Array(3 * 16));
|
|
836
843
|
drawPublished(renderer, world as WorldType);
|
|
837
844
|
|
|
838
845
|
// The shadow pass records the instance entity before the main pass, so
|
|
@@ -87,6 +87,15 @@ it.each([
|
|
|
87
87
|
requestDevice: async () => ok(++requests === 1 ? first : replacement),
|
|
88
88
|
} as never),
|
|
89
89
|
} as never;
|
|
90
|
+
const a = requireValue(
|
|
91
|
+
await constructRendererHost(canvas(), { rhi }, { shaderManifestUrl: manifest }),
|
|
92
|
+
).renderer;
|
|
93
|
+
const b = requireValue(
|
|
94
|
+
await constructRendererHost(canvas(), { rhi: recoverable }, { shaderManifestUrl: manifest }),
|
|
95
|
+
).renderer;
|
|
96
|
+
const collectionA = requireValue(a.instances.create({ transforms: matrices(0, count) }));
|
|
97
|
+
const collectionB = requireValue(b.instances.create({ transforms: matrices(0, count) }));
|
|
98
|
+
expect(collectionA.collectionId).toBe(collectionB.collectionId);
|
|
90
99
|
const world = new World();
|
|
91
100
|
const material = world.allocSharedRef('MaterialAsset', {
|
|
92
101
|
kind: 'material',
|
|
@@ -99,12 +108,12 @@ it.each([
|
|
|
99
108
|
],
|
|
100
109
|
values: { baseColor: [1, 0, 0] },
|
|
101
110
|
});
|
|
102
|
-
|
|
111
|
+
world
|
|
103
112
|
.spawn(
|
|
104
113
|
{ component: Transform, data: {} },
|
|
105
114
|
{ component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
|
|
106
115
|
{ component: MeshRenderer, data: { materials: [material] } },
|
|
107
|
-
{ component: Instances, data: {
|
|
116
|
+
{ component: Instances, data: { collectionId: collectionA.collectionId } },
|
|
108
117
|
)
|
|
109
118
|
.unwrap();
|
|
110
119
|
world
|
|
@@ -113,12 +122,6 @@ it.each([
|
|
|
113
122
|
{ component: Camera, data: { fov: Math.PI / 4, aspect: 1, near: 0.1, far: 100 } },
|
|
114
123
|
)
|
|
115
124
|
.unwrap();
|
|
116
|
-
const a = requireValue(
|
|
117
|
-
await constructRendererHost(canvas(), { rhi }, { shaderManifestUrl: manifest }),
|
|
118
|
-
).renderer;
|
|
119
|
-
const b = requireValue(
|
|
120
|
-
await constructRendererHost(canvas(), { rhi: recoverable }, { shaderManifestUrl: manifest }),
|
|
121
|
-
).renderer;
|
|
122
125
|
const la = requireValue(a.attach(world));
|
|
123
126
|
const lb = requireValue(b.attach(world));
|
|
124
127
|
const ra = { leases: [la], camera: { lease: la }, environment: { lease: la } };
|
|
@@ -132,12 +135,14 @@ it.each([
|
|
|
132
135
|
requireValue(b.draw(rb));
|
|
133
136
|
expect(b.inspect().instanceCollections[0]?.uploadedBytes).toBe(0);
|
|
134
137
|
const firstRevision = b.inspect().instanceCollections[0]?.revision;
|
|
135
|
-
|
|
138
|
+
requireValue(a.instances.replace(collectionA.collectionId, matrices(0.25, count)));
|
|
139
|
+
requireValue(b.instances.replace(collectionB.collectionId, matrices(0.25, count)));
|
|
136
140
|
requireValue(a.draw(ra));
|
|
137
|
-
|
|
141
|
+
requireValue(a.instances.replace(collectionA.collectionId, matrices(0.5, count)));
|
|
142
|
+
requireValue(b.instances.replace(collectionB.collectionId, matrices(0.5, count)));
|
|
138
143
|
requireValue(a.draw(ra));
|
|
139
144
|
await a.dispose();
|
|
140
|
-
expect(
|
|
145
|
+
expect(b.instances.snapshot(collectionB.collectionId).unwrap().transforms[12]).toBe(0.5);
|
|
141
146
|
requireValue(b.draw(rb));
|
|
142
147
|
expect(b.inspect().instanceCollections[0]?.revision).toBeGreaterThan(firstRevision ?? 0);
|
|
143
148
|
lose({ reason: 'unknown', message: 'instance recovery regression' });
|
|
@@ -145,17 +150,18 @@ it.each([
|
|
|
145
150
|
await Promise.resolve();
|
|
146
151
|
expect(b.state()).toBe('device-lost');
|
|
147
152
|
const finalX = count === 129 ? 0.75 : 0.5;
|
|
148
|
-
if (count === 129)
|
|
153
|
+
if (count === 129)
|
|
154
|
+
requireValue(b.instances.replace(collectionB.collectionId, matrices(finalX, count)));
|
|
149
155
|
const recovered = await b.recover();
|
|
150
156
|
expect(recovered.ok, recovered.ok ? '' : JSON.stringify(recovered.error)).toBe(true);
|
|
151
157
|
requireValue(b.draw(rb));
|
|
152
|
-
expect(b.inspect().instanceCollections[0]?.uploadedBytes).
|
|
158
|
+
expect(b.inspect().instanceCollections[0]?.uploadedBytes).toBeGreaterThanOrEqual(count * 64);
|
|
153
159
|
expect(b.inspect().recoveryEvidence.submissions.lastGeneration).toBe(1);
|
|
154
160
|
expect(b.inspect().frame.deviceGeneration).toBe(1);
|
|
155
161
|
// The GPU-driven primary lane owns no legacy instance buffer; its published
|
|
156
162
|
// device generation is evidenced by the submitted frame above.
|
|
157
163
|
if (count < 20000) expect(b.inspect().instanceCollections[0]?.residentGeneration).toBe(1);
|
|
158
|
-
const retained =
|
|
164
|
+
const retained = b.instances.snapshot(collectionB.collectionId).unwrap().transforms;
|
|
159
165
|
expect(retained.length).toBe(count * 16);
|
|
160
166
|
expect(retained[12]).toBe(finalX);
|
|
161
167
|
expect(retained[(count - 1) * 16 + 12]).toBe(finalX);
|
|
@@ -1,61 +1,22 @@
|
|
|
1
|
-
//
|
|
2
|
-
// ECS component (feat-20260514-ecs-children-instances-managed-buffer-array
|
|
3
|
-
// M3 / w14).
|
|
4
|
-
//
|
|
5
|
-
// Pairs with components/instances.ts (w14) which migrates the schema from
|
|
6
|
-
// the legacy `{ buffer: 'ref', count: 'u32' }` pair (cross-coupled with the
|
|
7
|
-
// retired `AssetRegistry.createInstancedBuffer` pipeline + the deleted
|
|
8
|
-
// `InstancedBufferAsset` POD) to the ECS-managed
|
|
9
|
-
// `{ transforms: 'array<f32>' }` path with `arrayStride: { transforms: 16 }`.
|
|
10
|
-
//
|
|
11
|
-
// The type-d coverage now locks the AC-06 spawn shape:
|
|
12
|
-
// 1. `InstancesData.transforms` narrows to `Float32Array` (no number / no
|
|
13
|
-
// Handle brand on the spawn surface);
|
|
14
|
-
// 2. omitting `transforms` from `InstancesData` is a TS error;
|
|
15
|
-
// 3. assigning a non-Float32Array (plain `number[]` or `Uint32Array`) to
|
|
16
|
-
// `InstancesData.transforms` is a TS error.
|
|
17
|
-
//
|
|
18
|
-
// The legacy three @ts-expect-error block (cross-brand `Handle<MeshAsset>` /
|
|
19
|
-
// untagged `number` / missing `count`) is retired alongside the
|
|
20
|
-
// `Handle<InstancedBufferAsset>` brand — the spawn shape no longer carries
|
|
21
|
-
// a handle field at all (charter "Optimal > compatible": new shape replaces
|
|
22
|
-
// the old one in the same PR; migration registry row in AGENTS.md).
|
|
23
|
-
//
|
|
24
|
-
// File location note: project convention places runtime tests under
|
|
25
|
-
// `src/__tests__/` (TS rootDir = `./src`); the plan-tasks.json target
|
|
26
|
-
// path matches this layout.
|
|
1
|
+
// Type-level contract for the renderer-owned large Instances association.
|
|
27
2
|
|
|
28
|
-
import type { InstancesData } from '@forgeax/engine-render';
|
|
3
|
+
import type { InstanceCollectionId, InstancesData } from '@forgeax/engine-render';
|
|
29
4
|
import { describe, expectTypeOf, it } from 'vitest';
|
|
30
5
|
|
|
31
|
-
describe('InstancesData
|
|
32
|
-
it('
|
|
6
|
+
describe('InstancesData renderer-owned collection shape', () => {
|
|
7
|
+
it('carries exactly one branded collection identity', () => {
|
|
33
8
|
expectTypeOf<InstancesData>().toEqualTypeOf<{
|
|
34
|
-
readonly
|
|
9
|
+
readonly collectionId: InstanceCollectionId;
|
|
35
10
|
}>();
|
|
11
|
+
expectTypeOf<InstancesData['collectionId']>().toEqualTypeOf<InstanceCollectionId>();
|
|
36
12
|
});
|
|
37
13
|
|
|
38
|
-
it('
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// @ts-expect-error `transforms` is required; partial shape must not type-check.
|
|
46
|
-
const wrong: InstancesData = {};
|
|
47
|
-
void wrong;
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('2. plain `number[]` is not assignable to InstancesData.transforms (must be Float32Array)', () => {
|
|
51
|
-
// @ts-expect-error `number[]` lacks the Float32Array brand.
|
|
52
|
-
const wrong: InstancesData = { transforms: [0, 0, 0, 0] };
|
|
53
|
-
void wrong;
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('3. `Uint32Array` is not assignable to InstancesData.transforms (Float32Array nominal)', () => {
|
|
57
|
-
// @ts-expect-error TypedArray nominal types do not cross-assign in TS.
|
|
58
|
-
const wrong: InstancesData = { transforms: new Uint32Array(16) };
|
|
59
|
-
void wrong;
|
|
14
|
+
it('rejects an omitted association and an unbranded number', () => {
|
|
15
|
+
// @ts-expect-error collectionId is required.
|
|
16
|
+
const missing: InstancesData = {};
|
|
17
|
+
void missing;
|
|
18
|
+
// @ts-expect-error collection ids are minted by renderer.instances.create().
|
|
19
|
+
const unbranded: InstancesData = { collectionId: 1 };
|
|
20
|
+
void unbranded;
|
|
60
21
|
});
|
|
61
22
|
});
|
|
@@ -1,50 +1,19 @@
|
|
|
1
|
-
// instantiate-mount-deferred-wiring.test.ts — D-8 red-first test for the
|
|
2
|
-
// pre-existing structural hole that mount-collapse's serialized-Children
|
|
3
|
-
// dependency was masking (feat-20260707-engine-world-clone-transient-for-
|
|
4
|
-
// editor-ssot, plan-strategy D-8 / §3 static chain).
|
|
5
|
-
//
|
|
6
|
-
// The hole: _instantiateSceneAsset expands mounts[] FIRST (world.ts:3672-3676)
|
|
7
|
-
// and spawns owned entities SECOND (world.ts:3768-3790). When a mount has
|
|
8
|
-
// `mount.parent` pointing at an OWNED entity slot, that slot is still
|
|
9
|
-
// ENTITY_NULL_RAW at mount-processing time, so the ChildOf wiring guard
|
|
10
|
-
// (world.ts:3740-3757) silently skips — and there is NO later pass that
|
|
11
|
-
// re-attempts the wire. The mount carrier is therefore left WITHOUT a ChildOf
|
|
12
|
-
// edge to its owned parent.
|
|
13
|
-
//
|
|
14
|
-
// Before M1, the serialized `Children` array on the owned parent was written
|
|
15
|
-
// back on reload and re-established the W -> carrier down-link (world.ts:
|
|
16
|
-
// 3975-3987 array-of-entity remap), so collect's BFS could still reach the
|
|
17
|
-
// anchor. M1 declared `Children` transient (no longer serialized), unmasking
|
|
18
|
-
// the missing edge. This test asserts the edge directly, independent of
|
|
19
|
-
// serialized Children — so it FAILS on main too (proving the hole is
|
|
20
|
-
// PRE-EXISTING, not introduced by M1). m2t4's deferred wiring turns it green.
|
|
21
|
-
|
|
22
|
-
import type { Asset } from '@forgeax/engine-assets-runtime';
|
|
23
1
|
import { AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
24
2
|
import { type EntityHandle, World } from '@forgeax/engine-ecs';
|
|
25
3
|
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
26
4
|
import { SceneInstance } from '@forgeax/engine-render';
|
|
27
5
|
import { ChildOf, Children, Transform } from '@forgeax/engine-scene';
|
|
28
|
-
import type { Handle, SceneAsset } from '@forgeax/engine-types';
|
|
6
|
+
import type { Asset, Handle, SceneAsset } from '@forgeax/engine-types';
|
|
29
7
|
import { describe, expect, it } from 'vitest';
|
|
30
8
|
import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
31
9
|
|
|
32
10
|
const G_CHILD = '11111111-1111-4111-8111-111111111111';
|
|
33
11
|
const G_PARENT = '22222222-2222-4222-8222-222222222222';
|
|
34
12
|
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const r = AssetGuid.parse(s);
|
|
40
|
-
if (!r.ok) throw new Error(`bad GUID: ${s}`);
|
|
41
|
-
return r.value;
|
|
42
|
-
}
|
|
43
|
-
function cat(reg: AssetRegistry, g: string, p: SceneAsset): void {
|
|
44
|
-
reg.catalog(pg(g), p as Asset);
|
|
45
|
-
}
|
|
46
|
-
function rs(w: World, a: SceneAsset): Handle<'SceneAsset', 'shared'> {
|
|
47
|
-
return w.allocSharedRef('SceneAsset', a);
|
|
13
|
+
function guid(value: string): AssetGuid {
|
|
14
|
+
const parsed = AssetGuid.parse(value);
|
|
15
|
+
if (!parsed.ok) throw new Error(`bad GUID: ${value}`);
|
|
16
|
+
return parsed.value;
|
|
48
17
|
}
|
|
49
18
|
|
|
50
19
|
function makeWorld(): World {
|
|
@@ -55,58 +24,51 @@ function makeWorld(): World {
|
|
|
55
24
|
return world;
|
|
56
25
|
}
|
|
57
26
|
|
|
58
|
-
describe('instantiate
|
|
59
|
-
it('
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
// Child scene = one owned entity.
|
|
27
|
+
describe('instantiate keyed nested instance wiring', () => {
|
|
28
|
+
it('attaches a nested SceneInstance root under the keyed instance entity', () => {
|
|
29
|
+
const registry = new AssetRegistry(makeMockShaderRegistry());
|
|
30
|
+
const world = makeWorld();
|
|
64
31
|
const child: SceneAsset = {
|
|
65
32
|
kind: 'scene',
|
|
66
|
-
entities:
|
|
33
|
+
entities: { childRoot: { components: { Transform: { pos: [7, 0, 0] } } } },
|
|
67
34
|
};
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// Parent scene: one OWNED entity W (localId 0) + one mount whose parent
|
|
71
|
-
// points at W's slot (localId 0). This is the editor's
|
|
72
|
-
// spawnGlbSceneAsMount on-disk shape (wrapper W + nested mount).
|
|
35
|
+
registry.catalog(guid(G_CHILD), child as Asset);
|
|
73
36
|
const parent: SceneAsset = {
|
|
74
37
|
kind: 'scene',
|
|
75
|
-
entities:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
source: G_CHILD,
|
|
80
|
-
memberFirst: 2 as never,
|
|
81
|
-
memberCount: 1,
|
|
82
|
-
parent: 0 as never,
|
|
38
|
+
entities: {
|
|
39
|
+
wrapper: {
|
|
40
|
+
components: { Transform: { pos: [1, 0, 0] } },
|
|
41
|
+
instance: { source: G_CHILD },
|
|
83
42
|
},
|
|
84
|
-
|
|
43
|
+
},
|
|
85
44
|
};
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const inst = reg.instantiate(rs(w, parent), w);
|
|
89
|
-
expect(inst.ok).toBe(true);
|
|
90
|
-
if (!inst.ok) return;
|
|
91
|
-
|
|
92
|
-
// The synthetic root carries SceneInstance; mapping[localId] -> live Entity.
|
|
93
|
-
const si = w.get(inst.value, SceneInstance);
|
|
94
|
-
expect(si.ok).toBe(true);
|
|
95
|
-
if (!si.ok) return;
|
|
96
|
-
const mapping = si.value.mapping;
|
|
45
|
+
registry.catalog(guid(G_PARENT), parent as Asset);
|
|
97
46
|
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
47
|
+
const result = registry.instantiate(
|
|
48
|
+
world.allocSharedRef('SceneAsset', parent) as Handle<'SceneAsset', 'shared'>,
|
|
49
|
+
world,
|
|
50
|
+
);
|
|
51
|
+
expect(result.ok).toBe(true);
|
|
52
|
+
if (!result.ok) return;
|
|
53
|
+
const outer = world.get(result.value, SceneInstance);
|
|
54
|
+
expect(outer.ok).toBe(true);
|
|
55
|
+
if (!outer.ok) return;
|
|
56
|
+
const wrapper = outer.value.mapping[0] as unknown as EntityHandle;
|
|
57
|
+
expect(wrapper).toBeDefined();
|
|
102
58
|
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
59
|
+
let nestedRoot: EntityHandle | undefined;
|
|
60
|
+
for (const entity of world.iterDescendants(result.value)) {
|
|
61
|
+
if (entity === result.value) continue;
|
|
62
|
+
if (world.get(entity, SceneInstance).ok) {
|
|
63
|
+
nestedRoot = entity;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
expect(nestedRoot).toBeDefined();
|
|
68
|
+
if (nestedRoot === undefined) return;
|
|
69
|
+
const childOf = world.get(nestedRoot, ChildOf);
|
|
70
|
+
expect(childOf.ok).toBe(true);
|
|
71
|
+
if (!childOf.ok) return;
|
|
72
|
+
expect(childOf.value.parent).toBe(wrapper);
|
|
111
73
|
});
|
|
112
74
|
});
|
|
@@ -602,6 +602,18 @@ type __MergedKeep =
|
|
|
602
602
|
fileURLToPath(new URL('../../../render/src/assembly/webgpu-renderer.ts', import.meta.url)),
|
|
603
603
|
'utf-8',
|
|
604
604
|
);
|
|
605
|
+
const rendererBootstrapSrc = readFileSync(
|
|
606
|
+
fileURLToPath(
|
|
607
|
+
new URL('../../../render/src/assembly/webgpu-renderer-bootstrap.ts', import.meta.url),
|
|
608
|
+
),
|
|
609
|
+
'utf-8',
|
|
610
|
+
);
|
|
611
|
+
const rendererInternalsSrc = readFileSync(
|
|
612
|
+
fileURLToPath(
|
|
613
|
+
new URL('../../../render/src/assembly/webgpu-renderer-contract.ts', import.meta.url),
|
|
614
|
+
),
|
|
615
|
+
'utf-8',
|
|
616
|
+
);
|
|
605
617
|
const rendererTypeSrc = readFileSync(
|
|
606
618
|
fileURLToPath(new URL('../../../render/src/render-contract.ts', import.meta.url)),
|
|
607
619
|
'utf-8',
|
|
@@ -626,9 +638,11 @@ type __MergedKeep =
|
|
|
626
638
|
expect(createRendererSrc).toMatch(
|
|
627
639
|
/new AssetRegistry\(\s*shaderCatalog,\s*internals\.importTransport\s*,\s*\[audioLoader\]\s*,\s*postSpawnResolveJoints/,
|
|
628
640
|
);
|
|
629
|
-
// The
|
|
630
|
-
//
|
|
631
|
-
|
|
641
|
+
// The renderer implementation and backend bootstrap are split owners:
|
|
642
|
+
// WebGPURendererInternals carries the dedicated non-RendererOptions
|
|
643
|
+
// channel, and the bootstrap derives it from BundlerOptions.
|
|
644
|
+
expect(rendererInternalsSrc).toMatch(/importTransport\?\s*:\s*ImportTransport/);
|
|
645
|
+
expect(rendererBootstrapSrc).toMatch(/const importTransport = bundler\?\.importTransport/);
|
|
632
646
|
});
|
|
633
647
|
|
|
634
648
|
it('(AC-05) RendererOptions gains no asset-layer transport field', () => {
|
|
@@ -519,14 +519,14 @@ type __MergedKeep =
|
|
|
519
519
|
});
|
|
520
520
|
|
|
521
521
|
it('sceneLoader produces a SceneAsset for a valid payload', () => {
|
|
522
|
-
const out = sceneLoaderLoad({ entities:
|
|
522
|
+
const out = sceneLoaderLoad({ entities: { 'entity-0': { components: {} } } }, undefined);
|
|
523
523
|
expect(out).toMatchObject({ kind: 'scene' });
|
|
524
524
|
});
|
|
525
525
|
|
|
526
526
|
it('sceneLoader returns structured error { ok: false } on refs out-of-bounds (F21)', () => {
|
|
527
527
|
const ctx = mockCtx();
|
|
528
528
|
const out = sceneLoader.load(
|
|
529
|
-
{ entities:
|
|
529
|
+
{ entities: { 'entity-7': { components: { MeshFilter: { assetHandle: 5 } } } } },
|
|
530
530
|
['guid-a'], // length 1; index 5 is out of bounds
|
|
531
531
|
ctx,
|
|
532
532
|
);
|
|
@@ -535,10 +535,10 @@ type __MergedKeep =
|
|
|
535
535
|
// instead of writing to ctx.reportParseError.
|
|
536
536
|
const errResult = out as {
|
|
537
537
|
ok: boolean;
|
|
538
|
-
error?: {
|
|
538
|
+
error?: { entityKey: string; index: number; refsLength: number };
|
|
539
539
|
};
|
|
540
540
|
expect(errResult.ok).toBe(false);
|
|
541
|
-
expect(errResult.error).toMatchObject({
|
|
541
|
+
expect(errResult.error).toMatchObject({ entityKey: 'entity-7', index: 5, refsLength: 1 });
|
|
542
542
|
});
|
|
543
543
|
|
|
544
544
|
it('materialLoader carries parentGuid resolved from refs index', () => {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RuntimeAssetBinding } from '@forgeax/engine-types';
|
|
2
|
+
import { it } from 'vitest';
|
|
3
|
+
import { commands } from 'vitest/browser';
|
|
4
|
+
import { verifyMaterialPublication } from './material-publication.fixture';
|
|
5
|
+
|
|
6
|
+
declare module 'vitest/browser' {
|
|
7
|
+
interface BrowserCommands {
|
|
8
|
+
startMaterialPublication(): Promise<{
|
|
9
|
+
id: string;
|
|
10
|
+
binding: RuntimeAssetBinding;
|
|
11
|
+
guids: string[];
|
|
12
|
+
}>;
|
|
13
|
+
updateMaterialPublication(id: string, value: 'red' | 'blue' | 'broken'): Promise<void>;
|
|
14
|
+
stopMaterialPublication(id: string): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
it('loads and rebuilds independent material programs through real Vite HTTP on browser WebGPU', async () => {
|
|
19
|
+
const server = await commands.startMaterialPublication();
|
|
20
|
+
try {
|
|
21
|
+
await verifyMaterialPublication({
|
|
22
|
+
...server,
|
|
23
|
+
shaderManifestUrl: '/shaders/manifest.json',
|
|
24
|
+
update: (value) => commands.updateMaterialPublication(server.id, value),
|
|
25
|
+
});
|
|
26
|
+
} finally {
|
|
27
|
+
await commands.stopMaterialPublication(server.id);
|
|
28
|
+
}
|
|
29
|
+
}, 120_000);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { startMaterialPublicationServer } from './material-publication.server';
|
|
2
|
+
|
|
3
|
+
const servers = new Map<string, Awaited<ReturnType<typeof startMaterialPublicationServer>>>();
|
|
4
|
+
|
|
5
|
+
export const materialPublicationCommands = {
|
|
6
|
+
async startMaterialPublication(_context: unknown) {
|
|
7
|
+
const server = await startMaterialPublicationServer();
|
|
8
|
+
const id = server.binding.catalogUrl;
|
|
9
|
+
servers.set(id, server);
|
|
10
|
+
return { id, binding: server.binding, guids: server.guids };
|
|
11
|
+
},
|
|
12
|
+
async updateMaterialPublication(_context: unknown, id: string, value: 'red' | 'blue' | 'broken') {
|
|
13
|
+
const server = servers.get(id);
|
|
14
|
+
if (server === undefined) throw new Error('unknown material publication test host');
|
|
15
|
+
await server.update(value);
|
|
16
|
+
},
|
|
17
|
+
async stopMaterialPublication(_context: unknown, id: string) {
|
|
18
|
+
const server = servers.get(id);
|
|
19
|
+
if (server === undefined) return;
|
|
20
|
+
servers.delete(id);
|
|
21
|
+
await server.close();
|
|
22
|
+
},
|
|
23
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { buildEngineShaderManifest } from '@forgeax/engine-vite-plugin-shader';
|
|
3
|
+
import { it } from 'vitest';
|
|
4
|
+
import { verifyMaterialPublication } from './material-publication.fixture';
|
|
5
|
+
import { startMaterialPublicationServer } from './material-publication.server';
|
|
6
|
+
|
|
7
|
+
it('loads independent material programs through real Vite transport and rebuilds raw WGSL on Dawn', async () => {
|
|
8
|
+
const server = await startMaterialPublicationServer();
|
|
9
|
+
try {
|
|
10
|
+
const manifest = await buildEngineShaderManifest();
|
|
11
|
+
await verifyMaterialPublication({
|
|
12
|
+
...server,
|
|
13
|
+
shaderManifestUrl: `data:application/json,${encodeURIComponent(JSON.stringify(manifest))}`,
|
|
14
|
+
...(process.env.FORGEAX_MATERIAL_TAPE === undefined
|
|
15
|
+
? {}
|
|
16
|
+
: {
|
|
17
|
+
saveTape: (bytes: Uint8Array) =>
|
|
18
|
+
writeFileSync(process.env.FORGEAX_MATERIAL_TAPE as string, bytes),
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
} finally {
|
|
22
|
+
await server.close();
|
|
23
|
+
}
|
|
24
|
+
}, 120_000);
|