phase(N.4): WbMeshAdapter.Tick — drain WB pipeline queues per frame

Without this, ObjectMeshManager.StagedMeshData and
OpenGLGraphicsDevice._glThreadQueue grow unbounded as background
workers prep mesh data + queue GL actions. Visual stress test of
flag-on at radius 7 showed real FPS drop and rising frame latency
from this leak.

Tick() drains both queues:
1. _graphicsDevice.ProcessGLQueue() applies pending GL state.
2. Loop _meshManager.StagedMeshData.TryDequeue -> UploadMeshData
   to materialize VAO/VBO/IBO for each prepared mesh.

Wired into GameWindow's render loop before draw work begins.
No-op when adapter is uninitialized or disposed.

Pattern matches WB's reference ObjectRenderManagerBase.ProcessUploads
without the prioritization heuristics (we're not yet drawing the
results — Task 22's WbDrawDispatcher will add prioritization when
visual budget matters).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-08 14:24:32 +02:00
parent f4f0101d2c
commit bf53cb4fce
3 changed files with 54 additions and 0 deletions

View file

@ -6076,6 +6076,12 @@ public sealed class GameWindow : IDisposable
_gl!.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
// Phase N.4: drain WB pipeline queues (staged mesh data +
// GL thread queue). Must happen before any draw work so that
// resources uploaded this frame are available immediately.
// No-op when ACDREAM_USE_WB_FOUNDATION is off (_wbMeshAdapter is null).
_wbMeshAdapter?.Tick();
// Phase D.2a — begin ImGui frame. Paired with the Render() call
// after the scene draws (below). ImGuiController.Update()
// consumes buffered Silk.NET input events and calls ImGui.NewFrame.

View file

@ -94,6 +94,38 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
_meshManager.DecrementRefCount(id);
}
/// <summary>
/// Per-frame drain of the WB pipeline's main-thread work queues. MUST be
/// called once per frame from the render thread. Without this, the staged
/// mesh data queue grows unbounded (memory leak) and queued GL actions
/// never execute.
///
/// <para>
/// Order matters: <c>ProcessGLQueue</c> runs first to apply any pending GL
/// state changes (e.g., texture uploads queued by background workers
/// during mesh prep). Then we drain staged mesh data, calling
/// <c>UploadMeshData</c> on each item to materialize the actual GL VAO /
/// VBO / IBO resources. After Tick, <c>GetRenderData</c> for any id
/// previously passed to <c>IncrementRefCount</c> may return non-null.
/// </para>
///
/// <para>
/// No-op when the adapter is uninitialized (e.g., flag is off and the
/// adapter was constructed via <c>CreateUninitialized</c>).
/// </para>
/// </summary>
public void Tick()
{
if (_isUninitialized) return;
if (_disposed) return;
_graphicsDevice!.ProcessGLQueue();
while (_meshManager!.StagedMeshData.TryDequeue(out var meshData))
{
_meshManager.UploadMeshData(meshData);
}
}
/// <inheritdoc/>
public void Dispose()
{

View file

@ -46,4 +46,20 @@ public sealed class WbMeshAdapterTests
var adapter = WbMeshAdapter.CreateUninitialized();
Assert.Null(adapter.GetRenderData(0x01000001ul));
}
[Fact]
public void Tick_OnUninitializedAdapter_DoesNotThrow()
{
var adapter = WbMeshAdapter.CreateUninitialized();
adapter.Tick(); // no-op, no throw
adapter.Tick(); // idempotent
}
[Fact]
public void Tick_AfterDispose_DoesNotThrow()
{
var adapter = WbMeshAdapter.CreateUninitialized();
adapter.Dispose();
adapter.Tick(); // no-op, no throw
}
}