diff --git a/src/AcDream.App/Rendering/Wb/IWbMeshAdapter.cs b/src/AcDream.App/Rendering/Wb/IWbMeshAdapter.cs
new file mode 100644
index 0000000..3ea4853
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/IWbMeshAdapter.cs
@@ -0,0 +1,12 @@
+namespace AcDream.App.Rendering.Wb;
+
+///
+/// Mockable interface over so adapters that
+/// drive ref-count lifecycle (e.g. LandblockSpawnAdapter, EntitySpawnAdapter)
+/// can be unit-tested without a real WB pipeline behind them.
+///
+public interface IWbMeshAdapter
+{
+ void IncrementRefCount(ulong id);
+ void DecrementRefCount(ulong id);
+}
diff --git a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
new file mode 100644
index 0000000..0f00620
--- /dev/null
+++ b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
@@ -0,0 +1,72 @@
+using System;
+using DatReaderWriter;
+using Microsoft.Extensions.Logging;
+using Silk.NET.OpenGL;
+
+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.
+///
+///
+/// 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.
+///
+///
+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 bool _disposed;
+
+ public WbMeshAdapter(GL gl, DatCollection dats, ILogger logger)
+ {
+ ArgumentNullException.ThrowIfNull(gl);
+ ArgumentNullException.ThrowIfNull(dats);
+ 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.
+ }
+
+ private WbMeshAdapter()
+ {
+ // Uninitialized constructor — only for tests / for cases where the
+ // flag is off and the caller wants a Dispose-safe no-op instance.
+ }
+
+ /// 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;
+
+ public void IncrementRefCount(ulong id)
+ {
+ // No-op until Task 9.
+ }
+
+ public void DecrementRefCount(ulong id)
+ {
+ // No-op until Task 9.
+ }
+
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+ // _meshManager?.Dispose();
+ // _graphicsDevice?.Dispose();
+ }
+}
diff --git a/tests/AcDream.Core.Tests/Rendering/Wb/WbMeshAdapterTests.cs b/tests/AcDream.Core.Tests/Rendering/Wb/WbMeshAdapterTests.cs
new file mode 100644
index 0000000..d92bd46
--- /dev/null
+++ b/tests/AcDream.Core.Tests/Rendering/Wb/WbMeshAdapterTests.cs
@@ -0,0 +1,47 @@
+using System;
+using AcDream.App.Rendering.Wb;
+using Microsoft.Extensions.Logging.Abstractions;
+using Silk.NET.OpenGL;
+
+namespace AcDream.Core.Tests.Rendering.Wb;
+
+public sealed class WbMeshAdapterTests
+{
+ [Fact]
+ public void Construct_WithNullGl_ThrowsArgumentNull()
+ {
+ 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));
+ }
+
+ [Fact]
+ public void Dispose_OnUninitializedAdapter_DoesNotThrow()
+ {
+ var adapter = WbMeshAdapter.CreateUninitialized();
+ adapter.Dispose(); // no-op when fields are null
+ adapter.Dispose(); // idempotent
+ }
+
+ [Fact]
+ public void IncrementRefCount_OnUninitializedAdapter_NoOps()
+ {
+ var adapter = WbMeshAdapter.CreateUninitialized();
+ // Should not throw, even though there's no underlying mesh manager.
+ adapter.IncrementRefCount(0x01000001ul);
+ adapter.DecrementRefCount(0x01000001ul);
+ }
+}