using System; using System.Numerics; namespace AcDream.App.Rendering; /// /// BR-2 (holistic building-render port): retail's invisible portal depth /// writes — the port of D3DPolyRender::DrawPortalPolyInternal /// (Ghidra 0x0059bc90, pc:424490). /// /// Wired by T1 (BR-3, `579c8b0`): seal on interior roots, punch /// on outdoor / look-in roots, via RetailPViewPassExecutor.DrawPortalDepthWrite /// (the DrawExitPortalMasks slice callback) — safe alongside the /// dynamics-drawn-LAST frame order (the first BR-2 attempt punched after /// dynamics and erased the player; reverted 88be519). #117 (2026-06-11) /// added the two-pass stencil depth gate on the punch side — see /// . /// /// 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: /// /// Seal (retail maxZ2=6, bit0 clear, data 0x00820e14): /// z = the polygon's true projected depth. Drawn on portals leading OUTSIDE /// (other_cell_id==0xFFFF) after the landscape pass — terrain seen /// through a doorway keeps its pixels because farther interior geometry /// z-fails inside the aperture (PView::DrawCells loop 1, Ghidra 0x005a4840, /// pc:432783-432786). /// Punch (retail maxZ1=7, bit0 set, data 0x00820e18): /// z forced to the far plane (0.99999988) — erases depth inside a building /// aperture so the interior cells drawn next land cleanly /// (ConstructView(CBldPortal) mode-1, pc:433827). BR-2 commit 2 wires this /// side. /// /// /// Where retail clips the polygon on the CPU against the view, we apply /// the SAME view region via gl_ClipDistance from the slice's clip-space /// half-planes (≤8, the validated output) — the /// depth write lands only inside the slice region, matching retail's clipped /// fan. /// /// The GL arm's inline program (rehomed clip planes, raw uniform /// locations) and this class's own dynamic VAO/VBO ring were deleted at /// Campaign V slice V11; the RHI arm compiles /// Rendering/Shaders/portal_depth.{vert,frag} from committed SPIR-V, /// and its per-frame fan vertices come from the frame ring instead. /// public sealed partial class PortalDepthMaskRenderer : IDisposable { private readonly ResourceCleanupGroup _resources; /// Shared by both arms: the largest fan accepts. private const int MaxFanVerts = 32; /// /// The GL arm's per-flight VAO/VBO ring this used to report on was deleted /// at Campaign V slice V11: the RHI arm draws every fan from a ring /// allocation that lives until its frame retires, so there is no /// persistent dynamic-buffer pool left to size. /// internal long DynamicBufferCapacityBytes => 0; /// /// Selects the GPU-fenced frame slot and resets its append cursor. Portal /// fans written during one frame occupy distinct ranges, so later portal /// slices never overwrite vertices still referenced by earlier draws. /// /// The GL arm's per-flight VAO/VBO ring this used to activate was /// deleted at Campaign V slice V11 — the RHI arm owns no per-flight ring /// at all, because every frame ring allocation is already distinct memory /// that lives until the frame retires — so all this edge carries now is /// the started latch. /// public void BeginFrame(int frameSlot) { ArgumentOutOfRangeException.ThrowIfNegative(frameSlot); _rhiFrameStarted = true; } /// /// #117 (2026-06-11): the mark-pass depth bias, in NDC, toward the /// viewer. Retail's punch is DEPTHTEST_ALWAYS and is safe only because /// retail's outdoor pass is painter's-ordered far→near (anything nearer /// redraws AFTER the punch and re-covers it). Our z-buffered MDI frame /// has no such order, so an unconditional far-Z punch erased the depth /// of NEARER occluders (terrain hills, closer buildings) at aperture /// pixels — doors/interiors painted through them (the T5 #117 report). /// The z-buffer-correct equivalent: punch ONLY where the aperture /// polygon itself wins a depth test at its true depth (two-pass /// stencil below). The bias keeps the #108 case covered — terrain /// hugging the door plane (centimeters in front of the aperture) must /// still be punched; a hill or another house meters nearer must not. /// private const float PunchMarkDepthBias = 0.0005f; /// /// #129 (2026-06-12): NDC depth is non-linear — a constant NDC bias b /// spans ≈ b·d²/near meters of eye depth at eye distance d. With /// znear = 0.1, the 0.0005 constant alone spanned 0.125 m at 5 m but /// ~190 m at a landblock away: every hill/house in front of a distant /// aperture passed the mark and got far-Z punched — door-shaped leaks /// through occluders. Fix: cap the bias's EYE-SPACE span at /// . Below the ~10 m crossover /// (sqrt(cap·near/0.0005)) the constant-NDC term is smaller and wins — /// bit-identical to the T5-validated close-range behavior (#108 grass /// coverage untouched); beyond it the punch can never reach an occluder /// more than the cap in front of the aperture plane. /// public const float PunchMarkBiasEyeCapMeters = 0.5f; /// Retail Render::znear = 0.1 (decomp :342173, re-landed /// d4b5c71). The cap conversion below assumes the production camera near /// plane; the small f/(f−n) factor (~1.00002 at far 5000) is ignored. public const float CameraNearPlaneMeters = 0.1f; /// CPU mirror of the vertex-shader mark-bias expression (keep in /// sync with VertSrc): the NDC bias applied at eye depth /// . public static float MarkBiasNdc(float eyeDepthMeters) => MathF.Min(PunchMarkDepthBias, PunchMarkBiasEyeCapMeters * CameraNearPlaneMeters / MathF.Max(eyeDepthMeters * eyeDepthMeters, 1e-6f)); /// /// Draw one portal polygon as an invisible depth write, clipped to the /// slice's clip-space half-planes. selects /// punch (true, retail maxZ1) vs seal (false, retail maxZ2 true depth). /// /// Seal (interior root): one pass, retail-verbatim — /// depth ALWAYS + true projected depth. It runs immediately after the /// gated full depth clear, so there is no nearer content to stomp. /// /// Punch (outdoor root / look-in): two passes (#117). /// Pass A marks stencil where the aperture fan passes a LEQUAL depth /// test at its (biased) true depth — i.e. where the aperture is /// actually visible against everything drawn so far. Pass B writes the /// far-Z punch with depth ALWAYS but stencil-gated to the marked /// pixels, and zeroes the stencil as it goes (self-cleaning). This is /// the z-buffered equivalent of retail's painter's-order safety. /// public void DrawDepthFan( ReadOnlySpan worldVerts, in Matrix4x4 viewProjection, ReadOnlySpan planes, bool forceFarZ) { if (worldVerts.Length < 3) return; DrawDepthFanRhi(worldVerts, in viewProjection, planes, forceFarZ); } public void Dispose() => DisposeRhiResources(); }