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();
}