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
|
|
@ -0,0 +1,217 @@
|
|||
using System.Reflection;
|
||||
using AcDream.App.Rendering;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
public sealed class TextRendererFailureSafetyTests
|
||||
{
|
||||
[Fact]
|
||||
public void Flush_CompilesCompleteGlStateScopeAsFinallyAroundBothDrawLayers()
|
||||
{
|
||||
MethodInfo flush = typeof(TextRenderer).GetMethod(nameof(TextRenderer.Flush))!;
|
||||
MethodBody body = flush.GetMethodBody()!;
|
||||
string source = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Rendering",
|
||||
"TextRenderer.cs"));
|
||||
|
||||
Assert.Contains(
|
||||
body.ExceptionHandlingClauses,
|
||||
clause => clause.Flags == ExceptionHandlingClauseOptions.Finally);
|
||||
AssertAppearsInOrder(
|
||||
source,
|
||||
"using var stateScope = new TextRenderGlStateScope(_glState);",
|
||||
"_gl.Disable(EnableCap.Multisample);",
|
||||
"DrawLayer(_spriteSegs,",
|
||||
"DrawLayer(_overlaySpriteSegs,");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FailedDraw_RestoresEveryGlValueMutatedByTheTextPass()
|
||||
{
|
||||
var gl = new RecordingGlState
|
||||
{
|
||||
DepthWrite = false,
|
||||
BlendSourceRgb = BlendingFactor.DstAlpha,
|
||||
BlendDestinationRgb = BlendingFactor.OneMinusDstAlpha,
|
||||
BlendSourceAlpha = BlendingFactor.One,
|
||||
BlendDestinationAlpha = BlendingFactor.Zero,
|
||||
Program = 17,
|
||||
VertexArray = 23,
|
||||
ArrayBuffer = 31,
|
||||
ActiveUnit = TextureUnit.Texture2,
|
||||
};
|
||||
gl.SetCapability(EnableCap.DepthTest, enabled: true);
|
||||
gl.SetCapability(EnableCap.Blend, enabled: false);
|
||||
gl.SetCapability(EnableCap.CullFace, enabled: true);
|
||||
gl.SetCapability(EnableCap.SampleAlphaToCoverage, enabled: true);
|
||||
gl.SetCapability(EnableCap.Multisample, enabled: false);
|
||||
gl.TextureBindings[TextureUnit.Texture0] = 41;
|
||||
gl.TextureBindings[TextureUnit.Texture2] = 43;
|
||||
StateSnapshot expected = gl.Capture();
|
||||
|
||||
Action failedDraw = () =>
|
||||
{
|
||||
using var stateScope = new TextRenderGlStateScope(gl);
|
||||
gl.SetCapability(EnableCap.DepthTest, enabled: false);
|
||||
gl.SetCapability(EnableCap.Blend, enabled: true);
|
||||
gl.SetCapability(EnableCap.CullFace, enabled: false);
|
||||
gl.SetCapability(EnableCap.SampleAlphaToCoverage, enabled: false);
|
||||
gl.SetCapability(EnableCap.Multisample, enabled: true);
|
||||
gl.DepthMask(true);
|
||||
gl.BlendFuncSeparate(
|
||||
BlendingFactor.SrcAlpha,
|
||||
BlendingFactor.OneMinusSrcAlpha,
|
||||
BlendingFactor.SrcAlpha,
|
||||
BlendingFactor.OneMinusSrcAlpha);
|
||||
gl.UseProgram(101);
|
||||
gl.BindVertexArray(103);
|
||||
gl.BindBuffer(BufferTargetARB.ArrayBuffer, 107);
|
||||
gl.ActiveTexture(TextureUnit.Texture0);
|
||||
gl.BindTexture(TextureTarget.Texture2D, 109);
|
||||
throw new InvalidOperationException("draw upload");
|
||||
};
|
||||
|
||||
Assert.Throws<InvalidOperationException>(failedDraw);
|
||||
|
||||
Assert.Equal(expected, gl.Capture());
|
||||
}
|
||||
|
||||
private readonly record struct StateSnapshot(
|
||||
bool DepthTest,
|
||||
bool Blend,
|
||||
bool Cull,
|
||||
bool AlphaToCoverage,
|
||||
bool Multisample,
|
||||
bool DepthWrite,
|
||||
BlendingFactor BlendSourceRgb,
|
||||
BlendingFactor BlendDestinationRgb,
|
||||
BlendingFactor BlendSourceAlpha,
|
||||
BlendingFactor BlendDestinationAlpha,
|
||||
uint Program,
|
||||
uint VertexArray,
|
||||
uint ArrayBuffer,
|
||||
TextureUnit ActiveUnit,
|
||||
uint Texture0,
|
||||
uint Texture2);
|
||||
|
||||
private sealed class RecordingGlState : ITextRenderGlStateApi
|
||||
{
|
||||
private readonly Dictionary<EnableCap, bool> _capabilities = [];
|
||||
|
||||
public bool DepthWrite { get; set; }
|
||||
public BlendingFactor BlendSourceRgb { get; set; }
|
||||
public BlendingFactor BlendDestinationRgb { get; set; }
|
||||
public BlendingFactor BlendSourceAlpha { get; set; }
|
||||
public BlendingFactor BlendDestinationAlpha { get; set; }
|
||||
public uint Program { get; set; }
|
||||
public uint VertexArray { get; set; }
|
||||
public uint ArrayBuffer { get; set; }
|
||||
public TextureUnit ActiveUnit { get; set; }
|
||||
public Dictionary<TextureUnit, uint> TextureBindings { get; } = [];
|
||||
|
||||
public StateSnapshot Capture() => new(
|
||||
IsEnabled(EnableCap.DepthTest),
|
||||
IsEnabled(EnableCap.Blend),
|
||||
IsEnabled(EnableCap.CullFace),
|
||||
IsEnabled(EnableCap.SampleAlphaToCoverage),
|
||||
IsEnabled(EnableCap.Multisample),
|
||||
DepthWrite,
|
||||
BlendSourceRgb,
|
||||
BlendDestinationRgb,
|
||||
BlendSourceAlpha,
|
||||
BlendDestinationAlpha,
|
||||
Program,
|
||||
VertexArray,
|
||||
ArrayBuffer,
|
||||
ActiveUnit,
|
||||
TextureBindings.GetValueOrDefault(TextureUnit.Texture0),
|
||||
TextureBindings.GetValueOrDefault(TextureUnit.Texture2));
|
||||
|
||||
public bool IsEnabled(EnableCap capability) =>
|
||||
_capabilities.GetValueOrDefault(capability);
|
||||
|
||||
public int GetInteger(GetPName parameter) => parameter switch
|
||||
{
|
||||
GetPName.BlendSrcRgb => (int)BlendSourceRgb,
|
||||
GetPName.BlendDstRgb => (int)BlendDestinationRgb,
|
||||
GetPName.BlendSrcAlpha => (int)BlendSourceAlpha,
|
||||
GetPName.BlendDstAlpha => (int)BlendDestinationAlpha,
|
||||
GetPName.CurrentProgram => (int)Program,
|
||||
GetPName.VertexArrayBinding => (int)VertexArray,
|
||||
GetPName.ArrayBufferBinding => (int)ArrayBuffer,
|
||||
GetPName.ActiveTexture => (int)ActiveUnit,
|
||||
GetPName.TextureBinding2D =>
|
||||
(int)TextureBindings.GetValueOrDefault(ActiveUnit),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(parameter)),
|
||||
};
|
||||
|
||||
public bool GetBoolean(GetPName parameter) =>
|
||||
parameter == GetPName.DepthWritemask
|
||||
? DepthWrite
|
||||
: throw new ArgumentOutOfRangeException(nameof(parameter));
|
||||
|
||||
public void SetCapability(EnableCap capability, bool enabled) =>
|
||||
_capabilities[capability] = enabled;
|
||||
|
||||
public void DepthMask(bool enabled) => DepthWrite = enabled;
|
||||
|
||||
public void BlendFuncSeparate(
|
||||
BlendingFactor sourceRgb,
|
||||
BlendingFactor destinationRgb,
|
||||
BlendingFactor sourceAlpha,
|
||||
BlendingFactor destinationAlpha)
|
||||
{
|
||||
BlendSourceRgb = sourceRgb;
|
||||
BlendDestinationRgb = destinationRgb;
|
||||
BlendSourceAlpha = sourceAlpha;
|
||||
BlendDestinationAlpha = destinationAlpha;
|
||||
}
|
||||
|
||||
public void UseProgram(uint program) => Program = program;
|
||||
|
||||
public void BindVertexArray(uint vertexArray) => VertexArray = vertexArray;
|
||||
|
||||
public void BindBuffer(BufferTargetARB target, uint buffer)
|
||||
{
|
||||
Assert.Equal(BufferTargetARB.ArrayBuffer, target);
|
||||
ArrayBuffer = buffer;
|
||||
}
|
||||
|
||||
public void ActiveTexture(TextureUnit unit) => ActiveUnit = unit;
|
||||
|
||||
public void BindTexture(TextureTarget target, uint texture)
|
||||
{
|
||||
Assert.Equal(TextureTarget.Texture2D, target);
|
||||
TextureBindings[ActiveUnit] = texture;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertAppearsInOrder(string source, params string[] needles)
|
||||
{
|
||||
int cursor = -1;
|
||||
foreach (string needle in needles)
|
||||
{
|
||||
int next = source.IndexOf(needle, cursor + 1, StringComparison.Ordinal);
|
||||
Assert.True(next >= 0, $"Missing expected source fragment: {needle}");
|
||||
Assert.True(next > cursor, $"Out-of-order source fragment: {needle}");
|
||||
cursor = next;
|
||||
}
|
||||
}
|
||||
|
||||
private static string FindRepoRoot()
|
||||
{
|
||||
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
||||
while (directory is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
|
||||
return directory.FullName;
|
||||
directory = directory.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue