#![deny(unsafe_op_in_unsafe_fn)]
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{
MTLDevice as _, MTLLoadAction, MTLPixelFormat, MTLRenderPipelineState, MTLTexture,
MTLTextureUsage,
};
use crate::metal::context::MtlContext;
use crate::metal::descriptors::TextureDesc;
use crate::metal::encode::RenderEncode;
use crate::metal::post::fullscreen::{
FullscreenBlend, FullscreenPass, PassTimer, build_slang_fullscreen_pipeline,
};
use crate::metal::slang_shaders::{BLOOM_DOWNSAMPLE, BLOOM_PREFILTER, BLOOM_UPSAMPLE, SlangLib};
pub(crate) const BLOOM_FORMAT: MTLPixelFormat = MTLPixelFormat::RGBA16Float;
pub(crate) struct BloomPipelines {
pub prefilter: Retained<ProtocolObject<dyn MTLRenderPipelineState>>,
pub downsample: Retained<ProtocolObject<dyn MTLRenderPipelineState>>,
pub upsample: Retained<ProtocolObject<dyn MTLRenderPipelineState>>,
}
pub(crate) fn build_bloom_pipelines(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
hot_reload: bool,
) -> Result<BloomPipelines, String> {
let build = |lib: &SlangLib, blend: FullscreenBlend| {
build_slang_fullscreen_pipeline(device, lib, BLOOM_FORMAT, blend, hot_reload)
};
Ok(BloomPipelines {
prefilter: build(&BLOOM_PREFILTER, FullscreenBlend::Replace)?,
downsample: build(&BLOOM_DOWNSAMPLE, FullscreenBlend::Replace)?,
upsample: build(&BLOOM_UPSAMPLE, FullscreenBlend::Additive)?,
})
}
pub(crate) struct BloomTargets {
pub mips: Vec<Retained<ProtocolObject<dyn MTLTexture>>>,
pub width: u32,
pub height: u32,
}
fn bloom_mip_count(width: u32, height: u32) -> u32 {
let min_dim = width.min(height).max(1);
let levels = (min_dim as f32).log2().floor() as i32 - 1;
levels.clamp(4, 6) as u32
}
pub(crate) fn create_bloom_targets(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
width: u32,
height: u32,
bloom_top: Retained<ProtocolObject<dyn MTLTexture>>,
) -> Result<BloomTargets, String> {
let full_w = width.max(1);
let full_h = height.max(1);
let count = bloom_mip_count(full_w, full_h);
let mut mips = Vec::with_capacity(count as usize);
mips.push(bloom_top);
for i in 1..count {
let mw = (full_w >> (i + 1)).max(1) as usize;
let mh = (full_h >> (i + 1)).max(1) as usize;
let desc = TextureDesc {
format: BLOOM_FORMAT,
width: mw,
height: mh,
usage: MTLTextureUsage(MTLTextureUsage::ShaderRead.0 | MTLTextureUsage::RenderTarget.0),
..Default::default()
}
.build();
let tex = device
.newTextureWithDescriptor(&desc)
.ok_or_else(|| format!("failed to create bloom mip {} texture", i))?;
mips.push(tex);
}
Ok(BloomTargets {
mips,
width: full_w,
height: full_h,
})
}
impl MtlContext {
pub(in crate::metal) fn encode_bloom(
&self,
cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
scene_color: &ProtocolObject<dyn objc2_metal::MTLTexture>,
) -> Result<u32, String> {
let Some(bloom_pipelines) = &self.bloom_pipelines else {
return Ok(0);
};
let mips = &self.bloom_targets.mips;
let n = mips.len();
let prefilter_timer = if n <= 1 {
PassTimer::Whole(crate::metal::pass_timing::PassId::Bloom)
} else {
PassTimer::First(crate::metal::pass_timing::PassId::Bloom)
};
self.fullscreen_pass(
cmd_buf,
FullscreenPass {
target: mips[0].as_ref(),
load: MTLLoadAction::DontCare,
timer: prefilter_timer,
pipeline: &bloom_pipelines.prefilter,
label: "bloom prefilter",
},
|enc| {
enc.set_fragment_texture(scene_color, 0);
enc.set_fragment_sampler(&self.post_sampler, 0);
enc.set_fragment_value(&self.post_process, 0);
},
)?;
for i in 1..n {
self.fullscreen_pass(
cmd_buf,
FullscreenPass {
target: mips[i].as_ref(),
load: MTLLoadAction::DontCare,
timer: PassTimer::None,
pipeline: &bloom_pipelines.downsample,
label: "bloom downsample",
},
|enc| {
enc.set_fragment_texture(mips[i - 1].as_ref(), 0);
enc.set_fragment_sampler(&self.post_sampler, 0);
},
)?;
}
for i in (0..n - 1).rev() {
let timer = if i == 0 {
PassTimer::Last(crate::metal::pass_timing::PassId::Bloom)
} else {
PassTimer::None
};
self.fullscreen_pass(
cmd_buf,
FullscreenPass {
target: mips[i].as_ref(),
load: MTLLoadAction::Load,
timer,
pipeline: &bloom_pipelines.upsample,
label: "bloom upsample",
},
|enc| {
enc.set_fragment_texture(mips[i + 1].as_ref(), 0);
enc.set_fragment_sampler(&self.post_sampler, 0);
},
)?;
}
Ok(0)
}
}