use concinnity_host::scratch::Scratch;
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2_metal::{MTLDevice, MTLLibrary};
use std::process::Command;
use std::sync::OnceLock;
pub(super) fn compiled_library(
device: &ProtocolObject<dyn MTLDevice>,
source: &str,
label: &str,
) -> Result<Retained<ProtocolObject<dyn MTLLibrary>>, String> {
let Some(compiler) = toolchain_id() else {
tracing::debug!("{label}: no Metal toolchain, compiling from source");
return source_library(device, source);
};
let key = crate::shader_cache::Key {
compiler,
source,
entry: "main",
target: "metallib",
options: 0,
};
match crate::shader_cache::cached(&key, label, || compile_to_metallib(source, label)) {
Ok(bytes) => match super::pipeline::load_library(device, &bytes) {
Ok(library) => Ok(library),
Err(e) => {
tracing::warn!("{label}: cached metallib rejected ({e}), compiling from source");
source_library(device, source)
}
},
Err(e) => {
tracing::debug!("{label}: metallib cache unavailable ({e}), compiling from source");
source_library(device, source)
}
}
}
fn source_library(
device: &ProtocolObject<dyn MTLDevice>,
source: &str,
) -> Result<Retained<ProtocolObject<dyn MTLLibrary>>, String> {
let options = objc2_metal::MTLCompileOptions::new();
device
.newLibraryWithSource_options_error(
&objc2_foundation::NSString::from_str(source),
Some(&options),
)
.map_err(|e| format!("{e:?}"))
}
fn toolchain_id() -> Option<&'static str> {
static ID: OnceLock<Option<String>> = OnceLock::new();
ID.get_or_init(|| {
let out = Command::new("xcrun")
.args(["--sdk", "macosx", "metal", "--version"])
.output()
.ok()?;
if !out.status.success() {
return None;
}
toolchain_id_from(&String::from_utf8_lossy(&out.stdout))
})
.as_deref()
}
fn toolchain_id_from(version_output: &str) -> Option<String> {
let line = version_output
.lines()
.map(str::trim)
.find(|l| !l.is_empty())?;
Some(format!("metal {line}"))
}
fn compile_to_metallib(source: &str, label: &str) -> Result<Vec<u8>, String> {
let msl = Scratch::file("msl.metal");
let air = Scratch::file("msl.air");
let metallib = Scratch::file("msl.metallib");
std::fs::write(msl.path(), source)
.map_err(|e| format!("write {}: {e}", msl.path().display()))?;
run_step(
Command::new("xcrun")
.args(["--sdk", "macosx", "metal", "-c"])
.arg(msl.path())
.arg("-o")
.arg(air.path()),
label,
"xcrun metal",
)?;
run_step(
Command::new("xcrun")
.args(["--sdk", "macosx", "metallib"])
.arg(air.path())
.arg("-o")
.arg(metallib.path()),
label,
"xcrun metallib",
)?;
std::fs::read(metallib.path()).map_err(|e| format!("read compiled metallib: {e}"))
}
fn run_step(cmd: &mut Command, label: &str, what: &str) -> Result<(), String> {
let output = cmd
.output()
.map_err(|e| format!("{what} failed to launch for {label}: {e}"))?;
if !output.status.success() {
return Err(format!(
"{what} failed for {label}:\n{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn two_toolchain_releases_key_differently() {
let older = toolchain_id_from(
"Apple metal version 32023.404 (metalfe-32023.404)\n\
Target: air64-apple-darwin25.6.0\n",
);
let newer = toolchain_id_from(
"Apple metal version 32023.864 (metalfe-32023.864)\n\
Target: air64-apple-darwin25.6.0\n",
);
assert!(older.is_some() && newer.is_some(), "{older:?} {newer:?}");
assert_ne!(older, newer);
}
#[test]
fn the_key_names_the_metal_toolchain() {
let id = toolchain_id_from("Apple metal version 32023.864 (metalfe-32023.864)\n")
.expect("a version line yields an id");
assert!(id.starts_with("metal "), "{id}");
assert!(id.contains("32023.864"), "{id}");
}
#[test]
fn an_empty_version_report_yields_no_id() {
assert_eq!(toolchain_id_from(""), None);
assert_eq!(toolchain_id_from("\n \n\t\n"), None);
}
#[test]
fn the_release_line_is_taken_trimmed() {
assert_eq!(
toolchain_id_from("\n\n Apple metal version 32023.864 \nTarget: air64\n"),
Some("metal Apple metal version 32023.864".to_string())
);
}
}