Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Synchronization

vk-graph provides a high-performance abstraction over Vulkan synchronization which retains the low driver overhead of correctly synchronized command buffers.

Pipeline Barriers

Vulkan specifies that resources and pipelines will have synchronized access when barriers are inserted into the command stream. Unsynchronized access results in undefined behavior.

Tip

Unsynchronized access may be detected through debug assertions or Vulkan SDK debugging layers.

Access Type Abstraction

vk-graph uses an enumeration of possible states to define all supported pipeline barriers in an easy-to-use way.

Sample access types:

TypeUsage
AccessType::GeneralCovers any access - useful for debug, generally avoid for performance reasons
AccessType::ColorAttachmentWriteWritten as a color attachment during rendering
AccessType::ComputeShaderReadUniformBufferRead as a uniform buffer in a compute shader

(Full list)

Resource Access

The required access varies depending on the function being called and what the Vulkan specification requires for a given command.

Generally, access must be specified before each command uses a resource. It appears as an “access” function call:

#![allow(unused)]
fn main() {
use vk_graph::Graph;
use vk_graph::driver::{DriverError, device::Device, sync::AccessType};
use vk_graph::node::{BufferNode, ImageNode};
fn test(
    device: &Device,
    some_buffer: BufferNode,
    some_image: ImageNode,
) -> Result<(), DriverError> {
let mut graph = Graph::default();
graph
    .begin_cmd()
    .resource_access(some_buffer, AccessType::TransferRead)
    .resource_access(some_image, AccessType::TransferWrite)
    .record_cmd_buf(|cmd_buf| {
        // we are synchronized!
        // You may:
        //  - Read some_buffer
        //  - Write some_image
    });
Ok(()) }
}

Resource access is specified for and consumed by the following command buffer recording. For multiple accesses, use multiple “access” and “record” function calls:

#![allow(unused)]
fn main() {
use vk_graph::Graph;
use vk_graph::driver::{DriverError, device::Device, sync::AccessType};
use vk_graph::node::{BufferNode, ImageNode};
fn test(
    device: &Device,
    buffer: BufferNode,
    image: ImageNode,
) -> Result<(), DriverError> {
let mut graph = Graph::default();
graph
    .begin_cmd()
    .resource_access(buffer, AccessType::TransferRead)
    .resource_access(image, AccessType::TransferWrite)
    .record_cmd_buf(|cmd_buf| {
        // Safe to copy buffer to image
    })
    .resource_access(image, AccessType::TransferRead)
    .resource_access(buffer, AccessType::TransferWrite)
    .record_cmd_buf(|cmd_buf| {
        // Safe to copy image to buffer
    });
Ok(()) }
}

Shader Resource Access

When a resource (buffer, image, or acceleration structure) is accessed from a shader the shader_resource_access function is used:

// clear_image.glsl
#version 460 core
#pragma shader_stage(compute)

layout(binding = 42, rgba8) writeonly uniform image2D dstImage;

void main() {
    imageStore(
        dstImage,
        ivec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y),
        vec4(0)
    );
}
#![allow(unused)]
fn main() {
macro_rules! include_bytes { ($path:expr) => { [0u8] }; }
use vk_graph::Graph;
use vk_graph::driver::{DriverError, ash::vk, device::Device, sync::AccessType};
use vk_graph::driver::compute::{ComputePipeline, ComputePipelineInfo};
use vk_graph::driver::image::{Image, ImageInfo};
fn test(device: &Device) -> Result<(), DriverError> {
let mut graph = Graph::default();

let fmt = vk::Format::R8G8B8A8_UNORM;
let usage = vk::ImageUsageFlags::STORAGE;
let info = ImageInfo::image_2d(32, 32, fmt, usage);
let image = graph.bind_resource(Image::create(
    device,
    info,
)?);

graph
    .begin_cmd()
    .bind_pipeline(ComputePipeline::create(
        device,
        ComputePipelineInfo::default(),
        include_bytes!("clear_image.spv").as_slice(),
    )?)
    .shader_resource_access(42, image, AccessType::ComputeShaderWrite)
    .record_cmd_buf(|cmd_buf| {
        cmd_buf.dispatch(32, 32, 1);
    });
Ok(()) }
}

Subresource Access

Buffer ranges and image views are referred to as subresource ranges and accessed using “subresource” function variants:

#![allow(unused)]
fn main() {
macro_rules! include_bytes { ($path:expr) => { [0u8] }; }
use vk_graph::Graph;
use vk_graph::driver::{DriverError, ash::vk, device::Device, sync::AccessType};
use vk_graph::driver::compute::{ComputePipeline, ComputePipelineInfo};
use vk_graph::driver::image::{Image, ImageInfo};
fn test(device: &Device) -> Result<(), DriverError> {
let mut graph = Graph::default();

let fmt = vk::Format::R8G8B8A8_UNORM;
let usage = vk::ImageUsageFlags::STORAGE;
let info = ImageInfo::image_2d(32, 32, fmt, usage);
let image = graph.bind_resource(Image::create(
    device,
    info,
)?);

graph
    .begin_cmd()
    .bind_pipeline(ComputePipeline::create(
        device,
        ComputePipelineInfo::default(),
        include_bytes!("clear_image.spv").as_slice(),
    )?)
    .shader_subresource_access(42, image, info, AccessType::ComputeShaderWrite)
    .record_cmd_buf(|cmd_buf| {
        cmd_buf.dispatch(32, 32, 1);
    });
Ok(()) }
}

Built-In Commands

The commands directly attached to a Graph, such as Graph::copy_buffer_to_image, do not require any access function calls.

The source code for these built-in commands uses public graph functions and provides good examples of typical usage.