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

@ -9,7 +9,6 @@ using DatReaderWriter;
using AcDream.Content;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering.Sky;
@ -43,46 +42,11 @@ namespace AcDream.App.Rendering.Sky;
/// measured clockwise from north.
/// </para>
/// </summary>
public sealed unsafe partial class SkyRenderer : IDisposable
public sealed partial class SkyRenderer : IDisposable
{
private readonly GL? _gl;
private readonly IDatReaderWriter _dats;
private readonly Shader? _shader;
private readonly TextureCache _textures;
private readonly SamplerCache? _samplers;
// Campaign V slice V6e. Two GL objects replace what used to be a run of
// glUniform* calls and a texture/sampler binding on unit 0:
//
// _paramsUbo — the std140 SkyParams block both stages declare.
// _uTextureIndexALoc — the push-constant-named uniform carrying the slot.
//
// The wrap mode is what makes the (texture, wrap) pairing necessary. A
// bindless handle BAKES its sampler state, so the per-submesh
// Repeat-vs-ClampToEdge choice that used to be a glBindSampler call has to be
// a different handle — which is also exactly how the Vulkan table works,
// where an entry is a combined image sampler. ManagedGLTextureArray has
// interned wrap/clamp handle pairs the same way since the world path went
// bindless.
//
// Campaign V slice V6k retired the interim per-renderer GlBindlessHandleTable
// — the last one in the tree — for the device's own retirement-gated table,
// exactly as V4t did for the other four world renderers. The handles are
// still minted here, because the sky is the one world path whose textures it
// produces itself; only the slot allocator moved.
private readonly Wb.BindlessSupport? _bindless;
/// <summary>
/// Campaign V slice V6k: the GL arm's slot allocator. The sky mints its own
/// resident handles — it is the one world path that does — so it registers
/// them into the device's table through V4t's world-handle seam instead of
/// keeping the last private <c>GlBindlessHandleTable</c> in the tree.
/// </summary>
private readonly AcDream.App.Rendering.Gpu.Gl.GlGpuDevice? _glDevice;
private readonly Dictionary<(uint Texture, bool Repeat), ulong> _handleByTextureAndWrap = new();
private uint _paramsUbo;
private int _uTextureIndexALoc = -1;
private SkyParams _params;
// Lazily-built GPU resources per sky-GfxObj.
@ -125,40 +89,6 @@ public sealed unsafe partial class SkyRenderer : IDisposable
public float Near { get; set; } = 0.1f;
public float Far { get; set; } = 1_000_000f;
internal SkyRenderer(
GL gl,
IDatReaderWriter dats,
Shader shader,
TextureCache textures,
SamplerCache samplers,
Wb.BindlessSupport bindless,
AcDream.App.Rendering.Gpu.Gl.GlGpuDevice device)
{
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
_shader = shader ?? throw new ArgumentNullException(nameof(shader));
_textures = textures ?? throw new ArgumentNullException(nameof(textures));
_samplers = samplers ?? throw new ArgumentNullException(nameof(samplers));
_bindless = bindless ?? throw new ArgumentNullException(nameof(bindless));
_glDevice = device ?? throw new ArgumentNullException(nameof(device));
_uTextureIndexALoc = _gl.GetUniformLocation(_shader.Program, "uTextureIndexA");
_paramsUbo = Wb.TrackedGlResource.CreateBuffer(_gl, "sky params UBO creation");
// Fixed size: SkyParams is a 256-byte std140 struct, so the buffer never
// grows and every write is one whole-struct BufferSubData.
_gl.BindBuffer(BufferTargetARB.UniformBuffer, _paramsUbo);
Wb.TrackedGlResource.AllocateBufferStorage(
_gl,
BufferTargetARB.UniformBuffer,
_paramsUbo,
0,
SkyParams.SizeInBytes,
BufferUsageARB.DynamicDraw,
"allocating sky params UBO");
_gl.BindBuffer(BufferTargetARB.UniformBuffer, 0);
}
/// <summary>
/// Draw all NON-WEATHER sky objects (dome, sun, moon, stars, clouds —
/// every <c>SkyObject</c> with <c>Properties &amp; 0x04 == 0</c>).
@ -265,7 +195,6 @@ public sealed unsafe partial class SkyRenderer : IDisposable
skyView.M42 = 0f;
skyView.M43 = 0f;
_shader?.Use();
// Campaign V slice V6e: the values below used to be individual
// glUniform* calls. They now populate the std140 SkyParams block, which
// is uploaded once per submesh right before its draw — the same cadence
@ -283,34 +212,6 @@ public sealed unsafe partial class SkyRenderer : IDisposable
_params.SunDir =
AcDream.Core.World.SkyStateProvider.SunDirectionFromKeyframe(keyframe);
bool wasCullFace = false;
if (_gl is { } gl)
{
gl.BindBufferBase(
BufferTargetARB.UniformBuffer,
AcDream.App.Rendering.Gpu.GpuBindingModel.UniformSkyParams,
_paramsUbo);
// Save + override GL state.
gl.DepthMask(false);
gl.Disable(EnableCap.DepthTest);
// Save + disable CullFace for the sky pass; restore at the end.
// Mirrors TextRenderer.cs's save/restore pattern. Without this the
// sky pass left CullFace disabled regardless of its prior state,
// which is benign today (the global convention in this codebase is
// off and subsequent renderers manage their own CullFace) but
// would break the moment any future caller assumes back-face
// culling stays on across the sky pass.
wasCullFace = gl.IsEnabled(EnableCap.CullFace);
gl.Disable(EnableCap.CullFace);
gl.Enable(EnableCap.Blend);
// Default blend — overridden per-submesh inside the inner loop.
// Additive surfaces (sun/moon/stars via SurfaceType.Additive =
// 0x10000) get GL_SRC_ALPHA / GL_ONE; alpha-blended (clouds, dome
// with Alpha flag) get GL_SRC_ALPHA / GL_ONE_MINUS_SRC_ALPHA.
gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
}
// Look up the keyframe's override list so we can apply
// SkyObjReplace (r12 §2.3): per-keyframe GfxObj swaps + rotation
// override + transparency fade + luminosity cap.
@ -434,14 +335,6 @@ public sealed unsafe partial class SkyRenderer : IDisposable
// mesh pipeline where Surface flags dictate state. On the RHI
// arm the same two blend functions are two pipelines, because
// Vulkan bakes blend rather than making it dynamic.
if (_gl is { } blendGl)
{
if (sub.IsAdditive)
blendGl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One);
else
blendGl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
}
// Emissive source picks the surface's authored Luminosity by
// default; the per-keyframe replace data can OVERRIDE
// (rep.Luminosity > 0) or CAP (rep.MaxBright). This matches
@ -532,46 +425,9 @@ public sealed unsafe partial class SkyRenderer : IDisposable
|| obj.TexVelocityX != 0f
|| obj.TexVelocityY != 0f;
uint slot = TextureTableSlot(sub.SurfaceId, needsRepeat);
if (_gl is { } drawGl)
{
drawGl.ProgramUniform1(
_shader!.Program,
_uTextureIndexALoc,
slot);
UploadParams();
FlushAndBindTextureTable();
drawGl.BindVertexArray(sub.Vao);
drawGl.DrawElements(PrimitiveType.Triangles,
(uint)sub.IndexCount,
DrawElementsType.UnsignedInt,
(void*)0);
}
else
{
DrawSubMeshRhi(sub, slot);
}
DrawSubMeshRhi(sub, slot);
}
}
// Restore GL state expected by the rest of the pipeline.
//
// Slice V6e removed the glBindSampler(0, …) that used to happen here
// and its matching unbind. The sky no longer touches texture unit 0 at
// all — its wrap mode rides inside the bindless handle — so there is
// nothing left to leak onto the next renderer's unit 0. That unbind was
// load-bearing precisely because the binding was global state; a table
// slot is not.
if (_gl is { } restoreGl)
{
restoreGl.Disable(EnableCap.Blend);
restoreGl.DepthMask(true);
restoreGl.Enable(EnableCap.DepthTest);
if (wasCullFace) restoreGl.Enable(EnableCap.CullFace);
restoreGl.BindVertexArray(0);
}
}
/// <summary>
@ -588,57 +444,8 @@ public sealed unsafe partial class SkyRenderer : IDisposable
/// handle table in the codebase — the sky's texture set is a fixed handful
/// per day group and does not churn.</para>
/// </summary>
private uint TextureTableSlot(uint surfaceId, bool repeat)
{
if (_gl is null)
return RhiTextureTableSlot(surfaceId, repeat);
uint textureName = _textures.GetOrUpload(surfaceId);
var key = (textureName, repeat);
if (!_handleByTextureAndWrap.TryGetValue(key, out ulong handle))
{
handle = _bindless!.GetResidentHandle(
textureName,
repeat ? _samplers!.Wrap : _samplers!.Clamp);
_handleByTextureAndWrap.Add(key, handle);
}
return _glDevice!.RegisterWorldTextureHandle(handle).Index;
}
/// <summary>
/// Writes the current <see cref="SkyParams"/> to its uniform buffer. Called
/// immediately before each draw, which is the cadence the per-submesh
/// glUniform* calls it replaces already had.
/// </summary>
private void UploadParams()
{
GL gl = _gl!;
fixed (void* p = &_params)
{
gl.BindBuffer(BufferTargetARB.UniformBuffer, _paramsUbo);
gl.BufferSubData(
BufferTargetARB.UniformBuffer, 0, (nuint)SkyParams.SizeInBytes, p);
}
}
/// <summary>
/// Campaign V slice V6k: drains the device texture table's dirty runs and
/// (re)binds it at
/// <see cref="AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable"/>.
/// Mirrors <c>ParticleRenderer.FlushAndBindTextureTable</c>, including its
/// reason for running per draw rather than per pass: a submesh partway
/// through the sky stack can be the first to ask for a wrap mode.
/// </summary>
private void FlushAndBindTextureTable()
{
AcDream.App.Rendering.Gpu.Gl.GlGpuDevice device = _glDevice!;
device.FlushTextureTable();
_gl!.BindBufferBase(
BufferTargetARB.ShaderStorageBuffer,
AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable,
device.TextureTableGlName);
}
private uint TextureTableSlot(uint surfaceId, bool repeat) =>
RhiTextureTableSlot(surfaceId, repeat);
/// <summary>
/// Find the <see cref="SkyObjectReplaceData"/> entries for the
@ -865,106 +672,9 @@ public sealed unsafe partial class SkyRenderer : IDisposable
}
}
private SubMeshGpu UploadSubMesh(GfxObjSubMesh sm)
{
if (_gl is null)
return UploadSubMeshRhi(sm);
private SubMeshGpu UploadSubMesh(GfxObjSubMesh sm) => UploadSubMeshRhi(sm);
uint vao = _gl.GenVertexArray();
_gl.BindVertexArray(vao);
uint vbo = _gl.GenBuffer();
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, vbo);
fixed (void* p = sm.Vertices)
_gl.BufferData(BufferTargetARB.ArrayBuffer,
(nuint)(sm.Vertices.Length * sizeof(Vertex)), p, BufferUsageARB.StaticDraw);
uint ebo = _gl.GenBuffer();
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, ebo);
fixed (void* p = sm.Indices)
_gl.BufferData(BufferTargetARB.ElementArrayBuffer,
(nuint)(sm.Indices.Length * sizeof(uint)), p, BufferUsageARB.StaticDraw);
uint stride = (uint)sizeof(Vertex);
_gl.EnableVertexAttribArray(0);
_gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, stride, (void*)0);
_gl.EnableVertexAttribArray(1);
_gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float)));
_gl.EnableVertexAttribArray(2);
_gl.VertexAttribPointer(2, 2, VertexAttribPointerType.Float, false, stride, (void*)(6 * sizeof(float)));
_gl.BindVertexArray(0);
// Classify blend mode from the Surface's flags. Sun/moon/stars with
// `SurfaceType.Additive = 0x10000` get GL_ONE / GL_ONE (their texture
// has a black background and a bright body; additive makes the
// background contribute nothing and the body glow on top of the sky).
//
// NOTE: earlier revision also treated `SurfaceType.Luminous = 0x40`
// as additive, but that flag is present on the sky DOME itself and
// on cloud sheets — turning those additive blew the whole sky to
// white. `Luminous` means "self-illuminated / unshaded" in retail's
// render pipeline, not "additive blend". Only the Additive bit
// toggles the blend mode.
bool isAdditive = sm.Translucency == TranslucencyKind.Additive;
return new SubMeshGpu
{
Vao = vao,
Vbo = vbo,
Ebo = ebo,
IndexCount = sm.Indices.Length,
SurfaceId = sm.SurfaceId,
IsAdditive = isAdditive,
SurfLuminosity = sm.Luminosity,
SurfDiffuse = sm.Diffuse,
NeedsUvRepeat = sm.NeedsUvRepeat,
SurfOpacity = sm.SurfOpacity,
DisableFog = sm.DisableFog,
};
}
public void Dispose()
{
if (_gl is null)
{
DisposeRhi();
return;
}
foreach (var subs in _gpuByGfxObj.Values)
{
foreach (var sub in subs)
{
_gl.DeleteBuffer(sub.Vbo);
_gl.DeleteBuffer(sub.Ebo);
_gl.DeleteVertexArray(sub.Vao);
}
}
_gpuByGfxObj.Clear();
// Campaign V slice V6e. Residency first: a handle made resident pins
// its texture's GPU address, and the textures themselves belong to
// TextureCache, which outlives this renderer and disposes them itself.
//
// Slice V6k: the device's table entry is retired alongside it, in the
// order V4t established — the table entry goes first, because the slot
// is only recycled once the frames that could still read it retire,
// whereas the handle stops being valid the instant it is non-resident.
foreach (ulong handle in _handleByTextureAndWrap.Values)
{
_glDevice!.ReleaseWorldTextureHandle(handle);
_bindless!.MakeNonResident(handle);
}
_handleByTextureAndWrap.Clear();
if (_paramsUbo != 0)
{
Wb.TrackedGlResource.DeleteBuffer(
_gl, _paramsUbo, SkyParams.SizeInBytes, "sky params UBO disposal");
_paramsUbo = 0;
}
}
public void Dispose() => DisposeRhi();
/// <summary>
/// Campaign V slice V6e: the CPU mirror of sky.{vert,frag}'s <c>SkyParams</c>
@ -999,14 +709,11 @@ public sealed unsafe partial class SkyRenderer : IDisposable
private sealed class SubMeshGpu
{
public uint Vao;
public uint Vbo;
public uint Ebo;
/// <summary>
/// Campaign V slice V6k: the RHI arm's vertex source. The sky's meshes are
/// built once per GfxObj and never change, so each submesh owns a
/// device-local buffer pair rather than taking a ring slice per frame.
/// Null on the GL arm, which uses <see cref="Vao"/>.
/// The raw-GL arm's VAO/VBO/EBO names were deleted at slice V11.
/// </summary>
public AcDream.App.Rendering.Gpu.IGpuBuffer? VertexBuffer;
public AcDream.App.Rendering.Gpu.IGpuBuffer? IndexBuffer;