vidya/crates/renderer/src/render_graph/commands.rs
2026-04-17 11:27:40 +02:00

825 lines
25 KiB
Rust

//! This module defines the commands that can be recorded in the render
//! graph.
//! Commands can be allocated, and then recorded into a command
//! buffer.
//! the lifecycle of a command is thus as follows:
//! 1. A command is constructed, with its parameters and resource dependencies.
//! 2. The command is inserted into a command list. At this stage, the command's side effects are recorded and barriers are automatically inserted as required.
//! 3. When the command list is recorded into a command buffer, the command's `apply` method is called, which translates the command into actual GPU commands using the provided `CommandRecorder`.
//!
//! Each command declares its side effects, such as what resources it
//! reads from or write to, and which pipeline stages it affects. Commands are
//! recorded sequentially, in the order they were inserted into a command
//! buffer. During command insertion
use crate::{
device::DeviceOwned,
render_graph::recorder::{CommandRecorder, SideEffectMap},
};
use super::resources::*;
pub struct ImportResource<T: Resource> {
pub resource: T,
pub access: vk::AccessFlags2,
pub stage: vk::PipelineStageFlags2,
pub layout: Option<vk::ImageLayout>,
}
impl<T: Resource> Command for ImportResource<T> {
fn side_effects(&self, mut map: SideEffectMap) {
self.resource
.side_effect(map.reborrow().into_side_effect_map2(
self.stage,
self.access,
self.layout,
));
}
fn apply(self, _recorder: &mut CommandRecorder) {
// No actual GPU commands needed for import, as it's just a logical operation
}
}
pub trait BufferImageBufferCopyRegion {
fn regions(&self) -> &[BufferRows];
}
impl BufferImageBufferCopyRegion for BufferRows {
fn regions(&self) -> &[BufferRows] {
core::slice::from_ref(self)
}
}
impl<T: AsRef<[BufferRows]>> BufferImageBufferCopyRegion for T {
fn regions(&self) -> &[BufferRows] {
self.as_ref()
}
}
pub trait ImageCopyRegion {
fn regions(&self) -> &[TextureRegion];
}
impl ImageCopyRegion for TextureRegion {
fn regions(&self) -> &[TextureRegion] {
core::slice::from_ref(self)
}
}
impl<T: AsRef<[TextureRegion]>> ImageCopyRegion for T {
fn regions(&self) -> &[TextureRegion] {
self.as_ref()
}
}
pub trait BufferCopyRegion {
fn regions(&self) -> &[BufferRange];
}
impl BufferCopyRegion for BufferRange {
fn regions(&self) -> &[BufferRange] {
core::slice::from_ref(self)
}
}
impl<T: AsRef<[BufferRange]>> BufferCopyRegion for T {
fn regions(&self) -> &[BufferRange] {
self.as_ref()
}
}
pub struct CopyBuffers<T: BufferCopyRegion> {
pub src: Buffer,
pub src_region: T,
pub dst: Buffer,
pub dst_region: T,
}
pub struct CopyTextures<T: ImageCopyRegion> {
pub src: Texture,
pub src_region: T,
pub dst: Texture,
pub dst_region: T,
}
pub struct CopyBufferToTexture<T: BufferImageBufferCopyRegion, U: ImageCopyRegion> {
pub src: Buffer,
pub src_region: T,
pub dst: Texture,
pub dst_region: U,
}
pub struct CopyTextureToBuffer<T: ImageCopyRegion, U: BufferImageBufferCopyRegion> {
pub src: Texture,
pub src_region: T,
pub dst: Buffer,
pub dst_region: U,
}
fn copy_side_effects_inner<T, U>(_src: &T, _dst: &U, _map: SideEffectMap)
where
T: Resource,
U: Resource,
{
unimplemented!()
}
impl<T, U> Command for CopyBufferToTexture<T, U>
where
T: BufferImageBufferCopyRegion,
U: ImageCopyRegion,
{
fn side_effects(&self, mut map: SideEffectMap) {
let block_size = self.dst.format().block_size() as u64;
let texels_per_block = self.dst.format().texels_per_block() as u64;
let src_id = self.src.id();
let dst_id = self.dst.id();
for (src_region, dst_region) in self
.src_region
.regions()
.iter()
.zip(self.dst_region.regions())
{
let texel_count = if src_region.row_size != 0 {
let texel_count = src_region.row_size as u64 * src_region.row_count as u64;
debug_assert!(
texel_count % texels_per_block == 0,
"Buffer copy region must be a multiple of the block size"
);
texel_count
} else {
dst_region.extent.width as u64
* dst_region.extent.height as u64
* dst_region.extent.depth as u64
};
let byte_size = texel_count.div_ceil(texels_per_block) * block_size;
map.with(
vk::PipelineStageFlags2::COPY,
vk::AccessFlags2::TRANSFER_READ,
Some(vk::ImageLayout::TRANSFER_SRC_OPTIMAL),
)
.insert_range(
src_id,
BufferRange {
offset: src_region.offset,
size: byte_size,
},
);
map.with(
vk::PipelineStageFlags2::COPY,
vk::AccessFlags2::TRANSFER_WRITE,
Some(vk::ImageLayout::TRANSFER_DST_OPTIMAL),
)
.insert_range(dst_id, dst_region.range);
}
}
fn apply(self, recorder: &mut CommandRecorder) {
let cmd = recorder.cmd_buffer();
let dev = &cmd.device().raw;
let regions = self
.src_region
.regions()
.iter()
.zip(self.dst_region.regions())
.map(|(src_region, dst_region)| {
vk::BufferImageCopy2::default()
.buffer_offset(src_region.offset)
.buffer_row_length(src_region.row_size)
.buffer_image_height(src_region.row_count)
.image_offset(dst_region.offset)
.image_extent(dst_region.extent)
.image_subresource(
vk::ImageSubresourceLayers::default()
.aspect_mask(dst_region.range.aspect)
.mip_level(dst_region.range.mip_level)
.base_array_layer(dst_region.range.array_layers.start())
.layer_count(dst_region.range.array_layers.len()),
)
})
.collect::<Box<[_]>>();
let info = vk::CopyBufferToImageInfo2::default()
.src_buffer(recorder.get_buffer_handle(self.src.id()))
.dst_image(recorder.get_image_handle(self.dst.id()))
.dst_image_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
.regions(&regions);
unsafe {
dev.cmd_copy_buffer_to_image2(cmd.raw(), &info);
}
}
}
impl<T, U> Command for CopyTextureToBuffer<T, U>
where
T: ImageCopyRegion,
U: BufferImageBufferCopyRegion,
{
fn side_effects(&self, mut map: SideEffectMap) {
let block_size = self.src.format().block_size() as u64;
let texels_per_block = self.src.format().texels_per_block() as u64;
let src_id = self.src.id();
let dst_id = self.dst.id();
for (src_region, dst_region) in self
.src_region
.regions()
.iter()
.zip(self.dst_region.regions())
{
let texel_count = if dst_region.row_size != 0 {
let texel_count = dst_region.row_size as u64 * dst_region.row_count as u64;
debug_assert!(
texel_count % texels_per_block == 0,
"Buffer copy region must be a multiple of the block size"
);
texel_count
} else {
src_region.extent.width as u64
* src_region.extent.height as u64
* src_region.extent.depth as u64
};
let byte_size = texel_count.div_ceil(texels_per_block) * block_size;
map.with(
vk::PipelineStageFlags2::COPY,
vk::AccessFlags2::TRANSFER_READ,
Some(vk::ImageLayout::TRANSFER_SRC_OPTIMAL),
)
.insert_range(dst_id, src_region.range);
map.with(
vk::PipelineStageFlags2::COPY,
vk::AccessFlags2::TRANSFER_WRITE,
Some(vk::ImageLayout::TRANSFER_DST_OPTIMAL),
)
.insert_range(
src_id,
BufferRange {
offset: dst_region.offset,
size: byte_size,
},
);
}
}
fn apply(self, recorder: &mut CommandRecorder) {
let cmd = recorder.cmd_buffer();
let dev = &cmd.device().raw;
let regions = self
.src_region
.regions()
.iter()
.zip(self.dst_region.regions())
.map(|(src_region, dst_region)| {
vk::BufferImageCopy2::default()
.buffer_offset(dst_region.offset)
.buffer_row_length(dst_region.row_size)
.buffer_image_height(dst_region.row_count)
.image_offset(src_region.offset)
.image_extent(src_region.extent)
.image_subresource(
vk::ImageSubresourceLayers::default()
.aspect_mask(src_region.range.aspect)
.mip_level(src_region.range.mip_level)
.base_array_layer(src_region.range.array_layers.start())
.layer_count(src_region.range.array_layers.len()),
)
})
.collect::<Box<[_]>>();
let info = vk::CopyImageToBufferInfo2::default()
.dst_buffer(recorder.get_buffer_handle(self.src.id()))
.src_image(recorder.get_image_handle(self.dst.id()))
.src_image_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
.regions(&regions);
unsafe {
dev.cmd_copy_image_to_buffer2(cmd.raw(), &info);
}
}
}
impl<T> Command for CopyBuffers<T>
where
T: BufferCopyRegion,
{
fn side_effects(&self, mut map: SideEffectMap) {
for region in self.src_region.regions() {
map.with(
vk::PipelineStageFlags2::COPY,
vk::AccessFlags2::TRANSFER_READ,
None,
)
.insert_range(self.src.id(), *region);
}
for region in self.dst_region.regions() {
map.with(
vk::PipelineStageFlags2::COPY,
vk::AccessFlags2::TRANSFER_WRITE,
None,
)
.insert_range(self.dst.id(), *region);
}
}
fn apply(self, recorder: &mut CommandRecorder) {
let cmd = recorder.cmd_buffer();
let dev = &cmd.device().raw;
let regions = self
.src_region
.regions()
.iter()
.zip(self.dst_region.regions())
.map(|(src_region, dst_region)| {
vk::BufferCopy2::default()
.src_offset(src_region.offset)
.dst_offset(dst_region.offset)
.size(src_region.size)
})
.collect::<Box<[_]>>();
let info = vk::CopyBufferInfo2::default()
.dst_buffer(recorder.get_buffer_handle(self.dst.id()))
.src_buffer(recorder.get_buffer_handle(self.src.id()))
.regions(&regions);
unsafe {
dev.cmd_copy_buffer2(cmd.raw(), &info);
}
}
}
impl<T> Command for CopyTextures<T>
where
T: ImageCopyRegion,
{
fn side_effects(&self, mut map: SideEffectMap) {
for region in self.src_region.regions() {
map.with(
vk::PipelineStageFlags2::COPY,
vk::AccessFlags2::TRANSFER_READ,
None,
)
.insert_range(self.src.id(), region.range);
}
for region in self.dst_region.regions() {
map.with(
vk::PipelineStageFlags2::COPY,
vk::AccessFlags2::TRANSFER_WRITE,
None,
)
.insert_range(self.dst.id(), region.range);
}
}
fn apply(self, recorder: &mut CommandRecorder) {
let cmd = recorder.cmd_buffer();
let dev = &cmd.device().raw;
let regions = self
.src_region
.regions()
.iter()
.zip(self.dst_region.regions())
.map(|(src_region, dst_region)| {
vk::ImageCopy2::default()
.src_offset(src_region.offset)
.src_subresource(src_region.range.into())
.dst_offset(dst_region.offset)
.dst_subresource(dst_region.range.into())
.extent(src_region.extent)
})
.collect::<Box<[_]>>();
let info = vk::CopyImageInfo2::default()
.src_image(recorder.get_image_handle(self.src.id()))
.src_image_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
.dst_image(recorder.get_image_handle(self.dst.id()))
.dst_image_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
.regions(&regions);
unsafe {
dev.cmd_copy_image2(cmd.raw(), &info);
}
}
}
pub struct ClearTexture {
pub dst: Texture,
pub dst_region: TextureRegion,
pub clear_value: [f32; 4],
}
impl Command for ClearTexture {
fn side_effects(&self, mut map: SideEffectMap) {
map.with(
vk::PipelineStageFlags2::CLEAR,
vk::AccessFlags2::TRANSFER_WRITE,
Some(vk::ImageLayout::TRANSFER_DST_OPTIMAL),
)
.insert_range(self.dst.id(), self.dst_region.range);
}
fn apply(self, recorder: &mut CommandRecorder) {
let cmd = recorder.cmd_buffer();
let dev = &cmd.device().raw;
let values = vk::ClearColorValue {
float32: self.clear_value,
};
unsafe {
dev.cmd_clear_color_image(
cmd.raw(),
recorder.get_image_handle(self.dst.id()),
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
&values,
&[self.dst_region.range.into()],
);
}
}
}
pub struct UpdateBuffer {
pub dst: Write<BufferSlice>,
pub data: Vec<u8>,
}
impl Command for UpdateBuffer {
fn side_effects(&self, mut map: SideEffectMap) {
self.dst.side_effect(map.reborrow().into_side_effect_map2(
vk::PipelineStageFlags2::TRANSFER,
vk::AccessFlags2::TRANSFER_WRITE,
None,
));
}
fn apply(self, _recorder: &mut CommandRecorder) {}
}
pub struct BeginRendering {
pub color_attachments: Vec<TextureRegion>,
pub depth_attachment: Option<TextureRegion>,
pub stencil_attachment: Option<TextureRegion>,
pub area: (u32, u32),
pub layers: u32,
}
impl Command for BeginRendering {
fn side_effects(&self, mut map: SideEffectMap) {
for attachment in &self.color_attachments {
// The dependencies declared here need to be barrier'd before the
// render pass: the spec says pipelineBarrier inside of a render
// pass instance referencing framebuffer-local stages must contain
// only framebuffer-local stages.
// TODO: consider loadop and storeop?
attachment.side_effect(map.reborrow().into_side_effect_map2(
vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT,
vk::AccessFlags2::COLOR_ATTACHMENT_READ,
Some(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL),
));
}
if let Some(depth) = &self.depth_attachment {
depth.side_effect(map.reborrow().into_side_effect_map2(
vk::PipelineStageFlags2::EARLY_FRAGMENT_TESTS,
vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_READ,
Some(vk::ImageLayout::DEPTH_ATTACHMENT_OPTIMAL),
));
}
if let Some(stencil) = &self.stencil_attachment {
stencil.side_effect(map.reborrow().into_side_effect_map2(
vk::PipelineStageFlags2::EARLY_FRAGMENT_TESTS,
vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_READ,
Some(vk::ImageLayout::STENCIL_ATTACHMENT_OPTIMAL),
));
}
}
fn apply(self, _recorder: &mut CommandRecorder) {}
}
pub struct EndRendering;
impl Command for EndRendering {
fn side_effects(&self, _map: SideEffectMap) {
// No resource access, but ends the render pass
}
fn apply(self, _recorder: &mut CommandRecorder) {}
}
pub struct BindPipeline(pub vk::Pipeline, pub vk::PipelineBindPoint);
impl Command for BindPipeline {
fn side_effects(&self, _map: SideEffectMap) {
// No resource access, but affects the pipeline state
}
fn apply(self, recorder: &mut CommandRecorder) {
let cmd = recorder.cmd_buffer();
let dev = &cmd.device().raw;
unsafe {
dev.cmd_bind_pipeline(cmd.raw(), self.1, self.0);
}
}
}
pub struct BindVertexBuffers {
pub buffers: Vec<BufferSlice>,
pub first_binding: u32,
}
impl Command for BindVertexBuffers {
fn side_effects(&self, mut map: SideEffectMap) {
for buffer in &self.buffers {
buffer.side_effect(map.reborrow().into_side_effect_map2(
vk::PipelineStageFlags2::VERTEX_ATTRIBUTE_INPUT,
vk::AccessFlags2::VERTEX_ATTRIBUTE_READ,
None,
));
}
}
fn apply(self, recorder: &mut CommandRecorder) {
let cmd = recorder.cmd_buffer();
let dev = &cmd.device().raw;
let buffers = self
.buffers
.iter()
.map(|buffer| recorder.get_buffer_handle(buffer.id()))
.collect::<Vec<_>>();
let offsets = self
.buffers
.iter()
.map(|buffer| buffer.range.offset)
.collect::<Vec<_>>();
unsafe {
dev.cmd_bind_vertex_buffers(cmd.raw(), self.first_binding, &buffers, &offsets);
}
}
}
pub struct BindIndexBuffer(pub BufferSlice, pub IndexFormat);
impl Command for BindIndexBuffer {
fn side_effects(&self, mut map: SideEffectMap) {
self.0.side_effect(map.reborrow().into_side_effect_map2(
vk::PipelineStageFlags2::INDEX_INPUT,
vk::AccessFlags2::INDEX_READ,
None,
));
}
fn apply(self, recorder: &mut CommandRecorder) {
let cmd = recorder.cmd_buffer();
let dev = &cmd.device().raw;
let buffer = recorder.get_buffer_handle(self.0.id());
let index_type = match self.1 {
IndexFormat::Uint8 => vk::IndexType::UINT8_KHR,
IndexFormat::Uint16 => vk::IndexType::UINT16,
IndexFormat::Uint32 => vk::IndexType::UINT32,
};
unsafe {
dev.cmd_bind_index_buffer(cmd.raw(), buffer, self.0.range.offset, index_type);
}
}
}
pub struct BindDescriptorSets {
pub first_set: u32,
pub pipeline_layout: vk::PipelineLayout,
pub bind_point: vk::PipelineBindPoint,
pub sets: Vec<vk::DescriptorSet>,
}
impl Command for BindDescriptorSets {
fn side_effects(&self, _map: SideEffectMap) {
// No resource access, but affects the descriptor set state
}
fn apply(self, recorder: &mut CommandRecorder) {
let cmd = recorder.cmd_buffer();
let dev = &cmd.device().raw;
unsafe {
dev.cmd_bind_descriptor_sets(
cmd.raw(),
self.bind_point,
self.pipeline_layout,
self.first_set,
&self.sets,
&[],
);
}
}
}
pub struct SetViewport {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub min_depth: f32,
pub max_depth: f32,
}
impl Command for SetViewport {
fn side_effects(&self, _map: SideEffectMap) {
// No resource access, but affects the viewport state
}
fn apply(self, _recorder: &mut CommandRecorder) {}
}
pub struct SetScissor {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
impl Command for SetScissor {
fn side_effects(&self, _map: SideEffectMap) {
// No resource access, but affects the scissor state
}
fn apply(self, _recorder: &mut CommandRecorder) {}
}
pub struct PushConstants {
pub pipeline_layout: vk::PipelineLayout,
pub stage_flags: vk::ShaderStageFlags,
pub offset: u32,
pub data: Vec<u8>,
}
impl Command for PushConstants {
fn side_effects(&self, _map: SideEffectMap) {
// No resource access, but affects the push constant state
}
fn apply(self, recorder: &mut CommandRecorder) {
let cmd = recorder.cmd_buffer();
let dev = &cmd.device().raw;
unsafe {
dev.cmd_push_constants(
cmd.raw(),
self.pipeline_layout,
self.stage_flags,
self.offset,
&self.data,
);
}
}
}
pub struct DrawData {
pub vertex_count: u32,
pub instance_count: u32,
pub first_vertex: u32,
pub first_instance: u32,
}
pub struct DrawIndexedData {
pub index_count: u32,
pub instance_count: u32,
pub first_index: u32,
pub vertex_offset: i32,
pub first_instance: u32,
}
pub struct DrawIndirectData {
pub indirect_buffer: BufferSlice,
pub count: u32,
pub stride: u32,
}
pub struct DrawIndexedIndirectData {
pub indirect_buffer: BufferSlice,
pub count: u32,
pub stride: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IndexFormat {
Uint8,
Uint16,
Uint32,
}
pub struct Draw<T: IsDrawData> {
pub data: T,
pub vertex_buffers: Vec<BufferSlice>,
pub index_buffer: Option<(BufferSlice, IndexFormat)>,
}
impl<T: IsDrawData> Command for Draw<T> {
fn side_effects(&self, mut map: SideEffectMap) {
self.data.side_effects(map.reborrow());
for vertex_buffer in &self.vertex_buffers {
vertex_buffer.side_effect(map.reborrow().into_side_effect_map2(
vk::PipelineStageFlags2::VERTEX_ATTRIBUTE_INPUT,
vk::AccessFlags2::VERTEX_ATTRIBUTE_READ,
None,
));
}
if let Some((index_buffer, _)) = &self.index_buffer {
index_buffer.side_effect(map.reborrow().into_side_effect_map2(
vk::PipelineStageFlags2::INDEX_INPUT,
vk::AccessFlags2::INDEX_READ,
None,
));
}
}
fn apply(self, _recorder: &mut CommandRecorder) {}
}
pub struct PipelineBarrier;
impl Command for PipelineBarrier {
fn side_effects(&self, _map: SideEffectMap) {}
fn apply(self, _recorder: &mut CommandRecorder) {
todo!()
}
}
pub trait Command: Sized {
fn side_effects(&self, map: SideEffectMap);
fn apply(self, recorder: &mut CommandRecorder);
}
mod sealed {
pub trait IsDrawData {
fn side_effects(&self, _map: super::SideEffectMap) {}
}
pub trait InsideRenderPass {}
pub trait OutsideRenderPass {}
}
use ash::vk;
use ash_formats::FormatExt;
pub(super) use sealed::*;
impl IsDrawData for DrawData {}
impl IsDrawData for DrawIndexedData {}
impl IsDrawData for DrawIndirectData {
fn side_effects(&self, mut map: SideEffectMap) {
self.indirect_buffer
.side_effect(map.reborrow().into_side_effect_map2(
vk::PipelineStageFlags2::DRAW_INDIRECT,
vk::AccessFlags2::INDIRECT_COMMAND_READ,
None,
));
}
}
impl IsDrawData for DrawIndexedIndirectData {
fn side_effects(&self, mut map: SideEffectMap) {
self.indirect_buffer
.side_effect(map.reborrow().into_side_effect_map2(
vk::PipelineStageFlags2::DRAW_INDIRECT,
vk::AccessFlags2::INDIRECT_COMMAND_READ,
None,
));
}
}
impl OutsideRenderPass for BeginRendering {}
impl InsideRenderPass for EndRendering {}
impl<T: Resource, U: Resource> OutsideRenderPass for Copy<T, U> {}
impl OutsideRenderPass for ClearTexture {}
impl OutsideRenderPass for UpdateBuffer {}
impl InsideRenderPass for BindPipeline {}
impl OutsideRenderPass for BindPipeline {}
impl InsideRenderPass for BindVertexBuffers {}
impl OutsideRenderPass for BindVertexBuffers {}
impl InsideRenderPass for BindIndexBuffer {}
impl OutsideRenderPass for BindIndexBuffer {}
impl InsideRenderPass for BindDescriptorSets {}
impl OutsideRenderPass for BindDescriptorSets {}
impl OutsideRenderPass for PushConstants {}
impl InsideRenderPass for SetViewport {}
impl InsideRenderPass for SetScissor {}
impl InsideRenderPass for PushConstants {}
impl<T: IsDrawData> InsideRenderPass for Draw<T> {}
impl<T: IsDrawData> InsideRenderPass for T {}