acdream/docs/research/2026-06-05-retail-pview-indoor-render-pseudocode.md
Erik 1405dd8e90 feat(render): indoor render WORKS — terminating portal flood + every-cell seal + look-in FPS
Checkpoint of the unified retail-faithful indoor render. The two-week HANG/grey is fixed and the
interior seals (live-verified by the user). Commits the session render-rewrite foundation together
with the fixes that made it functional.

- HANG fix: PortalVisibilityBuilder.Build portal flood did not terminate (the faithful ProjectToClip
  near-side clip drifts per round, defeating the CellView dedup; the BFS had no bound after U.2a removed
  MaxReprocessPerCell). Fix = drift-tolerant snapped/canonical CellView.Add dedup (PortalView.cs) plus
  restored MaxReprocessPerCell=16 bounded re-enqueue (PortalVisibilityBuilder.cs). Re-enqueue is kept
  (load-bearing for late-slice propagation, Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit);
  only its count is capped. CellViewDedupTests added.
- Seal (DrawCells Task 2): RetailPViewRenderer.DrawEnvCellShells draws EVERY visible cell via
  IndoorDrawPlan.ShellPass (was gated on the ClipFrameAssembler slot filter, leaving slot-less cells grey).
- Look-in FPS: GameWindow exterior look-in candidates limited to the player landblock +-1 (was all ~81
  loaded LBs iterated every outdoor frame). No behaviour change (far cells were >48m, already culled).

Remaining dominant issue = the FLAP at transitions: viewer-cell metastability (render roots at the
camera-eye cell, which oscillates outdoor-indoor as the 3rd-person boom drifts across the doorway,
confirmed in render-sig). SEPARATE fix, NOT the DrawCells port. Full handoff + flap fix plan + tracked
follow-ups (#78 terrain, look-in-from-inside, look-in FPS, L-spotlight):
docs/research/2026-06-07-indoor-render-session-handoff.md.

Baselines: build 0 err; App.Tests 210/210; Core.Tests 1331 pass / 4 fail (pre-existing) / 1 skip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 10:14:43 +02:00

4.3 KiB

Retail PView Indoor Render Pseudocode (2013 EoR)

This note pins the indoor render port to the named retail decomp. The goal is behavioral fidelity: modern GL renderers may supply the draw calls, but the frame ownership, visibility graph, and draw order follow these functions.

SmartBox::RenderNormalMode @ 0x00453aa0

if render device has open scene:
    outside = SmartBox::is_player_outside(player position)
    seenOutside = outside || viewer_cell.seen_outside
    set FOV/view distance

    if !outside:
        if seenOutside:
            LScape::update_viewpoint(lscape, Position::get_outside_cell_id(viewer))
        Render::update_viewpoint(viewer)
        RenderDeviceD3D::DrawInside(viewer_cell)
    else:
        LScape::update_viewpoint(lscape, viewer.objcell_id)
        Render::update_viewpoint(viewer)
        Render::set_default_view()
        Render::useSunlightSet(1)
        LScape::draw(lscape)

FlushAlphaList()
run targeting/render callbacks

Important split: the top-level branch follows is_player_outside, while indoor render calls DrawInside(viewer_cell).

RenderDeviceD3D::DrawInside @ 0x0059f0d0

PView::DrawInside(RenderDeviceD3D::indoor_pview, viewer_cell)

This is a thin forwarder. The PView owns the indoor frame.

PView::DrawInside @ 0x005a5860

reset object scale
CEnvCell::curr_view_push(root_cell)
PView::add_views(root_cell.num_stabs, root_cell.stab_list)
Frame::cache()
Render::positionPush(root identity frame)
Render::copy_view(root_cell.portal_view[last], null, 4)   # full-screen root view
forceClear = PView::ConstructView(root_cell, 0xffff)
PView::DrawCells(forceClear)
Render::framePop()
PView::remove_views(root_cell.num_stabs, root_cell.stab_list)
root_cell.num_view--

PView::ConstructView(CEnvCell*) @ 0x005a57b0

clear outside_view and cell draw/todo state
insert root cell into distance-priority todo list

while todo is not empty:
    cell = pop nearest
    append cell to cell_draw_list
    InitCell(cell, otherPortalId)
        project/clip each portal against the current cell view
        exit portals append clipped polygons to outside_view
        interior portals append clipped polygons to neighbor portal_view
        newly discovered neighbors enter the todo list once

return forceClear flag

cell_draw_list is the only indoor membership source. Later growth can add view polygons to a discovered cell, but does not create a second draw-list entry.

PView::DrawCells @ 0x005a4840

if outside_view.view_count > 0:
    Render::useSunlightSet(1)
    Render::PortalList = this
    LScape::draw(lscape)                         # landscape clipped by outside_view
    D3DPolyRender::FlushAlphaList(0)
    render_device.frameStamp++
    if forceClear || portalsDrawnCount != 0:
        render_device.Clear(DepthOnly)

    # Loop 1: exit portal masks, reverse cell_draw_list
    for cell in reverse(cell_draw_list):
        if cell.structure.drawing_bsp:
            push cell frame and surfaces
            for each current portal_view slice:
                CEnvCell::setup_view(cell, slice)
                for each exit portal:
                    DrawPortalPolyInternal(portal polygon)
            pop frame

Render::useSunlightSet(0)
Render::restore_all_lighting()

# Loop 2: closed cell shells, reverse cell_draw_list
for cell in reverse(cell_draw_list):
    if cell.structure.drawing_bsp:
        push cell frame and surfaces
        for each current portal_view slice:
            CEnvCell::setup_view(cell, slice)
            DrawEnvCell(cell)
        pop frame

# Loop 3: cell object lists, reverse cell_draw_list
for cell in reverse(cell_draw_list):
    Render::PortalList = cell.portal_view[last]
    DrawObjCellForDummies(cell)

restore object scale
Render::useSunlightSet(1)

There is no global indoor object, terrain, sky, weather, or particle pass. Every visible indoor object comes from the cell draw list, and the landscape appears only through outside_view.

RenderDeviceD3D::DrawObjCellForDummies @ 0x005a0760

for object in cell.object_list:
    draw object under Render::PortalList
    attached effects/particles follow the owning object visibility

acdream maps this to per-cell WorldEntity.ParentCellId buckets. Parentless live objects must not bypass the indoor PView graph.