refactor(render): establish frame orchestration contract

This commit is contained in:
Erik 2026-07-22 04:18:31 +02:00
parent f316bbd817
commit 7e4cfb37c3
4 changed files with 489 additions and 5 deletions

View file

@ -214,7 +214,10 @@ internal sealed class ImmediateGpuResourceRetirementQueue : IGpuResourceRetireme
}
}
internal sealed class GpuFrameFlightController : IGpuResourceRetirementQueue, IDisposable
internal sealed class GpuFrameFlightController :
IGpuResourceRetirementQueue,
IRenderFrameLifetime,
IDisposable
{
internal const int DefaultMaximumFramesInFlight = 3;
private const ulong WaitSliceNanoseconds = 1_000_000;

View file

@ -0,0 +1,131 @@
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>
/// 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;
public RenderFrameOrchestrator(
IRenderFrameLifetime lifetime,
IRenderFrameResourcePhase resources,
IWorldSceneFramePhase world,
IPrivatePresentationFramePhase presentation,
IRenderFrameDiagnosticsPhase diagnostics)
{
_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));
}
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
{
try
{
_lifetime.EndFrame();
}
catch (Exception closeFailure) when (renderFailure is not null)
{
throw new AggregateException(
"Rendering failed and the in-flight GPU frame could not be closed.",
renderFailure,
closeFailure);
}
}
}
}