fix(win32): link the Vulkan loader without a Vulkan SDK, link libuuid, and check that a Win32 executable links
Build
- The MinGW toolchains ship no import library for vulkan-1.dll, so
without a Vulkan SDK's every Win32 executable stopped at "unable to
find library -lvulkan-1". project.cpp now makes one with the
llvm-dlltool beside the toolchain's llvm-ar, from Vulkan-Loader's
loader/vulkan-1.def. That is the loader's own list of what
vulkan-1.dll exports, fetched with GitFetch, unpinned like
Vulkan-Headers, so the API the library compiles against and the
exports it links against move together. The import library the Wine
runs used, made from the calls the library happened to make, went
stale with the next new call (234fcce added
vkEnumerateInstanceExtensionProperties). The loader's list does not,
and a call it does not export fails at link time instead of when the
exe starts.
- It is made into build/vulkan-1-<target>/ when a stamp beside it,
holding the command and the .def it was made from, no longer matches;
a fetch or a copy says nothing through the .def's mtime. It goes into
the link by its absolute path rather than through -L, so it shadows
no consumer's Vulkan SDK library. A failed fetch or llvm-dlltool run
is a Progress::Diagnostic warning, and the link falls back to the
library a previous load made, or to -lvulkan-1.
- -luuid joins the Win32 link flags: the gamepad code compares against
IID_IUnknown and IID_IAgileObject, and StartSync's Gamepad::Tick pulls
it into every executable (#184).
- project.cpp uses ShellQuote, exported from crafter-build 3.0.0, which
is now the minimum.
Tests
- Win32Link: an executable that uses the library as an application does
(Device::Initialize, a Window, StartSync) links. Without Wine it runs
as --runner=cmd:true, which builds it and runs nothing; under Wine it
also opens a window and closes it. It fails to link without -luuid
(IID_IUnknown, IID_IAgileObject) and without the import library
("unable to find library -lvulkan-1"). Under the Flatpak it needs
tests/Win32Link/libc-single-threaded.c, which stands in for
Catcrafts/Crafter.Build#43 and goes with it.
- With the native crafter-build on the host, which has Wine and finds
the host's mingw-w64 itself, PointerWin32, TextWin32 and Win32Link now
build and pass under Wine with nothing added.
Docs
- AGENTS.md: the minimum crafter-build, the Vulkan-Loader fetch and what
project.cpp runs, the Win32Link check in the commands and the
done-list, and a one-line Wine recipe in place of the hand-built
import library, shim and project.cpp edit. AGENTS.consumer.md: the
minimum, how the import library is made, and the
__libc_single_threaded workaround under the Flatpak. README: the
minimum, the Win32 build command, and that it needs no Vulkan SDK.
Closes #184.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
781d95ae75
commit
7555504644
6 changed files with 152 additions and 15 deletions
|
|
@ -4,8 +4,9 @@ The Crafter engine's graphics library: a C++ module, `import Crafter.Graphics;`,
|
||||||
|
|
||||||
### Building against it
|
### 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 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 (`<name>.spv` from `<name>.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 (`<name>.spv` from `<name>.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.
|
- 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).
|
- 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.
|
- 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.
|
||||||
|
|
|
||||||
15
AGENTS.md
15
AGENTS.md
|
|
@ -29,16 +29,16 @@ Partitions, by what they are for. Every partition file exists on every target; o
|
||||||
|
|
||||||
## Dev environment
|
## 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-<target>/`, 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:
|
Targets, picked with `--target=` and passed down to the dependencies:
|
||||||
|
|
||||||
| Target | Backend | Notes |
|
| Target | Backend | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| native (`x86_64-unknown-linux-gnu`) | Wayland + Vulkan | the development loop |
|
| 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 |
|
| `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 |
|
| `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 --local # against ../Crafter.Event, ../Crafter.Math, ../Crafter.Asset
|
||||||
crafter-build --target=wasm32-wasip1 # the DOM/WebGPU library
|
crafter-build --target=wasm32-wasip1 # the DOM/WebGPU library
|
||||||
crafter-build --target=x86_64-w64-mingw32 # the Win32 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 # build and run every test for the host
|
||||||
crafter-build test 'TLAS*' # name glob
|
crafter-build test 'TLAS*' # name glob
|
||||||
crafter-build test --list
|
crafter-build test --list
|
||||||
|
|
@ -62,11 +63,11 @@ cd examples/HelloUI && crafter-build -r # build an example against t
|
||||||
crafter-build clean
|
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/<Name> && 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/<Name> && 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
|
## 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/<name>/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/<name>/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/<name>.log`; a later pass does not delete the log, so check its timestamp. What the tests need, in three groups:
|
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/<name>.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.
|
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=<scratch prefix> 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
|
## Code style
|
||||||
|
|
||||||
|
|
@ -127,7 +128,7 @@ These were decided deliberately. Do not undo them without the maintainer's say-s
|
||||||
|
|
||||||
## Security considerations
|
## 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.
|
- 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.
|
- 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.
|
- `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.
|
||||||
|
|
|
||||||
12
README.md
12
README.md
|
|
@ -289,18 +289,24 @@ frame.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
The repository is built with `crafter-build` (a project-config based
|
The repository is built with `crafter-build` 3.0.0 or later (a
|
||||||
build system; the project description lives in `project.cpp`):
|
project-config based build system; the project description lives in
|
||||||
|
`project.cpp`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
crafter-build # native: Wayland on Linux, Win32 on Windows
|
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 --target=wasm32-wasip1 # browser: DOM window + WebGPU renderer
|
||||||
crafter-build -r # build and run (in an example directory)
|
crafter-build -r # build and run (in an example directory)
|
||||||
```
|
```
|
||||||
|
|
||||||
The build picks the window + renderer pair automatically from the
|
The build picks the window + renderer pair automatically from the
|
||||||
target triple: any `wasm32-*` triple flips to DOM + WebGPU (no Vulkan
|
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
|
example with both backends ships GLSL *and* WGSL copies of its shaders
|
||||||
side-by-side (e.g. [raygen.glsl](examples/Sponza/raygen.glsl) +
|
side-by-side (e.g. [raygen.glsl](examples/Sponza/raygen.glsl) +
|
||||||
[raygen.wgsl](examples/Sponza/raygen.wgsl)); `project.cpp` selects the
|
[raygen.wgsl](examples/Sponza/raygen.wgsl)); `project.cpp` selects the
|
||||||
|
|
|
||||||
88
project.cpp
88
project.cpp
|
|
@ -6,6 +6,46 @@ import Crafter.Build;
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
using namespace Crafter;
|
using namespace Crafter;
|
||||||
|
|
||||||
|
// The whole of `path`, or nullopt when it can't be read.
|
||||||
|
static std::optional<std::string> ReadFile(const fs::path& path) {
|
||||||
|
std::ifstream file(path, std::ios::binary);
|
||||||
|
if (!file) return std::nullopt;
|
||||||
|
return std::string(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<std::string> exports = ReadFile(def);
|
||||||
|
if (!exports) return std::format("{} could not be read", def.string());
|
||||||
|
// llvm-ar is a bare name on PATH, or <prefix>/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<const std::string_view> args) {
|
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
||||||
std::vector<std::string> depArgs(args.begin(), args.end());
|
std::vector<std::string> depArgs(args.begin(), args.end());
|
||||||
|
|
||||||
|
|
@ -78,6 +118,10 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
// HSTRING and RoGetActivationFactory. Its gamepad code is C (see
|
// HSTRING and RoGetActivationFactory. Its gamepad code is C (see
|
||||||
// Crafter.Graphics-GamepadWin32.h).
|
// Crafter.Graphics-GamepadWin32.h).
|
||||||
cfg.linkFlags.push_back("-lruntimeobject");
|
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");
|
cfg.cFiles.push_back("implementations/Crafter.Graphics-GamepadWin32");
|
||||||
} else {
|
} else {
|
||||||
cfg.defines.push_back({"CRAFTER_GRAPHICS_WINDOW_WAYLAND", ""});
|
cfg.defines.push_back({"CRAFTER_GRAPHICS_WINDOW_WAYLAND", ""});
|
||||||
|
|
@ -145,7 +189,36 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
vkUtility.source.url = "https://github.com/KhronosGroup/Vulkan-Utility-Libraries.git";
|
vkUtility.source.url = "https://github.com/KhronosGroup/Vulkan-Utility-Libraries.git";
|
||||||
vkUtility.builder = ExternalBuilder::None;
|
vkUtility.builder = ExternalBuilder::None;
|
||||||
vkUtility.includeDirs = { "include" };
|
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", ""});
|
if (opts.Has("--timing")) cfg.defines.push_back({"CRAFTER_TIMING", ""});
|
||||||
|
|
@ -347,6 +420,19 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
win32TextTest.requires_ = { "tool:wine" };
|
win32TextTest.requires_ = { "tool:wine" };
|
||||||
#endif
|
#endif
|
||||||
cfg.tests.push_back(std::move(win32TextTest));
|
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
|
// Regression test for issue #32: the Wayland scroll-wheel chain
|
||||||
|
|
|
||||||
11
tests/Win32Link/libc-single-threaded.c
Normal file
11
tests/Win32Link/libc-single-threaded.c
Normal file
|
|
@ -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;
|
||||||
32
tests/Win32Link/main.cpp
Normal file
32
tests/Win32Link/main.cpp
Normal file
|
|
@ -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 <windows.h>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue