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()); 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.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 #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 #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)); } return cfg; }