Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .agents/docs/2026-09-13-add-vulkan-validation-layers-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# 2026-09-13 — add `compat.vulkan-validation-layers`

## Why

A debug build of a Vulkan program asks the loader for `VK_LAYER_KHRONOS_validation`.
On a machine with the layer installed, the loader finds the host manifest under
`/usr/share/vulkan/explicit_layer.d` and then dlopens the library it names by bare
soname. An mcpp-built program runs on the ecosystem's glibc, whose dynamic linker
searches the payloads on the RPATH and never `/usr/lib`, so the dlopen fails and
`vkCreateInstance` errors out (measured on xrgui's showcase: `failed to create
vulkan instance!` after the loader logged the layer as found). `LD_LIBRARY_PATH`
pointing at the host works and is exactly what the closed loop forbids.

Drivers are bridged from the host (`compat.vulkan-runtime`) because a driver can only
come from the machine. A layer is ordinary software, so it is packaged.

## Shape

Shape I (ecosystem-stack binding), the `compat.libgbm` pattern: the library lives in
`xim:vulkan-validation-layers` (xim-pkgindex), built in the `gfxbuild` subos from the
`vulkan-sdk-1.4.357.0` tag with `-static-libstdc++ -static-libgcc`, so its NEEDED set is
glibc's alone and it loads into any process. This descriptor compiles an anchor and
declares the runtime dependency; the anchor download is upstream's `LICENSE.txt` at the
tag, mirrored to `mcpp-res/vulkan-validation-layers` byte for byte.

## Discovery

Two declarations the xim recipe makes and a consumer inherits through the runtime
dependency:

- the manifest is placed in the subos's `share/vulkan/explicit_layer.d`
(`graphics.declare_vulkan_layer`, the ICD helper one directory over), with its
`library_path` rewritten to the absolute payload path in `install()`;
- the subos `share` goes on `XDG_DATA_DIRS` (`graphics.declare_subos_env`, one row).

mcpp carries subos declarations into `mcpp run` / `mcpp test` (mcpp#352), so no
environment is set by hand and no engine change was needed.

## Feature

None. The layer is one library; nothing is gated.

## Test

`tests/examples/vulkan-validation-layers`: the loader enumerates the layer, and
`vkEnumerateInstanceExtensionProperties` with the layer's name makes the loader load the
library and asks it for `VK_EXT_debug_utils`. Both hold on a runner with no GPU; no
instance is created. Linux only; a no-op `main` elsewhere.

## Order

xim-pkgindex first (recipe + helper, PR there), then this package, then the consumer
(`[dev-dependencies] compat.vulkan-validation-layers`).
1 change: 1 addition & 0 deletions mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ members = [
"tests/examples/opencl",
"tests/examples/sycl-runtime",
"tests/examples/vulkan",
"tests/examples/vulkan-validation-layers",
"tests/examples/vulkan-hpp-module",
"tests/examples/websocket",
"tests/examples/uwebsockets",
Expand Down
74 changes: 74 additions & 0 deletions pkgs/c/compat.vulkan-validation-layers.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
-- compat.vulkan-validation-layers — VK_LAYER_KHRONOS_validation for a program
-- built by mcpp, bound to the ecosystem's `xim:vulkan-validation-layers`.
--
-- WHAT GOES WRONG WITHOUT IT
--
-- A debug build asks the loader for the validation layer. The loader finds the
-- host's manifest under /usr/share/vulkan and then dlopens the library it
-- names -- by bare soname, through the process's dynamic linker. An mcpp-built
-- program runs on this ecosystem's glibc, whose loader searches the payloads
-- on the RPATH and never /usr/lib, so the dlopen fails and vkCreateInstance
-- returns an error after the layer was reported present:
--
-- [Vulkan Loader] ERROR: libVkLayer_khronos_validation.so: cannot open
-- shared object file
-- ... failed to create vulkan instance!
--
-- Host DRIVERS are bridged into the process because a driver can only come
-- from the machine (compat.vulkan-runtime). A layer is ordinary software, so
-- it is a payload of the ecosystem instead, and this package is the binding
-- (shape I in docs/package-types.md): no source of its own, a runtime
-- dependency on the xim payload, and an anchor so the descriptor has a
-- target.
--
-- HOW THE LAYER IS FOUND
--
-- Two declarations the xim recipe makes, and this package inherits: the
-- manifest is placed in the subos's share/vulkan/explicit_layer.d, and that
-- share is put on XDG_DATA_DIRS -- the directory the Khronos loader reads.
-- mcpp carries subos declarations into the processes it launches (mcpp#352),
-- so `mcpp run` and `mcpp test` see the layer with no environment set by
-- hand. The library itself is named by absolute payload path in the manifest
-- and is self-contained (static libstdc++), so nothing in the consumer's
-- runtime closure decides whether it loads.
--
-- A [dev-dependencies] entry is the intended spelling: the layer is for the
-- developer's runs and must not ship with the artifact.
--
-- Version = the Vulkan SDK tag the layer is built from, the line compat.vulkan
-- (the loader) tracks. The anchor is upstream's LICENSE.txt at that tag.
package = {
spec = "1",
namespace = "compat",
name = "vulkan-validation-layers",
description = "VK_LAYER_KHRONOS_validation for mcpp-built programs, bound to the ecosystem's xim:vulkan-validation-layers — zero host dependency",
licenses = {"Apache-2.0"},
repo = "https://github.com/KhronosGroup/Vulkan-ValidationLayers",
type = "package",

xpm = {
linux = {
deps = { runtime = { "xim:vulkan-validation-layers@1.4.357.0" } },
["1.4.357.0"] = {
url = {
GLOBAL = "https://raw.githubusercontent.com/KhronosGroup/Vulkan-ValidationLayers/vulkan-sdk-1.4.357.0/LICENSE.txt",
CN = "https://gitcode.com/mcpp-res/vulkan-validation-layers/releases/download/1.4.357.0/vulkan-validation-layers-1.4.357.0.txt",
},
sha256 = "db3010170b904cb7212ef6abd2336f316bf735060eeeca23f1a737f459cc73e4",
},
},
},

mcpp = {
language = "c++23",
import_std = false,
c_standard = "c11",
generated_files = {
["mcpp_generated/vulkan_validation_layers_anchor.c"] =
"int mcpp_compat_vulkan_validation_layers_anchor(void) { return 0; }\n",
},
sources = { "mcpp_generated/vulkan_validation_layers_anchor.c" },
targets = { ["vulkan_validation_layers_binding"] = { kind = "lib" } },
deps = {},
},
}
25 changes: 25 additions & 0 deletions tests/examples/vulkan-validation-layers/mcpp.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# compat.vulkan-validation-layers test project.
#
# What this member proves is the seam, not a link: the package compiles
# nothing a consumer calls. It pulls the ecosystem's layer payload into the
# project environment, whose declarations (the manifest in the subos, the
# subos share on XDG_DATA_DIRS) mcpp carries into the test process. So the
# assertions are answered by the LOADER: it enumerates the layer, and it can
# load the layer's library to ask it for its extensions. Both hold on a CI
# runner with no GPU -- no instance is created and no driver is involved.
#
# Linux only, like the payload: elsewhere the test compiles to a no-op main().
[package]
name = "vulkan-validation-layers-tests"
version = "0.1.0"

[target.'cfg(linux)'.dependencies.compat]
vulkan = "1.4.357.3"

# [dev-dependencies], the intended spelling for a consumer: the layer is for
# the developer's runs and is not part of the artifact.
[target.'cfg(linux)'.dev-dependencies.compat]
vulkan-validation-layers = "1.4.357.0"

[target.'cfg(linux)'.build]
cxxflags = ["-DHAVE_VULKAN_LOADER=1"]
80 changes: 80 additions & 0 deletions tests/examples/vulkan-validation-layers/tests/layer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// compat.vulkan-validation-layers — behavioral test, no GPU needed.
//
// Two questions, both answered by the loader before any driver is involved:
//
// 1. Is VK_LAYER_KHRONOS_validation enumerated? That is the manifest being
// found -- the subos share reached XDG_DATA_DIRS and the manifest reached
// share/vulkan/explicit_layer.d, and mcpp carried the variable into this
// process.
// 2. Can the layer's library be loaded? vkEnumerateInstanceExtensionProperties
// with the layer's name makes the loader dlopen the library and call into
// it. That is the very step that fails against a host copy of the layer
// (bare soname, no /usr/lib on this loader's path), and it is what the
// absolute library_path plus a self-contained .so make succeed.
//
// No vkCreateInstance: on a driverless runner that is VK_ERROR_INCOMPATIBLE_DRIVER
// and says nothing about the layer.
#if defined(HAVE_VULKAN_LOADER)
#include <vulkan/vulkan.h>
#endif
import std;

#if !defined(HAVE_VULKAN_LOADER)
int main() {
std::println("compat.vulkan-validation-layers: skipped (linux only)");
return 0;
}
#else
int main() {
constexpr const char* layer = "VK_LAYER_KHRONOS_validation";

std::uint32_t count = 0;
if (vkEnumerateInstanceLayerProperties(&count, nullptr) != VK_SUCCESS) {
std::println("vkEnumerateInstanceLayerProperties failed");
return 1;
}
std::vector<VkLayerProperties> layers(count);
if (count && vkEnumerateInstanceLayerProperties(&count, layers.data()) != VK_SUCCESS) {
std::println("vkEnumerateInstanceLayerProperties (fill) failed");
return 1;
}
bool found = false;
for (const auto& l : layers) {
std::println("layer: {} ({})", l.layerName, l.description);
if (std::string_view{l.layerName} == layer) found = true;
}
if (!found) {
std::println("{} is not enumerated: the manifest did not reach the loader", layer);
return 1;
}

// Loads the library. A stale or unreachable library_path fails here with
// VK_ERROR_LAYER_NOT_PRESENT; a library that cannot resolve its own
// dependencies fails the same way.
std::uint32_t ext_count = 0;
const auto rst = vkEnumerateInstanceExtensionProperties(layer, &ext_count, nullptr);
if (rst != VK_SUCCESS) {
std::println("vkEnumerateInstanceExtensionProperties({}) = {}: the layer library did not load", layer, static_cast<int>(rst));
return 1;
}
std::vector<VkExtensionProperties> exts(ext_count);
if (ext_count && vkEnumerateInstanceExtensionProperties(layer, &ext_count, exts.data()) != VK_SUCCESS) {
std::println("vkEnumerateInstanceExtensionProperties (fill) failed");
return 1;
}
bool debug_utils = false;
for (const auto& e : exts) {
std::println(" {} v{}", e.extensionName, e.specVersion);
if (std::string_view{e.extensionName} == "VK_EXT_debug_utils") debug_utils = true;
}
// The validation layer implements VK_EXT_debug_utils, which is how a
// program receives its messages; a layer that loaded but lists nothing
// is not the one asked for.
if (!debug_utils) {
std::println("{} loaded but does not provide VK_EXT_debug_utils", layer);
return 1;
}
std::println("ok: {} enumerated and loaded", layer);
return 0;
}
#endif
Loading