refactor(lifetime): own render resource prefixes

Give terrain, sky, retained UI, portal preparation, and the update/render frame pair explicit single owners. Make shader, texture, text, bindless, and GL construction prefixes checked and retryable so partial failure cannot lose or replay resource ownership.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-22 14:42:14 +02:00
parent fec0d94148
commit c87b15303d
41 changed files with 3768 additions and 355 deletions

View file

@ -92,8 +92,11 @@ public sealed class GameWindow : IDisposable
private AcDream.App.Diagnostics.FrameScreenshotController? _frameScreenshots;
private AcDream.App.Diagnostics.WorldLifecycleAutomationController? _worldLifecycleAutomation;
private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights;
private AcDream.App.Rendering.RenderFrameOrchestrator?
_renderFrameOrchestrator;
private readonly AcDream.App.Rendering.GameFrameGraphSlot _frameGraphs = new();
private readonly AcDream.App.Rendering.GameRenderResourceLifetime
_renderResourceLifetime = new();
private readonly AcDream.App.Rendering.GlConstructionCleanupLedger
_glConstructionCleanup = new();
private readonly AcDream.App.World.WorldEnvironmentController _worldEnvironment =
new(Console.WriteLine);
private ResourceShutdownTransaction? _shutdown;
@ -148,7 +151,6 @@ public sealed class GameWindow : IDisposable
private readonly AcDream.App.Input.MovementTruthDiagnosticController
_movementTruthDiagnostics;
private readonly AcDream.App.Input.LocalPlayerOutboundController _localPlayerOutbound;
private AcDream.App.Update.UpdateFrameOrchestrator _updateFrameOrchestrator = null!;
private readonly AcDream.App.Update.UpdateFrameClock _updateFrameClock = new();
private AcDream.App.Physics.RemoteTeleportController? _remoteTeleportController;
// Step 7 projectile presentation. The controller owns no identity map;
@ -202,7 +204,8 @@ public sealed class GameWindow : IDisposable
// R1 (render redesign): portal-view draw owners are retained transitively
// by the frame orchestrator rather than duplicated as GameWindow roots.
private AcDream.App.Rendering.PortalDepthMaskRenderer? _portalDepthMask;
private AcDream.App.Rendering.PortalTunnelPresentation? _portalTunnel;
private readonly AcDream.App.Rendering.TransferableResourceSlot<
AcDream.App.Rendering.PortalTunnelPresentation> _portalTunnelFallback = new();
// Phase U.3: the shared per-frame clip data (binding=2 mesh SSBO + terrain
// UBO). In U.3 a single ClipFrame.NoClip() instance is created lazily (??=) and
@ -342,6 +345,7 @@ public sealed class GameWindow : IDisposable
// Phase D.2b — retained host + composition runtime. Null unless ACDREAM_RETAIL_UI=1.
private AcDream.App.UI.UiHost? _uiHost;
private AcDream.App.UI.RetailUiRuntime? _retailUiRuntime;
private readonly AcDream.App.UI.RetailUiRuntimeLease _retailUiLease = new();
private readonly AcDream.App.Combat.CombatAttackOperationsSlot
_combatAttackOperations = new();
private readonly AcDream.App.Combat.CombatFeedbackSlot
@ -638,7 +642,15 @@ public sealed class GameWindow : IDisposable
_displayFramePacing,
_hostQuiescence);
_windowCallbacks.Attach();
_window.Run();
try
{
_window.Run();
}
catch (Exception failure)
{
_glConstructionCleanup.RetainFrom(failure);
throw;
}
}
private void OnLoad()
@ -1100,7 +1112,11 @@ public sealed class GameWindow : IDisposable
// Build the terrain atlas once from the Region dat. Phase N.5b: the
// atlas exposes bindless handles for the modern terrain path, so
// BindlessSupport is threaded through.
var terrainAtlas = AcDream.App.Rendering.TerrainAtlas.Build(_gl, _dats, _bindlessSupport);
var terrainAtlas = _renderResourceLifetime.AcquireTerrainAtlas(
() => AcDream.App.Rendering.TerrainAtlas.Build(
_gl,
_dats,
_bindlessSupport));
// A.5 T22.5: apply anisotropic level from quality preset. Build()
// hard-codes 16x; override here to match the resolved quality so Low
// (4x) and Medium (8x) actually take effect.
@ -1230,11 +1246,12 @@ public sealed class GameWindow : IDisposable
if (_options.RetailUi)
{
_vitalsVm ??= new AcDream.UI.Abstractions.Panels.Vitals.VitalsVM(Combat, LocalPlayer);
_uiHost = new AcDream.App.UI.UiHost(
_gl,
shadersDir,
_debugFont,
_hostQuiescence);
_uiHost = _retailUiLease.AcquireHost(
() => new AcDream.App.UI.UiHost(
_gl,
shadersDir,
_debugFont,
_hostQuiescence));
_retainedInputCapture.Root = _uiHost.Root;
_uiHost.Root.UiLocked = _runtimeSettings.Gameplay.LockUI;
var cursorFeedbackController = new AcDream.App.UI.CursorFeedbackController(
@ -1360,8 +1377,7 @@ public sealed class GameWindow : IDisposable
UiProbeLog);
}
_retailUiRuntime = AcDream.App.UI.RetailUiRuntime.Mount(
new AcDream.App.UI.RetailUiRuntimeBindings(
var retailUiBindings = new AcDream.App.UI.RetailUiRuntimeBindings(
Host: _uiHost,
Assets: new AcDream.App.UI.RetailUiAssets(
_dats!, _datLock, ResolveChrome, ResolveDatFont,
@ -1511,7 +1527,10 @@ public sealed class GameWindow : IDisposable
action => _inputDispatcher?.TryInvokeAutomationAction(action) == true,
(action, held) =>
_inputDispatcher?.TrySetAutomationActionHeld(action, held) == true,
_worldLifecycleAutomation)));
_worldLifecycleAutomation));
_retailUiRuntime = _retailUiLease.Mount(
() => AcDream.App.UI.RetailUiRuntime.CreateUninitialized(
retailUiBindings));
}
@ -1966,26 +1985,29 @@ public sealed class GameWindow : IDisposable
// T1: invisible portal depth writes (seal/punch) — retail
// DrawPortalPolyInternal (Ghidra 0x0059bc90).
_portalDepthMask = new AcDream.App.Rendering.PortalDepthMaskRenderer(_gl);
_portalTunnel = AcDream.App.Rendering.PortalTunnelPresentation.CreateRequired(
_gl,
_dats!,
_animLoader!,
_hookRouter,
_wbDrawDispatcher,
_sceneLightingUbo!,
_wbMeshAdapter!);
_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.
_portalTunnel.PrepareResources();
}
// 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 = new Shader(_gl,
Path.Combine(shadersDir, "sky.vert"),
Path.Combine(shadersDir, "sky.frag"));
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, _dats, skyShader, _textureCache!, _samplerCache);
@ -2422,13 +2444,8 @@ public sealed class GameWindow : IDisposable
_localPlayerMode,
_playerModeController));
_playerModeController.BindAutoEntry(_playerModeAutoEntry);
var localTeleportPresentation =
new AcDream.App.Streaming.LocalPlayerTeleportPresentation(
_portalTunnel
?? throw new InvalidOperationException(
"Portal presentation was not composed before local teleport."));
_localPlayerTeleport =
new AcDream.App.Streaming.LocalPlayerTeleportController(
_localPlayerTeleport = _portalTunnelFallback.Transfer(
portalTunnel => new AcDream.App.Streaming.LocalPlayerTeleportController(
new AcDream.App.Streaming.LiveLocalPlayerTeleportAuthority(
_liveEntities,
_localPlayerIdentity),
@ -2451,11 +2468,8 @@ public sealed class GameWindow : IDisposable
liveSpatialReconciler),
new AcDream.App.Streaming.LocalPlayerTeleportSession(
_liveSessionController),
localTeleportPresentation);
// Ownership transferred to LocalPlayerTeleportController. This field
// remains only as the staged-startup fallback used by shutdown when
// composition fails before the transfer.
_portalTunnel = null;
new AcDream.App.Streaming.LocalPlayerTeleportPresentation(
portalTunnel)));
_localPlayerTeleportSink.Bind(_localPlayerTeleport);
var teleportRenderState =
new AcDream.App.Rendering.LocalPlayerTeleportRenderStateSource(
@ -2652,7 +2666,7 @@ public sealed class GameWindow : IDisposable
renderFrameResources,
_devToolsFramePresenter,
renderWeatherFrame);
_renderFrameOrchestrator =
var renderFrameOrchestrator =
new AcDream.App.Rendering.RenderFrameOrchestrator(
_gpuFrameFlights!,
framePreparation,
@ -2681,7 +2695,7 @@ public sealed class GameWindow : IDisposable
Combat,
_selection,
_worldSelectionQuery!));
_updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(
var updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(
new AcDream.App.Update.LiveEntityTeardownFramePhase(
_liveEntities),
new AcDream.App.Update.ConsoleUpdateFrameFailureSink(),
@ -2697,6 +2711,7 @@ public sealed class GameWindow : IDisposable
new AcDream.App.Update.PlayerModeAutoEntryFramePhase(
_playerModeAutoEntry),
cameraFrame);
_frameGraphs.Publish(updateFrameOrchestrator, renderFrameOrchestrator);
_liveSessionHost = CreateLiveSessionHost();
AcDream.UI.Abstractions.Panels.Debug.DebugVM? debugVm = _debugVm;
@ -3055,8 +3070,7 @@ public sealed class GameWindow : IDisposable
{
using var _updStage = _frameProfiler.BeginStage(
AcDream.App.Diagnostics.FrameStage.Update);
_updateFrameOrchestrator.Tick(
new AcDream.App.Update.UpdateFrameInput(dt));
_frameGraphs.Tick(new AcDream.App.Update.UpdateFrameInput(dt));
}
// Performance overlay state — updated every ~0.5s and written to the
@ -3064,11 +3078,12 @@ public sealed class GameWindow : IDisposable
private void OnRender(double deltaSeconds)
{
Vector2D<int> size = _window!.Size;
_renderFrameOrchestrator!.Render(
_frameGraphs.Render(
new AcDream.App.Rendering.RenderFrameInput(
deltaSeconds,
size.X,
size.Y));
size.Y),
out _);
}
private AcDream.App.Diagnostics.WorldLifecycleResourceSnapshot
@ -3336,7 +3351,7 @@ public sealed class GameWindow : IDisposable
new("dispatcher", () => _inputDispatcher?.Deactivate()),
new("mouse source", () => _mouseSource?.Deactivate()),
new("keyboard source", () => _kbSource?.Deactivate()),
new("retained UI input", () => _uiHost?.QuiesceInput()),
new("retained UI input", _retailUiLease.QuiesceInput),
new("developer tools input", () => _devToolsInputContext?.Deactivate()),
]),
// Live-session reset retires the complete streaming window. Every
@ -3390,7 +3405,7 @@ public sealed class GameWindow : IDisposable
"Gameplay action callback removal remains pending.");
_gameplayInputActions = null;
}),
new("retained UI input", () => _uiHost?.DeactivateInput()),
new("retained UI input", _retailUiLease.DeactivateInput),
new("developer tools input", () => _devToolsInputContext?.Dispose()),
new("camera pointer", () =>
{
@ -3444,7 +3459,7 @@ public sealed class GameWindow : IDisposable
new("runtime settings targets", _runtimeSettings.UnbindRuntimeTargets),
new("world frame composition", () =>
{
_renderFrameOrchestrator = null;
_frameGraphs.Withdraw();
}),
]),
new ResourceShutdownStage("session dependents",
@ -3461,7 +3476,10 @@ public sealed class GameWindow : IDisposable
}),
new("retail UI", () =>
{
_retailUiRuntime?.Dispose();
_retailUiLease.Dispose();
if (!_retailUiLease.IsDisposalComplete)
throw new InvalidOperationException(
"The retained UI ownership lease did not complete disposal.");
_retailUiRuntime = null;
_retainedInputCapture.Root = null;
_uiHost = null;
@ -3544,8 +3562,7 @@ public sealed class GameWindow : IDisposable
{
_localPlayerTeleport?.Dispose();
_localPlayerTeleport = null;
_portalTunnel?.Dispose();
_portalTunnel = null;
_portalTunnelFallback.ReleaseFallback();
}),
new("paperdoll viewport", () =>
{
@ -3589,6 +3606,15 @@ public sealed class GameWindow : IDisposable
new("frame pacing", _displayFramePacing.Dispose),
new("frame profiler", _frameProfiler.Dispose),
]),
new ResourceShutdownStage("dedicated render resources",
[
new("sky shader", _renderResourceLifetime.ReleaseSkyShader),
new("terrain atlas", _renderResourceLifetime.ReleaseTerrainAtlas),
]),
new ResourceShutdownStage("failed render construction cleanup",
[
new("GL construction ledger", _glConstructionCleanup.Dispose),
]),
new ResourceShutdownStage("frame flight owner",
[
new("frame flights", () =>