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
|
|
@ -46,6 +46,49 @@ Copy this block when adding a new issue:
|
|||
|
||||
---
|
||||
|
||||
## #214 — Dungeon login could replace a complete EnvCell render with a partial second load
|
||||
|
||||
**Status:** IN-PROGRESS — architectural fix complete 2026-07-13, pending user visual gate
|
||||
**Severity:** HIGH
|
||||
**Component:** streaming / EnvCell rendering / portal visibility
|
||||
|
||||
**Description:** Logging directly into the starter dungeon could leave its room
|
||||
shells untextured or absent after a relaunch even though the same dungeon had
|
||||
rendered correctly before logout.
|
||||
|
||||
**Root cause:** Two independent load jobs published through shared, process-wide
|
||||
pending collections. The login spawn first pre-collapsed streaming onto
|
||||
`0x8C04FFFF`, then an obsolete post-spawn `ForceReloadWindow` reset the collapse
|
||||
and enqueued the same dungeon again. `EnvCellRenderer.RegisterCell` mutated a
|
||||
live landblock's pending lists from the worker while `FinalizeLandblock` drained
|
||||
and replaced them on the render thread; the global portal-visibility bag had the
|
||||
same ownership flaw across landblocks. The captured failure committed 1,123
|
||||
shells, then the duplicate completion replaced them with its remaining 11.
|
||||
|
||||
**Resolution:** A worker now builds one private, immutable
|
||||
`EnvCellLandblockBuild` per `LandblockBuild`. That exact payload travels with its
|
||||
streaming completion and is committed on the render thread to
|
||||
`CellVisibility`, `EnvCellRenderer`, and the cell physics cache. There are no
|
||||
shared pending cell/shell collections to drain or cross-contaminate. The login
|
||||
path now calls `InitializeKnownLoginCenter`: sealed dungeons pre-collapse once;
|
||||
outdoor spawns wait for the first correctly centered tick. The old forced reload
|
||||
was removed because `StreamingReadinessGate` already prevents guessed-center
|
||||
loads before the real player spawn.
|
||||
|
||||
**Files:** `src/AcDream.App/Streaming/LandblockBuild.cs`;
|
||||
`src/AcDream.App/Rendering/Wb/EnvCellLandblockBuild.cs`;
|
||||
`src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs`;
|
||||
`src/AcDream.App/Rendering/CellVisibility.cs`;
|
||||
`src/AcDream.App/Rendering/GameWindow.cs`.
|
||||
|
||||
**Research:** `docs/research/2026-07-13-envcell-landblock-transaction.md`.
|
||||
|
||||
**Acceptance:** Log out inside the starter dungeon, relaunch, and enter the same
|
||||
character. The dungeon is fully textured on its first frame, remains complete,
|
||||
and the log contains exactly one login dungeon collapse with no second reload.
|
||||
|
||||
---
|
||||
|
||||
## #213 — Retail client commands were sent to ACE as chat text
|
||||
|
||||
**Status:** IN-PROGRESS — command-family gate passed except two corrective fixes, pending re-gate
|
||||
|
|
|
|||
|
|
@ -394,6 +394,24 @@ bypass; no Scissor over-include). This is a **consolidation of existing machiner
|
|||
(`PortalVisibilityBuilder` + `ClipFrame`), not a rewrite. Do NOT add a fourth special-case
|
||||
gate to mask a seam — that anti-pattern produced the patchwork.
|
||||
|
||||
### Streaming publication ownership
|
||||
|
||||
The streaming worker may read dats and build CPU payloads, but it must not mutate
|
||||
live render-frame registries. A near-tier job produces one `LandblockBuild` whose
|
||||
optional `EnvCellLandblockBuild` owns the complete portal-cell and drawable-shell
|
||||
sets for that landblock. The exact build object crosses the `LandblockStreamer`
|
||||
completion channel. On the render thread, `StreamingController` applies that one
|
||||
completion to `CellVisibility`, cell physics state, `EnvCellRenderer`, and finally
|
||||
`GpuWorldState` during the same update-frame drain.
|
||||
|
||||
This is the publication boundary: per-job builders are private and single-use;
|
||||
completed payloads are immutable snapshots; live landblock stores are replaced as
|
||||
complete units. Process-wide pending bags or renderer-owned pending lists are not
|
||||
valid streaming seams because a duplicate load or a second landblock can drain or
|
||||
replace state produced by another job. CPU mesh extraction may still be scheduled
|
||||
onto `ObjectMeshManager`'s thread-safe work queue, but the worker never publishes
|
||||
partially hydrated cell membership or shell placement to the renderer.
|
||||
|
||||
## Roadmap Model
|
||||
|
||||
The old R1-R8 architecture sequence was a useful early refactor sketch, but it
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ src/AcDream.App/
|
|||
│ ├── TextureCache.cs # (already exists)
|
||||
│ ├── ParticleRenderer.cs # (already exists)
|
||||
│ ├── Sky/ # (already exists)
|
||||
│ ├── Wb/ # (already exists — WB seam)
|
||||
│ ├── Wb/ # WB seam + EnvCellLandblockBuild transaction
|
||||
│ └── Vfx/ # (already exists)
|
||||
├── Net/
|
||||
│ └── LiveSessionController.cs # owns WorldSession lifecycle, login/handshake, reconnect
|
||||
|
|
@ -191,7 +191,7 @@ src/AcDream.App/
|
|||
│ └── LiveEntityRuntime.cs # owns per-entity state dicts + ServerGuid↔entity.Id translation
|
||||
├── Interaction/
|
||||
│ └── SelectionInteractionController.cs # owns WorldPicker, selection state, Use/PickUp dispatch
|
||||
├── Streaming/ # (already exists)
|
||||
├── Streaming/ # LandblockStreamer + immutable LandblockBuild completion
|
||||
├── Input/ # (already exists)
|
||||
├── Audio/ # (already exists)
|
||||
└── Plugins/ # (already exists)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,15 @@ our tree (see CLAUDE.md for the full breakdown):
|
|||
`ObjectMeshManager`, `WbMeshAdapter`, `WbDrawDispatcher`, texture cache,
|
||||
shader infra, EnvCell/portal/scenery/terrain-blending pipeline classes.
|
||||
|
||||
**EnvCell streaming seam:** `EnvCellLandblockBuildBuilder` is an acdream-owned
|
||||
adapter around the extracted WB rendering path. One streaming job privately builds
|
||||
the complete portal-cell + shell-placement payload, and `EnvCellRenderer.CommitLandblock`
|
||||
publishes that completed snapshot on the render thread. WB's geometry-id arithmetic
|
||||
and mesh preparation remain unchanged; the transaction wrapper exists because
|
||||
acdream streams asynchronously while the WB editor's manager owned its own loading
|
||||
loop. Do not reintroduce worker-side `RegisterCell` calls or shared pending cell
|
||||
collections.
|
||||
|
||||
`DatCollectionAdapter` bridges our `IDatCollection` to the `IDatReaderWriter`
|
||||
interface WB's internals expect (O-D7 fallback; `ObjectMeshManager` has 26
|
||||
internal `_dats.*` call sites — above the 20-site inline-swap threshold).
|
||||
|
|
|
|||
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