using AcDream.App.Rendering.Residency; namespace AcDream.App.Tests.Rendering.Residency; public sealed class ResidencyManagerTests { private sealed class MeshAsset; private sealed class TextureAsset; [Fact] public void ScratchBytesAreCommittedCpuResidence() { var charges = new ResidencyCharges( LogicalBytes: 1, CpuPreparedBytes: 2, DecodedBytes: 3, ScratchBytes: 4, StagingBytes: 5); Assert.Equal(15, charges.CommittedCpuBytes); charges.Validate(); } [Fact] public void HandlesAndOwnersReserveZeroAsInvalid() { var manager = new ResidencyManager(); OwnerToken owner = manager.CreateOwner( ResidencyOwnerKind.Entity, logicalId: 42, worldGeneration: 7); AssetHandle asset = manager.Request( new ResidencyAssetKey(ResidencyDomain.ObjectMeshes, 0x01020304), ResidencyPriority.Visible, worldGeneration: 7, frame: 10); Assert.True(owner.IsValid); Assert.True(asset.IsValid); Assert.NotEqual(0u, owner.Index); Assert.NotEqual(0u, asset.Index); Assert.NotEqual((ushort)0, owner.Generation); Assert.NotEqual((ushort)0, asset.Generation); } [Fact] public void DuplicateAcquireIsIdempotentAndSingleReleaseOwnsLease() { var manager = new ResidencyManager(); OwnerToken owner = manager.CreateOwner( ResidencyOwnerKind.Entity, logicalId: 42, worldGeneration: 7); AssetHandle asset = manager.Request( new ResidencyAssetKey(ResidencyDomain.ObjectMeshes, 1), ResidencyPriority.Near, worldGeneration: 7, frame: 1); AssetLease first = manager.Acquire( asset, owner, ResidencyPriority.Visible, frame: 2); AssetLease duplicate = manager.Acquire( asset, owner, ResidencyPriority.Visible, frame: 3); Assert.Equal(first, duplicate); Assert.True(manager.TryGetSnapshot(asset, out var snapshot)); Assert.Equal(1, snapshot.OwnerCount); Assert.Equal(3, snapshot.LastUsedFrame); Assert.True(manager.Release(first)); Assert.False(manager.Release(duplicate)); } [Fact] public void RetiringAssetCannotBeAcquiredAndRecyclesWithNewGeneration() { var manager = new ResidencyManager(); ResidencyAssetKey key = new(ResidencyDomain.ObjectMeshes, 0x02000001); AssetHandle first = manager.Request( key, ResidencyPriority.Far, worldGeneration: 1, frame: 0); manager.Transition( first, AssetResidencyState.Prepared, new ResidencyCharges(CpuPreparedBytes: 64), frame: 1); manager.Transition( first, AssetResidencyState.Retiring, new ResidencyCharges(), frame: 2); Assert.True(manager.CompleteRetirement(first)); AssetHandle replacement = manager.Request( key, ResidencyPriority.Visible, worldGeneration: 2, frame: 3); Assert.Equal(first.Index, replacement.Index); Assert.NotEqual(first.Generation, replacement.Generation); Assert.False(manager.Touch(first, ResidencyPriority.Visible, frame: 4)); Assert.False(manager.CompleteRetirement(first)); } [Theory] [InlineData((int)AssetResidencyState.Cancelled)] [InlineData((int)AssetResidencyState.Missing)] [InlineData((int)AssetResidencyState.Corrupt)] [InlineData((int)AssetResidencyState.Failed)] public void TerminalCompletionDetachesOwnersAndRestartsAsNewGeneration( int terminalValue) { var terminal = (AssetResidencyState)terminalValue; var manager = new ResidencyManager(); OwnerToken owner = manager.CreateOwner( ResidencyOwnerKind.Landblock, logicalId: 0x1234, worldGeneration: 5); ResidencyAssetKey key = new(ResidencyDomain.PreparedMeshCpu, 7); AssetHandle first = manager.Request( key, ResidencyPriority.DestinationCritical, worldGeneration: 5, frame: 0); manager.Acquire( first, owner, ResidencyPriority.DestinationCritical, frame: 0); manager.Transition( first, terminal, ResidencyCharges.Zero, frame: 1, failure: terminal is AssetResidencyState.Corrupt or AssetResidencyState.Failed ? "fixture" : null); Assert.True(manager.TryGetSnapshot(first, out var terminalSnapshot)); Assert.Equal(0, terminalSnapshot.OwnerCount); AssetHandle replacement = manager.Request( key, ResidencyPriority.Near, worldGeneration: 5, frame: 2); Assert.Equal(first.Index, replacement.Index); Assert.NotEqual(first.Generation, replacement.Generation); Assert.False(manager.Release(new AssetLease(first, owner))); } [Fact] public void RetiringRejectsLiveOwners() { var manager = new ResidencyManager(); OwnerToken owner = manager.CreateOwner( ResidencyOwnerKind.Entity, logicalId: 1, worldGeneration: 1); AssetHandle asset = manager.Request( new ResidencyAssetKey(ResidencyDomain.ObjectMeshes, 1), ResidencyPriority.Visible, worldGeneration: 1, frame: 0); manager.Acquire(asset, owner, ResidencyPriority.Visible, frame: 0); manager.Transition( asset, AssetResidencyState.Prepared, new ResidencyCharges(CpuPreparedBytes: 10), frame: 1); InvalidOperationException error = Assert.Throws( () => manager.Transition( asset, AssetResidencyState.Retiring, ResidencyCharges.Zero, frame: 2)); Assert.Contains("owned asset", error.Message); } [Fact] public void IllegalTransitionAndTerminalChargesFailLoudly() { var manager = new ResidencyManager(); AssetHandle asset = manager.Request( new ResidencyAssetKey(ResidencyDomain.ObjectMeshes, 1), ResidencyPriority.Far, worldGeneration: 1, frame: 0); Assert.Throws(() => manager.Transition( asset, AssetResidencyState.Resident, new ResidencyCharges(GpuResidentBytes: 1), frame: 1)); Assert.Throws(() => manager.Transition( asset, AssetResidencyState.Failed, new ResidencyCharges(CpuPreparedBytes: 1), frame: 1)); } [Fact] public void OwnerGenerationPreventsStaleReleaseOfReplacementOwner() { var manager = new ResidencyManager(); OwnerToken first = manager.CreateOwner( ResidencyOwnerKind.Entity, logicalId: 1, worldGeneration: 1); Assert.True(manager.RetireOwner(first)); OwnerToken replacement = manager.CreateOwner( ResidencyOwnerKind.Entity, logicalId: 2, worldGeneration: 1); Assert.Equal(first.Index, replacement.Index); Assert.NotEqual(first.Generation, replacement.Generation); Assert.False(manager.RetireOwner(first)); Assert.Equal(1, manager.ActiveOwnerCount); } [Fact] public void WorldScopedOwnerCannotCrossGeneration() { var manager = new ResidencyManager(); OwnerToken owner = manager.CreateOwner( ResidencyOwnerKind.Entity, logicalId: 1, worldGeneration: 2); AssetHandle asset = manager.Request( new ResidencyAssetKey(ResidencyDomain.ObjectMeshes, 1), ResidencyPriority.Near, worldGeneration: 3, frame: 0); Assert.Throws(() => manager.Acquire( asset, owner, ResidencyPriority.Near, frame: 0)); } [Fact] public void EvictionIsOwnerSafePriorityThenAgeThenCostThenIndex() { var manager = new ResidencyManager(); AssetHandle farOldExpensive = Resident( manager, 1, ResidencyPriority.Far, frame: 1, bytes: 10, rebuild: 9); AssetHandle speculativeNew = Resident( manager, 2, ResidencyPriority.Speculative, frame: 9, bytes: 20, rebuild: 9); AssetHandle farOldCheap = Resident( manager, 3, ResidencyPriority.Far, frame: 1, bytes: 30, rebuild: 1); OwnerToken owner = manager.CreateOwner( ResidencyOwnerKind.Entity, logicalId: 9, worldGeneration: 1); manager.Acquire( speculativeNew, owner, ResidencyPriority.Visible, frame: 10); IReadOnlyList requests = manager.SelectEvictions( ResidencyDomain.ObjectMeshes, bytesToRelease: 35, maximumCount: 3); Assert.Collection( requests, first => Assert.Equal(farOldCheap.Untyped, first.Asset), second => Assert.Equal(farOldExpensive.Untyped, second.Asset)); Assert.DoesNotContain( requests, request => request.Asset == speculativeNew.Untyped); } [Fact] public void JournalCoalescesAndStaleObservationCannotReviveReplacement() { var manager = new ResidencyManager(); var journal = new ResidencyObservationJournal(maximumEntries: 4); ResidencyAssetKey key = new(ResidencyDomain.PreparedMeshCpu, 4); AssetHandle first = manager.Request( key, ResidencyPriority.Far, worldGeneration: 1, frame: 0); Assert.True(journal.TryPublish(new ResidencyObservation( first.Untyped, ResidencyObservationKind.Transition, AssetResidencyState.Prepared, new ResidencyCharges(CpuPreparedBytes: 10), ResidencyPriority.Far, Frame: 1))); Assert.True(journal.TryPublish(new ResidencyObservation( first.Untyped, ResidencyObservationKind.Transition, AssetResidencyState.Cancelled, ResidencyCharges.Zero, ResidencyPriority.Far, Frame: 2))); Assert.Equal(1, journal.Count); Assert.Equal(1, manager.Drain(journal)); AssetHandle replacement = manager.Request( key, ResidencyPriority.Visible, worldGeneration: 2, frame: 3); Assert.True(journal.TryPublish(new ResidencyObservation( first.Untyped, ResidencyObservationKind.Transition, AssetResidencyState.Prepared, new ResidencyCharges(CpuPreparedBytes: 10), ResidencyPriority.Far, Frame: 4))); Assert.Equal(0, manager.Drain(journal)); Assert.True(manager.TryGetSnapshot(replacement, out var snapshot)); Assert.Equal(AssetResidencyState.Requested, snapshot.State); } [Fact] public void JournalHasExplicitBoundForDistinctObservations() { var manager = new ResidencyManager(); var journal = new ResidencyObservationJournal(maximumEntries: 1); AssetHandle first = manager.Request( new ResidencyAssetKey(ResidencyDomain.PreparedMeshCpu, 1), ResidencyPriority.Far, worldGeneration: 1, frame: 0); AssetHandle second = manager.Request( new ResidencyAssetKey(ResidencyDomain.StandaloneTextures, 2), ResidencyPriority.Far, worldGeneration: 1, frame: 0); Assert.True(journal.TryPublish(Touch(first, frame: 1))); Assert.False(journal.TryPublish(Touch(second, frame: 1))); Assert.Equal(1, journal.Count); } [Fact] public void DomainSourcesAggregateWithoutCountingMappedAddressSpaceAsRam() { var manager = new ResidencyManager(); manager.RegisterDomainSource(new DelegateResidencyDomainSource( ResidencyDomain.PreparedPackage, () => new ResidencyDomainSnapshot( ResidencyDomain.PreparedPackage, EntryCount: 1, OwnerCount: 1, Charges: new ResidencyCharges( LogicalBytes: 64, PinnedBytes: 32, MappedVirtualBytes: 30_000)))); manager.RegisterDomainSource(new DelegateResidencyDomainSource( ResidencyDomain.StandaloneTextures, () => new ResidencyDomainSnapshot( ResidencyDomain.StandaloneTextures, EntryCount: 2, OwnerCount: 1, Charges: new ResidencyCharges( GpuResidentBytes: 128), BudgetBytes: 256, CapacityBytes: 128, UsedBytes: 96, LargestFreeBytes: 16))); ResidencySnapshot snapshot = manager.CaptureSnapshot(); Assert.Equal(64, snapshot.TotalCharges.CommittedCpuBytes); Assert.Equal(30_000, snapshot.TotalCharges.MappedVirtualBytes); Assert.Equal(128, snapshot.TotalCharges.PhysicalGpuBytes); Assert.Equal( 16, snapshot.Get(ResidencyDomain.StandaloneTextures) .FragmentedFreeBytes); } [Fact] public void OnlyOneCanonicalSourceMayOwnADomain() { var manager = new ResidencyManager(); var first = new DelegateResidencyDomainSource( ResidencyDomain.ObjectMeshes, () => default); manager.RegisterDomainSource(first); Assert.Throws(() => manager.RegisterDomainSource( new DelegateResidencyDomainSource( ResidencyDomain.ObjectMeshes, () => default))); } private static AssetHandle Resident( ResidencyManager manager, ulong id, ResidencyPriority priority, long frame, long bytes, long rebuild) { AssetHandle asset = manager.Request( new ResidencyAssetKey(ResidencyDomain.ObjectMeshes, id), priority, worldGeneration: 1, frame, rebuild); manager.Transition( asset, AssetResidencyState.Prepared, new ResidencyCharges(CpuPreparedBytes: bytes), frame); manager.Transition( asset, AssetResidencyState.UploadPending, new ResidencyCharges( StagingBytes: bytes, GpuRequestedBytes: bytes), frame); manager.Transition( asset, AssetResidencyState.Resident, new ResidencyCharges(GpuResidentBytes: bytes), frame); return asset; } private static ResidencyObservation Touch( AssetHandle asset, long frame) => new( asset.Untyped, ResidencyObservationKind.Touch, AssetResidencyState.Absent, ResidencyCharges.Zero, ResidencyPriority.Visible, frame); }