137 lines
5.6 KiB
C++
137 lines
5.6 KiB
C++
|
|
//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 <cstdio>
|
||
|
|
#include <cstdlib>
|
||
|
|
#include <sys/eventfd.h>
|
||
|
|
#include <unistd.h>
|
||
|
|
#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<VkPresentModeKHR> 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<bool> 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<bool> 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<FrameTime> 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;
|
||
|
|
}
|