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>
190 lines
6.3 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|