@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
|
@@ -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++;
|
|
@@ -378,7 +378,9 @@ type __MergedKeep =
|
|
|
378
378
|
written.push({ offset, data });
|
|
379
379
|
return { ok: true };
|
|
380
380
|
},
|
|
381
|
+
onSubmittedWorkDone: async () => undefined,
|
|
381
382
|
},
|
|
383
|
+
destroyBuffer: () => ({ ok: true, value: undefined }),
|
|
382
384
|
} as unknown as Parameters<typeof createSkinPaletteAllocator>[0],
|
|
383
385
|
buffers,
|
|
384
386
|
written,
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { HANDLE_CUBE } from '@forgeax/engine-assets-runtime';
|
|
9
9
|
import { World } from '@forgeax/engine-ecs';
|
|
10
|
+
import { packInterleavedVertexAttributes } from '@forgeax/engine-geometry';
|
|
10
11
|
import type { Renderer } from '@forgeax/engine-render';
|
|
11
12
|
import { Camera, DirectionalLight, MeshFilter, MeshRenderer } from '@forgeax/engine-render';
|
|
12
13
|
import { Transform } from '@forgeax/engine-scene';
|
|
@@ -67,7 +68,7 @@ function staticMaterial(world: World): Handle<'MaterialAsset', 'shared'> {
|
|
|
67
68
|
});
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
function skinnedMaterial(world: World): Handle<'MaterialAsset', 'shared'> {
|
|
71
|
+
function skinnedMaterial(world: World, masked: boolean): Handle<'MaterialAsset', 'shared'> {
|
|
71
72
|
return world.allocSharedRef<'MaterialAsset', MaterialAsset>('MaterialAsset', {
|
|
72
73
|
kind: 'material',
|
|
73
74
|
passes: [
|
|
@@ -86,7 +87,8 @@ function skinnedMaterial(world: World): Handle<'MaterialAsset', 'shared'> {
|
|
|
86
87
|
},
|
|
87
88
|
],
|
|
88
89
|
values: {
|
|
89
|
-
baseColor: [0.15, 0.45, 0.9, 1],
|
|
90
|
+
baseColor: [0.15, 0.45, 0.9, masked ? 0 : 1],
|
|
91
|
+
alphaCutoff: masked ? 0.5 : 0,
|
|
90
92
|
metallic: 0.1,
|
|
91
93
|
roughness: 0.55,
|
|
92
94
|
emissive: [0, 0, 0],
|
|
@@ -120,22 +122,22 @@ function floorMaterial(world: World): Handle<'MaterialAsset', 'shared'> {
|
|
|
120
122
|
function skinnedMesh(world: World): Handle<'MeshAsset', 'shared'> {
|
|
121
123
|
// Canonical interleaved order: position, normal, uv, tangent, skinIndex,
|
|
122
124
|
// skinWeight (72 bytes / vertex). All vertices use joint zero.
|
|
123
|
-
const
|
|
124
|
-
-0.75, 0, 0, 0
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
const attributes = {
|
|
126
|
+
position: new Float32Array([-0.75, 0, 0, 0.75, 0, 0, 0, 1.2, 0]),
|
|
127
|
+
normal: new Float32Array([0, 0, 1, 0, 0, 1, 0, 0, 1]),
|
|
128
|
+
uv: new Float32Array([0, 0, 1, 0, 0.5, 1]),
|
|
129
|
+
tangent: new Float32Array([1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1]),
|
|
130
|
+
skinIndex: new Uint16Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
|
|
131
|
+
skinWeight: new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]),
|
|
132
|
+
};
|
|
133
|
+
const packed = packInterleavedVertexAttributes(attributes, 3);
|
|
134
|
+
if (!packed.ok) throw packed.error;
|
|
135
|
+
const vertices = packed.value.vertices;
|
|
127
136
|
return world.allocSharedRef<'MeshAsset', MeshAsset>('MeshAsset', {
|
|
128
137
|
kind: 'mesh',
|
|
129
138
|
vertices,
|
|
130
139
|
indices: new Uint16Array([0, 1, 2]),
|
|
131
|
-
attributes
|
|
132
|
-
position: new Float32Array([-0.75, 0, 0, 0.75, 0, 0, 0, 1.2, 0]),
|
|
133
|
-
normal: new Float32Array([0, 0, 1, 0, 0, 1, 0, 0, 1]),
|
|
134
|
-
uv: new Float32Array([0, 0, 1, 0, 0.5, 1]),
|
|
135
|
-
tangent: new Float32Array([1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1]),
|
|
136
|
-
skinIndex: new Uint16Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
|
|
137
|
-
skinWeight: new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]),
|
|
138
|
-
},
|
|
140
|
+
attributes,
|
|
139
141
|
aabb: new Float32Array([-0.75, 0, 0, 0.75, 1.2, 0]),
|
|
140
142
|
materialSlots: [{ slotName: 'Default' }],
|
|
141
143
|
submeshes: [
|
|
@@ -150,7 +152,7 @@ function skinnedMesh(world: World): Handle<'MeshAsset', 'shared'> {
|
|
|
150
152
|
});
|
|
151
153
|
}
|
|
152
154
|
|
|
153
|
-
function skeleton(world: World): Handle<'SkeletonAsset', 'shared'> {
|
|
155
|
+
function skeleton(world: World, missingBounds: boolean): Handle<'SkeletonAsset', 'shared'> {
|
|
154
156
|
const inverseBindMatrices = new Float32Array(16);
|
|
155
157
|
inverseBindMatrices[0] = 1;
|
|
156
158
|
inverseBindMatrices[5] = 1;
|
|
@@ -160,6 +162,7 @@ function skeleton(world: World): Handle<'SkeletonAsset', 'shared'> {
|
|
|
160
162
|
kind: 'skeleton',
|
|
161
163
|
inverseBindMatrices,
|
|
162
164
|
jointCount: 1,
|
|
165
|
+
...(missingBounds ? {} : { bounds: new Float32Array([-1, -1, -1, 1, 1, 1]) }),
|
|
163
166
|
});
|
|
164
167
|
}
|
|
165
168
|
|
|
@@ -167,12 +170,17 @@ function identityTransform(pos: readonly [number, number, number] = [0, 0, 0]) {
|
|
|
167
170
|
return { pos, quat: [0, 0, 0, 1] as const, scale: [1, 1, 1] as const };
|
|
168
171
|
}
|
|
169
172
|
|
|
170
|
-
function spawnMixedScene(
|
|
173
|
+
function spawnMixedScene(
|
|
174
|
+
world: World,
|
|
175
|
+
castShadow: boolean,
|
|
176
|
+
maskedSkin: boolean,
|
|
177
|
+
omitSkin: boolean,
|
|
178
|
+
): void {
|
|
171
179
|
const staticMat = staticMaterial(world);
|
|
172
|
-
const skinMat = skinnedMaterial(world);
|
|
180
|
+
const skinMat = skinnedMaterial(world, maskedSkin);
|
|
173
181
|
const floorMat = floorMaterial(world);
|
|
174
182
|
const skinMesh = skinnedMesh(world);
|
|
175
|
-
const skinSkeleton = skeleton(world);
|
|
183
|
+
const skinSkeleton = skeleton(world, maskedSkin);
|
|
176
184
|
|
|
177
185
|
world.spawn(
|
|
178
186
|
{ component: Transform, data: { ...identityTransform([0, 5, 5]), scale: [10, 0.1, 10] } },
|
|
@@ -219,18 +227,19 @@ function spawnMixedScene(world: World, castShadow: boolean): void {
|
|
|
219
227
|
// Entity order is intentional: static A -> skinned B -> static C.
|
|
220
228
|
spawnStaticCaster(-3);
|
|
221
229
|
const joint = world.spawn({ component: Transform, data: identityTransform([0, 7, 5]) }).unwrap();
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
230
|
+
if (!omitSkin)
|
|
231
|
+
world.spawn(
|
|
232
|
+
{ component: Transform, data: identityTransform() },
|
|
233
|
+
{ component: MeshFilter, data: { assetHandle: skinMesh } },
|
|
234
|
+
{ component: MeshRenderer, data: { materials: [skinMat] } },
|
|
235
|
+
{
|
|
236
|
+
component: Skin,
|
|
237
|
+
data: {
|
|
238
|
+
skeleton: skinSkeleton,
|
|
239
|
+
joints: new Uint32Array([joint as unknown as number]),
|
|
240
|
+
},
|
|
231
241
|
},
|
|
232
|
-
|
|
233
|
-
);
|
|
242
|
+
);
|
|
234
243
|
spawnStaticCaster(3);
|
|
235
244
|
}
|
|
236
245
|
|
|
@@ -253,7 +262,18 @@ async function readPixels(device: GPUDevice, texture: GPUTexture): Promise<Uint8
|
|
|
253
262
|
return pixels;
|
|
254
263
|
}
|
|
255
264
|
|
|
256
|
-
|
|
265
|
+
/**
|
|
266
|
+
* Render both comparison worlds through one host/device. The previous
|
|
267
|
+
* fixture constructed and tore down a complete renderer twice; that paid the
|
|
268
|
+
* native shader prewarm cost twice while adding no GPU coverage. Keeping the
|
|
269
|
+
* renderer alive across the two worlds preserves the shadow/no-shadow pixel
|
|
270
|
+
* comparison and makes the Dawn test's startup cost proportional to one
|
|
271
|
+
* renderer initialization.
|
|
272
|
+
*/
|
|
273
|
+
async function renderMixedPair(maskedSkin = false): Promise<{
|
|
274
|
+
readonly shadow: MixedCapture;
|
|
275
|
+
readonly baseline: MixedCapture;
|
|
276
|
+
}> {
|
|
257
277
|
let device: GPUDevice | undefined;
|
|
258
278
|
let target: GPUTexture | undefined;
|
|
259
279
|
const originalRequestAdapter = globalThis.navigator.gpu.requestAdapter.bind(
|
|
@@ -313,22 +333,35 @@ async function renderMixed(castShadow: boolean): Promise<MixedCapture> {
|
|
|
313
333
|
errors.push(event.error.code);
|
|
314
334
|
}
|
|
315
335
|
});
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
336
|
+
const renderWorld = async (castShadow: boolean, omitSkin = false): Promise<MixedCapture> => {
|
|
337
|
+
const world = new World();
|
|
338
|
+
spawnMixedScene(world, castShadow, maskedSkin, omitSkin);
|
|
339
|
+
const attached = renderer?.attach(world);
|
|
340
|
+
if (attached === undefined || !attached.ok) {
|
|
341
|
+
throw attached?.error ?? new Error('renderer attachment missing');
|
|
342
|
+
}
|
|
343
|
+
// The first frame warms the static and skinned PSOs; the second frame is
|
|
344
|
+
// the asserted mixed command stream after both cache entries exist.
|
|
345
|
+
for (let frame = 0; frame < 2; frame += 1) {
|
|
346
|
+
const receipt = drawPublished(renderer, world);
|
|
347
|
+
expect(receipt.ok, `mixed ${castShadow ? 'shadow' : 'baseline'} draw`).toBe(true);
|
|
348
|
+
if (!receipt.ok) throw receipt.error;
|
|
349
|
+
const completed = await receipt.value.completed;
|
|
350
|
+
expect(completed.ok, 'Queue::submit completion').toBe(true);
|
|
351
|
+
if (!completed.ok) throw completed.error;
|
|
352
|
+
}
|
|
353
|
+
if (device === undefined || target === undefined)
|
|
354
|
+
throw new Error('Dawn target not initialized');
|
|
355
|
+
await device.queue.onSubmittedWorkDone();
|
|
356
|
+
const pixels = await readPixels(device, target);
|
|
357
|
+
attached.value.dispose();
|
|
358
|
+
return { pixels, errors: [...errors] };
|
|
359
|
+
};
|
|
360
|
+
const shadow = await renderWorld(true);
|
|
361
|
+
// Remove only the clipped skin for the mask control; retain neighboring
|
|
362
|
+
// rigid shadows so their PCF contributions cancel exactly.
|
|
363
|
+
const baseline = maskedSkin ? await renderWorld(true, true) : await renderWorld(false);
|
|
364
|
+
return { shadow, baseline };
|
|
332
365
|
} finally {
|
|
333
366
|
globalThis.navigator.gpu.requestAdapter = originalRequestAdapter;
|
|
334
367
|
unsubscribe?.();
|
|
@@ -356,12 +389,24 @@ function regionDiff(a: Uint8Array, b: Uint8Array, x0: number, x1: number): numbe
|
|
|
356
389
|
}
|
|
357
390
|
|
|
358
391
|
describe('mixed static/skinned shadow pipeline (Dawn)', () => {
|
|
392
|
+
it('keeps alpha clipping on CPU skin residuals without conservative bounds', async () => {
|
|
393
|
+
const { shadow, baseline } = await renderMixedPair(true);
|
|
394
|
+
expect(shadow.errors).toEqual([]);
|
|
395
|
+
expect(baseline.errors).toEqual([]);
|
|
396
|
+
// A fully clipped skin must match an absent skin in both color and depth.
|
|
397
|
+
// The opaque test below independently proves all three caster regions.
|
|
398
|
+
const changed = shadow.pixels.reduce(
|
|
399
|
+
(count, value, index) => count + Number(value !== baseline.pixels[index]),
|
|
400
|
+
0,
|
|
401
|
+
);
|
|
402
|
+
expect(changed).toBe(0);
|
|
403
|
+
}, 120000);
|
|
404
|
+
|
|
359
405
|
it('submits static → skinned → static casters with zero validation errors', async () => {
|
|
360
406
|
if (typeof globalThis.navigator?.gpu?.requestAdapter !== 'function') {
|
|
361
407
|
throw new Error('dawn-node navigator.gpu not injected');
|
|
362
408
|
}
|
|
363
|
-
const shadow = await
|
|
364
|
-
const baseline = await renderMixed(false);
|
|
409
|
+
const { shadow, baseline } = await renderMixedPair();
|
|
365
410
|
expect(shadow.errors).toEqual([]);
|
|
366
411
|
expect(baseline.errors).toEqual([]);
|
|
367
412
|
expect(shadow.pixels.length).toBeGreaterThan(0);
|
|
@@ -420,13 +420,17 @@ type __MergedKeep =
|
|
|
420
420
|
expect(layout?.label).toBe('pbr-pl');
|
|
421
421
|
});
|
|
422
422
|
|
|
423
|
-
it('(b) PBR material BGL entry count ===
|
|
423
|
+
it('(b) PBR material BGL entry count === 25 (user region 0..14 + Skylight 15..21 + transmission 22..23 + scene row 46)', () => {
|
|
424
424
|
const device = makeMockDevice();
|
|
425
425
|
// biome-ignore lint/suspicious/noExplicitAny: structural mock
|
|
426
426
|
buildPbrPipelineLayouts(device as any, STORAGE_CAPS);
|
|
427
427
|
const materialBgl = device.capturedBgls.find((b) => b.label === 'pbr-material-skylight-bgl');
|
|
428
428
|
expect(materialBgl).toBeDefined();
|
|
429
|
-
expect(materialBgl?.entries).toHaveLength(
|
|
429
|
+
expect(materialBgl?.entries).toHaveLength(25);
|
|
430
|
+
expect(materialBgl?.entries[24]).toMatchObject({
|
|
431
|
+
binding: 46,
|
|
432
|
+
buffer: { type: 'read-only-storage', hasDynamicOffset: false },
|
|
433
|
+
});
|
|
430
434
|
});
|
|
431
435
|
|
|
432
436
|
it('(c) binding indices 0..23 in order; 15..21 resource types in D-5 round-4 order; 22..23 transmission', () => {
|
|
@@ -24,7 +24,7 @@ describe('sparse tag scene persistence', () => {
|
|
|
24
24
|
.unwrap();
|
|
25
25
|
const registry = {} as AssetRegistry;
|
|
26
26
|
const collected = rootsToSceneAsset(registry, source, [entity]).unwrap();
|
|
27
|
-
const components = collected.entities[0]?.components as
|
|
27
|
+
const components = Object.values(collected.entities)[0]?.components as
|
|
28
28
|
| Record<string, Record<string, unknown>>
|
|
29
29
|
| undefined;
|
|
30
30
|
|
|
@@ -126,16 +126,24 @@ describe('sprite-lit BGL byte-identical to sprite (AC-07, w4/w5 close)', () => {
|
|
|
126
126
|
expect(base.length).toBe(15);
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
-
it('pbr-material-merged stays
|
|
129
|
+
it('pbr-material-merged stays 25 entries (sprite/sprite-lit do not change material BGL shape)', () => {
|
|
130
130
|
const merged = buildBindGroupLayoutDescriptor(makeSpec('forgeax::sprite-lit'), {
|
|
131
131
|
kind: 'pbr-material-merged',
|
|
132
132
|
});
|
|
133
|
-
expect(merged.entries.length).toBe(
|
|
133
|
+
expect(merged.entries.length).toBe(25);
|
|
134
134
|
// mergeSkylightIntoMaterialBgl + transmission injection must stay
|
|
135
135
|
// append-only against the 7-entry PBR base.
|
|
136
136
|
const base = buildPbrMaterialUserRegionEntries();
|
|
137
137
|
const afterSky = mergeSkylightIntoMaterialBgl(base);
|
|
138
|
-
const expected = [
|
|
138
|
+
const expected = [
|
|
139
|
+
...afterSky,
|
|
140
|
+
...appendInjection(afterSky, 'transmission'),
|
|
141
|
+
{
|
|
142
|
+
binding: 46,
|
|
143
|
+
visibility: 0x1 | 0x2,
|
|
144
|
+
buffer: { type: 'read-only-storage', hasDynamicOffset: false },
|
|
145
|
+
},
|
|
146
|
+
];
|
|
139
147
|
expect(merged.entries).toEqual(expected);
|
|
140
148
|
});
|
|
141
149
|
});
|
|
@@ -38,15 +38,11 @@ import { MeshRenderer, SceneInstance } from '@forgeax/engine-render';
|
|
|
38
38
|
// undefined during instantiate. ChildOf + SceneInstance are used directly
|
|
39
39
|
// below as values; Transform / MeshRenderer pin the side-effect import.
|
|
40
40
|
import { ChildOf, Transform } from '@forgeax/engine-scene';
|
|
41
|
-
import type {
|
|
41
|
+
import type { MaterialAsset, SceneAsset } from '@forgeax/engine-types';
|
|
42
42
|
import { describe, expect, it } from 'vitest';
|
|
43
43
|
import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
44
44
|
import { registerRuntimeComponents } from './helpers/register-runtime-components';
|
|
45
45
|
|
|
46
|
-
function lid(n: number): LocalEntityId {
|
|
47
|
-
return n as LocalEntityId;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
46
|
// Shape this fixture mirrors the `apps/game-capability-lab/assets/scene.pack.json`
|
|
51
47
|
// SceneAsset payload after the template's `instantiateScenePack` helper has
|
|
52
48
|
// rewritten ref-int → GUID strings + lifted MeshRenderer.material → materials[].
|
|
@@ -61,9 +57,8 @@ function buildSceneAssetWithGuidRef(): SceneAsset {
|
|
|
61
57
|
void Transform;
|
|
62
58
|
void MeshRenderer;
|
|
63
59
|
|
|
64
|
-
const nodes
|
|
65
|
-
{
|
|
66
|
-
localId: lid(0),
|
|
60
|
+
const nodes = {
|
|
61
|
+
'entity-0': {
|
|
67
62
|
components: {
|
|
68
63
|
Transform: { pos: [0, 0, 0] },
|
|
69
64
|
// GUID string (post template ref-int → GUID rewrite). The registry's
|
|
@@ -72,14 +67,13 @@ function buildSceneAssetWithGuidRef(): SceneAsset {
|
|
|
72
67
|
MeshRenderer: { materials: [MAT_GUID] },
|
|
73
68
|
},
|
|
74
69
|
},
|
|
75
|
-
{
|
|
76
|
-
localId: lid(1),
|
|
70
|
+
'entity-1': {
|
|
77
71
|
components: {
|
|
78
72
|
Transform: { pos: [1, 0, 0] },
|
|
79
|
-
ChildOf: { parent: 0
|
|
73
|
+
ChildOf: { parent: 'entity-0' },
|
|
80
74
|
},
|
|
81
75
|
},
|
|
82
|
-
|
|
76
|
+
};
|
|
83
77
|
return { kind: 'scene', entities: nodes };
|
|
84
78
|
}
|
|
85
79
|
|
|
@@ -272,13 +266,19 @@ describe('game-capability-lab API contract (regression for `world.sceneInstances
|
|
|
272
266
|
assets:
|
|
273
267
|
| {
|
|
274
268
|
kind: string;
|
|
275
|
-
payload?: {
|
|
269
|
+
payload?: {
|
|
270
|
+
entities?: Record<string, { components: Record<string, Record<string, unknown>> }>;
|
|
271
|
+
nodes?: Record<string, { components: Record<string, Record<string, unknown>> }>;
|
|
272
|
+
};
|
|
276
273
|
}[]
|
|
277
274
|
| Record<
|
|
278
275
|
string,
|
|
279
276
|
{
|
|
280
277
|
kind: string;
|
|
281
|
-
payload?: {
|
|
278
|
+
payload?: {
|
|
279
|
+
entities?: Record<string, { components: Record<string, Record<string, unknown>> }>;
|
|
280
|
+
nodes?: Record<string, { components: Record<string, Record<string, unknown>> }>;
|
|
281
|
+
};
|
|
282
282
|
}
|
|
283
283
|
>;
|
|
284
284
|
};
|
|
@@ -291,7 +291,7 @@ describe('game-capability-lab API contract (regression for `world.sceneInstances
|
|
|
291
291
|
// no longer restates it (Derive, Don't Duplicate — the loader synthesises
|
|
292
292
|
// Asset.kind from the envelope on load).
|
|
293
293
|
if (asset.kind !== 'scene') continue;
|
|
294
|
-
for (const node of asset.payload?.entities ?? asset.payload?.nodes ??
|
|
294
|
+
for (const node of Object.values(asset.payload?.entities ?? asset.payload?.nodes ?? {})) {
|
|
295
295
|
for (const [compName, data] of Object.entries(node.components)) {
|
|
296
296
|
if (STRIP_COMPONENTS.has(compName)) continue;
|
|
297
297
|
scanned++;
|
|
@@ -305,9 +305,7 @@ describe('game-capability-lab API contract (regression for `world.sceneInstances
|
|
|
305
305
|
for (const rawField of Object.keys(data ?? {})) {
|
|
306
306
|
const field = rename[rawField] ?? rawField;
|
|
307
307
|
if (!(field in schema)) {
|
|
308
|
-
offenders.push(
|
|
309
|
-
`${compName}.${rawField} (localId ${node.localId as unknown as number})`,
|
|
310
|
-
);
|
|
308
|
+
offenders.push(`${compName}.${rawField} (scene entity key)`);
|
|
311
309
|
}
|
|
312
310
|
}
|
|
313
311
|
}
|
|
@@ -330,7 +328,9 @@ describe('game-capability-lab API contract (regression for `world.sceneInstances
|
|
|
330
328
|
type TemplatePackAsset = {
|
|
331
329
|
guid?: string;
|
|
332
330
|
kind: string;
|
|
333
|
-
payload?: {
|
|
331
|
+
payload?: {
|
|
332
|
+
entities?: Record<string, { components: { MeshRenderer?: { materials?: number[] } } }>;
|
|
333
|
+
};
|
|
334
334
|
refs?: string[];
|
|
335
335
|
};
|
|
336
336
|
const pack = JSON.parse(readFileSync(packPath, 'utf-8')) as {
|
|
@@ -346,7 +346,7 @@ describe('game-capability-lab API contract (regression for `world.sceneInstances
|
|
|
346
346
|
if (scene === undefined) return;
|
|
347
347
|
|
|
348
348
|
const referencedMaterialGuids = new Set(
|
|
349
|
-
(scene.payload?.entities ??
|
|
349
|
+
Object.values(scene.payload?.entities ?? {}).flatMap((entity) =>
|
|
350
350
|
(entity.components.MeshRenderer?.materials ?? []).map((index) => scene.refs?.[index]),
|
|
351
351
|
),
|
|
352
352
|
);
|
|
@@ -369,7 +369,7 @@ describe('game-capability-lab API contract (regression for `world.sceneInstances
|
|
|
369
369
|
const reg = new AssetRegistry(makeMockShaderRegistry());
|
|
370
370
|
const sceneGuid = AssetGuid.parse(SCENE_GUID);
|
|
371
371
|
if (!sceneGuid.ok) throw new Error('parse');
|
|
372
|
-
const payload: SceneAsset = { kind: 'scene', entities:
|
|
372
|
+
const payload: SceneAsset = { kind: 'scene', entities: {} };
|
|
373
373
|
const cataloged = reg.catalog<SceneAsset>(sceneGuid.value, payload);
|
|
374
374
|
expect(cataloged.ok).toBe(true);
|
|
375
375
|
if (!cataloged.ok) return;
|
|
@@ -407,10 +407,10 @@ type __MergedKeep =
|
|
|
407
407
|
__mockTag: 'gpu-device',
|
|
408
408
|
lost,
|
|
409
409
|
features: new Set(),
|
|
410
|
-
// Model a native WebGPU device:
|
|
411
|
-
// swap-chain format selected by the storage-buffer path.
|
|
412
|
-
// object describes a downlevel device and correctly selects the
|
|
413
|
-
// fallback instead.
|
|
410
|
+
// Model a native WebGPU device: this test exercises the preferred
|
|
411
|
+
// swap-chain format selected by the storage-buffer path. An empty
|
|
412
|
+
// limits object describes a downlevel device and correctly selects the
|
|
413
|
+
// rgba fallback instead.
|
|
414
414
|
limits: {
|
|
415
415
|
maxStorageBuffersPerShaderStage: 8,
|
|
416
416
|
},
|
|
@@ -634,14 +634,17 @@ type __MergedKeep =
|
|
|
634
634
|
// test if the format is RENDER_ATTACHMENT-renderable. NOT an HDR
|
|
635
635
|
// colour attachment -- a one-shot probe destroyed immediately. Filter
|
|
636
636
|
// by label so the AC-03(c) lazy-HDR contract remains testable.
|
|
637
|
+
// The neutral normal-map fallback uses float16 to represent 0.5 exactly;
|
|
638
|
+
// it is a 1x1 sampled material input, not a scene HDR attachment.
|
|
637
639
|
const hdrTextureCalls = log.records.filter(
|
|
638
640
|
(r) =>
|
|
639
641
|
r.type === 'createTexture' &&
|
|
640
642
|
r.format === 'rgba16float' &&
|
|
643
|
+
r.label !== 'fallback-normal-1x1' &&
|
|
641
644
|
!(r.label?.startsWith('skylight-fallback-') ?? false) &&
|
|
642
645
|
!(r.label?.startsWith('forgeax-caps-probe-') ?? false),
|
|
643
646
|
);
|
|
644
|
-
expect(hdrTextureCalls.
|
|
647
|
+
expect(hdrTextureCalls.map((record) => record.label)).toEqual([]);
|
|
645
648
|
});
|
|
646
649
|
|
|
647
650
|
it('row 4: pbr + unlit engine modules compile at boot (tonemap defers to unified channel)', async () => {
|
|
@@ -519,10 +519,13 @@ type __MergedKeep =
|
|
|
519
519
|
},
|
|
520
520
|
getPipelineState: () => null,
|
|
521
521
|
assets: {
|
|
522
|
+
catalogEpoch: 0,
|
|
522
523
|
get: () => ({ ok: false, error: { code: 'asset-not-registered' } }),
|
|
523
524
|
getMeshGpuHandles: () => undefined,
|
|
524
525
|
getTextureGpuView: () => undefined,
|
|
525
526
|
},
|
|
527
|
+
deviceScope: { generation: 0 },
|
|
528
|
+
gpuStore: { meshResidencyEpoch: 0 },
|
|
526
529
|
errorRegistry: {
|
|
527
530
|
fire: () => undefined,
|
|
528
531
|
},
|