namespace AcDream.App.Rendering; /// /// Host-owned values for one render callback. Render phases borrow this value /// for the duration of only. /// internal readonly record struct RenderFrameInput( double DeltaSeconds, int ViewportWidth, int ViewportHeight); /// /// World facts published after the normal-world phase has either drawn or /// intentionally skipped the viewport. /// internal readonly record struct WorldRenderFrameOutcome( int VisibleLandblocks, int TotalLandblocks, bool NormalWorldDrawn); /// /// Private-presentation facts published after portal, paperdoll, retained UI, /// developer UI, and screenshot work has completed. /// internal readonly record struct PrivatePresentationFrameOutcome( bool PortalViewportDrawn, bool ScreenshotCaptured); /// The immutable result observed by post-screenshot diagnostics. 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); } /// /// 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. /// internal interface IRenderFrameFailureRecovery { void AbortFrame(); } internal sealed class NullRenderFrameFailureRecovery : IRenderFrameFailureRecovery { public static NullRenderFrameFailureRecovery Instance { get; } = new(); private NullRenderFrameFailureRecovery() { } public void AbortFrame() { } } /// /// Owns the accepted outer render transaction. Focused phase owners retain the /// detailed PView, private-viewport, UI, and diagnostic order inside their /// typed boundaries. /// /// /// 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. /// 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); } } } }