# Indoor Cell Rendering — Phase 2 Cause Report **Date:** 2026-05-19 **Predecessor:** Phase 1 capture confirmed H1 (silent failure in WB). **Capture method:** Phase 2's `ContinueWith` + `ConsoleErrorLogger` injected into WB's `ObjectMeshManager` surfaced the exception WB was silently catching. ## Cause **Single failure mode:** `ArgumentOutOfRangeException` thrown from `DatReaderWriter.DBObjs.Setup.Unpack` at WB's [`ObjectMeshManager.cs:1223`](../../references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/ObjectMeshManager.cs:1223): ```csharp // For EnvCell static objects, we need to manually collect emitters if they are Setups if (_dats.Portal.TryGet(stab.Id, out var stabSetup)) { // ← throws ``` WB iterates `envCell.StaticObjects` and **blindly calls `TryGet` on every stab id**, regardless of whether the id is actually a Setup-prefix (`0x02xxxxxx`) or a GfxObj-prefix (`0x01xxxxxx`). When stab.Id is a GfxObj, `DatReaderWriter` finds the file (Portal dat has both GfxObjs and Setups under the same tree-lookup) and attempts to deserialize the GfxObj bytes as a Setup record. The Setup format is structurally different — early parse fails inside `QualifiedDataId.Unpack` → `DatBinReader.ReadBytesInternal` throws `ArgumentOutOfRangeException`. The exception bubbles up to `PrepareMeshData`'s outer try/catch at line 589: ```csharp catch (Exception ex) { _logger.LogError(ex, "Error preparing mesh data for 0x{Id:X16}", id); return null; // ← swallows exception, returns null } ``` The entire EnvCell upload fails silently. The cell's room geometry (floor / walls / ceiling) never reaches `_renderData`, so the dispatcher skips drawing it. Static objects inside the cell (which acdream hydrates separately) still render — they have their own GfxObj uploads. **This also explains the user's "objects below ground" observation:** with the floor mesh missing, you see the cell's static objects (tables / chairs / fireplaces) through where the floor should be. Visually they appear "below ground." ## Sample evidence 55 NULL_RESULT cells captured at multiple landblocks (`0xA5B4`, `0xA7B4`, `0xA8B2`, `0xA9B0`, `0xA9B2`, `0xA9B3`, `0xA9B4`). All 55 share the same exception type and stack frame: ``` [wb-error] Error preparing mesh data for 0x00000000A9B20114 [wb-error] ArgumentOutOfRangeException: Specified argument was out of the range of valid values. [wb-error] at DatReaderWriter.DBObjs.Setup.Unpack(DatBinReader reader) [wb-error] at DatReaderWriter.DatDatabase.TryGet[T](UInt32 fileId, T& value) [wb-error] at WorldBuilder.Shared.Services.DefaultDatDatabase.TryGet[T](UInt32 fileId, T& value) [wb-error] at Chorizite.OpenGLSDLBackend.Lib.ObjectMeshManager.PrepareEnvCellMeshData(...) line 1223 [wb-error] at Chorizite.OpenGLSDLBackend.Lib.ObjectMeshManager.PrepareMeshData(...) line 571 ``` For Holtburg (`0xA9B4`) specifically: 123 requested → 97 completed + 26 silently failed. The 26 failures all match this exception signature. The first interior cell `0xA9B40100` is among them — exactly where the user reported a missing floor. ## Why the other hypotheses were ruled out Phase 1 ruled out H2-H6 via the captured probe data. Phase 2's diagnostic walk: 1. `ourCellDb.TryGet=True` — acdream's DatCollection finds the cell. 2. `wbResolveId.Count=1` — WB's ResolveId also finds it. 3. `wbSelectedType=EnvCell` — type classification is correct. 4. `wbDbTryGet=True` — the cell record IS loadable by WB. 5. `hadRenderData=False` at request time — no pre-existing cache hit. All preconditions for a successful upload were met. The failure was in a downstream emitter-collection step (line 1223) that's tangential to the cell's own geometry — but its exception silently kills the entire upload. ## Fix **One-line WB fork patch.** Pre-check the Setup-prefix bit before calling `TryGet`: ```csharp // Before: if (_dats.Portal.TryGet(stab.Id, out var stabSetup)) { // After: if ((stab.Id & 0xFF000000u) == 0x02000000u && _dats.Portal.TryGet(stab.Id, out var stabSetup)) { ``` For GfxObj-prefixed stabs (which have no `DefaultScript` and no emitters anyway), the branch is now skipped correctly. For Setup-prefixed stabs, behavior is unchanged. This is in our WB fork at [`references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/ObjectMeshManager.cs:1230`](../../references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/ObjectMeshManager.cs:1230). The patch should be upstreamed — it's a real WB bug. ## Verification approach After applying the fix: 1. Re-launch with `ACDREAM_PROBE_INDOOR_UPLOAD=1`. 2. Walk Holtburg. 3. Expect: zero `[wb-error]` lines, zero `[indoor-upload] NULL_RESULT` lines. Previously-failing cells now have `[indoor-upload] completed` lines. 4. Visual: floor renders in Holtburg Inn; objects no longer appear "below ground." ## Phase 1 → Phase 2 chain summary The diagnostic-driven approach worked end-to-end: - **Phase 1:** Added 5 probes. Identified that 26 Holtburg cells silently fail. Confirmed H1 class of bug. Could not pinpoint without exception data. - **Phase 2 Task 1:** Wrapped `PrepareMeshDataAsync` in a continuation to capture `Task.Exception`. Found that the task was never faulted — `tcs.TrySetResult(null)` ran instead. Hypothesized exception was swallowed inside `PrepareMeshData`. - **Phase 2 cause-narrowing diagnostics:** Added `ourCellDb.TryGet` + `wbResolveId.Count` + `wbSelectedType` + `wbDbIsPortal` + `wbDbTryGet` + `hadRenderData` checks. Each iteration narrowed the cause class. - **Phase 2 final probe:** Replaced WB's `NullLogger` with a Console-backed `ConsoleErrorLogger`. WB's existing `_logger.LogError(ex, ...)` call at the catch block immediately surfaced 55 ArgumentOutOfRangeException stack traces with file:line locations. **Cause definitively identified in one capture.** - **Phase 2 fix:** One-line guard at the throwing call site. Total runtime: ~3 client launches to nail it.