#![deny(unsafe_op_in_unsafe_fn)]
use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::probe_bake::{
CAPTURE_FACES, ProbeBake, ProbeBakeDevice, capture_ring_slot,
};
use concinnity_core::render::probe_book::ProbeBook;
use concinnity_core::render::reflection_probe::{self, PrefilterPlan, ProbePlacement};
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;
pub(crate) struct RenderingBake {
done: Arc<AtomicBool>,
eye: [f32; 3],
capture_distance: Option<f32>,
elapsed: f32,
gpu: BakeGpu,
counts: crate::metal::context::DrawRecordCounts,
}
pub(crate) struct PrefilteringBake {
gpu: PrefilterGpu,
}
pub(in crate::metal) type MtlProbeBake = ProbeBake<RenderingBake, PrefilteringBake>;
pub(crate) struct ProbeFrame<'f> {
pub(in crate::metal) elapsed: f32,
pub(in crate::metal) tex_args: Option<&'f Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>,
}
#[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>>,
material_params: 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.state.draw.objects.iter().map(|o| (o.bb_min, o.bb_max)),
);
let placed = self.with_probe_bake(|bake, ctx| bake.place(ctx, placements));
crate::probe_report::report_probe_placement(placed);
}
fn bake_ring_slot(&self) -> usize {
capture_ring_slot(self.frames_in_flight)
}
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);
let report = self.with_probe_bake(|bake, ctx| bake.advance_prefilter(ctx));
crate::probe_report::report_probe_bake(report);
}
pub(in crate::metal) fn advance_probe_capture(&mut self, frame: &ProbeFrame<'_>) {
let report = self.with_probe_bake(|bake, ctx| bake.advance_capture(ctx, frame));
crate::probe_report::report_probe_bake(report);
}
fn with_probe_bake<R>(&mut self, f: impl FnOnce(&mut MtlProbeBake, &mut Self) -> R) -> R {
let mut bake = std::mem::take(&mut self.probe.bake);
let out = f(&mut bake, self);
self.probe.bake = bake;
out
}
fn start_probe_capture(
&mut self,
frame: &ProbeFrame<'_>,
placement: ProbePlacement,
) -> RenderResult<RenderingBake> {
let ProbeFrame { elapsed, .. } = *frame;
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 material_params = self.rings.material_params.buffer(&self.hw.device, slot)?;
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.state.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)?;
Ok(RenderingBake {
done: Arc::new(AtomicBool::new(false)),
eye,
capture_distance: placement.capture_distance,
elapsed,
gpu: BakeGpu {
msaa_color,
depth,
capture,
object_buffer,
material_params,
draw_args,
joint_bufs,
morph_weight_bufs,
deformed,
},
counts,
})
}
fn record_probe_face(
&mut self,
bake: &RenderingBake,
face: usize,
tex_args: &Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>,
) -> RenderResult<()> {
let (eye, elapsed, counts) = (bake.eye, bake.elapsed, bake.counts);
let attach_done = face + 1 == CAPTURE_FACES;
self.ensure_icb_capacity(counts.total)?;
let vp = reflection_probe::face_view_projection(eye, face);
let view = reflection_probe::face_view_matrix(eye, face);
let frustum = reflection_probe::face_frustum(eye, face, bake.capture_distance);
let RenderingBake { done, gpu, .. } = bake;
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),
material_params: Some(&gpu.material_params),
bindless_tex_args: Some(tex_args),
deformed_skinned: gpu.deformed.as_ref(),
counts,
},
crate::metal::draw::main::FacePass::PROBE,
)?;
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();
Ok(())
}
fn record_prefilter_mip(&self, bake: &PrefilteringBake, mip: u32) -> RenderResult<()> {
let cmd_buf = self.hw.command_queue.commandBuffer().ok_or_else(|| {
RenderError::Other("probe: failed to get convolution command buffer".into())
})?;
let label = if mip == 0 {
self.encode_probe_pyramid(&cmd_buf, &bake.gpu, &PLAN)?;
"reflection probe pyramid"
} else {
self.encode_probe_ggx_mip(&cmd_buf, &bake.gpu, &PLAN, mip)?;
"reflection probe convolution"
};
super::fault_log::attach_fault_logger(&cmd_buf, label);
cmd_buf.commit();
Ok(())
}
}
impl ProbeBakeDevice for MtlContext {
type Capture = RenderingBake;
type Prefilter = PrefilteringBake;
type Frame<'f> = ProbeFrame<'f>;
fn book(&mut self) -> &mut ProbeBook {
&mut self.probe.book
}
fn capture_supported(&self) -> bool {
self.cull.bindless && !self.targets.geometry_less && self.probe.prefilter.is_some()
}
fn capture_ready(&self, _prefilter_in_flight: bool) -> bool {
self.cull_count() > 0
}
fn reserve_cubes(&mut self, count: usize) -> RenderResult<()> {
self.reserve_probe_cubes(&PLAN, count)
}
fn start_capture(
&mut self,
frame: &ProbeFrame<'_>,
_index: usize,
placement: ProbePlacement,
) -> RenderResult<RenderingBake> {
self.start_probe_capture(frame, placement)
}
fn render_face(
&mut self,
frame: &ProbeFrame<'_>,
capture: &mut RenderingBake,
face: usize,
) -> RenderResult<()> {
let tex_args = frame
.tex_args
.ok_or_else(|| RenderError::Other("probe: no bindless texture args".into()))?;
self.record_probe_face(capture, face, tex_args)
}
fn capture_retired(&self, capture: &RenderingBake) -> bool {
capture.done.load(Ordering::Acquire)
}
fn begin_prefilter(
&mut self,
index: usize,
capture: RenderingBake,
) -> RenderResult<PrefilteringBake> {
let gpu = PrefilterGpu::new(
capture.gpu.capture,
self.probe.cubes.texture(),
index,
&PLAN,
)?;
Ok(PrefilteringBake { gpu })
}
fn prefilter_mip(&mut self, prefilter: &mut PrefilteringBake, mip: u32) -> RenderResult<()> {
self.record_prefilter_mip(prefilter, mip)
}
fn prefilter_retired(&self, _prefilter: &PrefilteringBake) -> bool {
true
}
fn finish_prefilter(&mut self, prefilter: PrefilteringBake) {
drop(prefilter);
}
fn abandon(&mut self, capture: Option<RenderingBake>, prefilter: Option<PrefilteringBake>) {
let frame = self.frame_ring_index;
if let Some(bake) = capture {
self.probe
.retire_pool
.push(frame, RetiredBake::Capture(bake.gpu));
}
if let Some(bake) = prefilter {
self.probe
.retire_pool
.push(frame, RetiredBake::Prefilter(bake.gpu));
}
}
}
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"))
}