#![deny(unsafe_op_in_unsafe_fn)]
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{
MTLBuffer, MTLDevice, MTLRenderPipelineState, MTLResourceOptions, MTLSamplerAddressMode,
MTLSamplerDescriptor, MTLSamplerMinMagFilter, MTLSamplerState, MTLTexture,
};
use crate::gfx::auto_exposure::{AutoExposureSettings, AutoExposureState};
use crate::gfx::decal::DecalRecord;
use crate::gfx::particles::ParticleEmitterRecord;
use crate::gfx::rt_reflections::RtReflectionSettings;
use crate::gfx::ssao::SsaoSettings;
use crate::gfx::ssgi::SsgiSettings;
use crate::gfx::ssr::SsrSettings;
use crate::gfx::volumetric_fog::FogSettings;
use crate::metal::allocator::DeviceAllocator;
use crate::metal::auto_exposure::{AutoExposurePipelines, build_auto_exposure_pipelines};
use crate::metal::decal::build_decal_pipeline;
use crate::metal::fog::build_fog_pipeline;
use crate::metal::particle::{
ParticleEmitterGpuState, ParticlePipelines, build_emitter_gpu_state, build_particle_pipelines,
};
use crate::metal::post::{
BloomPipelines, BloomTargets, GBufferState, SsaoState, SsgiState, SsrState,
build_bloom_pipelines, build_gbuffer_bindless_pipeline, build_reflection_blur_pipeline,
build_reflection_composite_pipeline, build_rt_reflection_pipeline, build_ssao_pipeline,
build_ssgi_composite_pipeline, build_ssgi_gather_pipeline, build_ssr_pipeline,
build_taa_pipeline, create_bloom_targets, create_gbuffer_targets, create_ssao_targets,
create_ssgi_targets, create_ssr_targets, create_taa_targets,
};
use crate::metal::slang_shaders::{SSAO_BLUR, SSAO_KERNEL};
use crate::metal::texture::create_fallback_texture;
use crate::metal::transient_pool::{TransientTexturePool, transient_slots};
pub(crate) 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,
}
#[derive(Clone, Copy)]
pub(crate) struct EffectFlags {
pub taa_enabled: bool,
pub needs_velocity: bool,
pub hot_reload: bool,
}
#[derive(Clone, Copy)]
pub(crate) struct EffectDimensions {
pub render_w: u32,
pub render_h: u32,
pub output_w: u32,
pub output_h: u32,
}
pub(crate) struct WorldContentEffects<'a> {
pub fog_settings: &'a Option<FogSettings>,
pub decals: &'a [DecalRecord],
pub particles: &'a [ParticleEmitterRecord],
pub frames_in_flight: usize,
}
pub(crate) struct EffectsBundle {
pub bloom_targets: BloomTargets,
pub bloom_pipelines: Option<BloomPipelines>,
pub taa_pipeline_state: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub taa_targets: Vec<Retained<ProtocolObject<dyn MTLTexture>>>,
pub ssao: SsaoState,
pub transient_pool: TransientTexturePool,
pub ssr: SsrState,
pub gbuffer: GBufferState,
pub ssgi: SsgiState,
pub rt_pipeline: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub rt_pipeline_textured: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub rt_skin_pipeline:
Option<Retained<ProtocolObject<dyn objc2_metal::MTLComputePipelineState>>>,
pub decal_pipeline: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub decal_cube_vertex_buffer: Option<Retained<ProtocolObject<dyn MTLBuffer>>>,
pub decal_cube_index_buffer: Option<Retained<ProtocolObject<dyn MTLBuffer>>>,
pub decal_sampler: Option<Retained<ProtocolObject<dyn MTLSamplerState>>>,
pub fog_pipeline: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub fog_froxel_pipeline:
Option<Retained<ProtocolObject<dyn objc2_metal::MTLComputePipelineState>>>,
pub fog_froxel_volume: Option<Retained<ProtocolObject<dyn objc2_metal::MTLTexture>>>,
pub particle_pipelines: Option<ParticlePipelines>,
pub particle_emitter_state: Vec<ParticleEmitterGpuState>,
pub auto_exposure_pipelines: Option<AutoExposurePipelines>,
pub auto_exposure_histogram: Option<Retained<ProtocolObject<dyn MTLBuffer>>>,
pub auto_exposure_output: Option<Retained<ProtocolObject<dyn MTLBuffer>>>,
pub auto_exposure_state: Option<AutoExposureState>,
pub auto_exposure_bias_ev: f32,
}
pub(crate) struct QualityEffectsBundle {
pub taa_pipeline_state: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub taa_targets: Vec<Retained<ProtocolObject<dyn MTLTexture>>>,
pub ssao: SsaoState,
pub transient_pool: TransientTexturePool,
pub ssr: SsrState,
pub gbuffer: GBufferState,
pub ssgi: SsgiState,
pub rt_pipeline: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub rt_pipeline_textured: Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub rt_skin_pipeline:
Option<Retained<ProtocolObject<dyn objc2_metal::MTLComputePipelineState>>>,
pub auto_exposure_pipelines: Option<AutoExposurePipelines>,
pub auto_exposure_histogram: Option<Retained<ProtocolObject<dyn MTLBuffer>>>,
pub auto_exposure_output: Option<Retained<ProtocolObject<dyn MTLBuffer>>>,
pub auto_exposure_state: Option<AutoExposureState>,
pub auto_exposure_bias_ev: f32,
}
pub(crate) fn build_quality_effects(
alloc: &DeviceAllocator,
dims: EffectDimensions,
settings: EffectSettings,
flags: EffectFlags,
) -> Result<QualityEffectsBundle, String> {
let device = alloc.device();
let EffectDimensions {
render_w,
render_h,
output_w,
output_h,
} = dims;
let EffectSettings {
ssao: ssao_settings,
ssr: ssr_settings,
ssgi: ssgi_settings,
rt_reflection: rt_reflection_settings,
auto_exposure: auto_exposure_settings,
reflection_blur_scale,
auto_exposure_bias_ev,
} = settings;
let EffectFlags {
taa_enabled,
needs_velocity,
hot_reload,
} = flags;
let (taa_pipeline_state, taa_targets) = if taa_enabled {
(
Some(build_taa_pipeline(device, hot_reload)?),
create_taa_targets(device, render_w, render_h)?.to_vec(),
)
} else {
(None, Vec::new())
};
let (ssao_targets, ssao_kernel_pipeline, ssao_blur_pipeline) = if ssao_settings.is_some() {
(
Some(create_ssao_targets(device, render_w, render_h)?),
Some(build_ssao_pipeline(device, &SSAO_KERNEL, hot_reload)?),
Some(build_ssao_pipeline(device, &SSAO_BLUR, hot_reload)?),
)
} else {
(None, None, None)
};
let ssao = SsaoState {
settings: *ssao_settings,
targets: ssao_targets,
kernel_pipeline: ssao_kernel_pipeline,
blur_pipeline: ssao_blur_pipeline,
white: create_fallback_texture(alloc)?,
};
let needs_ssr_prepass =
ssr_settings.is_some() || ssgi_settings.is_some() || rt_reflection_settings.is_some();
let needs_gbuffer = needs_ssr_prepass || ssao_settings.is_some() || needs_velocity;
let transient_pool = TransientTexturePool::build(
device,
&transient_slots(
ssao_settings.is_some(),
needs_gbuffer,
(render_w, render_h),
(output_w, output_h),
)?,
)?;
let (ssr_targets, ssr_resolve_pipeline, ssr_composite_pipeline, ssr_blur_pipeline) =
if needs_ssr_prepass {
let ssr_resolve = if ssr_settings.is_some() {
Some(build_ssr_pipeline(device, hot_reload)?)
} else {
None
};
let (composite, blur) = if ssr_settings.is_some() || rt_reflection_settings.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_w,
render_h,
reflection_blur_scale,
)?),
ssr_resolve,
composite,
blur,
)
} else {
(None, None, None, None)
};
let (gbuffer_targets, gbuffer_bindless_pipeline, gbuffer_history_pipeline) = if needs_gbuffer {
(
Some(create_gbuffer_targets(device, render_w, render_h)?),
Some(build_gbuffer_bindless_pipeline(device, hot_reload)?),
Some(crate::metal::model_history::build_model_history_pipeline(
device, hot_reload,
)?),
)
} else {
(None, None, None)
};
let (ssgi_targets, ssgi_gather_pipeline, ssgi_composite_pipeline) =
if let Some(s) = ssgi_settings {
let (gw, gh) = s.gi_dimensions(render_w, render_h);
(
Some(create_ssgi_targets(device, gw, gh)?),
Some(build_ssgi_gather_pipeline(device, hot_reload)?),
Some(build_ssgi_composite_pipeline(device, hot_reload)?),
)
} else {
(None, None, None)
};
let ssr = SsrState {
settings: *ssr_settings,
targets: ssr_targets,
resolve_pipeline: ssr_resolve_pipeline,
composite_pipeline: ssr_composite_pipeline,
blur_pipeline: ssr_blur_pipeline,
blur_scale: reflection_blur_scale.max(1),
};
let gbuffer = GBufferState {
targets: gbuffer_targets,
bindless_pipeline: gbuffer_bindless_pipeline,
history_pipeline: gbuffer_history_pipeline,
};
let ssgi = SsgiState {
settings: *ssgi_settings,
targets: ssgi_targets,
gather_pipeline: ssgi_gather_pipeline,
composite_pipeline: ssgi_composite_pipeline,
};
let (rt_pipeline, rt_pipeline_textured) = if rt_reflection_settings.is_some() {
(
Some(build_rt_reflection_pipeline(
device,
&crate::metal::slang_shaders::RT_REFLECTIONS_FRAG,
hot_reload,
)?),
Some(build_rt_reflection_pipeline(
device,
&crate::metal::slang_shaders::RT_REFLECTIONS_FRAG_TEXTURED,
hot_reload,
)?),
)
} else {
(None, None)
};
let rt_skin_pipeline = if rt_reflection_settings.is_some()
&& crate::metal::raytrace::raytracing_supported(device)
{
Some(crate::metal::raytrace::build_rt_skin_pipeline(
device, hot_reload,
)?)
} else {
None
};
let (
auto_exposure_pipelines,
auto_exposure_histogram,
auto_exposure_output,
auto_exposure_state,
auto_exposure_bias,
) = if let Some(settings) = auto_exposure_settings.as_ref() {
let pipelines = build_auto_exposure_pipelines(device, hot_reload)?;
let hist = make_auto_exposure_histogram(device)?;
let out = make_auto_exposure_output(device)?;
let state = AutoExposureState::new(settings);
(
Some(pipelines),
Some(hist),
Some(out),
Some(state),
auto_exposure_bias_ev,
)
} else {
(None, None, None, None, 0.0)
};
Ok(QualityEffectsBundle {
taa_pipeline_state,
taa_targets,
ssao,
transient_pool,
ssr,
gbuffer,
ssgi,
rt_pipeline,
rt_pipeline_textured,
rt_skin_pipeline,
auto_exposure_pipelines,
auto_exposure_histogram,
auto_exposure_output,
auto_exposure_state,
auto_exposure_bias_ev: auto_exposure_bias,
})
}
pub(crate) fn build_effects(
alloc: &DeviceAllocator,
scene: bool,
dims: EffectDimensions,
settings: EffectSettings,
flags: EffectFlags,
world_content: WorldContentEffects,
) -> Result<EffectsBundle, String> {
let device = alloc.device();
let EffectDimensions {
output_w, output_h, ..
} = dims;
let WorldContentEffects {
fog_settings,
decals,
particles,
frames_in_flight,
} = world_content;
let hot_reload = flags.hot_reload;
let QualityEffectsBundle {
taa_pipeline_state,
taa_targets,
ssao,
transient_pool,
ssr,
gbuffer,
ssgi,
rt_pipeline,
rt_pipeline_textured,
rt_skin_pipeline,
auto_exposure_pipelines,
auto_exposure_histogram,
auto_exposure_output,
auto_exposure_state,
auto_exposure_bias_ev: auto_exposure_bias,
} = build_quality_effects(alloc, dims, settings, flags)?;
let bloom_targets =
create_bloom_targets(device, output_w, output_h, transient_pool.bloom_top()?)?;
let bloom_pipelines = if scene {
Some(build_bloom_pipelines(device, hot_reload)?)
} else {
None
};
let (decal_pipeline, decal_cube_vertex_buffer, decal_cube_index_buffer, decal_sampler) =
if !decals.is_empty() {
let (ps, vbuf, ibuf, samp) = build_decal_resources_for_runtime(device, hot_reload)?;
(Some(ps), Some(vbuf), Some(ibuf), Some(samp))
} else {
(None, None, None, None)
};
let (fog_pipeline, fog_froxel_pipeline, fog_froxel_volume) = if fog_settings.is_some() {
let render_ps = build_fog_pipeline(device, hot_reload)?;
let compute_ps = super::super::fog::build_fog_froxel_pipeline(device, hot_reload)?;
let volume = super::super::fog::build_fog_froxel_volume(device)?;
(Some(render_ps), Some(compute_ps), Some(volume))
} else {
(None, None, None)
};
let (particle_pipelines, particle_emitter_state) = if !particles.is_empty() {
let pipelines = build_particle_pipelines(device, hot_reload)?;
let mut states = Vec::with_capacity(particles.len());
for rec in particles {
states.push(build_emitter_gpu_state(device, rec, frames_in_flight)?);
}
(Some(pipelines), states)
} else {
(None, Vec::new())
};
Ok(EffectsBundle {
bloom_targets,
bloom_pipelines,
taa_pipeline_state,
taa_targets,
ssao,
transient_pool,
ssr,
gbuffer,
ssgi,
rt_pipeline,
rt_pipeline_textured,
rt_skin_pipeline,
decal_pipeline,
decal_cube_vertex_buffer,
decal_cube_index_buffer,
decal_sampler,
fog_pipeline,
fog_froxel_pipeline,
fog_froxel_volume,
particle_pipelines,
particle_emitter_state,
auto_exposure_pipelines,
auto_exposure_histogram,
auto_exposure_output,
auto_exposure_state,
auto_exposure_bias_ev: auto_exposure_bias,
})
}
type DecalResources = (
Retained<ProtocolObject<dyn MTLRenderPipelineState>>,
Retained<ProtocolObject<dyn MTLBuffer>>,
Retained<ProtocolObject<dyn MTLBuffer>>,
Retained<ProtocolObject<dyn MTLSamplerState>>,
);
pub(crate) fn build_decal_resources_for_runtime(
device: &ProtocolObject<dyn MTLDevice>,
hot_reload: bool,
) -> Result<DecalResources, String> {
let ps = build_decal_pipeline(device, hot_reload)?;
#[repr(C)]
#[derive(Copy, Clone)]
struct CubeVtx {
p: [f32; 3],
}
const CUBE_VERTS: [CubeVtx; 8] = [
CubeVtx {
p: [-0.5, -0.5, -0.5],
},
CubeVtx {
p: [0.5, -0.5, -0.5],
},
CubeVtx {
p: [0.5, 0.5, -0.5],
},
CubeVtx {
p: [-0.5, 0.5, -0.5],
},
CubeVtx {
p: [-0.5, -0.5, 0.5],
},
CubeVtx {
p: [0.5, -0.5, 0.5],
},
CubeVtx { p: [0.5, 0.5, 0.5] },
CubeVtx {
p: [-0.5, 0.5, 0.5],
},
];
const CUBE_INDICES: [u16; 36] = [
0, 2, 1, 0, 3, 2, 4, 5, 6, 4, 6, 7, 0, 1, 5, 0, 5, 4, 3, 6, 2, 3, 7, 6, 0, 4, 7, 0, 7, 3, 1, 2, 6, 1, 6, 5,
];
let vbuf = unsafe {
let ptr = std::ptr::NonNull::new(CUBE_VERTS.as_ptr() as *mut _)
.ok_or("decal cube vertex slice is null")?;
device
.newBufferWithBytes_length_options(
ptr,
std::mem::size_of_val(&CUBE_VERTS),
MTLResourceOptions::StorageModeShared,
)
.ok_or("failed to create decal cube vertex buffer")?
};
let ibuf = unsafe {
let ptr = std::ptr::NonNull::new(CUBE_INDICES.as_ptr() as *mut _)
.ok_or("decal cube index slice is null")?;
device
.newBufferWithBytes_length_options(
ptr,
std::mem::size_of_val(&CUBE_INDICES),
MTLResourceOptions::StorageModeShared,
)
.ok_or("failed to create decal cube index buffer")?
};
let samp = {
let desc = MTLSamplerDescriptor::new();
desc.setMinFilter(MTLSamplerMinMagFilter::Linear);
desc.setMagFilter(MTLSamplerMinMagFilter::Linear);
desc.setSAddressMode(MTLSamplerAddressMode::ClampToEdge);
desc.setTAddressMode(MTLSamplerAddressMode::ClampToEdge);
device
.newSamplerStateWithDescriptor(&desc)
.ok_or("failed to create decal sampler state")?
};
Ok((ps, vbuf, ibuf, samp))
}
fn make_auto_exposure_histogram(
device: &ProtocolObject<dyn MTLDevice>,
) -> Result<Retained<ProtocolObject<dyn MTLBuffer>>, String> {
let hist_bytes =
vec![0u8; std::mem::size_of::<u32>() * crate::gfx::auto_exposure::HISTOGRAM_BINS];
unsafe {
let ptr = std::ptr::NonNull::new(hist_bytes.as_ptr() as *mut _)
.ok_or("auto-exposure histogram allocation failed")?;
device
.newBufferWithBytes_length_options(
ptr,
hist_bytes.len(),
MTLResourceOptions::StorageModeShared,
)
.ok_or_else(|| "failed to create auto-exposure histogram buffer".to_string())
}
}
fn make_auto_exposure_output(
device: &ProtocolObject<dyn MTLDevice>,
) -> Result<Retained<ProtocolObject<dyn MTLBuffer>>, String> {
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("auto-exposure output allocation failed")?;
device
.newBufferWithBytes_length_options(
ptr,
out_bytes.len(),
MTLResourceOptions::StorageModeShared,
)
.ok_or_else(|| "failed to create auto-exposure output buffer".to_string())
}
}