feat(render #53): EntityClassificationCache.InvalidateLandblock + tests
Sweep-by-landblock removal for the streaming demote/unload path. Tests #6, #7, #8 from spec section 7.1 lock in: (a) all matching entries removed, (b) non-matching entries preserved, (c) idempotent on missing LB. Phase 1 (cache foundation) complete. 11 cache tests passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
aea4460eae
commit
a171e7007b
2 changed files with 71 additions and 0 deletions
|
|
@ -71,4 +71,30 @@ internal sealed class EntityClassificationCache
|
|||
{
|
||||
_entries.Remove(entityId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove every cache entry whose <see cref="EntityCacheEntry.LandblockHint"/>
|
||||
/// equals <paramref name="landblockId"/>. Used by the streaming pipeline
|
||||
/// when a landblock demotes from near to far or unloads. No-op if no
|
||||
/// entries match.
|
||||
/// </summary>
|
||||
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<uint>? toRemove = null;
|
||||
foreach (var (id, entry) in _entries)
|
||||
{
|
||||
if (entry.LandblockHint == landblockId)
|
||||
{
|
||||
toRemove ??= new List<uint>();
|
||||
toRemove.Add(id);
|
||||
}
|
||||
}
|
||||
if (toRemove is null) return;
|
||||
foreach (var id in toRemove) _entries.Remove(id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue