refactor(streaming): extract the update frame

This commit is contained in:
Erik 2026-07-22 01:01:04 +02:00
parent 4e4aac2c5a
commit 0bc9fda9de
13 changed files with 912 additions and 184 deletions

View file

@ -16,7 +16,7 @@ namespace AcDream.App.Streaming;
/// Threading: not thread-safe. All calls must happen on the render thread.
/// </remarks>
/// </summary>
public sealed class StreamingController
public sealed class StreamingController : IStreamingFrameBackend
{
private sealed class OriginRecenterRetirement
{

View file

@ -0,0 +1,227 @@
using System.Numerics;
using AcDream.App.Input;
using AcDream.App.Net;
using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Update;
using AcDream.App.World;
using AcDream.Core.Physics;
using AcDream.Core.World;
using AcDream.Core.World.Cells;
namespace AcDream.App.Streaming;
internal interface IStreamingOriginConvergence
{
bool Advance();
}
internal interface IStreamingFrameBackend
{
void Tick(int observerCx, int observerCy, bool insideDungeon = false);
}
internal interface IOfflineStreamingObserverSource
{
Vector3 Position { get; }
}
internal sealed class FlyCameraStreamingObserverSource
: IOfflineStreamingObserverSource
{
private readonly CameraController _camera;
public FlyCameraStreamingObserverSource(CameraController camera) =>
_camera = camera ?? throw new ArgumentNullException(nameof(camera));
public Vector3 Position => _camera.Fly.Position;
}
internal readonly record struct StreamingDungeonCellSnapshot(
uint CellId,
bool IsSealedDungeon);
internal interface IStreamingDungeonCellSource
{
StreamingDungeonCellSnapshot Capture();
}
internal sealed class PhysicsStreamingDungeonCellSource
: IStreamingDungeonCellSource
{
private readonly PhysicsEngine _physics;
public PhysicsStreamingDungeonCellSource(PhysicsEngine physics) =>
_physics = physics ?? throw new ArgumentNullException(nameof(physics));
public StreamingDungeonCellSnapshot Capture()
{
ObjCell? cell = _physics.DataCache?.CellGraph.CurrCell;
return new StreamingDungeonCellSnapshot(
cell?.Id ?? 0u,
cell is EnvCell envCell && !envCell.SeenOutside);
}
}
internal interface ILiveProjectionRescueRebucketter
{
void RebucketAll(int observerCx, int observerCy);
}
internal sealed class LiveProjectionRescueRebucketter
: ILiveProjectionRescueRebucketter
{
private readonly GpuWorldState _worldState;
private readonly LiveEntityRuntime _liveEntities;
public LiveProjectionRescueRebucketter(
GpuWorldState worldState,
LiveEntityRuntime liveEntities)
{
_worldState = worldState ?? throw new ArgumentNullException(nameof(worldState));
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
}
public void RebucketAll(int observerCx, int observerCy)
{
List<WorldEntity> rescued = _worldState.DrainRescued();
if (rescued.Count == 0)
return;
uint centerLandblock =
((uint)observerCx << 24) | ((uint)observerCy << 16) | 0xFFFFu;
foreach (WorldEntity entity in rescued)
_liveEntities.RebucketLiveEntity(entity.ServerGuid, centerLandblock);
}
}
/// <summary>
/// Owns the complete pre-input streaming phase of one host update: retained
/// origin convergence, readiness, observer selection, sealed-dungeon collapse,
/// completion publication, and persistent-projection rescue.
/// </summary>
internal sealed class StreamingFrameController : IStreamingFramePhase
{
private const float LandblockLength = 192f;
private readonly bool _liveModeEnabled;
private readonly ILocalPlayerModeSource _playerMode;
private readonly ILocalPlayerControllerSource _playerController;
private readonly ILiveInWorldSource _session;
private readonly LiveWorldOriginState _origin;
private readonly ILocalPlayerLandblockSource _playerLandblock;
private readonly IOfflineStreamingObserverSource _offlineObserver;
private readonly IStreamingDungeonCellSource _dungeonCell;
private readonly IStreamingOriginConvergence _originConvergence;
private readonly IStreamingFrameBackend _streaming;
private readonly ILiveProjectionRescueRebucketter _rescues;
public StreamingFrameController(
bool liveModeEnabled,
ILocalPlayerModeSource playerMode,
ILocalPlayerControllerSource playerController,
ILiveInWorldSource session,
LiveWorldOriginState origin,
ILocalPlayerLandblockSource playerLandblock,
IOfflineStreamingObserverSource offlineObserver,
IStreamingDungeonCellSource dungeonCell,
IStreamingOriginConvergence originConvergence,
IStreamingFrameBackend streaming,
ILiveProjectionRescueRebucketter rescues)
{
_liveModeEnabled = liveModeEnabled;
_playerMode = playerMode ?? throw new ArgumentNullException(nameof(playerMode));
_playerController = playerController
?? throw new ArgumentNullException(nameof(playerController));
_session = session ?? throw new ArgumentNullException(nameof(session));
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
_playerLandblock = playerLandblock
?? throw new ArgumentNullException(nameof(playerLandblock));
_offlineObserver = offlineObserver
?? throw new ArgumentNullException(nameof(offlineObserver));
_dungeonCell = dungeonCell ?? throw new ArgumentNullException(nameof(dungeonCell));
_originConvergence = originConvergence
?? throw new ArgumentNullException(nameof(originConvergence));
_streaming = streaming ?? throw new ArgumentNullException(nameof(streaming));
_rescues = rescues ?? throw new ArgumentNullException(nameof(rescues));
}
public void Tick()
{
// A pending teleport/session origin retirement must converge even while
// ordinary streaming is gated. This remains the first streaming edge.
_originConvergence.Advance();
bool liveInWorld = _session.IsInWorld;
// #192: InWorld precedes the player's first authoritative Position.
// Starting an async build against the placeholder origin bakes stale
// offsets that can publish after the real origin arrives, so live
// startup requires both edges. A prior chase lifetime remains open for
// intentional later fly-mode observation, exactly as before extraction.
if (!StreamingReadinessGate.ShouldStream(
_liveModeEnabled,
_playerMode.ChaseModeEverEntered,
liveInWorld,
_origin.IsKnown))
{
return;
}
(int observerCx, int observerCy) = SelectObserver(liveInWorld);
PlayerMovementController? controller = _playerController.Controller;
bool isTeleportHold = controller is { State: PlayerState.PortalSpace };
StreamingDungeonCellSnapshot currentCell = _dungeonCell.Capture();
DungeonGateResult gate = DungeonStreamingGate.Compute(
isTeleportHold,
currentCell.IsSealedDungeon,
currentCell.CellId);
if (gate.ObserverLandblockKey is { } cellLandblock)
{
observerCx = (int)((cellLandblock >> 8) & 0xFFu);
observerCy = (int)(cellLandblock & 0xFFu);
}
_streaming.Tick(observerCx, observerCy, gate.InsideDungeon);
_rescues.RebucketAll(observerCx, observerCy);
}
private (int X, int Y) SelectObserver(bool liveInWorld)
{
int observerCx = _origin.CenterX;
int observerCy = _origin.CenterY;
PlayerMovementController? controller = _playerController.Controller;
if (_playerMode.IsPlayerMode
&& controller is { State: PlayerState.PortalSpace })
{
// The player root remains frozen in the source coordinate frame.
// The accepted shared origin already names the destination.
return (observerCx, observerCy);
}
if (_playerMode.IsPlayerMode && controller is not null)
{
Vector3 position = controller.Position;
return (
observerCx + (int)Math.Floor(position.X / LandblockLength),
observerCy + (int)Math.Floor(position.Y / LandblockLength));
}
if (liveInWorld)
{
if (_playerLandblock.LastKnownLandblockId is { } landblockId)
{
return (
(int)((landblockId >> 24) & 0xFFu),
(int)((landblockId >> 16) & 0xFFu));
}
return (observerCx, observerCy);
}
Vector3 cameraPosition = _offlineObserver.Position;
return (
observerCx + (int)Math.Floor(cameraPosition.X / LandblockLength),
observerCy + (int)Math.Floor(cameraPosition.Y / LandblockLength));
}
}

View file

@ -9,7 +9,7 @@ namespace AcDream.App.Streaming;
/// while a session boundary releases the controller without recentering. The
/// transaction remains pending across frames when an owner teardown needs retry.
/// </summary>
internal sealed class StreamingOriginRecenterCoordinator
internal sealed class StreamingOriginRecenterCoordinator : IStreamingOriginConvergence
{
private readonly record struct Request(
int DestinationX,