acdream/tests/AcDream.App.Tests/Rendering/Wb/MeshUploadCachesTests.cs
Erik 91e82c3c68 fix(streaming): prepare quiesced destination entities
Separate loaded spatial residency from the world presentation gate so destination live objects acquire mesh owners behind portal space while drawing and simulation stay quiesced. Prevent unowned CPU-cache hits from recreating stale GPU staging work.

Co-authored-by: Erik Nilsson <erikn@users.noreply.github.com>
2026-07-24 20:21:57 +02:00

390 lines
14 KiB
C#

using AcDream.App.Rendering.Wb;
using AcDream.Content;
using Chorizite.Core.Render.Enums;
namespace AcDream.App.Tests.Rendering.Wb;
public sealed class MeshUploadCachesTests
{
[Fact]
public void CpuCacheHit_AfterCompletedUpload_ReStagesExactlyOnceWithTexturePayload()
{
var staging = new MeshUploadStagingQueue();
var cache = new CpuMeshUploadCache(capacity: 2);
var ownership = Owned(0x01000010ul);
byte[] textureBytes = [1, 2, 3, 4];
var data = new ObjectMeshData { ObjectId = 0x01000010ul, UploadAttempts = 2 };
data.TextureBatches[(1, 1, TextureFormat.RGBA8)] =
[
new TextureBatchData { TextureData = textureBytes },
];
cache.Store(data);
Assert.True(staging.Stage(data));
Assert.True(staging.TryDequeue(out var initial));
Assert.Same(data, initial.Data);
staging.Complete(initial); // GPU upload completed, then was later evicted.
data.UploadAttempts = 3;
Assert.True(cache.TryGetAndStageOwned(
data.ObjectId, staging, ownership, out var cached, out _));
Assert.Same(data, cached);
Assert.Equal(0, data.UploadAttempts);
Assert.Equal(textureBytes, cached!.TextureBatches.Values.Single().Single().TextureData);
Assert.True(cache.TryGetAndStageOwned(
data.ObjectId, staging, ownership, out _, out _));
Assert.True(staging.TryDequeue(out var restaged));
Assert.Same(data, restaged.Data);
Assert.False(staging.TryDequeue(out _));
}
[Fact]
public void Retry_RetainsQueueOwnershipUntilCompletion()
{
var staging = new MeshUploadStagingQueue();
var data = new ObjectMeshData { ObjectId = 0x01000010ul };
Assert.True(staging.Stage(data));
Assert.True(staging.TryDequeue(out var attempt));
staging.Requeue(attempt);
Assert.False(staging.Stage(data));
Assert.True(staging.TryDequeue(out var retry));
Assert.Same(data, retry.Data);
staging.Complete(retry);
Assert.True(staging.Stage(data));
}
[Fact]
public void UploadCompletion_NeverManufacturesLogicalOwnership()
{
const ulong id = 0x01000010ul;
var ownership = new MeshOwnershipCounter();
Assert.Equal(1, ownership.Acquire(id));
Assert.True(ownership.MarkUploadComplete(id));
Assert.Equal(1, ownership.Count(id));
Assert.Equal(0, ownership.Release(id));
Assert.False(ownership.MarkUploadComplete(id)); // late upload after unload
Assert.Equal(0, ownership.Count(id));
}
[Fact]
public void StagingQueueRejectsProducerWorkAboveByteHighWater()
{
var staging = new MeshUploadStagingQueue(maximumCount: 8, maximumBytes: 64);
ObjectMeshData first = DataWithTexture(1, 40);
ObjectMeshData second = DataWithTexture(2, 40);
Assert.True(staging.Stage(first));
Assert.False(staging.Stage(second));
Assert.Equal(1, staging.Count);
Assert.Equal(40, staging.QueuedBytes);
Assert.True(staging.TryDequeue(out MeshUploadQueueItem firstItem));
staging.Complete(firstItem);
Assert.True(staging.Stage(second));
}
[Fact]
public void CpuCacheHitReportsHighWaterWithoutPretendingItWasStaged()
{
var staging = new MeshUploadStagingQueue(maximumCount: 8, maximumBytes: 64);
var cache = new CpuMeshUploadCache(capacity: 2);
var ownership = Owned(2);
ObjectMeshData queued = DataWithTexture(1, 40);
ObjectMeshData cached = DataWithTexture(2, 40);
cache.Store(cached);
Assert.True(staging.Stage(queued));
Assert.True(cache.TryGetAndStageOwned(
2, staging, ownership, out ObjectMeshData? found, out MeshStageResult result));
Assert.Same(cached, found);
Assert.Equal(MeshStageResult.HighWater, result);
Assert.Equal(1, staging.Count);
Assert.True(staging.TryDequeue(out MeshUploadQueueItem queuedItem));
staging.Complete(queuedItem);
Assert.True(cache.TryGetAndStageOwned(
2, staging, ownership, out _, out result));
Assert.Equal(MeshStageResult.Staged, result);
Assert.True(staging.TryDequeue(out MeshUploadQueueItem staged));
Assert.Same(cached, staged.Data);
}
[Fact]
public void StalePrefixDrainIsBoundedAndDoesNotConsumeOwnedHead()
{
const int total = 1000;
var staging = new MeshUploadStagingQueue(
maximumCount: total + 1,
maximumBytes: long.MaxValue);
var ownership = new MeshOwnershipCounter();
for (ulong id = 1; id <= total; id++)
Assert.True(staging.Stage(new ObjectMeshData { ObjectId = id }));
ownership.Acquire(total);
Assert.Equal(64, staging.DiscardUnownedPrefix(ownership, maximum: 64));
Assert.Equal(total - 64, staging.Count);
Assert.Equal(total - 65, staging.DiscardUnownedPrefix(ownership, maximum: total));
Assert.True(staging.TryPeek(out MeshUploadQueueItem owned));
Assert.Equal((ulong)total, owned.Data.ObjectId);
}
[Fact]
public void CpuCacheRearmsMeshAfterStaleStageWasDiscarded()
{
const ulong id = 0x01000010;
var staging = new MeshUploadStagingQueue();
var cache = new CpuMeshUploadCache(capacity: 2);
var ownership = new MeshOwnershipCounter();
var data = DataWithTexture(id, 16);
cache.Store(data);
Assert.True(staging.Stage(data));
Assert.Equal(1, staging.DiscardUnownedPrefix(ownership, maximum: 1));
Assert.Equal(0, staging.Count);
ownership.Acquire(id);
Assert.True(cache.TryGetAndStageOwned(
id, staging, ownership, out ObjectMeshData? rearmed, out _));
Assert.Same(data, rearmed);
Assert.Equal(0, staging.DiscardUnownedPrefix(ownership, maximum: 1));
Assert.True(staging.TryPeek(out MeshUploadQueueItem queued));
Assert.Same(data, queued.Data);
}
[Fact]
public void CpuCacheHitWithoutOwnerDoesNotRecreateStaleUploadWork()
{
const ulong id = 0x01000010;
var staging = new MeshUploadStagingQueue();
var cache = new CpuMeshUploadCache(capacity: 2);
var ownership = new MeshOwnershipCounter();
ObjectMeshData data = DataWithTexture(id, 16);
cache.Store(data);
Assert.True(cache.TryGetAndStageOwned(
id,
staging,
ownership,
out ObjectMeshData? cached,
out MeshStageResult result));
Assert.Same(data, cached);
Assert.Equal(MeshStageResult.NotOwned, result);
Assert.Equal(0, staging.Count);
Assert.Equal(0, staging.ClaimCount);
ownership.Acquire(id);
Assert.True(cache.TryGetAndStageOwned(
id,
staging,
ownership,
out _,
out result));
Assert.Equal(MeshStageResult.Staged, result);
Assert.Equal(1, staging.Count);
}
[Fact]
public void CpuCacheUsesByteBudgetInAdditionToEntryCount()
{
var staging = new MeshUploadStagingQueue();
var cache = new CpuMeshUploadCache(capacity: 10, byteCapacity: 64);
var ownership = Owned(1, 2);
cache.Store(DataWithTexture(1, 40));
cache.Store(DataWithTexture(2, 40));
Assert.False(cache.TryGetAndStageOwned(
1, staging, ownership, out _, out _));
Assert.True(cache.TryGetAndStageOwned(
2, staging, ownership, out _, out _));
Assert.Equal(1, cache.Count);
Assert.Equal(40, cache.ResidentBytes);
}
[Fact]
public void CpuCacheRejectsOversizedEntryBeforeEvictingOrPublishingIt()
{
var staging = new MeshUploadStagingQueue(maximumBytes: 256);
var cache = new CpuMeshUploadCache(capacity: 2, byteCapacity: 64);
var ownership = Owned(1, 2);
ObjectMeshData retained = DataWithTexture(1, 40);
ObjectMeshData oversized = DataWithTexture(2, 80);
Assert.True(cache.Store(retained));
Assert.False(cache.Store(oversized));
Assert.Equal(1, cache.Count);
Assert.Equal(40, cache.ResidentBytes);
Assert.False(cache.TryGetAndStageOwned(
2, staging, ownership, out _, out _));
Assert.True(cache.TryGetAndStageOwned(
1, staging, ownership, out ObjectMeshData? found, out _));
Assert.Same(retained, found);
}
[Fact]
public void OversizedReplacementCannotDisplaceBoundedCachedGeneration()
{
var staging = new MeshUploadStagingQueue(maximumBytes: 256);
var cache = new CpuMeshUploadCache(capacity: 2, byteCapacity: 64);
var ownership = Owned(1);
ObjectMeshData retained = DataWithTexture(1, 40);
Assert.True(cache.Store(retained));
Assert.False(cache.Store(DataWithTexture(1, 80)));
Assert.True(cache.TryGetAndStageOwned(
1, staging, ownership, out ObjectMeshData? found, out _));
Assert.Same(retained, found);
Assert.Equal(40, cache.ResidentBytes);
}
[Fact]
public void DequeuedUnownedGenerationHandsOffToRacingCacheHit()
{
const ulong id = 0x01000010;
var staging = new MeshUploadStagingQueue();
var cache = new CpuMeshUploadCache(capacity: 2);
var ownership = new MeshOwnershipCounter();
ObjectMeshData data = DataWithTexture(id, 16);
cache.Store(data);
Assert.True(staging.Stage(data));
Assert.True(staging.TryDequeue(out MeshUploadQueueItem dequeued));
// The new owner cache-hits while the dequeued generation still owns
// staging's id claim, so its direct Stage attempt cannot enqueue yet.
ownership.Acquire(id);
Assert.True(cache.TryGetAndStageOwned(
id, staging, ownership, out ObjectMeshData? cached, out _));
Assert.Same(data, cached);
Assert.Equal(0, staging.Count);
Assert.True(staging.CompleteOrRestageIfOwned(dequeued, ownership));
Assert.True(staging.TryDequeue(out MeshUploadQueueItem handedOff));
Assert.Same(data, handedOff.Data);
Assert.False(staging.TryDequeue(out _));
}
[Fact]
public void OwnerArrivingAfterUnownedCompletionStagesCachedPayloadExactlyOnce()
{
const ulong id = 0x01000010;
var staging = new MeshUploadStagingQueue();
var cache = new CpuMeshUploadCache(capacity: 2);
var ownership = new MeshOwnershipCounter();
ObjectMeshData data = DataWithTexture(id, 16);
cache.Store(data);
Assert.True(staging.Stage(data));
Assert.True(staging.TryDequeue(out MeshUploadQueueItem dequeued));
Assert.False(staging.CompleteOrRestageIfOwned(dequeued, ownership));
Assert.Equal(0, staging.Count);
ownership.Acquire(id);
Assert.True(cache.TryGetAndStageOwned(
id, staging, ownership, out ObjectMeshData? cached, out _));
Assert.Same(data, cached);
Assert.True(staging.TryDequeue(out MeshUploadQueueItem handedOff));
Assert.Same(data, handedOff.Data);
Assert.False(staging.TryDequeue(out _));
}
[Fact]
public void ReacquiredObjectGetsNewImmutableQueueGeneration()
{
var staging = new MeshUploadStagingQueue();
var data = new ObjectMeshData { ObjectId = 0x01000010 };
Assert.True(staging.Stage(data));
Assert.True(staging.TryDequeue(out MeshUploadQueueItem first));
staging.Complete(first);
Assert.True(staging.Stage(data));
Assert.True(staging.TryPeek(out MeshUploadQueueItem replacement));
Assert.NotEqual(first.Generation, replacement.Generation);
Assert.Throws<InvalidOperationException>(() => staging.Complete(first));
Assert.True(staging.TryPeek(out MeshUploadQueueItem stillQueued));
Assert.Equal(replacement.Generation, stillQueued.Generation);
}
[Fact]
public void OversizedHeadIsExplicitlyBoundedAndFollowingProducerBackpressures()
{
var staging = new MeshUploadStagingQueue(
maximumCount: 8,
maximumBytes: 64,
maximumSingleEntryBytes: 96);
ObjectMeshData oversized = DataWithTexture(1, 80);
Assert.True(staging.Stage(oversized));
Assert.False(staging.Stage(DataWithTexture(2, 1)));
Assert.Throws<NotSupportedException>(() => staging.Stage(DataWithTexture(3, 97)));
Assert.Equal(1, staging.Count);
Assert.Equal(80, staging.QueuedBytes);
}
[Fact]
public void DequeuedGenerationStillCountsAgainstProducerByteAndCountBounds()
{
var staging = new MeshUploadStagingQueue(maximumCount: 1, maximumBytes: 64);
ObjectMeshData first = DataWithTexture(1, 40);
Assert.True(staging.Stage(first));
Assert.True(staging.TryDequeue(out MeshUploadQueueItem inFlight));
Assert.Equal(0, staging.Count);
Assert.Equal(1, staging.ClaimCount);
Assert.Equal(0, staging.QueuedBytes);
Assert.Equal(40, staging.ClaimedBytes);
Assert.True(staging.IsAtHighWater);
Assert.False(staging.Stage(DataWithTexture(2, 1)));
staging.Complete(inFlight);
Assert.True(staging.Stage(DataWithTexture(2, 1)));
}
[Fact]
public void CpuCacheStats_TracksHitsMissesAndEvictions()
{
// 2026-07-24 measurement-tooling review: the checkpoint JSON exposes
// cache traffic (not just residency) so a revisit's hit/miss ratio
// is auditable from an artifact alone.
var staging = new MeshUploadStagingQueue();
var cache = new CpuMeshUploadCache(capacity: 1);
var ownership = Owned(1);
Assert.False(cache.TryGetAndStageOwned(
1, staging, ownership, out _, out _)); // miss
cache.Store(DataWithTexture(1, 4));
Assert.True(cache.TryGetAndStageOwned(
1, staging, ownership, out _, out _)); // hit
cache.Store(DataWithTexture(2, 4)); // capacity=1 evicts id 1
CacheStats stats = cache.Stats;
Assert.Equal(1, stats.Hits);
Assert.Equal(1, stats.Misses);
Assert.Equal(1, stats.Evictions);
}
private static MeshOwnershipCounter Owned(params ulong[] ids)
{
var ownership = new MeshOwnershipCounter();
foreach (ulong id in ids)
ownership.Acquire(id);
return ownership;
}
private static ObjectMeshData DataWithTexture(ulong id, int bytes)
{
var data = new ObjectMeshData { ObjectId = id };
data.TextureBatches[(1, 1, TextureFormat.RGBA8)] =
[
new TextureBatchData { TextureData = new byte[bytes] },
];
return data;
}
}