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