Home

Awesome

vulkan-zig

A Vulkan binding generator for Zig.

Actions Status

Overview

vulkan-zig attempts to provide a better experience to programming Vulkan applications in Zig, by providing features such as integration of vulkan errors with Zig's error system, function pointer loading, renaming fields to standard Zig style, better bitfield handling, turning out parameters into return values and more.

vulkan-zig is automatically tested daily against the latest vk.xml and zig, and supports vk.xml from version 1.x.163.

Example

A partial implementation of https://vulkan-tutorial.com is implemented in examples/triangle.zig. This example can be ran by executing zig build --build-file $(pwd)/examples/build.zig run-triangle in vulkan-zig's root. See in particular the build file, which contains a concrete example of how to use vulkan-zig as a dependency.

Zig versions

vulkan-zig aims to be always compatible with the ever-changing Zig master branch (however, development may lag a few days behind). Sometimes, the Zig master branch breaks a bunch of functionality however, which may make the latest version vulkan-zig incompatible with older releases of Zig. This repository aims to have a version compatible for both the latest Zig master, and the latest Zig release. The master branch is compatible with the master branch of Zig, and versions for older versions of Zig are maintained in the zig-<version>-compat branch.

master is compatible and tested with the Zig self-hosted compiler. The zig-stage1-compat branch contains a version which is compatible with the Zig stage 1 compiler.

Features

CLI-interface

A CLI-interface is provided to generate vk.zig from the Vulkan XML registry, which is built by default when invoking zig build in the project root. To generate vk.zig, simply invoke the program as follows:

$ zig-out/bin/vulkan-zig-generator path/to/vk.xml output/path/to/vk.zig

This reads the xml file, parses its contents, renders the Vulkan bindings, and formats file, before writing the result to the output path. While the intended usage of vulkan-zig is through direct generation from build.zig (see below), the CLI-interface can be used for one-off generation and vendoring the result.

path/to/vk.xml can be obtained from several sources:

Generation with the package manager from build.zig

There is also support for adding this project as a dependency through zig package manager in its current form. In order to do this, add this repo as a dependency in your build.zig.zon:

.{
    // -- snip --
    .dependencies = .{
        // -- snip --
        .vulkan_zig = .{
            .url = "https://github.com/Snektron/vulkan-zig/archive/<commit SHA>.tar.gz",
            .hash = "<dependency hash>",
        },
    },
}

And then in your build.zig file, you'll need to add a line like this to your build function:

const vkzig_dep = b.dependency("vulkan_zig", .{
    .registry = @as([]const u8, b.pathFromRoot("path/to/vk.xml")),
});
const vkzig_bindings = vkzig_dep.module("vulkan-zig");
exe.root_module.addImport("vulkan", vkzig_bindings);

That will allow you to @import("vulkan") in your executable's source.

Manual generation with the package manager from build.zig

Bindings can also be generated by invoking the generator directly. This may be useful is some special cases, for example, it integrates particularly well with fetching the registry via the package manager. This can be done by adding the Vulkan-Headers repository to your dependencies, and then passing the vk.xml inside it to vulkan-zig-generator:

.{
    // -- snip --
    .depdendencies = .{
        // -- snip --
        .vulkan_headers = .{
            .url = "https://github.com/KhronosGroup/Vulkan-Headers/archive/<commit SHA>.tar.gz",
            .hash = "<dependency hash>",
        },
    },
}

And then pass vk.xml to vulkan-zig-generator as follows:

// Get the (lazy) path to vk.xml:
const registry = b.dependency("vulkan_headers", .{}).path("registry/vk.xml");
// Get generator executable reference
const vk_gen = b.dependency("vulkan_zig", .{}).artifact("vulkan-zig-generator");
// Set up a run step to generate the bindings
const vk_generate_cmd = b.addRunArtifact(vk_gen);
// Pass the registry to the generator
vk_generate_cmd.addFileArg(registry);
// Create a module from the generator's output...
const vulkan_zig = b.addModule("vulkan-zig", .{
    .root_source_file = vk_generate_cmd.addOutputFileArg("vk.zig"),
});
// ... and pass it as a module to your executable's build command
exe.root_module.addImport("vulkan", vulkan_zig);

See examples/build.zig and examples/build.zig.zon for a concrete example.

Function & field renaming

Functions and fields are renamed to be more or less in line with Zig's standard library style:

Function pointers & Wrappers

vulkan-zig provides no integration for statically linking libvulkan, and these symbols are not generated at all. Instead, vulkan functions are to be loaded dynamically. For each Vulkan function, a function pointer type is generated using the exact parameters and return types as defined by the Vulkan specification:

pub const PfnCreateInstance = fn (
    p_create_info: *const InstanceCreateInfo,
    p_allocator: ?*const AllocationCallbacks,
    p_instance: *Instance,
) callconv(vulkan_call_conv) Result;

For each function, a wrapper is generated into one of three structs:

To create a wrapper type, an "api specification" should be passed to it. This is a list of ApiInfo structs, which allows one to specify the functions that should be made available. An ApiInfo structure is initialized 3 optional fields, base_commands, instance_commands, and device_commands. Each of these takes a set of the vulkan functions that should be made available for that category, for example, setting .createInstance = true in base_commands makes the createInstance function available (loaded from vkCreateInstance). An entire feature level or extension can be pulled in at once too, for example, vk.features.version_1_0 contains all functions for Vulkan 1.0. vk.extensions.khr_surface contains all functions for the VK_KHR_surface extension.

const vk = @import("vulkan");
/// To construct base, instance and device wrappers for vulkan-zig, you need to pass a list of 'apis' to it.
const apis: []const vk.ApiInfo = &.{
    // You can either add invidiual functions by manually creating an 'api'
    .{
        .base_commands = .{
            .createInstance = true,
        },
        .instance_commands = .{
            .createDevice = true,
        },
    },
    // Or you can add entire feature sets or extensions
    vk.features.version_1_0,
    vk.extensions.khr_surface,
    vk.extensions.khr_swapchain,
};
const BaseDispatch = vk.BaseWrapper(apis);

The wrapper struct then provides wrapper functions for each function pointer in the dispatch struct:

pub const BaseWrapper(comptime cmds: anytype) type {
    ...
    const Dispatch = CreateDispatchStruct(cmds);
    return struct {
        dispatch: Dispatch,

        pub const CreateInstanceError = error{
            OutOfHostMemory,
            OutOfDeviceMemory,
            InitializationFailed,
            LayerNotPresent,
            ExtensionNotPresent,
            IncompatibleDriver,
            Unknown,
        };
        pub fn createInstance(
            self: Self,
            create_info: InstanceCreateInfo,
            p_allocator: ?*const AllocationCallbacks,
        ) CreateInstanceError!Instance {
            var instance: Instance = undefined;
            const result = self.dispatch.vkCreateInstance(
                &create_info,
                p_allocator,
                &instance,
            );
            switch (result) {
                .success => {},
                .error_out_of_host_memory => return error.OutOfHostMemory,
                .error_out_of_device_memory => return error.OutOfDeviceMemory,
                .error_initialization_failed => return error.InitializationFailed,
                .error_layer_not_present => return error.LayerNotPresent,
                .error_extension_not_present => return error.ExtensionNotPresent,
                .error_incompatible_driver => return error.IncompatibleDriver,
                else => return error.Unknown,
            }
            return instance;
        }

        ...
    }
}

Wrappers are generated according to the following rules:

Furthermore, each wrapper contains a function to load each function pointer member when passed either PfnGetInstanceProcAddr or PfnGetDeviceProcAddr, which attempts to load each member as function pointer and casts it to the appropriate type. These functions are loaded literally, and any wrongly named member or member with a wrong function pointer type will result in problems.

Note that these functions accepts a loader with the signature of anytype instead of PfnGetInstanceProcAddr. This is because it is valid for vkGetInstanceProcAddr to load itself, in which case the returned function is to be called with the vulkan calling convention. This calling convention is not required for loading vulkan-zig itself, though, and a loader to be called with any calling convention with the target architecture may be passed in. This is particularly useful when interacting with C libraries that provide vkGetInstanceProcAddr.

// vkGetInstanceProcAddr as provided by GLFW.
// Note that vk.Instance and vk.PfnVoidFunction are ABI compatible with VkInstance,
// and that `extern` implies the C calling convention.
pub extern fn glfwGetInstanceProcAddress(instance: vk.Instance, procname: [*:0]const u8) vk.PfnVoidFunction;

// Or provide a custom implementation.
// This function is called with the unspecified Zig-internal calling convention.
fn customGetInstanceProcAddress(instance: vk.Instance, procname: [*:0]const u8) vk.PfnVoidFunction {
    ...
}

// Both calls are valid, even
const vkb = try BaseDispatch.load(glfwGetInstanceProcAddress);
const vkb = try BaseDispatch.load(customGetInstanceProcAddress);

By default, wrapper load functions return error.CommandLoadFailure if a call to the loader resulted in null. If this behaviour is not desired, one can use loadNoFail. This function accepts the same parameters as load, but does not return an error any function pointer fails to load and sets its value to undefined instead. It is at the programmer's discretion not to invoke invalid functions, which can be tested for by checking whether the required core and extension versions the function requires are supported.

One can access the underlying unwrapped C functions by doing wrapper.dispatch.vkFuncYouWant(..).

Proxying Wrappers

Proxying wrappers wrap a wrapper and a pointer to the associated handle in a single struct, and automatically passes this handle to commands as appropriate. Besides the proxying wrappers for instances and devices, there are also proxying wrappers for queues and command buffers. Proxying wrapper type are constructed in the same way as a regular wrapper, by passing an api specification to them. To initialize a proxying wrapper, it must be passed a handle and a pointer to an appropriate wrapper. For queue and command buffer proxying wrappers, a pointer to a device wrapper must be passed.

// Create the dispatch tables
const InstanceDispatch = vk.InstanceWrapper(apis);
const Instance = vk.InstanceProxy(apis);

const instance_handle = try vkb.createInstance(...);
const vki = try InstanceDispatch.load(instance_handle, vkb.vkGetInstanceProcAddr);
const instance = Instance.load(instance_handle, &vki);
defer instance.destroyInstance(null);

For queue and command buffer proxying wrappers, the queue and cmd prefix is removed for functions where appropriate. Note that the device proxying wrappers also have the queue and command buffer functions made available for convenience, but there the prefix is not stripped.

Bitflags

Packed structs of bools are used for bit flags in vulkan-zig, instead of both a FlagBits and Flags variant. Places where either of these variants are used are both replaced by this packed struct instead. This means that even in places where just one flag would normally be accepted, the packed struct is accepted. The programmer is responsible for only enabling a single bit.

Each bit is defaulted to false, and the first bool is aligned to guarantee the overal alignment of each Flags type to guarantee ABI compatibility when passing bitfields through structs:

pub const QueueFlags = packed struct {
    graphics_bit: bool align(@alignOf(Flags)) = false,
    compute_bit: bool = false,
    transfer_bit: bool = false,
    sparse_binding_bit: bool = false,
    protected_bit: bool = false,
    _reserved_bit_5: bool = false,
    _reserved_bit_6: bool = false,
    ...
}

Note that on function call ABI boundaries, this alignment trick is not sufficient. Instead, the flags are reinterpreted as an integer which is passed instead. Each flags type is augmented by a mixin which provides IntType, an integer which represents the flags on function ABI boundaries. This mixin also provides some common set operation on bitflags:

pub fn FlagsMixin(comptime FlagsType: type) type {
    return struct {
        pub const IntType = Flags;

        // Return the integer representation of these flags
        pub fn toInt(self: FlagsType) IntType {...}

        // Turn an integer representation back into a flags type
        pub fn fromInt(flags: IntType) FlagsType { ... }

        // Return the set-union of `lhs` and `rhs.
        pub fn merge(lhs: FlagsType, rhs: FlagsType) FlagsType { ... }

        // Return the set-intersection of `lhs` and `rhs`.
        pub fn intersect(lhs: FlagsType, rhs: FlagsType) FlagsType { ... }

        // Return the set-complement of `lhs` and `rhs`. Note: this also inverses reserved bits.
        pub fn complement(self: FlagsType) FlagsType { ... }

        // Return the set-subtraction of `lhs` and `rhs`: All fields set in `rhs` are cleared in `lhs`.
        pub fn subtract(lhs: FlagsType, rhs: FlagsType) FlagsType { ... }

        // Returns whether all bits set in `rhs` are also set in `lhs`.
        pub fn contains(lhs: FlagsType, rhs: FlagsType) bool { ... }
    };
}

Handles

Handles are generated to a non-exhaustive enum, backed by a u64 for non-dispatchable handles and usize for dispatchable ones:

const Instance = extern enum(usize) { null_handle = 0, _ };

This means that handles are type-safe even when compiling for a 32-bit target.

Struct defaults

Defaults are generated for certain fields of structs:

pub const InstanceCreateInfo = extern struct {
    s_type: StructureType = .instance_create_info,
    p_next: ?*const anyopaque = null,
    flags: InstanceCreateFlags,
    ...
};

Pointer types

Pointer types in both commands (wrapped and function pointers) and struct fields are augmented with the following information, where available in the registry:

Note that this information is not everywhere as useful in the registry, leading to places where optional-ness is not correct. Most notably, CreateInfo type structures which take a slice often have the item count marked as optional, but the pointer itself not. As of yet, this is not fixed in vulkan-zig. If drivers properly follow the Vulkan specification, these can be initialized to undefined, however, that is not always the case.

Platform types

Defaults with the same ABI layout are generated for most platform-defined types. These can either by bitcasted to, or overridden by defining them in the project root:

pub const xcb_connection_t = if (@hasDecl(root, "xcb_connection_t")) root.xcb_connection_t else opaque{};

For some times (such as those from Google Games Platform) no default is known, but an opaque{} will be used by default. Usage of these without providing a concrete type in the project root is likely an error.

Shader compilation

Shaders should be compiled by invoking a shader compiler via the build system. For example:

pub fn build(b: *Builder) void {
    ...
    const vert_cmd = b.addSystemCommand(&.{
        "glslc",
        "--target-env=vulkan1.2",
        "-o"
    });
    const vert_spv = vert_cmd.addOutputFileArg("vert.spv");
    vert_cmd.addFileArg(b.path("shaders/triangle.vert"));
    exe.root_module.addAnonymousImport("vertex_shader", .{
        .root_source_file = vert_spv
    });
    ...
}

Note that SPIR-V must be 32-bit aligned when fed to Vulkan. The easiest way to do this is to dereference the shader's bytecode and manually align it as follows:

const vert_spv align(@alignOf(u32)) = @embedFile("vertex_shader").*;

See examples/build.zig for a working example.

For more advanced shader compiler usage, one may consider a library such as shader_compiler.

Limitations

See also