perf(render): consume prepared mesh package at runtime
This commit is contained in:
parent
c42f93b323
commit
f05afc07c1
22 changed files with 328 additions and 370 deletions
|
|
@ -84,6 +84,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
public uint SurfaceId { get; set; }
|
||||
public TextureKey Key { get; set; }
|
||||
public DatReaderWriter.Enums.CullMode CullMode { get; set; }
|
||||
public AcDream.Core.Meshing.TranslucencyKind Translucency { get; set; }
|
||||
public bool IsTransparent { get; set; }
|
||||
public bool IsAdditive { get; set; }
|
||||
public bool HasWrappingUVs { get; set; }
|
||||
|
|
@ -102,18 +103,15 @@ namespace AcDream.App.Rendering.Wb
|
|||
public class ObjectMeshManager : IDisposable
|
||||
{
|
||||
private readonly OpenGLGraphicsDevice _graphicsDevice;
|
||||
private readonly IDatReaderWriter _dats;
|
||||
private readonly IPreparedAssetSource _preparedAssets;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// MP1a (2026-07-05): the GL-free CPU extraction half, verbatim-moved to
|
||||
/// AcDream.Content so the MP1b bake tool can run it without a GL context.
|
||||
/// Owns the dat read → mesh build → inline texture decode pipeline; this
|
||||
/// class keeps the queue/worker lifecycle and all GL upload.
|
||||
/// The immutable prepared-payload source is injected by composition.
|
||||
/// Production uses the validated pak; UI Studio explicitly supplies the
|
||||
/// live-DAT tooling adapter. This class owns queue/worker lifecycle,
|
||||
/// bounded CPU staging, and all GL upload.
|
||||
/// </summary>
|
||||
private readonly MeshExtractor _extractor;
|
||||
|
||||
internal IDatReaderWriter Dats => _dats;
|
||||
|
||||
public bool IsDisposed { get; private set; }
|
||||
private readonly object _disposeGate = new();
|
||||
|
|
@ -141,7 +139,6 @@ namespace AcDream.App.Rendering.Wb
|
|||
private readonly Dictionary<ulong, RetryableResourceReleaseLedger> _uploadRollbacks = new();
|
||||
private readonly Queue<ulong> _uploadRollbackQueue = new();
|
||||
private readonly MeshOwnershipCounter _ownership = new();
|
||||
private readonly ConcurrentDictionary<ulong, (Vector3 Min, Vector3 Max)?> _boundsCache = new();
|
||||
private readonly ConcurrentDictionary<ulong, Task<ObjectMeshData?>> _preparationTasks = new();
|
||||
|
||||
// LRU Cache for Unused objects
|
||||
|
|
@ -280,12 +277,11 @@ namespace AcDream.App.Rendering.Wb
|
|||
internal CacheStats CpuMeshCacheStats => _cpuMeshCache.Stats;
|
||||
|
||||
/// <summary>Decoded-texture cache hit/miss/eviction counts (2026-07-24 measurement-tooling review).</summary>
|
||||
internal CacheStats DecodedTextureCacheStats => _extractor.DecodedTextureCacheStats;
|
||||
internal CacheStats DecodedTextureCacheStats =>
|
||||
_preparedAssets.DecodedTextureCacheStats;
|
||||
|
||||
private sealed class PreparationRequest(
|
||||
ulong id,
|
||||
bool isSetup,
|
||||
EnvCellGeomRequest? envCell,
|
||||
PreparedAssetRequest asset,
|
||||
ObjectMeshData? cachedData,
|
||||
TaskCompletionSource<ObjectMeshData?> completion,
|
||||
CancellationTokenSource cancellation)
|
||||
|
|
@ -296,9 +292,8 @@ namespace AcDream.App.Rendering.Wb
|
|||
private bool _disposeRequested;
|
||||
private bool _cancellationDisposed;
|
||||
|
||||
public ulong Id { get; } = id;
|
||||
public bool IsSetup { get; } = isSetup;
|
||||
public EnvCellGeomRequest? EnvCell { get; } = envCell;
|
||||
public PreparedAssetRequest Asset { get; } = asset;
|
||||
public ulong Id => Asset.RuntimeObjectId;
|
||||
public ObjectMeshData? CachedData { get; } = cachedData;
|
||||
public TaskCompletionSource<ObjectMeshData?> Completion { get; } = completion;
|
||||
public CancellationTokenSource Cancellation { get; } = cancellation;
|
||||
|
|
@ -402,26 +397,18 @@ namespace AcDream.App.Rendering.Wb
|
|||
public bool IsQueued { get; set; }
|
||||
}
|
||||
|
||||
public ObjectMeshManager(OpenGLGraphicsDevice graphicsDevice, IDatReaderWriter dats, ILogger<ObjectMeshManager> logger)
|
||||
public ObjectMeshManager(
|
||||
OpenGLGraphicsDevice graphicsDevice,
|
||||
IPreparedAssetSource preparedAssets,
|
||||
ILogger<ObjectMeshManager> logger)
|
||||
{
|
||||
_graphicsDevice = graphicsDevice;
|
||||
_dats = dats;
|
||||
_logger = logger;
|
||||
// Side-stage sink: particle-preload meshes staged mid-extraction go
|
||||
// straight onto the staged-upload queue, exactly as the pre-MP1a code
|
||||
// did — immediate enqueue, surviving a later throw in the same
|
||||
// Prepare* call. ConcurrentQueue.Enqueue is thread-safe for the
|
||||
// extractor's up-to-4 concurrent decode workers.
|
||||
_graphicsDevice = graphicsDevice
|
||||
?? throw new ArgumentNullException(nameof(graphicsDevice));
|
||||
_preparedAssets = preparedAssets
|
||||
?? throw new ArgumentNullException(nameof(preparedAssets));
|
||||
_logger = logger
|
||||
?? throw new ArgumentNullException(nameof(logger));
|
||||
_cpuMeshCache = new CpuMeshUploadCache(_maxCpuCacheSize);
|
||||
_extractor = new MeshExtractor(_dats, _logger, data =>
|
||||
{
|
||||
// Side-staged particle/setup dependencies obey the same bounded
|
||||
// producer policy as primary results. Keeping them in the CPU
|
||||
// cache makes a later explicit owner able to re-arm upload.
|
||||
_cpuMeshCache.Store(data);
|
||||
if (_ownership.IsOwned(data.ObjectId))
|
||||
_stagedMeshData.Stage(data);
|
||||
});
|
||||
_useModernRendering = _graphicsDevice.HasOpenGL43 && _graphicsDevice.HasBindless;
|
||||
if (_useModernRendering)
|
||||
{
|
||||
|
|
@ -809,6 +796,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
|
||||
public struct EnvCellGeomRequest
|
||||
{
|
||||
public uint SourceCellId;
|
||||
public uint EnvironmentId;
|
||||
public ushort CellStructure;
|
||||
public List<ushort> Surfaces;
|
||||
|
|
@ -817,12 +805,19 @@ namespace AcDream.App.Rendering.Wb
|
|||
/// <summary>
|
||||
/// Phase 1 (Background Thread): Prepare CPU-side mesh data for deduplicated EnvCell geometry.
|
||||
/// </summary>
|
||||
public Task<ObjectMeshData?> PrepareEnvCellGeomMeshDataAsync(ulong geomId, uint environmentId, ushort cellStructure, List<ushort> surfaces, CancellationToken ct = default)
|
||||
public Task<ObjectMeshData?> PrepareEnvCellGeomMeshDataAsync(
|
||||
ulong geomId,
|
||||
uint sourceCellId,
|
||||
uint environmentId,
|
||||
ushort cellStructure,
|
||||
List<ushort> surfaces,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (IsDisposed || HasRenderData(geomId)) return Task.FromResult<ObjectMeshData?>(null);
|
||||
|
||||
var envCell = new EnvCellGeomRequest
|
||||
{
|
||||
SourceCellId = sourceCellId,
|
||||
EnvironmentId = environmentId,
|
||||
CellStructure = cellStructure,
|
||||
Surfaces = surfaces
|
||||
|
|
@ -874,9 +869,12 @@ namespace AcDream.App.Rendering.Wb
|
|||
var cancellation = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
_preparationTasks[geomId] = task;
|
||||
var request = new PreparationRequest(
|
||||
geomId,
|
||||
false,
|
||||
envCell,
|
||||
PreparedAssetRequest.EnvCellGeometry(
|
||||
sourceCellId,
|
||||
geomId,
|
||||
environmentId,
|
||||
cellStructure,
|
||||
surfaces),
|
||||
deferredCachedData,
|
||||
tcs,
|
||||
cancellation);
|
||||
|
|
@ -933,10 +931,18 @@ namespace AcDream.App.Rendering.Wb
|
|||
EnvCellGeomRequest? envCell = _envCellDescriptors.TryGetValue(id, out EnvCellGeomRequest descriptor)
|
||||
? descriptor
|
||||
: null;
|
||||
PreparedAssetRequest asset = envCell is { } env
|
||||
? PreparedAssetRequest.EnvCellGeometry(
|
||||
env.SourceCellId,
|
||||
id,
|
||||
env.EnvironmentId,
|
||||
env.CellStructure,
|
||||
env.Surfaces)
|
||||
: isSetup
|
||||
? PreparedAssetRequest.Setup(checked((uint)id))
|
||||
: PreparedAssetRequest.GfxObj(checked((uint)id));
|
||||
var request = new PreparationRequest(
|
||||
id,
|
||||
isSetup,
|
||||
envCell,
|
||||
asset,
|
||||
deferredCachedData,
|
||||
tcs,
|
||||
cancellation);
|
||||
|
|
@ -956,7 +962,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
lock (_pendingRequests)
|
||||
{
|
||||
// IsDisposed re-check lets Dispose cancel and join every
|
||||
// tracked worker before the DAT mappings are released.
|
||||
// tracked worker before prepared content mappings are released.
|
||||
// Exit WITHOUT resetting the shared manual-reset event:
|
||||
// Dispose sets it once to wake all four persistent workers.
|
||||
// If the first worker reset it, the remaining three slept
|
||||
|
|
@ -991,7 +997,6 @@ namespace AcDream.App.Rendering.Wb
|
|||
}
|
||||
|
||||
ulong id = request.Id;
|
||||
bool isSetup = request.IsSetup;
|
||||
TaskCompletionSource<ObjectMeshData?> tcs = request.Completion;
|
||||
CancellationToken ct = request.Cancellation.Token;
|
||||
|
||||
|
|
@ -1010,38 +1015,21 @@ namespace AcDream.App.Rendering.Wb
|
|||
{
|
||||
|
||||
ObjectMeshData? data = request.CachedData;
|
||||
if (data is null && request.EnvCell is { } req)
|
||||
if (data is null)
|
||||
{
|
||||
uint envId = 0x0D000000u | req.EnvironmentId;
|
||||
if (_dats.Portal.TryGet<DatReaderWriter.DBObjs.Environment>(envId, out var environment))
|
||||
PreparedAssetReadResult read =
|
||||
_preparedAssets.Read(request.Asset, ct);
|
||||
data = read.Data;
|
||||
if (read.Status != PreparedAssetReadStatus.Loaded)
|
||||
{
|
||||
if (environment.Cells.TryGetValue(req.CellStructure, out var cellStruct))
|
||||
{
|
||||
data = _extractor.PrepareCellStructMeshData(id, cellStruct, req.Surfaces, Matrix4x4.Identity, ct);
|
||||
// TEMP diagnostic #105 (strip with fix): a null prep here means
|
||||
// this deduplicated cell geometry will NEVER render anywhere.
|
||||
if (data == null)
|
||||
Console.WriteLine($"[geom-null] prepare-null geom=0x{id:X10} env=0x{envId:X8} cs=0x{req.CellStructure:X4}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"[geom-null] cellstruct-missing geom=0x{id:X10} env=0x{envId:X8} cs=0x{req.CellStructure:X4}");
|
||||
}
|
||||
_logger.LogError(
|
||||
"Prepared mesh {Status} for {Type} source " +
|
||||
"0x{SourceId:X8}, runtime 0x{RuntimeId:X10}",
|
||||
read.Status,
|
||||
request.Asset.Type,
|
||||
request.Asset.SourceFileId,
|
||||
request.Asset.RuntimeObjectId);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"[geom-null] env-read-failed geom=0x{id:X10} env=0x{envId:X8}");
|
||||
}
|
||||
}
|
||||
else if (data is null)
|
||||
{
|
||||
// TEMP diagnostic #105 (strip with fix): an EnvCell geom id (bit 33)
|
||||
// whose pending request vanished gets misrouted to the generic path,
|
||||
// where its hash-derived low bits resolve to nothing -> silent null.
|
||||
if ((id & 0x2_0000_0000UL) != 0)
|
||||
Console.WriteLine($"[geom-misroute] envcell geom 0x{id:X10} had no pending request — generic path will null it");
|
||||
// If it's a direct setup or gfxobj, make sure background loads don't abort half-way
|
||||
data = _extractor.PrepareMeshData(id, isSetup, ct);
|
||||
}
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
|
|
@ -1168,7 +1156,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
/// Readiness barrier used by the streaming publisher. Missing owned
|
||||
/// data is re-armed through its retained schema, so synthetic EnvCell
|
||||
/// IDs can never fall into generic GfxObj decoding after cancellation.
|
||||
/// A deterministic DAT failure is retained until the next explicit
|
||||
/// A deterministic prepared-payload failure is retained until the next explicit
|
||||
/// ownership schedule instead of being retried every render frame.
|
||||
/// </summary>
|
||||
internal bool EnsureRenderDataReady(ulong id)
|
||||
|
|
@ -1194,6 +1182,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
{
|
||||
_ = PrepareEnvCellGeomMeshDataAsync(
|
||||
id,
|
||||
req.SourceCellId,
|
||||
req.EnvironmentId,
|
||||
req.CellStructure,
|
||||
req.Surfaces);
|
||||
|
|
@ -1206,16 +1195,16 @@ namespace AcDream.App.Rendering.Wb
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase 1 (Background Thread): Prepare CPU-side mesh data from DAT.
|
||||
/// This loads vertices, indices, and texture data but creates NO GPU resources.
|
||||
/// Thread-safe: only reads from DAT files.
|
||||
///
|
||||
/// MP1a (2026-07-05): delegates to <see cref="MeshExtractor.PrepareMeshData"/>,
|
||||
/// the verbatim-moved GL-free extraction dispatcher. See <see cref="_extractor"/>.
|
||||
/// Synchronously reads an immutable prepared payload without creating
|
||||
/// GPU resources. The production source is the validated pak; explicit
|
||||
/// tooling adapters may use live DAT extraction.
|
||||
/// </summary>
|
||||
public ObjectMeshData? PrepareMeshData(ulong id, bool isSetup, CancellationToken ct = default)
|
||||
{
|
||||
return _extractor.PrepareMeshData(id, isSetup, ct);
|
||||
PreparedAssetRequest request = isSetup
|
||||
? PreparedAssetRequest.Setup(checked((uint)id))
|
||||
: PreparedAssetRequest.GfxObj(checked((uint)id));
|
||||
return _preparedAssets.Read(request, ct).Data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1709,86 +1698,6 @@ namespace AcDream.App.Rendering.Wb
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets bounding box for an object (for frustum culling).
|
||||
/// </summary>
|
||||
public (Vector3 Min, Vector3 Max)? GetBounds(ulong id, bool isSetup)
|
||||
{
|
||||
if (_boundsCache.TryGetValue(id, out var cachedBounds))
|
||||
{
|
||||
return cachedBounds;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
(Vector3 Min, Vector3 Max)? result = null;
|
||||
uint datId = (uint)(id & 0xFFFFFFFFu);
|
||||
var resolutions = _dats.ResolveId(datId).ToList();
|
||||
var selectedResolution = resolutions.OrderByDescending(r => r.Database == _dats.Portal).FirstOrDefault();
|
||||
if (selectedResolution == null) return null;
|
||||
|
||||
var type = selectedResolution.Type;
|
||||
var db = selectedResolution.Database;
|
||||
|
||||
if (type == DBObjType.Setup)
|
||||
{
|
||||
var min = new Vector3(float.MaxValue);
|
||||
var max = new Vector3(float.MinValue);
|
||||
bool hasBounds = false;
|
||||
var parts = new List<(ulong GfxObjId, Matrix4x4 Transform)>();
|
||||
|
||||
_extractor.CollectParts(datId, Matrix4x4.Identity, parts, ref min, ref max, ref hasBounds, CancellationToken.None);
|
||||
result = hasBounds ? (min, max) : null;
|
||||
}
|
||||
else if (type == DBObjType.EnvCell)
|
||||
{
|
||||
if (!db.TryGet<EnvCell>(datId, out var envCell)) return null;
|
||||
|
||||
// If bit 32 is set, this is a request for the cell's synthetic geometry only
|
||||
if ((id & 0x1_0000_0000UL) != 0)
|
||||
{
|
||||
uint envId = 0x0D000000u | envCell.EnvironmentId;
|
||||
if (_dats.Portal.TryGet<DatReaderWriter.DBObjs.Environment>(envId, out var environment))
|
||||
{
|
||||
if (environment.Cells.TryGetValue(envCell.CellStructure, out var cellStruct))
|
||||
{
|
||||
var min = new Vector3(float.MaxValue);
|
||||
var max = new Vector3(float.MinValue);
|
||||
foreach (var vert in cellStruct.VertexArray.Vertices.Values)
|
||||
{
|
||||
min = Vector3.Min(min, vert.Origin);
|
||||
max = Vector3.Max(max, vert.Origin);
|
||||
}
|
||||
result = (min, max);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var min = new Vector3(float.MaxValue);
|
||||
var max = new Vector3(float.MinValue);
|
||||
bool hasBounds = false;
|
||||
var parts = new List<(ulong GfxObjId, Matrix4x4 Transform)>();
|
||||
|
||||
_extractor.CollectParts(datId, Matrix4x4.Identity, parts, ref min, ref max, ref hasBounds, CancellationToken.None);
|
||||
result = hasBounds ? (min, max) : null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!db.TryGet<GfxObj>(datId, out var gfxObj)) return null;
|
||||
result = _extractor.ComputeBounds(gfxObj, Vector3.One);
|
||||
}
|
||||
_boundsCache[id] = result;
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error computing bounds for 0x{Id:X8}", id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#region Private: Background Preparation
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1981,6 +1890,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
TextureIndex = textureIndex,
|
||||
TextureSize = (format.Width, format.Height),
|
||||
TextureFormat = format.Format,
|
||||
Translucency = batch.Translucency,
|
||||
IsTransparent = batch.IsTransparent,
|
||||
IsAdditive = batch.IsAdditive,
|
||||
HasWrappingUVs = batch.HasWrappingUVs,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue