Give terrain, sky, retained UI, portal preparation, and the update/render frame pair explicit single owners. Make shader, texture, text, bindless, and GL construction prefixes checked and retryable so partial failure cannot lose or replay resource ownership. Co-authored-by: Codex <codex@openai.com>
197 lines
7.1 KiB
C#
197 lines
7.1 KiB
C#
using AcDream.App.Rendering;
|
|
using Silk.NET.OpenGL;
|
|
|
|
namespace AcDream.App.Tests.Rendering;
|
|
|
|
public sealed class ShaderProgramConstructionTests
|
|
{
|
|
[Theory]
|
|
[InlineData("ShaderSource:VertexShader", 1, 0)]
|
|
[InlineData("CompileShader:VertexShader", 1, 0)]
|
|
[InlineData("CreateShader:FragmentShader", 1, 0)]
|
|
[InlineData("ShaderSource:FragmentShader", 2, 0)]
|
|
[InlineData("CompileShader:FragmentShader", 2, 0)]
|
|
[InlineData("CreateProgram", 2, 0)]
|
|
[InlineData("AttachShader:VertexShader", 2, 1)]
|
|
[InlineData("AttachShader:FragmentShader", 2, 1)]
|
|
[InlineData("LinkProgram", 2, 1)]
|
|
public void ThrowAfterEachAllocatedNameRollsBackEveryPublishedName(
|
|
string failingOperation,
|
|
int expectedShaderDeletes,
|
|
int expectedProgramDeletes)
|
|
{
|
|
var api = new FakeShaderApi { ThrowOn = failingOperation };
|
|
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
ShaderProgramConstruction.Build(api, "vertex", "fragment"));
|
|
|
|
Assert.Equal(expectedShaderDeletes, api.DeletedShaders.Count);
|
|
Assert.Equal(expectedProgramDeletes, api.DeletedPrograms.Count);
|
|
Assert.Equal(api.CreatedShaders, api.DeletedShaders.Order().ToArray());
|
|
Assert.Equal(api.CreatedPrograms, api.DeletedPrograms.Order().ToArray());
|
|
}
|
|
|
|
[Fact]
|
|
public void CompileFailurePreservesOriginalFailureWhenRollbackIsClean()
|
|
{
|
|
var api = new FakeShaderApi { VertexCompiles = false };
|
|
|
|
InvalidOperationException failure = Assert.Throws<InvalidOperationException>(() =>
|
|
ShaderProgramConstruction.Build(api, "vertex", "fragment"));
|
|
|
|
Assert.Contains("VertexShader compile failed", failure.Message);
|
|
Assert.Equal([1u], api.DeletedShaders);
|
|
Assert.Empty(api.CreatedPrograms);
|
|
}
|
|
|
|
[Fact]
|
|
public void LinkFailureDetachesAndDeletesBothShadersAndProgram()
|
|
{
|
|
var api = new FakeShaderApi { ProgramLinks = false };
|
|
|
|
InvalidOperationException failure = Assert.Throws<InvalidOperationException>(() =>
|
|
ShaderProgramConstruction.Build(api, "vertex", "fragment"));
|
|
|
|
Assert.Contains("program link failed", failure.Message);
|
|
Assert.Equal(["DetachShader:VertexShader", "DetachShader:FragmentShader"], api.DetachCalls);
|
|
Assert.Equal([1u, 2u], api.DeletedShaders);
|
|
Assert.Equal([3u], api.DeletedPrograms);
|
|
}
|
|
|
|
[Fact]
|
|
public void SuccessfulBuildCommitsOnlyProgramAndReleasesTemporaryShaders()
|
|
{
|
|
var api = new FakeShaderApi();
|
|
|
|
uint program = ShaderProgramConstruction.Build(api, "vertex", "fragment");
|
|
|
|
Assert.Equal(3u, program);
|
|
Assert.Equal([1u, 2u], api.DeletedShaders);
|
|
Assert.Empty(api.DeletedPrograms);
|
|
Assert.Equal(["DetachShader:VertexShader", "DetachShader:FragmentShader"], api.DetachCalls);
|
|
}
|
|
|
|
[Fact]
|
|
public void CleanupFailureStillAttemptsEveryOtherReleaseAndProgramRollback()
|
|
{
|
|
var api = new FakeShaderApi { ThrowOn = "DetachShader:VertexShader" };
|
|
|
|
AggregateException failure = Assert.Throws<AggregateException>(() =>
|
|
ShaderProgramConstruction.Build(api, "vertex", "fragment"));
|
|
|
|
Assert.Single(failure.InnerExceptions);
|
|
Assert.Equal(["DetachShader:VertexShader", "DetachShader:FragmentShader"], api.DetachCalls);
|
|
Assert.Equal([1u, 2u], api.DeletedShaders);
|
|
Assert.Equal([3u], api.DeletedPrograms);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConstructionAndRollbackFailuresAreReportedTogether()
|
|
{
|
|
var api = new FakeShaderApi
|
|
{
|
|
ProgramLinks = false,
|
|
ThrowOn = "DeleteShader:VertexShader",
|
|
};
|
|
|
|
GlResourceConstructionException failure = Assert.Throws<GlResourceConstructionException>(() =>
|
|
ShaderProgramConstruction.Build(api, "vertex", "fragment"));
|
|
|
|
Assert.Equal(2, failure.InnerExceptions.Count);
|
|
Assert.Contains("program link failed", failure.InnerExceptions[0].Message);
|
|
Assert.Equal([1u, 2u], api.DeleteShaderAttempts);
|
|
Assert.Equal([2u], api.DeletedShaders);
|
|
Assert.Equal([3u], api.DeletedPrograms);
|
|
|
|
api.ThrowOn = null;
|
|
failure.RetryCleanup();
|
|
|
|
Assert.True(failure.IsCleanupComplete);
|
|
Assert.Equal([2u, 1u], api.DeletedShaders);
|
|
Assert.Equal([1u, 2u, 1u], api.DeleteShaderAttempts);
|
|
}
|
|
|
|
private sealed class FakeShaderApi : IShaderProgramBuildApi
|
|
{
|
|
private readonly Dictionary<uint, ShaderType> _shaderTypes = [];
|
|
private uint _nextName = 1;
|
|
|
|
public string? ThrowOn { get; set; }
|
|
public bool VertexCompiles { get; init; } = true;
|
|
public bool FragmentCompiles { get; init; } = true;
|
|
public bool ProgramLinks { get; init; } = true;
|
|
public List<uint> CreatedShaders { get; } = [];
|
|
public List<uint> CreatedPrograms { get; } = [];
|
|
public List<uint> DeleteShaderAttempts { get; } = [];
|
|
public List<uint> DeletedShaders { get; } = [];
|
|
public List<uint> DeletedPrograms { get; } = [];
|
|
public List<string> DetachCalls { get; } = [];
|
|
|
|
public uint CreateShader(ShaderType type)
|
|
{
|
|
Fail($"CreateShader:{type}");
|
|
uint name = _nextName++;
|
|
CreatedShaders.Add(name);
|
|
_shaderTypes.Add(name, type);
|
|
return name;
|
|
}
|
|
|
|
public void ShaderSource(uint shader, string source) =>
|
|
Fail($"ShaderSource:{TypeOf(shader)}");
|
|
|
|
public void CompileShader(uint shader) =>
|
|
Fail($"CompileShader:{TypeOf(shader)}");
|
|
|
|
public int GetShaderCompileStatus(uint shader) => TypeOf(shader) switch
|
|
{
|
|
ShaderType.VertexShader => VertexCompiles ? 1 : 0,
|
|
ShaderType.FragmentShader => FragmentCompiles ? 1 : 0,
|
|
_ => 0,
|
|
};
|
|
|
|
public string GetShaderInfoLog(uint shader) => $"bad {TypeOf(shader)}";
|
|
|
|
public uint CreateProgram()
|
|
{
|
|
Fail("CreateProgram");
|
|
uint name = _nextName++;
|
|
CreatedPrograms.Add(name);
|
|
return name;
|
|
}
|
|
|
|
public void AttachShader(uint program, uint shader) =>
|
|
Fail($"AttachShader:{TypeOf(shader)}");
|
|
|
|
public void LinkProgram(uint program) => Fail("LinkProgram");
|
|
public int GetProgramLinkStatus(uint program) => ProgramLinks ? 1 : 0;
|
|
public string GetProgramInfoLog(uint program) => "bad link";
|
|
|
|
public void DetachShader(uint program, uint shader)
|
|
{
|
|
string operation = $"DetachShader:{TypeOf(shader)}";
|
|
DetachCalls.Add(operation);
|
|
Fail(operation);
|
|
}
|
|
|
|
public void DeleteShader(uint shader)
|
|
{
|
|
DeleteShaderAttempts.Add(shader);
|
|
Fail($"DeleteShader:{TypeOf(shader)}");
|
|
DeletedShaders.Add(shader);
|
|
}
|
|
|
|
public void DeleteProgram(uint program)
|
|
{
|
|
Fail("DeleteProgram");
|
|
DeletedPrograms.Add(program);
|
|
}
|
|
|
|
private ShaderType TypeOf(uint shader) => _shaderTypes[shader];
|
|
|
|
private void Fail(string operation)
|
|
{
|
|
if (ThrowOn == operation)
|
|
throw new InvalidOperationException(operation + " failed");
|
|
}
|
|
}
|
|
}
|