diff --git a/AGENTS.consumer.md b/AGENTS.consumer.md index 4bc6d3c..bda4aeb 100644 --- a/AGENTS.consumer.md +++ b/AGENTS.consumer.md @@ -4,8 +4,9 @@ The Crafter engine's graphics library: a C++ module, `import Crafter.Graphics;`, ### Building against it +- The library's `project.cpp` needs crafter-build 3.0.0 or later; older ones fail to compile it, for every target. - The target picks the backend, in the library and in your code. Any `wasm32-*` target builds the DOM + WebGPU library; everything else builds Vulkan, with Wayland on Linux (there is no X11 backend) or Win32 on `x86_64-w64-mingw32`. The library defines `CRAFTER_GRAPHICS_WINDOW_WAYLAND`, `_WIN32` or `_DOM` for its own units only, so a wasm consumer sets `cfg.target = "wasm32-wasip1"`, `cfg.defines.push_back({"CRAFTER_GRAPHICS_WINDOW_DOM", ""})`, `cfg.compileFlags.push_back("-msimd128")` (the dependency PCMs were compiled with it) and `EnableWasiBrowserRuntime(cfg)`, as `examples/HelloUI/project.cpp` does. Portable code `#ifdef CRAFTER_GRAPHICS_WINDOW_DOM`s at the few sites where the backends differ; the library has no shared signatures that are no-ops on one side. -- The library's link flags (the Vulkan loader, `wayland-client`, `xkbcommon`, `udev`, or the Win32 libraries) come with the dependency. Its shaders (`ui-*.comp.spv` and `ui-*.comp.sets.spv`) and, for wasm, `dom-env.js` and `dom-webgpu.js` are copied into your executable's bin directory, as are your `cfg.shaders` outputs (`.spv` from `.glsl`) and `cfg.files`. Paths are resolved against the working directory at runtime, so run from the bin directory, as `crafter-build -r` does. +- The library's link flags (the Vulkan loader, `wayland-client`, `xkbcommon`, `udev`, or the Win32 libraries) come with the dependency. On `x86_64-w64-mingw32` that includes an import library for `vulkan-1.dll`, so linking for Win32 needs no Vulkan SDK. The library makes it at configure time with the `llvm-dlltool` beside crafter-build's `llvm-ar`, from the loader's own export list (Vulkan-Loader's `loader/vulkan-1.def`, fetched unpinned). Under the Flatpak a Win32 executable still fails to link on `__libc_single_threaded` (Crafter.Build #43). Until that is fixed, add a C file defining `char __libc_single_threaded = 0;` to the executable, as the library's `Win32Link` test does. Its shaders (`ui-*.comp.spv` and `ui-*.comp.sets.spv`) and, for wasm, `dom-env.js` and `dom-webgpu.js` are copied into your executable's bin directory, as are your `cfg.shaders` outputs (`.spv` from `.glsl`) and `cfg.files`. Paths are resolved against the working directory at runtime, so run from the bin directory, as `crafter-build -r` does. - Your own shaders: `cfg.shaders.emplace_back(fs::path("raygen.glsl"), std::string("main"), ShaderType::RayGen)`, with `Compute`, `RayGen`, `Miss`, `ClosestHit`, `AnyHit`, `Intersect` and `Callable`. A compute shader for the UI heap is registered twice, see "Custom compute shaders". WGSL for the DOM build ships through `cfg.files` and is loaded at runtime. - Project args the library understands, passed through your `args`: `--local` (sibling checkouts of Crafter.Event, Crafter.Math and Crafter.Asset instead of clones), `--timing` (meant to add frame timing counters; the build does not compile with it at the moment, issue #178), `--no-webgpu` (wasm: a DOM-only page without the WebGPU bridge, which otherwise requests a GPU adapter on every load and replaces the page body with an error where WebGPU is missing). - Needs at runtime: Vulkan 1.3 (1.4 is used where the GPU has it), a presentable surface, and the features the library's own shaders use. `VK_EXT_descriptor_heap` and ray tracing are optional groups, see "Device and features". Mesa's llvmpipe runs the compute/UI path, and recent Mesa advertises the heap. diff --git a/AGENTS.md b/AGENTS.md index 999b17f..cbbc6e9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,16 +29,16 @@ Partitions, by what they are for. Every partition file exists on every target; o ## Dev environment -crafter-build is the only build tool: the Flatpak (`alias crafter-build='flatpak run net.catcrafts.CrafterBuild'`; a locally built one runs with `--branch=local`) or a native build of the sibling checkout. `crafter-build --version` prints the compiler it resolved. Loading `project.cpp` clones Crafter.Event, Crafter.Math and Crafter.Asset into the crafter-build cache and, since they are unpinned, `git pull`s them on every invocation, and clones the two Khronos header repositories, so a build needs the network. `--local` takes the sibling checkouts (`../Crafter.Event`, `../Crafter.Math`, `../Crafter.Asset`) instead and hands the flag on, so Crafter.Asset picks the sibling Crafter.Math too; two copies of one module on the path do not link. +crafter-build, 3.0.0 or later (`project.cpp` uses its `ShellQuote`), is the only build tool: the Flatpak (`alias crafter-build='flatpak run net.catcrafts.CrafterBuild'`; a locally built one runs with `--branch=local`) or a native build of the sibling checkout. `crafter-build --version` prints the compiler it resolved. Loading `project.cpp` clones Crafter.Event, Crafter.Math and Crafter.Asset into the crafter-build cache and, since they are unpinned, `git pull`s them on every invocation, and clones the two Khronos header repositories, and for a Win32 target Vulkan-Loader as well, so a build needs the network. `--local` takes the sibling checkouts (`../Crafter.Event`, `../Crafter.Math`, `../Crafter.Asset`) instead and hands the flag on, so Crafter.Asset picks the sibling Crafter.Math too; two copies of one module on the path do not link. -The host is Linux on Wayland. The native Linux backend is Wayland only (no X11), so every test that calls `Device::Initialize` needs `WAYLAND_DISPLAY`: it connects to the display right after creating the Vulkan instance, window or no window. The build links `wayland-client`, `xkbcommon`, `udev` and the Vulkan loader from the host or the sysroot; libevdev is vendored and built as its own static library, `Crafter.Graphics-libevdev`, once per build. A Vulkan 1.3 driver is the floor; `VK_EXT_descriptor_heap` (with `shaderUntypedPointers`), ray tracing and `swapchain_maintenance1` (with the instance's `surface_maintenance1`, for per-present-mode image counts) are optional groups detected at start-up. Recent Mesa's llvmpipe advertises the heap, so a machine without a GPU runs the UI path. The Khronos validation layer is optional and off by default. +The host is Linux on Wayland. The native Linux backend is Wayland only (no X11), so every test that calls `Device::Initialize` needs `WAYLAND_DISPLAY`: it connects to the display right after creating the Vulkan instance, window or no window. The build links `wayland-client`, `xkbcommon`, `udev` and the Vulkan loader from the host or the sysroot; for a Win32 target it links the loader through an import library that `project.cpp` makes with `llvm-dlltool` from Vulkan-Loader's `loader/vulkan-1.def`, into `build/vulkan-1-/`, since the MinGW toolchains ship none. libevdev is vendored and built as its own static library, `Crafter.Graphics-libevdev`, once per build. A Vulkan 1.3 driver is the floor; `VK_EXT_descriptor_heap` (with `shaderUntypedPointers`), ray tracing and `swapchain_maintenance1` (with the instance's `surface_maintenance1`, for per-present-mode image counts) are optional groups detected at start-up. Recent Mesa's llvmpipe advertises the heap, so a machine without a GPU runs the UI path. The Khronos validation layer is optional and off by default. Targets, picked with `--target=` and passed down to the dependencies: | Target | Backend | Notes | |---|---|---| | native (`x86_64-unknown-linux-gnu`) | Wayland + Vulkan | the development loop | -| `x86_64-w64-mingw32` | Win32 + Vulkan | cross-compiled; links `vulkan-1`, `kernel32`, `user32`, `gdi32`, `runtimeobject`; gamepads through Windows.Gaming.Input | +| `x86_64-w64-mingw32` | Win32 + Vulkan | cross-compiled; links `vulkan-1` (an import library made from Vulkan-Loader's `loader/vulkan-1.def`), `kernel32`, `user32`, `gdi32`, `runtimeobject`, `uuid`; gamepads through Windows.Gaming.Input | | `wasm32-wasip1` | DOM + WebGPU | no Vulkan loader, no Wayland; `dom-env.js` and `dom-webgpu.js` ship beside the `.wasm`; `--no-webgpu` drops the WebGPU bridge for DOM-only apps | | `aarch64-alpine-linux-musl` with `--sysroot=`, `--march=armv8-a --mtune=generic` | Wayland + Vulkan | the Mesa Turnip phone target: no descriptor heap, no ray tracing, the descriptor-set path | @@ -53,6 +53,7 @@ crafter-build # the native library and its crafter-build --local # against ../Crafter.Event, ../Crafter.Math, ../Crafter.Asset crafter-build --target=wasm32-wasip1 # the DOM/WebGPU library crafter-build --target=x86_64-w64-mingw32 # the Win32 library +crafter-build test --target=x86_64-w64-mingw32 --runner=cmd:true Win32Link # links a Win32 executable, without Wine crafter-build test # build and run every test for the host crafter-build test 'TLAS*' # name glob crafter-build test --list @@ -62,11 +63,11 @@ cd examples/HelloUI && crafter-build -r # build an example against t crafter-build clean ``` -`project.cpp` registers no lint rules, so `crafter-build lint` and `format` have nothing to do here; style is kept by hand (see "Code style"). There is no CI. Before a change is done: `crafter-build test` passes on the host with `CRAFTER_GRAPHICS_VALIDATION` set; the examples whose API you touched build (`cd examples/ && crafter-build`), since they are the in-tree consumers; `crafter-build --target=wasm32-wasip1` still compiles when you touched a partition with a DOM half or a backend-agnostic one; and `crafter-build --target=x86_64-w64-mingw32` still compiles when you touched `Window`, `Device`, `Gamepad` or `Clipboard`. +`project.cpp` registers no lint rules, so `crafter-build lint` and `format` have nothing to do here; style is kept by hand (see "Code style"). There is no CI. Before a change is done: `crafter-build test` passes on the host with `CRAFTER_GRAPHICS_VALIDATION` set; the examples whose API you touched build (`cd examples/ && crafter-build`), since they are the in-tree consumers; `crafter-build --target=wasm32-wasip1` still compiles when you touched a partition with a DOM half or a backend-agnostic one; and `crafter-build test --target=x86_64-w64-mingw32 --runner=cmd:true Win32Link` still links when you touched `Window`, `Device`, `Gamepad`, `Clipboard` or the link flags in `project.cpp`. ## Testing instructions -Tests are declared in `project.cpp`, not discovered, through the `LibraryTest(name)` lambda: it copies the library's settings field by field (`Configuration` is not copyable) and compiles the library's sources plus `tests//main.cpp` into one executable, so a test reaches non-exported internals the way the library does. A new test is a directory, a `cfg.tests.push_back(LibraryTest("Name"));` and a comment above it saying what it covers and what it needs (a GPU, ray tracing, a compositor, a tool). A test that needs shaders adds them to `test.config.shaders` with the same `ShaderType` lines the examples use; a font or asset goes in `test.config.files`; a tool it runs goes in `requires_` (`{"tool:glslang", "tool:spirv-val"}`), which turns the test into a skip when the tool is missing. The declarations sit in the native branch of `project.cpp`: wasm runs no tests. Wayland-only tests sit in `if (!windows)`; the Win32 tests, `PointerWin32` and `TextWin32`, sit in `if (windows)`, with `requires_ = {"tool:wine"}` when built on Linux. +Tests are declared in `project.cpp`, not discovered, through the `LibraryTest(name)` lambda: it copies the library's settings field by field (`Configuration` is not copyable) and compiles the library's sources plus `tests//main.cpp` into one executable, so a test reaches non-exported internals the way the library does. A new test is a directory, a `cfg.tests.push_back(LibraryTest("Name"));` and a comment above it saying what it covers and what it needs (a GPU, ray tracing, a compositor, a tool). A test that needs shaders adds them to `test.config.shaders` with the same `ShaderType` lines the examples use; a font or asset goes in `test.config.files`; a tool it runs goes in `requires_` (`{"tool:glslang", "tool:spirv-val"}`), which turns the test into a skip when the tool is missing. The declarations sit in the native branch of `project.cpp`: wasm runs no tests. Wayland-only tests sit in `if (!windows)`; the Win32 tests, `PointerWin32` and `TextWin32`, sit in `if (windows)`, with `requires_ = {"tool:wine"}` when built on Linux. `Win32Link` beside them has no `requires_`, since its link is the test: where there is no Wine it runs as `--runner=cmd:true`, which builds it and runs nothing. A `requires_` would skip the build as well, and a test cannot pick its own runner, as the harness derives every test's from its target. A test is an `int main()`: exit 0 passes, nonzero fails, 77 skips (for a missing capability, never for a failed check). Each runs in its own process, in parallel, with a 60 s default timeout, and the output of a failed, crashed or timed-out test is in `build/test-logs/.log`; a later pass does not delete the log, so check its timestamp. What the tests need, in three groups: @@ -85,7 +86,7 @@ Conventions the existing tests follow: A bug fix ships with a test that fails before the fix and passes after it. A change to a UI kernel keeps `UIFallbackParity` green: the heap and descriptor-set builds of one source must stay pixel-identical. A category drawn fused must also match the same category drawn standalone. `TextAntialiasing` compares the two for text, and keeps the text's edge ramp at one screen pixel; for quads, circles and images no test does (`UIFusedShader` only pins the push-constant layout): check those by eye, or add the comparison. -Cross targets: `crafter-build test --target=x86_64-w64-mingw32` needs `wine` on the sandbox's `PATH`, which the Flatpak does not have; a MinGW import library for `vulkan-1`, which the toolchain does not ship; `-luuid`, which the Win32 link flags lack (#184); and a definition of `__libc_single_threaded`, which the MinGW build of Crafter.Asset references. The Win32 tests have been run with an import library made by `llvm-dlltool` from the `vk*` symbols `llvm-nm -u` lists for the MinGW `libCrafter.Graphics.a`, and an archive defining `char __libc_single_threaded = 0;`, both under the gitignored `build/`. A temporary edit of `project.cpp` added `-L` to that directory, both libraries and `-luuid` to the Win32 tests' link flags and cleared their `requires_`; the tests were built with `--runner=cmd:true`, and each exe ran from its bin directory under the host's wine in a scratch `WINEPREFIX`. For an aarch64 device, `crafter-build test --target=...` tries qemu, which has no Vulkan: build with the sysroot, copy the test's whole bin directory (binary, `*.spv`, `*.sets.spv`, fonts) to the device and run it there, unlocked and with the window visible, since a locked phone's compositor sends no frame callbacks and the FIFO present then blocks forever. +Cross targets: the Win32 tests run under Wine from the native crafter-build on the host, which has `wine` on its `PATH` and finds the host's mingw-w64 itself: `WINEPREFIX= crafter-build test --target=x86_64-w64-mingw32 PointerWin32 TextWin32 Win32Link`. The Flatpak has no `wine`, so there `PointerWin32` and `TextWin32` skip and only `Win32Link`'s link is checked. Under the Flatpak a Win32 executable also links only with `tests/Win32Link/libc-single-threaded.c`. The MinGW sysroot the Flatpak picks is the host's `/usr`, which puts glibc's headers on the MinGW include path, and libstdc++'s `shared_ptr`, inlined from nlohmann/json in Crafter.Asset's GLB loader, then reads glibc's `__libc_single_threaded` (Crafter.Build #43). For an aarch64 device, `crafter-build test --target=...` tries qemu, which has no Vulkan: build with the sysroot, copy the test's whole bin directory (binary, `*.spv`, `*.sets.spv`, fonts) to the device and run it there, unlocked and with the window visible, since a locked phone's compositor sends no frame callbacks and the FIFO present then blocks forever. ## Code style @@ -127,7 +128,7 @@ These were decided deliberately. Do not undo them without the maintainer's say-s ## Security considerations -- `project.cpp` is compiled and run by crafter-build; it clones and pulls three sibling repositories from their default branch and two Khronos repositories from GitHub on every invocation, and the Sponza example `GitFetch`es a 280 MB asset repository (pinned to a commit). Pin a `commit` in a `GitProject` spec when that matters. +- `project.cpp` is compiled and run by crafter-build; it clones and pulls three sibling repositories from their default branch and two Khronos repositories from GitHub on every invocation, for a Win32 target also clones and pulls Vulkan-Loader, unpinned, and runs `llvm-dlltool` on its `loader/vulkan-1.def` when the import library under `build/` was made from another `.def` or command, and the Sponza example `GitFetch`es a 280 MB asset repository (pinned to a commit). Pin a `commit` in a `GitProject` spec when that matters. - The validation layer, GPU-assisted validation included, is a developer tool and off by default; `CRAFTER_GRAPHICS_VALIDATION` turns it on. GPU-AV has crashed a vendor SPIR-V compiler on the heap pipelines before, which is why it is opt-in. - Shaders are read from disk relative to the working directory and checked for the SPIR-V magic and word alignment before they reach the driver; the NVIDIA rewrite edits SPIR-V in memory and `PushConstantRewrite` validates its output with spirv-val. - `pipeline_cache.bin` is written to the working directory at exit and read back only when its header matches the device; it is gitignored here and should be in a consumer. diff --git a/README.md b/README.md index 58490b7..9aae54f 100644 --- a/README.md +++ b/README.md @@ -289,18 +289,24 @@ frame. ## Build -The repository is built with `crafter-build` (a project-config based -build system; the project description lives in `project.cpp`): +The repository is built with `crafter-build` 3.0.0 or later (a +project-config based build system; the project description lives in +`project.cpp`): ```bash -crafter-build # native: Wayland on Linux, Win32 on Windows -crafter-build --target=wasm32-wasip1 # browser: DOM window + WebGPU renderer -crafter-build -r # build and run (in an example directory) +crafter-build # native: Wayland on Linux, Win32 on Windows +crafter-build --target=x86_64-w64-mingw32 # Win32, cross-compiled from Linux +crafter-build --target=wasm32-wasip1 # browser: DOM window + WebGPU renderer +crafter-build -r # build and run (in an example directory) ``` The build picks the window + renderer pair automatically from the target triple: any `wasm32-*` triple flips to DOM + WebGPU (no Vulkan -loader linked), everything else stays on the native Vulkan path. Each +loader linked), everything else stays on the native Vulkan path. A +Win32 build links the Vulkan loader through an import library that +`project.cpp` makes with `llvm-dlltool` from the loader's own export +list, Vulkan-Loader's `loader/vulkan-1.def`, fetched like the Vulkan +headers, so it needs no Vulkan SDK. Each example with both backends ships GLSL *and* WGSL copies of its shaders side-by-side (e.g. [raygen.glsl](examples/Sponza/raygen.glsl) + [raygen.wgsl](examples/Sponza/raygen.wgsl)); `project.cpp` selects the diff --git a/project.cpp b/project.cpp index e6e32d2..441979b 100644 --- a/project.cpp +++ b/project.cpp @@ -6,6 +6,46 @@ import Crafter.Build; namespace fs = std::filesystem; using namespace Crafter; +// The whole of `path`, or nullopt when it can't be read. +static std::optional ReadFile(const fs::path& path) { + std::ifstream file(path, std::ios::binary); + if (!file) return std::nullopt; + return std::string(std::istreambuf_iterator(file), std::istreambuf_iterator()); +} + +// Makes `lib`, the x86-64 import library for the DLL whose exports `def` +// lists, with the llvm-dlltool beside the toolchain's llvm-ar. A stamp beside +// `lib` holds the command and the .def it was made from, and while both match +// it is not made again: a fetch or a copy can leave a .def whose mtime says +// nothing of what changed in it. Empty on success, otherwise what went wrong. +static std::string MakeImportLibrary(const fs::path& def, const fs::path& lib) { + const std::optional exports = ReadFile(def); + if (!exports) return std::format("{} could not be read", def.string()); + // llvm-ar is a bare name on PATH, or /bin/llvm-ar. + const fs::path dlltool = GetToolchain().llvmAr.parent_path() / "llvm-dlltool"; + const std::string command = std::format("{} -m i386:x86-64 -d {} -l {}", + ShellQuote(dlltool.string()), ShellQuote(def.string()), ShellQuote(lib.string())); + const std::string stamp = command + '\n' + *exports; + const fs::path stampFile = fs::path(lib) += ".stamp"; + std::error_code ec; + if (fs::exists(lib, ec) && ReadFile(stampFile) == stamp) return {}; + fs::create_directories(lib.parent_path(), ec); + // llvm-dlltool writes the archive beside `lib` and renames it into place, + // so two loads of this project at once leave one whole library, and one + // that fails leaves the previous library as it was. + try { + const auto result = RunCommandWithTimeout(command, std::chrono::seconds(60)); + if (result.timedOut) return std::format("{} took more than 60 s", dlltool.string()); + if (result.crashed) return std::format("{} died of signal {}", dlltool.string(), result.signal); + if (result.exitCode != 0) return std::format("{} exited with {}: {}", dlltool.string(), result.exitCode, result.output); + } catch (const std::exception& error) { + return std::format("{} could not run: {}", dlltool.string(), error.what()); + } + // After the library, so a run cut short leaves no stamp to trust. + std::ofstream(stampFile, std::ios::binary) << stamp; + return {}; +} + extern "C" Configuration CrafterBuildProject(std::span args) { std::vector depArgs(args.begin(), args.end()); @@ -78,6 +118,10 @@ extern "C" Configuration CrafterBuildProject(std::span a // HSTRING and RoGetActivationFactory. Its gamepad code is C (see // Crafter.Graphics-GamepadWin32.h). cfg.linkFlags.push_back("-lruntimeobject"); + // The IIDs its event handlers compare against, IID_IUnknown and + // IID_IAgileObject. StartSync's Gamepad::Tick pulls the gamepad code + // into every executable, which without libuuid did not link (#184). + cfg.linkFlags.push_back("-luuid"); cfg.cFiles.push_back("implementations/Crafter.Graphics-GamepadWin32"); } else { cfg.defines.push_back({"CRAFTER_GRAPHICS_WINDOW_WAYLAND", ""}); @@ -145,7 +189,36 @@ extern "C" Configuration CrafterBuildProject(std::span a 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 (!windows) { + cfg.linkFlags.push_back("-lvulkan"); + } else { + // The MinGW toolchains ship no import library for the Vulkan + // loader, so one is made from the loader's own list of what + // vulkan-1.dll exports: loader/vulkan-1.def, from Vulkan-Loader + // unpinned like the headers above, so the API the library is + // compiled against and the exports it links against move + // together. No new call outgrows that list, and a call the loader + // does not export fails at link time instead of when the exe + // starts. The library goes into the link by its path, absolute + // because this project.cpp is also loaded from a consumer's + // directory, rather than as -L, so it shadows nothing: a + // consumer's own -lvulkan-1, a Vulkan SDK's, still serves the + // functions this list lacks. Without the fetch or llvm-dlltool, + // the library a previous load made does, or else a Vulkan SDK's. + const fs::path importLibrary = fs::absolute(std::format("build/vulkan-1-{}/libvulkan-1.a", cfg.target)); + std::string error; + try { + const fs::path loader = GitFetch({ .url = "https://github.com/KhronosGroup/Vulkan-Loader.git" }); + error = MakeImportLibrary(loader / "loader" / "vulkan-1.def", importLibrary); + } catch (const std::exception& fetchError) { + error = fetchError.what(); + } + if (!error.empty()) { + Progress::Diagnostic(std::format("warning: Crafter.Graphics: the vulkan-1 import library was not made: {}", error)); + } + std::error_code ec; + cfg.linkFlags.push_back(fs::exists(importLibrary, ec) ? importLibrary.string() : std::string("-lvulkan-1")); + } } if (opts.Has("--timing")) cfg.defines.push_back({"CRAFTER_TIMING", ""}); @@ -347,6 +420,19 @@ extern "C" Configuration CrafterBuildProject(std::span a win32TextTest.requires_ = { "tool:wine" }; #endif cfg.tests.push_back(std::move(win32TextTest)); + + // A Win32 executable that uses the library as an application does + // links: the vulkan-1 import library made above and -luuid (#184). + // The Win32 library itself is only archived, and the tests above + // are skipped without Wine, so nothing else links one there. With + // no Wine, run it as --runner=cmd:true, which builds it and runs + // nothing; no requires_, since that would skip the build too. + // Under Wine it also runs, opening a window and closing it, which + // needs a Vulkan device. libc-single-threaded.c stands in for + // Crafter.Build #43 and goes with it. + Test win32LinkTest = LibraryTest("Win32Link"); + win32LinkTest.config.cFiles.push_back("tests/Win32Link/libc-single-threaded"); + cfg.tests.push_back(std::move(win32LinkTest)); } // Regression test for issue #32: the Wayland scroll-wheel chain diff --git a/tests/Win32Link/libc-single-threaded.c b/tests/Win32Link/libc-single-threaded.c new file mode 100644 index 0000000..9d08a74 --- /dev/null +++ b/tests/Win32Link/libc-single-threaded.c @@ -0,0 +1,11 @@ +//SPDX-License-Identifier: LGPL-3.0-only +//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +// Stands in for Crafter.Build #43, and goes when that is fixed, with its line +// in project.cpp. In the Flatpak the MinGW sysroot is the host's /usr, which +// puts glibc's headers on the include path, so the libstdc++ shared_ptr that +// nlohmann/json inlines into Crafter.Asset's GLB loader reads glibc's +// __libc_single_threaded, which no Windows library defines. Zero means "not +// single-threaded", so the reference counts stay atomic, as they would be +// without the leak. +char __libc_single_threaded = 0; diff --git a/tests/Win32Link/main.cpp b/tests/Win32Link/main.cpp new file mode 100644 index 0000000..a52b58f --- /dev/null +++ b/tests/Win32Link/main.cpp @@ -0,0 +1,32 @@ +//SPDX-License-Identifier: LGPL-3.0-only +//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +// A Win32 executable that uses the library as an application does links: +// Device::Initialize, a Window, and StartSync, whose Gamepad::Tick pulls in +// the gamepad code. That takes the vulkan-1 import library project.cpp makes +// from the Vulkan loader's export list, which no MinGW toolchain ships, and +// libuuid for the gamepad code's IIDs (#184). Without either, no Win32 +// executable linked, and nothing noticed: the Win32 library is only +// archived, and the other Win32 tests are skipped where there is no Wine. +// +// Where there is no Wine, the link is the test, and nothing runs: +// crafter-build test --target=x86_64-w64-mingw32 --runner=cmd:true Win32Link +// Under Wine it also runs, which needs a Vulkan device: it opens a window, and +// StartSync returns once the WM_CLOSE posted to it has cleared `open`. + +#include +#include + +import Crafter.Graphics; +import std; +using namespace Crafter; + +int main() { + Device::Initialize(); + Window window(64, 64, "Win32Link"); + PostMessageW(window.hwnd, WM_CLOSE, 0, 0); + window.StartSync(); + const bool closed = !window.open; + std::println("{} a window opened, and StartSync returned once WM_CLOSE closed it", closed ? "PASS" : "FAIL"); + return closed ? EXIT_SUCCESS : EXIT_FAILURE; +}