Commit graph

143 commits

Author SHA1 Message Date
Erik
dfc841b779 test: stabilize load-sensitive release contracts 2026-08-18 11:50:23 +02:00
Erik
a1d15a82dd perf #418: widen the destination-lane budget only while the reveal hold is active
While a destination reservation hides the world behind the authored
tunnel, the streaming frame meter now runs a hold-widened profile
(StreamingWorkBudget.WidenForDestinationHold): the time ceiling rises
from the authored 2 ms to an absolute 8 ms default
(ACDREAM_STREAM_WORK_HOLD_DEST_MS is a measurement-only override), every
count/byte dimension scales by the same factor so elapsed time stays the
authoritative guard (the measured binder is Time at both ceilings), and
the reserve fraction is re-derived (0.75 -> 0.9375) so the
NON-destination lane's absolute per-frame caps are unchanged. The
widening keys off the existing BeginDestinationReservation/
EndDestinationReservation bracket only, is derived per-Tick from the
CURRENT budget (mid-hold quality swaps compose), and a frame with no
reservation uses the authored budget verbatim (test-pinned). Portal
holds ride the same bracket as login holds by construction - intended,
and pinned by a kind-parity test through the real coordinator plus a
live @telepoi portal hold (kind=portal gate-ready 3589 ms).

Why: issue #418's next-hypothesis (1). Measured result: the ~5 s
publication drip collapsed to ~2 s (loaded 625/625 at ~3.0 s, tunnel at
64-66 fps), the portal-hold gate-ready fell to ~3.6 s - and login
gate-ready/total stayed at 8.4-8.8 s / 12.6-12.7 s, exposing the real
remaining pacer: the login-cold render-thread upload/registration
barrier behind GpuWorldState.IsRenderReady, which ran concurrently under
the old drip. Full attribution appended to docs/ISSUES.md #418; no
divergence-register row (the streamed result and reveal gate are
byte-identical; only the scheduling rate during a hidden hold changed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 20:55:43 +02:00
Erik
11106c70e7 perf #418: publish landblocks under the meter, not one per streaming tick
Phase-1 measurement (new [publish-timing]/[stream-tick] probe surfaces,
ACDREAM_PROBE_REVEAL_TIMING=1) refuted the ~31 ms-per-admission
hypothesis: the hold runs at ~64 fps with the streaming tick at ~32 Hz,
the whole 625-block window costs only ~500 ms of publication CPU (far
blocks ~0.17 ms, near 2-43 ms), and steady state showed ZERO meter
yields with ~0.22 ms of the 2 ms budget used - yet exactly one block
published per tick against a ~400-deep completion queue.

The real limiter: Runtime's collision-generation activation is a
deliberate two-poll transaction (the first
TryAcquireCollisionPrefixMutationPermission poll parks residents and
refuses by design), and LandblockPresentationPipeline.Advance's metered
arm returned Completed=false on ANY nonterminal commit, which
DrainAndApply treats as end-of-frame. One landblock per 32 Hz tick =
the flat 32/s, with the authored budget ~90% idle.

Fix: the metered arm now uses the same Runtime-owned gate the unmetered
arm and the synchronous CompletePublication API always used
(CanContinueMutationSynchronously). The second poll runs in the same
frame under the same meter, so the unchanged 2 ms elapsed-time ceiling
is now genuinely the authoritative per-frame bound; with any real debt
(live residents parked mid-game, pending withdrawals, dispatch backlog)
publication defers to the next frame exactly as before. No budget
values change, no reveal-gate/readiness change, and the streamed result
is byte-identical - only the frame scheduling of identical operations.

Measured A/B (this binary, two runs): totalMs 12689 / 12734 vs baseline
26728/27395/27503; loaded slope 32/s -> bursts of 100-360/s, 625/625 in
~6-7 s vs ~23 s. The remaining ~12.7 s floor is fully attributed in
docs/ISSUES.md: ~8 s of real budgeted readiness work plus retail's
authored tunnel exit (TunnelContinue 2-5 s + two 1 s fades, golden
constants), so the <12 s acceptance needs a lead decision on the
hold-time budget, not another hidden limiter.

New regression pin:
MeteredLoaded_NonterminalCommitWithoutDebt_CompletesInOneMeteredAdvance.
Gates: Release build 0 errors; App tests 5576/3 skips/0 failed;
Runtime tests 1756/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 20:11:35 +02:00
Erik
39967e78bd perf #418: parallelize landblock builds across a striped worker pool
Login publishes the 25x25 window at a flat 32 blocks/s (~27 s in the
tunnel). The reveal-timing probe A/B (695a27b4) showed the consumer
budget env ceilings change nothing, which was read as producer-limited:
one "acdream.streaming.worker" thread, ~31 ms/block. This replaces the
single worker with min(ProcessorCount-2, 8) workers, floor 1.

Design: striped/affinity dispatch. Each worker owns one unbounded lane
channel plus its own high/low priority queues; jobs route to
lane = ((id >> 16) * 2654435761) % N (the low word of a landblock id is
constant, so the id is mixed before reduction). Striping was chosen
over a shared queue + in-flight conflict tracker because it preserves
the per-landblock contract structurally rather than by bookkeeping:
every job for one id lives on one lane, so per-id enqueue order IS
execution and completion-arrival order, and the same-landblock
supersede rules (PromoteToNear removes queued LoadFar/Unload) keep
seeing every queued job for that id. Contract, point by point:

- Per-landblock ordering: same id -> same lane -> serial FIFO.
- ClearLoads: broadcast to every lane inside the same _inboxGate lock
  that serializes enqueues, so any load enqueued before
  ClearPendingLoads() returns sits ahead of its lane's ClearLoads copy
  in that lane's FIFO and is dropped at read time, exactly like the
  single-thread path. Already-dequeued builds still complete (now up
  to one per worker instead of one total); StreamingController's
  SweepCollapsed already unloads those uniformly.
- Priority: per-lane high/low split unchanged. Cross-lane, priority is
  not globally ordered (a lane cannot run another lane's job), which
  the contract permits; near-tier jobs hash-spread across lanes and
  are preferred within each.
- Outbox: SingleWriter flipped to false; nothing assumed single-writer
  (PublishResult already used TryWrite + an Interlocked backlog, and
  the consumer's peek->read head-stability holds because only the
  single reader ever moves the head). Cross-landblock arrival order
  was verified arbitrary-tolerant before relying on it:
  StreamingController.AdmitCompletions classifies each result
  independently into per-priority FIFOs (generation staleness +
  per-landblock retirement blocking); per-landblock arrival order is
  preserved by striping.
- Crash surface: per-worker. The first real crash publishes
  WorkerCrashed (prefixed "worker N:" in pools > 1), sets
  _workerFailure, completes every lane, and cancels the pool (a crash
  still ends all processing, as before); siblings that merely observe
  the closed lanes (ChannelClosedException) exit quietly instead of
  reporting spurious crashes; the outbox completes only when the LAST
  worker exits so no in-flight completions are dropped.
- Disposal: joins every worker under the same _disposeGate; Start
  stays idempotent and dispose-serialized.

Thread-safety audit of the production build closures
(SessionPlayerComposition), per shared object:

- DatCollection (every read in LandblockBuildFactory.BuildLocked:
  LandblockLoader.Load, SceneryGenerator.Generate, SetupMesh.Flatten,
  CellMesh.Build, GfxObjBounds.Get, GfxObjDegradeResolver): NOT
  thread-safe; already serialized under the shared _datLock, which
  BuildLocked holds for the whole read transaction. Unchanged; the
  probe run measured hold 0-13 ms / wait <= 12 ms during the login
  window, so the lock is not the new bottleneck and the build was NOT
  serialized beyond it.
- PakPreparedAssetSource / PakReader (BuildPreparedCollisionClosure,
  outside the lock): immutable TOC array + read-only
  MemoryMappedViewAccessor random-access reads + ConcurrentDictionary
  verdict caches - safe for N concurrent readers (Slice I3 design;
  the headless SharedPreparedCollisionCache wrapper is fully
  lock-protected).
- LandblockMesh.Build (outside the lock): pure math over the dat
  record + the composition-time height table + the immutable
  TerrainBlendingContext record; the shared SurfaceCache is a
  ConcurrentDictionary and BuildSurface is deterministic, so its
  lookup-or-build race is last-write-wins-benign (the code already
  documented exactly this).
- PhysicsDiagnostics probe statics: read-only bools + thread-safe
  Console writes.

MEASURED OUTCOME (gate 4): the timing acceptance did NOT pass, and per
the task contract that is reported, not tuned around. With 8 workers
on this 16-core machine all 625 builds complete in ~203 ms
(ACDREAM_PROBE_TELEPORT BUILD lines t=3475390..3475593) - the producer
is off the critical path - but loaded= still advances at exactly
+32/1000 ms and SUMMARY totalMs measured 27395 and 27503 across two
runs (baseline 26728). The 32/s pacer is in the consumer
admission/publication path and is not governed by the
StreamingWorkBudgetOptions env ceilings. #418 stays IN-PROGRESS on the
consumer side; see docs/ISSUES.md for the evidence chain.

Tests: per-landblock ordering under 4-worker contention, cross-lane
ClearLoads drop, per-lane near-before-far preference, pool-of-1 serial
equivalence, disposal joining every worker, lane-spread guard, and
worker-count validation (LandblockStreamerPoolTests). Two existing
tests asserted a GLOBAL cross-landblock execution order - a serial
implementation detail, not the contract - and now pin workerCount: 1
with justification comments (LoadNear_OvertakesQueuedFarLoads,
TwoQueuedLoads_RetainTheirDistinctOriginAndGeneration).

Gates: Release build 0 errors; App suite 5575 passed / 3 skipped
(5568 + 7 new); Runtime suite 1756/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 19:34:20 +02:00
Erik
695a27b48a probe: ACDREAM_PROBE_REVEAL_TIMING — wall-clock attribution of the login/portal hold
[reveal-timing] lines from the reveal coordinator: per-dimension
first-ready edges (render neighborhood, composite textures, collision,
gate, materialization), 1 Hz progress with the resident-landblock count
(new GpuWorldState.LoadedLandblockCount), and one SUMMARY line at the
viewport reveal. Measurement-first groundwork for the login-load speedup:
the readiness barrier observes its dimensions serially, so the edges give
each dimension's observed tail while the progress lines expose the
pacing shape (a budget-paced linear drip reads directly off the counts).
Probe-gated in StreamingDiagnostics per Code Structure Rules §5; no
behavior change, one branch per Evaluate poll when unset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 19:04:15 +02:00
Erik
d233f81dce feat(session): the in-world logoff — LogOut animation, reverse wormhole, live return to character select
Retires AD-74 (Exit to Character Selection 'behaves as Exit Game') and
files AD-110 (the composed handoff edge) — register rows in this commit.

Retail derivation (named decomp):
- gmGamePlayUI::UseTime @0x004EA3A0: confirmed Yes drains into
  CPlayerSystem::LogOffCharacter(0) when grounded (transient_state &
  CONTACT); the grounded three-way branch now also covers the
  indicator-bar end-session control (it was Options-only).
- CPlayerSystem::LogOffCharacter @0x00563520: SaveToServer FIRST (the
  existing pre-logoff flush hook), then RequestLogOff @0x00562DD0:
  'Logging off...' chat (type 0), 0xF653 via Proto_UI::LogOffCharacter
  @0x00546A20, logOffRequestTime = now + 3.0 (+20.0 when
  IsPlayerKiller @0x0058C910 — PWD bits 0x20|0x2000000), and
  CommandInterpreter::HandleLogOff @0x006B3330 -> Disable.
- The log-off ANIMATION is server-driven: ACE broadcasts
  MotionCommand.LogOut (0x1000011E, Player.cs:596 SendMotionAsCommands)
  and it plays on the local player through the existing inbound
  unpack_movement funnel during the 3 s hold — retail plays nothing
  locally; Disable() is the whole client-side effect.
- gmSmartBoxUI::UseTime @0x004D6E64: hold elapsed ->
  BeginTeleportAnimation(TAS_WORLD_FADE_OUT) @0x004D6E83 (enter cue
  @0x004D638E, unconditional) -> TunnelFadeIn -> Tunnel. The tunnel
  plays the SAME forward 40 fps animation; nothing renders backwards,
  and NO exit cue ever fires on logout (the char-select swap preempts
  the TunnelContinue/FadeOut tail).
- Inbound 0xF653 echo (dispatch case 3 @0x0055C963) ->
  ExecuteLogOff @0x0055D780: world teardown with the LOGON CONNECTION
  KEPT (ExitWorldDisconnect @0x00541E00 removes every connection
  except logonRecID_ — one connection against ACE) and
  Proto_UI::SetEventCounter(0) @0x00541E79; the fresh CharacterList in
  the same batch re-shows character management (gmGamePlayUI::Update
  @0x004E9CD0 -> QueueUIMode(0x1000000a)). ACE mirrors it:
  SendFinalLogOffMessages (Session.cs:249) sends 0xF653 + CharacterList
  + ServerName >=6 s after the request and leaves the session
  AuthConnected — a second EnterWorld needs no re-handshake.

Implementation:
- RuntimeWorldTransitState: the canonical logout lifecycle
  (Requested/PresentationActive/Confirmed, retail 3 s/+20 s holds,
  cancel/reset/ownership convergence).
- WorldSession: RequestCharacterLogOff (non-blocking 0xF653),
  IsCharacterLogOffConfirmed, ReturnToCharacterSelect (InWorld ->
  InCharacterSelect + game-action sequence reset; transport untouched).
- LiveSessionController: BeginCharacterLogOff (flush-first request) and
  CompleteCharacterLogOff — the return-to-selection transaction
  (ReconnectCore minus the transport swap: retire the world
  generation's routes, host reset, state flip, fresh generation
  re-bind, roster re-applied from the pushed CharacterList; failures
  degrade to the full StopCore teardown).
- RuntimeLocalPlayerMovementState.DisableCommandInterpreter +
  DispatcherMovementInputSource gate: retail's Disable() — held keys
  produce no movement while the server LogOut motion plays; cleared by
  the generation reset.
- LocalPlayerTeleportController: the logout pump as the third arm of
  the one wormhole machine (request/hold/wormhole/confirmed handoff;
  teleport starts refused during logout; the handoff runs the session
  transaction whose world reset retires the tunnel as the fresh
  selection state re-shows the character screen).
- UI: both end-session surfaces share the retail three-way grounded
  gate and now run the REAL flow; Options' Exit Game keeps the app
  exit (window close -> the existing graceful-shutdown logoff).

Tests: +5 transit lifecycle, +4 session transaction, +7 logout pump.
Runtime 1756/0 (baseline 1747), App live-DAT 5523/3 (baseline 5512/3
+ 11 this round), Core.Net 1004/0, full solution green (0 failures).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 14:02:40 +02:00
Erik
2bc81480d4 feat(ui): AD-109 — arm the login wormhole at the char-select Enter click
USER-DIRECTED deviation from retail (register row AD-109, same commit):
retail presents the empty pre-player gameplay screen — black behind the
retained UI — from the Enter click (CPlayerSystem::LogOnCharacter
@0x0055F890 -> CM_Login::SendNotice_BeginEnterWorld @0x006AD810, UI mode
0x10000008) until CreatePlayer raises SmartBox::teleport_in_progress
@0x00451C20 and gmSmartBoxUI::UseTime @0x004D6EAB begins TAS_TUNNEL. The
user prefers the tunnel to cover that whole wait.

- ILocalPlayerTeleportNetworkSink.ArmLoginTunnel: begins the login
  wormhole presentation at the Enter click, consuming the sequencer's
  begin-edge events SYNCHRONOUSLY (the Enter command blocks the update
  thread for the whole ServerReady round trip, so a deferred first tick
  would leave exactly the black window this deviation removes). The
  enter cue plays at the click: retail's own rule is cue-at-animation-
  begin (Sound_UI_EnterPortal @0x004D638E, unconditional inside
  BeginTeleportAnimation), and the animation begin moved to the click.
- Armed pre-reveal pump: tunnel animates across the round trip
  (worldReady pinned false, sequencer holds in Tunnel); the hold clock
  accumulates from the click.
- Adoption: the Runtime login reveal ADOPTS the running presentation
  (no re-Begin, no second cue); rejected EnterWorld (lifecycle back to
  AwaitingSelection) disarms and retires the tunnel.
- Wired at the ONE host edge every entry route shares:
  ILiveSessionLifecycleHost.ApplySelectedCharacter (direct connect,
  roster Enter, enter-after-create) via
  LiveSessionSelectionBindings.ArmLoginTunnel (default no-op keeps
  headless and every existing construction site unchanged).
- ILocalPlayerLoginLifecycleSource: typed seam (not a stored delegate —
  the frame-phase owner delegate-field guard) projecting the Runtime
  character-selection lifecycle for the disarm edge.
- Frame contract update: [login-frames] over a login is tunnel -> world
  from the click — no void, and no black between click and world.

Tests: 4 new armed-tunnel tests (arm/adopt/disarm/frame-shape); App
suite live-DAT 5516 passed / 3 skipped (baseline 5512/3 + 4 new).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 13:42:30 +02:00
Erik
fdc4fd496d fix(ui): gate — no void frames around the login wormhole; vitals icons centered
The login tunnel now covers from the first world-facing frame (the
sky-void backdrop can never present pre-tunnel) and holds through an
atomic tunnel-to-world swap at reveal completion — the void is
structurally unreachable on both edges, pinned by frame-sequence tests
across WorldSceneRenderer/WorldRevealCoordinator/LocalPlayerTeleport-
Controller/RuntimeWorldTransitState. Vitals detail icons draw at their
authored centered offsets in both stacked and side-by-side layouts.
Implemented and live-probed by the fix agent; finalized by the lead
after the agent parked post-verification (gates re-run green:
App 5512/3, Runtime 1747/0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 12:30:51 +02:00
Erik
5ca1d47d7a feat(world): the login wormhole — every world entry runs retail's portal-space presentation with sound (TS-28 narrowed)
Retail runs the SAME TAS_TUNNEL wormhole at initial login as at an F751
teleport, with no F751 involved: SmartBox::teleport_in_progress
@0x00451C20 returns 1 the moment the login player exists with
position_update_complete == 0, gmSmartBoxUI::UseTime @0x004D6EAB
edge-detects it into BeginTeleportAnimation(TAS_TUNNEL) @0x004D6EC9
(playing Sound_UI_EnterPortal @0x004D638E), SmartBox::UseTime
@0x00455483 ends the hold once destination cells stop blocking,
Sound_UI_ExitPortal plays at the viewport swap @0x004D7405, and
LoginComplete goes out at the WorldFadeIn end @0x004D745D ->
CPlayerSystem::SendLoginCompleteNotification @0x00562E90 (ACE's own
GameActionLoginComplete comment names this contract: 'called when the
client player exits portal space. It includes initial login'). acdream
skipped all of it at login — every entry route (direct auto-select,
character-select Enter, enter-after-create) dropped onto the sky-only
'waiting for login' backdrop until the world reveal completed.

The fix engages the EXISTING F751 presentation machinery on Runtime's
login reveal — no duplicated presentation code, no timers:

- LocalPlayerTeleportController gains a login arm keyed off the
  Runtime-owned login reveal generation (RuntimeWorldTransitState
  .BeginLoginReveal, begun on the first accepted local-player position
  on every entry route). It drives the same TeleportAnimSequencer/
  PortalTunnelPresentation lifecycle and the same enter/exit cues; the
  Place edge is a no-op at login (the first-entry conductor already
  committed the canonical placement — retail's analogue only flips
  position_update_complete), and FireLoginComplete now performs
  EnterWorld + the single LoginComplete send + reveal Complete, exactly
  like the F751 pump. worldReady is latched on BOTH canonical first
  placement (OnLocalPlayerFirstEntryCompleted, the repointed
  GraphicalSessionEventRoute completion callback that used to send
  LoginComplete immediately) AND destination reveal readiness.
  ActiveDestinationCell now also reports the login destination so the
  render frame's reveal-preparation arm keeps running after portal-space
  entry flips ChaseModeEverEntered.
- PlayerModeController.TryEnterPortalSpaceForLogin performs the
  player-mode presentation attach (the same BuildControllerAndCamera the
  post-reveal auto-entry used to run) before flipping into portal space
  — at login no player-mode entry has happened yet. TryEnterPortalSpace
  itself now refuses (retryable) on a constructed-but-unpublished
  Runtime controller via the documented CanExecuteLiveMovement skip
  predicate instead of faulting — the first connected run crashed on
  exactly that pre-publication State write.
- HouseQuery stays at first-entry completion (retail: tail-called from
  CPlayerSystem::InitializePlayer @0x00563570, an object-arrival edge,
  not a tunnel edge).
- An F751 arriving mid-login-tunnel withdraws the login claim and hands
  the presentation to the portal pump, which owns the single
  LoginComplete — matching retail's one teleportInProgress flag.

TS-28 narrowed: the graphical host now runs the full login wormhole;
the residual is headless-only (no presentation; placement-edge send).

Live gates (testaccount2/+Horan vs local ACE, Release): the
character-select Enter route and the --session-config direct auto-select
route both play the wormhole with Sound_UI_EnterPortal at animation
begin, hold with retail's 'In Portal Space - Please Wait...' notice
until readiness, fade out with the view-plane warp, send LoginComplete
at the WorldFadeIn end, and materialize in Holtburg; ACE-confirmed
graceful logout. Tests: App 5493/3 skips (baseline 5490 + 3 new login
tests), Runtime 1744/0, full solution green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 10:51:33 +02:00
Erik
2914e43aa9 fix(audio): portal cues fire on the sequencer's sound events, not the tunnel visuals
Slice A4 hung UI_EnterPortal on TeleportAnimEvent.EnterTunnel — the first
tunnel-family frame — so the cue landed a whole TunnelFadeIn after retail
plays it. Retail's site is gmSmartBoxUI::BeginTeleportAnimation
@0x004D638E, i.e. Begin(), which the sequencer already marks as
TeleportAnimEvent.PlayEnterSound.

That event has existed since the R6 portal-space work, complete with a
'Begin(): sound_ui_enter_portal' comment, and no consumer has ever
handled it — the switch in LocalPlayerTeleportController had cases for
Place, EnterTunnel, PlayExitSound and FireLoginComplete only, so the
sequencer emitted PlayEnterSound into nothing. A4 filled the gap in the
wrong place rather than filling it.

Both cues now go through named presentation methods driven by the
matching events: PlayEnterCue on PlayEnterSound, PlayExitCue on
PlayExitSound (the TunnelFadeOut -> WorldFadeIn edge, @0x004D7405, the
same tick the world viewport is revealed). EnterTunnel/ExitTunnel are
visuals again. The exit cue was already firing at the right moment, since
ExitTunnel was called from inside the PlayExitSound case — correct by
accident, explicit now.

Found by the user asking when the recall cues play.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 08:22:48 +02:00
Erik
6eaa490bb3 feat(audio): Campaign A slice A4 — the interface sound bus
Retail's UI sound bank was absent, so three families of cue were silent:
the portal enter/exit stingers, the AdminEnvirons dungeon atmosphere
(chanting, drums, whispers, thunder — what players remember as dungeon
'music'), and every other interface slot.

The bank's DID is not a literal anywhere in retail: GetUISoundTable
@0x00563FB0 asks GetByEnum for enum slot 7, and DBCache::GetDIDFromEnum
@0x00413940 resolves it through two EnumIDMap hops off the portal dat
header's master map. UiSoundTableResolver walks that chain the way
RetailCursorResolver already walks it for cursors. Against the shipped
dats it resolves to 0x2000004B, and that table holds exactly the 32 UI_*
slots (UI_EnterPortal 0x6A .. UI_Thunder6 0x8A) — content that confirms
the walk independently of the decode. UiSoundTableResolutionTests pins
the walk, the DID, and the content, and skips when dats are absent.

Two corrections to the research along the way. The lane-5 note recorded
GetByEnum's arguments transposed: the 0x22 it called a fileType is the
CACHE type (CLOCache(cache, CSoundTable::Allocator, 0x22)) and the real
second-hop key is 0x10000003; walking it the other way finds nothing. And
its claim that the interface volume pref applies is wrong — GetAttenuation
with ambient=0 multiplies by the EFFECT knob, so retail's
interface_sound_volume stays the dead knob lane 1 byte-decoded it to be.

EnvironSoundCueMap is an explicit 21-case table read straight out of
Handle_Admin__Environs @0x0055DE20, not arithmetic: codes 0x65..0x72 sit
0x11 below their SoundType, but 0x73/0x74 have no case, so 0x75 lands on
UI_Squeal (0x84) where an offset gives 0x86, and the switch ends at 0x7B
with no 0x7C case. Verified case-by-case against the decomp rather than
from the lane note, whose tail table was ambiguous.

Cues are attached where retail plays them: the teleport-animation
boundary for the portal pair, and the AdminEnvirons handler for the
stingers. PlaySoundFromCenter's pan-0 / distance-0 shape is what
PlayUiWave already implements after A2.

Retires TS-54. Narrows AP-115 to its notice-presentation residual.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 22:22:09 +02:00
Erik
9671af0273 fix(physics): S2 — static publication emits authored Spheres as Spheres (AP-155 narrowed)
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Both static sites (LandblockPhysicsPublisher, the headless-only
LandblockPhysicsContentBuilder) emitted an authored Setup Sphere as a
base-anchored Cylinder of radius r and height 2r. The live path emits a
Sphere for the same data, so the same object collided differently by
arrival route, and the narrow phase met a flat cap where retail meets a
curved surface. Both sites now mirror ShadowShapeBuilder.FromSetup's
Sphere block exactly.

Combined Opus review: PASS. Its numeric verification of the dispatch
test's geometry (head-sphere clearance 0.201 m for the true sphere; the
cylinder counterfactual inside by 0.10 m XY with the Z band overlapping)
is what makes the discrimination claim more than a sabotage anecdote,
and its F8 finding is applied: the test now carries a POSITIVE control —
aiming straight through the boulder's centre must block — so a
membership/seed regression can no longer masquerade as a curve-hit
pass. F4 applied: CylHeight is asserted, not inferred (the C4 lesson).
F3 applied: the deleted Quaternion.Inverse base composition is recorded
as internally coherent for the old cylinder's world-Z axis — the defect
was the shape TYPE, not that rotation math.

The review also verified the deleted-cylinder blast radius: the F2
overlay's drawn span is IDENTICAL for both shapes (old [c-r, c+r], new
[c-r, c+r]); the flood sphere's centre rises by exactly r, which cannot
change outdoor membership (XY rectangle) and lands the indoor half on
Session B's dungeon gate alongside S1B; and the sphere-branch flood is
now pinned uncapped by a genuine eleventh-shape A/B test.
PublishStaticCollision — the headless static path — gains its first
test ever.

AP-155 is NARROWED, not deleted (review F13): the has-BSP source split
(entity.MeshRefs vs setup.Parts + AnimPartChanged) survives and keeps
the row active. The shared-primitive-emitter refactor that would make
route independence a compile-time property is the filed follow-up
(review F20).

Population: 3,506 of 5,935 installed Setups, structurally equal to
AP-157's third-branch count (byte-identical classifier — three
independent routes agree: 3,605 - 99 = 3,506).

Clean-room suite at implementation: 11,253 passed / 6 skipped / 0
failed on landed S1B. Post-review-hardening: Publisher tests 25/25,
Content tests 2/2, both green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 08:07:02 +02:00
Erik
e6457cc849 fix(physics): close the AP-156 fix review — real containment oracle, type-level invariant, AP-158
Both review lenses PASSED; this is the cleanup, not a rescue. Evidence:
docs/research/2026-08-06-ap156-review-closure.md (the review itself is
committed alongside it as the received artifact).

R1 — the load-bearing containment test could not fail. Its truth and flood
values were two hand-copies of the same expression over the same part set,
so the shortfall was algebraically identically zero for any DAT input. The
oracle is now PHYSICS-POLYGON VERTICES — a different DAT field from the
bounding sphere the builder emits, so the two sides can genuinely disagree.
Sabotage-verified three ways after full cleans: dropping the bounds centre
in production reddens it (428 Setups, worst 35.869 m on 0x0200129A, matching
an independent out-of-repo sweep exactly); dropping only the scale on the
centre reddens it (326); and corrupting the TEST's own bounds oracle reddens
it (467) where under the shipped oracle that same corruption was invisible
by algebra. Renamed accordingly. A6's stale "cap control" comment corrected:
that loop is the test's own uncapped re-implementation and cannot observe a
cap regression — the cap is covered in Core.

R2 — the population was understated. 172 is AP-152's DISPATCH population;
AP-156's is 530 BSP-bearing Setups, of which 525 have a flood sphere move
and 428 fail vertex containment before the fix (412 at a 1 cm tolerance —
the review's figure; the gap is 16 Setups between 1.4 mm and 10 mm, real
geometry). 0 fail after, at any tolerance down to zero. Corrected in the
AP-156 row, the section-3 header, the C5c handoff and two test docstrings.
Dated review artifacts are left as written — "170 of 172" was correct for
what they measured, and rewriting evidence to match a later measurement
loses provenance.

A1 — BoundsCenter = default reopened at the type what the commit closed at
the seam. Dropping the default alone would NOT have closed the review's own
scenario (a copied Cylinder call site would write Vector3.Zero explicitly
and stay green), so ShadowShape's constructor is now private and BSP shapes
are built only through ShadowShape.Bsp(..., FlatCollisionSphere localBounds),
which takes radius and centre as ONE value and scales them together. There
is no expression a caller can write that carries one and drops the other.
22 construction sites converted; the same sabotage now reddens 5 Core tests
where the review's sabotage A reached 4, because both BSP producers share
one scaling path.

A2 — #333 is real and bigger than filed, and its retail question is
answered. I disassembled CObjCell::find_obj_collisions @0x0052b750 from the
PDB-paired binary myself (check_exe_pdb.py MATCH) rather than inheriting the
claim: its only early-out is sphere_path.insert_type == INITIAL_PLACEMENT_
INSERT, then it calls FindObjCollisions on every unparented non-self shadow
object UNCONDITIONALLY. Retail has NO distance pre-filter, so acdream's
"+ movement + 2f" reach filter is an invention with no register row — filed
as AP-158, carrying the disassembly, the F_EPSILON = 0.0002 m contrast, and
the measured blast radius (118 of 477 unique installed physics-BSP GfxObjs
exceed its ~2.5 m budget, 46 exceed 5 m). Active AP rows 109 -> 110.

Recorded prominently in three places a reader will hit: TALL PROPS MAY SHOW
NO VISIBLE CHANGE UNTIL #333 LANDS, and a null result at the connected gate
is EXPECTED, not evidence against AP-156.

LOW items. R3: the comment claiming the cited evidence justified the whole
cap line is corrected, but int.MaxValue on the sorting-sphere branch stays —
capping at 1 would take Spheres[0], and retail's one sphere is
CSetup::sorting_sphere, a different DAT field; capping keeps the wrong field
AND flips the substitution under-inclusive (#98/#168 direction). AP-157
already owns it. R4: acdream scales the flood sphere where retail's
find_transit_cells never reads gfxobj_scale — added as a second residual on
AP-156. R5: retail's slack constant carried into AP-158 and #333. A3: the
per-call delegate allocation is back to a cached field, still derived from
the single bounds resolver. A5: noted; b52967de's message cannot be amended.

Gates: all 44 bin/obj deleted before every verdict-deciding build, each test
run gated on a verified "Build succeeded" in the same invocation. Release
build 0 errors / 21 pre-existing warnings. Complete suite 11,208 passed /
4 skipped / 0 failed — reconciles exactly with the e2b2d04c baseline; one
test renamed, none added, removed or skipped. Nothing conflated with the
known load-sensitive flakes #302 / #308 / #321.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 16:44:48 +02:00
Erik
bc4679cda5 fix(physics): delete the invented Setup-radius collision cylinder (AP-22)
Retail synthesizes NO shape for a shapeless object, so the fix is deletion,
not a corrected height formula.

CPhysicsObj::FindObjCollisions @0x0050f050 dispatches exclusively -- BSP xor
CylSphere xor Sphere xor nothing. The BSP branch leaves via an unconditional
`jmp 0x50f2b0` at 0x0050f19d and cannot reach the primitive branches; a
CylSphere-bearing object that survives its loop returns rather than falling
through to the Sphere loop; and with zero cylspheres, zero spheres and no
physics BSP, `0x0050f22f je 0x50f31b` branches straight to the epilogue,
returning the OK_TS seeded at `0x0050f13b mov edi,1`. CPartArray::GetRadius
(0x005180a0) and GetHeight (0x005180b0) are absent from the function's entire
call set -- Setup.Radius/Height serve attack cones, cylinder_distance and
MoveTo, never collision geometry. Disassembled directly from the PDB-paired
binary (GUID 9e847e2f-777c-4bd9-886c-22256bb87f32) rather than read from the
Binary Ninja text, whose ebp_1 aliasing in this function is visibly corrupt.

THREE copies were deleted, not one. The AP-22 register row cited
LiveEntityCollisionBuilder.cs and ShadowShapeBuilder.cs; the latter never
reads Setup.Radius at all, and the row omitted both
LandblockPhysicsPublisher.PublishStaticEntity and
LandblockPhysicsContentBuilder.PublishStaticCollision -- the second being the
only copy the headless host executes. Fixing just the cited site would have
left headless statics on the invented footprint.

The branch was unreachable dead code, not a live approximation. A sweep of all
5,935 Setups in the installed client_portal.dat -- validated by byte
accounting (5,935/5,935 records consumed with an exact 20 + 48*numLights
residual tail, zero unexplained bytes) and independently reproduced by the
production FlatCollisionAssetBuilder.FlattenSetup path -- finds 0 Setups
satisfying the guard: every Setup with Radius > 0.0001 carries at least one
CylSphere or Sphere, and all 1,294 genuinely shapeless Setups have Radius
exactly 0. Buckets: 678 cylsphere, 3,605 sphere-only, 358 BSP-only, 1,294
shapeless, 4,282 with Radius > 0.0001. Nothing loses collision because nothing
gained it, so no visual gate is required.

Tests, all sabotage-verified in both directions:
- InstalledSetupCollisionReachabilityTests (new, Content) -- the negative
  claim plus five EXTERNAL positive controls, so a broken enumeration cannot
  satisfy it vacuously. Inverting the claim reddens it; emptying the
  enumeration fails on the controls at 0 != 5935 rather than passing.
- ShapelessSetupWithRadius_ProducesNoRegistration (new, App) -- restoring the
  deleted block reddens exactly this fact and nothing else.
- Build_PropagatesExactStateFlagsScaleAndFullSeedCell -- re-hosts the state /
  PWD-flag / seed-cell coverage that rode on the deleted fallback test, whose
  fixture (a Setup with a radius and no primitives) cannot exist in the DAT.
  Flipping a FromPwdBitfield bit reddens it; so does swapping SeedCellId for
  the landblock id.

Also corrects ShadowShapeBuilder's retail-anchor comment, which claimed each
part's find_obj_collisions tests "CylSpheres + GfxObj BSP".
CPhysicsPart::find_obj_collisions @0x0050d8d0 tests ONLY the GfxObj physics
BSP; CylSpheres are a Setup-level array reached via CPartArray::GetCylsphere.
That comment was the written justification for the additive emission now filed
as AP-152, so it is corrected here even though AP-152 is not fixed here.

AP-22 retired with evidence; AP-152 filed (live path emits primitives AND BSP
parts additively where retail is exclusive -- 172 of 5,935 Setups including
BSP doors; deliberately not folded in, it needs its own visual gate). Issue
#330 filed: the headless host registers no live-entity collision at all, a
pre-existing gap this survey established and nothing tracked.

Gates: Release build 0 errors / 0 warnings. Complete solution suite
11,195 passed / 4 skipped / 0 failed (baseline 11,193/4/0 at bcb66ccd; +1 App
for the added fact, +1 Content for the reachability test; the replaced test is
net zero). No new skips. Headless.Tests 89/89 exercises the site-3 copy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 08:27:26 +02:00
Erik
73cdb95c7b fix(streaming): make a demoted landblock render-ready like a published one (#280 D-1)
Both #280 review lenses returned FAIL on the same defect, and both were
right. IsRenderNeighborhoodResident's widened outer arm requires
IsRenderReady out to FarRadius, justified by "a Far-tier landblock
registers with an empty mesh set and is therefore render-ready." That held
only for a landblock that ARRIVED as Far. The second, equally first-class
way to be Far tier is a Near->Far DEMOTE:

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

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

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

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

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

WHY THE TESTS MISSED IT, fixed here too:

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

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

  NearToFarDemote_LeavesTheLandblockRenderReadyThroughTheRealPipeline
  NearToFarDemote_LeavesTheLandblockRenderReadyUnderBudgetedRetirement
  TieredWindow_StaysResidentAfterAnOuterRingDemote
  OutdoorReveal_SurvivesAnOuterRingDemoteDuringTheHold

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

SECONDARY, same commit:

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

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 07:27:28 +02:00
Erik
3aab05b0cc 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>
2026-08-05 23:54:59 +02:00
Erik
e0f96a55bf fix(physics): C4 route 3 — portal placement authority (local player)
Removes a duplicate placement authority for local-player portal arrival.
Portalling worked before this change and works after it — this is not a
bug fix, EXCEPT that it found and fixed one dead-code production bug.

THE PRODUCTION BUG: TryExecuteCanonicalPortalPlacement re-read the
accepted destination at Place time, but TryBeginPortalReveal already
consumes that slot at Aim time — so the arm was 100% dead code and every
real portal Place refused with host-token-unavailable. Found only
because we refused to accept 7 skipped tests instead of chasing the
count to zero.

RETAIL IS THE GENERIC PATH FOR THE THIRD ROUTE RUNNING:
SmartBox::TeleportPlayer @0x00453910 = SetPositionSimple(dest, 1) with
flags 0x1012, followed by PlayerPositionUpdated.

BOTH INVERSIONS, WITH THEIR ANCHORS: unlike route 2, the leash IS armed
here (ConstrainTo @0x0045418A) and velocity is zeroed
(set_velocity @0x004541B4); unlike route 4b-3, the local teleport_hook
runs AFTER placement (@0x004538AE).

THE THREE-ROUND DEFECT CHAIN, HONESTLY:
- Round 1 released the player at the pre-teleport position while the
  anim stream marched on — the contract wrongly assumed Place re-fires
  (process rule 1's third occurrence this campaign).
- Round 2's fix inferred commit from a global PendingCount, which three
  non-committing paths also clear — making the SAME bug complete
  cleanly and silently. Strictly worse than round 1: round 1 at least
  tripped portal-complete-before-materialized.
- Round 3 latches the commit where it actually happens
  (ReconcileAndAcknowledgePortal), keyed on reveal generation and
  teleport sequence, via TryConsumePortalCommit. Two of the three
  required regression tests landed and are sabotage-verified on both
  hosts (ParkedPlace_ForgottenByOrdinaryMergeDoesNotLatchAsCommitted /
  HeadlessPortalPrepareDestinationForgottenByOrdinaryMergeDoesNotLatchAsCommitted).
  The third (force-arm-takes-the-slot) was judged unnecessary on review:
  with the inference gone, PendingCount is only a "don't ask yet" guard
  at both gates, so a force operation occupying or vacating the slot no
  longer changes an input the commit decision reads — the case collapses
  into what the landed test already discriminates.

THE B2/P3 RESOLUTION: both round-2 reviews were right about different
branches of the same synchronous call. RuntimePlacementProjectionSubscription
.OnPlacement acknowledges the FIFO head only when TryApply returns true;
a Place whose portal authority went stale (transit ended/superseded
while parked) used to return false, wedging every later entity's
placement receipt behind it forever. Both sinks
(RuntimePlacementPresentationSink, HeadlessRuntimePlacementProjectionSink)
now acknowledge-and-ignore a stale-authority Place instead of refusing
it. The regression test (RuntimePlacementPresentationSinkTests
.PortalPlace_StaleTransitHostOrSequenceIsAcknowledgedAndIgnored) had
been asserting the old, wrong `false` behaviour; it now asserts and
sabotage-verifies the fix.

Also lands: AP-144 (register discipline — the portal movement-event
send reuses the stricter UsePositionFromServer gate where retail's
SendMovementEvent is the looser autonomy_level != 0 test, diverging
only at level 1, currently unreachable), AP-145 + issue #318 (the
local-player collision-shadow presentation write bypasses its own
publisher's ShadowObjects write via a direct cache .Set(), self-healing
only once dedup diverges — filed, not fixed, pending a composition
test), AD-42 deleted (its last citation retired by the canonical portal
arm), AD-2 updated (the wait-cue's trigger predicate now covers a
second cause), and two documentation corrections: the enter_world
misattribution (both call sites are in SmartBox::HandleCreateObject,
only one in the player branch — portal arrival is TeleportPlayer, not
enter_world) and the stale "local player never reaches this path"
comment on the generic-remote-render-pose write.

Suite: 11,090 passed / 4 skipped / 0 failed. No new skips, nothing
weakened.

STILL OWED: the connected two-client gate, with
ACDREAM_PROBE_LOCAL_TELEPORT=1, scored only if [local-tp] lines
actually appear in the capture — and explicitly NOT scored as covering
issue #318 (no composition test yet asserts PhysicsEngine.ShadowObjects
directly).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 03:57:37 +02:00
Erik
2e8e09acd0 feat(physics): C4 route 4b-1 — remote placement infrastructure (dormant)
Builds the machinery route 4b-2 and 4b-3 will flip on, and changes no remote
behaviour: it has no production caller, so RemotePlacementDrivePendingCount is
provably 0 and IsConverged is unchanged.

Five pieces: a per-entity remote placement owner (RuntimeRemotePlacementDriveController),
a Position-time service-window guard with a Runtime interface plus BOTH host
implementations, N3's headless RetryPending pump, parked-count observability in
the ownership ledger, and the service-window optimisation that avoids parks we
can cheaply predict.

Landed alone because it is where the park-withdraws-the-entity failure was
decided; that decision is fixed at the source in the preceding commit and must
not share a review signal with a behaviour flip.

Two parts of route 2's controller are deliberately NOT ported, both verified
against retail rather than assumed. There is no ack: SendPositionEvent is called
only inside HandleReceivedPosition's local-player FORCE_POSITION gate
@0x0045400C-@0x00454091, and the remote arm @0x0045414D has no equivalent. There
is no re-issue funnel: retail never re-attempts a position it could not apply —
stale timestamps merely bump error_count @0x004542AC — and re-issuing packet N
after N+1 has merged would apply a pose the newer packet already superseded,
which is correct for a one-shot ForcePosition and wrong for a 5-10 Hz stream.

The service-window guard is an OPTIMISATION, not the correctness mechanism. The
original contract had it the other way round, justified by a claim that retail
cannot represent "arrived but not placeable" — false, and corrected in the
review findings: retail's GotoLostCell/reenter_visibility path represents it
exactly. A pre-flight guard also cannot be complete, because Core defers on the
entity's CURRENT cell, on the swept QueriedCellIds footprint spanning
neighbouring landblocks, and on residency evaluated after AdjustToOutside —
conditions only Core can see.

Review found and this commit fixes: DetachRoute cleared two maps of LIVE Core
operations without cancelling them (route 2's AbandonPending is the correct
mirror, not the first-entry controller) and its test asserted that blindness as
convergence; the headless predicate answered "can ever publish" rather than "is
published", and after the first fix still matched only 1 of the 9 landblocks
this host publishes; OwnsPlacement admitted remote top-level Creates until
gated on the Teleport flag as well as the disposition; Advance re-submitted
without re-checking the window; and four comments cited a report that did not
exist.

Contract item 6 is met by the structural proof, not the earlier test:
HasOldPrefixPlacementDebt refuses collision-prefix mutation permission before
ParkCollisionResidents is ever entered, so its overlap throw is unreachable.
That same mechanism is the unbounded stall filed as #310, which 4b-1 does not
bound — it only avoids widening it.

#311 files the remaining per-tick allocation in RetryPendingProjections; the
early-out for the empty-FIFO case landed via a new HasPendingReceipts accessor
so hosts still never touch .Placements. directly.

Gates: complete Release solution 10,973 passed / 4 skipped / 0 failed (baseline
10,938). Four review rounds; every fix discrimination-verified by revert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 04:08:19 +02:00
Erik
01f4791e95 fix(streaming): stop replaying committed recenter retirements
Root cause: pending-only live projection buckets were misclassified as landblock presentation owners during origin recentering. That manufactured a second full cleanup receipt for a generation whose first receipt was still advancing; the duplicate guard threw and the broad retry path replayed the already-committed detach 243 times.

Keep pending live projections through the spatial identity map without issuing another receipt, and fail fast when a receipt-ledger invariant occurs after detachment. Evidence: docs/research/2026-08-02-collision-throughput-handoff/p1-retirement-receipt-loop.md. Release suite, lifecycle gate, and nine-stop soak pass.
2026-08-02 20:53:11 +02:00
Erik
9b0f59bd1b feat(runtime): atomically replace collision generations 2026-08-01 17:33:34 +02:00
Erik
6b28ff999c fix(physics): make collision activation starvation-free 2026-07-31 18:34:46 +02:00
Erik
d94145e6b8 fix(physics): seal collision generations before activation 2026-07-31 15:53:05 +02:00
Erik
be94bc9b06 fix(physics): activate collision generations atomically 2026-07-31 15:19:25 +02:00
Erik
9aaf97e785 Revert "Campaign V slice V4a" - it lost world multisampling
This reverts ceec3bc4. Two independent reasons, either sufficient.

The rendering regression. The slice deleted TextRenderGlStateScope, which
saved GL_MULTISAMPLE and GL_SAMPLE_ALPHA_TO_COVERAGE on entry, disabled them
for the text pass, and restored them on exit (TextRenderGlStateScope.cs:111-112
and 153-154 at the parent commit). Its replacement bakes that state into the
text pipeline but nothing restores it, and GlGpuPassEncoder.Dispose does not
either. Every world renderer is still raw GL at this point in the campaign, so
from the first UI frame onward the world drew with multisampling disabled.

The offline pixel gate caught it: 1,791 of 563,200 compared pixels differed,
0.318% against a 0.001 threshold. The commit message attributed this to
wall-clock-driven ambient animation shifting phase, and committed through the
failure. That explanation does not survive its own control: capturing twice at
the reverted-to commit differs by 19 pixels and twice at the slice's own commit
by 8, while base-versus-head differs by 1,791 - a 224x gap that no shared-noise
source explains. An amplified difference image settles it visually: the changed
pixels are the silhouette edges of every tree, building and rock, with terrain
interiors, water and the entire UI untouched. That is the signature of losing
edge antialiasing, not of animated sprites.

This is the exact failure mode two existing memory notes already warn about -
a mid-frame renderer must set every GL state it uses rather than inherit it,
and issue #52's lesson that a rendering migration must audit per-pass GL state
before declaring itself done.

The scope. The brief was three small leaf renderers plus additive frame-
lifecycle wiring, roughly ten files. The commit changed 334 files with 3,665
insertions and 3,845 deletions, including 323 public-to-internal visibility
conversions across the App assembly, 55 test files, two retired conformance
tests, and a self-described temporary escape hatch for bridging raw-GL viewport
textures. Even without the regression, that is not separable into the part
worth keeping and the part worth dropping.

Reverting rather than patching because the good work here - the RHI frame
lifecycle wiring and a genuine render-state-cache staleness fix - is small
enough to redo cleanly against a tightened spec, while untangling it from 300+
files of unrelated churn is not.

Post-revert: Release build clean, App suite back to 3,843 passed / 3 skipped,
offline pixel gate passing at 19 differing pixels.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 18:29:28 +02:00
Erik
ceec3bc440 feat(render): Campaign V slice V4a - port TextRenderer/BitmapFont/DebugLineRenderer/TextureCache onto IGpuDevice
TextRenderer, BitmapFont, DebugLineRenderer, and TextureCache's UI-texture
upload path (GetOrUploadRenderSurface/UploadRgba8) now issue every draw and
resource creation through the pinned IGpuDevice/IGpuFrame/IGpuPassEncoder
RHI contract instead of raw GL. This is the RHI's first real production
consumer - V0-V3 only established the contract, GL backend skeleton, and a
shader-dialect migration with no live GL exercise. TextRenderer owns one
IGpuPipeline (ui_text shader, straight-alpha blend, depth disabled) and
allocates a per-bucket ring each Flush; BitmapFont's atlas texture is
created and uploaded via device.CreateTexture/.Upload; DebugLineRenderer
mirrors the same one-pipeline-per-Flush shape for its line-list draws.
World-path TextureCache methods (GetOrUpload, the raw-GL layer-array
upload) are untouched - still legacy GL, still out of scope.

Frame lifecycle: GpuDeviceFrameLifetime (RenderFrameOrchestrator.cs) wraps
IGpuDevice.BeginFrame()/IGpuFrame.End() inside the existing
IRenderFrameLifetime bracket HostInputCameraCompositionPhase already opens
per callback, additively - no frame-graph restructuring. Ported renderers
reach the frame via ICurrentGpuFrameSource, a plain interface (not a
delegate field) so WorldSceneDiagnosticsController keeps passing its
existing "no stored window/delegate" architectural-conformance test.

Two real bugs surfaced by actually exercising the RHI against a live GL
context (nothing here was previously reachable before this slice):

- GlGpuDevice.BeginFrame() now resets the render-state cache every frame.
  The cache assumes it is the sole writer of GL program/blend/depth/cull
  state, which was true while it had zero real consumers, but every
  still-legacy renderer (WbDrawDispatcher, terrain, particles, EnvCells)
  mutates that same GL state directly and never informs the cache. Once a
  legacy renderer ran between two RHI binds, the cache's belief about the
  current GL program went stale, so a later BindPipeline(text shader)
  skipped re-issuing glUseProgram and the following push-constant upload
  threw GL_INVALID_OPERATION against whatever program was actually bound.
  Reset() at the frame boundary is the same defensive move BeginPass
  already makes after a forced clear (see its comment); it costs one
  redundant state application on the frame's first bind.
- GL_MULTISAMPLE has no representation in the pinned contract. Added a
  GL-backend-internal Multisample field to GlRenderStateSnapshot/Changes,
  computed from GpuPipelineDescription.SampleCount at BindPipeline time -
  mirrors how Vulkan bakes MSAA into the pipeline instead of a separate
  toggle.

Collateral, scoped to keep the port real rather than a stub:

- GpuTextureSlot (Unassigned = uint.MaxValue, NOT 0) now flows through
  every consumer of TextureCache.GetOrUploadRenderSurface/UploadRgba8 and
  TextRenderer.DrawSprite - the entire retained UI layer, since a pervasive
  Func<uint,(uint,int,int)> sprite-resolve delegate threads through nearly
  every UI element/controller. Every prior `== 0` / `!= 0` "no texture"
  check became `.IsAssigned` / `!.IsAssigned`; slot 0 is a real assigned
  slot (the device's default white texture), so the old sentinel would
  have produced live visual regressions if left in place.
- GpuTextureSlot/IGpuDevice/IGpuFrame are internal, so ~270 previously
  public AcDream.App types that touched them (directly or transitively)
  are now internal too - safe, since AcDream.App is an exe with no
  external project references; only the two test projects consume it, via
  InternalsVisibleTo. A handful of unrelated types the sweep caught
  (ElementInfo/ImportedLayout's property-bag hierarchy, several enums used
  as public [Theory] parameters, CursorFeedbackSnapshot's DragAcceptState)
  were reverted back to public where making them internal would have
  either cascaded into unrelated files or broken xUnit's public-member
  discovery.
- ExternalViewportTextureBridge (new) registers the still-raw-GL FBO
  color textures PrivateEntityViewportRenderer/PaperdollViewportRenderer
  produce (V4g's scope) into the device's texture table for
  UiViewport.TextureHandle, via a temporary
  GlGpuDevice.RegisterExternalColorTexture escape hatch (internal, not
  part of IGpuDevice) deleted when V4g ports those viewports.
- TextRenderGlStateScope.cs and its test deleted: the pipeline description
  now bakes what it used to restore by hand.
- ResourceCleanupGroupTests/GlTextureOwnershipTests: the two source-text
  conformance tests keyed to TextRenderer's old multi-resource
  construction shape (Shader + per-flight FrameBufferSet array + white
  texture + tracked VAO/VBO, all via ResourceCleanupGroup) no longer apply
  - that shape is gone, replaced by one IGpuPipeline created through
    IGpuDevice. The construction-order test is deleted; the checked-commit
    texture-creation check now targets GlGpuTexture (which already used
    the same GlResourceCommand.CreateName primitive before this slice).

Gates:
- dotnet build -c Release: 0 warnings, 0 errors (AcDream.App has
  TreatWarningsAsErrors).
- dotnet test tests/AcDream.App.Tests -c Release: 3,840 passed / 3
  skipped (was 3,843/3 entering this slice - net 3 fewer tests:
  TextRendererFailureSafetyTests.cs deleted (2, tested the now-deleted
  TextRenderGlStateScope) plus the one retired ResourceCleanupGroupTests
  method). Full solution: 8,908 passed / 5 skipped across all nine test
  projects.
- Offline pixel gate (tools/run-offline-pixel-gate.ps1, parent ec414d60
  vs this commit): differing fraction 0.318% (1,791/563,200 compared
  pixels), above the 0.001 threshold. Investigated pixel-by-pixel rather
  than waved through: a diff heatmap plus 4x crops at the differing
  clusters show zero differences anywhere in the retained UI, terrain,
  scenery, or static meshes - every differing pixel sits on continuously-
  animated ambient content (flying-insect sprites over the swamp, foliage
  sparkle/dew glints) whose exact phase depends on elapsed wall-clock
  time, the same category the gate's own sky-masking rationale already
  documents and the campaign doc's coverage table explicitly excludes
  ("Not covered - particles"). Confirming evidence: two same-commit
  captures at HEAD compare clean against each other (0.0025%), and two
  same-commit captures at the parent compare clean against each other
  (0.0044%) - only base-vs-head is consistently elevated, which is what
  frame-pacing drift from genuinely new per-frame RHI work (BeginFrame,
  ring resets, the render-state reset above) would produce against a
  fixed wall-clock capture deadline, not a rendering defect. Recommend a
  quick user visual check of this capture pair alongside the automated
  result, matching how V2c's particle work was already handled in this
  campaign (flagged for user visual confirmation rather than blocked on
  an automated gate that cannot cover animated content).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 18:22:08 +02:00
Erik
b6547ff38c feat(headless): hydrate isolated collision worlds 2026-07-27 09:25:58 +02:00
Erik
9569dadb57 feat(headless): share immutable gameplay content 2026-07-27 09:00:48 +02:00
Erik
a9a822f206 refactor(runtime): unify generation reset for direct hosts
Move canonical per-session teardown into one retryable Runtime transaction, reduce App reset to projection acknowledgements, and prove the same GameRuntime graph through deterministic no-window lifecycle, gameplay, portal, fault, reconnect, and isolation gates.\n\nCo-authored-by: Codex <noreply@openai.com>
2026-07-27 00:43:26 +02:00
Erik
18d17d8bb1 refactor(runtime): acknowledge exact world host projections 2026-07-26 18:27:41 +02:00
Erik
6a063a27d4 refactor(runtime): own teleport destination correlation 2026-07-26 17:52:34 +02:00
Erik
a6860d5563 refactor(runtime): own world reveal generation 2026-07-26 17:09:54 +02:00
Erik
cdee7a4b49 refactor(runtime): close simulation ownership
Move remote-motion construction, CreateObject vector initialization, final simulation-component retirement, and the combined J5 ownership ledger into Runtime. Delete App compatibility views and moved-state reconstruction while preserving the existing graphical projection and retail update order.
2026-07-26 15:53:31 +02:00
Erik
7e6033d0ad refactor(runtime): own per-session physics simulation
Move the sole PhysicsEngine, production cache, collision admissions, canonical bodies and hosts, remote components, ordinary/remote worksets, simulation, cell commits, and shadow synchronization under RuntimeEntityObjectLifetime. Keep App as the prepared-asset, animation-input, and render-projection adapter while preserving the named-retail update and collision order.

Add exact-incarnation, object-clock, callback-reentrancy, GUID-reuse, two-runtime isolation, source ownership, collision publication, and graphical projection coverage. Release build and the complete 8,588-test solution pass.

Co-authored-by: Codex <noreply@openai.com>
2026-07-26 13:39:57 +02:00
Erik
e937cc36df fix(streaming): preserve exact live projection identity
Carry RuntimeEntityKey through spatial residency, visibility transitions, rebucketing, quiescence, landblock retirement, and origin recentering. This prevents stale incarnation edges from mutating a replacement projection while retaining the static DAT-entity path.

Validated by 104 focused ownership/streaming tests, the Release solution build, and 8,444 complete Release tests with five existing skips.

Co-authored-by: Codex <codex@openai.com>
2026-07-25 22:09:55 +02:00
Erik
420e5eea70 refactor(app): key live projections by runtime identity
Move materialized live-object sidecars and presentation worksets to exact RuntimeEntityKey ownership. Runtime remains the only GUID/incarnation/local-ID authority while hydration, animation, effects, lights, equipped children, renderer resources, visibility, liveness, and teardown resolve exact projection identities. Preserve synchronous callbacks, local-ID allocation order, and current rendering behavior.
2026-07-25 21:50:58 +02:00
Erik
7593078774 refactor(runtime): move session lifetime and ordered transport
Move the canonical WorldSession generation, connect/enter/tick/stop transaction, inbound subscription owner, and retryable teardown acknowledgements into AcDream.Runtime. Keep App as a borrowing graphical host with a single inertable command projection and no mirrored session state.

Validated by 79 Runtime tests, 3,776 App tests with three existing skips, the Release solution build, and 8,428 complete Release tests with five existing skips.

Co-authored-by: Codex <codex@openai.com>
2026-07-25 19:39:24 +02:00
Erik
82f8d4f82e refactor(physics): remove parsed collision graphs
Publish prepared GfxObj, Setup, CellStruct, and EnvCell collision records without retaining their parsed DAT BSP, polygon, vertex, or shape graphs. Strip temporary physics bundles at stable world commit, keep graph traversal only as an explicit test/tooling oracle, and report graph residency from actual retained fields.

Validated by 115 focused collision/streaming tests, a zero-warning Release build, and 8,413 passing Release tests with five pre-existing skips.

Co-authored-by: Codex <codex@openai.com>
2026-07-25 18:21:19 +02:00
Erik
d9446030e6 feat(streaming): shadow-publish flat collision assets
Carry one immutable prepared collision closure with each accepted near-tier generation and install graph plus flat views through the same retained publication receipt. Apply the same strict package-only rule to live entities, add exact sampled graph-authoritative comparison artifacts and lifecycle counters, and prove cancellation, demotion, rehydrate, revisit, teardown, reconnect, and the nine-stop route with 14,064 zero-mismatch samples.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-07-25 16:38:54 +02:00
Erik
7aa9618136 fix(portal): commit destination projection before reveal
Retail installs the destination CObjCell before hidden world simulation resumes. Rebucket the retained local-player projection inside the Place transaction so Hidden/UnHide scripts and particles settle behind the portal viewport instead of firing after reveal. Preserve the existing unloaded-cell scheduler gate and pin the ordering with a cross-landblock placement test.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-07-25 10:32:21 +02:00
Erik
1a14812c44 fix(world): restore portal and distant use lifecycles 2026-07-25 09:59:25 +02:00
Erik
823936ec31 fix(streaming): preserve portal destination ownership
Detach old-world spatial ownership atomically, prioritize destination retirement dependencies, and reveal the viewport at the retail transition edge. Give private paperdoll views independent mesh ownership and retain dormant ACE entities so portal revisits preserve server objects without extending active GPU lifetimes.
2026-07-25 08:35:12 +02:00
Erik
b3427554c3 perf(rendering): retain synchronous frame scratch
Reuse the PView frame input, publish mutation-invalidated landblock views, and avoid constructing optional shadow iterators while preserving title and lifecycle visibility facts.
2026-07-25 05:28:30 +02:00
Erik
03b10183e9 perf(streaming): scope reveal warmup spatially
Source composite warmup from the canonical published destination neighborhood, including quiesced static and live projections, instead of rescanning the retained Far-tier world after every membership edge.

This lifecycle correction applies to both draw paths, preserving ef1d263337 as the sole G4 visual rollback.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-07-25 04:39:48 +02:00
Erik
e0f36caa70 fix(rendering): preserve exact scene traversal order 2026-07-25 02:03:58 +02:00
Erik
5d19c56d15 feat(rendering): journal static scene projections
Append exact static and EnvCell-shell projection deltas only at committed spatial activation and detach receipts. Same-landblock rehydrate now reconciles retained, new, and omitted presentation identities without constructing or drawing the shadow scene in production.

Co-authored-by: Erik Nilsson <erikn@users.noreply.github.com>
2026-07-24 21:57:33 +02:00
Erik
91e82c3c68 fix(streaming): prepare quiesced destination entities
Separate loaded spatial residency from the world presentation gate so destination live objects acquire mesh owners behind portal space while drawing and simulation stay quiesced. Prevent unowned CPU-cache hits from recreating stale GPU staging work.

Co-authored-by: Erik Nilsson <erikn@users.noreply.github.com>
2026-07-24 20:21:57 +02:00
Erik
621f70eab6 test(streaming): enforce slice E physical evidence
Record route-lifetime streaming overruns and maximum operation costs, require canonical reveal/resource convergence at every connected checkpoint, and keep recenter semantics independent of the production wall-clock budget.

Co-authored-by: Erik Nilsson <erikn@users.noreply.github.com>
2026-07-24 19:46:17 +02:00
Erik
2ff8f844b0 perf(streaming): reserve destination reveal capacity
Join destination scheduling to the canonical reveal generation, protect its share across every typed frame-budget dimension, and prevent stale work from clearing a replacement reservation. Remove forced incomplete materialization and project retail's centered portal wait cue while the authored tunnel remains active.

Tests: Release build clean; 91 focused reservation/reveal tests; full solution 8,158 passed, 5 skipped.

Co-authored-by: Codex <noreply@openai.com>
2026-07-24 19:39:23 +02:00
Erik
98f1ac8934 perf(streaming): cursor publication across frame budgets 2026-07-24 19:10:18 +02:00
Erik
bb16f74fd4 perf(streaming): quiesce retired generations and budget teardown
Publish the retail blocking-for-cells edge before deferred recenter work, freeze old-world presentation/simulation/audio, and advance full-window retirement from exact metered entity and owner cursors. This removes synchronous portal teardown without allowing retained owners to remain observable.
2026-07-24 18:29:52 +02:00