1#![allow(unsafe_code)]
2use vyre_driver::backend::{BackendError, VyreBackend};
11
12pub const METAL_BACKEND_ID: &str = "metal";
14
15#[cfg(any(target_os = "macos", target_os = "ios"))]
16mod runtime;
17
18#[cfg(any(target_os = "macos", target_os = "ios"))]
19pub use runtime::MetalBackend;
20#[cfg(any(target_os = "macos", target_os = "ios"))]
21pub use runtime::{
22 metal_resident_scan_resource_table, MetalResidentScanResourceEntry,
23 MetalResidentScanResourceError, MetalResidentScanResourceLifetime,
24 MetalResidentScanResourceTableEvidence, METAL_RESIDENT_SCAN_RESOURCE_TABLE_SCHEMA_VERSION,
25};
26
27#[cfg(any(target_os = "macos", target_os = "ios"))]
34pub fn acquire() -> Result<Box<dyn VyreBackend>, BackendError> {
35 MetalBackend::acquire().map(|backend| Box::new(backend) as Box<dyn VyreBackend>)
36}
37
38#[cfg(not(any(target_os = "macos", target_os = "ios")))]
45pub fn acquire() -> Result<Box<dyn VyreBackend>, BackendError> {
46 Err(BackendError::UnsupportedFeature {
47 name: "Apple Metal.framework native runtime".to_string(),
48 backend: METAL_BACKEND_ID.to_string(),
49 })
50}
51
52#[cfg(any(target_os = "macos", target_os = "ios"))]
53inventory::submit! {
54 vyre_driver::backend::BackendRegistration {
55 id: METAL_BACKEND_ID,
56 factory: acquire,
57 supported_ops: vyre_driver::backend::core_supported_ops,
58 }
59}
60
61#[cfg(any(target_os = "macos", target_os = "ios"))]
62inventory::submit! {
63 vyre_driver::backend::BackendCapability {
64 id: METAL_BACKEND_ID,
65 dispatches: true,
66 }
67}
68
69#[cfg(any(target_os = "macos", target_os = "ios"))]
70inventory::submit! {
71 vyre_driver::backend::BackendPrecedence {
72 id: METAL_BACKEND_ID,
73 rank: 25,
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[cfg(not(any(target_os = "macos", target_os = "ios")))]
82 #[test]
83 fn non_apple_acquire_fails_actionably() {
84 let error = match acquire() {
85 Ok(_) => panic!("non-Apple builds must not fabricate a Metal backend"),
86 Err(error) => error,
87 };
88 let message = error.to_string();
89 assert!(
90 message.contains("Apple Metal.framework") && message.contains("Fix:"),
91 "non-Apple Metal acquisition must be actionable: {message}"
92 );
93 }
94
95 #[cfg(not(any(target_os = "macos", target_os = "ios")))]
96 #[test]
97 fn non_apple_build_does_not_register_fake_backend() {
98 let registered = vyre_driver::backend::registered_backends();
99 assert!(
100 registered
101 .iter()
102 .all(|backend| backend.id != METAL_BACKEND_ID),
103 "non-Apple builds must not submit a fake `metal` backend registration"
104 );
105 }
106
107 #[test]
108 fn resident_batch_download_uses_backend_neutral_fusion_contract() {
109 let source = include_str!("runtime.rs");
110 let method = source
111 .split("fn download_resident_ranges_into(")
112 .nth(1)
113 .and_then(|tail| tail.split("fn free_resident(").next())
114 .expect("Fix: Metal runtime must expose download_resident_ranges_into before free_resident.");
115 assert!(
116 method.contains("fuse_resident_transfer_intervals(&copies)")
117 && method.contains("reserve_fused_resident_view_outputs")
118 && method.contains("copy_fused_resident_view_into")
119 && !method.contains(
120 "self.download_resident_range_into(resource, *byte_offset, *byte_len, output)"
121 ),
122 "Fix: Metal resident ranged batch download must reuse backend-neutral interval fusion instead of looping one readback per requested range."
123 );
124 }
125
126 #[test]
127 fn compile_native_uses_real_metal_compiled_pipeline_contract() {
128 let source = include_str!("runtime.rs");
129 assert!(
130 source.contains("fn compile_native(")
131 && source.contains("fn compile_native_shared(")
132 && source.contains("struct MetalPersistentPipeline")
133 && source.contains("impl CompiledPipeline for MetalPersistentPipeline")
134 && source.contains("dispatch_planned_buffers_with_queue(")
135 && source.contains("Ok(Some(Arc::new(MetalPersistentPipeline"),
136 "Fix: Metal compile_native must return a real CompiledPipeline object over the shared command path, not inherit the Ok(None) passthrough."
137 );
138 }
139
140 #[test]
141 fn compiled_pipeline_resident_handles_share_backend_table_contract() {
142 let source = include_str!("runtime.rs");
143 assert!(
144 source.contains("resident_buffers: MetalResidentBufferTable")
145 && source.contains("resident_buffers: Arc::new(Mutex::new(BTreeMap::new()))")
146 && source.contains("resident_buffers: Arc::clone(&self.resident_buffers)")
147 && source.contains("fn dispatch_persistent_handles_timed(")
148 && source.contains("resolve_resident_resources_from_table")
149 && source.contains("plan_resident_buffers("),
150 "Fix: Metal compiled persistent-handle dispatch must share the backend resident table and reuse resident buffer planning instead of reporting UnsupportedFeature."
151 );
152 }
153
154 #[test]
155 fn compiled_pipeline_resource_outputs_avoid_host_readback_contract() {
156 let source = include_str!("runtime.rs");
157 let method = source
158 .split("fn dispatch_persistent_resource_outputs(")
159 .nth(1)
160 .and_then(|tail| tail.split("fn lock_resident_buffer_table").next())
161 .expect("Fix: Metal compiled pipeline must implement dispatch_persistent_resource_outputs before resident table helpers.");
162 assert!(
163 method.contains("resident_output_resources(&base_plan, inputs)?")
164 && method.contains("submit_planned_buffers_with_queue(")
165 && !method.contains("dispatch_persistent_handles_timed")
166 && !method.contains("collect_outputs("),
167 "Fix: Metal compiled resource-output dispatch must submit the compiled resident command and return resident handles without host readback."
168 );
169 }
170
171 #[test]
172 fn pipeline_cache_miss_reasons_use_shared_classifier_contract() {
173 let source = include_str!("runtime.rs");
174 assert!(
175 source.contains("PipelineCacheIdentity")
176 && source.contains("PipelineCacheMissReason")
177 && source.contains("PipelineCacheIdentity::try_from_program(program, config, fingerprint)")
178 && source.contains("PipelineCacheMissReason::classify_identities(")
179 && source.contains("metal_pipeline_cache_miss_empty_cache")
180 && source.contains("metal_pipeline_cache_miss_program_changed")
181 && source.contains("metal_pipeline_cache_miss_dispatch_policy_changed")
182 && source.contains("metal_pipeline_cache_miss_device_or_runtime_changed")
183 && source.contains("metal_pipeline_cache_miss_key_absent")
184 && source.contains("metal_buffer_allocation_count")
185 && source.contains("metal_buffer_allocation_bytes")
186 && source.contains("metal_host_to_device_copy_count")
187 && source.contains("metal_host_to_device_bytes")
188 && source.contains("metal_device_to_host_copy_count")
189 && source.contains("metal_device_to_host_bytes")
190 && source.contains("metal_output_readback_bytes")
191 && !source.contains("struct MetalPipelineCacheIdentity")
192 && !source.contains("classify_metal_pipeline_cache_miss"),
193 "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."
194 );
195 }
196
197 #[test]
198 fn compiled_pipeline_metrics_share_backend_snapshot_contract() {
199 let source = include_str!("runtime.rs");
200 assert!(
201 source.contains("type MetalMetricCounters = Arc<MetalMetrics>")
202 && source.contains("metrics: MetalMetricCounters")
203 && source.contains("metrics: Arc::clone(&self.metrics)")
204 && source.contains("Vec::with_capacity(16)")
205 && source
206 .matches("record_planned_buffer_metrics(&self.metrics, &buffers)")
207 .count()
208 >= 3
209 && source
210 .matches("record_output_readback_metrics(&self.metrics, &result.outputs)")
211 .count()
212 >= 2,
213 "Fix: compiled Metal dispatch paths must account allocation, host-copy, and readback counters into the same backend metric snapshot as direct dispatch."
214 );
215 }
216
217 #[test]
218 fn resident_scan_resource_table_has_argument_buffer_lifetime_contract() {
219 let source = include_str!("runtime.rs");
220 assert!(
221 source.contains("pub struct MetalResidentScanResourceEntry")
222 && source.contains("pub enum MetalResidentScanResourceLifetime")
223 && source.contains("pub fn metal_resident_scan_resource_table(")
224 && source.contains("argument_buffer_entry")
225 && source.contains("shared_scan_metadata_digest")
226 && source.contains("metal_resource_metadata_digest")
227 && source.contains("DuplicateArgumentBufferEntry")
228 && source.contains("LifetimeMismatch"),
229 "Fix: Metal resident scan tables must expose argument-buffer entry, metadata parity, and lifetime validation before resident scan dispatch."
230 );
231 }
232
233 #[cfg(any(target_os = "macos", target_os = "ios"))]
234 #[test]
235 fn apple_acquire_registers_dispatch_backend() {
236 let backend = acquire().expect(
237 "Fix: Apple Metal builds must acquire the system default MTLDevice for native dispatch.",
238 );
239 assert_eq!(backend.id(), METAL_BACKEND_ID);
240 assert!(
241 vyre_driver::backend::registered_backends()
242 .iter()
243 .any(|registration| registration.id == METAL_BACKEND_ID),
244 "Fix: Apple Metal builds must submit a real backend registration."
245 );
246 assert!(
247 vyre_driver::backend::backend_dispatches(METAL_BACKEND_ID),
248 "Fix: Apple Metal registration must declare live dispatch capability."
249 );
250 assert_eq!(
251 vyre_driver::backend::backend_precedence(METAL_BACKEND_ID),
252 25,
253 "Fix: Metal precedence must stay above portable fallbacks only after live native dispatch exists."
254 );
255 }
256
257 #[cfg(any(target_os = "macos", target_os = "ios"))]
258 #[test]
259 fn apple_dispatches_store_literal_program() {
260 use vyre_driver::DispatchConfig;
261 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
262
263 let program = Program::wrapped(
264 vec![
265 BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
266 .with_count(1)
267 .with_output_byte_range(0..4),
268 ],
269 [1, 1, 1],
270 vec![Node::store("out", Expr::u32(0), Expr::u32(42))],
271 );
272
273 let backend = acquire().expect(
274 "Fix: Apple Metal builds must acquire the system default MTLDevice before dispatch.",
275 );
276 let outputs = backend
277 .dispatch(&program, &[], &DispatchConfig::default())
278 .expect("Fix: native Metal must execute a one-store u32 Program end to end.");
279 assert_eq!(outputs, vec![42u32.to_le_bytes().to_vec()]);
280 }
281
282 #[cfg(any(target_os = "macos", target_os = "ios"))]
283 #[test]
284 fn apple_native_metal_matches_wgpu_on_same_program_bytes() {
285 use vyre_driver::{DispatchConfig, VyreBackend as _};
286 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
287
288 let idx = Expr::var("idx");
289 let program = Program::wrapped(
290 vec![
291 BufferDecl::storage("a", 0, BufferAccess::ReadOnly, DataType::U32).with_count(8),
292 BufferDecl::storage("b", 1, BufferAccess::ReadOnly, DataType::U32).with_count(8),
293 BufferDecl::storage("out", 2, BufferAccess::WriteOnly, DataType::U32)
294 .with_count(8)
295 .with_output_byte_range(0..32),
296 ],
297 [8, 1, 1],
298 vec![
299 Node::let_bind("idx", Expr::gid_x()),
300 Node::if_then(
301 Expr::lt(idx.clone(), Expr::u32(8)),
302 vec![Node::store(
303 "out",
304 idx.clone(),
305 Expr::add(
306 Expr::load("a", idx.clone()),
307 Expr::mul(Expr::load("b", idx), Expr::u32(3)),
308 ),
309 )],
310 ),
311 ],
312 );
313 let a = [1u32, 2, 3, 4, 5, 6, 7, 8]
314 .into_iter()
315 .flat_map(u32::to_le_bytes)
316 .collect::<Vec<_>>();
317 let b = [10u32, 11, 12, 13, 14, 15, 16, 17]
318 .into_iter()
319 .flat_map(u32::to_le_bytes)
320 .collect::<Vec<_>>();
321 let expected = [31u32, 35, 39, 43, 47, 51, 55, 59]
322 .into_iter()
323 .flat_map(u32::to_le_bytes)
324 .collect::<Vec<_>>();
325
326 let metal = acquire().expect(
327 "Fix: Apple Metal builds must acquire the system default MTLDevice before differential dispatch.",
328 );
329 let wgpu = vyre_driver_wgpu::WgpuBackend::acquire()
330 .expect("Fix: WGPU-on-Metal must acquire on the Apple GPU differential lane.");
331 let config = DispatchConfig::default();
332 let metal_outputs = metal
333 .dispatch(&program, &[a.clone(), b.clone()], &config)
334 .expect("Fix: native Metal must dispatch the differential Program.");
335 let wgpu_outputs = wgpu
336 .dispatch(&program, &[a, b], &config)
337 .expect("Fix: WGPU-on-Metal must dispatch the same differential Program.");
338
339 assert_eq!(
340 metal_outputs,
341 vec![expected.clone()],
342 "Fix: native Metal output must match the explicit byte oracle before comparing backends."
343 );
344 assert_eq!(
345 wgpu_outputs,
346 vec![expected],
347 "Fix: WGPU-on-Metal output must match the explicit byte oracle before comparing backends."
348 );
349 assert_eq!(
350 metal_outputs, wgpu_outputs,
351 "Fix: native Metal and WGPU-on-Metal must produce byte-identical outputs for the same Program."
352 );
353 }
354
355 #[cfg(any(target_os = "macos", target_os = "ios"))]
356 #[test]
357 fn apple_dispatch_handles_empty_and_unaligned_output_ranges() {
358 use vyre_driver::DispatchConfig;
359 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
360
361 let program = Program::wrapped(
362 vec![
363 BufferDecl::storage("empty", 0, BufferAccess::WriteOnly, DataType::U32)
364 .with_count(0)
365 .with_output_byte_range(0..0),
366 BufferDecl::storage("word", 1, BufferAccess::WriteOnly, DataType::U32)
367 .with_count(1)
368 .with_output_byte_range(1..2),
369 ],
370 [1, 1, 1],
371 vec![Node::store("word", Expr::u32(0), Expr::u32(0x1122_3344))],
372 );
373
374 let backend = acquire().expect(
375 "Fix: Apple Metal builds must acquire the system default MTLDevice before boundary dispatch.",
376 );
377 let outputs = backend
378 .dispatch(&program, &[], &DispatchConfig::default())
379 .expect(
380 "Fix: native Metal must honor shared empty and unaligned output layout planning.",
381 );
382 assert_eq!(
383 outputs,
384 vec![Vec::new(), vec![0x33]],
385 "Fix: Metal output collection must preserve empty outputs and trim unaligned one-byte ranges from the stored word."
386 );
387 }
388
389 #[cfg(any(target_os = "macos", target_os = "ios"))]
390 #[test]
391 fn apple_dispatch_config_errors_are_actionable() {
392 use vyre_driver::DispatchConfig;
393 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
394
395 let program = Program::wrapped(
396 vec![
397 BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
398 .with_count(1)
399 .with_output_byte_range(0..4),
400 ],
401 [1, 1, 1],
402 vec![Node::store("out", Expr::u32(0), Expr::u32(42))],
403 );
404
405 let backend = acquire().expect(
406 "Fix: Apple Metal builds must acquire the system default MTLDevice before negative dispatch tests.",
407 );
408 let mut cooperative = DispatchConfig::default();
409 cooperative.cooperative = true;
410 let cooperative_error = backend
411 .dispatch(&program, &[], &cooperative)
412 .expect_err("Fix: native Metal must reject cooperative dispatch until implemented.")
413 .to_string();
414 assert!(
415 cooperative_error.contains("Metal cooperative grid dispatch")
416 && cooperative_error.contains("metal"),
417 "Fix: cooperative dispatch rejection must name the unsupported Metal feature and backend: {cooperative_error}"
418 );
419
420 let mut zero_iterations = DispatchConfig::default();
421 zero_iterations.fixpoint_iterations = Some(0);
422 let zero_error = backend
423 .dispatch(&program, &[], &zero_iterations)
424 .expect_err("Fix: native Metal must reject explicit zero fixpoint iterations.")
425 .to_string();
426 assert!(
427 zero_error.contains("fixpoint_iterations=0") && zero_error.contains("Fix:"),
428 "Fix: zero-iteration dispatch rejection must include actionable fix text: {zero_error}"
429 );
430 }
431
432 #[cfg(any(target_os = "macos", target_os = "ios"))]
433 #[test]
434 fn apple_dispatch_grid_uses_declared_output_count_not_trimmed_readback() {
435 use vyre_driver::DispatchConfig;
436 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
437
438 let local = Expr::var("local");
439 let token = Expr::var("token");
440 let program = Program::wrapped(
441 vec![
442 BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
443 .with_count(512)
444 .with_output_byte_range(0..8),
445 ],
446 [256, 1, 1],
447 vec![
448 Node::let_bind("local", Expr::LocalId { axis: 0 }),
449 Node::let_bind("token", Expr::WorkgroupId { axis: 0 }),
450 Node::if_then(
451 Expr::and(
452 Expr::eq(local, Expr::u32(0)),
453 Expr::lt(token.clone(), Expr::u32(2)),
454 ),
455 vec![Node::store(
456 "out",
457 token.clone(),
458 Expr::add(token, Expr::u32(11)),
459 )],
460 ),
461 ],
462 );
463
464 let backend = acquire().expect(
465 "Fix: Apple Metal builds must acquire the system default MTLDevice before dispatch.",
466 );
467 let outputs = backend
468 .dispatch(&program, &[], &DispatchConfig::default())
469 .expect("Fix: native Metal must infer grid from declared dispatch domain.");
470 assert_eq!(
471 outputs,
472 vec![[11u32.to_le_bytes(), 12u32.to_le_bytes()].concat()]
473 );
474 }
475
476 #[cfg(any(target_os = "macos", target_os = "ios"))]
477 #[test]
478 fn apple_dispatch_allocates_threadgroup_memory() {
479 use vyre_driver::DispatchConfig;
480 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
481
482 let local = Expr::var("local");
483 let program = Program::wrapped(
484 vec![
485 BufferDecl::storage("values", 0, BufferAccess::ReadOnly, DataType::U32)
486 .with_count(4),
487 BufferDecl::workgroup("scratch", 4, DataType::U32),
488 BufferDecl::storage("out", 1, BufferAccess::WriteOnly, DataType::U32)
489 .with_count(1)
490 .with_output_byte_range(0..4),
491 ],
492 [4, 1, 1],
493 vec![
494 Node::let_bind("local", Expr::LocalId { axis: 0 }),
495 Node::if_then(
496 Expr::lt(local.clone(), Expr::u32(4)),
497 vec![Node::store(
498 "scratch",
499 local.clone(),
500 Expr::load("values", local.clone()),
501 )],
502 ),
503 Node::barrier(),
504 Node::if_then(
505 Expr::eq(local, Expr::u32(0)),
506 vec![Node::store(
507 "out",
508 Expr::u32(0),
509 Expr::add(
510 Expr::add(
511 Expr::load("scratch", Expr::u32(0)),
512 Expr::load("scratch", Expr::u32(1)),
513 ),
514 Expr::add(
515 Expr::load("scratch", Expr::u32(2)),
516 Expr::load("scratch", Expr::u32(3)),
517 ),
518 ),
519 )],
520 ),
521 ],
522 );
523
524 let backend = acquire().expect(
525 "Fix: Apple Metal builds must acquire the system default MTLDevice before dispatch.",
526 );
527 let input = [1u32, 2, 3, 4]
528 .into_iter()
529 .flat_map(u32::to_le_bytes)
530 .collect::<Vec<_>>();
531 let outputs = backend
532 .dispatch(&program, &[input], &DispatchConfig::default())
533 .expect("Fix: native Metal must allocate threadgroup memory before dispatch.");
534 assert_eq!(outputs, vec![10u32.to_le_bytes().to_vec()]);
535 }
536
537 #[cfg(any(target_os = "macos", target_os = "ios"))]
538 #[test]
539 fn apple_dispatch_allocates_internal_trap_sidecar() {
540 use vyre_driver::DispatchConfig;
541 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
542
543 let program = Program::wrapped(
544 vec![
545 BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
546 .with_count(1)
547 .with_output_byte_range(0..4),
548 ],
549 [1, 1, 1],
550 vec![
551 Node::store("out", Expr::u32(0), Expr::u32(42)),
552 Node::trap(Expr::u32(7), "fault"),
553 ],
554 );
555
556 let backend = acquire().expect(
557 "Fix: Apple Metal builds must acquire the system default MTLDevice before dispatch.",
558 );
559 let outputs = backend
560 .dispatch(&program, &[], &DispatchConfig::default())
561 .expect("Fix: native Metal must allocate backend-owned trap sidecar storage.");
562 assert_eq!(outputs, vec![42u32.to_le_bytes().to_vec()]);
563 }
564
565 #[cfg(any(target_os = "macos", target_os = "ios"))]
566 #[test]
567 fn apple_dispatches_subgroup_size_builtin() {
568 use vyre_driver::DispatchConfig;
569 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
570
571 let program = Program::wrapped(
572 vec![
573 BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
574 .with_count(1)
575 .with_output_byte_range(0..4),
576 ],
577 [1, 1, 1],
578 vec![Node::store("out", Expr::u32(0), Expr::subgroup_size())],
579 );
580
581 let backend = acquire().expect(
582 "Fix: Apple Metal builds must acquire the system default MTLDevice before dispatch.",
583 );
584 assert!(
585 backend.supports_subgroup_ops(),
586 "Fix: native Metal must advertise subgroup ops only while its MSL path executes subgroup builtins."
587 );
588 let outputs = backend
589 .dispatch(&program, &[], &DispatchConfig::default())
590 .expect("Fix: native Metal must dispatch subgroup-size builtin programs.");
591 let observed = u32::from_le_bytes(
592 outputs[0]
593 .as_slice()
594 .try_into()
595 .expect("Fix: subgroup-size smoke output must be one u32."),
596 );
597 assert_eq!(
598 Some(observed),
599 backend.subgroup_size(),
600 "Fix: Metal-reported subgroup size must match the executed subgroup builtin."
601 );
602 }
603
604 #[cfg(any(target_os = "macos", target_os = "ios"))]
605 #[test]
606 fn apple_resident_transfers_cover_full_range_batch_and_stale_handles() {
607 let backend = acquire().expect(
608 "Fix: Apple Metal builds must acquire the system default MTLDevice before resident transfers.",
609 );
610 let first = backend
611 .allocate_resident(8)
612 .expect("Fix: native Metal must allocate resident buffers.");
613 let second = backend
614 .allocate_resident(4)
615 .expect("Fix: native Metal must allocate multiple resident buffers.");
616
617 backend
618 .upload_resident(&first, &[1, 2, 3, 4])
619 .expect("Fix: native Metal full resident upload must accept bounded payloads.");
620 assert_eq!(
621 backend.download_resident(&first).expect(
622 "Fix: native Metal resident download must return logical allocation bytes."
623 ),
624 vec![1, 2, 3, 4, 0, 0, 0, 0],
625 "Fix: full resident upload must zero-pad unwritten allocation bytes."
626 );
627
628 backend
629 .upload_resident_at(&first, 4, &[5, 6, 7, 8])
630 .expect("Fix: native Metal resident ranged upload must write subranges.");
631 assert_eq!(
632 backend
633 .download_resident_range(&first, 2, 4)
634 .expect("Fix: native Metal resident ranged download must read subranges."),
635 vec![3, 4, 5, 6]
636 );
637
638 backend
639 .upload_resident_many(&[(&first, &[9, 8, 7, 6, 5, 4, 3, 2]), (&second, &[1, 2])])
640 .expect(
641 "Fix: native Metal resident batch upload must validate and stage every handle.",
642 );
643 backend
644 .upload_resident_at_many(&[(&first, 0, &[10, 11, 12, 13]), (&second, 2, &[3, 4])])
645 .expect("Fix: native Metal resident ranged batch upload must validate and stage every range.");
646 let mut first_range = Vec::new();
647 let mut second_range = Vec::new();
648 backend
649 .download_resident_ranges_into(
650 &[(&first, 0, 4), (&second, 0, 4)],
651 &mut [&mut first_range, &mut second_range],
652 )
653 .expect(
654 "Fix: native Metal resident batch ranged download must fill caller-owned buffers.",
655 );
656 assert_eq!(first_range, vec![10, 11, 12, 13]);
657 assert_eq!(second_range, vec![1, 2, 3, 4]);
658
659 backend
660 .free_resident(second.clone())
661 .expect("Fix: native Metal resident free must release live handles.");
662 let stale = backend
663 .download_resident(&second)
664 .expect_err("Fix: native Metal must reject stale resident handles after free.");
665 assert!(
666 stale.to_string().contains("stale resident handle"),
667 "Fix: stale resident diagnostics must name the handle lifetime problem: {stale}"
668 );
669 backend
670 .free_resident(first)
671 .expect("Fix: native Metal resident free must release each live handle exactly once.");
672 }
673
674 #[cfg(any(target_os = "macos", target_os = "ios"))]
675 #[test]
676 fn apple_resident_transfer_range_errors_are_actionable() {
677 let backend = acquire().expect(
678 "Fix: Apple Metal builds must acquire the system default MTLDevice before resident transfer negative tests.",
679 );
680 let resource = backend.allocate_resident(4).expect(
681 "Fix: native Metal must allocate resident buffers before negative range checks.",
682 );
683
684 let oversized_upload = backend
685 .upload_resident(&resource, &[1, 2, 3, 4, 5])
686 .expect_err(
687 "Fix: native Metal must reject full resident uploads larger than the allocation.",
688 );
689 assert!(
690 oversized_upload
691 .to_string()
692 .contains("requested byte range [0..5) from allocation 4"),
693 "Fix: oversized resident upload error must name the invalid range and allocation: {oversized_upload}"
694 );
695
696 let ranged_upload = backend
697 .upload_resident_at(&resource, 3, &[9, 9])
698 .expect_err(
699 "Fix: native Metal must reject ranged resident uploads that cross allocation end.",
700 );
701 assert!(
702 ranged_upload
703 .to_string()
704 .contains("requested byte range [3..5) from allocation 4"),
705 "Fix: out-of-bounds resident ranged upload must name the invalid range and allocation: {ranged_upload}"
706 );
707
708 let ranged_download = backend.download_resident_range(&resource, 2, 3).expect_err(
709 "Fix: native Metal must reject ranged resident downloads that cross allocation end.",
710 );
711 assert!(
712 ranged_download
713 .to_string()
714 .contains("requested byte range [2..5) from allocation 4"),
715 "Fix: out-of-bounds resident ranged download must name the invalid range and allocation: {ranged_download}"
716 );
717
718 let mut only_output = Vec::new();
719 let count_mismatch = backend
720 .download_resident_ranges_into(
721 &[(&resource, 0, 1), (&resource, 1, 1)],
722 &mut [&mut only_output],
723 )
724 .expect_err("Fix: native Metal must reject resident range/output count mismatches.");
725 assert!(
726 count_mismatch
727 .to_string()
728 .contains("matching range/output counts"),
729 "Fix: resident batch download count mismatch must be actionable: {count_mismatch}"
730 );
731
732 backend
733 .free_resident(resource)
734 .expect("Fix: native Metal must free resident negative-test handles.");
735 }
736
737 #[cfg(any(target_os = "macos", target_os = "ios"))]
738 #[test]
739 fn apple_resident_ranged_batch_download_fuses_views_and_preflights_outputs() {
740 let backend = acquire().expect(
741 "Fix: Apple Metal builds must acquire the system default MTLDevice before fused resident batch readback tests.",
742 );
743 let resource = backend
744 .allocate_resident(16)
745 .expect("Fix: native Metal must allocate resident buffers before fused readback.");
746 let bytes = (0u8..16u8).collect::<Vec<_>>();
747 backend
748 .upload_resident(&resource, &bytes)
749 .expect("Fix: native Metal must upload resident bytes before fused readback.");
750
751 let mut first = vec![0xaa; 2];
752 let first_capacity = first.capacity();
753 let mut overlap = vec![0xbb; 1];
754 let mut empty = vec![0xcc; 3];
755 let mut tail = Vec::with_capacity(32);
756 let tail_capacity = tail.capacity();
757 backend
758 .download_resident_ranges_into(
759 &[
760 (&resource, 0, 6),
761 (&resource, 4, 6),
762 (&resource, 10, 0),
763 (&resource, 10, 4),
764 ],
765 &mut [&mut first, &mut overlap, &mut empty, &mut tail],
766 )
767 .expect("Fix: native Metal must materialize overlapping and empty views from one fused resident batch plan.");
768
769 assert_eq!(first, vec![0, 1, 2, 3, 4, 5]);
770 assert_eq!(overlap, vec![4, 5, 6, 7, 8, 9]);
771 assert_eq!(
772 empty,
773 Vec::<u8>::new(),
774 "Fix: zero-byte resident batch views must clear stale caller output bytes."
775 );
776 assert_eq!(tail, vec![10, 11, 12, 13]);
777 assert!(
778 first.capacity() >= first_capacity && tail.capacity() >= tail_capacity,
779 "Fix: fused Metal resident readback must preserve reusable caller output capacity."
780 );
781
782 let mut valid_output = vec![0xdd, 0xee];
783 let mut invalid_output = vec![0xff];
784 let before_valid = valid_output.clone();
785 let before_invalid = invalid_output.clone();
786 let error = backend
787 .download_resident_ranges_into(
788 &[(&resource, 0, 2), (&resource, 15, 4)],
789 &mut [&mut valid_output, &mut invalid_output],
790 )
791 .expect_err("Fix: native Metal fused resident batch readback must reject invalid ranges before mutating outputs.");
792 assert!(
793 error
794 .to_string()
795 .contains("requested byte range [15..19) from allocation 16"),
796 "Fix: fused resident readback range errors must name the invalid range and allocation: {error}"
797 );
798 assert_eq!(
799 valid_output, before_valid,
800 "Fix: fused resident batch download must not mutate earlier outputs when a later range fails validation."
801 );
802 assert_eq!(
803 invalid_output, before_invalid,
804 "Fix: fused resident batch download must not mutate the invalid output slot."
805 );
806
807 backend
808 .free_resident(resource)
809 .expect("Fix: native Metal must free fused readback resident handles.");
810 }
811
812 #[cfg(any(target_os = "macos", target_os = "ios"))]
813 #[test]
814 fn apple_resident_dispatch_uses_binding_order_handles_and_persists_output() {
815 use vyre_driver::DispatchConfig;
816 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
817
818 let program = Program::wrapped(
819 vec![
820 BufferDecl::storage("input", 0, BufferAccess::ReadOnly, DataType::U32)
821 .with_count(1),
822 BufferDecl::storage("out", 1, BufferAccess::WriteOnly, DataType::U32)
823 .with_count(1)
824 .with_output_byte_range(0..4),
825 ],
826 [1, 1, 1],
827 vec![Node::store(
828 "out",
829 Expr::u32(0),
830 Expr::add(Expr::load("input", Expr::u32(0)), Expr::u32(1)),
831 )],
832 );
833
834 let backend = acquire().expect(
835 "Fix: Apple Metal builds must acquire the system default MTLDevice before resident dispatch.",
836 );
837 let input = backend
838 .allocate_resident(4)
839 .expect("Fix: native Metal must allocate a resident input handle.");
840 let output = backend
841 .allocate_resident(4)
842 .expect("Fix: native Metal must allocate a resident output handle.");
843 backend
844 .upload_resident(&input, &41u32.to_le_bytes())
845 .expect("Fix: native Metal must upload resident input bytes before dispatch.");
846
847 let timed = backend
848 .dispatch_resident_timed(
849 &program,
850 &[input.clone(), output.clone()],
851 &DispatchConfig::default(),
852 )
853 .expect(
854 "Fix: native Metal resident dispatch must bind resources in Program binding order.",
855 );
856 assert_eq!(timed.outputs, vec![42u32.to_le_bytes().to_vec()]);
857 assert!(
858 timed.enqueue_ns.is_some() && timed.wait_ns.is_some() && timed.wall_ns > 0,
859 "Fix: Metal resident timed dispatch must report host timing fields."
860 );
861 assert_eq!(
862 backend
863 .download_resident(&output)
864 .expect("Fix: resident output must remain readable after dispatch."),
865 42u32.to_le_bytes().to_vec()
866 );
867
868 backend
869 .free_resident(input)
870 .expect("Fix: native Metal must free resident input handles.");
871 backend
872 .free_resident(output)
873 .expect("Fix: native Metal must free resident output handles.");
874 }
875
876 #[cfg(any(target_os = "macos", target_os = "ios"))]
877 #[test]
878 fn apple_resident_dispatch_resource_errors_are_actionable() {
879 use vyre_driver::DispatchConfig;
880 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
881
882 let program = Program::wrapped(
883 vec![
884 BufferDecl::storage("input", 0, BufferAccess::ReadOnly, DataType::U32)
885 .with_count(1),
886 BufferDecl::storage("out", 1, BufferAccess::WriteOnly, DataType::U32)
887 .with_count(1)
888 .with_output_byte_range(0..4),
889 ],
890 [1, 1, 1],
891 vec![Node::store(
892 "out",
893 Expr::u32(0),
894 Expr::add(Expr::load("input", Expr::u32(0)), Expr::u32(1)),
895 )],
896 );
897
898 let backend = acquire().expect(
899 "Fix: Apple Metal builds must acquire the system default MTLDevice before resident dispatch negative tests.",
900 );
901 let input = backend.allocate_resident(4).expect(
902 "Fix: native Metal must allocate resident input before negative dispatch checks.",
903 );
904 backend
905 .upload_resident(&input, &10u32.to_le_bytes())
906 .expect(
907 "Fix: native Metal must upload resident input before negative dispatch checks.",
908 );
909
910 let wrong_count = backend
911 .dispatch_resident_timed(
912 &program,
913 std::slice::from_ref(&input),
914 &DispatchConfig::default(),
915 )
916 .expect_err(
917 "Fix: native Metal resident dispatch must reject missing output resources.",
918 );
919 assert!(
920 wrong_count
921 .to_string()
922 .contains("expected 2 resource(s) in binding order but received 1"),
923 "Fix: resident dispatch wrong-count error must name expected and received resource counts: {wrong_count}"
924 );
925
926 let stale_output = backend.allocate_resident(4).expect(
927 "Fix: native Metal must allocate a resident output before stale-handle checks.",
928 );
929 backend
930 .free_resident(stale_output.clone())
931 .expect("Fix: native Metal must free resident output before stale-handle checks.");
932 let stale_resources = [input.clone(), stale_output];
933 let stale_error = backend
934 .dispatch_resident_timed(&program, &stale_resources, &DispatchConfig::default())
935 .expect_err("Fix: native Metal resident dispatch must reject stale output handles.");
936 assert!(
937 stale_error.to_string().contains("stale handle"),
938 "Fix: resident dispatch stale-handle error must name the handle lifetime problem: {stale_error}"
939 );
940
941 let undersized_output = backend
942 .allocate_resident(0)
943 .expect("Fix: native Metal must allow zero-byte logical resident allocations for boundary testing.");
944 let undersized_resources = [input.clone(), undersized_output.clone()];
945 let undersized_error = backend
946 .dispatch_resident_timed(&program, &undersized_resources, &DispatchConfig::default())
947 .expect_err(
948 "Fix: native Metal resident dispatch must reject undersized output handles.",
949 );
950 assert!(
951 undersized_error
952 .to_string()
953 .contains("requires 4 byte(s)")
954 && undersized_error.to_string().contains("has 0"),
955 "Fix: resident dispatch undersized-output error must name required and actual byte counts: {undersized_error}"
956 );
957
958 backend
959 .free_resident(input)
960 .expect("Fix: native Metal must free resident negative-dispatch input handles.");
961 backend
962 .free_resident(undersized_output)
963 .expect("Fix: native Metal must free resident negative-dispatch output handles.");
964 }
965
966 #[cfg(any(target_os = "macos", target_os = "ios"))]
967 #[test]
968 fn apple_resident_sequence_dispatches_ordered_steps_and_reads_ranges() {
969 use vyre_driver::{ResidentDispatchStep, ResidentReadRange};
970 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
971
972 let double_program = Program::wrapped(
973 vec![
974 BufferDecl::storage("input", 0, BufferAccess::ReadOnly, DataType::U32)
975 .with_count(1),
976 BufferDecl::storage("mid", 1, BufferAccess::WriteOnly, DataType::U32)
977 .with_count(1)
978 .with_output_byte_range(0..4),
979 ],
980 [1, 1, 1],
981 vec![Node::store(
982 "mid",
983 Expr::u32(0),
984 Expr::mul(Expr::load("input", Expr::u32(0)), Expr::u32(2)),
985 )],
986 );
987 let add_program = Program::wrapped(
988 vec![
989 BufferDecl::storage("mid", 0, BufferAccess::ReadOnly, DataType::U32).with_count(1),
990 BufferDecl::storage("out", 1, BufferAccess::WriteOnly, DataType::U32)
991 .with_count(1)
992 .with_output_byte_range(0..4),
993 ],
994 [1, 1, 1],
995 vec![Node::store(
996 "out",
997 Expr::u32(0),
998 Expr::add(Expr::load("mid", Expr::u32(0)), Expr::u32(7)),
999 )],
1000 );
1001
1002 let backend = acquire().expect(
1003 "Fix: Apple Metal builds must acquire the system default MTLDevice before resident sequence dispatch.",
1004 );
1005 let seed = backend
1006 .allocate_resident(4)
1007 .expect("Fix: native Metal must allocate resident sequence input.");
1008 let mid = backend
1009 .allocate_resident(4)
1010 .expect("Fix: native Metal must allocate resident sequence handoff.");
1011 let out = backend
1012 .allocate_resident(4)
1013 .expect("Fix: native Metal must allocate resident sequence output.");
1014 backend
1015 .upload_resident(&seed, &16u32.to_le_bytes())
1016 .expect("Fix: native Metal must upload resident sequence seed bytes.");
1017
1018 let first_resources = [seed.clone(), mid.clone()];
1019 let second_resources = [mid.clone(), out.clone()];
1020 let steps = [
1021 ResidentDispatchStep {
1022 program: &double_program,
1023 resources: &first_resources,
1024 grid_override: None,
1025 },
1026 ResidentDispatchStep {
1027 program: &add_program,
1028 resources: &second_resources,
1029 grid_override: None,
1030 },
1031 ];
1032 let read_ranges = [ResidentReadRange {
1033 resource: &out,
1034 byte_offset: 0,
1035 byte_len: 4,
1036 }];
1037 let mut readback = Vec::new();
1038
1039 let timing = backend
1040 .dispatch_resident_sequence_read_ranges_timed_into(
1041 &steps,
1042 &read_ranges,
1043 &mut [&mut readback],
1044 )
1045 .expect("Fix: native Metal must execute ordered resident sequences through the public backend API.");
1046
1047 assert_eq!(
1048 readback,
1049 39u32.to_le_bytes().to_vec(),
1050 "Fix: resident sequence readback must observe step-2 output fed by step-1 resident handoff."
1051 );
1052 assert_eq!(
1053 backend
1054 .download_resident(&mid)
1055 .expect("Fix: resident sequence handoff must remain readable."),
1056 32u32.to_le_bytes().to_vec(),
1057 "Fix: resident sequence must persist intermediate output in the handoff handle."
1058 );
1059 assert!(
1060 timing.wall_ns > 0 && timing.enqueue_ns.is_some() && timing.wait_ns.is_some(),
1061 "Fix: resident sequence timing must preserve Metal host enqueue/wait evidence."
1062 );
1063 assert_eq!(
1064 timing.device_ns, None,
1065 "Fix: Metal resident sequence must not fake device timing until native counters are wired."
1066 );
1067
1068 backend
1069 .free_resident(seed)
1070 .expect("Fix: native Metal must free resident sequence seed handles.");
1071 backend
1072 .free_resident(mid)
1073 .expect("Fix: native Metal must free resident sequence handoff handles.");
1074 backend
1075 .free_resident(out)
1076 .expect("Fix: native Metal must free resident sequence output handles.");
1077 }
1078
1079 #[cfg(any(target_os = "macos", target_os = "ios"))]
1080 #[test]
1081 fn apple_repeated_resident_sequence_updates_read_write_handle() {
1082 use vyre_driver::{ResidentDispatchStep, ResidentReadRange};
1083 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
1084
1085 let increment_program = Program::wrapped(
1086 vec![
1087 BufferDecl::storage("state", 0, BufferAccess::ReadWrite, DataType::U32)
1088 .with_count(1)
1089 .with_output_byte_range(0..4),
1090 ],
1091 [1, 1, 1],
1092 vec![Node::store(
1093 "state",
1094 Expr::u32(0),
1095 Expr::add(Expr::load("state", Expr::u32(0)), Expr::u32(1)),
1096 )],
1097 );
1098
1099 let backend = acquire().expect(
1100 "Fix: Apple Metal builds must acquire the system default MTLDevice before repeated resident sequence dispatch.",
1101 );
1102 let state = backend
1103 .allocate_resident(4)
1104 .expect("Fix: native Metal must allocate repeated resident sequence state.");
1105 backend
1106 .upload_resident(&state, &5u32.to_le_bytes())
1107 .expect("Fix: native Metal must upload repeated resident sequence state bytes.");
1108
1109 let step_resources = [state.clone()];
1110 let repeated_steps = [ResidentDispatchStep {
1111 program: &increment_program,
1112 resources: &step_resources,
1113 grid_override: None,
1114 }];
1115 let read_ranges = [ResidentReadRange {
1116 resource: &state,
1117 byte_offset: 0,
1118 byte_len: 4,
1119 }];
1120 let mut readback = Vec::new();
1121
1122 backend
1123 .dispatch_resident_repeated_sequence_read_ranges_into(
1124 &[],
1125 &repeated_steps,
1126 3,
1127 &read_ranges,
1128 &mut [&mut readback],
1129 )
1130 .expect("Fix: native Metal must execute repeated resident sequences through the public backend API.");
1131
1132 assert_eq!(
1133 readback,
1134 8u32.to_le_bytes().to_vec(),
1135 "Fix: repeated resident sequence must preserve state across repeated read-write dispatches."
1136 );
1137 assert_eq!(
1138 backend
1139 .download_resident(&state)
1140 .expect("Fix: repeated resident sequence state must remain readable."),
1141 8u32.to_le_bytes().to_vec(),
1142 "Fix: repeated resident sequence must persist the final state in the resident handle."
1143 );
1144
1145 backend
1146 .free_resident(state)
1147 .expect("Fix: native Metal must free repeated resident sequence state handles.");
1148 }
1149
1150 #[cfg(any(target_os = "macos", target_os = "ios"))]
1151 #[test]
1152 fn apple_dispatch_reuses_pipeline_cache_for_identical_program_and_policy() {
1153 use vyre_driver::DispatchConfig;
1154 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
1155
1156 let program = Program::wrapped(
1157 vec![
1158 BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
1159 .with_count(1)
1160 .with_output_byte_range(0..4),
1161 ],
1162 [1, 1, 1],
1163 vec![Node::store("out", Expr::u32(0), Expr::u32(99))],
1164 );
1165
1166 let backend = acquire().expect(
1167 "Fix: Apple Metal builds must acquire the system default MTLDevice before cache testing.",
1168 );
1169 let before = backend
1170 .pipeline_cache_snapshot()
1171 .expect("Fix: native Metal must expose honest pipeline cache counters.");
1172 let first = backend
1173 .dispatch(&program, &[], &DispatchConfig::default())
1174 .expect("Fix: first Metal dispatch must compile and execute the program.");
1175 let after_first = backend
1176 .pipeline_cache_snapshot()
1177 .expect("Fix: native Metal must expose cache counters after compile.");
1178 let second = backend
1179 .dispatch(&program, &[], &DispatchConfig::default())
1180 .expect("Fix: second Metal dispatch must reuse the compiled pipeline and execute.");
1181 let after_second = backend
1182 .pipeline_cache_snapshot()
1183 .expect("Fix: native Metal must expose cache counters after a cache hit.");
1184
1185 assert_eq!(first, vec![99u32.to_le_bytes().to_vec()]);
1186 assert_eq!(second, first);
1187 assert_eq!(
1188 after_first.misses,
1189 before.misses + 1,
1190 "Fix: first identical Metal dispatch should record exactly one pipeline cache miss."
1191 );
1192 assert_eq!(
1193 after_first.hits, before.hits,
1194 "Fix: first Metal dispatch must not claim a cache hit before the pipeline exists."
1195 );
1196 assert_eq!(
1197 after_second.hits,
1198 after_first.hits + 1,
1199 "Fix: second identical Metal dispatch must reuse the compiled pipeline cache."
1200 );
1201 assert_eq!(
1202 after_second.misses, after_first.misses,
1203 "Fix: cache hit dispatch must not increment Metal pipeline miss counters."
1204 );
1205 }
1206
1207 #[cfg(any(target_os = "macos", target_os = "ios"))]
1208 #[test]
1209 fn apple_pipeline_cache_partitions_workgroup_policy_changes() {
1210 use vyre_driver::DispatchConfig;
1211 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
1212
1213 let program = Program::wrapped(
1214 vec![
1215 BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
1216 .with_count(1)
1217 .with_output_byte_range(0..4),
1218 ],
1219 [1, 1, 1],
1220 vec![Node::store("out", Expr::u32(0), Expr::u32(88))],
1221 );
1222
1223 let backend = acquire().expect(
1224 "Fix: Apple Metal builds must acquire the system default MTLDevice before cache policy testing.",
1225 );
1226 let before = backend
1227 .pipeline_cache_snapshot()
1228 .expect("Fix: native Metal must expose honest pipeline cache counters.");
1229 let default_config = DispatchConfig::default();
1230 let default_output = backend
1231 .dispatch(&program, &[], &default_config)
1232 .expect("Fix: first Metal dispatch must compile the default policy pipeline.");
1233 let after_default = backend
1234 .pipeline_cache_snapshot()
1235 .expect("Fix: native Metal must expose cache counters after default policy compile.");
1236 let mut workgroup_policy = DispatchConfig::default();
1237 workgroup_policy.workgroup_override = Some([1, 1, 1]);
1238 let policy_output = backend
1239 .dispatch(&program, &[], &workgroup_policy)
1240 .expect("Fix: Metal dispatch must compile a distinct workgroup-policy pipeline.");
1241 let after_policy = backend
1242 .pipeline_cache_snapshot()
1243 .expect("Fix: native Metal must expose cache counters after policy-change compile.");
1244 let policy_hit_output = backend.dispatch(&program, &[], &workgroup_policy).expect(
1245 "Fix: repeated Metal workgroup-policy dispatch must hit the policy cache entry.",
1246 );
1247 let after_policy_hit = backend
1248 .pipeline_cache_snapshot()
1249 .expect("Fix: native Metal must expose cache counters after policy hit.");
1250
1251 assert_eq!(default_output, vec![88u32.to_le_bytes().to_vec()]);
1252 assert_eq!(policy_output, default_output);
1253 assert_eq!(policy_hit_output, default_output);
1254 assert_eq!(
1255 after_default.misses,
1256 before.misses + 1,
1257 "Fix: first default-policy Metal dispatch must record one pipeline cache miss."
1258 );
1259 assert_eq!(
1260 after_policy.misses,
1261 after_default.misses + 1,
1262 "Fix: changing Metal workgroup policy must compile a distinct cache entry."
1263 );
1264 assert_eq!(
1265 after_policy.hits, after_default.hits,
1266 "Fix: first dispatch for a changed workgroup policy must not claim a cache hit."
1267 );
1268 assert_eq!(
1269 after_policy_hit.hits,
1270 after_policy.hits + 1,
1271 "Fix: repeated dispatch for the same changed workgroup policy must hit the Metal pipeline cache."
1272 );
1273 assert_eq!(
1274 after_policy_hit.misses, after_policy.misses,
1275 "Fix: repeated dispatch for the same changed workgroup policy must not add another miss."
1276 );
1277 }
1278
1279 #[cfg(any(target_os = "macos", target_os = "ios"))]
1280 #[test]
1281 fn apple_backend_metric_snapshot_exposes_cache_and_resident_counters() {
1282 use std::collections::BTreeMap;
1283
1284 use vyre_driver::DispatchConfig;
1285 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
1286
1287 let program = Program::wrapped(
1288 vec![
1289 BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
1290 .with_count(1)
1291 .with_output_byte_range(0..4),
1292 ],
1293 [1, 1, 1],
1294 vec![Node::store("out", Expr::u32(0), Expr::u32(233))],
1295 );
1296
1297 let backend = acquire().expect(
1298 "Fix: Apple Metal builds must acquire the system default MTLDevice before backend metric testing.",
1299 );
1300 let resident = backend.allocate_resident(16).expect(
1301 "Fix: native Metal must allocate a resident buffer before metric snapshot testing.",
1302 );
1303 backend
1304 .upload_resident(&resident, &11u32.to_le_bytes())
1305 .expect("Fix: native Metal must upload resident bytes before metric snapshot testing.");
1306 let mut resident_readback = Vec::new();
1307 backend
1308 .download_resident_range_into(&resident, 0, 4, &mut resident_readback)
1309 .expect(
1310 "Fix: native Metal must download resident bytes before metric snapshot testing.",
1311 );
1312 assert_eq!(resident_readback, 11u32.to_le_bytes().to_vec());
1313 backend
1314 .dispatch(&program, &[], &DispatchConfig::default())
1315 .expect("Fix: first Metal metric-snapshot dispatch must compile and execute.");
1316 backend
1317 .dispatch(&program, &[], &DispatchConfig::default())
1318 .expect("Fix: second Metal metric-snapshot dispatch must hit the pipeline cache.");
1319 let mut changed_policy = DispatchConfig::default();
1320 changed_policy.workgroup_override = Some([2, 1, 1]);
1321 backend
1322 .dispatch(&program, &[], &changed_policy)
1323 .expect("Fix: Metal metric-snapshot policy probe must compile a distinct policy key.");
1324 let changed_program = Program::wrapped(
1325 vec![
1326 BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
1327 .with_count(1)
1328 .with_output_byte_range(0..4),
1329 ],
1330 [1, 1, 1],
1331 vec![Node::store("out", Expr::u32(0), Expr::u32(234))],
1332 );
1333 backend
1334 .dispatch(&changed_program, &[], &DispatchConfig::default())
1335 .expect(
1336 "Fix: Metal metric-snapshot program probe must compile a distinct Program key.",
1337 );
1338
1339 let metrics = backend
1340 .backend_metric_snapshot()
1341 .into_iter()
1342 .collect::<BTreeMap<_, _>>();
1343
1344 assert!(
1345 metrics.get("metal_pipeline_cache_hits").copied().unwrap_or(0) >= 1,
1346 "Fix: Metal backend metric snapshot must expose real pipeline cache hits for benchmark JSON."
1347 );
1348 assert!(
1349 metrics
1350 .get("metal_pipeline_cache_misses")
1351 .copied()
1352 .unwrap_or(0)
1353 >= 1,
1354 "Fix: Metal backend metric snapshot must expose real pipeline cache misses for benchmark JSON."
1355 );
1356 assert_eq!(
1357 metrics
1358 .get("metal_pipeline_cache_miss_empty_cache")
1359 .copied(),
1360 Some(1),
1361 "Fix: Metal metric snapshot must explain the first cold miss as an empty-cache miss."
1362 );
1363 assert_eq!(
1364 metrics
1365 .get("metal_pipeline_cache_miss_dispatch_policy_changed")
1366 .copied(),
1367 Some(1),
1368 "Fix: Metal metric snapshot must explain same-program policy changes as dispatch-policy cache misses."
1369 );
1370 assert_eq!(
1371 metrics
1372 .get("metal_pipeline_cache_miss_program_changed")
1373 .copied(),
1374 Some(1),
1375 "Fix: Metal metric snapshot must explain different Program digests as program-change cache misses."
1376 );
1377 assert_eq!(
1378 metrics
1379 .get("metal_pipeline_cache_miss_device_or_runtime_changed")
1380 .copied(),
1381 Some(0),
1382 "Fix: Metal metric snapshot must expose the device/runtime miss bucket even when a single backend instance cannot trigger it."
1383 );
1384 assert_eq!(
1385 metrics.get("metal_pipeline_cache_miss_key_absent").copied(),
1386 Some(0),
1387 "Fix: Metal metric snapshot must expose the fallback key-absent miss bucket for future key fields."
1388 );
1389 assert!(
1390 metrics
1391 .get("metal_buffer_allocation_count")
1392 .copied()
1393 .unwrap_or(0)
1394 >= 1,
1395 "Fix: Metal metric snapshot must expose buffer allocation count."
1396 );
1397 assert!(
1398 metrics
1399 .get("metal_buffer_allocation_bytes")
1400 .copied()
1401 .unwrap_or(0)
1402 >= 16,
1403 "Fix: Metal metric snapshot must expose buffer allocation bytes."
1404 );
1405 assert!(
1406 metrics
1407 .get("metal_host_to_device_copy_count")
1408 .copied()
1409 .unwrap_or(0)
1410 >= 1,
1411 "Fix: Metal metric snapshot must expose host-to-device copy count."
1412 );
1413 assert!(
1414 metrics
1415 .get("metal_host_to_device_bytes")
1416 .copied()
1417 .unwrap_or(0)
1418 >= 4,
1419 "Fix: Metal metric snapshot must expose host-to-device bytes."
1420 );
1421 assert!(
1422 metrics
1423 .get("metal_device_to_host_copy_count")
1424 .copied()
1425 .unwrap_or(0)
1426 >= 1,
1427 "Fix: Metal metric snapshot must expose device-to-host copy count."
1428 );
1429 assert!(
1430 metrics
1431 .get("metal_device_to_host_bytes")
1432 .copied()
1433 .unwrap_or(0)
1434 >= 4,
1435 "Fix: Metal metric snapshot must expose device-to-host bytes."
1436 );
1437 assert!(
1438 metrics
1439 .get("metal_output_readback_bytes")
1440 .copied()
1441 .unwrap_or(0)
1442 >= 4,
1443 "Fix: Metal metric snapshot must expose dispatch output readback bytes separately from resident downloads."
1444 );
1445 assert_eq!(
1446 metrics.get("metal_resident_buffer_count").copied(),
1447 Some(1),
1448 "Fix: Metal backend metric snapshot must expose live resident buffer count."
1449 );
1450 assert_eq!(
1451 metrics.get("metal_resident_bytes").copied(),
1452 Some(16),
1453 "Fix: Metal backend metric snapshot must expose logical resident bytes."
1454 );
1455
1456 backend
1457 .free_resident(resident)
1458 .expect("Fix: native Metal must free metric-snapshot resident handles.");
1459 }
1460
1461 #[cfg(any(target_os = "macos", target_os = "ios"))]
1462 #[test]
1463 fn apple_shutdown_invalidates_pipeline_cache_entries() {
1464 use vyre_driver::DispatchConfig;
1465 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
1466
1467 let program = Program::wrapped(
1468 vec![
1469 BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
1470 .with_count(1)
1471 .with_output_byte_range(0..4),
1472 ],
1473 [1, 1, 1],
1474 vec![Node::store("out", Expr::u32(0), Expr::u32(144))],
1475 );
1476
1477 let backend = acquire().expect(
1478 "Fix: Apple Metal builds must acquire the system default MTLDevice before lifecycle cache testing.",
1479 );
1480 let before = backend
1481 .pipeline_cache_snapshot()
1482 .expect("Fix: native Metal must expose honest pipeline cache counters.");
1483 let first = backend
1484 .dispatch(&program, &[], &DispatchConfig::default())
1485 .expect("Fix: first Metal dispatch must compile the lifecycle cache probe.");
1486 let after_first = backend
1487 .pipeline_cache_snapshot()
1488 .expect("Fix: native Metal must expose counters after first lifecycle cache dispatch.");
1489 let second = backend
1490 .dispatch(&program, &[], &DispatchConfig::default())
1491 .expect("Fix: second Metal dispatch must hit the lifecycle cache probe.");
1492 let after_hit = backend
1493 .pipeline_cache_snapshot()
1494 .expect("Fix: native Metal must expose counters after lifecycle cache hit.");
1495
1496 backend
1497 .shutdown()
1498 .expect("Fix: native Metal shutdown must invalidate backend-owned caches.");
1499 let after_shutdown = backend
1500 .pipeline_cache_snapshot()
1501 .expect("Fix: native Metal must keep cache counters observable after shutdown.");
1502 let third = backend
1503 .dispatch(&program, &[], &DispatchConfig::default())
1504 .expect("Fix: native Metal dispatch must recover after shutdown cache invalidation.");
1505 let after_recompile = backend
1506 .pipeline_cache_snapshot()
1507 .expect("Fix: native Metal must expose counters after post-shutdown recompile.");
1508
1509 assert_eq!(first, vec![144u32.to_le_bytes().to_vec()]);
1510 assert_eq!(second, first);
1511 assert_eq!(third, first);
1512 assert_eq!(
1513 after_first.misses,
1514 before.misses + 1,
1515 "Fix: first lifecycle cache probe dispatch must record one miss."
1516 );
1517 assert_eq!(
1518 after_hit.hits,
1519 after_first.hits + 1,
1520 "Fix: second lifecycle cache probe dispatch must hit the compiled pipeline cache."
1521 );
1522 assert_eq!(
1523 after_shutdown, after_hit,
1524 "Fix: Metal shutdown must invalidate cache entries without rewriting historical hit/miss counters."
1525 );
1526 assert_eq!(
1527 after_recompile.misses,
1528 after_hit.misses + 1,
1529 "Fix: dispatch after Metal shutdown must recompile instead of reusing stale cached pipeline state."
1530 );
1531 assert_eq!(
1532 after_recompile.hits, after_hit.hits,
1533 "Fix: post-shutdown recompile must not be counted as a cache hit."
1534 );
1535 }
1536
1537 #[cfg(any(target_os = "macos", target_os = "ios"))]
1538 #[test]
1539 fn apple_device_profile_reports_live_metal_limits() {
1540 let backend = acquire().expect(
1541 "Fix: Apple Metal builds must acquire the system default MTLDevice before profile testing.",
1542 );
1543 let profile = backend.device_profile();
1544
1545 assert_eq!(profile.backend, METAL_BACKEND_ID);
1546 assert!(profile.supports_subgroup_ops);
1547 assert_eq!(profile.subgroup_size, backend.subgroup_size().unwrap_or(0));
1548 assert_eq!(profile.max_workgroup_size, backend.max_workgroup_size());
1549 assert_eq!(
1550 profile.max_invocations_per_workgroup,
1551 backend.max_compute_invocations_per_workgroup()
1552 );
1553 assert!(
1554 profile.max_workgroup_size[0] > 0 && profile.max_invocations_per_workgroup > 0,
1555 "Fix: Metal profile must report nonzero live workgroup limits."
1556 );
1557 assert!(
1558 profile.max_shared_memory_bytes > 0 && profile.has_shared_memory,
1559 "Fix: Metal profile must expose threadgroup memory as a typed shared-memory capability."
1560 );
1561 assert_eq!(
1562 profile.max_storage_buffer_binding_size,
1563 backend.max_storage_buffer_bytes()
1564 );
1565 assert!(
1566 profile.max_storage_buffer_binding_size > 0,
1567 "Fix: Metal profile must expose the native maxBufferLength storage limit."
1568 );
1569 assert!(
1570 !profile.supports_specialization_constants,
1571 "Fix: Metal must not advertise function constants until lowering/runtime actually use them."
1572 );
1573 assert!(
1574 !profile.supports_indirect_dispatch,
1575 "Fix: Metal must not advertise indirect dispatch until the backend executes indirect dispatch nodes."
1576 );
1577 assert_eq!(
1578 profile.timing_quality,
1579 vyre_driver::DeviceTimingQuality::HostEnqueueWait,
1580 "Fix: Metal profile must classify timing as host enqueue/wait until device timestamps are implemented."
1581 );
1582 assert!(
1583 !profile.supports_device_timestamps && !profile.supports_hardware_counters,
1584 "Fix: Metal must not advertise timestamp or counter support until the runtime exposes real measurements."
1585 );
1586 assert!(profile.validation_capabilities().supports_subgroup_ops);
1587 assert_eq!(
1588 profile.adapter_caps().max_shared_memory_bytes,
1589 profile.max_shared_memory_bytes
1590 );
1591 }
1592
1593 #[cfg(any(target_os = "macos", target_os = "ios"))]
1594 #[test]
1595 fn apple_shutdown_clears_resident_handles() {
1596 let backend = acquire().expect(
1597 "Fix: Apple Metal builds must acquire the system default MTLDevice before shutdown testing.",
1598 );
1599 let resource = backend
1600 .allocate_resident(4)
1601 .expect("Fix: native Metal must allocate a resident handle before shutdown.");
1602 backend
1603 .upload_resident(&resource, &[1, 2, 3, 4])
1604 .expect("Fix: native Metal must upload resident bytes before shutdown.");
1605
1606 backend
1607 .shutdown()
1608 .expect("Fix: native Metal shutdown must clear backend-owned resources.");
1609 let error = backend.download_resident(&resource).expect_err(
1610 "Fix: resident handles must be invalid after Metal shutdown clears resources.",
1611 );
1612 assert!(
1613 error.to_string().contains("stale resident handle"),
1614 "Fix: post-shutdown resident use must fail closed as a stale handle: {error}"
1615 );
1616 }
1617
1618 #[cfg(any(target_os = "macos", target_os = "ios"))]
1619 #[test]
1620 fn apple_borrowed_dispatch_into_reuses_caller_output_slots() {
1621 use vyre_driver::DispatchConfig;
1622 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
1623
1624 let program = Program::wrapped(
1625 vec![
1626 BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
1627 .with_count(1)
1628 .with_output_byte_range(0..4),
1629 ],
1630 [1, 1, 1],
1631 vec![Node::store("out", Expr::u32(0), Expr::u32(123))],
1632 );
1633
1634 let backend = acquire().expect(
1635 "Fix: Apple Metal builds must acquire the system default MTLDevice before borrowed-into dispatch.",
1636 );
1637 let mut outputs = vec![Vec::with_capacity(64)];
1638 let original_capacity = outputs[0].capacity();
1639
1640 backend
1641 .dispatch_borrowed_into(&program, &[], &DispatchConfig::default(), &mut outputs)
1642 .expect("Fix: native Metal borrowed-into dispatch must execute through the public backend API.");
1643
1644 assert_eq!(
1645 outputs,
1646 vec![123u32.to_le_bytes().to_vec()],
1647 "Fix: borrowed-into Metal dispatch must write real kernel output into caller-owned slots."
1648 );
1649 assert!(
1650 outputs[0].capacity() >= original_capacity,
1651 "Fix: borrowed-into Metal dispatch must preserve reusable caller output capacity."
1652 );
1653 }
1654
1655 #[cfg(any(target_os = "macos", target_os = "ios"))]
1656 #[test]
1657 fn apple_compile_native_returns_persistent_pipeline_and_reuses_compiled_state() {
1658 use vyre_driver::DispatchConfig;
1659 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
1660
1661 let idx = Expr::var("idx");
1662 let program = Program::wrapped(
1663 vec![
1664 BufferDecl::storage("input", 0, BufferAccess::ReadOnly, DataType::U32)
1665 .with_count(4),
1666 BufferDecl::storage("out", 1, BufferAccess::WriteOnly, DataType::U32)
1667 .with_count(4)
1668 .with_output_byte_range(0..16),
1669 ],
1670 [4, 1, 1],
1671 vec![
1672 Node::let_bind("idx", Expr::gid_x()),
1673 Node::if_then(
1674 Expr::lt(idx.clone(), Expr::u32(4)),
1675 vec![Node::store(
1676 "out",
1677 idx.clone(),
1678 Expr::add(Expr::load("input", idx), Expr::u32(7)),
1679 )],
1680 ),
1681 ],
1682 );
1683 let input = [1u32, 2, 3, 4]
1684 .into_iter()
1685 .flat_map(u32::to_le_bytes)
1686 .collect::<Vec<_>>();
1687 let expected = [8u32, 9, 10, 11]
1688 .into_iter()
1689 .flat_map(u32::to_le_bytes)
1690 .collect::<Vec<_>>();
1691
1692 let backend = acquire().expect(
1693 "Fix: Apple Metal builds must acquire the system default MTLDevice before compiled pipeline testing.",
1694 );
1695 let config = DispatchConfig::default();
1696 let before = backend
1697 .pipeline_cache_snapshot()
1698 .expect("Fix: native Metal must expose cache counters before compile_native.");
1699 let pipeline = backend
1700 .compile_native(&program, &config)
1701 .expect("Fix: native Metal compile_native must compile a real Metal pipeline.")
1702 .expect("Fix: native Metal compile_native must return Some compiled pipeline.");
1703 assert!(
1704 pipeline.id().starts_with("metal:"),
1705 "Fix: Metal compiled pipeline id must be stable and backend-qualified: {}",
1706 pipeline.id()
1707 );
1708 let after_compile = backend
1709 .pipeline_cache_snapshot()
1710 .expect("Fix: native Metal must expose cache counters after compile_native.");
1711 assert_eq!(
1712 after_compile.misses,
1713 before.misses + 1,
1714 "Fix: Metal compile_native must populate the real pipeline cache exactly once on a cold program."
1715 );
1716 assert_eq!(
1717 after_compile.hits, before.hits,
1718 "Fix: first Metal compile_native for a cold program must not claim a cache hit."
1719 );
1720
1721 let first = pipeline
1722 .dispatch_borrowed(&[input.as_slice()], &config)
1723 .expect("Fix: Metal compiled pipeline must dispatch borrowed inputs through the real command path.");
1724 let second = pipeline
1725 .dispatch_borrowed(&[input.as_slice()], &config)
1726 .expect("Fix: repeated Metal compiled pipeline dispatch must reuse compiled state.");
1727 assert_eq!(first, vec![expected.clone()]);
1728 assert_eq!(second, first);
1729 let after_compiled_dispatch = backend
1730 .pipeline_cache_snapshot()
1731 .expect("Fix: native Metal must expose cache counters after compiled dispatch.");
1732 assert_eq!(
1733 after_compiled_dispatch, after_compile,
1734 "Fix: Metal compiled pipeline dispatch must not re-enter backend lowering/compile cache."
1735 );
1736
1737 let mut outputs = vec![Vec::with_capacity(64)];
1738 let output_capacity = outputs[0].capacity();
1739 pipeline
1740 .dispatch_borrowed_into(&[input.as_slice()], &config, &mut outputs)
1741 .expect("Fix: Metal compiled pipeline must fill caller-owned output slots.");
1742 assert_eq!(outputs, vec![expected]);
1743 assert!(
1744 outputs[0].capacity() >= output_capacity,
1745 "Fix: Metal compiled pipeline dispatch_into must preserve reusable output slot capacity."
1746 );
1747
1748 let direct = backend
1749 .dispatch_borrowed(&program, &[input.as_slice()], &config)
1750 .expect("Fix: direct Metal dispatch must still produce the same bytes as compiled pipeline dispatch.");
1751 assert_eq!(
1752 direct, outputs,
1753 "Fix: Metal compiled pipeline output must stay byte-identical to direct backend dispatch."
1754 );
1755 let after_direct = backend
1756 .pipeline_cache_snapshot()
1757 .expect("Fix: native Metal must expose cache counters after direct dispatch.");
1758 assert_eq!(
1759 after_direct.hits,
1760 after_compile.hits + 1,
1761 "Fix: direct dispatch after compile_native should hit the backend pipeline cache, proving compile_native populated it."
1762 );
1763 }
1764
1765 #[cfg(any(target_os = "macos", target_os = "ios"))]
1766 #[test]
1767 fn apple_compile_native_dispatches_persistent_resident_handles() {
1768 use vyre_driver::DispatchConfig;
1769 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
1770
1771 let program = Program::wrapped(
1772 vec![
1773 BufferDecl::storage("input", 0, BufferAccess::ReadOnly, DataType::U32)
1774 .with_count(1),
1775 BufferDecl::storage("out", 1, BufferAccess::WriteOnly, DataType::U32)
1776 .with_count(1)
1777 .with_output_byte_range(0..4),
1778 ],
1779 [1, 1, 1],
1780 vec![Node::store(
1781 "out",
1782 Expr::u32(0),
1783 Expr::add(Expr::load("input", Expr::u32(0)), Expr::u32(9)),
1784 )],
1785 );
1786
1787 let backend = acquire().expect(
1788 "Fix: Apple Metal builds must acquire the system default MTLDevice before compiled resident dispatch testing.",
1789 );
1790 let config = DispatchConfig::default();
1791 let input = backend
1792 .allocate_resident(4)
1793 .expect("Fix: native Metal must allocate compiled-pipeline resident input.");
1794 let output = backend
1795 .allocate_resident(4)
1796 .expect("Fix: native Metal must allocate compiled-pipeline resident output.");
1797 backend
1798 .upload_resident(&input, &33u32.to_le_bytes())
1799 .expect("Fix: native Metal must upload compiled-pipeline resident input bytes.");
1800 let before_compile = backend.pipeline_cache_snapshot().expect(
1801 "Fix: native Metal must expose cache counters before compiled resident testing.",
1802 );
1803 let pipeline = backend
1804 .compile_native(&program, &config)
1805 .expect("Fix: native Metal compile_native must compile resident-capable pipelines.")
1806 .expect(
1807 "Fix: native Metal compile_native must return Some for resident-capable pipelines.",
1808 );
1809 let after_compile = backend.pipeline_cache_snapshot().expect(
1810 "Fix: native Metal must expose cache counters after compiled resident compile.",
1811 );
1812
1813 let resources = [input.clone(), output.clone()];
1814 let timed = pipeline
1815 .dispatch_persistent_handles_timed(&resources, &config)
1816 .expect("Fix: Metal compiled pipeline must dispatch persistent resident handles.");
1817 assert_eq!(timed.outputs, vec![42u32.to_le_bytes().to_vec()]);
1818 assert!(
1819 timed.wall_ns > 0 && timed.enqueue_ns.is_some() && timed.wait_ns.is_some(),
1820 "Fix: Metal compiled resident dispatch must preserve host enqueue/wait timing evidence."
1821 );
1822 assert_eq!(
1823 backend
1824 .download_resident(&output)
1825 .expect("Fix: compiled resident output must remain readable."),
1826 42u32.to_le_bytes().to_vec(),
1827 "Fix: Metal compiled resident dispatch must persist output bytes in the resident handle."
1828 );
1829 let after_compiled_dispatch = backend.pipeline_cache_snapshot().expect(
1830 "Fix: native Metal must expose cache counters after compiled resident dispatch.",
1831 );
1832 assert_eq!(
1833 after_compiled_dispatch, after_compile,
1834 "Fix: Metal compiled resident dispatch must not re-enter backend lowering/compile cache."
1835 );
1836
1837 let mut outputs = vec![Vec::with_capacity(32)];
1838 let output_capacity = outputs[0].capacity();
1839 pipeline
1840 .dispatch_persistent_handles_into(&resources, &config, &mut outputs)
1841 .expect(
1842 "Fix: Metal compiled resident dispatch_into must fill caller-owned output slots.",
1843 );
1844 assert_eq!(outputs, vec![42u32.to_le_bytes().to_vec()]);
1845 assert!(
1846 outputs[0].capacity() >= output_capacity,
1847 "Fix: Metal compiled resident dispatch_into must preserve caller output slot capacity."
1848 );
1849 let after_dispatch_into = backend.pipeline_cache_snapshot().expect(
1850 "Fix: native Metal must expose cache counters after compiled resident dispatch_into.",
1851 );
1852 assert_eq!(
1853 after_dispatch_into, after_compiled_dispatch,
1854 "Fix: Metal compiled resident dispatch_into must reuse the compiled pipeline without cache traffic."
1855 );
1856 assert_eq!(
1857 after_compile.misses,
1858 before_compile.misses + 1,
1859 "Fix: compile_native must populate the real Metal cache once before resident compiled dispatch."
1860 );
1861
1862 backend
1863 .free_resident(output.clone())
1864 .expect("Fix: native Metal must free compiled resident output handles.");
1865 let stale_resources = [input.clone(), output];
1866 let stale_error = pipeline
1867 .dispatch_persistent_handles(&stale_resources, &config)
1868 .expect_err(
1869 "Fix: Metal compiled resident dispatch must reject stale resident handles.",
1870 );
1871 assert!(
1872 stale_error.to_string().contains("stale handle"),
1873 "Fix: Metal compiled resident stale-handle diagnostics must name the handle lifetime problem: {stale_error}"
1874 );
1875
1876 backend
1877 .free_resident(input)
1878 .expect("Fix: native Metal must free compiled resident input handles.");
1879 }
1880
1881 #[cfg(any(target_os = "macos", target_os = "ios"))]
1882 #[test]
1883 fn apple_compile_native_returns_resident_resource_outputs_for_zero_copy_chaining() {
1884 use vyre_driver::{DispatchConfig, Resource};
1885 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
1886
1887 let double_program = Program::wrapped(
1888 vec![
1889 BufferDecl::storage("input", 0, BufferAccess::ReadOnly, DataType::U32)
1890 .with_count(1),
1891 BufferDecl::storage("mid", 1, BufferAccess::WriteOnly, DataType::U32)
1892 .with_count(1)
1893 .with_output_byte_range(0..4),
1894 ],
1895 [1, 1, 1],
1896 vec![Node::store(
1897 "mid",
1898 Expr::u32(0),
1899 Expr::mul(Expr::load("input", Expr::u32(0)), Expr::u32(2)),
1900 )],
1901 );
1902 let add_program = Program::wrapped(
1903 vec![
1904 BufferDecl::storage("mid", 0, BufferAccess::ReadOnly, DataType::U32).with_count(1),
1905 BufferDecl::storage("out", 1, BufferAccess::WriteOnly, DataType::U32)
1906 .with_count(1)
1907 .with_output_byte_range(0..4),
1908 ],
1909 [1, 1, 1],
1910 vec![Node::store(
1911 "out",
1912 Expr::u32(0),
1913 Expr::add(Expr::load("mid", Expr::u32(0)), Expr::u32(5)),
1914 )],
1915 );
1916
1917 let backend = acquire().expect(
1918 "Fix: Apple Metal builds must acquire the system default MTLDevice before compiled resource-output testing.",
1919 );
1920 let config = DispatchConfig::default();
1921 let seed = backend
1922 .allocate_resident(4)
1923 .expect("Fix: native Metal must allocate zero-copy chain seed.");
1924 let mid = backend
1925 .allocate_resident(4)
1926 .expect("Fix: native Metal must allocate zero-copy chain middle output.");
1927 let out = backend
1928 .allocate_resident(4)
1929 .expect("Fix: native Metal must allocate zero-copy chain final output.");
1930 backend
1931 .upload_resident(&seed, &17u32.to_le_bytes())
1932 .expect("Fix: native Metal must upload zero-copy chain seed bytes.");
1933
1934 let double = backend
1935 .compile_native(&double_program, &config)
1936 .expect("Fix: native Metal must compile first zero-copy chain pipeline.")
1937 .expect("Fix: native Metal compile_native must return Some for first zero-copy chain pipeline.");
1938 let add = backend
1939 .compile_native(&add_program, &config)
1940 .expect("Fix: native Metal must compile second zero-copy chain pipeline.")
1941 .expect("Fix: native Metal compile_native must return Some for second zero-copy chain pipeline.");
1942
1943 let returned = double
1944 .dispatch_persistent_resource_outputs(&[seed.clone(), mid.clone()], &config)
1945 .expect("Fix: Metal compiled pipeline must return resident resource outputs without host readback.");
1946 assert_eq!(
1947 returned,
1948 vec![mid.clone()],
1949 "Fix: resource-output dispatch must return the caller-provided resident output handle in stable output order."
1950 );
1951 assert_eq!(
1952 backend.download_resident(&mid).expect(
1953 "Fix: zero-copy chain middle handle must remain readable for verification."
1954 ),
1955 34u32.to_le_bytes().to_vec(),
1956 "Fix: first zero-copy chain stage must persist bytes in the returned resident handle."
1957 );
1958
1959 let final_resources = [returned[0].clone(), out.clone()];
1960 let final_outputs = add
1961 .dispatch_persistent_handles(&final_resources, &config)
1962 .expect(
1963 "Fix: second Metal compiled pipeline must consume the returned resident output handle.",
1964 );
1965 assert_eq!(final_outputs, vec![39u32.to_le_bytes().to_vec()]);
1966 assert_eq!(
1967 backend
1968 .download_resident(&out)
1969 .expect("Fix: zero-copy chain final handle must remain readable."),
1970 39u32.to_le_bytes().to_vec(),
1971 "Fix: second zero-copy chain stage must persist final output bytes."
1972 );
1973
1974 let borrowed_error = double
1975 .dispatch_persistent_resource_outputs(
1976 &[seed.clone(), Resource::Borrowed(vec![0u8; 4])],
1977 &config,
1978 )
1979 .expect_err("Fix: resource-output dispatch must reject borrowed output resources.");
1980 assert!(
1981 borrowed_error
1982 .to_string()
1983 .contains("cannot return borrowed output binding"),
1984 "Fix: borrowed output rejection must explain how to keep the chain zero-copy: {borrowed_error}"
1985 );
1986
1987 backend
1988 .free_resident(seed)
1989 .expect("Fix: native Metal must free zero-copy chain seed.");
1990 backend
1991 .free_resident(mid)
1992 .expect("Fix: native Metal must free zero-copy chain middle output.");
1993 backend
1994 .free_resident(out)
1995 .expect("Fix: native Metal must free zero-copy chain final output.");
1996 }
1997
1998 #[cfg(any(target_os = "macos", target_os = "ios"))]
1999 #[test]
2000 fn apple_borrowed_timed_dispatch_reports_enqueue_and_wait() {
2001 use vyre_driver::DispatchConfig;
2002 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
2003
2004 let program = Program::wrapped(
2005 vec![
2006 BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
2007 .with_count(1)
2008 .with_output_byte_range(0..4),
2009 ],
2010 [1, 1, 1],
2011 vec![Node::store("out", Expr::u32(0), Expr::u32(77))],
2012 );
2013
2014 let backend = acquire().expect(
2015 "Fix: Apple Metal builds must acquire the system default MTLDevice before timed dispatch.",
2016 );
2017 let timed = backend
2018 .dispatch_borrowed_timed(&program, &[], &DispatchConfig::default())
2019 .expect("Fix: native Metal borrowed timed dispatch must execute through the real command path.");
2020
2021 assert_eq!(timed.outputs, vec![77u32.to_le_bytes().to_vec()]);
2022 assert!(
2023 timed.wall_ns > 0,
2024 "Fix: Metal borrowed timed dispatch must report nonzero wall time."
2025 );
2026 assert!(
2027 timed.enqueue_ns.is_some() && timed.wait_ns.is_some(),
2028 "Fix: Metal borrowed timed dispatch must expose native enqueue and wait timing."
2029 );
2030 assert_eq!(
2031 timed.device_ns, None,
2032 "Fix: Metal must not fake device timing until counter/timestamp support is implemented."
2033 );
2034 }
2035
2036 #[cfg(any(target_os = "macos", target_os = "ios"))]
2043 #[test]
2044 fn compile_native_returns_a_working_compiled_pipeline() {
2045 use vyre_driver::{DispatchConfig, VyreBackend};
2046 use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
2047
2048 let program = Program::wrapped(
2049 vec![
2050 BufferDecl::storage("out", 0, BufferAccess::WriteOnly, DataType::U32)
2051 .with_count(1)
2052 .with_output_byte_range(0..4),
2053 ],
2054 [1, 1, 1],
2055 vec![Node::store("out", Expr::u32(0), Expr::u32(0xDEAD_BEEFu32))],
2056 );
2057
2058 let backend = acquire().expect(
2059 "Fix: Apple Metal builds must acquire the system default MTLDevice.",
2060 );
2061 let compiled = backend
2062 .compile_native(&program, &DispatchConfig::default())
2063 .expect("Fix: Metal compile_native must succeed for a valid program.");
2064
2065 let pipeline = compiled.expect(
2067 "Fix: Metal compile_native must return Some(CompiledPipeline), not Ok(None)."
2068 );
2069
2070 let outputs = pipeline
2072 .dispatch(&[], &DispatchConfig::default())
2073 .expect("Fix: Metal compiled pipeline dispatch must succeed.");
2074 let result = u32::from_le_bytes(
2075 outputs[0]
2076 .as_slice()
2077 .try_into()
2078 .expect("Fix: compiled dispatch output must be one u32."),
2079 );
2080 assert_eq!(
2081 result, 0xDEAD_BEEFu32,
2082 "Fix: Metal compiled pipeline dispatch must produce the exact constant \
2083 written by the kernel; got 0x{result:08X}"
2084 );
2085 }
2086
2087 #[cfg(any(target_os = "macos", target_os = "ios"))]
2099 #[test]
2100 fn metric_snapshot_poisoned_mutex_is_loud() {
2101 use std::sync::Arc;
2102 use vyre_driver::VyreBackend;
2103
2104 let backend = acquire().expect(
2105 "Fix: Apple Metal builds must acquire the system default MTLDevice.",
2106 );
2107
2108 {
2112 }
2124
2125 let snapshot = backend.backend_metric_snapshot();
2128 let has_count = snapshot
2129 .iter()
2130 .any(|(k, _)| *k == "metal_resident_buffer_count");
2131 let has_bytes = snapshot
2132 .iter()
2133 .any(|(k, _)| *k == "metal_resident_bytes");
2134 assert!(
2135 has_count,
2136 "Fix: healthy backend snapshot must contain `metal_resident_buffer_count`; \
2137 got: {snapshot:?}"
2138 );
2139 assert!(
2140 has_bytes,
2141 "Fix: healthy backend snapshot must contain `metal_resident_bytes`; \
2142 got: {snapshot:?}"
2143 );
2144
2145 let has_error = snapshot
2147 .iter()
2148 .any(|(k, _)| *k == "metal_resident_buffer_error");
2149 assert!(
2150 !has_error,
2151 "Fix: healthy backend snapshot must not contain `metal_resident_buffer_error`; \
2152 got: {snapshot:?}"
2153 );
2154
2155 let count_entry = snapshot
2158 .iter()
2159 .find(|(k, _)| *k == "metal_resident_buffer_count")
2160 .map(|(_, v)| *v)
2161 .expect("Fix: metal_resident_buffer_count must be present in the snapshot");
2162 assert_eq!(
2163 count_entry, 0,
2164 "Fix: metal_resident_buffer_count must be 0 before any resident allocations; \
2165 if this fails with u64::MAX the mutex is unexpectedly poisoned"
2166 );
2167 }
2168}