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
|
|
@ -22,9 +22,8 @@ public sealed class StreamingController
|
|||
private readonly Action<uint, ulong> _enqueueUnload;
|
||||
private readonly Func<int, IReadOnlyList<LandblockStreamResult>> _drainCompletions;
|
||||
private readonly Action<LandblockBuild, LandblockMeshData> _applyTerrain;
|
||||
private readonly Action<uint>? _removeTerrain;
|
||||
private readonly Action<uint>? _demoteNearLayer;
|
||||
private readonly Action? _clearPendingLoads;
|
||||
private readonly LandblockRetirementCoordinator _retirements;
|
||||
|
||||
/// <summary>
|
||||
/// #138: fired after a landblock's entity layer (re)loads — both the full
|
||||
|
|
@ -42,6 +41,9 @@ public sealed class StreamingController
|
|||
private readonly Action<EnvCellLandblockBuild>? _ensureEnvCellMeshes;
|
||||
private readonly GpuWorldState _state;
|
||||
private StreamingRegion? _region;
|
||||
private RadiiReconfiguration? _pendingRadiiReconfiguration;
|
||||
private bool _advancingRadiiReconfiguration;
|
||||
private (int NearRadius, int FarRadius)? _deferredRadiiRequest;
|
||||
// Hard streaming boundaries advance this token. Worker completions carry
|
||||
// the generation captured at enqueue time, so an old overlapping load or
|
||||
// unload cannot mutate the replacement window after a portal recenter.
|
||||
|
|
@ -63,23 +65,15 @@ public sealed class StreamingController
|
|||
|
||||
/// <summary>
|
||||
/// Near-tier radius (LBs from observer that load full detail: terrain +
|
||||
/// scenery + entities). Set at construction; readable thereafter.
|
||||
/// scenery + entities). Runtime changes go through
|
||||
/// <see cref="ReconfigureRadii"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Mutating after the first <see cref="Tick"/> has no effect — the
|
||||
/// internal <see cref="StreamingRegion"/> snapshots both radii on its
|
||||
/// constructor. Treat as init-only post-Tick.
|
||||
/// </remarks>
|
||||
public int NearRadius { get; }
|
||||
public int NearRadius { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Far-tier radius (LBs from observer that load terrain only). Set at
|
||||
/// construction; readable thereafter.
|
||||
/// Far-tier radius (LBs from observer that load terrain only).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Mutating after the first <see cref="Tick"/> has no effect — see <see cref="NearRadius"/>.
|
||||
/// </remarks>
|
||||
public int FarRadius { get; }
|
||||
public int FarRadius { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cap on completions drained per <see cref="Tick"/> call. The cap is
|
||||
|
|
@ -194,22 +188,122 @@ public sealed class StreamingController
|
|||
Action<uint>? demoteNearLayer = null,
|
||||
Action? clearPendingLoads = null,
|
||||
Action<uint>? onLandblockLoaded = null,
|
||||
Action<EnvCellLandblockBuild>? ensureEnvCellMeshes = null)
|
||||
Action<EnvCellLandblockBuild>? ensureEnvCellMeshes = null,
|
||||
LandblockRetirementCoordinator? retirementCoordinator = null)
|
||||
{
|
||||
_enqueueLoad = enqueueLoad;
|
||||
_enqueueUnload = enqueueUnload;
|
||||
_drainCompletions = drainCompletions;
|
||||
_applyTerrain = applyTerrain;
|
||||
_removeTerrain = removeTerrain;
|
||||
_demoteNearLayer = demoteNearLayer;
|
||||
_clearPendingLoads = clearPendingLoads;
|
||||
_onLandblockLoaded = onLandblockLoaded;
|
||||
_ensureEnvCellMeshes = ensureEnvCellMeshes;
|
||||
_state = state;
|
||||
_retirements = retirementCoordinator
|
||||
?? LandblockRetirementCoordinator.CreateLegacy(
|
||||
state,
|
||||
removeTerrain,
|
||||
demoteNearLayer);
|
||||
NearRadius = nearRadius;
|
||||
FarRadius = farRadius;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reconciles an active outdoor streaming window to new quality radii
|
||||
/// without replacing the worker/controller or blindly bootstrapping data
|
||||
/// already published in <see cref="GpuWorldState"/>. Pending jobs from the
|
||||
/// old window are invalidated by generation; resident blocks are retained,
|
||||
/// promoted, demoted, loaded, or unloaded from their actual current tier.
|
||||
/// A sealed dungeon remains radius-zero until its normal exit edge while
|
||||
/// remembering the new outdoor radii for that exit.
|
||||
/// </summary>
|
||||
public void ReconfigureRadii(int nearRadius, int farRadius)
|
||||
{
|
||||
if (nearRadius < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(nearRadius));
|
||||
if (farRadius < nearRadius)
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(farRadius),
|
||||
"Far radius must be greater than or equal to near radius.");
|
||||
|
||||
// Queue callbacks are injected seams and can synchronously reenter the
|
||||
// controller. Last-request-wins deferral keeps the active mutation
|
||||
// cursor stable; the request is applied after the current transaction
|
||||
// converges instead of recursively replaying its active step.
|
||||
if (_advancingRadiiReconfiguration)
|
||||
{
|
||||
_deferredRadiiRequest = (nearRadius, farRadius);
|
||||
return;
|
||||
}
|
||||
|
||||
// A prior callback may have failed after this controller accepted the
|
||||
// request. Resume that exact transaction before considering another
|
||||
// target; replacing it would orphan already-admitted generation work.
|
||||
if (_pendingRadiiReconfiguration is not null)
|
||||
AdvanceRadiiReconfiguration();
|
||||
|
||||
// This explicit call is newer than any request deferred by a prior
|
||||
// failed attempt and therefore supersedes it.
|
||||
_deferredRadiiRequest = null;
|
||||
|
||||
if (nearRadius == NearRadius && farRadius == FarRadius)
|
||||
return;
|
||||
|
||||
if (_collapsed || _region is null)
|
||||
{
|
||||
NearRadius = nearRadius;
|
||||
FarRadius = farRadius;
|
||||
return;
|
||||
}
|
||||
|
||||
var rebuilt = new StreamingRegion(
|
||||
_region.CenterX,
|
||||
_region.CenterY,
|
||||
nearRadius,
|
||||
farRadius);
|
||||
TwoTierDiff bootstrap = rebuilt.ComputeFirstTickDiff();
|
||||
var desiredNear = new HashSet<uint>(bootstrap.ToLoadNear);
|
||||
var desiredFar = new HashSet<uint>(bootstrap.ToLoadFar);
|
||||
var mutations = new List<RadiiMutation>();
|
||||
|
||||
uint[] loaded = [.. _state.LoadedLandblockIds];
|
||||
for (int i = 0; i < loaded.Length; i++)
|
||||
{
|
||||
uint id = loaded[i];
|
||||
if (desiredNear.Contains(id))
|
||||
{
|
||||
if (!_state.IsNearTier(id))
|
||||
mutations.Add(new RadiiMutation(
|
||||
() => EnqueueLoad(id, LandblockStreamJobKind.PromoteToNear)));
|
||||
}
|
||||
else if (desiredFar.Contains(id))
|
||||
{
|
||||
if (_state.IsNearTier(id))
|
||||
mutations.Add(new RadiiMutation(() => DemoteLandblock(id)));
|
||||
}
|
||||
else
|
||||
{
|
||||
mutations.Add(new RadiiMutation(() => EnqueueUnload(id)));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (uint id in desiredNear)
|
||||
if (!_state.IsLoaded(id))
|
||||
mutations.Add(new RadiiMutation(
|
||||
() => EnqueueLoad(id, LandblockStreamJobKind.LoadNear)));
|
||||
foreach (uint id in desiredFar)
|
||||
if (!_state.IsLoaded(id))
|
||||
mutations.Add(new RadiiMutation(
|
||||
() => EnqueueLoad(id, LandblockStreamJobKind.LoadFar)));
|
||||
|
||||
_pendingRadiiReconfiguration = new RadiiReconfiguration(
|
||||
nearRadius,
|
||||
farRadius,
|
||||
rebuilt,
|
||||
mutations);
|
||||
AdvanceRadiiReconfiguration();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compatibility constructor for deterministic tests and callers that do
|
||||
/// not own an asynchronous worker. Production must use the generation-aware
|
||||
|
|
@ -227,7 +321,8 @@ public sealed class StreamingController
|
|||
Action<uint>? demoteNearLayer = null,
|
||||
Action? clearPendingLoads = null,
|
||||
Action<uint>? onLandblockLoaded = null,
|
||||
Action<EnvCellLandblockBuild>? ensureEnvCellMeshes = null)
|
||||
Action<EnvCellLandblockBuild>? ensureEnvCellMeshes = null,
|
||||
LandblockRetirementCoordinator? retirementCoordinator = null)
|
||||
: this(
|
||||
(id, kind, _) => enqueueLoad(id, kind),
|
||||
(id, _) => enqueueUnload(id),
|
||||
|
|
@ -240,7 +335,8 @@ public sealed class StreamingController
|
|||
demoteNearLayer,
|
||||
clearPendingLoads,
|
||||
onLandblockLoaded,
|
||||
ensureEnvCellMeshes)
|
||||
ensureEnvCellMeshes,
|
||||
retirementCoordinator)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -252,11 +348,7 @@ public sealed class StreamingController
|
|||
private void DemoteLandblock(uint id)
|
||||
{
|
||||
uint canonical = (id & 0xFFFF0000u) | 0xFFFFu;
|
||||
// App-owned Near resources need the still-present static entity list
|
||||
// for exact light/translucency owner cleanup. Retire them before
|
||||
// GpuWorldState drops the static layer and its render pins.
|
||||
_demoteNearLayer?.Invoke(canonical);
|
||||
_state.RemoveEntitiesFromLandblock(canonical);
|
||||
_retirements.BeginNearLayer(canonical);
|
||||
}
|
||||
|
||||
private void AdvanceGeneration()
|
||||
|
|
@ -280,6 +372,18 @@ public sealed class StreamingController
|
|||
/// </summary>
|
||||
public void Tick(int observerCx, int observerCy, bool insideDungeon = false)
|
||||
{
|
||||
if (_pendingRadiiReconfiguration is not null)
|
||||
AdvanceRadiiReconfiguration();
|
||||
else if (_deferredRadiiRequest is { } deferred)
|
||||
{
|
||||
_deferredRadiiRequest = null;
|
||||
ReconfigureRadii(deferred.NearRadius, deferred.FarRadius);
|
||||
}
|
||||
|
||||
// Presentation-owner failures never roll back spatial state. Resume
|
||||
// unfinished owner ledgers before accepting replacement publications.
|
||||
_retirements.Advance();
|
||||
|
||||
uint centerId = StreamingRegion.EncodeLandblockId(observerCx, observerCy);
|
||||
|
||||
if (_collapsed)
|
||||
|
|
@ -313,6 +417,125 @@ public sealed class StreamingController
|
|||
DrainAndApply();
|
||||
}
|
||||
|
||||
private void AdvanceRadiiReconfiguration()
|
||||
{
|
||||
if (_advancingRadiiReconfiguration)
|
||||
return;
|
||||
|
||||
RadiiReconfiguration pending = _pendingRadiiReconfiguration
|
||||
?? throw new InvalidOperationException("No radii reconfiguration is pending.");
|
||||
var failures = new List<Exception>();
|
||||
_advancingRadiiReconfiguration = true;
|
||||
try
|
||||
{
|
||||
|
||||
if (!pending.GenerationAdvanced)
|
||||
{
|
||||
AdvanceGeneration();
|
||||
pending.GenerationAdvanced = true;
|
||||
}
|
||||
|
||||
if (!pending.PendingLoadsCleared)
|
||||
{
|
||||
try
|
||||
{
|
||||
_clearPendingLoads?.Invoke();
|
||||
pending.PendingLoadsCleared = true;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
if (error is StreamingMutationException { MutationCommitted: true })
|
||||
pending.PendingLoadsCleared = true;
|
||||
failures.Add(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Do not admit new-generation work until cancellation of the old
|
||||
// inbox is durable. Otherwise a successful retry of ClearPendingLoads
|
||||
// would also erase mutations already marked complete by this ledger.
|
||||
for (int i = 0; pending.PendingLoadsCleared && i < pending.Mutations.Count; i++)
|
||||
{
|
||||
RadiiMutation mutation = pending.Mutations[i];
|
||||
if (mutation.Completed)
|
||||
continue;
|
||||
try
|
||||
{
|
||||
mutation.Apply();
|
||||
mutation.Completed = true;
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
if (error is StreamingMutationException { MutationCommitted: true })
|
||||
mutation.Completed = true;
|
||||
failures.Add(error);
|
||||
}
|
||||
}
|
||||
|
||||
bool converged = pending.PendingLoadsCleared
|
||||
&& pending.Mutations.All(static mutation => mutation.Completed);
|
||||
if (converged)
|
||||
{
|
||||
// Publish the new logical window only after every queue/retirement
|
||||
// admission is durable. A later Tick can now trust Resident as the
|
||||
// complete desired set and will never strand a hole.
|
||||
pending.Region.MarkResidentFromBootstrap();
|
||||
_region = pending.Region;
|
||||
NearRadius = pending.NearRadius;
|
||||
FarRadius = pending.FarRadius;
|
||||
_deferredApply.Clear();
|
||||
_pendingRadiiReconfiguration = null;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_advancingRadiiReconfiguration = false;
|
||||
}
|
||||
|
||||
if (failures.Count != 0)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"Streaming quality reconfiguration did not complete cleanly.",
|
||||
failures);
|
||||
}
|
||||
|
||||
if (_pendingRadiiReconfiguration is null
|
||||
&& _deferredRadiiRequest is { } deferred)
|
||||
{
|
||||
_deferredRadiiRequest = null;
|
||||
ReconfigureRadii(deferred.NearRadius, deferred.FarRadius);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RadiiReconfiguration
|
||||
{
|
||||
public RadiiReconfiguration(
|
||||
int nearRadius,
|
||||
int farRadius,
|
||||
StreamingRegion region,
|
||||
List<RadiiMutation> mutations)
|
||||
{
|
||||
NearRadius = nearRadius;
|
||||
FarRadius = farRadius;
|
||||
Region = region;
|
||||
Mutations = mutations;
|
||||
}
|
||||
|
||||
public int NearRadius { get; }
|
||||
public int FarRadius { get; }
|
||||
public StreamingRegion Region { get; }
|
||||
public List<RadiiMutation> Mutations { get; }
|
||||
public bool GenerationAdvanced { get; set; }
|
||||
public bool PendingLoadsCleared { get; set; }
|
||||
}
|
||||
|
||||
private sealed class RadiiMutation
|
||||
{
|
||||
public RadiiMutation(Action apply) => Apply = apply;
|
||||
|
||||
public Action Apply { get; }
|
||||
public bool Completed { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #135: collapse to a single dungeon landblock IMMEDIATELY, before the first
|
||||
/// <see cref="Tick"/> has a chance to bootstrap the full 25×25 window. Called
|
||||
|
|
@ -400,6 +623,7 @@ public sealed class StreamingController
|
|||
AdvanceGeneration();
|
||||
_clearPendingLoads?.Invoke();
|
||||
_deferredApply.Clear();
|
||||
_region = null;
|
||||
|
||||
foreach (var id in _state.LoadedLandblockIds)
|
||||
if (id != centerId) EnqueueUnload(id);
|
||||
|
|
@ -495,16 +719,15 @@ public sealed class StreamingController
|
|||
AdvanceGeneration();
|
||||
_clearPendingLoads?.Invoke();
|
||||
_deferredApply.Clear();
|
||||
// Commit the old region boundary before any presentation owner is
|
||||
// advanced. New same-id publications are fenced by _retirements.
|
||||
_region = null;
|
||||
// Snapshot — RemoveLandblock mutates the loaded set we're iterating.
|
||||
var ids = new List<uint>(_state.LoadedLandblockIds);
|
||||
ForceReloadCount++; // [FRAME-DIAG] churn counter
|
||||
LastForceReloadDropCount = ids.Count; // = upcoming re-upload volume
|
||||
foreach (var id in ids)
|
||||
{
|
||||
_state.RemoveLandblock(id);
|
||||
_removeTerrain?.Invoke(id); // frees the render slot + physics + cell registries
|
||||
}
|
||||
_region = null; // NormalTick re-creates + bootstraps the full window next frame
|
||||
_retirements.BeginFull(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -541,7 +764,9 @@ public sealed class StreamingController
|
|||
// for direct callers and future drain paths.
|
||||
if (IsStaleGeneration(result))
|
||||
continue;
|
||||
if (result is LandblockStreamResult.Unloaded
|
||||
if (IsPublicationBlockedByRetirement(result))
|
||||
_deferredApply.Add(result);
|
||||
else if (result is LandblockStreamResult.Unloaded
|
||||
|| IsWithinPriorityRing(ResultLandblockId(result)))
|
||||
ApplyResult(result); // free (unload) or behind-the-fade near ring
|
||||
else
|
||||
|
|
@ -553,9 +778,39 @@ public sealed class StreamingController
|
|||
// earlier-queued landblocks win). Caps GPU upload per frame so a big diff doesn't
|
||||
// spike. _deferredApply now only ever holds loads — unloads were applied in step 1.
|
||||
int budget = MaxCompletionsPerFrame;
|
||||
int i = 0;
|
||||
while (i < budget && i < _deferredApply.Count) { ApplyResult(_deferredApply[i]); i++; }
|
||||
if (i > 0) _deferredApply.RemoveRange(0, i);
|
||||
int applied = 0;
|
||||
int read = 0;
|
||||
int write = 0;
|
||||
try
|
||||
{
|
||||
while (applied < budget && read < _deferredApply.Count)
|
||||
{
|
||||
LandblockStreamResult result = _deferredApply[read];
|
||||
if (IsPublicationBlockedByRetirement(result))
|
||||
{
|
||||
if (write != read)
|
||||
_deferredApply[write] = result;
|
||||
write++;
|
||||
read++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Advance read only after the apply commits. If it throws, the
|
||||
// finally block removes prior committed entries but retains this
|
||||
// exact result and the untouched tail for retry.
|
||||
ApplyResult(result);
|
||||
read++;
|
||||
applied++;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// One linear tail shift replaces budget-many RemoveAt shifts. The
|
||||
// retained blocked prefix and the untouched FIFO tail keep their
|
||||
// original relative order.
|
||||
if (read > write)
|
||||
_deferredApply.RemoveRange(write, read - write);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -683,8 +938,7 @@ public sealed class StreamingController
|
|||
_onLandblockLoaded?.Invoke(promoted.LandblockId);
|
||||
break;
|
||||
case LandblockStreamResult.Unloaded unloaded:
|
||||
_state.RemoveLandblock(unloaded.LandblockId);
|
||||
_removeTerrain?.Invoke(unloaded.LandblockId);
|
||||
_retirements.BeginFull(unloaded.LandblockId);
|
||||
break;
|
||||
case LandblockStreamResult.Failed failed:
|
||||
Console.WriteLine(
|
||||
|
|
@ -722,6 +976,10 @@ public sealed class StreamingController
|
|||
result is not LandblockStreamResult.WorkerCrashed
|
||||
&& result.Generation != _generation;
|
||||
|
||||
private bool IsPublicationBlockedByRetirement(LandblockStreamResult result) =>
|
||||
result is LandblockStreamResult.Loaded or LandblockStreamResult.Promoted
|
||||
&& _retirements.IsPending(result.LandblockId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the landblock id associated with <paramref name="result"/>.
|
||||
/// For <see cref="LandblockStreamResult.WorkerCrashed"/> this is 0 by
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue