use concinnity_core::gfx::frustum::Frustum;
use concinnity_core::gfx::render_types::{
ClusterParams, FogFroxelParams, FogParams, RtParams, SsaoParams, SsgiParams, SsrParams,
TextDrawCall,
};
use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::planar_reflection::PlanarFramePlan;
use concinnity_core::render::reactive_mask::ReactiveMaskPlan;
#[cfg(debug_assertions)]
use concinnity_core::render::render_graph;
use concinnity_core::render::render_graph::{CompiledGraph, PassId, PassQueue};
use concinnity_core::render::uniforms::{GBufferView, PassCamera};
use concinnity_host::thread::jobs;
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{MTLBuffer, MTLCommandBuffer, MTLCommandQueue as _, MTLTexture};
use std::sync::atomic::Ordering;
use super::context::MtlContext;
use super::draw::main::ClusterGrid;
use super::frame_pacing::FrameJoin;
use super::graph_events;
use super::graph_events::PassSync;
use super::graph_queues::GraphQueues;
use super::parallel_encoder::{ParallelCtxRef, SendableCmdBuf};
pub(in crate::metal) struct GraphSubmission {
pub(in crate::metal) pending_terminal: Option<u64>,
}
pub(in crate::metal) struct GraphFrameParams<'a> {
pub cmd_buf: &'a ProtocolObject<dyn MTLCommandBuffer>,
pub cam_pos: [f32; 3],
pub skinned_joint_bufs: &'a [Retained<ProtocolObject<dyn MTLBuffer>>],
pub skinned_morph_weight_bufs: &'a [Retained<ProtocolObject<dyn MTLBuffer>>],
pub scene_color: Option<&'a Retained<ProtocolObject<dyn MTLTexture>>>,
pub text_calls: &'a [TextDrawCall],
pub ring_slot: usize,
pub world_hidden: bool,
pub elapsed: f32,
pub vp: [[f32; 4]; 4],
pub inv_vp: [[f32; 4]; 4],
pub frustum: &'a Frustum,
pub planar: PlanarFramePlan,
pub object_buffer: Option<&'a Retained<ProtocolObject<dyn MTLBuffer>>>,
pub material_params: Option<&'a Retained<ProtocolObject<dyn MTLBuffer>>>,
pub bindless_tex_args: Option<&'a Retained<ProtocolObject<dyn MTLBuffer>>>,
pub deformed_skinned: Option<&'a Retained<ProtocolObject<dyn MTLBuffer>>>,
pub deformed_prev: Option<&'a Retained<ProtocolObject<dyn MTLBuffer>>>,
pub prev_model_buffer: Option<&'a Retained<ProtocolObject<dyn MTLBuffer>>>,
pub history_targets: &'a [Retained<ProtocolObject<dyn MTLBuffer>>],
pub draw_args_buffer: Option<&'a Retained<ProtocolObject<dyn MTLBuffer>>>,
pub gbuffer_view: &'a GBufferView,
pub velocity_active: bool,
pub scene_pre_taa: Option<&'a Retained<ProtocolObject<dyn MTLTexture>>>,
pub ssr_params: Option<&'a SsrParams>,
pub fog_params: Option<&'a FogParams>,
pub fog_froxel_params: Option<&'a FogFroxelParams>,
pub cluster_params: Option<&'a ClusterParams>,
pub ssao_params: Option<&'a SsaoParams>,
pub ssgi_params: Option<&'a SsgiParams>,
pub rt_reflection_params: Option<&'a RtParams>,
pub reactive: ReactiveMaskPlan,
}
unsafe impl<'a> Send for GraphFrameParams<'a> {}
unsafe impl<'a> Sync for GraphFrameParams<'a> {}
fn pass_input<T>(value: Option<T>, pass: PassId, input: &str) -> RenderResult<T> {
value.ok_or_else(|| {
RenderError::Other(format!(
"graph executor: {pass:?} pass requires {input} but none was supplied"
))
})
}
fn encode_waits(
cmd_buf: &ProtocolObject<dyn MTLCommandBuffer>,
queues: &GraphQueues,
sync: &PassSync,
) {
for &(event_queue, value) in &sync.waits {
cmd_buf.encodeWaitForEvent_value(queues.event(event_queue), value);
}
}
fn encode_signals(
cmd_buf: &ProtocolObject<dyn MTLCommandBuffer>,
queues: &GraphQueues,
sync: &PassSync,
queue: PassQueue,
) {
for &value in &sync.signals {
cmd_buf.encodeSignalEvent_value(queues.event(queue), value);
}
}
impl MtlContext {
pub(in crate::metal) fn execute_graph(
&mut self,
graph: &CompiledGraph,
params: &GraphFrameParams<'_>,
join: &std::sync::Arc<FrameJoin>,
) -> RenderResult<GraphSubmission> {
#[cfg(debug_assertions)]
render_graph::assert_slot_aliasing_sound(
graph,
self.targets.transient_pool.slot_labels(),
"metal",
);
#[cfg(debug_assertions)]
render_graph::assert_serial_order_honors_schedule(graph, "metal");
let particle_frame = self.prepare_particle_pass(params.elapsed);
self.diagnostics
.draw_calls_accum
.store(0, Ordering::Relaxed);
let composite_idx = graph
.passes
.iter()
.position(|p| matches!(p.id, PassId::Composite));
let last_graphics = (0..graph.passes.len())
.rev()
.find(|&i| graph.passes[i].queue == PassQueue::Graphics);
let deferred_terminal = (composite_idx.is_some() && composite_idx == last_graphics)
.then_some(PassQueue::Graphics);
let plan = self.hw.graph_queues.as_ref().map(|queues| {
let (events, previous) = queues.begin_frame(graph.passes.len());
graph_events::plan_frame(graph, events, previous)
});
let worker_slots: std::sync::Mutex<Vec<Option<SendableCmdBuf>>> =
std::sync::Mutex::new((0..graph.passes.len()).map(|_| None).collect());
let first_error: std::sync::Mutex<Option<RenderError>> = std::sync::Mutex::new(None);
let plan_ref = plan.as_ref();
let ctx_ref = ParallelCtxRef::new(self);
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;
let pass_queue = pass.queue;
let particle_ref = particle_frame.as_ref();
let first_error_ref = &first_error;
let worker_slots_ref = &worker_slots;
scope.spawn(move |_| {
objc2::rc::autoreleasepool(|_| {
let ctx = ctx_ref.as_ctx();
let queue = match ctx.hw.graph_queues.as_ref() {
Some(queues) => queues.queue(pass_queue, &ctx.hw.command_queue),
None => &ctx.hw.command_queue,
};
let cmd_buf = match queue.commandBuffer() {
Some(cb) => cb,
None => {
let mut e = first_error_ref.lock().unwrap();
if e.is_none() {
*e = Some(RenderError::Other(
"graph executor: failed to mint per-pass cmd buf"
.into(),
));
}
return;
}
};
let sync = ctx.hw.graph_queues.as_ref().zip(plan_ref);
if let Some((queues, plan)) = sync {
encode_waits(&cmd_buf, queues, plan.pass(idx));
}
match ctx.encode_pass_into(pass_id, &cmd_buf, params, particle_ref) {
Ok(count) => {
if let Some((queues, plan)) = sync {
encode_signals(
&cmd_buf,
queues,
plan.pass(idx),
pass_queue,
);
}
ctx.diagnostics
.draw_calls_accum
.fetch_add(count, Ordering::Relaxed);
let mut lock = worker_slots_ref.lock().unwrap();
lock[idx] = Some(SendableCmdBuf(cmd_buf));
}
Err(e) => {
let mut lock = first_error_ref.lock().unwrap();
if lock.is_none() {
*lock = Some(e);
}
}
}
})
});
}
});
});
if let Some(err) = first_error.into_inner().unwrap_or(None) {
return Err(err);
}
let slots = worker_slots.into_inner().map_err(|_| {
RenderError::Other("graph executor: worker slot mutex poisoned".to_string())
})?;
for (idx, slot) in slots.into_iter().enumerate() {
if let Some(cb) = slot {
let pass_id = graph.passes.get(idx).map(|p| p.id);
let part = std::sync::Arc::clone(join);
join.add_part();
let handler = block2::RcBlock::new(
move |cbh: std::ptr::NonNull<ProtocolObject<dyn MTLCommandBuffer>>| {
let cbh = unsafe { cbh.as_ref() };
match pass_id {
Some(id) => super::fault_log::report_fault(
cbh,
format_args!("render-graph pass {id:?}"),
),
None => super::fault_log::report_fault(cbh, "render-graph pass"),
}
part.arrive();
},
);
unsafe {
cb.0.addCompletedHandler(block2::RcBlock::as_ptr(&handler));
}
cb.0.commit();
}
}
let mut submission = GraphSubmission {
pending_terminal: None,
};
if let (Some(queues), Some(plan)) = (self.hw.graph_queues.as_mut(), plan.as_ref()) {
queues.end_submission(plan, deferred_terminal);
submission.pending_terminal = deferred_terminal.and_then(|q| plan.terminal(q));
}
if let Some(idx) = composite_idx {
let sync = self.hw.graph_queues.as_ref().zip(plan.as_ref());
if let Some((queues, plan)) = sync {
encode_waits(params.cmd_buf, queues, plan.pass(idx));
}
let count = self.encode_pass_into(
PassId::Composite,
params.cmd_buf,
params,
particle_frame.as_ref(),
)?;
if let Some((queues, plan)) = sync {
encode_signals(params.cmd_buf, queues, plan.pass(idx), PassQueue::Graphics);
}
self.diagnostics
.draw_calls_accum
.fetch_add(count, Ordering::Relaxed);
}
self.diagnostics.frame_stats.draw_calls +=
self.diagnostics.draw_calls_accum.load(Ordering::Relaxed);
Ok(submission)
}
pub(in crate::metal) fn record_graph_terminal(&mut self, value: u64) {
if let Some(queues) = self.hw.graph_queues.as_mut() {
queues.record_terminal(PassQueue::Graphics, value);
}
}
fn build_raymarch_view(&self, params: &GraphFrameParams<'_>) -> super::raymarch::RaymarchView {
super::raymarch::RaymarchView::new(&self.pass_camera(params))
}
fn pass_camera(&self, params: &GraphFrameParams<'_>) -> PassCamera {
PassCamera {
vp: params.vp,
inv_vp: params.inv_vp,
cam_pos: params.cam_pos,
viewport: [
self.targets.hdr.width as f32,
self.targets.hdr.height as f32,
],
time: params.elapsed,
prefilter_mip_count: self.scene.env_map.prefilter_mip_count as f32,
sky_rot: self.state.view.sky_rot,
}
}
fn encode_pass_into(
&self,
pass_id: PassId,
cmd_buf: &ProtocolObject<dyn MTLCommandBuffer>,
params: &GraphFrameParams<'_>,
particle_frame: Option<&super::particle::ParticleFrame>,
) -> RenderResult<u32> {
Ok(match pass_id {
PassId::Cull => {
let object_buffer =
pass_input(params.object_buffer, PassId::Cull, "object_buffer")?;
let draw_args_buffer =
pass_input(params.draw_args_buffer, PassId::Cull, "draw_args_buffer")?;
if let Some(deformed) = params.deformed_skinned {
self.encode_main_skin(
cmd_buf,
deformed,
super::raytrace::MainSkinBuffers {
joints: params.skinned_joint_bufs,
morph_weights: params.skinned_morph_weight_bufs,
},
)?;
}
self.encode_cull(
cmd_buf,
object_buffer,
draw_args_buffer,
params.frustum,
params.cam_pos,
self.draw_record_counts(),
)?;
self.encode_shadow_culls(cmd_buf, object_buffer, draw_args_buffer)?;
0
}
PassId::HizBuild | PassId::HizFinal => {
self.encode_hiz_build(cmd_buf);
0
}
PassId::Cull2 => {
let object_buffer =
pass_input(params.object_buffer, PassId::Cull2, "object_buffer")?;
let draw_args_buffer =
pass_input(params.draw_args_buffer, PassId::Cull2, "draw_args_buffer")?;
self.encode_cull_phase2(
cmd_buf,
object_buffer,
draw_args_buffer,
params.frustum,
params.cam_pos,
)?
}
PassId::Main2 => self.encode_main_pass_phase2(
cmd_buf,
crate::metal::draw::main::MainPassCamera {
elapsed: params.elapsed,
vp: params.vp,
view: self.state.view.matrix,
cam_pos: params.cam_pos,
},
crate::metal::draw::main::GpuFrameBuffers {
object_buffer: params.object_buffer,
material_params: params.material_params,
bindless_tex_args: params.bindless_tex_args,
deformed_skinned: params.deformed_skinned,
counts: self.draw_record_counts(),
},
)?,
PassId::Shadow => {
let raymarch_view = if self.any_raymarch_shadow_casters() {
Some(self.build_raymarch_view(params))
} else {
None
};
self.encode_shadow_pass(
cmd_buf,
params.object_buffer,
params.deformed_skinned,
raymarch_view.as_ref(),
)?
}
PassId::SpotShadow => self.encode_spot_shadow_pass(
cmd_buf,
params.object_buffer,
params.deformed_skinned,
)?,
PassId::Main => self.encode_main_pass(
cmd_buf,
crate::metal::draw::main::MainPassCamera {
elapsed: params.elapsed,
vp: params.vp,
view: self.state.view.matrix,
cam_pos: params.cam_pos,
},
crate::metal::draw::main::GpuFrameBuffers {
object_buffer: params.object_buffer,
material_params: params.material_params,
bindless_tex_args: params.bindless_tex_args,
deformed_skinned: params.deformed_skinned,
counts: self.draw_record_counts(),
},
params.world_hidden,
)?,
PassId::AutoExposure => self.encode_auto_exposure(cmd_buf, params.ring_slot)?,
PassId::Bloom => {
let scene_color = pass_input(params.scene_color, PassId::Bloom, "scene_color")?;
self.encode_bloom(cmd_buf, scene_color)?
}
PassId::GBufferPrepass => {
let main_view =
self.main_view_uniforms(&crate::metal::draw::main::MainPassCamera {
elapsed: params.elapsed,
vp: params.vp,
view: self.state.view.matrix,
cam_pos: params.cam_pos,
});
let raymarch_view = super::raymarch::RaymarchView::for_gbuffer(
&self.pass_camera(params),
params.gbuffer_view,
);
self.encode_gbuffer_prepass(
cmd_buf,
crate::metal::post::gbuffer::GbufferPrepassViews {
gbuffer: params.gbuffer_view,
main: &main_view,
raymarch: &raymarch_view,
frustum: params.frustum,
},
crate::metal::post::gbuffer::GbufferGpuBuffers {
object_buffer: params.object_buffer,
material_params: params.material_params,
bindless_tex_args: params.bindless_tex_args,
prev_model_buffer: params.prev_model_buffer,
draw_args_buffer: params.draw_args_buffer,
history_targets: params.history_targets,
deformed_current: params.deformed_skinned,
deformed_prev: params.deformed_prev,
},
params.velocity_active,
)?
}
PassId::TaaResolve => {
let scene_pre_taa =
pass_input(params.scene_pre_taa, PassId::TaaResolve, "scene_pre_taa")?;
self.encode_taa(cmd_buf, scene_pre_taa, params.reactive.readable)?
}
PassId::SsrResolve => {
let ssr_params = pass_input(params.ssr_params, PassId::SsrResolve, "ssr_params")?;
self.encode_ssr_resolve(cmd_buf, ssr_params)?
}
PassId::Ssgi => {
let ssgi_params = pass_input(params.ssgi_params, PassId::Ssgi, "ssgi_params")?;
self.encode_ssgi(cmd_buf, ssgi_params)?
}
PassId::RtReflections => {
let rt_params = pass_input(
params.rt_reflection_params,
PassId::RtReflections,
"rt_reflection_params",
)?;
self.encode_rt_reflections(cmd_buf, rt_params, params.bindless_tex_args)?
}
PassId::SsaoBlur => {
let ssao_params = pass_input(params.ssao_params, PassId::SsaoBlur, "ssao_params")?;
self.encode_ssao(cmd_buf, ssao_params)?
}
PassId::SsaoDepth | PassId::SsaoKernel => {
return Err(RenderError::Other(format!(
"graph executor: pass {} is bundled inside SsaoBlur \
(encode_ssao encodes all three SSAO sub-passes); it \
should not appear as its own graph node",
pass_id.name()
)));
}
PassId::Sky => {
return Err(RenderError::Other(format!(
"graph executor: pass {} is drawn inline by the opaque scene \
passes; it should not appear as its own graph node",
pass_id.name()
)));
}
PassId::ReflectionComposite => {
return Err(RenderError::Other(format!(
"graph executor: pass {} is encoded inline by SsrResolve / \
RtReflections; it should not appear as its own graph node",
pass_id.name()
)));
}
PassId::Decals => {
self.encode_decals(cmd_buf, params.vp, params.inv_vp, params.frustum)?
}
PassId::Fog => {
let fog_params = pass_input(params.fog_params, PassId::Fog, "fog_params")?;
let fog_froxel_params =
pass_input(params.fog_froxel_params, PassId::Fog, "fog_froxel_params")?;
self.encode_fog(cmd_buf, fog_params, fog_froxel_params)?
}
PassId::FogFroxel => {
let fog_params = pass_input(params.fog_params, PassId::FogFroxel, "fog_params")?;
let fog_froxel_params = pass_input(
params.fog_froxel_params,
PassId::FogFroxel,
"fog_froxel_params",
)?;
self.encode_fog_froxel(cmd_buf, fog_params, fog_froxel_params)?
}
PassId::LightCull => {
let cluster_params =
pass_input(params.cluster_params, PassId::LightCull, "cluster_params")?;
self.encode_light_cull(
cmd_buf,
ClusterGrid {
params: cluster_params,
lists: &self.light_cull.cluster_buffer,
},
Some(PassId::LightCull),
)?
}
PassId::ParticlesSim => {
if let Some(frame) = particle_frame {
self.encode_particles_sim(cmd_buf, frame)?;
}
0
}
PassId::ParticlesDraw => {
let write = params.reactive.particles;
let draws = if let Some(frame) = particle_frame {
self.encode_particles_draw(cmd_buf, frame, params.vp, params.frustum, write)?
} else {
0
};
if draws == 0 {
self.clear_reactive_mask(cmd_buf, write)?;
}
draws
}
PassId::Lines => self.encode_lines(cmd_buf, params.vp)?,
PassId::Composite => {
let scene_color = pass_input(params.scene_color, PassId::Composite, "scene_color")?;
self.encode_composite_and_text(
cmd_buf,
scene_color,
params.text_calls,
params.reactive.readable,
)?
}
PassId::Upscale => {
let scene_pre_taa =
pass_input(params.scene_pre_taa, PassId::Upscale, "scene_pre_taa")?;
self.encode_upscale(cmd_buf, scene_pre_taa, params.reactive.readable)?
}
PassId::Transparent => {
let scene_pre_taa =
pass_input(params.scene_pre_taa, PassId::Transparent, "scene_pre_taa")?;
let view = concinnity_core::render::uniforms::TransparentView::new(
&self.pass_camera(params),
&self.light_uniforms,
);
let mirrors = if self.planar_mirrors_needed() {
params.planar
} else {
PlanarFramePlan::default()
};
let mut draws = Vec::new();
self.collect_water_transparent_draws(
&view,
params.bindless_tex_args.is_some(),
&mirrors,
&mut draws,
);
self.collect_glass_transparent_draws(
&view,
params.bindless_tex_args.is_some(),
&mirrors,
&mut draws,
);
self.collect_mesh_transparent_draws(
&view,
params.bindless_tex_args.is_some(),
&mut draws,
);
self.encode_transparent(
cmd_buf,
scene_pre_taa,
&draws,
super::transparent::TransparentFrame {
view: &view,
rt_params: params.rt_reflection_params,
bindless_tex_args: params.bindless_tex_args,
reactive: params.reactive.transparent,
},
)?
}
PassId::PlanarReflection => {
self.encode_planar_reflections(cmd_buf, params)?;
0
}
PassId::Raymarch => {
let view = self.build_raymarch_view(params);
self.encode_raymarch(cmd_buf, &view, params.frustum)?
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pass_input_names_the_pass_and_the_missing_input() {
assert_eq!(pass_input(Some(7), PassId::Fog, "fog_params"), Ok(7));
assert_eq!(
pass_input::<u32>(None, PassId::Fog, "fog_params"),
Err(RenderError::Other(
"graph executor: Fog pass requires fog_params but none was supplied".into()
))
);
}
}