@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
|
@@ -16,7 +16,7 @@ import { AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
|
16
16
|
import { type Component, defineComponent, type EntityHandle, World } from '@forgeax/engine-ecs';
|
|
17
17
|
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
18
18
|
import { SceneInstance } from '@forgeax/engine-render';
|
|
19
|
-
import type {
|
|
19
|
+
import type { SceneAsset } from '@forgeax/engine-types';
|
|
20
20
|
import { describe, expect, it } from 'vitest';
|
|
21
21
|
import { rootsToSceneAsset, serializeSceneAssetToPack } from '../collect-scene-asset';
|
|
22
22
|
import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
@@ -26,10 +26,6 @@ import { registerSceneComponents } from './helpers/register-scene-components';
|
|
|
26
26
|
// Helpers
|
|
27
27
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
28
28
|
|
|
29
|
-
function localId(n: number): LocalEntityId {
|
|
30
|
-
return n as LocalEntityId;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
29
|
function mkReg(): AssetRegistry {
|
|
34
30
|
return new AssetRegistry(makeMockShaderRegistry());
|
|
35
31
|
}
|
|
@@ -59,10 +55,14 @@ function findMountedMember(w: World, rootA: EntityHandle): EntityHandle {
|
|
|
59
55
|
}
|
|
60
56
|
throw new Error('no mounted member found');
|
|
61
57
|
}
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
function firstInstanceOverride(
|
|
59
|
+
scene: SceneAsset,
|
|
60
|
+
comp: string,
|
|
61
|
+
): { readonly value: unknown } | undefined {
|
|
62
|
+
for (const entity of Object.values(scene.entities)) {
|
|
63
|
+
for (const instanceOverride of entity.instance?.overrides ?? []) {
|
|
64
|
+
const value = instanceOverride.components[comp];
|
|
65
|
+
if (value !== undefined) return { value };
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
return undefined;
|
|
@@ -83,12 +83,7 @@ describe('m3-t1: serialize refs index with schema derivation', () => {
|
|
|
83
83
|
|
|
84
84
|
const sceneAsset: SceneAsset = {
|
|
85
85
|
kind: 'scene',
|
|
86
|
-
entities:
|
|
87
|
-
{
|
|
88
|
-
localId: localId(0),
|
|
89
|
-
components: { Test_SPackSharedScalar: { assetRef: guid1 } },
|
|
90
|
-
},
|
|
91
|
-
],
|
|
86
|
+
entities: { 'entity-0': { components: { Test_SPackSharedScalar: { assetRef: guid1 } } } },
|
|
92
87
|
};
|
|
93
88
|
|
|
94
89
|
const packResult = serializeSceneAssetToPack(
|
|
@@ -116,7 +111,7 @@ describe('m3-t1: serialize refs index with schema derivation', () => {
|
|
|
116
111
|
expect(refs).toContain(guid1);
|
|
117
112
|
|
|
118
113
|
const payload = asset.payload as Record<string, unknown>;
|
|
119
|
-
const entities = payload.entities as
|
|
114
|
+
const entities = Object.values(payload.entities as Record<string, Record<string, unknown>>);
|
|
120
115
|
expect(entities).toHaveLength(1);
|
|
121
116
|
|
|
122
117
|
const ent0 = entities[0];
|
|
@@ -138,12 +133,9 @@ describe('m3-t1: serialize refs index with schema derivation', () => {
|
|
|
138
133
|
|
|
139
134
|
const sceneAsset: SceneAsset = {
|
|
140
135
|
kind: 'scene',
|
|
141
|
-
entities:
|
|
142
|
-
{
|
|
143
|
-
|
|
144
|
-
components: { Test_SPackSharedArray: { sources: [guidA, guidB] } },
|
|
145
|
-
},
|
|
146
|
-
],
|
|
136
|
+
entities: {
|
|
137
|
+
'entity-0': { components: { Test_SPackSharedArray: { sources: [guidA, guidB] } } },
|
|
138
|
+
},
|
|
147
139
|
};
|
|
148
140
|
|
|
149
141
|
const packResult = serializeSceneAssetToPack(
|
|
@@ -163,7 +155,7 @@ describe('m3-t1: serialize refs index with schema derivation', () => {
|
|
|
163
155
|
expect(refs).toContain(guidB);
|
|
164
156
|
|
|
165
157
|
const payload = asset.payload as Record<string, unknown>;
|
|
166
|
-
const entities = payload.entities as
|
|
158
|
+
const entities = Object.values(payload.entities as Record<string, Record<string, unknown>>);
|
|
167
159
|
const ent0 = entities[0];
|
|
168
160
|
if (!ent0) throw new Error('entity 0 missing');
|
|
169
161
|
const comps = ent0.components as Record<string, Record<string, unknown>>;
|
|
@@ -190,12 +182,7 @@ describe('m3-t1: serialize refs index with schema derivation', () => {
|
|
|
190
182
|
|
|
191
183
|
const sceneAsset: SceneAsset = {
|
|
192
184
|
kind: 'scene',
|
|
193
|
-
entities:
|
|
194
|
-
{
|
|
195
|
-
localId: localId(0),
|
|
196
|
-
components: { Test_STilesetPack: { tileset: tilesetGuid } },
|
|
197
|
-
},
|
|
198
|
-
],
|
|
185
|
+
entities: { 'entity-0': { components: { Test_STilesetPack: { tileset: tilesetGuid } } } },
|
|
199
186
|
};
|
|
200
187
|
|
|
201
188
|
const packResult = serializeSceneAssetToPack(
|
|
@@ -226,12 +213,11 @@ describe('m3-t1: serialize refs index with schema derivation', () => {
|
|
|
226
213
|
// collection (only strings are collected), so Phase 2 index lookup will fail.
|
|
227
214
|
const sceneAsset: SceneAsset = {
|
|
228
215
|
kind: 'scene',
|
|
229
|
-
entities:
|
|
230
|
-
{
|
|
231
|
-
localId: localId(0),
|
|
216
|
+
entities: {
|
|
217
|
+
'entity-0': {
|
|
232
218
|
components: { Test_SFailFastPack: { loneRef: 'non-collected-guid-wont-be-in-refs' } },
|
|
233
219
|
},
|
|
234
|
-
|
|
220
|
+
},
|
|
235
221
|
};
|
|
236
222
|
|
|
237
223
|
// Use a completely unrelated GUID for the asset to ensure Phase 1 collects nothing.
|
|
@@ -272,15 +258,14 @@ describe('m3-t2: unregistered component silently skipped', () => {
|
|
|
272
258
|
|
|
273
259
|
const sceneAsset: SceneAsset = {
|
|
274
260
|
kind: 'scene',
|
|
275
|
-
entities:
|
|
276
|
-
{
|
|
277
|
-
localId: localId(0),
|
|
261
|
+
entities: {
|
|
262
|
+
'entity-0': {
|
|
278
263
|
components: {
|
|
279
264
|
Test_SRegComp: { val: 42 },
|
|
280
265
|
Unreg_TestSkip: { mysteryField: 'should-survive' },
|
|
281
266
|
},
|
|
282
267
|
},
|
|
283
|
-
|
|
268
|
+
},
|
|
284
269
|
};
|
|
285
270
|
|
|
286
271
|
const packResult = serializeSceneAssetToPack(
|
|
@@ -296,7 +281,7 @@ describe('m3-t2: unregistered component silently skipped', () => {
|
|
|
296
281
|
const asset = assets[0];
|
|
297
282
|
if (!asset) throw new Error('asset missing');
|
|
298
283
|
const payload = asset.payload as Record<string, unknown>;
|
|
299
|
-
const entities = payload.entities as
|
|
284
|
+
const entities = Object.values(payload.entities as Record<string, Record<string, unknown>>);
|
|
300
285
|
expect(entities).toHaveLength(1);
|
|
301
286
|
|
|
302
287
|
const ent0 = entities[0];
|
|
@@ -325,15 +310,14 @@ describe('m3-t2: unregistered component silently skipped', () => {
|
|
|
325
310
|
|
|
326
311
|
const sceneAsset: SceneAsset = {
|
|
327
312
|
kind: 'scene',
|
|
328
|
-
entities:
|
|
329
|
-
{
|
|
330
|
-
localId: localId(0),
|
|
313
|
+
entities: {
|
|
314
|
+
'entity-0': {
|
|
331
315
|
components: {
|
|
332
316
|
Test_SRegComp2: { count: 99 },
|
|
333
317
|
Unreg_TestSkip2: { ghostField: 12345 },
|
|
334
318
|
},
|
|
335
319
|
},
|
|
336
|
-
|
|
320
|
+
},
|
|
337
321
|
};
|
|
338
322
|
|
|
339
323
|
const packResult = serializeSceneAssetToPack(
|
|
@@ -349,7 +333,7 @@ describe('m3-t2: unregistered component silently skipped', () => {
|
|
|
349
333
|
const asset = assets[0];
|
|
350
334
|
if (!asset) throw new Error('asset missing');
|
|
351
335
|
const payload = asset.payload as Record<string, unknown>;
|
|
352
|
-
const entities = payload.entities as
|
|
336
|
+
const entities = Object.values(payload.entities as Record<string, Record<string, unknown>>);
|
|
353
337
|
expect(entities).toHaveLength(1);
|
|
354
338
|
|
|
355
339
|
const ent0 = entities[0];
|
|
@@ -409,13 +393,15 @@ function mintCatalogued(reg: AssetRegistry, w: World, guid: string, tag: number)
|
|
|
409
393
|
function mountOne(reg: AssetRegistry, w: World): EntityHandle {
|
|
410
394
|
const child: SceneAsset = {
|
|
411
395
|
kind: 'scene',
|
|
412
|
-
entities:
|
|
396
|
+
entities: { 'entity-0': { components: { Transform: { pos: [1, 0, 0] } } } },
|
|
413
397
|
};
|
|
414
398
|
catScene(reg, W18_CHILD, child);
|
|
415
399
|
const parent: SceneAsset = {
|
|
416
400
|
kind: 'scene',
|
|
417
|
-
entities:
|
|
418
|
-
|
|
401
|
+
entities: {
|
|
402
|
+
'entity-0': { components: { Transform: { pos: [0, 0, 0] } } },
|
|
403
|
+
child: { components: {}, instance: { source: W18_CHILD } },
|
|
404
|
+
},
|
|
419
405
|
};
|
|
420
406
|
catScene(reg, W18_PARENT, parent);
|
|
421
407
|
const inst = reg.instantiate(rs(w, parent), w);
|
|
@@ -443,7 +429,7 @@ describe('w18 — override-value shared handle→GUID two-state NULL-sentinel (A
|
|
|
443
429
|
const collect = rootsToSceneAsset(reg, w, [root]);
|
|
444
430
|
expect(collect.ok).toBe(true);
|
|
445
431
|
if (!collect.ok) return;
|
|
446
|
-
const ov =
|
|
432
|
+
const ov = firstInstanceOverride(collect.value, 'AnimationPlayer');
|
|
447
433
|
expect(ov).toBeDefined();
|
|
448
434
|
if (!ov) return;
|
|
449
435
|
const clips = (ov.value as Record<string, unknown>).clips as unknown[];
|
|
@@ -473,7 +459,7 @@ describe('w18 — override-value shared handle→GUID two-state NULL-sentinel (A
|
|
|
473
459
|
const collect = rootsToSceneAsset(reg, w, [root]);
|
|
474
460
|
expect(collect.ok).toBe(true);
|
|
475
461
|
if (!collect.ok) return;
|
|
476
|
-
const ov =
|
|
462
|
+
const ov = firstInstanceOverride(collect.value, 'AnimationPlayer');
|
|
477
463
|
expect(ov).toBeDefined();
|
|
478
464
|
if (!ov) return;
|
|
479
465
|
const clips = (ov.value as Record<string, unknown>).clips as unknown[];
|
|
@@ -499,7 +485,7 @@ describe('w18 — override-value shared handle→GUID two-state NULL-sentinel (A
|
|
|
499
485
|
const collect = rootsToSceneAsset(reg, w, [root]);
|
|
500
486
|
expect(collect.ok).toBe(true);
|
|
501
487
|
if (!collect.ok) return;
|
|
502
|
-
const ov =
|
|
488
|
+
const ov = firstInstanceOverride(collect.value, 'W18_ScalarShared');
|
|
503
489
|
expect(ov).toBeDefined();
|
|
504
490
|
if (!ov) return;
|
|
505
491
|
const val = ov.value as Record<string, unknown>;
|
|
@@ -522,7 +508,7 @@ describe('w18 — override-value shared handle→GUID two-state NULL-sentinel (A
|
|
|
522
508
|
const collect = rootsToSceneAsset(reg, w, [root]);
|
|
523
509
|
expect(collect.ok).toBe(true);
|
|
524
510
|
if (!collect.ok) return;
|
|
525
|
-
const ov =
|
|
511
|
+
const ov = firstInstanceOverride(collect.value, 'W18_ScalarShared');
|
|
526
512
|
expect(ov).toBeDefined();
|
|
527
513
|
if (!ov) return;
|
|
528
514
|
const val = ov.value as Record<string, unknown>;
|
|
@@ -45,7 +45,9 @@ describe('shadow_caster shared Surface schema', () => {
|
|
|
45
45
|
|
|
46
46
|
it('(b) shadow_caster has no material sidecar', () => {
|
|
47
47
|
const sourcePath = join(repoRoot, 'packages', 'shader', 'src', 'shadow_caster.wgsl');
|
|
48
|
-
|
|
48
|
+
const source = readFileSync(sourcePath, 'utf8');
|
|
49
|
+
expect(source).toContain('#define_import_path forgeax::default-shadow-caster');
|
|
50
|
+
expect(source).toContain('#import forgeax_material::surface_v1::{SurfaceInput, SurfaceData}');
|
|
49
51
|
expect(existsSync(`${sourcePath}.meta.json`)).toBe(false);
|
|
50
52
|
});
|
|
51
53
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { HANDLE_CUBE } from '@forgeax/engine-assets-runtime';
|
|
16
16
|
import { World } from '@forgeax/engine-ecs';
|
|
17
|
-
import type { Renderer } from '@forgeax/engine-render';
|
|
17
|
+
import type { InstanceCollectionId, Renderer } from '@forgeax/engine-render';
|
|
18
18
|
import {
|
|
19
19
|
Camera,
|
|
20
20
|
DirectionalLight,
|
|
@@ -39,11 +39,9 @@ const WIDTH = LIGHTWEIGHT_DAWN ? 128 : 256;
|
|
|
39
39
|
const HEIGHT = LIGHTWEIGHT_DAWN ? 128 : 256;
|
|
40
40
|
const BPR = Math.ceil((WIDTH * 4) / 256) * 256;
|
|
41
41
|
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
// completing well inside the nightly platform job's 45-minute budget.
|
|
46
|
-
const SHADOW_FIELDS_FALSIFIER_TIMEOUT_MS = 300_000;
|
|
42
|
+
// Four fresh readbacks, or the 2048-square map's full 300 frames, retain a
|
|
43
|
+
// bounded software-GPU budget. The lightweight map comparison keeps 60 seconds.
|
|
44
|
+
const SHADOW_FIELDS_FULL_PIXEL_TIMEOUT_MS = 300_000;
|
|
47
45
|
|
|
48
46
|
const ENGINE_MANIFEST = await (async () => {
|
|
49
47
|
const { buildEngineShaderManifest } = await import('@forgeax/engine-vite-plugin-shader');
|
|
@@ -191,6 +189,7 @@ function spawnScene(
|
|
|
191
189
|
normalBias: number,
|
|
192
190
|
pcf: number,
|
|
193
191
|
mapSize?: number,
|
|
192
|
+
instanceCollectionId?: InstanceCollectionId,
|
|
194
193
|
) {
|
|
195
194
|
const cm = casterMat(world);
|
|
196
195
|
const fm = floorMat(world);
|
|
@@ -234,6 +233,10 @@ function spawnScene(
|
|
|
234
233
|
intensity: 1,
|
|
235
234
|
castShadow,
|
|
236
235
|
mapSize: mapSize ?? 1024,
|
|
236
|
+
// One cascade covers this field-comparison scene. Multi-cascade rebuilds
|
|
237
|
+
// are exercised by shadow-csm-runtime-vary.dawn.test.ts; repeating four
|
|
238
|
+
// 2048-square depth maps here hid the field assertions behind timeouts.
|
|
239
|
+
cascadeCount: 1,
|
|
237
240
|
depthBias,
|
|
238
241
|
normalBias,
|
|
239
242
|
shadowDistance: 50,
|
|
@@ -253,7 +256,9 @@ function spawnScene(
|
|
|
253
256
|
},
|
|
254
257
|
{ component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
|
|
255
258
|
{ component: MeshRenderer, data: { materials: [cm] } },
|
|
256
|
-
|
|
259
|
+
...(instanceCollectionId === undefined
|
|
260
|
+
? []
|
|
261
|
+
: [{ component: Instances, data: { collectionId: instanceCollectionId } }]),
|
|
257
262
|
);
|
|
258
263
|
}
|
|
259
264
|
|
|
@@ -360,7 +365,10 @@ async function renderConfig(
|
|
|
360
365
|
const dev = sd;
|
|
361
366
|
|
|
362
367
|
const w = new World();
|
|
363
|
-
|
|
368
|
+
const instanceCollection = renderer.instances
|
|
369
|
+
.create({ transforms: buildTranslationGrid() })
|
|
370
|
+
.unwrap();
|
|
371
|
+
spawnScene(w, castShadow, depthBias, normalBias, pcf, mapSize, instanceCollection.collectionId);
|
|
364
372
|
let de = 0;
|
|
365
373
|
for (let i = 0; i < SHADOW_FIELDS_RENDER_FRAMES; i++) {
|
|
366
374
|
const r = drawPublished(renderer, w);
|
|
@@ -485,7 +493,10 @@ async function renderConfigWithSpy(
|
|
|
485
493
|
const dev = sd;
|
|
486
494
|
|
|
487
495
|
const w = new World();
|
|
488
|
-
|
|
496
|
+
const instanceCollection = renderer.instances
|
|
497
|
+
.create({ transforms: buildTranslationGrid() })
|
|
498
|
+
.unwrap();
|
|
499
|
+
spawnScene(w, castShadow, depthBias, normalBias, pcf, mapSize, instanceCollection.collectionId);
|
|
489
500
|
let de = 0;
|
|
490
501
|
for (let i = 0; i < 10; i++) {
|
|
491
502
|
const r = drawPublished(renderer, w);
|
|
@@ -623,13 +634,17 @@ describe('M6 shadow fields observability', () => {
|
|
|
623
634
|
}, 60000);
|
|
624
635
|
|
|
625
636
|
// --- Gate F: mapSize 256 vs 2048 (independent shadow proof) ---
|
|
626
|
-
it(
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
637
|
+
it(
|
|
638
|
+
'pixel A/B: mapSize 256 vs 2048',
|
|
639
|
+
async () => {
|
|
640
|
+
if (typeof globalThis.navigator?.gpu?.requestAdapter !== 'function') return;
|
|
641
|
+
const pxA = await renderConfig(true, 0.005, 0.05, 3, 256);
|
|
642
|
+
const pxB = await renderConfig(true, 0.005, 0.05, 3, 2048);
|
|
643
|
+
const d = diff(pxA, pxB);
|
|
644
|
+
expect(d.diff).toBeGreaterThan(0);
|
|
645
|
+
},
|
|
646
|
+
LIGHTWEIGHT_DAWN ? 60_000 : SHADOW_FIELDS_FULL_PIXEL_TIMEOUT_MS,
|
|
647
|
+
);
|
|
633
648
|
|
|
634
649
|
// --- Gate E: equal-control — same config twice => 0 diff ---
|
|
635
650
|
it('equal-control: same config twice = 0 byte diff', async () => {
|
|
@@ -678,7 +693,7 @@ describe('M6 shadow fields observability', () => {
|
|
|
678
693
|
expect(varied.diff).toBeGreaterThan(0);
|
|
679
694
|
expect(held.diff).toBe(0);
|
|
680
695
|
},
|
|
681
|
-
|
|
696
|
+
SHADOW_FIELDS_FULL_PIXEL_TIMEOUT_MS,
|
|
682
697
|
);
|
|
683
698
|
|
|
684
699
|
it(
|
|
@@ -696,7 +711,7 @@ describe('M6 shadow fields observability', () => {
|
|
|
696
711
|
expect(varied.diff).toBeGreaterThan(0);
|
|
697
712
|
expect(held.diff).toBe(0);
|
|
698
713
|
},
|
|
699
|
-
|
|
714
|
+
SHADOW_FIELDS_FULL_PIXEL_TIMEOUT_MS,
|
|
700
715
|
);
|
|
701
716
|
|
|
702
717
|
it(
|
|
@@ -714,6 +729,6 @@ describe('M6 shadow fields observability', () => {
|
|
|
714
729
|
expect(varied.diff).toBeGreaterThan(0);
|
|
715
730
|
expect(held.diff).toBe(0);
|
|
716
731
|
},
|
|
717
|
-
|
|
732
|
+
SHADOW_FIELDS_FULL_PIXEL_TIMEOUT_MS,
|
|
718
733
|
);
|
|
719
734
|
});
|
|
@@ -13,32 +13,25 @@ function sourceAfter(signature: string): string {
|
|
|
13
13
|
return shadowPass.slice(start);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
function expectValidatedOrderedLoop(
|
|
17
|
-
signature: string,
|
|
18
|
-
indexName: string,
|
|
19
|
-
dynamicOffsetBindGroup: string,
|
|
20
|
-
): void {
|
|
16
|
+
function expectValidatedOrderedLoop(signature: string, indexName: string): void {
|
|
21
17
|
const body = sourceAfter(signature);
|
|
22
18
|
expect(body).toContain(
|
|
23
19
|
`for (let ${indexName} = 0; ${indexName} < validatedOrdered.length; ${indexName}++)`,
|
|
24
20
|
);
|
|
25
|
-
expect(body).toContain(
|
|
21
|
+
expect(body).toContain('const meshSsboBase = c.shadowMeshSsboBase ?? 0');
|
|
22
|
+
expect(body).toContain(`(meshSsboBase + ${indexName}) * MESH_PER_ENTITY_STRIDE`);
|
|
26
23
|
}
|
|
27
24
|
|
|
28
25
|
describe('shadow mesh SSBO order', () => {
|
|
29
26
|
it('uses the upload order for typed directional, point, and spot shadow offsets', () => {
|
|
30
|
-
expectValidatedOrderedLoop(
|
|
31
|
-
'function recordShadowCasterDraws(',
|
|
32
|
-
'i',
|
|
33
|
-
'shadowPass.setBindGroup(2, shadowMeshBindGroup',
|
|
34
|
-
);
|
|
27
|
+
expectValidatedOrderedLoop('function recordShadowCasterDraws(', 'i');
|
|
35
28
|
expect(shadowPass).toContain('export function encodeDirectionalShadowPass(');
|
|
36
29
|
expect(shadowPass).toContain('export function encodePointShadowPass(');
|
|
37
30
|
expect(shadowPass).toContain('export function encodeSpotShadowPass(');
|
|
38
31
|
const spotBody = sourceAfter('export function encodeSpotShadowPass(');
|
|
39
32
|
expect(spotBody).toContain('recordShadowCasterDraws(');
|
|
40
33
|
expect(spotBody).toContain(
|
|
41
|
-
"buildMatchedRenderableIndices(c
|
|
34
|
+
"buildMatchedRenderableIndices(shadowDispatchEntries(c), { LightMode: ['ShadowCaster'] })",
|
|
42
35
|
);
|
|
43
36
|
expect(shadowPass).not.toContain('export function recordShadowPass(');
|
|
44
37
|
expect(shadowPass).not.toContain('export function recordPointShadowPass(');
|
|
@@ -4,8 +4,9 @@ import { dirname, join } from 'node:path';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { describe, expect, it } from 'vitest';
|
|
6
6
|
|
|
7
|
-
// Regression guard for the hot extract path: joint
|
|
8
|
-
//
|
|
7
|
+
// Regression guard for the hot extract path: joint and sprite array fields stay
|
|
8
|
+
// on column views, while large explicit instance matrices stay in the
|
|
9
|
+
// renderer-owned collection store instead of being copied into ECS.
|
|
9
10
|
|
|
10
11
|
const extractPaths = (() => {
|
|
11
12
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -14,12 +15,13 @@ const extractPaths = (() => {
|
|
|
14
15
|
})();
|
|
15
16
|
|
|
16
17
|
describe('_getArrayView count (AC-03 gate)', () => {
|
|
17
|
-
it('keeps skin
|
|
18
|
+
it('keeps skin/sprite hot fields on column views and instances renderer-owned', () => {
|
|
18
19
|
const src = extractPaths.map((path) => readFileSync(path, 'utf8')).join('\n');
|
|
19
20
|
expect(src).toMatch(/_getArrayView\(jointEntity,\s*GlobalTransform,\s*'world'\)/);
|
|
20
|
-
expect(src).toContain("_getArrayView(entity, Instances, 'transforms')");
|
|
21
21
|
expect(src).toContain("_getArrayView(entity, SpriteInstances, 'transforms')");
|
|
22
22
|
expect(src).toContain("_getArrayView(entity, SpriteInstances, 'regions')");
|
|
23
|
+
expect(src).toContain('instanceCollections.snapshot(collectionId)');
|
|
24
|
+
expect(src).not.toContain("_getArrayView(entity, Instances, 'transforms')");
|
|
23
25
|
expect(src).not.toMatch(/^\s*const\s+\w+\s*=\s*world\.get\(jointEntity, Transform\)/m);
|
|
24
26
|
expect(src).not.toMatch(/^\s*const\s+\w+\s*=\s*world\.get\(entity, Instances\)/m);
|
|
25
27
|
});
|
|
@@ -63,6 +63,7 @@ import { AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
|
63
63
|
import type { EntityHandle, World as WorldType } from '@forgeax/engine-ecs';
|
|
64
64
|
import { ENTITY_NULL_RAW, World } from '@forgeax/engine-ecs';
|
|
65
65
|
import { SpriteAnimationInvalidError } from '@forgeax/engine-ecs/projection';
|
|
66
|
+
import { buildMeshAttributeMapForUvSets } from '@forgeax/engine-geometry';
|
|
66
67
|
import { mat4, vec3 } from '@forgeax/engine-math';
|
|
67
68
|
import type { RenderErrorCode, Renderer as RendererType } from '@forgeax/engine-render';
|
|
68
69
|
import {
|
|
@@ -391,6 +392,7 @@ function registerSkinM2Mesh(world: World): Handle<'MeshAsset', 'shared'> {
|
|
|
391
392
|
vertices: SKIN_M2_TRIANGLE_VERTICES,
|
|
392
393
|
indices: new Uint16Array([0, 1, 2]),
|
|
393
394
|
attributes: {
|
|
395
|
+
...buildMeshAttributeMapForUvSets(1),
|
|
394
396
|
position: SKIN_M2_TRIANGLE_POSITIONS,
|
|
395
397
|
skinIndex: new Uint16Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
|
|
396
398
|
skinWeight: new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]),
|
|
@@ -492,17 +494,40 @@ function spawnSkinM2SkinnedEntity(
|
|
|
492
494
|
|
|
493
495
|
type SkinM2StubAllocator = {
|
|
494
496
|
buffer: null;
|
|
495
|
-
|
|
497
|
+
allocatePersistentSlice(input: {
|
|
498
|
+
identity: string;
|
|
499
|
+
generation: number;
|
|
500
|
+
jointCount: number;
|
|
501
|
+
bounds?: Float32Array;
|
|
502
|
+
}): SkinM2PersistentReceipt;
|
|
503
|
+
observePersistentJoints(
|
|
504
|
+
identity: string,
|
|
505
|
+
ibms: readonly Float32Array[],
|
|
506
|
+
jointWorlds: readonly Float32Array[],
|
|
507
|
+
): readonly number[];
|
|
508
|
+
allocateSlice(jointCount: number): { jointCount: number; byteOffset: number; buffer: null };
|
|
496
509
|
writeJointPalette(
|
|
497
510
|
slice: { jointCount: number; byteOffset: number },
|
|
498
511
|
ibms: readonly Float32Array[],
|
|
499
512
|
jointWorlds: readonly Float32Array[],
|
|
500
513
|
): void;
|
|
514
|
+
beginFrame(): void;
|
|
515
|
+
endFrame(): void;
|
|
516
|
+
releasePersistentSlice(identity: string): void;
|
|
517
|
+
writePersistentJointPalette(receipt: SkinM2PersistentReceipt): void;
|
|
501
518
|
resetForFrame(): void;
|
|
502
519
|
_resetCount(): number;
|
|
503
520
|
_writeCount(): number;
|
|
504
521
|
};
|
|
505
522
|
|
|
523
|
+
type SkinM2PersistentReceipt = {
|
|
524
|
+
identity: string;
|
|
525
|
+
generation: number;
|
|
526
|
+
jointCount: number;
|
|
527
|
+
byteOffset: number;
|
|
528
|
+
buffer: null;
|
|
529
|
+
};
|
|
530
|
+
|
|
506
531
|
function makeSkinM2StubAllocator(): SkinM2StubAllocator {
|
|
507
532
|
let cursor = 0;
|
|
508
533
|
let resetCount = 0;
|
|
@@ -513,11 +538,34 @@ function makeSkinM2StubAllocator(): SkinM2StubAllocator {
|
|
|
513
538
|
const offset = cursor;
|
|
514
539
|
// 256-aligned bump (mirrors real allocator dyn-offset alignment).
|
|
515
540
|
cursor = (cursor + jointCount * 64 + 255) & ~255;
|
|
516
|
-
return { jointCount, byteOffset: offset };
|
|
541
|
+
return { jointCount, byteOffset: offset, buffer: null };
|
|
517
542
|
},
|
|
518
543
|
writeJointPalette() {
|
|
519
544
|
writeCount++;
|
|
520
545
|
},
|
|
546
|
+
observePersistentJoints() {
|
|
547
|
+
return [];
|
|
548
|
+
},
|
|
549
|
+
allocatePersistentSlice(input) {
|
|
550
|
+
const slice = this.allocateSlice(input.jointCount);
|
|
551
|
+
return {
|
|
552
|
+
...slice,
|
|
553
|
+
identity: input.identity,
|
|
554
|
+
generation: input.generation,
|
|
555
|
+
fence: 1,
|
|
556
|
+
customDataStart: 0,
|
|
557
|
+
storageOrUniform: 'storage',
|
|
558
|
+
dirtyRanges: [{ startJoint: 0, jointCount: input.jointCount }],
|
|
559
|
+
uploadBytes: input.jointCount * 64,
|
|
560
|
+
...(input.bounds === undefined ? {} : { bounds: input.bounds }),
|
|
561
|
+
};
|
|
562
|
+
},
|
|
563
|
+
writePersistentJointPalette() {
|
|
564
|
+
writeCount++;
|
|
565
|
+
},
|
|
566
|
+
beginFrame() {},
|
|
567
|
+
endFrame() {},
|
|
568
|
+
releasePersistentSlice() {},
|
|
521
569
|
resetForFrame() {
|
|
522
570
|
cursor = 0;
|
|
523
571
|
resetCount++;
|
|
@@ -63,6 +63,7 @@ import { AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
|
63
63
|
import type { EntityHandle, World as WorldType } from '@forgeax/engine-ecs';
|
|
64
64
|
import { ENTITY_NULL_RAW, World } from '@forgeax/engine-ecs';
|
|
65
65
|
import { SpriteAnimationInvalidError } from '@forgeax/engine-ecs/projection';
|
|
66
|
+
import { buildMeshAttributeMapForUvSets } from '@forgeax/engine-geometry';
|
|
66
67
|
import { mat4, vec3 } from '@forgeax/engine-math';
|
|
67
68
|
import type { RenderErrorCode, Renderer as RendererType } from '@forgeax/engine-render';
|
|
68
69
|
import {
|
|
@@ -391,6 +392,7 @@ function registerSkinM2Mesh(world: World): Handle<'MeshAsset', 'shared'> {
|
|
|
391
392
|
vertices: SKIN_M2_TRIANGLE_VERTICES,
|
|
392
393
|
indices: new Uint16Array([0, 1, 2]),
|
|
393
394
|
attributes: {
|
|
395
|
+
...buildMeshAttributeMapForUvSets(1),
|
|
394
396
|
position: SKIN_M2_TRIANGLE_POSITIONS,
|
|
395
397
|
skinIndex: new Uint16Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
|
|
396
398
|
skinWeight: new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]),
|
|
@@ -492,17 +494,40 @@ function spawnSkinM2SkinnedEntity(
|
|
|
492
494
|
|
|
493
495
|
type SkinM2StubAllocator = {
|
|
494
496
|
buffer: null;
|
|
495
|
-
|
|
497
|
+
allocatePersistentSlice(input: {
|
|
498
|
+
identity: string;
|
|
499
|
+
generation: number;
|
|
500
|
+
jointCount: number;
|
|
501
|
+
bounds?: Float32Array;
|
|
502
|
+
}): SkinM2PersistentReceipt;
|
|
503
|
+
observePersistentJoints(
|
|
504
|
+
identity: string,
|
|
505
|
+
ibms: readonly Float32Array[],
|
|
506
|
+
jointWorlds: readonly Float32Array[],
|
|
507
|
+
): readonly number[];
|
|
508
|
+
allocateSlice(jointCount: number): { jointCount: number; byteOffset: number; buffer: null };
|
|
496
509
|
writeJointPalette(
|
|
497
510
|
slice: { jointCount: number; byteOffset: number },
|
|
498
511
|
ibms: readonly Float32Array[],
|
|
499
512
|
jointWorlds: readonly Float32Array[],
|
|
500
513
|
): void;
|
|
514
|
+
beginFrame(): void;
|
|
515
|
+
endFrame(): void;
|
|
516
|
+
releasePersistentSlice(identity: string): void;
|
|
517
|
+
writePersistentJointPalette(receipt: SkinM2PersistentReceipt): void;
|
|
501
518
|
resetForFrame(): void;
|
|
502
519
|
_resetCount(): number;
|
|
503
520
|
_writeCount(): number;
|
|
504
521
|
};
|
|
505
522
|
|
|
523
|
+
type SkinM2PersistentReceipt = {
|
|
524
|
+
identity: string;
|
|
525
|
+
generation: number;
|
|
526
|
+
jointCount: number;
|
|
527
|
+
byteOffset: number;
|
|
528
|
+
buffer: null;
|
|
529
|
+
};
|
|
530
|
+
|
|
506
531
|
function makeSkinM2StubAllocator(): SkinM2StubAllocator {
|
|
507
532
|
let cursor = 0;
|
|
508
533
|
let resetCount = 0;
|
|
@@ -513,11 +538,34 @@ function makeSkinM2StubAllocator(): SkinM2StubAllocator {
|
|
|
513
538
|
const offset = cursor;
|
|
514
539
|
// 256-aligned bump (mirrors real allocator dyn-offset alignment).
|
|
515
540
|
cursor = (cursor + jointCount * 64 + 255) & ~255;
|
|
516
|
-
return { jointCount, byteOffset: offset };
|
|
541
|
+
return { jointCount, byteOffset: offset, buffer: null };
|
|
517
542
|
},
|
|
518
543
|
writeJointPalette() {
|
|
519
544
|
writeCount++;
|
|
520
545
|
},
|
|
546
|
+
observePersistentJoints() {
|
|
547
|
+
return [];
|
|
548
|
+
},
|
|
549
|
+
allocatePersistentSlice(input) {
|
|
550
|
+
const slice = this.allocateSlice(input.jointCount);
|
|
551
|
+
return {
|
|
552
|
+
...slice,
|
|
553
|
+
identity: input.identity,
|
|
554
|
+
generation: input.generation,
|
|
555
|
+
fence: 1,
|
|
556
|
+
customDataStart: 0,
|
|
557
|
+
storageOrUniform: 'storage',
|
|
558
|
+
dirtyRanges: [{ startJoint: 0, jointCount: input.jointCount }],
|
|
559
|
+
uploadBytes: input.jointCount * 64,
|
|
560
|
+
...(input.bounds === undefined ? {} : { bounds: input.bounds }),
|
|
561
|
+
};
|
|
562
|
+
},
|
|
563
|
+
writePersistentJointPalette() {
|
|
564
|
+
writeCount++;
|
|
565
|
+
},
|
|
566
|
+
beginFrame() {},
|
|
567
|
+
endFrame() {},
|
|
568
|
+
releasePersistentSlice() {},
|
|
521
569
|
resetForFrame() {
|
|
522
570
|
cursor = 0;
|
|
523
571
|
resetCount++;
|