acdream/src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs
Erik 8a7a0837e1 feat(render): Vulkan campaign V11 step 2 — delete the OpenGL backend
Vulkan is the sole, user-signed-off backend (V10 landed) and step 1
already removed ImGui/Studio/DevTools. This step deletes the GL
rendering backend itself: every Gpu/Gl/** implementation, the Wb
ManagedGL*/GLHelpers/GLSLShader/GLStateScope/RenderStateCache/
BindlessSupport family, Shader/ShaderProgramConstruction/SamplerCache,
RenderBootstrap, and RenderFrameGlStateController.

GameWindow.cs's Run()/CreateGraphics()/CreateBackbufferReader()/
OnLoad() collapse to their Vulkan-only arm; GameWindowGraphics loses
its OpenGlGameWindowGraphics subclass. RuntimeOptions.RenderBackend and
RenderBackendKind (incl. the Gl member of GpuBackendKind) are gone —
there is nothing left to select between. The five world-draw dual-arm
renderers (WbDrawDispatcher, EnvCellRenderer, TerrainModernRenderer,
ParticleRenderer, SkyRenderer) and the composition roots
(WorldRenderComposition, HostInputCameraComposition,
LivePresentationComposition, FrameRootComposition) collapse to their
RHI-only arm. GL-only diagnostic properties with a live external reader
(DynamicBufferCount and friends) simplify to a documented `=> 0`/no-op
rather than disappearing, since the reader is out of this commit's
scope.

A few GL-flavored mechanisms turned out to be backend-neutral once
isolated: GlConstructionCleanupLedger is renamed
ResourceConstructionCleanupLedger (exception-chain walking has nothing
to do with GL), and GlfwNativePlatformProbe moved out of the otherwise
GL-only GraphicalCapabilityRecord.cs into
GraphicalWindowBackendSelection.cs before the rest of that file was
deleted.

Test files with no surviving subject are deleted outright
(GraphicalCapabilityRequirementsTests, ShaderProgramConstructionTests,
PortalDepthShaderParityTests, TextureCacheBindlessTests,
TextRendererFailureSafetyTests, ClipFrameUploadTests, every
Gpu/Gl/*Tests, GlTextureOwnershipTests, RenderFrameGlStateControllerTests);
others get their dead GL-only members trimmed while their live
assertions stay (ClipFrameLayoutTests' MeshClipSsboBinding check now
reads GpuBindingModel.StorageClipRegions, the same binding index under
its new backend-neutral name; GpuResourceRetirementTransactionTests
drops its OpenGLGraphicsDevice-subclassing test double and the two GL
queue tests it existed for). EnvCellRendererTests' construction helper
now builds a real ObjectMeshManager via VulkanMeshPipelineDevice
instead of passing null through a null-forgiving operator, since the
RHI constructor never tolerated a null mesh manager and the old GL
constructor (which did) is gone.

Deferred to the next two steps, deliberately not touched here: the
Silk.NET.OpenGL/.Extensions.ARB package references, IMeshPipelineDevice.Gl
(WbMeshAdapter's GL? threading stays in place), Chorizite.Core's stale
csproj comment (the package itself is still load-bearing —
TextureFormat and friends are used well beyond the deleted
ManagedGLUniformBuffer), and the CI/gate scripts.

Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors.
Tests: full-solution `dotnet test` green across every project
(App.Tests 3937/3940 + 3 skips, Core.Tests 3296/3298 + 2 skips, all
others 100%); the 2 App.Tests names that flake under full-suite
parallel execution (#250-family, documented pre-existing) pass in
isolation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 02:19:53 +02:00

153 lines
7.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Numerics;
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><b>Wired by T1 (BR-3, `579c8b0`):</b> seal on interior roots, punch
/// on outdoor / look-in roots, via <c>RetailPViewPassExecutor.DrawPortalDepthWrite</c>
/// (the <c>DrawExitPortalMasks</c> 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
/// <see cref="DrawDepthFan"/>.</para>
///
/// <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>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
/// <c>Rendering/Shaders/portal_depth.{vert,frag}</c> from committed SPIR-V,
/// and its per-frame fan vertices come from the frame ring instead.</para>
/// </summary>
public sealed partial class PortalDepthMaskRenderer : IDisposable
{
private readonly ResourceCleanupGroup _resources;
/// <summary>Shared by both arms: the largest fan <see cref="DrawDepthFan"/> accepts.</summary>
private const int MaxFanVerts = 32;
/// <summary>
/// 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.
/// </summary>
internal long DynamicBufferCapacityBytes => 0;
/// <summary>
/// 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.
///
/// <para>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.</para>
/// </summary>
public void BeginFrame(int frameSlot)
{
ArgumentOutOfRangeException.ThrowIfNegative(frameSlot);
_rhiFrameStarted = true;
}
/// <summary>
/// #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.
/// </summary>
private const float PunchMarkDepthBias = 0.0005f;
/// <summary>
/// #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
/// <see cref="PunchMarkBiasEyeCapMeters"/>. 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.
/// </summary>
public const float PunchMarkBiasEyeCapMeters = 0.5f;
/// <summary>Retail <c>Render::znear</c> = 0.1 (decomp :342173, re-landed
/// d4b5c71). The cap conversion below assumes the production camera near
/// plane; the small f/(fn) factor (~1.00002 at far 5000) is ignored.</summary>
public const float CameraNearPlaneMeters = 0.1f;
/// <summary>CPU mirror of the vertex-shader mark-bias expression (keep in
/// sync with <c>VertSrc</c>): the NDC bias applied at eye depth
/// <paramref name="eyeDepthMeters"/>.</summary>
public static float MarkBiasNdc(float eyeDepthMeters) =>
MathF.Min(PunchMarkDepthBias,
PunchMarkBiasEyeCapMeters * CameraNearPlaneMeters
/ MathF.Max(eyeDepthMeters * eyeDepthMeters, 1e-6f));
/// <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).
///
/// <para><b>Seal</b> (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.</para>
///
/// <para><b>Punch</b> (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.</para>
/// </summary>
public void DrawDepthFan(
ReadOnlySpan<Vector3> worldVerts,
in Matrix4x4 viewProjection,
ReadOnlySpan<Vector4> planes,
bool forceFarZ)
{
if (worldVerts.Length < 3)
return;
DrawDepthFanRhi(worldVerts, in viewProjection, planes, forceFarZ);
}
public void Dispose() => DisposeRhiResources();
}