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,6 @@
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Gpu.Gl;
using AcDream.App.Rendering.Gpu.Vk;
using AcDream.Core.Rendering.Wb;
using Chorizite.Core.Render.Enums;
using Microsoft.Extensions.Logging;
using Silk.NET.OpenGL;
@ -15,21 +15,21 @@ namespace AcDream.App.Rendering.Wb;
/// deliberately left CREATION with the caches — plan §5.5.11 records why, and
/// §5.5.12 item 1 hands the remainder forward: "the missing piece is an
/// <c>ITextureArray</c> implementation over <see cref="IGpuTexture"/>, not a
/// codec." This is that interface. <see cref="ManagedGLTextureArray"/> and
/// <see cref="RhiWorldTextureArray"/> implement it, and which one exists is
/// decided once at composition by <see cref="IWorldTextureArrayFactory"/> —
/// never per call, so the GL path executes exactly the statements it executed
/// before.</para>
/// codec." This is that interface. <c>ManagedGLTextureArray</c> used to be its
/// GL implementation, alongside <see cref="RhiWorldTextureArray"/>; which one
/// existed was decided once at composition by
/// <see cref="IWorldTextureArrayFactory"/>, never per call. Campaign V slice
/// V11 deleted <c>ManagedGLTextureArray</c> along with the rest of the raw-GL
/// arm, so <see cref="RhiWorldTextureArray"/> is now the sole implementation.</para>
///
/// <para><b>The slot, not the handle, is the seam.</b> Before this slice
/// <para><b>The slot, not the handle, is the seam.</b> Before V6i-2
/// <c>ObjectMeshManager</c> read <c>BindlessWrapHandle</c>/
/// <c>BindlessClampHandle</c> off the concrete GL array and interned them into
/// the device table itself. A 64-bit <c>ARB_bindless_texture</c> handle is
/// unspellable on Vulkan, so the array now answers the question the caller was
/// really asking — <see cref="ResolveSlot"/> — and each implementation gets
/// there its own way: the GL array interns its resident handle (the same
/// idempotent call, one level down), while the RHI array registered its two
/// (texture, sampler) pairs at construction and returns a field.</para>
/// unspellable on Vulkan, so the array answers the question the caller was
/// really asking — <see cref="ResolveSlot"/> — instead: the RHI array
/// registered its two (texture, sampler) pairs at construction and returns a
/// field.</para>
/// </summary>
internal interface IWorldTextureArray : IDisposable
{
@ -116,9 +116,9 @@ internal interface IWorldTextureArrayFactory
ArgumentNullException.ThrowIfNull(graphicsDevice);
ArgumentNullException.ThrowIfNull(gpuDevice);
ArgumentNullException.ThrowIfNull(logger);
return graphicsDevice is OpenGLGraphicsDevice gl && gpuDevice is GlGpuDevice table
? new GlWorldTextureArrayFactory(gl, table, logger)
: new RhiWorldTextureArrayFactory(gpuDevice);
// The GL arm this used to select between was deleted at Campaign V
// slice V11; the RHI arm is the only one left.
return new RhiWorldTextureArrayFactory(gpuDevice);
}
/// <summary>The retirement queue array layers and images are released through.</summary>
@ -132,36 +132,6 @@ internal interface IWorldTextureArrayFactory
IWorldTextureArray CreateClampedArray(TextureFormat format, int width, int height, int layers);
}
/// <summary>
/// The GL arm. Delegates to the same <c>OpenGLGraphicsDevice</c> entry point
/// <see cref="TextureAtlasManager"/> called directly before this slice, so the
/// shipping backend's construction is textually unchanged.
/// </summary>
internal sealed class GlWorldTextureArrayFactory(
OpenGLGraphicsDevice graphicsDevice,
GlGpuDevice worldTextureTable,
ILogger logger) : IWorldTextureArrayFactory
{
private readonly OpenGLGraphicsDevice _graphicsDevice = graphicsDevice
?? throw new ArgumentNullException(nameof(graphicsDevice));
private readonly GlGpuDevice _worldTextureTable = worldTextureTable
?? throw new ArgumentNullException(nameof(worldTextureTable));
private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
public IGpuResourceRetirementQueue Retirement => _graphicsDevice.ResourceRetirement;
public IWorldTextureArray CreateClampedArray(TextureFormat format, int width, int height, int layers) =>
new ManagedGLTextureArray(
_graphicsDevice,
format,
width,
height,
layers,
_logger,
_worldTextureTable,
TextureParameters.ClampToEdge);
}
/// <summary>
/// The backend-neutral arm. Creates through <see cref="IGpuDevice.CreateTexture"/>
/// and registers both address modes into the device's one texture table, so an
@ -353,11 +323,7 @@ internal sealed class RhiWorldTextureArray : IWorldTextureArray
ArgumentNullException.ThrowIfNull(data);
ArgumentOutOfRangeException.ThrowIfNegative(layer);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(layer, Size);
// The GL array validates the payload against the format's expected byte
// count and rejects transfer overrides that contradict it. Reusing that
// validator rather than writing a second one keeps the two arms agreeing
// on what a well-formed layer is.
ManagedGLTextureArray.ValidateUploadPayload(
ValidateUploadPayload(
SourceFormat,
_width,
_height,
@ -504,4 +470,65 @@ internal sealed class RhiWorldTextureArray : IWorldTextureArray
+ "and is not part of GpuTextureDescription. Campaign V's world-draw slice owns "
+ "extending the contract or proving no such atlas exists."),
};
private static bool IsCompressedFormat(TextureFormat format) =>
format is TextureFormat.DXT1 or TextureFormat.DXT3 or TextureFormat.DXT5;
/// <summary>
/// The expected byte count for one uploaded layer of <paramref name="format"/>
/// at <paramref name="width"/>x<paramref name="height"/>.
/// </summary>
internal static int CalculateExpectedDataSize(TextureFormat format, int width, int height)
{
if (IsCompressedFormat(format))
return TextureHelpers.GetCompressedLayerSize(width, height, format);
return format switch
{
TextureFormat.RGBA8 => checked(width * height * 4),
TextureFormat.RGB8 => checked(width * height * 3),
TextureFormat.A8 => checked(width * height),
TextureFormat.Rgba32f => checked(width * height * 16),
_ => throw new NotSupportedException($"Unsupported format {format}"),
};
}
/// <summary>
/// Validates an upload payload against the format's expected byte count and
/// rejects transfer overrides that contradict it.
/// </summary>
internal static void ValidateUploadPayload(
TextureFormat format,
int width,
int height,
int dataLength,
PixelFormat? uploadPixelFormat,
PixelType? uploadPixelType)
{
int expectedBytes = CalculateExpectedDataSize(format, width, height);
if (dataLength != expectedBytes)
{
throw new ArgumentException(
$"Texture-array layer payload has {dataLength} bytes; expected exactly {expectedBytes} "
+ $"for {format} {width}x{height}.",
nameof(dataLength));
}
if (IsCompressedFormat(format))
{
if (uploadPixelFormat.HasValue || uploadPixelType.HasValue)
throw new ArgumentException("Compressed texture uploads cannot specify pixel format/type overrides.");
return;
}
PixelFormat expectedFormat = format.ToPixelFormat();
PixelType expectedType = format.ToPixelType();
if ((uploadPixelFormat ?? expectedFormat) != expectedFormat
|| (uploadPixelType ?? expectedType) != expectedType)
{
throw new ArgumentException(
$"Upload descriptor {uploadPixelFormat}/{uploadPixelType} does not match "
+ $"the {expectedFormat}/{expectedType} transfer required by {format}.");
}
}
}