feat(render): unify physical residency accounting

This commit is contained in:
Erik 2026-07-24 16:20:48 +02:00
parent 1866ea0c6d
commit 3e18fc2730
25 changed files with 642 additions and 48 deletions

View file

@ -3,6 +3,7 @@ using System.Runtime.CompilerServices;
using AcDream.App.Composition;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Wb;
using AcDream.App.Rendering.Residency;
using AcDream.App.World;
using AcDream.Content;
using AcDream.Core.Terrain;
@ -19,7 +20,13 @@ public sealed class WorldRenderCompositionTests
[Fact]
public void SuccessPublishesExactModernFoundationInFrozenOrder()
{
var fixture = new Fixture();
ResidencyBudgetOptions budgets =
ResidencyBudgetOptions.Default with
{
ObjectMeshGpuBytes = 321,
CompositePhysicalBytes = 654,
};
var fixture = new Fixture(budgets: budgets);
WorldRenderResult result = fixture.Compose();
@ -30,6 +37,10 @@ public sealed class WorldRenderCompositionTests
Assert.Same(fixture.Lifetime.Atlas, result.Foundation.TerrainAtlas);
Assert.Equal(QualitySettings.From(QualityPreset.High).AnisotropicLevel,
fixture.Factory.AnisotropicLevel);
Assert.Same(budgets, result.Foundation.Residency.Budgets);
Assert.Same(budgets, fixture.Factory.MeshBudgets);
Assert.Same(budgets, fixture.Factory.TextureBudgets);
Assert.Same(result.Foundation.Residency, fixture.Factory.RegisteredResidency);
Assert.Equal(1, fixture.Lifetime.AcquireCalls);
Assert.Empty(fixture.Factory.Releases);
}
@ -134,13 +145,16 @@ public sealed class WorldRenderCompositionTests
private sealed class Fixture
{
private readonly WorldRenderCompositionPoint? _failurePoint;
private readonly ResidencyBudgetOptions _budgets;
public Fixture(
bool hasFont = true,
WorldRenderCompositionPoint? failurePoint = null,
string? publicationFailure = null)
string? publicationFailure = null,
ResidencyBudgetOptions? budgets = null)
{
_failurePoint = failurePoint;
_budgets = budgets ?? ResidencyBudgetOptions.Default;
Factory = new Factory(hasFont);
Publication = new Publication(publicationFailure);
Lifetime = new RenderLifetime(Factory.Atlas);
@ -161,6 +175,7 @@ public sealed class WorldRenderCompositionTests
new WorldEnvironmentController(),
Lifetime,
ImmediateGpuResourceRetirementQueue.Instance,
_budgets,
0xA9B4FFFFu,
_ => { }),
Publication,
@ -199,6 +214,9 @@ public sealed class WorldRenderCompositionTests
public TerrainAtlas Atlas { get; } = Stub<TerrainAtlas>();
public int AnisotropicLevel { get; private set; }
public List<string> Releases { get; } = [];
public ResidencyBudgetOptions? MeshBudgets { get; private set; }
public ResidencyBudgetOptions? TextureBudgets { get; private set; }
public ResidencyManager? RegisteredResidency { get; private set; }
public void InitializeGlState(GL gl) { }
@ -266,15 +284,32 @@ public sealed class WorldRenderCompositionTests
GL gl,
IDatReaderWriter dats,
IPreparedAssetSource preparedAssets,
IGpuResourceRetirementQueue retirement) =>
Resource<WbMeshAdapter>("WB mesh adapter");
IGpuResourceRetirementQueue retirement,
ResidencyBudgetOptions budgets)
{
MeshBudgets = budgets;
return Resource<WbMeshAdapter>("WB mesh adapter");
}
public TextureCache CreateTextureCache(
GL gl,
IDatReaderWriter dats,
BindlessSupport bindless,
IGpuResourceRetirementQueue retirement) =>
Resource<TextureCache>("texture cache");
IGpuResourceRetirementQueue retirement,
ResidencyBudgetOptions budgets)
{
TextureBudgets = budgets;
return Resource<TextureCache>("texture cache");
}
public void RegisterResidencySources(
ResidencyManager manager,
WbMeshAdapter meshes,
TextureCache textures,
IPreparedAssetSource preparedAssets)
{
RegisteredResidency = manager;
}
public SamplerCache CreateSamplerCache(GL gl) =>
Resource<SamplerCache>("sampler cache");

View file

@ -278,9 +278,9 @@ public sealed class WorldLifecycleAutomationControllerTests
private static WorldLifecycleResourceSnapshot EmptyResources() => new(
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0L, 0, 0L, 0L, 0, 0, 0, 0, 0, 0, 0, 0L, 0L,
0, 0, 0L, 0, 0L, 0L, 0L, 0L, 0, 0, 0, 0, 0, 0, 0, 0L, 0L,
0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L,
0d, 0d, null);
default, 0d, 0d, null);
private static string NewDirectory()
{

View file

@ -6,6 +6,40 @@ namespace AcDream.App.Tests.Rendering;
public sealed class CompositeTextureArrayCachePolicyTests
{
[Fact]
public void ResidencySnapshotSeparatesResidentRequestedAndRetiringStorage()
{
var backend = new FakeBackend(maximumLayers: 1);
var retirements = new DeferredRetirementQueue();
using var cache = CreateCache(
backend,
retirements,
unownedBudgetBytes: 0,
physicalBudgetBytes: 0);
cache.BeginFrame();
Assert.True(cache.TryAddAndAcquire(
1,
Key(1),
Texture(4, 4),
out _));
var resident = cache.CaptureResidency();
Assert.Equal(64, resident.Charges.GpuResidentBytes);
Assert.Equal(0, resident.Charges.RetiringBytes);
Assert.Equal(64, resident.CapacityBytes);
Assert.Equal(64, resident.UsedBytes);
cache.ReleaseOwner(1);
cache.Tick();
retirements.DrainAll();
cache.Tick();
var retired = cache.CaptureResidency();
Assert.Equal(0, retired.Charges.GpuResidentBytes);
Assert.Equal(0, retired.Charges.RetiringBytes);
Assert.Equal(0, retired.CapacityBytes);
}
[Theory]
[InlineData(16, 16, 64)]
[InlineData(128, 128, 64)]

View file

@ -343,6 +343,60 @@ public sealed class ResidencyManagerTests
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,

View file

@ -52,6 +52,8 @@ public sealed class StandaloneBindlessTextureCacheTests
cache.Tick();
Assert.Equal(2, cache.EntryCount);
Assert.Equal(12, cache.AllocatedBytes);
Assert.Equal(4, cache.RetiringBytes);
Assert.Equal(2, cache.UnownedEntryCount);
Assert.Single(retirements.Actions);
Assert.Empty(backend.Events);
@ -60,6 +62,8 @@ public sealed class StandaloneBindlessTextureCacheTests
retirements.Drain();
Assert.Equal(["nonresident:1", "delete:1"], backend.Events);
Assert.Equal(8, cache.AllocatedBytes);
Assert.Equal(0, cache.RetiringBytes);
}
[Fact]
@ -196,6 +200,32 @@ public sealed class StandaloneBindlessTextureCacheTests
backend.Events);
}
[Fact]
public void DisposePreservesAcceptedFenceRetirementAccountingUntilCompletion()
{
var backend = new FakeBackend();
var retirements = new DeferredRetirementQueue();
var cache = CreateCache(
backend,
retirements,
unownedBudgetBytes: 1_000,
maximumUnownedCount: 1);
AddAndRelease(cache, ownerId: 10, Resource(1, bytes: 4));
AddAndRelease(cache, ownerId: 20, Resource(2, bytes: 4));
cache.Tick();
Assert.Equal(8, cache.AllocatedBytes);
Assert.Equal(4, cache.RetiringBytes);
cache.Dispose();
Assert.Equal(4, cache.AllocatedBytes);
Assert.Equal(4, cache.RetiringBytes);
retirements.Drain();
Assert.Equal(0, cache.AllocatedBytes);
Assert.Equal(0, cache.RetiringBytes);
}
[Fact]
public void FailedResidencyReleaseNeverDeletesThatBackingTexture()
{