1mod allocator;
4mod buffer;
5mod command;
6pub mod profile;
7mod residency;
8
9use std::collections::{HashMap, HashSet};
10use std::ptr::NonNull;
11use std::sync::{Arc, Condvar, Mutex, OnceLock, PoisonError};
12
13use allocator::Pool;
14pub use buffer::Buffer;
15pub use command::{Arg, CommandBuffer, Pending, Submitted, Timeline};
16use nuvec_kernels::MetallibMode;
17use objc2::rc::{Retained, autoreleasepool};
18use objc2::runtime::ProtocolObject;
19use objc2_foundation::NSString;
20use objc2_metal::{
21 MTLCommandQueue, MTLComputePipelineState, MTLCreateSystemDefaultDevice, MTLDataType, MTLDevice,
22 MTLFunctionConstantValues, MTLLibrary,
23};
24
25use crate::info::parse_architecture;
26use crate::{DeviceInfo, GpuError, MemoryStats, Result};
27
28type Library = Retained<ProtocolObject<dyn MTLLibrary>>;
29
30struct Libraries {
33 libraries: Vec<Library>,
34 by_name: HashMap<String, usize>,
35}
36
37impl Libraries {
38 fn new(libraries: Vec<Library>) -> Self {
39 let mut by_name = HashMap::new();
40 for (i, library) in libraries.iter().enumerate() {
41 for name in library.functionNames().iter() {
42 by_name.entry(name.to_string()).or_insert(i);
43 }
44 }
45 Libraries { libraries, by_name }
46 }
47
48 fn get(&self, name: &str) -> Result<&Library> {
50 self.by_name
51 .get(name)
52 .map(|&i| &self.libraries[i])
53 .ok_or_else(|| GpuError::MissingKernel(name.to_owned()))
54 }
55}
56
57type Slot = Arc<Mutex<Option<Pipeline>>>;
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum Constant {
63 Bool(bool),
65 Int(i32),
67}
68
69#[derive(Clone)]
71pub struct Device {
72 inner: Arc<Inner>,
73}
74
75struct Inner {
76 device: Retained<ProtocolObject<dyn MTLDevice>>,
77 queue: Retained<ProtocolObject<dyn MTLCommandQueue>>,
78 info: DeviceInfo,
79 library: OnceLock<Result<Libraries>>,
80 pipelines: Mutex<HashMap<String, Slot>>,
85 pool: Arc<Pool>,
86 in_flight: InFlight,
87}
88
89impl Drop for Inner {
90 fn drop(&mut self) {
93 autoreleasepool(|_| self.pool.clear_cache());
94 }
95}
96
97#[derive(Default)]
99pub(crate) struct InFlight {
100 count: Mutex<usize>,
101 done: Condvar,
102}
103
104impl InFlight {
105 pub(crate) fn begin(&self) {
106 *self.count.lock().unwrap_or_else(PoisonError::into_inner) += 1;
107 }
108
109 pub(crate) fn end(&self) {
110 let mut count = self.count.lock().unwrap_or_else(PoisonError::into_inner);
111 *count = count.saturating_sub(1);
112 self.done.notify_all();
113 }
114
115 fn get(&self) -> usize {
116 *self.count.lock().unwrap_or_else(PoisonError::into_inner)
117 }
118
119 fn wait_below(&self, max: usize) {
120 let mut count = self.count.lock().unwrap_or_else(PoisonError::into_inner);
121 while *count >= max.max(1) {
122 count = self
123 .done
124 .wait(count)
125 .unwrap_or_else(PoisonError::into_inner);
126 }
127 }
128}
129
130impl Device {
131 pub fn system_default() -> Option<Device> {
134 autoreleasepool(|_| Self::create())
135 }
136
137 fn create() -> Option<Device> {
138 let device = MTLCreateSystemDefaultDevice()?;
139 let queue = device.newCommandQueue()?;
140 let architecture = device.architecture().name().to_string();
141 let (generation, class) = parse_architecture(&architecture);
142 let info = DeviceInfo {
143 name: device.name().to_string(),
144 architecture,
145 generation,
146 class,
147 recommended_working_set: device.recommendedMaxWorkingSetSize(),
148 max_threads_per_threadgroup: device.maxThreadsPerThreadgroup().width,
149 };
150 let ram = objc2_foundation::NSProcessInfo::processInfo().physicalMemory();
151 let options = objc2_metal::MTLResourceOptions::StorageModeShared;
152 let placeholder = device.newBufferWithLength_options(16, options);
153 let residency = residency::Residency::new(residency::attach(&device, &queue));
154 let pool = Arc::new(Pool::new(
155 info.recommended_working_set,
156 ram,
157 placeholder,
158 residency,
159 ));
160 Some(Device {
161 inner: Arc::new(Inner {
162 device,
163 queue,
164 info,
165 library: OnceLock::new(),
166 pipelines: Mutex::new(HashMap::new()),
167 pool,
168 in_flight: InFlight::default(),
169 }),
170 })
171 }
172
173 pub fn shared() -> Option<&'static Device> {
176 static SHARED: OnceLock<Option<Device>> = OnceLock::new();
177 SHARED.get_or_init(Device::system_default).as_ref()
178 }
179
180 pub fn memory(&self) -> MemoryStats {
182 self.inner.pool.stats()
183 }
184
185 pub fn set_memory_limit(&self, bytes: usize) -> usize {
196 autoreleasepool(|_| self.inner.pool.set_memory_limit(bytes))
197 }
198
199 pub fn set_cache_limit(&self, bytes: usize) -> usize {
201 autoreleasepool(|_| self.inner.pool.set_cache_limit(bytes))
202 }
203
204 pub fn set_wired_limit(&self, bytes: usize) -> Result<usize> {
211 if bytes as u64 > self.inner.info.recommended_working_set {
212 return Err(GpuError::InvalidArgument(format!(
213 "wired limit {bytes} exceeds the recommended working set ({} bytes)",
214 self.inner.info.recommended_working_set
215 )));
216 }
217 Ok(autoreleasepool(|_| self.inner.pool.set_wired_limit(bytes)))
218 }
219
220 pub fn can_wire(&self) -> bool {
222 self.inner.pool.can_wire()
223 }
224
225 pub fn clear_cache(&self) {
227 autoreleasepool(|_| self.inner.pool.clear_cache());
228 }
229
230 pub fn in_flight(&self) -> usize {
232 self.inner.in_flight.get()
233 }
234
235 pub fn wait_for_capacity(&self, max: usize) {
238 self.inner.in_flight.wait_below(max);
239 }
240
241 pub(crate) fn flights(&self) -> &InFlight {
242 &self.inner.in_flight
243 }
244
245 pub fn reset_peak(&self) {
248 self.inner.pool.reset_peak();
249 }
250
251 pub(crate) fn pool(&self) -> &Arc<Pool> {
252 &self.inner.pool
253 }
254
255 pub fn info(&self) -> &DeviceInfo {
257 &self.inner.info
258 }
259
260 pub fn pipeline(&self, name: &str) -> Result<Pipeline> {
266 self.pipeline_with(name, &[])
267 }
268
269 pub fn pipeline_with(&self, name: &str, constants: &[(usize, bool)]) -> Result<Pipeline> {
272 let constants: Vec<(usize, Constant)> = constants
273 .iter()
274 .map(|&(i, v)| (i, Constant::Bool(v)))
275 .collect();
276 self.pipeline_with_constants(name, &constants)
277 }
278
279 pub fn pipeline_with_constants(
282 &self,
283 name: &str,
284 constants: &[(usize, Constant)],
285 ) -> Result<Pipeline> {
286 let key = constants.iter().fold(name.to_owned(), |mut k, (i, v)| {
287 match v {
288 Constant::Bool(b) => k.push_str(&format!("#{i}={}", u8::from(*b))),
289 Constant::Int(n) => k.push_str(&format!("#{i}=i{n}")),
290 }
291 k
292 });
293 let library = self.library()?.get(name)?;
295 self.cached(key.clone(), || {
296 let ns_name = NSString::from_str(name);
297 let function = if constants.is_empty() {
298 library
299 .newFunctionWithName(&ns_name)
300 .ok_or_else(|| GpuError::MissingKernel(name.to_owned()))?
301 } else {
302 let values = MTLFunctionConstantValues::new();
303 for &(index, value) in constants {
304 match value {
305 Constant::Bool(b) => unsafe {
308 values.setConstantValue_type_atIndex(
309 NonNull::from(&b).cast(),
310 MTLDataType::Bool,
311 index,
312 );
313 },
314 Constant::Int(n) => unsafe {
316 values.setConstantValue_type_atIndex(
317 NonNull::from(&n).cast(),
318 MTLDataType::Int,
319 index,
320 );
321 },
322 }
323 }
324 library
325 .newFunctionWithName_constantValues_error(&ns_name, &values)
326 .map_err(|e| GpuError::Pipeline {
327 name: key.clone(),
328 message: e.localizedDescription().to_string(),
329 })?
330 };
331 let state = self
332 .inner
333 .device
334 .newComputePipelineStateWithFunction_error(&function)
335 .map_err(|e| GpuError::Pipeline {
336 name: key.clone(),
337 message: e.localizedDescription().to_string(),
338 })?;
339 Ok(Pipeline {
340 state,
341 name: key.as_str().into(),
342 })
343 })
344 }
345
346 pub fn pipeline_from_source(
357 &self,
358 name: &str,
359 source: impl FnOnce() -> String,
360 ) -> Result<Pipeline> {
361 self.cached(format!("src:{name}"), || {
362 let failed = |message: String| GpuError::Pipeline {
363 name: name.to_owned(),
364 message,
365 };
366 let source = inline_sources(&[("generated.metal", &source())]);
367 let options = objc2_metal::MTLCompileOptions::new();
368 options.setMathMode(objc2_metal::MTLMathMode::Safe);
369 options.setLanguageVersion(objc2_metal::MTLLanguageVersion::Version3_2);
370 let library = self
371 .inner
372 .device
373 .newLibraryWithSource_options_error(&NSString::from_str(&source), Some(&options))
374 .map_err(|e| failed(e.localizedDescription().to_string()))?;
375 let function = library
376 .newFunctionWithName(&NSString::from_str(name))
377 .ok_or_else(|| GpuError::MissingKernel(name.to_owned()))?;
378 let state = self
379 .inner
380 .device
381 .newComputePipelineStateWithFunction_error(&function)
382 .map_err(|e| failed(e.localizedDescription().to_string()))?;
383 Ok(Pipeline {
384 state,
385 name: name.into(),
386 })
387 })
388 }
389
390 fn cached(&self, key: String, compile: impl FnOnce() -> Result<Pipeline>) -> Result<Pipeline> {
395 let slot = {
396 let mut cache = self
397 .inner
398 .pipelines
399 .lock()
400 .unwrap_or_else(PoisonError::into_inner);
401 cache.entry(key).or_default().clone()
402 };
403 let mut slot = slot.lock().unwrap_or_else(PoisonError::into_inner);
404 if let Some(p) = &*slot {
405 return Ok(p.clone());
406 }
407 let pipeline = autoreleasepool(|_| compile())?;
408 *slot = Some(pipeline.clone());
409 Ok(pipeline)
410 }
411
412 fn library(&self) -> Result<&Libraries> {
414 self.inner
415 .library
416 .get_or_init(|| autoreleasepool(|_| load_library(&self.inner.device)))
417 .as_ref()
418 .map_err(Clone::clone)
419 }
420
421 pub(crate) fn raw(&self) -> &ProtocolObject<dyn MTLDevice> {
422 &self.inner.device
423 }
424
425 pub(crate) fn queue(&self) -> &ProtocolObject<dyn MTLCommandQueue> {
426 &self.inner.queue
427 }
428}
429
430impl std::fmt::Debug for Device {
431 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
432 f.debug_struct("Device")
433 .field("info", &self.inner.info)
434 .finish_non_exhaustive()
435 }
436}
437
438fn load_library(device: &ProtocolObject<dyn MTLDevice>) -> Result<Libraries> {
444 let error = |e: Retained<objc2_foundation::NSError>| {
445 GpuError::LibraryLoad(e.localizedDescription().to_string())
446 };
447 match nuvec_kernels::MODE {
448 MetallibMode::Precompiled => {
449 let bytes = nuvec_kernels::metallib().ok_or(GpuError::NoKernels)?;
450 let library = device
451 .newLibraryWithData_error(&dispatch2::DispatchData::from_static_bytes(bytes))
452 .map_err(error)?;
453 Ok(Libraries::new(vec![library]))
454 }
455 MetallibMode::RuntimeSource => {
456 let files: Vec<(&str, &str)> = nuvec_kernels::sources()
457 .iter()
458 .copied()
459 .filter(|(p, _)| p.ends_with(".metal"))
460 .collect();
461 let compile = |&(path, text): &(&str, &str)| -> Result<Library> {
462 let mut source = String::new();
463 if !nuvec_kernels::contracted(path) {
464 source.push_str("#pragma clang fp contract(off)\n");
466 }
467 source.push_str(&inline_sources(&[(path, text)]));
468 let options = objc2_metal::MTLCompileOptions::new();
469 options.setMathMode(objc2_metal::MTLMathMode::Safe);
470 options.setLanguageVersion(objc2_metal::MTLLanguageVersion::Version3_2);
471 device
472 .newLibraryWithSource_options_error(
473 &NSString::from_str(&source),
474 Some(&options),
475 )
476 .map_err(|e| {
477 GpuError::LibraryLoad(format!("{path}: {}", e.localizedDescription()))
478 })
479 };
480 let libraries = std::thread::scope(|scope| {
481 let handles: Vec<_> = files
482 .iter()
483 .map(|file| scope.spawn(move || compile(file)))
484 .collect();
485 handles
486 .into_iter()
487 .map(|h| h.join().expect("shader compilation does not panic"))
488 .collect::<Result<Vec<_>>>()
489 })?;
490 Ok(Libraries::new(libraries))
491 }
492 _ => Err(GpuError::NoKernels),
493 }
494}
495
496fn inline_sources(sources: &[(&str, &str)]) -> String {
499 let headers: HashMap<&str, &str> = nuvec_kernels::sources()
500 .iter()
501 .chain(sources)
502 .copied()
503 .chain([("common.h", nuvec_kernels::COMMON_H)])
504 .filter(|(p, _)| p.ends_with(".h"))
505 .collect();
506 let mut included = HashSet::new();
507 let mut out = String::new();
508 for (_, source) in sources.iter().filter(|(p, _)| p.ends_with(".metal")) {
509 for line in source.lines() {
510 let header = line
511 .trim()
512 .strip_prefix("#include \"")
513 .and_then(|r| r.strip_suffix('"'));
514 match header.and_then(|h| headers.get(h).map(|text| (h, text))) {
515 Some((h, text)) => {
516 if included.insert(h) {
517 out.push_str(text);
518 out.push('\n');
519 }
520 }
521 None => {
522 out.push_str(line);
523 out.push('\n');
524 }
525 }
526 }
527 }
528 out
529}
530
531#[derive(Clone)]
533pub struct Pipeline {
534 state: Retained<ProtocolObject<dyn MTLComputePipelineState>>,
535 name: Arc<str>,
536}
537
538impl Pipeline {
539 pub fn name(&self) -> &str {
541 &self.name
542 }
543
544 pub fn max_threads_per_threadgroup(&self) -> usize {
546 self.state.maxTotalThreadsPerThreadgroup()
547 }
548
549 pub fn execution_width(&self) -> usize {
551 self.state.threadExecutionWidth()
552 }
553
554 pub fn same_as(&self, other: &Pipeline) -> bool {
556 Retained::as_ptr(&self.state) == Retained::as_ptr(&other.state)
557 }
558
559 pub(crate) fn raw(&self) -> &ProtocolObject<dyn MTLComputePipelineState> {
560 &self.state
561 }
562}
563
564impl std::fmt::Debug for Pipeline {
565 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
566 f.debug_struct("Pipeline")
567 .field("name", &self.name)
568 .finish_non_exhaustive()
569 }
570}
571
572#[cfg(test)]
573mod tests {
574 use super::inline_sources;
575
576 #[test]
577 fn inlines_each_header_once() {
578 let sources = [
579 ("a.metal", "#include \"h.h\"\nkernel A;"),
580 ("b.metal", " #include \"h.h\"\nkernel B;"),
581 ("h.h", "#pragma once\ninline int f();"),
582 ];
583 let out = inline_sources(&sources);
584 assert_eq!(out.matches("inline int f();").count(), 1);
585 assert!(out.contains("kernel A;") && out.contains("kernel B;"));
586 assert!(!out.contains("#include \"h.h\""));
587 }
588}