BR-2 commit 1: exit-portal depth SEALS + retail full depth clear (the #108 machinery)

Ports the seal half of retail's invisible portal depth writes
(D3DPolyRender::DrawPortalPolyInternal, Ghidra 0x0059bc90; dispatched by
PView::DrawCells loop 1, Ghidra 0x005a4840 pc:432783-432786):

- NEW PortalDepthMaskRenderer: draws a portal polygon as a color-masked
  triangle fan, depth-test ALWAYS + depth-write ON, at the polygon's TRUE
  projected depth (retail maxZ2 seal) or forced to far-z 0.99999988
  (retail maxZ1 punch - the constant from 0x0059bc90's tail; punch wiring
  lands in BR-2 commit 2). Where retail software-clips the fan against
  the installed view (polyClipFinish), we apply the SAME slice region via
  gl_ClipDistance from the slice's <=8 clip-space half-planes. GL state
  fully self-contained (set -> draw -> restore, no early-outs).

- DrawExitPortalMasks is now WIRED in production (was a null-callback
  no-op since birth): for interior roots, every visible cell's portals
  with OtherCellId==0xFFFF get their world-space polygon sealed per view
  slice, far-to-near, after the landscape slices.

- ClearDepthSlice (per-slice scissored AABB clear - wrong shape, wrong
  scope, no seal after it) is REPLACED by ClearDepthForInterior: ONE
  full-buffer depth clear between the outside stage and the interior
  stage, gated on any outside slice having drawn (retail's
  portalsDrawnCount gate semantics staged as an open question, marked
  inline). DepthMask(true) asserted at the clear site (c4df241 lesson).
  Outdoor roots: no clear, no seals (interiors must depth-test against
  terrain until the commit-2 punch).

Closes the mechanism behind #108 (outdoor grass sweeping across the
upstairs door opening - terrain depth seen through the doorway is now
re-stamped at the door plane so farther interior geometry z-fails inside
the aperture). Visual gate: BR-2/BR-3 batched checklist (cellar doorway
+ cottage wall + tower stairs near/far).

Suites: build green, App 226 green, Core 1398 + 4 pre-existing #99-era
failures + 1 skip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-11 08:03:10 +02:00
parent 6cba95047c
commit 6d4cac2418
3 changed files with 279 additions and 17 deletions

View file

@ -231,8 +231,16 @@ public sealed class RetailPViewRenderer
ctx.DrawLandscapeSlice(new RetailPViewLandscapeSliceContext(slice, partition.Outdoor));
}
foreach (var slice in clipAssembly.OutsideViewSlices)
ctx.ClearDepthSlice?.Invoke(slice);
// BR-2: retail clears the FULL depth buffer ONCE between the outside
// stage and the interior stage (PView::DrawCells, Ghidra 0x005a4840 —
// Clear gated on portalsDrawnCount; the exact gate semantics is a plan
// open question, staged here as "any outside slice drawn"), then
// re-stamps every outside-leading portal's TRUE depth (the seals,
// DrawExitPortalMasks below). The old per-slice scissored AABB clear
// was the wrong shape (AABB ⊇ aperture polygon) and had no seal after
// it — the #108 mechanism.
if (clipAssembly.OutsideViewSlices.Length > 0)
ctx.ClearDepthForInterior?.Invoke();
UseIndoorMembershipOnlyRouting();
}
@ -575,7 +583,12 @@ public sealed class RetailPViewDrawContext : IRetailPViewCellDrawContext
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries { get; init; }
public required Action<uint> SetTerrainClipUbo { get; init; }
public required Action<RetailPViewLandscapeSliceContext> DrawLandscapeSlice { get; init; }
public Action<ClipViewSlice>? ClearDepthSlice { get; init; }
/// <summary>BR-2: one full-buffer depth clear between the outside stage and the
/// interior stage (retail PView::DrawCells, Ghidra 0x005a4840). Null for outdoor
/// roots — outdoors the interiors must depth-test against terrain + exteriors and
/// appear only through real apertures (the BR-2 commit-2 punch).</summary>
public Action? ClearDepthForInterior { get; init; }
public Action<RetailPViewCellSliceContext>? DrawExitPortalMasks { get; init; }
public Action<RetailPViewCellSliceContext>? DrawCellParticles { get; init; }
public Action<RetailPViewFrameResult>? EmitDiagnostics { get; init; }