Window - The constructor made a fractional scale, a toplevel decoration and a viewport from Device::fractionalScaleManager, Device::manager and Device::wpViewporter without looking at them. Device::handle_global binds those globals only when the compositor advertises them, and Initialize requires only wl_shm, wl_compositor and xdg_wm_base, so on a compositor without one of the three (GNOME's Mutter has no xdg-decoration) the constructor handed libwayland a null proxy and crashed before the window existed (#162). Each object is now made only when its global is there, which Resize and the destructor already assumed. - The fractional scale needs the viewporter too: the preferred scale is applied through the viewport, which shows the buffer's physical pixels at width/scale x height/scale. A surface without a viewport is as large as its buffer, so there the scale stays 1, which keeps pointer and touch positions, configure sizes and the text-input caret, all converted by it, in physical pixels; the compositor scales the window up on a HiDPI output. A scale that changes after the constructor is still applied only by the next resize, which is #165. - Without xdg-decoration the window has no title bar, since the library draws no client-side decorations. Device - The optional globals say they are optional, and that a request on a null proxy crashes. Tests - WindowOptionalGlobals: hides xdg-decoration, wp_viewporter and wp_fractional_scale_v1 from the Window one at a time and all at once, by nulling the Device statics after Initialize, and renders, resizes and closes a window each time without a protocol error. Before the fix it crashed on the window without xdg-decoration. A global the compositor does not offer is reported as a SKIP line, since the path that uses it goes untested there. README - Which Wayland protocols the window needs, and what it does without the optional ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
755 lines
44 KiB
C++
755 lines
44 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;
|
|
|
|
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");
|
|
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" };
|
|
cfg.linkFlags.push_back(windows ? "-lvulkan-1" : "-lvulkan");
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
// 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"));
|
|
|
|
// 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"));
|
|
|
|
// Issue #40: multi-frame-in-flight frame pacing (per-image fences
|
|
// + per-frame 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));
|
|
|
|
// 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 #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 don't touch
|
|
// 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 #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));
|
|
|
|
// 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"));
|
|
|
|
// 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;
|
|
}
|