acdream/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanViewportMappingTests.cs
Erik 15ed57a1e7 fix(rendering): carry retail SetSurface state to detail draws
Resolve exact SetSurface blend, alpha-test, and fog state once during extraction and preserve it through recipe-10 prepared payloads, Wb/EnvCell command data, Vulkan pipelines, push constants, and both ordinary/atmospheric one-pass shaders. Preserve AP-240 Wb pure-Clip immediate opaque/A2C while EnvCell uses retail premultiplied Clip; detail-off routing remains unchanged. Correct AP-232 and register the remaining detail-off state divergence.

Mutation first failures (all restored):
- raw Add->SRCALPHA/ONE: WalkStaticStreamPopulatorTests.ImmediateBuildingDetail_UsesExactResolvedSetSurfaceState line 1278, expected wb-mesh-raw-additive-1x.
- inverse-add->alpha-add: same test line 1278, expected wb-mesh-inverse-additive-1x.
- remove Env inverse: EnvCellAlphaDrawSourceTests.DetailOn_EveryEnvCellFamilyDrawsOnceInPlaceWithAuthoredOpacity line 132, expected envcell-inverse.
- raw IsAdditive precedence: same test line 132, Translucent|Clip|Additive expected envcell-alpha.
- Wb paletted ParamB=0: OrderPreservingSubmitterTests line 333, expected 0.392156869.
- Wb DDS ParamB=0.05: same test line 333, expected 0.784313738.
- disable Alpha+Clip test: WalkStaticStreamPopulatorTests line 1286, expected 0.784313738.
- always fog raw Add: same test line 1287, expected no-fog true.
- disable fog non-Add: same test line 1287, expected no-fog false.
- X=a*qA: RetailDetailTextureContractTests line 261, shared squared-alpha substring absent.
- X includes base alpha: same test line 262, forbidden baseTexel.a present.
- CLIP uses 0.05: EnvCellAlphaDrawSourceTests.ClipShaders_UseGreaterEqualForThePerRangeReference line 367.
- second detail draw: EnvCell detail-on line 131, collection contained 2 draws.
- straight-alpha substitute: Wb immediate line 1278, expected wb-mesh-additive-1x.
- omit ordered detail arm: OrderPreservingSubmitterTests line 318, expected (77,3.5), got (0,0).
- omit atmospheric combine: RetailDetailTextureContractTests line 260, shared include absent.
- drop serialized opacity: ObjectMeshDataSerializerTests line 292, expected opacity bits, got 1.0.
- stale detail arm: atmospheric adjacency line 402, expected slot 0, got 77.
- per-frame surface map: EnvCell warmed allocation line 285, expected 0 B, got 204800 B.
2026-09-05 02:03:13 +02:00

214 lines
9.1 KiB
C#

using System;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Gpu.Vk;
using Silk.NET.Vulkan;
namespace AcDream.App.Tests.Rendering.Gpu.Vk;
/// <summary>
/// Campaign V slice V6c — the coordinate reconciliation (plan §3.3, §4.7,
/// §4.10).
///
/// <para>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.</para>
///
/// <para>The scissor is the trap. It does NOT flip with the viewport:
/// <c>vkCmdSetScissor</c> is always top-left-origin regardless of viewport sign,
/// while every caller passes a GL-convention bottom-left rectangle. 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.</para>
/// </summary>
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);
}
/// <summary>
/// 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 <c>VulkanViewportMapping</c>'s remarks for the captures.
/// </summary>
[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 AllRetailBlendModesAreRepresentable()
{
Assert.Equal(
(BlendFactor.SrcAlpha, BlendFactor.OneMinusSrcAlpha),
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.StraightAlpha));
Assert.Equal(
(BlendFactor.One, BlendFactor.OneMinusSrcAlpha),
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.PremultipliedAlpha));
Assert.Equal(
(BlendFactor.SrcAlpha, BlendFactor.One),
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.Additive));
Assert.Equal(
(BlendFactor.One, BlendFactor.One),
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.RawAdditive));
Assert.Equal(
(BlendFactor.OneMinusSrcAlpha, BlendFactor.One),
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.InverseAdditive));
// 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));
}
}