refactor(render): Campaign V slice V6i-2 commit 3 — the mesh pipeline stops naming a backend
Plan §5.5.10 recorded the blocker as a fact about types: "WbMeshAdapter owns an
OpenGLGraphicsDevice, so it is not constructible on Vulkan until slice V4t" —
which is the entire reason NullWbMeshAdapter exists. §5.5.12 item 6 then measured
how wide that dependency really is, and the answer is seven members out of a
760-line class: a GL context, the retirement queue, the shared instance VBO, and
two capability flags.
IMeshPipelineDevice is exactly that surface. OpenGLGraphicsDevice declares it and
every member already existed under a GL-specific name, so the shipping backend
executes not one changed statement — these are aliases, not behaviour.
Two casts moved, and they are what actually blocked construction:
- ObjectMeshManager downcast IGpuDevice to GlGpuDevice in its CONSTRUCTOR, so a
Vulkan-composed pipeline threw before running a statement. V4t put it there
because the class registered bindless handles itself; commit 2 moved that into
the array, leaving the field a pass-through for the raw-GL renderers' handle
table. The cast now lives on that one property and names the backend it was
composed against instead of reporting a failed cast.
- The atlas array factory is selected by IWorldTextureArrayFactory.For, which is
the one place the texture stack branches on a backend.
MeshPipelineDeviceSeamTests proves the decoupling rather than describing it: it
builds ObjectMeshManager against a device whose Gl is null, asserts it constructs,
asserts construction built no GL object, asserts the handle table refuses by name,
and asserts the factory picks the RHI arm. A reflection test pins the seam's
member set so a later slice cannot quietly widen it back out — the whole value
here is that it is narrow.
What this does NOT claim: the mesh pipeline does not RUN on Vulkan. Its upload
bodies are still raw GL — GlobalMeshBuffer, the VAO/IBO construction, the layer
transfers — and they now fail through one RequireGl() accessor that names the
slice that owns porting them, instead of failing at construction. WbMeshAdapter
still creates an OpenGLGraphicsDevice in its GL constructor, because there is no
second implementation to create yet. Those bodies are items 3–5 of §5.5.12's
remainder list, along with RetailPViewPassExecutor and the three world renderers'
submission arms.
§5.5.13 reports the whole of V6i-2 and the slice table gains its V6i row.
Gates: Release build; App tests 4,109 / 3 skips (the 4,086 baseline plus 23 across
the three commits); complete Release suite 9,172 / 5; strict GL offline pixel gate
vs 0ca802cd 1.60e-05 (9 px of 563,200 — the low end of the documented 9–31 px
control band, and fewer than a same-commit control has measured); GL connected
tools/run-repeat-connected-gate.ps1 -Runs 3 at 3/3 RENDERED on the desktop witness
and 3/3 on the client capture; one Vulkan composition-host run with
VK_LAYER_KHRONOS_validation proven inserted by the loader at zero errors, zero
warnings, no [shutdown] diagnostic, and a captured frame.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c8d0f70bbe
commit
5b3d72a90c
7 changed files with 459 additions and 18 deletions
|
|
@ -0,0 +1,187 @@
|
|||
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)
|
||||
: IMeshPipelineDevice
|
||||
{
|
||||
public GL? Gl => null;
|
||||
|
||||
public IGpuResourceRetirementQueue ResourceRetirement { get; } = retirement;
|
||||
|
||||
public uint InstanceVBO => 0;
|
||||
|
||||
public bool HasBindless => false;
|
||||
|
||||
public bool HasOpenGL43 => false;
|
||||
|
||||
public bool HasPendingWork => false;
|
||||
|
||||
public int ProcessedQueues { get; private set; }
|
||||
|
||||
public void ProcessQueue() => ProcessedQueues++;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static ObjectMeshManager Build(RecordingGpuDevice device) =>
|
||||
new(
|
||||
new ContextFreeMeshPipelineDevice(device.Retirement),
|
||||
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 GL handle table is the emulation the Vulkan backend replaces with set
|
||||
/// 2, so asking a non-GL pipeline for it is a programming error — and it says
|
||||
/// which backend it was composed against rather than reporting a cast.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TheGlHandleTableIsRefusedByNameRatherThanCast()
|
||||
{
|
||||
using var device = new RecordingGpuDevice();
|
||||
using ObjectMeshManager manager = Build(device);
|
||||
|
||||
InvalidOperationException failure =
|
||||
Assert.Throws<InvalidOperationException>(() => manager.WorldTextureTable);
|
||||
Assert.Contains("GL-only", failure.Message, StringComparison.Ordinal);
|
||||
Assert.Contains(GpuBackendKind.Recording.ToString(), failure.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <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());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue