using System; using AcDream.App.Rendering.Gpu; using AcDream.App.Rendering.Gpu.Vk; using Silk.NET.Vulkan; namespace AcDream.App.Tests.Rendering.Gpu.Vk; /// /// Campaign V slice V6c — the coordinate reconciliation (plan §3.3, §4.7, /// §4.10). /// /// Renderers speak GL: viewport origin bottom-left, front faces /// counter-clockwise. The Vulkan backend renders with a NEGATIVE viewport /// height, which mirrors framebuffer space vertically and therefore also /// reverses winding, so the front face is inverted to compensate. The two flips /// are exact inverses and must always travel together — which is why they live /// in one file, and why these tests assert them together. /// /// The scissor is the trap. It does NOT flip with the viewport: /// vkCmdSetScissor is always top-left-origin regardless of viewport sign, /// while NdcScissorRect.ToPixels emits GL bottom-left rectangles. The V3 /// audit flagged this explicitly as a V6 acceptance item, and getting it wrong /// shows up as a doorway aperture clipped from the wrong edge — which only a /// scene containing one would reveal. /// public sealed class VulkanViewportMappingTests { [Fact] public void FullViewportBecomesANegativeHeightRectangleAnchoredAtTheBottom() { Viewport viewport = VulkanViewportMapping.ToVulkan(0, 0, 1280, 720, attachmentHeight: 720); Assert.Equal(0f, viewport.X); // Y is the BOTTOM edge in Vulkan's top-left space, and the height runs // upward from it. Together they mirror clip space. Assert.Equal(720f, viewport.Y); Assert.Equal(1280f, viewport.Width); Assert.Equal(-720f, viewport.Height); Assert.Equal(0f, viewport.MinDepth); Assert.Equal(1f, viewport.MaxDepth); } [Fact] public void AnOffsetViewportKeepsItsGlBottomLeftMeaning() { // A 100x50 viewport whose bottom edge sits 30 px above the bottom of a // 720 px attachment. Viewport viewport = VulkanViewportMapping.ToVulkan(10, 30, 100, 50, attachmentHeight: 720); Assert.Equal(10f, viewport.X); Assert.Equal(690f, viewport.Y); Assert.Equal(-50f, viewport.Height); } [Fact] public void ScissorFlipsAgainstTheAttachmentBecauseTheViewportSignDoesNotDoItForUs() { // GL rectangle: 100 px wide, 50 px tall, bottom edge 30 px up. Rect2D scissor = VulkanViewportMapping.ScissorToVulkan(10, 30, 100, 50, attachmentHeight: 720); Assert.Equal(10, scissor.Offset.X); // Top edge measured from the top: 720 - (30 + 50). Assert.Equal(640, scissor.Offset.Y); Assert.Equal(100u, scissor.Extent.Width); Assert.Equal(50u, scissor.Extent.Height); } [Fact] public void AFullAttachmentScissorIsUnchangedByTheFlip() { Rect2D scissor = VulkanViewportMapping.ScissorToVulkan(0, 0, 1280, 720, attachmentHeight: 720); Assert.Equal(0, scissor.Offset.X); Assert.Equal(0, scissor.Offset.Y); Assert.Equal(1280u, scissor.Extent.Width); Assert.Equal(720u, scissor.Extent.Height); } [Fact] public void AScissorStraddlingTheTopEdgeIsClampedRatherThanRejected() { // Bottom edge 700 px up in a 720 px attachment, 50 px tall: 30 px of it // is off the top. GL silently clips this; a driver error here would turn // a harmless off-screen aperture into a crash. Rect2D scissor = VulkanViewportMapping.ScissorToVulkan(0, 700, 100, 50, attachmentHeight: 720); Assert.Equal(0, scissor.Offset.Y); Assert.Equal(20u, scissor.Extent.Height); } /// /// Campaign V slice V6j: the winding a renderer declares is the winding /// Vulkan gets. V6c inverted it on the standard negative-viewport argument; /// the world arm — the mapping's first culling consumer — measured that the /// inversion culls terrain outright and turns every closed building shell /// inside-out. See VulkanViewportMapping's remarks for the captures. /// [Fact] public void FrontFacePassesThroughSoRenderersDeclareTheGlWinding() { Assert.Equal( FrontFace.CounterClockwise, VulkanViewportMapping.ToVulkan(GpuFrontFace.CounterClockwise)); Assert.Equal( FrontFace.Clockwise, VulkanViewportMapping.ToVulkan(GpuFrontFace.Clockwise)); } [Fact] public void TheViewportFlipTravelsAloneAndTheWindingIsUntouched() { // The viewport still mirrors — that is what puts GL-authored geometry // the right way up with no matrix change. What it does NOT do is drag a // winding inversion along with it. A later change that reintroduces one // fails here and, more usefully, fails visibly on any single-sided // surface. Viewport once = VulkanViewportMapping.ToVulkan(0, 0, 640, 480, attachmentHeight: 480); Assert.Equal(-480f, once.Height); Assert.Equal(480f, once.Y); Assert.Equal( FrontFace.CounterClockwise, VulkanViewportMapping.ToVulkan(GpuFrontFace.CounterClockwise)); } [Fact] public void CullModesMapStraightAcross() { Assert.Equal(CullModeFlags.None, VulkanViewportMapping.ToVulkan(GpuCullMode.None)); Assert.Equal(CullModeFlags.BackBit, VulkanViewportMapping.ToVulkan(GpuCullMode.Back)); Assert.Equal(CullModeFlags.FrontBit, VulkanViewportMapping.ToVulkan(GpuCullMode.Front)); } [Fact] public void AllThreeRetailBlendModesAreRepresentable() { Assert.Equal( (BlendFactor.SrcAlpha, BlendFactor.OneMinusSrcAlpha), VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.StraightAlpha)); Assert.Equal( (BlendFactor.SrcAlpha, BlendFactor.One), VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.Additive)); // Retail's third mode, found at slice V4c. Mapping it onto straight // alpha would have silently changed how every inverse-alpha surface // composites. Assert.Equal( (BlendFactor.OneMinusSrcAlpha, BlendFactor.SrcAlpha), VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.InverseAlpha)); } [Fact] public void IntegerVertexAttributesTakeAUintFormatNotANormalisedOne() { Assert.Equal(Format.R8G8B8A8Unorm, VulkanViewportMapping.ToVulkan(GpuVertexFormat.UByte4Normalized)); // terrain_modern.vert reads locations 2-5 as uvec4; those packed bytes // carry terrain-type, road and split-direction codes, so normalising // them would not be an approximation - it would be garbage. Assert.Equal(Format.R8G8B8A8Uint, VulkanViewportMapping.ToVulkan(GpuVertexFormat.UByte4UInt)); } [Fact] public void ResolveIsExpressedByAResolveTargetRatherThanAStoreOp() { // There is no VK_ATTACHMENT_STORE_OP_RESOLVE; a resolving attachment // discards its multisampled contents and names a resolve image instead. Assert.Equal(AttachmentStoreOp.DontCare, VulkanViewportMapping.ToVulkan(GpuStoreOp.Resolve)); Assert.Equal(AttachmentStoreOp.Store, VulkanViewportMapping.ToVulkan(GpuStoreOp.Store)); Assert.Equal(AttachmentStoreOp.DontCare, VulkanViewportMapping.ToVulkan(GpuStoreOp.DontCare)); } [Fact] public void PipelineCacheHeaderValidationRejectsAnotherDevicesBlob() { byte[] uuid = new byte[16]; for (int i = 0; i < 16; i++) uuid[i] = (byte)(i + 1); byte[] blob = new byte[64]; BitConverter.GetBytes(32u).CopyTo(blob, 0); BitConverter.GetBytes(1u).CopyTo(blob, 4); BitConverter.GetBytes(0x1002u).CopyTo(blob, 8); BitConverter.GetBytes(0x7550u).CopyTo(blob, 12); uuid.CopyTo(blob, 16); Assert.NotNull(VulkanPipelineCache.ValidateHeader(blob, 0x1002, 0x7550, uuid)); // A driver update changes the cache UUID, and feeding the old blob back // is exactly the case the header exists to catch. uuid[0] = 0xFF; Assert.Null(VulkanPipelineCache.ValidateHeader(blob, 0x1002, 0x7550, uuid)); } [Fact] public void PipelineCacheHeaderValidationRejectsTruncatedAndForeignBlobs() { byte[] uuid = new byte[16]; Assert.Null(VulkanPipelineCache.ValidateHeader(null, 1, 1, uuid)); Assert.Null(VulkanPipelineCache.ValidateHeader(new byte[8], 1, 1, uuid)); byte[] blob = new byte[32]; BitConverter.GetBytes(32u).CopyTo(blob, 0); BitConverter.GetBytes(1u).CopyTo(blob, 4); BitConverter.GetBytes(0x8086u).CopyTo(blob, 8); Assert.Null(VulkanPipelineCache.ValidateHeader(blob, 0x1002, 0, uuid)); } }