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>
1248 lines
63 KiB
Markdown
1248 lines
63 KiB
Markdown
# #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 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<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.
|