From bf53cb4fceb203625718fd233583afc13f7d8419 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 8 May 2026 14:24:32 +0200 Subject: [PATCH] =?UTF-8?q?phase(N.4):=20WbMeshAdapter.Tick=20=E2=80=94=20?= =?UTF-8?q?drain=20WB=20pipeline=20queues=20per=20frame?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/AcDream.App/Rendering/GameWindow.cs | 6 ++++ src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs | 32 +++++++++++++++++++ .../Rendering/Wb/WbMeshAdapterTests.cs | 16 ++++++++++ 3 files changed, 54 insertions(+) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index fee413b..61f4084 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -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. diff --git a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs index ec5f407..b8a3a23 100644 --- a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs +++ b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs @@ -94,6 +94,38 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter _meshManager.DecrementRefCount(id); } + /// + /// 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. + /// + /// + /// Order matters: ProcessGLQueue 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 + /// UploadMeshData on each item to materialize the actual GL VAO / + /// VBO / IBO resources. After Tick, GetRenderData for any id + /// previously passed to IncrementRefCount may return non-null. + /// + /// + /// + /// No-op when the adapter is uninitialized (e.g., flag is off and the + /// adapter was constructed via CreateUninitialized). + /// + /// + public void Tick() + { + if (_isUninitialized) return; + if (_disposed) return; + + _graphicsDevice!.ProcessGLQueue(); + while (_meshManager!.StagedMeshData.TryDequeue(out var meshData)) + { + _meshManager.UploadMeshData(meshData); + } + } + /// public void Dispose() { diff --git a/tests/AcDream.Core.Tests/Rendering/Wb/WbMeshAdapterTests.cs b/tests/AcDream.Core.Tests/Rendering/Wb/WbMeshAdapterTests.cs index 1aaa33d..5758026 100644 --- a/tests/AcDream.Core.Tests/Rendering/Wb/WbMeshAdapterTests.cs +++ b/tests/AcDream.Core.Tests/Rendering/Wb/WbMeshAdapterTests.cs @@ -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 + } }