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]
|
||||
bevy_vulkan_render = { path = "crates/bevy_vulkan_render" }
|
||||
bitflags.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
bevy = { workspace = true }
|
||||
|
|
@ -28,6 +29,7 @@ debug = true
|
|||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.89"
|
||||
criterion = "0.7"
|
||||
thiserror = "2.0"
|
||||
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"
|
||||
|
||||
arrayvec = "0.7.6"
|
||||
tinyvec = "1.8"
|
||||
tinyvec = { version = "1.8", features = ["alloc"] }
|
||||
indexmap = "2"
|
||||
petgraph = "0.7"
|
||||
fixedbitset = "0.5"
|
||||
|
|
|
|||
|
|
@ -571,7 +571,7 @@ pub fn egui_pass(
|
|||
.values()
|
||||
.map(|entry| {
|
||||
let texture = texture_handler.get_texture(entry.id).unwrap();
|
||||
let view = Arc::new(ManuallyDrop::into_inner(
|
||||
let view = Arc::new(
|
||||
texture
|
||||
.create_view(
|
||||
ImageViewDesc::color_2d()
|
||||
|
|
@ -579,7 +579,7 @@ pub fn egui_pass(
|
|||
.with_layer_range(0..1),
|
||||
)
|
||||
.unwrap(),
|
||||
));
|
||||
);
|
||||
let sampler = samplers.get_sampler_owned(entry.as_sampler_desc()).unwrap();
|
||||
(view, sampler)
|
||||
})
|
||||
|
|
@ -770,7 +770,7 @@ pub fn egui_pass(
|
|||
|
||||
let color_attachment = &vk::RenderingAttachmentInfo::default()
|
||||
.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)
|
||||
.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};
|
||||
|
||||
|
|
@ -329,7 +329,9 @@ impl Wireframe {
|
|||
|
||||
let color_attachment = &vk::RenderingAttachmentInfo::default()
|
||||
.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)
|
||||
.store_op(vk::AttachmentStoreOp::STORE);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use std::{
|
||||
mem::ManuallyDrop,
|
||||
num::NonZero,
|
||||
ops::Deref,
|
||||
sync::{
|
||||
|
|
@ -589,7 +588,7 @@ impl Swapchain {
|
|||
SwapchainImage {
|
||||
index: idx as u32,
|
||||
swapchain: self,
|
||||
view: ManuallyDrop::into_inner(view),
|
||||
view,
|
||||
acquire,
|
||||
release,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,10 +6,17 @@ edition = "2024"
|
|||
[dependencies]
|
||||
ash.workspace = true
|
||||
fixedbitset.workspace = true
|
||||
tinyvec.workspace = true
|
||||
renderer_device = { path = "../renderer_device" }
|
||||
renderer_resources = { path = "../renderer_resources" }
|
||||
ash-formats = { git = "https://github.com/janis-bhm/ash-formats", branch = "main" }
|
||||
bitflags.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { workspace = true }
|
||||
renderer_resources = { path = "../renderer_resources", 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 renderer_resources::images::{ImageView, ImageViewDesc};
|
||||
|
||||
use crate::{
|
||||
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
|
||||
/// itself to a Vulkan command buffer after resource binding.
|
||||
pub trait Command: RecordedCommand {
|
||||
|
|
@ -59,6 +299,10 @@ impl Command for CopyBuffer {
|
|||
}
|
||||
|
||||
impl RecordedCommand for CopyBuffer {
|
||||
fn kind(&self) -> CommandKind {
|
||||
CommandKind::Transfer
|
||||
}
|
||||
|
||||
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
|
||||
let region = vk::BufferCopy2::default()
|
||||
.src_offset(self.src_range.offset)
|
||||
|
|
@ -104,6 +348,10 @@ impl Command for ClearTexture {
|
|||
}
|
||||
|
||||
impl RecordedCommand for ClearTexture {
|
||||
fn kind(&self) -> CommandKind {
|
||||
CommandKind::Transfer
|
||||
}
|
||||
|
||||
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
|
||||
let value = vk::ClearColorValue {
|
||||
float32: self.color,
|
||||
|
|
@ -219,6 +467,14 @@ impl Command 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> {
|
||||
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(
|
||||
sets: &[GraphDescriptorSet],
|
||||
) -> 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 graph;
|
||||
mod graph_compiler;
|
||||
pub mod recorder;
|
||||
pub mod resources;
|
||||
|
||||
pub use commands::{
|
||||
AttachmentView, BeginRendering, ColorAttachment, CommandKind, DepthStencilAttachment,
|
||||
EndRendering, RenderPassDesc,
|
||||
};
|
||||
pub use graph::{
|
||||
CompiledGraph, GraphError, HappensBefore, MemoryBarrier, Placement, RenderGraph,
|
||||
ResourceInterference, ResourceLifetime, ResourceUse, aliasing_permitted,
|
||||
BarrierOrigin, CompiledGraph, GraphError, HappensBefore, MemoryBarrier, Placement, RenderGraph,
|
||||
RenderPassSpan, ResourceFrontiers, ResourceInterference, ResourceLifetime, ResourceUse,
|
||||
aliasing_permitted,
|
||||
};
|
||||
pub use recorder::{CommandIndex, CommandList, CommandRecorder, SideEffectMap};
|
||||
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 renderer_resources::images::{ImageView, ImageViewDesc};
|
||||
|
||||
use crate::resources::{
|
||||
BufferRange, ResourceId, Subresource, SubresourceScope, SubresourceScopeInner,
|
||||
|
|
@ -38,6 +43,7 @@ pub struct ResourceAccess {
|
|||
#[derive(Debug, Default, Clone)]
|
||||
pub struct SideEffectMap {
|
||||
accesses: Vec<Vec<ResourceAccess>>,
|
||||
lifetime_only: BTreeSet<(usize, ResourceId)>,
|
||||
}
|
||||
|
||||
impl SideEffectMap {
|
||||
|
|
@ -81,6 +87,23 @@ impl SideEffectMap {
|
|||
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] {
|
||||
self.accesses
|
||||
.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
|
||||
/// compilation/recording, while its access declarations are stored separately.
|
||||
pub struct CommandList {
|
||||
pub(crate) commands: Vec<Box<dyn RecordedCommand>>,
|
||||
pub(crate) commands: Vec<BoxedCommand>,
|
||||
pub side_effects: SideEffectMap,
|
||||
}
|
||||
|
||||
|
|
@ -105,6 +130,18 @@ pub trait RecordedCommand {
|
|||
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>;
|
||||
}
|
||||
|
||||
|
|
@ -149,6 +186,7 @@ impl Default for CommandList {
|
|||
pub enum RecordError {
|
||||
MissingBuffer(ResourceId),
|
||||
MissingImage(ResourceId),
|
||||
MissingView(ResourceId),
|
||||
UnresolvedCreatedBuffer(ResourceId),
|
||||
UnresolvedCreatedImage(ResourceId),
|
||||
DescriptorSet(String),
|
||||
|
|
@ -161,6 +199,7 @@ pub struct CommandRecorder<'a> {
|
|||
pub command_buffer: vk::CommandBuffer,
|
||||
pub buffers: BTreeMap<ResourceId, vk::Buffer>,
|
||||
pub images: BTreeMap<ResourceId, vk::Image>,
|
||||
pub views: HashMap<(ResourceId, ImageViewDesc), Arc<ImageView>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
|
@ -181,6 +220,7 @@ impl<'a> CommandRecorder<'a> {
|
|||
command_buffer,
|
||||
buffers: BTreeMap::new(),
|
||||
images: BTreeMap::new(),
|
||||
views: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -198,6 +238,18 @@ impl<'a> CommandRecorder<'a> {
|
|||
.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) {
|
||||
BARRIER_SCRATCH.with(|scratch| {
|
||||
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 {
|
||||
match scope.subresource {
|
||||
SubresourceScopeInner::Buffer(range) => Subresource::Buffer(range),
|
||||
|
|
@ -337,7 +402,7 @@ mod tests {
|
|||
dst_stage: vk::PipelineStageFlags2::FRAGMENT_SHADER,
|
||||
dst_access: vk::AccessFlags2::SHADER_READ,
|
||||
}],
|
||||
resources: Vec::new(),
|
||||
resources: tinyvec::TinyVec::new(),
|
||||
};
|
||||
MEMORY_BARRIER_COUNT.store(0, Ordering::SeqCst);
|
||||
MEMORY_BARRIER_SRC_STAGE.store(0, Ordering::SeqCst);
|
||||
|
|
|
|||
|
|
@ -6,16 +6,19 @@ use renderer_device::{
|
|||
testing::TestVulkan,
|
||||
};
|
||||
use renderer_rendergraph::{
|
||||
RenderGraph,
|
||||
commands::Command,
|
||||
BeginRendering, ColorAttachment, EndRendering, RenderGraph,
|
||||
commands::CopyBuffer,
|
||||
commands::{ClearTexture, Command},
|
||||
recorder::{CommandIndex, CommandRecorder, RecordError, RecordedCommand, SideEffectMap},
|
||||
resources::{
|
||||
BufferRange, GraphBufferDesc, GraphTextureDesc, ResourceId, ResourceState,
|
||||
SubresourceScope, SubresourceScopeInner,
|
||||
},
|
||||
};
|
||||
use renderer_resources::buffers::{Buffer, BufferDesc};
|
||||
use renderer_resources::{
|
||||
buffers::{Buffer, BufferDesc},
|
||||
images::ImageViewDesc,
|
||||
};
|
||||
|
||||
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 {
|
||||
SubresourceScope {
|
||||
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) {
|
||||
let device = test.device();
|
||||
let pool_info = vk::CommandPoolCreateInfo::default()
|
||||
|
|
@ -179,7 +243,7 @@ fn imported_buffer_copy_records_under_validation() {
|
|||
}
|
||||
|
||||
#[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 {
|
||||
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.offset, second_placement.offset);
|
||||
assert!(first_placement.aliased && second_placement.aliased);
|
||||
assert!(compiled.barriers().iter().any(|barrier| {
|
||||
barrier.before_command == CommandIndex(1) && !barrier.memory_barriers.is_empty()
|
||||
}));
|
||||
let alias_barrier = compiled
|
||||
.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);
|
||||
test.assert_no_validation_errors();
|
||||
|
|
@ -328,3 +404,149 @@ fn resolved_images_alias_and_submit_with_undefined_transition() {
|
|||
.expect("wait before shared allocation teardown");
|
||||
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::{
|
||||
borrow::Cow,
|
||||
mem::ManuallyDrop,
|
||||
ops::{Add, Sub},
|
||||
sync::Arc,
|
||||
};
|
||||
|
|
@ -555,10 +554,7 @@ impl Image {
|
|||
// }
|
||||
// }
|
||||
|
||||
pub fn create_view(
|
||||
self: &Arc<Self>,
|
||||
mut desc: ImageViewDesc,
|
||||
) -> crate::Result<ManuallyDrop<ImageView>> {
|
||||
pub fn create_view(self: &Arc<Self>, mut desc: ImageViewDesc) -> crate::Result<ImageView> {
|
||||
// validate
|
||||
if !view_kind_compatible(self.desc.kind, desc.kind) {
|
||||
tracing::error!(
|
||||
|
|
@ -618,7 +614,7 @@ impl Image {
|
|||
let device = self.image.device();
|
||||
let view = unsafe { device.raw.create_image_view(&create_info, None)? };
|
||||
|
||||
Ok(ManuallyDrop::new(ImageView {
|
||||
Ok(ImageView {
|
||||
view: ImageViewHandle::Owned(DeviceObject::new_debug_named(
|
||||
device.clone(),
|
||||
view,
|
||||
|
|
@ -626,7 +622,7 @@ impl Image {
|
|||
)),
|
||||
desc,
|
||||
image: self.clone(),
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1189,26 +1185,6 @@ fn image_view_subresource_range(desc: &ImageViewDesc) -> vk::ImageSubresourceRan
|
|||
.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 src: u32,
|
||||
pub dst: u32,
|
||||
|
|
@ -1336,3 +1312,23 @@ impl ImageSubresourceExt for vk::ImageSubresource {
|
|||
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