fix(world): remove non-retail portal exit fade

This commit is contained in:
Erik 2026-07-15 23:20:52 +02:00
parent 842bd89c16
commit dded9e6b17
21 changed files with 919 additions and 347 deletions

View file

@ -118,7 +118,7 @@ public sealed class StreamingController
/// so the player arrives to a near-empty world (the "Fort Tethana only one landblock
/// loaded" symptom) and their cell-walk can't root into neighbour cells yet (the
/// transient walk-through-walls). The far ring still drains at the budget. The eager
/// apply runs during the teleport hold (behind the fade), so the GPU spike is hidden.
/// apply runs while the portal viewport replaces the world, so the GPU spike is hidden.
/// </summary>
public int PriorityRadius { get; set; }
@ -377,7 +377,7 @@ public sealed class StreamingController
/// SYNCHRONOUSLY drop every resident landblock (render slot + physics + state) so none
/// survives into the next frame stale, and no async unload can race a re-bake, then null
/// the region so the next <see cref="NormalTick"/> re-bootstraps the WHOLE window fresh at
/// the new origin (the near ring is priority-applied behind the fade; the rest streams in).
/// the new origin (the near ring is priority-applied during portal travel; the rest streams in).
/// A sealed-dungeon destination uses <see cref="PreCollapseToDungeon"/> instead.
/// </summary>
public void ForceReloadWindow()
@ -405,7 +405,7 @@ public sealed class StreamingController
/// (rendering at its old world position as "floating terrain at the horizon"), and rapid
/// hops accumulated them faster than they cleared — a runaway resident count (951 observed
/// vs a 625 window) that also dragged FPS. Loads inside the teleport near ring
/// (<see cref="PriorityRadius"/>, applied behind the fade) likewise bypass the budget so the
/// (<see cref="PriorityRadius"/>, applied during portal travel) likewise bypass the budget so the
/// player materialises in a loaded world.
/// </summary>
private void DrainAndApply()

View file

@ -1,9 +1,11 @@
namespace AcDream.App.Streaming;
/// <summary>
/// Classifies whether a local-player teleport crosses an AC landblock boundary.
/// Cell identity comes from the complete retail <c>Position</c>, never from its
/// frame coordinates: dungeon EnvCells may have valid negative local origins.
/// Classifies both whether the player crosses an AC landblock boundary and
/// whether acdream's asynchronous streaming origin must recenter. These are
/// distinct while a teleport destination is aimed but not yet placed. Cell
/// identity comes from the complete retail <c>Position</c>, never from its frame
/// coordinates: dungeon EnvCells may have valid negative local origins.
/// </summary>
/// <remarks>
/// Retail carries <c>Position::objcell_id</c> through
@ -14,9 +16,12 @@ namespace AcDream.App.Streaming;
/// </remarks>
internal readonly record struct TeleportLandblockTransition(
uint SourceLandblockId,
uint DestinationLandblockId)
uint DestinationLandblockId,
uint StreamingCenterLandblockId)
{
public bool CrossesLandblock => SourceLandblockId != DestinationLandblockId;
public bool ChangesStreamingCenter =>
StreamingCenterLandblockId != DestinationLandblockId;
/// <summary>
/// Build the transition from the player's current cell and the received
@ -34,7 +39,8 @@ internal readonly record struct TeleportLandblockTransition(
return new TeleportLandblockTransition(
sourceLandblockId,
NormalizeLandblockId(destinationCellId));
NormalizeLandblockId(destinationCellId),
NormalizeLandblockId(currentStreamingCenterLandblockId));
}
private static uint NormalizeLandblockId(uint cellOrLandblockId)

View file

@ -0,0 +1,150 @@
namespace AcDream.App.Streaming;
/// <summary>
/// Correlates F751 notifications with accepted Position packets without
/// imposing presentation order on canonical physics. F751 deliberately does
/// not consume retail's TELEPORT_TS, and the matching Position may be observed
/// on either side of the notification.
/// </summary>
internal sealed class TeleportTransitCoordinator<TDestination>
where TDestination : struct
{
private bool _hasLastStart;
private ushort _lastStartSequence;
private readonly Dictionary<ushort, TDestination> _bufferedDestinations = new();
private bool _hasPendingStart;
private ushort _pendingStartSequence;
private bool _destinationAccepted;
public bool IsActive { get; private set; }
public ushort Sequence { get; private set; }
public bool HasPendingStart => _hasPendingStart;
/// <summary>Reject retransmitted or older F751 notifications.</summary>
public bool CanBegin(ushort sequence) =>
!_hasLastStart || IsNewer(_lastStartSequence, sequence);
/// <summary>
/// Record a fresh notification lifetime. Presentation activation is a
/// separate edge because acdream's optional developer camera can
/// temporarily withdraw the normal player projection.
/// </summary>
public bool QueueStart(ushort sequence)
{
if (!CanBegin(sequence))
return false;
PruneDestinationsOlderThan(sequence);
IsActive = false;
Sequence = 0;
_hasPendingStart = true;
_pendingStartSequence = sequence;
_hasLastStart = true;
_lastStartSequence = sequence;
_destinationAccepted = false;
return true;
}
/// <summary>
/// Activate the queued presentation and consume a destination that was
/// accepted before the player projection became available.
/// </summary>
public bool Activate(out TDestination bufferedDestination)
{
bufferedDestination = default;
if (!_hasPendingStart)
return false;
IsActive = true;
Sequence = _pendingStartSequence;
_hasPendingStart = false;
_pendingStartSequence = 0;
_destinationAccepted = false;
if (!_bufferedDestinations.Remove(Sequence, out bufferedDestination))
return false;
_destinationAccepted = true;
return true;
}
/// <summary>
/// Offer an accepted Position packet. A matching active transit consumes
/// its first packet even when TELEPORT_TS was already advanced. Without a
/// matching notification, only a timestamp-advancing packet is buffered;
/// ordinary movement can never become a future portal destination.
/// </summary>
public bool OfferDestination(
ushort sequence,
bool teleportTimestampAdvanced,
TDestination destination,
out TDestination acceptedDestination)
{
acceptedDestination = default;
// An advancing TELEPORT_TS makes every older destination impossible
// to match a future F751. Retire those generations now so a skipped
// notification cannot retain a stale destination through u16 wrap.
if (teleportTimestampAdvanced)
PruneDestinationsOlderThan(sequence);
if (IsActive && sequence == Sequence)
{
if (_destinationAccepted)
return false;
_destinationAccepted = true;
acceptedDestination = destination;
return true;
}
if (_hasPendingStart && sequence == _pendingStartSequence)
{
_bufferedDestinations.TryAdd(sequence, destination);
return false;
}
ushort correlationSequence = _hasPendingStart
? _pendingStartSequence
: Sequence;
bool mayBelongToFutureStart = (!IsActive && !_hasPendingStart)
|| IsNewer(correlationSequence, sequence);
if (!teleportTimestampAdvanced || !mayBelongToFutureStart)
return false;
_bufferedDestinations.TryAdd(sequence, destination);
return false;
}
/// <summary>End presentation while retaining duplicate and reorder history.</summary>
public void EndActive()
{
IsActive = false;
Sequence = 0;
_hasPendingStart = false;
_pendingStartSequence = 0;
_destinationAccepted = false;
}
/// <summary>Clear every correlation value at a network-session boundary.</summary>
public void ClearSession()
{
EndActive();
_hasLastStart = false;
_lastStartSequence = 0;
_bufferedDestinations.Clear();
}
private static bool IsNewer(ushort current, ushort incoming)
=> AcDream.Core.Physics.PhysicsTimestampGate.IsNewer(current, incoming);
private void PruneDestinationsOlderThan(ushort sequence)
{
foreach (ushort bufferedSequence in _bufferedDestinations.Keys.ToArray())
{
if (IsNewer(bufferedSequence, sequence))
_bufferedDestinations.Remove(bufferedSequence);
}
}
}