acdream/docs/research/2026-07-13-envcell-landblock-transaction.md
Erik b0c175afc0 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>
2026-07-13 18:45:24 +02:00

4.6 KiB

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:

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:

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:

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.