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,6 +1,4 @@
using AcDream.App.Rendering.Wb;
using Silk.NET.OpenGL;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace AcDream.App.Diagnostics;
@ -28,39 +26,6 @@ internal sealed class FrameScreenshotController
private readonly Dictionary<string, CaptureStatus> _status =
new(StringComparer.OrdinalIgnoreCase);
public FrameScreenshotController(
GL gl,
string directory,
Action<string>? log = null)
: this(
CreateReader(gl),
directory,
log)
{
}
private static Func<int, int, byte[]> CreateReader(GL gl)
{
ArgumentNullException.ThrowIfNull(gl);
var surface = new GlDefaultFramebufferSurface(gl);
return (width, height) => ReadDefaultFramebuffer(surface, width, height);
}
/// <summary>
/// Reads the default framebuffer through the same resolve-aware path the
/// screenshot gates use. Shared so there is exactly one implementation of
/// "read the backbuffer" in the process — see
/// <see cref="ReadDefaultFramebuffer(IDefaultFramebufferSurface, int, int)"/>.
/// </summary>
internal static byte[] ReadDefaultFramebuffer(GL gl, int width, int height)
{
ArgumentNullException.ThrowIfNull(gl);
return ReadDefaultFramebuffer(
new GlDefaultFramebufferSurface(gl),
width,
height);
}
internal FrameScreenshotController(
Func<int, int, byte[]> readRgba,
string directory,
@ -186,7 +151,7 @@ internal sealed class FrameScreenshotController
/// <summary>
/// Blits the whole colour buffer from the bound read framebuffer to the
/// bound draw framebuffer with <c>GL_NEAREST</c> and identical rectangles
/// the multisample resolve.
/// — the multisample resolve.
/// </summary>
void BlitColorNearest(int width, int height);
@ -194,7 +159,7 @@ internal sealed class FrameScreenshotController
}
/// <summary>
/// Reads the default framebuffer — framebuffer name 0, the backbuffer —
/// Reads the default framebuffer — framebuffer name 0, the backbuffer —
/// and nothing else.
/// </summary>
/// <remarks>
@ -204,13 +169,13 @@ internal sealed class FrameScreenshotController
/// offscreen target the previous renderer left bound. The frame this runs in
/// draws several: <c>PrivateEntityViewportRenderer</c> clears its paperdoll
/// and appraisal FBOs to exactly RGBA(0,0,0,0), which is what a leaked
/// binding writes to disk a fully transparent PNG that reads as a
/// binding writes to disk — a fully transparent PNG that reads as a
/// blank-world failure while the backbuffer on screen was correct.
/// </para>
/// <para>
/// This was latent for as long as something else rebound framebuffer 0 often
/// enough to mask it (before Campaign V slice V4c, GL <c>BeginPass</c> did so
/// on every pass — see plan §5.4). The capture states its own source instead
/// on every pass — see plan §5.4). The capture states its own source instead
/// of inheriting one, and restores the caller's binding so a diagnostic
/// capture cannot perturb the frame it observes.
/// </para>
@ -218,15 +183,15 @@ internal sealed class FrameScreenshotController
/// <b>Multisampling.</b> The window is created with the quality preset's
/// MSAA sample count, so the default framebuffer is normally 4x multisampled,
/// and <c>glReadPixels</c> against a multisampled read framebuffer is
/// <i>undefined</i> per the GL spec (GL 4.6 §18.2: an INVALID_OPERATION is
/// <i>undefined</i> per the GL spec (GL 4.6 §18.2: an INVALID_OPERATION is
/// generated only for framebuffer objects; for the default framebuffer the
/// result is simply unspecified, and AMD returns real pixels most of the time
/// and something else the rest). Every automated pixel gate and every blank-
/// world verdict in Campaign V reads through here, so an unspecified read is
/// an unsound instrument, not a cosmetic issue. When the default framebuffer
/// is multisampled the capture resolves it first blit the whole colour
/// is multisampled the capture resolves it first — blit the whole colour
/// buffer into a single-sampled RGBA8 framebuffer with identical rectangles
/// and <c>GL_NEAREST</c>, which is the defined resolve and reads that.
/// and <c>GL_NEAREST</c>, which is the defined resolve — and reads that.
/// A single-sampled default framebuffer keeps the original direct read, so
/// non-MSAA captures stay byte-for-byte what they were.
/// </para>
@ -271,7 +236,7 @@ internal sealed class FrameScreenshotController
uint resolve = surface.CreateResolveTarget(width, height);
try
{
// Read is still framebuffer 0 the multisampled source.
// Read is still framebuffer 0 — the multisampled source.
surface.BindDrawFramebuffer(resolve);
surface.BlitColorNearest(width, height);
surface.BindReadFramebuffer(resolve);
@ -283,134 +248,4 @@ internal sealed class FrameScreenshotController
}
}
private sealed class GlDefaultFramebufferSurface : IDefaultFramebufferSurface
{
private readonly GL _gl;
private uint _resolveRenderbuffer;
public GlDefaultFramebufferSurface(GL gl) => _gl = gl;
public uint ReadFramebufferBinding
{
get
{
_gl.GetInteger(GetPName.ReadFramebufferBinding, out int binding);
return (uint)binding;
}
}
public uint DrawFramebufferBinding
{
get
{
_gl.GetInteger(GetPName.DrawFramebufferBinding, out int binding);
return (uint)binding;
}
}
public int DefaultFramebufferSamples
{
get
{
_gl.GetInteger(GetPName.Samples, out int samples);
return samples;
}
}
public void BindReadFramebuffer(uint framebuffer) =>
_gl.BindFramebuffer(FramebufferTarget.ReadFramebuffer, framebuffer);
public void BindDrawFramebuffer(uint framebuffer) =>
_gl.BindFramebuffer(FramebufferTarget.DrawFramebuffer, framebuffer);
public uint CreateResolveTarget(int width, int height)
{
// RGBA8 matches the default framebuffer's colour encoding — the app
// never enables GL_FRAMEBUFFER_SRGB — so the blit is a pure resolve
// with no encoding conversion.
uint renderbuffer = _gl.GenRenderbuffer();
_gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, renderbuffer);
_gl.RenderbufferStorage(
RenderbufferTarget.Renderbuffer,
InternalFormat.Rgba8,
(uint)width,
(uint)height);
_gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, 0u);
uint framebuffer = _gl.GenFramebuffer();
_gl.BindFramebuffer(FramebufferTarget.DrawFramebuffer, framebuffer);
_gl.FramebufferRenderbuffer(
FramebufferTarget.DrawFramebuffer,
FramebufferAttachment.ColorAttachment0,
RenderbufferTarget.Renderbuffer,
renderbuffer);
GLEnum status =
_gl.CheckFramebufferStatus(FramebufferTarget.DrawFramebuffer);
if (status != GLEnum.FramebufferComplete)
{
_gl.BindFramebuffer(FramebufferTarget.DrawFramebuffer, 0u);
_gl.DeleteFramebuffer(framebuffer);
_gl.DeleteRenderbuffer(renderbuffer);
throw new InvalidOperationException(
$"multisample resolve framebuffer {width}x{height} is "
+ $"incomplete: {status}");
}
GLHelpers.ThrowOnResourceError(
_gl,
$"create screenshot resolve target {width}x{height}");
_resolveRenderbuffer = renderbuffer;
return framebuffer;
}
public void DeleteResolveTarget(uint framebuffer)
{
_gl.DeleteFramebuffer(framebuffer);
if (_resolveRenderbuffer != 0u)
_gl.DeleteRenderbuffer(_resolveRenderbuffer);
_resolveRenderbuffer = 0u;
}
public void BlitColorNearest(int width, int height)
{
// A blit is subject to the scissor test, so a frame that left a
// scissor rectangle armed would resolve only part of the image.
// This operation states the state it needs and restores it — the
// same self-contained-GL-state rule the render passes follow.
bool scissor = _gl.IsEnabled(EnableCap.ScissorTest);
if (scissor)
_gl.Disable(EnableCap.ScissorTest);
_gl.BlitFramebuffer(
0,
0,
width,
height,
0,
0,
width,
height,
ClearBufferMask.ColorBufferBit,
BlitFramebufferFilter.Nearest);
if (scissor)
_gl.Enable(EnableCap.ScissorTest);
GLHelpers.ThrowOnResourceError(
_gl,
$"resolve default framebuffer {width}x{height}");
}
public unsafe void ReadRgba(int width, int height, byte[] destination)
{
fixed (byte* pointer = destination)
{
_gl.ReadPixels(
0,
0,
(uint)width,
(uint)height,
PixelFormat.Rgba,
PixelType.UnsignedByte,
pointer);
}
}
}
}