feat(render): Phase A8 — indoor visibility + streaming fixes batch
Lands the working A8 indoor-rendering and streaming fixes accumulated this session. User has verified these visually to some degree (e.g. lifestone / translucent meshes confirmed fine under the FrontFace flip; bridge / wall / collision regressions confirmed fixed after travel); not every path has been exhaustively gated. The cellar-flap defect remains OPEN and will be solved the retail-faithful way via a dedicated brainstorm (see handoff docs). Rendering core (reviewed, high confidence): - EnvCellRenderer SSBO stride fix: upload packed Matrix4x4[] (64B) instead of the 80B CPU InstanceData struct the shader never expected — fixes the transform/texture "explosion" for any draw with >1 instance (cells that dedupe to a shared cellGeomId). Real root cause. - WB-style global FrontFace(CW) + per-batch CullMode carried through the MDI layout (GroupKey + BuildIndirectArrays + DrawIndirectRange split into same-cull runs with absolute uDrawIDOffset per run). - EntitySet partitioning (IndoorPass / OutdoorScenery / LiveDynamic) + WorldEntity.BuildingShellAnchorCellId so building shells scope to their dat-derived building cell instead of rendering everywhere. - RenderOutsideInAcdream (look into buildings from outside) + CollectVisiblePortalBuildings frustum cull of portal bounds. - Sky-when-inside-building + per-cell audit probe + GL-state probe. Streaming / perf (test-covered; not independently code-reviewed this session): - Near/far priority queues so near work wins over far; PromoteToNear carries full landblock + mesh data; LandblockEntriesWithoutAnimatedIndex avoids rebuilding the animated-lookup dict in the hot draw path. Fixes the bridge-not-appearing / missing-walls / broken-collision-after-travel regressions and improves post-transition FPS. Tooling + docs: - tools/A8CellAudit: offline dat cell/portal/building dumper (portals + buildings modes) — reproduces the cellar-flap investigation with no launch. - docs/research cellar-flap root-cause + option-2 handoff (the didInsideStencil double-duty finding + the WB-recursive design decision + brainstorm prompt), entity-taxonomy, replan, issue-78 visibility investigation. Diagnostics retained on purpose: ACDREAM_A8_DIAG_* gates, portal_stencil.vert provisional pos.w clamp, and the probe families are kept (env-var gated, zero cost when off) because the pending option-2 cellar-flap brainstorm needs them. Strip in the option-2 ship commit. Indoor branch stays behind ACDREAM_A8_INDOOR_BRANCH=1 (default off = pre-A8 visual). Build green; App tests + Core (streaming/dispatcher/loader) tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e415bb3863
commit
5dc4140c11
38 changed files with 3965 additions and 277 deletions
|
|
@ -0,0 +1,206 @@
|
|||
# Issue #78 + cellar-stairs visibility culling — investigation report
|
||||
|
||||
**Date:** 2026-05-25 PM (continuation session)
|
||||
**Status:** REPORT-ONLY. Awaiting user (a) camera-rotation falsification test and (b) approach selection before any code work.
|
||||
**Predecessor handoff:** [`docs/research/2026-05-25-issue-100-shipped-and-culling-handoff.md`](2026-05-25-issue-100-shipped-and-culling-handoff.md)
|
||||
|
||||
---
|
||||
|
||||
## Symptom
|
||||
|
||||
Two visible defects share one root cause:
|
||||
|
||||
1. **Cellar-stairs (observed 2026-05-25 PM, evidence for #78):** standing in a Holtburg cottage cellar with the camera at certain angles, the outdoor terrain mesh renders as a sharp-edged grass rectangle covering the cellar stair geometry. **Clears when camera moves closer** (cottage walls + stair treads geometrically occlude). Gameplay unaffected — player can walk up/down normally.
|
||||
2. **Inn-wall stabs (#78, filed 2026-05-19):** standing inside the Holtburg Inn looking at the floor or walls, the user sees other buildings in the distance at their correct world position + scale, visible THROUGH the floor and walls.
|
||||
|
||||
The user has NOT yet run the camera-rotation falsification test (Phase 1a of the handoff). Until they do, the diagnosis below is "high confidence" but not certain.
|
||||
|
||||
Sibling: **#95** (dungeon portal-graph blowup) is the same visibility subsystem but a different specific failure (over-inclusion). scen5 log shows `visibleCells` per cell reaching **295** (worse than the 135-145 filed).
|
||||
|
||||
---
|
||||
|
||||
## Hypotheses (ranked)
|
||||
|
||||
### H1 — Indoor-camera gate missing on outdoor render passes (HIGH confidence)
|
||||
|
||||
**Mechanism:** `TerrainModernRenderer.Draw` and `WbDrawDispatcher` render outdoor geometry unconditionally regardless of whether the camera is inside an EnvCell. Retail and WorldBuilder both gate the outdoor passes by the indoor portal-walk result. acdream does neither.
|
||||
|
||||
**Evidence FOR (strong):**
|
||||
- Retail anchor verified: `PView::DrawCells` at `acclient_2013_pseudo_c.txt:432709` gates `LScape::draw` (outdoor terrain dispatch) by `if (outside_view.view_count > 0)`. `outside_view.view_count` is only incremented during the indoor portal BFS (`PView::ConstructView`) when a portal targets `other_cell_id == 0xFFFFFFFF` (outdoor sentinel). When no portal sees outside, the entire outdoor pass is skipped.
|
||||
- Retail's per-mesh draw (`RenderDeviceD3D::DrawMesh` line 429245) iterates `Render::PortalList->view_count` and skips meshes that straddle 0 sub-views. **No stencil** — retail uses screen-space polygon clipping via `PView::GetClip`.
|
||||
- WB anchor verified: `VisibilityManager.RenderInsideOut` (lines 73-239) uses **stencil**: mark current-building portals stencil=1, punch portal regions to far depth, draw EnvCells unconditionally, then `terrain/scenery/statics` gated by `glStencilFunc(Equal, 1, 0x01)`. The top-level loop already skips the unconditional terrain draw via `if (!isInside) terrainManager.Render(...)` at GameScene.cs:965.
|
||||
- acdream audit verified the gate is missing: `WbDrawDispatcher.cs:360-362` gates by `entity.ParentCellId.HasValue && !visibleCellIds.Contains(...)`. When `ParentCellId == null` (outdoor stabs, scenery, live-spawned entities), the boolean short-circuits to `cellInVis = true` — the entity passes regardless of `visibleCellIds`.
|
||||
- `TerrainModernRenderer.Draw` (lines 191-208) only does per-slot frustum cull. No `visibleCellIds` parameter, no indoor-camera awareness.
|
||||
- Patch geometry size (~24 m × 24 m rectangle) matches a terrain cell footprint — that's a polygon, not a precision artifact.
|
||||
- "Clears when closer" matches geometric occlusion: cottage walls + stair treads come to occlude the offending terrain cells screen-space as the camera approaches. A 1 cm depth-buffer Z-fight (#100's nudge) at 2-5 m camera distance with 24-bit depth has sub-millimeter resolving power; precision is not the bottleneck.
|
||||
|
||||
**Evidence AGAINST:**
|
||||
- User has not yet run the camera-rotation test. If the patch flickers/shimmers when rotating the camera in place, the diagnosis pivots to Z-precision.
|
||||
|
||||
**How to falsify:** Stand at the spot showing the cellar-stairs artifact, look at the grass patch, rotate the camera slowly without moving the character. Polygon-stable edges that track predictably with the view = culling (H1). Flickering / shimmering = Z-precision (H2).
|
||||
|
||||
### H2 — Residual Z-fight from #100's nudge (LOW confidence)
|
||||
|
||||
The 1 cm shader nudge from issue #100 might be insufficient at certain Z values or with shader precision quirks.
|
||||
|
||||
**Evidence FOR:** Same code area was just touched.
|
||||
**Evidence AGAINST:** Predecessor research already established 1 cm @ 24-bit depth has sub-mm resolving at gameplay camera distances. Patch is rectangular polygon, not thin Z-fight strip. "Clears when closer" reverses precision direction.
|
||||
**How to falsify:** Same camera-rotation test.
|
||||
|
||||
### H3 — #95 portal-traversal blowup is independent of H1 (HIGH confidence it IS independent)
|
||||
|
||||
**Mechanism:** `CellVisibility.GetVisibleCells` BFS over portals lacks termination/cap-depth logic. Network hubs expose 100+ outbound portals to disconnected dungeons, all marked visible. scen5 log shows up to 295 cells in one visible set.
|
||||
|
||||
**Evidence FOR independence:**
|
||||
- H1 is an **asymmetric over-render** (outdoor passes ignore indoor state).
|
||||
- H3 is a **symmetric over-inclusion** (BFS doesn't terminate properly).
|
||||
- A fix to H1 would gate WHEN to render outdoor; H3's fix is to bound WHICH indoor cells the BFS includes.
|
||||
- Different code paths: H1 lives in `TerrainModernRenderer.Draw` + `WbDrawDispatcher`; H3 lives in `CellVisibility.GetVisibleCells`.
|
||||
|
||||
**Conclusion:** H1 and H3 should be **separate fixes**. Closing H1 will close cellar-stairs + the outdoor-stab side of #78 but NOT close #95. The next phase should plan H1 in scope and decide whether H3 fits in the same milestone (M1.5).
|
||||
|
||||
---
|
||||
|
||||
## What we've ruled out
|
||||
|
||||
- **It's not the #100 cell-collapse bug returning.** `hiddenTerrainCells` plumbing was fully removed in `a64e6f2`; terrain mesh now correctly renders everywhere on the landblock per retail. The new artifact's mechanism is "outdoor geometry visible at all when indoor," not "incorrect terrain mesh shape."
|
||||
- **It's not a depth-precision issue (high confidence, pending falsification).** Patch shape + "clears closer" both contradict Z-fight.
|
||||
- **It's not a `ParentCellId` propagation bug.** Audit confirmed that interior cell static objects (`GameWindow.BuildInteriorEntitiesForStreaming:5476`) and cell-mesh entities (line 5416) both receive non-null `ParentCellId = envCellId`. The dispatcher's existing filter already correctly culls them when the camera is in a different building. The bug is the OPPOSITE direction (outdoor entities w/ `ParentCellId == null` always pass).
|
||||
- **It's not WB extraction divergence.** Phase O extracted ~33 WB files into `src/AcDream.App/Rendering/Wb/` but the `VisibilityManager` / `RenderInsideOut` pipeline was NOT extracted — that code never existed in our tree.
|
||||
- **It's not a missing camera-cell signal at the render layer.** `cameraInsideCell`, `visibility.VisibleCellIds`, and `visibility.HasExitPortalVisible` are all already computed in `GameWindow.cs:6970-6984` and live in scope at the two `Draw` call sites (lines 7074 + 7110). No new plumbing required.
|
||||
|
||||
---
|
||||
|
||||
## Approach options for the fix
|
||||
|
||||
Three viable approaches, with tradeoffs:
|
||||
|
||||
### Approach A — WB-style stencil (recommended for first ship)
|
||||
|
||||
Port `VisibilityManager.RenderInsideOut`'s stencil pipeline to acdream. Two-pass render: (1) mark current-building portal silhouettes in stencil, (2) gate outdoor passes by `glStencilFunc(Equal, 1, 0x01)`.
|
||||
|
||||
**Pros:**
|
||||
- Closest to acdream's existing modern GL pipeline (we already use stencil for nothing else; adding one stencil bit is cheap).
|
||||
- WB is acdream's documented rendering base (per CLAUDE.md). Cross-reference checked against retail confirms WB's intent matches retail's, just via a different mechanism.
|
||||
- Handles the "see outside through open door" case correctly — terrain renders through portal silhouettes only.
|
||||
- Reusable for both outdoor terrain AND outdoor entities (single stencil gate applies to all subsequent draws).
|
||||
|
||||
**Cons:**
|
||||
- Multi-pass render adds GPU cost (small — one stencil pass per current-building's portals).
|
||||
- Requires a portal-mesh upload pipeline (WB has one in `PortalRenderManager.cs:488-628`; we'd port it).
|
||||
- More LOC than Approach C.
|
||||
|
||||
**Estimated scope:** 4-6 tasks, 1-2 weeks of implementation + verification.
|
||||
|
||||
### Approach B — Retail-faithful polygon-clip sub-views
|
||||
|
||||
Port `PView::ConstructView` + `PView::GetClip` + `Render::PortalList` from retail. Per-mesh viewport set to clipped portal polygon.
|
||||
|
||||
**Pros:**
|
||||
- 100% retail-faithful.
|
||||
|
||||
**Cons:**
|
||||
- Requires per-draw viewport scissor changes — current rendering uses bindless + MDI with one viewport per pass. Wedging per-mesh viewport in would break the modern pipeline's batching.
|
||||
- Multi-week port. Out of scope for one session.
|
||||
|
||||
**Estimated scope:** 8-12 tasks, 4-6 weeks. Defer to a future milestone if needed.
|
||||
|
||||
### Approach C — Ship-now binary gate
|
||||
|
||||
When `cameraInsideCell && !visibility.HasExitPortalVisible`, skip outdoor terrain pass entirely and gate `WbDrawDispatcher` to exclude `ParentCellId == null` entities.
|
||||
|
||||
**Pros:**
|
||||
- Smallest change. ~2-3 tasks. Closes the cellar-stairs symptom and the sealed-interior side of #78 immediately.
|
||||
- All required state already computed (`HasExitPortalVisible` from `CellVisibility.GetVisibleCells` line 404).
|
||||
|
||||
**Cons:**
|
||||
- Under-renders when player can see outside through an open door/window (renders nothing instead of clipping correctly). This is regressive vs. today for the doorway-view case.
|
||||
- Per CLAUDE.md "no workarounds": this *is* a symptom-gate rather than a root-cause fix. **Would need explicit user approval.** Approach A is the correct shape; Approach C is a temporary patch.
|
||||
|
||||
**Estimated scope:** 2-3 tasks, 1-2 days.
|
||||
|
||||
---
|
||||
|
||||
## Recommended next step
|
||||
|
||||
1. **User runs the camera-rotation falsification test (~60 seconds).** Spawn at Holtburg, walk into a cottage cellar, find the angle showing the grass patch, rotate the camera in place without moving. Report what happens.
|
||||
- Polygon-stable → confirms H1, proceed.
|
||||
- Flickering → pivots to H2, this report needs major revision.
|
||||
|
||||
2. **If H1 confirmed: user picks Approach A vs C.** Recommendation: **Approach A (WB-style stencil)**. Per CLAUDE.md's "no workarounds" rule, the right thing is to port the stencil pipeline, not gate at the symptom site. Approach C is offered only if the user wants to close cellar-stairs immediately and defer doorway-view correctness as known-incomplete; that's an explicit workaround that needs user sign-off.
|
||||
|
||||
3. **#95 should NOT be in scope for this work.** Different mechanism, different code path. File continues as separate work in M1.5.
|
||||
|
||||
4. **Phase identifier:** the handoff proposes A8 (visibility) alongside A6 (physics) and A7 (lighting). I'll defer naming to the user.
|
||||
|
||||
5. **CLAUDE.md update for #100 ship:** the handoff calls this out as pending. Recommendation: add a brief #100 ship entry mentioning the cellar-stairs finding linked to #78. Out of scope for investigate mode; will happen at the start of the implementation session.
|
||||
|
||||
---
|
||||
|
||||
## What this is NOT
|
||||
|
||||
This is NOT a #100 regression. The terrain Z-nudge ship works correctly; the new artifact has a different root cause (indoor-camera gate on outdoor passes was already missing pre-#100 — #100 just made it more visible by removing the terrain-cell hide mechanism that incidentally masked it inside building footprints).
|
||||
|
||||
This is NOT a depth-precision fix. The 1cm nudge is correctly sized; larger nudges would break coplanar-floor disambiguation elsewhere.
|
||||
|
||||
This is NOT a `ParentCellId` data fix. Interior entities are correctly tagged.
|
||||
|
||||
This is NOT covered by Phase O's WB extraction. The visibility-management code was deliberately NOT extracted.
|
||||
|
||||
---
|
||||
|
||||
## Reference appendix
|
||||
|
||||
### Retail anchors (acclient_2013_pseudo_c.txt)
|
||||
|
||||
| Line | Symbol | Role |
|
||||
|---|---|---|
|
||||
| 92635 | `SmartBox::RenderNormalMode` | Per-frame top-level dispatcher (indoor vs outdoor branch) |
|
||||
| 267912 | `LScape::draw` | Outdoor terrain dispatch |
|
||||
| 311397 | `CEnvCell::find_visible_child_cell` | Point-in-visible-cell query |
|
||||
| 311878 | `CEnvCell::grab_visible_cells` | Loads outdoor on `seen_outside` |
|
||||
| 427843 | `RenderDeviceD3D::DrawInside` | Indoor entry point |
|
||||
| 429245 | `RenderDeviceD3D::DrawMesh` | **Per-mesh portal-sub-view loop** |
|
||||
| 430027 | `RenderDeviceD3D::DrawBlock` | Outdoor landblock dispatch |
|
||||
| 432709 | **`PView::DrawCells`** | **The `outside_view.view_count > 0` gate** |
|
||||
| 433750 | `PView::ConstructView` | BFS portal walk |
|
||||
|
||||
### WorldBuilder anchors
|
||||
|
||||
| File:Line | Role |
|
||||
|---|---|
|
||||
| `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilityManager.cs:73-239` | `RenderInsideOut` — full stencil pipeline |
|
||||
| Same file:241-359 | `RenderOutsideIn` — outdoor branch |
|
||||
| Same file:47-71 | `PrepareVisibility` — visible cell set |
|
||||
| `references/WorldBuilder/Chorizite.OpenGLSDLBackend/GameScene.cs:880-1008` | Main render dispatch (lines 965, 988 are the gates) |
|
||||
| `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/PortalRenderManager.cs:488-628` | Portal mesh upload |
|
||||
| `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/CameraController.cs:142-174` | Camera-cell tracking (portal raycasts) |
|
||||
| `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Shaders/PortalStencil.frag:7-16` | Stencil shader (writes `gl_FragDepth = 1.0`) |
|
||||
|
||||
### acdream extension points (audit-verified)
|
||||
|
||||
| File:Line | Current behavior | Extension required |
|
||||
|---|---|---|
|
||||
| `src/AcDream.App/Rendering/CellVisibility.cs:222-232` | Returns `VisibilityResult` with `VisibleCellIds`, `HasExitPortalVisible`, `CameraCell` | None — state already in place |
|
||||
| `src/AcDream.App/Rendering/GameWindow.cs:6970-6984` | Computes `cameraInsideCell` and `playerInsideCell` per frame | None — values already in scope |
|
||||
| `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs:360-374` | Gates by `ParentCellId ∈ visibleCellIds`; outdoor entities (null) always pass | Add second gate: when `cameraInsideCell == true` and entity is outdoor (`ParentCellId == null`), require stencil pass or skip entirely |
|
||||
| `src/AcDream.App/Rendering/TerrainModernRenderer.cs:191-208` | Frustum-only cull; renders all loaded landblocks | Add parameter for stencil pass / indoor-camera state |
|
||||
| `src/AcDream.App/Rendering/GameWindow.cs:7074` | `_terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb)` | Add `cameraInsideCell` (or equivalent) parameter |
|
||||
| `src/AcDream.App/Rendering/GameWindow.cs:7110` | `WbDrawDispatcher.Draw(... visibleCellIds: visibility?.VisibleCellIds, ...)` | Add `cameraInsideCell` parameter |
|
||||
| `src/AcDream.Core/Rendering/RenderingDiagnostics.cs:75-77` | Existing probe flag registry (mirror of `PhysicsDiagnostics`) | Add `ProbeVisibilityEnabled` from `ACDREAM_PROBE_VIS=1` |
|
||||
|
||||
### Issues family map
|
||||
|
||||
| ID | Symptom | Closes with H1 fix? |
|
||||
|---|---|---|
|
||||
| #78 | Outdoor stabs visible through inn floor/walls | YES (same root cause) |
|
||||
| Cellar-stairs (NEW) | Outdoor terrain visible inside cottage cellar | YES (same root cause; new evidence for #78) |
|
||||
| #95 | Portal-graph visibility blowup (visibleCells up to 295) | NO — independent (different code path) |
|
||||
| #79/#80/#81/#93/#94 | Indoor lighting bugs | Maybe — #93 explicitly suspects "indoor visibility culling for lights" sub-cause; lighting subsystem may share infrastructure with visibility-gate but not directly impacted |
|
||||
|
||||
### Workflow notes (from CLAUDE.md "How to operate")
|
||||
|
||||
- "No workarounds without explicit approval" — Approach C is a workaround; Approach A is the correct shape.
|
||||
- Visual verification is the user's job; can't be automated.
|
||||
- Phase ID for visibility work is undecided. User picks at implementation-session start.
|
||||
- Per the milestones doc, this is M1.5 scope; cellar-stairs is on the M1.5 critical path because it blocks the building/cellar half of the M1.5 demo.
|
||||
137
docs/research/2026-05-26-a8-entity-taxonomy.md
Normal file
137
docs/research/2026-05-26-a8-entity-taxonomy.md
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# Phase A8 re-plan — entity taxonomy investigation
|
||||
|
||||
**Date:** 2026-05-26
|
||||
**Phase:** A8 — Indoor-cell visibility culling RE-PLAN
|
||||
**Predecessor handoff:** [docs/research/2026-05-26-a8-revert-handoff.md](2026-05-26-a8-revert-handoff.md)
|
||||
**Status:** Report-only. Awaiting user approval of recommended fix-shape before Phase 2 (plan writing).
|
||||
**Empirical context (added during investigation):** the bug exists on `main` too — verified by side-by-side launch of `main` vs `HEAD = fef6c61`. Both branches show outdoor buildings/terrain visible through the walls of a cottage when standing inside. The bug is **fundamental**, not a regression in this worktree's 149-commit divergence. The A8 framing in the predecessor handoff stands.
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
The retail data model, WorldBuilder's data model, and the comment at `GameWindow.cs:5175-5178` all agree on a single architectural fact: **building shells are tagged distinctly from outdoor scenery at the data layer.** acdream's `LandblockLoader` reads both `LandBlockInfo.Objects` (scenery) and `LandBlockInfo.Buildings` (shells) into the same `WorldEntity` pool with no tag, destroying the distinction. The fix is to add `WorldEntity.IsBuildingShell: bool` at the loader, propagate it through hydration, and use it in the `WbDrawDispatcher.EntitySet` partition. This is **retail-faithful** (matches `BuildInfo` array) and **WB-faithful** (matches `SceneryInstance.IsBuilding`).
|
||||
|
||||
GL state order from the A8 Round 3 learning (MarkAndPunch BEFORE indoor draw) is confirmed correct by reading WorldBuilder's `VisibilityManager.RenderInsideOut`.
|
||||
|
||||
Far-side-portal (WB "Step 5", 3-stencil-bit) is deferred. First-ship approximation: only stencil-mark the **camera's own cell's** portals, not BFS-extended `VisibleCellIds`.
|
||||
|
||||
---
|
||||
|
||||
## The seven entity classes in acdream's runtime
|
||||
|
||||
| # | Class | `ParentCellId` | `Id` prefix | `ServerGuid` | Source field |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | Cell mesh | set | `0x40xxxxxx` | 0 | `EnvCell.EnvironmentId` |
|
||||
| 2 | Cell static object | set | `0x40xxxxxx` | 0 | `EnvCell.StaticObjects` |
|
||||
| 3 | **Building shell stab** | **null** | `0xC0xxxxxx` | 0 | **`LandBlockInfo.Buildings`** |
|
||||
| 4 | **Outdoor scenery stab** | **null** | `0xC0xxxxxx` | 0 | **`LandBlockInfo.Objects`** |
|
||||
| 5 | Procedural scenery | null | `0x80xxxxxx` | 0 | `SceneryGenerator` (terrain table) |
|
||||
| 6a | Live animated | null | `0x10xxxxxx` | ≠0 | `CreateObject` packet |
|
||||
| 6b | Live static | null | `0x10xxxxxx` | ≠0 | `CreateObject` packet |
|
||||
|
||||
**Classes 3 and 4 are indistinguishable at runtime today** (identical field shape after hydration). This is the load-bearing wrong assumption from the A8 attempt.
|
||||
|
||||
### Code anchors (acdream)
|
||||
- `src/AcDream.Core/World/LandblockLoader.cs:62-71` — Objects (Class 4) loop
|
||||
- `src/AcDream.Core/World/LandblockLoader.cs:74-87` — Buildings (Class 3) loop, **same `nextId++` counter, same WorldEntity shape**
|
||||
- `src/AcDream.App/Rendering/GameWindow.cs:5129-5137` — hydration pass-through, no distinction preserved
|
||||
- `src/AcDream.App/Rendering/GameWindow.cs:5175-5178` — the comment that proves the distinction is intentional in dat:
|
||||
> *"Only Buildings suppress scenery. Stabs (LandBlockInfo.Objects) are static scenery placeholders themselves (rocks, tree clusters) that retail does NOT use to suppress scenery generation."*
|
||||
|
||||
---
|
||||
|
||||
## How retail tags buildings (cross-reference 1)
|
||||
|
||||
`CLandBlock::init_buildings` (`acclient_2013_pseudo_c.txt:313854-313920`) reads `CLandBlockInfo::buildings[]` — a **separate `BuildInfo**` array**, NOT a flag bit or ID-range scheme.
|
||||
|
||||
- `CLandBlockInfo.num_buildings` + `buildings[]` array (`acclient.h:31893-31905`)
|
||||
- `BuildInfo` struct: `building_id`, `building_frame`, `num_portals`, `CBldPortal** portals` (`acclient.h:32035-32042`)
|
||||
- Buildings hydrate via `CBuildingObj::makeBuilding()` (line 313879) and register into the landblock's `stablist[]` (per-landblock visible-cell set, line 313910)
|
||||
- Visibility uses **stablist (portal PVS)**, NOT AABB-encloses-camera. `CEnvCell::grab_visible` walks `stab_list[i]` directly.
|
||||
|
||||
Conclusion: **retail explicitly distinguishes the two via separate dat arrays.** This is the data-model truth we should match.
|
||||
|
||||
## How WorldBuilder tags buildings (cross-reference 2)
|
||||
|
||||
WB uses **two manager classes** sharing one mesh pool:
|
||||
- `StaticObjectRenderManager` — handles BOTH `LandBlockInfo.Objects` and `LandBlockInfo.Buildings`, tagging each `SceneryInstance.IsBuilding` (`StaticObjectRenderManager.cs:334-400`).
|
||||
- `SceneryRenderManager` — handles ONLY procedural terrain-derived scenery (different class entirely, doesn't share the dat path).
|
||||
|
||||
Tagging happens at **hydration time** in `GenerateForLandblockAsync` (lines 315-427). The instance is then split into separate `StaticPartGroups` vs `BuildingPartGroups` for draw dispatch.
|
||||
|
||||
`BuildingPortalGPU` (`PortalRenderManager.cs:687-701`) holds `EnvCellIds: HashSet<uint>` populated at landblock generation (line 549) — the "this building contains these EnvCells" association. The set is **never re-computed at render time**.
|
||||
|
||||
WB's `RenderInsideOut` GL state order (`VisibilityManager.cs:73-239`):
|
||||
1. Stencil bit 1 ← portal polygons (color/depth masks off)
|
||||
2. `gl_FragDepth = 1.0` ← portal polygons (depth mask on, depth-func = Always)
|
||||
3. **Interior EnvCells render WITHOUT stencil restriction** ← key step
|
||||
4. Stencil-restricted (`Equal, 1`): terrain + scenery + buildings render only at portal silhouettes
|
||||
5. (Step 5) 3-stencil-bit pipeline for cross-building visibility — DEFER
|
||||
|
||||
**WB's order = MarkAndPunch (Step 1 + 2) FIRST, then indoor cells (Step 3).** This matches A8 Round 3's correction. The handoff's GL-state-order conclusion stands.
|
||||
|
||||
---
|
||||
|
||||
## Recommended fix-shape (synthesized)
|
||||
|
||||
### Stage 1: Tag at hydration (`IsBuildingShell` flag)
|
||||
|
||||
Add `WorldEntity.IsBuildingShell: bool` (default false). In `LandblockLoader.cs`:
|
||||
- Objects loop (line 62): `IsBuildingShell = false`
|
||||
- Buildings loop (line 74): `IsBuildingShell = true`
|
||||
|
||||
In `GameWindow.cs:5129-5137` (hydration): copy `IsBuildingShell` from `e` to the hydrated entity. One-line change.
|
||||
|
||||
### Stage 2: Refine `WbDrawDispatcher.EntitySet` partition
|
||||
|
||||
Replace today's binary `IndoorOnly`/`OutdoorOnly` with:
|
||||
- `IndoorPass` — `ParentCellId.HasValue || IsBuildingShell` (Classes 1, 2, 3)
|
||||
- `OutdoorScenery` — `!ParentCellId.HasValue && !IsBuildingShell && (ServerGuid == 0)` (Classes 4, 5)
|
||||
- `LiveDynamic` — `ServerGuid != 0` (Classes 6a, 6b)
|
||||
|
||||
`WalkEntitiesInto` updates one branch (the partition predicate). 26 dispatcher tests will need their fixture entities tagged correctly; otherwise behavior is the same.
|
||||
|
||||
### Stage 3: Re-wire render frame with WB's order
|
||||
|
||||
When camera is inside a cell:
|
||||
1. Draw terrain (color in framebuffer)
|
||||
2. **MarkAndPunch** (stencil = 1 + depth = 1.0 at portal silhouettes)
|
||||
3. `WbDrawDispatcher.Draw(set: IndoorPass)` — cell mesh + cell statics + building shells. Stencil disabled, depth test normal. These write depth ON TOP of the 1.0 punch, correctly occluding the next stencil-gated pass.
|
||||
4. Re-draw terrain (color writes only) with `StencilFunc(Equal, 1)` — terrain visible only at portal silhouettes.
|
||||
5. `WbDrawDispatcher.Draw(set: OutdoorScenery)` with `StencilFunc(Equal, 1)` — outdoor scenery visible only at portal silhouettes.
|
||||
6. `WbDrawDispatcher.Draw(set: LiveDynamic)` — stencil disabled, depth test on. Live entities draw freely; depth occludes them by walls and cell meshes already in the depth buffer.
|
||||
|
||||
When camera is outside: stencil work skipped entirely. Today's all-entities single draw stands (or substitute the three EntitySet calls with stencil disabled — depth still sorts them correctly).
|
||||
|
||||
### Stage 4: Far-side-portal approximation (defer Step 5)
|
||||
|
||||
Stencil-mark **only the camera's own cell's portals** in Step 2, not the BFS-extended `VisibleCellIds`. This trades cross-cell-portal visibility (rare visually) for correctness in the common case (no "see-through-wall on the other side of the room"). Track as a known limitation; revisit if visual gate flags it.
|
||||
|
||||
---
|
||||
|
||||
## Reasons for confidence
|
||||
|
||||
1. **Triple-cited**: retail (`BuildInfo` array), WB (`IsBuilding` flag), acdream's own code comment (5175-5178) all agree on the distinction.
|
||||
2. **Tagging cost is microscopic** — one bool on `WorldEntity`, one branch in `LandblockLoader`. No new types, no new managers, no field migration.
|
||||
3. **`EntitySet` enum is already in place** (dormant from Tasks 1-6). Refactor is reshaping its semantics, not introducing it.
|
||||
4. **GL state order is validated** by both Round 3 of the A8 attempt and WB's reference. No remaining ambiguity.
|
||||
5. **Live-dynamic separation handles the Round 1 character-disappears bug** (handoff §Round 1). They draw last, stencil disabled, depth-tested against everything else.
|
||||
|
||||
## Open questions for user approval
|
||||
|
||||
1. Use `IsBuildingShell` flag (recommended) vs separate `0xC1xxxxxx` ID-namespace? Flag is more explicit, retail-faithful, and trivially greppable. ID-namespace is one less field but invisible at the call site.
|
||||
2. Defer Step 5 (far-side portals) and stencil-mark only camera's own cell? Recommendation: yes — ship simple, file follow-up.
|
||||
3. Live-dynamic entities (Class 6b: dropped items) — draw in `LivePass` or accept "invisible from inside" until a richer flag exists? Recommendation: `LivePass`. They're rare visually, and the player benefits from seeing dropped items through the floor (gameplay nicety, not retail violation).
|
||||
4. Cellar-stairs grass overlay from OUTSIDE: NOT A8 scope (no stencil runs when camera is outside). Open question for a future "deep-cell terrain occlusion" phase. Confirm we file this separately, not bundled.
|
||||
|
||||
---
|
||||
|
||||
## Reference anchors (still valid from predecessor handoff)
|
||||
|
||||
- WB stencil: `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilityManager.cs:73-239`
|
||||
- WB building-cell association: `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/PortalRenderManager.cs:518-551`
|
||||
- Retail building init: `docs/research/named-retail/acclient_2013_pseudo_c.txt:313854-313920`
|
||||
- Retail building struct: `docs/research/named-retail/acclient.h:31893-31905`, `:32035-32042`, `:32094-32103`
|
||||
- acdream LandblockLoader: `src/AcDream.Core/World/LandblockLoader.cs:62-87`
|
||||
- acdream hydration: `src/AcDream.App/Rendering/GameWindow.cs:5093-5148`, `:5175-5178`
|
||||
168
docs/research/2026-05-28-a8-cellar-flap-handoff.md
Normal file
168
docs/research/2026-05-28-a8-cellar-flap-handoff.md
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
# Phase A8 Cellar-Flap Handoff - 2026-05-28
|
||||
|
||||
## Status
|
||||
|
||||
The remaining cottage/cellar artifact is still visible. The user sees a short-lived green terrain-like rectangle/flap over the cellar entrance or floor opening when entering/exiting a building and when the chase camera angle changes.
|
||||
|
||||
This thread hit the project rule: 3 visual-gated fixes failed for this specific artifact. Stop here. Do not ship a fourth speculative renderer change. The next step must be source-provenance instrumentation or an architecture comparison.
|
||||
|
||||
The client was stopped after the failed visual gate to avoid build/file locks.
|
||||
|
||||
## Last Good Context
|
||||
|
||||
The broader A8 indoor renderer is much improved and shippable aside from this artifact:
|
||||
|
||||
- Theory A front-face alignment plus per-batch `CullMode` fixed major missing/inside-out geometry.
|
||||
- The `InstanceData` stride fix explained the texture explosion/distortion root cause.
|
||||
- Building shell scoping, portal bounds, streaming/promotion fixes, and FPS fixes remain important.
|
||||
- The current artifact is narrow: a green terrain-like flap at/near cellar entrance transitions.
|
||||
|
||||
## Failed Fixes In This Micro-Loop
|
||||
|
||||
### 1. Camera branch gate relaxation
|
||||
|
||||
Hypothesis: strict `PointInCell` camera-inside-building gating was causing the renderer to switch branches while the chase camera crossed walls/portals.
|
||||
|
||||
Change tried: allow the inside-out branch to continue based on `CameraCell.BuildingId`, without the strict `PointInCell` check.
|
||||
|
||||
Result: visual gate failed. User still saw the artifact. Change was reverted back to strict `PointInCell`.
|
||||
|
||||
Evidence ruled out: the artifact is not solely caused by the render branch dropping out when the camera is slightly outside the cell.
|
||||
|
||||
### 2. Portal stencil `pos.w` clamp
|
||||
|
||||
Hypothesis: near-zero clip W for portal stencil triangles was exploding the screen-space mask as the camera crossed a portal plane.
|
||||
|
||||
Change tried: restored WorldBuilder-style clamp in `src/AcDream.App/Rendering/Shaders/portal_stencil.vert`:
|
||||
|
||||
```glsl
|
||||
vec4 pos = uViewProjection * vec4(aPosition, 1.0);
|
||||
if (abs(pos.w) < 0.001)
|
||||
pos.w = pos.w < 0.0 ? -0.001 : 0.001;
|
||||
gl_Position = pos;
|
||||
```
|
||||
|
||||
Result: visual gate failed. User still saw the artifact.
|
||||
|
||||
Interpretation: this may still be WB parity and may be worth keeping, but it is not the root cause of the cellar flap.
|
||||
|
||||
### 3. Visible-cell portal mask
|
||||
|
||||
Hypothesis: `RenderInsideOutAcdream` was punching outdoor terrain through all exit portals in the camera building, not just exits reached by the current portal traversal. A window/door portal could project over the cellar opening and let terrain draw through it.
|
||||
|
||||
Changes tried:
|
||||
|
||||
- Added `CellVisibility.TryGetCell(uint, out LoadedCell?)`.
|
||||
- Added `IndoorCellStencilPipeline.DrawUploadedPortalMesh(...)` to draw the already-uploaded visible-cell portal mesh.
|
||||
- Changed `RenderInsideOutAcdream` Step 1/2 to upload portal triangles from `visibleCellIds`, not `camBuildings`.
|
||||
- Wrapped Step 4 terrain/outdoor scenery draw inside `if (didInsideStencil)`.
|
||||
- Added pure math test `BuildTriangles_OnlyIncludesProvidedVisibleCells`.
|
||||
|
||||
Verification before launch:
|
||||
|
||||
```powershell
|
||||
dotnet build src\AcDream.App\AcDream.App.csproj -c Debug --no-restore
|
||||
dotnet test tests\AcDream.App.Tests\AcDream.App.Tests.csproj -c Debug --filter FullyQualifiedName~IndoorCellStencilPipelineTests --no-restore
|
||||
```
|
||||
|
||||
Both passed after shutting down build servers. The first parallel test attempt only failed because `VBCSCompiler` locked `AcDream.App.dll` while a parallel build was running.
|
||||
|
||||
Visual result: failed. User still saw the flap.
|
||||
|
||||
Evidence ruled out: the artifact is not explained only by building-wide vs visible-cell exit portal masking.
|
||||
|
||||
## Current Uncommitted Changes From This Micro-Loop
|
||||
|
||||
These are not visually validated as a fix:
|
||||
|
||||
- `src/AcDream.App/Rendering/Shaders/portal_stencil.vert`
|
||||
- WB-style `pos.w` clamp.
|
||||
- `src/AcDream.App/Rendering/CellVisibility.cs`
|
||||
- `TryGetCell`.
|
||||
- `src/AcDream.App/Rendering/IndoorCellStencilPipeline.cs`
|
||||
- `DrawUploadedPortalMesh`.
|
||||
- `src/AcDream.App/Rendering/GameWindow.cs`
|
||||
- visible-cell portal mask in `RenderInsideOutAcdream`.
|
||||
- Step 4 fail-closed when no portal mesh uploaded.
|
||||
- `tests/AcDream.App.Tests/Rendering/IndoorCellStencilPipelineTests.cs`
|
||||
- visible-cell-only triangle generation test.
|
||||
|
||||
Do not commit these as "the fix" unless a later source-provenance check proves they are correct and harmless. If the next architecture route goes elsewhere, revert or split them honestly.
|
||||
|
||||
## What We Know About The Cellar Entrance
|
||||
|
||||
Prior inspection found:
|
||||
|
||||
- The cellar transition around Holtburg cottage cells `0xA9B40143 -> 0x0146 -> 0x0147` is indoor-to-indoor.
|
||||
- The relevant transition portals are not literal `OtherCellId == 0xFFFF` outside exits.
|
||||
- The portal cap polygons inspected there are `NoPos`.
|
||||
- That made "cell mesh uploads the outdoor floor polygon" unlikely, but not proven impossible for the visible green patch.
|
||||
|
||||
## What The Failed Fixes Suggest
|
||||
|
||||
The green pixels may not be caused by the portal mask at all. We need to prove which render pass writes them before touching behavior again.
|
||||
|
||||
Candidate explanations now:
|
||||
|
||||
1. **The green patch is not Step 4 terrain.**
|
||||
It could be an EnvCell surface with the wrong texture/material, stale texture binding, or an indoor static object surface resolving to a grass texture.
|
||||
|
||||
2. **The patch is Step 4 terrain, but the stencil/depth mask source is not the exit portal list.**
|
||||
It may be a stale stencil/depth state leak, a Step 3/4 depth ordering issue, or a missing retail occlusion/scissor lifecycle detail.
|
||||
|
||||
3. **The cellar opening has a missing or late-loaded indoor object.**
|
||||
If a hatch/stair/floor object should cover or occlude that area and is missing for one or more frames, outdoor/terrain pixels behind it become visible.
|
||||
|
||||
4. **Camera clipping is exposing an intentionally hidden surface.**
|
||||
Retail camera collision is incomplete in acdream. If the camera clips through the wall/floor volume, the renderer may be showing a view retail never permits. This does not explain all cases by itself, because the user also sees it during transitions, but it must be separated from a renderer bug.
|
||||
|
||||
## Next Step - Evidence Only
|
||||
|
||||
Add a provenance diagnostic, not a fix. One visual launch should answer what writes the green flap.
|
||||
|
||||
Recommended diagnostic:
|
||||
|
||||
- Add an env-gated pass tint or disable switch for only the inside-out Step 4 terrain draw.
|
||||
- Add a separate tint/disable for Step 4 `OutdoorScenery`.
|
||||
- Add a tint/disable for Step 3 EnvCell opaque.
|
||||
- Keep default behavior unchanged.
|
||||
|
||||
Example env names:
|
||||
|
||||
```text
|
||||
ACDREAM_A8_DIAG_DISABLE_INSIDE_STEP4_TERRAIN=1
|
||||
ACDREAM_A8_DIAG_DISABLE_INSIDE_STEP4_OUTDOOR=1
|
||||
ACDREAM_A8_DIAG_DISABLE_INSIDE_ENVCELL_OPAQUE=1
|
||||
```
|
||||
|
||||
Run only one diagnostic at a time:
|
||||
|
||||
- If disabling Step 4 terrain removes the green flap, the issue is still terrain leaking through the indoor view mask.
|
||||
- If disabling Step 4 terrain does not remove it, stop looking at portal exits and inspect EnvCell/static texture/material assignment for the specific surface.
|
||||
- If disabling EnvCell opaque removes it, dump the exact `CellStruct` polygon/material under the camera/screenshot area.
|
||||
|
||||
Do not ship the diagnostic switches. Strip them after the source is known.
|
||||
|
||||
## Launch/Test Notes
|
||||
|
||||
Last clean visual launch:
|
||||
|
||||
```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_A8_INDOOR_BRANCH = "1"
|
||||
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug
|
||||
```
|
||||
|
||||
The log files from the last launch were empty because this was a clean no-probe run:
|
||||
|
||||
- `a8-visible-cell-portalmask-cellar-flap-20260528-161702.out.log`
|
||||
- `a8-visible-cell-portalmask-cellar-flap-20260528-161702.err.log`
|
||||
|
||||
## Stop Rule
|
||||
|
||||
This artifact has now consumed three visual failures in this micro-loop. Next session should not make another fix until it has hard evidence identifying the pass/polygon/source texture responsible for the green pixels.
|
||||
176
docs/research/2026-05-28-a8-cellar-flap-option2-handoff.md
Normal file
176
docs/research/2026-05-28-a8-cellar-flap-option2-handoff.md
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
# A8 cellar-flap — option-2 handoff + brainstorming kickoff (2026-05-28 PM)
|
||||
|
||||
## Purpose
|
||||
|
||||
The cellar flap is the **last** A8 indoor-rendering defect. Its root cause is
|
||||
fully understood (offline-confirmed). The targeted fix (option 1) was tried,
|
||||
**failed**, and the failure revealed a deeper architectural coupling. The
|
||||
decision is to fix it the **retail-faithful way (option 2: WB-style recursive
|
||||
portal visibility)** via a fresh `superpowers:brainstorming` session. This doc
|
||||
is the single pickup point for that session.
|
||||
|
||||
## Current tree state (do NOT reset)
|
||||
|
||||
- Worktree: `.claude/worktrees/strange-albattani-3fc83c/`, branch
|
||||
`claude/strange-albattani-3fc83c`, tip `e415bb3` — **all A8 work is
|
||||
uncommitted** in a dirty tree.
|
||||
- Build green. App tests pass (90 baseline; the 3 option-1 tests were removed).
|
||||
- The option-1 code (`PortalMeshBuilder.CollectSameLevelPortalCells` +
|
||||
`IsVerticalPortal` + 3 tests + the GameWindow call-site change) has been
|
||||
**reverted/removed** — tree is back to the working-with-cellar-flap baseline.
|
||||
- `tools/A8CellAudit` gained a `portals` mode this session (offline cell/portal
|
||||
dumper) — **kept**, it's the investigation workhorse.
|
||||
|
||||
### What WORKS in the dirty tree (the valuable A8 batch — keep)
|
||||
- EnvCellRenderer SSBO **stride fix** (mat4 upload, not 80-byte InstanceData).
|
||||
- WB-style global `FrontFace(CW)` + per-batch `CullMode` through MDI.
|
||||
- `EntitySet` partitioning (IndoorPass / OutdoorScenery / LiveDynamic) +
|
||||
`BuildingShellAnchorCellId` scoping.
|
||||
- `RenderOutsideInAcdream` (look into buildings from outside).
|
||||
- `CollectVisiblePortalBuildings` frustum cull of portal bounds.
|
||||
- Streaming near/far priority queues + `PromoteToNear` + the
|
||||
`LandblockEntriesWithoutAnimatedIndex` hot-path fix (fixed bridge/wall/collision
|
||||
regressions after travel).
|
||||
- Temporary `ACDREAM_A8_DIAG_*` flags (strip before any commit).
|
||||
|
||||
### What DOESN'T work
|
||||
- **Cellar flap** + the broader inside-out fragility (see the coupling below).
|
||||
|
||||
> **Decision point for the human:** the working A8 batch is large and
|
||||
> uncommitted. Consider committing it (after stripping the `ACDREAM_A8_DIAG_*`
|
||||
> flags) so the option-2 work starts from a clean baseline. Deferred per
|
||||
> "don't commit yet," but flagged.
|
||||
|
||||
## The cellar flap — root cause (confirmed)
|
||||
|
||||
Full evidence: [`docs/research/2026-05-28-a8-cellar-flap-root-cause.md`](2026-05-28-a8-cellar-flap-root-cause.md).
|
||||
|
||||
Short version: the inside-out stencil mask flat-marks the exit portals
|
||||
(windows/doors) of **every** visibility-BFS-reached cell. From the cellar
|
||||
(`0xA9B40171`, **zero** exit portals), the BFS reaches the ground-floor cells
|
||||
(`0x16F`, `0x170`) up the stairwell and marks **their** windows. Step 4 then
|
||||
paints the whole outdoor world through those silhouettes wherever the cellar's
|
||||
stairwell hole leaves them un-occluded. There is no constraint tying a deeper
|
||||
cell's exit portal to the portal chain (the narrow stairwell) it was reached
|
||||
through.
|
||||
|
||||
## ⭐ THE KEY FINDING — `didInsideStencil` double-duty coupling
|
||||
|
||||
This is the expensive lesson; do not re-pay it.
|
||||
|
||||
`RenderInsideOutAcdream` Step 4 (GameWindow.cs ~11167) wraps **both** the
|
||||
terrain draw **and** the entire `OutdoorScenery` dispatcher draw (which includes
|
||||
neighbor **building shells**, scenery, and the depth-repair pass) in:
|
||||
|
||||
```csharp
|
||||
if (didInsideStencil) { ... terrain + OutdoorScenery ... }
|
||||
```
|
||||
|
||||
where `didInsideStencil == (camera-side-filtered exit-portal mask is non-empty)`.
|
||||
|
||||
So the portal mask is doing **two jobs at once**:
|
||||
- **Job A (intended):** gate "paint terrain/sky *through* the portal openings."
|
||||
- **Job B (accidental):** decide "draw exterior geometry (shells/scenery/depth-repair) **at all**."
|
||||
|
||||
**Why option 1 failed:** option 1 correctly shrank the mask (same-level cells
|
||||
only) so the cellar's mask went empty → `didInsideStencil=false` → **Step 4
|
||||
skipped entirely** → exterior shells + terrain vanished → "walls transparent,
|
||||
sky behind, terrain gone." The old flat mask (all visN cells) *papered over*
|
||||
this by almost always keeping the mask non-empty.
|
||||
|
||||
**Consequence for ANY fix:** correctly scoping/clipping the portal mask is not
|
||||
enough on its own — it will empty the mask in legitimate cases (looking at an
|
||||
interior wall, sealed cellar) and kill exterior rendering. **Job A and Job B
|
||||
must be decoupled** so exterior geometry draws regardless of whether any portal
|
||||
is currently visible. This is true for option 2 as much as option 1.
|
||||
|
||||
## Decision: option 2 (WB-faithful recursive portal visibility)
|
||||
|
||||
Chosen over option 1 (decouple-only) because:
|
||||
- The project mandate is faithful WB/retail porting; option 1 is a structural
|
||||
deviation from WB's RenderInsideOut, and prior "cleaner redesign" deviations
|
||||
were reverted.
|
||||
- Option 2 handles every case (cellar, stacked floors, deep dungeons) without
|
||||
per-case special-casing.
|
||||
- It is large enough to deserve design-first (brainstorm), not a mid-session patch.
|
||||
|
||||
Note: option 2 still has to solve the Job-A/Job-B decoupling above — it's not
|
||||
optional.
|
||||
|
||||
## Open design questions for the brainstorm (resolve BEFORE coding)
|
||||
|
||||
1. **Does WB even render a sealed sub-cell (cellar) via inside-out?** Check how
|
||||
WB derives `_buildingsWithCurrentCell` (VisibilityManager.PrepareVisibility +
|
||||
PortalRenderManager.GetBuildingPortalsByCellId). If WB *excludes* a cell with
|
||||
no exit portals from the inside-out path, the "fix" may be a classification
|
||||
change, not recursion. **Verify against WB source — don't assume recursion exists.**
|
||||
2. **How does WB ACTUALLY constrain per-portal visibility?** Re-read
|
||||
`VisibilityManager.cs` (RenderInsideOut/RenderOutsideIn) + `PortalRenderManager.cs`
|
||||
end-to-end. Is the clipping (a) recursive portal traversal, (b) the 3-bit
|
||||
stencil Step-5 pipeline, (c) pure Step-3 depth occlusion, or (d) the BSP
|
||||
portal-graph in PrepareVisibility? Our port copied the *flat* Steps 1-4; the
|
||||
constraint mechanism may live in code we didn't port.
|
||||
3. **Job-A/Job-B decoupling.** Design how exterior geometry (shells + scenery +
|
||||
depth-repair) draws independent of the portal mask, while terrain-through-portal
|
||||
stays stencil-gated. This must land regardless of the recursion design.
|
||||
4. **Stencil-bit budget + occlusion-query lifecycle** if the full WB Step-5
|
||||
cross-building path is adopted (currently gated off via `ACDREAM_A8_STEP5`).
|
||||
|
||||
## Key source references for the brainstorm
|
||||
|
||||
WB (the algorithm being ported):
|
||||
- `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilityManager.cs`
|
||||
— `PrepareVisibility` (47-71), `RenderInsideOut` (73-239), `RenderOutsideIn` (241+).
|
||||
- `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/PortalRenderManager.cs`
|
||||
— `RenderBuildingStencilMask`, `GetVisibleBuildingPortals`, `GetBuildingPortalsByCellId`.
|
||||
- `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/EnvCellRenderManager.cs`.
|
||||
|
||||
acdream (the current port):
|
||||
- `src/AcDream.App/Rendering/GameWindow.cs` — `RenderInsideOutAcdream`
|
||||
(~11012), `RenderOutsideInAcdream` (~196), the Step-4 `didInsideStencil` gate
|
||||
(~11167). **This is where the Job-A/Job-B coupling lives.**
|
||||
- `src/AcDream.App/Rendering/IndoorCellStencilPipeline.cs` — `PortalMeshBuilder`
|
||||
(camera-side filter), `RenderBuildingStencilMask`, `DrawUploadedPortalMesh`.
|
||||
- `src/AcDream.App/Rendering/Wb/BuildingLoader.cs` — building cell-set BFS
|
||||
(mirrors WB PortalService; cellar IS expanded into building 0xA).
|
||||
|
||||
Retail oracle (if WB is ambiguous):
|
||||
- `docs/research/named-retail/acclient_2013_pseudo_c.txt` — `CObjCell::find_visible_child_cell`
|
||||
(≈311397), `PView::DrawCells` (≈432709). Retail uses screen-space polygon-clip
|
||||
scissor recursion — the conceptual ancestor of "clip each portal to the chain."
|
||||
|
||||
Offline tooling:
|
||||
- `tools/A8CellAudit` — `dotnet run -- portals <cellId...>` dumps a cell's
|
||||
CellPortals (exit vs interior); `-- buildings <lb> <radius>` dumps
|
||||
building→cell grouping. Reproduces the whole investigation in seconds, no launch.
|
||||
|
||||
## Brainstorming kickoff prompt (copy-paste into a fresh session)
|
||||
|
||||
> Use the `superpowers:brainstorming` skill. We're designing the retail-faithful
|
||||
> fix for the A8 "cellar flap" — the last A8 indoor-rendering defect.
|
||||
>
|
||||
> Read first, in order:
|
||||
> 1. `docs/research/2026-05-28-a8-cellar-flap-option2-handoff.md` (this doc) —
|
||||
> current state, the confirmed root cause, the `didInsideStencil` double-duty
|
||||
> finding, the decision, and the open design questions.
|
||||
> 2. `docs/research/2026-05-28-a8-cellar-flap-root-cause.md` — the offline evidence.
|
||||
> 3. The WB + acdream source references listed in the handoff.
|
||||
>
|
||||
> The goal: design how acdream's indoor visibility should render outside-through-
|
||||
> portals **correctly clipped to the portal chain** (so a sealed cellar shows no
|
||||
> terrain, a windowed room shows its own windows, deep rooms show only the sliver
|
||||
> visible through the doorway chain) **AND** decouple "draw exterior geometry at
|
||||
> all" from "is the portal mask non-empty" (the coupling that made the targeted
|
||||
> fix regress).
|
||||
>
|
||||
> Brainstorm MUST resolve the 4 open design questions in the handoff before any
|
||||
> code — especially Q1 (does WB even render a sealed cellar inside-out?) and Q2
|
||||
> (what is WB's ACTUAL per-portal clipping mechanism — verify against source,
|
||||
> don't assume recursion). Output a written design/plan; do not start coding
|
||||
> until the design is agreed.
|
||||
>
|
||||
> Process rules still in force: no workarounds/band-aids; faithful WB/retail
|
||||
> port; one visual gate only when a complete fix is ready; the broken indoor
|
||||
> branch is behind `ACDREAM_A8_INDOOR_BRANCH=1` (default off = pre-A8 visual).
|
||||
> The dirty tree has valuable uncommitted A8 work — decide whether to commit it
|
||||
> (strip `ACDREAM_A8_DIAG_*` first) before starting.
|
||||
120
docs/research/2026-05-28-a8-cellar-flap-root-cause.md
Normal file
120
docs/research/2026-05-28-a8-cellar-flap-root-cause.md
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# A8 cellar-flap — structured debugging root cause (2026-05-28 PM)
|
||||
|
||||
## Method
|
||||
|
||||
Systematic-debugging Phases 1-3, all evidence gathered **offline** via the
|
||||
`tools/A8CellAudit` tool (extended with a `portals` mode) — no live launches
|
||||
needed. Deterministic, instant, reproducible.
|
||||
|
||||
## Phase 1 — evidence
|
||||
|
||||
Scenario (from `launch-a8-probe-normal-20260528-194536.out.log`):
|
||||
- Camera in cell `0xA9B40171`, `inside=True really=True`.
|
||||
- `camBldgs=[0xA]`, `visN=7 [0x16F,0x170,0x171,0x172,0x173,0x174,0x175]`.
|
||||
- Portal stencil mask = 12 verts (not the old over-punch case).
|
||||
- Bisection (prior session): writer is **Step 4 content**; disabling Step-2
|
||||
punch does **not** fix it.
|
||||
|
||||
Offline audit findings:
|
||||
|
||||
**Building grouping** (`A8CellAudit buildings 0xA9B40000`):
|
||||
```
|
||||
buildingOrdinal=10 registryId=0xA model=0x01002232 portalCells=[0xA9B4016F,0xA9B40170]
|
||||
```
|
||||
Building `0xA`'s LandBlockInfo seed = `{0x16F, 0x170}`. `BuildingLoader` then
|
||||
BFS-expands through interior portals → all 7 cells (incl. the cellar). The
|
||||
BFS matches WB's `PortalService` (same algorithm), so the grouping is not the
|
||||
divergence.
|
||||
|
||||
**Exit-portal ownership** (`A8CellAudit portals ...`):
|
||||
|
||||
| Cell | exit portals (0xFFFF) | interior | role |
|
||||
|------|----|----|------|
|
||||
| `0x16F` | **1** | 1 | ground floor (window/door) |
|
||||
| `0x170` | **1** | 1 | ground floor (window/door) |
|
||||
| `0x171` (camera) | **0** | 3 | cellar |
|
||||
| `0x172`–`0x175` | **0** | 1–2 | cellar rooms |
|
||||
|
||||
So the 12-vert mask = `0x16F` exit (6v) + `0x170` exit (6v). **The cellar
|
||||
camera (zero exit portals) is marking the two ground-floor windows.**
|
||||
|
||||
**Topology**:
|
||||
```
|
||||
0x171.portal[0] -> 0x170 (stairwell/hatch, polyId 54)
|
||||
0x170.portal[1] -> 0x171 (polyId 5)
|
||||
0x170.portal[0] -> EXIT (window/door to outside, polyId 4)
|
||||
```
|
||||
Cellar connects directly up to ground floor `0x170`; `0x16F` is one further hop.
|
||||
|
||||
**Occluder geometry** (`A8CellAudit 0xA9B40170` / `0xA9B40171`):
|
||||
- `0x170` floor poly `0x0002` (n.Z=+1) **emits** — the cellar's ceiling/occluder exists.
|
||||
- `0x171` has a ceiling `0x0003` (n.Z=-1, emits) AND three `NoPos` polys
|
||||
`0x0036/0x0037/0x0038` (surface `0x080000DF`) that do **not** emit —
|
||||
`0x0038` is a ceiling-plane poly = the **stairwell hole** up to the ground floor.
|
||||
|
||||
## Phase 2 — pattern vs WB
|
||||
|
||||
WB `RenderInsideOut` marks the building's exit portals (flat — same as us) and
|
||||
relies on **Step-3 cell depth** to occlude them: terrain only survives where the
|
||||
punched/cleared far-depth isn't overwritten by rendered cell geometry.
|
||||
|
||||
Our code matches that structure. The difference that produces the visible flap:
|
||||
WB's outside view through a portal is the world geometrically behind that
|
||||
portal; from a cellar, the only un-occluded opening is the **stairwell hole**
|
||||
(`0x0038`, not rendered). Through that hole, stencil=1 (ground-floor window
|
||||
marked) and depth=far → **Step 4 draws the entire outdoor world (terrain +
|
||||
buildings) through the hole**, not a window-sized sliver. The two ground-floor
|
||||
windows are 1–2 BFS hops above the camera and should contribute essentially
|
||||
nothing from the cellar, but their full silhouettes are marked.
|
||||
|
||||
"Disable Step-2 punch doesn't fix it" is explained: the leak pixels are the
|
||||
stairwell hole, which has **cleared (far) depth** regardless of the punch
|
||||
because no cell geometry covers it — terrain passes `DepthFunc.Less` either way.
|
||||
|
||||
## Phase 3 — single hypothesis (root cause)
|
||||
|
||||
**The inside-out exit-portal stencil mask is built by flat-marking the exit
|
||||
portals of every visibility-BFS-reached cell. From the cellar, the BFS reaches
|
||||
the ground-floor cells, whose windows get full-silhouette-marked. Where the
|
||||
cellar's stairwell hole leaves those silhouettes un-occluded, Step 4 paints the
|
||||
whole outdoor world through them. There is no constraint tying a deeper cell's
|
||||
exit portal to the portal chain (here: the narrow stairwell) through which its
|
||||
cell became visible.**
|
||||
|
||||
This is a flat-vs-constrained masking gap. Not a depth bug (occluders emit and
|
||||
render), not the Step-2 punch, not the camera-side filter (the cellar camera is
|
||||
geometrically on the interior side of a ground-floor window's plane, so the
|
||||
per-portal filter passes it).
|
||||
|
||||
## Phase 4 — fix options
|
||||
|
||||
1. **Camera-cell-scoped mask (minimal, conservative).** Mark only the camera
|
||||
cell's own exit portals. Cellar (0 exit portals) → empty mask → no leak;
|
||||
windowed room → marks its own windows. **Risk:** loses daylight through an
|
||||
*adjacent* cell's window seen across a doorway in multi-cell ground-floor
|
||||
rooms (e.g. the inn) — a visible-but-minor regression, and the flat approach
|
||||
was wrong there anyway.
|
||||
|
||||
2. **Vertical-portal-aware scoping (targeted).** Don't propagate exit-portal
|
||||
marking across a floor/ceiling (vertical-normal) portal. The cellar→ground
|
||||
stairwell is a horizontal-plane portal; suppressing inheritance across it
|
||||
stops the cellar from marking ground-floor windows while preserving
|
||||
same-level multi-cell rooms. Needs per-portal polygon-normal classification.
|
||||
|
||||
3. **WB recursive/constrained portal masking (faithful, largest).** Constrain
|
||||
each deeper portal's stencil to the screen region of the portal chain leading
|
||||
to it. Correct for all cases (cellar + multi-cell rooms) but a substantial
|
||||
port of WB's recursive RenderInsideOut.
|
||||
|
||||
**Recommendation:** option 2 is the best correctness/effort trade — it fixes the
|
||||
cellar without the inn regression risk of option 1, and is a principled scoping
|
||||
rule (don't inherit a different vertical level's exterior openings) rather than a
|
||||
band-aid. Option 3 remains the eventual faithful target if cross-level portal
|
||||
visibility ever needs to be exact.
|
||||
|
||||
## Reproduction / verification assets
|
||||
|
||||
- `tools/A8CellAudit` `portals` mode (added this session) dumps any cell's
|
||||
`CellPortals` offline. `A8CellAudit buildings <lb> <radius>` dumps
|
||||
building→cell grouping. These make the whole investigation re-runnable in
|
||||
seconds with zero launches.
|
||||
Loading…
Add table
Add a link
Reference in a new issue