2026-07-22 18:09:06 +02:00
|
|
|
//SPDX-License-Identifier: LGPL-3.0-only
|
|
|
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
|
|
2026-04-30 01:29:17 +02:00
|
|
|
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());
|
2026-05-12 00:24:48 +02:00
|
|
|
|
2026-07-19 01:38:25 +02:00
|
|
|
// Crafter.Event/Math/Asset are always resolved from forgejo (GitProject);
|
|
|
|
|
// there is no local-sibling branch for them here. A consumer building
|
|
|
|
|
// THIS project with --local (to pick up local Crafter.Graphics edits)
|
|
|
|
|
// forwards --local down through depArgs, but these git deps can't honour
|
|
|
|
|
// it — the flag makes their resolver look for an unsuffixed local
|
|
|
|
|
// checkout that doesn't exist. Strip it so the sub-deps resolve normally
|
|
|
|
|
// while Crafter.Graphics itself is still built from the local tree.
|
|
|
|
|
std::erase(depArgs, "--local");
|
|
|
|
|
|
2026-05-27 04:38:30 +02:00
|
|
|
Configuration* event = GitProject({
|
|
|
|
|
.source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Event.git" },
|
|
|
|
|
.args = depArgs,
|
|
|
|
|
});
|
|
|
|
|
Configuration* math = GitProject({
|
|
|
|
|
.source = { "https://forgejo.catcrafts.net/Catcrafts/Crafter.Math.git" },
|
|
|
|
|
.args = depArgs,
|
|
|
|
|
});
|
|
|
|
|
Configuration* asset = GitProject({
|
|
|
|
|
.source = { "https://forgejo.catcrafts.net/Catcrafts/Crafter.Asset.git" },
|
|
|
|
|
.args = depArgs,
|
|
|
|
|
});
|
2026-04-30 01:29:17 +02:00
|
|
|
|
|
|
|
|
Configuration cfg;
|
|
|
|
|
cfg.path = "./";
|
|
|
|
|
cfg.name = "Crafter.Graphics";
|
|
|
|
|
cfg.outputName = "Crafter.Graphics";
|
|
|
|
|
cfg.type = ConfigurationType::LibraryStatic;
|
|
|
|
|
auto opts = ApplyStandardArgs(cfg, args);
|
2026-05-19 00:27:09 +02:00
|
|
|
cfg.dependencies = { event, math, asset };
|
2026-04-30 01:29:17 +02:00
|
|
|
|
|
|
|
|
// 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
|
2026-05-18 02:07:48 +02:00
|
|
|
// 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.
|
2026-05-19 00:27:09 +02:00
|
|
|
//
|
|
|
|
|
// 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");
|
2026-05-18 02:07:48 +02:00
|
|
|
} else if (windows) {
|
2026-04-30 01:29:17 +02:00
|
|
|
cfg.defines.push_back({"CRAFTER_GRAPHICS_WINDOW_WIN32", ""});
|
|
|
|
|
cfg.linkFlags.push_back("-lkernel32");
|
|
|
|
|
cfg.linkFlags.push_back("-luser32");
|
|
|
|
|
cfg.linkFlags.push_back("-lgdi32");
|
2026-05-12 00:24:48 +02:00
|
|
|
// Windows.Gaming.Input (WGI) needs the WinRT activation runtime
|
|
|
|
|
// and combase for HSTRING / RoGetActivationFactory.
|
|
|
|
|
cfg.linkFlags.push_back("-lruntimeobject");
|
|
|
|
|
cfg.linkFlags.push_back("-lcombase");
|
2026-04-30 01:29:17 +02:00
|
|
|
} else {
|
|
|
|
|
cfg.defines.push_back({"CRAFTER_GRAPHICS_WINDOW_WAYLAND", ""});
|
|
|
|
|
cfg.linkFlags.push_back("-lwayland-client");
|
|
|
|
|
cfg.linkFlags.push_back("-lxkbcommon");
|
2026-05-12 00:24:48 +02:00
|
|
|
// Gamepad: libudev for hot-plug + device enumeration; libevdev
|
2026-10-19 03:03:16 +02:00
|
|
|
// for event parsing + axis calibration.
|
2026-05-12 00:24:48 +02:00
|
|
|
cfg.linkFlags.push_back("-ludev");
|
2026-10-19 03:03:16 +02:00
|
|
|
|
|
|
|
|
// 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.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()));
|
|
|
|
|
|
2026-04-30 01:29:17 +02:00
|
|
|
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");
|
2026-10-18 01:50:39 +02:00
|
|
|
cfg.cFiles.push_back("lib/text-input-unstable-v3-protocol");
|
2026-04-30 01:29:17 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-18 02:07:48 +02:00
|
|
|
// 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");
|
|
|
|
|
}
|
2026-04-30 01:29:17 +02:00
|
|
|
|
|
|
|
|
if (opts.Has("--timing")) cfg.defines.push_back({"CRAFTER_TIMING", ""});
|
|
|
|
|
|
2026-05-18 02:07:48 +02:00
|
|
|
// 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.
|
2026-05-24 13:32:08 +02:00
|
|
|
std::array<fs::path, 42> ifaces = {
|
2026-04-30 01:29:17 +02:00
|
|
|
"interfaces/Crafter.Graphics",
|
|
|
|
|
"interfaces/Crafter.Graphics-Animation",
|
2026-05-12 00:24:48 +02:00
|
|
|
"interfaces/Crafter.Graphics-Clipboard",
|
2026-05-02 21:08:20 +02:00
|
|
|
"interfaces/Crafter.Graphics-ComputeShader",
|
2026-05-12 00:24:48 +02:00
|
|
|
"interfaces/Crafter.Graphics-Decompress",
|
2026-04-30 01:29:17 +02:00
|
|
|
"interfaces/Crafter.Graphics-DescriptorHeapVulkan",
|
2026-05-18 04:58:52 +02:00
|
|
|
"interfaces/Crafter.Graphics-DescriptorHeapWebGPU",
|
2026-04-30 01:29:17 +02:00
|
|
|
"interfaces/Crafter.Graphics-Device",
|
2026-05-18 02:07:48 +02:00
|
|
|
"interfaces/Crafter.Graphics-Dom",
|
|
|
|
|
"interfaces/Crafter.Graphics-DomEvents",
|
2026-04-30 01:29:17 +02:00
|
|
|
"interfaces/Crafter.Graphics-Font",
|
2026-05-02 21:08:20 +02:00
|
|
|
"interfaces/Crafter.Graphics-FontAtlas",
|
2026-04-30 01:29:17 +02:00
|
|
|
"interfaces/Crafter.Graphics-ForwardDeclarations",
|
2026-05-12 00:24:48 +02:00
|
|
|
"interfaces/Crafter.Graphics-Gamepad",
|
2026-05-18 04:58:52 +02:00
|
|
|
"interfaces/Crafter.Graphics-GraphicsTypes",
|
2026-05-19 00:27:09 +02:00
|
|
|
"interfaces/Crafter.Graphics-Image2D",
|
2026-04-30 01:29:17 +02:00
|
|
|
"interfaces/Crafter.Graphics-ImageVulkan",
|
2026-05-12 00:24:48 +02:00
|
|
|
"interfaces/Crafter.Graphics-Input",
|
2026-05-03 02:45:38 +02:00
|
|
|
"interfaces/Crafter.Graphics-InputField",
|
2026-05-12 00:24:48 +02:00
|
|
|
"interfaces/Crafter.Graphics-Keys",
|
2026-04-30 01:29:17 +02:00
|
|
|
"interfaces/Crafter.Graphics-Mesh",
|
|
|
|
|
"interfaces/Crafter.Graphics-PipelineRTVulkan",
|
2026-05-18 18:43:30 +02:00
|
|
|
"interfaces/Crafter.Graphics-PipelineRTWebGPU",
|
2026-05-24 13:32:08 +02:00
|
|
|
"interfaces/Crafter.Graphics-PlainComputeShader",
|
2026-04-30 01:29:17 +02:00
|
|
|
"interfaces/Crafter.Graphics-RenderingElement3D",
|
2026-05-01 23:35:37 +02:00
|
|
|
"interfaces/Crafter.Graphics-RenderPass",
|
2026-05-18 02:07:48 +02:00
|
|
|
"interfaces/Crafter.Graphics-Router",
|
2026-05-18 18:43:30 +02:00
|
|
|
"interfaces/Crafter.Graphics-RT",
|
2026-05-01 23:35:37 +02:00
|
|
|
"interfaces/Crafter.Graphics-RTPass",
|
2026-04-30 01:29:17 +02:00
|
|
|
"interfaces/Crafter.Graphics-SamplerVulkan",
|
|
|
|
|
"interfaces/Crafter.Graphics-ShaderBindingTableVulkan",
|
2026-05-18 18:43:30 +02:00
|
|
|
"interfaces/Crafter.Graphics-ShaderBindingTableWebGPU",
|
2026-04-30 01:29:17 +02:00
|
|
|
"interfaces/Crafter.Graphics-ShaderVulkan",
|
|
|
|
|
"interfaces/Crafter.Graphics-Types",
|
2026-05-01 23:35:37 +02:00
|
|
|
"interfaces/Crafter.Graphics-UI",
|
2026-05-02 21:08:20 +02:00
|
|
|
"interfaces/Crafter.Graphics-UIComponents",
|
2026-04-30 01:29:17 +02:00
|
|
|
"interfaces/Crafter.Graphics-VulkanBuffer",
|
|
|
|
|
"interfaces/Crafter.Graphics-VulkanTransition",
|
2026-05-18 04:58:52 +02:00
|
|
|
"interfaces/Crafter.Graphics-WebGPU",
|
|
|
|
|
"interfaces/Crafter.Graphics-WebGPUBuffer",
|
|
|
|
|
"interfaces/Crafter.Graphics-WebGPUComputeShader",
|
2026-04-30 01:29:17 +02:00
|
|
|
"interfaces/Crafter.Graphics-Window",
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-18 02:07:48 +02:00
|
|
|
if (dom) {
|
2026-05-18 04:58:52 +02:00
|
|
|
// DOM impl set. UI-Shared.cpp is backend-agnostic; UI-WebGPU.cpp
|
|
|
|
|
// is the DOM-only implementation of UIRenderer's GPU-touching
|
2026-05-24 13:32:08 +02:00
|
|
|
// methods. Font / FontAtlas / UIComponents / InputField are now
|
|
|
|
|
// portable.
|
|
|
|
|
std::array<fs::path, 17> domImpls = {
|
2026-05-18 02:07:48 +02:00
|
|
|
"implementations/Crafter.Graphics-Clipboard",
|
|
|
|
|
"implementations/Crafter.Graphics-Dom",
|
2026-05-18 04:58:52 +02:00
|
|
|
"implementations/Crafter.Graphics-Font",
|
|
|
|
|
"implementations/Crafter.Graphics-FontAtlas",
|
2026-05-18 02:07:48 +02:00
|
|
|
"implementations/Crafter.Graphics-Gamepad",
|
|
|
|
|
"implementations/Crafter.Graphics-Input",
|
2026-05-24 13:32:08 +02:00
|
|
|
"implementations/Crafter.Graphics-InputField",
|
2026-05-18 18:43:30 +02:00
|
|
|
"implementations/Crafter.Graphics-Mesh-WebGPU",
|
|
|
|
|
"implementations/Crafter.Graphics-PipelineRTWebGPU",
|
|
|
|
|
"implementations/Crafter.Graphics-RenderingElement3D-WebGPU",
|
2026-05-18 02:07:48 +02:00
|
|
|
"implementations/Crafter.Graphics-Router",
|
2026-05-18 18:43:30 +02:00
|
|
|
"implementations/Crafter.Graphics-ShaderBindingTableWebGPU",
|
2026-05-18 04:58:52 +02:00
|
|
|
"implementations/Crafter.Graphics-UI-Shared",
|
|
|
|
|
"implementations/Crafter.Graphics-UI-WebGPU",
|
|
|
|
|
"implementations/Crafter.Graphics-UIComponents",
|
2026-05-18 05:39:17 +02:00
|
|
|
"implementations/Crafter.Graphics-WebGPUComputeShader",
|
2026-05-18 02:07:48 +02:00
|
|
|
"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"));
|
2026-08-05 04:08:13 +02:00
|
|
|
|
|
|
|
|
// 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"));
|
|
|
|
|
}
|
2026-05-18 02:07:48 +02:00
|
|
|
} else {
|
2026-05-18 04:58:52 +02:00
|
|
|
std::array<fs::path, 14> impls = {
|
2026-05-18 02:07:48 +02:00
|
|
|
"implementations/Crafter.Graphics-Clipboard",
|
|
|
|
|
"implementations/Crafter.Graphics-ComputeShader",
|
|
|
|
|
"implementations/Crafter.Graphics-Device",
|
|
|
|
|
"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",
|
2026-05-18 04:58:52 +02:00
|
|
|
"implementations/Crafter.Graphics-UI-Shared",
|
2026-05-18 02:07:48 +02:00
|
|
|
"implementations/Crafter.Graphics-UIComponents",
|
|
|
|
|
"implementations/Crafter.Graphics-Window",
|
|
|
|
|
};
|
|
|
|
|
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
|
|
|
|
|
feat(vulkan): run the compute/UI path without VK_EXT_descriptor_heap
Device::Initialize required twelve device extensions with no capability
check. Mesa Turnip (Adreno) lacks six of them: VK_EXT_descriptor_heap,
VK_KHR_shader_untyped_pointers, VK_EXT_device_fault and the three ray
tracing extensions. vkCreateDevice failed there, so not even a UI-only
program could start, although the UI path is compute-only and needs
nothing else that Turnip is missing.
The descriptor heap, ray tracing and device fault are now optional
groups, detected at start-up from the extension list and the feature
bits (Device::descriptorHeapSupported, rayTracingSupported,
deviceFaultSupported). Ray tracing needs the heap; on a device without
it the ray tracing entry points throw a message naming the cause.
Without the heap the same bindless slots live in one update-after-bind
descriptor set per frame, owned by DescriptorHeapVulkan: binding 0
storage images, 1 sampled images, 2 samplers, 3 storage buffers. Slot
allocation, the slot handles, UIDispatchHeader and the UIRenderer API
are unchanged; buffer slots simply stop being offset. Window::Render
binds that frame's set where it binds the heap, every compute pipeline
shares the heap object's pipeline layout, and push data travels as push
constants.
Each UI kernel is built twice from one source. ui-shared.glsl declares
its resources through UI_HEAP_* macros that expand to descriptor_heap
or, with CRAFTER_GRAPHICS_DESCRIPTOR_SETS defined, to set/binding.
<name>.comp.spv and <name>.comp.sets.spv ship side by side and
ComputeShader::Load picks one. The heap build of every shader is
byte-identical to before.
Breaking:
- ComputeShader::Load takes the heap as a second argument.
- DescriptorHeapVulkan is no longer copyable or movable.
- Custom UI shaders declare heap resources with the UI_HEAP_* macros
and are registered twice in project.cpp (see the README).
- project.cpp uses Shader::defines and Shader::variant, so it needs a
crafter-build that has them (Crafter.Build master, fe38cc1 or later).
Also:
- maintenance5 is enabled as a feature, not only as an extension:
every VulkanBuffer already relied on VkBufferUsageFlags2CreateInfo.
- Barrier stage masks drop RAY_TRACING_SHADER on devices without ray
tracing, and ImageVulkan::Create resolves its default consumer stage
to COMPUTE_SHADER there.
- Window refuses a swapchain whose image count is not numFrames
instead of writing past images[]. The limitation itself is #160.
- CRAFTER_GRAPHICS_NO_DESCRIPTOR_HEAP forces the fallback on a driver
that has the heap, so it can be exercised on a desktop GPU.
Verified on an RTX 4090 in both modes under the validation layer, and
on a Fairphone 6 (Turnip, Adreno 810, postmarketOS), where the fallback
is the only path.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 17:28:46 +02:00
|
|
|
// 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";
|
|
|
|
|
}
|
2026-05-18 02:07:48 +02:00
|
|
|
cfg.buildFiles.emplace_back(fs::path("shaders/ui-shared.glsl"));
|
2026-06-03 02:28:09 +00:00
|
|
|
|
|
|
|
|
// 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). The test executable recompiles the whole module plus
|
|
|
|
|
// tests/PushConstantRewrite/main.cpp; Configuration isn't copyable
|
|
|
|
|
// (it owns the parsed module list), so the shared build settings are
|
|
|
|
|
// mirrored field by field. 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;
|
|
|
|
|
Configuration& tc = pcTest.config;
|
|
|
|
|
tc.path = cfg.path;
|
|
|
|
|
tc.name = "PushConstantRewrite";
|
|
|
|
|
tc.outputName = "PushConstantRewrite";
|
|
|
|
|
tc.type = ConfigurationType::Executable;
|
|
|
|
|
tc.target = cfg.target;
|
|
|
|
|
tc.march = cfg.march;
|
|
|
|
|
tc.mtune = cfg.mtune;
|
|
|
|
|
tc.debug = cfg.debug;
|
|
|
|
|
tc.sysroot = cfg.sysroot;
|
|
|
|
|
tc.dependencies = cfg.dependencies;
|
|
|
|
|
tc.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
tc.compileFlags = cfg.compileFlags;
|
|
|
|
|
tc.linkFlags = cfg.linkFlags;
|
|
|
|
|
tc.defines = cfg.defines;
|
|
|
|
|
tc.cFiles = cfg.cFiles;
|
|
|
|
|
std::vector<fs::path> testImpls(impls.begin(), impls.end());
|
|
|
|
|
testImpls.emplace_back("tests/PushConstantRewrite/main");
|
|
|
|
|
tc.GetInterfacesAndImplementations(ifaces, testImpls);
|
|
|
|
|
pcTest.requires_ = { "tool:glslang", "tool:spirv-val" };
|
|
|
|
|
cfg.tests.push_back(std::move(pcTest));
|
2026-06-12 15:02:28 +00:00
|
|
|
|
|
|
|
|
// 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) {
|
|
|
|
|
Test scrollTest;
|
|
|
|
|
Configuration& sc = scrollTest.config;
|
|
|
|
|
sc.path = cfg.path;
|
|
|
|
|
sc.name = "MouseScroll";
|
|
|
|
|
sc.outputName = "MouseScroll";
|
|
|
|
|
sc.type = ConfigurationType::Executable;
|
|
|
|
|
sc.target = cfg.target;
|
|
|
|
|
sc.march = cfg.march;
|
|
|
|
|
sc.mtune = cfg.mtune;
|
|
|
|
|
sc.debug = cfg.debug;
|
|
|
|
|
sc.sysroot = cfg.sysroot;
|
|
|
|
|
sc.dependencies = cfg.dependencies;
|
|
|
|
|
sc.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
sc.compileFlags = cfg.compileFlags;
|
|
|
|
|
sc.linkFlags = cfg.linkFlags;
|
|
|
|
|
sc.defines = cfg.defines;
|
|
|
|
|
sc.cFiles = cfg.cFiles;
|
|
|
|
|
std::vector<fs::path> scrollImpls(impls.begin(), impls.end());
|
|
|
|
|
scrollImpls.emplace_back("tests/MouseScroll/main");
|
|
|
|
|
sc.GetInterfacesAndImplementations(ifaces, scrollImpls);
|
|
|
|
|
cfg.tests.push_back(std::move(scrollTest));
|
feat(window): multi-frame-in-flight frame pacing (#40)
The renderer was effectively single-buffered despite allocating
triple-buffered infrastructure: Render() ended with an unconditional
vkQueueWaitIdle, and a single (presentComplete, renderComplete)
semaphore pair with zero fences was shared for the Window's lifetime.
Rework the pacing model so up to numFrames frames overlap:
- Per-swapchain-image VkFence, signaled by the submit and waited+reset
before that image's command buffer / descriptor-heap slot is
re-recorded. Keyed by acquired image index (drawCmdBuffers, the heap
slots, and the swapchain images are all image-indexed — see
WriteSwapchainDescriptors). Created signaled so first use passes.
- Per-image render-finished (present) semaphore: the presentation engine
holds it until the image is re-acquired, so per-image is the only safe
key (per-CPU-frame trips VUID-vkQueueSubmit-pSignalSemaphores-00067).
- Per-CPU-frame acquire semaphores (image index unknown until acquire
returns), sized numFrames+1: in-flight depth is bounded by the
per-image fences, so numFrames+1 distinct acquire semaphores guarantee
the reused one has no pending op (VUID-vkAcquireNextImageKHR-01779).
- Drop the steady-state vkQueueWaitIdle; keep it on resize / OUT_OF_DATE
/ teardown.
Add tests/FrameLoopSync: drives the real frame loop against a live
Wayland compositor for 60 frames (>> in-flight slots) and asserts the
CPU frame counter advanced, the swapchain rotated across multiple
images, and the validation layer stayed silent — the load-bearing check,
since the old single-pair design only avoided being an active race
because the wait-idle masked it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 15:37:11 +00:00
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
Test frameLoopTest;
|
|
|
|
|
Configuration& fl = frameLoopTest.config;
|
|
|
|
|
fl.path = cfg.path;
|
|
|
|
|
fl.name = "FrameLoopSync";
|
|
|
|
|
fl.outputName = "FrameLoopSync";
|
|
|
|
|
fl.type = ConfigurationType::Executable;
|
|
|
|
|
fl.target = cfg.target;
|
|
|
|
|
fl.march = cfg.march;
|
|
|
|
|
fl.mtune = cfg.mtune;
|
|
|
|
|
fl.debug = cfg.debug;
|
|
|
|
|
fl.sysroot = cfg.sysroot;
|
|
|
|
|
fl.dependencies = cfg.dependencies;
|
|
|
|
|
fl.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
fl.compileFlags = cfg.compileFlags;
|
|
|
|
|
fl.linkFlags = cfg.linkFlags;
|
|
|
|
|
fl.defines = cfg.defines;
|
|
|
|
|
fl.cFiles = cfg.cFiles;
|
|
|
|
|
std::vector<fs::path> frameLoopImpls(impls.begin(), impls.end());
|
|
|
|
|
frameLoopImpls.emplace_back("tests/FrameLoopSync/main");
|
|
|
|
|
fl.GetInterfacesAndImplementations(ifaces, frameLoopImpls);
|
|
|
|
|
cfg.tests.push_back(std::move(frameLoopTest));
|
2026-06-18 18:04:25 +00:00
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
Test setupReuseTest;
|
|
|
|
|
Configuration& sr = setupReuseTest.config;
|
|
|
|
|
sr.path = cfg.path;
|
|
|
|
|
sr.name = "SetupCmdBufferReuse";
|
|
|
|
|
sr.outputName = "SetupCmdBufferReuse";
|
|
|
|
|
sr.type = ConfigurationType::Executable;
|
|
|
|
|
sr.target = cfg.target;
|
|
|
|
|
sr.march = cfg.march;
|
|
|
|
|
sr.mtune = cfg.mtune;
|
|
|
|
|
sr.debug = cfg.debug;
|
|
|
|
|
sr.sysroot = cfg.sysroot;
|
|
|
|
|
sr.dependencies = cfg.dependencies;
|
|
|
|
|
sr.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
sr.compileFlags = cfg.compileFlags;
|
|
|
|
|
sr.linkFlags = cfg.linkFlags;
|
|
|
|
|
sr.defines = cfg.defines;
|
|
|
|
|
sr.cFiles = cfg.cFiles;
|
|
|
|
|
std::vector<fs::path> setupReuseImpls(impls.begin(), impls.end());
|
|
|
|
|
setupReuseImpls.emplace_back("tests/SetupCmdBufferReuse/main");
|
|
|
|
|
sr.GetInterfacesAndImplementations(ifaces, setupReuseImpls);
|
|
|
|
|
cfg.tests.push_back(std::move(setupReuseTest));
|
2026-10-06 17:28:46 +02:00
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
Configuration& up = parityTest.config;
|
|
|
|
|
up.path = cfg.path;
|
|
|
|
|
up.name = "UIFallbackParity";
|
|
|
|
|
up.outputName = "UIFallbackParity";
|
|
|
|
|
up.type = ConfigurationType::Executable;
|
|
|
|
|
up.target = cfg.target;
|
|
|
|
|
up.march = cfg.march;
|
|
|
|
|
up.mtune = cfg.mtune;
|
|
|
|
|
up.debug = cfg.debug;
|
|
|
|
|
up.sysroot = cfg.sysroot;
|
|
|
|
|
up.dependencies = cfg.dependencies;
|
|
|
|
|
up.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
up.compileFlags = cfg.compileFlags;
|
|
|
|
|
up.linkFlags = cfg.linkFlags;
|
|
|
|
|
up.defines = cfg.defines;
|
|
|
|
|
up.cFiles = cfg.cFiles;
|
|
|
|
|
up.shaders = cfg.shaders;
|
|
|
|
|
up.buildFiles = cfg.buildFiles;
|
|
|
|
|
up.files.emplace_back(fs::path("examples/HelloUI/font.ttf"));
|
|
|
|
|
std::vector<fs::path> parityImpls(impls.begin(), impls.end());
|
|
|
|
|
parityImpls.emplace_back("tests/UIFallbackParity/main");
|
|
|
|
|
up.GetInterfacesAndImplementations(ifaces, parityImpls);
|
|
|
|
|
cfg.tests.push_back(std::move(parityTest));
|
2026-06-12 15:02:28 +00:00
|
|
|
}
|
2026-06-16 13:37:46 +00:00
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
Test blasTest;
|
|
|
|
|
Configuration& bc = blasTest.config;
|
|
|
|
|
bc.path = cfg.path;
|
|
|
|
|
bc.name = "BLASBuildOptions";
|
|
|
|
|
bc.outputName = "BLASBuildOptions";
|
|
|
|
|
bc.type = ConfigurationType::Executable;
|
|
|
|
|
bc.target = cfg.target;
|
|
|
|
|
bc.march = cfg.march;
|
|
|
|
|
bc.mtune = cfg.mtune;
|
|
|
|
|
bc.debug = cfg.debug;
|
|
|
|
|
bc.sysroot = cfg.sysroot;
|
|
|
|
|
bc.dependencies = cfg.dependencies;
|
|
|
|
|
bc.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
bc.compileFlags = cfg.compileFlags;
|
|
|
|
|
bc.linkFlags = cfg.linkFlags;
|
|
|
|
|
bc.defines = cfg.defines;
|
|
|
|
|
bc.cFiles = cfg.cFiles;
|
|
|
|
|
std::vector<fs::path> blasImpls(impls.begin(), impls.end());
|
|
|
|
|
blasImpls.emplace_back("tests/BLASBuildOptions/main");
|
|
|
|
|
bc.GetInterfacesAndImplementations(ifaces, blasImpls);
|
|
|
|
|
cfg.tests.push_back(std::move(blasTest));
|
perf(text): cache shaped runs in ShapeText (#52)
ShapeText re-decoded UTF-8, re-looked-up every glyph, and recomputed
layout every frame for static strings (DrawText / EmitCenteredLabel run
inside per-frame onBuild). DrawText and EmitCenteredLabel also did a
second full pass over the glyphs to apply alignment.
Memoize the shaped run, keyed by (Font*, pxSize, color, utf8), as an
origin-relative vector<GlyphItem>. A cache hit skips decode/lookup/layout
and just translates the cached run into the output buffer by
(x + alignShift, baselineY). The alignment shift (Center = -advance/2,
Right = -advance) is folded into ShapeText's single pass, dropping the
second loop in both callers. TextAlign moves to the :UI partition so
ShapeText can take it.
Pure CPU memoization — the glyph buffer is fully rewritten and re-flushed
every frame, so a hit is byte-equivalent. A miss still calls Ensure so new
glyphs rasterise and dirty the atlas; hits don't (atlas entries are never
evicted). InvalidateFont drops a font's runs before it is destroyed (the
cache, like FontAtlas's glyph cache, keys on the Font pointer). A soft cap
bounds memory against pathological per-frame-unique text.
Adds the ShapeTextCache test (15 checks): hit == miss byte-for-byte, hits
don't touch the atlas, translate/alignment/truncation/InvalidateFont.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 15:40:52 +00:00
|
|
|
|
2026-06-16 18:24:09 +00:00
|
|
|
// 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. Needs a
|
|
|
|
|
// Vulkan RT device at runtime, so it shares the native build settings.
|
|
|
|
|
Test tlasTest;
|
|
|
|
|
Configuration& tlc = tlasTest.config;
|
|
|
|
|
tlc.path = cfg.path;
|
|
|
|
|
tlc.name = "TLASHighWaterMark";
|
|
|
|
|
tlc.outputName = "TLASHighWaterMark";
|
|
|
|
|
tlc.type = ConfigurationType::Executable;
|
|
|
|
|
tlc.target = cfg.target;
|
|
|
|
|
tlc.march = cfg.march;
|
|
|
|
|
tlc.mtune = cfg.mtune;
|
|
|
|
|
tlc.debug = cfg.debug;
|
|
|
|
|
tlc.sysroot = cfg.sysroot;
|
|
|
|
|
tlc.dependencies = cfg.dependencies;
|
|
|
|
|
tlc.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
tlc.compileFlags = cfg.compileFlags;
|
|
|
|
|
tlc.linkFlags = cfg.linkFlags;
|
|
|
|
|
tlc.defines = cfg.defines;
|
|
|
|
|
tlc.cFiles = cfg.cFiles;
|
|
|
|
|
std::vector<fs::path> tlasImpls(impls.begin(), impls.end());
|
|
|
|
|
tlasImpls.emplace_back("tests/TLASHighWaterMark/main");
|
|
|
|
|
tlc.GetInterfacesAndImplementations(ifaces, tlasImpls);
|
|
|
|
|
cfg.tests.push_back(std::move(tlasTest));
|
|
|
|
|
|
perf(tlas): dirty-track the per-frame TLAS instance+metadata upload (#118)
BuildTLAS rebuilt the host instance+metadata buffers with an O(n) copy of
every 64 B VkAccelerationStructureInstanceKHR + metadata entry every frame,
unconditionally, then flushed the whole high-water capacity (VK_WHOLE_SIZE)
on both buffers. At the millions-of-instances target that copy dominates the
CPU frame, and the whole-buffer flush costs on non-coherent BAR/VRAM.
Add a generation counter so only changed host-authored fields are copied, and
feed the same dirty span into the ranged FlushDevice(offset, bytes) overload:
- RenderingElement3D::hostDataVersion + MarkHostDataDirty() (bumps a global
monotonic counter). TlasWithBuffer::uploadedVersion records, per frame, the
version last copied into each slot. A slot is copied only when its element
advanced past the recorded version; version 0 ("untracked") reads dirty every
frame, so callers that don't opt in keep the prior copy-every-frame behaviour.
Globally-unique versions make this correct under relocation (Remove's
swap-and-pop, and remove+add that nets the same count on the refit path)
without tracking element identity. The reset on every topology change covers
buffer reallocation and the reshuffled element->slot mapping.
- The dirty [first, last] envelope drives both the copy and the flush: a new
VulkanBuffer::FlushDevice(cmd, access, stage, offset, bytes) overload flushes
+ barriers just that span for instanceBuffer, and the ranged
FlushDevice(offset, bytes) for metadataBuffer. When nothing is dirty both are
skipped — the skipped HOST->build barrier only ever ordered host writes, never
the application's compute-written GPU-owned transform (that compute->build
ordering is the caller's, and is unchanged).
Constraint honoured: transformOwnedByGpu transforms are still never host-copied.
The API field/method are mirrored on the WebGPU class for source portability
(the WebGPU build re-uploads its small mirror wholesale and ignores the version).
New test TLASInstanceDirtyTracking drives the real RT device and reads back the
host-mapped buffers to assert: tracked elements upload once then skip until
re-marked, untracked elements always upload, and relocation on the refit path
re-uploads exactly the moved slots — with zero validation-layer errors over the
ranged flush.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 19:58:45 +00:00
|
|
|
// 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.
|
|
|
|
|
Test tlasDirtyTest;
|
|
|
|
|
Configuration& tld = tlasDirtyTest.config;
|
|
|
|
|
tld.path = cfg.path;
|
|
|
|
|
tld.name = "TLASInstanceDirtyTracking";
|
|
|
|
|
tld.outputName = "TLASInstanceDirtyTracking";
|
|
|
|
|
tld.type = ConfigurationType::Executable;
|
|
|
|
|
tld.target = cfg.target;
|
|
|
|
|
tld.march = cfg.march;
|
|
|
|
|
tld.mtune = cfg.mtune;
|
|
|
|
|
tld.debug = cfg.debug;
|
|
|
|
|
tld.sysroot = cfg.sysroot;
|
|
|
|
|
tld.dependencies = cfg.dependencies;
|
|
|
|
|
tld.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
tld.compileFlags = cfg.compileFlags;
|
|
|
|
|
tld.linkFlags = cfg.linkFlags;
|
|
|
|
|
tld.defines = cfg.defines;
|
|
|
|
|
tld.cFiles = cfg.cFiles;
|
|
|
|
|
std::vector<fs::path> tlasDirtyImpls(impls.begin(), impls.end());
|
|
|
|
|
tlasDirtyImpls.emplace_back("tests/TLASInstanceDirtyTracking/main");
|
|
|
|
|
tld.GetInterfacesAndImplementations(ifaces, tlasDirtyImpls);
|
|
|
|
|
cfg.tests.push_back(std::move(tlasDirtyTest));
|
|
|
|
|
|
2026-06-16 15:40:37 +00:00
|
|
|
// 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).
|
|
|
|
|
Test atlasTest;
|
|
|
|
|
Configuration& ac = atlasTest.config;
|
|
|
|
|
ac.path = cfg.path;
|
|
|
|
|
ac.name = "FontAtlasDirtyRect";
|
|
|
|
|
ac.outputName = "FontAtlasDirtyRect";
|
|
|
|
|
ac.type = ConfigurationType::Executable;
|
|
|
|
|
ac.target = cfg.target;
|
|
|
|
|
ac.march = cfg.march;
|
|
|
|
|
ac.mtune = cfg.mtune;
|
|
|
|
|
ac.debug = cfg.debug;
|
|
|
|
|
ac.sysroot = cfg.sysroot;
|
|
|
|
|
ac.dependencies = cfg.dependencies;
|
|
|
|
|
ac.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
ac.compileFlags = cfg.compileFlags;
|
|
|
|
|
ac.linkFlags = cfg.linkFlags;
|
|
|
|
|
ac.defines = cfg.defines;
|
|
|
|
|
ac.cFiles = cfg.cFiles;
|
|
|
|
|
std::vector<fs::path> atlasImpls(impls.begin(), impls.end());
|
|
|
|
|
atlasImpls.emplace_back("tests/FontAtlasDirtyRect/main");
|
|
|
|
|
ac.GetInterfacesAndImplementations(ifaces, atlasImpls);
|
|
|
|
|
cfg.tests.push_back(std::move(atlasTest));
|
2026-06-16 15:43:11 +00:00
|
|
|
|
perf(text): cache shaped runs in ShapeText (#52)
ShapeText re-decoded UTF-8, re-looked-up every glyph, and recomputed
layout every frame for static strings (DrawText / EmitCenteredLabel run
inside per-frame onBuild). DrawText and EmitCenteredLabel also did a
second full pass over the glyphs to apply alignment.
Memoize the shaped run, keyed by (Font*, pxSize, color, utf8), as an
origin-relative vector<GlyphItem>. A cache hit skips decode/lookup/layout
and just translates the cached run into the output buffer by
(x + alignShift, baselineY). The alignment shift (Center = -advance/2,
Right = -advance) is folded into ShapeText's single pass, dropping the
second loop in both callers. TextAlign moves to the :UI partition so
ShapeText can take it.
Pure CPU memoization — the glyph buffer is fully rewritten and re-flushed
every frame, so a hit is byte-equivalent. A miss still calls Ensure so new
glyphs rasterise and dirty the atlas; hits don't (atlas entries are never
evicted). InvalidateFont drops a font's runs before it is destroyed (the
cache, like FontAtlas's glyph cache, keys on the Font pointer). A soft cap
bounds memory against pathological per-frame-unique text.
Adds the ShapeTextCache test (15 checks): hit == miss byte-for-byte, hits
don't touch the atlas, translate/alignment/truncation/InvalidateFont.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 15:40:52 +00:00
|
|
|
// 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;
|
|
|
|
|
Configuration& xc = textTest.config;
|
|
|
|
|
xc.path = cfg.path;
|
|
|
|
|
xc.name = "ShapeTextCache";
|
|
|
|
|
xc.outputName = "ShapeTextCache";
|
|
|
|
|
xc.type = ConfigurationType::Executable;
|
|
|
|
|
xc.target = cfg.target;
|
|
|
|
|
xc.march = cfg.march;
|
|
|
|
|
xc.mtune = cfg.mtune;
|
|
|
|
|
xc.debug = cfg.debug;
|
|
|
|
|
xc.sysroot = cfg.sysroot;
|
|
|
|
|
xc.dependencies = cfg.dependencies;
|
|
|
|
|
xc.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
xc.compileFlags = cfg.compileFlags;
|
|
|
|
|
xc.linkFlags = cfg.linkFlags;
|
|
|
|
|
xc.defines = cfg.defines;
|
|
|
|
|
xc.cFiles = cfg.cFiles;
|
|
|
|
|
xc.files = { fs::path("tests/ShapeTextCache/font.ttf") };
|
|
|
|
|
std::vector<fs::path> textImpls(impls.begin(), impls.end());
|
|
|
|
|
textImpls.emplace_back("tests/ShapeTextCache/main");
|
|
|
|
|
xc.GetInterfacesAndImplementations(ifaces, textImpls);
|
|
|
|
|
cfg.tests.push_back(std::move(textTest));
|
2026-06-16 16:03:50 +00:00
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
Test memTest;
|
|
|
|
|
Configuration& mc = memTest.config;
|
|
|
|
|
mc.path = cfg.path;
|
|
|
|
|
mc.name = "MemoryTypeFallback";
|
|
|
|
|
mc.outputName = "MemoryTypeFallback";
|
|
|
|
|
mc.type = ConfigurationType::Executable;
|
|
|
|
|
mc.target = cfg.target;
|
|
|
|
|
mc.march = cfg.march;
|
|
|
|
|
mc.mtune = cfg.mtune;
|
|
|
|
|
mc.debug = cfg.debug;
|
|
|
|
|
mc.sysroot = cfg.sysroot;
|
|
|
|
|
mc.dependencies = cfg.dependencies;
|
|
|
|
|
mc.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
mc.compileFlags = cfg.compileFlags;
|
|
|
|
|
mc.linkFlags = cfg.linkFlags;
|
|
|
|
|
mc.defines = cfg.defines;
|
|
|
|
|
mc.cFiles = cfg.cFiles;
|
|
|
|
|
std::vector<fs::path> memImpls(impls.begin(), impls.end());
|
|
|
|
|
memImpls.emplace_back("tests/MemoryTypeFallback/main");
|
|
|
|
|
mc.GetInterfacesAndImplementations(ifaces, memImpls);
|
|
|
|
|
cfg.tests.push_back(std::move(memTest));
|
2026-06-16 17:03:24 +00:00
|
|
|
|
2026-06-16 18:30:44 +00:00
|
|
|
// 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.
|
|
|
|
|
Test mipBarrierTest;
|
|
|
|
|
Configuration& mbc = mipBarrierTest.config;
|
|
|
|
|
mbc.path = cfg.path;
|
|
|
|
|
mbc.name = "MipChainBarrierBatch";
|
|
|
|
|
mbc.outputName = "MipChainBarrierBatch";
|
|
|
|
|
mbc.type = ConfigurationType::Executable;
|
|
|
|
|
mbc.target = cfg.target;
|
|
|
|
|
mbc.march = cfg.march;
|
|
|
|
|
mbc.mtune = cfg.mtune;
|
|
|
|
|
mbc.debug = cfg.debug;
|
|
|
|
|
mbc.sysroot = cfg.sysroot;
|
|
|
|
|
mbc.dependencies = cfg.dependencies;
|
|
|
|
|
mbc.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
mbc.compileFlags = cfg.compileFlags;
|
|
|
|
|
mbc.linkFlags = cfg.linkFlags;
|
|
|
|
|
mbc.defines = cfg.defines;
|
|
|
|
|
mbc.cFiles = cfg.cFiles;
|
|
|
|
|
std::vector<fs::path> mipBarrierImpls(impls.begin(), impls.end());
|
|
|
|
|
mipBarrierImpls.emplace_back("tests/MipChainBarrierBatch/main");
|
|
|
|
|
mbc.GetInterfacesAndImplementations(ifaces, mipBarrierImpls);
|
|
|
|
|
cfg.tests.push_back(std::move(mipBarrierTest));
|
|
|
|
|
|
2026-06-17 19:44:33 +00:00
|
|
|
// 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.
|
|
|
|
|
Test swapBarrierTest;
|
|
|
|
|
Configuration& sbc = swapBarrierTest.config;
|
|
|
|
|
sbc.path = cfg.path;
|
|
|
|
|
sbc.name = "SwapchainBarrierScope";
|
|
|
|
|
sbc.outputName = "SwapchainBarrierScope";
|
|
|
|
|
sbc.type = ConfigurationType::Executable;
|
|
|
|
|
sbc.target = cfg.target;
|
|
|
|
|
sbc.march = cfg.march;
|
|
|
|
|
sbc.mtune = cfg.mtune;
|
|
|
|
|
sbc.debug = cfg.debug;
|
|
|
|
|
sbc.sysroot = cfg.sysroot;
|
|
|
|
|
sbc.dependencies = cfg.dependencies;
|
|
|
|
|
sbc.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
sbc.compileFlags = cfg.compileFlags;
|
|
|
|
|
sbc.linkFlags = cfg.linkFlags;
|
|
|
|
|
sbc.defines = cfg.defines;
|
|
|
|
|
sbc.cFiles = cfg.cFiles;
|
|
|
|
|
std::vector<fs::path> swapBarrierImpls(impls.begin(), impls.end());
|
|
|
|
|
swapBarrierImpls.emplace_back("tests/SwapchainBarrierScope/main");
|
|
|
|
|
sbc.GetInterfacesAndImplementations(ifaces, swapBarrierImpls);
|
|
|
|
|
cfg.tests.push_back(std::move(swapBarrierTest));
|
|
|
|
|
|
2026-06-16 17:03:24 +00:00
|
|
|
// 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;
|
2026-06-16 17:10:39 +00:00
|
|
|
Configuration& ftc = fusedTest.config;
|
|
|
|
|
ftc.path = cfg.path;
|
|
|
|
|
ftc.name = "UIFusedShader";
|
|
|
|
|
ftc.outputName = "UIFusedShader";
|
|
|
|
|
ftc.type = ConfigurationType::Executable;
|
|
|
|
|
ftc.target = cfg.target;
|
|
|
|
|
ftc.march = cfg.march;
|
|
|
|
|
ftc.mtune = cfg.mtune;
|
|
|
|
|
ftc.debug = cfg.debug;
|
|
|
|
|
ftc.sysroot = cfg.sysroot;
|
|
|
|
|
ftc.dependencies = cfg.dependencies;
|
|
|
|
|
ftc.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
ftc.compileFlags = cfg.compileFlags;
|
|
|
|
|
ftc.linkFlags = cfg.linkFlags;
|
|
|
|
|
ftc.defines = cfg.defines;
|
|
|
|
|
ftc.cFiles = cfg.cFiles;
|
|
|
|
|
std::vector<fs::path> fusedImpls(impls.begin(), impls.end());
|
|
|
|
|
fusedImpls.emplace_back("tests/UIFusedShader/main");
|
|
|
|
|
ftc.GetInterfacesAndImplementations(ifaces, fusedImpls);
|
|
|
|
|
fusedTest.requires_ = { "tool:glslang", "tool:spirv-val" };
|
|
|
|
|
cfg.tests.push_back(std::move(fusedTest));
|
|
|
|
|
|
2026-06-16 17:04:26 +00:00
|
|
|
// 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.
|
|
|
|
|
Test flushTest;
|
|
|
|
|
Configuration& fc = flushTest.config;
|
2026-06-16 17:03:24 +00:00
|
|
|
fc.path = cfg.path;
|
2026-06-16 17:04:26 +00:00
|
|
|
fc.name = "VulkanBufferFlushGate";
|
|
|
|
|
fc.outputName = "VulkanBufferFlushGate";
|
2026-06-16 17:03:24 +00:00
|
|
|
fc.type = ConfigurationType::Executable;
|
|
|
|
|
fc.target = cfg.target;
|
|
|
|
|
fc.march = cfg.march;
|
|
|
|
|
fc.mtune = cfg.mtune;
|
|
|
|
|
fc.debug = cfg.debug;
|
|
|
|
|
fc.sysroot = cfg.sysroot;
|
|
|
|
|
fc.dependencies = cfg.dependencies;
|
|
|
|
|
fc.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
fc.compileFlags = cfg.compileFlags;
|
|
|
|
|
fc.linkFlags = cfg.linkFlags;
|
|
|
|
|
fc.defines = cfg.defines;
|
|
|
|
|
fc.cFiles = cfg.cFiles;
|
2026-06-16 17:04:26 +00:00
|
|
|
std::vector<fs::path> flushImpls(impls.begin(), impls.end());
|
|
|
|
|
flushImpls.emplace_back("tests/VulkanBufferFlushGate/main");
|
|
|
|
|
fc.GetInterfacesAndImplementations(ifaces, flushImpls);
|
|
|
|
|
cfg.tests.push_back(std::move(flushTest));
|
2026-06-16 17:09:06 +00:00
|
|
|
|
2026-06-16 18:29:24 +00:00
|
|
|
// 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.
|
|
|
|
|
Test rangedFlushTest;
|
|
|
|
|
Configuration& rfc = rangedFlushTest.config;
|
|
|
|
|
rfc.path = cfg.path;
|
|
|
|
|
rfc.name = "VulkanBufferRangedFlush";
|
|
|
|
|
rfc.outputName = "VulkanBufferRangedFlush";
|
|
|
|
|
rfc.type = ConfigurationType::Executable;
|
|
|
|
|
rfc.target = cfg.target;
|
|
|
|
|
rfc.march = cfg.march;
|
|
|
|
|
rfc.mtune = cfg.mtune;
|
|
|
|
|
rfc.debug = cfg.debug;
|
|
|
|
|
rfc.sysroot = cfg.sysroot;
|
|
|
|
|
rfc.dependencies = cfg.dependencies;
|
|
|
|
|
rfc.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
rfc.compileFlags = cfg.compileFlags;
|
|
|
|
|
rfc.linkFlags = cfg.linkFlags;
|
|
|
|
|
rfc.defines = cfg.defines;
|
|
|
|
|
rfc.cFiles = cfg.cFiles;
|
|
|
|
|
std::vector<fs::path> rangedFlushImpls(impls.begin(), impls.end());
|
|
|
|
|
rangedFlushImpls.emplace_back("tests/VulkanBufferRangedFlush/main");
|
|
|
|
|
rfc.GetInterfacesAndImplementations(ifaces, rangedFlushImpls);
|
|
|
|
|
cfg.tests.push_back(std::move(rangedFlushTest));
|
|
|
|
|
|
2026-06-16 18:22:09 +00:00
|
|
|
// 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.
|
|
|
|
|
Test resizeTest;
|
|
|
|
|
Configuration& rc = resizeTest.config;
|
|
|
|
|
rc.path = cfg.path;
|
|
|
|
|
rc.name = "VulkanBufferResizeReuse";
|
|
|
|
|
rc.outputName = "VulkanBufferResizeReuse";
|
|
|
|
|
rc.type = ConfigurationType::Executable;
|
|
|
|
|
rc.target = cfg.target;
|
|
|
|
|
rc.march = cfg.march;
|
|
|
|
|
rc.mtune = cfg.mtune;
|
|
|
|
|
rc.debug = cfg.debug;
|
|
|
|
|
rc.sysroot = cfg.sysroot;
|
|
|
|
|
rc.dependencies = cfg.dependencies;
|
|
|
|
|
rc.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
rc.compileFlags = cfg.compileFlags;
|
|
|
|
|
rc.linkFlags = cfg.linkFlags;
|
|
|
|
|
rc.defines = cfg.defines;
|
|
|
|
|
rc.cFiles = cfg.cFiles;
|
|
|
|
|
std::vector<fs::path> resizeImpls(impls.begin(), impls.end());
|
|
|
|
|
resizeImpls.emplace_back("tests/VulkanBufferResizeReuse/main");
|
|
|
|
|
rc.GetInterfacesAndImplementations(ifaces, resizeImpls);
|
|
|
|
|
cfg.tests.push_back(std::move(resizeTest));
|
|
|
|
|
|
fix(device): fence-keyed deferred resource-deletion queue (#101)
Since #40 dropped the per-frame vkQueueWaitIdle, frames are pipelined:
a resource the CPU is done with may still be read by the GPU for up to
numFrames-1 more frames. VulkanBuffer::Resize's destroy-and-recreate
path was therefore a live use-after-free (#63), no longer masked by the
wait-idle.
Device gains a monotonic, frame-counter-keyed deletion queue:
EnqueueDeletion tags {buffer, memory} with the current frameCounter;
ReclaimDeletions (called per frame after the fence wait) frees entries
once framesInFlight frames have elapsed; DrainDeletions frees everything
after a wait-idle. VulkanBuffer::DeferredClear hands handles to the queue
and nulls the handle, and Resize uses it instead of immediate Clear().
Window::Render sets framesInFlight at init, reclaims after the per-image
fence wait, bumps Device::frameCounter once per frame, and drains on the
resize / OUT_OF_DATE wait-idle paths. The destructor keeps immediate
Clear() (callers destroying mid-flight remain responsible, unchanged).
Adds the DeferredDeletion test: drives the retire timing on a real
headless device with real buffers, stepping Device::frameCounter to pin
the exact reclaim frame, asserting validation stays silent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 13:21:12 +00:00
|
|
|
// 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.
|
|
|
|
|
Test deferredTest;
|
|
|
|
|
Configuration& dc = deferredTest.config;
|
|
|
|
|
dc.path = cfg.path;
|
|
|
|
|
dc.name = "DeferredDeletion";
|
|
|
|
|
dc.outputName = "DeferredDeletion";
|
|
|
|
|
dc.type = ConfigurationType::Executable;
|
|
|
|
|
dc.target = cfg.target;
|
|
|
|
|
dc.march = cfg.march;
|
|
|
|
|
dc.mtune = cfg.mtune;
|
|
|
|
|
dc.debug = cfg.debug;
|
|
|
|
|
dc.sysroot = cfg.sysroot;
|
|
|
|
|
dc.dependencies = cfg.dependencies;
|
|
|
|
|
dc.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
dc.compileFlags = cfg.compileFlags;
|
|
|
|
|
dc.linkFlags = cfg.linkFlags;
|
|
|
|
|
dc.defines = cfg.defines;
|
|
|
|
|
dc.cFiles = cfg.cFiles;
|
|
|
|
|
std::vector<fs::path> deferredImpls(impls.begin(), impls.end());
|
|
|
|
|
deferredImpls.emplace_back("tests/DeferredDeletion/main");
|
|
|
|
|
dc.GetInterfacesAndImplementations(ifaces, deferredImpls);
|
|
|
|
|
cfg.tests.push_back(std::move(deferredTest));
|
|
|
|
|
|
2026-06-17 17:40:39 +00:00
|
|
|
// 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).
|
|
|
|
|
Test meshStagingTest;
|
|
|
|
|
Configuration& msc = meshStagingTest.config;
|
|
|
|
|
msc.path = cfg.path;
|
|
|
|
|
msc.name = "MeshDecompressStagingRelease";
|
|
|
|
|
msc.outputName = "MeshDecompressStagingRelease";
|
|
|
|
|
msc.type = ConfigurationType::Executable;
|
|
|
|
|
msc.target = cfg.target;
|
|
|
|
|
msc.march = cfg.march;
|
|
|
|
|
msc.mtune = cfg.mtune;
|
|
|
|
|
msc.debug = cfg.debug;
|
|
|
|
|
msc.sysroot = cfg.sysroot;
|
|
|
|
|
msc.dependencies = cfg.dependencies;
|
|
|
|
|
msc.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
msc.compileFlags = cfg.compileFlags;
|
|
|
|
|
msc.linkFlags = cfg.linkFlags;
|
|
|
|
|
msc.defines = cfg.defines;
|
|
|
|
|
msc.cFiles = cfg.cFiles;
|
|
|
|
|
std::vector<fs::path> meshStagingImpls(impls.begin(), impls.end());
|
|
|
|
|
meshStagingImpls.emplace_back("tests/MeshDecompressStagingRelease/main");
|
|
|
|
|
msc.GetInterfacesAndImplementations(ifaces, meshStagingImpls);
|
|
|
|
|
cfg.tests.push_back(std::move(meshStagingTest));
|
|
|
|
|
|
2026-06-17 19:40:35 +00:00
|
|
|
// 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.
|
|
|
|
|
Test imageStagingTest;
|
|
|
|
|
Configuration& isc = imageStagingTest.config;
|
|
|
|
|
isc.path = cfg.path;
|
|
|
|
|
isc.name = "ImageStagingRelease";
|
|
|
|
|
isc.outputName = "ImageStagingRelease";
|
|
|
|
|
isc.type = ConfigurationType::Executable;
|
|
|
|
|
isc.target = cfg.target;
|
|
|
|
|
isc.march = cfg.march;
|
|
|
|
|
isc.mtune = cfg.mtune;
|
|
|
|
|
isc.debug = cfg.debug;
|
|
|
|
|
isc.sysroot = cfg.sysroot;
|
|
|
|
|
isc.dependencies = cfg.dependencies;
|
|
|
|
|
isc.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
isc.compileFlags = cfg.compileFlags;
|
|
|
|
|
isc.linkFlags = cfg.linkFlags;
|
|
|
|
|
isc.defines = cfg.defines;
|
|
|
|
|
isc.cFiles = cfg.cFiles;
|
|
|
|
|
std::vector<fs::path> imageStagingImpls(impls.begin(), impls.end());
|
|
|
|
|
imageStagingImpls.emplace_back("tests/ImageStagingRelease/main");
|
|
|
|
|
isc.GetInterfacesAndImplementations(ifaces, imageStagingImpls);
|
|
|
|
|
cfg.tests.push_back(std::move(imageStagingTest));
|
|
|
|
|
|
2026-06-16 17:23:27 +00:00
|
|
|
// 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.
|
|
|
|
|
Test uploadTest;
|
|
|
|
|
Configuration& uc = uploadTest.config;
|
|
|
|
|
uc.path = cfg.path;
|
|
|
|
|
uc.name = "UploadStrategy";
|
|
|
|
|
uc.outputName = "UploadStrategy";
|
|
|
|
|
uc.type = ConfigurationType::Executable;
|
|
|
|
|
uc.target = cfg.target;
|
|
|
|
|
uc.march = cfg.march;
|
|
|
|
|
uc.mtune = cfg.mtune;
|
|
|
|
|
uc.debug = cfg.debug;
|
|
|
|
|
uc.sysroot = cfg.sysroot;
|
|
|
|
|
uc.dependencies = cfg.dependencies;
|
|
|
|
|
uc.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
uc.compileFlags = cfg.compileFlags;
|
|
|
|
|
uc.linkFlags = cfg.linkFlags;
|
|
|
|
|
uc.defines = cfg.defines;
|
|
|
|
|
uc.cFiles = cfg.cFiles;
|
|
|
|
|
std::vector<fs::path> uploadImpls(impls.begin(), impls.end());
|
|
|
|
|
uploadImpls.emplace_back("tests/UploadStrategy/main");
|
|
|
|
|
uc.GetInterfacesAndImplementations(ifaces, uploadImpls);
|
|
|
|
|
cfg.tests.push_back(std::move(uploadTest));
|
|
|
|
|
|
2026-06-16 17:00:48 +00:00
|
|
|
// 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;
|
|
|
|
|
Configuration& vc = advTest.config;
|
|
|
|
|
vc.path = cfg.path;
|
|
|
|
|
vc.name = "FontAdvanceCache";
|
|
|
|
|
vc.outputName = "FontAdvanceCache";
|
|
|
|
|
vc.type = ConfigurationType::Executable;
|
|
|
|
|
vc.target = cfg.target;
|
|
|
|
|
vc.march = cfg.march;
|
|
|
|
|
vc.mtune = cfg.mtune;
|
|
|
|
|
vc.debug = cfg.debug;
|
|
|
|
|
vc.sysroot = cfg.sysroot;
|
|
|
|
|
vc.dependencies = cfg.dependencies;
|
|
|
|
|
vc.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
vc.compileFlags = cfg.compileFlags;
|
|
|
|
|
vc.linkFlags = cfg.linkFlags;
|
|
|
|
|
vc.defines = cfg.defines;
|
|
|
|
|
vc.cFiles = cfg.cFiles;
|
|
|
|
|
vc.files = { fs::path("tests/FontAdvanceCache/font.ttf") };
|
|
|
|
|
std::vector<fs::path> advImpls(impls.begin(), impls.end());
|
|
|
|
|
advImpls.emplace_back("tests/FontAdvanceCache/main");
|
|
|
|
|
vc.GetInterfacesAndImplementations(ifaces, advImpls);
|
|
|
|
|
cfg.tests.push_back(std::move(advTest));
|
2026-06-16 17:02:48 +00:00
|
|
|
|
2026-06-16 16:57:07 +00:00
|
|
|
// 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.
|
|
|
|
|
Test clipTest;
|
|
|
|
|
Configuration& clc = clipTest.config;
|
|
|
|
|
clc.path = cfg.path;
|
|
|
|
|
clc.name = "UIClipFlag";
|
|
|
|
|
clc.outputName = "UIClipFlag";
|
|
|
|
|
clc.type = ConfigurationType::Executable;
|
|
|
|
|
clc.target = cfg.target;
|
|
|
|
|
clc.march = cfg.march;
|
|
|
|
|
clc.mtune = cfg.mtune;
|
|
|
|
|
clc.debug = cfg.debug;
|
|
|
|
|
clc.sysroot = cfg.sysroot;
|
|
|
|
|
clc.dependencies = cfg.dependencies;
|
|
|
|
|
clc.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
clc.compileFlags = cfg.compileFlags;
|
|
|
|
|
clc.linkFlags = cfg.linkFlags;
|
|
|
|
|
clc.defines = cfg.defines;
|
|
|
|
|
clc.cFiles = cfg.cFiles;
|
|
|
|
|
std::vector<fs::path> clipImpls(impls.begin(), impls.end());
|
|
|
|
|
clipImpls.emplace_back("tests/UIClipFlag/main");
|
|
|
|
|
clc.GetInterfacesAndImplementations(ifaces, clipImpls);
|
|
|
|
|
cfg.tests.push_back(std::move(clipTest));
|
2026-06-16 16:59:05 +00:00
|
|
|
|
2026-06-16 16:55:03 +00:00
|
|
|
// 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;
|
|
|
|
|
Configuration& hc = hitTest.config;
|
|
|
|
|
hc.path = cfg.path;
|
|
|
|
|
hc.name = "InputFieldHitTest";
|
|
|
|
|
hc.outputName = "InputFieldHitTest";
|
|
|
|
|
hc.type = ConfigurationType::Executable;
|
|
|
|
|
hc.target = cfg.target;
|
|
|
|
|
hc.march = cfg.march;
|
|
|
|
|
hc.mtune = cfg.mtune;
|
|
|
|
|
hc.debug = cfg.debug;
|
|
|
|
|
hc.sysroot = cfg.sysroot;
|
|
|
|
|
hc.dependencies = cfg.dependencies;
|
|
|
|
|
hc.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
hc.compileFlags = cfg.compileFlags;
|
|
|
|
|
hc.linkFlags = cfg.linkFlags;
|
|
|
|
|
hc.defines = cfg.defines;
|
|
|
|
|
hc.cFiles = cfg.cFiles;
|
|
|
|
|
hc.files = { fs::path("tests/InputFieldHitTest/font.ttf") };
|
|
|
|
|
std::vector<fs::path> hitImpls(impls.begin(), impls.end());
|
|
|
|
|
hitImpls.emplace_back("tests/InputFieldHitTest/main");
|
|
|
|
|
hc.GetInterfacesAndImplementations(ifaces, hitImpls);
|
|
|
|
|
cfg.tests.push_back(std::move(hitTest));
|
2026-06-16 18:23:25 +00:00
|
|
|
|
2026-06-18 13:29:55 +00:00
|
|
|
// 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;
|
|
|
|
|
Configuration& crc = caretTest.config;
|
|
|
|
|
crc.path = cfg.path;
|
|
|
|
|
crc.name = "InputFieldCaretCache";
|
|
|
|
|
crc.outputName = "InputFieldCaretCache";
|
|
|
|
|
crc.type = ConfigurationType::Executable;
|
|
|
|
|
crc.target = cfg.target;
|
|
|
|
|
crc.march = cfg.march;
|
|
|
|
|
crc.mtune = cfg.mtune;
|
|
|
|
|
crc.debug = cfg.debug;
|
|
|
|
|
crc.sysroot = cfg.sysroot;
|
|
|
|
|
crc.dependencies = cfg.dependencies;
|
|
|
|
|
crc.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
crc.compileFlags = cfg.compileFlags;
|
|
|
|
|
crc.linkFlags = cfg.linkFlags;
|
|
|
|
|
crc.defines = cfg.defines;
|
|
|
|
|
crc.cFiles = cfg.cFiles;
|
|
|
|
|
crc.files = { fs::path("tests/InputFieldCaretCache/font.ttf") };
|
|
|
|
|
std::vector<fs::path> caretImpls(impls.begin(), impls.end());
|
|
|
|
|
caretImpls.emplace_back("tests/InputFieldCaretCache/main");
|
|
|
|
|
crc.GetInterfacesAndImplementations(ifaces, caretImpls);
|
|
|
|
|
cfg.tests.push_back(std::move(caretTest));
|
|
|
|
|
|
2026-06-16 18:23:25 +00:00
|
|
|
// 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.
|
|
|
|
|
Test cacheTest;
|
|
|
|
|
Configuration& cc = cacheTest.config;
|
|
|
|
|
cc.path = cfg.path;
|
|
|
|
|
cc.name = "PipelineCacheValidation";
|
|
|
|
|
cc.outputName = "PipelineCacheValidation";
|
|
|
|
|
cc.type = ConfigurationType::Executable;
|
|
|
|
|
cc.target = cfg.target;
|
|
|
|
|
cc.march = cfg.march;
|
|
|
|
|
cc.mtune = cfg.mtune;
|
|
|
|
|
cc.debug = cfg.debug;
|
|
|
|
|
cc.sysroot = cfg.sysroot;
|
|
|
|
|
cc.dependencies = cfg.dependencies;
|
|
|
|
|
cc.externalDependencies = cfg.externalDependencies;
|
|
|
|
|
cc.compileFlags = cfg.compileFlags;
|
|
|
|
|
cc.linkFlags = cfg.linkFlags;
|
|
|
|
|
cc.defines = cfg.defines;
|
|
|
|
|
cc.cFiles = cfg.cFiles;
|
|
|
|
|
std::vector<fs::path> cacheImpls(impls.begin(), impls.end());
|
|
|
|
|
cacheImpls.emplace_back("tests/PipelineCacheValidation/main");
|
|
|
|
|
cc.GetInterfacesAndImplementations(ifaces, cacheImpls);
|
|
|
|
|
cfg.tests.push_back(std::move(cacheTest));
|
2026-05-18 02:07:48 +02:00
|
|
|
}
|
2026-05-01 23:35:37 +02:00
|
|
|
|
2026-04-30 01:29:17 +02:00
|
|
|
return cfg;
|
|
|
|
|
}
|