feat: new draw command with arguments and resource tracking

This commit is contained in:
janis 2026-10-25 23:04:27 +01:00
parent d896a207de
commit d901fe250b
Signed by: janis
SSH key fingerprint: SHA256:bB1qbbqmDXZNT0KKD5c2Dfjg53JGhj7B3CFcLIzSqq8
4 changed files with 523 additions and 18 deletions

View file

@ -3,7 +3,9 @@ use ash::vk;
use crate::{
recorder::{CommandIndex, CommandRecorder, RecordError, RecordedCommand, SideEffectMap},
resources::{
Buffer, BufferRange, ResourceId, SubresourceScope, SubresourceScopeInner, Texture,
Buffer, BufferRange, DescriptorResourceAccess, GraphDescriptorSet, IndexBinding,
IndexFormat, IndirectBinding, PushConstantData, ResourceId, SubresourceScope,
SubresourceScopeInner, Texture, VertexBinding,
},
};
@ -120,27 +122,205 @@ impl RecordedCommand for ClearTexture {
}
#[derive(Debug, Clone, Copy)]
pub enum DrawArgs {
Direct {
vertex_count: u32,
instance_count: u32,
first_vertex: u32,
first_instance: u32,
},
Indexed {
index_count: u32,
instance_count: u32,
first_index: u32,
vertex_offset: i32,
first_instance: u32,
},
}
/// A self-contained graphics draw packet. Every graph resource consumed by
/// vertex/index/indirect input or descriptor sets is declared by this packet.
#[derive(Debug, Clone)]
pub struct Draw {
pub vertex_count: u32,
pub instance_count: u32,
pub first_vertex: u32,
pub first_instance: u32,
pub args: DrawArgs,
/// Layout compatible with the bound pipeline, descriptor sets, and push constants.
pub pipeline_layout: vk::PipelineLayout,
pub vertices: Vec<VertexBinding>,
pub index: Option<IndexBinding>,
pub indirect: Option<IndirectBinding>,
pub descriptor_sets: Vec<GraphDescriptorSet>,
pub push_constants: Vec<PushConstantData>,
}
impl Command for Draw {
fn side_effects(&self, _command_index: CommandIndex, _map: &mut SideEffectMap) {}
fn side_effects(&self, command_index: CommandIndex, map: &mut SideEffectMap) {
for vertex in &self.vertices {
map.insert(
command_index,
vertex.slice.buffer.id(),
access(
vk::PipelineStageFlags2::VERTEX_INPUT,
vk::AccessFlags2::VERTEX_ATTRIBUTE_READ,
SubresourceScopeInner::Buffer(vertex.slice.range),
),
);
}
if let Some(index) = self.index {
map.insert(
command_index,
index.slice.buffer.id(),
access(
vk::PipelineStageFlags2::INDEX_INPUT,
vk::AccessFlags2::INDEX_READ,
SubresourceScopeInner::Buffer(index.slice.range),
),
);
}
if let Some(indirect) = self.indirect {
map.insert(
command_index,
indirect.slice.buffer.id(),
access(
vk::PipelineStageFlags2::DRAW_INDIRECT,
vk::AccessFlags2::INDIRECT_COMMAND_READ,
SubresourceScopeInner::Buffer(indirect.slice.range),
),
);
}
for set in &self.descriptor_sets {
for declared in &set.accesses {
let scope = match *declared {
DescriptorResourceAccess::Buffer {
range,
stage,
access: mask,
..
} => access(stage, mask, SubresourceScopeInner::Buffer(range)),
DescriptorResourceAccess::Texture {
range,
layout,
stage,
access: mask,
..
} => access(
stage,
mask,
SubresourceScopeInner::Texture { range, layout },
),
};
let resource = match *declared {
DescriptorResourceAccess::Buffer { buffer, .. } => buffer.id(),
DescriptorResourceAccess::Texture { texture, .. } => texture.id(),
};
map.insert(command_index, resource, scope);
}
}
}
}
impl RecordedCommand for Draw {
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
for vertex in &self.vertices {
let buffers = [recorder.buffer(vertex.slice.buffer.id())?];
let offsets = [vertex.slice.range.offset];
unsafe {
recorder.device.cmd_bind_vertex_buffers(
recorder.command_buffer,
vertex.binding,
&buffers,
&offsets,
);
}
}
if let Some(index) = self.index {
let index_type = match index.format {
IndexFormat::Uint8 => vk::IndexType::UINT8_KHR,
IndexFormat::Uint16 => vk::IndexType::UINT16,
IndexFormat::Uint32 => vk::IndexType::UINT32,
};
unsafe {
recorder.device.cmd_bind_index_buffer(
recorder.command_buffer,
recorder.buffer(index.slice.buffer.id())?,
index.slice.range.offset,
index_type,
);
}
}
for constants in &self.push_constants {
unsafe {
recorder.device.cmd_push_constants(
recorder.command_buffer,
self.pipeline_layout,
constants.stage_flags,
constants.offset,
&constants.data,
);
}
}
let descriptor_sets: Vec<_> = self.descriptor_sets.iter().map(|set| set.raw).collect();
if !descriptor_sets.is_empty() {
unsafe {
recorder.device.cmd_bind_descriptor_sets(
recorder.command_buffer,
vk::PipelineBindPoint::GRAPHICS,
self.pipeline_layout,
0,
&descriptor_sets,
&[],
);
}
}
unsafe {
recorder.device.cmd_draw(
recorder.command_buffer,
self.vertex_count,
self.instance_count,
self.first_vertex,
self.first_instance,
);
if let Some(indirect) = self.indirect {
let buffer = recorder.buffer(indirect.slice.buffer.id())?;
if matches!(self.args, DrawArgs::Indexed { .. }) {
recorder.device.cmd_draw_indexed_indirect(
recorder.command_buffer,
buffer,
indirect.slice.range.offset,
indirect.draw_count,
indirect.stride,
);
} else {
recorder.device.cmd_draw_indirect(
recorder.command_buffer,
buffer,
indirect.slice.range.offset,
indirect.draw_count,
indirect.stride,
);
}
} else {
match self.args {
DrawArgs::Direct {
vertex_count,
instance_count,
first_vertex,
first_instance,
} => recorder.device.cmd_draw(
recorder.command_buffer,
vertex_count,
instance_count,
first_vertex,
first_instance,
),
DrawArgs::Indexed {
index_count,
instance_count,
first_index,
vertex_offset,
first_instance,
} => recorder.device.cmd_draw_indexed(
recorder.command_buffer,
index_count,
instance_count,
first_index,
vertex_offset,
first_instance,
),
}
}
}
Ok(())
}

View file

@ -18,8 +18,9 @@ use renderer_device::QueueFamilyIndex;
use crate::{
recorder::{CommandIndex, CommandList, CommandRecorder, RecordError, scope_range},
resources::{
Buffer, GraphBufferDesc, GraphTextureDesc, ResourceDesc, ResourceId, Subresource,
SubresourceScope, SubresourceScopeInner, Texture, access_flag_is_write,
Buffer, DescriptorResourceAccess, GraphBufferDesc, GraphDescriptorSet, GraphTextureDesc,
ResourceDesc, ResourceId, Subresource, SubresourceScope, SubresourceScopeInner, Texture,
access_flag_is_write,
},
};
@ -244,6 +245,19 @@ impl RenderGraph {
}))
}
/// Associate graph-visible resource accesses with a raw Vulkan descriptor set.
///
/// The caller is responsible for declaring every resource referenced by the
/// set accurately; compilation validates that each declared resource belongs
/// to this graph, but cannot inspect the raw descriptor contents.
pub fn track_descriptor_set(
&self,
raw: vk::DescriptorSet,
accesses: Vec<DescriptorResourceAccess>,
) -> GraphDescriptorSet {
GraphDescriptorSet { raw, accesses }
}
pub fn push<C: crate::commands::Command + 'static>(&mut self, command: C) -> CommandIndex {
self.commands.push(command)
}
@ -923,8 +937,11 @@ fn texture_size_estimate(desc: &GraphTextureDesc) -> u64 {
mod tests {
use super::*;
use crate::{
commands::{ClearTexture, CopyBuffer},
resources::{BufferRange, GraphBufferDesc, GraphTextureDesc, ResourceDesc},
commands::{ClearTexture, CopyBuffer, Draw, DrawArgs},
resources::{
BufferRange, BufferSlice, GraphBufferDesc, GraphTextureDesc, IndexBinding, IndexFormat,
IndirectBinding, ResourceDesc, VertexBinding,
},
};
#[test]
@ -1079,6 +1096,237 @@ mod tests {
assert!(compiled.interference().interferes(src.id(), dst.id()));
}
#[test]
fn draw_vertex_index_and_indirect_reads_follow_copies_and_derive_usage() {
let mut graph = RenderGraph::new();
let source = graph.create_buffer(GraphBufferDesc {
size: 64,
..Default::default()
});
let vertex = graph.create_buffer(GraphBufferDesc {
size: 64,
..Default::default()
});
let index = graph.create_buffer(GraphBufferDesc {
size: 64,
..Default::default()
});
let indirect = graph.create_buffer(GraphBufferDesc {
size: 64,
..Default::default()
});
let range = BufferRange {
offset: 0,
size: 64,
};
for dst in [vertex, index, indirect] {
graph.push(CopyBuffer {
src: source,
src_range: range,
dst,
dst_range: range,
});
}
graph.push(Draw {
args: DrawArgs::Indexed {
index_count: 3,
instance_count: 1,
first_index: 0,
vertex_offset: 0,
first_instance: 0,
},
pipeline_layout: vk::PipelineLayout::null(),
vertices: vec![VertexBinding {
binding: 0,
slice: BufferSlice {
buffer: vertex,
range,
},
}],
index: Some(IndexBinding {
slice: BufferSlice {
buffer: index,
range,
},
format: IndexFormat::Uint16,
}),
indirect: Some(IndirectBinding {
slice: BufferSlice {
buffer: indirect,
range,
},
draw_count: 1,
stride: 20,
}),
descriptor_sets: Vec::new(),
push_constants: Vec::new(),
});
let accesses = graph.commands.side_effects.command_accesses();
assert_eq!(
derive_buffer_usage(
&accesses_for_resource(vertex.id(), accesses),
vk::BufferUsageFlags::empty()
),
vk::BufferUsageFlags::TRANSFER_DST | vk::BufferUsageFlags::VERTEX_BUFFER
);
assert_eq!(
derive_buffer_usage(
&accesses_for_resource(index.id(), accesses),
vk::BufferUsageFlags::empty()
),
vk::BufferUsageFlags::TRANSFER_DST | vk::BufferUsageFlags::INDEX_BUFFER
);
assert_eq!(
derive_buffer_usage(
&accesses_for_resource(indirect.id(), accesses),
vk::BufferUsageFlags::empty()
),
vk::BufferUsageFlags::TRANSFER_DST | vk::BufferUsageFlags::INDIRECT_BUFFER
);
let compiled = graph.compile().expect("graph compiles");
assert_eq!(compiled.batches().len(), 2);
assert_eq!(
compiled.batches()[0],
[CommandIndex(0), CommandIndex(1), CommandIndex(2)]
);
assert_eq!(compiled.batches()[1], [CommandIndex(3)]);
let draw_barrier = compiled
.barriers()
.iter()
.find(|barrier| barrier.before_command == CommandIndex(3))
.expect("draw input barriers are generated");
assert_eq!(draw_barrier.resources.len(), 3);
assert!(
draw_barrier
.resources
.iter()
.any(|resource| resource.id == vertex.id())
);
assert!(
draw_barrier
.resources
.iter()
.any(|resource| resource.id == index.id())
);
assert!(
draw_barrier
.resources
.iter()
.any(|resource| resource.id == indirect.id())
);
}
#[test]
fn descriptor_set_declarations_create_buffer_and_texture_accesses() {
let mut graph = RenderGraph::new();
let source = graph.create_buffer(GraphBufferDesc {
size: 32,
..Default::default()
});
let uniform = graph.create_buffer(GraphBufferDesc {
size: 32,
..Default::default()
});
let texture = graph.create_texture(GraphTextureDesc {
format: vk::Format::R8G8B8A8_UNORM,
..Default::default()
});
let buffer_range = BufferRange {
offset: 0,
size: 32,
};
graph.push(CopyBuffer {
src: source,
src_range: buffer_range,
dst: uniform,
dst_range: buffer_range,
});
graph.push(ClearTexture {
texture,
range: texture_range(vk::ImageAspectFlags::COLOR, 0, 1, 0, 1),
layout: vk::ImageLayout::TRANSFER_DST_OPTIMAL,
color: [0.0; 4],
});
let set = graph.track_descriptor_set(
vk::DescriptorSet::null(),
vec![
DescriptorResourceAccess::Buffer {
buffer: uniform,
range: buffer_range,
stage: vk::PipelineStageFlags2::VERTEX_SHADER,
access: vk::AccessFlags2::UNIFORM_READ,
},
DescriptorResourceAccess::Texture {
texture,
range: texture_range(vk::ImageAspectFlags::COLOR, 0, 1, 0, 1),
layout: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
stage: vk::PipelineStageFlags2::FRAGMENT_SHADER,
access: vk::AccessFlags2::SHADER_SAMPLED_READ,
},
],
);
graph.push(Draw {
args: DrawArgs::Direct {
vertex_count: 3,
instance_count: 1,
first_vertex: 0,
first_instance: 0,
},
pipeline_layout: vk::PipelineLayout::null(),
vertices: Vec::new(),
index: None,
indirect: None,
descriptor_sets: vec![set],
push_constants: Vec::new(),
});
let accesses = graph.commands.side_effects.command_accesses();
assert!(
derive_buffer_usage(
&accesses_for_resource(uniform.id(), accesses),
vk::BufferUsageFlags::empty()
)
.contains(vk::BufferUsageFlags::UNIFORM_BUFFER)
);
assert!(
derive_image_usage(
&accesses_for_resource(texture.id(), accesses),
vk::ImageUsageFlags::empty()
)
.contains(vk::ImageUsageFlags::SAMPLED)
);
let compiled = graph.compile().expect("graph compiles");
let draw_barrier = compiled
.barriers()
.iter()
.find(|barrier| barrier.before_command == CommandIndex(2))
.expect("descriptor accesses produce barriers");
assert_eq!(draw_barrier.resources.len(), 2);
let texture_barrier = draw_barrier
.resources
.iter()
.find(|resource| resource.id == texture.id())
.expect("sampled texture barrier is present");
assert_eq!(
texture_barrier.new_layout,
Some(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
);
assert_eq!(
texture_barrier.dst_access,
vk::AccessFlags2::SHADER_SAMPLED_READ
);
assert!(compiled.resource_lifetimes().contains_key(&uniform.id()));
assert!(compiled.resource_lifetimes().contains_key(&texture.id()));
assert!(
compiled
.interference()
.interferes(uniform.id(), texture.id())
);
}
#[test]
fn write_then_read_creates_order_and_barrier() {
let mut graph = RenderGraph::new();

View file

@ -8,4 +8,8 @@ pub mod resources;
pub use graph::{CompiledGraph, GraphError, RenderGraph, ResourceInterference};
pub use recorder::{CommandIndex, CommandList, CommandRecorder, SideEffectMap};
pub use resources::{Buffer, GraphBufferDesc, GraphTextureDesc, ResourceId, Texture};
pub use resources::{
Buffer, BufferRange, BufferSlice, DescriptorResourceAccess, GraphBufferDesc,
GraphDescriptorSet, GraphTextureDesc, IndexBinding, IndexFormat, IndirectBinding,
PushConstantData, ResourceId, Texture, VertexBinding,
};

View file

@ -130,6 +130,79 @@ impl BufferRange {
}
}
/// A graph buffer together with the byte range used by a command.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BufferSlice {
pub buffer: Buffer,
pub range: BufferRange,
}
/// A vertex-input binding. `binding` is the Vulkan vertex binding number.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VertexBinding {
pub binding: u32,
pub slice: BufferSlice,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IndexFormat {
Uint8,
Uint16,
Uint32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IndexBinding {
pub slice: BufferSlice,
pub format: IndexFormat,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IndirectBinding {
pub slice: BufferSlice,
/// Number of draw commands in the indirect buffer.
pub draw_count: u32,
/// Stride in bytes between draw commands. Must satisfy Vulkan's alignment
/// and minimum-size requirements for the corresponding draw command.
pub stride: u32,
}
#[derive(Debug, Clone, Copy)]
pub enum DescriptorResourceAccess {
Buffer {
buffer: Buffer,
range: BufferRange,
stage: vk::PipelineStageFlags2,
access: vk::AccessFlags2,
},
Texture {
texture: Texture,
range: vk::ImageSubresourceRange,
layout: vk::ImageLayout,
stage: vk::PipelineStageFlags2,
access: vk::AccessFlags2,
},
}
/// A Vulkan descriptor set accompanied by graph-visible resource accesses.
///
/// Callers must list every resource used by the set with its accurate range,
/// pipeline stage, access mask, and (for textures) image layout. The graph can
/// track these declarations, but cannot inspect or verify raw descriptor-set
/// contents.
#[derive(Debug, Clone)]
pub struct GraphDescriptorSet {
pub raw: vk::DescriptorSet,
pub accesses: Vec<DescriptorResourceAccess>,
}
#[derive(Debug, Clone)]
pub struct PushConstantData {
pub stage_flags: vk::ShaderStageFlags,
pub offset: u32,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, Copy)]
pub enum Subresource {
Buffer(BufferRange),