Zum Inhalt

apple/tools/wgsl2msl/src/main.rs

//! 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());
}