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>
396 lines
14 KiB
C#
396 lines
14 KiB
C#
using AcDream.App.Rendering;
|
|
|
|
namespace AcDream.App.Tests.Rendering;
|
|
|
|
public sealed class GlTextureOwnershipTests
|
|
{
|
|
[Fact]
|
|
public void ConstructionRollbackDeletesAllNamesInReverseOrder()
|
|
{
|
|
var api = new FakeTextureNameApi();
|
|
var transaction = new GlTextureConstructionTransaction(api);
|
|
|
|
Assert.Equal(1u, transaction.Allocate());
|
|
Assert.Equal(2u, transaction.Allocate());
|
|
Assert.Equal(3u, transaction.Allocate());
|
|
|
|
transaction.Rollback();
|
|
transaction.Rollback();
|
|
|
|
Assert.Equal([3u, 2u, 1u], api.DeleteAttempts);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConstructionRollbackAttemptsEveryNameAndReportsFailures()
|
|
{
|
|
var api = new FakeTextureNameApi { FailingDelete = 2 };
|
|
var transaction = new GlTextureConstructionTransaction(api);
|
|
_ = transaction.Allocate();
|
|
_ = transaction.Allocate();
|
|
_ = transaction.Allocate();
|
|
|
|
AggregateException failure = Assert.Throws<AggregateException>(transaction.Rollback);
|
|
|
|
Assert.Single(failure.InnerExceptions);
|
|
Assert.Equal([3u, 2u, 1u], api.DeleteAttempts);
|
|
Assert.Equal([3u, 1u], api.Deleted);
|
|
|
|
api.FailingDelete = null;
|
|
transaction.RetryCleanup();
|
|
|
|
Assert.True(transaction.IsCleanupComplete);
|
|
Assert.Equal([3u, 2u, 1u, 2u], api.DeleteAttempts);
|
|
Assert.Equal([3u, 1u, 2u], api.Deleted);
|
|
}
|
|
|
|
[Fact]
|
|
public void CommittedNamesAreNeverDeletedByConstructionTransaction()
|
|
{
|
|
var api = new FakeTextureNameApi();
|
|
var transaction = new GlTextureConstructionTransaction(api);
|
|
_ = transaction.Allocate();
|
|
_ = transaction.Allocate();
|
|
|
|
transaction.Commit();
|
|
transaction.Rollback();
|
|
|
|
Assert.Empty(api.DeleteAttempts);
|
|
Assert.Throws<InvalidOperationException>(() => transaction.Allocate());
|
|
}
|
|
|
|
[Fact]
|
|
public void TrackedTextureUploadFailureLeavesEveryActualBranchNameInTransaction()
|
|
{
|
|
var api = new FakeTextureNameApi();
|
|
var transaction = new GlTextureConstructionTransaction(api);
|
|
uint first = TrackedTextureConstruction.Create(transaction, _ => { });
|
|
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
TrackedTextureConstruction.Create(
|
|
transaction,
|
|
_ => throw new InvalidOperationException("upload failed")));
|
|
transaction.Rollback();
|
|
|
|
Assert.Equal(1u, first);
|
|
Assert.Equal([2u, 1u], api.DeleteAttempts);
|
|
}
|
|
|
|
[Fact]
|
|
public void TerrainAtlasRoutesEveryTerrainAlphaAndFallbackUploadThroughTracker()
|
|
{
|
|
string source = File.ReadAllText(Path.Combine(
|
|
FindRepoRoot(),
|
|
"src",
|
|
"AcDream.App",
|
|
"Rendering",
|
|
"TerrainAtlas.cs"));
|
|
|
|
Assert.Equal(5, CountOccurrences(source, "TrackedTextureConstruction.Create("));
|
|
Assert.Equal(
|
|
5,
|
|
System.Text.RegularExpressions.Regex.Matches(
|
|
source,
|
|
"TrackedTextureConstruction\\.Create\\(\\s*textures,\\s*gl,\\s*\\\"").Count);
|
|
Assert.DoesNotContain("textures.Allocate()", source, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProductionGlResourcePathsUseAlwaysOnCheckedCommitBoundaries()
|
|
{
|
|
string root = FindRepoRoot();
|
|
string textureNames = File.ReadAllText(Path.Combine(
|
|
root, "src", "AcDream.App", "Rendering", "GlTextureConstructionTransaction.cs"));
|
|
string shaderPrograms = File.ReadAllText(Path.Combine(
|
|
root, "src", "AcDream.App", "Rendering", "ShaderProgramConstruction.cs"));
|
|
string terrain = File.ReadAllText(Path.Combine(
|
|
root, "src", "AcDream.App", "Rendering", "TerrainAtlas.cs"));
|
|
string text = File.ReadAllText(Path.Combine(
|
|
root, "src", "AcDream.App", "Rendering", "TextRenderer.cs"));
|
|
string bindless = File.ReadAllText(Path.Combine(
|
|
root, "src", "AcDream.App", "Rendering", "Wb", "BindlessSupport.cs"));
|
|
|
|
Assert.Contains("GlResourceCommand.CreateTexture", textureNames, StringComparison.Ordinal);
|
|
Assert.Contains("GlResourceCommand.DeleteTexture", textureNames, StringComparison.Ordinal);
|
|
Assert.Contains("GlResourceCommand.CreateName", shaderPrograms, StringComparison.Ordinal);
|
|
Assert.Contains("GlResourceCommand.DeleteShader", shaderPrograms, StringComparison.Ordinal);
|
|
Assert.Contains("GlResourceCommand.DeleteProgram", shaderPrograms, StringComparison.Ordinal);
|
|
Assert.Contains("_anisotropyBindingMutation.Execute", terrain, StringComparison.Ordinal);
|
|
Assert.Contains("GlResourceCommand.DeleteTexture", terrain, StringComparison.Ordinal);
|
|
Assert.Contains("GlResourceCommand.CreateTexture", text, StringComparison.Ordinal);
|
|
Assert.Contains("GlResourceCommand.Execute", text, StringComparison.Ordinal);
|
|
Assert.Contains("make bindless handle", bindless, StringComparison.Ordinal);
|
|
Assert.Contains("GlResourceCommand.Execute", bindless, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void SecondBindlessAcquireFailureRollsBackFirstHandle()
|
|
{
|
|
var residency = new FakeResidency { FailingAcquireTexture = 20 };
|
|
var pair = new BindlessTexturePair(10, 20, residency.Acquire, residency.Release);
|
|
|
|
Assert.Throws<InvalidOperationException>(() => pair.Acquire());
|
|
|
|
Assert.False(pair.IsFullyResident);
|
|
Assert.Equal([10u, 20u], residency.AcquireAttempts);
|
|
Assert.Equal([1010ul], residency.ReleaseAttempts);
|
|
}
|
|
|
|
[Fact]
|
|
public void FailedPrefixRollbackRemainsOwnedAndRetryDoesNotReacquireIt()
|
|
{
|
|
var residency = new FakeResidency
|
|
{
|
|
FailingAcquireTexture = 20,
|
|
FailingReleaseHandle = 1010,
|
|
};
|
|
var pair = new BindlessTexturePair(10, 20, residency.Acquire, residency.Release);
|
|
|
|
Assert.Throws<AggregateException>(() => pair.Acquire());
|
|
residency.FailingAcquireTexture = null;
|
|
residency.FailingReleaseHandle = null;
|
|
|
|
Assert.Equal((1010ul, 1020ul), pair.Acquire());
|
|
|
|
Assert.True(pair.IsFullyResident);
|
|
Assert.Equal([10u, 20u, 20u], residency.AcquireAttempts);
|
|
Assert.Equal([1010ul], residency.ReleaseAttempts);
|
|
}
|
|
|
|
[Fact]
|
|
public void BindlessReleaseAttemptsBothAndRetriesOnlyPendingHandle()
|
|
{
|
|
var residency = new FakeResidency();
|
|
var pair = new BindlessTexturePair(10, 20, residency.Acquire, residency.Release);
|
|
_ = pair.Acquire();
|
|
residency.FailingReleaseHandle = 1010;
|
|
|
|
Assert.Throws<AggregateException>(pair.Release);
|
|
Assert.False(pair.IsFullyResident);
|
|
Assert.True(pair.HasAnyResident);
|
|
Assert.Equal([1010ul, 1020ul], residency.ReleaseAttempts);
|
|
|
|
residency.FailingReleaseHandle = null;
|
|
pair.Release();
|
|
pair.Release();
|
|
|
|
Assert.False(pair.HasAnyResident);
|
|
Assert.Equal([1010ul, 1020ul, 1010ul], residency.ReleaseAttempts);
|
|
}
|
|
|
|
[Fact]
|
|
public void MutationFailureStillRestoresThePreviouslyResidentPair()
|
|
{
|
|
var residency = new FakeResidency();
|
|
var pair = new BindlessTexturePair(10, 20, residency.Acquire, residency.Release);
|
|
var guard = new BindlessTextureMutationGuard(pair);
|
|
_ = pair.Acquire();
|
|
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
guard.Execute(() => throw new InvalidOperationException("mutation failed")));
|
|
|
|
Assert.True(pair.IsFullyResident);
|
|
Assert.False(guard.RestoreRequired);
|
|
Assert.Equal([10u, 20u, 10u, 20u], residency.AcquireAttempts);
|
|
}
|
|
|
|
[Fact]
|
|
public void FailedReacquireKeepsRestoreIntentUntilALaterMutationRetry()
|
|
{
|
|
var residency = new FakeResidency();
|
|
var pair = new BindlessTexturePair(10, 20, residency.Acquire, residency.Release);
|
|
var guard = new BindlessTextureMutationGuard(pair);
|
|
_ = pair.Acquire();
|
|
residency.FailingAcquireTexture = 20;
|
|
|
|
Assert.Throws<InvalidOperationException>(() => guard.Execute(() => { }));
|
|
Assert.True(guard.RestoreRequired);
|
|
Assert.False(pair.HasAnyResident);
|
|
|
|
residency.FailingAcquireTexture = null;
|
|
guard.Execute(() => { });
|
|
|
|
Assert.False(guard.RestoreRequired);
|
|
Assert.True(pair.IsFullyResident);
|
|
Assert.Equal([10u, 20u, 10u, 20u, 10u, 20u], residency.AcquireAttempts);
|
|
}
|
|
|
|
[Fact]
|
|
public void PartialReleaseFailurePreventsMutationAndRestoresPairBeforeThrowing()
|
|
{
|
|
var residency = new FakeResidency();
|
|
var pair = new BindlessTexturePair(10, 20, residency.Acquire, residency.Release);
|
|
var guard = new BindlessTextureMutationGuard(pair);
|
|
_ = pair.Acquire();
|
|
residency.FailingReleaseHandle = 1010;
|
|
int mutations = 0;
|
|
|
|
Assert.Throws<AggregateException>(() => guard.Execute(() => mutations++));
|
|
|
|
Assert.Equal(0, mutations);
|
|
Assert.True(pair.IsFullyResident);
|
|
Assert.False(guard.RestoreRequired);
|
|
}
|
|
|
|
[Fact]
|
|
public void FailedTextureMutationRestoresTheExactPriorBinding()
|
|
{
|
|
var bindings = new List<uint>();
|
|
var owner = new RestoredTextureBindingMutation();
|
|
|
|
InvalidOperationException failure = Assert.Throws<InvalidOperationException>(() =>
|
|
owner.Execute(
|
|
() => 77,
|
|
bindings.Add,
|
|
42,
|
|
() => throw new InvalidOperationException("mutation failed")));
|
|
|
|
Assert.Equal("mutation failed", failure.Message);
|
|
Assert.Equal([42u, 77u], bindings);
|
|
}
|
|
|
|
[Fact]
|
|
public void TextureMutationReportsBothMutationAndBindingRestoreFailure()
|
|
{
|
|
int bindCalls = 0;
|
|
var owner = new RestoredTextureBindingMutation();
|
|
|
|
AggregateException failure = Assert.Throws<AggregateException>(() =>
|
|
owner.Execute(
|
|
() => 77,
|
|
_ =>
|
|
{
|
|
bindCalls++;
|
|
if (bindCalls == 2)
|
|
throw new InvalidOperationException("restore failed");
|
|
},
|
|
42,
|
|
() => throw new InvalidOperationException("mutation failed")));
|
|
|
|
Assert.Equal(2, failure.InnerExceptions.Count);
|
|
Assert.Equal(2, bindCalls);
|
|
Assert.True(owner.HasPendingRestore);
|
|
}
|
|
|
|
[Fact]
|
|
public void FailedBindingRestoreRetriesTheOriginalBindingBeforeAnotherMutation()
|
|
{
|
|
var owner = new RestoredTextureBindingMutation();
|
|
var bindings = new List<uint>();
|
|
int restoreFailures = 1;
|
|
int mutations = 0;
|
|
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
owner.Execute(
|
|
() => 77,
|
|
binding =>
|
|
{
|
|
bindings.Add(binding);
|
|
if (binding == 77 && restoreFailures-- > 0)
|
|
throw new InvalidOperationException("restore failed");
|
|
},
|
|
42,
|
|
() => mutations++));
|
|
|
|
Assert.True(owner.HasPendingRestore);
|
|
owner.Execute(
|
|
() => 77,
|
|
bindings.Add,
|
|
42,
|
|
() => mutations++);
|
|
|
|
Assert.False(owner.HasPendingRestore);
|
|
Assert.Equal([42u, 77u, 77u, 42u, 77u], bindings);
|
|
Assert.Equal(2, mutations);
|
|
}
|
|
|
|
[Fact]
|
|
public void ConstructionCleanupLedgerRetainsNestedFailureUntilRetryCompletes()
|
|
{
|
|
var api = new FakeTextureNameApi { FailingDelete = 1 };
|
|
var transaction = new GlTextureConstructionTransaction(api);
|
|
_ = transaction.Allocate();
|
|
AggregateException cleanupFailure = Assert.Throws<AggregateException>(
|
|
transaction.Rollback);
|
|
var constructionFailure = new GlResourceConstructionException(
|
|
"synthetic construction failure",
|
|
transaction,
|
|
[new InvalidOperationException("build failed"), cleanupFailure]);
|
|
var ledger = new GlConstructionCleanupLedger();
|
|
|
|
Assert.True(ledger.RetainFrom(constructionFailure));
|
|
Assert.Throws<AggregateException>(ledger.Dispose);
|
|
Assert.False(ledger.IsComplete);
|
|
|
|
api.FailingDelete = null;
|
|
ledger.Dispose();
|
|
|
|
Assert.True(ledger.IsComplete);
|
|
Assert.True(constructionFailure.IsCleanupComplete);
|
|
}
|
|
|
|
private sealed class FakeTextureNameApi : IGlTextureNameApi
|
|
{
|
|
private uint _nextName = 1;
|
|
|
|
public uint? FailingDelete { get; set; }
|
|
public List<uint> DeleteAttempts { get; } = [];
|
|
public List<uint> Deleted { get; } = [];
|
|
|
|
public uint GenTexture() => _nextName++;
|
|
|
|
public void DeleteTexture(uint texture)
|
|
{
|
|
DeleteAttempts.Add(texture);
|
|
if (FailingDelete == texture)
|
|
throw new InvalidOperationException("delete failed");
|
|
Deleted.Add(texture);
|
|
}
|
|
}
|
|
|
|
private static int CountOccurrences(string source, string value)
|
|
{
|
|
int count = 0;
|
|
int cursor = 0;
|
|
while ((cursor = source.IndexOf(value, cursor, StringComparison.Ordinal)) >= 0)
|
|
{
|
|
count++;
|
|
cursor += value.Length;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
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.");
|
|
}
|
|
|
|
private sealed class FakeResidency
|
|
{
|
|
public uint? FailingAcquireTexture { get; set; }
|
|
public ulong? FailingReleaseHandle { get; set; }
|
|
public List<uint> AcquireAttempts { get; } = [];
|
|
public List<ulong> ReleaseAttempts { get; } = [];
|
|
|
|
public ulong Acquire(uint texture)
|
|
{
|
|
AcquireAttempts.Add(texture);
|
|
if (FailingAcquireTexture == texture)
|
|
throw new InvalidOperationException("acquire failed");
|
|
return 1000ul + texture;
|
|
}
|
|
|
|
public void Release(ulong handle)
|
|
{
|
|
ReleaseAttempts.Add(handle);
|
|
if (FailingReleaseHandle == handle)
|
|
throw new InvalidOperationException("release failed");
|
|
}
|
|
}
|
|
}
|