vkrust 0.13.2+1.4.365

Rust Vulkan bindings for graphics and GPU compute, with cached dispatch and a native allocator
Documentation

vk

Vulkan in Rust. Native bindings, cached dispatch and an integrated allocator.

Registry-generated Vulkan bindings for graphics, games and GPU compute, including the low-level building blocks for AI and ML workloads. The Cargo package is vkrust; the Rust library and import namespace are vk.

Crates.io Documentation CI MIT

Realm · Developer guide · Rust API reference

Registry coverage, enabled device capabilities and runtime qualification are separate; see the qualification and limitations.

Install

[dependencies]
vk = { package = "vkrust", version = "0.13" }

No Vulkan SDK, C compiler, C++ library or consumer-side generator is needed. The default loader feature loads the system Vulkan library at runtime.

Library and Vulkan versions

The package version is 0.13.1+1.4.365:

Version Meaning
0.13.1 This Rust library's API release; Git tag v0.13.1
+1.4.365 Generated from Vulkan 1.4 headers, revision 365
ApplicationInfo::api_version The Vulkan API version your application requests at runtime

Cargo treats the suffix as build metadata, so the dependency requirement stays version = "0.13". The suffix identifies the pinned API snapshot; it is not a Vulkan 1.4 driver requirement or runtime qualification claim. Applications can request Vulkan 1.0, 1.1 or 1.2 and use the commands/features admitted by their actual implementation. The examples below request Vulkan 1.0.

vk::HEADER_VERSION and vk::HEADER_VERSION_COMPLETE expose the corresponding native header constants. See the registry provenance for the exact pinned Khronos commit.

Two calling styles, one implementation

Use free functions when you prefer Vulkan's C-style flow, or associated methods when you prefer an object-oriented, Python-like spelling. Both use the same cached PFNs and canonical operation body.

Prefer Type::new(...).set_*() to prepare records throughout the bindings and vk::mem. This is the shared construction style for both calling APIs; borrowed setters retain the backing host storage needed by native arguments.

Operation Free function Constructor or method
Create instance vk::create_instance(&entry, &info, None) vk::Instance::new(&entry, &info, None)
Enumerate GPUs vk::enumerate_physical_devices(&instance) instance.enumerate_physical_devices()
Create device vk::create_device(&instance, physical_device, &info, None) vk::Device::new(&instance, physical_device, &info, None)
Retrieve queue vk::get_device_queue(&device, family, index) device.get_device_queue(family, index)
Record dispatch vk::cmd_dispatch(&device, commands, x, y, z) device.cmd_dispatch(commands, x, y, z)
Destroy device vk::destroy_device(device, None) device.destroy(None)

Free functions take their dispatch context explicitly. There is no mutable global dispatch or per-call symbol lookup. Both styles retain Vulkan's unsafe valid-usage and synchronization requirements; method syntax is not a safe API or a Python binding.

Free functions

Create an instance, enumerate physical devices, then explicitly destroy it. The enumeration result is propagated after cleanup, including on query failure.

fn main() -> Result<(), Box<dyn std::error::Error>> {
	// SAFETY: use the trusted system Vulkan loader; all create-info pointers
	// remain live during creation. This example creates no child objects.
	unsafe {
		let entry = vk::Entry::new()?;
		let application = vk::ApplicationInfo::new()
			.set_application_name(c"vk example")
			.set_api_version(vk::API_VERSION_1_0);
		let info = vk::InstanceCreateInfo::new()
			.set_application_info(&application);

		let instance = vk::create_instance(&entry, &info, None)?;
		let devices = vk::enumerate_physical_devices(&instance);
		vk::destroy_instance(instance, None);

		println!("Physical devices: {}", devices?.len());
	}
	Ok(())
}

Constructors and methods

The equivalent instance lifecycle through Instance:

fn main() -> Result<(), Box<dyn std::error::Error>> {
	// SAFETY: use the trusted system loader; borrowed create-info storage
	// remains live. No children or outstanding work remain at destruction.
	unsafe {
		let entry = vk::Entry::new()?;
		let application = vk::ApplicationInfo::new()
			.set_application_name(c"vk example")
			.set_api_version(vk::API_VERSION_1_0);
		let info = vk::InstanceCreateInfo::new()
			.set_application_info(&application);

		let instance = vk::Instance::new(&entry, &info, None)?;
		let devices = instance.enumerate_physical_devices();
		instance.destroy(None);

		println!("Physical devices: {}", devices?.len());
	}
	Ok(())
}

If native creation succeeds but dispatch initialization cannot recover or destroy the object, CallError::InitializationFailed retains its handle and library. Applications must handle that variant explicitly to recover ownership; the short examples propagate it to their caller.

Borrowed record construction

Use Type::new().set_*() to prepare native arguments. Allocator records use the same construction style, with required arguments passed to new(...) where needed. Type::default() produces the same initial state for default-backed records. This is the default construction approach, available without an additional feature flag. It works with both free functions and methods; these setters construct the same Vulkan records passed to either calling style. Direct native fields remain available, and stored borrowed records carry lifetime parameters.

fn main() -> Result<(), vk::ConstructionError> {
	let application = vk::ApplicationInfo::new()
		.set_application_name(c"vk example")
		.set_api_version(vk::API_VERSION_1_0);
	let info = vk::InstanceCreateInfo::new()
		.set_application_info(&application);

	let priorities = [1.0_f32];
	let queues = [vk::DeviceQueueCreateInfo::new()
		.set_queue_family_index(0)
		.set_queue_priorities(&priorities)?];
	let device_info = vk::DeviceCreateInfo::new()
		.set_queue_create_infos(&queues)?;

	assert_eq!(info.p_application_info, &application as *const _);
	assert_eq!(device_info.queue_create_info_count, 1);
	Ok(())
}

Borrowed setters keep backing strings, records and arrays live through their last use. Array setters derive checked native counts; reviewed parallel arrays are set together. push_next admits registry-compatible chain types and retains an exclusive borrow of each inserted node:

/// The caller supplies a compatible physical device and Vulkan 1.2 query context.
unsafe fn timeline_semaphore_supported(
	instance: &vk::Instance,
	physical_device: vk::PhysicalDevice,
) -> Result<bool, vk::CallError> {
	let mut vulkan12 = vk::PhysicalDeviceVulkan12Features::new();
	{
		let mut features = vk::PhysicalDeviceFeatures2::new()
			.push_next(&mut vulkan12)?;
		// SAFETY: the caller establishes the native query's valid usage.
		unsafe { instance.get_physical_device_features2(physical_device, &mut features)? };
	}
	Ok(vulkan12.timeline_semaphore != 0)
}
# fn main() {}

These methods prepare the same native records. They do not create GPU objects, validate all Vulkan usage, enable features or prove GPU completion. Public raw pointers remain an unsafe-call obligation; borrowing a pointer array keeps the array alive, not the objects behind its pointers. Fluent field setters use the set_* prefix on the semantic field name, while existing bitfield getters and set_* accessors retain their spelling.

Generated records and unions carry complete construction coverage: byte payloads, SPIR-V words, optional parallel arrays, initialized writable outputs and reviewed parent/cross-record capacities. Opaque addresses and foreign objects without a known host extent use explicit unsafe pointer setters. See the changelog for compatibility changes.

For direct-field construction, prefer Type { field: value, ..Default::default() } over exhaustive literals. Declare stored borrowed records as Type<'a>. Records retaining exclusive chain borrows no longer implement Copy or Clone; create fresh array elements with std::array::from_fn and borrow single records with std::slice::from_ref. The native union AccelerationStructureGeometryDataKHR uses ManuallyDrop for its non-Copy record members. Native field layout remains subject to the independent C ABI gate. Raw handle lifetimes and explicit destruction requirements are unchanged.

Mix styles when recording commands

An existing compute recording can use either spelling without creating another device or dispatch table:

/// # Safety
/// `commands` must be recording outside a render pass, from a pool on this
/// device's compute-capable queue family. `pipeline` must be a compatible live
/// compute pipeline; descriptor sets, push constants and resource barriers
/// required by its shader must already be valid for this dispatch.
unsafe fn record_compute(
	device: &vk::Device,
	commands: vk::CommandBuffer,
	pipeline: vk::Pipeline,
) -> Result<(), vk::CallError> {
	unsafe {
		vk::cmd_bind_pipeline(device, commands, vk::PipelineBindPoint::COMPUTE, pipeline)?;
		device.cmd_dispatch(commands, 1, 1, 1)?;
	}
	Ok(())
}

Recording does not submit or wait. The application owns command-buffer state, feature enablement, object lifetimes and host/GPU synchronization.

Wrappers and raw handles

Instance and Device are the two main dispatch wrappers. They combine the native identity with loaded commands, library lifetime retention and optional owned creation-capability metadata. This is what lets device.cmd_dispatch(...) know which native handle and PFN table to use.

Rust type Meaning
vk::Instance Instance wrapper with cached instance dispatch
vk::InstanceHandle Raw native VkInstance identity
vk::Device Device wrapper with cached device dispatch
vk::DeviceHandle Raw native VkDevice identity
fn native_handles(instance: &vk::Instance, device: &vk::Device) {
	let instance_handle: vk::InstanceHandle = instance.handle();
	let device_handle: vk::DeviceHandle = device.handle();
	assert!(!instance_handle.is_null());
	assert!(!device_handle.is_null());
}

Handles are copyable identities, not owners or dispatch contexts. PFN signatures use the raw handle types to preserve the Vulkan ABI. Other native handles keep their familiar names: PhysicalDevice, Queue, Buffer, Image and so on. Import existing instances/devices with unsafe new_from_handle or new_from_handle_with_capabilities; importing must not create duplicate owners.

Native destruction remains explicit. Drop never destroys Vulkan objects, submits work or waits. Destroy children and complete outstanding work before destroying their device or instance, using callbacks compatible with creation.

Native Vulkan, Rust spelling

The namespace is flat: bindings, StdVideo definitions, dispatch tables, free functions and the Instance / Device wrappers live directly under vk::. The two public subsystem modules are vk::load for library loading and vk::mem for the Rust-native allocator.

Item Rust convention Native → Rust example
Types PascalCase VkApplicationInfo → vk::ApplicationInfo
Constants SCREAMING_SNAKE_CASE VK_SUCCESS → vk::Result::SUCCESS
Functions and methods snake_case vkCmdDispatch → vk::cmd_dispatch / device.cmd_dispatch
Struct members snake_case pApplicationInfo → p_application_info
Fluent setters set_* + semantic name pApplicationInfo → set_application_info
Raw instance/device handles Explicit Handle suffix VkDevice → vk::DeviceHandle

Vulkan acronyms, vendor suffixes, StdVideo* names and PFN_vk* aliases remain recognizable. C-compatible representations preserve layout independently of field spelling.

let info = vk::InstanceCreateInfo::new();
assert_eq!(info.s_type, vk::StructureType::INSTANCE_CREATE_INFO);
assert!(info.p_next.is_null());
assert!(vk::DeviceHandle::NULL.is_null());

Search the API reference using native names such as VkDevice, vkCreateInstance, VK_SUCCESS or pApplicationInfo; generated rustdoc search aliases lead to the Rust spellings. Vulkan declarations link to Khronos reference pages. StdVideo definitions and registry-only constants link to their pinned Khronos registry source. Reference pages follow Khronos's latest specification; the bindings follow the pinned registry snapshot. The development ABI map retains both C and Rust field names for independent layout checks; renaming fields does not reorder or change their native storage.

Loading and capabilities

vk::Entry::new() opens the OS Vulkan loader. FnEntry, FnInstance and FnDevice cache nullable native function pointers. Created wrappers retain version, extension and enabled-feature provenance; known disabled commands are filtered from dispatch at construction. Raw imports without metadata report unknown provenance. Resolving a pointer does not establish feature enablement.

Command-specific arguments, feature requirements and pointer chains remain the caller's responsibility. The API reference documents the Rust contracts alongside links to the native Vulkan requirements.

Integrated allocator

Enable features = ["allocator"] for the Rust-native, VMA-derived allocator:

[dependencies]
vk = { package = "vkrust", version = "0.13", features = ["allocator"] }

vk::mem owns allocator operations and configuration; vk::Allocator is the same type as vk::mem::Allocator. This keeps allocator-backed creation distinct from the native binding vk::create_buffer, which creates an unbound buffer. Use use vk::mem as vma; for a familiar local alias without another crate, wrapper or runtime cost. Both spellings are equally valid; the canonical module name in this repository is mem.

Use new(...).set_*() for allocator record construction too. Every public field on a plain vk::mem record has a chainable set_* method, including requests, configuration, callbacks, import metadata, cache ranges, budgets and diagnostic snapshots. Default-backed records use new().set_*(). AllocRequest::new(size, alignment, memory_type_bits), ImportMemoryInfo::new(allocation_size, memory_type_bits) and CacheRange::new(&allocation) retain their required inputs. Setters consume and return the same record; public fields and struct literals remain available. Editing a snapshot changes only that record. Live resources keep their checked operation APIs, and operations still own validation.

Allocator free functions and methods share one implementation, just like the binding API. They take the allocator explicitly and retain Rust naming rather than adding a second set of vma* symbols:

Operation Free function Method
Create, allocate and bind buffer vk::mem::create_buffer_with_options(&mut allocator, &info, &options) allocator.create_buffer_with_options(&info, &options)
Create, allocate and bind image vk::mem::create_image_with_options(&mut allocator, &info, &options) allocator.create_image_with_options(&info, &options)
Allocate existing buffer's memory vk::mem::allocate_for_buffer(&mut allocator, buffer, &options) allocator.allocate_for_buffer(buffer, &options)
Map an allocation vk::mem::map(&mut allocator, &allocation) allocator.map(&allocation)
Inspect an allocation vk::mem::allocation_info(&allocator, &allocation) allocator.allocation_info(&allocation)
Destroy managed buffer and free memory vk::mem::destroy_buffer(&mut allocator, buffer, &mut allocation) allocator.destroy_buffer(buffer, &mut allocation)

The following helper returns both objects to its caller for explicit cleanup. The caller supplies an allocator constructed for its device, preferably with Allocator::new_from_physical_device(instance, physical_device, device):

# #[cfg(feature = "allocator")]
# mod allocator_example {
use vk::mem as vma;

/// # Safety
/// The allocator's device must be live and support this buffer size and usage.
/// The caller must destroy the returned buffer and free its allocation after
/// all host and GPU uses complete.
unsafe fn create_upload_buffer(
	allocator: &mut vk::Allocator<'_>,
) -> Result<(vk::Buffer, vk::Allocation), vk::AllocError> {
	let info = vk::BufferCreateInfo::new()
		.set_size(4096)
		.set_usage(vk::BufferUsageFlags::TRANSFER_SRC_BIT)
		.set_sharing_mode(vk::SharingMode::EXCLUSIVE);
	let options = vma::AllocationCreateInfo::new()
		.set_usage(vma::MemoryUsage::PreferHost)
		.set_host_access(vma::HostAccess::SequentialWrite)
		.set_required_flags(vk::MemoryPropertyFlags::HOST_VISIBLE_BIT)
		.set_policy(vma::AllocationPolicy::Automatic);

	// Method spelling: allocator.create_buffer_with_options(&info, &options).
	// SAFETY: the caller establishes the native valid-usage requirements.
	unsafe { vma::create_buffer_with_options(allocator, &info, &options) }
}

/// # Safety
/// Complete every host/GPU use and release explicit map holds first. The buffer
/// must still be live and paired with this allocation in this allocator.
unsafe fn destroy_completed_buffer(
	allocator: &mut vk::Allocator<'_>,
	buffer: vk::Buffer,
	allocation: &mut vk::Allocation,
) -> Result<(), vk::AllocError> {
	// Free-function spelling: vma::destroy_buffer(allocator, buffer, allocation).
	// SAFETY: the caller supplies completion and the matching live pair.
	unsafe { allocator.destroy_buffer(buffer, allocation) }
}
# }
# fn main() {}

The allocator implements managed buffers/non-disjoint images, mixed-resource TLSF/linear pools, automatic memory preferences, mapping and cache operations, budgets, callbacks, statistics, virtual placement and explicit multi-pass relocation. SynchronizedAllocator exposes direct &self methods. Its map, unmap and bind calls use a stable mutex per native backing, so another backing can allocate/free or map/bind concurrently, including within the same pool. Placement remains serialized per owner; borrowed pool/type scopes provide independent placement owners. Borrowed results, relocation and global services use exclusive with_allocator access and wait for active native calls. SynchronizedVirtualBlock optionally serializes host-only placement; VMA's virtual blocks themselves require external synchronization. Ordinary Allocator remains exclusive and non-Send/non-Sync. The new_retained constructor family produces Allocator<'static> backed by an owned Arc<Device> clone, letting an allocator outlive its construction scope and finish on a dedicated cleanup thread; SynchronizedAllocator::new_owned adopts it directly.

The pinned VMA implementation is a development oracle; consumers need no C++ allocator or FFI bridge. The broad allocator service surface is implemented and hardware-qualified on the recorded baseline. Full VMA equivalence, wider concurrent stress, multi-driver execution and additional platforms remain unqualified. Allocations and native resources need explicit cleanup. Host locks do not order GPU work or protect escaped mapped bytes; operations and Drop never submit or wait for the GPU. See qualification and limitations for the exact scope.

Features

Cargo feature Compiled functionality
loader (default) Dynamic loading, instance/device wrappers and native operations
allocator Rust-native allocator in vk::mem; enables loader
platform-* Matching platform-specific ABI declarations
provisional Provisional registry declarations

Set default-features = false for ABI declarations and raw dispatch tables without the dynamic loader. Cargo features do not enable Vulkan device extensions or features.

This build still uses Rust std. Android/iOS do not require no_std; see platform and standard-library scope for loader paths, portability setup and cross-build limits.

Performance

Selected host-latency results on Intel Iris Xe (Surface Laptop 5, November 15, 2026), with 20 fresh-process samples per backend/workload. Below 1 favors vk. Allocation rows combine allocation/free and native create/bind/destroy work; 128 live allocations unless noted. Intervals crossing 1 do not establish a lead. 15 of 44 measured workloads complete below 1.0× VMA, including every virtual-placement row; the worst measured gap is pooled TLSF shuffled free at 1.162×.

Workload vk / VMA time (95% confidence interval)
Virtual TLSF, minimum-offset churn 0.624× (0.621–0.629)
Virtual linear, LIFO 0.708× (0.699–0.737)
Virtual linear, churn 0.813× (0.788–0.845)
Pooled buffers, TLSF, reverse, single live 0.937× (0.914–1.016)
Separate buffers, LIFO 1.070× (1.027–1.087)
Separate images, LIFO 1.072× (1.058–1.089)
Pooled images, TLSF, LIFO 1.076× (1.063–1.098)
Pooled buffers, TLSF, LIFO 1.006× (0.984–1.043)
Pooled buffers, TLSF, churn, 32 live 1.147× (1.139–1.164)
Pooled buffers, TLSF, shuffled free 1.162× (1.129–1.196)
Pooled buffer map + 8-byte write + unmap 1.017× (1.007–1.031)

These exploratory runs used unlocked clocks and shared workstation load. Native VMA uses its default internal locks; ordinary vk uses exclusive access. The full benchmark report includes all 44 allocator configurations, subsequent candidate passes, Ash/Vulkano history, methodology and retained evidence.

Development

cargo test --workspace --all-features
cargo run -p vkrust-gen -- --registry registry/vk.xml --video-registry registry/video.xml --output . --check

Runtime source lives in src/rs, generation in tool/gen, and tests in test/rs. Binding source and tests are flat; load and mem have their own subdirectories. Generated files use singular *.gen.rs names. Dev/test builds disable incremental compilation and debug information.

The optional SDK isolates Ash/Vulkano and C++ VMA comparison dependencies from the library. It prepares explicit hardware profiles, fresh-process benchmarks and an offscreen Vulkan 1.3 triangle using the same new().set_*() records and allocator methods. The current Surface Laptop 5 baseline records 32-profile core/synchronization validation, the graphics oracle and paired Ash and VMA confirmation host-latency ratios on Intel Iris Xe. Broader qualification remains open.

License

MIT. See license texts and third-party notices for Khronos registry and AMD allocator provenance. Realm animation shared with OA.