444 lines
15 KiB
C#
444 lines
15 KiB
C#
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 HandlesAndOwnersReserveZeroAsInvalid()
|
|
{
|
|
var manager = new ResidencyManager();
|
|
|
|
OwnerToken owner = manager.CreateOwner(
|
|
ResidencyOwnerKind.Entity,
|
|
logicalId: 42,
|
|
worldGeneration: 7);
|
|
AssetHandle<MeshAsset> asset = manager.Request<MeshAsset>(
|
|
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<MeshAsset> asset = manager.Request<MeshAsset>(
|
|
new ResidencyAssetKey(ResidencyDomain.ObjectMeshes, 1),
|
|
ResidencyPriority.Near,
|
|
worldGeneration: 7,
|
|
frame: 1);
|
|
|
|
AssetLease<MeshAsset> first = manager.Acquire(
|
|
asset,
|
|
owner,
|
|
ResidencyPriority.Visible,
|
|
frame: 2);
|
|
AssetLease<MeshAsset> 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<MeshAsset> first = manager.Request<MeshAsset>(
|
|
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<MeshAsset> replacement = manager.Request<MeshAsset>(
|
|
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<MeshAsset> first = manager.Request<MeshAsset>(
|
|
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<MeshAsset> replacement = manager.Request<MeshAsset>(
|
|
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<MeshAsset>(first, owner)));
|
|
}
|
|
|
|
[Fact]
|
|
public void RetiringRejectsLiveOwners()
|
|
{
|
|
var manager = new ResidencyManager();
|
|
OwnerToken owner = manager.CreateOwner(
|
|
ResidencyOwnerKind.Entity,
|
|
logicalId: 1,
|
|
worldGeneration: 1);
|
|
AssetHandle<MeshAsset> asset = manager.Request<MeshAsset>(
|
|
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<InvalidOperationException>(
|
|
() => manager.Transition(
|
|
asset,
|
|
AssetResidencyState.Retiring,
|
|
ResidencyCharges.Zero,
|
|
frame: 2));
|
|
|
|
Assert.Contains("owned asset", error.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void IllegalTransitionAndTerminalChargesFailLoudly()
|
|
{
|
|
var manager = new ResidencyManager();
|
|
AssetHandle<MeshAsset> asset = manager.Request<MeshAsset>(
|
|
new ResidencyAssetKey(ResidencyDomain.ObjectMeshes, 1),
|
|
ResidencyPriority.Far,
|
|
worldGeneration: 1,
|
|
frame: 0);
|
|
|
|
Assert.Throws<InvalidOperationException>(() => manager.Transition(
|
|
asset,
|
|
AssetResidencyState.Resident,
|
|
new ResidencyCharges(GpuResidentBytes: 1),
|
|
frame: 1));
|
|
Assert.Throws<InvalidOperationException>(() => 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<MeshAsset> asset = manager.Request<MeshAsset>(
|
|
new ResidencyAssetKey(ResidencyDomain.ObjectMeshes, 1),
|
|
ResidencyPriority.Near,
|
|
worldGeneration: 3,
|
|
frame: 0);
|
|
|
|
Assert.Throws<InvalidOperationException>(() => manager.Acquire(
|
|
asset,
|
|
owner,
|
|
ResidencyPriority.Near,
|
|
frame: 0));
|
|
}
|
|
|
|
[Fact]
|
|
public void EvictionIsOwnerSafePriorityThenAgeThenCostThenIndex()
|
|
{
|
|
var manager = new ResidencyManager();
|
|
AssetHandle<MeshAsset> farOldExpensive = Resident(
|
|
manager, 1, ResidencyPriority.Far, frame: 1, bytes: 10, rebuild: 9);
|
|
AssetHandle<MeshAsset> speculativeNew = Resident(
|
|
manager, 2, ResidencyPriority.Speculative, frame: 9, bytes: 20, rebuild: 9);
|
|
AssetHandle<MeshAsset> 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<ResidencyTrimRequest> 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<MeshAsset> first = manager.Request<MeshAsset>(
|
|
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<MeshAsset> replacement = manager.Request<MeshAsset>(
|
|
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<MeshAsset> first = manager.Request<MeshAsset>(
|
|
new ResidencyAssetKey(ResidencyDomain.PreparedMeshCpu, 1),
|
|
ResidencyPriority.Far,
|
|
worldGeneration: 1,
|
|
frame: 0);
|
|
AssetHandle<TextureAsset> second = manager.Request<TextureAsset>(
|
|
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<InvalidOperationException>(() =>
|
|
manager.RegisterDomainSource(
|
|
new DelegateResidencyDomainSource(
|
|
ResidencyDomain.ObjectMeshes,
|
|
() => default)));
|
|
}
|
|
|
|
private static AssetHandle<MeshAsset> Resident(
|
|
ResidencyManager manager,
|
|
ulong id,
|
|
ResidencyPriority priority,
|
|
long frame,
|
|
long bytes,
|
|
long rebuild)
|
|
{
|
|
AssetHandle<MeshAsset> asset = manager.Request<MeshAsset>(
|
|
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<T>(
|
|
AssetHandle<T> asset,
|
|
long frame) =>
|
|
new(
|
|
asset.Untyped,
|
|
ResidencyObservationKind.Touch,
|
|
AssetResidencyState.Absent,
|
|
ResidencyCharges.Zero,
|
|
ResidencyPriority.Visible,
|
|
frame);
|
|
}
|