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
| Command | Typical use |
|---|---|
bind_index_buffer | Provide indices for indexed drawing |
bind_vertex_buffers | Bind one or more vertex streams |
draw | Draw non-indexed geometry |
draw_indexed | Draw indexed geometry |
draw_indexed_indirect | Read indexed draw parameters from a buffer |
draw_indexed_indirect_count | GPU-driven indexed draws with a count buffer |
draw_indirect | Read non-indexed draw parameters from a buffer |
draw_indirect_count | GPU-driven non-indexed draws with a count buffer |
set_scissor | Restrict drawing to one or more rectangles |
set_viewport | Override the default viewport dynamically |
push_constants | Update 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
drawanddraw_indexedare the best fit for CPU-driven rendering.draw_indirectanddraw_indexed_indirectmove only the parameters onto the GPU.draw_*_indirect_countis the usual choice for fully GPU-driven visibility results.