#![deny(unsafe_op_in_unsafe_fn)]
use concinnity_core::components::{GlassPanel, WaterSurface};
use concinnity_core::gfx::render_types::DrawObject;
use concinnity_core::render::backend_init::PlanarBudget;
use concinnity_core::render::backend_init::SdfVolumeSource;
use concinnity_core::render::decal::{DecalRecord, DecalSet};
use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::particles::ParticleEmitterRecord;
use concinnity_core::render::planar_reflection::PlanarReflectors;
use concinnity_core::render::volumetric_fog::FogSettings;
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{
MTLBuffer, MTLDevice, MTLRenderPipelineState, MTLResourceOptions, MTLSamplerAddressMode,
MTLSamplerDescriptor, MTLSamplerMinMagFilter, MTLSamplerState,
};
use super::{Features, InitGpu};
use crate::metal::context::{GlassState, RaymarchState, WaterState};
use crate::metal::decal::{DecalState, build_decal_pipeline};
use crate::metal::error::allocation_failed;
use crate::metal::fog::{
FogState, build_fog_froxel_pipeline, build_fog_froxel_volume, build_fog_pipeline,
};
use crate::metal::particle::{ParticleState, build_emitter_gpu_state, build_particle_pipelines};
use crate::metal::planar::{MAX_PLANAR_PLANES, PlanarReflectionSet};
use crate::metal::{glass, raymarch, raytrace, water};
pub(super) fn build_decals(
gpu: &InitGpu<'_>,
decals: Vec<DecalRecord>,
) -> RenderResult<DecalState> {
let (pipeline, cube_vertex_buffer, cube_index_buffer, sampler) = if !decals.is_empty() {
let (ps, vbuf, ibuf, samp) =
build_decal_resources_for_runtime(&gpu.hw.device, gpu.hot_reload)?;
(Some(ps), Some(vbuf), Some(ibuf), Some(samp))
} else {
(None, None, None, None)
};
let mut set = DecalSet::new(usize::MAX, gpu.frames_in_flight);
for record in decals {
set.insert(record)
.map_err(|_| RenderError::Other("decals: decal slot table is full".to_string()))?;
}
Ok(DecalState {
set,
pipeline,
cube_vertex_buffer,
cube_index_buffer,
sampler,
})
}
type DecalResources = (
Retained<ProtocolObject<dyn MTLRenderPipelineState>>,
Retained<ProtocolObject<dyn MTLBuffer>>,
Retained<ProtocolObject<dyn MTLBuffer>>,
Retained<ProtocolObject<dyn MTLSamplerState>>,
);
pub(in crate::metal) fn build_decal_resources_for_runtime(
device: &ProtocolObject<dyn MTLDevice>,
hot_reload: bool,
) -> RenderResult<DecalResources> {
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_else(|| RenderError::Other("decal cube vertex slice is null".to_string()))?;
device
.newBufferWithBytes_length_options(
ptr,
std::mem::size_of_val(&CUBE_VERTS),
MTLResourceOptions::StorageModeShared,
)
.ok_or_else(|| allocation_failed("decal cube vertex buffer"))?
};
let ibuf = unsafe {
let ptr = std::ptr::NonNull::new(CUBE_INDICES.as_ptr() as *mut _)
.ok_or_else(|| RenderError::Other("decal cube index slice is null".to_string()))?;
device
.newBufferWithBytes_length_options(
ptr,
std::mem::size_of_val(&CUBE_INDICES),
MTLResourceOptions::StorageModeShared,
)
.ok_or_else(|| allocation_failed("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_else(|| RenderError::Other("failed to create decal sampler state".to_string()))?
};
Ok((ps, vbuf, ibuf, samp))
}
pub(super) fn build_fog(
gpu: &InitGpu<'_>,
settings: Option<FogSettings>,
) -> RenderResult<FogState> {
let device = &*gpu.hw.device;
let (pipeline, froxel_pipeline, froxel_volume) = if settings.is_some() {
let render_ps = build_fog_pipeline(device, gpu.hot_reload)?;
let compute_ps = build_fog_froxel_pipeline(device, gpu.hot_reload)?;
let volume = build_fog_froxel_volume(device)?;
(Some(render_ps), Some(compute_ps), Some(volume))
} else {
(None, None, None)
};
Ok(FogState {
settings,
pipeline,
froxel_pipeline,
froxel_volume,
})
}
pub(super) fn build_particles(
gpu: &InitGpu<'_>,
particles: Vec<ParticleEmitterRecord>,
) -> RenderResult<ParticleState> {
let device = &*gpu.hw.device;
let (pipelines, emitter_state) = if !particles.is_empty() {
let pipelines = build_particle_pipelines(device, gpu.hot_reload)?;
let mut states = Vec::with_capacity(particles.len());
for rec in &particles {
states.push(build_emitter_gpu_state(device, rec)?);
}
(Some(pipelines), states)
} else {
(None, Vec::new())
};
Ok(ParticleState {
records: particles.into_iter().map(Some).collect(),
emitter_state: emitter_state.into_iter().map(Some).collect(),
free_slots: Vec::new(),
pipelines,
last_elapsed: 0.0,
frame_index: 0,
})
}
pub(super) fn plan_planar(
water_surfaces: &[WaterSurface],
glass_panels: &[GlassPanel],
budget: PlanarBudget,
) -> PlanarReflectors {
let planar = PlanarReflectors::plan(water_surfaces, glass_panels, budget);
let overflow = planar.overflow();
if overflow > 0 {
tracing::warn!(
"planar reflection: {} reflector plane(s) exceed the budget of {} \
and fall back to the box-projected probe cube",
overflow,
budget.planes.min(MAX_PLANAR_PLANES)
);
}
planar
}
pub(super) fn build_water(
gpu: &InitGpu<'_>,
water_surfaces: &[WaterSurface],
planar_slots: &[Option<usize>],
) -> RenderResult<WaterState> {
let device = &*gpu.hw.device;
let hot_reload = gpu.hot_reload;
let (pipeline, pipeline_rt, pipeline_rt_textured, surfaces) = if water_surfaces.is_empty() {
(None, None, None, Vec::new())
} else {
let ps = water::build_water_pipeline(device, hot_reload)?;
let (ps_rt, ps_rt_tex) = if raytrace::raytracing_supported(device) {
(
Some(water::build_water_pipeline_rt(device, hot_reload)?),
Some(water::build_water_pipeline_rt_textured(device, hot_reload)?),
)
} else {
(None, None)
};
let mut records = Vec::with_capacity(water_surfaces.len());
for (s, slot) in water_surfaces.iter().zip(planar_slots) {
let mut record = water::build_water_surface_record(device, s)?;
record.planar_slot = *slot;
records.push(record);
}
(Some(ps), ps_rt, ps_rt_tex, records)
};
Ok(WaterState {
pipeline,
pipeline_rt,
pipeline_rt_textured,
surfaces,
})
}
pub(super) fn build_glass(
gpu: &InitGpu<'_>,
glass_panels: &[GlassPanel],
planar_slots: &[Option<usize>],
draw_objects: &[DrawObject],
) -> RenderResult<GlassState> {
let device = &*gpu.hw.device;
let hot_reload = gpu.hot_reload;
let (pipeline, pipeline_rt, pipeline_rt_textured, panels) = if glass_panels.is_empty() {
(None, None, None, Vec::new())
} else {
let ps = glass::build_glass_pipeline(device, hot_reload)?;
let (ps_rt, ps_rt_tex) = if raytrace::raytracing_supported(device) {
(
Some(glass::build_glass_pipeline_rt(device, hot_reload)?),
Some(glass::build_glass_pipeline_rt_textured(device, hot_reload)?),
)
} else {
(None, None)
};
let mut records = Vec::with_capacity(glass_panels.len());
for (g, slot) in glass_panels.iter().zip(planar_slots) {
let mut record = glass::build_glass_panel_record(device, g)?;
record.planar_slot = *slot;
records.push(record);
}
(Some(ps), ps_rt, ps_rt_tex, records)
};
let (mesh_pipeline_rt, mesh_pipeline_rt_textured) = if raytrace::raytracing_supported(device) {
(
Some(glass::build_glass_mesh_pipeline_rt(device, hot_reload)?),
Some(glass::build_glass_mesh_pipeline_rt_textured(
device, hot_reload,
)?),
)
} else {
(None, None)
};
let seethrough_mesh_indices: Vec<usize> = draw_objects
.iter()
.enumerate()
.filter(|(_, o)| o.material.transparent != 0 && o.material.see_through != 0)
.map(|(i, _)| i)
.collect();
Ok(GlassState {
pipeline,
pipeline_rt,
pipeline_rt_textured,
mesh_pipeline_rt,
mesh_pipeline_rt_textured,
seethrough_mesh_indices,
panels,
reflection_targets: None,
})
}
pub(super) fn build_planar_reflection(
gpu: &InitGpu<'_>,
planar: PlanarReflectors,
features: &Features,
) -> RenderResult<Option<PlanarReflectionSet>> {
if planar.planes().is_empty() {
return Ok(None);
}
Ok(Some(PlanarReflectionSet::new(
&gpu.hw.device,
planar,
features.render,
features.hdr_samples,
)?))
}
pub(super) fn build_raymarch(
gpu: &InitGpu<'_>,
sdf_volumes: &[SdfVolumeSource],
) -> RenderResult<RaymarchState> {
let device = &*gpu.hw.device;
let (volumes, cube_vertex_buffer, cube_index_buffer) = if sdf_volumes.is_empty() {
(Vec::new(), None, None)
} else {
let mut records = Vec::with_capacity(sdf_volumes.len());
for SdfVolumeSource {
volume,
fragment_source: payload,
label,
} in sdf_volumes
{
records.push(raymarch::build_raymarch_volume_record(
device,
volume,
payload,
gpu.hot_reload,
label,
)?);
}
let (vb, ib) = raymarch::build_raymarch_cube_buffers(device)?;
(records, Some(vb), Some(ib))
};
Ok(RaymarchState {
volumes,
cube_vertex_buffer,
cube_index_buffer,
})
}