Skip to content

Shared Concepts: Vulkan Safety & Binding Vocabulary

The shared vocabulary of this survey: every synchronization primitive, architecture pattern, and type-system technique the deep-dives reference, each defined once, grounded in real examples from the surveyed systems, and assessed on the two axes the whole tree cares about — what it buys in safety and what it costs at runtime.

Scope. This is a reference document, not a library deep-dive. Terms are grouped into five clusters: the two synchronization domains Vulkan defines, the device-side primitive set wrappers must model, the architecture patterns built above the raw API, the type-system techniques used to encode rules statically, and binding generation from vk.xml. For the registry/spec/validation-layer ground truth behind the synchronization terms, see sync-validation; for how each surveyed system combines these concepts, see the per-system deep-dives and the comparison.

Last reviewed: June 11, 2026


The two synchronization domains

Vulkan's correctness rules split into two domains that wrapper designs routinely conflate, with very different machine-readability and very different type-system mappings (the split is developed in full in sync-validation):

DomainQuestion it answersMachine-readable?Natural static encoding
Host synchronizationWhich CPU threads may touch which handles concurrentlyYes — externsync in vk.xmlExclusive borrows (&mut, DIP1000 scope ref)
Device synchronizationWhich GPU operations happen-before whichNo — prose Valid Usage onlyNone mainstream; hence graphs and tracking

External synchronization & externsync

Definition. A parameter of a Vulkan command is externally synchronized when the application — not the driver — must guarantee exclusive access to it for the call's duration. From the spec's Threading Behavior chapter (verbatim, via fundamentals.adoc):

"All commands support being called concurrently from multiple threads, but certain parameters, or components of parameters are defined to be externally synchronized. This means that the caller must guarantee that no more than one thread is using such a parameter at a given time."

The requirement is machine-encoded in vk.xml as the externsync attribute — 402 attribute instances on main as of June 11, 2026, in four value forms (true, maybe, member-path expressions like pNameInfo->objectHandle, and maybe: member paths); see sync-validation § What vk.xml encodes for the full grammar. Violations are plain undefined behavior — there is no VK_ERROR_RACE_CONDITION.

Why it matters. This is a textbook exclusive-borrow obligation: externsync="true" on a parameter is semantically &mut/inout, and externsync on every vkDestroy* handle is semantically a consuming move. It is the one piece of Vulkan safety metadata that is complete, trustworthy (the spec's own host-sync tables are generated from it), and mechanically consumable by a binding generator — at zero runtime cost, since the encoding would live entirely in signatures.

Who uses it. Almost nobody — the survey's central negative finding. The attribute is parsed-and-discarded by Vulkan-Hpp's generator, never read by ash's, vulkanalia's, or vulkan-zig's, dropped with a literal (* TODO *) by olivine, and absent from erupted, Silk.NET, and every JVM binding surveyed. haskell-vulkan is the high-water mark: the requirement survives as generated Haddock prose ("Host access to queue must be externally synchronized"), still not as types. vulkano discharges it at runtime instead (internal parking_lot mutex on Queue, !Send recording command buffers), and daxa/Tephra restate it as per-type documentation.

Implicit vs explicit host synchronization

Definition. Vulkan objects fall into three host-threading classes:

  1. Internally synchronized — the driver locks internally; any thread may use the handle concurrently (e.g. VkDevice for most commands, vkAllocateDescriptorSets when the pool has VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT unset is not one of them — the pool is the sync unit).
  2. Explicitly externally synchronized — the parameter carries an externsync tag (e.g. commandBuffer in vkBeginCommandBuffer).
  3. Implicitly externally synchronized — the exclusivity obligation falls on an object not named in the call at all. From fundamentals.adoc (verbatim): "when a commandBuffer parameter needs to be externally synchronized, it implies that the commandPool from which that command buffer was allocated also needs to be externally synchronized." vk.xml records these as 7 free-text implicitexternsyncparams blocks — prose, not machine-checkable structure.

Why it matters. The implicit class contains the single most architecturally important host rule (command buffer ⇒ its pool: one recording thread per VkCommandPool), and because it is prose, every consumer must hand-curate it. It is also the rule that shapes wrapper APIs most visibly: it is why thread-safe wrappers organize command recording around per-thread pools.

Who uses it. The Khronos thread-safety validation layer special-cases command pools (see sync-validation); vulkano runtime-locks the pool inside its command-buffer builder; Tephra's documented threading rule is literally "one thread per pool"; daxa's CommandRecorder is documented as "must be externally synchronized" while Device is internally synchronized. No surveyed binding encodes the pool coupling in types — the sync-validation page sketches the DIP1000/pool-branded-recorder encoding a D library could use.


Device-side synchronization primitives

The five-primitive vocabulary every wrapper either exposes raw, wraps in builders, or compiles away. All are defined normatively in the spec's Synchronization and Cache Control chapter; the per-barrier (stage, access) shape described below is the VK_KHR_synchronization2 formulation (core since Vulkan 1.3), which every modern auto-sync layer surveyed (daxa, vuk, and jcoronado by mandate) targets exclusively.

Pipeline barriers

Definition. vkCmdPipelineBarrier2 records an intra-queue execution + memory dependency: everything matching srcStageMask/srcAccessMask before the barrier happens-before everything matching dstStageMask/dstAccessMask after it, with caches flushed/invalidated for the named access types. Under synchronization2 each VkMemoryBarrier2/VkBufferMemoryBarrier2/VkImageMemoryBarrier2 carries its own self-contained (stage, access) pair — the atomic unit syncval validates and graph compilers emit.

Why it matters. Barriers are where most synchronization bugs live (missing, wrong scope, or — the silent performance bug — too wide). They are pure recorded commands: zero host-side cost beyond recording, so the entire design question is who computes them. Hand-written (ash, erupted, Vulkan-Hpp), runtime-derived (wgpu, vulkano gen-2, Tephra's job tier), or graph-compiled (daxa, vuk — vuk has no user-facing barrier API at all). The minimal mitigation short of any automation — still hand-placed, but no longer hand-assembled — is the simplified barrier vocabulary pattern below.

Events (split barriers)

Definition. VkEvent splits a barrier in two: vkCmdSetEvent2 marks the source point (carrying its own VkDependencyInfo since sync2), vkCmdWaitEvents2 the destination, letting unrelated work execute between them.

Why it matters. Events are the fine-grained-overlap tool and the long tail of every wrapper: maximum scheduling freedom, minimum abstraction support. No surveyed system automates themdaxa exposes them manually outside its graph, vuk's README lists split barriers as an unchecked checkbox, wgpu and vulkano never emit them. Their universal absence is a finding: per-range event placement is exactly the optimization a whole-frame graph compiler is positioned to do and none currently does.

Fences

Definition. VkFence is a queue→host signal: passed to vkQueueSubmit, signaled when the submission's work completes, waited on with vkWaitForFences. The canonical gate for reusing per-frame resources.

Why it matters. Fences are the primitive behind every deferred-destruction and frames-in-flight scheme. Wrappers either expose them raw (thin bindings), wrap them as waitable tokens (vulkano's FenceSignalFuture, its taskgraph's fence-gated Flight frames), or hide them entirely behind timeline counters (wgpu's wgpu_hal::Api::Fence is a timeline semaphore when available, with a VkFence-pool fallback; vuk's API has no user-visible fence at all).

Binary semaphores

Definition. VkSemaphore (binary type) orders work between queues and with the presentation engine: signaled by one submission, waited by another, automatically reset on wait completion. Swapchain acquire/present is their irreducible use: vkAcquireNextImageKHR and vkQueuePresentKHR accept only binary semaphores.

Why it matters. The strict signal-then-wait, one-shot protocol is a typestate-shaped contract no surveyed system types (a double-wait compiles everywhere). Auto-sync layers internalize them: vuk and daxa derive swapchain semaphores in their graph executors; wgpu hides acquire/present semaphores inside its hal (including a two-semaphore RelaySemaphores alternation working around a Mesa hang).

Timeline semaphores

Definition. VK_KHR_timeline_semaphore (core in Vulkan 1.2) gives a semaphore a monotonically increasing 64-bit payload: any queue or host thread waits for "value ≥ N", any submission signals a higher value, and one object replaces whole families of binary semaphores and fences.

Why it matters. The monotonic counter is the natural "GPU progress epoch" primitive, and the surveyed systems converge on it as infrastructure: daxa gates its zombie lists on per-queue timeline values, vuk rides all GPU/host ordering on one timeline semaphore per QueueExecutor (SyncPoint{executor, visibility}), Tephra unifies job IDs and timeline values into one device-wide counter, wgpu implements its Fence as one, and vulkano's taskgraph uses them for cross-queue edges. Host-side they map cleanly to futures/awaitables — and to D, a timeline value is the obvious epoch tag for @safe deferred reclamation.

Image layout transitions

Definition. Every VkImage subresource is, at each point on the GPU timeline, in a layout (UNDEFINED, GENERAL, COLOR_ATTACHMENT_OPTIMAL, READ_ONLY_OPTIMAL, PRESENT_SRC_KHR, …) that licenses certain accesses and may imply a different physical memory arrangement. Transitions are expressed as the oldLayout/newLayout fields of an image memory barrier — i.e. layout is a hidden state machine threaded through barriers.

Why it matters. Layout is device-side typestate with no host-side representation: the C API gives the program no value that holds an image's current layout, so wrappers must reconstruct it. Strategies span the whole design space: track it per subresource at runtime (wgpu's TextureUses include layout-relevant states; vulkano; Tephra's access maps), derive it from declared accesses (vuk's Access values each imply a stage+access+layout triple, with read groups merged into "a merged layout (TRANSFER_SRC_OPTIMAL / READ_ONLY_OPTIMAL / GENERAL)", per src/IRPasses.cpp), or abolish the state machine: daxa release 3.3 (November 2025) reduced supported layouts to UNDEFINED/GENERAL/PRESENT_SRC, eliminating the transition bug class by fiat on its modern-GPU-only floor. Thin bindings (ash, erupted, …) leave layout entirely to the programmer and the validation layers.

Queue-family ownership transfer (QFOT)

Definition. A buffer or image created with VK_SHARING_MODE_EXCLUSIVE has its contents owned by one queue family at a time; moving it (e.g. transfer queue → graphics queue) requires a matching release barrier on the source queue and acquire barrier on the destination queue, with equal srcQueueFamilyIndex/dstQueueFamilyIndex fields. VK_SHARING_MODE_CONCURRENT waives the protocol at a (hardware-dependent) bandwidth cost.

Why it matters. QFOT is a two-phase, cross-queue protocol whose halves live in different command buffers — maximally hostile to local reasoning, invisible to the registry (the indices are plain uint32_ts in vk.xml), and unchecked by thin bindings. The automation strategies: vuk emits QFOT only when its compiled streams cross queue families and routes buffers via CONCURRENT sharing to dodge it entirely; daxa's TaskGraph derives transfers alongside its cross-queue timeline-semaphore sync; Tephra broadcasts access-map state and ownership via message passing between queues on cross-queue export; wgpu deletes the concept — WebGPU has a single queue, so QFOT is absent by construction.

Hazards (RAW/WAR/WAW) — syncval's taxonomy

Definition. A hazard is a pair of memory accesses to overlapping resource ranges without a sufficient dependency chain between them. The Khronos synchronization validation layer (syncval, part of VK_LAYER_KHRONOS_validation) defines five kinds in docs/syncval_usage.md (first three verbatim):

HazardDefinition
RAW (Read-after-write)"Occurs when a subsequent operation uses the result of a previous operation without waiting for the result to be completed."
WAR (Write-after-read)"Occurs when a subsequent operation overwrites a memory location read by a previous operation before that operation is complete (requires only execution dependency)."
WAW (Write-after-write)"Occurs when a subsequent operation writes to the same set of memory locations (in whole or in part) being written by a previous operation."
WRW (Write-racing-write)Write-racing-write between unsynchronized subpasses/queues
RRW (Read-racing-write)Read-racing-write between unsynchronized subpasses/queues

Detection is per-resource-range most-recent-access tracking (ResourceAccessState in interval trees) — see sync-validation for the design and its blind spots.

Why it matters. This taxonomy is the survey's common currency for "what sync safety means": it is exactly the property set that usage tracking checks dynamically, graph compilers make unrepresentable, and no surveyed type system proves statically. wgpu's usage-scope rule turns intra-pass write/write and read/write conflicts into validation errors — WRW/RRW caught at runtime with zero false positives; vulkano's gen-2 tracker and Tephra's access maps mirror syncval's per-range state; daxa and vuk derive barriers such that RAW/WAR/WAW cannot occur between declared accesses. Syncval itself remains the ground-truth oracle below every wrapper: a layer that emits sync2 barriers can be validated by it, which is how the graph libraries test their compilers.


Architecture patterns

Simplified barrier vocabulary (named usage states)

Definition. A small closed enum of named usage states — one value per way a resource is actually used (color-attachment write, any-shader sampled read, transfer source, present, …) — each of which expands by table lookup to a correct (stageMask, accessMask, imageLayout) tuple, so a barrier is declared as previous accesses → next accesses instead of five hand-assembled masks. The pattern's reference implementation is simple_vulkan_synchronization by Tobias Hector — a Vulkan specification author and the author of VK_KHR_synchronization2 — a single-header C library (thsvs_simpler_vulkan_synchronization.h) whose README states the move (verbatim, README):

"Rather than the complex maze of enums and bitflags in Vulkan - many combinations of which are invalid or nonsensical - this library collapses this to a much shorter list of 40 distinct usage types, and a couple of options for handling image layouts."

ThsvsAccessType (e.g. THSVS_ACCESS_COLOR_ATTACHMENT_WRITE, THSVS_ACCESS_ANY_SHADER_READ_UNIFORM_BUFFER_OR_VERTEX_BUFFER) "defines all potential resource usages in the Vulkan API" (header); thsvsGetAccessInfo expands one to the {stageMask, accessMask, imageLayout} triple, and thsvsCmdPipelineBarrier/thsvsCmdWaitEvents wrap the raw commands. Image layout handling collapses to three modes (THSVS_IMAGE_LAYOUT_OPTIMAL, _GENERAL, _GENERAL_AND_PRESENTATION) — note the family resemblance to daxa's later layout abolition (above). The Rust port is Graham Wihlidal's vk-sync over ash: an AccessType enum (ColorAttachmentWrite, AnyShaderReadSampledImageOrUniformTexelBuffer, …) consumed by GlobalBarrier/BufferBarrier/ImageBarrier structs whose previous_accesses/next_accesses are slices of AccessType — dormant upstream since v0.1.6 (July 14, 2019) but kept current by community forks (vk-sync-fork).

Why it matters. This is the minimal type-safety move for device synchronization: it does not automate barrier placement (unlike graphs or tracking) but makes barrier contents correct by construction, collapsing an error-prone 5-tuple — where the invalid combinations vastly outnumber the valid ones and a too-wide mask is a silent performance bug — into one semantic value per endpoint. The cost is a constant table lookup (CTFE-able in D: zero runtime cost); the residual constraints are themselves enum-shaped (the C header asserts that a write access "should appear on its own" — at most one write per barrier endpoint), and Hector accepts a deliberate expressiveness loss: "Execution only dependencies cannot be expressed" (README), against the claim that the enum still "expresses 99% of what you'd actually ever want to do in practice." The survey's graph systems internalized exactly this vocabulary as their declaration language: vuk's Access values each expand via to_use(Access) to a ResourceUse { stages, access, layout } triple, and daxa's task attachments declare access-type + stage pairs — the named-usage-state enum is the unit vocabulary a graph compiler consumes, with placement automated on top. The survey's production instance is NVRHI: its D3D12-style ResourceStates enum-class bitflags are the entire synchronization vocabulary the API exposes, lowered to batched vkCmdPipelineBarrier2 calls — while blade marks the opposite pole, abolishing the per-resource vocabulary altogether in favor of one catch-all global barrier between passes. What the pattern does not give is hazard freedom: it names a dependency edge correctly but cannot ensure the edge exists where needed or is ordered correctly — the RAW/WAR/WAW set stays syncval's territory under manual placement.

Render graph / task graph / frame graph

Definition. A frame's GPU work declared as a DAG of passes/tasks, each naming the resources it reads and writes (and how), which a graph compiler lowers to concrete command-buffer recordings: barriers, layout transitions, QFOT, semaphores, queue assignment, and optionally pass reordering and transient-memory aliasing. The pattern was popularized as "FrameGraph" by Frostbite (O'Donnell, GDC 2017); the canonical Vulkan treatment is Hans-Kristian Arntzen's render-graph deep-dive, implemented in his Granite engine — the category's 2017 ancestor and a deep-dive of this survey. "Render graph", "task graph", and "frame graph" are used interchangeably in this tree; the surveyed systems prefer "task graph" when the nodes are general (compute/transfer) rather than render passes.

Why it matters. The graph is the field's answer to the fact that device-side synchronization needs whole-frame knowledge: with every access declared up front, the RAW/WAR/WAW hazard set is computable before anything executes, and the cost is amortized into a compile step rather than paid per command. The key axis among implementations is when that compile happens:

SystemDeclarationCompile cadence
Granite RenderGraphString-named per-pass resource I/O, wired at graph buildbake() only on graph-topology change — per-frame replay of precomputed physical passes
daxa TaskGraphRuntime attachment lists per taskRecord once, execute many — analysis front-loaded at complete()
vukIn the pass's C++ type: Arg<T, Access, tag> via VUK_IA/VUK_BAPer submit, mitigated by partial evaluation (executed nodes "morph into acquire")
vulkano-taskgraphnode.buffer_access(id, AccessTypes) per nodeCompile once (unsafe, currently unvalidated), execute per frame against a ResourceMap
TephraOne export declaration per write (not per read)Explicitly "a render graph that does not reorder passes" — incremental, at job submit

A second hallmark is virtual resources: graph-time identities (daxa's TaskBuffer/TaskImage, vulkano's phantom-typed Id<T> with a virtual-resource tag bit) bound to physical resources only at execution, enabling transient aliasing and N-frames-in-flight reuse. The pattern's cost is explicitness — accesses are declared, not inferred, and a wrong declaration is a silent bug unless the compiler validates it (vulkano's taskgraph currently does not; vuk validates at graph-compile time with std::source_location provenance).

Auto-sync via per-resource usage tracking

Definition. The inverse strategy: the wrapper infers synchronization by recording, for every buffer and image subresource, its most recent usage state, and diffing state at each new use to emit the minimal barrier — effectively reimplementing syncval's ResourceAccessState model inline, as a mandatory production component instead of a debug layer. wgpu's wgpu-core/src/track module states the job (verbatim, track/mod.rs):

"These structures are responsible for keeping track of resource state, generating barriers where needednd [sic] making sure resources are kept alive until the trackers die."

Why it matters. Tracking requires no up-front declarations (maximum ergonomics) but pays per-command runtime cost, every frame, with no whole-frame view — it cannot reorder or globally batch barriers. The survey's best-grounded overhead numbers come from this pattern: wgpu's maintainers estimate 5–10% typical CPU overhead over raw hal (~2× worst case, discussion #2080), and vulkano's gen-2 HashMap<Arc<Buffer>, RangeMap<DeviceSize, BufferState>> per command buffer is the measured cost that motivated its migration to a task graph. The field's trajectory is the survey's clearest signal: vulkano moved from inferred tracking to declared graphs; Tephra split the difference (track coarse job commands, never analyze per-draw command lists, because that "would have unacceptable performance overhead", per its README). V-EZ marks the historical maximum — per-command record-time inference with no opt-out, abandoned within six months — while blade, by wgpu's original author, deletes tracking outright in favor of one global catch-all barrier. Full tracking survives in production in wgpu, as the price of WebGPU's declare-nothing portability contract, and in NVRHI — the camp's production survivor — which tracks D3D12-style named states per command list behind a graded per-resource opt-out ladder.

Bindless descriptors

Definition. Instead of binding small per-draw descriptor sets, the application maintains one huge descriptor table (update-after-bind, via VK_EXT_descriptor_indexing, core in 1.2) and shaders index into it with plain integer IDs passed in push constants or buffers — "bindless" because resources are never individually bound between draws.

Why it matters. Bindless converts descriptor management — a per-draw CPU cost and a classic wrapper pain point — into array indexing, and it changes the safety frontier: the host-side type system loses sight of which resources a draw uses (the index lives in GPU-visible memory), moving use-after-free from "validation layer catches it" to unchecked GPU-side reads. daxa is the survey's bindless-by-default subject: one update-after-bind mega-set indexed by the resource ID's index bits, README pledge "Bindless by default – no descriptor management nor bindings", with the implementation noting the set "does not need external sync given we use update after bind" (src/impl_device.cpp) — and, as the trade-off, "GPU-side stale bindless IDs are unchecked". vuk keeps classic per-draw descriptor hashing with caches; wgpu supports bindless only partially (WebGPU limits).

Deferred destruction

Definition. vkDestroy* while the GPU may still read the resource is UB, so safe wrappers never destroy immediately: a "destroyed" resource enters a queue tagged with the current GPU progress epoch (a timeline-semaphore value, submission index, or frame counter), and is actually freed only once the device's completed-value passes that epoch. daxa names the pattern memorably (include/daxa/device.hpp, verbatim):

"a zombie lives until the gpu catches up to the point of zombification."

Why it matters. Deferred destruction is the practical resolution of the temporal half of handle safety — the half RAII alone cannot deliver, because scope exit on the CPU says nothing about the GPU timeline. Cost character: a per-destroy queue push plus a periodic collection sweep (amortized, allocation-light), against the alternative of per-resource fence waits. Every safety-oriented system surveyed implements a variant: daxa's per-type zombie lists + collect_garbage(), Tephra's job-ID-keyed destruction (at the documented cost of over-extending unused resources' lifetimes), Granite's frame-context-bucketed destruction queues gated on per-context fences, NVRHI's per-queue timeline trackingSemaphore consulted by a once-per-frame runGarbageCollection(), wgpu's per-submission active lists freed when its timeline Fence passes, vulkano's queue-owned Arcs (gen 2) and per-Flight garbage queues with hyaline-style reclamation (taskgraph), and vuk's DeviceFrameResource N-frame ring allocators. Thin bindings have none — in ash, erupted, or vulkan-zig, destroying an in-flight resource compiles silently — and blade and V-EZ omit it by deliberate choice (immediate explicit destroy_* / vezDestroy*).

Memory allocation & VMA (out of the survey spine)

Definition. Vulkan Memory Allocator (VMA, from AMD's GPUOpen) is the de-facto standard allocation layer above vkAllocateMemory: a single-header (vk_mem_alloc.h), MIT-licensed C/C++ library — self-described simply as an "Easy to integrate Vulkan memory allocation library" (README) — that handles memory-type selection from declared usage, block allocation and suballocation (Vulkan caps maxMemoryAllocationCount, so per-resource vkAllocateMemory does not scale), alignment and resource binding, dedicated allocations, and defragmentation. Actively maintained (v3.4.0, June 4, 2026).

Why it matters — and why it is not a spine dimension. Nearly every surveyed wrapper that owns resources delegates to VMA rather than reimplementing it: Tephra is "Built on VMA for all allocations", daxa's device struct holds a VmaAllocator vma_allocator member (src/impl_device.hpp), vulkanalia ships a dedicated vulkanalia-vma crate, and haskell-vulkan's generator produces a sibling VulkanMemoryAllocator package in the same style. That consensus is precisely why this catalog treats memory allocation as out of the survey spine: it is a solved, delegated problem with one dominant implementation, and it is orthogonal to the two axes the tree compares systems on — VMA suballocates device memory but knows nothing of the GPU timeline (freeing an allocation the GPU still reads remains the wrapper's problem, solved by deferred destruction above, not by the allocator) and nothing of hazards (aliased transient memory still needs the barriers a graph compiler or syncval reasons about). Allocation therefore appears in the deep-dives only where it shapes the binding surface — e.g. whether a wrapper's buffer-creation API takes VMA-style usage declarations — not as a per-system analysis dimension.


Type-system techniques

Phantom / branded types

Definition. A type parameter (or generatively minted fresh type) that exists only at compile time to make otherwise-identical representations un-mixable. Phantom refers to the unused parameter (Rust PhantomData, the Haskell wiki's phantom types); branding to using it as an identity tag.

Why it matters. Phantom typing is the cheapest safety in the survey — always zero-cost (erased at compile time) — and the most widely deployed: distinct handle newtypes are what separate VkBuffer from VkImage in every binding better than raw uint64_t. Gradations observed:

  • Per-handle-type branding (the baseline): ash's repr(transparent) newtypes, Vulkan-Hpp's layout-asserted wrapper classes, Tephra's VkObjectHandle<T, VkObjectType>, vulkan-zig's non-exhaustive enum(u64) handles, olivine's generative functors (a fresh abstract type per handle, minted by Make()), vulkan4j's record-per-handle. Negative data points: erupted handles degrade to ulong aliases on 32-bit; LWJGL leaves all non-dispatchable handles as bare longs.
  • Per-resource branding: vulkano-taskgraph's Id<T> slot-map IDs and daxa's TypedImageViewId<VIEW_TYPE> brand IDs by kind (and daxa's by view type), though both stay copyable.
  • Per-value branding (e.g. handle branded by its parent VkDevice) appears nowhere in the survey — wrong-device pairing is a runtime error even in vulkano.

A related zero-cost refinement: olivine's phantom singleton/plural parameter on bitsets statically distinguishes "one flag" from "a union of flags" — the FlagBits-vs-Flags distinction most bindings encode only nominally and vulkan-zig deliberately drops.

Typestate

Definition. Encoding an object's protocol state in its type, so operations valid only in some states simply do not exist on the others — typically by consuming a value and returning a different type (the typestate pattern in Rust). Vulkan is full of latent typestate: command buffers (initial → recording → executable → pending), swapchain images (acquired or not), semaphores (signaled/unsignaled), image layouts.

Why it matters. Typestate is zero-cost and precisely shaped for Vulkan's protocols — and the survey finds it almost entirely unused. The strongest sightings are partial: vulkano's GpuFuture combinator chains (nested concrete types encoding the submission DAG — "typestate-ish", and being retired with the taskgraph migration) and its marker-typed AutoCommandBufferBuilder<L>; daxa's fluent task builder is a weak form (order-suggesting, not type-enforced). No surveyed system types begin/end recording, acquire/present, or semaphore signal/wait as state transitions. The blocker is practical: typestate requires move semantics with use-after-move prevention (affine types), which C++ lacks and which fights Arc-style sharing in Rust wrappers. The gap is a standing invitation for a D design — @disable this(this) plus DIP1000 gives the needed affine moves.

Linear & affine types

Definition. Substructural type systems: a linear value must be consumed exactly once; an affine value at most once. Rust's move semantics are affine (drop is implicit); true linearity ("you must call vkDestroyBuffer") exists in no mainstream surveyed language.

Why it matters. Affinity is the natural encoding of two registry facts: every vkDestroy*/vkFree* marks its handle externsync="true" (destruction = consuming move), and one-shot protocols (typestate transitions) need at-most-once consumption. Observed usage is thin: Rust bindings (ash, vulkanalia) make handles Copy, deliberately forfeiting affine destruction; vulkano wraps everything in Arc (shared, not affine) and recovers temporal safety via deferred destruction instead; Vulkan-Hpp's UniqueHandle/raii types are move-only but C++ cannot reject use-after-move, so "a dangling plain vk::Buffer copy of a destroyed UniqueHandle/raii handle compiles and crashes exactly as in C" (deep-dive). The honest summary: no surveyed system derives affine ownership from the registry's destroy/externsync metadata — each hand-picks where moves apply.

RAII vs bracket-style resource management

Definition. Two idioms for pairing create with destroy. RAII: destruction in the destructor of a scope-owned object (cppreference: RAII) — used by Vulkan-Hpp's vk::raii namespace and UniqueHandles, Tephra's owning handles and Lifeguard, vuk's Unique<T>, jcoronado's AutoCloseable try-with-resources. Bracket style: a higher-order function receives create/destroy and a continuation, guaranteeing the destroy runs after the continuation — haskell-vulkan's generated with* pairs (withInstance hands bracket/ContT the create/destroy pair; Control.Exception.bracket is the canonical form), and Java's Arena.ofConfined() for host memory is bracket-shaped (close the arena ⇒ dangling access throws).

Why it matters. Both give deterministic, exception-safe release at scope exit at zero or near-zero cost, and both share the same two limits: they bind lifetime to lexical scope (awkward for resources whose lifetime is a frame ring or a cache), and they release at CPU scope exit, which is wrong for anything the GPU still references — which is why every RAII layer surveyed either documents the hazard (Vulkan-Hpp: no temporal safety) or backs onto deferred destruction (Tephra, vuk). D offers both idioms natively (struct destructors + @disable this(this); scope(exit)); the design question for sparkles:vulkan is not RAII-vs-bracket but what epoch check sits inside the release.

Typed pNext structure chains

Definition. Vulkan extends create-info structs at runtime through pNext — a const void* linked list whose legal links are declared per-struct in vk.xml's structextends attribute. A typed chain promotes that attribute into the type system so an illegal extension struct cannot be attached.

Why it matters. This is the best-surviving piece of registry safety metadata — the positive counterpart to the externsync finding — and a pure compile-time construct in every implementation (zero bytes, zero instructions at runtime). The implementation spectrum:

SystemMechanismNotable
Vulkan-Hppvk::StructureChain tuple + 1,213 generated StructExtends<X, Y> traits + static_assert"only chains which are valid according to the Vulkan specification can be created, which is verified at compile time" (docs/Usage.md)
ash1,217 generated unsafe impl Extends<Base> impls gating a safe push()The one place ash promotes registry metadata into types
vulkanaliaExtends* marker traits + generated InputChainStruct/OutputChainStruct + chain iteratorsOnly thin binding that types the output chain
haskell-vulkanType-level list (a ::& es) + closed type families Extends/Extendss; Chain is injectiveOutput-chain inference: pattern-matching a result infers the query chain
Silk.NETIChainStart / IExtendsChain<TChain> generic constraintsPlus documented *Any escape methods for registry gaps
vulkanoAbolished — extension members flattened into create-info structsMisuse-proof, but generic extensibility and coverage lag traded away
vulkan-zig, eruptedNone — pNext stays ?*const anyopaque / const(void)*The untyped floor

Remaining gaps even at the top of the table: duplicate-sType and chain-order rules are unchecked everywhere (only allowduplicate survives, in Vulkan-Hpp), and heterogeneously-chained arrays force erasure (haskell-vulkan's existential SomeStruct). For D, structextends → template constraints + CTFE is a direct mapping.


Binding generation

Binding generators & vk.xml

Definition. vk.xml is the machine-readable Vulkan registry — every type, command, extension, and a layer of semantic attributes (len, optional, successcodes/errorcodes, structextends, externsync, handle parent, implicitexternsyncparams) — from which the C headers, the spec's own tables, the validation layers' thread-safety checks, and nearly every surveyed binding are generated. Generator architectures observed: Khronos's own Python framework reused (erupted), bespoke offline generators committed with their output (ash in Rust, vulkanalia in Kotlin — regenerated nightly by cron, Vulkan-Hpp in C++ — released weekly per spec patch, haskell-vulkan — which also consumes the built spec asciidoc to embed Valid Usage prose in Haddocks), build-time generation against the user's own vk.xml (vulkan-zig's generator binary run from build.zig, the closest existing analogue to D CTFE), and no generator at all (daxa, vuk, Tephra, wgpu — hand-authored replacement APIs, for which no registry metadata can survive by construction).

Why it matters. Generation determines both coverage (generated bindings track the spec in days; hand-written layers curate a subset and lag) and which safety metadata survives into the target type system — the survey's question 5. The observed survival table is stark: successcodes/errorcodes survive almost universally (typed results: ash/vulkanalia Result splits, vulkan-zig's per-command Zig error sets, olivine's narrowed polymorphic variants — the natural D target is Expected!(T, VkResult)); structextends survives in the typed-chain systems; len survives as slices/views; defaults from the registry survive uniquely well in D (erupted's 666 sType-defaulted structs, via default field initializers); and externsync survives nowhere as types. Since the metadata is identical for all consumers, what survives is a generator choice, not a language limit — the central premise behind a CTFE-driven sparkles:vulkan generator.


Concept → system matrix

Where each concept is load-bearing, at a glance (deep-dive links; the comparison holds the full per-dimension matrix):

ConceptCompile-time-only usersRuntime-cost usersAbsent / negative data points
externsync in typesvulkano (mutexes, !Send)every generated binding
Pipeline barriers, automateddaxa · vuk · Tephra · wgpu · vulkano · Granite (graph only) · NVRHI · blade (global catch-all) · V-EZ (historical)thin bindings (manual)
Named usage statesvuk (AccessResourceUse) · daxa (attachment access types)vk-sync / thsvs (constant lookup) · NVRHI (ResourceStates, the entire sync vocabulary)thin bindings (raw stage/access masks); abolished in blade
Timeline-semaphore epochsdaxa · vuk · Tephra · wgpu · vulkanothin bindings (raw)
Render/task graphvuk (Access in the pass type)daxa · vulkano-taskgraph · Granite (bake(), the 2017 ancestor) · (Tephra, partial)JVM ecosystem (survey); all of D (erupted)
Usage trackingwgpu · vulkano gen-2 · Tephra (job tier) · NVRHI (per command list) · V-EZ (per command, historical)graph-only systems; deleted by design in blade
Bindlessdaxa (default)wgpu (partial), classic binding elsewhere
Deferred destructiondaxa · Tephra · wgpu · vulkano · vuk · Granite (frame-context-bucketed, fence-gated) · NVRHI (trackingSemaphore + runGarbageCollection())thin bindings; deliberately absent in blade · V-EZ
Phantom/branded handlesash · Vulkan-Hpp · Tephra · olivine · vulkan-zig · vulkano (Id<T>)erupted (32-bit), LWJGL non-dispatchable
Typestatevulkano (GpuFuture, partial)everyone else — the survey's largest unused-technique gap
Typed pNext chainsVulkan-Hpp · ash · vulkanalia · haskell-vulkan · Silk.NETvulkan-zig · erupted; abolished in vulkano
RAII/bracketVulkan-Hpp · Tephra · haskell-vulkan · jcoronado(deferred-destruction backstops above)ash · vulkanalia · vulkan-zig · erupted

Sources