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; 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 existed. §5.5.12 item 6 measured how wide the /// dependency really was: 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 originally proved construction and nothing more, back when the /// upload bodies were still raw GL and the world renderers still bound a GL /// handle table. Campaign V slice V11 deleted both along with the rest of the /// raw-GL arm (and the interface's own Gl member, which nothing read any /// more once they were gone) — the arena-build and upload tests below now cover /// what those slices only asserted would eventually fail loudly. /// public sealed class MeshPipelineDeviceSeamTests { /// A device with the mesh pipeline's whole surface and no GL behind it. private sealed class ContextFreeMeshPipelineDevice( IGpuResourceRetirementQueue retirement, bool modernPath = false) : IMeshPipelineDevice { 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.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 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 — six members measured out /// of a 760-line class (seven until Campaign V slice V11 deleted the unread /// Gl member). 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( [ "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()); } /// /// 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. /// [Fact] public void TheModernArenaBuildsWithoutAGlContext() { using var device = new RecordingGpuDevice(); using ObjectMeshManager manager = Build(device, modernPath: true); GlobalMeshBuffer arena = Assert.IsType(manager.GlobalBuffer); Assert.True(arena.HasStores); Assert.NotNull(arena.VertexStore); Assert.NotNull(arena.IndexStore); } /// /// 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. /// [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 readback = stackalloc byte[3 * VertexPositionNormalTexture.Size]; arena.VertexStore!.Read( (long)allocation.Vertices.Offset * VertexPositionNormalTexture.Size, readback); var uploaded = System.Runtime.InteropServices.MemoryMarshal .Cast(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 indexBytes = stackalloc byte[3 * sizeof(ushort)]; arena.IndexStore!.Read((long)allocation.Indices.Offset * sizeof(ushort), indexBytes); Assert.Equal( indices, System.Runtime.InteropServices.MemoryMarshal.Cast(indexBytes).ToArray()); } /// /// 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. /// [Fact] public void TheVulkanMeshPipelineDeviceReportsTheModernPath() { using var device = new RecordingGpuDevice(); using var vulkanDevice = new AcDream.App.Rendering.Gpu.Vk.VulkanMeshPipelineDevice(device.Retirement); Assert.True(vulkanDevice.HasBindless); Assert.True(vulkanDevice.HasOpenGL43); Assert.False(vulkanDevice.HasPendingWork); Assert.Equal(0u, vulkanDevice.InstanceVBO); Assert.Same(device.Retirement, vulkanDevice.ResourceRetirement); } }