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(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(() => transaction.Allocate()); } [Fact] public void TrackedTextureUploadFailureLeavesEveryActualBranchNameInTransaction() { var api = new FakeTextureNameApi(); var transaction = new GlTextureConstructionTransaction(api); uint first = TrackedTextureConstruction.Create(transaction, _ => { }); Assert.Throws(() => 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(() => 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(() => 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(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(() => 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(() => 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(() => guard.Execute(() => mutations++)); Assert.Equal(0, mutations); Assert.True(pair.IsFullyResident); Assert.False(guard.RestoreRequired); } [Fact] public void FailedTextureMutationRestoresTheExactPriorBinding() { var bindings = new List(); var owner = new RestoredTextureBindingMutation(); InvalidOperationException failure = Assert.Throws(() => 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(() => 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(); int restoreFailures = 1; int mutations = 0; Assert.Throws(() => 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( 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(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 DeleteAttempts { get; } = []; public List 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 AcquireAttempts { get; } = []; public List 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"); } } }