#![deny(unsafe_op_in_unsafe_fn)]
use concinnity_core::gfx::auto_exposure;
use concinnity_core::gfx::auto_exposure::{AutoExposureSettings, ExposureAdaptation};
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::SsaoPass;
use concinnity_core::render::post::ssao::settings::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::context::{CompositeState, MtlSceneAssets};
use crate::metal::error::allocation_failed;
use crate::metal::post::post_device::MtlPostDevice;
use crate::metal::post::{
GBufferState, MetalFXUpscaler, SsaoState, SsgiState, SsrState, TaaState, UpscaleState,
build_reflection_composite, build_taa_pass, create_gbuffer_targets, create_reflection_target,
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: Default::default(),
}
}
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(
post_device: &MtlPostDevice,
alloc: &DeviceAllocator,
settings: &EffectSettings,
render: (u32, u32),
) -> RenderResult<SsaoState> {
let pass = match settings.ssao {
Some(_) => Some(SsaoPass::new(
post_device,
PostExtent {
width: render.0,
height: render.1,
},
)?),
None => None,
};
Ok(SsaoState {
settings: *settings.ssao,
pass,
white: create_fallback_texture(alloc)?,
})
}
pub(in crate::metal) fn build_ssr(
post_device: &MtlPostDevice,
settings: &EffectSettings,
render: (u32, u32),
) -> RenderResult<SsrState> {
let device = post_device.device;
let trace_scale = settings
.rt_reflection
.filter(|_| crate::metal::raytrace::raytracing_supported(device))
.map_or(1, |rt| rt.divisor.max(1));
let reflection = if settings.reflections() {
Some(create_reflection_target(
device,
render.0,
render.1,
trace_scale,
)?)
} else {
None
};
let resolve = if settings.ssr.is_some() {
Some(SsrPass::new(post_device)?)
} else {
None
};
let composite = if settings.ssr.is_some() || settings.rt_reflection.is_some() {
Some(build_reflection_composite(
post_device,
settings.reflection_blur_scale,
render,
)?)
} else {
None
};
Ok(SsrState {
settings: *settings.ssr,
reflection,
resolve,
composite,
trace_scale,
})
}
pub(in crate::metal) fn build_gbuffer(
device: &ProtocolObject<dyn MTLDevice>,
enabled: bool,
render: (u32, u32),
hot_reload: bool,
) -> RenderResult<GBufferState> {
let (targets, history_pipeline) = if enabled {
(
Some(create_gbuffer_targets(device, render.0, render.1)?),
Some(crate::metal::model_history::build_model_history_pipeline(
device, hot_reload,
)?),
)
} else {
(None, None)
};
Ok(GBufferState {
targets,
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, adaptation) =
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 adaptation = ExposureAdaptation::new(*ae_settings, settings.auto_exposure_bias_ev);
(Some(pipelines), Some(hist), outputs, Some(adaptation))
} else {
(None, None, Vec::new(), None)
};
Ok(AutoExposureGpu {
adaptation,
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"))
}
}