feat(render #53): EntityClassificationCache skeleton + first test

Adds CachedBatch, EntityCacheEntry, and EntityClassificationCache with
just TryGet (returns false on empty). The skeleton compiles and the first
test (TryGet_EmptyCache_ReturnsFalse) passes. Subsequent tasks add
Populate, InvalidateEntity, InvalidateLandblock, and the dispatcher
integration. Per spec design Section 6.1.

Note: CachedBatch / EntityCacheEntry / EntityClassificationCache are
internal (not public as the plan snippet showed). Their members
transitively reference the internal GroupKey type, so promoting them to
public produces CS0051 inconsistent-accessibility errors. The cache is
dispatcher-internal coordination state anyway, and the AcDream.App
csproj already exposes internals to AcDream.Core.Tests via
InternalsVisibleTo, so the test sees everything it needs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-10 17:23:37 +02:00
parent c02405cbb7
commit 773e9703da
3 changed files with 123 additions and 0 deletions

View file

@ -0,0 +1,51 @@
using System.Collections.Generic;
namespace AcDream.App.Rendering.Wb;
/// <summary>
/// Cache of per-entity classification results for static entities (those NOT
/// in <c>GameWindow._animatedEntities</c>). Holds one
/// <see cref="EntityCacheEntry"/> per cached entity. The cache is opaque
/// w.r.t. classification logic — it simply stores what callers populate.
///
/// <para>
/// <b>Invariants:</b>
/// <list type="bullet">
/// <item><see cref="Populate"/> overwrites any existing entry for the same id (defensive).</item>
/// <item><see cref="InvalidateEntity"/> is idempotent (no-throw on missing id).</item>
/// <item><see cref="InvalidateLandblock"/> walks all entries; entries whose
/// <see cref="EntityCacheEntry.LandblockHint"/> equals the argument are removed.</item>
/// <item>All operations are render-thread only. No internal locking.</item>
/// </list>
/// </para>
///
/// <para>
/// <b>Audit foundation:</b> see
/// <c>docs/research/2026-05-10-tier1-mutation-audit.md</c> for why static
/// entities can be cached and what invalidation is needed.
/// </para>
///
/// <para>
/// <b>Accessibility:</b> <c>internal</c>. <see cref="EntityCacheEntry"/> and
/// <see cref="CachedBatch"/> both transitively reference the <c>internal</c>
/// <see cref="GroupKey"/>; surfacing the cache as <c>public</c> would create
/// inconsistent-accessibility errors. Cross-assembly access for the test
/// project comes via <c>InternalsVisibleTo("AcDream.Core.Tests")</c> on
/// <c>AcDream.App.csproj</c>.
/// </para>
/// </summary>
internal sealed class EntityClassificationCache
{
private readonly Dictionary<uint, EntityCacheEntry> _entries = new();
/// <summary>Number of cached entities — for diagnostics.</summary>
public int Count => _entries.Count;
/// <summary>
/// Look up an entity's cached classification. Returns <c>true</c> with
/// the entry on hit; <c>false</c> with <paramref name="entry"/> set to
/// <c>null</c> on miss.
/// </summary>
public bool TryGet(uint entityId, out EntityCacheEntry? entry)
=> _entries.TryGetValue(entityId, out entry);
}