acdream/tests/AcDream.App.Tests/Rendering/RenderFrameRecoveryIntegrationTests.cs
Erik 9d7df1bfc5 refactor(render): compose render frame orchestrator
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>
2026-07-22 08:03:49 +02:00

178 lines
5.1 KiB
C#

using AcDream.App.Rendering;
namespace AcDream.App.Tests.Rendering;
public sealed class RenderFrameRecoveryIntegrationTests
{
private static readonly RenderFrameInput Input = new(1d / 60d, 1280, 720);
[Theory]
[InlineData("devtools-begin")]
[InlineData("weather")]
[InlineData("world")]
[InlineData("portal")]
[InlineData("paperdoll")]
[InlineData("gameplay-ui")]
[InlineData("devtools-render")]
public void DownstreamFailure_AbortsDevToolsBeforeGpuClose_ThenNextFrameSucceeds(
string failurePoint)
{
var failure = new FailureSwitch { Point = failurePoint };
var gpu = new GpuLifetime(failure);
var devTools = new DevTools(failure);
var preparation = new RenderFramePreparationController(
new Resources(),
devTools,
new Weather(failure));
var presentation = new PrivatePresentationRenderer(
new Portal(failure),
new FoundationSource(),
new Paperdoll(failure),
new GameplayUi(failure),
devTools,
new Screenshots());
var orchestrator = new RenderFrameOrchestrator(
gpu,
preparation,
new World(failure),
presentation,
new Diagnostics(),
devTools);
Assert.Throws<InvalidOperationException>(() => orchestrator.Render(Input));
Assert.False(devTools.IsOpen);
Assert.Equal(1, devTools.AbortCount);
Assert.Equal(1, gpu.EndCount);
Assert.True(devTools.AbortSequence < gpu.LastEndSequence);
failure.Point = null;
RenderFrameOutcome outcome = orchestrator.Render(Input);
Assert.False(devTools.IsOpen);
Assert.Equal(2, devTools.BeginCount);
Assert.Equal(2, gpu.EndCount);
Assert.True(outcome.World.NormalWorldDrawn);
}
private sealed class FailureSwitch
{
private int _sequence;
public string? Point { get; set; }
public int NextSequence() => ++_sequence;
public void ThrowIf(string point)
{
if (Point == point)
throw new InvalidOperationException(point);
}
}
private sealed class GpuLifetime(FailureSwitch failure) : IRenderFrameLifetime
{
public int EndCount { get; private set; }
public int LastEndSequence { get; private set; }
public void BeginFrame()
{
}
public void EndFrame()
{
EndCount++;
LastEndSequence = failure.NextSequence();
}
}
private sealed class DevTools(FailureSwitch failure) : IDevToolsFrameLifecycle
{
public bool IsOpen { get; private set; }
public int BeginCount { get; private set; }
public int AbortCount { get; private set; }
public int AbortSequence { get; private set; }
public void BeginFrame(float deltaSeconds)
{
Assert.False(IsOpen);
IsOpen = true;
BeginCount++;
failure.ThrowIf("devtools-begin");
}
public void Render(double deltaSeconds, int viewportWidth, int viewportHeight)
{
Assert.True(IsOpen);
failure.ThrowIf("devtools-render");
IsOpen = false;
}
public void AbortFrame()
{
if (!IsOpen)
return;
AbortCount++;
AbortSequence = failure.NextSequence();
IsOpen = false;
}
}
private sealed class Resources : IRenderFrameResourcePhase
{
public void Prepare(RenderFrameInput input)
{
}
}
private sealed class Weather(FailureSwitch failure) : IRenderWeatherFramePhase
{
public void Tick(double deltaSeconds) => failure.ThrowIf("weather");
}
private sealed class World(FailureSwitch failure) : IWorldSceneFramePhase
{
public WorldRenderFrameOutcome Render(RenderFrameInput input)
{
failure.ThrowIf("world");
return new WorldRenderFrameOutcome(1, 1, NormalWorldDrawn: true);
}
}
private sealed class Portal(FailureSwitch failure) : IPrivatePortalViewport
{
public void Draw(int width, int height) => failure.ThrowIf("portal");
}
private sealed class Paperdoll(FailureSwitch failure) : IPrivatePaperdollFrame
{
public void Render() => failure.ThrowIf("paperdoll");
}
private sealed class GameplayUi(FailureSwitch failure) : IRetainedGameplayUiFrame
{
public void Render(double deltaSeconds, int width, int height) =>
failure.ThrowIf("gameplay-ui");
}
private sealed class FoundationSource : IRenderFrameFoundationSource
{
public RenderFrameFoundation Foundation { get; } = new(
PortalViewportVisible: false,
default,
default);
}
private sealed class Screenshots : IPrivateFrameScreenshot
{
public bool CapturePending(int width, int height) => false;
}
private sealed class Diagnostics : IRenderFrameDiagnosticsPhase
{
public void Publish(RenderFrameInput input, RenderFrameOutcome outcome)
{
}
}
}