fix(streaming): derive the portal reveal window from the live streaming radii (#280)

The user watched far terrain visibly assemble after portal space exits.
The reveal gate was NOT missing a hold — Slice E's hold mechanism is
correct and already in place. The hold was measuring the wrong domain:
it opened at a hardcoded 3x3 landblock neighbourhood (~192 m) while the
visible world extends to the fog end (~2,189 m at the shipped High
preset, inside a 2,304 m Far window). An 11.4:1 ratio.

Retail's equivalent ratio is 1:1 BY CONSTRUCTION. `LScape` owns one
`mid_width x mid_width` array of `CLandBlock*` (`LScape::SetMidRadius`
@0x00504C00, `LScape::update_block` @0x005063A0), `mid_radius` is
assigned directly from the user's `Render.LandscapeDrawDistance`
preference (`SmartBox::SetRegion` @0x004531F0; values
`Render_LandscapeDrawDistance_Values` @0x007CA988 = {3,5,8,11,15,25},
default 8 — both byte-verified against the PDB-paired 2013 binary), and
that same square is simultaneously the prefetched set
(`LScape::PreFetchCells` @0x00505660), the drawn set (`block_draw_list`
over the same array), and the set the simulation blocks on
(`CellManager::blocking_for_cells`). There is no retail configuration in
which the client streams farther than it gates, because there is only
one number.

So the fix derives rather than duplicates. Four coupled parts, which is
why this is one commit and not four — D1 without D2 hangs the client and
D2 without D1 is dead code:

D1 `WorldRevealReadinessBarrier` takes a live `Func<StreamingRevealWindow>`
and stops being static: outdoor requires `FarRadius`, indoor still 0
(retail's `CEnvCell::PreFetchCells` @0x0052D1E0 arm). Read per
evaluation, never captured — the radii are runtime mutable through
Settings, and retail's answer to a mid-hold radius change is to reset,
re-radius, and re-arm the blocking prefetch at the NEW value
(`SmartBox::set_mid_radius` @0x00453180). `OutdoorNeighborhoodRadius`
is deleted; there is no constant left to drift.

D2 `StreamingController.IsRenderNeighborhoodResident` becomes tiered,
because acdream's loaded landscape is: inside `NearRadius`,
`IsNearTier && IsRenderReady`; out to `FarRadius`, `IsRenderReady` only.
Without this the fix cannot work at all — nothing outside the Near ring
is ever promoted, so any radius above `NearRadius` was unsatisfiable and
would have held the reveal forever. Proof obligation P1 (a Far-tier
landblock genuinely satisfies `IsRenderReady`) is now a test driven
through the real `PublicationKind.Far` pipeline against a real
`LandblockSpawnAdapter`, not an inference.

D7 `RuntimeWorldTransitState.AcknowledgeDestinationReadiness` re-derived
`indoor ? 0 : 1` and failed `invalid-readiness-shape` on any other
value, so changing the radius alone would have looked like "the fix
hangs the client". It is now a SHAPE invariant (`indoor => 0`,
`outdoor => >= 1`). Runtime does not own the graphical host's streaming
configuration and must not learn it; plumbing App radii into Runtime to
preserve the strict equality is exactly the assert-a-mechanism-that-does-
not-exist failure C5b was built to stop. Both non-graphical producers
keep emitting their centre-ring token and stay legal, annotated in place.

D6 `PhysicsEngine.IsNeighborhoodTerrainResident` rebuilt a full-map
`HashSet` on every call, every frame of every hold. At radius 1 that was
invisible; at radius 12 (625 ring members) it violates Slice I1's
0 B/resolve standard. Now an engine-owned scratch set, cleared in place;
measured at 0 bytes over 1,000 warmed radius-12 queries.

Also: the destination reservation opens at exactly the gate's radius and
reopens on the same generation when the radius changes mid-hold (retail
has one square for both, and no concept of prioritising an inner ring
differently). Composite warmup deliberately stays `NearRadius`-scoped —
the composite domain is entity-scoped and Far builds carry no entities,
so widening it would walk the outer window to warm nothing.
`ACDREAM_PROBE_REVEAL_RADIUS` is a measurement probe in a diagnostic
owner (CLAUDE.md rule 5) so the connected route can be run A/B on one
binary; it is NOT a user-facing prefetch knob, since a low setting would
reintroduce the decoupling this slice exists to close.

Register: AD-2 amended with the derived window, the two-tier split, and
the four new retail anchors. AP-149 FILED for the residual this does not
close — the outer ring accepts terrain-only publication where retail
requires LandBlockInfo and every building EnvCell, so a distant building
can still pop in at Far-ring distances. Do not let a later closeout
claim parity.

Docs: `ACDREAM_STREAM_RADIUS`'s CLAUDE.md description was wrong on every
clause (the default is unset, not 2; it forces `NearRadius`; it is
silently discarded by any Settings save) — corrected, since that is the
file every session reads. `reference_two_tier_streaming.md` corrected in
four ways, including "Far tier = terrain only": Far also publishes
terrain COLLISION, which is precisely what makes this fix viable.
#280's issue text had the right conclusion from a wrong premise (it
names a view-distance setting acdream does not have) — corrected, and
the missing Viewing Distance option filed separately as #326, with #327
(DDD progress readout) and #328 (hardcoded 5000 f far plane vs retail's
byte-verified 4000) filed alongside.

Expect LONGER holds and the "In Portal Space - Please Wait..." cue on
recalls MORE often. That is convergence toward retail, not away from it:
retail emits the byte-identical string for the whole duration of a
blocked prefetch and polls at 5 s intervals. The failure condition is
non-convergence, not duration.

Gates: Release build 0 errors. Complete suite 11,178 passed / 4 skipped
/ 0 failed, against a re-measured 11,142 / 4 / 0 baseline at 9ee9c1a1 —
+36, reconciled exactly as 36 new tests (App +23, Runtime +10, Core +3),
zero deleted, zero newly skipped. Nine discriminating tests
sabotage-verified in both directions. The connected/visual gate is
batched into C5's matrix; its recipe, its three positive artifacts, and
its required recall leg are written into the campaign plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-05 23:54:59 +02:00
parent 9ee9c1a1a6
commit 3aab05b0cc
23 changed files with 2502 additions and 86 deletions

View file

@ -1684,14 +1684,63 @@ it. Do #297 FIRST — #298 depends on it.
entity cell. Spell buffs, recalls, arrows, and combat spell projectiles were
verified in the connected client; focused effect, projectile, and
cell-transition tests cover the race. Landed at `f24532ad`.
- **#280 — OPEN — portal reveal can expose an incompletely streamed distant
landscape.** User-observed 2026-08-03: after some recalls, the nearby
destination is playable but terrain near the far end of the view continues
visibly building after portal space exits. The current outdoor reveal gate
is explicitly only `WorldRevealReadinessBarrier.OutdoorNeighborhoodRadius =
1` (a 3x3 landblock neighborhood), while the normal configured view extends
substantially farther; this permits the world viewport to open before its
visible static destination is complete.
- **#280 — FIXED (2026-08-05), pending the batched C5c connected/visual gate —
portal reveal could expose an incompletely streamed distant landscape.**
User-observed 2026-08-03: after some recalls, the nearby destination is
playable but terrain near the far end of the view continues visibly building
after portal space exits. **Premise correction (the original text named a
setting that does not exist):** acdream has no "configured view distance" —
`grep -rniE "viewdistance|view_distance|LandscapeDrawDistance|DrawDistance"`
over `src/` returns nothing. The correct premise is the configured
*streaming/fog* window, `QualitySettings.FarRadius`: at the shipped `High`
preset the user sees terrain out to the fog end
(`FarRadius * 192 m * 0.95` ≈ 2,189 m) inside a 2,304 m Far window, while the
outdoor reveal gate opened at a hardcoded radius-1 3×3 neighbourhood
(≈192 m) — an 11.4:1 ratio where retail's is 1:1 by construction, because
retail's prefetched, loaded and drawn squares are literally the same array
(`LScape::mid_radius`; `LScape::PreFetchCells` @0x00505660). The conclusion
in the original text was right; the premise was not.
**Fix correction:** raising the constant alone could not work, twice over.
`StreamingController.IsRenderNeighborhoodResident` demanded `IsNearTier` for
every ring member, so any radius above `NearRadius` was unsatisfiable and
would have held the reveal forever; and
`RuntimeWorldTransitState.AcknowledgeDestinationReadiness` re-derived and
asserted `indoor ? 0 : 1`, so a changed radius failed
`invalid-readiness-shape` and the reveal never opened. The landed change is a
radius derivation **and** a tier-aware predicate **and** an invariant
loosening, plus the forced allocation fix in
`PhysicsEngine.IsNeighborhoodTerrainResident`. Contract:
`docs/research/2026-08-05-280-contract.md`. Residual filed as register row
AP-149 (the outer ring accepts terrain-only publication where retail requires
LandBlockInfo and every building EnvCell).
- **#326 — OPEN — acdream has no Viewing Distance option.** Retail exposes one
user-facing landscape-extent preference,
`Render.LandscapeDrawDistance` — a six-position enum
(`Render_LandscapeDrawDistance_Values` @0x007CA988 = 3/5/8/11/15/25, labels
VeryLow/…/Extreme, **default 8**, both byte-verified), registered at
`UserPreferences::RegisterPreference` @0x0054ECBE and pushed into
`SmartBox::set_mid_radius` @0x004531D0. acdream's structural analogue is the
quality preset's `NearRadius`/`FarRadius` pair, which is not separately
user-controllable. Split out of #280 deliberately (§3/§14 of that contract):
#280 derives its reveal window from whatever feeds the streaming radii, so
this feature lands by changing what feeds them and #280's derivation keeps
working untouched.
- **#327 — OPEN — acdream has no analogue of retail's DDD prefetch progress
readout.** While `CellManager::blocking_for_cells` is latched, retail reports
`ECM_DDD::SendNotice_RuntimeDDDStatus(active, remaining, total)` @0x00692870
into `gmPowerbarUI::RecvNotice_RuntimeDDDStatus` @0x004DA5C0, which drives a
powerbar progress bar with an "N of M" cell count (string id
`ID_Powerbar_DDDModeText`). acdream shows only the centered
"In Portal Space - Please Wait..." cue. #280 makes reveal holds longer and
more frequent, which makes the missing readout more noticeable.
- **#328 — OPEN — the camera far plane is a hardcoded 5000 f in four camera
classes.** `RetailChaseCamera.cs`, `ChaseCamera.cs`, `FlyCamera.cs`,
`OrbitCamera.cs` each hardcode it with no config path. Retail's
`Render::zfar` is statically initialised to **4000.0** (byte-verified at
`0x0081EC88`), and the only writers are `GameSky::Draw` @0x00507055 /
@0x005070EE, which temporarily multiply by 4 for the skybox and restore.
Independent of #280 — in both clients the landscape horizon is the landblock
window, not the frustum — but it is an uncited divergence.
- **#281 — DONE (2026-08-03) — the stabilization commits left the automated
suites red, and the world-frame contract they introduced had no coverage.**
The 2026-08-03 handoff recorded "six selected fixture failures". A measured

File diff suppressed because one or more lines are too long

View file

@ -64,6 +64,16 @@ Remaining campaign work, in order:
conversion remains a trigger-conditioned carry, not a completed item.**
3. Resolve #280 with retail's configured destination-prefetch window so the
portal viewport never reveals visibly constructing far terrain.
**DONE 2026-08-05 (implementation + suite); the connected/visual gate is
batched into C5's matrix. Shape correction: retail has NO separate prefetch
window** — it has one landscape square (`LScape::mid_radius`) that is
simultaneously the loaded, drawn and blocked-on set, and whose configured
value is `Render.LandscapeDrawDistance`. acdream now derives its reveal
window from the live streaming radii (`QualitySettings.FarRadius`) and made
the render-completeness predicate tier-aware so the outer rings can satisfy
it. Contract: [`2026-08-05-280-contract.md`](../research/2026-08-05-280-contract.md).
Residual filed as AP-149; the missing user-facing Viewing Distance option is
filed separately as #326 and is explicitly NOT part of #280.
4. Run C5's complete Release suite, lifecycle/reconnect route, latest-binary
nine-stop soak, two-client observation, and the remaining #269 slope-glide
visual check. A pass from `01f4791e` is evidence for that fix, not a
@ -597,6 +607,25 @@ same commit) → docs/handoff commit. No workarounds; no fused slices.
physics probe family strip (`REMOTE_LANDING`/`REMOTE_SLIDE`/`PARK`/
`REMOTE_TELEPORT`/`CHILD_CELL`/`LOCAL_TELEPORT`) — after, never before,
the four owed gates consume them.
**#280's connected gate rides this matrix (added 2026-08-05).** Release,
`ACDREAM_RETAIL_UI=1`, `ACDREAM_STREAM_RADIUS` **UNSET** (it forces
`NearRadius` and only raises `FarRadius`, so a run with it set measures a
different window than production). Run the route TWICE on the same binary —
once with `ACDREAM_PROBE_REVEAL_RADIUS=1` (reproduces the pre-#280 gate) and
once without — and report BOTH. The user-facing observable is an ABSENCE, so
the pass criteria are three positive artifacts per stop, all from existing
machinery: (1) a `world-visible` checkpoint JSON whose
`StreamingWork.NearBacklog` / `.FarBacklog` / `.DestinationBacklog` /
`.PendingPublications` are zero for the destination window at the moment the
viewport opened; (2) a hold-duration pair — **the post-fix hold is EXPECTED
to be LONGER**, and a hold that is not longer means the gate did not widen
and the run proves nothing; (3) a paired screenshot per stop, where the
pre-fix run is the one that shows the defect. `wait world-visible 30000` in
`tools/connected-world-lifecycle.route.txt` is the convergence ceiling — a
trip is a failure, a longer pass is not. **The reported repro was a RECALL,
not `/teleloc`: the matrix needs a lifestone/recall leg**, and it must
include a first-login stop, because login shares the same barrier and its
gate widened too.
After C5: AP-22 (authored collision shapes), then AD-10 (remote
contact-plane projection), then the campaign's final matrix and ledger

File diff suppressed because it is too large Load diff