refactor(render): compose render frame orchestrator

Move the accepted draw transaction and failure recovery behind typed frame-phase owners so GameWindow only supplies immutable frame input. Preserve retail draw order, make ImGui/bootstrap shutdown ownership explicit, and restore exact text-render GL state on failures.\n\nCo-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-22 08:03:49 +02:00
parent 28e1cf8029
commit 9d7df1bfc5
25 changed files with 1675 additions and 279 deletions

View file

@ -93,23 +93,14 @@ public sealed class GameWindow : IDisposable
new AcDream.App.Rendering.ConsoleRenderFrameDiagnosticLog();
private readonly AcDream.App.Rendering.DebugVmRenderFactsPublisher
_debugVmRenderFacts = new();
private AcDream.App.Rendering.WorldRenderDiagnostics? _worldRenderDiagnostics;
private AcDream.App.Rendering.RenderFrameDiagnosticsController?
_renderFrameDiagnostics;
private AcDream.App.Diagnostics.FrameScreenshotController? _frameScreenshots;
private AcDream.App.Diagnostics.WorldLifecycleAutomationController? _worldLifecycleAutomation;
private AcDream.App.Rendering.GpuFrameFlightController? _gpuFrameFlights;
private AcDream.App.Rendering.RenderFrameResourceController?
_renderFrameResources;
private AcDream.App.Rendering.RuntimeRenderFrameLivePreparation?
_renderFrameLivePreparation;
private AcDream.App.Rendering.RenderWeatherFrameController?
_renderWeatherFrame;
private AcDream.App.Rendering.WorldRenderFrameBuilder?
_worldRenderFrameBuilder;
private AcDream.App.Rendering.RenderFrameOrchestrator?
_renderFrameOrchestrator;
private AcDream.App.Rendering.WorldSceneSkyState? _worldSceneSkyState;
private AcDream.App.Rendering.WorldSceneRenderer? _worldSceneRenderer;
private AcDream.App.Rendering.SkyPesFrameController? _skyPesFrame;
private ResourceShutdownTransaction? _shutdown;
private readonly DisplayFramePacingController _displayFramePacing;
@ -221,13 +212,8 @@ public sealed class GameWindow : IDisposable
private AcDream.App.Rendering.Wb.EnvCellRenderer? _envCellRenderer;
private AcDream.App.Rendering.Wb.WbFrustum? _envCellFrustum;
// R1 (render redesign): the per-cell DrawInside flood and concrete pass
// executor are borrowed by WorldSceneRenderer for each normal-world frame.
private AcDream.App.Rendering.RetailPViewRenderer? _retailPViewRenderer;
private AcDream.App.Rendering.RetailPViewPassExecutor? _retailPViewPassExecutor;
private AcDream.App.Rendering.RetailPViewCellSource? _retailPViewCells;
private AcDream.App.Rendering.TerrainDrawDiagnosticsController?
_terrainDrawDiagnostics;
// 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;
@ -746,7 +732,7 @@ public sealed class GameWindow : IDisposable
_gl = GL.GetApi(_window!);
_gpuFrameFlights = new AcDream.App.Rendering.GpuFrameFlightController(_gl);
_worldRenderDiagnostics = new AcDream.App.Rendering.WorldRenderDiagnostics(
var worldRenderDiagnostics = new AcDream.App.Rendering.WorldRenderDiagnostics(
new AcDream.App.Rendering.SilkRenderGlStateReader(_gl),
_renderDiagnosticLog);
_input = _window!.CreateInput();
@ -1682,7 +1668,6 @@ public sealed class GameWindow : IDisposable
{
_frameScreenshots = new AcDream.App.Diagnostics.FrameScreenshotController(
_gl!,
() => (_window!.Size.X, _window.Size.Y),
System.IO.Path.Combine(artifactDirectory, "screenshots"),
UiProbeLog);
_worldLifecycleAutomation =
@ -2285,8 +2270,6 @@ public sealed class GameWindow : IDisposable
landblockRenderPublisher.PrepareAfterRenderPins);
_clipFrame ??= ClipFrame.NoClip();
_retailPViewRenderer = new AcDream.App.Rendering.RetailPViewRenderer();
// T1: invisible portal depth writes (seal/punch) — retail
// DrawPortalPolyInternal (Ghidra 0x0059bc90).
_portalDepthMask = new AcDream.App.Rendering.PortalDepthMaskRenderer(_gl);
@ -2782,7 +2765,7 @@ public sealed class GameWindow : IDisposable
_localPlayerMode);
var renderFrameGlState = new AcDream.App.Rendering.RenderFrameGlStateController(
new AcDream.App.Rendering.SilkRenderFrameGlStateApi(_gl!));
_renderFrameLivePreparation =
var renderFrameLivePreparation =
new AcDream.App.Rendering.RuntimeRenderFrameLivePreparation(
_textureCache,
_wbMeshAdapter,
@ -2795,7 +2778,7 @@ public sealed class GameWindow : IDisposable
_particleRenderer,
_frameProfiler,
_frameDiag);
_renderFrameResources =
var renderFrameResources =
new AcDream.App.Rendering.RenderFrameResourceController(
_gpuFrameFlights!,
new AcDream.App.Rendering.RuntimeRenderFrameBeginResources(
@ -2816,14 +2799,14 @@ public sealed class GameWindow : IDisposable
Weather,
teleportRenderState,
_particleVisibility,
_worldRenderDiagnostics!,
worldRenderDiagnostics,
renderFrameGlState),
_renderFrameLivePreparation);
_renderWeatherFrame =
renderFrameLivePreparation);
var renderWeatherFrame =
new AcDream.App.Rendering.RenderWeatherFrameController(
WorldTime,
Weather);
_skyPesFrame = new AcDream.App.Rendering.SkyPesFrameController(
var skyPesFrame = new AcDream.App.Rendering.SkyPesFrameController(
_scriptRunner!,
_particleSink!,
_effectPoses,
@ -2837,8 +2820,8 @@ public sealed class GameWindow : IDisposable
_envCellRenderer,
_sceneLightingUbo,
_renderRange,
_skyPesFrame);
_worldRenderFrameBuilder =
skyPesFrame);
var worldRenderFrameBuilder =
new AcDream.App.Rendering.WorldRenderFrameBuilder(
new AcDream.App.Rendering.RuntimeWorldFrameCameraSource(
_cameraController!,
@ -2869,22 +2852,22 @@ public sealed class GameWindow : IDisposable
new AcDream.App.Rendering.RuntimeWorldFrameBuildingSource(
_landblockPresentationPipeline!,
_cellVisibility));
_terrainDrawDiagnostics =
var terrainDrawDiagnostics =
new AcDream.App.Rendering.TerrainDrawDiagnosticsController(
_frameDiag,
_worldRenderDiagnostics!,
worldRenderDiagnostics,
new AcDream.App.Rendering.RuntimeFramePipelineDiagnosticFactsSource(
_terrain,
_landblockPresentationPipeline,
_renderFrameLivePreparation,
renderFrameLivePreparation,
_wbDrawDispatcher,
_streamingController,
_liveEntities!,
_worldState),
_renderDiagnosticLog);
_retailPViewCells = new AcDream.App.Rendering.RetailPViewCellSource(
var retailPViewCells = new AcDream.App.Rendering.RetailPViewCellSource(
_cellVisibility);
_retailPViewPassExecutor =
var retailPViewPassExecutor =
new AcDream.App.Rendering.RetailPViewPassExecutor(
_gl!,
renderFrameGlState,
@ -2899,11 +2882,11 @@ public sealed class GameWindow : IDisposable
_particleRenderer,
_portalDepthMask,
_retailAlphaQueue,
_worldRenderDiagnostics!,
_terrainDrawDiagnostics);
worldRenderDiagnostics,
terrainDrawDiagnostics);
var worldSceneDiagnostics =
new AcDream.App.Rendering.WorldSceneDiagnosticsController(
_worldRenderDiagnostics!,
worldRenderDiagnostics,
new AcDream.App.Rendering.RuntimeWorldScenePViewDiagnosticSource(
_playerControllerSlot,
_physicsEngine,
@ -2922,27 +2905,63 @@ public sealed class GameWindow : IDisposable
_wbDrawDispatcher!,
_envCellRenderer!,
_terrain,
_terrainDrawDiagnostics,
terrainDrawDiagnostics,
_skyRenderer,
_particleSystem,
_particleRenderer);
_worldSceneRenderer = new AcDream.App.Rendering.WorldSceneRenderer(
_renderFrameResources,
var worldSceneRenderer = new AcDream.App.Rendering.WorldSceneRenderer(
renderFrameResources,
renderLoginState,
_worldSceneSkyState!,
_worldRenderFrameBuilder,
worldRenderFrameBuilder,
new AcDream.App.Rendering.RuntimeWorldSceneEntitySource(_worldState),
_retailSelectionScene,
_retailAlphaQueue,
_particleVisibility,
new AcDream.App.Rendering.WorldScenePViewRenderer(
_retailPViewRenderer!,
_retailPViewPassExecutor,
_retailPViewPassExecutor),
_retailPViewCells,
new AcDream.App.Rendering.RetailPViewRenderer(),
retailPViewPassExecutor,
retailPViewPassExecutor),
retailPViewCells,
worldScenePasses,
_renderRange,
worldSceneDiagnostics);
AcDream.App.Rendering.IRetainedGameplayUiFrame? retainedGameplayUi =
_options.RetailUi && _retailUiRuntime is not null
? new AcDream.App.Rendering.RetainedGameplayUiFrame(
_retailUiRuntime,
_input)
: null;
AcDream.App.Rendering.IPrivateFrameScreenshot? privateScreenshot =
_frameScreenshots is not null
? new AcDream.App.Rendering.PrivateFrameScreenshot(
_frameScreenshots)
: null;
var privatePresentation =
new AcDream.App.Rendering.PrivatePresentationRenderer(
new AcDream.App.Rendering.LocalPlayerPortalViewport(
_localPlayerTeleport!,
_cameraController!),
renderFrameResources,
_paperdollFramePresenter,
retainedGameplayUi,
_devToolsFramePresenter,
privateScreenshot);
var framePreparation =
new AcDream.App.Rendering.RenderFramePreparationController(
renderFrameResources,
_devToolsFramePresenter,
renderWeatherFrame);
_renderFrameOrchestrator =
new AcDream.App.Rendering.RenderFrameOrchestrator(
_gpuFrameFlights!,
framePreparation,
worldSceneRenderer,
privatePresentation,
_renderFrameDiagnostics!,
(AcDream.App.Rendering.IRenderFrameFailureRecovery?)
_devToolsFramePresenter
?? AcDream.App.Rendering.NullRenderFrameFailureRecovery.Instance);
var liveFrameCoordinator = new AcDream.App.World.RetailLiveFrameCoordinator(
liveObjectFrame,
_worldState,
@ -3415,126 +3434,12 @@ public sealed class GameWindow : IDisposable
// window title so there's zero rendering cost (no font/overlay needed).
private void OnRender(double deltaSeconds)
{
_gpuFrameFlights!.BeginFrame();
Exception? renderFailure = null;
try
{
var frameInput = new AcDream.App.Rendering.RenderFrameInput(
Vector2D<int> size = _window!.Size;
_renderFrameOrchestrator!.Render(
new AcDream.App.Rendering.RenderFrameInput(
deltaSeconds,
_window!.Size.X,
_window.Size.Y);
_renderFrameResources!.Prepare(frameInput);
AcDream.App.Rendering.RenderFrameFoundation foundation =
_renderFrameResources.Foundation;
// gmSmartBoxUI::UseTime @ 0x004D6E30 swaps visibility between the
// normal SmartBox viewport and UIElement_Viewport's portal
// CreatureMode; it does not redraw the world behind portal space.
bool portalViewportVisible = foundation.PortalViewportVisible;
// Phase D.2a — begin ImGui frame. Paired with the Render() call
// after the scene draws (below). ImGuiController.Update()
// consumes buffered Silk.NET input events and calls ImGui.NewFrame.
_devToolsFramePresenter?.BeginFrame((float)deltaSeconds);
// Phase G.1: weather state machine — deterministic per-day roll
// + transitions + lightning flash.
_renderWeatherFrame!.Tick(deltaSeconds);
// (Pre-Bug-A code spawned camera-attached rain/snow particle
// emitters here as a workaround for missing weather-mesh
// rendering. Deleted 2026-04-26 once the retail-faithful world-
// space mesh path landed in SkyRenderer.RenderWeather. Retail
// rain is GfxObj 0x01004C42/0x01004C44 — a hollow octagonal
// cylinder anchored at player_pos + (0, 0, -120m) per
// GameSky::UpdatePosition at 0x00506dd0 — drawn after the
// landblock pass per LScape::draw at 0x00506330. There is no
// server-driven weather event and no camera-attached emitter
// in retail. Snow renders identically when a Snowy DayGroup is
// active in some other Region; the partition by Properties&0x04
// and the SkyRenderer.RenderWeather pass both pick up snow
// weather meshes for free.)
AcDream.App.Rendering.WorldRenderFrameOutcome worldOutcome =
_worldSceneRenderer!.Render(frameInput);
int visibleLandblocks = worldOutcome.VisibleLandblocks;
int totalLandblocks = worldOutcome.TotalLandblocks;
bool normalWorldDrawn = worldOutcome.NormalWorldDrawn;
// Retail gmSmartBoxUI swaps the world SmartBox viewport for a
// CreatureMode portal-space viewport. The world block above is skipped
// while this replacement scene is visible. This scene and
// its projection transition draws BEFORE retained UI, so chat, windows,
// cursor, and toolbar remain visible and interactive in transit.
_localPlayerTeleport?.DrawPortalViewport(
_window!.Size.X,
_window.Size.Y,
_cameraController!.Active.Projection);
// Phase D.2b Sub-phase C Slice 2 — paperdoll 3-D doll: render the re-dressed player clone into the
// viewport's off-screen texture BEFORE the 2-D UI pass blits it, but only when the inventory window
// is open AND the paperdoll is in doll-view (the widget's Visible is driven by the Slots toggle).
// The 3-D pass is fully sealed in a GLStateScope so it doesn't disturb the world/UI state.
_paperdollFramePresenter?.Render();
// Phase D.2b — retail-look UI tree (render-only; input integration deferred).
// Self-contained 2D pass: UiHost.Draw → TextRenderer.Flush sets its own
// blend/depth state and restores. Drawn before ImGui so the devtools
// overlay composites on top during development.
if (_options.RetailUi && _retailUiRuntime is not null)
{
_retailUiRuntime.Tick(deltaSeconds);
if (_input is not null)
_retailUiRuntime.UpdateCursor(_input.Mice);
_retailUiRuntime.Draw(new System.Numerics.Vector2(_window!.Size.X, _window.Size.Y));
}
// Devtools remain above retained gameplay UI. The focused presenter
// owns menu policy, panels, and ImGui draw-data upload.
_devToolsFramePresenter?.Render(
deltaSeconds,
_window!.Size.X,
_window.Size.Y);
// Explicit diagnostic captures are requested by the retained-UI
// automation tick above and executed only after every presentation
// layer has drawn into the back buffer on this render thread.
bool screenshotCaptured = _frameScreenshots?.CapturePending() == true;
_renderFrameDiagnostics?.Publish(
new AcDream.App.Rendering.RenderFrameInput(
deltaSeconds,
_window!.Size.X,
_window.Size.Y),
new AcDream.App.Rendering.RenderFrameOutcome(
new AcDream.App.Rendering.WorldRenderFrameOutcome(
visibleLandblocks,
totalLandblocks,
normalWorldDrawn),
new AcDream.App.Rendering.PrivatePresentationFrameOutcome(
portalViewportVisible,
screenshotCaptured)));
}
catch (Exception ex)
{
renderFailure = ex;
throw;
}
finally
{
try
{
_gpuFrameFlights.EndFrame();
}
catch (Exception closeFailure) when (renderFailure is not null)
{
throw new AggregateException(
"Rendering failed and the in-flight GPU frame could not be closed.",
renderFailure,
closeFailure);
}
}
size.X,
size.Y));
}
private AcDream.App.Diagnostics.WorldLifecycleResourceSnapshot
@ -4563,14 +4468,8 @@ public sealed class GameWindow : IDisposable
[
new("world frame composition", () =>
{
_worldSceneRenderer = null;
_worldRenderFrameBuilder = null;
_renderFrameOrchestrator = null;
_worldSceneSkyState = null;
_skyPesFrame = null;
_retailPViewPassExecutor = null;
_retailPViewCells = null;
_terrainDrawDiagnostics = null;
_retailPViewRenderer = null;
}),
]),
new ResourceShutdownStage("session dependents",
@ -4643,11 +4542,13 @@ public sealed class GameWindow : IDisposable
]),
new ResourceShutdownStage("render frontends",
[
new("frame composition", () =>
new("developer tools", () =>
{
_renderFrameResources = null;
_renderFrameLivePreparation = null;
_renderWeatherFrame = null;
_devToolsFramePresenter = null;
_devToolsCameraMenu = null;
_devToolsCommandBus = null;
_devToolsBackend?.Dispose();
_devToolsBackend = null;
}),
new("portal tunnel", () =>
{