fix(portal): synchronize destination presentation state
This commit is contained in:
parent
4b1bceefbb
commit
e95f55f25b
42 changed files with 2815 additions and 288 deletions
|
|
@ -105,7 +105,7 @@ namespace AcDream.App.Rendering.Wb {
|
|||
|
||||
public bool IsDisposed { get; private set; }
|
||||
private readonly ConcurrentDictionary<ulong, ObjectRenderData> _renderData = new();
|
||||
private readonly ConcurrentDictionary<ulong, int> _usageCount = new();
|
||||
private readonly MeshOwnershipCounter _ownership = new();
|
||||
private readonly ConcurrentDictionary<ulong, (Vector3 Min, Vector3 Max)?> _boundsCache = new();
|
||||
private readonly ConcurrentDictionary<ulong, Task<ObjectMeshData?>> _preparationTasks = new();
|
||||
|
||||
|
|
@ -119,12 +119,10 @@ namespace AcDream.App.Rendering.Wb {
|
|||
private readonly Dictionary<(int Width, int Height, TextureFormat Format), List<TextureAtlasManager>> _globalAtlases = new();
|
||||
|
||||
// CPU-side cache for prepared mesh data (to avoid re-reading/decoding from DAT)
|
||||
private readonly Dictionary<ulong, ObjectMeshData> _cpuMeshCache = new();
|
||||
private readonly LinkedList<ulong> _cpuLruList = new();
|
||||
private readonly int _maxCpuCacheSize = 100;
|
||||
private readonly CpuMeshUploadCache _cpuMeshCache;
|
||||
|
||||
private readonly ConcurrentQueue<ObjectMeshData> _stagedMeshData = new();
|
||||
public ConcurrentQueue<ObjectMeshData> StagedMeshData => _stagedMeshData;
|
||||
private readonly MeshUploadStagingQueue _stagedMeshData = new();
|
||||
|
||||
/// <summary>#125: how many times a failed GL upload is re-staged before
|
||||
/// giving up loudly. Small — a transient GL error clears on the next
|
||||
|
|
@ -141,17 +139,28 @@ namespace AcDream.App.Rendering.Wb {
|
|||
/// on re-prepare) and gives up loudly past <see cref="MaxUploadRetries"/>.
|
||||
/// </summary>
|
||||
public bool UploadOrRequeue(ObjectMeshData meshData) {
|
||||
if (UploadMeshData(meshData) is not null)
|
||||
if (UploadMeshData(meshData) is not null) {
|
||||
_stagedMeshData.Complete(meshData.ObjectId);
|
||||
return false; // success (incl. legitimate 0-vertex → empty render data)
|
||||
if (HasRenderData(meshData.ObjectId))
|
||||
}
|
||||
if (HasRenderData(meshData.ObjectId)) {
|
||||
_stagedMeshData.Complete(meshData.ObjectId);
|
||||
return false; // raced to present by another path
|
||||
}
|
||||
meshData.UploadAttempts++;
|
||||
if (meshData.UploadAttempts < MaxUploadRetries)
|
||||
return true; // re-stage for next frame
|
||||
_stagedMeshData.Complete(meshData.ObjectId);
|
||||
Console.WriteLine($"[up-retry] 0x{meshData.ObjectId:X10} upload failed {meshData.UploadAttempts}x — giving up (was the #125 silent sticky drop; a GL error is being surfaced, not hidden)");
|
||||
return false;
|
||||
}
|
||||
|
||||
internal bool TryDequeueStagedMeshData(out ObjectMeshData? data) =>
|
||||
_stagedMeshData.TryDequeue(out data);
|
||||
|
||||
internal void RequeueStagedMeshData(ObjectMeshData data) =>
|
||||
_stagedMeshData.Requeue(data);
|
||||
|
||||
public GlobalMeshBuffer? GlobalBuffer { get; }
|
||||
private readonly bool _useModernRendering;
|
||||
|
||||
|
|
@ -167,7 +176,8 @@ namespace AcDream.App.Rendering.Wb {
|
|||
// 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.
|
||||
_extractor = new MeshExtractor(_dats, _logger, data => _stagedMeshData.Enqueue(data));
|
||||
_cpuMeshCache = new CpuMeshUploadCache(_maxCpuCacheSize);
|
||||
_extractor = new MeshExtractor(_dats, _logger, data => _stagedMeshData.Stage(data));
|
||||
_useModernRendering = _graphicsDevice.HasOpenGL43 && _graphicsDevice.HasBindless;
|
||||
if (_useModernRendering) {
|
||||
GlobalBuffer = new GlobalMeshBuffer(_graphicsDevice.GL);
|
||||
|
|
@ -180,7 +190,7 @@ namespace AcDream.App.Rendering.Wb {
|
|||
/// </summary>
|
||||
public ObjectRenderData? GetRenderData(ulong id) {
|
||||
if (_renderData.TryGetValue(id, out var data)) {
|
||||
_usageCount.AddOrUpdate(id, 1, (_, count) => count + 1);
|
||||
_ownership.Acquire(id);
|
||||
|
||||
if (data.IsSetup) {
|
||||
foreach (var (partId, _) in data.SetupParts) {
|
||||
|
|
@ -223,7 +233,7 @@ namespace AcDream.App.Rendering.Wb {
|
|||
/// Increment reference count for an object (e.g. when a landblock starts using it).
|
||||
/// </summary>
|
||||
public void IncrementRefCount(ulong id) {
|
||||
_usageCount.AddOrUpdate(id, 1, (_, count) => count + 1);
|
||||
_ownership.Acquire(id);
|
||||
lock (_lruList) {
|
||||
_lruList.Remove(id);
|
||||
}
|
||||
|
|
@ -258,7 +268,7 @@ namespace AcDream.App.Rendering.Wb {
|
|||
/// Decrement reference count and unload GPU resources if no longer needed.
|
||||
/// </summary>
|
||||
public void DecrementRefCount(ulong id) {
|
||||
var newCount = _usageCount.AddOrUpdate(id, 0, (_, c) => c - 1);
|
||||
var newCount = _ownership.Release(id);
|
||||
if (newCount <= 0) {
|
||||
// Instead of unloading, move to LRU
|
||||
lock (_lruList) {
|
||||
|
|
@ -272,8 +282,8 @@ namespace AcDream.App.Rendering.Wb {
|
|||
/// Decrement reference count and unload if no longer needed.
|
||||
/// </summary>
|
||||
public void ReleaseRenderData(ulong id) {
|
||||
if (_usageCount.TryGetValue(id, out var count) && count > 0) {
|
||||
var newCount = _usageCount.AddOrUpdate(id, 0, (_, c) => c - 1);
|
||||
if (_ownership.IsOwned(id)) {
|
||||
var newCount = _ownership.Release(id);
|
||||
if (newCount <= 0) {
|
||||
// Instead of unloading, move to LRU
|
||||
lock (_lruList) {
|
||||
|
|
@ -291,9 +301,9 @@ namespace AcDream.App.Rendering.Wb {
|
|||
var idToEvict = _lruList.First!.Value;
|
||||
_lruList.RemoveFirst();
|
||||
|
||||
if (_usageCount.TryGetValue(idToEvict, out var count) && count <= 0) {
|
||||
if (!_ownership.IsOwned(idToEvict)) {
|
||||
UnloadObject(idToEvict);
|
||||
_usageCount.TryRemove(idToEvict, out _);
|
||||
_ownership.Remove(idToEvict);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -309,18 +319,15 @@ namespace AcDream.App.Rendering.Wb {
|
|||
var idToEvict = _lruList.First!.Value;
|
||||
_lruList.RemoveFirst();
|
||||
|
||||
if (_usageCount.TryGetValue(idToEvict, out var count) && count <= 0) {
|
||||
if (!_ownership.IsOwned(idToEvict)) {
|
||||
UnloadObject(idToEvict);
|
||||
_usageCount.TryRemove(idToEvict, out _);
|
||||
_ownership.Remove(idToEvict);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also clear CPU mesh cache
|
||||
lock (_cpuMeshCache) {
|
||||
_cpuMeshCache.Clear();
|
||||
_cpuLruList.Clear();
|
||||
}
|
||||
_cpuMeshCache.Clear();
|
||||
}
|
||||
|
||||
public struct EnvCellGeomRequest {
|
||||
|
|
@ -338,13 +345,8 @@ namespace AcDream.App.Rendering.Wb {
|
|||
if (IsDisposed || HasRenderData(geomId)) return Task.FromResult<ObjectMeshData?>(null);
|
||||
|
||||
// Check CPU cache first
|
||||
lock (_cpuMeshCache) {
|
||||
if (_cpuMeshCache.TryGetValue(geomId, out var cachedData)) {
|
||||
_cpuLruList.Remove(geomId);
|
||||
_cpuLruList.AddLast(geomId);
|
||||
return Task.FromResult<ObjectMeshData?>(cachedData);
|
||||
}
|
||||
}
|
||||
if (_cpuMeshCache.TryGetAndStage(geomId, _stagedMeshData, out var cachedData))
|
||||
return Task.FromResult(cachedData);
|
||||
|
||||
// Return existing task if already running or queued
|
||||
if (_preparationTasks.TryGetValue(geomId, out var existing)) {
|
||||
|
|
@ -381,13 +383,8 @@ namespace AcDream.App.Rendering.Wb {
|
|||
if (IsDisposed || HasRenderData(id)) return Task.FromResult<ObjectMeshData?>(null);
|
||||
|
||||
// Check CPU cache first
|
||||
lock (_cpuMeshCache) {
|
||||
if (_cpuMeshCache.TryGetValue(id, out var cachedData)) {
|
||||
_cpuLruList.Remove(id);
|
||||
_cpuLruList.AddLast(id);
|
||||
return Task.FromResult<ObjectMeshData?>(cachedData);
|
||||
}
|
||||
}
|
||||
if (_cpuMeshCache.TryGetAndStage(id, _stagedMeshData, out var cachedData))
|
||||
return Task.FromResult(cachedData);
|
||||
|
||||
// Return existing task if already running or queued
|
||||
if (_preparationTasks.TryGetValue(id, out var existing)) {
|
||||
|
|
@ -476,16 +473,8 @@ namespace AcDream.App.Rendering.Wb {
|
|||
data = _extractor.PrepareMeshData(id, isSetup, CancellationToken.None);
|
||||
}
|
||||
if (data != null) {
|
||||
lock (_cpuMeshCache) {
|
||||
if (_cpuMeshCache.Count >= _maxCpuCacheSize) {
|
||||
var oldest = _cpuLruList.First!.Value;
|
||||
_cpuLruList.RemoveFirst();
|
||||
_cpuMeshCache.Remove(oldest);
|
||||
}
|
||||
_cpuMeshCache[id] = data;
|
||||
_cpuLruList.AddLast(id);
|
||||
}
|
||||
_stagedMeshData.Enqueue(data);
|
||||
_cpuMeshCache.Store(data);
|
||||
_stagedMeshData.Stage(data);
|
||||
}
|
||||
tcs.TrySetResult(data);
|
||||
}
|
||||
|
|
@ -538,26 +527,7 @@ namespace AcDream.App.Rendering.Wb {
|
|||
try {
|
||||
if (_renderData.TryGetValue(meshData.ObjectId, out var existing)) {
|
||||
_preparationTasks.TryRemove(meshData.ObjectId, out _);
|
||||
if (existing.IsSetup) {
|
||||
foreach (var (partId, _) in existing.SetupParts) {
|
||||
IncrementRefCount(partId);
|
||||
lock (_lruList) {
|
||||
_lruList.Remove(partId);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Increment ref counts for all textures in this GfxObj
|
||||
foreach (var batch in existing.Batches) {
|
||||
if (batch.Atlas != null) {
|
||||
batch.Atlas.AddTexture(batch.Key, Array.Empty<byte>());
|
||||
}
|
||||
}
|
||||
}
|
||||
IncrementRefCount(meshData.ObjectId);
|
||||
lock (_lruList) {
|
||||
_lruList.Remove(meshData.ObjectId);
|
||||
}
|
||||
UpdateLruAfterUpload(meshData.ObjectId);
|
||||
return existing;
|
||||
}
|
||||
|
||||
|
|
@ -588,7 +558,6 @@ namespace AcDream.App.Rendering.Wb {
|
|||
MemorySize = 1024 // Small overhead for the setup itself
|
||||
};
|
||||
_renderData.TryAdd(meshData.ObjectId, data);
|
||||
IncrementRefCount(meshData.ObjectId);
|
||||
_currentGpuMemory += data.MemorySize;
|
||||
|
||||
// Increment ref counts for all parts
|
||||
|
|
@ -596,6 +565,8 @@ namespace AcDream.App.Rendering.Wb {
|
|||
IncrementRefCount(partId);
|
||||
}
|
||||
|
||||
UpdateLruAfterUpload(meshData.ObjectId);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
|
@ -619,15 +590,12 @@ namespace AcDream.App.Rendering.Wb {
|
|||
renderData.DIDDegrade = meshData.DIDDegrade;
|
||||
renderData.SelectionSphere = meshData.SelectionSphere;
|
||||
_renderData.TryAdd(meshData.ObjectId, renderData);
|
||||
IncrementRefCount(meshData.ObjectId);
|
||||
_currentGpuMemory += renderData.MemorySize;
|
||||
UpdateLruAfterUpload(meshData.ObjectId);
|
||||
|
||||
// Clear texture data after upload to save RAM
|
||||
foreach (var batchList in meshData.TextureBatches.Values) {
|
||||
foreach (var batch in batchList) {
|
||||
batch.TextureData = Array.Empty<byte>();
|
||||
}
|
||||
}
|
||||
// Keep the bounded CPU cache's texture payload intact. GPU LRU
|
||||
// eviction may need to upload this same prepared mesh again;
|
||||
// clearing these bytes made a cache hit produce blank textures.
|
||||
return renderData;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
|
|
@ -636,6 +604,14 @@ namespace AcDream.App.Rendering.Wb {
|
|||
}
|
||||
}
|
||||
|
||||
private void UpdateLruAfterUpload(ulong id) {
|
||||
lock (_lruList) {
|
||||
_lruList.Remove(id);
|
||||
if (!_ownership.MarkUploadComplete(id))
|
||||
_lruList.AddLast(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets bounding box for an object (for frustum culling).
|
||||
/// </summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue