acdream/docs/research/2026-08-02-collision-throughput-handoff/p1-retirement-receipt-loop.md
Erik 01f4791e95 fix(streaming): stop replaying committed recenter retirements
Root cause: pending-only live projection buckets were misclassified as landblock presentation owners during origin recentering. That manufactured a second full cleanup receipt for a generation whose first receipt was still advancing; the duplicate guard threw and the broad retry path replayed the already-committed detach 243 times.

Keep pending live projections through the spatial identity map without issuing another receipt, and fail fast when a receipt-ledger invariant occurs after detachment. Evidence: docs/research/2026-08-02-collision-throughput-handoff/p1-retirement-receipt-loop.md. Release suite, lifecycle gate, and nine-stop soak pass.
2026-08-02 20:53:11 +02:00

4.3 KiB
Raw Blame History

P1 — origin-recenter retirement-receipt loop

Observed failure

launch-feeltest-oclone.log contains 243 consecutive failures with this shape:

streaming: origin-recenter preparation will resume:
InvalidOperationException: Landblock 0xC85AFFFF already has a full
retirement receipt.

The stack is StreamingController.TryAdvanceOriginRecenterPreparationLandblockPresentationPipeline.DetachAllForOriginRecenterLandblockRetirementCoordinator.AdoptDetachedFull.

Root cause

An ordinary full retirement detaches every landblock-owned presentation resource first, then parks surviving live entities in GpuWorldState._pendingByLandblock while the exact cleanup ticket advances asynchronously (GpuWorldState.DetachLandblock, around lines 11881314).

The origin-recenter swap incorrectly treated every pending-only live bucket as another landblock presentation generation (GpuWorldState.cs, former lines 13521353). It therefore emitted a second full cleanup receipt for the same already-retired generation. LandblockRetirementCoordinator correctly rejected that duplicate at lines 416425. Because spatial detachment had already committed, the broad retry catch in StreamingController.TryAdvanceOriginRecenterPreparation then repeated the detach against the changed state every frame.

The pre-fix regression test OriginRecenterAdoption_PendingOnlyLiveProjectionDoesNotCreateSecondFullReceipt failed because the recenter returned one receipt for the pending-only bucket.

Retail and reference boundary

Retail destroys one concrete landblock owner synchronously: CLandBlock::destroy_static_objects (0x0052FA50) leaves and deletes the landblock's static objects; CLandBlock::Destroy (0x0052FAA0) releases its buildings and landblock data; CLandBlock::release_all (0x0052FCF0) releases the landblock's object and visibility ownership. A live object parked outside a loaded landblock is not a second CLandBlock and therefore cannot create a second landblock-destruction transaction.

The extracted WorldBuilder reference follows the same ownership boundary: ObjectRenderManagerBase removes an actual _landblocks entry before UnloadLandblockResources, and PortalRenderManager only unloads a removed PortalLandblock. Neither treats an independently parked object as a new landblock resource owner.

Acdream retains its approved asynchronous adaptation: the first exact receipt owns cleanup, while the live projection survives spatial recentering.

Fix

  • GpuWorldState.DetachAllForOriginRecenter no longer creates retirement receipts from _pendingByLandblock alone. Pending live identities are still captured from _projectionLocations, cleared atomically, and re-parked unchanged.
  • A landblock that also owns loaded, pending-render, pending-near, tier, or bounds state still receives its exact full receipt.
  • A receipt-ledger invariant thrown after spatial detachment is now surfaced as a committed StreamingMutationException; it is terminal rather than falsely logged as resumable work.
  • The genuine duplicate-receipt guard remains unchanged.

Deterministic evidence

  • The new pending-only regression failed before the source fix and passes afterward.
  • OriginRecenter_PendingOnlyLiveProjectionKeepsItsExistingRetirementOwner drives the production recenter/controller sequence and proves the origin commits while the first cleanup ticket remains pending.
  • OriginRecenter_CommittedReceiptInvariantFailsFastInsteadOfReplayingDetach proves a genuine post-detach ledger violation surfaces once rather than entering a frame-by-frame retry loop.
  • The complete OriginRecenter focused group passes 20/20.

Gate evidence

  • Release build: 0 errors (21 pre-existing warnings).
  • Complete Release suite: 10,815 passed, 0 failed, 4 skipped.
  • Connected lifecycle/reconnect gate: logs/connected-world-gate-20260802-203751/report.jsonPassed=true.
  • Connected nine-stop soak: logs/connected-r6-soak-20260802-204309.report.jsonPassed=true, Failures=[], graceful exit, all 9 canonical checkpoints present, no wait cue, no pending landblock retirement, no reveal invariant failure, and no render-shadow mismatch.
  • The soak artifacts contain zero occurrences of already has a full retirement receipt; the captured failing session had 243.