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
328
src/AcDream.App/Streaming/LandblockRetirementCoordinator.cs
Normal file
328
src/AcDream.App/Streaming/LandblockRetirementCoordinator.cs
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue