Skip to content

Vulkan-Hpp (C++)

The official Khronos C++ binding for Vulkan — a fully generated, header-only layer over the C API that adds strong handle types, compile-time-validated pNext chains, and three opt-in ownership models (UniqueHandle, SharedHandle, vk::raii) while deliberately adding no synchronization model of its own.

FieldValue
LanguageC++ (C++11 baseline; designated initializers need C++20, std::expected C++23)
LicenseApache-2.0 (repository/generator); the generated vulkan/*.hpp headers are Apache-2.0 OR MIT
RepositoryKhronosGroup/Vulkan-Hpp
Documentationdocs/Usage.md · docs/Handles.md · RAII programming guide
CategoryThin / generated binding
First release2016 (developed at NVIDIA, adopted as the official Khronos C++ binding; in the LunarG SDK)
Latest releasev1.4.353 (newest tag as of June 11, 2026) — tags track the weekly Vulkan spec patch releases 1:1

NOTE

Vulkan-Hpp is the baseline of this survey: it is what "maximum safety with zero runtime cost, but no synchronization help" looks like in practice. Every other subject — ash, vulkanalia, erupted, vulkano, daxa, vuk — either copies one of its mechanisms or exists to fill a gap it leaves open.


Overview

What it solves

The Vulkan C API is verbose and weakly typed: every handle is (on 64-bit) a distinct pointer type but every non-dispatchable handle on 32-bit collapses to uint64_t; enums and flag bits are plain C constants that intermix freely; every extensible struct must have its sType set by hand and its pNext chain assembled from untyped void *; every array is a count-plus-pointer pair; and every function returns a bare VkResult the caller must remember to check. None of these mistakes is caught before the validation layers — or the GPU — sees them.

Vulkan-Hpp closes exactly this class of bugs at compile time and stops there. It is generated wholesale from vk.xml (the machine-readable Vulkan registry), so it covers the entire API surface — core, every extension, and the vulkan_video headers — and re-emits it as scoped enums, type-safe flag bitmasks, one strong class per handle type, structs whose sType is pre-set and whose pNext chains are typed (see StructureChain), and member functions on the handle that logically owns the call (device.createBuffer(...) instead of vkCreateBuffer(device, ...)).

Design philosophy

The first sentence of the README is the whole contract:

"Vulkan-Hpp provides header-only C++ bindings for the Vulkan C API to improve the developer experience with Vulkan without introducing run-time CPU costs."

"Without run-time CPU costs" is enforced structurally: a vk::Buffer is asserted to be layout-identical to a VkBuffer (the generated vulkan_static_assertions.hpp checks sizeof( VULKAN_HPP_NAMESPACE::Instance ) == sizeof( VkInstance )"handle and wrapper have different size!" — for every handle and struct), so arrays of wrappers can be reinterpret_cast to arrays of C handles and the C ABI is reachable from any point. Everything that does cost something — std::vector-returning enumeration wrappers, exception throwing, smart handles, the RAII layer — is a separately opt-in/opt-out layer controlled by macros (docs/Configuration.md): VULKAN_HPP_NO_EXCEPTIONS, VULKAN_HPP_NO_SMART_HANDLE, VULKAN_HPP_NO_CONSTRUCTORS, VULKAN_HPP_DISPATCH_LOADER_DYNAMIC, and ~30 more.

What it deliberately does not attempt: any model of GPU-CPU or queue synchronization, any resource state tracking, any use-after-free protection beyond what RAII scoping gives. Those are left to the validation layers and to higher-level libraries (daxa, vuk, tephra).


How it works

Binding generation & API coverage

The generator lives in-tree under generator/: VkXMLParser.cpp (~3,300 lines) parses vk.xml with tinyxml2 into a typed model, and VulkanHppGenerator.cpp (~15,000 lines, 686 KB) emits the entire vulkan/ directory: vulkan.hpp (core types, StructureChain, dispatch loaders, exceptions), vulkan_enums.hpp, vulkan_structs.hpp, vulkan_handles.hpp, vulkan_funcs.hpp, vulkan_raii.hpp, vulkan_shared.hpp, vulkan_static_assertions.hpp, vulkan_format_traits.hpp, vulkan_extension_inspection.hpp, vulkan_hash.hpp, vulkan_to_string.hpp, and the C++20 named module vulkan.cppm (import vulkan_hpp;). The Vulkan-Headers repo is a git submodule, and CI re-generates and tags a release for every weekly spec patch — coverage is total and lag is days, not months.

Which registry metadata survives into the generated types is the interesting question:

vk.xml attributeFate in Vulkan-Hpp
structextendsSurvives — one StructExtends<X, Y> trait specialization per legal extension pair (1,213 in vulkan.hpp as of v1.4.353); powers compile-time pNext validation
allowduplicateSurvives — a per-struct static bool const allowDuplicate consumed by StructureChainValidation
len (count/pointer pairing)Survives — count+pointer parameter pairs merge into a single ArrayProxy parameter or a returned std::vector
successcodes / errorcodesSurvives — drives the per-function return type (value vs ResultValue) and the typed exception hierarchy
optionalSurvives — optional pointer parameters become vk::Optional<T> / default arguments
externsyncDoes not survive — see below

The externsync story is the survey's cleanest negative finding. The parser dutifully reads and schema-validates the attribute — VkXMLParser.cpp checks param.externSync.starts_with( "maybe:" ) forms and reports "param <...> has unknown value <...> for attribute externsync" on malformed input — and parseImplicitExternSyncParamsParam is literally an empty validator:

cpp
// generator/VkXMLParser.cpp — the implicitexternsyncparams content is checked and discarded
void parseImplicitExternSyncParamsParam( tinyxml2::XMLElement const * element )
{
  int const line = element->GetLineNum();
  checkAttributes( "vk.xml", line, getAttributes( element ), {}, {} );
  checkElements( "vk.xml", line, getChildElements( element ), {} );
}

VulkanHppGenerator.cpp — the file that actually emits code — contains zero references to externSync. The registry's machine-readable external-synchronization contract (which parameters must be externally synchronized per command, including the maybe: and host-access variants) is parsed for XML hygiene and then thrown away. No generated type, attribute, comment, or assertion reflects it.

Handle lifetime & ownership model

Vulkan-Hpp ships four handle families, escalating in ownership semantics (docs/Handles.md):

  1. Plain vk:: handles"thin wrappers around the Vulkan C handles" that "provide type safety and convenience functions, but do not manage the lifetime of the underlying Vulkan resources." Trivially copyable, same size as the C handle, no destructor logic. Lifetime is entirely manual, exactly as in C.
  2. vk::UniqueHandlestd::unique_ptr-style scope ownership, created via parallel *Unique factory functions (device.createBufferUnique(...)). The deleter (vk::detail::ObjectDestroy<OwnerType, Dispatch>, vulkan.hpp) stores the owning handle, the vk::AllocationCallbacks pointer, and a dispatcher reference — the docs are upfront that this means "additional memory overhead, and function pointer chain dereferencing during destruction."
  3. vk::SharedHandle (vulkan_shared.hpp) — shared_ptr-style reference counting that also retains the parent: "the parent handle will not be destroyed until all child resources are deleted." This is the only family that addresses the destroy-parent-before-child ordering bug. The docs warn verbatim: "Shared handles are not thread-safe. Multi-threaded access to the same vk::SharedHandle instance must be synchronised by the user."
  4. vk::raii handles (vulkan_raii.hpp, guide) — "vulkan_raii.hpp is a C++ layer on top of vulkan.hpp that follows the RAII-principle": the constructor performs vkCreate*/vkAllocate*, the destructor performs vkDestroy*/vkFree*. RAII objects owning destructible resources are "just movable, but not copyable"; non-owning ones (vk::raii::PhysicalDevice) are copyable. Each handle stores its parent, allocator, and a dispatcher pointer — e.g. vk::raii::Buffer carries m_device, m_buffer, m_allocator, m_dispatcher (vulkan_raii.hpp, class Buffer). The entry point is vk::raii::Context, a class with "no counterpart in either the vk namespace or the pure C-API" that loads vkGetInstanceProcAddr and bootstraps per-instance and per-device dispatchers. release() and clear() are the escape hatches back to manual management.

None of the four families is a linear type: C++ cannot forbid use-after-move or use-after-destroy, so a dangling plain vk::Buffer copy of a destroyed UniqueHandle/raii handle compiles and crashes exactly as in C. Parent/child destruction ordering is unchecked except under SharedHandle.

Synchronization safety

This dimension is explicitly absent, by design. Vulkan-Hpp wraps vk::Fence, vk::Semaphore, vk::Event, and the timeline-semaphore structs (vk::TimelineSemaphoreSubmitInfo participates in StructureChain like any other extension struct) as plain typed handles with member functions — the protocol of when to signal, wait, and insert barriers is byte-for-byte the C API's manual protocol. There is:

  • no render/task graph and no automatic barrier or layout-transition derivation (contrast daxa, vuk, tephra);
  • no runtime tracking of resource states, queue-family ownership, or submission order (contrast vulkano, wgpu);
  • no compile-time distinction between externally-synchronized and internally-synchronized commands — a direct consequence of externsync being discarded by the generator. commandBuffer.draw(...) and a thread-safe call like device.getQueue(...) have identical type signatures and identical (none) host-synchronization requirements expressed in the types.

The implicit position, consistent with the README's no-runtime-cost charter, is that synchronization correctness is the job of the Khronos validation layers and synchronization validation, which Vulkan-Hpp neither integrates with nor duplicates. For a survey of bindings that do encode synchronization, see the comparison.

Type-system techniques

The techniques are classic C++ template machinery — no typestate, no linear ownership — but applied thoroughly:

  • Strong handle types. One class per VkHandle with objectType/debugReportObjectType constants and CType typedefs; mixing a vk::Buffer where a vk::Image is expected is a compile error. Caveat: on 32-bit targets non-dispatchable C handles all alias uint64_t, so cross-type conversions are only blocked when VULKAN_HPP_TYPESAFE_CONVERSION semantics permit — the docs note "32-bit vulkan is not typesafe for non-dispatchable handles, so we don't allow copy constructors on this platform by default."

  • Scoped enums + vk::Flags<BitType>. VK_IMAGE_TYPE_2D becomes vk::ImageType::e2D; flag bits get a per-enum Flags bitmask template so OR-ing bits from unrelated enums no longer compiles.

  • Typed pNext chains. vk::StructureChain<ChainElements...> derives from std::tuple<ChainElements...> and static_asserts, at construction, that every element legally extends the head struct and that duplicates appear only where vk.xml allows them (vulkan.hpp):

    cpp
    // vulkan/vulkan.hpp — compile-time pNext chain validation
    template <size_t Index, typename... ChainElements>
    struct StructureChainValidation
    {
      using TestType          = typename std::tuple_element<Index, std::tuple<ChainElements...>>::type;
      static bool const valid = StructExtends<TestType, typename std::tuple_element<0, std::tuple<ChainElements...>>::type>::value &&
                                ( TestType::allowDuplicate || !StructureChainContains<Index - 1, TestType, ChainElements...>::value ) &&
                                StructureChainValidation<Index - 1, ChainElements...>::valid;
    };
    // ...
    VULKAN_HPP_STATIC_ASSERT( StructureChainValidation<sizeof...( ChainElements ) - 1, ChainElements...>::valid,
                              "The structure chain is not valid!" );

    The constructor wires the pNext pointers automatically; get<T>() retrieves a member struct; unlink<T>()/relink<T>() toggle a member in and out of the chain at runtime (with static_assert-checked membership). Per docs/Usage.md: "only chains which are valid according to the Vulkan specification can be created, which is verified at compile time." Query functions have chain-returning overloads (device.getBufferMemoryRequirements2<...>() returning a populated StructureChain).

  • sType elimination. Every generated struct's constructor (or default member initializer) pre-sets sType; with VULKAN_HPP_NO_CONSTRUCTORS and C++20 the structs become aggregates usable with designated initializers (.pApplicationName = AppName, .applicationVersion = 1), trading the builder-ish setter chains for plain-text member naming.

  • ArrayProxy / ArrayProxyNoTemporaries. A borrowed (pointer, count) view that accepts a single value, a C array, std::initializer_list, std::array, or std::vector — collapsing every len-annotated count/pointer pair into one parameter. The NoTemporaries variant is used in struct constructors because "that pointer is assumed to be valid throughout the lifetime of the structure" (docs/Usage.md) — a lifetime hint encoded as overload-set restriction, the closest the library gets to borrow checking.

  • Dispatch as a template parameter. Every function takes a trailing Dispatch const & defaulting to VULKAN_HPP_DEFAULT_DISPATCHER — either vk::detail::DispatchLoaderStatic (direct calls into the loader's exported prototypes) or vk::detail::DispatchLoaderDynamic, which "pre-fetches all function pointers known to the library" in a three-step init() (loader → instance → device). The vk::raii namespace instead bakes per-object dispatchers in: "For each instantiated vk::raii::Device, the device-specific Vulkan function pointers are resolved" (RAII guide) into a heap-allocated detail::DeviceDispatcher the handles share — skipping the loader's per-call dispatch trampoline, which is the standard advice for multi-GPU and for shaving call overhead.

  • Auxiliary comptime metadata. vulkan_format_traits.hpp exposes constexpr per-format block size/texel queries; vulkan_extension_inspection.hpp exposes extension dependency/promotion data — registry knowledge surfaced as compile-time functions.

Overhead & escape hatches

The cost model is tiered, and each tier is optional:

LayerRuntime cost
Plain handles, enums, structsZero — layout-asserted identical to C; calls inline to the C entry points
Enhanced-mode functionsA std::vector heap allocation per enumeration call; exception machinery on error paths
StructureChainZero beyond the std::tuple layout; validation is static_assert-only; link/unlink are pointer writes
UniqueHandleDeleter storage (owner + allocation callbacks + dispatcher) per handle
SharedHandleReference-count control block per handle; parent retention
vk::raiiParent + allocator + dispatcher pointer per handle; one heap-allocated full function-pointer table per Context/Instance/Device
DispatchLoaderDynamicOne big table of every known PFN_vk*; eliminates the loader trampoline (commonly a gain)

There are no locks, no hash maps, no per-resource state tracking anywhere — the most expensive thing in the library is a std::vector return. Escape hatches are pervasive: every wrapper converts to/from its C handle (static_cast<VkBuffer>( buffer )), the layout static_asserts license reinterpret_cast of whole arrays, C-style output-parameter overloads coexist with the std::vector-returning ones, raii handles expose release(), and because the headers are additive over vulkan.h, raw C calls can be mixed in freely at any point.

Error handling & validation integration

The generator turns successcodes/errorcodes into a per-function policy (docs/Usage.md): a function whose only success code is VK_SUCCESS returns its value directly and throws on error — a typed exception per VkResult (e.g. vk::OutOfDeviceMemoryError), all deriving from vk::SystemError so errc-style handling works. Functions with multiple success codes return a ResultValue<T>. Three alternative regimes exist:

  • VULKAN_HPP_NO_EXCEPTIONS — returns vk::ResultValue<T> everywhere (struct of vk::Result + value; structured bindings recommended), with VULKAN_HPP_ASSERT (default <cassert>, overridable) guarding unexpected codes.
  • VULKAN_HPP_USE_STD_EXPECTED (C++23) — vk::ResultValue<T>::type becomes std::expected<T, vk::Result> with monadic and_then/or_else. Under VULKAN_HPP_RAII_NO_EXCEPTIONS the raii constructors (which would have to throw) are replaced by factory functions returning std::expected<vk::raii::Object, vk::Result>.

This maps cleanly onto the D Expected idiom this repo already standardizes on. Validation-layer integration is otherwise nil: Vulkan-Hpp generates the DebugUtilsMessengerEXT plumbing like any other extension but performs no validity checking of its own at runtime — the division of labor is compile-time structure (Hpp) vs runtime behavior (validation layers).


Strengths

  • Total, perpetually fresh API coverage — generated from vk.xml and re-released for every weekly spec patch; extensions appear in the binding the week they appear in the registry.
  • Genuinely zero-cost core — layout-asserted handle/struct parity with C, header-only inlining, all conveniences opt-in; the C ABI is one static_cast away at every point.
  • StructureChain is the gold standard for typed pNext — 1,213 generated StructExtends pairs make invalid chains a compile error, with runtime unlink/relink flexibility on top.
  • Four ownership models on one substrate lets a codebase choose its safety/overhead point per resource (plain in the hot path, raii at the architecture level).
  • Per-device dispatcher in vk::raii removes the loader trampoline — the "safer" layer is also the faster call path.
  • Configurable error-handling spectrum (exceptions → ResultValuestd::expected) plus a C++20 named module (import vulkan_hpp;) that tames the multi-megabyte header's compile-time cost.

Weaknesses

  • No synchronization model at all — barriers, layouts, semaphores, fences, timeline waits, and queue-family ownership transfer are exactly as manual and as silently wrong as in C; the types do not even mark which commands require external synchronization.
  • externsync registry metadata is parsed and discarded — the one piece of machine-readable host-threading contract in vk.xml never reaches the type system, docs, or even comments.
  • No temporal safety — handles are freely copyable values; use-after-destroy, destroy-order, and dangling-pNext/ArrayProxy pointer bugs all compile (only ArrayProxyNoTemporaries and SharedHandle nibble at the edges).
  • Compile-time weightvulkan.hpp is ~27,000 lines before vulkan_structs.hpp/vulkan_funcs.hpp; without the C++20 module, including it everywhere is a build-time tax.
  • Template-error ergonomics — a wrong StructureChain fails with a static_assert ("The structure chain is not valid!") but locating which element offends is left to the reader of a tuple-instantiation backtrace.
  • The raii layer's heap-allocated dispatchers and parent pointers make it strictly heavier than plain handles, and vk::raii types "are not compatible" with the UniqueHandle/SharedHandle families — mixing models mid-codebase is awkward.

Key design decisions and trade-offs

DecisionRationaleTrade-off
Generate everything from vk.xml, release per spec patchTotal coverage, zero lag, registry metadata (structextends, len, result codes) drives typesGenerator is a 15k-line bespoke C++ program; binding fidelity is bounded by what it chooses to read
Layout-identical wrappers + static_assert parityTrue zero overhead; reinterpret_cast interop; C escape hatch everywhereNo room for fat pointers/generation counters → no temporal safety in the core types
pNext validation via StructExtends traits + static_assertInvalid chains caught at compile time at zero runtime costErrors surface as tuple-template noise; chain composition fixed at compile time (modulo unlink)
Discard externsync during generationHost-sync contracts don't fit C++'s type system cheaply; keeps generator and API simpleThe only machine-readable threading contract in the registry is lost; no const/marker distinction
Ownership as opt-in layers (Unique/Shared/raii) over plainPay-for-what-you-use; hot paths keep raw-handle costFour coexisting, partially incompatible models; safety depends on which one a codebase picks
Dispatch as a defaulted template parameter + per-device raii tablesStatic, dynamic, and per-object dispatch coexist; raii skips the loader trampolineVULKAN_HPP_DEFAULT_DISPATCHER global storage macro dance; raii dispatchers heap-allocate
Exceptions by default, ResultValue/std::expected by macroIdiomatic C++ default; embedded/no-exceptions and monadic styles still servedMacro-flavored API surface: the same function has three signatures across the ecosystem

Sources