#![deny(unsafe_op_in_unsafe_fn)]
use concinnity_core::gfx::auto_exposure;
use concinnity_core::gfx::auto_exposure::ExposureAdaptation;
use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::uniforms::*;
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_foundation::ns_string;
use objc2_metal::{
MTLBuffer as _, MTLCommandBuffer as _, MTLComputeCommandEncoder as _, MTLComputePassDescriptor,
MTLComputePipelineState, MTLSize, MTLTexture as _,
};
use super::builtin_shaders::compute_pipeline;
use super::context::*;
use super::encode::ComputeEncode;
use super::scoped_encoder::ScopedEncoder;
pub(crate) struct AutoExposureGpu {
pub adaptation: Option<ExposureAdaptation>,
pub pipelines: Option<AutoExposurePipelines>,
pub histogram: Option<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>,
pub outputs: Vec<Retained<ProtocolObject<dyn objc2_metal::MTLBuffer>>>,
pub last_elapsed: f32,
}
impl MtlContext {
pub(super) fn update_auto_exposure(&mut self, elapsed: f32, slot: usize) {
let Some(adaptation) = self.auto_exposure.adaptation.as_mut() else {
return;
};
let Some(output_buf) = self.auto_exposure.outputs.get(slot) else {
return;
};
let avg_log_lum = unsafe {
let ptr = output_buf.contents().as_ptr() as *const f32;
ptr.read()
};
let dt = (elapsed - self.auto_exposure.last_elapsed).max(0.0);
self.auto_exposure.last_elapsed = elapsed;
self.post_process.exposure = adaptation.step(avg_log_lum, dt);
}
pub(in crate::metal) fn encode_auto_exposure(
&self,
cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
slot: usize,
) -> RenderResult<u32> {
let (Some(pipelines), Some(histogram), Some(output)) = (
self.auto_exposure.pipelines.as_ref(),
self.auto_exposure.histogram.as_ref(),
self.auto_exposure.outputs.get(slot),
) else {
return Ok(0);
};
let params = AutoExposureParams::HISTOGRAM;
let hdr_tex: &ProtocolObject<dyn objc2_metal::MTLTexture> =
self.targets.hdr.hdr_resolve.as_ref();
let tex_w = hdr_tex.width();
let tex_h = hdr_tex.height();
if tex_w == 0 || tex_h == 0 {
return Ok(0);
}
let ae_desc = MTLComputePassDescriptor::new();
if let Some(t) = &self.diagnostics.pass_timing {
t.attach_compute(&ae_desc, super::pass_timing::PassId::AutoExposure);
}
let enc = ScopedEncoder::new(
cmd_buf
.computeCommandEncoderWithDescriptor(&ae_desc)
.ok_or_else(|| {
RenderError::Other("failed to get auto-exposure compute encoder".into())
})?,
ns_string!("auto-exposure"),
);
enc.set_pipeline(&pipelines.build);
enc.set_texture(hdr_tex, 0);
enc.set_buffer(histogram, 0, 0);
enc.set_value(¶ms, 1);
let tg = MTLSize {
width: 16,
height: 16,
depth: 1,
};
let grid = MTLSize {
width: tex_w,
height: tex_h,
depth: 1,
};
enc.dispatchThreads_threadsPerThreadgroup(grid, tg);
enc.set_pipeline(&pipelines.average);
enc.set_buffer(histogram, 0, 0);
enc.set_buffer(output, 0, 1);
enc.set_value(¶ms, 2);
let avg_grid = MTLSize {
width: auto_exposure::HISTOGRAM_BINS,
height: 1,
depth: 1,
};
let avg_tg = MTLSize {
width: auto_exposure::HISTOGRAM_BINS,
height: 1,
depth: 1,
};
enc.dispatchThreads_threadsPerThreadgroup(avg_grid, avg_tg);
Ok(0)
}
}
pub(super) struct AutoExposurePipelines {
pub build: Retained<ProtocolObject<dyn MTLComputePipelineState>>,
pub average: Retained<ProtocolObject<dyn MTLComputePipelineState>>,
}
pub(super) fn build_auto_exposure_pipelines(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
hot_reload: bool,
) -> RenderResult<AutoExposurePipelines> {
Ok(AutoExposurePipelines {
build: compute_pipeline(
device,
&super::builtin_shaders::AUTO_EXPOSURE_BUILD,
hot_reload,
)?,
average: compute_pipeline(
device,
&super::builtin_shaders::AUTO_EXPOSURE_AVERAGE,
hot_reload,
)?,
})
}