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>
254 lines
9.7 KiB
C#
254 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;
|
|
using Silk.NET.OpenGL;
|
|
|
|
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> exists. §5.5.12 item 6 measured how wide the
|
|
/// dependency really is: 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 deliberately proves construction and nothing more. The upload bodies
|
|
/// are still raw GL and the world renderers still bind a GL handle table; both
|
|
/// belong to the slice that draws Dereth on Vulkan. What matters here is that
|
|
/// each of those now fails at the site that needs GL, naming why, instead of
|
|
/// throwing a cast before the constructor has run a statement.</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 GL? Gl => null;
|
|
|
|
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 — seven members measured out
|
|
/// of a 760-line class. 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(
|
|
[
|
|
"Gl",
|
|
"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.Null(vulkanDevice.Gl);
|
|
Assert.True(vulkanDevice.HasBindless);
|
|
Assert.True(vulkanDevice.HasOpenGL43);
|
|
Assert.False(vulkanDevice.HasPendingWork);
|
|
Assert.Equal(0u, vulkanDevice.InstanceVBO);
|
|
Assert.Same(device.Retirement, vulkanDevice.ResourceRetirement);
|
|
}
|
|
}
|