fix(rendering): bound portal resource lifetime
Separate logical ownership, render publication, and GPU retirement across live entities, landblocks, particles, textures, mesh arenas, portal/UI teardown, and per-frame scratch storage. Add bounded DAT/texture caches, upload budgets, three-frame fence retirement, exact-incarnation appearance reconciliation, frame pacing, and extensive lifetime conformance coverage.\n\nThe seven-destination connected route now cuts peak working/private memory roughly in half, returns Caul to 125-153 FPS locally, and produces no WER or AMD reset.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
3971997689
commit
749e8ceeb1
225 changed files with 29107 additions and 3914 deletions
|
|
@ -0,0 +1,609 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.Core.Textures;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
public sealed class CompositeTextureArrayCachePolicyTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(16, 16, 64)]
|
||||
[InlineData(128, 128, 64)]
|
||||
[InlineData(256, 256, 16)]
|
||||
[InlineData(512, 512, 4)]
|
||||
[InlineData(1024, 1024, 1)]
|
||||
public void CapacityTargetsFourMiBWithoutOversizingLargeLayers(
|
||||
int width,
|
||||
int height,
|
||||
int expected)
|
||||
{
|
||||
Assert.Equal(
|
||||
expected,
|
||||
CompositeTextureArrayCache.CalculateLayerCapacity(width, height, driverMaximumLayers: 2048));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CapacityHonorsDriverLayerLimit()
|
||||
{
|
||||
Assert.Equal(
|
||||
8,
|
||||
CompositeTextureArrayCache.CalculateLayerCapacity(16, 16, driverMaximumLayers: 8));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompatibleTexturesShareArrayAndOwnersShareExactSlice()
|
||||
{
|
||||
var backend = new FakeBackend(maximumLayers: 2);
|
||||
var retirements = new DeferredRetirementQueue();
|
||||
using var cache = CreateCache(backend, retirements);
|
||||
cache.BeginFrame();
|
||||
|
||||
Assert.True(cache.TryAddAndAcquire(1, Key(1), Texture(4, 4), out BindlessTextureLocation first));
|
||||
Assert.True(cache.TryAcquire(2, Key(1), out BindlessTextureLocation shared));
|
||||
Assert.True(cache.TryAddAndAcquire(1, Key(2), Texture(4, 4), out BindlessTextureLocation second));
|
||||
|
||||
Assert.Equal(first, shared);
|
||||
Assert.Equal(first.Handle, second.Handle);
|
||||
Assert.NotEqual(first.Layer, second.Layer);
|
||||
Assert.Single(backend.Created);
|
||||
Assert.Equal(2, backend.Uploads.Count);
|
||||
Assert.Equal(2, cache.ActiveResourceCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FinalReleaseCachesAndReacquireAvoidsUpload()
|
||||
{
|
||||
var backend = new FakeBackend(maximumLayers: 2);
|
||||
using var cache = CreateCache(backend, new DeferredRetirementQueue());
|
||||
cache.BeginFrame();
|
||||
Assert.True(cache.TryAddAndAcquire(1, Key(1), Texture(4, 4), out BindlessTextureLocation original));
|
||||
Assert.True(cache.TryAcquire(2, Key(1), out _));
|
||||
|
||||
cache.ReleaseOwner(1);
|
||||
Assert.Equal(0, cache.UnownedEntryCount);
|
||||
cache.ReleaseOwner(2);
|
||||
Assert.Equal(1, cache.UnownedEntryCount);
|
||||
Assert.Empty(backend.NonResident);
|
||||
Assert.Empty(backend.Deleted);
|
||||
|
||||
Assert.True(cache.TryAcquire(3, Key(1), out BindlessTextureLocation reacquired));
|
||||
Assert.Equal(original, reacquired);
|
||||
Assert.Single(backend.Uploads);
|
||||
Assert.Equal(0, cache.UnownedEntryCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EvictedLayerCannotBeReusedBeforeFenceAndOldCallbackCannotTouchReplacement()
|
||||
{
|
||||
var backend = new FakeBackend(maximumLayers: 1);
|
||||
var retirements = new DeferredRetirementQueue();
|
||||
using var cache = CreateCache(
|
||||
backend,
|
||||
retirements,
|
||||
unownedBudgetBytes: 0,
|
||||
physicalBudgetBytes: long.MaxValue);
|
||||
cache.BeginFrame();
|
||||
Assert.True(cache.TryAddAndAcquire(1, Key(1), Texture(4, 4), out BindlessTextureLocation old));
|
||||
cache.ReleaseOwner(1);
|
||||
cache.Tick();
|
||||
|
||||
Assert.Equal(1, retirements.Count);
|
||||
cache.BeginFrame();
|
||||
Assert.True(cache.TryAddAndAcquire(2, Key(1), Texture(4, 4), out BindlessTextureLocation replacement));
|
||||
Assert.NotEqual(old.Handle, replacement.Handle);
|
||||
Assert.Equal(2, backend.Created.Count);
|
||||
|
||||
retirements.DrainAll();
|
||||
Assert.True(cache.TryAcquire(3, Key(1), out BindlessTextureLocation stillReplacement));
|
||||
Assert.Equal(replacement, stillReplacement);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaintenanceBatchesLogicalEvictionButDeletesOnlyOneArrayPerTick()
|
||||
{
|
||||
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 _));
|
||||
cache.BeginFrame();
|
||||
Assert.True(cache.TryAddAndAcquire(1, Key(2), Texture(4, 4), out _));
|
||||
cache.BeginFrame();
|
||||
Assert.True(cache.TryAddAndAcquire(1, Key(3), Texture(4, 4), out _));
|
||||
cache.ReleaseOwner(1);
|
||||
|
||||
cache.Tick();
|
||||
Assert.Equal(0, cache.CachedEntryCount);
|
||||
Assert.Equal(3, retirements.Count);
|
||||
Assert.Empty(backend.Deleted);
|
||||
|
||||
retirements.DrainAll();
|
||||
cache.Tick();
|
||||
Assert.Equal(0, cache.CachedEntryCount);
|
||||
Assert.Single(backend.Deleted);
|
||||
Assert.Equal(
|
||||
["nonresident:1", "delete:1"],
|
||||
backend.Events.TakeLast(2));
|
||||
|
||||
cache.Tick();
|
||||
Assert.Equal(2, backend.Deleted.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UploadFailureRollsBackAndDeletesNewEmptyArray()
|
||||
{
|
||||
var backend = new FakeBackend(maximumLayers: 2) { FailNextUpload = true };
|
||||
using var cache = CreateCache(backend, new DeferredRetirementQueue());
|
||||
cache.BeginFrame();
|
||||
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => cache.TryAddAndAcquire(1, Key(1), Texture(4, 4), out _));
|
||||
Assert.Equal(0, cache.CachedEntryCount);
|
||||
Assert.Equal(0, cache.AtlasCount);
|
||||
Assert.Equal(0, cache.AllocatedBytes);
|
||||
Assert.Equal(["nonresident:1", "delete:1"], backend.Events);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UploadAndRollbackFailureRetainsEmptyAtlasForMaintenanceRetry()
|
||||
{
|
||||
var backend = new FakeBackend(maximumLayers: 2)
|
||||
{
|
||||
FailNextUpload = true,
|
||||
FailNextNonResident = true,
|
||||
};
|
||||
using var cache = CreateCache(backend, new DeferredRetirementQueue());
|
||||
cache.BeginFrame();
|
||||
|
||||
Assert.Throws<AggregateException>(
|
||||
() => cache.TryAddAndAcquire(1, Key(1), Texture(4, 4), out _));
|
||||
Assert.Equal(128, cache.AllocatedBytes);
|
||||
Assert.Equal(1, cache.AtlasCount);
|
||||
|
||||
cache.Tick();
|
||||
Assert.Equal(0, cache.AllocatedBytes);
|
||||
Assert.Equal(0, cache.AtlasCount);
|
||||
Assert.Equal(["nonresident:1", "delete:1"], backend.Events);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisposeMakesEveryHandleNonResidentBeforeDeletingAnyArray()
|
||||
{
|
||||
var backend = new FakeBackend(maximumLayers: 1);
|
||||
var cache = CreateCache(backend, new DeferredRetirementQueue());
|
||||
cache.BeginFrame();
|
||||
Assert.True(cache.TryAddAndAcquire(1, Key(1), Texture(4, 4), out _));
|
||||
cache.BeginFrame();
|
||||
Assert.True(cache.TryAddAndAcquire(1, Key(2), Texture(8, 8), out _));
|
||||
|
||||
cache.Dispose();
|
||||
|
||||
int firstDelete = backend.Events.FindIndex(e => e.StartsWith("delete:"));
|
||||
Assert.Equal(2, backend.Events.Take(firstDelete).Count(e => e.StartsWith("nonresident:")));
|
||||
Assert.Equal(2, backend.Deleted.Count);
|
||||
cache.Dispose();
|
||||
Assert.Equal(4, backend.Events.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StructuralPaletteEqualityRejectsSameHashWithDifferentRanges()
|
||||
{
|
||||
const ulong collision = 1234;
|
||||
var left = new PaletteCompositeIdentity(
|
||||
new PaletteOverride(1, [new PaletteOverride.SubPaletteRange(2, 3, 4)]),
|
||||
collision);
|
||||
var right = new PaletteCompositeIdentity(
|
||||
new PaletteOverride(1, [new PaletteOverride.SubPaletteRange(9, 3, 4)]),
|
||||
collision);
|
||||
|
||||
Assert.NotEqual(left, right);
|
||||
Assert.NotEqual(
|
||||
new CompositeTextureKey(CompositeTextureKind.PaletteComposite, 1, 0, left),
|
||||
new CompositeTextureKey(CompositeTextureKind.PaletteComposite, 1, 0, right));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UploadBudgetIncludesIncomingLayerAndResetsEachFrame()
|
||||
{
|
||||
var backend = new FakeBackend(maximumLayers: 8);
|
||||
using var cache = new CompositeTextureArrayCache(
|
||||
backend,
|
||||
new DeferredRetirementQueue(),
|
||||
unownedBudgetBytes: long.MaxValue,
|
||||
physicalBudgetBytes: long.MaxValue,
|
||||
maximumUploadsPerFrame: 8,
|
||||
maximumUploadBytesPerFrame: 100);
|
||||
|
||||
cache.BeginFrame();
|
||||
Assert.True(cache.TryAddAndAcquire(1, Key(1), Texture(4, 4), out _)); // 64 bytes
|
||||
Assert.False(cache.TryAddAndAcquire(1, Key(2), Texture(4, 4), out _));
|
||||
Assert.False(cache.CanStartUpload);
|
||||
Assert.Single(backend.Uploads);
|
||||
|
||||
cache.BeginFrame();
|
||||
Assert.True(cache.CanStartUpload);
|
||||
Assert.True(cache.TryAddAndAcquire(1, Key(2), Texture(4, 4), out _));
|
||||
Assert.Equal(2, backend.Uploads.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OversizedTextureIsAllowedAsOnlyUploadToGuaranteeProgress()
|
||||
{
|
||||
var backend = new FakeBackend(maximumLayers: 8);
|
||||
using var cache = new CompositeTextureArrayCache(
|
||||
backend,
|
||||
new DeferredRetirementQueue(),
|
||||
unownedBudgetBytes: long.MaxValue,
|
||||
physicalBudgetBytes: long.MaxValue,
|
||||
maximumUploadsPerFrame: 8,
|
||||
maximumUploadBytesPerFrame: 32);
|
||||
|
||||
cache.BeginFrame();
|
||||
Assert.True(cache.TryAddAndAcquire(1, Key(1), Texture(4, 4), out _));
|
||||
Assert.False(cache.TryAddAndAcquire(1, Key(2), Texture(1, 1), out _));
|
||||
Assert.Single(backend.Uploads);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DimensionPreflightRejectsBeforeAllocatingDecodedPixelsForBlockedUpload()
|
||||
{
|
||||
var backend = new FakeBackend(maximumLayers: 8);
|
||||
using var cache = new CompositeTextureArrayCache(
|
||||
backend,
|
||||
new DeferredRetirementQueue(),
|
||||
unownedBudgetBytes: long.MaxValue,
|
||||
physicalBudgetBytes: long.MaxValue,
|
||||
maximumUploadsPerFrame: 8,
|
||||
maximumUploadBytesPerFrame: 100);
|
||||
|
||||
cache.BeginFrame();
|
||||
Assert.True(cache.CanPrepareUpload(4, 4));
|
||||
Assert.True(cache.TryAddAndAcquire(1, Key(1), Texture(4, 4), out _)); // 64 bytes
|
||||
Assert.False(cache.CanPrepareUpload(4, 4));
|
||||
Assert.False(cache.CanStartUpload);
|
||||
Assert.Single(backend.Uploads);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DimensionPreflightHonorsOneNewArrayPerFrame()
|
||||
{
|
||||
var backend = new FakeBackend(maximumLayers: 8);
|
||||
using var cache = CreateCache(backend, new DeferredRetirementQueue());
|
||||
|
||||
cache.BeginFrame();
|
||||
Assert.True(cache.TryAddAndAcquire(1, Key(1), Texture(4, 4), out _));
|
||||
Assert.False(cache.CanPrepareUpload(8, 8));
|
||||
Assert.False(cache.CanStartUpload);
|
||||
Assert.Single(backend.Created);
|
||||
|
||||
cache.BeginFrame();
|
||||
cache.Tick();
|
||||
Assert.True(cache.CanPrepareUpload(8, 8));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PhysicalAdmissionReclaimsCompatibleLayerBeforeAllocatingAgain()
|
||||
{
|
||||
var backend = new FakeBackend(maximumLayers: 1);
|
||||
var retirements = new DeferredRetirementQueue();
|
||||
using var cache = CreateCache(
|
||||
backend,
|
||||
retirements,
|
||||
unownedBudgetBytes: long.MaxValue,
|
||||
physicalBudgetBytes: 64);
|
||||
|
||||
cache.BeginFrame();
|
||||
Assert.True(cache.TryAddAndAcquire(1, Key(1), Texture(4, 4), out BindlessTextureLocation first));
|
||||
cache.ReleaseOwner(1);
|
||||
|
||||
cache.BeginFrame();
|
||||
Assert.False(cache.TryAddAndAcquire(2, Key(2), Texture(4, 4), out _));
|
||||
Assert.Single(backend.Created);
|
||||
Assert.Equal(64, cache.AllocatedBytes);
|
||||
|
||||
cache.Tick();
|
||||
Assert.Single(retirements.Actions);
|
||||
retirements.DrainAll();
|
||||
|
||||
cache.BeginFrame();
|
||||
cache.Tick();
|
||||
Assert.True(cache.TryAddAndAcquire(2, Key(2), Texture(4, 4), out BindlessTextureLocation reused));
|
||||
Assert.Equal(first.Handle, reused.Handle);
|
||||
Assert.Single(backend.Created);
|
||||
Assert.Equal(64, cache.AllocatedBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AtlasRelease_NonResidentFailureRetainsAtlasAndAccountingForRetry()
|
||||
{
|
||||
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 _));
|
||||
cache.ReleaseOwner(1);
|
||||
cache.Tick();
|
||||
retirements.DrainAll();
|
||||
|
||||
backend.FailNextNonResident = true;
|
||||
Assert.Throws<InvalidOperationException>(cache.Tick);
|
||||
Assert.Equal(64, cache.AllocatedBytes);
|
||||
Assert.Equal(1, cache.AtlasCount);
|
||||
Assert.Empty(backend.Deleted);
|
||||
|
||||
cache.Tick();
|
||||
Assert.Equal(0, cache.AllocatedBytes);
|
||||
Assert.Equal(0, cache.AtlasCount);
|
||||
Assert.Single(backend.NonResident);
|
||||
Assert.Single(backend.Deleted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AtlasRelease_DeleteFailureRevokesReuseButPreservesPhysicalAccounting()
|
||||
{
|
||||
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 _));
|
||||
cache.ReleaseOwner(1);
|
||||
cache.Tick();
|
||||
retirements.DrainAll();
|
||||
|
||||
backend.FailNextDelete = true;
|
||||
Assert.Throws<InvalidOperationException>(cache.Tick);
|
||||
Assert.Equal(64, cache.AllocatedBytes);
|
||||
Assert.Single(backend.NonResident);
|
||||
Assert.Empty(backend.Deleted);
|
||||
|
||||
cache.BeginFrame();
|
||||
Assert.False(cache.TryAddAndAcquire(2, Key(2), Texture(4, 4), out _));
|
||||
Assert.Single(backend.Created);
|
||||
|
||||
cache.Tick();
|
||||
Assert.Equal(0, cache.AllocatedBytes);
|
||||
Assert.Single(backend.NonResident);
|
||||
Assert.Single(backend.Deleted);
|
||||
|
||||
cache.BeginFrame();
|
||||
Assert.True(cache.TryAddAndAcquire(2, Key(2), Texture(4, 4), out _));
|
||||
Assert.Equal(2, backend.Created.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AtlasRelease_PostCommitNonResidentFailureDoesNotReplayMutation()
|
||||
{
|
||||
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 _));
|
||||
cache.ReleaseOwner(1);
|
||||
cache.Tick();
|
||||
retirements.DrainAll();
|
||||
|
||||
backend.FailNextNonResidentAfterCommit = true;
|
||||
Assert.Throws<GpuResourceMutationException>(cache.Tick);
|
||||
Assert.Single(backend.NonResident);
|
||||
Assert.Empty(backend.Deleted);
|
||||
|
||||
cache.Tick();
|
||||
Assert.Single(backend.NonResident);
|
||||
Assert.Single(backend.Deleted);
|
||||
Assert.Equal(0, cache.AllocatedBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AtlasRelease_PostCommitDeleteFailureDoesNotDeleteOrAccountTwice()
|
||||
{
|
||||
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 _));
|
||||
cache.ReleaseOwner(1);
|
||||
cache.Tick();
|
||||
retirements.DrainAll();
|
||||
|
||||
backend.FailNextDeleteAfterCommit = true;
|
||||
Assert.Throws<GpuResourceMutationException>(cache.Tick);
|
||||
Assert.Single(backend.NonResident);
|
||||
Assert.Single(backend.Deleted);
|
||||
Assert.Equal(64, cache.AllocatedBytes);
|
||||
|
||||
cache.Tick();
|
||||
Assert.Single(backend.Deleted);
|
||||
Assert.Equal(0, cache.AllocatedBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LayerRetirement_PublicationFailureKeepsSlotUnavailableUntilRetry()
|
||||
{
|
||||
var backend = new FakeBackend(maximumLayers: 1);
|
||||
var retirements = new DeferredRetirementQueue { FailNextRetire = true };
|
||||
using var cache = CreateCache(
|
||||
backend,
|
||||
retirements,
|
||||
unownedBudgetBytes: 0,
|
||||
physicalBudgetBytes: 64);
|
||||
cache.BeginFrame();
|
||||
Assert.True(cache.TryAddAndAcquire(1, Key(1), Texture(4, 4), out _));
|
||||
cache.ReleaseOwner(1);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(cache.Tick);
|
||||
Assert.Equal(0, cache.CachedEntryCount);
|
||||
Assert.Empty(retirements.Actions);
|
||||
|
||||
cache.BeginFrame();
|
||||
Assert.Single(retirements.Actions);
|
||||
cache.BeginFrame();
|
||||
Assert.False(cache.TryAddAndAcquire(2, Key(2), Texture(4, 4), out _));
|
||||
|
||||
retirements.DrainAll();
|
||||
cache.BeginFrame();
|
||||
Assert.True(cache.TryAddAndAcquire(2, Key(2), Texture(4, 4), out _));
|
||||
Assert.Single(backend.Created);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_ResidencyFailureDeletesNothingAndRetryResumesExactAtlas()
|
||||
{
|
||||
var backend = new FakeBackend(maximumLayers: 1);
|
||||
var cache = CreateCache(backend, new DeferredRetirementQueue());
|
||||
cache.BeginFrame();
|
||||
Assert.True(cache.TryAddAndAcquire(1, Key(1), Texture(4, 4), out _));
|
||||
cache.BeginFrame();
|
||||
Assert.True(cache.TryAddAndAcquire(1, Key(2), Texture(8, 8), out _));
|
||||
|
||||
backend.FailNextNonResident = true;
|
||||
Assert.Throws<AggregateException>(cache.Dispose);
|
||||
Assert.Empty(backend.Deleted);
|
||||
Assert.Single(backend.NonResident);
|
||||
|
||||
cache.Dispose();
|
||||
Assert.Equal(2, backend.NonResident.Count);
|
||||
Assert.Equal(2, backend.Deleted.Count);
|
||||
Assert.Equal(0, cache.AllocatedBytes);
|
||||
}
|
||||
|
||||
private static CompositeTextureArrayCache CreateCache(
|
||||
FakeBackend backend,
|
||||
DeferredRetirementQueue retirements,
|
||||
long unownedBudgetBytes = long.MaxValue,
|
||||
long physicalBudgetBytes = long.MaxValue) =>
|
||||
new(
|
||||
backend,
|
||||
retirements,
|
||||
unownedBudgetBytes,
|
||||
physicalBudgetBytes,
|
||||
maximumUploadsPerFrame: 100,
|
||||
maximumUploadBytesPerFrame: long.MaxValue);
|
||||
|
||||
private static CompositeTextureKey Key(uint id) =>
|
||||
new(CompositeTextureKind.OriginalTextureOverride, id, id + 100, default);
|
||||
|
||||
private static DecodedTexture Texture(int width, int height) =>
|
||||
new(new byte[width * height * 4], width, height);
|
||||
|
||||
private sealed class DeferredRetirementQueue : IGpuResourceRetirementQueue
|
||||
{
|
||||
private readonly Queue<Action> _actions = new();
|
||||
public int Count => _actions.Count;
|
||||
public IReadOnlyCollection<Action> Actions => _actions;
|
||||
public bool FailNextRetire { get; set; }
|
||||
public void Retire(Action release)
|
||||
{
|
||||
if (FailNextRetire)
|
||||
{
|
||||
FailNextRetire = false;
|
||||
throw new InvalidOperationException("synthetic retirement publication failure");
|
||||
}
|
||||
_actions.Enqueue(release);
|
||||
}
|
||||
public void DrainAll()
|
||||
{
|
||||
while (_actions.TryDequeue(out Action? action))
|
||||
action();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeBackend(int maximumLayers) : ICompositeTextureArrayBackend
|
||||
{
|
||||
private uint _nextName = 1;
|
||||
public int MaximumArrayLayers { get; } = maximumLayers;
|
||||
public List<CompositeTextureArrayResource> Created { get; } = [];
|
||||
public List<(uint Name, int Layer)> Uploads { get; } = [];
|
||||
public List<uint> NonResident { get; } = [];
|
||||
public List<uint> Deleted { get; } = [];
|
||||
public List<string> Events { get; } = [];
|
||||
public bool FailNextUpload { get; set; }
|
||||
public bool FailNextNonResident { get; set; }
|
||||
public bool FailNextDelete { get; set; }
|
||||
public bool FailNextNonResidentAfterCommit { get; set; }
|
||||
public bool FailNextDeleteAfterCommit { get; set; }
|
||||
|
||||
public CompositeTextureArrayResource Create(int width, int height, int capacity)
|
||||
{
|
||||
uint name = _nextName++;
|
||||
var resource = new CompositeTextureArrayResource
|
||||
{
|
||||
Name = name,
|
||||
Handle = 1000UL + name,
|
||||
Width = width,
|
||||
Height = height,
|
||||
Capacity = capacity,
|
||||
Bytes = (long)width * height * 4 * capacity,
|
||||
};
|
||||
Created.Add(resource);
|
||||
return resource;
|
||||
}
|
||||
|
||||
public void Upload(CompositeTextureArrayResource resource, int layer, byte[] rgba)
|
||||
{
|
||||
if (FailNextUpload)
|
||||
{
|
||||
FailNextUpload = false;
|
||||
throw new InvalidOperationException("synthetic upload failure");
|
||||
}
|
||||
Uploads.Add((resource.Name, layer));
|
||||
}
|
||||
|
||||
public void MakeNonResident(CompositeTextureArrayResource resource)
|
||||
{
|
||||
if (FailNextNonResident)
|
||||
{
|
||||
FailNextNonResident = false;
|
||||
throw new InvalidOperationException("synthetic non-resident failure");
|
||||
}
|
||||
NonResident.Add(resource.Name);
|
||||
Events.Add($"nonresident:{resource.Name}");
|
||||
if (FailNextNonResidentAfterCommit)
|
||||
{
|
||||
FailNextNonResidentAfterCommit = false;
|
||||
throw new GpuResourceMutationException(
|
||||
"synthetic post-commit nonresident failure",
|
||||
mutationCommitted: true,
|
||||
new InvalidOperationException("observer"));
|
||||
}
|
||||
}
|
||||
|
||||
public void Delete(CompositeTextureArrayResource resource)
|
||||
{
|
||||
if (FailNextDelete)
|
||||
{
|
||||
FailNextDelete = false;
|
||||
throw new InvalidOperationException("synthetic delete failure");
|
||||
}
|
||||
Deleted.Add(resource.Name);
|
||||
Events.Add($"delete:{resource.Name}");
|
||||
if (FailNextDeleteAfterCommit)
|
||||
{
|
||||
FailNextDeleteAfterCommit = false;
|
||||
throw new GpuResourceMutationException(
|
||||
"synthetic post-commit delete failure",
|
||||
mutationCommitted: true,
|
||||
new InvalidOperationException("observer"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue