phase(N.4) Task 9: real WB pipeline bring-up + InstancedMeshRenderer routing

WbMeshAdapter now actually constructs the WB pipeline:
- OpenGLGraphicsDevice(gl, logger, DebugRenderSettings)
- DefaultDatReaderWriter(datDir) — opens its own file handles for now
  (memory cost ~50-100MB of duplicate index caches, acceptable for
  foundation work per plan Adjustment 1)
- ObjectMeshManager(graphicsDevice, dats, NullLogger)

InstancedMeshRenderer.EnsureUploaded routes through the adapter when
ACDREAM_USE_WB_FOUNDATION=1 is set; uses a WbManagedSentinel entry
in the local cache to mark "this GfxObj lives in WB now". CollectGroups
skips sentinel entries; both Draw passes skip them; Dispose skips them
(no GL resources to free — ObjectMeshManager owns those). Task 22's
WbDrawDispatcher will eventually draw WB-managed objects. With flag
off, behavior is byte-identical to before.

WbMeshAdapter constructor signature changed from (GL, DatCollection,
Logger) to (GL, string datDir, Logger). Updated tests to use
CreateUninitialized() for behavior tests and single null-GL guard test
for constructor validation. GameWindow updated to pass _datDir and to
wire _wbMeshAdapter into InstancedMeshRenderer.

AcDream.App.csproj gets direct ProjectReferences to WorldBuilder.Shared
and Chorizite.OpenGLSDLBackend — project refs are not transitive in
.NET, so AcDream.App must list them explicitly even though AcDream.Core
already references them.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-08 13:31:30 +02:00
parent 3d111e473e
commit 4ad7a985cf
5 changed files with 137 additions and 49 deletions

View file

@ -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;
/// <summary>
/// Single seam between acdream and WB's render pipeline. Owns the
/// <c>ObjectMeshManager</c> 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.
/// <c>ObjectMeshManager</c> instance and exposes a stable acdream-shaped API
/// so the rest of the renderer doesn't need to know about WB's types directly.
///
/// <para>
/// <b>Phase N.4 staging:</b> currently a stub. Real <c>ObjectMeshManager</c>
/// + <c>OpenGLGraphicsDevice</c> 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
/// <see cref="WbFoundationFlag.IsEnabled"/> flag is on.
/// The adapter constructs its own <c>DefaultDatReaderWriter</c> internally; it
/// does NOT share file handles with our <c>DatCollection</c>. This duplicates
/// index-cache memory (~50100 MB) but keeps the two subsystems fully decoupled.
/// Acceptable for Phase N.4 foundation work (plan Adjustment 1).
/// </para>
/// </summary>
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;
/// <summary>
/// True when this instance was created via <see cref="CreateUninitialized"/>;
/// all public methods no-op when uninitialized.
/// </summary>
private readonly bool _isUninitialized;
private bool _disposed;
public WbMeshAdapter(GL gl, DatCollection dats, ILogger<WbMeshAdapter> logger)
/// <summary>
/// Constructs the full WB pipeline: OpenGLGraphicsDevice → DefaultDatReaderWriter
/// → ObjectMeshManager.
/// </summary>
/// <param name="gl">Active Silk.NET GL context. Must be bound to the current
/// thread (construction runs GL queries; call from OnLoad).</param>
/// <param name="datDir">Path to the dat directory (same as the one supplied
/// to our DatCollection). DefaultDatReaderWriter opens its own file handles.</param>
/// <param name="logger">Logger for the adapter; ObjectMeshManager uses
/// NullLogger internally.</param>
public WbMeshAdapter(GL gl, string datDir, ILogger<WbMeshAdapter> 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<ObjectMeshManager>.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;
}
/// <summary>Test/init helper — produces a Dispose-safe instance with no
/// underlying mesh manager. Public methods are all no-ops.</summary>
public static WbMeshAdapter CreateUninitialized() => new();
/// <summary>Returns null until Task 9 wires up the real mesh manager.</summary>
public object? GetRenderData(ulong id) => null;
/// <summary>
/// Returns the WB render data for <paramref name="id"/>, or null if not
/// yet uploaded or if this adapter is uninitialized.
/// </summary>
public ObjectRenderData? GetRenderData(ulong id)
{
if (_isUninitialized || _meshManager is null) return null;
return _meshManager.GetRenderData(id);
}
/// <inheritdoc/>
public void IncrementRefCount(ulong id)
{
// No-op until Task 9.
if (_isUninitialized || _meshManager is null) return;
_meshManager.IncrementRefCount(id);
}
/// <inheritdoc/>
public void DecrementRefCount(ulong id)
{
// No-op until Task 9.
if (_isUninitialized || _meshManager is null) return;
_meshManager.DecrementRefCount(id);
}
/// <inheritdoc/>
public void Dispose()
{
if (_disposed) return;
_disposed = true;
// _meshManager?.Dispose();
// _graphicsDevice?.Dispose();
_meshManager?.Dispose();
_wbDats?.Dispose();
_graphicsDevice?.Dispose();
}
}