#![deny(unsafe_op_in_unsafe_fn)]
use std::borrow::Cow;
use concinnity_core::render::error::{RenderError, RenderResult};
use concinnity_core::render::shader_programs::surface;
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_fullscreen_pipeline};
pub(super) fn ns_str(s: &str) -> Retained<NSString> {
NSString::from_str(s)
}
pub(super) const CULL_ENCODE: &str = "cull_encode.metal";
pub(super) fn cull_encode_source(hot_reload: bool) -> Cow<'static, str> {
if hot_reload {
let path = format!(
"{}/src/metal/shaders/{CULL_ENCODE}",
env!("CARGO_MANIFEST_DIR")
);
match std::fs::read_to_string(&path) {
Ok(s) => return Cow::Owned(s),
Err(e) => {
tracing::debug!(
"hot-reload: falling back to embedded source for {CULL_ENCODE} ({e})"
);
}
}
}
Cow::Borrowed(include_str!("shaders/cull_encode.metal"))
}
pub(super) fn cull_encode_library(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
hot_reload: bool,
) -> RenderResult<Retained<ProtocolObject<dyn objc2_metal::MTLLibrary>>> {
let msl = cull_encode_source(hot_reload);
super::msl_cache::compiled_library(
device,
&msl,
CULL_ENCODE,
super::metallib::embedded_metallib(CULL_ENCODE),
)
}
pub(super) const WORLD_VERTEX_ENTRY: &str = surface::MAIN_VERTEX.entry;
pub(super) const WORLD_FRAGMENT_ENTRY: &str = surface::MAIN_FRAGMENT.entry;
pub(super) const WORLD_PREPASS_VERTEX_ENTRY: &str = surface::PREPASS_VERTEX.entry;
pub(super) const WORLD_PREPASS_FRAGMENT_ENTRY: &str = surface::PREPASS_FRAGMENT.entry;
fn world_msl<'a>(
programs: &'a concinnity_core::components::ShaderPrograms,
entry: &str,
hot_reload: bool,
) -> RenderResult<Cow<'a, [u8]>> {
let req = crate::shader::surface_source::Request {
platform: concinnity_core::platform::Platform::Metal,
hot_reload,
};
crate::shader::surface_source::artifact(programs, entry, &req, crate::shader::compile::cooked)
}
fn world_label(entry: &str) -> String {
format!("world shader {entry}")
}
pub(super) fn world_function(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
hot_reload: bool,
programs: &concinnity_core::components::ShaderPrograms,
entry: &str,
) -> RenderResult<Retained<ProtocolObject<dyn objc2_metal::MTLFunction>>> {
let msl = world_msl(programs, entry, hot_reload)?;
super::msl_cache::cooked_function(device, &msl, entry, &world_label(entry))
}
pub(super) fn load_library(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
bytes: &[u8],
) -> RenderResult<Retained<ProtocolObject<dyn objc2_metal::MTLLibrary>>> {
let data = DispatchData::from_bytes(bytes);
device
.newLibraryWithData_error(&data)
.map_err(|e| RenderError::ShaderCompile(format!("{e:?}")))
}
pub(super) fn build_text_pipeline(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
swap_pixel_format: MTLPixelFormat,
hot_reload: bool,
) -> RenderResult<Retained<ProtocolObject<dyn MTLRenderPipelineState>>> {
use objc2_metal::{MTLBlendFactor, MTLVertexFormat, MTLVertexStepFunction};
let vert_fn = crate::metal::builtin_shaders::entry_function(
device,
&crate::metal::builtin_shaders::TEXT_VERT,
hot_reload,
)?;
let frag_fn = crate::metal::builtin_shaders::entry_function(
device,
&crate::metal::builtin_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| RenderError::ShaderCompile(format!("text pipeline state: {e:?}")))
}
pub(super) fn build_post_pipeline(
device: &ProtocolObject<dyn objc2_metal::MTLDevice>,
swap_pixel_format: MTLPixelFormat,
hot_reload: bool,
) -> RenderResult<Retained<ProtocolObject<dyn MTLRenderPipelineState>>> {
build_fullscreen_pipeline(
device,
&super::builtin_shaders::COMPOSITE_FRAG,
swap_pixel_format,
FullscreenBlend::Replace,
hot_reload,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn embedded_cull_encode_holds_its_kernel() {
assert!(cull_encode_source(false).contains("kernel void cull_encode("));
}
#[test]
fn hot_reload_reads_the_checkout_copy() {
assert!(cull_encode_source(true).contains("kernel void cull_encode("));
}
}