refactor(runtime): cut graphical host over to canonical root

Make every App composition phase borrow one GameRuntime, retire the duplicate view/event adapters, and dispose the root only after its graphical borrowers release. This preserves synchronous UI commands while giving shutdown one exact ownership ledger.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-26 19:06:09 +02:00
parent ecb9f79444
commit ce41efb9e5
29 changed files with 613 additions and 778 deletions

View file

@ -6,6 +6,7 @@ using AcDream.App.Rendering.Wb;
using AcDream.App.Settings;
using AcDream.App.World;
using AcDream.Content;
using AcDream.Runtime;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Session;
@ -163,7 +164,7 @@ public sealed class GameWindow :
private readonly AcDream.App.Input.MovementTruthDiagnosticController
_movementTruthDiagnostics;
private readonly LocalPlayerOutboundController _localPlayerOutbound;
private readonly AcDream.App.Update.UpdateFrameClock _updateFrameClock = new();
private readonly AcDream.App.Update.UpdateFrameClock _updateFrameClock;
private AcDream.App.Physics.RemoteTeleportController? _remoteTeleportController;
// Step 7 projectile presentation. The controller owns no identity map;
// each runtime component is stored on the canonical LiveEntityRecord.
@ -325,8 +326,11 @@ public sealed class GameWindow :
// J4/J5.1: Runtime owns communication, selection, combat, and target-mode
// state. App, UI, plugins, and live-session routing borrow exact children.
private readonly RuntimeCommunicationState _runtimeCommunication = new();
private readonly RuntimeActionState _runtimeActions;
private readonly GameRuntime _runtime;
private readonly IDisposable _runtimeHostLease;
private RuntimeCommunicationState _runtimeCommunication =>
_runtime.CommunicationOwner;
private RuntimeActionState _runtimeActions => _runtime.ActionOwner;
public AcDream.Core.Selection.SelectionState Selection =>
_runtimeActions.Selection;
public AcDream.Core.Chat.ChatLog Chat => _runtimeCommunication.Chat;
@ -338,9 +342,12 @@ public sealed class GameWindow :
_runtimeCommunication.Squelch;
public AcDream.Core.Combat.CombatState Combat =>
_runtimeActions.Combat;
private readonly RuntimeEntityObjectLifetime _runtimeEntityObjects = new();
private readonly RuntimeInventoryState _runtimeInventory;
private readonly RuntimeCharacterState _runtimeCharacter;
private RuntimeEntityObjectLifetime _runtimeEntityObjects =>
_runtime.EntityObjects;
private RuntimeInventoryState _runtimeInventory =>
_runtime.InventoryOwner;
private RuntimeCharacterState _runtimeCharacter =>
_runtime.CharacterOwner;
public AcDream.Core.Items.ItemManaState ItemMana =>
_runtimeInventory.ItemMana;
// Retail resolves learned IDs through portal.dat's CSpellTable. MagicCatalog
@ -431,7 +438,8 @@ public sealed class GameWindow :
private AcDream.App.Rendering.SceneLightingUboBinding? _sceneLightingUbo;
private AcDream.App.Rendering.Sky.SkyRenderer? _skyRenderer;
// Phase B.2: player movement mode.
private readonly RuntimeLocalPlayerMovementState _playerControllerSlot = new();
private RuntimeLocalPlayerMovementState _playerControllerSlot =>
_runtime.MovementOwner;
private PlayerMovementController? _playerController
=> _playerControllerSlot.Controller;
private readonly AcDream.App.Input.ChaseCameraInputState _chaseCameraInput = new();
@ -442,7 +450,8 @@ public sealed class GameWindow :
private readonly AcDream.App.Input.LocalPlayerModeState _localPlayerMode = new();
private bool _playerMode
=> _localPlayerMode.IsPlayerMode;
private readonly AcDream.App.Input.LocalPlayerIdentityState _localPlayerIdentity = new();
private readonly AcDream.App.Input.LocalPlayerIdentityState
_localPlayerIdentity;
private uint _playerServerGuid
{
get => _localPlayerIdentity.ServerGuid;
@ -510,7 +519,6 @@ public sealed class GameWindow :
// Runtime owns the canonical session generation and transport lifetime.
// The window retains only the composition handles needed for startup and
// shutdown; graphical feature callbacks borrow state through App adapters.
private LiveSessionController? _liveSessionController;
private LiveSessionHost? _liveSessionHost;
private AcDream.Core.Net.WorldSession? LiveSession =>
_liveSessionHost?.CurrentSession;
@ -565,22 +573,24 @@ public sealed class GameWindow :
AcDream.App.Plugins.BufferedUiRegistry? uiRegistry = null)
{
_options = options ?? throw new System.ArgumentNullException(nameof(options));
_worldEnvironment = new AcDream.App.World.WorldEnvironmentController(
new AcDream.Runtime.World.RuntimeWorldEnvironmentState(
log: Console.WriteLine,
timeSyncDiagnostic:
options.DumpSky ? Console.WriteLine : null),
options.ForcedDayGroupIndex,
Console.WriteLine);
_runtimeInventory = new RuntimeInventoryState(_runtimeEntityObjects);
_runtimeCharacter = new RuntimeCharacterState();
_runtimeActions = new RuntimeActionState(
_runtimeInventory.Transactions,
_runtimeCharacter.Spellbook,
_runtime = new GameRuntime(new GameRuntimeDependencies(
_combatAttackOperations,
_combatTargetOperations,
_combatModeOperations,
_spellCastOperations);
_spellCastOperations,
Log: Console.WriteLine,
TimeSyncDiagnostic:
options.DumpSky ? Console.WriteLine : null));
_runtimeHostLease = _runtime.AcquireHostLease(
"graphical GameWindow");
_localPlayerIdentity = new AcDream.App.Input.LocalPlayerIdentityState(
_runtime.PlayerIdentity);
_updateFrameClock = new AcDream.App.Update.UpdateFrameClock(
_runtime.Clock);
_worldEnvironment = new AcDream.App.World.WorldEnvironmentController(
_runtime.EnvironmentOwner,
options.ForcedDayGroupIndex,
Console.WriteLine);
var alphaScratchBudgets =
AcDream.App.Rendering.Residency.AlphaScratchBudgetProfile.Create(
_options.ResidencyBudgets.AlphaScratchBytes);
@ -1039,7 +1049,6 @@ public sealed class GameWindow :
|| _streamingOriginRecenter is not null
|| _worldReveal is not null
|| _spawnClaimHydration is not null
|| _liveSessionController is not null
|| _liveEntityHydration is not null
|| _liveEntityNetworkUpdates is not null
|| _liveEntityLiveness is not null
@ -1063,7 +1072,6 @@ public sealed class GameWindow :
_streamingOriginRecenter = result.StreamingOriginRecenter;
_worldReveal = result.WorldReveal;
_spawnClaimHydration = result.SpawnClaimHydration;
_liveSessionController = result.LiveSession;
_liveEntityHydration = result.Hydration;
_liveEntityNetworkUpdates = result.NetworkUpdates;
_liveEntityLiveness = result.Liveness;
@ -1163,7 +1171,7 @@ public sealed class GameWindow :
_options.ResidencyBudgets,
_physicsDataCache,
_animationDiagnostics.DumpMotionEnabled,
_runtimeCharacter,
_runtime,
_hookRouter,
_effectPoses,
_entityEffectAdvance,
@ -1221,9 +1229,7 @@ public sealed class GameWindow :
hostInputCamera.CameraController,
contentEffectsAudio.Audio?.Engine),
_hostQuiescence,
_runtimeCommunication,
Combat,
LocalPlayer,
_runtime,
optionalDevTools,
_runtimeDiagnosticCommands,
_combatFeedback,
@ -1270,19 +1276,15 @@ public sealed class GameWindow :
_retainedInputCapture,
hostInputCamera.InputDispatcher,
_runtimeSettings,
_runtimeActions,
_runtime,
_combatAttackOperations,
_combatTargetOperations,
_spellCastOperations,
_runtimeInventory,
contentEffectsAudio.MagicCatalog,
_runtimeCharacter,
_runtimeCommunication,
_stackSplitQuantity,
_uiRegistry,
_liveCombatModeCommands,
_localPlayerIdentity,
_playerControllerSlot,
_localPlayerMode,
viewPlane => new SelectionCameraSource(
hostInputCamera.CameraController,
@ -1323,8 +1325,7 @@ public sealed class GameWindow :
_physicsDataCache,
_worldGameState,
_worldEvents,
Selection,
_runtimeEntityObjects,
_runtime,
_liveEntityRuntimeSlot,
_liveEntityMotionBindings,
_entityEffectAdvance,
@ -1339,7 +1340,6 @@ public sealed class GameWindow :
_cellVisibility,
_liveWorldOrigin,
_localPlayerIdentity,
_playerControllerSlot,
_pointerPosition,
_playerApproachCompletions,
_renderResourceLifetime,
@ -1369,6 +1369,7 @@ public sealed class GameWindow :
new SessionPlayerCompositionPhase(
new SessionPlayerDependencies(
_options,
_runtime,
_window!,
_datLock,
_runtimeSettings,
@ -1383,8 +1384,6 @@ public sealed class GameWindow :
_physicsDataCache,
_worldGameState,
_worldEvents,
_runtimeActions,
_runtimeEntityObjects,
_classificationCache,
_liveEntityRuntimeSlot,
_animatedEntities,
@ -1394,7 +1393,6 @@ public sealed class GameWindow :
_inboundEntityEvents,
_liveEntityMotionBindings,
_localPlayerIdentity,
_playerControllerSlot,
_playerHostSlot,
_localPlayerMode,
_chaseCameraInput,
@ -1405,7 +1403,6 @@ public sealed class GameWindow :
_localPlayerShadow,
_viewportAspect,
_playerApproachCompletions,
_runtimeInventory,
_pointerPosition,
_movementInput,
_inputCapture,
@ -1416,8 +1413,6 @@ public sealed class GameWindow :
_movementTruthDiagnostics,
_combatAttackOperations,
_combatFeedback,
_runtimeCommunication,
_runtimeCharacter,
_portalTunnelFallback,
Console.WriteLine),
this).Compose(
@ -1437,6 +1432,7 @@ public sealed class GameWindow :
sessionPlayer) => new FrameRootCompositionPhase(
new FrameRootDependencies(
_options,
_runtime,
platformResult.Graphics,
_window!,
platformResult.Input,
@ -1449,7 +1445,6 @@ public sealed class GameWindow :
_localPlayerMode,
_localPlayerIdentity,
_chaseCameraInput,
_playerControllerSlot,
_liveWorldOrigin,
_particleVisibility,
_effectPoses,
@ -1464,10 +1459,8 @@ public sealed class GameWindow :
_debugVmRenderFacts,
_inputCapture,
_cameraInput,
Selection,
_animatedEntities,
_updateFrameClock,
Combat,
_frameGraphs,
Console.WriteLine),
this).Compose(
@ -1571,7 +1564,7 @@ public sealed class GameWindow :
_retailUiLease,
_uiHost,
_devToolsComposition,
_liveSessionController,
_runtime,
_runtimeSettings,
_movementInput,
_cameraInput,
@ -1590,12 +1583,8 @@ public sealed class GameWindow :
_streamer,
_equippedChildRenderer,
_liveEntities,
_runtimeEntityObjects,
_runtimeInventory,
_runtimeCharacter,
_runtimeCommunication,
_runtimeActions,
_playerControllerSlot,
_runtime,
_runtimeHostLease,
_renderSceneShadow,
_livePresentationBindings,
_entityEffectAdvance,

View file

@ -17,6 +17,7 @@ using AcDream.App.World;
using AcDream.Content;
using AcDream.Core.Audio;
using AcDream.Core.Physics;
using AcDream.Runtime;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Session;
@ -62,7 +63,7 @@ internal sealed record IngressShutdownRoots(
// Keeps failed physical UI bindings alive through native-window release.
UiHost? RetainedUiHost,
DevToolsCompositionOwner? DevTools,
LiveSessionController? LiveSession,
GameRuntime Runtime,
RuntimeSettingsController Settings,
DispatcherMovementInputSource MovementInput,
DispatcherCameraInputSource CameraInput,
@ -83,12 +84,8 @@ internal sealed record LiveShutdownRoots(
LandblockStreamer? Streamer,
EquippedChildRenderController? EquippedChildren,
LiveEntityRuntime? LiveEntities,
RuntimeEntityObjectLifetime EntityObjects,
RuntimeInventoryState Inventory,
RuntimeCharacterState Character,
RuntimeCommunicationState Communication,
RuntimeActionState Actions,
RuntimeLocalPlayerMovementState Movement,
GameRuntime Runtime,
IDisposable RuntimeHostLease,
RenderSceneShadowRuntime? RenderSceneShadow,
LivePresentationRuntimeBindings? PresentationBindings,
DeferredEntityEffectAdvanceSource EffectAdvance,
@ -352,7 +349,7 @@ internal static class GameWindowShutdownManifest
Hard("keyboard source", () => ingress.KeyboardSource?.Deactivate()),
Hard("retained UI input", ingress.RetailUi.QuiesceInput),
Hard("developer tools input", () => ingress.DevTools?.DeactivateInput()),
Hard("live session", () => DisposeLiveSession(ingress.LiveSession)),
Hard("game runtime session", ingress.Runtime.StopSession),
]),
new ResourceShutdownStage("physical ingress cleanup",
[
@ -391,24 +388,6 @@ internal static class GameWindowShutdownManifest
"shadow render scene",
() => live.RenderSceneShadow?.Dispose()),
]),
new ResourceShutdownStage("runtime entity/object stream",
[
Hard(
"runtime local movement state",
live.Movement.Dispose),
Hard(
"runtime action state",
live.Actions.Dispose),
Hard(
"runtime character state",
live.Character.Dispose),
Hard(
"runtime inventory state",
live.Inventory.Dispose),
Hard(
"runtime entity/object lifetime",
live.EntityObjects.Dispose),
]),
new ResourceShutdownStage("effect dispatch edges",
[
Hard("live-presentation bindings", () => live.PresentationBindings?.Dispose()),
@ -456,9 +435,10 @@ internal static class GameWindowShutdownManifest
Hard("sky", () => render.Sky?.Dispose()),
Hard("particles", () => render.Particles?.Dispose()),
]),
new ResourceShutdownStage("runtime communication state",
new ResourceShutdownStage("game runtime root",
[
Hard("communication owner", live.Communication.Dispose),
Hard("graphical runtime host lease", live.RuntimeHostLease.Dispose),
Hard("game runtime", () => DisposeGameRuntime(live.Runtime)),
]),
new ResourceShutdownStage("shared texture owners",
[
@ -515,15 +495,14 @@ internal static class GameWindowShutdownManifest
private static ResourceShutdownOperation Soft(string name, Action action) =>
new(name, action, ResourceShutdownOperationPolicy.ReportAndContinue);
private static void DisposeLiveSession(LiveSessionController? controller)
private static void DisposeGameRuntime(GameRuntime runtime)
{
if (controller is null)
return;
controller.Dispose();
if (!controller.IsDisposalComplete)
runtime.Dispose();
GameRuntimeOwnershipSnapshot ownership = runtime.CaptureOwnership();
if (!ownership.IsConverged)
{
throw new InvalidOperationException(
"Live-session disposal was deferred by a reentrant lifecycle callback.");
"The canonical game runtime did not converge after disposal.");
}
}