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()); } }