refactor(streaming): extract the update frame
This commit is contained in:
parent
4e4aac2c5a
commit
0bc9fda9de
13 changed files with 912 additions and 184 deletions
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
227
src/AcDream.App/Streaming/StreamingFrameController.cs
Normal file
227
src/AcDream.App/Streaming/StreamingFrameController.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue