Task 26 visual verification surfaced three bugs in the dispatcher. Two are fixed here; the third is documented as a remaining issue. 1. WB's IncrementRefCount only bumps a usage counter — it does NOT trigger mesh loading. Fixed in WbMeshAdapter.IncrementRefCount: call PrepareMeshDataAsync(id, isSetup: false) on first registration. Result auto-enqueues to _stagedMeshData (line 510 of WB's ObjectMeshManager) which Tick() drains onto the GPU. 2. EntitySpawnAdapter never registered per-instance entity meshes with WB. LandblockSpawnAdapter only registers atlas-tier (ServerGuid == 0); per-instance entities fell through. Fixed by adding optional IWbMeshAdapter constructor param + tracking unique GfxObj ids per server-guid for IncrementRefCount on OnCreate / DecrementRefCount on OnRemove. 3. WbDrawDispatcher.ResolveTexture used batch.SurfaceId which WB never populates (line 1746 of ObjectMeshManager only sets batch.Key — the TextureKey struct that has SurfaceId). Switched to batch.Key.SurfaceId. Plus diagnostic counters (ACDREAM_WB_DIAG=1) for entity-seen / drawn / mesh-missing / draws-issued counts. Status: with these fixes the dispatcher now issues real draw calls (~16K/frame, validated via diagnostic). However visual verification shows characters appear "exploded" (parts spaced too far apart) and scenery (trees/rocks/fences/buildings) does not appear. Root cause analysis pending — Adjustment 7 in the plan documents the deferred work. Flag stays default-off; legacy renderer remains the production path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
203 lines
7.9 KiB
C#
203 lines
7.9 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using AcDream.Core.Meshing;
|
||
using Chorizite.OpenGLSDLBackend;
|
||
using Chorizite.OpenGLSDLBackend.Lib;
|
||
using DatReaderWriter;
|
||
using DatReaderWriter.DBObjs;
|
||
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 and exposes a stable acdream-shaped API
|
||
/// so the rest of the renderer doesn't need to know about WB's types directly.
|
||
///
|
||
/// <para>
|
||
/// 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 (~50–100 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
|
||
{
|
||
private readonly OpenGLGraphicsDevice? _graphicsDevice;
|
||
private readonly DefaultDatReaderWriter? _wbDats;
|
||
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 → 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="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, string datDir, DatCollection dats, ILogger<WbMeshAdapter> logger)
|
||
{
|
||
ArgumentNullException.ThrowIfNull(gl);
|
||
ArgumentNullException.ThrowIfNull(datDir);
|
||
ArgumentNullException.ThrowIfNull(dats);
|
||
ArgumentNullException.ThrowIfNull(logger);
|
||
|
||
_dats = dats;
|
||
_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 / 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>
|
||
/// 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();
|
||
_wbDats?.Dispose();
|
||
_graphicsDevice?.Dispose();
|
||
}
|
||
}
|