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:
Erik 2026-07-21 22:07:36 +02:00
parent dc39772bf9
commit ea7ffbc186
10 changed files with 860 additions and 140 deletions

View file

@ -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,