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:
parent
c02405cbb7
commit
773e9703da
3 changed files with 123 additions and 0 deletions
39
src/AcDream.App/Rendering/Wb/CachedBatch.cs
Normal file
39
src/AcDream.App/Rendering/Wb/CachedBatch.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace AcDream.App.Rendering.Wb;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Per-(entity, partIdx, batchIdx) classification result, stored flat inside
|
||||||
|
/// <see cref="EntityCacheEntry.Batches"/>. For Setup multi-part MeshRefs each
|
||||||
|
/// subPart contributes its own <see cref="CachedBatch"/> entries, with
|
||||||
|
/// <see cref="RestPose"/> already containing the
|
||||||
|
/// <c>subPart.PartTransform * meshRef.PartTransform</c> product.
|
||||||
|
///
|
||||||
|
/// Accessibility: <c>internal</c> because <see cref="GroupKey"/> is
|
||||||
|
/// <c>internal</c> and shows up in this struct's constructor / <c>Deconstruct</c>
|
||||||
|
/// signature. The cache itself is dispatcher-internal coordination state;
|
||||||
|
/// <see cref="InternalsVisibleTo"/> on <c>AcDream.App</c> exposes the type to
|
||||||
|
/// <c>AcDream.Core.Tests</c>.
|
||||||
|
/// </summary>
|
||||||
|
internal readonly record struct CachedBatch(
|
||||||
|
GroupKey Key,
|
||||||
|
ulong BindlessTextureHandle,
|
||||||
|
Matrix4x4 RestPose);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One entity's cached classification. <see cref="Batches"/> is flat across
|
||||||
|
/// (partIdx, batchIdx) and ordered as <c>WbDrawDispatcher.ClassifyBatches</c>
|
||||||
|
/// produced them. <see cref="LandblockHint"/> lets
|
||||||
|
/// <see cref="EntityClassificationCache.InvalidateLandblock"/> sweep entries
|
||||||
|
/// efficiently when a landblock demotes or unloads.
|
||||||
|
///
|
||||||
|
/// Accessibility: <c>internal</c> for the same reason as <see cref="CachedBatch"/>
|
||||||
|
/// — its <see cref="Batches"/> property is <c>CachedBatch[]</c>, which
|
||||||
|
/// transitively involves <see cref="GroupKey"/>.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class EntityCacheEntry
|
||||||
|
{
|
||||||
|
public required uint EntityId { get; init; }
|
||||||
|
public required uint LandblockHint { get; init; }
|
||||||
|
public required CachedBatch[] Batches { get; init; }
|
||||||
|
}
|
||||||
51
src/AcDream.App/Rendering/Wb/EntityClassificationCache.cs
Normal file
51
src/AcDream.App/Rendering/Wb/EntityClassificationCache.cs
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue