#![deny(unsafe_op_in_unsafe_fn)]
use concinnity_core::components::GlassPanel;
use concinnity_core::geometry::glass_quad::build_glass_quad;
use concinnity_core::gfx::mesh_payload::Vertex;
use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::planar_reflection::PlanarFramePlan;
use concinnity_core::render::transparent;
use concinnity_core::render::transparent::SeeThroughMesh;
use concinnity_core::render::uniforms::{GlassParams, TransparentView};
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{
MTLBlendFactor, MTLBuffer, MTLDepthStencilState, MTLDevice, MTLPixelFormat,
MTLRenderPipelineDescriptor, MTLRenderPipelineState, MTLResourceOptions, MTLTexture,
MTLTextureUsage, MTLVertexFormat, MTLVertexStepFunction,
};
use super::allocator::{DeviceAllocator, PooledTexture};
use super::builtin_shaders;
use super::context::MtlContext;
use super::depth::Depth;
use super::descriptors::{TextureDesc, VertexAttr, VertexLayout, vertex_descriptor};
use super::error::allocation_failed;
use super::texture::upload_texture;
use super::transparent::{TransparentDraw, bytes_of};
pub(in crate::metal) struct GlassPanelRecord {
pub(in crate::metal) vertex_buffer: Retained<ProtocolObject<dyn MTLBuffer>>,
pub(in crate::metal) index_buffer: Retained<ProtocolObject<dyn MTLBuffer>>,
pub(in crate::metal) index_count: u32,
pub(in crate::metal) params: GlassParams,
pub(in crate::metal) visible: bool,
pub(in crate::metal) center: [f32; 3],
pub(in crate::metal) planar_slot: Option<usize>,
}
pub(in crate::metal) fn build_glass_panel_record(
device: &ProtocolObject<dyn MTLDevice>,
panel: &GlassPanel,
) -> RenderResult<GlassPanelRecord> {
let (verts, idxs) = build_glass_quad(panel.center, panel.normal, panel.half_size);
let mut packed: Vec<Vertex> = Vec::with_capacity(verts.len());
for (pos, normal, color, uv) in verts {
packed.push(Vertex {
pos,
normal,
tangent: [1.0, 0.0, 0.0],
color,
uv,
});
}
let vb_bytes = packed.len() * std::mem::size_of::<Vertex>();
let ib_bytes = idxs.len() * std::mem::size_of::<u16>();
let vb = unsafe {
let ptr = std::ptr::NonNull::new(packed.as_ptr() as *mut _).ok_or_else(|| {
RenderError::Other("glass vertex buffer: source pointer is null".into())
})?;
device
.newBufferWithBytes_length_options(ptr, vb_bytes, MTLResourceOptions::StorageModeShared)
.ok_or_else(|| allocation_failed("the glass vertex buffer"))?
};
let ib = unsafe {
let ptr = std::ptr::NonNull::new(idxs.as_ptr() as *mut _).ok_or_else(|| {
RenderError::Other("glass index buffer: source pointer is null".into())
})?;
device
.newBufferWithBytes_length_options(ptr, ib_bytes, MTLResourceOptions::StorageModeShared)
.ok_or_else(|| allocation_failed("the glass index buffer"))?
};
Ok(GlassPanelRecord {
vertex_buffer: vb,
index_buffer: ib,
index_count: idxs.len() as u32,
params: GlassParams::from_panel(panel, false),
visible: panel.visible,
center: panel.center,
planar_slot: None,
})
}
pub(super) fn build_glass_pipeline(
device: &ProtocolObject<dyn MTLDevice>,
hot_reload: bool,
) -> RenderResult<Retained<ProtocolObject<dyn MTLRenderPipelineState>>> {
build_glass_pipeline_with(device, hot_reload, &builtin_shaders::GLASS_FRAG)
}
pub(in crate::metal) struct TracedGlassPipelines {
pub(in crate::metal) shade: Retained<ProtocolObject<dyn MTLRenderPipelineState>>,
pub(in crate::metal) reflection: Retained<ProtocolObject<dyn MTLRenderPipelineState>>,
}
pub(super) fn build_glass_pipeline_rt(
device: &ProtocolObject<dyn MTLDevice>,
hot_reload: bool,
) -> RenderResult<TracedGlassPipelines> {
build_traced_glass_pipelines(
device,
hot_reload,
&builtin_shaders::GLASS_VERT,
&builtin_shaders::GLASS_FRAG_RT,
&builtin_shaders::GLASS_REFLECTION_FRAG,
)
}
pub(super) fn build_glass_pipeline_rt_textured(
device: &ProtocolObject<dyn MTLDevice>,
hot_reload: bool,
) -> RenderResult<TracedGlassPipelines> {
build_traced_glass_pipelines(
device,
hot_reload,
&builtin_shaders::GLASS_VERT,
&builtin_shaders::GLASS_FRAG_RT_TEXTURED,
&builtin_shaders::GLASS_REFLECTION_FRAG_TEXTURED,
)
}
pub(super) fn build_glass_mesh_pipeline_rt(
device: &ProtocolObject<dyn MTLDevice>,
hot_reload: bool,
) -> RenderResult<TracedGlassPipelines> {
build_traced_glass_pipelines(
device,
hot_reload,
&builtin_shaders::GLASS_MESH_VERT,
&builtin_shaders::GLASS_MESH_FRAG_RT,
&builtin_shaders::GLASS_MESH_REFLECTION_FRAG,
)
}
pub(super) fn build_glass_mesh_pipeline_rt_textured(
device: &ProtocolObject<dyn MTLDevice>,
hot_reload: bool,
) -> RenderResult<TracedGlassPipelines> {
build_traced_glass_pipelines(
device,
hot_reload,
&builtin_shaders::GLASS_MESH_VERT,
&builtin_shaders::GLASS_MESH_FRAG_RT_TEXTURED,
&builtin_shaders::GLASS_MESH_REFLECTION_FRAG_TEXTURED,
)
}
fn build_glass_pipeline_with(
device: &ProtocolObject<dyn MTLDevice>,
hot_reload: bool,
fragment: &builtin_shaders::ShaderProgram,
) -> RenderResult<Retained<ProtocolObject<dyn MTLRenderPipelineState>>> {
let vert_fn =
builtin_shaders::entry_function(device, &builtin_shaders::GLASS_VERT, hot_reload)?;
let frag_fn = builtin_shaders::entry_function(device, fragment, hot_reload)?;
build_transparent_pipeline_stages(device, &vert_fn, &frag_fn)
}
fn build_traced_glass_pipelines(
device: &ProtocolObject<dyn MTLDevice>,
hot_reload: bool,
vertex: &builtin_shaders::ShaderProgram,
shade: &builtin_shaders::ShaderProgram,
reflection: &builtin_shaders::ShaderProgram,
) -> RenderResult<TracedGlassPipelines> {
let vert_fn = builtin_shaders::entry_function(device, vertex, hot_reload)?;
let shade_fn = builtin_shaders::entry_function(device, shade, hot_reload)?;
let reflection_fn = builtin_shaders::entry_function(device, reflection, hot_reload)?;
Ok(TracedGlassPipelines {
shade: build_transparent_pipeline_stages(device, &vert_fn, &shade_fn)?,
reflection: transparent_pipeline(
device,
&vert_fn,
&reflection_fn,
TransparentOutput::ReflectionLayer,
)?,
})
}
pub(in crate::metal) struct GlassReflectionTargets {
pub(in crate::metal) layers: [Retained<ProtocolObject<dyn MTLTexture>>; 2],
pub(in crate::metal) depth: Retained<ProtocolObject<dyn MTLTexture>>,
pub(in crate::metal) empty: PooledTexture,
pub(in crate::metal) depth_state: Retained<ProtocolObject<dyn MTLDepthStencilState>>,
}
impl GlassReflectionTargets {
fn new(alloc: &DeviceAllocator, width: u32, height: u32) -> RenderResult<Self> {
let device = alloc.device();
let layer = || {
reduced_target(device, MTLPixelFormat::RGBA16Float, width, height)
.ok_or_else(|| allocation_failed("a glass reflection layer"))
};
Ok(Self {
layers: [layer()?, layer()?],
depth: reduced_target(device, MTLPixelFormat::Depth32Float, width, height)
.ok_or_else(|| allocation_failed("the glass reflection depth"))?,
empty: upload_texture(alloc, 1, 1, &[0u8; 4])?,
depth_state: Depth::write().state(device)?,
})
}
fn extent(&self) -> (u32, u32) {
(self.depth.width() as u32, self.depth.height() as u32)
}
}
fn reduced_target(
device: &ProtocolObject<dyn MTLDevice>,
format: MTLPixelFormat,
width: u32,
height: u32,
) -> Option<Retained<ProtocolObject<dyn MTLTexture>>> {
let desc = TextureDesc {
format,
width: width as usize,
height: height as usize,
usage: MTLTextureUsage(MTLTextureUsage::ShaderRead.0 | MTLTextureUsage::RenderTarget.0),
..Default::default()
}
.build();
device.newTextureWithDescriptor(&desc)
}
pub(in crate::metal) fn build_transparent_pipeline_stages(
device: &ProtocolObject<dyn MTLDevice>,
vert_fn: &ProtocolObject<dyn objc2_metal::MTLFunction>,
frag_fn: &ProtocolObject<dyn objc2_metal::MTLFunction>,
) -> RenderResult<Retained<ProtocolObject<dyn MTLRenderPipelineState>>> {
transparent_pipeline(device, vert_fn, frag_fn, TransparentOutput::Scene)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum TransparentOutput {
Scene,
ReflectionLayer,
}
fn transparent_pipeline(
device: &ProtocolObject<dyn MTLDevice>,
vert_fn: &ProtocolObject<dyn objc2_metal::MTLFunction>,
frag_fn: &ProtocolObject<dyn objc2_metal::MTLFunction>,
output: TransparentOutput,
) -> RenderResult<Retained<ProtocolObject<dyn MTLRenderPipelineState>>> {
let vert_desc = vertex_descriptor(
&[
VertexAttr {
index: 0,
format: MTLVertexFormat::Float3,
offset: 0,
buffer_index: 1,
},
VertexAttr {
index: 1,
format: MTLVertexFormat::Float3,
offset: 12,
buffer_index: 1,
},
VertexAttr {
index: 2,
format: MTLVertexFormat::Float3,
offset: 24,
buffer_index: 1,
},
VertexAttr {
index: 3,
format: MTLVertexFormat::Float3,
offset: 36,
buffer_index: 1,
},
VertexAttr {
index: 4,
format: MTLVertexFormat::Float2,
offset: 48,
buffer_index: 1,
},
],
&[VertexLayout {
buffer_index: 1,
stride: std::mem::size_of::<Vertex>(),
step: MTLVertexStepFunction::PerVertex,
}],
);
let desc = MTLRenderPipelineDescriptor::new();
desc.setVertexDescriptor(Some(&vert_desc));
desc.setVertexFunction(Some(vert_fn));
desc.setFragmentFunction(Some(frag_fn));
desc.setRasterSampleCount(1);
let blend = output == TransparentOutput::Scene;
if !blend {
desc.setDepthAttachmentPixelFormat(MTLPixelFormat::Depth32Float);
}
unsafe {
let ca = desc.colorAttachments().objectAtIndexedSubscript(0);
ca.setPixelFormat(MTLPixelFormat::RGBA16Float);
ca.setBlendingEnabled(blend);
ca.setSourceRGBBlendFactor(MTLBlendFactor::SourceAlpha);
ca.setDestinationRGBBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
ca.setSourceAlphaBlendFactor(MTLBlendFactor::SourceAlpha);
ca.setDestinationAlphaBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
}
if blend {
super::reactive_mask::declare_target(&desc);
}
device
.newRenderPipelineStateWithDescriptor_error(&desc)
.map_err(|e| RenderError::ShaderCompile(format!("transparent pipeline state: {e:?}")))
}
impl MtlContext {
pub(in crate::metal) fn collect_glass_transparent_draws(
&self,
view: &TransparentView,
bindless: bool,
mirrors: &PlanarFramePlan,
out: &mut Vec<TransparentDraw>,
) {
let rt_on = self.rt.accel.is_some();
let traced = match (
rt_on && bindless,
&self.glass.pipeline_rt_textured,
rt_on,
&self.glass.pipeline_rt,
) {
(true, Some(p), _, _) => Some(p),
(_, _, true, Some(p)) => Some(p),
_ => None,
};
let (pipeline, reflection_pipeline) = match traced {
Some(t) => (&t.shade, self.glass_reflection_pipeline(t)),
None => match &self.glass.pipeline {
Some(p) => (p, None),
None => return,
},
};
let cam = view.camera_pos;
let planar_set = self.planar_reflection.as_ref();
for panel in &self.glass.panels {
if !panel.visible {
continue;
}
let mut params = panel.params;
let mut fragment_textures = vec![
(0, self.targets.hdr.transparent_scene_copy.clone()),
(1, self.targets.hdr.depth_resolve.clone()),
];
if mirrors.samples_mirror(panel.planar_slot)
&& let Some(targets) = panel
.planar_slot
.and_then(|s| planar_set.and_then(|set| set.targets.get(s)))
{
params.planar = GlassParams::planar_lane(true);
fragment_textures.push((
super::transparent::GLASS_PLANAR_TEXTURE_INDEX,
targets.resolve.clone(),
));
}
let c = panel.center;
let sort_distance = transparent::sort_distance(c, [cam[0], cam[1], cam[2]]);
out.push(TransparentDraw {
pipeline: pipeline.clone(),
reflection_pipeline: reflection_pipeline.clone(),
vertex_buffer: panel.vertex_buffer.clone(),
index_buffer: panel.index_buffer.clone(),
index_count: panel.index_count,
index_type: objc2_metal::MTLIndexType::UInt16,
index_offset_bytes: 0,
base_vertex: 0,
params: bytes_of(¶ms),
fragment_textures,
fragment_samplers: vec![(0, self.composite.sampler.clone())],
sort_distance,
});
}
}
pub(in crate::metal) fn sync_glass_reflection_target(
&mut self,
render_w: u32,
render_h: u32,
) -> RenderResult<()> {
let traced = self.glass.pipeline_rt.is_some() || self.glass.mesh_pipeline_rt.is_some();
let extent = self
.rt
.settings
.filter(|s| s.divisor > 1 && traced && self.rt.accel.is_some())
.map(|s| s.trace_extent(render_w, render_h));
let current = self
.glass
.reflection_targets
.as_ref()
.map(GlassReflectionTargets::extent);
if extent == current {
return Ok(());
}
self.glass.reflection_targets = match extent {
Some((w, h)) => Some(GlassReflectionTargets::new(&self.hw.allocator, w, h)?),
None => None,
};
Ok(())
}
fn glass_reflection_pipeline(
&self,
traced: &TracedGlassPipelines,
) -> Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>> {
self.glass
.reflection_targets
.as_ref()
.map(|_| traced.reflection.clone())
}
pub(in crate::metal) fn seethrough_meshes_enabled(&self) -> bool {
!self.glass.seethrough_mesh_indices.is_empty() && self.glass.mesh_pipeline_rt.is_some()
}
pub(in crate::metal) fn mesh_glass_active(&self) -> bool {
self.seethrough_meshes_enabled() && self.rt.accel.is_some()
}
pub(in crate::metal) fn mesh_glass_visible(&self) -> bool {
self.mesh_glass_active()
&& self.glass.seethrough_mesh_indices.iter().any(|&i| {
self.state
.draw
.objects
.get(i)
.is_some_and(|o| o.visible && o.resident)
})
}
pub(in crate::metal) fn collect_mesh_transparent_draws(
&self,
view: &TransparentView,
bindless: bool,
out: &mut Vec<TransparentDraw>,
) {
if !self.mesh_glass_active() {
return;
}
let traced = match (bindless, &self.glass.mesh_pipeline_rt_textured) {
(true, Some(p)) => p,
_ => match &self.glass.mesh_pipeline_rt {
Some(p) => p,
None => return,
},
};
let reflection_pipeline = self.glass_reflection_pipeline(traced);
let prefilter_mip_count = self.scene.env_map.prefilter_mip_count as f32;
let cam = [view.camera_pos[0], view.camera_pos[1], view.camera_pos[2]];
for &idx in &self.glass.seethrough_mesh_indices {
let Some(obj) = self.state.draw.objects.get(idx) else {
continue;
};
if !obj.visible || !obj.resident {
continue;
}
let mesh = SeeThroughMesh::new(obj, cam, prefilter_mip_count);
out.push(TransparentDraw {
pipeline: traced.shade.clone(),
reflection_pipeline: reflection_pipeline.clone(),
vertex_buffer: self.scene.vertex_buffer.retained(),
index_buffer: self.scene.index_buffer.retained(),
index_count: mesh.index_count as u32,
index_type: objc2_metal::MTLIndexType::UInt32,
index_offset_bytes: mesh.index_offset * std::mem::size_of::<u32>(),
base_vertex: mesh.base_vertex,
params: bytes_of(&mesh.params),
fragment_textures: vec![
(0, self.targets.hdr.transparent_scene_copy.clone()),
(1, self.targets.hdr.depth_resolve.clone()),
],
fragment_samplers: vec![(0, self.composite.sampler.clone())],
sort_distance: mesh.distance,
});
}
}
}