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;
///
/// Campaign V slice V6i-2: the mesh pipeline no longer names 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" — which is
/// why NullWbMeshAdapter 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.
///
/// 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.
///
public sealed class MeshPipelineDeviceSeamTests
{
/// A device with the mesh pipeline's whole surface and no GL behind it.
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.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()
{
}
}
///
/// The whole point. Before this slice the constructor downcast the RHI device
/// to GlGpuDevice, so this threw before running a statement.
///
[Fact]
public void TheMeshPipelineConstructsAgainstADeviceWithNoGlContext()
{
using var device = new RecordingGpuDevice();
using ObjectMeshManager manager = Build(device);
Assert.False(manager.IsDisposed);
}
///
/// 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.
///
[Fact]
public void TheGlHandleTableIsRefusedByNameRatherThanCast()
{
using var device = new RecordingGpuDevice();
using ObjectMeshManager manager = Build(device);
InvalidOperationException failure =
Assert.Throws(() => manager.WorldTextureTable);
Assert.Contains("GL-only", failure.Message, StringComparison.Ordinal);
Assert.Contains(GpuBackendKind.Recording.ToString(), failure.Message, StringComparison.Ordinal);
}
///
/// 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.
///
[Fact]
public void TheArrayFactorySelectsTheRhiArmWithoutAGlPair()
{
using var device = new RecordingGpuDevice();
IWorldTextureArrayFactory arrays = IWorldTextureArrayFactory.For(
new ContextFreeMeshPipelineDevice(device.Retirement),
device,
NullLogger.Instance);
Assert.IsType(arrays);
using IWorldTextureArray array =
arrays.CreateClampedArray(TextureFormat.RGBA8, 32, 32, 2);
Assert.IsType(array);
}
///
/// 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.
///
[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);
}
///
/// 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.
///
[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());
}
}