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 + } }