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>
217 lines
8 KiB
C#
217 lines
8 KiB
C#
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.");
|
|
}
|
|
}
|