acdream/docs/research/2026-08-05-280-contract.md
Erik 73cdb95c7b fix(streaming): make a demoted landblock render-ready like a published one (#280 D-1)
Both #280 review lenses returned FAIL on the same defect, and both were
right. IsRenderNeighborhoodResident's widened outer arm requires
IsRenderReady out to FarRadius, justified by "a Far-tier landblock
registers with an empty mesh set and is therefore render-ready." That held
only for a landblock that ARRIVED as Far. The second, equally first-class
way to be Far tier is a Near->Far DEMOTE:

  DemoteLandblock -> EnqueueNearLayerRetirement
    -> LandblockRetirementStage.MeshReferences
    -> GpuWorldState.ReleaseLandblockMeshReferences
    -> LandblockSpawnAdapter.OnLandblockUnloaded  => WantsLoaded = false

while DetachNearLayer deliberately keeps the landblock loaded, terrain-mesh
resident, terrain-collision resident and DRAWN. Nothing re-publishes an
already-loaded landblock, so the demoted member satisfied NEITHER arm of
the gate, permanently: wormhole tunnel plus centered "In Portal Space -
Please Wait..." forever, no recovery short of relog.

Reachable by ordinary play. Two consecutive recalls to the same landblock
with walking in between makes ChangesStreamingCenter false, so there is no
origin recenter and the region recentres through the ordinary demote diff.
Also reachable via a mid-hold quality-preset drop -- ironically the exact
scenario ReconcileDestinationReservationRadius was added to support. The
pre-#280 radius-1 gate never touched that band, because nothing inside the
Near ring can demote.

FIX SHAPE. Make the two routes genuinely equivalent rather than teaching
the predicate to tolerate the difference. ReleaseLandblockMeshReferences
becomes "reconcile the registration to the post-retirement tier": after the
release converges, if the landblock is still loaded AND still Far tier,
re-assert the empty registration -- the identical OnLandblockLoaded(lb,
empty) a PublicationKind.Far activation makes. It is empty by construction:
DetachNearLayer retains only live server projections, which the adapter's
atlas-tier filter skips. A full retirement is unaffected (DetachLandblock
clears both _loaded and _tierByLandblock), and a throwing release still
retries because the re-assert is only reached after the adapter converged.

The alternative -- "|| (IsFarTier && IsLoaded)" at the gate -- was
rejected: it fixes one caller while leaving IsRenderReady meaning two
different things, which is precisely how this defect arose. After this
change the predicate reads "drawable at its current tier" for every caller,
with no knowledge of how the landblock got there.

WHY THE TESTS MISSED IT, fixed here too:

- Proof obligation P2 was discharged against RESIDENCY (the FarRadius+2
  eviction threshold) rather than against IsRenderReady, the gate's actual
  atom. The contract now carries the correction and the restated
  obligation: no transition may REVOKE IsRenderReady from a landblock that
  stays inside FarRadius.
- WorldRevealDerivedWindowIntegrationTests advertised itself as end-to-end
  against the real GpuWorldState but constructed it with no spawn adapter,
  so its IsRenderReady degenerated to IsLoaded via the "?? true". The
  single most load-bearing predicate in the change was stubbed out by a
  null in the test named after it -- the same shape as C5b's D3 and #276's
  three settler tests. Every fixture in that file now owns a real
  LandblockSpawnAdapter.
- The P1 test's comment described its subject as "a Near-shaped completion
  the streaming window has since DEMOTED to Far". It is not; it is a fresh
  PublishAsFar, the case that does hold. Corrected, since a future reader
  would have taken it as demote coverage.

Four new regression tests, all driving the real GpuWorldState +
LandblockSpawnAdapter + LandblockPresentationPipeline through an actual
demote, and all sabotage-verified in both directions (fail with the
production change reverted, pass with it):

  NearToFarDemote_LeavesTheLandblockRenderReadyThroughTheRealPipeline
  NearToFarDemote_LeavesTheLandblockRenderReadyUnderBudgetedRetirement
  TieredWindow_StaysResidentAfterAnOuterRingDemote
  OutdoorReveal_SurvivesAnOuterRingDemoteDuringTheHold

The budgeted variant exists because production composes
LandblockRetirementCoordinator.CreateBudgeted, whose MeshReferences stage
is a separate call site from the legacy pipeline's.

SECONDARY, same commit:

- R-1: ACDREAM_PROBE_REVEAL_RADIUS=0 was parser-accepted and
  Runtime-rejected -- it yields far = 0 for an outdoor destination, which
  fails invalid-readiness-shape on every acknowledgement, hanging the very
  A/B route the probe exists to measure. Parser floor raised to 1, with a
  7-case table test.
- R-2: the composite-warmup TRIGGER had silently moved onto the far
  window's critical path. Pre-#280 the gate and the composite domain were
  the same radius-1 square; #280 widened the gate without widening the
  domain, so every composite upload serialised behind the last outer-ring
  landblock for no readiness benefit. Warmup now starts once the NEAR
  sub-window is published -- trigger scope == domain scope, as before. The
  reveal gate is untouched: Evaluate still requires the full window AND
  composite readiness.
- AP-150 filed: acdream's RetailWaitCueDelay = 5 s arming is NOT retail's
  trigger, and #280's commit message got this wrong on both clauses. Retail
  emits the notice unconditionally per tunnel rotation segment, in the else
  arm of the segment-expiry test at 0x004D6FCD; segment duration is
  RandDouble(0.6, 1.8) s, byte-decoded at 0x004D6FE6. The 5.0 constant at
  VA 0x007991B0 is CellManager::CheckPrefetchStatus's prefetch RETRY
  cadence and has nothing to do with the cue. acdream's own 0.6/1.8 segment
  constants already match retail exactly; only the arming is wrong.
  Adopting retail's unconditional emit is filed as #329 rather than folded
  in here -- it is a user-visible presentation change and wants the user's
  eyes.
- AP-151 filed: the gate is materially STRICTER than retail on the
  mesh-build/GPU-upload axis. Retail's LScape::PreFetchCells blocks on DAT
  RESIDENCY only -- no geometry construction, no upload; that work is lazy
  at draw. acdream requires a DAT read, terrain mesh build, render-thread
  upload, spatial commit, collision admission and spawn-adapter activation
  per member of a 625-member window, metered at MaxCompletionsPerFrame.
  Nothing bounds the hold. This is the OPPOSITE asymmetry from AP-149; both
  are live at once, on different axes.
- AD-2's amendment stated the false Far-tier readiness assumption verbatim;
  corrected, along with the same error in
  claude-memory/reference_two_tier_streaming.md, which now carries an
  explicit DO-NOT-RETRY on the special-case-the-predicate shape.
- AP-115 scope-noted (it covers the cue's presentation, not its arming).
- #326's SmartBox::set_mid_radius citation corrected: the entry is
  0x00453180; 0x004531D0 is the mid-function re-arm branch.

Blast radius: GpuWorldState, LandblockSpawnAdapter,
WorldRevealReadinessBarrier and StreamingDiagnostics are all App-internal;
AcDream.Headless and AcDream.Runtime reference none of them outside
comments. Headless tests run green as part of the gate below, per C5b's
lesson about surveys that skip the no-window host.

Gates: Release build 0 errors, 18 pre-existing xUnit analyzer warnings.
Complete suite "dotnet test AcDream.slnx -c Release -m:1" with
ACDREAM_PAK_PATH set: 11,192 passed / 4 skipped / 0 failed, from a clean
rebuild (a prior session's deleted probe file had been compiled into a
stale test DLL). Baseline at fafc0b65 was 11,179 / 4 / 0; the +13 delta
reconciles exactly to this commit's additions -- 3 readiness tests, 1
integration test, 7 parser table cases, 2 warmup-trigger tests. None of the
known flakes #302/#308/#321 surfaced, and none is conflated with the
finding above.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 07:27:28 +02:00

1248 lines
63 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# #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<uint>` 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<StreamingRevealWindow>` (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<uint>` 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 3060 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<WorldEntity>()`), 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.