Skip to content

Commit 4badc62

Browse files
committed
feat(compat.vulkan-validation-layers): the Khronos validation layer for mcpp-built programs, bound to xim:vulkan-validation-layers
A debug build asks the loader for VK_LAYER_KHRONOS_validation; the host's copy is found by manifest and lost at dlopen, because an mcpp-built program runs on the ecosystem's glibc whose loader never searches /usr/lib. Drivers are bridged from the host (compat.vulkan-runtime) because they can only come from the machine; a layer is ordinary software and is a payload of the ecosystem, so this package is the binding: an anchor, a runtime dependency on the xim payload, and nothing to link. Discovery is the xim recipe's: the manifest in the subos's share/vulkan/explicit_layer.d, the subos share on XDG_DATA_DIRS, and mcpp carrying subos declarations into the programs it launches (mcpp#352). tests/examples/vulkan-validation-layers asserts the loader enumerates the layer and can load its library (vkEnumerateInstanceExtensionProperties by layer name), on a runner with no GPU.
1 parent 36028aa commit 4badc62

5 files changed

Lines changed: 233 additions & 0 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# 2026-09-13 — add `compat.vulkan-validation-layers`
2+
3+
## Why
4+
5+
A debug build of a Vulkan program asks the loader for `VK_LAYER_KHRONOS_validation`.
6+
On a machine with the layer installed, the loader finds the host manifest under
7+
`/usr/share/vulkan/explicit_layer.d` and then dlopens the library it names by bare
8+
soname. An mcpp-built program runs on the ecosystem's glibc, whose dynamic linker
9+
searches the payloads on the RPATH and never `/usr/lib`, so the dlopen fails and
10+
`vkCreateInstance` errors out (measured on xrgui's showcase: `failed to create
11+
vulkan instance!` after the loader logged the layer as found). `LD_LIBRARY_PATH`
12+
pointing at the host works and is exactly what the closed loop forbids.
13+
14+
Drivers are bridged from the host (`compat.vulkan-runtime`) because a driver can only
15+
come from the machine. A layer is ordinary software, so it is packaged.
16+
17+
## Shape
18+
19+
Shape I (ecosystem-stack binding), the `compat.libgbm` pattern: the library lives in
20+
`xim:vulkan-validation-layers` (xim-pkgindex), built in the `gfxbuild` subos from the
21+
`vulkan-sdk-1.4.357.0` tag with `-static-libstdc++ -static-libgcc`, so its NEEDED set is
22+
glibc's alone and it loads into any process. This descriptor compiles an anchor and
23+
declares the runtime dependency; the anchor download is upstream's `LICENSE.txt` at the
24+
tag, mirrored to `mcpp-res/vulkan-validation-layers` byte for byte.
25+
26+
## Discovery
27+
28+
Two declarations the xim recipe makes and a consumer inherits through the runtime
29+
dependency:
30+
31+
- the manifest is placed in the subos's `share/vulkan/explicit_layer.d`
32+
(`graphics.declare_vulkan_layer`, the ICD helper one directory over), with its
33+
`library_path` rewritten to the absolute payload path in `install()`;
34+
- the subos `share` goes on `XDG_DATA_DIRS` (`graphics.declare_subos_env`, one row).
35+
36+
mcpp carries subos declarations into `mcpp run` / `mcpp test` (mcpp#352), so no
37+
environment is set by hand and no engine change was needed.
38+
39+
## Feature
40+
41+
None. The layer is one library; nothing is gated.
42+
43+
## Test
44+
45+
`tests/examples/vulkan-validation-layers`: the loader enumerates the layer, and
46+
`vkEnumerateInstanceExtensionProperties` with the layer's name makes the loader load the
47+
library and asks it for `VK_EXT_debug_utils`. Both hold on a runner with no GPU; no
48+
instance is created. Linux only; a no-op `main` elsewhere.
49+
50+
## Order
51+
52+
xim-pkgindex first (recipe + helper, PR there), then this package, then the consumer
53+
(`[dev-dependencies] compat.vulkan-validation-layers`).

mcpp.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ members = [
143143
"tests/examples/opencl",
144144
"tests/examples/sycl-runtime",
145145
"tests/examples/vulkan",
146+
"tests/examples/vulkan-validation-layers",
146147
"tests/examples/vulkan-hpp-module",
147148
"tests/examples/websocket",
148149
"tests/examples/uwebsockets",
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
-- compat.vulkan-validation-layers — VK_LAYER_KHRONOS_validation for a program
2+
-- built by mcpp, bound to the ecosystem's `xim:vulkan-validation-layers`.
3+
--
4+
-- WHAT GOES WRONG WITHOUT IT
5+
--
6+
-- A debug build asks the loader for the validation layer. The loader finds the
7+
-- host's manifest under /usr/share/vulkan and then dlopens the library it
8+
-- names -- by bare soname, through the process's dynamic linker. An mcpp-built
9+
-- program runs on this ecosystem's glibc, whose loader searches the payloads
10+
-- on the RPATH and never /usr/lib, so the dlopen fails and vkCreateInstance
11+
-- returns an error after the layer was reported present:
12+
--
13+
-- [Vulkan Loader] ERROR: libVkLayer_khronos_validation.so: cannot open
14+
-- shared object file
15+
-- ... failed to create vulkan instance!
16+
--
17+
-- Host DRIVERS are bridged into the process because a driver can only come
18+
-- from the machine (compat.vulkan-runtime). A layer is ordinary software, so
19+
-- it is a payload of the ecosystem instead, and this package is the binding
20+
-- (shape I in docs/package-types.md): no source of its own, a runtime
21+
-- dependency on the xim payload, and an anchor so the descriptor has a
22+
-- target.
23+
--
24+
-- HOW THE LAYER IS FOUND
25+
--
26+
-- Two declarations the xim recipe makes, and this package inherits: the
27+
-- manifest is placed in the subos's share/vulkan/explicit_layer.d, and that
28+
-- share is put on XDG_DATA_DIRS -- the directory the Khronos loader reads.
29+
-- mcpp carries subos declarations into the processes it launches (mcpp#352),
30+
-- so `mcpp run` and `mcpp test` see the layer with no environment set by
31+
-- hand. The library itself is named by absolute payload path in the manifest
32+
-- and is self-contained (static libstdc++), so nothing in the consumer's
33+
-- runtime closure decides whether it loads.
34+
--
35+
-- A [dev-dependencies] entry is the intended spelling: the layer is for the
36+
-- developer's runs and must not ship with the artifact.
37+
--
38+
-- Version = the Vulkan SDK tag the layer is built from, the line compat.vulkan
39+
-- (the loader) tracks. The anchor is upstream's LICENSE.txt at that tag.
40+
package = {
41+
spec = "1",
42+
namespace = "compat",
43+
name = "vulkan-validation-layers",
44+
description = "VK_LAYER_KHRONOS_validation for mcpp-built programs, bound to the ecosystem's xim:vulkan-validation-layers — zero host dependency",
45+
licenses = {"Apache-2.0"},
46+
repo = "https://github.com/KhronosGroup/Vulkan-ValidationLayers",
47+
type = "package",
48+
49+
xpm = {
50+
linux = {
51+
deps = { runtime = { "xim:vulkan-validation-layers@1.4.357.0" } },
52+
["1.4.357.0"] = {
53+
url = {
54+
GLOBAL = "https://raw.githubusercontent.com/KhronosGroup/Vulkan-ValidationLayers/vulkan-sdk-1.4.357.0/LICENSE.txt",
55+
CN = "https://gitcode.com/mcpp-res/vulkan-validation-layers/releases/download/1.4.357.0/vulkan-validation-layers-1.4.357.0.txt",
56+
},
57+
sha256 = "db3010170b904cb7212ef6abd2336f316bf735060eeeca23f1a737f459cc73e4",
58+
},
59+
},
60+
},
61+
62+
mcpp = {
63+
language = "c++23",
64+
import_std = false,
65+
c_standard = "c11",
66+
generated_files = {
67+
["mcpp_generated/vulkan_validation_layers_anchor.c"] =
68+
"int mcpp_compat_vulkan_validation_layers_anchor(void) { return 0; }\n",
69+
},
70+
sources = { "mcpp_generated/vulkan_validation_layers_anchor.c" },
71+
targets = { ["vulkan_validation_layers_binding"] = { kind = "lib" } },
72+
deps = {},
73+
},
74+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# compat.vulkan-validation-layers test project.
2+
#
3+
# What this member proves is the seam, not a link: the package compiles
4+
# nothing a consumer calls. It pulls the ecosystem's layer payload into the
5+
# project environment, whose declarations (the manifest in the subos, the
6+
# subos share on XDG_DATA_DIRS) mcpp carries into the test process. So the
7+
# assertions are answered by the LOADER: it enumerates the layer, and it can
8+
# load the layer's library to ask it for its extensions. Both hold on a CI
9+
# runner with no GPU -- no instance is created and no driver is involved.
10+
#
11+
# Linux only, like the payload: elsewhere the test compiles to a no-op main().
12+
[package]
13+
name = "vulkan-validation-layers-tests"
14+
version = "0.1.0"
15+
16+
[target.'cfg(linux)'.dependencies.compat]
17+
vulkan = "1.4.357.3"
18+
19+
# [dev-dependencies], the intended spelling for a consumer: the layer is for
20+
# the developer's runs and is not part of the artifact.
21+
[target.'cfg(linux)'.dev-dependencies.compat]
22+
vulkan-validation-layers = "1.4.357.0"
23+
24+
[target.'cfg(linux)'.build]
25+
cxxflags = ["-DHAVE_VULKAN_LOADER=1"]
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// compat.vulkan-validation-layers — behavioral test, no GPU needed.
2+
//
3+
// Two questions, both answered by the loader before any driver is involved:
4+
//
5+
// 1. Is VK_LAYER_KHRONOS_validation enumerated? That is the manifest being
6+
// found -- the subos share reached XDG_DATA_DIRS and the manifest reached
7+
// share/vulkan/explicit_layer.d, and mcpp carried the variable into this
8+
// process.
9+
// 2. Can the layer's library be loaded? vkEnumerateInstanceExtensionProperties
10+
// with the layer's name makes the loader dlopen the library and call into
11+
// it. That is the very step that fails against a host copy of the layer
12+
// (bare soname, no /usr/lib on this loader's path), and it is what the
13+
// absolute library_path plus a self-contained .so make succeed.
14+
//
15+
// No vkCreateInstance: on a driverless runner that is VK_ERROR_INCOMPATIBLE_DRIVER
16+
// and says nothing about the layer.
17+
#if defined(HAVE_VULKAN_LOADER)
18+
#include <vulkan/vulkan.h>
19+
#endif
20+
import std;
21+
22+
#if !defined(HAVE_VULKAN_LOADER)
23+
int main() {
24+
std::println("compat.vulkan-validation-layers: skipped (linux only)");
25+
return 0;
26+
}
27+
#else
28+
int main() {
29+
constexpr const char* layer = "VK_LAYER_KHRONOS_validation";
30+
31+
std::uint32_t count = 0;
32+
if (vkEnumerateInstanceLayerProperties(&count, nullptr) != VK_SUCCESS) {
33+
std::println("vkEnumerateInstanceLayerProperties failed");
34+
return 1;
35+
}
36+
std::vector<VkLayerProperties> layers(count);
37+
if (count && vkEnumerateInstanceLayerProperties(&count, layers.data()) != VK_SUCCESS) {
38+
std::println("vkEnumerateInstanceLayerProperties (fill) failed");
39+
return 1;
40+
}
41+
bool found = false;
42+
for (const auto& l : layers) {
43+
std::println("layer: {} ({})", l.layerName, l.description);
44+
if (std::string_view{l.layerName} == layer) found = true;
45+
}
46+
if (!found) {
47+
std::println("{} is not enumerated: the manifest did not reach the loader", layer);
48+
return 1;
49+
}
50+
51+
// Loads the library. A stale or unreachable library_path fails here with
52+
// VK_ERROR_LAYER_NOT_PRESENT; a library that cannot resolve its own
53+
// dependencies fails the same way.
54+
std::uint32_t ext_count = 0;
55+
const auto rst = vkEnumerateInstanceExtensionProperties(layer, &ext_count, nullptr);
56+
if (rst != VK_SUCCESS) {
57+
std::println("vkEnumerateInstanceExtensionProperties({}) = {}: the layer library did not load", layer, static_cast<int>(rst));
58+
return 1;
59+
}
60+
std::vector<VkExtensionProperties> exts(ext_count);
61+
if (ext_count && vkEnumerateInstanceExtensionProperties(layer, &ext_count, exts.data()) != VK_SUCCESS) {
62+
std::println("vkEnumerateInstanceExtensionProperties (fill) failed");
63+
return 1;
64+
}
65+
bool debug_utils = false;
66+
for (const auto& e : exts) {
67+
std::println(" {} v{}", e.extensionName, e.specVersion);
68+
if (std::string_view{e.extensionName} == "VK_EXT_debug_utils") debug_utils = true;
69+
}
70+
// The validation layer implements VK_EXT_debug_utils, which is how a
71+
// program receives its messages; a layer that loaded but lists nothing
72+
// is not the one asked for.
73+
if (!debug_utils) {
74+
std::println("{} loaded but does not provide VK_EXT_debug_utils", layer);
75+
return 1;
76+
}
77+
std::println("ok: {} enumerated and loaded", layer);
78+
return 0;
79+
}
80+
#endif

0 commit comments

Comments
 (0)