Two distinct validation errors the native frame loop emitted, both originating in Crafter.Graphics with no consumer-side influence. Problem 1 — per-frame acquire-barrier access/stage mismatch. The acquire->GENERAL barrier hardcoded dstAccessMask = SHADER_WRITE|TRANSFER_WRITE but used the per-pass stage union as its dst stage mask. For an all-compute frame the union narrows to COMPUTE_SHADER, which does not support TRANSFER_WRITE, so VUID-02820 fired every frame. Derive the access mask from the same stage union via a new SwapchainWriterAccess() helper (mirroring SwapchainStageUnion), and apply it to both the acquire dst and present src masks for symmetry. Problem 2 — mid-session StartInit/FinishInit (and GetCmd/EndCmd) reuse the shared drawCmdBuffers[currentBuffer]. With no steady-state wait-idle the loop's last submission of that buffer may still be in flight when scene setup runs (building map meshes / acceleration structures), so the old code re-began (VUID-00049) and re-submitted (VUID-00071) a pending buffer, and resources freed in the StartInit..FinishInit bracket could still be referenced by it. Drain the queue at the start of StartInit/GetCmd before re-recording; setup is rare, so a wait-idle is fine (FinishInit/EndCmd already wait-idle at the end). Tests: extend SwapchainBarrierScope with SwapchainWriterAccess coverage (pure CPU), and add SetupCmdBufferReuse — a real-frame-loop regression test driving a compute pass plus interleaved mid-session StartInit rounds, asserting the validation layer stays silent. Verified both halves fail (reproducing the exact VUIDs) when their respective fix is reverted. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
964 lines
53 KiB
C++
964 lines
53 KiB
C++
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());
|
|
|
|
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,
|
|
});
|
|
|
|
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
|
|
// and combase for HSTRING / RoGetActivationFactory.
|
|
cfg.linkFlags.push_back("-lruntimeobject");
|
|
cfg.linkFlags.push_back("-lcombase");
|
|
} 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. libevdev ships its headers
|
|
// under a versioned dir (libevdev-1.0/) so the -I is mandatory.
|
|
cfg.linkFlags.push_back("-ludev");
|
|
cfg.linkFlags.push_back("-levdev");
|
|
cfg.compileFlags.push_back("-I/usr/include/libevdev-1.0");
|
|
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");
|
|
}
|
|
|
|
// 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"));
|
|
cfg.files.emplace_back(fs::path("additional/dom-webgpu.js"));
|
|
} else {
|
|
std::array<fs::path, 14> impls = {
|
|
"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",
|
|
"implementations/Crafter.Graphics-UI-Shared",
|
|
"implementations/Crafter.Graphics-UIComponents",
|
|
"implementations/Crafter.Graphics-Window",
|
|
};
|
|
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
|
|
|
cfg.shaders.emplace_back(fs::path("shaders/ui-quads.comp.glsl"), std::string("main"), ShaderType::Compute);
|
|
cfg.shaders.emplace_back(fs::path("shaders/ui-circles.comp.glsl"), std::string("main"), ShaderType::Compute);
|
|
cfg.shaders.emplace_back(fs::path("shaders/ui-images.comp.glsl"), std::string("main"), ShaderType::Compute);
|
|
cfg.shaders.emplace_back(fs::path("shaders/ui-text.comp.glsl"), std::string("main"), ShaderType::Compute);
|
|
cfg.shaders.emplace_back(fs::path("shaders/ui-fused.comp.glsl"), std::string("main"), ShaderType::Compute);
|
|
cfg.buildFiles.emplace_back(fs::path("shaders/ui-shared.glsl"));
|
|
|
|
// 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));
|
|
|
|
// 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));
|
|
|
|
// 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));
|
|
|
|
// 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));
|
|
}
|
|
|
|
// 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));
|
|
|
|
// 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));
|
|
|
|
// 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));
|
|
|
|
// 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));
|
|
|
|
// 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));
|
|
|
|
// 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));
|
|
|
|
// 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));
|
|
|
|
// 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));
|
|
|
|
// 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;
|
|
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));
|
|
|
|
// 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;
|
|
fc.path = cfg.path;
|
|
fc.name = "VulkanBufferFlushGate";
|
|
fc.outputName = "VulkanBufferFlushGate";
|
|
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;
|
|
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));
|
|
|
|
// 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));
|
|
|
|
// 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));
|
|
|
|
// 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));
|
|
|
|
// 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));
|
|
|
|
// 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));
|
|
|
|
// 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));
|
|
|
|
// 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));
|
|
|
|
// 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));
|
|
|
|
// 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));
|
|
|
|
// 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));
|
|
|
|
// 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));
|
|
}
|
|
|
|
return cfg;
|
|
}
|