#![deny(unsafe_op_in_unsafe_fn)]
use dispatch2::DispatchData;
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_foundation::NSString;
use objc2_metal::{
MTLDevice as _, MTLPixelFormat, MTLRenderPipelineDescriptor, MTLRenderPipelineState,
};
use crate::metal::descriptors::{VertexAttr, VertexLayout, vertex_descriptor};
use crate::metal::post::fullscreen::{FullscreenBlend, build_slang_fullscreen_pipeline};
pub(super) fn ns_str(s: &str) -> Retained<NSString> {
NSString::from_str(s)
}
pub(super) fn shader_source(hot_reload: bool, name: &str) -> std::borrow::Cow<'static, str> {
let embedded: &'static str = match name {
"cull_encode.metal" => include_str!("shaders/cull_encode.metal"),
_ => panic!(
"shader_source: '{name}' is not a registered Metal shader. Add an \
`include_str!(\"shaders/{name}\")` arm to shader_source in \
metal/pipeline.rs -- every shipped shader must be registered."
),
};
if hot_reload {
let path = format!("{}/src/metal/shaders/{}", env!("CARGO_MANIFEST_DIR"), name);
match std::fs::read_to_string(&path) {
Ok(s) => return std::borrow::Cow::Owned(s),
Err(e) => {
tracing::debug!(
"hot-reload: falling back to embedded source for {} ({})",
name,
e
);
}
}
}
std::borrow::Cow::Borrowed(embedded)
}
pub(super) fn shader_library(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
hot_reload: bool,
name: &str,
) -> Result<Retained<ProtocolObject<dyn objc2_metal::MTLLibrary>>, String> {
let msl = shader_source(hot_reload, name);
if let Some((digest, bytes)) = crate::metal::metallib::embedded_metallib(name)
&& digest == concinnity_core::render::slang_source::source_digest(&msl)
{
return load_library(device, bytes)
.map_err(|e| format!("{name}: failed to load precompiled metallib: {e}"));
}
let options = objc2_metal::MTLCompileOptions::new();
device
.newLibraryWithSource_options_error(&ns_str(msl.as_ref()), Some(&options))
.map_err(|e| format!("{name}: shader compile error: {e:?}"))
}
pub(super) fn world_library(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
hot_reload: bool,
programs: &concinnity_core::components::ShaderPrograms,
entry: &str,
) -> Result<Retained<ProtocolObject<dyn objc2_metal::MTLLibrary>>, String> {
let req = crate::surface_source::Request {
platform: concinnity_core::platform::Platform::Metal,
pool_size: concinnity_core::render::uniforms::BINDLESS_POOL_SIZE,
probe_count: concinnity_core::render::uniforms::MAX_PROBES,
hot_reload,
};
let msl = crate::surface_source::artifact(programs, entry, &req)?;
let msl = std::str::from_utf8(&msl)
.map_err(|e| format!("world shader {entry}: artifact is not MSL text: {e}"))?;
super::msl_cache::compiled_library(device, msl, &format!("world shader {entry}"))
}
pub(super) fn load_library(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
bytes: &[u8],
) -> Result<Retained<ProtocolObject<dyn objc2_metal::MTLLibrary>>, String> {
let data = DispatchData::from_bytes(bytes);
device
.newLibraryWithData_error(&data)
.map_err(|e| format!("{:?}", e))
}
pub(super) fn build_text_pipeline(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
swap_pixel_format: MTLPixelFormat,
hot_reload: bool,
) -> Result<Retained<ProtocolObject<dyn MTLRenderPipelineState>>, String> {
use objc2_metal::{MTLBlendFactor, MTLVertexFormat, MTLVertexStepFunction};
let vert_fn = crate::metal::slang_shaders::entry_function(
device,
&crate::metal::slang_shaders::TEXT_VERT,
hot_reload,
)?;
let frag_fn = crate::metal::slang_shaders::entry_function(
device,
&crate::metal::slang_shaders::TEXT_FRAG,
hot_reload,
)?;
let vert_desc = vertex_descriptor(
&[
VertexAttr {
index: 0,
format: MTLVertexFormat::Float2,
offset: 0,
buffer_index: 1,
},
VertexAttr {
index: 1,
format: MTLVertexFormat::Float2,
offset: 8,
buffer_index: 1,
},
VertexAttr {
index: 2,
format: MTLVertexFormat::Float3,
offset: 16,
buffer_index: 1,
},
VertexAttr {
index: 3,
format: MTLVertexFormat::Float,
offset: 28,
buffer_index: 1,
},
],
&[VertexLayout {
buffer_index: 1,
stride: 32,
step: MTLVertexStepFunction::PerVertex,
}],
);
let pipeline_desc = MTLRenderPipelineDescriptor::new();
pipeline_desc.setVertexDescriptor(Some(&vert_desc));
pipeline_desc.setVertexFunction(Some(&vert_fn));
pipeline_desc.setFragmentFunction(Some(&frag_fn));
pipeline_desc.setRasterSampleCount(1);
unsafe {
let ca = pipeline_desc.colorAttachments().objectAtIndexedSubscript(0);
ca.setPixelFormat(swap_pixel_format);
ca.setBlendingEnabled(true);
ca.setSourceRGBBlendFactor(MTLBlendFactor::SourceAlpha);
ca.setDestinationRGBBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
ca.setSourceAlphaBlendFactor(MTLBlendFactor::One);
ca.setDestinationAlphaBlendFactor(MTLBlendFactor::OneMinusSourceAlpha);
}
device
.newRenderPipelineStateWithDescriptor_error(&pipeline_desc)
.map_err(|e| format!("failed to create text pipeline state: {:?}", e))
}
pub(super) fn build_post_pipeline(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
swap_pixel_format: MTLPixelFormat,
hot_reload: bool,
) -> Result<Retained<ProtocolObject<dyn MTLRenderPipelineState>>, String> {
build_slang_fullscreen_pipeline(
device,
&super::slang_shaders::COMPOSITE_FRAG,
swap_pixel_format,
FullscreenBlend::Replace,
hot_reload,
)
}
#[cfg(test)]
mod shader_source_tests {
use super::shader_source;
#[test]
fn embedded_path_serves_the_registered_source() {
let s = shader_source(false, "cull_encode.metal");
assert!(s.contains("kernel void cull_encode("));
}
#[test]
#[should_panic(expected = "not a registered Metal shader")]
fn unknown_name_panics() {
let _ = shader_source(false, "nope.metal");
}
#[test]
#[should_panic(expected = "not a registered Metal shader")]
fn unknown_name_panics_even_with_hot_reload() {
let _ = shader_source(true, "nope.metal");
}
#[test]
fn hot_reload_prefers_disk_when_present() {
let s = shader_source(true, "cull_encode.metal");
assert!(s.contains("kernel void cull_encode("));
}
#[test]
fn shipped_shaders_are_registered() {
const ASSEMBLED_ELSEWHERE: &[&str] = &[];
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/src/metal/shaders");
let mut checked = 0usize;
for entry in std::fs::read_dir(dir).expect("read shaders dir") {
let file_name = entry.expect("dir entry").file_name();
let name = file_name.to_str().expect("utf8 shader filename");
if !name.ends_with(".metal") || ASSEMBLED_ELSEWHERE.contains(&name) {
continue;
}
assert!(
!shader_source(false, name).trim().is_empty(),
"{name}: shader_source(false) returned empty source",
);
assert!(
!shader_source(true, name).trim().is_empty(),
"{name}: shader_source(true) returned empty source",
);
checked += 1;
}
assert!(checked > 0, "no .metal shaders found under {dir}");
}
}