diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index f3472e77..ca791b08 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -252,6 +252,13 @@ public sealed class GameWindow : IDisposable /// private readonly Dictionary _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 _animatedIdsScratch = new(); + /// /// Tier 1 cache (#53): per-entity classification results for static /// entities (those NOT in ). Conceptually @@ -9104,13 +9111,15 @@ public sealed class GameWindow : IDisposable var envCellViewProj = camera.View * camera.Projection; _envCellFrustum?.Update(envCellViewProj); - HashSet? animatedIds = null; - if (_animatedEntities.Count > 0) - { - animatedIds = new HashSet(_animatedEntities.Count); - foreach (var k in _animatedEntities.Keys) - animatedIds.Add(k); - } + // MP-Alloc: reuse _animatedIdsScratch instead of `new`ing a + // HashSet 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? animatedIds = _animatedIdsScratch; // Phase G.1: sky renderer — draws the far-plane-infinity // celestial meshes FIRST so the rest of the scene z-tests diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs index cfa83e9a..7e30609b 100644 --- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs +++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs @@ -50,6 +50,13 @@ public sealed class RetailPViewRenderer // visible. private readonly InteriorEntityPartition.Result _partitionResult = new(); + // MP-Alloc (2026-07-05): DrawInside's drawable-cell set, reused across + // frames instead of `new HashSet(pvFrame.OrderedVisibleCells)` every + // call. Every consumer (DrawEntityBucket, DrawExitPortalMasks, + // DrawCellObjectLists, RetailPViewFrameResult.DrawableCells) reads it + // synchronously within the same frame it was built. + private readonly HashSet _drawableCellsScratch = new(); + // T2 (BR-4): retail has NO distance constant on the flood-admission chain // (DrawBuilding → portal walk → ConstructView: viewconeCheck + side test + // GetClip + GetVisible only). The old 48 m seed cap is replaced by the @@ -118,7 +125,9 @@ public sealed class RetailPViewRenderer // so every visible cell's shell has a prepared batch and seals — killing the grey // (the old clipAssembly.CellIdToSlot.Keys filter silently dropped slot-less cells). // Per-slice trim still applies in DrawEnvCellShells (Task 4 makes it self-contained). - var drawableCells = new HashSet(pvFrame.OrderedVisibleCells); + _drawableCellsScratch.Clear(); + _drawableCellsScratch.UnionWith(pvFrame.OrderedVisibleCells); + var drawableCells = _drawableCellsScratch; UseIndoorMembershipOnlyRouting(); // #124: look-in cells need prepared shell batches + their statics routed