fix(render): Campaign V slice V6j commit 1 - the Vulkan winding needs no inversion
VulkanViewportMapping has inverted the front face since V6c, on the standard argument that rendering with a negative viewport height mirrors framebuffer space and therefore reverses triangle orientation. The world arm is the first consumer that culls anything, and it falsified the inversion twice over on one frame. Nothing exercised it before now. Every Vulkan consumer through V6i - TextRenderer, DebugLineRenderer and the bring-up scene - declares Cull = GpuCullMode.None, so the mapping had never decided a single fragment. That is why a wrong answer survived four slices and a validation-clean run: an unexercised path. What the world arm measured, on the same offline scene the GL pixel gate captures. Terrain is the one single-sided surface acdream draws - FrontFace(Ccw) plus Cull(Back), matching ACRender::landPolysDraw's per-triangle eye-side predicate - and under the inversion it vanished completely, 190 multi-draw commands issuing against 625 loaded landblocks with nothing on screen. Every closed building shell rendered inside-out in the same frame: the front wall culled and the interior beams visible through the gap, which is what a back-face-front cull looks like on geometry that is only nearly convex. Declaring the GL winding verbatim restores both at once - terrain draws single-sided from above, and the shells close. Two independent surfaces, one change, and the correction is the identity mapping. Recorded here rather than worked around in the renderers, because a renderer that compensates for its backend is exactly the shape this file exists to prevent: the contract says renderers speak GL and the backend translates, and the backend was translating wrongly. The viewport flip itself is untouched and still correct - it is what puts GL-authored geometry the right way up with no shader or matrix change. What goes is the claim that a winding inversion has to travel with it. The scissor's explicit flip is a separate correction with a separate justification and is likewise untouched. The test suite says so now rather than describing the old behaviour: the pass-through is asserted directly, and the exact-inverses test becomes a travels-alone test, so a later change that reintroduces the inversion fails here first and on any single-sided surface second. Gates. Release build green. App tests 4,112 passed / 3 skipped, the unchanged baseline. GL offline pixel gate unaffected by construction - this file has no GL arm - and measured with the world arm in commit 2. No divergence-register row: this corrects a backend translation error rather than introducing a deviation from retail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
847f14aeef
commit
81fe5e1b63
2 changed files with 52 additions and 19 deletions
|
|
@ -10,11 +10,30 @@ namespace AcDream.App.Rendering.Gpu.Vk;
|
||||||
/// counter-clockwise. Vulkan's framebuffer origin is top-left, so this backend
|
/// counter-clockwise. Vulkan's framebuffer origin is top-left, so this backend
|
||||||
/// renders with a NEGATIVE viewport height, which mirrors clip space vertically
|
/// 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
|
/// and makes GL-authored geometry land in the right place with no shader or
|
||||||
/// matrix change anywhere. Mirroring also reverses triangle winding, so the
|
/// matrix change anywhere.</para>
|
||||||
/// 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
|
/// <para><b>The winding does NOT need a compensating inversion, and slice V6j
|
||||||
/// one file with one test suite rather than at the dozen call sites that would
|
/// measured that rather than reasoning about it.</b> V6c wrote this file with an
|
||||||
/// otherwise each have to remember.</para>
|
/// 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 <c>Cull = GpuCullMode.None</c>, so the mapping had never decided a
|
||||||
|
/// single fragment.</para>
|
||||||
|
///
|
||||||
|
/// <para>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 (<c>FrontFace(Ccw)</c> + <c>Cull(Back)</c>, matching
|
||||||
|
/// <c>ACRender::landPolysDraw</c>'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
|
||||||
|
/// <c>artifacts/v6j-vk1</c> (inverted: no terrain, hollow shells) and
|
||||||
|
/// <c>artifacts/v6j-head</c> (GL reference) against the corrected frame.</para>
|
||||||
|
///
|
||||||
|
/// <para>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.</para>
|
||||||
///
|
///
|
||||||
/// <para><b>Scissor does NOT flip with the viewport.</b> The V3 audit called
|
/// <para><b>Scissor does NOT flip with the viewport.</b> The V3 audit called
|
||||||
/// this out as a concrete acceptance item (plan §4.10, item 1):
|
/// this out as a concrete acceptance item (plan §4.10, item 1):
|
||||||
|
|
@ -80,14 +99,14 @@ internal static class VulkanViewportMapping
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Inverts the winding a renderer asked for, because the negative viewport
|
/// Passes the winding a renderer asked for straight through. See the class
|
||||||
/// height mirrors framebuffer space. No renderer performs this flip itself
|
/// remarks for the measurement that replaced V6c's inversion — and note that
|
||||||
/// and no other code in the backend may repeat it.
|
/// a renderer must still never flip anything itself.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static FrontFace ToVulkan(GpuFrontFace frontFace) => frontFace switch
|
internal static FrontFace ToVulkan(GpuFrontFace frontFace) => frontFace switch
|
||||||
{
|
{
|
||||||
GpuFrontFace.CounterClockwise => FrontFace.Clockwise,
|
GpuFrontFace.CounterClockwise => FrontFace.CounterClockwise,
|
||||||
GpuFrontFace.Clockwise => FrontFace.CounterClockwise,
|
GpuFrontFace.Clockwise => FrontFace.Clockwise,
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(frontFace), frontFace, "Unknown winding."),
|
_ => throw new ArgumentOutOfRangeException(nameof(frontFace), frontFace, "Unknown winding."),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,25 +88,39 @@ public sealed class VulkanViewportMappingTests
|
||||||
Assert.Equal(20u, scissor.Extent.Height);
|
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]
|
[Fact]
|
||||||
public void FrontFaceIsInvertedBecauseTheViewportMirrorsFramebufferSpace()
|
public void FrontFacePassesThroughSoRenderersDeclareTheGlWinding()
|
||||||
{
|
{
|
||||||
Assert.Equal(FrontFace.Clockwise, VulkanViewportMapping.ToVulkan(GpuFrontFace.CounterClockwise));
|
Assert.Equal(
|
||||||
Assert.Equal(FrontFace.CounterClockwise, VulkanViewportMapping.ToVulkan(GpuFrontFace.Clockwise));
|
FrontFace.CounterClockwise,
|
||||||
|
VulkanViewportMapping.ToVulkan(GpuFrontFace.CounterClockwise));
|
||||||
|
Assert.Equal(
|
||||||
|
FrontFace.Clockwise,
|
||||||
|
VulkanViewportMapping.ToVulkan(GpuFrontFace.Clockwise));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void TheFlipAndTheWindingInversionAreExactInverses()
|
public void TheViewportFlipTravelsAloneAndTheWindingIsUntouched()
|
||||||
{
|
{
|
||||||
// Mirroring twice is identity, and inverting the winding twice is too.
|
// The viewport still mirrors — that is what puts GL-authored geometry
|
||||||
// If a later change ever flipped one without the other, this is the
|
// the right way up with no matrix change. What it does NOT do is drag a
|
||||||
// shape of the assertion that catches it.
|
// 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);
|
Viewport once = VulkanViewportMapping.ToVulkan(0, 0, 640, 480, attachmentHeight: 480);
|
||||||
Assert.Equal(-480f, once.Height);
|
Assert.Equal(-480f, once.Height);
|
||||||
Assert.Equal(480f, once.Y);
|
Assert.Equal(480f, once.Y);
|
||||||
|
|
||||||
FrontFace inverted = VulkanViewportMapping.ToVulkan(GpuFrontFace.CounterClockwise);
|
Assert.Equal(
|
||||||
Assert.NotEqual(FrontFace.CounterClockwise, inverted);
|
FrontFace.CounterClockwise,
|
||||||
|
VulkanViewportMapping.ToVulkan(GpuFrontFace.CounterClockwise));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue