diff --git a/src/AcDream.App/Rendering/Wb/CachedBatch.cs b/src/AcDream.App/Rendering/Wb/CachedBatch.cs
new file mode 100644
index 0000000..d1bccb7
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/CachedBatch.cs
@@ -0,0 +1,39 @@
+using System.Numerics;
+
+namespace AcDream.App.Rendering.Wb;
+
+///
+/// Per-(entity, partIdx, batchIdx) classification result, stored flat inside
+/// . For Setup multi-part MeshRefs each
+/// subPart contributes its own entries, with
+/// already containing the
+/// subPart.PartTransform * meshRef.PartTransform product.
+///
+/// Accessibility: internal because is
+/// internal and shows up in this struct's constructor / Deconstruct
+/// signature. The cache itself is dispatcher-internal coordination state;
+/// on AcDream.App exposes the type to
+/// AcDream.Core.Tests.
+///
+internal readonly record struct CachedBatch(
+ GroupKey Key,
+ ulong BindlessTextureHandle,
+ Matrix4x4 RestPose);
+
+///
+/// One entity's cached classification. is flat across
+/// (partIdx, batchIdx) and ordered as WbDrawDispatcher.ClassifyBatches
+/// produced them. lets
+/// sweep entries
+/// efficiently when a landblock demotes or unloads.
+///
+/// Accessibility: internal for the same reason as
+/// — its property is CachedBatch[], which
+/// transitively involves .
+///
+internal sealed class EntityCacheEntry
+{
+ public required uint EntityId { get; init; }
+ public required uint LandblockHint { get; init; }
+ public required CachedBatch[] Batches { get; init; }
+}
diff --git a/src/AcDream.App/Rendering/Wb/EntityClassificationCache.cs b/src/AcDream.App/Rendering/Wb/EntityClassificationCache.cs
new file mode 100644
index 0000000..0ae7cfc
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/EntityClassificationCache.cs
@@ -0,0 +1,51 @@
+using System.Collections.Generic;
+
+namespace AcDream.App.Rendering.Wb;
+
+///
+/// Cache of per-entity classification results for static entities (those NOT
+/// in GameWindow._animatedEntities). Holds one
+/// per cached entity. The cache is opaque
+/// w.r.t. classification logic — it simply stores what callers populate.
+///
+///
+/// Invariants:
+///
+/// - overwrites any existing entry for the same id (defensive).
+/// - is idempotent (no-throw on missing id).
+/// - walks all entries; entries whose
+/// equals the argument are removed.
+/// - All operations are render-thread only. No internal locking.
+///
+///
+///
+///
+/// Audit foundation: see
+/// docs/research/2026-05-10-tier1-mutation-audit.md for why static
+/// entities can be cached and what invalidation is needed.
+///
+///
+///
+/// Accessibility: internal. and
+/// both transitively reference the internal
+/// ; surfacing the cache as public would create
+/// inconsistent-accessibility errors. Cross-assembly access for the test
+/// project comes via InternalsVisibleTo("AcDream.Core.Tests") on
+/// AcDream.App.csproj.
+///
+///
+internal sealed class EntityClassificationCache
+{
+ private readonly Dictionary _entries = new();
+
+ /// Number of cached entities — for diagnostics.
+ public int Count => _entries.Count;
+
+ ///
+ /// Look up an entity's cached classification. Returns true with
+ /// the entry on hit; false with set to
+ /// null on miss.
+ ///
+ public bool TryGet(uint entityId, out EntityCacheEntry? entry)
+ => _entries.TryGetValue(entityId, out entry);
+}
diff --git a/tests/AcDream.Core.Tests/Rendering/Wb/EntityClassificationCacheTests.cs b/tests/AcDream.Core.Tests/Rendering/Wb/EntityClassificationCacheTests.cs
new file mode 100644
index 0000000..b60b34b
--- /dev/null
+++ b/tests/AcDream.Core.Tests/Rendering/Wb/EntityClassificationCacheTests.cs
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using System.Numerics;
+using AcDream.App.Rendering.Wb;
+using AcDream.Core.Meshing;
+using Xunit;
+
+namespace AcDream.Core.Tests.Rendering.Wb;
+
+public class EntityClassificationCacheTests
+{
+ [Fact]
+ public void TryGet_EmptyCache_ReturnsFalse()
+ {
+ var cache = new EntityClassificationCache();
+ bool found = cache.TryGet(entityId: 42, out var entry);
+ Assert.False(found);
+ Assert.Null(entry);
+ }
+
+ private static CachedBatch MakeCachedBatch(
+ uint ibo, uint firstIndex, int indexCount, ulong texHandle)
+ {
+ var key = new GroupKey(
+ Ibo: ibo,
+ FirstIndex: firstIndex,
+ BaseVertex: 0,
+ IndexCount: indexCount,
+ BindlessTextureHandle: texHandle,
+ TextureLayer: 0,
+ Translucency: TranslucencyKind.Opaque);
+ return new CachedBatch(key, texHandle, Matrix4x4.Identity);
+ }
+}