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>
This commit is contained in:
parent
28e1cf8029
commit
9d7df1bfc5
25 changed files with 1675 additions and 279 deletions
|
|
@ -42,8 +42,10 @@ public sealed class DevToolsFramePresenterTests
|
|||
var commands = new RecordingCommandSource { Current = first };
|
||||
var presenter = Create(backend, commands: commands);
|
||||
|
||||
presenter.BeginFrame(0.1f);
|
||||
presenter.Render(0.1, 800, 600);
|
||||
commands.Current = second;
|
||||
presenter.BeginFrame(0.2f);
|
||||
presenter.Render(0.2, 800, 600);
|
||||
|
||||
Assert.Same(first, backend.Contexts[0].Commands);
|
||||
|
|
@ -60,6 +62,7 @@ public sealed class DevToolsFramePresenterTests
|
|||
var panels = new RecordingPanels();
|
||||
var presenter = Create(backend, panels: panels);
|
||||
|
||||
presenter.BeginFrame(0.1f);
|
||||
presenter.Render(0.1, 1920, 1080);
|
||||
|
||||
Assert.Equal(
|
||||
|
|
@ -93,6 +96,7 @@ public sealed class DevToolsFramePresenterTests
|
|||
var camera = new RecordingCamera { IsFlyMode = true, CollideCamera = true };
|
||||
var presenter = Create(backend, camera: camera);
|
||||
|
||||
presenter.BeginFrame(0.1f);
|
||||
presenter.Render(0.1, 1280, 720);
|
||||
|
||||
Assert.Equal(1, camera.ToggleCount);
|
||||
|
|
@ -113,6 +117,7 @@ public sealed class DevToolsFramePresenterTests
|
|||
|
||||
presenter.ToggleSettingsPanel();
|
||||
presenter.ResetLayout(1280, 720, DevToolsPanelLayoutCondition.FirstUseEver);
|
||||
presenter.BeginFrame(0.1f);
|
||||
presenter.Render(0.1, 1280, 720);
|
||||
|
||||
Assert.DoesNotContain(DevToolsPanelKind.Settings, panels.Toggles);
|
||||
|
|
@ -156,6 +161,68 @@ public sealed class DevToolsFramePresenterTests
|
|||
call.WithoutCondition()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AbortFrame_ClosesAnOpenBackendFrameAndAllowsTheNextFrame()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var backend = new RecordingBackend(calls);
|
||||
var presenter = Create(backend);
|
||||
|
||||
presenter.BeginFrame(0.1f);
|
||||
presenter.AbortFrame();
|
||||
presenter.AbortFrame();
|
||||
presenter.BeginFrame(0.2f);
|
||||
presenter.Render(0.2, 800, 600);
|
||||
|
||||
Assert.Equal(1, calls.Count(call => call == "abort"));
|
||||
Assert.Equal(2, calls.Count(call => call.StartsWith("begin:")));
|
||||
Assert.Equal(1, calls.Count(call => call == "draw-data"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RenderFailure_RemainsAbortableAndDoesNotPoisonTheNextFrame()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var backend = new RecordingBackend(calls)
|
||||
{
|
||||
DrawFailure = new InvalidOperationException("draw"),
|
||||
};
|
||||
var presenter = Create(backend);
|
||||
|
||||
presenter.BeginFrame(0.1f);
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
presenter.Render(0.1, 800, 600));
|
||||
presenter.AbortFrame();
|
||||
|
||||
backend.DrawFailure = null;
|
||||
presenter.BeginFrame(0.2f);
|
||||
presenter.Render(0.2, 800, 600);
|
||||
|
||||
Assert.Equal(0, calls.Count(call => call == "abort"));
|
||||
Assert.Equal(2, calls.Count(call => call == "draw-data"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BeginFailure_RemainsAbortableAndDoesNotPoisonTheNextFrame()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var backend = new RecordingBackend(calls)
|
||||
{
|
||||
BeginFailure = new InvalidOperationException("begin"),
|
||||
};
|
||||
var presenter = Create(backend);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => presenter.BeginFrame(0.1f));
|
||||
presenter.AbortFrame();
|
||||
|
||||
backend.BeginFailure = null;
|
||||
presenter.BeginFrame(0.2f);
|
||||
presenter.Render(0.2, 800, 600);
|
||||
|
||||
Assert.Equal(1, calls.Count(call => call == "abort"));
|
||||
Assert.Equal(2, calls.Count(call => call.StartsWith("begin:")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InputFacingActionsDelegateToTheTypedPanelOwner()
|
||||
{
|
||||
|
|
@ -198,7 +265,7 @@ public sealed class DevToolsFramePresenterTests
|
|||
field => field.FieldType == typeof(Silk.NET.Windowing.IWindow));
|
||||
}
|
||||
|
||||
Assert.False(typeof(IDisposable).IsAssignableFrom(
|
||||
Assert.True(typeof(IDisposable).IsAssignableFrom(
|
||||
typeof(ImGuiDevToolsFrameBackend)));
|
||||
}
|
||||
|
||||
|
|
@ -221,8 +288,29 @@ public sealed class DevToolsFramePresenterTests
|
|||
public List<(string Label, string? Shortcut, bool Selected)> MenuItems { get; } = [];
|
||||
public List<PanelContext> Contexts { get; } = [];
|
||||
public List<LayoutCallWithCondition> Layouts { get; } = [];
|
||||
public Exception? BeginFailure { get; set; }
|
||||
public Exception? DrawFailure { get; set; }
|
||||
|
||||
public void BeginFrame(float deltaSeconds) => calls.Add($"begin:{deltaSeconds}");
|
||||
public bool ControllerFrameOpen { get; private set; }
|
||||
|
||||
public void BeginFrame(float deltaSeconds)
|
||||
{
|
||||
calls.Add($"begin:{deltaSeconds}");
|
||||
ControllerFrameOpen = true;
|
||||
if (BeginFailure is not null)
|
||||
throw BeginFailure;
|
||||
}
|
||||
|
||||
public void AbortFrame()
|
||||
{
|
||||
// Silk ImGuiController.Render is idempotent. It closes an active
|
||||
// controller frame, but is a no-op if Render already cleared its
|
||||
// private _frameBegun flag before a GL upload failure.
|
||||
if (!ControllerFrameOpen)
|
||||
return;
|
||||
ControllerFrameOpen = false;
|
||||
calls.Add("abort");
|
||||
}
|
||||
|
||||
public bool BeginMainMenuBar()
|
||||
{
|
||||
|
|
@ -254,7 +342,14 @@ public sealed class DevToolsFramePresenterTests
|
|||
calls.Add($"panels:{context.DeltaSeconds}");
|
||||
}
|
||||
|
||||
public void RenderDrawData() => calls.Add("draw-data");
|
||||
public void RenderDrawData()
|
||||
{
|
||||
Assert.True(ControllerFrameOpen);
|
||||
ControllerFrameOpen = false;
|
||||
calls.Add("draw-data");
|
||||
if (DrawFailure is not null)
|
||||
throw DrawFailure;
|
||||
}
|
||||
|
||||
public void SetWindowLayout(
|
||||
string title,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue