initial renderer_rendergraph
This commit is contained in:
parent
f9c18bcd3c
commit
e7a6536df5
|
|
@ -4,3 +4,4 @@ version = "0.1.0"
|
|||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
ash.workspace = true
|
||||
|
|
|
|||
151
crates/renderer_rendergraph/src/commands.rs
Normal file
151
crates/renderer_rendergraph/src/commands.rs
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
use ash::vk;
|
||||
|
||||
use crate::{
|
||||
recorder::{CommandIndex, CommandRecorder, RecordError, RecordedCommand, SideEffectMap},
|
||||
resources::{
|
||||
Buffer, BufferRange, ResourceId, SubresourceScope, SubresourceScopeInner, Texture,
|
||||
},
|
||||
};
|
||||
|
||||
/// A recorded operation declares its accesses before compilation and applies
|
||||
/// itself to a Vulkan command buffer after resource binding.
|
||||
pub trait Command: RecordedCommand {
|
||||
fn side_effects(&self, command_index: CommandIndex, map: &mut SideEffectMap);
|
||||
}
|
||||
|
||||
fn access(
|
||||
stage: vk::PipelineStageFlags2,
|
||||
access: vk::AccessFlags2,
|
||||
subresource: SubresourceScopeInner,
|
||||
) -> SubresourceScope {
|
||||
SubresourceScope {
|
||||
stage,
|
||||
access,
|
||||
subresource,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct CopyBuffer {
|
||||
pub src: Buffer,
|
||||
pub src_range: BufferRange,
|
||||
pub dst: Buffer,
|
||||
pub dst_range: BufferRange,
|
||||
}
|
||||
|
||||
impl Command for CopyBuffer {
|
||||
fn side_effects(&self, command_index: CommandIndex, map: &mut SideEffectMap) {
|
||||
map.insert(
|
||||
command_index,
|
||||
self.src.id(),
|
||||
access(
|
||||
vk::PipelineStageFlags2::COPY,
|
||||
vk::AccessFlags2::TRANSFER_READ,
|
||||
SubresourceScopeInner::Buffer(self.src_range),
|
||||
),
|
||||
);
|
||||
map.insert(
|
||||
command_index,
|
||||
self.dst.id(),
|
||||
access(
|
||||
vk::PipelineStageFlags2::COPY,
|
||||
vk::AccessFlags2::TRANSFER_WRITE,
|
||||
SubresourceScopeInner::Buffer(self.dst_range),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl RecordedCommand for CopyBuffer {
|
||||
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
|
||||
let region = vk::BufferCopy2::default()
|
||||
.src_offset(self.src_range.offset)
|
||||
.dst_offset(self.dst_range.offset)
|
||||
.size(self.src_range.size.min(self.dst_range.size));
|
||||
let regions = [region];
|
||||
let info = vk::CopyBufferInfo2::default()
|
||||
.src_buffer(recorder.buffer(self.src.id())?)
|
||||
.dst_buffer(recorder.buffer(self.dst.id())?)
|
||||
.regions(®ions);
|
||||
unsafe {
|
||||
recorder
|
||||
.device
|
||||
.cmd_copy_buffer2(recorder.command_buffer, &info)
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ClearTexture {
|
||||
pub texture: Texture,
|
||||
pub range: vk::ImageSubresourceRange,
|
||||
pub layout: vk::ImageLayout,
|
||||
pub color: [f32; 4],
|
||||
}
|
||||
|
||||
impl Command for ClearTexture {
|
||||
fn side_effects(&self, command_index: CommandIndex, map: &mut SideEffectMap) {
|
||||
map.insert(
|
||||
command_index,
|
||||
self.texture.id(),
|
||||
access(
|
||||
vk::PipelineStageFlags2::CLEAR,
|
||||
vk::AccessFlags2::TRANSFER_WRITE,
|
||||
SubresourceScopeInner::Texture {
|
||||
range: self.range,
|
||||
layout: self.layout,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl RecordedCommand for ClearTexture {
|
||||
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
|
||||
let value = vk::ClearColorValue {
|
||||
float32: self.color,
|
||||
};
|
||||
unsafe {
|
||||
recorder.device.cmd_clear_color_image(
|
||||
recorder.command_buffer,
|
||||
recorder.image(self.texture.id())?,
|
||||
self.layout,
|
||||
&value,
|
||||
&[self.range],
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Draw {
|
||||
pub vertex_count: u32,
|
||||
pub instance_count: u32,
|
||||
pub first_vertex: u32,
|
||||
pub first_instance: u32,
|
||||
}
|
||||
|
||||
impl Command for Draw {
|
||||
fn side_effects(&self, _command_index: CommandIndex, _map: &mut SideEffectMap) {}
|
||||
}
|
||||
|
||||
impl RecordedCommand for Draw {
|
||||
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
|
||||
unsafe {
|
||||
recorder.device.cmd_draw(
|
||||
recorder.command_buffer,
|
||||
self.vertex_count,
|
||||
self.instance_count,
|
||||
self.first_vertex,
|
||||
self.first_instance,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resource_id(buffer: Buffer) -> ResourceId {
|
||||
buffer.id()
|
||||
}
|
||||
442
crates/renderer_rendergraph/src/graph.rs
Normal file
442
crates/renderer_rendergraph/src/graph.rs
Normal file
|
|
@ -0,0 +1,442 @@
|
|||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use ash::vk;
|
||||
|
||||
use crate::{
|
||||
recorder::{CommandIndex, CommandList, CommandRecorder, RecordError, scope_range},
|
||||
resources::{
|
||||
Buffer, ResourceDesc, ResourceId, Subresource, SubresourceScope, SubresourceScopeInner,
|
||||
Texture, access_flag_is_write,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum GraphError {
|
||||
UnknownResource(ResourceId),
|
||||
DependencyCycle,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BarrierResource {
|
||||
pub id: ResourceId,
|
||||
pub range: Subresource,
|
||||
pub src_stage: vk::PipelineStageFlags2,
|
||||
pub src_access: vk::AccessFlags2,
|
||||
pub dst_stage: vk::PipelineStageFlags2,
|
||||
pub dst_access: vk::AccessFlags2,
|
||||
pub old_layout: Option<vk::ImageLayout>,
|
||||
pub new_layout: Option<vk::ImageLayout>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PipelineBarrier {
|
||||
pub before_command: CommandIndex,
|
||||
pub resources: Vec<BarrierResource>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ResourceLifetime {
|
||||
pub first_command: CommandIndex,
|
||||
pub last_command: CommandIndex,
|
||||
/// Placeholder allocator binding; resources with disjoint lifetimes can
|
||||
/// share an offset when their descriptor sizes permit it.
|
||||
pub memory_offset: u64,
|
||||
}
|
||||
|
||||
pub struct RenderGraph {
|
||||
next_id: u64,
|
||||
resources: BTreeMap<ResourceId, ResourceDesc>,
|
||||
pub commands: CommandList,
|
||||
}
|
||||
|
||||
impl RenderGraph {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
next_id: 0,
|
||||
resources: BTreeMap::new(),
|
||||
commands: CommandList::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_resource(&mut self, desc: ResourceDesc) -> ResourceId {
|
||||
let id = ResourceId(self.next_id);
|
||||
self.next_id = self.next_id.checked_add(1).expect("resource id overflow");
|
||||
self.resources.insert(id, desc);
|
||||
id
|
||||
}
|
||||
|
||||
pub fn create_buffer(&mut self, desc: crate::resources::BufferDesc) -> Buffer {
|
||||
Buffer(self.add_resource(ResourceDesc::CreatedBuffer(desc)))
|
||||
}
|
||||
|
||||
pub fn create_texture(&mut self, desc: crate::resources::TextureDesc) -> Texture {
|
||||
Texture(self.add_resource(ResourceDesc::CreatedTexture(desc)))
|
||||
}
|
||||
|
||||
pub fn import_buffer(
|
||||
&mut self,
|
||||
handle: vk::Buffer,
|
||||
initial: crate::resources::ResourceState,
|
||||
) -> Buffer {
|
||||
Buffer(self.add_resource(ResourceDesc::ImportedBuffer { handle, initial }))
|
||||
}
|
||||
|
||||
pub fn import_texture(
|
||||
&mut self,
|
||||
handle: vk::Image,
|
||||
initial: crate::resources::ResourceState,
|
||||
) -> Texture {
|
||||
Texture(self.add_resource(ResourceDesc::ImportedTexture { handle, initial }))
|
||||
}
|
||||
|
||||
pub fn push<C: crate::commands::Command + 'static>(&mut self, command: C) -> CommandIndex {
|
||||
self.commands.push(command)
|
||||
}
|
||||
|
||||
pub fn compile(self) -> Result<CompiledGraph, GraphError> {
|
||||
let command_count = self.commands.len();
|
||||
let mut edges = vec![BTreeSet::new(); command_count];
|
||||
let mut indegree = vec![0usize; command_count];
|
||||
let accesses = self.commands.side_effects.command_accesses();
|
||||
|
||||
for current in 0..command_count {
|
||||
for prior in 0..current {
|
||||
let dependency = accesses[prior].iter().any(|earlier| {
|
||||
accesses[current].iter().any(|later| {
|
||||
earlier.resource == later.resource && earlier.scope.conflicts(&later.scope)
|
||||
})
|
||||
});
|
||||
if dependency && edges[prior].insert(current) {
|
||||
indegree[current] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut ready: BTreeSet<_> = indegree
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, °ree)| (degree == 0).then_some(index))
|
||||
.collect();
|
||||
let mut order = Vec::with_capacity(command_count);
|
||||
while let Some(index) = ready.pop_first() {
|
||||
order.push(index.into());
|
||||
for &dependent in &edges[index] {
|
||||
indegree[dependent] -= 1;
|
||||
if indegree[dependent] == 0 {
|
||||
ready.insert(dependent);
|
||||
}
|
||||
}
|
||||
}
|
||||
if order.len() != command_count {
|
||||
return Err(GraphError::DependencyCycle);
|
||||
}
|
||||
|
||||
for command_accesses in accesses {
|
||||
for access in command_accesses {
|
||||
if !self.resources.contains_key(&access.resource) {
|
||||
return Err(GraphError::UnknownResource(access.resource));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let barriers = build_barriers(&order, accesses, &self.resources);
|
||||
let lifetimes = bind_resources(&self.resources, accesses);
|
||||
|
||||
Ok(CompiledGraph {
|
||||
resources: self.resources,
|
||||
commands: self.commands,
|
||||
order,
|
||||
barriers,
|
||||
lifetimes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RenderGraph {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CompiledGraph {
|
||||
resources: BTreeMap<ResourceId, ResourceDesc>,
|
||||
commands: CommandList,
|
||||
order: Vec<CommandIndex>,
|
||||
barriers: Vec<PipelineBarrier>,
|
||||
lifetimes: BTreeMap<ResourceId, ResourceLifetime>,
|
||||
}
|
||||
|
||||
impl CompiledGraph {
|
||||
pub fn command_order(&self) -> &[CommandIndex] {
|
||||
&self.order
|
||||
}
|
||||
|
||||
pub fn barriers(&self) -> &[PipelineBarrier] {
|
||||
&self.barriers
|
||||
}
|
||||
|
||||
pub fn resource_lifetimes(&self) -> &BTreeMap<ResourceId, ResourceLifetime> {
|
||||
&self.lifetimes
|
||||
}
|
||||
|
||||
/// Record the compiled order into a real Vulkan command buffer. Imported
|
||||
/// handles are automatically bound. Created resources require a future
|
||||
/// allocator integration and currently return a missing-handle error when
|
||||
/// used by a command.
|
||||
pub fn record_commands(
|
||||
&mut self,
|
||||
recorder: &mut CommandRecorder<'_>,
|
||||
) -> Result<(), RecordError> {
|
||||
for (&id, &desc) in &self.resources {
|
||||
match desc {
|
||||
ResourceDesc::ImportedBuffer { handle, .. } => {
|
||||
recorder.buffers.insert(id, handle);
|
||||
}
|
||||
ResourceDesc::ImportedTexture { handle, .. } => {
|
||||
recorder.images.insert(id, handle);
|
||||
}
|
||||
ResourceDesc::CreatedBuffer(_) | ResourceDesc::CreatedTexture(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
let mut barriers = self.barriers.iter().peekable();
|
||||
for (ordered_index, &command_index) in self.order.iter().enumerate() {
|
||||
while barriers
|
||||
.peek()
|
||||
.is_some_and(|barrier| barrier.before_command == ordered_index.into())
|
||||
{
|
||||
recorder.pipeline_barrier(barriers.next().expect("peeked barrier"));
|
||||
}
|
||||
self.commands.commands[command_index.index()].apply(recorder)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn build_barriers(
|
||||
order: &[CommandIndex],
|
||||
accesses: &[Vec<crate::recorder::ResourceAccess>],
|
||||
resource_descs: &BTreeMap<ResourceId, ResourceDesc>,
|
||||
) -> Vec<PipelineBarrier> {
|
||||
let mut result = Vec::new();
|
||||
for (ordered_index, &command_index) in order.iter().enumerate() {
|
||||
let mut barrier_resources = Vec::new();
|
||||
for current in &accesses[command_index.index()] {
|
||||
let previous = order[..ordered_index]
|
||||
.iter()
|
||||
.rev()
|
||||
.flat_map(|&prior_index| accesses[prior_index.index()].iter().rev())
|
||||
.find(|previous| {
|
||||
previous.resource == current.resource
|
||||
&& previous.scope.conflicts(¤t.scope)
|
||||
});
|
||||
if let Some(previous) = previous {
|
||||
let (old_layout, new_layout) = layouts(previous.scope, current.scope);
|
||||
barrier_resources.push(BarrierResource {
|
||||
id: current.resource,
|
||||
range: scope_range(current.scope),
|
||||
src_stage: previous.scope.stage,
|
||||
src_access: previous.scope.access,
|
||||
dst_stage: current.scope.stage,
|
||||
dst_access: current.scope.access,
|
||||
old_layout,
|
||||
new_layout,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let desc = resource_descs[¤t.resource];
|
||||
let initial = desc.initial_state();
|
||||
let (old_layout, new_layout) = layouts_from_state(initial, current.scope);
|
||||
let layout_transition = old_layout != new_layout && new_layout.is_some();
|
||||
let initial_hazard = matches!(
|
||||
desc,
|
||||
ResourceDesc::ImportedBuffer { .. } | ResourceDesc::ImportedTexture { .. }
|
||||
) && !initial.access.is_empty()
|
||||
&& (access_flag_is_write(initial.access)
|
||||
|| access_flag_is_write(current.scope.access));
|
||||
if layout_transition || initial_hazard {
|
||||
barrier_resources.push(BarrierResource {
|
||||
id: current.resource,
|
||||
range: scope_range(current.scope),
|
||||
src_stage: initial.stage,
|
||||
src_access: initial.access,
|
||||
dst_stage: current.scope.stage,
|
||||
dst_access: current.scope.access,
|
||||
old_layout,
|
||||
new_layout,
|
||||
});
|
||||
}
|
||||
}
|
||||
if !barrier_resources.is_empty() {
|
||||
result.push(PipelineBarrier {
|
||||
before_command: ordered_index.into(),
|
||||
resources: barrier_resources,
|
||||
});
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn layouts(
|
||||
previous: SubresourceScope,
|
||||
current: SubresourceScope,
|
||||
) -> (Option<vk::ImageLayout>, Option<vk::ImageLayout>) {
|
||||
let layout = |scope: SubresourceScope| match scope.subresource {
|
||||
SubresourceScopeInner::Texture { layout, .. } => Some(layout),
|
||||
SubresourceScopeInner::Buffer(_) => None,
|
||||
};
|
||||
(layout(previous), layout(current))
|
||||
}
|
||||
|
||||
fn layouts_from_state(
|
||||
initial: crate::resources::ResourceState,
|
||||
current: SubresourceScope,
|
||||
) -> (Option<vk::ImageLayout>, Option<vk::ImageLayout>) {
|
||||
let current_layout = match current.subresource {
|
||||
SubresourceScopeInner::Texture { layout, .. } => Some(layout),
|
||||
SubresourceScopeInner::Buffer(_) => None,
|
||||
};
|
||||
(initial.layout, current_layout)
|
||||
}
|
||||
|
||||
fn bind_resources(
|
||||
resources: &BTreeMap<ResourceId, ResourceDesc>,
|
||||
accesses: &[Vec<crate::recorder::ResourceAccess>],
|
||||
) -> BTreeMap<ResourceId, ResourceLifetime> {
|
||||
let mut lifetimes = BTreeMap::new();
|
||||
for (command, command_accesses) in accesses.iter().enumerate() {
|
||||
for access in command_accesses {
|
||||
lifetimes
|
||||
.entry(access.resource)
|
||||
.and_modify(|lifetime: &mut ResourceLifetime| {
|
||||
lifetime.last_command = command.into()
|
||||
})
|
||||
.or_insert(ResourceLifetime {
|
||||
first_command: command.into(),
|
||||
last_command: command.into(),
|
||||
memory_offset: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Skeleton aliasing policy: resources with non-overlapping lifetimes may
|
||||
// share a heap offset. Actual size/alignment-aware Vulkan allocation is TODO.
|
||||
let mut slots: Vec<(u64, CommandIndex, u64)> = Vec::new(); // (offset, last use, capacity)
|
||||
let mut next_offset = 0;
|
||||
let mut ids: Vec<_> = lifetimes.keys().copied().collect();
|
||||
ids.sort_by_key(|id| lifetimes[id].first_command);
|
||||
for id in ids {
|
||||
let lifetime = lifetimes.get_mut(&id).expect("lifetime key exists");
|
||||
let size = match resources[&id] {
|
||||
ResourceDesc::CreatedBuffer(desc) => desc.size,
|
||||
ResourceDesc::CreatedTexture(desc) => u64::from(desc.extent.width)
|
||||
.saturating_mul(u64::from(desc.extent.height))
|
||||
.saturating_mul(u64::from(desc.extent.depth))
|
||||
.saturating_mul(4),
|
||||
ResourceDesc::ImportedBuffer { .. } | ResourceDesc::ImportedTexture { .. } => 0,
|
||||
};
|
||||
if resources[&id].is_created()
|
||||
&& let Some((offset, last_use, capacity)) = slots
|
||||
.iter_mut()
|
||||
.find(|(_, last, cap)| *last < lifetime.first_command && *cap >= size)
|
||||
{
|
||||
lifetime.memory_offset = *offset;
|
||||
*last_use = lifetime.last_command;
|
||||
let _ = capacity;
|
||||
} else {
|
||||
lifetime.memory_offset = next_offset;
|
||||
if resources[&id].is_created() {
|
||||
slots.push((next_offset, lifetime.last_command, size));
|
||||
next_offset = next_offset.saturating_add(size);
|
||||
}
|
||||
}
|
||||
}
|
||||
lifetimes
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{commands::CopyBuffer, resources::BufferRange};
|
||||
|
||||
#[test]
|
||||
fn copy_buffer_compiles_with_dependency_and_barrier() {
|
||||
let mut graph = RenderGraph::new();
|
||||
let src = graph.create_buffer(crate::resources::BufferDesc {
|
||||
size: 64,
|
||||
usage: vk::BufferUsageFlags::TRANSFER_SRC,
|
||||
});
|
||||
let dst = graph.create_buffer(crate::resources::BufferDesc {
|
||||
size: 64,
|
||||
usage: vk::BufferUsageFlags::TRANSFER_DST,
|
||||
});
|
||||
graph.push(CopyBuffer {
|
||||
src,
|
||||
src_range: BufferRange {
|
||||
offset: 0,
|
||||
size: 64,
|
||||
},
|
||||
dst,
|
||||
dst_range: BufferRange {
|
||||
offset: 0,
|
||||
size: 64,
|
||||
},
|
||||
});
|
||||
|
||||
let compiled = graph.compile().expect("graph compiles");
|
||||
assert_eq!(compiled.command_order(), &[CommandIndex(0)]);
|
||||
assert!(compiled.barriers().is_empty());
|
||||
assert_eq!(compiled.resource_lifetimes().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_then_read_creates_order_and_barrier() {
|
||||
let mut graph = RenderGraph::new();
|
||||
let a = graph.create_buffer(crate::resources::BufferDesc {
|
||||
size: 32,
|
||||
usage: vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST,
|
||||
});
|
||||
let b = graph.create_buffer(crate::resources::BufferDesc {
|
||||
size: 32,
|
||||
usage: vk::BufferUsageFlags::TRANSFER_DST,
|
||||
});
|
||||
let c = graph.create_buffer(crate::resources::BufferDesc {
|
||||
size: 32,
|
||||
usage: vk::BufferUsageFlags::TRANSFER_DST,
|
||||
});
|
||||
graph.push(CopyBuffer {
|
||||
src: a,
|
||||
src_range: BufferRange {
|
||||
offset: 0,
|
||||
size: 32,
|
||||
},
|
||||
dst: b,
|
||||
dst_range: BufferRange {
|
||||
offset: 0,
|
||||
size: 32,
|
||||
},
|
||||
});
|
||||
graph.push(CopyBuffer {
|
||||
src: b,
|
||||
src_range: BufferRange {
|
||||
offset: 0,
|
||||
size: 32,
|
||||
},
|
||||
dst: c,
|
||||
dst_range: BufferRange {
|
||||
offset: 0,
|
||||
size: 32,
|
||||
},
|
||||
});
|
||||
|
||||
let compiled = graph.compile().expect("graph compiles");
|
||||
assert_eq!(
|
||||
compiled.command_order(),
|
||||
&[CommandIndex(0), CommandIndex(1)]
|
||||
);
|
||||
assert_eq!(compiled.barriers().len(), 1);
|
||||
assert_eq!(compiled.barriers()[0].before_command, CommandIndex(1));
|
||||
assert_eq!(compiled.barriers()[0].resources[0].id, b.id());
|
||||
}
|
||||
}
|
||||
11
crates/renderer_rendergraph/src/lib.rs
Normal file
11
crates/renderer_rendergraph/src/lib.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
//! A small render-graph skeleton following the design in `rendergraph.org`.
|
||||
#![allow(dead_code)]
|
||||
|
||||
pub mod commands;
|
||||
pub mod graph;
|
||||
pub mod recorder;
|
||||
pub mod resources;
|
||||
|
||||
pub use graph::{CompiledGraph, GraphError, RenderGraph};
|
||||
pub use recorder::{CommandIndex, CommandList, CommandRecorder, SideEffectMap};
|
||||
pub use resources::{Buffer, ResourceId, Texture};
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
214
crates/renderer_rendergraph/src/recorder.rs
Normal file
214
crates/renderer_rendergraph/src/recorder.rs
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use ash::vk;
|
||||
|
||||
use crate::resources::{
|
||||
BufferRange, ResourceId, Subresource, SubresourceScope, SubresourceScopeInner,
|
||||
};
|
||||
|
||||
/// The position of a command in its command list.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct CommandIndex(pub usize);
|
||||
|
||||
impl CommandIndex {
|
||||
pub fn index(self) -> usize {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<usize> for CommandIndex {
|
||||
fn from(index: usize) -> Self {
|
||||
Self(index)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CommandIndex> for usize {
|
||||
fn from(index: CommandIndex) -> Self {
|
||||
index.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ResourceAccess {
|
||||
pub resource: ResourceId,
|
||||
pub scope: SubresourceScope,
|
||||
}
|
||||
|
||||
/// Stores every declared access together with the command that declared it.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct SideEffectMap {
|
||||
accesses: Vec<Vec<ResourceAccess>>,
|
||||
}
|
||||
|
||||
impl SideEffectMap {
|
||||
pub fn begin_command(&mut self) -> CommandIndex {
|
||||
let index = self.accesses.len();
|
||||
self.accesses.push(Vec::new());
|
||||
index.into()
|
||||
}
|
||||
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
command_index: CommandIndex,
|
||||
resource: ResourceId,
|
||||
scope: SubresourceScope,
|
||||
) {
|
||||
if self.accesses.len() <= command_index.0 {
|
||||
self.accesses.resize_with(command_index.0 + 1, Vec::new);
|
||||
}
|
||||
let accesses = &mut self.accesses[command_index.0];
|
||||
for existing in accesses.iter_mut() {
|
||||
if existing.resource == resource && existing.scope.try_merge(&scope) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
accesses.push(ResourceAccess { resource, scope });
|
||||
}
|
||||
|
||||
pub fn accesses(&self, command_index: CommandIndex) -> &[ResourceAccess] {
|
||||
self.accesses
|
||||
.get(command_index.0)
|
||||
.map_or(&[], Vec::as_slice)
|
||||
}
|
||||
|
||||
pub fn command_accesses(&self) -> &[Vec<ResourceAccess>] {
|
||||
&self.accesses
|
||||
}
|
||||
}
|
||||
|
||||
/// A safe, type-erased temporary command buffer. Each command is owned until
|
||||
/// compilation/recording, while its access declarations are stored separately.
|
||||
pub struct CommandList {
|
||||
pub(crate) commands: Vec<Box<dyn RecordedCommand>>,
|
||||
pub side_effects: SideEffectMap,
|
||||
}
|
||||
|
||||
pub trait RecordedCommand {
|
||||
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError>;
|
||||
}
|
||||
|
||||
impl CommandList {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
commands: Vec::new(),
|
||||
side_effects: SideEffectMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push<C: crate::commands::Command + 'static>(&mut self, command: C) -> CommandIndex {
|
||||
let index = self.side_effects.begin_command();
|
||||
command.side_effects(index, &mut self.side_effects);
|
||||
self.commands.push(Box::new(command));
|
||||
index
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.commands.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.commands.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CommandList {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum RecordError {
|
||||
MissingBuffer(ResourceId),
|
||||
MissingImage(ResourceId),
|
||||
}
|
||||
|
||||
/// Vulkan recording context. Handle maps are populated for imported resources;
|
||||
/// transient allocation is intentionally left to a later allocator integration.
|
||||
pub struct CommandRecorder<'a> {
|
||||
pub device: &'a ash::Device,
|
||||
pub command_buffer: vk::CommandBuffer,
|
||||
pub buffers: BTreeMap<ResourceId, vk::Buffer>,
|
||||
pub images: BTreeMap<ResourceId, vk::Image>,
|
||||
}
|
||||
|
||||
impl<'a> CommandRecorder<'a> {
|
||||
pub fn new(device: &'a ash::Device, command_buffer: vk::CommandBuffer) -> Self {
|
||||
Self {
|
||||
device,
|
||||
command_buffer,
|
||||
buffers: BTreeMap::new(),
|
||||
images: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer(&self, id: ResourceId) -> Result<vk::Buffer, RecordError> {
|
||||
self.buffers
|
||||
.get(&id)
|
||||
.copied()
|
||||
.ok_or(RecordError::MissingBuffer(id))
|
||||
}
|
||||
|
||||
pub fn image(&self, id: ResourceId) -> Result<vk::Image, RecordError> {
|
||||
self.images
|
||||
.get(&id)
|
||||
.copied()
|
||||
.ok_or(RecordError::MissingImage(id))
|
||||
}
|
||||
|
||||
pub fn pipeline_barrier(&self, barrier: &super::graph::PipelineBarrier) {
|
||||
let buffer_barriers: Vec<_> = barrier
|
||||
.resources
|
||||
.iter()
|
||||
.filter_map(|resource| match resource.range {
|
||||
Subresource::Buffer(BufferRange { offset, size }) => {
|
||||
self.buffers.get(&resource.id).map(|&buffer| {
|
||||
vk::BufferMemoryBarrier2::default()
|
||||
.src_stage_mask(resource.src_stage)
|
||||
.src_access_mask(resource.src_access)
|
||||
.dst_stage_mask(resource.dst_stage)
|
||||
.dst_access_mask(resource.dst_access)
|
||||
.buffer(buffer)
|
||||
.offset(offset)
|
||||
.size(size)
|
||||
})
|
||||
}
|
||||
Subresource::Texture(_) => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let image_barriers: Vec<_> = barrier
|
||||
.resources
|
||||
.iter()
|
||||
.filter_map(|resource| match resource.range {
|
||||
Subresource::Texture(range) => self.images.get(&resource.id).map(|&image| {
|
||||
vk::ImageMemoryBarrier2::default()
|
||||
.src_stage_mask(resource.src_stage)
|
||||
.src_access_mask(resource.src_access)
|
||||
.dst_stage_mask(resource.dst_stage)
|
||||
.dst_access_mask(resource.dst_access)
|
||||
.old_layout(resource.old_layout.unwrap_or(vk::ImageLayout::UNDEFINED))
|
||||
.new_layout(resource.new_layout.unwrap_or(vk::ImageLayout::GENERAL))
|
||||
.image(image)
|
||||
.subresource_range(range)
|
||||
}),
|
||||
Subresource::Buffer(_) => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let dependency = vk::DependencyInfo::default()
|
||||
.buffer_memory_barriers(&buffer_barriers)
|
||||
.image_memory_barriers(&image_barriers);
|
||||
unsafe {
|
||||
self.device
|
||||
.cmd_pipeline_barrier2(self.command_buffer, &dependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn scope_range(scope: SubresourceScope) -> Subresource {
|
||||
match scope.subresource {
|
||||
SubresourceScopeInner::Buffer(range) => Subresource::Buffer(range),
|
||||
SubresourceScopeInner::Texture { range, .. } => Subresource::Texture(range),
|
||||
}
|
||||
}
|
||||
248
crates/renderer_rendergraph/src/resources.rs
Normal file
248
crates/renderer_rendergraph/src/resources.rs
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
use ash::vk;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[repr(transparent)]
|
||||
pub struct ResourceId(pub u64);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Buffer(pub ResourceId);
|
||||
|
||||
impl Buffer {
|
||||
pub fn id(self) -> ResourceId {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Texture(pub ResourceId);
|
||||
|
||||
impl Texture {
|
||||
pub fn id(self) -> ResourceId {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct BufferRange {
|
||||
pub offset: u64,
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
impl BufferRange {
|
||||
pub const fn full() -> Self {
|
||||
Self {
|
||||
offset: 0,
|
||||
size: vk::WHOLE_SIZE,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end(self) -> u64 {
|
||||
if self.size == vk::WHOLE_SIZE {
|
||||
u64::MAX
|
||||
} else {
|
||||
self.offset.saturating_add(self.size)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn overlaps(self, other: Self) -> bool {
|
||||
self.offset < other.end() && other.offset < self.end()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Subresource {
|
||||
Buffer(BufferRange),
|
||||
Texture(vk::ImageSubresourceRange),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum SubresourceScopeInner {
|
||||
Buffer(BufferRange),
|
||||
Texture {
|
||||
range: vk::ImageSubresourceRange,
|
||||
layout: vk::ImageLayout,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct SubresourceScope {
|
||||
pub access: vk::AccessFlags2,
|
||||
pub stage: vk::PipelineStageFlags2,
|
||||
pub subresource: SubresourceScopeInner,
|
||||
}
|
||||
|
||||
impl SubresourceScope {
|
||||
/// Fold identical ranges with compatible access polarity into one scope.
|
||||
pub fn try_merge(&mut self, other: &Self) -> bool {
|
||||
if access_flag_is_read(self.access) != access_flag_is_read(other.access)
|
||||
|| access_flag_is_write(self.access) != access_flag_is_write(other.access)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let same_range = match (self.subresource, other.subresource) {
|
||||
(SubresourceScopeInner::Buffer(a), SubresourceScopeInner::Buffer(b)) => a == b,
|
||||
(
|
||||
SubresourceScopeInner::Texture {
|
||||
range: a,
|
||||
layout: layout_a,
|
||||
},
|
||||
SubresourceScopeInner::Texture {
|
||||
range: b,
|
||||
layout: layout_b,
|
||||
},
|
||||
) => texture_ranges_equal(a, b) && layout_a == layout_b,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if same_range {
|
||||
self.stage |= other.stage;
|
||||
self.access |= other.access;
|
||||
}
|
||||
same_range
|
||||
}
|
||||
|
||||
/// Whether accesses overlap and require ordering (a write or layout change).
|
||||
pub fn conflicts(&self, other: &Self) -> bool {
|
||||
let overlaps = match (self.subresource, other.subresource) {
|
||||
(SubresourceScopeInner::Buffer(a), SubresourceScopeInner::Buffer(b)) => a.overlaps(b),
|
||||
(
|
||||
SubresourceScopeInner::Texture { range: a, .. },
|
||||
SubresourceScopeInner::Texture { range: b, .. },
|
||||
) => texture_ranges_overlap(a, b),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if !overlaps {
|
||||
return false;
|
||||
}
|
||||
|
||||
match (self.subresource, other.subresource) {
|
||||
(
|
||||
SubresourceScopeInner::Texture { layout: a, .. },
|
||||
SubresourceScopeInner::Texture { layout: b, .. },
|
||||
) if a != b => true,
|
||||
_ => {
|
||||
!(access_flag_is_read(self.access)
|
||||
&& !access_flag_is_write(self.access)
|
||||
&& access_flag_is_read(other.access)
|
||||
&& !access_flag_is_write(other.access))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn texture_ranges_equal(a: vk::ImageSubresourceRange, b: vk::ImageSubresourceRange) -> bool {
|
||||
a.aspect_mask == b.aspect_mask
|
||||
&& a.base_mip_level == b.base_mip_level
|
||||
&& a.level_count == b.level_count
|
||||
&& a.base_array_layer == b.base_array_layer
|
||||
&& a.layer_count == b.layer_count
|
||||
}
|
||||
|
||||
fn range_end(base: u32, count: u32) -> u64 {
|
||||
if count == vk::REMAINING_MIP_LEVELS || count == vk::REMAINING_ARRAY_LAYERS {
|
||||
u64::MAX
|
||||
} else {
|
||||
u64::from(base) + u64::from(count)
|
||||
}
|
||||
}
|
||||
|
||||
fn texture_ranges_overlap(a: vk::ImageSubresourceRange, b: vk::ImageSubresourceRange) -> bool {
|
||||
a.aspect_mask.intersects(b.aspect_mask)
|
||||
&& u64::from(a.base_mip_level) < range_end(b.base_mip_level, b.level_count)
|
||||
&& u64::from(b.base_mip_level) < range_end(a.base_mip_level, a.level_count)
|
||||
&& u64::from(a.base_array_layer) < range_end(b.base_array_layer, b.layer_count)
|
||||
&& u64::from(b.base_array_layer) < range_end(a.base_array_layer, a.layer_count)
|
||||
}
|
||||
|
||||
/// True when an access mask contains a Vulkan read access.
|
||||
pub fn access_flag_is_read(access: vk::AccessFlags2) -> bool {
|
||||
access.intersects(
|
||||
vk::AccessFlags2::INDIRECT_COMMAND_READ
|
||||
| vk::AccessFlags2::INDEX_READ
|
||||
| vk::AccessFlags2::VERTEX_ATTRIBUTE_READ
|
||||
| vk::AccessFlags2::UNIFORM_READ
|
||||
| vk::AccessFlags2::INPUT_ATTACHMENT_READ
|
||||
| vk::AccessFlags2::SHADER_READ
|
||||
| vk::AccessFlags2::COLOR_ATTACHMENT_READ
|
||||
| vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_READ
|
||||
| vk::AccessFlags2::TRANSFER_READ
|
||||
| vk::AccessFlags2::HOST_READ
|
||||
| vk::AccessFlags2::MEMORY_READ
|
||||
| vk::AccessFlags2::SHADER_SAMPLED_READ
|
||||
| vk::AccessFlags2::SHADER_STORAGE_READ
|
||||
| vk::AccessFlags2::ACCELERATION_STRUCTURE_READ_KHR,
|
||||
)
|
||||
}
|
||||
|
||||
/// True when an access mask contains a Vulkan write access.
|
||||
pub fn access_flag_is_write(access: vk::AccessFlags2) -> bool {
|
||||
access.intersects(
|
||||
vk::AccessFlags2::SHADER_WRITE
|
||||
| vk::AccessFlags2::COLOR_ATTACHMENT_WRITE
|
||||
| vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_WRITE
|
||||
| vk::AccessFlags2::TRANSFER_WRITE
|
||||
| vk::AccessFlags2::HOST_WRITE
|
||||
| vk::AccessFlags2::MEMORY_WRITE
|
||||
| vk::AccessFlags2::SHADER_STORAGE_WRITE
|
||||
| vk::AccessFlags2::ACCELERATION_STRUCTURE_WRITE_KHR,
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BufferDesc {
|
||||
pub size: u64,
|
||||
pub usage: vk::BufferUsageFlags,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct TextureDesc {
|
||||
pub format: vk::Format,
|
||||
pub extent: vk::Extent3D,
|
||||
pub usage: vk::ImageUsageFlags,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ResourceState {
|
||||
pub stage: vk::PipelineStageFlags2,
|
||||
pub access: vk::AccessFlags2,
|
||||
pub layout: Option<vk::ImageLayout>,
|
||||
}
|
||||
|
||||
impl Default for ResourceState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
stage: vk::PipelineStageFlags2::NONE,
|
||||
access: vk::AccessFlags2::NONE,
|
||||
layout: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ResourceDesc {
|
||||
CreatedBuffer(BufferDesc),
|
||||
CreatedTexture(TextureDesc),
|
||||
ImportedBuffer {
|
||||
handle: vk::Buffer,
|
||||
initial: ResourceState,
|
||||
},
|
||||
ImportedTexture {
|
||||
handle: vk::Image,
|
||||
initial: ResourceState,
|
||||
},
|
||||
}
|
||||
|
||||
impl ResourceDesc {
|
||||
pub fn initial_state(self) -> ResourceState {
|
||||
match self {
|
||||
Self::ImportedBuffer { initial, .. } | Self::ImportedTexture { initial, .. } => initial,
|
||||
Self::CreatedBuffer(_) | Self::CreatedTexture(_) => ResourceState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_created(self) -> bool {
|
||||
matches!(self, Self::CreatedBuffer(_) | Self::CreatedTexture(_))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue