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>
29 KiB
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.csreferences/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/PortalRenderManager.csreferences/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/EnvCellRenderManager.csreferences/WorldBuilder/Chorizite.OpenGLSDLBackend/GameScene.cslines 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 (
RenderOutsideInor fallback) —VisibilityManager.RenderOutsideIn(lines 241–358) orRenderEnvCellsFallbackwhenEnableCameraCollisionis 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)
-
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 withDepthFunc(Always)so portals draw regardless of what's in front of them. -
Punch depth at doorways: The same portal geometry is drawn again with
uWriteFarDepth=1(the stencil shader writesgl_FragDepth = 1.0). This clears depth at the doorway pixels so the outdoor terrain can bleed through them. -
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. -
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. -
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.
-
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:
-
The flap — The
isInside/currentEnvCellId != 0gate 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. -
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'sseen_outside=falsedoes for sealed dungeons. -
Transparent walls — In RenderOutsideIn, WB clears depth with
uWriteFarDepth=1at 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. -
Outdoor scenery entities indoors — WB's
RenderInsideOutgates 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. -
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.csreferences/ACViewer/ACViewer/Render/R_EnvCell.csreferences/ACViewer/ACViewer/Render/Buffer.csreferences/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 buildsVisibleCellsdict viabuild_visible_cells().SeenOutside—envCell.SeenOutsideflag. ACViewer reads it.Portals— theCellPortal[]list. ACViewer reads it.find_visible_child_cell(Vector3 origin, bool searchCells)(lines 206–231): Checks iforiginis in this cell's AABB; if not, searchesVisibleCells.Values(or falls back toPortals) for a cell containingorigin. This is the retailCEnvCell::find_visible_child_cellported 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
EnvCelldata model (especiallyfind_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.SeenOutsideinCellVisibility.cs). The ACViewer source confirms the field interpretations. CellPortalstruct (PolygonId,OtherCellId,OtherPortalId,PortalSide): confirms the exact field layout. acdream'sCellPortalInforecord matches.- Algorithm understanding: ACViewer's
find_visible_child_cellconfirms the retail pattern — first checkpoint_in_cell(origin)(self), then searchVisibleCellsby AABB, then fallback to portal-linked neighbours. This is the retailCEnvCell::find_visible_child_cellatacclient_2013_pseudo_c.txt:311397.
3. ACE — Cell/Portal/Visibility Data Model
Source files:
references/ACE/Source/ACE.DatLoader/FileTypes/EnvCell.csreferences/ACE/Source/ACE.DatLoader/FileTypes/LandblockInfo.csreferences/ACE/Source/ACE.DatLoader/Entity/CBldPortal.csreferences/ACE/Source/ACE.DatLoader/Entity/BuildInfo.csreferences/ACE/Source/ACE.Entity/Enum/EnvCellFlags.csreferences/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 |
EnvironmentId |
uint |
Environment dat id (`0x0D000000 |
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 meshFrame— world placementPortals—List<CBldPortal>— the building-level portal list (distinct from the per-cellCellPortals)
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 listStabList— 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'sCellManager::ChangePositionreleases the landscape (terrain) whenseen_outsideis 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
-
Retail PView portal-traversal as the single unified visibility pass (retail anchor:
PView::ConstructView~433750,ClipPortals~433572,GetClip~432344). acdream already hasPortalVisibilityBuilderandCellVisibility.GetVisibleCellsFromRootwhich are correct unit-tested ports. These are the keepers. -
Single code path regardless of camera position. The camera being inside or outside an EnvCell changes only which
LoadedCellis 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. -
Per-cell PVS stab-list (
LoadedCell.VisibleCells) as the filter ground. Retail'sgrab_visible_cells(decomp 311878) loads the stab-list intoVisibleCellsat 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. -
SeenOutsidegate for terrain (already in acdream at GameWindow.cs:7177).seen_outside=false→ skip terrain entirely (pure dungeon).seen_outside=trueand an exit portal is visible → draw terrain clipped to theOutsideViewclip region. This is retail'sCellManager::ChangePosition@0x004559B0behavior. -
CellPortal.OtherCellId == 0xFFFFas the outdoor-world gate (already in acdream atCellVisibility.cs:467,PortalVisibilityBuilder). Every exit portal reached in the BFS contributes toOutsideView. WhenOutsideViewis empty, NOTHING outdoor draws (terrain, scenery, outdoor entities all cull). This is the closed seal. -
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 whenOutdoorVisible = false(no exit portal in view). This is the outdoor-scenery seal.
-
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). -
EnvCellRenderer.PrepareRenderBatcheswith an explicitfilterset (already wired:envCellShellFilterin GameWindow.cs:7333). The filter set is the BFS-visible cell ids fromClipFrameAssembly.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
-
WB's
RenderInsideOut/RenderOutsideIntwo-pipe stencil — abandoned 2026-05-30, do not reintroduce. The architecture is the bug, not a parameter of the architecture. -
Per-building stencil mesh (
BuildingPortalGPU,PortalRenderManager.RenderBuildingStencilMask, the_stencilShader+InitializeStencilShadermachinery 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. -
The
isInside/cameraInsideBuildinggate — this is the two-pipe switch. Phase U's redesign must not have any version of this. The outdoor case isroot == null(player in a LandCell); the indoor case isroot != null(player in an EnvCell). These are inputs to the SAME algorithm, not selectors for different algorithms. -
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. -
Any "grace frame" or fallback AABB resolver for the portal-visibility root. The root comes from the physics-ownership answer (
CellGraph.CurrCell) exclusively — retail'sCellManager::ChangePositionreads the transition-ownedcurr_cellwith no AABB fallback. Stage 3 (2026-06-02) deleted the FindCameraCell AABB grace-frame fallback from acdream. -
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 intosrc/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)
- WB branches hard on
isInside(camera in ANY EnvCell) → two completely different render paths. - Retail has ONE path — portal-traversal BFS from the camera cell; indoor and outdoor are just cells.
- WB's "seal" is a per-building GPU stencil derived from portal polygon rasters (flat, building-level).
- Retail's seal is the CPU-derived
OutsideViewclip polygon (recursive, per-portal, per-cell). - WB uses NO per-cell PVS stab-list (
VisibleCells) for rendering; retail uses it as the BFS frontier. - WB's outdoor gate (terrain/scenery draw only where stencil=1) fails at doorway-crossing frames (the flap).
- Retail's outdoor gate (terrain clips to
OutsideView; skip when empty) is frame-exact and derived from the same BFS as the cell draw. - WB cannot express per-portal clip precision (one stencil per building); retail clips each portal opening independently.
- WB's approach is sound for a static dat-viewer where you never cross thresholds; it is architecturally wrong for a live game client.
- 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 shaderreferences/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/EnvCellRenderManager.cs— WB cell discovery loop (reusable), PrepareRenderBatches with filter (reusable)references/WorldBuilder/Chorizite.OpenGLSDLBackend/GameScene.cslines 880–1008 — theisInsidebranch; root of the two-pipe problemreferences/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 includingVisibleCells,SeenOutside,CellPortalsreferences/ACE/Source/ACE.DatLoader/Entity/CellPortal.cs—OtherCellId == 0xFFFFexit-portal sentinelreferences/ACE/Source/ACE.DatLoader/FileTypes/LandblockInfo.cs+Entity/BuildInfo.cs+Entity/CBldPortal.cs— building portal entry-point graphsrc/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 viaResolveEntitySlot(correct, keep)docs/research/2026-05-30-unified-render-pipeline-decision-and-handoff.md— Phase U decision rationale