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:
Erik 2026-07-18 21:35:16 +02:00
parent 3971997689
commit 749e8ceeb1
225 changed files with 29107 additions and 3914 deletions

View file

@ -0,0 +1,328 @@
using AcDream.Core.World;
namespace AcDream.App.Streaming;
[Flags]
public enum LandblockRetirementStage : ushort
{
None = 0,
MeshReferences = 1 << 0,
StaticScripts = 1 << 1,
Classification = 1 << 2,
EntityLighting = 1 << 3,
EntityTranslucency = 1 << 4,
PluginProjection = 1 << 5,
Terrain = 1 << 6,
Physics = 1 << 7,
CellVisibility = 1 << 8,
BuildingRegistry = 1 << 9,
EnvironmentCells = 1 << 10,
LegacyPresentation = 1 << 11,
}
/// <summary>
/// One retryable landblock-retirement ledger. Successful owner operations are
/// committed once; a later attempt resumes at the exact failed owner/entity.
/// </summary>
public sealed class LandblockRetirementTicket
{
private readonly Dictionary<LandblockRetirementStage, int> _entityCursors = new();
private readonly Dictionary<LandblockRetirementStage, Exception> _failures = new();
private Exception? _callbackFailure;
private Exception? _lastReportedFailure;
internal LandblockRetirementTicket(
GpuLandblockRetirement state,
LandblockRetirementStage requiredStages)
{
State = state;
RequiredStages = requiredStages;
}
public GpuLandblockRetirement State { get; }
public uint LandblockId => State.LandblockId;
public LandblockRetirementKind Kind => State.Kind;
public IReadOnlyList<WorldEntity> Entities => State.Entities;
public LandblockRetirementStage RequiredStages { get; }
public LandblockRetirementStage CompletedStages { get; private set; }
public bool IsComplete =>
(CompletedStages & RequiredStages) == RequiredStages
&& _callbackFailure is null;
public bool RunOnce(LandblockRetirementStage stage, Action operation)
{
ValidateSingleStage(stage);
ArgumentNullException.ThrowIfNull(operation);
if ((CompletedStages & stage) != 0)
return true;
try
{
operation();
CompletedStages |= stage;
_failures.Remove(stage);
return true;
}
catch (Exception error)
{
_failures[stage] = error;
return false;
}
}
public bool RunForEachEntity(
LandblockRetirementStage stage,
Func<WorldEntity, bool> predicate,
Action<WorldEntity> operation)
{
ValidateSingleStage(stage);
ArgumentNullException.ThrowIfNull(predicate);
ArgumentNullException.ThrowIfNull(operation);
if ((CompletedStages & stage) != 0)
return true;
_entityCursors.TryGetValue(stage, out int cursor);
while (cursor < Entities.Count)
{
WorldEntity entity = Entities[cursor];
if (predicate(entity))
{
try
{
operation(entity);
}
catch (Exception error)
{
_entityCursors[stage] = cursor;
_failures[stage] = error;
return false;
}
}
cursor++;
_entityCursors[stage] = cursor;
}
CompletedStages |= stage;
_entityCursors.Remove(stage);
_failures.Remove(stage);
return true;
}
internal void BeginAttempt() => _callbackFailure = null;
internal void RecordCallbackFailure(Exception error) =>
_callbackFailure = error;
internal bool TryTakeNewFailure(out Exception? error)
{
error = _callbackFailure ?? _failures.Values.FirstOrDefault();
if (error is null || ReferenceEquals(error, _lastReportedFailure))
return false;
_lastReportedFailure = error;
return true;
}
private static void ValidateSingleStage(LandblockRetirementStage stage)
{
ushort value = (ushort)stage;
if (value == 0 || (value & (value - 1)) != 0)
throw new ArgumentOutOfRangeException(
nameof(stage),
stage,
"A retirement operation must own exactly one stage bit.");
}
}
/// <summary>
/// Game-window-lifetime owner for landblock retirement. World-state detachment
/// commits first; renderer/cache/script cleanup advances afterward from a
/// per-owner ledger. The coordinator is intentionally shared by replacement
/// <see cref="StreamingController"/> instances so a quality-setting rebuild
/// cannot abandon an unfinished retirement.
/// </summary>
public sealed class LandblockRetirementCoordinator
{
private const LandblockRetirementStage CoreStages =
LandblockRetirementStage.MeshReferences
| LandblockRetirementStage.StaticScripts
| LandblockRetirementStage.Classification;
public const LandblockRetirementStage ProductionPresentationStages =
LandblockRetirementStage.EntityLighting
| LandblockRetirementStage.EntityTranslucency
| LandblockRetirementStage.PluginProjection
| LandblockRetirementStage.Terrain
| LandblockRetirementStage.Physics
| LandblockRetirementStage.CellVisibility
| LandblockRetirementStage.BuildingRegistry
| LandblockRetirementStage.EnvironmentCells;
private readonly GpuWorldState _state;
private readonly Action<LandblockRetirementTicket> _advancePresentation;
private readonly Func<LandblockRetirementKind, LandblockRetirementStage>
_requiredPresentationStages;
private readonly Dictionary<uint, List<LandblockRetirementTicket>> _pending = new();
private readonly List<uint> _completedIds = new();
public LandblockRetirementCoordinator(
GpuWorldState state,
Action<LandblockRetirementTicket> advancePresentation,
Func<LandblockRetirementKind, LandblockRetirementStage>? requiredPresentationStages = null)
{
ArgumentNullException.ThrowIfNull(state);
ArgumentNullException.ThrowIfNull(advancePresentation);
_state = state;
_advancePresentation = advancePresentation;
_requiredPresentationStages = requiredPresentationStages
?? (kind => kind == LandblockRetirementKind.Full
? ProductionPresentationStages
: ProductionPresentationStages & ~LandblockRetirementStage.Terrain);
}
public int PendingCount { get; private set; }
public bool IsPending(uint landblockId) =>
_pending.ContainsKey(Canonicalize(landblockId));
public void BeginFull(uint landblockId) =>
Begin(landblockId, LandblockRetirementKind.Full);
public void BeginNearLayer(uint landblockId) =>
Begin(landblockId, LandblockRetirementKind.NearLayer);
public void Advance()
{
if (_pending.Count == 0)
return;
_completedIds.Clear();
foreach ((uint id, List<LandblockRetirementTicket> tickets) in _pending)
{
for (int i = tickets.Count - 1; i >= 0; i--)
{
LandblockRetirementTicket ticket = tickets[i];
AdvanceTicket(ticket);
if (!ticket.IsComplete)
continue;
tickets.RemoveAt(i);
PendingCount--;
}
if (tickets.Count == 0)
_completedIds.Add(id);
}
for (int i = 0; i < _completedIds.Count; i++)
_pending.Remove(_completedIds[i]);
}
internal static LandblockRetirementCoordinator CreateLegacy(
GpuWorldState state,
Action<uint>? removeTerrain,
Action<uint>? demoteNearLayer)
{
LandblockRetirementStage Required(LandblockRetirementKind kind) =>
kind switch
{
LandblockRetirementKind.Full when removeTerrain is not null =>
LandblockRetirementStage.LegacyPresentation,
LandblockRetirementKind.NearLayer when demoteNearLayer is not null =>
LandblockRetirementStage.LegacyPresentation,
_ => LandblockRetirementStage.None,
};
void AdvanceLegacy(LandblockRetirementTicket ticket)
{
Action<uint>? callback = ticket.Kind == LandblockRetirementKind.Full
? removeTerrain
: demoteNearLayer;
if (callback is not null)
{
ticket.RunOnce(
LandblockRetirementStage.LegacyPresentation,
() => callback(ticket.LandblockId));
}
}
return new LandblockRetirementCoordinator(state, AdvanceLegacy, Required);
}
private void Begin(uint landblockId, LandblockRetirementKind kind)
{
uint canonical = Canonicalize(landblockId);
if (_pending.TryGetValue(canonical, out List<LandblockRetirementTicket>? existing))
{
for (int i = 0; i < existing.Count; i++)
{
if (existing[i].Kind == kind)
{
AdvanceTicket(existing[i]);
return;
}
}
}
GpuLandblockRetirement? stateRetirement = kind == LandblockRetirementKind.Full
? _state.DetachLandblock(canonical)
: _state.DetachNearLayer(canonical);
if (stateRetirement is null)
return;
LandblockRetirementStage required =
CoreStages | _requiredPresentationStages(kind);
var ticket = new LandblockRetirementTicket(stateRetirement, required);
if (existing is null)
{
existing = new List<LandblockRetirementTicket>(1);
_pending.Add(canonical, existing);
}
existing.Add(ticket);
PendingCount++;
AdvanceTicket(ticket);
if (ticket.IsComplete)
{
existing.Remove(ticket);
PendingCount--;
if (existing.Count == 0)
_pending.Remove(canonical);
}
}
private void AdvanceTicket(LandblockRetirementTicket ticket)
{
ticket.BeginAttempt();
ticket.RunOnce(
LandblockRetirementStage.MeshReferences,
() => _state.ReleaseLandblockMeshReferences(ticket.LandblockId));
ticket.RunForEachEntity(
LandblockRetirementStage.StaticScripts,
static entity => entity.ServerGuid == 0,
_state.StopStaticEntityScript);
ticket.RunOnce(
LandblockRetirementStage.Classification,
() => _state.InvalidateLandblockClassification(ticket.LandblockId));
try
{
_advancePresentation(ticket);
}
catch (Exception error)
{
ticket.RecordCallbackFailure(error);
}
if (ticket.TryTakeNewFailure(out Exception? failure))
{
Console.WriteLine(
$"streaming: retirement for 0x{ticket.LandblockId:X8} " +
$"({ticket.Kind}) remains pending: {failure}");
}
}
private static uint Canonicalize(uint id) =>
(id & 0xFFFF0000u) | 0xFFFFu;
}