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

@ -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.");
}
}