#![allow(unsafe_code)]
use vyre_driver::backend::{BackendError, VyreBackend};
pub const METAL_BACKEND_ID: &str = "metal";
#[cfg(any(target_os = "macos", target_os = "ios"))]
mod runtime;
#[cfg(any(target_os = "macos", target_os = "ios"))]
pub use runtime::MetalBackend;
#[cfg(any(target_os = "macos", target_os = "ios"))]
pub use runtime::{
metal_resident_scan_resource_table, MetalResidentScanResourceEntry,
MetalResidentScanResourceError, MetalResidentScanResourceLifetime,
MetalResidentScanResourceTableEvidence, METAL_RESIDENT_SCAN_RESOURCE_TABLE_SCHEMA_VERSION,
};
#[cfg(any(target_os = "macos", target_os = "ios"))]
pub fn acquire() -> Result<Box<dyn VyreBackend>, BackendError> {
MetalBackend::acquire().map(|backend| Box::new(backend) as Box<dyn VyreBackend>)
}
#[cfg(not(any(target_os = "macos", target_os = "ios")))]
pub fn acquire() -> Result<Box<dyn VyreBackend>, BackendError> {
Err(BackendError::UnsupportedFeature {
name: "Apple Metal.framework native runtime".to_string(),
backend: METAL_BACKEND_ID.to_string(),
})
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
inventory::submit! {
vyre_driver::backend::BackendRegistration {
id: METAL_BACKEND_ID,
factory: acquire,
supported_ops: vyre_driver::backend::core_supported_ops,
}
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
inventory::submit! {
vyre_driver::backend::BackendCapability {
id: METAL_BACKEND_ID,
dispatches: true,
}
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
inventory::submit! {
vyre_driver::backend::BackendPrecedence {
id: METAL_BACKEND_ID,
rank: 25,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(any(target_os = "macos", target_os = "ios")))]
#[test]
fn non_apple_acquire_fails_actionably() {
let error = match acquire() {
Ok(_) => panic!("non-Apple builds must not fabricate a Metal backend"),
Err(error) => error,
};
let message = error.to_string();
assert!(
message.contains("Apple Metal.framework") && message.contains("Fix:"),
"non-Apple Metal acquisition must be actionable: {message}"
);
}
#[cfg(not(any(target_os = "macos", target_os = "ios")))]
#[test]
fn non_apple_build_does_not_register_fake_backend() {
let registered = vyre_driver::backend::registered_backends();
assert!(
registered
.iter()
.all(|backend| backend.id != METAL_BACKEND_ID),
"non-Apple builds must not submit a fake `metal` backend registration"
);
}
#[test]
fn resident_batch_download_uses_backend_neutral_fusion_contract() {
let source = include_str!("runtime.rs");
let method = source
.split("fn download_resident_ranges_into(")
.nth(1)
.and_then(|tail| tail.split("fn free_resident(").next())
.expect("Fix: Metal runtime must expose download_resident_ranges_into before free_resident.");
assert!(
method.contains("fuse_resident_transfer_intervals(&copies)")
&& method.contains("reserve_fused_resident_view_outputs")
&& method.contains("copy_fused_resident_view_into")
&& !method.contains(
"self.download_resident_range_into(resource, *byte_offset, *byte_len, output)"
),
"Fix: Metal resident ranged batch download must reuse backend-neutral interval fusion instead of looping one readback per requested range."
);
}
#[test]
fn compile_native_uses_real_metal_compiled_pipeline_contract() {
let source = include_str!("runtime.rs");
assert!(
source.contains("fn compile_native(")
&& source.contains("fn compile_native_shared(")
&& source.contains("struct MetalPersistentPipeline")
&& source.contains("impl CompiledPipeline for MetalPersistentPipeline")
&& source.contains("dispatch_planned_buffers_with_queue(")
&& source.contains("Ok(Some(Arc::new(MetalPersistentPipeline"),
"Fix: Metal compile_native must return a real CompiledPipeline object over the shared command path, not inherit the Ok(None) passthrough."
);
}
#[test]
fn compiled_pipeline_resident_handles_share_backend_table_contract() {
let source = include_str!("runtime.rs");
assert!(
source.contains("resident_buffers: MetalResidentBufferTable")
&& source.contains("resident_buffers: Arc::new(Mutex::new(BTreeMap::new()))")
&& source.contains("resident_buffers: Arc::clone(&self.resident_buffers)")
&& source.contains("fn dispatch_persistent_handles_timed(")
&& source.contains("resolve_resident_resources_from_table")
&& source.contains("plan_resident_buffers("),
"Fix: Metal compiled persistent-handle dispatch must share the backend resident table and reuse resident buffer planning instead of reporting UnsupportedFeature."
);
}
#[test]
fn compiled_pipeline_resource_outputs_avoid_host_readback_contract() {
let source = include_str!("runtime.rs");
let method = source
.split("fn dispatch_persistent_resource_outputs(")
.nth(1)
.and_then(|tail| tail.split("fn lock_resident_buffer_table").next())
.expect("Fix: Metal compiled pipeline must implement dispatch_persistent_resource_outputs before resident table helpers.");
assert!(
method.contains("resident_output_resources(&base_plan, inputs)?")
&& method.contains("submit_planned_buffers_with_queue(")
&& !method.contains("dispatch_persistent_handles_timed")
&& !method.contains("collect_outputs("),
"Fix: Metal compiled resource-output dispatch must submit the compiled resident command and return resident handles without host readback."
);
}
#[test]
fn pipeline_cache_miss_reasons_use_shared_classifier_contract() {
let source = include_str!("runtime.rs");
assert!(
source.contains("PipelineCacheIdentity")
&& source.contains("PipelineCacheMissReason")
&& source.contains("PipelineCacheIdentity::try_from_program(program, config, fingerprint)")
&& source.contains("PipelineCacheMissReason::classify_identities(")
&& source.contains("metal_pipeline_cache_miss_empty_cache")
&& source.contains("metal_pipeline_cache_miss_program_changed")
&& source.contains("metal_pipeline_cache_miss_dispatch_policy_changed")
&& source.contains("metal_pipeline_cache_miss_device_or_runtime_changed")
&& source.contains("metal_pipeline_cache_miss_key_absent")
&& source.contains("metal_buffer_allocation_count")
&& source.contains("metal_buffer_allocation_bytes")
&& source.contains("metal_host_to_device_copy_count")
&& source.contains("metal_host_to_device_bytes")
&& source.contains("metal_device_to_host_copy_count")
&& source.contains("metal_device_to_host_bytes")
&& source.contains("metal_output_readback_bytes")
&& !source.contains("struct MetalPipelineCacheIdentity")
&& !source.contains("classify_metal_pipeline_cache_miss"),
"Fix: Metal pipeline-cache miss telemetry must use the shared identity/classifier seam and expose stable cache, allocation, copy, and readback counters for benchmark gates."
);
}
#[test]
fn compiled_pipeline_metrics_share_backend_snapshot_contract() {
let source = include_str!("runtime.rs");
assert!(
source.contains("type MetalMetricCounters = Arc<MetalMetrics>")
&& source.contains("metrics: MetalMetricCounters")
&& source.contains("metrics: Arc::clone(&self.metrics)")
&& source.contains("Vec::with_capacity(16)")
&& source
.matches("record_planned_buffer_metrics(&self.metrics, &buffers)")
.count()
>= 3
&& source
.matches("record_output_readback_metrics(&self.metrics, &result.outputs)")
.count()
>= 2,
"Fix: compiled Metal dispatch paths must account allocation, host-copy, and readback counters into the same backend metric snapshot as direct dispatch."
);
}
#[test]
fn resident_scan_resource_table_has_argument_buffer_lifetime_contract() {
let source = include_str!("runtime.rs");
assert!(
source.contains("pub struct MetalResidentScanResourceEntry")
&& source.contains("pub enum MetalResidentScanResourceLifetime")
&& source.contains("pub fn metal_resident_scan_resource_table(")
&& source.contains("argument_buffer_entry")
&& source.contains("shared_scan_metadata_digest")
&& source.contains("metal_resource_metadata_digest")
&& source.contains("DuplicateArgumentBufferEntry")
&& source.contains("LifetimeMismatch"),
"Fix: Metal resident scan tables must expose argument-buffer entry, metadata parity, and lifetime validation before resident scan dispatch."
);
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_acquire_registers_dispatch_backend() {
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice for native dispatch.",
);
assert_eq!(backend.id(), METAL_BACKEND_ID);
assert!(
vyre_driver::backend::registered_backends()
.iter()
.any(|registration| registration.id == METAL_BACKEND_ID),
"Fix: Apple Metal builds must submit a real backend registration."
);
assert!(
vyre_driver::backend::backend_dispatches(METAL_BACKEND_ID),
"Fix: Apple Metal registration must declare live dispatch capability."
);
assert_eq!(
vyre_driver::backend::backend_precedence(METAL_BACKEND_ID),
25,
"Fix: Metal precedence must stay above portable fallbacks only after live native dispatch exists."
);
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_dispatches_store_literal_program() {
use vyre_driver::DispatchConfig;
use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
let program = Program::wrapped(
vec![
BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
.with_count(1)
.with_output_byte_range(0..4),
],
[1, 1, 1],
vec![Node::store("out", Expr::u32(0), Expr::u32(42))],
);
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before dispatch.",
);
let outputs = backend
.dispatch(&program, &[], &DispatchConfig::default())
.expect("Fix: native Metal must execute a one-store u32 Program end to end.");
assert_eq!(outputs, vec![42u32.to_le_bytes().to_vec()]);
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_native_metal_matches_wgpu_on_same_program_bytes() {
use vyre_driver::{DispatchConfig, VyreBackend as _};
use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
let idx = Expr::var("idx");
let program = Program::wrapped(
vec![
BufferDecl::storage("a", 0, BufferAccess::ReadOnly, DataType::U32).with_count(8),
BufferDecl::storage("b", 1, BufferAccess::ReadOnly, DataType::U32).with_count(8),
BufferDecl::storage("out", 2, BufferAccess::WriteOnly, DataType::U32)
.with_count(8)
.with_output_byte_range(0..32),
],
[8, 1, 1],
vec![
Node::let_bind("idx", Expr::gid_x()),
Node::if_then(
Expr::lt(idx.clone(), Expr::u32(8)),
vec![Node::store(
"out",
idx.clone(),
Expr::add(
Expr::load("a", idx.clone()),
Expr::mul(Expr::load("b", idx), Expr::u32(3)),
),
)],
),
],
);
let a = [1u32, 2, 3, 4, 5, 6, 7, 8]
.into_iter()
.flat_map(u32::to_le_bytes)
.collect::<Vec<_>>();
let b = [10u32, 11, 12, 13, 14, 15, 16, 17]
.into_iter()
.flat_map(u32::to_le_bytes)
.collect::<Vec<_>>();
let expected = [31u32, 35, 39, 43, 47, 51, 55, 59]
.into_iter()
.flat_map(u32::to_le_bytes)
.collect::<Vec<_>>();
let metal = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before differential dispatch.",
);
let wgpu = vyre_driver_wgpu::WgpuBackend::acquire()
.expect("Fix: WGPU-on-Metal must acquire on the Apple GPU differential lane.");
let config = DispatchConfig::default();
let metal_outputs = metal
.dispatch(&program, &[a.clone(), b.clone()], &config)
.expect("Fix: native Metal must dispatch the differential Program.");
let wgpu_outputs = wgpu
.dispatch(&program, &[a, b], &config)
.expect("Fix: WGPU-on-Metal must dispatch the same differential Program.");
assert_eq!(
metal_outputs,
vec![expected.clone()],
"Fix: native Metal output must match the explicit byte oracle before comparing backends."
);
assert_eq!(
wgpu_outputs,
vec![expected],
"Fix: WGPU-on-Metal output must match the explicit byte oracle before comparing backends."
);
assert_eq!(
metal_outputs, wgpu_outputs,
"Fix: native Metal and WGPU-on-Metal must produce byte-identical outputs for the same Program."
);
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_dispatch_handles_empty_and_unaligned_output_ranges() {
use vyre_driver::DispatchConfig;
use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
let program = Program::wrapped(
vec![
BufferDecl::storage("empty", 0, BufferAccess::WriteOnly, DataType::U32)
.with_count(0)
.with_output_byte_range(0..0),
BufferDecl::storage("word", 1, BufferAccess::WriteOnly, DataType::U32)
.with_count(1)
.with_output_byte_range(1..2),
],
[1, 1, 1],
vec![Node::store("word", Expr::u32(0), Expr::u32(0x1122_3344))],
);
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before boundary dispatch.",
);
let outputs = backend
.dispatch(&program, &[], &DispatchConfig::default())
.expect(
"Fix: native Metal must honor shared empty and unaligned output layout planning.",
);
assert_eq!(
outputs,
vec![Vec::new(), vec![0x33]],
"Fix: Metal output collection must preserve empty outputs and trim unaligned one-byte ranges from the stored word."
);
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_dispatch_config_errors_are_actionable() {
use vyre_driver::DispatchConfig;
use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
let program = Program::wrapped(
vec![
BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
.with_count(1)
.with_output_byte_range(0..4),
],
[1, 1, 1],
vec![Node::store("out", Expr::u32(0), Expr::u32(42))],
);
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before negative dispatch tests.",
);
let mut cooperative = DispatchConfig::default();
cooperative.cooperative = true;
let cooperative_error = backend
.dispatch(&program, &[], &cooperative)
.expect_err("Fix: native Metal must reject cooperative dispatch until implemented.")
.to_string();
assert!(
cooperative_error.contains("Metal cooperative grid dispatch")
&& cooperative_error.contains("metal"),
"Fix: cooperative dispatch rejection must name the unsupported Metal feature and backend: {cooperative_error}"
);
let mut zero_iterations = DispatchConfig::default();
zero_iterations.fixpoint_iterations = Some(0);
let zero_error = backend
.dispatch(&program, &[], &zero_iterations)
.expect_err("Fix: native Metal must reject explicit zero fixpoint iterations.")
.to_string();
assert!(
zero_error.contains("fixpoint_iterations=0") && zero_error.contains("Fix:"),
"Fix: zero-iteration dispatch rejection must include actionable fix text: {zero_error}"
);
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_dispatch_grid_uses_declared_output_count_not_trimmed_readback() {
use vyre_driver::DispatchConfig;
use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
let local = Expr::var("local");
let token = Expr::var("token");
let program = Program::wrapped(
vec![
BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
.with_count(512)
.with_output_byte_range(0..8),
],
[256, 1, 1],
vec![
Node::let_bind("local", Expr::LocalId { axis: 0 }),
Node::let_bind("token", Expr::WorkgroupId { axis: 0 }),
Node::if_then(
Expr::and(
Expr::eq(local, Expr::u32(0)),
Expr::lt(token.clone(), Expr::u32(2)),
),
vec![Node::store(
"out",
token.clone(),
Expr::add(token, Expr::u32(11)),
)],
),
],
);
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before dispatch.",
);
let outputs = backend
.dispatch(&program, &[], &DispatchConfig::default())
.expect("Fix: native Metal must infer grid from declared dispatch domain.");
assert_eq!(
outputs,
vec![[11u32.to_le_bytes(), 12u32.to_le_bytes()].concat()]
);
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_dispatch_allocates_threadgroup_memory() {
use vyre_driver::DispatchConfig;
use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
let local = Expr::var("local");
let program = Program::wrapped(
vec![
BufferDecl::storage("values", 0, BufferAccess::ReadOnly, DataType::U32)
.with_count(4),
BufferDecl::workgroup("scratch", 4, DataType::U32),
BufferDecl::storage("out", 1, BufferAccess::WriteOnly, DataType::U32)
.with_count(1)
.with_output_byte_range(0..4),
],
[4, 1, 1],
vec![
Node::let_bind("local", Expr::LocalId { axis: 0 }),
Node::if_then(
Expr::lt(local.clone(), Expr::u32(4)),
vec![Node::store(
"scratch",
local.clone(),
Expr::load("values", local.clone()),
)],
),
Node::barrier(),
Node::if_then(
Expr::eq(local, Expr::u32(0)),
vec![Node::store(
"out",
Expr::u32(0),
Expr::add(
Expr::add(
Expr::load("scratch", Expr::u32(0)),
Expr::load("scratch", Expr::u32(1)),
),
Expr::add(
Expr::load("scratch", Expr::u32(2)),
Expr::load("scratch", Expr::u32(3)),
),
),
)],
),
],
);
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before dispatch.",
);
let input = [1u32, 2, 3, 4]
.into_iter()
.flat_map(u32::to_le_bytes)
.collect::<Vec<_>>();
let outputs = backend
.dispatch(&program, &[input], &DispatchConfig::default())
.expect("Fix: native Metal must allocate threadgroup memory before dispatch.");
assert_eq!(outputs, vec![10u32.to_le_bytes().to_vec()]);
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_dispatch_allocates_internal_trap_sidecar() {
use vyre_driver::DispatchConfig;
use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
let program = Program::wrapped(
vec![
BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
.with_count(1)
.with_output_byte_range(0..4),
],
[1, 1, 1],
vec![
Node::store("out", Expr::u32(0), Expr::u32(42)),
Node::trap(Expr::u32(7), "fault"),
],
);
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before dispatch.",
);
let outputs = backend
.dispatch(&program, &[], &DispatchConfig::default())
.expect("Fix: native Metal must allocate backend-owned trap sidecar storage.");
assert_eq!(outputs, vec![42u32.to_le_bytes().to_vec()]);
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_dispatches_subgroup_size_builtin() {
use vyre_driver::DispatchConfig;
use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
let program = Program::wrapped(
vec![
BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
.with_count(1)
.with_output_byte_range(0..4),
],
[1, 1, 1],
vec![Node::store("out", Expr::u32(0), Expr::subgroup_size())],
);
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before dispatch.",
);
assert!(
backend.supports_subgroup_ops(),
"Fix: native Metal must advertise subgroup ops only while its MSL path executes subgroup builtins."
);
let outputs = backend
.dispatch(&program, &[], &DispatchConfig::default())
.expect("Fix: native Metal must dispatch subgroup-size builtin programs.");
let observed = u32::from_le_bytes(
outputs[0]
.as_slice()
.try_into()
.expect("Fix: subgroup-size smoke output must be one u32."),
);
assert_eq!(
Some(observed),
backend.subgroup_size(),
"Fix: Metal-reported subgroup size must match the executed subgroup builtin."
);
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_resident_transfers_cover_full_range_batch_and_stale_handles() {
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before resident transfers.",
);
let first = backend
.allocate_resident(8)
.expect("Fix: native Metal must allocate resident buffers.");
let second = backend
.allocate_resident(4)
.expect("Fix: native Metal must allocate multiple resident buffers.");
backend
.upload_resident(&first, &[1, 2, 3, 4])
.expect("Fix: native Metal full resident upload must accept bounded payloads.");
assert_eq!(
backend.download_resident(&first).expect(
"Fix: native Metal resident download must return logical allocation bytes."
),
vec![1, 2, 3, 4, 0, 0, 0, 0],
"Fix: full resident upload must zero-pad unwritten allocation bytes."
);
backend
.upload_resident_at(&first, 4, &[5, 6, 7, 8])
.expect("Fix: native Metal resident ranged upload must write subranges.");
assert_eq!(
backend
.download_resident_range(&first, 2, 4)
.expect("Fix: native Metal resident ranged download must read subranges."),
vec![3, 4, 5, 6]
);
backend
.upload_resident_many(&[(&first, &[9, 8, 7, 6, 5, 4, 3, 2]), (&second, &[1, 2])])
.expect(
"Fix: native Metal resident batch upload must validate and stage every handle.",
);
backend
.upload_resident_at_many(&[(&first, 0, &[10, 11, 12, 13]), (&second, 2, &[3, 4])])
.expect("Fix: native Metal resident ranged batch upload must validate and stage every range.");
let mut first_range = Vec::new();
let mut second_range = Vec::new();
backend
.download_resident_ranges_into(
&[(&first, 0, 4), (&second, 0, 4)],
&mut [&mut first_range, &mut second_range],
)
.expect(
"Fix: native Metal resident batch ranged download must fill caller-owned buffers.",
);
assert_eq!(first_range, vec![10, 11, 12, 13]);
assert_eq!(second_range, vec![1, 2, 3, 4]);
backend
.free_resident(second.clone())
.expect("Fix: native Metal resident free must release live handles.");
let stale = backend
.download_resident(&second)
.expect_err("Fix: native Metal must reject stale resident handles after free.");
assert!(
stale.to_string().contains("stale resident handle"),
"Fix: stale resident diagnostics must name the handle lifetime problem: {stale}"
);
backend
.free_resident(first)
.expect("Fix: native Metal resident free must release each live handle exactly once.");
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_resident_transfer_range_errors_are_actionable() {
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before resident transfer negative tests.",
);
let resource = backend.allocate_resident(4).expect(
"Fix: native Metal must allocate resident buffers before negative range checks.",
);
let oversized_upload = backend
.upload_resident(&resource, &[1, 2, 3, 4, 5])
.expect_err(
"Fix: native Metal must reject full resident uploads larger than the allocation.",
);
assert!(
oversized_upload
.to_string()
.contains("requested byte range [0..5) from allocation 4"),
"Fix: oversized resident upload error must name the invalid range and allocation: {oversized_upload}"
);
let ranged_upload = backend
.upload_resident_at(&resource, 3, &[9, 9])
.expect_err(
"Fix: native Metal must reject ranged resident uploads that cross allocation end.",
);
assert!(
ranged_upload
.to_string()
.contains("requested byte range [3..5) from allocation 4"),
"Fix: out-of-bounds resident ranged upload must name the invalid range and allocation: {ranged_upload}"
);
let ranged_download = backend.download_resident_range(&resource, 2, 3).expect_err(
"Fix: native Metal must reject ranged resident downloads that cross allocation end.",
);
assert!(
ranged_download
.to_string()
.contains("requested byte range [2..5) from allocation 4"),
"Fix: out-of-bounds resident ranged download must name the invalid range and allocation: {ranged_download}"
);
let mut only_output = Vec::new();
let count_mismatch = backend
.download_resident_ranges_into(
&[(&resource, 0, 1), (&resource, 1, 1)],
&mut [&mut only_output],
)
.expect_err("Fix: native Metal must reject resident range/output count mismatches.");
assert!(
count_mismatch
.to_string()
.contains("matching range/output counts"),
"Fix: resident batch download count mismatch must be actionable: {count_mismatch}"
);
backend
.free_resident(resource)
.expect("Fix: native Metal must free resident negative-test handles.");
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_resident_ranged_batch_download_fuses_views_and_preflights_outputs() {
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before fused resident batch readback tests.",
);
let resource = backend
.allocate_resident(16)
.expect("Fix: native Metal must allocate resident buffers before fused readback.");
let bytes = (0u8..16u8).collect::<Vec<_>>();
backend
.upload_resident(&resource, &bytes)
.expect("Fix: native Metal must upload resident bytes before fused readback.");
let mut first = vec![0xaa; 2];
let first_capacity = first.capacity();
let mut overlap = vec![0xbb; 1];
let mut empty = vec![0xcc; 3];
let mut tail = Vec::with_capacity(32);
let tail_capacity = tail.capacity();
backend
.download_resident_ranges_into(
&[
(&resource, 0, 6),
(&resource, 4, 6),
(&resource, 10, 0),
(&resource, 10, 4),
],
&mut [&mut first, &mut overlap, &mut empty, &mut tail],
)
.expect("Fix: native Metal must materialize overlapping and empty views from one fused resident batch plan.");
assert_eq!(first, vec![0, 1, 2, 3, 4, 5]);
assert_eq!(overlap, vec![4, 5, 6, 7, 8, 9]);
assert_eq!(
empty,
Vec::<u8>::new(),
"Fix: zero-byte resident batch views must clear stale caller output bytes."
);
assert_eq!(tail, vec![10, 11, 12, 13]);
assert!(
first.capacity() >= first_capacity && tail.capacity() >= tail_capacity,
"Fix: fused Metal resident readback must preserve reusable caller output capacity."
);
let mut valid_output = vec![0xdd, 0xee];
let mut invalid_output = vec![0xff];
let before_valid = valid_output.clone();
let before_invalid = invalid_output.clone();
let error = backend
.download_resident_ranges_into(
&[(&resource, 0, 2), (&resource, 15, 4)],
&mut [&mut valid_output, &mut invalid_output],
)
.expect_err("Fix: native Metal fused resident batch readback must reject invalid ranges before mutating outputs.");
assert!(
error
.to_string()
.contains("requested byte range [15..19) from allocation 16"),
"Fix: fused resident readback range errors must name the invalid range and allocation: {error}"
);
assert_eq!(
valid_output, before_valid,
"Fix: fused resident batch download must not mutate earlier outputs when a later range fails validation."
);
assert_eq!(
invalid_output, before_invalid,
"Fix: fused resident batch download must not mutate the invalid output slot."
);
backend
.free_resident(resource)
.expect("Fix: native Metal must free fused readback resident handles.");
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_resident_dispatch_uses_binding_order_handles_and_persists_output() {
use vyre_driver::DispatchConfig;
use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
let program = Program::wrapped(
vec![
BufferDecl::storage("input", 0, BufferAccess::ReadOnly, DataType::U32)
.with_count(1),
BufferDecl::storage("out", 1, BufferAccess::WriteOnly, DataType::U32)
.with_count(1)
.with_output_byte_range(0..4),
],
[1, 1, 1],
vec![Node::store(
"out",
Expr::u32(0),
Expr::add(Expr::load("input", Expr::u32(0)), Expr::u32(1)),
)],
);
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before resident dispatch.",
);
let input = backend
.allocate_resident(4)
.expect("Fix: native Metal must allocate a resident input handle.");
let output = backend
.allocate_resident(4)
.expect("Fix: native Metal must allocate a resident output handle.");
backend
.upload_resident(&input, &41u32.to_le_bytes())
.expect("Fix: native Metal must upload resident input bytes before dispatch.");
let timed = backend
.dispatch_resident_timed(
&program,
&[input.clone(), output.clone()],
&DispatchConfig::default(),
)
.expect(
"Fix: native Metal resident dispatch must bind resources in Program binding order.",
);
assert_eq!(timed.outputs, vec![42u32.to_le_bytes().to_vec()]);
assert!(
timed.enqueue_ns.is_some() && timed.wait_ns.is_some() && timed.wall_ns > 0,
"Fix: Metal resident timed dispatch must report host timing fields."
);
assert_eq!(
backend
.download_resident(&output)
.expect("Fix: resident output must remain readable after dispatch."),
42u32.to_le_bytes().to_vec()
);
backend
.free_resident(input)
.expect("Fix: native Metal must free resident input handles.");
backend
.free_resident(output)
.expect("Fix: native Metal must free resident output handles.");
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_resident_dispatch_resource_errors_are_actionable() {
use vyre_driver::DispatchConfig;
use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
let program = Program::wrapped(
vec![
BufferDecl::storage("input", 0, BufferAccess::ReadOnly, DataType::U32)
.with_count(1),
BufferDecl::storage("out", 1, BufferAccess::WriteOnly, DataType::U32)
.with_count(1)
.with_output_byte_range(0..4),
],
[1, 1, 1],
vec![Node::store(
"out",
Expr::u32(0),
Expr::add(Expr::load("input", Expr::u32(0)), Expr::u32(1)),
)],
);
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before resident dispatch negative tests.",
);
let input = backend.allocate_resident(4).expect(
"Fix: native Metal must allocate resident input before negative dispatch checks.",
);
backend
.upload_resident(&input, &10u32.to_le_bytes())
.expect(
"Fix: native Metal must upload resident input before negative dispatch checks.",
);
let wrong_count = backend
.dispatch_resident_timed(
&program,
std::slice::from_ref(&input),
&DispatchConfig::default(),
)
.expect_err(
"Fix: native Metal resident dispatch must reject missing output resources.",
);
assert!(
wrong_count
.to_string()
.contains("expected 2 resource(s) in binding order but received 1"),
"Fix: resident dispatch wrong-count error must name expected and received resource counts: {wrong_count}"
);
let stale_output = backend.allocate_resident(4).expect(
"Fix: native Metal must allocate a resident output before stale-handle checks.",
);
backend
.free_resident(stale_output.clone())
.expect("Fix: native Metal must free resident output before stale-handle checks.");
let stale_resources = [input.clone(), stale_output];
let stale_error = backend
.dispatch_resident_timed(&program, &stale_resources, &DispatchConfig::default())
.expect_err("Fix: native Metal resident dispatch must reject stale output handles.");
assert!(
stale_error.to_string().contains("stale handle"),
"Fix: resident dispatch stale-handle error must name the handle lifetime problem: {stale_error}"
);
let undersized_output = backend
.allocate_resident(0)
.expect("Fix: native Metal must allow zero-byte logical resident allocations for boundary testing.");
let undersized_resources = [input.clone(), undersized_output.clone()];
let undersized_error = backend
.dispatch_resident_timed(&program, &undersized_resources, &DispatchConfig::default())
.expect_err(
"Fix: native Metal resident dispatch must reject undersized output handles.",
);
assert!(
undersized_error
.to_string()
.contains("requires 4 byte(s)")
&& undersized_error.to_string().contains("has 0"),
"Fix: resident dispatch undersized-output error must name required and actual byte counts: {undersized_error}"
);
backend
.free_resident(input)
.expect("Fix: native Metal must free resident negative-dispatch input handles.");
backend
.free_resident(undersized_output)
.expect("Fix: native Metal must free resident negative-dispatch output handles.");
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_resident_sequence_dispatches_ordered_steps_and_reads_ranges() {
use vyre_driver::{ResidentDispatchStep, ResidentReadRange};
use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
let double_program = Program::wrapped(
vec![
BufferDecl::storage("input", 0, BufferAccess::ReadOnly, DataType::U32)
.with_count(1),
BufferDecl::storage("mid", 1, BufferAccess::WriteOnly, DataType::U32)
.with_count(1)
.with_output_byte_range(0..4),
],
[1, 1, 1],
vec![Node::store(
"mid",
Expr::u32(0),
Expr::mul(Expr::load("input", Expr::u32(0)), Expr::u32(2)),
)],
);
let add_program = Program::wrapped(
vec![
BufferDecl::storage("mid", 0, BufferAccess::ReadOnly, DataType::U32).with_count(1),
BufferDecl::storage("out", 1, BufferAccess::WriteOnly, DataType::U32)
.with_count(1)
.with_output_byte_range(0..4),
],
[1, 1, 1],
vec![Node::store(
"out",
Expr::u32(0),
Expr::add(Expr::load("mid", Expr::u32(0)), Expr::u32(7)),
)],
);
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before resident sequence dispatch.",
);
let seed = backend
.allocate_resident(4)
.expect("Fix: native Metal must allocate resident sequence input.");
let mid = backend
.allocate_resident(4)
.expect("Fix: native Metal must allocate resident sequence handoff.");
let out = backend
.allocate_resident(4)
.expect("Fix: native Metal must allocate resident sequence output.");
backend
.upload_resident(&seed, &16u32.to_le_bytes())
.expect("Fix: native Metal must upload resident sequence seed bytes.");
let first_resources = [seed.clone(), mid.clone()];
let second_resources = [mid.clone(), out.clone()];
let steps = [
ResidentDispatchStep {
program: &double_program,
resources: &first_resources,
grid_override: None,
},
ResidentDispatchStep {
program: &add_program,
resources: &second_resources,
grid_override: None,
},
];
let read_ranges = [ResidentReadRange {
resource: &out,
byte_offset: 0,
byte_len: 4,
}];
let mut readback = Vec::new();
let timing = backend
.dispatch_resident_sequence_read_ranges_timed_into(
&steps,
&read_ranges,
&mut [&mut readback],
)
.expect("Fix: native Metal must execute ordered resident sequences through the public backend API.");
assert_eq!(
readback,
39u32.to_le_bytes().to_vec(),
"Fix: resident sequence readback must observe step-2 output fed by step-1 resident handoff."
);
assert_eq!(
backend
.download_resident(&mid)
.expect("Fix: resident sequence handoff must remain readable."),
32u32.to_le_bytes().to_vec(),
"Fix: resident sequence must persist intermediate output in the handoff handle."
);
assert!(
timing.wall_ns > 0 && timing.enqueue_ns.is_some() && timing.wait_ns.is_some(),
"Fix: resident sequence timing must preserve Metal host enqueue/wait evidence."
);
assert_eq!(
timing.device_ns, None,
"Fix: Metal resident sequence must not fake device timing until native counters are wired."
);
backend
.free_resident(seed)
.expect("Fix: native Metal must free resident sequence seed handles.");
backend
.free_resident(mid)
.expect("Fix: native Metal must free resident sequence handoff handles.");
backend
.free_resident(out)
.expect("Fix: native Metal must free resident sequence output handles.");
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_repeated_resident_sequence_updates_read_write_handle() {
use vyre_driver::{ResidentDispatchStep, ResidentReadRange};
use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
let increment_program = Program::wrapped(
vec![
BufferDecl::storage("state", 0, BufferAccess::ReadWrite, DataType::U32)
.with_count(1)
.with_output_byte_range(0..4),
],
[1, 1, 1],
vec![Node::store(
"state",
Expr::u32(0),
Expr::add(Expr::load("state", Expr::u32(0)), Expr::u32(1)),
)],
);
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before repeated resident sequence dispatch.",
);
let state = backend
.allocate_resident(4)
.expect("Fix: native Metal must allocate repeated resident sequence state.");
backend
.upload_resident(&state, &5u32.to_le_bytes())
.expect("Fix: native Metal must upload repeated resident sequence state bytes.");
let step_resources = [state.clone()];
let repeated_steps = [ResidentDispatchStep {
program: &increment_program,
resources: &step_resources,
grid_override: None,
}];
let read_ranges = [ResidentReadRange {
resource: &state,
byte_offset: 0,
byte_len: 4,
}];
let mut readback = Vec::new();
backend
.dispatch_resident_repeated_sequence_read_ranges_into(
&[],
&repeated_steps,
3,
&read_ranges,
&mut [&mut readback],
)
.expect("Fix: native Metal must execute repeated resident sequences through the public backend API.");
assert_eq!(
readback,
8u32.to_le_bytes().to_vec(),
"Fix: repeated resident sequence must preserve state across repeated read-write dispatches."
);
assert_eq!(
backend
.download_resident(&state)
.expect("Fix: repeated resident sequence state must remain readable."),
8u32.to_le_bytes().to_vec(),
"Fix: repeated resident sequence must persist the final state in the resident handle."
);
backend
.free_resident(state)
.expect("Fix: native Metal must free repeated resident sequence state handles.");
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_dispatch_reuses_pipeline_cache_for_identical_program_and_policy() {
use vyre_driver::DispatchConfig;
use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
let program = Program::wrapped(
vec![
BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
.with_count(1)
.with_output_byte_range(0..4),
],
[1, 1, 1],
vec![Node::store("out", Expr::u32(0), Expr::u32(99))],
);
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before cache testing.",
);
let before = backend
.pipeline_cache_snapshot()
.expect("Fix: native Metal must expose honest pipeline cache counters.");
let first = backend
.dispatch(&program, &[], &DispatchConfig::default())
.expect("Fix: first Metal dispatch must compile and execute the program.");
let after_first = backend
.pipeline_cache_snapshot()
.expect("Fix: native Metal must expose cache counters after compile.");
let second = backend
.dispatch(&program, &[], &DispatchConfig::default())
.expect("Fix: second Metal dispatch must reuse the compiled pipeline and execute.");
let after_second = backend
.pipeline_cache_snapshot()
.expect("Fix: native Metal must expose cache counters after a cache hit.");
assert_eq!(first, vec![99u32.to_le_bytes().to_vec()]);
assert_eq!(second, first);
assert_eq!(
after_first.misses,
before.misses + 1,
"Fix: first identical Metal dispatch should record exactly one pipeline cache miss."
);
assert_eq!(
after_first.hits, before.hits,
"Fix: first Metal dispatch must not claim a cache hit before the pipeline exists."
);
assert_eq!(
after_second.hits,
after_first.hits + 1,
"Fix: second identical Metal dispatch must reuse the compiled pipeline cache."
);
assert_eq!(
after_second.misses, after_first.misses,
"Fix: cache hit dispatch must not increment Metal pipeline miss counters."
);
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_pipeline_cache_partitions_workgroup_policy_changes() {
use vyre_driver::DispatchConfig;
use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
let program = Program::wrapped(
vec![
BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
.with_count(1)
.with_output_byte_range(0..4),
],
[1, 1, 1],
vec![Node::store("out", Expr::u32(0), Expr::u32(88))],
);
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before cache policy testing.",
);
let before = backend
.pipeline_cache_snapshot()
.expect("Fix: native Metal must expose honest pipeline cache counters.");
let default_config = DispatchConfig::default();
let default_output = backend
.dispatch(&program, &[], &default_config)
.expect("Fix: first Metal dispatch must compile the default policy pipeline.");
let after_default = backend
.pipeline_cache_snapshot()
.expect("Fix: native Metal must expose cache counters after default policy compile.");
let mut workgroup_policy = DispatchConfig::default();
workgroup_policy.workgroup_override = Some([1, 1, 1]);
let policy_output = backend
.dispatch(&program, &[], &workgroup_policy)
.expect("Fix: Metal dispatch must compile a distinct workgroup-policy pipeline.");
let after_policy = backend
.pipeline_cache_snapshot()
.expect("Fix: native Metal must expose cache counters after policy-change compile.");
let policy_hit_output = backend.dispatch(&program, &[], &workgroup_policy).expect(
"Fix: repeated Metal workgroup-policy dispatch must hit the policy cache entry.",
);
let after_policy_hit = backend
.pipeline_cache_snapshot()
.expect("Fix: native Metal must expose cache counters after policy hit.");
assert_eq!(default_output, vec![88u32.to_le_bytes().to_vec()]);
assert_eq!(policy_output, default_output);
assert_eq!(policy_hit_output, default_output);
assert_eq!(
after_default.misses,
before.misses + 1,
"Fix: first default-policy Metal dispatch must record one pipeline cache miss."
);
assert_eq!(
after_policy.misses,
after_default.misses + 1,
"Fix: changing Metal workgroup policy must compile a distinct cache entry."
);
assert_eq!(
after_policy.hits, after_default.hits,
"Fix: first dispatch for a changed workgroup policy must not claim a cache hit."
);
assert_eq!(
after_policy_hit.hits,
after_policy.hits + 1,
"Fix: repeated dispatch for the same changed workgroup policy must hit the Metal pipeline cache."
);
assert_eq!(
after_policy_hit.misses, after_policy.misses,
"Fix: repeated dispatch for the same changed workgroup policy must not add another miss."
);
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_backend_metric_snapshot_exposes_cache_and_resident_counters() {
use std::collections::BTreeMap;
use vyre_driver::DispatchConfig;
use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
let program = Program::wrapped(
vec![
BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
.with_count(1)
.with_output_byte_range(0..4),
],
[1, 1, 1],
vec![Node::store("out", Expr::u32(0), Expr::u32(233))],
);
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before backend metric testing.",
);
let resident = backend.allocate_resident(16).expect(
"Fix: native Metal must allocate a resident buffer before metric snapshot testing.",
);
backend
.upload_resident(&resident, &11u32.to_le_bytes())
.expect("Fix: native Metal must upload resident bytes before metric snapshot testing.");
let mut resident_readback = Vec::new();
backend
.download_resident_range_into(&resident, 0, 4, &mut resident_readback)
.expect(
"Fix: native Metal must download resident bytes before metric snapshot testing.",
);
assert_eq!(resident_readback, 11u32.to_le_bytes().to_vec());
backend
.dispatch(&program, &[], &DispatchConfig::default())
.expect("Fix: first Metal metric-snapshot dispatch must compile and execute.");
backend
.dispatch(&program, &[], &DispatchConfig::default())
.expect("Fix: second Metal metric-snapshot dispatch must hit the pipeline cache.");
let mut changed_policy = DispatchConfig::default();
changed_policy.workgroup_override = Some([2, 1, 1]);
backend
.dispatch(&program, &[], &changed_policy)
.expect("Fix: Metal metric-snapshot policy probe must compile a distinct policy key.");
let changed_program = Program::wrapped(
vec![
BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
.with_count(1)
.with_output_byte_range(0..4),
],
[1, 1, 1],
vec![Node::store("out", Expr::u32(0), Expr::u32(234))],
);
backend
.dispatch(&changed_program, &[], &DispatchConfig::default())
.expect(
"Fix: Metal metric-snapshot program probe must compile a distinct Program key.",
);
let metrics = backend
.backend_metric_snapshot()
.into_iter()
.collect::<BTreeMap<_, _>>();
assert!(
metrics.get("metal_pipeline_cache_hits").copied().unwrap_or(0) >= 1,
"Fix: Metal backend metric snapshot must expose real pipeline cache hits for benchmark JSON."
);
assert!(
metrics
.get("metal_pipeline_cache_misses")
.copied()
.unwrap_or(0)
>= 1,
"Fix: Metal backend metric snapshot must expose real pipeline cache misses for benchmark JSON."
);
assert_eq!(
metrics
.get("metal_pipeline_cache_miss_empty_cache")
.copied(),
Some(1),
"Fix: Metal metric snapshot must explain the first cold miss as an empty-cache miss."
);
assert_eq!(
metrics
.get("metal_pipeline_cache_miss_dispatch_policy_changed")
.copied(),
Some(1),
"Fix: Metal metric snapshot must explain same-program policy changes as dispatch-policy cache misses."
);
assert_eq!(
metrics
.get("metal_pipeline_cache_miss_program_changed")
.copied(),
Some(1),
"Fix: Metal metric snapshot must explain different Program digests as program-change cache misses."
);
assert_eq!(
metrics
.get("metal_pipeline_cache_miss_device_or_runtime_changed")
.copied(),
Some(0),
"Fix: Metal metric snapshot must expose the device/runtime miss bucket even when a single backend instance cannot trigger it."
);
assert_eq!(
metrics.get("metal_pipeline_cache_miss_key_absent").copied(),
Some(0),
"Fix: Metal metric snapshot must expose the fallback key-absent miss bucket for future key fields."
);
assert!(
metrics
.get("metal_buffer_allocation_count")
.copied()
.unwrap_or(0)
>= 1,
"Fix: Metal metric snapshot must expose buffer allocation count."
);
assert!(
metrics
.get("metal_buffer_allocation_bytes")
.copied()
.unwrap_or(0)
>= 16,
"Fix: Metal metric snapshot must expose buffer allocation bytes."
);
assert!(
metrics
.get("metal_host_to_device_copy_count")
.copied()
.unwrap_or(0)
>= 1,
"Fix: Metal metric snapshot must expose host-to-device copy count."
);
assert!(
metrics
.get("metal_host_to_device_bytes")
.copied()
.unwrap_or(0)
>= 4,
"Fix: Metal metric snapshot must expose host-to-device bytes."
);
assert!(
metrics
.get("metal_device_to_host_copy_count")
.copied()
.unwrap_or(0)
>= 1,
"Fix: Metal metric snapshot must expose device-to-host copy count."
);
assert!(
metrics
.get("metal_device_to_host_bytes")
.copied()
.unwrap_or(0)
>= 4,
"Fix: Metal metric snapshot must expose device-to-host bytes."
);
assert!(
metrics
.get("metal_output_readback_bytes")
.copied()
.unwrap_or(0)
>= 4,
"Fix: Metal metric snapshot must expose dispatch output readback bytes separately from resident downloads."
);
assert_eq!(
metrics.get("metal_resident_buffer_count").copied(),
Some(1),
"Fix: Metal backend metric snapshot must expose live resident buffer count."
);
assert_eq!(
metrics.get("metal_resident_bytes").copied(),
Some(16),
"Fix: Metal backend metric snapshot must expose logical resident bytes."
);
backend
.free_resident(resident)
.expect("Fix: native Metal must free metric-snapshot resident handles.");
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_shutdown_invalidates_pipeline_cache_entries() {
use vyre_driver::DispatchConfig;
use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
let program = Program::wrapped(
vec![
BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
.with_count(1)
.with_output_byte_range(0..4),
],
[1, 1, 1],
vec![Node::store("out", Expr::u32(0), Expr::u32(144))],
);
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before lifecycle cache testing.",
);
let before = backend
.pipeline_cache_snapshot()
.expect("Fix: native Metal must expose honest pipeline cache counters.");
let first = backend
.dispatch(&program, &[], &DispatchConfig::default())
.expect("Fix: first Metal dispatch must compile the lifecycle cache probe.");
let after_first = backend
.pipeline_cache_snapshot()
.expect("Fix: native Metal must expose counters after first lifecycle cache dispatch.");
let second = backend
.dispatch(&program, &[], &DispatchConfig::default())
.expect("Fix: second Metal dispatch must hit the lifecycle cache probe.");
let after_hit = backend
.pipeline_cache_snapshot()
.expect("Fix: native Metal must expose counters after lifecycle cache hit.");
backend
.shutdown()
.expect("Fix: native Metal shutdown must invalidate backend-owned caches.");
let after_shutdown = backend
.pipeline_cache_snapshot()
.expect("Fix: native Metal must keep cache counters observable after shutdown.");
let third = backend
.dispatch(&program, &[], &DispatchConfig::default())
.expect("Fix: native Metal dispatch must recover after shutdown cache invalidation.");
let after_recompile = backend
.pipeline_cache_snapshot()
.expect("Fix: native Metal must expose counters after post-shutdown recompile.");
assert_eq!(first, vec![144u32.to_le_bytes().to_vec()]);
assert_eq!(second, first);
assert_eq!(third, first);
assert_eq!(
after_first.misses,
before.misses + 1,
"Fix: first lifecycle cache probe dispatch must record one miss."
);
assert_eq!(
after_hit.hits,
after_first.hits + 1,
"Fix: second lifecycle cache probe dispatch must hit the compiled pipeline cache."
);
assert_eq!(
after_shutdown, after_hit,
"Fix: Metal shutdown must invalidate cache entries without rewriting historical hit/miss counters."
);
assert_eq!(
after_recompile.misses,
after_hit.misses + 1,
"Fix: dispatch after Metal shutdown must recompile instead of reusing stale cached pipeline state."
);
assert_eq!(
after_recompile.hits, after_hit.hits,
"Fix: post-shutdown recompile must not be counted as a cache hit."
);
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_device_profile_reports_live_metal_limits() {
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before profile testing.",
);
let profile = backend.device_profile();
assert_eq!(profile.backend, METAL_BACKEND_ID);
assert!(profile.supports_subgroup_ops);
assert_eq!(profile.subgroup_size, backend.subgroup_size().unwrap_or(0));
assert_eq!(profile.max_workgroup_size, backend.max_workgroup_size());
assert_eq!(
profile.max_invocations_per_workgroup,
backend.max_compute_invocations_per_workgroup()
);
assert!(
profile.max_workgroup_size[0] > 0 && profile.max_invocations_per_workgroup > 0,
"Fix: Metal profile must report nonzero live workgroup limits."
);
assert!(
profile.max_shared_memory_bytes > 0 && profile.has_shared_memory,
"Fix: Metal profile must expose threadgroup memory as a typed shared-memory capability."
);
assert_eq!(
profile.max_storage_buffer_binding_size,
backend.max_storage_buffer_bytes()
);
assert!(
profile.max_storage_buffer_binding_size > 0,
"Fix: Metal profile must expose the native maxBufferLength storage limit."
);
assert!(
!profile.supports_specialization_constants,
"Fix: Metal must not advertise function constants until lowering/runtime actually use them."
);
assert!(
!profile.supports_indirect_dispatch,
"Fix: Metal must not advertise indirect dispatch until the backend executes indirect dispatch nodes."
);
assert_eq!(
profile.timing_quality,
vyre_driver::DeviceTimingQuality::HostEnqueueWait,
"Fix: Metal profile must classify timing as host enqueue/wait until device timestamps are implemented."
);
assert!(
!profile.supports_device_timestamps && !profile.supports_hardware_counters,
"Fix: Metal must not advertise timestamp or counter support until the runtime exposes real measurements."
);
assert!(profile.validation_capabilities().supports_subgroup_ops);
assert_eq!(
profile.adapter_caps().max_shared_memory_bytes,
profile.max_shared_memory_bytes
);
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_shutdown_clears_resident_handles() {
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before shutdown testing.",
);
let resource = backend
.allocate_resident(4)
.expect("Fix: native Metal must allocate a resident handle before shutdown.");
backend
.upload_resident(&resource, &[1, 2, 3, 4])
.expect("Fix: native Metal must upload resident bytes before shutdown.");
backend
.shutdown()
.expect("Fix: native Metal shutdown must clear backend-owned resources.");
let error = backend.download_resident(&resource).expect_err(
"Fix: resident handles must be invalid after Metal shutdown clears resources.",
);
assert!(
error.to_string().contains("stale resident handle"),
"Fix: post-shutdown resident use must fail closed as a stale handle: {error}"
);
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_borrowed_dispatch_into_reuses_caller_output_slots() {
use vyre_driver::DispatchConfig;
use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
let program = Program::wrapped(
vec![
BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
.with_count(1)
.with_output_byte_range(0..4),
],
[1, 1, 1],
vec![Node::store("out", Expr::u32(0), Expr::u32(123))],
);
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before borrowed-into dispatch.",
);
let mut outputs = vec![Vec::with_capacity(64)];
let original_capacity = outputs[0].capacity();
backend
.dispatch_borrowed_into(&program, &[], &DispatchConfig::default(), &mut outputs)
.expect("Fix: native Metal borrowed-into dispatch must execute through the public backend API.");
assert_eq!(
outputs,
vec![123u32.to_le_bytes().to_vec()],
"Fix: borrowed-into Metal dispatch must write real kernel output into caller-owned slots."
);
assert!(
outputs[0].capacity() >= original_capacity,
"Fix: borrowed-into Metal dispatch must preserve reusable caller output capacity."
);
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_compile_native_returns_persistent_pipeline_and_reuses_compiled_state() {
use vyre_driver::DispatchConfig;
use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
let idx = Expr::var("idx");
let program = Program::wrapped(
vec![
BufferDecl::storage("input", 0, BufferAccess::ReadOnly, DataType::U32)
.with_count(4),
BufferDecl::storage("out", 1, BufferAccess::WriteOnly, DataType::U32)
.with_count(4)
.with_output_byte_range(0..16),
],
[4, 1, 1],
vec![
Node::let_bind("idx", Expr::gid_x()),
Node::if_then(
Expr::lt(idx.clone(), Expr::u32(4)),
vec![Node::store(
"out",
idx.clone(),
Expr::add(Expr::load("input", idx), Expr::u32(7)),
)],
),
],
);
let input = [1u32, 2, 3, 4]
.into_iter()
.flat_map(u32::to_le_bytes)
.collect::<Vec<_>>();
let expected = [8u32, 9, 10, 11]
.into_iter()
.flat_map(u32::to_le_bytes)
.collect::<Vec<_>>();
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before compiled pipeline testing.",
);
let config = DispatchConfig::default();
let before = backend
.pipeline_cache_snapshot()
.expect("Fix: native Metal must expose cache counters before compile_native.");
let pipeline = backend
.compile_native(&program, &config)
.expect("Fix: native Metal compile_native must compile a real Metal pipeline.")
.expect("Fix: native Metal compile_native must return Some compiled pipeline.");
assert!(
pipeline.id().starts_with("metal:"),
"Fix: Metal compiled pipeline id must be stable and backend-qualified: {}",
pipeline.id()
);
let after_compile = backend
.pipeline_cache_snapshot()
.expect("Fix: native Metal must expose cache counters after compile_native.");
assert_eq!(
after_compile.misses,
before.misses + 1,
"Fix: Metal compile_native must populate the real pipeline cache exactly once on a cold program."
);
assert_eq!(
after_compile.hits, before.hits,
"Fix: first Metal compile_native for a cold program must not claim a cache hit."
);
let first = pipeline
.dispatch_borrowed(&[input.as_slice()], &config)
.expect("Fix: Metal compiled pipeline must dispatch borrowed inputs through the real command path.");
let second = pipeline
.dispatch_borrowed(&[input.as_slice()], &config)
.expect("Fix: repeated Metal compiled pipeline dispatch must reuse compiled state.");
assert_eq!(first, vec![expected.clone()]);
assert_eq!(second, first);
let after_compiled_dispatch = backend
.pipeline_cache_snapshot()
.expect("Fix: native Metal must expose cache counters after compiled dispatch.");
assert_eq!(
after_compiled_dispatch, after_compile,
"Fix: Metal compiled pipeline dispatch must not re-enter backend lowering/compile cache."
);
let mut outputs = vec![Vec::with_capacity(64)];
let output_capacity = outputs[0].capacity();
pipeline
.dispatch_borrowed_into(&[input.as_slice()], &config, &mut outputs)
.expect("Fix: Metal compiled pipeline must fill caller-owned output slots.");
assert_eq!(outputs, vec![expected]);
assert!(
outputs[0].capacity() >= output_capacity,
"Fix: Metal compiled pipeline dispatch_into must preserve reusable output slot capacity."
);
let direct = backend
.dispatch_borrowed(&program, &[input.as_slice()], &config)
.expect("Fix: direct Metal dispatch must still produce the same bytes as compiled pipeline dispatch.");
assert_eq!(
direct, outputs,
"Fix: Metal compiled pipeline output must stay byte-identical to direct backend dispatch."
);
let after_direct = backend
.pipeline_cache_snapshot()
.expect("Fix: native Metal must expose cache counters after direct dispatch.");
assert_eq!(
after_direct.hits,
after_compile.hits + 1,
"Fix: direct dispatch after compile_native should hit the backend pipeline cache, proving compile_native populated it."
);
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_compile_native_dispatches_persistent_resident_handles() {
use vyre_driver::DispatchConfig;
use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
let program = Program::wrapped(
vec![
BufferDecl::storage("input", 0, BufferAccess::ReadOnly, DataType::U32)
.with_count(1),
BufferDecl::storage("out", 1, BufferAccess::WriteOnly, DataType::U32)
.with_count(1)
.with_output_byte_range(0..4),
],
[1, 1, 1],
vec![Node::store(
"out",
Expr::u32(0),
Expr::add(Expr::load("input", Expr::u32(0)), Expr::u32(9)),
)],
);
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before compiled resident dispatch testing.",
);
let config = DispatchConfig::default();
let input = backend
.allocate_resident(4)
.expect("Fix: native Metal must allocate compiled-pipeline resident input.");
let output = backend
.allocate_resident(4)
.expect("Fix: native Metal must allocate compiled-pipeline resident output.");
backend
.upload_resident(&input, &33u32.to_le_bytes())
.expect("Fix: native Metal must upload compiled-pipeline resident input bytes.");
let before_compile = backend.pipeline_cache_snapshot().expect(
"Fix: native Metal must expose cache counters before compiled resident testing.",
);
let pipeline = backend
.compile_native(&program, &config)
.expect("Fix: native Metal compile_native must compile resident-capable pipelines.")
.expect(
"Fix: native Metal compile_native must return Some for resident-capable pipelines.",
);
let after_compile = backend.pipeline_cache_snapshot().expect(
"Fix: native Metal must expose cache counters after compiled resident compile.",
);
let resources = [input.clone(), output.clone()];
let timed = pipeline
.dispatch_persistent_handles_timed(&resources, &config)
.expect("Fix: Metal compiled pipeline must dispatch persistent resident handles.");
assert_eq!(timed.outputs, vec![42u32.to_le_bytes().to_vec()]);
assert!(
timed.wall_ns > 0 && timed.enqueue_ns.is_some() && timed.wait_ns.is_some(),
"Fix: Metal compiled resident dispatch must preserve host enqueue/wait timing evidence."
);
assert_eq!(
backend
.download_resident(&output)
.expect("Fix: compiled resident output must remain readable."),
42u32.to_le_bytes().to_vec(),
"Fix: Metal compiled resident dispatch must persist output bytes in the resident handle."
);
let after_compiled_dispatch = backend.pipeline_cache_snapshot().expect(
"Fix: native Metal must expose cache counters after compiled resident dispatch.",
);
assert_eq!(
after_compiled_dispatch, after_compile,
"Fix: Metal compiled resident dispatch must not re-enter backend lowering/compile cache."
);
let mut outputs = vec![Vec::with_capacity(32)];
let output_capacity = outputs[0].capacity();
pipeline
.dispatch_persistent_handles_into(&resources, &config, &mut outputs)
.expect(
"Fix: Metal compiled resident dispatch_into must fill caller-owned output slots.",
);
assert_eq!(outputs, vec![42u32.to_le_bytes().to_vec()]);
assert!(
outputs[0].capacity() >= output_capacity,
"Fix: Metal compiled resident dispatch_into must preserve caller output slot capacity."
);
let after_dispatch_into = backend.pipeline_cache_snapshot().expect(
"Fix: native Metal must expose cache counters after compiled resident dispatch_into.",
);
assert_eq!(
after_dispatch_into, after_compiled_dispatch,
"Fix: Metal compiled resident dispatch_into must reuse the compiled pipeline without cache traffic."
);
assert_eq!(
after_compile.misses,
before_compile.misses + 1,
"Fix: compile_native must populate the real Metal cache once before resident compiled dispatch."
);
backend
.free_resident(output.clone())
.expect("Fix: native Metal must free compiled resident output handles.");
let stale_resources = [input.clone(), output];
let stale_error = pipeline
.dispatch_persistent_handles(&stale_resources, &config)
.expect_err(
"Fix: Metal compiled resident dispatch must reject stale resident handles.",
);
assert!(
stale_error.to_string().contains("stale handle"),
"Fix: Metal compiled resident stale-handle diagnostics must name the handle lifetime problem: {stale_error}"
);
backend
.free_resident(input)
.expect("Fix: native Metal must free compiled resident input handles.");
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_compile_native_returns_resident_resource_outputs_for_zero_copy_chaining() {
use vyre_driver::{DispatchConfig, Resource};
use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
let double_program = Program::wrapped(
vec![
BufferDecl::storage("input", 0, BufferAccess::ReadOnly, DataType::U32)
.with_count(1),
BufferDecl::storage("mid", 1, BufferAccess::WriteOnly, DataType::U32)
.with_count(1)
.with_output_byte_range(0..4),
],
[1, 1, 1],
vec![Node::store(
"mid",
Expr::u32(0),
Expr::mul(Expr::load("input", Expr::u32(0)), Expr::u32(2)),
)],
);
let add_program = Program::wrapped(
vec![
BufferDecl::storage("mid", 0, BufferAccess::ReadOnly, DataType::U32).with_count(1),
BufferDecl::storage("out", 1, BufferAccess::WriteOnly, DataType::U32)
.with_count(1)
.with_output_byte_range(0..4),
],
[1, 1, 1],
vec![Node::store(
"out",
Expr::u32(0),
Expr::add(Expr::load("mid", Expr::u32(0)), Expr::u32(5)),
)],
);
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before compiled resource-output testing.",
);
let config = DispatchConfig::default();
let seed = backend
.allocate_resident(4)
.expect("Fix: native Metal must allocate zero-copy chain seed.");
let mid = backend
.allocate_resident(4)
.expect("Fix: native Metal must allocate zero-copy chain middle output.");
let out = backend
.allocate_resident(4)
.expect("Fix: native Metal must allocate zero-copy chain final output.");
backend
.upload_resident(&seed, &17u32.to_le_bytes())
.expect("Fix: native Metal must upload zero-copy chain seed bytes.");
let double = backend
.compile_native(&double_program, &config)
.expect("Fix: native Metal must compile first zero-copy chain pipeline.")
.expect("Fix: native Metal compile_native must return Some for first zero-copy chain pipeline.");
let add = backend
.compile_native(&add_program, &config)
.expect("Fix: native Metal must compile second zero-copy chain pipeline.")
.expect("Fix: native Metal compile_native must return Some for second zero-copy chain pipeline.");
let returned = double
.dispatch_persistent_resource_outputs(&[seed.clone(), mid.clone()], &config)
.expect("Fix: Metal compiled pipeline must return resident resource outputs without host readback.");
assert_eq!(
returned,
vec![mid.clone()],
"Fix: resource-output dispatch must return the caller-provided resident output handle in stable output order."
);
assert_eq!(
backend.download_resident(&mid).expect(
"Fix: zero-copy chain middle handle must remain readable for verification."
),
34u32.to_le_bytes().to_vec(),
"Fix: first zero-copy chain stage must persist bytes in the returned resident handle."
);
let final_resources = [returned[0].clone(), out.clone()];
let final_outputs = add
.dispatch_persistent_handles(&final_resources, &config)
.expect(
"Fix: second Metal compiled pipeline must consume the returned resident output handle.",
);
assert_eq!(final_outputs, vec![39u32.to_le_bytes().to_vec()]);
assert_eq!(
backend
.download_resident(&out)
.expect("Fix: zero-copy chain final handle must remain readable."),
39u32.to_le_bytes().to_vec(),
"Fix: second zero-copy chain stage must persist final output bytes."
);
let borrowed_error = double
.dispatch_persistent_resource_outputs(
&[seed.clone(), Resource::Borrowed(vec![0u8; 4])],
&config,
)
.expect_err("Fix: resource-output dispatch must reject borrowed output resources.");
assert!(
borrowed_error
.to_string()
.contains("cannot return borrowed output binding"),
"Fix: borrowed output rejection must explain how to keep the chain zero-copy: {borrowed_error}"
);
backend
.free_resident(seed)
.expect("Fix: native Metal must free zero-copy chain seed.");
backend
.free_resident(mid)
.expect("Fix: native Metal must free zero-copy chain middle output.");
backend
.free_resident(out)
.expect("Fix: native Metal must free zero-copy chain final output.");
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn apple_borrowed_timed_dispatch_reports_enqueue_and_wait() {
use vyre_driver::DispatchConfig;
use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
let program = Program::wrapped(
vec![
BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
.with_count(1)
.with_output_byte_range(0..4),
],
[1, 1, 1],
vec![Node::store("out", Expr::u32(0), Expr::u32(77))],
);
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice before timed dispatch.",
);
let timed = backend
.dispatch_borrowed_timed(&program, &[], &DispatchConfig::default())
.expect("Fix: native Metal borrowed timed dispatch must execute through the real command path.");
assert_eq!(timed.outputs, vec![77u32.to_le_bytes().to_vec()]);
assert!(
timed.wall_ns > 0,
"Fix: Metal borrowed timed dispatch must report nonzero wall time."
);
assert!(
timed.enqueue_ns.is_some() && timed.wait_ns.is_some(),
"Fix: Metal borrowed timed dispatch must expose native enqueue and wait timing."
);
assert_eq!(
timed.device_ns, None,
"Fix: Metal must not fake device timing until counter/timestamp support is implemented."
);
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn compile_native_returns_a_working_compiled_pipeline() {
use vyre_driver::{DispatchConfig, VyreBackend};
use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
let program = Program::wrapped(
vec![
BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
.with_count(1)
.with_output_byte_range(0..4),
],
[1, 1, 1],
vec![Node::store("out", Expr::u32(0), Expr::u32(0xDEAD_BEEFu32))],
);
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice.",
);
let compiled = backend
.compile_native(&program, &DispatchConfig::default())
.expect("Fix: Metal compile_native must succeed for a valid program.");
let pipeline = compiled.expect(
"Fix: Metal compile_native must return Some(CompiledPipeline), not Ok(None)."
);
let outputs = pipeline
.dispatch(&[], &DispatchConfig::default())
.expect("Fix: Metal compiled pipeline dispatch must succeed.");
let result = u32::from_le_bytes(
outputs[0]
.as_slice()
.try_into()
.expect("Fix: compiled dispatch output must be one u32."),
);
assert_eq!(
result, 0xDEAD_BEEFu32,
"Fix: Metal compiled pipeline dispatch must produce the exact constant \
written by the kernel; got 0x{result:08X}"
);
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[test]
fn metric_snapshot_poisoned_mutex_is_loud() {
use std::sync::Arc;
use vyre_driver::VyreBackend;
let backend = acquire().expect(
"Fix: Apple Metal builds must acquire the system default MTLDevice.",
);
{
}
let snapshot = backend.backend_metric_snapshot();
let has_count = snapshot
.iter()
.any(|(k, _)| *k == "metal_resident_buffer_count");
let has_bytes = snapshot
.iter()
.any(|(k, _)| *k == "metal_resident_bytes");
assert!(
has_count,
"Fix: healthy backend snapshot must contain `metal_resident_buffer_count`; \
got: {snapshot:?}"
);
assert!(
has_bytes,
"Fix: healthy backend snapshot must contain `metal_resident_bytes`; \
got: {snapshot:?}"
);
let has_error = snapshot
.iter()
.any(|(k, _)| *k == "metal_resident_buffer_error");
assert!(
!has_error,
"Fix: healthy backend snapshot must not contain `metal_resident_buffer_error`; \
got: {snapshot:?}"
);
let count_entry = snapshot
.iter()
.find(|(k, _)| *k == "metal_resident_buffer_count")
.map(|(_, v)| *v)
.expect("Fix: metal_resident_buffer_count must be present in the snapshot");
assert_eq!(
count_entry, 0,
"Fix: metal_resident_buffer_count must be 0 before any resident allocations; \
if this fails with u64::MAX the mutex is unexpectedly poisoned"
);
}
}