revert(render): BR-2 depth discipline - the gate proved #108 is MEMBERSHIP, not depth
Visual gate (2026-06-11) on the seal+punch build, then on the punch-reverted build, isolated the truth: - With the punch wired: #108 (cellar grass-sweep) gone BUT the player/NPCs go transparent by exactly their overlap with any doorway viewed from outside (the far-Z punch erases the depth of dynamic objects standing in the aperture, so the interior paints over them). - With ONLY the punch reverted (seal+full-clear kept): characters render correctly AND #108 is BACK. The punch is wired for OUTDOOR roots + the look-in path ONLY; it never runs on a clean interior (cellar) frame. For it to have suppressed #108, the cellar-transition frames must render through the OUTDOOR root -> the player is being classified OUTDOOR mid-cellar (the known #112/#106 cellar membership ping-pong). So: - #108 is a MEMBERSHIP bug (render is downstream of membership); the punch was MASKING it, harmfully. Re-attributed to the membership track. - The interior-root SEAL addresses a case that is NOT #108 (confirmed: #108 isn't an interior-root frame), so it has no verified visible effect yet. Per no-workarounds + verify-before-layering: reverted ALL of BR-2's depth machinery (seal, punch, the per-slice->full-clear swap) to the pre-BR-2 baseline (restored from6cba950). The phantom-site probe (6cba950) is kept. PortalDepthMaskRenderer.cs is KEPT as a RESERVED, unwired primitive (it is verified-correct; the depth discipline will be rebuilt during BR-3 with dynamics-after-interior ordering, where it can be verified against the shell-chop deletion). What survives from this session's execution: BR-1 (already-equivalent,695eca2) stands. #108 moves to membership. BR-2 to be re-approached under BR-3 with correct ordering. No net production behavior change vs6cba950. Suites: build green, App 226 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
4ac547f6eb
commit
88be519ec0
3 changed files with 29 additions and 96 deletions
|
|
@ -171,7 +171,6 @@ public sealed class GameWindow : IDisposable
|
||||||
// each frame on an indoor root (null on the outdoor root).
|
// each frame on an indoor root (null on the outdoor root).
|
||||||
private AcDream.App.Rendering.InteriorRenderer? _interiorRenderer;
|
private AcDream.App.Rendering.InteriorRenderer? _interiorRenderer;
|
||||||
private AcDream.App.Rendering.RetailPViewRenderer? _retailPViewRenderer;
|
private AcDream.App.Rendering.RetailPViewRenderer? _retailPViewRenderer;
|
||||||
private AcDream.App.Rendering.PortalDepthMaskRenderer? _portalDepthMask;
|
|
||||||
private AcDream.App.Rendering.InteriorEntityPartition.Result? _interiorPartition;
|
private AcDream.App.Rendering.InteriorEntityPartition.Result? _interiorPartition;
|
||||||
|
|
||||||
// Phase U.3: the shared per-frame clip data (binding=2 mesh SSBO + terrain
|
// Phase U.3: the shared per-frame clip data (binding=2 mesh SSBO + terrain
|
||||||
|
|
@ -1846,10 +1845,6 @@ public sealed class GameWindow : IDisposable
|
||||||
_clipFrame ??= ClipFrame.NoClip();
|
_clipFrame ??= ClipFrame.NoClip();
|
||||||
_retailPViewRenderer = new AcDream.App.Rendering.RetailPViewRenderer(
|
_retailPViewRenderer = new AcDream.App.Rendering.RetailPViewRenderer(
|
||||||
_gl, _clipFrame, _envCellRenderer!, _wbDrawDispatcher!);
|
_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)
|
// Phase G.1 sky renderer — its own shader (sky.vert / sky.frag)
|
||||||
|
|
@ -7637,31 +7632,24 @@ public sealed class GameWindow : IDisposable
|
||||||
renderSky,
|
renderSky,
|
||||||
kf,
|
kf,
|
||||||
environOverrideActive),
|
environOverrideActive),
|
||||||
// BR-2: retail's depth discipline between the outside stage and the
|
// The depth clear is a doorway "look-in" trick: clear depth inside a door/window
|
||||||
// interior stage (PView::DrawCells, Ghidra 0x005a4840): one FULL depth
|
// region so the cell seen THROUGH it draws over the terrain drawn through that
|
||||||
// clear (no scissor — the old per-slice AABB clear was the wrong shape),
|
// region (the indoor root looking out). For the OUTDOOR-node root the only
|
||||||
// then DrawExitPortalMasks re-stamps every outside-leading portal's TRUE
|
// OutsideView slice is the FULL-SCREEN base terrain, so clearing its depth wipes the
|
||||||
// depth so terrain seen through a doorway keeps its pixels (#108).
|
// entire depth buffer AFTER terrain/exteriors/player drew — the flooded building
|
||||||
// For the OUTDOOR-node root the only OutsideView slice is the FULL-SCREEN
|
// interiors (cellars) would then paint over everything (cellar in front of the
|
||||||
// base terrain, so a clear would wipe the entire depth buffer AFTER
|
// player; building interiors through the ground). Outdoors the interiors must
|
||||||
// terrain/exteriors/player drew — the flooded building interiors would
|
// depth-test against terrain+exteriors and appear only through real door openings,
|
||||||
// paint over everything. Outdoors the interiors must depth-test against
|
// so issue NO depth clear. Interior roots keep the doorway clear (unchanged).
|
||||||
// terrain+exteriors and appear only through real apertures (the BR-2
|
ClearDepthSlice = clipRoot.IsOutdoorNode
|
||||||
// commit-2 far-Z punch), so: NO clear, NO seals.
|
|
||||||
ClearDepthForInterior = clipRoot.IsOutdoorNode
|
|
||||||
? null
|
? null
|
||||||
: () =>
|
: slice =>
|
||||||
{
|
{
|
||||||
_gl.Disable(EnableCap.ScissorTest);
|
bool zc = BeginDoorwayScissor(true, slice.NdcAabb);
|
||||||
_gl.DepthMask(true); // depth clears honor glDepthMask (c4df241 lesson)
|
|
||||||
_gl.Clear(ClearBufferMask.DepthBufferBit);
|
_gl.Clear(ClearBufferMask.DepthBufferBit);
|
||||||
|
if (zc)
|
||||||
|
_gl.Disable(EnableCap.ScissorTest);
|
||||||
},
|
},
|
||||||
// BR-2: interior roots SEAL exit doors at true depth (#108);
|
|
||||||
// outdoor roots PUNCH building entry apertures to far-Z so
|
|
||||||
// flooded interiors show through doorways from outside.
|
|
||||||
DrawExitPortalMasks = sliceCtx =>
|
|
||||||
DrawRetailPViewPortalDepthWrite(sliceCtx, envCellViewProj,
|
|
||||||
forceFarZ: clipRoot.IsOutdoorNode),
|
|
||||||
DrawCellParticles = sliceCtx =>
|
DrawCellParticles = sliceCtx =>
|
||||||
DrawRetailPViewCellParticles(sliceCtx, camera, camPos),
|
DrawRetailPViewCellParticles(sliceCtx, camera, camPos),
|
||||||
EmitDiagnostics = result =>
|
EmitDiagnostics = result =>
|
||||||
|
|
@ -7807,11 +7795,6 @@ public sealed class GameWindow : IDisposable
|
||||||
MaxSeedDistance = 48f,
|
MaxSeedDistance = 48f,
|
||||||
LandblockEntries = _worldState.LandblockEntries,
|
LandblockEntries = _worldState.LandblockEntries,
|
||||||
SetTerrainClipUbo = uboId => _terrain?.SetClipUbo(uboId),
|
SetTerrainClipUbo = uboId => _terrain?.SetClipUbo(uboId),
|
||||||
// BR-2: outside-looking-in — PUNCH building entry apertures
|
|
||||||
// to far-Z so the flooded interior shows through the doorway.
|
|
||||||
DrawExitPortalMasks = sliceCtx =>
|
|
||||||
DrawRetailPViewPortalDepthWrite(sliceCtx, envCellViewProj,
|
|
||||||
forceFarZ: true),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (portalResult is not null)
|
if (portalResult is not null)
|
||||||
|
|
@ -9567,54 +9550,6 @@ public sealed class GameWindow : IDisposable
|
||||||
DisableClipDistances();
|
DisableClipDistances();
|
||||||
}
|
}
|
||||||
|
|
||||||
// BR-2: retail's invisible portal depth writes on every outside-leading
|
|
||||||
// portal (other_cell_id==0xFFFF) of this cell, clipped to the slice's view
|
|
||||||
// region — D3DPolyRender::DrawPortalPolyInternal (Ghidra 0x0059bc90),
|
|
||||||
// dispatched by PView::DrawCells (Ghidra 0x005a4840). The forceFarZ flag is
|
|
||||||
// retail's maxZ1(true)/maxZ2(false) selector:
|
|
||||||
//
|
|
||||||
// • INTERIOR root (forceFarZ=false → SEAL, true depth): after the full
|
|
||||||
// depth clear, stamp the door plane so interior geometry beyond the door
|
|
||||||
// z-fails inside the aperture and the terrain drawn through the outside
|
|
||||||
// view keeps its pixels (#108).
|
|
||||||
// • OUTDOOR root / look-in (forceFarZ=true → PUNCH, far depth): after the
|
|
||||||
// landscape + shell drew, erase the terrain depth inside the building's
|
|
||||||
// entry aperture so the flooded interior shows THROUGH the doorway
|
|
||||||
// against the nearer front-ground. Our pipeline draws the shell FIRST
|
|
||||||
// (as an outdoor entity in the landscape pass), so — unlike retail's
|
|
||||||
// shell-LAST order — we get the outside-the-aperture wall occlusion for
|
|
||||||
// free and need only the punch for in-aperture visibility (no reorder).
|
|
||||||
//
|
|
||||||
// Wiring only — the draw lives in PortalDepthMaskRenderer.
|
|
||||||
private void DrawRetailPViewPortalDepthWrite(
|
|
||||||
AcDream.App.Rendering.RetailPViewCellSliceContext sliceCtx,
|
|
||||||
System.Numerics.Matrix4x4 viewProjection,
|
|
||||||
bool forceFarZ)
|
|
||||||
{
|
|
||||||
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; // depth writes 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawRetailPViewCellParticles(
|
private void DrawRetailPViewCellParticles(
|
||||||
AcDream.App.Rendering.RetailPViewCellSliceContext sliceCtx,
|
AcDream.App.Rendering.RetailPViewCellSliceContext sliceCtx,
|
||||||
ICamera camera,
|
ICamera camera,
|
||||||
|
|
@ -11838,7 +11773,6 @@ public sealed class GameWindow : IDisposable
|
||||||
_audioEngine?.Dispose(); // Phase E.2: stop all voices, close AL context
|
_audioEngine?.Dispose(); // Phase E.2: stop all voices, close AL context
|
||||||
_wbDrawDispatcher?.Dispose();
|
_wbDrawDispatcher?.Dispose();
|
||||||
_envCellRenderer?.Dispose(); // Phase A8
|
_envCellRenderer?.Dispose(); // Phase A8
|
||||||
_portalDepthMask?.Dispose(); // BR-2
|
|
||||||
_clipFrame?.Dispose(); // Phase U.3
|
_clipFrame?.Dispose(); // Phase U.3
|
||||||
_skyRenderer?.Dispose(); // depends on sampler cache; dispose first
|
_skyRenderer?.Dispose(); // depends on sampler cache; dispose first
|
||||||
_samplerCache?.Dispose();
|
_samplerCache?.Dispose();
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,18 @@ namespace AcDream.App.Rendering;
|
||||||
/// writes — the port of <c>D3DPolyRender::DrawPortalPolyInternal</c>
|
/// writes — the port of <c>D3DPolyRender::DrawPortalPolyInternal</c>
|
||||||
/// (Ghidra 0x0059bc90, pc:424490).
|
/// (Ghidra 0x0059bc90, pc:424490).
|
||||||
///
|
///
|
||||||
|
/// <para><b>⚠ RESERVED — NOT wired into the frame as of 2026-06-11.</b> The
|
||||||
|
/// first BR-2 attempt wired this as a seal (interior root) + punch (outdoor /
|
||||||
|
/// look-in) and was reverted at the visual gate: the outdoor far-Z punch
|
||||||
|
/// erased the depth of DYNAMIC objects (player / NPCs) standing in a door
|
||||||
|
/// aperture, so the interior painted over them. The gate also proved #108
|
||||||
|
/// (cellar grass-sweep) is a MEMBERSHIP bug, not a depth bug — the punch was
|
||||||
|
/// only masking it on outdoor-classified cellar frames. The correct depth
|
||||||
|
/// discipline (punch → interior → dynamics-last ordering) will be rebuilt
|
||||||
|
/// during BR-3 when it can be verified against the shell-chop deletion. This
|
||||||
|
/// class is the verified-correct depth-write primitive kept for that work; it
|
||||||
|
/// has no callers today.</para>
|
||||||
|
///
|
||||||
/// <para>Retail projects a portal polygon, software-clips it against the
|
/// <para>Retail projects a portal polygon, software-clips it against the
|
||||||
/// installed portal view (<c>polyClipFinish</c>), and draws the survivor as a
|
/// installed portal view (<c>polyClipFinish</c>), and draws the survivor as a
|
||||||
/// COLOR-INVISIBLE triangle fan with depth-test ALWAYS + depth-write ON:</para>
|
/// COLOR-INVISIBLE triangle fan with depth-test ALWAYS + depth-write ON:</para>
|
||||||
|
|
|
||||||
|
|
@ -231,16 +231,8 @@ public sealed class RetailPViewRenderer
|
||||||
ctx.DrawLandscapeSlice(new RetailPViewLandscapeSliceContext(slice, partition.Outdoor));
|
ctx.DrawLandscapeSlice(new RetailPViewLandscapeSliceContext(slice, partition.Outdoor));
|
||||||
}
|
}
|
||||||
|
|
||||||
// BR-2: retail clears the FULL depth buffer ONCE between the outside
|
foreach (var slice in clipAssembly.OutsideViewSlices)
|
||||||
// stage and the interior stage (PView::DrawCells, Ghidra 0x005a4840 —
|
ctx.ClearDepthSlice?.Invoke(slice);
|
||||||
// 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();
|
UseIndoorMembershipOnlyRouting();
|
||||||
}
|
}
|
||||||
|
|
@ -583,12 +575,7 @@ public sealed class RetailPViewDrawContext : IRetailPViewCellDrawContext
|
||||||
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries { get; init; }
|
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries { get; init; }
|
||||||
public required Action<uint> SetTerrainClipUbo { get; init; }
|
public required Action<uint> SetTerrainClipUbo { get; init; }
|
||||||
public required Action<RetailPViewLandscapeSliceContext> DrawLandscapeSlice { 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>? DrawExitPortalMasks { get; init; }
|
||||||
public Action<RetailPViewCellSliceContext>? DrawCellParticles { get; init; }
|
public Action<RetailPViewCellSliceContext>? DrawCellParticles { get; init; }
|
||||||
public Action<RetailPViewFrameResult>? EmitDiagnostics { get; init; }
|
public Action<RetailPViewFrameResult>? EmitDiagnostics { get; init; }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue