diff --git a/src/AcDream.App/Rendering/Wb/EntityClassificationCache.cs b/src/AcDream.App/Rendering/Wb/EntityClassificationCache.cs index 7d5a65b..1b0bebf 100644 --- a/src/AcDream.App/Rendering/Wb/EntityClassificationCache.cs +++ b/src/AcDream.App/Rendering/Wb/EntityClassificationCache.cs @@ -71,4 +71,30 @@ internal sealed class EntityClassificationCache { _entries.Remove(entityId); } + + /// + /// Remove every cache entry whose + /// equals . Used by the streaming pipeline + /// when a landblock demotes from near to far or unloads. No-op if no + /// entries match. + /// + public void InvalidateLandblock(uint landblockId) + { + if (_entries.Count == 0) return; + + // Collect the ids to remove first to avoid mutating the dict during iteration. + // Buffered locally because the typical case removes ~all entries in the LB + // (which is still small relative to the total cache). + List? toRemove = null; + foreach (var (id, entry) in _entries) + { + if (entry.LandblockHint == landblockId) + { + toRemove ??= new List(); + toRemove.Add(id); + } + } + if (toRemove is null) return; + foreach (var id in toRemove) _entries.Remove(id); + } } diff --git a/tests/AcDream.Core.Tests/Rendering/Wb/EntityClassificationCacheTests.cs b/tests/AcDream.Core.Tests/Rendering/Wb/EntityClassificationCacheTests.cs index a7949c1..4766461 100644 --- a/tests/AcDream.Core.Tests/Rendering/Wb/EntityClassificationCacheTests.cs +++ b/tests/AcDream.Core.Tests/Rendering/Wb/EntityClassificationCacheTests.cs @@ -126,6 +126,51 @@ public class EntityClassificationCacheTests Assert.Equal(0, cache.Count); } + [Fact] + public void InvalidateLandblock_RemovesAllMatchingEntries() + { + var cache = new EntityClassificationCache(); + cache.Populate(1, 0xA9B40000u, new[] { MakeCachedBatch(1, 0, 6, 0xAA) }); + cache.Populate(2, 0xA9B40000u, new[] { MakeCachedBatch(2, 0, 6, 0xBB) }); + cache.Populate(3, 0xA9B40000u, new[] { MakeCachedBatch(3, 0, 6, 0xCC) }); + Assert.Equal(3, cache.Count); + + cache.InvalidateLandblock(0xA9B40000u); + + Assert.Equal(0, cache.Count); + Assert.False(cache.TryGet(1, out _)); + Assert.False(cache.TryGet(2, out _)); + Assert.False(cache.TryGet(3, out _)); + } + + [Fact] + public void InvalidateLandblock_LeavesNonMatchingEntries() + { + var cache = new EntityClassificationCache(); + cache.Populate(1, 0xA9B40000u, new[] { MakeCachedBatch(1, 0, 6, 0xAA) }); + cache.Populate(2, 0xA9B50000u, new[] { MakeCachedBatch(2, 0, 6, 0xBB) }); + cache.Populate(3, 0xA9B40000u, new[] { MakeCachedBatch(3, 0, 6, 0xCC) }); + + cache.InvalidateLandblock(0xA9B40000u); + + Assert.Equal(1, cache.Count); + Assert.False(cache.TryGet(1, out _)); + Assert.True(cache.TryGet(2, out var keep)); + Assert.NotNull(keep); + Assert.Equal(0xA9B50000u, keep!.LandblockHint); + Assert.False(cache.TryGet(3, out _)); + } + + [Fact] + public void InvalidateLandblock_OnMissingLb_NoThrow() + { + var cache = new EntityClassificationCache(); + cache.Populate(1, 0xA9B40000u, new[] { MakeCachedBatch(1, 0, 6, 0xAA) }); + var ex = Record.Exception(() => cache.InvalidateLandblock(0xDEADBEEFu)); + Assert.Null(ex); + Assert.Equal(1, cache.Count); + } + private static CachedBatch MakeCachedBatch( uint ibo, uint firstIndex, int indexCount, ulong texHandle) {