Six surgical edits to GameWindow.cs (+1 MeshManager accessor on WbMeshAdapter): 1. Field declarations (line 166-167): _envCellRenderer + _envCellFrustum. 2. Ctor init (line 1775-1778): construct WbFrustum + EnvCellRenderer, Initialize with the existing _meshShader (loaded from mesh_modern.vert/frag). 3. BuildInteriorEntitiesForStreaming (line 5444): _envCellRenderer.RegisterCell(...) replaces the cell-as-WorldEntity creation block. staticObjects is empty — cell stabs continue as WorldEntity records via the dispatcher's IndoorPass. 4. ApplyLoadedTerrainLocked (line 5885): _envCellRenderer.FinalizeLandblock(...) immediately after _buildingRegistries[lb.LandblockId] = ... — atomically commits the landblock's per-cell instance store. 5. RemoveLandblock callbacks (lines 1861 + 8955): mirror the existing _buildingRegistries.Remove(id) sites so EnvCellRenderer's storage clears in lockstep. 6. Dispose (line 10595): _envCellRenderer?.Dispose() after _wbDrawDispatcher. Plan revision (vs original plan.md Task 6): we keep the static-object stab WorldEntity hydration (lines 5440-5489) instead of deleting it — stabs need WorldEntity records for interaction (clicking) and physics. EnvCellRenderer receives empty staticObjects so it only renders cell geometry; stab rendering continues unchanged through the dispatcher. Build green. 23/23 EnvCellRenderer + WbFrustum + EnvCellSceneryInstance tests pass. App.Tests baseline holds (82/82). Pre-existing Core.Tests static-leak flakiness (8-19 failures, documented baseline) unrelated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
240 lines
9.5 KiB
C#
240 lines
9.5 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using AcDream.Core.Meshing;
|
|
using AcDream.Core.Rendering;
|
|
using DatReaderWriter;
|
|
using DatReaderWriter.DBObjs;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Silk.NET.OpenGL;
|
|
|
|
namespace AcDream.App.Rendering.Wb;
|
|
|
|
/// <summary>
|
|
/// Single seam between acdream and WB's render pipeline. Owns the
|
|
/// <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>
|
|
/// As of Phase O-T7, all DAT I/O routes through <see cref="DatCollectionAdapter"/>
|
|
/// (backed by our shared <see cref="DatCollection"/>) — the separate
|
|
/// <c>DefaultDatReaderWriter</c> file-handle set has been removed.
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|
{
|
|
private readonly OpenGLGraphicsDevice? _graphicsDevice;
|
|
private readonly ObjectMeshManager? _meshManager;
|
|
private readonly DatCollection? _dats;
|
|
private readonly AcSurfaceMetadataTable _metadataTable = new();
|
|
private readonly HashSet<ulong> _metadataPopulated = new();
|
|
|
|
/// <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;
|
|
|
|
/// <summary>
|
|
/// Constructs the full WB pipeline: OpenGLGraphicsDevice → DatCollectionAdapter
|
|
/// → 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="dats">acdream's DatCollection, used to populate the surface
|
|
/// metadata side-table via <c>GfxObjMesh.Build</c>. Shares file handles with
|
|
/// the rest of the client; read-only access from the render thread.</param>
|
|
/// <param name="logger">Logger for the adapter; ObjectMeshManager uses
|
|
/// NullLogger internally.</param>
|
|
public WbMeshAdapter(GL gl, DatCollection dats, ILogger<WbMeshAdapter> logger)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(gl);
|
|
ArgumentNullException.ThrowIfNull(dats);
|
|
ArgumentNullException.ThrowIfNull(logger);
|
|
|
|
_dats = dats;
|
|
_graphicsDevice = new OpenGLGraphicsDevice(gl, logger, new DebugRenderSettings());
|
|
_graphicsDevice.ParticleBatcher = new ParticleBatcher(_graphicsDevice);
|
|
// ConsoleErrorLogger surfaces WB's silently-caught exceptions
|
|
// (ObjectMeshManager.PrepareMeshData try/catch at line ~589).
|
|
_meshManager = new ObjectMeshManager(
|
|
_graphicsDevice,
|
|
new DatCollectionAdapter(dats),
|
|
new ConsoleErrorLogger<ObjectMeshManager>());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Minimal Console-backed logger that fires only on
|
|
/// <see cref="LogLevel.Error"/> and above. Format:
|
|
/// <code>[wb-error] <message>
|
|
/// [wb-error] <ExceptionType>: <ExceptionMessage>
|
|
/// [wb-error] at <frame> (up to 5 frames)</code>
|
|
/// Used to surface WB's silently-caught exceptions in
|
|
/// <c>ObjectMeshManager.PrepareMeshData</c>.
|
|
/// </summary>
|
|
private sealed class ConsoleErrorLogger<T> : ILogger<T>
|
|
{
|
|
public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;
|
|
public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Error;
|
|
public void Log<TState>(
|
|
LogLevel logLevel, EventId eventId, TState state, Exception? exception,
|
|
Func<TState, Exception?, string> formatter)
|
|
{
|
|
if (!IsEnabled(logLevel)) return;
|
|
var message = formatter(state, exception);
|
|
Console.WriteLine($"[wb-error] {message}");
|
|
if (exception is not null)
|
|
{
|
|
Console.WriteLine($"[wb-error] {exception.GetType().Name}: {exception.Message}");
|
|
var stack = (exception.StackTrace ?? "")
|
|
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
|
.Take(5);
|
|
foreach (var s in stack) Console.WriteLine($"[wb-error] {s.Trim()}");
|
|
}
|
|
}
|
|
|
|
private sealed class NullScope : IDisposable
|
|
{
|
|
public static readonly NullScope Instance = new();
|
|
public void Dispose() { }
|
|
}
|
|
}
|
|
|
|
private WbMeshAdapter()
|
|
{
|
|
// 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>
|
|
/// The surface metadata side-table populated on each first
|
|
/// <see cref="IncrementRefCount"/>. Queried by the draw dispatcher
|
|
/// to determine translucency, luminosity, and fog behavior per batch.
|
|
/// </summary>
|
|
public AcSurfaceMetadataTable MetadataTable => _metadataTable;
|
|
|
|
/// <summary>
|
|
/// Phase A8 (2026-05-28): exposes the underlying <see cref="ObjectMeshManager"/>
|
|
/// so <c>EnvCellRenderer</c> can share the same global mesh buffer (VAO/VBO/IBO).
|
|
/// Returns null when the adapter is uninitialized.
|
|
/// </summary>
|
|
public ObjectMeshManager? MeshManager => _meshManager;
|
|
|
|
/// <summary>
|
|
/// Returns the WB render data for <paramref name="id"/>, or null if not
|
|
/// yet uploaded or if this adapter is uninitialized. Increments WB's
|
|
/// internal usage counter — use <see cref="TryGetRenderData"/> for
|
|
/// render-loop lookups that should not affect lifecycle.
|
|
/// </summary>
|
|
public ObjectRenderData? GetRenderData(ulong id)
|
|
{
|
|
if (_isUninitialized || _meshManager is null) return null;
|
|
return _meshManager.GetRenderData(id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the WB render data for <paramref name="id"/> without
|
|
/// modifying reference counts. Returns null if the mesh is not yet
|
|
/// uploaded. Safe for render-loop lookups.
|
|
/// </summary>
|
|
public ObjectRenderData? TryGetRenderData(ulong id)
|
|
{
|
|
if (_isUninitialized || _meshManager is null) return null;
|
|
return _meshManager.TryGetRenderData(id);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void IncrementRefCount(ulong id)
|
|
{
|
|
if (_isUninitialized || _meshManager is null) return;
|
|
_meshManager.IncrementRefCount(id);
|
|
|
|
if (_metadataPopulated.Add(id))
|
|
{
|
|
PopulateMetadata(id);
|
|
|
|
// WB's IncrementRefCount alone only bumps a usage counter; it does
|
|
// NOT trigger mesh loading. We must explicitly call PrepareMeshDataAsync
|
|
// so the background workers actually decode the GfxObj. The result
|
|
// auto-enqueues into _stagedMeshData (ObjectMeshManager line 510),
|
|
// which Tick() drains onto the GPU. Until that completes,
|
|
// TryGetRenderData(id) returns null and the dispatcher silently
|
|
// skips the entity — standard streaming flicker.
|
|
//
|
|
// isSetup: false — acdream's MeshRefs already carry expanded
|
|
// per-part GfxObj ids (0x01XXXXXX). WB's Setup-expansion path is
|
|
// unused.
|
|
_meshManager.PrepareMeshDataAsync(id, isSetup: false);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void DecrementRefCount(ulong id)
|
|
{
|
|
if (_isUninitialized || _meshManager is null) return;
|
|
_meshManager.DecrementRefCount(id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
///
|
|
/// <para>
|
|
/// Order matters: <c>ProcessGLQueue</c> 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
|
|
/// <c>UploadMeshData</c> on each item to materialize the actual GL VAO /
|
|
/// VBO / IBO resources. After Tick, <c>GetRenderData</c> for any id
|
|
/// previously passed to <c>IncrementRefCount</c> may return non-null.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// No-op when the adapter is uninitialized (e.g., flag is off and the
|
|
/// adapter was constructed via <c>CreateUninitialized</c>).
|
|
/// </para>
|
|
/// </summary>
|
|
public void Tick()
|
|
{
|
|
if (_isUninitialized) return;
|
|
if (_disposed) return;
|
|
|
|
_graphicsDevice!.ProcessGLQueue();
|
|
while (_meshManager!.StagedMeshData.TryDequeue(out var meshData))
|
|
{
|
|
_meshManager.UploadMeshData(meshData);
|
|
}
|
|
}
|
|
|
|
private void PopulateMetadata(ulong id)
|
|
{
|
|
if (_dats is null) return;
|
|
if (!_dats.Portal.TryGet<GfxObj>((uint)id, out var gfxObj)) return;
|
|
|
|
var subMeshes = GfxObjMesh.Build(gfxObj, _dats);
|
|
for (int i = 0; i < subMeshes.Count; i++)
|
|
{
|
|
var sm = subMeshes[i];
|
|
_metadataTable.Add(id, i, new AcSurfaceMetadata(
|
|
sm.Translucency, sm.Luminosity, sm.Diffuse,
|
|
sm.SurfOpacity, sm.NeedsUvRepeat, sm.DisableFog));
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void Dispose()
|
|
{
|
|
if (_disposed) return;
|
|
_disposed = true;
|
|
_meshManager?.Dispose();
|
|
_graphicsDevice?.Dispose();
|
|
}
|
|
}
|