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

Graphics

Graphics commands are recorded after binding a GraphicsPipeline and declaring attachments such as color_attachment_image or depth_stencil_attachment_image.

Adjacent graphics commands in a submitted graph can share a Vulkan subpass when their attachments, layouts, sample counts, and multiview masks agree and non-attachment resources are read-only. Changing uniform buffers, sampled textures, descriptor sets, or compatible pipelines does not by itself cause another subpass. Each command still has its own bindings, recording callbacks, dynamic state setup, and timestamp positions.

Note

Input attachments, resolves, shader writes, non-sampled image reads, later clears/discards, and incompatible attachment configurations cause subpasses to conservatively retain command-defined boundaries.

Warning

Recording callback boundaries are not guaranteed Vulkan subpass boundaries; raw Vulkan calls within callbacks must not advance or end the graph-managed render pass.

The first declaration of an attachment supplies its render-pass load operation. A later LoadOp::Clear explicitly clears that attachment before the execution’s callback, over the full graph-managed render-pass area, regardless of the callback’s viewport or scissor. DontCare permits undefined contents; it does not request a zero fill.

API docs: GraphicsCommandRef::draw, GraphicsCommandRef::draw_indexed, GraphicsCommandRef::draw_indirect, GraphicsCommandRef::push_constants.

For external objects such as debug pipeline-statistics query pools, capture their owner in a caller-owned CommandStream graphics record_cmd callback. Build an unprepared stream with CommandStream::finalize(...).into_stream(), declare attachments and shader-resource accesses using stream arguments, then bind parent graph resources with insert_cmd_stream(...).with_arg(...). The inserted callbacks retain their captures through submission completion even after the caller drops its stream and owner handles. Ordinary graph record_cmd callbacks are one-shot and do not provide that lifetime.

Available Commands

CommandTypical use
bind_index_bufferProvide indices for indexed drawing
bind_vertex_buffersBind one or more vertex streams
drawDraw non-indexed geometry
draw_indexedDraw indexed geometry
draw_indexed_indirectRead indexed draw parameters from a buffer
draw_indexed_indirect_countGPU-driven indexed draws with a count buffer
draw_indirectRead non-indexed draw parameters from a buffer
draw_indirect_countGPU-driven non-indexed draws with a count buffer
set_scissorRestrict drawing to one or more rectangles
set_viewportOverride the default viewport dynamically
push_constantsUpdate small pipeline constants without a buffer upload

Direct Draws

The most common pattern is to bind vertex and index buffers, then issue draw or draw_indexed.

let mut graph = Graph::default();

let color = graph.bind_resource(Image::create(
    &device,
    ImageInfo::image_2d(
        1280,
        720,
        vk::Format::R8G8B8A8_UNORM,
        vk::ImageUsageFlags::COLOR_ATTACHMENT | vk::ImageUsageFlags::TRANSFER_SRC,
    ),
)?);
let vertices = graph.bind_resource(Buffer::create(
    &device,
    BufferInfo::device_mem(4096, vk::BufferUsageFlags::VERTEX_BUFFER),
)?);
let indices = graph.bind_resource(Buffer::create(
    &device,
    BufferInfo::device_mem(1024, vk::BufferUsageFlags::INDEX_BUFFER),
)?);

let pipeline = GraphicsPipeline::create(
    &device,
    GraphicsPipelineInfo::default(),
    [
        Shader::new_vertex([0u8; 4].as_slice()),
        Shader::new_fragment([0u8; 4].as_slice()),
    ],
)?;

graph
    .begin_cmd()
    .debug_name("main geometry pass")
    .bind_pipeline(&pipeline)
    .color_attachment_image(
        0,
        color,
        LoadOp::Clear(ClearColorValue::Float32([0.0, 0.0, 0.0, 1.0])),
        StoreOp::Store,
    )
    .resource_access(vertices, AccessType::VertexBuffer)
    .resource_access(indices, AccessType::IndexBuffer)
    .record_cmd(move |cmd| {
        cmd
            .bind_vertex_buffers(0, [(vertices, 0)])
            .bind_index_buffer(indices, 0, vk::IndexType::UINT32)
            .draw_indexed(36, 1, 0, 0, 0);
    });

MSAA Resolve

Use color_attachment_image for the multisampled target and color_attachment_resolve_image for the single-sampled resolve target.

graph
    .begin_cmd()
    .bind_pipeline(&pipeline)
    .color_attachment_image(0, msaa_color, LoadOp::DontCare, StoreOp::DontCare)
    .color_attachment_resolve_image(0, 1, resolve_color)
    .record_cmd(|cmd| {
        cmd.draw(3, 1, 0, 0);
    });

Dynamic Viewports And Scissors

The default viewport covers the full attachment extent and the default scissor does not clip. Override them when a pass renders only part of the target.

let mut graph = Graph::default();
let color = graph.bind_resource(Image::create(
    &device,
    ImageInfo::image_2d(
        1280,
        720,
        vk::Format::R8G8B8A8_UNORM,
        vk::ImageUsageFlags::COLOR_ATTACHMENT,
    ),
)?);
let vertices = graph.bind_resource(Buffer::create(
    &device,
    BufferInfo::device_mem(4096, vk::BufferUsageFlags::VERTEX_BUFFER),
)?);
let pipeline = GraphicsPipeline::create(
    &device,
    GraphicsPipelineInfo::default(),
    [
        Shader::new_vertex([0u8; 4].as_slice()),
        Shader::new_fragment([0u8; 4].as_slice()),
    ],
)?;

graph
    .begin_cmd()
    .bind_pipeline(&pipeline)
    .color_attachment_image(0, color, LoadOp::DontCare, StoreOp::Store)
    .resource_access(vertices, AccessType::VertexBuffer)
    .record_cmd(move |cmd| {
        cmd
            .set_viewport(
                0,
                &[vk::Viewport {
                    x: 0.0,
                    y: 0.0,
                    width: 640.0,
                    height: 360.0,
                    min_depth: 0.0,
                    max_depth: 1.0,
                }],
            )
            .set_scissor(
                0,
                &[vk::Rect2D {
                    offset: vk::Offset2D { x: 0, y: 0 },
                    extent: vk::Extent2D { width: 640, height: 360 },
                }],
            )
            .bind_vertex_buffers(0, [(vertices, 0)])
            .draw(3, 1, 0, 0);
    });

Indirect Draws

Indirect drawing is the usual next step once culling, LOD selection, or instance generation moves onto the GPU.

let mut graph = Graph::default();
let color = graph.bind_resource(Image::create(
    &device,
    ImageInfo::image_2d(
        1280,
        720,
        vk::Format::R8G8B8A8_UNORM,
        vk::ImageUsageFlags::COLOR_ATTACHMENT,
    ),
)?);
let vertices = graph.bind_resource(Buffer::create(
    &device,
    BufferInfo::device_mem(4096, vk::BufferUsageFlags::VERTEX_BUFFER),
)?);
let indices = graph.bind_resource(Buffer::create(
    &device,
    BufferInfo::device_mem(1024, vk::BufferUsageFlags::INDEX_BUFFER),
)?);
let draw_command = vk::DrawIndexedIndirectCommand {
    index_count: 36,
    instance_count: 1,
    first_index: 0,
    vertex_offset: 0,
    first_instance: 0,
};
let draw_args = graph.bind_resource(Buffer::create_from_slice(
    &device,
    vk::BufferUsageFlags::INDIRECT_BUFFER,
    bytemuck::cast_slice(&[
        draw_command.index_count as i32,
        draw_command.instance_count as i32,
        draw_command.first_index as i32,
        draw_command.vertex_offset,
        draw_command.first_instance as i32,
    ]),
)?);
let draw_count = graph.bind_resource(Buffer::create_from_slice(
    &device,
    vk::BufferUsageFlags::INDIRECT_BUFFER,
    &1u32.to_ne_bytes(),
)?);
let pipeline = GraphicsPipeline::create(
    &device,
    GraphicsPipelineInfo::default(),
    [
        Shader::new_vertex([0u8; 4].as_slice()),
        Shader::new_fragment([0u8; 4].as_slice()),
    ],
)?;

graph
    .begin_cmd()
    .bind_pipeline(&pipeline)
    .color_attachment_image(0, color, LoadOp::DontCare, StoreOp::Store)
    .resource_access(vertices, AccessType::VertexBuffer)
    .resource_access(indices, AccessType::IndexBuffer)
    .resource_access(draw_args, AccessType::IndirectBuffer)
    .resource_access(draw_count, AccessType::IndirectBuffer)
    .record_cmd(move |cmd| {
        cmd
            .bind_vertex_buffers(0, [(vertices, 0)])
            .bind_index_buffer(indices, 0, vk::IndexType::UINT32)
            .draw_indexed_indirect_count(
                draw_args,
                0,
                draw_count,
                0,
                16,
                size_of::<vk::DrawIndexedIndirectCommand>() as u32,
            );
    });

Push Constants

Use GraphicsCommandRef::push_constants for compact per-draw state that fits within the device’s push-constant limit.

graph
    .begin_cmd()
    .bind_pipeline(&pipeline)
    .color_attachment_image(0, image, LoadOp::DontCare, StoreOp::Store)
    .record_cmd(move |cmd| {
        cmd.push_constants(0, &[42])
            .draw(3, 1, 0, 0);
    });

Notes

  • draw and draw_indexed are the best fit for CPU-driven rendering.
  • draw_indirect and draw_indexed_indirect move only the parameters onto the GPU.
  • draw_*_indirect_count is the usual choice for fully GPU-driven visibility results.