From 773e9703da30b7303da5b45048d8e227002d9153 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 10 May 2026 17:23:37 +0200 Subject: [PATCH] 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) --- src/AcDream.App/Rendering/Wb/CachedBatch.cs | 39 ++++++++++++++ .../Rendering/Wb/EntityClassificationCache.cs | 51 +++++++++++++++++++ .../Wb/EntityClassificationCacheTests.cs | 33 ++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 src/AcDream.App/Rendering/Wb/CachedBatch.cs create mode 100644 src/AcDream.App/Rendering/Wb/EntityClassificationCache.cs create mode 100644 tests/AcDream.Core.Tests/Rendering/Wb/EntityClassificationCacheTests.cs 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); + } +}