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>
106 lines
4.6 KiB
Markdown
106 lines
4.6 KiB
Markdown
# 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.
|