diff --git a/src/AcDream.App/Rendering/Gpu/Vk/VulkanViewportMapping.cs b/src/AcDream.App/Rendering/Gpu/Vk/VulkanViewportMapping.cs
index 5a19f9f9..f7c81759 100644
--- a/src/AcDream.App/Rendering/Gpu/Vk/VulkanViewportMapping.cs
+++ b/src/AcDream.App/Rendering/Gpu/Vk/VulkanViewportMapping.cs
@@ -10,11 +10,30 @@ namespace AcDream.App.Rendering.Gpu.Vk;
/// counter-clockwise. Vulkan's framebuffer origin is top-left, so this backend
/// renders with a NEGATIVE viewport height, which mirrors clip space vertically
/// and makes GL-authored geometry land in the right place with no shader or
-/// matrix change anywhere. Mirroring also reverses triangle winding, so the
-/// front face is inverted to compensate. The two flips are exact inverses and
-/// must therefore always travel together — which is precisely why they live in
-/// one file with one test suite rather than at the dozen call sites that would
-/// otherwise each have to remember.
+/// matrix change anywhere.
+///
+/// The winding does NOT need a compensating inversion, and slice V6j
+/// measured that rather than reasoning about it. V6c wrote this file with an
+/// inverted front face on the standard argument that mirroring framebuffer space
+/// reverses triangle orientation. Nothing exercised it: every Vulkan consumer
+/// through V6i — the retained UI's text, debug lines and the bring-up scene —
+/// declares Cull = GpuCullMode.None, so the mapping had never decided a
+/// single fragment.
+///
+/// The world arm is its first culling consumer, and it falsified the
+/// inversion twice over on the same frame. Terrain is the one single-sided
+/// surface acdream draws (FrontFace(Ccw) + Cull(Back), matching
+/// ACRender::landPolysDraw's eye-side predicate) and it vanished
+/// completely; every closed building shell rendered inside-out, its front wall
+/// culled and its interior beams visible through the gap. Declaring the GL
+/// winding verbatim — no inversion — restores both: terrain draws single-sided
+/// from above, and the shells close. The captures are
+/// artifacts/v6j-vk1 (inverted: no terrain, hollow shells) and
+/// artifacts/v6j-head (GL reference) against the corrected frame.
+///
+/// So the viewport flip travels alone. The scissor's explicit flip below
+/// is unaffected — that one was always a separate correction and is separately
+/// justified.
///
/// Scissor does NOT flip with the viewport. The V3 audit called
/// this out as a concrete acceptance item (plan §4.10, item 1):
@@ -80,14 +99,14 @@ internal static class VulkanViewportMapping
}
///
- /// Inverts the winding a renderer asked for, because the negative viewport
- /// height mirrors framebuffer space. No renderer performs this flip itself
- /// and no other code in the backend may repeat it.
+ /// Passes the winding a renderer asked for straight through. See the class
+ /// remarks for the measurement that replaced V6c's inversion — and note that
+ /// a renderer must still never flip anything itself.
///
internal static FrontFace ToVulkan(GpuFrontFace frontFace) => frontFace switch
{
- GpuFrontFace.CounterClockwise => FrontFace.Clockwise,
- GpuFrontFace.Clockwise => FrontFace.CounterClockwise,
+ GpuFrontFace.CounterClockwise => FrontFace.CounterClockwise,
+ GpuFrontFace.Clockwise => FrontFace.Clockwise,
_ => throw new ArgumentOutOfRangeException(nameof(frontFace), frontFace, "Unknown winding."),
};
diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanViewportMappingTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanViewportMappingTests.cs
index 90de252f..0b889d57 100644
--- a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanViewportMappingTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanViewportMappingTests.cs
@@ -88,25 +88,39 @@ public sealed class VulkanViewportMappingTests
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 FrontFaceIsInvertedBecauseTheViewportMirrorsFramebufferSpace()
+ public void FrontFacePassesThroughSoRenderersDeclareTheGlWinding()
{
- Assert.Equal(FrontFace.Clockwise, VulkanViewportMapping.ToVulkan(GpuFrontFace.CounterClockwise));
- Assert.Equal(FrontFace.CounterClockwise, VulkanViewportMapping.ToVulkan(GpuFrontFace.Clockwise));
+ Assert.Equal(
+ FrontFace.CounterClockwise,
+ VulkanViewportMapping.ToVulkan(GpuFrontFace.CounterClockwise));
+ Assert.Equal(
+ FrontFace.Clockwise,
+ VulkanViewportMapping.ToVulkan(GpuFrontFace.Clockwise));
}
[Fact]
- public void TheFlipAndTheWindingInversionAreExactInverses()
+ public void TheViewportFlipTravelsAloneAndTheWindingIsUntouched()
{
- // Mirroring twice is identity, and inverting the winding twice is too.
- // If a later change ever flipped one without the other, this is the
- // shape of the assertion that catches it.
+ // 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);
- FrontFace inverted = VulkanViewportMapping.ToVulkan(GpuFrontFace.CounterClockwise);
- Assert.NotEqual(FrontFace.CounterClockwise, inverted);
+ Assert.Equal(
+ FrontFace.CounterClockwise,
+ VulkanViewportMapping.ToVulkan(GpuFrontFace.CounterClockwise));
}
[Fact]