//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // Issue #160: the swapchain gets as many images as its present mode needs, // and the frame loop takes however many that is. CreateSwapchain used to ask // for exactly Window::numFrames (3) images whatever the surface said, read // them into a three-element array, and key everything a frame in flight // owns, its command buffer, its fence and the descriptor-heap copy holding // "its" swapchain image, by the acquired image index. A swapchain of any // other size fell back to FIFO (asked for MAILBOX or IMMEDIATE) or threw, // and a surface whose minimum is above three got a create info below it. // Mesa's MAILBOX needs at least four images: Turnip on phoc, and llvmpipe on // any Wayland compositor. // // Checked here: // // - Window::SwapchainMinImageCount on the numbers drivers report. // - For every present mode the surface offers, SetPresentMode makes a // swapchain of that mode, with at least the images the window asks for // (Window::SwapchainCapabilities, the mode's own count where the device // has swapchain_maintenance1). // - Every frame draws into the image it presents. A UI pass fills the // window with a colour that differs from frame to frame, and a transfer // pass after it copies the middle texel of the frame's swapchain image // into the frame slot's buffer, read back when the slot comes round // again. With an image count other than numFrames, frames and images // pair up differently from frame to frame: a heap copy that kept one // image would have the UI draw into another frame's image, and the texel // would hold an older colour. // - A SetPresentMode or Resize from a listener of the frame being recorded // takes effect after that frame's present: the rest of the frame indexes // the swapchain it acquired from. // - A swapchain slot listed during a frame is written for that frame (heap // mode, where the descriptor can be read back and compared with the UI's). // - Two windows sharing one heap each draw their own colours: a window // writes only its own swapchain slots. // - No Vulkan validation errors, which only means something with // CRAFTER_GRAPHICS_VALIDATION set and the layer loaded. // // The GPU this process picks may give every mode numFrames images (NVIDIA // does), which leaves the case that matters untried, and a process runs one // binding model. So the frame checks also run in child processes, picked // with CRAFTER_GRAPHICS_DEVICE, on a GPU whose surface needs more images for // some mode (this process's own, else llvmpipe, which most Linux machines // have), in each binding model the parent didn't run there: on the // descriptor heap, and with CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP on descriptor // sets, the path Turnip takes. Without such a GPU that part is a note, not a // failure. `--frames heap|sets` runs the frame checks alone on whichever GPU // the environment picks. // // Needs a live Wayland compositor and a Vulkan device, like FrameLoopSync. #include "vulkan/vulkan.h" #include "vulkan/vk_enum_string_helper.h" #include #include import Crafter.Graphics; import Crafter.Event; import std; using namespace Crafter; namespace fs = std::filesystem; namespace { int failures = 0; void Check(bool ok, std::string_view what) { std::println("{} {}", ok ? "PASS" : "FAIL", what); if (!ok) ++failures; } // The test binary's own directory: the build places the UI kernels (both // variants) next to it, and the test runner's working directory is not that // directory. fs::path ExeDir() { return fs::read_symlink("/proc/self/exe").parent_path(); } int RunCommand(const std::string& cmd) { int status = std::system(cmd.c_str()); if (status == -1) return -1; return (status & 0x7f) == 0 ? ((status >> 8) & 0xff) : 128 + (status & 0x7f); } struct ModeCase { PresentMode mode; VkPresentModeKHR vk; std::string_view name; }; constexpr ModeCase kModes[] = { { PresentMode::Fifo, VK_PRESENT_MODE_FIFO_KHR, "FIFO" }, { PresentMode::Mailbox, VK_PRESENT_MODE_MAILBOX_KHR, "MAILBOX" }, { PresentMode::Immediate, VK_PRESENT_MODE_IMMEDIATE_KHR, "IMMEDIATE" }, }; std::vector OfferedModes(VkPhysicalDevice gpu, VkSurfaceKHR surface) { std::uint32_t count = 0; Device::CheckVkResult(vkGetPhysicalDeviceSurfacePresentModesKHR(gpu, surface, &count, nullptr)); std::vector modes(count); Device::CheckVkResult(vkGetPhysicalDeviceSurfacePresentModesKHR(gpu, surface, &count, modes.data())); return modes; } bool Offers(const std::vector& offered, VkPresentModeKHR mode) { return std::ranges::find(offered, mode) != offered.end(); } // The most images any present mode under test asks for, for a swapchain of // `surface` on `gpu`, sized as a window on it would size it. std::uint32_t MostImagesAsked(VkPhysicalDevice gpu, VkSurfaceKHR surface, bool modePassedAlong) { const std::vector offered = OfferedModes(gpu, surface); std::uint32_t most = 0; for (const ModeCase& m : kModes) { if (Offers(offered, m.vk)) { most = std::max(most, Window::SwapchainMinImageCount( Window::SwapchainCapabilities(gpu, surface, m.vk, modePassedAlong))); } } return most; } void CheckMinImageCount() { const auto asked = [](std::uint32_t minImageCount, std::uint32_t maxImageCount) { VkSurfaceCapabilitiesKHR capabilities{}; capabilities.minImageCount = minImageCount; capabilities.maxImageCount = maxImageCount; return Window::SwapchainMinImageCount(capabilities); }; Check(asked(2, 8) == Window::numFrames, "a minimum below numFrames asks for numFrames (NVIDIA: 2, at most 8)"); Check(asked(3, 0) == 3, "a minimum of numFrames asks for numFrames (Mesa's FIFO: 3, no maximum)"); Check(asked(4, 0) == 4, "a minimum above numFrames asks for the minimum (Mesa's MAILBOX: 4, no maximum)"); Check(asked(5, 6) == 5, "a minimum above numFrames within the maximum asks for the minimum"); Check(asked(1, 2) == 2, "a maximum below numFrames caps the request"); Check(asked(2, 0) == Window::numFrames, "no maximum leaves numFrames as it is"); } // A colour for frame `n` that differs from every frame's within 200 of it, // exact in eight bits. std::array Colour(std::uint64_t n) { return { static_cast(40 + (n * 7) % 200), static_cast(250 - (n * 13) % 200), static_cast((n * 29) % 256), }; } // Copies the middle texel of the frame's swapchain image into the frame // slot's buffer, after the UI pass drew it: what the frame presents. Added // only for an 8-bit RGBA or BGRA swapchain, whose texel fits the buffer. struct ReadbackPass : RenderPass { std::array, Window::numFrames> texels; void Record(VkCommandBuffer cmd, std::uint32_t frameIdx, Window& window) override { const VkBufferImageCopy region { .imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }, .imageOffset = { static_cast(window.width / 2), static_cast(window.height / 2), 0 }, .imageExtent = { 1, 1, 1 }, }; vkCmdCopyImageToBuffer(cmd, window.images[window.currentImage], VK_IMAGE_LAYOUT_GENERAL, texels[frameIdx].buffer, 1, ®ion); const VkBufferMemoryBarrier copied { .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, .dstAccessMask = VK_ACCESS_HOST_READ_BIT, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .buffer = texels[frameIdx].buffer, .offset = 0, .size = VK_WHOLE_SIZE, }; vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 0, nullptr, 1, &copied, 0, nullptr); } VkPipelineStageFlags SwapchainStage() const override { return VK_PIPELINE_STAGE_TRANSFER_BIT; } }; // One window filled with a colour per frame through a UIRenderer on `heap`, // and what each frame presented read back. Holds listeners that capture it: // build it in place and don't move it. struct ColourScene { Window& window; GraphicsDescriptorHeap& heap; std::string_view name; // Added to the frame number: two scenes sharing a heap draw different // colours, so one that drew into the other's image is seen. std::uint64_t colourOffset; UIRenderer ui; // One quad buffer per frame slot: the colour changes every frame, and a // buffer the frames shared would be overwritten under the frames still // in flight. std::array, Window::numFrames> quads; std::array quadSlots; ReadbackPass readback; bool bgra = false; bool checkTexels = false; // The frame each slot's texel buffer holds, -1 for none, and what the // texel checks found since Reset. std::array frameOfSlot; std::uint64_t frame = 0; std::uint64_t framesChecked = 0; std::uint32_t wrong = 0; std::string firstWrong; std::set imagesUsed; std::set> pairsUsed; // Run once at the start of the next frame's build, then dropped. std::function duringNextFrame; EventListener buildSub; ColourScene(Window& window, GraphicsDescriptorHeap& heap, std::string_view name, std::uint64_t colourOffset) : window(window), heap(heap), name(name), colourOffset(colourOffset) { const fs::path dir = ExeDir(); VkCommandBuffer init = window.StartInit(); ui.Initialize(window, heap, init, dir / "ui-quads.comp.spv", dir / "ui-circles.comp.spv", dir / "ui-images.comp.spv", dir / "ui-text.comp.spv", dir / "ui-fused.comp.spv"); for (std::uint32_t f = 0; f < Window::numFrames; ++f) { quads[f].Create(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, 1, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); quadSlots[f] = ui.RegisterBuffer(quads[f]); } bgra = window.colorFormat == VK_FORMAT_B8G8R8A8_UNORM; checkTexels = bgra || window.colorFormat == VK_FORMAT_R8G8B8A8_UNORM; window.passes.push_back(&ui); if (checkTexels) { for (VulkanBuffer& texel : readback.texels) { texel.Create(VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, 1); } window.passes.push_back(&readback); } else { std::println("NOTE {}: the swapchain format {} isn't 8-bit RGBA or BGRA: its colours aren't checked", name, string_VkFormat(window.colorFormat)); } window.FinishInit(); Reset(); buildSub.SetEvent(&ui.onBuild, [this](UIBuildArgs a) { if (duringNextFrame) { std::function run = std::move(duringNextFrame); duringNextFrame = nullptr; run(); } // The slot's fence has been waited: its buffer holds what the // frame numFrames ago presented. This frame's copy comes after. CheckSlot(a.frameIdx); frameOfSlot[a.frameIdx] = static_cast(frame); imagesUsed.insert(this->window.currentImage); pairsUsed.insert({ a.frameIdx, this->window.currentImage }); const std::array colour = Colour(frame + this->colourOffset); quads[a.frameIdx].value[0] = QuadItem{ 0, 0, 4096, 4096, colour[0] / 255.0f, colour[1] / 255.0f, colour[2] / 255.0f, 1.0f, 0, 0, 0, 0, 0, 0, 0, 0 }; quads[a.frameIdx].FlushDevice(a.cmd, VK_ACCESS_SHADER_READ_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); ui.DispatchQuads(a.cmd, quadSlots[a.frameIdx], 1); }); } ~ColourScene() { std::erase(window.passes, &ui); std::erase(window.passes, &readback); } void Reset() { frameOfSlot.fill(-1); framesChecked = 0; wrong = 0; firstWrong.clear(); imagesUsed.clear(); pairsUsed.clear(); } void CheckSlot(std::uint32_t slot) { if (!checkTexels || frameOfSlot[slot] < 0) return; const std::uint32_t texel = readback.texels[slot].value[0]; const std::array rgb = bgra ? std::array{ static_cast(texel >> 16), static_cast(texel >> 8), static_cast(texel) } : std::array{ static_cast(texel), static_cast(texel >> 8), static_cast(texel >> 16) }; const std::array expected = Colour(static_cast(frameOfSlot[slot]) + colourOffset); bool same = true; for (std::size_t c = 0; c < 3; ++c) { same = same && std::abs(static_cast(rgb[c]) - static_cast(expected[c])) <= 1; } ++framesChecked; if (!same && wrong++ == 0) { firstWrong = std::format("frame {} presented ({}, {}, {}), drew ({}, {}, {})", frameOfSlot[slot], rgb[0], rgb[1], rgb[2], expected[0], expected[1], expected[2]); } } // One frame of this scene's window. void Render() { window.Render(); ++frame; } // After the frames since Reset: settles the queue, reads the slots not // read yet, and reports. void Report(std::string_view what) { Device::CheckVkResult(vkQueueWaitIdle(Device::queue)); for (std::uint32_t slot = 0; slot < Window::numFrames; ++slot) { CheckSlot(slot); frameOfSlot[slot] = -1; } if (checkTexels) { Check(wrong == 0, std::format("{}: each of {} frames presented the colour it drew{}", what, framesChecked, wrong == 0 ? std::string() : std::format(" ({} didn't; {})", wrong, firstWrong))); } } }; // The present modes on the scene's window, then a rebuild asked for during a // frame, then a slot listed during one. Sets `moreImagesThanFrames` when some // mode's swapchain had more images than numFrames. void CheckModes(ColourScene& scene, bool& moreImagesThanFrames) { Window& window = scene.window; const std::vector offered = OfferedModes(Device::physDevice, window.vulkanSurface); for (const ModeCase& m : kModes) { if (!Offers(offered, m.vk)) { std::println("NOTE the surface doesn't offer {}", m.name); continue; } // Asked before the swapchain is made, which asks again: the // validation layer checks the image count against the last answer. const VkSurfaceCapabilitiesKHR capabilities = Window::SwapchainCapabilities( Device::physDevice, window.vulkanSurface, m.vk, Device::swapchainMaintenance1Supported); const std::uint32_t asked = Window::SwapchainMinImageCount(capabilities); window.SetPresentMode(m.mode); Check(window.swapchainPresentMode == m.vk, std::format("{}: the swapchain was made with it, not a fallback ({})", m.name, string_VkPresentModeKHR(window.swapchainPresentMode))); const std::size_t images = window.images.size(); Check(images >= asked && images >= capabilities.minImageCount, std::format("{}: {} images, at least the {} asked for and the mode's minimum of {}", m.name, images, asked, capabilities.minImageCount)); Check(window.imageViewInfos.size() == images && window.imageViews.size() == images && window.imageInitialised.size() == images && window.renderFinishedSemaphores.size() >= images, std::format("{}: the per-image state covers all {} images", m.name, images)); moreImagesThanFrames = moreImagesThanFrames || images > Window::numFrames; scene.Reset(); // Enough frames for every frame slot to meet several images. const std::uint64_t frames = 4 * std::max(images, Window::numFrames); for (std::uint64_t i = 0; i < frames; ++i) { scene.Render(); } scene.Report(m.name); Check(scene.imagesUsed.size() > 1, std::format("{}: the frames went round {} of the {} images", m.name, scene.imagesUsed.size(), images)); std::println(" {}: {} images, {} pairings of frame slot and image", m.name, images, scene.pairsUsed.size()); } // A present mode set, and a resize asked for, from a listener of the // frame being recorded: the frame goes on with the swapchain it // acquired from, and the new one is made after its present. const ModeCase* first = nullptr; const ModeCase* second = nullptr; for (const ModeCase& m : kModes) { if (!Offers(offered, m.vk)) continue; (first == nullptr ? first : second) = &m; if (second != nullptr) break; } if (second != nullptr) { window.SetPresentMode(first->mode); const VkSwapchainKHR before = window.swapChain; const std::uint32_t widthBefore = window.width; const std::uint32_t heightBefore = window.height; bool unchangedInFrame = false; scene.duringNextFrame = [&] { window.SetPresentMode(second->mode); window.Resize(widthBefore + 16, heightBefore + 8); unchangedInFrame = window.swapChain == before && window.swapchainPresentMode == first->vk && window.width == widthBefore && window.height == heightBefore && window.currentImage < window.images.size(); }; scene.Reset(); scene.Render(); Check(unchangedInFrame, std::format("a {} -> {} switch and a resize asked for during a frame leave that frame's swapchain and size alone", first->name, second->name)); Check(window.swapChain != before && window.swapchainPresentMode == second->vk, std::format("after the frame the swapchain is made again, with {} ({})", second->name, string_VkPresentModeKHR(window.swapchainPresentMode))); Check(window.width == widthBefore + 16 && window.height == heightBefore + 8, std::format("after the frame the window has the size asked for ({}x{})", window.width, window.height)); for (int i = 0; i < 2 * Window::numFrames; ++i) { scene.Render(); } scene.Report("after the rebuild a frame asked for"); } else { std::println("NOTE the surface offers one present mode: no mid-frame switch to check"); } // A swapchain slot listed during a frame holds that frame's image at // once: in heap mode the descriptor bytes match the UI output slot's, // which the frame wrote before onUpdate. if (Device::descriptorHeapSupported) { const ImageSlotRange spare = scene.heap.AllocateImageSlots(1); bool same = false; scene.duringNextFrame = [&] { scene.heap.AddSwapchainSlot(spare.firstElement, window); const std::uint8_t* copy = scene.heap.resourceHeap[window.currentFrame].value; same = std::memcmp(copy + scene.heap.ImageByteOffset(spare.firstElement), copy + scene.heap.ImageByteOffset(scene.ui.OutImageSlot()), Device::descriptorHeapProperties.imageDescriptorSize) == 0; }; scene.Render(); Device::CheckVkResult(vkQueueWaitIdle(Device::queue)); scene.heap.FreeImageSlots(spare); Check(same, "a swapchain slot listed during a frame holds that frame's image at once"); } } // Renders on this process's GPU: every offered mode on one window, then two // windows sharing the heap. Sets `moreImagesThanFrames` when some mode's // swapchain had more images than numFrames. int RenderFrames(bool& moreImagesThanFrames) { // Two scenes' output slots, one spare for the mid-frame slot, and each // scene's quad buffers. GraphicsDescriptorHeap heap; heap.Initialize(/*images*/ 3, /*buffers*/ 2 * Window::numFrames, /*samplers*/ 1); { Window window(160, 120, "SwapchainImageCount test"); window.descriptorHeap = &heap; { ColourScene scene(window, heap, "first window", 0); CheckModes(scene, moreImagesThanFrames); // A second window on the same heap. Interleaved frames: one // window's slot writes land in copies the other's frames in // flight are bound to, and must not touch its slots. Window second(160, 120, "SwapchainImageCount second window"); second.descriptorHeap = &heap; { ColourScene other(second, heap, "second window", 100); scene.Reset(); for (int i = 0; i < 4 * Window::numFrames; ++i) { scene.Render(); other.Render(); } scene.Report("two windows on one heap, the first"); other.Report("two windows on one heap, the second"); } Device::CheckVkResult(vkQueueWaitIdle(Device::queue)); } Device::CheckVkResult(vkQueueWaitIdle(Device::queue)); } if (std::getenv("CRAFTER_GRAPHICS_VALIDATION") != nullptr) { Check(Device::validationErrorCount == 0, std::format("no Vulkan validation errors ({} seen)", Device::validationErrorCount)); } return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; } // A child's run of the frame checks in binding model `model` ("heap" or // "sets") on whatever GPU its environment picks. int FramesOnly(std::string_view model) { try { Device::Initialize(); } catch (const std::runtime_error& error) { // A missing capability is a skip; anything else that failed is a // failure of the run that was asked for. if (std::string_view(error.what()).find("can't run this application") != std::string_view::npos) { std::println("SKIP this GPU can't run the library: {}", error.what()); return 77; } std::println("FAIL Device::Initialize: {}", error.what()); return EXIT_FAILURE; } std::println(" on {}, {}", std::string_view(Device::deviceProperties.deviceName), Device::descriptorHeapSupported ? "descriptor heap" : "descriptor sets"); if (model == "heap" && !Device::descriptorHeapSupported) { std::println("SKIP this GPU has no descriptor heap"); return 77; } Check(Device::descriptorHeapSupported == (model == "heap"), std::format("the run is on {}, as asked", model == "heap" ? "the descriptor heap" : "descriptor sets")); bool moreImagesThanFrames = false; return RenderFrames(moreImagesThanFrames); } // The number CRAFTER_GRAPHICS_DEVICE gives the GPU `gpu` is one of. std::optional GpuNumber(const std::vector>& gpus, VkPhysicalDevice gpu) { for (std::size_t i = 0; i < gpus.size(); ++i) { if (std::ranges::find(gpus[i], gpu) != gpus[i].end()) return i; } return std::nullopt; } bool PresentsTo(VkPhysicalDevice gpu, VkSurfaceKHR surface) { std::uint32_t families = 0; vkGetPhysicalDeviceQueueFamilyProperties(gpu, &families, nullptr); for (std::uint32_t family = 0; family < families; ++family) { VkBool32 supported = VK_FALSE; if (vkGetPhysicalDeviceSurfaceSupportKHR(gpu, family, surface, &supported) == VK_SUCCESS && supported) { return true; } } return false; } // The number of a GPU, other than `own`, whose swapchain for `surface` would // have more images than numFrames in some mode. Whether it has // swapchain_maintenance1 isn't known without a device, so its counts are // taken as without: at most an overestimate, and that GPU's MAILBOX needs the // most either way. std::optional GpuWithMoreImages(const std::vector>& gpus, std::size_t own, VkSurfaceKHR surface) { for (std::size_t i = 0; i < gpus.size(); ++i) { if (i == own || !PresentsTo(gpus[i].front(), surface)) continue; if (MostImagesAsked(gpus[i].front(), surface, /*modePassedAlong*/ false) > Window::numFrames) { return i; } } return std::nullopt; } int Drive() { CheckMinImageCount(); Device::Initialize(); const bool ownHeap = Device::descriptorHeapSupported; std::println(" on {}, {}, swapchain_maintenance1 {}", std::string_view(Device::deviceProperties.deviceName), ownHeap ? "descriptor heap" : "descriptor sets", Device::swapchainMaintenance1Supported ? "on" : "off"); bool moreImagesThanFrames = false; RenderFrames(moreImagesThanFrames); // Where frames and images pair up differently, on each binding model: // this process covered its own GPU's model if that GPU makes more images // than frames; the children cover the rest. const std::vector> gpus = Device::DistinctGpus(Device::instance); const std::optional own = GpuNumber(gpus, Device::physDevice); std::optional target = moreImagesThanFrames ? own : std::nullopt; if (!target && own) { Window probe(160, 120, "SwapchainImageCount probe"); target = GpuWithMoreImages(gpus, *own, probe.vulkanSurface); } if (!target) { std::println("NOTE no GPU here makes a swapchain with more than {} images, " "so frames and images were never paired differently", Window::numFrames); return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; } const fs::path exe = fs::read_symlink("/proc/self/exe"); const auto child = [&](std::string_view model) { // `env -u` so a CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP inherited from // the caller's environment can't turn the heap run into a sets run. // LP_NUM_THREADS keeps llvmpipe, the usual target, off most of the // machine's cores while the other tests run, timing ones among them. // env reads its options before the first assignment. const int exit = RunCommand(std::format("env {} LP_NUM_THREADS=4 CRAFTER_GRAPHICS_DEVICE={} '{}' --frames {}", model == "heap" ? "-u CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP" : "CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP=1", *target, exe.string(), model)); if (exit == 77) { std::println("NOTE GPU {} skipped the run on {} (above)", *target, model); } else { Check(exit == 0, std::format("GPU {} draws the frames right on {} (exit {})", *target, model, exit)); } }; const bool coveredHere = moreImagesThanFrames; if (!(coveredHere && ownHeap)) child("heap"); if (!(coveredHere && !ownHeap)) child("sets"); return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; } } // namespace int main(int argc, char** argv) { // It would override every present mode under test. unsetenv("CRAFTER_PRESENT_IMMEDIATE"); const int result = argc == 3 && std::string_view(argv[1]) == "--frames" ? FramesOnly(argv[2]) : Drive(); std::println("{}", result == EXIT_SUCCESS ? "all checks passed" : result == 77 ? "skipped" : "check(s) failed"); return result; }