keyboard_key set lastFireTime to the press and TickKeyRepeats caught up on every period since it, so the first tick past the compositor's delay fired floor(delay / period) repeats at once: with rate 25 and delay 600 a held Backspace deleted sixteen characters at the delay mark and a held letter put fifteen copies in the field, from a hardware keyboard and from an on-screen keyboard that sends Backspace as key events. A rate above 1000 made the period zero and the loop endless. The first repeat now lands at the delay, as wl_keyboard.repeat_info promises (one after `delay`, then `rate` a second): the first tick past the delay sets lastFireTime to pressTime + delay - period. The period is at least a millisecond, and the catch-up after a stalled frame is capped (kKeyRepeatCatchUp, three) with the rest of the stall dropped, so a held key is never a burst of edits. TickKeyRepeats takes the clock as a parameter for the tests; the frame path's call is unchanged. tests/KeyRepeat drives TickKeyRepeats with an injected clock on a default-constructed Window: nothing before the delay, one at it, one per period after, a capped catch-up, a key without text, a rate above 1000, a released key, rate 0.
942 lines
56 KiB
C++
942 lines
56 KiB
C++
//SPDX-License-Identifier: LGPL-3.0-only
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
import std;
|
|
import Crafter.Build;
|
|
namespace fs = std::filesystem;
|
|
using namespace Crafter;
|
|
|
|
// The whole of `path`, or nullopt when it can't be read.
|
|
static std::optional<std::string> ReadFile(const fs::path& path) {
|
|
std::ifstream file(path, std::ios::binary);
|
|
if (!file) return std::nullopt;
|
|
return std::string(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
|
|
}
|
|
|
|
// Makes `lib`, the x86-64 import library for the DLL whose exports `def`
|
|
// lists, with the llvm-dlltool beside the toolchain's llvm-ar. A stamp beside
|
|
// `lib` holds the command and the .def it was made from, and while both match
|
|
// it is not made again: a fetch or a copy can leave a .def whose mtime says
|
|
// nothing of what changed in it. Empty on success, otherwise what went wrong.
|
|
static std::string MakeImportLibrary(const fs::path& def, const fs::path& lib) {
|
|
const std::optional<std::string> exports = ReadFile(def);
|
|
if (!exports) return std::format("{} could not be read", def.string());
|
|
// llvm-ar is a bare name on PATH, or <prefix>/bin/llvm-ar.
|
|
const fs::path dlltool = GetToolchain().llvmAr.parent_path() / "llvm-dlltool";
|
|
const std::string command = std::format("{} -m i386:x86-64 -d {} -l {}",
|
|
ShellQuote(dlltool.string()), ShellQuote(def.string()), ShellQuote(lib.string()));
|
|
const std::string stamp = command + '\n' + *exports;
|
|
const fs::path stampFile = fs::path(lib) += ".stamp";
|
|
std::error_code ec;
|
|
if (fs::exists(lib, ec) && ReadFile(stampFile) == stamp) return {};
|
|
fs::create_directories(lib.parent_path(), ec);
|
|
// llvm-dlltool writes the archive beside `lib` and renames it into place,
|
|
// so two loads of this project at once leave one whole library, and one
|
|
// that fails leaves the previous library as it was.
|
|
try {
|
|
const auto result = RunCommandWithTimeout(command, std::chrono::seconds(60));
|
|
if (result.timedOut) return std::format("{} took more than 60 s", dlltool.string());
|
|
if (result.crashed) return std::format("{} died of signal {}", dlltool.string(), result.signal);
|
|
if (result.exitCode != 0) return std::format("{} exited with {}: {}", dlltool.string(), result.exitCode, result.output);
|
|
} catch (const std::exception& error) {
|
|
return std::format("{} could not run: {}", dlltool.string(), error.what());
|
|
}
|
|
// After the library, so a run cut short leaves no stamp to trust.
|
|
std::ofstream(stampFile, std::ios::binary) << stamp;
|
|
return {};
|
|
}
|
|
|
|
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
|
std::vector<std::string> depArgs(args.begin(), args.end());
|
|
|
|
// Crafter.Event/Math/Asset come from forgejo, or with --local from the
|
|
// sibling checkouts next to this one (../Crafter.Event, ...), the way
|
|
// Crafter.Asset resolves Crafter.Math. --local stays in depArgs so
|
|
// Crafter.Asset picks its local Crafter.Math too: a consumer building
|
|
// against local checkouts has to see the same sibling projects
|
|
// everywhere, because Crafter.Build tells projects apart by path and
|
|
// arguments and two Crafter.Math projects would put two copies of the
|
|
// module on the compile path.
|
|
const bool useLocal = std::ranges::find(args, std::string_view("--local")) != args.end();
|
|
auto sibling = [&](std::string_view name) -> Configuration* {
|
|
if (useLocal) {
|
|
return LocalProject({
|
|
.projectFile = std::format("../{}/project.cpp", name),
|
|
.args = depArgs,
|
|
});
|
|
}
|
|
return GitProject({
|
|
.source = { .url = std::format("https://forgejo.catcrafts.net/Catcrafts/{}.git", name) },
|
|
.args = depArgs,
|
|
});
|
|
};
|
|
Configuration* event = sibling("Crafter.Event");
|
|
Configuration* math = sibling("Crafter.Math");
|
|
Configuration* asset = sibling("Crafter.Asset");
|
|
|
|
Configuration cfg;
|
|
cfg.path = "./";
|
|
cfg.name = "Crafter.Graphics";
|
|
cfg.outputName = "Crafter.Graphics";
|
|
cfg.type = ConfigurationType::LibraryStatic;
|
|
auto opts = ApplyStandardArgs(cfg, args);
|
|
cfg.dependencies = { event, math, asset };
|
|
|
|
// Window backend follows the target triple. V1 had separate lib-wayland /
|
|
// lib-win32 configurations; V2 picks the right one automatically based on
|
|
// where the build is going. Cross-compile (`--target=...`) flips the
|
|
// backend along with everything else. The DOM backend is reached by any
|
|
// wasm32-* target and produces a Vulkan-free build whose Window is wired
|
|
// to a custom JS env (see additional/dom-env.js).
|
|
bool dom = cfg.target.find("wasm") != std::string::npos;
|
|
bool windows = !dom
|
|
&& (cfg.target.find("windows") != std::string::npos
|
|
|| cfg.target.find("mingw") != std::string::npos);
|
|
if (dom) {
|
|
cfg.defines.push_back({"CRAFTER_GRAPHICS_WINDOW_DOM", ""});
|
|
// No native window libs, no Vulkan loader, no Wayland/X11. The JS
|
|
// bridge satisfies every dynamic symbol via wasm imports. Crafter.Build
|
|
// strips -march/-mtune from the clang command line for any wasm32-*
|
|
// triple, so cfg.march/mtune can stay at their defaults — keeping them
|
|
// matches the VariantId of dependency PCMs.
|
|
//
|
|
// WasmAlloc / WasmFree live in Crafter.Graphics-Dom.cpp and back
|
|
// dom-env.js's __writeUtf8 path (every keyboard / text-input event
|
|
// routes through them). The TU defines no symbols main.cpp would
|
|
// reference, so wasm-ld dead-strips it from libCrafter.Graphics.a
|
|
// for examples that don't touch the `Dom::HtmlElement*` API (like
|
|
// Sponza). `--export=` both forces the export AND pulls the
|
|
// defining .o in — solving both halves of the dead-strip problem.
|
|
cfg.linkFlags.push_back("-Wl,--export=WasmAlloc");
|
|
cfg.linkFlags.push_back("-Wl,--export=WasmFree");
|
|
} else if (windows) {
|
|
cfg.defines.push_back({"CRAFTER_GRAPHICS_WINDOW_WIN32", ""});
|
|
cfg.linkFlags.push_back("-lkernel32");
|
|
cfg.linkFlags.push_back("-luser32");
|
|
cfg.linkFlags.push_back("-lgdi32");
|
|
// Windows.Gaming.Input (WGI) needs the WinRT activation runtime for
|
|
// HSTRING and RoGetActivationFactory. Its gamepad code is C (see
|
|
// Crafter.Graphics-GamepadWin32.h).
|
|
cfg.linkFlags.push_back("-lruntimeobject");
|
|
// The IIDs its event handlers compare against, IID_IUnknown and
|
|
// IID_IAgileObject. StartSync's Gamepad::Tick pulls the gamepad code
|
|
// into every executable, which without libuuid did not link (#184).
|
|
cfg.linkFlags.push_back("-luuid");
|
|
cfg.cFiles.push_back("implementations/Crafter.Graphics-GamepadWin32");
|
|
} else {
|
|
cfg.defines.push_back({"CRAFTER_GRAPHICS_WINDOW_WAYLAND", ""});
|
|
cfg.linkFlags.push_back("-lwayland-client");
|
|
cfg.linkFlags.push_back("-lxkbcommon");
|
|
// Gamepad: libudev for hot-plug + device enumeration; libevdev
|
|
// for event parsing + axis calibration.
|
|
cfg.linkFlags.push_back("-ludev");
|
|
|
|
// libevdev is vendored in lib/libevdev rather than linked from the
|
|
// system: the Flatpak SDK crafter-build runs in ships no libevdev
|
|
// headers, and a vendored copy cross-builds like everything else. It
|
|
// is its own static library so it compiles once, not again in every
|
|
// test configuration (they copy cfg.cFiles). One per call, since each
|
|
// distinct argument list loads this project separately; it lives for
|
|
// the whole build like a GitProject's configuration. Its paths are
|
|
// absolute because Crafter.Build only resolves the returned
|
|
// configuration's paths against this project's root, and this
|
|
// project.cpp runs with that root as cwd.
|
|
static std::deque<Configuration> libevdevConfigs;
|
|
Configuration& libevdev = libevdevConfigs.emplace_back();
|
|
libevdev.path = fs::absolute(cfg.path);
|
|
libevdev.name = "Crafter.Graphics-libevdev";
|
|
libevdev.outputName = "Crafter.Graphics-libevdev";
|
|
libevdev.type = ConfigurationType::LibraryStatic;
|
|
libevdev.target = cfg.target;
|
|
libevdev.march = cfg.march;
|
|
libevdev.mtune = cfg.mtune;
|
|
libevdev.debug = cfg.debug;
|
|
libevdev.sysroot = cfg.sysroot;
|
|
libevdev.projectArgs = cfg.projectArgs;
|
|
libevdev.cFiles = {
|
|
fs::absolute("lib/libevdev/libevdev/libevdev"),
|
|
fs::absolute("lib/libevdev/libevdev/libevdev-names"),
|
|
};
|
|
// Its generated event-names.h names every input code by macro, so it
|
|
// compiles against the kernel headers that table was generated from
|
|
// rather than the target's.
|
|
libevdev.compileFlags.push_back(std::format("-I{}", fs::absolute("lib/libevdev/kernel-headers").string()));
|
|
cfg.dependencies.push_back(&libevdev);
|
|
// For Gamepad's #include <libevdev/libevdev.h>.
|
|
cfg.compileFlags.push_back(std::format("-I{}", fs::absolute("lib/libevdev").string()));
|
|
|
|
cfg.cFiles.push_back("lib/xdg-shell-protocol");
|
|
cfg.cFiles.push_back("lib/wayland-xdg-decoration-unstable-v1-client-protocol");
|
|
cfg.cFiles.push_back("lib/fractional-scale-v1");
|
|
cfg.cFiles.push_back("lib/viewporter");
|
|
cfg.cFiles.push_back("lib/text-input-unstable-v3-protocol");
|
|
}
|
|
|
|
// Vulkan is the only renderer on native targets. Software fallback is
|
|
// provided externally via the Vulkan loader (e.g. llvmpipe / lavapipe) —
|
|
// no separate code path. The DOM backend doesn't render in V1 (the
|
|
// UIRenderer and every Vulkan-typed module are excluded below); the
|
|
// WebGPU follow-up will gain its own headers/loader rather than reuse
|
|
// the Vulkan ones.
|
|
if (!dom) {
|
|
ExternalDependency& vkHeaders = cfg.externalDependencies.emplace_back();
|
|
vkHeaders.name = "Vulkan-Headers";
|
|
vkHeaders.source.url = "https://github.com/KhronosGroup/Vulkan-Headers.git";
|
|
vkHeaders.builder = ExternalBuilder::None;
|
|
vkHeaders.includeDirs = { "include" };
|
|
ExternalDependency& vkUtility = cfg.externalDependencies.emplace_back();
|
|
vkUtility.name = "Vulkan-Utility-Libraries";
|
|
vkUtility.source.url = "https://github.com/KhronosGroup/Vulkan-Utility-Libraries.git";
|
|
vkUtility.builder = ExternalBuilder::None;
|
|
vkUtility.includeDirs = { "include" };
|
|
if (!windows) {
|
|
cfg.linkFlags.push_back("-lvulkan");
|
|
} else {
|
|
// The MinGW toolchains ship no import library for the Vulkan
|
|
// loader, so one is made from the loader's own list of what
|
|
// vulkan-1.dll exports: loader/vulkan-1.def, from Vulkan-Loader
|
|
// unpinned like the headers above, so the API the library is
|
|
// compiled against and the exports it links against move
|
|
// together. No new call outgrows that list, and a call the loader
|
|
// does not export fails at link time instead of when the exe
|
|
// starts. The library goes into the link by its path, absolute
|
|
// because this project.cpp is also loaded from a consumer's
|
|
// directory, rather than as -L, so it shadows nothing: a
|
|
// consumer's own -lvulkan-1, a Vulkan SDK's, still serves the
|
|
// functions this list lacks. Without the fetch or llvm-dlltool,
|
|
// the library a previous load made does, or else a Vulkan SDK's.
|
|
const fs::path importLibrary = fs::absolute(std::format("build/vulkan-1-{}/libvulkan-1.a", cfg.target));
|
|
std::string error;
|
|
try {
|
|
const fs::path loader = GitFetch({ .url = "https://github.com/KhronosGroup/Vulkan-Loader.git" });
|
|
error = MakeImportLibrary(loader / "loader" / "vulkan-1.def", importLibrary);
|
|
} catch (const std::exception& fetchError) {
|
|
error = fetchError.what();
|
|
}
|
|
if (!error.empty()) {
|
|
Progress::Diagnostic(std::format("warning: Crafter.Graphics: the vulkan-1 import library was not made: {}", error));
|
|
}
|
|
std::error_code ec;
|
|
cfg.linkFlags.push_back(fs::exists(importLibrary, ec) ? importLibrary.string() : std::string("-lvulkan-1"));
|
|
}
|
|
}
|
|
|
|
if (opts.Has("--timing")) cfg.defines.push_back({"CRAFTER_TIMING", ""});
|
|
|
|
// One master interface list. Every partition exists on every target
|
|
// — Crafter.Build's dependency scanner doesn't respect `#ifdef` on
|
|
// `import :X` statements, so the partition file must be present even
|
|
// when its body is gated out. Vulkan-typed partitions stub to empty
|
|
// modules under CRAFTER_GRAPHICS_WINDOW_DOM; the Dom/DomEvents/Router
|
|
// partitions stub to empty modules in the opposite direction.
|
|
std::array<fs::path, 42> ifaces = {
|
|
"interfaces/Crafter.Graphics",
|
|
"interfaces/Crafter.Graphics-Animation",
|
|
"interfaces/Crafter.Graphics-Clipboard",
|
|
"interfaces/Crafter.Graphics-ComputeShader",
|
|
"interfaces/Crafter.Graphics-Decompress",
|
|
"interfaces/Crafter.Graphics-DescriptorHeapVulkan",
|
|
"interfaces/Crafter.Graphics-DescriptorHeapWebGPU",
|
|
"interfaces/Crafter.Graphics-Device",
|
|
"interfaces/Crafter.Graphics-Dom",
|
|
"interfaces/Crafter.Graphics-DomEvents",
|
|
"interfaces/Crafter.Graphics-Font",
|
|
"interfaces/Crafter.Graphics-FontAtlas",
|
|
"interfaces/Crafter.Graphics-ForwardDeclarations",
|
|
"interfaces/Crafter.Graphics-Gamepad",
|
|
"interfaces/Crafter.Graphics-GraphicsTypes",
|
|
"interfaces/Crafter.Graphics-Image2D",
|
|
"interfaces/Crafter.Graphics-ImageVulkan",
|
|
"interfaces/Crafter.Graphics-Input",
|
|
"interfaces/Crafter.Graphics-InputField",
|
|
"interfaces/Crafter.Graphics-Keys",
|
|
"interfaces/Crafter.Graphics-Mesh",
|
|
"interfaces/Crafter.Graphics-PipelineRTVulkan",
|
|
"interfaces/Crafter.Graphics-PipelineRTWebGPU",
|
|
"interfaces/Crafter.Graphics-PlainComputeShader",
|
|
"interfaces/Crafter.Graphics-RenderingElement3D",
|
|
"interfaces/Crafter.Graphics-RenderPass",
|
|
"interfaces/Crafter.Graphics-Router",
|
|
"interfaces/Crafter.Graphics-RT",
|
|
"interfaces/Crafter.Graphics-RTPass",
|
|
"interfaces/Crafter.Graphics-SamplerVulkan",
|
|
"interfaces/Crafter.Graphics-ShaderBindingTableVulkan",
|
|
"interfaces/Crafter.Graphics-ShaderBindingTableWebGPU",
|
|
"interfaces/Crafter.Graphics-ShaderVulkan",
|
|
"interfaces/Crafter.Graphics-Types",
|
|
"interfaces/Crafter.Graphics-UI",
|
|
"interfaces/Crafter.Graphics-UIComponents",
|
|
"interfaces/Crafter.Graphics-VulkanBuffer",
|
|
"interfaces/Crafter.Graphics-VulkanTransition",
|
|
"interfaces/Crafter.Graphics-WebGPU",
|
|
"interfaces/Crafter.Graphics-WebGPUBuffer",
|
|
"interfaces/Crafter.Graphics-WebGPUComputeShader",
|
|
"interfaces/Crafter.Graphics-Window",
|
|
};
|
|
|
|
if (dom) {
|
|
// DOM impl set. UI-Shared.cpp is backend-agnostic; UI-WebGPU.cpp
|
|
// is the DOM-only implementation of UIRenderer's GPU-touching
|
|
// methods. Font / FontAtlas / UIComponents / InputField are now
|
|
// portable.
|
|
std::array<fs::path, 17> domImpls = {
|
|
"implementations/Crafter.Graphics-Clipboard",
|
|
"implementations/Crafter.Graphics-Dom",
|
|
"implementations/Crafter.Graphics-Font",
|
|
"implementations/Crafter.Graphics-FontAtlas",
|
|
"implementations/Crafter.Graphics-Gamepad",
|
|
"implementations/Crafter.Graphics-Input",
|
|
"implementations/Crafter.Graphics-InputField",
|
|
"implementations/Crafter.Graphics-Mesh-WebGPU",
|
|
"implementations/Crafter.Graphics-PipelineRTWebGPU",
|
|
"implementations/Crafter.Graphics-RenderingElement3D-WebGPU",
|
|
"implementations/Crafter.Graphics-Router",
|
|
"implementations/Crafter.Graphics-ShaderBindingTableWebGPU",
|
|
"implementations/Crafter.Graphics-UI-Shared",
|
|
"implementations/Crafter.Graphics-UI-WebGPU",
|
|
"implementations/Crafter.Graphics-UIComponents",
|
|
"implementations/Crafter.Graphics-WebGPUComputeShader",
|
|
"implementations/Crafter.Graphics-Window",
|
|
};
|
|
cfg.GetInterfacesAndImplementations(ifaces, domImpls);
|
|
// JS glue shipped alongside the .wasm so the loader has the
|
|
// env-import surface the Window/Dom bindings expect.
|
|
cfg.files.emplace_back(fs::path("additional/dom-env.js"));
|
|
|
|
// dom-webgpu.js is opt-out via --no-webgpu, for apps that only use
|
|
// the Dom/Router partitions and never touch Device/RTPass/UIRenderer.
|
|
//
|
|
// Three reasons a DOM-only app wants it gone, in increasing order of
|
|
// severity:
|
|
// 1. It is ~182 KB (47 KB gzip) of dead weight.
|
|
// 2. Its init is an async IIFE that runtime.js awaits before
|
|
// _start(), and that init does `await navigator.gpu
|
|
// .requestAdapter()` — so every page load blocks wasm startup on
|
|
// a GPU adapter request, including on machines with no GPU.
|
|
// 3. Worst: when navigator.gpu is absent it REPLACES
|
|
// document.body.innerHTML with an error message and throws, so
|
|
// _start() never runs. For a server-rendered page that means the
|
|
// real content is destroyed and replaced with a WebGPU warning on
|
|
// any browser without WebGPU support.
|
|
//
|
|
// Safety: with --gc-sections an app that never calls into the WebGPU
|
|
// partitions emits no wgpu* env imports, so omitting the bridge links
|
|
// and instantiates cleanly. An app that DOES use WebGPU and passes
|
|
// --no-webgpu anyway fails at instantiate with a missing-import error
|
|
// naming the specific wgpu* symbol.
|
|
if (!opts.Has("--no-webgpu")) {
|
|
cfg.files.emplace_back(fs::path("additional/dom-webgpu.js"));
|
|
}
|
|
} else {
|
|
std::array<fs::path, 15> impls = {
|
|
"implementations/Crafter.Graphics-Clipboard",
|
|
"implementations/Crafter.Graphics-ComputeShader",
|
|
"implementations/Crafter.Graphics-Device",
|
|
"implementations/Crafter.Graphics-Device-Features",
|
|
"implementations/Crafter.Graphics-Font",
|
|
"implementations/Crafter.Graphics-FontAtlas",
|
|
"implementations/Crafter.Graphics-Gamepad",
|
|
"implementations/Crafter.Graphics-Input",
|
|
"implementations/Crafter.Graphics-InputField",
|
|
"implementations/Crafter.Graphics-Mesh",
|
|
"implementations/Crafter.Graphics-RenderingElement3D",
|
|
"implementations/Crafter.Graphics-UI",
|
|
"implementations/Crafter.Graphics-UI-Shared",
|
|
"implementations/Crafter.Graphics-UIComponents",
|
|
"implementations/Crafter.Graphics-Window",
|
|
};
|
|
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
|
|
|
// Every UI kernel is built twice from one source: <name>.comp.spv
|
|
// indexes VK_EXT_descriptor_heap, <name>.comp.sets.spv reads the same
|
|
// slots from a bindless descriptor set for drivers without the heap
|
|
// extension (Mesa Turnip). ui-shared.glsl switches on the define and
|
|
// ComputeShader::Load picks the file at runtime.
|
|
for (const char* shader : {"shaders/ui-quads.comp.glsl", "shaders/ui-circles.comp.glsl", "shaders/ui-images.comp.glsl", "shaders/ui-text.comp.glsl", "shaders/ui-fused.comp.glsl"}) {
|
|
cfg.shaders.emplace_back(fs::path(shader), std::string("main"), ShaderType::Compute);
|
|
Shader& sets = cfg.shaders.emplace_back(fs::path(shader), std::string("main"), ShaderType::Compute);
|
|
sets.defines = {"CRAFTER_GRAPHICS_DESCRIPTOR_SETS"};
|
|
sets.variant = "sets";
|
|
}
|
|
cfg.buildFiles.emplace_back(fs::path("shaders/ui-shared.glsl"));
|
|
|
|
// A test executable built the way the library is (its target, flags,
|
|
// dependencies and C files) from the library's sources plus
|
|
// tests/<name>/main.cpp. Configuration isn't copyable (it owns the
|
|
// parsed module list), so the shared settings are copied field by
|
|
// field, here once for every test. Callers add what else a test needs.
|
|
auto LibraryTest = [&](std::string_view name) {
|
|
Test test;
|
|
Configuration& c = test.config;
|
|
c.path = cfg.path;
|
|
c.name = name;
|
|
c.outputName = name;
|
|
c.type = ConfigurationType::Executable;
|
|
c.target = cfg.target;
|
|
c.march = cfg.march;
|
|
c.mtune = cfg.mtune;
|
|
c.debug = cfg.debug;
|
|
c.sysroot = cfg.sysroot;
|
|
c.projectArgs = cfg.projectArgs;
|
|
c.dependencies = cfg.dependencies;
|
|
c.externalDependencies = cfg.externalDependencies;
|
|
c.compileFlags = cfg.compileFlags;
|
|
c.linkFlags = cfg.linkFlags;
|
|
c.defines = cfg.defines;
|
|
c.cFiles = cfg.cFiles;
|
|
std::vector<fs::path> sources(impls.begin(), impls.end());
|
|
sources.emplace_back(std::format("tests/{}/main", name));
|
|
c.GetInterfacesAndImplementations(ifaces, sources);
|
|
return test;
|
|
};
|
|
|
|
// Regression test for issue #18: drive the NVIDIA descriptor-heap
|
|
// AS-read workaround's SPIR-V rewrite over real compiled shaders and
|
|
// check the result with spirv-val (one push-constant block, correct
|
|
// TLAS offset). glslang and spirv-val are invoked at runtime, so the
|
|
// test declares them as required tools. Remove with the rest of the
|
|
// workaround.
|
|
Test pcTest = LibraryTest("PushConstantRewrite");
|
|
pcTest.requires_ = { "tool:glslang", "tool:spirv-val" };
|
|
cfg.tests.push_back(std::move(pcTest));
|
|
|
|
// The Win32 backend's pointer, capture, close and teardown handling,
|
|
// through a real HWND: posts it the messages Windows would and checks
|
|
// what the Window made of them. The Window needs Vulkan for its
|
|
// swapchain. Built on Linux it runs under Wine, which may be absent.
|
|
if (windows) {
|
|
Test win32PointerTest = LibraryTest("PointerWin32");
|
|
#ifndef _WIN32
|
|
win32PointerTest.requires_ = { "tool:wine" };
|
|
#endif
|
|
cfg.tests.push_back(std::move(win32PointerTest));
|
|
|
|
// The Win32 backend's text, UTF-8 to the library and UTF-16 to
|
|
// Windows: the title from the constructor and SetTitle, read back
|
|
// from the HWND, and the characters WM_CHAR brings. Needs what
|
|
// PointerWin32 needs.
|
|
Test win32TextTest = LibraryTest("TextWin32");
|
|
#ifndef _WIN32
|
|
win32TextTest.requires_ = { "tool:wine" };
|
|
#endif
|
|
cfg.tests.push_back(std::move(win32TextTest));
|
|
|
|
// A Win32 executable that uses the library as an application does
|
|
// links: the vulkan-1 import library made above and -luuid (#184).
|
|
// The Win32 library itself is only archived, and the tests above
|
|
// are skipped without Wine, so nothing else links one there. With
|
|
// no Wine, run it as --runner=cmd:true, which builds it and runs
|
|
// nothing; no requires_, since that would skip the build too.
|
|
// Under Wine it also runs, opening a window and closing it, which
|
|
// needs a Vulkan device.
|
|
cfg.tests.push_back(LibraryTest("Win32Link"));
|
|
}
|
|
|
|
// Regression test for issue #32: the Wayland scroll-wheel chain
|
|
// (wl_pointer.axis → Window::onMouseScroll). Drives the listener
|
|
// callback directly, so it needs no compositor — but it does need
|
|
// the Wayland backend compiled in, hence Linux-only.
|
|
if (!windows) {
|
|
cfg.tests.push_back(LibraryTest("MouseScroll"));
|
|
|
|
// Key repeat as wl_keyboard.repeat_info promises it: the first
|
|
// repeat at the delay (not a burst that counts the delay as missed
|
|
// periods), one per period after, a capped catch-up after a stall,
|
|
// a period of at least a millisecond. Drives TickKeyRepeats with
|
|
// an injected clock; no compositor.
|
|
cfg.tests.push_back(LibraryTest("KeyRepeat"));
|
|
|
|
// The pointer's enter, move and leave through the helpers both
|
|
// native backends report with (Window::PointerEnter and co.).
|
|
// Drives the wl_pointer callbacks directly, as MouseScroll does.
|
|
cfg.tests.push_back(LibraryTest("PointerEnterLeave"));
|
|
|
|
// SetTitle's xdg_toplevel.set_title, read off a wl_display on a
|
|
// socketpair: the view's bytes only, and an empty view as an
|
|
// empty title, not a null string. No compositor.
|
|
cfg.tests.push_back(LibraryTest("TitleWayland"));
|
|
|
|
// Issue #40: multi-frame-in-flight frame pacing (per-frame fences
|
|
// and acquire semaphores, per-image render-finished semaphores, no
|
|
// steady-state wait-idle). Drives the
|
|
// real frame loop against a live Wayland compositor for many more
|
|
// frames than there are in-flight slots and asserts the validation
|
|
// layer stays silent — the old singleton-semaphore design only
|
|
// avoided being an active race because the wait-idle masked it, so
|
|
// a clean multi-frame run is the regression guard. Needs the
|
|
// Wayland backend + a real compositor, hence inside the !windows
|
|
// block alongside MouseScroll.
|
|
cfg.tests.push_back(LibraryTest("FrameLoopSync"));
|
|
|
|
// Issue #153: two frame-loop validation errors. (1) The acquire
|
|
// barrier hardcoded dstAccessMask = SHADER_WRITE|TRANSFER_WRITE but
|
|
// used the per-pass stage union (COMPUTE_SHADER for an all-compute
|
|
// frame) as its dst stage, so TRANSFER_WRITE was unsupported and
|
|
// VUID-02820 fired every frame — FrameLoopSync can't see it because
|
|
// it runs with no passes (the union falls back to the conservative
|
|
// writer set, which includes TRANSFER). (2) Mid-session
|
|
// StartInit/FinishInit reuse the shared draw command buffer while
|
|
// the loop's last submission of it is still in flight, re-beginning
|
|
// (00049) and re-submitting (00071) a pending buffer. Drives a real
|
|
// compute pass through the loop plus interleaved StartInit rounds
|
|
// and asserts the layer stays silent. Needs the Wayland backend + a
|
|
// real compositor, so it lives in the !windows block.
|
|
cfg.tests.push_back(LibraryTest("SetupCmdBufferReuse"));
|
|
|
|
// The UI path has two binding models: VK_EXT_descriptor_heap, and
|
|
// a bindless descriptor set for drivers without it (Mesa Turnip).
|
|
// The test renders one scene through each — the second by
|
|
// re-running itself with CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP — and
|
|
// compares the frames, so the fallback cannot drift from the heap
|
|
// path unnoticed on a machine that never takes it. Unlike the
|
|
// other tests it loads the real UI kernels, so it carries the
|
|
// library's shaders (both variants) and a font into its own bin
|
|
// dir. Needs the Wayland backend + a real compositor.
|
|
Test parityTest = LibraryTest("UIFallbackParity");
|
|
parityTest.config.shaders = cfg.shaders;
|
|
parityTest.config.buildFiles = cfg.buildFiles;
|
|
parityTest.config.files.emplace_back(fs::path("examples/HelloUI/font.ttf"));
|
|
cfg.tests.push_back(std::move(parityTest));
|
|
|
|
// Issue #55: a glyph ShapeText places may take a cell another
|
|
// glyph had, so every kernel UIRenderer dispatches uploads the font
|
|
// atlas first, a Tier 1 one through ui.Dispatch included. Shapes
|
|
// new glyphs in onBuild and checks the atlas is clean after a
|
|
// custom ui.Dispatch. Loads the UI kernels like UIFallbackParity.
|
|
// Needs the Wayland backend + a real compositor.
|
|
Test atlasFlushTest = LibraryTest("UIDispatchAtlasFlush");
|
|
atlasFlushTest.config.shaders = cfg.shaders;
|
|
atlasFlushTest.config.buildFiles = cfg.buildFiles;
|
|
atlasFlushTest.config.files.emplace_back(fs::path("examples/HelloUI/font.ttf"));
|
|
cfg.tests.push_back(std::move(atlasFlushTest));
|
|
|
|
// Text from the UI kernels ramps over one screen pixel at any
|
|
// size and aspect. The kernels decoded FontAtlas's SDF with
|
|
// kPixelDistScale where 255 / kPixelDistScale belongs: a
|
|
// quarter-pixel ramp that made text look pixelated. Draws a
|
|
// string at 14 and 64 px and stretched, a glyph turned by its UVs
|
|
// and shrunk empty texels, through DispatchText and DispatchFused,
|
|
// and compares the frame with stb_truetype's own antialiased
|
|
// rasterizer. Loads the UI kernels and a font like
|
|
// UIFallbackParity. Needs the Wayland backend + a real compositor.
|
|
Test textAATest = LibraryTest("TextAntialiasing");
|
|
textAATest.config.shaders = cfg.shaders;
|
|
textAATest.config.buildFiles = cfg.buildFiles;
|
|
textAATest.config.files.emplace_back(fs::path("examples/HelloUI/font.ttf"));
|
|
cfg.tests.push_back(std::move(textAATest));
|
|
|
|
// Issue #160: a swapchain has as many images as its present mode
|
|
// needs, and the frame loop takes any count. Every offered mode
|
|
// is kept (no fallback to FIFO), and every frame presents what it
|
|
// drew, read back a texel per frame. Where this process's GPU
|
|
// never makes more than numFrames images, the frame checks run
|
|
// again in child processes on a GPU that does (llvmpipe), on the
|
|
// heap and on descriptor sets. Loads the UI kernels like
|
|
// UIFallbackParity. Needs the Wayland backend + a real compositor.
|
|
Test imageCountTest = LibraryTest("SwapchainImageCount");
|
|
imageCountTest.config.shaders = cfg.shaders;
|
|
imageCountTest.config.buildFiles = cfg.buildFiles;
|
|
cfg.tests.push_back(std::move(imageCountTest));
|
|
|
|
// Reading the TLAS from the descriptor heap (#7/#15): renders a
|
|
// triangle through traceRayEXT and an inline ray query and checks
|
|
// the read-back pixels, through the engine's normal path (so on
|
|
// NVIDIA through the SPIR-V rewrite). `HeapASRead --no-workaround`
|
|
// is the manual probe for whether a driver still has the fault.
|
|
// Needs the Wayland backend, a real compositor and an RT device
|
|
// (skips without RT).
|
|
Test heapAsTest = LibraryTest("HeapASRead");
|
|
heapAsTest.config.shaders.emplace_back(fs::path("tests/HeapASRead/heapas-raygen.glsl"), std::string("main"), ShaderType::RayGen);
|
|
heapAsTest.config.shaders.emplace_back(fs::path("tests/HeapASRead/heapas-closesthit.glsl"), std::string("main"), ShaderType::ClosestHit);
|
|
heapAsTest.config.shaders.emplace_back(fs::path("tests/HeapASRead/heapas-miss.glsl"), std::string("main"), ShaderType::Miss);
|
|
cfg.tests.push_back(std::move(heapAsTest));
|
|
|
|
// Dynamic descriptor-heap indexing in a hit shader: a closest-hit
|
|
// picks a storage buffer and a sampled image by a runtime index
|
|
// (the same across a wave on one instance, different between
|
|
// neighbouring lanes on the other), which lost the device on
|
|
// NVIDIA 610.43.02; skips on NVIDIA drivers older than 615.78.08.
|
|
// Same requirements as HeapASRead.
|
|
Test dynIndexTest = LibraryTest("HeapDynamicIndexHit");
|
|
dynIndexTest.config.shaders.emplace_back(fs::path("tests/HeapDynamicIndexHit/dynidx-raygen.glsl"), std::string("main"), ShaderType::RayGen);
|
|
dynIndexTest.config.shaders.emplace_back(fs::path("tests/HeapDynamicIndexHit/dynidx-closesthit.glsl"), std::string("main"), ShaderType::ClosestHit);
|
|
dynIndexTest.config.shaders.emplace_back(fs::path("tests/HeapDynamicIndexHit/dynidx-miss.glsl"), std::string("main"), ShaderType::Miss);
|
|
cfg.tests.push_back(std::move(dynIndexTest));
|
|
|
|
// The swapchain is created with TRANSFER_DST where the surface
|
|
// allows it: clears the image from a pass and reads it back.
|
|
// Needs the Wayland backend and a real compositor.
|
|
cfg.tests.push_back(LibraryTest("SwapchainTransferDst"));
|
|
|
|
// A window on its own update clock keeps updating while
|
|
// minimized, and the loop wakes for a registered eventfd. Needs
|
|
// the Wayland backend and a real compositor.
|
|
cfg.tests.push_back(LibraryTest("WindowUpdateClock"));
|
|
|
|
// Issue #162: xdg-decoration, wp_viewporter and
|
|
// wp_fractional_scale_v1 are optional (GNOME has no
|
|
// xdg-decoration). Hides each from the Window in turn, and all
|
|
// three, and renders, resizes and closes a window each time.
|
|
// Needs the Wayland backend and a real compositor.
|
|
cfg.tests.push_back(LibraryTest("WindowOptionalGlobals"));
|
|
|
|
// RTPass pushes the application's push data and traces at an
|
|
// explicit size: clears the frame, traces a quadrant with a hit
|
|
// colour that changes every frame, and checks the read-back.
|
|
// Same requirements as HeapASRead.
|
|
Test rtPushTest = LibraryTest("RTPassPushData");
|
|
rtPushTest.config.shaders.emplace_back(fs::path("tests/RTPassPushData/rtpass-raygen.glsl"), std::string("main"), ShaderType::RayGen);
|
|
rtPushTest.config.shaders.emplace_back(fs::path("tests/RTPassPushData/rtpass-closesthit.glsl"), std::string("main"), ShaderType::ClosestHit);
|
|
rtPushTest.config.shaders.emplace_back(fs::path("tests/RTPassPushData/rtpass-miss.glsl"), std::string("main"), ShaderType::Miss);
|
|
cfg.tests.push_back(std::move(rtPushTest));
|
|
}
|
|
|
|
// Issue #36: BLAS build options. Drives the real hardware AS-build
|
|
// path — records Mesh::Build / Refit / BuildProcedural /
|
|
// RefitProcedural with fast-build/fast-trace + allow-update flags
|
|
// into one-time command buffers and submits them, asserting the
|
|
// requested flags land, that an allowUpdate refit keeps the AS
|
|
// handle (in-place UPDATE), and that the validation layer reports no
|
|
// errors. Needs a Vulkan RT device at runtime (same as the RT
|
|
// examples), so it shares the native build settings.
|
|
cfg.tests.push_back(LibraryTest("BLASBuildOptions"));
|
|
|
|
// Issue #64: TLAS host-input buffers (instanceBuffer / metadataBuffer)
|
|
// grow on a high-water mark instead of reallocating to the exact count
|
|
// every topology change. Drives the real hardware AS-build path — a
|
|
// cube BLAS plus RenderingElement3D::BuildTLAS at a sequence of
|
|
// instance counts — and asserts that a shrink (and any growth within
|
|
// the high-water capacity) reuses the existing allocation while a
|
|
// growth past it reallocates, with the validation layer reporting no
|
|
// errors when an oversized instance buffer is fed to the build. The
|
|
// acceleration structure follows the same high-water mark: a rebuild
|
|
// that fits its storage builds into the same handle. Needs a Vulkan
|
|
// RT device at runtime, so it shares the native build settings.
|
|
cfg.tests.push_back(LibraryTest("TLASHighWaterMark"));
|
|
|
|
// The barriers BuildTLAS records around the build: the one before it
|
|
// orders earlier builds, traces and compute ray queries before the
|
|
// build overwrites the TLAS, and the one after it carries the
|
|
// acceleration-structure memory dependency to the shaders that read
|
|
// the TLAS, not just execution order. BuildTlasWriteBarrier and
|
|
// BuildTlasReadBarrier are pure CPU logic, so no GPU device is needed
|
|
// at runtime.
|
|
cfg.tests.push_back(LibraryTest("TLASBuildBarrier"));
|
|
|
|
// A TlasWithBuffer queues its acceleration structure for deferred
|
|
// deletion, ahead of its storage, when it is destroyed or assigned
|
|
// over, and moving one hands the acceleration structure over.
|
|
// Headless; needs a Vulkan RT device at runtime, like MeshLifetime.
|
|
cfg.tests.push_back(LibraryTest("TLASLifetime"));
|
|
|
|
// BuildTLAS on an empty scene (no elements registered yet) builds a
|
|
// valid empty TLAS instead of creating zero-size buffers, and adding
|
|
// and removing instances afterwards still rebuilds it. Needs a Vulkan
|
|
// RT device at runtime, like TLASHighWaterMark.
|
|
cfg.tests.push_back(LibraryTest("TLASEmptyBuild"));
|
|
|
|
// Issue #118: the per-frame TLAS instance+metadata host rebuild copies
|
|
// (and flushes) only the slots whose element's host-authored data
|
|
// changed, tracked via RenderingElement3D::hostDataVersion against the
|
|
// per-frame uploadedVersion record; untracked elements (version 0) keep
|
|
// the always-copy behaviour. Drives the real AS-build path — a cube
|
|
// BLAS plus BuildTLAS at a sequence of marks/mutations — and reads back
|
|
// the host-mapped buffers to assert that clean slots are skipped, dirty
|
|
// slots re-uploaded, and relocation on the refit path re-uploads exactly
|
|
// the moved slots, with the validation layer reporting no errors over
|
|
// the ranged FlushDevice the dirty span feeds. Needs a Vulkan RT device
|
|
// at runtime, so it shares the native build settings.
|
|
cfg.tests.push_back(LibraryTest("TLASInstanceDirtyTracking"));
|
|
|
|
// Issue #51: FontAtlas only re-uploads the dirty sub-rect now,
|
|
// tracked via FontAtlas::DirtyRect. The accumulation/clamp math is
|
|
// pure CPU, so this test drives it directly — no GPU device needed
|
|
// at runtime (the real copy params are covered by HelloUI rendering
|
|
// text on both backends).
|
|
cfg.tests.push_back(LibraryTest("FontAtlasDirtyRect"));
|
|
|
|
// Issue #55: FontAtlas::Packer, the atlas's shelves and its evictions
|
|
// once full: the fill order, least recently used first, never a glyph
|
|
// a frame in flight may sample, nothing without `evict`, a glyph taller
|
|
// than every shelf spanning several (and the free rows below), Touch
|
|
// past a stale handle, dirty spans over everything it hands out or
|
|
// clears, and the invariants over a random workload. Pure CPU, with
|
|
// made-up boxes: no GPU device at runtime.
|
|
cfg.tests.push_back(LibraryTest("FontAtlasPacker"));
|
|
|
|
// Issue #52: shaped-run cache for UIRenderer::ShapeText. Shapes a
|
|
// string against a real CPU-side FontAtlas and asserts that a cache
|
|
// hit is byte-equivalent to the uncached path, that hits rasterise
|
|
// nothing into the atlas, and that translate / alignment / truncation /
|
|
// InvalidateFont all behave. Needs a headless Vulkan device (the
|
|
// atlas image is a real GPU image) but no swapchain — same native
|
|
// build settings as the others. The font file is copied next to the
|
|
// binary; the test also probes the project-root path.
|
|
Test textTest = LibraryTest("ShapeTextCache");
|
|
textTest.config.files = { fs::path("tests/ShapeTextCache/font.ttf") };
|
|
cfg.tests.push_back(std::move(textTest));
|
|
|
|
// Issue #55: a glyph without free room keeps its advance, is not
|
|
// rasterised until it gets a cell, and gets one when the next frame
|
|
// begins, from the glyphs drawn least recently but never one a frame
|
|
// in flight may sample. Fills the atlas through ShapeText frame by
|
|
// frame on Device::frameCounter and checks the layout, the frames in
|
|
// flight, the order of the evictions, the recovery, the cleared
|
|
// texels and InvalidateFont. Needs a headless Vulkan device (the
|
|
// atlas is a real GPU image) and the ShapeTextCache font, which covers
|
|
// enough codepoints to fill the atlas.
|
|
Test evictionTest = LibraryTest("FontAtlasEviction");
|
|
evictionTest.config.files = { fs::path("tests/ShapeTextCache/font.ttf") };
|
|
cfg.tests.push_back(std::move(evictionTest));
|
|
|
|
// Issue #59: Device::GetMemoryType gained a `preferred` mask and a
|
|
// required-only fallback so callers combining a mandatory flag with a
|
|
// perf-only one (HOST_VISIBLE | DEVICE_LOCAL descriptor heaps) no
|
|
// longer throw on devices without a host-visible device-local heap.
|
|
// The selection is pure CPU logic over Device::memoryProperties, so
|
|
// this test installs synthetic memory layouts and drives it directly —
|
|
// no GPU device needed at runtime.
|
|
cfg.tests.push_back(LibraryTest("MemoryTypeFallback"));
|
|
|
|
// Issue #70: ImageVulkan's mip-chain upload folds the never-read final
|
|
// blit destination into a single batched final-transition barrier
|
|
// instead of giving it a dedicated DST->SRC barrier. The barrier set is
|
|
// built by BuildMipChainFinalBarriers, pure CPU logic over mip count and
|
|
// layout, so this test drives it directly — no GPU device at runtime.
|
|
cfg.tests.push_back(LibraryTest("MipChainBarrierBatch"));
|
|
|
|
// 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.
|
|
cfg.tests.push_back(LibraryTest("SwapchainBarrierScope"));
|
|
|
|
// Issue #47: the fused UI uber-kernel (shaders/ui-fused.comp.glsl) and
|
|
// its C++ push-constant mirror UIFusedHeader. Compiles the real shader
|
|
// with glslang, validates with spirv-val, and pins the push-constant
|
|
// member offsets to UIFusedHeader's layout so a GLSL/C++ drift can't
|
|
// slip through (the C++ static_assert only guards the C++ side). No GPU
|
|
// device at runtime, but glslang + spirv-val are required tools.
|
|
Test fusedTest = LibraryTest("UIFusedShader");
|
|
fusedTest.requires_ = { "tool:glslang", "tool:spirv-val" };
|
|
cfg.tests.push_back(std::move(fusedTest));
|
|
|
|
// Issue #164: every GLSL shader under examples/ compiles. Nothing here
|
|
// builds the examples, so when glslang began rejecting Sponza's
|
|
// descriptor_heap sampler2D nothing noticed. Compiles each one as
|
|
// crafter-build would for its example, the descriptor-set build too
|
|
// where it has one. No GPU; glslang is a required tool.
|
|
Test exampleShadersTest = LibraryTest("ExampleShaders");
|
|
exampleShadersTest.requires_ = { "tool:glslang" };
|
|
cfg.tests.push_back(std::move(exampleShadersTest));
|
|
|
|
// The text kernels decode FontAtlas's SDF with copies of its two
|
|
// encoding constants, in ui-shared.glsl and in the WGSL of
|
|
// dom-webgpu.js, which no test renders. Reads the sources and holds
|
|
// the copies to FontAtlas's, and checks that ui-text and ui-fused
|
|
// decode through the shared helper. No GPU.
|
|
cfg.tests.push_back(LibraryTest("FontAtlasDecode"));
|
|
|
|
// Issue #60: VulkanBuffer::FlushDevice / FlushHost now record the chosen
|
|
// memory type's propertyFlags at Create time and skip the
|
|
// flush/invalidate when the memory is HOST_COHERENT. The gate is pure
|
|
// logic over the recorded flags, so this test stamps them directly and
|
|
// verifies the coherent path issues no Vulkan call — no GPU device
|
|
// needed at runtime.
|
|
cfg.tests.push_back(LibraryTest("VulkanBufferFlushGate"));
|
|
|
|
// VulkanBuffer::Create requests and queries a device address only
|
|
// when the usage includes SHADER_DEVICE_ADDRESS; other buffers report
|
|
// address 0. Needs a Vulkan device at runtime.
|
|
cfg.tests.push_back(LibraryTest("VulkanBufferAddress"));
|
|
|
|
// Ranged FlushDevice: UI descriptor registration now flushes only the
|
|
// written descriptor byte range instead of the whole heap, rounding the
|
|
// range outward to nonCoherentAtomSize via AlignMappedFlushRange. The
|
|
// rounding is pure math and the coherent gate is pure logic, so this
|
|
// test drives both directly with no GPU device.
|
|
cfg.tests.push_back(LibraryTest("VulkanBufferRangedFlush"));
|
|
|
|
// Issue #63: VulkanBuffer::Resize now reuses the existing allocation in
|
|
// place when a new request still fits the created capacity and the
|
|
// immutable-at-create properties match (usage flags fixed at create;
|
|
// chosen memory type still satisfies the required flags), instead of
|
|
// always destroying + reallocating. The reuse guard is pure logic over
|
|
// recorded fields, so this test stamps a fake handle + capacity/flags and
|
|
// verifies the in-place path issues no Vulkan call — no GPU device needed.
|
|
cfg.tests.push_back(LibraryTest("VulkanBufferResizeReuse"));
|
|
|
|
// Issue #101: fence-keyed deferred resource-deletion queue. Since #40
|
|
// dropped the per-frame wait-idle, destroying a buffer the GPU may
|
|
// still read (Resize's reallocate path) is a use-after-free.
|
|
// VulkanBuffer::DeferredClear / Resize now hand handles to Device's
|
|
// queue, which ReclaimDeletions frees only after framesInFlight frames
|
|
// and DrainDeletions frees on a wait-idle. The retire timing is driven
|
|
// on a real headless device (real buffers so the frees execute and the
|
|
// validation layer can object) by stepping Device::frameCounter — no
|
|
// swapchain/window needed, so it shares the native build settings.
|
|
cfg.tests.push_back(LibraryTest("DeferredDeletion"));
|
|
|
|
// The deletion queue takes the handles that destructors on other
|
|
// threads queue while the render loop reclaims it.
|
|
cfg.tests.push_back(LibraryTest("DeletionQueueThreads"));
|
|
|
|
// Issue #67: the compressed Mesh::Build path no longer pins its
|
|
// host-visible `compressedStaging` for the mesh's life — it releases it
|
|
// via DeferredClear() right after recording the GPU decompress, so the
|
|
// fence-keyed deletion queue (#101/#102) frees it once that submit's
|
|
// frame has cleared. Drives the real VK_EXT_memory_decompression /
|
|
// GDeflate path on a headless device (no swapchain/window — a decompress
|
|
// + BLAS build only needs the queue + command pool) and asserts the
|
|
// staging is enqueued (not pinned), the build is validation-clean with
|
|
// correct decompressed data, and the entry retires only after
|
|
// framesInFlight frames. Shares the native build settings; needs the
|
|
// asset pipeline for SaveCompressed/LoadCompressedMesh (cfg.dependencies
|
|
// already carries Crafter.Asset).
|
|
cfg.tests.push_back(LibraryTest("MeshDecompressStagingRelease"));
|
|
|
|
// A Mesh queues its acceleration structure for deferred deletion when
|
|
// it is rebuilt, destroyed or moved from, and a rebuild builds into
|
|
// fresh storage. Headless like the test above; needs a Vulkan RT
|
|
// device at runtime.
|
|
cfg.tests.push_back(LibraryTest("MeshLifetime"));
|
|
|
|
// A VulkanShader queues its shader module for deferred deletion when
|
|
// it is destroyed or move-assigned over, and can only be moved.
|
|
// Headless; needs any Vulkan device.
|
|
Test shaderLifetimeTest = LibraryTest("VulkanShaderLifetime");
|
|
shaderLifetimeTest.config.shaders.emplace_back(fs::path("tests/VulkanShaderLifetime/noop.comp.glsl"), std::string("main"), ShaderType::Compute);
|
|
cfg.tests.push_back(std::move(shaderLifetimeTest));
|
|
|
|
// ComputeShader and PipelineRTVulkan queue their pipelines for deferred
|
|
// deletion when destroyed or move-assigned over, and only move.
|
|
// Headless; needs any Vulkan device, and ray tracing for half of it.
|
|
Test pipelineLifetimeTest = LibraryTest("PipelineLifetime");
|
|
pipelineLifetimeTest.config.shaders.emplace_back(fs::path("tests/VulkanShaderLifetime/noop.comp.glsl"), std::string("main"), ShaderType::Compute);
|
|
pipelineLifetimeTest.config.shaders.emplace_back(fs::path("tests/PipelineLifetime/lifetime-raygen.glsl"), std::string("main"), ShaderType::RayGen);
|
|
cfg.tests.push_back(std::move(pipelineLifetimeTest));
|
|
|
|
// DescriptorHeapVulkan hands freed slots out again, ranges included.
|
|
// Headless; needs any Vulkan device.
|
|
cfg.tests.push_back(LibraryTest("HeapSlotReuse"));
|
|
|
|
// Issue #163: where DescriptorHeapVulkan::Initialize puts the buffer
|
|
// descriptors and the reserved ranges. The buffer region's start was
|
|
// rounded down, so on a driver whose image descriptors are smaller
|
|
// than its buffer descriptors the buffers overlapped the last images
|
|
// or ran into the reserved range, which was itself aligned for image
|
|
// descriptors only, and on llvmpipe, which reserves nothing, a heap
|
|
// without sampler slots was a zero-size buffer.
|
|
// DescriptorHeapVulkan::ComputeLayout is pure arithmetic over the
|
|
// descriptor sizes, so this test feeds it every legal combination, no
|
|
// GPU device needed at runtime.
|
|
cfg.tests.push_back(LibraryTest("DescriptorHeapLayout"));
|
|
|
|
// The debug messenger prints info messages only with validation on.
|
|
// Headless; needs any Vulkan device.
|
|
cfg.tests.push_back(LibraryTest("DebugMessengerSeverity"));
|
|
|
|
// A re-upload into a buffer the GPU may still read lands after the
|
|
// reads already submitted. Headless; needs any Vulkan device.
|
|
cfg.tests.push_back(LibraryTest("UploadInFlight"));
|
|
|
|
// CRAFTER_GRAPHICS_DEVICE picks the GPU. Needs two Vulkan devices.
|
|
cfg.tests.push_back(LibraryTest("DeviceSelection"));
|
|
|
|
// Issue #114: a static ImageVulkan no longer pins its host-visible
|
|
// staging `buffer` for the image's life — Update releases it via
|
|
// DeferredClear() right after recording the buffer→image copy, so the
|
|
// fence-keyed deletion queue (#101/#102) frees it once that submit's
|
|
// frame clears, while a `streamed` image (the FontAtlas) keeps its
|
|
// persistent map. Drives the real upload path on a headless device (no
|
|
// swapchain/window — a buffer→image copy + readback only needs the queue
|
|
// + command pool) and asserts the staging is enqueued (not pinned), the
|
|
// image reads back byte-equal (the released staging outlived the submit),
|
|
// the entry retires only after framesInFlight frames, and a streamed
|
|
// image keeps + then frees its staging on Destroy. Shares the native
|
|
// build settings.
|
|
cfg.tests.push_back(LibraryTest("ImageStagingRelease"));
|
|
|
|
// A non-square image gets every level of its mip chain, each at least
|
|
// one texel on a side. Headless; needs any Vulkan device.
|
|
cfg.tests.push_back(LibraryTest("MipChainNonSquare"));
|
|
|
|
// Issue #89: Device::PreferDirectDeviceWrite chooses the upload strategy
|
|
// for a CPU-written, GPU-read buffer — direct HOST_VISIBLE|DEVICE_LOCAL
|
|
// map+write on ReBAR/UMA vs. staged-into-pure-DEVICE_LOCAL on a small
|
|
// BAR window (#58). The decision is pure CPU logic over
|
|
// Device::memoryProperties (types + heaps), so this test installs
|
|
// synthetic ReBAR / UMA / small-window / no-BAR layouts and drives it
|
|
// directly — no GPU device needed at runtime.
|
|
cfg.tests.push_back(LibraryTest("UploadStrategy"));
|
|
|
|
// Issue #57: Font::GetLineWidth memoises per-codepoint advances in
|
|
// font units (Font::AdvanceUnits) and rescales per call, instead of
|
|
// calling stbtt_GetCodepointHMetrics for every glyph on every caret
|
|
// query. Pure CPU — Font only touches stb_truetype — so this drives
|
|
// the public API directly with no Vulkan device. The font file is
|
|
// copied next to the binary; the test also probes the project root.
|
|
Test advTest = LibraryTest("FontAdvanceCache");
|
|
advTest.config.files = { fs::path("tests/FontAdvanceCache/font.ttf") };
|
|
cfg.tests.push_back(std::move(advTest));
|
|
|
|
// Issue #50: uiResolveScreenPixel now gates its per-pixel clip-rect
|
|
// compares on the reserved kUIFlagClip bit, which FillHeader sets only
|
|
// when the clip rect is narrower than the surface. The decision lives
|
|
// in UIRenderer::ClipFlags — pure CPU logic over the clip rect and the
|
|
// surface size — so this test drives it directly with synthetic
|
|
// dimensions, no Window or GPU device needed at runtime.
|
|
cfg.tests.push_back(LibraryTest("UIClipFlag"));
|
|
|
|
// Issue #56: InputField_HitTestCursor mapped a click x to a cursor byte
|
|
// offset by re-walking the prefix for every boundary (O(n^2) glyph
|
|
// metric lookups) over raw byte boundaries. It now delegates to
|
|
// Font::NearestCursorByte — one cumulative-advance pass over codepoint
|
|
// boundaries. The mapping is pure CPU over a TrueType file, so this test
|
|
// drives it directly: no Vulkan device or window. The font is copied
|
|
// next to the binary; the test also probes the project-root path.
|
|
Test hitTest = LibraryTest("InputFieldHitTest");
|
|
hitTest.config.files = { fs::path("tests/InputFieldHitTest/font.ttf") };
|
|
cfg.tests.push_back(std::move(hitTest));
|
|
|
|
// Issue #128: DrawInputField re-measured the cursor prefix via
|
|
// Font::GetLineWidth every frame of a focused field even though only the
|
|
// blink changes frame-to-frame. The prefix WIDTH is now memoised on the
|
|
// InputField keyed on (prefix bytes, fontSize); the absolute caretX is
|
|
// deliberately not cached so a relocated field can't get a stale caret.
|
|
// The memo is transparent presentation logic over a TrueType file —
|
|
// DrawText no-ops with a null atlas/renderer — so this test drives
|
|
// DrawInputField directly with no Vulkan device or window. The font is
|
|
// copied next to the binary; the test also probes the project root.
|
|
Test caretTest = LibraryTest("InputFieldCaretCache");
|
|
caretTest.config.files = { fs::path("tests/InputFieldCaretCache/font.ttf") };
|
|
cfg.tests.push_back(std::move(caretTest));
|
|
|
|
// Issue #69: the engine feeds one shared Device::pipelineCache to every
|
|
// vkCreate*Pipelines call and persists it across runs, discarding an
|
|
// on-disk blob whose header doesn't match the current GPU. That gate —
|
|
// Device::PipelineCacheDataCompatible — is pure logic over the standard
|
|
// VkPipelineCache header and Device::deviceProperties, so this test
|
|
// stamps synthetic device identities + headers and drives it directly,
|
|
// no GPU device needed at runtime.
|
|
cfg.tests.push_back(LibraryTest("PipelineCacheValidation"));
|
|
|
|
// Which features Device::Initialize enables, which GPU it picks, and
|
|
// which SPIR-V capabilities VulkanShader lets through:
|
|
// Device::ResolveFeatures, ChooseDevice and CheckShaderCapabilities are
|
|
// pure logic, so this test drives them with synthetic GPUs and
|
|
// hand-built SPIR-V, no GPU device needed at runtime.
|
|
cfg.tests.push_back(LibraryTest("DeviceFeatures"));
|
|
|
|
// The same on a real GPU, end to end: required and optional features
|
|
// through Device::Initialize, and the Vulkan 1.3 path
|
|
// (CRAFTER_GRAPHICS_NO_VULKAN_1_4) in a child process. Needs a Vulkan
|
|
// device.
|
|
cfg.tests.push_back(LibraryTest("FeatureRequests"));
|
|
}
|
|
|
|
return cfg;
|
|
}
|