#![deny(unsafe_op_in_unsafe_fn)]
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_foundation::NSRange;
use objc2_metal::{
MTLCommandBuffer as _, MTLComputeCommandEncoder as _, MTLComputePipelineState, MTLDevice,
MTLLibrary as _, MTLPixelFormat, MTLSize, MTLTexture, MTLTextureType, MTLTextureUsage,
};
use concinnity_core::render::reflection_probe::PrefilterPlan;
use super::allocator::{DeviceAllocator, PooledTexture};
use super::descriptors::TextureDesc;
use super::encode::ComputeEncode;
use super::pipeline::ns_str;
const PREFILTER_TILE: usize = 8;
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,
) -> Result<ProbePrefilterPipelines, String> {
Ok(ProbePrefilterPipelines {
mip0: build_kernel(
device,
&super::slang_shaders::PROBE_MIP0,
"probe_mip0",
hot_reload,
)?,
downsample: build_kernel(
device,
&super::slang_shaders::PROBE_DOWNSAMPLE,
"probe_downsample",
hot_reload,
)?,
ggx: build_kernel(
device,
&super::slang_shaders::PROBE_GGX,
"probe_ggx",
hot_reload,
)?,
})
}
}
fn build_kernel(
device: &ProtocolObject<dyn MTLDevice>,
lib: &super::slang_shaders::SlangLib,
entry: &str,
hot_reload: bool,
) -> Result<Retained<ProtocolObject<dyn MTLComputePipelineState>>, String> {
let library = lib.library(device, hot_reload)?;
let function = library
.newFunctionWithName(&ns_str(entry))
.ok_or_else(|| format!("{entry} not found in its probe prefilter library"))?;
device
.newComputePipelineStateWithFunction_error(&function)
.map_err(|e| format!("failed to create {entry} pipeline: {e:?}"))
}
pub(in crate::metal) fn create_capture_cube(
device: &ProtocolObject<dyn MTLDevice>,
plan: &PrefilterPlan,
) -> Result<Retained<ProtocolObject<dyn MTLTexture>>, String> {
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(|| "probe: failed to create capture cube".into())
}
pub(in crate::metal) struct PrefilterGpu {
capture: Retained<ProtocolObject<dyn MTLTexture>>,
capture_mip_views: Vec<Retained<ProtocolObject<dyn MTLTexture>>>,
probe: PooledTexture,
probe_mip_views: Vec<Retained<ProtocolObject<dyn MTLTexture>>>,
}
impl PrefilterGpu {
pub(in crate::metal) fn new(
alloc: &DeviceAllocator,
capture: Retained<ProtocolObject<dyn MTLTexture>>,
plan: &PrefilterPlan,
) -> Result<PrefilterGpu, String> {
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::ShaderRead.0 | MTLTextureUsage::ShaderWrite.0),
..Default::default()
}
.build();
let probe = alloc.alloc_texture(&desc)?;
let capture_mip_views = mip_array_views(&capture, plan.mips(), "capture")?;
let probe_mip_views = mip_array_views(&probe, plan.mips(), "probe")?;
Ok(PrefilterGpu {
capture,
capture_mip_views,
probe,
probe_mip_views,
})
}
pub(in crate::metal) fn into_probe_cube(self) -> PooledTexture {
self.probe
}
}
fn mip_array_views(
texture: &ProtocolObject<dyn MTLTexture>,
mips: u32,
label: &str,
) -> Result<Vec<Retained<ProtocolObject<dyn MTLTexture>>>, String> {
(0..mips)
.map(|mip| {
unsafe {
texture.newTextureViewWithPixelFormat_textureType_levels_slices(
PROBE_CUBE_FORMAT,
MTLTextureType::Type2DArray,
NSRange::new(mip as usize, 1),
NSRange::new(0, 6),
)
}
.ok_or_else(|| 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,
) -> Result<(), String> {
let pipelines = self
.probe
.prefilter
.as_ref()
.ok_or("probe: prefilter pipelines missing")?;
let enc = super::scoped_encoder::ScopedEncoder::new(
cmd_buf
.computeCommandEncoder()
.ok_or("probe: failed to get prefilter compute encoder")?,
"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,
) -> Result<(), String> {
let pipelines = self
.probe
.prefilter
.as_ref()
.ok_or("probe: prefilter pipelines missing")?;
let enc = super::scoped_encoder::ScopedEncoder::new(
cmd_buf
.computeCommandEncoder()
.ok_or("probe: failed to get prefilter compute encoder")?,
"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.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);
}