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>
135 lines
4.3 KiB
Markdown
135 lines
4.3 KiB
Markdown
# 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
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
PView::DrawInside(RenderDeviceD3D::indoor_pview, viewer_cell)
|
|
```
|
|
|
|
This is a thin forwarder. The PView owns the indoor frame.
|
|
|
|
## PView::DrawInside @ 0x005a5860
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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.
|