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,61 +1,39 @@
using System;
using System.Runtime.InteropServices;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Wb;
using AcDream.Core.Lighting;
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
/// <summary>
/// GL wrapper that owns the SceneLighting UBO buffer, updates its
/// contents each frame, and keeps it bound at binding=1 so every
/// shader sampling <c>uLights[]</c> / <c>uFogColor</c> / etc reads
/// consistent data without per-shader re-upload.
/// Publishes the SceneLighting UBO each frame so every shader sampling
/// <c>uLights[]</c> / <c>uFogColor</c> / etc reads consistent data without
/// per-shader re-upload.
///
/// <para>
/// Usage (r12 §13.2 + r13 §12.3):
/// <list type="number">
/// <item><description>Instantiate once at startup, after the GL context exists.</description></item>
/// <item><description>Each frame, after <see cref="LightManager.Tick"/>, call <see cref="Upload"/> with a freshly-built <see cref="SceneLightingUbo"/>.</description></item>
/// <item><description>Shader programs that declare <c>layout(std140, binding = 1) uniform SceneLighting { ... }</c> automatically pick up the data.</description></item>
/// </list>
/// </para>
/// <para>The raw-GL arm this used to have alongside it — a global uniform
/// binding point kept live by a per-flight-slot buffer pool — was deleted at
/// Campaign V slice V11. Vulkan has no such global binding point: a
/// descriptor set is bound per draw, so the upload is a frame ring slice
/// PUBLISHED on <see cref="WorldFrameSections.SceneLighting"/>, and each world
/// renderer binds it inside the pass after its own binds (plan §5.5.14 item
/// 2). Every allocation within a frame is already distinct memory that lives
/// until the frame retires, which is the property the deleted buffer pool
/// existed to provide when the world, portal-space and paperdoll draws each
/// upload different lighting.</para>
/// </summary>
public sealed unsafe class SceneLightingUboBinding : IDisposable
{
private readonly GL? _gl;
private readonly ICurrentGpuFrameSource? _frames;
private readonly WorldFrameSections? _sections;
private uint _ubo;
private readonly List<uint>[] _buffersByFrame = [[], [], []];
private int _frameSlot;
private int _bufferCursor;
private readonly ICurrentGpuFrameSource _frames;
private readonly WorldFrameSections _sections;
private bool _frameStarted;
private bool _disposed;
internal int DynamicBufferCount => _buffersByFrame.Sum(buffers => buffers.Count);
public SceneLightingUboBinding(GL gl)
{
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
}
/// <summary>
/// Campaign V slice V6j: the RHI arm.
///
/// <para>GL keeps the block bound at a global uniform binding point and every
/// shader inherits it. Vulkan has no such point — a descriptor set is bound
/// per draw and the renderer's own binds select the scope this block has to
/// land in — so the upload becomes a frame ring slice PUBLISHED on
/// <see cref="WorldFrameSections.SceneLighting"/>, and each world renderer
/// binds it inside the pass after its own binds (plan §5.5.14 item 2).</para>
///
/// <para>The per-flight-slot buffer pool disappears with it: every allocation
/// within a frame is already distinct memory that lives until the frame
/// retires, which is the property the pool existed to provide when the world,
/// portal-space and paperdoll draws each upload different lighting.</para>
/// The GL arm's per-flight-slot buffer pool this used to report on was
/// deleted at Campaign V slice V11: every allocation now comes from a ring
/// that lives until its frame retires, so there is no persistent
/// dynamic-buffer pool left to size.
/// </summary>
internal int DynamicBufferCount => 0;
internal SceneLightingUboBinding(
ICurrentGpuFrameSource frames,
WorldFrameSections sections)
@ -71,80 +49,20 @@ public sealed unsafe class SceneLightingUboBinding : IDisposable
/// </summary>
public void BeginFrame(int frameSlot)
{
if ((uint)frameSlot >= (uint)_buffersByFrame.Length)
throw new ArgumentOutOfRangeException(nameof(frameSlot));
_frameSlot = frameSlot;
_bufferCursor = 0;
ArgumentOutOfRangeException.ThrowIfNegative(frameSlot);
_frameStarted = true;
}
private void ActivateNextBuffer()
{
if (!_frameStarted)
throw new InvalidOperationException("BeginFrame must be called before uploading scene lighting.");
if (_gl is null)
throw new InvalidOperationException("The RHI arm publishes a ring section rather than a buffer.");
List<uint> buffers = _buffersByFrame[_frameSlot];
if (_bufferCursor == buffers.Count)
{
uint buffer = TrackedGlResource.CreateBuffer(
_gl,
"SceneLighting frame UBO creation");
try
{
TrackedGlResource.AllocateBufferStorage(
_gl,
GLEnum.UniformBuffer,
buffer,
0,
SceneLightingUbo.SizeInBytes,
GLEnum.DynamicDraw,
"SceneLighting frame UBO allocation");
buffers.Add(buffer);
}
catch
{
TrackedGlResource.DeleteBuffer(
_gl,
buffer,
0,
"SceneLighting frame UBO rollback");
throw;
}
}
_ubo = buffers[_bufferCursor++];
}
/// <summary>
/// Push the current frame's UBO contents to the GPU. Cheap (576 bytes)
/// so fine to call unconditionally every frame.
/// </summary>
public void Upload(SceneLightingUbo data)
{
if (_gl is null)
{
PublishSection(data);
return;
}
ActivateNextBuffer();
_gl.BindBuffer(BufferTargetARB.UniformBuffer, _ubo);
_gl.BufferSubData(BufferTargetARB.UniformBuffer,
(nint)0, (nuint)SceneLightingUbo.SizeInBytes, &data);
_gl.BindBufferBase(BufferTargetARB.UniformBuffer,
SceneLightingUbo.BindingPoint, _ubo);
_gl.BindBuffer(BufferTargetARB.UniformBuffer, 0);
}
private void PublishSection(SceneLightingUbo data)
{
if (!_frameStarted)
throw new InvalidOperationException("BeginFrame must be called before uploading scene lighting.");
IGpuFrame frame = _frames!.CurrentFrame
IGpuFrame frame = _frames.CurrentFrame
?? throw new InvalidOperationException(
"Scene lighting requires an open IGpuFrame (see GpuDeviceFrameLifetime).");
GpuRingAllocation allocation = frame.AllocateRing(
@ -152,30 +70,19 @@ public sealed unsafe class SceneLightingUboBinding : IDisposable
GpuRingUsage.Uniform);
new ReadOnlySpan<byte>(&data, SceneLightingUbo.SizeInBytes)
.CopyTo(allocation.Data);
_sections!.SceneLighting = new GpuBufferSection(
_sections.SceneLighting = new GpuBufferSection(
allocation.Buffer,
allocation.OffsetBytes,
(uint)SceneLightingUbo.SizeInBytes);
}
/// <summary>
/// No-op: this binding owns no GPU resource of its own on the RHI arm — the
/// deleted GL arm's per-flight buffer pool was the only thing to release.
/// Kept as a method so callers that hold this behind an <see cref="IDisposable"/>
/// reference (composition's acquisition scope) don't need a special case.
/// </summary>
public void Dispose()
{
if (_disposed) return;
if (_gl is not null)
{
foreach (List<uint> buffers in _buffersByFrame)
{
foreach (uint buffer in buffers)
{
TrackedGlResource.DeleteBuffer(
_gl,
buffer,
SceneLightingUbo.SizeInBytes,
"SceneLighting frame UBO disposal");
}
buffers.Clear();
}
}
_disposed = true;
}
}