use std::sync::atomic::Ordering;
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{MTLBuffer, MTLCommandBuffer, MTLCommandQueue as _, MTLTexture};
use crate::gfx::frustum::Frustum;
use crate::gfx::render_graph::{CompiledGraph, PassId};
use crate::gfx::render_types::{
ClusterParams, FogFroxelParams, FogParams, RtParams, SsaoParams, SsgiParams, SsrParams,
TextDrawCall,
};
use super::context::MtlContext;
use super::parallel_encoder::{ParallelCtxRef, SendableCmdBuf};
use super::uniforms::VelocityUniforms;
use concinnity_render::uniforms::GBufferView;
use concinnity_render::uniforms::TaaParams;
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 lines: &'a [crate::gfx::render_types::LineVertex],
pub world_hidden: bool,
pub elapsed: f32,
pub vp: [[f32; 4]; 4],
pub inv_vp: [[f32; 4]; 4],
pub visible: &'a [u32],
pub frustum: &'a Frustum,
pub prepared_instances: &'a super::instanced::PreparedInstances,
pub object_buffer: 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 draw_args_buffer: Option<&'a Retained<ProtocolObject<dyn MTLBuffer>>>,
pub vel_uniforms: Option<&'a VelocityUniforms>,
pub prev_skinned_joint_bufs: &'a [Retained<ProtocolObject<dyn MTLBuffer>>],
pub taa_uniforms: Option<&'a TaaParams>,
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>,
}
unsafe impl<'a> Send for GraphFrameParams<'a> {}
unsafe impl<'a> Sync for GraphFrameParams<'a> {}
impl MtlContext {
pub(in crate::metal) fn execute_graph(
&mut self,
graph: &CompiledGraph,
params: &GraphFrameParams<'_>,
) -> Result<(), String> {
#[cfg(debug_assertions)]
crate::gfx::render_graph::assert_slot_aliasing_sound(
graph,
self.transient_pool.slot_labels(),
"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 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<String>> = std::sync::Mutex::new(None);
let pass_fault_count = std::sync::Arc::clone(&self.diagnostics.pass_fault_count);
let ctx_ref = ParallelCtxRef::new(self);
crate::jobs::pool().install(|| {
rayon::scope(|scope| {
for (idx, pass) in graph.passes.iter().enumerate() {
if Some(idx) == composite_idx {
continue;
}
let _ = &pass.barriers_before;
let pass_id = pass.id;
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 cmd_buf = match ctx.command_queue.commandBuffer() {
Some(cb) => cb,
None => {
let mut e = first_error_ref.lock().unwrap();
if e.is_none() {
*e = Some(
"graph executor: failed to mint per-pass cmd buf"
.into(),
);
}
return;
}
};
match ctx.encode_pass_into(pass_id, &cmd_buf, params, particle_ref) {
Ok(count) => {
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(|_| "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 throttle = std::sync::Arc::clone(&pass_fault_count);
let handler = block2::RcBlock::new(
move |cbh: std::ptr::NonNull<ProtocolObject<dyn MTLCommandBuffer>>| {
let cbh = unsafe { cbh.as_ref() };
if cbh.status() == objc2_metal::MTLCommandBufferStatus::Error
&& throttle.fetch_add(1, Ordering::Relaxed) < 8
{
tracing::error!(
"render-graph pass {:?} command buffer faulted: {:?}",
pass_id,
cbh.error()
);
}
},
);
unsafe {
cb.0.addCompletedHandler(block2::RcBlock::as_ptr(&handler));
}
cb.0.commit();
}
}
if composite_idx.is_some() {
let count = self.encode_pass_into(
PassId::Composite,
params.cmd_buf,
params,
particle_frame.as_ref(),
)?;
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(())
}
fn build_raymarch_view(&self, params: &GraphFrameParams<'_>) -> super::raymarch::RaymarchView {
super::raymarch::RaymarchView {
vp: params.vp,
inv_vp: params.inv_vp,
cam_pos: [params.cam_pos[0], params.cam_pos[1], params.cam_pos[2], 0.0],
viewport: [
self.hdr_targets.width as f32,
self.hdr_targets.height as f32,
],
time: params.elapsed,
prefilter_mip_count: self.env_map.prefilter_mip_count as f32,
}
}
fn encode_pass_into(
&self,
pass_id: PassId,
cmd_buf: &ProtocolObject<dyn MTLCommandBuffer>,
params: &GraphFrameParams<'_>,
particle_frame: Option<&super::particle::ParticleFrame>,
) -> Result<u32, String> {
Ok(match pass_id {
PassId::Cull => {
let object_buffer = params.object_buffer.ok_or(
"graph executor: Cull pass requires object_buffer but none was supplied",
)?;
let draw_args_buffer = params.draw_args_buffer.ok_or(
"graph executor: Cull pass requires draw_args_buffer but none was supplied",
)?;
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 = params.object_buffer.ok_or(
"graph executor: Cull2 pass requires object_buffer but none was supplied",
)?;
let draw_args_buffer = params.draw_args_buffer.ok_or(
"graph executor: Cull2 pass requires draw_args_buffer but none was supplied",
)?;
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.view.matrix,
cam_pos: params.cam_pos,
},
crate::metal::draw::main::GpuFrameBuffers {
object_buffer: params.object_buffer,
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.skinned_joint_bufs,
params.cam_pos,
params.object_buffer,
params.deformed_skinned,
raymarch_view.as_ref(),
)?
}
PassId::SpotShadow => {
self.encode_spot_shadow_pass(cmd_buf, params.skinned_joint_bufs, params.cam_pos)?
}
PassId::Main => self.encode_main_pass(
cmd_buf,
crate::metal::draw::main::MainPassCamera {
elapsed: params.elapsed,
vp: params.vp,
view: self.view.matrix,
cam_pos: params.cam_pos,
},
crate::metal::draw::main::DrawInputs {
visible: params.visible,
prepared_instances: params.prepared_instances,
skinned_joint_bufs: params.skinned_joint_bufs,
},
crate::metal::draw::main::GpuFrameBuffers {
object_buffer: params.object_buffer,
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)?,
PassId::Bloom => {
let scene_color = params.scene_color.ok_or(
"graph executor: Bloom pass requires scene_color but none was supplied",
)?;
self.encode_bloom(cmd_buf, scene_color)?
}
PassId::Velocity | PassId::SsrPrepass => {
return Err(format!(
"graph executor (metal): pass {} is merged into GBufferPrepass \
and should not appear in the frame graph",
pass_id.name()
));
}
PassId::GBufferPrepass => {
let gview = match params.vel_uniforms {
Some(v) => GBufferView {
jittered_vp: v.jittered_vp,
cur_vp: v.cur_vp,
prev_vp: v.prev_vp,
view: self.view.matrix,
},
None => GBufferView {
jittered_vp: params.vp,
cur_vp: params.vp,
prev_vp: params.vp,
view: self.view.matrix,
},
};
self.encode_gbuffer_prepass(
cmd_buf,
&gview,
crate::metal::post::gbuffer::GbufferSceneInputs {
visible: params.visible,
cam_pos: params.cam_pos,
prepared_instances: params.prepared_instances,
cur_joint_bufs: params.skinned_joint_bufs,
prev_joint_bufs: params.prev_skinned_joint_bufs,
},
crate::metal::post::gbuffer::GbufferGpuBuffers {
object_buffer: params.object_buffer,
prev_model_buffer: params.prev_model_buffer,
deformed_current: params.deformed_skinned,
deformed_prev: params.deformed_prev,
},
params.vel_uniforms.is_some(),
)?
}
PassId::TaaResolve => {
let taa_uniforms = params.taa_uniforms.ok_or(
"graph executor: TaaResolve pass requires taa_uniforms but none was supplied",
)?;
let scene_pre_taa = params.scene_pre_taa.ok_or(
"graph executor: TaaResolve pass requires scene_pre_taa but none was supplied",
)?;
self.encode_taa(cmd_buf, taa_uniforms, scene_pre_taa)?
}
PassId::SsrResolve => {
let ssr_params = params.ssr_params.ok_or(
"graph executor: SsrResolve pass requires ssr_params but none was supplied",
)?;
self.encode_ssr_resolve(cmd_buf, ssr_params)?
}
PassId::Ssgi => {
let ssgi_params = params.ssgi_params.ok_or(
"graph executor: Ssgi pass requires ssgi_params but none was supplied",
)?;
self.encode_ssgi(cmd_buf, ssgi_params)?
}
PassId::RtReflections => {
let rt_params = params.rt_reflection_params.ok_or(
"graph executor: RtReflections pass requires rt_reflection_params but none was supplied",
)?;
self.encode_rt_reflections(cmd_buf, rt_params, params.bindless_tex_args)?
}
PassId::SsaoBlur => {
let ssao_params = params.ssao_params.ok_or(
"graph executor: SsaoBlur pass requires ssao_params but none was supplied",
)?;
self.encode_ssao(cmd_buf, ssao_params)?
}
PassId::SsaoPrepass | PassId::SsaoKernel => {
return Err(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::ReflectionComposite => {
return Err(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 = params
.fog_params
.ok_or("graph executor: Fog pass requires fog_params but none was supplied")?;
let fog_froxel_params = params.fog_froxel_params.ok_or(
"graph executor: Fog pass requires fog_froxel_params but none was supplied",
)?;
self.encode_fog(cmd_buf, fog_params, fog_froxel_params)?
}
PassId::FogFroxel => {
let fog_params = params.fog_params.ok_or(
"graph executor: FogFroxel pass requires fog_params but none was supplied",
)?;
let fog_froxel_params = params.fog_froxel_params.ok_or(
"graph executor: FogFroxel pass requires fog_froxel_params but none was supplied",
)?;
self.encode_fog_froxel(cmd_buf, fog_params, fog_froxel_params)?
}
PassId::LightCull => {
let cluster_params = params.cluster_params.ok_or(
"graph executor: LightCull pass requires cluster_params but none was supplied",
)?;
self.encode_light_cull(cmd_buf, cluster_params)?
}
PassId::ParticlesDraw => {
if let Some(frame) = particle_frame {
self.encode_particles(cmd_buf, frame, params.vp, params.frustum)?
} else {
0
}
}
PassId::Lines => self.encode_lines(cmd_buf, params.vp, params.lines)?,
PassId::Composite => {
let scene_color = params.scene_color.ok_or(
"graph executor: Composite pass requires scene_color but none was supplied",
)?;
self.encode_composite_and_text(cmd_buf, scene_color, params.text_calls)?
}
PassId::ParticlesSim => {
return Err(format!(
"graph executor: pass {} is bundled inside ParticlesDraw \
(encode_particles encodes both); it should not appear as \
its own graph node",
pass_id.name()
));
}
PassId::Upscale => {
let scene_pre_taa = params.scene_pre_taa.ok_or(
"graph executor: Upscale pass requires scene_pre_taa but none was supplied",
)?;
self.encode_upscale(cmd_buf, scene_pre_taa)?
}
PassId::Transparent => {
let scene_pre_taa = params.scene_pre_taa.ok_or(
"graph executor: Transparent pass requires scene_pre_taa but none was supplied",
)?;
let inv_vp = params.inv_vp;
let view = concinnity_render::uniforms::TransparentView {
vp: params.vp,
inv_vp,
camera_pos: [params.cam_pos[0], params.cam_pos[1], params.cam_pos[2], 0.0],
viewport: [
self.hdr_targets.width as f32,
self.hdr_targets.height as f32,
],
time: params.elapsed,
prefilter_mip_count: self.env_map.prefilter_mip_count as f32,
};
let planar_live = crate::gfx::planar_reflection::planar_pass_needed(
self.planar_reflection
.as_ref()
.is_some_and(|s| !s.targets.is_empty()),
self.water_planar_slot_live(),
self.rt_transparent_active(),
);
if planar_live {
self.encode_planar_reflections(cmd_buf, params)?;
}
let mut draws = Vec::new();
self.collect_water_transparent_draws(
&view,
params.bindless_tex_args.is_some(),
planar_live,
&mut draws,
);
self.collect_glass_transparent_draws(
&view,
params.bindless_tex_args.is_some(),
planar_live,
&mut draws,
);
self.collect_mesh_transparent_draws(
&view,
params.bindless_tex_args.is_some(),
&mut draws,
);
self.encode_transparent(
cmd_buf,
&view,
scene_pre_taa,
&draws,
params.rt_reflection_params,
params.bindless_tex_args,
)?
}
PassId::Raymarch => {
let view = self.build_raymarch_view(params);
self.encode_raymarch(cmd_buf, &view, params.frustum)?
}
})
}
}