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
|
|
@ -9,20 +9,24 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// entities (procedural / dat-hydrated, identified by
|
||||
/// <c>ServerGuid == 0</c>) drive ref counts. Server-spawned entities
|
||||
/// (per-instance tier) are skipped — those go through
|
||||
/// <c>EntitySpawnAdapter</c> + <c>TextureCache.GetOrUploadWithPaletteOverride</c>
|
||||
/// <c>EntitySpawnAdapter</c> and the owner-scoped texture path
|
||||
/// (see Phase N.4 spec, Architecture → Two-tier rendering split).
|
||||
///
|
||||
/// <para>
|
||||
/// On load: walks the landblock's atlas-tier entities, collects unique
|
||||
/// GfxObj ids from their <c>MeshRefs</c>, calls
|
||||
/// <c>IncrementRefCount</c> per id, and pins each specialized EnvCell geometry
|
||||
/// id without starting generic GfxObj decode. Snapshots both id-sets per
|
||||
/// landblock so unload can match the load 1:1.
|
||||
/// id without starting generic GfxObj decode. Each reference has an explicit
|
||||
/// desired/held marker so a throwing backend cannot make the logical snapshot
|
||||
/// disagree with the physical reference count.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// On unload: looks up both snapshots, calls <c>DecrementRefCount</c> per id,
|
||||
/// drops the snapshots. Unknown / never-loaded landblocks no-op.
|
||||
/// On unload: releases only references whose held marker is still set. A
|
||||
/// before-commit failure remains retryable; an after-commit
|
||||
/// <see cref="MeshReferenceMutationException"/> advances the marker before the
|
||||
/// exception is propagated. The registration is dropped only after every held
|
||||
/// reference has been released. Unknown / never-loaded landblocks no-op.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
|
|
@ -42,15 +46,21 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// </summary>
|
||||
public sealed class LandblockSpawnAdapter
|
||||
{
|
||||
private readonly IWbMeshAdapter _adapter;
|
||||
private sealed class ReferenceRegistration
|
||||
{
|
||||
public bool Desired;
|
||||
public bool Held;
|
||||
}
|
||||
|
||||
// Maps landblock id → unique GfxObj ids registered for that landblock.
|
||||
// Written on load, read+cleared on unload. Single-threaded (streaming worker).
|
||||
private readonly Dictionary<uint, HashSet<ulong>> _idsByLandblock = new();
|
||||
// EnvCell shells are prepared through PrepareEnvCellGeomMeshDataAsync rather
|
||||
// than generic GfxObj loading, but still require explicit lifetime pins.
|
||||
// Keep their synthetic ids separate so registration uses the no-decode pin.
|
||||
private readonly Dictionary<uint, HashSet<ulong>> _additionalReadinessIdsByLandblock = new();
|
||||
private sealed class LandblockRegistration
|
||||
{
|
||||
public bool WantsLoaded;
|
||||
public Dictionary<ulong, ReferenceRegistration> Ordinary { get; } = new();
|
||||
public Dictionary<ulong, ReferenceRegistration> Prepared { get; } = new();
|
||||
}
|
||||
|
||||
private readonly IWbMeshAdapter _adapter;
|
||||
private readonly Dictionary<uint, LandblockRegistration> _registrations = new();
|
||||
|
||||
public LandblockSpawnAdapter(IWbMeshAdapter adapter)
|
||||
{
|
||||
|
|
@ -81,33 +91,40 @@ public sealed class LandblockSpawnAdapter
|
|||
unique.Add((ulong)meshRef.GfxObjId);
|
||||
}
|
||||
|
||||
if (!_idsByLandblock.TryGetValue(landblock.LandblockId, out var registered))
|
||||
HashSet<ulong>? preparedIds = additionalReadinessIds is null
|
||||
? null
|
||||
: new HashSet<ulong>(additionalReadinessIds);
|
||||
|
||||
if (!_registrations.TryGetValue(landblock.LandblockId, out var registration))
|
||||
{
|
||||
_idsByLandblock[landblock.LandblockId] = unique;
|
||||
foreach (var id in unique) _adapter.IncrementRefCount(id);
|
||||
registration = new LandblockRegistration { WantsLoaded = true };
|
||||
_registrations.Add(landblock.LandblockId, registration);
|
||||
}
|
||||
else
|
||||
else if (!registration.WantsLoaded)
|
||||
{
|
||||
foreach (var id in unique)
|
||||
{
|
||||
if (registered.Add(id))
|
||||
_adapter.IncrementRefCount(id);
|
||||
}
|
||||
// This is a new load edge that arrived while a preceding unload
|
||||
// still had unfinished releases. The new snapshot replaces the old
|
||||
// desired set. Any still-held overlap remains acquired; obsolete
|
||||
// residual references are released by Reconcile below.
|
||||
MarkAllUndesired(registration.Ordinary);
|
||||
MarkAllUndesired(registration.Prepared);
|
||||
registration.WantsLoaded = true;
|
||||
}
|
||||
|
||||
if (!_additionalReadinessIdsByLandblock.TryGetValue(
|
||||
landblock.LandblockId,
|
||||
out var additional))
|
||||
{
|
||||
additional = new HashSet<ulong>();
|
||||
_additionalReadinessIdsByLandblock[landblock.LandblockId] = additional;
|
||||
}
|
||||
if (additionalReadinessIds is not null)
|
||||
{
|
||||
foreach (var id in additionalReadinessIds)
|
||||
if (additional.Add(id))
|
||||
_adapter.PinPreparedRenderData(id);
|
||||
}
|
||||
MarkDesired(registration.Ordinary, unique);
|
||||
if (preparedIds is not null)
|
||||
MarkDesired(registration.Prepared, preparedIds);
|
||||
|
||||
List<Exception>? failures = null;
|
||||
ReleaseUndesired(registration.Ordinary, ref failures);
|
||||
ReleaseUndesired(registration.Prepared, ref failures);
|
||||
PruneReleasedUndesired(registration.Ordinary);
|
||||
PruneReleasedUndesired(registration.Prepared);
|
||||
AcquireDesired(registration.Ordinary, prepared: false, ref failures);
|
||||
AcquireDesired(registration.Prepared, prepared: true, ref failures);
|
||||
ThrowFailures(
|
||||
failures,
|
||||
$"Landblock 0x{landblock.LandblockId:X8} mesh-reference acquisition did not fully converge.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -117,17 +134,19 @@ public sealed class LandblockSpawnAdapter
|
|||
/// </summary>
|
||||
public bool IsLandblockRenderReady(uint landblockId)
|
||||
{
|
||||
if (!_idsByLandblock.TryGetValue(landblockId, out var registered)
|
||||
|| !_additionalReadinessIdsByLandblock.TryGetValue(landblockId, out var additional))
|
||||
{
|
||||
if (!_registrations.TryGetValue(landblockId, out var registration)
|
||||
|| !registration.WantsLoaded)
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var id in registered)
|
||||
if (!_adapter.IsRenderDataReady(id))
|
||||
foreach (var pair in registration.Ordinary)
|
||||
if (!pair.Value.Desired
|
||||
|| !pair.Value.Held
|
||||
|| !_adapter.IsRenderDataReady(pair.Key))
|
||||
return false;
|
||||
foreach (var id in additional)
|
||||
if (!_adapter.IsRenderDataReady(id))
|
||||
foreach (var pair in registration.Prepared)
|
||||
if (!pair.Value.Desired
|
||||
|| !pair.Value.Held
|
||||
|| !_adapter.IsRenderDataReady(pair.Key))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -139,11 +158,127 @@ public sealed class LandblockSpawnAdapter
|
|||
/// </summary>
|
||||
public void OnLandblockUnloaded(uint landblockId)
|
||||
{
|
||||
if (!_idsByLandblock.TryGetValue(landblockId, out var unique)) return;
|
||||
foreach (var id in unique) _adapter.DecrementRefCount(id);
|
||||
if (_additionalReadinessIdsByLandblock.TryGetValue(landblockId, out var additional))
|
||||
foreach (var id in additional) _adapter.DecrementRefCount(id);
|
||||
_idsByLandblock.Remove(landblockId);
|
||||
_additionalReadinessIdsByLandblock.Remove(landblockId);
|
||||
if (!_registrations.TryGetValue(landblockId, out var registration))
|
||||
return;
|
||||
|
||||
registration.WantsLoaded = false;
|
||||
MarkAllUndesired(registration.Ordinary);
|
||||
MarkAllUndesired(registration.Prepared);
|
||||
|
||||
List<Exception>? failures = null;
|
||||
ReleaseUndesired(registration.Ordinary, ref failures);
|
||||
ReleaseUndesired(registration.Prepared, ref failures);
|
||||
PruneReleasedUndesired(registration.Ordinary);
|
||||
PruneReleasedUndesired(registration.Prepared);
|
||||
|
||||
// Even an after-commit backend exception may report failure after the
|
||||
// final physical release. Drop the registration before propagating in
|
||||
// that case so a caller retry cannot decrement the same reference.
|
||||
if (registration.Ordinary.Count == 0 && registration.Prepared.Count == 0)
|
||||
_registrations.Remove(landblockId);
|
||||
|
||||
ThrowFailures(
|
||||
failures,
|
||||
$"Landblock 0x{landblockId:X8} mesh-reference release did not fully converge.");
|
||||
}
|
||||
|
||||
private static void MarkDesired(
|
||||
Dictionary<ulong, ReferenceRegistration> registrations,
|
||||
IEnumerable<ulong> ids)
|
||||
{
|
||||
foreach (ulong id in ids)
|
||||
{
|
||||
if (!registrations.TryGetValue(id, out var reference))
|
||||
{
|
||||
reference = new ReferenceRegistration();
|
||||
registrations.Add(id, reference);
|
||||
}
|
||||
|
||||
reference.Desired = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void MarkAllUndesired(
|
||||
Dictionary<ulong, ReferenceRegistration> registrations)
|
||||
{
|
||||
foreach (var reference in registrations.Values)
|
||||
reference.Desired = false;
|
||||
}
|
||||
|
||||
private void AcquireDesired(
|
||||
Dictionary<ulong, ReferenceRegistration> registrations,
|
||||
bool prepared,
|
||||
ref List<Exception>? failures)
|
||||
{
|
||||
foreach (var pair in registrations)
|
||||
{
|
||||
ReferenceRegistration reference = pair.Value;
|
||||
if (!reference.Desired || reference.Held)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
if (prepared)
|
||||
_adapter.PinPreparedRenderData(pair.Key);
|
||||
else
|
||||
_adapter.IncrementRefCount(pair.Key);
|
||||
reference.Held = true;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
if (error is MeshReferenceMutationException { MutationCommitted: true })
|
||||
reference.Held = true;
|
||||
(failures ??= new List<Exception>()).Add(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ReleaseUndesired(
|
||||
Dictionary<ulong, ReferenceRegistration> registrations,
|
||||
ref List<Exception>? failures)
|
||||
{
|
||||
foreach (var pair in registrations)
|
||||
{
|
||||
ReferenceRegistration reference = pair.Value;
|
||||
if (reference.Desired || !reference.Held)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
_adapter.DecrementRefCount(pair.Key);
|
||||
reference.Held = false;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
if (error is MeshReferenceMutationException { MutationCommitted: true })
|
||||
reference.Held = false;
|
||||
(failures ??= new List<Exception>()).Add(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void PruneReleasedUndesired(
|
||||
Dictionary<ulong, ReferenceRegistration> registrations)
|
||||
{
|
||||
List<ulong>? released = null;
|
||||
foreach (var pair in registrations)
|
||||
{
|
||||
if (!pair.Value.Desired && !pair.Value.Held)
|
||||
(released ??= new List<ulong>()).Add(pair.Key);
|
||||
}
|
||||
|
||||
if (released is null)
|
||||
return;
|
||||
foreach (ulong id in released)
|
||||
registrations.Remove(id);
|
||||
}
|
||||
|
||||
private static void ThrowFailures(List<Exception>? failures, string message)
|
||||
{
|
||||
if (failures is null)
|
||||
return;
|
||||
if (failures.Count == 1)
|
||||
throw failures[0];
|
||||
throw new AggregateException(message, failures);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue