fix(render #53): key cache by (entityId, landblockHint) to defeat ID collision
User confirmed via A/B test (ACDREAM_DISABLE_TIER1_CACHE=1) that the
visual bug — buildings rendering up in the air outside Holtburg — is in
the cache wiring, not elsewhere. The matrix math (restPose * entityWorld
== model) was provably correct, so the bug had to be cache key collision.
Stabs were namespaced in commit 71d0edc, but scenery (0x80LLBB00 +
localIndex) and interior (0x40LLBB00 + localCounter) still have the
same 256-overflow risk. Dense LBs outside Holtburg (forest, urban) push
localIndex past 255, wrapping into the lbY byte and creating cross-LB
collisions.
Fix: change the cache key from uint entityId to (uint, uint) tuple of
(EntityId, LandblockHint). The cache is now correct-by-construction
regardless of any hydration path's Id-generation strategy. Defensive
against future regressions in any ID namespace.
InvalidateEntity becomes a sweep (was O(1)), but it's called rarely
(only on live-entity despawn). InvalidateLandblock was already a sweep.
Updated 14 existing cache tests + 1 dispatcher integration test to thread
landblockHint through TryGet / DebugCrossCheck calls.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
71d0edc3d7
commit
95ebbf3004
4 changed files with 78 additions and 49 deletions
|
|
@ -12,7 +12,7 @@ public class EntityClassificationCacheTests
|
|||
public void TryGet_EmptyCache_ReturnsFalse()
|
||||
{
|
||||
var cache = new EntityClassificationCache();
|
||||
bool found = cache.TryGet(entityId: 42, out var entry);
|
||||
bool found = cache.TryGet(entityId: 42, landblockHint: 0u, out var entry);
|
||||
Assert.False(found);
|
||||
Assert.Null(entry);
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ public class EntityClassificationCacheTests
|
|||
|
||||
cache.Populate(entityId: 100, landblockHint: 0xA9B40000u, batches);
|
||||
|
||||
Assert.True(cache.TryGet(100, out var entry));
|
||||
Assert.True(cache.TryGet(100, 0xA9B40000u, out var entry));
|
||||
Assert.NotNull(entry);
|
||||
Assert.Equal(100u, entry!.EntityId);
|
||||
Assert.Equal(0xA9B40000u, entry.LandblockHint);
|
||||
|
|
@ -43,7 +43,7 @@ public class EntityClassificationCacheTests
|
|||
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.True(cache.TryGet(100, 0u, out var entry));
|
||||
Assert.NotNull(entry);
|
||||
Assert.Single(entry!.Batches);
|
||||
Assert.Equal(0xCCu, entry.Batches[0].BindlessTextureHandle);
|
||||
|
|
@ -72,7 +72,7 @@ public class EntityClassificationCacheTests
|
|||
var cache = new EntityClassificationCache();
|
||||
cache.Populate(entityId: 7, landblockHint: 0u, System.Array.Empty<CachedBatch>());
|
||||
|
||||
Assert.True(cache.TryGet(7, out var entry));
|
||||
Assert.True(cache.TryGet(7, 0u, out var entry));
|
||||
Assert.NotNull(entry);
|
||||
Assert.Empty(entry!.Batches);
|
||||
}
|
||||
|
|
@ -96,7 +96,7 @@ public class EntityClassificationCacheTests
|
|||
}
|
||||
cache.Populate(99, 0u, batches);
|
||||
|
||||
Assert.True(cache.TryGet(99, out var entry));
|
||||
Assert.True(cache.TryGet(99, 0u, out var entry));
|
||||
Assert.NotNull(entry);
|
||||
Assert.Equal(6, entry!.Batches.Length);
|
||||
Assert.Equal(0x100u, entry.Batches[0].BindlessTextureHandle);
|
||||
|
|
@ -108,11 +108,11 @@ public class EntityClassificationCacheTests
|
|||
{
|
||||
var cache = new EntityClassificationCache();
|
||||
cache.Populate(100, 0u, new[] { MakeCachedBatch(1, 0, 6, 0xAA) });
|
||||
Assert.True(cache.TryGet(100, out _));
|
||||
Assert.True(cache.TryGet(100, 0u, out _));
|
||||
|
||||
cache.InvalidateEntity(100);
|
||||
|
||||
Assert.False(cache.TryGet(100, out var entry));
|
||||
Assert.False(cache.TryGet(100, 0u, out var entry));
|
||||
Assert.Null(entry);
|
||||
Assert.Equal(0, cache.Count);
|
||||
}
|
||||
|
|
@ -138,9 +138,9 @@ public class EntityClassificationCacheTests
|
|||
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 _));
|
||||
Assert.False(cache.TryGet(1, 0xA9B40000u, out _));
|
||||
Assert.False(cache.TryGet(2, 0xA9B40000u, out _));
|
||||
Assert.False(cache.TryGet(3, 0xA9B40000u, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -154,11 +154,11 @@ public class EntityClassificationCacheTests
|
|||
cache.InvalidateLandblock(0xA9B40000u);
|
||||
|
||||
Assert.Equal(1, cache.Count);
|
||||
Assert.False(cache.TryGet(1, out _));
|
||||
Assert.True(cache.TryGet(2, out var keep));
|
||||
Assert.False(cache.TryGet(1, 0xA9B40000u, out _));
|
||||
Assert.True(cache.TryGet(2, 0xA9B50000u, out var keep));
|
||||
Assert.NotNull(keep);
|
||||
Assert.Equal(0xA9B50000u, keep!.LandblockHint);
|
||||
Assert.False(cache.TryGet(3, out _));
|
||||
Assert.False(cache.TryGet(3, 0xA9B40000u, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -187,7 +187,7 @@ public class EntityClassificationCacheTests
|
|||
cache.InvalidateEntity(100);
|
||||
cache.Populate(100, 0xA9B40000u, batchesV2);
|
||||
|
||||
Assert.True(cache.TryGet(100, out var entry));
|
||||
Assert.True(cache.TryGet(100, 0xA9B40000u, out var entry));
|
||||
Assert.NotNull(entry);
|
||||
Assert.Equal(batchesV2, entry!.Batches);
|
||||
Assert.Equal(0xCCu, entry.Batches[0].BindlessTextureHandle);
|
||||
|
|
@ -216,7 +216,7 @@ public class EntityClassificationCacheTests
|
|||
|
||||
try
|
||||
{
|
||||
cache.DebugCrossCheck(100, liveBatches);
|
||||
cache.DebugCrossCheck(100, 0u, liveBatches);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -244,7 +244,7 @@ public class EntityClassificationCacheTests
|
|||
|
||||
try
|
||||
{
|
||||
cache.DebugCrossCheck(100, batches);
|
||||
cache.DebugCrossCheck(100, 0u, batches);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
|||
|
|
@ -428,7 +428,7 @@ public sealed class WbDrawDispatcherBucketingTests
|
|||
|
||||
// First-frame post-conditions: 1 cache entry, 2 batches in it.
|
||||
Assert.Equal(1, cache.Count);
|
||||
Assert.True(cache.TryGet(EntityId, out var entry));
|
||||
Assert.True(cache.TryGet(EntityId, LandblockId, out var entry));
|
||||
Assert.NotNull(entry);
|
||||
Assert.Equal(2, entry!.Batches.Length);
|
||||
Assert.Equal(0xAAul, entry.Batches[0].BindlessTextureHandle);
|
||||
|
|
@ -449,7 +449,7 @@ public sealed class WbDrawDispatcherBucketingTests
|
|||
list.Add(m);
|
||||
}
|
||||
|
||||
Assert.True(cache.TryGet(EntityId, out var entryHit));
|
||||
Assert.True(cache.TryGet(EntityId, LandblockId, out var entryHit));
|
||||
Assert.NotNull(entryHit);
|
||||
var entityWorld = Matrix4x4.CreateTranslation(new Vector3(10f, 20f, 30f));
|
||||
WbDrawDispatcher.ApplyCacheHit(entryHit!, entityWorld, AppendInstance);
|
||||
|
|
@ -510,11 +510,7 @@ public sealed class WbDrawDispatcherBucketingTests
|
|||
|
||||
// Cache should never be populated for animated entities.
|
||||
Assert.Equal(0, cache.Count);
|
||||
Assert.False(cache.TryGet(AnimatedId, out _));
|
||||
|
||||
// Suppress unused-variable warning — LandblockId is here for parity
|
||||
// with the static-entity test's structure.
|
||||
_ = LandblockId;
|
||||
Assert.False(cache.TryGet(AnimatedId, LandblockId, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -589,7 +585,7 @@ public sealed class WbDrawDispatcherBucketingTests
|
|||
|
||||
// Assertions: ONE cache entry with ALL 6 batches in MeshRef order.
|
||||
Assert.Equal(1, cache.Count);
|
||||
Assert.True(cache.TryGet(EntityId, out var entry));
|
||||
Assert.True(cache.TryGet(EntityId, LandblockId, out var entry));
|
||||
Assert.NotNull(entry);
|
||||
Assert.Equal(EntityId, entry!.EntityId);
|
||||
Assert.Equal(LandblockId, entry.LandblockHint);
|
||||
|
|
@ -667,7 +663,7 @@ public sealed class WbDrawDispatcherBucketingTests
|
|||
// Skip subsequent tuples of an entity that cache-hit (the fix).
|
||||
if (lastHitEntityId == EntityId) continue;
|
||||
|
||||
if (cache.TryGet(EntityId, out var entry))
|
||||
if (cache.TryGet(EntityId, 0xA9B40000u, out var entry))
|
||||
{
|
||||
Assert.NotNull(entry);
|
||||
WbDrawDispatcher.ApplyCacheHit(entry!, entityWorld, AppendInstance);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue