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>
63 KiB
#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):
Note the second guard: the radius cannot be changed while the block array is allocated. Callers must reset first (see §1.5).if (arg2 < 1 || this->land_blocks != 0) return 0; this->mid_radius = arg2; this->mid_width = arg2 * 2 + 1; return 1;LScape::LScape@0x00505370(:267007) constructs withmid_radius = 5,mid_width = 0xB. (byte-verified — file offset for0x00505390readsc706 05000000=mov dword [esi], 5,c74604 0b000000=mov dword [esi+4], 0xB.) This is the pre-preferences default only.LScape::update_block@0x005063A0(:267954) allocatesoperator 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 byviewer_b_xoff/viewer_b_yoff = ((mid_radius << 3) - origin + coord) >> 3, andblock_draw_list(allocated alongsideland_blocks, freed together at0x00504BCB) 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): whenRender::m_RenderPrefs.LandscapeDrawDistancediffers from the cachedCurrent_Render_LandscapeDrawDistance, it re-issuesSmartBox::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:
DBObj::PreFetch(qdid(did, type 1)). If the result is neitherCACHE_OBJECT_IN_MEMORYnorCACHE_OBJECT_IN_FILE,result = 0; if it isCACHE_OBJECT_LOOKING, also*waitingCount += 1.- Otherwise
DBObj::Get(...). IfGetreturns null (present in the file but not yet resident), it additionally prefetches the LandBlockInfo record(did & ~1) | 0xFFFEas type 2, setsresult = 0, and*waitingCount += 1. - Otherwise
CLandBlock::PreFetchCells(block)@0x00530240(:314195), which prefetches the LandBlockInfo type-2 record and, viaCLandBlockInfo::PreFetchCells@0x0052E7C0(:312329), walks every building and callsCBldPortal::PreFetchCells@0x0053BD00(:325061) for each, which prefetches everystab_list[i]as type 3 (the building's EnvCells). Any failure propagatesresult = 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 != 0and cells are missing, it reportsECM_DDD::SendNotice_RuntimeDDDStatus(1, remaining, total)and latchesthis->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:
- The DDD progress readout.
ECM_DDD::SendNotice_RuntimeDDDStatus(active, remaining, total)@0x00692870reachesgmPowerbarUI::RecvNotice_RuntimeDDDStatus@0x004DA5C0(:222574), which sets a text element to string idID_Powerbar_DDDModeTextwithCURRENT/TOTALinteger variables and writescurrent/totalinto a float attribute0x69on element0x10000034— a progress bar with an "N of M" cell count on the powerbar. Onactive == 0it restores normal state. - The portal-space notice.
gmSmartBoxUI::UseTime@0x004D6E30(:219400): whileteleportAnimState == TAS_TUNNEL, each time the current rotation segment expires (teleportRotationStartTime + teleportRotationDuration <= cur_time,:004D6FC7) it picks a fresh random segment and emitsECM_UI::SendNotice_DisplayStringInfo(0x1A, "In Portal Space - Please Wait...")(literal at0x004D7064,: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.
:38internal const int OutdoorNeighborhoodRadius = 1;:143-146
Note it isinternal static int RequiredRenderRadius(uint destinationCell) => IsIndoor(destinationCell) ? 0 : OutdoorNeighborhoodRadius; private static bool IsIndoor(uint cellId) => (cellId & 0xFFFFu) >= 0x0100u;static— that is the first structural obstacle to a derived radius.:107-141Evaluateshort-circuits render → composites → collision and returns aWorldRevealReadinessSnapshot(:9) whoseIsReadyisHasDestination && (IsUnhydratable || (render && composites && collision)).:84-92Preparecalls_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:
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
5000fin every camera (src/AcDream.App/Rendering/RetailChaseCamera.cs:56, and identically inChaseCamera.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-491withLandblockSize = 192f(:418):fogStart = NearRadius * 192 * FogStartMultiplier,fogEnd = FarRadius * 192 * FogEndMultiplier, defaults0.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):
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(portaldataReady) andLivePlayerModeAutoEntryContext.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-framePrepareAndEvaluate. 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).StreamingWorkBudgetdestination/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; itsIsReadyjoin 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:
- "retail emits it for the whole duration of a blocked prefetch" — the
emit site is inside
gmSmartBoxUI::UseTime'sTAS_TUNNEL*branch, in theelsearm of the rotation-segment-expiry test at0x004D6FCD, and fires unconditionally per tunnel rotation segment whether or notblocking_for_cellsis set. The cue is a property of being in the tunnel, not of being blocked. Byte-decoded at0x004D6FE6-0x004D7049:teleportRotationDuration = RandDouble(0.6, 1.8)s (0x3ffccccc/0xcccccccd= 1.8,0x3fe33333/0x33333333= 0.6),teleportRotationEndAngle = RandDouble(0, 360)(0x40768000). - "whose retry is quantised to 5 s" — the 5.0 constant at VA
0x007991B0belongs toCellManager::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 sarming 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:
- 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 isgmSmartBoxUI::UseTime @ 0x004D6E30. - acdream has no analogue of retail's DDD progress bar
(
gmPowerbarUI::RecvNotice_RuntimeDDDStatus @ 0x004DA5C0, stringID_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:
- 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.
IsNeighborhoodTerrainResidentper-call cost (D6). Unfixed, this grows from ~9 to ~625 membership tests plus a full-mapHashSetrebuild, every frame, during the hold. D6 removes the rebuild; the remaining 625 lookups are the honest cost.- Near-ring composite warmup. D1 does not widen the composite radius
(D3), but the reveal now waits on
AreCompositeTexturesReady, which is alreadyNearRadius-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 30000intools/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→OnLandblockLoadedwith an empty mesh set →IsLandblockRenderReadytrue). 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, soIsRenderReadywent 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 revokeIsRenderReadyfrom a landblock that stays insideFarRadius— which now holds becauseGpuWorldState.ReleaseLandblockMeshReferencesre-asserts the Far registration on demote. Every remaining sentence below (hysteresis, recenter, dungeon collapse) is correct as stated.The window unloads at
FarRadius + 2Chebyshev (src/AcDream.App/Streaming/StreamingRegion.cs:208-209), so an outer-ring member cannot be evicted while it is insideFarRadius. Prove the gate cannot deadlock against hysteresis, recenter (IsRecenterPending, already gated atLocalPlayerTeleportController.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. ConfirmIsCompositeWarmupCandidatesees no Far-ring entities (Far builds carryArray.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/ControlBacklogacross 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_logincheckpoint. -
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.
- 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. - Live re-read. Mutate the fake window between two
Evaluatecalls with no reconstruction; assert the second evaluation used the new radius (P6). - 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. - P1 as a test. A Far publication through the real pipeline, then
GpuWorldState.IsRenderReady(farLandblock) == true. If this needs the realLandblockSpawnAdapter, use it; a fake here proves nothing. - Off-map skip. A destination at a map corner still converges — the out-of-bounds members are skipped, not required (retail parity, §1.4).
- Runtime shape invariant (
RuntimeWorldTransitStateTests). AssertAcknowledgeDestinationReadinessaccepts(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). - Headless producer stays legal
(
HeadlessSessionWorldProjection/AcDream.Headless.Tests): itsindoor ? 0 : 1acknowledgement is accepted, and the existing dependency/loaded-assembly guards still pass. - Login parity.
LivePlayerModeAutoEntryContext.IsWorldReadyreturns false while an outer-ring member is missing and true once it lands. - Allocation (D6/P8): a warmed loop over
IsNeighborhoodTerrainResidentat radius 12 measures 0 managed bytes, matching the Slice I1 pattern. - Reservation radius follows the gate (D4): the tuple handed to
BeginDestinationReservationcarries 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_PATHset,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 (DatSoundCacheTestsconcurrent-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.ps1overtools/connected-world-lifecycle.route.txt, once withACDREAM_PROBE_REVEAL_RADIUS=1and 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. Itswait world-visible 30000is 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:
- A checkpoint JSON captured at
world-visiblewhoseStreamingWork.NearBacklog,.FarBacklog,.DestinationBacklogand.PendingPublicationsare 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. - A hold-duration pair:
_holdSeconds(equivalentlyReveal.WaitCueShownplus 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. - 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.
OutdoorNeighborhoodRadiusis not the single source of truth (§2.2). Changing it alone tripsFailInvariant("invalid-readiness-shape")atRuntimeWorldTransitState.cs:573and 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
NearRadiusis unsatisfiable whileIsRenderNeighborhoodResidentdemandsIsNearTier. 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)withAssert(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_RADIUSduring measurement. It forcesNearRadiusto its value and only raisesFarRadius(SessionPlayerComposition.cs:249-257), and it is silently discarded by any laterApplyQuality(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.95is 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
- 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"oversrc/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." - 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
NearRadiusis unsatisfiable whileIsRenderNeighborhoodResidentrequiresIsNearTier(StreamingController.cs:250), and the change also fails the Runtime invariant atRuntimeWorldTransitState.cs:573. It is a radius change and a predicate change and an invariant loosening. OutdoorNeighborhoodRadiusis not the single source of truth. Three uncitedindoor ? 0 : 1literals exist outside it, one of them validating (§2.2). Any doc or comment implying one owner is wrong.- CLAUDE.md's
ACDREAM_STREAM_RADIUSdescription 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 isnull/unset; when set it forcesNearRadiusand only raisesFarRadius(SessionPlayerComposition.cs:249-257); and it is silently discarded by any runtimeApplyQuality(RuntimeSettingsTargets.cs:247-253). The shipped defaults are theQualityPreset.Highrow, 4/12. claude-memory/reference_two_tier_streaming.mdis stale in four ways. (a) N₁=4 / N₂=12 are theHighrow 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 forPublicationKind.FarviaLandblockPresentationPipeline.cs:619), which is precisely why D2's outer arm is viable. (c)MaxCompletionsPerFrameis 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 ofIsRenderReady,WorldRevealReadinessBarrier, the destination reservation, or the 75% reserved lane — all of which are now the mechanism for "is this landblock done."- 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 isRender.LandscapeDrawDistance. - 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).
- The
#280issue 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. - acdream's
RetailWaitCueDelay = 5 sis 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. - 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_793f8crather than an inline literal (:718533), so its text is not visible in the pseudo-C. By position betweenVeryLowandMediumit 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::Hideis called when the portal space becomes visible (0x004D6FB6) andSmartBox::Draw @ 0x00455570returns early onhidden, which strongly implies the world is not drawn behind the tunnel — but I did not trace everyhiddentransition (0x00451D30/0x00451D40are 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.
RandDoubleat0x004D701B/0x004D7049has 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::zfaris ever assigned outsideGameSky::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.