Skip to content

[Visual Test] Menu screenshot RMSE 0.0437 exceeds 0.015 tolerance after g_pipeline inherits alphaToCoverageEnable #911

Description

@MichaelFisher1997

Visual Test Failure

The nightly visual-test workflow (run 29078561400) regressed the menu golden-image comparison. The game launches successfully, Lavapipe renders, and screenshot.png is captured, but the pixel diff against docs/visual-test/golden/menu.png is RMSE 0.0436523, which is ~2.9x the configured VISUAL_DIFF_RMSE_TOLERANCE of 0.015.

Exact error output

The Compare against golden image step in visual-test.yml (line 99-105) exits with status 1:

Visual RMSE: 0.0436523 (tolerance 0.015)
Visual golden diff exceeds tolerance: 0.0436523 > 0.015
##[error]Process completed with exit code 1.

The build log captured by run-with-log shows two Vulkan validation errors that point at broken pipeline contracts introduced (or exposed) by commit 9feb275 "refactor: remove legacy lighting paths (#910)":

[ERROR] Vulkan validation error: vkCreateGraphicsPipelines():
  pCreateInfos[0].pMultisampleState->alphaToCoverageEnable is VK_TRUE,
  but the fragment shader doesn't declare a variable that covers
  Location 0, Component 3 (alpha channel).
  (VUID-VkGraphicsPipelineCreateInfo-alphaToCoverageEnable-08891)

[ERROR] Vulkan validation error: vkCreateGraphicsPipelines():
  pCreateInfos[0].pMultisampleState->rasterizationSamples (1)
  does not match the number of samples of the RenderPass color and/or
  depth attachment (4).
  (VUID-VkGraphicsPipelineCreateInfo-multisampledRenderToSingleSampled-06853)

Followed at runtime by image-layout violations on the offscreen swapchain image (VkImage 0x180000000018):

[ERROR] Vulkan validation error: vkQueueSubmit(): pSubmits[0] command buffer ...
  expects VkImage ... to be in layout VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
  -- instead, current layout is VK_IMAGE_LAYOUT_UNDEFINED.
[ERROR] Vulkan validation error: vkQueueSubmit(): pSubmits[0] command buffer ...
  expects VkImage ... to be in layout VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
  -- instead, current layout is VK_IMAGE_LAYOUT_PRESENT_SRC_KHR.

These are emitted under VK_LAYER_KHRONOS_validation + VK_VALIDATION_FEATURE_ENABLE_BEST_PRACTICES_EXT (configured by .github/actions/setup-lavapipe/action.yml). Lavapipe keeps executing despite the spec violations, but the G-buffer output is undefined, which is why the final post-processed menu frame no longer matches the golden capture.

Root cause

1. g_pipeline inherits alphaToCoverageEnable = VK_TRUE

modules/engine-graphics/src/vulkan/pipeline_manager.zig:280-285 builds the shared multisample state:

var multisampling = std.mem.zeroes(c.VkPipelineMultisampleStateCreateInfo);
multisampling.sType = c.VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
multisampling.rasterizationSamples = sample_count;

var terrain_multisampling = multisampling;
terrain_multisampling.alphaToCoverageEnable =
    if (sample_count != c.VK_SAMPLE_COUNT_1_BIT) c.VK_TRUE else c.VK_FALSE;

createTerrainPipeline then forwards &terrain_multisampling (which has alphaToCoverageEnable = VK_TRUE) as the multisampling parameter to pipeline_specialized.createTerrainPipeline.

In modules/engine-graphics/src/vulkan/pipeline_specialized.zig:130-131, the G-buffer pipeline is built by copying that struct and only overriding rasterizationSamples:

var g_multisampling = multisampling.*;          // inherits alphaToCoverageEnable = VK_TRUE
g_multisampling.rasterizationSamples = c.VK_SAMPLE_COUNT_1_BIT;

But assets/shaders/vulkan/g_pass.frag declares layout(location = 0) out vec3 outNormal; (no alpha component). Vulkan therefore rejects the pipeline with VUID-VkGraphicsPipelineCreateInfo-alphaToCoverageEnable-08891, and on Lavapipe the G-buffer pass writes an undefined alpha-driven coverage mask, corrupting SSAO/velocity inputs that downstream post-process passes sample.

2. water_pipeline sample-count mismatch with hdr_render_pass

modules/engine-graphics/src/vulkan/water_system.zig:281-387 constructs the water pipeline with rasterizationSamples = c.VK_SAMPLE_COUNT_1_BIT but is invoked from rhi_init_deinit.zig:99 with ctx.render_pass_manager.hdr_render_pass. When MSAA is on (default msaa_samples = 4 per modules/game-core/src/settings/data.zig:113), that render pass has sampleCount = 4 on its color and depth attachments (render_pass_manager.zig:129, 139), producing VUID-VkGraphicsPipelineCreateInfo-multisampledRenderToSingleSampled-06853.

Why this regresses only the menu capture

  • The HomeScreen draws UI (ui.frag, ui_tex.frag) into the offscreen HDR MSAA color/depth attachments.
  • The MSAA-resolved HDR target is then sampled by post-process passes (post_process_system.zig, bloom_system.zig, FXAA, etc.) and finally copied into the swapchain image via the UI swapchain render pass.
  • The undefined G-buffer coverage + invalid VK_IMAGE_LAYOUT_* transitions on the offscreen image leave the swapchain contents in an inconsistent state at the moment screenshot.captureFrame issues its vkCmdCopyImageToBuffer.
  • On a real GPU, hardware may "do the right thing" anyway, but Lavapipe deterministically diverges, producing the ~4.4% RMSE that the golden-image gate now rejects.

Origin of the failure

  • modules/engine-graphics/src/vulkan/pipeline_specialized.zig:130g_multisampling = multisampling.* copy-then-override in createTerrainPipeline.
  • modules/engine-graphics/src/vulkan/pipeline_manager.zig:284-285terrain_multisampling with alphaToCoverageEnable = VK_TRUE.
  • modules/engine-graphics/src/vulkan/water_system.zig:337 — water pipeline hard-coded VK_SAMPLE_COUNT_1_BIT while bound to a 4-sample HDR render pass.

Suggested fix

A. Stop inheriting alphaToCoverageEnable for the G-buffer pipeline

modules/engine-graphics/src/vulkan/pipeline_specialized.zig:130-131

var g_multisampling = std.mem.zeroes(c.VkPipelineMultisampleStateCreateInfo);
g_multisampling.sType = c.VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
g_multisampling.rasterizationSamples = c.VK_SAMPLE_COUNT_1_BIT;

(alphaToCoverageEnable, sampleShadingEnable, minSampleShading, etc. all stay at the spec defaults of VK_FALSE/0.)

B. Match water pipeline sample count to the HDR render pass

Either pass the active msaa_samples through createWaterPipeline and set rasterizationSamples = getMSAASampleCountFlag(msaa_samples), or move the water pass into its own single-sample render pass before the HDR MSAA resolve. Minimal change in modules/engine-graphics/src/vulkan/water_system.zig:281:

pub fn createWaterPipeline(
    self: *WaterSystem,
    allocator: std.mem.Allocator,
    device: c.VkDevice,
    main_render_pass: c.VkRenderPass,
    msaa_samples: u8,
) !void {
    ...
    multisampling.rasterizationSamples = getMSAASampleCountFlag(msaa_samples);
    ...
}

…and update the two callers in modules/engine-graphics/src/vulkan/rhi_init_deinit.zig:99 and modules/engine-graphics/src/vulkan/rhi_frame_orchestration.zig:129 to pass ctx.options.msaa_samples.

C. Optional: also stop inheriting alphaToCoverageEnable in any other pipeline that derives from terrain_multisampling

The wireframe/selection/line pipelines share the same pipeline_info as the terrain pipeline, so they currently do not trip the validation error (they do output alpha in terrain.frag). No change required there, but if the G-buffer fix exposes other consumers of multisampling.*, prefer std.mem.zeroes + targeted field assignments over struct copies.

Verification

After applying A and B:

nix develop --command zig fmt modules/engine-graphics/src
nix develop --command zig build test
nix develop --command zig build -Doptimize=ReleaseFast

# Manual re-run of the failing CI step
nix develop .#ci-graphics --command \
  zig build run -Dscreenshot-path=screenshot.png -Dskip-present=true
magick compare -metric RMSE docs/visual-test/golden/menu.png screenshot.png visual-diff.png

The expected RMSE should drop below the 0.015 tolerance configured via VISUAL_DIFF_RMSE_TOLERANCE in .github/workflows/visual-test.yml:105, and the VK_LAYER_KHRONOS_validation log should no longer report alphaToCoverageEnable-08891 or multisampledRenderToSingleSampled-06853 for the home-screen capture path.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdocumentationImprovements or additions to documentationenhancementNew feature or requestquestionFurther information is requestedvisual-testIssues from automated visual regression tests

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions