BR-2 commit 1: exit-portal depth SEALS + retail full depth clear (the #108 machinery)
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>
This commit is contained in:
parent
6cba95047c
commit
6d4cac2418
3 changed files with 279 additions and 17 deletions
|
|
@ -171,6 +171,7 @@ public sealed class GameWindow : IDisposable
|
|||
// each frame on an indoor root (null on the outdoor root).
|
||||
private AcDream.App.Rendering.InteriorRenderer? _interiorRenderer;
|
||||
private AcDream.App.Rendering.RetailPViewRenderer? _retailPViewRenderer;
|
||||
private AcDream.App.Rendering.PortalDepthMaskRenderer? _portalDepthMask;
|
||||
private AcDream.App.Rendering.InteriorEntityPartition.Result? _interiorPartition;
|
||||
|
||||
// Phase U.3: the shared per-frame clip data (binding=2 mesh SSBO + terrain
|
||||
|
|
@ -1845,6 +1846,10 @@ public sealed class GameWindow : IDisposable
|
|||
_clipFrame ??= ClipFrame.NoClip();
|
||||
_retailPViewRenderer = new AcDream.App.Rendering.RetailPViewRenderer(
|
||||
_gl, _clipFrame, _envCellRenderer!, _wbDrawDispatcher!);
|
||||
|
||||
// BR-2: invisible portal depth writes (seal/punch) — retail
|
||||
// DrawPortalPolyInternal (Ghidra 0x0059bc90).
|
||||
_portalDepthMask = new AcDream.App.Rendering.PortalDepthMaskRenderer(_gl);
|
||||
}
|
||||
|
||||
// Phase G.1 sky renderer — its own shader (sky.vert / sky.frag)
|
||||
|
|
@ -7632,24 +7637,28 @@ public sealed class GameWindow : IDisposable
|
|||
renderSky,
|
||||
kf,
|
||||
environOverrideActive),
|
||||
// The depth clear is a doorway "look-in" trick: clear depth inside a door/window
|
||||
// region so the cell seen THROUGH it draws over the terrain drawn through that
|
||||
// region (the indoor root looking out). For the OUTDOOR-node root the only
|
||||
// OutsideView slice is the FULL-SCREEN base terrain, so clearing its depth wipes the
|
||||
// entire depth buffer AFTER terrain/exteriors/player drew — the flooded building
|
||||
// interiors (cellars) would then paint over everything (cellar in front of the
|
||||
// player; building interiors through the ground). Outdoors the interiors must
|
||||
// depth-test against terrain+exteriors and appear only through real door openings,
|
||||
// so issue NO depth clear. Interior roots keep the doorway clear (unchanged).
|
||||
ClearDepthSlice = clipRoot.IsOutdoorNode
|
||||
// BR-2: retail's depth discipline between the outside stage and the
|
||||
// interior stage (PView::DrawCells, Ghidra 0x005a4840): one FULL depth
|
||||
// clear (no scissor — the old per-slice AABB clear was the wrong shape),
|
||||
// then DrawExitPortalMasks re-stamps every outside-leading portal's TRUE
|
||||
// depth so terrain seen through a doorway keeps its pixels (#108).
|
||||
// For the OUTDOOR-node root the only OutsideView slice is the FULL-SCREEN
|
||||
// base terrain, so a clear would wipe the entire depth buffer AFTER
|
||||
// terrain/exteriors/player drew — the flooded building interiors would
|
||||
// paint over everything. Outdoors the interiors must depth-test against
|
||||
// terrain+exteriors and appear only through real apertures (the BR-2
|
||||
// commit-2 far-Z punch), so: NO clear, NO seals.
|
||||
ClearDepthForInterior = clipRoot.IsOutdoorNode
|
||||
? null
|
||||
: slice =>
|
||||
: () =>
|
||||
{
|
||||
bool zc = BeginDoorwayScissor(true, slice.NdcAabb);
|
||||
_gl.Disable(EnableCap.ScissorTest);
|
||||
_gl.DepthMask(true); // depth clears honor glDepthMask (c4df241 lesson)
|
||||
_gl.Clear(ClearBufferMask.DepthBufferBit);
|
||||
if (zc)
|
||||
_gl.Disable(EnableCap.ScissorTest);
|
||||
},
|
||||
DrawExitPortalMasks = clipRoot.IsOutdoorNode
|
||||
? null
|
||||
: sliceCtx => DrawRetailPViewExitPortalSeal(sliceCtx, envCellViewProj),
|
||||
DrawCellParticles = sliceCtx =>
|
||||
DrawRetailPViewCellParticles(sliceCtx, camera, camPos),
|
||||
EmitDiagnostics = result =>
|
||||
|
|
@ -9550,6 +9559,43 @@ public sealed class GameWindow : IDisposable
|
|||
DisableClipDistances();
|
||||
}
|
||||
|
||||
// BR-2 seal: re-stamp the TRUE depth of every outside-leading portal of this
|
||||
// cell, clipped to the slice's view region — retail PView::DrawCells loop 1
|
||||
// (Ghidra 0x005a4840, pc:432783-432786): after the landscape draws through
|
||||
// the outside views and the depth buffer is cleared, every portal with
|
||||
// other_cell_id==0xFFFF gets DrawPortalPolyInternal(poly, false) — an
|
||||
// invisible depth write at the portal plane, so interior geometry FARTHER
|
||||
// than the doorway z-fails inside the aperture and the terrain seen through
|
||||
// it keeps its pixels (#108). Wiring only — the draw lives in
|
||||
// PortalDepthMaskRenderer.
|
||||
private void DrawRetailPViewExitPortalSeal(
|
||||
AcDream.App.Rendering.RetailPViewCellSliceContext sliceCtx,
|
||||
System.Numerics.Matrix4x4 viewProjection)
|
||||
{
|
||||
if (_portalDepthMask is null)
|
||||
return;
|
||||
if (!_cellVisibility.TryGetCell(sliceCtx.CellId, out var cell) || cell is null)
|
||||
return;
|
||||
|
||||
Span<System.Numerics.Vector3> world = stackalloc System.Numerics.Vector3[32];
|
||||
for (int i = 0; i < cell.Portals.Count; i++)
|
||||
{
|
||||
if (cell.Portals[i].OtherCellId != 0xFFFF)
|
||||
continue; // seals apply to portals leading OUTSIDE only
|
||||
if (i >= cell.PortalPolygons.Count)
|
||||
break;
|
||||
var localVerts = cell.PortalPolygons[i];
|
||||
if (localVerts.Length < 3)
|
||||
continue;
|
||||
|
||||
int n = System.Math.Min(localVerts.Length, world.Length);
|
||||
for (int v = 0; v < n; v++)
|
||||
world[v] = System.Numerics.Vector3.Transform(localVerts[v], cell.WorldTransform);
|
||||
|
||||
_portalDepthMask.DrawDepthFan(world[..n], viewProjection, sliceCtx.Slice.Planes, forceFarZ: false);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawRetailPViewCellParticles(
|
||||
AcDream.App.Rendering.RetailPViewCellSliceContext sliceCtx,
|
||||
ICamera camera,
|
||||
|
|
@ -11773,6 +11819,7 @@ public sealed class GameWindow : IDisposable
|
|||
_audioEngine?.Dispose(); // Phase E.2: stop all voices, close AL context
|
||||
_wbDrawDispatcher?.Dispose();
|
||||
_envCellRenderer?.Dispose(); // Phase A8
|
||||
_portalDepthMask?.Dispose(); // BR-2
|
||||
_clipFrame?.Dispose(); // Phase U.3
|
||||
_skyRenderer?.Dispose(); // depends on sampler cache; dispose first
|
||||
_samplerCache?.Dispose();
|
||||
|
|
|
|||
202
src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs
Normal file
202
src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -231,8 +231,16 @@ public sealed class RetailPViewRenderer
|
|||
ctx.DrawLandscapeSlice(new RetailPViewLandscapeSliceContext(slice, partition.Outdoor));
|
||||
}
|
||||
|
||||
foreach (var slice in clipAssembly.OutsideViewSlices)
|
||||
ctx.ClearDepthSlice?.Invoke(slice);
|
||||
// BR-2: retail clears the FULL depth buffer ONCE between the outside
|
||||
// stage and the interior stage (PView::DrawCells, Ghidra 0x005a4840 —
|
||||
// Clear gated on portalsDrawnCount; the exact gate semantics is a plan
|
||||
// open question, staged here as "any outside slice drawn"), then
|
||||
// re-stamps every outside-leading portal's TRUE depth (the seals,
|
||||
// DrawExitPortalMasks below). The old per-slice scissored AABB clear
|
||||
// was the wrong shape (AABB ⊇ aperture polygon) and had no seal after
|
||||
// it — the #108 mechanism.
|
||||
if (clipAssembly.OutsideViewSlices.Length > 0)
|
||||
ctx.ClearDepthForInterior?.Invoke();
|
||||
|
||||
UseIndoorMembershipOnlyRouting();
|
||||
}
|
||||
|
|
@ -575,7 +583,12 @@ public sealed class RetailPViewDrawContext : IRetailPViewCellDrawContext
|
|||
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries { get; init; }
|
||||
public required Action<uint> SetTerrainClipUbo { get; init; }
|
||||
public required Action<RetailPViewLandscapeSliceContext> DrawLandscapeSlice { get; init; }
|
||||
public Action<ClipViewSlice>? ClearDepthSlice { get; init; }
|
||||
|
||||
/// <summary>BR-2: one full-buffer depth clear between the outside stage and the
|
||||
/// interior stage (retail PView::DrawCells, Ghidra 0x005a4840). Null for outdoor
|
||||
/// roots — outdoors the interiors must depth-test against terrain + exteriors and
|
||||
/// appear only through real apertures (the BR-2 commit-2 punch).</summary>
|
||||
public Action? ClearDepthForInterior { get; init; }
|
||||
public Action<RetailPViewCellSliceContext>? DrawExitPortalMasks { get; init; }
|
||||
public Action<RetailPViewCellSliceContext>? DrawCellParticles { get; init; }
|
||||
public Action<RetailPViewFrameResult>? EmitDiagnostics { get; init; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue