@fps-games/engine 0.0.1-beta.5 → 0.0.1-beta.8
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/dist/asset-resource.d.ts +39 -0
- package/dist/asset-resource.d.ts.map +1 -0
- package/dist/asset-resource.js +121 -0
- package/dist/asset-resource.js.map +1 -0
- package/dist/babylon/game-world-plugin-lifecycle.d.ts +26 -0
- package/dist/babylon/game-world-plugin-lifecycle.d.ts.map +1 -0
- package/dist/babylon/game-world-plugin-lifecycle.js +257 -0
- package/dist/babylon/game-world-plugin-lifecycle.js.map +1 -0
- package/dist/babylon/plugin-contract.d.ts +15 -0
- package/dist/babylon/plugin-contract.d.ts.map +1 -0
- package/dist/babylon/plugin-contract.js +9 -0
- package/dist/babylon/plugin-contract.js.map +1 -0
- package/dist/babylon/renderer/brdf-texture-settlement.d.ts +7 -0
- package/dist/babylon/renderer/brdf-texture-settlement.d.ts.map +1 -0
- package/dist/babylon/renderer/brdf-texture-settlement.js +29 -0
- package/dist/babylon/renderer/brdf-texture-settlement.js.map +1 -0
- package/dist/babylon/renderer/environment/environment-texture-controller.d.ts +7 -0
- package/dist/babylon/renderer/environment/environment-texture-controller.d.ts.map +1 -0
- package/dist/babylon/renderer/environment/environment-texture-controller.js +357 -0
- package/dist/babylon/renderer/environment/environment-texture-controller.js.map +1 -0
- package/dist/babylon/renderer/environment/index.d.ts +4 -0
- package/dist/babylon/renderer/environment/index.d.ts.map +1 -0
- package/dist/babylon/renderer/environment/index.js +4 -0
- package/dist/babylon/renderer/environment/index.js.map +1 -0
- package/dist/babylon/renderer/environment/profile.d.ts +3 -0
- package/dist/babylon/renderer/environment/profile.d.ts.map +1 -0
- package/dist/babylon/renderer/environment/profile.js +27 -0
- package/dist/babylon/renderer/environment/profile.js.map +1 -0
- package/dist/babylon/renderer/environment/types.d.ts +24 -0
- package/dist/babylon/renderer/environment/types.d.ts.map +1 -0
- package/dist/babylon/renderer/environment/types.js +6 -0
- package/dist/babylon/renderer/environment/types.js.map +1 -0
- package/dist/babylon/renderer/index.d.ts +24 -0
- package/dist/babylon/renderer/index.d.ts.map +1 -0
- package/dist/babylon/renderer/index.js +24 -0
- package/dist/babylon/renderer/index.js.map +1 -0
- package/dist/babylon/renderer/lighting/directional-light-angles.d.ts +17 -0
- package/dist/babylon/renderer/lighting/directional-light-angles.d.ts.map +1 -0
- package/dist/babylon/renderer/lighting/directional-light-angles.js +67 -0
- package/dist/babylon/renderer/lighting/directional-light-angles.js.map +1 -0
- package/dist/babylon/renderer/material-owner-candidates.d.ts +19 -0
- package/dist/babylon/renderer/material-owner-candidates.d.ts.map +1 -0
- package/dist/babylon/renderer/material-owner-candidates.js +212 -0
- package/dist/babylon/renderer/material-owner-candidates.js.map +1 -0
- package/dist/babylon/renderer/materials/index.d.ts +3 -0
- package/dist/babylon/renderer/materials/index.d.ts.map +1 -0
- package/dist/babylon/renderer/materials/index.js +3 -0
- package/dist/babylon/renderer/materials/index.js.map +1 -0
- package/dist/babylon/renderer/materials/material-runtime.d.ts +29 -0
- package/dist/babylon/renderer/materials/material-runtime.d.ts.map +1 -0
- package/dist/babylon/renderer/materials/material-runtime.js +660 -0
- package/dist/babylon/renderer/materials/material-runtime.js.map +1 -0
- package/dist/babylon/renderer/materials/material-slot-owner.d.ts +42 -0
- package/dist/babylon/renderer/materials/material-slot-owner.d.ts.map +1 -0
- package/dist/babylon/renderer/materials/material-slot-owner.js +318 -0
- package/dist/babylon/renderer/materials/material-slot-owner.js.map +1 -0
- package/dist/babylon/renderer/outlines/babylon-default/apply.d.ts +4 -0
- package/dist/babylon/renderer/outlines/babylon-default/apply.d.ts.map +1 -0
- package/dist/babylon/renderer/outlines/babylon-default/apply.js +45 -0
- package/dist/babylon/renderer/outlines/babylon-default/apply.js.map +1 -0
- package/dist/babylon/renderer/outlines/babylon-default/index.d.ts +3 -0
- package/dist/babylon/renderer/outlines/babylon-default/index.d.ts.map +1 -0
- package/dist/babylon/renderer/outlines/babylon-default/index.js +2 -0
- package/dist/babylon/renderer/outlines/babylon-default/index.js.map +1 -0
- package/dist/babylon/renderer/outlines/babylon-default/types.d.ts +7 -0
- package/dist/babylon/renderer/outlines/babylon-default/types.d.ts.map +1 -0
- package/dist/babylon/renderer/outlines/babylon-default/types.js +2 -0
- package/dist/babylon/renderer/outlines/babylon-default/types.js.map +1 -0
- package/dist/babylon/renderer/outlines/index.d.ts +4 -0
- package/dist/babylon/renderer/outlines/index.d.ts.map +1 -0
- package/dist/babylon/renderer/outlines/index.js +3 -0
- package/dist/babylon/renderer/outlines/index.js.map +1 -0
- package/dist/babylon/renderer/outlines/runtime-override.d.ts +36 -0
- package/dist/babylon/renderer/outlines/runtime-override.d.ts.map +1 -0
- package/dist/babylon/renderer/outlines/runtime-override.js +131 -0
- package/dist/babylon/renderer/outlines/runtime-override.js.map +1 -0
- package/dist/babylon/renderer/outlines/types.d.ts +7 -0
- package/dist/babylon/renderer/outlines/types.d.ts.map +1 -0
- package/dist/babylon/renderer/outlines/types.js +2 -0
- package/dist/babylon/renderer/outlines/types.js.map +1 -0
- package/dist/babylon/renderer/placement/index.d.ts +2 -0
- package/dist/babylon/renderer/placement/index.d.ts.map +1 -0
- package/dist/babylon/renderer/placement/index.js +2 -0
- package/dist/babylon/renderer/placement/index.js.map +1 -0
- package/dist/babylon/renderer/placement/runtime-drop-position.d.ts +24 -0
- package/dist/babylon/renderer/placement/runtime-drop-position.d.ts.map +1 -0
- package/dist/babylon/renderer/placement/runtime-drop-position.js +63 -0
- package/dist/babylon/renderer/placement/runtime-drop-position.js.map +1 -0
- package/dist/babylon/renderer/post-process/babylon-default-pipeline.d.ts +7 -0
- package/dist/babylon/renderer/post-process/babylon-default-pipeline.d.ts.map +1 -0
- package/dist/babylon/renderer/post-process/babylon-default-pipeline.js +301 -0
- package/dist/babylon/renderer/post-process/babylon-default-pipeline.js.map +1 -0
- package/dist/babylon/renderer/post-process/index.d.ts +5 -0
- package/dist/babylon/renderer/post-process/index.d.ts.map +1 -0
- package/dist/babylon/renderer/post-process/index.js +5 -0
- package/dist/babylon/renderer/post-process/index.js.map +1 -0
- package/dist/babylon/renderer/post-process/profile.d.ts +5 -0
- package/dist/babylon/renderer/post-process/profile.d.ts.map +1 -0
- package/dist/babylon/renderer/post-process/profile.js +155 -0
- package/dist/babylon/renderer/post-process/profile.js.map +1 -0
- package/dist/babylon/renderer/post-process/types.d.ts +81 -0
- package/dist/babylon/renderer/post-process/types.d.ts.map +1 -0
- package/dist/babylon/renderer/post-process/types.js +24 -0
- package/dist/babylon/renderer/post-process/types.js.map +1 -0
- package/dist/babylon/renderer/post-process/volume-stack.d.ts +4 -0
- package/dist/babylon/renderer/post-process/volume-stack.d.ts.map +1 -0
- package/dist/babylon/renderer/post-process/volume-stack.js +105 -0
- package/dist/babylon/renderer/post-process/volume-stack.js.map +1 -0
- package/dist/babylon/renderer/rendering/image-node.d.ts +35 -0
- package/dist/babylon/renderer/rendering/image-node.d.ts.map +1 -0
- package/dist/babylon/renderer/rendering/image-node.js +39 -0
- package/dist/babylon/renderer/rendering/image-node.js.map +1 -0
- package/dist/babylon/renderer/rendering/index.d.ts +3 -0
- package/dist/babylon/renderer/rendering/index.d.ts.map +1 -0
- package/dist/babylon/renderer/rendering/index.js +3 -0
- package/dist/babylon/renderer/rendering/index.js.map +1 -0
- package/dist/babylon/renderer/rendering/node-rendering.d.ts +19 -0
- package/dist/babylon/renderer/rendering/node-rendering.d.ts.map +1 -0
- package/dist/babylon/renderer/rendering/node-rendering.js +66 -0
- package/dist/babylon/renderer/rendering/node-rendering.js.map +1 -0
- package/dist/babylon/renderer/shadows/blob-shadow-shaders.d.ts +5 -0
- package/dist/babylon/renderer/shadows/blob-shadow-shaders.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/blob-shadow-shaders.js +46 -0
- package/dist/babylon/renderer/shadows/blob-shadow-shaders.js.map +1 -0
- package/dist/babylon/renderer/shadows/blob-shadow-system.d.ts +5 -0
- package/dist/babylon/renderer/shadows/blob-shadow-system.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/blob-shadow-system.js +402 -0
- package/dist/babylon/renderer/shadows/blob-shadow-system.js.map +1 -0
- package/dist/babylon/renderer/shadows/dynamic-shadow-system.d.ts +84 -0
- package/dist/babylon/renderer/shadows/dynamic-shadow-system.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/dynamic-shadow-system.js +566 -0
- package/dist/babylon/renderer/shadows/dynamic-shadow-system.js.map +1 -0
- package/dist/babylon/renderer/shadows/planar-shadow-shaders.d.ts +7 -0
- package/dist/babylon/renderer/shadows/planar-shadow-shaders.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/planar-shadow-shaders.js +316 -0
- package/dist/babylon/renderer/shadows/planar-shadow-shaders.js.map +1 -0
- package/dist/babylon/renderer/shadows/planar-shadow-system.d.ts +5 -0
- package/dist/babylon/renderer/shadows/planar-shadow-system.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/planar-shadow-system.js +1562 -0
- package/dist/babylon/renderer/shadows/planar-shadow-system.js.map +1 -0
- package/dist/babylon/renderer/shadows/shadow-generator-shaders.d.ts +14 -0
- package/dist/babylon/renderer/shadows/shadow-generator-shaders.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/shadow-generator-shaders.js +14 -0
- package/dist/babylon/renderer/shadows/shadow-generator-shaders.js.map +1 -0
- package/dist/babylon/renderer/shadows/shadow-map-experiment-depth-backend.d.ts +68 -0
- package/dist/babylon/renderer/shadows/shadow-map-experiment-depth-backend.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/shadow-map-experiment-depth-backend.js +829 -0
- package/dist/babylon/renderer/shadows/shadow-map-experiment-depth-backend.js.map +1 -0
- package/dist/babylon/renderer/shadows/shadow-map-experiment-system.d.ts +148 -0
- package/dist/babylon/renderer/shadows/shadow-map-experiment-system.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/shadow-map-experiment-system.js +4529 -0
- package/dist/babylon/renderer/shadows/shadow-map-experiment-system.js.map +1 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-artifact-system.d.ts +4 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-artifact-system.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-artifact-system.js +645 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-artifact-system.js.map +1 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-atlas.d.ts +61 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-atlas.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-atlas.js +382 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-atlas.js.map +1 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-bake.d.ts +47 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-bake.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-bake.js +285 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-bake.js.map +1 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-baker.d.ts +88 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-baker.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-baker.js +830 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-baker.js.map +1 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-options.d.ts +3 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-options.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-options.js +60 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-options.js.map +1 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-rendering.d.ts +27 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-rendering.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-rendering.js +220 -0
- package/dist/babylon/renderer/shadows/static-projected-shadow-rendering.js.map +1 -0
- package/dist/babylon/renderer/shadows/types.d.ts +421 -0
- package/dist/babylon/renderer/shadows/types.d.ts.map +1 -0
- package/dist/babylon/renderer/shadows/types.js +127 -0
- package/dist/babylon/renderer/shadows/types.js.map +1 -0
- package/dist/babylon/renderer-controller.d.ts +93 -0
- package/dist/babylon/renderer-controller.d.ts.map +1 -0
- package/dist/babylon/renderer-controller.js +699 -0
- package/dist/babylon/renderer-controller.js.map +1 -0
- package/dist/babylon.d.ts +4 -0
- package/dist/babylon.d.ts.map +1 -1
- package/dist/babylon.js +4 -0
- package/dist/babylon.js.map +1 -1
- package/dist/build-info.json +4 -4
- package/dist/compiled-scene-data.d.ts +23 -0
- package/dist/compiled-scene-data.d.ts.map +1 -0
- package/dist/compiled-scene-data.js +100 -0
- package/dist/compiled-scene-data.js.map +1 -0
- package/dist/ground-decal.d.ts +22 -0
- package/dist/ground-decal.d.ts.map +1 -0
- package/dist/ground-decal.js +48 -0
- package/dist/ground-decal.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -1
- package/dist/material-owner.d.ts +128 -0
- package/dist/material-owner.d.ts.map +1 -0
- package/dist/material-owner.js +434 -0
- package/dist/material-owner.js.map +1 -0
- package/dist/material-profile.d.ts +62 -0
- package/dist/material-profile.d.ts.map +1 -0
- package/dist/material-profile.js +132 -0
- package/dist/material-profile.js.map +1 -0
- package/dist/rendering/index.d.ts +14 -0
- package/dist/rendering/index.d.ts.map +1 -0
- package/dist/rendering/index.js +12 -0
- package/dist/rendering/index.js.map +1 -0
- package/dist/rendering/rendering-profile-types.d.ts +322 -0
- package/dist/rendering/rendering-profile-types.d.ts.map +1 -0
- package/dist/rendering/rendering-profile-types.js +2 -0
- package/dist/rendering/rendering-profile-types.js.map +1 -0
- package/dist/rendering/rendering-profile-validation.d.ts +2 -0
- package/dist/rendering/rendering-profile-validation.d.ts.map +1 -0
- package/dist/rendering/rendering-profile-validation.js +364 -0
- package/dist/rendering/rendering-profile-validation.js.map +1 -0
- package/dist/rendering/rendering-profile-values.d.ts +19 -0
- package/dist/rendering/rendering-profile-values.d.ts.map +1 -0
- package/dist/rendering/rendering-profile-values.js +79 -0
- package/dist/rendering/rendering-profile-values.js.map +1 -0
- package/dist/rendering/rendering-profile.d.ts +15 -0
- package/dist/rendering/rendering-profile.d.ts.map +1 -0
- package/dist/rendering/rendering-profile.js +442 -0
- package/dist/rendering/rendering-profile.js.map +1 -0
- package/dist/rendering/shadow-map-experiment-depth.d.ts +63 -0
- package/dist/rendering/shadow-map-experiment-depth.d.ts.map +1 -0
- package/dist/rendering/shadow-map-experiment-depth.js +186 -0
- package/dist/rendering/shadow-map-experiment-depth.js.map +1 -0
- package/dist/rendering/shadow-map-experiment-lifecycle.d.ts +94 -0
- package/dist/rendering/shadow-map-experiment-lifecycle.d.ts.map +1 -0
- package/dist/rendering/shadow-map-experiment-lifecycle.js +407 -0
- package/dist/rendering/shadow-map-experiment-lifecycle.js.map +1 -0
- package/dist/rendering/shadow-map-experiment.d.ts +189 -0
- package/dist/rendering/shadow-map-experiment.d.ts.map +1 -0
- package/dist/rendering/shadow-map-experiment.js +549 -0
- package/dist/rendering/shadow-map-experiment.js.map +1 -0
- package/dist/rendering/shadow-quality.d.ts +5 -0
- package/dist/rendering/shadow-quality.d.ts.map +1 -0
- package/dist/rendering/shadow-quality.js +83 -0
- package/dist/rendering/shadow-quality.js.map +1 -0
- package/dist/rendering/shadow-runtime.d.ts +59 -0
- package/dist/rendering/shadow-runtime.d.ts.map +1 -0
- package/dist/rendering/shadow-runtime.js +378 -0
- package/dist/rendering/shadow-runtime.js.map +1 -0
- package/dist/rendering/shadow-settings.d.ts +15 -0
- package/dist/rendering/shadow-settings.d.ts.map +1 -0
- package/dist/rendering/shadow-settings.js +129 -0
- package/dist/rendering/shadow-settings.js.map +1 -0
- package/dist/rendering/shadow-types.d.ts +99 -0
- package/dist/rendering/shadow-types.d.ts.map +1 -0
- package/dist/rendering/shadow-types.js +5 -0
- package/dist/rendering/shadow-types.js.map +1 -0
- package/dist/rendering/shadow-validation.d.ts +3 -0
- package/dist/rendering/shadow-validation.d.ts.map +1 -0
- package/dist/rendering/shadow-validation.js +166 -0
- package/dist/rendering/shadow-validation.js.map +1 -0
- package/dist/rendering/value.d.ts +2 -0
- package/dist/rendering/value.d.ts.map +1 -0
- package/dist/rendering/value.js +6 -0
- package/dist/rendering/value.js.map +1 -0
- package/dist/runtime-data-token.d.ts +4 -0
- package/dist/runtime-data-token.d.ts.map +1 -0
- package/dist/runtime-data-token.js +4 -0
- package/dist/runtime-data-token.js.map +1 -0
- package/dist/runtime-data-types.d.ts +83 -0
- package/dist/runtime-data-types.d.ts.map +1 -0
- package/dist/runtime-data-types.js +2 -0
- package/dist/runtime-data-types.js.map +1 -0
- package/dist/runtime-data.d.ts +18 -0
- package/dist/runtime-data.d.ts.map +1 -0
- package/dist/runtime-data.js +301 -0
- package/dist/runtime-data.js.map +1 -0
- package/dist/runtime-scene-resolvers.d.ts +37 -0
- package/dist/runtime-scene-resolvers.d.ts.map +1 -0
- package/dist/runtime-scene-resolvers.js +40 -0
- package/dist/runtime-scene-resolvers.js.map +1 -0
- package/dist/scene-artifact-types.d.ts +383 -0
- package/dist/scene-artifact-types.d.ts.map +1 -0
- package/dist/scene-artifact-types.js +2 -0
- package/dist/scene-artifact-types.js.map +1 -0
- package/dist/scene-artifact-validation.d.ts +19 -0
- package/dist/scene-artifact-validation.d.ts.map +1 -0
- package/dist/scene-artifact-validation.js +1352 -0
- package/dist/scene-artifact-validation.js.map +1 -0
- package/dist/static-shadow-artifact.d.ts +131 -0
- package/dist/static-shadow-artifact.d.ts.map +1 -0
- package/dist/static-shadow-artifact.js +327 -0
- package/dist/static-shadow-artifact.js.map +1 -0
- package/package.json +9 -1
|
@@ -0,0 +1,4529 @@
|
|
|
1
|
+
import { DirectionalLight } from '@babylonjs/core/Lights/directionalLight.js';
|
|
2
|
+
import { ShadowGenerator } from '@babylonjs/core/Lights/Shadows/shadowGenerator.js';
|
|
3
|
+
import './shadow-generator-shaders.js';
|
|
4
|
+
import { Matrix, Vector3 } from '@babylonjs/core/Maths/math.vector.js';
|
|
5
|
+
import { createShadowMapExperimentPlanSnapshot, createShadowMapExperimentPlanFingerprint, resolveShadowMapExperimentAppearance, createShadowMapExperimentActivityScheduler, createShadowMapExperimentDepthRefreshScheduler, createShadowMapExperimentInfluenceCullingState, createShadowMapExperimentRegistry, diffShadowMapExperimentPlans, shadowMapExperimentDepthLayerForUpdateClass, } from '../../../rendering/index.js';
|
|
6
|
+
import { createBabylonShadowMapExperimentDepthBackend, createBabylonShadowMapExperimentSingleDepthBackend, readBabylonShadowMapExperimentAllocationCleanup, } from './shadow-map-experiment-depth-backend.js';
|
|
7
|
+
import '@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent.js';
|
|
8
|
+
export function readBabylonShadowMapExperimentSystemCleanup(error) {
|
|
9
|
+
if (!error || (typeof error !== 'object' && typeof error !== 'function'))
|
|
10
|
+
return null;
|
|
11
|
+
const cleanup = error.shadowMapExperimentSystemCleanup;
|
|
12
|
+
return typeof cleanup === 'function' ? cleanup : null;
|
|
13
|
+
}
|
|
14
|
+
export function readBabylonRuntimeShadowObjectsRecoveryOwner(error) {
|
|
15
|
+
if (!error || (typeof error !== 'object' && typeof error !== 'function'))
|
|
16
|
+
return null;
|
|
17
|
+
const owner = error.runtimeShadowObjectsRecoveryOwner;
|
|
18
|
+
return owner && typeof owner === 'object' && typeof owner.dispose === 'function'
|
|
19
|
+
? owner
|
|
20
|
+
: null;
|
|
21
|
+
}
|
|
22
|
+
export function createBabylonShadowMapExperimentSystem(scene, initialPlan = null, options = {}) {
|
|
23
|
+
let disposed = false;
|
|
24
|
+
let disposing = false;
|
|
25
|
+
let sourcePlan = null;
|
|
26
|
+
let plan = null;
|
|
27
|
+
let runtimeOverlayRevision = 0;
|
|
28
|
+
let runtimeIncrementalMutationCount = 0;
|
|
29
|
+
let effectivePlanMaterializationCount = 0;
|
|
30
|
+
let materializedEffectivePlan = null;
|
|
31
|
+
let lightState = null;
|
|
32
|
+
let generator = null;
|
|
33
|
+
let depthBackend = null;
|
|
34
|
+
let registry = null;
|
|
35
|
+
let previousSceneShadowsEnabled = null;
|
|
36
|
+
let refreshCount = 0;
|
|
37
|
+
let lastRefreshReasons = Object.freeze([]);
|
|
38
|
+
const activityScheduler = createShadowMapExperimentActivityScheduler();
|
|
39
|
+
const depthRefreshScheduler = createShadowMapExperimentDepthRefreshScheduler();
|
|
40
|
+
const influenceCulling = createShadowMapExperimentInfluenceCullingState(0.25);
|
|
41
|
+
const candidateLayerByMesh = new Map();
|
|
42
|
+
const candidateMeshCounts = { static: 0, dynamic: 0 };
|
|
43
|
+
const meshDataCacheByMesh = new Map();
|
|
44
|
+
const conservativeCasterMeshes = new Set();
|
|
45
|
+
const pendingMeshObserverCleanups = new Map();
|
|
46
|
+
const pendingMeshObserverRestorations = new Map();
|
|
47
|
+
const pendingMeshRegistrationRollbacks = new Set();
|
|
48
|
+
const pendingSceneMeshRegistrations = new Set();
|
|
49
|
+
const pendingMeshUnregistrations = new Set();
|
|
50
|
+
const pendingRenderableMembershipRetries = new Map();
|
|
51
|
+
const pendingExplicitVisibility = new Map();
|
|
52
|
+
const pendingTransformMembershipRetries = new Set();
|
|
53
|
+
const activeCasterEntityIds = new Set();
|
|
54
|
+
const activeDeformationSourceCountByEntity = new Map();
|
|
55
|
+
const pendingFinalDeformationEntityIds = new Set();
|
|
56
|
+
const pendingDeformationInvalidations = new Map();
|
|
57
|
+
const pendingMembershipInvalidationLayers = new Set();
|
|
58
|
+
const pendingCompositeRefreshReasons = new Set();
|
|
59
|
+
const fallbackWarnings = new Set();
|
|
60
|
+
let pendingBackendAllocationCleanup = null;
|
|
61
|
+
let pendingFallbackReason = null;
|
|
62
|
+
let pendingCoverageMembershipRebuild = false;
|
|
63
|
+
// Scene-fit bounds are read only at an explicit membership invalidation
|
|
64
|
+
// boundary, never by an otherwise idle render frame.
|
|
65
|
+
let sceneFitCoverageInvalidated = options.coverageMode === 'scene-fit';
|
|
66
|
+
let syncingSceneFitMembership = false;
|
|
67
|
+
let cullingStable = false;
|
|
68
|
+
let lastCullingInvalidationReason = null;
|
|
69
|
+
let effectiveCoverageCenter = null;
|
|
70
|
+
let effectiveCoverageExtent = null;
|
|
71
|
+
let effectiveCoverageNear = null;
|
|
72
|
+
let effectiveCoverageFar = null;
|
|
73
|
+
const objectByEntity = new Map();
|
|
74
|
+
const objectByRenderable = new Map();
|
|
75
|
+
const meshesByRenderable = new Map();
|
|
76
|
+
const bindings = new Map();
|
|
77
|
+
const automaticActivityCandidateBindings = new Map();
|
|
78
|
+
const receiverStates = new Map();
|
|
79
|
+
const receiverTargetByMesh = new Map();
|
|
80
|
+
// The target, rather than the requesting mesh, owns a failed receiver write:
|
|
81
|
+
// several source meshes can resolve to the same Babylon receiver.
|
|
82
|
+
const pendingReceiverRestorations = new Map();
|
|
83
|
+
const receiverRestorationAttemptsThisFlush = new Set();
|
|
84
|
+
const diagnostics = [];
|
|
85
|
+
const diagnosticKeys = new Set();
|
|
86
|
+
const runtimeRegistrations = new Map();
|
|
87
|
+
const sceneOwnedMeshes = new Set(scene.meshes);
|
|
88
|
+
const runtimeOwnedMeshes = new Set();
|
|
89
|
+
const explicitRuntimeActivityEntityIds = new Set();
|
|
90
|
+
const automaticAnimationGroups = new Map();
|
|
91
|
+
const automaticAnimationGroupsByResolvedTarget = new Map();
|
|
92
|
+
const automaticAnimationGroupsByResolvedTargetAncestor = new Map();
|
|
93
|
+
const automaticAnimationGroupsByResolvedTargetRenderable = new Map();
|
|
94
|
+
// Failed dormant source or full-detach cleanup is the only automatic-
|
|
95
|
+
// activity work allowed to wake the frame hook. Normal playback is event-driven.
|
|
96
|
+
const pendingAutomaticAnimationGroupSourceCleanups = new Set();
|
|
97
|
+
const pendingAutomaticAnimationGroupDetachCleanups = new Set();
|
|
98
|
+
const pendingAutomaticAnimationGroupReconciliations = new Set();
|
|
99
|
+
const pendingAutomaticAnimationGroupAttachments = new Set();
|
|
100
|
+
let pendingAutomaticActivityCandidateTransition = null;
|
|
101
|
+
let automaticActivityCandidateRevision = 0;
|
|
102
|
+
let automaticActivityTargetResolutionCount = 0;
|
|
103
|
+
let suppressAutomaticActivityCandidateRevision = false;
|
|
104
|
+
let stagingPlanCandidateTransitions = false;
|
|
105
|
+
let applyAutomaticActivityCandidateTransition = () => false;
|
|
106
|
+
let flushPendingAutomaticActivityCandidateTransition = () => { };
|
|
107
|
+
let flushingRefresh = false;
|
|
108
|
+
let pendingPlanRollbackRecovery = null;
|
|
109
|
+
let activatingRecoveryPlan = null;
|
|
110
|
+
let allowingRecoveryActivityRegistration = false;
|
|
111
|
+
const bumpAutomaticActivityCandidateRevision = () => {
|
|
112
|
+
if (suppressAutomaticActivityCandidateRevision)
|
|
113
|
+
return;
|
|
114
|
+
automaticActivityCandidateRevision += 1;
|
|
115
|
+
};
|
|
116
|
+
const isAutomaticActivityCandidateObject = (object) => object?.cast === true
|
|
117
|
+
&& object.updateClass !== 'static'
|
|
118
|
+
&& !explicitRuntimeActivityEntityIds.has(object.entityId);
|
|
119
|
+
const haveSameAutomaticActivityCandidates = (left, right, leftCandidateObjects = left.objects) => {
|
|
120
|
+
// Incremental runtime mutations intentionally leave `plan.objects` as the
|
|
121
|
+
// authored operational snapshot and advance only its revision. An
|
|
122
|
+
// authored update, on the other hand, arrives as a fully composed Plan.
|
|
123
|
+
// Compare the same candidate topology without forcing the operational
|
|
124
|
+
// snapshot to materialize every runtime overlay object.
|
|
125
|
+
const signature = (objects) => objects
|
|
126
|
+
.filter(isAutomaticActivityCandidateObject)
|
|
127
|
+
.flatMap(object => object.renderableIds.map(renderableId => `${object.entityId}\u0000${renderableId}\u0000${object.updateClass}`))
|
|
128
|
+
.sort();
|
|
129
|
+
const leftCandidates = signature(leftCandidateObjects);
|
|
130
|
+
const rightCandidates = signature(right.objects);
|
|
131
|
+
return leftCandidates.length === rightCandidates.length
|
|
132
|
+
&& leftCandidates.every((candidate, index) => candidate === rightCandidates[index]);
|
|
133
|
+
};
|
|
134
|
+
const updateAutomaticActivityCandidateBinding = (candidateView, binding, object) => {
|
|
135
|
+
// Candidate topology describes which registered meshes can ever provide
|
|
136
|
+
// automatic activity. Runtime visibility only controls caster membership
|
|
137
|
+
// and entity eligibility; it must not invalidate every AnimationGroup
|
|
138
|
+
// binding when a runtime state temporarily hides a model.
|
|
139
|
+
const shouldTrack = isAutomaticActivityCandidateObject(object);
|
|
140
|
+
const existing = candidateView.get(binding.mesh);
|
|
141
|
+
const tracked = existing !== undefined;
|
|
142
|
+
// A mesh can keep the same Babylon identity while its plan renderable is
|
|
143
|
+
// reassigned to another entity. Treat that as a topology transition too:
|
|
144
|
+
// the automatic source belongs to the entity, not just to the mesh.
|
|
145
|
+
if (shouldTrack && tracked
|
|
146
|
+
&& existing.entityId === binding.entityId
|
|
147
|
+
&& existing.renderableId === binding.renderableId)
|
|
148
|
+
return false;
|
|
149
|
+
if (!shouldTrack && !tracked)
|
|
150
|
+
return false;
|
|
151
|
+
if (shouldTrack)
|
|
152
|
+
candidateView.set(binding.mesh, binding);
|
|
153
|
+
else
|
|
154
|
+
candidateView.delete(binding.mesh);
|
|
155
|
+
return true;
|
|
156
|
+
};
|
|
157
|
+
const syncAutomaticActivityCandidateBindingsForRenderable = (renderableId) => {
|
|
158
|
+
const object = objectByRenderable.get(renderableId);
|
|
159
|
+
const renderableBindings = [...(meshesByRenderable.get(renderableId) ?? [])]
|
|
160
|
+
.flatMap(mesh => {
|
|
161
|
+
const binding = bindings.get(mesh);
|
|
162
|
+
return binding ? [binding] : [];
|
|
163
|
+
});
|
|
164
|
+
const alreadySynchronized = renderableBindings.every(binding => {
|
|
165
|
+
const existing = automaticActivityCandidateBindings.get(binding.mesh);
|
|
166
|
+
if (!isAutomaticActivityCandidateObject(object))
|
|
167
|
+
return existing === undefined;
|
|
168
|
+
return existing?.entityId === binding.entityId
|
|
169
|
+
&& existing.renderableId === binding.renderableId;
|
|
170
|
+
});
|
|
171
|
+
if (alreadySynchronized)
|
|
172
|
+
return false;
|
|
173
|
+
const nextCandidates = new Map(automaticActivityCandidateBindings);
|
|
174
|
+
let changed = false;
|
|
175
|
+
for (const binding of renderableBindings) {
|
|
176
|
+
changed = updateAutomaticActivityCandidateBinding(nextCandidates, binding, object) || changed;
|
|
177
|
+
}
|
|
178
|
+
if (changed && !applyAutomaticActivityCandidateTransition(nextCandidates)) {
|
|
179
|
+
throw new Error('shadowMapExperiment.automaticActivityCandidateCommitFailed');
|
|
180
|
+
}
|
|
181
|
+
return changed;
|
|
182
|
+
};
|
|
183
|
+
const assertUsable = (allowDisposing = false) => {
|
|
184
|
+
if (disposed || (disposing && !allowDisposing))
|
|
185
|
+
throw new Error('shadowMapExperiment.systemDisposed');
|
|
186
|
+
};
|
|
187
|
+
const assertNoPlanRollbackRecovery = () => {
|
|
188
|
+
if (pendingPlanRollbackRecovery) {
|
|
189
|
+
throw new Error('shadowMapExperiment.planRollbackRecoveryPending');
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
const rememberDiagnostic = (code, message, identity = {}) => {
|
|
193
|
+
const key = `${code}\u0000${identity.entityId ?? ''}\u0000${identity.renderableId ?? ''}`;
|
|
194
|
+
if (diagnosticKeys.has(key))
|
|
195
|
+
return;
|
|
196
|
+
diagnosticKeys.add(key);
|
|
197
|
+
diagnostics.push({ code, message, ...identity });
|
|
198
|
+
};
|
|
199
|
+
const requireRuntimeProfile = (basePlan, behaviorProfileId, entityId) => {
|
|
200
|
+
if (!basePlan || basePlan.schemaVersion !== 3) {
|
|
201
|
+
const code = 'shadowMapExperiment.runtimeProfileCatalogUnavailable';
|
|
202
|
+
rememberDiagnostic(code, 'Plan v3 behavior profile catalog is required for runtime shadow registration.', { entityId });
|
|
203
|
+
throw new Error(`${code}:plan-v${basePlan?.schemaVersion ?? 'none'}`);
|
|
204
|
+
}
|
|
205
|
+
const profile = basePlan.behaviorProfiles[behaviorProfileId];
|
|
206
|
+
if (!profile) {
|
|
207
|
+
const code = 'shadowMapExperiment.runtimeBehaviorProfileUnknown';
|
|
208
|
+
rememberDiagnostic(code, `Unknown runtime behavior profile: ${behaviorProfileId}`, { entityId });
|
|
209
|
+
throw new Error(`${code}:${behaviorProfileId}`);
|
|
210
|
+
}
|
|
211
|
+
if (profile.updateClass === 'auto') {
|
|
212
|
+
const code = 'shadowMapExperiment.runtimeBehaviorProfileAutoUnsupported';
|
|
213
|
+
rememberDiagnostic(code, `Runtime behavior profile ${behaviorProfileId} must declare a deterministic updateClass.`, { entityId });
|
|
214
|
+
throw new Error(`${code}:${behaviorProfileId}`);
|
|
215
|
+
}
|
|
216
|
+
return profile;
|
|
217
|
+
};
|
|
218
|
+
const composeEffectivePlan = (basePlan, records, overlayRevision) => {
|
|
219
|
+
if (!basePlan) {
|
|
220
|
+
if (records.length > 0)
|
|
221
|
+
throw new Error('shadowMapExperiment.runtimePlanUnavailable');
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
if (records.length === 0)
|
|
225
|
+
return basePlan;
|
|
226
|
+
const runtimeObjects = createRuntimePlanObjects(basePlan, records);
|
|
227
|
+
return Object.freeze({
|
|
228
|
+
...basePlan,
|
|
229
|
+
revision: basePlan.revision + overlayRevision,
|
|
230
|
+
objects: Object.freeze([...basePlan.objects, ...runtimeObjects]
|
|
231
|
+
.sort((left, right) => left.entityId.localeCompare(right.entityId))),
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
const createRuntimePlanObjects = (basePlan, records) => Object.freeze(records.map(record => {
|
|
235
|
+
const profile = requireRuntimeProfile(basePlan, record.behaviorProfileId, record.entityId);
|
|
236
|
+
if (record.activities.length > 0 && (!profile.cast || profile.updateClass === 'static')) {
|
|
237
|
+
throw new Error(`shadowMapExperiment.runtimeActivityProfileIneligible:${record.behaviorProfileId}`);
|
|
238
|
+
}
|
|
239
|
+
return Object.freeze({
|
|
240
|
+
entityId: record.entityId,
|
|
241
|
+
renderableIds: Object.freeze(record.renderables.map(renderable => renderable.renderableId).sort()),
|
|
242
|
+
cast: profile.cast,
|
|
243
|
+
receive: profile.receive,
|
|
244
|
+
updateClass: profile.updateClass,
|
|
245
|
+
behaviorProfileId: record.behaviorProfileId,
|
|
246
|
+
});
|
|
247
|
+
}));
|
|
248
|
+
const invalidateMaterializedEffectivePlan = () => {
|
|
249
|
+
materializedEffectivePlan = null;
|
|
250
|
+
};
|
|
251
|
+
const readMaterializedEffectivePlan = () => {
|
|
252
|
+
if (!sourcePlan || !plan)
|
|
253
|
+
return null;
|
|
254
|
+
if (!materializedEffectivePlan) {
|
|
255
|
+
materializedEffectivePlan = composeEffectivePlan(sourcePlan, [...runtimeRegistrations.values()].filter(record => !record.disposed), runtimeOverlayRevision);
|
|
256
|
+
effectivePlanMaterializationCount += 1;
|
|
257
|
+
}
|
|
258
|
+
return materializedEffectivePlan;
|
|
259
|
+
};
|
|
260
|
+
const updateOperationalPlanRevision = (overlayRevision) => {
|
|
261
|
+
if (!plan || !sourcePlan)
|
|
262
|
+
return;
|
|
263
|
+
plan = Object.freeze({
|
|
264
|
+
...plan,
|
|
265
|
+
revision: sourcePlan.revision + overlayRevision,
|
|
266
|
+
});
|
|
267
|
+
invalidateMaterializedEffectivePlan();
|
|
268
|
+
};
|
|
269
|
+
const requestRefreshFrame = (reason) => {
|
|
270
|
+
if (disposed || disposing || flushingRefresh || !options.requestFrame)
|
|
271
|
+
return;
|
|
272
|
+
try {
|
|
273
|
+
options.requestFrame(reason);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
const code = 'shadowMapExperiment.frameRequestFailed';
|
|
277
|
+
if (diagnosticKeys.has(code))
|
|
278
|
+
return;
|
|
279
|
+
diagnosticKeys.add(code);
|
|
280
|
+
diagnostics.push({
|
|
281
|
+
code,
|
|
282
|
+
message: `Shadow refresh frame request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
const enqueueRefresh = (reason) => {
|
|
287
|
+
activityScheduler.markDirty(reason);
|
|
288
|
+
requestRefreshFrame(`shadow-map-experiment-${reason}`);
|
|
289
|
+
};
|
|
290
|
+
const markDepthDirty = (layer, reason) => {
|
|
291
|
+
depthRefreshScheduler.markDirty(layer, reason);
|
|
292
|
+
requestRefreshFrame(`shadow-map-experiment-${reason}`);
|
|
293
|
+
};
|
|
294
|
+
const markAllDepthDirty = (reason) => {
|
|
295
|
+
depthRefreshScheduler.markAllDirty(reason);
|
|
296
|
+
requestRefreshFrame(`shadow-map-experiment-${reason}`);
|
|
297
|
+
};
|
|
298
|
+
const markCompositeDepthDirty = (reason) => {
|
|
299
|
+
depthRefreshScheduler.markCompositeDirty(reason);
|
|
300
|
+
requestRefreshFrame(`shadow-map-experiment-${reason}`);
|
|
301
|
+
};
|
|
302
|
+
const invalidateSceneFitBounds = (renderableIds = null, refreshLocalBounds = false) => {
|
|
303
|
+
if (options.coverageMode !== 'scene-fit')
|
|
304
|
+
return;
|
|
305
|
+
const selected = renderableIds ? new Set(renderableIds) : null;
|
|
306
|
+
if (refreshLocalBounds) {
|
|
307
|
+
for (const binding of bindings.values()) {
|
|
308
|
+
if (!selected || selected.has(binding.renderableId)) {
|
|
309
|
+
binding.sceneFitLocalBounds = readMeshLocalBounds(binding.mesh);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
sceneFitCoverageInvalidated = true;
|
|
314
|
+
};
|
|
315
|
+
const syncSceneFitCoverage = (force = false) => {
|
|
316
|
+
if (!plan?.enabled || !lightState || options.coverageMode !== 'scene-fit')
|
|
317
|
+
return;
|
|
318
|
+
if (!force && !sceneFitCoverageInvalidated)
|
|
319
|
+
return;
|
|
320
|
+
const sceneBounds = collectActiveCasterWorldBounds(bindings, registry);
|
|
321
|
+
if (!sceneBounds) {
|
|
322
|
+
sceneFitCoverageInvalidated = false;
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const nextCoverage = createSceneFitCoverage(plan, sceneBounds);
|
|
326
|
+
const currentCenter = effectiveCoverageCenter ?? plan.generator.bounds.center;
|
|
327
|
+
const currentExtent = effectiveCoverageExtent ?? plan.generator.bounds.extent;
|
|
328
|
+
const currentNear = effectiveCoverageNear ?? plan.generator.bounds.near;
|
|
329
|
+
const currentFar = effectiveCoverageFar ?? plan.generator.bounds.far;
|
|
330
|
+
if (!force
|
|
331
|
+
&& areTuple3Equal(currentCenter, nextCoverage.center)
|
|
332
|
+
&& areTuple3Equal(currentExtent, nextCoverage.extent)
|
|
333
|
+
&& currentNear === nextCoverage.near
|
|
334
|
+
&& currentFar === nextCoverage.far) {
|
|
335
|
+
sceneFitCoverageInvalidated = false;
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
effectiveCoverageCenter = nextCoverage.center;
|
|
339
|
+
effectiveCoverageExtent = nextCoverage.extent;
|
|
340
|
+
effectiveCoverageNear = nextCoverage.near;
|
|
341
|
+
effectiveCoverageFar = nextCoverage.far;
|
|
342
|
+
configureLight(lightState.light, plan, nextCoverage);
|
|
343
|
+
pendingCoverageMembershipRebuild = true;
|
|
344
|
+
sceneFitCoverageInvalidated = false;
|
|
345
|
+
lastCullingInvalidationReason = 'scene-fit-coverage-changed';
|
|
346
|
+
markAllDepthDirty('camera-influence-region-changed');
|
|
347
|
+
};
|
|
348
|
+
const handleCompositorLifecycleEvent = (event) => {
|
|
349
|
+
if (disposed || disposing)
|
|
350
|
+
return;
|
|
351
|
+
if (event === 'ready')
|
|
352
|
+
markAllDepthDirty('compositor-ready');
|
|
353
|
+
else
|
|
354
|
+
requestRefreshFrame(`shadow-map-experiment-compositor-${event}`);
|
|
355
|
+
};
|
|
356
|
+
const routeDirtyReason = (reason) => {
|
|
357
|
+
if (isDynamicDirtyReason(reason)) {
|
|
358
|
+
depthRefreshScheduler.markDirty('dynamic', reason);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (isStaticDirtyReason(reason)) {
|
|
362
|
+
depthRefreshScheduler.markDirty('static', reason);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
depthRefreshScheduler.markAllDirty(reason);
|
|
366
|
+
};
|
|
367
|
+
const syncCandidateCounts = () => {
|
|
368
|
+
depthBackend?.setCandidateCounts(candidateMeshCounts.static, candidateMeshCounts.dynamic);
|
|
369
|
+
};
|
|
370
|
+
const setCandidateLayer = (mesh, nextLayer) => {
|
|
371
|
+
const previousLayer = candidateLayerByMesh.get(mesh) ?? null;
|
|
372
|
+
if (previousLayer === nextLayer)
|
|
373
|
+
return;
|
|
374
|
+
if (previousLayer)
|
|
375
|
+
candidateMeshCounts[previousLayer] -= 1;
|
|
376
|
+
if (nextLayer) {
|
|
377
|
+
candidateLayerByMesh.set(mesh, nextLayer);
|
|
378
|
+
candidateMeshCounts[nextLayer] += 1;
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
candidateLayerByMesh.delete(mesh);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
const warnForFallback = (reason) => {
|
|
385
|
+
if (fallbackWarnings.has(reason))
|
|
386
|
+
return;
|
|
387
|
+
fallbackWarnings.add(reason);
|
|
388
|
+
console.warn(`[shadow-map-experiment] split backend unavailable; using single ShadowMap (${reason})`);
|
|
389
|
+
};
|
|
390
|
+
const flushRefresh = () => {
|
|
391
|
+
if (disposed || disposing)
|
|
392
|
+
return;
|
|
393
|
+
let postFlushFrameReason = null;
|
|
394
|
+
flushingRefresh = true;
|
|
395
|
+
receiverRestorationAttemptsThisFlush.clear();
|
|
396
|
+
try {
|
|
397
|
+
// Recovery owns the old transaction until cleanup and (when applicable)
|
|
398
|
+
// reactivation have both completed. Do one bounded attempt and leave all
|
|
399
|
+
// ordinary queues untouched until that ownership is released.
|
|
400
|
+
if (pendingPlanRollbackRecovery) {
|
|
401
|
+
flushPendingPlanRollbackRecovery();
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
flushPendingReceiverRestorations();
|
|
405
|
+
// No animation group is polled during normal rendering. A frame retry
|
|
406
|
+
// exists only while a previous observer/source teardown is incomplete.
|
|
407
|
+
flushPendingAutomaticAnimationGroupDetachCleanups();
|
|
408
|
+
if (pendingAutomaticActivityCandidateTransition
|
|
409
|
+
&& pendingMeshUnregistrations.size === 0
|
|
410
|
+
&& pendingRenderableMembershipRetries.size === 0) {
|
|
411
|
+
// One candidate-transition retry gets one cleanup attempt per frame.
|
|
412
|
+
// Running the generic cleanup queue again in the same frame would
|
|
413
|
+
// collapse consecutive retry boundaries and could spin on a failing
|
|
414
|
+
// disposer. A mesh-unregistration owner retries the transition from
|
|
415
|
+
// inside its own backend/observer/registry journal instead.
|
|
416
|
+
flushPendingAutomaticActivityCandidateTransition();
|
|
417
|
+
}
|
|
418
|
+
else if (!pendingAutomaticActivityCandidateTransition) {
|
|
419
|
+
flushPendingAutomaticAnimationGroupSourceCleanups();
|
|
420
|
+
}
|
|
421
|
+
flushPendingAutomaticAnimationGroupAttachments();
|
|
422
|
+
flushPendingAutomaticAnimationGroupReconciliations();
|
|
423
|
+
flushPendingMeshObserverCleanups();
|
|
424
|
+
flushPendingMeshRegistrationRollbacks();
|
|
425
|
+
flushPendingBackendAllocationCleanup();
|
|
426
|
+
recoverPendingFallbackBackend();
|
|
427
|
+
if (!depthBackend || !registry)
|
|
428
|
+
return;
|
|
429
|
+
downgradeFailedSplitBackend();
|
|
430
|
+
if (!depthBackend)
|
|
431
|
+
return;
|
|
432
|
+
flushPendingMeshUnregistrations();
|
|
433
|
+
flushPendingSceneMeshRegistrations();
|
|
434
|
+
flushPendingRenderableMembershipRetries();
|
|
435
|
+
flushPendingTransformMembershipRetries();
|
|
436
|
+
flushMembershipInvalidations();
|
|
437
|
+
registry.consumeDirtyReasons();
|
|
438
|
+
const activityDecision = activityScheduler.consumeFrameDecision();
|
|
439
|
+
resyncExplicitDeformationMembership();
|
|
440
|
+
for (const reason of activityDecision.reasons)
|
|
441
|
+
routeDirtyReason(reason);
|
|
442
|
+
// Membership and deformation invalidations refresh local bounds first;
|
|
443
|
+
// scene-fit consumes the resulting committed topology once per event.
|
|
444
|
+
syncSceneFitCoverage();
|
|
445
|
+
updateCameraInfluenceCoverage();
|
|
446
|
+
const decision = depthRefreshScheduler.consumeFrameDecision();
|
|
447
|
+
let refreshDecision = decision;
|
|
448
|
+
if (options.requestFrame && depthBackend.mode === 'split-static-dynamic') {
|
|
449
|
+
const settlementReasons = Object.freeze([...pendingCompositeRefreshReasons]);
|
|
450
|
+
pendingCompositeRefreshReasons.clear();
|
|
451
|
+
for (const reason of decision.composite.reasons)
|
|
452
|
+
pendingCompositeRefreshReasons.add(reason);
|
|
453
|
+
refreshDecision = Object.freeze({
|
|
454
|
+
static: decision.static,
|
|
455
|
+
dynamic: decision.dynamic,
|
|
456
|
+
composite: Object.freeze({
|
|
457
|
+
shouldRefresh: settlementReasons.length > 0,
|
|
458
|
+
reasons: settlementReasons,
|
|
459
|
+
}),
|
|
460
|
+
});
|
|
461
|
+
if (pendingCompositeRefreshReasons.size > 0) {
|
|
462
|
+
postFlushFrameReason = 'shadow-map-experiment-composite-settlement';
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
else if (pendingCompositeRefreshReasons.size > 0) {
|
|
466
|
+
const compositeReasons = Object.freeze([
|
|
467
|
+
...new Set([...pendingCompositeRefreshReasons, ...decision.composite.reasons]),
|
|
468
|
+
]);
|
|
469
|
+
pendingCompositeRefreshReasons.clear();
|
|
470
|
+
refreshDecision = Object.freeze({
|
|
471
|
+
static: decision.static,
|
|
472
|
+
dynamic: decision.dynamic,
|
|
473
|
+
composite: Object.freeze({ shouldRefresh: true, reasons: compositeReasons }),
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
if (!refreshDecision.static.shouldRefresh
|
|
477
|
+
&& !refreshDecision.dynamic.shouldRefresh
|
|
478
|
+
&& !refreshDecision.composite.shouldRefresh)
|
|
479
|
+
return;
|
|
480
|
+
depthBackend.refresh(refreshDecision);
|
|
481
|
+
if (!refreshDecision.composite.shouldRefresh)
|
|
482
|
+
return;
|
|
483
|
+
registry.markShadowRendered();
|
|
484
|
+
refreshCount += 1;
|
|
485
|
+
lastRefreshReasons = refreshDecision.composite.reasons;
|
|
486
|
+
}
|
|
487
|
+
finally {
|
|
488
|
+
receiverRestorationAttemptsThisFlush.clear();
|
|
489
|
+
if (pendingPlanRollbackRecovery) {
|
|
490
|
+
postFlushFrameReason = 'shadow-map-experiment-plan-rollback-recovery';
|
|
491
|
+
}
|
|
492
|
+
const pendingFallbackRecovery = pendingFallbackReason !== null
|
|
493
|
+
&& depthBackend === null
|
|
494
|
+
&& plan?.enabled === true
|
|
495
|
+
&& plan.generator.backend.fallback !== 'disable';
|
|
496
|
+
if (!postFlushFrameReason
|
|
497
|
+
&& (pendingAutomaticActivityCandidateTransition !== null
|
|
498
|
+
|| pendingAutomaticAnimationGroupSourceCleanups.size > 0
|
|
499
|
+
|| pendingAutomaticAnimationGroupDetachCleanups.size > 0
|
|
500
|
+
|| pendingAutomaticAnimationGroupAttachments.size > 0
|
|
501
|
+
|| pendingAutomaticAnimationGroupReconciliations.size > 0
|
|
502
|
+
|| pendingSceneMeshRegistrations.size > 0
|
|
503
|
+
|| pendingMeshRegistrationRollbacks.size > 0
|
|
504
|
+
|| pendingMeshObserverCleanups.size > 0
|
|
505
|
+
|| pendingMeshObserverRestorations.size > 0
|
|
506
|
+
|| pendingReceiverRestorations.size > 0
|
|
507
|
+
|| pendingBackendAllocationCleanup !== null
|
|
508
|
+
|| pendingFallbackRecovery
|
|
509
|
+
|| pendingMeshUnregistrations.size > 0
|
|
510
|
+
|| pendingRenderableMembershipRetries.size > 0
|
|
511
|
+
|| pendingTransformMembershipRetries.size > 0
|
|
512
|
+
|| pendingFinalDeformationEntityIds.size > 0
|
|
513
|
+
|| pendingDeformationInvalidations.size > 0
|
|
514
|
+
|| pendingCoverageMembershipRebuild
|
|
515
|
+
|| pendingMembershipInvalidationLayers.size > 0)) {
|
|
516
|
+
postFlushFrameReason = pendingAutomaticActivityCandidateTransition !== null
|
|
517
|
+
|| pendingAutomaticAnimationGroupSourceCleanups.size > 0
|
|
518
|
+
|| pendingAutomaticAnimationGroupDetachCleanups.size > 0
|
|
519
|
+
|| pendingAutomaticAnimationGroupAttachments.size > 0
|
|
520
|
+
|| pendingAutomaticAnimationGroupReconciliations.size > 0
|
|
521
|
+
? 'shadow-map-experiment-automatic-activity-cleanup-retry'
|
|
522
|
+
: pendingReceiverRestorations.size > 0
|
|
523
|
+
? 'shadow-map-experiment-receiver-restoration-retry'
|
|
524
|
+
: pendingBackendAllocationCleanup !== null
|
|
525
|
+
? 'shadow-map-experiment-backend-allocation-cleanup-retry'
|
|
526
|
+
: pendingFallbackRecovery
|
|
527
|
+
? 'shadow-map-experiment-fallback-retry'
|
|
528
|
+
: 'shadow-map-experiment-membership-retry';
|
|
529
|
+
}
|
|
530
|
+
flushingRefresh = false;
|
|
531
|
+
if (postFlushFrameReason)
|
|
532
|
+
requestRefreshFrame(postFlushFrameReason);
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
const syncActivityEligibility = (entityId) => {
|
|
536
|
+
const object = objectByEntity.get(entityId);
|
|
537
|
+
const active = object?.renderableIds.some(renderableId => registry?.getRecord(renderableId)?.state === 'active') === true;
|
|
538
|
+
activityScheduler.setEntityActive(entityId, active);
|
|
539
|
+
if (active)
|
|
540
|
+
activeCasterEntityIds.add(entityId);
|
|
541
|
+
else
|
|
542
|
+
activeCasterEntityIds.delete(entityId);
|
|
543
|
+
};
|
|
544
|
+
const shouldRetainCaster = (mesh, updateClass, reuseCachedBounds = false) => {
|
|
545
|
+
if (plan?.culling.enabled === false) {
|
|
546
|
+
conservativeCasterMeshes.delete(mesh);
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
const deformable = isDeformableCaster(mesh, updateClass);
|
|
550
|
+
const binding = bindings.get(mesh);
|
|
551
|
+
let currentBoundingInfo;
|
|
552
|
+
try {
|
|
553
|
+
currentBoundingInfo = mesh.getBoundingInfo();
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
currentBoundingInfo = undefined;
|
|
557
|
+
}
|
|
558
|
+
const canReuseBounds = reuseCachedBounds
|
|
559
|
+
&& binding?.influenceWorldBounds !== undefined
|
|
560
|
+
&& binding.influenceBoundingInfo === currentBoundingInfo;
|
|
561
|
+
const bounds = canReuseBounds
|
|
562
|
+
? binding.influenceWorldBounds ?? null
|
|
563
|
+
: readCasterWorldBounds(mesh, deformable, deformable
|
|
564
|
+
? meshDataCacheByMesh.get(mesh) ?? createMeshDataCache(mesh, meshDataCacheByMesh)
|
|
565
|
+
: null);
|
|
566
|
+
if (binding && !canReuseBounds) {
|
|
567
|
+
binding.influenceWorldBounds = bounds;
|
|
568
|
+
binding.influenceBoundingInfo = currentBoundingInfo;
|
|
569
|
+
}
|
|
570
|
+
if (!bounds) {
|
|
571
|
+
conservativeCasterMeshes.add(mesh);
|
|
572
|
+
const binding = bindings.get(mesh);
|
|
573
|
+
const code = deformable
|
|
574
|
+
? 'shadowMapExperiment.deformationBoundsUnavailable'
|
|
575
|
+
: 'shadowMapExperiment.casterBoundsUnavailable';
|
|
576
|
+
const key = `${code}:${mesh.uniqueId}`;
|
|
577
|
+
if (!diagnosticKeys.has(key)) {
|
|
578
|
+
diagnosticKeys.add(key);
|
|
579
|
+
diagnostics.push({
|
|
580
|
+
code,
|
|
581
|
+
message: deformable
|
|
582
|
+
? 'Deformed caster bounds were unavailable, so influence culling retained the caster conservatively.'
|
|
583
|
+
: 'Caster bounds were unavailable, so influence culling retained the caster conservatively.',
|
|
584
|
+
entityId: binding?.entityId,
|
|
585
|
+
renderableId: binding?.renderableId,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
conservativeCasterMeshes.delete(mesh);
|
|
591
|
+
}
|
|
592
|
+
return influenceCulling.shouldRetainCaster({
|
|
593
|
+
casterBounds: bounds,
|
|
594
|
+
lightDirection: plan?.light.direction ?? null,
|
|
595
|
+
projectionDistance: plan
|
|
596
|
+
? Math.max(0, (effectiveCoverageFar ?? plan.generator.bounds.far)
|
|
597
|
+
- (effectiveCoverageNear ?? plan.generator.bounds.near))
|
|
598
|
+
: 0,
|
|
599
|
+
});
|
|
600
|
+
};
|
|
601
|
+
const resyncExplicitDeformationMembership = () => {
|
|
602
|
+
const finalEntityIds = [...pendingFinalDeformationEntityIds];
|
|
603
|
+
const deformationInvalidations = [...pendingDeformationInvalidations.entries()];
|
|
604
|
+
if (finalEntityIds.length === 0 && deformationInvalidations.length === 0)
|
|
605
|
+
return;
|
|
606
|
+
const renderableIds = new Set();
|
|
607
|
+
if (finalEntityIds.length > 0) {
|
|
608
|
+
for (const entityId of finalEntityIds) {
|
|
609
|
+
if (!activeCasterEntityIds.has(entityId))
|
|
610
|
+
continue;
|
|
611
|
+
const object = objectByEntity.get(entityId);
|
|
612
|
+
if (!object)
|
|
613
|
+
continue;
|
|
614
|
+
for (const renderableId of object.renderableIds)
|
|
615
|
+
renderableIds.add(renderableId);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
for (const [entityId] of deformationInvalidations) {
|
|
619
|
+
if (!activeCasterEntityIds.has(entityId))
|
|
620
|
+
continue;
|
|
621
|
+
const object = objectByEntity.get(entityId);
|
|
622
|
+
if (!object)
|
|
623
|
+
continue;
|
|
624
|
+
for (const renderableId of object.renderableIds)
|
|
625
|
+
renderableIds.add(renderableId);
|
|
626
|
+
}
|
|
627
|
+
for (const renderableId of renderableIds)
|
|
628
|
+
syncCaster(renderableId);
|
|
629
|
+
if (renderableIds.size > 0)
|
|
630
|
+
invalidateSceneFitBounds(renderableIds, true);
|
|
631
|
+
for (const entityId of finalEntityIds)
|
|
632
|
+
pendingFinalDeformationEntityIds.delete(entityId);
|
|
633
|
+
if (finalEntityIds.length > 0) {
|
|
634
|
+
markDepthDirty('dynamic', 'caster-activity-stopped');
|
|
635
|
+
}
|
|
636
|
+
for (const [entityId, invalidationReasons] of deformationInvalidations) {
|
|
637
|
+
pendingDeformationInvalidations.delete(entityId);
|
|
638
|
+
for (const reason of invalidationReasons)
|
|
639
|
+
markDepthDirty('dynamic', reason);
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
const resyncAllCasterMembership = () => {
|
|
643
|
+
const beforeStatic = depthBackend?.getEvidence().static.retainedMeshCount ?? 0;
|
|
644
|
+
const beforeDynamic = depthBackend?.getEvidence().dynamic.retainedMeshCount ?? 0;
|
|
645
|
+
const casterRenderableIds = new Set();
|
|
646
|
+
for (const mesh of candidateLayerByMesh.keys()) {
|
|
647
|
+
const binding = bindings.get(mesh);
|
|
648
|
+
if (binding)
|
|
649
|
+
casterRenderableIds.add(binding.renderableId);
|
|
650
|
+
}
|
|
651
|
+
for (const renderableId of casterRenderableIds)
|
|
652
|
+
syncCaster(renderableId, true);
|
|
653
|
+
const after = depthBackend?.getEvidence();
|
|
654
|
+
const changed = [];
|
|
655
|
+
if (beforeStatic !== (after?.static.retainedMeshCount ?? 0))
|
|
656
|
+
changed.push('static');
|
|
657
|
+
if (beforeDynamic !== (after?.dynamic.retainedMeshCount ?? 0))
|
|
658
|
+
changed.push('dynamic');
|
|
659
|
+
return Object.freeze(changed);
|
|
660
|
+
};
|
|
661
|
+
const resyncLayerCasterMembership = (layer) => {
|
|
662
|
+
const renderableIds = new Set();
|
|
663
|
+
for (const [mesh, candidateLayer] of candidateLayerByMesh) {
|
|
664
|
+
if (candidateLayer !== layer)
|
|
665
|
+
continue;
|
|
666
|
+
const binding = bindings.get(mesh);
|
|
667
|
+
if (binding)
|
|
668
|
+
renderableIds.add(binding.renderableId);
|
|
669
|
+
}
|
|
670
|
+
for (const renderableId of renderableIds)
|
|
671
|
+
syncCaster(renderableId);
|
|
672
|
+
};
|
|
673
|
+
const flushMembershipInvalidations = () => {
|
|
674
|
+
for (const layer of pendingMembershipInvalidationLayers)
|
|
675
|
+
resyncLayerCasterMembership(layer);
|
|
676
|
+
pendingMembershipInvalidationLayers.clear();
|
|
677
|
+
};
|
|
678
|
+
const updateCameraInfluenceCoverage = () => {
|
|
679
|
+
if (!plan?.enabled)
|
|
680
|
+
return;
|
|
681
|
+
if (!plan.culling.enabled) {
|
|
682
|
+
cullingStable = true;
|
|
683
|
+
pendingCoverageMembershipRebuild = false;
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const camera = scene.activeCamera;
|
|
687
|
+
const decision = influenceCulling.updateCoverage({
|
|
688
|
+
cameraId: camera ? String(camera.uniqueId) : 'none',
|
|
689
|
+
projectionSignature: camera
|
|
690
|
+
? `${readCameraProjectionSignature(camera, scene)}:${JSON.stringify({
|
|
691
|
+
...plan.generator.bounds,
|
|
692
|
+
center: effectiveCoverageCenter ?? plan.generator.bounds.center,
|
|
693
|
+
extent: effectiveCoverageExtent ?? plan.generator.bounds.extent,
|
|
694
|
+
near: effectiveCoverageNear ?? plan.generator.bounds.near,
|
|
695
|
+
far: effectiveCoverageFar ?? plan.generator.bounds.far,
|
|
696
|
+
})}`
|
|
697
|
+
: 'none',
|
|
698
|
+
visibleBounds: camera
|
|
699
|
+
? calculateCameraShadowCoverage(camera, plan, effectiveCoverageCenter ?? plan.generator.bounds.center, effectiveCoverageExtent ?? plan.generator.bounds.extent)
|
|
700
|
+
: null,
|
|
701
|
+
});
|
|
702
|
+
const retryingMembershipRebuild = pendingCoverageMembershipRebuild;
|
|
703
|
+
cullingStable = !decision.rebuilt && !retryingMembershipRebuild;
|
|
704
|
+
if (!decision.rebuilt && !retryingMembershipRebuild)
|
|
705
|
+
return;
|
|
706
|
+
if (decision.rebuilt && !retryingMembershipRebuild) {
|
|
707
|
+
lastCullingInvalidationReason = 'camera-influence-region-changed';
|
|
708
|
+
}
|
|
709
|
+
let changedLayers;
|
|
710
|
+
try {
|
|
711
|
+
syncingSceneFitMembership = true;
|
|
712
|
+
changedLayers = resyncAllCasterMembership();
|
|
713
|
+
pendingCoverageMembershipRebuild = false;
|
|
714
|
+
}
|
|
715
|
+
catch (error) {
|
|
716
|
+
pendingCoverageMembershipRebuild = true;
|
|
717
|
+
cullingStable = false;
|
|
718
|
+
throw error;
|
|
719
|
+
}
|
|
720
|
+
finally {
|
|
721
|
+
syncingSceneFitMembership = false;
|
|
722
|
+
}
|
|
723
|
+
const dirtyLayers = retryingMembershipRebuild
|
|
724
|
+
? Object.freeze(['static', 'dynamic'])
|
|
725
|
+
: changedLayers;
|
|
726
|
+
for (const layer of dirtyLayers) {
|
|
727
|
+
markDepthDirty(layer, 'camera-influence-region-changed');
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
const downgradeFailedSplitBackend = () => {
|
|
731
|
+
if (!depthBackend || depthBackend.mode !== 'split-static-dynamic' || !plan?.enabled || !lightState)
|
|
732
|
+
return;
|
|
733
|
+
const runtimeFailureReason = depthBackend.getRuntimeFailureReason();
|
|
734
|
+
if (!runtimeFailureReason)
|
|
735
|
+
return;
|
|
736
|
+
const failedBackend = depthBackend;
|
|
737
|
+
try {
|
|
738
|
+
failedBackend.dispose();
|
|
739
|
+
}
|
|
740
|
+
catch (error) {
|
|
741
|
+
depthBackend = null;
|
|
742
|
+
generator = null;
|
|
743
|
+
pendingFallbackReason = runtimeFailureReason;
|
|
744
|
+
pendingBackendAllocationCleanup = () => {
|
|
745
|
+
try {
|
|
746
|
+
failedBackend.dispose();
|
|
747
|
+
return Object.freeze([]);
|
|
748
|
+
}
|
|
749
|
+
catch (retryError) {
|
|
750
|
+
return Object.freeze([retryError]);
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
throw error;
|
|
754
|
+
}
|
|
755
|
+
depthBackend = null;
|
|
756
|
+
generator = null;
|
|
757
|
+
pendingFallbackReason = runtimeFailureReason;
|
|
758
|
+
recoverPendingFallbackBackend();
|
|
759
|
+
};
|
|
760
|
+
const rememberBackendAllocationCleanup = (error) => {
|
|
761
|
+
const cleanup = readBabylonShadowMapExperimentAllocationCleanup(error);
|
|
762
|
+
if (cleanup)
|
|
763
|
+
pendingBackendAllocationCleanup = cleanup;
|
|
764
|
+
};
|
|
765
|
+
const flushPendingBackendAllocationCleanup = () => {
|
|
766
|
+
if (!pendingBackendAllocationCleanup)
|
|
767
|
+
return;
|
|
768
|
+
const cleanupErrors = pendingBackendAllocationCleanup();
|
|
769
|
+
if (cleanupErrors.length > 0) {
|
|
770
|
+
throw Object.assign(new Error('shadowMapExperiment.backendAllocationCleanupFailed'), { cleanupErrors });
|
|
771
|
+
}
|
|
772
|
+
pendingBackendAllocationCleanup = null;
|
|
773
|
+
};
|
|
774
|
+
const prepareBackendCasterMembership = (backend) => {
|
|
775
|
+
let staticCandidateCount = 0;
|
|
776
|
+
let dynamicCandidateCount = 0;
|
|
777
|
+
for (const [mesh, layer] of candidateLayerByMesh) {
|
|
778
|
+
if (layer === 'static')
|
|
779
|
+
staticCandidateCount += 1;
|
|
780
|
+
else
|
|
781
|
+
dynamicCandidateCount += 1;
|
|
782
|
+
const binding = bindings.get(mesh);
|
|
783
|
+
const updateClass = binding ? objectByRenderable.get(binding.renderableId)?.updateClass : null;
|
|
784
|
+
if (!updateClass || shouldRetainCaster(mesh, updateClass))
|
|
785
|
+
backend.addCaster(mesh, layer);
|
|
786
|
+
}
|
|
787
|
+
backend.setCandidateCounts(staticCandidateCount, dynamicCandidateCount);
|
|
788
|
+
};
|
|
789
|
+
const recoverPendingFallbackBackend = () => {
|
|
790
|
+
if (!pendingFallbackReason || depthBackend || !plan?.enabled || !lightState)
|
|
791
|
+
return;
|
|
792
|
+
flushPendingBackendAllocationCleanup();
|
|
793
|
+
const fallbackReason = pendingFallbackReason;
|
|
794
|
+
if (plan.generator.backend.fallback === 'disable')
|
|
795
|
+
return;
|
|
796
|
+
let replacement = null;
|
|
797
|
+
try {
|
|
798
|
+
replacement = createBabylonShadowMapExperimentSingleDepthBackend(lightState.light, plan, fallbackReason);
|
|
799
|
+
prepareBackendCasterMembership(replacement);
|
|
800
|
+
depthBackend = replacement;
|
|
801
|
+
generator = replacement.generator;
|
|
802
|
+
pendingFallbackReason = null;
|
|
803
|
+
}
|
|
804
|
+
catch (error) {
|
|
805
|
+
rememberBackendAllocationCleanup(error);
|
|
806
|
+
if (replacement) {
|
|
807
|
+
const ownedReplacement = replacement;
|
|
808
|
+
try {
|
|
809
|
+
ownedReplacement.dispose();
|
|
810
|
+
}
|
|
811
|
+
catch (cleanupError) {
|
|
812
|
+
pendingBackendAllocationCleanup = () => {
|
|
813
|
+
try {
|
|
814
|
+
ownedReplacement.dispose();
|
|
815
|
+
return Object.freeze([]);
|
|
816
|
+
}
|
|
817
|
+
catch (retryError) {
|
|
818
|
+
return Object.freeze([retryError]);
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
if (error && typeof error === 'object') {
|
|
822
|
+
attachFailureMetadata(error, {
|
|
823
|
+
fallbackPreparationCleanupErrors: Object.freeze([cleanupError]),
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
throw error;
|
|
829
|
+
}
|
|
830
|
+
warnForFallback(fallbackReason);
|
|
831
|
+
markAllDepthDirty('generator-created');
|
|
832
|
+
};
|
|
833
|
+
const syncCaster = (renderableId, reuseCachedBounds = false) => {
|
|
834
|
+
const record = registry?.getRecord(renderableId);
|
|
835
|
+
const meshes = meshesByRenderable.get(renderableId);
|
|
836
|
+
if (!depthBackend || !record || !meshes)
|
|
837
|
+
return () => { };
|
|
838
|
+
const layer = shadowMapExperimentDepthLayerForUpdateClass(record.updateClass);
|
|
839
|
+
const shouldCast = record.state === 'active';
|
|
840
|
+
const changedLayers = new Set();
|
|
841
|
+
const previousMembership = [...meshes].map(mesh => ({
|
|
842
|
+
mesh,
|
|
843
|
+
candidateLayer: candidateLayerByMesh.get(mesh) ?? null,
|
|
844
|
+
staticCaster: depthBackend.hasCaster(mesh, 'static'),
|
|
845
|
+
dynamicCaster: depthBackend.hasCaster(mesh, 'dynamic'),
|
|
846
|
+
conservative: conservativeCasterMeshes.has(mesh),
|
|
847
|
+
}));
|
|
848
|
+
if (options.coverageMode === 'scene-fit' && !syncingSceneFitMembership) {
|
|
849
|
+
sceneFitCoverageInvalidated = true;
|
|
850
|
+
}
|
|
851
|
+
const rollbackMembership = (primaryError) => {
|
|
852
|
+
const rollbackErrors = [];
|
|
853
|
+
for (const previous of [...previousMembership].reverse()) {
|
|
854
|
+
try {
|
|
855
|
+
if (previous.staticCaster)
|
|
856
|
+
depthBackend.addCaster(previous.mesh, 'static');
|
|
857
|
+
else if (previous.dynamicCaster)
|
|
858
|
+
depthBackend.addCaster(previous.mesh, 'dynamic');
|
|
859
|
+
else
|
|
860
|
+
depthBackend.removeCaster(previous.mesh);
|
|
861
|
+
}
|
|
862
|
+
catch (rollbackError) {
|
|
863
|
+
rollbackErrors.push(rollbackError);
|
|
864
|
+
}
|
|
865
|
+
setCandidateLayer(previous.mesh, previous.candidateLayer);
|
|
866
|
+
if (previous.conservative)
|
|
867
|
+
conservativeCasterMeshes.add(previous.mesh);
|
|
868
|
+
else
|
|
869
|
+
conservativeCasterMeshes.delete(previous.mesh);
|
|
870
|
+
}
|
|
871
|
+
syncCandidateCounts();
|
|
872
|
+
if (rollbackErrors.length > 0 && primaryError && typeof primaryError === 'object') {
|
|
873
|
+
attachFailureMetadata(primaryError, {
|
|
874
|
+
casterMembershipRollbackErrors: Object.freeze(rollbackErrors),
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
try {
|
|
879
|
+
for (const mesh of meshes) {
|
|
880
|
+
const previousLayer = candidateLayerByMesh.get(mesh);
|
|
881
|
+
if (shouldCast && isMeshRuntimeVisible(mesh)) {
|
|
882
|
+
const retained = shouldRetainCaster(mesh, record.updateClass, reuseCachedBounds);
|
|
883
|
+
if (retained) {
|
|
884
|
+
const wasRetained = depthBackend.hasCaster(mesh, layer);
|
|
885
|
+
depthBackend.addCaster(mesh, layer);
|
|
886
|
+
setCandidateLayer(mesh, layer);
|
|
887
|
+
if (!wasRetained || previousLayer !== layer)
|
|
888
|
+
changedLayers.add(layer);
|
|
889
|
+
if (previousLayer && previousLayer !== layer)
|
|
890
|
+
changedLayers.add(previousLayer);
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
const wasRetained = depthBackend.hasCaster(mesh);
|
|
894
|
+
depthBackend.removeCaster(mesh);
|
|
895
|
+
setCandidateLayer(mesh, layer);
|
|
896
|
+
if (wasRetained)
|
|
897
|
+
changedLayers.add(previousLayer ?? layer);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
else {
|
|
901
|
+
const wasRetained = depthBackend.hasCaster(mesh);
|
|
902
|
+
depthBackend.removeCaster(mesh);
|
|
903
|
+
setCandidateLayer(mesh, null);
|
|
904
|
+
conservativeCasterMeshes.delete(mesh);
|
|
905
|
+
if (wasRetained)
|
|
906
|
+
changedLayers.add(previousLayer ?? layer);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
catch (error) {
|
|
911
|
+
rollbackMembership(error);
|
|
912
|
+
throw error;
|
|
913
|
+
}
|
|
914
|
+
syncCandidateCounts();
|
|
915
|
+
for (const changedLayer of changedLayers) {
|
|
916
|
+
markDepthDirty(changedLayer, 'caster-influence-membership-changed');
|
|
917
|
+
}
|
|
918
|
+
syncActivityEligibility(record.entityId);
|
|
919
|
+
return rollbackMembership;
|
|
920
|
+
};
|
|
921
|
+
const restorePendingReceiver = (target) => {
|
|
922
|
+
const desired = pendingReceiverRestorations.get(target);
|
|
923
|
+
if (desired === undefined)
|
|
924
|
+
return;
|
|
925
|
+
if (flushingRefresh && receiverRestorationAttemptsThisFlush.has(target)) {
|
|
926
|
+
throw new Error('shadowMapExperiment.receiverRestorationPending');
|
|
927
|
+
}
|
|
928
|
+
if (!isDisposed(target) && target.receiveShadows !== desired)
|
|
929
|
+
target.receiveShadows = desired;
|
|
930
|
+
pendingReceiverRestorations.delete(target);
|
|
931
|
+
};
|
|
932
|
+
const flushPendingReceiverRestorations = () => {
|
|
933
|
+
for (const target of [...pendingReceiverRestorations.keys()]) {
|
|
934
|
+
receiverRestorationAttemptsThisFlush.add(target);
|
|
935
|
+
try {
|
|
936
|
+
const desired = pendingReceiverRestorations.get(target);
|
|
937
|
+
if (desired !== undefined && !isDisposed(target) && target.receiveShadows !== desired) {
|
|
938
|
+
target.receiveShadows = desired;
|
|
939
|
+
}
|
|
940
|
+
pendingReceiverRestorations.delete(target);
|
|
941
|
+
}
|
|
942
|
+
catch {
|
|
943
|
+
// The target remains the owner and the post-flush predicate schedules
|
|
944
|
+
// exactly one further frame; no unrelated receiver is scanned.
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
const writeReceiver = (target, desired) => {
|
|
949
|
+
try {
|
|
950
|
+
if (!isDisposed(target) && target.receiveShadows !== desired)
|
|
951
|
+
target.receiveShadows = desired;
|
|
952
|
+
pendingReceiverRestorations.delete(target);
|
|
953
|
+
}
|
|
954
|
+
catch (error) {
|
|
955
|
+
pendingReceiverRestorations.set(target, desired);
|
|
956
|
+
throw error;
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
const restoreReceiver = (mesh) => {
|
|
960
|
+
const target = receiverTargetByMesh.get(mesh) ?? resolveReceiverTarget(mesh);
|
|
961
|
+
receiverTargetByMesh.delete(mesh);
|
|
962
|
+
const state = receiverStates.get(target);
|
|
963
|
+
if (!state)
|
|
964
|
+
return;
|
|
965
|
+
if (state.requests.get(mesh) === true)
|
|
966
|
+
state.enabledRequestCount -= 1;
|
|
967
|
+
state.requests.delete(mesh);
|
|
968
|
+
if (state.requests.size > 0) {
|
|
969
|
+
const next = state.enabledRequestCount > 0;
|
|
970
|
+
state.ownedValue = next;
|
|
971
|
+
writeReceiver(target, next);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
if (!isDisposed(target) && target.receiveShadows === state.ownedValue) {
|
|
975
|
+
writeReceiver(target, state.previousReceiveShadows);
|
|
976
|
+
}
|
|
977
|
+
receiverStates.delete(target);
|
|
978
|
+
};
|
|
979
|
+
const setReceiver = (mesh, enabled, renderableId) => {
|
|
980
|
+
const eligible = !enabled || isReceiverEligible(mesh);
|
|
981
|
+
const next = enabled && eligible;
|
|
982
|
+
const previousTarget = receiverTargetByMesh.get(mesh);
|
|
983
|
+
const target = resolveReceiverTarget(mesh);
|
|
984
|
+
// A failed journal rollback must be restored before this target is read as
|
|
985
|
+
// a new baseline. Otherwise a later retry would capture our own stale
|
|
986
|
+
// value as caller state.
|
|
987
|
+
restorePendingReceiver(target);
|
|
988
|
+
if (previousTarget && previousTarget !== target)
|
|
989
|
+
restoreReceiver(mesh);
|
|
990
|
+
receiverTargetByMesh.set(mesh, target);
|
|
991
|
+
let state = receiverStates.get(target);
|
|
992
|
+
if (!state) {
|
|
993
|
+
state = {
|
|
994
|
+
mesh: target,
|
|
995
|
+
previousReceiveShadows: target.receiveShadows,
|
|
996
|
+
requests: new Map(),
|
|
997
|
+
enabledRequestCount: 0,
|
|
998
|
+
ownedValue: next,
|
|
999
|
+
};
|
|
1000
|
+
receiverStates.set(target, state);
|
|
1001
|
+
}
|
|
1002
|
+
const previousRequest = state.requests.get(mesh);
|
|
1003
|
+
if (previousRequest !== next) {
|
|
1004
|
+
if (previousRequest === true)
|
|
1005
|
+
state.enabledRequestCount -= 1;
|
|
1006
|
+
if (next)
|
|
1007
|
+
state.enabledRequestCount += 1;
|
|
1008
|
+
}
|
|
1009
|
+
state.requests.set(mesh, next);
|
|
1010
|
+
const ownedValue = state.enabledRequestCount > 0;
|
|
1011
|
+
state.ownedValue = ownedValue;
|
|
1012
|
+
writeReceiver(target, ownedValue);
|
|
1013
|
+
if (enabled && !eligible) {
|
|
1014
|
+
const code = 'shadowMapExperiment.transparentReceiver';
|
|
1015
|
+
const key = `${code}:${renderableId}`;
|
|
1016
|
+
if (!diagnosticKeys.has(key)) {
|
|
1017
|
+
diagnosticKeys.add(key);
|
|
1018
|
+
diagnostics.push({
|
|
1019
|
+
code,
|
|
1020
|
+
message: 'Receiver was excluded because its current material is not shadow-receiver eligible.',
|
|
1021
|
+
renderableId,
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
const captureReceiverJournal = (meshes) => {
|
|
1027
|
+
const touchedMeshes = [...meshes];
|
|
1028
|
+
const touchedMeshesByTarget = new Map();
|
|
1029
|
+
const trackTarget = (target, mesh) => {
|
|
1030
|
+
let targetMeshes = touchedMeshesByTarget.get(target);
|
|
1031
|
+
if (!targetMeshes) {
|
|
1032
|
+
targetMeshes = new Set();
|
|
1033
|
+
touchedMeshesByTarget.set(target, targetMeshes);
|
|
1034
|
+
}
|
|
1035
|
+
targetMeshes.add(mesh);
|
|
1036
|
+
};
|
|
1037
|
+
for (const mesh of touchedMeshes) {
|
|
1038
|
+
trackTarget(resolveReceiverTarget(mesh), mesh);
|
|
1039
|
+
const previousTarget = receiverTargetByMesh.get(mesh);
|
|
1040
|
+
if (previousTarget)
|
|
1041
|
+
trackTarget(previousTarget, mesh);
|
|
1042
|
+
}
|
|
1043
|
+
for (const target of touchedMeshesByTarget.keys())
|
|
1044
|
+
restorePendingReceiver(target);
|
|
1045
|
+
const states = [...touchedMeshesByTarget].map(([target, targetMeshes]) => {
|
|
1046
|
+
const state = receiverStates.get(target);
|
|
1047
|
+
return {
|
|
1048
|
+
target,
|
|
1049
|
+
state: state ?? null,
|
|
1050
|
+
enabledRequestCount: state?.enabledRequestCount ?? 0,
|
|
1051
|
+
ownedValue: state?.ownedValue ?? false,
|
|
1052
|
+
requests: [...targetMeshes].map(mesh => [mesh, state?.requests.get(mesh)]),
|
|
1053
|
+
};
|
|
1054
|
+
});
|
|
1055
|
+
return {
|
|
1056
|
+
states,
|
|
1057
|
+
targetsByMesh: touchedMeshes.map(mesh => [mesh, receiverTargetByMesh.get(mesh) ?? null]),
|
|
1058
|
+
targetValues: [...touchedMeshesByTarget.keys()].map(target => [target, target.receiveShadows]),
|
|
1059
|
+
};
|
|
1060
|
+
};
|
|
1061
|
+
const rollbackReceiverJournal = (journal, primaryError) => {
|
|
1062
|
+
const rollbackErrors = [];
|
|
1063
|
+
for (const entry of journal.states) {
|
|
1064
|
+
if (!entry.state) {
|
|
1065
|
+
receiverStates.delete(entry.target);
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
for (const [mesh, request] of entry.requests) {
|
|
1069
|
+
if (request === undefined)
|
|
1070
|
+
entry.state.requests.delete(mesh);
|
|
1071
|
+
else
|
|
1072
|
+
entry.state.requests.set(mesh, request);
|
|
1073
|
+
}
|
|
1074
|
+
entry.state.enabledRequestCount = entry.enabledRequestCount;
|
|
1075
|
+
entry.state.ownedValue = entry.ownedValue;
|
|
1076
|
+
receiverStates.set(entry.target, entry.state);
|
|
1077
|
+
}
|
|
1078
|
+
for (const [mesh, target] of journal.targetsByMesh) {
|
|
1079
|
+
receiverTargetByMesh.delete(mesh);
|
|
1080
|
+
if (target)
|
|
1081
|
+
receiverTargetByMesh.set(mesh, target);
|
|
1082
|
+
}
|
|
1083
|
+
for (const [target, receiveShadows] of journal.targetValues) {
|
|
1084
|
+
try {
|
|
1085
|
+
writeReceiver(target, receiveShadows);
|
|
1086
|
+
}
|
|
1087
|
+
catch (error) {
|
|
1088
|
+
rollbackErrors.push(error);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
if (rollbackErrors.length > 0 && primaryError && typeof primaryError === 'object') {
|
|
1092
|
+
attachFailureMetadata(primaryError, { receiverRollbackErrors: Object.freeze(rollbackErrors) });
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
const syncRenderable = (renderableId) => {
|
|
1096
|
+
const meshes = meshesByRenderable.get(renderableId);
|
|
1097
|
+
const object = objectByRenderable.get(renderableId);
|
|
1098
|
+
if (!meshes || !object || !registry)
|
|
1099
|
+
return;
|
|
1100
|
+
const visible = [...meshes].some(isMeshRuntimeVisible);
|
|
1101
|
+
const previousRecord = registry.getRecord(renderableId);
|
|
1102
|
+
const receiverJournal = captureReceiverJournal(meshes);
|
|
1103
|
+
let rollbackCasterMembership = null;
|
|
1104
|
+
const nextRecord = previousRecord?.renderableReady
|
|
1105
|
+
? registry.setVisible(renderableId, visible)
|
|
1106
|
+
: registry.attach(renderableId, visible);
|
|
1107
|
+
if (previousRecord?.state === 'active'
|
|
1108
|
+
&& nextRecord?.state !== 'active'
|
|
1109
|
+
&& registry.snapshot().activeCasterCount === 0) {
|
|
1110
|
+
registry.markDirty('empty-list-clear');
|
|
1111
|
+
markDepthDirty(shadowMapExperimentDepthLayerForUpdateClass(object.updateClass), 'empty-list-clear');
|
|
1112
|
+
}
|
|
1113
|
+
try {
|
|
1114
|
+
// Receiver preparation is fallible in host integrations and is journaled
|
|
1115
|
+
// together with registry/backend state. Visibility/membership changes
|
|
1116
|
+
// never publish automatic activity topology.
|
|
1117
|
+
for (const mesh of meshes)
|
|
1118
|
+
setReceiver(mesh, object.receive, renderableId);
|
|
1119
|
+
rollbackCasterMembership = syncCaster(renderableId);
|
|
1120
|
+
pendingRenderableMembershipRetries.delete(renderableId);
|
|
1121
|
+
}
|
|
1122
|
+
catch (error) {
|
|
1123
|
+
rollbackCasterMembership?.(error);
|
|
1124
|
+
rollbackReceiverJournal(receiverJournal, error);
|
|
1125
|
+
if (previousRecord) {
|
|
1126
|
+
if (previousRecord.renderableReady)
|
|
1127
|
+
registry.attach(renderableId, previousRecord.runtimeVisible);
|
|
1128
|
+
else
|
|
1129
|
+
registry.detach(renderableId);
|
|
1130
|
+
registry.setVisible(renderableId, previousRecord.runtimeVisible);
|
|
1131
|
+
}
|
|
1132
|
+
syncActivityEligibility(object.entityId);
|
|
1133
|
+
pendingRenderableMembershipRetries.set(renderableId, 'renderable');
|
|
1134
|
+
requestRefreshFrame('shadow-map-experiment-membership-retry');
|
|
1135
|
+
throw error;
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
const flushPendingRenderableMembershipRetries = () => {
|
|
1139
|
+
// Membership retries own only local receiver/caster state. Candidate
|
|
1140
|
+
// topology is published by Mesh registration, unregistration, or an
|
|
1141
|
+
// explicit Plan/runtime-object delta.
|
|
1142
|
+
for (const [renderableId, retryKind] of [...pendingRenderableMembershipRetries]) {
|
|
1143
|
+
if (!objectByRenderable.has(renderableId) || !meshesByRenderable.has(renderableId)) {
|
|
1144
|
+
pendingRenderableMembershipRetries.delete(renderableId);
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
if (retryKind === 'renderable') {
|
|
1148
|
+
try {
|
|
1149
|
+
syncRenderable(renderableId);
|
|
1150
|
+
}
|
|
1151
|
+
catch (error) {
|
|
1152
|
+
if (error instanceof Error
|
|
1153
|
+
&& error.message === 'shadowMapExperiment.automaticActivityCandidateCommitFailed') {
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
throw error;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
else {
|
|
1160
|
+
const pendingVisibility = pendingExplicitVisibility.get(renderableId);
|
|
1161
|
+
const previousRecord = registry?.getRecord(renderableId) ?? null;
|
|
1162
|
+
let rollbackCasterMembership = null;
|
|
1163
|
+
if (pendingVisibility !== undefined)
|
|
1164
|
+
registry?.setVisible(renderableId, pendingVisibility);
|
|
1165
|
+
try {
|
|
1166
|
+
rollbackCasterMembership = syncCaster(renderableId);
|
|
1167
|
+
}
|
|
1168
|
+
catch (error) {
|
|
1169
|
+
rollbackCasterMembership?.(error);
|
|
1170
|
+
if (previousRecord && registry) {
|
|
1171
|
+
if (previousRecord.renderableReady)
|
|
1172
|
+
registry.attach(renderableId, previousRecord.runtimeVisible);
|
|
1173
|
+
else
|
|
1174
|
+
registry.detach(renderableId);
|
|
1175
|
+
registry.setVisible(renderableId, previousRecord.runtimeVisible);
|
|
1176
|
+
}
|
|
1177
|
+
requestRefreshFrame('shadow-map-experiment-membership-retry');
|
|
1178
|
+
if (error instanceof Error
|
|
1179
|
+
&& error.message === 'shadowMapExperiment.automaticActivityCandidateCommitFailed') {
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
throw error;
|
|
1183
|
+
}
|
|
1184
|
+
pendingExplicitVisibility.delete(renderableId);
|
|
1185
|
+
pendingRenderableMembershipRetries.delete(renderableId);
|
|
1186
|
+
}
|
|
1187
|
+
pendingRenderableMembershipRetries.delete(renderableId);
|
|
1188
|
+
enqueueRefresh('caster-visibility-changed');
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
const syncMatrixObserver = (binding) => {
|
|
1192
|
+
const object = objectByRenderable.get(binding.renderableId);
|
|
1193
|
+
if (!object?.cast) {
|
|
1194
|
+
if (binding.matrixObserver)
|
|
1195
|
+
binding.mesh.onAfterWorldMatrixUpdateObservable.remove(binding.matrixObserver);
|
|
1196
|
+
binding.matrixObserver = null;
|
|
1197
|
+
binding.worldMatrixSnapshot = null;
|
|
1198
|
+
pendingTransformMembershipRetries.delete(binding.mesh);
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
if (binding.matrixObserver)
|
|
1202
|
+
return;
|
|
1203
|
+
binding.worldMatrixSnapshot = readWorldMatrixSnapshot(binding.mesh);
|
|
1204
|
+
binding.matrixObserver = binding.mesh.onAfterWorldMatrixUpdateObservable.add(() => {
|
|
1205
|
+
if (disposed || disposing)
|
|
1206
|
+
return;
|
|
1207
|
+
const nextSnapshot = readWorldMatrixSnapshot(binding.mesh);
|
|
1208
|
+
if (areMatrixSnapshotsEqual(binding.worldMatrixSnapshot, nextSnapshot))
|
|
1209
|
+
return;
|
|
1210
|
+
const currentObject = objectByRenderable.get(binding.renderableId);
|
|
1211
|
+
if (!currentObject?.cast) {
|
|
1212
|
+
binding.worldMatrixSnapshot = nextSnapshot;
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
try {
|
|
1216
|
+
syncCaster(binding.renderableId);
|
|
1217
|
+
}
|
|
1218
|
+
catch (error) {
|
|
1219
|
+
pendingTransformMembershipRetries.add(binding.mesh);
|
|
1220
|
+
requestRefreshFrame('shadow-map-experiment-membership-retry');
|
|
1221
|
+
throw error;
|
|
1222
|
+
}
|
|
1223
|
+
binding.worldMatrixSnapshot = nextSnapshot;
|
|
1224
|
+
enqueueRefresh(currentObject.updateClass === 'static' ? 'static-transform-changed' : 'dynamic-transform-changed');
|
|
1225
|
+
});
|
|
1226
|
+
};
|
|
1227
|
+
const flushPendingTransformMembershipRetries = () => {
|
|
1228
|
+
for (const mesh of [...pendingTransformMembershipRetries]) {
|
|
1229
|
+
const binding = bindings.get(mesh);
|
|
1230
|
+
const currentObject = binding ? objectByRenderable.get(binding.renderableId) : null;
|
|
1231
|
+
if (!binding || !currentObject?.cast) {
|
|
1232
|
+
pendingTransformMembershipRetries.delete(mesh);
|
|
1233
|
+
if (binding)
|
|
1234
|
+
binding.worldMatrixSnapshot = readWorldMatrixSnapshot(mesh);
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
syncCaster(binding.renderableId);
|
|
1238
|
+
binding.worldMatrixSnapshot = readWorldMatrixSnapshot(mesh);
|
|
1239
|
+
pendingTransformMembershipRetries.delete(mesh);
|
|
1240
|
+
enqueueRefresh(currentObject.updateClass === 'static' ? 'static-transform-changed' : 'dynamic-transform-changed');
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
const flushPendingMeshObserverRestoration = (mesh) => {
|
|
1244
|
+
const pending = pendingMeshObserverRestorations.get(mesh);
|
|
1245
|
+
if (!pending)
|
|
1246
|
+
return Object.freeze([]);
|
|
1247
|
+
const restorationErrors = [];
|
|
1248
|
+
const mutable = pending.binding;
|
|
1249
|
+
if (pending.enabledObserver) {
|
|
1250
|
+
try {
|
|
1251
|
+
const template = pending.enabledObserver;
|
|
1252
|
+
mutable.enabledObserver = mesh.onEnabledStateChangedObservable.add(template.callback, template.mask, false, template.scope);
|
|
1253
|
+
pending.enabledObserver = null;
|
|
1254
|
+
}
|
|
1255
|
+
catch (error) {
|
|
1256
|
+
restorationErrors.push(error);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
for (const restoration of [...pending.ancestorEnabledObservers]) {
|
|
1260
|
+
try {
|
|
1261
|
+
const { binding, template } = restoration;
|
|
1262
|
+
binding.observer = binding.node.onEnabledStateChangedObservable.add(template.callback, template.mask, false, template.scope);
|
|
1263
|
+
pending.ancestorEnabledObservers.splice(pending.ancestorEnabledObservers.indexOf(restoration), 1);
|
|
1264
|
+
}
|
|
1265
|
+
catch (error) {
|
|
1266
|
+
restorationErrors.push(error);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
if (pending.disposeObserver) {
|
|
1270
|
+
try {
|
|
1271
|
+
const template = pending.disposeObserver;
|
|
1272
|
+
mutable.disposeObserver = mesh.onDisposeObservable.add(template.callback, template.mask, false, template.scope);
|
|
1273
|
+
pending.disposeObserver = null;
|
|
1274
|
+
}
|
|
1275
|
+
catch (error) {
|
|
1276
|
+
restorationErrors.push(error);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
if (pending.matrixObserver) {
|
|
1280
|
+
try {
|
|
1281
|
+
const template = pending.matrixObserver;
|
|
1282
|
+
mutable.matrixObserver = mesh.onAfterWorldMatrixUpdateObservable.add(template.callback, template.mask, false, template.scope);
|
|
1283
|
+
pending.matrixObserver = null;
|
|
1284
|
+
}
|
|
1285
|
+
catch (error) {
|
|
1286
|
+
restorationErrors.push(error);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
if (!pending.enabledObserver
|
|
1290
|
+
&& pending.ancestorEnabledObservers.length === 0
|
|
1291
|
+
&& !pending.disposeObserver
|
|
1292
|
+
&& !pending.matrixObserver) {
|
|
1293
|
+
pendingMeshObserverRestorations.delete(mesh);
|
|
1294
|
+
}
|
|
1295
|
+
return Object.freeze(restorationErrors);
|
|
1296
|
+
};
|
|
1297
|
+
const unregisterMesh = (mesh) => {
|
|
1298
|
+
const binding = bindings.get(mesh);
|
|
1299
|
+
if (!binding)
|
|
1300
|
+
return false;
|
|
1301
|
+
const pendingObserverRestorationErrors = flushPendingMeshObserverRestoration(mesh);
|
|
1302
|
+
if (pendingObserverRestorationErrors.length > 0) {
|
|
1303
|
+
pendingMeshUnregistrations.add(mesh);
|
|
1304
|
+
requestRefreshFrame('shadow-map-experiment-mesh-observer-restoration-retry');
|
|
1305
|
+
throw Object.assign(new Error('shadowMapExperiment.meshObserverRestorationFailed'), {
|
|
1306
|
+
cleanupErrors: pendingObserverRestorationErrors,
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
const previousLayer = candidateLayerByMesh.get(mesh);
|
|
1310
|
+
const wasStaticCaster = depthBackend?.hasCaster(mesh, 'static') === true;
|
|
1311
|
+
const wasDynamicCaster = depthBackend?.hasCaster(mesh, 'dynamic') === true;
|
|
1312
|
+
const wasCaster = wasStaticCaster || wasDynamicCaster;
|
|
1313
|
+
const wasConservative = conservativeCasterMeshes.has(mesh);
|
|
1314
|
+
const previousRecord = registry?.getRecord(binding.renderableId) ?? null;
|
|
1315
|
+
const wasActiveRecord = previousRecord?.state === 'active';
|
|
1316
|
+
const receiverJournal = captureReceiverJournal([mesh]);
|
|
1317
|
+
const meshes = meshesByRenderable.get(binding.renderableId);
|
|
1318
|
+
const remainingMeshes = [...(meshes ?? [])].filter(candidate => candidate !== mesh);
|
|
1319
|
+
let enabledObserverRemoved = false;
|
|
1320
|
+
const ancestorEnabledObserversRemoved = [];
|
|
1321
|
+
let disposeObserverRemoved = false;
|
|
1322
|
+
let matrixObserverRemoved = false;
|
|
1323
|
+
const enabledObserverTemplate = binding.enabledObserver;
|
|
1324
|
+
const disposeObserverTemplate = binding.disposeObserver;
|
|
1325
|
+
const matrixObserverTemplate = binding.matrixObserver;
|
|
1326
|
+
let automaticActivityCandidatesChanged = false;
|
|
1327
|
+
const restoreRegistry = () => {
|
|
1328
|
+
if (!registry || !previousRecord)
|
|
1329
|
+
return;
|
|
1330
|
+
if (previousRecord.renderableReady)
|
|
1331
|
+
registry.attach(binding.renderableId, previousRecord.runtimeVisible);
|
|
1332
|
+
else
|
|
1333
|
+
registry.detach(binding.renderableId);
|
|
1334
|
+
registry.setVisible(binding.renderableId, previousRecord.runtimeVisible);
|
|
1335
|
+
};
|
|
1336
|
+
try {
|
|
1337
|
+
depthBackend?.removeCaster(mesh);
|
|
1338
|
+
restoreReceiver(mesh);
|
|
1339
|
+
if (remainingMeshes.length === 0)
|
|
1340
|
+
registry?.detach(binding.renderableId);
|
|
1341
|
+
else
|
|
1342
|
+
registry?.setVisible(binding.renderableId, remainingMeshes.some(isMeshRuntimeVisible));
|
|
1343
|
+
enabledObserverRemoved = mesh.onEnabledStateChangedObservable.remove(binding.enabledObserver);
|
|
1344
|
+
for (const ancestorBinding of binding.ancestorEnabledObservers) {
|
|
1345
|
+
const template = ancestorBinding.observer;
|
|
1346
|
+
if (ancestorBinding.node.onEnabledStateChangedObservable.remove(template)) {
|
|
1347
|
+
ancestorEnabledObserversRemoved.push({ binding: ancestorBinding, template });
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
disposeObserverRemoved = mesh.onDisposeObservable.remove(binding.disposeObserver);
|
|
1351
|
+
if (binding.matrixObserver) {
|
|
1352
|
+
matrixObserverRemoved = mesh.onAfterWorldMatrixUpdateObservable.remove(binding.matrixObserver);
|
|
1353
|
+
}
|
|
1354
|
+
if (!stagingPlanCandidateTransitions) {
|
|
1355
|
+
// A registration can fail while its new binding exists only in the
|
|
1356
|
+
// unpublished proposal. Rollback must remove it from that proposed
|
|
1357
|
+
// view, not just from the last committed candidate map, otherwise the
|
|
1358
|
+
// next frame could publish a candidate whose mesh binding was deleted.
|
|
1359
|
+
const ownsPendingCandidateTransition = pendingAutomaticActivityCandidateTransition !== null;
|
|
1360
|
+
const nextCandidates = new Map(pendingAutomaticActivityCandidateTransition
|
|
1361
|
+
?? automaticActivityCandidateBindings);
|
|
1362
|
+
automaticActivityCandidatesChanged = updateAutomaticActivityCandidateBinding(nextCandidates, binding, null);
|
|
1363
|
+
if ((automaticActivityCandidatesChanged || ownsPendingCandidateTransition)
|
|
1364
|
+
&& !applyAutomaticActivityCandidateTransition(nextCandidates)) {
|
|
1365
|
+
throw new Error('shadowMapExperiment.automaticActivityCandidateCommitFailed');
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
catch (error) {
|
|
1370
|
+
const rollbackErrors = [];
|
|
1371
|
+
try {
|
|
1372
|
+
if (wasStaticCaster)
|
|
1373
|
+
depthBackend?.addCaster(mesh, 'static');
|
|
1374
|
+
else if (wasDynamicCaster)
|
|
1375
|
+
depthBackend?.addCaster(mesh, 'dynamic');
|
|
1376
|
+
else
|
|
1377
|
+
depthBackend?.removeCaster(mesh);
|
|
1378
|
+
}
|
|
1379
|
+
catch (rollbackError) {
|
|
1380
|
+
rollbackErrors.push(rollbackError);
|
|
1381
|
+
}
|
|
1382
|
+
try {
|
|
1383
|
+
rollbackReceiverJournal(receiverJournal, error);
|
|
1384
|
+
}
|
|
1385
|
+
catch (rollbackError) {
|
|
1386
|
+
rollbackErrors.push(rollbackError);
|
|
1387
|
+
}
|
|
1388
|
+
try {
|
|
1389
|
+
restoreRegistry();
|
|
1390
|
+
}
|
|
1391
|
+
catch (rollbackError) {
|
|
1392
|
+
rollbackErrors.push(rollbackError);
|
|
1393
|
+
}
|
|
1394
|
+
if (enabledObserverRemoved
|
|
1395
|
+
|| ancestorEnabledObserversRemoved.length > 0
|
|
1396
|
+
|| disposeObserverRemoved
|
|
1397
|
+
|| matrixObserverRemoved) {
|
|
1398
|
+
pendingMeshObserverRestorations.set(mesh, {
|
|
1399
|
+
mesh,
|
|
1400
|
+
binding,
|
|
1401
|
+
enabledObserver: enabledObserverRemoved ? enabledObserverTemplate : null,
|
|
1402
|
+
ancestorEnabledObservers: ancestorEnabledObserversRemoved,
|
|
1403
|
+
disposeObserver: disposeObserverRemoved ? disposeObserverTemplate : null,
|
|
1404
|
+
matrixObserver: matrixObserverRemoved ? matrixObserverTemplate : null,
|
|
1405
|
+
});
|
|
1406
|
+
rollbackErrors.push(...flushPendingMeshObserverRestoration(mesh));
|
|
1407
|
+
}
|
|
1408
|
+
if (wasConservative)
|
|
1409
|
+
conservativeCasterMeshes.add(mesh);
|
|
1410
|
+
else
|
|
1411
|
+
conservativeCasterMeshes.delete(mesh);
|
|
1412
|
+
if (rollbackErrors.length > 0 && error && typeof error === 'object') {
|
|
1413
|
+
attachFailureMetadata(error, { unregisterRollbackErrors: Object.freeze(rollbackErrors) });
|
|
1414
|
+
}
|
|
1415
|
+
// The binding/candidate map deliberately stays intact. The queued retry
|
|
1416
|
+
// is therefore the sole owner until every teardown step commits.
|
|
1417
|
+
pendingMeshUnregistrations.add(mesh);
|
|
1418
|
+
requestRefreshFrame(pendingMeshObserverRestorations.has(mesh)
|
|
1419
|
+
? 'shadow-map-experiment-mesh-observer-restoration-retry'
|
|
1420
|
+
: 'shadow-map-experiment-membership-retry');
|
|
1421
|
+
throw error;
|
|
1422
|
+
}
|
|
1423
|
+
if (remainingMeshes.length === 0) {
|
|
1424
|
+
meshesByRenderable.delete(binding.renderableId);
|
|
1425
|
+
pendingRenderableMembershipRetries.delete(binding.renderableId);
|
|
1426
|
+
pendingExplicitVisibility.delete(binding.renderableId);
|
|
1427
|
+
}
|
|
1428
|
+
else {
|
|
1429
|
+
meshes?.delete(mesh);
|
|
1430
|
+
}
|
|
1431
|
+
setCandidateLayer(mesh, null);
|
|
1432
|
+
conservativeCasterMeshes.delete(mesh);
|
|
1433
|
+
meshDataCacheByMesh.delete(mesh);
|
|
1434
|
+
syncCandidateCounts();
|
|
1435
|
+
syncActivityEligibility(binding.entityId);
|
|
1436
|
+
if (wasCaster || wasActiveRecord) {
|
|
1437
|
+
if (registry?.snapshot().activeCasterCount === 0)
|
|
1438
|
+
registry.markDirty('empty-list-clear');
|
|
1439
|
+
if (previousLayer)
|
|
1440
|
+
markDepthDirty(previousLayer, 'caster-removed');
|
|
1441
|
+
else
|
|
1442
|
+
enqueueRefresh('caster-removed');
|
|
1443
|
+
if (candidateLayerByMesh.size === 0 && previousLayer) {
|
|
1444
|
+
markDepthDirty(previousLayer, 'empty-list-clear');
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
bindings.delete(mesh);
|
|
1448
|
+
pendingMeshObserverRestorations.delete(mesh);
|
|
1449
|
+
pendingMeshRegistrationRollbacks.delete(mesh);
|
|
1450
|
+
pendingMeshUnregistrations.delete(mesh);
|
|
1451
|
+
pendingTransformMembershipRetries.delete(mesh);
|
|
1452
|
+
// Commit the candidate removal only after every fallible renderer,
|
|
1453
|
+
// observer and receiver teardown above has completed. Retrying a failed
|
|
1454
|
+
// unregister therefore continues to expose the old valid topology.
|
|
1455
|
+
return automaticActivityCandidatesChanged;
|
|
1456
|
+
};
|
|
1457
|
+
const requestMeshUnregistration = (mesh) => {
|
|
1458
|
+
pendingSceneMeshRegistrations.delete(mesh);
|
|
1459
|
+
try {
|
|
1460
|
+
unregisterMesh(mesh);
|
|
1461
|
+
pendingMeshUnregistrations.delete(mesh);
|
|
1462
|
+
}
|
|
1463
|
+
catch (error) {
|
|
1464
|
+
pendingMeshUnregistrations.add(mesh);
|
|
1465
|
+
requestRefreshFrame('shadow-map-experiment-membership-retry');
|
|
1466
|
+
throw error;
|
|
1467
|
+
}
|
|
1468
|
+
};
|
|
1469
|
+
const flushPendingMeshUnregistrations = () => {
|
|
1470
|
+
for (const mesh of [...pendingMeshUnregistrations])
|
|
1471
|
+
requestMeshUnregistration(mesh);
|
|
1472
|
+
};
|
|
1473
|
+
const registerMesh = (mesh) => {
|
|
1474
|
+
const pendingObserverCleanupErrors = flushPendingMeshObserverCleanup(mesh);
|
|
1475
|
+
if (pendingObserverCleanupErrors.length > 0) {
|
|
1476
|
+
throw Object.assign(new Error('shadowMapExperiment.meshObserverCleanupFailed'), {
|
|
1477
|
+
cleanupErrors: pendingObserverCleanupErrors,
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
if (pendingMeshRegistrationRollbacks.has(mesh)) {
|
|
1481
|
+
try {
|
|
1482
|
+
unregisterMesh(mesh);
|
|
1483
|
+
}
|
|
1484
|
+
catch (error) {
|
|
1485
|
+
throw Object.assign(new Error('shadowMapExperiment.meshRegistrationRollbackFailed'), {
|
|
1486
|
+
cause: error,
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
pendingMeshRegistrationRollbacks.delete(mesh);
|
|
1490
|
+
}
|
|
1491
|
+
if (!(plan ?? activatingRecoveryPlan)?.enabled || !registry || bindings.has(mesh) || isDisposed(mesh))
|
|
1492
|
+
return false;
|
|
1493
|
+
const renderableId = resolveRenderableId(mesh, objectByRenderable);
|
|
1494
|
+
if (!renderableId)
|
|
1495
|
+
return false;
|
|
1496
|
+
const object = objectByRenderable.get(renderableId);
|
|
1497
|
+
if (!object || (!object.cast && !object.receive))
|
|
1498
|
+
return false;
|
|
1499
|
+
let enabledObserver = null;
|
|
1500
|
+
const ancestorEnabledObservers = [];
|
|
1501
|
+
let disposeObserver = null;
|
|
1502
|
+
let bindingCommitted = false;
|
|
1503
|
+
try {
|
|
1504
|
+
const syncEnabledState = () => {
|
|
1505
|
+
if (!disposed && !disposing) {
|
|
1506
|
+
try {
|
|
1507
|
+
syncRenderable(renderableId);
|
|
1508
|
+
}
|
|
1509
|
+
catch (error) {
|
|
1510
|
+
pendingRenderableMembershipRetries.set(renderableId, 'renderable');
|
|
1511
|
+
requestRefreshFrame('shadow-map-experiment-membership-retry');
|
|
1512
|
+
if (!(error instanceof Error)
|
|
1513
|
+
|| error.message !== 'shadowMapExperiment.automaticActivityCandidateCommitFailed') {
|
|
1514
|
+
throw error;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
};
|
|
1519
|
+
enabledObserver = mesh.onEnabledStateChangedObservable.add(syncEnabledState);
|
|
1520
|
+
let ancestor = mesh.parent;
|
|
1521
|
+
while (ancestor) {
|
|
1522
|
+
ancestorEnabledObservers.push({
|
|
1523
|
+
node: ancestor,
|
|
1524
|
+
observer: ancestor.onEnabledStateChangedObservable.add(syncEnabledState),
|
|
1525
|
+
});
|
|
1526
|
+
ancestor = ancestor.parent;
|
|
1527
|
+
}
|
|
1528
|
+
disposeObserver = mesh.onDisposeObservable.add(() => requestMeshUnregistration(mesh));
|
|
1529
|
+
const meshes = meshesByRenderable.get(renderableId) ?? new Set();
|
|
1530
|
+
meshes.add(mesh);
|
|
1531
|
+
meshesByRenderable.set(renderableId, meshes);
|
|
1532
|
+
const binding = {
|
|
1533
|
+
mesh,
|
|
1534
|
+
entityId: object.entityId,
|
|
1535
|
+
renderableId,
|
|
1536
|
+
sceneFitLocalBounds: readMeshLocalBounds(mesh),
|
|
1537
|
+
influenceWorldBounds: undefined,
|
|
1538
|
+
influenceBoundingInfo: undefined,
|
|
1539
|
+
enabledObserver,
|
|
1540
|
+
ancestorEnabledObservers,
|
|
1541
|
+
disposeObserver,
|
|
1542
|
+
matrixObserver: null,
|
|
1543
|
+
worldMatrixSnapshot: null,
|
|
1544
|
+
};
|
|
1545
|
+
bindings.set(mesh, binding);
|
|
1546
|
+
bindingCommitted = true;
|
|
1547
|
+
if (options.coverageMode === 'scene-fit')
|
|
1548
|
+
sceneFitCoverageInvalidated = true;
|
|
1549
|
+
syncMatrixObserver(binding);
|
|
1550
|
+
syncRenderable(renderableId);
|
|
1551
|
+
if (!stagingPlanCandidateTransitions) {
|
|
1552
|
+
syncAutomaticActivityCandidateBindingsForRenderable(renderableId);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
catch (error) {
|
|
1556
|
+
const rollbackErrors = [];
|
|
1557
|
+
if (bindingCommitted) {
|
|
1558
|
+
try {
|
|
1559
|
+
unregisterMesh(mesh);
|
|
1560
|
+
}
|
|
1561
|
+
catch (rollbackError) {
|
|
1562
|
+
pendingMeshRegistrationRollbacks.add(mesh);
|
|
1563
|
+
requestRefreshFrame('shadow-map-experiment-mesh-registration-rollback-retry');
|
|
1564
|
+
rollbackErrors.push(rollbackError);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
else {
|
|
1568
|
+
pendingMeshObserverCleanups.set(mesh, {
|
|
1569
|
+
mesh,
|
|
1570
|
+
enabledObserver,
|
|
1571
|
+
ancestorEnabledObservers,
|
|
1572
|
+
disposeObserver,
|
|
1573
|
+
});
|
|
1574
|
+
rollbackErrors.push(...flushPendingMeshObserverCleanup(mesh));
|
|
1575
|
+
if (pendingMeshObserverCleanups.has(mesh)) {
|
|
1576
|
+
requestRefreshFrame('shadow-map-experiment-mesh-observer-cleanup-retry');
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
if (rollbackErrors.length > 0 && error && typeof error === 'object') {
|
|
1580
|
+
attachFailureMetadata(error, { registrationRollbackErrors: Object.freeze(rollbackErrors) });
|
|
1581
|
+
}
|
|
1582
|
+
throw error;
|
|
1583
|
+
}
|
|
1584
|
+
return true;
|
|
1585
|
+
};
|
|
1586
|
+
const flushPendingMeshObserverCleanup = (mesh) => {
|
|
1587
|
+
const pending = pendingMeshObserverCleanups.get(mesh);
|
|
1588
|
+
if (!pending)
|
|
1589
|
+
return Object.freeze([]);
|
|
1590
|
+
const cleanupErrors = [];
|
|
1591
|
+
if (pending.disposeObserver) {
|
|
1592
|
+
try {
|
|
1593
|
+
pending.mesh.onDisposeObservable.remove(pending.disposeObserver);
|
|
1594
|
+
pending.disposeObserver = null;
|
|
1595
|
+
}
|
|
1596
|
+
catch (error) {
|
|
1597
|
+
cleanupErrors.push(error);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
if (pending.enabledObserver) {
|
|
1601
|
+
try {
|
|
1602
|
+
pending.mesh.onEnabledStateChangedObservable.remove(pending.enabledObserver);
|
|
1603
|
+
pending.enabledObserver = null;
|
|
1604
|
+
}
|
|
1605
|
+
catch (error) {
|
|
1606
|
+
cleanupErrors.push(error);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
for (const binding of [...pending.ancestorEnabledObservers]) {
|
|
1610
|
+
try {
|
|
1611
|
+
binding.node.onEnabledStateChangedObservable.remove(binding.observer);
|
|
1612
|
+
pending.ancestorEnabledObservers.splice(pending.ancestorEnabledObservers.indexOf(binding), 1);
|
|
1613
|
+
}
|
|
1614
|
+
catch (error) {
|
|
1615
|
+
cleanupErrors.push(error);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
if (!pending.disposeObserver
|
|
1619
|
+
&& !pending.enabledObserver
|
|
1620
|
+
&& pending.ancestorEnabledObservers.length === 0) {
|
|
1621
|
+
pendingMeshObserverCleanups.delete(mesh);
|
|
1622
|
+
}
|
|
1623
|
+
return Object.freeze(cleanupErrors);
|
|
1624
|
+
};
|
|
1625
|
+
const flushPendingMeshObserverCleanups = () => {
|
|
1626
|
+
for (const mesh of [...pendingMeshObserverCleanups.keys()]) {
|
|
1627
|
+
flushPendingMeshObserverCleanup(mesh);
|
|
1628
|
+
}
|
|
1629
|
+
};
|
|
1630
|
+
const flushPendingMeshRegistrationRollbacks = () => {
|
|
1631
|
+
for (const mesh of [...pendingMeshRegistrationRollbacks]) {
|
|
1632
|
+
try {
|
|
1633
|
+
unregisterMesh(mesh);
|
|
1634
|
+
pendingMeshRegistrationRollbacks.delete(mesh);
|
|
1635
|
+
}
|
|
1636
|
+
catch {
|
|
1637
|
+
// Ownership remains in the queue and the post-flush wake predicate
|
|
1638
|
+
// schedules the next bounded attempt.
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
};
|
|
1642
|
+
const flushPendingSceneMeshRegistrations = () => {
|
|
1643
|
+
for (const mesh of [...pendingSceneMeshRegistrations]) {
|
|
1644
|
+
if (!scene.meshes.includes(mesh)) {
|
|
1645
|
+
pendingSceneMeshRegistrations.delete(mesh);
|
|
1646
|
+
continue;
|
|
1647
|
+
}
|
|
1648
|
+
registerMesh(mesh);
|
|
1649
|
+
pendingSceneMeshRegistrations.delete(mesh);
|
|
1650
|
+
}
|
|
1651
|
+
};
|
|
1652
|
+
const deactivate = () => {
|
|
1653
|
+
pendingCompositeRefreshReasons.clear();
|
|
1654
|
+
// A proposed candidate view is unpublished state. Revoking it before any
|
|
1655
|
+
// fallible backend cleanup prevents a later retry frame from committing
|
|
1656
|
+
// that stale proposal after the Plan has been disabled or replaced.
|
|
1657
|
+
pendingAutomaticActivityCandidateTransition = null;
|
|
1658
|
+
const cleanupErrors = [];
|
|
1659
|
+
const attempt = (operation) => {
|
|
1660
|
+
try {
|
|
1661
|
+
operation();
|
|
1662
|
+
return true;
|
|
1663
|
+
}
|
|
1664
|
+
catch (error) {
|
|
1665
|
+
cleanupErrors.push(error);
|
|
1666
|
+
return false;
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1669
|
+
for (const target of [...pendingReceiverRestorations.keys()]) {
|
|
1670
|
+
attempt(() => restorePendingReceiver(target));
|
|
1671
|
+
}
|
|
1672
|
+
for (const [mesh, binding] of [...bindings]) {
|
|
1673
|
+
let cleaned = true;
|
|
1674
|
+
cleaned = attempt(() => mesh.onEnabledStateChangedObservable.remove(binding.enabledObserver)) && cleaned;
|
|
1675
|
+
for (const ancestorBinding of binding.ancestorEnabledObservers) {
|
|
1676
|
+
cleaned = attempt(() => {
|
|
1677
|
+
ancestorBinding.node.onEnabledStateChangedObservable.remove(ancestorBinding.observer);
|
|
1678
|
+
}) && cleaned;
|
|
1679
|
+
}
|
|
1680
|
+
cleaned = attempt(() => mesh.onDisposeObservable.remove(binding.disposeObserver)) && cleaned;
|
|
1681
|
+
if (binding.matrixObserver) {
|
|
1682
|
+
cleaned = attempt(() => mesh.onAfterWorldMatrixUpdateObservable.remove(binding.matrixObserver)) && cleaned;
|
|
1683
|
+
}
|
|
1684
|
+
cleaned = attempt(() => restoreReceiver(mesh)) && cleaned;
|
|
1685
|
+
if (depthBackend) {
|
|
1686
|
+
cleaned = attempt(() => depthBackend.removeCaster(mesh)) && cleaned;
|
|
1687
|
+
}
|
|
1688
|
+
cleaned = attempt(() => activityScheduler.setEntityActive(binding.entityId, false)) && cleaned;
|
|
1689
|
+
if (cleaned)
|
|
1690
|
+
bindings.delete(mesh);
|
|
1691
|
+
if (cleaned)
|
|
1692
|
+
pendingMeshRegistrationRollbacks.delete(mesh);
|
|
1693
|
+
}
|
|
1694
|
+
for (const mesh of [...pendingMeshObserverCleanups.keys()]) {
|
|
1695
|
+
cleanupErrors.push(...flushPendingMeshObserverCleanup(mesh));
|
|
1696
|
+
}
|
|
1697
|
+
for (const [mesh, state] of [...receiverStates]) {
|
|
1698
|
+
pendingReceiverRestorations.set(state.mesh, state.previousReceiveShadows);
|
|
1699
|
+
if (attempt(() => {
|
|
1700
|
+
if (!isDisposed(state.mesh) && state.mesh.receiveShadows === state.ownedValue) {
|
|
1701
|
+
writeReceiver(state.mesh, state.previousReceiveShadows);
|
|
1702
|
+
}
|
|
1703
|
+
else {
|
|
1704
|
+
pendingReceiverRestorations.delete(state.mesh);
|
|
1705
|
+
}
|
|
1706
|
+
}))
|
|
1707
|
+
receiverStates.delete(mesh);
|
|
1708
|
+
}
|
|
1709
|
+
if (bindings.size === 0) {
|
|
1710
|
+
if (automaticActivityCandidateBindings.size > 0) {
|
|
1711
|
+
automaticActivityCandidateBindings.clear();
|
|
1712
|
+
bumpAutomaticActivityCandidateRevision();
|
|
1713
|
+
}
|
|
1714
|
+
meshesByRenderable.clear();
|
|
1715
|
+
candidateLayerByMesh.clear();
|
|
1716
|
+
candidateMeshCounts.static = 0;
|
|
1717
|
+
candidateMeshCounts.dynamic = 0;
|
|
1718
|
+
conservativeCasterMeshes.clear();
|
|
1719
|
+
meshDataCacheByMesh.clear();
|
|
1720
|
+
pendingMeshRegistrationRollbacks.clear();
|
|
1721
|
+
pendingMeshObserverRestorations.clear();
|
|
1722
|
+
pendingSceneMeshRegistrations.clear();
|
|
1723
|
+
pendingMeshUnregistrations.clear();
|
|
1724
|
+
pendingRenderableMembershipRetries.clear();
|
|
1725
|
+
pendingTransformMembershipRetries.clear();
|
|
1726
|
+
activeCasterEntityIds.clear();
|
|
1727
|
+
pendingFinalDeformationEntityIds.clear();
|
|
1728
|
+
pendingDeformationInvalidations.clear();
|
|
1729
|
+
pendingMembershipInvalidationLayers.clear();
|
|
1730
|
+
pendingCoverageMembershipRebuild = false;
|
|
1731
|
+
objectByRenderable.clear();
|
|
1732
|
+
objectByEntity.clear();
|
|
1733
|
+
}
|
|
1734
|
+
receiverTargetByMesh.clear();
|
|
1735
|
+
if (registry) {
|
|
1736
|
+
const ownedRegistry = registry;
|
|
1737
|
+
if (attempt(() => ownedRegistry.dispose()))
|
|
1738
|
+
registry = null;
|
|
1739
|
+
}
|
|
1740
|
+
if (depthBackend) {
|
|
1741
|
+
const ownedBackend = depthBackend;
|
|
1742
|
+
if (attempt(() => ownedBackend.dispose())) {
|
|
1743
|
+
depthBackend = null;
|
|
1744
|
+
generator = null;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
if (pendingBackendAllocationCleanup) {
|
|
1748
|
+
const cleanup = pendingBackendAllocationCleanup;
|
|
1749
|
+
if (attempt(() => {
|
|
1750
|
+
const errors = cleanup();
|
|
1751
|
+
if (errors.length > 0) {
|
|
1752
|
+
throw Object.assign(new Error('shadowMapExperiment.backendAllocationCleanupFailed'), { cleanupErrors: errors });
|
|
1753
|
+
}
|
|
1754
|
+
}))
|
|
1755
|
+
pendingBackendAllocationCleanup = null;
|
|
1756
|
+
}
|
|
1757
|
+
if (!depthBackend && !pendingBackendAllocationCleanup)
|
|
1758
|
+
pendingFallbackReason = null;
|
|
1759
|
+
if (lightState) {
|
|
1760
|
+
const ownedLightState = lightState;
|
|
1761
|
+
if (attempt(() => restoreLight(ownedLightState)))
|
|
1762
|
+
lightState = null;
|
|
1763
|
+
}
|
|
1764
|
+
if (previousSceneShadowsEnabled !== null) {
|
|
1765
|
+
const ownedSceneShadowsEnabled = previousSceneShadowsEnabled;
|
|
1766
|
+
if (attempt(() => { scene.shadowsEnabled = ownedSceneShadowsEnabled; }))
|
|
1767
|
+
previousSceneShadowsEnabled = null;
|
|
1768
|
+
}
|
|
1769
|
+
effectiveCoverageCenter = null;
|
|
1770
|
+
effectiveCoverageExtent = null;
|
|
1771
|
+
effectiveCoverageNear = null;
|
|
1772
|
+
effectiveCoverageFar = null;
|
|
1773
|
+
if (cleanupErrors.length > 0) {
|
|
1774
|
+
throw Object.assign(new Error('shadowMapExperiment.deactivationFailed'), { cleanupErrors });
|
|
1775
|
+
}
|
|
1776
|
+
};
|
|
1777
|
+
const activate = (nextPlan) => {
|
|
1778
|
+
const light = resolveBabylonDirectionalLight(scene, nextPlan.light.entityId);
|
|
1779
|
+
if (!light)
|
|
1780
|
+
throw new Error(`shadowMapExperiment.lightMissing:${nextPlan.light.entityId}`);
|
|
1781
|
+
const existingGenerators = readShadowGenerators(light);
|
|
1782
|
+
if (existingGenerators.length > 0)
|
|
1783
|
+
throw new Error(`shadowMapExperiment.backendConflict:${nextPlan.light.entityId}`);
|
|
1784
|
+
previousSceneShadowsEnabled = scene.shadowsEnabled;
|
|
1785
|
+
influenceCulling.configure(nextPlan.culling.guardBandRatio);
|
|
1786
|
+
lightState = captureLight(light);
|
|
1787
|
+
effectiveCoverageCenter = nextPlan.generator.bounds.center;
|
|
1788
|
+
effectiveCoverageExtent = nextPlan.generator.bounds.extent;
|
|
1789
|
+
effectiveCoverageNear = nextPlan.generator.bounds.near;
|
|
1790
|
+
effectiveCoverageFar = nextPlan.generator.bounds.far;
|
|
1791
|
+
configureLight(light, nextPlan, {
|
|
1792
|
+
center: nextPlan.generator.bounds.center,
|
|
1793
|
+
extent: nextPlan.generator.bounds.extent,
|
|
1794
|
+
near: nextPlan.generator.bounds.near,
|
|
1795
|
+
far: nextPlan.generator.bounds.far,
|
|
1796
|
+
});
|
|
1797
|
+
scene.shadowsEnabled = true;
|
|
1798
|
+
light.shadowEnabled = true;
|
|
1799
|
+
try {
|
|
1800
|
+
depthBackend = createBabylonShadowMapExperimentDepthBackend(scene, light, nextPlan, {
|
|
1801
|
+
onCompositorLifecycleEvent: handleCompositorLifecycleEvent,
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
catch (error) {
|
|
1805
|
+
rememberBackendAllocationCleanup(error);
|
|
1806
|
+
throw error;
|
|
1807
|
+
}
|
|
1808
|
+
generator = depthBackend.generator;
|
|
1809
|
+
if (depthBackend.fallbackReason)
|
|
1810
|
+
warnForFallback(depthBackend.fallbackReason);
|
|
1811
|
+
registry = createShadowMapExperimentRegistry(nextPlan);
|
|
1812
|
+
for (const object of nextPlan.objects) {
|
|
1813
|
+
objectByEntity.set(object.entityId, object);
|
|
1814
|
+
for (const renderableId of object.renderableIds)
|
|
1815
|
+
objectByRenderable.set(renderableId, object);
|
|
1816
|
+
}
|
|
1817
|
+
updateCameraInfluenceCoverage();
|
|
1818
|
+
stagingPlanCandidateTransitions = true;
|
|
1819
|
+
try {
|
|
1820
|
+
for (const mesh of scene.meshes)
|
|
1821
|
+
registerMesh(mesh);
|
|
1822
|
+
}
|
|
1823
|
+
finally {
|
|
1824
|
+
stagingPlanCandidateTransitions = false;
|
|
1825
|
+
}
|
|
1826
|
+
const nextCandidates = new Map();
|
|
1827
|
+
for (const binding of bindings.values()) {
|
|
1828
|
+
updateAutomaticActivityCandidateBinding(nextCandidates, binding, objectByRenderable.get(binding.renderableId));
|
|
1829
|
+
}
|
|
1830
|
+
if (!applyAutomaticActivityCandidateTransition(nextCandidates)
|
|
1831
|
+
&& pendingAutomaticAnimationGroupAttachments.size === 0
|
|
1832
|
+
&& pendingAutomaticAnimationGroupDetachCleanups.size === 0) {
|
|
1833
|
+
throw new Error('shadowMapExperiment.automaticActivityCandidateCommitFailed');
|
|
1834
|
+
}
|
|
1835
|
+
markAllDepthDirty('generator-created');
|
|
1836
|
+
};
|
|
1837
|
+
const registerActivitySource = (input) => {
|
|
1838
|
+
assertUsable();
|
|
1839
|
+
const controller = activityScheduler.registerSource(input);
|
|
1840
|
+
const deformationSource = input.kind === 'animation' || input.kind === 'morph';
|
|
1841
|
+
let sourceActive = false;
|
|
1842
|
+
let controllerDisposed = false;
|
|
1843
|
+
const updateDeformationSourceCount = (previousActive, nextActive) => {
|
|
1844
|
+
if (!deformationSource || previousActive === nextActive)
|
|
1845
|
+
return;
|
|
1846
|
+
const previousCount = activeDeformationSourceCountByEntity.get(input.entityId) ?? 0;
|
|
1847
|
+
if (nextActive) {
|
|
1848
|
+
activeDeformationSourceCountByEntity.set(input.entityId, previousCount + 1);
|
|
1849
|
+
pendingFinalDeformationEntityIds.delete(input.entityId);
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
if (previousCount <= 1) {
|
|
1853
|
+
activeDeformationSourceCountByEntity.delete(input.entityId);
|
|
1854
|
+
if (activeCasterEntityIds.has(input.entityId))
|
|
1855
|
+
pendingFinalDeformationEntityIds.add(input.entityId);
|
|
1856
|
+
}
|
|
1857
|
+
else {
|
|
1858
|
+
activeDeformationSourceCountByEntity.set(input.entityId, previousCount - 1);
|
|
1859
|
+
}
|
|
1860
|
+
};
|
|
1861
|
+
return Object.freeze({
|
|
1862
|
+
entityId: controller.entityId,
|
|
1863
|
+
kind: controller.kind,
|
|
1864
|
+
sourceId: controller.sourceId,
|
|
1865
|
+
setActive(nextActive) {
|
|
1866
|
+
if (disposed || disposing || controllerDisposed || sourceActive === nextActive)
|
|
1867
|
+
return false;
|
|
1868
|
+
if (!controller.setActive(nextActive))
|
|
1869
|
+
return false;
|
|
1870
|
+
updateDeformationSourceCount(sourceActive, nextActive);
|
|
1871
|
+
sourceActive = nextActive;
|
|
1872
|
+
requestRefreshFrame(`shadow-map-experiment-${nextActive ? 'caster-activity-started' : 'caster-activity-stopped'}`);
|
|
1873
|
+
return true;
|
|
1874
|
+
},
|
|
1875
|
+
invalidate() {
|
|
1876
|
+
if (disposed || disposing || controllerDisposed)
|
|
1877
|
+
return false;
|
|
1878
|
+
const invalidated = controller.invalidate();
|
|
1879
|
+
if (!invalidated)
|
|
1880
|
+
return false;
|
|
1881
|
+
const reason = dirtyReasonForActivityKind(input.kind);
|
|
1882
|
+
if (deformationSource) {
|
|
1883
|
+
const invalidationReasons = pendingDeformationInvalidations.get(input.entityId) ?? new Set();
|
|
1884
|
+
invalidationReasons.add(reason);
|
|
1885
|
+
pendingDeformationInvalidations.set(input.entityId, invalidationReasons);
|
|
1886
|
+
}
|
|
1887
|
+
requestRefreshFrame(`shadow-map-experiment-${reason}`);
|
|
1888
|
+
return true;
|
|
1889
|
+
},
|
|
1890
|
+
dispose() {
|
|
1891
|
+
if (controllerDisposed)
|
|
1892
|
+
return;
|
|
1893
|
+
const wasActive = sourceActive;
|
|
1894
|
+
updateDeformationSourceCount(sourceActive, false);
|
|
1895
|
+
sourceActive = false;
|
|
1896
|
+
controllerDisposed = true;
|
|
1897
|
+
controller.dispose();
|
|
1898
|
+
if (wasActive)
|
|
1899
|
+
requestRefreshFrame('shadow-map-experiment-caster-activity-stopped');
|
|
1900
|
+
},
|
|
1901
|
+
});
|
|
1902
|
+
};
|
|
1903
|
+
const resolveAutomaticActivityTargets = (group, candidateView = automaticActivityCandidateBindings) => {
|
|
1904
|
+
const desired = new Map();
|
|
1905
|
+
if (candidateView.size === 0)
|
|
1906
|
+
return desired;
|
|
1907
|
+
const targets = new Set(group.targetedAnimations.map(targetedAnimation => targetedAnimation.target));
|
|
1908
|
+
// Build the candidate relationship index once per group reconciliation.
|
|
1909
|
+
// A skinned AnimationGroup commonly contains one targeted animation per
|
|
1910
|
+
// linked bone TransformNode. Resolving every target by rescanning every
|
|
1911
|
+
// candidate skeleton makes playback start O(targets × meshes × bones).
|
|
1912
|
+
const targetIndex = createAutomaticActivityTargetIndex(candidateView, targets);
|
|
1913
|
+
for (const target of targets) {
|
|
1914
|
+
const resolvedTarget = resolveAutomaticActivityTarget(candidateView, target, objectByRenderable, targetIndex);
|
|
1915
|
+
for (const mesh of resolvedTarget.meshes) {
|
|
1916
|
+
const meshBinding = candidateView.get(mesh);
|
|
1917
|
+
const object = meshBinding ? objectByEntity.get(meshBinding.entityId) : null;
|
|
1918
|
+
if (!meshBinding || !isAutomaticActivityCandidateObject(object))
|
|
1919
|
+
continue;
|
|
1920
|
+
const key = `${meshBinding.entityId}\u0000${resolvedTarget.kind}`;
|
|
1921
|
+
desired.set(key, { entityId: meshBinding.entityId, kind: resolvedTarget.kind });
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
return desired;
|
|
1925
|
+
};
|
|
1926
|
+
const updateAutomaticAnimationGroupTargetIndex = (binding, targets) => {
|
|
1927
|
+
const removeBinding = (index, key) => {
|
|
1928
|
+
const indexedBindings = index.get(key);
|
|
1929
|
+
indexedBindings?.delete(binding);
|
|
1930
|
+
if (indexedBindings?.size === 0)
|
|
1931
|
+
index.delete(key);
|
|
1932
|
+
};
|
|
1933
|
+
for (const key of binding.resolvedTargetIndexKeys) {
|
|
1934
|
+
removeBinding(automaticAnimationGroupsByResolvedTarget, key);
|
|
1935
|
+
}
|
|
1936
|
+
for (const key of binding.resolvedTargetAncestorIndexKeys) {
|
|
1937
|
+
removeBinding(automaticAnimationGroupsByResolvedTargetAncestor, key);
|
|
1938
|
+
}
|
|
1939
|
+
for (const key of binding.resolvedTargetRenderableIndexKeys) {
|
|
1940
|
+
removeBinding(automaticAnimationGroupsByResolvedTargetRenderable, key);
|
|
1941
|
+
}
|
|
1942
|
+
const targetKeys = new Set();
|
|
1943
|
+
const ancestorKeys = new Set();
|
|
1944
|
+
const renderableKeys = new Set();
|
|
1945
|
+
for (const target of targets) {
|
|
1946
|
+
if (!target || (typeof target !== 'object' && typeof target !== 'function'))
|
|
1947
|
+
continue;
|
|
1948
|
+
targetKeys.add(target);
|
|
1949
|
+
let current = target;
|
|
1950
|
+
let ancestor = false;
|
|
1951
|
+
const visited = new Set();
|
|
1952
|
+
while (current && (typeof current === 'object' || typeof current === 'function')) {
|
|
1953
|
+
if (visited.has(current))
|
|
1954
|
+
break;
|
|
1955
|
+
visited.add(current);
|
|
1956
|
+
if (ancestor)
|
|
1957
|
+
ancestorKeys.add(current);
|
|
1958
|
+
for (const identity of collectIdentityCandidates(current)) {
|
|
1959
|
+
renderableKeys.add(identity);
|
|
1960
|
+
}
|
|
1961
|
+
current = current.parent ?? null;
|
|
1962
|
+
ancestor = true;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
for (const key of targetKeys) {
|
|
1966
|
+
addMapSetValue(automaticAnimationGroupsByResolvedTarget, key, binding);
|
|
1967
|
+
}
|
|
1968
|
+
for (const key of ancestorKeys) {
|
|
1969
|
+
addMapSetValue(automaticAnimationGroupsByResolvedTargetAncestor, key, binding);
|
|
1970
|
+
}
|
|
1971
|
+
for (const key of renderableKeys) {
|
|
1972
|
+
addMapSetValue(automaticAnimationGroupsByResolvedTargetRenderable, key, binding);
|
|
1973
|
+
}
|
|
1974
|
+
binding.resolvedTargetIndexKeys = Object.freeze([...targetKeys]);
|
|
1975
|
+
binding.resolvedTargetAncestorIndexKeys = Object.freeze([...ancestorKeys]);
|
|
1976
|
+
binding.resolvedTargetRenderableIndexKeys = Object.freeze([...renderableKeys]);
|
|
1977
|
+
};
|
|
1978
|
+
const collectAutomaticActivityCandidateRelationshipKeys = (mesh) => {
|
|
1979
|
+
const keys = new Set();
|
|
1980
|
+
let current = mesh;
|
|
1981
|
+
while (current && (typeof current === 'object' || typeof current === 'function')) {
|
|
1982
|
+
if (keys.has(current))
|
|
1983
|
+
break;
|
|
1984
|
+
keys.add(current);
|
|
1985
|
+
current = current.parent ?? null;
|
|
1986
|
+
}
|
|
1987
|
+
const skeleton = mesh.skeleton;
|
|
1988
|
+
if (skeleton) {
|
|
1989
|
+
keys.add(skeleton);
|
|
1990
|
+
for (const bone of skeleton.bones ?? []) {
|
|
1991
|
+
keys.add(bone);
|
|
1992
|
+
const transformNode = callOptional(bone, 'getTransformNode')
|
|
1993
|
+
?? bone?._linkedTransformNode
|
|
1994
|
+
?? null;
|
|
1995
|
+
if (transformNode)
|
|
1996
|
+
keys.add(transformNode);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
const manager = mesh.morphTargetManager;
|
|
2000
|
+
const count = manager?.numTargets ?? 0;
|
|
2001
|
+
if (manager?.getTarget && Number.isInteger(count) && count > 0) {
|
|
2002
|
+
for (let index = 0; index < count; index += 1) {
|
|
2003
|
+
try {
|
|
2004
|
+
const target = manager.getTarget(index);
|
|
2005
|
+
if (target)
|
|
2006
|
+
keys.add(target);
|
|
2007
|
+
}
|
|
2008
|
+
catch {
|
|
2009
|
+
// A single malformed morph slot must not make the candidate delta
|
|
2010
|
+
// fan out to every AnimationGroup.
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
return Object.freeze([...keys]);
|
|
2015
|
+
};
|
|
2016
|
+
const collectAutomaticAnimationGroupsAffectedByCandidateTransition = (nextCandidates) => {
|
|
2017
|
+
const affected = new Set();
|
|
2018
|
+
const changedCandidates = [];
|
|
2019
|
+
const changedEntityIds = new Set();
|
|
2020
|
+
for (const mesh of new Set([
|
|
2021
|
+
...automaticActivityCandidateBindings.keys(),
|
|
2022
|
+
...nextCandidates.keys(),
|
|
2023
|
+
])) {
|
|
2024
|
+
const previous = automaticActivityCandidateBindings.get(mesh);
|
|
2025
|
+
const next = nextCandidates.get(mesh);
|
|
2026
|
+
if (previous?.entityId === next?.entityId
|
|
2027
|
+
&& previous?.renderableId === next?.renderableId)
|
|
2028
|
+
continue;
|
|
2029
|
+
changedCandidates.push({ mesh, previous, next });
|
|
2030
|
+
if (previous)
|
|
2031
|
+
changedEntityIds.add(previous.entityId);
|
|
2032
|
+
if (next)
|
|
2033
|
+
changedEntityIds.add(next.entityId);
|
|
2034
|
+
}
|
|
2035
|
+
for (const binding of automaticAnimationGroups.values()) {
|
|
2036
|
+
if (!binding.ready)
|
|
2037
|
+
continue;
|
|
2038
|
+
if (binding.rebindPending
|
|
2039
|
+
|| binding.resolvedCandidateRevision !== automaticActivityCandidateRevision
|
|
2040
|
+
|| [...binding.sources.values()].some(source => changedEntityIds.has(source.entityId))) {
|
|
2041
|
+
affected.add(binding);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
const includeIndexedBindings = (index, key) => {
|
|
2045
|
+
for (const binding of index.get(key) ?? []) {
|
|
2046
|
+
if (binding.ready)
|
|
2047
|
+
affected.add(binding);
|
|
2048
|
+
}
|
|
2049
|
+
};
|
|
2050
|
+
for (const { mesh, previous, next } of changedCandidates) {
|
|
2051
|
+
for (const key of collectAutomaticActivityCandidateRelationshipKeys(mesh)) {
|
|
2052
|
+
includeIndexedBindings(automaticAnimationGroupsByResolvedTarget, key);
|
|
2053
|
+
}
|
|
2054
|
+
// A target below the candidate mesh is related, but a target below a
|
|
2055
|
+
// sibling that merely shares a generic root is not.
|
|
2056
|
+
includeIndexedBindings(automaticAnimationGroupsByResolvedTargetAncestor, mesh);
|
|
2057
|
+
for (const renderableId of new Set([
|
|
2058
|
+
previous?.renderableId,
|
|
2059
|
+
next?.renderableId,
|
|
2060
|
+
].filter((value) => typeof value === 'string'))) {
|
|
2061
|
+
includeIndexedBindings(automaticAnimationGroupsByResolvedTargetRenderable, renderableId);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
return affected;
|
|
2065
|
+
};
|
|
2066
|
+
const shouldFullyResolveAutomaticActivityTargetsOnPlay = (binding) => {
|
|
2067
|
+
if (binding.resolvedCandidateRevision !== automaticActivityCandidateRevision)
|
|
2068
|
+
return false;
|
|
2069
|
+
const currentTargets = binding.group.targetedAnimations.map(targetedAnimation => targetedAnimation.target);
|
|
2070
|
+
if (currentTargets.length !== binding.resolvedTargets.length
|
|
2071
|
+
|| currentTargets.some((target, index) => target !== binding.resolvedTargets[index])) {
|
|
2072
|
+
return true;
|
|
2073
|
+
}
|
|
2074
|
+
// A negative binding has no owner-scoped subset to validate. Preserve the
|
|
2075
|
+
// restart boundary that lets a previously unresolved target become owned.
|
|
2076
|
+
if (binding.sources.size === 0)
|
|
2077
|
+
return true;
|
|
2078
|
+
const sourceEntityIds = new Set([...binding.sources.values()].map(source => source.entityId));
|
|
2079
|
+
const sourceCandidates = new Map();
|
|
2080
|
+
for (const [mesh, candidate] of automaticActivityCandidateBindings) {
|
|
2081
|
+
if (sourceEntityIds.has(candidate.entityId))
|
|
2082
|
+
sourceCandidates.set(mesh, candidate);
|
|
2083
|
+
}
|
|
2084
|
+
const desired = resolveAutomaticActivityTargets(binding.group, sourceCandidates);
|
|
2085
|
+
return desired.size !== binding.sources.size
|
|
2086
|
+
|| [...desired.keys()].some(key => !binding.sources.has(key));
|
|
2087
|
+
};
|
|
2088
|
+
const syncAutomaticAnimationGroupPlayback = (binding) => {
|
|
2089
|
+
const active = binding.group.isPlaying === true;
|
|
2090
|
+
for (const source of binding.sources.values())
|
|
2091
|
+
source.controller.setActive(active);
|
|
2092
|
+
binding.active = active;
|
|
2093
|
+
};
|
|
2094
|
+
const cleanupStagedAutomaticAnimationGroupSources = (binding) => {
|
|
2095
|
+
const cleanupErrors = [];
|
|
2096
|
+
for (const [key, source] of [...binding.stagedSources.entries()].reverse()) {
|
|
2097
|
+
try {
|
|
2098
|
+
source.controller.setActive(false);
|
|
2099
|
+
}
|
|
2100
|
+
catch (error) {
|
|
2101
|
+
cleanupErrors.push(error);
|
|
2102
|
+
}
|
|
2103
|
+
try {
|
|
2104
|
+
source.controller.dispose();
|
|
2105
|
+
binding.stagedSources.delete(key);
|
|
2106
|
+
}
|
|
2107
|
+
catch (error) {
|
|
2108
|
+
cleanupErrors.push(error);
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
if (cleanupErrors.length > 0) {
|
|
2112
|
+
pendingAutomaticAnimationGroupSourceCleanups.add(binding);
|
|
2113
|
+
}
|
|
2114
|
+
return Object.freeze(cleanupErrors);
|
|
2115
|
+
};
|
|
2116
|
+
const prepareAutomaticAnimationGroupRebind = (binding, candidateView, forceTargetResolution = false) => {
|
|
2117
|
+
const shouldResolveTargets = forceTargetResolution
|
|
2118
|
+
|| binding.resolvedCandidateRevision !== automaticActivityCandidateRevision;
|
|
2119
|
+
if (!shouldResolveTargets)
|
|
2120
|
+
return false;
|
|
2121
|
+
automaticActivityTargetResolutionCount += 1;
|
|
2122
|
+
if (binding.stagedSources.size > 0) {
|
|
2123
|
+
const cleanupErrors = cleanupStagedAutomaticAnimationGroupSources(binding);
|
|
2124
|
+
if (cleanupErrors.length > 0) {
|
|
2125
|
+
throw cleanupErrors[0];
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
const desired = resolveAutomaticActivityTargets(binding.group, candidateView);
|
|
2129
|
+
if (desired.size !== binding.sources.size
|
|
2130
|
+
|| [...desired.keys()].some(key => !binding.sources.has(key))) {
|
|
2131
|
+
binding.rebindPending = true;
|
|
2132
|
+
binding.resolvedCandidateRevision = -1;
|
|
2133
|
+
}
|
|
2134
|
+
// Source replacement has one commit boundary: every stale committed
|
|
2135
|
+
// source must be fully disposed before any replacement is acquired.
|
|
2136
|
+
for (const [key, source] of [...binding.sources]) {
|
|
2137
|
+
if (desired.has(key))
|
|
2138
|
+
continue;
|
|
2139
|
+
binding.rebindPending = true;
|
|
2140
|
+
try {
|
|
2141
|
+
source.controller.setActive(false);
|
|
2142
|
+
source.controller.dispose();
|
|
2143
|
+
binding.sources.delete(key);
|
|
2144
|
+
}
|
|
2145
|
+
catch (error) {
|
|
2146
|
+
binding.resolvedCandidateRevision = -1;
|
|
2147
|
+
pendingAutomaticAnimationGroupSourceCleanups.add(binding);
|
|
2148
|
+
throw error;
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
try {
|
|
2152
|
+
for (const [key, descriptor] of desired) {
|
|
2153
|
+
if (binding.sources.has(key) || binding.stagedSources.has(key))
|
|
2154
|
+
continue;
|
|
2155
|
+
const sourceId = `fps:auto:animation-group:${binding.group.uniqueId}:${descriptor.kind}`;
|
|
2156
|
+
const controller = system.registerCasterActivitySource({ ...descriptor, sourceId });
|
|
2157
|
+
binding.stagedSources.set(key, { ...descriptor, controller });
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
catch (error) {
|
|
2161
|
+
cleanupStagedAutomaticAnimationGroupSources(binding);
|
|
2162
|
+
throw error;
|
|
2163
|
+
}
|
|
2164
|
+
return true;
|
|
2165
|
+
};
|
|
2166
|
+
const commitPreparedAutomaticAnimationGroupRebind = (binding, candidateRevision) => {
|
|
2167
|
+
for (const [key, source] of binding.stagedSources) {
|
|
2168
|
+
binding.sources.set(key, source);
|
|
2169
|
+
}
|
|
2170
|
+
binding.stagedSources.clear();
|
|
2171
|
+
binding.rebindPending = false;
|
|
2172
|
+
binding.resolvedCandidateRevision = candidateRevision;
|
|
2173
|
+
const resolvedTargets = Object.freeze(binding.group.targetedAnimations.map(targetedAnimation => targetedAnimation.target));
|
|
2174
|
+
binding.resolvedTargets = resolvedTargets;
|
|
2175
|
+
updateAutomaticAnimationGroupTargetIndex(binding, resolvedTargets);
|
|
2176
|
+
};
|
|
2177
|
+
const reconcileAutomaticAnimationGroup = (binding, forceTargetResolution = false) => {
|
|
2178
|
+
const prepared = prepareAutomaticAnimationGroupRebind(binding, automaticActivityCandidateBindings, forceTargetResolution);
|
|
2179
|
+
if (prepared) {
|
|
2180
|
+
commitPreparedAutomaticAnimationGroupRebind(binding, automaticActivityCandidateRevision);
|
|
2181
|
+
}
|
|
2182
|
+
syncAutomaticAnimationGroupPlayback(binding);
|
|
2183
|
+
};
|
|
2184
|
+
const safelyReconcileAutomaticAnimationGroup = (binding, forceTargetResolution = false) => {
|
|
2185
|
+
if (disposed || disposing || !binding.ready)
|
|
2186
|
+
return true;
|
|
2187
|
+
try {
|
|
2188
|
+
reconcileAutomaticAnimationGroup(binding, forceTargetResolution);
|
|
2189
|
+
pendingAutomaticAnimationGroupReconciliations.delete(binding);
|
|
2190
|
+
return true;
|
|
2191
|
+
}
|
|
2192
|
+
catch (error) {
|
|
2193
|
+
pendingAutomaticAnimationGroupReconciliations.add(binding);
|
|
2194
|
+
requestRefreshFrame('shadow-map-experiment-automatic-activity-reconcile-retry');
|
|
2195
|
+
if (pendingAutomaticAnimationGroupSourceCleanups.has(binding)) {
|
|
2196
|
+
requestRefreshFrame('shadow-map-experiment-automatic-activity-cleanup-retry');
|
|
2197
|
+
}
|
|
2198
|
+
rememberDiagnostic('shadowMapExperiment.automaticActivityBindingFailed', `AnimationGroup ${binding.group.name || binding.group.uniqueId} could not bind shadow activity: ${error instanceof Error ? error.message : String(error)}`);
|
|
2199
|
+
return false;
|
|
2200
|
+
}
|
|
2201
|
+
};
|
|
2202
|
+
const attachAutomaticAnimationGroup = (group, deferInitialReconcile = false) => {
|
|
2203
|
+
if (automaticAnimationGroups.has(group) || disposed || disposing)
|
|
2204
|
+
return;
|
|
2205
|
+
const binding = {
|
|
2206
|
+
group,
|
|
2207
|
+
playObserver: null,
|
|
2208
|
+
pauseObserver: null,
|
|
2209
|
+
endObserver: null,
|
|
2210
|
+
sources: new Map(),
|
|
2211
|
+
stagedSources: new Map(),
|
|
2212
|
+
active: false,
|
|
2213
|
+
ready: false,
|
|
2214
|
+
rebindPending: false,
|
|
2215
|
+
resolvedCandidateRevision: -1,
|
|
2216
|
+
resolvedTargets: Object.freeze([]),
|
|
2217
|
+
resolvedTargetIndexKeys: Object.freeze([]),
|
|
2218
|
+
resolvedTargetAncestorIndexKeys: Object.freeze([]),
|
|
2219
|
+
resolvedTargetRenderableIndexKeys: Object.freeze([]),
|
|
2220
|
+
};
|
|
2221
|
+
automaticAnimationGroups.set(group, binding);
|
|
2222
|
+
try {
|
|
2223
|
+
binding.playObserver = group.onAnimationGroupPlayObservable.add(() => {
|
|
2224
|
+
if (!binding.ready)
|
|
2225
|
+
return;
|
|
2226
|
+
safelyReconcileAutomaticAnimationGroup(binding, shouldFullyResolveAutomaticActivityTargetsOnPlay(binding));
|
|
2227
|
+
requestRefreshFrame('shadow-map-experiment-animation-group-played');
|
|
2228
|
+
});
|
|
2229
|
+
binding.pauseObserver = group.onAnimationGroupPauseObservable.add(() => {
|
|
2230
|
+
if (!binding.ready)
|
|
2231
|
+
return;
|
|
2232
|
+
try {
|
|
2233
|
+
syncAutomaticAnimationGroupPlayback(binding);
|
|
2234
|
+
}
|
|
2235
|
+
catch (error) {
|
|
2236
|
+
rememberDiagnostic('shadowMapExperiment.automaticActivityBindingFailed', `AnimationGroup ${binding.group.name || binding.group.uniqueId} could not update shadow activity: ${error instanceof Error ? error.message : String(error)}`);
|
|
2237
|
+
}
|
|
2238
|
+
});
|
|
2239
|
+
binding.endObserver = group.onAnimationGroupEndObservable.add(() => {
|
|
2240
|
+
if (!binding.ready)
|
|
2241
|
+
return;
|
|
2242
|
+
try {
|
|
2243
|
+
syncAutomaticAnimationGroupPlayback(binding);
|
|
2244
|
+
}
|
|
2245
|
+
catch (error) {
|
|
2246
|
+
rememberDiagnostic('shadowMapExperiment.automaticActivityBindingFailed', `AnimationGroup ${binding.group.name || binding.group.uniqueId} could not update shadow activity: ${error instanceof Error ? error.message : String(error)}`);
|
|
2247
|
+
}
|
|
2248
|
+
});
|
|
2249
|
+
binding.ready = true;
|
|
2250
|
+
if (!deferInitialReconcile)
|
|
2251
|
+
safelyReconcileAutomaticAnimationGroup(binding);
|
|
2252
|
+
pendingAutomaticAnimationGroupAttachments.delete(group);
|
|
2253
|
+
}
|
|
2254
|
+
catch (error) {
|
|
2255
|
+
binding.ready = false;
|
|
2256
|
+
const cleanupErrors = detachAutomaticAnimationGroup(group);
|
|
2257
|
+
if (cleanupErrors.length > 0 && error && typeof error === 'object') {
|
|
2258
|
+
attachFailureMetadata(error, { cleanupErrors });
|
|
2259
|
+
}
|
|
2260
|
+
throw error;
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
2263
|
+
const detachAutomaticAnimationGroup = (group) => {
|
|
2264
|
+
const binding = automaticAnimationGroups.get(group);
|
|
2265
|
+
if (!binding)
|
|
2266
|
+
return Object.freeze([]);
|
|
2267
|
+
binding.ready = false;
|
|
2268
|
+
updateAutomaticAnimationGroupTargetIndex(binding, Object.freeze([]));
|
|
2269
|
+
pendingAutomaticAnimationGroupReconciliations.delete(binding);
|
|
2270
|
+
const cleanupErrors = [...cleanupAutomaticAnimationGroupSources(binding)];
|
|
2271
|
+
pendingAutomaticAnimationGroupSourceCleanups.delete(binding);
|
|
2272
|
+
if (binding.playObserver) {
|
|
2273
|
+
try {
|
|
2274
|
+
group.onAnimationGroupPlayObservable.remove(binding.playObserver);
|
|
2275
|
+
binding.playObserver = null;
|
|
2276
|
+
}
|
|
2277
|
+
catch (error) {
|
|
2278
|
+
cleanupErrors.push(error);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
if (binding.pauseObserver) {
|
|
2282
|
+
try {
|
|
2283
|
+
group.onAnimationGroupPauseObservable.remove(binding.pauseObserver);
|
|
2284
|
+
binding.pauseObserver = null;
|
|
2285
|
+
}
|
|
2286
|
+
catch (error) {
|
|
2287
|
+
cleanupErrors.push(error);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
if (binding.endObserver) {
|
|
2291
|
+
try {
|
|
2292
|
+
group.onAnimationGroupEndObservable.remove(binding.endObserver);
|
|
2293
|
+
binding.endObserver = null;
|
|
2294
|
+
}
|
|
2295
|
+
catch (error) {
|
|
2296
|
+
cleanupErrors.push(error);
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
if (cleanupErrors.length === 0
|
|
2300
|
+
&& binding.sources.size === 0
|
|
2301
|
+
&& binding.stagedSources.size === 0
|
|
2302
|
+
&& !binding.playObserver
|
|
2303
|
+
&& !binding.pauseObserver
|
|
2304
|
+
&& !binding.endObserver) {
|
|
2305
|
+
automaticAnimationGroups.delete(group);
|
|
2306
|
+
pendingAutomaticAnimationGroupDetachCleanups.delete(binding);
|
|
2307
|
+
}
|
|
2308
|
+
else {
|
|
2309
|
+
pendingAutomaticAnimationGroupDetachCleanups.add(binding);
|
|
2310
|
+
requestRefreshFrame('shadow-map-experiment-automatic-activity-detach-retry');
|
|
2311
|
+
}
|
|
2312
|
+
return Object.freeze(cleanupErrors);
|
|
2313
|
+
};
|
|
2314
|
+
const flushPendingAutomaticAnimationGroupDetachCleanups = () => {
|
|
2315
|
+
if (pendingAutomaticAnimationGroupDetachCleanups.size === 0)
|
|
2316
|
+
return;
|
|
2317
|
+
for (const binding of [...pendingAutomaticAnimationGroupDetachCleanups]) {
|
|
2318
|
+
const group = binding.group;
|
|
2319
|
+
const cleanupErrors = detachAutomaticAnimationGroup(group);
|
|
2320
|
+
if (cleanupErrors.length > 0)
|
|
2321
|
+
continue;
|
|
2322
|
+
if (!pendingAutomaticActivityCandidateTransition
|
|
2323
|
+
&& automaticActivityCandidateBindings.size > 0
|
|
2324
|
+
&& scene.animationGroups.includes(group)) {
|
|
2325
|
+
try {
|
|
2326
|
+
attachAutomaticAnimationGroup(group);
|
|
2327
|
+
pendingAutomaticAnimationGroupAttachments.delete(group);
|
|
2328
|
+
}
|
|
2329
|
+
catch (error) {
|
|
2330
|
+
pendingAutomaticAnimationGroupAttachments.add(group);
|
|
2331
|
+
requestRefreshFrame('shadow-map-experiment-automatic-activity-attach-retry');
|
|
2332
|
+
rememberDiagnostic('shadowMapExperiment.automaticActivityObserverFailed', `AnimationGroup ${group.name || group.uniqueId} could not be reattached after cleanup: ${error instanceof Error ? error.message : String(error)}`);
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
};
|
|
2337
|
+
const flushPendingAutomaticAnimationGroupAttachments = () => {
|
|
2338
|
+
// The proposed candidate transition owns observer acquisition while it is
|
|
2339
|
+
// pending. Attaching here would reconcile against the old committed view.
|
|
2340
|
+
if (pendingAutomaticActivityCandidateTransition)
|
|
2341
|
+
return;
|
|
2342
|
+
for (const group of [...pendingAutomaticAnimationGroupAttachments]) {
|
|
2343
|
+
if (!scene.animationGroups.includes(group) || automaticActivityCandidateBindings.size === 0) {
|
|
2344
|
+
pendingAutomaticAnimationGroupAttachments.delete(group);
|
|
2345
|
+
continue;
|
|
2346
|
+
}
|
|
2347
|
+
try {
|
|
2348
|
+
attachAutomaticAnimationGroup(group);
|
|
2349
|
+
pendingAutomaticAnimationGroupAttachments.delete(group);
|
|
2350
|
+
}
|
|
2351
|
+
catch {
|
|
2352
|
+
// Retain ownership in the queue; the post-flush wake predicate retries.
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
};
|
|
2356
|
+
const flushPendingAutomaticAnimationGroupReconciliations = () => {
|
|
2357
|
+
// A candidate transition owns source acquisition until it either commits
|
|
2358
|
+
// or rolls back. Rebinding an individual group against the previously
|
|
2359
|
+
// committed view here would recreate the stale source set mid-transaction.
|
|
2360
|
+
if (pendingAutomaticActivityCandidateTransition)
|
|
2361
|
+
return;
|
|
2362
|
+
for (const binding of [...pendingAutomaticAnimationGroupReconciliations]) {
|
|
2363
|
+
if (!binding.ready || !automaticAnimationGroups.has(binding.group)) {
|
|
2364
|
+
pendingAutomaticAnimationGroupReconciliations.delete(binding);
|
|
2365
|
+
continue;
|
|
2366
|
+
}
|
|
2367
|
+
safelyReconcileAutomaticAnimationGroup(binding);
|
|
2368
|
+
}
|
|
2369
|
+
};
|
|
2370
|
+
const cleanupAutomaticAnimationGroupSources = (binding) => {
|
|
2371
|
+
const cleanupErrors = [];
|
|
2372
|
+
binding.active = false;
|
|
2373
|
+
for (const [key, source] of [...binding.sources.entries()].reverse()) {
|
|
2374
|
+
try {
|
|
2375
|
+
source.controller.setActive(false);
|
|
2376
|
+
}
|
|
2377
|
+
catch (error) {
|
|
2378
|
+
cleanupErrors.push(error);
|
|
2379
|
+
}
|
|
2380
|
+
try {
|
|
2381
|
+
source.controller.dispose();
|
|
2382
|
+
binding.sources.delete(key);
|
|
2383
|
+
}
|
|
2384
|
+
catch (error) {
|
|
2385
|
+
cleanupErrors.push(error);
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
for (const [key, source] of [...binding.stagedSources.entries()].reverse()) {
|
|
2389
|
+
try {
|
|
2390
|
+
source.controller.setActive(false);
|
|
2391
|
+
}
|
|
2392
|
+
catch (error) {
|
|
2393
|
+
cleanupErrors.push(error);
|
|
2394
|
+
}
|
|
2395
|
+
try {
|
|
2396
|
+
source.controller.dispose();
|
|
2397
|
+
binding.stagedSources.delete(key);
|
|
2398
|
+
}
|
|
2399
|
+
catch (error) {
|
|
2400
|
+
cleanupErrors.push(error);
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
return Object.freeze(cleanupErrors);
|
|
2404
|
+
};
|
|
2405
|
+
const enterAutomaticAnimationGroupDormant = (binding) => {
|
|
2406
|
+
const cleanupErrors = cleanupAutomaticAnimationGroupSources(binding);
|
|
2407
|
+
binding.resolvedCandidateRevision = automaticActivityCandidateRevision;
|
|
2408
|
+
if (cleanupErrors.length === 0 && binding.sources.size === 0 && binding.stagedSources.size === 0) {
|
|
2409
|
+
binding.rebindPending = false;
|
|
2410
|
+
pendingAutomaticAnimationGroupSourceCleanups.delete(binding);
|
|
2411
|
+
return;
|
|
2412
|
+
}
|
|
2413
|
+
binding.rebindPending = true;
|
|
2414
|
+
pendingAutomaticAnimationGroupSourceCleanups.add(binding);
|
|
2415
|
+
rememberDiagnostic('shadowMapExperiment.automaticActivityCleanupFailed', `AnimationGroup ${binding.group.name || binding.group.uniqueId} activity cleanup failed.`);
|
|
2416
|
+
};
|
|
2417
|
+
const flushPendingAutomaticAnimationGroupSourceCleanups = () => {
|
|
2418
|
+
if (pendingAutomaticAnimationGroupSourceCleanups.size === 0)
|
|
2419
|
+
return;
|
|
2420
|
+
for (const binding of [...pendingAutomaticAnimationGroupSourceCleanups]) {
|
|
2421
|
+
const cleanupErrors = cleanupAutomaticAnimationGroupSources(binding);
|
|
2422
|
+
if (cleanupErrors.length > 0) {
|
|
2423
|
+
rememberDiagnostic('shadowMapExperiment.automaticActivityCleanupFailed', `AnimationGroup ${binding.group.name || binding.group.uniqueId} activity cleanup failed.`);
|
|
2424
|
+
}
|
|
2425
|
+
else if (binding.sources.size === 0 && binding.stagedSources.size === 0) {
|
|
2426
|
+
if (!pendingAutomaticActivityCandidateTransition)
|
|
2427
|
+
binding.rebindPending = false;
|
|
2428
|
+
pendingAutomaticAnimationGroupSourceCleanups.delete(binding);
|
|
2429
|
+
if (!pendingAutomaticActivityCandidateTransition
|
|
2430
|
+
&& automaticActivityCandidateBindings.size > 0
|
|
2431
|
+
&& binding.ready) {
|
|
2432
|
+
safelyReconcileAutomaticAnimationGroup(binding);
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
if (pendingAutomaticAnimationGroupSourceCleanups.size > 0) {
|
|
2437
|
+
requestRefreshFrame('shadow-map-experiment-automatic-activity-cleanup-retry');
|
|
2438
|
+
}
|
|
2439
|
+
};
|
|
2440
|
+
const haveSameAutomaticActivityCandidateView = (left, right) => left.size === right.size && [...left].every(([mesh, binding]) => {
|
|
2441
|
+
const other = right.get(mesh);
|
|
2442
|
+
return other?.entityId === binding.entityId
|
|
2443
|
+
&& other.renderableId === binding.renderableId;
|
|
2444
|
+
});
|
|
2445
|
+
applyAutomaticActivityCandidateTransition = (nextCandidates) => {
|
|
2446
|
+
if (haveSameAutomaticActivityCandidateView(automaticActivityCandidateBindings, nextCandidates)) {
|
|
2447
|
+
pendingAutomaticActivityCandidateTransition = null;
|
|
2448
|
+
return true;
|
|
2449
|
+
}
|
|
2450
|
+
if (suppressAutomaticActivityCandidateRevision) {
|
|
2451
|
+
// A full backend rebuild whose authored candidate signature is unchanged
|
|
2452
|
+
// preserves the already-owned automatic sources. Only replace the
|
|
2453
|
+
// equivalent mesh-binding view; there is no topology publication.
|
|
2454
|
+
automaticActivityCandidateBindings.clear();
|
|
2455
|
+
for (const [mesh, binding] of nextCandidates) {
|
|
2456
|
+
automaticActivityCandidateBindings.set(mesh, binding);
|
|
2457
|
+
}
|
|
2458
|
+
pendingAutomaticActivityCandidateTransition = null;
|
|
2459
|
+
return true;
|
|
2460
|
+
}
|
|
2461
|
+
pendingAutomaticActivityCandidateTransition = new Map(nextCandidates);
|
|
2462
|
+
if (pendingAutomaticAnimationGroupDetachCleanups.size > 0) {
|
|
2463
|
+
requestRefreshFrame('shadow-map-experiment-automatic-activity-detach-retry');
|
|
2464
|
+
return false;
|
|
2465
|
+
}
|
|
2466
|
+
if (pendingAutomaticAnimationGroupSourceCleanups.size > 0) {
|
|
2467
|
+
flushPendingAutomaticAnimationGroupSourceCleanups();
|
|
2468
|
+
if (pendingAutomaticAnimationGroupSourceCleanups.size > 0) {
|
|
2469
|
+
requestRefreshFrame('shadow-map-experiment-automatic-activity-cleanup-retry');
|
|
2470
|
+
return false;
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
if (nextCandidates.size > 0) {
|
|
2474
|
+
for (const group of scene.animationGroups) {
|
|
2475
|
+
try {
|
|
2476
|
+
// The proposed candidate view owns initial source resolution. Only
|
|
2477
|
+
// acquire the lifecycle observers here; resolving against the old
|
|
2478
|
+
// committed view would publish a stale source set mid-transition.
|
|
2479
|
+
attachAutomaticAnimationGroup(group, true);
|
|
2480
|
+
}
|
|
2481
|
+
catch (error) {
|
|
2482
|
+
pendingAutomaticAnimationGroupAttachments.add(group);
|
|
2483
|
+
requestRefreshFrame('shadow-map-experiment-automatic-activity-attach-retry');
|
|
2484
|
+
rememberDiagnostic('shadowMapExperiment.automaticActivityObserverFailed', `AnimationGroup ${group.name || group.uniqueId} could not be observed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2485
|
+
return false;
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
const affectedBindings = collectAutomaticAnimationGroupsAffectedByCandidateTransition(nextCandidates);
|
|
2490
|
+
const preparedBindings = [];
|
|
2491
|
+
try {
|
|
2492
|
+
for (const binding of affectedBindings) {
|
|
2493
|
+
prepareAutomaticAnimationGroupRebind(binding, nextCandidates, true);
|
|
2494
|
+
preparedBindings.push(binding);
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
catch (error) {
|
|
2498
|
+
for (const binding of preparedBindings) {
|
|
2499
|
+
cleanupStagedAutomaticAnimationGroupSources(binding);
|
|
2500
|
+
}
|
|
2501
|
+
requestRefreshFrame('shadow-map-experiment-automatic-activity-reconcile-retry');
|
|
2502
|
+
rememberDiagnostic('shadowMapExperiment.automaticActivityBindingFailed', `Automatic candidate transition could not prepare activity sources: ${error instanceof Error ? error.message : String(error)}`);
|
|
2503
|
+
return false;
|
|
2504
|
+
}
|
|
2505
|
+
const nextRevision = automaticActivityCandidateRevision + 1;
|
|
2506
|
+
automaticActivityCandidateBindings.clear();
|
|
2507
|
+
for (const [mesh, binding] of nextCandidates) {
|
|
2508
|
+
automaticActivityCandidateBindings.set(mesh, binding);
|
|
2509
|
+
}
|
|
2510
|
+
automaticActivityCandidateRevision = nextRevision;
|
|
2511
|
+
for (const binding of preparedBindings) {
|
|
2512
|
+
commitPreparedAutomaticAnimationGroupRebind(binding, nextRevision);
|
|
2513
|
+
try {
|
|
2514
|
+
syncAutomaticAnimationGroupPlayback(binding);
|
|
2515
|
+
pendingAutomaticAnimationGroupReconciliations.delete(binding);
|
|
2516
|
+
}
|
|
2517
|
+
catch (error) {
|
|
2518
|
+
pendingAutomaticAnimationGroupReconciliations.add(binding);
|
|
2519
|
+
requestRefreshFrame('shadow-map-experiment-automatic-activity-reconcile-retry');
|
|
2520
|
+
rememberDiagnostic('shadowMapExperiment.automaticActivityBindingFailed', `AnimationGroup ${binding.group.name || binding.group.uniqueId} could not activate committed shadow activity: ${error instanceof Error ? error.message : String(error)}`);
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
for (const binding of automaticAnimationGroups.values()) {
|
|
2524
|
+
if (binding.ready && !affectedBindings.has(binding)) {
|
|
2525
|
+
binding.resolvedCandidateRevision = nextRevision;
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
pendingAutomaticActivityCandidateTransition = null;
|
|
2529
|
+
return true;
|
|
2530
|
+
};
|
|
2531
|
+
flushPendingAutomaticActivityCandidateTransition = () => {
|
|
2532
|
+
if (!pendingAutomaticActivityCandidateTransition)
|
|
2533
|
+
return;
|
|
2534
|
+
applyAutomaticActivityCandidateTransition(new Map(pendingAutomaticActivityCandidateTransition));
|
|
2535
|
+
};
|
|
2536
|
+
const reconcileAutomaticAnimationGroups = () => {
|
|
2537
|
+
if (automaticActivityCandidateBindings.size === 0) {
|
|
2538
|
+
pendingAutomaticAnimationGroupAttachments.clear();
|
|
2539
|
+
pendingAutomaticAnimationGroupReconciliations.clear();
|
|
2540
|
+
for (const binding of automaticAnimationGroups.values())
|
|
2541
|
+
enterAutomaticAnimationGroupDormant(binding);
|
|
2542
|
+
if (pendingAutomaticAnimationGroupSourceCleanups.size > 0) {
|
|
2543
|
+
requestRefreshFrame('shadow-map-experiment-automatic-activity-cleanup-retry');
|
|
2544
|
+
}
|
|
2545
|
+
return pendingAutomaticAnimationGroupSourceCleanups.size === 0;
|
|
2546
|
+
}
|
|
2547
|
+
// A candidate can recover while an earlier dormant cleanup is still
|
|
2548
|
+
// retrying. Finish that cleanup before acquiring a new observer set.
|
|
2549
|
+
flushPendingAutomaticAnimationGroupSourceCleanups();
|
|
2550
|
+
for (const group of scene.animationGroups) {
|
|
2551
|
+
try {
|
|
2552
|
+
attachAutomaticAnimationGroup(group);
|
|
2553
|
+
}
|
|
2554
|
+
catch (error) {
|
|
2555
|
+
pendingAutomaticAnimationGroupAttachments.add(group);
|
|
2556
|
+
requestRefreshFrame('shadow-map-experiment-automatic-activity-attach-retry');
|
|
2557
|
+
rememberDiagnostic('shadowMapExperiment.automaticActivityObserverFailed', `AnimationGroup ${group.name || group.uniqueId} could not be observed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
let committed = true;
|
|
2561
|
+
for (const binding of automaticAnimationGroups.values()) {
|
|
2562
|
+
if (!pendingAutomaticAnimationGroupSourceCleanups.has(binding)) {
|
|
2563
|
+
committed = safelyReconcileAutomaticAnimationGroup(binding) && committed;
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
return committed && pendingAutomaticAnimationGroupSourceCleanups.size === 0;
|
|
2567
|
+
};
|
|
2568
|
+
const applyPlanInPlace = (previousPlan, nextPlan, runtimeOverlayMeshes = null, previousCandidateObjects = previousPlan.objects) => {
|
|
2569
|
+
if (!registry || !generator || !depthBackend)
|
|
2570
|
+
throw new Error('shadowMapExperiment.backendUnavailable');
|
|
2571
|
+
const delta = diffShadowMapExperimentPlans(previousPlan, nextPlan);
|
|
2572
|
+
const appearanceUpdate = delta.appearanceChanged
|
|
2573
|
+
? depthBackend.setAppearance(resolveShadowMapExperimentAppearance(nextPlan.generator))
|
|
2574
|
+
: null;
|
|
2575
|
+
plan = nextPlan;
|
|
2576
|
+
if (delta.appearanceChanged) {
|
|
2577
|
+
requestRefreshFrame('shadow-map-experiment-appearance-changed');
|
|
2578
|
+
if (appearanceUpdate?.compositeRefreshRequired)
|
|
2579
|
+
markCompositeDepthDirty('quality-changed');
|
|
2580
|
+
}
|
|
2581
|
+
if (delta.appearanceOnly)
|
|
2582
|
+
return;
|
|
2583
|
+
if (delta.lightDirectionChanged && lightState) {
|
|
2584
|
+
configureLight(lightState.light, nextPlan, {
|
|
2585
|
+
center: effectiveCoverageCenter ?? nextPlan.generator.bounds.center,
|
|
2586
|
+
extent: effectiveCoverageExtent ?? nextPlan.generator.bounds.extent,
|
|
2587
|
+
near: effectiveCoverageNear ?? nextPlan.generator.bounds.near,
|
|
2588
|
+
far: effectiveCoverageFar ?? nextPlan.generator.bounds.far,
|
|
2589
|
+
});
|
|
2590
|
+
influenceCulling.configure(nextPlan.culling.guardBandRatio);
|
|
2591
|
+
pendingCoverageMembershipRebuild = nextPlan.culling.enabled;
|
|
2592
|
+
cullingStable = !nextPlan.culling.enabled;
|
|
2593
|
+
lastCullingInvalidationReason = 'light-direction-changed';
|
|
2594
|
+
}
|
|
2595
|
+
objectByRenderable.clear();
|
|
2596
|
+
objectByEntity.clear();
|
|
2597
|
+
for (const object of nextPlan.objects) {
|
|
2598
|
+
objectByEntity.set(object.entityId, object);
|
|
2599
|
+
for (const renderableId of object.renderableIds)
|
|
2600
|
+
objectByRenderable.set(renderableId, object);
|
|
2601
|
+
}
|
|
2602
|
+
activityScheduler.applyPlan(nextPlan);
|
|
2603
|
+
registry.applyPlan(nextPlan);
|
|
2604
|
+
invalidateSceneFitBounds();
|
|
2605
|
+
stagingPlanCandidateTransitions = true;
|
|
2606
|
+
try {
|
|
2607
|
+
const bindingsToReconcile = runtimeOverlayMeshes
|
|
2608
|
+
? [...runtimeOverlayMeshes].flatMap(mesh => {
|
|
2609
|
+
const binding = bindings.get(mesh);
|
|
2610
|
+
return binding ? [[mesh, binding]] : [];
|
|
2611
|
+
})
|
|
2612
|
+
: [...bindings];
|
|
2613
|
+
for (const [mesh, binding] of bindingsToReconcile) {
|
|
2614
|
+
const resolvedId = resolveRenderableId(mesh, objectByRenderable);
|
|
2615
|
+
const resolvedObject = resolvedId ? objectByRenderable.get(resolvedId) : null;
|
|
2616
|
+
const resolvedEntityId = resolvedObject?.entityId ?? null;
|
|
2617
|
+
if (resolvedId !== binding.renderableId
|
|
2618
|
+
|| resolvedEntityId !== binding.entityId
|
|
2619
|
+
|| (!resolvedObject?.cast && !resolvedObject?.receive))
|
|
2620
|
+
unregisterMesh(mesh);
|
|
2621
|
+
else {
|
|
2622
|
+
syncMatrixObserver(binding);
|
|
2623
|
+
syncRenderable(binding.renderableId);
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
for (const mesh of runtimeOverlayMeshes ?? scene.meshes)
|
|
2627
|
+
registerMesh(mesh);
|
|
2628
|
+
}
|
|
2629
|
+
finally {
|
|
2630
|
+
stagingPlanCandidateTransitions = false;
|
|
2631
|
+
}
|
|
2632
|
+
if (delta.lightDirectionChanged) {
|
|
2633
|
+
syncSceneFitCoverage(true);
|
|
2634
|
+
updateCameraInfluenceCoverage();
|
|
2635
|
+
}
|
|
2636
|
+
if (!haveSameAutomaticActivityCandidates(previousPlan, nextPlan, previousCandidateObjects)) {
|
|
2637
|
+
const nextCandidates = new Map();
|
|
2638
|
+
for (const binding of bindings.values()) {
|
|
2639
|
+
updateAutomaticActivityCandidateBinding(nextCandidates, binding, objectByRenderable.get(binding.renderableId));
|
|
2640
|
+
}
|
|
2641
|
+
if (!applyAutomaticActivityCandidateTransition(nextCandidates)) {
|
|
2642
|
+
throw new Error('shadowMapExperiment.automaticActivityCandidateCommitFailed');
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
for (const reason of delta.dirtyReasons) {
|
|
2646
|
+
if (reason === 'quality-changed'
|
|
2647
|
+
&& delta.appearanceChanged
|
|
2648
|
+
&& !delta.generatorResourcesChanged)
|
|
2649
|
+
continue;
|
|
2650
|
+
registry.markDirty(reason);
|
|
2651
|
+
enqueueRefresh(reason);
|
|
2652
|
+
}
|
|
2653
|
+
};
|
|
2654
|
+
const flushPendingPlanRollbackRecovery = () => {
|
|
2655
|
+
const recovery = pendingPlanRollbackRecovery;
|
|
2656
|
+
if (!recovery)
|
|
2657
|
+
return;
|
|
2658
|
+
if (recovery.phase === 'deactivate') {
|
|
2659
|
+
try {
|
|
2660
|
+
deactivate();
|
|
2661
|
+
}
|
|
2662
|
+
catch { /* queue ownership remains for the next frame */ }
|
|
2663
|
+
if (hasOwnedBackendState()) {
|
|
2664
|
+
requestRefreshFrame('shadow-map-experiment-plan-rollback-recovery');
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
if (!recovery.targetPlan?.enabled) {
|
|
2668
|
+
pendingPlanRollbackRecovery = null;
|
|
2669
|
+
return;
|
|
2670
|
+
}
|
|
2671
|
+
recovery.phase = 'activate';
|
|
2672
|
+
}
|
|
2673
|
+
const targetPlan = recovery.targetPlan;
|
|
2674
|
+
if (!targetPlan)
|
|
2675
|
+
return;
|
|
2676
|
+
try {
|
|
2677
|
+
if (!recovery.activated) {
|
|
2678
|
+
// `plan` and the scheduler intentionally remain null until activation
|
|
2679
|
+
// and automatic candidate/source publication have both completed.
|
|
2680
|
+
activityScheduler.applyPlan(targetPlan);
|
|
2681
|
+
activatingRecoveryPlan = targetPlan;
|
|
2682
|
+
allowingRecoveryActivityRegistration = true;
|
|
2683
|
+
try {
|
|
2684
|
+
activate(targetPlan);
|
|
2685
|
+
}
|
|
2686
|
+
finally {
|
|
2687
|
+
activatingRecoveryPlan = null;
|
|
2688
|
+
}
|
|
2689
|
+
recovery.activated = true;
|
|
2690
|
+
}
|
|
2691
|
+
allowingRecoveryActivityRegistration = true;
|
|
2692
|
+
try {
|
|
2693
|
+
flushPendingAutomaticAnimationGroupDetachCleanups();
|
|
2694
|
+
flushPendingAutomaticAnimationGroupSourceCleanups();
|
|
2695
|
+
flushPendingAutomaticActivityCandidateTransition();
|
|
2696
|
+
flushPendingAutomaticAnimationGroupAttachments();
|
|
2697
|
+
flushPendingAutomaticAnimationGroupReconciliations();
|
|
2698
|
+
}
|
|
2699
|
+
finally {
|
|
2700
|
+
allowingRecoveryActivityRegistration = false;
|
|
2701
|
+
}
|
|
2702
|
+
if (pendingAutomaticActivityCandidateTransition
|
|
2703
|
+
|| pendingAutomaticAnimationGroupSourceCleanups.size > 0
|
|
2704
|
+
|| pendingAutomaticAnimationGroupDetachCleanups.size > 0
|
|
2705
|
+
|| pendingAutomaticAnimationGroupAttachments.size > 0
|
|
2706
|
+
|| pendingAutomaticAnimationGroupReconciliations.size > 0) {
|
|
2707
|
+
requestRefreshFrame('shadow-map-experiment-plan-rollback-recovery');
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
for (const entityId of recovery.activeEntityIds) {
|
|
2711
|
+
activityScheduler.setEntityActive(entityId, true);
|
|
2712
|
+
syncActivityEligibility(entityId);
|
|
2713
|
+
}
|
|
2714
|
+
plan = targetPlan;
|
|
2715
|
+
automaticActivityCandidateRevision = recovery.candidateRevision;
|
|
2716
|
+
for (const binding of automaticAnimationGroups.values()) {
|
|
2717
|
+
binding.resolvedCandidateRevision = recovery.candidateRevision;
|
|
2718
|
+
}
|
|
2719
|
+
pendingPlanRollbackRecovery = null;
|
|
2720
|
+
}
|
|
2721
|
+
catch {
|
|
2722
|
+
allowingRecoveryActivityRegistration = false;
|
|
2723
|
+
plan = null;
|
|
2724
|
+
activityScheduler.applyPlan(null);
|
|
2725
|
+
recovery.phase = 'deactivate';
|
|
2726
|
+
recovery.activated = false;
|
|
2727
|
+
try {
|
|
2728
|
+
deactivate();
|
|
2729
|
+
}
|
|
2730
|
+
catch { /* retained for bounded retry */ }
|
|
2731
|
+
requestRefreshFrame('shadow-map-experiment-plan-rollback-recovery');
|
|
2732
|
+
}
|
|
2733
|
+
};
|
|
2734
|
+
const rollbackPlanUpdate = (previousPlan, error, previousCandidateRevision = automaticActivityCandidateRevision) => {
|
|
2735
|
+
let cleanupError = null;
|
|
2736
|
+
try {
|
|
2737
|
+
deactivate();
|
|
2738
|
+
}
|
|
2739
|
+
catch (deactivationError) {
|
|
2740
|
+
cleanupError = deactivationError;
|
|
2741
|
+
}
|
|
2742
|
+
plan = null;
|
|
2743
|
+
activityScheduler.applyPlan(null);
|
|
2744
|
+
automaticActivityCandidateRevision = previousCandidateRevision;
|
|
2745
|
+
if (cleanupError || hasOwnedBackendState()) {
|
|
2746
|
+
pendingPlanRollbackRecovery = {
|
|
2747
|
+
targetPlan: previousPlan,
|
|
2748
|
+
candidateRevision: previousCandidateRevision,
|
|
2749
|
+
activeEntityIds: new Set(activeCasterEntityIds),
|
|
2750
|
+
phase: 'deactivate',
|
|
2751
|
+
activated: false,
|
|
2752
|
+
};
|
|
2753
|
+
requestRefreshFrame('shadow-map-experiment-plan-rollback-recovery');
|
|
2754
|
+
if (cleanupError && error && typeof error === 'object') {
|
|
2755
|
+
attachFailureMetadata(error, { rollbackDeactivationErrors: Object.freeze([cleanupError]) });
|
|
2756
|
+
}
|
|
2757
|
+
throw error;
|
|
2758
|
+
}
|
|
2759
|
+
if (previousPlan?.enabled) {
|
|
2760
|
+
pendingPlanRollbackRecovery = {
|
|
2761
|
+
targetPlan: previousPlan,
|
|
2762
|
+
candidateRevision: previousCandidateRevision,
|
|
2763
|
+
activeEntityIds: new Set(activeCasterEntityIds),
|
|
2764
|
+
phase: 'activate',
|
|
2765
|
+
activated: false,
|
|
2766
|
+
};
|
|
2767
|
+
requestRefreshFrame('shadow-map-experiment-plan-rollback-recovery');
|
|
2768
|
+
}
|
|
2769
|
+
else {
|
|
2770
|
+
automaticActivityCandidateRevision = previousCandidateRevision;
|
|
2771
|
+
}
|
|
2772
|
+
throw error;
|
|
2773
|
+
};
|
|
2774
|
+
const hasOwnedBackendState = () => bindings.size > 0
|
|
2775
|
+
|| pendingAutomaticActivityCandidateTransition !== null
|
|
2776
|
+
|| pendingMeshObserverCleanups.size > 0
|
|
2777
|
+
|| pendingMeshObserverRestorations.size > 0
|
|
2778
|
+
|| pendingMeshRegistrationRollbacks.size > 0
|
|
2779
|
+
|| pendingSceneMeshRegistrations.size > 0
|
|
2780
|
+
|| pendingMeshUnregistrations.size > 0
|
|
2781
|
+
|| pendingRenderableMembershipRetries.size > 0
|
|
2782
|
+
|| pendingTransformMembershipRetries.size > 0
|
|
2783
|
+
|| receiverStates.size > 0
|
|
2784
|
+
|| pendingReceiverRestorations.size > 0
|
|
2785
|
+
|| registry !== null
|
|
2786
|
+
|| depthBackend !== null
|
|
2787
|
+
|| pendingBackendAllocationCleanup !== null
|
|
2788
|
+
|| generator !== null
|
|
2789
|
+
|| lightState !== null
|
|
2790
|
+
|| previousSceneShadowsEnabled !== null;
|
|
2791
|
+
const applyEffectivePlan = (value, allowDisposing = false, runtimeOverlayMeshes = null) => {
|
|
2792
|
+
assertUsable(allowDisposing);
|
|
2793
|
+
// Every caller supplies either a source snapshot or composeEffectivePlan()
|
|
2794
|
+
// output. Re-cloning the full Plan for each runtime entity made a batch of
|
|
2795
|
+
// synchronous overlay additions scale with the complete authored scene.
|
|
2796
|
+
const nextPlan = value;
|
|
2797
|
+
const previousPlan = plan;
|
|
2798
|
+
const previousRuntimeOverlayObjects = [...runtimeRegistrations.values()]
|
|
2799
|
+
.filter(record => !record.disposed)
|
|
2800
|
+
.flatMap(record => {
|
|
2801
|
+
const object = objectByEntity.get(record.entityId);
|
|
2802
|
+
return object ? [object] : [];
|
|
2803
|
+
});
|
|
2804
|
+
// `plan.objects` may be a lazy operational snapshot (missing active
|
|
2805
|
+
// overlays) or a previously materialized effective Plan (retaining a
|
|
2806
|
+
// disposed overlay). Rebuild only the candidate topology from the
|
|
2807
|
+
// authored source and live overlays, never from that mixed snapshot.
|
|
2808
|
+
const previousCandidateObjects = sourcePlan
|
|
2809
|
+
? [...sourcePlan.objects, ...previousRuntimeOverlayObjects]
|
|
2810
|
+
: previousPlan?.objects ?? [];
|
|
2811
|
+
const previousCandidateRevision = automaticActivityCandidateRevision;
|
|
2812
|
+
if (previousPlan?.enabled && nextPlan?.enabled) {
|
|
2813
|
+
const delta = diffShadowMapExperimentPlans(previousPlan, nextPlan);
|
|
2814
|
+
if (!delta.generatorResourcesChanged) {
|
|
2815
|
+
try {
|
|
2816
|
+
applyPlanInPlace(previousPlan, nextPlan, runtimeOverlayMeshes, previousCandidateObjects);
|
|
2817
|
+
return;
|
|
2818
|
+
}
|
|
2819
|
+
catch (error) {
|
|
2820
|
+
rollbackPlanUpdate(previousPlan, error, previousCandidateRevision);
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
const preserveAutomaticCandidates = previousPlan?.enabled === true
|
|
2825
|
+
&& nextPlan?.enabled === true
|
|
2826
|
+
&& haveSameAutomaticActivityCandidates(previousPlan, nextPlan, previousCandidateObjects);
|
|
2827
|
+
suppressAutomaticActivityCandidateRevision = preserveAutomaticCandidates;
|
|
2828
|
+
try {
|
|
2829
|
+
deactivate();
|
|
2830
|
+
plan = nextPlan;
|
|
2831
|
+
activityScheduler.applyPlan(nextPlan?.enabled ? nextPlan : null);
|
|
2832
|
+
if (!nextPlan?.enabled) {
|
|
2833
|
+
reconcileAutomaticAnimationGroups();
|
|
2834
|
+
return;
|
|
2835
|
+
}
|
|
2836
|
+
try {
|
|
2837
|
+
activate(nextPlan);
|
|
2838
|
+
}
|
|
2839
|
+
catch (error) {
|
|
2840
|
+
rollbackPlanUpdate(previousPlan, error, previousCandidateRevision);
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
finally {
|
|
2844
|
+
suppressAutomaticActivityCandidateRevision = false;
|
|
2845
|
+
}
|
|
2846
|
+
};
|
|
2847
|
+
const setPlan = (value) => {
|
|
2848
|
+
assertUsable();
|
|
2849
|
+
assertNoPlanRollbackRecovery();
|
|
2850
|
+
const nextSourcePlan = value
|
|
2851
|
+
? deepFreezeShadowMapExperimentPlan(createShadowMapExperimentPlanSnapshot(value))
|
|
2852
|
+
: null;
|
|
2853
|
+
const records = [...runtimeRegistrations.values()].filter(record => !record.disposed);
|
|
2854
|
+
const nextEffectivePlan = composeEffectivePlan(nextSourcePlan, records, runtimeOverlayRevision);
|
|
2855
|
+
const previousSourcePlan = sourcePlan;
|
|
2856
|
+
const previousMaterializedEffectivePlan = materializedEffectivePlan;
|
|
2857
|
+
try {
|
|
2858
|
+
applyEffectivePlan(nextEffectivePlan);
|
|
2859
|
+
sourcePlan = nextSourcePlan;
|
|
2860
|
+
materializedEffectivePlan = nextEffectivePlan;
|
|
2861
|
+
}
|
|
2862
|
+
catch (error) {
|
|
2863
|
+
sourcePlan = previousSourcePlan;
|
|
2864
|
+
materializedEffectivePlan = previousMaterializedEffectivePlan;
|
|
2865
|
+
throw error;
|
|
2866
|
+
}
|
|
2867
|
+
};
|
|
2868
|
+
const previewLightDirection = (entityId, direction) => {
|
|
2869
|
+
assertUsable();
|
|
2870
|
+
assertNoPlanRollbackRecovery();
|
|
2871
|
+
if (!sourcePlan?.enabled
|
|
2872
|
+
|| !plan?.enabled
|
|
2873
|
+
|| !registry
|
|
2874
|
+
|| !lightState
|
|
2875
|
+
|| sourcePlan.light.entityId !== entityId)
|
|
2876
|
+
return false;
|
|
2877
|
+
if (areTuple3Equal(sourcePlan.light.direction, direction))
|
|
2878
|
+
return true;
|
|
2879
|
+
const nextLight = Object.freeze({
|
|
2880
|
+
...sourcePlan.light,
|
|
2881
|
+
direction: Object.freeze([...direction]),
|
|
2882
|
+
});
|
|
2883
|
+
const nextSourcePlan = Object.freeze({
|
|
2884
|
+
...sourcePlan,
|
|
2885
|
+
light: nextLight,
|
|
2886
|
+
});
|
|
2887
|
+
const nextOperationalPlan = Object.freeze({ ...plan, light: nextLight });
|
|
2888
|
+
const nextMaterializedEffectivePlan = materializedEffectivePlan
|
|
2889
|
+
? Object.freeze({ ...materializedEffectivePlan, light: nextLight })
|
|
2890
|
+
: null;
|
|
2891
|
+
const previousPlan = plan;
|
|
2892
|
+
const previousCandidateRevision = automaticActivityCandidateRevision;
|
|
2893
|
+
const previousLightState = captureLight(lightState.light);
|
|
2894
|
+
try {
|
|
2895
|
+
configureLight(lightState.light, nextOperationalPlan, {
|
|
2896
|
+
center: effectiveCoverageCenter ?? nextOperationalPlan.generator.bounds.center,
|
|
2897
|
+
extent: effectiveCoverageExtent ?? nextOperationalPlan.generator.bounds.extent,
|
|
2898
|
+
near: effectiveCoverageNear ?? nextOperationalPlan.generator.bounds.near,
|
|
2899
|
+
far: effectiveCoverageFar ?? nextOperationalPlan.generator.bounds.far,
|
|
2900
|
+
});
|
|
2901
|
+
}
|
|
2902
|
+
catch (error) {
|
|
2903
|
+
try {
|
|
2904
|
+
restoreLight(previousLightState);
|
|
2905
|
+
}
|
|
2906
|
+
catch (rollbackError) {
|
|
2907
|
+
if (error && typeof error === 'object') {
|
|
2908
|
+
attachFailureMetadata(error, {
|
|
2909
|
+
lightPreviewRollbackErrors: Object.freeze([rollbackError]),
|
|
2910
|
+
});
|
|
2911
|
+
}
|
|
2912
|
+
rollbackPlanUpdate(previousPlan, error, previousCandidateRevision);
|
|
2913
|
+
}
|
|
2914
|
+
throw error;
|
|
2915
|
+
}
|
|
2916
|
+
sourcePlan = nextSourcePlan;
|
|
2917
|
+
plan = nextOperationalPlan;
|
|
2918
|
+
materializedEffectivePlan = nextMaterializedEffectivePlan;
|
|
2919
|
+
influenceCulling.configure(nextOperationalPlan.culling.guardBandRatio);
|
|
2920
|
+
pendingCoverageMembershipRebuild = nextOperationalPlan.culling.enabled;
|
|
2921
|
+
cullingStable = !nextOperationalPlan.culling.enabled;
|
|
2922
|
+
lastCullingInvalidationReason = 'light-direction-changed';
|
|
2923
|
+
registry.markDirty('light-direction-changed');
|
|
2924
|
+
enqueueRefresh('light-direction-changed');
|
|
2925
|
+
return true;
|
|
2926
|
+
};
|
|
2927
|
+
const previewAppearance = (value) => {
|
|
2928
|
+
assertUsable();
|
|
2929
|
+
assertNoPlanRollbackRecovery();
|
|
2930
|
+
if (!sourcePlan?.enabled || !plan?.enabled || !depthBackend)
|
|
2931
|
+
return false;
|
|
2932
|
+
const nextAppearance = resolveShadowMapExperimentAppearance(value);
|
|
2933
|
+
const currentAppearance = resolveShadowMapExperimentAppearance(sourcePlan.generator);
|
|
2934
|
+
if (nextAppearance.darkness === currentAppearance.darkness
|
|
2935
|
+
&& nextAppearance.softness === currentAppearance.softness)
|
|
2936
|
+
return true;
|
|
2937
|
+
setPlan(Object.freeze({
|
|
2938
|
+
...sourcePlan,
|
|
2939
|
+
generator: Object.freeze({
|
|
2940
|
+
...sourcePlan.generator,
|
|
2941
|
+
darkness: nextAppearance.darkness,
|
|
2942
|
+
softness: nextAppearance.softness,
|
|
2943
|
+
}),
|
|
2944
|
+
}));
|
|
2945
|
+
return true;
|
|
2946
|
+
};
|
|
2947
|
+
const { newMeshObserver, removedMeshObserver, frameObserver, newAnimationGroupObserver, removedAnimationGroupObserver, } = (() => {
|
|
2948
|
+
let acquiredNewMeshObserver = null;
|
|
2949
|
+
let acquiredRemovedMeshObserver = null;
|
|
2950
|
+
let acquiredFrameObserver = null;
|
|
2951
|
+
let acquiredNewAnimationGroupObserver = null;
|
|
2952
|
+
let acquiredRemovedAnimationGroupObserver = null;
|
|
2953
|
+
try {
|
|
2954
|
+
acquiredNewMeshObserver = scene.onNewMeshAddedObservable.add(mesh => {
|
|
2955
|
+
sceneOwnedMeshes.add(mesh);
|
|
2956
|
+
// Babylon announces a mesh from its constructor before every identity and
|
|
2957
|
+
// visibility field is guaranteed to be initialized. Bind after that
|
|
2958
|
+
// constructor completes; this is still an event-local operation, not a scan.
|
|
2959
|
+
if (disposed || disposing || !scene.meshes.includes(mesh))
|
|
2960
|
+
return;
|
|
2961
|
+
pendingSceneMeshRegistrations.add(mesh);
|
|
2962
|
+
requestRefreshFrame('shadow-map-experiment-mesh-registration-retry');
|
|
2963
|
+
queueMicrotask(() => {
|
|
2964
|
+
if (disposed || disposing || !pendingSceneMeshRegistrations.has(mesh))
|
|
2965
|
+
return;
|
|
2966
|
+
if (!scene.meshes.includes(mesh)) {
|
|
2967
|
+
pendingSceneMeshRegistrations.delete(mesh);
|
|
2968
|
+
return;
|
|
2969
|
+
}
|
|
2970
|
+
try {
|
|
2971
|
+
registerMesh(mesh);
|
|
2972
|
+
pendingSceneMeshRegistrations.delete(mesh);
|
|
2973
|
+
}
|
|
2974
|
+
catch {
|
|
2975
|
+
pendingSceneMeshRegistrations.add(mesh);
|
|
2976
|
+
requestRefreshFrame('shadow-map-experiment-mesh-registration-retry');
|
|
2977
|
+
}
|
|
2978
|
+
});
|
|
2979
|
+
});
|
|
2980
|
+
acquiredRemovedMeshObserver = scene.onMeshRemovedObservable.add(mesh => {
|
|
2981
|
+
sceneOwnedMeshes.delete(mesh);
|
|
2982
|
+
requestMeshUnregistration(mesh);
|
|
2983
|
+
});
|
|
2984
|
+
acquiredNewAnimationGroupObserver = scene.onNewAnimationGroupAddedObservable.add(group => {
|
|
2985
|
+
if (automaticActivityCandidateBindings.size === 0)
|
|
2986
|
+
return;
|
|
2987
|
+
try {
|
|
2988
|
+
attachAutomaticAnimationGroup(group);
|
|
2989
|
+
}
|
|
2990
|
+
catch (error) {
|
|
2991
|
+
pendingAutomaticAnimationGroupAttachments.add(group);
|
|
2992
|
+
requestRefreshFrame('shadow-map-experiment-automatic-activity-attach-retry');
|
|
2993
|
+
rememberDiagnostic('shadowMapExperiment.automaticActivityObserverFailed', `AnimationGroup ${group.name || group.uniqueId} could not be observed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2994
|
+
}
|
|
2995
|
+
});
|
|
2996
|
+
acquiredRemovedAnimationGroupObserver = scene.onAnimationGroupRemovedObservable.add(group => {
|
|
2997
|
+
const cleanupErrors = detachAutomaticAnimationGroup(group);
|
|
2998
|
+
if (cleanupErrors.length > 0) {
|
|
2999
|
+
rememberDiagnostic('shadowMapExperiment.automaticActivityCleanupFailed', `AnimationGroup ${group.name || group.uniqueId} activity cleanup failed.`);
|
|
3000
|
+
}
|
|
3001
|
+
});
|
|
3002
|
+
acquiredFrameObserver = scene.onBeforeRenderObservable.add(() => flushRefresh());
|
|
3003
|
+
if (automaticActivityCandidateBindings.size > 0) {
|
|
3004
|
+
for (const group of scene.animationGroups)
|
|
3005
|
+
attachAutomaticAnimationGroup(group);
|
|
3006
|
+
}
|
|
3007
|
+
return {
|
|
3008
|
+
newMeshObserver: acquiredNewMeshObserver,
|
|
3009
|
+
removedMeshObserver: acquiredRemovedMeshObserver,
|
|
3010
|
+
frameObserver: acquiredFrameObserver,
|
|
3011
|
+
newAnimationGroupObserver: acquiredNewAnimationGroupObserver,
|
|
3012
|
+
removedAnimationGroupObserver: acquiredRemovedAnimationGroupObserver,
|
|
3013
|
+
};
|
|
3014
|
+
}
|
|
3015
|
+
catch (error) {
|
|
3016
|
+
const cleanup = () => {
|
|
3017
|
+
const cleanupErrors = [];
|
|
3018
|
+
for (const group of [...automaticAnimationGroups.keys()].reverse()) {
|
|
3019
|
+
cleanupErrors.push(...detachAutomaticAnimationGroup(group));
|
|
3020
|
+
}
|
|
3021
|
+
if (acquiredRemovedAnimationGroupObserver) {
|
|
3022
|
+
try {
|
|
3023
|
+
scene.onAnimationGroupRemovedObservable.remove(acquiredRemovedAnimationGroupObserver);
|
|
3024
|
+
acquiredRemovedAnimationGroupObserver = null;
|
|
3025
|
+
}
|
|
3026
|
+
catch (cleanupError) {
|
|
3027
|
+
cleanupErrors.push(cleanupError);
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
if (acquiredNewAnimationGroupObserver) {
|
|
3031
|
+
try {
|
|
3032
|
+
scene.onNewAnimationGroupAddedObservable.remove(acquiredNewAnimationGroupObserver);
|
|
3033
|
+
acquiredNewAnimationGroupObserver = null;
|
|
3034
|
+
}
|
|
3035
|
+
catch (cleanupError) {
|
|
3036
|
+
cleanupErrors.push(cleanupError);
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
if (acquiredFrameObserver) {
|
|
3040
|
+
try {
|
|
3041
|
+
scene.onBeforeRenderObservable.remove(acquiredFrameObserver);
|
|
3042
|
+
acquiredFrameObserver = null;
|
|
3043
|
+
}
|
|
3044
|
+
catch (cleanupError) {
|
|
3045
|
+
cleanupErrors.push(cleanupError);
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
if (acquiredRemovedMeshObserver) {
|
|
3049
|
+
try {
|
|
3050
|
+
scene.onMeshRemovedObservable.remove(acquiredRemovedMeshObserver);
|
|
3051
|
+
acquiredRemovedMeshObserver = null;
|
|
3052
|
+
}
|
|
3053
|
+
catch (cleanupError) {
|
|
3054
|
+
cleanupErrors.push(cleanupError);
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
if (acquiredNewMeshObserver) {
|
|
3058
|
+
try {
|
|
3059
|
+
scene.onNewMeshAddedObservable.remove(acquiredNewMeshObserver);
|
|
3060
|
+
acquiredNewMeshObserver = null;
|
|
3061
|
+
}
|
|
3062
|
+
catch (cleanupError) {
|
|
3063
|
+
cleanupErrors.push(cleanupError);
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
return Object.freeze(cleanupErrors);
|
|
3067
|
+
};
|
|
3068
|
+
const cleanupErrors = cleanup();
|
|
3069
|
+
if (cleanupErrors.length > 0)
|
|
3070
|
+
throw createInitializationFailure(error, cleanup, cleanupErrors);
|
|
3071
|
+
throw error;
|
|
3072
|
+
}
|
|
3073
|
+
})();
|
|
3074
|
+
let disposeNewMeshObserverRemoved = false;
|
|
3075
|
+
let disposeRemovedMeshObserverRemoved = false;
|
|
3076
|
+
let disposeFrameObserverRemoved = false;
|
|
3077
|
+
let disposeNewAnimationGroupObserverRemoved = false;
|
|
3078
|
+
let disposeRemovedAnimationGroupObserverRemoved = false;
|
|
3079
|
+
let disposeActivitySchedulerDisposed = false;
|
|
3080
|
+
let disposeDepthRefreshSchedulerDisposed = false;
|
|
3081
|
+
let disposeInfluenceCullingDisposed = false;
|
|
3082
|
+
const disposeRuntimeRecordsForSystem = (records) => {
|
|
3083
|
+
const cleanupErrors = [];
|
|
3084
|
+
for (const record of [...records].reverse()) {
|
|
3085
|
+
for (const [sourceId, controller] of [...record.controllers.entries()].reverse()) {
|
|
3086
|
+
try {
|
|
3087
|
+
controller.setActive(false);
|
|
3088
|
+
}
|
|
3089
|
+
catch (error) {
|
|
3090
|
+
cleanupErrors.push(error);
|
|
3091
|
+
}
|
|
3092
|
+
try {
|
|
3093
|
+
controller.dispose();
|
|
3094
|
+
record.controllers.delete(sourceId);
|
|
3095
|
+
}
|
|
3096
|
+
catch (error) {
|
|
3097
|
+
cleanupErrors.push(error);
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
if (record.controllers.size === 0) {
|
|
3101
|
+
runtimeRegistrations.delete(record.entityId);
|
|
3102
|
+
for (const renderable of record.renderables)
|
|
3103
|
+
runtimeOwnedMeshes.delete(renderable.mesh);
|
|
3104
|
+
explicitRuntimeActivityEntityIds.delete(record.entityId);
|
|
3105
|
+
record.disposed = true;
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
return Object.freeze(cleanupErrors);
|
|
3109
|
+
};
|
|
3110
|
+
const applyRuntimeObjectDelta = (input) => {
|
|
3111
|
+
if (!sourcePlan || !plan?.enabled || !registry || !generator) {
|
|
3112
|
+
throw new Error('shadowMapExperiment.runtimePlanUnavailable');
|
|
3113
|
+
}
|
|
3114
|
+
runtimeIncrementalMutationCount += 1;
|
|
3115
|
+
const removedEntityIds = input.removedObjects.map(object => object.entityId);
|
|
3116
|
+
const removedRenderableIds = input.removedObjects.flatMap(object => object.renderableIds);
|
|
3117
|
+
updateOperationalPlanRevision(input.overlayRevision);
|
|
3118
|
+
for (const object of input.removedObjects) {
|
|
3119
|
+
objectByEntity.delete(object.entityId);
|
|
3120
|
+
for (const renderableId of object.renderableIds) {
|
|
3121
|
+
if (objectByRenderable.get(renderableId)?.entityId === object.entityId) {
|
|
3122
|
+
objectByRenderable.delete(renderableId);
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
for (const object of input.upsertedObjects) {
|
|
3127
|
+
objectByEntity.set(object.entityId, object);
|
|
3128
|
+
for (const renderableId of object.renderableIds)
|
|
3129
|
+
objectByRenderable.set(renderableId, object);
|
|
3130
|
+
}
|
|
3131
|
+
activityScheduler.applyObjectDelta({
|
|
3132
|
+
upsertedObjects: input.upsertedObjects,
|
|
3133
|
+
removedEntityIds,
|
|
3134
|
+
});
|
|
3135
|
+
registry.applyObjectDelta({
|
|
3136
|
+
planRevision: sourcePlan.revision + input.overlayRevision,
|
|
3137
|
+
upsertedObjects: input.upsertedObjects,
|
|
3138
|
+
removedRenderableIds,
|
|
3139
|
+
});
|
|
3140
|
+
invalidateSceneFitBounds();
|
|
3141
|
+
stagingPlanCandidateTransitions = true;
|
|
3142
|
+
try {
|
|
3143
|
+
for (const mesh of input.meshes) {
|
|
3144
|
+
const binding = bindings.get(mesh);
|
|
3145
|
+
if (!binding)
|
|
3146
|
+
continue;
|
|
3147
|
+
const resolvedId = resolveRenderableId(mesh, objectByRenderable);
|
|
3148
|
+
const resolvedObject = resolvedId ? objectByRenderable.get(resolvedId) : null;
|
|
3149
|
+
if (resolvedId !== binding.renderableId
|
|
3150
|
+
|| resolvedObject?.entityId !== binding.entityId
|
|
3151
|
+
|| (!resolvedObject.cast && !resolvedObject.receive)) {
|
|
3152
|
+
unregisterMesh(mesh);
|
|
3153
|
+
}
|
|
3154
|
+
else {
|
|
3155
|
+
syncMatrixObserver(binding);
|
|
3156
|
+
syncRenderable(binding.renderableId);
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
for (const mesh of input.meshes)
|
|
3160
|
+
registerMesh(mesh);
|
|
3161
|
+
}
|
|
3162
|
+
finally {
|
|
3163
|
+
stagingPlanCandidateTransitions = false;
|
|
3164
|
+
}
|
|
3165
|
+
const candidateTopologyChanged = [...input.upsertedObjects, ...input.removedObjects]
|
|
3166
|
+
.some(isAutomaticActivityCandidateObject);
|
|
3167
|
+
if (candidateTopologyChanged) {
|
|
3168
|
+
const nextCandidates = new Map();
|
|
3169
|
+
for (const binding of bindings.values()) {
|
|
3170
|
+
updateAutomaticActivityCandidateBinding(nextCandidates, binding, objectByRenderable.get(binding.renderableId));
|
|
3171
|
+
}
|
|
3172
|
+
if (!applyAutomaticActivityCandidateTransition(nextCandidates)) {
|
|
3173
|
+
throw new Error('shadowMapExperiment.automaticActivityCandidateCommitFailed');
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
};
|
|
3177
|
+
const createRuntimeOwner = (records) => {
|
|
3178
|
+
const owner = Object.freeze({
|
|
3179
|
+
entityIds: Object.freeze(records.map(record => record.entityId).sort()),
|
|
3180
|
+
renderableIds: Object.freeze(records.flatMap(record => (record.renderables.map(renderable => renderable.renderableId))).sort()),
|
|
3181
|
+
getBehaviorProfileId(entityId) {
|
|
3182
|
+
return records.find(record => record.entityId === entityId && !record.disposed)?.behaviorProfileId ?? null;
|
|
3183
|
+
},
|
|
3184
|
+
getActivityController(entityId, sourceId) {
|
|
3185
|
+
const record = records.find(candidate => candidate.entityId === entityId && !candidate.disposed);
|
|
3186
|
+
return record?.controllers.get(sourceId) ?? null;
|
|
3187
|
+
},
|
|
3188
|
+
dispose() {
|
|
3189
|
+
if (records.every(record => record.disposed))
|
|
3190
|
+
return;
|
|
3191
|
+
assertUsable(true);
|
|
3192
|
+
if (pendingPlanRollbackRecovery && !disposing) {
|
|
3193
|
+
throw new Error('shadowMapExperiment.planRollbackRecoveryPending');
|
|
3194
|
+
}
|
|
3195
|
+
const cleanupErrors = [];
|
|
3196
|
+
for (const record of [...records].reverse()) {
|
|
3197
|
+
for (const sourceId of [...record.controllers.keys()].reverse()) {
|
|
3198
|
+
const controller = record.controllers.get(sourceId);
|
|
3199
|
+
if (!controller)
|
|
3200
|
+
continue;
|
|
3201
|
+
try {
|
|
3202
|
+
controller.setActive(false);
|
|
3203
|
+
}
|
|
3204
|
+
catch (error) {
|
|
3205
|
+
cleanupErrors.push(error);
|
|
3206
|
+
}
|
|
3207
|
+
try {
|
|
3208
|
+
controller.dispose();
|
|
3209
|
+
record.controllers.delete(sourceId);
|
|
3210
|
+
}
|
|
3211
|
+
catch (error) {
|
|
3212
|
+
cleanupErrors.push(error);
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
const controllersDisposed = records.every(record => record.controllers.size === 0);
|
|
3217
|
+
const anyRegistered = records.some(record => runtimeRegistrations.get(record.entityId) === record);
|
|
3218
|
+
if (controllersDisposed && anyRegistered) {
|
|
3219
|
+
const nextOverlayRevision = runtimeOverlayRevision + 1;
|
|
3220
|
+
const removedObjects = records.flatMap(record => {
|
|
3221
|
+
const object = objectByEntity.get(record.entityId);
|
|
3222
|
+
return object ? [object] : [];
|
|
3223
|
+
});
|
|
3224
|
+
const runtimeMeshes = collectRuntimeRecordMeshes(records);
|
|
3225
|
+
const previousCandidateRevision = automaticActivityCandidateRevision;
|
|
3226
|
+
try {
|
|
3227
|
+
applyRuntimeObjectDelta({
|
|
3228
|
+
upsertedObjects: Object.freeze([]),
|
|
3229
|
+
removedObjects,
|
|
3230
|
+
overlayRevision: nextOverlayRevision,
|
|
3231
|
+
meshes: runtimeMeshes,
|
|
3232
|
+
});
|
|
3233
|
+
for (const record of records) {
|
|
3234
|
+
runtimeRegistrations.delete(record.entityId);
|
|
3235
|
+
for (const renderable of record.renderables)
|
|
3236
|
+
runtimeOwnedMeshes.delete(renderable.mesh);
|
|
3237
|
+
explicitRuntimeActivityEntityIds.delete(record.entityId);
|
|
3238
|
+
}
|
|
3239
|
+
runtimeOverlayRevision = nextOverlayRevision;
|
|
3240
|
+
}
|
|
3241
|
+
catch (error) {
|
|
3242
|
+
try {
|
|
3243
|
+
rollbackPlanUpdate(composeEffectivePlan(sourcePlan, [...runtimeRegistrations.values()].filter(candidate => !candidate.disposed), runtimeOverlayRevision), error, previousCandidateRevision);
|
|
3244
|
+
}
|
|
3245
|
+
catch (rollbackError) {
|
|
3246
|
+
cleanupErrors.push(rollbackError);
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
if (controllersDisposed && records.every(record => runtimeRegistrations.get(record.entityId) !== record)) {
|
|
3251
|
+
for (const record of records)
|
|
3252
|
+
record.disposed = true;
|
|
3253
|
+
}
|
|
3254
|
+
if (cleanupErrors.length > 0) {
|
|
3255
|
+
const error = Object.assign(new Error('shadowMapExperiment.runtimeOwnerDisposeFailed'), {
|
|
3256
|
+
cleanupErrors: Object.freeze(cleanupErrors),
|
|
3257
|
+
runtimeShadowObjectsRecoveryOwner: owner,
|
|
3258
|
+
});
|
|
3259
|
+
throw error;
|
|
3260
|
+
}
|
|
3261
|
+
},
|
|
3262
|
+
});
|
|
3263
|
+
for (const record of records)
|
|
3264
|
+
record.owner = owner;
|
|
3265
|
+
return owner;
|
|
3266
|
+
};
|
|
3267
|
+
const collectRuntimeRecordMeshes = (records) => {
|
|
3268
|
+
const meshes = new Set();
|
|
3269
|
+
for (const record of records) {
|
|
3270
|
+
for (const renderable of record.renderables) {
|
|
3271
|
+
meshes.add(renderable.mesh);
|
|
3272
|
+
for (const mesh of meshesByRenderable.get(renderable.renderableId) ?? [])
|
|
3273
|
+
meshes.add(mesh);
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
return meshes;
|
|
3277
|
+
};
|
|
3278
|
+
const isMeshOwnedByScene = (mesh) => {
|
|
3279
|
+
if (mesh.getScene() !== scene || isDisposed(mesh))
|
|
3280
|
+
return false;
|
|
3281
|
+
if (sceneOwnedMeshes.has(mesh))
|
|
3282
|
+
return true;
|
|
3283
|
+
// Babylon can emit a removal notification while a caller-owned Mesh still
|
|
3284
|
+
// remains in Scene.meshes. Revalidate only this cache-miss boundary and
|
|
3285
|
+
// repair the event-maintained index; normal runtime registration is O(1).
|
|
3286
|
+
if (!scene.meshes.includes(mesh))
|
|
3287
|
+
return false;
|
|
3288
|
+
sceneOwnedMeshes.add(mesh);
|
|
3289
|
+
return true;
|
|
3290
|
+
};
|
|
3291
|
+
const validateRuntimeRegistration = (input, batchEntityIds, batchRenderableIds, batchMeshes, batchActivityIdentities) => {
|
|
3292
|
+
const entityId = input.entityId?.trim();
|
|
3293
|
+
const behaviorProfileId = input.behaviorProfileId?.trim();
|
|
3294
|
+
if (!entityId || entityId !== input.entityId)
|
|
3295
|
+
throw new Error('shadowMapExperiment.runtimeEntityIdRequired');
|
|
3296
|
+
if (!behaviorProfileId || behaviorProfileId !== input.behaviorProfileId) {
|
|
3297
|
+
throw new Error('shadowMapExperiment.runtimeBehaviorProfileIdRequired');
|
|
3298
|
+
}
|
|
3299
|
+
const profile = requireRuntimeProfile(sourcePlan, behaviorProfileId, entityId);
|
|
3300
|
+
if (!sourcePlan?.enabled)
|
|
3301
|
+
throw new Error('shadowMapExperiment.runtimePlanDisabled');
|
|
3302
|
+
if (batchEntityIds.has(entityId) || runtimeRegistrations.has(entityId) || objectByEntity.has(entityId)) {
|
|
3303
|
+
throw new Error(`shadowMapExperiment.runtimeEntityConflict:${entityId}`);
|
|
3304
|
+
}
|
|
3305
|
+
batchEntityIds.add(entityId);
|
|
3306
|
+
if (!Array.isArray(input.renderables) || input.renderables.length === 0) {
|
|
3307
|
+
throw new Error(`shadowMapExperiment.runtimeRenderableRequired:${entityId}`);
|
|
3308
|
+
}
|
|
3309
|
+
const renderableIds = new Set();
|
|
3310
|
+
const renderables = input.renderables.map((renderable, index) => {
|
|
3311
|
+
const renderableId = renderable.renderableId?.trim();
|
|
3312
|
+
if (!renderableId || renderableId !== renderable.renderableId) {
|
|
3313
|
+
throw new Error(`shadowMapExperiment.runtimeRenderableIdRequired:${entityId}:${index}`);
|
|
3314
|
+
}
|
|
3315
|
+
if (renderableIds.has(renderableId) || batchRenderableIds.has(renderableId) || objectByRenderable.has(renderableId)) {
|
|
3316
|
+
throw new Error(`shadowMapExperiment.runtimeRenderableConflict:${renderableId}`);
|
|
3317
|
+
}
|
|
3318
|
+
if (!renderable.mesh || !isMeshOwnedByScene(renderable.mesh)) {
|
|
3319
|
+
throw new Error(`shadowMapExperiment.runtimeMeshSceneMismatch:${renderableId}`);
|
|
3320
|
+
}
|
|
3321
|
+
if (batchMeshes.has(renderable.mesh)
|
|
3322
|
+
|| runtimeOwnedMeshes.has(renderable.mesh)
|
|
3323
|
+
|| bindings.has(renderable.mesh)) {
|
|
3324
|
+
throw new Error(`shadowMapExperiment.runtimeMeshConflict:${renderableId}`);
|
|
3325
|
+
}
|
|
3326
|
+
if (!meshIdentityChainContains(renderable.mesh, renderableId)) {
|
|
3327
|
+
throw new Error(`shadowMapExperiment.runtimeRenderableIdentityMismatch:${renderableId}`);
|
|
3328
|
+
}
|
|
3329
|
+
renderableIds.add(renderableId);
|
|
3330
|
+
batchRenderableIds.add(renderableId);
|
|
3331
|
+
batchMeshes.add(renderable.mesh);
|
|
3332
|
+
return Object.freeze({ renderableId, mesh: renderable.mesh });
|
|
3333
|
+
});
|
|
3334
|
+
const activitySourceIds = new Set();
|
|
3335
|
+
const activities = (input.activities ?? []).map((activity, index) => {
|
|
3336
|
+
const sourceId = activity.sourceId?.trim();
|
|
3337
|
+
if (!sourceId || sourceId !== activity.sourceId) {
|
|
3338
|
+
throw new Error(`shadowMapExperiment.runtimeActivitySourceIdRequired:${entityId}:${index}`);
|
|
3339
|
+
}
|
|
3340
|
+
if (!['transform', 'animation', 'morph', 'physics'].includes(activity.kind)) {
|
|
3341
|
+
throw new Error(`shadowMapExperiment.runtimeActivityKindInvalid:${activity.kind}`);
|
|
3342
|
+
}
|
|
3343
|
+
const identity = `${entityId}\u0000${sourceId}`;
|
|
3344
|
+
if (activitySourceIds.has(sourceId)
|
|
3345
|
+
|| batchActivityIdentities.has(identity)
|
|
3346
|
+
|| activityScheduler.hasSource(entityId, sourceId)) {
|
|
3347
|
+
throw new Error(`shadowMapExperiment.runtimeActivityConflict:${entityId}:${sourceId}`);
|
|
3348
|
+
}
|
|
3349
|
+
if (!profile.cast || profile.updateClass === 'static') {
|
|
3350
|
+
throw new Error(`shadowMapExperiment.runtimeActivityProfileIneligible:${behaviorProfileId}`);
|
|
3351
|
+
}
|
|
3352
|
+
activitySourceIds.add(sourceId);
|
|
3353
|
+
batchActivityIdentities.add(identity);
|
|
3354
|
+
return Object.freeze({ kind: activity.kind, sourceId, initiallyActive: activity.initiallyActive === true });
|
|
3355
|
+
});
|
|
3356
|
+
return {
|
|
3357
|
+
entityId,
|
|
3358
|
+
behaviorProfileId,
|
|
3359
|
+
renderables: Object.freeze(renderables),
|
|
3360
|
+
activities: Object.freeze(activities),
|
|
3361
|
+
controllers: new Map(),
|
|
3362
|
+
owner: null,
|
|
3363
|
+
disposed: false,
|
|
3364
|
+
};
|
|
3365
|
+
};
|
|
3366
|
+
const createRuntimeSnapshot = () => Object.freeze({
|
|
3367
|
+
planSchemaVersion: sourcePlan?.schemaVersion ?? null,
|
|
3368
|
+
sourcePlanRevision: sourcePlan?.revision ?? null,
|
|
3369
|
+
effectivePlanRevision: plan?.revision ?? null,
|
|
3370
|
+
overlayRevision: runtimeOverlayRevision,
|
|
3371
|
+
incrementalMutationCount: runtimeIncrementalMutationCount,
|
|
3372
|
+
effectivePlanMaterializationCount,
|
|
3373
|
+
registrationCount: runtimeRegistrations.size,
|
|
3374
|
+
activityOwnerCount: [...runtimeRegistrations.values()].reduce((total, record) => total + record.controllers.size, 0),
|
|
3375
|
+
registrations: Object.freeze([...runtimeRegistrations.values()]
|
|
3376
|
+
.filter(record => !record.disposed)
|
|
3377
|
+
.map(record => Object.freeze({
|
|
3378
|
+
entityId: record.entityId,
|
|
3379
|
+
behaviorProfileId: record.behaviorProfileId,
|
|
3380
|
+
renderableIds: Object.freeze(record.renderables.map(renderable => renderable.renderableId).sort()),
|
|
3381
|
+
activitySourceIds: Object.freeze([...record.controllers.keys()].sort()),
|
|
3382
|
+
}))
|
|
3383
|
+
.sort((left, right) => left.entityId.localeCompare(right.entityId))),
|
|
3384
|
+
});
|
|
3385
|
+
const system = {
|
|
3386
|
+
setPlan,
|
|
3387
|
+
previewLightDirection,
|
|
3388
|
+
previewAppearance,
|
|
3389
|
+
getPlan: readMaterializedEffectivePlan,
|
|
3390
|
+
getShadowGenerator: () => generator,
|
|
3391
|
+
registerMesh(mesh) {
|
|
3392
|
+
assertUsable();
|
|
3393
|
+
if (pendingPlanRollbackRecovery) {
|
|
3394
|
+
pendingSceneMeshRegistrations.add(mesh);
|
|
3395
|
+
requestRefreshFrame('shadow-map-experiment-plan-rollback-recovery');
|
|
3396
|
+
return false;
|
|
3397
|
+
}
|
|
3398
|
+
return registerMesh(mesh);
|
|
3399
|
+
},
|
|
3400
|
+
unregisterMesh(mesh) {
|
|
3401
|
+
assertUsable();
|
|
3402
|
+
if (pendingPlanRollbackRecovery) {
|
|
3403
|
+
pendingMeshUnregistrations.add(mesh);
|
|
3404
|
+
requestRefreshFrame('shadow-map-experiment-plan-rollback-recovery');
|
|
3405
|
+
return;
|
|
3406
|
+
}
|
|
3407
|
+
requestMeshUnregistration(mesh);
|
|
3408
|
+
},
|
|
3409
|
+
setRenderableVisible(renderableId, visible) {
|
|
3410
|
+
assertUsable();
|
|
3411
|
+
if (pendingPlanRollbackRecovery) {
|
|
3412
|
+
pendingExplicitVisibility.set(renderableId, visible);
|
|
3413
|
+
pendingRenderableMembershipRetries.set(renderableId, 'caster');
|
|
3414
|
+
requestRefreshFrame('shadow-map-experiment-plan-rollback-recovery');
|
|
3415
|
+
return;
|
|
3416
|
+
}
|
|
3417
|
+
const previousRecord = registry?.getRecord(renderableId) ?? null;
|
|
3418
|
+
let rollbackCasterMembership = null;
|
|
3419
|
+
registry?.setVisible(renderableId, visible);
|
|
3420
|
+
try {
|
|
3421
|
+
rollbackCasterMembership = syncCaster(renderableId);
|
|
3422
|
+
pendingRenderableMembershipRetries.delete(renderableId);
|
|
3423
|
+
}
|
|
3424
|
+
catch (error) {
|
|
3425
|
+
rollbackCasterMembership?.(error);
|
|
3426
|
+
if (previousRecord && registry) {
|
|
3427
|
+
if (previousRecord.renderableReady)
|
|
3428
|
+
registry.attach(renderableId, previousRecord.runtimeVisible);
|
|
3429
|
+
else
|
|
3430
|
+
registry.detach(renderableId);
|
|
3431
|
+
registry.setVisible(renderableId, previousRecord.runtimeVisible);
|
|
3432
|
+
}
|
|
3433
|
+
pendingExplicitVisibility.set(renderableId, visible);
|
|
3434
|
+
pendingRenderableMembershipRetries.set(renderableId, 'caster');
|
|
3435
|
+
requestRefreshFrame('shadow-map-experiment-membership-retry');
|
|
3436
|
+
throw error;
|
|
3437
|
+
}
|
|
3438
|
+
const entityId = objectByRenderable.get(renderableId)?.entityId;
|
|
3439
|
+
if (entityId)
|
|
3440
|
+
syncActivityEligibility(entityId);
|
|
3441
|
+
},
|
|
3442
|
+
registerCasterActivitySource(input) {
|
|
3443
|
+
if (pendingPlanRollbackRecovery && !allowingRecoveryActivityRegistration) {
|
|
3444
|
+
assertNoPlanRollbackRecovery();
|
|
3445
|
+
}
|
|
3446
|
+
return registerActivitySource(input);
|
|
3447
|
+
},
|
|
3448
|
+
registerRuntimeShadowObjects(input) {
|
|
3449
|
+
assertUsable();
|
|
3450
|
+
assertNoPlanRollbackRecovery();
|
|
3451
|
+
if (!Array.isArray(input.objects) || input.objects.length === 0) {
|
|
3452
|
+
throw new Error('shadowMapExperiment.runtimeObjectsRequired');
|
|
3453
|
+
}
|
|
3454
|
+
const batchEntityIds = new Set();
|
|
3455
|
+
const batchRenderableIds = new Set();
|
|
3456
|
+
const batchMeshes = new Set();
|
|
3457
|
+
const batchActivityIdentities = new Set();
|
|
3458
|
+
const records = input.objects.map(object => validateRuntimeRegistration(object, batchEntityIds, batchRenderableIds, batchMeshes, batchActivityIdentities));
|
|
3459
|
+
if (!sourcePlan)
|
|
3460
|
+
throw new Error('shadowMapExperiment.runtimePlanUnavailable');
|
|
3461
|
+
const runtimeObjects = createRuntimePlanObjects(sourcePlan, records);
|
|
3462
|
+
const owner = createRuntimeOwner(records);
|
|
3463
|
+
const nextOverlayRevision = runtimeOverlayRevision + 1;
|
|
3464
|
+
const previousCandidateRevision = automaticActivityCandidateRevision;
|
|
3465
|
+
for (const record of records) {
|
|
3466
|
+
if (record.activities.length > 0)
|
|
3467
|
+
explicitRuntimeActivityEntityIds.add(record.entityId);
|
|
3468
|
+
}
|
|
3469
|
+
let effectivePlanMutationStarted = false;
|
|
3470
|
+
let effectivePlanApplied = false;
|
|
3471
|
+
try {
|
|
3472
|
+
effectivePlanMutationStarted = true;
|
|
3473
|
+
applyRuntimeObjectDelta({
|
|
3474
|
+
upsertedObjects: runtimeObjects,
|
|
3475
|
+
removedObjects: Object.freeze([]),
|
|
3476
|
+
overlayRevision: nextOverlayRevision,
|
|
3477
|
+
meshes: collectRuntimeRecordMeshes(records),
|
|
3478
|
+
});
|
|
3479
|
+
effectivePlanApplied = true;
|
|
3480
|
+
for (const record of records) {
|
|
3481
|
+
runtimeRegistrations.set(record.entityId, record);
|
|
3482
|
+
for (const renderable of record.renderables)
|
|
3483
|
+
runtimeOwnedMeshes.add(renderable.mesh);
|
|
3484
|
+
}
|
|
3485
|
+
for (const record of records) {
|
|
3486
|
+
for (const activity of record.activities) {
|
|
3487
|
+
const controller = system.registerCasterActivitySource({
|
|
3488
|
+
entityId: record.entityId,
|
|
3489
|
+
kind: activity.kind,
|
|
3490
|
+
sourceId: activity.sourceId,
|
|
3491
|
+
});
|
|
3492
|
+
record.controllers.set(activity.sourceId, controller);
|
|
3493
|
+
if (activity.initiallyActive)
|
|
3494
|
+
controller.setActive(true);
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
runtimeOverlayRevision = nextOverlayRevision;
|
|
3498
|
+
return owner;
|
|
3499
|
+
}
|
|
3500
|
+
catch (error) {
|
|
3501
|
+
const rollbackErrors = [];
|
|
3502
|
+
for (const record of [...records].reverse()) {
|
|
3503
|
+
for (const [sourceId, controller] of [...record.controllers.entries()].reverse()) {
|
|
3504
|
+
try {
|
|
3505
|
+
controller.setActive(false);
|
|
3506
|
+
}
|
|
3507
|
+
catch (rollbackError) {
|
|
3508
|
+
rollbackErrors.push(rollbackError);
|
|
3509
|
+
}
|
|
3510
|
+
try {
|
|
3511
|
+
controller.dispose();
|
|
3512
|
+
record.controllers.delete(sourceId);
|
|
3513
|
+
}
|
|
3514
|
+
catch (rollbackError) {
|
|
3515
|
+
rollbackErrors.push(rollbackError);
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
let effectivePlanRollbackFailed = false;
|
|
3520
|
+
if (effectivePlanMutationStarted) {
|
|
3521
|
+
try {
|
|
3522
|
+
const previousEffectivePlan = composeEffectivePlan(sourcePlan, [...runtimeRegistrations.values()].filter(candidate => (!records.includes(candidate) && !candidate.disposed)), runtimeOverlayRevision);
|
|
3523
|
+
if (effectivePlanApplied) {
|
|
3524
|
+
applyEffectivePlan(previousEffectivePlan, false, collectRuntimeRecordMeshes(records));
|
|
3525
|
+
}
|
|
3526
|
+
else {
|
|
3527
|
+
rollbackPlanUpdate(previousEffectivePlan, error, previousCandidateRevision);
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
catch (rollbackError) {
|
|
3531
|
+
if (effectivePlanApplied || rollbackError !== error) {
|
|
3532
|
+
effectivePlanRollbackFailed = true;
|
|
3533
|
+
rollbackErrors.push(rollbackError);
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
for (const record of records) {
|
|
3538
|
+
runtimeRegistrations.delete(record.entityId);
|
|
3539
|
+
for (const renderable of record.renderables)
|
|
3540
|
+
runtimeOwnedMeshes.delete(renderable.mesh);
|
|
3541
|
+
}
|
|
3542
|
+
if (rollbackErrors.length > 0) {
|
|
3543
|
+
for (const record of records) {
|
|
3544
|
+
runtimeRegistrations.set(record.entityId, record);
|
|
3545
|
+
for (const renderable of record.renderables)
|
|
3546
|
+
runtimeOwnedMeshes.add(renderable.mesh);
|
|
3547
|
+
}
|
|
3548
|
+
if (effectivePlanRollbackFailed)
|
|
3549
|
+
runtimeOverlayRevision = nextOverlayRevision;
|
|
3550
|
+
throw createRuntimeRegistrationRollbackFailure(error, owner, rollbackErrors);
|
|
3551
|
+
}
|
|
3552
|
+
else {
|
|
3553
|
+
for (const record of records) {
|
|
3554
|
+
explicitRuntimeActivityEntityIds.delete(record.entityId);
|
|
3555
|
+
record.disposed = true;
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
throw error;
|
|
3559
|
+
}
|
|
3560
|
+
},
|
|
3561
|
+
markDirty(reason, targetEntityOrRenderableId) {
|
|
3562
|
+
assertUsable();
|
|
3563
|
+
const targetedObject = targetEntityOrRenderableId
|
|
3564
|
+
? objectByEntity.get(targetEntityOrRenderableId) ?? objectByRenderable.get(targetEntityOrRenderableId)
|
|
3565
|
+
: null;
|
|
3566
|
+
const targetedLayer = targetedObject?.cast
|
|
3567
|
+
? shadowMapExperimentDepthLayerForUpdateClass(targetedObject.updateClass)
|
|
3568
|
+
: null;
|
|
3569
|
+
if (reason === 'static-geometry-changed') {
|
|
3570
|
+
pendingMembershipInvalidationLayers.add('static');
|
|
3571
|
+
invalidateSceneFitBounds(targetedObject ? targetedObject.renderableIds : null, true);
|
|
3572
|
+
}
|
|
3573
|
+
if (reason === 'material-alpha-test-changed' && targetedLayer) {
|
|
3574
|
+
markDepthDirty(targetedLayer, reason);
|
|
3575
|
+
return;
|
|
3576
|
+
}
|
|
3577
|
+
enqueueRefresh(reason);
|
|
3578
|
+
},
|
|
3579
|
+
getRuntimeSnapshot() {
|
|
3580
|
+
assertUsable();
|
|
3581
|
+
return createRuntimeSnapshot();
|
|
3582
|
+
},
|
|
3583
|
+
getEvidence() {
|
|
3584
|
+
const effectivePlanSnapshot = readMaterializedEffectivePlan();
|
|
3585
|
+
const bounds = plan?.generator.bounds;
|
|
3586
|
+
const lightSpaceSize = plan?.enabled && bounds
|
|
3587
|
+
? calculateLightSpaceSize(plan.light.direction, effectiveCoverageExtent ?? bounds.extent)
|
|
3588
|
+
: null;
|
|
3589
|
+
const activity = activityScheduler.snapshot();
|
|
3590
|
+
const automaticBindings = [...automaticAnimationGroups.values()];
|
|
3591
|
+
const automaticActivity = Object.freeze({
|
|
3592
|
+
observedAnimationGroupCount: automaticBindings.length,
|
|
3593
|
+
boundAnimationGroupCount: automaticBindings.filter(binding => binding.sources.size > 0).length,
|
|
3594
|
+
activeAnimationGroupCount: automaticBindings.filter(binding => binding.active).length,
|
|
3595
|
+
registeredActivitySourceCount: automaticBindings.reduce((count, binding) => count + binding.sources.size, 0),
|
|
3596
|
+
unresolvedActiveAnimationGroupCount: automaticBindings.filter(binding => binding.active && binding.sources.size === 0).length,
|
|
3597
|
+
candidateMeshCount: automaticActivityCandidateBindings.size,
|
|
3598
|
+
candidateRevision: automaticActivityCandidateRevision,
|
|
3599
|
+
targetResolutionCount: automaticActivityTargetResolutionCount,
|
|
3600
|
+
cachedNegativeAnimationGroupCount: automaticBindings.filter(binding => !binding.rebindPending
|
|
3601
|
+
&& binding.resolvedCandidateRevision === automaticActivityCandidateRevision
|
|
3602
|
+
&& binding.sources.size === 0).length,
|
|
3603
|
+
});
|
|
3604
|
+
const depthEvidence = depthBackend?.getEvidence() ?? null;
|
|
3605
|
+
const thresholdDiagnostics = effectivePlanSnapshot ? createThresholdDiagnostics(effectivePlanSnapshot, depthEvidence?.static.candidateMeshCount ?? 0, depthEvidence?.dynamic.candidateMeshCount ?? 0) : [];
|
|
3606
|
+
return Object.freeze({
|
|
3607
|
+
enabled: plan?.enabled === true,
|
|
3608
|
+
planRevision: plan?.revision ?? null,
|
|
3609
|
+
planFingerprint: effectivePlanSnapshot
|
|
3610
|
+
? createShadowMapExperimentPlanFingerprint(effectivePlanSnapshot)
|
|
3611
|
+
: null,
|
|
3612
|
+
effectivePlan: effectivePlanSnapshot
|
|
3613
|
+
? createShadowMapExperimentPlanSnapshot(effectivePlanSnapshot)
|
|
3614
|
+
: null,
|
|
3615
|
+
lightEntityId: plan?.light.entityId ?? null,
|
|
3616
|
+
generatorAllocated: generator !== null,
|
|
3617
|
+
resolution: plan?.enabled ? plan.generator.maps.compositeResolution : null,
|
|
3618
|
+
configuredMapResolutions: plan?.enabled ? Object.freeze({
|
|
3619
|
+
static: plan.generator.maps.staticResolution,
|
|
3620
|
+
dynamic: plan.generator.maps.dynamicResolution,
|
|
3621
|
+
composite: plan.generator.maps.compositeResolution,
|
|
3622
|
+
}) : null,
|
|
3623
|
+
mapResolutions: depthEvidence ? Object.freeze({
|
|
3624
|
+
static: depthEvidence.static.resolution,
|
|
3625
|
+
dynamic: depthEvidence.dynamic.resolution,
|
|
3626
|
+
composite: depthEvidence.compositeResolution,
|
|
3627
|
+
}) : null,
|
|
3628
|
+
diagnosticThresholds: plan?.enabled ? plan.diagnosticThresholds : null,
|
|
3629
|
+
filter: plan?.enabled ? plan.generator.filter : null,
|
|
3630
|
+
appearance: plan?.enabled
|
|
3631
|
+
? resolveShadowMapExperimentAppearance(plan.generator)
|
|
3632
|
+
: null,
|
|
3633
|
+
renderListCount: depthBackend
|
|
3634
|
+
? depthBackend.getEvidence().static.renderListCount + depthBackend.getEvidence().dynamic.renderListCount
|
|
3635
|
+
: 0,
|
|
3636
|
+
receiverMeshCount: [...receiverStates.values()].filter(state => !isDisposed(state.mesh)
|
|
3637
|
+
&& state.ownedValue
|
|
3638
|
+
&& state.mesh.receiveShadows === state.ownedValue).length,
|
|
3639
|
+
boundMeshCount: bindings.size,
|
|
3640
|
+
activity,
|
|
3641
|
+
automaticActivity,
|
|
3642
|
+
runtime: createRuntimeSnapshot(),
|
|
3643
|
+
refreshCount,
|
|
3644
|
+
lastRefreshReasons,
|
|
3645
|
+
worldUnitsPerTexel: plan?.enabled && lightSpaceSize && depthEvidence
|
|
3646
|
+
? Object.freeze([
|
|
3647
|
+
lightSpaceSize[0] / depthEvidence.compositeResolution,
|
|
3648
|
+
lightSpaceSize[1] / depthEvidence.compositeResolution,
|
|
3649
|
+
])
|
|
3650
|
+
: null,
|
|
3651
|
+
worldUnitsPerTexelByMap: plan?.enabled && lightSpaceSize && depthEvidence ? Object.freeze({
|
|
3652
|
+
static: texelSize(lightSpaceSize, depthEvidence.static.resolution),
|
|
3653
|
+
dynamic: texelSize(lightSpaceSize, depthEvidence.dynamic.resolution),
|
|
3654
|
+
composite: texelSize(lightSpaceSize, depthEvidence.compositeResolution),
|
|
3655
|
+
}) : null,
|
|
3656
|
+
registry: registry?.snapshot() ?? null,
|
|
3657
|
+
backend: depthBackend?.mode ?? null,
|
|
3658
|
+
fallbackReason: depthBackend?.fallbackReason ?? pendingFallbackReason,
|
|
3659
|
+
fallbackWarningEmitted: depthBackend?.fallbackReason
|
|
3660
|
+
? fallbackWarnings.has(depthBackend.fallbackReason)
|
|
3661
|
+
: false,
|
|
3662
|
+
depth: depthEvidence,
|
|
3663
|
+
culling: Object.freeze({
|
|
3664
|
+
...influenceCulling.snapshot(),
|
|
3665
|
+
enabled: plan?.culling.enabled === true,
|
|
3666
|
+
conservative: influenceCulling.snapshot().conservative || conservativeCasterMeshes.size > 0,
|
|
3667
|
+
stable: cullingStable,
|
|
3668
|
+
lastInvalidationReason: lastCullingInvalidationReason,
|
|
3669
|
+
coverageMode: options.coverageMode === 'scene-fit' ? 'scene-fit' : 'fixed',
|
|
3670
|
+
authoredCenter: plan
|
|
3671
|
+
? Object.freeze([...plan.generator.bounds.center])
|
|
3672
|
+
: null,
|
|
3673
|
+
effectiveCenter: plan
|
|
3674
|
+
? Object.freeze([
|
|
3675
|
+
...(effectiveCoverageCenter ?? plan.generator.bounds.center),
|
|
3676
|
+
])
|
|
3677
|
+
: null,
|
|
3678
|
+
authoredExtent: plan
|
|
3679
|
+
? Object.freeze([...plan.generator.bounds.extent])
|
|
3680
|
+
: null,
|
|
3681
|
+
effectiveExtent: plan
|
|
3682
|
+
? Object.freeze([
|
|
3683
|
+
...(effectiveCoverageExtent ?? plan.generator.bounds.extent),
|
|
3684
|
+
])
|
|
3685
|
+
: null,
|
|
3686
|
+
anchor: null,
|
|
3687
|
+
}),
|
|
3688
|
+
diagnostics: Object.freeze([
|
|
3689
|
+
...diagnostics.map(entry => Object.freeze({ ...entry })),
|
|
3690
|
+
...thresholdDiagnostics,
|
|
3691
|
+
...((depthBackend?.fallbackReason ?? pendingFallbackReason) ? [Object.freeze({
|
|
3692
|
+
code: plan?.generator.backend.fallback === 'disable'
|
|
3693
|
+
? 'shadowMapExperiment.splitBackendDisabled'
|
|
3694
|
+
: 'shadowMapExperiment.splitBackendFallback',
|
|
3695
|
+
message: plan?.generator.backend.fallback === 'disable'
|
|
3696
|
+
? `Split depth backend was disabled: ${depthBackend?.fallbackReason ?? pendingFallbackReason}`
|
|
3697
|
+
: `Split depth backend fell back to a single ShadowMap: ${depthBackend?.fallbackReason ?? pendingFallbackReason}`,
|
|
3698
|
+
})] : []),
|
|
3699
|
+
]),
|
|
3700
|
+
});
|
|
3701
|
+
},
|
|
3702
|
+
dispose() {
|
|
3703
|
+
if (disposed)
|
|
3704
|
+
return;
|
|
3705
|
+
disposing = true;
|
|
3706
|
+
// Disposal is cleanup-only. A rollback recovery may be waiting to
|
|
3707
|
+
// reactivate an old Plan; cancel that target before runtime owners start
|
|
3708
|
+
// removing their overlays so no dispose retry can resurrect it.
|
|
3709
|
+
if (pendingPlanRollbackRecovery) {
|
|
3710
|
+
pendingPlanRollbackRecovery = {
|
|
3711
|
+
...pendingPlanRollbackRecovery,
|
|
3712
|
+
targetPlan: null,
|
|
3713
|
+
phase: 'deactivate',
|
|
3714
|
+
};
|
|
3715
|
+
}
|
|
3716
|
+
plan = null;
|
|
3717
|
+
sourcePlan = null;
|
|
3718
|
+
materializedEffectivePlan = null;
|
|
3719
|
+
activityScheduler.applyPlan(null);
|
|
3720
|
+
const runtimeCleanupErrors = disposeRuntimeRecordsForSystem([...runtimeRegistrations.values()]);
|
|
3721
|
+
if (runtimeCleanupErrors.length > 0) {
|
|
3722
|
+
throw Object.assign(new Error('shadowMapExperiment.disposeFailed'), {
|
|
3723
|
+
cleanupErrors: Object.freeze(runtimeCleanupErrors),
|
|
3724
|
+
});
|
|
3725
|
+
}
|
|
3726
|
+
const cleanupErrors = [];
|
|
3727
|
+
const attempt = (operation, completed) => {
|
|
3728
|
+
try {
|
|
3729
|
+
operation();
|
|
3730
|
+
completed();
|
|
3731
|
+
}
|
|
3732
|
+
catch (error) {
|
|
3733
|
+
cleanupErrors.push(error);
|
|
3734
|
+
}
|
|
3735
|
+
};
|
|
3736
|
+
if (!disposeNewAnimationGroupObserverRemoved) {
|
|
3737
|
+
attempt(() => { scene.onNewAnimationGroupAddedObservable.remove(newAnimationGroupObserver); }, () => { disposeNewAnimationGroupObserverRemoved = true; });
|
|
3738
|
+
}
|
|
3739
|
+
if (!disposeRemovedAnimationGroupObserverRemoved) {
|
|
3740
|
+
attempt(() => { scene.onAnimationGroupRemovedObservable.remove(removedAnimationGroupObserver); }, () => { disposeRemovedAnimationGroupObserverRemoved = true; });
|
|
3741
|
+
}
|
|
3742
|
+
for (const group of [...automaticAnimationGroups.keys()].reverse()) {
|
|
3743
|
+
cleanupErrors.push(...detachAutomaticAnimationGroup(group));
|
|
3744
|
+
}
|
|
3745
|
+
if (!disposeNewMeshObserverRemoved) {
|
|
3746
|
+
attempt(() => { scene.onNewMeshAddedObservable.remove(newMeshObserver); }, () => { disposeNewMeshObserverRemoved = true; });
|
|
3747
|
+
}
|
|
3748
|
+
if (!disposeRemovedMeshObserverRemoved) {
|
|
3749
|
+
attempt(() => { scene.onMeshRemovedObservable.remove(removedMeshObserver); }, () => { disposeRemovedMeshObserverRemoved = true; });
|
|
3750
|
+
}
|
|
3751
|
+
if (!disposeFrameObserverRemoved) {
|
|
3752
|
+
attempt(() => { scene.onBeforeRenderObservable.remove(frameObserver); }, () => { disposeFrameObserverRemoved = true; });
|
|
3753
|
+
}
|
|
3754
|
+
if (hasOwnedBackendState())
|
|
3755
|
+
attempt(deactivate, () => undefined);
|
|
3756
|
+
pendingPlanRollbackRecovery = null;
|
|
3757
|
+
const observersRemoved = disposeNewMeshObserverRemoved
|
|
3758
|
+
&& disposeRemovedMeshObserverRemoved
|
|
3759
|
+
&& disposeFrameObserverRemoved
|
|
3760
|
+
&& disposeNewAnimationGroupObserverRemoved
|
|
3761
|
+
&& disposeRemovedAnimationGroupObserverRemoved
|
|
3762
|
+
&& automaticAnimationGroups.size === 0;
|
|
3763
|
+
if (observersRemoved && !hasOwnedBackendState()) {
|
|
3764
|
+
if (!disposeActivitySchedulerDisposed) {
|
|
3765
|
+
attempt(activityScheduler.dispose, () => { disposeActivitySchedulerDisposed = true; });
|
|
3766
|
+
}
|
|
3767
|
+
if (!disposeDepthRefreshSchedulerDisposed) {
|
|
3768
|
+
attempt(depthRefreshScheduler.dispose, () => { disposeDepthRefreshSchedulerDisposed = true; });
|
|
3769
|
+
}
|
|
3770
|
+
if (!disposeInfluenceCullingDisposed) {
|
|
3771
|
+
attempt(influenceCulling.dispose, () => { disposeInfluenceCullingDisposed = true; });
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
if (!hasOwnedBackendState()) {
|
|
3775
|
+
sceneOwnedMeshes.clear();
|
|
3776
|
+
activeDeformationSourceCountByEntity.clear();
|
|
3777
|
+
pendingFinalDeformationEntityIds.clear();
|
|
3778
|
+
pendingDeformationInvalidations.clear();
|
|
3779
|
+
activeCasterEntityIds.clear();
|
|
3780
|
+
pendingTransformMembershipRetries.clear();
|
|
3781
|
+
pendingSceneMeshRegistrations.clear();
|
|
3782
|
+
pendingMeshUnregistrations.clear();
|
|
3783
|
+
pendingRenderableMembershipRetries.clear();
|
|
3784
|
+
pendingExplicitVisibility.clear();
|
|
3785
|
+
pendingAutomaticAnimationGroupAttachments.clear();
|
|
3786
|
+
pendingAutomaticAnimationGroupReconciliations.clear();
|
|
3787
|
+
}
|
|
3788
|
+
disposed = observersRemoved
|
|
3789
|
+
&& !hasOwnedBackendState()
|
|
3790
|
+
&& disposeActivitySchedulerDisposed
|
|
3791
|
+
&& disposeDepthRefreshSchedulerDisposed
|
|
3792
|
+
&& disposeInfluenceCullingDisposed;
|
|
3793
|
+
if (cleanupErrors.length > 0) {
|
|
3794
|
+
throw Object.assign(new Error('shadowMapExperiment.disposeFailed'), { cleanupErrors });
|
|
3795
|
+
}
|
|
3796
|
+
},
|
|
3797
|
+
};
|
|
3798
|
+
if (initialPlan) {
|
|
3799
|
+
try {
|
|
3800
|
+
setPlan(initialPlan);
|
|
3801
|
+
}
|
|
3802
|
+
catch (error) {
|
|
3803
|
+
let newMeshObserverRemoved = false;
|
|
3804
|
+
let removedMeshObserverRemoved = false;
|
|
3805
|
+
let frameObserverRemoved = false;
|
|
3806
|
+
let newAnimationGroupObserverRemoved = false;
|
|
3807
|
+
let removedAnimationGroupObserverRemoved = false;
|
|
3808
|
+
let activitySchedulerDisposed = false;
|
|
3809
|
+
let depthRefreshSchedulerDisposed = false;
|
|
3810
|
+
let influenceCullingDisposed = false;
|
|
3811
|
+
let cleanupComplete = false;
|
|
3812
|
+
const cleanup = () => {
|
|
3813
|
+
if (cleanupComplete)
|
|
3814
|
+
return Object.freeze([]);
|
|
3815
|
+
const cleanupErrors = [];
|
|
3816
|
+
const attempt = (operation, completed) => {
|
|
3817
|
+
try {
|
|
3818
|
+
operation();
|
|
3819
|
+
completed();
|
|
3820
|
+
}
|
|
3821
|
+
catch (cleanupError) {
|
|
3822
|
+
cleanupErrors.push(cleanupError);
|
|
3823
|
+
}
|
|
3824
|
+
};
|
|
3825
|
+
if (!newAnimationGroupObserverRemoved) {
|
|
3826
|
+
attempt(() => { scene.onNewAnimationGroupAddedObservable.remove(newAnimationGroupObserver); }, () => { newAnimationGroupObserverRemoved = true; });
|
|
3827
|
+
}
|
|
3828
|
+
if (!removedAnimationGroupObserverRemoved) {
|
|
3829
|
+
attempt(() => { scene.onAnimationGroupRemovedObservable.remove(removedAnimationGroupObserver); }, () => { removedAnimationGroupObserverRemoved = true; });
|
|
3830
|
+
}
|
|
3831
|
+
for (const group of [...automaticAnimationGroups.keys()].reverse()) {
|
|
3832
|
+
cleanupErrors.push(...detachAutomaticAnimationGroup(group));
|
|
3833
|
+
}
|
|
3834
|
+
if (!newMeshObserverRemoved) {
|
|
3835
|
+
attempt(() => { scene.onNewMeshAddedObservable.remove(newMeshObserver); }, () => { newMeshObserverRemoved = true; });
|
|
3836
|
+
}
|
|
3837
|
+
if (!removedMeshObserverRemoved) {
|
|
3838
|
+
attempt(() => { scene.onMeshRemovedObservable.remove(removedMeshObserver); }, () => { removedMeshObserverRemoved = true; });
|
|
3839
|
+
}
|
|
3840
|
+
if (!frameObserverRemoved) {
|
|
3841
|
+
attempt(() => { scene.onBeforeRenderObservable.remove(frameObserver); }, () => { frameObserverRemoved = true; });
|
|
3842
|
+
}
|
|
3843
|
+
if (hasOwnedBackendState()) {
|
|
3844
|
+
attempt(deactivate, () => undefined);
|
|
3845
|
+
}
|
|
3846
|
+
plan = null;
|
|
3847
|
+
const observersRemoved = newMeshObserverRemoved
|
|
3848
|
+
&& removedMeshObserverRemoved
|
|
3849
|
+
&& frameObserverRemoved
|
|
3850
|
+
&& newAnimationGroupObserverRemoved
|
|
3851
|
+
&& removedAnimationGroupObserverRemoved
|
|
3852
|
+
&& automaticAnimationGroups.size === 0;
|
|
3853
|
+
if (observersRemoved && !hasOwnedBackendState()) {
|
|
3854
|
+
if (!activitySchedulerDisposed) {
|
|
3855
|
+
attempt(activityScheduler.dispose, () => { activitySchedulerDisposed = true; });
|
|
3856
|
+
}
|
|
3857
|
+
if (!depthRefreshSchedulerDisposed) {
|
|
3858
|
+
attempt(depthRefreshScheduler.dispose, () => { depthRefreshSchedulerDisposed = true; });
|
|
3859
|
+
}
|
|
3860
|
+
if (!influenceCullingDisposed) {
|
|
3861
|
+
attempt(influenceCulling.dispose, () => { influenceCullingDisposed = true; });
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
cleanupComplete = observersRemoved
|
|
3865
|
+
&& !hasOwnedBackendState()
|
|
3866
|
+
&& activitySchedulerDisposed
|
|
3867
|
+
&& depthRefreshSchedulerDisposed
|
|
3868
|
+
&& influenceCullingDisposed;
|
|
3869
|
+
if (cleanupComplete)
|
|
3870
|
+
disposed = true;
|
|
3871
|
+
return Object.freeze(cleanupErrors);
|
|
3872
|
+
};
|
|
3873
|
+
const initializationCleanupErrors = cleanup();
|
|
3874
|
+
if (initializationCleanupErrors.length > 0) {
|
|
3875
|
+
throw createInitializationFailure(error, cleanup, initializationCleanupErrors);
|
|
3876
|
+
}
|
|
3877
|
+
throw error;
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
return system;
|
|
3881
|
+
}
|
|
3882
|
+
function createThresholdDiagnostics(plan, staticCasterCount, dynamicCasterCount) {
|
|
3883
|
+
const thresholds = plan.diagnosticThresholds;
|
|
3884
|
+
const result = [];
|
|
3885
|
+
if (staticCasterCount > thresholds.staticCasterCountWarning) {
|
|
3886
|
+
result.push({
|
|
3887
|
+
code: 'shadowMapExperiment.staticCasterCountWarning',
|
|
3888
|
+
message: `Static caster count ${staticCasterCount} exceeds the configured warning threshold ${thresholds.staticCasterCountWarning}.`,
|
|
3889
|
+
});
|
|
3890
|
+
}
|
|
3891
|
+
if (dynamicCasterCount > thresholds.dynamicCasterCountWarning) {
|
|
3892
|
+
result.push({
|
|
3893
|
+
code: 'shadowMapExperiment.dynamicCasterCountWarning',
|
|
3894
|
+
message: `Dynamic caster count ${dynamicCasterCount} exceeds the configured warning threshold ${thresholds.dynamicCasterCountWarning}.`,
|
|
3895
|
+
});
|
|
3896
|
+
}
|
|
3897
|
+
const renderableCount = plan.objects.reduce((total, object) => total + object.renderableIds.length, 0);
|
|
3898
|
+
if (renderableCount > thresholds.renderableCountWarning) {
|
|
3899
|
+
result.push({
|
|
3900
|
+
code: 'shadowMapExperiment.renderableCountWarning',
|
|
3901
|
+
message: `Renderable count ${renderableCount} exceeds the configured warning threshold ${thresholds.renderableCountWarning}.`,
|
|
3902
|
+
});
|
|
3903
|
+
}
|
|
3904
|
+
return Object.freeze(result.map(entry => Object.freeze(entry)));
|
|
3905
|
+
}
|
|
3906
|
+
function texelSize(lightSpaceSize, resolution) {
|
|
3907
|
+
return Object.freeze([
|
|
3908
|
+
lightSpaceSize[0] / resolution,
|
|
3909
|
+
lightSpaceSize[1] / resolution,
|
|
3910
|
+
]);
|
|
3911
|
+
}
|
|
3912
|
+
function configureLight(light, plan, coverage) {
|
|
3913
|
+
const direction = Vector3.FromArray(plan.light.direction).normalize();
|
|
3914
|
+
if (direction.lengthSquared() < 1e-8)
|
|
3915
|
+
throw new Error('shadowMapExperiment.lightDirectionInvalid');
|
|
3916
|
+
const center = Vector3.FromArray(coverage.center);
|
|
3917
|
+
const depth = (coverage.near + coverage.far) * 0.5;
|
|
3918
|
+
const [width, height] = calculateLightSpaceSize(plan.light.direction, coverage.extent);
|
|
3919
|
+
light.direction.copyFrom(direction);
|
|
3920
|
+
light.position.copyFrom(center.subtract(direction.scale(depth)));
|
|
3921
|
+
light.autoUpdateExtends = false;
|
|
3922
|
+
light.orthoLeft = -width * 0.5;
|
|
3923
|
+
light.orthoRight = width * 0.5;
|
|
3924
|
+
light.orthoBottom = -height * 0.5;
|
|
3925
|
+
light.orthoTop = height * 0.5;
|
|
3926
|
+
light.shadowMinZ = coverage.near;
|
|
3927
|
+
light.shadowMaxZ = coverage.far;
|
|
3928
|
+
}
|
|
3929
|
+
function createSceneFitCoverage(plan, bounds) {
|
|
3930
|
+
const min = Vector3.FromArray(bounds.min);
|
|
3931
|
+
const max = Vector3.FromArray(bounds.max);
|
|
3932
|
+
const rawExtent = max.subtract(min);
|
|
3933
|
+
const maxExtent = Math.max(rawExtent.x, rawExtent.y, rawExtent.z, 1);
|
|
3934
|
+
const padding = Math.max(0.5, maxExtent * 0.05);
|
|
3935
|
+
const extent = Object.freeze([
|
|
3936
|
+
Math.max(4, rawExtent.x + padding * 2),
|
|
3937
|
+
Math.max(4, rawExtent.y + padding * 2),
|
|
3938
|
+
Math.max(4, rawExtent.z + padding * 2),
|
|
3939
|
+
]);
|
|
3940
|
+
const near = Math.min(plan.generator.bounds.near, 0.1);
|
|
3941
|
+
const far = Math.max(plan.generator.bounds.far, Math.hypot(extent[0], extent[1], extent[2]) + padding * 2);
|
|
3942
|
+
const center = snapSceneFitCenterToTexels(plan, vectorTuple(min.add(max).scale(0.5)), extent, near, far);
|
|
3943
|
+
return Object.freeze({ center, extent, near, far });
|
|
3944
|
+
}
|
|
3945
|
+
function collectActiveCasterWorldBounds(bindings, registry) {
|
|
3946
|
+
let min = null;
|
|
3947
|
+
let max = null;
|
|
3948
|
+
for (const binding of bindings.values()) {
|
|
3949
|
+
if (registry?.getRecord(binding.renderableId)?.state !== 'active')
|
|
3950
|
+
continue;
|
|
3951
|
+
if (!isMeshRuntimeVisible(binding.mesh))
|
|
3952
|
+
continue;
|
|
3953
|
+
const bounds = readStableMeshWorldBounds(binding);
|
|
3954
|
+
if (!bounds)
|
|
3955
|
+
continue;
|
|
3956
|
+
const boundsMin = Vector3.FromArray(bounds.min);
|
|
3957
|
+
const boundsMax = Vector3.FromArray(bounds.max);
|
|
3958
|
+
min = min ? Vector3.Minimize(min, boundsMin) : boundsMin;
|
|
3959
|
+
max = max ? Vector3.Maximize(max, boundsMax) : boundsMax;
|
|
3960
|
+
}
|
|
3961
|
+
return min && max
|
|
3962
|
+
? Object.freeze({ min: vectorTuple(min), max: vectorTuple(max) })
|
|
3963
|
+
: null;
|
|
3964
|
+
}
|
|
3965
|
+
function createLightSpaceBasis(direction) {
|
|
3966
|
+
const upSeed = Math.abs(Vector3.Dot(direction, Vector3.Up())) > 0.98 ? Vector3.Forward() : Vector3.Up();
|
|
3967
|
+
const right = Vector3.Cross(upSeed, direction).normalize();
|
|
3968
|
+
const up = Vector3.Cross(direction, right).normalize();
|
|
3969
|
+
return { right, up };
|
|
3970
|
+
}
|
|
3971
|
+
function snapSceneFitCenterToTexels(plan, centerValue, extentValue, near, far) {
|
|
3972
|
+
const direction = Vector3.FromArray(plan.light.direction).normalize();
|
|
3973
|
+
const { right, up } = createLightSpaceBasis(direction);
|
|
3974
|
+
const center = Vector3.FromArray(centerValue);
|
|
3975
|
+
const [width, height] = calculateLightSpaceSize(plan.light.direction, extentValue);
|
|
3976
|
+
const resolution = Math.max(1, plan.generator.maps.compositeResolution);
|
|
3977
|
+
const depth = Math.max(far - near, 1e-4);
|
|
3978
|
+
const snap = (value, step) => Math.round(value / step) * step;
|
|
3979
|
+
const rightValue = snap(Vector3.Dot(center, right), width / resolution);
|
|
3980
|
+
const upValue = snap(Vector3.Dot(center, up), height / resolution);
|
|
3981
|
+
const depthValue = snap(Vector3.Dot(center, direction), depth / resolution);
|
|
3982
|
+
const result = right.scale(rightValue).add(up.scale(upValue)).add(direction.scale(depthValue));
|
|
3983
|
+
return vectorTuple(result);
|
|
3984
|
+
}
|
|
3985
|
+
function areTuple3Equal(left, right) {
|
|
3986
|
+
return left.every((value, index) => Math.abs(value - right[index]) <= 1e-8);
|
|
3987
|
+
}
|
|
3988
|
+
function calculateLightSpaceSize(directionValue, extent) {
|
|
3989
|
+
const direction = Vector3.FromArray(directionValue).normalize();
|
|
3990
|
+
const { right, up } = createLightSpaceBasis(direction);
|
|
3991
|
+
const half = new Vector3(extent[0] * 0.5, extent[1] * 0.5, extent[2] * 0.5);
|
|
3992
|
+
let width = 0;
|
|
3993
|
+
let height = 0;
|
|
3994
|
+
for (const x of [-half.x, half.x]) {
|
|
3995
|
+
for (const y of [-half.y, half.y]) {
|
|
3996
|
+
for (const z of [-half.z, half.z]) {
|
|
3997
|
+
const corner = new Vector3(x, y, z);
|
|
3998
|
+
width = Math.max(width, Math.abs(Vector3.Dot(corner, right)) * 2);
|
|
3999
|
+
height = Math.max(height, Math.abs(Vector3.Dot(corner, up)) * 2);
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
4003
|
+
return Object.freeze([Math.max(width, 1e-4), Math.max(height, 1e-4)]);
|
|
4004
|
+
}
|
|
4005
|
+
export function resolveBabylonDirectionalLight(scene, entityId) {
|
|
4006
|
+
for (const light of scene.lights) {
|
|
4007
|
+
if (!isBabylonDirectionalLight(light))
|
|
4008
|
+
continue;
|
|
4009
|
+
if (collectIdentityCandidates(light).has(entityId))
|
|
4010
|
+
return light;
|
|
4011
|
+
}
|
|
4012
|
+
return null;
|
|
4013
|
+
}
|
|
4014
|
+
function isBabylonDirectionalLight(light) {
|
|
4015
|
+
if (light instanceof DirectionalLight)
|
|
4016
|
+
return true;
|
|
4017
|
+
if (!isRecord(light) || typeof light.getClassName !== 'function')
|
|
4018
|
+
return false;
|
|
4019
|
+
try {
|
|
4020
|
+
return light.getClassName() === 'DirectionalLight';
|
|
4021
|
+
}
|
|
4022
|
+
catch {
|
|
4023
|
+
return false;
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
function resolveRenderableId(mesh, objectByRenderable) {
|
|
4027
|
+
let current = mesh;
|
|
4028
|
+
while (current) {
|
|
4029
|
+
for (const candidate of collectIdentityCandidates(current)) {
|
|
4030
|
+
if (objectByRenderable.has(candidate))
|
|
4031
|
+
return candidate;
|
|
4032
|
+
}
|
|
4033
|
+
current = current.parent;
|
|
4034
|
+
}
|
|
4035
|
+
return null;
|
|
4036
|
+
}
|
|
4037
|
+
function collectIdentityCandidates(node) {
|
|
4038
|
+
const candidates = new Set();
|
|
4039
|
+
if (node.id)
|
|
4040
|
+
candidates.add(node.id);
|
|
4041
|
+
if (node.name)
|
|
4042
|
+
candidates.add(node.name);
|
|
4043
|
+
const metadata = isRecord(node.metadata) ? node.metadata : null;
|
|
4044
|
+
if (!metadata)
|
|
4045
|
+
return candidates;
|
|
4046
|
+
addIdentityCandidatesFromRecord(candidates, metadata);
|
|
4047
|
+
const projection = isRecord(metadata.editorProjection) ? metadata.editorProjection : null;
|
|
4048
|
+
if (projection)
|
|
4049
|
+
addIdentityCandidatesFromRecord(candidates, projection);
|
|
4050
|
+
const editorSource = isRecord(metadata.__fpsEditor) ? metadata.__fpsEditor : null;
|
|
4051
|
+
if (editorSource)
|
|
4052
|
+
addIdentityCandidatesFromRecord(candidates, editorSource);
|
|
4053
|
+
const sourceBinding = isRecord(metadata.sourceBinding) ? metadata.sourceBinding : null;
|
|
4054
|
+
if (sourceBinding)
|
|
4055
|
+
addIdentityCandidatesFromRecord(candidates, sourceBinding);
|
|
4056
|
+
return candidates;
|
|
4057
|
+
}
|
|
4058
|
+
function addIdentityCandidatesFromRecord(candidates, record) {
|
|
4059
|
+
for (const key of [
|
|
4060
|
+
'entityId',
|
|
4061
|
+
'renderableId',
|
|
4062
|
+
'runtimeNodeId',
|
|
4063
|
+
'nodeId',
|
|
4064
|
+
'rootNodeId',
|
|
4065
|
+
'objectId',
|
|
4066
|
+
'sourceId',
|
|
4067
|
+
]) {
|
|
4068
|
+
const value = record[key];
|
|
4069
|
+
if (typeof value === 'string' && value.trim())
|
|
4070
|
+
candidates.add(value);
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
function meshIdentityChainContains(mesh, identity) {
|
|
4074
|
+
let current = mesh;
|
|
4075
|
+
while (current) {
|
|
4076
|
+
if (collectIdentityCandidates(current).has(identity))
|
|
4077
|
+
return true;
|
|
4078
|
+
current = current.parent;
|
|
4079
|
+
}
|
|
4080
|
+
return false;
|
|
4081
|
+
}
|
|
4082
|
+
function isReceiverEligible(mesh) {
|
|
4083
|
+
const metadata = isRecord(mesh.metadata) ? mesh.metadata : null;
|
|
4084
|
+
const projection = metadata && isRecord(metadata.editorProjection) ? metadata.editorProjection : null;
|
|
4085
|
+
if (metadata?.helper === true || projection?.helper === true)
|
|
4086
|
+
return false;
|
|
4087
|
+
const material = mesh.material;
|
|
4088
|
+
if (!material)
|
|
4089
|
+
return true;
|
|
4090
|
+
if ('disableLighting' in material && material.disableLighting === true)
|
|
4091
|
+
return false;
|
|
4092
|
+
try {
|
|
4093
|
+
if (material.needAlphaBlendingForMesh(mesh) && !allowsTransparentShadowReceiving(material))
|
|
4094
|
+
return false;
|
|
4095
|
+
}
|
|
4096
|
+
catch {
|
|
4097
|
+
return false;
|
|
4098
|
+
}
|
|
4099
|
+
return true;
|
|
4100
|
+
}
|
|
4101
|
+
function deepFreezeShadowMapExperimentPlan(plan) {
|
|
4102
|
+
const freeze = (value) => {
|
|
4103
|
+
if (!value || typeof value !== 'object')
|
|
4104
|
+
return;
|
|
4105
|
+
for (const child of Object.values(value))
|
|
4106
|
+
freeze(child);
|
|
4107
|
+
if (!Object.isFrozen(value))
|
|
4108
|
+
Object.freeze(value);
|
|
4109
|
+
};
|
|
4110
|
+
freeze(plan);
|
|
4111
|
+
return plan;
|
|
4112
|
+
}
|
|
4113
|
+
function allowsTransparentShadowReceiving(material) {
|
|
4114
|
+
if (!material)
|
|
4115
|
+
return false;
|
|
4116
|
+
const metadata = isRecord(material.metadata) ? material.metadata : null;
|
|
4117
|
+
const shadowMapExperiment = metadata && isRecord(metadata.shadowMapExperiment)
|
|
4118
|
+
? metadata.shadowMapExperiment
|
|
4119
|
+
: null;
|
|
4120
|
+
return shadowMapExperiment?.allowTransparentReceiver === true;
|
|
4121
|
+
}
|
|
4122
|
+
function resolveReceiverTarget(mesh) {
|
|
4123
|
+
const sourceMesh = mesh.sourceMesh;
|
|
4124
|
+
return sourceMesh && !isDisposed(sourceMesh) ? sourceMesh : mesh;
|
|
4125
|
+
}
|
|
4126
|
+
function readWorldMatrixSnapshot(mesh) {
|
|
4127
|
+
const matrix = mesh.getWorldMatrix?.();
|
|
4128
|
+
if (!matrix || typeof matrix.asArray !== 'function')
|
|
4129
|
+
return null;
|
|
4130
|
+
return [...matrix.asArray()];
|
|
4131
|
+
}
|
|
4132
|
+
function areMatrixSnapshotsEqual(previous, next) {
|
|
4133
|
+
if (previous === next)
|
|
4134
|
+
return true;
|
|
4135
|
+
if (!previous || !next || previous.length !== next.length)
|
|
4136
|
+
return false;
|
|
4137
|
+
for (let index = 0; index < previous.length; index += 1) {
|
|
4138
|
+
if (previous[index] !== next[index])
|
|
4139
|
+
return false;
|
|
4140
|
+
}
|
|
4141
|
+
return true;
|
|
4142
|
+
}
|
|
4143
|
+
function isMeshRuntimeVisible(mesh) {
|
|
4144
|
+
return !isDisposed(mesh) && mesh.isEnabled() && mesh.isVisible && mesh.visibility > 0;
|
|
4145
|
+
}
|
|
4146
|
+
function isDisposed(mesh) {
|
|
4147
|
+
return typeof mesh.isDisposed === 'function' && mesh.isDisposed();
|
|
4148
|
+
}
|
|
4149
|
+
function readMeshLocalBounds(mesh) {
|
|
4150
|
+
try {
|
|
4151
|
+
const bounds = mesh.getBoundingInfo?.().boundingBox;
|
|
4152
|
+
if (!bounds)
|
|
4153
|
+
return null;
|
|
4154
|
+
const min = bounds.minimum;
|
|
4155
|
+
const max = bounds.maximum;
|
|
4156
|
+
if (![min.x, min.y, min.z, max.x, max.y, max.z].every(Number.isFinite))
|
|
4157
|
+
return null;
|
|
4158
|
+
return Object.freeze({ min: vectorTuple(min), max: vectorTuple(max) });
|
|
4159
|
+
}
|
|
4160
|
+
catch {
|
|
4161
|
+
return null;
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4164
|
+
function readStableMeshWorldBounds(binding) {
|
|
4165
|
+
const local = binding.sceneFitLocalBounds;
|
|
4166
|
+
if (!local)
|
|
4167
|
+
return readMeshWorldBounds(binding.mesh);
|
|
4168
|
+
try {
|
|
4169
|
+
binding.mesh.computeWorldMatrix(false);
|
|
4170
|
+
const matrix = binding.mesh.getWorldMatrix();
|
|
4171
|
+
const min = Vector3.FromArray(local.min);
|
|
4172
|
+
const max = Vector3.FromArray(local.max);
|
|
4173
|
+
let worldMin = null;
|
|
4174
|
+
let worldMax = null;
|
|
4175
|
+
for (const x of [min.x, max.x]) {
|
|
4176
|
+
for (const y of [min.y, max.y]) {
|
|
4177
|
+
for (const z of [min.z, max.z]) {
|
|
4178
|
+
const point = Vector3.TransformCoordinates(new Vector3(x, y, z), matrix);
|
|
4179
|
+
worldMin = worldMin ? Vector3.Minimize(worldMin, point) : point;
|
|
4180
|
+
worldMax = worldMax ? Vector3.Maximize(worldMax, point) : point;
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
return worldMin && worldMax
|
|
4185
|
+
? Object.freeze({ min: vectorTuple(worldMin), max: vectorTuple(worldMax) })
|
|
4186
|
+
: null;
|
|
4187
|
+
}
|
|
4188
|
+
catch {
|
|
4189
|
+
return null;
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
function readMeshWorldBounds(mesh) {
|
|
4193
|
+
try {
|
|
4194
|
+
mesh.computeWorldMatrix(false);
|
|
4195
|
+
const bounds = mesh.getBoundingInfo?.().boundingBox;
|
|
4196
|
+
if (!bounds)
|
|
4197
|
+
return null;
|
|
4198
|
+
const min = bounds.minimumWorld;
|
|
4199
|
+
const max = bounds.maximumWorld;
|
|
4200
|
+
if (![min.x, min.y, min.z, max.x, max.y, max.z].every(Number.isFinite))
|
|
4201
|
+
return null;
|
|
4202
|
+
return Object.freeze({ min: vectorTuple(min), max: vectorTuple(max) });
|
|
4203
|
+
}
|
|
4204
|
+
catch {
|
|
4205
|
+
return null;
|
|
4206
|
+
}
|
|
4207
|
+
}
|
|
4208
|
+
function isDeformableCaster(mesh, updateClass) {
|
|
4209
|
+
const deformable = mesh;
|
|
4210
|
+
return updateClass === 'skinned' || Boolean(deformable.skeleton) || Boolean(deformable.morphTargetManager);
|
|
4211
|
+
}
|
|
4212
|
+
function createMeshDataCache(mesh, caches) {
|
|
4213
|
+
const cache = {};
|
|
4214
|
+
caches.set(mesh, cache);
|
|
4215
|
+
return cache;
|
|
4216
|
+
}
|
|
4217
|
+
function readCasterWorldBounds(mesh, deformable, cache) {
|
|
4218
|
+
if (deformable) {
|
|
4219
|
+
const deformableMesh = mesh;
|
|
4220
|
+
const applySkeleton = Boolean(deformableMesh.skeleton);
|
|
4221
|
+
const applyMorph = Boolean(deformableMesh.morphTargetManager);
|
|
4222
|
+
if (applySkeleton || applyMorph) {
|
|
4223
|
+
try {
|
|
4224
|
+
mesh.refreshBoundingInfo({
|
|
4225
|
+
applySkeleton,
|
|
4226
|
+
applyMorph,
|
|
4227
|
+
updatePositionsArray: false,
|
|
4228
|
+
...(cache ? { cache } : {}),
|
|
4229
|
+
});
|
|
4230
|
+
}
|
|
4231
|
+
catch {
|
|
4232
|
+
return null;
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
return readMeshWorldBounds(mesh);
|
|
4237
|
+
}
|
|
4238
|
+
function readCameraProjectionSignature(camera, scene) {
|
|
4239
|
+
const projection = camera.getProjectionMatrix().asArray()
|
|
4240
|
+
.map(value => Number.isFinite(value) ? value.toFixed(5) : String(value))
|
|
4241
|
+
.join(',');
|
|
4242
|
+
const engine = scene.getEngine();
|
|
4243
|
+
return `${camera.mode}:${engine.getRenderWidth()}:${engine.getRenderHeight()}:${projection}`;
|
|
4244
|
+
}
|
|
4245
|
+
function calculateCameraShadowCoverage(camera, plan, centerValue = plan.generator.bounds.center, extentValue = plan.generator.bounds.extent) {
|
|
4246
|
+
try {
|
|
4247
|
+
const fixedMin = Vector3.FromArray(centerValue).subtract(Vector3.FromArray(extentValue).scale(0.5));
|
|
4248
|
+
const fixedMax = Vector3.FromArray(centerValue).add(Vector3.FromArray(extentValue).scale(0.5));
|
|
4249
|
+
const inverse = Matrix.Invert(camera.getTransformationMatrix());
|
|
4250
|
+
const engine = camera.getScene().getEngine();
|
|
4251
|
+
const ndcNearZ = engine.isNDCHalfZRange ? 0 : -1;
|
|
4252
|
+
const frustumCorners = [];
|
|
4253
|
+
for (const x of [-1, 1]) {
|
|
4254
|
+
for (const y of [-1, 1]) {
|
|
4255
|
+
for (const z of [ndcNearZ, 1]) {
|
|
4256
|
+
frustumCorners.push(Vector3.TransformCoordinates(new Vector3(x, y, z), inverse));
|
|
4257
|
+
}
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
4260
|
+
const frustumMin = frustumCorners.reduce((result, point) => Vector3.Minimize(result, point), frustumCorners[0].clone());
|
|
4261
|
+
const frustumMax = frustumCorners.reduce((result, point) => Vector3.Maximize(result, point), frustumCorners[0].clone());
|
|
4262
|
+
const min = Vector3.Maximize(frustumMin, fixedMin);
|
|
4263
|
+
const max = Vector3.Minimize(frustumMax, fixedMax);
|
|
4264
|
+
if (min.x > max.x || min.y > max.y || min.z > max.z)
|
|
4265
|
+
return null;
|
|
4266
|
+
return Object.freeze({ min: vectorTuple(min), max: vectorTuple(max) });
|
|
4267
|
+
}
|
|
4268
|
+
catch {
|
|
4269
|
+
return null;
|
|
4270
|
+
}
|
|
4271
|
+
}
|
|
4272
|
+
function vectorTuple(value) {
|
|
4273
|
+
return Object.freeze([value.x, value.y, value.z]);
|
|
4274
|
+
}
|
|
4275
|
+
function resolveAutomaticActivityTarget(bindings, target, objectByRenderable, targetIndex) {
|
|
4276
|
+
const className = readBabylonClassName(target);
|
|
4277
|
+
const resolved = new Set();
|
|
4278
|
+
if (!target || (typeof target !== 'object' && typeof target !== 'function')) {
|
|
4279
|
+
return Object.freeze({ kind: 'transform', meshes: resolved });
|
|
4280
|
+
}
|
|
4281
|
+
if (className === 'MorphTarget') {
|
|
4282
|
+
for (const mesh of targetIndex.meshesByMorphTarget.get(target) ?? [])
|
|
4283
|
+
resolved.add(mesh);
|
|
4284
|
+
return Object.freeze({ kind: 'morph', meshes: resolved });
|
|
4285
|
+
}
|
|
4286
|
+
const skeletons = new Set(targetIndex.skeletonsByTarget.get(target) ?? []);
|
|
4287
|
+
const directSkeleton = callOptional(target, 'getSkeleton')
|
|
4288
|
+
?? target?._skeleton
|
|
4289
|
+
?? null;
|
|
4290
|
+
if (directSkeleton)
|
|
4291
|
+
skeletons.add(directSkeleton);
|
|
4292
|
+
if (className === 'Bone' || hasFunction(target, 'getSkeleton') || skeletons.size > 0) {
|
|
4293
|
+
for (const skeleton of skeletons) {
|
|
4294
|
+
for (const mesh of targetIndex.meshesBySkeleton.get(skeleton) ?? [])
|
|
4295
|
+
resolved.add(mesh);
|
|
4296
|
+
}
|
|
4297
|
+
return Object.freeze({ kind: 'animation', meshes: resolved });
|
|
4298
|
+
}
|
|
4299
|
+
const authoredOwner = resolveBabylonNodeAuthoredOwner(target, objectByRenderable);
|
|
4300
|
+
if (authoredOwner) {
|
|
4301
|
+
for (const mesh of targetIndex.meshesByRenderable.get(authoredOwner.renderableId) ?? []) {
|
|
4302
|
+
if (mesh === authoredOwner.node || isBabylonNodeDescendantOf(mesh, authoredOwner.node)) {
|
|
4303
|
+
resolved.add(mesh);
|
|
4304
|
+
}
|
|
4305
|
+
}
|
|
4306
|
+
return Object.freeze({ kind: 'transform', meshes: resolved });
|
|
4307
|
+
}
|
|
4308
|
+
for (const [mesh, binding] of bindings) {
|
|
4309
|
+
if (mesh === target
|
|
4310
|
+
|| isBabylonNodeDescendantOf(mesh, target)
|
|
4311
|
+
|| isBabylonNodeDescendantOf(target, mesh)) {
|
|
4312
|
+
resolved.add(mesh);
|
|
4313
|
+
}
|
|
4314
|
+
}
|
|
4315
|
+
return Object.freeze({ kind: 'transform', meshes: resolved });
|
|
4316
|
+
}
|
|
4317
|
+
function resolveBabylonNodeAuthoredOwner(target, objectByRenderable) {
|
|
4318
|
+
let current = target;
|
|
4319
|
+
while (current && (typeof current === 'object' || typeof current === 'function')) {
|
|
4320
|
+
for (const candidate of collectIdentityCandidates(current)) {
|
|
4321
|
+
if (objectByRenderable.has(candidate)) {
|
|
4322
|
+
return Object.freeze({
|
|
4323
|
+
node: current,
|
|
4324
|
+
renderableId: candidate,
|
|
4325
|
+
});
|
|
4326
|
+
}
|
|
4327
|
+
}
|
|
4328
|
+
current = current.parent ?? null;
|
|
4329
|
+
}
|
|
4330
|
+
return null;
|
|
4331
|
+
}
|
|
4332
|
+
function createAutomaticActivityTargetIndex(bindings, targets) {
|
|
4333
|
+
const meshesByRenderable = new Map();
|
|
4334
|
+
const meshesBySkeleton = new Map();
|
|
4335
|
+
const skeletonsByTarget = new Map();
|
|
4336
|
+
const meshesByMorphTarget = new Map();
|
|
4337
|
+
const indexedSkeletons = new Set();
|
|
4338
|
+
for (const [mesh, binding] of bindings) {
|
|
4339
|
+
addMapSetValue(meshesByRenderable, binding.renderableId, mesh);
|
|
4340
|
+
const skeleton = mesh.skeleton;
|
|
4341
|
+
if (skeleton) {
|
|
4342
|
+
addMapSetValue(meshesBySkeleton, skeleton, mesh);
|
|
4343
|
+
}
|
|
4344
|
+
if (skeleton && !indexedSkeletons.has(skeleton)) {
|
|
4345
|
+
indexedSkeletons.add(skeleton);
|
|
4346
|
+
for (const bone of skeleton.bones ?? []) {
|
|
4347
|
+
if (targets.has(bone))
|
|
4348
|
+
addMapSetValue(skeletonsByTarget, bone, skeleton);
|
|
4349
|
+
const transformNode = callOptional(bone, 'getTransformNode')
|
|
4350
|
+
?? bone?._linkedTransformNode
|
|
4351
|
+
?? null;
|
|
4352
|
+
if (transformNode && targets.has(transformNode)) {
|
|
4353
|
+
addMapSetValue(skeletonsByTarget, transformNode, skeleton);
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
const manager = mesh.morphTargetManager;
|
|
4358
|
+
const count = manager?.numTargets ?? 0;
|
|
4359
|
+
if (!manager?.getTarget || !Number.isInteger(count) || count < 1)
|
|
4360
|
+
continue;
|
|
4361
|
+
for (let index = 0; index < count; index += 1) {
|
|
4362
|
+
let morphTarget;
|
|
4363
|
+
try {
|
|
4364
|
+
morphTarget = manager.getTarget(index);
|
|
4365
|
+
}
|
|
4366
|
+
catch {
|
|
4367
|
+
continue;
|
|
4368
|
+
}
|
|
4369
|
+
if (morphTarget && targets.has(morphTarget)) {
|
|
4370
|
+
addMapSetValue(meshesByMorphTarget, morphTarget, mesh);
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
}
|
|
4374
|
+
return {
|
|
4375
|
+
meshesByRenderable,
|
|
4376
|
+
meshesBySkeleton,
|
|
4377
|
+
skeletonsByTarget,
|
|
4378
|
+
meshesByMorphTarget,
|
|
4379
|
+
};
|
|
4380
|
+
}
|
|
4381
|
+
function addMapSetValue(map, key, value) {
|
|
4382
|
+
const values = map.get(key);
|
|
4383
|
+
if (values) {
|
|
4384
|
+
values.add(value);
|
|
4385
|
+
}
|
|
4386
|
+
else {
|
|
4387
|
+
map.set(key, new Set([value]));
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
function readBabylonClassName(value) {
|
|
4391
|
+
const className = callOptional(value, 'getClassName');
|
|
4392
|
+
return typeof className === 'string' ? className : null;
|
|
4393
|
+
}
|
|
4394
|
+
function isBabylonNodeDescendantOf(value, ancestor) {
|
|
4395
|
+
if (!hasFunction(value, 'isDescendantOf'))
|
|
4396
|
+
return false;
|
|
4397
|
+
try {
|
|
4398
|
+
return value.isDescendantOf(ancestor);
|
|
4399
|
+
}
|
|
4400
|
+
catch {
|
|
4401
|
+
return false;
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
function hasFunction(value, key) {
|
|
4405
|
+
return Boolean(value)
|
|
4406
|
+
&& (typeof value === 'object' || typeof value === 'function')
|
|
4407
|
+
&& typeof value[key] === 'function';
|
|
4408
|
+
}
|
|
4409
|
+
function callOptional(value, key) {
|
|
4410
|
+
if (!hasFunction(value, key))
|
|
4411
|
+
return undefined;
|
|
4412
|
+
try {
|
|
4413
|
+
return value[key]();
|
|
4414
|
+
}
|
|
4415
|
+
catch {
|
|
4416
|
+
return undefined;
|
|
4417
|
+
}
|
|
4418
|
+
}
|
|
4419
|
+
function isStaticDirtyReason(reason) {
|
|
4420
|
+
return reason === 'static-transform-changed'
|
|
4421
|
+
|| reason === 'static-geometry-changed'
|
|
4422
|
+
|| reason === 'static-depth-invalidated';
|
|
4423
|
+
}
|
|
4424
|
+
function isDynamicDirtyReason(reason) {
|
|
4425
|
+
return reason === 'dynamic-transform-changed'
|
|
4426
|
+
|| reason === 'caster-activity-started'
|
|
4427
|
+
|| reason === 'caster-activity-stopped'
|
|
4428
|
+
|| reason === 'dynamic-frame'
|
|
4429
|
+
|| reason === 'skinned-animation-frame'
|
|
4430
|
+
|| reason === 'morph-animation-frame'
|
|
4431
|
+
|| reason === 'physics-motion-frame'
|
|
4432
|
+
|| reason === 'dynamic-depth-invalidated';
|
|
4433
|
+
}
|
|
4434
|
+
function dirtyReasonForActivityKind(kind) {
|
|
4435
|
+
switch (kind) {
|
|
4436
|
+
case 'animation': return 'skinned-animation-frame';
|
|
4437
|
+
case 'morph': return 'morph-animation-frame';
|
|
4438
|
+
case 'physics': return 'physics-motion-frame';
|
|
4439
|
+
case 'transform': return 'dynamic-frame';
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
function captureLight(light) {
|
|
4443
|
+
return {
|
|
4444
|
+
light,
|
|
4445
|
+
direction: light.direction.clone(),
|
|
4446
|
+
position: light.position.clone(),
|
|
4447
|
+
autoUpdateExtends: light.autoUpdateExtends,
|
|
4448
|
+
shadowEnabled: light.shadowEnabled,
|
|
4449
|
+
orthoLeft: light.orthoLeft,
|
|
4450
|
+
orthoRight: light.orthoRight,
|
|
4451
|
+
orthoTop: light.orthoTop,
|
|
4452
|
+
orthoBottom: light.orthoBottom,
|
|
4453
|
+
shadowMinZ: light.shadowMinZ,
|
|
4454
|
+
shadowMaxZ: light.shadowMaxZ,
|
|
4455
|
+
};
|
|
4456
|
+
}
|
|
4457
|
+
function restoreLight(state) {
|
|
4458
|
+
const light = state.light;
|
|
4459
|
+
if (light.isDisposed())
|
|
4460
|
+
return;
|
|
4461
|
+
light.direction.copyFrom(state.direction);
|
|
4462
|
+
light.position.copyFrom(state.position);
|
|
4463
|
+
light.autoUpdateExtends = state.autoUpdateExtends;
|
|
4464
|
+
light.shadowEnabled = state.shadowEnabled;
|
|
4465
|
+
light.orthoLeft = state.orthoLeft;
|
|
4466
|
+
light.orthoRight = state.orthoRight;
|
|
4467
|
+
light.orthoTop = state.orthoTop;
|
|
4468
|
+
light.orthoBottom = state.orthoBottom;
|
|
4469
|
+
light.shadowMinZ = state.shadowMinZ;
|
|
4470
|
+
light.shadowMaxZ = state.shadowMaxZ;
|
|
4471
|
+
}
|
|
4472
|
+
function readShadowGenerators(light) {
|
|
4473
|
+
const generators = light.getShadowGenerators?.();
|
|
4474
|
+
if (!generators)
|
|
4475
|
+
return [];
|
|
4476
|
+
if (Array.isArray(generators))
|
|
4477
|
+
return generators;
|
|
4478
|
+
if (generators instanceof Map)
|
|
4479
|
+
return [...generators.values()];
|
|
4480
|
+
return [];
|
|
4481
|
+
}
|
|
4482
|
+
function isRecord(value) {
|
|
4483
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
4484
|
+
}
|
|
4485
|
+
function attachFailureMetadata(error, metadata) {
|
|
4486
|
+
try {
|
|
4487
|
+
Object.assign(error, metadata);
|
|
4488
|
+
}
|
|
4489
|
+
catch {
|
|
4490
|
+
// Error annotation is diagnostic only; it must never replace the primary failure.
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
function createInitializationFailure(error, cleanup, initializationCleanupErrors) {
|
|
4494
|
+
const metadata = {
|
|
4495
|
+
initializationCleanupErrors: Object.freeze([...initializationCleanupErrors]),
|
|
4496
|
+
shadowMapExperimentSystemCleanup: cleanup,
|
|
4497
|
+
};
|
|
4498
|
+
if (error && (typeof error === 'object' || typeof error === 'function')) {
|
|
4499
|
+
try {
|
|
4500
|
+
Object.assign(error, metadata);
|
|
4501
|
+
if (readBabylonShadowMapExperimentSystemCleanup(error) === cleanup)
|
|
4502
|
+
return error;
|
|
4503
|
+
}
|
|
4504
|
+
catch {
|
|
4505
|
+
// A non-extensible primary error is wrapped below so cleanup ownership remains reachable.
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4508
|
+
const failure = new Error(error instanceof Error ? error.message : 'shadowMapExperiment.initializationFailed');
|
|
4509
|
+
return Object.assign(failure, { cause: error, ...metadata });
|
|
4510
|
+
}
|
|
4511
|
+
function createRuntimeRegistrationRollbackFailure(error, owner, rollbackErrors) {
|
|
4512
|
+
const metadata = {
|
|
4513
|
+
rollbackErrors: Object.freeze([...rollbackErrors]),
|
|
4514
|
+
runtimeShadowObjectsRecoveryOwner: owner,
|
|
4515
|
+
};
|
|
4516
|
+
if (error && (typeof error === 'object' || typeof error === 'function')) {
|
|
4517
|
+
try {
|
|
4518
|
+
Object.assign(error, metadata);
|
|
4519
|
+
if (readBabylonRuntimeShadowObjectsRecoveryOwner(error) === owner)
|
|
4520
|
+
return error;
|
|
4521
|
+
}
|
|
4522
|
+
catch {
|
|
4523
|
+
// A non-extensible primary error is wrapped below so recovery ownership remains reachable.
|
|
4524
|
+
}
|
|
4525
|
+
}
|
|
4526
|
+
const failure = new Error(error instanceof Error ? error.message : 'shadowMapExperiment.runtimeRegistrationFailed');
|
|
4527
|
+
return Object.assign(failure, { cause: error, ...metadata });
|
|
4528
|
+
}
|
|
4529
|
+
//# sourceMappingURL=shadow-map-experiment-system.js.map
|