#![deny(unsafe_op_in_unsafe_fn)]
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_foundation::NSString;
use objc2_metal::{
MTLBlendFactor, MTLBlendOperation, MTLBlitCommandEncoder as _, MTLBuffer,
MTLCommandBuffer as _, MTLCommandEncoder as _, MTLCullMode, MTLDevice, MTLIndexType,
MTLLibrary as _, MTLLoadAction, MTLPixelFormat, MTLPrimitiveType, MTLRenderCommandEncoder as _,
MTLRenderPassDescriptor, MTLRenderPipelineDescriptor, MTLRenderPipelineState,
MTLResourceOptions, MTLStoreAction, MTLVertexFormat, MTLVertexStepFunction,
};
use crate::components::sdf_volume::SdfVolume;
use crate::gfx::mesh_payload::Vertex;
use crate::gfx::render_types::LightUniforms;
use super::context::MtlContext;
use super::descriptors::{VertexAttr, VertexLayout, vertex_descriptor};
use super::encode::RenderEncode;
use super::pipeline::ns_str;
use super::scoped_encoder::ScopedEncoder;
pub(in crate::metal) use crate::metal::uniforms::RaymarchShadowCascade;
pub(in crate::metal) use crate::metal::uniforms::RaymarchView;
pub(in crate::metal) use crate::metal::uniforms::RaymarchVolumeUniforms;
const RAYMARCH_HELPERS_MSL: &str = include_str!("shaders/raymarch_helpers.metal");
const RAYMARCH_TEMPLATE_MSL: &str = include_str!("shaders/raymarch_template.metal");
const RAYMARCH_SHADOW_MSL: &str = include_str!("shaders/raymarch_shadow.metal");
const RAYMARCH_VOLUMETRIC_MSL: &str = include_str!("shaders/raymarch_volumetric_template.metal");
type RaymarchLightsGpu = LightUniforms;
pub(in crate::metal) struct RaymarchVolumeRecord {
pub(in crate::metal) pipeline: Retained<ProtocolObject<dyn MTLRenderPipelineState>>,
pub(in crate::metal) shadow_pipeline:
Option<Retained<ProtocolObject<dyn MTLRenderPipelineState>>>,
pub(in crate::metal) uniforms: RaymarchVolumeUniforms,
pub(in crate::metal) visible: bool,
pub(in crate::metal) volumetric: bool,
pub(in crate::metal) cast_shadows: bool,
pub(in crate::metal) world_centre: [f32; 3],
pub(in crate::metal) world_extent: [f32; 3],
}
pub(in crate::metal) fn volume_in_frustum(
centre: [f32; 3],
extent: [f32; 3],
frustum: &crate::gfx::frustum::Frustum,
) -> bool {
let min = [
centre[0] - extent[0],
centre[1] - extent[1],
centre[2] - extent[2],
];
let max = [
centre[0] + extent[0],
centre[1] + extent[1],
centre[2] + extent[2],
];
frustum.intersects_aabb(min, max)
}
pub(in crate::metal) fn build_raymarch_pipeline(
device: &ProtocolObject<dyn MTLDevice>,
user_source: &str,
asset_label: &str,
) -> Result<Retained<ProtocolObject<dyn MTLRenderPipelineState>>, String> {
let wrapped = format!(
"{}\n// === user SdfVolume::fragment_shader: {} ===\n{}\n// === engine raymarch template ===\n{}\n",
RAYMARCH_HELPERS_MSL, asset_label, user_source, RAYMARCH_TEMPLATE_MSL
);
let library = super::msl_cache::compiled_library(device, &wrapped, asset_label)
.map_err(|e| format!("raymarch shader compile error for SdfVolume '{asset_label}': {e}"))?;
let vert_fn = library
.newFunctionWithName(&ns_str("raymarch_vertex"))
.ok_or_else(|| {
format!(
"raymarch_vertex entry not found in compiled library for SdfVolume '{}'",
asset_label
)
})?;
let frag_fn = library
.newFunctionWithName(&ns_str("raymarch_fragment"))
.ok_or_else(|| {
format!(
"raymarch_fragment entry not found in compiled library for SdfVolume '{}'",
asset_label
)
})?;
let vert_desc = vertex_descriptor(
&[
VertexAttr {
index: 0,
format: MTLVertexFormat::Float3,
offset: 0,
buffer_index: 2,
},
VertexAttr {
index: 1,
format: MTLVertexFormat::Float3,
offset: 12,
buffer_index: 2,
},
VertexAttr {
index: 2,
format: MTLVertexFormat::Float3,
offset: 24,
buffer_index: 2,
},
VertexAttr {
index: 3,
format: MTLVertexFormat::Float3,
offset: 36,
buffer_index: 2,
},
VertexAttr {
index: 4,
format: MTLVertexFormat::Float2,
offset: 48,
buffer_index: 2,
},
],
&[VertexLayout {
buffer_index: 2,
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);
unsafe {
let ca = desc.colorAttachments().objectAtIndexedSubscript(0);
ca.setPixelFormat(MTLPixelFormat::RGBA16Float);
ca.setBlendingEnabled(false);
}
desc.setDepthAttachmentPixelFormat(MTLPixelFormat::Depth32Float);
device
.newRenderPipelineStateWithDescriptor_error(&desc)
.map_err(|e| {
format!(
"failed to create raymarch pipeline state for SdfVolume '{}': {:?}",
asset_label, e
)
})
}
pub(in crate::metal) fn build_raymarch_shadow_pipeline(
device: &ProtocolObject<dyn MTLDevice>,
user_source: &str,
asset_label: &str,
) -> Result<Retained<ProtocolObject<dyn MTLRenderPipelineState>>, String> {
let wrapped = format!(
"{}\n// === user SdfVolume::fragment_shader (shadow): {} ===\n{}\n// === engine raymarch shadow template ===\n{}\n",
RAYMARCH_HELPERS_MSL, asset_label, user_source, RAYMARCH_SHADOW_MSL
);
let library =
super::msl_cache::compiled_library(device, &wrapped, asset_label).map_err(|e| {
format!("raymarch shadow shader compile error for SdfVolume '{asset_label}': {e}")
})?;
let vert_fn = library
.newFunctionWithName(&ns_str("raymarch_shadow_vertex"))
.ok_or_else(|| {
format!(
"raymarch_shadow_vertex entry not found in compiled library for SdfVolume '{}'",
asset_label
)
})?;
let frag_fn = library
.newFunctionWithName(&ns_str("raymarch_shadow_fragment"))
.ok_or_else(|| {
format!(
"raymarch_shadow_fragment entry not found in compiled library for SdfVolume '{}'",
asset_label
)
})?;
let vert_desc = vertex_descriptor(
&[
VertexAttr {
index: 0,
format: MTLVertexFormat::Float3,
offset: 0,
buffer_index: 2,
},
VertexAttr {
index: 1,
format: MTLVertexFormat::Float3,
offset: 12,
buffer_index: 2,
},
VertexAttr {
index: 2,
format: MTLVertexFormat::Float3,
offset: 24,
buffer_index: 2,
},
VertexAttr {
index: 3,
format: MTLVertexFormat::Float3,
offset: 36,
buffer_index: 2,
},
VertexAttr {
index: 4,
format: MTLVertexFormat::Float2,
offset: 48,
buffer_index: 2,
},
],
&[VertexLayout {
buffer_index: 2,
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);
desc.setDepthAttachmentPixelFormat(MTLPixelFormat::Depth32Float);
device
.newRenderPipelineStateWithDescriptor_error(&desc)
.map_err(|e| {
format!(
"failed to create raymarch shadow pipeline state for SdfVolume '{}': {:?}",
asset_label, e
)
})
}
pub(in crate::metal) fn build_raymarch_volumetric_pipeline(
device: &ProtocolObject<dyn MTLDevice>,
user_source: &str,
asset_label: &str,
) -> Result<Retained<ProtocolObject<dyn MTLRenderPipelineState>>, String> {
let wrapped = format!(
"{}\n// === user SdfVolume::fragment_shader (volumetric): {} ===\n{}\n// === engine raymarch volumetric template ===\n{}\n",
RAYMARCH_HELPERS_MSL, asset_label, user_source, RAYMARCH_VOLUMETRIC_MSL
);
let library =
super::msl_cache::compiled_library(device, &wrapped, asset_label).map_err(|e| {
format!("raymarch volumetric shader compile error for SdfVolume '{asset_label}': {e}")
})?;
let vert_fn = library
.newFunctionWithName(&ns_str("raymarch_volumetric_vertex"))
.ok_or_else(|| {
format!(
"raymarch_volumetric_vertex entry not found in compiled library for SdfVolume '{}'",
asset_label
)
})?;
let frag_fn = library
.newFunctionWithName(&ns_str("raymarch_volumetric_fragment"))
.ok_or_else(|| {
format!(
"raymarch_volumetric_fragment entry not found in compiled library for SdfVolume '{}'",
asset_label
)
})?;
let vert_desc = vertex_descriptor(
&[
VertexAttr {
index: 0,
format: MTLVertexFormat::Float3,
offset: 0,
buffer_index: 2,
},
VertexAttr {
index: 1,
format: MTLVertexFormat::Float3,
offset: 12,
buffer_index: 2,
},
VertexAttr {
index: 2,
format: MTLVertexFormat::Float3,
offset: 24,
buffer_index: 2,
},
VertexAttr {
index: 3,
format: MTLVertexFormat::Float3,
offset: 36,
buffer_index: 2,
},
VertexAttr {
index: 4,
format: MTLVertexFormat::Float2,
offset: 48,
buffer_index: 2,
},
],
&[VertexLayout {
buffer_index: 2,
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);
unsafe {
let ca = desc.colorAttachments().objectAtIndexedSubscript(0);
ca.setPixelFormat(MTLPixelFormat::RGBA16Float);
ca.setBlendingEnabled(true);
ca.setSourceRGBBlendFactor(MTLBlendFactor::SourceAlpha);
ca.setDestinationRGBBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
ca.setRgbBlendOperation(MTLBlendOperation::Add);
ca.setSourceAlphaBlendFactor(MTLBlendFactor::One);
ca.setDestinationAlphaBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
ca.setAlphaBlendOperation(MTLBlendOperation::Add);
}
desc.setDepthAttachmentPixelFormat(MTLPixelFormat::Depth32Float);
device
.newRenderPipelineStateWithDescriptor_error(&desc)
.map_err(|e| {
format!(
"failed to create raymarch volumetric pipeline state for SdfVolume '{}': {:?}",
asset_label, e
)
})
}
pub(in crate::metal) fn build_raymarch_volume_record(
device: &ProtocolObject<dyn MTLDevice>,
volume: &SdfVolume,
user_source_bytes: &[u8],
asset_label: &str,
) -> Result<RaymarchVolumeRecord, String> {
let user_source = std::str::from_utf8(user_source_bytes).map_err(|e| {
format!(
"SdfVolume '{}': fragment shader payload is not valid UTF-8: {}",
asset_label, e
)
})?;
let pipeline = if volume.volumetric {
build_raymarch_volumetric_pipeline(device, user_source, asset_label)?
} else {
build_raymarch_pipeline(device, user_source, asset_label)?
};
let shadow_pipeline = if volume.cast_shadows {
Some(build_raymarch_shadow_pipeline(
device,
user_source,
asset_label,
)?)
} else {
None
};
Ok(RaymarchVolumeRecord {
pipeline,
shadow_pipeline,
uniforms: volume_uniforms_from(volume),
visible: volume.visible,
volumetric: volume.volumetric,
cast_shadows: volume.cast_shadows,
world_centre: volume.centre,
world_extent: volume.extent,
})
}
fn volume_uniforms_from(volume: &SdfVolume) -> RaymarchVolumeUniforms {
RaymarchVolumeUniforms {
centre: volume.centre,
_pad0: 0.0,
extent: volume.extent,
_pad1: 0.0,
cone_ratio: volume.cone_ratio(),
max_distance: volume.max_distance,
max_steps: volume.max_steps as i32,
receive_shadows: if volume.receive_shadows { 1 } else { 0 },
params: volume.params,
}
}
type RaymarchCubeBuffers = (
Retained<ProtocolObject<dyn MTLBuffer>>,
Retained<ProtocolObject<dyn MTLBuffer>>,
);
pub(in crate::metal) fn build_raymarch_cube_buffers(
device: &ProtocolObject<dyn MTLDevice>,
) -> Result<RaymarchCubeBuffers, String> {
#[rustfmt::skip]
let corners: [Vertex; 8] = [
v([-1.0, -1.0, -1.0]),
v([ 1.0, -1.0, -1.0]),
v([ 1.0, 1.0, -1.0]),
v([-1.0, 1.0, -1.0]),
v([-1.0, -1.0, 1.0]),
v([ 1.0, -1.0, 1.0]),
v([ 1.0, 1.0, 1.0]),
v([-1.0, 1.0, 1.0]),
];
#[rustfmt::skip]
let indices: [u16; 36] = [
0, 2, 1, 0, 3, 2,
4, 5, 6, 4, 6, 7,
0, 4, 7, 0, 7, 3,
1, 2, 6, 1, 6, 5,
0, 1, 5, 0, 5, 4,
3, 7, 6, 3, 6, 2,
];
let vb_bytes = std::mem::size_of_val(&corners);
let ib_bytes = std::mem::size_of_val(&indices);
let vb = unsafe {
let ptr = std::ptr::NonNull::new(corners.as_ptr() as *mut _)
.ok_or("raymarch cube vertex pointer null")?;
device
.newBufferWithBytes_length_options(ptr, vb_bytes, MTLResourceOptions::StorageModeShared)
.ok_or("failed to allocate raymarch cube vertex buffer")?
};
let ib = unsafe {
let ptr = std::ptr::NonNull::new(indices.as_ptr() as *mut _)
.ok_or("raymarch cube index pointer null")?;
device
.newBufferWithBytes_length_options(ptr, ib_bytes, MTLResourceOptions::StorageModeShared)
.ok_or("failed to allocate raymarch cube index buffer")?
};
Ok((vb, ib))
}
fn v(pos: [f32; 3]) -> Vertex {
Vertex {
pos,
normal: [0.0, 0.0, 0.0],
tangent: [0.0, 0.0, 0.0],
color: [0.0, 0.0, 0.0],
uv: [0.0, 0.0],
}
}
impl MtlContext {
pub(in crate::metal) fn encode_raymarch(
&self,
cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
view: &RaymarchView,
frustum: &crate::gfx::frustum::Frustum,
) -> Result<u32, String> {
if self.raymarch.volumes.is_empty() {
return Ok(0);
}
let visible: Vec<bool> = self
.raymarch
.volumes
.iter()
.map(|v| v.visible && volume_in_frustum(v.world_centre, v.world_extent, frustum))
.collect();
if !visible.iter().any(|&v| v) {
return Ok(0);
}
let vbuf = self
.raymarch
.cube_vertex_buffer
.as_ref()
.ok_or("raymarch cube vertex buffer missing")?;
let ibuf = self
.raymarch
.cube_index_buffer
.as_ref()
.ok_or("raymarch cube index buffer missing")?;
let depth_sampler = self.post_sampler.as_ref();
let lights_gpu: RaymarchLightsGpu = self.light_uniforms;
let shadow_uniforms = self.shadow.uniforms;
let blit = cmd_buf
.blitCommandEncoder()
.ok_or("failed to get raymarch scene-copy blit encoder")?;
blit.pushDebugGroup(&NSString::from_str("raymarch_scene_copy"));
unsafe {
blit.copyFromTexture_toTexture(
self.hdr_targets.hdr_resolve.as_ref(),
self.hdr_targets.hdr_resolve_copy.as_ref(),
);
}
blit.popDebugGroup();
blit.endEncoding();
let pass_desc = MTLRenderPassDescriptor::new();
unsafe {
let ca = pass_desc.colorAttachments().objectAtIndexedSubscript(0);
ca.setTexture(Some(self.hdr_targets.hdr_resolve.as_ref()));
ca.setLoadAction(MTLLoadAction::Load);
ca.setStoreAction(MTLStoreAction::Store);
let da = pass_desc.depthAttachment();
da.setTexture(Some(self.hdr_targets.depth_resolve.as_ref()));
da.setLoadAction(MTLLoadAction::Load);
da.setStoreAction(MTLStoreAction::Store);
}
if let Some(t) = &self.diagnostics.pass_timing {
t.attach_render(&pass_desc, super::pass_timing::PassId::Raymarch);
}
let enc = ScopedEncoder::new(
cmd_buf
.renderCommandEncoderWithDescriptor(&pass_desc)
.ok_or("failed to get raymarch render encoder")?,
"raymarch",
);
enc.setCullMode(MTLCullMode::Front);
enc.set_depth_stencil(self.depth_state.as_ref());
enc.set_vertex_value(view, 0);
enc.set_fragment_value(view, 0);
enc.set_fragment_value(&lights_gpu, 2);
enc.set_fragment_value(&shadow_uniforms, 3);
enc.set_vertex_buffer(vbuf, 0, 2);
enc.set_fragment_texture(self.hdr_targets.depth.as_ref(), 0);
enc.set_fragment_texture(self.shadow.map.as_ref(), 1);
enc.set_fragment_texture(self.env_map.irradiance.as_ref(), 2);
enc.set_fragment_texture(self.env_map.prefilter.as_ref(), 3);
enc.set_fragment_texture(self.hdr_targets.hdr_resolve_copy.as_ref(), 4);
enc.set_fragment_sampler(depth_sampler, 0);
enc.set_fragment_sampler(self.shadow.sampler.as_ref(), 1);
enc.set_fragment_sampler(self.cube_sampler.as_ref(), 2);
enc.set_fragment_sampler(depth_sampler, 3);
let mut draws: u32 = 0;
for (i, vol) in self.raymarch.volumes.iter().enumerate() {
if !visible[i] {
continue;
}
enc.set_pipeline(&vol.pipeline);
if vol.volumetric {
enc.set_depth_stencil(self.depth_state_read_only.as_ref());
} else {
enc.set_depth_stencil(self.depth_state.as_ref());
}
enc.set_vertex_value(&vol.uniforms, 1);
enc.set_fragment_value(&vol.uniforms, 1);
unsafe {
enc.drawIndexedPrimitives_indexCount_indexType_indexBuffer_indexBufferOffset(
MTLPrimitiveType::Triangle,
36,
MTLIndexType::UInt16,
ibuf,
0,
);
}
draws += 1;
}
Ok(draws)
}
pub(in crate::metal) fn any_raymarch_shadow_casters(&self) -> bool {
self.raymarch
.volumes
.iter()
.any(|v| v.visible && v.cast_shadows && v.shadow_pipeline.is_some())
}
pub(in crate::metal) fn encode_sdf_shadow_casters(
&self,
cmd_buf: &ProtocolObject<dyn objc2_metal::MTLCommandBuffer>,
view: &RaymarchView,
) -> Result<u32, String> {
use crate::gfx::render_types::NUM_SHADOW_CASCADES;
if !self.any_raymarch_shadow_casters() {
return Ok(0);
}
let vbuf = self
.raymarch
.cube_vertex_buffer
.as_ref()
.ok_or("raymarch shadow: cube vertex buffer missing")?;
let ibuf = self
.raymarch
.cube_index_buffer
.as_ref()
.ok_or("raymarch shadow: cube index buffer missing")?;
let lights_gpu: RaymarchLightsGpu = self.light_uniforms;
let shadow_uniforms = self.shadow.uniforms;
let mut draws: u32 = 0;
let render_mask = if self.shadow.render_mask == 0 {
(1u32 << NUM_SHADOW_CASCADES) - 1
} else {
self.shadow.render_mask
};
for cascade_idx in 0..NUM_SHADOW_CASCADES {
if render_mask & (1u32 << cascade_idx) == 0 {
continue;
}
let pass_desc = MTLRenderPassDescriptor::new();
let da = pass_desc.depthAttachment();
da.setTexture(Some(self.shadow.map.as_ref()));
da.setSlice(cascade_idx);
da.setLoadAction(MTLLoadAction::Load);
da.setStoreAction(MTLStoreAction::Store);
let enc = ScopedEncoder::new(
cmd_buf
.renderCommandEncoderWithDescriptor(&pass_desc)
.ok_or("failed to get raymarch shadow render encoder")?,
"raymarch shadow",
);
enc.setCullMode(MTLCullMode::Front);
enc.set_depth_stencil(self.depth_state.as_ref());
let cascade = RaymarchShadowCascade {
cascade_idx: cascade_idx as u32,
_pad: [0; 3],
};
enc.set_fragment_value(view, 0);
enc.set_fragment_value(&lights_gpu, 2);
enc.set_vertex_value(&shadow_uniforms, 3);
enc.set_fragment_value(&shadow_uniforms, 3);
enc.set_vertex_value(&cascade, 4);
enc.set_fragment_value(&cascade, 4);
enc.set_vertex_buffer(vbuf, 0, 2);
for vol in &self.raymarch.volumes {
if !vol.visible || !vol.cast_shadows {
continue;
}
let Some(pso) = vol.shadow_pipeline.as_ref() else {
continue;
};
enc.set_pipeline(pso);
enc.set_vertex_value(&vol.uniforms, 1);
enc.set_fragment_value(&vol.uniforms, 1);
unsafe {
enc.drawIndexedPrimitives_indexCount_indexType_indexBuffer_indexBufferOffset(
MTLPrimitiveType::Triangle,
36,
MTLIndexType::UInt16,
ibuf,
0,
);
}
draws += 1;
}
}
Ok(draws)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn volume_in_frustum_culls_offscreen_boxes() {
use crate::gfx::frustum::Frustum;
let identity = [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
];
let f = Frustum::from_view_projection(identity);
assert!(volume_in_frustum([0.0, 0.0, 0.0], [0.5, 0.5, 0.5], &f));
assert!(!volume_in_frustum([10.0, 0.0, 0.0], [0.5, 0.5, 0.5], &f));
assert!(volume_in_frustum(
[0.0, 0.0, 0.0],
[100.0, 100.0, 100.0],
&f
));
}
}