refactor(lifetime): own render resource prefixes
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>
This commit is contained in:
parent
fec0d94148
commit
c87b15303d
41 changed files with 3768 additions and 355 deletions
|
|
@ -0,0 +1,197 @@
|
|||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue