//! wgsl2msl — transpile a WGSL shader to Metal Shading Language with explicit
//! Metal binding indices, plus a JSON manifest the Swift renderer uses to bind
//! buffers/textures/samplers deterministically.
//!
//! naga's CLI emits `[[user(fakeN)]]` placeholders because it doesn't know the
//! pipeline layout. We supply a per-entry-point binding map (Metal slot = WGSL
//! @binding number, per resource class) and an explicit `sizes_buffer` slot for
//! the runtime-array length buffer naga injects under its bounds-check policy.
use std::collections::BTreeMap;
use std::fs;
use naga::back::msl;
use naga::valid::{Capabilities, ValidationFlags, Validator};
/// Metal buffer slot for naga's runtime-array sizes buffer. Kept well above the
/// real bindings (which top out at @binding(3)) to avoid collisions.
const SIZES_SLOT: u8 = 16;
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() < 4 {
eprintln!("usage: wgsl2msl <in.wgsl> <out.metal> <out.json> [OVERRIDE=VALUE ...]");
std::process::exit(2);
}
let src = fs::read_to_string(&args[1]).expect("read wgsl");
let owned_module = naga::front::wgsl::parse_str(&src).expect("parse wgsl");
let owned_info = Validator::new(ValidationFlags::all(), Capabilities::all())
.validate(&owned_module)
.expect("validate");
// Bake `override` constants: explicit values from extra CLI args (e.g.
// BLUR_DIR_X=0 BLUR_DIR_Y=1 for the vertical blur variant), otherwise the
// declared defaults are used.
let mut pc = naga::back::PipelineConstants::default();
for a in args.iter().skip(4) {
if let Some((k, v)) = a.split_once('=') {
pc.insert(k.to_string(), v.parse::<f64>().expect("override value must be a number"));
}
}
let (cow_module, cow_info) =
naga::back::pipeline_constants::process_overrides(&owned_module, &owned_info, None, &pc)
.expect("process overrides");
let module = cow_module.as_ref();
let info = cow_info.as_ref();
// Build a binding map over every bound global; record a manifest entry too.
let mut resources: msl::BindingMap = BTreeMap::new();
let mut manifest: Vec<String> = Vec::new();
for (_h, var) in module.global_variables.iter() {
let Some(br) = var.binding.clone() else { continue };
let idx = br.binding as u8;
let mut target = msl::BindTarget {
buffer: None,
texture: None,
sampler: None,
external_texture: None,
mutable: false,
};
let class = match &module.types[var.ty].inner {
naga::TypeInner::Image { .. } => {
target.texture = Some(idx);
"texture"
}
naga::TypeInner::Sampler { .. } => {
target.sampler = Some(msl::BindSamplerTarget::Resource(idx));
"sampler"
}
_ => {
target.buffer = Some(idx);
if let naga::AddressSpace::Storage { access } = var.space {
target.mutable = access.contains(naga::StorageAccess::STORE);
}
"buffer"
}
};
manifest.push(format!(
" {{\"name\":\"{}\",\"group\":{},\"binding\":{},\"class\":\"{}\",\"index\":{}}}",
var.name.clone().unwrap_or_default(),
br.group,
br.binding,
class,
idx
));
resources.insert(br, target);
}
// Apply the same resource map + sizes buffer to every entry point.
let mut per_entry_point_map = msl::EntryPointResourceMap::new();
for ep in module.entry_points.iter() {
per_entry_point_map.insert(
ep.name.clone(),
msl::EntryPointResources {
resources: resources.clone(),
immediates_buffer: None,
sizes_buffer: Some(SIZES_SLOT),
},
);
}
// Default bounds-check policy matches what the verified spike used; we only
// override the binding resolution.
let options = msl::Options {
lang_version: (2, 0),
per_entry_point_map,
fake_missing_bindings: false,
..Default::default()
};
let pipeline_options = msl::PipelineOptions::default();
let (out, _trans) = msl::write_string(&module, &info, &options, &pipeline_options)
.expect("msl write");
fs::write(&args[2], out).expect("write metal");
fs::write(
&args[3],
format!(
"{{\n \"sizesBuffer\": {},\n \"resources\": [\n{}\n ]\n}}\n",
SIZES_SLOT,
manifest.join(",\n")
),
)
.expect("write json");
eprintln!("ok: {} ({} resources)", &args[1], resources.len());
}