fix(render): #113 - enable GL clip distances for the PView shell pass (phantom exterior staircase)

Attribution (dat-evidenced, supersedes the misplaced-cell hypothesis):
the phantom staircase is the Holtburg MEETING HALL (AAB3 building[0],
model 0x010014C3 at AAB3-local (36,84,116)), NOT an A9B3 building - the
user stood at the A9B3/AAB3 boundary (cell-transit trail in
issue112-gate1.log) and clicked through the hall to the NPC behind it.
The hall's interior stair cells (0x100..0x106, ring climbing z 116->124.5
to the deck hatch) have geometry coincident with the shell's west wall
(both at local x=29.0). Our outdoor per-building flood admits them with
CORRECT tight clip regions (4-6 planes, door-aperture NDC boxes -
Issue113MeetingHallFloodTests proves it), but DrawEnvCellShells drew them
WHOLE: mesh_modern.vert writes gl_ClipDistance from the routed CellClip
slot, and gl_ClipDistance is ignored unless GL_CLIP_DISTANCEi is enabled -
which no caller ever did for the shell pass (born inert in 1405dd8).
Interior staircase painted across the exterior wall; unpickable because
it is cell geometry, not an entity.

Retail oracle: cell geometry IS clipped to the accumulated portal view -
Render::set_view (:343750) installs the view polygon edge planes,
DrawEnvCell submits every cell polygon with planeMask=0xffffffff (:427922)
through ACRender::polyClipFinish. Characters/meshes are NOT poly-clipped
(viewconeCheck path) - entity routing stays cleared, comment scoped.

Fix: enable GL_CLIP_DISTANCE0..7 around exactly the shell pass
(self-contained per feedback_render_self_contained_gl_state; no early-outs
between set and restore). Slot-0 fallback slices (>8-plane regions) still
draw pass-all - the assembler's scissor fallback remains unimplemented and
documented; the new flood test pins 0 such slices at the hall.

Refuted along the way (full evidence in Issue113PhantomStairsDumpTests):
- ONE misplaced interior EnvCell unifying #113+#112+collision gaps: all 17
  A9B3 cottage cells share an identical dat Position (nothing to misplace);
  the #112 gap is a real 20cm doorway micro-gap 0.23m outside threshold
  cell 0x104 (straddles its exterior portal plane at foot radius 0.48);
  missing object collision remains #99/A6.P4.
- A9B3 dat content near the spot: no stair geometry in shell (balcony at
  z119 + turret roof only), cells (flat 116/118.8), statics, or stabs.

Tests: Core 1389 green (+6 dump facts) / App 224 (+1 flood replay) /
UI 420 / Net 294; pre-existing 4 #99-era failures unchanged.
Visual gate pending: user re-check of the hall west face vs retail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-10 16:26:55 +02:00
parent 6d2218cac3
commit 927fd8fde2
3 changed files with 1085 additions and 4 deletions

View file

@ -341,6 +341,26 @@ public sealed class RetailPViewRenderer
// (far→near), per portal_view slice. No drawableCells filter — a cell without a
// clip-slot falls through GetCellSlicesOrNoClip to NoClipSlice and draws unclipped
// (sealed; per-slice trim returns in Task 4).
//
// #113 (2026-06-10): the per-slice clip MUST actually clip. Retail clips drawn
// CELL geometry to the accumulated portal view — Render::set_view (:343750)
// installs the view polygon's edge planes and DrawEnvCell submits every cell
// polygon with planeMask=0xffffffff (:427922) through ACRender::polyClipFinish.
// Our equivalent (UseShellClipRouting → mesh_modern.vert gl_ClipDistance) was
// routed but INERT: gl_ClipDistance writes are ignored unless GL_CLIP_DISTANCEi
// is enabled, and no caller enabled it for this pass — so flooded interior cells
// drew WHOLE, painting interior geometry across exterior walls (the Holtburg
// meeting-hall phantom staircase, AAB3 0x100 stair cell coincident with the
// shell's west wall). Self-contained per feedback_render_self_contained_gl_state;
// no early-outs between enable and disable. Slot-0 slices (SSBO count=0) still
// pass-all — the assembler's >8-plane scissor fallback remains unimplemented
// (rare; Issue113MeetingHallFloodTests pins 0 such slices at the hall).
// Characters/statics stay unclipped (DrawCellObjectLists): retail's mesh path is
// viewcone-check + BoundingType handling, and hard-clipping slices characters at
// doorways (the original UseIndoorMembershipOnlyRouting observation).
for (int i = 0; i < ClipFrame.MaxPlanes; i++)
_gl.Enable(Silk.NET.OpenGL.EnableCap.ClipDistance0 + i);
foreach (var entry in IndoorDrawPlan.ShellPass(pvFrame))
{
uint cellId = entry.CellId;
@ -354,6 +374,9 @@ public sealed class RetailPViewRenderer
_envCells.Render(WbRenderPass.Transparent, _oneCell);
}
}
for (int i = 0; i < ClipFrame.MaxPlanes; i++)
_gl.Disable(Silk.NET.OpenGL.EnableCap.ClipDistance0 + i);
}
private void DrawCellObjectLists(
@ -396,10 +419,13 @@ public sealed class RetailPViewRenderer
private void UseIndoorMembershipOnlyRouting()
{
// Retail's PView portal views decide which cells/objects are eligible,
// but DrawMesh only performs portal-view visibility checks before drawing
// the mesh. Feeding those 2D views into gl_ClipDistance slices characters
// and cell shells at stair/door boundaries, which retail does not do.
// For MESHES (characters, statics) retail's DrawMesh performs portal-view
// visibility checks (Render::viewconeCheck on the drawing sphere) rather
// than hard per-poly clipping — feeding the 2D views into gl_ClipDistance
// slices characters at stair/door boundaries, which retail does not do.
// CELL SHELL geometry is different: retail clips it to the portal view
// (planeMask=0xffffffff per cell polygon, decomp :427922 + :343750) —
// DrawEnvCellShells enables exactly that (#113).
_envCells.SetClipRouting(null);
_entities.ClearClipRouting();
}