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>
35 lines
1.4 KiB
C#
35 lines
1.4 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Rendering.Wb;
|
|
using Xunit;
|
|
|
|
namespace AcDream.App.Tests.Rendering.Wb;
|
|
|
|
public class InstanceGroupClearTests
|
|
{
|
|
// #193 (regression from #188, 2026-07-09): WbDrawDispatcher's InstanceGroup holds
|
|
// five per-instance parallel lists — Matrices, Slots, LightSets, IndoorFlags,
|
|
// Opacities — appended in lockstep (one entry per drawn instance) every frame. The
|
|
// per-frame reset must clear ALL of them. #188 added Opacities but left it out of
|
|
// the inline clear loop, so it grew one float per instance per frame forever; as
|
|
// List<float>'s backing array doubled it produced ~128 MB / ~512 MB LOH float[]
|
|
// arrays and leaked ~1 GB/min -> OOM after ~50 min of play. This test pins that
|
|
// every parallel list is reset, so a newly-added list can't silently drift again.
|
|
[Fact]
|
|
public void ClearPerInstanceData_ClearsEveryParallelPerInstanceList()
|
|
{
|
|
var grp = new WbDrawDispatcher.InstanceGroup();
|
|
grp.Matrices.Add(Matrix4x4.Identity);
|
|
grp.Slots.Add(1u);
|
|
grp.LightSets.Add(-1);
|
|
grp.IndoorFlags.Add(0u);
|
|
grp.Opacities.Add(1.0f);
|
|
|
|
grp.ClearPerInstanceData();
|
|
|
|
Assert.Empty(grp.Matrices);
|
|
Assert.Empty(grp.Slots);
|
|
Assert.Empty(grp.LightSets);
|
|
Assert.Empty(grp.IndoorFlags);
|
|
Assert.Empty(grp.Opacities); // #193 — the list that leaked
|
|
}
|
|
}
|