#![deny(unsafe_op_in_unsafe_fn)]
use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::reflection_probe::PrefilterPlan;
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_foundation::NSRange;
use objc2_foundation::ns_string;
use objc2_metal::{
MTLCommandBuffer as _, MTLComputeCommandEncoder as _, MTLComputePipelineState, MTLDevice,
MTLPixelFormat, MTLSize, MTLTexture, MTLTextureType, MTLTextureUsage,
};
use super::builtin_shaders::compute_pipeline;
use super::descriptors::TextureDesc;
use super::encode::ComputeEncode;
use super::error::allocation_failed;
const PREFILTER_TILE: usize = 8;
pub(in crate::metal) const PROBE_CUBE_FORMAT: MTLPixelFormat = MTLPixelFormat::RGBA16Float;
pub(in crate::metal) struct ProbePrefilterPipelines {
mip0: Retained<ProtocolObject<dyn MTLComputePipelineState>>,
downsample: Retained<ProtocolObject<dyn MTLComputePipelineState>>,
ggx: Retained<ProtocolObject<dyn MTLComputePipelineState>>,
}
impl ProbePrefilterPipelines {
pub(in crate::metal) fn new(
device: &ProtocolObject<dyn MTLDevice>,
hot_reload: bool,
) -> RenderResult<ProbePrefilterPipelines> {
Ok(ProbePrefilterPipelines {
mip0: compute_pipeline(device, &super::builtin_shaders::PROBE_MIP0, hot_reload)?,
downsample: compute_pipeline(
device,
&super::builtin_shaders::PROBE_DOWNSAMPLE,
hot_reload,
)?,
ggx: compute_pipeline(device, &super::builtin_shaders::PROBE_GGX, hot_reload)?,
})
}
}
pub(in crate::metal) fn create_capture_cube(
device: &ProtocolObject<dyn MTLDevice>,
plan: &PrefilterPlan,
) -> RenderResult<Retained<ProtocolObject<dyn MTLTexture>>> {
let desc = TextureDesc {
kind: MTLTextureType::TypeCube,
format: PROBE_CUBE_FORMAT,
width: plan.face_size() as usize,
height: plan.face_size() as usize,
mip_count: plan.mips() as usize,
usage: MTLTextureUsage(
MTLTextureUsage::RenderTarget.0
| MTLTextureUsage::ShaderRead.0
| MTLTextureUsage::ShaderWrite.0,
),
..Default::default()
}
.build();
device
.newTextureWithDescriptor(&desc)
.ok_or_else(|| allocation_failed("probe capture cube"))
}
pub(in crate::metal) struct PrefilterGpu {
capture: Retained<ProtocolObject<dyn MTLTexture>>,
capture_mip_views: Vec<Retained<ProtocolObject<dyn MTLTexture>>>,
probe_mip_views: Vec<Retained<ProtocolObject<dyn MTLTexture>>>,
}
impl PrefilterGpu {
pub(in crate::metal) fn new(
capture: Retained<ProtocolObject<dyn MTLTexture>>,
cubes: &ProtocolObject<dyn MTLTexture>,
slot: usize,
plan: &PrefilterPlan,
) -> RenderResult<PrefilterGpu> {
let capture_mip_views = mip_array_views(&capture, 0, plan.mips(), "capture")?;
let probe_mip_views = mip_array_views(cubes, slot, plan.mips(), "probe")?;
Ok(PrefilterGpu {
capture,
capture_mip_views,
probe_mip_views,
})
}
}
fn mip_array_views(
texture: &ProtocolObject<dyn MTLTexture>,
cube: usize,
mips: u32,
label: &str,
) -> RenderResult<Vec<Retained<ProtocolObject<dyn MTLTexture>>>> {
let slices = texture.arrayLength() * 6;
if (cube + 1) * 6 > slices || mips as usize > texture.mipmapLevelCount() {
return Err(RenderError::Other(format!(
"probe: {label} has no cube {cube} at {mips} mips"
)));
}
(0..mips)
.map(|mip| {
unsafe {
texture.newTextureViewWithPixelFormat_textureType_levels_slices(
PROBE_CUBE_FORMAT,
MTLTextureType::Type2DArray,
NSRange::new(mip as usize, 1),
NSRange::new(cube * 6, 6),
)
}
.ok_or_else(|| {
RenderError::Other(format!("probe: failed to create {label} mip {mip} view"))
})
})
.collect()
}
impl super::context::MtlContext {
pub(in crate::metal) fn encode_probe_pyramid(
&self,
cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
gpu: &PrefilterGpu,
plan: &PrefilterPlan,
) -> RenderResult<()> {
let pipelines = self
.probe
.prefilter
.as_ref()
.ok_or_else(|| RenderError::Other("probe: prefilter pipelines missing".into()))?;
let enc = super::scoped_encoder::ScopedEncoder::new(
cmd_buf.computeCommandEncoder().ok_or_else(|| {
RenderError::Other("probe: failed to get prefilter compute encoder".into())
})?,
ns_string!("probe-pyramid"),
);
let params = plan.mip0_params();
enc.set_pipeline(&pipelines.mip0);
enc.set_value(¶ms, 0);
enc.set_texture(gpu.capture_mip_views[0].as_ref(), 0);
enc.set_texture(gpu.probe_mip_views[0].as_ref(), 1);
dispatch_cube(&enc, plan.face_size());
for mip in 1..plan.mips() {
let params = plan.downsample_params(mip);
enc.set_pipeline(&pipelines.downsample);
enc.set_value(¶ms, 0);
enc.set_texture(gpu.capture_mip_views[(mip - 1) as usize].as_ref(), 0);
enc.set_texture(gpu.capture_mip_views[mip as usize].as_ref(), 1);
dispatch_cube(&enc, plan.mip_face_size(mip));
}
Ok(())
}
pub(in crate::metal) fn encode_probe_ggx_mip(
&self,
cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
gpu: &PrefilterGpu,
plan: &PrefilterPlan,
dst_mip: u32,
) -> RenderResult<()> {
let pipelines = self
.probe
.prefilter
.as_ref()
.ok_or_else(|| RenderError::Other("probe: prefilter pipelines missing".into()))?;
let enc = super::scoped_encoder::ScopedEncoder::new(
cmd_buf.computeCommandEncoder().ok_or_else(|| {
RenderError::Other("probe: failed to get prefilter compute encoder".into())
})?,
ns_string!("probe-ggx"),
);
let params = plan.ggx_params(dst_mip);
enc.set_pipeline(&pipelines.ggx);
enc.set_value(¶ms, 0);
enc.set_texture(gpu.capture.as_ref(), 0);
enc.set_sampler(&self.scene.cube_sampler, 0);
enc.set_texture(gpu.probe_mip_views[dst_mip as usize].as_ref(), 1);
dispatch_cube(&enc, plan.mip_face_size(dst_mip));
Ok(())
}
}
fn dispatch_cube(enc: &ProtocolObject<dyn objc2_metal::MTLComputeCommandEncoder>, size: u32) {
let grid = MTLSize {
width: size.max(1) as usize,
height: size.max(1) as usize,
depth: 6,
};
let tg = MTLSize {
width: PREFILTER_TILE,
height: PREFILTER_TILE,
depth: 1,
};
enc.dispatchThreads_threadsPerThreadgroup(grid, tg);
}