#![allow(clippy::too_many_arguments, clippy::type_complexity)]
pub mod as_shadow;
mod emit_sidecar;
pub mod env_vars;
mod fc_specialize;
mod layout;
pub mod meta;
pub mod native;
pub mod passes;
mod passthrough;
mod primary_retry;
pub mod reflect;
mod retry;
mod spirv_binary;
mod spirv_module;
mod spirv_operand;
mod spirv_variable_ptr;
pub mod tools;
pub(crate) mod types;
pub use fc_specialize::specialize_function_constants;
pub use passthrough::translate_passthrough;
use primary_retry::*;
use crate::spirv_module::{load_bytes as load_owned_module, Module};
use std::path::Path;
pub fn detect_stage(src: &str, tmp: &Path) -> Result<passes::Stage, String> {
let ll = tools::air_to_sanitized_ll(src, tmp)?;
if ll.contains("!air.vertex =") {
Ok(passes::Stage::Vertex)
} else if ll.contains("!air.fragment =") {
Ok(passes::Stage::Fragment)
} else if ll.contains("!air.kernel =") {
Ok(passes::Stage::Kernel)
} else {
Err(
"metal2vulkan: no !air.vertex/!air.fragment/!air.kernel stage metadata in module"
.into(),
)
}
}
pub fn translate(src: &str, stage: passes::Stage, tmp: &Path) -> Result<Vec<u8>, String> {
translate_with_options(src, stage, tmp, passes::TransformOptions::default())
}
pub fn translate_with_options(
src: &str,
stage: passes::Stage,
tmp: &Path,
options: passes::TransformOptions,
) -> Result<Vec<u8>, String> {
let san_ll = tools::air_to_sanitized_ll(src, tmp)?;
translate_sanitized_native_with_options(&san_ll, stage, tmp, options)
}
pub fn translate_sanitized_native(
san_ll: &str,
stage: passes::Stage,
tmp: &Path,
) -> Result<Vec<u8>, String> {
translate_sanitized_native_with_options(san_ll, stage, tmp, passes::TransformOptions::default())
}
fn lower_async_copy_if_enabled(san_ll: &str) -> String {
native::lower_simdgroup_async_copy(san_ll)
}
struct StageMeta {
frag: Option<meta::FragMeta>,
vert: Option<meta::VertMeta>,
kern: Option<meta::KernMeta>,
promoted_kern: Option<meta::KernMeta>,
entry_name: Option<String>,
}
fn parse_stage_meta(san_ll: &str, stage: passes::Stage) -> StageMeta {
match stage {
passes::Stage::Fragment => {
let (frag, entry_name) = meta::parse_air_fragment_meta_with_entry(san_ll);
StageMeta {
frag,
vert: None,
kern: None,
promoted_kern: None,
entry_name,
}
}
passes::Stage::Vertex => {
let (vert, entry_name) = meta::parse_air_vertex_meta_with_entry(san_ll);
StageMeta {
frag: None,
vert,
kern: None,
promoted_kern: None,
entry_name,
}
}
passes::Stage::Kernel => {
let (kern, promoted_kern, entry_name) = meta::parse_air_kernel_meta_variants(san_ll);
StageMeta {
frag: None,
vert: None,
kern,
promoted_kern,
entry_name,
}
}
}
}
fn stage_buffer_layouts<'a>(
stage: passes::Stage,
frag: Option<&'a meta::FragMeta>,
vert: Option<&'a meta::VertMeta>,
kern: Option<&'a meta::KernMeta>,
) -> Option<&'a std::collections::HashMap<u32, meta::AirType>> {
match stage {
passes::Stage::Fragment => frag.map(|meta| &meta.buffer_layouts),
passes::Stage::Vertex => vert.map(|meta| &meta.buffer_layouts),
passes::Stage::Kernel => kern.map(|meta| &meta.buffer_layouts),
}
}
fn build_reflection(
stage: passes::Stage,
frag: Option<&meta::FragMeta>,
vert: Option<&meta::VertMeta>,
kern: Option<&meta::KernMeta>,
entry_name: Option<&str>,
options: &passes::TransformOptions,
) -> reflect::ShaderReflection {
match stage {
passes::Stage::Fragment => {
reflect::ShaderReflection::from_fragment(&frag.cloned().unwrap_or_default(), entry_name)
}
passes::Stage::Vertex => {
reflect::ShaderReflection::from_vertex(&vert.cloned().unwrap_or_default(), entry_name)
}
passes::Stage::Kernel => reflect::ShaderReflection::from_kernel(
&kern.cloned().unwrap_or_default(),
entry_name,
options.kernel_local_size,
),
}
}
pub fn translate_pre_psb_probe(
san_ll: &str,
stage: passes::Stage,
tmp: &Path,
) -> Result<Vec<u8>, String> {
let lowered = lower_async_copy_if_enabled(san_ll);
let san_ll = lowered.as_str();
let stage_meta = parse_stage_meta(san_ll, stage);
translate_sanitized_with_meta(
san_ll,
stage,
stage_meta.frag.as_ref(),
stage_meta.vert.as_ref(),
stage_meta.kern.as_ref(),
stage_meta.promoted_kern.as_ref(),
stage_meta.entry_name.as_deref(),
tmp,
passes::TransformOptions::default(),
false,
)
}
pub fn translate_sanitized_native_with_options(
san_ll: &str,
stage: passes::Stage,
tmp: &Path,
options: passes::TransformOptions,
) -> Result<Vec<u8>, String> {
let lowered = lower_async_copy_if_enabled(san_ll);
let san_ll = lowered.as_str();
let stage_meta = parse_stage_meta(san_ll, stage);
translate_sanitized_with_meta(
san_ll,
stage,
stage_meta.frag.as_ref(),
stage_meta.vert.as_ref(),
stage_meta.kern.as_ref(),
stage_meta.promoted_kern.as_ref(),
stage_meta.entry_name.as_deref(),
tmp,
options,
true,
)
}
pub fn translate_reflected(
src: &str,
stage: passes::Stage,
tmp: &Path,
) -> Result<(Vec<u8>, reflect::ShaderReflection), String> {
translate_reflected_with_options(src, stage, tmp, passes::TransformOptions::default())
}
pub fn translate_reflected_with_options(
src: &str,
stage: passes::Stage,
tmp: &Path,
options: passes::TransformOptions,
) -> Result<(Vec<u8>, reflect::ShaderReflection), String> {
let (san_ll, datalayout) = tools::air_to_sanitized_ll_with_datalayout(src, tmp)?;
let (spv, mut reflection) = translate_sanitized_native_reflected(&san_ll, stage, tmp, options)?;
reflection.datalayout = datalayout;
Ok((spv, reflection))
}
pub fn translate_sanitized_native_reflected(
san_ll: &str,
stage: passes::Stage,
tmp: &Path,
options: passes::TransformOptions,
) -> Result<(Vec<u8>, reflect::ShaderReflection), String> {
let lowered = lower_async_copy_if_enabled(san_ll);
let san_ll = lowered.as_str();
let stage_meta = parse_stage_meta(san_ll, stage);
let mut reflection = build_reflection(
stage,
stage_meta.frag.as_ref(),
stage_meta.vert.as_ref(),
stage_meta.kern.as_ref(),
stage_meta.entry_name.as_deref(),
&options,
);
reflection.function_constants = meta::parse_function_constants(san_ll);
let spv = translate_sanitized_with_meta(
san_ll,
stage,
stage_meta.frag.as_ref(),
stage_meta.vert.as_ref(),
stage_meta.kern.as_ref(),
stage_meta.promoted_kern.as_ref(),
stage_meta.entry_name.as_deref(),
tmp,
options,
true,
)?;
Ok((spv, reflection))
}
pub fn translate_native_no_retry(san_ll: &str, stage: passes::Stage) -> Result<Vec<u8>, String> {
let lowered = lower_async_copy_if_enabled(san_ll);
let stage_meta = parse_stage_meta(&lowered, stage);
translate_native_no_retry_with_meta(
&lowered,
stage,
stage_meta.frag.as_ref(),
stage_meta.vert.as_ref(),
stage_meta.kern.as_ref(),
stage_meta.entry_name.as_deref(),
)
}
fn translate_native_no_retry_with_meta(
san_ll: &str,
stage: passes::Stage,
frag: Option<&meta::FragMeta>,
vert: Option<&meta::VertMeta>,
kern: Option<&meta::KernMeta>,
entry_name: Option<&str>,
) -> Result<Vec<u8>, String> {
emit_finish_primary_module(
san_ll,
stage,
frag,
vert,
kern,
entry_name,
passes::TransformOptions::default(),
)
.map(|finished| finished.bytes)
}
pub fn translate_native_primary_validated(
san_ll: &str,
stage: passes::Stage,
tmp: &Path,
) -> Result<Vec<u8>, String> {
let lowered = lower_async_copy_if_enabled(san_ll);
let san_ll = lowered.as_str();
let stage_meta = parse_stage_meta(san_ll, stage);
let finished = emit_finish_primary_module(
san_ll,
stage,
stage_meta.frag.as_ref(),
stage_meta.vert.as_ref(),
stage_meta.kern.as_ref(),
stage_meta.entry_name.as_deref(),
passes::TransformOptions::default(),
)?;
let primary_module = finished.module;
let primary = finished.bytes;
let Err(validation_error) = tools::spirv_val_bytes(&primary, tmp) else {
return Ok(primary);
};
if let Some(primary) = primary_xbind_phi_psb_for_validation_error(
&primary_module,
&primary,
&validation_error,
tmp,
) {
return Ok(primary);
}
let retry = retry::RetryCtx::new(
san_ll,
stage,
stage_meta.frag.as_ref(),
stage_meta.vert.as_ref(),
stage_meta.kern.as_ref(),
stage_meta.promoted_kern.as_ref(),
stage_meta.entry_name.as_deref(),
tmp,
passes::TransformOptions::default(),
true,
);
Ok(primary_primitive_phi_metadata_if_needed(
&validation_error,
san_ll,
stage,
stage_meta.frag.as_ref(),
stage_meta.vert.as_ref(),
stage_meta.kern.as_ref(),
stage_meta.entry_name.as_deref(),
tmp,
passes::TransformOptions::default(),
)
.or_else(|| primary_pointer_typing_raw_if_needed(&validation_error, &retry))
.or_else(|| {
primary_pointer_typing_wide_raw_guard_chain_if_needed(
&validation_error,
&primary,
&primary_module,
&retry,
)
})
.or_else(|| primary_other_prune_if_needed(&validation_error, &primary, tmp))
.or_else(|| {
primary_wide_raw_store_guard_cfg_chain_if_needed(
&validation_error,
&primary,
&primary_module,
&retry,
)
})
.or_else(|| primary_construct_tree_if_needed(&validation_error, &retry))
.or_else(|| primary_relooper_if_needed(&validation_error, &primary, tmp))
.or_else(|| primary_cfg_prune_then_relooper_if_needed(&validation_error, &primary, tmp))
.or_else(|| {
let original = finished
.original_module
.as_ref()
.map(assemble_finished_module)
.unwrap_or_else(|| primary.clone());
primary_cfg_raw_then_relooper_if_needed(&validation_error, &original, &retry)
})
.or_else(|| {
primary_dynamic_struct_index_inline_sroa_relooper_if_needed(
&validation_error,
san_ll,
stage,
stage_meta.frag.as_ref(),
stage_meta.vert.as_ref(),
stage_meta.kern.as_ref(),
stage_meta.entry_name.as_deref(),
tmp,
passes::TransformOptions::default(),
)
})
.or_else(|| {
primary_logical_pointer_inline_sroa_raw_if_needed(
&validation_error,
san_ll,
stage,
stage_meta.frag.as_ref(),
stage_meta.vert.as_ref(),
stage_meta.kern.as_ref(),
stage_meta.entry_name.as_deref(),
tmp,
passes::TransformOptions::default(),
)
})
.or_else(|| {
primary_opaque_image_select_module_if_needed(
&validation_error,
&primary_module,
&primary,
tmp,
)
})
.unwrap_or(primary))
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum FinishRewrites {
Plain,
Primary,
}
#[derive(Clone)]
struct FinishedModule {
module: Module,
bytes: Vec<u8>,
original_module: Option<Module>,
}
#[cfg(test)]
thread_local! {
static FINISH_ASSEMBLE_COUNT: std::cell::Cell<usize> = const {
std::cell::Cell::new(0)
};
}
fn assemble_finished_module(module: &Module) -> Vec<u8> {
#[cfg(test)]
FINISH_ASSEMBLE_COUNT.with(|count| count.set(count.get() + 1));
module
.assemble()
.iter()
.flat_map(|word| word.to_le_bytes())
.collect()
}
#[cfg(test)]
fn reset_finish_assemble_count() {
FINISH_ASSEMBLE_COUNT.with(|count| count.set(0));
}
#[cfg(test)]
fn finish_assemble_count() -> usize {
FINISH_ASSEMBLE_COUNT.with(std::cell::Cell::get)
}
fn finish_module(
emitted: emit_sidecar::EmittedSpirv,
stage: passes::Stage,
frag: Option<&meta::FragMeta>,
vert: Option<&meta::VertMeta>,
kern: Option<&meta::KernMeta>,
entry_name: Option<&str>,
options: passes::TransformOptions,
rewrites: FinishRewrites,
) -> Result<FinishedModule, String> {
let (mut out, sidecar) = passes::transform_with_options_and_sidecar(
emitted.module,
emitted.sidecar,
stage,
frag,
vert,
kern,
entry_name,
options,
)?;
native::rewrite_logical_pointer_phis_module(&mut out);
native::rewrite_integer_width_phis_module(&mut out);
native::demote_nondominating_values_module(&mut out);
native::split_multientry_loop_selection_exits_module(&mut out);
let mut retained_global_ids = sidecar
.local_pointer_field_stores
.iter()
.map(|fact| fact.id)
.collect::<Vec<_>>();
passes::canonicalize_ids_and_remap(&mut out, &mut retained_global_ids);
let original_module = (rewrites == FinishRewrites::Primary).then(|| out.clone());
if rewrites == FinishRewrites::Primary {
primary_retry::apply_primary_emit_rewrites_module(&mut out, &mut retained_global_ids);
}
let bytes = assemble_finished_module(&out);
Ok(FinishedModule {
module: out,
bytes,
original_module,
})
}
pub fn translate_raw_tiers_probe(
san_ll: &str,
stage: passes::Stage,
tmp: &Path,
) -> Vec<Result<Vec<u8>, String>> {
let lowered = lower_async_copy_if_enabled(san_ll);
let san_ll = lowered.as_str();
let stage_meta = parse_stage_meta(san_ll, stage);
let opts = passes::TransformOptions::default();
let run = |emitted: Result<emit_sidecar::EmittedSpirv, String>| -> Result<Vec<u8>, String> {
emitted.and_then(|b| {
finish_module(
b,
stage,
stage_meta.frag.as_ref(),
stage_meta.vert.as_ref(),
stage_meta.kern.as_ref(),
stage_meta.entry_name.as_deref(),
opts,
FinishRewrites::Plain,
)
.map(|finished| finished.bytes)
})
};
vec![
run(tools::llc_vulkan_spirv_all_buffers_raw_with_sidecar(
san_ll,
tmp,
stage_meta.kern.as_ref(),
stage_meta.entry_name.as_deref(),
stage_buffer_layouts(
stage,
stage_meta.frag.as_ref(),
stage_meta.vert.as_ref(),
stage_meta.kern.as_ref(),
),
)),
run(
tools::llc_vulkan_spirv_all_buffers_raw_with_workgroup_sidecar(
san_ll,
tmp,
stage_meta.kern.as_ref(),
stage_meta.entry_name.as_deref(),
stage_buffer_layouts(
stage,
stage_meta.frag.as_ref(),
stage_meta.vert.as_ref(),
stage_meta.kern.as_ref(),
),
),
),
]
}
pub fn translate_bda_probe(
san_ll: &str,
stage: passes::Stage,
tmp: &Path,
) -> Result<Vec<u8>, String> {
let lowered = lower_async_copy_if_enabled(san_ll);
let san_ll = lowered.as_str();
let stage_meta = parse_stage_meta(san_ll, stage);
let opts = passes::TransformOptions::default();
tools::llc_vulkan_spirv_all_buffers_raw_bda_with_sidecar(
san_ll,
tmp,
stage_meta.kern.as_ref(),
stage_meta.entry_name.as_deref(),
stage_buffer_layouts(
stage,
stage_meta.frag.as_ref(),
stage_meta.vert.as_ref(),
stage_meta.kern.as_ref(),
),
)
.and_then(|b| {
finish_module(
b,
stage,
stage_meta.frag.as_ref(),
stage_meta.vert.as_ref(),
stage_meta.kern.as_ref(),
stage_meta.entry_name.as_deref(),
opts,
FinishRewrites::Plain,
)
.map(|finished| finished.bytes)
})
}
fn translate_sanitized_with_meta(
san_ll: &str,
stage: passes::Stage,
frag: Option<&meta::FragMeta>,
vert: Option<&meta::VertMeta>,
kern: Option<&meta::KernMeta>,
promoted_kern: Option<&meta::KernMeta>,
entry_name: Option<&str>,
tmp: &Path,
options: passes::TransformOptions,
psb_retry_enabled: bool,
) -> Result<Vec<u8>, String> {
let rc = retry::RetryCtx::new(
san_ll,
stage,
frag,
vert,
kern,
promoted_kern,
entry_name,
tmp,
options,
psb_retry_enabled,
);
let retry_debug_on = rc.retry_debug_on;
let translated = match tools::llc_vulkan_spirv_with_sidecar(
san_ll,
tmp,
rc.kern,
rc.entry_name,
stage_buffer_layouts(rc.stage, rc.frag, rc.vert, rc.kern),
)
.and_then(|b| {
finish_module(
b,
rc.stage,
rc.frag,
rc.vert,
rc.kern,
rc.entry_name,
rc.options,
FinishRewrites::Primary,
)
}) {
Ok(finished) => {
let primary_module = finished.module;
let primary = finished.bytes;
let out_module = finished
.original_module
.expect("primary finish always retains canonical pre-rewrite module");
let primary_validation = tools::spirv_val_bytes(&primary, tmp);
if primary_validation.is_ok() {
if retry_debug_on {
eprintln!("[retry-debug] default emission validated in-translate");
}
Ok(primary)
} else {
let out = assemble_finished_module(&out_module);
let validation_error = primary_validation.expect_err("checked above");
if let Some(primary) = primary_xbind_phi_psb_for_validation_error(
&primary_module,
&primary,
&validation_error,
tmp,
) {
Ok(primary)
} else if let Some(primary) = primary_primitive_phi_metadata_if_needed(
&validation_error,
san_ll,
rc.stage,
rc.frag,
rc.vert,
rc.kern,
rc.entry_name,
tmp,
rc.options,
) {
Ok(primary)
} else if let Some(primary) =
primary_pointer_typing_raw_if_needed(&validation_error, &rc)
{
Ok(primary)
} else if let Some(primary) = primary_pointer_typing_wide_raw_guard_chain_if_needed(
&validation_error,
&primary,
&primary_module,
&rc,
) {
Ok(primary)
} else if let Some(primary) =
primary_other_prune_if_needed(&validation_error, &primary, tmp)
{
Ok(primary)
} else if let Some(primary) = primary_wide_raw_store_guard_cfg_chain_if_needed(
&validation_error,
&primary,
&primary_module,
&rc,
) {
Ok(primary)
} else if let Some(primary) =
primary_construct_tree_if_needed(&validation_error, &rc)
{
Ok(primary)
} else if let Some(primary) =
primary_cfg_prune_then_relooper_if_needed(&validation_error, &primary, tmp)
{
Ok(primary)
} else if let Some(primary) =
primary_dynamic_struct_index_inline_sroa_relooper_if_needed(
&validation_error,
san_ll,
rc.stage,
rc.frag,
rc.vert,
rc.kern,
rc.entry_name,
tmp,
rc.options,
)
{
Ok(primary)
} else if let Some(primary) = primary_logical_pointer_inline_sroa_raw_if_needed(
&validation_error,
san_ll,
rc.stage,
rc.frag,
rc.vert,
rc.kern,
rc.entry_name,
tmp,
rc.options,
) {
Ok(primary)
} else if let Some(primary) = primary_opaque_image_select_module_if_needed(
&validation_error,
&primary_module,
&primary,
tmp,
) {
Ok(primary)
} else {
match tools::spirv_val_bytes(&out, tmp) {
Ok(()) => {
if retry_debug_on {
eprintln!("[retry-debug] default emission validated in-translate");
}
Ok(out)
}
Err(e)
if native::classify_validation_error(&e)
== native::ValidationClass::PointerTyping =>
{
let fc_promote_first = e
.contains("reached non-composite")
.then(|| {
rc.census("val-ptr:fc_promote_logical", rc.fc_promote_logical())
})
.flatten();
Ok(fc_promote_first
.or_else(|| rc.census("val-ptr:raw_retry", rc.raw_retry()))
.or_else(|| rc.census("val-ptr:prune", rc.prune_retry(&out)))
.or_else(|| {
rc.census(
"val-ptr:subword_pack",
rc.subword_pack_retry(&out_module),
)
})
.or_else(|| {
rc.census(
"val-ptr:prune_then_relooper",
rc.prune_then_relooper(&out),
)
})
.or_else(|| {
rc.raw_then_psb_chain(
"val-ptr:raw_then_relooper",
"val-ptr:raw_psb",
)
})
.or_else(|| {
rc.inline_sroa_chain(
"val-ptr:inline_sroa",
"val-ptr:inline_sroa_raw",
)
})
.or_else(|| {
rc.census("val-ptr:fc_promote_psb", rc.fc_promote_psb())
})
.unwrap_or(out))
}
Err(e)
if native::classify_validation_error(&e)
== native::ValidationClass::CfgStructurization =>
{
Ok({
if retry_debug_on {
let head: Vec<&str> = e.lines().take(4).collect();
eprintln!(
"[retry-debug] default module failed cfg-class spirv-val: {}",
head.join(" | ")
);
}
rc.census("val-cfg:construct_tree", rc.construct_tree_retry())
.or_else(|| {
rc.census("val-cfg:relooper", rc.relooper_retry(&out))
})
}
.or_else(|| {
rc.census(
"val-cfg:prune_then_relooper",
rc.prune_then_relooper(&out),
)
})
.or_else(|| {
rc.census("val-cfg:relooper_huge", rc.relooper_retry_huge(&out))
})
.or_else(|| {
rc.census(
"val-cfg:relooper_then_value_select",
rc.relooper_then_value_select(&out),
)
})
.or_else(|| {
rc.census("val-cfg:raw_then_relooper", rc.raw_then_relooper())
})
.or_else(|| {
rc.inline_sroa_chain(
"val-cfg:inline_sroa",
"val-cfg:inline_sroa_raw",
)
})
.or_else(|| {
rc.census(
"val-cfg:inline_sroa_raw_cfg_restructure",
rc.inline_sroa_raw_cfg_restructure_retry(),
)
})
.unwrap_or(out))
}
Err(e)
if native::classify_validation_error(&e)
== native::ValidationClass::LogicalPointerPhi =>
{
Ok(rc
.census("val-phi:phi_index", rc.phi_index_retry(&out_module))
.or_else(|| rc.census("val-phi:prune", rc.prune_retry(&out)))
.unwrap_or(out))
}
Err(e)
if native::classify_validation_error(&e)
== native::ValidationClass::CrossBindingPointerMerge =>
{
Ok(rc
.census("val-xbind:value_select", rc.value_select_retry(&out))
.or_else(|| rc.census("val-xbind:psb", rc.psb_retry(&out)))
.or_else(|| rc.census("val-xbind:raw_psb", rc.raw_psb()))
.or_else(|| rc.census("val-xbind:prune", rc.prune_retry(&out)))
.unwrap_or(out))
}
Err(_) => Ok(rc
.census("val-other:prune", rc.prune_retry(&out))
.or_else(|| {
rc.inline_sroa_chain(
"val-other:inline_sroa",
"val-other:inline_sroa_raw",
)
})
.or_else(|| {
rc.census(
"val-other:prune_then_relooper",
rc.prune_then_relooper(&out),
)
})
.or_else(|| {
rc.census("val-other:raw_then_relooper", rc.raw_then_relooper())
})
.or_else(|| {
rc.census("val-other:value_select", rc.value_select_retry(&out))
})
.unwrap_or(out)),
}
}
}
}
Err(emit_err)
if native::classify_emit_error(&emit_err) == native::EmitErrorClass::PointerTyping =>
{
rc.census("emit-ptr:raw_retry", rc.raw_retry())
.or_else(|| rc.raw_then_psb_chain("emit-ptr:raw_then_relooper", "emit-ptr:raw_psb"))
.or_else(|| rc.census("emit-ptr:bda", rc.bda_retry()))
.or_else(|| {
rc.inline_sroa_chain("emit-ptr:inline_sroa", "emit-ptr:inline_sroa_raw")
})
.ok_or(emit_err)
}
Err(emit_err) => rc
.census("emit-other:bda", rc.bda_retry())
.or_else(|| rc.raw_then_psb_chain("emit-other:raw_then_relooper", "emit-other:raw_psb"))
.ok_or(emit_err),
};
if crate::env_vars::tier_census() {
let label = match &translated {
Ok(_) => rc.adopted_tier().unwrap_or("default"),
Err(_) => "fallback",
};
eprintln!("[tier-census] {label}");
}
translated.map(|out| {
let candidate = load_owned_module(&out).ok().and_then(|mut module| {
native::rewrite_variable_pointer_phis_module(&mut module).ok()?;
let bytes = module
.assemble()
.iter()
.flat_map(|word| word.to_le_bytes())
.collect::<Vec<_>>();
rc.validates_dbg("variable_pointer_phi_normalization", &bytes)
.then_some(bytes)
});
candidate.unwrap_or(out)
})
}
pub fn canonicalize_spirv_bytes(spv: &[u8]) -> Result<Vec<u8>, String> {
let mut module = load_owned_module(spv).map_err(|e| format!("SPIR-V load: {e:?}"))?;
passes::canonicalize_ids(&mut module);
Ok(module
.assemble()
.iter()
.flat_map(|w| w.to_le_bytes())
.collect())
}
pub fn disassemble(spv: &[u8]) -> Result<String, String> {
let m = load_owned_module(spv).map_err(|e| format!("SPIR-V load: {e:?}"))?;
Ok(m.disassemble())
}
#[cfg(test)]
mod single_meta_parse_tests {
use super::*;
const SIMPLE_KERNEL: &str = r#"
define void @k(ptr addrspace(1) %out) {
entry:
store i32 7, ptr addrspace(1) %out, align 4
ret void
}
!air.kernel = !{!0}
!0 = !{ptr @k, !1, !2}
!1 = !{}
!2 = !{!3}
!3 = !{i32 0, !"air.buffer", !"air.location_index", i32 0, i32 1, !"air.write", !"air.address_space", i32 1, !"air.arg_type_size", i32 4, !"air.arg_type_align_size", i32 4, !"air.arg_type_name", !"uint", !"air.arg_name", !"out"}
"#;
#[test]
fn production_kernel_emit_reuses_one_stage_meta_parse() {
meta::reset_air_meta_parse_count();
let stage_meta = parse_stage_meta(SIMPLE_KERNEL, passes::Stage::Kernel);
tools::llc_vulkan_spirv_with_sidecar(
SIMPLE_KERNEL,
Path::new(""),
stage_meta.kern.as_ref(),
stage_meta.entry_name.as_deref(),
stage_buffer_layouts(
passes::Stage::Kernel,
stage_meta.frag.as_ref(),
stage_meta.vert.as_ref(),
stage_meta.kern.as_ref(),
),
)
.expect("production emitter consumes threaded metadata");
assert_eq!(meta::air_meta_parse_count(), 1);
}
#[test]
fn validating_primary_finish_assembles_once() {
let tmp =
std::env::temp_dir().join(format!("metal2vulkan_finish_once_{}", std::process::id()));
let _ = std::fs::create_dir_all(&tmp);
reset_finish_assemble_count();
let spv = translate_sanitized_native(SIMPLE_KERNEL, passes::Stage::Kernel, &tmp)
.expect("simple primary translation validates");
tools::spirv_val_bytes(&spv, &tmp).expect("simple primary spirv-val");
assert_eq!(
finish_assemble_count(),
1,
"a validating primary must not assemble fallback bytes"
);
}
}