use ash::vk;
use ash::Device;
use crate::gfx::frustum::Frustum;
use crate::gfx::render_graph::{
CompiledGraph, CompiledPass, GraphResourceClass, PassId, final_states,
};
use crate::gfx::render_types::{LineVertex, TextDrawCall};
use super::barrier_translate::{VkResting, vk_restore, vk_transition};
use super::context::VkContext;
use super::parallel_encoder::ParallelCtxRef;
use super::post::gbuffer::GbufferPrepassView;
use super::record::Recorder;
#[derive(Copy, Clone)]
enum VkTargetObject {
Image {
image: vk::Image,
mip_levels: u32,
layer_count: u32,
},
Buffer {
buffer: vk::Buffer,
},
}
struct VkBarrierTarget {
object: VkTargetObject,
class: GraphResourceClass,
resting: VkResting,
}
struct VkBarrierRegistry(Vec<Option<VkBarrierTarget>>);
pub(in crate::vulkan) struct VkBarrierScratch {
registry: VkBarrierRegistry,
alias: Vec<Vec<vk::Image>>,
}
impl VkBarrierScratch {
fn new() -> Self {
Self {
registry: VkBarrierRegistry(Vec::new()),
alias: Vec::new(),
}
}
}
fn emit_graph_barriers(
device: &Device,
cmd: vk::CommandBuffer,
registry: &VkBarrierRegistry,
pass: &CompiledPass,
) {
for op in &pass.barriers_before {
let Some(Some(target)) = registry.0.get(op.resource_index()) else {
continue;
};
let Some(transition) = vk_transition(
target.class,
target.resting,
op.source_state(),
op.to_state(),
op.read_stages(),
) else {
continue;
};
emit_one(device, cmd, target, transition);
}
}
fn emit_one(
device: &Device,
cmd: vk::CommandBuffer,
target: &VkBarrierTarget,
transition: (
vk::ImageLayout,
vk::ImageLayout,
vk::AccessFlags,
vk::AccessFlags,
vk::PipelineStageFlags,
vk::PipelineStageFlags,
),
) {
let (old_layout, new_layout, src_access, dst_access, src_stage, dst_stage) = transition;
match target.object {
VkTargetObject::Buffer { buffer } => {
let barrier = vk::BufferMemoryBarrier::default()
.src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
.dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
.buffer(buffer)
.offset(0)
.size(vk::WHOLE_SIZE)
.src_access_mask(src_access)
.dst_access_mask(dst_access);
unsafe {
device.cmd_pipeline_barrier(
cmd,
src_stage,
dst_stage,
vk::DependencyFlags::empty(),
&[],
std::slice::from_ref(&barrier),
&[],
);
}
}
VkTargetObject::Image {
image,
mip_levels,
layer_count,
} => {
let aspect = match target.class {
GraphResourceClass::DepthTarget => vk::ImageAspectFlags::DEPTH,
_ => vk::ImageAspectFlags::COLOR,
};
let barrier = vk::ImageMemoryBarrier::default()
.old_layout(old_layout)
.new_layout(new_layout)
.src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
.dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
.image(image)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: aspect,
base_mip_level: 0,
level_count: mip_levels,
base_array_layer: 0,
layer_count,
})
.src_access_mask(src_access)
.dst_access_mask(dst_access);
unsafe {
device.cmd_pipeline_barrier(
cmd,
src_stage,
dst_stage,
vk::DependencyFlags::empty(),
&[],
&[],
std::slice::from_ref(&barrier),
);
}
}
}
}
fn emit_graph_restores(
device: &Device,
cmd: vk::CommandBuffer,
registry: &VkBarrierRegistry,
graph: &CompiledGraph,
) {
for (idx, (state, stages)) in final_states(graph).into_iter().enumerate() {
let Some(Some(target)) = registry.0.get(idx) else {
continue;
};
let Some(transition) = vk_restore(target.class, target.resting, state, stages) else {
continue;
};
emit_one(device, cmd, target, transition);
}
}
fn emit_alias_barriers(device: &Device, cmd: vk::CommandBuffer, images: &[vk::Image]) {
for &image in images {
let barrier = vk::ImageMemoryBarrier::default()
.old_layout(vk::ImageLayout::UNDEFINED)
.new_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL)
.src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
.dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
.image(image)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
})
.src_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE | vk::AccessFlags::SHADER_READ)
.dst_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE);
unsafe {
device.cmd_pipeline_barrier(
cmd,
vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
| vk::PipelineStageFlags::FRAGMENT_SHADER,
vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
vk::DependencyFlags::empty(),
&[],
&[],
std::slice::from_ref(&barrier),
);
}
}
}
fn emit_pass_prologue(
device: &Device,
cmd: vk::CommandBuffer,
registry: &VkBarrierRegistry,
alias: &[vk::Image],
pass: &CompiledPass,
) {
emit_alias_barriers(device, cmd, alias);
emit_graph_barriers(device, cmd, registry, pass);
}
#[cfg(debug_assertions)]
fn debug_assert_graph_drives(graph: &CompiledGraph, registry: &VkBarrierRegistry) {
use super::barrier_translate::vk_state;
use crate::gfx::render_graph::{ResourceState, barrier_coverage_gaps_for_driven};
let driven: Vec<bool> = registry.0.iter().map(|t| t.is_some()).collect();
let gaps = barrier_coverage_gaps_for_driven(graph, &driven);
assert!(
gaps.is_empty(),
"render graph (vulkan): uncovered accesses on graph-driven resources: {}",
gaps.iter()
.map(|g| g.to_string())
.collect::<Vec<_>>()
.join(", ")
);
for (idx, (state, stages)) in final_states(graph).into_iter().enumerate() {
let Some(Some(target)) = registry.0.get(idx) else {
continue;
};
if target.class.is_buffer()
|| target.resting == VkResting::Discarded
|| state == ResourceState::Undefined
{
continue;
}
let restored = match vk_restore(target.class, target.resting, state, stages) {
Some((_, new, ..)) => new,
None => vk_state(target.class, state, stages).0,
};
assert_eq!(
restored,
target.resting.layout(),
"render graph (vulkan): {} rests in {:?} but the frame leaves it in {:?}",
graph.resources[idx].label,
target.resting.layout(),
restored,
);
}
}
pub(in crate::vulkan) struct GraphFrameParams<'a> {
pub cmd: vk::CommandBuffer,
pub image_index: u32,
pub frame_idx: usize,
pub text_calls: &'a [TextDrawCall],
pub lines: &'a [LineVertex],
pub world_hidden: bool,
pub frustum: &'a Frustum,
pub cam_pos: [f32; 3],
pub vp_mat: [[f32; 4]; 4],
pub cur_vp: [[f32; 4]; 4],
pub fov_y_radians: f32,
pub aspect: f32,
pub elapsed: f32,
pub near: f32,
pub far: f32,
}
impl VkContext {
pub(in crate::vulkan) fn execute_graph(
&mut self,
graph: &CompiledGraph,
params: &GraphFrameParams<'_>,
) -> Result<Vec<vk::CommandBuffer>, String> {
let particle_frame = self.prepare_particle_pass(params.elapsed);
self.prepare_instanced_clusters(params.cam_pos);
let composite_idx = graph.passes.iter().position(|p| p.id == PassId::Composite);
let frame_idx = params.frame_idx;
let device = ash::Device::clone(&self.device);
let worker_slots: std::sync::Mutex<Vec<Option<vk::CommandBuffer>>> =
std::sync::Mutex::new(vec![None; graph.passes.len()]);
let first_error: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
let mut scratch = self
.draw
.barrier_scratch
.take()
.unwrap_or_else(VkBarrierScratch::new);
self.refill_barrier_registry(&mut scratch.registry, graph, frame_idx);
#[cfg(debug_assertions)]
debug_assert_graph_drives(graph, &scratch.registry);
#[cfg(debug_assertions)]
crate::gfx::render_graph::assert_slot_aliasing_sound(
graph,
self.transient_pool.slot_labels(),
"vulkan",
);
self.refill_alias_barriers(&mut scratch.alias, graph, frame_idx);
let VkBarrierScratch {
registry,
alias: alias_barriers,
} = &scratch;
let ctx_ref = ParallelCtxRef::new(self);
let particle_ref = particle_frame.as_ref();
let device_ref = &device;
let worker_slots_ref = &worker_slots;
let first_error_ref = &first_error;
let registry_ref = registry;
let alias_barriers_ref = alias_barriers;
crate::jobs::pool().install(|| {
rayon::scope(|scope| {
for (idx, pass) in graph.passes.iter().enumerate() {
if Some(idx) == composite_idx {
continue;
}
let pass_id = pass.id;
scope.spawn(move |_| {
let ctx = ctx_ref.as_ctx();
let pool_idx =
frame_idx * crate::gfx::render_graph::PASS_COUNT + pass_id as usize;
let buf = ctx.commands.pass_command_buffers[pool_idx];
let set_err = |msg: String| {
let mut lock = first_error_ref.lock().unwrap();
if lock.is_none() {
*lock = Some(msg);
}
};
let reset = unsafe {
device_ref
.reset_command_buffer(buf, vk::CommandBufferResetFlags::empty())
};
if let Err(e) = reset {
set_err(format!("reset pass cmd buf ({}): {e}", pass_id.name()));
return;
}
let rec = match Recorder::begin(
device_ref,
buf,
vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT,
) {
Ok(rec) => rec,
Err(e) => {
set_err(format!("begin pass cmd buf ({}): {e}", pass_id.name()));
return;
}
};
if let Some(pool) = ctx.timestamp_query_pool {
let (ts_start, _) = super::pass_timing::pass_pair(frame_idx, pass_id);
rec.write_timestamp(
vk::PipelineStageFlags::TOP_OF_PIPE,
pool,
ts_start,
);
}
emit_pass_prologue(
device_ref,
buf,
registry_ref,
&alias_barriers_ref[idx],
pass,
);
if let Err(e) = ctx.encode_pass_into(pass_id, &rec, params, particle_ref) {
set_err(e);
return;
}
if let Some(pool) = ctx.timestamp_query_pool {
let (_, ts_end) = super::pass_timing::pass_pair(frame_idx, pass_id);
rec.write_timestamp(
vk::PipelineStageFlags::BOTTOM_OF_PIPE,
pool,
ts_end,
);
}
if let Err(e) = rec.end() {
set_err(format!("end pass cmd buf ({}): {e}", pass_id.name()));
return;
}
worker_slots_ref.lock().unwrap()[idx] = Some(buf);
});
}
});
});
if let Some(e) = first_error.into_inner().unwrap() {
return Err(e);
}
if let Some(idx) = composite_idx {
if let Some(pool) = self.timestamp_query_pool {
let (ts_start, _) = super::pass_timing::pass_pair(frame_idx, PassId::Composite);
unsafe {
self.device.cmd_write_timestamp(
params.cmd,
vk::PipelineStageFlags::TOP_OF_PIPE,
pool,
ts_start,
);
}
}
emit_pass_prologue(
&self.device,
params.cmd,
registry,
&alias_barriers[idx],
&graph.passes[idx],
);
let rec = unsafe { Recorder::assume_recording(&self.device, params.cmd) };
self.encode_pass_into(PassId::Composite, &rec, params, particle_frame.as_ref())?;
if let Some(pool) = self.timestamp_query_pool {
let (_, ts_end) = super::pass_timing::pass_pair(frame_idx, PassId::Composite);
unsafe {
self.device.cmd_write_timestamp(
params.cmd,
vk::PipelineStageFlags::BOTTOM_OF_PIPE,
pool,
ts_end,
);
}
}
}
emit_graph_restores(&self.device, params.cmd, registry, graph);
let ordered: Vec<vk::CommandBuffer> = worker_slots
.into_inner()
.unwrap()
.into_iter()
.flatten()
.collect();
self.draw.barrier_scratch = Some(scratch);
Ok(ordered)
}
fn refill_barrier_registry(
&self,
registry: &mut VkBarrierRegistry,
graph: &CompiledGraph,
frame_idx: usize,
) {
registry.0.clear();
registry.0.extend(graph.resources.iter().map(|res| {
let class = res.class()?;
let (object, resting) = self.barrier_object_for_label(res.label, frame_idx)?;
Some(VkBarrierTarget {
object,
class,
resting,
})
}));
}
fn refill_alias_barriers(
&self,
table: &mut Vec<Vec<vk::Image>>,
graph: &CompiledGraph,
frame_idx: usize,
) {
for row in table.iter_mut() {
row.clear();
}
table.resize_with(graph.passes.len(), Vec::new);
for res in &graph.resources {
if self.transient_pool.alias_predecessor(res.label).is_none() {
continue;
}
if let Some(image) = self.transient_pool.image_for(res.label, frame_idx) {
let first = res.lifetime.first;
if first < table.len() {
table[first].push(image);
}
}
}
}
fn barrier_object_for_label(
&self,
label: &str,
frame_idx: usize,
) -> Option<(VkTargetObject, VkResting)> {
let buffer = |slots: &[super::allocator::PooledBuffer]| {
slots.get(frame_idx).map(|b| {
(
VkTargetObject::Buffer { buffer: b.buffer() },
VkResting::Discarded,
)
})
};
let image = |image, mip_levels, layer_count, resting| {
(
VkTargetObject::Image {
image,
mip_levels,
layer_count,
},
resting,
)
};
match label {
"draw_args" => buffer(&self.cull.indirect_buffers),
"draw_args2" => buffer(&self.cull.indirect_buffers2),
"cull_status" => buffer(&self.cull.cull_status_buffers),
"cluster_light_list" => Some((
VkTargetObject::Buffer {
buffer: self.light_cull.cluster_buffer.buffer(),
},
VkResting::Discarded,
)),
"ao_output" => self
.transient_pool
.image_for("ao_output", frame_idx)
.map(|i| image(i, 1, 1, VkResting::Discarded)),
"shadow_map" if !self.shadow.framebuffers.is_empty() => Some(image(
self.shadow.map.image,
1,
self.shadow.framebuffers.len() as u32,
VkResting::Sampled,
)),
"spot_shadow_map" if !self.spot_shadow.framebuffers.is_empty() => Some(image(
self.spot_shadow.map.image,
1,
self.spot_shadow.framebuffers.len() as u32,
VkResting::Sampled,
)),
"fog_froxel_volume" => self
.fog
.resources
.as_ref()
.map(|f| image(f.volume.image(), 1, 1, VkResting::Sampled)),
"hdr_depth" => self
.depth_images
.get(frame_idx)
.map(|d| image(d.image, 1, 1, VkResting::Discarded)),
"hiz_pyramid" => self
.cull
.hiz
.as_ref()
.map(|h| image(h.pyramid.image(), h.mip_count, 1, VkResting::Sampled)),
_ => None,
}
}
pub(in crate::vulkan) fn encode_pass_into(
&self,
pass_id: PassId,
rec: &Recorder<'_>,
params: &GraphFrameParams<'_>,
particle_frame: Option<&(f32, u32, Vec<u32>)>,
) -> Result<(), String> {
let cmd = rec.raw();
match pass_id {
PassId::Cull => {
self.encode_cull(cmd, params.frame_idx, params.frustum, params.cam_pos);
self.encode_skin(cmd, params.frame_idx);
}
PassId::LightCull => {
self.encode_light_cull(rec, params.frame_idx);
}
PassId::SsaoBlur => {
self.encode_ssao(rec, params.frame_idx, params.fov_y_radians, params.aspect);
}
PassId::SsaoPrepass | PassId::SsaoKernel => {
return Err(format!(
"graph executor (vulkan): pass {} is bundled inside SsaoBlur \
(encode_ssao encodes the SSAO kernel + blur sub-passes); it \
should not appear as its own graph node",
pass_id.name()
));
}
PassId::ReflectionComposite => {
return Err(format!(
"graph executor (vulkan): pass {} is a Metal-only inline \
reflection composite and should not appear as a graph node",
pass_id.name()
));
}
PassId::SsrPrepass => {
return Err(format!(
"graph executor (vulkan): pass {} is merged into GBufferPrepass \
and should not appear in the frame graph",
pass_id.name()
));
}
PassId::SsrResolve => {
self.encode_ssr_resolve(
cmd,
params.frame_idx,
params.fov_y_radians,
params.aspect,
params.cam_pos,
);
}
PassId::Ssgi => {
self.encode_ssgi(cmd, params.frame_idx, params.fov_y_radians, params.aspect);
}
PassId::RtReflections => {
self.encode_rt_reflections(
cmd,
params.frame_idx,
params.fov_y_radians,
params.aspect,
params.cam_pos,
);
}
PassId::Velocity => {
return Err(format!(
"graph executor (vulkan): pass {} is merged into GBufferPrepass \
and should not appear in the frame graph",
pass_id.name()
));
}
PassId::TaaResolve => {
self.encode_taa(cmd, params.frame_idx);
}
PassId::Upscale => {
self.encode_upscale(cmd, params)?;
}
PassId::Bloom => {
self.encode_bloom(cmd, params.frame_idx);
}
PassId::Shadow => {
self.encode_shadow_pass(cmd, params.frame_idx, params.cam_pos, params.elapsed);
}
PassId::SpotShadow => {
self.encode_spot_shadow_pass(cmd, params.frame_idx, params.cam_pos);
}
PassId::Main => {
self.encode_main_pass(cmd, params.frame_idx, params.world_hidden);
}
PassId::Composite => {
self.encode_composite_and_text(
cmd,
params.image_index,
params.frame_idx,
params.text_calls,
)?;
}
PassId::Decals => {
self.encode_decals(cmd, params.frame_idx, params.vp_mat, params.frustum);
}
PassId::Lines => {
self.encode_lines(cmd, params.frame_idx, params.vp_mat, params.lines);
}
PassId::FogFroxel => {
self.encode_fog_froxel(
cmd,
params.frame_idx,
params.near,
params.vp_mat,
params.cam_pos,
);
}
PassId::Fog => {
self.encode_fog(cmd, params.frame_idx, params.vp_mat, params.cam_pos);
}
PassId::AutoExposure => {
self.encode_auto_exposure(cmd, params.frame_idx);
}
PassId::ParticlesDraw => {
if let Some(frame) = particle_frame {
self.encode_particles(
cmd,
params.frame_idx,
frame,
params.vp_mat,
params.frustum,
);
}
}
PassId::Raymarch => {
let view = self.build_raymarch_view(params.vp_mat, params.cam_pos, params.elapsed);
self.encode_raymarch(cmd, params.frame_idx, &view)?;
}
PassId::Transparent => {
if self.planar_pass_needed() {
self.encode_planar_reflections(
cmd,
params.frame_idx,
params.vp_mat,
params.cam_pos,
params.elapsed,
)?;
}
let view =
self.build_transparent_view(params.vp_mat, params.cam_pos, params.elapsed);
self.encode_transparent(
cmd,
params.frame_idx,
&view,
params.fov_y_radians,
params.aspect,
)?;
}
PassId::HizBuild | PassId::HizFinal => {
self.encode_hiz_build(cmd, params.frame_idx);
}
PassId::Cull2 => {
self.encode_cull_phase2(
cmd,
params.frame_idx,
params.frustum,
params.cam_pos,
params.cur_vp,
);
}
PassId::Main2 => {
self.encode_main_pass_phase2(cmd, params.frame_idx);
}
PassId::GBufferPrepass => {
let gb = self.gbuffer.as_ref().ok_or(
"graph executor (vulkan): GBufferPrepass emitted but self.gbuffer is None",
)?;
let velocity_active = self.taa.is_some() || self.upscale.is_some();
self.encode_gbuffer_prepass(
gb,
cmd,
params.frame_idx,
GbufferPrepassView {
jittered_vp: params.vp_mat,
cur_vp: params.cur_vp,
},
velocity_active,
);
}
other => {
return Err(format!(
"graph executor (vulkan): pass {} is not handled by this \
executor; it should not appear in the frame graph",
other.name()
));
}
}
Ok(())
}
}