From 694815c49979c9d4d9bc0768b55475074db8de61 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 10 May 2026 17:34:48 +0200 Subject: [PATCH] feat(render #53): EntityClassificationCache.Populate + roundtrip tests Implements Populate (insert-or-overwrite) and adds 5 tests covering the populate->TryGet round-trip including the Setup pre-flatten shape. Per spec test plan section 7.1 tests #2, #3, #9, #10, #14. Tests use xUnit Assert.* (not FluentAssertions) to match the Task 2 implementer's choice and the existing 149 sibling assertions in the Wb test directory. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Rendering/Wb/EntityClassificationCache.cs | 14 +++ .../Wb/EntityClassificationCacheTests.cs | 86 +++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/src/AcDream.App/Rendering/Wb/EntityClassificationCache.cs b/src/AcDream.App/Rendering/Wb/EntityClassificationCache.cs index 0ae7cfc..5683346 100644 --- a/src/AcDream.App/Rendering/Wb/EntityClassificationCache.cs +++ b/src/AcDream.App/Rendering/Wb/EntityClassificationCache.cs @@ -48,4 +48,18 @@ internal sealed class EntityClassificationCache /// public bool TryGet(uint entityId, out EntityCacheEntry? entry) => _entries.TryGetValue(entityId, out entry); + + /// + /// Insert or overwrite a cache entry for . + /// Defensive: if an entry already exists, replaces it. + /// + public void Populate(uint entityId, uint landblockHint, CachedBatch[] batches) + { + _entries[entityId] = new EntityCacheEntry + { + EntityId = entityId, + LandblockHint = landblockHint, + Batches = batches, + }; + } } diff --git a/tests/AcDream.Core.Tests/Rendering/Wb/EntityClassificationCacheTests.cs b/tests/AcDream.Core.Tests/Rendering/Wb/EntityClassificationCacheTests.cs index b60b34b..6f37bff 100644 --- a/tests/AcDream.Core.Tests/Rendering/Wb/EntityClassificationCacheTests.cs +++ b/tests/AcDream.Core.Tests/Rendering/Wb/EntityClassificationCacheTests.cs @@ -17,6 +17,92 @@ public class EntityClassificationCacheTests Assert.Null(entry); } + [Fact] + public void Populate_ThenTryGet_ReturnsBatchesInOrder() + { + var cache = new EntityClassificationCache(); + var batches = new[] + { + MakeCachedBatch(ibo: 1, firstIndex: 0, indexCount: 6, texHandle: 0xAA), + MakeCachedBatch(ibo: 1, firstIndex: 6, indexCount: 6, texHandle: 0xBB), + }; + + cache.Populate(entityId: 100, landblockHint: 0xA9B40000u, batches); + + Assert.True(cache.TryGet(100, out var entry)); + Assert.NotNull(entry); + Assert.Equal(100u, entry!.EntityId); + Assert.Equal(0xA9B40000u, entry.LandblockHint); + Assert.Equal(batches, entry.Batches); + } + + [Fact] + public void Populate_OverridesExistingEntry() + { + var cache = new EntityClassificationCache(); + cache.Populate(100, 0u, new[] { MakeCachedBatch(1, 0, 6, 0xAA) }); + cache.Populate(100, 0u, new[] { MakeCachedBatch(2, 0, 12, 0xCC) }); + + Assert.True(cache.TryGet(100, out var entry)); + Assert.NotNull(entry); + Assert.Single(entry!.Batches); + Assert.Equal(0xCCu, entry.Batches[0].BindlessTextureHandle); + } + + [Fact] + public void Count_TracksLiveEntries() + { + var cache = new EntityClassificationCache(); + Assert.Equal(0, cache.Count); + + cache.Populate(1, 0u, new[] { MakeCachedBatch(1, 0, 6, 0xAA) }); + Assert.Equal(1, cache.Count); + + cache.Populate(2, 0u, new[] { MakeCachedBatch(2, 0, 6, 0xAA) }); + Assert.Equal(2, cache.Count); + + // Re-populate same id — should not double-count. + cache.Populate(1, 0u, new[] { MakeCachedBatch(3, 0, 6, 0xBB) }); + Assert.Equal(2, cache.Count); + } + + [Fact] + public void Populate_WithEmptyBatches_StoresEmptyEntry() + { + var cache = new EntityClassificationCache(); + cache.Populate(entityId: 7, landblockHint: 0u, System.Array.Empty()); + + Assert.True(cache.TryGet(7, out var entry)); + Assert.NotNull(entry); + Assert.Empty(entry!.Batches); + } + + [Fact] + public void Populate_SetupMultiPart_StoresFlatBatchPerSubPart() + { + // Synthetic Setup with 3 subParts × 2 batches each = 6 flat entries. + // This pins the spec §3 Q4 decision: pre-flatten Setup multi-parts at + // populate time so the per-frame hot path is branchless. + var cache = new EntityClassificationCache(); + var batches = new CachedBatch[6]; + for (int subPart = 0; subPart < 3; subPart++) + for (int b = 0; b < 2; b++) + { + batches[subPart * 2 + b] = MakeCachedBatch( + ibo: (uint)(subPart + 1), + firstIndex: (uint)(b * 6), + indexCount: 6, + texHandle: (ulong)(0x100 + subPart * 2 + b)); + } + cache.Populate(99, 0u, batches); + + Assert.True(cache.TryGet(99, out var entry)); + Assert.NotNull(entry); + Assert.Equal(6, entry!.Batches.Length); + Assert.Equal(0x100u, entry.Batches[0].BindlessTextureHandle); + Assert.Equal(0x105u, entry.Batches[5].BindlessTextureHandle); + } + private static CachedBatch MakeCachedBatch( uint ibo, uint firstIndex, int indexCount, ulong texHandle) {