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

@ -60,6 +60,29 @@ 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
@ -79,19 +102,22 @@ internal sealed class RenderFrameOrchestrator
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)
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)
@ -115,17 +141,50 @@ internal sealed class RenderFrameOrchestrator
}
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);
}
}
}
}