# Cathedral review — retail interior-root frame reconstruction (companion report) Delivered 2026-08-30 by the decomp-reconstruction agent (BN pseudo-C + live Ghidra arbitration on 8081 + acclient.h + symbols.json). THE retail mechanism reference for the synthesis. Condensed; all pc:/address citations preserved. ## Frame skeleton (interior root) 1. `SceneTool::BeginScene` @0x0043dad0: **full Clear(7) — COLOR+depth+ stencil — every frame** (pc:68673-68686). `Flip` bumps m_nFrameStamp at frame end (pc:428642). 2. `RenderNormalMode` @0x00453aa0: interior camera → optional LScape::update_viewpoint (only when viewer_cell->seen_outside, pc:92667-71) → DrawInside(viewer_cell) → **unconditional final drain FlushAlphaList(0f)** (pc:92687). 3. TWO PView singletons (pc:427800/427813): `indoor_pview` (draw_landscape =1) for the root; `outdoor_pview` (0) for building look-ins — look-in floods can never re-trigger landscape/clear/seals (their outside_view.view_count is always 0; ClipPortals never raises it when draw_landscape=0, pc:433666-84). 4. `PView::DrawInside` @0x005a5860: push root view (full viewport) → `ConstructView(root, 0xffff)` — PURE CPU flood, draws nothing; exit portals merge clipped apertures into outside_view (pc:433662-76, cliplandscape=1 @0x00820f4c). ## PView::DrawCells @0x005a4840 — ALL of this inside `ov > 0` (pc:432715): 1. `Render::PortalList = &outside_view`; `LScape::draw` (pc:432718-19). 2. **Full outdoor alpha drain** FlushAlphaList(0f) (pc:432720). 3. `m_nFrameStamp += 1` (pc:432722) — re-arms part dedupe so a part can draw in BOTH the outdoor and interior passes of one frame (straddlers draw twice; "emit once" is anti-retail). 4. **Gated z-only clear** (pc:432725-32, Ghidra-verified): `forceClear(const 0) || portalsDrawnCount != 0` — the counter is incremented ONLY by own-depth portal stamps (seals, arg2==0, pc:424569-70) → **a ONE-FRAME LATCH: last frame's seals arm this frame's clear.** Steady-state vista frames clear every frame; the FIRST such frame does not. 5. **Exit seals** (pc:432737-807): per flooded cell (far→near), per view, EVERY portal with other_cell 0xffffffff: `DrawPortalPolyInternal(poly, 0)` — REAL projected depth, DEPTHTEST_ALWAYS + z-write ON (pc:424584), invisible (vertex alpha 0, pc:424686), CULL_NONE, CPU-clipped to the current view (pc:424577). (Sibling mode arg2==1 = the far-Z PUNCH z≈0.999999, look-ins only; does not increment the counter.) Then UNCONDITIONALLY (even when ov==0): 6. Lighting restore (pc:432811-12). 7. **Cell shells** far→near: DrawEnvCell dedupes per frame stamp — the shell draws ONCE, WHOLE; the per-view loop is vestigial for it; no view planes applied; correctness = pure z-test (pc:427885-88). 8. **Cell objects** far→near: PortalList = the cell's views → DrawObjCellForDummies → shadow parts sorted by CYpt (viewer distance to scaled sort_center, insertion_sort @pc:701156-75) → CPhysicsPart::Draw — statics, dynamics, AND particle-emitter parts through ONE machinery. 9. Final drain (pc:92687) vs interior depth + seal-stamped apertures. ## Clip vs cull (the definitive table) - CPU polygon-clipped: ONLY portal-chain polys during the flood (GetClip @0x005a4320) and seal/punch fans at draw (their own polyClipFinish vs the current view, pc:424577, + a ±12 local-XY trivial reject pc:424506-50). - Sphere-vs-view CULLED, whole-drawn: all meshes (statics, dynamics, particles' parts) — DrawMesh per view: viewconeCheck(sphere) (pc:429295-96); PARTIAL draws WHOLE under that view; a mesh in N views submits N times. Building PORTAL pass is cone-exempt (punches always attempted; GetClip <3 points is the real stop, pc:429302-10). - Cached-flag culled: terrain cells via in_view stamped by draw_check_blocks @0x00505f80 iterating EVERY view of PortalList (blocks wholly outside every exit view skip at pc:267936). Terrain polys then draw whole + immediate (landPolysDraw @0x006b7040 — no view clip, no alpha list). - No test at all: EnvCell shell geometry; seal z-behavior (ALWAYS). - `alwaysDrawObjects = 1` (@0x00820ed4): DrawSortCell runs for EVERY cell of a drawn block — the per-part per-view cone check is the real object gate (no pixel leak, just CPU). ## The alpha lists (retail) TWO static FIFO lists, 3000 entries each (Clip @0x00871b10, Alpha @0x008af370). NO depth key stored, NO sort at flush — order = submission order; far-to-near correctness comes from the cell walk + per-cell CYpt insertion sort. Overflow DROPS the subset (pc:424983). Entry routing: s_AlphaDelayMask (default 0xE) vs the surface mask; bit3 → Clip list. Flush sites (grep-complete): | # | Site | Arg | Depth in place | |---|------|-----|----------------| | 1 | DrawBuilding @0x0059f30b | 0f FULL | outdoor-so-far (pre-punch) | | 2 | DrawBlock @0x005a1a07 | 0.75 valve, fires per outdoor cell (BN's `flush>1` guard is a MISREAD — Ghidra: fires when flush<=1.0) | mid-landscape | | 3 | DrawCells @0x005a4872 | 0f FULL | complete outdoor depth (pre-clear) | | 4 | RenderNormalMode @0x00453b8b | 0f FULL (final) | interior + seals | | 5 | CreatureMode::Render (chargen only) | 0f | its own viewport | ## Depth at final drain (interior root) - Aperture/vista pixels: the SEAL's portal-plane depth (outdoor per-pixel depth NO LONGER EXISTS — cleared; outdoor COLOR survives under the stamp). Interior alpha fragments behind the portal plane FAIL there. - Outdoor particles (falls): drawn in the landscape pass, drained at site #3 vs outdoor depth. Cone-PARTIAL spill outside the aperture is buried because EVERY interior pixel paints after every outdoor drain point — protection is pure ORDERING, not depth. ## ShouldDrawParticles @0x0050fe60 — an UPDATE-time gate Called per emitter from ParticleEmitter::UpdateParticles (physics-side), NOT during rendering: `ExaminationObject → 1; CYpt > degrade_distance → 0; cell NULL → 0; cell->IsInView()==0 → 0`. On false: SetNoDraw(1), degraded_out=1 (particles stop updating AND drawing). - CLandCell::IsInView @0x00532cb0 = the PREVIOUS frame's stamped in_view (one-frame-stale gate). - **CEnvCell does NOT override it** — the slot resolves to the ICF-folded constant @0x005269f0 `return 1` (PARTIALLY_INSIDE, PDB-verified): for interior emitters the cell test is VACUOUS; distance is the only update-time gate; their draw is flood-gated instead. ## Can content outside the flood/exit views draw? NO. Interior: pure flood membership. Outdoor blocks/cells: in_view under ≥1 exit view. Outdoor parts: per-exit-view sphere-cone in DrawMesh. Building portal pass cone-exempt but clip-stopped. No distance bypass exists. ## Corrections to campaign docs 1. DrawBlock per-cell flush polarity RESOLVED (fires; 0.75 pressure valve) — BN misread. 2. The m_nFrameStamp bump separates the landscape and interior passes of ONE interior-root frame; look-ins NEVER bump (outdoor_pview ov==0). 3. NEW: CEnvCell::IsInView = constant PARTIALLY_INSIDE (ICF fold, symbols.json 19-name cluster) — not in any FW doc. ## UNVERIFIED (the agent's own list) The 0xF418 falls emitters' owner (land cell vs EnvCell static — dat question; note the DAT-geometry companion resolves this: landblock object-list stabs); z-write during alpha drains; insertion_sort direction byte-proof; the ±12 reject polarity; GameSky depth behavior.