response to llm review

This commit is contained in:
janis 2026-10-25 02:27:28 +01:00
parent c0f60b890c
commit f84b2a1ea4
Signed by: janis
SSH key fingerprint: SHA256:bB1qbbqmDXZNT0KKD5c2Dfjg53JGhj7B3CFcLIzSqq8

View file

@ -156,14 +156,15 @@ impl Kahn {
} }
} }
fn pop_ready(&mut self) -> Option<usize> { fn pop_batch(&mut self) -> Vec<usize> {
let mut batch = Vec::new();
while let Some(Reverse(node)) = self.ready.pop() { while let Some(Reverse(node)) = self.ready.pop() {
if self.is_ready[node] { if self.is_ready[node] {
self.is_ready[node] = false; self.is_ready[node] = false;
return Some(node); batch.push(node);
} }
} }
None batch
} }
fn complete(&mut self, node: usize) { fn complete(&mut self, node: usize) {
@ -242,16 +243,18 @@ impl RenderGraph {
let command_count = self.commands.len(); let command_count = self.commands.len();
let accesses = self.commands.side_effects.command_accesses(); let accesses = self.commands.side_effects.command_accesses();
let compiler = GraphCompiler::new(&self.resources, accesses, command_count); let compiler = GraphCompiler::new(&self.resources, accesses, command_count);
let order = compiler.topo_sort()?; let batches = compiler.topo_batches()?;
compiler.validate_resources()?; compiler.validate_resources()?;
let barriers = compiler.build_barriers(&order); let flat_order = batches.iter().flatten().copied().collect::<Vec<_>>();
let barriers = compiler.build_barriers(&flat_order, &batches);
let (lifetimes, interference) = compiler.bind_lifetimes(); let (lifetimes, interference) = compiler.bind_lifetimes();
Ok(CompiledGraph { Ok(CompiledGraph {
resources: self.resources, resources: self.resources,
resolved: HashMap::new(), resolved: HashMap::new(),
commands: self.commands, commands: self.commands,
order, batches,
flat_order,
barriers, barriers,
lifetimes, lifetimes,
interference, interference,
@ -296,17 +299,22 @@ impl<'a> GraphCompiler<'a> {
kahn kahn
} }
fn topo_sort(&self) -> Result<Vec<CommandIndex>, GraphError> { fn topo_batches(&self) -> Result<Vec<Vec<CommandIndex>>, GraphError> {
let mut kahn = self.dependencies(); let mut kahn = self.dependencies();
let mut order = Vec::with_capacity(self.command_count); let mut batches = Vec::new();
while let Some(index) = kahn.pop_ready() { while !kahn.is_complete() {
order.push(index.into()); let batch = kahn.pop_batch();
kahn.complete(index); if batch.is_empty() {
return Err(GraphError::DependencyCycle);
}
let batch = batch
.into_iter()
.inspect(|&index| kahn.complete(index))
.map(CommandIndex::from)
.collect();
batches.push(batch);
} }
if !kahn.is_complete() { Ok(batches)
return Err(GraphError::DependencyCycle);
}
Ok(order)
} }
fn validate_resources(&self) -> Result<(), GraphError> { fn validate_resources(&self) -> Result<(), GraphError> {
@ -320,7 +328,17 @@ impl<'a> GraphCompiler<'a> {
Ok(()) Ok(())
} }
fn build_barriers(&self, order: &[CommandIndex]) -> Vec<PipelineBarrier> { fn build_barriers(
&self,
order: &[CommandIndex],
batches: &[Vec<CommandIndex>],
) -> Vec<PipelineBarrier> {
let mut batch_indices = vec![usize::MAX; self.command_count];
for (batch_index, batch) in batches.iter().enumerate() {
for command in batch {
batch_indices[command.index()] = batch_index;
}
}
let mut result = Vec::new(); let mut result = Vec::new();
for (ordered_index, &command_index) in order.iter().enumerate() { for (ordered_index, &command_index) in order.iter().enumerate() {
let mut barrier_resources = Vec::new(); let mut barrier_resources = Vec::new();
@ -328,13 +346,25 @@ impl<'a> GraphCompiler<'a> {
let previous = order[..ordered_index] let previous = order[..ordered_index]
.iter() .iter()
.rev() .rev()
.flat_map(|&prior_index| self.accesses[prior_index.index()].iter().rev()) .find_map(|&prior_index| {
.find(|previous| { self.accesses[prior_index.index()]
previous.resource == current.resource .iter()
&& previous.scope.conflicts(&current.scope) .rev()
.find(|previous| {
previous.resource == current.resource
&& previous.scope.conflicts(&current.scope)
})
.map(|access| (prior_index, access))
}); });
if let Some(previous) = previous {
if let Some((previous_command, previous)) = previous {
debug_assert!(
batch_indices[previous_command.index()]
< batch_indices[command_index.index()]
);
let (old_layout, new_layout) = Self::layouts(previous.scope, current.scope); let (old_layout, new_layout) = Self::layouts(previous.scope, current.scope);
barrier_resources.push(BarrierResource { barrier_resources.push(BarrierResource {
id: current.resource, id: current.resource,
range: scope_range(current.scope), range: scope_range(current.scope),
@ -345,19 +375,24 @@ impl<'a> GraphCompiler<'a> {
old_layout, old_layout,
new_layout, new_layout,
}); });
continue; continue;
} }
let desc = &self.resources[current.resource.0 as usize]; let desc = &self.resources[current.resource.0 as usize];
let initial = desc.initial_state(); let initial = desc.initial_state();
let (old_layout, new_layout) = Self::layouts_from_state(initial, current.scope); let (old_layout, new_layout) = Self::layouts_from_state(initial, current.scope);
let layout_transition = old_layout != new_layout && new_layout.is_some(); let layout_transition = old_layout != new_layout && new_layout.is_some();
let initial_hazard = matches!( let initial_hazard = matches!(
desc, desc,
ResourceDesc::ImportedBuffer { .. } | ResourceDesc::ImportedTexture { .. } ResourceDesc::ImportedBuffer { .. } | ResourceDesc::ImportedTexture { .. }
) && !initial.access.is_empty() ) && !initial.access.is_empty()
&& (access_flag_is_write(initial.access) && (access_flag_is_write(initial.access)
|| access_flag_is_write(current.scope.access)); || access_flag_is_write(current.scope.access));
if layout_transition || initial_hazard { if layout_transition || initial_hazard {
barrier_resources.push(BarrierResource { barrier_resources.push(BarrierResource {
id: current.resource, id: current.resource,
@ -371,13 +406,15 @@ impl<'a> GraphCompiler<'a> {
}); });
} }
} }
if !barrier_resources.is_empty() { if !barrier_resources.is_empty() {
result.push(PipelineBarrier { result.push(PipelineBarrier {
before_command: ordered_index.into(), before_command: command_index,
resources: barrier_resources, resources: barrier_resources,
}); });
} }
} }
result result
} }
@ -389,6 +426,7 @@ impl<'a> GraphCompiler<'a> {
SubresourceScopeInner::Texture { layout, .. } => Some(layout), SubresourceScopeInner::Texture { layout, .. } => Some(layout),
SubresourceScopeInner::Buffer(_) => None, SubresourceScopeInner::Buffer(_) => None,
}; };
(layout(previous), layout(current)) (layout(previous), layout(current))
} }
@ -400,6 +438,7 @@ impl<'a> GraphCompiler<'a> {
SubresourceScopeInner::Texture { layout, .. } => Some(layout), SubresourceScopeInner::Texture { layout, .. } => Some(layout),
SubresourceScopeInner::Buffer(_) => None, SubresourceScopeInner::Buffer(_) => None,
}; };
(initial.layout, current_layout) (initial.layout, current_layout)
} }
@ -475,7 +514,8 @@ pub struct CompiledGraph {
resources: Vec<ResourceDesc>, resources: Vec<ResourceDesc>,
resolved: HashMap<ResourceId, ResolvedResource>, resolved: HashMap<ResourceId, ResolvedResource>,
commands: CommandList, commands: CommandList,
order: Vec<CommandIndex>, batches: Vec<Vec<CommandIndex>>,
flat_order: Vec<CommandIndex>,
barriers: Vec<PipelineBarrier>, barriers: Vec<PipelineBarrier>,
lifetimes: HashMap<ResourceId, ResourceLifetime>, lifetimes: HashMap<ResourceId, ResourceLifetime>,
interference: ResourceInterference, interference: ResourceInterference,
@ -487,8 +527,12 @@ enum ResolvedResource {
} }
impl CompiledGraph { impl CompiledGraph {
pub fn batches(&self) -> &[Vec<CommandIndex>] {
&self.batches
}
pub fn command_order(&self) -> &[CommandIndex] { pub fn command_order(&self) -> &[CommandIndex] {
&self.order &self.flat_order
} }
pub fn barriers(&self) -> &[PipelineBarrier] { pub fn barriers(&self) -> &[PipelineBarrier] {
@ -600,20 +644,39 @@ impl CompiledGraph {
} }
} }
let mut barriers = self.barriers.iter().peekable(); // TODO: `batches` are currently recorded sequentially into one command buffer;
for (ordered_index, &command_index) in self.order.iter().enumerate() { // record per-batch command buffers and submit sequentially/in parallel, using split barriers/events or timeline semaphores for overlap.
while barriers for step in recording_steps(&self.batches, &self.barriers) {
.peek() match step {
.is_some_and(|barrier| barrier.before_command == ordered_index.into()) // TODO: Merge `PipelineBarrier.resources` across commands or a batch to reduce vkCmdPipelineBarrier2 calls.
{ RecordingStep::Barrier(barrier) => recorder.pipeline_barrier(barrier),
recorder.pipeline_barrier(barriers.next().expect("peeked barrier")); RecordingStep::Command(command_index) => {
self.commands.commands[command_index.index()].apply(recorder)?;
}
} }
self.commands.commands[command_index.index()].apply(recorder)?;
} }
Ok(()) Ok(())
} }
} }
enum RecordingStep<'a> {
Barrier(&'a PipelineBarrier),
Command(CommandIndex),
}
fn recording_steps<'a>(
batches: &'a [Vec<CommandIndex>],
barriers: &'a [PipelineBarrier],
) -> impl Iterator<Item = RecordingStep<'a>> + 'a {
batches.iter().flatten().flat_map(move |&command_index| {
barriers
.iter()
.filter(move |barrier| barrier.before_command == command_index)
.map(RecordingStep::Barrier)
.chain(std::iter::once(RecordingStep::Command(command_index)))
})
}
/// The skeleton switches to a dedicated allocation at 64 MiB. This is a /// The skeleton switches to a dedicated allocation at 64 MiB. This is a
/// placeholder heuristic until resource lifetime/aliasing allocation is real. /// placeholder heuristic until resource lifetime/aliasing allocation is real.
const DEDICATED_ALLOCATION_THRESHOLD: u64 = 64 * 1024 * 1024; const DEDICATED_ALLOCATION_THRESHOLD: u64 = 64 * 1024 * 1024;
@ -850,13 +913,17 @@ mod tests {
kahn.add_edge(1, 2); kahn.add_edge(1, 2);
kahn.add_edge(2, 3); kahn.add_edge(2, 3);
let mut order = Vec::new(); let mut batches = Vec::new();
while let Some(node) = kahn.pop_ready() { while !kahn.is_complete() {
order.push(node); let batch = kahn.pop_batch();
kahn.complete(node); assert!(!batch.is_empty());
for &node in &batch {
kahn.complete(node);
}
batches.push(batch);
} }
assert_eq!(order, [0, 1, 2, 3]); assert_eq!(batches, [vec![0, 1], vec![2], vec![3]]);
assert!(kahn.is_complete()); assert!(kahn.is_complete());
} }
@ -867,7 +934,7 @@ mod tests {
kahn.add_edge(1, 2); kahn.add_edge(1, 2);
kahn.add_edge(2, 1); kahn.add_edge(2, 1);
while let Some(node) = kahn.pop_ready() { for node in kahn.pop_batch() {
kahn.complete(node); kahn.complete(node);
} }
@ -1032,6 +1099,227 @@ mod tests {
assert_eq!(compiled.barriers()[0].resources[0].id, b.id()); assert_eq!(compiled.barriers()[0].resources[0].id, b.id());
} }
#[test]
fn independent_commands_share_a_batch_and_dependencies_advance_to_later_batches() {
let mut graph = RenderGraph::new();
let a = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let shared = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let b = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let independent_src = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let independent_dst = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
graph.push(CopyBuffer {
src: a,
src_range: BufferRange { offset: 0, size: 8 },
dst: shared,
dst_range: BufferRange { offset: 0, size: 8 },
});
graph.push(CopyBuffer {
src: shared,
src_range: BufferRange { offset: 0, size: 8 },
dst: b,
dst_range: BufferRange { offset: 0, size: 8 },
});
graph.push(CopyBuffer {
src: independent_src,
src_range: BufferRange { offset: 0, size: 8 },
dst: independent_dst,
dst_range: BufferRange { offset: 0, size: 8 },
});
let compiled = graph.compile().expect("graph compiles");
assert_eq!(
compiled.batches(),
&[
vec![CommandIndex(0), CommandIndex(2)],
vec![CommandIndex(1)]
]
);
assert_eq!(
compiled.command_order(),
[CommandIndex(0), CommandIndex(2), CommandIndex(1)]
);
assert_eq!(compiled.barriers().len(), 1);
assert_eq!(compiled.barriers()[0].before_command, CommandIndex(1));
let batch_for = |command| {
compiled
.batches()
.iter()
.position(|batch| batch.contains(&command))
.expect("command appears in a batch")
};
for barrier in compiled.barriers() {
assert!(
batch_for(CommandIndex(0)) < batch_for(barrier.before_command),
"barrier target follows its conflicting source in another batch"
);
}
}
#[test]
fn recording_steps_place_barriers_immediately_before_their_target_command() {
let first_barrier = PipelineBarrier {
before_command: CommandIndex(1),
resources: Vec::new(),
};
let second_barrier = PipelineBarrier {
before_command: CommandIndex(1),
resources: Vec::new(),
};
let barriers = [first_barrier, second_barrier];
let batches = [vec![CommandIndex(0), CommandIndex(1)]];
let steps = recording_steps(&batches, &barriers).collect::<Vec<_>>();
assert!(matches!(steps[0], RecordingStep::Command(CommandIndex(0))));
assert!(
matches!(steps[1], RecordingStep::Barrier(barrier) if std::ptr::eq(barrier, &barriers[0]))
);
assert!(
matches!(steps[2], RecordingStep::Barrier(barrier) if std::ptr::eq(barrier, &barriers[1]))
);
assert!(matches!(steps[3], RecordingStep::Command(CommandIndex(1))));
}
#[test]
fn barrier_for_later_command_follows_independent_command_in_same_batch() {
let mut graph = RenderGraph::new();
let first_src = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let first_shared = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let first_dst = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let second_src = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let second_shared = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let second_dst = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let copy = |src, dst| CopyBuffer {
src,
src_range: BufferRange { offset: 0, size: 8 },
dst,
dst_range: BufferRange { offset: 0, size: 8 },
};
graph.push(copy(first_src, first_shared));
graph.push(copy(second_src, second_shared));
graph.push(copy(second_shared, second_dst));
graph.push(copy(first_shared, first_dst));
let compiled = graph.compile().expect("graph compiles");
assert_eq!(
compiled.batches(),
&[
vec![CommandIndex(0), CommandIndex(1)],
vec![CommandIndex(2), CommandIndex(3)]
]
);
let steps = recording_steps(compiled.batches(), compiled.barriers()).collect::<Vec<_>>();
let command_two_position = steps
.iter()
.position(|step| matches!(step, RecordingStep::Command(CommandIndex(2))))
.expect("independent command is recorded");
let barrier_three_position = steps
.iter()
.position(|step| {
matches!(step, RecordingStep::Barrier(barrier) if barrier.before_command == CommandIndex(3))
})
.expect("barrier for command three is recorded");
assert!(command_two_position < barrier_three_position);
}
#[test]
fn dependency_chain_occupies_three_batches() {
let mut graph = RenderGraph::new();
let a = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let b = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let c = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
let d = graph.create_buffer(GraphBufferDesc {
size: 8,
..Default::default()
});
graph.push(CopyBuffer {
src: a,
src_range: BufferRange { offset: 0, size: 8 },
dst: b,
dst_range: BufferRange { offset: 0, size: 8 },
});
graph.push(CopyBuffer {
src: b,
src_range: BufferRange { offset: 0, size: 8 },
dst: c,
dst_range: BufferRange { offset: 0, size: 8 },
});
graph.push(CopyBuffer {
src: c,
src_range: BufferRange { offset: 0, size: 8 },
dst: d,
dst_range: BufferRange { offset: 0, size: 8 },
});
let compiled = graph.compile().expect("graph compiles");
assert_eq!(
compiled.batches(),
&[
vec![CommandIndex(0)],
vec![CommandIndex(1)],
vec![CommandIndex(2)]
]
);
for barrier in compiled.barriers() {
let source_command = CommandIndex(barrier.before_command.index() - 1);
let source_batch = compiled
.batches()
.iter()
.position(|batch| batch.contains(&source_command))
.expect("conflicting source appears in a batch");
let target_batch = compiled
.batches()
.iter()
.position(|batch| batch.contains(&barrier.before_command))
.expect("barrier target appears in a batch");
assert!(source_batch < target_batch);
}
}
#[test] #[test]
fn texture_aliasing_size_uses_format_block_size() { fn texture_aliasing_size_uses_format_block_size() {
let mut graph = RenderGraph::new(); let mut graph = RenderGraph::new();