Skip to content

fix(ci): provide the Vulkan loader on the windows leg, the way everyone else does - #391

Closed
Sunrisepeak wants to merge 2 commits into
mainfrom
feat/vulkan-windows-loader-dep
Closed

fix(ci): provide the Vulkan loader on the windows leg, the way everyone else does#391
Sunrisepeak wants to merge 2 commits into
mainfrom
feat/vulkan-windows-loader-dep

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

Completes what #388 could not: the three Windows Vulkan failures, fixed by shipping the loader rather than by finding a runner that already has one.

The gap

This package ships vulkan-1.lib — symbol stubs, no code. The code is vulkan-1.dll, and that is not an OS component: it arrives with a GPU driver, with LunarG's Runtime redistributable, or bundled beside an application.

So a machine without a driver has no loader, and a consumer linking the import library dies at process start with 0xC0000135 (STATUS_DLL_NOT_FOUND), before main:

image vulkan-1.dll vulkan, eui-neo-vulkan, vulkan-hpp-module
windows-latest / windows-2025 present pass
windows-2022 absent fail

#388 spent four rounds looking for an image where that and everything else worked. Each round broke a different consumer of the VS installation — mcpp scans the toolset directory, clang reads the environment, and OpenSSL's nmake reads the VS registration. It is closed for that reason.

The fix

deps = { "xim:vulkan-loader@>=1.4.313" } on the windows branch.

xim:vulkan-loader grew a Windows payload for this (openxlings/xim-pkgindex#818, merged). It is built on a runner, and the build loads the DLL it just produced and resolves vkEnumerateInstanceVersion, vkCreateInstance, vkGetInstanceProcAddr before publishing:

exports vkEnumerateInstanceVersion
exports vkCreateInstance
exports vkGetInstanceProcAddr
sha256: 7159d30530572403079e583ebab3c9834d1dd760473487d6476c8a2f34fffe22

Both mirrors verified byte-identical.

It brings no ICD, and does not need to

The loader finds drivers through HKLM\SOFTWARE\Khronos\Vulkan\Drivers, so a driverless machine enumerates no devices. That is exactly the scope this package's own test already declares:

Everything asserted here is answered by the LOADER itself, before any ICD is involved, so it is meaningful on a CI runner with no GPU and no driver.

What this run also proves

This is the first mcpp-index PR since #390 landed, which stopped the registry cache from carrying the resolved index. Before that, a change to a xim package could not be seen from this repo's CI at all — two successive xim:wix fixes were each merged, each re-run, and each appeared to do nothing. If the windows legs go green here, they also confirm #390 did what it claims.

… the image

This package ships `vulkan-1.lib` — symbol stubs, no code. The code is
`vulkan-1.dll`, and that is not an OS component: it arrives with a GPU
driver, with LunarG's Runtime redistributable, or bundled beside an
application. A machine without a driver therefore has no loader at all,
and a consumer linking the import library dies at process start with
0xC0000135, before `main`.

Measured on this index's CI: `vulkan`, `eui-neo-vulkan` and
`vulkan-hpp-module` fail that way on `windows-2022` and pass on images
that happen to carry a driver. #388 spent four rounds looking for an image
where that and everything else worked; each round broke a different
consumer of the VS installation, which is why it is closed. Declaring the
loader takes the image out of the question instead of chasing it.

`xim:vulkan-loader` grew a windows payload for this
(openxlings/xim-pkgindex#818). It is built on a runner, and the build
loads the DLL and resolves vkEnumerateInstanceVersion, vkCreateInstance
and vkGetInstanceProcAddr before publishing — an artifact that exists is
not an artifact that works.

It brings no ICD, and does not need to. The loader finds drivers through
the registry, so a driverless machine enumerates no devices — which is
exactly what this package's test already scopes itself to: "answered by
the LOADER itself, before any ICD is involved, so it is meaningful on a CI
runner with no GPU and no driver."
Sunrisepeak added a commit to openxlings/xim-pkgindex that referenced this pull request Sep 11, 2026
…819)

The windows branch added in #818 shipped a payload and declared nothing
about where it is. This adds `exports.runtime.libdirs = {"bin"}` to the
windows xpm block and removes the `subos.env` PATH prepend from config().

WHY THE FIRST ATTEMPT WAS THE WRONG LAYER. My comment in #818 claimed
`exports.runtime.libdirs` is read only by xlings's elfpatch and therefore
has nothing to say about a PE. That is false: mcpp reads the same field to
COPY a dependency's *.dll beside the executable it builds
(`src/build/plan.cppm`, `runtimeDeployFiles`), and its own comment names
this exact case -- "only a Windows prebuilt-DLL package (or a test that
ships a .dll) populates it". The chain is resolver.cpp:167 ->
installer.cpp:2486 -> the runtime binding mcpp reads. `bin` and not `lib`:
bin/ holds vulkan-1.dll, lib/ holds the import library the linker reads.

PATH was also too wide even where it worked -- process-wide, so it would
put our loader in front of the system's for every child of that shell.

MEASURED, BOTH HALVES. The PATH declaration installs cleanly under a full
xlings (this repo's windows-test: `done`, `[PASS] new shim`, subos
"default" present) and fails inside mcpp's project sandbox with
`E_INTERNAL: config hook failed` (mcpplibs/mcpp-index#391, windows leg;
every other leg green, loader downloaded and installed before the hook
runs). Two environments, one descriptor, opposite results. The asymmetry
behind that is filed as mcpp-community/mcpp#614 -- mcpp invokes xlings with
`env -u XLINGS_PROJECT_DIR` on POSIX and sets it to the EMPTY STRING on
Windows, and absent and present-but-empty are not the same answer to
"which scope is this". This change does not depend on that being fixed.

RECORDED HONESTLY: WHY THIS PACKAGE EXISTS AT ALL. The windows payload
(#818) was not motivated by a user report. It was motivated by a runner
image choice made in mcpplibs/mcpp-index#385, where the Windows leg was
pinned to `windows-2022` to dodge an MSVC STL 14.51 bug
(mcpp-community/mcpp#609, microsoft/STL#6294). That image is the one
without the loader. Measured on three images (mcpp-index#387 probe):

    windows-2022        vulkan-1.dll absent    MSVC 14.29/14.44
                        huxerui PASS           vulkan FAIL 0xC0000135
    windows-2025        vulkan-1.dll PRESENT   MSVC +14.51
                        huxerui FAIL (STL)     vulkan PASS
    windows-latest      same as windows-2025

Neither image satisfies both, which is what four rounds of
mcpp-index#388 failed to escape before it was closed. Shipping the loader
takes the image out of the question instead -- and the underlying claim
stands on its own: vulkan-1.dll is not an OS component, it arrives with a
GPU driver or as LunarG's redistributable, so a driverless machine has no
loader and anything linking vulkan-1.lib dies before main.

The `subos` import goes with the call; nothing else used it. Linux is
untouched.

Co-authored-by: sunrisepeak <x.d2learn.org@gmail.com>
…ne else does

`vulkan-1.dll` is not part of Windows. It arrives with a GPU driver, with
LunarG's Vulkan Runtime redistributable, or bundled beside an application,
so a runner with no driver has no loader and anything linking
`vulkan-1.lib` dies at process start with 0xC0000135, before main.

MEASURED across the three images (#387 probe, run 34569282837):

    windows-2022    vulkan-1.dll absent    MSVC 14.29/14.44
                    huxerui PASS           vulkan FAIL 0xC0000135
    windows-2025    vulkan-1.dll PRESENT   MSVC +14.51
                    huxerui FAIL (STL)     vulkan PASS
    windows-latest  same as windows-2025

Neither image satisfies both. This leg is pinned to `windows-2022` to
dodge the MSVC STL 14.51 bug (mcpp-community/mcpp#609,
microsoft/STL#6294), and windows-2022 is the image without the loader.

THIS REPLACES THE PACKAGE-DEPENDENCY APPROACH, which did not work. The
first version of this PR had `compat.vulkan`'s windows branch declare
`xim:vulkan-loader@>=1.4.313`. Three runs:

  1. cold store -- loader installed, its config() hook failed
     (`subos.env` PATH declaration; fixed by openxlings/xim-pkgindex#819,
     asymmetry filed as mcpp-community/mcpp#614)
  2. warm store -- `compat.vulkan@1.4.357.0` already installed, so its
     dependency closure was never re-evaluated and the loader was not
     installed at all
  3. cold store again, after deleting the three registry caches -- still
     no loader, and no error saying why

The declaration sits in `xpm.windows` beside working precedents
(`compat.cuda-runtime`, `compat.cudart`), and range versions have
precedent too (`compat.openssl`, `compat.vulkan-runtime`), so the shape
is not obviously wrong -- but three runs produced no loader and no
diagnostic, and chasing it further is not worth it for a problem whose
actual shape is "the runner is missing a redistributable".

WHAT EVERYONE ELSE DOES, since this is a common Windows problem:

  * dynamic loading in the consumer -- volk, Vulkan-Hpp's
    VULKAN_HPP_DISPATCH_LOADER_DYNAMIC, glfwVulkanSupported(),
    SDL_Vulkan_LoadLibrary. The process starts and degrades gracefully
    instead of dying before main. This is the robust answer for
    applications.
  * rely on the GPU driver -- what most games do, and sound: a machine
    that can run Vulkan has a driver, and the driver brought the loader.
  * ship the loader -- Apache-2.0, which is what the LunarG Runtime
    redistributable is for.
  * on CI specifically -- install the Runtime/SDK, or register a software
    ICD (SwiftShader, lavapipe) when device-level coverage is wanted.

These members assert loader-level calls only (`vkEnumerateInstanceVersion`,
instance extension enumeration), deliberately: the loader advertises WSI
extensions only when an ICD supports them, so asserting on those would be
testing the runner's hardware. A loader with no ICD is exactly the right
amount here.

The payload is the one built for openxlings/xim-pkgindex#818 -- built on a
runner from Khronos' source, and the build loads the DLL and resolves
vkEnumerateInstanceVersion, vkCreateInstance and vkGetInstanceProcAddr
before publishing. Hash-pinned here, and the step is idempotent: if the
image ever grows the DLL, it leaves it alone.

The xim-pkgindex loader package stays. It is a real ecosystem capability
for a driverless Windows machine; it is simply not the layer that fixes a
missing redistributable on a CI runner.

Co-authored-by: sunrisepeak <x.d2learn.org@gmail.com>
@Sunrisepeak Sunrisepeak changed the title fix(compat.vulkan): declare the loader, so windows stops depending on the image fix(ci): provide the Vulkan loader on the windows leg, the way everyone else does Sep 11, 2026
Sunrisepeak added a commit that referenced this pull request Sep 11, 2026
Every reason to distrust the diff used to escalate to the whole workspace.
On a PR that is the wrong trade twice over: it buries the change under ~22
shards of unrelated work, and it puts the PR's own legs behind an hour of
queue, so the thing under review is the last thing to report.

Touching this file was enough to trigger it, which is the case that keeps
biting: a one-line CI edit cannot be reviewed against a fast signal.
Measured three times today -- #390, #391 and #392 each ran a full matrix to
validate a change whose blast radius was one job.

`full` now answers only where "check everything" IS the request: the weekly
cron and a manual `workflow_dispatch`. On a PR or a push, the same reasons
call `widen` instead, which records them, prints them, and writes them to
the run summary so a reviewer sees WITHOUT opening a job that this run
deliberately tested less -- and that the sweep is one manual dispatch away.

Degraded, with their reasons kept:

    .github/workflows/validate.yml, tests/*.sh
    mcpp.toml non-member change
    unclassified path
    push with no predecessor / predecessor not in history

NOT weakened: descriptor and member changes select their members exactly as
before. The only thing that changed is what happens when the diff cannot be
classified.

One consequence handled. `widen` does not exit, so the push-with-no-
predecessor path has to leave a usable range behind. `HEAD` alone would diff
root-to-HEAD, name every file and select every member -- a full run by
another name, which is what this change exists to stop. It leaves the range
EMPTY instead: nothing selected, the reason already said, the summary
carrying it to a human. Rare either way, since merges here are squashes and
`event.before` is present on every normal push.

The weekly Sunday 06:00 sweep is deliberately kept. It is the only thing
that would have caught the windows vulkan regression before a user did --
main's leg was pinned to `windows-2022` in #385 and the vulkan members were
never re-tested there until a full run happened to fire today. Removing the
automatic full run on PRs and keeping the periodic net is the same shape
openxlings/xim-pkgindex#815 just added on the other side.

Verified: the rendered fragment on `pull_request` with a validate.yml edit
plus an unclassified path selects only the member its descriptor change
names, and the same input under `workflow_dispatch` still goes full.

Co-authored-by: sunrisepeak <x.d2learn.org@gmail.com>
@Sunrisepeak

Copy link
Copy Markdown
Member Author

Superseded by #395, closing.

This PR put vulkan-1.dll into System32 on the CI runner. That makes the windows leg pass whether or not the package works, so it would have hidden the real fix rather than proved it.

#395 ships the loader inside compat.vulkan 1.4.357.3 instead, and it was measured on windows-2022 with no system loader present (probe #396, run on #395's branch):

no system vulkan-1.dll: confirmed
PROBE: loader api 1.4.357
PROBE: vulkan-1.dll = D:\a\_temp\vkprobe\target\x86_64-windows-msvc\...\bin\vulkan-1.dll
archive: target/dist/vkprobe-0.1.0-x86_64-pc-windows-msvc.zip
   741376  vkprobe-0.1.0-x86_64-pc-windows-msvc/vulkan-1.dll
PROBE: vulkan-1.dll = D:\a\_temp\clean-run\vkprobe-0.1.0-x86_64-pc-windows-msvc\vulkan-1.dll   (PATH = System32 only)

Deployed beside the executable by mcpp build, carried by mcpp pack, and loaded from a clean directory. vkCreateInstance returns VK_ERROR_INCOMPATIBLE_DRIVER with device count 0 on that driverless runner — no crash, which is the correct state of such a machine.

Sunrisepeak added a commit that referenced this pull request Sep 11, 2026
* feat(compat.vulkan): 1.4.357.3 ships the Vulkan loader on Windows

A Windows program that links `vulkan-1.lib` needs `vulkan-1.dll` at process
start, and that DLL is not part of Windows. It arrives with a GPU driver, with
LunarG's Vulkan Runtime redistributable, or beside an application. A machine
without a driver has none, and the program dies before `main` with 0xC0000135
(STATUS_DLL_NOT_FOUND). Measured on GitHub's `windows-2022` image (#387 probe,
run 34569282837): `vulkan`, `eui-neo-vulkan` and `vulkan-hpp-module` all fail
that way, while images that happen to carry a driver pass.

WHAT CHANGES

  compat.vulkan 1.4.357.3   windows artifact adds bin/vulkan-1.dll and
                            LICENSE.txt; mcpp.windows.runtime gains
                            library_dirs = { "bin" }
  khronos.vulkan-hpp 1.4.357.1   same headers, pin -> compat.vulkan 1.4.357.3
  compat.eui-neo 0.5.9.1    upstream 0.5.9 unchanged, `vulkan` feature pin
                            -> compat.vulkan 1.4.357.3
  members                   vulkan 1.4.357.3, vulkan-hpp-module 1.4.357.1,
                            eui-neo-vulkan 0.5.9.1
  tests/examples/vulkan     on Windows, asserts the mapped vulkan-1.dll lives
                            in the executable's own directory

THE MECHANISM ALREADY EXISTS AND IS NOT NEW HERE. mcpp copies every *.dll under
a dependency's `runtime.library_dirs` beside the executable it builds (mcpp #185,
v0.0.73); `compat.openblas` has shipped `bin/libopenblas.dll` this way on
Windows CI since mcpp-index #55. Two properties this change leans on were
measured locally with mcpp 2026.9.11.2 rather than assumed:

  * transitive -- app -> mid -> dep(bin/vulkan-1.dll): the DLL lands beside
    `app`. `vulkan-hpp-module` and `eui-neo-vulkan` reach compat.vulkan only
    transitively.
  * test binaries -- `mcpp test` places it beside the test executable too.

`mcpp pack` needs nothing further: its PE closure always searches the
executable's own directory ("whatever the build staged beside it ... is by
definition part of what it runs with"), and `vulkan-1.dll` is not in its
system-DLL allow-list, so the deployed copy is what a packed program carries.

THE ARTIFACT (xlings-res/vulkan-import 1.4.357.3, sha256 8118f1bd...12f5)

  lib/vulkan-1.lib    byte-identical to 1.4.357.1's
  vulkan-1.def        upstream `loader/vulkan-1.def`, tag vulkan-sdk-1.4.357.0
  bin/vulkan-1.dll    built from vulkan-sdk-1.4.357.0 by
                      xlings-res/vulkan-loader's windows workflow, which loads
                      the DLL and resolves vkEnumerateInstanceVersion,
                      vkCreateInstance and vkGetInstanceProcAddr before
                      publishing (run 34608619850)
  LICENSE.txt         Vulkan-Loader's Apache-2.0 -- a redistributed binary
                      carries its license
  README.md           how each file was produced

Packed deterministically (sorted, fixed mtime, numeric owner, gzip -n); the
sha was computed twice, read back from GitHub, and the gitcode mirror
(mcpp-res/vulkan-import 1.4.357.3) is byte-identical.

COMPATIBILITY, MEASURED. The DLL exports exactly the 265 names in the .def --
no additions, no omissions -- so every import `vulkan-1.lib` can produce
resolves, and no consumer can hit "entry point not found". The loader version
the earlier xim payload carried (1.4.313) exports the same 265; the build is at
1.4.357 anyway so the loader matches the headers it is consumed with.

On a machine that ALREADY has a GPU driver nothing is lost: the copy beside
the executable is found first (the application directory precedes System32),
and the loader still reads HKLM\SOFTWARE\Khronos\Vulkan\Drivers, so the driver
the machine has is the ICD it uses. The ICD is deliberately not supplied -- a
software fallback would hide a missing driver behind a slow device.

WHY NEW VERSIONS INSTEAD OF MOVING PINS. An installed copy records the pins it
resolved with. Moving a pin inside a published version does not reach a warm
store, which is not hypothetical here: with #391's first approach, a warm CI
store kept `compat.vulkan@1.4.357.0`, never re-evaluated its closure, and the
loader never arrived. That is the same rule as compat.vulkan 1.4.357.1.
Versions before 1.4.357.3 have no bin/; mcpp skips a declared runtime
directory that does not exist, so `library_dirs` is inert for them.

WHY THE TEST ASSERTION IS A REAL CHECK. On Windows the test now requires the
mapped vulkan-1.dll to sit in the executable's directory. With an older
compat.vulkan that fails on a machine with a driver (the loader comes from
System32) and never runs on one without (the process dies first). The
`windows-2022` leg of this PR has no system loader at all, so it can only pass
if the deployment works. This is also why #391 -- which put the DLL in
System32 on the runner -- is superseded rather than merged: it would make that
leg pass whether or not the package works.

Co-authored-by: sunrisepeak <x.d2learn.org@gmail.com>

* fix(eui-neo): find upstream's directory under a re-released version; vulkan-hpp moves to a follow-up

TWO THINGS LOCAL VERIFICATION FOUND in the first push of this branch.

1. compat.eui-neo 0.5.9.1 could not install. The install hook looks for the
   unpacked archive by name, `EUI-NEO-<version>`, and took the version
   verbatim -- so it looked for `EUI-NEO-0.5.9.1/` in an archive that unpacks
   to `EUI-NEO-0.5.9/`:

       eui-neo: no CMakeLists.txt under .../compat-x-eui-neo/0.5.9.1/eui-neo-0.5.9.1
       after unpacking; the archive layout is neither wrapped nor flat

   A fourth version component is this index re-releasing the same upstream tag,
   so the lookup now drops it. Three-component versions are unchanged; checked
   directly: 0.5.9 -> 0.5.9, 0.5.9.1 -> 0.5.9, 0.5.10 -> 0.5.10,
   1.2.3.45 -> 1.2.3, 0.5.9-rc1 -> 0.5.9-rc1. The `layer` directory keeps the
   package's own version; the sources are `*/` globs, so its name never
   mattered.

   After the fix, locally with mcpp 2026.9.11.2:
       compat.eui-neo[vulkan]: ok (backend=vulkan, loader api 1.4.357)
   i.e. eui-neo 0.5.9.1 resolved compat.vulkan 1.4.357.3 through the feature.

   This is the same class of bug as openxlings/xim-pkgindex#821 fixed in
   vulkan-loader: a hook deriving a path from the version works exactly until a
   second version shares an archive.

2. khronos.vulkan-hpp 1.4.357.1 cannot be verified in the same change that
   introduces compat.vulkan 1.4.357.3. `vulkan-hpp-module` redirects only the
   `khronos` namespace to this checkout, so `compat` comes from the PUBLISHED
   index, which does not have 1.4.357.3 until this merges:

       xlings install_packages failed (exit 1) for 'compat.vulkan@1.4.357.3'
       with 1 index repo configured [mcpplibs -> https://github.com/mcpplibs/mcpp-index.git]

   Redirecting `compat` as well was tried and is still refused -- mcpp reports
   "≥2 project-level index repos is a known xlings resolution gap (mcpp #238;
   root cause openxlings/xlings#374)" even though #238 is closed. So the
   khronos.vulkan-hpp bump and its member pin are reverted here and follow
   once this is merged and the index republished -- the same order
   openxlings/xim-pkgindex#818 and mcpp-index#391 needed.

   Consequence for this PR's CI, stated in advance: `vulkan-hpp-module` is
   still selected (its manifest names vulkan) and still resolves
   compat.vulkan 1.4.357.0, so on the windows leg it fails exactly as it does
   on main today. `vulkan` and `eui-neo-vulkan` are the members this change is
   judged by.

Co-authored-by: sunrisepeak <x.d2learn.org@gmail.com>

---------

Co-authored-by: sunrisepeak <x.d2learn.org@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant