perf(render): consume prepared mesh package at runtime

This commit is contained in:
Erik 2026-07-24 15:07:25 +02:00
parent c42f93b323
commit f05afc07c1
22 changed files with 328 additions and 370 deletions

View file

@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using AcDream.Content;
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;
@ -48,9 +46,7 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
private readonly OpenGLGraphicsDevice? _graphicsDevice;
private readonly ObjectMeshManager? _meshManager;
private readonly AcDream.App.Rendering.IGpuResourceRetirementQueue? _resourceRetirement;
private readonly IDatReaderWriter? _dats;
private readonly AcSurfaceMetadataTable _metadataTable = new();
private readonly HashSet<ulong> _metadataPopulated = new();
private readonly IPreparedAssetSource? _ownedPreparedAssets;
private readonly MeshUploadFrameBudget _uploadBudget = new(new MeshUploadBudgetLimits(
MaximumUploadsPerFrame,
MaximumUploadBytesPerFrame,
@ -95,35 +91,72 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
_meshManager?.CpuCacheDiagnostics ?? default;
/// <summary>
/// Constructs the full WB pipeline: OpenGLGraphicsDevice → shared bounded
/// DAT facade → ObjectMeshManager.
/// Constructs the UI-Studio/tooling WB pipeline. Production composition
/// supplies the validated pak source through the internal overload below;
/// this explicit tooling seam retains live-DAT extraction.
/// </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 shared runtime DAT facade, 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="dats">acdream's shared runtime DAT facade. Tooling uses it
/// through an explicitly owned <see cref="DatPreparedAssetSource"/>.</param>
/// <param name="logger">Logger for the adapter; ObjectMeshManager uses
/// NullLogger internally.</param>
public WbMeshAdapter(GL gl, IDatReaderWriter dats, ILogger<WbMeshAdapter> logger)
: this(gl, dats, logger, AcDream.App.Rendering.ImmediateGpuResourceRetirementQueue.Instance)
: this(
gl,
dats,
preparedAssets: null,
logger,
AcDream.App.Rendering.ImmediateGpuResourceRetirementQueue.Instance,
ownsPreparedAssets: true)
{
}
internal static WbMeshAdapter CreateWithLiveDatPreparedAssets(
GL gl,
IDatReaderWriter dats,
ILogger<WbMeshAdapter> logger,
AcDream.App.Rendering.IGpuResourceRetirementQueue resourceRetirement) =>
new(
gl,
dats,
preparedAssets: null,
logger,
resourceRetirement,
ownsPreparedAssets: true);
internal WbMeshAdapter(
GL gl,
IDatReaderWriter dats,
IPreparedAssetSource preparedAssets,
ILogger<WbMeshAdapter> logger,
AcDream.App.Rendering.IGpuResourceRetirementQueue resourceRetirement)
: this(
gl,
dats,
preparedAssets,
logger,
resourceRetirement,
ownsPreparedAssets: false)
{
}
private WbMeshAdapter(
GL gl,
IDatReaderWriter dats,
IPreparedAssetSource? preparedAssets,
ILogger<WbMeshAdapter> logger,
AcDream.App.Rendering.IGpuResourceRetirementQueue resourceRetirement,
bool ownsPreparedAssets)
{
ArgumentNullException.ThrowIfNull(gl);
ArgumentNullException.ThrowIfNull(dats);
ArgumentNullException.ThrowIfNull(logger);
_dats = dats;
_resourceRetirement = resourceRetirement;
var resources = new AcDream.App.Rendering.ResourceCleanupGroup();
OpenGLGraphicsDevice? graphicsDevice = null;
IPreparedAssetSource? resolvedPreparedAssets = preparedAssets;
ObjectMeshManager? meshManager = null;
try
{
@ -145,11 +178,20 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
}
});
resources.Add("WB graphics device", graphicsDeviceRelease.Run);
if (resolvedPreparedAssets is null)
{
resolvedPreparedAssets = new DatPreparedAssetSource(
dats,
new ConsoleErrorLogger<ObjectMeshManager>());
resources.Add(
"WB tooling prepared asset source",
resolvedPreparedAssets.Dispose);
}
// ConsoleErrorLogger surfaces WB's silently-caught exceptions
// (ObjectMeshManager.PrepareMeshData try/catch at line ~589).
meshManager = new ObjectMeshManager(
graphicsDevice,
dats,
resolvedPreparedAssets,
new ConsoleErrorLogger<ObjectMeshManager>());
resources.Add("WB object mesh manager", meshManager.Dispose);
resources.TransferAll();
@ -163,6 +205,9 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
_graphicsDevice = graphicsDevice;
_meshManager = meshManager;
_ownedPreparedAssets = ownsPreparedAssets
? resolvedPreparedAssets
: null;
}
/// <summary>
@ -213,13 +258,6 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
/// 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).
@ -265,13 +303,8 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
if (_isUninitialized || _meshManager is null) return;
_meshManager.IncrementRefCount(id);
bool metadataClaimed = false;
try
{
metadataClaimed = _metadataPopulated.Add(id);
if (metadataClaimed)
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
@ -281,9 +314,8 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
// skips the entity — standard streaming flicker.
//
// #128 (2026-06-11): Prepare must RE-ARM whenever the id has no render
// data — NOT only on the first-ever registration. The old
// first-ever-only gate (`if (_metadataPopulated.Add(id))`) permanently
// lost any id whose initial decode was cancelled before completing
// data — NOT only on the first-ever registration. A first-only gate
// permanently lost any id whose initial read was cancelled before completing
// (landblock unload → CancelStagedUploads during login/teleport
// churn) or whose upload was later LRU-evicted: every subsequent
// registration skipped Prepare, so the mesh stayed invisible for the
@ -299,19 +331,13 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
// isSetup: false — acdream's MeshRefs already carry expanded
// per-part GfxObj ids (0x01XXXXXX). WB's Setup-expansion path is
// unused.
if (metadataClaimed || _meshManager.TryGetRenderData(id) is null)
if (_meshManager.TryGetRenderData(id) is null)
_meshManager.PrepareMeshDataAsync(id, isSetup: false);
}
catch (Exception acquireFailure)
{
if (metadataClaimed)
{
_metadataPopulated.Remove(id);
_metadataTable.Remove(id);
}
// IncrementRefCount is a public ownership boundary. Metadata/DAT
// preparation happens after ObjectMeshManager commits its count,
// IncrementRefCount is a public ownership boundary. Prepared-data
// acquisition happens after ObjectMeshManager commits its count,
// so compensate before reporting failure. ObjectMeshManager's
// decrement has a strong exception guarantee; if compensation
// itself fails, expose that the increment remains committed so a
@ -612,21 +638,6 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
$" (arraysBefore={before.ArraysWithPending})");
}
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()
{
@ -646,6 +657,7 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
frameFlights.WaitForSubmittedWork();
},
() => _meshManager?.Dispose(),
() => _ownedPreparedAssets?.Dispose(),
() => DrainGraphicsQueue("publishing mesh resource retirements"),
() =>
{