The Phase W indoor seal did NOT land. The 2026-06-02 visual gate proved the interior render is fundamentally broken (#78: transparent walls, outdoor terrain + scenery entities bleeding in, grey floors, no outside-looking-in). Stage 4 (sky-through-door clip) was real but a top layer on a base that never sealed. DECISIVE EVIDENCE (committed in the handoff): the PVS computes correctly AND the cell shells render correctly (opaque, textured, complete — the [shell] probe shows zero NOSNAP / zero missing-texture). The failure is the SEAL + three inconsistent gates — concretely the WbDrawDispatcher.cs:1756 ParentCellId==null -> return true bypass draws outdoor scenery indoors, and the indoor path draws the outdoor world then gates it instead of running ONLY DrawInside. Retail, when inside, runs ONE PView flood: visibility IS the cull; the landscape enters only through clipped exit portals + a conditional depth-only clear. Dossier (per the user's mandate: NO shortcuts/bandaids, port from retail, redesign the whole pipeline if needed, brainstorm first): - Master handoff (root cause + retail target + reusable-vs-redesign + apparatus + do-not-repeat + copy-paste pickup prompt). - Huge staged redesign plan R0(brainstorm)->R1(one visibility authority, kill the bleed)->R2(indoor=DrawInside-only)->R3(the seal, DrawCells port)->R4(per-cell object/particle clip)->R5(outside-looking-in)->R6(dungeons)->R7(polish/conformance). Each ends at a user visual gate. - 3 research docs: full retail render pipeline reference (705 lines, decomp-verified), acdream pipeline inventory + failure map, reference cross-check (WB two-pipe is the wrong model). #78 promoted to the redesign. The 5 remaining Core test failures are pre-existing physics/collision bugs, none render-related. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
449 lines
29 KiB
Markdown
449 lines
29 KiB
Markdown
# Indoor Cell / Portal Visibility Render Reference Cross-Check
|
||
**Date:** 2026-06-02
|
||
**Purpose:** Inform acdream's Phase U unified render pipeline redesign by documenting what
|
||
each reference client does for indoor cell rendering, portal visibility, the interior seal,
|
||
outdoor-scenery gating, and object/particle clipping — with explicit ADOPT / AVOID verdicts.
|
||
|
||
---
|
||
|
||
## 1. WorldBuilder — Indoor Render Approach (RenderInsideOut Two-Pipe Stencil)
|
||
|
||
### What WB actually does
|
||
|
||
**Source files:**
|
||
- `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilityManager.cs`
|
||
- `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/PortalRenderManager.cs`
|
||
- `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/EnvCellRenderManager.cs`
|
||
- `references/WorldBuilder/Chorizite.OpenGLSDLBackend/GameScene.cs` lines 880–1008
|
||
|
||
#### The branch decision
|
||
|
||
`GameScene.cs:881` sets `isInside = currentEnvCellId != 0` (the camera is in ANY EnvCell).
|
||
Two completely different code paths branch on that boolean:
|
||
|
||
- **Inside (`RenderInsideOut`)** — `VisibilityManager.RenderInsideOut` (lines 73–239)
|
||
- **Outside (`RenderOutsideIn` or fallback)** — `VisibilityManager.RenderOutsideIn` (lines 241–358)
|
||
or `RenderEnvCellsFallback` when `EnableCameraCollision` is off
|
||
|
||
This is the split acdream inherited from Phase N.4. **It is the root cause of every
|
||
doorway seam bug.** The two paths are fundamentally incompatible at a threshold crossing —
|
||
the hand-off point is where the flap, transparent walls, and terrain bleed all happen.
|
||
|
||
#### RenderInsideOut walkthrough (VisibilityManager.cs:73–239)
|
||
|
||
1. **Stencil Bit 1 — doorway mask:** All portal polygons of the *camera's building* are rasterized
|
||
with `StencilOp.Replace` → Bit 1 = 1. This marks the screen pixels that correspond to doorways.
|
||
Done with `DepthFunc(Always)` so portals draw regardless of what's in front of them.
|
||
|
||
2. **Punch depth at doorways:** The same portal geometry is drawn again with `uWriteFarDepth=1`
|
||
(the stencil shader writes `gl_FragDepth = 1.0`). This clears depth at the doorway pixels
|
||
so the outdoor terrain can bleed through them.
|
||
|
||
3. **Render indoor cells ALWAYS** (no stencil guard). The camera building's full EnvCell set
|
||
is drawn unconditionally (`_currentEnvCellIds` = union of all cells in the camera building).
|
||
No per-portal clip. All cells in the building render even if behind the player.
|
||
|
||
4. **Gate outdoor geometry (terrain/scenery/static) through Bit 1.** Stencil func `Equal(1, 0x01)` —
|
||
terrain, scenery, and static objects only draw where the portal polygons were rasterized.
|
||
This is the "seal" against outdoor bleed: if Bit 1 wasn't set, the pixel never gets terrain.
|
||
|
||
5. **Other buildings' cells** (Step 5, lines 157–229): For each other building visible through
|
||
our doorways, WB does a further two-step mask — Bit 2 marks the intersection of our doorway
|
||
AND the other building's portals (stencil == 3 meaning both bits set), then draws that
|
||
building's cells only where both portals are open. Uses occlusion queries to skip buildings
|
||
that were fully occluded last frame.
|
||
|
||
6. **RenderOutsideIn** (outside path, lines 241–358): mirrors Step 1–2 but camera is outside
|
||
looking in. Portal polygons mark Bit 1; depth is cleared at those pixels; EnvCells render
|
||
through the mask. Terrain/scenery/statics draw normally (no stencil guard).
|
||
|
||
#### What `GetVisibleBuildingPortals` provides
|
||
|
||
`PortalRenderManager.GetVisibleBuildingPortals` returns a `BuildingPortalGPU` per **building**
|
||
(not per cell or per portal). The `BuildingPortalGPU` is a triangle-fan tessellation of ALL
|
||
portal polygons for the building concatenated into a single VAO/VBO. This is the flat union —
|
||
there is no per-portal polygon tracking. One stencil pass per building.
|
||
|
||
`EnvCellRenderManager.GenerateForLandblockAsync` discovers cells recursively from building
|
||
portals (`portal.OtherCellId != 0xFFFF` — exit portals are skipped). The `seenOutsideCells`
|
||
set tracks cells with `EnvCellFlags.SeenOutside` but WB only stores this for diagnostic use;
|
||
it does NOT gate the cell draw off `SeenOutside`.
|
||
|
||
#### How WB decides cell visibility for the filter
|
||
|
||
`VisibilityManager.PrepareVisibility` (lines 47–71): when `isInside`, adds ALL cells of every
|
||
building the camera is in (`_buildingsWithCurrentCell`). No per-portal traversal. No
|
||
per-portal clip. No `VisibleCells` stab-list from the dat. The full cell set of the building
|
||
is the filter.
|
||
|
||
When `isInside=false`, `GetVisibleBuildingPortals` returns frustum-visible building groups;
|
||
ALL their cells are added to `visibleEnvCells`. Again, no per-portal traversal.
|
||
|
||
**There is no WB equivalent of retail's per-cell `VisibleCells` (the PVS stab-list). WB
|
||
never reads `EnvCell.VisibleCells`.** WB's visibility is building-level, not cell-level.
|
||
|
||
#### Why the WB two-pipe diverges from retail's recursive PView
|
||
|
||
Retail `PView::ConstructView` (decomp ~433750) and `ClipPortals` (~433572):
|
||
|
||
| Property | WB RenderInsideOut | Retail PView |
|
||
|---|---|---|
|
||
| **Visibility unit** | Building (all cells in a building) | Per-cell portal traversal |
|
||
| **Clip granularity** | One stencil mask per building | Per-portal screen-space clip polygon |
|
||
| **Camera branching** | Hard `isInside` branch switching two completely different code paths | No branch — "which cell is the camera in" changes only the BFS root, not the algorithm |
|
||
| **Outdoor geometry gate** | Stencil Bit 1 derived from the portal polygon raster at the wall | OutsideView clip polygon accumulated by clipping through exit portals in the BFS |
|
||
| **Per-cell PVS** | Not used | `CEnvCell.stab_list` + `seen_outside` read per cell; portal side test per edge |
|
||
| **Scenery gating** | Outdoor scenery draws only where Bit 1 is set (all portals of the camera building) | Outdoor entities/scenery assigned OutsideView clip slot; cull when OutsideView empty |
|
||
| **Terrain gate** | Depth punched at portal pixels; terrain only draws stencil==1 | TerrainClipMode: Skip when no exit portal visible, Planes/Scissor when one is |
|
||
| **Other-building cells** | 2-bit stencil composed gate with occlusion query fallback | Same recursive BFS — other buildings' cells are cells in the PVS; no separate pipe |
|
||
| **Seam at doorway** | **Inherent** — the two pipes switch at `currentEnvCellId != 0`; the frame of the switch always tears | **None** — outdoor/indoor are the same draw loop with different clip regions |
|
||
|
||
#### Why acdream abandoned WB's two-pipe (2026-05-30)
|
||
|
||
From `docs/research/2026-05-30-unified-render-pipeline-decision-and-handoff.md`:
|
||
|
||
> "The A8.F effort tried to graft retail's recursive clip *on top of* WB's two-pipe stencil
|
||
> (a CPU-built NDC mask bridging the two pipes). That hybrid is inherently fragile and failed
|
||
> its visual gate (issue #103). You cannot make two pipes hand off seamlessly at a doorway;
|
||
> retail avoids the entire bug class by never splitting."
|
||
|
||
The specific failure modes WB's architecture cannot fix without replacing the architecture:
|
||
|
||
1. **The flap** — The `isInside` / `currentEnvCellId != 0` gate flips on the same frame the camera
|
||
crosses the doorway. On the flip frame WB switches from RenderInsideOut to RenderOutsideIn (or
|
||
vice versa). The stencil state from the prior frame is cleared; the new portal polygon raster
|
||
hasn't loaded. There is one frame where the indoor draw is missing OR the outdoor draw bleeds
|
||
through. This is inherent to the architecture.
|
||
|
||
2. **Terrain bleed indoors** — WB's stencil Bit 1 mask is derived from the camera-building portal
|
||
polygons rasterized with `DepthFunc(Always)` BEFORE terrain. If the portal polygon is degenerate,
|
||
or the stencil clear races the portal raster, terrain bleeds through. The mask is not ground in
|
||
the per-cell PVS (`VisibleCells` / `SeenOutside`) so it cannot make a definitive "no terrain here
|
||
EVER" decision the way retail's `seen_outside=false` does for sealed dungeons.
|
||
|
||
3. **Transparent walls** — In RenderOutsideIn, WB clears depth with `uWriteFarDepth=1` at the portal
|
||
stencil, then renders EnvCells into the cleared region. The wall geometry adjacent to the portal
|
||
also had its depth cleared (the portal polygon doesn't exactly hug the wall opening), so a wall
|
||
polygon's depth test can fail against the cleared far-plane, making it appear transparent.
|
||
|
||
4. **Outdoor scenery entities indoors** — WB's `RenderInsideOut` gates terrain/scenery/static-objects
|
||
through Bit 1 (only where the portal polygon rasters hit). But if the entity's ParentCellId is
|
||
not tracked or is 0 (outdoor scenery has no indoor cell parent), it routes to the stencil
|
||
unguarded path and draws everywhere.
|
||
|
||
5. **Cannot be fixed without rebuilding** — The stencil approach is a GPU-side approximation of
|
||
what retail does on the CPU (per-portal clip-polygon BFS). Every "fix" to the stencil approach
|
||
adds more GPU state to paper over a case where the stencil and the intended visibility diverge.
|
||
The correct fix replaces the stencil entirely with retail's CPU-driven visibility.
|
||
|
||
**VERDICT: DO NOT REINTRODUCE the WB RenderInsideOut/RenderOutsideIn two-pipe stencil or any
|
||
approximation of it. The architecture is the bug.**
|
||
|
||
---
|
||
|
||
## 2. ACViewer — Cell/Portal Rendering and Sealing Approach
|
||
|
||
**Source files:**
|
||
- `references/ACViewer/ACViewer/Render/R_Landblock.cs`
|
||
- `references/ACViewer/ACViewer/Render/R_EnvCell.cs`
|
||
- `references/ACViewer/ACViewer/Render/Buffer.cs`
|
||
- `references/ACViewer/ACViewer/Physics/Common/EnvCell.cs`
|
||
|
||
### What ACViewer does
|
||
|
||
ACViewer is a **dat viewer and map editor** (MonoGame/DirectX), not a game client. Its rendering
|
||
is a brute-force draw with no runtime visibility or sealing:
|
||
|
||
**`R_Landblock.cs:83–101`** `BuildEnvCells()`: For each cell id from 0x100 to
|
||
`0x100 + NumCells - 1` in the landblock, creates an `R_EnvCell` and adds it to the list.
|
||
All cells for the landblock are built, with no portal traversal.
|
||
|
||
**`Buffer.Draw()` / `Buffer.DrawWithZSlicing()`**: Draws all batches in `RB_EnvCell`,
|
||
`RB_StaticObjs`, `RB_Buildings`, `RB_Scenery` unconditionally (gated only by the Z-slicing
|
||
filter for multi-floor dungeon inspection). No portal culling. No stencil. No clip.
|
||
|
||
**`R_EnvCell.Draw()`** (`R_EnvCell.cs:70–87`): Calls `DrawEnv()` (sets xWorld + draws the
|
||
environment cell struct mesh) + `DrawStaticObjs()` (draws each stab). No filter.
|
||
|
||
**There is no portal-based visibility in ACViewer at all.** ACViewer draws ALL cells and ALL
|
||
objects in every loaded landblock every frame. Culling is done only by the MonoGame frustum
|
||
culling on the DirectX state (backface culling ON in dungeon mode per `Buffer.cs:166`).
|
||
|
||
### ACViewer's EnvCell data model — what it DOES read
|
||
|
||
**`EnvCell.cs`** (Physics/Common, the ACViewer version used by `R_EnvCell`):
|
||
|
||
- `VisibleCellIDs` — list of low-byte cell IDs from the dat (`stab_list` / `numStabs`). This
|
||
is the DAT-baked PVS for this cell. ACViewer reads it in the constructor (`VisibleCellIDs =
|
||
envCell.VisibleCells`) and builds `VisibleCells` dict via `build_visible_cells()`.
|
||
- `SeenOutside` — `envCell.SeenOutside` flag. ACViewer reads it.
|
||
- `Portals` — the `CellPortal[]` list. ACViewer reads it.
|
||
- `find_visible_child_cell(Vector3 origin, bool searchCells)` (lines 206–231): Checks if `origin`
|
||
is in this cell's AABB; if not, searches `VisibleCells.Values` (or falls back to `Portals`) for
|
||
a cell containing `origin`. This is the **retail `CEnvCell::find_visible_child_cell`** ported
|
||
to ACViewer's physics tree — it is the cell-membership resolver for moving objects in physics.
|
||
|
||
**Critically, ACViewer reads and maintains the PVS / portal data but uses it ONLY for physics
|
||
collision, not for rendering.** The render path is entirely brute-force.
|
||
|
||
### Does ACViewer seal interiors?
|
||
|
||
**No.** ACViewer does not try to occlude the outdoor world when drawing from inside a building.
|
||
It draws everything: terrain + scenery + building geometry + dungeon cells simultaneously. It
|
||
relies on correct depth testing to show the right surfaces. This works for a static viewer (you
|
||
can rotate to any position and inspect geometry) but would be completely broken for a game client
|
||
(outdoor terrain bleeds through dungeon floors when the camera is inside).
|
||
|
||
**ACViewer is GPL-licensed (read for understanding only; do not copy code).**
|
||
|
||
### What is reusable from ACViewer
|
||
|
||
- **The `EnvCell` data model** (especially `find_visible_child_cell`, `build_visible_cells`,
|
||
`VisibleCellIDs`, `SeenOutside`): these are faithful ports of the retail data structures.
|
||
acdream already has its own equivalent (`LoadedCell.VisibleCells`, `LoadedCell.SeenOutside`
|
||
in `CellVisibility.cs`). The ACViewer source confirms the field interpretations.
|
||
- **`CellPortal` struct** (`PolygonId`, `OtherCellId`, `OtherPortalId`, `PortalSide`): confirms
|
||
the exact field layout. acdream's `CellPortalInfo` record matches.
|
||
- **Algorithm understanding**: ACViewer's `find_visible_child_cell` confirms the retail pattern —
|
||
first check `point_in_cell(origin)` (self), then search `VisibleCells` by AABB, then fallback
|
||
to portal-linked neighbours. This is the retail `CEnvCell::find_visible_child_cell` at
|
||
`acclient_2013_pseudo_c.txt:311397`.
|
||
|
||
---
|
||
|
||
## 3. ACE — Cell/Portal/Visibility Data Model
|
||
|
||
**Source files:**
|
||
- `references/ACE/Source/ACE.DatLoader/FileTypes/EnvCell.cs`
|
||
- `references/ACE/Source/ACE.DatLoader/FileTypes/LandblockInfo.cs`
|
||
- `references/ACE/Source/ACE.DatLoader/Entity/CBldPortal.cs`
|
||
- `references/ACE/Source/ACE.DatLoader/Entity/BuildInfo.cs`
|
||
- `references/ACE/Source/ACE.Entity/Enum/EnvCellFlags.cs`
|
||
- `references/ACE/Source/ACE.DatLoader/Entity/CellPortal.cs`
|
||
|
||
ACE is the **server** — it defines the canonical dat file format that both the retail client and
|
||
acdream read. The data model here is authoritative.
|
||
|
||
### EnvCell fields (ACE DatLoader, `EnvCell.cs`)
|
||
|
||
| Field | Type | Purpose |
|
||
|---|---|---|
|
||
| `Flags` | `EnvCellFlags` | Bitflags: `SeenOutside (0x1)`, `HasStaticObjs (0x2)`, `HasRestrictionObj (0x8)` |
|
||
| `Surfaces` | `List<uint>` | Surface IDs (`0x08000000 | shortId`) — the textures for this cell |
|
||
| `EnvironmentId` | `uint` | Environment dat id (`0x0D000000 | shortId`) — the prefab geometry block |
|
||
| `CellStructure` | `ushort` | Which sub-structure within the environment (one environment can have many cell shapes) |
|
||
| `Position` | `Frame` | World-space placement of this cell (position + rotation) |
|
||
| `CellPortals` | `List<CellPortal>` | Connectivity list: `OtherCellId` (0xFFFF = exit portal), `OtherPortalId`, `PolygonId`, `ExactMatch`, `PortalSide` |
|
||
| `VisibleCells` | `List<ushort>` | PVS stab-list: low-byte cell IDs of cells potentially visible from this one (precomputed by AC's content tools) |
|
||
| `StaticObjects` | `List<Stab>` | Static geometry placed within this cell (furniture, decorations) |
|
||
| `RestrictionObj` | `uint` | GUID of the object controlling access (housing barriers) |
|
||
| `SeenOutside` | `bool` | Derived from `Flags.HasFlag(SeenOutside)` — this cell has line of sight to the exterior |
|
||
|
||
**Note from the ACE comment at line 46:** `numStabs` (the `VisibleCells` count) — "I believe this
|
||
is what cells can be seen from this one. So the engine knows what else it needs to load/draw."
|
||
This confirms the stab-list is the visibility PVS the renderer should use as a filter.
|
||
|
||
### The exit portal sentinel: `CellPortal.OtherCellId == 0xFFFF`
|
||
|
||
`CellPortal.cs:10,19`: `OtherCellId` is a `ushort`. Value `0xFFFF` (65535) means "exit to
|
||
outdoor world" — the portal connects this cell to the exterior. This is the seal-breaking
|
||
portal. A cell with an exit portal in its `CellPortals` list has line of sight to the
|
||
outdoors, which is why retail's `SeenOutside` flag is TRUE for such cells (and recursively
|
||
for any cell that can reach an exit portal through the connectivity graph).
|
||
|
||
When `OtherCellId != 0xFFFF`, the portal connects to another EnvCell with low-byte id
|
||
`OtherCellId` in the same landblock. The full id is `(landblockId << 16) | OtherCellId`.
|
||
|
||
### LandblockInfo — building portal graph (`LandblockInfo.cs`, `BuildInfo.cs`, `CBldPortal.cs`)
|
||
|
||
The `LandblockInfo` dat file (`xxxxFFFE`) contains:
|
||
|
||
| Field | Type | Purpose |
|
||
|---|---|---|
|
||
| `NumCells` | `uint` | Total number of EnvCells in this landblock |
|
||
| `Objects` | `List<Stab>` | Static objects at landblock level (not inside any EnvCell) |
|
||
| `Buildings` | `List<BuildInfo>` | One `BuildInfo` per building structure |
|
||
|
||
Each `BuildInfo` has:
|
||
- `ModelId` — the GfxObj/Setup id of the building mesh
|
||
- `Frame` — world placement
|
||
- `Portals` — `List<CBldPortal>` — the **building-level portal list** (distinct from the per-cell `CellPortals`)
|
||
|
||
Each `CBldPortal` has:
|
||
- `OtherCellId` — the EnvCell low-byte id that this portal opens into (0xFFFF = no indoor side)
|
||
- `OtherPortalId` — back-link into that cell's portal list
|
||
- `StabList` — list of cells visible through this portal opening (the per-building-portal PVS)
|
||
|
||
This is the **entry-point graph** acdream uses in `EnvCellRenderManager.GenerateForLandblockAsync`
|
||
(line 657) to discover which EnvCells belong to which building: start from `BuildInfo.Portals`,
|
||
follow `CBldPortal.OtherCellId` → first EnvCell of the building → follow `CellPortal` connections
|
||
recursively to discover all cells in the building.
|
||
|
||
**WB's cell discovery correctly skips `portal.OtherCellId == 0xFFFF`** — these are exit portals
|
||
that lead outside, not to another EnvCell (`EnvCellRenderManager.cs:658,780`). Acdream's
|
||
`CellVisibility.GetVisibleCellsFromRoot` also skips them (line 467) and sets `HasExitPortalVisible`.
|
||
|
||
### What the seal means in the data model
|
||
|
||
A cell is "sealed" (truly indoor, no outdoor bleed) if:
|
||
- `SeenOutside == false`: no exit portal reachable from this cell via the connectivity graph.
|
||
Retail's `CellManager::ChangePosition` releases the landscape (terrain) when `seen_outside`
|
||
is false on the current cell, because there is definitively nothing outdoor to draw.
|
||
|
||
A cell "sees outside" if:
|
||
- `SeenOutside == true`: at least one exit portal (`OtherCellId == 0xFFFF`) is reachable.
|
||
Terrain may be visible; the outdoor world should be drawn, clipped to the visible exit portal.
|
||
|
||
Acdream already reads this correctly. The `LoadedCell.SeenOutside` field is set from the dat
|
||
flag (GameWindow.cs:7704), and `rootSeenOutside = physicsRoot?.SeenOutside ?? true` gates terrain.
|
||
|
||
---
|
||
|
||
## 4. Chorizite.ACProtocol
|
||
|
||
`references/Chorizite.ACProtocol/` — generated from the protocol XML. No rendering-relevant files;
|
||
it covers network wire types only. Not applicable to this cross-check.
|
||
|
||
---
|
||
|
||
## 5. AC2D
|
||
|
||
`references/AC2D/` — C++ fixed-function OpenGL client. Does not implement indoor rendering (it
|
||
renders everything via the server's authoritative Z; no client-side interior/exterior split). Not
|
||
applicable to indoor rendering cross-check.
|
||
|
||
---
|
||
|
||
## 6. Reusability Table
|
||
|
||
| Reference | Component | Reusable? | License | Caveat |
|
||
|---|---|---|---|---|
|
||
| **WorldBuilder** | `ObjectMeshManager`, `WbMeshAdapter`, `WbDrawDispatcher` mesh pipeline | YES (in-tree) | MIT | Keep — Phase U is about visibility, not mesh extraction |
|
||
| **WorldBuilder** | `EnvCellRenderManager.GenerateForLandblockAsync` cell discovery via `CBldPortal` / recursive `CellPortal` | YES (in-tree, adapted) | MIT | The discovery loop is correct; WB already skips 0xFFFF exit portals |
|
||
| **WorldBuilder** | `EnvCellRenderManager.PrepareRenderBatches` frustum + filter | YES (in-tree) | MIT | Used by acdream's `EnvCellRenderer`; only the visibility FILTER needs replacing (use stab-list PVS instead of per-building set) |
|
||
| **WorldBuilder** | `PortalRenderManager.GetVisibleBuildingPortals` + `BuildingPortalGPU` mesh | AVOID | MIT | This is the per-building stencil mesh — only useful for WB's two-pipe stencil; not needed for a unified PView pipeline |
|
||
| **WorldBuilder** | `VisibilityManager.RenderInsideOut` / `RenderOutsideIn` two-pipe stencil | **DO NOT USE** | MIT | Root cause of all indoor seam bugs; see §1 |
|
||
| **WorldBuilder** | `EnvCellFlags.SeenOutside` / `CBldPortal.OtherCellId != 0xFFFF` sentinel | YES (already used) | MIT | Data semantics confirmed correct |
|
||
| **ACViewer** | `EnvCell.find_visible_child_cell` | READ-ONLY (GPL) | GPL | Confirms retail `CEnvCell::find_visible_child_cell` interpretation; already ported in acdream's `CellVisibility` |
|
||
| **ACViewer** | `EnvCell.build_visible_cells` + `VisibleCells` dict | READ-ONLY (GPL) | GPL | Confirms PVS stab-list usage; acdream has `LoadedCell.VisibleCells` equivalent |
|
||
| **ACViewer** | Brute-force render of all cells (Buffer.cs) | **DO NOT USE** | GPL | No visibility, no seal, not usable for a game client |
|
||
| **ACE** | `EnvCell` dat file format (all fields) | YES (already used) | AGPL | acdream reads these via DatReaderWriter; field semantics confirmed authoritative |
|
||
| **ACE** | `CellPortal.OtherCellId == 0xFFFF` exit-portal sentinel | YES (already used) | AGPL | Critical for detecting the indoor/outdoor boundary |
|
||
| **ACE** | `LandblockInfo.Buildings` / `BuildInfo.Portals` entry-point graph | YES (already used) | AGPL | The cell discovery starting point; WB and acdream both use this correctly |
|
||
| **ACE** | `CBldPortal.StabList` (per-building-portal PVS) | INVESTIGATE | AGPL | Per-portal stab-list not currently used by acdream; may supplement the per-cell stab-list for cross-building portal resolution |
|
||
|
||
---
|
||
|
||
## 7. Recommendations for Phase U Redesign
|
||
|
||
### Adopt
|
||
|
||
1. **Retail PView portal-traversal as the single unified visibility pass** (retail anchor:
|
||
`PView::ConstructView` ~433750, `ClipPortals` ~433572, `GetClip` ~432344).
|
||
acdream already has `PortalVisibilityBuilder` and `CellVisibility.GetVisibleCellsFromRoot`
|
||
which are correct unit-tested ports. These are the keepers.
|
||
|
||
2. **Single code path regardless of camera position.** The camera being inside or outside an EnvCell
|
||
changes only which `LoadedCell` is the BFS root — null (outdoor root: player in a LandCell) vs
|
||
non-null (indoor root: player in an EnvCell). The draw algorithm does NOT branch on this.
|
||
|
||
3. **Per-cell PVS stab-list (`LoadedCell.VisibleCells`) as the filter ground.** Retail's
|
||
`grab_visible_cells` (decomp 311878) loads the stab-list into `VisibleCells` at cell entry
|
||
time. The per-frame BFS walks portal connectivity PLUS the stab-list to determine the drawable
|
||
set. The stab-list is precomputed by AC's content tools and is more conservative than a
|
||
per-frame portal clip (it includes cells reachable from any viewpoint in the cell, not just
|
||
from the current camera position). Use it as the BFS frontier expansion to avoid missed cells
|
||
at glancing angles.
|
||
|
||
4. **`SeenOutside` gate for terrain** (already in acdream at GameWindow.cs:7177).
|
||
`seen_outside=false` → skip terrain entirely (pure dungeon). `seen_outside=true` and an
|
||
exit portal is visible → draw terrain clipped to the `OutsideView` clip region.
|
||
This is retail's `CellManager::ChangePosition` @ `0x004559B0` behavior.
|
||
|
||
5. **`CellPortal.OtherCellId == 0xFFFF` as the outdoor-world gate** (already in acdream at
|
||
`CellVisibility.cs:467`, `PortalVisibilityBuilder`). Every exit portal reached in the BFS
|
||
contributes to `OutsideView`. When `OutsideView` is empty, NOTHING outdoor draws (terrain,
|
||
scenery, outdoor entities all cull). This is the closed seal.
|
||
|
||
6. **Outdoor scenery/entity gating via ParentCellId** (already in `WbDrawDispatcher.ResolveEntitySlot`):
|
||
- Live-dynamic entities (server GUID != 0): always slot 0 (no clip) — retail draws players/NPCs
|
||
through depth without portal clipping.
|
||
- Indoor entities (ParentCellId is a full EnvCell id): route to that cell's clip slot; cull if
|
||
the cell is not in the visible set.
|
||
- Outdoor scenery/statics (ParentCellId == null or is a LandCell id): route to `OutdoorSlot`;
|
||
cull when `OutdoorVisible = false` (no exit portal in view). **This is the outdoor-scenery seal.**
|
||
|
||
7. **The WB-derived mesh pipeline** (`ObjectMeshManager`, `WbMeshAdapter`, `WbDrawDispatcher`,
|
||
`TerrainModernRenderer`) is NOT the visibility problem and should not be replaced. Phase U
|
||
replaces the draw ORCHESTRATION (what gets drawn, when, with what clip), not the mesh
|
||
extraction (what vertices are in the VBO).
|
||
|
||
8. **`EnvCellRenderer.PrepareRenderBatches` with an explicit `filter` set** (already wired:
|
||
`envCellShellFilter` in GameWindow.cs:7333). The filter set is the BFS-visible cell ids from
|
||
`ClipFrameAssembly.CellIdToSlot.Keys`. This correctly excludes cells outside the visible set
|
||
(e.g. the other side of a multi-story building when the player is only in one floor).
|
||
|
||
### Avoid
|
||
|
||
1. **WB's `RenderInsideOut` / `RenderOutsideIn` two-pipe stencil** — abandoned 2026-05-30, do not
|
||
reintroduce. The architecture is the bug, not a parameter of the architecture.
|
||
|
||
2. **Per-building stencil mesh** (`BuildingPortalGPU`, `PortalRenderManager.RenderBuildingStencilMask`,
|
||
the `_stencilShader` + `InitializeStencilShader` machinery from WB) — only useful for WB's
|
||
stencil two-pipe. If acdream needs per-portal depth-clip, the retail mechanism is a software
|
||
clip plane (gl_ClipDistance) set from the per-portal NDC clip region, not a GPU stencil.
|
||
|
||
3. **The `isInside` / `cameraInsideBuilding` gate** — this is the two-pipe switch. Phase U's
|
||
redesign must not have any version of this. The outdoor case is `root == null` (player in a
|
||
LandCell); the indoor case is `root != null` (player in an EnvCell). These are inputs to the
|
||
SAME algorithm, not selectors for different algorithms.
|
||
|
||
4. **ACViewer's brute-force all-cells draw** — usable for map viewing tools, not for a game
|
||
client. The `Buffer.Draw()` approach will render hundreds of EnvCells including those in
|
||
completely different buildings on the other side of the landblock, causing massive overdraw
|
||
and incorrect visibility.
|
||
|
||
5. **Any "grace frame" or fallback AABB resolver for the portal-visibility root.** The root
|
||
comes from the physics-ownership answer (`CellGraph.CurrCell`) exclusively — retail's
|
||
`CellManager::ChangePosition` reads the transition-owned `curr_cell` with no AABB fallback.
|
||
Stage 3 (2026-06-02) deleted the FindCameraCell AABB grace-frame fallback from acdream.
|
||
|
||
6. **Re-porting the outdoor-terrain or EnvCell mesh extraction from retail decomp.** The WB
|
||
inventory doc (`docs/architecture/worldbuilder-inventory.md`) classifies these as green
|
||
(already correctly extracted from WB into `src/AcDream.{Core,App}/Rendering/Wb/`). The
|
||
rendering *orchestration* is 🔴 (must come from retail), the mesh *extraction* is 🟢
|
||
(WB has a tested port). Do not re-port what WB already got right.
|
||
|
||
---
|
||
|
||
## 8. Summary of WB-vs-Retail Divergence (10-line version)
|
||
|
||
1. WB branches hard on `isInside` (camera in ANY EnvCell) → two completely different render paths.
|
||
2. Retail has ONE path — portal-traversal BFS from the camera cell; indoor and outdoor are just cells.
|
||
3. WB's "seal" is a per-building GPU stencil derived from portal polygon rasters (flat, building-level).
|
||
4. Retail's seal is the CPU-derived `OutsideView` clip polygon (recursive, per-portal, per-cell).
|
||
5. WB uses NO per-cell PVS stab-list (`VisibleCells`) for rendering; retail uses it as the BFS frontier.
|
||
6. WB's outdoor gate (terrain/scenery draw only where stencil=1) fails at doorway-crossing frames (the flap).
|
||
7. Retail's outdoor gate (terrain clips to `OutsideView`; skip when empty) is frame-exact and derived from the same BFS as the cell draw.
|
||
8. WB cannot express per-portal clip precision (one stencil per building); retail clips each portal opening independently.
|
||
9. WB's approach is sound for a static dat-viewer where you never cross thresholds; it is architecturally wrong for a live game client.
|
||
10. The Phase U unified pipeline (retail PView port) is the correct fix; grafting anything onto WB's two-pipe stencil is not.
|
||
|
||
---
|
||
|
||
## Key File Paths Referenced
|
||
|
||
- `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilityManager.cs` — WB RenderInsideOut/RenderOutsideIn (full stencil two-pipe; read for understanding; DO NOT reintroduce)
|
||
- `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/PortalRenderManager.cs` — WB building portal GPU mesh + stencil shader
|
||
- `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/EnvCellRenderManager.cs` — WB cell discovery loop (reusable), PrepareRenderBatches with filter (reusable)
|
||
- `references/WorldBuilder/Chorizite.OpenGLSDLBackend/GameScene.cs` lines 880–1008 — the `isInside` branch; root of the two-pipe problem
|
||
- `references/ACViewer/ACViewer/Physics/Common/EnvCell.cs` — `find_visible_child_cell`, `build_visible_cells`, `VisibleCellIDs`, `SeenOutside` (read-for-understanding; GPL; don't copy)
|
||
- `references/ACViewer/ACViewer/Render/Buffer.cs` / `R_EnvCell.cs` / `R_Landblock.cs` — brute-force all-cells draw (read-for-understanding; GPL; DO NOT use)
|
||
- `references/ACE/Source/ACE.DatLoader/FileTypes/EnvCell.cs` — authoritative field list including `VisibleCells`, `SeenOutside`, `CellPortals`
|
||
- `references/ACE/Source/ACE.DatLoader/Entity/CellPortal.cs` — `OtherCellId == 0xFFFF` exit-portal sentinel
|
||
- `references/ACE/Source/ACE.DatLoader/FileTypes/LandblockInfo.cs` + `Entity/BuildInfo.cs` + `Entity/CBldPortal.cs` — building portal entry-point graph
|
||
- `src/AcDream.App/Rendering/CellVisibility.cs` — acdream's BFS visibility system (correct, keep)
|
||
- `src/AcDream.App/Rendering/PortalVisibilityBuilder.cs` — acdream's recursive portal-clip BFS (correct, keep)
|
||
- `src/AcDream.App/Rendering/ClipFrameAssembler.cs` — acdream's per-cell clip slot assembly (correct, keep)
|
||
- `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` — entity outdoor-slot routing via `ResolveEntitySlot` (correct, keep)
|
||
- `docs/research/2026-05-30-unified-render-pipeline-decision-and-handoff.md` — Phase U decision rationale
|