perf(sync): scope frame-loop barriers to swapchain image + real per-pass stages (#115)

The inter-pass and acquire/present barriers in the frame loop set both
stage masks to ALL_COMMANDS, and the inter-pass dependency used a
queue-wide VkMemoryBarrier — fully serialising against every pipeline
stage and flushing all caches every frame, when all the next pass needs
is the swapchain image the previous one wrote.

Replace the inter-pass global VkMemoryBarrier with an image memory
barrier scoped to the swapchain image's single colour subresource (as
the intra-pass UI barrier already does), and derive the barrier stage
masks per pass: RenderPass::SwapchainStage() is overridden by UIRenderer
(COMPUTE_SHADER) and RTPass (RAY_TRACING_SHADER), so a compute->compute
edge only serialises COMPUTE while an RT pass pulls in RAY_TRACING — the
acquire/present frame-edge masks use the real union of the frame's
passes (SwapchainStageUnion). The base default and the empty-passes
fallback are the conservative COMPUTE | RAY_TRACING | TRANSFER union, so
a polymorphic or un-overridden pass can only be over- not
under-synchronised.

Adds SwapchainBarrierScope (pure CPU) pinning the per-pass derivation,
the union narrowing, and the inter-pass barrier scope; FrameLoopSync
already drives the real GPU frame loop with validation enabled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
catbot 2026-06-17 19:44:33 +00:00
commit 9414863cff
6 changed files with 316 additions and 8 deletions

View file

@ -815,7 +815,16 @@ void Window::Render() {
: VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; : VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
acquireBarrier.image = images[currentBuffer]; acquireBarrier.image = images[currentBuffer];
vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &acquireBarrier); // Real per-pass stage union for this frame's passes — the swapchain image
// is only ever written by compute/RT (and possibly transfer), never by the
// whole pipeline, so the frame-edge barriers wait on exactly those stages
// instead of ALL_COMMANDS. Derived once here and reused for the present
// barrier below (passes don't change within a Render()).
const VkPipelineStageFlags swapWriterStages = SwapchainStageUnion(passes);
// The acquire->GENERAL transition only needs to be visible before the first
// pass writes the image, i.e. at the writer stages — not ALL_COMMANDS.
vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, swapWriterStages, 0, 0, nullptr, 0, nullptr, 1, &acquireBarrier);
// Synthesise key-repeat events before listeners run, so the focused // Synthesise key-repeat events before listeners run, so the focused
// widget's OnTextInput / OnKeyDown sees them in the same frame. // widget's OnTextInput / OnKeyDown sees them in the same frame.
@ -879,18 +888,28 @@ void Window::Render() {
passes[i]->Record(drawCmdBuffers[currentBuffer], currentBuffer, *this); passes[i]->Record(drawCmdBuffers[currentBuffer], currentBuffer, *this);
if (i + 1 < passes.size()) { if (i + 1 < passes.size()) {
VkMemoryBarrier mb { // Make pass i's writes to the swapchain image visible to pass i+1.
.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, // The only resource the inter-pass hazard touches is that one image
.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT, // (later passes overdraw earlier ones), so use an image memory
.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, // barrier scoped to its subresource instead of a queue-wide
}; // VkMemoryBarrier — only this image's caches round-trip, exactly as
vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &mb, 0, nullptr, 0, nullptr); // the intra-pass UI barrier already does. The stage masks come from
// each pass's own SwapchainStage(), so a compute->compute edge only
// serialises COMPUTE_SHADER while an RT pass on either side pulls in
// RAY_TRACING_SHADER — never the ALL_COMMANDS full-pipeline stall.
VkImageMemoryBarrier ib = BuildSwapchainInterPassBarrier(images[currentBuffer]);
vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer],
passes[i]->SwapchainStage(), passes[i + 1]->SwapchainStage(),
0, 0, nullptr, 0, nullptr, 1, &ib);
} }
} }
presentBarrier.image = images[currentBuffer]; presentBarrier.image = images[currentBuffer];
vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &presentBarrier); // The ->PRESENT_SRC transition only needs to wait on the stages that
// actually wrote the image (the per-pass union), not ALL_COMMANDS; present
// itself is gated by the render-finished semaphore, so dst stays BOTTOM.
vkCmdPipelineBarrier(drawCmdBuffers[currentBuffer], swapWriterStages, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &presentBarrier);
Device::CheckVkResult(vkEndCommandBuffer(drawCmdBuffers[currentBuffer])); Device::CheckVkResult(vkEndCommandBuffer(drawCmdBuffers[currentBuffer]));

View file

@ -35,6 +35,13 @@ export namespace Crafter {
RTPass(PipelineRTVulkan* p) : pipeline(p) {} RTPass(PipelineRTVulkan* p) : pipeline(p) {}
// An RT pass writes the swapchain image from the ray-tracing pipeline,
// so the frame loop's barriers must wait on RAY_TRACING_SHADER — using
// the compute stage here would under-synchronise and corrupt the image.
VkPipelineStageFlags SwapchainStage() const override {
return VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR;
}
void Record(VkCommandBuffer cmd, std::uint32_t frameIdx, Window& window) override { void Record(VkCommandBuffer cmd, std::uint32_t frameIdx, Window& window) override {
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_RAY_TRACING_KHR, pipeline->pipeline); vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_RAY_TRACING_KHR, pipeline->pipeline);
// NVIDIA descriptor-heap AS-read workaround (issue #15 / #7): feed // NVIDIA descriptor-heap AS-read workaround (issue #15 / #7): feed

View file

@ -16,6 +16,10 @@ You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/ */
module;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
#include "vulkan/vulkan.h"
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
export module Crafter.Graphics:RenderPass; export module Crafter.Graphics:RenderPass;
import std; import std;
import :GraphicsTypes; import :GraphicsTypes;
@ -23,8 +27,74 @@ import :GraphicsTypes;
export namespace Crafter { export namespace Crafter {
struct Window; struct Window;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
// Conservative union of every pipeline stage that can write the swapchain
// storage image: a compute pass via COMPUTE_SHADER, a ray-tracing pass via
// RAY_TRACING_SHADER, plus TRANSFER for any blit/copy writer. Used as the
// RenderPass default (a pass that doesn't narrow its own stage) and as the
// fallback when there are no passes to derive a real union from. Far tighter
// than ALL_COMMANDS, but never under-synchronises a polymorphic pass.
inline constexpr VkPipelineStageFlags kSwapchainWriterStages =
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT
| VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR
| VK_PIPELINE_STAGE_TRANSFER_BIT;
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
struct RenderPass { struct RenderPass {
virtual void Record(GraphicsCommandBuffer cmd, std::uint32_t frameIdx, Window& window) = 0; virtual void Record(GraphicsCommandBuffer cmd, std::uint32_t frameIdx, Window& window) = 0;
virtual ~RenderPass() = default; virtual ~RenderPass() = default;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
// Pipeline stage at which this pass reads and writes the swapchain
// storage image. The frame loop uses this to scope the inter-pass and
// frame-edge barriers to the stages that actually touch the image
// instead of ALL_COMMANDS. A subclass MUST narrow this to its real
// stage (RTPass -> RAY_TRACING_SHADER, a compute pass -> COMPUTE_SHADER):
// the default is the conservative writer union so an un-overridden pass
// can never be under-synchronised, only over-synchronised.
virtual VkPipelineStageFlags SwapchainStage() const { return kSwapchainWriterStages; }
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
};
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
// Union of the swapchain-access stages declared by `passes` — the "real"
// per-pass stage union the frame-edge barriers (acquire dst / present src)
// narrow to: an all-compute frame yields COMPUTE_SHADER only, a frame with
// any RT pass folds in RAY_TRACING_SHADER, and so on. Falls back to the
// conservative writer union when there are no passes (the image is still
// transitioned, just never written), so the barrier is always well-formed.
inline VkPipelineStageFlags SwapchainStageUnion(std::span<RenderPass* const> passes) {
if (passes.empty()) return kSwapchainWriterStages;
VkPipelineStageFlags stages = 0;
for (RenderPass* pass : passes) stages |= pass->SwapchainStage();
return stages;
}
// The inter-pass swapchain barrier (replacing the old queue-wide
// VkMemoryBarrier at the #115 location). Scoped to the swapchain image's
// single colour subresource so only that image's shader caches round-trip —
// unrelated buffers/images stay resident — exactly as the intra-pass UI
// barrier already does. The image is bound as a storage image and stays in
// VK_IMAGE_LAYOUT_GENERAL, so this is a pure memory dependency (no layout
// transition). The execution scope (stage masks) is supplied per pass pair
// by the caller via vkCmdPipelineBarrier.
inline VkImageMemoryBarrier BuildSwapchainInterPassBarrier(VkImage image) {
return {
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
.oldLayout = VK_IMAGE_LAYOUT_GENERAL,
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = image,
.subresourceRange = {
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.baseMipLevel = 0,
.levelCount = 1,
.baseArrayLayer = 0,
.layerCount = 1,
},
}; };
} }
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
}

View file

@ -201,6 +201,15 @@ export namespace Crafter {
void Record(GraphicsCommandBuffer cmd, std::uint32_t frameIdx, Window& window) override; void Record(GraphicsCommandBuffer cmd, std::uint32_t frameIdx, Window& window) override;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
// A UI pass draws into the swapchain image entirely from compute
// shaders (see Dispatch), so the frame loop's inter-pass / frame-edge
// barriers only need to synchronise the COMPUTE_SHADER stage for it.
VkPipelineStageFlags SwapchainStage() const override {
return VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
}
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
UIDispatchHeader FillHeader(std::uint32_t itemBufferSlot, UIDispatchHeader FillHeader(std::uint32_t itemBufferSlot,
std::uint32_t itemCount, std::uint32_t itemCount,
std::array<float,4> clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f}, std::array<float,4> clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f},

View file

@ -473,6 +473,35 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
mbc.GetInterfacesAndImplementations(ifaces, mipBarrierImpls); mbc.GetInterfacesAndImplementations(ifaces, mipBarrierImpls);
cfg.tests.push_back(std::move(mipBarrierTest)); cfg.tests.push_back(std::move(mipBarrierTest));
// Issue #115: the frame loop's inter-pass and acquire/present barriers
// no longer use ALL_COMMANDS / a queue-wide VkMemoryBarrier. The stage
// masks are derived per pass via RenderPass::SwapchainStage() (compute
// vs ray-tracing) and unioned across the frame by SwapchainStageUnion,
// and the inter-pass dependency is scoped to the swapchain image by
// BuildSwapchainInterPassBarrier — all pure CPU logic over the pass
// list, so this test drives them directly with no GPU at runtime.
Test swapBarrierTest;
Configuration& sbc = swapBarrierTest.config;
sbc.path = cfg.path;
sbc.name = "SwapchainBarrierScope";
sbc.outputName = "SwapchainBarrierScope";
sbc.type = ConfigurationType::Executable;
sbc.target = cfg.target;
sbc.march = cfg.march;
sbc.mtune = cfg.mtune;
sbc.debug = cfg.debug;
sbc.sysroot = cfg.sysroot;
sbc.dependencies = cfg.dependencies;
sbc.externalDependencies = cfg.externalDependencies;
sbc.compileFlags = cfg.compileFlags;
sbc.linkFlags = cfg.linkFlags;
sbc.defines = cfg.defines;
sbc.cFiles = cfg.cFiles;
std::vector<fs::path> swapBarrierImpls(impls.begin(), impls.end());
swapBarrierImpls.emplace_back("tests/SwapchainBarrierScope/main");
sbc.GetInterfacesAndImplementations(ifaces, swapBarrierImpls);
cfg.tests.push_back(std::move(swapBarrierTest));
// Issue #47: the fused UI uber-kernel (shaders/ui-fused.comp.glsl) and // Issue #47: the fused UI uber-kernel (shaders/ui-fused.comp.glsl) and
// its C++ push-constant mirror UIFusedHeader. Compiles the real shader // its C++ push-constant mirror UIFusedHeader. Compiles the real shader
// with glslang, validates with spirv-val, and pins the push-constant // with glslang, validates with spirv-val, and pins the push-constant

View file

@ -0,0 +1,174 @@
/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
catcrafts.net
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License version 3.0 as published by the Free Software Foundation;
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
// Issue #115: the frame loop's inter-pass and frame-edge (acquire/present)
// barriers used to set BOTH stage masks to VK_PIPELINE_STAGE_ALL_COMMANDS_BIT
// and the inter-pass dependency to a queue-wide VkMemoryBarrier — flushing
// every cache and fully serialising against the whole pipeline every frame,
// when all the next pass needs is the swapchain image the previous one wrote.
//
// The fix scopes the inter-pass dependency to the swapchain image's single
// colour subresource (BuildSwapchainInterPassBarrier) and narrows the stage
// masks to the stages that actually touch that image, derived per pass via the
// polymorphic RenderPass::SwapchainStage() and unioned across the frame by
// SwapchainStageUnion. The load-bearing correctness constraint is that this
// derivation is per-pass: a compute pass writes via COMPUTE_SHADER, a ray-
// tracing pass via RAY_TRACING_SHADER, so a hardcoded compute mask would
// UNDER-synchronise an RT pass and corrupt the image. All three helpers are
// pure CPU logic over the pass list, so this test drives them directly with no
// GPU device at runtime — mirroring MipChainBarrierBatch / UploadStrategy.
#include <cstdlib>
#include "vulkan/vulkan.h"
import Crafter.Graphics;
import std;
using namespace Crafter;
namespace {
int failures = 0;
void Check(bool ok, std::string_view what) {
std::println("{} {}", ok ? "PASS" : "FAIL", what);
if (!ok) ++failures;
}
// A pass that never overrides SwapchainStage(): stands in for any future pass
// that forgets to narrow its stage, and must fall back to the conservative
// writer union rather than silently picking one stage (which could under-sync).
struct DefaultPass : RenderPass {
void Record(GraphicsCommandBuffer, std::uint32_t, Window&) override {}
};
VkImage SentinelImage() {
return reinterpret_cast<VkImage>(static_cast<std::uintptr_t>(0xC0FFEE));
}
} // namespace
int main() {
// ─── per-pass SwapchainStage() polymorphism ─────────────────────────────
// The whole reason the fix is "moderate, not trivial": these two stages
// MUST differ, or scoping an RT pass's barrier to the compute stage would
// under-synchronise it. Pin that they are distinct and each correct.
Check(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT != VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR,
"compute and ray-tracing write stages are distinct (the under-sync trap)");
UIRenderer ui; // real compute UI pass
RTPass rt(nullptr); // real RT pass (SwapchainStage doesn't touch the pipeline)
DefaultPass def; // un-overridden fallback
// Access through the base pointer — the frame loop only ever sees RenderPass*.
RenderPass* uiBase = &ui;
RenderPass* rtBase = &rt;
RenderPass* defBase = &def;
Check(uiBase->SwapchainStage() == VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
"UIRenderer reports the COMPUTE_SHADER stage");
Check(rtBase->SwapchainStage() == VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR,
"RTPass reports the RAY_TRACING_SHADER stage");
Check(defBase->SwapchainStage() == kSwapchainWriterStages,
"an un-overridden pass falls back to the conservative writer union");
// ─── the conservative writer union constant ─────────────────────────────
constexpr VkPipelineStageFlags expectedUnion =
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT
| VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR
| VK_PIPELINE_STAGE_TRANSFER_BIT;
Check(kSwapchainWriterStages == expectedUnion,
"writer union = COMPUTE_SHADER | RAY_TRACING_SHADER | TRANSFER");
// The point of the whole change: even the conservative fallback is far
// tighter than the ALL_COMMANDS mask it replaces.
Check(kSwapchainWriterStages != VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
"writer union is narrower than ALL_COMMANDS");
// ─── SwapchainStageUnion over the frame's pass list ─────────────────────
// Empty frame: no writers to derive from, so fall back to the conservative
// union (the image is still transitioned, just never written).
Check(SwapchainStageUnion(std::span<RenderPass* const>{}) == kSwapchainWriterStages,
"empty pass list falls back to the conservative writer union");
// All-compute frame: the union narrows to COMPUTE_SHADER ONLY — this is the
// real perf win, no RAY_TRACING / TRANSFER / ALL_COMMANDS dragged in.
{
std::array<RenderPass*, 2> computeOnly = {uiBase, uiBase};
VkPipelineStageFlags u = SwapchainStageUnion(computeOnly);
Check(u == VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
"all-compute frame narrows the union to COMPUTE_SHADER only");
Check((u & VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR) == 0,
"all-compute frame does NOT pull in RAY_TRACING_SHADER");
}
// RT-only frame: narrows to RAY_TRACING_SHADER only.
{
std::array<RenderPass*, 1> rtOnly = {rtBase};
Check(SwapchainStageUnion(rtOnly) == VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR,
"RT-only frame narrows the union to RAY_TRACING_SHADER only");
}
// Mixed frame: the union folds in BOTH stages — never under-syncs the RT
// pass, never over-syncs to ALL_COMMANDS.
{
std::array<RenderPass*, 2> mixed = {uiBase, rtBase};
VkPipelineStageFlags u = SwapchainStageUnion(mixed);
Check(u == (VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR),
"mixed compute+RT frame unions COMPUTE_SHADER | RAY_TRACING_SHADER");
Check((u & VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR) != 0,
"mixed frame keeps RAY_TRACING_SHADER (no RT under-sync)");
}
// A frame containing an un-overridden pass conservatively widens to the
// full writer union — safe by construction.
{
std::array<RenderPass*, 2> withDefault = {uiBase, defBase};
Check(SwapchainStageUnion(withDefault) == kSwapchainWriterStages,
"a frame with an un-overridden pass widens to the conservative union");
}
// ─── BuildSwapchainInterPassBarrier ─────────────────────────────────────
// Replaces the queue-wide VkMemoryBarrier: an image memory barrier scoped
// to the swapchain image's single colour subresource, GENERAL->GENERAL (no
// layout change — the storage image stays bound), matching the intra-pass
// UI barrier's access scope.
{
VkImageMemoryBarrier b = BuildSwapchainInterPassBarrier(SentinelImage());
Check(b.sType == VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, "inter-pass barrier sType is IMAGE_MEMORY_BARRIER");
Check(b.image == SentinelImage(), "inter-pass barrier carries the swapchain image handle");
Check(b.oldLayout == VK_IMAGE_LAYOUT_GENERAL && b.newLayout == VK_IMAGE_LAYOUT_GENERAL,
"inter-pass barrier is GENERAL->GENERAL (pure memory dependency, no transition)");
Check(b.srcAccessMask == VK_ACCESS_SHADER_WRITE_BIT,
"inter-pass barrier src access is shader write");
Check(b.dstAccessMask == (VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT),
"inter-pass barrier dst access is shader read|write");
Check(b.srcQueueFamilyIndex == VK_QUEUE_FAMILY_IGNORED && b.dstQueueFamilyIndex == VK_QUEUE_FAMILY_IGNORED,
"inter-pass barrier is not a queue-family transfer");
Check(b.subresourceRange.aspectMask == VK_IMAGE_ASPECT_COLOR_BIT,
"inter-pass barrier covers the colour aspect");
Check(b.subresourceRange.levelCount == 1 && b.subresourceRange.layerCount == 1,
"inter-pass barrier is scoped to the single swapchain subresource");
}
if (failures != 0) {
std::println("{} check(s) failed", failures);
return EXIT_FAILURE;
}
std::println("all checks passed");
return EXIT_SUCCESS;
}