acdream/tests/AcDream.App.Tests/Rendering/Wb/MeshPipelineDeviceSeamTests.cs
Erik 7a0227c12e feat(render): Vulkan campaign V11 step 3 — drop the GL packages and shaders
Commit 2 deleted the GL rendering backend's implementations; this step
removes the package references and shader vocabulary they leave behind,
so nothing in the App project still spells Silk.NET.OpenGL.

Silk.NET.OpenGL and Silk.NET.OpenGL.Extensions.ARB are dropped from
AcDream.App.csproj. Chorizite.Core stays — the audit is NOT clean: its
Render.Enums (TextureFormat, BufferUsage) and Lib.BoundingBox types are
used directly and extensively across the Wb texture/mesh pipeline,
independent of the deleted GL IUniformBuffer implementers the package
comment used to cite. The stale comment is corrected in place.

IMeshPipelineDevice.Gl is removed along with the GL? gl parameter
threaded through WbMeshAdapter's four constructors, WorldRenderComposition's
CreateMeshAdapter, and VulkanMeshPipelineDevice's Gl => null
implementation — nothing read any of them once the legacy per-mesh
upload bodies were gone (confirmed by grep: the sole non-doc-comment hit
was a test assertion). While in WbMeshAdapter.Dispose(), found and fixed
a real bug along the way: its teardown still pattern-matched the deleted
GL GpuFrameFlightController to decide whether to wait for submitted work,
which VulkanFrameFlightController replaced at slice V6a without this site
being updated — so the wait had been silently dead on every Vulkan run
since then. Retargeted to VulkanFrameFlightController, which carries the
same WaitForSubmittedWork().

The GL pixel-format vocabulary (Silk.NET.OpenGL.PixelFormat/PixelType) that
WorldTextureArray/TextureFormatExtensions/TextureAtlasManager used for
upload validation is replaced by AcDream.Content's existing Silk.NET-free
UploadPixelFormat/UploadPixelType enums (added at MP1a to keep the bake
tool GL-free); two new members (Rgb, Red, Float) extend that enum with
their GL ABI constants to cover the full vocabulary WorldTextureArray
needs, since MP1a's original set only covered what the extractor itself
emits. ObjectMeshManager's App-boundary cast
`(Silk.NET.OpenGL.PixelFormat?)batch.UploadPixelFormat` becomes a direct
pass-through now that both sides share the type.

GpuBindingModel.StorageTextureTable (the GL-only binding=9 emulation of
the Vulkan texture table) is deleted and StorageBindingCount drops from
10 to 9; the descriptor-set-layout code that builds from that count
(VulkanPipelineLayouts, VulkanFrameBindings) is untouched and just
allocates one fewer always-dummy-seeded, always-unused binding.

Several fully dead GL-only classes came along for the ride, confirmed by
zero construction sites: SilkFramebufferViewportTarget
(NullFramebufferViewportTarget is the sole production
IFramebufferViewportTarget), SilkRenderGlStateReader
(NullRenderGlStateReader.Instance is the sole IRenderGlStateReader),
RuntimeRenderFrameClearPhase (VulkanRenderFrameClearPhase is the sole
IRenderFrameClearPhase, expressing the same atmosphere-clear logic as a
pass load-op instead), and GpuFrameTimer plus FrameProfiler's
GL-owning FrameBoundary(GL) overload and BeginGpuFrame/EndGpuFrame
bracket (RecordGpuSample is the only GPU-timing path any backend uses
now — the ACDREAM_WB_DIAG nested-query exclusion these existed for no
longer applies, since WbDrawDispatcher's own diagnostic GPU sampling
already moved to the device's Vulkan timer pool). GpuFrameFlightController
itself stays (never constructed with a real fence API in production, but
its retirement-ledger/serial-ring logic is backend-neutral and still
covered by its own unit tests) — only its GL-specific parts (the public
GL constructor overload, SilkGpuFenceApi) are deleted, since removing the
whole class would mean restructuring the frozen Slice-8 composition
shape's GpuFrameFlightController? threading, which is out of this
commit's scope. TextureParameters.cs and BufferUsageExtensions.cs
(zero callers each) are deleted outright.

common.glsl is deleted: nothing in the actual Vulkan .spv build reads
it. tools/ShaderCompiler/Program.cs compiles each .vert/.frag pair
directly and tools/ShaderCompiler/VulkanGlslPreamble.cs injects its own
complete self-contained preamble per file; common.glsl's textual
concatenation was exclusively Shader.cs's GL-only mechanism, deleted at
Commit 2. The five shader files that named it in comments
(mesh_modern.vert, particle.vert, particle.frag, sky.frag,
terrain_modern.frag) are corrected to point at VulkanGlslPreamble.cs
instead. mesh.vert/mesh.frag — the pre-N.5 legacy shader pair the
mandatory modern path already made unreachable, with zero C# consumers
and no compiled .spv — are deleted too. Regenerated via
tools/compile-shaders.ps1: 9/9 remaining shader pairs compile
(previously 9/10, with mesh the sole failure — the VulkanShaderManifestTests
doc comment's "nine of ten are not Vulkan-expressible" was already
stale before this commit).

Test fallout: dead-subject test methods/files are deleted rather than
patched (TextRendererFailureSafetyTests.cs, ClipFrameUploadTests.cs,
GpuResourceRetirementTransactionTests.cs's GL queue tests, one
WorldRenderDiagnosticsTests source-order test, one
RenderFrameResourceControllerTests clear-phase-order test); tests whose
subject moved or was renamed are updated in place rather than deleted
(GpuContractTests, VulkanCapabilityGateTests, MeshPipelineDeviceSeamTests'
pinned seven-member surface now reads six, ParticleBindlessInstanceTests'
cross-dialect check now covers the one surviving dialect,
WbMeshAdapterTests' misleadingly-named null-gl test — gpuDevice was
always the parameter that actually threw).

Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors,
with the Silk.NET.OpenGL/.Extensions.ARB package references physically
removed from the csproj (not just unreferenced in code).
Tests: full-solution `dotnet test` green across every project.
Zero remaining `using Silk.NET.OpenGL` anywhere in src/ or tests/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 02:58:15 +02:00

251 lines
9.7 KiB
C#

using System;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Wb;
using AcDream.App.Tests.Rendering.Gpu;
using System.Threading;
using AcDream.Content;
using Chorizite.Core.Render.Enums;
using Microsoft.Extensions.Logging.Abstractions;
namespace AcDream.App.Tests.Rendering.Wb;
/// <summary>
/// Campaign V slice V6i-2: the mesh pipeline no longer names a backend.
///
/// <para>Plan §5.5.10 recorded the blocker as a fact about types — "WbMeshAdapter
/// owns an OpenGLGraphicsDevice, so it is not constructible on Vulkan" — which is
/// why <c>NullWbMeshAdapter</c> existed. §5.5.12 item 6 measured how wide the
/// dependency really was: a GL context, the retirement queue, the instance VBO,
/// and two capability flags. This suite proves the interface at that surface is
/// load-bearing rather than cosmetic, by building the object graph against a
/// device that has NO GL context at all.</para>
///
/// <para>It originally proved construction and nothing more, back when the
/// upload bodies were still raw GL and the world renderers still bound a GL
/// handle table. Campaign V slice V11 deleted both along with the rest of the
/// raw-GL arm (and the interface's own <c>Gl</c> member, which nothing read any
/// more once they were gone) — the arena-build and upload tests below now cover
/// what those slices only asserted would eventually fail loudly.</para>
/// </summary>
public sealed class MeshPipelineDeviceSeamTests
{
/// <summary>A device with the mesh pipeline's whole surface and no GL behind it.</summary>
private sealed class ContextFreeMeshPipelineDevice(
IGpuResourceRetirementQueue retirement,
bool modernPath = false)
: IMeshPipelineDevice
{
public IGpuResourceRetirementQueue ResourceRetirement { get; } = retirement;
public uint InstanceVBO => 0;
public bool HasBindless => modernPath;
public bool HasOpenGL43 => modernPath;
public bool HasPendingWork => false;
public int ProcessedQueues { get; private set; }
public void ProcessQueue() => ProcessedQueues++;
public void Dispose()
{
}
}
private static ObjectMeshManager Build(
RecordingGpuDevice device,
bool modernPath = false) =>
new(
new ContextFreeMeshPipelineDevice(device.Retirement, modernPath),
device,
new NullPreparedAssetSource(),
NullLogger<ObjectMeshManager>.Instance);
private sealed class NullPreparedAssetSource : IPreparedAssetSource
{
public PreparedAssetSourceStats Stats => default;
public CacheStats DecodedTextureCacheStats => default;
public PreparedAssetPresence Probe(
AcDream.Content.Pak.PakAssetType type,
uint sourceFileId) =>
PreparedAssetPresence.Missing;
public PreparedAssetReadResult Read(
in PreparedAssetRequest request,
CancellationToken cancellationToken = default) =>
PreparedAssetReadResult.Missing;
public void Dispose()
{
}
}
/// <summary>
/// The whole point. Before this slice the constructor downcast the RHI device
/// to <c>GlGpuDevice</c>, so this threw before running a statement.
/// </summary>
[Fact]
public void TheMeshPipelineConstructsAgainstADeviceWithNoGlContext()
{
using var device = new RecordingGpuDevice();
using ObjectMeshManager manager = Build(device);
Assert.False(manager.IsDisposed);
}
/// <summary>
/// The one branch the texture stack keeps: a GL pair yields the GL arm, and
/// anything else yields the RHI arm. Selection happens once, at construction.
/// </summary>
[Fact]
public void TheArrayFactorySelectsTheRhiArmWithoutAGlPair()
{
using var device = new RecordingGpuDevice();
IWorldTextureArrayFactory arrays = IWorldTextureArrayFactory.For(
new ContextFreeMeshPipelineDevice(device.Retirement),
device,
NullLogger.Instance);
Assert.IsType<RhiWorldTextureArrayFactory>(arrays);
using IWorldTextureArray array =
arrays.CreateClampedArray(TextureFormat.RGBA8, 32, 32, 2);
Assert.IsType<RhiWorldTextureArray>(array);
}
/// <summary>
/// The seam's whole value is that it is NARROW — six members measured out
/// of a 760-line class (seven until Campaign V slice V11 deleted the unread
/// <c>Gl</c> member). A later slice that quietly widens it back out would
/// re-couple the mesh pipeline to a backend without any other gate noticing,
/// so the member set is pinned rather than described.
/// </summary>
[Fact]
public void TheDeviceSeamStaysAtTheMeasuredSurface()
{
string[] members =
[
.. typeof(IMeshPipelineDevice)
.GetMembers()
// Property accessors are the same members under another name.
.Where(member => member is not System.Reflection.MethodInfo
{
IsSpecialName: true,
})
.Select(member => member.Name)
.Order(StringComparer.Ordinal),
];
Assert.Equal(
[
"HasBindless",
"HasOpenGL43",
"HasPendingWork",
"InstanceVBO",
"ProcessQueue",
"ResourceRetirement",
],
members);
}
/// <summary>
/// Construction touched no GL object at all. The shared mesh arena is the
/// only one the constructor would build, and it is gated on the two
/// capability flags the interface carries — so a device reporting neither
/// leaves it absent rather than dereferencing a null context.
/// </summary>
[Fact]
public void ConstructionBuildsNoGlObject()
{
using var device = new RecordingGpuDevice();
using ObjectMeshManager manager = Build(device);
Assert.Null(manager.GlobalBuffer);
// Read-only policy queries still answer, which is what lets streaming
// residence accounting keep running on a backend with no world draws.
Assert.Equal((0, 0, 0), manager.GetPendingTextureUpdateStats());
}
/// <summary>
/// Campaign V slice V6i-3. V6i-2 could only prove construction, because the
/// arena's own body still spoke GL — a device reporting the modern-path
/// capabilities and no context would have dereferenced a null one. It now
/// builds, and what it publishes is the contract's handle rather than a raw
/// name: no vertex array, two live stores.
/// </summary>
[Fact]
public void TheModernArenaBuildsWithoutAGlContext()
{
using var device = new RecordingGpuDevice();
using ObjectMeshManager manager = Build(device, modernPath: true);
GlobalMeshBuffer arena = Assert.IsType<GlobalMeshBuffer>(manager.GlobalBuffer);
Assert.True(arena.HasStores);
Assert.NotNull(arena.VertexStore);
Assert.NotNull(arena.IndexStore);
}
/// <summary>
/// And it UPLOADS. The vertex and index bytes land in the stores a pass
/// encoder binds, at the offsets the allocator handed out — which is the
/// whole of what a draw needs from this class and the thing V6i-2 could not
/// claim.
/// </summary>
[Fact]
public void AMeshUploadsIntoTheArenaWithoutAGlContext()
{
using var device = new RecordingGpuDevice();
using ObjectMeshManager manager = Build(device, modernPath: true);
GlobalMeshBuffer arena = manager.GlobalBuffer!;
var vertices = new VertexPositionNormalTexture[3];
vertices[0].Position = new System.Numerics.Vector3(1f, 2f, 3f);
vertices[2].Position = new System.Numerics.Vector3(7f, 8f, 9f);
ushort[] indices = [0, 1, 2];
GlobalMeshAllocation allocation = arena.UploadMesh(vertices, [indices]);
Assert.Equal(3, allocation.Vertices.Length);
Assert.Equal(3, allocation.Indices.Length);
Assert.Equal(1, arena.UploadCount);
Span<byte> readback = stackalloc byte[3 * VertexPositionNormalTexture.Size];
arena.VertexStore!.Read(
(long)allocation.Vertices.Offset * VertexPositionNormalTexture.Size,
readback);
var uploaded = System.Runtime.InteropServices.MemoryMarshal
.Cast<byte, VertexPositionNormalTexture>(readback);
Assert.Equal(new System.Numerics.Vector3(1f, 2f, 3f), uploaded[0].Position);
Assert.Equal(new System.Numerics.Vector3(7f, 8f, 9f), uploaded[2].Position);
Span<byte> indexBytes = stackalloc byte[3 * sizeof(ushort)];
arena.IndexStore!.Read((long)allocation.Indices.Offset * sizeof(ushort), indexBytes);
Assert.Equal(
indices,
System.Runtime.InteropServices.MemoryMarshal.Cast<byte, ushort>(indexBytes).ToArray());
}
/// <summary>
/// The production Vulkan implementation of the seam, checked against the
/// same surface. Its two capability flags answer true because what they
/// gate is the modern path, which Vulkan supplies unconditionally — see the
/// type's own documentation for why the GL-shaped names survive.
/// </summary>
[Fact]
public void TheVulkanMeshPipelineDeviceReportsTheModernPath()
{
using var device = new RecordingGpuDevice();
using var vulkanDevice =
new AcDream.App.Rendering.Gpu.Vk.VulkanMeshPipelineDevice(device.Retirement);
Assert.True(vulkanDevice.HasBindless);
Assert.True(vulkanDevice.HasOpenGL43);
Assert.False(vulkanDevice.HasPendingWork);
Assert.Equal(0u, vulkanDevice.InstanceVBO);
Assert.Same(device.Retirement, vulkanDevice.ResourceRetirement);
}
}