refactor(app): compose live presentation startup

This commit is contained in:
Erik 2026-07-22 17:55:15 +02:00
parent aa6ffa5176
commit 88f32dc4e2
23 changed files with 1767 additions and 626 deletions

View file

@ -20,7 +20,8 @@ public sealed class GameWindow :
IGameWindowContentEffectsAudioPublication,
IGameWindowSettingsDevToolsPublication,
IGameWindowWorldRenderPublication,
IGameWindowInteractionRetainedUiPublication
IGameWindowInteractionRetainedUiPublication,
IGameWindowLivePresentationPublication
{
private static double ClientTimerNow() =>
System.Diagnostics.Stopwatch.GetTimestamp()
@ -97,6 +98,7 @@ public sealed class GameWindow :
_debugVmRenderFacts = new();
private AcDream.App.Rendering.RenderFrameDiagnosticsController?
_renderFrameDiagnostics;
private LivePresentationRuntimeBindings? _livePresentationBindings;
private AcDream.App.Diagnostics.FrameScreenshotController? _frameScreenshots;
private AcDream.App.Diagnostics.WorldLifecycleAutomationController? _worldLifecycleAutomation;
private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights;
@ -924,6 +926,76 @@ public sealed class GameWindow :
}
}
void IGameWindowLivePresentationPublication.PublishLivePresentation(
LivePresentationResult result)
{
ArgumentNullException.ThrowIfNull(result);
if (_liveEntities is not null
|| _wbEntitySpawnAdapter is not null
|| _entityScriptActivator is not null
|| _staticAnimationScheduler is not null
|| _wbDrawDispatcher is not null
|| _retailSelectionScene is not null
|| _worldSelectionQuery is not null
|| _selectionInteractions is not null
|| _retainedUiGameplayBinding is not null
|| _equippedChildRenderer is not null
|| _entityEffects is not null
|| _liveEntityPresentation is not null
|| _remoteTeleportController is not null
|| _projectileController is not null
|| _liveEntityProjectionWithdrawal is not null
|| _liveEntityLights is not null
|| _liveAnimationScheduler is not null
|| _animationPresenter is not null
|| _paperdollViewportRenderer is not null
|| _paperdollFramePresenter is not null
|| _envCellRenderer is not null
|| _envCellFrustum is not null
|| _landblockPresentationPipeline is not null
|| _portalDepthMask is not null
|| _clipFrame is not null
|| _skyRenderer is not null
|| _particleRenderer is not null
|| _renderFrameDiagnostics is not null
|| _livePresentationBindings is not null)
{
throw new InvalidOperationException(
"The GameWindow composition shell already owns live presentation state.");
}
_wbEntitySpawnAdapter = result.EntitySpawnAdapter;
_entityScriptActivator = result.EntityScriptActivator;
_staticAnimationScheduler = result.StaticAnimationScheduler;
_worldState = result.WorldState;
_liveEntities = result.LiveEntities;
_projectileController = result.ProjectileController;
_liveEntityProjectionWithdrawal = result.ProjectionWithdrawal;
_liveEntityLights = result.Lights;
_liveAnimationScheduler = result.AnimationScheduler;
_animationPresenter = result.AnimationPresenter;
_equippedChildRenderer = result.EquippedChildren;
_entityEffects = result.EntityEffects;
_liveEntityPresentation = result.Presentation;
_remoteTeleportController = result.RemoteTeleport;
_wbDrawDispatcher = result.DrawDispatcher;
_retailSelectionScene = result.SelectionScene;
_worldSelectionQuery = result.SelectionQuery;
_selectionInteractions = result.SelectionInteractions;
_retainedUiGameplayBinding = result.RetainedGameplay;
_paperdollViewportRenderer = result.PaperdollRenderer;
_paperdollFramePresenter = result.PaperdollPresenter;
_envCellFrustum = result.EnvCellFrustum;
_envCellRenderer = result.EnvCellRenderer;
_landblockPresentationPipeline = result.LandblockPipeline;
_clipFrame = result.ClipFrame;
_portalDepthMask = result.PortalDepthMask;
_skyRenderer = result.SkyRenderer;
_particleRenderer = result.ParticleRenderer;
_renderFrameDiagnostics = result.FrameDiagnostics;
_livePresentationBindings = result.RuntimeBindings;
}
private static void PublishCompositionOwner<T>(
ref T? destination,
T value,
@ -1124,532 +1196,52 @@ public sealed class GameWindow :
settingsDevTools,
worldRender);
AcDream.App.World.DeferredLiveEntityParentAcceptance? parentAcceptance = null;
// Phase N.4 Task 12: construct LandblockSpawnAdapter under the feature flag
// and rebuild _worldState so it threads the adapter in. _worldState starts
// as an unadorned GpuWorldState (field initializer); here we replace it with
// one that carries the adapter so AddLandblock/RemoveLandblock notify WB.
// Phase N.4 Task 17: also construct EntitySpawnAdapter for server-spawned
// per-instance content under the same flag.
// N.5 mandatory path: spawn adapters + dispatcher always construct.
// _wbMeshAdapter, _meshShader, _textureCache, and _bindlessSupport are
// all guaranteed non-null here (startup throws above if any are missing).
var liveEntityComponentLifecycle =
new AcDream.App.World.DeferredLiveEntityRuntimeComponentLifecycle();
AcDream.App.Physics.LiveEntityMotionRuntimeController?
liveEntityMotionRuntime = null;
{
var wbSpawnAdapter = new AcDream.App.Rendering.Wb.LandblockSpawnAdapter(_wbMeshAdapter!);
// Sequencer factory: look up Setup + MotionTable from dats and build
// an AnimationSequencer. Falls back to a no-op sequencer when the
// entity has no motion table (static props, etc.). Uses _animLoader
// which is initialised earlier in OnLoad; it is non-null here.
var capturedDats = _dats;
var capturedAnimLoader = _animLoader;
AcDream.Core.Physics.AnimationSequencer SequencerFactory(AcDream.Core.World.WorldEntity e)
{
if (capturedDats is not null && capturedAnimLoader is not null)
{
var setup = capturedDats.Get<DatReaderWriter.DBObjs.Setup>(e.SourceGfxObjOrSetupId);
if (setup is not null)
{
uint mtableId = (uint)setup.DefaultMotionTable;
if (mtableId != 0)
{
var mtable = capturedDats.Get<DatReaderWriter.DBObjs.MotionTable>(mtableId);
if (mtable is not null)
return new AcDream.Core.Physics.AnimationSequencer(setup, mtable, capturedAnimLoader);
}
// Setup exists but no motion table — no-op sequencer.
return new AcDream.Core.Physics.AnimationSequencer(
setup,
new DatReaderWriter.DBObjs.MotionTable(),
capturedAnimLoader);
}
}
// Complete fallback: empty setup + empty motion table + null loader.
return new AcDream.Core.Physics.AnimationSequencer(
new DatReaderWriter.DBObjs.Setup(),
new DatReaderWriter.DBObjs.MotionTable(),
new NullAnimLoader());
}
var wbEntitySpawnAdapter = new AcDream.App.Rendering.Wb.EntitySpawnAdapter(
_textureCache!, SequencerFactory, _wbMeshAdapter!);
_wbEntitySpawnAdapter = wbEntitySpawnAdapter;
// Phase C.1.5a/b: construct EntityScriptActivator so static entities
// (server-spawned AND dat-hydrated) fire Setup.DefaultScript through
// the PhysicsScriptRunner on enter-world. C.1.5b adds per-part
// transforms from the entity's exact current MeshRefs so
// multi-emitter scripts distribute across mesh parts (closes #56).
// Animated frames replace this snapshot through EntityEffectPoseRegistry.
// _scriptRunner and
// _particleSink are initialised earlier in OnLoad (line ~1083); both
// are non-null here. The resolver lambda captures _dats and swallows
// dat-lookup throws — see C.1.5a spec §6 (error handling) for rationale.
AcDream.App.Rendering.Vfx.EntityEffectController? entityEffects = null;
var staticLiveRootCommitter = new StaticLiveRootCommitter(
_liveEntityRuntimeSlot,
_physicsEngine.ShadowObjects,
_liveWorldOrigin,
_effectPoses);
var staticAnimationResidency = new LiveStaticAnimationResidency(
_liveEntityRuntimeSlot);
_staticAnimationScheduler = new RetailStaticAnimatingObjectScheduler(
capturedAnimLoader!,
_animationHookFrames!.Capture,
_effectPoses.Publish,
staticAnimationResidency.IsResident,
(entity, body) =>
_ = staticLiveRootCommitter.Commit(entity, body),
staticAnimationResidency.ProjectionVersion);
AcDream.App.Rendering.Vfx.ScriptActivationInfo? ResolveActivation(AcDream.Core.World.WorldEntity e)
{
try
{
DatReaderWriter.DBObjs.Setup? setup =
capturedDats?.Get<DatReaderWriter.DBObjs.Setup>(
e.SourceGfxObjOrSetupId);
if (setup is null) return null;
uint scriptId = setup.DefaultScript.DataId;
if (e.IndexedPartTransforms.Count == 0)
{
var indexed = AcDream.App.Rendering.Vfx.IndexedSetupPartPoseBuilder.Build(
setup,
e);
e.SetIndexedPartPoses(indexed.Poses, indexed.Available);
}
IReadOnlyList<System.Numerics.Matrix4x4> parts = e.IndexedPartTransforms;
IReadOnlyList<bool> availability = e.IndexedPartAvailable;
var profile = AcDream.App.Rendering.Vfx.EntityEffectProfile.CreateDatStatic(setup);
bool usesStaticAnimationWorkset = e.ServerGuid == 0
|| (_liveEntities?.TryGetRecord(
e.ServerGuid,
out LiveEntityRecord liveRecord) == true
&& (liveRecord.FinalPhysicsState
& AcDream.Core.Physics.PhysicsStateFlags.Static) != 0);
return new AcDream.App.Rendering.Vfx.ScriptActivationInfo(
scriptId,
parts,
profile,
availability,
setup,
(uint)setup.DefaultAnimation,
usesStaticAnimationWorkset);
}
catch
{
return null;
}
}
var entityScriptActivator = new AcDream.App.Rendering.Vfx.EntityScriptActivator(
_scriptRunner!,
_particleSink!,
_effectPoses,
ResolveActivation,
(ownerId, entity, profile) =>
entityEffects?.OnDatStaticEntityReady(ownerId, entity, profile),
ownerId =>
{
entityEffects?.OnDatStaticEntityRemoved(ownerId);
_lightingSink?.UnregisterOwner(ownerId);
_translucencyFades.ClearEntity(ownerId);
},
(entity, info) => _staticAnimationScheduler.Register(entity, info),
ownerId => _staticAnimationScheduler.Unregister(ownerId),
(entity, info) => _staticAnimationScheduler.Rebind(entity, info));
_entityScriptActivator = entityScriptActivator;
// Phase Post-A.5 #53 (Task 12): wire EntityClassificationCache.InvalidateLandblock
// so Tier 1 cache entries get swept on LB demote (Near to Far) and unload.
// Per spec §5.3 W3b. The callback receives the canonical landblock id
// matching the LandblockHint stored at Populate time.
_worldState = new AcDream.App.Streaming.GpuWorldState(
wbSpawnAdapter,
onLandblockUnloaded: _classificationCache.InvalidateLandblock,
entityScriptActivator: entityScriptActivator);
_devToolsComposition?.LateBindings.WorldEntities.Bind(_worldState);
_liveEntities = new AcDream.App.World.LiveEntityRuntime(
_worldState,
new AcDream.App.World.CompositeLiveEntityResourceLifecycle(
new(
entity => wbEntitySpawnAdapter.OnCreate(entity),
entity => wbEntitySpawnAdapter.OnRemove(entity)),
new(
entityScriptActivator.OnCreate,
entityScriptActivator.OnRemove)),
liveEntityComponentLifecycle);
_liveEntityRuntimeSlot.Bind(_liveEntities);
liveEntityMotionRuntime =
new AcDream.App.Physics.LiveEntityMotionRuntimeController(
_liveEntities,
LivePresentationResult livePresentation =
new LivePresentationCompositionPhase(
new LivePresentationDependencies(
_options,
platform.Graphics,
_window!,
_datLock,
_runtimeSettings,
_hostQuiescence,
_physicsEngine,
_physicsDataCache,
() => _selectionInteractions,
_selection,
_liveWorldOrigin);
_liveEntityMotionBindings.Bind(liveEntityMotionRuntime);
_liveEntities.ProjectionVisibilityChanged += (record, visible) =>
{
if (record.WorldEntity is { } entity)
wbEntitySpawnAdapter.SetPresentationResident(entity, visible);
};
_liveEntities.ProjectionVisibilityChanged += (record, visible) =>
{
if (record.WorldEntity is { } entity)
_particleSink!.SetEntityPresentationVisible(entity.Id, visible);
};
_projectileController = new AcDream.App.Physics.ProjectileController(
_liveEntities,
_physicsEngine,
new AcDream.App.Physics.DatProjectileSetupResolver(_dats!, _datLock),
new EntityRootPosePublisher(_effectPoses),
_liveWorldOrigin)
{
DiagnosticSink = message => Console.Error.WriteLine($"projectile: {message}"),
};
_liveEntityProjectionWithdrawal =
new AcDream.App.World.LiveEntityProjectionWithdrawalController(
_liveEntities,
_projectileController,
_worldGameState,
_worldEvents,
_physicsEngine.ShadowObjects,
_effectPoses,
_localPlayerShadow);
_liveEntityLights = new AcDream.App.Rendering.Vfx.LiveEntityLightController(
_liveEntities,
_effectPoses,
_lightingSink!,
setupId => _dats!.Get<DatReaderWriter.DBObjs.Setup>(setupId));
var ordinaryPhysicsUpdater =
new AcDream.App.Physics.LiveEntityOrdinaryPhysicsUpdater(
_physicsEngine,
_liveEntityMotionBindings.GetSetupCylinder);
_liveAnimationScheduler = new LiveEntityAnimationScheduler(
_liveEntities,
_localPlayerIdentity,
_remotePhysicsUpdater,
ordinaryPhysicsUpdater,
_projectileController,
new EntityRootPosePublisher(_effectPoses),
new AnimationHookCaptureSink(_animationHookFrames!));
_animationPresenter = new LiveEntityAnimationPresenter(
_liveEntities,
_staticAnimationScheduler,
_effectPoses,
new LiveAnimationPresentationContext(
_liveEntities,
_localPlayerIdentity,
_playerControllerSlot),
_animationDiagnostics,
_options.HidePartIndex);
parentAcceptance = new AcDream.App.World.DeferredLiveEntityParentAcceptance();
_equippedChildRenderer = new AcDream.App.Rendering.EquippedChildRenderController(
_dats!,
_datLock,
Objects,
_liveEntities,
_effectPoses,
parentAcceptance.TryAccept,
(childRecord, positionVersion, projectionVersion) =>
_liveEntityProjectionWithdrawal!.WithdrawExact(
childRecord,
positionVersion,
projectionVersion,
_playerServerGuid));
var tableResolver = new AcDream.Core.Vfx.PhysicsScriptTableResolver(
id => _dats!.Get<DatReaderWriter.DBObjs.PhysicsScriptTable>(id));
entityEffects = new AcDream.App.Rendering.Vfx.EntityEffectController(
_liveEntities,
_scriptRunner!,
tableResolver,
_effectPoses,
(parentLocalId, partIndex) =>
_equippedChildRenderer.FindChildLocalIdAtPart(parentLocalId, partIndex),
childLocalId => _equippedChildRenderer.FindParentLocalId(childLocalId),
ownerId => _entitySoundTables?.Remove(ownerId),
(ownerId, soundTableDid) =>
{
_entitySoundTables?.Remove(ownerId);
if (soundTableDid is { } did)
_entitySoundTables?.Set(ownerId, did);
});
_entityEffects = entityEffects;
_entityEffectAdvance.Bind(entityEffects);
entityEffects.DiagnosticSink = message =>
Console.Error.WriteLine($"vfx: {message}");
var partArrayLifecycle =
new AcDream.App.Rendering.LiveEntityPartArrayLifecycle(
_animatedEntities);
_liveEntityPresentation = new AcDream.App.World.LiveEntityPresentationController(
_liveEntities,
_physicsEngine.ShadowObjects,
entityEffects.PlayTypedFromHiddenTransition,
_equippedChildRenderer.SetDirectChildrenNoDraw,
_liveEntityMotionBindings.ClearTargetForHiddenEntity,
_liveWorldOrigin.GetCenter,
handlePartArrayEnterWorld: partArrayLifecycle.HandleEnterWorld);
var remoteShadowPlacement =
new AcDream.App.Physics.RemoteShadowPlacementSynchronizer(
_remotePhysicsUpdater,
_liveWorldOrigin);
var remoteTeleportPresentation =
new AcDream.App.Physics.RemoteTeleportPlacementPresentation(
_liveEntityPresentation);
_remoteTeleportController = new AcDream.App.Physics.RemoteTeleportController(
_physicsEngine,
_liveEntities,
_liveEntityMotionBindings.GetSetupCylinder,
_liveWorldOrigin.CellLocalForSeed,
remoteShadowPlacement.Sync,
remoteTeleportPresentation.Complete,
remoteTeleportPresentation.Begin);
_equippedChildRenderer.ProjectionPoseReady += guid =>
_liveEntityLights.OnAttachedPoseReady(guid);
_hookRegistrations!.Register(entityEffects);
_wbDrawDispatcher = new AcDream.App.Rendering.Wb.WbDrawDispatcher(
_gl, _meshShader!, _textureCache!, _wbMeshAdapter!, _wbEntitySpawnAdapter, _bindlessSupport!,
_classificationCache, _translucencyFades,
_retailSelectionScene ??= new AcDream.App.Rendering.Selection.RetailSelectionScene(
new AcDream.App.Rendering.Selection.RetailSelectionGeometryCache(
_dats!, _datLock)),
_retailAlphaQueue);
_worldSelectionQuery ??= new AcDream.App.Interaction.WorldSelectionQuery(
_liveEntities,
Objects,
_retailSelectionScene,
() => _localPlayerIdentity.ServerGuid,
interactionUi.LateBindings.SelectionCamera.Snapshot,
() => new System.Numerics.Vector2(
_pointerPosition.X,
_pointerPosition.Y),
() => _playerControllerSlot.Controller is { } player
? new AcDream.App.Interaction.PlayerInteractionPose(
player.CellId,
player.Position)
: null,
_liveEntityMotionBindings.GetSetupCylinder,
setupId =>
{
lock (_datLock)
{
if (!contentEffectsAudio.Dats.TryGet<DatReaderWriter.DBObjs.Setup>(
setupId,
out var setup)
|| setup.SelectionSphere is not { } sphere)
{
return null;
}
return (sphere.Origin, sphere.Radius);
}
});
var radarSnapshotProvider = new AcDream.App.UI.Layout.RadarSnapshotProvider(
Objects,
() => _liveEntities.WorldEntities,
() => _liveEntities.Snapshots,
playerGuid: () => _localPlayerIdentity.ServerGuid,
playerYawRadians: () => _playerControllerSlot.Controller?.Yaw ?? 0f,
playerCellId: () => _playerControllerSlot.Controller?.CellId ?? 0u,
selectedGuid: () => _selection.SelectedObjectId,
coordinatesOnRadar: () =>
_runtimeSettings.Gameplay.CoordinatesOnRadar,
uiLocked: () => _runtimeSettings.Gameplay.LockUI,
playerEntities: () => _liveEntities.MaterializedWorldEntities,
spatialQuery: () => _worldState);
interactionUi.LateBindings.AdoptLateOwnerBinding(
"radar snapshot",
interactionUi.LateBindings.Radar.Bind(radarSnapshotProvider));
if (_itemInteractionController is { } itemInteractions)
{
_selectionInteractions ??= new AcDream.App.Interaction.SelectionInteractionController(
_selection,
_worldSelectionQuery,
itemInteractions,
new AcDream.App.Interaction.WorldSessionSelectionInteractionTransport(
() => interactionUi.LateBindings.Session.CurrentSession),
new AcDream.App.Interaction.PlayerInteractionMovementSink(
() => _playerControllerSlot.Controller,
_playerApproachCompletions),
compositionToast,
_playerApproachCompletions);
interactionUi.LateBindings.AdoptLateOwnerBinding(
"world selection",
interactionUi.LateBindings.Selection.Bind(
_worldSelectionQuery,
_selectionInteractions));
}
if (_uiHost is { } retainedHost
&& _selectionInteractions is { } selectionInteractions)
{
_retainedUiGameplayBinding =
AcDream.App.Input.RetainedUiGameplayBinding.Create(
retainedHost.Root,
(item, x, y) =>
selectionInteractions.PlaceDraggedItem(item, x, y),
_hostQuiescence);
_retainedUiGameplayBinding.Attach();
}
// A.5 T22.5: apply A2C gate from quality preset.
_wbDrawDispatcher.AlphaToCoverage =
_runtimeSettings.ResolvedQuality.AlphaToCoverage;
// Slice 2: now that the dispatcher exists, build the paperdoll doll RTT renderer and hand it to
// the viewport widget. Reuses the dispatcher + the scene-lighting UBO + _gl. The widget only
// blits the texture; the pre-UI hook drives the 3-D pass (see OnRender).
if (_retailUiRuntime?.PaperdollViewportWidget is { } paperdollViewport
&& _retailUiRuntime.InventoryFrame is { } paperdollInventoryFrame
&& _sceneLightingUbo is not null)
{
_paperdollViewportRenderer = new AcDream.App.Rendering.PaperdollViewportRenderer(
_gl, _wbDrawDispatcher, _sceneLightingUbo, _textureCache!);
paperdollViewport.Renderer = _paperdollViewportRenderer;
_paperdollFramePresenter =
new AcDream.App.Rendering.PaperdollFramePresenter(
_paperdollViewportRenderer,
new AcDream.App.Rendering.RetailPaperdollFrameView(
paperdollViewport,
new AcDream.App.Rendering.PaperdollInventoryVisibility(
paperdollInventoryFrame)),
new AcDream.App.Rendering.RetailPaperdollDollFactory(
new AcDream.App.Rendering.LivePaperdollEntityLookup(
_liveEntities),
_localPlayerIdentity,
new AcDream.App.Rendering.RetailPaperdollPoseApplicator(
_dats!,
_animLoader!,
_datLock)));
}
// Phase A8: EnvCellRenderer init. Shares _meshShader (mesh_modern.{vert,frag})
// with the dispatcher — both consume the same global mesh buffer (VAO/IBO)
// from ObjectMeshManager.GlobalBuffer.
_envCellFrustum = new AcDream.App.Rendering.Wb.WbFrustum();
_envCellRenderer = new AcDream.App.Rendering.Wb.EnvCellRenderer(
_gl, _wbMeshAdapter!.MeshManager!, _envCellFrustum);
_envCellRenderer.Initialize(_meshShader!);
var landblockRenderPublisher = new AcDream.App.Streaming.LandblockRenderPublisher(
publishTerrain: (landblockId, meshData, origin) =>
_terrain!.AddLandblockWithMesh(landblockId, meshData, origin),
removeTerrain: landblockId => _terrain!.RemoveLandblock(landblockId),
cellVisibility: _cellVisibility,
worldState: _worldState,
commitEnvCells: _envCellRenderer.CommitLandblock,
prepareEnvCells: build =>
AcDream.App.Rendering.Wb.EnvCellMeshPreparationScheduler.Schedule(
build,
_wbMeshAdapter.MeshManager!),
removeEnvCells: _envCellRenderer.RemoveLandblock);
var landblockPhysicsPublisher =
new AcDream.App.Streaming.LandblockPhysicsPublisher(
_physicsEngine,
_heightTable!);
var landblockStaticPresentationPublisher =
new AcDream.App.Streaming.LandblockStaticPresentationPublisher(
_lightingSink!,
_translucencyFades,
_worldGameState,
_worldEvents);
var landblockRetirementOwner =
new AcDream.App.Streaming.LandblockPresentationRetirementOwner(
landblockRenderPublisher,
landblockPhysicsPublisher,
landblockStaticPresentationPublisher,
_lightingSink!,
_translucencyFades);
_landblockPresentationPipeline =
new AcDream.App.Streaming.LandblockPresentationPipeline(
landblockRenderPublisher,
landblockPhysicsPublisher,
landblockStaticPresentationPublisher,
_worldState,
landblockRetirementOwner,
onLandblockLoaded: loadedLandblockId =>
_liveEntityHydration!.OnLandblockLoaded(loadedLandblockId),
ensureEnvCellMeshes:
landblockRenderPublisher.PrepareAfterRenderPins);
_clipFrame ??= ClipFrame.NoClip();
// T1: invisible portal depth writes (seal/punch) — retail
// DrawPortalPolyInternal (Ghidra 0x0059bc90).
_portalDepthMask = new AcDream.App.Rendering.PortalDepthMaskRenderer(_gl);
_portalTunnelFallback.AcquirePrepared(
() => AcDream.App.Rendering.PortalTunnelPresentation.CreateRequired(
_gl,
_dats!,
_animLoader!,
_hookRouter,
_wbDrawDispatcher,
_sceneLightingUbo!,
_wbMeshAdapter!),
static portalTunnel => portalTunnel.PrepareResources());
// Publish the presentation before touching the mesh-reference
// backend. Partial acquisition remains reachable by staged
// shutdown instead of becoming constructor-local leaked state.
}
// Phase G.1 sky renderer — its own shader (sky.vert / sky.frag)
// with depth writes off + far plane 1e6 so celestial meshes
// never clip. Shares the TextureCache with the static pipeline.
var skyShader = _renderResourceLifetime.AcquireSkyShader(
() => new Shader(
_gl,
Path.Combine(shadersDir, "sky.vert"),
Path.Combine(shadersDir, "sky.frag")));
_skyRenderer = new AcDream.App.Rendering.Sky.SkyRenderer(
_gl,
contentEffectsAudio.Dats,
skyShader,
_textureCache!,
_samplerCache!);
// Phase G.1 particle renderer — renders rain / snow / spell auras
// spawned into the shared ParticleSystem. Mode-1/no-degrade GfxObjs
// retain their authored mesh; Always2D degrade modes use billboards.
// Weather uses AttachLocal emitters so its volume follows the player.
_particleRenderer = new ParticleRenderer(
_gl,
shadersDir,
contentEffectsAudio.ParticleSystem,
_textureCache,
contentEffectsAudio.Dats,
_wbMeshAdapter,
_retailAlphaQueue);
AcDream.App.Rendering.IRenderFrameResourceDiagnosticsSource? resourceDiagnostics =
_options.UiProbeDump
? new AcDream.App.Rendering.RuntimeRenderFrameResourceDiagnosticsSource(
_particleSystem,
_particleSink,
_wbDrawDispatcher,
_envCellRenderer,
_particleRenderer,
_uiHost?.TextRenderer,
_portalDepthMask,
_clipFrame,
_terrain,
_sceneLightingUbo,
_wbMeshAdapter,
_textureCache)
: null;
_renderFrameDiagnostics =
new AcDream.App.Rendering.RenderFrameDiagnosticsController(
new AcDream.App.Rendering.RuntimeRenderFrameTitleFactsSource(
_worldState,
Objects,
_liveEntityRuntimeSlot,
_liveEntityMotionBindings,
_entityEffectAdvance,
_effectPoses,
_remotePhysicsUpdater,
_localPlayerShadow,
_animatedEntities,
WorldTime),
new AcDream.App.Rendering.SilkRenderFrameTitleSink(_window!),
_renderDiagnosticLog,
_options.UiProbeDump,
resourceDiagnostics);
_devToolsComposition?.LateBindings.FrameDiagnostics.Bind(
_renderFrameDiagnostics);
interactionUi.LateBindings.AdoptLateOwnerBinding(
"render-frame diagnostics",
_uiFrameDiagnostics.BindOwned(_renderFrameDiagnostics));
_animationDiagnostics,
_classificationCache,
_translucencyFades,
_retailAlphaQueue,
_cellVisibility,
_liveWorldOrigin,
_localPlayerIdentity,
_playerControllerSlot,
_pointerPosition,
_playerApproachCompletions,
_renderResourceLifetime,
_portalTunnelFallback,
_hookRouter,
_renderDiagnosticLog,
WorldTime,
settingsDevTools.DevTools?.LateBindings.WorldEntities,
settingsDevTools.DevTools?.LateBindings.FrameDiagnostics,
_uiFrameDiagnostics,
compositionToast),
this).Compose(
hostInputCamera,
contentEffectsAudio,
worldRender,
interactionUi);
// Apply radii from the same immutable quality snapshot used for the
// window's MSAA, terrain anisotropy, and dispatcher A2C setup.
@ -1792,7 +1384,7 @@ public sealed class GameWindow :
_liveSessionController);
var liveEntityTeardown =
new AcDream.App.World.LiveEntityRuntimeTeardownController(
_liveEntities,
livePresentation.LiveEntities,
_liveEntityPresentation!,
_entityEffects!,
_remoteTeleportController!,
@ -1807,23 +1399,23 @@ public sealed class GameWindow :
_liveEntityLights!,
_classificationCache,
_localPlayerIdentity);
liveEntityComponentLifecycle.Bind(liveEntityTeardown);
livePresentation.ComponentLifecycle.Bind(liveEntityTeardown);
var liveEntityDeletion =
new AcDream.App.World.LiveEntityDeletionController(
_liveEntities,
livePresentation.LiveEntities,
Objects,
liveEntityTeardown,
_localPlayerIdentity,
_options.DumpLiveSpawns ? Console.WriteLine : null);
_liveEntityHydration = new AcDream.App.World.LiveEntityHydrationController(
_liveEntities,
livePresentation.LiveEntities,
Objects,
_datLock,
projectionMaterializer,
new AcDream.App.World.LiveEntityRelationshipProjection(
_equippedChildRenderer),
livePresentation.EquippedChildren),
new AcDream.App.World.LiveEntityReadyPublisher(
_liveEntities,
livePresentation.LiveEntities,
_entityEffects!,
_liveEntityPresentation!),
originCoordinator,
@ -1832,9 +1424,12 @@ public sealed class GameWindow :
_localPlayerIdentity,
liveEntityDeletion,
_options.DumpLiveSpawns ? Console.WriteLine : null);
livePresentation.RuntimeBindings.Adopt(
"landblock-loaded hydration",
livePresentation.LandblockLoaded.Bind(_liveEntityHydration));
_liveEntityNetworkUpdates =
new AcDream.App.Physics.LiveEntityNetworkUpdateController(
_liveEntities,
livePresentation.LiveEntities,
Objects,
_liveEntityHydration,
_entityEffects!,
@ -1849,7 +1444,7 @@ public sealed class GameWindow :
_remoteMovementObservations,
_remotePhysicsUpdater,
_remoteInboundMotion,
liveEntityMotionRuntime!,
livePresentation.MotionRuntime,
_physicsEngine,
_dats!,
_animLoader!,
@ -1865,7 +1460,7 @@ public sealed class GameWindow :
localPhysicsTimestamps.Publish,
_movementTruthDiagnostics);
_liveEntityLiveness = new AcDream.App.World.LiveEntityLivenessController(
_liveEntities,
livePresentation.LiveEntities,
_localPlayerIdentity,
liveEntityDeletion);
_liveEntitySessionEvents = new AcDream.App.Net.LiveEntitySessionController(
@ -1874,9 +1469,10 @@ public sealed class GameWindow :
_liveEntityNetworkUpdates,
_localPlayerTeleportSink,
_entityEffects!);
parentAcceptance!.Bind(_liveEntityHydration.TryAcceptParentForProjection);
livePresentation.ParentAcceptance.Bind(
_liveEntityHydration.TryAcceptParentForProjection);
networkUpdateBridge.Bind(_liveEntityNetworkUpdates);
_equippedChildRenderer.EntityReady += candidate =>
livePresentation.EquippedChildren.EntityReady += candidate =>
_liveEntityHydration.OnEntityReady(candidate);
_liveEntityHydration.AppearanceApplied += guid =>
{
@ -1940,7 +1536,7 @@ public sealed class GameWindow :
_streamingController!,
new AcDream.App.Streaming.LiveProjectionRescueRebucketter(
_worldState,
_liveEntities));
livePresentation.LiveEntities));
var liveEffectFrame = new AcDream.App.Update.LiveEffectFrameController(
_translucencyFades,
_animationHookFrames!,
@ -1961,7 +1557,7 @@ public sealed class GameWindow :
_liveEntityLights!);
_localPlayerAnimation =
new AcDream.App.Input.LocalPlayerAnimationController(
_liveEntities,
livePresentation.LiveEntities,
_localPlayerIdentity,
_animatedEntities,
_animationPresenter,
@ -1969,14 +1565,14 @@ public sealed class GameWindow :
_localPlayerShadowSynchronizer =
new AcDream.App.Physics.LocalPlayerShadowSynchronizer(
_physicsEngine,
_liveEntities,
livePresentation.LiveEntities,
_localPlayerIdentity,
_liveWorldOrigin,
_localPlayerShadow);
var localPlayerProjection =
new AcDream.App.Input.LocalPlayerProjectionController(
new AcDream.App.Input.LiveLocalPlayerProjectionRuntime(
_liveEntities,
livePresentation.LiveEntities,
_localPlayerIdentity,
_liveWorldOrigin,
_localPlayerShadowSynchronizer));
@ -1988,7 +1584,7 @@ public sealed class GameWindow :
_chaseCameraInput,
_movementInput,
_inputCapture,
_liveEntities,
livePresentation.LiveEntities,
_localPlayerIdentity,
_playerHostSlot,
localPlayerProjection,
@ -2002,11 +1598,11 @@ public sealed class GameWindow :
_inboundEntityEvents,
localPlayerFrame,
_selectionInteractions,
_liveEntities,
livePresentation.LiveEntities,
_localPlayerIdentity,
_liveWorldOrigin,
_liveAnimationScheduler,
_staticAnimationScheduler,
livePresentation.StaticAnimationScheduler,
_animationPresenter,
_animatedEntities,
_equippedChildRenderer!,
@ -2018,7 +1614,7 @@ public sealed class GameWindow :
_chaseCameraInput,
_cameraController!,
_physicsEngine,
_liveEntities,
livePresentation.LiveEntities,
_localPlayerIdentity,
_liveWorldOrigin,
_liveEntityMotionBindings,
@ -2040,7 +1636,7 @@ public sealed class GameWindow :
_playerModeAutoEntry = new AcDream.App.Input.PlayerModeAutoEntry(
new AcDream.App.Input.LivePlayerModeAutoEntryContext(
_liveSessionController,
_liveEntities,
livePresentation.LiveEntities,
_localPlayerIdentity,
_worldReveal
?? throw new InvalidOperationException(
@ -2051,7 +1647,7 @@ public sealed class GameWindow :
_localPlayerTeleport = _portalTunnelFallback.Transfer(
portalTunnel => new AcDream.App.Streaming.LocalPlayerTeleportController(
new AcDream.App.Streaming.LiveLocalPlayerTeleportAuthority(
_liveEntities,
livePresentation.LiveEntities,
_localPlayerIdentity),
_gameplayInputFrame,
_playerModeController,
@ -2063,7 +1659,7 @@ public sealed class GameWindow :
_worldReveal,
new AcDream.App.Streaming.LocalPlayerTeleportPlacement(
_physicsEngine,
_liveEntities,
livePresentation.LiveEntities,
_localPlayerIdentity,
_playerControllerSlot,
_playerHostSlot,
@ -2323,7 +1919,7 @@ public sealed class GameWindow :
_worldSelectionQuery!));
var updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(
new AcDream.App.Update.LiveEntityTeardownFramePhase(
_liveEntities),
livePresentation.LiveEntities),
new AcDream.App.Update.ConsoleUpdateFrameFailureSink(),
_updateFrameClock,
new AcDream.App.Update.PhysicsScriptClockPublisher(_scriptRunner!),
@ -3039,6 +2635,15 @@ public sealed class GameWindow :
]),
new ResourceShutdownStage("effect dispatch edges",
[
new("live-presentation bindings", () =>
{
LivePresentationRuntimeBindings? bindings =
_livePresentationBindings;
if (bindings is null)
return;
bindings.Dispose();
_livePresentationBindings = null;
}),
new("entity-effect advance source", () =>
{
if (_entityEffects is { } effects)
@ -3232,15 +2837,4 @@ public sealed class GameWindow :
_window = null;
}
/// <summary>
/// Fallback <see cref="AcDream.Core.Physics.IAnimationLoader"/> for the
/// <see cref="AcDream.App.Rendering.Wb.EntitySpawnAdapter"/> sequencer
/// factory when neither <c>_dats</c> nor the entity's setup is available.
/// Returns null for all animation lookups so the sequencer silently has
/// no data (same behaviour as a new empty Setup).
/// </summary>
private sealed class NullAnimLoader : AcDream.Core.Physics.IAnimationLoader
{
public DatReaderWriter.DBObjs.Animation? LoadAnimation(uint id) => null;
}
}