#![deny(unsafe_op_in_unsafe_fn)]
use concinnity_core::gfx::auto_exposure;
use concinnity_core::gfx::auto_exposure::{AutoExposureSettings, AutoExposureState};
use concinnity_core::render::backend_init::PostSettings;
use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::post::device::PostExtent;
use concinnity_core::render::post::rt_reflections::RtReflectionSettings;
use concinnity_core::render::post::ssao::SsaoSettings;
use concinnity_core::render::post::ssgi::SsgiPass;
use concinnity_core::render::post::ssgi::settings::SsgiSettings;
use concinnity_core::render::post::ssr::SsrPass;
use concinnity_core::render::post::ssr::settings::SsrSettings;
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{MTLBuffer, MTLDevice, MTLResourceOptions};
use super::InitGpu;
use crate::metal::allocator::DeviceAllocator;
use crate::metal::auto_exposure::{AutoExposureGpu, build_auto_exposure_pipelines};
use crate::metal::builtin_shaders::{SSAO_BLUR, SSAO_KERNEL};
use crate::metal::context::{CompositeState, MtlSceneAssets};
use crate::metal::error::allocation_failed;
use crate::metal::post::post_device::MtlPostDevice;
use crate::metal::post::{
GBufferState, MetalFXUpscaler, ReflectionScales, SsaoState, SsgiState, SsrState, TaaState,
UpscaleState, build_gbuffer_bindless_pipeline, build_reflection_blur_pipeline,
build_reflection_composite_pipeline, build_ssao_pipeline, build_taa_pass,
create_gbuffer_targets, create_ssao_targets, create_ssr_targets, temporal_scaler_supported,
};
use crate::metal::texture::create_fallback_texture;
pub(in crate::metal) struct EffectSettings<'a> {
pub ssao: &'a Option<SsaoSettings>,
pub ssr: &'a Option<SsrSettings>,
pub ssgi: &'a Option<SsgiSettings>,
pub rt_reflection: &'a Option<RtReflectionSettings>,
pub auto_exposure: &'a Option<AutoExposureSettings>,
pub reflection_blur_scale: u32,
pub auto_exposure_bias_ev: f32,
}
impl<'a> EffectSettings<'a> {
pub(super) fn from_post(post: &'a PostSettings) -> Self {
Self {
ssao: &post.ssao,
ssr: &post.ssr,
ssgi: &post.ssgi,
rt_reflection: &post.rt_reflections,
auto_exposure: &post.auto_exposure,
reflection_blur_scale: post.reflection_blur_scale,
auto_exposure_bias_ev: post.auto_exposure_bias_ev,
}
}
fn reflections(&self) -> bool {
self.ssr.is_some() || self.ssgi.is_some() || self.rt_reflection.is_some()
}
pub(in crate::metal) fn gbuffer_needed(&self, needs_velocity: bool) -> bool {
self.reflections() || self.ssao.is_some() || needs_velocity
}
}
pub(super) fn build_upscale(
gpu: &InitGpu<'_>,
output: (u32, u32),
post: &PostSettings,
) -> UpscaleState {
let device = &*gpu.hw.device;
let upscaler = if post.temporal_upscaling {
if temporal_scaler_supported(device) {
match MetalFXUpscaler::new(device, output.0, output.1, post.upscale_scale) {
Ok(u) => {
tracing::info!(
"MetalFX: temporal upscaling on: render {}x{} → present {}x{} ({}x scale)",
u.input_width,
u.input_height,
u.output_width,
u.output_height,
(u.input_width as f32) / (u.output_width.max(1) as f32),
);
Some(u)
}
Err(e) => {
tracing::warn!(
"MetalFX: temporal scaler creation failed ({}); falling back to native resolution",
e
);
None
}
}
} else {
tracing::warn!(
"MetalFX: temporal scaler not supported on this GPU; falling back to native resolution"
);
None
}
} else {
None
};
let scale = if upscaler.is_some() {
post.upscale_scale
} else {
1.0
};
UpscaleState {
scaler: upscaler,
scale,
jitter: Default::default(),
reset_pending: std::sync::atomic::AtomicBool::new(true),
}
}
pub(super) fn post_device<'a>(
gpu: &InitGpu<'a>,
composite: &'a CompositeState,
scene: &'a MtlSceneAssets,
) -> MtlPostDevice<'a> {
MtlPostDevice {
device: &gpu.hw.device,
sampler: &composite.sampler,
cube_sampler: &scene.cube_sampler,
probes: None,
timing: None,
hot_reload: gpu.hot_reload,
}
}
pub(in crate::metal) fn build_taa(
post_device: &MtlPostDevice,
enabled: bool,
render: (u32, u32),
) -> RenderResult<TaaState> {
let pass = if enabled {
Some(build_taa_pass(post_device, render.0, render.1)?)
} else {
None
};
Ok(TaaState {
enabled,
pass,
frame: 0,
})
}
pub(in crate::metal) fn build_ssao(
alloc: &DeviceAllocator,
settings: &EffectSettings,
render: (u32, u32),
hot_reload: bool,
) -> RenderResult<SsaoState> {
let device = alloc.device();
let (ssao_targets, ssao_kernel_pipeline, ssao_blur_pipeline) = if settings.ssao.is_some() {
(
Some(create_ssao_targets(device, render.0, render.1)?),
Some(build_ssao_pipeline(device, &SSAO_KERNEL, hot_reload)?),
Some(build_ssao_pipeline(device, &SSAO_BLUR, hot_reload)?),
)
} else {
(None, None, None)
};
Ok(SsaoState {
settings: *settings.ssao,
targets: ssao_targets,
kernel_pipeline: ssao_kernel_pipeline,
blur_pipeline: ssao_blur_pipeline,
white: create_fallback_texture(alloc)?,
})
}
pub(in crate::metal) fn build_ssr(
post_device: &MtlPostDevice,
settings: &EffectSettings,
render: (u32, u32),
) -> RenderResult<SsrState> {
let (device, hot_reload) = (post_device.device, post_device.hot_reload);
let scales = ReflectionScales {
trace: settings
.rt_reflection
.filter(|_| crate::metal::raytrace::raytracing_supported(device))
.map_or(1, |rt| rt.divisor.max(1)),
blur: settings.reflection_blur_scale.max(1),
};
let (ssr_targets, ssr_resolve, ssr_composite_pipeline, ssr_blur_pipeline) =
if settings.reflections() {
let ssr_resolve = if settings.ssr.is_some() {
Some(SsrPass::new(post_device)?)
} else {
None
};
let (composite, blur) = if settings.ssr.is_some() || settings.rt_reflection.is_some() {
(
Some(build_reflection_composite_pipeline(device, hot_reload)?),
Some(build_reflection_blur_pipeline(device, hot_reload)?),
)
} else {
(None, None)
};
(
Some(create_ssr_targets(device, render.0, render.1, scales)?),
ssr_resolve,
composite,
blur,
)
} else {
(None, None, None, None)
};
Ok(SsrState {
settings: *settings.ssr,
targets: ssr_targets,
resolve: ssr_resolve,
composite_pipeline: ssr_composite_pipeline,
blur_pipeline: ssr_blur_pipeline,
scales,
})
}
pub(in crate::metal) fn build_gbuffer(
device: &ProtocolObject<dyn MTLDevice>,
enabled: bool,
render: (u32, u32),
hot_reload: bool,
) -> RenderResult<GBufferState> {
let (targets, bindless_pipeline, history_pipeline) = if enabled {
(
Some(create_gbuffer_targets(device, render.0, render.1)?),
Some(build_gbuffer_bindless_pipeline(device, hot_reload)?),
Some(crate::metal::model_history::build_model_history_pipeline(
device, hot_reload,
)?),
)
} else {
(None, None, None)
};
Ok(GBufferState {
targets,
bindless_pipeline,
history_pipeline,
})
}
pub(in crate::metal) fn build_ssgi(
post_device: &MtlPostDevice,
settings: &EffectSettings,
render: (u32, u32),
) -> RenderResult<SsgiState> {
let pass = match settings.ssgi {
Some(s) => Some(SsgiPass::new(
post_device,
s.gi_scale,
PostExtent {
width: render.0,
height: render.1,
},
)?),
None => None,
};
Ok(SsgiState {
settings: *settings.ssgi,
pass,
})
}
pub(in crate::metal) fn build_auto_exposure(
device: &ProtocolObject<dyn MTLDevice>,
settings: &EffectSettings,
frames_in_flight: usize,
hot_reload: bool,
) -> RenderResult<AutoExposureGpu> {
let (pipelines, histogram, outputs, state, bias_ev) =
if let Some(ae_settings) = settings.auto_exposure.as_ref() {
let pipelines = build_auto_exposure_pipelines(device, hot_reload)?;
let hist = make_auto_exposure_histogram(device)?;
let outputs = (0..frames_in_flight.max(1))
.map(|_| make_auto_exposure_output(device))
.collect::<Result<Vec<_>, _>>()?;
let state = AutoExposureState::new(ae_settings);
(
Some(pipelines),
Some(hist),
outputs,
Some(state),
settings.auto_exposure_bias_ev,
)
} else {
(None, None, Vec::new(), None, 0.0)
};
Ok(AutoExposureGpu {
settings: *settings.auto_exposure,
state,
bias_ev,
pipelines,
histogram,
outputs,
last_elapsed: 0.0,
})
}
fn make_auto_exposure_histogram(
device: &ProtocolObject<dyn MTLDevice>,
) -> RenderResult<Retained<ProtocolObject<dyn MTLBuffer>>> {
let hist_bytes = vec![0u8; std::mem::size_of::<u32>() * auto_exposure::HISTOGRAM_BINS];
unsafe {
let ptr = std::ptr::NonNull::new(hist_bytes.as_ptr() as *mut _).ok_or_else(|| {
RenderError::Other("auto-exposure histogram bytes pointer is null".to_string())
})?;
device
.newBufferWithBytes_length_options(
ptr,
hist_bytes.len(),
MTLResourceOptions::StorageModeShared,
)
.ok_or_else(|| allocation_failed("auto-exposure histogram buffer"))
}
}
fn make_auto_exposure_output(
device: &ProtocolObject<dyn MTLDevice>,
) -> RenderResult<Retained<ProtocolObject<dyn MTLBuffer>>> {
let out_bytes = vec![0u8; std::mem::size_of::<f32>()];
unsafe {
let ptr = std::ptr::NonNull::new(out_bytes.as_ptr() as *mut _).ok_or_else(|| {
RenderError::Other("auto-exposure output bytes pointer is null".to_string())
})?;
device
.newBufferWithBytes_length_options(
ptr,
out_bytes.len(),
MTLResourceOptions::StorageModeShared,
)
.ok_or_else(|| allocation_failed("auto-exposure output buffer"))
}
}