From 63c601ff4d4cb345c1011a3c5b4e8755684ccd24 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 2 Aug 2026 07:43:11 +0200 Subject: [PATCH] perf(runtime): halve accepted-placement allocations via pooled operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cutover slice C2: the dormant placement path's per-operation cost was the recorded activation blocker for routing frame-frequency traffic through the canonical SetPosition owner (1,880 B/op measured at 4B2, cap 2,048). Root-cause removal, not a raised cap: the per-operation envelope is now pooled (bounded 64, reset-at-rent, InPool double-retire guard, cleared on session reset/dispose and surfaced as a diagnostic ownership count), the two engine-callback closures became one cached delegate over an explicit context stack, and the pending-projection head read no longer boxes the sorted enumerator. Measured 2,032 -> 944 B/op; the regression gate tightens to 1,536. The residual floor is documented at the gate: ~520 B inside Core's PhysicsEngine.SetPosition (outside this slice's scope) and ~208 B of sorted-tree node per pending receipt. Pooling demanded — and received — the full staleness-discipline rework: every frame holding an operation across a reentrancy point now captures its never-reissued token and revalidates via fresh lookup (IsCurrentByToken / token-shaped CancelCore), because a recycled instance reinstalled at the same key makes every reference-identity check a tautology. All ~26 sites audited (15 remain reference-based with per-site no-reentrancy proofs); CommitCanonical's post-callback reads are hoisted stack locals mirroring retail's savedTransientState pattern (handle_all_collisions bits, pseudo-C 283952), its bookkeeping writes are token-gated, and the settle path stays deliberately identity- agnostic because retail's SetPositionInternal runs its physical settle unconditionally even for displaced operations. Reviewed: retail-conformance PASS + architecture/adversarial PASS after two fix rounds (the ground-edge recycle window, the pool's cross-reset retention, the class-wide tautology, a self-found snapshot-reference iteration hazard). Runtime 927/927; complete Release solution 10,722 passed / 4 intentional skips; budget test green at the tightened gate. Co-Authored-By: Claude Fable 5 --- .../Physics/RuntimeSetPositionState.cs | 1137 ++++++++++++++--- .../Physics/RuntimeSetPositionStateTests.cs | 416 +++++- 2 files changed, 1362 insertions(+), 191 deletions(-) diff --git a/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs b/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs index 68efb27f..7a285b24 100644 --- a/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs +++ b/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Numerics; using AcDream.Content; using AcDream.Core.Items; @@ -265,7 +266,8 @@ internal readonly record struct RuntimeSetPositionOwnershipSnapshot( int PlacementCompletionWatchCount, int AcknowledgedPlacementCompletionCount, int CollisionPrefixQuiescenceCount, - int PendingQuiescenceProjectionCount) + int PendingQuiescenceProjectionCount, + int PooledOperationCount) { internal bool IndexesConsistent => LostDeadlineCount == LostDeadlineNodeCount @@ -275,6 +277,16 @@ internal readonly record struct RuntimeSetPositionOwnershipSnapshot( && UnboundDeferredCellCount == UnboundDeferredCellOrderCount && MoverPreparationAuthorityCount <= ActiveOperationCount; + /// + /// F2: deliberately EXCLUDED from . + /// is retained, idle Operation + /// capacity in the C2 object pool - legitimate to hold mid-session (that + /// is the entire point of pooling), so it must not make an otherwise + /// fully-drained, healthy session read as "not converged". Reset/dispose + /// tests assert it separately drops to zero + /// (OperationPoolClearsOnResetSession / + /// OperationPoolClearsOnDispose) instead of folding it into this gate. + /// internal bool IsConverged => ActiveOperationCount == 0 && AwaitingPreparationCount == 0 && DeferredCellCount == 0 @@ -334,19 +346,33 @@ internal sealed class RuntimeSetPositionState : IDisposable bool Prepared, RuntimeSetPositionCommand PreparedCommand); + /// + /// C2: settable (not init/required) so + /// / can recycle instances via + /// instead of allocating a fresh + /// object on every accepted placement. This was the single largest + /// contributor to the C2 allocation budget finding (see + /// docs/research/2026-07-31-canonical-set-position.md). Every + /// construction site (BeginAcceptedPlacementCore, + /// CreateWithdrawalOperation, ParkDeferred) still sets + /// every field it always set before; the only behavior change is that a + /// field left unset by a given site now comes from an explicit reset + /// instead of the CLR's implicit new-object default - the two are + /// identical in value. + /// private sealed class Operation { - internal required RuntimeEntityRecord Record { get; init; } + internal RuntimeEntityRecord Record { get; set; } = null!; internal PhysicsBody? Body { get; set; } - internal required RuntimeEntityPlacementToken Token { get; init; } - internal required RuntimeEntityKey Key { get; init; } - internal required ulong PositionAuthorityVersion { get; init; } - internal required ulong SessionLifetimeVersion { get; init; } - internal required ulong SourceSpatialAuthorityVersion { get; init; } - internal required ulong SourceVelocityAuthorityVersion { get; set; } - internal required bool PreviousContact { get; set; } - internal required bool PreviousOnWalkable { get; set; } - internal required RuntimeSetPositionCommand Command { get; set; } + internal RuntimeEntityPlacementToken Token { get; set; } + internal RuntimeEntityKey Key { get; set; } + internal ulong PositionAuthorityVersion { get; set; } + internal ulong SessionLifetimeVersion { get; set; } + internal ulong SourceSpatialAuthorityVersion { get; set; } + internal ulong SourceVelocityAuthorityVersion { get; set; } + internal bool PreviousContact { get; set; } + internal bool PreviousOnWalkable { get; set; } + internal RuntimeSetPositionCommand Command { get; set; } internal PhysicsSetPositionResult Result { get; set; } internal ulong SpatialAuthorityVersion { get; set; } internal ulong PlacementCommitVersion { get; set; } @@ -359,8 +385,8 @@ internal sealed class RuntimeSetPositionState : IDisposable internal ulong ProjectionSequence { get; set; } internal bool WakeableLostCell { get; set; } internal RuntimeEntityPlacementStage Stage { get; set; } - internal RuntimeSetPositionOperationKind Kind { get; init; } - internal RuntimePortalPlacementAuthority Portal { get; init; } + internal RuntimeSetPositionOperationKind Kind { get; set; } + internal RuntimePortalPlacementAuthority Portal { get; set; } internal bool RequiresPreparation { get; set; } internal bool Expired { get; set; } internal List? LostFamilyKeys { get; set; } @@ -372,6 +398,69 @@ internal sealed class RuntimeSetPositionState : IDisposable get; set; } + + /// + /// F3: pool-membership guard, not operation data. Set true by + /// right before pushing; cleared + /// here (called from right after + /// popping). Lets RetireOperationToPool detect and throw on a + /// double-retire - two retire calls for the same instance without an + /// intervening rent would otherwise silently duplicate it in the + /// pool stack. + /// + internal bool InPool { get; set; } + + /// + /// C2/F4: the ONLY place every field is set to its inert default - + /// this is the completeness net that `required`/`init` used to + /// provide before pooling needed plain settable properties. Called + /// exactly once when an Operation is retired to the pool + /// (), before it can be handed + /// back out by . is + /// left null! only for the instant the instance sits in the pool - + /// every rent site immediately overwrites it before any other + /// member is read. A reflection-based test + /// (OperationResetAllFieldsToDefaultTouchesEveryDeclaredField) pins + /// that this method assigns every declared instance field of this + /// class by name - a newly added field fails that test until both + /// the reset and the test's expected-field list are updated. + /// + internal void ResetAllFieldsToDefault() + { + Record = null!; + Body = null; + Token = default; + Key = default; + PositionAuthorityVersion = 0UL; + SessionLifetimeVersion = 0UL; + SourceSpatialAuthorityVersion = 0UL; + SourceVelocityAuthorityVersion = 0UL; + PreviousContact = false; + PreviousOnWalkable = false; + Command = default; + Result = default; + SpatialAuthorityVersion = 0UL; + PlacementCommitVersion = 0UL; + ExactCellId = 0u; + CollisionGeneration = 0UL; + CollisionPrefix = 0u; + WithdrawalAcknowledged = false; + CollisionGenerationReady = false; + CollisionQuiescenceHeld = false; + ProjectionSequence = 0UL; + WakeableLostCell = false; + Stage = default; + Kind = default; + Portal = default; + RequiresPreparation = false; + Expired = false; + LostFamilyKeys = null; + InheritedLostDeadline = false; + EnteringWorldFromCelllessResidence = false; + DormantLocalActivation = false; + PreparedCommandAwaitingWithdrawalAck = null; + InPool = false; + } } private sealed class CollisionPrefixQuiescence @@ -393,9 +482,24 @@ internal sealed class RuntimeSetPositionState : IDisposable internal bool ReleaseGenerationReady { get; set; } } + /// + /// F1: captures the operation's PositionAuthorityVersion/ + /// SpatialAuthorityVersion as PLAIN VALUES at construction time instead + /// of holding an reference and reading its + /// fields lazily inside . IsCurrent can run + /// DURING the ground-edge HitGround/LeaveGround callbacks this guard is + /// built for - a synchronous cancel-then-begin (or begin-twice) chain + /// for the SAME entity can retire this exact Operation instance to the + /// pool and rent it right back out (LIFO) for a different logical + /// operation before this guard is asked whether it is still current. A + /// live Operation reference would then silently read the WRONG + /// operation's authority versions; these captured scalars cannot be + /// repurposed out from under it. + /// private sealed class ContactCommitGuard( RuntimeSetPositionState owner, - Operation operation, + ulong positionAuthorityVersion, + ulong spatialAuthorityVersion, RuntimeEntityRecord record, PhysicsBody body, ulong placementCommitVersion, @@ -403,7 +507,8 @@ internal sealed class RuntimeSetPositionState : IDisposable { internal bool IsCurrent() => owner.IsCanonicalPlacementCommitCurrent( - operation, + positionAuthorityVersion, + spatialAuthorityVersion, record, body, placementCommitVersion, @@ -452,12 +557,163 @@ internal sealed class RuntimeSetPositionState : IDisposable double Deadline, ulong Sequence); + /// + /// C2: everything reads + /// to invoke + /// on behalf of the in-flight PhysicsEngine.SetPosition call. A + /// plain per-call lambda closing over the live + /// (and, at one call site, a local canonicalCommand) allocated a + /// fresh display class every accepted placement; this struct is pushed by + /// value onto instead so the ONE + /// cached delegate below never needs a new closure. A stack (not a single + /// field) survives any theoretical nested/re-entrant SetPosition call at + /// the PhysicsEngine layer - TransitionScratchArena.ActiveDepth/ + /// Capacity implies nesting is possible there even though + /// itself + /// never calls back into this class. + /// + private readonly record struct CollisionCallbackContext( + RuntimeEntityRecord Record, + ulong PositionAuthorityVersion, + ulong SpatialAuthorityVersion, + ulong VelocityAuthorityVersion, + double GameTime, + bool PreviousContact, + bool PreviousOnWalkable); + + private readonly Stack _collisionCallbackContexts + = new(4); + private readonly Func + _handleSetPositionCollisionsCallback; + + // C2: retired Operation instances wait here for reuse by + // BeginAcceptedPlacementCore instead of a fresh `new Operation` every + // accepted placement. Bounded so a pathological retirement/rent + // imbalance (e.g. many entities despawning with none spawning) cannot + // grow this into an unbounded retained cache - beyond the cap we simply + // let the retired instance become garbage, exactly like before pooling + // existed. + private const int MaxPooledOperations = 64; + private readonly Stack _operationPool = new(); + internal RuntimeSetPositionState( RuntimePhysicsState physics, RuntimeEntityDirectory entities) { _physics = physics ?? throw new ArgumentNullException(nameof(physics)); _entities = entities ?? throw new ArgumentNullException(nameof(entities)); + _handleSetPositionCollisionsCallback = + HandleSetPositionCollisionsCallback; + } + + /// + /// C2: the single cached delegate every accepted-placement SetPosition + /// call passes to PhysicsEngine.SetPosition in place of a fresh + /// per-call closure. Reads the innermost pushed + /// rather than capturing state + /// directly, so this delegate instance (created once in the + /// constructor) is reused for the lifetime of this owner. + /// + private bool HandleSetPositionCollisionsCallback( + PhysicsSetPositionCollisionReport report) + { + CollisionCallbackContext context = _collisionCallbackContexts.Peek(); + return _physics.HandleSetPositionCollisions( + context.Record, + context.PositionAuthorityVersion, + context.SpatialAuthorityVersion, + context.VelocityAuthorityVersion, + context.GameTime, + context.PreviousContact, + context.PreviousOnWalkable, + report); + } + + /// + /// C2: returns a pooled instance reset to inert defaults right here (NOT + /// at retirement - see for why that + /// ordering matters), or allocates a fresh instance exactly as + /// BeginAcceptedPlacementCore did before pooling existed. Callers + /// MUST set every field they previously set via object-initializer + /// syntax - a rented instance's unset fields are the SAME defaults a + /// brand-new instance would have (see + /// ), never leftover + /// state from a prior use. + /// + private Operation RentOperation() + { + if (_operationPool.Count == 0) + return new Operation(); + Operation pooled = _operationPool.Pop(); + pooled.ResetAllFieldsToDefault(); + return pooled; + } + + /// + /// C2: the only place an Operation is retired to the pool - called the + /// instant one is removed from for good. + /// Deliberately does NOT reset the instance here (that happens in + /// instead, right before reuse): a reentrant + /// callback chain can retire the operation the OUTER frame is still + /// executing inside of and needs to keep reading. + /// + /// Round 3 correction: an earlier revision of this comment claimed + /// IsCanonicalPlacementCommitCurrent additionally compared the + /// operation's Token to detect exactly this recycling. That claim was + /// wrong and the check it described was reverted - retail's + /// SetPositionInternal settle is UNCONDITIONAL, so an in-flight + /// ground-edge commit for an entity a reentrant cancel-then-begin has + /// since displaced must still complete its physical settle (contact + /// transition, collision reports, shadow sync) - see + /// ReentrantGroundEdgePlacementCannotBeCancelledByDisplacedOperation. A + /// Token/identity gate on the settle path itself would incorrectly abort + /// that commit the moment the entity is displaced. + /// + /// The ACTUAL safety mechanism, class-wide, is captured-token-vs- + /// fresh-lookup at every frame that holds an Operation reference across + /// a reentrancy point (a synchronous publish, a collision-report + /// dispatch that can reach an arbitrary + /// IRuntimeCollisionReportObserver, or a ground-edge HitGround/ + /// LeaveGround callback): capture operation.Token (globally + /// unique - checked(++_nextOperationId) is never reissued) into a + /// local BEFORE the reentrancy point, then after it re-resolve via a + /// fresh _operations.TryGetValue and compare + /// current.Token == capturedToken - never + /// ReferenceEquals/IsCurrent(Operation) against the + /// original reference, which becomes a tautology once pooling can hand + /// that SAME physical instance back out for a different logical + /// operation. This identity gate belongs at publication/cancellation/ + /// ownership decisions (see the token comparisons in + /// BeginAcceptedPlacementCore, SubmitPreparedPlacementCore, + /// RetryDeferred, and CommitCanonical's own bookkeeping- + /// write gate), never inside the settle/currency layer itself. + /// + /// F3 reorders BeginAcceptedPlacementCore to rent only AFTER this method + /// retires the displaced operation - every field this class still needs + /// from a displaced operation is captured into a local before that + /// retire point (never read from the possibly-the-same, freshly-reset + /// instance after), so it is safe, and more efficient, for a single- + /// entity churn cycle to hand the SAME instance right back out as the + /// next operation (LIFO). guards the one + /// invariant that ordering depends on: this method must never be called + /// twice for the same instance without an intervening + /// in between - that would silently + /// duplicate the instance in , so it throws + /// instead. + /// + private void RetireOperationToPool(Operation operation) + { + if (operation.InPool) + { + throw new InvalidOperationException( + "Operation was already retired to the pool - a double-retire " + + "without an intervening rent would duplicate it in the pool " + + "stack."); + } + if (_operationPool.Count >= MaxPooledOperations) + return; + operation.InPool = true; + _operationPool.Push(operation); } internal RuntimeSetPositionOwnershipSnapshot CaptureOwnership() @@ -501,7 +757,8 @@ internal sealed class RuntimeSetPositionState : IDisposable _placementCompletionWatches.Count, _acknowledgedPlacementCompletions.Count, _collisionPrefixQuiescence.Count, - pendingQuiescenceProjections); + pendingQuiescenceProjections, + _operationPool.Count); } internal int PendingProjectionCount => _pendingProjection.Count; @@ -922,6 +1179,8 @@ internal sealed class RuntimeSetPositionState : IDisposable in RuntimeEntityPlacementToken token) { EnsureNotDisposed(); + // Round 3 audit: safe - fresh lookup + Token check on the SAME line + // immediately precede IsCurrent, nothing reentrant in between. return token.IsValid && _operations.TryGetValue(token.Entity, out Operation? operation) && operation.Token == token @@ -998,7 +1257,11 @@ internal sealed class RuntimeSetPositionState : IDisposable { return default; } - return CancelCore(operation); + // Round 3: `token` is this call's own parameter, verified fresh + // against `_operations` immediately above with nothing reentrant in + // between - passing it straight through is equivalent to (and safer + // than) re-deriving it from `operation`. + return CancelCore(token.Entity, token); } internal RuntimeEntityPlacementToken TryBeginExclusiveAuthoredPlacement( @@ -1031,6 +1294,8 @@ internal sealed class RuntimeSetPositionState : IDisposable EnsureNotDisposed(); ArgumentNullException.ThrowIfNull(record); ArgumentNullException.ThrowIfNull(body); + // Round 3 audit: safe - fresh lookup + Token check earlier in this + // SAME guard clause precede IsCurrent, nothing reentrant in between. if (!token.IsValid || record.Key != token.Entity || !_operations.TryGetValue(token.Entity, out Operation? operation) @@ -1088,28 +1353,19 @@ internal sealed class RuntimeSetPositionState : IDisposable captureMoverPreparationAuthority ? RuntimeEntityPlacementPreparationKind.AuthoredMover : RuntimeEntityPlacementPreparationKind.LegacyDirect); - var replacement = new Operation - { - Record = record, - Token = token, - Key = key, - PositionAuthorityVersion = expectedPositionAuthorityVersion, - SessionLifetimeVersion = _entities.SessionLifetimeVersion, - SourceSpatialAuthorityVersion = record.SpatialAuthorityVersion, - SourceVelocityAuthorityVersion = record.VelocityAuthorityVersion, - PreviousContact = record.PhysicsBody?.InContact ?? false, - PreviousOnWalkable = record.PhysicsBody?.OnWalkable ?? false, - Command = default, - Result = default, - SpatialAuthorityVersion = record.SpatialAuthorityVersion, - PlacementCommitVersion = record.PlacementCommitVersion, - Stage = RuntimeEntityPlacementStage.AwaitingPreparation, - Kind = kind, - Portal = portal, - }; List? inheritedLostFamily = null; RuntimePlacementProjectionSnapshot? inheritedWithdrawal = null; bool inheritedWithdrawalAcknowledged = false; + // F3: captured into locals (rather than re-read from `displaced` + // after CancelCoreDeferred below) because `replacement` is + // deliberately rented AFTER that retire - see the no-self-aliasing + // note below and RetireOperationToPool's doc comment. Reading every + // scalar this method still needs before the retire point keeps this + // correct regardless of which physical instance `replacement` ends + // up being. + PhysicsSetPositionResult inheritedResult = default; + uint inheritedExactCellId = 0u; + ulong inheritedCollisionGeneration = 0UL; if (_operations.TryGetValue(key, out Operation? displaced) && (displaced.WakeableLostCell || displaced.InheritedLostDeadline)) @@ -1118,6 +1374,9 @@ internal sealed class RuntimeSetPositionState : IDisposable displaced.LostFamilyKeys = null; inheritedWithdrawalAcknowledged = displaced.WithdrawalAcknowledged; + inheritedResult = displaced.Result; + inheritedExactCellId = displaced.ExactCellId; + inheritedCollisionGeneration = displaced.CollisionGeneration; if (displaced.ProjectionSequence != 0UL && _pendingProjection.TryGetValue( displaced.ProjectionSequence, @@ -1134,6 +1393,36 @@ internal sealed class RuntimeSetPositionState : IDisposable cancelLostFamily: false, preserveLostFamily: inheritedLostFamily is not null, out RuntimePlacementProjectionSnapshot? discard); + + // F3: rent only AFTER the retire above (not before, as the C2 + // landing originally had it). Every field this method needs from a + // displaced operation was already captured into the locals above, + // so it is both SAFE and efficient for a single-entity churn cycle + // to hand that exact instance right back out here (the pool is + // LIFO) instead of allocating or drawing a different pooled + // instance - a fresh RentOperation() call always starts from + // `ResetAllFieldsToDefault`'s inert state regardless of which + // instance it returns. + Operation replacement = RentOperation(); + replacement.Record = record; + replacement.Token = token; + replacement.Key = key; + replacement.PositionAuthorityVersion = expectedPositionAuthorityVersion; + replacement.SessionLifetimeVersion = _entities.SessionLifetimeVersion; + replacement.SourceSpatialAuthorityVersion = + record.SpatialAuthorityVersion; + replacement.SourceVelocityAuthorityVersion = + record.VelocityAuthorityVersion; + replacement.PreviousContact = record.PhysicsBody?.InContact ?? false; + replacement.PreviousOnWalkable = + record.PhysicsBody?.OnWalkable ?? false; + replacement.Command = default; + replacement.Result = default; + replacement.SpatialAuthorityVersion = record.SpatialAuthorityVersion; + replacement.PlacementCommitVersion = record.PlacementCommitVersion; + replacement.Stage = RuntimeEntityPlacementStage.AwaitingPreparation; + replacement.Kind = kind; + replacement.Portal = portal; replacement.LostFamilyKeys = inheritedLostFamily; replacement.InheritedLostDeadline = inheritedLostFamily is not null; replacement.WithdrawalAcknowledged = inheritedWithdrawalAcknowledged; @@ -1141,10 +1430,9 @@ internal sealed class RuntimeSetPositionState : IDisposable { replacement.ProjectionSequence = retainedWithdrawal.Token.Sequence; - replacement.Result = displaced!.Result; - replacement.ExactCellId = displaced.ExactCellId; - replacement.CollisionGeneration = - displaced.CollisionGeneration; + replacement.Result = inheritedResult; + replacement.ExactCellId = inheritedExactCellId; + replacement.CollisionGeneration = inheritedCollisionGeneration; } _operations[key] = replacement; if (captureMoverPreparationAuthority) @@ -1156,7 +1444,26 @@ internal sealed class RuntimeSetPositionState : IDisposable } if (discard is { } cancelled) PublishPlacement(cancelled); - return IsCurrent(replacement) ? token : default; + // F3: deliberately does NOT use `IsCurrent(replacement)` here. + // `PublishPlacement` above can synchronously notify a subscriber + // that reentrantly calls BeginAcceptedPlacement for the SAME entity + // (see ReentrantBeginDuringDiscardCannotBeOverwrittenByOuterBegin) - + // that reentrant call retires `replacement` and, per this method's + // rent-after-retire ordering, can rent it right back out (LIFO) for + // the INNER operation. `replacement` (the physical object) would + // then read as the inner operation's Key/Token, and comparing it + // against itself via `_operations.TryGetValue(replacement.Key, ...) + // && ReferenceEquals(current, replacement)` is a tautology - it + // would report "still current" even though THIS (outer) Begin + // invocation was clearly superseded. `token` was captured fresh at + // the top of this call, before any reentrancy could touch it, so + // comparing it against whatever now actually owns the key correctly + // answers "is my own invocation still canonical" regardless of + // what happened to `replacement` in between. + return _operations.TryGetValue(key, out Operation? currentOperation) + && currentOperation.Token == token + ? token + : default; } private bool HasRetainedCompletion(RuntimeEntityKey key) @@ -1177,6 +1484,8 @@ internal sealed class RuntimeSetPositionState : IDisposable { EnsureNotDisposed(); command = default; + // Round 3 audit: safe - fresh lookup + Token check earlier in this + // SAME guard clause precede IsCurrent, nothing reentrant in between. if (!token.IsValid || !_operations.TryGetValue(token.Entity, out Operation? operation) || operation.Token != token @@ -1322,6 +1631,9 @@ internal sealed class RuntimeSetPositionState : IDisposable { EnsureNotDisposed(); ArgumentNullException.ThrowIfNull(record); + // Round 3 audit: safe - fresh lookup + Token check earlier in this + // SAME return expression precede IsCurrent, nothing reentrant in + // between. return token.IsValid && token.Entity == record.Key && _operations.TryGetValue(token.Entity, out Operation? operation) @@ -1396,6 +1708,13 @@ internal sealed class RuntimeSetPositionState : IDisposable ulong objectTableBindingAuthority = _physics.ObjectTableBindingAuthority; ulong objectTableAuthority = objectTable?.MutationRevision ?? 0UL; + // Round 3 audit: safe - `handleCollisions: null` means this call + // never invokes HandleSetPositionCollisions/HandleReports/ + // ReportEnvironment (no collision-report observer can be reached), + // and this dormant-activation path never calls CommitCanonical (so + // no ground-edge HitGround/LeaveGround dispatch either) - there is + // no reentrancy point between `operation` being obtained above and + // the ReferenceEquals check below. PhysicsSetPositionResult result = _physics.Engine.SetPosition( canonicalRequest, handleCollisions: null); @@ -1836,6 +2155,12 @@ internal sealed class RuntimeSetPositionState : IDisposable SetPositionCollisionBatchDispatchResult dispatch = _physics .CollisionReports.DispatchSetPositionBatchResult(receipt.Collision); bool reported = dispatch.Reported; + // Round 3 audit: safe - fresh lookup + Token.OperationId check + // earlier in this SAME guard clause precede IsCurrent, nothing + // reentrant in between. This dormant-activation family has zero + // production callers and never routes through CommitCanonical's + // ground-edge callback or the live collision-report dispatch that + // can reach an arbitrary observer. if (receipt.Status is RuntimeDormantSetPositionCommitStatus.RejectedPlacement && _operations.TryGetValue(receipt.Entity, out Operation? operation) @@ -1860,6 +2185,10 @@ internal sealed class RuntimeSetPositionState : IDisposable PhysicsBody body, in RuntimeDormantSetPositionCommitReceipt receipt) { + // Round 3 audit: safe - fresh lookup + Token.OperationId check + // earlier in this SAME return expression precede IsCurrent, nothing + // reentrant in between; this dormant family has zero production + // callers. return receipt.Status is RuntimeDormantSetPositionCommitStatus .AwaitingFinalShadowPreparation && _operations.TryGetValue(receipt.Entity, out Operation? operation) @@ -1898,6 +2227,10 @@ internal sealed class RuntimeSetPositionState : IDisposable if (receipt.Status is RuntimeDormantSetPositionCommitStatus .AwaitingFinalShadowPreparation) return IsDormantLocalActivationPrephaseCurrent(record, body, receipt); + // Round 3 audit: safe - fresh lookup + Token.OperationId check + // earlier in this SAME return expression precede IsCurrent, nothing + // reentrant in between; this dormant family has zero production + // callers. return receipt.Status is RuntimeDormantSetPositionCommitStatus .RejectedPlacement && _operations.TryGetValue(receipt.Entity, out Operation? operation) @@ -1938,6 +2271,15 @@ internal sealed class RuntimeSetPositionState : IDisposable PhysicsBody body, in RuntimeDormantSetPositionCommitReceipt receipt) { + // Round 3 audit: safe - fresh lookup + Token.OperationId check + // immediately precede IsCurrent, nothing reentrant in between; this + // dormant family has zero production callers. Nothing between here + // and the final `IsCurrent(operation)` below invokes anything + // reentrant either (IsVelocityCurrent/HandleAllCollisions/ + // CommitStationaryBits are pure PhysicsObjUpdate calls on `body`, + // and the nested IsDormantLocalActivationPrephaseCurrent call is + // itself a fresh-lookup check), so the SAME verified `operation` + // reference remains valid through the final check. if (!_operations.TryGetValue(receipt.Entity, out Operation? operation) || operation.Token.OperationId != receipt.OperationId || !IsCurrent(operation) @@ -2115,7 +2457,10 @@ internal sealed class RuntimeSetPositionState : IDisposable if (_operations.TryGetValue(receipt.Entity, out Operation? operation) && operation.Token.OperationId == receipt.OperationId) { - _ = CancelCore(operation); + // Round 3: `operation.Token` is read from the fresh lookup + // immediately above, with nothing reentrant in between - safe + // to pass straight through as the captured token. + _ = CancelCore(receipt.Entity, operation.Token); } } @@ -2131,7 +2476,10 @@ internal sealed class RuntimeSetPositionState : IDisposable { return; } - _ = CancelCore(operation); + // Round 3: `token` is this call's own parameter, verified fresh + // against `_operations` immediately above with nothing reentrant in + // between. + _ = CancelCore(token.Entity, token); } internal bool IsDormantLocalActivationCommitCurrent( @@ -2139,6 +2487,9 @@ internal sealed class RuntimeSetPositionState : IDisposable PhysicsBody body, in RuntimeDormantSetPositionCommitReceipt receipt) { + // Round 3 audit: safe - fresh lookup + Stage/ProjectionSequence + // check earlier in this SAME guard clause precede IsCurrent, + // nothing reentrant in between. if (!receipt.IsCommitted || !receipt.Projection.Token.IsValid || record.Key != receipt.Projection.Token.Entity @@ -2246,6 +2597,11 @@ internal sealed class RuntimeSetPositionState : IDisposable token.Entity, out exactAuthority) && exactAuthority.OperationId == token.OperationId; + // Round 3 audit: safe - `ownsToken` (fresh lookup + Token check) + // was just computed above with nothing reentrant in between; this + // is the function's own entry validation, before any of its + // internal reentrancy points (the SetPosition/CommitCanonical calls + // further down, which ARE converted to IsCurrentByToken). if (!ownsToken || operation is null || operation.Stage @@ -2399,19 +2755,34 @@ internal sealed class RuntimeSetPositionState : IDisposable quiescence.Token.LandblockPrefix); } - PhysicsSetPositionResult result = - _physics.Engine.SetPosition( + PhysicsSetPositionResult result; + _collisionCallbackContexts.Push(new CollisionCallbackContext( + operation.Record, + operation.PositionAuthorityVersion, + operation.SourceSpatialAuthorityVersion, + operation.SourceVelocityAuthorityVersion, + canonicalCommand.GameTime, + operation.PreviousContact, + operation.PreviousOnWalkable)); + try + { + result = _physics.Engine.SetPosition( canonicalRequest, - report => _physics.HandleSetPositionCollisions( - operation.Record, - operation.PositionAuthorityVersion, - operation.SourceSpatialAuthorityVersion, - operation.SourceVelocityAuthorityVersion, - canonicalCommand.GameTime, - operation.PreviousContact, - operation.PreviousOnWalkable, - report)); - if (!IsCurrent(operation)) + _handleSetPositionCollisionsCallback); + } + finally + { + _collisionCallbackContexts.Pop(); + } + // Round 3: `_physics.Engine.SetPosition` above can reenter this class + // - its collision-report callback can reach an arbitrary + // IRuntimeCollisionReportObserver subscriber that calls back into + // Begin/Cancel for this (or any) entity, which can retire-then-rent + // (LIFO) this exact `operation` instance for a different logical + // operation. `token` (the function parameter, captured before any + // of this ran) proves identity by value instead of trusting + // `operation`'s live fields. + if (!IsCurrentByToken(token.Entity, token, out operation)) return Outcome(RuntimeSetPositionStatus.Cancelled, result, default); if (result.IsSuccessful && TryGetBlockingQuiescence( @@ -2442,7 +2813,10 @@ internal sealed class RuntimeSetPositionState : IDisposable return Outcome(RuntimeSetPositionStatus.Rejected, result, default); } - if (!IsCurrent(operation)) + // Round 3: same reentrancy hazard as the check right after the + // SetPosition call above - re-verify by token rather than trusting + // `operation` across the collision-report dispatch. + if (!IsCurrentByToken(token.Entity, token, out operation)) return Outcome(RuntimeSetPositionStatus.Cancelled, result, default); operation.RequiresPreparation = false; operation.ExactCellId = result.CellId; @@ -2453,7 +2827,29 @@ internal sealed class RuntimeSetPositionState : IDisposable if (!CommitCanonical(operation, result)) { - PublishCancellation(CancelCore(operation)); + // Round 3: CommitCanonical's own ground-edge callback and + // collision-report dispatch can reenter this class, so + // `operation` may already be stale here even though it was + // just re-verified before the call. `token` (this function's + // own parameter, untouched since entry) is the safe capture. + PublishCancellation(CancelCore(token.Entity, token)); + return Outcome(RuntimeSetPositionStatus.Cancelled, result, default); + } + + // F1: CommitCanonical can succeed (fully applying the physical + // settle - contact transition, collision reports, shadow sync) for + // an operation a reentrant ground-edge callback has since displaced + // - retail lets that physical commit land regardless (see + // ReentrantGroundEdgePlacementCannotBeCancelledByDisplacedOperation). + // But THIS caller's own operation is no longer canonical, so it must + // still report Cancelled rather than publish a Place projection + // nothing will ever acknowledge again. `token` (the parameter, + // captured before any of this ran) is compared against a fresh + // lookup instead of trusting `operation`'s live fields, which may + // already reflect whatever displaced it. + if (!_operations.TryGetValue(token.Entity, out Operation? stillOwns) + || stillOwns.Token != token) + { return Outcome(RuntimeSetPositionStatus.Cancelled, result, default); } @@ -2469,6 +2865,38 @@ internal sealed class RuntimeSetPositionState : IDisposable projection); } + /// + /// C2: zero-allocation replacement for the LINQ + /// _pendingProjection.First() pattern used at every call site + /// below. Enumerable.First<TSource> takes an + /// IEnumerable<TSource> parameter, so calling it on a + /// dispatches through the + /// interface-typed IEnumerable<KeyValuePair<TKey, + /// TValue>>.GetEnumerator(), which BOXES the dictionary's + /// normally-struct Enumerator - this was the entire measured C2 + /// acknowledgement-path residual (120 B/op). A plain foreach on + /// the concrete field type + /// resolves to its public non-interface, struct-returning + /// GetEnumerator() instead and never boxes. Every call site below + /// already checks _pendingProjection.Count != 0 immediately + /// before calling this (short-circuiting `||`/`&&`), exactly + /// mirroring the precondition LINQ's First() relied on - the + /// throw path is unreachable in current usage, kept only so a future + /// caller that skips the guard fails loudly instead of silently, same as + /// LINQ's own contract would have. + /// + private KeyValuePair + FirstPendingProjection() + { + foreach (KeyValuePair entry + in _pendingProjection) + { + return entry; + } + throw new InvalidOperationException( + "FirstPendingProjection requires at least one pending entry."); + } + internal bool TryPeekProjection( out RuntimePlacementProjectionSnapshot projection) { @@ -2478,7 +2906,7 @@ internal sealed class RuntimeSetPositionState : IDisposable projection = default; return false; } - projection = _pendingProjection.First().Value; + projection = FirstPendingProjection().Value; return true; } @@ -2488,7 +2916,7 @@ internal sealed class RuntimeSetPositionState : IDisposable EnsureNotDisposed(); if (!token.IsValid || _pendingProjection.Count == 0 - || _pendingProjection.First().Key != token.Sequence + || FirstPendingProjection().Key != token.Sequence || !_pendingProjection.TryGetValue( token.Sequence, out RuntimePlacementProjectionSnapshot pending) @@ -2516,6 +2944,9 @@ internal sealed class RuntimeSetPositionState : IDisposable } return true; } + // Round 3 audit: safe - fresh lookup + ProjectionSequence check + // immediately precede IsCurrent, nothing reentrant in between (this + // is AcknowledgeProjection's own entry validation). if (!_operations.TryGetValue( token.Entity, out Operation? operation) @@ -2540,13 +2971,19 @@ internal sealed class RuntimeSetPositionState : IDisposable } operation.Stage = RuntimeEntityPlacementStage.AwaitingCommitAcknowledgement; _moverPreparationAuthorities.Remove(operation.Key); - return _operations.Remove(operation.Key); + bool removed = _operations.Remove(operation.Key); + if (removed) + RetireOperationToPool(operation); + return removed; } if (!operation.WakeableLostCell && !operation.InheritedLostDeadline) { _moverPreparationAuthorities.Remove(operation.Key); - return _operations.Remove(operation.Key); + bool removed = _operations.Remove(operation.Key); + if (removed) + RetireOperationToPool(operation); + return removed; } operation.WithdrawalAcknowledged = true; @@ -2649,9 +3086,21 @@ internal sealed class RuntimeSetPositionState : IDisposable } var operation = CreateWithdrawalOperation(record, key); _operations[key] = operation; + // Round 3: `operation.Token` is captured HERE, before + // PublishPlacement below can synchronously reenter this class - a + // subscriber reacting to the withdrawal-of-the-old-operation + // notification can call Begin/Cancel for this SAME entity, which + // (per RetireOperationToPool's doc comment) can retire-then-rent + // (LIFO) this exact `operation` instance for a brand-new logical + // operation before the checks below run. Comparing + // `ReferenceEquals`/`IsCurrent(operation)` against the stale + // reference afterward would be a tautology - it would report "still + // current" and then publish a Withdraw projection carrying the + // NEWER operation's state under the OLD operation's identity. + RuntimeEntityPlacementToken capturedToken = operation.Token; if (discard is { } cancelledOld) PublishPlacement(cancelledOld); - if (!IsCurrent(operation)) + if (!IsCurrentByToken(key, capturedToken, out operation)) return true; _ = PublishProjection( operation, @@ -2821,33 +3270,39 @@ internal sealed class RuntimeSetPositionState : IDisposable RuntimeSetPositionOperationKind.RemoteAuthoritative, body.LastUpdateTime, record.VelocityAuthorityVersion); - var operation = new Operation - { - Record = record, - Body = body, - Token = new RuntimeEntityPlacementToken( - _entities.SessionLifetimeVersion, - key, - record.PositionAuthorityVersion, - checked(++_nextOperationId), - RuntimeEntityPlacementPreparationKind.AuthoredMover), - Key = key, - PositionAuthorityVersion = record.PositionAuthorityVersion, - SessionLifetimeVersion = _entities.SessionLifetimeVersion, - SourceSpatialAuthorityVersion = record.SpatialAuthorityVersion, - SourceVelocityAuthorityVersion = record.VelocityAuthorityVersion, - PreviousContact = body.InContact, - PreviousOnWalkable = body.OnWalkable, - Command = command, - Result = result, - SpatialAuthorityVersion = record.SpatialAuthorityVersion, - PlacementCommitVersion = record.PlacementCommitVersion, - ExactCellId = cellId, - Stage = RuntimeEntityPlacementStage.AwaitingPreparation, - Kind = RuntimeSetPositionOperationKind.RemoteAuthoritative, - Portal = default, - RequiresPreparation = !hasPrepared, - }; + // F4: routed through RentOperation() (rather than a fresh + // `new Operation { ... }`) so every construction flows one path + // - see ResetAllFieldsToDefault's completeness-net doc comment. + // No key/displaced-operation collision is possible here: the + // loop above already skips this record when + // `_operations.ContainsKey(key)`. + Operation operation = RentOperation(); + operation.Record = record; + operation.Body = body; + operation.Token = new RuntimeEntityPlacementToken( + _entities.SessionLifetimeVersion, + key, + record.PositionAuthorityVersion, + checked(++_nextOperationId), + RuntimeEntityPlacementPreparationKind.AuthoredMover); + operation.Key = key; + operation.PositionAuthorityVersion = record.PositionAuthorityVersion; + operation.SessionLifetimeVersion = _entities.SessionLifetimeVersion; + operation.SourceSpatialAuthorityVersion = + record.SpatialAuthorityVersion; + operation.SourceVelocityAuthorityVersion = + record.VelocityAuthorityVersion; + operation.PreviousContact = body.InContact; + operation.PreviousOnWalkable = body.OnWalkable; + operation.Command = command; + operation.Result = result; + operation.SpatialAuthorityVersion = record.SpatialAuthorityVersion; + operation.PlacementCommitVersion = record.PlacementCommitVersion; + operation.ExactCellId = cellId; + operation.Stage = RuntimeEntityPlacementStage.AwaitingPreparation; + operation.Kind = RuntimeSetPositionOperationKind.RemoteAuthoritative; + operation.Portal = default; + operation.RequiresPreparation = !hasPrepared; _operations.Add(key, operation); _moverPreparationAuthorities[key] = CapturePreparationAuthority( operation, @@ -2970,7 +3425,7 @@ internal sealed class RuntimeSetPositionState : IDisposable private bool HasPendingProjectionThrough(ulong barrierSequence) => barrierSequence != 0UL && _pendingProjection.Count != 0 - && _pendingProjection.First().Key <= barrierSequence; + && FirstPendingProjection().Key <= barrierSequence; private bool HasCollisionDispatchDebt() { @@ -3153,8 +3608,27 @@ internal sealed class RuntimeSetPositionState : IDisposable bool ready, bool releaseUnavailable = false) { - foreach (Operation operation in _operations.Values.ToArray()) + // Round 3: snapshot (Key, Token) PAIRS, not Operation references. + // This loop's own RetryDeferred call below can reenter this class + // (CommitCanonical's ground-edge callbacks, or an arbitrary + // collision-report observer reentrantly calling Begin/Cancel) and + // retire-then-rent (LIFO) a LATER array entry's physical instance + // for a brand-new operation before this loop ever reaches it - a + // stale `Operation` reference captured once at the top (the + // pre-round-3 `.ToArray()` shape) would then silently read/mutate + // the wrong logical operation. Re-resolving by captured Token at + // the top of every iteration (before touching any field) detects + // that instead. + var snapshot = + new List<(RuntimeEntityKey Key, RuntimeEntityPlacementToken Token)>( + _operations.Count); + foreach (Operation existing in _operations.Values) + snapshot.Add((existing.Key, existing.Token)); + foreach ((RuntimeEntityKey key, RuntimeEntityPlacementToken capturedToken) + in snapshot) { + if (!IsCurrentByToken(key, capturedToken, out Operation? operation)) + continue; bool unavailableAfterReadyCommit = ready && operation.CollisionQuiescenceHeld && operation.CollisionGeneration == 0UL @@ -3333,6 +3807,18 @@ internal sealed class RuntimeSetPositionState : IDisposable continue; } RemoveDeferredBucket(cell); + // Round 3 audit: safe - `exact` snapshots KEYS (RuntimeEntityKey + // values), never Operation references, so nothing here can go + // stale the way a snapshotted-reference loop + // (RebindQuiescedDeferredOperations, before its round-3 fix) + // could. Every iteration re-resolves `operation` via a fresh + // `_operations.TryGetValue` and re-validates the exact + // WakeableLostCell/ExactCellId/CollisionPrefix/ + // CollisionGeneration shape before acting - a reentrant + // cancel-then-begin from an earlier iteration's RetryDeferred + // call (itself converted to IsCurrentByToken) is either not + // found at all or correctly rejected by this shape check for + // any later iteration touching the same or a different entity. foreach (RuntimeEntityKey entity in exact) { if (!_operations.TryGetValue( @@ -3379,6 +3865,16 @@ internal sealed class RuntimeSetPositionState : IDisposable _pendingProjection.Clear(); _expiredLostCells.Clear(); _expiredLostCellNodes.Clear(); + // F2: the C2 object pool retains full previous-generation entity + // graphs (Record -> Snapshot/PhysicsBody/clock/host references) + // through every pooled instance until it is rented and reset. Both + // callers of this method (ResetSession and Dispose) end the session + // those graphs belonged to, so nothing can still be mid-rent across + // this clear - unlike RetireOperationToPool/RentOperation, which + // must worry about a reentrant frame still executing inside a + // ground-edge callback, a session clear cannot be reentered from + // inside itself. + _operationPool.Clear(); } private RuntimeSetPositionOutcome ParkDeferred( @@ -3472,6 +3968,21 @@ internal sealed class RuntimeSetPositionState : IDisposable projection); } + /// + /// Round 3 audit: the entry check + /// below is safe (not converted to ) + /// because every one of this method's 4 call sites passes an + /// reference obtained via a fresh + /// _operations lookup (or an call) + /// with NOTHING reentrant executed between that lookup and this call - + /// AcknowledgeProjection and SubmitPreparedPlacementCore + /// call it immediately after their own entry validation, the + /// exact-cell-ready loop re-resolves by key every iteration and rejects + /// a repurposed operation via its WakeableLostCell/ExactCellId/ + /// CollisionPrefix/CollisionGeneration shape, and + /// RebindQuiescedDeferredOperations was converted in round 3 to + /// call immediately before this method. + /// private void RetryDeferred(Operation operation) { // The local-player activation lease owns its dormant body/controller @@ -3494,6 +4005,14 @@ internal sealed class RuntimeSetPositionState : IDisposable if (!IsDeferredWakePreparationCurrent(operation)) return; + // F1: hoisted before the ground-edge-callback-bearing + // CommitCanonical call below (same reasoning as + // SubmitPreparedPlacementCore's `token` parameter) - lets the + // post-CommitCanonical check confirm THIS operation is still + // canonical without trusting `operation`'s live fields, which a + // reentrant cancel-then-begin during the callback may have already + // repurposed. + RuntimeEntityPlacementToken operationToken = operation.Token; RuntimeCollisionPrefixQuiescenceToken restoringQuiescence = default; if (TryGetBlockingQuiescence( operation.Command.Physics, @@ -3531,23 +4050,49 @@ internal sealed class RuntimeSetPositionState : IDisposable operation.Command.GameTime), }; RebindPreparedCommand(operation); - PhysicsSetPositionResult result = IsStructurallyValid( - operation.Command.Physics) - ? _physics.Engine.SetPosition( - operation.Command.Physics, - report => _physics.HandleSetPositionCollisions( - operation.Record, - operation.PositionAuthorityVersion, - operation.SpatialAuthorityVersion, - operation.SourceVelocityAuthorityVersion, - operation.Command.GameTime, - operation.PreviousContact, - operation.PreviousOnWalkable, - report)) - : InvalidResult(operation.Command.Physics); + PhysicsSetPositionResult result; + if (IsStructurallyValid(operation.Command.Physics)) + { + _collisionCallbackContexts.Push(new CollisionCallbackContext( + operation.Record, + operation.PositionAuthorityVersion, + operation.SpatialAuthorityVersion, + operation.SourceVelocityAuthorityVersion, + operation.Command.GameTime, + operation.PreviousContact, + operation.PreviousOnWalkable)); + try + { + result = _physics.Engine.SetPosition( + operation.Command.Physics, + _handleSetPositionCollisionsCallback); + } + finally + { + _collisionCallbackContexts.Pop(); + } + } + else + { + result = InvalidResult(operation.Command.Physics); + } operation.CollisionGenerationReady = false; - if (!IsCurrent(operation)) + // Round 3: same reentrancy hazard as SubmitPreparedPlacementCore's + // post-SetPosition checks - the collision-report callback above can + // reach an arbitrary observer that calls back into Begin/Cancel and + // retires-then-rents (LIFO) this exact instance. `operationToken` + // was hoisted before the SetPosition call. (`operation` is a plain + // non-nullable parameter here, not a nullable local like + // SubmitPreparedPlacementCore's - route through a temporary so the + // NotNullWhen-proven reference can be assigned back to it.) + if (!IsCurrentByToken( + operationToken.Entity, + operationToken, + out Operation? refreshed)) + { return; + } + operation = refreshed; if (result.IsSuccessful && TryGetBlockingQuiescence( result, @@ -3605,12 +4150,47 @@ internal sealed class RuntimeSetPositionState : IDisposable } return; } - if (!IsCurrent(operation)) + // Round 3: nothing reentrant runs between here and the previous + // token check (only quiescence/deferred bookkeeping branches, all + // of which return before reaching this point) - re-verifying by + // token again anyway keeps this in lockstep with the same pattern + // used everywhere else in this method, and `operation` is already + // the freshly-verified reference from that check. + if (!IsCurrentByToken( + operationToken.Entity, + operationToken, + out Operation? stillCurrent)) + { return; + } + operation = stillCurrent; _preparedMovers[operation.Key] = operation.Command.Physics; if (!CommitCanonical(operation, result)) { - PublishCancellation(CancelCore(operation)); + // Round 3: CommitCanonical's own ground-edge callback and + // collision-report dispatch can reenter this class, so + // `operation` may already be stale here even though it was + // just re-verified before the call. + PublishCancellation(CancelCore(operationToken.Entity, operationToken)); + return; + } + + // F1: CommitCanonical can succeed (fully applying the physical + // settle) for an operation a reentrant ground-edge callback has + // since displaced - retail lets that physical commit land + // regardless (see + // ReentrantGroundEdgePlacementCannotBeCancelledByDisplacedOperation). + // But if THIS operation is no longer canonical, publishing a Place + // projection for it is wrong - nothing will ever acknowledge it + // again, and `operation`'s own fields may already reflect whatever + // displaced it. `operationToken` (hoisted before the callback) is + // compared against a fresh lookup rather than trusting `operation` + // itself. + if (!_operations.TryGetValue( + operationToken.Entity, + out Operation? stillOwns) + || stillOwns.Token != operationToken) + { return; } @@ -3626,10 +4206,49 @@ internal sealed class RuntimeSetPositionState : IDisposable Operation operation, in PhysicsSetPositionResult result) { + // Round 3 audit: safe - this is the function's own entry + // validation; every caller (SubmitPreparedPlacementCore, + // RetryDeferred) passes an `operation` obtained via + // IsCurrentByToken/IsCurrent immediately before calling + // CommitCanonical, with nothing reentrant in between. if (!result.IsCommitted || !IsCurrent(operation)) return false; RuntimeEntityRecord record = operation.Record; PhysicsBody body = operation.Body!; + // F1: every operation-derived scalar this method still needs AFTER + // invoking the ground-edge HitGround/LeaveGround callbacks below is + // captured into a local HERE, before those callbacks run - retail's + // own savedTransientState pattern (pseudo-C 283952 stacks the exact + // same bits before handle_all_collisions). A synchronous ground-edge + // chain that cancels then begins (or begins twice) for the SAME + // entity can retire this exact `operation` instance to the pool and + // rent it right back out (LIFO) for a DIFFERENT logical operation - + // reading `operation`'s live fields after that point would silently + // observe the wrong operation's state. The SETTLE below (contact + // transition, collision reports, shadow sync, and the currency + // checks gating them) deliberately does NOT hoist/compare the + // operation's Token: retail intentionally lets an in-flight + // ground-edge commit for a DISPLACED operation still complete (see + // IsCanonicalPlacementCommitCurrent's doc comment and + // ReentrantGroundEdgePlacementCannotBeCancelledByDisplacedOperation). + // `operationToken` is hoisted anyway - not for the settle, but for + // the Runtime-owned BOOKKEEPING writes at the very end of this + // method (Round 3 addendum A1), which must land on the operation + // THIS frame actually started with, never a nested cancel-then- + // begin's freshly-begun operation that happens to share the key and + // pass the record-state checks (a bare Begin never advances + // PlacementCommitVersion, so the settle-layer checks cannot detect + // that repurposing - only Token identity can). + RuntimeEntityPlacementToken operationToken = operation.Token; + RuntimeEntityKey operationKey = operation.Key; + ulong positionAuthorityVersion = operation.PositionAuthorityVersion; + ulong sourceVelocityAuthorityVersion = + operation.SourceVelocityAuthorityVersion; + double commandGameTime = operation.Command.GameTime; + bool previousContact = operation.PreviousContact; + bool previousOnWalkable = operation.PreviousOnWalkable; + float shadowWorldOffsetX = operation.Command.ShadowWorldOffsetX; + float shadowWorldOffsetY = operation.Command.ShadowWorldOffsetY; body.Orientation = result.Orientation; body.SnapToCell( result.CellId, @@ -3638,7 +4257,7 @@ internal sealed class RuntimeSetPositionState : IDisposable bool isStatic = (record.FinalPhysicsState & PhysicsStateFlags.Static) != 0; if (operation.EnteringWorldFromCelllessResidence) { - body.LastUpdateTime = operation.Command.GameTime; + body.LastUpdateTime = commandGameTime; _entities.ResetObjectClockForEnterWorld(record, isStatic); } if (operation.EnteringWorldFromCelllessResidence && !isStatic) @@ -3664,6 +4283,7 @@ internal sealed class RuntimeSetPositionState : IDisposable (result.CellId & 0xFFFF0000u) | 0xFFFFu); } operation.SpatialAuthorityVersion = record.SpatialAuthorityVersion; + ulong spatialAuthorityVersion = record.SpatialAuthorityVersion; _entities.AdvancePlacementCommit(record); operation.PlacementCommitVersion = record.PlacementCommitVersion; ulong canonicalCommitVersion = record.PlacementCommitVersion; @@ -3681,7 +4301,8 @@ internal sealed class RuntimeSetPositionState : IDisposable System.Collections.Immutable.ImmutableArray collidedObjectIds = result.CollidedObjectIds; if (!IsCanonicalPlacementCommitCurrent( - operation, + positionAuthorityVersion, + spatialAuthorityVersion, record, body, canonicalCommitVersion, @@ -3695,13 +4316,14 @@ internal sealed class RuntimeSetPositionState : IDisposable body, result.InContact, result.OnWalkable, - operation.PreviousOnWalkable); + previousOnWalkable); } else { var guard = new ContactCommitGuard( this, - operation, + positionAuthorityVersion, + spatialAuthorityVersion, record, body, canonicalCommitVersion, @@ -3710,14 +4332,15 @@ internal sealed class RuntimeSetPositionState : IDisposable body, result.InContact, result.OnWalkable, - operation.PreviousOnWalkable, + previousOnWalkable, remote.HitGround, remote.LeaveGround, guard.IsCurrent); } if (!contactCommitted || !IsCanonicalPlacementCommitCurrent( - operation, + positionAuthorityVersion, + spatialAuthorityVersion, record, body, canonicalCommitVersion, @@ -3729,17 +4352,18 @@ internal sealed class RuntimeSetPositionState : IDisposable bool reportingCurrent = !IsCollisionReportingEligible(record, body) || _physics.HandleSetPositionCollisionReports( record, - operation.PositionAuthorityVersion, - operation.SpatialAuthorityVersion, - operation.Command.GameTime, - operation.PreviousContact, - operation.PreviousOnWalkable, + positionAuthorityVersion, + spatialAuthorityVersion, + commandGameTime, + previousContact, + previousOnWalkable, collidedWithEnvironment, collidedObjectIds, out _); if (!reportingCurrent || !IsCanonicalPlacementCommitCurrent( - operation, + positionAuthorityVersion, + spatialAuthorityVersion, record, body, canonicalCommitVersion, @@ -3747,14 +4371,14 @@ internal sealed class RuntimeSetPositionState : IDisposable requireSpatialRoot: false)) return false; body.FramesStationaryFall = result.FramesStationaryFall; - if (IsVelocityCurrent(operation)) + if (IsVelocityCurrent(sourceVelocityAuthorityVersion, record)) { PhysicsObjUpdate.HandleAllCollisions( body, result.CollisionNormalValid, result.CollisionNormal, - operation.PreviousContact, - operation.PreviousOnWalkable, + previousContact, + previousOnWalkable, body.OnWalkable); } body.TransientState &= ~(TransientStateFlags.StationaryFall @@ -3770,7 +4394,8 @@ internal sealed class RuntimeSetPositionState : IDisposable if (remote is not null) remote.Airborne = !body.OnWalkable; if (!IsCanonicalPlacementCommitCurrent( - operation, + positionAuthorityVersion, + spatialAuthorityVersion, record, body, canonicalCommitVersion, @@ -3779,33 +4404,72 @@ internal sealed class RuntimeSetPositionState : IDisposable return false; _physics.Engine.ShadowObjects.CommitSetPosition( - operation.Key.LocalEntityId, + operationKey.LocalEntityId, result.Position, result.Orientation, result.CellId, - operation.Command.ShadowWorldOffsetX, - operation.Command.ShadowWorldOffsetY, + shadowWorldOffsetX, + shadowWorldOffsetY, result.ShadowAction, result.CrossCellIds); _physics.AcknowledgeSpatialProjection(record, spatial: true); - operation.ExactCellId = result.CellId; - operation.Result = result; - operation.WakeableLostCell = false; - operation.EnteringWorldFromCelllessResidence = false; - CancelLostFamilyDeadlines(operation); - return IsCurrent(operation) - && IsCanonicalPlacementCommitCurrent( - operation, - record, - body, - canonicalCommitVersion, - committedCellId, - requireSpatialRoot: true); + // A1 (round 3 addendum): unlike the settle above, these are + // Runtime's OWN bookkeeping for which logical operation this settle + // belongs to - a nested cancel-then-begin (reachable via either the + // ground-edge callbacks above OR an arbitrary collision-report + // observer reentrantly calling Begin/Cancel from inside + // HandleSetPositionCollisionReports) can recycle `operation` for a + // brand-new operation that stages at AwaitingPreparation without + // ever advancing PlacementCommitVersion - invisible to the + // record-state checks above. Re-resolve by the token captured + // before any callback ran and only write if it is still the exact + // instance; skipping the writes (rather than failing the whole + // commit) matches retail's already-unconditional physical settle - + // only the ownership bookkeeping is conditional. + if (_operations.TryGetValue(operationKey, out Operation? currentOperation) + && currentOperation.Token == operationToken) + { + currentOperation.ExactCellId = result.CellId; + currentOperation.Result = result; + currentOperation.WakeableLostCell = false; + currentOperation.EnteringWorldFromCelllessResidence = false; + CancelLostFamilyDeadlines(currentOperation); + } + + return IsCanonicalPlacementCommitCurrent( + positionAuthorityVersion, + spatialAuthorityVersion, + record, + body, + canonicalCommitVersion, + committedCellId, + requireSpatialRoot: true); } + /// + /// F1: takes the operation's authority versions as explicit VALUES + /// (captured by the caller before invoking the ground-edge HitGround/ + /// LeaveGround callbacks) rather than reading them live off an + /// reference that a reentrant cancel-then-begin + /// (or begin-twice) chain may have already retired and repurposed for a + /// DIFFERENT logical operation. Deliberately does NOT also compare the + /// operation's Token/identity: retail intentionally lets an in-flight + /// ground-edge commit for a DISPLACED operation still complete (its + /// physical contact-transition/shadow-sync settle even though a newer + /// operation now owns future placement authority for this entity - see + /// ReentrantGroundEdgePlacementCannotBeCancelledByDisplacedOperation). + /// Adding an identity check here would incorrectly abort that commit the + /// moment the entity is displaced, which is exactly the behavior that + /// test pins as wrong. The self-aliasing hazard a Token comparison would + /// otherwise guard against is real, but belongs at the call sites that + /// need "is this SPECIFIC Begin invocation still canonical" (see the + /// local-token comparison at the end of + /// BeginAcceptedPlacementCore), not here. + /// private bool IsCanonicalPlacementCommitCurrent( - Operation operation, + ulong positionAuthorityVersion, + ulong spatialAuthorityVersion, RuntimeEntityRecord record, PhysicsBody body, ulong placementCommitVersion, @@ -3813,10 +4477,8 @@ internal sealed class RuntimeSetPositionState : IDisposable bool requireSpatialRoot) => _entities.IsCurrent(record) && ReferenceEquals(record.PhysicsBody, body) - && record.PositionAuthorityVersion - == operation.PositionAuthorityVersion - && record.SpatialAuthorityVersion - == operation.SpatialAuthorityVersion + && record.PositionAuthorityVersion == positionAuthorityVersion + && record.SpatialAuthorityVersion == spatialAuthorityVersion && record.PlacementCommitVersion == placementCommitVersion && record.FullCellId == fullCellId && (!requireSpatialRoot || _physics.IsSpatialRoot(record)); @@ -3928,46 +4590,75 @@ internal sealed class RuntimeSetPositionState : IDisposable body.CellPosition.Frame.Origin, CrossCellIds: ImmutableArray.Empty, CollidedObjectIds: ImmutableArray.Empty); - return new Operation - { - Record = record, - Body = body, - Token = new RuntimeEntityPlacementToken( - _entities.SessionLifetimeVersion, - key, - record.PositionAuthorityVersion, - checked(++_nextOperationId), - RuntimeEntityPlacementPreparationKind.LegacyDirect), - Key = key, - PositionAuthorityVersion = record.PositionAuthorityVersion, - SessionLifetimeVersion = _entities.SessionLifetimeVersion, - SourceSpatialAuthorityVersion = record.SpatialAuthorityVersion, - SourceVelocityAuthorityVersion = record.VelocityAuthorityVersion, - PreviousContact = body.InContact, - PreviousOnWalkable = body.OnWalkable, - Command = new RuntimeSetPositionCommand( - physics, - RuntimeSetPositionOperationKind.RemoteAuthoritative, - body.LastUpdateTime, - record.VelocityAuthorityVersion, - ShadowWorldOffsetX: 0f, - ShadowWorldOffsetY: 0f), - Result = result, - SpatialAuthorityVersion = record.SpatialAuthorityVersion, - PlacementCommitVersion = record.PlacementCommitVersion, - ExactCellId = result.CellId, - WakeableLostCell = false, - Stage = RuntimeEntityPlacementStage - .AwaitingWithdrawalAcknowledgement, - Kind = RuntimeSetPositionOperationKind.RemoteAuthoritative, - Portal = default, - }; + // F4: routed through RentOperation() (rather than a fresh + // `new Operation { ... }`) so every construction flows one path - + // see ResetAllFieldsToDefault's completeness-net doc comment. The + // sole caller (Cancel) always runs CancelCoreDeferred against this + // same key first, so there is no displaced-operation state left to + // read here. + Operation operation = RentOperation(); + operation.Record = record; + operation.Body = body; + operation.Token = new RuntimeEntityPlacementToken( + _entities.SessionLifetimeVersion, + key, + record.PositionAuthorityVersion, + checked(++_nextOperationId), + RuntimeEntityPlacementPreparationKind.LegacyDirect); + operation.Key = key; + operation.PositionAuthorityVersion = record.PositionAuthorityVersion; + operation.SessionLifetimeVersion = _entities.SessionLifetimeVersion; + operation.SourceSpatialAuthorityVersion = record.SpatialAuthorityVersion; + operation.SourceVelocityAuthorityVersion = record.VelocityAuthorityVersion; + operation.PreviousContact = body.InContact; + operation.PreviousOnWalkable = body.OnWalkable; + operation.Command = new RuntimeSetPositionCommand( + physics, + RuntimeSetPositionOperationKind.RemoteAuthoritative, + body.LastUpdateTime, + record.VelocityAuthorityVersion, + ShadowWorldOffsetX: 0f, + ShadowWorldOffsetY: 0f); + operation.Result = result; + operation.SpatialAuthorityVersion = record.SpatialAuthorityVersion; + operation.PlacementCommitVersion = record.PlacementCommitVersion; + operation.ExactCellId = result.CellId; + operation.WakeableLostCell = false; + operation.Stage = RuntimeEntityPlacementStage + .AwaitingWithdrawalAcknowledgement; + operation.Kind = RuntimeSetPositionOperationKind.RemoteAuthoritative; + operation.Portal = default; + return operation; } + /// + /// Reference-identity currency check. SAFE ONLY when no reentrancy + /// point (a synchronous publish, a collision-report dispatch reaching + /// an arbitrary IRuntimeCollisionReportObserver, or a + /// ground-edge HitGround/LeaveGround callback) has intervened between + /// `operation` being obtained/last verified and this call - every + /// call site is audited and either (a) has no such reentrancy point in + /// between (commented at the call site), or (b) has been converted to + /// instead. See + /// 's doc comment for why + /// ReferenceEquals becomes a tautology once pooling can hand + /// the SAME physical instance back out for a different logical + /// operation at the same key. + /// private bool IsCurrent(Operation operation) => _operations.TryGetValue(operation.Key, out Operation? current) && ReferenceEquals(current, operation) - && _entities.SessionLifetimeVersion == operation.SessionLifetimeVersion + && IsOperationStateConsistent(operation); + + /// + /// Round 3: the consistency half of , + /// factored out so can reuse the exact + /// same checks after establishing identity via Token (safe under + /// pooling) instead of ReferenceEquals (unsafe under pooling - + /// see 's doc comment). + /// + private bool IsOperationStateConsistent(Operation operation) => + _entities.SessionLifetimeVersion == operation.SessionLifetimeVersion && _entities.IsCurrent(operation.Record) && operation.Record.Key == operation.Key && (operation.Body is null @@ -3979,6 +4670,40 @@ internal sealed class RuntimeSetPositionState : IDisposable && operation.Record.PlacementCommitVersion == operation.PlacementCommitVersion; + /// + /// Round 3: the class-wide replacement for + /// ReferenceEquals/ at any frame + /// that holds an Operation reference across a reentrancy point (a + /// synchronous publish, a collision-report dispatch that can reach an + /// arbitrary IRuntimeCollisionReportObserver, or a ground-edge + /// HitGround/LeaveGround callback). + /// MUST be read from the operation (or already be the caller's own + /// token parameter) BEFORE that reentrancy point - reading it fresh off + /// a possibly-already-repurposed reference here would be exactly as + /// tautological as the ReferenceEquals check this replaces (see + /// 's doc comment for the full + /// hazard: a reentrant cancel-then-begin can retire-then-rent the SAME + /// physical instance for a DIFFERENT logical operation, LIFO). On + /// success, returns the FRESHLY resolved Operation so the caller + /// continues with a reference proven current at THIS instant, never + /// the possibly-stale one it held before the reentrancy point. + /// + private bool IsCurrentByToken( + RuntimeEntityKey key, + in RuntimeEntityPlacementToken capturedToken, + [NotNullWhen(true)] out Operation? operation) + { + if (_operations.TryGetValue(key, out Operation? current) + && current.Token == capturedToken + && IsOperationStateConsistent(current)) + { + operation = current; + return true; + } + operation = null; + return false; + } + private bool IsExactDormantLocalActivationCurrent( RuntimeEntityRecord record, PhysicsBody body, @@ -3989,6 +4714,9 @@ internal sealed class RuntimeSetPositionState : IDisposable bool allowDeferredLease = false) { operation = null; + // Round 3 audit: safe - fresh lookup + Token check earlier in this + // SAME guard clause (below) precede IsCurrent, nothing reentrant in + // between; this dormant family has zero production callers. if (!token.IsValid || token.Entity != record.Key || token.PreparationKind @@ -4052,9 +4780,21 @@ internal sealed class RuntimeSetPositionState : IDisposable } private bool IsVelocityCurrent(Operation operation) => - operation.SourceVelocityAuthorityVersion == 0UL - || operation.Record.VelocityAuthorityVersion - == operation.SourceVelocityAuthorityVersion; + IsVelocityCurrent( + operation.SourceVelocityAuthorityVersion, + operation.Record); + + /// + /// F1: overload taking the hoisted scalar directly, for callers (like + /// CommitCanonical) that must not re-read a live + /// after a ground-edge callback may have retired + /// and repurposed it. + /// + private static bool IsVelocityCurrent( + ulong sourceVelocityAuthorityVersion, + RuntimeEntityRecord record) => + sourceVelocityAuthorityVersion == 0UL + || record.VelocityAuthorityVersion == sourceVelocityAuthorityVersion; private static MoverPreparationAuthority CapturePreparationAuthority( Operation operation, @@ -4242,20 +4982,43 @@ internal sealed class RuntimeSetPositionState : IDisposable .CancelledAwaitingAcknowledgement; discard = cancelled; } + // C2: `operation` was just removed from `_operations` above - see + // RetireOperationToPool's doc comment for why every caller (this one + // included) is safe to hand it back here, even the discard-pending + // case (AcknowledgeProjection's Discard branch never looks the + // operation back up by key). + RetireOperationToPool(operation); return true; } + /// + /// Round 3: takes the entity key plus a Token CAPTURED BY THE CALLER + /// (before any reentrancy point the caller passed through) instead of + /// an reference. The ReferenceEquals + /// check this replaced was exactly the shape the round 3 architecture + /// review flagged: safe before pooling (an Operation instance was never + /// reused), silently tautological after (a reentrant cancel-then-begin + /// can retire-then-rent the SAME instance for a DIFFERENT logical + /// operation, so `expected` and the freshly-looked-up `current` could be + /// the same reference while representing different operations) - with + /// the worse outcome that this method would then cancel the NEWER + /// operation instead of correctly no-op'ing. See + /// 's doc comment for the full + /// hazard and for the equivalent + /// conversion applied to plain currency checks. + /// private RuntimePlacementCancellationReceipt CancelCore( - Operation expected, + RuntimeEntityKey key, + in RuntimeEntityPlacementToken expectedToken, bool preserveLostFamily = false) { - if (!_operations.TryGetValue(expected.Key, out Operation? current) - || !ReferenceEquals(current, expected)) + if (!_operations.TryGetValue(key, out Operation? current) + || current.Token != expectedToken) { return default; } _ = CancelCoreDeferred( - expected.Key, + key, cancelLostFamily: false, preserveLostFamily, out RuntimePlacementProjectionSnapshot? discard); diff --git a/tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs b/tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs index 7b685664..d1e4f973 100644 --- a/tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs +++ b/tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using System.Numerics; +using System.Reflection; using AcDream.Content; using AcDream.Content.Pak; using AcDream.Core.Net; @@ -288,10 +289,28 @@ public sealed class RuntimeSetPositionStateTests } long allocated = GC.GetAllocatedBytesForCurrentThread() - before; - // The dormant 4B1 owner still allocates its operation/projection - // envelope. Pin the measured Release ceiling so 4B2 cannot activate - // the route without making this cost explicit or reducing it. - Assert.InRange(allocated / iterations, 1L, 2_048L); + // C2: root-caused and fixed the dormant 4B1 owner's per-operation + // allocations instead of raising this cap. Measured exactly 944 B/op + // (stable across 5+ repeat runs), down from the pre-fix 2,032 B/op - + // pooling `Operation` instances (BeginAcceptedPlacementCore no + // longer allocates a fresh instance every accepted placement), + // caching the `PhysicsEngine.SetPosition` collision-report delegate + // instead of a per-call closure, and replacing the LINQ + // `_pendingProjection.First()` pattern (which boxed + // SortedDictionary's struct Enumerator through IEnumerable every + // acknowledgement) with a non-boxing foreach. See + // docs/research/2026-07-31-canonical-set-position.md for the C2 + // finding this closes and the accepted residual floor: ~520 B/op + // lives inside AcDream.Core's PhysicsEngine.SetPosition (transition + // init/inner solve/query-footprint materialization - shared physics + // infrastructure, out of this Runtime-only slice's scope) and + // ~208 B/op is SortedDictionary's inherent per-Add tree-node + // allocation for `_pendingProjection` (replacing that ordered + // structure to chase the last ~230 B/op was judged too invasive/ + // risky for the remaining headroom under this cap). 1,536 keeps + // roughly 60% headroom over the measured value for JIT/environment + // variance without re-opening the door to unbounded per-op growth. + Assert.InRange(allocated / iterations, 1L, 1_536L); } [Fact] @@ -1081,6 +1100,243 @@ public sealed class RuntimeSetPositionStateTests Assert.True(lifetime.Physics.SetPosition.CaptureOwnership().IsConverged); } + /// + /// F1 regression: from within the ground-edge HitGround callback, drive + /// an explicit cancel-then-begin for the SAME entity. Because the + /// Operation pool is LIFO, this retires the outer, still-executing + /// operation and immediately rents that EXACT instance back out for the + /// new ("recycled") operation - a strictly more adversarial recycle than + /// ReentrantGroundEdgePlacementCannotBeCancelledByDisplacedOperation's + /// begin-only chain (there, nothing had retired the outer operation to + /// the pool by the time the reentrant Begin ran, so it always drew a + /// different or fresh instance). CommitCanonical must still feed the + /// ORIGINAL (pre-callback) PreviousContact/PreviousOnWalkable into + /// HandleSetPositionCollisionReports, not whatever the recycled instance + /// now holds for the unrelated new operation. This is observable: the + /// environment-collision report only fires at all when + /// (!previousOnWalkable && body.OnWalkable) - the recycled + /// instance's PreviousOnWalkable (captured from the body's ALREADY- + /// landed post-transition state) would read true instead of the + /// original false, silently suppressing the report if the operation's + /// live (potentially repurposed) fields were read instead of hoisted + /// locals. + /// + [Fact] + public void ReentrantCancelThenBeginRecyclesInstanceButCollisionReportUsesPreCallbackValues() + { + PhysicsEngine engine = FlatEngine(SourceLandblock, 0f); + engine.TransitionCellCollisionTestHook = + (transition, phase, _, observed) => + { + if (phase == TransitionCellCollisionPhase.Environment) + { + transition.CollisionInfo.SetContactPlane( + new Plane(Vector3.UnitZ, 0f), + SourceCell); + } + return observed; + }; + using var lifetime = new RuntimeEntityObjectLifetime(engine); + RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001046u, 1); + PhysicsBody body = AttachBody( + lifetime, + record, + SourceCell, + PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions); + var collisionObserver = new CollisionReportObserver(); + using IDisposable collisionSubscription = lifetime.Physics + .CollisionReports.Subscribe(collisionObserver); + + RuntimeEntityPlacementToken recycled = default; + var remote = new ReentrantRemotePlacement(body) + { + CellId = SourceCell, + OnHitGround = () => + { + lifetime.Physics.SetPosition.Cancel( + record, + publishWithdrawal: false); + recycled = lifetime.Physics.SetPosition.BeginAcceptedPlacement( + record, + record.PositionAuthorityVersion, + RuntimeSetPositionOperationKind.RemoteAuthoritative); + }, + }; + lifetime.Entities.SetRemoteMotion(record, remote); + + _ = lifetime.Physics.SetPosition.Apply( + record, + record.PositionAuthorityVersion, + Command(Request(SourceCell, new Vector3(13f, 18f, 7f)))); + + Assert.True(recycled.IsValid); + Assert.True(body.OnWalkable); + RuntimeCollisionReport report = Assert.Single(collisionObserver.Reports); + Assert.Equal(RuntimeCollisionReportKind.EnvironmentCollision, report.Kind); + Assert.False(report.RecipientWasInContact); + } + + /// + /// Round 3 pinned regression: the coordinator's precise reachability + /// trace for the Cancel-path recycle hazard. Cancel(record, bool) + /// creates a withdrawal operation and installs it at this entity's key, + /// then calls PublishPlacement(cancelledOld) to notify observers + /// that the still-pending Place projection has been superseded by a + /// Discard - a synchronous dispatch that can reach an arbitrary + /// subscriber, which here + /// reentrantly calls BeginAcceptedPlacement for the SAME entity. + /// Because the Operation pool is LIFO and + /// BeginAcceptedPlacementCore rents only after retiring, that + /// reentrant Begin retires the withdrawal operation Cancel is + /// mid-way through publishing for and rents the exact same physical + /// instance right back out for a brand-new "inner" operation. Before + /// round 3's captured-token-vs-fresh-lookup conversion, Cancel's + /// post-publish check read IsCurrent(operation) off that live + /// (now-repurposed) reference - a tautology that would have let the + /// outer withdrawal proceed to publish a SECOND (Withdraw) projection + /// stamped with the inner operation's state, corrupting a Begin-only + /// operation that never asked for a pending projection. The fix + /// captures operation.Token into a local BEFORE + /// PublishPlacement runs and re-verifies it via a fresh + /// IsCurrentByToken lookup afterward, so the stale reference is + /// detected and the outer call returns without publishing anything + /// beyond the Discard. + /// + [Fact] + public void ReentrantBeginDuringCancelPublishCannotBeOverwrittenByOuterWithdraw() + { + PhysicsEngine engine = FlatEngine(SourceLandblock, 0f); + using var lifetime = new RuntimeEntityObjectLifetime(engine); + RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001250u, 1); + _ = AttachBody(lifetime, record, SourceCell); + RuntimeSetPositionOutcome pending = lifetime.Physics.SetPosition.Apply( + record, + record.PositionAuthorityVersion, + Command(Request(SourceCell, new Vector3(18f, 18f, 7f)))); + Assert.Equal( + RuntimeSetPositionStatus.CommittedHostAcknowledgementPending, + pending.Status); + + RuntimeEntityPlacementToken inner = default; + var observer = new PlacementObserver(delta => + { + if (delta.Placement.Kind is RuntimePlacementProjectionKind.Discard) + { + inner = lifetime.Physics.SetPosition.BeginAcceptedPlacement( + record, + record.PositionAuthorityVersion, + RuntimeSetPositionOperationKind.RemoteAuthoritative); + } + }); + using IDisposable subscription = lifetime.Events.SubscribePlacement(observer); + + bool removed = lifetime.Physics.SetPosition.Cancel( + record, + publishWithdrawal: true); + + Assert.True(removed); + Assert.True(inner.IsValid); + // Only the Discard (for the pending Place this Cancel superseded) + // should have published - a stale-reference bug would have added a + // second (Withdraw) delta stamped with `inner`'s state. + RuntimePlacementDelta delta = Assert.Single(observer.Deltas); + Assert.Equal(RuntimePlacementProjectionKind.Discard, delta.Placement.Kind); + // `inner` must still be exactly what BeginAcceptedPlacement handed + // back - untouched by the outer Cancel's withdrawal publish. + Assert.True(lifetime.Physics.SetPosition.IsPlacementCurrent(inner)); + RuntimeSetPositionOwnershipSnapshot ownership = + lifetime.Physics.SetPosition.CaptureOwnership(); + Assert.Equal(1, ownership.ActiveOperationCount); + Assert.Equal(1, ownership.AwaitingPreparationCount); + } + + /// + /// Round 3 CancelCore-shape regression: from within the ground-edge + /// HitGround callback, recycle the SAME operation instance + /// (cancel-then-begin, LIFO pool) for a brand-new "inner" operation on + /// the same entity, AND advance the record's PlacementCommitVersion a + /// second time (exactly what a completed nested commit would also have + /// produced) so CommitCanonical's post-callback + /// IsCanonicalPlacementCommitCurrent check fails for the OUTER commit. + /// SubmitPreparedPlacementCore's own failure branch then calls + /// PublishCancellation(CancelCore(token.Entity, token)) using + /// `token` - the outer caller's own (now-stale) captured token. Before + /// round 3, CancelCore(Operation expected, ...) compared + /// `ReferenceEquals(current, expected)`; with the physical instance + /// recycled for `inner`, that check would have found the SAME instance + /// at this key and retired/cancelled it out from under the still-active + /// inner operation - the "worse outcome: cancelling the newer + /// operation" the reviewer flagged. The Token-keyed + /// CancelCore(key, expectedToken) must instead see + /// `current.Token != expectedToken` and no-op, leaving `inner` + /// untouched. + /// + [Fact] + public void ReentrantCancelThenBeginDuringCommitFailureLeavesInnerOperationUncancelled() + { + PhysicsEngine engine = FlatEngine(SourceLandblock, 0f); + engine.TransitionCellCollisionTestHook = + (transition, phase, _, observed) => + { + if (phase == TransitionCellCollisionPhase.Environment) + { + transition.CollisionInfo.SetContactPlane( + new Plane(Vector3.UnitZ, 0f), + SourceCell); + } + return observed; + }; + using var lifetime = new RuntimeEntityObjectLifetime(engine); + RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001251u, 1); + PhysicsBody body = AttachBody( + lifetime, + record, + SourceCell, + PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions); + + RuntimeEntityPlacementToken inner = default; + var remote = new ReentrantRemotePlacement(body) + { + CellId = SourceCell, + OnHitGround = () => + { + lifetime.Physics.SetPosition.Cancel( + record, + publishWithdrawal: false); + // Simulate a nested commit completing for `inner` (without + // needing a full nested SetPosition round-trip): advance + // PlacementCommitVersion BEFORE `inner`'s operation is + // created so `inner` snapshots the already-advanced value + // and stays internally self-consistent, while the OUTER + // commit's `canonicalCommitVersion` (captured before this + // callback ran) now mismatches the record. + lifetime.Entities.AdvancePlacementCommit(record); + inner = lifetime.Physics.SetPosition.BeginAcceptedPlacement( + record, + record.PositionAuthorityVersion, + RuntimeSetPositionOperationKind.RemoteAuthoritative); + }, + }; + lifetime.Entities.SetRemoteMotion(record, remote); + + RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply( + record, + record.PositionAuthorityVersion, + Command(Request(SourceCell, new Vector3(13f, 18f, 7f)))); + + Assert.Equal(RuntimeSetPositionStatus.Cancelled, outcome.Status); + Assert.True(inner.IsValid); + // The bug this pins: a reference-based CancelCore would retire the + // recycled instance (now serving `inner`) out from under it. + // Confirm `inner` is still the live, current operation for this + // entity. + Assert.True(lifetime.Physics.SetPosition.IsPlacementCurrent(inner)); + RuntimeSetPositionOwnershipSnapshot ownership = + lifetime.Physics.SetPosition.CaptureOwnership(); + Assert.Equal(1, ownership.ActiveOperationCount); + Assert.Equal(1, ownership.AwaitingPreparationCount); + } + [Fact] public void RetrySnapshotPreservesOrderWhenObserverAcknowledgesTwoPendingTokens() { @@ -2012,6 +2268,158 @@ public sealed class RuntimeSetPositionStateTests Assert.True(lifetime.Physics.CaptureOwnership().IsConverged); } + /// + /// F2 regression: the C2 Operation pool retains full previous-generation + /// entity graphs (Record -> Snapshot/PhysicsBody/clock/host references) + /// through every pooled instance until it is rented and reset - a + /// session reset must not leave that behind uncleared, even though + /// PooledOperationCount is deliberately excluded from + /// (see + /// its doc comment). + /// + [Fact] + public void OperationPoolClearsOnResetSession() + { + PhysicsEngine engine = FlatEngine(SourceLandblock, 0f); + using var lifetime = new RuntimeEntityObjectLifetime(engine); + RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001048u, 1); + _ = AttachBody(lifetime, record, SourceCell); + + RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply( + record, + record.PositionAuthorityVersion, + Command(Request(SourceCell, new Vector3(11f, 18f, 7f)))); + Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection( + outcome.Projection)); + + RuntimeSetPositionOwnershipSnapshot beforeReset = + lifetime.Physics.SetPosition.CaptureOwnership(); + Assert.True(beforeReset.PooledOperationCount >= 1); + + lifetime.Physics.SetPosition.ResetSession(); + + Assert.Equal( + 0, + lifetime.Physics.SetPosition.CaptureOwnership().PooledOperationCount); + } + + /// + /// F2 regression: same guarantee as + /// , via Dispose instead + /// of ResetSession - both callers of ClearOwnedState must clear the + /// pool. + /// + [Fact] + public void OperationPoolClearsOnDispose() + { + PhysicsEngine engine = FlatEngine(SourceLandblock, 0f); + var lifetime = new RuntimeEntityObjectLifetime(engine); + RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001049u, 1); + _ = AttachBody(lifetime, record, SourceCell); + + RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply( + record, + record.PositionAuthorityVersion, + Command(Request(SourceCell, new Vector3(11f, 18f, 7f)))); + Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection( + outcome.Projection)); + + RuntimeSetPositionOwnershipSnapshot beforeDispose = + lifetime.Physics.SetPosition.CaptureOwnership(); + Assert.True(beforeDispose.PooledOperationCount >= 1); + + lifetime.Dispose(); + + Assert.Equal( + 0, + lifetime.Physics.SetPosition.CaptureOwnership().PooledOperationCount); + } + + /// + /// F4 regression: converting Operation's properties from + /// required { get; init; } to plain { get; set; } (so + /// pooling could recycle instances) dropped the compiler's completeness + /// net - nothing any longer forces every construction site to set every + /// field. This reflection-based test is the runtime replacement: it + /// pins the exact set of backing fields Operation declares (private, so + /// only reflection can reach it from a test) against a hardcoded, + /// maintained list. Adding a new auto-property to Operation changes its + /// backing-field set and fails this test immediately - the failure + /// message is the prompt to update BOTH this list and + /// Operation.ResetAllFieldsToDefault in the same change, exactly + /// mirroring what a missing `required` member assignment used to force + /// at compile time. + /// + [Fact] + public void OperationResetAllFieldsToDefaultTouchesEveryDeclaredField() + { + Type? operationType = typeof(RuntimeSetPositionState).GetNestedType( + "Operation", + BindingFlags.NonPublic); + Assert.NotNull(operationType); + + FieldInfo[] actualFields = operationType!.GetFields( + BindingFlags.Instance + | BindingFlags.NonPublic + | BindingFlags.Public); + string[] actualFieldNames = actualFields + .Select(field => field.Name) + .OrderBy(name => name, StringComparer.Ordinal) + .ToArray(); + + // The maintained list: one entry per auto-property Operation + // declares. Kept as property names (not the `k__BackingField` + // form the compiler actually emits) so a reviewer can read it + // directly against the property list in the source file. + string[] expectedPropertyNames = + [ + "Record", + "Body", + "Token", + "Key", + "PositionAuthorityVersion", + "SessionLifetimeVersion", + "SourceSpatialAuthorityVersion", + "SourceVelocityAuthorityVersion", + "PreviousContact", + "PreviousOnWalkable", + "Command", + "Result", + "SpatialAuthorityVersion", + "PlacementCommitVersion", + "ExactCellId", + "CollisionGeneration", + "CollisionPrefix", + "WithdrawalAcknowledged", + "CollisionGenerationReady", + "CollisionQuiescenceHeld", + "ProjectionSequence", + "WakeableLostCell", + "Stage", + "Kind", + "Portal", + "RequiresPreparation", + "Expired", + "LostFamilyKeys", + "InheritedLostDeadline", + "EnteringWorldFromCelllessResidence", + "DormantLocalActivation", + "PreparedCommandAwaitingWithdrawalAck", + "InPool", + ]; + string[] expectedFieldNames = expectedPropertyNames + .Select(name => $"<{name}>k__BackingField") + .OrderBy(name => name, StringComparer.Ordinal) + .ToArray(); + + Assert.Equal(expectedFieldNames, actualFieldNames); + + MethodInfo? resetMethod = operationType.GetMethod( + "ResetAllFieldsToDefault", + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(resetMethod); + } + [Fact] public void CommittedParentDoesNotLeakAcrossChildGuidReuse() {