rendergraph: dynamic-rendering render passes, single-pass compiler, perf
- Add BeginRendering/EndRendering commands with imported/created attachment
views, view plumbing, and aspect-gated depth/stencil slots
- Derive and validate render-pass spans at compile time; contract spans into
single DAG nodes before hazard analysis; hoist cross-boundary barriers to
pass begins with in-pass policy and created-view resolution
- Replace the four-pass ordering pipeline with a single command walk
(build_command_orderings): span derivation, validation, lifetime-only
injection, dependency edges, and Kahn batching in one pass; batches are
ranges into execution_order and RenderPassSpan keeps only begin/end
- Perf: node-collapsed frontier analysis, dense command/resource indexing
(no SipHash on hot paths), inline barrier storage, exact preallocation,
lazy Kahn ready heap, scoped barrier fast paths
- Scope buffer and image alias barriers to the incoming resource per the
Vulkan resource-memory-aliasing rules (availability unscoped, visibility
scoped to the new owner)
- Add criterion compile benchmarks (two_pass/{no_pass,pass}/per_{20,50,100})
This commit is contained in:
parent
4bf2d874b0
commit
afb322fa93
|
|
@ -17,6 +17,7 @@ members = [
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy_vulkan_render = { path = "crates/bevy_vulkan_render" }
|
bevy_vulkan_render = { path = "crates/bevy_vulkan_render" }
|
||||||
|
bitflags.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
bevy = { workspace = true }
|
bevy = { workspace = true }
|
||||||
|
|
@ -28,6 +29,7 @@ debug = true
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
anyhow = "1.0.89"
|
anyhow = "1.0.89"
|
||||||
|
criterion = "0.7"
|
||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
derive_more = { version = "1.0.0", features = ["deref", "debug", "deref_mut"] }
|
derive_more = { version = "1.0.0", features = ["deref", "debug", "deref_mut"] }
|
||||||
|
|
||||||
|
|
@ -46,7 +48,7 @@ rectangle-pack = "0.4.2"
|
||||||
# vk-sync = "0.1.6"
|
# vk-sync = "0.1.6"
|
||||||
|
|
||||||
arrayvec = "0.7.6"
|
arrayvec = "0.7.6"
|
||||||
tinyvec = "1.8"
|
tinyvec = { version = "1.8", features = ["alloc"] }
|
||||||
indexmap = "2"
|
indexmap = "2"
|
||||||
petgraph = "0.7"
|
petgraph = "0.7"
|
||||||
fixedbitset = "0.5"
|
fixedbitset = "0.5"
|
||||||
|
|
|
||||||
|
|
@ -571,7 +571,7 @@ pub fn egui_pass(
|
||||||
.values()
|
.values()
|
||||||
.map(|entry| {
|
.map(|entry| {
|
||||||
let texture = texture_handler.get_texture(entry.id).unwrap();
|
let texture = texture_handler.get_texture(entry.id).unwrap();
|
||||||
let view = Arc::new(ManuallyDrop::into_inner(
|
let view = Arc::new(
|
||||||
texture
|
texture
|
||||||
.create_view(
|
.create_view(
|
||||||
ImageViewDesc::color_2d()
|
ImageViewDesc::color_2d()
|
||||||
|
|
@ -579,7 +579,7 @@ pub fn egui_pass(
|
||||||
.with_layer_range(0..1),
|
.with_layer_range(0..1),
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
));
|
);
|
||||||
let sampler = samplers.get_sampler_owned(entry.as_sampler_desc()).unwrap();
|
let sampler = samplers.get_sampler_owned(entry.as_sampler_desc()).unwrap();
|
||||||
(view, sampler)
|
(view, sampler)
|
||||||
})
|
})
|
||||||
|
|
@ -770,7 +770,7 @@ pub fn egui_pass(
|
||||||
|
|
||||||
let color_attachment = &vk::RenderingAttachmentInfo::default()
|
let color_attachment = &vk::RenderingAttachmentInfo::default()
|
||||||
.image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
|
.image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
|
||||||
.image_view(target.create_view(ImageViewDesc::color_2d())?.raw())
|
.image_view(ManuallyDrop::new(target.create_view(ImageViewDesc::color_2d())?).raw())
|
||||||
.load_op(vk::AttachmentLoadOp::LOAD)
|
.load_op(vk::AttachmentLoadOp::LOAD)
|
||||||
.store_op(vk::AttachmentStoreOp::STORE);
|
.store_op(vk::AttachmentStoreOp::STORE);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::sync::Arc;
|
use std::{mem::ManuallyDrop, sync::Arc};
|
||||||
|
|
||||||
pub use crate::egui_pass::{egui_pass, egui_pre_pass};
|
pub use crate::egui_pass::{egui_pass, egui_pre_pass};
|
||||||
|
|
||||||
|
|
@ -329,7 +329,9 @@ impl Wireframe {
|
||||||
|
|
||||||
let color_attachment = &vk::RenderingAttachmentInfo::default()
|
let color_attachment = &vk::RenderingAttachmentInfo::default()
|
||||||
.image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
|
.image_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
|
||||||
.image_view(target.create_view(ImageViewDesc::color_2d())?.raw())
|
.image_view(
|
||||||
|
ManuallyDrop::new(target.create_view(ImageViewDesc::color_2d())?).raw(),
|
||||||
|
)
|
||||||
.load_op(vk::AttachmentLoadOp::LOAD)
|
.load_op(vk::AttachmentLoadOp::LOAD)
|
||||||
.store_op(vk::AttachmentStoreOp::STORE);
|
.store_op(vk::AttachmentStoreOp::STORE);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use std::{
|
use std::{
|
||||||
mem::ManuallyDrop,
|
|
||||||
num::NonZero,
|
num::NonZero,
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
sync::{
|
sync::{
|
||||||
|
|
@ -589,7 +588,7 @@ impl Swapchain {
|
||||||
SwapchainImage {
|
SwapchainImage {
|
||||||
index: idx as u32,
|
index: idx as u32,
|
||||||
swapchain: self,
|
swapchain: self,
|
||||||
view: ManuallyDrop::into_inner(view),
|
view,
|
||||||
acquire,
|
acquire,
|
||||||
release,
|
release,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,17 @@ edition = "2024"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ash.workspace = true
|
ash.workspace = true
|
||||||
fixedbitset.workspace = true
|
fixedbitset.workspace = true
|
||||||
|
tinyvec.workspace = true
|
||||||
renderer_device = { path = "../renderer_device" }
|
renderer_device = { path = "../renderer_device" }
|
||||||
renderer_resources = { path = "../renderer_resources" }
|
renderer_resources = { path = "../renderer_resources" }
|
||||||
ash-formats = { git = "https://github.com/janis-bhm/ash-formats", branch = "main" }
|
ash-formats = { git = "https://github.com/janis-bhm/ash-formats", branch = "main" }
|
||||||
|
bitflags.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
criterion = { workspace = true }
|
||||||
renderer_resources = { path = "../renderer_resources", features = ["testing"] }
|
renderer_resources = { path = "../renderer_resources", features = ["testing"] }
|
||||||
renderer_device = { path = "../renderer_device", features = ["testing"] }
|
renderer_device = { path = "../renderer_device", features = ["testing"] }
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "compile"
|
||||||
|
harness = false
|
||||||
|
|
|
||||||
141
crates/renderer_rendergraph/benches/compile.rs
Normal file
141
crates/renderer_rendergraph/benches/compile.rs
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
//! These graph shapes compare command-level batching with render-pass span
|
||||||
|
//! contraction and barrier hoisting. Each has two disjoint buffer access sets
|
||||||
|
//! joined by a shared-buffer dependency across the passes.
|
||||||
|
|
||||||
|
use std::{hint::black_box, time::Duration};
|
||||||
|
|
||||||
|
use ash::vk;
|
||||||
|
use criterion::{BatchSize, Criterion, criterion_group, criterion_main};
|
||||||
|
use renderer_rendergraph::{
|
||||||
|
Buffer, BufferRange, GraphBufferDesc, RenderGraph,
|
||||||
|
commands::{BeginRendering, Command, EndRendering},
|
||||||
|
recorder::{CommandIndex, CommandRecorder, RecordError, RecordedCommand, SideEffectMap},
|
||||||
|
resources::{SubresourceScope, SubresourceScopeInner},
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Use {
|
||||||
|
buffer: Buffer,
|
||||||
|
write: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Command for Use {
|
||||||
|
fn side_effects(&self, command_index: CommandIndex, map: &mut SideEffectMap) {
|
||||||
|
map.insert(
|
||||||
|
command_index,
|
||||||
|
self.buffer.id(),
|
||||||
|
SubresourceScope {
|
||||||
|
stage: vk::PipelineStageFlags2::COMPUTE_SHADER,
|
||||||
|
access: if self.write {
|
||||||
|
vk::AccessFlags2::SHADER_WRITE
|
||||||
|
} else {
|
||||||
|
vk::AccessFlags2::SHADER_READ
|
||||||
|
},
|
||||||
|
subresource: SubresourceScopeInner::Buffer(BufferRange::full()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RecordedCommand for Use {
|
||||||
|
fn allowed_in_render_pass(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply(&mut self, _recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(n_groups: usize, per: usize, pass: bool) -> RenderGraph {
|
||||||
|
let mut graph = RenderGraph::new();
|
||||||
|
let a: Vec<Buffer> = (0..n_groups)
|
||||||
|
.map(|_| {
|
||||||
|
graph.create_buffer(GraphBufferDesc {
|
||||||
|
size: 4096,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let b: Vec<Buffer> = (0..n_groups)
|
||||||
|
.map(|_| {
|
||||||
|
graph.create_buffer(GraphBufferDesc {
|
||||||
|
size: 4096,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let shared = graph.create_buffer(GraphBufferDesc {
|
||||||
|
size: 4096,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
if pass {
|
||||||
|
graph.push(BeginRendering {
|
||||||
|
color: vec![],
|
||||||
|
depth: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for _ in 0..per {
|
||||||
|
for &buffer in &a {
|
||||||
|
graph.push(Use {
|
||||||
|
buffer,
|
||||||
|
write: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
graph.push(Use {
|
||||||
|
buffer: shared,
|
||||||
|
write: true,
|
||||||
|
});
|
||||||
|
if pass {
|
||||||
|
graph.push(EndRendering);
|
||||||
|
graph.push(BeginRendering {
|
||||||
|
color: vec![],
|
||||||
|
depth: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for _ in 0..per {
|
||||||
|
for &buffer in &b {
|
||||||
|
graph.push(Use {
|
||||||
|
buffer,
|
||||||
|
write: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
graph.push(Use {
|
||||||
|
buffer: shared,
|
||||||
|
write: false,
|
||||||
|
});
|
||||||
|
if pass {
|
||||||
|
graph.push(EndRendering);
|
||||||
|
}
|
||||||
|
|
||||||
|
graph
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compile_benchmarks(criterion: &mut Criterion) {
|
||||||
|
let mut group = criterion.benchmark_group("compile");
|
||||||
|
group
|
||||||
|
.measurement_time(Duration::from_secs(4))
|
||||||
|
.sample_size(20);
|
||||||
|
|
||||||
|
for (pass_name, pass) in [("no_pass", false), ("pass", true)] {
|
||||||
|
for per in [20, 50, 100] {
|
||||||
|
group.bench_function(format!("two_pass/{pass_name}/per_{per}"), |bencher| {
|
||||||
|
bencher.iter_batched(
|
||||||
|
|| build(50, per, pass),
|
||||||
|
|graph| {
|
||||||
|
let compiled = graph.compile().unwrap();
|
||||||
|
black_box(compiled.barriers().len())
|
||||||
|
},
|
||||||
|
BatchSize::LargeInput,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
group.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, compile_benchmarks);
|
||||||
|
criterion_main!(benches);
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
|
use std::{cell::RefCell, sync::Arc};
|
||||||
|
|
||||||
use ash::vk;
|
use ash::vk;
|
||||||
|
use renderer_resources::images::{ImageView, ImageViewDesc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
recorder::{CommandIndex, CommandRecorder, RecordError, RecordedCommand, SideEffectMap},
|
recorder::{CommandIndex, CommandRecorder, RecordError, RecordedCommand, SideEffectMap},
|
||||||
|
|
@ -9,6 +12,243 @@ use crate::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Classification used by the compiler to validate render-pass spans.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum CommandKind {
|
||||||
|
Begin,
|
||||||
|
End,
|
||||||
|
Graphics,
|
||||||
|
Transfer,
|
||||||
|
Compute,
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A view used by a render-pass attachment.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum AttachmentView {
|
||||||
|
/// A view the graph must create after resource resolution, described concretely.
|
||||||
|
Created(ImageViewDesc),
|
||||||
|
/// A caller-supplied view over an imported image.
|
||||||
|
Imported(Arc<ImageView>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A color attachment used by dynamic rendering.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ColorAttachment {
|
||||||
|
pub texture: Texture,
|
||||||
|
pub view: AttachmentView,
|
||||||
|
/// Layout used while rendering (must be a legal rendering layout; validated later).
|
||||||
|
pub layout: vk::ImageLayout,
|
||||||
|
pub load_op: vk::AttachmentLoadOp,
|
||||||
|
pub store_op: vk::AttachmentStoreOp,
|
||||||
|
pub clear_value: vk::ClearValue,
|
||||||
|
/// Concrete subresource range covered by the view. Created-view sentinels are preserved for
|
||||||
|
/// compile-time validation.
|
||||||
|
pub range: vk::ImageSubresourceRange,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for ColorAttachment {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
formatter
|
||||||
|
.debug_struct("ColorAttachment")
|
||||||
|
.field("texture", &self.texture)
|
||||||
|
.field("view", &self.view)
|
||||||
|
.field("layout", &self.layout)
|
||||||
|
.field("load_op", &self.load_op)
|
||||||
|
.field("store_op", &self.store_op)
|
||||||
|
.field("clear_value", &"<Vulkan clear value>")
|
||||||
|
.field("range", &self.range)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorAttachment {
|
||||||
|
/// Builds an attachment around a caller-supplied view of an imported image.
|
||||||
|
pub fn imported(
|
||||||
|
texture: Texture,
|
||||||
|
view: Arc<ImageView>,
|
||||||
|
layout: vk::ImageLayout,
|
||||||
|
load_op: vk::AttachmentLoadOp,
|
||||||
|
store_op: vk::AttachmentStoreOp,
|
||||||
|
clear_value: vk::ClearValue,
|
||||||
|
) -> Self {
|
||||||
|
let range = view.subresource_range();
|
||||||
|
Self {
|
||||||
|
texture,
|
||||||
|
view: AttachmentView::Imported(view),
|
||||||
|
layout,
|
||||||
|
load_op,
|
||||||
|
store_op,
|
||||||
|
clear_value,
|
||||||
|
range,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds an attachment whose view will be created after graph resolution.
|
||||||
|
pub fn created(
|
||||||
|
texture: Texture,
|
||||||
|
desc: ImageViewDesc,
|
||||||
|
layout: vk::ImageLayout,
|
||||||
|
load_op: vk::AttachmentLoadOp,
|
||||||
|
store_op: vk::AttachmentStoreOp,
|
||||||
|
clear_value: vk::ClearValue,
|
||||||
|
) -> Self {
|
||||||
|
let range = image_view_desc_range(&desc);
|
||||||
|
Self {
|
||||||
|
texture,
|
||||||
|
view: AttachmentView::Created(desc),
|
||||||
|
layout,
|
||||||
|
load_op,
|
||||||
|
store_op,
|
||||||
|
clear_value,
|
||||||
|
range,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A depth/stencil attachment used by dynamic rendering. The view's aspect mask
|
||||||
|
/// selects which depth/stencil attachment slots receive the view.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DepthStencilAttachment {
|
||||||
|
pub texture: Texture,
|
||||||
|
pub view: AttachmentView,
|
||||||
|
pub layout: vk::ImageLayout,
|
||||||
|
pub depth_load_op: vk::AttachmentLoadOp,
|
||||||
|
pub depth_store_op: vk::AttachmentStoreOp,
|
||||||
|
pub stencil_load_op: vk::AttachmentLoadOp,
|
||||||
|
pub stencil_store_op: vk::AttachmentStoreOp,
|
||||||
|
pub clear_depth: f32,
|
||||||
|
pub clear_stencil: u32,
|
||||||
|
pub range: vk::ImageSubresourceRange,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DepthStencilAttachment {
|
||||||
|
/// Builds an attachment around a caller-supplied view of an imported image.
|
||||||
|
pub fn imported(
|
||||||
|
texture: Texture,
|
||||||
|
view: Arc<ImageView>,
|
||||||
|
layout: vk::ImageLayout,
|
||||||
|
depth_load_op: vk::AttachmentLoadOp,
|
||||||
|
depth_store_op: vk::AttachmentStoreOp,
|
||||||
|
stencil_load_op: vk::AttachmentLoadOp,
|
||||||
|
stencil_store_op: vk::AttachmentStoreOp,
|
||||||
|
clear_depth: f32,
|
||||||
|
clear_stencil: u32,
|
||||||
|
) -> Self {
|
||||||
|
let range = view.subresource_range();
|
||||||
|
Self {
|
||||||
|
texture,
|
||||||
|
view: AttachmentView::Imported(view),
|
||||||
|
layout,
|
||||||
|
depth_load_op,
|
||||||
|
depth_store_op,
|
||||||
|
stencil_load_op,
|
||||||
|
stencil_store_op,
|
||||||
|
clear_depth,
|
||||||
|
clear_stencil,
|
||||||
|
range,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds an attachment whose view will be created after graph resolution.
|
||||||
|
pub fn created(
|
||||||
|
texture: Texture,
|
||||||
|
desc: ImageViewDesc,
|
||||||
|
layout: vk::ImageLayout,
|
||||||
|
depth_load_op: vk::AttachmentLoadOp,
|
||||||
|
depth_store_op: vk::AttachmentStoreOp,
|
||||||
|
stencil_load_op: vk::AttachmentLoadOp,
|
||||||
|
stencil_store_op: vk::AttachmentStoreOp,
|
||||||
|
clear_depth: f32,
|
||||||
|
clear_stencil: u32,
|
||||||
|
) -> Self {
|
||||||
|
let range = image_view_desc_range(&desc);
|
||||||
|
Self {
|
||||||
|
texture,
|
||||||
|
view: AttachmentView::Created(desc),
|
||||||
|
layout,
|
||||||
|
depth_load_op,
|
||||||
|
depth_store_op,
|
||||||
|
stencil_load_op,
|
||||||
|
stencil_store_op,
|
||||||
|
clear_depth,
|
||||||
|
clear_stencil,
|
||||||
|
range,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn image_view_desc_range(desc: &ImageViewDesc) -> vk::ImageSubresourceRange {
|
||||||
|
vk::ImageSubresourceRange::default()
|
||||||
|
.aspect_mask(desc.aspect)
|
||||||
|
.base_mip_level(desc.mip_range.start)
|
||||||
|
.level_count(if desc.mip_range.end == vk::REMAINING_MIP_LEVELS {
|
||||||
|
vk::REMAINING_MIP_LEVELS
|
||||||
|
} else {
|
||||||
|
desc.mip_range.end - desc.mip_range.start
|
||||||
|
})
|
||||||
|
.base_array_layer(desc.layer_range.start)
|
||||||
|
.layer_count(if desc.layer_range.end == vk::REMAINING_ARRAY_LAYERS {
|
||||||
|
vk::REMAINING_ARRAY_LAYERS
|
||||||
|
} else {
|
||||||
|
desc.layer_range.end - desc.layer_range.start
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A borrowed summary of a render-pass attachment configuration.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct RenderPassDesc<'a> {
|
||||||
|
pub color: &'a [ColorAttachment],
|
||||||
|
pub depth: Option<&'a DepthStencilAttachment>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Begins a dynamic-rendering instance.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BeginRendering {
|
||||||
|
pub color: Vec<ColorAttachment>,
|
||||||
|
pub depth: Option<DepthStencilAttachment>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ends a dynamic-rendering instance.
|
||||||
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
pub struct EndRendering;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct RenderingScratch {
|
||||||
|
color_attachments: Vec<vk::RenderingAttachmentInfo<'static>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static RENDERING_SCRATCH: RefCell<RenderingScratch> = RefCell::new(RenderingScratch::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_attachment_view(
|
||||||
|
texture: Texture,
|
||||||
|
view: &AttachmentView,
|
||||||
|
recorder: &CommandRecorder<'_>,
|
||||||
|
) -> Result<(vk::ImageView, vk::Extent2D), RecordError> {
|
||||||
|
recorder.image(texture.id())?;
|
||||||
|
let view = match view {
|
||||||
|
AttachmentView::Created(desc) => recorder.view(texture.id(), desc)?,
|
||||||
|
AttachmentView::Imported(view) => view.clone(),
|
||||||
|
};
|
||||||
|
let range = view.subresource_range();
|
||||||
|
let image_extent = view.image().extent_2d();
|
||||||
|
let mip = range.base_mip_level.min(u32::BITS - 1);
|
||||||
|
let extent = vk::Extent2D {
|
||||||
|
width: (image_extent.width >> mip).max(1),
|
||||||
|
height: (image_extent.height >> mip).max(1),
|
||||||
|
};
|
||||||
|
Ok((view.raw(), extent))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rendering_extent(current: Option<vk::Extent2D>, next: vk::Extent2D) -> vk::Extent2D {
|
||||||
|
current.map_or(next, |current| vk::Extent2D {
|
||||||
|
width: current.width.min(next.width),
|
||||||
|
height: current.height.min(next.height),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// A recorded operation declares its accesses before compilation and applies
|
/// A recorded operation declares its accesses before compilation and applies
|
||||||
/// itself to a Vulkan command buffer after resource binding.
|
/// itself to a Vulkan command buffer after resource binding.
|
||||||
pub trait Command: RecordedCommand {
|
pub trait Command: RecordedCommand {
|
||||||
|
|
@ -59,6 +299,10 @@ impl Command for CopyBuffer {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecordedCommand for CopyBuffer {
|
impl RecordedCommand for CopyBuffer {
|
||||||
|
fn kind(&self) -> CommandKind {
|
||||||
|
CommandKind::Transfer
|
||||||
|
}
|
||||||
|
|
||||||
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
|
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
|
||||||
let region = vk::BufferCopy2::default()
|
let region = vk::BufferCopy2::default()
|
||||||
.src_offset(self.src_range.offset)
|
.src_offset(self.src_range.offset)
|
||||||
|
|
@ -104,6 +348,10 @@ impl Command for ClearTexture {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecordedCommand for ClearTexture {
|
impl RecordedCommand for ClearTexture {
|
||||||
|
fn kind(&self) -> CommandKind {
|
||||||
|
CommandKind::Transfer
|
||||||
|
}
|
||||||
|
|
||||||
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
|
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
|
||||||
let value = vk::ClearColorValue {
|
let value = vk::ClearColorValue {
|
||||||
float32: self.color,
|
float32: self.color,
|
||||||
|
|
@ -219,6 +467,14 @@ impl Command for Draw {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecordedCommand for Draw {
|
impl RecordedCommand for Draw {
|
||||||
|
fn kind(&self) -> CommandKind {
|
||||||
|
CommandKind::Graphics
|
||||||
|
}
|
||||||
|
|
||||||
|
fn allowed_in_render_pass(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
fn preflight(&self) -> Result<(), RecordError> {
|
fn preflight(&self) -> Result<(), RecordError> {
|
||||||
validate_managed_descriptor_sets(&self.descriptor_sets).map(|_| ())
|
validate_managed_descriptor_sets(&self.descriptor_sets).map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
@ -335,6 +591,160 @@ impl RecordedCommand for Draw {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Command for BeginRendering {
|
||||||
|
fn side_effects(&self, command_index: CommandIndex, map: &mut SideEffectMap) {
|
||||||
|
for attachment in &self.color {
|
||||||
|
let mut mask = vk::AccessFlags2::COLOR_ATTACHMENT_WRITE;
|
||||||
|
if attachment.load_op == vk::AttachmentLoadOp::LOAD {
|
||||||
|
mask |= vk::AccessFlags2::COLOR_ATTACHMENT_READ;
|
||||||
|
}
|
||||||
|
map.insert(
|
||||||
|
command_index,
|
||||||
|
attachment.texture.id(),
|
||||||
|
access(
|
||||||
|
vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT,
|
||||||
|
mask,
|
||||||
|
SubresourceScopeInner::Texture {
|
||||||
|
range: attachment.range,
|
||||||
|
layout: attachment.layout,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(attachment) = &self.depth {
|
||||||
|
let mut mask = vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_WRITE;
|
||||||
|
if attachment.depth_load_op == vk::AttachmentLoadOp::LOAD
|
||||||
|
|| attachment.stencil_load_op == vk::AttachmentLoadOp::LOAD
|
||||||
|
{
|
||||||
|
mask |= vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_READ;
|
||||||
|
}
|
||||||
|
map.insert(
|
||||||
|
command_index,
|
||||||
|
attachment.texture.id(),
|
||||||
|
access(
|
||||||
|
vk::PipelineStageFlags2::EARLY_FRAGMENT_TESTS
|
||||||
|
| vk::PipelineStageFlags2::LATE_FRAGMENT_TESTS,
|
||||||
|
mask,
|
||||||
|
SubresourceScopeInner::Texture {
|
||||||
|
range: attachment.range,
|
||||||
|
layout: attachment.layout,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RecordedCommand for BeginRendering {
|
||||||
|
fn kind(&self) -> CommandKind {
|
||||||
|
CommandKind::Begin
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_pass_desc(&self) -> Option<RenderPassDesc<'_>> {
|
||||||
|
Some(RenderPassDesc {
|
||||||
|
color: &self.color,
|
||||||
|
depth: self.depth.as_ref(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
|
||||||
|
RENDERING_SCRATCH.with(|scratch| {
|
||||||
|
let mut scratch = scratch.borrow_mut();
|
||||||
|
let attachments = &mut scratch.color_attachments;
|
||||||
|
attachments.clear();
|
||||||
|
|
||||||
|
let mut extent = None;
|
||||||
|
for attachment in &self.color {
|
||||||
|
let (view, attachment_extent) =
|
||||||
|
resolve_attachment_view(attachment.texture, &attachment.view, recorder)?;
|
||||||
|
extent = Some(rendering_extent(extent, attachment_extent));
|
||||||
|
attachments.push(
|
||||||
|
vk::RenderingAttachmentInfo::default()
|
||||||
|
.image_view(view)
|
||||||
|
.image_layout(attachment.layout)
|
||||||
|
.load_op(attachment.load_op)
|
||||||
|
.store_op(attachment.store_op)
|
||||||
|
.clear_value(attachment.clear_value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut depth_info = None;
|
||||||
|
let mut stencil_info = None;
|
||||||
|
if let Some(attachment) = &self.depth {
|
||||||
|
let (view, attachment_extent) =
|
||||||
|
resolve_attachment_view(attachment.texture, &attachment.view, recorder)?;
|
||||||
|
extent = Some(rendering_extent(extent, attachment_extent));
|
||||||
|
let clear_value = vk::ClearValue {
|
||||||
|
depth_stencil: vk::ClearDepthStencilValue {
|
||||||
|
depth: attachment.clear_depth,
|
||||||
|
stencil: attachment.clear_stencil,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if attachment.range.aspect_mask.contains(vk::ImageAspectFlags::DEPTH) {
|
||||||
|
depth_info = Some(
|
||||||
|
vk::RenderingAttachmentInfo::default()
|
||||||
|
.image_view(view)
|
||||||
|
.image_layout(attachment.layout)
|
||||||
|
.load_op(attachment.depth_load_op)
|
||||||
|
.store_op(attachment.depth_store_op)
|
||||||
|
.clear_value(clear_value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if attachment.range.aspect_mask.contains(vk::ImageAspectFlags::STENCIL) {
|
||||||
|
stencil_info = Some(
|
||||||
|
vk::RenderingAttachmentInfo::default()
|
||||||
|
.image_view(view)
|
||||||
|
.image_layout(attachment.layout)
|
||||||
|
.load_op(attachment.stencil_load_op)
|
||||||
|
.store_op(attachment.stencil_store_op)
|
||||||
|
.clear_value(clear_value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let extent = extent.unwrap_or(vk::Extent2D {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
});
|
||||||
|
let mut info = vk::RenderingInfo::default()
|
||||||
|
.render_area(vk::Rect2D {
|
||||||
|
offset: vk::Offset2D { x: 0, y: 0 },
|
||||||
|
extent,
|
||||||
|
})
|
||||||
|
.layer_count(1)
|
||||||
|
.color_attachments(attachments);
|
||||||
|
if let Some(depth) = depth_info.as_ref() {
|
||||||
|
info = info.depth_attachment(depth);
|
||||||
|
}
|
||||||
|
if let Some(stencil) = stencil_info.as_ref() {
|
||||||
|
info = info.stencil_attachment(stencil);
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
recorder
|
||||||
|
.device
|
||||||
|
.cmd_begin_rendering(recorder.command_buffer, &info)
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Command for EndRendering {
|
||||||
|
fn side_effects(&self, _command_index: CommandIndex, _map: &mut SideEffectMap) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RecordedCommand for EndRendering {
|
||||||
|
fn kind(&self) -> CommandKind {
|
||||||
|
CommandKind::End
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
|
||||||
|
unsafe { recorder.device.cmd_end_rendering(recorder.command_buffer) };
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_managed_descriptor_sets(
|
fn validate_managed_descriptor_sets(
|
||||||
sets: &[GraphDescriptorSet],
|
sets: &[GraphDescriptorSet],
|
||||||
) -> Result<Vec<vk::DescriptorSet>, RecordError> {
|
) -> Result<Vec<vk::DescriptorSet>, RecordError> {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
250
crates/renderer_rendergraph/src/graph_compiler.rs
Normal file
250
crates/renderer_rendergraph/src/graph_compiler.rs
Normal file
|
|
@ -0,0 +1,250 @@
|
||||||
|
use ash::vk;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
CommandIndex, GraphError, SideEffectMap,
|
||||||
|
graph::{Kahn, RenderPassSpan},
|
||||||
|
recorder::BoxedCommand,
|
||||||
|
resources::{ResourceDesc, SubresourceScope, SubresourceScopeInner},
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SpanBuilder {
|
||||||
|
spans: Vec<RenderPassSpan>,
|
||||||
|
current: Option<CommandIndex>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpanBuilder {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
spans: Vec::new(),
|
||||||
|
current: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn in_span(&self) -> bool {
|
||||||
|
self.current.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_begin(&self) -> Option<CommandIndex> {
|
||||||
|
self.current
|
||||||
|
}
|
||||||
|
|
||||||
|
fn begin(&mut self, index: CommandIndex) -> Result<(), GraphError> {
|
||||||
|
if self.current.is_some() {
|
||||||
|
Err(GraphError::NestedRenderPass)
|
||||||
|
} else {
|
||||||
|
self.current = Some(index);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end(&mut self, end: CommandIndex) -> Result<(), GraphError> {
|
||||||
|
if let Some(begin) = self.current.take() {
|
||||||
|
self.spans.push(RenderPassSpan { begin, end });
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(GraphError::UnbalancedRenderPass)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(self) -> Result<Vec<RenderPassSpan>, GraphError> {
|
||||||
|
if self.current.is_some() {
|
||||||
|
Err(GraphError::UnbalancedRenderPass)
|
||||||
|
} else {
|
||||||
|
Ok(self.spans)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Batch {
|
||||||
|
pub(crate) start: usize,
|
||||||
|
pub(crate) count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct CommandOrderings {
|
||||||
|
pub(crate) execution_order: Vec<CommandIndex>,
|
||||||
|
pub(crate) batches: Vec<Batch>,
|
||||||
|
pub(crate) spans: Vec<RenderPassSpan>,
|
||||||
|
pub(crate) node_of_command: Vec<usize>,
|
||||||
|
pub(crate) node_representatives: Vec<CommandIndex>,
|
||||||
|
pub(crate) dependency_edges: Vec<Vec<usize>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_command_orderings(
|
||||||
|
resources: &[ResourceDesc],
|
||||||
|
commands: &[BoxedCommand],
|
||||||
|
side_effects: &mut SideEffectMap,
|
||||||
|
) -> Result<CommandOrderings, GraphError> {
|
||||||
|
let per_command_resource_access = side_effects.command_accesses();
|
||||||
|
let mut kahn = Kahn::new(commands.len());
|
||||||
|
let mut access_by_resource: Vec<Vec<(CommandIndex, Vec<SubresourceScope>)>> =
|
||||||
|
vec![Vec::new(); resources.len()];
|
||||||
|
|
||||||
|
let mut span_builder = SpanBuilder::new();
|
||||||
|
let mut node_of_command = vec![usize::MAX; commands.len()];
|
||||||
|
let mut node_representatives = Vec::new();
|
||||||
|
|
||||||
|
for (index, (command, accesses)) in commands
|
||||||
|
.iter()
|
||||||
|
.zip(per_command_resource_access)
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, c)| (CommandIndex::from(i), c))
|
||||||
|
{
|
||||||
|
let rep = span_builder.current_begin().unwrap_or(index);
|
||||||
|
kahn.add_node(rep.index());
|
||||||
|
|
||||||
|
// Node ids follow first encounter of each representative in command order.
|
||||||
|
let node = if node_of_command[rep.index()] == usize::MAX {
|
||||||
|
let node = node_representatives.len();
|
||||||
|
node_representatives.push(rep);
|
||||||
|
node
|
||||||
|
} else {
|
||||||
|
node_of_command[rep.index()]
|
||||||
|
};
|
||||||
|
node_of_command[index.index()] = node;
|
||||||
|
|
||||||
|
for access in accesses {
|
||||||
|
let Some(resource_access) = access_by_resource.get_mut(access.resource.index()) else {
|
||||||
|
// `validate_resources` reports invalid ids after dependency analysis.
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let priors = resource_access
|
||||||
|
.iter()
|
||||||
|
.take_while(|&&(c, _)| c < rep)
|
||||||
|
.filter(|(_, scopes)| scopes.iter().any(|scope| scope.conflicts(&access.scope)));
|
||||||
|
|
||||||
|
for &(prior, _) in priors {
|
||||||
|
kahn.add_edge(prior.index(), rep.index());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(&mut (last, ref mut scopes)) = resource_access.last_mut()
|
||||||
|
&& last == rep
|
||||||
|
{
|
||||||
|
scopes.push(access.scope);
|
||||||
|
} else {
|
||||||
|
resource_access.push((rep, vec![access.scope]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match command.kind() {
|
||||||
|
crate::CommandKind::Begin => {
|
||||||
|
if let Some(desc) = command.render_pass_desc() {
|
||||||
|
crate::graph::validate_render_pass_attachments(resources, desc)?;
|
||||||
|
}
|
||||||
|
span_builder.begin(index)?;
|
||||||
|
}
|
||||||
|
crate::CommandKind::Graphics => {
|
||||||
|
if !command.allowed_in_render_pass() {
|
||||||
|
return Err(GraphError::CommandNotAllowedInRenderPass(index));
|
||||||
|
}
|
||||||
|
if !span_builder.in_span() {
|
||||||
|
return Err(GraphError::DrawOutsideRenderPass(index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::CommandKind::Transfer
|
||||||
|
| crate::CommandKind::Compute
|
||||||
|
| crate::CommandKind::Other => {
|
||||||
|
if span_builder.in_span() && !command.allowed_in_render_pass() {
|
||||||
|
return Err(GraphError::CommandNotAllowedInRenderPass(index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crate::CommandKind::End => {
|
||||||
|
span_builder.end(index)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// spans are ordered by beginning command
|
||||||
|
let spans = span_builder.finish()?;
|
||||||
|
|
||||||
|
for span in &spans {
|
||||||
|
// Snapshot accesses intentionally predate lifetime-only attachment injection:
|
||||||
|
// these accesses extend lifetimes but must not add hazard edges.
|
||||||
|
if let Some(desc) = commands[span.begin.index()].render_pass_desc() {
|
||||||
|
for attachment in desc.color {
|
||||||
|
side_effects.insert_lifetime_only(
|
||||||
|
span.end,
|
||||||
|
attachment.texture.id(),
|
||||||
|
SubresourceScope {
|
||||||
|
stage: vk::PipelineStageFlags2::COLOR_ATTACHMENT_OUTPUT,
|
||||||
|
access: vk::AccessFlags2::COLOR_ATTACHMENT_WRITE,
|
||||||
|
subresource: SubresourceScopeInner::Texture {
|
||||||
|
range: attachment.range,
|
||||||
|
layout: attachment.layout,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(attachment) = desc.depth {
|
||||||
|
side_effects.insert_lifetime_only(
|
||||||
|
span.end,
|
||||||
|
attachment.texture.id(),
|
||||||
|
SubresourceScope {
|
||||||
|
stage: vk::PipelineStageFlags2::EARLY_FRAGMENT_TESTS
|
||||||
|
| vk::PipelineStageFlags2::LATE_FRAGMENT_TESTS,
|
||||||
|
access: vk::AccessFlags2::DEPTH_STENCIL_ATTACHMENT_WRITE,
|
||||||
|
subresource: SubresourceScopeInner::Texture {
|
||||||
|
range: attachment.range,
|
||||||
|
layout: attachment.layout,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut execution_order: Vec<CommandIndex> = Vec::with_capacity(commands.len());
|
||||||
|
let mut batches = Vec::<Batch>::new();
|
||||||
|
|
||||||
|
let mut span_iter = spans.iter().peekable();
|
||||||
|
|
||||||
|
while !kahn.is_complete() {
|
||||||
|
let first = execution_order.len();
|
||||||
|
|
||||||
|
for cmd in kahn.iter_batch().map(CommandIndex::from) {
|
||||||
|
use core::cmp::Ordering;
|
||||||
|
match span_iter.peek().map(|s| s.begin.cmp(&cmd)) {
|
||||||
|
Some(Ordering::Equal) => {
|
||||||
|
let span = span_iter.next().unwrap();
|
||||||
|
execution_order
|
||||||
|
.extend((span.begin.index()..=span.end.index()).map(CommandIndex::from));
|
||||||
|
}
|
||||||
|
_ => execution_order.push(cmd),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = execution_order.len() - first;
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
return Err(GraphError::DependencyCycle);
|
||||||
|
}
|
||||||
|
|
||||||
|
for command in &execution_order[first..] {
|
||||||
|
kahn.complete(command.index());
|
||||||
|
}
|
||||||
|
|
||||||
|
batches.push(Batch {
|
||||||
|
start: first,
|
||||||
|
count,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut dependency_edges = vec![Vec::new(); node_representatives.len()];
|
||||||
|
for (node, representative) in node_representatives.iter().enumerate() {
|
||||||
|
let representative = representative.index();
|
||||||
|
let node_edges = &mut dependency_edges[node];
|
||||||
|
for &edge in &kahn.edges[representative] {
|
||||||
|
node_edges.push(node_of_command[edge]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(CommandOrderings {
|
||||||
|
execution_order,
|
||||||
|
batches,
|
||||||
|
spans,
|
||||||
|
node_of_command,
|
||||||
|
node_representatives,
|
||||||
|
dependency_edges,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -3,12 +3,18 @@
|
||||||
|
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod graph;
|
pub mod graph;
|
||||||
|
mod graph_compiler;
|
||||||
pub mod recorder;
|
pub mod recorder;
|
||||||
pub mod resources;
|
pub mod resources;
|
||||||
|
|
||||||
|
pub use commands::{
|
||||||
|
AttachmentView, BeginRendering, ColorAttachment, CommandKind, DepthStencilAttachment,
|
||||||
|
EndRendering, RenderPassDesc,
|
||||||
|
};
|
||||||
pub use graph::{
|
pub use graph::{
|
||||||
CompiledGraph, GraphError, HappensBefore, MemoryBarrier, Placement, RenderGraph,
|
BarrierOrigin, CompiledGraph, GraphError, HappensBefore, MemoryBarrier, Placement, RenderGraph,
|
||||||
ResourceInterference, ResourceLifetime, ResourceUse, aliasing_permitted,
|
RenderPassSpan, ResourceFrontiers, ResourceInterference, ResourceLifetime, ResourceUse,
|
||||||
|
aliasing_permitted,
|
||||||
};
|
};
|
||||||
pub use recorder::{CommandIndex, CommandList, CommandRecorder, SideEffectMap};
|
pub use recorder::{CommandIndex, CommandList, CommandRecorder, SideEffectMap};
|
||||||
pub use resources::{
|
pub use resources::{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
use std::{cell::RefCell, collections::BTreeMap};
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
collections::{BTreeMap, BTreeSet, HashMap},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
use ash::vk;
|
use ash::vk;
|
||||||
|
use renderer_resources::images::{ImageView, ImageViewDesc};
|
||||||
|
|
||||||
use crate::resources::{
|
use crate::resources::{
|
||||||
BufferRange, ResourceId, Subresource, SubresourceScope, SubresourceScopeInner,
|
BufferRange, ResourceId, Subresource, SubresourceScope, SubresourceScopeInner,
|
||||||
|
|
@ -38,6 +43,7 @@ pub struct ResourceAccess {
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct SideEffectMap {
|
pub struct SideEffectMap {
|
||||||
accesses: Vec<Vec<ResourceAccess>>,
|
accesses: Vec<Vec<ResourceAccess>>,
|
||||||
|
lifetime_only: BTreeSet<(usize, ResourceId)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SideEffectMap {
|
impl SideEffectMap {
|
||||||
|
|
@ -81,6 +87,23 @@ impl SideEffectMap {
|
||||||
accesses.push(ResourceAccess { resource, scope });
|
accesses.push(ResourceAccess { resource, scope });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Record an access used only to extend a resource's lifetime through a command.
|
||||||
|
pub fn insert_lifetime_only(
|
||||||
|
&mut self,
|
||||||
|
command_index: CommandIndex,
|
||||||
|
resource: ResourceId,
|
||||||
|
scope: SubresourceScope,
|
||||||
|
) {
|
||||||
|
self.insert(command_index, resource, scope);
|
||||||
|
self.lifetime_only.insert((command_index.index(), resource));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this command/resource access exists only for lifetime analysis.
|
||||||
|
pub fn is_lifetime_only(&self, command_index: CommandIndex, resource: ResourceId) -> bool {
|
||||||
|
self.lifetime_only
|
||||||
|
.contains(&(command_index.index(), resource))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn accesses(&self, command_index: CommandIndex) -> &[ResourceAccess] {
|
pub fn accesses(&self, command_index: CommandIndex) -> &[ResourceAccess] {
|
||||||
self.accesses
|
self.accesses
|
||||||
.get(command_index.0)
|
.get(command_index.0)
|
||||||
|
|
@ -92,10 +115,12 @@ impl SideEffectMap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type BoxedCommand = Box<dyn RecordedCommand>;
|
||||||
|
|
||||||
/// A safe, type-erased temporary command buffer. Each command is owned until
|
/// A safe, type-erased temporary command buffer. Each command is owned until
|
||||||
/// compilation/recording, while its access declarations are stored separately.
|
/// compilation/recording, while its access declarations are stored separately.
|
||||||
pub struct CommandList {
|
pub struct CommandList {
|
||||||
pub(crate) commands: Vec<Box<dyn RecordedCommand>>,
|
pub(crate) commands: Vec<BoxedCommand>,
|
||||||
pub side_effects: SideEffectMap,
|
pub side_effects: SideEffectMap,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,6 +130,18 @@ pub trait RecordedCommand {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn kind(&self) -> crate::commands::CommandKind {
|
||||||
|
crate::commands::CommandKind::Other
|
||||||
|
}
|
||||||
|
|
||||||
|
fn allowed_in_render_pass(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_pass_desc(&self) -> Option<crate::commands::RenderPassDesc<'_>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError>;
|
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,6 +186,7 @@ impl Default for CommandList {
|
||||||
pub enum RecordError {
|
pub enum RecordError {
|
||||||
MissingBuffer(ResourceId),
|
MissingBuffer(ResourceId),
|
||||||
MissingImage(ResourceId),
|
MissingImage(ResourceId),
|
||||||
|
MissingView(ResourceId),
|
||||||
UnresolvedCreatedBuffer(ResourceId),
|
UnresolvedCreatedBuffer(ResourceId),
|
||||||
UnresolvedCreatedImage(ResourceId),
|
UnresolvedCreatedImage(ResourceId),
|
||||||
DescriptorSet(String),
|
DescriptorSet(String),
|
||||||
|
|
@ -161,6 +199,7 @@ pub struct CommandRecorder<'a> {
|
||||||
pub command_buffer: vk::CommandBuffer,
|
pub command_buffer: vk::CommandBuffer,
|
||||||
pub buffers: BTreeMap<ResourceId, vk::Buffer>,
|
pub buffers: BTreeMap<ResourceId, vk::Buffer>,
|
||||||
pub images: BTreeMap<ResourceId, vk::Image>,
|
pub images: BTreeMap<ResourceId, vk::Image>,
|
||||||
|
pub views: HashMap<(ResourceId, ImageViewDesc), Arc<ImageView>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|
@ -181,6 +220,7 @@ impl<'a> CommandRecorder<'a> {
|
||||||
command_buffer,
|
command_buffer,
|
||||||
buffers: BTreeMap::new(),
|
buffers: BTreeMap::new(),
|
||||||
images: BTreeMap::new(),
|
images: BTreeMap::new(),
|
||||||
|
views: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -198,6 +238,18 @@ impl<'a> CommandRecorder<'a> {
|
||||||
.ok_or(RecordError::MissingImage(id))
|
.ok_or(RecordError::MissingImage(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve a cached image view by graph resource and concrete view description.
|
||||||
|
pub fn view(
|
||||||
|
&self,
|
||||||
|
id: ResourceId,
|
||||||
|
desc: &ImageViewDesc,
|
||||||
|
) -> Result<Arc<ImageView>, RecordError> {
|
||||||
|
self.views
|
||||||
|
.get(&(id, image_view_desc_without_name(desc)))
|
||||||
|
.cloned()
|
||||||
|
.ok_or(RecordError::MissingView(id))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn pipeline_barrier(&self, barrier: &super::graph::PipelineBarrier) {
|
pub fn pipeline_barrier(&self, barrier: &super::graph::PipelineBarrier) {
|
||||||
BARRIER_SCRATCH.with(|scratch| {
|
BARRIER_SCRATCH.with(|scratch| {
|
||||||
let mut scratch = scratch.borrow_mut();
|
let mut scratch = scratch.borrow_mut();
|
||||||
|
|
@ -288,6 +340,19 @@ impl<'a> CommandRecorder<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn image_view_desc_without_name(desc: &ImageViewDesc) -> ImageViewDesc {
|
||||||
|
ImageViewDesc {
|
||||||
|
name: None,
|
||||||
|
flags: desc.flags,
|
||||||
|
kind: desc.kind,
|
||||||
|
format: desc.format,
|
||||||
|
components: desc.components,
|
||||||
|
aspect: desc.aspect,
|
||||||
|
mip_range: desc.mip_range,
|
||||||
|
layer_range: desc.layer_range,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn scope_range(scope: SubresourceScope) -> Subresource {
|
pub(crate) fn scope_range(scope: SubresourceScope) -> Subresource {
|
||||||
match scope.subresource {
|
match scope.subresource {
|
||||||
SubresourceScopeInner::Buffer(range) => Subresource::Buffer(range),
|
SubresourceScopeInner::Buffer(range) => Subresource::Buffer(range),
|
||||||
|
|
@ -337,7 +402,7 @@ mod tests {
|
||||||
dst_stage: vk::PipelineStageFlags2::FRAGMENT_SHADER,
|
dst_stage: vk::PipelineStageFlags2::FRAGMENT_SHADER,
|
||||||
dst_access: vk::AccessFlags2::SHADER_READ,
|
dst_access: vk::AccessFlags2::SHADER_READ,
|
||||||
}],
|
}],
|
||||||
resources: Vec::new(),
|
resources: tinyvec::TinyVec::new(),
|
||||||
};
|
};
|
||||||
MEMORY_BARRIER_COUNT.store(0, Ordering::SeqCst);
|
MEMORY_BARRIER_COUNT.store(0, Ordering::SeqCst);
|
||||||
MEMORY_BARRIER_SRC_STAGE.store(0, Ordering::SeqCst);
|
MEMORY_BARRIER_SRC_STAGE.store(0, Ordering::SeqCst);
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,19 @@ use renderer_device::{
|
||||||
testing::TestVulkan,
|
testing::TestVulkan,
|
||||||
};
|
};
|
||||||
use renderer_rendergraph::{
|
use renderer_rendergraph::{
|
||||||
RenderGraph,
|
BeginRendering, ColorAttachment, EndRendering, RenderGraph,
|
||||||
commands::Command,
|
|
||||||
commands::CopyBuffer,
|
commands::CopyBuffer,
|
||||||
|
commands::{ClearTexture, Command},
|
||||||
recorder::{CommandIndex, CommandRecorder, RecordError, RecordedCommand, SideEffectMap},
|
recorder::{CommandIndex, CommandRecorder, RecordError, RecordedCommand, SideEffectMap},
|
||||||
resources::{
|
resources::{
|
||||||
BufferRange, GraphBufferDesc, GraphTextureDesc, ResourceId, ResourceState,
|
BufferRange, GraphBufferDesc, GraphTextureDesc, ResourceId, ResourceState,
|
||||||
SubresourceScope, SubresourceScopeInner,
|
SubresourceScope, SubresourceScopeInner,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use renderer_resources::buffers::{Buffer, BufferDesc};
|
use renderer_resources::{
|
||||||
|
buffers::{Buffer, BufferDesc},
|
||||||
|
images::ImageViewDesc,
|
||||||
|
};
|
||||||
|
|
||||||
struct ScopedNoop(Vec<(ResourceId, SubresourceScope)>);
|
struct ScopedNoop(Vec<(ResourceId, SubresourceScope)>);
|
||||||
|
|
||||||
|
|
@ -33,6 +36,26 @@ impl RecordedCommand for ScopedNoop {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct PassNoop(Vec<(ResourceId, SubresourceScope)>);
|
||||||
|
|
||||||
|
impl Command for PassNoop {
|
||||||
|
fn side_effects(&self, command_index: CommandIndex, map: &mut SideEffectMap) {
|
||||||
|
for &(resource, scope) in &self.0 {
|
||||||
|
map.insert(command_index, resource, scope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RecordedCommand for PassNoop {
|
||||||
|
fn allowed_in_render_pass(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply(&mut self, _recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn buffer_scope() -> SubresourceScope {
|
fn buffer_scope() -> SubresourceScope {
|
||||||
SubresourceScope {
|
SubresourceScope {
|
||||||
stage: vk::PipelineStageFlags2::COPY,
|
stage: vk::PipelineStageFlags2::COPY,
|
||||||
|
|
@ -61,6 +84,47 @@ fn image_scope() -> SubresourceScope {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pass_buffer_read_scope() -> SubresourceScope {
|
||||||
|
SubresourceScope {
|
||||||
|
stage: vk::PipelineStageFlags2::VERTEX_SHADER,
|
||||||
|
access: vk::AccessFlags2::SHADER_READ,
|
||||||
|
subresource: SubresourceScopeInner::Buffer(BufferRange {
|
||||||
|
offset: 0,
|
||||||
|
size: 256,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pass_texture_read_scope() -> SubresourceScope {
|
||||||
|
SubresourceScope {
|
||||||
|
stage: vk::PipelineStageFlags2::FRAGMENT_SHADER,
|
||||||
|
access: vk::AccessFlags2::SHADER_SAMPLED_READ,
|
||||||
|
subresource: SubresourceScopeInner::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::SHADER_READ_ONLY_OPTIMAL,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_render_pass_test(test: &TestVulkan) -> bool {
|
||||||
|
let features = test.device().enabled_features().core13;
|
||||||
|
if features.synchronization2 != vk::TRUE {
|
||||||
|
eprintln!("Skipping graph render pass: synchronization2 is unsupported");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if features.dynamic_rendering != vk::TRUE {
|
||||||
|
eprintln!("Skipping graph render pass: dynamic_rendering is unsupported");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
fn submit_graph(test: &TestVulkan, graph: &mut renderer_rendergraph::CompiledGraph) {
|
fn submit_graph(test: &TestVulkan, graph: &mut renderer_rendergraph::CompiledGraph) {
|
||||||
let device = test.device();
|
let device = test.device();
|
||||||
let pool_info = vk::CommandPoolCreateInfo::default()
|
let pool_info = vk::CommandPoolCreateInfo::default()
|
||||||
|
|
@ -179,7 +243,7 @@ fn imported_buffer_copy_records_under_validation() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolved_buffers_alias_and_submit_with_memory_barrier() {
|
fn resolved_buffers_alias_and_submit_with_scoped_barrier() {
|
||||||
let Some(test) = TestVulkan::acquire() else {
|
let Some(test) = TestVulkan::acquire() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
@ -233,9 +297,21 @@ fn resolved_buffers_alias_and_submit_with_memory_barrier() {
|
||||||
assert_eq!(first_placement.pool, second_placement.pool);
|
assert_eq!(first_placement.pool, second_placement.pool);
|
||||||
assert_eq!(first_placement.offset, second_placement.offset);
|
assert_eq!(first_placement.offset, second_placement.offset);
|
||||||
assert!(first_placement.aliased && second_placement.aliased);
|
assert!(first_placement.aliased && second_placement.aliased);
|
||||||
assert!(compiled.barriers().iter().any(|barrier| {
|
let alias_barrier = compiled
|
||||||
barrier.before_command == CommandIndex(1) && !barrier.memory_barriers.is_empty()
|
.barriers()
|
||||||
}));
|
.iter()
|
||||||
|
.find(|barrier| barrier.before_command == CommandIndex(1))
|
||||||
|
.expect("incoming buffer's first use has an alias barrier");
|
||||||
|
assert!(alias_barrier.memory_barriers.is_empty());
|
||||||
|
let incoming = alias_barrier
|
||||||
|
.resources
|
||||||
|
.iter()
|
||||||
|
.find(|resource| resource.id == second.id())
|
||||||
|
.expect("alias barrier is scoped to the incoming buffer");
|
||||||
|
assert_eq!(incoming.src_stage, vk::PipelineStageFlags2::COPY);
|
||||||
|
assert_eq!(incoming.src_access, vk::AccessFlags2::TRANSFER_WRITE);
|
||||||
|
assert_eq!(incoming.dst_stage, vk::PipelineStageFlags2::COPY);
|
||||||
|
assert_eq!(incoming.dst_access, vk::AccessFlags2::TRANSFER_WRITE);
|
||||||
|
|
||||||
submit_graph(&test, &mut compiled);
|
submit_graph(&test, &mut compiled);
|
||||||
test.assert_no_validation_errors();
|
test.assert_no_validation_errors();
|
||||||
|
|
@ -328,3 +404,149 @@ fn resolved_images_alias_and_submit_with_undefined_transition() {
|
||||||
.expect("wait before shared allocation teardown");
|
.expect("wait before shared allocation teardown");
|
||||||
test.assert_no_validation_errors();
|
test.assert_no_validation_errors();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn created_render_pass_records_submits_and_validates() {
|
||||||
|
let Some(test) = TestVulkan::acquire() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if !supports_render_pass_test(&test) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let device = test.device();
|
||||||
|
let make_buffer = || {
|
||||||
|
Arc::new(
|
||||||
|
Buffer::new(
|
||||||
|
device.clone(),
|
||||||
|
BufferDesc {
|
||||||
|
size: 256,
|
||||||
|
usage: vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST,
|
||||||
|
queue_families: QueueFlags::empty(),
|
||||||
|
mem_location: MemoryLocation::CpuToGpu,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("create render-pass test buffer"),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let producer_source = make_buffer();
|
||||||
|
let producer_destination = make_buffer();
|
||||||
|
let unrelated_destination = make_buffer();
|
||||||
|
|
||||||
|
let mut graph = RenderGraph::new();
|
||||||
|
let target = graph.create_texture(GraphTextureDesc {
|
||||||
|
format: vk::Format::R8G8B8A8_UNORM,
|
||||||
|
extent: vk::Extent3D {
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
depth: 1,
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
let producer_source = graph
|
||||||
|
.import_buffer(producer_source, ResourceState::default())
|
||||||
|
.expect("import producer source");
|
||||||
|
let producer_destination = graph
|
||||||
|
.import_buffer(producer_destination, ResourceState::default())
|
||||||
|
.expect("import producer destination");
|
||||||
|
let unrelated_destination = graph
|
||||||
|
.import_buffer(unrelated_destination, ResourceState::default())
|
||||||
|
.expect("import unrelated destination");
|
||||||
|
let buffer_range = BufferRange {
|
||||||
|
offset: 0,
|
||||||
|
size: 256,
|
||||||
|
};
|
||||||
|
let image_range = vk::ImageSubresourceRange {
|
||||||
|
aspect_mask: vk::ImageAspectFlags::COLOR,
|
||||||
|
base_mip_level: 0,
|
||||||
|
level_count: 1,
|
||||||
|
base_array_layer: 0,
|
||||||
|
layer_count: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
graph.push(ClearTexture {
|
||||||
|
texture: target,
|
||||||
|
range: image_range,
|
||||||
|
layout: vk::ImageLayout::TRANSFER_DST_OPTIMAL,
|
||||||
|
color: [0.1, 0.2, 0.3, 1.0],
|
||||||
|
});
|
||||||
|
let producer = graph.push(CopyBuffer {
|
||||||
|
src: producer_source,
|
||||||
|
src_range: buffer_range,
|
||||||
|
dst: producer_destination,
|
||||||
|
dst_range: buffer_range,
|
||||||
|
});
|
||||||
|
let begin = graph.push(BeginRendering {
|
||||||
|
color: vec![ColorAttachment::created(
|
||||||
|
target,
|
||||||
|
ImageViewDesc::color_2d(),
|
||||||
|
vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
|
||||||
|
vk::AttachmentLoadOp::LOAD,
|
||||||
|
vk::AttachmentStoreOp::STORE,
|
||||||
|
vk::ClearValue::default(),
|
||||||
|
)],
|
||||||
|
depth: None,
|
||||||
|
});
|
||||||
|
let interior = graph.push(PassNoop(vec![(
|
||||||
|
producer_destination.id(),
|
||||||
|
pass_buffer_read_scope(),
|
||||||
|
)]));
|
||||||
|
let end = graph.push(EndRendering);
|
||||||
|
let post_end = graph.push(ScopedNoop(vec![(target.id(), pass_texture_read_scope())]));
|
||||||
|
let unrelated = graph.push(CopyBuffer {
|
||||||
|
src: producer_destination,
|
||||||
|
src_range: buffer_range,
|
||||||
|
dst: unrelated_destination,
|
||||||
|
dst_range: buffer_range,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut compiled = graph.compile().expect("compile render-pass graph");
|
||||||
|
let execution_order = compiled.execution_order();
|
||||||
|
let position = |command| {
|
||||||
|
execution_order
|
||||||
|
.iter()
|
||||||
|
.position(|&item| item == command)
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
assert!(position(begin) < position(interior));
|
||||||
|
assert!(position(interior) < position(end));
|
||||||
|
assert!(position(unrelated) > position(end));
|
||||||
|
let begin_barrier = compiled
|
||||||
|
.barriers()
|
||||||
|
.iter()
|
||||||
|
.find(|barrier| barrier.before_command == begin)
|
||||||
|
.expect("incoming barriers are hoisted before BeginRendering");
|
||||||
|
assert!(begin_barrier.resources.iter().any(|resource| {
|
||||||
|
resource.id == target.id()
|
||||||
|
&& resource.old_layout == Some(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
|
||||||
|
&& resource.new_layout == Some(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
|
||||||
|
}));
|
||||||
|
assert!(begin_barrier.resources.iter().any(|resource| {
|
||||||
|
resource.id == producer_destination.id()
|
||||||
|
&& resource.origin == renderer_rendergraph::BarrierOrigin::Command(producer)
|
||||||
|
}));
|
||||||
|
assert!(compiled.barriers().iter().any(|barrier| {
|
||||||
|
barrier.before_command == post_end
|
||||||
|
&& barrier.resources.iter().any(|resource| {
|
||||||
|
resource.id == target.id()
|
||||||
|
&& resource.old_layout == Some(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
|
||||||
|
&& resource.new_layout == Some(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
compiled
|
||||||
|
.resolve(device)
|
||||||
|
.expect("resolve created attachment view");
|
||||||
|
assert!(
|
||||||
|
compiled
|
||||||
|
.created_view(target.id(), &ImageViewDesc::color_2d())
|
||||||
|
.is_some()
|
||||||
|
);
|
||||||
|
submit_graph(&test, &mut compiled);
|
||||||
|
test.assert_no_validation_errors();
|
||||||
|
drop(compiled);
|
||||||
|
device
|
||||||
|
.wait_idle()
|
||||||
|
.expect("wait before render target teardown");
|
||||||
|
test.assert_no_validation_errors();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
mem::ManuallyDrop,
|
|
||||||
ops::{Add, Sub},
|
ops::{Add, Sub},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
@ -555,10 +554,7 @@ impl Image {
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
pub fn create_view(
|
pub fn create_view(self: &Arc<Self>, mut desc: ImageViewDesc) -> crate::Result<ImageView> {
|
||||||
self: &Arc<Self>,
|
|
||||||
mut desc: ImageViewDesc,
|
|
||||||
) -> crate::Result<ManuallyDrop<ImageView>> {
|
|
||||||
// validate
|
// validate
|
||||||
if !view_kind_compatible(self.desc.kind, desc.kind) {
|
if !view_kind_compatible(self.desc.kind, desc.kind) {
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
|
|
@ -618,7 +614,7 @@ impl Image {
|
||||||
let device = self.image.device();
|
let device = self.image.device();
|
||||||
let view = unsafe { device.raw.create_image_view(&create_info, None)? };
|
let view = unsafe { device.raw.create_image_view(&create_info, None)? };
|
||||||
|
|
||||||
Ok(ManuallyDrop::new(ImageView {
|
Ok(ImageView {
|
||||||
view: ImageViewHandle::Owned(DeviceObject::new_debug_named(
|
view: ImageViewHandle::Owned(DeviceObject::new_debug_named(
|
||||||
device.clone(),
|
device.clone(),
|
||||||
view,
|
view,
|
||||||
|
|
@ -626,7 +622,7 @@ impl Image {
|
||||||
)),
|
)),
|
||||||
desc,
|
desc,
|
||||||
image: self.clone(),
|
image: self.clone(),
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1189,26 +1185,6 @@ fn image_view_subresource_range(desc: &ImageViewDesc) -> vk::ImageSubresourceRan
|
||||||
.layer_count(desc.layer_range.len())
|
.layer_count(desc.layer_range.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod descriptor_range_tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn normalized_range_accessor_resolves_remaining_sentinels() {
|
|
||||||
let mut desc = ImageViewDesc::color_2d()
|
|
||||||
.with_mip_range(1..)
|
|
||||||
.with_layer_range(2..);
|
|
||||||
desc.mip_range.set_max_end(6);
|
|
||||||
desc.layer_range.set_max_end(9);
|
|
||||||
let range = image_view_subresource_range(&desc);
|
|
||||||
assert_eq!(range.aspect_mask, vk::ImageAspectFlags::COLOR);
|
|
||||||
assert_eq!(range.base_mip_level, 1);
|
|
||||||
assert_eq!(range.level_count, 5);
|
|
||||||
assert_eq!(range.base_array_layer, 2);
|
|
||||||
assert_eq!(range.layer_count, 7);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct QueueOwnership {
|
pub struct QueueOwnership {
|
||||||
pub src: u32,
|
pub src: u32,
|
||||||
pub dst: u32,
|
pub dst: u32,
|
||||||
|
|
@ -1336,3 +1312,23 @@ impl ImageSubresourceExt for vk::ImageSubresource {
|
||||||
MipRange::single(self.mip_level)
|
MipRange::single(self.mip_level)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod descriptor_range_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalized_range_accessor_resolves_remaining_sentinels() {
|
||||||
|
let mut desc = ImageViewDesc::color_2d()
|
||||||
|
.with_mip_range(1..)
|
||||||
|
.with_layer_range(2..);
|
||||||
|
desc.mip_range.set_max_end(6);
|
||||||
|
desc.layer_range.set_max_end(9);
|
||||||
|
let range = image_view_subresource_range(&desc);
|
||||||
|
assert_eq!(range.aspect_mask, vk::ImageAspectFlags::COLOR);
|
||||||
|
assert_eq!(range.base_mip_level, 1);
|
||||||
|
assert_eq!(range.level_count, 5);
|
||||||
|
assert_eq!(range.base_array_layer, 2);
|
||||||
|
assert_eq!(range.layer_count, 7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
575
design-plans/rendergraph-render-passes.md
Normal file
575
design-plans/rendergraph-render-passes.md
Normal file
|
|
@ -0,0 +1,575 @@
|
||||||
|
# Plan: Render-Pass Support (`BeginRendering` / `EndRendering`) in `renderer_rendergraph`
|
||||||
|
|
||||||
|
Status: proposed — rev 5 (rev-1 correctness, rev-2 performance, rev-3 adversarial, and rev-4 re-review applied; rev-4 verdict was REJECT, findings resolved here)
|
||||||
|
Date: 2026-10-27
|
||||||
|
Scope: `crates/renderer_rendergraph`
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Add dynamic-rendering render-pass support to the rendergraph: `BeginRendering`
|
||||||
|
and `EndRendering` commands that bracket a span of graph commands, with the
|
||||||
|
compiler guaranteeing that every command recorded between the two is emitted
|
||||||
|
inside the rendering instance — i.e. commands that must be in a rendergraph
|
||||||
|
pass are ordered by the begin/end rendering commands, and no foreign command
|
||||||
|
may be scheduled between them.
|
||||||
|
|
||||||
|
## Current state (verified against code)
|
||||||
|
|
||||||
|
- Commands implement `Command` (declares `side_effects` → `SubresourceScope`
|
||||||
|
accesses) + `RecordedCommand` (`preflight`/`apply`), and are pushed via
|
||||||
|
`RenderGraph::push` into `CommandList` (`commands.rs`, `recorder.rs`).
|
||||||
|
- `GraphCompiler::dependencies()` (`graph.rs:820-863`) builds hazard edges
|
||||||
|
purely from resource conflicts; `topo_batches_and_dependencies()`
|
||||||
|
(`graph.rs:870-889`) Kahn-sorts commands into `batches`;
|
||||||
|
`record_commands` (`graph.rs:2500-2554`) flattens batches, emitting barriers
|
||||||
|
(keyed `before_command` via `grouped_barrier_indices`) immediately before
|
||||||
|
each command.
|
||||||
|
- There is no notion of a render pass. Nothing prevents the flattener from
|
||||||
|
emitting an unrelated command between two commands the user recorded
|
||||||
|
adjacently: Kahn batching interleaves hazard-free commands freely.
|
||||||
|
- `CommandRecorder` (`recorder.rs:159-164`) maps `ResourceId → vk::Buffer` /
|
||||||
|
`vk::Image` only; there is no image-view plumbing. Descriptor imports read
|
||||||
|
`Arc<ImageView>` bindings at import time (`graph.rs:507-513`) but retain
|
||||||
|
only the underlying image (`StagedImportResource::Texture(view.image().clone())`,
|
||||||
|
`graph.rs:524-532`) — attachment views are new plumbing, not an existing
|
||||||
|
pattern to reuse.
|
||||||
|
- `CommandList` stores type-erased `Box<dyn RecordedCommand>` (`recorder.rs:97-109`);
|
||||||
|
the `Command` trait exposes only `side_effects` (`commands.rs:14-16`), so
|
||||||
|
there is currently no way to classify a command or introspect its render-pass
|
||||||
|
descriptor.
|
||||||
|
- The graph documents a single-queue submission contract (`graph.rs:8-10`);
|
||||||
|
ordinary barriers set `queue_transfer: None` (`graph.rs:972-982, 1001-1012`).
|
||||||
|
- Compile-time cost profile (recomputed independently): `dependencies()` is
|
||||||
|
`O(Σ_r u_r²)` pair checks (plus a per-pair scope cross-product factor);
|
||||||
|
`HappensBefore::from_dag` allocates one `FixedBitSet` row per node
|
||||||
|
(`graph.rs:213-217`, `O(N²/64)` memory) and unions per edge
|
||||||
|
(`O(E·N/64)` time). `ResourceInterference::from_resource_uses` **already**
|
||||||
|
computes terminal/initial frontier sets once per resource
|
||||||
|
(`graph.rs:1348-1356`); the remaining recomputations are in
|
||||||
|
`first_fit_placement` (`graph.rs:1642-1649`), the `overlapping` loop in
|
||||||
|
`plan_placements` (`graph.rs:2384-2399`), and `build_alias_barriers`
|
||||||
|
(`graph.rs:1088, 1109-1117`). Contraction does not remove those; see D8.
|
||||||
|
- The crate is not yet referenced by `renderer`/`bevy_vulkan_render`; the
|
||||||
|
current per-frame path is the legacy `renderer::render_graph`. If this
|
||||||
|
crate replaces it, `compile()` becomes per-frame, so compile cost matters.
|
||||||
|
- `dynamic_rendering` and `synchronization2` are requested/enabled
|
||||||
|
(`crates/renderer/src/lib.rs:352-357`, `crates/renderer_device/src/testing.rs:166-172`).
|
||||||
|
- **Pre-existing defect (reproduced):** `build_barriers` records only the last
|
||||||
|
*conflicting* producer's layout (`graph.rs:951-982`) and never chains
|
||||||
|
`old_layout` through earlier barriers. Two same-layout readers of one
|
||||||
|
producer yield two image barriers both claiming `oldLayout = producerLayout`;
|
||||||
|
the second is stale (Vulkan requires `oldLayout` be the current layout or
|
||||||
|
`UNDEFINED`). Fixing this generally (per-subresource current-layout map) is
|
||||||
|
part of Task 3 — the plan's own tests hit it.
|
||||||
|
- `Image::create_view` returns `Result<ManuallyDrop<ImageView>>`
|
||||||
|
(`renderer_resources/src/images.rs:558-621`); storing it directly leaks the
|
||||||
|
`vk::ImageView` and the `Arc<Image>` (the existing renderer calls `.raw()`
|
||||||
|
on the `ManuallyDrop`, `crates/renderer/src/rendering/mod.rs:332`, which
|
||||||
|
leaks today). New code must use `ManuallyDrop::into_inner`.
|
||||||
|
- `ImageView` holds `Arc<Image>` (`images.rs:1123-1127`), so a view cache
|
||||||
|
keeps its image alive; drop-order interaction with `CompiledGraph` is benign
|
||||||
|
provided the `ManuallyDrop` is unwrapped.
|
||||||
|
|
||||||
|
## Design decisions
|
||||||
|
|
||||||
|
### D1 — Dynamic rendering, not `VkRenderPass`
|
||||||
|
|
||||||
|
New commands `BeginRendering` / `EndRendering` wrapping `vkCmdBeginRendering`
|
||||||
|
/ `vkCmdEndRendering` (Vulkan 1.3 `dynamic_rendering`). Attachments are graph
|
||||||
|
`Texture` handles plus a view specification (see D9 for the created-texture
|
||||||
|
problem).
|
||||||
|
|
||||||
|
### D2 — The pass span is an atomic scheduling unit, contracted *before* hazard-edge construction
|
||||||
|
|
||||||
|
The compiler contracts the `[begin, end]` command span into a single DAG node
|
||||||
|
("pass node"). This is the mechanism that satisfies the ordering contract:
|
||||||
|
|
||||||
|
- No foreign command can ever be scheduled inside the span (Kahn batching
|
||||||
|
would otherwise interleave hazard-free commands into it).
|
||||||
|
- The whole span is ordered relative to everything else by the begin/end
|
||||||
|
hazard edges.
|
||||||
|
|
||||||
|
Contraction happens **before** `dependencies()` builds hazard edges. This
|
||||||
|
removes the `O(Σ u_r²)` interior-draw edge construction (edges that would be
|
||||||
|
discarded as self-edges anyway) and shrinks the `O(N²/64)` closure memory/time
|
||||||
|
by roughly `(N_c/N)²`.
|
||||||
|
|
||||||
|
The contraction mapping used by every consumer:
|
||||||
|
|
||||||
|
- **Type-safe node indexing:** use `NodeId(NonZeroU32)` newtypes and
|
||||||
|
`Option<NodeId>` rather than raw `u32` sentinels mixed into `CommandIndex`
|
||||||
|
spaces. To minimize churn, keep `batches: Vec<Vec<CommandIndex>>` with the
|
||||||
|
`BeginRendering` command index as the documented pass-node representative;
|
||||||
|
`spans: Vec<Span>` on `CompiledGraph` records `{ begin, end, interior }`
|
||||||
|
for expansion. Document the semantic change to the public `batches()` /
|
||||||
|
`dependency_edges()` accessors. Closure rows are indexed `0..N_c`.
|
||||||
|
- **HappensBefore is span-aware internally:** `HappensBefore` (public,
|
||||||
|
`lib.rs:10`) stores the command→node map and span slices and translates in
|
||||||
|
`contains`/`happens_before` (`graph.rs:244-251`), so command-space queries
|
||||||
|
keep working and interior indices never silently return `false` from a
|
||||||
|
node-space row lookup.
|
||||||
|
- **Intra-span record order is NOT a happens-before fact.** Interior commands
|
||||||
|
map to the pass node and same-node uses are treated as **unordered** by the
|
||||||
|
closure used for aliasing/interference. This is a memory-correctness
|
||||||
|
requirement, not a style choice: if intra-span record order were visible to
|
||||||
|
`resources_alias_in_order`, a created attachment `A` (single use at `Begin`)
|
||||||
|
and a created storage image `B` used only by an interior command would be
|
||||||
|
judged ordered, `first_fit_placement` could place `B` over `A`'s memory, and
|
||||||
|
`build_alias_barriers` would either emit no barrier or an illegal in-pass
|
||||||
|
`UNDEFINED → layout` transition — discarding a live render target mid-pass.
|
||||||
|
Intra-span record order is needed only by `build_barriers` (which iterates
|
||||||
|
execution order directly) and by recording; it must not leak into the
|
||||||
|
aliasing/interference closure.
|
||||||
|
- **Attachment liveness is modeled through `End`.** `EndRendering` carries
|
||||||
|
synthetic accesses (the attachment layout, `COLOR_ATTACHMENT_WRITE` /
|
||||||
|
`DEPTH_STENCIL_ATTACHMENT_WRITE` at the attachment stage) used **only** for
|
||||||
|
lifetime/alias analysis — excluded from barrier generation (the begin's
|
||||||
|
merged scope already covers synchronization) and from usage derivation.
|
||||||
|
This makes `terminal_uses(attachment) == End`, so an attachment is never
|
||||||
|
judged dead before the store.
|
||||||
|
- **Barrier analysis:** `build_barriers` walks scheduled command indices and
|
||||||
|
reads only `accesses[command_index]` (`graph.rs:930-934`). Barrier analysis
|
||||||
|
runs over the *uncontracted* access list in **execution order** (see D6),
|
||||||
|
with each interior command's scheduling position mapped to the pass node.
|
||||||
|
This preserves, e.g., the barrier for an outside producer consumed only by
|
||||||
|
an interior `Draw`.
|
||||||
|
- **Lifetimes:** `bind_lifetimes` indexes `batch_for_command` for every
|
||||||
|
original access (`graph.rs:1258-1275`); interior commands map to the pass
|
||||||
|
node's batch index so they are never left at `usize::MAX`. Placement uses
|
||||||
|
command-level interference, not batch intervals, so placement quality is
|
||||||
|
unaffected; external consumers of `resource_lifetimes()` get coarser
|
||||||
|
intervals for interior commands (documented).
|
||||||
|
- **Self-edges:** intra-span hazard pairs are skipped during edge
|
||||||
|
construction (they become intra-span execution-order constraints handled by
|
||||||
|
`build_barriers`, not DAG edges). Note `dependencies()` only adds
|
||||||
|
prior→current edges (`graph.rs:825-861`), so the DAG is acyclic regardless;
|
||||||
|
only a literal self-edge would trip `Kahn`, and skipping intra-span pairs
|
||||||
|
prevents exactly that.
|
||||||
|
|
||||||
|
### D3 — Access declaration: one merged scope per attachment; interior accesses preserved
|
||||||
|
|
||||||
|
- `BeginRendering` declares **one access per attachment** with
|
||||||
|
`access = COLOR_ATTACHMENT_WRITE | (LOAD ? COLOR_ATTACHMENT_READ : 0)` at
|
||||||
|
the same stage (`COLOR_ATTACHMENT_OUTPUT` / `EARLY|LATE_FRAGMENT_TESTS`),
|
||||||
|
subresource range from the view (concrete range rules in D9). Declaring
|
||||||
|
read and write as two separate accesses is a blocker:
|
||||||
|
`SideEffectMap::insert` deliberately keeps opposite-polarity scopes separate
|
||||||
|
(`recorder.rs:55-82`), and `build_barriers` then emits **two**
|
||||||
|
`ImageMemoryBarrier2` entries for the same image/range/layout in one
|
||||||
|
`vkCmdPipelineBarrier2` (reproduced). A single merged scope is handled
|
||||||
|
correctly by `SideEffectMap`, `conflicts`, `derive_image_usage`
|
||||||
|
(`graph.rs:2696-2709`), and the barrier builder.
|
||||||
|
- `EndRendering`'s synthetic accesses (D2) are tagged so barrier generation
|
||||||
|
and usage derivation skip them.
|
||||||
|
- Interior command accesses are *not* replaced by the begin declarations;
|
||||||
|
they remain in the access list (see D2 barrier analysis) so the compiler
|
||||||
|
can emit barriers for outside producers consumed inside the pass.
|
||||||
|
- Because the span is contracted, any later consumer is ordered after the
|
||||||
|
*entire pass node*, i.e. after `EndRendering` — matching store-visibility
|
||||||
|
semantics (verified: the consumer's barrier takes
|
||||||
|
`COLOR_ATTACHMENT_OUTPUT`/`COLOR_ATTACHMENT_WRITE` as src, and hoisting a
|
||||||
|
barrier before `BeginRendering` with those dst stages is sufficient
|
||||||
|
visibility for LOAD reads and in-pass attachment reads).
|
||||||
|
- **Intra-span hazard policy (narrowed):**
|
||||||
|
- **Allowed inside a span:** any barrier with `old_layout == new_layout` —
|
||||||
|
memory barriers, buffer barriers, *and* image barriers with no layout
|
||||||
|
change (a storage-image read/write hazard at `GENERAL` needs an
|
||||||
|
`ImageMemoryBarrier2`; buffer barriers do not synchronize image
|
||||||
|
accesses). These are legal inside a rendering instance.
|
||||||
|
- **Rejected at compile time** (`GraphError::UnsupportedIntraPassDependency`):
|
||||||
|
intra-span hazards requiring an image layout transition (`old != new`).
|
||||||
|
The rejection operates on the **generated barrier plan** (D5), not on an
|
||||||
|
independent declared-layout scan.
|
||||||
|
- **Feedback loops:** defined as any interior access (read *or* write)
|
||||||
|
whose `(aspect, mip, layer)` set intersects a `Begin` attachment
|
||||||
|
subresource, excluding the begin's own load operation. A storage-image
|
||||||
|
write to an attachment subresource is equally a feedback loop; an
|
||||||
|
interior read of a disjoint mip/layer is legal. Genuine feedback loops
|
||||||
|
require splitting.
|
||||||
|
- In-pass barrier emission is part of Task 4, not a follow-up: the recorder
|
||||||
|
emits the barrier between the two interior commands in execution order.
|
||||||
|
- **Aliasing vs. interior first uses is a placement constraint, not a
|
||||||
|
validation message.** For image resources, add an interference edge (or an
|
||||||
|
explicit `aliasing_allowed` predicate consulted by `plan_placements` /
|
||||||
|
`first_fit_placement`) whenever a required alias transition would land at a
|
||||||
|
command that maps to a pass node — covering interior *and* boundary
|
||||||
|
(`Begin`/`End`) positions. There is no "reject aliasing" hook after
|
||||||
|
`plan_placements` commits offsets, so this must be enforced before
|
||||||
|
placement. Attachments themselves are safe: with D2's `End` modeling,
|
||||||
|
their first use is `Begin` and their terminal use is `End`, both boundary
|
||||||
|
commands of the pass node itself.
|
||||||
|
|
||||||
|
### D4 — Barrier placement: hoist cross-boundary barriers to BeginRendering, with per-subresource layout handling
|
||||||
|
|
||||||
|
Barriers whose `before_command` targets an interior command but whose origin
|
||||||
|
lies outside the span are re-keyed to the span's `BeginRendering` command,
|
||||||
|
after batching when spans are known. Constraints:
|
||||||
|
|
||||||
|
- **Barrier origin must be explicit.** `BarrierResource` (`graph.rs:84-95`)
|
||||||
|
records only masks/layout — no source command and no initial-vs-command
|
||||||
|
origin — so a post-pass hoisting pass cannot distinguish an outside-producer
|
||||||
|
barrier from an initial-state transition. Add
|
||||||
|
`src_command: Option<CommandIndex>` or a
|
||||||
|
`BarrierOrigin::{Initial, Command(CommandIndex)}` to `BarrierResource`.
|
||||||
|
**Initial-state transitions for interior first uses are hoisted to
|
||||||
|
`Begin`** and are not covered by the intra-span rejection (a transient
|
||||||
|
storage image first used inside a pass is the common case, not an edge
|
||||||
|
case).
|
||||||
|
- **Coalesce hoisted image transitions per `(resource, exact range,
|
||||||
|
target_layout)` into one barrier** with a unioned `dst_stage/dst_access`
|
||||||
|
(safe: the dst scope is a wait target; unioning only makes all consumers
|
||||||
|
wait). Layout chaining is defined **per `(aspect, mip, layer)`**, not per
|
||||||
|
barrier: for overlapping-but-unequal ranges, transition only the
|
||||||
|
subresources still in the producer layout; for disjoint ranges, emit
|
||||||
|
independent `producer → layout` transitions. The earlier "chain different
|
||||||
|
target layouts" rule is removed: for same-pass overlapping ranges it is
|
||||||
|
unreachable (D3 rejects the required in-pass transition), and for disjoint
|
||||||
|
ranges chaining `producer → A → B` is simply wrong (the second range is
|
||||||
|
still in the producer layout).
|
||||||
|
- The general `build_barriers` fix (per-subresource current-layout map, not
|
||||||
|
"last conflicting access's layout") makes non-hoisted multi-reader cases
|
||||||
|
correct too; D4's rules then only govern the hoisted subset.
|
||||||
|
- After hoisting, rebuild `barrier_indices_by_command` and
|
||||||
|
`ordinary_barrier_count` (`O(B)`), and apply the same coalescing in
|
||||||
|
`rebuild_barriers` (`graph.rs:2092-2106`) for alias barriers.
|
||||||
|
**Sequencing:** hoisting runs before `build_alias_barriers`, and hoisted
|
||||||
|
barriers live strictly within the ordinary prefix so `rebuild_barriers`'
|
||||||
|
truncation to `ordinary_barrier_count` (`graph.rs:2098`) cannot drop or
|
||||||
|
duplicate them; `rebuild_barriers` re-runs hoisting after the alias rebuild.
|
||||||
|
- Consumers after the pass keep their own barrier keys; expansion emits them
|
||||||
|
after `EndRendering` naturally. No barrier targets `EndRendering` itself
|
||||||
|
(its synthetic accesses are excluded from barrier generation).
|
||||||
|
- In-pass barriers (per D3) are emitted between the two interior commands in
|
||||||
|
execution order; they are not hoisted.
|
||||||
|
|
||||||
|
### D5 — Compile-time validation on the generated barrier plan, all linear
|
||||||
|
|
||||||
|
New `GraphError` variants for:
|
||||||
|
|
||||||
|
- unbalanced begin/end; nested/overlapping spans (dynamic rendering cannot
|
||||||
|
nest); `EndRendering` before its `BeginRendering` in record order;
|
||||||
|
- graphics commands outside any span (`GraphError::DrawOutsideRenderPass`),
|
||||||
|
and **transfer/clear commands inside a span** (`CopyBuffer`,
|
||||||
|
`ClearTexture`, and any custom command not marked render-pass-legal —
|
||||||
|
`vkCmdCopyBuffer2`/`vkCmdClearColorImage` are illegal inside a rendering
|
||||||
|
instance). Classification uses a `fn kind(&self) -> CommandKind` method on
|
||||||
|
**`RecordedCommand`** (not `Command` — a method on `Command` is
|
||||||
|
unreachable after `Box<dyn RecordedCommand>` erasure), returning
|
||||||
|
`CommandKind::{Begin, End, Graphics, Transfer, Compute, Other}` plus a
|
||||||
|
`fn allowed_in_render_pass(&self) -> bool` (default false; `Draw` → true;
|
||||||
|
test/custom no-op commands may override so Task 5's in-pass barrier tests
|
||||||
|
can exist). **Note:** five existing unit tests compile graphs containing
|
||||||
|
`Draw` with no pass (pushes at `graph.rs:3563, 3749, 3815, 4329, 4469`)
|
||||||
|
and must be updated (wrap in begin/end or make the restriction opt-in) —
|
||||||
|
added to Task 5.
|
||||||
|
- unsupported intra-span dependencies (per D3). **The check operates on the
|
||||||
|
generated barrier plan after hoisting/chaining, not on an independent
|
||||||
|
declared-layout scan:** after hoisting, any barrier whose `before_command`
|
||||||
|
is interior and whose `old_layout != new_layout` is either hoistable
|
||||||
|
(initial/outside origin) or must be rejected. This is exact, `O(B)`, and
|
||||||
|
removes the fragile per-subresource validation machinery whose
|
||||||
|
`previous`-selection (`graph.rs:951-982`) can diverge from declared
|
||||||
|
layouts (e.g. producer `P` at layout `P0`, outside reader `R1` transitions
|
||||||
|
to `A`, interior reader `R2` reads at `A`: a declared-layout scan sees
|
||||||
|
`A → A` and accepts, while the un-fixed builder would emit `P0 → A` at
|
||||||
|
`R2` inside the pass).
|
||||||
|
- attachment consistency: each attachment view must refer to the same image
|
||||||
|
as its graph `Texture` — compare **raw handles** (`Image: PartialEq`
|
||||||
|
compares raw handles, `images.rs:484-488`; `Arc::ptr_eq` would
|
||||||
|
false-positive), consistent with import dedup by raw handle
|
||||||
|
(`graph.rs:389-399`). Applies to the imported case at compile time and to
|
||||||
|
the created case after view creation (D9). The recorder binds graph images
|
||||||
|
by `ResourceId` (`graph.rs:2513-2529`); this check closes that gap.
|
||||||
|
- attachment layout legality: the caller-specified layout must be a legal
|
||||||
|
rendering layout (`COLOR_ATTACHMENT_OPTIMAL`, `GENERAL`, etc.). The
|
||||||
|
previously proposed "layout must match compiler-produced state" rule is
|
||||||
|
vacuous — the barrier's `new_layout` is derived from the begin's own
|
||||||
|
declared access, so no independent state exists.
|
||||||
|
- duplicate attachment subresources: two attachments covering the same
|
||||||
|
subresource would be silently merged by `SideEffectMap::insert`
|
||||||
|
(`recorder.rs:55-82`), making them invisible from `command_accesses`;
|
||||||
|
reject via the introspection hook (below), using a per-subresource set
|
||||||
|
(`O(covered subresources)`, not pairwise `O(a²)`).
|
||||||
|
- queue ownership: passes cannot cross queues or require ownership
|
||||||
|
transfers; the single-queue submission contract (`graph.rs:8-10`) applies
|
||||||
|
unchanged, and any access requiring a queue transfer inside or across a
|
||||||
|
span is rejected until paired transfer/barrier support exists.
|
||||||
|
- **Introspection hook:** commands are type-erased, and `CommandKind` carries
|
||||||
|
no data, so Task 2's attachment validations (view/image identity, duplicate
|
||||||
|
subresources, layout legality) need a `fn render_pass_desc(&self) ->
|
||||||
|
Option<&RenderPassDesc>` method on `RecordedCommand` (or a validation hook
|
||||||
|
called from `compile()`, or an attachment summary stored in
|
||||||
|
`CommandList::push`, which is generic over `C: Command`). Without one,
|
||||||
|
these checks are not implementable as written.
|
||||||
|
- **Error-path safety:** `record_commands` returning mid-pass must not leave
|
||||||
|
an unclosed rendering instance; track an `in_render_pass` flag in the
|
||||||
|
recorder/compiled graph, end the rendering instance (or discard the command
|
||||||
|
buffer) on the error path, and reset the flag (`graph.rs:2540-2553`).
|
||||||
|
|
||||||
|
### D6 — Execution order is precomputed at compile time
|
||||||
|
|
||||||
|
Compile an `execution_order: Vec<CommandIndex>` once: expanded contracted
|
||||||
|
batches — begin → interiors in **original record order** → end. Barrier
|
||||||
|
analysis and recording both iterate it; `batches` is retained (begin-index
|
||||||
|
representative) for future parallel recording. This avoids per-frame
|
||||||
|
expansion branches and keeps `recording_steps_cached` (`graph.rs:2595-2608`)
|
||||||
|
allocation-free. Interior commands are emitted in record order, not topo
|
||||||
|
order: ordering within the pass is the user's responsibility, matching the
|
||||||
|
"ordered by begin/end" contract.
|
||||||
|
|
||||||
|
**Batch-index assertion fix:** `build_barriers_with_initial_states` asserts
|
||||||
|
`batch_indices[previous] < batch_indices[current]` (`graph.rs:962-966`).
|
||||||
|
With interior commands mapped to the pass node's batch index, an allowed
|
||||||
|
intra-span hazard gives equal batch indices → debug panic. Use
|
||||||
|
**execution-order positions** for that assertion, not batch indices.
|
||||||
|
|
||||||
|
### D7 — Recording is allocation-free per frame
|
||||||
|
|
||||||
|
- `BeginRendering::apply` precomputes `vk::RenderingAttachmentInfo`s in the
|
||||||
|
command (or uses `ArrayVec`/thread-local scratch, mirroring
|
||||||
|
`pipeline_barrier`'s `BARRIER_SCRATCH` pattern, `recorder.rs:217-292`) so
|
||||||
|
no `Vec` is built per frame. Resolving attachment images per frame is
|
||||||
|
`O(a log R)` `BTreeMap` lookups — negligible.
|
||||||
|
- **View plumbing into `apply`:** `RecordedCommand::apply` receives only
|
||||||
|
`CommandRecorder` (`recorder.rs:108`), which cannot see `CompiledGraph`'s
|
||||||
|
view cache. Extend `CommandRecorder` with a view map
|
||||||
|
(`ResourceId → Arc<ImageView>`, or `ResourceId → Arc<Image>` plus
|
||||||
|
command-local view cache populated via `&mut self`) populated by
|
||||||
|
`record_commands`. `BeginRendering::apply` looks up the view; a missing or
|
||||||
|
unresolved view is a new `RecordError::MissingView`. Views are created
|
||||||
|
with `ManuallyDrop::into_inner(create_view(...))` — never store the
|
||||||
|
`ManuallyDrop` directly (leak).
|
||||||
|
- **Resolve idempotence:** views are created only after a fully successful
|
||||||
|
placement pass, atomically; a failed view creation must not leave a
|
||||||
|
partially-populated cache that later makes `Arc::get_mut`
|
||||||
|
(`graph.rs:2472-2480`) fail on a retried placement. Repeated `resolve()`
|
||||||
|
calls are idempotent; `create_view` failures map to `GraphError` variants.
|
||||||
|
|
||||||
|
### D8 — Frontier reuse for placement and alias barriers (performance amendment, corrected)
|
||||||
|
|
||||||
|
`ResourceInterference::from_resource_uses` already caches frontier sets per
|
||||||
|
resource (`graph.rs:1348-1356`); the plan does not claim to change it. The
|
||||||
|
actual recomputation hot spots contraction does not touch are
|
||||||
|
`first_fit_placement` (`graph.rs:1642-1649`), the `overlapping` loop in
|
||||||
|
`plan_placements` (`graph.rs:2384-2399`), and `build_alias_barriers`
|
||||||
|
(`terminal_uses` per old resource per new resource at `graph.rs:1109-1117`;
|
||||||
|
`initial_uses` per new at `graph.rs:1088`). Compute terminal/initial
|
||||||
|
frontiers **once per resource** and reuse them across all three sites, so
|
||||||
|
the performance rationale is honest: contraction removes the dependency-edge
|
||||||
|
and closure quadratics; frontier reuse removes the placement/alias ones.
|
||||||
|
|
||||||
|
### D9 — Attachment API for created textures
|
||||||
|
|
||||||
|
`RenderGraph::create_texture` returns only a `Texture` id (`graph.rs:352-354`);
|
||||||
|
the `Arc<Image>` is created privately in `plan_placements`/`resolve`
|
||||||
|
(`graph.rs:2129-2495`) and `CompiledGraph` exposes no accessor, so a caller
|
||||||
|
can never supply an `Arc<ImageView>` for a transient render target — the
|
||||||
|
main use case. Resolution:
|
||||||
|
|
||||||
|
- `ColorAttachment.view` is an enum:
|
||||||
|
`AttachmentView::Created(ImageViewDesc) | Imported(Arc<ImageView>)`.
|
||||||
|
- **`Created(ImageViewDesc)` must carry concrete aspect/mip/layer ranges**
|
||||||
|
(`REMAINING_*` sentinels are rejected at validation): `side_effects` runs
|
||||||
|
at `push` time (`recorder.rs:119-124`), before `resolve()` creates any
|
||||||
|
image, `image_view_subresource_range` is private (`images.rs:1183`), and
|
||||||
|
`create_view` normalizes/mutates the desc (`set_max_end`, format fill-in,
|
||||||
|
`images.rs:566-589`; `MipRange` addition panics on overflow in debug for
|
||||||
|
`base > 0` with a `REMAINING` count, `images.rs:1319-1333`). The declared
|
||||||
|
access range therefore comes from the desc, and is validated against the
|
||||||
|
image's mip/layer counts at compile time (from `GraphTextureDesc` /
|
||||||
|
imported `ImageDesc`).
|
||||||
|
- The crate creates and caches views after `resolve()` (`Image::create_view`
|
||||||
|
needs `&Arc<Image>`, `images.rs:558`), keyed by `(Texture, ImageViewDesc)`;
|
||||||
|
`BeginRendering::apply` consumes the cached view via the D7 recorder view
|
||||||
|
map.
|
||||||
|
- The D5 view/image consistency check applies to the imported case at
|
||||||
|
compile time and to the created case after view creation.
|
||||||
|
|
||||||
|
## Work items (all in `crates/renderer_rendergraph`)
|
||||||
|
|
||||||
|
### Task 1 — New commands in `src/commands.rs`
|
||||||
|
|
||||||
|
- `BeginRendering { color: Vec<ColorAttachment>, depth: Option<DepthStencilAttachment>, ... }`
|
||||||
|
with `ColorAttachment { texture: Texture, view: AttachmentView (D9),
|
||||||
|
layout: vk::ImageLayout, load_op, store_op, clear_value }` (depth/stencil
|
||||||
|
analogous). `EndRendering` (unit struct).
|
||||||
|
- `Command::side_effects`: `BeginRendering` inserts **one merged scope per
|
||||||
|
attachment** (write + conditional LOAD read, same stage, concrete range
|
||||||
|
from the desc/view — D3, D9). `EndRendering` inserts synthetic
|
||||||
|
lifetime-only accesses (D2), tagged to be skipped by barrier generation
|
||||||
|
and usage derivation.
|
||||||
|
- Add `fn kind(&self) -> CommandKind` and
|
||||||
|
`fn allowed_in_render_pass(&self) -> bool` and
|
||||||
|
`fn render_pass_desc(&self) -> Option<&RenderPassDesc>` to
|
||||||
|
`RecordedCommand` (D5): `Draw` → `Graphics`/allowed; begin/end →
|
||||||
|
`Begin`/`End`; `CopyBuffer`/`ClearTexture` → `Transfer`/not allowed;
|
||||||
|
default `Other`/not allowed.
|
||||||
|
- `RecordedCommand::apply`: `BeginRendering` builds `vk::RenderingInfo` +
|
||||||
|
precomputed `vk::RenderingAttachmentInfo`s (resolving `vk::Image` via
|
||||||
|
`recorder.image(...)`, using the cached/created view via the D7 view map)
|
||||||
|
and calls `cmd_begin_rendering`; `EndRendering` calls `cmd_end_rendering`.
|
||||||
|
Allocation-free per D7.
|
||||||
|
- Export both from `lib.rs`.
|
||||||
|
|
||||||
|
### Task 2 — Span tracking & validation in `src/graph.rs`
|
||||||
|
|
||||||
|
- Derive spans in `compile()` by scanning command record order using
|
||||||
|
`CommandKind`; build `node_of_command` and `spans`.
|
||||||
|
- Validation pass implementing the D5 rules, including the raw-handle
|
||||||
|
view/image consistency check (imported case), layout legality, duplicate
|
||||||
|
attachment subresources (via the introspection hook), and the
|
||||||
|
classification-based inside/outside-pass checks. Linear per D5.
|
||||||
|
- Update the five existing unit tests that compile `Draw` without a pass
|
||||||
|
(pushes at `graph.rs:3563, 3749, 3815, 4329, 4469`).
|
||||||
|
|
||||||
|
### Task 3 — Span contraction in the compiler
|
||||||
|
|
||||||
|
- Contract spans **before** `dependencies()` per D2: aggregate access scopes
|
||||||
|
per pass node, skip intra-span pairs during edge construction, begin-index
|
||||||
|
node representation, contracted `dependency_edges`; `HappensBefore` stores
|
||||||
|
the command→node map and translates in queries.
|
||||||
|
- Fix the pre-existing stale-`old_layout` defect in `build_barriers`
|
||||||
|
(`graph.rs:951-982`) with a per-subresource current-layout map (D4).
|
||||||
|
- Add `BarrierOrigin` to `BarrierResource` (D4).
|
||||||
|
- Barrier analysis runs over the uncontracted access list in execution order
|
||||||
|
with interior positions mapped to the pass node (D2); switch the
|
||||||
|
`batch_indices` debug assertion to execution-order positions (D6).
|
||||||
|
- `bind_lifetimes`, `HappensBefore::from_dag`, `uses_by_resource`, and
|
||||||
|
`build_alias_barriers` thread the span map through per D2; all closure
|
||||||
|
consumers route through the span-aware query; same-node uses are unordered
|
||||||
|
for aliasing/interference (D2 blocker fix).
|
||||||
|
- Precompute `execution_order` per D6.
|
||||||
|
- Frontier reuse per D8 (`first_fit_placement`, `plan_placements` overlapping
|
||||||
|
loop, `build_alias_barriers`).
|
||||||
|
|
||||||
|
### Task 4 — Barrier hoisting, in-pass barriers & recording
|
||||||
|
|
||||||
|
- Hoist cross-boundary barriers per D4, including initial-state transitions
|
||||||
|
for interior first uses (`BarrierOrigin::Initial`); coalesce per
|
||||||
|
`(resource, exact range, target_layout)` with unioned dst masks;
|
||||||
|
per-`(aspect, mip, layer)` layout handling for overlapping/disjoint ranges.
|
||||||
|
- Enforce the aliasing placement constraint per D3 (interference edge /
|
||||||
|
`aliasing_allowed` predicate) before placement commits offsets.
|
||||||
|
- Emit in-pass barriers (any `old_layout == new_layout`) between interior
|
||||||
|
commands in execution order for allowed intra-span hazards (D3).
|
||||||
|
- Hoisting sequenced before `build_alias_barriers`, hoisted barriers within
|
||||||
|
the ordinary prefix, `rebuild_barriers` re-runs hoisting (D4).
|
||||||
|
- `recording_steps_cached` iterates `execution_order`, expanding pass nodes:
|
||||||
|
barriers (hoisted) → `BeginRendering` → interior commands in record order
|
||||||
|
(with in-pass barriers interleaved) → `EndRendering`.
|
||||||
|
- `record_commands`: expanded steps, D7 view map population, and the D5
|
||||||
|
error-path safety (`in_render_pass` flag, no unclosed rendering instance
|
||||||
|
on mid-pass `RecordError`).
|
||||||
|
|
||||||
|
### Task 5 — Tests
|
||||||
|
|
||||||
|
- Unit tests in `graph.rs`:
|
||||||
|
- span validation errors (unbalanced, nested, draw-outside-pass,
|
||||||
|
transfer-inside-pass, view/image mismatch, layout legality, duplicate
|
||||||
|
attachment subresources, feedback-loop rejection);
|
||||||
|
- contraction correctness (foreign command never appears between begin and
|
||||||
|
end in `execution_order`; interior accesses still produce barriers;
|
||||||
|
lifetimes resolved for interior commands);
|
||||||
|
- barrier hoisting (barrier for an outside producer consumed by an interior
|
||||||
|
command is keyed to begin; **two interior consumers of one
|
||||||
|
outside-produced image coalesce into one barrier with unioned dst masks
|
||||||
|
and correct per-subresource `old_layout`** — regression test for the
|
||||||
|
reproduced blocker; also cover the non-hoisted two-reader case fixed by
|
||||||
|
the general `build_barriers` change);
|
||||||
|
- initial-state hoisting (transient storage image first used inside a pass
|
||||||
|
gets its `UNDEFINED → GENERAL` transition before `BeginRendering`);
|
||||||
|
- in-pass barrier emission (interior storage-buffer *and* storage-image
|
||||||
|
same-layout hazards produce barriers between the interior commands, no
|
||||||
|
pass split; layout-transition hazard is rejected);
|
||||||
|
- **aliasing safety (regression test for the rev-4 blocker):** created
|
||||||
|
attachment `A` + interior-only storage image `B` must **not** share
|
||||||
|
memory, and no barrier may target an interior command for `A`/`B`;
|
||||||
|
attachment `terminal_uses == End`;
|
||||||
|
- ordering of both incoming and outgoing dependencies around the whole
|
||||||
|
span;
|
||||||
|
- span-aware closure queries (`happens_before` with interior endpoints;
|
||||||
|
same-node uses unordered for aliasing).
|
||||||
|
- Integration tests in `tests/graph_validation.rs` (following the existing
|
||||||
|
`TestVulkan` pattern):
|
||||||
|
- skip when `core13.dynamic_rendering != TRUE` in addition to the existing
|
||||||
|
`synchronization2` check (`tests/graph_validation.rs:108-111`);
|
||||||
|
- the previously proposed `Draw`-based graph is **not constructible**
|
||||||
|
(`CopyBuffer` accepts only buffers, `commands.rs:30-79`; `Draw` has no
|
||||||
|
pipeline handle and never binds one, `commands.rs:143-153, 226-335`).
|
||||||
|
Use a custom no-op command (like `ScopedNoop`) with a texture read
|
||||||
|
access as the post-`End` consumer:
|
||||||
|
`ClearTexture → BeginRendering(color=target) → allowed no-op (texture
|
||||||
|
read) → EndRendering → no-op (texture read)` compiles, records,
|
||||||
|
submits, and validates. Adding a real `BindPipeline` command (with a
|
||||||
|
documented sequence edge to the following `Draw`, since pipeline binding
|
||||||
|
declares no resources) is an optional separate work item;
|
||||||
|
- a case proving a hazard-free foreign command is emitted *after*
|
||||||
|
`EndRendering`, not inside the pass;
|
||||||
|
- an outside-producer → interior-consumer case asserting the hoisted
|
||||||
|
barrier appears before `BeginRendering`.
|
||||||
|
|
||||||
|
### Task 6 — Existing test/API churn
|
||||||
|
|
||||||
|
- `batches()` assertions and helpers that assume command-space batches:
|
||||||
|
`graph.rs:3774, 4285, 4388-4393, 4674, 5237, 5245, 5257, 5337, 5343, 5397,
|
||||||
|
5409, 5414` (including `recording_steps(compiled.batches(), …)` at 5343).
|
||||||
|
- `GraphCompiler::new` / `build_barriers` signature changes (execution order +
|
||||||
|
span map) break direct unit-test call sites at `graph.rs:3177-3178, 4125,
|
||||||
|
4242, 4716-4718, 4792-4800, 4846-4854, 4893-4901, 4962-4976, 5051-5052,
|
||||||
|
5102-5104, 5142-5145, 5171-5173`; `HappensBefore::from_dag` is called
|
||||||
|
directly by a test (`graph.rs:4140-4155`) and its signature changes.
|
||||||
|
- Wrapping a `Draw` in a pass shifts barrier indices in tests such as
|
||||||
|
`sampled_image_import_sets_usage_and_layout_barrier` (asserts
|
||||||
|
`compiled.barriers()[0]`, `graph.rs:3837-3845`).
|
||||||
|
- Provide compatibility constructors where cheap, or update all listed call
|
||||||
|
sites; `cargo test -p renderer_rendergraph` must pass at the end.
|
||||||
|
|
||||||
|
## Sequencing
|
||||||
|
|
||||||
|
Tasks 1–2 and 3–4 share types, so execute strictly in order: **1 → 2 → 3 →
|
||||||
|
4 → 5 → 6**, with `cargo check -p renderer_rendergraph` after each and
|
||||||
|
`cargo test -p renderer_rendergraph` at the end. No changes to other crates;
|
||||||
|
`renderer`'s legacy `render_graph` module is out of scope.
|
||||||
|
|
||||||
|
## Performance summary (from rev-2 review, corrected in revs 3–5)
|
||||||
|
|
||||||
|
- **Contraction before `dependencies()`** removes the interior-draw
|
||||||
|
`O(Σ u_r²)` edge construction and shrinks the `O(N²/64)` closure
|
||||||
|
memory/time by roughly `(N_c/N)²` (a pass spanning 90% of commands →
|
||||||
|
~100× less closure work).
|
||||||
|
- **Contraction does not touch** the placement/alias frontiers; D8's
|
||||||
|
frontier reuse addresses `first_fit_placement`, `plan_placements`'
|
||||||
|
overlapping loop, and `build_alias_barriers` (interference frontier
|
||||||
|
caching already exists).
|
||||||
|
- **Barrier hoisting is cost-neutral to slightly positive** provided
|
||||||
|
coalescing follows D4 (per-target-layout, per-subresource handling,
|
||||||
|
unioned dst masks only for identical ranges/layouts).
|
||||||
|
- **In-pass barriers for same-layout hazards** avoid the significant
|
||||||
|
tiled-GPU regression of forced pass splits (attachment store/load
|
||||||
|
round-trips); only layout transitions and feedback loops force splits.
|
||||||
|
- **Validation is linear** with the barrier-plan-based intra-span check
|
||||||
|
(`O(B)`) and per-subresource-set duplicate detection; the existing
|
||||||
|
layout-conflict precheck grows by `O(a²)` pair iterations per pass
|
||||||
|
(28 for 8 attachments with the merged single access) — negligible.
|
||||||
|
- **Recording stays allocation-free** via precomputed attachment infos,
|
||||||
|
thread-local scratch, precomputed `execution_order`, and begin-index node
|
||||||
|
representation.
|
||||||
|
|
||||||
|
## Risks / open points
|
||||||
|
|
||||||
|
- **API shape:** raw `push(BeginRendering)` / `push(EndRendering)` vs. a
|
||||||
|
scoped `render_pass(desc, |graph| ...)` builder that pushes begin, runs the
|
||||||
|
closure, and pushes end while recording span command indices. The scoped
|
||||||
|
builder makes unbalanced spans unrepresentable; raw push is more flexible.
|
||||||
|
Decide before Task 1.
|
||||||
|
- **Created-texture views (D9):** view creation after `resolve()` adds a
|
||||||
|
cache keyed by `(Texture, ImageViewDesc)`; `ImageView` holds `Arc<Image>`
|
||||||
|
so lifetime is safe; resolve idempotence and atomic creation per D7.
|
||||||
|
- **Public API semantics:** `batches()` / `dependency_edges()` keep
|
||||||
|
command-space indices with the begin index as pass-node representative;
|
||||||
|
document the semantics and consider adding `execution_order()` as the
|
||||||
|
primary accessor.
|
||||||
|
- **`BindPipeline`:** out of scope for this plan; without it, submitted
|
||||||
|
tests cannot include real draws. If added later, it needs a sequence edge
|
||||||
|
to the following `Draw` (pipeline binding declares no resources, so the
|
||||||
|
hazard graph will not order it).
|
||||||
Loading…
Reference in a new issue