using System.Reflection;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu.Gl;
using Silk.NET.OpenGL;
namespace AcDream.App.Tests.Rendering;
///
/// The retained UI's whole draw is one RHI pass, and a failure anywhere inside
/// it must not leave GL state behind for the raw-GL world renderers that run in
/// the next frame. Until Campaign V slice V6d, TextRenderer owned a private
/// state scope for that; V6d made the renderer backend-neutral and moved the
/// guarantee onto , which every RHI pass
/// gets. These tests follow it there.
///
public sealed class TextRendererFailureSafetyTests
{
[Fact]
public void Flush_CompilesThePassEncoderAsFinallyAroundBothDrawLayers()
{
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"));
// The encoder's `using` is the finally: disposing it closes the pass,
// which is what restores the ambient capability state the pass changed.
Assert.Contains(
body.ExceptionHandlingClauses,
clause => clause.Flags == ExceptionHandlingClauseOptions.Finally);
AssertAppearsInOrder(
source,
"using IGpuPassEncoder encoder = frame.BeginPass(new GpuPassDescription",
"encoder.BindPipeline(_pipeline);",
"DrawLayer(_spriteSegs,",
"DrawLayer(_overlaySpriteSegs,");
// And no raw GL of its own is left: the renderer draws on both backends.
Assert.DoesNotContain("Silk.NET.OpenGL", source, StringComparison.Ordinal);
Assert.DoesNotContain("_gl.", source, StringComparison.Ordinal);
}
[Fact]
public void FailedDraw_RestoresEveryGlValueMutatedByThePass()
{
var gl = new RecordingGlState
{
DepthWrite = false,
DepthFuncValue = DepthFunction.Greater,
BlendSourceRgb = BlendingFactor.DstAlpha,
BlendDestinationRgb = BlendingFactor.OneMinusDstAlpha,
BlendSourceAlpha = BlendingFactor.One,
BlendDestinationAlpha = BlendingFactor.Zero,
CullFaceMode = TriangleFace.Front,
FrontFaceDirection = FrontFaceDirection.CW,
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);
// Enabled on entry — the world's MSAA. The UI pass turns it off and the
// restore has to put it back, which is the exact dimension the V4a
// revert lost and which nothing covered before this test.
gl.SetCapability(EnableCap.Multisample, enabled: true);
gl.TextureBindings[TextureUnit.Texture0] = 41;
gl.TextureBindings[TextureUnit.Texture2] = 43;
StateSnapshot expected = gl.Capture();
Action failedDraw = () =>
{
GlAmbientCapabilityState ambient = GlAmbientCapabilityState.Capture(gl);
try
{
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: false);
gl.DepthMask(true);
gl.DepthFunc(DepthFunction.Lequal);
gl.BlendFuncSeparate(
BlendingFactor.SrcAlpha,
BlendingFactor.OneMinusSrcAlpha,
BlendingFactor.SrcAlpha,
BlendingFactor.OneMinusSrcAlpha);
gl.CullFace(TriangleFace.Back);
gl.FrontFace(FrontFaceDirection.Ccw);
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");
}
finally
{
ambient.Restore(gl);
}
};
Assert.Throws(failedDraw);
Assert.Equal(expected, gl.Capture());
}
private readonly record struct StateSnapshot(
bool DepthTest,
bool Blend,
bool Cull,
bool AlphaToCoverage,
bool Multisample,
bool DepthWrite,
DepthFunction DepthFunc,
BlendingFactor BlendSourceRgb,
BlendingFactor BlendDestinationRgb,
BlendingFactor BlendSourceAlpha,
BlendingFactor BlendDestinationAlpha,
TriangleFace CullFaceMode,
FrontFaceDirection FrontFaceDirection,
uint Program,
uint VertexArray,
uint ArrayBuffer,
TextureUnit ActiveUnit,
uint Texture0,
uint Texture2,
bool StencilTest,
StencilFunction StencilFunc,
int StencilReference,
uint StencilValueMask,
uint StencilWriteMask,
Silk.NET.OpenGL.StencilOp StencilFail,
Silk.NET.OpenGL.StencilOp StencilDepthFail,
Silk.NET.OpenGL.StencilOp StencilPass,
bool ColorMaskRed,
bool ColorMaskGreen,
bool ColorMaskBlue,
bool ColorMaskAlpha);
private sealed class RecordingGlState : IGlAmbientStateApi
{
private readonly Dictionary _capabilities = [];
public bool DepthWrite { get; set; }
public DepthFunction DepthFuncValue { get; set; }
public BlendingFactor BlendSourceRgb { get; set; }
public BlendingFactor BlendDestinationRgb { get; set; }
public BlendingFactor BlendSourceAlpha { get; set; }
public BlendingFactor BlendDestinationAlpha { get; set; }
public TriangleFace CullFaceMode { get; set; }
public FrontFaceDirection FrontFaceDirection { get; set; }
public uint Program { get; set; }
public uint VertexArray { get; set; }
public uint ArrayBuffer { get; set; }
public TextureUnit ActiveUnit { get; set; }
public Dictionary TextureBindings { get; } = [];
// Slice V6l added the stencil and colour-mask dimensions to the ambient
// save/restore, for the portal depth mask's sake. GL's own defaults.
public StencilFunction StencilFuncValue { get; set; } = StencilFunction.Always;
public int StencilReference { get; set; }
public uint StencilValueMask { get; set; } = 0xFFFFFFFFu;
public uint StencilWriteMaskValue { get; set; } = 0xFFFFFFFFu;
public Silk.NET.OpenGL.StencilOp StencilFailOp { get; set; } = Silk.NET.OpenGL.StencilOp.Keep;
public Silk.NET.OpenGL.StencilOp StencilDepthFailOp { get; set; } = Silk.NET.OpenGL.StencilOp.Keep;
public Silk.NET.OpenGL.StencilOp StencilPassOp { get; set; } = Silk.NET.OpenGL.StencilOp.Keep;
public bool[] ColorMaskValue { get; } = [true, true, true, true];
public StateSnapshot Capture() => new(
IsEnabled(EnableCap.DepthTest),
IsEnabled(EnableCap.Blend),
IsEnabled(EnableCap.CullFace),
IsEnabled(EnableCap.SampleAlphaToCoverage),
IsEnabled(EnableCap.Multisample),
DepthWrite,
DepthFuncValue,
BlendSourceRgb,
BlendDestinationRgb,
BlendSourceAlpha,
BlendDestinationAlpha,
CullFaceMode,
FrontFaceDirection,
Program,
VertexArray,
ArrayBuffer,
ActiveUnit,
TextureBindings.GetValueOrDefault(TextureUnit.Texture0),
TextureBindings.GetValueOrDefault(TextureUnit.Texture2),
IsEnabled(EnableCap.StencilTest),
StencilFuncValue,
StencilReference,
StencilValueMask,
StencilWriteMaskValue,
StencilFailOp,
StencilDepthFailOp,
StencilPassOp,
ColorMaskValue[0],
ColorMaskValue[1],
ColorMaskValue[2],
ColorMaskValue[3]);
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.DepthFunc => (int)DepthFuncValue,
GetPName.CullFaceMode => (int)CullFaceMode,
GetPName.FrontFace => (int)FrontFaceDirection,
GetPName.CurrentProgram => (int)Program,
GetPName.VertexArrayBinding => (int)VertexArray,
GetPName.ArrayBufferBinding => (int)ArrayBuffer,
GetPName.ActiveTexture => (int)ActiveUnit,
GetPName.TextureBinding2D =>
(int)TextureBindings.GetValueOrDefault(ActiveUnit),
GetPName.StencilFunc => (int)StencilFuncValue,
GetPName.StencilRef => StencilReference,
GetPName.StencilValueMask => (int)StencilValueMask,
GetPName.StencilWritemask => (int)StencilWriteMaskValue,
GetPName.StencilFail => (int)StencilFailOp,
GetPName.StencilPassDepthFail => (int)StencilDepthFailOp,
GetPName.StencilPassDepthPass => (int)StencilPassOp,
_ => throw new ArgumentOutOfRangeException(nameof(parameter)),
};
public bool[] GetColorMask() => (bool[])ColorMaskValue.Clone();
public void StencilFunc(StencilFunction function, int reference, uint mask)
{
StencilFuncValue = function;
StencilReference = reference;
StencilValueMask = mask;
}
public void StencilOp(
Silk.NET.OpenGL.StencilOp fail,
Silk.NET.OpenGL.StencilOp depthFail,
Silk.NET.OpenGL.StencilOp pass)
{
StencilFailOp = fail;
StencilDepthFailOp = depthFail;
StencilPassOp = pass;
}
public void StencilMask(uint mask) => StencilWriteMaskValue = mask;
public void ColorMask(bool red, bool green, bool blue, bool alpha)
{
ColorMaskValue[0] = red;
ColorMaskValue[1] = green;
ColorMaskValue[2] = blue;
ColorMaskValue[3] = alpha;
}
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 DepthFunc(DepthFunction function) => DepthFuncValue = function;
public void BlendFuncSeparate(
BlendingFactor sourceRgb,
BlendingFactor destinationRgb,
BlendingFactor sourceAlpha,
BlendingFactor destinationAlpha)
{
BlendSourceRgb = sourceRgb;
BlendDestinationRgb = destinationRgb;
BlendSourceAlpha = sourceAlpha;
BlendDestinationAlpha = destinationAlpha;
}
public void CullFace(TriangleFace face) => CullFaceMode = face;
public void FrontFace(FrontFaceDirection direction) => FrontFaceDirection = direction;
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.");
}
}