#![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 as _,
MTLLibrary as _, MTLPixelFormat, MTLSize, MTLTexture, MTLTextureType, MTLTextureUsage,
};
use super::context::{HDR_SAMPLE_COUNT, MtlContext};
use super::descriptors::TextureDesc;
use super::encode::ComputeEncode;
use super::pipeline::ns_str;
use super::scoped_encoder::ScopedEncoder;
use concinnity_core::render::uniforms::HizParams;
const HIZ_TILE: usize = 8;
pub(super) fn hiz_mip_count(width: u32, height: u32) -> u32 {
let m = width.max(height).max(1);
32 - m.leading_zeros()
}
pub(super) struct HiZResources {
pub(super) init_pipeline: Retained<ProtocolObject<dyn MTLComputePipelineState>>,
pub(super) downsample_pipeline: Retained<ProtocolObject<dyn MTLComputePipelineState>>,
pub(super) texture: Retained<ProtocolObject<dyn MTLTexture>>,
pub(super) mip_views: Vec<Retained<ProtocolObject<dyn MTLTexture>>>,
pub(super) width: u32,
pub(super) height: u32,
pub(super) mip_count: u32,
}
type HizPipelines = (
Retained<ProtocolObject<dyn MTLComputePipelineState>>,
Retained<ProtocolObject<dyn MTLComputePipelineState>>,
);
pub(super) fn build_hiz_pipelines(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
hot_reload: bool,
) -> Result<HizPipelines, String> {
let init_lib = super::slang_shaders::HIZ_INIT_MSAA.library(device, hot_reload)?;
let downsample_lib = super::slang_shaders::HIZ_DOWNSAMPLE.library(device, hot_reload)?;
let init_fn = init_lib
.newFunctionWithName(&ns_str("hiz_init_msaa"))
.ok_or("hiz_init_msaa not found in hiz library")?;
let downsample_fn = downsample_lib
.newFunctionWithName(&ns_str("hiz_downsample"))
.ok_or("hiz_downsample not found in hiz library")?;
let init_pipeline = device
.newComputePipelineStateWithFunction_error(&init_fn)
.map_err(|e| format!("failed to create hiz_init_msaa pipeline: {:?}", e))?;
let downsample_pipeline = device
.newComputePipelineStateWithFunction_error(&downsample_fn)
.map_err(|e| format!("failed to create hiz_downsample pipeline: {:?}", e))?;
Ok((init_pipeline, downsample_pipeline))
}
type HizTextureAndViews = (
Retained<ProtocolObject<dyn MTLTexture>>,
Vec<Retained<ProtocolObject<dyn MTLTexture>>>,
);
fn create_hiz_texture_and_views(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
width: u32,
height: u32,
mip_count: u32,
) -> Result<HizTextureAndViews, String> {
let desc = TextureDesc {
format: MTLPixelFormat::R32Float,
width: width.max(1) as usize,
height: height.max(1) as usize,
mip_count: mip_count.max(1) as usize,
usage: MTLTextureUsage(MTLTextureUsage::ShaderRead.0 | MTLTextureUsage::ShaderWrite.0),
..Default::default()
}
.build();
let texture = device
.newTextureWithDescriptor(&desc)
.ok_or("failed to create hiz texture")?;
let mut mip_views = Vec::with_capacity(mip_count as usize);
for mip in 0..mip_count {
let view = unsafe {
texture.newTextureViewWithPixelFormat_textureType_levels_slices(
MTLPixelFormat::R32Float,
MTLTextureType::Type2D,
NSRange::new(mip as usize, 1),
NSRange::new(0, 1),
)
}
.ok_or_else(|| format!("failed to create hiz mip {} view", mip))?;
mip_views.push(view);
}
Ok((texture, mip_views))
}
impl HiZResources {
pub(super) fn new(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
width: u32,
height: u32,
hot_reload: bool,
) -> Result<Self, String> {
let mip_count = hiz_mip_count(width, height);
let (init_pipeline, downsample_pipeline) = build_hiz_pipelines(device, hot_reload)?;
let (texture, mip_views) = create_hiz_texture_and_views(device, width, height, mip_count)?;
Ok(Self {
init_pipeline,
downsample_pipeline,
texture,
mip_views,
width,
height,
mip_count,
})
}
pub(super) fn resize_to(
&mut self,
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
width: u32,
height: u32,
) -> Result<(), String> {
let mip_count = hiz_mip_count(width, height);
let (texture, mip_views) = create_hiz_texture_and_views(device, width, height, mip_count)?;
self.texture = texture;
self.mip_views = mip_views;
self.width = width;
self.height = height;
self.mip_count = mip_count;
Ok(())
}
pub(super) fn swap_pipelines(
&mut self,
init_pipeline: Retained<ProtocolObject<dyn MTLComputePipelineState>>,
downsample_pipeline: Retained<ProtocolObject<dyn MTLComputePipelineState>>,
) {
self.init_pipeline = init_pipeline;
self.downsample_pipeline = downsample_pipeline;
}
}
impl MtlContext {
pub(in crate::metal) fn encode_hiz_build(
&self,
cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
) {
let Some(hiz) = self.cull.hiz.as_ref() else {
return;
};
if hiz.mip_count == 0 || hiz.mip_views.is_empty() {
return;
}
let depth: &ProtocolObject<dyn MTLTexture> = self.hdr_targets.depth.as_ref();
let Some(enc) = cmd_buf.computeCommandEncoder() else {
tracing::error!("hiz: failed to get compute encoder");
return;
};
let enc = ScopedEncoder::new(enc, "hiz-build");
let init_params = HizParams {
dst_width: hiz.width,
dst_height: hiz.height,
src_mip: 0,
sample_count: HDR_SAMPLE_COUNT,
};
enc.set_pipeline(&hiz.init_pipeline);
enc.set_value(&init_params, 0);
enc.set_texture(depth, 0);
enc.set_texture(hiz.mip_views[0].as_ref(), 1);
dispatch_2d(&enc, hiz.width, hiz.height);
let mut cur_w = hiz.width;
let mut cur_h = hiz.height;
for mip in 1..hiz.mip_count {
let next_w = (cur_w / 2).max(1);
let next_h = (cur_h / 2).max(1);
let params = HizParams {
dst_width: next_w,
dst_height: next_h,
src_mip: mip - 1,
sample_count: 0,
};
enc.set_pipeline(&hiz.downsample_pipeline);
enc.set_value(¶ms, 0);
enc.set_texture(hiz.mip_views[(mip - 1) as usize].as_ref(), 0);
enc.set_texture(hiz.mip_views[mip as usize].as_ref(), 1);
dispatch_2d(&enc, next_w, next_h);
cur_w = next_w;
cur_h = next_h;
}
}
}
fn dispatch_2d(enc: &ProtocolObject<dyn objc2_metal::MTLComputeCommandEncoder>, w: u32, h: u32) {
let grid = MTLSize {
width: w.max(1) as usize,
height: h.max(1) as usize,
depth: 1,
};
let tg = MTLSize {
width: HIZ_TILE,
height: HIZ_TILE,
depth: 1,
};
enc.dispatchThreads_threadsPerThreadgroup(grid, tg);
}
#[cfg(test)]
mod tests {
use super::hiz_mip_count;
#[test]
fn mip_count_power_of_two() {
assert_eq!(hiz_mip_count(1, 1), 1);
assert_eq!(hiz_mip_count(2, 2), 2);
assert_eq!(hiz_mip_count(256, 256), 9);
assert_eq!(hiz_mip_count(1024, 1024), 11);
}
#[test]
fn mip_count_uses_larger_dimension() {
assert_eq!(hiz_mip_count(1920, 1080), hiz_mip_count(1920, 1920));
assert_eq!(hiz_mip_count(1920, 1080), 11);
assert_eq!(hiz_mip_count(1280, 720), 11);
}
#[test]
fn mip_count_clamps_zero() {
assert_eq!(hiz_mip_count(0, 0), 1);
assert_eq!(hiz_mip_count(0, 8), 4);
}
}