fix(streaming): commit EnvCell landblocks atomically (#214)

Carry one complete cell payload with each streaming completion and publish visibility, physics, and render state on the render thread. Remove the obsolete post-readiness login reload that triggered a duplicate dungeon build.

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-13 18:45:24 +02:00
parent 85980fe13f
commit b0c175afc0
26 changed files with 973 additions and 617 deletions

View file

@ -0,0 +1,106 @@
# EnvCell landblock transaction — dungeon-login texture loss
## Symptom and captured evidence
Logging the `lundberg` account directly into starter dungeon landblock
`0x8C04FFFF` produced an untextured/incomplete world after relaunch. The capture
in `logs/lundberg-dungeon-relaunch-20260713-182018.out.log` established the
ordering:
```text
streaming: dungeon collapse -> 0x8C04FFFF
streaming: dungeon collapse -> 0x8C04FFFF
[late-register] ... 1123 already committed
[finalize-replace] ... DISCARDING 1123 committed instances, replacing with 11 pending
```
The old renderer diagnostics came from commit `0a38d934`. They proved that the
first job completed a full dungeon and a second job then overwrote it with the
small tail of a concurrently mutated pending list. This was not a texture decode
or GPU upload failure: the complete cell-shell set was discarded before draw.
## Root causes
There were two causes, both required for the observed deterministic failure:
1. Commit `9b06a9b8` added a post-login `ForceReloadWindow` to recover from a
guessed-center streaming window. Commit `fa9aedca` later added
`StreamingReadinessGate`, which prevents that guessed window from being built
at all, but the force reload remained. A sealed-dungeon login therefore
pre-collapsed once during spawn and collapsed a second time after the forced
reset.
2. The streaming worker published partial state while it was still building.
`EnvCellRenderer.RegisterCell` appended to landblock-owned pending lists and a
global `_pendingCells` bag collected portal cells. The render thread drained
those mutable collections independently. Neither collection identified the
streaming job that produced an item, so duplicate and concurrent landblock
completions could consume or replace each other's state.
The second cause is the architectural defect. Removing only the duplicate load
would make the current reproduction rarer, but leave the race available to
normal unload/reload and promotion timing.
## Retail/WB boundary
No retail gameplay algorithm changes here. The cell portal construction remains
the existing mechanical port of retail `PView::InitCell @ 0x005A4B70`, including
the dat `PortalSide` bit and reciprocal portal indexing. Geometry-id arithmetic
remains the verbatim WorldBuilder `EnvCellRenderManager.cs:94-103` port.
This change is an acdream streaming ownership correction: retail does not expose
partially hydrated cell arrays to its draw traversal, while acdream must bridge a
background CPU loader to a single OpenGL/render thread.
## Transaction design
Worker-side pseudocode:
```text
builder = new EnvCellLandblockBuildBuilder(landblockId)
for each EnvCell in this one landblock:
read dat cell + CellStruct
append visibility cell to builder-local storage
append drawable shell placement to builder-local storage
build = builder.Build() // one-way publication; immutable snapshot
schedule CPU mesh preparation(build) // queue only; no live renderer mutation
return LandblockBuild(terrain/entities, build)
```
Render-thread pseudocode:
```text
completion = streamer.Drain()
validate completion.EnvCells belongs to completion.Landblock
replace this landblock's complete CellVisibility set
cache this landblock's cell physics structs
replace this landblock's complete EnvCellRenderer snapshot
publish the LoadedLandblock to GpuWorldState
```
The invariant is: **one streaming completion owns one complete EnvCell payload;
only the render thread may publish it to live per-frame state.** A later reload
may replace an earlier snapshot, but it can only replace it with another complete
snapshot from that exact job—never a shared-list remainder.
## Login sequencing correction
`StreamingController.InitializeKnownLoginCenter` now encodes the post-readiness-
gate behavior:
- sealed dungeon: pre-collapse exactly once before the first normal tick;
- outdoor: enqueue nothing until the first tick at the server-confirmed center.
The obsolete `_pendingForceReloadWindow` path was deleted. This removes the
duplicate dungeon job without adding a timing guard, retry, or suppression flag.
## Regression coverage
- A 1,123-shell build produces a 1,123-instance committed snapshot.
- Independently built 1,123- and 11-shell transactions cannot share storage.
- A builder can publish only once.
- Visibility replacement affects only the matching landblock and rejects a
foreign cell before mutating existing state.
- `LandblockStreamer` carries the exact `LandblockBuild`/EnvCell transaction
instance from worker to consumer.
- Repeated sealed-dungeon login initialization enqueues one collapse/load; an
outdoor login waits for its first correctly centered tick.