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:
parent
6cba95047c
commit
6d4cac2418
3 changed files with 279 additions and 17 deletions
|
|
@ -171,6 +171,7 @@ public sealed class GameWindow : IDisposable
|
|||
// each frame on an indoor root (null on the outdoor root).
|
||||
private AcDream.App.Rendering.InteriorRenderer? _interiorRenderer;
|
||||
private AcDream.App.Rendering.RetailPViewRenderer? _retailPViewRenderer;
|
||||
private AcDream.App.Rendering.PortalDepthMaskRenderer? _portalDepthMask;
|
||||
private AcDream.App.Rendering.InteriorEntityPartition.Result? _interiorPartition;
|
||||
|
||||
// Phase U.3: the shared per-frame clip data (binding=2 mesh SSBO + terrain
|
||||
|
|
@ -1845,6 +1846,10 @@ public sealed class GameWindow : IDisposable
|
|||
_clipFrame ??= ClipFrame.NoClip();
|
||||
_retailPViewRenderer = new AcDream.App.Rendering.RetailPViewRenderer(
|
||||
_gl, _clipFrame, _envCellRenderer!, _wbDrawDispatcher!);
|
||||
|
||||
// BR-2: invisible portal depth writes (seal/punch) — retail
|
||||
// DrawPortalPolyInternal (Ghidra 0x0059bc90).
|
||||
_portalDepthMask = new AcDream.App.Rendering.PortalDepthMaskRenderer(_gl);
|
||||
}
|
||||
|
||||
// Phase G.1 sky renderer — its own shader (sky.vert / sky.frag)
|
||||
|
|
@ -7632,24 +7637,28 @@ public sealed class GameWindow : IDisposable
|
|||
renderSky,
|
||||
kf,
|
||||
environOverrideActive),
|
||||
// The depth clear is a doorway "look-in" trick: clear depth inside a door/window
|
||||
// region so the cell seen THROUGH it draws over the terrain drawn through that
|
||||
// region (the indoor root looking out). For the OUTDOOR-node root the only
|
||||
// OutsideView slice is the FULL-SCREEN base terrain, so clearing its depth wipes the
|
||||
// entire depth buffer AFTER terrain/exteriors/player drew — the flooded building
|
||||
// interiors (cellars) would then paint over everything (cellar in front of the
|
||||
// player; building interiors through the ground). Outdoors the interiors must
|
||||
// depth-test against terrain+exteriors and appear only through real door openings,
|
||||
// so issue NO depth clear. Interior roots keep the doorway clear (unchanged).
|
||||
ClearDepthSlice = clipRoot.IsOutdoorNode
|
||||
// BR-2: retail's depth discipline between the outside stage and the
|
||||
// interior stage (PView::DrawCells, Ghidra 0x005a4840): one FULL depth
|
||||
// clear (no scissor — the old per-slice AABB clear was the wrong shape),
|
||||
// then DrawExitPortalMasks re-stamps every outside-leading portal's TRUE
|
||||
// depth so terrain seen through a doorway keeps its pixels (#108).
|
||||
// For the OUTDOOR-node root the only OutsideView slice is the FULL-SCREEN
|
||||
// base terrain, so a clear would wipe the entire depth buffer AFTER
|
||||
// terrain/exteriors/player drew — the flooded building interiors would
|
||||
// paint over everything. Outdoors the interiors must depth-test against
|
||||
// terrain+exteriors and appear only through real apertures (the BR-2
|
||||
// commit-2 far-Z punch), so: NO clear, NO seals.
|
||||
ClearDepthForInterior = clipRoot.IsOutdoorNode
|
||||
? null
|
||||
: slice =>
|
||||
: () =>
|
||||
{
|
||||
bool zc = BeginDoorwayScissor(true, slice.NdcAabb);
|
||||
_gl.Disable(EnableCap.ScissorTest);
|
||||
_gl.DepthMask(true); // depth clears honor glDepthMask (c4df241 lesson)
|
||||
_gl.Clear(ClearBufferMask.DepthBufferBit);
|
||||
if (zc)
|
||||
_gl.Disable(EnableCap.ScissorTest);
|
||||
},
|
||||
DrawExitPortalMasks = clipRoot.IsOutdoorNode
|
||||
? null
|
||||
: sliceCtx => DrawRetailPViewExitPortalSeal(sliceCtx, envCellViewProj),
|
||||
DrawCellParticles = sliceCtx =>
|
||||
DrawRetailPViewCellParticles(sliceCtx, camera, camPos),
|
||||
EmitDiagnostics = result =>
|
||||
|
|
@ -9550,6 +9559,43 @@ public sealed class GameWindow : IDisposable
|
|||
DisableClipDistances();
|
||||
}
|
||||
|
||||
// BR-2 seal: re-stamp the TRUE depth of every outside-leading portal of this
|
||||
// cell, clipped to the slice's view region — retail PView::DrawCells loop 1
|
||||
// (Ghidra 0x005a4840, pc:432783-432786): after the landscape draws through
|
||||
// the outside views and the depth buffer is cleared, every portal with
|
||||
// other_cell_id==0xFFFF gets DrawPortalPolyInternal(poly, false) — an
|
||||
// invisible depth write at the portal plane, so interior geometry FARTHER
|
||||
// than the doorway z-fails inside the aperture and the terrain seen through
|
||||
// it keeps its pixels (#108). Wiring only — the draw lives in
|
||||
// PortalDepthMaskRenderer.
|
||||
private void DrawRetailPViewExitPortalSeal(
|
||||
AcDream.App.Rendering.RetailPViewCellSliceContext sliceCtx,
|
||||
System.Numerics.Matrix4x4 viewProjection)
|
||||
{
|
||||
if (_portalDepthMask is null)
|
||||
return;
|
||||
if (!_cellVisibility.TryGetCell(sliceCtx.CellId, out var cell) || cell is null)
|
||||
return;
|
||||
|
||||
Span<System.Numerics.Vector3> world = stackalloc System.Numerics.Vector3[32];
|
||||
for (int i = 0; i < cell.Portals.Count; i++)
|
||||
{
|
||||
if (cell.Portals[i].OtherCellId != 0xFFFF)
|
||||
continue; // seals apply to portals leading OUTSIDE only
|
||||
if (i >= cell.PortalPolygons.Count)
|
||||
break;
|
||||
var localVerts = cell.PortalPolygons[i];
|
||||
if (localVerts.Length < 3)
|
||||
continue;
|
||||
|
||||
int n = System.Math.Min(localVerts.Length, world.Length);
|
||||
for (int v = 0; v < n; v++)
|
||||
world[v] = System.Numerics.Vector3.Transform(localVerts[v], cell.WorldTransform);
|
||||
|
||||
_portalDepthMask.DrawDepthFan(world[..n], viewProjection, sliceCtx.Slice.Planes, forceFarZ: false);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawRetailPViewCellParticles(
|
||||
AcDream.App.Rendering.RetailPViewCellSliceContext sliceCtx,
|
||||
ICamera camera,
|
||||
|
|
@ -11773,6 +11819,7 @@ public sealed class GameWindow : IDisposable
|
|||
_audioEngine?.Dispose(); // Phase E.2: stop all voices, close AL context
|
||||
_wbDrawDispatcher?.Dispose();
|
||||
_envCellRenderer?.Dispose(); // Phase A8
|
||||
_portalDepthMask?.Dispose(); // BR-2
|
||||
_clipFrame?.Dispose(); // Phase U.3
|
||||
_skyRenderer?.Dispose(); // depends on sampler cache; dispose first
|
||||
_samplerCache?.Dispose();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue