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); } /// /// Diagnostic GPU timer bracketing only render-resource, world, and private /// presentation submission. It deliberately excludes diagnostics, pacing, /// and the idle interval before the next render callback. /// internal interface IRenderFrameGpuMeasurement { void BeginFrame(); void EndFrame(); } 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); } internal interface IRenderFramePostDiagnosticsPhase { void Process(RenderFrameInput input, RenderFrameOutcome outcome); } internal sealed class NullRenderFramePostDiagnosticsPhase : IRenderFramePostDiagnosticsPhase { public static NullRenderFramePostDiagnosticsPhase Instance { get; } = new(); private NullRenderFramePostDiagnosticsPhase() { } public void Process(RenderFrameInput input, RenderFrameOutcome outcome) { } } /// /// Fixed-order post-diagnostic fan-out. The shadow referee must run before /// lifecycle checkpoint publication so each artifact observes the comparison /// made against that exact completed render frame. /// internal sealed class SerialRenderFramePostDiagnosticsPhase : IRenderFramePostDiagnosticsPhase { private readonly IRenderFramePostDiagnosticsPhase[] _phases; public SerialRenderFramePostDiagnosticsPhase( params IRenderFramePostDiagnosticsPhase[] phases) { ArgumentNullException.ThrowIfNull(phases); if (phases.Length == 0 || phases.Any(static phase => phase is null)) { throw new ArgumentException( "At least one non-null post-diagnostic phase is required.", nameof(phases)); } _phases = [.. phases]; } public void Process(RenderFrameInput input, RenderFrameOutcome outcome) { for (int i = 0; i < _phases.Length; i++) _phases[i].Process(input, 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 : IGameRenderFrameRoot { private readonly IRenderFrameLifetime _lifetime; private readonly IRenderFrameGpuMeasurement _gpuMeasurement; private readonly IRenderFrameResourcePhase _resources; private readonly IWorldSceneFramePhase _world; private readonly IPrivatePresentationFramePhase _presentation; private readonly IRenderFrameDiagnosticsPhase _diagnostics; private readonly IRenderFramePostDiagnosticsPhase _postDiagnostics; private readonly IRenderFrameFailureRecovery _recovery; public RenderFrameOrchestrator( IRenderFrameLifetime lifetime, IRenderFrameGpuMeasurement gpuMeasurement, IRenderFrameResourcePhase resources, IWorldSceneFramePhase world, IPrivatePresentationFramePhase presentation, IRenderFrameDiagnosticsPhase diagnostics, IRenderFramePostDiagnosticsPhase postDiagnostics, IRenderFrameFailureRecovery recovery) { _lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime)); _gpuMeasurement = gpuMeasurement ?? throw new ArgumentNullException(nameof(gpuMeasurement)); _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)); _postDiagnostics = postDiagnostics ?? throw new ArgumentNullException(nameof(postDiagnostics)); _recovery = recovery ?? throw new ArgumentNullException(nameof(recovery)); } public RenderFrameOutcome Render(RenderFrameInput input) { _lifetime.BeginFrame(); Exception? renderFailure = null; try { WorldRenderFrameOutcome world; PrivatePresentationFrameOutcome presentation; Exception? measuredRenderFailure = null; _gpuMeasurement.BeginFrame(); try { _resources.Prepare(input); world = _world.Render(input); presentation = _presentation.Render(input, world); } catch (Exception error) { measuredRenderFailure = error; throw; } finally { try { _gpuMeasurement.EndFrame(); } catch (Exception measurementFailure) when (measuredRenderFailure is not null) { throw new AggregateException( "Rendering failed and its GPU measurement could not be closed.", measuredRenderFailure, measurementFailure); } } var outcome = new RenderFrameOutcome(world, presentation); _diagnostics.Publish(input, outcome); _postDiagnostics.Process(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); } } } }