//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® import std; import Crafter.Build; namespace fs = std::filesystem; using namespace Crafter; extern "C" Configuration CrafterBuildProject(std::span args) { std::vector depArgs(args.begin(), args.end()); // 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"); 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 cacheImpls(impls.begin(), impls.end()); cacheImpls.emplace_back("tests/PipelineCacheValidation/main"); cc.GetInterfacesAndImplementations(ifaces, cacheImpls); cfg.tests.push_back(std::move(cacheTest)); } return cfg; }