use renderer_resources

This commit is contained in:
janis 2026-10-24 23:05:51 +02:00
parent e7a6536df5
commit d367b4d05b
Signed by: janis
SSH key fingerprint: SHA256:bB1qbbqmDXZNT0KKD5c2Dfjg53JGhj7B3CFcLIzSqq8
4 changed files with 174 additions and 55 deletions

View file

@ -5,3 +5,5 @@ edition = "2024"
[dependencies] [dependencies]
ash.workspace = true ash.workspace = true
renderer_resources = { path = "../renderer_resources" }
ash-formats = { git = "https://github.com/janis-bhm/ash-formats", branch = "main" }

View file

@ -1,6 +1,13 @@
use std::collections::{BTreeMap, BTreeSet}; use std::{
collections::{BTreeMap, BTreeSet, HashMap},
sync::Arc,
};
use ash::vk; use ash::vk;
use ash_formats::FormatExt;
use renderer_resources::{
Device, buffers::Buffer as BufferResource, images::Image as ImageResource,
};
use crate::{ use crate::{
recorder::{CommandIndex, CommandList, CommandRecorder, RecordError, scope_range}, recorder::{CommandIndex, CommandList, CommandRecorder, RecordError, scope_range},
@ -65,28 +72,34 @@ impl RenderGraph {
id id
} }
pub fn create_buffer(&mut self, desc: crate::resources::BufferDesc) -> Buffer { pub fn create_buffer(&mut self, desc: renderer_resources::buffers::BufferDesc) -> Buffer {
Buffer(self.add_resource(ResourceDesc::CreatedBuffer(desc))) Buffer(self.add_resource(ResourceDesc::CreatedBuffer(desc)))
} }
pub fn create_texture(&mut self, desc: crate::resources::TextureDesc) -> Texture { pub fn create_texture(&mut self, desc: renderer_resources::images::ImageDesc) -> Texture {
Texture(self.add_resource(ResourceDesc::CreatedTexture(desc))) Texture(self.add_resource(ResourceDesc::CreatedTexture(desc)))
} }
pub fn import_buffer( pub fn import_buffer(
&mut self, &mut self,
handle: vk::Buffer, resource: impl Into<Arc<BufferResource>>,
initial: crate::resources::ResourceState, initial: crate::resources::ResourceState,
) -> Buffer { ) -> Buffer {
Buffer(self.add_resource(ResourceDesc::ImportedBuffer { handle, initial })) Buffer(self.add_resource(ResourceDesc::ImportedBuffer {
resource: resource.into(),
initial,
}))
} }
pub fn import_texture( pub fn import_texture(
&mut self, &mut self,
handle: vk::Image, resource: impl Into<Arc<ImageResource>>,
initial: crate::resources::ResourceState, initial: crate::resources::ResourceState,
) -> Texture { ) -> Texture {
Texture(self.add_resource(ResourceDesc::ImportedTexture { handle, initial })) Texture(self.add_resource(ResourceDesc::ImportedTexture {
resource: resource.into(),
initial,
}))
} }
pub fn push<C: crate::commands::Command + 'static>(&mut self, command: C) -> CommandIndex { pub fn push<C: crate::commands::Command + 'static>(&mut self, command: C) -> CommandIndex {
@ -144,6 +157,7 @@ impl RenderGraph {
Ok(CompiledGraph { Ok(CompiledGraph {
resources: self.resources, resources: self.resources,
resolved: HashMap::new(),
commands: self.commands, commands: self.commands,
order, order,
barriers, barriers,
@ -160,10 +174,16 @@ impl Default for RenderGraph {
pub struct CompiledGraph { pub struct CompiledGraph {
resources: BTreeMap<ResourceId, ResourceDesc>, resources: BTreeMap<ResourceId, ResourceDesc>,
resolved: HashMap<ResourceId, ResolvedResource>,
commands: CommandList, commands: CommandList,
order: Vec<CommandIndex>, order: Vec<CommandIndex>,
barriers: Vec<PipelineBarrier>, barriers: Vec<PipelineBarrier>,
lifetimes: BTreeMap<ResourceId, ResourceLifetime>, lifetimes: HashMap<ResourceId, ResourceLifetime>,
}
enum ResolvedResource {
Buffer(Arc<BufferResource>),
Image(Arc<ImageResource>),
} }
impl CompiledGraph { impl CompiledGraph {
@ -175,27 +195,64 @@ impl CompiledGraph {
&self.barriers &self.barriers
} }
pub fn resource_lifetimes(&self) -> &BTreeMap<ResourceId, ResourceLifetime> { pub fn resource_lifetimes(&self) -> &HashMap<ResourceId, ResourceLifetime> {
&self.lifetimes &self.lifetimes
} }
/// Create the graph's transient resources. Compilation remains independent
/// of a Vulkan device, while command recording requires this resolution step
/// for any created resources used by commands.
pub fn resolve(&mut self, device: &Device) -> renderer_resources::Result<()> {
for (&id, desc) in &self.resources {
if self.resolved.contains_key(&id) {
continue;
}
let resource = match desc {
ResourceDesc::CreatedBuffer(desc) => ResolvedResource::Buffer(Arc::new(
BufferResource::new(device.clone(), desc.clone())?,
)),
ResourceDesc::CreatedTexture(desc) => ResolvedResource::Image(Arc::new(
ImageResource::new(device.clone(), desc.clone())?,
)),
ResourceDesc::ImportedBuffer { .. } | ResourceDesc::ImportedTexture { .. } => {
continue;
}
};
self.resolved.insert(id, resource);
}
Ok(())
}
/// Record the compiled order into a real Vulkan command buffer. Imported /// Record the compiled order into a real Vulkan command buffer. Imported
/// handles are automatically bound. Created resources require a future /// handles are automatically bound. Created resources must first be
/// allocator integration and currently return a missing-handle error when /// resolved with a device.
/// used by a command.
pub fn record_commands( pub fn record_commands(
&mut self, &mut self,
recorder: &mut CommandRecorder<'_>, recorder: &mut CommandRecorder<'_>,
) -> Result<(), RecordError> { ) -> Result<(), RecordError> {
for (&id, &desc) in &self.resources { for (&id, desc) in &self.resources {
if !self.lifetimes.contains_key(&id) {
continue;
}
match desc { match desc {
ResourceDesc::ImportedBuffer { handle, .. } => { ResourceDesc::ImportedBuffer { resource, .. } => {
recorder.buffers.insert(id, handle); recorder.buffers.insert(id, resource.raw());
} }
ResourceDesc::ImportedTexture { handle, .. } => { ResourceDesc::ImportedTexture { resource, .. } => {
recorder.images.insert(id, handle); recorder.images.insert(id, resource.raw());
} }
ResourceDesc::CreatedBuffer(_) | ResourceDesc::CreatedTexture(_) => {} ResourceDesc::CreatedBuffer(_) => match self.resolved.get(&id) {
Some(ResolvedResource::Buffer(resource)) => {
recorder.buffers.insert(id, resource.raw());
}
_ => return Err(RecordError::UnresolvedCreatedBuffer(id)),
},
ResourceDesc::CreatedTexture(_) => match self.resolved.get(&id) {
Some(ResolvedResource::Image(resource)) => {
recorder.images.insert(id, resource.raw());
}
_ => return Err(RecordError::UnresolvedCreatedImage(id)),
},
} }
} }
@ -245,7 +302,7 @@ fn build_barriers(
continue; continue;
} }
let desc = resource_descs[&current.resource]; let desc = &resource_descs[&current.resource];
let initial = desc.initial_state(); let initial = desc.initial_state();
let (old_layout, new_layout) = layouts_from_state(initial, current.scope); let (old_layout, new_layout) = layouts_from_state(initial, current.scope);
let layout_transition = old_layout != new_layout && new_layout.is_some(); let layout_transition = old_layout != new_layout && new_layout.is_some();
@ -303,8 +360,8 @@ fn layouts_from_state(
fn bind_resources( fn bind_resources(
resources: &BTreeMap<ResourceId, ResourceDesc>, resources: &BTreeMap<ResourceId, ResourceDesc>,
accesses: &[Vec<crate::recorder::ResourceAccess>], accesses: &[Vec<crate::recorder::ResourceAccess>],
) -> BTreeMap<ResourceId, ResourceLifetime> { ) -> HashMap<ResourceId, ResourceLifetime> {
let mut lifetimes = BTreeMap::new(); let mut lifetimes = HashMap::new();
for (command, command_accesses) in accesses.iter().enumerate() { for (command, command_accesses) in accesses.iter().enumerate() {
for access in command_accesses { for access in command_accesses {
lifetimes lifetimes
@ -328,12 +385,19 @@ fn bind_resources(
ids.sort_by_key(|id| lifetimes[id].first_command); ids.sort_by_key(|id| lifetimes[id].first_command);
for id in ids { for id in ids {
let lifetime = lifetimes.get_mut(&id).expect("lifetime key exists"); let lifetime = lifetimes.get_mut(&id).expect("lifetime key exists");
let size = match resources[&id] { let size = match &resources[&id] {
ResourceDesc::CreatedBuffer(desc) => desc.size, ResourceDesc::CreatedBuffer(desc) => desc.size,
ResourceDesc::CreatedTexture(desc) => u64::from(desc.extent.width) ResourceDesc::CreatedTexture(desc) => {
let texels_per_block = u64::from(desc.format.texels_per_block()).max(1);
let block_size = u64::from(desc.format.block_size());
u64::from(desc.extent.width)
.saturating_mul(u64::from(desc.extent.height)) .saturating_mul(u64::from(desc.extent.height))
.saturating_mul(u64::from(desc.extent.depth)) .saturating_mul(u64::from(desc.extent.depth))
.saturating_mul(4), .div_ceil(texels_per_block)
.saturating_mul(block_size)
.saturating_mul(u64::from(desc.array_layers))
.saturating_mul(u64::from(desc.samples.as_raw().max(1)))
}
ResourceDesc::ImportedBuffer { .. } | ResourceDesc::ImportedTexture { .. } => 0, ResourceDesc::ImportedBuffer { .. } | ResourceDesc::ImportedTexture { .. } => 0,
}; };
if resources[&id].is_created() if resources[&id].is_created()
@ -358,18 +422,23 @@ fn bind_resources(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::{commands::CopyBuffer, resources::BufferRange}; use crate::{
commands::{ClearTexture, CopyBuffer},
resources::BufferRange,
};
#[test] #[test]
fn copy_buffer_compiles_with_dependency_and_barrier() { fn copy_buffer_compiles_with_dependency_and_barrier() {
let mut graph = RenderGraph::new(); let mut graph = RenderGraph::new();
let src = graph.create_buffer(crate::resources::BufferDesc { let src = graph.create_buffer(renderer_resources::buffers::BufferDesc {
size: 64, size: 64,
usage: vk::BufferUsageFlags::TRANSFER_SRC, usage: vk::BufferUsageFlags::TRANSFER_SRC,
..Default::default()
}); });
let dst = graph.create_buffer(crate::resources::BufferDesc { let dst = graph.create_buffer(renderer_resources::buffers::BufferDesc {
size: 64, size: 64,
usage: vk::BufferUsageFlags::TRANSFER_DST, usage: vk::BufferUsageFlags::TRANSFER_DST,
..Default::default()
}); });
graph.push(CopyBuffer { graph.push(CopyBuffer {
src, src,
@ -393,17 +462,20 @@ mod tests {
#[test] #[test]
fn write_then_read_creates_order_and_barrier() { fn write_then_read_creates_order_and_barrier() {
let mut graph = RenderGraph::new(); let mut graph = RenderGraph::new();
let a = graph.create_buffer(crate::resources::BufferDesc { let a = graph.create_buffer(renderer_resources::buffers::BufferDesc {
size: 32, size: 32,
usage: vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST, usage: vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST,
..Default::default()
}); });
let b = graph.create_buffer(crate::resources::BufferDesc { let b = graph.create_buffer(renderer_resources::buffers::BufferDesc {
size: 32, size: 32,
usage: vk::BufferUsageFlags::TRANSFER_DST, usage: vk::BufferUsageFlags::TRANSFER_DST,
..Default::default()
}); });
let c = graph.create_buffer(crate::resources::BufferDesc { let c = graph.create_buffer(renderer_resources::buffers::BufferDesc {
size: 32, size: 32,
usage: vk::BufferUsageFlags::TRANSFER_DST, usage: vk::BufferUsageFlags::TRANSFER_DST,
..Default::default()
}); });
graph.push(CopyBuffer { graph.push(CopyBuffer {
src: a, src: a,
@ -439,4 +511,56 @@ mod tests {
assert_eq!(compiled.barriers()[0].before_command, CommandIndex(1)); assert_eq!(compiled.barriers()[0].before_command, CommandIndex(1));
assert_eq!(compiled.barriers()[0].resources[0].id, b.id()); assert_eq!(compiled.barriers()[0].resources[0].id, b.id());
} }
#[test]
fn texture_aliasing_size_uses_format_block_size() {
let mut graph = RenderGraph::new();
let texture = graph.create_texture(renderer_resources::images::ImageDesc {
format: vk::Format::R8G8B8A8_UNORM,
extent: vk::Extent3D {
width: 4,
height: 4,
depth: 1,
},
usage: vk::ImageUsageFlags::TRANSFER_DST,
..Default::default()
});
let buffer = graph.create_buffer(renderer_resources::buffers::BufferDesc {
size: 8,
usage: vk::BufferUsageFlags::TRANSFER_DST,
..Default::default()
});
graph.push(ClearTexture {
texture,
range: vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
},
layout: vk::ImageLayout::TRANSFER_DST_OPTIMAL,
color: [0.0; 4],
});
// Keep the buffer live in the same command, so its slot follows the image.
graph.commands.side_effects.insert(
CommandIndex(0),
buffer.id(),
SubresourceScope {
stage: vk::PipelineStageFlags2::CLEAR,
access: vk::AccessFlags2::TRANSFER_WRITE,
subresource: SubresourceScopeInner::Buffer(BufferRange { offset: 0, size: 8 }),
},
);
let compiled = graph.compile().expect("graph compiles");
assert_eq!(
compiled.resource_lifetimes()[&texture.id()].memory_offset,
0
);
assert_eq!(
compiled.resource_lifetimes()[&buffer.id()].memory_offset,
64
);
}
} }

View file

@ -121,10 +121,11 @@ impl Default for CommandList {
pub enum RecordError { pub enum RecordError {
MissingBuffer(ResourceId), MissingBuffer(ResourceId),
MissingImage(ResourceId), MissingImage(ResourceId),
UnresolvedCreatedBuffer(ResourceId),
UnresolvedCreatedImage(ResourceId),
} }
/// Vulkan recording context. Handle maps are populated for imported resources; /// Vulkan recording context. Handle maps are populated by the compiled graph.
/// transient allocation is intentionally left to a later allocator integration.
pub struct CommandRecorder<'a> { pub struct CommandRecorder<'a> {
pub device: &'a ash::Device, pub device: &'a ash::Device,
pub command_buffer: vk::CommandBuffer, pub command_buffer: vk::CommandBuffer,

View file

@ -1,4 +1,7 @@
use std::sync::Arc;
use ash::vk; use ash::vk;
use renderer_resources::{buffers::Buffer as BufferResource, images::Image as ImageResource};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)] #[repr(transparent)]
@ -190,19 +193,6 @@ pub fn access_flag_is_write(access: vk::AccessFlags2) -> bool {
) )
} }
#[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)] #[derive(Debug, Clone, Copy)]
pub struct ResourceState { pub struct ResourceState {
pub stage: vk::PipelineStageFlags2, pub stage: vk::PipelineStageFlags2,
@ -220,29 +210,31 @@ impl Default for ResourceState {
} }
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone)]
pub enum ResourceDesc { pub enum ResourceDesc {
CreatedBuffer(BufferDesc), CreatedBuffer(renderer_resources::buffers::BufferDesc),
CreatedTexture(TextureDesc), CreatedTexture(renderer_resources::images::ImageDesc),
ImportedBuffer { ImportedBuffer {
handle: vk::Buffer, resource: Arc<BufferResource>,
initial: ResourceState, initial: ResourceState,
}, },
ImportedTexture { ImportedTexture {
handle: vk::Image, resource: Arc<ImageResource>,
initial: ResourceState, initial: ResourceState,
}, },
} }
impl ResourceDesc { impl ResourceDesc {
pub fn initial_state(self) -> ResourceState { pub fn initial_state(&self) -> ResourceState {
match self { match self {
Self::ImportedBuffer { initial, .. } | Self::ImportedTexture { initial, .. } => initial, Self::ImportedBuffer { initial, .. } | Self::ImportedTexture { initial, .. } => {
*initial
}
Self::CreatedBuffer(_) | Self::CreatedTexture(_) => ResourceState::default(), Self::CreatedBuffer(_) | Self::CreatedTexture(_) => ResourceState::default(),
} }
} }
pub fn is_created(self) -> bool { pub fn is_created(&self) -> bool {
matches!(self, Self::CreatedBuffer(_) | Self::CreatedTexture(_)) matches!(self, Self::CreatedBuffer(_) | Self::CreatedTexture(_))
} }
} }