Construct Slice F's non-drawing scene only for lifecycle automation, drain accepted static and live deltas at the final update boundary, and compare exact current-path fingerprints at cadence and checkpoints. Publish bounded mismatch, journal, index, digest, and memory evidence without changing normal launches or draw submission. Release: 8,211 passed, 5 skipped.
287 lines
9.4 KiB
C#
287 lines
9.4 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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)
|
|
{
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <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 : 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);
|
|
}
|
|
}
|
|
}
|
|
}
|