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>
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.