# #280 — portal destination prefetch: pinned contract **Slice:** Placement cutover campaign, plan item 3 (`docs/plans/2026-08-02-placement-cutover.md:65`). **Base:** worktree `.claude/worktrees/peaceful-visvesvaraya-e0a196`, branch `claude/acdream-physics-divergence-5aa784`, HEAD `9ee9c1a1`. **Status:** contract only. No production or test code written; no commit made. **Retail binary used for byte verification:** `C:\Users\erikn\Downloads\acclient.exe`, PE timestamp `0x52291f34` (2013-09-06T00:17:56Z), CodeView GUID `9e847e2f-777c-4bd9-886c-22256bb87f32`, image base `0x00400000`. `py tools/pdb-extract/check_exe_pdb.py` prints `=== MATCH: this exe pairs with our acclient.pdb ===`. Every retail constant below that carries a "(byte-verified)" tag was read out of that image, not out of Binary Ninja's pseudo-C. --- ## 0. One-paragraph statement of the slice Retail loads, draws, and blocks on exactly one square of landblocks whose half-width is the user's landscape draw-distance preference; there is no second, smaller "reveal" radius, because the loaded set and the drawable set are the same array. acdream splits that into a two-tier streaming window (Near/Far) plus a reveal barrier gated at a hardcoded radius of 1. The barrier therefore opens the viewport when one landblock ring is complete while the user can see roughly twelve — which is #280. The fix derives the reveal radius from the live streaming window instead of hardcoding it, and makes the render-completeness predicate tier-aware so the outer rings can actually satisfy it. It is **both** a radius change and a predicate change; either alone is broken (§4). --- ## 1. Retail ground truth ### 1.1 There is exactly one radius, and it is the user's draw-distance setting `LScape` owns the loaded landscape as a flat `mid_width × mid_width` array of `CLandBlock*`: - `LScape::SetMidRadius` @ `0x00504C00` (pseudo-C `:266528`): ``` if (arg2 < 1 || this->land_blocks != 0) return 0; this->mid_radius = arg2; this->mid_width = arg2 * 2 + 1; return 1; ``` Note the second guard: **the radius cannot be changed while the block array is allocated.** Callers must reset first (see §1.5). - `LScape::LScape` @ `0x00505370` (`:267007`) constructs with `mid_radius = 5`, `mid_width = 0xB`. **(byte-verified** — file offset for `0x00505390` reads `c706 05000000` = `mov dword [esi], 5`, `c74604 0b000000` = `mov dword [esi+4], 0xB`.**)** This is the pre-preferences default only. - `LScape::update_block` @ `0x005063A0` (`:267954`) allocates `operator new[](mid_width * mid_width * 4)` — the array *is* the loaded landscape. - `LScape::update_viewpoint` / `LScape::set_viewer_block` @ `0x00505C70` (`:267520`) index that same array by `viewer_b_xoff/viewer_b_yoff = ((mid_radius << 3) - origin + coord) >> 3`, and `block_draw_list` (allocated alongside `land_blocks`, freed together at `0x00504BCB`) is the draw order over it. The value assigned to `mid_radius` comes from one place: - `SmartBox::SetRegion` @ `0x004531F0` (`:92064`): `SmartBox::set_mid_radius(this, Render::m_RenderPrefs.LandscapeDrawDistance);` - `Render::GRPCallback_OnRenderPreferenceChanged` @ `0x0054D9A8`–`0x0054DA43` (`:344372`, `:344422`): when `Render::m_RenderPrefs.LandscapeDrawDistance` differs from the cached `Current_Render_LandscapeDrawDistance`, it re-issues `SmartBox::set_mid_radius(SmartBox::smartbox, Render::m_RenderPrefs.LandscapeDrawDistance)`. `Render.LandscapeDrawDistance` is a registered user preference — `UserPreferences::RegisterPreference(&Render::m_RenderPrefs.LandscapeDrawDistance, &Render_LandscapeDrawDistance, …, 6, 0x86f2a4, &Render_LandscapeDrawDistance_Values)` @ `0x0054ECBE` (`:345471`), string `"Render.LandscapeDrawDistance"` @ `0x006C33CA`, UI string id `ID_Graphics_LandscapeDrawDistanc…` @ `0x004041B7`. **The full ladder, byte-verified.** `uint32_t const Render_LandscapeDrawDistance_Values[6]` @ `0x007CA988` (`:1021469`) reads, from the image: | Choice label (@`0x006C363A`+) | Value | Loaded/drawn square | Half-extent @192 m/LB | |---|---|---|---| | `VeryLow` | 3 | 7×7 | 576 m | | *(idx 1, `data_793f8c`)* | 5 | 11×11 | 960 m | | `Medium` | 8 | 17×17 | 1536 m | | `High` | 11 | 23×23 | 2112 m | | `VeryHigh` | 15 | 31×31 | 2880 m | | `Extreme` | 25 | 51×51 | 4800 m | **Default = 8** — two independent sites, both byte-verified: `PlayerOptionPage::AddMenuOption(...)->SetDefaultValue(8)` @ `0x0049E70D` (`:169449`; image bytes at `0x0049E6F0` contain `6a 08 … ff 92 d8020000` = `push 8; call [edx+0x2D8]`), and `Render::m_RenderPrefs.LandscapeDrawDistance = 8` @ `0x0054EF0B` (`:345535`; image bytes `c705 a4ef8100 08000000`). The overall-quality presets (`Render::SetOverallGraphicsQuality` @ `0x0054B020`, `:341947`) map quality 1..5 onto the first five of those values: 3, 5, 8, 11, 15 (`:341956`, `:341970`, `:341982`, `:341993`, `:342004`). **Consequence, and this is the load-bearing retail fact for the whole slice:** retail's prefetch window, its loaded window, and its drawable landscape window are *the same square*. There is no retail configuration in which the client streams farther than it gates, because there is only one number. ### 1.2 The far clip plane is not the landscape bound `float Render::zfar` is statically initialised to **4000.0** (`:1101868`; **byte-verified** at `0x0081EC88` = `4000.0f`). The only `Render::set_zfar` calls are `GameSky::Draw` @ `0x00507055` / `0x005070EE` (`:268724`, `:268765`), which temporarily multiply it by 4 for the skybox and restore it. At the default `mid_radius = 8` the landscape ends at 1536 m, well inside `zfar`. **The landscape horizon is the prefetch square, not the frustum.** ### 1.3 `m_bUseViewDistance` is NOT a view-distance setting Because it was raised as a candidate: `SmartBox::SetOverrideFovDistance` @ `0x00451BC0` (`:90783`) sets `m_bUseViewDistance` / `m_fViewDistFOV`, and the two readers — `CreatureMode`-style camera setup @ `0x00452AFD` (`:91723`) and `SmartBox::DrawNoBlit` @ `0x00453AE6` (`:92655`) — use it as a **projection parameterisation switch**, not a distance: ``` if (m_bUseViewDistance == 0) Render::SetFOVRad(m_fGameFOV / (aspect - 0.1f)); else Render::set_vdst(m_fViewDistFOV); ``` and `Render::set_vdst` @ `0x0054B240` (`:342121`) is `SetFOVInternal(2 * atan(x))` with `znear = (x < 0.4) ? 0.1 : x * 0.25`. It is "frame the camera to see an object this far away" — an FOV, used by the creature/portrait camera path. It has no relationship to `mid_radius`, to `LandscapeDrawDistance`, or to streaming. **Do not build anything on it.** ### 1.4 What "each required cell is in" actually tests `LScape::PreFetchCells` @ `0x00505660` (`:267154`) walks `for dy in -mid_radius..mid_radius: for dx in -mid_radius..mid_radius` (`:267172-267255`), computes each landblock DID `(((x & ~7) << 5) | (y >> 3)) << 16 | 0xFFFF` (`:267199`), skips out-of-bounds (`< 0 || >= 0x7F8`, i.e. off-map), and for each in-bounds landblock: 1. `DBObj::PreFetch(qdid(did, type 1))`. If the result is neither `CACHE_OBJECT_IN_MEMORY` nor `CACHE_OBJECT_IN_FILE`, `result = 0`; if it is `CACHE_OBJECT_LOOKING`, also `*waitingCount += 1`. 2. Otherwise `DBObj::Get(...)`. If `Get` returns null (present in the file but not yet resident), it additionally prefetches the LandBlockInfo record `(did & ~1) | 0xFFFE` as type 2, sets `result = 0`, and `*waitingCount += 1`. 3. Otherwise `CLandBlock::PreFetchCells(block)` @ `0x00530240` (`:314195`), which prefetches the LandBlockInfo type-2 record and, via `CLandBlockInfo::PreFetchCells` @ `0x0052E7C0` (`:312329`), walks every building and calls `CBldPortal::PreFetchCells` @ `0x0053BD00` (`:325061`) for each, which prefetches every `stab_list[i]` as type 3 (the building's EnvCells). Any failure propagates `result = 0`. So retail's completeness predicate is: **for every in-bounds landblock in the `mid_radius` square — its terrain record, its LandBlockInfo, and every EnvCell of every building it contains are resident.** It is a static-DAT residency test, not a render-upload test (retail uploads synchronously), and it does not include procedural scenery (derived from the terrain record it already required). Indoor destinations take a different arm: `CellManager::PreFetchCells` @ `0x00455820` (`:94471`) dispatches on `cellIndex >= 0x100` to `CEnvCell::PreFetchCells` @ `0x0052D1E0` (`:310659`), which walks the EnvCell's visible-cell graph recursively; and `CEnvCell::PreFetchCells` @ `0x0052C460` (`:309754`) additionally requires the whole `mid_radius` landscape square when `seen_outside != 0` (`:309759`). **Indoor cells that can see outside still require the full outdoor square.** ### 1.5 What `blocking_for_cells` gates, and how it clears `CellManager::PreFetchCells` @ `0x00455820`: - Early-outs to "available" if `DBCache::IsLoader()` (`:94477`). - Outdoor arm only re-sweeps if `blocking != 0 || all_cells_available == 0 || ((last_prefetch_cell_id ^ cellId) & 0xFFFF0000) != 0` — i.e. standing still in an already-complete landblock is free (`:94496`). - When called with `blocking != 0` and cells are missing, it reports `ECM_DDD::SendNotice_RuntimeDDDStatus(1, remaining, total)` and latches `this->blocking_for_cells = 1` (`:94538-94541`). - When everything is in, it clears the latch and reports `SendNotice_RuntimeDDDStatus(0,0,0)` (`:94549-94552`). - `CellManager::Reset` @ `0x00455930` (`:94588`) also clears it. The latch gates the **entire simulation**. `SmartBox::UseTime` @ `0x00455410` (`:94168`): ``` if (cell_manager->blocking_for_cells == 0) { if (!all_cells_available && CheckPrefetchStatus()) UpdateLoadPoint(); CellManager::ChangePosition(player->m_position, /*blocking*/ 0); ... position_update_complete / has_been_teleported latch ... CObjectMaint::UseTime(); CPhysics::UseTime(); GameTime::UseTime(); LScape::UseTime(); Ambient::UseTime(); } else { CellManager::CheckPrefetchStatus(cell_manager); // and nothing else } SceneTool::Think(); ... drain the inbound NetBlob queue and dispatch ... cmdinterp->UseTime(); Render::CalcDegLevel(); ``` So while blocked: **no object maintenance, no physics, no game clock, no landscape update, no ambient sound.** Networking still drains and events still dispatch. This is the exact behaviour AD-2 already cites. The retry is rate-limited. `CellManager::CheckPrefetchStatus` @ `0x00455BE0` (`:94734`) compares `Timer::cur_time - last_prefetch_check` against a qword constant at `0x007991B0`; **byte-verified as `5.0`** (`0000000000001440`). The instruction sequence at `0x00455BE0` is `fld qword [0x8369A8]; fsub qword [esi+0x10]; fcomp qword [0x7991B0]; fnstsw ax; test ah,0x41; jnz` — `CF|ZF` after `fcomp` means "elapsed < 5.0 or elapsed == 5.0", so the function returns 0 without re-sweeping. **Retail's blocked hold is therefore quantised to 5-second poll intervals.** (BN renders the tail `-((eax_3 - eax_3))`; the image is `neg eax; sbb eax,eax; neg eax`, an ordinary boolean normalise of the `PreFetchCells` result. This is one of the flag-test drops the C5b review warned about; it is benign here.) Which callers block: | Site | Call | Blocking? | |---|---|---| | `SmartBox::HandleCreateObject` (initial player) @ `0x00455069` (`:93814`) | `ChangePosition(pos, 1)` | **yes** | | `SmartBox::PlayerPositionUpdated` @ `0x00453903` (`:92508`), teleport arm | `ChangePosition(pos, arg2 != 0)` | **yes on teleport** | | `SmartBox::PlayerPositionUpdated`, ordinary arm | `ChangePosition(pos, 0)` | no | | `SmartBox::UseTime` @ `0x00455462` (`:94180`) | `ChangePosition(pos, 0)` | no | | `SmartBox::set_mid_radius` @ `0x00453180` (re-arm branch at `0x004531D0`, `:92053`) | `ChangePosition(pos, 1)` | **yes, if already blocking** | `CellManager::ChangePosition` @ `0x004559B0` (`:94601`) additionally promotes any call to blocking while the latch is set (`if (blocking_for_cells == 0) edi = arg3;` — i.e. once latched, always blocking until cleared). **`SmartBox::set_mid_radius` @ `0x00453180` (`:92036`) is the retail answer to "what if the radius changes mid-hold":** ``` ebx = cell_manager->blocking_for_cells; CellManager::Reset(cell_manager); // clears the latch, releases lscape ok = LScape::SetMidRadius(this->lscape, arg2) != 0; if (ok && ebx != 0 && player && player->m_position.objcell_id != 0) CellManager::ChangePosition(this->cell_manager, &player->m_position, 1); ``` Reset, re-radius, and **re-arm the blocking prefetch at the new radius**. It does not finish the old hold at the old radius and it does not ignore the change. ### 1.6 What the user sees while blocked Two things, both confirmed: 1. **The DDD progress readout.** `ECM_DDD::SendNotice_RuntimeDDDStatus(active, remaining, total)` @ `0x00692870` reaches `gmPowerbarUI::RecvNotice_RuntimeDDDStatus` @ `0x004DA5C0` (`:222574`), which sets a text element to string id `ID_Powerbar_DDDModeText` with `CURRENT`/`TOTAL` integer variables and writes `current/total` into a float attribute `0x69` on element `0x10000034` — a **progress bar with an "N of M" cell count on the powerbar**. On `active == 0` it restores normal state. 2. **The portal-space notice.** `gmSmartBoxUI::UseTime` @ `0x004D6E30` (`:219400`): while `teleportAnimState == TAS_TUNNEL`, each time the current rotation segment expires (`teleportRotationStartTime + teleportRotationDuration <= cur_time`, `:004D6FC7`) it picks a fresh random segment and emits `ECM_UI::SendNotice_DisplayStringInfo(0x1A, "In Portal Space - Please Wait...")` (literal at `0x004D7064`, `:219516`). It repeats every rotation segment for as long as the tunnel runs. Also relevant: on the initial-entry path `SmartBox::hidden = 1` is set (`0x004553F7`, `:94139`) so `SmartBox::Draw` @ `0x00455570` returns without drawing at all. On a mid-session teleport the portal tunnel (`m_pPortalSpace`) is made visible and `SmartBox::Hide(m_pSmartBox)` is called (`0x004D6FA3`/`0x004D6FB6`) — the world is not drawn behind the tunnel either. **So: retail blocks. It does not reveal progressively.** It freezes simulation, hides the world, shows a tunnel plus a repeating wait string plus a cell-count progress bar, and only resumes once every landblock in the full draw-distance square is resident. --- ## 2. acdream ground truth at HEAD `9ee9c1a1` Every path and line below was read at this HEAD. Do not inherit line numbers from the issue, the campaign plan, the streaming memory doc, or this contract into a later session without re-verifying. ### 2.1 The reveal barrier `src/AcDream.App/Streaming/WorldRevealReadinessBarrier.cs` — `internal sealed class`, 147 lines, seven injected delegates. - `:38` `internal const int OutdoorNeighborhoodRadius = 1;` - `:143-146` ```csharp internal static int RequiredRenderRadius(uint destinationCell) => IsIndoor(destinationCell) ? 0 : OutdoorNeighborhoodRadius; private static bool IsIndoor(uint cellId) => (cellId & 0xFFFFu) >= 0x0100u; ``` Note it is **`static`** — that is the first structural obstacle to a derived radius. - `:107-141` `Evaluate` short-circuits render → composites → collision and returns a `WorldRevealReadinessSnapshot` (`:9`) whose `IsReady` is `HasDestination && (IsUnhydratable || (render && composites && collision))`. - `:84-92` `Prepare` calls `_prepareCompositeTextures(destinationCell, radius)` with the **same** radius. Production wiring, `src/AcDream.App/Composition/SessionPlayerComposition.cs:371-396`: | Delegate | Implementation | |---|---| | `isRenderNeighborhoodReady` | `StreamingController.IsRenderNeighborhoodResident` (`src/AcDream.App/Streaming/StreamingController.cs:227`) | | `isTerrainNeighborhoodReady` | `PhysicsEngine.IsNeighborhoodTerrainResident` (`src/AcDream.Core/Physics/PhysicsEngine.cs:129`) | | `isSpawnCellReady` | `PhysicsEngine.IsSpawnCellReady` (`:1797`) | | `areCompositeTexturesReady` | `WbDrawDispatcher.CompositeTexturesReady` | | `prepareCompositeTextures` | `CompositeWarmupEntitySource.Refresh` + `WbDrawDispatcher.PrepareCompositeTextures` | | `invalidateCompositeTextures` | `CompositeWarmupEntitySource.Reset` + `WbDrawDispatcher.InvalidateCompositeWarmupReadiness` | | `isSpawnClaimUnhydratable` | `DatSpawnClaimHydrationClassifier.IsUnhydratable` | ### 2.2 The radius `1` exists in FOUR places, three of them uncited | # | Site | Shape | |---|---|---| | 1 | `src/AcDream.App/Streaming/WorldRevealReadinessBarrier.cs:38,144` | the named constant | | 2 | `src/AcDream.Runtime/World/RuntimeWorldTransitState.cs:573` | `int requiredRenderRadius = isIndoor ? 0 : 1;` — **a validating invariant** | | 3 | `src/AcDream.Runtime/Session/RuntimeLiveEntitySessionController.cs:776` | `RequiredRenderRadius: indoor ? 0 : 1` (producer) | | 4 | `src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs:973` | `RequiredRenderRadius: indoor ? 0 : 1` (producer) | Site 2 is the sharp edge. `RuntimeWorldTransitState.AcknowledgeDestinationReadiness` re-derives the expected radius and calls `FailInvariant("invalid-readiness-shape", …)` on mismatch. **Changing `OutdoorNeighborhoodRadius` alone makes every graphical readiness acknowledgement fail this invariant, and the reveal never opens.** This is the C5b lesson in its purest form — a contract asserting a mechanism, with the mechanism's value copied rather than referenced. ### 2.3 The render-completeness predicate refuses Far-tier landblocks `StreamingController.IsRenderNeighborhoodResident` (`:227-254`), the ring body: ```csharp uint canonical = ((uint)nx << 24) | ((uint)ny << 16) | 0xFFFFu; if (!_state.IsNearTier(canonical) || !_state.IsRenderReady(canonical)) return false; ``` `GpuWorldState.IsNearTier` (`src/AcDream.App/Streaming/GpuWorldState.cs:176`) is true only for `LandblockStreamTier.Near`. **A Far-tier landblock can never satisfy this predicate.** Since the Near window is `NearRadius` (4 at the default preset), any reveal radius above 4 would hang forever. That is why #280 cannot be fixed by editing the constant. `GpuWorldState.IsRenderReady` (`:180`) is `_loaded.ContainsKey(id) && (_wbSpawnAdapter?.IsLandblockRenderReady(id) ?? true)`. Verified that a Far-tier landblock **does** get a spawn-adapter registration and therefore *is* render-ready: `PublicationKind.Far` flows through `LandblockPresentationPipeline.cs:900-923` → `GpuWorldState.CommitLandblockSpatial` → `ActivateLandblockPresentation` (`GpuWorldState.cs:1009`) → `_wbSpawnAdapter.OnLandblockLoaded(...)` (`:1022`), which creates a registration with `WantsLoaded = true` (`src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs:98-101`) and an empty `Ordinary`/`Prepared` set, so `IsLandblockRenderReady` (`:135-152`) returns true. **`IsNearTier` is the sole blocker.** `PhysicsEngine.IsNeighborhoodTerrainResident` (`:129-146`) is already tier-agnostic: it keys off `PhysicsEngine._landblocks`, and Far-tier publication does construct the terrain surface (`src/AcDream.App/Streaming/LandblockPhysicsPublisher.cs:390-403`, reached for `PublicationKind.Far` via `LandblockPresentationPipeline.cs:619`). **It also allocates a fresh `HashSet` over every resident landblock on every call** (`:131-133`) — see §7. ### 2.4 The streaming window and what the user can actually see `src/AcDream.UI.Abstractions/Settings/QualityPreset.cs:29-36`: | Preset | NearRadius | FarRadius | Near ring | Far window | |---|---|---|---|---| | Low | 2 | 5 | 5×5 | 11×11 | | Medium | 3 | 8 | 7×7 | 17×17 | | **High (default)** | **4** | **12** | 9×9 | 25×25 | | Ultra | 5 | 15 | 11×11 | 31×31 | Default preset is `High` (`src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs:52`). Per-axis env overrides `ACDREAM_NEAR_RADIUS` / `ACDREAM_FAR_RADIUS` (`QualityPreset.cs:45-46`). Radii are **runtime-mutable** via `StreamingController.ReconfigureRadii` (`StreamingController.cs:377`), driven from the Settings panel through `RuntimeSettingsTargets.ApplyQuality` (`src/AcDream.App/Settings/RuntimeSettingsTargets.cs:247-253`). Tier contents: - **Far** = the LandBlock heightmap record only (`src/AcDream.App/Streaming/LandblockBuildFactory.cs:131-142`), empty entity list, `PhysicsDatBundle.Empty`, and the prepared-collision closure is skipped (`:97-101`) — but the terrain render mesh and terrain collision surface *are* published. - **Near** = LandBlock + LandBlockInfo, static entities, procedural scenery, EnvCell shells, interior statics, prepared collision closure (`LandblockBuildFactory.cs:144-202`). Visible extent: - Far plane: hardcoded `5000f` in every camera (`src/AcDream.App/Rendering/RetailChaseCamera.cs:56`, and identically in `ChaseCamera.cs:64`, `FlyCamera.cs:37`, `OrbitCamera.cs:28`). No config path. Retail's is 4000. - Fog, derived from the streaming radii — `src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs:481-491` with `LandblockSize = 192f` (`:418`): `fogStart = NearRadius * 192 * FogStartMultiplier`, `fogEnd = FarRadius * 192 * FogEndMultiplier`, defaults `0.7` / `0.95` (`src/AcDream.App/RuntimeOptions.cs:145-146`). At High: **fogStart 537.6 m, fogEnd 2188.8 m.** ### 2.5 The defect, quantified At the shipped default the user can see terrain to roughly **2189 m** (fog end, inside a 2304 m Far window). The reveal barrier opens once **192 m** — the centre landblock plus one ring — is Near-tier complete. That is an **11.4:1** gap, and the far end of it is exactly where the user reported watching the world assemble. Retail's equivalent gap is 1:1 by construction (§1.1). ### 2.6 The reveal lifecycle owners (post-J6.2) Canonical: `src/AcDream.Runtime/World/RuntimeWorldTransitState.cs` (`public sealed class … : IRuntimePortalView`). | Concern | Owner | Line | |---|---|---| | reveal generation | `BeginRevealCore` (`checked(++_nextGeneration)`) | `:386` (entered `:135`, `:159`) | | destination/readiness latch | `AcknowledgeDestinationReadiness` | `:558-593` | | materialization/simulation edge | `AcknowledgePortalMaterialized` | `:595-638` | | viewport | `AcknowledgeWorldViewportVisible` | `:640-660` | | completion / cancellation | `Complete` / `Cancel` | `:685`, `:722` | | wait-cue latch | `ObserveWait`, `RetailWaitCueDelay = 5 s` | `:662-683`, `:66` | | host projection lifetime | `TryRegisterHostProjection` … | `:189`, `:282`, `:299`, `:309` | Graphical adapter: `WorldRevealCoordinator` (`src/AcDream.App/Streaming/WorldRevealCoordinator.cs:38`); it owns the barrier (`:58`, `:80-87`), bridges App readiness into the Runtime latch in `Evaluate` (`:183-202`), and computes the reservation radius via the static `WorldRevealReadinessBarrier.RequiredRenderRadius` at `:167`, handing it to `IWorldRevealStreamingScheduler.BeginDestinationReservation` at `:408-411`. Reveal decision (portal): `LocalPlayerTeleportController.Tick` (`src/AcDream.App/Streaming/LocalPlayerTeleportController.cs:486`, decision block `:503-539`): ```csharp bool dataReady = haveDestination && originReady && _worldReveal.Evaluate(_pendingCell).IsReady; bool placementReady = dataReady && TryAdvancePortalCommit(sequence); if (haveDestination && !placementReady) _holdSeconds += deltaSeconds; _presentation.SetWaitCue(haveDestination && !placementReady && _worldReveal.ObserveWait(TimeSpan.FromSeconds(_holdSeconds))); ``` Reveal decision (login): `LivePlayerModeAutoEntryContext.IsWorldReady` (`src/AcDream.App/Input/PlayerModeAutoEntry.cs:106-111`). **Slice E's hold mechanism is correct and already in place.** #280 is not a missing hold — it is the hold measuring the wrong domain. Say this plainly in the commit message; it is the difference between a two-file change and a redesign. ### 2.7 The destination reservation `StreamingController` `DestinationReservation(long RevealGeneration, uint LandblockId, int Radius)` (`:24-27`), opened at `:145-161`, radius supplied by the coordinator = the reveal radius. Two effects: enqueue-order priority (`EnqueueLoadsByRevealPriority` `:994-1012`, membership `IsDestinationWork` `:1079-1082`, Chebyshev ≤ radius) and a budget lane reservation of `DestinationReserveFraction = 0.75` (`src/AcDream.App/Streaming/StreamingWorkBudgetOptions.cs:33`; enforcement `StreamingWorkBudget.cs:385-438`) capping non-destination work at 25% of every dimension. --- ## 3. The scoping question: derive from what? **Retail's `mid_radius` is derived from nothing — it *is* the user's landscape-draw-distance preference, assigned directly** (§1.1). It is not a function of `m_bUseViewDistance` (§1.3) and not a function of the frustum (§1.2). Retail exposes exactly one landscape-extent number and uses it for loading, drawing, and blocking. **acdream has no view-distance setting.** `ViewDistance` / `view_distance` / `LandscapeDrawDistance` / `DrawDistance` return zero hits under `src/`. What acdream has is `QualitySettings.FarRadius` — a per-preset landblock radius that bounds the loaded and drawn landscape and drives the fog end. **`FarRadius` is acdream's structural analogue of retail's `LandscapeDrawDistance`,** and the two ladders are strikingly close (retail 3/5/8/11/15/25 vs acdream 5/8/12/15). Therefore: > **D0 — #280 derives its window from the live streaming radii > (`NearRadius`, `FarRadius`) and does NOT depend on a Viewing Distance > option existing.** Retail's Viewing Distance *option* — a dedicated, user-facing, six-position enum with retail's exact values, replacing or reparameterising the quality preset's radii — **is a genuinely separate missing feature.** It is not required for #280 and must not be invented inside it. File it as its own issue (suggested text in §14). When it lands, it lands by changing what feeds `NearRadius`/`FarRadius`; #280's derivation keeps working untouched, which is the point of deriving rather than duplicating. **No standalone prefetch knob.** The defect *is* the decoupling of the reveal window from the visible window; a knob re-exposes it as a feature and a low setting reintroduces the bug. A **diagnostic** override is legitimate and belongs in a `PhysicsDiagnostics`-style owner per CLAUDE.md rule 5 — see D5. **Runtime mutability.** The radii can change mid-session (`ReconfigureRadii` `:377`). Retail's answer is unambiguous (`SmartBox::set_mid_radius` @ `0x00453180`, §1.5): reset, re-radius, and re-arm the blocking prefetch at the **new** value. So acdream must read the radii **live**, per evaluation, not capture them at barrier construction — which falls out for free from D1 and needs no extra machinery, because `Evaluate` already runs every frame. See D4 for the reservation half, which does need explicit handling. --- ## 4. The change — pinned ### D1 — the barrier's radius becomes an instance value read live from the streaming window `WorldRevealReadinessBarrier` gains an injected `Func` (or equivalent two-int accessor) supplying the **current** `NearRadius` and `FarRadius`, and `RequiredRenderRadius(uint)` stops being `static`: ``` indoor -> 0 (unchanged; retail's EnvCell arm, §1.4) outdoor -> FarRadius (retail's mid_radius, §1.1) ``` The snapshot record gains the near/far split so the predicate and the Runtime acknowledgement carry the same shape. Composite preparation (`Prepare`, `:84-92`) keeps using **`NearRadius`**, not `FarRadius` — see D3. Rationale for `FarRadius` rather than `NearRadius`: retail gates on the whole drawn square, and acdream's drawn square is the Far window (fog end 2189 m < Far extent 2304 m at every preset, since `FogEndMultiplier = 0.95 < 1`, so `FarRadius` always covers everything the user can see). Gating at `NearRadius` would fix only the near-detail pop and leave the reported far-terrain symptom intact. ### D2 — the render predicate becomes tier-aware `StreamingController.IsRenderNeighborhoodResident` takes a near radius and a far radius, and per ring member at Chebyshev distance `d`: ``` d <= nearRadius : require IsNearTier(canonical) && IsRenderReady(canonical) d <= farRadius : require IsLoaded(canonical) && IsRenderReady(canonical) ``` Off-map coordinates keep being skipped (`nx/ny` outside `0..254`), matching retail's `>= 0x7F8` bounds skip (§1.4) and the existing comment at `StreamingController.cs:244-245`. This is the half without which D1 cannot work: today's `IsNearTier` requirement makes any radius above `NearRadius` unsatisfiable (§2.3). `IsRenderReady` is already true for a published Far landblock (verified, §2.3), so the outer arm is a real test of "this landblock's terrain is published and drawable", not a rubber stamp. `PhysicsEngine.IsNeighborhoodTerrainResident` needs no semantic change — it is already tier-agnostic and Far publishes terrain collision — but it needs the allocation fix in D6. ### D3 — composites stay at the near radius, and this is a fact about the data, not a shortcut `WbDrawDispatcher.IsCompositeWarmupCandidate` (`src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs:335-360`) filters **entities** by `IsWithinLandblockRadius` (`:451-457`) and admits only those with a `PaletteOverride` or per-mesh `SurfaceOverrides`. Far-tier landblocks carry **zero entities** (`LandblockBuildFactory.cs:131-142`), so widening the composite radius over Far rings warms nothing while walking 625 landblocks' worth of nothing. Passing `NearRadius` is the honest domain, not a scope cut. Pin it explicitly so a later reader does not "fix the inconsistency" by widening it. ### D4 — the destination reservation uses the same radius as the gate `WorldRevealCoordinator` (`:167`, `:408-411`) hands `BeginDestinationReservation` the new (Far) radius. Retail has one square for prefetch and for blocking; it has no concept of prioritising an inner ring differently, and splitting them here would invent a mechanism retail does not have. Consequence to measure, not to assume (§8-P4): with the reservation covering the whole Far window, "non-destination work" during a hold shrinks to out-of-window traffic (retirements, unloads, live entities), which the 25% lane must still drain. If the measurement shows retirement/unload backlog growth across a hold, that is a real finding and gets its own issue — **not** a quiet radius split inside this slice. Runtime mutability: if the radii change while a reservation is open, follow retail — end the reservation and reopen it at the new radius on the same reveal generation. `EndDestinationReservation` already refuses a generation mismatch (`:145-183`), so the reopen is generation-safe. ### D5 — the diagnostic override One diagnostic-owner property (CLAUDE.md rule 5), read once at startup, e.g. `StreamingDiagnostics.RevealRadiusOverride` from `ACDREAM_PROBE_REVEAL_RADIUS`. Default unset ⇒ D1's derivation. Its only purpose is A/B measurement of the stall (§7) and reproduction of the pre-fix behaviour during the connected gate (§10). It is **not** a user setting, is not surfaced in Settings, and is not persisted. Do not add it to `RuntimeOptions` as a general knob — it is a probe. ### D6 — `IsNeighborhoodTerrainResident` stops allocating per call `src/AcDream.Core/Physics/PhysicsEngine.cs:129-146` builds a `HashSet` over every resident landblock on **every** call. It is called every frame during a hold (via `RenderFrameResourceController.Prepare`, `src/AcDream.App/Rendering/RenderFrameResourceController.cs:256-265`). Today that is 9 ring members and one set build; after D1 it is up to 625 ring members and the same set build, at 30–60 Hz, against Slice I1's "0 B/resolve" standard. Replace the set build with direct per-member lookups against the existing landblock map. This is a direct cost of the change, not opportunistic cleanup — it ships in the same commit. ### D7 — the Runtime invariant stops re-encoding the App's value `RuntimeWorldTransitState.cs:573` currently asserts `RequiredRenderRadius == (isIndoor ? 0 : 1)`. Runtime must not know the App's streaming configuration — that is the J-slice ownership boundary. Replace with a **shape** invariant that is still a real invariant: ``` indoor => RequiredRenderRadius == 0 outdoor => RequiredRenderRadius >= 1 ``` plus the existing `IsIndoor` consistency check, which stays. The two producers (`RuntimeLiveEntitySessionController.cs:776`, `HeadlessSessionWorldProjection.cs:973`) keep emitting `indoor ? 0 : 1` and remain legal: neither host has a streaming window, so "the centre ring" is the honest token there (§5, class C). Do **not** plumb the App's radii into Runtime to make the strict equality survive — that would be exactly the mechanism-that-does-not-exist assertion C5b was built to stop. ### D8 — register bookkeeping, in the implementation commit AD-2 in `docs/architecture/retail-divergence-register.md` is the row that covers this machinery and already cites `blocking_for_cells` and `SmartBox::UseTime @0x00455410`. It currently describes the outdoor gate as "terrain/collision residency for the required **Near** ring". Amend it in the same commit to state the derived Far-window gate, the two-tier completeness split (Near ring: full publication; Far ring: terrain publication), the `NearRadius`-scoped composite domain, and the anchor `LScape::PreFetchCells @0x00505660` / `Render_LandscapeDrawDistance_Values @0x007CA988`. A **new** row is required for the residual deviation the fix does not remove: acdream's outer reveal ring accepts terrain-only publication where retail requires the landblock's LandBlockInfo and every building EnvCell (§1.4). Risk column: a distant building or its interior shells can still pop in after reveal, at Far-ring distances, where retail would have blocked. This is a real, named, bounded residual — do not let the slice close claiming parity it does not have. --- ## 5. Blast radius, by consumer class, both hosts C5b's enumeration leaked because it was performed over one host's call graph. This one is organised by host first. ### Class A — graphical host, reveal-gating consumers. Verdict: INTENDED CHANGE. - `WorldRevealReadinessBarrier.Evaluate` / `IsReady` — the behaviour under change. - `LocalPlayerTeleportController.Tick:503-539` (portal `dataReady`) and `LivePlayerModeAutoEntryContext.IsWorldReady` (`PlayerModeAutoEntry.cs:106-111`) (login). Both hold longer. Login already held behind the same barrier, so first-login also gets the wider gate — **state this as intended**, and gate it (§10), because the issue only reported recall. - `RenderFrameResourceController.Prepare:256-265` — per-frame `PrepareAndEvaluate`. Frequency unchanged; per-call cost rises (D6). - `WorldRevealCoordinator.Evaluate:183-202` — forwards the wider snapshot into the Runtime latch. ### Class B — graphical host, streaming scheduling. Verdict: CHANGED BY DESIGN, measured. - `StreamingController.BeginDestinationReservation:145-161`, `IsDestinationWork:1079-1082`, `EnqueueLoadsByRevealPriority:994-1012` — the destination set grows from 9 to up to 625 landblocks (D4). - `StreamingWorkBudget` destination/non-destination lanes (`:385-438`) — the 25% non-destination cap now applies to a much larger fraction of the frame's work during a hold. Measured, P4. ### Class C — Runtime, canonical reveal state. Verdict: SHAPE LOOSENED, no behavioural change for existing producers. - `RuntimeWorldTransitState.AcknowledgeDestinationReadiness:558-593` — invariant loosened (D7). Every value the two non-graphical producers emit today remains legal. - `RuntimeDestinationReadiness` (`src/AcDream.Runtime/GameRuntimeViews.cs:114`) — carries the radius as data; its `IsReady` join is unchanged. - `RuntimeLiveEntitySessionController.TryAdvancePortalCompletion:757-799` — unchanged. ### Class D — no-window host. Verdict: UNAFFECTED, and that is correct. `HeadlessSessionWorldProjection.PrepareDestination` (`src/AcDream.Headless/Hosting/HeadlessSessionWorldProjection.cs:832`, readiness at `:955-976`) returns render-ready and composites-ready hardcoded `true`, with `IsCollisionReady` = "the placement committed". The headless host **has no streaming window, no render publication, and no composites**; there is nothing for a wider radius to mean. Its `RequiredRenderRadius: indoor ? 0 : 1` at `:973` is a token that satisfies the Runtime shape check, and D7 keeps it legal. Explicitly: **`AcDream.App` types must not appear in the headless path, and the headless radius must not be derived from anything.** The only way #280 can break headless is by tightening the Runtime invariant instead of loosening it — hence D7, hence the headless assertion in §9. ### Class E — presentation-only. Verdict: HARMLESS. `PortalTunnelPresentation` (`:290`, `:296-297`, `:381`), `PortalWaitNoticeController`, `LocalPlayerTeleportPresentation:353`, `RuntimeWorldFrameVisibilityPreparation.Begin` (`src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs:332-342`). These observe a longer hold; none of them gate anything. The wait cue's *frequency of appearance* changes — §6. ### Class F — tests. Verdict: MUST BE REWRITTEN, NOT RELAXED. `tests/AcDream.App.Tests/Streaming/WorldRevealReadinessBarrierTests.cs` (`:63`, `:69`, `:76`, `:140`) and `tests/AcDream.App.Tests/Streaming/LocalPlayerTeleportControllerTests.cs:65` assert the constant. §9 replaces them with derivation and tier assertions. Anything that "fixes" them by re-asserting a new literal is the trap in T3. --- ## 6. Interaction with the wait cue acdream's cue: `RuntimeWorldTransitState.ObserveWait:662-683`, threshold `RetailWaitCueDelay = TimeSpan.FromSeconds(5)` (`:66`), driven only from `LocalPlayerTeleportController.cs:536` via `WorldRevealCoordinator.ObserveWait:245-250`; rendered as a centered `UiText` `"PortalSpaceWaitNotice"` (`src/AcDream.App/UI/PortalWaitNoticeController.cs:10-37`) with the literal `"In Portal Space - Please Wait..."` (`src/AcDream.App/Rendering/PortalTunnelPresentation.cs:296-297`, `:381`). It is graphical-host-only; headless has no `ObserveWait` caller anywhere (its analogue is the parked-completion log at `RuntimeLiveEntitySessionController.cs:792-799`). **Pinned expectation:** after the fix the user will see `"In Portal Space - Please Wait..."` on recalls **more often**, and the tunnel will run longer before the world appears. **Longer holds ARE retail-convergent — but the argument below was wrong on both of its clauses, and is corrected here (2026-08-06, #280 retail-conformance review, finding F2).** The retail string is byte-identical (§1.6; VA `0x007BD6A8`, verified). Two things this section originally asserted are not true of the binary: 1. *"retail emits it for the whole duration of a blocked prefetch"* — the emit site is inside `gmSmartBoxUI::UseTime`'s `TAS_TUNNEL*` branch, in the `else` arm of the rotation-segment-expiry test at `0x004D6FCD`, and fires **unconditionally per tunnel rotation segment** whether or not `blocking_for_cells` is set. The cue is a property of being in the tunnel, not of being blocked. Byte-decoded at `0x004D6FE6`-`0x004D7049`: `teleportRotationDuration = RandDouble(0.6, 1.8)` s (`0x3ffccccc/0xcccccccd` = 1.8, `0x3fe33333/0x33333333` = 0.6), `teleportRotationEndAngle = RandDouble(0, 360)` (`0x40768000`). 2. *"whose retry is quantised to 5 s"* — the 5.0 constant at VA `0x007991B0` belongs to `CellManager::CheckPrefetchStatus` @`0x00455BE0`, the prefetch **retry cadence**. It has no connection to the UI notice. #280's commit message repeated this mis-attribution; it is retracted. What survives, and is the real justification: retail genuinely **blocks**. `SmartBox::UseTime` @`0x00455410` runs only `CheckPrefetchStatus` while `blocking_for_cells` is latched — object maintenance, physics, the game clock, landscape and ambient are all skipped. A retail client at `LandscapeDrawDistance = 8` recalling into a cold cache sits in the tunnel until all 289 landblocks are in. A longer acdream hold is therefore convergent *in kind*. **Two register rows ARE required, and were filed 2026-08-06:** - **AP-150** — acdream's `RetailWaitCueDelay = 5 s` arming is not retail's trigger (see the out-of-scope note immediately below, which was right; the error was concluding no row was needed). - **AP-151** — acdream's per-member predicate is much heavier than retail's (mesh build + GPU upload vs. DAT residency), so the hold is not merely "retail's, honestly measured", and nothing bounds it. Two adjacent facts, both **out of scope**, both worth writing down so a later session does not mistake them for #280 regressions: 1. acdream's 5-second threshold is not retail's trigger. Retail emits on each tunnel rotation-segment boundary (`0x004D6FC7` → `0x004D70A1`), not on a fixed elapsed threshold. The 5 s constant is an acdream approximation. Do not change it in this slice; if it is ever revisited, the anchor is `gmSmartBoxUI::UseTime @ 0x004D6E30`. 2. acdream has no analogue of retail's DDD progress bar (`gmPowerbarUI::RecvNotice_RuntimeDDDStatus @ 0x004DA5C0`, string `ID_Powerbar_DDDModeText`, `CURRENT`/`TOTAL`). With longer holds this becomes more noticeable. File it; do not build it here. --- ## 7. Performance risk, stated honestly **The change does not add streaming work.** The Far window is already 25×25 at the default preset; those landblocks are already queued and already built. #280 makes the reveal *wait* for a tail that is already being produced. What lengthens is the hold, not the workload — with three exceptions, each of which must be measured: 1. **Priority inversion at the reservation boundary (D4).** Growing the destination set from 9 to 625 landblocks changes what the 75% reserved lane prioritises. Best case it strictly helps (the tail we now wait on is the tail we now prioritise). Worst case out-of-window retirement and unload starve inside the 25% remainder. 2. **`IsNeighborhoodTerrainResident` per-call cost (D6).** Unfixed, this grows from ~9 to ~625 membership tests *plus* a full-map `HashSet` rebuild, every frame, during the hold. D6 removes the rebuild; the remaining 625 lookups are the honest cost. 3. **Near-ring composite warmup.** D1 does not widen the composite radius (D3), but the *reveal* now waits on `AreCompositeTexturesReady`, which is already `NearRadius`-scoped — i.e. the composite domain is unchanged and is not a new risk. Confirm rather than assume (P3). **The budget this is measured against.** The project already has an explicit acceptance edge, written into `src/AcDream.App/Streaming/StreamingWorkBudgetOptions.cs:26-30`: an earlier `MaxEntityOperations: 256` was raised to `4096` precisely because it "stretched destination publication past retail's five-second wait-notice edge." So: - **Primary budget: destination convergence.** The hold must converge; the automated ceiling already exists in the harness as `wait world-visible 30000` in `tools/connected-world-lifecycle.route.txt`. A hold that trips that timeout is a failure, not a slower pass. - **Secondary budget: steady-state frame cost must not regress.** The standing reference figures are the ordinary production profile at 519.7 FPS with CPU/GPU p50 1.869 / 1.096 ms and 652.1 / 928.3 MiB working/private (CLAUDE.md, Slice G5). The hold is a transient; the post-reveal steady state must land inside those figures. - **Tertiary: allocation.** Slice I1's 0 B/resolve standard is what D6 protects. **Measurement, not judgement.** `WorldLifecycleCheckpoint` (`src/AcDream.App/Diagnostics/WorldLifecycleAutomationController.cs:101-110`) already serialises `RuntimePortalSnapshot Reveal`, `StreamingWorkDiagnostics StreamingWork`, `ResidencySnapshot Residency`, `RenderFrameOutcome Render`, `Fps`, `FrameMilliseconds`. `StreamingWorkDiagnostics` (`src/AcDream.App/Streaming/StreamingWorkBudget.cs:166-184`) carries `PendingPublications`, `PendingRetirements`, `DestinationBacklog`, `ControlBacklog`, `UnloadBacklog`, `NearBacklog`, `FarBacklog`, `WorkerCompletionBacklog`, `DeferredCompletions`, `LifetimeFrameOverrunCount`, `MaximumFrameMilliseconds`. **Everything §8 needs is already emitted; no new telemetry is required.** **The A/B.** Run the same route twice on the same binary — once with `ACDREAM_PROBE_REVEAL_RADIUS=1` (pre-fix behaviour) and once without (D5). The delta in hold duration per stop, and the delta in `NearBacklog`/`FarBacklog` at the `world-visible` checkpoint, is the whole performance story. Report both; do not report only the post-fix number. --- ## 8. Proof obligations — must be proven, not assumed - **P1 — A Far-tier landblock satisfies `IsRenderReady`.** Traced statically (§2.3: `PublicationKind.Far` → `CommitLandblockSpatial` → `ActivateLandblockPresentation` → `OnLandblockLoaded` with an empty mesh set → `IsLandblockRenderReady` true). **Prove it with a test**, because the entire fix rests on it: if a Far landblock is not render-ready, D2's outer arm never satisfies and the reveal hangs. - **P2 — The outer ring converges within the Far window's own lifetime.** > **CORRECTED 2026-08-06 (both #280 review lenses, FAIL).** As written below, > P2 was discharged against **residency** — "cannot be evicted" — when the > gate's actual atom is `IsRenderReady`. Those are different predicates, and > the gap between them is exactly the shipped defect: a Near→Far **demote** > leaves the landblock resident and drawn while revoking its spawn-adapter > registration, so `IsRenderReady` went permanently false and the gate could > never open. Discharging a proof obligation against a predicate the code does > not use proves nothing. The restated obligation is: **no transition may > revoke `IsRenderReady` from a landblock that stays inside `FarRadius`** — > which now holds because `GpuWorldState.ReleaseLandblockMeshReferences` > re-asserts the Far registration on demote. Every remaining sentence below > (hysteresis, recenter, dungeon collapse) is correct as stated. The window unloads at `FarRadius + 2` Chebyshev (`src/AcDream.App/Streaming/StreamingRegion.cs:208-209`), so an outer-ring member cannot be evicted while it is inside `FarRadius`. Prove the gate cannot deadlock against hysteresis, recenter (`IsRecenterPending`, already gated at `LocalPlayerTeleportController.cs:505`), or dungeon collapse (`StreamingController.IsCollapsedToDungeon`) — specifically: an **outdoor** destination while the window is collapsed to a dungeon. - **P3 — Composite readiness is `NearRadius`-scoped and unchanged.** Confirm `IsCompositeWarmupCandidate` sees no Far-ring entities (Far builds carry `Array.Empty()`), so D3 is a statement about the data and not a scope cut. - **P4 — The 25% non-destination lane still drains during a hold.** Measured from the checkpoint's `PendingRetirements` / `UnloadBacklog` / `ControlBacklog` across the hold. A monotonic rise is a finding. - **P5 — Runtime and headless are untouched.** Assert the loosened invariant accepts every radius both non-graphical producers emit, and that the headless dependency guard still passes with no App reference. - **P6 — The radii are read live.** Prove that changing the quality preset mid-hold changes the gate on the next evaluation, and that the reservation reopens at the new radius on the same generation (D4). Retail anchor: `SmartBox::set_mid_radius @ 0x00453180`. - **P7 — Login uses the same widened gate.** The barrier is shared; confirm first-login readiness moves with it and does not regress the `capped_login` checkpoint. - **P8 — D6 restores 0 B/allocation** on the terrain-neighborhood path at the new radius. --- ## 9. Test plan Assert at layers that have broken historically. **No source-text pins. No test that re-encodes the constant under test** — a test asserting `RequiredRenderRadius == 12` is the same defect in a different file. 1. **Derivation, not value** (`WorldRevealReadinessBarrierTests`). Drive the barrier with a fake window reporting `(near: 3, far: 8)` and then `(near: 5, far: 15)`; assert the outdoor required radius **equals the fake's far radius** in both cases and that indoor is 0 in both. The assertion references the fake's input, never a literal. 2. **Live re-read.** Mutate the fake window between two `Evaluate` calls with no reconstruction; assert the second evaluation used the new radius (P6). 3. **Tier-aware ring** (`StreamingController` / `IsRenderNeighborhoodResident`). Three cases: (a) inner-ring member at Far tier ⇒ **not** resident; (b) outer-ring member at Far tier and render-ready ⇒ resident; (c) outer-ring member absent ⇒ not resident. Case (a) is the discriminating one — it proves the near arm did not get loosened into the far arm. 4. **P1 as a test.** A Far publication through the real pipeline, then `GpuWorldState.IsRenderReady(farLandblock) == true`. If this needs the real `LandblockSpawnAdapter`, use it; a fake here proves nothing. 5. **Off-map skip.** A destination at a map corner still converges — the out-of-bounds members are skipped, not required (retail parity, §1.4). 6. **Runtime shape invariant** (`RuntimeWorldTransitStateTests`). Assert `AcknowledgeDestinationReadiness` accepts `(indoor: false, radius: 1)`, `(false, 12)`, `(false, 25)`; rejects `(indoor: true, radius: 1)` and `(false, 0)`. This is the test that would have caught the four-site duplication (§2.2). 7. **Headless producer stays legal** (`HeadlessSessionWorldProjection` / `AcDream.Headless.Tests`): its `indoor ? 0 : 1` acknowledgement is accepted, and the existing dependency/loaded-assembly guards still pass. 8. **Login parity.** `LivePlayerModeAutoEntryContext.IsWorldReady` returns false while an outer-ring member is missing and true once it lands. 9. **Allocation** (D6/P8): a warmed loop over `IsNeighborhoodTerrainResident` at radius 12 measures 0 managed bytes, matching the Slice I1 pattern. 10. **Reservation radius follows the gate** (D4): the tuple handed to `BeginDestinationReservation` carries the derived far radius, asserted against the fake window's input, not a literal. Delete — do not adapt — the two existing assertions that pin `1` (`WorldRevealReadinessBarrierTests.cs:63,69,76,140`, `LocalPlayerTeleportControllerTests.cs:65`). Test 1 replaces them. --- ## 10. Gates - Focused tests above. - **Release build**, then the complete Release suite: `$env:ACDREAM_PAK_PATH` set, `dotnet test AcDream.slnx -c Release -m:1`. **Re-measure the baseline at the implementation HEAD; do not inherit it.** Known separately-filed flakes — **#302** (`PortalProjectionTests.ClipToRegion_FrameOwnedStore_…`), **#308** (`NakEmissionTests.LossSoak_…`), **#321** (`DatSoundCacheTests` concurrent-decode-dedup). If one appears, re-run and name which; never fold, never mask, never retry-loop. - **Automated route, twice, same binary** — `tools/run-connected-world-lifecycle-gate.ps1` over `tools/connected-world-lifecycle.route.txt`, once with `ACDREAM_PROBE_REVEAL_RADIUS=1` and once without (§7 A/B). The route already covers the cases that matter: `capped_login`, a dense outdoor island, a world-edge streaming transition, and an indoor Facility Hub cell. Its `wait world-visible 30000` is the convergence ceiling. - **Connected/visual gate: YES**, and **batched into C5c's visual matrix**, which the campaign sequences after #280. Release, `ACDREAM_RETAIL_UI=1`, two-client. ### The visual gate's positive evidence The user-facing observable — "no visibly constructing far terrain after portal space exits" — is an **absence**, and C5b's rule (g) says an absence is not a pass criterion. So the gate produces three positive artifacts per stop, all from machinery that already exists: 1. **A checkpoint JSON captured at `world-visible`** whose `StreamingWork.NearBacklog`, `.FarBacklog`, `.DestinationBacklog` and `.PendingPublications` are **zero for the destination window at the moment the viewport opened**. That is the positive form of "nothing was still building." A screenshot alone cannot say this; the backlog counts can. 2. **A hold-duration pair**: `_holdSeconds` (equivalently `Reveal.WaitCueShown` plus the checkpoint timestamps) with the probe on and off. The expected, *reportable* result is that the post-fix hold is **longer**. A post-fix hold that is not longer means the gate did not actually widen and the run proves nothing. 3. **A paired screenshot** at each stop from the two A/B runs. The pre-fix run is expected to *show* the defect; that is the run that makes the post-fix screenshot mean something. Recall specifically (the reported repro) must be in the matrix, not only `/teleloc`: the user observed this "after some recalls". Add a lifestone / recall leg to the C5c session even though the automated route uses `/teleloc`. --- ## 11. Traps - **T1 — the four-site radius.** `OutdoorNeighborhoodRadius` is not the single source of truth (§2.2). Changing it alone trips `FailInvariant("invalid-readiness-shape")` at `RuntimeWorldTransitState.cs:573` and the reveal never opens — which will look like "the fix hangs the client" and invite reverting the radius instead of fixing the invariant. - **T2 — bumping the constant without D2.** Any radius above `NearRadius` is unsatisfiable while `IsRenderNeighborhoodResident` demands `IsNearTier`. The symptom is an infinite hold with the wait cue up. The wrong conclusion available at that moment is "the world can't stream that far"; the right one is "the predicate refuses Far tier." - **T3 — a test that re-encodes the new number.** Replacing `Assert(radius == 1)` with `Assert(radius == 12)` reproduces the exact defect class this slice exists to remove. Assert against the window's reported value (§9-1). - **T4 — plumbing App radii into Runtime** to keep the strict equality at `:573`. That is the C5b mechanism-that-does-not-exist failure and it breaks the J-slice ownership boundary and the headless dependency guard. Loosen the invariant (D7). - **T5 — widening the composite radius "for consistency."** Far-tier landblocks have no entities; the composite domain is entity-scoped (D3/P3). Widening it walks 625 landblocks to warm nothing and pressures a 128 MiB physical budget for no benefit. - **T6 — reading the radii once at construction.** Radii are runtime mutable (`ReconfigureRadii:377`) and retail explicitly re-arms the blocking prefetch on a radius change (`set_mid_radius @0x00453180`). A captured radius is a latent bug that only appears when someone opens Settings mid-portal. - **T7 — `ACDREAM_STREAM_RADIUS` during measurement.** It forces `NearRadius` to its value and only *raises* `FarRadius` (`SessionPlayerComposition.cs:249-257`), and it is silently discarded by any later `ApplyQuality` (`RuntimeSettingsTargets.cs:247-253`). A gate run with it set is measuring a different window than production. Leave it unset. - **T8 — treating the longer hold as a regression.** It is the fix working, and it is retail (§1.5, §6). The failure condition is *non-convergence*, not duration. - **T9 — "the far terrain is behind the fog, so don't gate it."** It is not: `fogEnd = FarRadius * 192 * 0.95` is inside the Far extent at every preset, so far-ring terrain is visible through fog, which is exactly what the user watched assemble. Do not introduce a fog-derived radius; keep the retail shape (one square). - **T10 — declaring parity.** The outer ring accepts terrain-only publication where retail requires LandBlockInfo and building EnvCells (§1.4). A distant building can still pop. That residual gets its own register row (D8) and must not be quietly dropped from the closeout. - **T11 — scope creep into the Viewing Distance option.** §3 files it as a separate feature. Adding a user-facing setting inside #280 makes the slice unreviewable and re-opens the decoupling the slice exists to close. --- ## 12. Size and split call **One slice. Do not split.** Estimated production change, by site: | Site | Lines | |---|---| | `WorldRevealReadinessBarrier.cs` (instance radius, near/far split, snapshot shape) | ~40 | | `StreamingController.IsRenderNeighborhoodResident` (tier-aware ring) | ~20 | | `WorldRevealCoordinator.cs` (instance `RequiredRenderRadius`, reservation radius, radius-change reopen) | ~15 | | `SessionPlayerComposition.cs` (wire the live window accessor) | ~10 | | `PhysicsEngine.IsNeighborhoodTerrainResident` (D6, allocation) | ~15 | | `RuntimeWorldTransitState.cs:573` (D7, shape invariant) | ~8 | | Diagnostic owner (D5) | ~10 | | **Total** | **~120** | Plus ~10 focused tests and two register edits (D8). Splitting is worse here, not better: D1 without D2 hangs the client (T2), and D2 without D1 is dead code. D6 and D7 are both *forced* by D1 — D7 because the acknowledgement fails otherwise, D6 because the per-frame cost otherwise regresses against a standing budget. There is no bisectable intermediate state, so a split produces commits that are individually broken, which is worse for the campaign's bisect discipline than one 120-line behaviour commit. Sequence within the one commit: D7 (loosen) → D2 (predicate) → D1 (derive) → D4/D5/D6. Register edits in the same commit (D8). --- ## 13. Claims found false or stale at HEAD `9ee9c1a1` 1. **The issue's "the normal configured view extends substantially farther" names a setting that does not exist.** `grep -rniE "viewdistance|view_distance|ViewDistance|LandscapeDrawDistance|DrawDistance"` over `src/` returns nothing. The *conclusion* is right — the visible extent is ~2189 m of fog inside a 2304 m Far window against a 192 m gate — but the premise should read "the configured streaming/fog window (`QualitySettings.FarRadius`)", not "the configured view distance." 2. **The issue's implied fix is incomplete.** It frames #280 as "the gate is only radius 1." Raising that constant alone cannot work: any radius above `NearRadius` is unsatisfiable while `IsRenderNeighborhoodResident` requires `IsNearTier` (`StreamingController.cs:250`), and the change also fails the Runtime invariant at `RuntimeWorldTransitState.cs:573`. It is a radius change **and** a predicate change **and** an invariant loosening. 3. **`OutdoorNeighborhoodRadius` is not the single source of truth.** Three uncited `indoor ? 0 : 1` literals exist outside it, one of them validating (§2.2). Any doc or comment implying one owner is wrong. 4. **CLAUDE.md's `ACDREAM_STREAM_RADIUS` description is stale.** It says "tune landblock visible-window radius (default 2 = 5×5)". At HEAD it is a *legacy* override (`RuntimeOptions.cs:113-115`) whose default is `null`/unset; when set it forces `NearRadius` and only raises `FarRadius` (`SessionPlayerComposition.cs:249-257`); and it is silently discarded by any runtime `ApplyQuality` (`RuntimeSettingsTargets.cs:247-253`). The shipped defaults are the `QualityPreset.High` row, 4/12. 5. **`claude-memory/reference_two_tier_streaming.md` is stale in four ways.** (a) N₁=4 / N₂=12 are the `High` row of a four-row preset table (`QualityPreset.cs:29-36`), not standalone constants, and are runtime mutable. (b) The Far tier is not "terrain render only" — it also publishes terrain **collision** (`LandblockPhysicsPublisher.cs:390-403`, reached for `PublicationKind.Far` via `LandblockPresentationPipeline.cs:619`), which is precisely why D2's outer arm is viable. (c) `MaxCompletionsPerFrame` is no longer a per-frame completion drain; it is a whole-profile scalar over a seven-dimension typed budget (`StreamingController.cs:117-134`, `StreamingWorkBudgetOptions.cs:79-96`). (d) It documents none of `IsRenderReady`, `WorldRevealReadinessBarrier`, the destination reservation, or the 75% reserved lane — all of which are now the mechanism for "is this landblock done." 6. **The campaign plan's phrase "retail's configured destination-prefetch window" (`2026-08-02-placement-cutover.md:65`) is right in intent and slightly wrong in shape.** Retail has no separate prefetch window; it has one landscape window (`mid_radius`) that is simultaneously loaded, drawn, and blocked on, and its configured value is `Render.LandscapeDrawDistance`. 7. **AD-2's outdoor-gate wording will be wrong after this slice.** It describes the outdoor claim as requiring residency "for the required Near ring." Amend in the implementation commit (D8). 8. **The `#280` issue text implies portal-only.** The barrier is shared with login (`PlayerModeAutoEntry.cs:106-111`), so first-login reveal widens too. Intended, but it must be gated (P7) and stated at closeout. 9. **acdream's `RetailWaitCueDelay = 5 s` is not retail's trigger.** The *string* is exact; the *trigger* is retail's tunnel rotation-segment boundary (`gmSmartBoxUI::UseTime @ 0x004D6E30`, `0x004D6FC7` → `0x004D70A1`), not a fixed elapsed threshold. Out of #280's scope; recorded so it is not mistaken for a #280 regression. 10. **acdream's far plane (5000 m) differs from retail's `Render::zfar` (4000 m, byte-verified).** Independent of #280 — in both clients the landscape horizon is the landblock window, not the frustum — but it is an uncited divergence sitting in four camera classes and should be filed. --- ## 14. What #280 does NOT do - It does **not** add a user-facing Viewing Distance option. That is a separate missing feature: a dedicated six-position enum matching retail's `Render.LandscapeDrawDistance` (labels VeryLow/Low/Medium/High/VeryHigh/ Extreme, values 3/5/8/11/15/25 @ `0x007CA988`, default 8) feeding the streaming radii, replacing or reparameterising the quality preset's Near/Far pair. File it. #280's derivation keeps working when it lands. - It does **not** add a user-facing prefetch knob (§3). - It does **not** implement retail's DDD progress readout (§6). - It does **not** change the wait cue's threshold or trigger (§6). - It does **not** make the outer ring require LandBlockInfo or building EnvCells the way retail does; that residual gets a register row (D8, T10). - It does **not** touch the far plane (§13-10). - It does **not** change `blocking_for_cells`-equivalent semantics: acdream holds in the portal tunnel where retail freezes simulation behind a hidden world. That divergence is AD-2's and stays AD-2's. --- ## 15. Retail facts I could NOT establish Flagged rather than guessed: - **The index-1 choice label.** `Render_LandscapeDrawDistance_Choices[1]` is initialised from `&data_793f8c` rather than an inline literal (`:718533`), so its text is not visible in the pseudo-C. By position between `VeryLow` and `Medium` it is almost certainly `"Low"`, but I did not decode the string. The **value** (5) is byte-verified and is what matters here. - **Whether retail draws anything of the destination during a blocked mid-session teleport.** `SmartBox::Hide` is called when the portal space becomes visible (`0x004D6FB6`) and `SmartBox::Draw @ 0x00455570` returns early on `hidden`, which strongly implies the world is not drawn behind the tunnel — but I did not trace every `hidden` transition (`0x00451D30` / `0x00451D40` are its setters and I did not name their callers). Nothing in this contract depends on the answer; acdream's tunnel covers the viewport either way. - **The exact retail tunnel rotation-segment duration.** `RandDouble` at `0x004D701B` / `0x004D7049` has its arguments mangled by BN's FPU handling; the nearby immediates (`0x40768000`, `0x3FFCCCCC`, `0x3FE33333`) look like the double halves but I did not decode them. Only relevant to §13-9, which is out of scope. - **Whether `Render::zfar` is ever assigned outside `GameSky::Draw`.** Grep found only the static initialiser (4000.0, byte-verified) and the two sky calls. I did not exhaustively search for indirect writes. Relevant only to §13-10.