Ports the seal half of retail's invisible portal depth writes
(D3DPolyRender::DrawPortalPolyInternal, Ghidra 0x0059bc90; dispatched by
PView::DrawCells loop 1, Ghidra 0x005a4840 pc:432783-432786):
- NEW PortalDepthMaskRenderer: draws a portal polygon as a color-masked
triangle fan, depth-test ALWAYS + depth-write ON, at the polygon's TRUE
projected depth (retail maxZ2 seal) or forced to far-z 0.99999988
(retail maxZ1 punch - the constant from 0x0059bc90's tail; punch wiring
lands in BR-2 commit 2). Where retail software-clips the fan against
the installed view (polyClipFinish), we apply the SAME slice region via
gl_ClipDistance from the slice's <=8 clip-space half-planes. GL state
fully self-contained (set -> draw -> restore, no early-outs).
- DrawExitPortalMasks is now WIRED in production (was a null-callback
no-op since birth): for interior roots, every visible cell's portals
with OtherCellId==0xFFFF get their world-space polygon sealed per view
slice, far-to-near, after the landscape slices.
- ClearDepthSlice (per-slice scissored AABB clear - wrong shape, wrong
scope, no seal after it) is REPLACED by ClearDepthForInterior: ONE
full-buffer depth clear between the outside stage and the interior
stage, gated on any outside slice having drawn (retail's
portalsDrawnCount gate semantics staged as an open question, marked
inline). DepthMask(true) asserted at the clear site (c4df241 lesson).
Outdoor roots: no clear, no seals (interiors must depth-test against
terrain until the commit-2 punch).
Closes the mechanism behind #108 (outdoor grass sweeping across the
upstairs door opening - terrain depth seen through the doorway is now
re-stamped at the door plane so farther interior geometry z-fails inside
the aperture). Visual gate: BR-2/BR-3 batched checklist (cellar doorway
+ cottage wall + tower stairs near/far).
Suites: build green, App 226 green, Core 1398 + 4 pre-existing #99-era
failures + 1 skip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
202 lines
8 KiB
C#
202 lines
8 KiB
C#
using System;
|
|
using System.Numerics;
|
|
using Silk.NET.OpenGL;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
/// <summary>
|
|
/// BR-2 (holistic building-render port): retail's invisible portal depth
|
|
/// writes — the port of <c>D3DPolyRender::DrawPortalPolyInternal</c>
|
|
/// (Ghidra 0x0059bc90, pc:424490).
|
|
///
|
|
/// <para>Retail projects a portal polygon, software-clips it against the
|
|
/// installed portal view (<c>polyClipFinish</c>), and draws the survivor as a
|
|
/// COLOR-INVISIBLE triangle fan with depth-test ALWAYS + depth-write ON:</para>
|
|
/// <list type="bullet">
|
|
/// <item><b>Seal</b> (retail <c>maxZ2=6</c>, bit0 clear, data 0x00820e14):
|
|
/// z = the polygon's true projected depth. Drawn on portals leading OUTSIDE
|
|
/// (<c>other_cell_id==0xFFFF</c>) 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).</item>
|
|
/// <item><b>Punch</b> (retail <c>maxZ1=7</c>, 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.</item>
|
|
/// </list>
|
|
///
|
|
/// <para>Where retail clips the polygon on the CPU against the view, we apply
|
|
/// the SAME view region via <c>gl_ClipDistance</c> from the slice's clip-space
|
|
/// half-planes (≤8, the validated <see cref="ClipPlaneSet"/> output) — the
|
|
/// depth write lands only inside the slice region, matching retail's clipped
|
|
/// fan.</para>
|
|
///
|
|
/// <para>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.</para>
|
|
/// </summary>
|
|
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;
|
|
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)
|
|
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 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");
|
|
|
|
_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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draw one portal polygon as an invisible depth write, clipped to the
|
|
/// slice's clip-space half-planes. <paramref name="forceFarZ"/> selects
|
|
/// punch (true, retail maxZ1) vs seal (false, retail maxZ2 true depth).
|
|
/// </summary>
|
|
public void DrawDepthFan(
|
|
ReadOnlySpan<Vector3> worldVerts,
|
|
in Matrix4x4 viewProjection,
|
|
ReadOnlySpan<Vector4> 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.DepthFunc(DepthFunction.Always); // retail DEPTHTEST ALWAYS
|
|
_gl.DepthMask(true); // z-write on
|
|
_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<float> 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.Uniform1(_locForceFarZ, forceFarZ ? 1 : 0);
|
|
|
|
_gl.BindVertexArray(_vao);
|
|
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
|
|
fixed (float* v = _scratch)
|
|
_gl.BufferSubData(BufferTargetARB.ArrayBuffer, 0, (nuint)(n * 3 * sizeof(float)), v);
|
|
_gl.DrawArrays(PrimitiveType.TriangleFan, 0, (uint)n);
|
|
_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.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);
|
|
}
|
|
}
|