# AREA 3 — Interior cell rendering and the draw-side portal clip (#114) ## RETAIL THE DATA STRUCTURES. A "portal view" is NOT a set of clip planes handed to the rasterizer — it is a 2D screen polygon whose edges each carry a 3D plane through the eye, stored per cell. `portal_view_type` (acclient.h:32345-32355) = { DArray portal; view_type view; float max_indist; uint view_count; int cell_view_done; int view_timestamp; int update_count }. `view_type` (acclient.h:32337-32343) = { vertex_count_total; DArray; DArray } — note the PLURAL: one cell accumulates a LIST of view polygons. `view_poly` (acclient.h:32465-32473) = { vertex_count, vertex_index, xmin/xmax/ymin/ymax } (a slice into the vertex array + 2D screen bbox). `view_vertex` (acclient.h:32483-32487) = { Vec2D pt; Plane plane } — a screen point PLUS the 3D eye-edge plane used for object culling. `portal_info` (acclient.h:32458-32462) = { int seen; int inflag }. `CCellPortal` (acclient.h:32300-32308) = { other_cell_id, other_cell_ptr, CPolygon* portal, portal_side, other_portal_id, exact_match }. Crucially, `CCellStruct::UnPack` (Ghidra 0x00533d00) shows `portals[i] = polygons + portal_poly_id` — portal-aperture polygons are ordinary entries in the SAME drawn-polygon array. VIEW CONSTRUCTION (the flood). `PView::DrawInside` (Ghidra 0x005a5860, pc:433793): curr_view_push(cell), add_views over the cell's stab list, positionPush(cell), then `Render::copy_view(cell.top_view, nullptr, 4)` — a NULL source installs the FULL-SCREEN 4-vertex viewport quad as the root view (Ghidra 0x0054dfc0, also pc:345574) — then `ConstructView(cell, 0xffff)` and `DrawCells(this, 0)`. ConstructView(CEnvCell) (pc:433750-433792): master_timestamp++, InitCell(root, 0xffff), InsCellTodoList(root, 0f), then a worklist loop popping the NEAREST cell (cell_todo_list is sorted by InitCell's max/min portal-vertex distance), appending it to cell_draw_list, and running ClipPortals + AddViewToPortals on it (pc:433786-433787). `PView::InitCell` (Ghidra 0x005a4b70, pc:432896): per portal of the cell, classify the EYE against the portal polygon's plane with F_EPSILON = 0.0002 (acclient.h-adjacent const at 0x7e32f8): dist > +eps → side 0, dist < -eps → side 1; if side != portal_side the portal faces AWAY (inflag=1, not traversable); if side == portal_side OR |dist| <= eps (the knife-edge in-plane case) → inflag=0, candidate. The portal you ENTERED through (index == arg3) is force-marked inflag=1/seen=1 so the flood never walks back. `PView::ClipPortals` (Ghidra/pc:433572): for each portal with seen && !inflag, resolve the neighbour (CEnvCell::GetVisible), then FOR EACH accumulated view i of this cell (`Render::set_view(&view, i)` then `PView::GetClip(portal_side, portal_poly, &clip_view, &n, 1)` pc:433651): project the portal polygon to homogeneous screen space and software-clip it against the INSTALLED view. If the portal leads outside (other_cell_id == 0xffffffff) and cliplandscape (default 1, 0x00820f4c) → `Render::copy_view(&pview->outside_view, clip, n)` appends the clipped aperture to outside_view (pc:433668-433676); else after `PView::OtherPortalClip` (pc:433524, the neighbour-side reciprocal re-clip through the matching back-portal indexed DIRECTLY by other_portal_id, 0x005a54b2/0x005a54f6) → `Render::copy_view(neighbour.top_view, clip, n)` APPENDS the polygon to the NEIGHBOUR's view list (pc:433674, target resolved via num_view/portal_view at 0x134/0x138). MULTI-PORTAL ACCUMULATION: a cell visible through multiple portals accumulates MULTIPLE view polygons — copy_view (Ghidra 0x0054dfc0) perspective-divides the clipped verts, merges vertices closer than ~1 PIXEL (|dx|<=1 && |dy|<=1 screen units), appends a new view_poly + grows view_count. It is a UNION-AS-LIST; polygons are never merged geometrically. `PView::AddViewToPortals` (pc:433446, 0x005a52d0): for each portal whose clip produced something, if the neighbour was never seen this timestamp → InitCell + InsCellTodoList (enqueue ONCE); if already seen and its view GREW (0x44 watermark != 0x38 view_count) → `AddToCell` IN PLACE and, if the cell was already drawn-listed, `FixCellList` = AdjustCellPlace (re-sorts cell_draw_list so the grown cell draws in dependency order, pc:433247) + AdjustCellView (re-clips ONLY the new views: ClipPortals(cell, update_count), pc:433741-433745). There is NO re-enqueue and no iteration cap — growth propagates recursively in place, and the 1-px vertex dedup gives the fixpoint a hard floor. WHAT set_view INSTALLS: `Render::set_view(view_type*, n)` (pc:343750, 0x0054d0e0) sets Render::portal_view/portal_view_num, portal_npnts = poly.vertex_count, portal_inmask = (1<<(npnts+1))-1, portal_vertex = &vertex.data[poly.vertex_index] (the screen points + eye-edge planes), and the 2D xmin/xmax/ymin/ymax bbox. This is GLOBAL clipper state consumed by polyClipFinish and viewconeCheck. THE SOFTWARE CLIP: `ACRender::polyClipFinish` (Ghidra 0x006b6d00, pc:702749) is a Sutherland-Hodgman clipper in HOMOGENEOUS screen coordinates (Vec2Dscreen = xw,yw,zw,w): stage 1 clips against w = cdstW (the near/eye plane, interpolating all four homogeneous components — no divide, so eye-grazing portals never blow up); stage 2 clips against each of the installed view's portal_npnts edges using the perspective-correct 2D test (xw - x_edge*w)*dy - (yw - y_edge*w)*dx, gated per-edge by the planeMask shifted by (0x1e - npnts) — a SET bit means SKIP that edge. <3 surviving verts → output count 0. It is pixel-exact because (a) it clips polygon-vs-polygon with no plane-count budget (views are DArrays, blocksize 0x80; the loop runs all npnts edges), and (b) the homogeneous interpolation is exactly the rasterizer's math. THE DRAW — THE LOAD-BEARING SURPRISE: retail NEVER clips cell geometry. `RenderDeviceD3D::DrawEnvCell` (0x0059f170, pc:427880-427930): production path is the prebuilt D3D mesh — `D3DPolyRender::DrawMesh(num_surfaces, surfaces, constructed_mesh, 1)` (pc:427905) built once at `CEnvCell::UnPack` (Ghidra 0x0052d470, ConstructMesh of ALL structure->polygons, 3.0 detail, use_built_mesh=1). The legacy poly-list path submits every polygon with planeMask=0xffffffff (pc:427922) — and 0xffffffff after the (0x1e-npnts) shift has the sign bit set for every edge iteration, i.e. SKIP ALL VIEW EDGES; moreover `D3DPolyRender::polyListFinishInternal` (Ghidra 0x0059dba0) just calls `DrawPolyInternal` per poly, and `DrawPolyInternal` (Ghidra 0x0059d7c0) does NO view clipping whatsoever — it builds a triangle fan from the 3D verts and calls DrawPrimitiveUP, gated only by `(surface->type & 6) != 0` (BASE1_IMAGE|BASE1_CLIPMAP, acclient.h:5820-5824). The accumulated views gate ADMISSION, OBJECT CULLING and PUNCHES — never geometry pixels. PIXEL-EXACTNESS = DEPTH PUNCH + ORDER + Z-BUFFER. `PView::DrawCells` (Ghidra 0x005a4840): pass 1 (only when outside_view.view_count != 0): PortalList=&outside_view, `LScape::draw` FIRST (landscape through the accumulated outside views), FlushAlphaList, m_nFrameStamp++, then for every cell far→near, FOR EACH VIEW (`CEnvCell::setup_view(cell, i)` = set_view of view i, Ghidra 0x0052c430), every portal with other_cell_id == -1 (to landscape) gets `D3DPolyRender::DrawPortalPolyInternal(poly, false)`. That function (Ghidra 0x0059bc90, pc:424490) projects the aperture polygon, SOFTWARE-CLIPS it against the installed view via polyClipFinish(mask=0 → clip ALL edges), and draws the clipped polygon as an INVISIBLE DEPTH-ONLY quad: DEPTHTEST_ALWAYS, z-write on, z = the portal's true projected depth zw/w when maxZ2=6 (bit0 clear; 0x00820e14) or z = 0.99999988 (far plane) when maxZ1=7 (bit0 set; 0x00820e18); alpha byte 0 so no color. Writing the DOOR-PLANE depth into the aperture after the landscape protects the landscape pixels: any interior geometry FARTHER than the door fails the z-test inside the aperture; nearer geometry draws normally. Pass 2 draws cells far→near, per view, via vtbl+0x5c = DrawEnvCell (vtable at pc:1037045, 0x7e555c) — the GetDrawnThisFrame guard (Ghidra 0x0052c0c0, == m_nFrameStamp) makes the per-view repeats no-ops, so cell GEOMETRY draws ONCE, unclipped; the per-view loop only matters for punches and object culling. Epilogue: per cell, PortalList = the cell's view, vtbl+0x64 = DrawObjCell — objects are tested per view by `Render::viewconeCheck` (Ghidra 0x0054c250): bounding sphere vs the camera plane AND each view_vertex.plane of the installed view (stride 6 floats), OUTSIDE → skipped; objects are CULLED, never clipped (RenderDeviceD3D::DrawMesh loops PortalList views with building_view filtering, pc:427940-428060 / 0x005a0860). OUTSIDE-LOOKING-IN: `RenderDeviceD3D::DrawBuilding` (Ghidra 0x0059f2a0, pc:427938): set outdoor_pview->outdoor_portal_list = building->portals, then CPhysicsPart::Draw(part, 1) → DrawMeshInternal arg3=1 → TWO drawing-BSP walks `BSPTREE::build_draw_portals_only(drawing_bsp, 1)` then `(.., 2)` (pc:427993-427994, 0x0059f3cc/0x0059f3d9); each BSPPORTAL node fires RenderDevice::DrawPortal(portalPoly, 1, mode) (pc:326947-326992, 0x0053d870) → `PView::DrawPortal` (Ghidra 0x005a5ab0, pc:433895) → `ConstructView(CBldPortal)` (Ghidra 0x005a59a0, pc:433827). Mode 1 = eye-side gate (F_EPSILON, IN_PLANE → return 0 — building portals reject the knife-edge OUTRIGHT), GetClip vs the current view, copy_view into the interior cell, then PUNCH the door aperture to FAR-Z (param_4==1 → maxZ1) and stop. Mode 2 = no punch, RECURSE ConstructView(cell, other_portal_id) building the interior view graph, then DrawCells(this, 1) draws the interior cells into the punched aperture. THEN CPhysicsPart::Draw(part, 0) draws the building SHELL mesh last; interior pixels survive only inside the punched aperture (everywhere else the shell's nearer depth rejects them). Pixel-exact, again with zero geometry clipping. KNIFE-EDGE (Q3): two distinct behaviors. Building portals (ConstructView(CBldPortal), Ghidra 0x005a59a0): |eye·N + d| <= 0.0002 → Sidedness IN_PLANE → return 0; the portal contributes nothing that frame; no degenerate view is ever built. Cell-to-cell portals (InitCell, Ghidra 0x005a4b70): the in-plane case leaves inflag=0 (candidate) and the degenerate projection dies naturally downstream — polyClipFinish's homogeneous near-W clip plus copy_view's 1-pixel vertex dedup collapse a sub-pixel sliver to <3 verts → no view appended → no propagation. Physically correct: an edge-on aperture subtends zero pixels. >8 PLANES (Q4): confirmed unbounded. Views are DArray-backed (grow on demand, blocksize 0x80); polyClipFinish iterates all portal_npnts edges; the only fixed-width artifact is the 32-bit planeMask (shift by 0x1e-npnts) which the D3D path ignores entirely. There is no plane budget and no fallback tier. CELL PORTAL POLYS (Q5): they live in the drawn polygon array (UnPack, 0x00533d00) and ARE emitted into the built mesh — D3DPolyRender::ConstructMesh (Ghidra 0x0059dfa0) has NO stippling or portal gate in either the counting or emission loop. Suppression is DATA + SURFACE-GATE: in the Holtburg cellar dat (a8-current-room-cellar-audit.txt / corner-cells-audit.txt, EnvCell 0xA9B40175), portal polys carry stippling NoPos and pos_surface → 0x080000DF (an untextured surface), and the draw gate skips untextured batches: DrawPolyInternal requires (type & 6) != 0 (0x0059d7c0), and DrawMesh (Ghidra 0x0059d4a0, pc:426064) skips a batch when skipNoTexture (default 1, 0x00820e30) && !(type & 6), with the bypass branch only for non-building/non-cell meshes. So cell apertures are never VISIBLY drawn; the only per-frame conditional draw of a cell portal poly is the invisible depth punch (DrawCells pass 1 for landscape portals; ConstructView/DrawPortal for building doors). Cell-to-cell apertures inside a dungeon get NO punch at all — plain z-buffer + far→near order suffices there. ## ACDREAM FRAME ENTRY. GameWindow decides per frame on clipRoot (the VIEWER cell or the synthetic outdoor node): indoor/outdoor-node frames run ONLY RetailPViewRenderer.DrawInside (GameWindow.cs:7590-7604); the global terrain/sky block runs only when clipRoot is null (GameWindow.cs:7546-7589). The outside→in look (retail DrawPortal) exists as RetailPViewRenderer.DrawPortal driven from GameWindow.cs:7750-7780 via BuildFromExterior seeding. VIEW CONSTRUCTION. PortalVisibilityBuilder.Build (PortalVisibilityBuilder.cs:63-381) is the ConstructView port: root view = full-screen NDC quad (PortalVisibilityBuilder.cs:77, 557-558), distance-priority CellTodoList (PortalVisibilityBuilder.cs:96-97), per-portal side test CameraOnInteriorSide with PortalSideEpsilon = 0.01 (PortalVisibilityBuilder.cs:38, 734-741), portal projection ProjectToClip = homogeneous transform + eye-plane-only clip at w >= 1e-4 (PortalProjection.cs:81-97), then ClipToRegion = homogeneous Sutherland-Hodgman against each active view polygon with w-multiplied edge tests (PortalProjection.cs:105-134) — a faithful polyClipFinish equivalent. Clipped regions append to the neighbour's CellView (union-as-list, AddRegion with dedup) and exit portals append to OutsideView (PortalVisibilityBuilder.cs:269-281, 334-336). Reciprocal OtherPortalClip by direct OtherPortalId index is ported (PortalVisibilityBuilder.cs:305-332, 755-764). Late view growth RE-ENQUEUES the cell, capped at MaxReprocessPerCell = 16 because ProjectToClip numerical drift otherwise never settles (PortalVisibilityBuilder.cs:40-51, 348-354); OrderedVisibleCells appends once on first pop and is never re-sorted (PortalVisibilityBuilder.cs:168-172). A clip-empty portal whose opening the eye stands inside (within 1.75 m) is RESCUED by substituting the whole current view (PortalVisibilityBuilder.cs:258-267). Per-building outdoor floods: ConstructViewBuilding == BuildFromExterior (PortalVisibilityBuilder.cs:548-554), merged by MergeBuildingFrame FIRST-WINS — a cell already present in the frame keeps its existing views and the building flood's views are dropped (RetailPViewRenderer.cs:151-160). CLIP ASSEMBLY. ClipFrameAssembler.Assemble (ClipFrameAssembler.cs:78-196) packs ONE GPU clip slot per view polygon: ClipPlaneSet.From converts a single CCW NDC polygon of 3..8 edges (after ~0.5° collinear merge) into <=8 clip-space half-planes (nx,ny,0,d) (ClipPlaneSet.cs:135-149); >8 edges → scissor AABB fallback (ClipPlaneSet.cs:130-133) which the assembler maps to SLOT 0 = PASS-ALL with the renderer-side scissor documented as unimplemented (ClipFrameAssembler.cs:13-15, 114-119); degenerate/area<1e-7 → IsNothingVisible → slice omitted (ClipPlaneSet.cs:66-68, 241-242 + ClipFrameAssembler.cs:102-103). CellIdToSlot keeps only slices[0] for single-slot consumers (ClipFrameAssembler.cs:130). DRAW. RetailPViewRenderer.DrawInside (RetailPViewRenderer.cs:44-109): build frame → (outdoor root) merge per-building floods → assemble clip slots → PrepareRenderBatches for all OrderedVisibleCells → DrawLandscapeThroughOutsideView: per outside slice, terrain UBO planes set + landscape drawn CLIPPED to the slice planes via gl_ClipDistance in terrain/sky shaders (RetailPViewRenderer.cs:214-238; sky.vert:153, terrain_modern.vert:47), then ClearDepthSlice per slice = scissored AABB depth CLEAR to far (indoor roots only; null for outdoor roots) (GameWindow.cs:7644-7652) → DrawExitPortalMasks — UNWIRED, GameWindow never sets the callback so it no-ops (RetailPViewRenderer.cs:331-332; absent from the ctx at GameWindow.cs:7604-7670) → DrawEnvCellShells far→near per IndoorDrawPlan.ShellPass (IndoorDrawPlan.cs:18-29), per view slice, with UseShellClipRouting routing the cell to its slice slot and the shell vertex shader writing gl_ClipDistance[i] = dot(planes[i], gl_Position) (mesh_modern.vert:120; EnvCellRenderer.cs:262, 1195-1230) — but GL clip distances are ENABLED only when clipShells == ctx.RootCell.IsOutdoorNode (9ce335e #114 scoping; RetailPViewRenderer.cs:96-105, 378-398): indoor roots draw shells UNCLIPPED → DrawCellObjectLists: entities drawn with membership cull only (UseIndoorMembershipOnlyRouting clears clip routing, RetailPViewRenderer.cs:439-450; the comment cites retail viewconeCheck but no per-view sphere-vs-plane test exists), and particles drawn per cell SCISSORED to the slice NDC AABB with clip distances disabled (GameWindow.cs:9553-9580). Cell-side portal polys are suppressed at mesh-extraction time by StipplingType.NoPos/NoNeg (ObjectMeshManager.cs:1385-1402 in PrepareCellStructMeshData, reached from the EnvCell path at ObjectMeshManager.cs:1343-1350) — a different criterion from retail's untextured-surface batch skip, agreeing on the audited cellar data. ## DIVERGENCES ### [CRITICAL] shell-chop-vs-depth-discipline (UNVERIFIED (verifier hit token limit)) — acdream clips cell-shell GEOMETRY to the view region; retail clips NOTHING — pixel exactness comes from aperture depth-punch + far→near order + z-buffer - blastRadius: #114 in full (chopped interior stairs, vanished candle-holder area, neighbour-room barrel visible through a chopped wall — all are under-inclusive regions amputating real geometry), the e46d3d9→124c6cb door-regression cycle, and the structural reason 9ce335e had to scope the clip back out of indoor roots (leaving indoors with NO draw-side discipline at all). This is the one-drawing-discipline invariant breaker. - retailEvidence: DrawEnvCell submits polys with planeMask=0xffffffff (pc:427922) which the shift in polyClipFinish (Ghidra 0x006b6d00: mask << (0x1e - npnts), set bit = SKIP edge) turns into skip-all; the production path is the prebuilt mesh DrawMesh(…, constructed_mesh, 1) (pc:427905) and DrawPolyInternal (Ghidra 0x0059d7c0) performs zero view clipping — a raw triangle fan to D3D. The accumulated view is consumed ONLY by: admission (ClipPortals/copy_view), object culling (viewconeCheck Ghidra 0x0054c250), and the depth punch (DrawPortalPolyInternal Ghidra 0x0059bc90: polyClipFinish-clipped aperture drawn DEPTHTEST_ALWAYS, z-write, alpha 0, z = portal depth (maxZ2=6 @0x00820e14) or far-z (maxZ1=7 @0x00820e18)). Cell geometry is always drawn whole; cells draw once (GetDrawnThisFrame guard Ghidra 0x0052c0c0) far→near (DrawCells Ghidra 0x005a4840). - acdreamEvidence: DrawEnvCellShells enables GL_CLIP_DISTANCE0..7 and hard-clips shell vertices to the per-slice region planes (RetailPViewRenderer.cs:378-398; mesh_modern.vert:120; EnvCellRenderer.cs:1195-1230) — for outdoor roots only after 9ce335e (RetailPViewRenderer.cs:96-105); indoor roots draw shells unclipped with no compensating depth discipline. The region polygons themselves drift per frame (PortalVisibilityBuilder.cs:40-51 cap rationale), so any chop is also unstable. - portShape: Remove the shell gl_ClipDistance chop as the enforcement mechanism (keep regions for admission). Port the retail discipline: draw every admitted cell's shell WHOLE, far→near per OrderedVisibleCells (already the order, IndoorDrawPlan.cs:21), and enforce aperture exactness with depth-only punches of the software-clipped portal polygons (see next divergence). gl_ClipDistance may survive only as the LANDSCAPE gate (terrain has no z-protection of its own until the punch exists). ### [CRITICAL] missing-aperture-depth-punch (UNVERIFIED (verifier hit token limit)) — The retail depth punch (DrawPortalPolyInternal) has no acdream equivalent — DrawExitPortalMasks is an unwired no-op and ClearDepthSlice clears an AABB to far instead of writing portal-plane depth on the exact clipped polygon - blastRadius: Landscape-vs-interior compositing at every aperture: far interior cells can overpaint the terrain seen through a door (no door-plane z floor), the outdoor root has NO depth discipline at building doors at all (ClearDepthSlice=null), and the AABB-shaped clear over-includes around door frames — a direct candidate mechanism for #108 (grass sweeping at the aperture edge) and a contributor to the #114 see-through-to-neighbour-rooms class. - retailEvidence: DrawCells pass 1 (Ghidra 0x005a4840): after LScape::draw, per cell per view, every other_cell_id==-1 portal is punched at its TRUE projected depth (maxZ2=6, bit0 clear → z=zw/w; Ghidra 0x0059bc90 tail) so geometry behind the door plane z-fails inside the aperture while the landscape keeps its pixels. Outside→in: ConstructView(CBldPortal) mode-1 walk punches the door to FAR-z (maxZ1=7) before the mode-2 walk draws interior cells into it, and the shell mesh draws LAST (DrawBuilding Ghidra 0x0059f2a0: Draw(part,1) then Draw(part,0); walks at pc:427993-427994). The punched polygon is the polyClipFinish-clipped aperture — pixel-exact. - acdreamEvidence: RetailPViewRenderer.DrawExitPortalMasks exists but GameWindow never supplies the callback (RetailPViewRenderer.cs:331-332; ctx construction GameWindow.cs:7604-7670 sets only DrawLandscapeSlice/ClearDepthSlice/DrawCellParticles/EmitDiagnostics). The only depth management is ClearDepthSlice: glScissor on the slice NDC AABB + Clear(DepthBufferBit) — wrong shape (AABB ⊇ polygon), wrong value (far clear ≈ retail's maxZ1 special case, never the protective portal-depth maxZ2 write), and disabled outright for outdoor roots (GameWindow.cs:7644-7652). - portShape: Wire DrawExitPortalMasks as a depth-only polygon draw: project + software-clip the aperture polygon against the slice view (ClipToRegion already does this math, PortalProjection.cs:105-134), rasterize it depth-always/z-write/color-masked at the portal's interpolated depth (indoor→out, retail maxZ2) or far-z (outside→in before interior cells, retail maxZ1), replacing ClearDepthSlice. ~80 lines of GL + the existing clipper. ### [HIGH] multiview-loss-first-wins (UNVERIFIED (verifier hit token limit)) — Multi-portal view accumulation is lossy: MergeBuildingFrame drops a building flood's views when the cell is already in the frame, and CellIdToSlot keeps only slices[0] - blastRadius: A cell visible through TWO apertures (two doors, door+window) renders/punches/scissors with only the first view — missing second-door visibility, and per-frame winner flips drive oscillation: a named suspect for #109 (far-door oscillation) and the multi-aperture cases in #114 (meeting hall). - retailEvidence: Render::copy_view APPENDS every clipped portal polygon as a new view_poly (Ghidra 0x0054dfc0: poly DArray grow + view_count++; called per portal per view from ClipPortals pc:433674 and ConstructView(CBldPortal) Ghidra 0x005a59a0). DrawCells iterates ALL views per cell for punches and object culling (Ghidra 0x005a4840 per-view loops; DrawMesh per-view viewconeCheck pc:427940-428060). AddViewToPortals propagates late growth in place via AddToCell/FixCellList/AdjustCellView (pc:433446, 433741-433745). - acdreamEvidence: MergeBuildingFrame: `if (target.CellViews.ContainsKey(cellId)) continue;` — first-wins, src views discarded (RetailPViewRenderer.cs:151-160). ClipFrameAssembler.CellIdToSlot = sliceArray[0].Slot (ClipFrameAssembler.cs:130) feeds the single-slot consumers (entity routing RetailPViewRenderer.cs:227). The per-slice shell loop itself does handle multiple slices (RetailPViewRenderer.cs:388-393), so the loss is at merge/routing, not the draw loop. - portShape: Merge = UNION the view lists (append src polygons through the existing AddRegion dedup) instead of skipping; once the shell chop is gone (divergence 1) multi-slice only matters for punches/scissors/object culling, where iterating all slices is already the code shape. ### [HIGH] eight-plane-budget-passall (UNVERIFIED (verifier hit token limit)) — The 8-plane GPU budget with an unimplemented scissor fallback (slot-0 = PASS-ALL) has no retail counterpart — retail's clip is polygon-vs-polygon software with no plane cap - blastRadius: Any view polygon with >8 edges after collinear-merge silently degrades to pass-all: terrain slices flood the whole screen through a small aperture (grey/grass artifacts at complex doorways, #108 class), and under the current shell-chop model a per-frame flip between exact-planes and pass-all is a visible strobe. Issue113MeetingHallFloodTests pins 0 such slices at the hall, but the fallback is load-bearing wherever clipped polygons accrete vertices (every reciprocal clip adds edges). - retailEvidence: polyClipFinish loops all portal_npnts view edges with DArray-backed vertex storage (Ghidra 0x006b6d00; view DArrays blocksize 0x80, acclient.h:32408-32445); the only 32-bit-mask artifact is ignored by the D3D draw path (DrawPolyInternal Ghidra 0x0059d7c0 takes no mask). copy_view's 1-pixel vertex dedup (Ghidra 0x0054dfc0) bounds vertex growth physically, not by a budget. - acdreamEvidence: ClipPlaneSet: >8 edges → Scissor AABB (ClipPlaneSet.cs:130-133); assembler maps scissor fallbacks to slot 0 with 'the renderer uses scissor for passes that need that fallback' documented but no glScissor implemented in the slice consumers (ClipFrameAssembler.cs:13-15, 114-119; no scissor in RetailPViewRenderer draw paths — only particles use BeginDoorwayScissor, GameWindow.cs:9569). - portShape: Once enforcement moves to the depth punch (CPU-rasterized clipped polygons), the 8-plane budget stops being load-bearing for shells; the landscape gate either keeps planes for the common ≤8 case with a real scissor (or stencil) fallback, or the punch protects terrain too and the budget disappears entirely. Do not extend gl_ClipDistance count. ### [HIGH] knife-edge-epsilon-and-rescue (UNVERIFIED (verifier hit token limit)) — Knife-edge handling diverges: retail uses ±0.0002 side classification (building portals reject IN_PLANE outright; cell portals degenerate naturally via the homogeneous near-W clip + 1-px dedup); acdream uses ±0.01 plus a non-retail 1.75 m eye-in-opening rescue that substitutes the ENTIRE current view - blastRadius: #114's edge-on doorway grey (degenerate slice omitted → cell admitted by rescue but landscape/region slice missing → clear color through the aperture) and admission over-inclusion when grazing a doorway; the 50x-wider epsilon plus the rescue create a band where acdream and retail disagree about which portals contribute, feeding flap-class instability at thresholds. - retailEvidence: ConstructView(CBldPortal) Ghidra 0x005a59a0: |dot| <= F_EPSILON (0.000199999995, const at 0x7e32f8) → IN_PLANE → return 0, no view, no punch. InitCell Ghidra 0x005a4b70: cell-portal in-plane falls through to inflag=0 (candidate) and the sliver dies in polyClipFinish stage-1 (w < cdstW homogeneous clip) or copy_view's |dx|<=1px && |dy|<=1px vertex merge → <3 verts → nothing appended. No rescue path exists anywhere. - acdreamEvidence: PortalSideEpsilon = 0.01 (PortalVisibilityBuilder.cs:38, used at :734-741); clip-empty + EyeInsidePortalOpening(≤1.75 m) → clippedRegion := copy of the WHOLE current view (PortalVisibilityBuilder.cs:258-267, same in BuildFromExterior :501-507 and the reciprocal-empty rescue :324-332); ClipPlaneSet's MinPolygonArea=1e-7 gate turns surviving slivers into omitted slices (ClipPlaneSet.cs:66-68, 241-242) while the cell stays admitted. - portShape: Adopt retail's constants and asymmetry: 0.0002 epsilon; building/exterior seeds reject in-plane; cell portals keep the candidate-then-degenerate path with a ~1-px screen-space vertex dedup in ClipToRegion output (the missing stabilizer that retail has and our drift cap compensates for). The rescue should shrink to what retail's geometry already implies — a portal the eye is truly inside projects near-full-screen through the homogeneous clip and needs no substitution. ### [MEDIUM] growth-requeue-vs-in-place (UNVERIFIED (verifier hit token limit)) — Late view growth: retail propagates in place (AddToCell + FixCellList/AdjustCellPlace re-sorts the draw list, AdjustCellView re-clips only new views via the update_count watermark); acdream re-enqueues with a drift cap and never re-sorts OrderedVisibleCells - blastRadius: Draw-order staleness for late-grown cells (mostly masked by z-buffer) and the MaxReprocessPerCell=16 cap silently truncating legitimate propagation in portal-dense interiors — a churn/oscillation contributor (#109) and the source of the 2026-06-07 indoor-hang workaround complex. - retailEvidence: AddViewToPortals pc:433446/0x005a52d0: first discovery → InitCell+InsCellTodoList; growth → AddToCell, and if cell_view_done, FixCellList = AdjustCellPlace (re-sort cell_draw_list, pc:433247) + AdjustCellView (ClipPortals(cell, update_count) → only NEW views re-clipped, pc:433741-433745; watermark fields 0x38/0x44 read at 0x005a5357-0x005a53b8). Termination comes from the 1-px dedup floor, not a cap. - acdreamEvidence: Re-enqueue on grew with popCounts cap (PortalVisibilityBuilder.cs:348-354) and the cap's own comment attributing it to ProjectToClip drift (PortalVisibilityBuilder.cs:40-51); processedViewCounts is a faithful update_count port (PortalVisibilityBuilder.cs:174-184); OrderedVisibleCells append-once, never adjusted (PortalVisibilityBuilder.cs:168-172). - portShape: With the dedup stabilizer (divergence 5) the cap becomes removable; the faithful shape is in-place propagation: on growth of an already-popped cell, immediately re-clip only the new view slice through its portals (recursive, like AdjustCellView) and re-position the cell in the ordered list (AdjustCellPlace) — or document why append-order + z-buffer makes re-positioning unnecessary in our GL pipeline. ### [MEDIUM] object-particle-gating (UNVERIFIED (verifier hit token limit)) — Objects/particles: retail culls per view by sphere-vs-view-edge-planes (viewconeCheck) and lets depth do the pixels; acdream culls by cell membership only and scissors particles to the slice NDC AABB - blastRadius: particles-through-walls (AABB ⊇ aperture polygon → emitters visible outside the true opening), neighbour-room objects drawn whenever their cell is admitted even when their sphere is outside every view (the barrel half of the #114 barrel-through-wall once the wall chop is fixed, the object side remains over-inclusive), minor overdraw cost. - retailEvidence: viewconeCheck Ghidra 0x0054c250: sphere vs viewer plane + each installed view_vertex.plane (stride 6 floats = Vec2D pt + Plane, acclient.h:32483-32487), OUTSIDE → skip; driven per view from RenderDeviceD3D::DrawMesh when PortalList is set (pc:427940-428060, building_view filter) and per cell from the DrawCells epilogue (PortalList = cell's view before vtbl+0x64 DrawObjCell, Ghidra 0x005a4840). No scissor, no geometry clip — depth + culling only. - acdreamEvidence: UseIndoorMembershipOnlyRouting clears all clip routing for entities and the comment explicitly defers the viewconeCheck equivalent (RetailPViewRenderer.cs:439-450); entity draw passes visibleCellIds membership only (RetailPViewRenderer.cs:460-477); particles: DisableClipDistances + BeginDoorwayScissor(slice.NdcAabb) (GameWindow.cs:9553-9580). - portShape: Port viewconeCheck: each ClipViewSlice already has the NDC edge set — lift the per-edge eye-planes (view_vertex.plane analog: the plane through the eye and the NDC edge, recoverable from the inverse view-projection) and test entity/emitter bounding spheres per slice before draw; drop the particle scissor once the punch protects apertures. ### [MEDIUM] portal-poly-suppression-criterion (UNVERIFIED (verifier hit token limit)) — Cell-side portal-poly suppression keys on the wrong property: acdream gates on Stippling NoPos/NoNeg at mesh build; retail includes them in the mesh and skips UNTEXTURED surface batches at draw (skipNoTexture + type&6) - blastRadius: Anywhere stippling and surface-texturedness disagree the two renderers diverge: a TEXTURED portal poly without NoPos (window-filling between cell and outdoors, closed-door fillings) draws in retail but our criterion may pass or drop it for the wrong reason; this is the same mechanism family as the #113 phantom staircase / door-vanish on the GfxObj side (e223325) viewed from the cell side. On the audited cellar cells the criteria agree (both suppress), so blast radius today is latent, not pinned to an open issue. - retailEvidence: ConstructMesh (Ghidra 0x0059dfa0) emits ALL polygons — no stippling/portal gate in counting or emission loops; CCellStruct::UnPack (Ghidra 0x00533d00) places portal polys inside the polygons array; the draw skip is per-surface: DrawPolyInternal requires (surface->type & 6) != 0 (Ghidra 0x0059d7c0; BASE1_IMAGE|BASE1_CLIPMAP acclient.h:5820-5824) and DrawMesh skips untextured batches when skipNoTexture (default 1 @0x00820e30) except for plain objects (Ghidra 0x0059d4a0 / pc:426064-426074). Dat evidence: cellar portal polys have stip=NoPos AND pos_surface→0x080000DF (untextured) — corner-cells-audit.txt EnvCell 0xA9B40175 polys 0x0004/0x0005. - acdreamEvidence: PrepareCellStructMeshData: hasPos/hasNeg from StipplingType.NoPos/NoNeg decide emission entirely (ObjectMeshManager.cs:1394-1402); a NoPos poly is dropped from the mesh before any surface consideration; conversely a portal poly WITHOUT NoPos would be emitted and drawn textured with no draw-time portal awareness (ObjectMeshManager.cs:1343-1350 entry). - portShape: Align the criterion with retail: emit all polys, classify batches by surface texturedness (we already read Surface.Type at ObjectMeshManager.cs:1430+), and skip untextured batches for cell/building meshes at draw — or prove from a dat sweep that NoPos ⇔ untextured-surface for all CellStructs (extend the e223325 conformance sweep to environments) and keep the cheaper build-time gate with the proof pinned. ## OPEN QUESTIONS - LScape::draw internals: whether retail clips terrain POLYGONS against outside_view in software or only culls land blocks/cells per view (PortalList is installed before LScape::draw in DrawCells pass 1, Ghidra 0x005a4840, but I did not decompile LScape::draw). The punch+order discipline works either way, but a faithful landscape pass should know which; acdream currently plane-CLIPS terrain per slice, which retail may not do at all. - PView::InitCell's second loop (Ghidra 0x005a4b70 tail): the decompile shows seen=1 set for every inflag==0 portal per view with no visible screen-bbox test — either a test was optimized out of the decompile or 'seen' is simply 'candidate'. The exact seen gate (and whether it uses the view xmin/xmax bbox installed by set_view) is unconfirmed. - Who calls PView::DrawPortal with mode 3 (punch-on-ConstructView-failure, Ghidra 0x005a5ab0)? The BSP walks pass modes 1 and 2 (pc:427993-427994); mode 3 implies a 'seal this aperture in depth even though nothing is visible through it' caller I did not locate — possibly the unloaded-interior or option-disabled path. Matters for the port's behavior when an interior cell is not streamed in. - GfxObj-side (building shell) portal-poly surfaces: the cell-side data proves the untextured-surface skip for the cellar; whether the Holtburg building models' door/window-filling polys split into textured (visible filling) vs untextured (open aperture) the way the skipNoTexture gate requires is AREA 1's question — the e223325 test dump should be extended to record each portal poly's pos_surface type bits before the holistic port relies on this gate. - Retail DrawMesh's garbled else-branch (`skipNoTexture = 1` when ObjBuildingOrBuildingPart==0 && param_4==0, both BN pc:426074 and Ghidra agree on the assignment) — semantically it looks like a latch that should be an assignment to 0 or a one-shot draw-anyway; since skipNoTexture init is already 1 (0x00820e30) the net effect (skip untextured for buildings/cells, draw for plain objects) holds, but the branch's exact intent deserves a disassembly-level check before porting it verbatim. - Spatially overlapping cells within one dungeon: retail punches ONLY landscape portals (other_cell_id==-1) and building doors — cell-to-cell apertures get no punch, so retail relies on z-buffer + non-overlapping cell volumes. Whether any shipped dungeon has same-dungeon overlapping cell volumes (which would bleed under the unclipped discipline and therefore under a faithful port too) is a dat question worth a one-off sweep before declaring the port's z-only indoor compositing sufficient. - cdstW's exact value (the homogeneous near-W threshold in polyClipFinish, Ghidra 0x006b6d00): not extracted; acdream's EyePlaneW=1e-4/MinW=0.05 (PortalProjection.cs:182-188) should be pinned to retail's constant during the port.