acdream/src/AcDream.App/Streaming/GraphicalRemotePlacementServiceWindow.cs
Erik 2e8e09acd0 feat(physics): C4 route 4b-1 — remote placement infrastructure (dormant)
Builds the machinery route 4b-2 and 4b-3 will flip on, and changes no remote
behaviour: it has no production caller, so RemotePlacementDrivePendingCount is
provably 0 and IsConverged is unchanged.

Five pieces: a per-entity remote placement owner (RuntimeRemotePlacementDriveController),
a Position-time service-window guard with a Runtime interface plus BOTH host
implementations, N3's headless RetryPending pump, parked-count observability in
the ownership ledger, and the service-window optimisation that avoids parks we
can cheaply predict.

Landed alone because it is where the park-withdraws-the-entity failure was
decided; that decision is fixed at the source in the preceding commit and must
not share a review signal with a behaviour flip.

Two parts of route 2's controller are deliberately NOT ported, both verified
against retail rather than assumed. There is no ack: SendPositionEvent is called
only inside HandleReceivedPosition's local-player FORCE_POSITION gate
@0x0045400C-@0x00454091, and the remote arm @0x0045414D has no equivalent. There
is no re-issue funnel: retail never re-attempts a position it could not apply —
stale timestamps merely bump error_count @0x004542AC — and re-issuing packet N
after N+1 has merged would apply a pose the newer packet already superseded,
which is correct for a one-shot ForcePosition and wrong for a 5-10 Hz stream.

The service-window guard is an OPTIMISATION, not the correctness mechanism. The
original contract had it the other way round, justified by a claim that retail
cannot represent "arrived but not placeable" — false, and corrected in the
review findings: retail's GotoLostCell/reenter_visibility path represents it
exactly. A pre-flight guard also cannot be complete, because Core defers on the
entity's CURRENT cell, on the swept QueriedCellIds footprint spanning
neighbouring landblocks, and on residency evaluated after AdjustToOutside —
conditions only Core can see.

Review found and this commit fixes: DetachRoute cleared two maps of LIVE Core
operations without cancelling them (route 2's AbandonPending is the correct
mirror, not the first-entry controller) and its test asserted that blindness as
convergence; the headless predicate answered "can ever publish" rather than "is
published", and after the first fix still matched only 1 of the 9 landblocks
this host publishes; OwnsPlacement admitted remote top-level Creates until
gated on the Teleport flag as well as the disposition; Advance re-submitted
without re-checking the window; and four comments cited a report that did not
exist.

Contract item 6 is met by the structural proof, not the earlier test:
HasOldPrefixPlacementDebt refuses collision-prefix mutation permission before
ParkCollisionResidents is ever entered, so its overlap throw is unreachable.
That same mechanism is the unbounded stall filed as #310, which 4b-1 does not
bound — it only avoids widening it.

#311 files the remaining per-tick allocation in RetryPendingProjections; the
early-out for the empty-FIFO case landed via a new HasPendingReceipts accessor
so hosts still never touch .Placements. directly.

Gates: complete Release solution 10,973 passed / 4 skipped / 0 failed (baseline
10,938). Four review rounds; every fix discrimination-verified by revert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 04:08:19 +02:00

91 lines
4.7 KiB
C#

using AcDream.Runtime.Session;
namespace AcDream.App.Streaming;
/// <summary>
/// C4 route 4b-1: the graphical host's
/// <see cref="IRuntimeRemotePlacementServiceWindow"/> — headless already had
/// one (<c>HeadlessCollisionNeighborhood.IsWithinServiceWindow</c>,
/// <c>HeadlessSessionWorldProjection.cs:27</c>/<c>:245-257</c>); the
/// graphical host had none.
/// </summary>
/// <remarks>
/// <para>
/// <b>Predicate choice — <see cref="GpuWorldState.IsNearTier"/>, not
/// <see cref="GpuWorldState.IsNearTierOrPending"/>.</b> The contract asks
/// whether near-tier residency is exactly co-extensive with collision
/// publication and, if not, to propose the correct predicate rather than
/// ship the assumption. It is not — for the "pending" half specifically:
/// <c>IsNearTierOrPending</c> is true for a landblock that has only been
/// PARKED as a pending near-tier entity bucket
/// (<c>GpuWorldState.CommitEntitiesToExistingLandblockSpatialCore</c>'s
/// <c>parkIfMissing</c> branch, <c>:1919-1952</c>) — i.e. queued, not yet
/// collision-published. Using it here would let this guard say "go" for a
/// destination whose collision generation has not committed, reopening
/// exactly the DeferredCell park this route exists to prevent.
/// <see cref="GpuWorldState.IsNearTier"/> alone is the correct predicate:
/// </para>
/// <para>
/// <b>Promotion direction (verified by reading, not assumed):</b> the only
/// two writers of <c>_tierByLandblock[...] = LandblockStreamTier.Near</c> are
/// <c>GpuWorldState.CommitLandblockSpatialCore</c> (:969) and
/// <c>CommitEntitiesToExistingLandblockSpatialCore</c> (:1972), both called
/// from <c>LandblockPresentationPipeline.Advance</c>'s
/// <c>SpatialPresentationCommitted</c> stage — which runs strictly AFTER the
/// preceding <c>PresentationCommitted</c> stage, the one that drives
/// <c>LandblockPhysicsPublisher</c>'s staged collision/EnvCell advance to
/// completion (<c>LandblockPresentationPipeline.cs</c> ~826-935). A
/// landblock's tier cannot read Near before its collision has committed.
/// </para>
/// <para>
/// <b>Retirement direction (verified by reading, not assumed):</b>
/// <c>GpuWorldState.DetachNearLayer</c> (:1755-1834) flips the tier to Far as
/// the FIRST, synchronous step of a "detach-first" landblock retirement —
/// <c>LandblockRetirementCoordinator.AdoptDetachedFull</c>'s own doc comment
/// states spatial detachment has already committed by the time its retirement
/// ticket is constructed, and that ticket's <c>LandblockRetirementStage.Physics</c>
/// step (<c>_physics.AdvanceRemoval</c> → <c>RuntimeSetPositionState
/// .WithdrawCollision</c> → the collision-side retirement
/// <c>ParkCollisionResidents</c> can reach) runs strictly AFTER. A
/// landblock's tier cannot still read Near once its collision has begun
/// retiring.
/// </para>
/// <para>
/// <b>Residual, not closed:</b> a landblock-prefix collision MUTATION that is
/// not a full retirement (a live in-place quiescence/refresh while the tier
/// stays Near) is not ruled out by this reading and is not exercised by any
/// gate this route adds. <c>RuntimeSetPositionState</c>'s private
/// <c>TryGetBlockingQuiescence</c> (:3820-3863) is Core's own check for
/// exactly this case — an active <c>CollisionPrefixQuiescence</c> entry for
/// the destination's landblock prefix, independent of tier/residency — and
/// it is what a placement can still hit even after this guard passes,
/// producing the narrow <c>DeferredCell</c> residual
/// <see cref="AcDream.Runtime.Session.RuntimeRemotePlacementDriveController"/>'s
/// class doc describes.
/// </para>
/// </remarks>
internal sealed class GraphicalRemotePlacementServiceWindow
: IRuntimeRemotePlacementServiceWindow
{
private readonly GpuWorldState _state;
internal GraphicalRemotePlacementServiceWindow(GpuWorldState state)
{
_state = state ?? throw new ArgumentNullException(nameof(state));
}
/// <summary>
/// <paramref name="landblockId"/> is a full ACE cell id (landblock high
/// word + cell low word — <c>CreateObject.ServerPosition.LandblockId</c>
/// carries the full id despite its name; see
/// <c>RuntimeAcceptedPositionDriveControllerTests</c>'s own
/// <c>SourceCell = SourceLandblock | 0x0001u</c> fixture shape).
/// Canonicalized to <c>0xAAAAFFFF</c> the same way every
/// <see cref="GpuWorldState"/> tier writer/reader does
/// (<c>DetachNearLayer</c>, <c>CommitLandblockSpatialCore</c>) before the
/// dictionary lookup — <see cref="GpuWorldState.IsNearTier"/> itself does
/// not canonicalize its argument.
/// </summary>
public bool IsWithinServiceWindow(uint landblockId) =>
_state.IsNearTier((landblockId & 0xFFFF0000u) | 0xFFFFu);
}