#![deny(unsafe_op_in_unsafe_fn)]
use concinnity_core::gfx::frustum::Frustum;
use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::reflection_probe::{
self, BakeAction, BakePhase, BakeSignals, PrefilterPlan,
};
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{
MTLBuffer as _, MTLCommandBuffer as _, MTLCommandQueue as _, MTLDevice as _, MTLPixelFormat,
MTLResourceOptions, MTLTexture, MTLTextureType, MTLTextureUsage,
};
use std::ptr::NonNull;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use super::context::MtlContext;
use super::descriptors::TextureDesc;
use super::error::allocation_failed;
use super::probe_prefilter::{PrefilterGpu, create_capture_cube};
pub(super) const PLAN: PrefilterPlan = PrefilterPlan::RUNTIME;
const PROBE_FACE_COUNT: usize = 6;
pub(in crate::metal) struct RenderingBake {
index: usize,
done: Arc<AtomicBool>,
cursor: usize,
eye: [f32; 3],
near: f32,
far: f32,
elapsed: f32,
gpu: BakeGpu,
counts: crate::metal::context::DrawRecordCounts,
}
pub(in crate::metal) struct PrefilteringBake {
index: usize,
gpu: PrefilterGpu,
cursor: u32,
}
#[expect(
dead_code,
reason = "payloads are held to defer their free, never read"
)]
pub(in crate::metal) enum RetiredBake {
Capture(BakeGpu),
Prefilter(PrefilterGpu),
CubeArray(super::probe_set::ProbeCubeArray),
}
pub(in crate::metal) struct BakeGpu {
msaa_color: Option<Retained<ProtocolObject<dyn MTLTexture>>>,
depth: Retained<ProtocolObject<dyn MTLTexture>>,
capture: Retained<ProtocolObject<dyn MTLTexture>>,
object_buffer: Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>,
draw_args: Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>,
joint_bufs: Vec<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>,
morph_weight_bufs: Vec<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>,
deformed: Option<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>,
}
impl MtlContext {
pub(in crate::metal) fn set_reflection_probes(
&mut self,
declared: &[reflection_probe::ProbePlacement],
) {
let placements = reflection_probe::resolve_placements(
declared,
self.draw.objects.iter().map(|o| (o.bb_min, o.bb_max)),
);
self.retire_in_flight_bakes();
let placements = match self.reserve_probe_cubes(&PLAN, placements.len()) {
Ok(()) => placements,
Err(e) => {
tracing::warn!("reflection probes: {e}; keeping the sky");
Vec::new()
}
};
self.probe.book.reset(placements);
}
fn bake_ring_slot(&self) -> usize {
self.frames_in_flight
}
fn retire_in_flight_bakes(&mut self) {
let frame = self.frame_ring_index;
if let Some(bake) = self.probe.rendering.take() {
self.probe
.retire_pool
.push(frame, RetiredBake::Capture(bake.gpu));
}
if let Some(bake) = self.probe.prefiltering.take() {
self.probe
.retire_pool
.push(frame, RetiredBake::Prefilter(bake.gpu));
}
}
pub(in crate::metal) fn advance_probe_prefilter(&mut self) {
self.probe
.retire_pool
.collect(self.frame_ring_index, self.frames_in_flight as u64);
if !self.cull.bindless || self.targets.geometry_less || self.probe.prefilter.is_none() {
self.retire_in_flight_bakes();
self.probe.book.abort();
return;
}
let prefiltering_occupied = self.probe.prefiltering.is_some();
let more_mips = self
.probe
.prefiltering
.as_ref()
.is_some_and(|p| p.cursor < PLAN.mips());
match reflection_probe::next_bake_action(
if prefiltering_occupied {
BakePhase::Prefiltering
} else {
BakePhase::Idle
},
BakeSignals {
more_mips,
mips_done: true,
..Default::default()
},
) {
BakeAction::PrefilterMip => {
if let Err(e) = self.probe_prefilter_next_mip() {
self.fail_bake(e);
}
}
BakeAction::Install => {
if let Err(e) = self.probe_install() {
self.fail_bake(e);
}
}
_ => {}
}
}
pub(in crate::metal) fn advance_probe_capture(
&mut self,
elapsed: f32,
near: f32,
far: f32,
tex_args: Option<&Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>,
) {
let prefiltering_free = self.probe.prefiltering.is_none();
let rendering_occupied = self.probe.rendering.is_some();
let more_faces = self
.probe
.rendering
.as_ref()
.is_some_and(|r| r.cursor < PROBE_FACE_COUNT);
let done = self
.probe
.rendering
.as_ref()
.is_some_and(|r| r.done.load(Ordering::Acquire));
let eligible = self.cull_count() > 0;
match reflection_probe::next_bake_action(
if rendering_occupied {
BakePhase::Rendering
} else {
BakePhase::Idle
},
BakeSignals {
faces_done: done && prefiltering_free,
queue_pending: self.probe.book.pending(),
eligible,
more_faces,
..Default::default()
},
) {
BakeAction::RenderFace => {
let face = tex_args
.ok_or_else(|| RenderError::Other("probe: no bindless texture args".into()))
.and_then(|tex_args| self.probe_render_next_face(tex_args));
if let Err(e) = face {
self.fail_bake(e);
}
}
BakeAction::StartPrefilter => {
if let Err(e) = self.probe_begin_prefilter() {
self.fail_bake(e);
}
}
BakeAction::StartNext => {
if let Err(e) = self.probe_start_next(near, far, elapsed) {
self.fail_bake(e);
}
}
BakeAction::PrefilterMip | BakeAction::Install | BakeAction::Idle => {}
}
}
fn fail_bake(&mut self, e: RenderError) {
tracing::warn!(
"reflection probe bake failed, keeping {} baked: {e}",
self.probe.book.count()
);
self.retire_in_flight_bakes();
self.probe.book.abort();
}
fn probe_start_next(&mut self, near: f32, far: f32, elapsed: f32) -> RenderResult<()> {
let Some((index, placement)) = self.probe.book.take_next() else {
return Ok(());
};
let eye = placement.position;
let slot = self.bake_ring_slot();
let object_buffer = self
.build_object_buffer(slot)?
.ok_or_else(|| RenderError::Other("probe: no static geometry to bake".into()))?;
let draw_args = self
.build_draw_args_buffer(
eye,
slot,
concinnity_core::render::model_history::HistoryMode::Untracked,
)?
.ok_or_else(|| RenderError::Other("probe: no draw args to bake".into()))?;
let counts = self.draw_record_counts();
let joint_bufs = self.build_joint_buffers(slot)?;
let morph_weight_bufs = self.build_morph_weight_buffers(slot)?;
let deformed: Option<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>> =
if self.draw.n_skinned > 0 {
match self.skinned.deformed.first().map(|b| b.length()) {
Some(len) if len > 0 => Some(
self.hw
.device
.newBufferWithLength_options(len, MTLResourceOptions::StorageModeShared)
.ok_or_else(|| allocation_failed("probe deformed buffer"))?,
),
_ => None,
}
} else {
None
};
let samples = self.targets.hdr.sample_count;
let msaa_color = (samples > 1)
.then(|| make_face_color(&self.hw.device, PLAN.face_size(), samples))
.transpose()?;
let depth = make_face_depth(&self.hw.device, PLAN.face_size(), samples)?;
let capture = create_capture_cube(&self.hw.device, &PLAN)?;
self.probe.rendering = Some(RenderingBake {
index,
done: Arc::new(AtomicBool::new(false)),
cursor: 0,
eye,
near,
far,
elapsed,
gpu: BakeGpu {
msaa_color,
depth,
capture,
object_buffer,
draw_args,
joint_bufs,
morph_weight_bufs,
deformed,
},
counts,
});
Ok(())
}
fn probe_render_next_face(
&mut self,
tex_args: &Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>,
) -> RenderResult<()> {
let Some(bake) = self.probe.rendering.as_ref() else {
return Err(RenderError::Other(
"probe: render face with no capture in flight".into(),
));
};
let (face, eye, near, far, elapsed, counts) = (
bake.cursor,
bake.eye,
bake.near,
bake.far,
bake.elapsed,
bake.counts,
);
let attach_done = face + 1 == PROBE_FACE_COUNT;
self.ensure_icb_capacity(counts.total)?;
let vp = reflection_probe::face_view_projection(eye, face, near, far);
let view = reflection_probe::face_view_matrix(eye, face);
let frustum = Frustum::from_view_projection(vp);
let RenderingBake { done, gpu, .. } = self
.probe
.rendering
.as_ref()
.expect("probe capture was just checked");
let cull_cb =
self.hw.command_queue.commandBuffer().ok_or_else(|| {
RenderError::Other("probe: failed to get cull command buffer".into())
})?;
if face == 0
&& let Some(def) = gpu.deformed.as_ref()
{
self.encode_main_skin(
&cull_cb,
def,
crate::metal::raytrace::MainSkinBuffers {
joints: &gpu.joint_bufs,
morph_weights: &gpu.morph_weight_bufs,
},
)?;
}
self.encode_cull(
&cull_cb,
&gpu.object_buffer,
&gpu.draw_args,
&frustum,
eye,
counts,
)?;
super::fault_log::attach_fault_logger(&cull_cb, "reflection probe cull");
cull_cb.commit();
let render_cb = self.hw.command_queue.commandBuffer().ok_or_else(|| {
RenderError::Other("probe: failed to get render command buffer".into())
})?;
self.encode_main_into_face(
&render_cb,
crate::metal::draw::main::FaceTargets {
color_msaa: gpu.msaa_color.as_deref(),
depth: &gpu.depth,
resolve: &gpu.capture,
resolve_slice: face,
},
crate::metal::draw::main::MainPassCamera {
elapsed,
vp,
view,
cam_pos: eye,
},
crate::metal::draw::main::GpuFrameBuffers {
object_buffer: Some(&gpu.object_buffer),
bindless_tex_args: Some(tex_args),
deformed_skinned: gpu.deformed.as_ref(),
counts,
},
None,
)?;
super::fault_log::attach_fault_logger(&render_cb, "reflection probe face");
if attach_done {
let flag = Arc::clone(done);
let handler = block2::RcBlock::new(
move |_: NonNull<ProtocolObject<dyn objc2_metal::MTLCommandBuffer>>| {
flag.store(true, Ordering::Release);
},
);
unsafe {
render_cb.addCompletedHandler(block2::RcBlock::as_ptr(&handler));
}
}
render_cb.commit();
if let Some(RenderingBake { cursor, .. }) = &mut self.probe.rendering {
*cursor += 1;
}
Ok(())
}
fn probe_begin_prefilter(&mut self) -> RenderResult<()> {
let RenderingBake { index, gpu, .. } =
self.probe.rendering.take().ok_or_else(|| {
RenderError::Other("probe: prefilter with no bake in flight".into())
})?;
let BakeGpu { capture, .. } = gpu;
let prefilter_gpu = PrefilterGpu::new(capture, self.probe.cubes.texture(), index, &PLAN)?;
let cmd_buf = self.hw.command_queue.commandBuffer().ok_or_else(|| {
RenderError::Other("probe: failed to get prefilter command buffer".into())
})?;
self.encode_probe_pyramid(&cmd_buf, &prefilter_gpu, &PLAN)?;
super::fault_log::attach_fault_logger(&cmd_buf, "reflection probe pyramid");
cmd_buf.commit();
self.probe.prefiltering = Some(PrefilteringBake {
index,
gpu: prefilter_gpu,
cursor: 1,
});
Ok(())
}
fn probe_prefilter_next_mip(&mut self) -> RenderResult<()> {
let (cursor, gpu) = {
let bake = self.probe.prefiltering.as_ref().ok_or_else(|| {
RenderError::Other("probe: convolve with no bake in flight".into())
})?;
(bake.cursor, &bake.gpu)
};
let cmd_buf = self.hw.command_queue.commandBuffer().ok_or_else(|| {
RenderError::Other("probe: failed to get convolution command buffer".into())
})?;
self.encode_probe_ggx_mip(&cmd_buf, gpu, &PLAN, cursor)?;
super::fault_log::attach_fault_logger(&cmd_buf, "reflection probe convolution");
cmd_buf.commit();
if let Some(bake) = self.probe.prefiltering.as_mut() {
bake.cursor += 1;
}
Ok(())
}
fn probe_install(&mut self) -> RenderResult<()> {
let PrefilteringBake { index, gpu, .. } =
self.probe.prefiltering.take().ok_or_else(|| {
RenderError::Other("probe: install with no bake in flight".into())
})?;
drop(gpu);
let progress = self.probe.book.install(index)?;
tracing::info!("reflection probes: {progress}");
Ok(())
}
}
fn make_face_color(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
size: u32,
sample_count: u32,
) -> RenderResult<Retained<ProtocolObject<dyn MTLTexture>>> {
let desc = TextureDesc {
kind: MTLTextureType::Type2DMultisample,
format: MTLPixelFormat::RGBA16Float,
width: size as usize,
height: size as usize,
sample_count: sample_count as usize,
usage: MTLTextureUsage::RenderTarget,
..Default::default()
}
.build();
device
.newTextureWithDescriptor(&desc)
.ok_or_else(|| allocation_failed("probe color face"))
}
fn make_face_depth(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
size: u32,
sample_count: u32,
) -> RenderResult<Retained<ProtocolObject<dyn MTLTexture>>> {
let desc = TextureDesc {
kind: if sample_count > 1 {
MTLTextureType::Type2DMultisample
} else {
MTLTextureType::Type2D
},
format: MTLPixelFormat::Depth32Float,
width: size as usize,
height: size as usize,
sample_count: sample_count as usize,
usage: MTLTextureUsage::RenderTarget,
..Default::default()
}
.build();
device
.newTextureWithDescriptor(&desc)
.ok_or_else(|| allocation_failed("probe depth face"))
}