refactor(streaming): own presentation retirement in pipeline
Move full and near-tier presentation retirement out of GameWindow and into a concrete pipeline-owned stage mapper. Harden the retry ledger against reentrancy and committed visibility observer failures, enforce a single state/resource ownership graph, and split concrete versus legacy controller construction so callbacks cannot be silently ignored. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
dc39772bf9
commit
ea7ffbc186
10 changed files with 860 additions and 140 deletions
|
|
@ -150,7 +150,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
// Phase A.1: streaming fields replacing the one-shot _entities list.
|
||||
private AcDream.App.Streaming.LandblockStreamer? _streamer;
|
||||
private AcDream.App.Streaming.GpuWorldState _worldState = new();
|
||||
private AcDream.App.Streaming.LandblockRetirementCoordinator? _landblockRetirements;
|
||||
private AcDream.App.Streaming.LandblockRenderPublisher? _landblockRenderPublisher;
|
||||
private AcDream.App.Streaming.LandblockPhysicsPublisher? _landblockPhysicsPublisher;
|
||||
private AcDream.App.Streaming.LandblockStaticPresentationPublisher?
|
||||
|
|
@ -2208,11 +2207,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
wbSpawnAdapter,
|
||||
onLandblockUnloaded: _classificationCache.InvalidateLandblock,
|
||||
entityScriptActivator: entityScriptActivator);
|
||||
_landblockRetirements =
|
||||
new AcDream.App.Streaming.LandblockRetirementCoordinator(
|
||||
_worldState,
|
||||
AdvanceLandblockPresentationRetirement);
|
||||
|
||||
_liveEntities = new AcDream.App.World.LiveEntityRuntime(
|
||||
_worldState,
|
||||
new AcDream.App.World.CompositeLiveEntityResourceLifecycle(
|
||||
|
|
@ -2529,17 +2523,24 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
});
|
||||
_streamer.Start();
|
||||
|
||||
var landblockRetirementOwner =
|
||||
new AcDream.App.Streaming.LandblockPresentationRetirementOwner(
|
||||
_landblockRenderPublisher!,
|
||||
_landblockPhysicsPublisher!,
|
||||
_landblockStaticPresentationPublisher!,
|
||||
_lightingSink!,
|
||||
_translucencyFades);
|
||||
var landblockPresentationPipeline =
|
||||
new AcDream.App.Streaming.LandblockPresentationPipeline(
|
||||
_landblockRenderPublisher!,
|
||||
_landblockPhysicsPublisher!,
|
||||
_landblockStaticPresentationPublisher!,
|
||||
_worldState,
|
||||
landblockRetirementOwner,
|
||||
onLandblockLoaded: loadedLandblockId =>
|
||||
_liveEntityHydration!.OnLandblockLoaded(loadedLandblockId),
|
||||
ensureEnvCellMeshes:
|
||||
_landblockRenderPublisher!.PrepareAfterRenderPins,
|
||||
retirementCoordinator: _landblockRetirements);
|
||||
_landblockRenderPublisher!.PrepareAfterRenderPins);
|
||||
_streamingController = new AcDream.App.Streaming.StreamingController(
|
||||
enqueueLoad: (id, kind, generation) => _streamer.EnqueueLoad(
|
||||
new AcDream.App.Streaming.LandblockBuildRequest(
|
||||
|
|
@ -2551,18 +2552,13 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
_liveCenterY))),
|
||||
enqueueUnload: (id, generation) => _streamer.EnqueueUnload(id, generation),
|
||||
drainCompletions: _streamer.DrainCompletions,
|
||||
applyTerrain: ApplyLoadedTerrain,
|
||||
state: _worldState,
|
||||
nearRadius: _nearRadius,
|
||||
farRadius: _farRadius,
|
||||
clearPendingLoads: _streamer.ClearPendingLoads,
|
||||
// #138: restore retained server objects when a landblock reloads
|
||||
// Retained-object recovery runs after each
|
||||
// (dungeon-exit expand or Far→Near promote). ACE won't re-send the
|
||||
// objects it thinks we still know, so we re-project them ourselves.
|
||||
onLandblockLoaded: loadedLandblockId =>
|
||||
_liveEntityHydration!.OnLandblockLoaded(loadedLandblockId),
|
||||
ensureEnvCellMeshes: _landblockRenderPublisher!.PrepareAfterRenderPins,
|
||||
retirementCoordinator: _landblockRetirements,
|
||||
// objects it still considers known.
|
||||
presentationPipeline: landblockPresentationPipeline);
|
||||
// A.5 T22.5: apply max-completions from resolved quality.
|
||||
_streamingController.MaxCompletionsPerFrame = _resolvedQuality.MaxCompletionsPerFrame;
|
||||
|
|
@ -3805,65 +3801,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
$"live: AdminEnvirons sound cue = {name} " +
|
||||
$"(0x{environChangeType:X2}) — audio binding pending");
|
||||
}
|
||||
private void AdvanceLandblockPresentationRetirement(
|
||||
AcDream.App.Streaming.LandblockRetirementTicket ticket)
|
||||
{
|
||||
bool staticOnly =
|
||||
ticket.Kind == AcDream.App.Streaming.LandblockRetirementKind.NearLayer;
|
||||
|
||||
ticket.RunForEachEntity(
|
||||
AcDream.App.Streaming.LandblockRetirementStage.EntityLighting,
|
||||
entity => !staticOnly || entity.ServerGuid == 0,
|
||||
entity =>
|
||||
{
|
||||
if (entity.ServerGuid == 0)
|
||||
_landblockStaticPresentationPublisher!.RemoveLighting(entity);
|
||||
else
|
||||
_lightingSink?.UnregisterOwner(entity.Id);
|
||||
});
|
||||
ticket.RunForEachEntity(
|
||||
AcDream.App.Streaming.LandblockRetirementStage.EntityTranslucency,
|
||||
entity => !staticOnly || entity.ServerGuid == 0,
|
||||
entity =>
|
||||
{
|
||||
if (entity.ServerGuid == 0)
|
||||
_landblockStaticPresentationPublisher!.RemoveTranslucency(entity);
|
||||
else
|
||||
_translucencyFades.ClearEntity(entity.Id);
|
||||
});
|
||||
ticket.RunForEachEntity(
|
||||
AcDream.App.Streaming.LandblockRetirementStage.PluginProjection,
|
||||
static entity => entity.ServerGuid == 0,
|
||||
entity => _landblockStaticPresentationPublisher!
|
||||
.RemovePluginProjection(entity));
|
||||
|
||||
if (ticket.Kind == AcDream.App.Streaming.LandblockRetirementKind.Full)
|
||||
{
|
||||
ticket.RunOnce(
|
||||
AcDream.App.Streaming.LandblockRetirementStage.Terrain,
|
||||
() => _landblockRenderPublisher?.RemoveTerrain(ticket.LandblockId));
|
||||
}
|
||||
|
||||
ticket.RunOnce(
|
||||
AcDream.App.Streaming.LandblockRetirementStage.Physics,
|
||||
() =>
|
||||
{
|
||||
if (ticket.Kind == AcDream.App.Streaming.LandblockRetirementKind.Full)
|
||||
_landblockPhysicsPublisher!.RemoveLandblock(ticket.LandblockId);
|
||||
else
|
||||
_landblockPhysicsPublisher!.DemoteToTerrain(ticket.LandblockId);
|
||||
});
|
||||
ticket.RunOnce(
|
||||
AcDream.App.Streaming.LandblockRetirementStage.CellVisibility,
|
||||
() => _landblockRenderPublisher?.RemoveCellVisibility(ticket.LandblockId));
|
||||
ticket.RunOnce(
|
||||
AcDream.App.Streaming.LandblockRetirementStage.BuildingRegistry,
|
||||
() => _landblockRenderPublisher?.RemoveBuildingRegistry(ticket.LandblockId));
|
||||
ticket.RunOnce(
|
||||
AcDream.App.Streaming.LandblockRetirementStage.EnvironmentCells,
|
||||
() => _landblockRenderPublisher?.RemoveEnvironmentCells(ticket.LandblockId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase A.1 / A.5 T12: render-thread callback from StreamingController.Tick
|
||||
/// whenever a new landblock's terrain + entities are ready for GPU upload.
|
||||
|
|
@ -5961,7 +5898,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
|||
LiveEntities: _liveEntities?.Count ?? 0,
|
||||
MaterializedLiveEntities: _liveEntities?.MaterializedCount ?? 0,
|
||||
PendingLiveTeardowns: _liveEntities?.PendingTeardownCount ?? 0,
|
||||
PendingLandblockRetirements: _landblockRetirements?.PendingCount ?? 0,
|
||||
PendingLandblockRetirements:
|
||||
_streamingController?.PendingRetirementCount ?? 0,
|
||||
ParticleEmitters: _particleSystem?.ActiveEmitterCount ?? 0,
|
||||
Particles: _particleSystem?.ActiveParticleCount ?? 0,
|
||||
ParticleBindings: _particleSink?.ActiveBindingCount ?? 0,
|
||||
|
|
|
|||
|
|
@ -79,6 +79,13 @@ public sealed class LandblockPresentationPipeline
|
|||
_state = state;
|
||||
_onLandblockLoaded = onLandblockLoaded;
|
||||
_ensureEnvCellMeshes = ensureEnvCellMeshes;
|
||||
if (retirementCoordinator is not null
|
||||
&& !retirementCoordinator.MatchesState(_state))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"The retirement coordinator must own the pipeline's world state.",
|
||||
nameof(retirementCoordinator));
|
||||
}
|
||||
_retirements = retirementCoordinator
|
||||
?? LandblockRetirementCoordinator.CreateLegacy(
|
||||
state,
|
||||
|
|
@ -96,9 +103,9 @@ public sealed class LandblockPresentationPipeline
|
|||
LandblockPhysicsPublisher physicsPublisher,
|
||||
LandblockStaticPresentationPublisher staticPublisher,
|
||||
GpuWorldState state,
|
||||
LandblockPresentationRetirementOwner retirementOwner,
|
||||
Action<uint>? onLandblockLoaded = null,
|
||||
Action<EnvCellLandblockBuild>? ensureEnvCellMeshes = null,
|
||||
LandblockRetirementCoordinator? retirementCoordinator = null)
|
||||
Action<EnvCellLandblockBuild>? ensureEnvCellMeshes = null)
|
||||
{
|
||||
_renderPublisher = renderPublisher
|
||||
?? throw new ArgumentNullException(nameof(renderPublisher));
|
||||
|
|
@ -107,19 +114,35 @@ public sealed class LandblockPresentationPipeline
|
|||
_staticPublisher = staticPublisher
|
||||
?? throw new ArgumentNullException(nameof(staticPublisher));
|
||||
_state = state ?? throw new ArgumentNullException(nameof(state));
|
||||
if (!_renderPublisher.MatchesState(_state))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"The render publisher must own the pipeline's world state.",
|
||||
nameof(state));
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(retirementOwner);
|
||||
if (!retirementOwner.Matches(
|
||||
_renderPublisher,
|
||||
_physicsPublisher,
|
||||
_staticPublisher))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"The retirement owner must reference the pipeline's concrete publishers.",
|
||||
nameof(retirementOwner));
|
||||
}
|
||||
_onLandblockLoaded = onLandblockLoaded;
|
||||
_ensureEnvCellMeshes = ensureEnvCellMeshes;
|
||||
// Until Slice 5F internalizes concrete retirement ownership, a
|
||||
// concrete publication pipeline must share the production coordinator.
|
||||
// A legacy no-op retirement suffix would detach spatial state while
|
||||
// leaking the concrete render/physics/static owners.
|
||||
_retirements = retirementCoordinator
|
||||
?? throw new ArgumentNullException(nameof(retirementCoordinator));
|
||||
_retirements = new LandblockRetirementCoordinator(
|
||||
state,
|
||||
retirementOwner.Advance);
|
||||
}
|
||||
|
||||
public int PendingRetirementCount => _retirements.PendingCount;
|
||||
public int PendingPublicationCount => _publications.Count;
|
||||
|
||||
internal bool MatchesState(GpuWorldState state) =>
|
||||
ReferenceEquals(_state, state);
|
||||
|
||||
public bool IsRetirementPending(uint landblockId) =>
|
||||
_retirements.IsPending(landblockId);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
using AcDream.Core.Lighting;
|
||||
using AcDream.Core.Rendering;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Streaming;
|
||||
|
||||
/// <summary>
|
||||
/// Composes the presentation-owner half of one detach-first landblock
|
||||
/// retirement. <see cref="LandblockRetirementTicket"/> remains the exact
|
||||
/// retry ledger; this class only maps each stage to its concrete owner.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Retail separates <c>CLandBlock::release_all</c> (0x0052FCF0),
|
||||
/// <c>CLandBlock::destroy_static_objects</c> (0x0052FA50), and
|
||||
/// <c>CLandBlock::Destroy</c> (0x0052FAA0). Acdream's asynchronous equivalent
|
||||
/// detaches spatial reachability first, then advances these concrete owners
|
||||
/// from the retained ticket without replaying successful stages or entities.
|
||||
/// </remarks>
|
||||
public sealed class LandblockPresentationRetirementOwner
|
||||
{
|
||||
private readonly LandblockRenderPublisher _render;
|
||||
private readonly LandblockPhysicsPublisher _physics;
|
||||
private readonly LandblockStaticPresentationPublisher _staticPresentation;
|
||||
private readonly LightingHookSink _lighting;
|
||||
private readonly TranslucencyFadeManager _translucency;
|
||||
|
||||
public LandblockPresentationRetirementOwner(
|
||||
LandblockRenderPublisher render,
|
||||
LandblockPhysicsPublisher physics,
|
||||
LandblockStaticPresentationPublisher staticPresentation,
|
||||
LightingHookSink lighting,
|
||||
TranslucencyFadeManager translucency)
|
||||
{
|
||||
_render = render ?? throw new ArgumentNullException(nameof(render));
|
||||
_physics = physics ?? throw new ArgumentNullException(nameof(physics));
|
||||
_staticPresentation = staticPresentation
|
||||
?? throw new ArgumentNullException(nameof(staticPresentation));
|
||||
_lighting = lighting ?? throw new ArgumentNullException(nameof(lighting));
|
||||
_translucency = translucency
|
||||
?? throw new ArgumentNullException(nameof(translucency));
|
||||
if (!_staticPresentation.MatchesResources(_lighting, _translucency))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"The retirement resources must be those owned by the static " +
|
||||
"presentation publisher.",
|
||||
nameof(staticPresentation));
|
||||
}
|
||||
}
|
||||
|
||||
internal bool Matches(
|
||||
LandblockRenderPublisher render,
|
||||
LandblockPhysicsPublisher physics,
|
||||
LandblockStaticPresentationPublisher staticPresentation) =>
|
||||
ReferenceEquals(_render, render)
|
||||
&& ReferenceEquals(_physics, physics)
|
||||
&& ReferenceEquals(_staticPresentation, staticPresentation)
|
||||
&& _staticPresentation.MatchesResources(_lighting, _translucency);
|
||||
|
||||
public void Advance(LandblockRetirementTicket ticket)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(ticket);
|
||||
bool staticOnly = ticket.Kind == LandblockRetirementKind.NearLayer;
|
||||
|
||||
ticket.RunForEachEntity(
|
||||
LandblockRetirementStage.EntityLighting,
|
||||
entity => !staticOnly || entity.ServerGuid == 0,
|
||||
RemoveLighting);
|
||||
ticket.RunForEachEntity(
|
||||
LandblockRetirementStage.EntityTranslucency,
|
||||
entity => !staticOnly || entity.ServerGuid == 0,
|
||||
RemoveTranslucency);
|
||||
ticket.RunForEachEntity(
|
||||
LandblockRetirementStage.PluginProjection,
|
||||
static entity => entity.ServerGuid == 0,
|
||||
_staticPresentation.RemovePluginProjection);
|
||||
|
||||
if (ticket.Kind == LandblockRetirementKind.Full)
|
||||
{
|
||||
ticket.RunOnce(
|
||||
LandblockRetirementStage.Terrain,
|
||||
() => _render.RemoveTerrain(ticket.LandblockId));
|
||||
}
|
||||
|
||||
ticket.RunOnce(
|
||||
LandblockRetirementStage.Physics,
|
||||
() =>
|
||||
{
|
||||
if (ticket.Kind == LandblockRetirementKind.Full)
|
||||
_physics.RemoveLandblock(ticket.LandblockId);
|
||||
else
|
||||
_physics.DemoteToTerrain(ticket.LandblockId);
|
||||
});
|
||||
ticket.RunOnce(
|
||||
LandblockRetirementStage.CellVisibility,
|
||||
() => _render.RemoveCellVisibility(ticket.LandblockId));
|
||||
ticket.RunOnce(
|
||||
LandblockRetirementStage.BuildingRegistry,
|
||||
() => _render.RemoveBuildingRegistry(ticket.LandblockId));
|
||||
ticket.RunOnce(
|
||||
LandblockRetirementStage.EnvironmentCells,
|
||||
() => _render.RemoveEnvironmentCells(ticket.LandblockId));
|
||||
}
|
||||
|
||||
private void RemoveLighting(WorldEntity entity)
|
||||
{
|
||||
if (entity.ServerGuid == 0)
|
||||
_staticPresentation.RemoveLighting(entity);
|
||||
else
|
||||
_lighting.UnregisterOwner(entity.Id);
|
||||
}
|
||||
|
||||
private void RemoveTranslucency(WorldEntity entity)
|
||||
{
|
||||
if (entity.ServerGuid == 0)
|
||||
_staticPresentation.RemoveTranslucency(entity);
|
||||
else
|
||||
_translucency.ClearEntity(entity.Id);
|
||||
}
|
||||
}
|
||||
|
|
@ -145,6 +145,9 @@ public sealed class LandblockRenderPublisher
|
|||
_buildingRegistryRemovalCount,
|
||||
_envCellRemovalCount);
|
||||
|
||||
internal bool MatchesState(GpuWorldState state) =>
|
||||
ReferenceEquals(_worldState, state);
|
||||
|
||||
/// <summary>
|
||||
/// Publishes the prefix that retail establishes before physics/static
|
||||
/// activation: terrain, complete visibility cells, and spatial bounds.
|
||||
|
|
|
|||
|
|
@ -136,14 +136,23 @@ public sealed class LandblockRetirementTicket
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Game-window-lifetime owner for landblock retirement. World-state detachment
|
||||
/// Pipeline-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.
|
||||
/// per-owner ledger. Reentrant begin/advance requests are serialized so an
|
||||
/// owner callback cannot recursively replay its active stage or mutate the
|
||||
/// pending-ticket map while it is being enumerated.
|
||||
/// </summary>
|
||||
public sealed class LandblockRetirementCoordinator
|
||||
{
|
||||
private enum RequestKind : byte
|
||||
{
|
||||
BeginFull,
|
||||
BeginNearLayer,
|
||||
Advance,
|
||||
}
|
||||
|
||||
private readonly record struct Request(RequestKind Kind, uint LandblockId);
|
||||
|
||||
private const LandblockRetirementStage CoreStages =
|
||||
LandblockRetirementStage.MeshReferences
|
||||
| LandblockRetirementStage.StaticScripts
|
||||
|
|
@ -165,6 +174,9 @@ public sealed class LandblockRetirementCoordinator
|
|||
_requiredPresentationStages;
|
||||
private readonly Dictionary<uint, List<LandblockRetirementTicket>> _pending = new();
|
||||
private readonly List<uint> _completedIds = new();
|
||||
private readonly Queue<Request> _requests = new();
|
||||
private readonly HashSet<Request> _seenDuringDrain = new();
|
||||
private bool _drainingRequests;
|
||||
|
||||
public LandblockRetirementCoordinator(
|
||||
GpuWorldState state,
|
||||
|
|
@ -186,13 +198,19 @@ public sealed class LandblockRetirementCoordinator
|
|||
public bool IsPending(uint landblockId) =>
|
||||
_pending.ContainsKey(Canonicalize(landblockId));
|
||||
|
||||
public void BeginFull(uint landblockId) =>
|
||||
Begin(landblockId, LandblockRetirementKind.Full);
|
||||
internal bool MatchesState(GpuWorldState state) =>
|
||||
ReferenceEquals(_state, state);
|
||||
|
||||
public void BeginNearLayer(uint landblockId) =>
|
||||
Begin(landblockId, LandblockRetirementKind.NearLayer);
|
||||
public void BeginFull(uint landblockId) => EnqueueRequest(
|
||||
new Request(RequestKind.BeginFull, Canonicalize(landblockId)));
|
||||
|
||||
public void Advance()
|
||||
public void BeginNearLayer(uint landblockId) => EnqueueRequest(
|
||||
new Request(RequestKind.BeginNearLayer, Canonicalize(landblockId)));
|
||||
|
||||
public void Advance() => EnqueueRequest(
|
||||
new Request(RequestKind.Advance, 0u));
|
||||
|
||||
private void AdvanceCore()
|
||||
{
|
||||
if (_pending.Count == 0)
|
||||
return;
|
||||
|
|
@ -250,7 +268,7 @@ public sealed class LandblockRetirementCoordinator
|
|||
return new LandblockRetirementCoordinator(state, AdvanceLegacy, Required);
|
||||
}
|
||||
|
||||
private void Begin(uint landblockId, LandblockRetirementKind kind)
|
||||
private void BeginCore(uint landblockId, LandblockRetirementKind kind)
|
||||
{
|
||||
uint canonical = Canonicalize(landblockId);
|
||||
if (_pending.TryGetValue(canonical, out List<LandblockRetirementTicket>? existing))
|
||||
|
|
@ -259,20 +277,52 @@ public sealed class LandblockRetirementCoordinator
|
|||
{
|
||||
if (existing[i].Kind == kind)
|
||||
{
|
||||
AdvanceTicket(existing[i]);
|
||||
LandblockRetirementTicket existingTicket = existing[i];
|
||||
AdvanceTicket(existingTicket);
|
||||
if (existingTicket.IsComplete)
|
||||
{
|
||||
existing.RemoveAt(i);
|
||||
PendingCount--;
|
||||
if (existing.Count == 0)
|
||||
_pending.Remove(canonical);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GpuLandblockRetirement? stateRetirement = kind == LandblockRetirementKind.Full
|
||||
? _state.DetachLandblock(canonical)
|
||||
: _state.DetachNearLayer(canonical);
|
||||
if (stateRetirement is null)
|
||||
return;
|
||||
|
||||
LandblockRetirementStage required =
|
||||
CoreStages | _requiredPresentationStages(kind);
|
||||
GpuLandblockRetirement? stateRetirement = null;
|
||||
Exception? detachmentObserverFailure = null;
|
||||
GpuWorldState.MutationBatch mutation = _state.BeginMutationBatch();
|
||||
try
|
||||
{
|
||||
// The detach methods create a nested batch. Holding this outer
|
||||
// transaction lets us retain their returned teardown context
|
||||
// before visibility observers run at the outer commit boundary.
|
||||
stateRetirement = kind == LandblockRetirementKind.Full
|
||||
? _state.DetachLandblock(canonical)
|
||||
: _state.DetachNearLayer(canonical);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
mutation.Dispose();
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
detachmentObserverFailure = error;
|
||||
}
|
||||
}
|
||||
if (stateRetirement is null)
|
||||
{
|
||||
if (detachmentObserverFailure is not null)
|
||||
throw detachmentObserverFailure;
|
||||
return;
|
||||
}
|
||||
|
||||
var ticket = new LandblockRetirementTicket(stateRetirement, required);
|
||||
if (existing is null)
|
||||
{
|
||||
|
|
@ -290,6 +340,13 @@ public sealed class LandblockRetirementCoordinator
|
|||
if (existing.Count == 0)
|
||||
_pending.Remove(canonical);
|
||||
}
|
||||
// Visibility is a delivered-by-attempt broadcast: every subscriber is
|
||||
// invoked and failures are aggregated, but failed subscribers are not
|
||||
// retained as replayable work. Complete the presentation teardown
|
||||
// from the preserved detach receipt, then surface that committed-edge
|
||||
// failure to the caller without inventing a one-frame pending fence.
|
||||
if (detachmentObserverFailure is not null)
|
||||
throw detachmentObserverFailure;
|
||||
}
|
||||
|
||||
private void AdvanceTicket(LandblockRetirementTicket ticket)
|
||||
|
|
@ -325,4 +382,70 @@ public sealed class LandblockRetirementCoordinator
|
|||
|
||||
private static uint Canonicalize(uint id) =>
|
||||
(id & 0xFFFF0000u) | 0xFFFFu;
|
||||
|
||||
private void EnqueueRequest(Request request)
|
||||
{
|
||||
if (_drainingRequests)
|
||||
{
|
||||
if (_seenDuringDrain.Add(request))
|
||||
_requests.Enqueue(request);
|
||||
return;
|
||||
}
|
||||
|
||||
_drainingRequests = true;
|
||||
_seenDuringDrain.Clear();
|
||||
_requests.Clear();
|
||||
_seenDuringDrain.Add(request);
|
||||
_requests.Enqueue(request);
|
||||
List<Exception>? failures = null;
|
||||
try
|
||||
{
|
||||
while (_requests.TryDequeue(out Request pending))
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (pending.Kind)
|
||||
{
|
||||
case RequestKind.BeginFull:
|
||||
BeginCore(
|
||||
pending.LandblockId,
|
||||
LandblockRetirementKind.Full);
|
||||
break;
|
||||
case RequestKind.BeginNearLayer:
|
||||
BeginCore(
|
||||
pending.LandblockId,
|
||||
LandblockRetirementKind.NearLayer);
|
||||
break;
|
||||
case RequestKind.Advance:
|
||||
AdvanceCore();
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException(
|
||||
$"Unknown retirement request {pending.Kind}.");
|
||||
}
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
(failures ??= new List<Exception>()).Add(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_requests.Clear();
|
||||
_seenDuringDrain.Clear();
|
||||
_drainingRequests = false;
|
||||
}
|
||||
|
||||
if (failures is { Count: 1 })
|
||||
System.Runtime.ExceptionServices.ExceptionDispatchInfo
|
||||
.Capture(failures[0])
|
||||
.Throw();
|
||||
if (failures is { Count: > 1 })
|
||||
{
|
||||
throw new AggregateException(
|
||||
"One or more serialized landblock retirement requests failed.",
|
||||
failures);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,6 +108,12 @@ public sealed class LandblockStaticPresentationPublisher
|
|||
_activeByLandblock.Count,
|
||||
_landblockByEntityId.Count);
|
||||
|
||||
internal bool MatchesResources(
|
||||
LightingHookSink lighting,
|
||||
TranslucencyFadeManager translucency) =>
|
||||
ReferenceEquals(_lighting, lighting)
|
||||
&& ReferenceEquals(_translucency, translucency);
|
||||
|
||||
public LandblockStaticPresentationPublication PreparePublication(
|
||||
LandblockPhysicsPublication physicsPublication)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ public sealed class StreamingController
|
|||
// deferred-LOAD backlog. A non-zero value during/after a teleport is the
|
||||
// GPU-upload tail draining at MaxCompletionsPerFrame. Removable probe surface.
|
||||
public int DeferredApplyBacklog => _deferredApply.Count;
|
||||
public int PendingRetirementCount => _presentation.PendingRetirementCount;
|
||||
|
||||
/// <summary>
|
||||
/// True once every in-bounds landblock in the requested Chebyshev ring has
|
||||
|
|
@ -174,8 +175,40 @@ public sealed class StreamingController
|
|||
Action? clearPendingLoads = null,
|
||||
Action<uint>? onLandblockLoaded = null,
|
||||
Action<EnvCellLandblockBuild>? ensureEnvCellMeshes = null,
|
||||
LandblockRetirementCoordinator? retirementCoordinator = null,
|
||||
LandblockPresentationPipeline? presentationPipeline = null)
|
||||
LandblockRetirementCoordinator? retirementCoordinator = null)
|
||||
: this(
|
||||
enqueueLoad,
|
||||
enqueueUnload,
|
||||
drainCompletions,
|
||||
state,
|
||||
nearRadius,
|
||||
farRadius,
|
||||
new LandblockPresentationPipeline(
|
||||
applyTerrain,
|
||||
state,
|
||||
onLandblockLoaded,
|
||||
ensureEnvCellMeshes,
|
||||
retirementCoordinator,
|
||||
removeTerrain,
|
||||
demoteNearLayer),
|
||||
clearPendingLoads)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Concrete presentation path. Its pipeline is the sole presentation and
|
||||
/// retirement owner, so no legacy publication delegate can be supplied or
|
||||
/// silently ignored.
|
||||
/// </summary>
|
||||
public StreamingController(
|
||||
Action<uint, LandblockStreamJobKind, ulong> enqueueLoad,
|
||||
Action<uint, ulong> enqueueUnload,
|
||||
Func<int, IReadOnlyList<LandblockStreamResult>> drainCompletions,
|
||||
GpuWorldState state,
|
||||
int nearRadius,
|
||||
int farRadius,
|
||||
LandblockPresentationPipeline presentationPipeline,
|
||||
Action? clearPendingLoads = null)
|
||||
{
|
||||
_enqueueLoad = enqueueLoad;
|
||||
_enqueueUnload = enqueueUnload;
|
||||
|
|
@ -183,14 +216,13 @@ public sealed class StreamingController
|
|||
_clearPendingLoads = clearPendingLoads;
|
||||
_state = state;
|
||||
_presentation = presentationPipeline
|
||||
?? new LandblockPresentationPipeline(
|
||||
applyTerrain,
|
||||
state,
|
||||
onLandblockLoaded,
|
||||
ensureEnvCellMeshes,
|
||||
retirementCoordinator,
|
||||
removeTerrain,
|
||||
demoteNearLayer);
|
||||
?? throw new ArgumentNullException(nameof(presentationPipeline));
|
||||
if (!_presentation.MatchesState(_state))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"The presentation pipeline must own the controller's world state.",
|
||||
nameof(presentationPipeline));
|
||||
}
|
||||
NearRadius = nearRadius;
|
||||
FarRadius = farRadius;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue