acdream/src/AcDream.App/Rendering/RenderFrameOrchestrator.cs
Erik 9d7df1bfc5 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>
2026-07-22 08:03:49 +02:00

190 lines
6.3 KiB
C#

namespace AcDream.App.Rendering;
/// <summary>
/// Host-owned values for one render callback. Render phases borrow this value
/// for the duration of <see cref="RenderFrameOrchestrator.Render"/> only.
/// </summary>
internal readonly record struct RenderFrameInput(
double DeltaSeconds,
int ViewportWidth,
int ViewportHeight);
/// <summary>
/// World facts published after the normal-world phase has either drawn or
/// intentionally skipped the viewport.
/// </summary>
internal readonly record struct WorldRenderFrameOutcome(
int VisibleLandblocks,
int TotalLandblocks,
bool NormalWorldDrawn);
/// <summary>
/// Private-presentation facts published after portal, paperdoll, retained UI,
/// developer UI, and screenshot work has completed.
/// </summary>
internal readonly record struct PrivatePresentationFrameOutcome(
bool PortalViewportDrawn,
bool ScreenshotCaptured);
/// <summary>The immutable result observed by post-screenshot diagnostics.</summary>
internal readonly record struct RenderFrameOutcome(
WorldRenderFrameOutcome World,
PrivatePresentationFrameOutcome Presentation);
internal interface IRenderFrameLifetime
{
void BeginFrame();
void EndFrame();
}
internal interface IRenderFrameResourcePhase
{
void Prepare(RenderFrameInput input);
}
internal interface IWorldSceneFramePhase
{
WorldRenderFrameOutcome Render(RenderFrameInput input);
}
internal interface IPrivatePresentationFramePhase
{
PrivatePresentationFrameOutcome Render(
RenderFrameInput input,
WorldRenderFrameOutcome world);
}
internal interface IRenderFrameDiagnosticsPhase
{
void Publish(RenderFrameInput input, RenderFrameOutcome outcome);
}
/// <summary>
/// Closes presentation transactions which may have opened before a later
/// render phase failed. Recovery is idempotent so failures before the
/// transaction opens and failures after it closes are both safe.
/// </summary>
internal interface IRenderFrameFailureRecovery
{
void AbortFrame();
}
internal sealed class NullRenderFrameFailureRecovery : IRenderFrameFailureRecovery
{
public static NullRenderFrameFailureRecovery Instance { get; } = new();
private NullRenderFrameFailureRecovery()
{
}
public void AbortFrame()
{
}
}
/// <summary>
/// Owns the accepted outer render transaction. Focused phase owners retain the
/// detailed PView, private-viewport, UI, and diagnostic order inside their
/// typed boundaries.
/// </summary>
/// <remarks>
/// <see cref="IRenderFrameLifetime.BeginFrame"/> intentionally runs outside
/// the protected render body. Once begin succeeds, close is attempted exactly
/// once. This preserves the production GPU-flight contract: a render failure
/// and close failure are reported together, while either failure alone
/// propagates directly.
/// </remarks>
internal sealed class RenderFrameOrchestrator
{
private readonly IRenderFrameLifetime _lifetime;
private readonly IRenderFrameResourcePhase _resources;
private readonly IWorldSceneFramePhase _world;
private readonly IPrivatePresentationFramePhase _presentation;
private readonly IRenderFrameDiagnosticsPhase _diagnostics;
private readonly IRenderFrameFailureRecovery _recovery;
public RenderFrameOrchestrator(
IRenderFrameLifetime lifetime,
IRenderFrameResourcePhase resources,
IWorldSceneFramePhase world,
IPrivatePresentationFramePhase presentation,
IRenderFrameDiagnosticsPhase diagnostics,
IRenderFrameFailureRecovery recovery)
{
_lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime));
_resources = resources ?? throw new ArgumentNullException(nameof(resources));
_world = world ?? throw new ArgumentNullException(nameof(world));
_presentation = presentation ?? throw new ArgumentNullException(nameof(presentation));
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
_recovery = recovery ?? throw new ArgumentNullException(nameof(recovery));
}
public RenderFrameOutcome Render(RenderFrameInput input)
{
_lifetime.BeginFrame();
Exception? renderFailure = null;
try
{
_resources.Prepare(input);
WorldRenderFrameOutcome world = _world.Render(input);
PrivatePresentationFrameOutcome presentation =
_presentation.Render(input, world);
var outcome = new RenderFrameOutcome(world, presentation);
_diagnostics.Publish(input, outcome);
return outcome;
}
catch (Exception error)
{
renderFailure = error;
throw;
}
finally
{
Exception? recoveryFailure = null;
if (renderFailure is not null)
{
try
{
// ImGui NewFrame is opened during preparation but normally
// closed during private presentation. Abort it before the
// GPU flight closes when any intervening phase fails.
_recovery.AbortFrame();
}
catch (Exception error)
{
recoveryFailure = error;
}
}
try
{
_lifetime.EndFrame();
}
catch (Exception closeFailure) when (renderFailure is not null)
{
if (recoveryFailure is not null)
{
throw new AggregateException(
"Rendering failed and neither presentation recovery nor the in-flight GPU frame could be closed.",
renderFailure,
recoveryFailure,
closeFailure);
}
throw new AggregateException(
"Rendering failed and the in-flight GPU frame could not be closed.",
renderFailure,
closeFailure);
}
if (renderFailure is not null && recoveryFailure is not null)
{
throw new AggregateException(
"Rendering failed and the presentation frame could not be aborted.",
renderFailure,
recoveryFailure);
}
}
}
}