diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 1aa4f592..324c7fe6 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -171,7 +171,6 @@ 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 @@ -1846,10 +1845,6 @@ 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) @@ -7637,31 +7632,24 @@ public sealed class GameWindow : IDisposable renderSky, kf, environOverrideActive), - // 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 + // 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 ? null - : () => + : slice => { - _gl.Disable(EnableCap.ScissorTest); - _gl.DepthMask(true); // depth clears honor glDepthMask (c4df241 lesson) + bool zc = BeginDoorwayScissor(true, slice.NdcAabb); _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 => DrawRetailPViewCellParticles(sliceCtx, camera, camPos), EmitDiagnostics = result => @@ -7807,11 +7795,6 @@ public sealed class GameWindow : IDisposable MaxSeedDistance = 48f, LandblockEntries = _worldState.LandblockEntries, 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) @@ -9567,54 +9550,6 @@ public sealed class GameWindow : IDisposable 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 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( AcDream.App.Rendering.RetailPViewCellSliceContext sliceCtx, ICamera camera, @@ -11838,7 +11773,6 @@ 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(); diff --git a/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs b/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs index 088f2b7a..809d6211 100644 --- a/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs +++ b/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs @@ -9,6 +9,18 @@ namespace AcDream.App.Rendering; /// writes — the port of D3DPolyRender::DrawPortalPolyInternal /// (Ghidra 0x0059bc90, pc:424490). /// +/// ⚠ RESERVED — NOT wired into the frame as of 2026-06-11. 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. +/// /// Retail projects a portal polygon, software-clips it against the /// installed portal view (polyClipFinish), and draws the survivor as a /// COLOR-INVISIBLE triangle fan with depth-test ALWAYS + depth-write ON: diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs index a7fb7bae..be309602 100644 --- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs +++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs @@ -231,16 +231,8 @@ public sealed class RetailPViewRenderer ctx.DrawLandscapeSlice(new RetailPViewLandscapeSliceContext(slice, partition.Outdoor)); } - // 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(); + foreach (var slice in clipAssembly.OutsideViewSlices) + ctx.ClearDepthSlice?.Invoke(slice); UseIndoorMembershipOnlyRouting(); } @@ -583,12 +575,7 @@ public sealed class RetailPViewDrawContext : IRetailPViewCellDrawContext IReadOnlyDictionary? AnimatedById)> LandblockEntries { get; init; } public required Action SetTerrainClipUbo { get; init; } public required Action DrawLandscapeSlice { get; init; } - - /// 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). - public Action? ClearDepthForInterior { get; init; } + public Action? ClearDepthSlice { get; init; } public Action? DrawExitPortalMasks { get; init; } public Action? DrawCellParticles { get; init; } public Action? EmitDiagnostics { get; init; }