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:
parent
85980fe13f
commit
b0c175afc0
26 changed files with 973 additions and 617 deletions
106
docs/research/2026-07-13-envcell-landblock-transaction.md
Normal file
106
docs/research/2026-07-13-envcell-landblock-transaction.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue