using System;
using System.Numerics;
using Silk.NET.OpenGL;
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 GameWindow.DrawRetailPViewPortalDepthWrite
/// (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.
///
/// Self-contained GL state (feedback_render_self_contained_gl_state):
/// sets everything it depends on, restores the frame-global convention on
/// exit, no early-outs between set and restore.
///
public sealed class PortalDepthMaskRenderer : IDisposable
{
private const string VertSrc = @"#version 430 core
layout(location = 0) in vec3 aPos;
uniform mat4 uViewProjection;
uniform int uPlaneCount;
uniform vec4 uPlanes[8];
uniform int uForceFarZ;
uniform float uDepthBias; // NDC bias toward the viewer (mark pass only)
out float gl_ClipDistance[8];
void main()
{
vec4 clipPos = uViewProjection * vec4(aPos, 1.0);
for (int i = 0; i < 8; i++)
gl_ClipDistance[i] = (i < uPlaneCount) ? dot(uPlanes[i], clipPos) : 1.0;
if (uForceFarZ == 1)
clipPos.z = clipPos.w * 0.99999988; // retail far-z punch constant (0x0059bc90 tail)
else if (uDepthBias > 0.0)
clipPos.z -= uDepthBias * clipPos.w; // #117 mark-pass bias (see DrawDepthFan)
gl_Position = clipPos;
}";
private const string FragSrc = @"#version 430 core
void main() { } // depth-only: color writes are masked off by the caller state
";
private readonly GL _gl;
private readonly uint _program;
private readonly uint _vao;
private readonly uint _vbo;
private readonly int _locViewProjection;
private readonly int _locPlaneCount;
private readonly int _locPlanes;
private readonly int _locForceFarZ;
private readonly int _locDepthBias;
private const int MaxFanVerts = 32;
private readonly float[] _scratch = new float[MaxFanVerts * 3];
public PortalDepthMaskRenderer(GL gl)
{
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
uint vs = Compile(ShaderType.VertexShader, VertSrc);
uint fs = Compile(ShaderType.FragmentShader, FragSrc);
_program = _gl.CreateProgram();
_gl.AttachShader(_program, vs);
_gl.AttachShader(_program, fs);
_gl.LinkProgram(_program);
_gl.GetProgram(_program, ProgramPropertyARB.LinkStatus, out int linked);
if (linked == 0)
throw new InvalidOperationException($"PortalDepthMask link failed: {_gl.GetProgramInfoLog(_program)}");
_gl.DeleteShader(vs);
_gl.DeleteShader(fs);
_locViewProjection = _gl.GetUniformLocation(_program, "uViewProjection");
_locPlaneCount = _gl.GetUniformLocation(_program, "uPlaneCount");
_locPlanes = _gl.GetUniformLocation(_program, "uPlanes");
_locForceFarZ = _gl.GetUniformLocation(_program, "uForceFarZ");
_locDepthBias = _gl.GetUniformLocation(_program, "uDepthBias");
_vao = _gl.GenVertexArray();
_vbo = _gl.GenBuffer();
_gl.BindVertexArray(_vao);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
unsafe
{
_gl.BufferData(BufferTargetARB.ArrayBuffer,
(nuint)(MaxFanVerts * 3 * sizeof(float)), null, BufferUsageARB.DynamicDraw);
_gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 3 * sizeof(float), (void*)0);
}
_gl.EnableVertexAttribArray(0);
_gl.BindVertexArray(0);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
}
private uint Compile(ShaderType type, string src)
{
uint s = _gl.CreateShader(type);
_gl.ShaderSource(s, src);
_gl.CompileShader(s);
_gl.GetShader(s, ShaderParameterName.CompileStatus, out int ok);
if (ok == 0)
throw new InvalidOperationException($"PortalDepthMask {type} compile failed: {_gl.GetShaderInfoLog(s)}");
return s;
}
///
/// #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.
/// 0.0005 NDC ≈ 6 cm at 5 m / ≈ 1 m at 20 m with znear=0.1.
///
private const float PunchMarkDepthBias = 0.0005f;
///
/// 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;
int n = Math.Min(worldVerts.Length, MaxFanVerts);
int planeCount = Math.Min(planes.Length, 8);
for (int i = 0; i < n; i++)
{
_scratch[i * 3 + 0] = worldVerts[i].X;
_scratch[i * 3 + 1] = worldVerts[i].Y;
_scratch[i * 3 + 2] = worldVerts[i].Z;
}
// ---- set state (everything this draw depends on) ----
_gl.UseProgram(_program);
_gl.Disable(EnableCap.Blend);
_gl.Disable(EnableCap.CullFace); // portal fans face either way
_gl.Disable(EnableCap.ScissorTest);
_gl.Enable(EnableCap.DepthTest);
_gl.ColorMask(false, false, false, false); // alpha-0 fan ≙ no color
for (int i = 0; i < planeCount; i++)
_gl.Enable(EnableCap.ClipDistance0 + i);
unsafe
{
var m = viewProjection;
_gl.UniformMatrix4(_locViewProjection, 1, false, (float*)&m);
_gl.Uniform1(_locPlaneCount, planeCount);
if (planeCount > 0)
{
Span p = stackalloc float[planeCount * 4];
for (int i = 0; i < planeCount; i++)
{
p[i * 4 + 0] = planes[i].X;
p[i * 4 + 1] = planes[i].Y;
p[i * 4 + 2] = planes[i].Z;
p[i * 4 + 3] = planes[i].W;
}
fixed (float* pp = p)
_gl.Uniform4(_locPlanes, (uint)planeCount, pp);
}
_gl.BindVertexArray(_vao);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
fixed (float* v = _scratch)
_gl.BufferSubData(BufferTargetARB.ArrayBuffer, 0, (nuint)(n * 3 * sizeof(float)), v);
if (!forceFarZ)
{
// ── SEAL: retail-verbatim single pass ──
_gl.DepthFunc(DepthFunction.Always);
_gl.DepthMask(true);
_gl.Uniform1(_locForceFarZ, 0);
_gl.Uniform1(_locDepthBias, 0f);
_gl.DrawArrays(PrimitiveType.TriangleFan, 0, (uint)n);
}
else
{
// ── PUNCH pass A: stencil-mark visible aperture pixels ──
_gl.Enable(EnableCap.StencilTest);
_gl.StencilFunc(StencilFunction.Always, 1, 0xFF);
_gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Replace);
_gl.StencilMask(0xFF);
_gl.DepthFunc(DepthFunction.Lequal);
_gl.DepthMask(false);
_gl.Uniform1(_locForceFarZ, 0);
_gl.Uniform1(_locDepthBias, PunchMarkDepthBias);
_gl.DrawArrays(PrimitiveType.TriangleFan, 0, (uint)n);
// ── PUNCH pass B: far-Z write on marked pixels only;
// zero the stencil as we go (self-cleaning) ──
_gl.StencilFunc(StencilFunction.Equal, 1, 0xFF);
_gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Zero);
_gl.DepthFunc(DepthFunction.Always);
_gl.DepthMask(true);
_gl.Uniform1(_locForceFarZ, 1);
_gl.Uniform1(_locDepthBias, 0f);
_gl.DrawArrays(PrimitiveType.TriangleFan, 0, (uint)n);
_gl.Disable(EnableCap.StencilTest);
}
_gl.BindVertexArray(0);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
}
// ---- restore the frame-global convention ----
for (int i = 0; i < planeCount; i++)
_gl.Disable(EnableCap.ClipDistance0 + i);
_gl.ColorMask(true, true, true, true);
_gl.DepthMask(true);
_gl.DepthFunc(DepthFunction.Less);
_gl.Enable(EnableCap.CullFace);
_gl.CullFace(TriangleFace.Back);
_gl.FrontFace(FrontFaceDirection.CW);
_gl.UseProgram(0);
}
public void Dispose()
{
_gl.DeleteProgram(_program);
_gl.DeleteVertexArray(_vao);
_gl.DeleteBuffer(_vbo);
}
}