fix(#193): clear InstanceGroup.Opacities each frame — stop the ~1 GB/min LOH leak
Root cause (measured, not guessed): the OOM was a MANAGED Large-Object-Heap leak (~5 GB in 6 min of roaming; working set 1.6 GB -> 7.6 GB), dominated by ~3.8 GB of live System.Single[]. A heap-retention analysis of a captured gcdump traced 241 of 242 giant float arrays to a single List<float>: WbDrawDispatcher.InstanceGroup.Opacities. #188 (2026-07-09, fading doors) added Opacities as a 5th per-instance parallel list alongside Matrices/Slots/LightSets/IndoorFlags, but left it out of the per-frame clear loop (WbDrawDispatcher.cs:959). So Opacities.Add(1.0f) fired once per drawn instance per frame and the list never reset — growing forever; List<float> capacity doubling produced the observed ~128 MB / ~512 MB power-of-two arrays and OOM after ~50 min. Hit both UI builds because it's core render, and started only recently because Opacities did not exist before #188. Fix: extract the per-frame reset into InstanceGroup.ClearPerInstanceData() (promoted InstanceGroup private->internal) that clears ALL FIVE parallel lists in one place, so a future 6th list can't silently drift out of the frame lifecycle again. Line 959 now calls it. TDD: InstanceGroupClearTests asserts every parallel list resets (RED with the old 4-list clear, GREEN after adding Opacities.Clear()). Static-audit note: this is exactly why we measured before fixing — the audit ranked GlobalMeshBuffer (GPU) #1 and the entity-dict leak #3; both were wrong about the dominant cause. The dotnet-counters (managed vs native) + gcdump retention graph named the real one-line culprit. Secondary (NOT this commit): _groups dictionary is never pruned (bounded by distinct group keys, empty groups are cheap) — filed as a follow-up, not the multi-GB driver. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
02487da0ce
commit
119a2326be
2 changed files with 56 additions and 2 deletions
|
|
@ -956,7 +956,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
camPos = invView.Translation;
|
||||
|
||||
// ── Phase 1: clear groups, walk entities, build groups ──────────────
|
||||
foreach (var grp in _groups.Values) { grp.Matrices.Clear(); grp.Slots.Clear(); grp.LightSets.Clear(); grp.IndoorFlags.Clear(); }
|
||||
foreach (var grp in _groups.Values) grp.ClearPerInstanceData();
|
||||
|
||||
var metaTable = _meshAdapter.MetadataTable;
|
||||
uint anyVao = 0;
|
||||
|
|
@ -2556,7 +2556,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private sealed class InstanceGroup
|
||||
internal sealed class InstanceGroup
|
||||
{
|
||||
public uint Ibo;
|
||||
public uint FirstIndex;
|
||||
|
|
@ -2598,5 +2598,24 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
// layout time the dispatcher writes Opacities[i] into _alphaData at
|
||||
// the same cursor, so the binding=7 instanceAlpha[] tracks binding=0.
|
||||
public readonly List<float> Opacities = new();
|
||||
|
||||
/// <summary>
|
||||
/// Resets every per-instance parallel list for a new frame. These lists are
|
||||
/// appended in lockstep (one entry per drawn instance) during group build, so
|
||||
/// they MUST all be cleared together each frame. Keeping the reset in one
|
||||
/// method stops a newly-added parallel list from silently drifting out of the
|
||||
/// frame lifecycle — which is exactly the #193 OOM: #188 added
|
||||
/// <see cref="Opacities"/> alongside the others but left it out of the old
|
||||
/// inline clear loop, so it grew one float per instance per frame forever and
|
||||
/// leaked ~1 GB/min of LOH <c>float[]</c> as its backing array doubled.
|
||||
/// </summary>
|
||||
public void ClearPerInstanceData()
|
||||
{
|
||||
Matrices.Clear();
|
||||
Slots.Clear();
|
||||
LightSets.Clear();
|
||||
IndoorFlags.Clear();
|
||||
Opacities.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue