#![deny(unsafe_op_in_unsafe_fn)]
use crate::metal::error::allocation_failed;
use concinnity_core::gfx::render_types;
use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::post::ssao::SsaoSettings;
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{
MTLDevice as _, MTLLoadAction, MTLPixelFormat, MTLRenderPipelineState, MTLTexture,
MTLTextureUsage,
};
use crate::metal::builtin_shaders::ShaderProgram;
use crate::metal::context::MtlContext;
use crate::metal::descriptors::TextureDesc;
use crate::metal::encode::RenderEncode;
use crate::metal::post::fullscreen::{
FullscreenBlend, FullscreenPass, PassTimer, build_fullscreen_pipeline,
set_fragment_sampler_range,
};
pub(crate) struct SsaoState {
pub settings: Option<SsaoSettings>,
pub targets: Option<SsaoTargets>,
pub kernel_pipeline: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub blur_pipeline: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub white: crate::metal::allocator::PooledTexture,
}
pub(crate) const SSAO_OCCLUSION_FORMAT: MTLPixelFormat = MTLPixelFormat::R8Unorm;
pub(crate) fn build_ssao_pipeline(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
fragment: &ShaderProgram,
hot_reload: bool,
) -> RenderResult<Retained<ProtocolObject<dyn MTLRenderPipelineState>>> {
build_fullscreen_pipeline(
device,
fragment,
SSAO_OCCLUSION_FORMAT,
FullscreenBlend::Replace,
hot_reload,
)
}
pub(crate) struct SsaoTargets {
pub ao_raw: Retained<ProtocolObject<dyn MTLTexture>>,
}
pub(crate) fn create_ssao_targets(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
width: u32,
height: u32,
) -> RenderResult<SsaoTargets> {
let w = width.max(1) as usize;
let h = height.max(1) as usize;
let sampled = MTLTextureUsage(MTLTextureUsage::ShaderRead.0 | MTLTextureUsage::RenderTarget.0);
let make = |label: &str| -> RenderResult<Retained<ProtocolObject<dyn MTLTexture>>> {
let desc = TextureDesc {
format: SSAO_OCCLUSION_FORMAT,
width: w,
height: h,
usage: sampled,
..Default::default()
}
.build();
device
.newTextureWithDescriptor(&desc)
.ok_or_else(|| allocation_failed(format_args!("SSAO {label} texture")))
};
let ao_raw = make("raw occlusion")?;
Ok(SsaoTargets { ao_raw })
}
impl MtlContext {
pub(in crate::metal) fn encode_ssao(
&self,
cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
ssao_params: &render_types::SsaoParams,
) -> RenderResult<u32> {
let (targets, kernel_ps, blur_ps, gbuffer) = match (
&self.ssao.targets,
&self.ssao.kernel_pipeline,
&self.ssao.blur_pipeline,
self.gbuffer_normal_depth(),
) {
(Some(t), Some(b), Some(c), Some(g)) => (t, b, c, g),
_ => return Ok(0),
};
let ao_output = self
.targets
.transient_pool
.texture_for("ao_output")
.ok_or_else(|| RenderError::Other("ao_output missing from transient pool".into()))?;
self.fullscreen_pass(
cmd_buf,
FullscreenPass {
target: targets.ao_raw.as_ref(),
load: MTLLoadAction::DontCare,
timer: PassTimer::Whole(crate::metal::pass_timing::PassId::SsaoKernel),
pipeline: kernel_ps,
label: "SSAO kernel",
},
|enc| {
enc.set_fragment_texture(gbuffer, 0);
set_fragment_sampler_range(enc, &self.composite.sampler, 0, 1);
enc.set_fragment_value(ssao_params, 0);
},
)?;
self.fullscreen_pass(
cmd_buf,
FullscreenPass {
target: ao_output,
load: MTLLoadAction::DontCare,
timer: PassTimer::Whole(crate::metal::pass_timing::PassId::SsaoBlur),
pipeline: blur_ps,
label: "SSAO blur",
},
|enc| {
enc.set_fragment_texture(targets.ao_raw.as_ref(), 0);
enc.set_fragment_texture(gbuffer, 1);
set_fragment_sampler_range(enc, &self.composite.sampler, 0, 2);
},
)?;
Ok(0)
}
}