acdream/docs/research/2026-08-30-fw-walk-pseudocode-appendix.md
Erik a6885aa2f0 research(render) Campaign FW0: the frame-walk pseudocode model + decomp appendix
The distilled port-ready model for FW1: camera-cell rooting, the
invisible-panel primitive (punch far-Z / seal own-depth, byte-verified
constants), the far-to-near landscape walk, the building two-pass portal
machinery with its push/pop asymmetry, the interior flood + DrawCells
passes, the view machinery, and the constants/struct anchor table. The
panel question is resolved: retail DOES draw depth-only portal-polygon
panels via DrawPortalPolyInternal - AD-117 re-invented a real mechanism
at the wrong site. FW0 is complete: oracle fixtures, replay helper,
decomp model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 09:28:19 +02:00

1218 lines
108 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# FW0 decomp-read appendix - raw extraction reports (2026-08-30)
Five parallel reads of the named retail decomp (BN pseudo-C cross-checked against Ghidra patchmem where flag-mush demanded it). The distilled port model is docs/research/2026-08-30-fw-walk-pseudocode.md; this appendix preserves the complete per-function extractions verbatim.
## Report 1 - Rooting (RenderNormalMode / update_viewer / set_default_view)
### SmartBox::RenderNormalMode @0x00453aa0
**Summary:** The per-frame world-render root. If the scene is open, roots the frame on ONE test: is the viewer (camera) position's cell id outdoor (low word < 0x100)? Outdoor: full-screen default view + sunlight + LScape::draw. Indoor: optional landscape-viewpoint refresh (only when viewer_cell->seen_outside) then RenderDevice::DrawInside(viewer_cell). Afterwards flushes the alpha list, fires the target bounding-box callback and the generic rendering callback.
```c
void SmartBox::RenderNormalMode() {
RenderDevice* rd = RenderDevice::render_device;
if (rd->m_bOpenScene) {
bool viewer_outdoors = (viewer.objcell_id & 0xFFFF) < 0x100; // THE rooting decision
bool sees_outside = viewer_outdoors || viewer_cell->seen_outside; // short-circuit: seen_outside only read when indoors
if (!m_bUseViewDistance)
Render::SetFOVRad(m_fGameFOV / (rd->m_ViewportAspectRatio - 0.1f)); // 0x3dcccccd
else
Render::set_vdst(m_fViewDistFOV);
if (viewer_outdoors) {
LScape::update_viewpoint(lscape, viewer.objcell_id);
Render::update_viewpoint(&viewer);
Render::set_default_view(); // full-screen clip window, no portal clipping
Render::useSunlightSet(1);
LScape::draw(lscape); // landscape+buildings pass (the trace's outdoor frame)
} else {
if (sees_outside) { // == viewer_cell->seen_outside here
uint outCell = Position::get_outside_cell_id(&viewer);
LScape::update_viewpoint(lscape, outCell); // pre-arm LScape so DrawCells(ov=1) can draw it through the exit view
}
Render::update_viewpoint(&viewer);
rd->vtbl->DrawInside(rd, (CEnvCell*)viewer_cell); // vtbl slot +0x48 = DrawInside(CEnvCell*); NO set_default_view, NO useSunlightSet here
}
}
D3DPolyRender::FlushAlphaList(0.0f);
if (target_object_id != 0 && target_callback != NULL) {
ObjectSelectStatus st = SmartBox::GetObjectBoundingBox(this, target_object_id, &rect, &depth);
target_callback(target_object_id, st, &rect, depth);
}
if (m_renderingCallback) m_renderingCallback();
}
// Frame-root chain (callers):
// SmartBox::Draw @0x00455570: { if (hidden) return; tailcall DrawNoBlit(); }
// SmartBox::DrawNoBlit @0x00454c20:
// SetNormalMode(); // leaves creature (portrait) mode if active, restores game ambient, flush_cells
// if (player) { update_viewer(); // resolves viewer + viewer_cell (see below)
// if (viewer_cell != NULL) RenderNormalMode(); } // nothing drawn when camera cell unresolved
// if (lookingForObject) { harvest Render::GetMouseSelectionPartIndex/ObjectID -> click_object_*; ECM_UI::SendNotice_SmartBoxObjectFound; lookingForObject=0; }
// Render::clear_selection_cursor();
```
**Gotchas:** BN pseudo-C mangles the rooting condition into register mush (`edi_2 = -((edi - edi))`); Ghidra on the same PDB-paired binary resolves it as `((viewer.objcell_id & 0xFFFF) < 0x100)` — i.e. the test is on the VIEWER (camera) position's cell id, not the player's, and not a null test on viewer_cell. `viewer_cell` is guaranteed non-null by DrawNoBlit's guard; outdoors it is the camera's CLandCell but is not used by the outdoor branch. `sees_outside` is computed before the FOV block but only matters on the indoor branch (short-circuit protects the deref order, not a semantic OR). The FOV divisor constant is aspect_ratio - 0.1f (0x3dcccccd). Vtable offset 0x48 verified against RenderDeviceVtbl in acclient.h = DrawInside(CEnvCell*). Note the indoor branch does NOT call set_default_view or useSunlightSet — DrawInside/PView machinery owns view + lighting setup there.
### SmartBox::update_viewer @0x00453ce0
**Summary:** Resolves the camera position (`viewer`) and camera cell (`viewer_cell`) each frame, called by DrawNoBlit immediately before RenderNormalMode. Sweeps the global viewer collision sphere from a pivot point on the player (CameraManager pivot part + pivot_offset) toward viewer_sought_position (produced by CameraManager::UpdateCamera via SmartBox::PlayerPhysicsUpdatedCallback) using the full CTransition collision machinery; the sweep's final curr_pos/curr_cell become viewer/viewer_cell. Two fallbacks: AdjustPosition on the raw sought position, else camera at player position with viewer_cell = NULL (frame not rendered).
```c
void SmartBox::update_viewer() {
CPhysicsObj* player = this->player;
if (!player) return;
if (player->cell == NULL) {
CPhysicsObj::reenter_visibility(player);
player = this->player;
if (player->cell == NULL) { set_viewer(&player->m_position, 1); viewer_cell = NULL; return; }
}
Render::player_pos.objcell_id = player->m_position.objcell_id; // global used by lighting distance math
Render::player_pos.frame = player->m_position.frame;
if (static_camera) return; // global debug toggle: keep last viewer
Position sought = viewer_sought_position; // camera's DESIRED position (CameraManager output)
CameraManager* cm = camera_manager;
// pivot frame: a specific setup part when pivot_part_index valid, else the object frame
Frame F;
if (cm->pivot_part_index != -1 && cm->pivot_part_index < player->part_array->num_parts)
F = player->part_array->parts[cm->pivot_part_index]->pos.frame;
else
F = player->m_position.frame;
F.m_fOrigin += F.localtoglobalvec(cm->pivot_offset); // pivot offset rotated into world axes (writes Frame +0x34..0x3C = m_fOrigin)
// resolve the START cell containing the pivot point
CObjCell* startCell;
if ((player->m_position.objcell_id & 0xFFFF) < 0x100) {
startCell = player->cell; // outdoors: pivot stays in the player's landcell
} else {
Position p = { player->m_position.objcell_id, F };
if (!CPhysicsObj::AdjustPosition(&p, &viewer_sphere, &startCell, 0, 1))
startCell = player->cell; // fallback
}
Position startPos = { startCell->m_DID.id, F }; // pivot expressed in startCell
sought.frame.m_fOrigin = Position::localtoglobal(&startPos, ..., &sought); // sought origin re-based into startCell's block space
Position endPos = { startCell->m_DID.id, sought.frame };
CTransition* t = CTransition::makeTransition();
if (!t) return;
CTransition::init_object(t, player, 0x5C); // OBJECTINFO state = 0x5C
CTransition::init_sphere(t, 1, &viewer_sphere, 1.0f); // ONE sphere, global viewer_sphere, scale 1
CTransition::init_path(t, startCell, &startPos, &endPos); // sweep pivot -> desired camera spot
if (CTransition::find_valid_position(t)) {
set_viewer(&t->sphere_path.curr_pos, 0);
viewer_cell = t->sphere_path.curr_cell; // <- THE camera cell (CEnvCell indoors / CLandCell outdoors)
} else if (CPhysicsObj::AdjustPosition(&sought /*local_120*/, &viewer_sphere, &cellOut, 0, 1)) {
set_viewer(&sought, 0);
viewer_cell = cellOut; // fallback 1: desired spot pushed into a valid cell
} else {
set_viewer(&player->m_position, 1); // fallback 2: camera collapses onto the player,
viewer_cell = NULL; // viewer_cell NULL => DrawNoBlit skips RenderNormalMode
}
CTransition::cleanupTransition(t);
}
```
**Gotchas:** Ghidra mislabels the pivot-offset add as `local_16c.fl2gv[6..8]` — its own stale Frame typedef; BN's stack offsets (+0x34/+0x38/+0x3C from the Frame base) match acclient.h's Frame {quat 0x00; m_fl2gv[9] 0x10; m_fOrigin 0x34}, so the target is m_fOrigin. The Position::localtoglobal arg list is heuristic in both decomps (BN shows 3 args, the retail signature has a point arg); the semantic effect — sought origin re-expressed relative to startCell's landblock — is solid from the surrounding data flow. init_object flag 0x5C reads, per GDLE's ObjectInfoEnum naming, as IS_VIEWER(0x4)|PATH_CLIPPED(0x8)|FREE_ROTATE(0x10)|PERFECT_CLIP(0x40) — interpretive, verify before porting names. `viewer_sphere` is a global CSphere (the camera's collision ball); its radius initializer was not chased in this pass. Note set_viewer itself nulls viewer_cell unconditionally, so the success paths assign viewer_cell AFTER the set_viewer call — preserve that order. viewer_sought_position is written by SmartBox::PlayerPhysicsUpdatedCallback @0x00452d60 = CameraManager::UpdateCamera output, i.e. camera desire updates on physics ticks, camera RESOLUTION happens per render frame here.
### SmartBox::set_viewer @0x00452c40
**Summary:** Commits a camera position: copies pos into `viewer` (and into `viewer_sought_position` when arg3 != 0 — the failure/reset paths), NULLs viewer_cell, rebuilds the single viewer dynamic light (offset z=+2m above the player when a player exists, else at the viewer itself, for light types 0/2), re-adds cell dynamic lights, and pushes the viewer position into SoundManager, LScape sky, and SceneTool camera.
```c
void SmartBox::set_viewer(const Position* pos, int reset_sought) {
viewer.objcell_id = pos->objcell_id; viewer.frame = pos->frame;
if (reset_sought) { viewer_sought_position = *pos; }
viewer_cell = NULL; // caller reassigns on success paths
viewer_light.intensity = s_fViewerLightIntensity;
viewer_light.falloff = s_fViewerLightFalloff;
Render::world_lights.num_dynamic_lights = 0;
if (player != NULL) {
if (viewer_light.type == 0 || viewer_light.type == 2) viewer_light.offset.m_fOrigin = (0, 0, 2.0f);
lightCell = player->m_position.objcell_id; lightFrame = &player->m_position.frame;
} else {
if (viewer_light.type == 0 || viewer_light.type == 2) viewer_light.offset.m_fOrigin = (0, 0, 0);
lightCell = viewer.objcell_id; lightFrame = &viewer.frame;
}
Render::add_dynamic_light(&viewer_light, lightCell, lightFrame);
CObjCell::add_dynamic_lights();
SoundManager::SetPlayerPosition(&viewer);
LScape::set_sky_position(lscape, &viewer);
SceneTool::SetupCamera(&viewer);
}
```
**Gotchas:** arg3 semantics: 1 = also overwrite viewer_sought_position (used when the camera is forced onto the player after a resolution failure, and by teleport paths at 0x004538d5); 0 = normal per-frame commit. The unconditional `viewer_cell = NULL` means callers MUST set viewer_cell after calling — an easy ordering bug in a port.
### Position::get_outside_cell_id @0x004527b0
**Summary:** Projects a position to the outdoor landcell gid that contains its x,y. Copies origin + objcell_id, calls LandDefs::adjust_to_outside on the copies, returns the adjusted outdoor cell id on success, 0 on failure. Used by RenderNormalMode's indoor branch to point LScape's viewpoint at the landcell 'outside' the viewer's EnvCell.
```c
uint Position::get_outside_cell_id() {
Vector3 p = frame.m_fOrigin;
uint cid = objcell_id;
int ok = LandDefs::adjust_to_outside(&cid, &p); // regparm
return ok ? cid : 0; // (BN renders the select as reg mush; Ghidra: -(uint)(ok!=0) & cid)
}
// LandDefs::adjust_to_outside @0x005a9bc0:
// low16 of cid must be in [1,0x40] (outdoor cells) or [0x100,0xFFFD] (envcells) or == 0xFFFF (block-only), else fail;
// |x| < 0.0002f -> x = 0; |y| < 0.0002f -> y = 0; // epsilon snap
// if (LandDefs::get_outside_lcoord(cid, &p, &lx, &ly)) {
// cid = LandDefs::lcoord_to_gid(lx, ly);
// p.x = p.x - floor(p.x / C) * C; p.y likewise; // wrap into cell-local range
// return 1;
// }
// cid = 0; return 0; // failure zeroes the id
```
**Gotchas:** The wrap constant C in adjust_to_outside is FPU-elided by BN (prints as `0f`) — recover it from the raw binary (reference_pe_byte_decode.md) before porting; it is the cell/block modulus, not zero. The epsilon 0.000199999995f is exact (2e-4). Both decomps' return-select expressions are branchless artifacts of `neg/sbb`; semantics are the ternary shown.
### Render::set_default_view @0x0054ef50
**Summary:** Resets the portal-view system to one full-screen view. Lazily initializes a function-static `window` (portal_view_type: portal/poly/vertex DArrays, blocksize 0x80, view_timestamp 0), zeroes its view_count and vertex_count_total, calls copy_view(&window, NULL, 0) to synthesize the 4-vertex full-viewport clip polygon, sets Render::PortalList = &window, and makes it current via Render::set_view(&window.view, 0). This is the outdoor frame's 'no portal clipping' state; interior rendering replaces it with portal-clipped views.
```c
void Render::set_default_view() {
static portal_view_type window; // one-time static init:
// window.portal.{data,sizeOf,next_available}=0, blocksize=0x80;
// window.view.poly.{...}=0, blocksize=0x80; window.view.vertex.{...}=0, blocksize=0x80;
// window.view_timestamp = 0; atexit(dtor);
window.view_count = 0;
window.view.vertex_count_total = 0;
Render::copy_view(&window, NULL, 0);
Render::PortalList = &window;
Render::set_view(&window.view, 0);
}
// copy_view(&window, NULL, 0) — the src==NULL branch @0x0054e003 (else-arm):
// poly[view_count] = { num_pts = 4, vertex_index = vertex_count_total };
// vertices (stride 0x18): v0=(0, H), v1=(W, H), v2=(W, 0), v3=(0, 0), v4=v0 repeated // W/H = RenderDevice viewport width/height
// then computes and stores poly xmin/xmax/ymin/ymax from the vertices (here: 0..W, 0..H);
// when newmethod != 1, additionally builds per-edge view planes from Render::Yaxis * Render::vdst (frustum side planes).
//
// Render::set_view(view_type* v, int n) @0x0054d0e0 — makes poly n of v the ACTIVE clip window:
// Render::portal_view = v; Render::portal_view_num = n;
// Render::portal_npnts = v->poly.data[n].num_pts;
// Render::portal_inmask = (1 << (npnts + 1)) - 1;
// Render::portal_vertex = &v->vertex.data[v->poly.data[n].vertex_index];
// Render::xmin/xmax/ymin/ymax = poly[n].xmin/xmax/ymin/ymax;
```
**Gotchas:** Other production callers: Render::Set3DViewInternal @0x0054f070 (after SetFOVInternal, clearing selection state) and SmartBox::GetObjectBoundingBox @0x00452e4a plus the creature-portrait path @0x00452bb9 — so the 'default full-screen view' is also the baseline for selection/bounds math, not only outdoor world draw. copy_view's screen-space poly winding here is (0,H)->(W,H)->(W,0)->(0,0) with the first vertex duplicated at the end — preserve the duplicate; portal_inmask's (1<<(n+1))-1 counts it. The `newmethod` global gates the per-edge plane build; its value at runtime was not established in this pass (live-read before assuming a branch runs — feedback_live_read_capability_gates).
**Report notes:** Reconciliation with the 2026-08-30 fw-walk oracle: the trace's two frame shapes fall directly out of RenderNormalMode's single rooting test on the CAMERA cell id, (viewer.objcell_id & 0xFFFF) < 0x100. Outdoor frames: set_default_view (full-screen clip window) + useSunlightSet(1) + LScape::draw the interleaved DrawBuilding far-to-near and PView::DrawCells(ov=0) look-in punches happen INSIDE LScape::draw / the building draw, not in this function. Interior frames: RenderDevice::DrawInside(viewer_cell) with NO set_default_view and NO sunlight-set switch at this level (PView/DrawInside owns those); the only landscape work RenderNormalMode does indoors is pre-arming LScape::update_viewpoint with Position::get_outside_cell_id(&viewer) when viewer_cell->seen_outside — which is exactly what lets DrawCells(ov=1) draw LScape through the exit view. Camera pipeline ownership: CameraManager::UpdateCamera writes viewer_sought_position on physics updates (SmartBox::PlayerPhysicsUpdatedCallback @0x00452d60); SmartBox::update_viewer resolves it per render frame by sweeping the global viewer_sphere from the player pivot with the full CTransition collision machinery (OBJECTINFO 0x5C, one sphere, scale 1.0), so viewer_cell is a collision-resolved cell, never a lookup by coordinates; when resolution fails entirely, viewer_cell = NULL and DrawNoBlit skips RenderNormalMode — the frame draws no world. Frame chain: SmartBox::Draw (hidden gate) -> DrawNoBlit -> SetNormalMode (portrait-mode exit) -> update_viewer -> [viewer_cell != NULL] RenderNormalMode -> mouse-selection harvest -> clear_selection_cursor. Cross-checks used: Ghidra MCP (patchmem.gpr, port 8081) decomps of 0x00453aa0/0x00453ce0/0x004527b0/0x00454c20 to resolve BN register mush, and acclient.h verbatim structs (SmartBox @35189, Frame @30647, Position @30658, RenderDeviceVtbl @39019 — slot +0x48 = DrawInside(CEnvCell*), CameraManager @35238). Source file: docs/research/named-retail/acclient_2013_pseudo_c.txt (RenderNormalMode at line 92635; update_viewer 92761; set_viewer 91780; get_outside_cell_id 91552; set_default_view 345550; set_view 343750; copy_view 344784; adjust_to_outside 438719; DrawNoBlit 93707; Draw 94280).
## Report 2 - Interior recursion (DrawInside / DrawCells / remove_views)
### PView::DrawInside @0x005a5860
**Summary:** Interior-frame root draw. Given the camera's CEnvCell, it pushes a fresh view onto that cell, adds views for every cell in the cell's stab list, pushes an identity Position stamped with the cell's DID, seeds the top view as the full screen, runs the ConstructView flood (portal BFS that builds cell_draw_list and may add exit views into outside_view), draws everything via DrawCells, then pops the position and unwinds every view it pushed.
```c
void PView::DrawInside(PView* this, CEnvCell* cell) {
Render::object_scale_vec = (1,1,1); Render::object_scale = 1.0f;
CEnvCell::curr_view_push(cell); // cell->num_view += 1; (re)allocs+zeroes portal_view.data[num_view] (0x48 bytes: portal DArray, view poly/vertex DArrays, blocksize 0x80) and resets view_count=0, update_count=0, view_timestamp=0
PView::add_views(this, cell->num_stabs, cell->stab_list); // curr_view_push on every stab cell that GetVisible() finds
Position pos; // local, vtable 0x796910
pos.objcell_id = 0;
pos.frame = identity; // quat (1,0,0,0), origin (0,0,0)
Frame::cache(&pos.frame); // build cached rotation matrix
pos.objcell_id = cell->m_DID.id; // Ghidra-confirmed: DID stored AFTER Frame::cache, before push
Render::positionPush(3, &pos); // push identity position, mode 3
Render::copy_view(cell->portal_view.data[cell->num_view - 1], nullptr, 4); // seed the cell's TOP view as the full screen (null clip pts, 4 corners)
PView::ConstructView(this, cell, 0xffff); // CEnvCell overload @0x005a57b0: outside_view.view_count = 0; master_timestamp++; BFS: InitCell + InsCellTodoList, then pop todo -> append to cell_draw_list (grow by +0x1e), mark top view cell_view_done=1, ClipPortals(cell,0) -> AddViewToPortals; exit portals feed outside_view
PView::DrawCells(this, 0); // literal 0 pushed (Ghidra); the param is DEAD inside DrawCells
Render::framePop(); // Ghidra names this call Render::positionPop() — undoes positionPush
PView::remove_views(this, cell->num_stabs, cell->stab_list); // num_view -= 1 on every visible stab cell
cell->num_view -= 1; // undo the initial curr_view_push on the camera cell
}
```
**Gotchas:** BN shows `edx_2 = ConstructView(...)` then `DrawCells(this, edx_2)` — that is a fastcall misdetect; Ghidra shows the real pushed literal is 0 (BN's stray `var_60_2 = 0` IS that stack argument), and DrawCells never reads it anyway. BN drops the `pos.objcell_id = cell->m_DID.id` store (shows a dead `uint32_t id = arg2->m_DID.id`); Ghidra confirms the store into the local Position after Frame::cache. BN names the epilogue call Render::framePop, Ghidra names the same call site Render::positionPop — it is the counterpart of positionPush(3, …). num_stabs/stab_list live on the CObjCell base (acclient.h ~30927), not on CEnvCell itself. remove_views only decrements num_view — the portal_view slot object stays allocated for reuse.
### PView::DrawCells @0x005a4840
**Summary:** Draws the flooded cell list built by ConstructView, far-to-near (cell_draw_list walked from cell_draw_num-1 down to 0). Gate: only if outside_view.view_count != 0 (the trace's 'ov') does it draw the landscape through the exit views, conditionally clear, and punch the never-drawn portal polys of portals whose other_cell_id == -1. Then, unconditionally, it draws every cell's BSP geometry per view and finally the cells' contained objects with Render::PortalList pointed at each cell's top view.
```c
void PView::DrawCells(PView* this, int deadArg /*UNUSED*/) {
// ---- ov branch: only when exit views into the outdoors exist ----
if (this->outside_view.view_count != 0) { // 'ov' in the live trace
Render::useSunlightSet(1);
Render::PortalList = &this->outside_view; // offset 0 of PView; LScape clips through the exit views
LScape::draw(this->lscape);
D3DPolyRender::FlushAlphaList(0.0f);
RenderDevice::render_device->m_nFrameStamp += 1;
// clear condition: forceClear global OR portals were drawn since last reset
bool drawn = (D3DPolyRender::portalsDrawnCount != 0);
D3DPolyRender::portalsDrawnCount = 0; // read-then-reset happens even when forceClear != 0 skips the read? (BN: reset only in forceClear==0 arm; Ghidra: reset in the condition — reset occurs whenever the second operand is evaluated, i.e. when forceClear==0. With forceClear!=0 the count is left alone.)
if (forceClear != 0 || drawn)
RenderDevice::render_device->vtbl->Clear(4, &RGBAColor_Black /*0x00820fc0 = ADDRESS of the constant*/, 1.0f);
// per-cell portal-poly punch pass, far-to-near
for (i = cell_draw_num; i != 0; i--) {
CEnvCell* c = cell_draw_list.data[i-1];
if (c->structure->drawing_bsp == 0) continue;
RenderDeviceD3D::SetCurrentMaterial(render_device, nullptr, 0);
Render::SetSurfaceArray(c->surfaces);
object_scale_vec=(1,1,1); object_scale=1.0f;
Render::positionPush(3, &c->pos);
uint views = (c->num_view == 0) ? 0xFFFF // degenerate sentinel path, see gotchas
: (uint16)c->portal_view.data[c->num_view-1]->view_count;
if (views != 0) { // view_count==0 -> skip straight to pop
for (v = 0; v < views; v++) {
CEnvCell::setup_view(c, v); // = Render::set_view(&top_view->view, v)
for (j = 0; j < c->num_portals; j++) // CCellPortal stride 0x18
if (c->portals[j].other_cell_id == 0xFFFFFFFF) // portal leads OUTSIDE
D3DPolyRender::DrawPortalPolyInternal(c->portals[j].portal, 0);
}
}
Render::framePop(); // positionPop
}
}
// ---- always runs, ov or not ----
Render::useSunlightSet(0);
Render::restore_all_lighting();
// pass 2: cell BSP geometry, far-to-near, per view
for (i = cell_draw_num; i != 0; i--) {
CEnvCell* c = cell_draw_list.data[i-1];
if (c->structure->drawing_bsp == 0) continue;
SetCurrentMaterial(render_device, nullptr, 0);
Render::SetSurfaceArray(c->surfaces);
object_scale_vec=(1,1,1); object_scale=1.0f;
Render::positionPush(3, &c->pos);
uint views = (c->num_view == 0) ? 0xFFFF : (uint16)c->portal_view.data[c->num_view-1]->view_count;
if (views != 0)
for (v = 0; v < views; v++) {
CEnvCell::setup_view(c, v);
render_device->vtbl->DrawEnvCell(c); // vtable +0x5c
}
Render::framePop();
}
// pass 3: contained/static objects, far-to-near — no drawing_bsp gate, no per-view loop
for (i = cell_draw_num; i != 0; i--) {
CEnvCell* c = cell_draw_list.data[i-1];
Render::PortalList = c->portal_view.data[c->num_view - 1]; // objects clip against the CELL's top view
render_device->vtbl->DrawObjCellForDummies(c); // vtable +0x64
}
object_scale_vec=(1,1,1); object_scale=1.0f;
Render::useSunlightSet(1);
}
```
**Gotchas:** (1) The int parameter (0 from DrawInside, 1 from DrawPortal) is NEVER read — the live trace's 'ov' is NOT this argument, it is outside_view.view_count read at entry (confirmed by the fw-walk-oracle README line format 'DC pv=… ov=<outside_view.view_count>'). (2) BN's 0x820fc0 in the Clear call is the ADDRESS of the RGBAColor_Black constant, not a packed color; flags=4, z=1.0f — the flag's D3D mapping goes through RenderDevice::Clear, don't assume raw D3DCLEAR bits. (3) The num_view==0 path loads a 0xFFFF(-1 masked to uint16) view count and would spin 65535 setup_view iterations — a degenerate path that can't be hit in practice because every cell in cell_draw_list got curr_view_push'd (num_view>=1); do not port it as meaningful behavior. (4) portalsDrawnCount is reset only on the forceClear==0 arm (short-circuit). (5) Punch pass draws portal polys only for portals with other_cell_id == -1 (CCellPortal offset 0, stride 0x18; poly at offset 8) — i.e. portals opening to the landscape — once per view of the cell's TOP portal_view. (6) Passes 1/2 gate on structure->drawing_bsp != 0; pass 3 (objects) does not, and it swaps Render::PortalList per cell without restoring it. (7) view_count checks in Ghidra are !=0 (BN shows >0 on an int16) — counts are non-negative in practice so equivalent.
### PView::remove_views @0x005a42e0
**Summary:** Exact inverse of PView::add_views: walks a stab list (count + id array) and decrements num_view on every cell CEnvCell::GetVisible resolves, popping the view that add_views pushed via curr_view_push. It frees nothing — the portal_view slot object (0x48 bytes) stays allocated on the cell for reuse by the next push.
```c
void PView::remove_views(PView* this, uint16 num_stabs, uint32* stab_list) {
if (num_stabs > 0) {
for (i = 0; i < num_stabs; i++) {
CEnvCell* c = CEnvCell::GetVisible(stab_list[i]);
if (c != 0)
c->num_view -= 1; // pop the top view; no deallocation, no field reset
}
}
}
```
**Gotchas:** Cells that were visible at add_views time but no longer resolve via GetVisible at remove time are silently skipped — a cell unloaded mid-draw would leak a num_view increment (can't happen within one frame). Counterpart add_views @0x005a5210 has the identical shape but calls CEnvCell::curr_view_push, which resets view_count/update_count/view_timestamp to 0 on the (re)used top slot; remove_views does NOT touch those fields on the way down.
**Report notes:** Cross-checked BN pseudo-C (docs/research/named-retail/acclient_2013_pseudo_c.txt) against the live Ghidra MCP decomp (patchmem, port 8081) for all three bodies plus DrawPortal; struct offsets verified in docs/research/named-retail/acclient.h. Key reconciliation with the 2026-08-30 fw-walk-oracle trace: 'ov' in the DC log lines is this->outside_view.view_count at DrawCells entry (the README's own line format says so), NOT DrawCells' int argument — that argument (0 from DrawInside @0x005a5952, 1 from DrawPortal @0x005a5b53) is dead code inside DrawCells. So interior frames with a surviving exit-view chain hit the ov!=0 branch (LScape drawn through the exit views with Render::PortalList = &outside_view, conditional depth-ish Clear(4, black, 1.0), portal-poly punches for other_cell_id==-1 portals), while foundry-deep-style ov=0 frames skip straight to the BSP + object passes. Supporting bodies read: PView::add_views @0x005a5210, CEnvCell::curr_view_push @0x005a5090 (num_view++ with 0x48-byte lazy slot alloc + counter reset), PView::ConstructView(CEnvCell,uint16) @0x005a57b0 (zeroes outside_view.view_count, bumps master_timestamp, BFS via cell_todo_list -> cell_draw_list with +0x1e growth, sets top view's cell_view_done=1, ClipPortals->AddViewToPortals), PView::ConstructView(CBldPortal,CPolygon,int,int) @0x005a59a0 (sidedness test against ±0.0002 with portal_side gating, GetClip, copy_view into the other cell's top view, optional DrawPortalPolyInternal, recursive ConstructView through other_portal_id), PView::DrawPortal @0x005a5ab0, CEnvCell::setup_view @0x0052c430 (= Render::set_view(&top_view->view, i)), PView::GetClip @0x005a4320. PView layout (acclient.h ~45934): outside_view @0, draw_landscape, outdoor_portal_list, cell_draw_list, cell_draw_num, cell_todo_list, cell_todo_num, lscape. portal_view_type (~32346): portal DArray, view_type view, max_indist, view_count, cell_view_done, view_timestamp, update_count. CCellPortal (~32300, stride 0x18): other_cell_id, other_cell_ptr, portal, portal_side, other_portal_id, exact_match. num_stabs/stab_list/seen_outside live on the CObjCell base (~30927).
## Report 3 - Building portal machinery (DrawBuilding family, punch/seal primitive)
### RenderDeviceD3D::DrawBuilding @0x0059f2a0
**Summary:** Per-building draw entry (called from DrawSortCell during far-to-near DrawBlock iteration). Publishes the building's CBldPortal array to the outdoor PView, then runs TWO CPhysicsPart::Draw calls on part 0: first the portal pass (flag 1 → BSP portal walk that punches/constructs look-in views and calls DrawCells), then the normal pass (flag 0 → the visible shell mesh). The 'cover mechanism' is NOT in this body — it is delegated through DrawMeshInternal → build_draw_portals_only → DrawPortal → PView::DrawPortal → DrawPortalPolyInternal (invisible depth-only portal fans).
```c
void RenderDeviceD3D::DrawBuilding(CBuildingObj* b) {
RenderDeviceD3D::outdoor_pview->outdoor_portal_list = b->portals; // ALWAYS, even if nothing draws below
CPhysicsPart::UpdateViewerDistance(b->part_array->parts[0]); // updates deg_level
CPhysicsPart* part = b->part_array->parts[0];
if (part->gfxobj[part->deg_level] != 0) { // degraded-out slot == null → whole building skipped
CBuildingObj::curr_pos = &b->m_position; // static; consumed by PView::DrawPortal restore + DrawBuildingLeaf
Render::curr_detail_surface = Render::building_detail_surface;
Render::curr_detail_tiling = Render::building_detail_tiling;
Render::curr_detail_src_blend = 9;
Render::curr_detail_dst_blend = 6;
D3DPolyRender::FlushAlphaList(0f);
CPhysicsPart::Draw(b->part_array->parts[0], 1); // PORTAL pass: punch far-Z apertures + construct look-in views + DrawCells
RenderDeviceD3D::ObjBuildingOrBuildingPart = 1;
CPhysicsPart::Draw(b->part_array->parts[0], 0); // SHELL pass: normal mesh draw of the building exterior
RenderDeviceD3D::ObjBuildingOrBuildingPart = 0;
Render::curr_detail_surface = nullptr;
}
}
```
**Gotchas:** ObjBuildingOrBuildingPart=1 wraps ONLY the shell pass, not the portal pass. curr_pos is the only write to that static in the whole binary. The portal pass and shell pass both go through the same CPhysicsPart::Draw → vtable DrawMesh path; the arg (1 vs 0) is what forks them inside DrawMeshInternal. No render-state (z/depth) changes happen in this body itself — all depth trickery lives in DrawPortalPolyInternal.
### RenderDeviceD3D::DrawMeshInternal @0x0059f360
**Summary:** Fork point between portal pass and shell pass. Portal flag set → pins building_view to the current portal view index and runs the drawing BSP's portal-only walk twice (pass 1 then pass 2). Flag clear → draws the constructed mesh, with a once-per-frame dedupe for non-player physics parts.
```c
void DrawMeshInternal(CGfxObj* g, uint8 portalFlag, BoundingType bt) {
part = RenderDeviceD3D::s_current_physics_part;
if (portalFlag == 0 && part != 0 && !CPhysicsPart::IsPartOfPlayerObj(part)) {
if (CPhysicsPart::GetDrawnThisFrame(part) != 0) return; // already drawn this frame-stamp → skip entirely
CPhysicsPart::SetDrawnThisFrame(part);
}
if (Render::useSunlight == 0) Render::minimize_object_lighting();
if (g->use_built_mesh != 0) {
if (portalFlag != 0) {
Render::obj_view_set();
saved = RenderDeviceD3D::building_view;
RenderDeviceD3D::building_view = Render::portal_view_num; // pin walk to current portal view
BSPTREE::build_draw_portals_only(g->drawing_bsp, 1); // pass 1: far-Z punch polys
BSPTREE::build_draw_portals_only(g->drawing_bsp, 2); // pass 2: construct views + DrawCells
RenderDeviceD3D::building_view = saved;
return;
}
D3DPolyRender::DrawMesh(g, g->constructed_mesh); // shell geometry
}
}
```
**Gotchas:** If use_built_mesh == 0 nothing at all is drawn (no unbuilt-mesh fallback here). The drawn-this-frame dedupe does NOT apply to the portal pass or the player. building_view pinning means the two-pass portal walk executes once per portal view of the enclosing DrawMesh loop.
### RenderDeviceD3D::DrawMesh @0x005a0860
**Summary:** Vtable DrawMesh (called by CPhysicsPart::Draw). Two modes: Render::PortalList == 0 → single view, viewcone-check the drawing sphere, DrawMeshInternal; PortalList != 0 → loop every portal view i, gated by building_view (== -1 or == i), set_view + viewconeCheck per view, DrawMeshInternal per passing view. Portal-flagged draws (arg4!=0) run DrawMeshInternal even when viewcone says OUTSIDE.
```c
ObjectDrawStatus DrawMesh(CGfxObj* g, Position* pos, uint8 portalFlag) {
result = 0; Render::lighting_type = FULL_LIGHTING;
if (Render::PortalList == 0) {
Render::positionPush(1, pos);
bt = Render::viewconeCheck(g->drawing_sphere);
if (bt != OUTSIDE) { Render::positionPush(2, null); Render::GfxObjUnderSelectionRay(g); DrawMeshInternal(g, portalFlag, bt); Render::framePop(); return 2; }
if (portalFlag != 0) { Render::positionPush(2, null); DrawMeshInternal(g, portalFlag, OUTSIDE); Render::framePop(); return 2; }
result = 1;
} else {
Render::positionPush(1, pos); outsideCount = 0; selectionRayDone = 0;
for (i = 0; i < PortalList->view_count; i++) {
if (building_view == 0xffffffff || building_view == i) {
Render::set_view(&PortalList->view, i);
bt = Render::viewconeCheck(g->drawing_sphere);
if (bt == OUTSIDE) {
if (portalFlag != 0) { positionPush(2, null); DrawMeshInternal(g, portalFlag, OUTSIDE); result = max(result,2); }
else result = max(result,1);
outsideCount++;
} else {
if (!selectionRayDone) { positionPush(2, null); GfxObjUnderSelectionRay(g); selectionRayDone = 1; }
DrawMeshInternal(g, portalFlag, bt); result = max(result,2);
}
}
}
if (outsideCount == PortalList->view_count) result = 1;
}
Render::framePop();
return result;
}
```
**Gotchas:** BN stack mush in the multi-view branch (arg3 reused as the outside counter, phantom 'top' variable) — semantics reconstructed above. Note frame-push imbalance is BN presentation: positionPush(2,...) inside each view is popped by the single framePop pairing per DrawMeshInternal path (framePop pops one level; the trailing framePop pops the level-1 push).
### CPhysicsPart::Draw @0x0050d7a0
**Summary:** Per-part draw wrapper. Skips hidden parts (draw_state bit0) and, for non-portal draws, parts already drawn at the current device frame stamp. Sets material/surfaces/scale, marks Render::check_curr_object, sets s_current_physics_part around the vtable DrawMesh call, and latches selected_object_in_view for the viewcone-check object.
```c
void CPhysicsPart::Draw(CPhysicsPart* p, int portalFlag) {
if (p->draw_state & 1) return; // hidden
if (portalFlag == 0 && p->m_current_render_frame_num == render_device->m_nFrameStamp) return; // per-frame dedupe (normal draws only)
deg = p->deg_level; if (p->degrades == 0 || deg >= p->degrades->num_degrades) deg = 0;
g = p->gfxobj[deg]; if (g == 0) return;
RenderDeviceD3D::SetCurrentMaterial(render_device, p->material, 0);
Render::SetSurfaceArray(p->surfaces);
Render::SetObjectScale(&p->gfxobj_scale);
Render::check_curr_object = (p->physobj && p->physobj->id) || CPhysicsPart::creature_mode; // exact: 0 unless physobj-with-id, else creature_mode
RenderDeviceD3D::s_current_physics_part = p;
status = render_device->vtable->DrawMesh(g, &p->draw_pos, portalFlag != 0);
RenderDeviceD3D::s_current_physics_part = null;
if (status == 2 && CPhysicsPart::viewcone_check_object_id != 0
&& CPhysicsPart::viewcone_check_object_id == (p->physobj ? p->physobj->id : 0))
CPhysicsPart::selected_object_in_view = 1;
}
```
**Gotchas:** m_current_render_frame_num is compared but not visibly updated here — the drawn-this-frame stamping for parts happens via SetDrawnThisFrame inside DrawMeshInternal. The building path calls this with parts[0] only; buildings never iterate multiple parts here.
### BSPTREE::build_draw_portals_only (+ BSPNODE @0x0053c100) @0x00539860
**Summary:** Entry to the portal-only walk of a building's drawing BSP. Dispatches root by node tag ('FAIL' 0x4c454146 = stop, 'PORT' 0x504f5254 = BSPPORTAL::portal_draw_portals_only, else BSPNODE::build_draw_portals_only), then flushes the poly list. BSPNODE variant walks front-to-back relative to the CURRENT viewer position against each splitting plane (epsilon 0.0002), recursing into far side first is NOT done — it visits the viewer-side subtree ordering and dispatches PORT nodes to the portal drawer, passing the pass number (1 or 2) through unchanged.
```c
void BSPTREE::build_draw_portals_only(BSPTREE* t, int pass) {
n = t->root_node;
if (n) {
if (n->tag != 'FAIL') {
if (n->tag == 'PORT') BSPPORTAL::portal_draw_portals_only(n, pass);
else BSPNODE::build_draw_portals_only(n, pass);
}
}
Render::m_pRenderer->vtable->polyListFinishInternal();
Render::PolyNext = &Render::PolyList;
}
void BSPNODE::build_draw_portals_only(BSPNODE* n, int pass) { // iterative + recursive mix
loop {
d = dot(FrameCurrent->viewer.viewpoint, n->splitting_plane.N) + n->splitting_plane.d;
side = (d > 0.000199999995) ? 0 /*positive*/ : (d < -0.000199999995 ? 1 /*negative*/ : 2 /*in-plane*/);
if (side == 0) { // viewer on positive side: visit neg child first, then continue into pos child
c = n->neg_node;
if (c && c->tag != 'FAIL') { if (c->tag=='PORT') BSPPORTAL::portal_draw_portals_only(c,pass); else recurse(c,pass); }
next = n->pos_node;
} else { // negative or in-plane: visit pos child first, then continue into neg child
c = n->pos_node;
if (c && c->tag != 'FAIL') { if (c->tag=='PORT') BSPPORTAL::portal_draw_portals_only(c,pass); else recurse(c,pass); }
next = n->neg_node;
}
if (!next || next->tag == 'FAIL') return;
if (next->tag == 'PORT') { BSPPORTAL::portal_draw_portals_only(next, pass); return; }
n = next; // tail-continue
}
}
```
**Gotchas:** Tags are 4CC constants: 0x4c454146='FAIL' (leaf sentinel), 0x504f5254='PORT'. Traversal order: the child OPPOSITE the viewer is visited first (back-to-front), viewer-side child processed by tail-continuation — i.e., portals are emitted far-to-near. Plane epsilon is exactly 0.000199999995.
### BSPPORTAL::portal_draw_portals_only @0x0053d870
**Summary:** The PORT-node handler. Same plane-side ordering as BSPNODE, but after visiting the first child it emits every in_portal of this node via the render-device vtable: render_device->DrawPortal(in_portals[i], 1, pass). Positive-side and negative-side branches both emit; the in-plane branch (side==2) emits nothing and just continues.
```c
void BSPPORTAL::portal_draw_portals_only(BSPPORTAL* n, int pass) {
loop {
d = dot(viewer.viewpoint, n->splitting_plane.N) + n->splitting_plane.d; // epsilon 0.000199999995
side = pos(0)/neg(1)/in-plane(2);
if (side == 0) {
visit n->neg_node (PORTportal_draw_portals_only, else build_draw_portals_only, skip FAIL/null);
for (i = 0; i < n->num_portals; i++)
RenderDevice::render_device->vtable->DrawPortal(n->in_portals[i], 1, pass); // arg3 ALWAYS 1
next = n->pos_node;
} else if (side == 1) {
visit n->pos_node (same dispatch);
for (i = 0; i < n->num_portals; i++)
render_device->vtable->DrawPortal(n->in_portals[i], 1, pass);
next = n->neg_node;
} else { // side == 2 (viewer in plane): visit pos child, NO portal emission
visit n->pos_node (same dispatch);
next = n->neg_node;
}
if (!next || next->tag=='FAIL') return;
if (next->tag != 'PORT') { build_draw_portals_only(next, pass); return; }
n = next;
}
}
```
**Gotchas:** These vtable calls are the ONLY call sites of DrawPortal in the whole binary, and pass is only ever 1 or 2 (from DrawMeshInternal). Therefore PView::DrawPortal's arg4==3 branch is dead code in the 2013 build. arg3 is the literal constant 1 always.
### RenderDeviceD3D::DrawPortal @0x0059f0e0
**Summary:** Vtable shim: saves building_view, sets it to -1 (so meshes drawn inside the look-in draw in ALL portal views), forwards to PView::DrawPortal on the OUTDOOR pview, restores building_view.
```c
void RenderDeviceD3D::DrawPortal(CPortalPoly* pp, int arg3, int pass) {
RenderDeviceD3D::backup_building_view = RenderDeviceD3D::building_view;
RenderDeviceD3D::building_view = 0xffffffff;
PView::DrawPortal(RenderDeviceD3D::outdoor_pview, pp, arg3, pass);
RenderDeviceD3D::building_view = RenderDeviceD3D::backup_building_view;
}
```
**Gotchas:** Always the outdoor pview — building look-ins are an outdoor-pview mechanism even though they render EnvCells. backup_building_view is a static, not stack: not reentrant, but the call graph never nests it.
### PView::DrawPortal @0x005a5ab0
**Summary:** The look-in punch driver for one building portal polygon. Flushes the pending poly list, backs up render state, resets object scale to 1, resolves the CBldPortal via outdoor_portal_list[pp->portal_index], adds the portal's stab views, then ConstructView. On success: pass!=1 → DrawCells (this is the ov=0 look-in punch in the live trace); then re-pushes the building frame (positionPush(3, CBuildingObj::curr_pos)) so the continuing BSP walk stays in building object space. On failure: pass==3 would seal the portal poly at its own depth (dead in this build).
```c
void PView::DrawPortal(PView* this, CPortalPoly* pp, int arg3 /*always 1*/, int pass /*1|2*/) {
Render::m_pRenderer->vtable->polyListFinishInternal();
Render::PolyNext = &Render::PolyList;
ACRender::backup_curr_state();
Render::object_scale = 1f; Render::object_scale_vec = (1,1,1);
CBldPortal* bp = this->outdoor_portal_list[pp->portal_index];
CPolygon* poly = pp->portal;
PView::add_views(this, bp->num_stabs, bp->stab_list);
ok = PView::ConstructView(this, bp, poly, arg3, pass); // the CBldPortal overload @005a59a0
if (ok == 0) {
if (pass == 3) D3DPolyRender::DrawPortalPolyInternal(poly, 0); // DEAD: pass is never 3
ACRender::restore_curr_state();
} else {
if (pass != 1) PView::DrawCells(this, <BN-stale edx>); // pass 2: draw the constructed cell list (the trace's DrawCells ov=0 punch)
ACRender::restore_curr_state();
Render::positionPush(3, CBuildingObj::curr_pos); // restore building object frame for the ongoing BSP walk
Render::obj_view_set();
}
PView::remove_views(this, bp->num_stabs, bp->stab_list);
}
```
**Gotchas:** DrawCells' second argument is a stale register (DrawCells never reads it — BN artifact). The success path pushes a frame (positionPush) with no visible pop here; the balance is restored by the enclosing DrawMesh/DrawMeshInternal frame stack. This is WHY DrawBuilding must set CBuildingObj::curr_pos before the portal pass.
### PView::ConstructView (CBldPortal overload) @0x005a59a0
**Summary:** Gates and builds the look-in view for one building portal. Sidedness of the VIEWER against the portal polygon plane must match the authored portal_side (portal_side==0 requires POSITIVE, portal_side!=0 requires NEGATIVE — in-plane fails both... see gotchas). Then GetClip produces a clipped view; the destination EnvCell must be currently Visible; copy_view pushes the clipped view onto that cell's portal_view stack. Pass 1 additionally draws the portal poly with flag 1 (the far-Z punch); pass !=1 recurses into the EnvCell-overload ConstructView (BFS over interior cells).
```c
int PView::ConstructView(PView* this, CBldPortal* bp, CPolygon* poly, int arg4 /*1*/, int pass) {
d = dot(FrameCurrent->viewer.viewpoint, poly->plane.N) + poly->plane.d; // epsilon 0.000199999995
side = d > eps ? POSITIVE : (d < -eps ? NEGATIVE : IN_PLANE);
if (bp->portal_side != 0) { if (side != NEGATIVE) return 0; }
else { if (side != POSITIVE) return 0; }
PView::GetClip(this, side, poly, &clip_view, &clippedPoly, arg4);
if (clippedPoly == 0) return 0;
CEnvCell* cell = CEnvCell::GetVisible(bp->other_cell_id);
if (cell == 0) return 0;
if (Render::copy_view(cell->portal_view.data[cell->num_view - 1], &clip_view, clippedPoly) == 0) return 0; // pushes the view frame
if (pass != 2) D3DPolyRender::DrawPortalPolyInternal(poly, pass == 1); // pass 1 → flag 1 (far-Z punch); pass 3 → flag 0 (own-depth seal, dead)
Render::framePop();
if (pass != 1) PView::ConstructView(this, cell, bp->other_portal_id); // EnvCell overload @005a57b0: BFS builds cell_draw_list
return 1;
}
```
**Gotchas:** IN_PLANE (viewer within ±0.0002 of the portal plane) fails BOTH sidedness gates → no punch, no look-in, portal contributes nothing that pass. If GetVisible fails (destination cell not loaded/visible) the punch is silently skipped too — pass 1 draws nothing for that portal. BN artifact: the decomp shows arg names shifted (arg3 reused as GetClip out-poly); layout above is reconstructed from CBldPortal header fields (portal_side@0, other_cell_id@4, other_portal_id@8, num_stabs@0x10, stab_list@0x14).
### D3DPolyRender::DrawPortalPolyInternal @0x0059bc90
**Summary:** THE cover-panel primitive: draws one portal polygon as an untextured triangle fan with DEPTHTEST_ALWAYS and (by default) depth-write ON and color alpha 0 under SRCALPHA/INVSRCALPHA blending — i.e., an invisible depth-only write. flag=1 (mode global maxZ1, default 7): every vertex Z is FORCED to 0.999998987 (far plane) — this 'punches' the aperture open in the depth buffer. flag=0 (maxZ2, default 6): real projected Z — this 'seals' the aperture at the portal's own depth. Rejects polys entirely outside a ±12.0 local-XY box first.
```c
void DrawPortalPolyInternal(CPolygon* poly, uint8 flag) {
Render::CalcObjectMatrix();
mode = flag ? maxZ1 : maxZ2; // globals: maxZ1 = 7, maxZ2 = 6 (static defaults in .data)
// trivial reject vs local box (24-unit cell): track per-axis all-outside flags across vertices
allBeyond(+12x)=1; allBeyond(-12x)=1; allBeyond(+12y)=1; allBeyond(-12y)=1;
for (i = 0; i < poly->num_pts; i++) {
v = poly->vertices[i];
clear corresponding flag when v.x/v.y is inside the ±12.0 bound; // FP-idiom (test ah,0x44) — polarity see gotchas
scrBuf[i] = PrimD3DRender::xformStart(v, 1);
}
if (any allBeyond flag still set) return; // poly wholly outside one ±12 bound → skip
if (flag == 0) D3DPolyRender::portalsDrawnCount++; // ONLY own-depth seals are counted
Render::PolyCurrent = null; Render::PolyCurrentMod = 1f; Render::PolyCurrentPos = 1;
ACRender::polyClipFinish(scrBuf, num_pts, scrBufclipped, &n, 0);
if (n < 3) return;
SetStageTexture(0, null);
SetAlphaTestEnable(0);
SetBlendFunction(BLEND_SRCALPHA, BLEND_INVSRCALPHA, BLENDOP_ADD);
SetDepthBufferMode(DEPTHTEST_ALWAYS, (mode >> 2) & 1); // depth test ALWAYS; z-write = bit2 → ON for both 7 and 6
ApplyVertexFormat(0x144);
SetCullMode(CULLMODE_NONE);
colorSel = (colorSel + 1) & cycle-of-8 picks a debug RGB (white/red/green/blue/yellow/cyan/magenta/black); wraps colorSel to -1 at 7;
for each clipped vertex k:
screen.x = clip.x / clip.w + viewportX; screen.y = clip.y / clip.w + viewportY;
screen.z = (mode & 1) ? 0.999998987f : clip.z / clip.w; // bit0 → force far-plane Z (maxZ1=7 has it; maxZ2=6 does not)
rhw = 1 / clip.w;
color = debugRGB | (alphaBit(mode) ? 0x80000000 : 0); // alphaBit = ~(mode << 30) & 0x80000000 → 0 for both 6 and 7 → alpha 0 → INVISIBLE
RenderDeviceD3D::DrawPrimitiveUP(D3DPT_TRIANGLEFAN, n - 2, v, 0x1c);
}
```
**Gotchas:** This is the invisible 'panel'. Default mode words decode as: bit0 = force-far-Z, bit2 = enable z-write, bit1 (via the <<30 alpha mush) = make visible at 50% alpha a debug visualization toggle (change maxZ1/maxZ2 in a debugger to SEE the panels in the cycling colors). The ±12.0 reject-loop polarity is an FP-status-word idiom (test ah,0x44) BN could not lift cleanly the accept path requires all four all-outside flags cleared; exact per-comparison polarity should be byte-verified against the binary before porting. It does NOT restore the depth/blend/cull state it set callers rely on ACRender::backup_curr_state/restore_curr_state or later SetupState calls. Vertex stride 0x1c, FVF 0x144 (XYZRHW|DIFFUSE).
### PView::DrawCells @0x005a4840
**Summary:** Draws the cell_draw_list built by ConstructView, far-to-near (list is BFS/distance-ordered; iterated from the END down). Only when outside_view.view_count > 0 (ov>0 — the interior/exit-view case): draws the LANDSCAPE through the constructed exit views, then conditionally full-screen depth-clears to 1.0 (if forceClear, or if any own-depth seal poly was drawn since the last check), then seals every portal whose other_cell_id == 0xFFFFFFFF with an invisible own-depth panel. Then unconditionally: far-to-near DrawEnvCell per view, then far-to-near object pass (DrawObjCellForDummies with PortalList pointed at each cell's top portal_view).
```c
void PView::DrawCells(PView* this, int unusedArg) {
if (this->outside_view.view_count > 0) { // ov>0: only interior frames / exit-view case
Render::useSunlightSet(1);
Render::PortalList = &this->outside_view;
LScape::draw(this->lscape); // landscape THROUGH the exit view(s)
D3DPolyRender::FlushAlphaList(0f);
render_device->m_nFrameStamp += 1;
doClear = forceClear;
if (!forceClear) { doClear = (portalsDrawnCount != 0); portalsDrawnCount = 0; }
if (doClear) render_device->vtable->Clear(4, &color_820fc0, 1.0f); // flag4 → D3DCLEAR_ZBUFFER, full RT, z=1.0
for (i = cell_draw_num; i >= 1; i--) { // far-to-near SEAL pass
cell = cell_draw_list[i-1];
if (!cell->structure->drawing_bsp) continue;
SetCurrentMaterial(null); SetSurfaceArray(cell->surfaces); scale=1;
Render::positionPush(3, &cell->pos);
vc = cell->num_view ? cell->portal_view[num_view-1]->view_count : -1;
if (vc != 0) for (v = 0; v != vc; v++) { // vc==-1 → loop runs once with no set_view? see gotchas
CEnvCell::setup_view(cell, v);
for (j = 0; j < cell->num_portals; j++) // CCellPortal stride 0x18
if (cell->portals[j].other_cell_id == 0xffffffff)
D3DPolyRender::DrawPortalPolyInternal(cell->portals[j].portal, 0); // SEAL: own-depth, invisible, z-write
}
Render::framePop();
}
}
Render::useSunlightSet(0);
Render::restore_all_lighting();
for (i = cell_draw_num; i >= 1; i--) { // far-to-near GEOMETRY pass
cell = cell_draw_list[i-1];
if (!cell->structure->drawing_bsp) continue;
SetCurrentMaterial(null); SetSurfaceArray(cell->surfaces); scale=1;
Render::positionPush(3, &cell->pos);
vc as above; if (vc != 0) for (v = 0; v != vc; v++) { CEnvCell::setup_view(cell, v); render_device->vtable->DrawEnvCell(cell); }
Render::framePop();
}
for (i = cell_draw_num; i >= 1; i--) { // far-to-near OBJECT pass
cell = cell_draw_list[i-1];
Render::PortalList = cell->portal_view.data[cell->num_view - 1]; // fields +0x134/+0x138
render_device->vtable->DrawObjCellForDummies(cell);
}
Render::object_scale=(1,1,1); Render::useSunlightSet(1);
}
```
**Gotchas:** RECONCILES THE TRACE: outdoor building look-ins arrive here with outside_view.view_count == 0 (trace's 'ov=0') → NO LScape, NO depth clear, NO sealing — just the interior cells + objects, drawn INTO the far-Z-punched aperture. Interior frames (DrawInside → ov=1) get the LScape-through-exit + conditional z-clear + sealing. The vc==-1 path (num_view==0) enters the loop body once WITHOUT a valid portal_view — setup_view is called with v=0 on a cell with no views; verify against binary before porting. portalsDrawnCount is only incremented by flag-0 seal draws, so the depth clear triggers on the NEXT DrawCells after any sealing. The frame-stamp bump (m_nFrameStamp += 1) re-arms CPhysicsPart per-frame dedupe so objects already drawn outside can draw again inside the look-in. Second arg is dead (BN stale edx at both call sites).
### RenderDeviceD3D::DrawSortCell @0x0059f140
**Summary:** The actual caller into DrawBuilding — there is NO CBuildingObj::draw method in the 2013 build. A CSortCell (outdoor cell) draws its attached building first (vtable DrawBuilding), then its object contents (vtable DrawObjCell).
```c
void RenderDeviceD3D::DrawSortCell(CSortCell* cell) {
if (cell->building != 0) this->vtable->DrawBuilding(cell->building);
this->vtable->DrawObjCell(cell);
}
```
**Gotchas:** Building before objects, per cell — combined with DrawBlock's far-to-near draw_array order this produces the trace's 'DrawBuilding far-to-near with DrawCells punches interleaved'. CBuildingObj is drawn purely through this render-device path; grep confirms no draw method on the class itself.
### RenderDeviceD3D::DrawBlock @0x005a17c0
**Summary:** Per-landblock draw (vtable DrawBlock, invoked from the LScape draw loop at 0x00506374). Pushes an identity Position stamped with the block DID, sets vertex lighting, then two loops over draw_array[side_cell_count²] (pre-sorted far-to-near): loop 1 updates in-view obj cells and insertion-sorts their shadow parts by depth; loop 2 per cell draws terrain (DrawLandCell, with landscape detail surface when side_cell_count==8, src_blend=5 dst_blend=6) then DrawSortCell (buildings+objects) with an alpha-list flush when the global 'flush' != 1.0.
```c
void RenderDeviceD3D::DrawBlock(CLandBlock* lb) {
scale=(1,1,1); localPos = identity Position (vtbl 0x796910), objcell_id = lb->m_DID.id;
Frame::cache(&localPos.frame); Render::positionPush(3, &localPos);
ACRender::curLandBlockVertexLighting = lb->vertex_lighting;
n = lb->side_cell_count * lb->side_cell_count;
for (i = 0; i < n; i++) { // pass 1: update + sort
c = lb->draw_array[i];
if (c->vtable->IsInView() && c->num_shadow_objects != 0) {
RenderDeviceD3D::UpdateObjCell(this, c);
if (c->num_shadow_parts > 1) CShadowPart::insertion_sort(&c->shadow_part_list, c->num_shadow_parts);
}
}
isFullBlock = (lb->side_cell_count == 8);
for (i = 0; i < n; i++) { // pass 2: draw (draw_array order = far-to-near)
(lazy one-shot: flush pending FF light state at +0x7e4/+0x468, reset diffuse/ambient source to FromVertex);
Render::SetSurfaceArray(CRegionDesc::current_region->terrain_info-> ...hold_run);
scale=(1,1,1);
c = lb->draw_array[i];
if (c->vtable->IsInView()) {
if (isFullBlock) { curr_detail_surface = landscape_detail_surface; tiling = landscape_detail_tiling; src_blend=5; dst_blend=6; }
this->vtable->DrawLandCell(c);
curr_detail_surface = null;
}
if (alwaysDrawObjects || c->vtable->IsInView()) {
this->vtable->DrawSortCell(c);
if (flush != 1f) D3DPolyRender::FlushAlphaList(flush);
}
}
ACRender::curLandBlockVertexLighting = null;
Render::framePop();
}
```
**Gotchas:** BN drops the store of lb->m_DID.id into the local Position's objcell_id (the load is shown, the store elided) — same pattern in PView::DrawInside. IsInView is a virtual at vtable+0x68. Terrain detail texturing only on full 8x8 blocks. The trace's outdoor frame = LScape::draw iterating blocks → this function → DrawSortCell → DrawBuilding, far-to-near.
### RenderDeviceD3D::DrawObjCell + RenderDeviceD3D::DrawPartCell @0x005a1a40 / 0x005a07a0
**Summary:** DrawObjCell (0x005a1a40) forwards a CObjCell to DrawPartCell(cell->m_DID.id, cell, 0). DrawPartCell (0x005a07a0) clears curr_detail_surface and draws every entry of the cell's depth-sorted shadow_part_list via CShadowPart::draw → CPhysicsPart::Draw(part->part, 0) — the normal (non-portal) mesh path.
```c
void DrawObjCell(CObjCell* c) {
if (c != 0) { DrawPartCell(this, c->m_DID.id, (CPartCell*)c, 0); return; }
DrawPartCell(this, c->m_DID.id, null, 0); // BN artifact: derefs null id — unreachable in practice
}
void DrawPartCell(uint32 cellId, CPartCell* pc, uint32 idx) {
Render::curr_detail_surface = null;
for (i = 0; i < pc->num_shadow_parts; i++)
if (pc->shadow_part_list.data[i]) CShadowPart::draw(pc->shadow_part_list.data[i]); // → CPhysicsPart::Draw(part, 0)
}
```
**Gotchas:** cellId and idx are dead parameters in DrawPartCell (only used by the DrawBuildingLeaf caller for bookkeeping that no longer exists). The null-cell branch of DrawObjCell reads c->m_DID.id off a null pointer — decompiled control-flow artifact of a shared tail; the guard makes it unreachable.
### RenderDeviceD3D::DrawBuildingLeaf @0x005a07e0
**Summary:** Legacy per-BSP-leaf building-interior draw: looks up CBuildingObj::curr_leaf_cells[leafIdx]; if it has shadow parts, flushes the poly list, backs up state, sets pushLevelOffset=1, and DrawPartCell's that CPartCell under the building's curr_pos cell id, then restores. APPEARS DEAD in the 2013 build: no call site of the vtable slot exists anywhere in the pseudo-C, and the statics curr_leaf_cells/curr_num_leaves are never assigned (initialized 0 in .data; the CBuildingObj member leaf_cells is likewise only ever zero-initialized and freed).
```c
void RenderDeviceD3D::DrawBuildingLeaf(uint32 leafIdx) {
CPartCell* pc = CBuildingObj::curr_leaf_cells[leafIdx];
if (pc != 0 && pc->num_shadow_parts > 0) {
Render::m_pRenderer->vtable->polyListFinishInternal();
Render::PolyNext = &Render::PolyList;
ACRender::backup_curr_state();
Render::pushLevelOffset = 1;
RenderDeviceD3D::DrawPartCell(this, CBuildingObj::curr_pos->objcell_id, CBuildingObj::curr_leaf_cells[leafIdx], leafIdx);
Render::pushLevelOffset = 0;
ACRender::restore_curr_state();
Render::obj_view_set();
}
}
```
**Gotchas:** Dead legacy path from the pre-EnvCell building-interior model: curr_leaf_cells is never written (would null-deref if the slot were ever invoked with curr_num_leaves>0), and grep finds zero '->DrawBuildingLeaf(' call sites. Building interiors in this build are EnvCells drawn through PView::DrawCells instead. Do not port as live behavior.
### PView::ConstructView (CEnvCell overload) + PView::DrawInside @0x005a57b0 / 0x005a5860
**Summary:** ConstructView(cell, portalIdx): resets outside_view.view_count=0, bumps master_timestamp, clears todo/draw lists, InitCell(cell, portalIdx), seeds the todo list, then BFS: pop nearest-last todo cell, append to cell_draw_list, mark cell_view_done, ClipPortals → AddViewToPortals (which propagates clipped views into neighbor cells and can raise outside_view). DrawInside (interior frame entry, via RenderDeviceD3D::DrawInside on the indoor pview): pushes the cell's view + stab views, pushes an identity Position stamped with the cell DID, copy_view of the cell's top portal_view, ConstructView(cell, 0xffff), DrawCells, then unwinds (framePop, remove_views, num_view -= 1).
```c
void PView::ConstructView(PView* this, CEnvCell* cell, uint16 throughPortal) {
this->outside_view.view_count = 0; PView::master_timestamp += 1;
this->cell_todo_num = 0; this->cell_draw_num = 0;
PView::InitCell(this, cell, throughPortal);
PView::InsCellTodoList(this, cell, 0f); // distance-sorted insert
while (this->cell_todo_num > 0) {
c = cell_todo_list[--cell_todo_num]->cell; if (c == 0) return;
grow cell_draw_list if needed (by 0x1e); cell_draw_list[cell_draw_num++] = c;
c->portal_view[c->num_view - 1]->cell_view_done = 1;
if (PView::ClipPortals(this, c, 0) != 0) PView::AddViewToPortals(this, c);
}
}
void PView::DrawInside(PView* this, CEnvCell* cell) {
scale=(1,1,1);
CEnvCell::curr_view_push(cell);
PView::add_views(this, cell->num_stabs, cell->stab_list);
localPos = identity Position (vtbl 0x796910), objcell_id = cell->m_DID.id; Frame::cache; Render::positionPush(3, &localPos);
Render::copy_view(cell->portal_view.data[cell->num_view - 1], null, 4);
PView::ConstructView(this, cell, 0xffff);
PView::DrawCells(this, <stale edx>);
Render::framePop();
PView::remove_views(this, cell->num_stabs, cell->stab_list);
cell->num_view -= 1;
}
```
**Gotchas:** Included for reconciliation only (bodies of InitCell/ClipPortals/AddViewToPortals/GetClip not fully extracted this batch). outside_view is raised inside the ClipPortals/AddViewToPortals propagation when a portal leads outdoors — which is what makes DrawCells' ov>0 LScape-through-exit branch fire on interior frames (trace: DrawInside then DrawCells ov=1). 0xffff = 'no through-portal' sentinel matching InitCell's uint16 compare.
**Report notes:** THE COVER MECHANISM (the priority question) is not inside DrawBuilding's own body — DrawBuilding is 20 lines — it is the portal pass it triggers. Full outdoor sequence per building, in submission order: (1) DrawBuilding publishes b->portals as outdoor_pview->outdoor_portal_list, sets CBuildingObj::curr_pos, sets building detail-surface state (src=9,dst=6), FlushAlphaList(0). (2) PORTAL pass — CPhysicsPart::Draw(part,1) → vtable DrawMesh (per portal view, building_view-gated) → DrawMeshInternal pins building_view=portal_view_num and walks the building's drawing BSP TWICE via build_draw_portals_only: pass 1 then pass 2, both far-to-near over the BSP portal nodes. At each portal: render_device->DrawPortal(pp,1,pass) → building_view=-1 → PView::DrawPortal → ConstructView(CBldPortal): viewer sidedness must match authored portal_side, clip must survive, destination EnvCell must be Visible. Pass 1 then draws the portal polygon via D3DPolyRender::DrawPortalPolyInternal(poly,1): an UNTEXTURED, CULL-NONE, DEPTHTEST_ALWAYS, ALPHA-0 (invisible under SRCALPHA/INVSRCALPHA) triangle fan with every Z forced to 0.999998987 and Z-WRITE ON (mode word maxZ1=7: bit0=force-far-Z, bit2=zwrite) — i.e., it punches the aperture's depth back to the far plane. Pass 2 draws no poly; it recurses ConstructView(EnvCell) (BFS building cell_draw_list) and then PView::DrawCells — which for outdoor look-ins runs with outside_view.view_count==0 (the trace's ov=0), skipping LScape/z-clear/sealing and just drawing the interior EnvCells + their objects far-to-near into the punched aperture. (3) SHELL pass — CPhysicsPart::Draw(part,0) draws the building exterior mesh normally (ObjBuildingOrBuildingPart=1 around it). So every submitted 'extra' geometry beyond the visible shell is: per qualifying portal, one invisible far-Z depth fan (pass 1), the look-in cell geometry + objects (pass 2), and — only on interior frames (ov>0) — invisible OWN-depth seal fans for portals with other_cell_id==0xFFFFFFFF plus a conditional full depth clear to 1.0 (Clear flag 4 → D3DCLEAR_ZBUFFER) triggered when any seal was drawn since the last check (portalsDrawnCount counts ONLY flag-0 seals). This matches the PV campaign's proven model (panels = never-drawn portal polys): the 'panels' are DrawPortalPolyInternal fans, alpha-0 by default, visualizable by flipping the maxZ1/maxZ2 debug globals (defaults 7/6 in .data at 0x820e18/0x820e14; bit1-ish of the mode via the <<30 mush enables 0x80 alpha with cycling debug colors). Key retail invariants a port must preserve: depth-test ALWAYS + depth-write ON for both punch and seal; punch Z is exactly 0.999998987; sidedness epsilon 0.000199999995 with IN_PLANE failing both gates; punches are skipped entirely when the destination cell is not Visible (no fallback seal on the outdoor path the arg4==3 seal-on-fail branch in PView::DrawPortal is dead, since BSPPORTAL only ever passes pass∈{1,2}); the ±12.0 local-XY trivial reject in DrawPortalPolyInternal; the m_nFrameStamp bump in DrawCells re-arms per-frame part dedupe inside look-ins. On the trace's invisible-distant-building puzzle: DrawBuilding submits the shell through the normal depth-tested path, but its apertures' far-Z punches OVERWRITE whatever depth was already in those pixels (test ALWAYS), and look-in cell content then depth-tests against that nothing in this chain draws an opaque cover over the shell itself; the shell's visibility is decided by ordinary depth vs. previously drawn landscape plus the viewcone/building_view gates in DrawMesh. DEAD CODE flagged: RenderDeviceD3D::DrawBuildingLeaf + CBuildingObj::curr_leaf_cells/curr_num_leaves/curr_shadow (never written, slot never called) legacy pre-EnvCell interior path; do not port. Struct anchors used (acclient.h): CCellPortal{other_cell_id,+4 other_cell_ptr,+8 portal,+0xC portal_side,+0x10 other_portal_id,+0x14 exact_match} stride 0x18; CBldPortal{portal_side,other_cell_id,other_portal_id,exact_match,num_stabs,stab_list,sidedness}. Source lines: acclient_2013_pseudo_c.txt DrawBuilding 427938, DrawMeshInternal 427965, DrawMesh 429245, CPhysicsPart::Draw 274964, BSPTREE 323225, BSPNODE 325389, BSPPORTAL 326881, RenderDeviceD3D::DrawPortal 427852, PView::DrawPortal 433895, ConstructView(CBldPortal) 433827, DrawPortalPolyInternal 424490, PView::DrawCells 432709, DrawSortCell 427872, DrawBlock 430027, DrawObjCell 430147, DrawPartCell 429198, DrawBuildingLeaf 429223, ConstructView(CEnvCell) 433750, DrawInside 433793.
## Report 4 - Landscape walk (LScape draw / order / visibility, DrawBlock/DrawSortCell)
### LScape::draw @0x00506330
**Summary:** Top-level outdoor landscape walk. Draws the sky dome, then iterates block_draw_list BACKWARDS (index mid_width*mid_width-1 down to 0). Since get_block_order builds that list near-to-far (viewer's block at index 0, then rings outward), the backwards walk is FAR-TO-NEAR, confirming the trace. Each block with in_view != OUTSIDE goes through the RenderDevice vtable DrawBlock. Weather overlay sky pass last.
```c
LScape::draw(this):
if (this->sky) GameSky::Draw(this->sky, 0) // pass 0 = sky dome background
if (this->block_draw_list == 0) return // no viewpoint -> nothing else
LScape::draw_check_blocks(this) // recompute block/cell in_view for all portal views
for (i = this->mid_width * this->mid_width - 1; i >= 0; i--): // BACKWARDS = far-to-near
blk = this->block_draw_list[i]
if (blk != 0 && blk->in_view != 0) // in_view @ CLandBlock+0xfc, BoundingType
RenderDevice::render_device->vtable->DrawBlock(blk) // = RenderDeviceD3D::DrawBlock @0x005a17c0
if (this->sky && LScape::weather_enabled)
GameSky::Draw(this->sky, 1) // pass 1 = weather (drawn after terrain)
```
**Gotchas:** block_draw_list has exactly mid_width^2 entries (11x11 = 121 with the ctor defaults mid_radius=5, mid_width=0xb); the ring fill in get_block_order covers every grid slot exactly once so no stale entries survive a rebuild. The in_view test reads raw +0xfc verified as CLandBlock::in_view (BoundingType) in acclient.h. Callers: PView::DrawCells @0x005a4840 (sets Render::PortalList = the PView first, then calls LScape::draw, then FlushAlphaList(0f), then the cell_draw_num punch list matches the trace's DrawCells look-in interleave) and SmartBox::RenderNormalMode @0x00453aa0 (0x00453b4d).
### LScape::calc_draw_order @0x00505c70
**Summary:** Recomputes the viewer-relative block grid when the viewer's cell changes. Converts the viewer cell to block-grid offsets (viewer_b_xoff/yoff); if the viewer left the loaded grid, tears the viewpoint down. Optionally rebuilds the near-to-far block_draw_list (get_block_order), then for EVERY loaded block sets its viewer-relative frame (calc_frame) and recomputes its internal cell draw order (CLandBlock::calc_draw_order) keyed by the block's compass direction from the viewer.
```c
LScape::calc_draw_order(this, new_cell_id, rebuild_block_list):
if (new_cell_id == 0) return
if (this->loaded_cell_id != 0):
center_x8 = (loaded_cell_id >> 0x15) & 0x7f8 // loaded-center block lcoord x * 8
center_y8 = ((int8)(loaded_cell_id >> 0x10)) << 3 // block lcoord y * 8
// else: center_x8/y8 UNINITIALIZED in the decomp (see gotchas)
LandDefs::gid_to_lcoord(new_cell_id, &vx, &vy) // viewer global cell lcoord
r8 = this->mid_radius << 3
this->viewer_b_xoff = (r8 - center_x8 + vx) >> 3 // viewer block's index in the grid
this->viewer_b_yoff = (r8 - center_y8 + vy) >> 3
if (viewer_b_xoff >= mid_width || viewer_b_yoff >= mid_width || viewer_b_xoff < 0 || viewer_b_yoff < 0):
LScape::update_viewpoint(this, 0) // out of grid: free block_draw_list, viewer_cell_id = 0
return
if (rebuild_block_list != 0) LScape::get_block_order(this) // rebuild near-to-far ring list
sq = SqCoord{ vx & 7, vy & 7 } // viewer's CELL coord within its block
for (row = 0; row < mid_width; row++):
for (col = 0; col < mid_width; col++):
blk = this->land_blocks[mid_width*row + col]
if (blk == 0) continue
dir = LandDefs::get_dir(row - viewer_b_xoff, col - viewer_b_yoff)
LScape::calc_frame(this, blk, row, col) // block_frame.origin = ((row-xoff)*192, (col-yoff)*192, 0)
CLandBlock::calc_draw_order(blk, dir, &sq) // per-block far-to-near cell order
```
**Gotchas:** 1) Block frames are VIEWER-RELATIVE: calc_frame sets block_frame.m_fOrigin = ((row - viewer_b_xoff) * K, (col - viewer_b_yoff) * K, 0) where K is an FPU-elided constant BN prints as '0f' it is the landblock side length 192.0 (landcell_check uses an explicit 24f per cell x 8 cells). The whole landscape renders in viewer-block-local space for float precision. 2) The loaded_cell_id==0 branch leaves center_x8/y8 as uninitialized locals in the BN output (var_14 mush) real retail flow can only reach the math with loaded_cell_id != 0 or with values that immediately fail the bounds check; do not port the uninitialized read. 3) Caller LScape::update_viewpoint passes arg3 via the BN-mangled expression '-((eax_4 - eax_4))' the underlying neg/sbb idiom is ((old_viewer_cell ^ new_cell) & 0xffff0000) != 0 ? 1 : 0, i.e. rebuild the block list only when the viewer crossed a LANDBLOCK boundary; within-block cell changes reorder cells but keep the block list.
### LScape::get_block_order @0x00504c50
**Summary:** Builds block_draw_list NEAR-TO-FAR: index 0 = the block containing the viewer, then concentric square rings outward (ring radius 1..max distance to grid edge), 8 quadrant-symmetric slots per ring step generated from static coefficient tables. LScape::draw walking this list backwards yields the observed far-to-near block order.
```c
LScape::get_block_order(this):
if (this->block_draw_list == 0)
this->block_draw_list = new CLandBlock*[mid_width * mid_width]
this->block_draw_list[0] = this->land_blocks[viewer_b_xoff * mid_width + viewer_b_yoff] // viewer block FIRST
max_ring = max over the 4 edge distances of (viewer_b_xoff, viewer_b_yoff) within [0, mid_width)
n = 1
for (ring = 1; ring <= max_ring; ring++):
for (step = 0; step < ring; step++):
for each of 8 symmetric quadrant slots: // 2 table groups x 4 combos, coeff tables @0x0081cc6c..0x0081cd18
bx = f_x(ring, step) + viewer_b_xoff // linear combo: cx1*step + cx2*ring + cx0
by = f_y(ring, step) + viewer_b_yoff
if (0 <= bx < mid_width && 0 <= by < mid_width):
this->block_draw_list[n++] = this->land_blocks[mid_width*bx + by]
```
**Gotchas:** The exact within-ring visit order is encoded in eight static int32 coefficient triplets per axis (tables at 0x0081cc6c0x0081cd18, read in two 0x10-stride groups); BN shows only raw table loads. Each in-bounds grid slot lands in the list exactly once (ring sweep covers the full square), so the list is fully populated; a faithful port only needs 'center first, rings outward, skip out-of-bounds' the intra-ring order only affects tie-breaking between equidistant blocks.
### LScape::draw_check_blocks @0x00505f80
**Summary:** Per-frame visibility pass over the block grid, run once per portal view. Clears every block's and cell's in_view, (re)allocates the shared block_interval scratch (two rows of mid_width+1 ViewIntervalType, 0x80 bytes each), then for EACH view in Render::PortalList (a PView; the outdoor view plus any portal-punched views): computes per-column clip-height intervals at block corners (x = (bx - viewer_b_xoff)*192, y = (by - viewer_b_yoff)*192) double-buffered by column parity, runs Render::block_check(corner intervals, max_zval, min_zval) per block, and for blocks not OUTSIDE sets block->in_view = BoundingType and refines per-cell in_view via landcell_check.
```c
LScape::draw_check_blocks(this):
// pass 1: clear all visibility
for each land_blocks[row][col] != 0:
blk->in_view = 0 // +0xfc
for (i = 0; i < side_cell_count^2; i++)
blk->lcell[i].in_view = 0 // cell stride 0x108, in_view @ +0x104
// scratch (file-static): ViewIntervalType block_interval[2*(mid_width+1)], realloc if mid_width changed
if (block_interval == 0 || block_int_size != mid_width):
delete[] block_interval; block_interval = new[ (mid_width+1) << 8 ]; block_int_size = mid_width
// per portal view (Render::PortalList = the active PView; view_count == 0 -> single pass, no set_view)
view_count = Render::PortalList ? Render::PortalList->view_count : 0
v = 0
loop:
if (view_count != 0): Render::set_view(&Render::PortalList->view, v); v++
last_pass = (view_count == 0 || v == view_count)
// column x=0 of the corner grid
for (j = 0; j <= mid_width; j++)
Render::get_clip_height((0 - viewer_b_xoff)*192.0, (j - viewer_b_yoff)*192.0, &block_interval[j]) // 192.0 FPU-elided ('0f' in BN)
for (bx = 0; bx < mid_width; bx++):
for (j = 0; j <= mid_width; j++) // next column, double-buffered by parity
Render::get_clip_height((bx - viewer_b_xoff + 1)*192.0, (j - viewer_b_yoff)*192.0,
&block_interval[((bx-1)&1)*(mid_width+1) + j])
for (by = 0; by < mid_width; by++):
blk = land_blocks[mid_width*bx + by]
if (blk == 0) continue
curr = &block_interval[(bx&1)*(mid_width+1) + by]; next = &block_interval[((bx-1)&1)*(mid_width+1) + by]
bt = Render::block_check(curr, curr+1, next, next+1, blk->max_zval, blk->min_zval)
if (bt != OUTSIDE):
blk->in_view = bt // NOTE: only ever raises; cleared once in pass 1,
LScape::landcell_check(this, blk) // so visibility accumulates ACROSS views
if (last_pass) return
goto loop
```
**Gotchas:** Both world-coordinate multiplies print as '* 0f' — the FPU constant was elided by BN; it is 192.0 (landblock side; landcell_check's cell version uses an explicit 24f). block_interval/block_int_size are file-static globals shared with ~LScape (freed there). ViewIntervalType is 0x80 bytes (the allocation is (mid_width+1)<<8 = 2*(mid_width+1)*0x80). Render::PortalList is set by PView::DrawCells to the PView itself immediately before LScape::draw, so 'views' here are the PView's outside_view entries (main frustum + portal-punched look-out/look-in views); blocks/cells visible through ANY view stay marked. The column parity buffering means column bx uses parity rows (bx&1) = current x and ((bx-1)&1) = x+1 the freshly written row is the FAR edge; keep the parity exactly as written when porting.
### LScape::landcell_check @0x005050a0
**Summary:** Refines per-cell visibility inside one block that passed block_check, for the current view. Non-8-cell blocks (low-res distant blocks): every cell in_view = PARTIALLY_INSIDE(1). Block ENTIRELY_INSIDE(2): every cell in_view = 2. Otherwise (full 8x8 block, partially visible): builds a (side+1)^2 corner clip grid at 24 m cell pitch offset by the block's viewer-relative frame origin, and per cell whose IsInView() is still 0 computes cell->in_view = Render::block_check(cell corner intervals, block max_zval/min_zval).
```c
LScape::landcell_check(this, blk):
n = blk->side_cell_count
if (n != 8): // low-detail far block
for (i = 0; i < n*n; i++) blk->lcell[i].in_view = 1 // PARTIALLY_INSIDE
return
if (blk->in_view == ENTIRELY_INSIDE): // == 2
for (i = 0; i < n*n; i++) blk->lcell[i].in_view = 2
return
// partial 8x8 block: stack grid of ViewIntervalType, double-buffered by column parity like draw_check_blocks
x0 = blk->block_frame.m_fOrigin.x; y0 = blk->block_frame.m_fOrigin.y // viewer-relative block origin
for (j = 0; j <= n; j++) Render::get_clip_height(x0, j*24.0 + y0, &grid[j]) // column cx=0
for (cx = 0; cx < n; cx++):
for (j = 0; j <= n; j++)
Render::get_clip_height((cx+1)*24.0 + x0, j*24.0 + y0, &grid[((cx-1)&1)*(n+1) + j])
for (cy = 0; cy < n; cy++):
cell = &blk->lcell[n*cx + cy]
if (cell != 0 && cell->vtable->IsInView() == 0): // vtable+0x68; only cells not already seen by an earlier view
curr = &grid[(cx&1)*(n+1) + cy]; next = &grid[((cx-1)&1)*(n+1) + cy]
cell->in_view = Render::block_check(curr, curr+1, next, next+1, blk->max_zval, blk->min_zval)
```
**Gotchas:** Two BN artifacts in the final store: 'eax_10->num_static_objects = Render::block_check(...)' is field-name garbage — the store target is the CELL's in_view (+0x104 within the 0x108-byte CLandCell), NOT the block's num_static_objects; and 'eax_9 != -(lcell)' is pointer-null-check mush. The 24f here is explicit (not elided), which is what pins draw_check_blocks'/calc_frame's elided constant at 8*24 = 192. The IsInView()==0 pre-check makes cell visibility a UNION across portal views (a later view never downgrades a cell already marked). BoundingType numeric values used raw: 0 = OUTSIDE, 1 = PARTIALLY_INSIDE, 2 = ENTIRELY_INSIDE.
### CLandBlock::calc_draw_order (-> calc_sq_draw_order) @0x00530300 (tailcall to 0x0052f4a0)
**Summary:** Per-block CELL ordering; the exact mirror of get_block_order one level down, but written back-to-front. Picks the block's cell nearest the viewer from the block's compass direction (dir switch over 9 LandDefs::Direction cases; IN_VIEWER_BLOCK uses the viewer's own cell coord scaled by 8/side_cell_count), early-outs if that closest cell is unchanged, then fills draw_array from the LAST index (closest cell) backwards through outward rings — so forward iteration of draw_array (as DrawBlock does) visits cells FAR-TO-NEAR.
```c
CLandBlock::calc_sq_draw_order(this, dir, viewer_sq): // viewer_sq = viewer cell & 7 per axis
if (this->draw_array == 0 || draw_array_size < n*n) // n = side_cell_count
(re)alloc draw_array[n*n], draw_array_size = n*n
if (n == 1) { draw_array[0] = this->lcell; return n }
scale = 8 / n
switch (dir): // jump table @0x0052f8e0: 0=IN_VIEWER_BLOCK,1=NORTH,2=SOUTH,
IN_VIEWER_BLOCK: cx = viewer_sq->x/scale; cy = viewer_sq->y/scale // 3=EAST,4=WEST,5=NORTHWEST,6=SOUTHWEST,
NORTH: cx = viewer_sq->x/scale; cy = 0 // 7=NORTHEAST,8=SOUTHEAST
SOUTH: cx = viewer_sq->x/scale; cy = n-1
EAST: cx = 0; cy = viewer_sq->y/scale
WEST: cx = n-1; cy = viewer_sq->y/scale
NORTHWEST: cx = n-1; cy = 0
SOUTHWEST: cx = n-1; cy = n-1
NORTHEAST: cx = 0; cy = 0
SOUTHEAST: cx = 0; cy = n-1
this->dir = dir // stored BEFORE the early-out check
if (cx == this->closest.x && cy == this->closest.y) return 0 // no reorder needed
this->closest = {cx, cy}
max_ring = max(edge distances of cx, cy in [0,n)) // same formula as get_block_order
k = n*n
draw_array[--k] = &this->lcell[n*cx + cy] // CLOSEST cell at the LAST slot
for (ring = 1; ring <= max_ring; ring++):
for (step = 0; step < ring; step++):
for each of 8 symmetric quadrant slots: // coeff tables @0x0081df88..0x0081e034 (cell twin of the block tables)
ex = f_x(ring, step) + cx; ey = f_y(ring, step) + cy
if (0 <= ex <= n-1 && 0 <= ey <= n-1)
draw_array[--k] = &this->lcell[n*ex + ey] // fills BACKWARDS -> forward walk = far-to-near
return 1
```
**Gotchas:** The early-out compares ONLY closest.x/y, not dir — a direction flip that maps to the same closest cell keeps the stale order (retail behavior; keep it). The 'if (dir > SOUTHEAST_OF_VIEWER) { use uninitialized locals }' branch is BN's rendering of the switch default — LandDefs::get_dir only returns 0..8, so it is unreachable; do not port. Direction semantics imply lcoord y grows northward (NORTH_OF_VIEWER -> closest edge y=0). CLandCell stride 0x108 is hardcoded in the index math. Entries are written with --k so the array is exactly filled (k reaches 0); DrawBlock then iterates 0..n*n-1 forward.
### RenderDeviceD3D::DrawBlock @0x005a17c0
**Summary:** Draws one landblock, two passes over the block's far-to-near draw_array. Pass 1: for visible cells with shadow objects, UpdateObjCell + insertion-sort the cell's shadow part list. Pass 2 per cell: restore FF material state if dirty, bind the region terrain surface array, then if the cell IsInView draw terrain (DrawLandCell, with the landscape detail texture enabled only for full 8x8 blocks), then if (alwaysDrawObjects || IsInView) draw the cell's contents (DrawSortCell = building + object parts) and conditionally flush the accumulated alpha mesh list. Pushes an identity viewer-relative Position for the whole block (the block offset lives in each cell's data/parts).
```c
RenderDeviceD3D::DrawBlock(this, blk):
Render::object_scale_vec = (1,1,1); Render::object_scale = 1
local Position pos = identity (vtable 0x796910, objcell_id 0); Frame::cache(&pos.frame)
Render::positionPush(3, &pos)
ACRender::curLandBlockVertexLighting = blk->vertex_lighting // per-block baked vertex light
n2 = blk->side_cell_count^2
for (i = 0; i < n2; i++): // PASS 1: sort translucent/shadow parts
cell = blk->draw_array[i]
if (cell->IsInView() != 0 && cell->num_shadow_objects != 0):
RenderDeviceD3D::UpdateObjCell(this, cell)
if (cell->num_shadow_parts > 1)
CShadowPart::insertion_sort(&cell->shadow_part_list, cell->num_shadow_parts)
full_res = (blk->side_cell_count == 8)
for (i = 0; i < n2; i++): // PASS 2: draw, far-to-near cells
if (render_device->ffStateDirty): // +0x7e4 flag; restores FF lighting sources
... reset material via device vtable, SetFFDiffuseColorSource(FromVertex), SetFFAmbientColorSource(FromVertex)
Render::SetSurfaceArray(CommandList::GetHead(CRegionDesc::current_region->terrain_info)->hold_run)
Render::object_scale_vec = (1,1,1); Render::object_scale = 1
cell = blk->draw_array[i]
if (cell->IsInView() != 0): // virtual, vtable+0x68
if (full_res): // detail texture only on full-res blocks
Render::curr_detail_surface = Render::landscape_detail_surface
Render::curr_detail_tiling = Render::landscape_detail_tiling
Render::curr_detail_src_blend = 5; Render::curr_detail_dst_blend = 6
this->vtable->DrawLandCell(cell) // terrain polys
Render::curr_detail_surface = 0
if (alwaysDrawObjects != 0 || cell->IsInView() != 0):
this->vtable->DrawSortCell(cell) // building + objects (below)
if (flush <compares vs 1.0>) // global float flush = 0.75 @0x00820ed0
D3DPolyRender::FlushAlphaList(flush) // flushes only if alpha list > flush*3000 entries
ACRender::curLandBlockVertexLighting = 0
Render::framePop()
```
**Gotchas:** 1) The FlushAlphaList guard is x87 mush (fcom flush vs 1f, test ah,0x41) — polarity ambiguous from BN, but the global 'float flush = 0.75' (@0x00820ed0) plus FlushAlphaList's own internal gate (it flushes only when alphaedMeshCountClip or alphaedMeshCountAlpha exceeds arg*3000, i.e. 2250) means the sensible reading is 'if (flush < 1.0f) FlushAlphaList(flush)': a mid-walk pressure valve, not an every-cell flush; end-of-frame calls use FlushAlphaList(0f) = flush everything. 2) 'uint32_t id = blk->m_DID.id' is read then apparently unused in the BN output — likely it fills the local Position's objcell_id (var_44); harmless either way since the frame is identity. 3) IsInView is the virtual at vtable+0x68 (BN shows it named in pass 1 but as a raw '+0x68' call in pass 2 — same slot). 4) draw_array entries are CLandCell* but dispatched through the CObjCell/CSortCell interfaces (CLandCell : CSortCell : CObjCell).
### RenderDeviceD3D::DrawSortCell (+ DrawLandCell, where DrawBuilding fires) @0x0059f140 (DrawLandCell 0x0059f120, DrawBuilding 0x0059f2a0, DrawObjCell 0x005a1a40)
**Summary:** The per-cell content dispatch, one level below DrawBlock. DrawLandCell submits the cell's terrain polygons (ACRender::landPolysDraw(cell->polygons, 2)). DrawSortCell is where buildings enter the outdoor walk: if the cell owns a CBuildingObj it calls the vtable DrawBuilding FIRST, then DrawObjCell for the cell's remaining parts (scenery/static/dynamic shadow parts via DrawPartCell). Because DrawBlock walks cells far-to-near inside blocks that LScape::draw walks far-to-near, DrawBuilding fires strictly far-to-near — exactly the trace's interleave.
```c
RenderDeviceD3D::DrawLandCell(this, cell): // @0x0059f120
ACRender::landPolysDraw(cell->polygons, 2)
RenderDeviceD3D::DrawSortCell(this, cell): // @0x0059f140; cell is CSortCell
if (cell->building != 0)
this->vtable->DrawBuilding(cell->building) // = RenderDeviceD3D::DrawBuilding @0x0059f2a0
this->vtable->DrawObjCell(cell) // = RenderDeviceD3D::DrawObjCell @0x005a1a40
RenderDeviceD3D::DrawObjCell(this, cell): // @0x005a1a40
RenderDeviceD3D::DrawPartCell(this, cell->m_DID.id, cell ? &cell->vtable : 0, 0)
RenderDeviceD3D::DrawBuilding(this, bldg): // @0x0059f2a0 (head only; body not in scope this task)
RenderDeviceD3D::outdoor_pview->outdoor_portal_list = bldg->portals // arms look-in portal punches
CPhysicsPart::UpdateViewerDistance(bldg->part_array->parts[0]); ...
```
**Gotchas:** DrawObjCell's BN body has a bogus else-branch dereferencing arg2 after a null check ('if (arg2) DrawPartCell(..., &arg2->vtable, 0); else DrawPartCell(arg2->m_DID.id, nullptr, 0)') — decomp artifact of a cmov/sete; the intent is DrawPartCell(cell_id, cell-or-null, 0). DrawBuilding's very first act is publishing the building's portal list into the outdoor PView — this is the hook that produces the DrawCells look-in punches (ov=0, 13 cells) interleaved with far-to-near DrawBuilding calls in the live trace; the building body itself was only skimmed here (out of assigned scope).
**Report notes:** Trace reconciliation (docs/research/2026-08-30-fw-walk-oracle/README.md): fully consistent. PView::DrawCells @0x005a4840 with an outdoor view (outside_view.view_count > 0) sets Render::PortalList = the PView, calls LScape::draw, then FlushAlphaList(0f), then walks its cell_draw_num punch list — that is the interleave of LScape::draw / DrawBuilding / DrawCells look-ins the trace shows. The far-to-near ordering has TWO nested sources, both ports of the same ring pattern: (a) LScape::get_block_order builds block_draw_list near-to-far (viewer block index 0, rings outward via static coefficient tables @0x0081cccc..0x0081cd18) and LScape::draw iterates it BACKWARDS; (b) CLandBlock::calc_sq_draw_order fills the per-block cell draw_array BACKWARDS from the closest cell (direction-selected via the 9-case LandDefs::Direction switch, jump table @0x0052f8e0: 0=IN_VIEWER_BLOCK,1=N,2=S,3=E,4=W,5=NW,6=SW,7=NE,8=SE) so DrawBlock's forward walk is far-to-near. Terrain vs buildings vs scenery: DrawBlock per cell draws terrain first (DrawLandCell -> landPolysDraw, detail texture only when side_cell_count==8) then DrawSortCell = building (if the CSortCell owns one) then DrawObjCell (scenery/static/dynamic shadow parts via DrawPartCell); shadow parts are pre-sorted per cell (CShadowPart::insertion_sort) in DrawBlock pass 1. Visibility: LScape::draw_check_blocks clears all block/cell in_view then unions visibility across every portal view using clip-height interval grids (Render::get_clip_height per corner, Render::block_check per block/cell against max_zval/min_zval); a cell already marked by an earlier view is skipped (IsInView()==0 pre-check), so downgrades never happen. Key constants: grid is mid_width=11 x 11 blocks, mid_radius=5 (LScape ctor @0x00505370); landblock side = 192.0 m (FPU-elided as '0f' in calc_frame/draw_check_blocks — pinned by the explicit 24f cell pitch in landcell_check); block frames are viewer-relative (origin (row-viewer_b_xoff)*192, (col-viewer_b_yoff)*192, 0); global float flush = 0.75 @0x00820ed0 gates mid-walk alpha flushing (FlushAlphaList flushes only above arg*3000 queued alpha meshes). Struct authority (acclient.h): CLandBlock : SerializeUsingPackDBObj, CLandBlockStruct — side_cell_count @+0x48, lcell @+0x90 (CLandCell stride 0x108, cell in_view @+0x104), in_view @+0xfc (BoundingType 0=OUTSIDE,1=PARTIALLY_INSIDE,2=ENTIRELY_INSIDE), max_zval/min_zval, draw_array/draw_array_size at tail; LScape = {mid_radius, mid_width, land_blocks, block_draw_list, loaded_cell_id, viewer_cell_id, viewer_b_xoff, viewer_b_yoff, sky, 4 detail surfaces}. Second LScape::draw caller: SmartBox::RenderNormalMode @0x00453aa0. All BN mush encountered is flagged per-function; the two worth repeating: the '-((eax_4 - eax_4))' arg in update_viewpoint decodes to 'landblock (high-word) changed ? 1 : 0' (it gates get_block_order), and landcell_check's 'num_static_objects =' store is really the cell's in_view.
## Report 5 - View construction (ConstructView overloads / GetClip / viewconeCheck / set_view)
### PView::ConstructView (CEnvCell overload — interior cell flood) @0x005a57b0
**Summary:** The interior visibility flood. Resets the PView's outside_view and per-frame counters, bumps the master timestamp, seeds the todo list with the entry cell (portal index 0xffff = 'entered from no portal' when called from DrawInside; other_portal_id when recursed into from the CBldPortal overload), then LIFO-pops cells: each popped cell is appended to cell_draw_list, its TOP portal_view entry marked cell_view_done, and if ClipPortals finds visible portals, AddViewToPortals pushes neighbor cells (with clipped views) onto the todo list. Returns void.
```c
void PView::ConstructView(CEnvCell* seed, uint16_t thru_portal_idx) // __thiscall, 0x005a57b0
{
this->outside_view.view_count = 0; // reset the accumulated exit-to-landscape view
PView::master_timestamp += 1;
this->cell_todo_num = 0;
this->cell_draw_num = 0;
PView::InitCell(this, seed, thru_portal_idx); // 0x005a4b70
PView::InsCellTodoList(this, seed, 0.0f); // 0x005a4f50 — seed at distance key 0.0
while (true) {
if (this->cell_todo_num == 0) return;
cell = this->cell_todo_list.data[--this->cell_todo_num]->cell; // LIFO: pop from END
if (cell == null) return; // null entry terminates the whole flood
if (this->cell_draw_num >= this->cell_draw_list.sizeOf)
DArray<CEnvCell*>::grow(&this->cell_draw_list, this->cell_draw_num + 0x1e); // +30
this->cell_draw_list.data[this->cell_draw_num++] = cell;
cell->portal_view.data[cell->num_view - 1]->cell_view_done = 1; // mark TOP view done
if (PView::ClipPortals(this, cell, 0) != 0) // 0x005a5520, arg3=0 (portal start index)
PView::AddViewToPortals(this, cell); // 0x005a52d0 — enqueues neighbors w/ views
}
}
```
**Gotchas:** The actual view-array appending is DELEGATED: ClipPortals clips each of the cell's portals against the cell's current view (via GetClip with do_clip=1 and a temp_view + Render::copy_view), and AddViewToPortals does per-neighbor InitCell + InsCellTodoList (distance key read at neighbor+0x34) — neither was in this assignment's scope; only their call points are recorded here. cell_todo_list pop is from the END (LIFO), while InsCellTodoList's float key suggests sorted insertion — pop order therefore depends on InsCellTodoList's insert position (unverified). The 'if (cell == 0) break' path exits the entire flood, not just skips the entry. Callers: PView::DrawInside @0x005a5860 (seed = player's cell, idx 0xffff, after Render::copy_view(top_view, NULL, 4) installed a full-viewport quad as the root view and an identity Position with the cell's id was pushed via Render::positionPush(3,...)); and the CBldPortal overload's recursion. DrawInside then calls PView::DrawCells(this, 0) — literal 0.
### PView::ConstructView (CBldPortal overload — look through one building portal) @0x005a59a0
**Summary:** Views through a single building portal polygon. Tests which side of the portal plane the eye is on (epsilon 0.0002), rejects if the eye is on the wrong side for this portal's authored side (or exactly in-plane), clips the portal polygon to the active view via GetClip, installs the clipped screen polygon as a NEW view on the destination cell's top portal_view entry via Render::copy_view, optionally rasterizes the portal poly, pops the current position frame, then recurses into the CEnvCell flood overload. Returns 1 on success, 0 on any rejection.
```c
int PView::ConstructView(CBldPortal* bp, CPolygon* poly, int do_clip, int mode) // __thiscall, 0x005a59a0
{
// Viewpoint side test. F_EPSILON = 0.000199999995f (~0.0002). Viewpoint is
// Render::FrameCurrent->viewer.viewpoint = eye in the CURRENTLY-PUSHED frame's local space
// (the building's object frame), same space as poly->plane.
d = poly->plane.N.x*vp.x + poly->plane.N.y*vp.y + poly->plane.N.z*vp.z + poly->plane.d;
if (d <= 0.0002f) { side = NEGATIVE; if (d >= -0.0002f) side = IN_PLANE; }
else side = POSITIVE;
if (bp->portal_side == 0) { if (side != POSITIVE) return 0; } // authored front-side portal
else { if (side != NEGATIVE) return 0; } // authored back-side portal
// (IN_PLANE fails BOTH gates -> return 0)
int npts;
PView::GetClip(this, side, poly, PView::clip_view /*static Vec2Dscreen* buffer*/, &npts, do_clip); // 0x005a4320
if (npts == 0) return 0;
CEnvCell* cell = CEnvCell::GetVisible(bp->other_cell_id); // 0x0052dc10
if (cell == null) return 0;
// APPEND: one view_poly + its screen pts + per-edge world planes into the DESTINATION
// cell's TOP portal_view entry (portal_view.data[num_view-1]).view.{poly,vertex}; ++view_count
ok = Render::copy_view(cell->portal_view.data[cell->num_view - 1], PView::clip_view, npts); // 0x0054dfc0
if (ok == 0) return 0; // degenerate (<3 surviving screen pts) -> reject
if (mode != 2)
D3DPolyRender::DrawPortalPolyInternal(poly, mode == 1); // 0x0059bc90
Render::positionPop(); // BN names it framePop — pops the building frame, SUCCESS PATH ONLY
if (mode != 1)
PView::ConstructView(this, cell, (uint16_t)bp->other_portal_id); // recurse into 0x005a57b0 flood
return 1;
}
```
**Gotchas:** (1) BN's Sidedness selection at 0x005a59ea has an INVERTED flag decode ('if (p_1) eax = IN_PLANE' from test ah,0x5); Ghidra confirms the correct reading: side = POSITIVE iff d > +0.0002, NEGATIVE iff d < -0.0002, IN_PLANE iff -0.0002 <= d <= +0.0002 both boundary comparisons are INCLUSIVE toward IN_PLANE, and IN_PLANE always rejects (nothing drawn through a portal whose plane contains the eye). (2) Render::positionPop happens ONLY on the success path, and BEFORE the recursion; on failure the pushed building frame is left in place the caller PView::DrawPortal @0x005a5ab0 compensates by re-pushing CBuildingObj::curr_pos + Render::obj_view_set only on success. Any port must preserve this push/pop asymmetry. (3) BN's 'st0_1 = copy_view(...)' return handling is mush; the return is a plain int in eax. (4) mode semantics at this level: mode==2 skips drawing the portal poly; mode==1 draws it with flag=true and SKIPS the recursion (caller DrawPortal also skips DrawCells when mode==1); DrawPortal's failure path draws the poly with flag=false only when mode==3. (5) do_clip (param_3) is forwarded verbatim to GetClip as its clip/copy switch. (6) clip_view is a shared static output buffer (also used by ClipPortals' GetClip call at 0x005a5655) not reentrant.
### PView::GetClip @0x005a4320
**Summary:** Projects a portal polygon's vertices to screen space and produces the ordered (winding-corrected) screen point list, either verbatim (do_clip=0) or clipped against the currently installed view (do_clip!=0) via ACRender::polyClipFinish the chain-polygon clip against the active view's screen polygon/planes installed by Render::set_view. NEGATIVE side reverses vertex order so the output winding is consistent regardless of which face of the portal is toward the eye.
```c
void PView::GetClip(Sidedness side, CPolygon* poly, Vec2Dscreen** out_pts, int* out_npts, int do_clip) // this unused; 0x005a4320
{
*out_npts = 0;
for (i = 0; i < poly->num_pts; i++) // num_pts is a uint8
poly->screen[i] = PrimD3DRender::xformStart(poly->vertices[i], 1); // 0x0059b990, project; returns ptr into xform buffer
if (do_clip == 0) {
*out_npts = poly->num_pts;
if (side == POSITIVE) for (i) out_pts[i] = poly->screen[i]; // forward order
else for (i) out_pts[i] = poly->screen[poly->num_pts - 1 - i]; // reversed (winding flip)
return;
}
if (side == POSITIVE) {
Render::PolyCurrent = null; Render::PolyCurrentMod = 1.0f; Render::PolyCurrentPos = 1;
ACRender::polyClipFinish(poly->screen, poly->num_pts, out_pts, out_npts, 0); // 0x006b6d00
} else {
Vec2Dscreen* rev[32]; // fixed 32-slot stack buffer
for (i = 0; i < poly->num_pts; i++) rev[i] = poly->screen[poly->num_pts - 1 - i];
Render::PolyCurrent = null; Render::PolyCurrentMod = 1.0f; Render::PolyCurrentPos = 1;
ACRender::polyClipFinish(rev, poly->num_pts, out_pts, out_npts, 0);
}
}
```
**Gotchas:** The clip itself lives in ACRender::polyClipFinish (not in scope), which clips against the CPU view context installed by Render::set_view (portal_npnts/portal_vertex/portal_inmask/xmin..ymax) GetClip only projects, orders, and resets Render::PolyCurrent/PolyCurrentMod/PolyCurrentPos before delegating. The IN_PLANE side takes the NEGATIVE (reversed) branch in both modes, though ConstructView never passes IN_PLANE (other callers: ClipPortals @0x005a54f6/0x005a5655 pass sides from portal records). The reversal buffer is a fixed 32 pointers portal polys are assumed <= 32 verts. Side effect: mutates poly->screen[] with pointers into a shared per-frame transform buffer (xformStart output), so results are only valid within the current frame context. Convention quirk: declared thiscall (this in ecx) but the body never touches this.
### Render::viewconeCheck @0x0054c250
**Summary:** Sphere-vs-active-view test. Scales the sphere by Render::object_scale, transforms its center from the current pushed frame into viewer_pos block space ('global'), publishes the viewer-frame-local center and scaled radius into Render::local_object_center/local_object_radius (as CPU globals for downstream draw code), then tests the global center against the fixed viewer_world_space.CY plane plus the portal_npnts active view edge planes (portal_vertex[i].plane, installed by set_view). Returns OUTSIDE if fully behind any plane; PARTIALLY_INSIDE if any plane's distance <= radius; else ENTIRELY_INSIDE.
```c
BoundingType Render::viewconeCheck(const CSphere* s) // __cdecl, 0x0054c250
{
c = Render::object_scale * s->center; // Vec3, assembled contiguously on stack
r = Render::object_scale * s->radius;
// current-frame local -> viewer_pos block space ('global'):
g = Position::localtoglobal(&Render::viewer_pos, /*out*/, &Render::FrameCurrent->position, /*in*/ c);
// publish viewer-frame-local center + radius (side effect, ALWAYS, even for OUTSIDE):
l = Frame::globaltolocal(&Render::viewer_pos.frame, /*out*/, /*in*/ g);
Render::local_object_center = l;
Render::local_object_radius = r;
// plane 1: the fixed CY plane of viewer_world_space (world-space view cone)
d = dot(Render::viewer_world_space.CY.N, g) + Render::viewer_world_space.CY.d;
if (d < -r) return OUTSIDE; // strictly less
partial = (d <= r); // inclusive
// planes 2..1+portal_npnts: active view edge planes (view_vertex stride 24, plane at +8)
for (i = 0; i < Render::portal_npnts; i++) {
P = Render::portal_vertex[i].plane;
d = P.N.x*g.x + P.N.y*g.y + P.N.z*g.z + P.d;
if (d < -r) return OUTSIDE;
if (d <= r) partial = true;
}
return partial ? PARTIALLY_INSIDE : ENTIRELY_INSIDE; // enum: OUTSIDE=0, PARTIALLY_INSIDE=1, ENTIRELY_INSIDE=2
}
```
**Gotchas:** The BN pseudo-C for this function is UNUSABLE for the comparisons: its esi_1 partial-flag logic ('if (p_1) esi_1 = 0 else 1' and the final 'eax_4 = esi_1 == 0') has inverted flag senses and a lost return value; the pseudocode above is from the Ghidra decomp of the same PDB-paired binary and is geometrically self-consistent. Two distinct spaces are in play: the PLANE tests use the 'global' (viewer_pos block-space) center g, while the PUBLISHED Render::local_object_center is the viewer-frame-LOCAL center l — BN's stack-slot aliasing (local_10/local_4 reuse across the two transform calls) obscures this completely. The local_object_center/radius stores happen BEFORE any plane test, so they are valid even when OUTSIDE is returned. Boundary semantics: cull is strict (d < -r); the partial flag is inclusive (d <= r), so a sphere exactly tangent from inside counts as PARTIAL, not ENTIRELY_INSIDE. With portal_npnts == 0, the result depends on the CY plane alone. The edge planes were built in world space by Render::copy_view (cross products of unprojected screen-point rays, d = -dot(N, viewer_world_space.viewpoint)), which is why a sphere test against screen-derived portal views is done in world space here. Caller RenderDeviceD3D::DrawMesh @0x005a0860 shows the enum contract: != OUTSIDE draws with the returned value passed into DrawMeshInternal (ENTIRELY_INSIDE presumably skips per-poly clip).
### Render::set_view @0x0054d0e0
**Summary:** Installs one view (poly record i of a view_type) as the active CPU clip context: sets the portal_view/portal_view_num globals, the active edge count portal_npnts, the clip in-mask with npnts+1 bits (the +1 covering the fixed CY plane alongside the npnts edge planes), the portal_vertex base pointer (screen pt + world plane per vertex), and the screen-space bounding rect xmin/xmax/ymin/ymax. Pure global-installation; no computation.
```c
void Render::set_view(view_type* v, int i) // __cdecl, 0x0054d0e0
{
Render::portal_view_num = i;
Render::portal_view = v; // callers pass &portal_view_type::view (offset 16)
view_poly* vp = &v->poly.data[i]; // {int vertex_count; int vertex_index; float xmin,xmax,ymin,ymax}
Render::portal_npnts = vp->vertex_count;
Render::portal_inmask = (1 << ((vp->vertex_count + 1) & 0x1f)) - 1; // npnts edge planes + 1 CY plane
Render::portal_vertex = &v->vertex.data[vp->vertex_index]; // view_vertex {Vec2D pt; Plane plane}, stride 24
Render::xmin = vp->xmin; Render::xmax = vp->xmax;
Render::ymin = vp->ymin; Render::ymax = vp->ymax;
}
```
**Gotchas:** The CPU globals installed: portal_view_num, portal_view, portal_npnts, portal_inmask, portal_vertex, xmin, xmax, ymin, ymax. These are exactly what viewconeCheck (planes) and ACRender::polyClipFinish / the poly pipeline (mask + bounds) consume. The '& 0x1f' on the shift count is the x86 shl truncation made explicit by Ghidra with copy_view capping views at 31 points, vertex_count+1 <= 32, and a 32 shift would wrap; retail never hits it because copy_view clamps to 0x1f. Callers iterate i over [0, portal_view_type::view_count) on a cell's TOP portal_view entry (e.g. PView::DrawCells at 0x005a08d8-pattern sites, RenderDeviceD3D::DrawMesh @0x005a08d8, CEnvCell code @0x0052c449) one cell can hold several view polys (one per portal it was reached through) and each gets installed and tested in turn. BN renders some call sites as 'set_view(&esi[4], i)' or 'set_view(&arg2->m_timeStamp, arg3)' — field-name garbage; all pass a portal_view_type's embedded view_type.
**Report notes:** Sources: docs/research/named-retail/acclient_2013_pseudo_c.txt (lines 433750-433933 ConstructView overloads + DrawInside/DrawPortal; 432344-432425 GetClip; 342860-342941 viewconeCheck; 343750-343764 set_view) cross-checked against the live Ghidra patchmem decomp (port 8081) for every x87-flag-ambiguous branch, and docs/research/named-retail/acclient.h for structs: view_poly (line 32465), view_vertex {Vec2D pt; Plane plane} (32483, stride 24), view_type {vertex_count_total; DArray<view_poly> poly; DArray<view_vertex> vertex} (32338), portal_view_type {DArray<portal_info> portal; view_type view; float max_indist; uint view_count; int cell_view_done; int view_timestamp; int update_count} (32346), PView {portal_view_type outside_view; int draw_landscape; CBldPortal** outdoor_portal_list; DArray<CEnvCell*> cell_draw_list; uint cell_draw_num; DArray<CellListType*> cell_todo_list; uint cell_todo_num; LScape* lscape} (45934), CBldPortal {portal_side; other_cell_id; other_portal_id; exact_match; num_stabs; stab_list; sidedness} (32094), CCellPortal (32300). Enums: Sidedness {POSITIVE=0, NEGATIVE=1, IN_PLANE=2, CROSSING=3} (2527), BoundingType {OUTSIDE=0, PARTIALLY_INSIDE=1, ENTIRELY_INSIDE=2} (5365).
APPEND MECHANICS (Render::copy_view @0x0054dfc0, read for call-site accuracy, not a full assignment): copy_view(dest_pv, pts, npts) appends ONE view to dest_pv->view: a new view_poly at index view_count (vertex_index = current vertex_count_total, which RESETS to 0 when view_count==0), the surviving screen points into .view.vertex (perspective-divide by w unless w==1; drop points within ~1px of the previous kept point AND collinear within a 1px cross-product tolerance; <3 survivors => return 0; cap 0x1f=31; a closing duplicate vertex is appended after the poly), the xmin/xmax/ymin/ymax bounds, then per-edge WORLD-space planes into each view_vertex.plane (unproject each screen pt to a view ray PrimD3DRender::ScreenToViewTransform when Render::newmethod==1, else the manual Xaxis/Yaxis/Zaxis/xinvscale/yinvscale/tx/ty/vdst path N = normalize(cross(ray_i, ray_i+1)) with F_EPSILON degeneracy skip, d = -dot(N, viewer_world_space.viewpoint)), and finally ++view_count, return 1. copy_view(dest, NULL, 4) is the special full-viewport-quad path used by DrawInside for the root cell view.
TRACE RECONCILIATION (docs/research/2026-08-30-fw-walk-oracle/README.md): (a) The overall shape matches the code: outdoor building look-ins go RenderDeviceD3D::DrawBuilding -> PView::DrawPortal @0x005a5ab0 -> ConstructView(CBldPortal) -> recursion into the cell flood -> PView::DrawCells; interior frames go PView::DrawInside @0x005a5860 -> full-viewport root view -> ConstructView(CEnvCell, 0xffff) -> DrawCells. The 1-3 cell look-in punches are the cell_draw_list contents of that flood. (b) DISCREPANCY TO RE-CHECK ON THE TRACE SIDE: the literal second argument to PView::DrawCells is 0 from DrawInside and 1 from DrawPortal (both confirmed in Ghidra; BN's 'DrawCells(this, edx_2)' consuming ConstructView's return is a phantom — the CEnvCell overload is void and the real arg is the stack literal). The oracle's labels (look-ins 'ov=0', interior 'ov=1') are EXACTLY INVERTED relative to these raw literals — either the probe read the wrong slot for this (stack vs register) or 'ov' in the trace is a derived label, not the raw argument. Worth re-verifying before anything keys off ov. These are the only two DrawCells call sites in the binary. (c) 'DrawCells draws LScape through the exit view' is consistent with PView::outside_view being zeroed at flood start and (per PView layout: draw_landscape flag, lscape pointer) consumed inside DrawCells @0x005a4840 — DrawCells was not in this assignment.
ADJACENT FUNCTIONS a porter will need next (addresses pinned, bodies not read in full): PView::InitCell @0x005a4b70, PView::InsCellTodoList @0x005a4f50, PView::ClipPortals @0x005a5520 (contains the second GetClip site with do_clip=1 and a temp_view + copy_view at 0x005a5495), PView::AddViewToPortals @0x005a52d0, PView::DrawCells @0x005a4840, PView::add_views @0x005a5210 / remove_views @0x005a42e0 (push/pop portal_view entries on stab-list cells around a portal draw), ACRender::polyClipFinish @0x006b6d00, PrimD3DRender::xformStart @0x0059b990, Render::copy_view @0x0054dfc0.
Global epsilon: F_EPSILON = 0.000199999995f (raw float 0x3951B717), used both by ConstructView's side test and copy_view's degenerate-normal skip.