1mod allocator;
4mod buffer;
5mod command;
6pub mod profile;
7mod residency;
8
9use std::collections::{HashMap, HashSet};
10use std::ptr::NonNull;
11use std::sync::atomic::{AtomicBool, Ordering};
12use std::sync::{Arc, Condvar, Mutex, OnceLock, PoisonError};
13
14use allocator::Pool;
15pub use buffer::Buffer;
16pub use command::{Arg, CommandBuffer, Pending, Submitted, Timeline};
17use nuvec_kernels::{MetallibMode, NaxMode};
18use objc2::rc::{Retained, autoreleasepool};
19use objc2::runtime::ProtocolObject;
20use objc2_foundation::NSString;
21use objc2_metal::{
22 MTLCommandQueue, MTLComputePipelineState, MTLCreateSystemDefaultDevice, MTLDataType, MTLDevice,
23 MTLFunctionConstantValues, MTLLibrary,
24};
25
26use crate::info::{NaxOverride, NaxReason, NaxStatus, nax_status, parse_architecture};
27use crate::{DeviceInfo, GpuError, MemoryStats, Result};
28
29type Library = Retained<ProtocolObject<dyn MTLLibrary>>;
30
31struct Libraries {
34 libraries: Vec<Library>,
35 by_name: Vec<(Box<str>, usize)>,
40}
41
42impl Libraries {
43 fn new(libraries: Vec<Library>) -> Self {
44 let mut by_name: Vec<(Box<str>, usize)> = Vec::new();
45 for (i, library) in libraries.iter().enumerate() {
46 for name in library.functionNames().iter() {
47 by_name.push((name.to_string().into_boxed_str(), i));
48 }
49 }
50 by_name.sort_by(|a, b| a.0.cmp(&b.0));
52 by_name.dedup_by(|later, first| later.0 == first.0);
53 Libraries { libraries, by_name }
54 }
55
56 fn get(&self, name: &str) -> Result<&Library> {
58 self.by_name
59 .binary_search_by(|(n, _)| n.as_ref().cmp(name))
60 .map(|at| &self.libraries[self.by_name[at].1])
61 .map_err(|_| GpuError::MissingKernel(name.to_owned()))
62 }
63}
64
65type Slot = Arc<Mutex<Option<Pipeline>>>;
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum Constant {
71 Bool(bool),
73 Int(i32),
75}
76
77#[derive(Clone)]
79pub struct Device {
80 inner: Arc<Inner>,
81}
82
83struct Inner {
84 device: Retained<ProtocolObject<dyn MTLDevice>>,
85 queue: Retained<ProtocolObject<dyn MTLCommandQueue>>,
86 info: DeviceInfo,
87 library: OnceLock<Result<Libraries>>,
88 nax_library: OnceLock<Result<Libraries>>,
90 nax: OnceLock<NaxStatus>,
92 nax_failed: AtomicBool,
94 pipelines: Mutex<HashMap<String, Slot>>,
99 pool: Arc<Pool>,
100 in_flight: InFlight,
101}
102
103impl Drop for Inner {
104 fn drop(&mut self) {
107 autoreleasepool(|_| self.pool.clear_cache());
108 }
109}
110
111#[derive(Default)]
113pub(crate) struct InFlight {
114 count: Mutex<usize>,
115 done: Condvar,
116}
117
118impl InFlight {
119 pub(crate) fn begin(&self) {
120 *self.count.lock().unwrap_or_else(PoisonError::into_inner) += 1;
121 }
122
123 pub(crate) fn end(&self) {
124 let mut count = self.count.lock().unwrap_or_else(PoisonError::into_inner);
125 *count = count.saturating_sub(1);
126 self.done.notify_all();
127 }
128
129 fn get(&self) -> usize {
130 *self.count.lock().unwrap_or_else(PoisonError::into_inner)
131 }
132
133 fn wait_below(&self, max: usize) {
134 let mut count = self.count.lock().unwrap_or_else(PoisonError::into_inner);
135 while *count >= max.max(1) {
136 count = self
137 .done
138 .wait(count)
139 .unwrap_or_else(PoisonError::into_inner);
140 }
141 }
142}
143
144impl Device {
145 pub fn system_default() -> Option<Device> {
148 autoreleasepool(|_| Self::create())
149 }
150
151 fn create() -> Option<Device> {
152 let device = MTLCreateSystemDefaultDevice()?;
153 let queue = device.newCommandQueue()?;
154 let architecture = device.architecture().name().to_string();
155 let (generation, class) = parse_architecture(&architecture);
156 let info = DeviceInfo {
157 name: device.name().to_string(),
158 architecture,
159 generation,
160 class,
161 recommended_working_set: device.recommendedMaxWorkingSetSize(),
162 max_threads_per_threadgroup: device.maxThreadsPerThreadgroup().width,
163 physical_memory: objc2_foundation::NSProcessInfo::processInfo().physicalMemory(),
164 };
165 let ram = info.physical_memory;
166 let options = objc2_metal::MTLResourceOptions::StorageModeShared;
167 let placeholder = device.newBufferWithLength_options(16, options);
168 let residency = residency::Residency::new(residency::attach(&device, &queue));
169 let pool = Arc::new(Pool::new(
170 info.recommended_working_set,
171 ram,
172 placeholder,
173 residency,
174 ));
175 Some(Device {
176 inner: Arc::new(Inner {
177 device,
178 queue,
179 info,
180 library: OnceLock::new(),
181 nax_library: OnceLock::new(),
182 nax: OnceLock::new(),
183 nax_failed: AtomicBool::new(false),
184 pipelines: Mutex::new(HashMap::new()),
185 pool,
186 in_flight: InFlight::default(),
187 }),
188 })
189 }
190
191 pub fn shared() -> Option<&'static Device> {
194 static SHARED: OnceLock<Option<Device>> = OnceLock::new();
195 SHARED.get_or_init(Device::system_default).as_ref()
196 }
197
198 pub fn memory(&self) -> MemoryStats {
200 self.inner.pool.stats()
201 }
202
203 pub fn set_memory_limit(&self, bytes: usize) -> usize {
214 autoreleasepool(|_| self.inner.pool.set_memory_limit(bytes))
215 }
216
217 pub fn set_cache_limit(&self, bytes: usize) -> usize {
219 autoreleasepool(|_| self.inner.pool.set_cache_limit(bytes))
220 }
221
222 pub fn set_wired_limit(&self, bytes: usize) -> Result<usize> {
229 if bytes as u64 > self.inner.info.recommended_working_set {
230 return Err(GpuError::InvalidArgument(format!(
231 "wired limit {bytes} exceeds the recommended working set ({} bytes)",
232 self.inner.info.recommended_working_set
233 )));
234 }
235 Ok(autoreleasepool(|_| self.inner.pool.set_wired_limit(bytes)))
236 }
237
238 pub fn can_wire(&self) -> bool {
240 self.inner.pool.can_wire()
241 }
242
243 pub fn clear_cache(&self) {
245 autoreleasepool(|_| self.inner.pool.clear_cache());
246 }
247
248 pub fn in_flight(&self) -> usize {
250 self.inner.in_flight.get()
251 }
252
253 pub fn wait_for_capacity(&self, max: usize) {
256 self.inner.in_flight.wait_below(max);
257 }
258
259 pub(crate) fn flights(&self) -> &InFlight {
260 &self.inner.in_flight
261 }
262
263 pub fn reset_peak(&self) {
266 self.inner.pool.reset_peak();
267 }
268
269 pub(crate) fn pool(&self) -> &Arc<Pool> {
270 &self.inner.pool
271 }
272
273 pub fn info(&self) -> &DeviceInfo {
275 &self.inner.info
276 }
277
278 pub fn kernel_names(&self) -> Result<Vec<String>> {
281 Ok(self
283 .library()?
284 .by_name
285 .iter()
286 .map(|(n, _)| n.to_string())
287 .collect())
288 }
289
290 pub fn pipeline(&self, name: &str) -> Result<Pipeline> {
296 self.pipeline_with(name, &[])
297 }
298
299 pub fn pipeline_with(&self, name: &str, constants: &[(usize, bool)]) -> Result<Pipeline> {
302 let constants: Vec<(usize, Constant)> = constants
303 .iter()
304 .map(|&(i, v)| (i, Constant::Bool(v)))
305 .collect();
306 self.pipeline_with_constants(name, &constants)
307 }
308
309 pub fn pipeline_with_constants(
312 &self,
313 name: &str,
314 constants: &[(usize, Constant)],
315 ) -> Result<Pipeline> {
316 thread_local! {
317 static KEY: std::cell::RefCell<String> = const { std::cell::RefCell::new(String::new()) };
319 }
320 let hit = KEY.with(|k| {
321 use std::fmt::Write;
322 let mut k = k.borrow_mut();
323 k.clear();
324 k.push_str(name);
325 for (i, v) in constants {
326 let _ = match v {
327 Constant::Bool(b) => write!(k, "#{i}={}", u8::from(*b)),
328 Constant::Int(n) => write!(k, "#{i}=i{n}"),
329 };
330 }
331 let slot = {
332 let cache = self
333 .inner
334 .pipelines
335 .lock()
336 .unwrap_or_else(PoisonError::into_inner);
337 cache.get(k.as_str()).cloned()
338 };
339 match slot.and_then(|slot| slot.lock().unwrap_or_else(PoisonError::into_inner).clone())
340 {
341 Some(pipeline) => Ok(pipeline),
342 None => Err(k.clone()),
343 }
344 });
345 let key = match hit {
346 Ok(pipeline) => return Ok(pipeline),
347 Err(key) => key,
348 };
349 let nax = name.starts_with("nuvec_nax_");
353 let built = match nax {
354 true => self.nax_library().and_then(|l| l.get(name)),
355 false => self.library().and_then(|l| l.get(name)),
356 }
357 .and_then(|library| self.build_pipeline(name, &key, library, constants));
358 if nax && built.is_err() && self.nax().is_active() {
359 self.inner.nax_failed.store(true, Ordering::Relaxed);
360 }
361 built
362 }
363
364 fn build_pipeline(
365 &self,
366 name: &str,
367 key: &str,
368 library: &Library,
369 constants: &[(usize, Constant)],
370 ) -> Result<Pipeline> {
371 let key = key.to_owned();
372 self.cached(key.clone(), || {
373 let ns_name = NSString::from_str(name);
374 let function = if constants.is_empty() {
375 library
376 .newFunctionWithName(&ns_name)
377 .ok_or_else(|| GpuError::MissingKernel(name.to_owned()))?
378 } else {
379 let values = MTLFunctionConstantValues::new();
380 for &(index, value) in constants {
381 match value {
382 Constant::Bool(b) => unsafe {
385 values.setConstantValue_type_atIndex(
386 NonNull::from(&b).cast(),
387 MTLDataType::Bool,
388 index,
389 );
390 },
391 Constant::Int(n) => unsafe {
393 values.setConstantValue_type_atIndex(
394 NonNull::from(&n).cast(),
395 MTLDataType::Int,
396 index,
397 );
398 },
399 }
400 }
401 library
402 .newFunctionWithName_constantValues_error(&ns_name, &values)
403 .map_err(|e| GpuError::Pipeline {
404 name: key.clone(),
405 message: e.localizedDescription().to_string(),
406 })?
407 };
408 let state = self
409 .inner
410 .device
411 .newComputePipelineStateWithFunction_error(&function)
412 .map_err(|e| GpuError::Pipeline {
413 name: key.clone(),
414 message: e.localizedDescription().to_string(),
415 })?;
416 Ok(Pipeline {
417 state,
418 name: key.as_str().into(),
419 })
420 })
421 }
422
423 pub fn pipeline_from_source(
435 &self,
436 name: &str,
437 source: impl FnOnce() -> String,
438 ) -> Result<Pipeline> {
439 self.cached(format!("src:{name}"), || {
440 let failed = |message: String| GpuError::Pipeline {
441 name: name.to_owned(),
442 message,
443 };
444 let source = inline_sources(&[("generated.metal", &source())]);
445 let options = objc2_metal::MTLCompileOptions::new();
449 options.setMathMode(objc2_metal::MTLMathMode::Safe);
450 options.setLanguageVersion(objc2_metal::MTLLanguageVersion::Version3_2);
451 let library = self
452 .inner
453 .device
454 .newLibraryWithSource_options_error(&NSString::from_str(&source), Some(&options))
455 .map_err(|e| failed(e.localizedDescription().to_string()))?;
456 let function = library
457 .newFunctionWithName(&NSString::from_str(name))
458 .ok_or_else(|| GpuError::MissingKernel(name.to_owned()))?;
459 let state = self
460 .inner
461 .device
462 .newComputePipelineStateWithFunction_error(&function)
463 .map_err(|e| failed(e.localizedDescription().to_string()))?;
464 Ok(Pipeline {
465 state,
466 name: name.into(),
467 })
468 })
469 }
470
471 fn cached(&self, key: String, compile: impl FnOnce() -> Result<Pipeline>) -> Result<Pipeline> {
476 let slot = {
477 let mut cache = self
478 .inner
479 .pipelines
480 .lock()
481 .unwrap_or_else(PoisonError::into_inner);
482 cache.entry(key).or_default().clone()
483 };
484 let mut slot = slot.lock().unwrap_or_else(PoisonError::into_inner);
485 if let Some(p) = &*slot {
486 return Ok(p.clone());
487 }
488 let pipeline = autoreleasepool(|_| compile())?;
489 *slot = Some(pipeline.clone());
490 Ok(pipeline)
491 }
492
493 fn library(&self) -> Result<&Libraries> {
495 self.inner
496 .library
497 .get_or_init(|| autoreleasepool(|_| load_library(&self.inner.device)))
498 .as_ref()
499 .map_err(Clone::clone)
500 }
501
502 fn nax_library(&self) -> Result<&Libraries> {
503 self.inner
504 .nax_library
505 .get_or_init(|| autoreleasepool(|_| load_nax_library(&self.inner.device)))
506 .as_ref()
507 .map_err(Clone::clone)
508 }
509
510 pub fn nax(&self) -> NaxStatus {
517 if self.inner.nax_failed.load(Ordering::Relaxed) {
518 return NaxStatus::Off(NaxReason::PipelineError);
519 }
520 *self.inner.nax.get_or_init(|| {
521 let v = objc2_foundation::NSProcessInfo::processInfo().operatingSystemVersion();
522 let os = (v.majorVersion as u32, v.minorVersion as u32);
523 let info = &self.inner.info;
524 nax_status(
525 info.generation,
526 info.class,
527 os,
528 NaxOverride::from_env(),
529 nuvec_kernels::NAX_MODE != NaxMode::Absent,
530 )
531 })
532 }
533
534 pub(crate) fn raw(&self) -> &ProtocolObject<dyn MTLDevice> {
535 &self.inner.device
536 }
537
538 pub(crate) fn queue(&self) -> &ProtocolObject<dyn MTLCommandQueue> {
539 &self.inner.queue
540 }
541}
542
543impl std::fmt::Debug for Device {
544 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
545 f.debug_struct("Device")
546 .field("info", &self.inner.info)
547 .finish_non_exhaustive()
548 }
549}
550
551fn load_library(device: &ProtocolObject<dyn MTLDevice>) -> Result<Libraries> {
557 let error = |e: Retained<objc2_foundation::NSError>| {
558 GpuError::LibraryLoad(e.localizedDescription().to_string())
559 };
560 match nuvec_kernels::MODE {
561 MetallibMode::Precompiled => {
562 let bytes = nuvec_kernels::metallib().ok_or(GpuError::NoKernels)?;
563 let library = device
564 .newLibraryWithData_error(&dispatch2::DispatchData::from_static_bytes(bytes))
565 .map_err(error)?;
566 Ok(Libraries::new(vec![library]))
567 }
568 MetallibMode::RuntimeSource => {
569 let files: Vec<(&str, &str)> = nuvec_kernels::sources()
570 .iter()
571 .copied()
572 .filter(|(p, _)| p.ends_with(".metal"))
573 .collect();
574 let compile = |&(path, text): &(&str, &str)| -> Result<Library> {
575 let mut source = String::new();
576 if !nuvec_kernels::contracted(path) {
577 source.push_str("#pragma clang fp contract(off)\n");
579 }
580 source.push_str(&inline_sources(&[(path, text)]));
581 let options = objc2_metal::MTLCompileOptions::new();
582 options.setMathMode(objc2_metal::MTLMathMode::Safe);
583 options.setMathFloatingPointFunctions(
586 objc2_metal::MTLMathFloatingPointFunctions::Precise,
587 );
588 options.setLanguageVersion(objc2_metal::MTLLanguageVersion::Version3_2);
589 device
590 .newLibraryWithSource_options_error(
591 &NSString::from_str(&source),
592 Some(&options),
593 )
594 .map_err(|e| {
595 GpuError::LibraryLoad(format!("{path}: {}", e.localizedDescription()))
596 })
597 };
598 let libraries = std::thread::scope(|scope| {
599 let handles: Vec<_> = files
600 .iter()
601 .map(|file| scope.spawn(move || compile(file)))
602 .collect();
603 handles
604 .into_iter()
605 .map(|h| h.join().expect("shader compilation does not panic"))
606 .collect::<Result<Vec<_>>>()
607 })?;
608 Ok(Libraries::new(libraries))
609 }
610 _ => Err(GpuError::NoKernels),
611 }
612}
613
614fn load_nax_library(device: &ProtocolObject<dyn MTLDevice>) -> Result<Libraries> {
617 match nuvec_kernels::NAX_MODE {
618 NaxMode::Precompiled => {
619 let bytes = nuvec_kernels::nax_metallib().ok_or(GpuError::NoKernels)?;
620 let library = device
621 .newLibraryWithData_error(&dispatch2::DispatchData::from_static_bytes(bytes))
622 .map_err(|e| GpuError::LibraryLoad(e.localizedDescription().to_string()))?;
623 Ok(Libraries::new(vec![library]))
624 }
625 NaxMode::RuntimeSource => {
626 let libraries = nuvec_kernels::nax_sources()
627 .iter()
628 .filter(|(p, _)| p.ends_with(".metal"))
629 .map(|(path, text)| {
630 let options = objc2_metal::MTLCompileOptions::new();
631 options.setMathMode(objc2_metal::MTLMathMode::Safe);
632 options.setMathFloatingPointFunctions(
633 objc2_metal::MTLMathFloatingPointFunctions::Precise,
634 );
635 options.setLanguageVersion(objc2_metal::MTLLanguageVersion::Version4_0);
636 device
637 .newLibraryWithSource_options_error(
638 &NSString::from_str(text),
639 Some(&options),
640 )
641 .map_err(|e| {
642 GpuError::LibraryLoad(format!("{path}: {}", e.localizedDescription()))
643 })
644 })
645 .collect::<Result<Vec<_>>>()?;
646 Ok(Libraries::new(libraries))
647 }
648 _ => Err(GpuError::NoKernels),
649 }
650}
651
652fn inline_sources(sources: &[(&str, &str)]) -> String {
656 let headers: HashMap<&str, &str> = nuvec_kernels::sources()
657 .iter()
658 .chain(sources)
659 .copied()
660 .chain([("common.h", nuvec_kernels::COMMON_H)])
661 .filter(|(p, _)| p.ends_with(".h"))
662 .collect();
663 let mut included = HashSet::new();
664 let mut out = String::new();
665 for (_, source) in sources.iter().filter(|(p, _)| p.ends_with(".metal")) {
666 inline_into(source, &headers, &mut included, &mut out);
667 }
668 out
669}
670
671fn inline_into<'a>(
672 source: &str,
673 headers: &HashMap<&'a str, &'a str>,
674 included: &mut HashSet<&'a str>,
675 out: &mut String,
676) {
677 for line in source.lines() {
678 let header = line
679 .trim()
680 .strip_prefix("#include \"")
681 .and_then(|r| r.strip_suffix('"'));
682 match header.and_then(|h| headers.get_key_value(h)) {
683 Some((&h, &text)) => {
684 if included.insert(h) {
685 inline_into(text, headers, included, out);
686 out.push('\n');
687 }
688 }
689 None => {
690 out.push_str(line);
691 out.push('\n');
692 }
693 }
694 }
695}
696
697#[derive(Clone)]
699pub struct Pipeline {
700 state: Retained<ProtocolObject<dyn MTLComputePipelineState>>,
701 name: Arc<str>,
702}
703
704impl Pipeline {
705 pub fn name(&self) -> &str {
707 &self.name
708 }
709
710 pub fn max_threads_per_threadgroup(&self) -> usize {
712 self.state.maxTotalThreadsPerThreadgroup()
713 }
714
715 pub fn execution_width(&self) -> usize {
717 self.state.threadExecutionWidth()
718 }
719
720 pub fn same_as(&self, other: &Pipeline) -> bool {
722 Retained::as_ptr(&self.state) == Retained::as_ptr(&other.state)
723 }
724
725 pub(crate) fn raw(&self) -> &ProtocolObject<dyn MTLComputePipelineState> {
726 &self.state
727 }
728}
729
730impl std::fmt::Debug for Pipeline {
731 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
732 f.debug_struct("Pipeline")
733 .field("name", &self.name)
734 .finish_non_exhaustive()
735 }
736}
737
738#[cfg(test)]
739mod tests {
740 use super::inline_sources;
741
742 #[test]
743 fn inlines_each_header_once() {
744 let sources = [
745 ("a.metal", "#include \"h.h\"\nkernel A;"),
746 ("b.metal", " #include \"h.h\"\nkernel B;"),
747 ("h.h", "#pragma once\ninline int f();"),
748 ];
749 let out = inline_sources(&sources);
750 assert_eq!(out.matches("inline int f();").count(), 1);
751 assert!(out.contains("kernel A;") && out.contains("kernel B;"));
752 assert!(!out.contains("#include \"h.h\""));
753 }
754
755 #[test]
756 fn inlines_headers_included_by_headers() {
757 let sources = [
758 (
759 "a.metal",
760 "#include \"outer.h\"\n#include \"inner.h\"\nkernel A;",
761 ),
762 ("outer.h", "#include \"inner.h\"\ninline int g();"),
763 ("inner.h", "inline int f();"),
764 ];
765 let out = inline_sources(&sources);
766 assert_eq!(out.matches("inline int f();").count(), 1);
767 assert!(out.find("inline int f();") < out.find("inline int g();"));
768 assert!(!out.contains("#include"));
769 }
770}