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>
This commit is contained in:
Erik 2026-07-29 02:19:53 +02:00
parent b70b9832ff
commit 8a7a0837e1
121 changed files with 1243 additions and 19840 deletions

View file

@ -1,8 +1,5 @@
using System;
using System.Linq;
using System.Numerics;
using AcDream.App.Rendering.Wb;
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
@ -42,196 +39,42 @@ namespace AcDream.App.Rendering;
/// 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>
/// <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
{
/// <summary>
/// The GL arm's inline program. Campaign V slice V6l added a SECOND arm that
/// compiles <c>Rendering/Shaders/portal_depth.{vert,frag}</c> — the same body,
/// with its loose uniforms rehomed onto the shared push block and the clip
/// planes onto the <c>TerrainClip</c> UBO — because a Vulkan pipeline needs a
/// named, SPIR-V-compiled pair. This string stays because the GL arm keeps
/// its raw path through to V10 (plan §5.5.6) and because rehoming its clip
/// planes onto UBO binding 2 would clobber the terrain clip block that
/// <c>ClipFrame</c> binds there globally on GL. The two are kept in step by
/// <c>PortalDepthShaderParityTests</c>, and this one is deleted at V11.
/// </summary>
internal 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)
uniform float uDepthBiasEyeCapN; // eye-span cap x near plane (#129; see MarkBiasNdc)
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)
{
// #117 mark-pass bias, #129 eye-space cap. clipPos.w = eye depth d;
// an NDC bias b spans ~b*d*d/near meters of eye depth, so the
// constant-NDC form alone reached METERS at distance (door-shaped
// leaks through hills/houses). Keep in sync with MarkBiasNdc.
float biasNdc = min(uDepthBias, uDepthBiasEyeCapN / max(clipPos.w * clipPos.w, 1e-6));
clipPos.z -= biasNdc * clipPos.w;
}
gl_Position = clipPos;
}";
internal const string FragSrc = @"#version 430 core
void main() { } // depth-only: color writes are masked off by the caller state
";
private readonly GL? _glContext;
private GL _gl => _glContext
?? throw new InvalidOperationException(
"PortalDepthMaskRenderer's GL arm was reached on a backend with no GL context "
+ "(campaign plan slice V6l; the RHI arm lives in PortalDepthMaskRenderer.Rhi.cs).");
private readonly uint _program;
private readonly int _locViewProjection;
private readonly int _locPlaneCount;
private readonly int _locPlanes;
private readonly int _locForceFarZ;
private readonly int _locDepthBias;
private readonly int _locDepthBiasEyeCapN;
private readonly ResourceCleanupGroup _resources;
/// <summary>Shared by both arms: the largest fan <see cref="DrawDepthFan"/> accepts.</summary>
private const int MaxFanVerts = 32;
private readonly float[] _scratch = new float[MaxFanVerts * 3];
private sealed class FrameBufferSet
{
public uint Vao;
public uint Vbo;
public int CapacityBytes;
public int UsedVertices;
}
private readonly FrameBufferSet?[] _frameBuffers = new FrameBufferSet[3];
private FrameBufferSet? _activeFrameBuffer;
internal long DynamicBufferCapacityBytes =>
_frameBuffers.Sum(set => (long)(set?.CapacityBytes ?? 0));
public PortalDepthMaskRenderer(GL gl)
{
_glContext = gl ?? throw new ArgumentNullException(nameof(gl));
var resources = new ResourceCleanupGroup();
try
{
_program = ShaderProgramConstruction.Build(
new GlShaderProgramBuildApi(gl),
VertSrc,
FragSrc);
resources.Add(
"portal-depth program",
() => GlResourceCommand.DeleteProgram(
gl,
_program,
$"delete PortalDepthMask program {_program}"));
(
_locViewProjection,
_locPlaneCount,
_locPlanes,
_locForceFarZ,
_locDepthBias,
_locDepthBiasEyeCapN) = GlResourceCommand.Execute(
_gl,
"resolve PortalDepthMask uniforms",
() => (
_gl.GetUniformLocation(_program, "uViewProjection"),
_gl.GetUniformLocation(_program, "uPlaneCount"),
_gl.GetUniformLocation(_program, "uPlanes"),
_gl.GetUniformLocation(_program, "uForceFarZ"),
_gl.GetUniformLocation(_program, "uDepthBias"),
_gl.GetUniformLocation(_program, "uDepthBiasEyeCapN")));
for (int i = 0; i < _frameBuffers.Length; i++)
{
FrameBufferSet set = CreateFrameBufferSet(resources, i);
_frameBuffers[i] = set;
}
}
catch (Exception constructionFailure)
{
resources.RollbackConstructionAndThrow(
"PortalDepthMaskRenderer construction failed and its GL prefix did not cleanly roll back.",
constructionFailure);
throw new System.Diagnostics.UnreachableException();
}
_resources = resources;
}
/// <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)
{
if ((uint)frameSlot >= (uint)_frameBuffers.Length)
throw new ArgumentOutOfRangeException(nameof(frameSlot));
// Slice V6l: the RHI arm owns no per-flight VBO ring — every frame ring
// allocation is already distinct memory that lives until the frame
// retires — so all this edge carries there is the started latch.
ArgumentOutOfRangeException.ThrowIfNegative(frameSlot);
_rhiFrameStarted = true;
if (_glContext is null)
return;
_activeFrameBuffer = _frameBuffers[frameSlot];
_activeFrameBuffer!.UsedVertices = 0;
}
private FrameBufferSet CreateFrameBufferSet(
ResourceCleanupGroup resources,
int frameIndex)
{
uint vao = TrackedGlResource.CreateVertexArray(
_gl,
"PortalDepthMask frame VAO creation");
RetryableGpuResourceRelease vaoRelease =
TrackedGlResource.CreateRetryableVertexArrayDeletion(
_gl,
vao,
"PortalDepthMask frame VAO disposal");
resources.Add($"portal-depth frame {frameIndex} VAO", vaoRelease.Run);
uint vbo = TrackedGlResource.CreateBuffer(
_gl,
"PortalDepthMask frame VBO creation");
var set = new FrameBufferSet { Vao = vao, Vbo = vbo };
RetryableGpuResourceRelease vboRelease =
TrackedGlResource.CreateRetryableBufferDeletion(
_gl,
vbo,
() => set.CapacityBytes,
"PortalDepthMask frame VBO disposal");
resources.Add($"portal-depth frame {frameIndex} VBO", vboRelease.Run);
GlResourceCommand.Execute(_gl, "configure PortalDepthMask frame buffers", () =>
{
_gl.BindVertexArray(set.Vao);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, set.Vbo);
unsafe
{
_gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 3 * sizeof(float), (void*)0);
}
_gl.EnableVertexAttribArray(0);
_gl.BindVertexArray(0);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
});
return set;
}
/// <summary>
@ -303,143 +146,8 @@ void main() { } // depth-only: color writes are masked off by the caller state
{
if (worldVerts.Length < 3)
return;
if (_glContext is null)
{
DrawDepthFanRhi(worldVerts, in viewProjection, planes, forceFarZ);
return;
}
FrameBufferSet frameBuffer = _activeFrameBuffer
?? throw new InvalidOperationException("BeginFrame must be called before drawing portal depth masks.");
int n = Math.Min(worldVerts.Length, MaxFanVerts);
int planeCount = Math.Min(planes.Length, 8);
int firstVertex = frameBuffer.UsedVertices;
int requiredBytes = checked((firstVertex + n) * 3 * sizeof(float));
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<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.BindVertexArray(frameBuffer.Vao);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, frameBuffer.Vbo);
if (frameBuffer.CapacityBytes < requiredBytes)
{
int newCapacity = DynamicBufferCapacity.Grow(
frameBuffer.CapacityBytes,
requiredBytes);
TrackedGlResource.AllocateBufferStorage(
_gl,
GLEnum.ArrayBuffer,
frameBuffer.Vbo,
frameBuffer.CapacityBytes,
newCapacity,
GLEnum.DynamicDraw,
"PortalDepthMask frame VBO growth");
frameBuffer.CapacityBytes = newCapacity;
}
fixed (float* v = _scratch)
_gl.BufferSubData(
BufferTargetARB.ArrayBuffer,
(nint)(firstVertex * 3 * sizeof(float)),
(nuint)(n * 3 * sizeof(float)),
v);
frameBuffer.UsedVertices += n;
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, firstVertex, (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.Uniform1(_locDepthBiasEyeCapN,
PunchMarkBiasEyeCapMeters * CameraNearPlaneMeters);
_gl.DrawArrays(PrimitiveType.TriangleFan, firstVertex, (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, firstVertex, (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);
// Renderers opt into culling locally; the shared frame convention is
// cull-off. Keep this success exit identical to frame-abort recovery.
_gl.Disable(EnableCap.CullFace);
_gl.CullFace(TriangleFace.Back);
_gl.FrontFace(FrontFaceDirection.CW);
_gl.UseProgram(0);
DrawDepthFanRhi(worldVerts, in viewProjection, planes, forceFarZ);
}
public void Dispose()
{
if (_glContext is null)
{
DisposeRhiResources();
return;
}
_resources.RetryCleanup();
}
public void Dispose() => DisposeRhiResources();
}