//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // A window can update on its own clock (Window::updateInterval) instead of // the compositor's frame callbacks, which stop while the window is hidden or // minimized, so an app whose output goes somewhere other than the screen // keeps rendering. The event loop also wakes for file descriptors the app // registers (Window::AddPollFd), such as an eventfd another thread signals, // and SetPresentMode picks MAILBOX so presenting never waits on the display. // // The test presents with MAILBOX, updates every 20 ms, and has a thread // signal an eventfd every 50 ms. Half a second in, it minimizes its own // window, and then counts the updates and wakes for a second. Before, the // Wayland loop only updated from frame callbacks, which KWin stops for a // minimized window, and blocked in wl_display_dispatch, so it never saw // another descriptor. // // Needs a live Wayland compositor that honours set_minimized, and a Vulkan // device. The validation check only means something with // CRAFTER_GRAPHICS_VALIDATION set. #include #include #include #include #include "vulkan/vulkan.h" #include "../../lib/xdg-shell-client-protocol.h" import Crafter.Graphics; import Crafter.Event; import std; using namespace Crafter; using namespace std::chrono_literals; namespace { int failures = 0; void Check(bool ok, std::string_view what) { std::println("{} {}", ok ? "PASS" : "FAIL", what); if (!ok) ++failures; } } // namespace int main() { Device::Initialize(); Device::validationErrorCount = 0; Window window(320, 240, "WindowUpdateClock test"); window.SetPresentMode(PresentMode::Mailbox); std::uint32_t modeCount = 0; Device::CheckVkResult(vkGetPhysicalDeviceSurfacePresentModesKHR(Device::physDevice, window.vulkanSurface, &modeCount, nullptr)); std::vector modes(modeCount); Device::CheckVkResult(vkGetPhysicalDeviceSurfacePresentModesKHR(Device::physDevice, window.vulkanSurface, &modeCount, modes.data())); const bool mailboxOffered = std::ranges::find(modes, VK_PRESENT_MODE_MAILBOX_KHR) != modes.end(); Check(window.swapchainPresentMode == (mailboxOffered ? VK_PRESENT_MODE_MAILBOX_KHR : VK_PRESENT_MODE_FIFO_KHR), std::format("SetPresentMode(Mailbox) gives MAILBOX where the surface offers it, else FIFO ({})", mailboxOffered ? "offered" : "not offered")); window.StartInit(); window.FinishInit(); // A thread signals an eventfd every 50 ms; the loop must wake for it. const int wake = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); std::atomic stopSignalling = false; std::thread signaller([&] { while (!stopSignalling) { const std::uint64_t one = 1; (void)write(wake, &one, sizeof(one)); std::this_thread::sleep_for(50ms); } }); std::uint32_t wakes = 0; window.AddPollFd(wake, [&] { std::uint64_t count = 0; (void)read(wake, &count, sizeof(count)); ++wakes; }); // If an update ever blocks (a present waiting on a hidden surface), fail // instead of hanging until the runner's timeout. std::atomic finished = false; std::thread watchdog([&] { const auto deadline = std::chrono::steady_clock::now() + 15s; while (!finished) { if (std::chrono::steady_clock::now() > deadline) { std::println("FAIL the event loop stopped making progress"); // _Exit skips the flush that would print the line. std::fflush(stdout); std::_Exit(EXIT_FAILURE); } std::this_thread::sleep_for(50ms); } }); const auto start = std::chrono::steady_clock::now(); bool minimized = false; std::uint32_t updatesVisible = 0; std::uint32_t updatesMinimized = 0; std::uint32_t wakesAtMinimize = 0; EventListener onUpdate(&window.onUpdate, [&](FrameTime) { const auto elapsed = std::chrono::steady_clock::now() - start; if (!minimized && elapsed >= 500ms) { xdg_toplevel_set_minimized(window.xdgToplevel); minimized = true; wakesAtMinimize = wakes; } else if (minimized && elapsed >= 1500ms) { window.open = false; } (minimized ? updatesMinimized : updatesVisible)++; }); window.updateInterval = 20ms; window.StartUpdate(); window.StartSync(); finished = true; watchdog.join(); stopSignalling = true; signaller.join(); window.RemovePollFd(wake); close(wake); Device::CheckVkResult(vkQueueWaitIdle(Device::queue)); // 20 ms apart: 25 updates in the visible half second, 50 in the minimized // second. Leave room for a loaded machine, not for a stopped clock. std::println(" {} updates visible, {} minimized, {} wakes ({} while minimized)", updatesVisible, updatesMinimized, wakes, wakes - wakesAtMinimize); Check(updatesVisible >= 18 && updatesVisible <= 27, "the clock updates every 20 ms while the window is visible"); Check(updatesMinimized >= 40 && updatesMinimized <= 52, "and keeps updating every 20 ms while it is minimized"); Check(wakes - wakesAtMinimize >= 14, "the eventfd wakes the loop every 50 ms, minimized too"); if (std::getenv("CRAFTER_GRAPHICS_VALIDATION") != nullptr) { Check(Device::validationErrorCount == 0, std::format("no Vulkan validation errors ({} seen)", Device::validationErrorCount)); } std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed"); return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; }