perf(pipeline): MP-Alloc Task 4 - reuse per-frame animatedIds + drawableCells sets

Completes the safe-batch (implementer finished this but died before
committing; coordinator independently verified bit-identity + safety
post-interruption). Two per-frame HashSet<uint> allocations replaced by
reused cleared-in-place fields:
- GameWindow._animatedIdsScratch: WalkEntitiesInto treats null and empty
  identically (line 682 `is null || Count == 0`), so an always-non-null
  empty set == the old null-when-empty local. Verified.
- RetailPViewRenderer._drawableCellsScratch: flows into
  RetailPViewFrameResult.DrawableCells (live ref) but every consumer reads
  it same-frame; sigDrawableCells is a per-frame OnRender local that
  EmitRenderSignatureIfChanged only FORMATS to a string (no reference
  retention, no _lastSig set field). Built frame N -> read frame N ->
  cleared frame N+1. No cross-frame corruption.

Build green, full suite 4116 passed / 4 skipped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-06 00:09:03 +02:00
parent 76b8b4e9fc
commit 91afea24b4
2 changed files with 26 additions and 8 deletions

View file

@ -252,6 +252,13 @@ public sealed class GameWindow : IDisposable
/// </summary>
private readonly Dictionary<uint, AnimatedEntity> _animatedEntities = new();
// MP-Alloc (2026-07-05): reusable per-frame snapshot of _animatedEntities.Keys.
// WbDrawDispatcher.WalkEntitiesInto treats null and an empty set identically
// (`animatedEntityIds is null || animatedEntityIds.Count == 0`), so this can
// always be a live (possibly empty) HashSet instead of `new`ing one every
// frame or passing null.
private readonly HashSet<uint> _animatedIdsScratch = new();
/// <summary>
/// Tier 1 cache (#53): per-entity classification results for static
/// entities (those NOT in <see cref="_animatedEntities"/>). Conceptually
@ -9104,13 +9111,15 @@ public sealed class GameWindow : IDisposable
var envCellViewProj = camera.View * camera.Projection;
_envCellFrustum?.Update(envCellViewProj);
HashSet<uint>? animatedIds = null;
if (_animatedEntities.Count > 0)
{
animatedIds = new HashSet<uint>(_animatedEntities.Count);
foreach (var k in _animatedEntities.Keys)
animatedIds.Add(k);
}
// MP-Alloc: reuse _animatedIdsScratch instead of `new`ing a
// HashSet<uint> every frame. Downstream consumers (WbDrawDispatcher.
// WalkEntitiesInto) treat null and an empty set identically, so an
// always-non-null (possibly empty) set is behaviorally the same as
// the old null-when-nothing-animated local.
_animatedIdsScratch.Clear();
foreach (var k in _animatedEntities.Keys)
_animatedIdsScratch.Add(k);
HashSet<uint>? animatedIds = _animatedIdsScratch;
// Phase G.1: sky renderer — draws the far-plane-infinity
// celestial meshes FIRST so the rest of the scene z-tests