diff --git a/src/AcDream.App/AcDream.App.csproj b/src/AcDream.App/AcDream.App.csproj
index c8a473b..a0c4b77 100644
--- a/src/AcDream.App/AcDream.App.csproj
+++ b/src/AcDream.App/AcDream.App.csproj
@@ -26,6 +26,12 @@
+
+
+
diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index 543a1f4..902ca5b 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -1427,17 +1427,18 @@ public sealed class GameWindow : IDisposable
// Phase N.4 — WB rendering pipeline foundation. Constructed only when
// ACDREAM_USE_WB_FOUNDATION=1 is set; otherwise the legacy renderer
- // path stays in charge. The full ObjectMeshManager bring-up is
- // deferred to Task 9 — for now this is a stub adapter that exposes
- // the public API so call sites can wire without behavioral effect.
+ // path stays in charge. The full ObjectMeshManager bring-up lives in
+ // WbMeshAdapter (Task 9): OpenGLGraphicsDevice + DefaultDatReaderWriter
+ // + ObjectMeshManager. WbMeshAdapter opens its own file handles for
+ // the dat files (independent of our DatCollection).
if (AcDream.App.Rendering.Wb.WbFoundationFlag.IsEnabled)
{
var wbLogger = Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance;
- _wbMeshAdapter = new AcDream.App.Rendering.Wb.WbMeshAdapter(_gl, _dats, wbLogger);
+ _wbMeshAdapter = new AcDream.App.Rendering.Wb.WbMeshAdapter(_gl, _datDir, wbLogger);
Console.WriteLine("[N.4] WbFoundation flag is ENABLED — routing static content through ObjectMeshManager.");
}
- _staticMesh = new InstancedMeshRenderer(_gl, _meshShader, _textureCache);
+ _staticMesh = new InstancedMeshRenderer(_gl, _meshShader, _textureCache, _wbMeshAdapter);
// Phase G.1 sky renderer — its own shader (sky.vert / sky.frag)
// with depth writes off + far plane 1e6 so celestial meshes
diff --git a/src/AcDream.App/Rendering/InstancedMeshRenderer.cs b/src/AcDream.App/Rendering/InstancedMeshRenderer.cs
index 92e8f5c..2ba5093 100644
--- a/src/AcDream.App/Rendering/InstancedMeshRenderer.cs
+++ b/src/AcDream.App/Rendering/InstancedMeshRenderer.cs
@@ -20,6 +20,7 @@
// needs to update the shader and uniform setup at the call sites.
using System.Numerics;
using System.Runtime.InteropServices;
+using AcDream.App.Rendering.Wb;
using AcDream.Core.Meshing;
using AcDream.Core.Terrain;
using AcDream.Core.World;
@@ -33,6 +34,20 @@ public sealed unsafe class InstancedMeshRenderer : IDisposable
private readonly Shader _shader;
private readonly TextureCache _textures;
+ ///
+ /// Optional WB adapter. When non-null and ,
+ /// hands the GfxObj ref to the WB pipeline instead of
+ /// uploading into our own VAO pool. The draw loop skips sentinel entries — Task 22's
+ /// WbDrawDispatcher will eventually draw them.
+ ///
+ private readonly WbMeshAdapter? _wbMeshAdapter;
+
+ // Sentinel: a GfxObj that has been handed to the WB pipeline gets this list
+ // stored in _gpuByGfxObj. The Draw loop recognises it by reference identity
+ // (object.ReferenceEquals) and skips it — no legacy VAO draw for WB-managed
+ // objects until Task 22 wires up WbDrawDispatcher.
+ private static readonly List WbManagedSentinel = new(0);
+
// One GPU bundle per unique GfxObj id. Each GfxObj can have multiple sub-meshes.
private readonly Dictionary> _gpuByGfxObj = new();
@@ -67,11 +82,13 @@ public sealed unsafe class InstancedMeshRenderer : IDisposable
private readonly record struct GroupKey(uint GfxObjId, ulong TextureSignature);
- public InstancedMeshRenderer(GL gl, Shader shader, TextureCache textures)
+ public InstancedMeshRenderer(GL gl, Shader shader, TextureCache textures,
+ WbMeshAdapter? wbMeshAdapter = null)
{
_gl = gl;
_shader = shader;
_textures = textures;
+ _wbMeshAdapter = wbMeshAdapter;
_instanceVbo = _gl.GenBuffer();
}
@@ -83,6 +100,17 @@ public sealed unsafe class InstancedMeshRenderer : IDisposable
if (_gpuByGfxObj.ContainsKey(gfxObjId))
return;
+ // Phase N.4 Task 9: when the WB foundation flag is on and we have an
+ // adapter, hand this GfxObj to the WB pipeline instead of uploading our
+ // own VAO. The sentinel entry marks "this GfxObj lives in WB now" so the
+ // draw loop knows to skip it. Task 22's WbDrawDispatcher will draw them.
+ if (WbFoundationFlag.IsEnabled && _wbMeshAdapter is not null)
+ {
+ _wbMeshAdapter.IncrementRefCount(gfxObjId);
+ _gpuByGfxObj[gfxObjId] = WbManagedSentinel;
+ return;
+ }
+
var list = new List(subMeshes.Count);
foreach (var sm in subMeshes)
list.Add(UploadSubMesh(sm));
@@ -217,6 +245,11 @@ public sealed unsafe class InstancedMeshRenderer : IDisposable
if (!_gpuByGfxObj.TryGetValue(key.GfxObjId, out var subMeshes))
continue;
+ // WB-managed GfxObjs have a sentinel entry; Task 22 (WbDrawDispatcher)
+ // will draw them. Skip here to avoid drawing with stale/null VAO data.
+ if (object.ReferenceEquals(subMeshes, WbManagedSentinel))
+ continue;
+
bool hasOpaqueSubMesh = false;
foreach (var sub in subMeshes)
{
@@ -292,6 +325,10 @@ public sealed unsafe class InstancedMeshRenderer : IDisposable
if (!_gpuByGfxObj.TryGetValue(key.GfxObjId, out var subMeshes))
continue;
+ // WB-managed GfxObjs — skip; Task 22 will draw them.
+ if (object.ReferenceEquals(subMeshes, WbManagedSentinel))
+ continue;
+
bool hasTranslucentSubMesh = false;
foreach (var sub in subMeshes)
{
@@ -419,7 +456,10 @@ public sealed unsafe class InstancedMeshRenderer : IDisposable
foreach (var meshRef in entity.MeshRefs)
{
- if (!_gpuByGfxObj.ContainsKey(meshRef.GfxObjId))
+ if (!_gpuByGfxObj.TryGetValue(meshRef.GfxObjId, out var cachedMeshes))
+ continue;
+ // WB-managed GfxObjs don't go through our instance pipeline.
+ if (object.ReferenceEquals(cachedMeshes, WbManagedSentinel))
continue;
var model = meshRef.PartTransform * entityRoot;
@@ -525,6 +565,11 @@ public sealed unsafe class InstancedMeshRenderer : IDisposable
{
foreach (var subs in _gpuByGfxObj.Values)
{
+ // WB-managed entries use the sentinel — no GL resources to free here;
+ // ObjectMeshManager owns those resources.
+ if (object.ReferenceEquals(subs, WbManagedSentinel))
+ continue;
+
foreach (var sub in subs)
{
_gl.DeleteBuffer(sub.Vbo);
diff --git a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
index 0f00620..ec5f407 100644
--- a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
+++ b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
@@ -1,72 +1,106 @@
using System;
-using DatReaderWriter;
+using Chorizite.OpenGLSDLBackend;
+using Chorizite.OpenGLSDLBackend.Lib;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
using Silk.NET.OpenGL;
+using WorldBuilder.Shared.Models;
+using WorldBuilder.Shared.Services;
namespace AcDream.App.Rendering.Wb;
///
/// Single seam between acdream and WB's render pipeline. Owns the
-/// ObjectMeshManager instance (when fully initialized) and exposes
-/// a stable acdream-shaped API so the rest of the renderer doesn't need
-/// to know about WB's types directly.
+/// ObjectMeshManager instance and exposes a stable acdream-shaped API
+/// so the rest of the renderer doesn't need to know about WB's types directly.
///
///
-/// Phase N.4 staging: currently a stub. Real ObjectMeshManager
-/// + OpenGLGraphicsDevice initialization is added in Task 9 once
-/// the dat-reader adapter (Task 6) lands. Until then, methods no-op so
-/// call sites can wire the adapter without behavioral effect when the
-/// flag is on.
+/// The adapter constructs its own DefaultDatReaderWriter internally; it
+/// does NOT share file handles with our DatCollection. This duplicates
+/// index-cache memory (~50–100 MB) but keeps the two subsystems fully decoupled.
+/// Acceptable for Phase N.4 foundation work (plan Adjustment 1).
///
///
public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
{
- // _meshManager and _graphicsDevice will be wired in Task 9 once
- // WbDatReaderAdapter (Task 6) lands. For now, both are null and all
- // methods no-op.
- // private ObjectMeshManager? _meshManager;
- // private OpenGLGraphicsDevice? _graphicsDevice;
+ private readonly OpenGLGraphicsDevice? _graphicsDevice;
+ private readonly DefaultDatReaderWriter? _wbDats;
+ private readonly ObjectMeshManager? _meshManager;
+
+ ///
+ /// True when this instance was created via ;
+ /// all public methods no-op when uninitialized.
+ ///
+ private readonly bool _isUninitialized;
+
private bool _disposed;
- public WbMeshAdapter(GL gl, DatCollection dats, ILogger logger)
+ ///
+ /// Constructs the full WB pipeline: OpenGLGraphicsDevice → DefaultDatReaderWriter
+ /// → ObjectMeshManager.
+ ///
+ /// Active Silk.NET GL context. Must be bound to the current
+ /// thread (construction runs GL queries; call from OnLoad).
+ /// Path to the dat directory (same as the one supplied
+ /// to our DatCollection). DefaultDatReaderWriter opens its own file handles.
+ /// Logger for the adapter; ObjectMeshManager uses
+ /// NullLogger internally.
+ public WbMeshAdapter(GL gl, string datDir, ILogger logger)
{
ArgumentNullException.ThrowIfNull(gl);
- ArgumentNullException.ThrowIfNull(dats);
+ ArgumentNullException.ThrowIfNull(datDir);
ArgumentNullException.ThrowIfNull(logger);
- // TODO(N.4 Task 9): construct OpenGLGraphicsDevice and ObjectMeshManager
- // once WbDatReaderAdapter (Task 6) is available to bridge our DatCollection
- // to WB's IDatReaderWriter.
+ _graphicsDevice = new OpenGLGraphicsDevice(gl, logger, new DebugRenderSettings());
+ _wbDats = new DefaultDatReaderWriter(datDir);
+ _meshManager = new ObjectMeshManager(
+ _graphicsDevice,
+ _wbDats,
+ NullLogger.Instance);
}
private WbMeshAdapter()
{
- // Uninitialized constructor — only for tests / for cases where the
- // flag is off and the caller wants a Dispose-safe no-op instance.
+ // Uninitialized constructor — only for tests / flag-off cases where
+ // the caller wants a Dispose-safe no-op instance.
+ _isUninitialized = true;
}
/// Test/init helper — produces a Dispose-safe instance with no
/// underlying mesh manager. Public methods are all no-ops.
public static WbMeshAdapter CreateUninitialized() => new();
- /// Returns null until Task 9 wires up the real mesh manager.
- public object? GetRenderData(ulong id) => null;
+ ///
+ /// Returns the WB render data for , or null if not
+ /// yet uploaded or if this adapter is uninitialized.
+ ///
+ public ObjectRenderData? GetRenderData(ulong id)
+ {
+ if (_isUninitialized || _meshManager is null) return null;
+ return _meshManager.GetRenderData(id);
+ }
+ ///
public void IncrementRefCount(ulong id)
{
- // No-op until Task 9.
+ if (_isUninitialized || _meshManager is null) return;
+ _meshManager.IncrementRefCount(id);
}
+ ///
public void DecrementRefCount(ulong id)
{
- // No-op until Task 9.
+ if (_isUninitialized || _meshManager is null) return;
+ _meshManager.DecrementRefCount(id);
}
+ ///
public void Dispose()
{
if (_disposed) return;
_disposed = true;
- // _meshManager?.Dispose();
- // _graphicsDevice?.Dispose();
+ _meshManager?.Dispose();
+ _wbDats?.Dispose();
+ _graphicsDevice?.Dispose();
}
}
diff --git a/tests/AcDream.Core.Tests/Rendering/Wb/WbMeshAdapterTests.cs b/tests/AcDream.Core.Tests/Rendering/Wb/WbMeshAdapterTests.cs
index d92bd46..1aaa33d 100644
--- a/tests/AcDream.Core.Tests/Rendering/Wb/WbMeshAdapterTests.cs
+++ b/tests/AcDream.Core.Tests/Rendering/Wb/WbMeshAdapterTests.cs
@@ -10,22 +10,11 @@ public sealed class WbMeshAdapterTests
[Fact]
public void Construct_WithNullGl_ThrowsArgumentNull()
{
+ // GL is the first guarded parameter; verifies the constructor validates inputs.
+ // We can't pass a real GL (no context in tests), so we verify only the
+ // null-GL guard. The real pipeline is tested via integration.
Assert.Throws(() =>
- new WbMeshAdapter(gl: null!, dats: null!, logger: NullLogger.Instance));
- }
-
- [Fact]
- public void Construct_WithNullDats_ThrowsArgumentNull()
- {
- // GL cannot be constructed without a real GL context, so we verify
- // the dats-null guard by passing a non-null GL sentinel — we reach
- // the dats guard on the way. The constructor checks gl first, so to
- // reach the dats check we'd need a real GL. Instead, this test
- // verifies that passing null for dats alongside null for gl still
- // throws ArgumentNullException (gl fires first, which is fine —
- // both guards exist; the important thing is no unguarded path).
- Assert.Throws(() =>
- new WbMeshAdapter(gl: null!, dats: null!, logger: NullLogger.Instance));
+ new WbMeshAdapter(gl: null!, datDir: "some/path", logger: NullLogger.Instance));
}
[Fact]
@@ -42,6 +31,19 @@ public sealed class WbMeshAdapterTests
var adapter = WbMeshAdapter.CreateUninitialized();
// Should not throw, even though there's no underlying mesh manager.
adapter.IncrementRefCount(0x01000001ul);
+ }
+
+ [Fact]
+ public void DecrementRefCount_OnUninitializedAdapter_NoOps()
+ {
+ var adapter = WbMeshAdapter.CreateUninitialized();
adapter.DecrementRefCount(0x01000001ul);
}
+
+ [Fact]
+ public void GetRenderData_OnUninitializedAdapter_ReturnsNull()
+ {
+ var adapter = WbMeshAdapter.CreateUninitialized();
+ Assert.Null(adapter.GetRenderData(0x01000001ul));
+ }
}