acdream/tests/AcDream.App.Tests/Rendering/Residency/ResidencyManagerTests.cs
Erik 1866ea0c6d feat(render): establish typed residency policy ledger
Define Slice D's transition and concurrency contract, add generation-safe asset handles and owner leases, and parse the existing cache ceilings through typed runtime budgets. Stale generations cannot release or revive replacements, and worker observations are bounded and coalesced.

Co-authored-by: Codex <noreply@openai.com>
2026-07-24 16:05:46 +02:00

390 lines
13 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);
}
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);
}