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:130 — g_multisampling = multisampling.* copy-then-override in createTerrainPipeline.
modules/engine-graphics/src/vulkan/pipeline_manager.zig:284-285 — terrain_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
Visual Test Failure
The nightly
visual-testworkflow (run 29078561400) regressed the menu golden-image comparison. The game launches successfully, Lavapipe renders, andscreenshot.pngis captured, but the pixel diff againstdocs/visual-test/golden/menu.pngis RMSE 0.0436523, which is ~2.9x the configuredVISUAL_DIFF_RMSE_TOLERANCEof 0.015.Exact error output
The
Compare against golden imagestep invisual-test.yml(line 99-105) exits with status 1:The build log captured by
run-with-logshows two Vulkan validation errors that point at broken pipeline contracts introduced (or exposed) by commit9feb275"refactor: remove legacy lighting paths (#910)":Followed at runtime by image-layout violations on the offscreen swapchain image (VkImage
0x180000000018):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_pipelineinheritsalphaToCoverageEnable = VK_TRUEmodules/engine-graphics/src/vulkan/pipeline_manager.zig:280-285builds the shared multisample state:createTerrainPipelinethen forwards&terrain_multisampling(which hasalphaToCoverageEnable = VK_TRUE) as themultisamplingparameter topipeline_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 overridingrasterizationSamples:But
assets/shaders/vulkan/g_pass.fragdeclareslayout(location = 0) out vec3 outNormal;(no alpha component). Vulkan therefore rejects the pipeline withVUID-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_pipelinesample-count mismatch withhdr_render_passmodules/engine-graphics/src/vulkan/water_system.zig:281-387constructs the water pipeline withrasterizationSamples = c.VK_SAMPLE_COUNT_1_BITbut is invoked fromrhi_init_deinit.zig:99withctx.render_pass_manager.hdr_render_pass. When MSAA is on (defaultmsaa_samples = 4permodules/game-core/src/settings/data.zig:113), that render pass hassampleCount = 4on its color and depth attachments (render_pass_manager.zig:129, 139), producingVUID-VkGraphicsPipelineCreateInfo-multisampledRenderToSingleSampled-06853.Why this regresses only the menu capture
ui.frag,ui_tex.frag) into the offscreen HDR MSAA color/depth attachments.post_process_system.zig,bloom_system.zig, FXAA, etc.) and finally copied into the swapchain image via the UI swapchain render pass.VK_IMAGE_LAYOUT_*transitions on the offscreen image leave the swapchain contents in an inconsistent state at the momentscreenshot.captureFrameissues itsvkCmdCopyImageToBuffer.Origin of the failure
modules/engine-graphics/src/vulkan/pipeline_specialized.zig:130—g_multisampling = multisampling.*copy-then-override increateTerrainPipeline.modules/engine-graphics/src/vulkan/pipeline_manager.zig:284-285—terrain_multisamplingwithalphaToCoverageEnable = VK_TRUE.modules/engine-graphics/src/vulkan/water_system.zig:337— water pipeline hard-codedVK_SAMPLE_COUNT_1_BITwhile bound to a 4-sample HDR render pass.Suggested fix
A. Stop inheriting
alphaToCoverageEnablefor the G-buffer pipelinemodules/engine-graphics/src/vulkan/pipeline_specialized.zig:130-131(
alphaToCoverageEnable,sampleShadingEnable,minSampleShading, etc. all stay at the spec defaults ofVK_FALSE/0.)B. Match water pipeline sample count to the HDR render pass
Either pass the active
msaa_samplesthroughcreateWaterPipelineand setrasterizationSamples = getMSAASampleCountFlag(msaa_samples), or move the water pass into its own single-sample render pass before the HDR MSAA resolve. Minimal change inmodules/engine-graphics/src/vulkan/water_system.zig:281:…and update the two callers in
modules/engine-graphics/src/vulkan/rhi_init_deinit.zig:99andmodules/engine-graphics/src/vulkan/rhi_frame_orchestration.zig:129to passctx.options.msaa_samples.C. Optional: also stop inheriting
alphaToCoverageEnablein any other pipeline that derives fromterrain_multisamplingThe wireframe/selection/line pipelines share the same
pipeline_infoas the terrain pipeline, so they currently do not trip the validation error (they do output alpha interrain.frag). No change required there, but if the G-buffer fix exposes other consumers ofmultisampling.*, preferstd.mem.zeroes+ targeted field assignments over struct copies.Verification
After applying A and B:
The expected RMSE should drop below the 0.015 tolerance configured via
VISUAL_DIFF_RMSE_TOLERANCEin.github/workflows/visual-test.yml:105, and theVK_LAYER_KHRONOS_validationlog should no longer reportalphaToCoverageEnable-08891ormultisampledRenderToSingleSampled-06853for the home-screen capture path.Related