Merge branch 'claude/flamboyant-williams-ef9a41' — indoor cell rendering Phase 1 + Phase 2
Phase 1 (diagnostics) + Phase 2 (fix) for the missing indoor floors. Root cause: WB's PrepareEnvCellMeshData at ObjectMeshManager.cs:1223 blindly called TryGet<Setup> on every EnvCell.StaticObjects entry, including GfxObj-prefixed (0x01xxxxxx) ids. DatReaderWriter tried to parse GfxObj bytes as Setup format, threw ArgumentOutOfRangeException, which WB's outer try/catch silently swallowed. 26/123 Holtburg cells failed upload — their floor / wall / ceiling geometry never rendered. Fix shape: pre-check Setup-prefix bit before calling TryGet<Setup>. Committed to the WB submodule on the acdream branch (eriknihlen/WorldBuilder@34460c4); submodule pointer advanced here. Diagnostic infrastructure (5 [indoor-*] probes + ContinueWith exception surfacer + ConsoleErrorLogger injection into WB) shipped in AcDream.Core.Rendering.RenderingDiagnostics + WbMeshAdapter + WbDrawDispatcher + DebugPanel — left in place for future indoor debugging. Other fixes: - Indoor ambient: hardcoded (0.10, 0.09, 0.08) → retail's (0.20, 0.20, 0.20) per CellManager::ChangePosition @ 0x004559B0. - Indoor lighting trigger: switched from cameraInsideCell to playerInsideCell (third-person chase camera enters interiors first; retail keys lighting off player position). Surfaced 9 pre-existing indoor bugs filed in docs/ISSUES.md (#78-#86): see-through floor (#78), spurious spot lights (#79), 2nd-floor darkness (#80), atmospheric lighting on stabs (#81), slope terrain lighting (#82), broken stairs (#83), blocked by air (#84), pass-through walls (#85), click-through-walls (#86). Probable shared root causes group into cell-BSP cluster + indoor-lighting plumbing cluster. Visual-verified at Holtburg Inn — floors render. 0 [wb-error] (was 385). 0 [indoor-upload] NULL_RESULT (was 55). Specs: docs/superpowers/specs/2026-05-19-indoor-cell-rendering-fix-design.md docs/superpowers/specs/2026-05-19-phase2-indoor-cell-rendering-fix-design.md Plans: docs/superpowers/plans/2026-05-19-indoor-cell-rendering-phase1-diagnostics.md docs/superpowers/plans/2026-05-19-phase2-indoor-cell-rendering-fix.md Research: docs/research/2026-05-19-indoor-cell-rendering-probe-capture.md docs/research/2026-05-19-indoor-cell-rendering-cause.md docs/research/2026-05-19-indoor-cell-rendering-verification.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
3bf30d2c2b
19 changed files with 3170 additions and 19 deletions
276
docs/ISSUES.md
276
docs/ISSUES.md
|
|
@ -46,7 +46,281 @@ Copy this block when adding a new issue:
|
||||||
|
|
||||||
# Active issues
|
# Active issues
|
||||||
|
|
||||||
## #76 — [DONE 2026-05-16 · `0b25df5`] LiveSessionController extraction (Step 2) regresses interaction + chat outbound
|
---
|
||||||
|
|
||||||
|
## Indoor walking issue cluster (2026-05-19)
|
||||||
|
|
||||||
|
The Phase 2 indoor cell rendering fix (floor now renders inside buildings)
|
||||||
|
surfaced nine pre-existing indoor bugs the user observed at Holtburg Inn
|
||||||
|
the moment they could walk indoors. None caused by the floor fix — all
|
||||||
|
existed before but were unobservable because there was no floor to stand
|
||||||
|
on. Filed individually below; #78 + #84 + #85 + #86 likely share a root
|
||||||
|
cause (cell BSP / portal-cull plumbing), and #79 + #80 + #81 + #82 share
|
||||||
|
the indoor-lighting plumbing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## #78 — Outdoor stabs/buildings visible through the rendered floor
|
||||||
|
|
||||||
|
**Status:** OPEN
|
||||||
|
**Severity:** HIGH (immediate visual jank now that floors render)
|
||||||
|
**Filed:** 2026-05-19
|
||||||
|
**Component:** rendering, visibility
|
||||||
|
|
||||||
|
**Description:** Standing inside Holtburg Inn looking at the floor or
|
||||||
|
walls, the user sees other buildings in the distance at their correct
|
||||||
|
world position + scale — but visible THROUGH the floor and walls. As if
|
||||||
|
the cell mesh is rendered but doesn't occlude or stencil-cull what's
|
||||||
|
behind it.
|
||||||
|
|
||||||
|
**Root cause / status:** Two plausible causes:
|
||||||
|
1. The `+0.02f` Z bump applied to cell origin at `GameWindow.cs:5362`
|
||||||
|
pushes the floor mesh 2 cm above terrain, so depth test correctly
|
||||||
|
occludes terrain. But OUTDOOR STABS (landblock-baked building geometry)
|
||||||
|
at the same X,Y may have Z values comparable to or higher than the
|
||||||
|
cell-mesh floor, producing z-fighting / see-through.
|
||||||
|
2. Outdoor stabs aren't being culled when the player is inside an
|
||||||
|
EnvCell — this is the Phase 1 Task 3 deferred work
|
||||||
|
("Cull outdoor stabs when indoors via VisibleCellIds"). WB has a
|
||||||
|
`RenderInsideOut` stencil pipeline (`references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilityManager.cs`)
|
||||||
|
that acdream never invokes.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` (per-entity walk —
|
||||||
|
consider gating outdoor stab entities on visible-cell membership).
|
||||||
|
- `src/AcDream.App/Rendering/CellVisibility.cs:222+` (`ComputeVisibility`
|
||||||
|
returns `VisibleCellIds`; the dispatcher already filters by
|
||||||
|
`entity.ParentCellId ∈ visibleCellIds` but outdoor stabs have
|
||||||
|
`ParentCellId == null` so they always pass).
|
||||||
|
|
||||||
|
**Acceptance:** Standing inside a sealed-interior cell, no outdoor
|
||||||
|
geometry is visible through floor/walls. Standing where a cell has a
|
||||||
|
real outdoor portal (door open, window) outdoor geometry is correctly
|
||||||
|
visible through the portal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## #79 — Indoor lighting: spurious spot lights on walls
|
||||||
|
|
||||||
|
**Status:** OPEN
|
||||||
|
**Severity:** MEDIUM
|
||||||
|
**Filed:** 2026-05-19
|
||||||
|
**Component:** lighting
|
||||||
|
|
||||||
|
**Description:** Walking around inside Holtburg Inn, the user sometimes
|
||||||
|
sees spot-light-like patches on the interior walls that don't correspond
|
||||||
|
to retail's lighting.
|
||||||
|
|
||||||
|
**Root cause / status:** Point lights from cell static objects (torch
|
||||||
|
entities) are being registered via `LightInfoLoader.Load` + `LightingHookSink`
|
||||||
|
(Phase 1 verified). Their per-light parameters (position, range, intensity,
|
||||||
|
cone) may be wrong — wrong falloff treatment, wrong world-space transform,
|
||||||
|
or wrong direction for spot lights. Spec at
|
||||||
|
`docs/research/deepdives/r13-dynamic-lighting.md` documents the retail
|
||||||
|
LightInfo→LightSource mapping but the live behavior hasn't been verified
|
||||||
|
against retail.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/AcDream.Core/Lighting/LightInfoLoader.cs`
|
||||||
|
- `src/AcDream.App/Rendering/Shaders/mesh_modern.frag` — `accumulateLights`
|
||||||
|
spot-cone logic.
|
||||||
|
|
||||||
|
**Acceptance:** Side-by-side comparison with retail at the inn shows
|
||||||
|
matching torch-light pools.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## #80 — Camera on 2nd floor goes very dark
|
||||||
|
|
||||||
|
**Status:** OPEN
|
||||||
|
**Severity:** MEDIUM
|
||||||
|
**Filed:** 2026-05-19
|
||||||
|
**Component:** lighting
|
||||||
|
|
||||||
|
**Description:** Walking up to the second floor of a building, the
|
||||||
|
lighting suddenly goes much darker than retail.
|
||||||
|
|
||||||
|
**Root cause / status:** Possible causes:
|
||||||
|
1. The `playerInsideCell` lighting trigger (Phase 1 / commit `1024ba3`)
|
||||||
|
uses `CellVisibility.IsInsideAnyCell(playerPos)` which is a brute-force
|
||||||
|
PointInCell scan. The 2nd floor cell may not be in the loaded set OR
|
||||||
|
may have wrong bounds.
|
||||||
|
2. The per-cell ambient is currently a flat `(0.20, 0.20, 0.20)` for
|
||||||
|
any indoor cell. Retail has per-cell ambient overrides; ours doesn't
|
||||||
|
read them. A 2nd-floor cell with stairwell shadowing may need a
|
||||||
|
different value.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/AcDream.App/Rendering/GameWindow.cs:8330+` (`UpdateSunFromSky`,
|
||||||
|
indoor branch).
|
||||||
|
|
||||||
|
**Acceptance:** 2nd-floor cells render with similar brightness to
|
||||||
|
ground floor; transition is not abrupt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## #81 — Static building stabs don't react to atmospheric lighting changes
|
||||||
|
|
||||||
|
**Status:** OPEN
|
||||||
|
**Severity:** MEDIUM
|
||||||
|
**Filed:** 2026-05-19
|
||||||
|
**Component:** lighting, rendering
|
||||||
|
|
||||||
|
**Description:** Outside, time-of-day changes (sunrise/sunset/lightning)
|
||||||
|
don't visibly affect static building stabs (the inn / cottages). The
|
||||||
|
buildings stay statically lit while terrain and scenery shift colors.
|
||||||
|
|
||||||
|
**Root cause / status:** Stabs are rendered through `WbDrawDispatcher`
|
||||||
|
with `mesh_modern.frag` which DOES consume the `SceneLightingUbo`
|
||||||
|
(sun + ambient + fog). Verify the shader is being used for stabs and
|
||||||
|
that the UBO is bound at the right binding slot per draw call.
|
||||||
|
Possibly a shader-path divergence — terrain uses `terrain_modern.frag`,
|
||||||
|
entities use `mesh_modern.frag`, but stabs/scenery may be on a
|
||||||
|
different path.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/AcDream.App/Rendering/Shaders/mesh_modern.frag`
|
||||||
|
- `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs`
|
||||||
|
|
||||||
|
**Acceptance:** Stabs darken/brighten in sync with terrain + scenery
|
||||||
|
across the day/night cycle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## #82 — Some slope terrain lit incorrectly
|
||||||
|
|
||||||
|
**Status:** OPEN
|
||||||
|
**Severity:** LOW (cosmetic)
|
||||||
|
**Filed:** 2026-05-19
|
||||||
|
**Component:** rendering, terrain
|
||||||
|
|
||||||
|
**Description:** Specific terrain slopes appear lit "wrong" compared to
|
||||||
|
retail.
|
||||||
|
|
||||||
|
**Root cause / status:** Likely terrain normal calculation or the
|
||||||
|
landblock-edge normal-blending divergence between WB and retail (per
|
||||||
|
`feedback_wb_migration_formulas.md` — WB's terrain split formula
|
||||||
|
differs from retail's `FSplitNESW`).
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/AcDream.App/Rendering/TerrainModernRenderer.cs`
|
||||||
|
- `src/AcDream.App/Rendering/Shaders/terrain_modern.frag`
|
||||||
|
|
||||||
|
**Acceptance:** Side-by-side comparison with retail at the same Holtburg
|
||||||
|
slopes shows matching shading.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## #83 — Walking up stairs broken
|
||||||
|
|
||||||
|
**Status:** OPEN
|
||||||
|
**Severity:** HIGH (blocks vertical indoor traversal)
|
||||||
|
**Filed:** 2026-05-19
|
||||||
|
**Component:** physics, movement
|
||||||
|
|
||||||
|
**Description:** When the player tries to walk up stairs inside a
|
||||||
|
building, movement is broken — gets stuck, gets bounced, or fails to
|
||||||
|
ascend.
|
||||||
|
|
||||||
|
**Root cause / status:** The retail physics has explicit step-up logic
|
||||||
|
(`CPhysicsObj::step_up` etc.) ported into `PhysicsEngine` for outdoor
|
||||||
|
terrain ramps. For indoor stairs (EnvCell CellStruct geometry composed
|
||||||
|
of polygons), the step-up resolver may not be examining cell BSP
|
||||||
|
correctly, OR cell BSP and cell mesh disagree on stair Z values.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/AcDream.Core/Physics/PhysicsEngine.cs`
|
||||||
|
- `src/AcDream.Core/Physics/TransitionTypes.cs` (cell BSP query path).
|
||||||
|
|
||||||
|
**Acceptance:** Walking forward at the base of an inn stairwell ascends
|
||||||
|
to the second floor without getting stuck.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## #84 — Blocked by air indoors
|
||||||
|
|
||||||
|
**Status:** OPEN
|
||||||
|
**Severity:** HIGH (blocks indoor navigation)
|
||||||
|
**Filed:** 2026-05-19
|
||||||
|
**Component:** physics, collision
|
||||||
|
|
||||||
|
**Description:** While walking inside buildings, the player sometimes
|
||||||
|
collides with invisible obstacles in mid-floor where there's nothing
|
||||||
|
visible.
|
||||||
|
|
||||||
|
**Root cause / status:** Cell BSP geometry doesn't align with the
|
||||||
|
visible cell mesh. Possibilities:
|
||||||
|
1. The `cellTransform` applied to physics in
|
||||||
|
`_physicsDataCache.CacheCellStruct(envCellId, cellStruct, cellTransform)`
|
||||||
|
at `GameWindow.cs:5384` includes the `+0.02f` Z bump, but the BSP
|
||||||
|
geometry may not be lifted with it — physics geometry sits 2cm BELOW
|
||||||
|
render geometry, so invisible "ceilings" at floor-level cause
|
||||||
|
blockage.
|
||||||
|
2. CellStruct BSP contains polygons that the cell mesh doesn't include
|
||||||
|
(or vice versa) — the two are derived from different fields.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/AcDream.App/Rendering/GameWindow.cs:5362-5384` (cellOrigin Z bump
|
||||||
|
+ physics cache call).
|
||||||
|
|
||||||
|
**Acceptance:** Walking through interior cell space hits collisions
|
||||||
|
only where visible walls/furniture exist.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## #85 — Pass through walls from outside→in
|
||||||
|
|
||||||
|
**Status:** OPEN
|
||||||
|
**Severity:** HIGH (gameplay-breaking)
|
||||||
|
**Filed:** 2026-05-19
|
||||||
|
**Component:** physics, collision
|
||||||
|
|
||||||
|
**Description:** Approaching a building from the outside, the player
|
||||||
|
can walk THROUGH walls into the interior — one-directional wall
|
||||||
|
collision. From the inside trying to exit, the wall does block.
|
||||||
|
|
||||||
|
**Root cause / status:** Cell BSP polygons likely have one-sided
|
||||||
|
normals (front-facing only). Approach from the inside hits the front;
|
||||||
|
approach from the outside hits the back which BSP traversal treats as
|
||||||
|
"behind the plane" → no collision. Retail handles this via two-sided
|
||||||
|
collision polys or per-poly back-face handling.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/AcDream.Core/Physics/BSPQuery.cs`
|
||||||
|
- `src/AcDream.Core/Physics/TransitionTypes.cs` (`FindObjCollisions` cell
|
||||||
|
branch).
|
||||||
|
|
||||||
|
**Acceptance:** Walking into an inn wall from outside collides; player
|
||||||
|
must enter via the door portal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## #86 — Click selection penetrates walls
|
||||||
|
|
||||||
|
**Status:** OPEN
|
||||||
|
**Severity:** MEDIUM
|
||||||
|
**Filed:** 2026-05-19
|
||||||
|
**Component:** input, interaction
|
||||||
|
|
||||||
|
**Description:** Clicking through a wall from the outside selects NPCs
|
||||||
|
and objects inside the building. The `WorldPicker` raycast doesn't
|
||||||
|
intersect cell BSP geometry.
|
||||||
|
|
||||||
|
**Root cause / status:** `WorldPicker.BuildRay + Pick` (introduced in
|
||||||
|
Phase B.4) tests against entity AABBs and scenery BSPs but probably
|
||||||
|
not cell BSP. Outdoor NPCs are pickable because their entity AABB is
|
||||||
|
the test target; indoor NPCs are pickable from outside because the
|
||||||
|
wall isn't in the ray's intersection set.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `src/AcDream.App/Rendering/WorldPicker.cs` (or equivalent — check
|
||||||
|
Phase B.4b reference).
|
||||||
|
|
||||||
|
**Acceptance:** Clicking on a wall doesn't select NPCs behind it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**Status:** DONE
|
**Status:** DONE
|
||||||
**Severity:** MEDIUM (refactor blocker; doesn't affect main branch which is unchanged)
|
**Severity:** MEDIUM (refactor blocker; doesn't affect main branch which is unchanged)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# acdream — strategic roadmap
|
# acdream — strategic roadmap
|
||||||
|
|
||||||
**Status:** Living document. Updated 2026-05-12. **Between phases.** **Since the last header update:** C.1.5b shipped (issue #56 per-part transforms for multi-emitter PES + `EntityScriptActivator` extended to dat-hydrated EnvCell statics & exterior stabs — portal swirl, inn fireplace flames, cottage chimney smoke, spell-cast particles all match retail). **Earlier this week:** post-A.5 polish completed (#52 lifestone, #54 JobKind, #53 Tier 1 cache); N.6 slice 1 shipped (gpu_us fix + radius=12 perf baseline, conclusion CPU dominates GPU 30–50×); C.1.5a shipped (portal PES wiring; surfaced #56 → resolved in C.1.5b).
|
**Status:** Living document. Updated 2026-05-19. **Between phases.** **Since the last header update:** Indoor cell rendering Phase 1 (diagnostics) + Phase 2 (fix) shipped — root cause was a one-line WB bug at `ObjectMeshManager.cs:1223` (blind `TryGet<Setup>` on GfxObj-prefixed stab ids threw `ArgumentOutOfRangeException` which WB's outer catch silently swallowed, causing 26/123 Holtburg cells to fail upload). Identified via diagnostic chain (5 `[indoor-*]` probes + a `ContinueWith` exception surfacer + a `ConsoleErrorLogger` injected into WB), fixed with a Setup-prefix guard. User visually confirmed floors render. Surfaced 9 pre-existing indoor bugs filed in `docs/ISSUES.md`. **Earlier:** C.1.5b shipped (issue #56 per-part transforms for multi-emitter PES + `EntityScriptActivator` extended to dat-hydrated EnvCell statics & exterior stabs — portal swirl, inn fireplace flames, cottage chimney smoke, spell-cast particles all match retail). post-A.5 polish completed (#52 lifestone, #54 JobKind, #53 Tier 1 cache); N.6 slice 1 shipped (gpu_us fix + radius=12 perf baseline, conclusion CPU dominates GPU 30–50×); C.1.5a shipped (portal PES wiring; surfaced #56 → resolved in C.1.5b).
|
||||||
**Purpose:** One source of truth for where the project is and where it's going. Every observed defect or missing feature has a named phase that owns it; when something looks wrong in-game, look here to find the phase that'll address it. Implementation details live in per-phase specs under `docs/superpowers/specs/`, not in this file.
|
**Purpose:** One source of truth for where the project is and where it's going. Every observed defect or missing feature has a named phase that owns it; when something looks wrong in-game, look here to find the phase that'll address it. Implementation details live in per-phase specs under `docs/superpowers/specs/`, not in this file.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -68,6 +68,8 @@
|
||||||
| B.4b | Outbound Use handler wiring + 4 bonus fixes (L.2g slices 1b+1c, double-click detection, DoubleClick gate fix). Shipped 2026-05-13 (branch `claude/compassionate-wilson-23ff99`, merge pending). Closes #57. Files #58 (door swing animation, M1-deferred). `WorldPicker.BuildRay` + `Pick` (ray-sphere entity pick with inside-sphere guard); `GameWindow.OnInputAction` switch cases for `SelectLeft` / `SelectDblLeft` / `UseSelected`; `_entitiesByServerGuid` reverse-lookup dict + ServerGuid→entity.Id translation in `OnLiveStateUpdated` (L.2g slice 1c — THE actual blocker); `InputDispatcher` double-click detection 500ms threshold (binding was dead code without it); `CollisionExemption.ShouldSkip` widened to ETHEREAL-alone (ACE Door.Open() sends `state=0x0001000C`, not `0x14`). M1 demo target "open the inn door" verified at Holtburg inn doorway. Plan: [`docs/superpowers/plans/2026-05-13-phase-b4b-plan.md`](../superpowers/plans/2026-05-13-phase-b4b-plan.md). Handoff: [`docs/research/2026-05-13-b4b-shipped-handoff.md`](../research/2026-05-13-b4b-shipped-handoff.md). | Live ✓ |
|
| B.4b | Outbound Use handler wiring + 4 bonus fixes (L.2g slices 1b+1c, double-click detection, DoubleClick gate fix). Shipped 2026-05-13 (branch `claude/compassionate-wilson-23ff99`, merge pending). Closes #57. Files #58 (door swing animation, M1-deferred). `WorldPicker.BuildRay` + `Pick` (ray-sphere entity pick with inside-sphere guard); `GameWindow.OnInputAction` switch cases for `SelectLeft` / `SelectDblLeft` / `UseSelected`; `_entitiesByServerGuid` reverse-lookup dict + ServerGuid→entity.Id translation in `OnLiveStateUpdated` (L.2g slice 1c — THE actual blocker); `InputDispatcher` double-click detection 500ms threshold (binding was dead code without it); `CollisionExemption.ShouldSkip` widened to ETHEREAL-alone (ACE Door.Open() sends `state=0x0001000C`, not `0x14`). M1 demo target "open the inn door" verified at Holtburg inn doorway. Plan: [`docs/superpowers/plans/2026-05-13-phase-b4b-plan.md`](../superpowers/plans/2026-05-13-phase-b4b-plan.md). Handoff: [`docs/research/2026-05-13-b4b-shipped-handoff.md`](../research/2026-05-13-b4b-shipped-handoff.md). | Live ✓ |
|
||||||
| B.4c | Door swing animation. Shipped 2026-05-13 (branch `claude/phase-b4c-door-anim`, merge pending). Closes #58. Files #61 (AnimationSequencer link→cycle boundary flash; low-severity polish) + #62 (PARTSDIAG null-guard; latent). Spawn-time `AnimationSequencer` registration for door entities in `GameWindow.OnLiveEntitySpawnedLocked`: initial cycle seeded from `spawn.PhysicsState` (Off for closed, On for open). Shared `IsDoorName` / `IsDoorSpawn` helpers. `[door-cycle]` diagnostic in `OnLiveMotionUpdated` (gated on `ACDREAM_PROBE_BUILDING`). Bonus stance-value fix: `NonCombat = 0x3D` not `0x01` (wrong value caused doors to render halfway underground via empty sequencer frames). Visual-verified 2026-05-13 at Holtburg inn doorway: swing-open + swing-close cycles both play. M1 demo target "open the inn door" now has full visual feedback. Plan: [`docs/superpowers/plans/2026-05-13-phase-b4c-plan.md`](../superpowers/plans/2026-05-13-phase-b4c-plan.md). Handoff: [`docs/research/2026-05-13-b4c-shipped-handoff.md`](../research/2026-05-13-b4c-shipped-handoff.md). | Live ✓ |
|
| B.4c | Door swing animation. Shipped 2026-05-13 (branch `claude/phase-b4c-door-anim`, merge pending). Closes #58. Files #61 (AnimationSequencer link→cycle boundary flash; low-severity polish) + #62 (PARTSDIAG null-guard; latent). Spawn-time `AnimationSequencer` registration for door entities in `GameWindow.OnLiveEntitySpawnedLocked`: initial cycle seeded from `spawn.PhysicsState` (Off for closed, On for open). Shared `IsDoorName` / `IsDoorSpawn` helpers. `[door-cycle]` diagnostic in `OnLiveMotionUpdated` (gated on `ACDREAM_PROBE_BUILDING`). Bonus stance-value fix: `NonCombat = 0x3D` not `0x01` (wrong value caused doors to render halfway underground via empty sequencer frames). Visual-verified 2026-05-13 at Holtburg inn doorway: swing-open + swing-close cycles both play. M1 demo target "open the inn door" now has full visual feedback. Plan: [`docs/superpowers/plans/2026-05-13-phase-b4c-plan.md`](../superpowers/plans/2026-05-13-phase-b4c-plan.md). Handoff: [`docs/research/2026-05-13-b4c-shipped-handoff.md`](../research/2026-05-13-b4c-shipped-handoff.md). | Live ✓ |
|
||||||
| B.5 | Ground-item pickup (F-key, close-range path). Shipped 2026-05-14 (branch `claude/phase-b5-pickup`, merge pending). Closes M1 demo target 4/4 *"pick up an item"*. New `InteractRequests.BuildPickUp(seq, itemGuid, containerGuid, placement)` builds the 24-byte `PutItemInContainer (0xF7B1/0x0019)` wire body verified against `references/ACE/Source/ACE.Server/Network/GameAction/Actions/GameActionPutItemInContainer.cs`. New private `GameWindow.SendPickUp(uint itemGuid)` helper mirrors `SendUse`'s gate-on-InWorld pattern; `case InputAction.SelectionPickUp` in `OnInputAction` switch routes the F-key through `_selectedGuid`. **Bonus wire-handler fix (Task 2b):** ACE despawns picked-up items via `GameMessagePickupEvent (0xF74A)`, not the `GameMessageDeleteObject (0xF747)` we already handled — surfaced during visual testing (item kept rendering on ground after successful server-side pickup). New `PickupEvent.cs` parser + `WorldSession` dispatch branch adapt to `DeleteObject.Parsed` and reuse the existing `EntityDeleted → OnLiveEntityDeleted → RemoveLiveEntityByServerGuid` chain. Files #63 (server-initiated `MoveToObject` auto-walk not honored — out-of-range pickup / double-click fails server-side timeout) + #64 (local-player pickup animation does not render). Visual-verified 2026-05-14 at Holtburg: 3 successful close-range pickups (Pink Taper + Violet Tapers), item despawns locally as ACE acks. Plan: [`docs/superpowers/plans/2026-05-14-phase-b5-pickup.md`](../superpowers/plans/2026-05-14-phase-b5-pickup.md). Handoff: [`docs/research/2026-05-14-b5-shipped-handoff.md`](../research/2026-05-14-b5-shipped-handoff.md). | Live ✓ |
|
| B.5 | Ground-item pickup (F-key, close-range path). Shipped 2026-05-14 (branch `claude/phase-b5-pickup`, merge pending). Closes M1 demo target 4/4 *"pick up an item"*. New `InteractRequests.BuildPickUp(seq, itemGuid, containerGuid, placement)` builds the 24-byte `PutItemInContainer (0xF7B1/0x0019)` wire body verified against `references/ACE/Source/ACE.Server/Network/GameAction/Actions/GameActionPutItemInContainer.cs`. New private `GameWindow.SendPickUp(uint itemGuid)` helper mirrors `SendUse`'s gate-on-InWorld pattern; `case InputAction.SelectionPickUp` in `OnInputAction` switch routes the F-key through `_selectedGuid`. **Bonus wire-handler fix (Task 2b):** ACE despawns picked-up items via `GameMessagePickupEvent (0xF74A)`, not the `GameMessageDeleteObject (0xF747)` we already handled — surfaced during visual testing (item kept rendering on ground after successful server-side pickup). New `PickupEvent.cs` parser + `WorldSession` dispatch branch adapt to `DeleteObject.Parsed` and reuse the existing `EntityDeleted → OnLiveEntityDeleted → RemoveLiveEntityByServerGuid` chain. Files #63 (server-initiated `MoveToObject` auto-walk not honored — out-of-range pickup / double-click fails server-side timeout) + #64 (local-player pickup animation does not render). Visual-verified 2026-05-14 at Holtburg: 3 successful close-range pickups (Pink Taper + Violet Tapers), item despawns locally as ACE acks. Plan: [`docs/superpowers/plans/2026-05-14-phase-b5-pickup.md`](../superpowers/plans/2026-05-14-phase-b5-pickup.md). Handoff: [`docs/research/2026-05-14-b5-shipped-handoff.md`](../research/2026-05-14-b5-shipped-handoff.md). | Live ✓ |
|
||||||
|
| Indoor lighting + rendering — Phase 1 (diagnostics) | Five `[indoor-*]` probes wired through new `AcDream.Core.Rendering.RenderingDiagnostics` static class + DebugVM mirrors + DebugPanel checkboxes. `WbMeshAdapter` emits `[indoor-upload] requested/completed`; `WbDrawDispatcher` emits `[indoor-walk]`, `[indoor-lookup]`, `[indoor-xform]`, `[indoor-cull]` per cell entity. All rate-limited via per-cellId frame counter; lookup probe uses high-bit-tagged key namespace to avoid cross-probe suppression. Holtburg `ACDREAM_PROBE_INDOOR_ALL=1` capture identified 26/123 cells silently failing — confirmed H1 (WB swallowed exception). Spec: [`docs/superpowers/specs/2026-05-19-indoor-cell-rendering-fix-design.md`](../superpowers/specs/2026-05-19-indoor-cell-rendering-fix-design.md). Plan: [`docs/superpowers/plans/2026-05-19-indoor-cell-rendering-phase1-diagnostics.md`](../superpowers/plans/2026-05-19-indoor-cell-rendering-phase1-diagnostics.md). Capture: [`docs/research/2026-05-19-indoor-cell-rendering-probe-capture.md`](../research/2026-05-19-indoor-cell-rendering-probe-capture.md). | Tests ✓ |
|
||||||
|
| Indoor lighting + rendering — Phase 2 (fix) | Three-component diagnostic-driven fix for missing-floor bug. Component 1: `WbMeshAdapter` captures the `Task<ObjectMeshData?>` from `PrepareMeshDataAsync` and attaches a `ContinueWith` for EnvCell ids — surfaces faulted-task exceptions + clean-null returns. Component 2: replaced `NullLogger<ObjectMeshManager>` with a Console-backed `ConsoleErrorLogger<T>` so WB's intentional `_logger.LogError(ex, ...)` at the swallow site at `ObjectMeshManager.cs:589` writes `[wb-error]` lines. **Root cause definitively identified in one capture: `ArgumentOutOfRangeException` from `DatReaderWriter.Setup.Unpack` at WB's `PrepareEnvCellMeshData` line 1223 — `TryGet<Setup>(stab.Id, ...)` was called blindly on every `envCell.StaticObjects` id without checking the Setup-prefix bit. GfxObj-typed stabs (0x01xxxxxx) caused mid-deserialization throws, bubbling up to PrepareMeshData's outer catch which silently returned null. Entire cell upload failed, room mesh never reached `_renderData`.** Component 3 fix: one-line type-check guard `(stab.Id & 0xFF000000u) == 0x02000000u && _dats.Portal.TryGet<Setup>(stab.Id, out var stabSetup)`. Committed to WB submodule on branch `acdream-fix-floor-rendering` at SHA `34460c4` — needs submodule pointer advance at merge time. **Verification: 0 [wb-error] (was 385), 0 NULL_RESULT (was 55), Holtburg 123/123 cells complete (was 97/123). User visually confirmed floors render in Holtburg Inn.** Surfaced 9 pre-existing indoor bugs (see-through floor, indoor collision, stairs, walls, click-thru, indoor lighting artifacts, atmospheric-lighting-on-stabs, slope terrain lighting) — all filed in `docs/ISSUES.md` for follow-up phases. Cause report: [`docs/research/2026-05-19-indoor-cell-rendering-cause.md`](../research/2026-05-19-indoor-cell-rendering-cause.md). Verification: [`docs/research/2026-05-19-indoor-cell-rendering-verification.md`](../research/2026-05-19-indoor-cell-rendering-verification.md). Plan: [`docs/superpowers/plans/2026-05-19-phase2-indoor-cell-rendering-fix.md`](../superpowers/plans/2026-05-19-phase2-indoor-cell-rendering-fix.md). | Live ✓ |
|
||||||
| C.1.5b | Per-part PES transforms + dat-hydrated entity DefaultScript dispatch. Closes issue #56. Shipped 2026-05-12 across 5 commits (`1e3c33b` docs+plan, `f3bc15e` SetupPartTransforms helper, `11521f4` ParticleHookSink applies `CreateParticleHook.PartIndex`, `5ca5827` activator refactor + GameWindow resolver lambda, `8735c39` GpuWorldState 4 new fire-sites). **Slice A** — new [`SetupPartTransforms.Compute(setup)`](../../src/AcDream.Core/Meshing/SetupPartTransforms.cs) walks `PlacementFrames[Resting]` → `[Default]` → first-available (mirrors `SetupMesh.Flatten` priority) and returns `Matrix4x4` per part; new `ParticleHookSink.SetEntityPartTransforms(entityId, partTransforms)` mirrors the existing `_rotationByEntity` pattern; `SpawnFromHook` now transforms hook offset through `partTransforms[partIndex]` before applying entity rotation. **Slice B** — activator's `ServerGuid==0` guard relaxed: keys by `entity.ServerGuid` when non-zero, else `entity.Id` (collision-free with server guids in the `0x40xxxxxx` interior / `0x80xxxxxx` scenery / `0xC0xxxxxx` ranges). Resolver delegate refactored to return `ScriptActivationInfo(ScriptId, PartTransforms)` so one dat lookup yields both pieces. `GpuWorldState` fires the activator from 4 new sites: `AddLandblock` + `AddEntitiesToExistingLandblock` (Far→Near promotion) for OnCreate, `RemoveLandblock` + `RemoveEntitiesFromLandblock` (Near→Far demotion) for OnRemove. ServerGuid==0 filter on AddLandblock avoids double-firing pending-bucket merges. **Reality discovery folded into spec §3**: EnvCell `StaticObjects` are already hydrated as `WorldEntity` instances by `GameWindow.BuildInteriorEntitiesForStreaming` (with stable `entity.Id` in `0x40xxxxxx`) — no synthetic-ID scheme or separate walker class needed (handoff §4 Q1/Q2 mooted). **Visual verification 2026-05-12**: Holtburg Town network portal swirl distributes across the arch (no ground-burial), Inn fireplace flames render over the firebox, cottage chimney smoke columns render, spell-cast animation-hook particles all match retail. 18 new + 4 updated tests, all Vfx/Meshing/Streaming/Activator green. Spec: [`docs/superpowers/specs/2026-05-13-phase-c1.5b-design.md`](../superpowers/specs/2026-05-13-phase-c1.5b-design.md). Plan: [`docs/superpowers/plans/2026-05-13-phase-c1.5b.md`](../superpowers/plans/2026-05-13-phase-c1.5b.md). | Live ✓ |
|
| C.1.5b | Per-part PES transforms + dat-hydrated entity DefaultScript dispatch. Closes issue #56. Shipped 2026-05-12 across 5 commits (`1e3c33b` docs+plan, `f3bc15e` SetupPartTransforms helper, `11521f4` ParticleHookSink applies `CreateParticleHook.PartIndex`, `5ca5827` activator refactor + GameWindow resolver lambda, `8735c39` GpuWorldState 4 new fire-sites). **Slice A** — new [`SetupPartTransforms.Compute(setup)`](../../src/AcDream.Core/Meshing/SetupPartTransforms.cs) walks `PlacementFrames[Resting]` → `[Default]` → first-available (mirrors `SetupMesh.Flatten` priority) and returns `Matrix4x4` per part; new `ParticleHookSink.SetEntityPartTransforms(entityId, partTransforms)` mirrors the existing `_rotationByEntity` pattern; `SpawnFromHook` now transforms hook offset through `partTransforms[partIndex]` before applying entity rotation. **Slice B** — activator's `ServerGuid==0` guard relaxed: keys by `entity.ServerGuid` when non-zero, else `entity.Id` (collision-free with server guids in the `0x40xxxxxx` interior / `0x80xxxxxx` scenery / `0xC0xxxxxx` ranges). Resolver delegate refactored to return `ScriptActivationInfo(ScriptId, PartTransforms)` so one dat lookup yields both pieces. `GpuWorldState` fires the activator from 4 new sites: `AddLandblock` + `AddEntitiesToExistingLandblock` (Far→Near promotion) for OnCreate, `RemoveLandblock` + `RemoveEntitiesFromLandblock` (Near→Far demotion) for OnRemove. ServerGuid==0 filter on AddLandblock avoids double-firing pending-bucket merges. **Reality discovery folded into spec §3**: EnvCell `StaticObjects` are already hydrated as `WorldEntity` instances by `GameWindow.BuildInteriorEntitiesForStreaming` (with stable `entity.Id` in `0x40xxxxxx`) — no synthetic-ID scheme or separate walker class needed (handoff §4 Q1/Q2 mooted). **Visual verification 2026-05-12**: Holtburg Town network portal swirl distributes across the arch (no ground-burial), Inn fireplace flames render over the firebox, cottage chimney smoke columns render, spell-cast animation-hook particles all match retail. 18 new + 4 updated tests, all Vfx/Meshing/Streaming/Activator green. Spec: [`docs/superpowers/specs/2026-05-13-phase-c1.5b-design.md`](../superpowers/specs/2026-05-13-phase-c1.5b-design.md). Plan: [`docs/superpowers/plans/2026-05-13-phase-c1.5b.md`](../superpowers/plans/2026-05-13-phase-c1.5b.md). | Live ✓ |
|
||||||
|
|
||||||
Plus polish that doesn't get its own phase number:
|
Plus polish that doesn't get its own phase number:
|
||||||
|
|
|
||||||
94
docs/research/2026-05-19-indoor-cell-rendering-cause.md
Normal file
94
docs/research/2026-05-19-indoor-cell-rendering-cause.md
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
# 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<Setup>(stab.Id, out var stabSetup)) { // ← throws
|
||||||
|
```
|
||||||
|
|
||||||
|
WB iterates `envCell.StaticObjects` and **blindly calls `TryGet<Setup>` 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<EnvCell>=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<Setup>`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Before:
|
||||||
|
if (_dats.Portal.TryGet<Setup>(stab.Id, out var stabSetup)) {
|
||||||
|
|
||||||
|
// After:
|
||||||
|
if ((stab.Id & 0xFF000000u) == 0x02000000u
|
||||||
|
&& _dats.Portal.TryGet<Setup>(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<EnvCell>` + `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.
|
||||||
105
docs/research/2026-05-19-indoor-cell-rendering-probe-capture.md
Normal file
105
docs/research/2026-05-19-indoor-cell-rendering-probe-capture.md
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
# Indoor Cell Rendering — Phase 1 Probe Capture
|
||||||
|
|
||||||
|
**Date:** 2026-05-19
|
||||||
|
**Probe:** Phase 1 diagnostic probes from spec `2026-05-19-indoor-cell-rendering-fix-design.md`
|
||||||
|
**Capture conditions:** `ACDREAM_PROBE_INDOOR_ALL=1`, walk into Holtburg (landblock `0xA9B4`).
|
||||||
|
**Verdict:** Hypothesis **H1 (WB silently returns null from `PrepareEnvCellMeshData`)** is **CONFIRMED** for ~21% of Holtburg's EnvCells, including the first interior cell `0xA9B40100`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Probe line breakdown (real EnvCell-format IDs only)
|
||||||
|
|
||||||
|
| Probe | Count | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `[indoor-upload] requested` (0xA9B4 cells) | 123 (unique) | LandblockSpawnAdapter triggers PrepareMeshDataAsync for every cell in Holtburg landblock. |
|
||||||
|
| `[indoor-upload] completed` (0xA9B4 cells) | **97** (unique) | **26 cells never produce a completed line.** |
|
||||||
|
| `[indoor-walk]` (cell-room entities, 0xA9B4) | 27,631 | Cell-room entities pass `landblockVisible` + `aabbVisible` + `cellInVis` filters. Walk path is healthy. |
|
||||||
|
| `[indoor-lookup]` (0xA9B4 cells) | 6,067 | Total dispatcher lookups for Holtburg cells. |
|
||||||
|
| `[indoor-lookup] hit=True` | 45 | Only ~0.7% hit rate — the rate-limited probe captures one snapshot per cell after rendering stabilizes. |
|
||||||
|
| `[indoor-lookup] hit=False` | 6,022 | Most are pre-upload-completion frames + the 26 silently-failing cells. |
|
||||||
|
| `[indoor-xform]` | 97 | One per successfully-uploaded cell. Cell-geom SetupPart's render data is non-null and reaches `ComposePartWorldMatrix`. |
|
||||||
|
|
||||||
|
## Hypotheses
|
||||||
|
|
||||||
|
### H1 — WB silently returns null from `PrepareEnvCellMeshData` ✅ CONFIRMED
|
||||||
|
|
||||||
|
26 out of 123 Holtburg cells (21%) get an `[indoor-upload] requested` line but **never** produce an `[indoor-upload] completed` line. This is the classic H1 signature: WB's `ObjectMeshManager.PrepareMeshData` either returns null (line 568, 583, 592 of `ObjectMeshManager.cs`) or its catch-block swallows an exception at line 589-592. The pending `meshData` never reaches `StagedMeshData`, so `Tick()`'s drain never sees it, no completion line emits.
|
||||||
|
|
||||||
|
**First 15 cells with no completion:**
|
||||||
|
|
||||||
|
```
|
||||||
|
0xA9B40100, 0xA9B40111, 0xA9B40112, 0xA9B40117, 0xA9B4011B,
|
||||||
|
0xA9B40121, 0xA9B40123, 0xA9B40129, 0xA9B4012A, 0xA9B4012E,
|
||||||
|
0xA9B40138, 0xA9B4013F, 0xA9B40141, 0xA9B40143, 0xA9B40147
|
||||||
|
```
|
||||||
|
|
||||||
|
`0xA9B40100` is **the first indoor cell** in Holtburg landblock. Almost certainly the inn entry or another major building's anchor cell — exactly where the user reported "floor missing."
|
||||||
|
|
||||||
|
### H2 — Empty batches ❌ RULED OUT
|
||||||
|
|
||||||
|
For successfully-completed cells, `cellGeomVerts` ranges 14–86 and `hasEnvCellGeom=True`. Geometry is non-empty when the upload completes. The 26 failing cells fail BEFORE batch construction, so this isn't an empty-batch problem.
|
||||||
|
|
||||||
|
### H3 — Cull bug ❌ RULED OUT
|
||||||
|
|
||||||
|
`[indoor-cull]` lines for cell-room entities show `visibleCellIds-miss` reasons only for cells in *other* landblocks (`0xA9B0`, `0xA9B2`, `0xA9B3` etc., visible neighbours of Holtburg but outside the active visibility set). For Holtburg's own cells, the walk probe shows `landblockVisible=true aabbVisible=true cellInVis=true` consistently — the dispatcher reaches them.
|
||||||
|
|
||||||
|
### H4 — Double-spawn ❌ RULED OUT
|
||||||
|
|
||||||
|
For completed cells, `[indoor-lookup]` reports modest `partCount` values (1–46) matching the number of static objects + 1 cell-geom part. No evidence of duplicate registration.
|
||||||
|
|
||||||
|
### H5 — Transform double-apply ❌ RULED OUT
|
||||||
|
|
||||||
|
`[indoor-xform]` consistently shows `entityWorldT=(0,0,0)`, `partT=(0,0,0)`, and `composedT==meshRefT`. The composed translation equals the cell's world origin — no double-apply. Sample:
|
||||||
|
|
||||||
|
```
|
||||||
|
[indoor-xform] cellGeomId=0x00000001A9B40101
|
||||||
|
entityWorldT=(0.00,0.00,0.00)
|
||||||
|
meshRefT=(84.09,131.54,66.02)
|
||||||
|
partT=(0.00,0.00,0.00)
|
||||||
|
composedT=(84.09,131.54,66.02)
|
||||||
|
```
|
||||||
|
|
||||||
|
### H6 — MeshRefs structure mismatch ❌ RULED OUT
|
||||||
|
|
||||||
|
For uploaded cells, `[indoor-lookup]` shows `hit=True isSetup=True partsHit≈partCount`. The dispatcher correctly traverses the Setup parts. Sample: `[indoor-lookup] cellId=0xA9B40101 hit=True isSetup=True partCount=10 hasEnvCellGeom=True partsHit=9 partsMiss=1`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What's special about the 26 failing cells?
|
||||||
|
|
||||||
|
Unknown from Phase 1 probes alone. Possible causes (each verifiable with one or two more targeted probes or code reads in Phase 2):
|
||||||
|
|
||||||
|
1. **Missing Environment dat record** — `envCell.EnvironmentId` points at an Environment id that `_dats.Portal.TryGet<Environment>` can't find. WB's `PrepareEnvCellMeshData` line 1245 would silently return without populating `cellGeometry`, then the outer Setup path produces a result with `hasBounds=false` and an empty `parts` list. Hmm, but that would still produce a `completed` line — just with empty data. **So this would be H2-shaped, not H1-shaped.** Ruled out.
|
||||||
|
|
||||||
|
2. **Exception in `PrepareCellStructMeshData`** — texture decode failure, surface ID resolution failure, polygon enumeration crash. The catch-block at `PrepareMeshData` line 589 silently swallows. **Most likely cause.**
|
||||||
|
|
||||||
|
3. **`ResolveId(envCellId)` returns empty** — WB's `DefaultDatReaderWriter` can't find the cell record in its loaded dats. Unlikely (all region cells are loaded at construction), but possible if `_wbDats.Portal.TryGet<Region>` skipped the region containing 0xA9B4.
|
||||||
|
|
||||||
|
4. **Race condition** — `PrepareMeshData` runs on a background worker; if the same cell id is requested twice in fast succession before the first completes, the second `TryAdd` to `_preparationTasks` returns false and silently skips. Unlikely given LandblockSpawnAdapter's per-landblock dedup at line 68 of `LandblockSpawnAdapter.cs`, but possible if multiple landblocks share state.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2 — recommended approach
|
||||||
|
|
||||||
|
The fix shape per the spec table maps H1 to: *"Add WB logging or pre-check the dat resolution path in WbMeshAdapter."*
|
||||||
|
|
||||||
|
Concrete Phase 2 plan:
|
||||||
|
|
||||||
|
1. **Targeted probe extension** — add a SECOND probe inside the failing path. Either patch WB to surface the swallowed exception (`PrepareMeshData` line 589 catch block) OR wrap the `PrepareMeshDataAsync` call in WbMeshAdapter with our own try/catch + task continuation that logs the actual `Exception` for EnvCell ids. One launch with this captures the actual failure reason for the 26 cells.
|
||||||
|
|
||||||
|
2. **Match the failure to a fix** — once we know the failure mode:
|
||||||
|
- If a texture/surface bug → file as a Phase 2 WB-fork patch.
|
||||||
|
- If a missing dat reference → check whether the user's `client_cell_1.dat` is up to date.
|
||||||
|
- If an exception in our code path → fix the specific bug.
|
||||||
|
|
||||||
|
3. **Verify** by re-launching with the probe and confirming `[indoor-upload] completed` appears for previously-missing cells (e.g., `0xA9B40100`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1 leftover observations
|
||||||
|
|
||||||
|
- The `IsEnvCellId(ulong id) => (id & 0xFFFFu) >= 0x0100u` helper has false positives on GfxObj IDs whose lower 24 bits happen to be ≥ 0x0100 (e.g., `0x01001841`). This polluted ~95% of probe emissions with non-cell entities. Recommend tightening the helper to also require `(id >> 24) != 0x01 && (id >> 24) != 0x02` (and any other DBObj-type prefixes), OR `(id >> 16) > 0x00FF` to require a real landblock prefix.
|
||||||
|
|
||||||
|
- The lookup probe's rate-limit namespace separation (Task 7 fix) works correctly — uploaded cells DO appear in the hit set when their lookup probe fires.
|
||||||
|
|
||||||
|
- Cell-room entities have `Position=(0,0,0)` with the cell transform in `MeshRef.PartTransform`. The dispatcher's `aabbVisible` filter passed for them, presumably because `RefreshAabb()` computes a sensible world AABB from the mesh-ref's transform or because the landblock equals `neverCullLandblockId`. Worth a brief audit if there's any reason to believe the cell-room AABB is wrong.
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
# Indoor Cell Rendering — Phase 2 Verification
|
||||||
|
|
||||||
|
**Date:** 2026-05-19
|
||||||
|
**Outcome:** ✅ Floor renders in Holtburg Inn. User visually confirmed.
|
||||||
|
**Predecessor:** [Phase 2 cause report](2026-05-19-indoor-cell-rendering-cause.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Probe re-capture
|
||||||
|
|
||||||
|
After applying the one-line WB fix at [`ObjectMeshManager.cs:1230`](../../references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/ObjectMeshManager.cs:1230):
|
||||||
|
|
||||||
|
| Metric | Pre-fix | Post-fix |
|
||||||
|
|---|---|---|
|
||||||
|
| `[wb-error]` lines | 385 | **0** |
|
||||||
|
| `[indoor-upload] NULL_RESULT` | 55 | **0** |
|
||||||
|
| `[indoor-upload] FAILED` | 0 | 0 |
|
||||||
|
| Total `[indoor-upload] requested` | — | 1157 |
|
||||||
|
| Total `[indoor-upload] completed` | — | **1157** |
|
||||||
|
| Holtburg (`0xA9B4`) requested | 123 | 123 |
|
||||||
|
| Holtburg (`0xA9B4`) completed | 97 | **123** |
|
||||||
|
| Holtburg (`0xA9B4`) missing | 26 | **0** |
|
||||||
|
|
||||||
|
100% success rate on EnvCell uploads. Zero swallowed exceptions. Zero null returns.
|
||||||
|
|
||||||
|
## Visual confirmation
|
||||||
|
|
||||||
|
User walked into Holtburg Inn (and other nearby buildings whose cells were previously failing) and confirmed:
|
||||||
|
|
||||||
|
> "Yes floors are rendering now inside houses."
|
||||||
|
|
||||||
|
The previously-failing cells (`0xA9B40100`, `0xA9B40111`, `0xA9B40112`, `0xA9B40117`, `0xA9B4011B`, etc.) now upload successfully, the dispatcher finds their render data, and the floor / wall / ceiling geometry renders.
|
||||||
|
|
||||||
|
## Regressions checked
|
||||||
|
|
||||||
|
- Outdoor terrain still renders correctly. ✓
|
||||||
|
- Outdoor scenery (trees, rocks, stabs) still render. ✓
|
||||||
|
- NPCs, mobs, world entities still render. ✓
|
||||||
|
- Build clean, no new warnings. ✓
|
||||||
|
- No new test failures. ✓
|
||||||
|
|
||||||
|
## Other observations during the walk
|
||||||
|
|
||||||
|
The user reported **other indoor-related bugs** that are now observable because the floor is rendering. These are all **pre-existing** (not caused by this Phase 2 fix) but were hidden by the missing-floor bug. They are filed as separate issues for follow-up phases:
|
||||||
|
|
||||||
|
1. See-through floor — other buildings visible "below" / "through" the rendered floor (depth/stab-culling).
|
||||||
|
2. Spot lights on walls indoors (point-light positioning).
|
||||||
|
3. Camera on 2nd floor goes very dark (per-cell ambient or trigger).
|
||||||
|
4. Static building stabs don't react to atmospheric lighting changes (shader path).
|
||||||
|
5. Some slope terrain lit incorrectly (terrain normal calculation).
|
||||||
|
6. Collision "blocked by air" indoors (cell BSP misalignment).
|
||||||
|
7. Walking up stairs broken (stair-step physics on EnvCell geometry).
|
||||||
|
8. Pass through walls from outside→in (one-sided wall collision).
|
||||||
|
9. Click selection penetrates walls (WorldPicker raycast not testing cell BSP).
|
||||||
|
|
||||||
|
These nine items are tracked in `docs/ISSUES.md` with proposed phase groupings. None block Phase 2 closure.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
**Phase 2 of the indoor cell rendering fix is complete.** The single-root-cause exception was identified via the diagnostic chain shipped in Phase 1 + Phase 2, and resolved with a one-line guard at the WB call site that prevented blind `TryGet<Setup>` deserialization of GfxObj-typed stab ids.
|
||||||
|
|
||||||
|
Total runtime for Phase 2: ~4 client launches.
|
||||||
|
|
@ -46,7 +46,7 @@ public partial class LightInfo : IDatObjType {
|
||||||
|
|
||||||
- **Practical consequence.** For indoor cells, retail sets directional sun to zero (the cell is windowless) and relies on the baked vertex colours for the ambient "floor". Any `LightInfo` inside the cell is additive.
|
- **Practical consequence.** For indoor cells, retail sets directional sun to zero (the cell is windowless) and relies on the baked vertex colours for the ambient "floor". Any `LightInfo` inside the cell is additive.
|
||||||
- **No cell has a separate ambient RGB field.** The only global ambient is `SkyTimeOfDay.AmbColor` / `AmbBright`, which is only applied outdoors.
|
- **No cell has a separate ambient RGB field.** The only global ambient is `SkyTimeOfDay.AmbColor` / `AmbBright`, which is only applied outdoors.
|
||||||
- **acdream action.** We need a `CellAmbientState` that holds the current `AmbColor * AmbBright` (outdoors, driven by sky dat) or a fixed dark RGB like `(0.10, 0.09, 0.08)` (indoors, approximating the dungeon "deep" tone) — then add active lights on top. See §12 for the C# class.
|
- **acdream action.** We need a `CellAmbientState` that holds the current `AmbColor * AmbBright` (outdoors, driven by sky dat) or **a flat `(0.20, 0.20, 0.20)` neutral** (indoors) — then add active lights on top. The indoor constant is taken **directly from retail**: `CellManager::ChangePosition` (0x004559B0) calls `SmartBox::SetWorldAmbientLight(0.2f, 0xFFFFFFFF)` whenever `CObjCell::seen_outside == 0`. The early-2026 guess at `(0.10, 0.09, 0.08)` was eyeballed; the retail value is both brighter and neutral. See §12 for the C# class.
|
||||||
|
|
||||||
## 4. Torch lights and `WeenieType.LightSource`
|
## 4. Torch lights and `WeenieType.LightSource`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,964 @@
|
||||||
|
# Indoor Cell Rendering Fix — Phase 1 Diagnostics Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Add five toggleable diagnostic probes that pinpoint where the EnvCell rendering chain breaks, so Phase 2's fix can target the actual failure point.
|
||||||
|
|
||||||
|
**Architecture:** Single `RenderingDiagnostics` static class in `AcDream.Core.Rendering` exposes five bool flags + a master toggle (env-var-initialized, runtime-settable). DebugVM mirrors them as live-toggle properties; DebugPanel exposes them as checkboxes. Probe call sites in `WbMeshAdapter` and `WbDrawDispatcher` emit one structured `[indoor-*]` line per event when the corresponding flag is on. The Holtburg Inn floor-missing bug is the test case — log output identifies which of six hypotheses (H1–H6 in the spec) the failure matches.
|
||||||
|
|
||||||
|
**Tech Stack:** C# .NET 10, xUnit (test framework), Silk.NET OpenGL (rendering), Chorizite.OpenGLSDLBackend (WB ObjectMeshManager).
|
||||||
|
|
||||||
|
**Spec:** [`docs/superpowers/specs/2026-05-19-indoor-cell-rendering-fix-design.md`](../specs/2026-05-19-indoor-cell-rendering-fix-design.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
| File | Status | Responsibility |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/AcDream.Core/Rendering/RenderingDiagnostics.cs` | NEW | Static class with five `bool` properties + master toggle. Env-var read at startup; runtime-settable. |
|
||||||
|
| `tests/AcDream.Core.Tests/Rendering/RenderingDiagnosticsTests.cs` | NEW | Verify default values and get/set behavior of the diagnostic flags. |
|
||||||
|
| `src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs` | MODIFY | Add five mirror properties that forward to `RenderingDiagnostics`. |
|
||||||
|
| `src/AcDream.UI.Abstractions/Panels/Debug/DebugPanel.cs` | MODIFY | Add an "Indoor rendering" subsection in `DrawDiagnostics` with six checkboxes. |
|
||||||
|
| `src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs` | MODIFY | Emit `[indoor-upload] requested` on first `IncrementRefCount` for an EnvCell id; emit `[indoor-upload] completed` in `Tick()` when WB's staged drain produces that id's `ObjectMeshData`. |
|
||||||
|
| `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` | MODIFY | Emit `[indoor-walk]` + `[indoor-cull]` in `WalkVisibleEntities` per cell entity; emit `[indoor-lookup]` and `[indoor-xform]` in `DrawAccumulated` per cell-entity render-data lookup + composed transform. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Create `RenderingDiagnostics` static class
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/AcDream.Core/Rendering/RenderingDiagnostics.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the file**
|
||||||
|
|
||||||
|
The class mirrors `AcDream.Core.Physics.PhysicsDiagnostics` exactly — same env-var-init pattern, same get/set, same XML comments style. Five individual probe flags + one `IndoorAll` master. The master setter cascades to all five.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Rendering;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 2026-05-19 — runtime-toggleable diagnostic flags for the indoor cell
|
||||||
|
/// rendering pipeline. Initialized from env vars at process start;
|
||||||
|
/// flippable at runtime via the DebugPanel mirror. Log call sites read
|
||||||
|
/// these statics so a checkbox toggle takes effect on the next frame
|
||||||
|
/// without relaunching.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Mirrors the L.2a <see cref="AcDream.Core.Physics.PhysicsDiagnostics"/>
|
||||||
|
/// pattern. The master <see cref="IndoorAll"/> toggle is the user's
|
||||||
|
/// common case — flipping it cascades to all five probe flags.
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Spec: <c>docs/superpowers/specs/2026-05-19-indoor-cell-rendering-fix-design.md</c>.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public static class RenderingDiagnostics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// When true, <c>WbDrawDispatcher.WalkVisibleEntities</c> emits one
|
||||||
|
/// <c>[indoor-walk]</c> line per visible cell entity per second:
|
||||||
|
/// entity id, world position, parent cell id, landblock visible flag,
|
||||||
|
/// AABB-visible flag, "in visible cells" flag, drew flag.
|
||||||
|
/// Initial state from <c>ACDREAM_PROBE_INDOOR_WALK=1</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static bool ProbeIndoorWalkEnabled { get; set; } =
|
||||||
|
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_WALK") == "1"
|
||||||
|
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When true, <c>WbDrawDispatcher</c> emits one <c>[indoor-lookup]</c>
|
||||||
|
/// line per visible cell entity per second: render-data hit/miss,
|
||||||
|
/// IsSetup flag, SetupParts count, parts-hit / parts-miss tallies.
|
||||||
|
/// Initial state from <c>ACDREAM_PROBE_INDOOR_LOOKUP=1</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static bool ProbeIndoorLookupEnabled { get; set; } =
|
||||||
|
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_LOOKUP") == "1"
|
||||||
|
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When true, <c>WbMeshAdapter</c> emits two lines per EnvCell id:
|
||||||
|
/// <c>[indoor-upload] requested</c> on first IncrementRefCount and
|
||||||
|
/// <c>[indoor-upload] completed</c> when WB's staged drain produces
|
||||||
|
/// its <c>ObjectMeshData</c>. Missing "completed" lines indicate WB
|
||||||
|
/// silently returned null (hypothesis H1).
|
||||||
|
/// Initial state from <c>ACDREAM_PROBE_INDOOR_UPLOAD=1</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static bool ProbeIndoorUploadEnabled { get; set; } =
|
||||||
|
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_UPLOAD") == "1"
|
||||||
|
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When true, <c>WbDrawDispatcher</c> emits one <c>[indoor-xform]</c>
|
||||||
|
/// line per visible cell entity per second: cell-geometry SetupPart's
|
||||||
|
/// composed world matrix translation. Disambiguates transform
|
||||||
|
/// double-apply (hypothesis H5).
|
||||||
|
/// Initial state from <c>ACDREAM_PROBE_INDOOR_XFORM=1</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static bool ProbeIndoorXformEnabled { get; set; } =
|
||||||
|
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_XFORM") == "1"
|
||||||
|
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When true, <c>WbDrawDispatcher.WalkVisibleEntities</c> emits one
|
||||||
|
/// <c>[indoor-cull]</c> line per cell entity that gets culled, with
|
||||||
|
/// the reason (visibleCellIds-miss, frustum, landblock). Disambiguates
|
||||||
|
/// cull bugs (hypothesis H3).
|
||||||
|
/// Initial state from <c>ACDREAM_PROBE_INDOOR_CULL=1</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static bool ProbeIndoorCullEnabled { get; set; } =
|
||||||
|
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_CULL") == "1"
|
||||||
|
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Master toggle. Reading reflects the AND of all five flags
|
||||||
|
/// (true only when every probe is on). Writing cascades — setting
|
||||||
|
/// to <see langword="true"/> turns ALL five flags on; setting to
|
||||||
|
/// <see langword="false"/> turns ALL five off.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IndoorAll
|
||||||
|
{
|
||||||
|
get => ProbeIndoorWalkEnabled
|
||||||
|
&& ProbeIndoorLookupEnabled
|
||||||
|
&& ProbeIndoorUploadEnabled
|
||||||
|
&& ProbeIndoorXformEnabled
|
||||||
|
&& ProbeIndoorCullEnabled;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
ProbeIndoorWalkEnabled = value;
|
||||||
|
ProbeIndoorLookupEnabled = value;
|
||||||
|
ProbeIndoorUploadEnabled = value;
|
||||||
|
ProbeIndoorXformEnabled = value;
|
||||||
|
ProbeIndoorCullEnabled = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper for probe call sites. Returns <see langword="true"/> when
|
||||||
|
/// the low 16 bits of <paramref name="id"/> are ≥ 0x0100 — the AC
|
||||||
|
/// convention for EnvCell (indoor) cells, as opposed to outdoor cells
|
||||||
|
/// in the 8×8 landblock grid (0x0001–0x0040).
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsEnvCellId(ulong id) => (id & 0xFFFFu) >= 0x0100u;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Build**
|
||||||
|
|
||||||
|
Run: `dotnet build src/AcDream.Core/AcDream.Core.csproj -c Debug`
|
||||||
|
Expected: 0 errors, 0 warnings.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/AcDream.Core/Rendering/RenderingDiagnostics.cs
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
feat(diagnostics): RenderingDiagnostics static class for indoor probes
|
||||||
|
|
||||||
|
Five toggleable bool flags + master IndoorAll cascade, mirroring the
|
||||||
|
L.2a PhysicsDiagnostics pattern. Env vars at startup, runtime-settable
|
||||||
|
via DebugPanel mirrors (added next task). Probe call sites and DebugVM
|
||||||
|
wiring follow in subsequent tasks.
|
||||||
|
|
||||||
|
Spec: docs/superpowers/specs/2026-05-19-indoor-cell-rendering-fix-design.md
|
||||||
|
|
||||||
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Unit-test `RenderingDiagnostics`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `tests/AcDream.Core.Tests/Rendering/RenderingDiagnosticsTests.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using AcDream.Core.Rendering;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Tests.Rendering;
|
||||||
|
|
||||||
|
public sealed class RenderingDiagnosticsTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void IndoorAll_True_TurnsAllFlagsOn()
|
||||||
|
{
|
||||||
|
// Reset all flags off first to make the test deterministic
|
||||||
|
// regardless of env-var state on the test runner.
|
||||||
|
RenderingDiagnostics.ProbeIndoorWalkEnabled = false;
|
||||||
|
RenderingDiagnostics.ProbeIndoorLookupEnabled = false;
|
||||||
|
RenderingDiagnostics.ProbeIndoorUploadEnabled = false;
|
||||||
|
RenderingDiagnostics.ProbeIndoorXformEnabled = false;
|
||||||
|
RenderingDiagnostics.ProbeIndoorCullEnabled = false;
|
||||||
|
|
||||||
|
RenderingDiagnostics.IndoorAll = true;
|
||||||
|
|
||||||
|
Assert.True(RenderingDiagnostics.ProbeIndoorWalkEnabled);
|
||||||
|
Assert.True(RenderingDiagnostics.ProbeIndoorLookupEnabled);
|
||||||
|
Assert.True(RenderingDiagnostics.ProbeIndoorUploadEnabled);
|
||||||
|
Assert.True(RenderingDiagnostics.ProbeIndoorXformEnabled);
|
||||||
|
Assert.True(RenderingDiagnostics.ProbeIndoorCullEnabled);
|
||||||
|
Assert.True(RenderingDiagnostics.IndoorAll);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IndoorAll_False_TurnsAllFlagsOff()
|
||||||
|
{
|
||||||
|
RenderingDiagnostics.IndoorAll = true; // start from all-on
|
||||||
|
RenderingDiagnostics.IndoorAll = false;
|
||||||
|
|
||||||
|
Assert.False(RenderingDiagnostics.ProbeIndoorWalkEnabled);
|
||||||
|
Assert.False(RenderingDiagnostics.ProbeIndoorLookupEnabled);
|
||||||
|
Assert.False(RenderingDiagnostics.ProbeIndoorUploadEnabled);
|
||||||
|
Assert.False(RenderingDiagnostics.ProbeIndoorXformEnabled);
|
||||||
|
Assert.False(RenderingDiagnostics.ProbeIndoorCullEnabled);
|
||||||
|
Assert.False(RenderingDiagnostics.IndoorAll);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IndoorAll_OneOff_ReadsAsFalse()
|
||||||
|
{
|
||||||
|
RenderingDiagnostics.IndoorAll = true;
|
||||||
|
RenderingDiagnostics.ProbeIndoorCullEnabled = false; // flip one off
|
||||||
|
Assert.False(RenderingDiagnostics.IndoorAll);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0x00000029ul, false)] // outdoor cell 0x29 in 8x8 grid
|
||||||
|
[InlineData(0xA9B40029ul, false)] // outdoor cell with landblock prefix
|
||||||
|
[InlineData(0x00000100ul, true)] // indoor cell minimum
|
||||||
|
[InlineData(0x00000105ul, true)] // typical Holtburg Inn interior
|
||||||
|
[InlineData(0xA9B40105ul, true)] // indoor with landblock prefix
|
||||||
|
[InlineData(0xA9B401FFul, true)] // indoor near top of range
|
||||||
|
public void IsEnvCellId_DistinguishesOutdoorVsIndoorByLow16Bits(ulong id, bool expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, RenderingDiagnostics.IsEnvCellId(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests — expect failure on first build**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~RenderingDiagnostics" -c Debug --nologo`
|
||||||
|
|
||||||
|
Expected: Build green (Task 1 already implemented the class). All 7 tests pass (1 cascade-on + 1 cascade-off + 1 partial-off + 4 IsEnvCellId rows).
|
||||||
|
|
||||||
|
If any test fails, the implementation in Task 1 has a bug — go back and fix.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add tests/AcDream.Core.Tests/Rendering/RenderingDiagnosticsTests.cs
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
test(diagnostics): RenderingDiagnostics cascade + IsEnvCellId rows
|
||||||
|
|
||||||
|
Covers the master IndoorAll cascade (both directions) and the IsEnvCellId
|
||||||
|
helper's 0x0100 boundary check across outdoor cells, indoor cells, and
|
||||||
|
landblock-prefixed forms.
|
||||||
|
|
||||||
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: Mirror `RenderingDiagnostics` into `DebugVM`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Read DebugVM and find the existing `ProbeBuilding` mirror block**
|
||||||
|
|
||||||
|
Find the `ProbeBuilding` property (around line 270) — that's an existing live-mirror to `PhysicsDiagnostics.ProbeBuildingEnabled`. New mirrors go immediately AFTER `ProbeAutoWalk` (next property in the file), in a new clearly-commented block.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add `using AcDream.Core.Rendering;` at the top of `DebugVM.cs`**
|
||||||
|
|
||||||
|
If the using statement is already present, skip. Otherwise insert alphabetically after `using AcDream.Core.Physics;`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Append the five mirror properties to the file**
|
||||||
|
|
||||||
|
Find the closing brace of the last existing property block (after `ProbeAutoWalk` or the last `Probe*` property). Insert this block before the class's closing brace:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ── Indoor rendering diagnostics (2026-05-19) ───────────────────
|
||||||
|
// Mirror RenderingDiagnostics statics so DebugPanel checkbox toggles
|
||||||
|
// take effect on the next render frame without relaunching.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runtime mirror of <c>RenderingDiagnostics.ProbeIndoorWalkEnabled</c>
|
||||||
|
/// (env var <c>ACDREAM_PROBE_INDOOR_WALK</c>).
|
||||||
|
/// </summary>
|
||||||
|
public bool ProbeIndoorWalk
|
||||||
|
{
|
||||||
|
get => RenderingDiagnostics.ProbeIndoorWalkEnabled;
|
||||||
|
set => RenderingDiagnostics.ProbeIndoorWalkEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runtime mirror of <c>RenderingDiagnostics.ProbeIndoorLookupEnabled</c>
|
||||||
|
/// (env var <c>ACDREAM_PROBE_INDOOR_LOOKUP</c>).
|
||||||
|
/// </summary>
|
||||||
|
public bool ProbeIndoorLookup
|
||||||
|
{
|
||||||
|
get => RenderingDiagnostics.ProbeIndoorLookupEnabled;
|
||||||
|
set => RenderingDiagnostics.ProbeIndoorLookupEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runtime mirror of <c>RenderingDiagnostics.ProbeIndoorUploadEnabled</c>
|
||||||
|
/// (env var <c>ACDREAM_PROBE_INDOOR_UPLOAD</c>).
|
||||||
|
/// </summary>
|
||||||
|
public bool ProbeIndoorUpload
|
||||||
|
{
|
||||||
|
get => RenderingDiagnostics.ProbeIndoorUploadEnabled;
|
||||||
|
set => RenderingDiagnostics.ProbeIndoorUploadEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runtime mirror of <c>RenderingDiagnostics.ProbeIndoorXformEnabled</c>
|
||||||
|
/// (env var <c>ACDREAM_PROBE_INDOOR_XFORM</c>).
|
||||||
|
/// </summary>
|
||||||
|
public bool ProbeIndoorXform
|
||||||
|
{
|
||||||
|
get => RenderingDiagnostics.ProbeIndoorXformEnabled;
|
||||||
|
set => RenderingDiagnostics.ProbeIndoorXformEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runtime mirror of <c>RenderingDiagnostics.ProbeIndoorCullEnabled</c>
|
||||||
|
/// (env var <c>ACDREAM_PROBE_INDOOR_CULL</c>).
|
||||||
|
/// </summary>
|
||||||
|
public bool ProbeIndoorCull
|
||||||
|
{
|
||||||
|
get => RenderingDiagnostics.ProbeIndoorCullEnabled;
|
||||||
|
set => RenderingDiagnostics.ProbeIndoorCullEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runtime mirror of <c>RenderingDiagnostics.IndoorAll</c> — toggles all
|
||||||
|
/// five indoor probes together.
|
||||||
|
/// </summary>
|
||||||
|
public bool ProbeIndoorAll
|
||||||
|
{
|
||||||
|
get => RenderingDiagnostics.IndoorAll;
|
||||||
|
set => RenderingDiagnostics.IndoorAll = value;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build**
|
||||||
|
|
||||||
|
Run: `dotnet build src/AcDream.UI.Abstractions/AcDream.UI.Abstractions.csproj -c Debug`
|
||||||
|
Expected: 0 errors. The `using AcDream.Core.Rendering;` resolves; new properties compile.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
feat(debugvm): mirror RenderingDiagnostics indoor probes
|
||||||
|
|
||||||
|
Live-toggle wrappers for the five indoor-rendering probe flags plus the
|
||||||
|
ProbeIndoorAll master cascade. Pattern matches existing ProbeResolve /
|
||||||
|
ProbeCell / ProbeBuilding / ProbeAutoWalk mirrors so a checkbox flip in
|
||||||
|
the DebugPanel takes effect on the next frame.
|
||||||
|
|
||||||
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: Expose probes in `DebugPanel` Diagnostics group
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/AcDream.UI.Abstractions/Panels/Debug/DebugPanel.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Find `DrawDiagnostics(IPanelRenderer r)` method**
|
||||||
|
|
||||||
|
Open the file. Find the method at approximately line 226. The existing pattern reads probe values into locals at the top of the method, then conditionally re-assigns through checkboxes. The new indoor probes follow the same shape, appended after the last existing probe checkbox.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Read the locals + checkboxes at the bottom of the existing block**
|
||||||
|
|
||||||
|
Find the line that says `if (r.Checkbox("Probe auto-walk (ACDREAM_PROBE_AUTOWALK)", ref probeAutoWalk)) _vm.ProbeAutoWalk = probeAutoWalk;` or similar last existing probe checkbox in `DrawDiagnostics`. New checkboxes go immediately AFTER this line, before the method's closing brace.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Insert the new checkboxes**
|
||||||
|
|
||||||
|
Before the closing brace of `DrawDiagnostics`, insert:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
|
||||||
|
// ── Indoor rendering diagnostics (2026-05-19) ───────────────
|
||||||
|
// Pinpoint where the EnvCell rendering chain breaks for
|
||||||
|
// hypothesis-driven Phase 2 fix. Spec:
|
||||||
|
// docs/superpowers/specs/2026-05-19-indoor-cell-rendering-fix-design.md
|
||||||
|
r.Separator();
|
||||||
|
r.Text("Indoor rendering (envCell):");
|
||||||
|
|
||||||
|
bool probeIndoorAll = _vm.ProbeIndoorAll;
|
||||||
|
bool probeIndoorWalk = _vm.ProbeIndoorWalk;
|
||||||
|
bool probeIndoorLookup = _vm.ProbeIndoorLookup;
|
||||||
|
bool probeIndoorUpload = _vm.ProbeIndoorUpload;
|
||||||
|
bool probeIndoorXform = _vm.ProbeIndoorXform;
|
||||||
|
bool probeIndoorCull = _vm.ProbeIndoorCull;
|
||||||
|
|
||||||
|
if (r.Checkbox("Indoor: ALL (ACDREAM_PROBE_INDOOR_ALL)", ref probeIndoorAll)) _vm.ProbeIndoorAll = probeIndoorAll;
|
||||||
|
if (r.Checkbox("Indoor: walk (ACDREAM_PROBE_INDOOR_WALK)", ref probeIndoorWalk)) _vm.ProbeIndoorWalk = probeIndoorWalk;
|
||||||
|
if (r.Checkbox("Indoor: lookup (ACDREAM_PROBE_INDOOR_LOOKUP)", ref probeIndoorLookup)) _vm.ProbeIndoorLookup = probeIndoorLookup;
|
||||||
|
if (r.Checkbox("Indoor: upload (ACDREAM_PROBE_INDOOR_UPLOAD)", ref probeIndoorUpload)) _vm.ProbeIndoorUpload = probeIndoorUpload;
|
||||||
|
if (r.Checkbox("Indoor: xform (ACDREAM_PROBE_INDOOR_XFORM)", ref probeIndoorXform)) _vm.ProbeIndoorXform = probeIndoorXform;
|
||||||
|
if (r.Checkbox("Indoor: cull (ACDREAM_PROBE_INDOOR_CULL)", ref probeIndoorCull)) _vm.ProbeIndoorCull = probeIndoorCull;
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: `r.Separator()` and `r.Text(string)` are the existing `IPanelRenderer` API methods used elsewhere in the file. If they don't exist, drop those two lines (the checkboxes still work standalone).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build**
|
||||||
|
|
||||||
|
Run: `dotnet build src/AcDream.UI.Abstractions/AcDream.UI.Abstractions.csproj -c Debug`
|
||||||
|
Expected: 0 errors.
|
||||||
|
|
||||||
|
If `r.Separator()` / `r.Text()` aren't on `IPanelRenderer`, the build will fail. Remove those two lines and re-build.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/AcDream.UI.Abstractions/Panels/Debug/DebugPanel.cs
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
feat(debugpanel): "Indoor rendering" probe checkboxes
|
||||||
|
|
||||||
|
Six checkboxes (ALL master + five individual probes) in the existing
|
||||||
|
DrawDiagnostics block. Toggling flips the corresponding
|
||||||
|
RenderingDiagnostics.Probe* flag live via DebugVM forwarding.
|
||||||
|
|
||||||
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: Instrument `WbMeshAdapter` with `[indoor-upload]` probes
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs`
|
||||||
|
|
||||||
|
The upload probe has TWO emission points:
|
||||||
|
1. `IncrementRefCount` — emits `requested` on the first call for an EnvCell id (gated by the existing `_metadataPopulated.Add(id)` first-call check).
|
||||||
|
2. `Tick()` — emits `completed` when WB's `StagedMeshData` drain produces an `ObjectMeshData` whose `ObjectId` is in our pending-EnvCell set.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add the pending-EnvCell tracking field + `using` statement**
|
||||||
|
|
||||||
|
Open `src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs`. Add `using AcDream.Core.Rendering;` near the top with the other `using` statements (after `using AcDream.Core.Meshing;`).
|
||||||
|
|
||||||
|
Find the field declarations near the top of the class (around line 34 — `_metadataPopulated`). Add immediately after:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>
|
||||||
|
/// EnvCell ids we've requested via PrepareMeshDataAsync but not yet
|
||||||
|
/// seen completion for in Tick(). Used by the [indoor-upload] probe
|
||||||
|
/// to log requested + completed pairs. Cleared per completion;
|
||||||
|
/// missing completions after a few seconds indicate WB silently
|
||||||
|
/// returned null (hypothesis H1 in the design spec).
|
||||||
|
/// </summary>
|
||||||
|
private readonly HashSet<ulong> _pendingEnvCellRequests = new();
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Emit `[indoor-upload] requested` in `IncrementRefCount`**
|
||||||
|
|
||||||
|
Find the `IncrementRefCount(ulong id)` method (around line 116). Inside the `if (_metadataPopulated.Add(id))` block, immediately AFTER the `_ = _meshManager.PrepareMeshDataAsync(id, isSetup: false);` line, add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// [indoor-upload] requested probe — only for EnvCell ids.
|
||||||
|
if (RenderingDiagnostics.IsEnvCellId(id) && RenderingDiagnostics.ProbeIndoorUploadEnabled)
|
||||||
|
{
|
||||||
|
_pendingEnvCellRequests.Add(id);
|
||||||
|
Console.WriteLine($"[indoor-upload] requested cellId=0x{id:X8}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Emit `[indoor-upload] completed` in `Tick`**
|
||||||
|
|
||||||
|
Find the `Tick()` method (around line 167). Replace the existing drain loop:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
while (_meshManager!.StagedMeshData.TryDequeue(out var meshData))
|
||||||
|
{
|
||||||
|
_meshManager.UploadMeshData(meshData);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
while (_meshManager!.StagedMeshData.TryDequeue(out var meshData))
|
||||||
|
{
|
||||||
|
// [indoor-upload] completed probe — check BEFORE upload so we
|
||||||
|
// see what WB actually produced (vertex counts, parts) before
|
||||||
|
// any post-upload mutation.
|
||||||
|
bool isPendingEnvCell = RenderingDiagnostics.ProbeIndoorUploadEnabled
|
||||||
|
&& _pendingEnvCellRequests.Remove(meshData.ObjectId);
|
||||||
|
|
||||||
|
var renderData = _meshManager.UploadMeshData(meshData);
|
||||||
|
|
||||||
|
if (isPendingEnvCell)
|
||||||
|
{
|
||||||
|
int parts = meshData.SetupParts?.Count ?? 0;
|
||||||
|
bool hasGeom = meshData.EnvCellGeometry is not null;
|
||||||
|
int cellGeomVerts = meshData.EnvCellGeometry?.Vertices?.Length ?? 0;
|
||||||
|
bool uploadOk = renderData is not null;
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[indoor-upload] completed cellId=0x{meshData.ObjectId:X8} " +
|
||||||
|
$"isSetup={meshData.IsSetup} parts={parts} " +
|
||||||
|
$"hasEnvCellGeom={hasGeom} cellGeomVerts={cellGeomVerts} " +
|
||||||
|
$"uploadOk={uploadOk}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build**
|
||||||
|
|
||||||
|
Run: `dotnet build src/AcDream.App/AcDream.App.csproj -c Debug`
|
||||||
|
Expected: 0 errors.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
feat(wb): [indoor-upload] probe for EnvCell mesh requests + completions
|
||||||
|
|
||||||
|
Instruments WbMeshAdapter at two sites:
|
||||||
|
- IncrementRefCount: on first call for an EnvCell id (low 16 bits ≥
|
||||||
|
0x0100), tag the id in _pendingEnvCellRequests and log
|
||||||
|
[indoor-upload] requested.
|
||||||
|
- Tick: when WB's StagedMeshData drains an ObjectMeshData whose
|
||||||
|
ObjectId matches a pending EnvCell, log [indoor-upload] completed
|
||||||
|
with parts count, EnvCellGeometry vertex count, and upload result.
|
||||||
|
|
||||||
|
Missing "completed" lines after "requested" identify hypothesis H1
|
||||||
|
(WB silently returns null from PrepareEnvCellMeshData).
|
||||||
|
|
||||||
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: Instrument `WbDrawDispatcher` walk + cull probes
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs`
|
||||||
|
|
||||||
|
The `WalkVisibleEntities` method (around line 280) does landblock visibility, per-entity AABB cull, and the `visibleCellIds` filter. Cell entities (entities whose `MeshRefs[0].GfxObjId` low-16-bits ≥ 0x0100) need probes at three decision sites: passed-all, culled-by-aabb, culled-by-visibleCellIds.
|
||||||
|
|
||||||
|
To rate-limit, maintain a per-cellId last-log frame counter as a class-level field.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add the rate-limit tracking field + `using` statement**
|
||||||
|
|
||||||
|
Add `using AcDream.Core.Rendering;` near the top with the other `using` statements (after `using AcDream.Core.Meshing;`).
|
||||||
|
|
||||||
|
Find the class field declarations. Add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>
|
||||||
|
/// Per-cell-entity last-log frame number for rate-limiting the
|
||||||
|
/// [indoor-walk] / [indoor-lookup] / [indoor-xform] / [indoor-cull]
|
||||||
|
/// probes. Defaults to 30 frames at 30Hz = 1 sec.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Dictionary<ulong, int> _lastIndoorProbeFrame = new();
|
||||||
|
private int _indoorProbeFrameCounter;
|
||||||
|
private const int IndoorProbeRateLimitFrames = 30;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true at most once per <see cref="IndoorProbeRateLimitFrames"/>
|
||||||
|
/// frames per cellId. Caller must already have checked that an indoor
|
||||||
|
/// probe flag is enabled.
|
||||||
|
/// </summary>
|
||||||
|
private bool ShouldEmitIndoorProbe(ulong cellId)
|
||||||
|
{
|
||||||
|
if (!_lastIndoorProbeFrame.TryGetValue(cellId, out int last)
|
||||||
|
|| _indoorProbeFrameCounter - last >= IndoorProbeRateLimitFrames)
|
||||||
|
{
|
||||||
|
_lastIndoorProbeFrame[cellId] = _indoorProbeFrameCounter;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Bump the frame counter at the top of `Draw(...)`**
|
||||||
|
|
||||||
|
Find the `Draw` method (around line 339). At its very top, after the existing `_shader.Use();` line, add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
_indoorProbeFrameCounter++;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Replace the per-entity filter block in `WalkVisibleEntities`**
|
||||||
|
|
||||||
|
Find the per-entity loop in `WalkVisibleEntities` (around lines 313-335). The current shape (simplified):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
foreach (var entity in entry.Entities)
|
||||||
|
{
|
||||||
|
if (entity.MeshRefs.Count == 0) continue;
|
||||||
|
|
||||||
|
if (entity.ParentCellId.HasValue && visibleCellIds is not null
|
||||||
|
&& !visibleCellIds.Contains(entity.ParentCellId.Value))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
bool isAnimated = animatedEntityIds?.Contains(entity.Id) == true;
|
||||||
|
if (frustum is not null && !isAnimated && entry.LandblockId != neverCullLandblockId)
|
||||||
|
{
|
||||||
|
if (entity.AabbDirty) entity.RefreshAabb();
|
||||||
|
if (!FrustumCuller.IsAabbVisible(frustum.Value, entity.AabbMin, entity.AabbMax))
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.EntitiesWalked++;
|
||||||
|
for (int i = 0; i < entity.MeshRefs.Count; i++)
|
||||||
|
scratch.Add((entity, i, entry.LandblockId));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the entire `foreach (var entity in entry.Entities)` body with this instrumented version:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
foreach (var entity in entry.Entities)
|
||||||
|
{
|
||||||
|
if (entity.MeshRefs.Count == 0) continue;
|
||||||
|
|
||||||
|
// Detect cell entity for indoor probes — first MeshRef.GfxObjId
|
||||||
|
// is an EnvCell id (low 16 bits ≥ 0x0100). Cheap to compute;
|
||||||
|
// result reused for all four probe checks below.
|
||||||
|
ulong cellProbeId = (ulong)entity.MeshRefs[0].GfxObjId;
|
||||||
|
bool isCellEntity = RenderingDiagnostics.IsEnvCellId(cellProbeId);
|
||||||
|
|
||||||
|
bool cellInVis = !(entity.ParentCellId.HasValue
|
||||||
|
&& visibleCellIds is not null
|
||||||
|
&& !visibleCellIds.Contains(entity.ParentCellId.Value));
|
||||||
|
if (!cellInVis)
|
||||||
|
{
|
||||||
|
if (isCellEntity && RenderingDiagnostics.ProbeIndoorCullEnabled
|
||||||
|
&& ShouldEmitIndoorProbe(cellProbeId))
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[indoor-cull] cellEnt=0x{entity.Id:X8} " +
|
||||||
|
$"reason=visibleCellIds-miss " +
|
||||||
|
$"parentCell=0x{entity.ParentCellId!.Value:X8}");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isAnimated = animatedEntityIds?.Contains(entity.Id) == true;
|
||||||
|
bool aabbVisible = true;
|
||||||
|
if (frustum is not null && !isAnimated && entry.LandblockId != neverCullLandblockId)
|
||||||
|
{
|
||||||
|
if (entity.AabbDirty) entity.RefreshAabb();
|
||||||
|
aabbVisible = FrustumCuller.IsAabbVisible(frustum.Value, entity.AabbMin, entity.AabbMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!aabbVisible)
|
||||||
|
{
|
||||||
|
if (isCellEntity && RenderingDiagnostics.ProbeIndoorCullEnabled
|
||||||
|
&& ShouldEmitIndoorProbe(cellProbeId))
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[indoor-cull] cellEnt=0x{entity.Id:X8} " +
|
||||||
|
$"reason=frustum " +
|
||||||
|
$"aabbMin=({entity.AabbMin.X:F1},{entity.AabbMin.Y:F1},{entity.AabbMin.Z:F1}) " +
|
||||||
|
$"aabbMax=({entity.AabbMax.X:F1},{entity.AabbMax.Y:F1},{entity.AabbMax.Z:F1})");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passed all filters — emit walk probe.
|
||||||
|
if (isCellEntity && RenderingDiagnostics.ProbeIndoorWalkEnabled
|
||||||
|
&& ShouldEmitIndoorProbe(cellProbeId))
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[indoor-walk] cellEnt=0x{entity.Id:X8} " +
|
||||||
|
$"pos=({entity.Position.X:F1},{entity.Position.Y:F1},{entity.Position.Z:F1}) " +
|
||||||
|
$"parentCell=0x{(entity.ParentCellId ?? 0u):X8} " +
|
||||||
|
$"meshRef0=0x{cellProbeId:X8} " +
|
||||||
|
$"meshRefCount={entity.MeshRefs.Count} " +
|
||||||
|
$"landblockVisible=true aabbVisible=true cellInVis=true");
|
||||||
|
}
|
||||||
|
|
||||||
|
result.EntitiesWalked++;
|
||||||
|
for (int i = 0; i < entity.MeshRefs.Count; i++)
|
||||||
|
scratch.Add((entity, i, entry.LandblockId));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Important: `ShouldEmitIndoorProbe(cellProbeId)` is intentionally called only once per probe-decision-site per cellId, so each cellId emits at most ONE line per frame across all four probe sites (whichever fires first).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build**
|
||||||
|
|
||||||
|
Run: `dotnet build src/AcDream.App/AcDream.App.csproj -c Debug`
|
||||||
|
Expected: 0 errors. The `using AcDream.Core.Rendering;` resolves; the new field + helper compile; the instrumented loop builds cleanly.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
feat(dispatcher): [indoor-walk] + [indoor-cull] probes
|
||||||
|
|
||||||
|
Instruments WalkVisibleEntities to identify whether cell entities (first
|
||||||
|
MeshRef.GfxObjId low-16-bits ≥ 0x0100) pass all visibility filters or
|
||||||
|
get culled. Three emission paths:
|
||||||
|
|
||||||
|
- [indoor-cull] reason=visibleCellIds-miss — when the ParentCellId
|
||||||
|
filter rejects the entity.
|
||||||
|
- [indoor-cull] reason=frustum — when AABB frustum cull rejects.
|
||||||
|
- [indoor-walk] — when the entity passes all filters and reaches the
|
||||||
|
draw list.
|
||||||
|
|
||||||
|
Rate-limited to once per cellId per ~1 sec (30 frames at 30 Hz) via
|
||||||
|
_lastIndoorProbeFrame dictionary. Bumped from Draw()'s top.
|
||||||
|
|
||||||
|
Disambiguates hypothesis H3 (cull bug — cell entity dropped before
|
||||||
|
draw).
|
||||||
|
|
||||||
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 7: Instrument `WbDrawDispatcher` lookup + xform probes
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs`
|
||||||
|
|
||||||
|
These probes fire deeper in the per-MeshRef draw loop, where the render-data lookup happens and the `IsSetup` branch composes per-part transforms. The dispatcher's per-MeshRef body is around line 590-627.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Find the per-MeshRef body and the IsSetup branch**
|
||||||
|
|
||||||
|
Open the file. Find the line `var renderData = _meshAdapter.TryGetRenderData(gfxObjId);` (or similar TryGetRenderData lookup inside the per-MeshRef draw loop). The relevant block is the if/else at line 607 (the `IsSetup` branch).
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add the `[indoor-lookup]` probe at the lookup site**
|
||||||
|
|
||||||
|
Find the line that fetches the renderData (likely `var renderData = _meshAdapter.TryGetRenderData(gfxObjId);` or equivalent). Immediately AFTER that lookup and BEFORE the existing null/miss handling at line 595 (`if (diag) _meshesMissing++; continue;`), insert:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// [indoor-lookup] probe — emit once per cell entity per sec.
|
||||||
|
ulong _lookupCellId = (ulong)gfxObjId;
|
||||||
|
if (RenderingDiagnostics.IsEnvCellId(_lookupCellId)
|
||||||
|
&& RenderingDiagnostics.ProbeIndoorLookupEnabled
|
||||||
|
&& ShouldEmitIndoorProbe(_lookupCellId))
|
||||||
|
{
|
||||||
|
bool hit = renderData is not null;
|
||||||
|
bool isSetup = hit && renderData!.IsSetup;
|
||||||
|
int partCount = isSetup ? renderData!.SetupParts.Count : 0;
|
||||||
|
|
||||||
|
int partsHit = 0, partsMiss = 0;
|
||||||
|
if (isSetup)
|
||||||
|
{
|
||||||
|
foreach (var (partId, _) in renderData!.SetupParts)
|
||||||
|
{
|
||||||
|
if (_meshAdapter.TryGetRenderData(partId) is not null) partsHit++;
|
||||||
|
else partsMiss++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasEnvCellGeom = isSetup
|
||||||
|
&& renderData!.SetupParts.Exists(t => (t.GfxObjId & 0x1_0000_0000UL) != 0);
|
||||||
|
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[indoor-lookup] cellId=0x{_lookupCellId:X8} " +
|
||||||
|
$"hit={hit} isSetup={isSetup} partCount={partCount} " +
|
||||||
|
$"hasEnvCellGeom={hasEnvCellGeom} partsHit={partsHit} partsMiss={partsMiss}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: this probe emits BEFORE the null-renderData early-`continue`, so a null lookup still emits `hit=false`. That's intentional — it tells us if the lookup itself failed (hypothesis H1 fallout).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the `[indoor-xform]` probe inside the IsSetup branch**
|
||||||
|
|
||||||
|
Find the `if (renderData.IsSetup && renderData.SetupParts.Count > 0)` block (line 607 in current code). Inside the `foreach (var (partGfxObjId, partTransform) in renderData.SetupParts)` loop, AFTER the `var model = ComposePartWorldMatrix(...)` line, insert:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// [indoor-xform] probe — only for the cell's synthetic
|
||||||
|
// geometry part (bit 32 set, per WB's PrepareEnvCellMeshData
|
||||||
|
// line 1247). One line per cell per sec.
|
||||||
|
if ((partGfxObjId & 0x1_0000_0000UL) != 0
|
||||||
|
&& RenderingDiagnostics.ProbeIndoorXformEnabled
|
||||||
|
&& ShouldEmitIndoorProbe(partGfxObjId))
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[indoor-xform] cellGeomId=0x{partGfxObjId:X16} " +
|
||||||
|
$"entityWorldT=({entityWorld.Translation.X:F2},{entityWorld.Translation.Y:F2},{entityWorld.Translation.Z:F2}) " +
|
||||||
|
$"meshRefT=({meshRef.PartTransform.Translation.X:F2},{meshRef.PartTransform.Translation.Y:F2},{meshRef.PartTransform.Translation.Z:F2}) " +
|
||||||
|
$"partT=({partTransform.Translation.X:F2},{partTransform.Translation.Y:F2},{partTransform.Translation.Z:F2}) " +
|
||||||
|
$"composedT=({model.Translation.X:F2},{model.Translation.Y:F2},{model.Translation.Z:F2})");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build**
|
||||||
|
|
||||||
|
Run: `dotnet build src/AcDream.App/AcDream.App.csproj -c Debug`
|
||||||
|
Expected: 0 errors.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Test (existing tests, sanity)**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj -c Debug --filter "FullyQualifiedName~Rendering" --no-build --nologo`
|
||||||
|
Expected: All Rendering tests (including new RenderingDiagnosticsTests) pass.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
feat(dispatcher): [indoor-lookup] + [indoor-xform] probes
|
||||||
|
|
||||||
|
Instruments the per-MeshRef draw loop in WbDrawDispatcher:
|
||||||
|
|
||||||
|
- [indoor-lookup]: per cell entity, dumps render-data hit/miss,
|
||||||
|
IsSetup, parts count, and a partsHit/partsMiss tally over the
|
||||||
|
SetupParts. Disambiguates hypothesis H2 (WB produces empty
|
||||||
|
ObjectRenderData with zero parts) and H6 (dispatcher fails to
|
||||||
|
traverse Setup).
|
||||||
|
|
||||||
|
- [indoor-xform]: only fires for the cell's synthetic geometry part
|
||||||
|
(the SetupPart whose GfxObjId has bit 32 set, per WB's
|
||||||
|
PrepareEnvCellMeshData cellGeomId convention). Logs the three
|
||||||
|
composed transform translations: entityWorld, meshRef.PartTransform,
|
||||||
|
partTransform, and the final composed matrix translation. Disambiguates
|
||||||
|
hypothesis H5 (transform double-apply — composedT lands at 2 ×
|
||||||
|
cellOrigin).
|
||||||
|
|
||||||
|
Rate-limited via existing _lastIndoorProbeFrame map.
|
||||||
|
|
||||||
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 8: Build + visual capture procedure
|
||||||
|
|
||||||
|
**Files:** none modified. Build verification + runtime data capture.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Full solution build**
|
||||||
|
|
||||||
|
Run: `dotnet build AcDream.slnx -c Debug --nologo 2>&1 | tail -10`
|
||||||
|
Expected: 0 errors, 0 warnings. All projects compile.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run full test suite**
|
||||||
|
|
||||||
|
Run: `dotnet test AcDream.slnx -c Debug --nologo --no-build 2>&1 | tail -15`
|
||||||
|
Expected: New RenderingDiagnostics tests pass. Pre-existing failures in `DispatcherToMovementIntegrationTests`, `BSPStepUpTests`, and `MotionInterpreterTests` (8 total) remain — those are unrelated to this work. No NEW failures.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Gracefully close any prior AcDream.App instance**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$proc = Get-Process -Name AcDream.App -ErrorAction SilentlyContinue
|
||||||
|
if ($proc) {
|
||||||
|
$proc | ForEach-Object { $_.CloseMainWindow() | Out-Null }
|
||||||
|
$proc | ForEach-Object { if (-not $_.WaitForExit(5000)) { Stop-Process -Id $_.Id -Force } }
|
||||||
|
Start-Sleep -Seconds 3
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Launch with all indoor probes enabled**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
|
||||||
|
$env:ACDREAM_LIVE = "1"
|
||||||
|
$env:ACDREAM_TEST_HOST = "127.0.0.1"
|
||||||
|
$env:ACDREAM_TEST_PORT = "9000"
|
||||||
|
$env:ACDREAM_TEST_USER = "testaccount"
|
||||||
|
$env:ACDREAM_TEST_PASS = "testpassword"
|
||||||
|
$env:ACDREAM_DEVTOOLS = "1"
|
||||||
|
$env:ACDREAM_PROBE_INDOOR_ALL = "1"
|
||||||
|
$logPath = "launch.log"
|
||||||
|
Remove-Item $logPath -ErrorAction SilentlyContinue
|
||||||
|
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 | Tee-Object -FilePath $logPath
|
||||||
|
```
|
||||||
|
|
||||||
|
Run this in the background (the launching tool supports `run_in_background: true`).
|
||||||
|
|
||||||
|
- [ ] **Step 5: User reproduces the bug**
|
||||||
|
|
||||||
|
In the running client:
|
||||||
|
- Wait until in-world at Holtburg (8-12 s after launch).
|
||||||
|
- Walk to Holtburg Inn (north of spawn — Fispur's Foodstuffs is visible).
|
||||||
|
- Stand at the doorway. Then step inside. Look at the floor.
|
||||||
|
- Walk around the inn interior.
|
||||||
|
- Close the client window (graceful close — close button, NOT taskkill).
|
||||||
|
|
||||||
|
- [ ] **Step 6: Grep the log for probe output**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -E "\[indoor-" launch.log | head -100
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: a mix of `[indoor-upload] requested`, `[indoor-upload] completed`, `[indoor-walk]`, `[indoor-lookup]`, `[indoor-xform]`, `[indoor-cull]` lines for the Holtburg Inn cell IDs (0xA9B40100-ish range).
|
||||||
|
|
||||||
|
- [ ] **Step 7: Identify which hypothesis matches**
|
||||||
|
|
||||||
|
Compare the captured log against the hypothesis table in the spec (§3 of `2026-05-19-indoor-cell-rendering-fix-design.md`):
|
||||||
|
|
||||||
|
| Hypothesis | Probe pattern in log |
|
||||||
|
|---|---|
|
||||||
|
| H1 — WB silently returns null | `[indoor-upload] requested` lines exist but NO matching `completed` lines for cell ids |
|
||||||
|
| H2 — Empty batches | `[indoor-upload] completed ... cellGeomVerts=0` |
|
||||||
|
| H3 — Cull bug | `[indoor-cull]` lines for cell entity ids with `reason=visibleCellIds-miss` |
|
||||||
|
| H4 — Double-spawn | `[indoor-lookup] partCount=N` where N includes static object IDs that ALSO appear in the entity walk — cross-check against `[indoor-walk]` lines |
|
||||||
|
| H5 — Transform double-apply | `[indoor-xform] composedT` translation roughly 2× the cell's known world origin |
|
||||||
|
| H6 — MeshRefs structure | `[indoor-lookup] hit=true isSetup=true partCount>0 partsHit=0` (all parts missing) |
|
||||||
|
|
||||||
|
- [ ] **Step 8: Document the captured data + matched hypothesis**
|
||||||
|
|
||||||
|
Create a short investigation note at `docs/research/2026-05-19-indoor-cell-rendering-probe-capture.md` summarizing:
|
||||||
|
- The exact `[indoor-*]` log lines captured (or a representative subset).
|
||||||
|
- The matched hypothesis number.
|
||||||
|
- A one-line proposed fix sketch.
|
||||||
|
|
||||||
|
This file will be referenced by Phase 2's spec.
|
||||||
|
|
||||||
|
- [ ] **Step 9: Commit the capture note**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add docs/research/2026-05-19-indoor-cell-rendering-probe-capture.md
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
docs(research): Phase 1 indoor probe capture — identifies hypothesis HX
|
||||||
|
|
||||||
|
[Replace HX with the matched hypothesis number, and summarize the
|
||||||
|
captured log evidence in 1-2 sentences.]
|
||||||
|
|
||||||
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 10: Hand off to Phase 2 design**
|
||||||
|
|
||||||
|
The captured data is now the input to Phase 2's design. Either:
|
||||||
|
- Amend `docs/superpowers/specs/2026-05-19-indoor-cell-rendering-fix-design.md` with a Phase 2 section, OR
|
||||||
|
- Write a new spec `docs/superpowers/specs/YYYY-MM-DD-indoor-cell-rendering-phase2-fix-design.md` targeting the identified hypothesis.
|
||||||
|
|
||||||
|
The plan for Phase 2 follows the standard brainstorming → writing-plans → executing-plans flow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] All eight tasks complete + committed.
|
||||||
|
- [ ] `dotnet build` clean. `dotnet test` clean (no new failures; pre-existing 8 physics/input failures unchanged).
|
||||||
|
- [ ] Probe captured at Holtburg Inn produces enough log evidence to identify which of H1-H6 is the root cause.
|
||||||
|
- [ ] Capture note written and committed.
|
||||||
|
- [ ] Phase 2 design follow-up spec started.
|
||||||
|
|
@ -0,0 +1,550 @@
|
||||||
|
# Indoor Cell Rendering Fix — Phase 2 Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Surface WB's silent `PrepareEnvCellMeshData` failures via an exception-capturing continuation in `WbMeshAdapter`, identify the root cause for the 26 missing-completion cells, then implement the targeted fix that lands the indoor floor rendering.
|
||||||
|
|
||||||
|
**Architecture:** `WbMeshAdapter.IncrementRefCount` captures the `Task<ObjectMeshData?>` returned by WB's `PrepareMeshDataAsync` and attaches a `ContinueWith` that logs faulted-task exceptions + clean-null results for EnvCell IDs only. Gated by the existing `ProbeIndoorUploadEnabled` flag — zero cost when off. Component 3 (the actual fix) is data-driven: the captured exception type + message determines the surgical code change.
|
||||||
|
|
||||||
|
**Tech Stack:** C# .NET 10, Silk.NET OpenGL, WorldBuilder's `Chorizite.OpenGLSDLBackend.Lib.ObjectMeshManager`. xUnit for any unit tests.
|
||||||
|
|
||||||
|
**Spec:** [`docs/superpowers/specs/2026-05-19-phase2-indoor-cell-rendering-fix-design.md`](../specs/2026-05-19-phase2-indoor-cell-rendering-fix-design.md).
|
||||||
|
**Phase 1 capture:** [`docs/research/2026-05-19-indoor-cell-rendering-probe-capture.md`](../../research/2026-05-19-indoor-cell-rendering-probe-capture.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
| File | Status | Responsibility |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs` | MODIFY (Task 1) | Capture `prepTask` from `PrepareMeshDataAsync`. Attach a `ContinueWith` for EnvCell IDs that emits `[indoor-upload] FAILED` on faulted tasks and `[indoor-upload] NULL_RESULT` on clean-null returns. |
|
||||||
|
| `launch.log` (and the user's walk-through) | NEW (Task 2) | Captured probe output. Drives Component 3's fix shape. Not committed. |
|
||||||
|
| `docs/research/2026-05-19-indoor-cell-rendering-cause.md` | NEW (Task 3) | One-page report documenting the captured exception type(s) + the chosen fix shape. Becomes Phase 2's "design closure" doc. |
|
||||||
|
| TBD-by-data (Component 3) | MODIFY (Task 4) | Fix shape depends on captured cause. Likely candidates: `WbMeshAdapter.PopulateMetadata`, `CellMesh.Build`, a guard at the dat-access call site, or a small WB fork patch. |
|
||||||
|
| `docs/research/2026-05-19-indoor-cell-rendering-verification.md` | NEW (Task 5) | Post-fix verification record: previously-missing cells now emit `[indoor-upload] completed`, visual confirmation. |
|
||||||
|
| `docs/plans/2026-04-11-roadmap.md` | MODIFY (Task 6) | Roadmap update: Phase 2 shipped, link to spec + research notes. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Add exception-surfacing continuation in `WbMeshAdapter`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add `using System.Linq;` and `using System.Threading.Tasks;` if missing**
|
||||||
|
|
||||||
|
Open `src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs`. Verify both `using System.Linq;` and `using System.Threading.Tasks;` are present at the top. Add them if not.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Replace the fire-and-forget call with a captured task + continuation**
|
||||||
|
|
||||||
|
Find the `IncrementRefCount` method (around line 116). The current block looks like:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public void IncrementRefCount(ulong id)
|
||||||
|
{
|
||||||
|
if (_isUninitialized || _meshManager is null) return;
|
||||||
|
_meshManager.IncrementRefCount(id);
|
||||||
|
|
||||||
|
if (_metadataPopulated.Add(id))
|
||||||
|
{
|
||||||
|
PopulateMetadata(id);
|
||||||
|
|
||||||
|
// WB's IncrementRefCount alone only bumps a usage counter; it does
|
||||||
|
// NOT trigger mesh loading. We must explicitly call PrepareMeshDataAsync
|
||||||
|
// so the background workers actually decode the GfxObj. The result
|
||||||
|
// auto-enqueues into _stagedMeshData (ObjectMeshManager line 510),
|
||||||
|
// which Tick() drains onto the GPU. Until that completes,
|
||||||
|
// TryGetRenderData(id) returns null and the dispatcher silently
|
||||||
|
// skips the entity — standard streaming flicker.
|
||||||
|
//
|
||||||
|
// isSetup: false — acdream's MeshRefs already carry expanded
|
||||||
|
// per-part GfxObj ids (0x01XXXXXX). WB's Setup-expansion path is
|
||||||
|
// unused.
|
||||||
|
_ = _meshManager.PrepareMeshDataAsync(id, isSetup: false);
|
||||||
|
|
||||||
|
// [indoor-upload] requested probe — only for EnvCell ids.
|
||||||
|
if (RenderingDiagnostics.IsEnvCellId(id) && RenderingDiagnostics.ProbeIndoorUploadEnabled)
|
||||||
|
{
|
||||||
|
_pendingEnvCellRequests.Add(id);
|
||||||
|
Console.WriteLine($"[indoor-upload] requested cellId=0x{id:X8}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the `_metadataPopulated.Add(id)` block body with this exact content (note: the `_ = _meshManager.PrepareMeshDataAsync(...)` line becomes `var prepTask = ...` — capture the task instead of discarding it):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
PopulateMetadata(id);
|
||||||
|
|
||||||
|
// WB's IncrementRefCount alone only bumps a usage counter; it does
|
||||||
|
// NOT trigger mesh loading. We must explicitly call PrepareMeshDataAsync
|
||||||
|
// so the background workers actually decode the GfxObj. The result
|
||||||
|
// auto-enqueues into _stagedMeshData (ObjectMeshManager line 510),
|
||||||
|
// which Tick() drains onto the GPU. Until that completes,
|
||||||
|
// TryGetRenderData(id) returns null and the dispatcher silently
|
||||||
|
// skips the entity — standard streaming flicker.
|
||||||
|
//
|
||||||
|
// isSetup: false — acdream's MeshRefs already carry expanded
|
||||||
|
// per-part GfxObj ids (0x01XXXXXX). WB's Setup-expansion path is
|
||||||
|
// unused.
|
||||||
|
var prepTask = _meshManager.PrepareMeshDataAsync(id, isSetup: false);
|
||||||
|
|
||||||
|
// [indoor-upload] requested probe — only for EnvCell ids.
|
||||||
|
if (RenderingDiagnostics.IsEnvCellId(id) && RenderingDiagnostics.ProbeIndoorUploadEnabled)
|
||||||
|
{
|
||||||
|
_pendingEnvCellRequests.Add(id);
|
||||||
|
Console.WriteLine($"[indoor-upload] requested cellId=0x{id:X8}");
|
||||||
|
|
||||||
|
// Phase 2 — surface what WB's catch block silently swallows.
|
||||||
|
// ObjectMeshManager.PrepareMeshData has try/catch at line 589
|
||||||
|
// that calls _logger.LogError on exceptions and returns null.
|
||||||
|
// We construct ObjectMeshManager with NullLogger so the log
|
||||||
|
// goes nowhere. This continuation captures the same data
|
||||||
|
// (scoped to EnvCell ids only). Runs on ThreadPool; non-
|
||||||
|
// blocking. Zero cost when probe is off.
|
||||||
|
ulong cellId = id;
|
||||||
|
_ = prepTask.ContinueWith(t =>
|
||||||
|
{
|
||||||
|
if (t.IsFaulted && t.Exception is not null)
|
||||||
|
{
|
||||||
|
var ex = t.Exception.InnerException ?? t.Exception;
|
||||||
|
var stack = (ex.StackTrace ?? "").Split('\n')
|
||||||
|
.Take(3).Select(s => s.Trim()).Where(s => s.Length > 0);
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[indoor-upload] FAILED cellId=0x{cellId:X8} " +
|
||||||
|
$"exception={ex.GetType().Name}: {ex.Message} " +
|
||||||
|
$"stack=[{string.Join(" | ", stack)}]");
|
||||||
|
}
|
||||||
|
else if (t.IsCompletedSuccessfully && t.Result is null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[indoor-upload] NULL_RESULT cellId=0x{cellId:X8}");
|
||||||
|
}
|
||||||
|
}, TaskScheduler.Default);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Build**
|
||||||
|
|
||||||
|
Run: `dotnet build src/AcDream.App/AcDream.App.csproj -c Debug`
|
||||||
|
Expected: 0 errors, 0 warnings (any new warnings about discarded tasks are fixed by the `_ = prepTask.ContinueWith(...)` assignment).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run tests (sanity)**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~Rendering" -c Debug --nologo --no-build`
|
||||||
|
Expected: All 130 Rendering tests still pass (the change doesn't touch any tested code path — `WbMeshAdapter.IncrementRefCount` isn't covered by unit tests).
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
feat(wb): surface WB-swallowed exceptions for EnvCell upload failures
|
||||||
|
|
||||||
|
Phase 1 confirmed 26/123 Holtburg cells silently fail in WB's
|
||||||
|
PrepareEnvCellMeshData / PrepareMeshData. WB's catch block at
|
||||||
|
ObjectMeshManager.cs:589 calls _logger.LogError(ex, ...) — but we
|
||||||
|
construct ObjectMeshManager with NullLogger, so the log is dropped.
|
||||||
|
|
||||||
|
Capture the Task from PrepareMeshDataAsync (previously fire-and-forget)
|
||||||
|
and attach a ContinueWith that, for EnvCell ids only when the probe
|
||||||
|
is on, logs:
|
||||||
|
|
||||||
|
[indoor-upload] FAILED cellId=0x... exception=<Type>: <Message>
|
||||||
|
stack=[<top 3 frames>]
|
||||||
|
[indoor-upload] NULL_RESULT cellId=0x...
|
||||||
|
|
||||||
|
Runs on ThreadPool — non-blocking. Zero cost when ProbeIndoorUploadEnabled
|
||||||
|
is off. AggregateException is unwrapped to InnerException for readability.
|
||||||
|
Stack truncated to top 3 frames.
|
||||||
|
|
||||||
|
Next: capture procedure, identify cause, target the fix.
|
||||||
|
|
||||||
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Capture procedure — run client, identify cause
|
||||||
|
|
||||||
|
This task is operator-driven, not subagent-driven. The user (not a subagent) walks the client. Subagent role is limited to launching + analyzing the log.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- New: `launch.log` (transient — not committed)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Full solution build (sanity)**
|
||||||
|
|
||||||
|
Run: `dotnet build AcDream.slnx -c Debug --nologo 2>&1 | tail -10`
|
||||||
|
Expected: `Build succeeded. 0 Error(s)`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Gracefully close any prior `AcDream.App` instance**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$proc = Get-Process -Name AcDream.App -ErrorAction SilentlyContinue
|
||||||
|
if ($proc) {
|
||||||
|
$proc | ForEach-Object { $_.CloseMainWindow() | Out-Null }
|
||||||
|
$proc | ForEach-Object { if (-not $_.WaitForExit(5000)) { Stop-Process -Id $_.Id -Force } }
|
||||||
|
Start-Sleep -Seconds 3
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Launch with `ACDREAM_PROBE_INDOOR_UPLOAD=1`**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
|
||||||
|
$env:ACDREAM_LIVE = "1"
|
||||||
|
$env:ACDREAM_TEST_HOST = "127.0.0.1"
|
||||||
|
$env:ACDREAM_TEST_PORT = "9000"
|
||||||
|
$env:ACDREAM_TEST_USER = "testaccount"
|
||||||
|
$env:ACDREAM_TEST_PASS = "testpassword"
|
||||||
|
$env:ACDREAM_DEVTOOLS = "1"
|
||||||
|
$env:ACDREAM_PROBE_INDOOR_UPLOAD = "1"
|
||||||
|
$logPath = "launch.log"
|
||||||
|
Remove-Item $logPath -ErrorAction SilentlyContinue
|
||||||
|
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 | Tee-Object -FilePath $logPath
|
||||||
|
```
|
||||||
|
|
||||||
|
Run in background via `run_in_background: true`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: User walks Holtburg**
|
||||||
|
|
||||||
|
User waits for the client to reach in-world (~8-12 s), then:
|
||||||
|
- Walks into Holtburg Inn (where the floor was missing in Phase 1).
|
||||||
|
- Walks into 2-3 other nearby buildings to capture varied failure causes.
|
||||||
|
- Closes the client window with the close button (graceful — NOT taskkill).
|
||||||
|
|
||||||
|
- [ ] **Step 5: Analyze the log**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$lines = Get-Content launch.log | Where-Object { $_ -match '\[indoor-upload\] (FAILED|NULL_RESULT)' }
|
||||||
|
Write-Host "Total failure lines: $($lines.Count)"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "=== Distinct exception types (FAILED) ==="
|
||||||
|
$lines | Where-Object { $_ -match '\[indoor-upload\] FAILED' } |
|
||||||
|
ForEach-Object { if ($_ -match 'exception=(\w+):') { $matches[1] } } |
|
||||||
|
Group-Object | Sort-Object Count -Descending | Format-Table -AutoSize
|
||||||
|
|
||||||
|
Write-Host "=== Distinct NULL_RESULT count ==="
|
||||||
|
($lines | Where-Object { $_ -match 'NULL_RESULT' }).Count
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "=== Sample FAILED lines ==="
|
||||||
|
$lines | Where-Object { $_ -match '\[indoor-upload\] FAILED' } | Select-Object -First 10
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "=== Sample NULL_RESULT lines ==="
|
||||||
|
$lines | Where-Object { $_ -match '\[indoor-upload\] NULL_RESULT' } | Select-Object -First 5
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify the previously-failing cells (from Phase 1: `0xA9B40100`, `0xA9B40111`, `0xA9B40112`, etc.) now appear in either FAILED or NULL_RESULT.
|
||||||
|
|
||||||
|
If they DON'T appear:
|
||||||
|
- Confirm the probe flag is on (check `$env:ACDREAM_PROBE_INDOOR_UPLOAD` reads `"1"`).
|
||||||
|
- Confirm the user actually walked into the failing cells.
|
||||||
|
- Possible BUG: the continuation isn't firing — check Task 1's edits for typos.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: Write the cause report
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `docs/research/2026-05-19-indoor-cell-rendering-cause.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the report based on Task 2's output**
|
||||||
|
|
||||||
|
Create the file with this structure (replace bracketed sections with captured data):
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Indoor Cell Rendering — Phase 2 Cause Report
|
||||||
|
|
||||||
|
**Date:** 2026-05-19
|
||||||
|
**Predecessor:** Phase 1 capture confirmed H1 (silent failure in WB).
|
||||||
|
**Capture method:** Task 1's `ContinueWith` surfaced WB's swallowed exceptions for EnvCell IDs.
|
||||||
|
|
||||||
|
## Cause(s)
|
||||||
|
|
||||||
|
[Replace this section with the captured findings. Example shape:]
|
||||||
|
|
||||||
|
Two distinct failure modes captured at Holtburg:
|
||||||
|
|
||||||
|
1. **`KeyNotFoundException` — N cells affected** — Exception thrown from `PrepareCellStructMeshData` line XXX when trying to look up surface `0x08001234`. Affected cells: `0xA9B40100`, `0xA9B40111`, ...
|
||||||
|
|
||||||
|
2. **`NULL_RESULT` — M cells affected** — WB's `ResolveId` returned empty for `EnvironmentId 0xD000XXXX`, causing `PrepareEnvCellMeshData` to skip the cellGeometry branch and produce an empty result. Affected cells: ...
|
||||||
|
|
||||||
|
[OR if only one cause is observed:]
|
||||||
|
|
||||||
|
Single failure mode: [exception type] thrown in [location] for all 26 cells. Root cause: [analysis].
|
||||||
|
|
||||||
|
## Sample log lines
|
||||||
|
|
||||||
|
```
|
||||||
|
[paste 5-10 actual captured FAILED / NULL_RESULT lines here]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Proposed fix
|
||||||
|
|
||||||
|
[Concrete code change for each distinct cause. For example:]
|
||||||
|
|
||||||
|
- For `KeyNotFoundException` on surface lookup: add a null-guard in `WbMeshAdapter.PopulateMetadata` AND skip the failing surface in our acdream-side processing.
|
||||||
|
- For `NULL_RESULT` from missing `EnvironmentId`: log + skip with a sentinel render-data so the dispatcher gracefully draws nothing instead of failing silently.
|
||||||
|
|
||||||
|
Each fix is a single-file change. Task 4 of this plan implements them.
|
||||||
|
|
||||||
|
## Verification approach
|
||||||
|
|
||||||
|
After Task 4's fix:
|
||||||
|
- Re-launch with the same probe flag.
|
||||||
|
- Confirm previously-failing cells now emit `[indoor-upload] completed` lines.
|
||||||
|
- Visual: floor renders in Holtburg Inn.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add docs/research/2026-05-19-indoor-cell-rendering-cause.md
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
docs(research): Phase 2 cause report — <one-line summary of finding>
|
||||||
|
|
||||||
|
Captured at Holtburg with the ContinueWith-based exception surfacer
|
||||||
|
from Task 1. <Describe finding in 2-3 sentences: which exception types
|
||||||
|
fired, for how many cells, the root cause.>
|
||||||
|
|
||||||
|
Fix shape decided: <one sentence>. Implemented in next commit.
|
||||||
|
|
||||||
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: Apply the targeted fix
|
||||||
|
|
||||||
|
**The fix shape is unknown until Task 2 captures.** This task's code is data-driven. The plan below lists the four most likely fix shapes; the implementer picks the matching one(s) and implements them.
|
||||||
|
|
||||||
|
### 4a — If the cause is `KeyNotFoundException` / missing dat record
|
||||||
|
|
||||||
|
Most likely path: WB's `PrepareCellStructMeshData` calls `_dats.Portal.TryGet<Surface>(surfaceId, out var surface)`, gets `false`, then crashes when later code assumes non-null.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: TBD by exception stack — likely a WB fork patch OR a guard at our acdream call site.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Open the throwing file based on the exception stack trace**
|
||||||
|
|
||||||
|
The probe line will show:
|
||||||
|
```
|
||||||
|
stack=[at PrepareCellStructMeshData in ObjectMeshManager.cs:line | at PrepareEnvCellMeshData in ObjectMeshManager.cs:line | ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
Open that file at that line. Confirm the missing-dat-record assumption.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Patch shape (WB fork, if in WB)**
|
||||||
|
|
||||||
|
In `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/ObjectMeshManager.cs`, add a null-guard at the throwing line:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Pre-Phase-2: WB assumed every surface in envCell.Surfaces was
|
||||||
|
// resolvable. Some Holtburg cells reference surfaces that aren't in the
|
||||||
|
// loaded portal dat, causing a NullRef in the throwing line below.
|
||||||
|
// Guard: skip the surface if it doesn't resolve.
|
||||||
|
if (!_dats.Portal.TryGet<Surface>(surfaceId, out var surface))
|
||||||
|
{
|
||||||
|
continue; // or: surface = _fallbackSurface; whichever fits
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
(Exact code depends on the stack. The implementer reads the actual throwing line and adapts.)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Build, capture, verify**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build src/AcDream.App/AcDream.App.csproj -c Debug
|
||||||
|
```
|
||||||
|
|
||||||
|
Then re-run Task 2's launch + capture. Confirm:
|
||||||
|
- Previously-failing cells now have `[indoor-upload] completed` lines.
|
||||||
|
- No new `[indoor-upload] FAILED` lines for those cells.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/ObjectMeshManager.cs
|
||||||
|
# OR whatever file was patched
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
fix(wb): null-guard for missing surface in PrepareCellStructMeshData
|
||||||
|
|
||||||
|
Phase 2 capture found <N> Holtburg cells silently failing with
|
||||||
|
<ExceptionType> thrown at <file>:<line> when WB tried to look up
|
||||||
|
surface 0x... that isn't resolvable in the loaded portal dat.
|
||||||
|
|
||||||
|
Patch: <one-sentence description of the guard>.
|
||||||
|
|
||||||
|
Visual-verified: floor now renders in Holtburg Inn.
|
||||||
|
|
||||||
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4b — If the cause is `NULL_RESULT` (clean null return from WB)
|
||||||
|
|
||||||
|
WB's `PrepareMeshData` returns null without throwing. Examined paths in the WB source:
|
||||||
|
- Line 568: `_dats.Portal.TryGet<Environment>(envId, ...)` fails → returns null.
|
||||||
|
- Line 583: `type == DBObjType.Unknown` (ResolveId didn't classify the record) → returns null.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: probably WbMeshAdapter to detect and log, then either accept the cell as "no geometry" gracefully OR investigate the dat issue.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Read which path triggered**
|
||||||
|
|
||||||
|
Look at the `NULL_RESULT` cells' EnvironmentId values. If the EnvironmentId looks corrupt or out of range, the dat is the issue. If it looks valid, WB's `ResolveId` is broken for that record.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add a guard at our acdream call site OR patch WB**
|
||||||
|
|
||||||
|
Depending on the finding:
|
||||||
|
- **If dat is genuinely missing data**: skip the cell with a warning. Don't try to render its mesh. Log once via memory.
|
||||||
|
- **If WB's ResolveId mis-classifies**: patch WB or work around by pre-checking with our own `_dats.Get<EnvCell>(envCellId)` before calling `IncrementRefCount`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Build, capture, verify, commit** (same pattern as 4a Step 3-4).
|
||||||
|
|
||||||
|
### 4c — If the cause is a `NullReferenceException` in our code path
|
||||||
|
|
||||||
|
Less likely but possible — if `PopulateMetadata` or `CellMesh.Build` crashes when invoked from a worker thread.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: the specific acdream file the stack trace points to.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Read the throwing line**
|
||||||
|
- [ ] **Step 2: Add the appropriate null-guard**
|
||||||
|
- [ ] **Step 3: Build, capture, verify, commit.**
|
||||||
|
|
||||||
|
### 4d — If the cause is something else entirely
|
||||||
|
|
||||||
|
If the captured exception type doesn't match 4a-4c, **STOP and re-design**. The fix shape needs the implementer's judgment + possibly a fresh brainstorm session. Don't paper over the cause with a generic try/catch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: Verification + visualization
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `docs/research/2026-05-19-indoor-cell-rendering-verification.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Re-launch with the probe and re-walk Holtburg**
|
||||||
|
|
||||||
|
Same as Task 2 Steps 2-4, but expectation flipped: `[indoor-upload] FAILED` / `NULL_RESULT` lines for previously-failing cells should NOT appear; `[indoor-upload] completed` lines should appear instead.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Visual verification by user**
|
||||||
|
|
||||||
|
User walks into Holtburg Inn AND the other buildings whose cells were previously missing. Expected: floors visible, no missing geometry.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Write the verification report**
|
||||||
|
|
||||||
|
Create the file documenting:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Indoor Cell Rendering — Phase 2 Verification
|
||||||
|
|
||||||
|
**Date:** 2026-05-19
|
||||||
|
**Outcome:** Floor renders in Holtburg Inn.
|
||||||
|
|
||||||
|
## Probe re-capture
|
||||||
|
|
||||||
|
After Task 4's fix:
|
||||||
|
- Previously-failing cells: <list — e.g. `0xA9B40100`, `0xA9B40111`, ...>
|
||||||
|
- Now emit `[indoor-upload] completed cellId=0x... isSetup=True hasEnvCellGeom=True cellGeomVerts=<N> uploadOk=True`
|
||||||
|
- No new `[indoor-upload] FAILED` or `NULL_RESULT` lines for these cells.
|
||||||
|
|
||||||
|
## Visual confirmation
|
||||||
|
|
||||||
|
User walked into:
|
||||||
|
- Holtburg Inn — floor visible. ✓
|
||||||
|
- <other buildings tested> — floor visible. ✓
|
||||||
|
|
||||||
|
## Regressions checked
|
||||||
|
|
||||||
|
- Outdoor terrain still renders correctly. ✓
|
||||||
|
- NPCs, mobs, scenery still render. ✓
|
||||||
|
- No new build warnings, no new test failures.
|
||||||
|
|
||||||
|
## Closes
|
||||||
|
|
||||||
|
This concludes Phase 2 of the indoor cell rendering fix.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add docs/research/2026-05-19-indoor-cell-rendering-verification.md
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
docs(research): Phase 2 verification — floor renders in Holtburg Inn
|
||||||
|
|
||||||
|
Post-fix re-capture confirms previously-failing cells now emit
|
||||||
|
[indoor-upload] completed. Visual verification by user confirms
|
||||||
|
floors visible in Holtburg Inn and <other tested buildings>.
|
||||||
|
|
||||||
|
Phase 2 complete.
|
||||||
|
|
||||||
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: Roadmap update
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `docs/plans/2026-04-11-roadmap.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Read the roadmap's "shipped" section**
|
||||||
|
|
||||||
|
Open `docs/plans/2026-04-11-roadmap.md`. Find the section listing recently-shipped phases (likely near the top, in a "shipped" table or chronological list).
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add an entry for Phase 2 indoor cell rendering fix**
|
||||||
|
|
||||||
|
Add an entry matching the existing pattern of shipped-row entries. Example shape:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
| <next row number> | 2026-05-19 | Indoor cell rendering — Phase 1 (diagnostics) + Phase 2 (fix) | Surfaced + fixed WB's silent failure for 26/123 Holtburg cells. Spec at [phase 1](../superpowers/specs/2026-05-19-indoor-cell-rendering-fix-design.md) + [phase 2](../superpowers/specs/2026-05-19-phase2-indoor-cell-rendering-fix-design.md). Cause: <one-line>. Fix: <one-line>. Visual-verified at Holtburg Inn. |
|
||||||
|
```
|
||||||
|
|
||||||
|
(Read the actual existing row format and match it.)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add docs/plans/2026-04-11-roadmap.md
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
docs(roadmap): Phase 2 indoor cell rendering fix shipped
|
||||||
|
|
||||||
|
Phase 1 diagnostics + Phase 2 fix landed today. Indoor floor rendering
|
||||||
|
restored for Holtburg cells previously missing due to WB silent
|
||||||
|
failure. Spec, plan, and verification documents committed.
|
||||||
|
|
||||||
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] Task 1 commits: `WbMeshAdapter.IncrementRefCount` attaches the continuation. `dotnet build` clean.
|
||||||
|
- [ ] Task 2 capture: `[indoor-upload] FAILED` or `NULL_RESULT` lines fire for previously-failing cells. Distinct cause(s) identified.
|
||||||
|
- [ ] Task 3 cause report: documented in `docs/research/2026-05-19-indoor-cell-rendering-cause.md`.
|
||||||
|
- [ ] Task 4 fix: applied + committed. Build clean. Tests clean (no new failures; pre-existing 8 physics/input failures unchanged).
|
||||||
|
- [ ] Task 5 verification: post-fix probe re-capture confirms `[indoor-upload] completed` for previously-failing cells. User visually confirms floor renders in Holtburg Inn.
|
||||||
|
- [ ] Task 6 roadmap update: shipped row added.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Subagent dispatch notes
|
||||||
|
|
||||||
|
- **Task 1** is mechanical (well-specified code edit) — dispatch to Sonnet.
|
||||||
|
- **Task 2** is operator-driven — the controller (parent) drives the launch + capture, not a subagent. The user MUST walk the client.
|
||||||
|
- **Task 3** is analytical (interpret captured data) — controller writes inline, or dispatch a Sonnet subagent with the captured log as context.
|
||||||
|
- **Task 4** is judgment-intensive (fix shape depends on data) — controller writes inline. If complex, a fresh brainstorm may be needed.
|
||||||
|
- **Task 5** is similar to Task 2 (user-driven walk + analysis).
|
||||||
|
- **Task 6** is mechanical — dispatch to Sonnet OR controller writes inline.
|
||||||
|
|
@ -0,0 +1,248 @@
|
||||||
|
# Indoor Cell Rendering Fix — Design
|
||||||
|
|
||||||
|
**Status:** Brainstormed 2026-05-19. Pivoted mid-brainstorm — see §1.5 for
|
||||||
|
the corrected root-cause analysis. Awaiting user review.
|
||||||
|
**Scope:** Diagnose + fix the actual break in the EnvCell rendering chain.
|
||||||
|
**Out of scope this phase:** Cell collision symptoms (no wall collision
|
||||||
|
exiting, weird open-air collisions). Filed as a follow-up phase pending
|
||||||
|
user repro data.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Symptom
|
||||||
|
|
||||||
|
Walking into Holtburg Inn: the exterior building stab renders (walls visible
|
||||||
|
from inside), but the interior cell's own room mesh — floor, inner walls,
|
||||||
|
ceiling — is missing. The user can walk through the empty interior with no
|
||||||
|
floor visible underfoot.
|
||||||
|
|
||||||
|
## 1.5 What the root cause is NOT (corrected mid-brainstorm)
|
||||||
|
|
||||||
|
Initial hypothesis: N.5 retirement (commit
|
||||||
|
[`dcae2b6`](../../../#) 2026-05-08) deleted the legacy cell-mesh drain path
|
||||||
|
with the assumption "WB handles EnvCell geometry through its own pipeline,"
|
||||||
|
and that assumption was wrong.
|
||||||
|
|
||||||
|
**Closer inspection during brainstorm proved that assumption is correct.**
|
||||||
|
WB's `ObjectMeshManager.PrepareMeshData(id, isSetup)` at
|
||||||
|
[`ObjectMeshManager.cs:557`](../../../references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/ObjectMeshManager.cs:557)
|
||||||
|
dispatches on the **dat record type** (not on the `isSetup` parameter).
|
||||||
|
When the id resolves to a `DBObjType.EnvCell`, it routes to
|
||||||
|
`PrepareEnvCellMeshData(id, envCell, ct)` at line 1186, which produces an
|
||||||
|
`ObjectMeshData` with `IsSetup=true`, `SetupParts` = [static objects +
|
||||||
|
cellGeometry], `EnvCellGeometry` = the floor/wall/ceiling room mesh.
|
||||||
|
|
||||||
|
The dispatcher correctly handles `IsSetup=true` at
|
||||||
|
[`WbDrawDispatcher.cs:607-621`](../../../src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs:607) —
|
||||||
|
it iterates `SetupParts`, looks up each part's render data, composes
|
||||||
|
transforms, and draws each.
|
||||||
|
|
||||||
|
`DefaultDatReaderWriter` loads region cell dats during construction
|
||||||
|
([`DefaultDatReaderWriter.cs:66-89`](../../../references/WorldBuilder/WorldBuilder.Shared/Services/DefaultDatReaderWriter.cs:66))
|
||||||
|
so `ResolveId(envCellId)` will find the cell record.
|
||||||
|
|
||||||
|
`LandblockSpawnAdapter.OnLandblockLoaded` iterates `landblock.Entities` and
|
||||||
|
calls `_adapter.IncrementRefCount(meshRef.GfxObjId)` for each
|
||||||
|
([`LandblockSpawnAdapter.cs:75-80`](../../../src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs:75)).
|
||||||
|
Cell entities have `ServerGuid == 0` (atlas-tier), so they pass the filter
|
||||||
|
at line 73. Their `MeshRef.GfxObjId == envCellId` reaches `IncrementRefCount`.
|
||||||
|
|
||||||
|
**The chain looks structurally intact.** Floors SHOULD render today. They
|
||||||
|
don't. Therefore the failure is subtler than "we never invoke the load."
|
||||||
|
|
||||||
|
## 2. Real failure point — to be determined by diagnostics
|
||||||
|
|
||||||
|
Six untested hypotheses, in rough order of probability:
|
||||||
|
|
||||||
|
1. **WB silently fails to build the `ObjectMeshData`.** `PrepareEnvCellMeshData`
|
||||||
|
returns null when the Environment dat can't resolve, or when
|
||||||
|
`PrepareCellStructMeshData` returns null (texture issues, surface
|
||||||
|
resolution failure). WB doesn't log; the failure is invisible.
|
||||||
|
|
||||||
|
2. **`SetupParts.cellGeomId` is uploaded but its texture batches are empty.**
|
||||||
|
`UploadGfxObjMeshData` returning null at line 675 is treated as a
|
||||||
|
non-fatal substitution — the render data has no draw batches, dispatcher
|
||||||
|
silently draws nothing.
|
||||||
|
|
||||||
|
3. **Cell entity is culled before reaching the dispatcher.** `visibleCellIds`
|
||||||
|
filter at `WbDrawDispatcher.cs:317-319` rejects entities whose
|
||||||
|
`ParentCellId` isn't in the visible set. If the cell entity's
|
||||||
|
`ParentCellId == envCellId` but the visibility BFS doesn't include the
|
||||||
|
player's current cell (because `FindCameraCell` returns null when camera
|
||||||
|
is in third-person above the building, etc.), the cell entity is
|
||||||
|
skipped.
|
||||||
|
|
||||||
|
4. **Double-spawn conflict between WB's static-object SetupParts and
|
||||||
|
acdream's per-stab entity hydration.** `PrepareEnvCellMeshData` iterates
|
||||||
|
`envCell.StaticObjects` and adds each as a SetupPart. Meanwhile acdream
|
||||||
|
already hydrates the same static objects as separate `WorldEntity`
|
||||||
|
instances at [`GameWindow.cs:5390-5439`](../../../src/AcDream.App/Rendering/GameWindow.cs:5390).
|
||||||
|
WB might be holding extra ref counts on those GfxObj IDs that block
|
||||||
|
eviction or cause cache thrash. Unlikely to cause "missing floor" but
|
||||||
|
worth ruling out.
|
||||||
|
|
||||||
|
5. **Transform composition bug.** `ComposePartWorldMatrix(entityWorld,
|
||||||
|
meshRef.PartTransform, partTransform)` — if our cell entity's
|
||||||
|
`meshRef.PartTransform == cellTransform` and WB's `partTransform`
|
||||||
|
already bakes the cell origin, the floor lands at `2 × cellOrigin`,
|
||||||
|
far below or beside the actual cell. The user would describe this
|
||||||
|
as "missing" because the floor is now outside the visible frustum.
|
||||||
|
|
||||||
|
6. **The cell entity's `MeshRefs` only has one entry, but WB expects
|
||||||
|
multiple.** The dispatcher iterates `entity.MeshRefs`, but each MeshRef
|
||||||
|
gets its own `TryGetRenderData(meshRef.GfxObjId)` call. For cell
|
||||||
|
entities we have `MeshRefs = { MeshRef(envCellId, cellTransform) }`.
|
||||||
|
When the lookup returns an `IsSetup=true` render data, the dispatcher
|
||||||
|
does the right thing (line 607-621) — iterates SetupParts. So this
|
||||||
|
should work; ruling out.
|
||||||
|
|
||||||
|
## 3. Solution
|
||||||
|
|
||||||
|
### Phase 1 — Diagnostics (this phase's work)
|
||||||
|
|
||||||
|
Five probes, each individually toggleable via env-var + DebugPanel
|
||||||
|
checkbox. The probes live in a new
|
||||||
|
`AcDream.Core.Rendering.RenderingDiagnostics` static class (mirroring
|
||||||
|
the `AcDream.Core.Physics.PhysicsDiagnostics` pattern shipped in L.2a)
|
||||||
|
so they're discoverable from one place and survive across the
|
||||||
|
Core / App seam.
|
||||||
|
|
||||||
|
Each probe is **rate-limited**: by default, one line per (envCellId,
|
||||||
|
frame-modulo-30) — i.e., once per second per cell at 30 Hz — to avoid
|
||||||
|
log spam. When `ACDREAM_PROBE_INDOOR_VERBOSE=1` is also set, the
|
||||||
|
rate-limit drops and every frame logs.
|
||||||
|
|
||||||
|
| Env var (and DebugPanel mirror) | Probe | Code location | Line format |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `ACDREAM_PROBE_INDOOR_WALK` | Cell-entity dispatcher walk | `WbDrawDispatcher.WalkVisibleEntities` (rate-limited per cellId) | `[indoor-walk] cellEnt=0xID pos=(x,y,z) parentCell=0xID landblockVisible=B aabbVisible=B cellInVis=B drawn=B` |
|
||||||
|
| `ACDREAM_PROBE_INDOOR_LOOKUP` | Render-data lookup for cell entities | `WbDrawDispatcher.DrawAccumulated` per cell entity | `[indoor-lookup] cellId=0xID hit=B isSetup=B partCount=N hasEnvCellGeom=B partsHit=N partsMiss=N` |
|
||||||
|
| `ACDREAM_PROBE_INDOOR_UPLOAD` | WB upload result for envCellId | `WbMeshAdapter.IncrementRefCount` (on first call per id) + a callback hooked into `_meshManager.Tick()` for completion | `[indoor-upload] cellId=0xID requested=true completed=B partsCount=N cellGeomVerts=N error="..."` |
|
||||||
|
| `ACDREAM_PROBE_INDOOR_XFORM` | Composed world transform for cell-geometry SetupPart | `WbDrawDispatcher` inside the `IsSetup` branch at line 607-621, for partGfxObjId matching `(envCellId | 0x1_00000000UL)` | `[indoor-xform] cellId=0xID cellOrigin=(x,y,z) entityWorld=(...) partTransform=(...) composed=(x,y,z y-axis,z-axis) detExpected≈1 detActual=F` |
|
||||||
|
| `ACDREAM_PROBE_INDOOR_CULL` | Visibility / cull decision per cell entity | `WbDrawDispatcher.WalkVisibleEntities` (the two filter sites at lines 304-305 and 317-319) | `[indoor-cull] cellEnt=0xID reason="visibleCellIds-miss" or "frustum" or "served" details="..."` |
|
||||||
|
|
||||||
|
The five probes can be enabled independently or together. The user's
|
||||||
|
common case is `ACDREAM_PROBE_INDOOR_ALL=1` which sets all five at
|
||||||
|
once.
|
||||||
|
|
||||||
|
#### Implementation outline
|
||||||
|
|
||||||
|
1. **New file** `src/AcDream.Core/Rendering/RenderingDiagnostics.cs` —
|
||||||
|
five static `bool` properties, each backed by an env-var read at
|
||||||
|
startup, each runtime-settable from the DebugPanel.
|
||||||
|
2. **DebugPanel section** — new "Indoor rendering diagnostics" block
|
||||||
|
in the existing DebugPanel "Diagnostics" group, with one checkbox
|
||||||
|
per probe + a master "all" toggle.
|
||||||
|
3. **WbDrawDispatcher edits** — instrument the walk and the IsSetup
|
||||||
|
draw branch. The walk probe needs to know whether the entity passed
|
||||||
|
the cell-visibility filter; the cull probe needs the same data.
|
||||||
|
Cleanest: emit BOTH lines in one place when either probe is on.
|
||||||
|
4. **WbMeshAdapter edits** — `IncrementRefCount` logs an `[indoor-upload]
|
||||||
|
requested=true` line when the id is recognized as an EnvCell
|
||||||
|
(high-bit check `(id & 0xFFFF) >= 0x0100`). On Tick(), when a
|
||||||
|
completion drains for an envCellId, log the result line with the
|
||||||
|
actual ObjectMeshData/ObjectRenderData fields.
|
||||||
|
5. **No GameWindow changes** beyond passing the diagnostics class
|
||||||
|
into the dispatcher (if not already accessible).
|
||||||
|
|
||||||
|
#### Capture procedure
|
||||||
|
|
||||||
|
1. Build with the probe instrumentation. `dotnet build` green.
|
||||||
|
2. Launch with `ACDREAM_PROBE_INDOOR_ALL=1`. Walk to Holtburg Inn,
|
||||||
|
stand at the doorway, then step inside, then walk around the room.
|
||||||
|
3. Stop the client, grep `launch.log` for `[indoor-*]` lines.
|
||||||
|
4. The captured log identifies WHICH hypothesis matches:
|
||||||
|
- **H1 (null upload)** → `[indoor-upload] completed=false`
|
||||||
|
- **H2 (empty batches)** → `[indoor-upload] cellGeomVerts=0`
|
||||||
|
- **H3 (cull bug)** → `[indoor-cull] reason="visibleCellIds-miss"`
|
||||||
|
- **H4 (double-spawn)** → `[indoor-lookup] partCount` includes
|
||||||
|
static-object IDs that ALSO appear in `landblock.Entities`
|
||||||
|
- **H5 (transform double-apply)** → `[indoor-xform] composed`
|
||||||
|
world position lands at `2 × cellOrigin` instead of `cellOrigin`
|
||||||
|
- **H6 (MeshRefs structure)** → ruled out; probe data would still
|
||||||
|
surface it as `hit=true isSetup=true partCount=N` followed by
|
||||||
|
all `partsHit=0`
|
||||||
|
|
||||||
|
### Phase 2 — Fix the specific break (next phase)
|
||||||
|
|
||||||
|
Once the probe identifies the failure point, implement the surgical
|
||||||
|
fix. Likely shapes per hypothesis:
|
||||||
|
|
||||||
|
| Hypothesis | Fix shape |
|
||||||
|
|---|---|
|
||||||
|
| H1 — WB returns null | Add WB logging or pre-check the dat resolution path in WbMeshAdapter |
|
||||||
|
| H2 — Empty batches | Investigate WB texture pipeline; possibly a missing texture in the cell's surface list |
|
||||||
|
| H3 — Cull bug | Fix `ParentCellId` assignment OR loosen the visibility filter for cell entities |
|
||||||
|
| H4 — Double-spawn | Stop WB from spawning static-object parts in EnvCell setups (filter them in PrepareEnvCellMeshData, or skip acdream's per-stab hydration when WB handles the cell) |
|
||||||
|
| H5 — Transform double-apply | Replace `MeshRef.PartTransform = cellTransform` with `entity.Position+Rotation = cellPosition` |
|
||||||
|
| H6 — MeshRefs structure | Already ruled out in §2 |
|
||||||
|
|
||||||
|
Phase 2's actual code change is small and well-targeted once Phase 1
|
||||||
|
gives us a definite answer.
|
||||||
|
|
||||||
|
## 4. Why NOT build a separate cell renderer
|
||||||
|
|
||||||
|
The original brainstorm proposed adapting `_pendingCellMeshes` data into
|
||||||
|
WB via a new `UploadCellMesh` adapter method. **That solution is wrong** —
|
||||||
|
it would duplicate work WB already does, fragment the rendering pipeline,
|
||||||
|
and bypass WB's existing GPU memory management. Worse, it would hide
|
||||||
|
whatever the actual bug is, not fix it.
|
||||||
|
|
||||||
|
## 5. Edge cases
|
||||||
|
|
||||||
|
| Scenario | Behavior |
|
||||||
|
|---|---|
|
||||||
|
| Visible during diagnostic capture | Probe is heavy (per-frame, per-entity). Bounded by short walk; runtime-toggle off when done. |
|
||||||
|
| Probe spam in production | Default OFF, mirrored to DebugPanel. Same pattern as L.2a `ACDREAM_PROBE_RESOLVE` / `ACDREAM_PROBE_CELL`. |
|
||||||
|
| Concurrent landblock stream | Probe records per frame across all loaded cells — useful for cross-cell comparison ("does cell X load but cell Y not?"). |
|
||||||
|
|
||||||
|
## 6. Testing strategy
|
||||||
|
|
||||||
|
**Unit tests:** none in Phase 1. The probe is diagnostic, not behavioral.
|
||||||
|
|
||||||
|
**Visual verification (user-driven, end-to-end):**
|
||||||
|
|
||||||
|
- Add probe, launch client, walk into Holtburg Inn.
|
||||||
|
- Read probe output to identify which hypothesis matches.
|
||||||
|
- Brief Phase 2 in a new design (or amend this one) once the failure
|
||||||
|
point is known.
|
||||||
|
|
||||||
|
**Phase 2 unit tests:** depend on the fix shape. If H5 (transform
|
||||||
|
double-apply), tests verify the world matrix composition. If H3 (cull
|
||||||
|
bug), tests verify visibility BFS for indoor entities.
|
||||||
|
|
||||||
|
## 7. What's NOT in this phase
|
||||||
|
|
||||||
|
- Cell collision symptoms — investigated separately.
|
||||||
|
- Particle/fire emitter integration — already shipped.
|
||||||
|
- Light registration — already shipped.
|
||||||
|
- Stab-leak-through-walls — deferred.
|
||||||
|
|
||||||
|
## 8. Acceptance criteria
|
||||||
|
|
||||||
|
**Phase 1 (this phase):**
|
||||||
|
|
||||||
|
- [ ] `AcDream.Core.Rendering.RenderingDiagnostics` static class created
|
||||||
|
with five `bool` properties + master `IndoorAll` toggle, each backed
|
||||||
|
by an env-var read at startup and runtime-settable.
|
||||||
|
- [ ] DebugPanel "Diagnostics" group has a new "Indoor rendering"
|
||||||
|
subsection with six checkboxes (five probes + master).
|
||||||
|
- [ ] `WbDrawDispatcher` emits `[indoor-walk]`, `[indoor-lookup]`,
|
||||||
|
`[indoor-xform]`, `[indoor-cull]` lines when the respective probe
|
||||||
|
is on. Rate-limited to ~1/sec per cell unless verbose mode active.
|
||||||
|
- [ ] `WbMeshAdapter` emits `[indoor-upload]` lines for EnvCell IDs:
|
||||||
|
one `requested` line on first `IncrementRefCount`, one `completed`
|
||||||
|
line when WB's Tick drains the result (success or failure).
|
||||||
|
- [ ] `dotnet build` clean. `dotnet test` clean (the diagnostics-only
|
||||||
|
change should not affect any test).
|
||||||
|
- [ ] Probe captured at Holtburg Inn confirms which hypothesis matches.
|
||||||
|
Capture procedure documented in §3 above.
|
||||||
|
- [ ] Phase 2 design (amended spec or new spec) documents the surgical
|
||||||
|
fix matched to the identified hypothesis.
|
||||||
|
|
||||||
|
**Phase 2 (next phase, driven by Phase 1 output):**
|
||||||
|
|
||||||
|
- [ ] `dotnet build` clean, `dotnet test` clean.
|
||||||
|
- [ ] Visual verification: walking into Holtburg Inn renders interior
|
||||||
|
floor + walls correctly.
|
||||||
|
- [ ] Roadmap updated.
|
||||||
|
- [ ] Probes left in place for future regressions but defaulted off.
|
||||||
|
|
@ -0,0 +1,189 @@
|
||||||
|
# Indoor Cell Rendering Fix — Phase 2 Design
|
||||||
|
|
||||||
|
**Status:** Brainstormed 2026-05-19. Awaiting user review.
|
||||||
|
**Scope:** Surface the silent failure in WB's `PrepareEnvCellMeshData` for 26/123 Holtburg cells, then implement the targeted fix.
|
||||||
|
**Predecessor:** Phase 1 (`docs/superpowers/specs/2026-05-19-indoor-cell-rendering-fix-design.md`) shipped the five `[indoor-*]` probes that confirmed hypothesis H1.
|
||||||
|
**Capture evidence:** `docs/research/2026-05-19-indoor-cell-rendering-probe-capture.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. What we know
|
||||||
|
|
||||||
|
Phase 1's `ACDREAM_PROBE_INDOOR_ALL=1` capture at Holtburg `0xA9B4` proved:
|
||||||
|
|
||||||
|
- 123 EnvCells requested via `WbMeshAdapter.IncrementRefCount` → only **97 complete**.
|
||||||
|
- **26 cells** silently fail. They get `[indoor-upload] requested` but never `[indoor-upload] completed`.
|
||||||
|
- The dispatcher then tries to draw them, `TryGetRenderData` returns null, draw is silently skipped → user sees **missing floor**.
|
||||||
|
- The first interior cell `0xA9B40100` (likely the inn entry or another major building anchor) is among the 26.
|
||||||
|
|
||||||
|
The smoking gun is in WB's [`ObjectMeshManager.PrepareMeshData`](../../../references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/ObjectMeshManager.cs):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
catch (Exception ex) {
|
||||||
|
_logger.LogError(ex, "Error preparing mesh data for 0x{Id:X16}", id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
WB logs the exception via its injected `_logger`. But [`WbMeshAdapter.cs:71`](../../../src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs:71) constructs `ObjectMeshManager` with `NullLogger<ObjectMeshManager>.Instance` — so the log goes to `/dev/null`. The exception type and message are lost.
|
||||||
|
|
||||||
|
## 2. Solution — three components
|
||||||
|
|
||||||
|
### Component 1 — Exception-surfacing wrap
|
||||||
|
|
||||||
|
Capture the `Task<ObjectMeshData?>` returned by `_meshManager.PrepareMeshDataAsync(id, isSetup: false)` and attach a continuation that, for EnvCell IDs only, logs the failure cause.
|
||||||
|
|
||||||
|
Three logged outcomes:
|
||||||
|
|
||||||
|
- **Task faulted** → `[indoor-upload] FAILED cellId=0x... exception=<TypeName>: <Message> stack=[<top 3 frames>]`. Unwrap `AggregateException.InnerException` for cleaner output.
|
||||||
|
- **Task succeeded with null result** → `[indoor-upload] NULL_RESULT cellId=0x...`. WB's deliberate null-return path (e.g., `ResolveId` returned empty, type was `Unknown`).
|
||||||
|
- **Task succeeded with non-null result** → no extra log. The existing `Tick()` drain already emits `[indoor-upload] completed`.
|
||||||
|
|
||||||
|
The continuation:
|
||||||
|
- Runs on `TaskScheduler.Default` (`ThreadPool`) so it doesn't block the render thread.
|
||||||
|
- Only attached for EnvCell IDs (gated by `RenderingDiagnostics.IsEnvCellId(id)`) when `ProbeIndoorUploadEnabled` is true — zero cost when off.
|
||||||
|
- Captures `cellId` (a `ulong` value) only; no instance closure leakage.
|
||||||
|
- Truncates stack trace to top 3 frames.
|
||||||
|
|
||||||
|
Concrete code shape:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
if (_metadataPopulated.Add(id))
|
||||||
|
{
|
||||||
|
PopulateMetadata(id);
|
||||||
|
var prepTask = _meshManager.PrepareMeshDataAsync(id, isSetup: false);
|
||||||
|
|
||||||
|
if (RenderingDiagnostics.IsEnvCellId(id) && RenderingDiagnostics.ProbeIndoorUploadEnabled)
|
||||||
|
{
|
||||||
|
_pendingEnvCellRequests.Add(id);
|
||||||
|
Console.WriteLine($"[indoor-upload] requested cellId=0x{id:X8}");
|
||||||
|
|
||||||
|
ulong cellId = id;
|
||||||
|
_ = prepTask.ContinueWith(t =>
|
||||||
|
{
|
||||||
|
if (t.IsFaulted && t.Exception is not null)
|
||||||
|
{
|
||||||
|
var ex = t.Exception.InnerException ?? t.Exception;
|
||||||
|
var stack = (ex.StackTrace ?? "").Split('\n')
|
||||||
|
.Take(3).Select(s => s.Trim()).Where(s => s.Length > 0);
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[indoor-upload] FAILED cellId=0x{cellId:X8} " +
|
||||||
|
$"exception={ex.GetType().Name}: {ex.Message} " +
|
||||||
|
$"stack=[{string.Join(" | ", stack)}]");
|
||||||
|
}
|
||||||
|
else if (t.IsCompletedSuccessfully && t.Result is null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[indoor-upload] NULL_RESULT cellId=0x{cellId:X8}");
|
||||||
|
}
|
||||||
|
}, TaskScheduler.Default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`using System.Linq;` and `using System.Threading.Tasks;` may need adding (likely already present).
|
||||||
|
|
||||||
|
### Component 2 — Capture procedure
|
||||||
|
|
||||||
|
Standard launch:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:ACDREAM_PROBE_INDOOR_UPLOAD = "1"
|
||||||
|
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 | Tee-Object -FilePath launch.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Walk into Holtburg Inn, walk into nearby buildings whose cells were on the missing-26 list (`0xA9B40100`, `0xA9B40111`, etc.). Close gracefully.
|
||||||
|
|
||||||
|
Analyze:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-Content launch.log |
|
||||||
|
Where-Object { $_ -match '\[indoor-upload\] (FAILED|NULL_RESULT)' } |
|
||||||
|
Select-Object -Unique
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output: a per-cell list of distinct exception types or null-return signals. Most cells likely share 1–3 root causes.
|
||||||
|
|
||||||
|
### Component 3 — Targeted fix (shape unknown until Component 2 captures)
|
||||||
|
|
||||||
|
Once Component 2 reveals the exception type + message, the fix is one localized code change. Likely shapes:
|
||||||
|
|
||||||
|
| Captured cause | Fix shape |
|
||||||
|
|---|---|
|
||||||
|
| Texture decode `Exception` (e.g. `KeyNotFoundException` on surface ID) | Guard at `WbMeshAdapter.PopulateMetadata` or pre-validate surfaces; possibly patch WB fork. |
|
||||||
|
| `KeyNotFoundException` for missing `Environment` / `CellStruct` | Log + skip cell with a sentinel render-data; report which dat is stale. |
|
||||||
|
| `NullReferenceException` in `PrepareCellStructMeshData` | Add null guard at the specific call site. |
|
||||||
|
| WB internal logic bug | Fork patch to WB. |
|
||||||
|
| `NULL_RESULT` (ResolveId returned empty / type was Unknown) | Investigate dat file integrity; possibly user needs a dat update. |
|
||||||
|
|
||||||
|
The fix is one or two code edits, lands as a single commit, and is followed by a re-launch verifying:
|
||||||
|
- `[indoor-upload] FAILED` / `NULL_RESULT` lines disappear for the previously-failing cells.
|
||||||
|
- `[indoor-upload] completed` appears for those cells.
|
||||||
|
- Visual verification: floor renders in Holtburg Inn.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Edge cases
|
||||||
|
|
||||||
|
| Scenario | Behavior |
|
||||||
|
|---|---|
|
||||||
|
| Probe toggled off mid-session | Continuation still emits if attached at request time. Acceptable — capturing the cause once matters more than honoring runtime toggle. |
|
||||||
|
| Continuation fires after adapter disposed | Harmless console write on dying process. No memory leak; closure captures only the `ulong` cellId. |
|
||||||
|
| Same cell requested twice | `_metadataPopulated.Add(id)` guards; continuation attaches exactly once. Re-streaming after Remove+Add keeps the sticky set. First failure is what we want. |
|
||||||
|
| Cancellation | `t.IsCanceled` is neither `IsFaulted` nor `IsCompletedSuccessfully`. Continuation silently skips. Acceptable — cancellation isn't a failure cause. |
|
||||||
|
| `Task.Result` on faulted task | Re-throws AggregateException. Our gate `else if (t.IsCompletedSuccessfully && t.Result is null)` ensures we never read Result without a clean success state. |
|
||||||
|
| WB's `_logger.LogError` for the same exception | WbMeshAdapter passes `NullLogger` — WB's log goes nowhere. Our continuation is what surfaces it. Discussed below. |
|
||||||
|
|
||||||
|
**Why not just inject a real logger into `ObjectMeshManager`?** Could replace `NullLogger<ObjectMeshManager>.Instance` with a real logger that writes to `Console.WriteLine`. Tradeoff:
|
||||||
|
|
||||||
|
- Real logger: simpler, leverages WB's existing `_logger.LogError` call → catches GfxObj + Setup + EnvCell failures.
|
||||||
|
- Our continuation: scoped to EnvCell IDs only → less noise.
|
||||||
|
|
||||||
|
Going with the continuation approach because:
|
||||||
|
1. The probe flag is already in place.
|
||||||
|
2. Phase 2 is targeted at EnvCells.
|
||||||
|
3. Real-logger would emit thousands of GfxObj/Setup log lines during landblock streaming, drowning the EnvCell signal.
|
||||||
|
|
||||||
|
We can revisit if a future debugging session calls for broader visibility.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Testing strategy
|
||||||
|
|
||||||
|
### Unit tests
|
||||||
|
|
||||||
|
None for Component 1 — the continuation is straight wiring around an async API; the logic is "if faulted, log; if null result, log." Testing requires either mocking `Task<ObjectMeshData?>` (low value) or running a real WB instance (impractical in unit tests).
|
||||||
|
|
||||||
|
### Visual verification (end-to-end)
|
||||||
|
|
||||||
|
Component 2's capture procedure is the verification mechanism:
|
||||||
|
|
||||||
|
1. Build green.
|
||||||
|
2. Launch with probe flag on, walk into Holtburg.
|
||||||
|
3. Confirm `[indoor-upload] FAILED` or `NULL_RESULT` lines appear for ~26 cells.
|
||||||
|
4. Apply Component 3's fix.
|
||||||
|
5. Re-launch, re-walk Holtburg.
|
||||||
|
6. **Acceptance:** previously-failing cells now produce `[indoor-upload] completed` lines AND the user can see the floor in Holtburg Inn.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. What's NOT in this phase
|
||||||
|
|
||||||
|
- Tightening `IsEnvCellId` false-positives (flagged in Phase 1 capture note). Deferred — doesn't block Phase 2 since the upload probe gates on the correct path.
|
||||||
|
- Cell collision symptoms (no wall collision when exiting, weird open-air collisions). Separate investigation phase.
|
||||||
|
- Stab-leak-through-walls (Phase 1 Task 3). Deferred.
|
||||||
|
- Broader WB logger injection for GfxObj/Setup failures. Open if we ever want broader diagnostic visibility.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Acceptance criteria
|
||||||
|
|
||||||
|
- [ ] `WbMeshAdapter.IncrementRefCount` captures the prep task and attaches a continuation for EnvCell IDs.
|
||||||
|
- [ ] Continuation logs `[indoor-upload] FAILED cellId=0x... exception=<TypeName>: <Message> stack=[...]` for faulted tasks.
|
||||||
|
- [ ] Continuation logs `[indoor-upload] NULL_RESULT cellId=0x...` for clean-null returns.
|
||||||
|
- [ ] `dotnet build` clean. `dotnet test` clean (no new failures; pre-existing 8 physics/input failures unchanged).
|
||||||
|
- [ ] Capture launched, FAILED/NULL_RESULT lines appear for the previously-missing cells, distinct causes identified.
|
||||||
|
- [ ] Component 3 fix designed and implemented for each distinct cause.
|
||||||
|
- [ ] Re-capture confirms `[indoor-upload] completed` appears for cells previously missing.
|
||||||
|
- [ ] Visual verification: floor renders in Holtburg Inn.
|
||||||
|
- [ ] Roadmap updated with Phase 2 shipped.
|
||||||
|
- [ ] Commit messages cite the captured exception types + the fix rationale.
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 167788be6fce65f5ebe79eef07a0b7d28bd7aa81
|
Subproject commit 34460c44d7fb921afa50ee30288a53236f50f451
|
||||||
|
|
@ -330,6 +330,21 @@ public sealed class CellVisibility
|
||||||
local.Z <= cell.LocalBoundsMax.Z + PointInCellEpsilon;
|
local.Z <= cell.LocalBoundsMax.Z + PointInCellEpsilon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Brute-force scan of every loaded cell to test whether
|
||||||
|
/// <paramref name="worldPoint"/> is inside any of them. Does not touch
|
||||||
|
/// the camera cache (<see cref="_lastCameraCell"/>), so this is safe
|
||||||
|
/// to call alongside <see cref="ComputeVisibility"/> in the same frame
|
||||||
|
/// for a different position (e.g. player position when the camera is
|
||||||
|
/// in third-person chase mode).
|
||||||
|
/// </summary>
|
||||||
|
public bool IsInsideAnyCell(Vector3 worldPoint)
|
||||||
|
{
|
||||||
|
foreach (var cell in _cellLookup.Values)
|
||||||
|
if (PointInCell(worldPoint, cell)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// GetVisibleCells (BFS)
|
// GetVisibleCells (BFS)
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -6848,6 +6848,19 @@ public sealed class GameWindow : IDisposable
|
||||||
var visibility = _cellVisibility.ComputeVisibility(camPos);
|
var visibility = _cellVisibility.ComputeVisibility(camPos);
|
||||||
bool cameraInsideCell = visibility?.CameraCell is not null;
|
bool cameraInsideCell = visibility?.CameraCell is not null;
|
||||||
|
|
||||||
|
// Lighting decisions (sun zeroed, indoor ambient applied) must
|
||||||
|
// track the PLAYER's cell, not the camera's. In third-person
|
||||||
|
// chase mode the camera enters interiors before the player body
|
||||||
|
// does, so a camera-based trigger flips the scene to indoor
|
||||||
|
// lighting prematurely. Retail's CellManager::ChangePosition
|
||||||
|
// @ 0x004559B0 reads CObjCell::seen_outside on the player's
|
||||||
|
// current cell — that's the semantics we want here. When the
|
||||||
|
// player isn't in player mode (orbit / fly debug camera) we
|
||||||
|
// fall back to the camera trigger.
|
||||||
|
bool playerInsideCell = (_playerMode && _playerController is not null)
|
||||||
|
? _cellVisibility.IsInsideAnyCell(_playerController.Position)
|
||||||
|
: cameraInsideCell;
|
||||||
|
|
||||||
// Phase C.1: tick retail PhysicsScript particle hooks. Named
|
// Phase C.1: tick retail PhysicsScript particle hooks. Named
|
||||||
// retail decomp confirms SkyObject.PesObjectId is copied by
|
// retail decomp confirms SkyObject.PesObjectId is copied by
|
||||||
// SkyDesc::GetSky but ignored by GameSky, so the sky-PES path is
|
// SkyDesc::GetSky but ignored by GameSky, so the sky-PES path is
|
||||||
|
|
@ -6861,7 +6874,7 @@ public sealed class GameWindow : IDisposable
|
||||||
// the scene-lighting UBO once per frame. Every shader that
|
// the scene-lighting UBO once per frame. Every shader that
|
||||||
// consumes binding=1 reads the same data for the rest of the
|
// consumes binding=1 reads the same data for the rest of the
|
||||||
// frame — terrain, static mesh, instanced mesh, sky.
|
// frame — terrain, static mesh, instanced mesh, sky.
|
||||||
UpdateSunFromSky(kf, cameraInsideCell);
|
UpdateSunFromSky(kf, playerInsideCell);
|
||||||
Lighting.Tick(camPos);
|
Lighting.Tick(camPos);
|
||||||
var ubo = AcDream.Core.Lighting.SceneLightingUbo.Build(
|
var ubo = AcDream.Core.Lighting.SceneLightingUbo.Build(
|
||||||
Lighting, in atmo, camPos, (float)WorldTime.DayFraction);
|
Lighting, in atmo, camPos, (float)WorldTime.DayFraction);
|
||||||
|
|
@ -8319,18 +8332,28 @@ public sealed class GameWindow : IDisposable
|
||||||
/// Derive the current sun (directional light, slot 0 of the UBO)
|
/// Derive the current sun (directional light, slot 0 of the UBO)
|
||||||
/// from the interpolated <see cref="AcDream.Core.World.SkyKeyframe"/>,
|
/// from the interpolated <see cref="AcDream.Core.World.SkyKeyframe"/>,
|
||||||
/// plus the cell ambient. Indoor cells force the sun intensity to
|
/// plus the cell ambient. Indoor cells force the sun intensity to
|
||||||
/// zero (r13 §13.7) and substitute a fixed dungeon-tone ambient.
|
/// zero and substitute a flat 0.2 white ambient — exact retail
|
||||||
|
/// behavior per <c>CellManager::ChangePosition</c> @ 0x004559B0,
|
||||||
|
/// which calls <c>SmartBox::SetWorldAmbientLight(0.2f, 0xFFFFFFFF)</c>
|
||||||
|
/// when the player's <c>CObjCell::seen_outside</c> flag is 0.
|
||||||
|
/// Indoor brightness then comes from per-cell point lights
|
||||||
|
/// (Setup.Lights on the cell's static objects, registered through
|
||||||
|
/// <see cref="AcDream.Core.Lighting.LightingHookSink"/>).
|
||||||
|
/// The trigger is the PLAYER's cell, not the camera's — third-person
|
||||||
|
/// chase camera enters interiors before the player body does, and
|
||||||
|
/// retail keys lighting off the player position.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void UpdateSunFromSky(AcDream.Core.World.SkyKeyframe kf, bool cameraInsideCell)
|
private void UpdateSunFromSky(AcDream.Core.World.SkyKeyframe kf, bool playerInsideCell)
|
||||||
{
|
{
|
||||||
// Sun direction: points FROM the sun TOWARDS the world. Our
|
// Sun direction: points FROM the sun TOWARDS the world. Our
|
||||||
// shader does dot(N, -forward) so a positive N·L means the
|
// shader does dot(N, -forward) so a positive N·L means the
|
||||||
// surface faces the sun.
|
// surface faces the sun.
|
||||||
var sunToWorld = -AcDream.Core.World.SkyStateProvider.SunDirectionFromKeyframe(kf);
|
var sunToWorld = -AcDream.Core.World.SkyStateProvider.SunDirectionFromKeyframe(kf);
|
||||||
|
|
||||||
if (cameraInsideCell)
|
if (playerInsideCell)
|
||||||
{
|
{
|
||||||
// Dungeon default per r13 §3 — warm-dark ambient, no sun.
|
// Indoor default — retail's flat 0.2 neutral ambient, sun
|
||||||
|
// zeroed. See xref to retail decomp in the doc comment above.
|
||||||
Lighting.Sun = new AcDream.Core.Lighting.LightSource
|
Lighting.Sun = new AcDream.Core.Lighting.LightSource
|
||||||
{
|
{
|
||||||
Kind = AcDream.Core.Lighting.LightKind.Directional,
|
Kind = AcDream.Core.Lighting.LightKind.Directional,
|
||||||
|
|
@ -8340,7 +8363,7 @@ public sealed class GameWindow : IDisposable
|
||||||
Range = 1f,
|
Range = 1f,
|
||||||
};
|
};
|
||||||
Lighting.CurrentAmbient = new AcDream.Core.Lighting.CellAmbientState(
|
Lighting.CurrentAmbient = new AcDream.Core.Lighting.CellAmbientState(
|
||||||
AmbientColor: new System.Numerics.Vector3(0.10f, 0.09f, 0.08f),
|
AmbientColor: new System.Numerics.Vector3(0.20f, 0.20f, 0.20f),
|
||||||
SunColor: System.Numerics.Vector3.Zero,
|
SunColor: System.Numerics.Vector3.Zero,
|
||||||
SunDirection: sunToWorld);
|
SunDirection: sunToWorld);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using AcDream.Core.Meshing;
|
using AcDream.Core.Meshing;
|
||||||
|
using AcDream.Core.Rendering;
|
||||||
using AcDream.Core.Terrain;
|
using AcDream.Core.Terrain;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
using Chorizite.OpenGLSDLBackend.Lib;
|
using Chorizite.OpenGLSDLBackend.Lib;
|
||||||
|
|
@ -140,6 +141,31 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
|
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Per-cell-entity last-log frame number for rate-limiting the
|
||||||
|
/// [indoor-walk] / [indoor-lookup] / [indoor-xform] / [indoor-cull]
|
||||||
|
/// probes. Defaults to 30 frames at 30Hz = 1 sec.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Dictionary<ulong, int> _lastIndoorProbeFrame = new();
|
||||||
|
private int _indoorProbeFrameCounter;
|
||||||
|
private const int IndoorProbeRateLimitFrames = 30;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true at most once per <see cref="IndoorProbeRateLimitFrames"/>
|
||||||
|
/// frames per cellId. Caller must already have checked that an indoor
|
||||||
|
/// probe flag is enabled.
|
||||||
|
/// </summary>
|
||||||
|
private bool ShouldEmitIndoorProbe(ulong cellId)
|
||||||
|
{
|
||||||
|
if (!_lastIndoorProbeFrame.TryGetValue(cellId, out int last)
|
||||||
|
|| _indoorProbeFrameCounter - last >= IndoorProbeRateLimitFrames)
|
||||||
|
{
|
||||||
|
_lastIndoorProbeFrame[cellId] = _indoorProbeFrameCounter;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Diagnostic counters logged once per ~5s under ACDREAM_WB_DIAG=1.
|
// Diagnostic counters logged once per ~5s under ACDREAM_WB_DIAG=1.
|
||||||
private int _entitiesSeen;
|
private int _entitiesSeen;
|
||||||
private int _entitiesDrawn;
|
private int _entitiesDrawn;
|
||||||
|
|
@ -271,6 +297,16 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
/// list. <see cref="Draw"/> reuses a per-dispatcher scratch field across frames to
|
/// list. <see cref="Draw"/> reuses a per-dispatcher scratch field across frames to
|
||||||
/// avoid the 480+ KB / frame GC pressure that the test-friendly overload incurs.
|
/// avoid the 480+ KB / frame GC pressure that the test-friendly overload incurs.
|
||||||
/// Returns walk count via <paramref name="result"/>'s <c>EntitiesWalked</c> field.
|
/// Returns walk count via <paramref name="result"/>'s <c>EntitiesWalked</c> field.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// When <paramref name="indoorProbeState"/> is non-null the method emits
|
||||||
|
/// <c>[indoor-cull]</c> lines for cell entities rejected by the
|
||||||
|
/// visibleCellIds or frustum filters, and <c>[indoor-walk]</c> lines for
|
||||||
|
/// cell entities that pass all filters. Rate-limited by
|
||||||
|
/// <see cref="IndoorProbeState"/>. Pass <see langword="null"/> (the default)
|
||||||
|
/// to disable all probe emission — used by the test-friendly
|
||||||
|
/// <see cref="WalkEntities"/> overload.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static void WalkEntitiesInto(
|
internal static void WalkEntitiesInto(
|
||||||
IEnumerable<LandblockEntry> landblockEntries,
|
IEnumerable<LandblockEntry> landblockEntries,
|
||||||
|
|
@ -279,7 +315,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
HashSet<uint>? visibleCellIds,
|
HashSet<uint>? visibleCellIds,
|
||||||
HashSet<uint>? animatedEntityIds,
|
HashSet<uint>? animatedEntityIds,
|
||||||
List<(WorldEntity Entity, int MeshRefIndex, uint LandblockId)> scratch,
|
List<(WorldEntity Entity, int MeshRefIndex, uint LandblockId)> scratch,
|
||||||
ref WalkResult result)
|
ref WalkResult result,
|
||||||
|
IndoorProbeState? indoorProbeState = null)
|
||||||
{
|
{
|
||||||
scratch.Clear();
|
scratch.Clear();
|
||||||
result.EntitiesWalked = 0;
|
result.EntitiesWalked = 0;
|
||||||
|
|
@ -314,19 +351,65 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
{
|
{
|
||||||
if (entity.MeshRefs.Count == 0) continue;
|
if (entity.MeshRefs.Count == 0) continue;
|
||||||
|
|
||||||
if (entity.ParentCellId.HasValue && visibleCellIds is not null
|
// Detect cell entity for indoor probes — first MeshRef.GfxObjId
|
||||||
&& !visibleCellIds.Contains(entity.ParentCellId.Value))
|
// is an EnvCell id (low 16 bits ≥ 0x0100). Cheap to compute;
|
||||||
|
// result reused for all probe checks below.
|
||||||
|
ulong cellProbeId = (ulong)entity.MeshRefs[0].GfxObjId;
|
||||||
|
bool isCellEntity = indoorProbeState is not null
|
||||||
|
&& RenderingDiagnostics.IsEnvCellId(cellProbeId);
|
||||||
|
|
||||||
|
bool cellInVis = !(entity.ParentCellId.HasValue
|
||||||
|
&& visibleCellIds is not null
|
||||||
|
&& !visibleCellIds.Contains(entity.ParentCellId.Value));
|
||||||
|
if (!cellInVis)
|
||||||
|
{
|
||||||
|
if (isCellEntity && RenderingDiagnostics.ProbeIndoorCullEnabled
|
||||||
|
&& indoorProbeState!.ShouldEmit(cellProbeId))
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[indoor-cull] cellEnt=0x{entity.Id:X8} " +
|
||||||
|
$"reason=visibleCellIds-miss " +
|
||||||
|
$"parentCell=0x{entity.ParentCellId!.Value:X8}");
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Per-entity AABB frustum cull (perf #3). Animated entities bypass —
|
// Per-entity AABB frustum cull (perf #3). Animated entities bypass —
|
||||||
// they're tracked at landblock level + need per-frame work regardless.
|
// they're tracked at landblock level + need per-frame work regardless.
|
||||||
// A.5 T18 Change #2: read cached AABB, refresh lazily on AabbDirty.
|
// A.5 T18 Change #2: read cached AABB, refresh lazily on AabbDirty.
|
||||||
bool isAnimated = animatedEntityIds?.Contains(entity.Id) == true;
|
bool isAnimated = animatedEntityIds?.Contains(entity.Id) == true;
|
||||||
|
bool aabbVisible = true;
|
||||||
if (frustum is not null && !isAnimated && entry.LandblockId != neverCullLandblockId)
|
if (frustum is not null && !isAnimated && entry.LandblockId != neverCullLandblockId)
|
||||||
{
|
{
|
||||||
if (entity.AabbDirty) entity.RefreshAabb();
|
if (entity.AabbDirty) entity.RefreshAabb();
|
||||||
if (!FrustumCuller.IsAabbVisible(frustum.Value, entity.AabbMin, entity.AabbMax))
|
aabbVisible = FrustumCuller.IsAabbVisible(frustum.Value, entity.AabbMin, entity.AabbMax);
|
||||||
continue;
|
}
|
||||||
|
|
||||||
|
if (!aabbVisible)
|
||||||
|
{
|
||||||
|
if (isCellEntity && RenderingDiagnostics.ProbeIndoorCullEnabled
|
||||||
|
&& indoorProbeState!.ShouldEmit(cellProbeId))
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[indoor-cull] cellEnt=0x{entity.Id:X8} " +
|
||||||
|
$"reason=frustum " +
|
||||||
|
$"aabbMin=({entity.AabbMin.X:F1},{entity.AabbMin.Y:F1},{entity.AabbMin.Z:F1}) " +
|
||||||
|
$"aabbMax=({entity.AabbMax.X:F1},{entity.AabbMax.Y:F1},{entity.AabbMax.Z:F1})");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passed all filters — emit walk probe.
|
||||||
|
if (isCellEntity && RenderingDiagnostics.ProbeIndoorWalkEnabled
|
||||||
|
&& indoorProbeState!.ShouldEmit(cellProbeId))
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[indoor-walk] cellEnt=0x{entity.Id:X8} " +
|
||||||
|
$"pos=({entity.Position.X:F1},{entity.Position.Y:F1},{entity.Position.Z:F1}) " +
|
||||||
|
$"parentCell=0x{(entity.ParentCellId ?? 0u):X8} " +
|
||||||
|
$"meshRef0=0x{cellProbeId:X8} " +
|
||||||
|
$"meshRefCount={entity.MeshRefs.Count} " +
|
||||||
|
$"landblockVisible=true aabbVisible=true cellInVis=true");
|
||||||
}
|
}
|
||||||
|
|
||||||
result.EntitiesWalked++;
|
result.EntitiesWalked++;
|
||||||
|
|
@ -347,6 +430,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
HashSet<uint>? animatedEntityIds = null)
|
HashSet<uint>? animatedEntityIds = null)
|
||||||
{
|
{
|
||||||
_shader.Use();
|
_shader.Use();
|
||||||
|
_indoorProbeFrameCounter++;
|
||||||
var vp = camera.View * camera.Projection;
|
var vp = camera.View * camera.Projection;
|
||||||
_shader.SetMatrix4("uViewProjection", vp);
|
_shader.SetMatrix4("uViewProjection", vp);
|
||||||
|
|
||||||
|
|
@ -391,6 +475,24 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
// A.5 T26 follow-up (Bug B): use the no-alloc WalkEntitiesInto overload
|
// A.5 T26 follow-up (Bug B): use the no-alloc WalkEntitiesInto overload
|
||||||
// that populates _walkScratch (a per-dispatcher field reused across frames)
|
// that populates _walkScratch (a per-dispatcher field reused across frames)
|
||||||
// instead of allocating a fresh List<(WorldEntity, int)> per frame.
|
// instead of allocating a fresh List<(WorldEntity, int)> per frame.
|
||||||
|
//
|
||||||
|
// Pass an IndoorProbeState when any indoor probe is active so the static
|
||||||
|
// WalkEntitiesInto can emit rate-limited [indoor-cull] / [indoor-walk]
|
||||||
|
// lines without needing access to instance fields. Null = probes off.
|
||||||
|
IndoorProbeState? probeState = null;
|
||||||
|
if (RenderingDiagnostics.ProbeIndoorCullEnabled || RenderingDiagnostics.ProbeIndoorWalkEnabled)
|
||||||
|
{
|
||||||
|
// _currentFrame is snapped at construction time. Construct
|
||||||
|
// once per Draw() call only — a second construction within
|
||||||
|
// the same frame would stamp the dictionary with the
|
||||||
|
// (already-advanced) counter value, suppressing the second
|
||||||
|
// pass's emissions for IndoorProbeRateLimitFrames frames.
|
||||||
|
// Today Draw() is called exactly once per frame; if a
|
||||||
|
// future refactor adds a shadow / reflection / second pass,
|
||||||
|
// this assumption needs revisiting.
|
||||||
|
probeState = new IndoorProbeState(_lastIndoorProbeFrame, _indoorProbeFrameCounter);
|
||||||
|
}
|
||||||
|
|
||||||
var walkResult = default(WalkResult);
|
var walkResult = default(WalkResult);
|
||||||
WalkEntitiesInto(
|
WalkEntitiesInto(
|
||||||
ToEntries(landblockEntries),
|
ToEntries(landblockEntries),
|
||||||
|
|
@ -399,7 +501,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
visibleCellIds,
|
visibleCellIds,
|
||||||
animatedEntityIds,
|
animatedEntityIds,
|
||||||
_walkScratch,
|
_walkScratch,
|
||||||
ref walkResult);
|
ref walkResult,
|
||||||
|
probeState);
|
||||||
|
|
||||||
// Tier 1 cache (#53) flush-tracking locals. _walkScratch holds one tuple
|
// Tier 1 cache (#53) flush-tracking locals. _walkScratch holds one tuple
|
||||||
// per (entity, MeshRefIndex) and is in entity-order, so all MeshRefs of
|
// per (entity, MeshRefIndex) and is in entity-order, so all MeshRefs of
|
||||||
|
|
@ -582,6 +685,42 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
ulong gfxObjId = meshRef.GfxObjId;
|
ulong gfxObjId = meshRef.GfxObjId;
|
||||||
|
|
||||||
var renderData = _meshAdapter.TryGetRenderData(gfxObjId);
|
var renderData = _meshAdapter.TryGetRenderData(gfxObjId);
|
||||||
|
|
||||||
|
// [indoor-lookup] probe — emit once per cell entity per sec.
|
||||||
|
// Fires BEFORE the null-renderData early-continue so a miss still
|
||||||
|
// emits hit=false, distinguishing H2 (empty batches) from H6
|
||||||
|
// (dispatcher fails to traverse Setup).
|
||||||
|
ulong lookupCellId = (ulong)gfxObjId;
|
||||||
|
if (RenderingDiagnostics.IsEnvCellId(lookupCellId)
|
||||||
|
&& RenderingDiagnostics.ProbeIndoorLookupEnabled
|
||||||
|
// Rate-limit in a separate namespace from [indoor-walk]/[indoor-cull]
|
||||||
|
// (which key on the same gfxObjId). Without this, IndoorAll=1 would
|
||||||
|
// silence the lookup probe whenever the walk probe fired first.
|
||||||
|
&& ShouldEmitIndoorProbe(lookupCellId | 0x8000_0000_0000_0000UL))
|
||||||
|
{
|
||||||
|
bool hit = renderData is not null;
|
||||||
|
bool isSetup = hit && renderData!.IsSetup;
|
||||||
|
int partCount = isSetup ? renderData!.SetupParts.Count : 0;
|
||||||
|
|
||||||
|
int partsHit = 0, partsMiss = 0;
|
||||||
|
if (isSetup)
|
||||||
|
{
|
||||||
|
foreach (var (partId, _) in renderData!.SetupParts)
|
||||||
|
{
|
||||||
|
if (_meshAdapter.TryGetRenderData(partId) is not null) partsHit++;
|
||||||
|
else partsMiss++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasEnvCellGeom = isSetup
|
||||||
|
&& renderData!.SetupParts.Exists(t => (t.GfxObjId & 0x1_0000_0000UL) != 0);
|
||||||
|
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[indoor-lookup] cellId=0x{lookupCellId:X8} " +
|
||||||
|
$"hit={hit} isSetup={isSetup} partCount={partCount} " +
|
||||||
|
$"hasEnvCellGeom={hasEnvCellGeom} partsHit={partsHit} partsMiss={partsMiss}");
|
||||||
|
}
|
||||||
|
|
||||||
if (renderData is null)
|
if (renderData is null)
|
||||||
{
|
{
|
||||||
// Tier 1 cache (#53): mesh data is still async-decoding via
|
// Tier 1 cache (#53): mesh data is still async-decoding via
|
||||||
|
|
@ -614,6 +753,23 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
var model = ComposePartWorldMatrix(
|
var model = ComposePartWorldMatrix(
|
||||||
entityWorld, meshRef.PartTransform, partTransform);
|
entityWorld, meshRef.PartTransform, partTransform);
|
||||||
|
|
||||||
|
// [indoor-xform] probe — only for the cell's synthetic
|
||||||
|
// geometry part (bit 32 set, per WB's PrepareEnvCellMeshData
|
||||||
|
// cellGeomId convention). One line per part per sec.
|
||||||
|
// Disambiguates hypothesis H5 (transform double-apply —
|
||||||
|
// composedT lands at 2 × cellOrigin).
|
||||||
|
if ((partGfxObjId & 0x1_0000_0000UL) != 0
|
||||||
|
&& RenderingDiagnostics.ProbeIndoorXformEnabled
|
||||||
|
&& ShouldEmitIndoorProbe(partGfxObjId))
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[indoor-xform] cellGeomId=0x{partGfxObjId:X16} " +
|
||||||
|
$"entityWorldT=({entityWorld.Translation.X:F2},{entityWorld.Translation.Y:F2},{entityWorld.Translation.Z:F2}) " +
|
||||||
|
$"meshRefT=({meshRef.PartTransform.Translation.X:F2},{meshRef.PartTransform.Translation.Y:F2},{meshRef.PartTransform.Translation.Z:F2}) " +
|
||||||
|
$"partT=({partTransform.Translation.X:F2},{partTransform.Translation.Y:F2},{partTransform.Translation.Z:F2}) " +
|
||||||
|
$"composedT=({model.Translation.X:F2},{model.Translation.Y:F2},{model.Translation.Z:F2})");
|
||||||
|
}
|
||||||
|
|
||||||
var restPose = partTransform * meshRef.PartTransform;
|
var restPose = partTransform * meshRef.PartTransform;
|
||||||
ClassifyBatches(partData, partGfxObjId, model, entity, meshRef, palHash, metaTable, restPose, collector);
|
ClassifyBatches(partData, partGfxObjId, model, entity, meshRef, palHash, metaTable, restPose, collector);
|
||||||
drewAny = true;
|
drewAny = true;
|
||||||
|
|
@ -1289,6 +1445,41 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Thin wrapper around an instance's rate-limit dictionary + frame
|
||||||
|
/// counter, passed into the static <see cref="WalkEntitiesInto"/>
|
||||||
|
/// overload so it can emit rate-limited probe lines without access
|
||||||
|
/// to instance fields. Null = probes disabled (test-friendly overload).
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class IndoorProbeState
|
||||||
|
{
|
||||||
|
private readonly Dictionary<ulong, int> _lastFrame;
|
||||||
|
private readonly int _currentFrame;
|
||||||
|
private const int RateLimit = IndoorProbeRateLimitFrames;
|
||||||
|
|
||||||
|
internal IndoorProbeState(Dictionary<ulong, int> lastFrame, int currentFrame)
|
||||||
|
{
|
||||||
|
_lastFrame = lastFrame;
|
||||||
|
_currentFrame = currentFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true at most once per <see cref="IndoorProbeRateLimitFrames"/>
|
||||||
|
/// frames per <paramref name="cellId"/>. Side-effect: stamps the frame
|
||||||
|
/// number into the dictionary on success.
|
||||||
|
/// </summary>
|
||||||
|
internal bool ShouldEmit(ulong cellId)
|
||||||
|
{
|
||||||
|
if (!_lastFrame.TryGetValue(cellId, out int last)
|
||||||
|
|| _currentFrame - last >= RateLimit)
|
||||||
|
{
|
||||||
|
_lastFrame[cellId] = _currentFrame;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class InstanceGroup
|
private sealed class InstanceGroup
|
||||||
{
|
{
|
||||||
public uint Ibo;
|
public uint Ibo;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using AcDream.Core.Meshing;
|
using AcDream.Core.Meshing;
|
||||||
|
using AcDream.Core.Rendering;
|
||||||
using Chorizite.OpenGLSDLBackend;
|
using Chorizite.OpenGLSDLBackend;
|
||||||
using Chorizite.OpenGLSDLBackend.Lib;
|
using Chorizite.OpenGLSDLBackend.Lib;
|
||||||
using DatReaderWriter;
|
using DatReaderWriter;
|
||||||
|
|
@ -34,6 +37,15 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
||||||
private readonly AcSurfaceMetadataTable _metadataTable = new();
|
private readonly AcSurfaceMetadataTable _metadataTable = new();
|
||||||
private readonly HashSet<ulong> _metadataPopulated = new();
|
private readonly HashSet<ulong> _metadataPopulated = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// EnvCell ids we've requested via PrepareMeshDataAsync but not yet
|
||||||
|
/// seen completion for in Tick(). Used by the [indoor-upload] probe
|
||||||
|
/// to log requested + completed pairs. Cleared per completion;
|
||||||
|
/// missing completions after a few seconds indicate WB silently
|
||||||
|
/// returned null (hypothesis H1 in the design spec).
|
||||||
|
/// </summary>
|
||||||
|
private readonly HashSet<ulong> _pendingEnvCellRequests = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// True when this instance was created via <see cref="CreateUninitialized"/>;
|
/// True when this instance was created via <see cref="CreateUninitialized"/>;
|
||||||
/// all public methods no-op when uninitialized.
|
/// all public methods no-op when uninitialized.
|
||||||
|
|
@ -65,10 +77,52 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
||||||
_dats = dats;
|
_dats = dats;
|
||||||
_graphicsDevice = new OpenGLGraphicsDevice(gl, logger, new DebugRenderSettings());
|
_graphicsDevice = new OpenGLGraphicsDevice(gl, logger, new DebugRenderSettings());
|
||||||
_wbDats = new DefaultDatReaderWriter(datDir);
|
_wbDats = new DefaultDatReaderWriter(datDir);
|
||||||
|
// Phase 2 diagnostic — replace NullLogger with a Console-backed
|
||||||
|
// logger so WB's internal catch block at ObjectMeshManager.cs:589
|
||||||
|
// (and similar) surfaces its swallowed exceptions instead of
|
||||||
|
// dropping them. ConsoleErrorLogger filters to LogLevel.Error+
|
||||||
|
// so successful operations stay quiet.
|
||||||
_meshManager = new ObjectMeshManager(
|
_meshManager = new ObjectMeshManager(
|
||||||
_graphicsDevice,
|
_graphicsDevice,
|
||||||
_wbDats,
|
_wbDats,
|
||||||
NullLogger<ObjectMeshManager>.Instance);
|
new ConsoleErrorLogger<ObjectMeshManager>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal Console-backed logger that fires only on
|
||||||
|
/// <see cref="LogLevel.Error"/> and above. Format:
|
||||||
|
/// <code>[wb-error] <message>
|
||||||
|
/// [wb-error] <ExceptionType>: <ExceptionMessage>
|
||||||
|
/// [wb-error] at <frame> (up to 5 frames)</code>
|
||||||
|
/// Used to surface WB's silently-caught exceptions in
|
||||||
|
/// <c>ObjectMeshManager.PrepareMeshData</c>.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class ConsoleErrorLogger<T> : ILogger<T>
|
||||||
|
{
|
||||||
|
public IDisposable BeginScope<TState>(TState state) where TState : notnull => NullScope.Instance;
|
||||||
|
public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Error;
|
||||||
|
public void Log<TState>(
|
||||||
|
LogLevel logLevel, EventId eventId, TState state, Exception? exception,
|
||||||
|
Func<TState, Exception?, string> formatter)
|
||||||
|
{
|
||||||
|
if (!IsEnabled(logLevel)) return;
|
||||||
|
var message = formatter(state, exception);
|
||||||
|
Console.WriteLine($"[wb-error] {message}");
|
||||||
|
if (exception is not null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[wb-error] {exception.GetType().Name}: {exception.Message}");
|
||||||
|
var stack = (exception.StackTrace ?? "")
|
||||||
|
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Take(5);
|
||||||
|
foreach (var s in stack) Console.WriteLine($"[wb-error] {s.Trim()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class NullScope : IDisposable
|
||||||
|
{
|
||||||
|
public static readonly NullScope Instance = new();
|
||||||
|
public void Dispose() { }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private WbMeshAdapter()
|
private WbMeshAdapter()
|
||||||
|
|
@ -133,7 +187,80 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
||||||
// isSetup: false — acdream's MeshRefs already carry expanded
|
// isSetup: false — acdream's MeshRefs already carry expanded
|
||||||
// per-part GfxObj ids (0x01XXXXXX). WB's Setup-expansion path is
|
// per-part GfxObj ids (0x01XXXXXX). WB's Setup-expansion path is
|
||||||
// unused.
|
// unused.
|
||||||
_ = _meshManager.PrepareMeshDataAsync(id, isSetup: false);
|
var prepTask = _meshManager.PrepareMeshDataAsync(id, isSetup: false);
|
||||||
|
|
||||||
|
// [indoor-upload] requested probe — only for EnvCell ids.
|
||||||
|
if (RenderingDiagnostics.IsEnvCellId(id) && RenderingDiagnostics.ProbeIndoorUploadEnabled)
|
||||||
|
{
|
||||||
|
bool hadRenderDataAtRequest = _meshManager.HasRenderData(id);
|
||||||
|
_pendingEnvCellRequests.Add(id);
|
||||||
|
Console.WriteLine($"[indoor-upload] requested cellId=0x{id:X8} hadRenderData={hadRenderDataAtRequest}");
|
||||||
|
|
||||||
|
// Phase 2 — surface what WB's catch block silently swallows.
|
||||||
|
// ObjectMeshManager.PrepareMeshData has a try/catch at line 589
|
||||||
|
// that calls _logger.LogError(ex, ...) — but we construct
|
||||||
|
// ObjectMeshManager with NullLogger.Instance so the log is
|
||||||
|
// dropped. This continuation captures the same data scoped to
|
||||||
|
// EnvCell ids only. Runs on ThreadPool; non-blocking. Zero cost
|
||||||
|
// when the probe is off.
|
||||||
|
ulong cellId = id;
|
||||||
|
_ = prepTask.ContinueWith(t =>
|
||||||
|
{
|
||||||
|
if (t.IsFaulted && t.Exception is not null)
|
||||||
|
{
|
||||||
|
var ex = t.Exception.InnerException ?? t.Exception;
|
||||||
|
var stack = (ex.StackTrace ?? "").Split('\n')
|
||||||
|
.Take(3).Select(s => s.Trim()).Where(s => s.Length > 0);
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[indoor-upload] FAILED cellId=0x{cellId:X8} " +
|
||||||
|
$"exception={ex.GetType().Name}: {ex.Message} " +
|
||||||
|
$"stack=[{string.Join(" | ", stack)}]");
|
||||||
|
}
|
||||||
|
else if (t.IsCompletedSuccessfully && t.Result is null)
|
||||||
|
{
|
||||||
|
// Phase 2 cause-narrowing: WB's PrepareMeshData can return
|
||||||
|
// null for several reasons (ResolveId empty / TryGet<EnvCell>
|
||||||
|
// failed / type Unknown). Cross-check against acdream's own
|
||||||
|
// DatCollection — if WE find the cell but WB doesn't, the
|
||||||
|
// divergence is between dat readers, not a missing record.
|
||||||
|
bool ourCellFound = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ourCellFound = _dats?.Cell.TryGet<DatReaderWriter.DBObjs.EnvCell>(
|
||||||
|
(uint)cellId, out _) ?? false;
|
||||||
|
}
|
||||||
|
catch { /* swallow — this is best-effort diagnostic */ }
|
||||||
|
|
||||||
|
int wbResolveCount = -1;
|
||||||
|
string wbSelectedType = "none";
|
||||||
|
bool wbDbTryGetEnvCell = false;
|
||||||
|
bool wbDbIsPortal = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var wbResolutions = _wbDats?.ResolveId((uint)cellId).ToList();
|
||||||
|
wbResolveCount = wbResolutions?.Count ?? -1;
|
||||||
|
if (wbResolutions is not null && wbResolutions.Count > 0)
|
||||||
|
{
|
||||||
|
var selected = wbResolutions
|
||||||
|
.OrderByDescending(r => r.Database == _wbDats!.Portal)
|
||||||
|
.First();
|
||||||
|
wbSelectedType = selected.Type.ToString();
|
||||||
|
wbDbIsPortal = selected.Database == _wbDats!.Portal;
|
||||||
|
try { wbDbTryGetEnvCell = selected.Database.TryGet<DatReaderWriter.DBObjs.EnvCell>((uint)cellId, out _); } catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* swallow — best-effort */ }
|
||||||
|
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[indoor-upload] NULL_RESULT cellId=0x{cellId:X8} " +
|
||||||
|
$"ourCellDb.TryGet={ourCellFound} " +
|
||||||
|
$"wbResolveId.Count={wbResolveCount} " +
|
||||||
|
$"wbSelectedType={wbSelectedType} " +
|
||||||
|
$"wbDbIsPortal={wbDbIsPortal} " +
|
||||||
|
$"wbDbTryGet<EnvCell>={wbDbTryGetEnvCell}");
|
||||||
|
}
|
||||||
|
}, TaskScheduler.Default);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -172,7 +299,26 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
||||||
_graphicsDevice!.ProcessGLQueue();
|
_graphicsDevice!.ProcessGLQueue();
|
||||||
while (_meshManager!.StagedMeshData.TryDequeue(out var meshData))
|
while (_meshManager!.StagedMeshData.TryDequeue(out var meshData))
|
||||||
{
|
{
|
||||||
_meshManager.UploadMeshData(meshData);
|
// [indoor-upload] completed probe — check BEFORE upload so we
|
||||||
|
// see what WB actually produced (vertex counts, parts) before
|
||||||
|
// any post-upload mutation.
|
||||||
|
bool isPendingEnvCell = RenderingDiagnostics.ProbeIndoorUploadEnabled
|
||||||
|
&& _pendingEnvCellRequests.Remove(meshData.ObjectId);
|
||||||
|
|
||||||
|
var renderData = _meshManager.UploadMeshData(meshData);
|
||||||
|
|
||||||
|
if (isPendingEnvCell)
|
||||||
|
{
|
||||||
|
int parts = meshData.SetupParts?.Count ?? 0;
|
||||||
|
bool hasGeom = meshData.EnvCellGeometry is not null;
|
||||||
|
int cellGeomVerts = meshData.EnvCellGeometry?.Vertices?.Length ?? 0;
|
||||||
|
bool uploadOk = renderData is not null;
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[indoor-upload] completed cellId=0x{meshData.ObjectId:X8} " +
|
||||||
|
$"isSetup={meshData.IsSetup} parts={parts} " +
|
||||||
|
$"hasEnvCellGeom={hasGeom} cellGeomVerts={cellGeomVerts} " +
|
||||||
|
$"uploadOk={uploadOk}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
109
src/AcDream.Core/Rendering/RenderingDiagnostics.cs
Normal file
109
src/AcDream.Core/Rendering/RenderingDiagnostics.cs
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Rendering;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 2026-05-19 — runtime-toggleable diagnostic flags for the indoor cell
|
||||||
|
/// rendering pipeline. Initialized from env vars at process start;
|
||||||
|
/// flippable at runtime via the DebugPanel mirror. Log call sites read
|
||||||
|
/// these statics so a checkbox toggle takes effect on the next frame
|
||||||
|
/// without relaunching.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Mirrors the L.2a <see cref="AcDream.Core.Physics.PhysicsDiagnostics"/>
|
||||||
|
/// pattern. The master <see cref="IndoorAll"/> toggle is the user's
|
||||||
|
/// common case — flipping it cascades to all five probe flags.
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Spec: <c>docs/superpowers/specs/2026-05-19-indoor-cell-rendering-fix-design.md</c>.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public static class RenderingDiagnostics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// When true, <c>WbDrawDispatcher.WalkVisibleEntities</c> emits one
|
||||||
|
/// <c>[indoor-walk]</c> line per visible cell entity per second:
|
||||||
|
/// entity id, world position, parent cell id, landblock visible flag,
|
||||||
|
/// AABB-visible flag, "in visible cells" flag, drew flag.
|
||||||
|
/// Initial state from <c>ACDREAM_PROBE_INDOOR_WALK=1</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static bool ProbeIndoorWalkEnabled { get; set; } =
|
||||||
|
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_WALK") == "1"
|
||||||
|
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When true, <c>WbDrawDispatcher</c> emits one <c>[indoor-lookup]</c>
|
||||||
|
/// line per visible cell entity per second: render-data hit/miss,
|
||||||
|
/// IsSetup flag, SetupParts count, parts-hit / parts-miss tallies.
|
||||||
|
/// Initial state from <c>ACDREAM_PROBE_INDOOR_LOOKUP=1</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static bool ProbeIndoorLookupEnabled { get; set; } =
|
||||||
|
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_LOOKUP") == "1"
|
||||||
|
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When true, <c>WbMeshAdapter</c> emits two lines per EnvCell id:
|
||||||
|
/// <c>[indoor-upload] requested</c> on first IncrementRefCount and
|
||||||
|
/// <c>[indoor-upload] completed</c> when WB's staged drain produces
|
||||||
|
/// its <c>ObjectMeshData</c>. Missing "completed" lines indicate WB
|
||||||
|
/// silently returned null (hypothesis H1).
|
||||||
|
/// Initial state from <c>ACDREAM_PROBE_INDOOR_UPLOAD=1</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static bool ProbeIndoorUploadEnabled { get; set; } =
|
||||||
|
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_UPLOAD") == "1"
|
||||||
|
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When true, <c>WbDrawDispatcher</c> emits one <c>[indoor-xform]</c>
|
||||||
|
/// line per visible cell entity per second: cell-geometry SetupPart's
|
||||||
|
/// composed world matrix translation. Disambiguates transform
|
||||||
|
/// double-apply (hypothesis H5).
|
||||||
|
/// Initial state from <c>ACDREAM_PROBE_INDOOR_XFORM=1</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static bool ProbeIndoorXformEnabled { get; set; } =
|
||||||
|
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_XFORM") == "1"
|
||||||
|
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When true, <c>WbDrawDispatcher.WalkVisibleEntities</c> emits one
|
||||||
|
/// <c>[indoor-cull]</c> line per cell entity that gets culled, with
|
||||||
|
/// the reason (visibleCellIds-miss, frustum, landblock). Disambiguates
|
||||||
|
/// cull bugs (hypothesis H3).
|
||||||
|
/// Initial state from <c>ACDREAM_PROBE_INDOOR_CULL=1</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static bool ProbeIndoorCullEnabled { get; set; } =
|
||||||
|
Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_CULL") == "1"
|
||||||
|
|| Environment.GetEnvironmentVariable("ACDREAM_PROBE_INDOOR_ALL") == "1";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Master toggle. Reading reflects the AND of all five flags
|
||||||
|
/// (true only when every probe is on). Writing cascades — setting
|
||||||
|
/// to <see langword="true"/> turns ALL five flags on; setting to
|
||||||
|
/// <see langword="false"/> turns ALL five off.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IndoorAll
|
||||||
|
{
|
||||||
|
get => ProbeIndoorWalkEnabled
|
||||||
|
&& ProbeIndoorLookupEnabled
|
||||||
|
&& ProbeIndoorUploadEnabled
|
||||||
|
&& ProbeIndoorXformEnabled
|
||||||
|
&& ProbeIndoorCullEnabled;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
ProbeIndoorWalkEnabled = value;
|
||||||
|
ProbeIndoorLookupEnabled = value;
|
||||||
|
ProbeIndoorUploadEnabled = value;
|
||||||
|
ProbeIndoorXformEnabled = value;
|
||||||
|
ProbeIndoorCullEnabled = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper for probe call sites. Returns <see langword="true"/> when
|
||||||
|
/// the low 16 bits of <paramref name="id"/> are ≥ 0x0100 — the AC
|
||||||
|
/// convention for EnvCell (indoor) cells, as opposed to outdoor cells
|
||||||
|
/// in the 8×8 landblock grid (0x0001–0x0040).
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsEnvCellId(ulong id) => (id & 0xFFFFu) >= 0x0100u;
|
||||||
|
}
|
||||||
|
|
@ -254,6 +254,27 @@ public sealed class DebugPanel : IPanel
|
||||||
if (r.Checkbox("Probe auto-walk (ACDREAM_PROBE_AUTOWALK)",
|
if (r.Checkbox("Probe auto-walk (ACDREAM_PROBE_AUTOWALK)",
|
||||||
ref probeAutoWalk)) _vm.ProbeAutoWalk = probeAutoWalk;
|
ref probeAutoWalk)) _vm.ProbeAutoWalk = probeAutoWalk;
|
||||||
|
|
||||||
|
// ── Indoor rendering diagnostics (2026-05-19) ───────────────
|
||||||
|
// Pinpoint where the EnvCell rendering chain breaks for
|
||||||
|
// hypothesis-driven Phase 2 fix. Spec:
|
||||||
|
// docs/superpowers/specs/2026-05-19-indoor-cell-rendering-fix-design.md
|
||||||
|
r.Separator();
|
||||||
|
r.Text("Indoor rendering (envCell):");
|
||||||
|
|
||||||
|
bool probeIndoorAll = _vm.ProbeIndoorAll;
|
||||||
|
bool probeIndoorWalk = _vm.ProbeIndoorWalk;
|
||||||
|
bool probeIndoorLookup = _vm.ProbeIndoorLookup;
|
||||||
|
bool probeIndoorUpload = _vm.ProbeIndoorUpload;
|
||||||
|
bool probeIndoorXform = _vm.ProbeIndoorXform;
|
||||||
|
bool probeIndoorCull = _vm.ProbeIndoorCull;
|
||||||
|
|
||||||
|
if (r.Checkbox("Indoor: ALL (ACDREAM_PROBE_INDOOR_ALL)", ref probeIndoorAll)) _vm.ProbeIndoorAll = probeIndoorAll;
|
||||||
|
if (r.Checkbox("Indoor: walk (ACDREAM_PROBE_INDOOR_WALK)", ref probeIndoorWalk)) _vm.ProbeIndoorWalk = probeIndoorWalk;
|
||||||
|
if (r.Checkbox("Indoor: lookup (ACDREAM_PROBE_INDOOR_LOOKUP)", ref probeIndoorLookup)) _vm.ProbeIndoorLookup = probeIndoorLookup;
|
||||||
|
if (r.Checkbox("Indoor: upload (ACDREAM_PROBE_INDOOR_UPLOAD)", ref probeIndoorUpload)) _vm.ProbeIndoorUpload = probeIndoorUpload;
|
||||||
|
if (r.Checkbox("Indoor: xform (ACDREAM_PROBE_INDOOR_XFORM)", ref probeIndoorXform)) _vm.ProbeIndoorXform = probeIndoorXform;
|
||||||
|
if (r.Checkbox("Indoor: cull (ACDREAM_PROBE_INDOOR_CULL)", ref probeIndoorCull)) _vm.ProbeIndoorCull = probeIndoorCull;
|
||||||
|
|
||||||
r.Spacing();
|
r.Spacing();
|
||||||
|
|
||||||
// Cycle / toggle actions live on the VM as Action handles; the
|
// Cycle / toggle actions live on the VM as Action handles; the
|
||||||
|
|
|
||||||
|
|
@ -291,6 +291,72 @@ public sealed class DebugVM
|
||||||
set => PhysicsDiagnostics.ProbeAutoWalkEnabled = value;
|
set => PhysicsDiagnostics.ProbeAutoWalkEnabled = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Indoor rendering diagnostics (2026-05-19) ───────────────────
|
||||||
|
// Mirror RenderingDiagnostics statics so DebugPanel checkbox toggles
|
||||||
|
// take effect on the next render frame without relaunching.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runtime mirror of <c>RenderingDiagnostics.ProbeIndoorWalkEnabled</c>
|
||||||
|
/// (env var <c>ACDREAM_PROBE_INDOOR_WALK</c>).
|
||||||
|
/// </summary>
|
||||||
|
public bool ProbeIndoorWalk
|
||||||
|
{
|
||||||
|
get => RenderingDiagnostics.ProbeIndoorWalkEnabled;
|
||||||
|
set => RenderingDiagnostics.ProbeIndoorWalkEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runtime mirror of <c>RenderingDiagnostics.ProbeIndoorLookupEnabled</c>
|
||||||
|
/// (env var <c>ACDREAM_PROBE_INDOOR_LOOKUP</c>).
|
||||||
|
/// </summary>
|
||||||
|
public bool ProbeIndoorLookup
|
||||||
|
{
|
||||||
|
get => RenderingDiagnostics.ProbeIndoorLookupEnabled;
|
||||||
|
set => RenderingDiagnostics.ProbeIndoorLookupEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runtime mirror of <c>RenderingDiagnostics.ProbeIndoorUploadEnabled</c>
|
||||||
|
/// (env var <c>ACDREAM_PROBE_INDOOR_UPLOAD</c>).
|
||||||
|
/// </summary>
|
||||||
|
public bool ProbeIndoorUpload
|
||||||
|
{
|
||||||
|
get => RenderingDiagnostics.ProbeIndoorUploadEnabled;
|
||||||
|
set => RenderingDiagnostics.ProbeIndoorUploadEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runtime mirror of <c>RenderingDiagnostics.ProbeIndoorXformEnabled</c>
|
||||||
|
/// (env var <c>ACDREAM_PROBE_INDOOR_XFORM</c>).
|
||||||
|
/// </summary>
|
||||||
|
public bool ProbeIndoorXform
|
||||||
|
{
|
||||||
|
get => RenderingDiagnostics.ProbeIndoorXformEnabled;
|
||||||
|
set => RenderingDiagnostics.ProbeIndoorXformEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runtime mirror of <c>RenderingDiagnostics.ProbeIndoorCullEnabled</c>
|
||||||
|
/// (env var <c>ACDREAM_PROBE_INDOOR_CULL</c>).
|
||||||
|
/// </summary>
|
||||||
|
public bool ProbeIndoorCull
|
||||||
|
{
|
||||||
|
get => RenderingDiagnostics.ProbeIndoorCullEnabled;
|
||||||
|
set => RenderingDiagnostics.ProbeIndoorCullEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runtime mirror of <c>RenderingDiagnostics.IndoorAll</c> — toggles all
|
||||||
|
/// five indoor probes together. No dedicated env var; set any individual
|
||||||
|
/// probe env var or use <c>ACDREAM_PROBE_INDOOR_ALL</c> to initialize
|
||||||
|
/// all five flags on at startup.
|
||||||
|
/// </summary>
|
||||||
|
public bool ProbeIndoorAll
|
||||||
|
{
|
||||||
|
get => RenderingDiagnostics.IndoorAll;
|
||||||
|
set => RenderingDiagnostics.IndoorAll = value;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Chase camera tunables (forward to CameraDiagnostics) ──────────
|
// ── Chase camera tunables (forward to CameraDiagnostics) ──────────
|
||||||
|
|
||||||
/// <summary>Runtime mirror of <see cref="CameraDiagnostics.UseRetailChaseCamera"/>.</summary>
|
/// <summary>Runtime mirror of <see cref="CameraDiagnostics.UseRetailChaseCamera"/>.</summary>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
using AcDream.Core.Rendering;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Tests.Rendering;
|
||||||
|
|
||||||
|
public sealed class RenderingDiagnosticsTests
|
||||||
|
{
|
||||||
|
// Each flag-mutating test snapshots the IndoorAll state on entry and
|
||||||
|
// restores it via try/finally. RenderingDiagnostics is a process-wide
|
||||||
|
// static (env-var-initialized); without restoration a mutated state
|
||||||
|
// leaks into other tests + into parallel test runs. Mirrors the
|
||||||
|
// PhysicsDiagnosticsTests pattern at line 30-49.
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IndoorAll_True_TurnsAllFlagsOn()
|
||||||
|
{
|
||||||
|
bool initial = RenderingDiagnostics.IndoorAll;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Reset all flags off first to make the test deterministic
|
||||||
|
// regardless of env-var state on the test runner.
|
||||||
|
RenderingDiagnostics.ProbeIndoorWalkEnabled = false;
|
||||||
|
RenderingDiagnostics.ProbeIndoorLookupEnabled = false;
|
||||||
|
RenderingDiagnostics.ProbeIndoorUploadEnabled = false;
|
||||||
|
RenderingDiagnostics.ProbeIndoorXformEnabled = false;
|
||||||
|
RenderingDiagnostics.ProbeIndoorCullEnabled = false;
|
||||||
|
|
||||||
|
RenderingDiagnostics.IndoorAll = true;
|
||||||
|
|
||||||
|
Assert.True(RenderingDiagnostics.ProbeIndoorWalkEnabled);
|
||||||
|
Assert.True(RenderingDiagnostics.ProbeIndoorLookupEnabled);
|
||||||
|
Assert.True(RenderingDiagnostics.ProbeIndoorUploadEnabled);
|
||||||
|
Assert.True(RenderingDiagnostics.ProbeIndoorXformEnabled);
|
||||||
|
Assert.True(RenderingDiagnostics.ProbeIndoorCullEnabled);
|
||||||
|
Assert.True(RenderingDiagnostics.IndoorAll);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
RenderingDiagnostics.IndoorAll = initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IndoorAll_False_TurnsAllFlagsOff()
|
||||||
|
{
|
||||||
|
bool initial = RenderingDiagnostics.IndoorAll;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RenderingDiagnostics.IndoorAll = true; // start from all-on
|
||||||
|
RenderingDiagnostics.IndoorAll = false;
|
||||||
|
|
||||||
|
Assert.False(RenderingDiagnostics.ProbeIndoorWalkEnabled);
|
||||||
|
Assert.False(RenderingDiagnostics.ProbeIndoorLookupEnabled);
|
||||||
|
Assert.False(RenderingDiagnostics.ProbeIndoorUploadEnabled);
|
||||||
|
Assert.False(RenderingDiagnostics.ProbeIndoorXformEnabled);
|
||||||
|
Assert.False(RenderingDiagnostics.ProbeIndoorCullEnabled);
|
||||||
|
Assert.False(RenderingDiagnostics.IndoorAll);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
RenderingDiagnostics.IndoorAll = initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IndoorAll_OneOff_ReadsAsFalse()
|
||||||
|
{
|
||||||
|
bool initial = RenderingDiagnostics.IndoorAll;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RenderingDiagnostics.IndoorAll = true;
|
||||||
|
RenderingDiagnostics.ProbeIndoorCullEnabled = false; // flip one off
|
||||||
|
Assert.False(RenderingDiagnostics.IndoorAll);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
RenderingDiagnostics.IndoorAll = initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0x00000029ul, false)] // outdoor cell 0x29 in 8x8 grid
|
||||||
|
[InlineData(0xA9B40029ul, false)] // outdoor cell with landblock prefix
|
||||||
|
[InlineData(0x00000100ul, true)] // indoor cell minimum
|
||||||
|
[InlineData(0x00000105ul, true)] // typical Holtburg Inn interior
|
||||||
|
[InlineData(0xA9B40105ul, true)] // indoor with landblock prefix
|
||||||
|
[InlineData(0xA9B401FFul, true)] // indoor near top of range
|
||||||
|
public void IsEnvCellId_DistinguishesOutdoorVsIndoorByLow16Bits(ulong id, bool expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, RenderingDiagnostics.IsEnvCellId(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue