fix(rendering): bound portal resource lifetime
Separate logical ownership, render publication, and GPU retirement across live entities, landblocks, particles, textures, mesh arenas, portal/UI teardown, and per-frame scratch storage. Add bounded DAT/texture caches, upload budgets, three-frame fence retirement, exact-incarnation appearance reconciliation, frame pacing, and extensive lifetime conformance coverage.\n\nThe seven-destination connected route now cuts peak working/private memory roughly in half, returns Caul to 125-153 FPS locally, and produces no WER or AMD reset.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
3971997689
commit
749e8ceeb1
225 changed files with 29107 additions and 3914 deletions
|
|
@ -17,18 +17,56 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// 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
|
||||
/// As of Phase O-T7, all DAT I/O routes through the runtime-owned shared
|
||||
/// <see cref="IDatReaderWriter"/> facade — the separate
|
||||
/// <c>DefaultDatReaderWriter</c> file-handle set has been removed.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
||||
{
|
||||
internal const int MaximumUploadsPerFrame = 8;
|
||||
internal const long MaximumUploadBytesPerFrame = 8L * 1024 * 1024;
|
||||
internal const long MaximumArrayAllocationBytesPerFrame = 8L * 1024 * 1024;
|
||||
internal const long MaximumMipmapBytesPerFrame = 8L * 1024 * 1024;
|
||||
internal const int MaximumNewArraysPerFrame = 1;
|
||||
internal const long MaximumBufferUploadBytesPerFrame = 8L * 1024 * 1024;
|
||||
// A GL buffer store is indivisible. The active vertex arena's explicit
|
||||
// supported ceiling is therefore the truthful one-allocation bound; only
|
||||
// its prefix copy is staged across frames.
|
||||
internal const long MaximumBufferAllocationBytesPerFrame =
|
||||
GlobalMeshBuffer.MaximumVertexBufferBytes;
|
||||
internal const long MaximumBufferCopyBytesPerFrame = 32L * 1024 * 1024;
|
||||
internal const int MaximumNewBuffersPerFrame = 1;
|
||||
internal const long MaximumSingleUploadBytes = 128L * 1024 * 1024;
|
||||
internal const long MaximumSingleArrayAllocationBytes = 128L * 1024 * 1024;
|
||||
internal const long MaximumSingleMipmapBytes = 128L * 1024 * 1024;
|
||||
internal const int MaximumSingleNewArrays = 16;
|
||||
internal const long MaximumSingleBufferUploadBytes = 32L * 1024 * 1024;
|
||||
internal const int MaximumReclaimedMeshesPerFrame = MaximumUploadsPerFrame;
|
||||
internal const long MaximumReclaimedMeshBytesPerFrame = 64L * 1024 * 1024;
|
||||
internal const int MaximumStaleDiscardsPerFrame = 64;
|
||||
private readonly OpenGLGraphicsDevice? _graphicsDevice;
|
||||
private readonly ObjectMeshManager? _meshManager;
|
||||
private readonly DatCollection? _dats;
|
||||
private readonly AcDream.App.Rendering.IGpuResourceRetirementQueue? _resourceRetirement;
|
||||
private readonly IDatReaderWriter? _dats;
|
||||
private readonly AcSurfaceMetadataTable _metadataTable = new();
|
||||
private readonly HashSet<ulong> _metadataPopulated = new();
|
||||
private readonly MeshUploadFrameBudget _uploadBudget = new(new MeshUploadBudgetLimits(
|
||||
MaximumUploadsPerFrame,
|
||||
MaximumUploadBytesPerFrame,
|
||||
MaximumArrayAllocationBytesPerFrame,
|
||||
MaximumMipmapBytesPerFrame,
|
||||
MaximumNewArraysPerFrame,
|
||||
MaximumBufferUploadBytesPerFrame,
|
||||
MaximumBufferAllocationBytesPerFrame,
|
||||
MaximumBufferCopyBytesPerFrame,
|
||||
MaximumNewBuffersPerFrame,
|
||||
MaximumSingleUploadBytes,
|
||||
MaximumSingleArrayAllocationBytes,
|
||||
MaximumSingleMipmapBytes,
|
||||
MaximumSingleNewArrays,
|
||||
MaximumSingleBufferUploadBytes));
|
||||
private readonly HashSet<TextureAtlasManager> _mipmapsBudgeted = new();
|
||||
|
||||
/// <summary>
|
||||
/// True when this instance was created via <see cref="CreateUninitialized"/>;
|
||||
|
|
@ -37,32 +75,64 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
private readonly bool _isUninitialized;
|
||||
|
||||
private bool _disposed;
|
||||
private AcDream.App.Rendering.OrderedResourceTeardown? _teardown;
|
||||
internal int LastUploadCount { get; private set; }
|
||||
internal long LastUploadBytes { get; private set; }
|
||||
internal int LastStaleDiscardCount { get; private set; }
|
||||
internal long LastArrayAllocationBytes { get; private set; }
|
||||
internal long LastPlannedMipmapBytes { get; private set; }
|
||||
internal int LastNewArrayCount { get; private set; }
|
||||
internal long LastBufferUploadBytes { get; private set; }
|
||||
internal long LastBufferAllocationBytes { get; private set; }
|
||||
internal long LastBufferCopyBytes { get; private set; }
|
||||
internal int LastNewBufferCount { get; private set; }
|
||||
internal int LastMipmapArrayCount { get; private set; }
|
||||
internal long LastMipmapBytes { get; private set; }
|
||||
internal int StagedUploadBacklog => _meshManager?.StagedMeshCount ?? 0;
|
||||
internal long StagedUploadBytes => _meshManager?.StagedMeshBytes ?? 0;
|
||||
internal bool StagingAtHighWater => _meshManager?.StagingAtHighWater ?? false;
|
||||
internal (int Count, long Bytes) CpuMeshCacheDiagnostics =>
|
||||
_meshManager?.CpuCacheDiagnostics ?? default;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs the full WB pipeline: OpenGLGraphicsDevice → DatCollectionAdapter
|
||||
/// → ObjectMeshManager.
|
||||
/// Constructs the full WB pipeline: OpenGLGraphicsDevice → shared bounded
|
||||
/// DAT facade → 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
|
||||
/// <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="logger">Logger for the adapter; ObjectMeshManager uses
|
||||
/// NullLogger internally.</param>
|
||||
public WbMeshAdapter(GL gl, DatCollection dats, ILogger<WbMeshAdapter> logger)
|
||||
public WbMeshAdapter(GL gl, IDatReaderWriter dats, ILogger<WbMeshAdapter> logger)
|
||||
: this(gl, dats, logger, AcDream.App.Rendering.ImmediateGpuResourceRetirementQueue.Instance)
|
||||
{
|
||||
}
|
||||
|
||||
internal WbMeshAdapter(
|
||||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
ILogger<WbMeshAdapter> logger,
|
||||
AcDream.App.Rendering.IGpuResourceRetirementQueue resourceRetirement)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gl);
|
||||
ArgumentNullException.ThrowIfNull(dats);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
_dats = dats;
|
||||
_graphicsDevice = new OpenGLGraphicsDevice(gl, logger, new DebugRenderSettings());
|
||||
_resourceRetirement = resourceRetirement;
|
||||
_graphicsDevice = new OpenGLGraphicsDevice(
|
||||
gl,
|
||||
logger,
|
||||
new DebugRenderSettings(),
|
||||
resourceRetirement);
|
||||
_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),
|
||||
dats,
|
||||
new ConsoleErrorLogger<ObjectMeshManager>());
|
||||
}
|
||||
|
||||
|
|
@ -157,7 +227,7 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
// An uninitialized adapter owns no render pipeline, so it must not
|
||||
// manufacture a readiness wait that can never complete. The modern
|
||||
// production path is always initialized at startup.
|
||||
return _isUninitialized || _meshManager?.TryGetRenderData(id) is not null;
|
||||
return _isUninitialized || (_meshManager?.EnsureRenderDataReady(id) ?? false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -166,9 +236,12 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
if (_isUninitialized || _meshManager is null) return;
|
||||
_meshManager.IncrementRefCount(id);
|
||||
|
||||
bool firstEver = _metadataPopulated.Add(id);
|
||||
if (firstEver)
|
||||
PopulateMetadata(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
|
||||
|
|
@ -197,8 +270,37 @@ 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 (firstEver || _meshManager.TryGetRenderData(id) is null)
|
||||
_meshManager.PrepareMeshDataAsync(id, isSetup: false);
|
||||
if (metadataClaimed || _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,
|
||||
// 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
|
||||
// higher-level transactional owner can retain its held marker.
|
||||
try
|
||||
{
|
||||
_meshManager.DecrementRefCount(id);
|
||||
}
|
||||
catch (Exception rollbackFailure)
|
||||
{
|
||||
throw new MeshReferenceMutationException(
|
||||
$"Mesh 0x{id:X10} reference acquisition failed and its committed increment could not be rolled back.",
|
||||
mutationCommitted: true,
|
||||
new AggregateException(acquireFailure, rollbackFailure));
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -263,6 +365,7 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
if (_isUninitialized) return;
|
||||
if (_disposed) return;
|
||||
|
||||
ObjectMeshManager meshManager = _meshManager!;
|
||||
_graphicsDevice!.ProcessGLQueue();
|
||||
// #125: drain staged uploads; a FAILED upload (UploadMeshData returned
|
||||
// null from its catch) is re-staged for a LATER frame, not dropped. The
|
||||
|
|
@ -270,43 +373,190 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
// inside the while would let a deterministic failure spin the queue in a
|
||||
// single frame. UploadOrRequeue bounds the retries (MaxUploadRetries) so
|
||||
// a genuine defect surfaces loudly instead of the old silent sticky drop.
|
||||
List<ObjectMeshData>? requeue = null;
|
||||
while (_meshManager!.TryDequeueStagedMeshData(out var meshData))
|
||||
List<MeshUploadQueueItem>? requeue = null;
|
||||
_uploadBudget.Reset();
|
||||
_mipmapsBudgeted.Clear();
|
||||
int staleDiscardCount = 0;
|
||||
bool arenaBackpressured = false;
|
||||
GlobalMeshBuffer? globalBuffer = meshManager.GlobalBuffer;
|
||||
|
||||
// A backing-store migration is real GL work, not a cost estimate. One
|
||||
// bounded copy chunk runs per frame while the old VAO remains active.
|
||||
// Uploads stay queued until the atomic final swap has completed.
|
||||
if (globalBuffer?.IsMigrationInProgress == true)
|
||||
{
|
||||
if (meshData is null)
|
||||
continue;
|
||||
if (_meshManager.UploadOrRequeue(meshData))
|
||||
GlobalMeshMaintenanceStep maintenance =
|
||||
globalBuffer.AdvanceMigration(MaximumBufferCopyBytesPerFrame);
|
||||
_uploadBudget.RecordBufferMaintenance(
|
||||
maintenance.AllocationBytes,
|
||||
maintenance.CopyBytes,
|
||||
maintenance.NewBufferCount);
|
||||
arenaBackpressured = !maintenance.Completed;
|
||||
}
|
||||
|
||||
while (globalBuffer?.IsMigrationInProgress != true
|
||||
&& _uploadBudget.BufferCopyBytes == 0
|
||||
&& _uploadBudget.ObjectCount < MaximumUploadsPerFrame)
|
||||
{
|
||||
staleDiscardCount += meshManager.DiscardUnownedStagedPrefix(
|
||||
MaximumStaleDiscardsPerFrame - staleDiscardCount);
|
||||
if (staleDiscardCount >= MaximumStaleDiscardsPerFrame)
|
||||
break;
|
||||
if (!meshManager.TryPeekStagedMeshData(out MeshUploadQueueItem next))
|
||||
break;
|
||||
|
||||
GlobalMeshCapacityResult capacity;
|
||||
GlobalMeshMaintenanceStep maintenance;
|
||||
try
|
||||
{
|
||||
capacity = meshManager.EnsureGlobalBufferCapacity(next, out maintenance);
|
||||
}
|
||||
catch (NotSupportedException error)
|
||||
{
|
||||
RejectUnsupportedHead(meshManager, next, error);
|
||||
throw;
|
||||
}
|
||||
if (capacity == GlobalMeshCapacityResult.MigrationStarted)
|
||||
{
|
||||
_uploadBudget.RecordBufferMaintenance(
|
||||
maintenance.AllocationBytes,
|
||||
maintenance.CopyBytes,
|
||||
maintenance.NewBufferCount);
|
||||
arenaBackpressured = true;
|
||||
break;
|
||||
}
|
||||
if (capacity == GlobalMeshCapacityResult.MigrationInProgress)
|
||||
{
|
||||
arenaBackpressured = true;
|
||||
break;
|
||||
}
|
||||
if (capacity == GlobalMeshCapacityResult.NeedsReclamation)
|
||||
{
|
||||
// Do not evict another batch while the frame-fence queue is
|
||||
// already returning ranges or retiring an old backing store.
|
||||
// Waiting for real allocator state prevents three frames of
|
||||
// duplicate eviction before the first release becomes reusable.
|
||||
if (globalBuffer?.HasPendingReclamation != true)
|
||||
{
|
||||
var reclaimed = meshManager.ReclaimUnusedResources(
|
||||
MaximumReclaimedMeshesPerFrame,
|
||||
MaximumReclaimedMeshBytesPerFrame,
|
||||
forceArenaReclamation: true);
|
||||
if (reclaimed.Count == 0)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
$"The live global-mesh working set cannot fit the supported "
|
||||
+ $"{GlobalMeshBuffer.MaximumVertexBufferBytes + GlobalMeshBuffer.MaximumIndexBufferBytes:N0}-byte arena "
|
||||
+ $"(blocked staging generation {next.Generation}, object 0x{next.Data.ObjectId:X10}).");
|
||||
}
|
||||
}
|
||||
arenaBackpressured = true;
|
||||
break;
|
||||
}
|
||||
|
||||
MeshUploadCost cost;
|
||||
try
|
||||
{
|
||||
cost = meshManager.PlanUploadCost(
|
||||
next.Data,
|
||||
_mipmapsBudgeted,
|
||||
next.Generation);
|
||||
if (!_uploadBudget.TryAdmit(cost))
|
||||
break;
|
||||
}
|
||||
catch (NotSupportedException error)
|
||||
{
|
||||
RejectUnsupportedHead(meshManager, next, error);
|
||||
throw;
|
||||
}
|
||||
|
||||
if (!meshManager.TryDequeueStagedMeshData(out MeshUploadQueueItem meshData))
|
||||
break;
|
||||
if (meshData.Generation != next.Generation)
|
||||
throw new InvalidOperationException("The staged mesh FIFO head changed during render-thread admission.");
|
||||
if (meshManager.UploadOrRequeue(meshData))
|
||||
(requeue ??= new()).Add(meshData);
|
||||
meshManager.AddDirtyAtlasesTo(_mipmapsBudgeted);
|
||||
}
|
||||
if (requeue is not null)
|
||||
foreach (var m in requeue)
|
||||
_meshManager.RequeueStagedMeshData(m);
|
||||
meshManager.RequeueStagedMeshData(m);
|
||||
meshManager.SetArenaBackpressure(arenaBackpressured);
|
||||
|
||||
bool texProbe = AcDream.Core.Rendering.RenderingDiagnostics.ProbeTexFlushEnabled;
|
||||
var pendingBefore = texProbe
|
||||
? _meshManager.GetPendingTextureUpdateStats()
|
||||
? meshManager.GetPendingTextureUpdateStats()
|
||||
: default;
|
||||
|
||||
// #105 root cause (2026-06-10): TextureAtlasManager.AddTexture only STAGES
|
||||
// texture content (PBO write + ManagedGLTextureArray._pendingUpdates) — the
|
||||
// immutable decoded payloads in ManagedGLTextureArray._pendingUpdates — the
|
||||
// actual TexSubImage3D copies + mipmap regeneration happen in
|
||||
// ProcessDirtyUpdates, which WB drives ONCE PER FRAME from its render loop
|
||||
// (WB GameScene.cs:975 `_meshManager?.GenerateMipmaps()`, just before the
|
||||
// opaque pass). That call site lived in the GameScene file the N.4/O-T4
|
||||
// extraction replaced with GameWindow, so the driver was silently dropped:
|
||||
// staged updates only ever reached the GPU as a side effect of PBO growth,
|
||||
// and every layer staged after an array's LAST growth kept undefined
|
||||
// TexStorage3D content behind a valid resident bindless handle — the
|
||||
// staged updates never reached TexSubImage3D, leaving undefined
|
||||
// TexStorage3D content behind valid resident bindless handles — the
|
||||
// intermittent white indoor walls (#105). Pre-fix evidence: 126 updates
|
||||
// stuck across 34/34 arrays at standstill (texflush-prefix.log). Tick()
|
||||
// runs before all draw passes (GameWindow OnRender), so this is the exact
|
||||
// WB-equivalent position.
|
||||
_meshManager.GenerateMipmaps();
|
||||
(LastMipmapArrayCount, LastMipmapBytes) = meshManager.GenerateMipmaps();
|
||||
if (globalBuffer?.HasPendingReclamation != true)
|
||||
{
|
||||
meshManager.ReclaimUnusedResources(
|
||||
MaximumReclaimedMeshesPerFrame,
|
||||
MaximumReclaimedMeshBytesPerFrame);
|
||||
}
|
||||
meshManager.EvictOneEmptyAtlas();
|
||||
|
||||
// Arena maintenance is capacity-driven and uses genuinely idle upload
|
||||
// frames. It never competes with destination materialization, and the
|
||||
// GlobalMeshBuffer policy retains enough hysteresis that portal-route
|
||||
// revisits do not alternate shrink/grow every frame.
|
||||
if (_uploadBudget.ObjectCount == 0
|
||||
&& LastMipmapArrayCount == 0
|
||||
&& meshManager.StagedMeshCount == 0
|
||||
&& globalBuffer?.IsMigrationInProgress == false
|
||||
&& globalBuffer.TryTrimUnusedTail(out GlobalMeshMaintenanceStep trim))
|
||||
{
|
||||
_uploadBudget.RecordBufferMaintenance(
|
||||
trim.AllocationBytes,
|
||||
trim.CopyBytes,
|
||||
trim.NewBufferCount);
|
||||
meshManager.SetArenaBackpressure(true);
|
||||
}
|
||||
|
||||
LastUploadCount = _uploadBudget.ObjectCount;
|
||||
LastUploadBytes = _uploadBudget.SourceBytes;
|
||||
LastStaleDiscardCount = staleDiscardCount;
|
||||
LastArrayAllocationBytes = _uploadBudget.ArrayAllocationBytes;
|
||||
LastPlannedMipmapBytes = _uploadBudget.MipmapBytes;
|
||||
LastNewArrayCount = _uploadBudget.NewArrayCount;
|
||||
LastBufferUploadBytes = _uploadBudget.BufferUploadBytes;
|
||||
LastBufferAllocationBytes = _uploadBudget.BufferAllocationBytes;
|
||||
LastBufferCopyBytes = _uploadBudget.BufferCopyBytes;
|
||||
LastNewBufferCount = _uploadBudget.NewBufferCount;
|
||||
|
||||
if (texProbe)
|
||||
EmitTexFlushProbe(pendingBefore);
|
||||
}
|
||||
|
||||
private static void RejectUnsupportedHead(
|
||||
ObjectMeshManager meshManager,
|
||||
MeshUploadQueueItem expected,
|
||||
NotSupportedException error)
|
||||
{
|
||||
if (!meshManager.TryDequeueStagedMeshData(out MeshUploadQueueItem rejected)
|
||||
|| rejected.Generation != expected.Generation)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The staged mesh FIFO head changed while rejecting an unsupported generation.",
|
||||
error);
|
||||
}
|
||||
meshManager.RejectUnsupportedStagedUpload(rejected, error);
|
||||
}
|
||||
|
||||
// #105 apparatus state — see RenderingDiagnostics.ProbeTexFlushEnabled.
|
||||
private int _lastTexFlushBefore = -1;
|
||||
private int _texFlushHeartbeat;
|
||||
|
|
@ -351,9 +601,46 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
_meshManager?.Dispose();
|
||||
_graphicsDevice?.Dispose();
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
// These are dependency edges, not merely a best-effort cleanup list.
|
||||
// In particular, the sampler objects owned by the device must remain
|
||||
// alive until every resident texture-array handle has been retired.
|
||||
_teardown ??= new AcDream.App.Rendering.OrderedResourceTeardown(
|
||||
() =>
|
||||
{
|
||||
// The current global arena is still directly owned by the mesh
|
||||
// manager. Fence every submitted draw before manager teardown
|
||||
// can delete that arena's VAO/VBO/IBO.
|
||||
if (_resourceRetirement is AcDream.App.Rendering.GpuFrameFlightController frameFlights)
|
||||
frameFlights.WaitForSubmittedWork();
|
||||
},
|
||||
() => _meshManager?.Dispose(),
|
||||
() => DrainGraphicsQueue("publishing mesh resource retirements"),
|
||||
() =>
|
||||
{
|
||||
if (_resourceRetirement is AcDream.App.Rendering.GpuFrameFlightController frameFlights)
|
||||
frameFlights.WaitForSubmittedWork();
|
||||
},
|
||||
() => DrainGraphicsQueue("releasing retired mesh resources"),
|
||||
() => _graphicsDevice?.Dispose(),
|
||||
() => DrainGraphicsQueue("deleting mesh graphics-device resources"));
|
||||
|
||||
_teardown.Advance();
|
||||
_disposed = _teardown.IsComplete;
|
||||
}
|
||||
|
||||
private void DrainGraphicsQueue(string operation)
|
||||
{
|
||||
if (_graphicsDevice is null)
|
||||
return;
|
||||
|
||||
_graphicsDevice.ProcessGLQueue();
|
||||
if (_graphicsDevice.HasPendingGLWork)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"OpenGL work remains pending after {operation}; retry adapter disposal to continue the exact stage.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue