@forgeax/engine-render 0.1.24 → 0.1.25
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 +100 -1
- package/dist/assembly/factory.d.ts +1 -1
- package/dist/assembly/factory.d.ts.map +1 -1
- package/dist/assembly/material/surface-projection.d.ts +24 -0
- package/dist/assembly/material/surface-projection.d.ts.map +1 -0
- package/dist/assembly/material-shader-policy.d.ts +20 -0
- package/dist/assembly/material-shader-policy.d.ts.map +1 -1
- package/dist/assembly/webgpu-ready.d.ts.map +1 -1
- package/dist/assembly/webgpu-renderer.d.ts +1 -1
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
- package/dist/assembly/webgpu-vertex-layouts.d.ts +89 -0
- package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -0
- package/dist/assets/asset-decoders.d.ts.map +1 -1
- package/dist/authoring.mjs +1 -1
- package/dist/{chunk-Q752SHI5.mjs → chunk-ADHHVYLW.mjs} +3 -3
- package/dist/chunk-ADHHVYLW.mjs.map +1 -0
- package/dist/{chunk-TFH5I7JJ.mjs → chunk-E34VL5VI.mjs} +3 -3
- package/dist/{chunk-TFH5I7JJ.mjs.map → chunk-E34VL5VI.mjs.map} +1 -1
- package/dist/{chunk-KZFTK6DO.mjs → chunk-HKXTW355.mjs} +7 -4
- package/dist/chunk-HKXTW355.mjs.map +1 -0
- package/dist/{chunk-YKB5DQV7.mjs → chunk-JSEDTZYS.mjs} +87 -19
- package/dist/chunk-JSEDTZYS.mjs.map +1 -0
- package/dist/{chunk-UQYS7FRM.mjs → chunk-KM2NOX2I.mjs} +4 -4
- package/dist/{chunk-UQYS7FRM.mjs.map → chunk-KM2NOX2I.mjs.map} +1 -1
- package/dist/{chunk-Z3YYH2MB.mjs → chunk-N3RT2EUR.mjs} +94 -41
- package/dist/chunk-N3RT2EUR.mjs.map +1 -0
- package/dist/{chunk-YRUYGA4A.mjs → chunk-TCLDX7SZ.mjs} +559 -138
- package/dist/chunk-TCLDX7SZ.mjs.map +1 -0
- package/dist/{chunk-RR5L4SRS.mjs → chunk-ZZ4YQ474.mjs} +2 -2
- package/dist/{chunk-RR5L4SRS.mjs.map → chunk-ZZ4YQ474.mjs.map} +1 -1
- package/dist/components/instances.d.ts +13 -31
- package/dist/components/instances.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +865 -142
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/gpu-driven/production-raster.d.ts +6 -1
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/gpu-scene.d.ts +4 -0
- package/dist/gpu-scene.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +27 -9
- package/dist/index.mjs.map +1 -1
- package/dist/instance-buffer-cache.d.ts +7 -1
- package/dist/instance-buffer-cache.d.ts.map +1 -1
- package/dist/instances.d.ts +101 -0
- package/dist/instances.d.ts.map +1 -0
- package/dist/internal.mjs +5 -5
- package/dist/materials.d.ts +63 -2
- package/dist/materials.d.ts.map +1 -1
- package/dist/pipeline-builder.d.ts.map +1 -1
- package/dist/pipeline-spec.d.ts.map +1 -1
- package/dist/record/frame-snapshot.d.ts +13 -2
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts +22 -3
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +2 -1
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts +11 -1
- package/dist/record/shadow-pass.d.ts.map +1 -1
- package/dist/recovery/render-system-candidate.d.ts +1 -1
- package/dist/recovery/render-system-candidate.d.ts.map +1 -1
- package/dist/render-contract.d.ts +4 -0
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts +4 -0
- package/dist/render-system-extract-tail.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +25 -1
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +4 -1
- package/dist/render-system.d.ts.map +1 -1
- package/dist/scene/render-scene-types.d.ts +2 -0
- package/dist/scene/render-scene-types.d.ts.map +1 -1
- package/dist/scene/render-scene.d.ts +12 -1
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/dist/systems/skin-palette-allocator.d.ts +6 -0
- package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
- package/dist/temporal/index.mjs +5 -5
- package/package.json +20 -20
- package/src/__tests__/gpu-scene.unit.test.ts +38 -2
- package/src/__tests__/instances-store.unit.test.ts +46 -0
- package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
- package/src/__tests__/material-contract-inventory.unit.test.ts +5 -13
- package/src/__tests__/material-pass-policy.unit.test.ts +69 -0
- package/src/__tests__/material-snapshot-mutation.integration.test.ts +69 -0
- package/src/__tests__/material-surface-authoring.test-d.ts +62 -0
- package/src/__tests__/materials-transmission-validation.unit.test.ts +24 -0
- package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +0 -4
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +42 -0
- package/src/__tests__/skin-motion-regression.unit.test.ts +33 -0
- package/src/__tests__/skinned-shadow-caster.test.ts +30 -2
- package/src/__tests__/surface-adjacent-regressions.integration.test.ts +60 -0
- package/src/__tests__/surface-standard-pipeline.browser.test.ts +20 -0
- package/src/__tests__/surface-standard-pipeline.dawn.test.ts +20 -0
- package/src/__tests__/surface-standard-pipeline.fixture.ts +132 -0
- package/src/__tests__/surface-variant-provenance.integration.test.ts +33 -0
- package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +1 -1
- package/src/assembly/factory.ts +2 -0
- package/src/assembly/material/__tests__/standard-layer-plan-projection.integration.test.ts +18 -0
- package/src/assembly/material/__tests__/surface-pass-projection.integration.test.ts +62 -0
- package/src/assembly/material/surface-projection.ts +90 -0
- package/src/assembly/material-shader-policy.ts +88 -10
- package/src/assembly/recovery/renderer-recover.ts +1 -1
- package/src/assembly/webgpu-ready.ts +3 -16
- package/src/assembly/webgpu-renderer.ts +90 -158
- package/src/assembly/webgpu-vertex-layouts.ts +132 -0
- package/src/assets/asset-decoders.ts +27 -4
- package/src/components/instances.ts +14 -197
- package/src/gpu-driven/production-raster.ts +34 -1
- package/src/gpu-scene.ts +84 -29
- package/src/index.ts +1 -0
- package/src/instance-buffer-cache.ts +14 -1
- package/src/instances.ts +288 -0
- package/src/materials.ts +107 -12
- package/src/pipeline-builder.ts +14 -10
- package/src/pipeline-spec.ts +6 -14
- package/src/record/frame-snapshot.ts +19 -2
- package/src/record/frame.ts +31 -2
- package/src/record/main-pass-geometry.ts +498 -59
- package/src/record/main-pass-sprite-draws.ts +1 -1
- package/src/record/render-context.ts +4 -0
- package/src/record/shadow-pass.ts +46 -99
- package/src/recovery/render-system-candidate.ts +41 -4
- package/src/render-contract.ts +4 -0
- package/src/render-system-extract-tail.ts +107 -51
- package/src/render-system-extract.ts +53 -26
- package/src/render-system.ts +40 -5
- package/src/scene/render-scene-types.ts +2 -0
- package/src/scene/render-scene.ts +579 -36
- package/src/systems/skin-palette-allocator.ts +129 -7
- package/src/transmission/__tests__/surface-compatibility.integration.test.ts +32 -0
- package/dist/chunk-KZFTK6DO.mjs.map +0 -1
- package/dist/chunk-Q752SHI5.mjs.map +0 -1
- package/dist/chunk-YKB5DQV7.mjs.map +0 -1
- package/dist/chunk-YRUYGA4A.mjs.map +0 -1
- package/dist/chunk-Z3YYH2MB.mjs.map +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { Query, World } from '@forgeax/engine-ecs';
|
|
1
|
+
import type { EntityHandle, Query, World } from '@forgeax/engine-ecs';
|
|
2
2
|
import { componentId } from '@forgeax/engine-ecs/internal';
|
|
3
3
|
import type { RenderReadLease, RenderReadVersion } from '@forgeax/engine-ecs/projection';
|
|
4
4
|
import { box3, frustum, mat4 } from '@forgeax/engine-math';
|
|
5
5
|
import { type RhiDevice, RhiError } from '@forgeax/engine-rhi';
|
|
6
|
-
import { ChildOf, GlobalTransform, Transform } from '@forgeax/engine-scene';
|
|
6
|
+
import { ChildOf, GlobalTransform, MorphWeights, Transform } from '@forgeax/engine-scene';
|
|
7
7
|
import { Skin } from '@forgeax/engine-skinning';
|
|
8
8
|
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
9
9
|
import {
|
|
@@ -12,15 +12,18 @@ import {
|
|
|
12
12
|
Instances,
|
|
13
13
|
Layer,
|
|
14
14
|
LightProbe,
|
|
15
|
+
Lines,
|
|
15
16
|
MeshFilter,
|
|
16
17
|
MeshRenderer,
|
|
17
18
|
MotionBlur,
|
|
18
19
|
PointLight,
|
|
19
20
|
PointLightShadow,
|
|
21
|
+
Points,
|
|
20
22
|
PostProcessParams,
|
|
21
23
|
ReflectionProbe,
|
|
22
24
|
SkyboxBackground,
|
|
23
25
|
Skylight,
|
|
26
|
+
SortKey,
|
|
24
27
|
SpotLight,
|
|
25
28
|
SpriteInstances,
|
|
26
29
|
SpriteRegionOverride,
|
|
@@ -34,6 +37,7 @@ import {
|
|
|
34
37
|
} from '../gpu-driven/batch-topology';
|
|
35
38
|
import { GpuScene } from '../gpu-scene';
|
|
36
39
|
import type { PersistentRenderSceneInspection } from '../inspection-types';
|
|
40
|
+
import type { InstanceProjectionStore } from '../instances';
|
|
37
41
|
import { fingerprintNumericArray, InstanceBoundsCache } from '../instances-derived-bounds';
|
|
38
42
|
import type { PointsLinesInspection } from '../points-lines/inspection';
|
|
39
43
|
import type { PointsLinesRetainedSnapshot } from '../points-lines/snapshot';
|
|
@@ -53,6 +57,7 @@ import type {
|
|
|
53
57
|
RenderableSnapshot,
|
|
54
58
|
RenderableTemporalSnapshot,
|
|
55
59
|
} from '../render-system-extract';
|
|
60
|
+
import type { SkinPaletteAllocator } from '../systems/skin-palette-allocator';
|
|
56
61
|
import {
|
|
57
62
|
PersistentTransmissionDemandProjection,
|
|
58
63
|
type TransmissionDemand,
|
|
@@ -97,6 +102,7 @@ interface PendingIdentity {
|
|
|
97
102
|
readonly initial: RenderSceneSlot | undefined;
|
|
98
103
|
current: RenderableSnapshot | undefined;
|
|
99
104
|
removed: boolean;
|
|
105
|
+
contentChanged: boolean;
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
interface WorldTransformSpan {
|
|
@@ -107,6 +113,16 @@ interface WorldTransformSpan {
|
|
|
107
113
|
|
|
108
114
|
type GlobalTransformChangeQuery = Query<readonly [typeof GlobalTransform]>;
|
|
109
115
|
|
|
116
|
+
/** The normal extract request used by the persistent scene owner. */
|
|
117
|
+
export type PersistentRenderCandidateRequest =
|
|
118
|
+
| 'full'
|
|
119
|
+
| 'none'
|
|
120
|
+
| {
|
|
121
|
+
readonly kind: 'partial';
|
|
122
|
+
/** Entity keys are local to the corresponding World entry. */
|
|
123
|
+
readonly entitiesByWorld: readonly (ReadonlySet<number> | undefined)[];
|
|
124
|
+
};
|
|
125
|
+
|
|
110
126
|
export interface RenderSceneSubmissionCapture {
|
|
111
127
|
readonly revision: number;
|
|
112
128
|
readonly slots: readonly {
|
|
@@ -188,6 +204,9 @@ function ownSnapshot(snapshot: RenderableSnapshot): RenderableSnapshot {
|
|
|
188
204
|
...(snapshot.pointsLines === undefined
|
|
189
205
|
? {}
|
|
190
206
|
: { pointsLines: ownPointsLinesSnapshot(snapshot.pointsLines) }),
|
|
207
|
+
...(snapshot.skinJointEntities === undefined
|
|
208
|
+
? {}
|
|
209
|
+
: { skinJointEntities: [...snapshot.skinJointEntities] }),
|
|
191
210
|
};
|
|
192
211
|
}
|
|
193
212
|
|
|
@@ -357,6 +376,7 @@ export class RenderScene {
|
|
|
357
376
|
initial,
|
|
358
377
|
current: initial?.snapshot,
|
|
359
378
|
removed: false,
|
|
379
|
+
contentChanged: false,
|
|
360
380
|
};
|
|
361
381
|
entities.set(entityKey, pending);
|
|
362
382
|
pendingOrder.push(pending);
|
|
@@ -368,6 +388,7 @@ export class RenderScene {
|
|
|
368
388
|
const { worldId, entityKey } = operation.snapshot;
|
|
369
389
|
const pending = pendingFor(worldId, entityKey);
|
|
370
390
|
pending.current = ownSnapshot(operation.snapshot);
|
|
391
|
+
pending.contentChanged = true;
|
|
371
392
|
continue;
|
|
372
393
|
}
|
|
373
394
|
const pending = pendingFor(operation.worldId, operation.entityKey);
|
|
@@ -389,6 +410,7 @@ export class RenderScene {
|
|
|
389
410
|
let recreated = 0;
|
|
390
411
|
const createdSlots: RenderSceneSlot[] = [];
|
|
391
412
|
const updatedSlots: RenderSceneSlot[] = [];
|
|
413
|
+
const contentUpdatedSlots: RenderSceneSlot[] = [];
|
|
392
414
|
const removedSlots: RenderSceneRecord[] = [];
|
|
393
415
|
const recreatedSlots: RenderSceneSlot[] = [];
|
|
394
416
|
for (const pending of pendingOrder) {
|
|
@@ -426,6 +448,7 @@ export class RenderScene {
|
|
|
426
448
|
this.indexMaterials(updatedSlot);
|
|
427
449
|
this.dirtySinceSubmitted.add(updatedSlot.slot);
|
|
428
450
|
updatedSlots.push(updatedSlot);
|
|
451
|
+
if (pending.contentChanged) contentUpdatedSlots.push(updatedSlot);
|
|
429
452
|
updated += 1;
|
|
430
453
|
}
|
|
431
454
|
|
|
@@ -442,6 +465,7 @@ export class RenderScene {
|
|
|
442
465
|
ignoredLateUpdates,
|
|
443
466
|
createdSlots,
|
|
444
467
|
updatedSlots,
|
|
468
|
+
contentUpdatedSlots,
|
|
445
469
|
removedSlots,
|
|
446
470
|
recreatedSlots,
|
|
447
471
|
resynced: 0,
|
|
@@ -866,7 +890,7 @@ export class RenderScene {
|
|
|
866
890
|
entityKey: snapshot.entityKey,
|
|
867
891
|
meshGeneration: fingerprintNumericArray(snapshot.localAabb) ^ (snapshot.assetHandle >>> 0),
|
|
868
892
|
transformGeneration: fingerprintNumericArray(snapshot.transform.world),
|
|
869
|
-
matrixGeneration: snapshot.instances.archVersion,
|
|
893
|
+
matrixGeneration: snapshot.instances.revision ?? snapshot.instances.archVersion,
|
|
870
894
|
meshAabb: snapshot.localAabb,
|
|
871
895
|
entityWorld: snapshot.transform.world,
|
|
872
896
|
transforms: snapshot.instances.transforms,
|
|
@@ -890,6 +914,9 @@ export interface PersistentRenderSceneOptions {
|
|
|
890
914
|
readonly onGpuError?: ((error: RhiError) => void) | undefined;
|
|
891
915
|
readonly onSharedRefMutation?: ((worldId: number, handle: number) => void) | undefined;
|
|
892
916
|
readonly configuredQueryBudget?: number | undefined;
|
|
917
|
+
/** Rebuildable projection of World-owned instance matrices. */
|
|
918
|
+
readonly instanceCollections?: InstanceProjectionStore | undefined;
|
|
919
|
+
readonly getSkinPaletteAllocator?: (() => SkinPaletteAllocator | null) | undefined;
|
|
893
920
|
}
|
|
894
921
|
|
|
895
922
|
export interface PersistentGpuDrivenState {
|
|
@@ -971,7 +998,7 @@ interface PersistentCompositionEntry {
|
|
|
971
998
|
readonly projection: RenderScene;
|
|
972
999
|
readonly topology: BatchTopology;
|
|
973
1000
|
readonly transmissionDemand: PersistentTransmissionDemandProjection;
|
|
974
|
-
|
|
1001
|
+
structure: Pick<
|
|
975
1002
|
ExtractedFrame,
|
|
976
1003
|
| 'dispatch'
|
|
977
1004
|
| 'visibilityStats'
|
|
@@ -984,15 +1011,24 @@ interface PersistentCompositionEntry {
|
|
|
984
1011
|
readonly readVersions: RenderReadVersion[];
|
|
985
1012
|
readonly leaseIdentities: readonly string[];
|
|
986
1013
|
readonly transformQueries: readonly GlobalTransformChangeQuery[];
|
|
1014
|
+
readonly renderChangeQueries: readonly ReadonlyMap<number, Query>[];
|
|
1015
|
+
readonly dispatchBySlot: Map<number, readonly DispatchEntry[]>;
|
|
1016
|
+
dispatchRevision: number;
|
|
1017
|
+
dispatchCache:
|
|
1018
|
+
| { readonly revision: number; readonly value: readonly DispatchEntry[] }
|
|
1019
|
+
| undefined;
|
|
1020
|
+
readonly skinConsumersByJoint: Map<string, Set<number>>;
|
|
1021
|
+
/** Renderable identities whose partial producer still needs a successful publish. */
|
|
1022
|
+
readonly pendingRenderableEntitiesByWorld: Set<number>[];
|
|
1023
|
+
/** Slots retained for recovery but excluded from direct and indirect submission. */
|
|
1024
|
+
readonly unavailableSlots: Set<number>;
|
|
987
1025
|
catalogEpoch: number;
|
|
988
|
-
readonly rigidOnly: boolean;
|
|
989
1026
|
readonly probeProjection: ProbeBlendSceneProjection;
|
|
990
1027
|
temporalDemanded: boolean;
|
|
991
1028
|
gpuDrivenSceneRequired: boolean;
|
|
992
1029
|
}
|
|
993
1030
|
|
|
994
1031
|
const RENDER_RELEVANT_COMPONENT_IDS = new Set([
|
|
995
|
-
componentId(Transform),
|
|
996
1032
|
componentId(GlobalTransform),
|
|
997
1033
|
componentId(ChildOf),
|
|
998
1034
|
componentId(MeshFilter),
|
|
@@ -1003,6 +1039,10 @@ const RENDER_RELEVANT_COMPONENT_IDS = new Set([
|
|
|
1003
1039
|
componentId(Skin),
|
|
1004
1040
|
componentId(Layer),
|
|
1005
1041
|
componentId(Visibility),
|
|
1042
|
+
componentId(MorphWeights),
|
|
1043
|
+
componentId(Points),
|
|
1044
|
+
componentId(Lines),
|
|
1045
|
+
componentId(SortKey),
|
|
1006
1046
|
componentId(Camera),
|
|
1007
1047
|
componentId(MotionBlur),
|
|
1008
1048
|
componentId(DirectionalLight),
|
|
@@ -1017,6 +1057,41 @@ const RENDER_RELEVANT_COMPONENT_IDS = new Set([
|
|
|
1017
1057
|
componentId(VolumetricFog),
|
|
1018
1058
|
]);
|
|
1019
1059
|
|
|
1060
|
+
// These components can change the retained renderable source. Other relevant
|
|
1061
|
+
// components affect frame resources and still require a composition rebuild;
|
|
1062
|
+
// they must not cause a renderable-only partial extraction to silently retain
|
|
1063
|
+
// stale lights, cameras, or environment facts.
|
|
1064
|
+
const RENDERABLE_SOURCE_COMPONENTS = [
|
|
1065
|
+
GlobalTransform,
|
|
1066
|
+
ChildOf,
|
|
1067
|
+
MeshFilter,
|
|
1068
|
+
MeshRenderer,
|
|
1069
|
+
Instances,
|
|
1070
|
+
SpriteInstances,
|
|
1071
|
+
SpriteRegionOverride,
|
|
1072
|
+
Skin,
|
|
1073
|
+
Layer,
|
|
1074
|
+
Visibility,
|
|
1075
|
+
MorphWeights,
|
|
1076
|
+
Points,
|
|
1077
|
+
Lines,
|
|
1078
|
+
SortKey,
|
|
1079
|
+
] as const;
|
|
1080
|
+
const RENDERABLE_SOURCE_COMPONENT_IDS = new Set(
|
|
1081
|
+
RENDERABLE_SOURCE_COMPONENTS.map((component) => componentId(component)),
|
|
1082
|
+
);
|
|
1083
|
+
const VISIBILITY_DEPENDENCY_COMPONENT_IDS = new Set([
|
|
1084
|
+
componentId(ChildOf),
|
|
1085
|
+
componentId(Visibility),
|
|
1086
|
+
]);
|
|
1087
|
+
const STRUCTURAL_MEMBERSHIP_SCAN_COMPONENT_IDS = new Set([
|
|
1088
|
+
componentId(ChildOf),
|
|
1089
|
+
componentId(Layer),
|
|
1090
|
+
componentId(Visibility),
|
|
1091
|
+
componentId(SpriteRegionOverride),
|
|
1092
|
+
componentId(SortKey),
|
|
1093
|
+
]);
|
|
1094
|
+
|
|
1020
1095
|
function createGlobalTransformChangeQuery(world: World): GlobalTransformChangeQuery {
|
|
1021
1096
|
const result = world.query({ read: [GlobalTransform], changed: [GlobalTransform] });
|
|
1022
1097
|
if (!result.ok) throw result.error;
|
|
@@ -1027,6 +1102,149 @@ function createGlobalTransformChangeQuery(world: World): GlobalTransformChangeQu
|
|
|
1027
1102
|
return query;
|
|
1028
1103
|
}
|
|
1029
1104
|
|
|
1105
|
+
function createRenderChangeQueries(world: World): ReadonlyMap<number, Query> {
|
|
1106
|
+
const queries = new Map<number, Query>();
|
|
1107
|
+
for (const component of RENDERABLE_SOURCE_COMPONENTS) {
|
|
1108
|
+
const result = world.query({ read: [component], changed: [component] });
|
|
1109
|
+
if (!result.ok) throw result.error;
|
|
1110
|
+
const query = result.value as Query;
|
|
1111
|
+
// The initial full extraction has already consumed this change evidence.
|
|
1112
|
+
// Keep the query compiled so subsequent frames iterate only rows whose
|
|
1113
|
+
// component version changed.
|
|
1114
|
+
for (const _span of query.spans().unwrap()) {
|
|
1115
|
+
// Establish the observation baseline.
|
|
1116
|
+
}
|
|
1117
|
+
queries.set(componentId(component), query);
|
|
1118
|
+
}
|
|
1119
|
+
return queries;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function collectRenderableEntities(world: World): ReadonlySet<number> {
|
|
1123
|
+
const result = new Set<number>();
|
|
1124
|
+
const query = world.query({ read: [MeshRenderer], with: [Transform, MeshFilter] }).unwrap();
|
|
1125
|
+
for (const row of query) result.add(row.entity as number);
|
|
1126
|
+
return result;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function sourceMembershipChanged(world: World, slot: RenderSceneSlot): boolean {
|
|
1130
|
+
const entity = slot.entityKey as EntityHandle;
|
|
1131
|
+
const snapshot = slot.snapshot;
|
|
1132
|
+
return (
|
|
1133
|
+
!world.hasComponent(entity, GlobalTransform) ||
|
|
1134
|
+
(snapshot.instances !== undefined) !== world.hasComponent(entity, Instances) ||
|
|
1135
|
+
(snapshot.spriteInstances !== undefined) !== world.hasComponent(entity, SpriteInstances) ||
|
|
1136
|
+
(snapshot.skin !== undefined) !== world.hasComponent(entity, Skin) ||
|
|
1137
|
+
(snapshot.morph !== undefined) !== world.hasComponent(entity, MorphWeights) ||
|
|
1138
|
+
(snapshot.pointsLines?.component === 'Points') !== world.hasComponent(entity, Points) ||
|
|
1139
|
+
(snapshot.pointsLines?.component === 'Lines') !== world.hasComponent(entity, Lines)
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function skinJointKey(worldId: number, jointEntity: number): string {
|
|
1144
|
+
return `${worldId}:${jointEntity}`;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
function addSkinConsumer(index: Map<string, Set<number>>, snapshot: RenderableSnapshot): void {
|
|
1148
|
+
for (const jointEntity of snapshot.skinJointEntities ?? []) {
|
|
1149
|
+
const key = skinJointKey(snapshot.worldId, jointEntity);
|
|
1150
|
+
let consumers = index.get(key);
|
|
1151
|
+
if (consumers === undefined) {
|
|
1152
|
+
consumers = new Set<number>();
|
|
1153
|
+
index.set(key, consumers);
|
|
1154
|
+
}
|
|
1155
|
+
consumers.add(snapshot.entityKey);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function removeSkinConsumer(index: Map<string, Set<number>>, snapshot: RenderableSnapshot): void {
|
|
1160
|
+
for (const jointEntity of snapshot.skinJointEntities ?? []) {
|
|
1161
|
+
const key = skinJointKey(snapshot.worldId, jointEntity);
|
|
1162
|
+
const consumers = index.get(key);
|
|
1163
|
+
consumers?.delete(snapshot.entityKey);
|
|
1164
|
+
if (consumers?.size === 0) index.delete(key);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function dispatchEntriesBySlot(
|
|
1169
|
+
frame: ExtractedFrame,
|
|
1170
|
+
projection: RenderScene,
|
|
1171
|
+
): Map<number, readonly DispatchEntry[]> {
|
|
1172
|
+
const bySlot = new Map<number, DispatchEntry[]>();
|
|
1173
|
+
const byRenderable = new Map<number, DispatchEntry[]>();
|
|
1174
|
+
for (const entry of frame.dispatch) {
|
|
1175
|
+
const list = byRenderable.get(entry.renderableIndex);
|
|
1176
|
+
if (list === undefined) byRenderable.set(entry.renderableIndex, [entry]);
|
|
1177
|
+
else list.push(entry);
|
|
1178
|
+
}
|
|
1179
|
+
for (let index = 0; index < frame.renderables.length; index += 1) {
|
|
1180
|
+
const renderable = frame.renderables[index];
|
|
1181
|
+
if (renderable === undefined) continue;
|
|
1182
|
+
const slot = projection.slot(renderable.worldId, renderable.entityKey);
|
|
1183
|
+
if (slot === undefined) continue;
|
|
1184
|
+
bySlot.set(slot.slot, byRenderable.get(index) ?? []);
|
|
1185
|
+
}
|
|
1186
|
+
return bySlot;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function dispatchForProjection(
|
|
1190
|
+
dispatchBySlot: ReadonlyMap<number, readonly DispatchEntry[]>,
|
|
1191
|
+
projection: RenderScene,
|
|
1192
|
+
unavailableSlots: ReadonlySet<number>,
|
|
1193
|
+
): readonly DispatchEntry[] {
|
|
1194
|
+
const dispatch: DispatchEntry[] = [];
|
|
1195
|
+
const renderables = projection.materialize();
|
|
1196
|
+
for (let renderableIndex = 0; renderableIndex < renderables.length; renderableIndex += 1) {
|
|
1197
|
+
const renderable = renderables[renderableIndex];
|
|
1198
|
+
if (renderable === undefined) continue;
|
|
1199
|
+
const slot = projection.slot(renderable.worldId, renderable.entityKey);
|
|
1200
|
+
if (slot === undefined) continue;
|
|
1201
|
+
if (unavailableSlots.has(slot.slot)) continue;
|
|
1202
|
+
for (const entry of dispatchBySlot.get(slot.slot) ?? []) {
|
|
1203
|
+
dispatch.push({ ...entry, renderableIndex });
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return dispatch;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function topologyDeltaForAvailable(
|
|
1210
|
+
delta: RenderSceneApplyResult,
|
|
1211
|
+
unavailableSlots: ReadonlySet<number>,
|
|
1212
|
+
newlyUnavailable: readonly RenderSceneSlot[],
|
|
1213
|
+
): RenderSceneApplyResult {
|
|
1214
|
+
const updatedSlots = delta.updatedSlots.filter((slot) => !unavailableSlots.has(slot.slot));
|
|
1215
|
+
const contentUpdatedSlots = delta.contentUpdatedSlots?.filter(
|
|
1216
|
+
(slot) => !unavailableSlots.has(slot.slot),
|
|
1217
|
+
);
|
|
1218
|
+
return {
|
|
1219
|
+
...delta,
|
|
1220
|
+
updatedSlots: [
|
|
1221
|
+
...updatedSlots,
|
|
1222
|
+
...newlyUnavailable.map((slot) => ({
|
|
1223
|
+
...slot,
|
|
1224
|
+
snapshot: { ...slot.snapshot, gpuDrivenDraws: [] },
|
|
1225
|
+
})),
|
|
1226
|
+
],
|
|
1227
|
+
...(contentUpdatedSlots === undefined ? {} : { contentUpdatedSlots }),
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
function addSharedRefConsumers(
|
|
1232
|
+
projection: RenderScene,
|
|
1233
|
+
worldId: number,
|
|
1234
|
+
handle: number,
|
|
1235
|
+
target: Set<number>,
|
|
1236
|
+
): void {
|
|
1237
|
+
for (const slot of projection.slotsSnapshot()) {
|
|
1238
|
+
if (
|
|
1239
|
+
slot.worldId === worldId &&
|
|
1240
|
+
(slot.snapshot.assetHandle === handle ||
|
|
1241
|
+
slot.snapshot.materials.some((material) => material.materialHandle === handle))
|
|
1242
|
+
) {
|
|
1243
|
+
target.add(slot.entityKey);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1030
1248
|
function frameDemandsTemporal(frame: ExtractedFrame): boolean {
|
|
1031
1249
|
const camera = frame.cameras[0];
|
|
1032
1250
|
return camera?.antialias === 'taa' || (camera?.motionBlur?.shutterAngle ?? 0) > 0;
|
|
@@ -1090,6 +1308,10 @@ function cullPersistentFrame(
|
|
|
1090
1308
|
for (let index = 0; index < candidates.length; index += 1) {
|
|
1091
1309
|
const candidate = candidates[index];
|
|
1092
1310
|
if (candidate === undefined) continue;
|
|
1311
|
+
if (candidate.authorVisible === false) {
|
|
1312
|
+
culled += 1;
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1093
1315
|
let isVisible = true;
|
|
1094
1316
|
const candidateBounds = boundsOf(candidate, index);
|
|
1095
1317
|
if (candidateBounds !== undefined) {
|
|
@@ -1395,7 +1617,7 @@ export class PersistentRenderScene {
|
|
|
1395
1617
|
bounds.length >= 6 &&
|
|
1396
1618
|
[...bounds].every((value) => Number.isFinite(value));
|
|
1397
1619
|
draw = decideVisibility({
|
|
1398
|
-
authorVisible:
|
|
1620
|
+
authorVisible: renderable.authorVisible !== false,
|
|
1399
1621
|
validBounds,
|
|
1400
1622
|
frustumVisible: true,
|
|
1401
1623
|
lodReady: true,
|
|
@@ -1499,7 +1721,7 @@ export class PersistentRenderScene {
|
|
|
1499
1721
|
worlds: readonly World[],
|
|
1500
1722
|
owner: { readonly cameraOwner: number; readonly resourceOwner: number },
|
|
1501
1723
|
catalogEpoch: number,
|
|
1502
|
-
buildCandidateFrame: (
|
|
1724
|
+
buildCandidateFrame: (request: PersistentRenderCandidateRequest) => ExtractedFrame,
|
|
1503
1725
|
leases?: readonly RenderReadLease[],
|
|
1504
1726
|
): ExtractedFrame {
|
|
1505
1727
|
this.worldEntitiesScanned = 0;
|
|
@@ -1544,11 +1766,16 @@ export class PersistentRenderScene {
|
|
|
1544
1766
|
preserveVisibilityOnReorder,
|
|
1545
1767
|
);
|
|
1546
1768
|
}
|
|
1547
|
-
if (!entry.rigidOnly) {
|
|
1548
|
-
return this.rebuildComposition(worlds, owner, catalogEpoch, buildCandidateFrame, leases);
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
1769
|
let requiresRebuild = false;
|
|
1770
|
+
const changedEntitiesByWorld = worlds.map(
|
|
1771
|
+
(_, worldId) => new Set(entry.pendingRenderableEntitiesByWorld[worldId] ?? []),
|
|
1772
|
+
);
|
|
1773
|
+
const nextReadVersions = entry.readVersions.slice();
|
|
1774
|
+
const structureChangedWorlds = new Set<number>();
|
|
1775
|
+
const structuralComponentIdsByWorld = worlds.map(() => new Set<number>());
|
|
1776
|
+
let refreshStructureFacts = false;
|
|
1777
|
+
let currentRenderableEntitiesByWorld: readonly (ReadonlySet<number> | undefined)[] | undefined;
|
|
1778
|
+
const removedSlots: RenderSceneSlot[] = [];
|
|
1552
1779
|
for (let worldId = 0; worldId < worlds.length; worldId += 1) {
|
|
1553
1780
|
const world = worlds[worldId];
|
|
1554
1781
|
if (world === undefined) continue;
|
|
@@ -1569,35 +1796,294 @@ export class PersistentRenderScene {
|
|
|
1569
1796
|
requiresRebuild = true;
|
|
1570
1797
|
break;
|
|
1571
1798
|
}
|
|
1572
|
-
if (read.status
|
|
1573
|
-
|
|
1574
|
-
|
|
1799
|
+
if (read.status === 'rebuild') {
|
|
1800
|
+
structureChangedWorlds.add(worldId);
|
|
1801
|
+
refreshStructureFacts = true;
|
|
1575
1802
|
}
|
|
1576
1803
|
const changes = read.world;
|
|
1577
1804
|
const shared = read.sharedRefs;
|
|
1578
1805
|
if (shared.records.length > 0) {
|
|
1579
1806
|
for (const record of shared.records) {
|
|
1580
1807
|
this.options.onSharedRefMutation?.(worldId, record.handle);
|
|
1808
|
+
const changed = changedEntitiesByWorld[worldId];
|
|
1809
|
+
if (changed !== undefined)
|
|
1810
|
+
addSharedRefConsumers(entry.projection, worldId, record.handle, changed);
|
|
1581
1811
|
}
|
|
1582
|
-
requiresRebuild = true;
|
|
1583
|
-
break;
|
|
1584
1812
|
}
|
|
1585
1813
|
for (const changedComponentId of changes.changedComponentIds) {
|
|
1814
|
+
if (read.status === 'rebuild') {
|
|
1815
|
+
structuralComponentIdsByWorld[worldId]?.add(changedComponentId);
|
|
1816
|
+
}
|
|
1586
1817
|
if (!RENDER_RELEVANT_COMPONENT_IDS.has(changedComponentId)) continue;
|
|
1587
|
-
if (
|
|
1588
|
-
changedComponentId !== componentId(Transform) &&
|
|
1589
|
-
changedComponentId !== componentId(GlobalTransform)
|
|
1590
|
-
) {
|
|
1818
|
+
if (!RENDERABLE_SOURCE_COMPONENT_IDS.has(changedComponentId)) {
|
|
1591
1819
|
requiresRebuild = true;
|
|
1592
1820
|
break;
|
|
1593
1821
|
}
|
|
1822
|
+
const changed = changedEntitiesByWorld[worldId];
|
|
1823
|
+
if (changed === undefined) continue;
|
|
1824
|
+
if (VISIBILITY_DEPENDENCY_COMPONENT_IDS.has(changedComponentId)) {
|
|
1825
|
+
refreshStructureFacts = true;
|
|
1826
|
+
// Visibility and parent changes affect the effective state of a
|
|
1827
|
+
// subtree. The normal update path evaluates the current rows; the
|
|
1828
|
+
// set is deliberately allowed to be broad, while empty work still
|
|
1829
|
+
// exits naturally without a candidate loop.
|
|
1830
|
+
for (const entity of collectRenderableEntities(world)) changed.add(entity);
|
|
1831
|
+
continue;
|
|
1832
|
+
}
|
|
1833
|
+
if (changedComponentId === componentId(GlobalTransform)) {
|
|
1834
|
+
const query = entry.renderChangeQueries[worldId]?.get(changedComponentId);
|
|
1835
|
+
for (const row of query ?? []) {
|
|
1836
|
+
const consumers = entry.skinConsumersByJoint.get(
|
|
1837
|
+
skinJointKey(worldId, row.entity as number),
|
|
1838
|
+
);
|
|
1839
|
+
for (const consumer of consumers ?? []) changed.add(consumer);
|
|
1840
|
+
}
|
|
1841
|
+
continue;
|
|
1842
|
+
}
|
|
1843
|
+
const query = entry.renderChangeQueries[worldId]?.get(changedComponentId);
|
|
1844
|
+
if (query !== undefined) {
|
|
1845
|
+
for (const row of query) changed.add(row.entity as number);
|
|
1846
|
+
}
|
|
1594
1847
|
}
|
|
1595
1848
|
if (requiresRebuild) break;
|
|
1596
|
-
|
|
1849
|
+
nextReadVersions[worldId] = read.version;
|
|
1597
1850
|
}
|
|
1598
1851
|
if (requiresRebuild) {
|
|
1599
1852
|
return this.rebuildComposition(worlds, owner, catalogEpoch, buildCandidateFrame, leases);
|
|
1600
1853
|
}
|
|
1854
|
+
|
|
1855
|
+
if (structureChangedWorlds.size > 0) {
|
|
1856
|
+
this.visibilityFacets.clear();
|
|
1857
|
+
this.invalidateActiveEntityRevision();
|
|
1858
|
+
const currentByWorld = worlds.map((world) => collectRenderableEntities(world));
|
|
1859
|
+
currentRenderableEntitiesByWorld = currentByWorld;
|
|
1860
|
+
for (let worldId = 0; worldId < currentByWorld.length; worldId += 1) {
|
|
1861
|
+
const world = worlds[worldId];
|
|
1862
|
+
const current = currentByWorld[worldId];
|
|
1863
|
+
if (world === undefined || current === undefined) continue;
|
|
1864
|
+
const changed = changedEntitiesByWorld[worldId];
|
|
1865
|
+
if (changed !== undefined && structureChangedWorlds.has(worldId)) {
|
|
1866
|
+
// A joint can disappear without leaving a row in the joint's
|
|
1867
|
+
// GlobalTransform change query. Wake every dependent renderable so
|
|
1868
|
+
// the next partial extraction validates the dangling reference and
|
|
1869
|
+
// either republishes or retires its palette data.
|
|
1870
|
+
for (const [jointKey, consumers] of entry.skinConsumersByJoint) {
|
|
1871
|
+
const separator = jointKey.indexOf(':');
|
|
1872
|
+
const indexedWorldId = Number(jointKey.slice(0, separator));
|
|
1873
|
+
if (indexedWorldId !== worldId) continue;
|
|
1874
|
+
const jointEntity = Number(jointKey.slice(separator + 1));
|
|
1875
|
+
if (world.hasComponent(jointEntity as EntityHandle, GlobalTransform)) continue;
|
|
1876
|
+
for (const consumer of consumers) changed.add(consumer);
|
|
1877
|
+
}
|
|
1878
|
+
for (const entity of current) {
|
|
1879
|
+
if (entry.projection.slot(worldId, entity) === undefined) changed.add(entity);
|
|
1880
|
+
}
|
|
1881
|
+
const structuralComponentIds = structuralComponentIdsByWorld[worldId];
|
|
1882
|
+
const scanAllMembership =
|
|
1883
|
+
structuralComponentIds !== undefined &&
|
|
1884
|
+
[...structuralComponentIds].some((id) =>
|
|
1885
|
+
STRUCTURAL_MEMBERSHIP_SCAN_COMPONENT_IDS.has(id),
|
|
1886
|
+
);
|
|
1887
|
+
for (const slot of entry.projection.slotsSnapshot()) {
|
|
1888
|
+
if (slot.worldId !== worldId || !current.has(slot.entityKey)) continue;
|
|
1889
|
+
const membershipChanged = sourceMembershipChanged(world, slot);
|
|
1890
|
+
if (scanAllMembership || membershipChanged) {
|
|
1891
|
+
changed.add(slot.entityKey);
|
|
1892
|
+
if (
|
|
1893
|
+
slot.snapshot.instances !== undefined &&
|
|
1894
|
+
!world.hasComponent(slot.entityKey as EntityHandle, Instances)
|
|
1895
|
+
) {
|
|
1896
|
+
this.options.instanceCollections?.release(world, slot.entityKey);
|
|
1897
|
+
}
|
|
1898
|
+
if (
|
|
1899
|
+
slot.snapshot.skin !== undefined &&
|
|
1900
|
+
!world.hasComponent(slot.entityKey as EntityHandle, Skin)
|
|
1901
|
+
) {
|
|
1902
|
+
this.options
|
|
1903
|
+
.getSkinPaletteAllocator?.()
|
|
1904
|
+
?.releaseSliceFor(`${world.identity}:${slot.entityKey}`);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
for (const slot of entry.projection.slotsSnapshot()) {
|
|
1910
|
+
if (slot.worldId !== worldId || current.has(slot.entityKey)) continue;
|
|
1911
|
+
removedSlots.push(slot);
|
|
1912
|
+
}
|
|
1913
|
+
this.worldEntitiesScanned += current.size;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
// Query iteration is an observation step, not publication. Persist every
|
|
1918
|
+
// requested identity before invoking any producer so a thrown or omitted
|
|
1919
|
+
// partial extraction is retried even after ECS change evidence is drained.
|
|
1920
|
+
for (let worldId = 0; worldId < changedEntitiesByWorld.length; worldId += 1) {
|
|
1921
|
+
const pending = entry.pendingRenderableEntitiesByWorld[worldId];
|
|
1922
|
+
if (pending === undefined) continue;
|
|
1923
|
+
for (const entityKey of changedEntitiesByWorld[worldId] ?? []) pending.add(entityKey);
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
let changedDelta: RenderSceneApplyResult | undefined;
|
|
1927
|
+
const hasChangedEntities = changedEntitiesByWorld.some((entities) => entities.size > 0);
|
|
1928
|
+
if (hasChangedEntities || removedSlots.length > 0) {
|
|
1929
|
+
const operations: RenderSceneOperation[] = [];
|
|
1930
|
+
const removedDemand: TransmissionDemandOperation[] = [];
|
|
1931
|
+
const replacedSkinSnapshots: RenderableSnapshot[] = [];
|
|
1932
|
+
for (let worldId = 0; worldId < changedEntitiesByWorld.length; worldId += 1) {
|
|
1933
|
+
for (const entityKey of changedEntitiesByWorld[worldId] ?? []) {
|
|
1934
|
+
const slot = entry.projection.slot(worldId, entityKey);
|
|
1935
|
+
if (slot !== undefined) replacedSkinSnapshots.push(slot.snapshot);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
for (const slot of removedSlots) {
|
|
1939
|
+
operations.push({ kind: 'remove', worldId: slot.worldId, entityKey: slot.entityKey });
|
|
1940
|
+
if (slot.snapshot.skinJointEntities !== undefined) {
|
|
1941
|
+
removeSkinConsumer(entry.skinConsumersByJoint, slot.snapshot);
|
|
1942
|
+
}
|
|
1943
|
+
const world = worlds[slot.worldId];
|
|
1944
|
+
if (world !== undefined) this.options.instanceCollections?.release(world, slot.entityKey);
|
|
1945
|
+
this.options
|
|
1946
|
+
.getSkinPaletteAllocator?.()
|
|
1947
|
+
?.releaseSliceFor(`${world?.identity ?? slot.worldId}:${slot.entityKey}`);
|
|
1948
|
+
for (
|
|
1949
|
+
let materialIndex = 0;
|
|
1950
|
+
materialIndex < slot.snapshot.materials.length;
|
|
1951
|
+
materialIndex += 1
|
|
1952
|
+
) {
|
|
1953
|
+
removedDemand.push({
|
|
1954
|
+
kind: 'remove',
|
|
1955
|
+
key: `${slot.worldId}:${slot.entityKey}:${materialIndex}`,
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
for (let worldId = 0; worldId < changedEntitiesByWorld.length; worldId += 1) {
|
|
1960
|
+
for (const entityKey of changedEntitiesByWorld[worldId] ?? []) {
|
|
1961
|
+
const slot = entry.projection.slot(worldId, entityKey);
|
|
1962
|
+
if (slot === undefined) continue;
|
|
1963
|
+
for (
|
|
1964
|
+
let materialIndex = 0;
|
|
1965
|
+
materialIndex < slot.snapshot.materials.length;
|
|
1966
|
+
materialIndex += 1
|
|
1967
|
+
) {
|
|
1968
|
+
removedDemand.push({ kind: 'remove', key: `${worldId}:${entityKey}:${materialIndex}` });
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
const partial = hasChangedEntities
|
|
1973
|
+
? buildCandidateFrame({ kind: 'partial', entitiesByWorld: changedEntitiesByWorld })
|
|
1974
|
+
: undefined;
|
|
1975
|
+
const publishedEntitiesByWorld = worlds.map(() => new Set<number>());
|
|
1976
|
+
for (const snapshot of partial?.renderables ?? []) {
|
|
1977
|
+
operations.push({ kind: 'create', snapshot });
|
|
1978
|
+
publishedEntitiesByWorld[snapshot.worldId]?.add(snapshot.entityKey);
|
|
1979
|
+
}
|
|
1980
|
+
changedDelta = entry.projection.apply(operations);
|
|
1981
|
+
const publishedSkinKeys = new Set<string>();
|
|
1982
|
+
for (const snapshot of partial?.renderables ?? []) {
|
|
1983
|
+
publishedSkinKeys.add(`${snapshot.worldId}:${snapshot.entityKey}`);
|
|
1984
|
+
}
|
|
1985
|
+
for (const snapshot of replacedSkinSnapshots) {
|
|
1986
|
+
// Keep the old dependency while a failed/omitted partial publication
|
|
1987
|
+
// retains its CPU slot. The pending identity must still be woken by
|
|
1988
|
+
// a later joint transform change so the producer gets another chance.
|
|
1989
|
+
if (
|
|
1990
|
+
snapshot.skinJointEntities !== undefined &&
|
|
1991
|
+
publishedSkinKeys.has(`${snapshot.worldId}:${snapshot.entityKey}`)
|
|
1992
|
+
) {
|
|
1993
|
+
removeSkinConsumer(entry.skinConsumersByJoint, snapshot);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
for (const snapshot of partial?.renderables ?? []) {
|
|
1997
|
+
if (snapshot.skinJointEntities !== undefined) {
|
|
1998
|
+
addSkinConsumer(entry.skinConsumersByJoint, snapshot);
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
entry.transmissionDemand.apply([
|
|
2002
|
+
...removedDemand,
|
|
2003
|
+
...transmissionDemandOperations(partial?.renderables ?? []),
|
|
2004
|
+
]);
|
|
2005
|
+
for (const removed of changedDelta.removedSlots) entry.dispatchBySlot.delete(removed.slot);
|
|
2006
|
+
for (const recreated of changedDelta.recreatedSlots)
|
|
2007
|
+
entry.dispatchBySlot.delete(recreated.slot);
|
|
2008
|
+
if (partial !== undefined) {
|
|
2009
|
+
if (refreshStructureFacts) {
|
|
2010
|
+
entry.structure = {
|
|
2011
|
+
...entry.structure,
|
|
2012
|
+
dispatch: partial.dispatch,
|
|
2013
|
+
visibilityStats: partial.visibilityStats,
|
|
2014
|
+
visibilitySnapshots: partial.visibilitySnapshots,
|
|
2015
|
+
featureVisibilitySnapshots: partial.featureVisibilitySnapshots,
|
|
2016
|
+
hiddenEntityReports: partial.hiddenEntityReports,
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
const partialDispatch = dispatchEntriesBySlot(partial, entry.projection);
|
|
2020
|
+
for (const [slot, dispatch] of partialDispatch) entry.dispatchBySlot.set(slot, dispatch);
|
|
2021
|
+
// A changed renderable which now fails producer validation still owns
|
|
2022
|
+
// its retained identity; replace its old dispatch with an empty list.
|
|
2023
|
+
for (const snapshot of partial.renderables) {
|
|
2024
|
+
const slot = entry.projection.slot(snapshot.worldId, snapshot.entityKey);
|
|
2025
|
+
if (slot !== undefined && !partialDispatch.has(slot.slot))
|
|
2026
|
+
entry.dispatchBySlot.set(slot.slot, []);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
for (let worldId = 0; worldId < changedEntitiesByWorld.length; worldId += 1) {
|
|
2030
|
+
const requested = changedEntitiesByWorld[worldId];
|
|
2031
|
+
const published = publishedEntitiesByWorld[worldId];
|
|
2032
|
+
if (requested === undefined || published === undefined) continue;
|
|
2033
|
+
const pending = entry.pendingRenderableEntitiesByWorld[worldId];
|
|
2034
|
+
for (const entityKey of requested) {
|
|
2035
|
+
const slot = entry.projection.slot(worldId, entityKey);
|
|
2036
|
+
if (published.has(entityKey)) {
|
|
2037
|
+
pending?.delete(entityKey);
|
|
2038
|
+
continue;
|
|
2039
|
+
}
|
|
2040
|
+
if (slot === undefined) {
|
|
2041
|
+
const current = currentRenderableEntitiesByWorld?.[worldId];
|
|
2042
|
+
if (current !== undefined && !current.has(entityKey)) pending?.delete(entityKey);
|
|
2043
|
+
continue;
|
|
2044
|
+
}
|
|
2045
|
+
// The producer did not publish this requested identity. Keep the
|
|
2046
|
+
// old CPU slot for recovery, but stop submitting stale dispatch and
|
|
2047
|
+
// retry the same identity even if its World version stays still.
|
|
2048
|
+
pending?.add(entityKey);
|
|
2049
|
+
if (slot !== undefined) entry.dispatchBySlot.set(slot.slot, []);
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
const newlyUnavailable: RenderSceneSlot[] = [];
|
|
2053
|
+
for (let worldId = 0; worldId < changedEntitiesByWorld.length; worldId += 1) {
|
|
2054
|
+
const requested = changedEntitiesByWorld[worldId];
|
|
2055
|
+
const published = publishedEntitiesByWorld[worldId];
|
|
2056
|
+
if (requested === undefined || published === undefined) continue;
|
|
2057
|
+
for (const entityKey of requested) {
|
|
2058
|
+
const slot = entry.projection.slot(worldId, entityKey);
|
|
2059
|
+
if (slot === undefined || published.has(entityKey)) {
|
|
2060
|
+
if (slot !== undefined) entry.unavailableSlots.delete(slot.slot);
|
|
2061
|
+
continue;
|
|
2062
|
+
}
|
|
2063
|
+
if (!entry.unavailableSlots.has(slot.slot)) newlyUnavailable.push(slot);
|
|
2064
|
+
entry.unavailableSlots.add(slot.slot);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
for (const removed of changedDelta.removedSlots) entry.unavailableSlots.delete(removed.slot);
|
|
2068
|
+
for (const recreated of changedDelta.recreatedSlots)
|
|
2069
|
+
entry.unavailableSlots.delete(recreated.slot);
|
|
2070
|
+
entry.dispatchRevision += 1;
|
|
2071
|
+
entry.dispatchCache = undefined;
|
|
2072
|
+
entry.topology.apply(
|
|
2073
|
+
topologyDeltaForAvailable(changedDelta, entry.unavailableSlots, newlyUnavailable),
|
|
2074
|
+
);
|
|
2075
|
+
this.syncGpuOwner(entry.token, entry.projection, changedDelta);
|
|
2076
|
+
this.deltaFrames +=
|
|
2077
|
+
changedDelta.created +
|
|
2078
|
+
changedDelta.updated +
|
|
2079
|
+
changedDelta.removed +
|
|
2080
|
+
changedDelta.recreated >
|
|
2081
|
+
0
|
|
2082
|
+
? 1
|
|
2083
|
+
: 0;
|
|
2084
|
+
this.transformUpdates += changedDelta.updated;
|
|
2085
|
+
}
|
|
2086
|
+
|
|
1601
2087
|
const transformSpans: WorldTransformSpan[] = [];
|
|
1602
2088
|
for (let worldId = 0; worldId < entry.transformQueries.length; worldId += 1) {
|
|
1603
2089
|
const query = entry.transformQueries[worldId];
|
|
@@ -1615,17 +2101,29 @@ export class PersistentRenderScene {
|
|
|
1615
2101
|
if (requiresRebuild) {
|
|
1616
2102
|
return this.rebuildComposition(worlds, owner, catalogEpoch, buildCandidateFrame, leases);
|
|
1617
2103
|
}
|
|
1618
|
-
if (transformSpans.length === 0) {
|
|
2104
|
+
if (transformSpans.length === 0 && changedDelta === undefined) {
|
|
1619
2105
|
this.noChangeFrames += 1;
|
|
1620
2106
|
this.syncGpuOwner(entry.token, entry.projection);
|
|
1621
|
-
|
|
2107
|
+
const result = this.deriveFramePlan(entry, buildCandidateFrame);
|
|
2108
|
+
for (let worldId = 0; worldId < nextReadVersions.length; worldId += 1) {
|
|
2109
|
+
const version = nextReadVersions[worldId];
|
|
2110
|
+
if (version !== undefined) entry.readVersions[worldId] = version;
|
|
2111
|
+
}
|
|
2112
|
+
return result;
|
|
1622
2113
|
}
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
2114
|
+
if (transformSpans.length > 0) {
|
|
2115
|
+
const delta = entry.projection.applyTransformSpans(transformSpans);
|
|
2116
|
+
entry.topology.apply(topologyDeltaForAvailable(delta, entry.unavailableSlots, []));
|
|
2117
|
+
this.syncGpuOwner(entry.token, entry.projection, delta);
|
|
2118
|
+
this.deltaFrames += 1;
|
|
2119
|
+
this.transformUpdates += delta.updated;
|
|
2120
|
+
}
|
|
2121
|
+
const result = this.deriveFramePlan(entry, buildCandidateFrame);
|
|
2122
|
+
for (let worldId = 0; worldId < nextReadVersions.length; worldId += 1) {
|
|
2123
|
+
const version = nextReadVersions[worldId];
|
|
2124
|
+
if (version !== undefined) entry.readVersions[worldId] = version;
|
|
2125
|
+
}
|
|
2126
|
+
return result;
|
|
1629
2127
|
}
|
|
1630
2128
|
|
|
1631
2129
|
invalidate(): void {
|
|
@@ -1651,6 +2149,16 @@ export class PersistentRenderScene {
|
|
|
1651
2149
|
const detachedComposition = this.composition?.worlds.includes(world)
|
|
1652
2150
|
? this.composition
|
|
1653
2151
|
: undefined;
|
|
2152
|
+
if (detachedComposition !== undefined) {
|
|
2153
|
+
for (const slot of detachedComposition.projection.slotsSnapshot()) {
|
|
2154
|
+
if (detachedComposition.worlds[slot.worldId] !== world) continue;
|
|
2155
|
+
this.options.instanceCollections?.release(world, slot.entityKey);
|
|
2156
|
+
if (slot.snapshot.skin === undefined) continue;
|
|
2157
|
+
this.options
|
|
2158
|
+
.getSkinPaletteAllocator?.()
|
|
2159
|
+
?.releaseSliceFor(`${world.identity}:${slot.entityKey}`);
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
1654
2162
|
if (detachedComposition !== undefined) this.composition = undefined;
|
|
1655
2163
|
if (detachedComposition !== undefined) this.temporalCapture = undefined;
|
|
1656
2164
|
if (detachedComposition !== undefined) {
|
|
@@ -1904,7 +2412,7 @@ export class PersistentRenderScene {
|
|
|
1904
2412
|
worlds: readonly World[],
|
|
1905
2413
|
owner: { readonly cameraOwner: number; readonly resourceOwner: number },
|
|
1906
2414
|
catalogEpoch: number,
|
|
1907
|
-
buildCandidateFrame: (
|
|
2415
|
+
buildCandidateFrame: (request: PersistentRenderCandidateRequest) => ExtractedFrame,
|
|
1908
2416
|
leases?: readonly RenderReadLease[],
|
|
1909
2417
|
preserveVisibilityOnReorder = false,
|
|
1910
2418
|
): ExtractedFrame {
|
|
@@ -1974,6 +2482,10 @@ export class PersistentRenderScene {
|
|
|
1974
2482
|
transmissionDemand.apply(transmissionDemandOperations(candidateFrame.renderables));
|
|
1975
2483
|
const topology = new BatchTopology();
|
|
1976
2484
|
topology.rebuild(projection.slotsSnapshot());
|
|
2485
|
+
const skinConsumersByJoint = new Map<string, Set<number>>();
|
|
2486
|
+
for (const snapshot of stableCandidateFrame.renderables) {
|
|
2487
|
+
if (snapshot.skinJointEntities !== undefined) addSkinConsumer(skinConsumersByJoint, snapshot);
|
|
2488
|
+
}
|
|
1977
2489
|
const token = {};
|
|
1978
2490
|
const entry: PersistentCompositionEntry = {
|
|
1979
2491
|
token,
|
|
@@ -2003,8 +2515,17 @@ export class PersistentRenderScene {
|
|
|
2003
2515
|
leases === undefined || leases.length !== worlds.length
|
|
2004
2516
|
? []
|
|
2005
2517
|
: worlds.map(createGlobalTransformChangeQuery),
|
|
2518
|
+
renderChangeQueries:
|
|
2519
|
+
leases === undefined || leases.length !== worlds.length
|
|
2520
|
+
? []
|
|
2521
|
+
: worlds.map(createRenderChangeQueries),
|
|
2522
|
+
dispatchBySlot: dispatchEntriesBySlot(stableCandidateFrame, projection),
|
|
2523
|
+
dispatchRevision: 0,
|
|
2524
|
+
dispatchCache: undefined,
|
|
2525
|
+
skinConsumersByJoint,
|
|
2526
|
+
pendingRenderableEntitiesByWorld: worlds.map(() => new Set<number>()),
|
|
2527
|
+
unavailableSlots: new Set<number>(),
|
|
2006
2528
|
catalogEpoch,
|
|
2007
|
-
rigidOnly: candidateFrame.renderables.every((renderable) => renderable.skin === undefined),
|
|
2008
2529
|
probeProjection: new ProbeBlendSceneProjection(),
|
|
2009
2530
|
temporalDemanded: frameDemandsTemporal(candidateFrame),
|
|
2010
2531
|
gpuDrivenSceneRequired: true,
|
|
@@ -2064,10 +2585,23 @@ export class PersistentRenderScene {
|
|
|
2064
2585
|
*/
|
|
2065
2586
|
private deriveFramePlan(
|
|
2066
2587
|
entry: PersistentCompositionEntry,
|
|
2067
|
-
buildCandidateFrame: (
|
|
2588
|
+
buildCandidateFrame: (request: PersistentRenderCandidateRequest) => ExtractedFrame,
|
|
2068
2589
|
): ExtractedFrame {
|
|
2069
2590
|
const resourceFrame = buildCandidateFrame('none');
|
|
2070
|
-
const
|
|
2591
|
+
const cachedDispatch = entry.dispatchCache;
|
|
2592
|
+
const dispatch: DispatchEntry[] = [
|
|
2593
|
+
...(cachedDispatch?.revision === entry.dispatchRevision
|
|
2594
|
+
? cachedDispatch.value
|
|
2595
|
+
: dispatchForProjection(entry.dispatchBySlot, entry.projection, entry.unavailableSlots)),
|
|
2596
|
+
];
|
|
2597
|
+
if (cachedDispatch?.revision !== entry.dispatchRevision) {
|
|
2598
|
+
entry.dispatchCache = { revision: entry.dispatchRevision, value: dispatch };
|
|
2599
|
+
}
|
|
2600
|
+
const candidateFrame: ExtractedFrame = {
|
|
2601
|
+
...resourceFrame,
|
|
2602
|
+
...entry.structure,
|
|
2603
|
+
dispatch,
|
|
2604
|
+
};
|
|
2071
2605
|
entry.temporalDemanded = frameDemandsTemporal(candidateFrame);
|
|
2072
2606
|
entry.projection.setTemporalTracking(entry.temporalDemanded);
|
|
2073
2607
|
this.projectProbes(entry, candidateFrame);
|
|
@@ -2076,9 +2610,18 @@ export class PersistentRenderScene {
|
|
|
2076
2610
|
const candidates = entry.temporalDemanded
|
|
2077
2611
|
? materialized.map((snapshot) => entry.projection.withTemporal(snapshot))
|
|
2078
2612
|
: materialized;
|
|
2613
|
+
const drawableCandidates =
|
|
2614
|
+
entry.unavailableSlots.size === 0
|
|
2615
|
+
? candidates
|
|
2616
|
+
: candidates.map((snapshot) => {
|
|
2617
|
+
const slot = entry.projection.slot(snapshot.worldId, snapshot.entityKey);
|
|
2618
|
+
return slot !== undefined && entry.unavailableSlots.has(slot.slot)
|
|
2619
|
+
? { ...snapshot, authorVisible: false }
|
|
2620
|
+
: snapshot;
|
|
2621
|
+
});
|
|
2079
2622
|
const frameWithProbes = attachProbeRecords(
|
|
2080
|
-
{ ...candidateFrame, renderables:
|
|
2081
|
-
|
|
2623
|
+
{ ...candidateFrame, renderables: drawableCandidates },
|
|
2624
|
+
drawableCandidates,
|
|
2082
2625
|
entry.projection.slotsSnapshot(),
|
|
2083
2626
|
entry.probeProjection,
|
|
2084
2627
|
);
|