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
58
src/AcDream.App/Rendering/Wb/IMeshPipelineDevice.cs
Normal file
58
src/AcDream.App/Rendering/Wb/IMeshPipelineDevice.cs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
using AcDream.App.Rendering;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6i-2: everything the mesh pipeline actually needs from a
|
||||
/// graphics device.
|
||||
///
|
||||
/// <para>Plan §5.5.10 recorded the blocker plainly: "<c>WbMeshAdapter</c> owns an
|
||||
/// <c>OpenGLGraphicsDevice</c>, so it is not constructible on Vulkan" — which is
|
||||
/// why <c>NullWbMeshAdapter</c> exists at all. §5.5.12 item 6 then MEASURED how
|
||||
/// wide that dependency really is, and the answer is this: a GL context, the
|
||||
/// retirement queue, the shared instance VBO, and two capability flags. Seven
|
||||
/// members out of a 760-line class.</para>
|
||||
///
|
||||
/// <para>So the coupling is expressed as an interface at exactly that surface
|
||||
/// and <see cref="OpenGLGraphicsDevice"/> declares it — every member already
|
||||
/// existed, so the GL arm executes not one changed statement. What this buys is
|
||||
/// that <c>ObjectMeshManager</c> and <c>WbMeshAdapter</c> no longer NAME a
|
||||
/// backend, which is the prerequisite for the slice that gives them a second
|
||||
/// implementation.</para>
|
||||
///
|
||||
/// <para><b>What it does not yet buy, stated plainly.</b> <see cref="Gl"/> is
|
||||
/// still a GL type, because the mesh pipeline's upload bodies are still raw GL —
|
||||
/// <c>GlobalMeshBuffer</c>, the VAO/IBO construction, and the layer transfers all
|
||||
/// speak it directly. Those bodies are the pass-structure work items 3–5 of
|
||||
/// §5.5.12's remainder list own. This slice removes the TYPE-level blocker and
|
||||
/// names the rest; it does not claim the mesh pipeline runs on Vulkan today, and
|
||||
/// <see cref="Gl"/> being nullable is what will make the remaining sites fail
|
||||
/// loudly rather than silently when that arm is written.</para>
|
||||
/// </summary>
|
||||
internal interface IMeshPipelineDevice : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The GL context, or null on a backend that has none. Every reader is a
|
||||
/// raw-GL upload body awaiting its own port.
|
||||
/// </summary>
|
||||
GL? Gl { get; }
|
||||
|
||||
/// <summary>Frame-flight-gated release for everything the pipeline allocates.</summary>
|
||||
IGpuResourceRetirementQueue ResourceRetirement { get; }
|
||||
|
||||
/// <summary>The shared per-instance attribute buffer the legacy draw path binds.</summary>
|
||||
uint InstanceVBO { get; }
|
||||
|
||||
/// <summary><c>GL_ARB_bindless_texture</c>. Half of the modern-path gate.</summary>
|
||||
bool HasBindless { get; }
|
||||
|
||||
/// <summary>GL 4.3 or better. The other half of the modern-path gate.</summary>
|
||||
bool HasOpenGL43 { get; }
|
||||
|
||||
/// <summary>True while deferred device work is still queued.</summary>
|
||||
bool HasPendingWork { get; }
|
||||
|
||||
/// <summary>Runs deferred device work. Called once per frame from the render thread.</summary>
|
||||
void ProcessQueue();
|
||||
}
|
||||
|
|
@ -116,7 +116,25 @@ namespace AcDream.App.Rendering.Wb
|
|||
/// </summary>
|
||||
public class ObjectMeshManager : IDisposable
|
||||
{
|
||||
private readonly OpenGLGraphicsDevice _graphicsDevice;
|
||||
/// <summary>
|
||||
/// Campaign V slice V6i-2: the graphics device, no longer named by
|
||||
/// backend. See <see cref="IMeshPipelineDevice"/> for the measured
|
||||
/// surface this class actually needs, and for what still has to move
|
||||
/// before there is a second implementation of it.
|
||||
/// </summary>
|
||||
private readonly IMeshPipelineDevice _graphicsDevice;
|
||||
|
||||
/// <summary>
|
||||
/// The GL context this class's still-raw upload bodies write through.
|
||||
/// Null only on a backend with none, where every one of those bodies is
|
||||
/// a programming error rather than a runtime condition — the slice that
|
||||
/// ports them owns deleting this accessor.
|
||||
/// </summary>
|
||||
private GL RequireGl() =>
|
||||
_graphicsDevice.Gl
|
||||
?? throw new InvalidOperationException(
|
||||
"The mesh pipeline's upload bodies are still raw GL and this device has no "
|
||||
+ "context. Campaign V's world-draw slice owns porting them.");
|
||||
private readonly IPreparedAssetSource _preparedAssets;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
|
|
@ -129,9 +147,21 @@ namespace AcDream.App.Rendering.Wb
|
|||
/// composition wire, because a batch's slot and the table that resolves
|
||||
/// it must come from the same device by construction.
|
||||
/// </summary>
|
||||
private readonly AcDream.App.Rendering.Gpu.Gl.GlGpuDevice _worldTextureTable;
|
||||
private readonly AcDream.App.Rendering.Gpu.IGpuDevice _gpuDevice;
|
||||
|
||||
internal AcDream.App.Rendering.Gpu.Gl.GlGpuDevice WorldTextureTable => _worldTextureTable;
|
||||
/// <summary>
|
||||
/// Campaign V slice V6i-2: the downcast moved here from the constructor.
|
||||
/// Only the raw-GL world renderers reach this — the Vulkan backend binds
|
||||
/// set 2 and never touches the handle table — so a Vulkan-composed mesh
|
||||
/// pipeline can now be CONSTRUCTED, and only a caller that genuinely
|
||||
/// needs a GL handle table fails, naming why.
|
||||
/// </summary>
|
||||
internal AcDream.App.Rendering.Gpu.Gl.GlGpuDevice WorldTextureTable =>
|
||||
_gpuDevice as AcDream.App.Rendering.Gpu.Gl.GlGpuDevice
|
||||
?? throw new InvalidOperationException(
|
||||
"The GL bindless handle table was requested from a mesh pipeline composed against "
|
||||
+ $"the {_gpuDevice.Backend} backend. It is GL-only emulation of the Vulkan texture "
|
||||
+ "table and is deleted with the raw-GL world path.");
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V6i-2: how a shared atlas's physical array is made.
|
||||
|
|
@ -451,7 +481,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
// already lives inside AcDream.App or its InternalsVisibleTo test
|
||||
// assemblies.
|
||||
internal ObjectMeshManager(
|
||||
OpenGLGraphicsDevice graphicsDevice,
|
||||
IMeshPipelineDevice graphicsDevice,
|
||||
AcDream.App.Rendering.Gpu.IGpuDevice gpuDevice,
|
||||
IPreparedAssetSource preparedAssets,
|
||||
ILogger<ObjectMeshManager> logger,
|
||||
|
|
@ -461,17 +491,20 @@ namespace AcDream.App.Rendering.Wb
|
|||
_graphicsDevice = graphicsDevice
|
||||
?? throw new ArgumentNullException(nameof(graphicsDevice));
|
||||
ArgumentNullException.ThrowIfNull(gpuDevice);
|
||||
// Slice V4t: this class only ever exists on GL — it takes an
|
||||
// OpenGLGraphicsDevice — so the backend cast states that fact rather
|
||||
// than narrowing anything.
|
||||
_worldTextureTable = (AcDream.App.Rendering.Gpu.Gl.GlGpuDevice)gpuDevice;
|
||||
_gpuDevice = gpuDevice;
|
||||
// Slice V6i-2: which physical array a shared atlas gets is decided
|
||||
// once, here. Everything below — capacity, slot allocation, ref
|
||||
// counting, layer retirement, eviction — is written against
|
||||
// IWorldTextureArray and does not branch on the backend.
|
||||
_atlasArrays = new GlWorldTextureArrayFactory(
|
||||
//
|
||||
// Slice V4t downcast gpuDevice to GlGpuDevice HERE, which is what
|
||||
// made a Vulkan-composed mesh pipeline throw before it had run a
|
||||
// statement. The cast now lives on the one property that genuinely
|
||||
// needs it — the raw-GL world renderers' handle table — so
|
||||
// construction itself no longer names a backend.
|
||||
_atlasArrays = IWorldTextureArrayFactory.For(
|
||||
graphicsDevice,
|
||||
_worldTextureTable,
|
||||
gpuDevice,
|
||||
logger ?? throw new ArgumentNullException(nameof(logger)));
|
||||
_preparedAssets = preparedAssets
|
||||
?? throw new ArgumentNullException(nameof(preparedAssets));
|
||||
|
|
@ -489,7 +522,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
if (_useModernRendering)
|
||||
{
|
||||
GlobalBuffer = new GlobalMeshBuffer(
|
||||
_graphicsDevice.GL,
|
||||
RequireGl(),
|
||||
gpuDevice,
|
||||
_graphicsDevice.ResourceRetirement);
|
||||
}
|
||||
|
|
@ -1951,7 +1984,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
{
|
||||
if (meshData.Vertices.Length == 0) return null;
|
||||
|
||||
var gl = _graphicsDevice.GL;
|
||||
var gl = RequireGl();
|
||||
uint vao = 0, vbo = 0;
|
||||
var modernIndexBatches = meshData.TextureBatches.Values
|
||||
.SelectMany(batches => batches)
|
||||
|
|
@ -2398,7 +2431,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
return null;
|
||||
|
||||
var releases = new List<(string Name, Action Release)>();
|
||||
GL gl = _graphicsDevice.GL;
|
||||
GL gl = RequireGl();
|
||||
if (_useModernRendering)
|
||||
{
|
||||
if (data.GlobalAllocation is { } allocation)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ namespace AcDream.App.Rendering.Wb {
|
|||
/// <summary>
|
||||
/// OpenGL graphics device
|
||||
/// </summary>
|
||||
public unsafe class OpenGLGraphicsDevice : BaseGraphicsDevice {
|
||||
public unsafe class OpenGLGraphicsDevice : BaseGraphicsDevice, IMeshPipelineDevice {
|
||||
private readonly ILogger _log;
|
||||
private readonly DebugRenderSettings _renderSettings;
|
||||
private readonly AcDream.App.Rendering.IGpuResourceRetirementQueue _resourceRetirement;
|
||||
|
|
@ -34,6 +34,20 @@ namespace AcDream.App.Rendering.Wb {
|
|||
internal bool HasPendingGLWork =>
|
||||
!_glThreadQueue.IsEmpty || !_nextGlThreadQueue.IsEmpty;
|
||||
|
||||
// Campaign V slice V6i-2: IMeshPipelineDevice. Every member below already
|
||||
// existed under a GL-specific name; these are aliases, not behaviour, so
|
||||
// the shipping backend executes exactly the statements it executed
|
||||
// before. See the interface for what the mesh pipeline actually needs
|
||||
// and what still has to move before it has a second implementation.
|
||||
GL? IMeshPipelineDevice.Gl => GL;
|
||||
|
||||
AcDream.App.Rendering.IGpuResourceRetirementQueue IMeshPipelineDevice.ResourceRetirement =>
|
||||
_resourceRetirement;
|
||||
|
||||
bool IMeshPipelineDevice.HasPendingWork => HasPendingGLWork;
|
||||
|
||||
void IMeshPipelineDevice.ProcessQueue() => ProcessGLQueue();
|
||||
|
||||
public void QueueGLAction(Action<GL> action) {
|
||||
_glThreadQueue.Enqueue(action);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ public sealed class WbMeshAdapter
|
|||
internal const int MaximumReclaimedMeshesPerFrame = MaximumUploadsPerFrame;
|
||||
internal const long MaximumReclaimedMeshBytesPerFrame = 64L * 1024 * 1024;
|
||||
internal const int MaximumStaleDiscardsPerFrame = 64;
|
||||
private readonly OpenGLGraphicsDevice? _graphicsDevice;
|
||||
private readonly IMeshPipelineDevice? _graphicsDevice;
|
||||
private readonly ObjectMeshManager? _meshManager;
|
||||
private readonly AcDream.App.Rendering.IGpuResourceRetirementQueue? _resourceRetirement;
|
||||
private readonly IPreparedAssetSource? _ownedPreparedAssets;
|
||||
|
|
@ -495,7 +495,7 @@ public sealed class WbMeshAdapter
|
|||
if (_disposed) return;
|
||||
|
||||
ObjectMeshManager meshManager = _meshManager!;
|
||||
_graphicsDevice!.ProcessGLQueue();
|
||||
_graphicsDevice!.ProcessQueue();
|
||||
// #125: drain staged uploads; a FAILED upload (UploadMeshData returned
|
||||
// null from its catch) is re-staged for a LATER frame, not dropped. The
|
||||
// re-stages are collected and re-enqueued AFTER the loop — re-enqueuing
|
||||
|
|
@ -759,8 +759,8 @@ public sealed class WbMeshAdapter
|
|||
if (_graphicsDevice is null)
|
||||
return;
|
||||
|
||||
_graphicsDevice.ProcessGLQueue();
|
||||
if (_graphicsDevice.HasPendingGLWork)
|
||||
_graphicsDevice.ProcessQueue();
|
||||
if (_graphicsDevice.HasPendingWork)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"OpenGL work remains pending after {operation}; retry adapter disposal to continue the exact stage.");
|
||||
|
|
|
|||
|
|
@ -101,6 +101,26 @@ internal interface IWorldTextureArray : IDisposable
|
|||
/// </summary>
|
||||
internal interface IWorldTextureArrayFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Campaign V slice V6i-2: picks the arm from what the composed devices
|
||||
/// actually are. This is the ONE place the mesh pipeline's texture stack
|
||||
/// branches on a backend, which is what lets everything above it — capacity
|
||||
/// policy, slot allocation, ref counting, layer retirement, eviction — be
|
||||
/// written once.
|
||||
/// </summary>
|
||||
internal static IWorldTextureArrayFactory For(
|
||||
IMeshPipelineDevice graphicsDevice,
|
||||
IGpuDevice gpuDevice,
|
||||
ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(graphicsDevice);
|
||||
ArgumentNullException.ThrowIfNull(gpuDevice);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
return graphicsDevice is OpenGLGraphicsDevice gl && gpuDevice is GlGpuDevice table
|
||||
? new GlWorldTextureArrayFactory(gl, table, logger)
|
||||
: new RhiWorldTextureArrayFactory(gpuDevice);
|
||||
}
|
||||
|
||||
/// <summary>The retirement queue array layers and images are released through.</summary>
|
||||
IGpuResourceRetirementQueue Retirement { get; }
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue