refactor(streaming): centralize world reveal lifetime
Compose destination readiness and lifecycle telemetry behind one App-layer coordinator so login and portal begin, preparation, materialization, visibility, completion, and cancellation cannot drift apart in GameWindow wiring.
This commit is contained in:
parent
68578fa5fa
commit
a4ef57885c
4 changed files with 213 additions and 30 deletions
|
|
@ -239,7 +239,8 @@ src/
|
||||||
Streaming/
|
Streaming/
|
||||||
StreamingController.cs -> done
|
StreamingController.cs -> done
|
||||||
GpuWorldState.cs -> done
|
GpuWorldState.cs -> done
|
||||||
WorldRevealReadinessBarrier.cs -> shared login/portal reveal owner
|
WorldRevealCoordinator.cs -> shared login/portal lifetime owner
|
||||||
|
WorldRevealReadinessBarrier.cs -> canonical destination predicate
|
||||||
Input/
|
Input/
|
||||||
PlayerMovementController.cs -> active movement driver
|
PlayerMovementController.cs -> active movement driver
|
||||||
Plugins/
|
Plugins/
|
||||||
|
|
@ -708,14 +709,16 @@ partially hydrated cell membership or shell placement to the renderer.
|
||||||
|
|
||||||
### World-reveal readiness ownership
|
### World-reveal readiness ownership
|
||||||
|
|
||||||
`WorldRevealReadinessBarrier` is the single App-layer owner of the edge that
|
`WorldRevealCoordinator` is the single App-layer owner of each login or portal
|
||||||
allows normal world geometry to become visible at initial login or after a
|
reveal lifetime. It composes the canonical `WorldRevealReadinessBarrier` with
|
||||||
portal. It joins `StreamingController`/`GpuWorldState` static-mesh publication,
|
generation-scoped lifecycle observation, so begin, preparation, readiness,
|
||||||
`WbDrawDispatcher` composite-texture warmup, and `PhysicsEngine` destination
|
materialization, world visibility, completion, and cancellation cannot be
|
||||||
residency. Login's sky-only gate and portal-space transit consume the same
|
wired independently. The barrier joins `StreamingController`/`GpuWorldState`
|
||||||
predicate; they differ only in presentation. Composite preparation advances
|
static-mesh publication, `WbDrawDispatcher` composite-texture warmup, and
|
||||||
after `WbMeshAdapter.Tick` on the render thread, never from the streaming
|
`PhysicsEngine` destination residency. Login's sky-only gate and portal-space
|
||||||
worker. This is acdream's asynchronous equivalent of retail
|
transit consume the same predicate; they differ only in presentation.
|
||||||
|
Composite preparation advances after `WbMeshAdapter.Tick` on the render
|
||||||
|
thread, never from the streaming worker. This is acdream's asynchronous equivalent of retail
|
||||||
`SmartBox::UseTime` holding position completion while
|
`SmartBox::UseTime` holding position completion while
|
||||||
`CellManager::blocking_for_cells` is set.
|
`CellManager::blocking_for_cells` is set.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -149,9 +149,7 @@ public sealed class GameWindow : IDisposable
|
||||||
private AcDream.App.Streaming.LandblockRetirementCoordinator? _landblockRetirements;
|
private AcDream.App.Streaming.LandblockRetirementCoordinator? _landblockRetirements;
|
||||||
private AcDream.App.Rendering.EquippedChildRenderController? _equippedChildRenderer;
|
private AcDream.App.Rendering.EquippedChildRenderController? _equippedChildRenderer;
|
||||||
private AcDream.App.Streaming.StreamingController? _streamingController;
|
private AcDream.App.Streaming.StreamingController? _streamingController;
|
||||||
private AcDream.App.Streaming.WorldRevealReadinessBarrier? _worldRevealReadiness;
|
private AcDream.App.Streaming.WorldRevealCoordinator? _worldReveal;
|
||||||
private readonly AcDream.App.Streaming.WorldRevealLifecycleTelemetry _worldRevealTelemetry =
|
|
||||||
new(message => Console.WriteLine(message));
|
|
||||||
private int _streamingRadius = 2; // default 5×5 (kept for debug overlay getStreamingRadius callback)
|
private int _streamingRadius = 2; // default 5×5 (kept for debug overlay getStreamingRadius callback)
|
||||||
private int _nearRadius = 4; // Phase A.5 T16: two-tier near ring (default 4 → 9×9)
|
private int _nearRadius = 4; // Phase A.5 T16: two-tier near ring (default 4 → 9×9)
|
||||||
private int _farRadius = 12; // Phase A.5 T16: two-tier far ring (default 12 → 25×25)
|
private int _farRadius = 12; // Phase A.5 T16: two-tier far ring (default 12 → 25×25)
|
||||||
|
|
@ -2287,8 +2285,8 @@ public sealed class GameWindow : IDisposable
|
||||||
UiProbeLog);
|
UiProbeLog);
|
||||||
_worldLifecycleAutomation =
|
_worldLifecycleAutomation =
|
||||||
new AcDream.App.Diagnostics.WorldLifecycleAutomationController(
|
new AcDream.App.Diagnostics.WorldLifecycleAutomationController(
|
||||||
() => _worldRevealTelemetry.Snapshot,
|
() => _worldReveal?.Snapshot ?? default,
|
||||||
() => _worldRevealTelemetry.PortalMaterializationCount,
|
() => _worldReveal?.PortalMaterializationCount ?? 0,
|
||||||
CaptureWorldLifecycleResourceSnapshot,
|
CaptureWorldLifecycleResourceSnapshot,
|
||||||
_frameScreenshots,
|
_frameScreenshots,
|
||||||
artifactDirectory,
|
artifactDirectory,
|
||||||
|
|
@ -2867,7 +2865,7 @@ public sealed class GameWindow : IDisposable
|
||||||
retirementCoordinator: _landblockRetirements);
|
retirementCoordinator: _landblockRetirements);
|
||||||
// A.5 T22.5: apply max-completions from resolved quality.
|
// A.5 T22.5: apply max-completions from resolved quality.
|
||||||
_streamingController.MaxCompletionsPerFrame = _resolvedQuality.MaxCompletionsPerFrame;
|
_streamingController.MaxCompletionsPerFrame = _resolvedQuality.MaxCompletionsPerFrame;
|
||||||
_worldRevealReadiness = new AcDream.App.Streaming.WorldRevealReadinessBarrier(
|
_worldReveal = new AcDream.App.Streaming.WorldRevealCoordinator(
|
||||||
isRenderNeighborhoodReady: _streamingController.IsRenderNeighborhoodResident,
|
isRenderNeighborhoodReady: _streamingController.IsRenderNeighborhoodResident,
|
||||||
isSpawnCellReady: _physicsEngine.IsSpawnCellReady,
|
isSpawnCellReady: _physicsEngine.IsSpawnCellReady,
|
||||||
isTerrainNeighborhoodReady: _physicsEngine.IsNeighborhoodTerrainResident,
|
isTerrainNeighborhoodReady: _physicsEngine.IsNeighborhoodTerrainResident,
|
||||||
|
|
@ -2880,7 +2878,8 @@ public sealed class GameWindow : IDisposable
|
||||||
radius),
|
radius),
|
||||||
invalidateCompositeTextures: () =>
|
invalidateCompositeTextures: () =>
|
||||||
_wbDrawDispatcher?.InvalidateCompositeWarmupReadiness(),
|
_wbDrawDispatcher?.InvalidateCompositeWarmupReadiness(),
|
||||||
isSpawnClaimUnhydratable: IsSpawnClaimUnhydratable);
|
isSpawnClaimUnhydratable: IsSpawnClaimUnhydratable,
|
||||||
|
log: message => Console.WriteLine(message));
|
||||||
|
|
||||||
// Phase 4.7: optional live-mode startup. Connect to the ACE server,
|
// Phase 4.7: optional live-mode startup. Connect to the ACE server,
|
||||||
// enter the world as the first character on the account, and stream
|
// enter the world as the first character on the account, and stream
|
||||||
|
|
@ -3637,8 +3636,7 @@ public sealed class GameWindow : IDisposable
|
||||||
// the same shared reveal lifetime used by portal arrivals: initial
|
// the same shared reveal lifetime used by portal arrivals: initial
|
||||||
// login must not inherit the dispatcher's default/previous composite
|
// login must not inherit the dispatcher's default/previous composite
|
||||||
// readiness and expose a terrain-only or partially uploaded world.
|
// readiness and expose a terrain-only or partially uploaded world.
|
||||||
_worldRevealReadiness?.Begin();
|
_worldReveal?.Begin(AcDream.App.Streaming.WorldRevealKind.Login);
|
||||||
_worldRevealTelemetry.Begin(AcDream.App.Streaming.WorldRevealKind.Login);
|
|
||||||
|
|
||||||
// Streaming is normally gated until this point, so the next loaded-
|
// Streaming is normally gated until this point, so the next loaded-
|
||||||
// landblock callback will materialize deferred records. Also cover an
|
// landblock callback will materialize deferred records. Also cover an
|
||||||
|
|
@ -7561,8 +7559,7 @@ public sealed class GameWindow : IDisposable
|
||||||
_pendingTeleportCell = p.LandblockId;
|
_pendingTeleportCell = p.LandblockId;
|
||||||
_teleportHoldSeconds = 0f;
|
_teleportHoldSeconds = 0f;
|
||||||
_teleportForced = false;
|
_teleportForced = false;
|
||||||
_worldRevealReadiness?.Begin();
|
_worldReveal?.Begin(AcDream.App.Streaming.WorldRevealKind.Portal);
|
||||||
_worldRevealTelemetry.Begin(AcDream.App.Streaming.WorldRevealKind.Portal);
|
|
||||||
if (_streamingController is not null)
|
if (_streamingController is not null)
|
||||||
{
|
{
|
||||||
_streamingController.PriorityLandblockId =
|
_streamingController.PriorityLandblockId =
|
||||||
|
|
@ -7588,7 +7585,7 @@ public sealed class GameWindow : IDisposable
|
||||||
if (clearSession)
|
if (clearSession)
|
||||||
{
|
{
|
||||||
_teleportTransit.ClearSession();
|
_teleportTransit.ClearSession();
|
||||||
_worldRevealTelemetry.Cancel();
|
_worldReveal?.Cancel();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
_teleportTransit.EndActive();
|
_teleportTransit.EndActive();
|
||||||
|
|
@ -7647,9 +7644,7 @@ public sealed class GameWindow : IDisposable
|
||||||
private AcDream.App.Streaming.WorldRevealReadinessSnapshot EvaluateWorldRevealReadiness(
|
private AcDream.App.Streaming.WorldRevealReadinessSnapshot EvaluateWorldRevealReadiness(
|
||||||
uint destinationCell)
|
uint destinationCell)
|
||||||
{
|
{
|
||||||
var snapshot = _worldRevealReadiness?.Evaluate(destinationCell) ?? default;
|
return _worldReveal?.Evaluate(destinationCell) ?? default;
|
||||||
_worldRevealTelemetry.ObserveReadiness(snapshot);
|
|
||||||
return snapshot;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryGetLoginWorldCell(out uint cell)
|
private bool TryGetLoginWorldCell(out uint cell)
|
||||||
|
|
@ -9778,7 +9773,7 @@ public sealed class GameWindow : IDisposable
|
||||||
{
|
{
|
||||||
case AcDream.Core.World.TeleportAnimEvent.Place:
|
case AcDream.Core.World.TeleportAnimEvent.Place:
|
||||||
PlaceTeleportArrival(_pendingTeleportPos, _pendingTeleportCell, _teleportForced);
|
PlaceTeleportArrival(_pendingTeleportPos, _pendingTeleportCell, _teleportForced);
|
||||||
_worldRevealTelemetry.ObserveMaterialized();
|
_worldReveal?.ObserveMaterialized();
|
||||||
if (_streamingController is not null)
|
if (_streamingController is not null)
|
||||||
{
|
{
|
||||||
_streamingController.PriorityLandblockId = 0u;
|
_streamingController.PriorityLandblockId = 0u;
|
||||||
|
|
@ -9798,7 +9793,7 @@ public sealed class GameWindow : IDisposable
|
||||||
// each portal transition.
|
// each portal transition.
|
||||||
_liveSession?.SendGameAction(
|
_liveSession?.SendGameAction(
|
||||||
AcDream.Core.Net.Messages.GameActionLoginComplete.Build());
|
AcDream.Core.Net.Messages.GameActionLoginComplete.Build());
|
||||||
_worldRevealTelemetry.Complete();
|
_worldReveal?.Complete();
|
||||||
ResetTeleportTransitState();
|
ResetTeleportTransitState();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
@ -10157,8 +10152,7 @@ public sealed class GameWindow : IDisposable
|
||||||
: 0u;
|
: 0u;
|
||||||
if (revealCell != 0)
|
if (revealCell != 0)
|
||||||
{
|
{
|
||||||
_worldRevealReadiness?.Prepare(revealCell);
|
_worldReveal?.PrepareAndEvaluate(revealCell);
|
||||||
EvaluateWorldRevealReadiness(revealCell);
|
|
||||||
}
|
}
|
||||||
_particleRenderer?.BeginFrame(_gpuFrameFlights.CurrentSlot);
|
_particleRenderer?.BeginFrame(_gpuFrameFlights.CurrentSlot);
|
||||||
}
|
}
|
||||||
|
|
@ -10219,8 +10213,8 @@ public sealed class GameWindow : IDisposable
|
||||||
if (!IsLiveModeWaitingForLogin)
|
if (!IsLiveModeWaitingForLogin)
|
||||||
{
|
{
|
||||||
_particleVisibility.UseWorldView();
|
_particleVisibility.UseWorldView();
|
||||||
_worldRevealTelemetry.ObserveWorldViewportVisible();
|
_worldReveal?.ObserveWorldViewportVisible();
|
||||||
_worldRevealTelemetry.Complete();
|
_worldReveal?.Complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
// L.0 Audio tab: push the SettingsVM's live AudioDraft into the
|
// L.0 Audio tab: push the SettingsVM's live AudioDraft into the
|
||||||
|
|
|
||||||
69
src/AcDream.App/Streaming/WorldRevealCoordinator.cs
Normal file
69
src/AcDream.App/Streaming/WorldRevealCoordinator.cs
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
namespace AcDream.App.Streaming;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Owns one login/portal reveal lifetime across readiness and lifecycle
|
||||||
|
/// diagnostics. This keeps the destination barrier and its observations on a
|
||||||
|
/// single seam so callers cannot begin, prepare, or evaluate one owner while
|
||||||
|
/// forgetting the other.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class WorldRevealCoordinator
|
||||||
|
{
|
||||||
|
private readonly WorldRevealReadinessBarrier _readiness;
|
||||||
|
private readonly WorldRevealLifecycleTelemetry _lifecycle;
|
||||||
|
|
||||||
|
public WorldRevealCoordinator(
|
||||||
|
Func<uint, int, bool> isRenderNeighborhoodReady,
|
||||||
|
Func<uint, bool> isSpawnCellReady,
|
||||||
|
Func<uint, int, bool> isTerrainNeighborhoodReady,
|
||||||
|
Func<bool> areCompositeTexturesReady,
|
||||||
|
Action<uint, int> prepareCompositeTextures,
|
||||||
|
Action invalidateCompositeTextures,
|
||||||
|
Func<uint, bool> isSpawnClaimUnhydratable,
|
||||||
|
Action<string>? log = null)
|
||||||
|
{
|
||||||
|
_readiness = new WorldRevealReadinessBarrier(
|
||||||
|
isRenderNeighborhoodReady,
|
||||||
|
isSpawnCellReady,
|
||||||
|
isTerrainNeighborhoodReady,
|
||||||
|
areCompositeTexturesReady,
|
||||||
|
prepareCompositeTextures,
|
||||||
|
invalidateCompositeTextures,
|
||||||
|
isSpawnClaimUnhydratable);
|
||||||
|
_lifecycle = new WorldRevealLifecycleTelemetry(log);
|
||||||
|
}
|
||||||
|
|
||||||
|
public WorldRevealLifecycleSnapshot Snapshot => _lifecycle.Snapshot;
|
||||||
|
public int PortalMaterializationCount => _lifecycle.PortalMaterializationCount;
|
||||||
|
|
||||||
|
public long Begin(WorldRevealKind kind)
|
||||||
|
{
|
||||||
|
_readiness.Begin();
|
||||||
|
return _lifecycle.Begin(kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Advances render-thread preparation and publishes the resulting
|
||||||
|
/// canonical readiness snapshot to lifecycle diagnostics.
|
||||||
|
/// </summary>
|
||||||
|
public WorldRevealReadinessSnapshot PrepareAndEvaluate(uint destinationCell)
|
||||||
|
{
|
||||||
|
_readiness.Prepare(destinationCell);
|
||||||
|
return Evaluate(destinationCell);
|
||||||
|
}
|
||||||
|
|
||||||
|
public WorldRevealReadinessSnapshot Evaluate(uint destinationCell)
|
||||||
|
{
|
||||||
|
WorldRevealReadinessSnapshot snapshot = _readiness.Evaluate(destinationCell);
|
||||||
|
_lifecycle.ObserveReadiness(snapshot);
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ObserveMaterialized() => _lifecycle.ObserveMaterialized();
|
||||||
|
|
||||||
|
public void ObserveWorldViewportVisible() =>
|
||||||
|
_lifecycle.ObserveWorldViewportVisible();
|
||||||
|
|
||||||
|
public void Complete() => _lifecycle.Complete();
|
||||||
|
|
||||||
|
public void Cancel() => _lifecycle.Cancel();
|
||||||
|
}
|
||||||
117
tests/AcDream.App.Tests/Streaming/WorldRevealCoordinatorTests.cs
Normal file
117
tests/AcDream.App.Tests/Streaming/WorldRevealCoordinatorTests.cs
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
using AcDream.App.Streaming;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Streaming;
|
||||||
|
|
||||||
|
public sealed class WorldRevealCoordinatorTests
|
||||||
|
{
|
||||||
|
private sealed class State
|
||||||
|
{
|
||||||
|
public bool RenderReady { get; set; }
|
||||||
|
public bool CollisionReady { get; set; }
|
||||||
|
public bool CompositesReady { get; set; }
|
||||||
|
public int InvalidateCount { get; private set; }
|
||||||
|
public int PrepareCount { get; private set; }
|
||||||
|
|
||||||
|
public WorldRevealCoordinator Build(List<string>? logs = null) => new(
|
||||||
|
isRenderNeighborhoodReady: (_, _) => RenderReady,
|
||||||
|
isSpawnCellReady: _ => CollisionReady,
|
||||||
|
isTerrainNeighborhoodReady: (_, _) => CollisionReady,
|
||||||
|
areCompositeTexturesReady: () => CompositesReady,
|
||||||
|
prepareCompositeTextures: (_, _) => PrepareCount++,
|
||||||
|
invalidateCompositeTextures: () =>
|
||||||
|
{
|
||||||
|
InvalidateCount++;
|
||||||
|
CompositesReady = false;
|
||||||
|
},
|
||||||
|
isSpawnClaimUnhydratable: _ => false,
|
||||||
|
log: logs is null ? null : new Action<string>(logs.Add));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Begin_InvalidatesPriorDestinationAndStartsOneGeneration()
|
||||||
|
{
|
||||||
|
var state = new State { CompositesReady = true };
|
||||||
|
WorldRevealCoordinator coordinator = state.Build();
|
||||||
|
|
||||||
|
long generation = coordinator.Begin(WorldRevealKind.Login);
|
||||||
|
|
||||||
|
Assert.Equal(1, generation);
|
||||||
|
Assert.Equal(1, state.InvalidateCount);
|
||||||
|
Assert.False(state.CompositesReady);
|
||||||
|
Assert.Equal(WorldRevealKind.Login, coordinator.Snapshot.Kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PrepareAndEvaluate_UsesOneCanonicalSnapshotForLifecycle()
|
||||||
|
{
|
||||||
|
const uint outdoorCell = 0x11340021u;
|
||||||
|
var state = new State
|
||||||
|
{
|
||||||
|
RenderReady = true,
|
||||||
|
CollisionReady = true,
|
||||||
|
};
|
||||||
|
WorldRevealCoordinator coordinator = state.Build();
|
||||||
|
coordinator.Begin(WorldRevealKind.Login);
|
||||||
|
|
||||||
|
WorldRevealReadinessSnapshot first = coordinator.PrepareAndEvaluate(outdoorCell);
|
||||||
|
state.CompositesReady = true;
|
||||||
|
WorldRevealReadinessSnapshot ready = coordinator.PrepareAndEvaluate(outdoorCell);
|
||||||
|
|
||||||
|
Assert.False(first.IsReady);
|
||||||
|
Assert.True(ready.IsReady);
|
||||||
|
Assert.Equal(2, state.PrepareCount);
|
||||||
|
Assert.Equal(ready, coordinator.Snapshot.Readiness);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PortalLifecycle_CountsMaterializationAndCompletesVisibleGeneration()
|
||||||
|
{
|
||||||
|
const uint outdoorCell = 0x3032001Cu;
|
||||||
|
var logs = new List<string>();
|
||||||
|
var state = new State
|
||||||
|
{
|
||||||
|
RenderReady = true,
|
||||||
|
CollisionReady = true,
|
||||||
|
CompositesReady = true,
|
||||||
|
};
|
||||||
|
WorldRevealCoordinator coordinator = state.Build(logs);
|
||||||
|
coordinator.Begin(WorldRevealKind.Portal);
|
||||||
|
state.CompositesReady = true;
|
||||||
|
|
||||||
|
Assert.True(coordinator.Evaluate(outdoorCell).IsReady);
|
||||||
|
coordinator.ObserveMaterialized();
|
||||||
|
coordinator.ObserveMaterialized();
|
||||||
|
coordinator.ObserveWorldViewportVisible();
|
||||||
|
coordinator.Complete();
|
||||||
|
|
||||||
|
Assert.Equal(1, coordinator.PortalMaterializationCount);
|
||||||
|
Assert.True(coordinator.Snapshot.Materialized);
|
||||||
|
Assert.True(coordinator.Snapshot.WorldViewportObserved);
|
||||||
|
Assert.True(coordinator.Snapshot.Completed);
|
||||||
|
Assert.Equal(0, coordinator.Snapshot.InvariantFailureCount);
|
||||||
|
Assert.Single(logs, line => line.Contains("event=materialized", StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Cancel_PreventsLateDestinationWorkFromMutatingLifecycle()
|
||||||
|
{
|
||||||
|
var state = new State
|
||||||
|
{
|
||||||
|
RenderReady = true,
|
||||||
|
CollisionReady = true,
|
||||||
|
CompositesReady = true,
|
||||||
|
};
|
||||||
|
WorldRevealCoordinator coordinator = state.Build();
|
||||||
|
coordinator.Begin(WorldRevealKind.Portal);
|
||||||
|
coordinator.Cancel();
|
||||||
|
|
||||||
|
coordinator.PrepareAndEvaluate(0x11340021u);
|
||||||
|
coordinator.ObserveMaterialized();
|
||||||
|
coordinator.ObserveWorldViewportVisible();
|
||||||
|
coordinator.Complete();
|
||||||
|
|
||||||
|
Assert.True(coordinator.Snapshot.Cancelled);
|
||||||
|
Assert.Equal(default, coordinator.Snapshot.Readiness);
|
||||||
|
Assert.Equal(0, coordinator.PortalMaterializationCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue