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;
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
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum Constant {
60 Bool(bool),
62 Int(i32),
64}
65
66#[derive(Clone)]
68pub struct Device {
69 inner: Arc<Inner>,
70}
71
72struct Inner {
73 device: Retained<ProtocolObject<dyn MTLDevice>>,
74 queue: Retained<ProtocolObject<dyn MTLCommandQueue>>,
75 info: DeviceInfo,
76 library: OnceLock<Result<Libraries>>,
77 pipelines: Mutex<HashMap<String, Pipeline>>,
78 pool: Arc<Pool>,
79 in_flight: InFlight,
80}
81
82#[derive(Default)]
84pub(crate) struct InFlight {
85 count: Mutex<usize>,
86 done: Condvar,
87}
88
89impl InFlight {
90 pub(crate) fn begin(&self) {
91 *self.count.lock().unwrap_or_else(PoisonError::into_inner) += 1;
92 }
93
94 pub(crate) fn end(&self) {
95 let mut count = self.count.lock().unwrap_or_else(PoisonError::into_inner);
96 *count = count.saturating_sub(1);
97 self.done.notify_all();
98 }
99
100 fn get(&self) -> usize {
101 *self.count.lock().unwrap_or_else(PoisonError::into_inner)
102 }
103
104 fn wait_below(&self, max: usize) {
105 let mut count = self.count.lock().unwrap_or_else(PoisonError::into_inner);
106 while *count >= max.max(1) {
107 count = self
108 .done
109 .wait(count)
110 .unwrap_or_else(PoisonError::into_inner);
111 }
112 }
113}
114
115impl Device {
116 pub fn system_default() -> Option<Device> {
119 let device = MTLCreateSystemDefaultDevice()?;
120 let queue = device.newCommandQueue()?;
121 let architecture = device.architecture().name().to_string();
122 let (generation, class) = parse_architecture(&architecture);
123 let info = DeviceInfo {
124 name: device.name().to_string(),
125 architecture,
126 generation,
127 class,
128 recommended_working_set: device.recommendedMaxWorkingSetSize(),
129 max_threads_per_threadgroup: device.maxThreadsPerThreadgroup().width,
130 };
131 let ram = objc2_foundation::NSProcessInfo::processInfo().physicalMemory();
132 let options = objc2_metal::MTLResourceOptions::StorageModeShared;
133 let placeholder = device.newBufferWithLength_options(16, options);
134 let residency = residency::Residency::new(residency::attach(&device, &queue));
135 let pool = Arc::new(Pool::new(
136 info.recommended_working_set,
137 ram,
138 placeholder,
139 residency,
140 ));
141 Some(Device {
142 inner: Arc::new(Inner {
143 device,
144 queue,
145 info,
146 library: OnceLock::new(),
147 pipelines: Mutex::new(HashMap::new()),
148 pool,
149 in_flight: InFlight::default(),
150 }),
151 })
152 }
153
154 pub fn shared() -> Option<&'static Device> {
157 static SHARED: OnceLock<Option<Device>> = OnceLock::new();
158 SHARED.get_or_init(Device::system_default).as_ref()
159 }
160
161 pub fn memory(&self) -> MemoryStats {
163 self.inner.pool.stats()
164 }
165
166 pub fn set_memory_limit(&self, bytes: usize) -> usize {
168 self.inner.pool.set_memory_limit(bytes)
169 }
170
171 pub fn set_cache_limit(&self, bytes: usize) -> usize {
173 self.inner.pool.set_cache_limit(bytes)
174 }
175
176 pub fn set_wired_limit(&self, bytes: usize) -> Result<usize> {
184 if bytes as u64 > self.inner.info.recommended_working_set {
185 return Err(GpuError::InvalidArgument(format!(
186 "wired limit {bytes} exceeds the recommended working set ({} bytes)",
187 self.inner.info.recommended_working_set
188 )));
189 }
190 Ok(self.inner.pool.set_wired_limit(bytes))
191 }
192
193 pub fn can_wire(&self) -> bool {
196 self.inner.pool.can_wire()
197 }
198
199 pub fn clear_cache(&self) {
201 self.inner.pool.clear_cache();
202 }
203
204 pub fn in_flight(&self) -> usize {
206 self.inner.in_flight.get()
207 }
208
209 pub fn wait_for_capacity(&self, max: usize) {
212 self.inner.in_flight.wait_below(max);
213 }
214
215 pub(crate) fn flights(&self) -> &InFlight {
216 &self.inner.in_flight
217 }
218
219 pub fn reset_peak(&self) {
221 self.inner.pool.reset_peak();
222 }
223
224 pub(crate) fn pool(&self) -> &Arc<Pool> {
225 &self.inner.pool
226 }
227
228 pub fn info(&self) -> &DeviceInfo {
230 &self.inner.info
231 }
232
233 pub fn pipeline(&self, name: &str) -> Result<Pipeline> {
236 self.pipeline_with(name, &[])
237 }
238
239 pub fn pipeline_with(&self, name: &str, constants: &[(usize, bool)]) -> Result<Pipeline> {
242 let constants: Vec<(usize, Constant)> = constants
243 .iter()
244 .map(|&(i, v)| (i, Constant::Bool(v)))
245 .collect();
246 self.pipeline_with_constants(name, &constants)
247 }
248
249 pub fn pipeline_with_constants(
252 &self,
253 name: &str,
254 constants: &[(usize, Constant)],
255 ) -> Result<Pipeline> {
256 let key = constants.iter().fold(name.to_owned(), |mut k, (i, v)| {
257 match v {
258 Constant::Bool(b) => k.push_str(&format!("#{i}={}", u8::from(*b))),
259 Constant::Int(n) => k.push_str(&format!("#{i}=i{n}")),
260 }
261 k
262 });
263 let mut cache = self
264 .inner
265 .pipelines
266 .lock()
267 .unwrap_or_else(PoisonError::into_inner);
268 if let Some(p) = cache.get(&key) {
269 return Ok(p.clone());
270 }
271 let library = self.library()?.get(name)?;
272 let ns_name = NSString::from_str(name);
273 let function = if constants.is_empty() {
274 library
275 .newFunctionWithName(&ns_name)
276 .ok_or_else(|| GpuError::MissingKernel(name.to_owned()))?
277 } else {
278 let values = MTLFunctionConstantValues::new();
279 for &(index, value) in constants {
280 match value {
281 Constant::Bool(b) => unsafe {
284 values.setConstantValue_type_atIndex(
285 NonNull::from(&b).cast(),
286 MTLDataType::Bool,
287 index,
288 );
289 },
290 Constant::Int(n) => unsafe {
292 values.setConstantValue_type_atIndex(
293 NonNull::from(&n).cast(),
294 MTLDataType::Int,
295 index,
296 );
297 },
298 }
299 }
300 library
301 .newFunctionWithName_constantValues_error(&ns_name, &values)
302 .map_err(|e| GpuError::Pipeline {
303 name: key.clone(),
304 message: e.localizedDescription().to_string(),
305 })?
306 };
307 let state = self
308 .inner
309 .device
310 .newComputePipelineStateWithFunction_error(&function)
311 .map_err(|e| GpuError::Pipeline {
312 name: key.clone(),
313 message: e.localizedDescription().to_string(),
314 })?;
315 let pipeline = Pipeline {
316 state,
317 name: key.as_str().into(),
318 };
319 cache.insert(key, pipeline.clone());
320 Ok(pipeline)
321 }
322
323 pub fn pipeline_from_source(
328 &self,
329 name: &str,
330 source: impl FnOnce() -> String,
331 ) -> Result<Pipeline> {
332 let mut cache = self
333 .inner
334 .pipelines
335 .lock()
336 .unwrap_or_else(PoisonError::into_inner);
337 if let Some(p) = cache.get(name) {
338 return Ok(p.clone());
339 }
340 let failed = |message: String| GpuError::Pipeline {
341 name: name.to_owned(),
342 message,
343 };
344 let source = inline_sources(&[("generated.metal", &source())]);
345 let options = objc2_metal::MTLCompileOptions::new();
346 options.setMathMode(objc2_metal::MTLMathMode::Safe);
347 let library = self
348 .inner
349 .device
350 .newLibraryWithSource_options_error(&NSString::from_str(&source), Some(&options))
351 .map_err(|e| failed(e.localizedDescription().to_string()))?;
352 let function = library
353 .newFunctionWithName(&NSString::from_str(name))
354 .ok_or_else(|| GpuError::MissingKernel(name.to_owned()))?;
355 let state = self
356 .inner
357 .device
358 .newComputePipelineStateWithFunction_error(&function)
359 .map_err(|e| failed(e.localizedDescription().to_string()))?;
360 let pipeline = Pipeline {
361 state,
362 name: name.into(),
363 };
364 cache.insert(name.to_owned(), pipeline.clone());
365 Ok(pipeline)
366 }
367
368 fn library(&self) -> Result<&Libraries> {
370 self.inner
371 .library
372 .get_or_init(|| load_library(&self.inner.device))
373 .as_ref()
374 .map_err(Clone::clone)
375 }
376
377 pub(crate) fn raw(&self) -> &ProtocolObject<dyn MTLDevice> {
378 &self.inner.device
379 }
380
381 pub(crate) fn queue(&self) -> &ProtocolObject<dyn MTLCommandQueue> {
382 &self.inner.queue
383 }
384}
385
386impl std::fmt::Debug for Device {
387 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
388 f.debug_struct("Device")
389 .field("info", &self.inner.info)
390 .finish_non_exhaustive()
391 }
392}
393
394fn load_library(device: &ProtocolObject<dyn MTLDevice>) -> Result<Libraries> {
400 let error = |e: Retained<objc2_foundation::NSError>| {
401 GpuError::LibraryLoad(e.localizedDescription().to_string())
402 };
403 match nuvec_kernels::MODE {
404 MetallibMode::Precompiled => {
405 let bytes = nuvec_kernels::metallib().ok_or(GpuError::NoKernels)?;
406 let library = device
407 .newLibraryWithData_error(&dispatch2::DispatchData::from_static_bytes(bytes))
408 .map_err(error)?;
409 Ok(Libraries::new(vec![library]))
410 }
411 MetallibMode::RuntimeSource => {
412 let files: Vec<(&str, &str)> = nuvec_kernels::sources()
413 .iter()
414 .copied()
415 .filter(|(p, _)| p.ends_with(".metal"))
416 .collect();
417 let compile = |&(path, text): &(&str, &str)| -> Result<Library> {
418 let mut source = String::new();
419 if !nuvec_kernels::contracted(path) {
420 source.push_str("#pragma clang fp contract(off)\n");
422 }
423 source.push_str(&inline_sources(&[(path, text)]));
424 let options = objc2_metal::MTLCompileOptions::new();
425 options.setMathMode(objc2_metal::MTLMathMode::Safe);
426 options.setLanguageVersion(objc2_metal::MTLLanguageVersion::Version3_2);
427 device
428 .newLibraryWithSource_options_error(
429 &NSString::from_str(&source),
430 Some(&options),
431 )
432 .map_err(|e| {
433 GpuError::LibraryLoad(format!("{path}: {}", e.localizedDescription()))
434 })
435 };
436 let libraries = std::thread::scope(|scope| {
437 let handles: Vec<_> = files
438 .iter()
439 .map(|file| scope.spawn(move || compile(file)))
440 .collect();
441 handles
442 .into_iter()
443 .map(|h| h.join().expect("shader compilation does not panic"))
444 .collect::<Result<Vec<_>>>()
445 })?;
446 Ok(Libraries::new(libraries))
447 }
448 _ => Err(GpuError::NoKernels),
449 }
450}
451
452fn inline_sources(sources: &[(&str, &str)]) -> String {
455 let headers: HashMap<&str, &str> = nuvec_kernels::sources()
456 .iter()
457 .chain(sources)
458 .copied()
459 .chain([("common.h", nuvec_kernels::COMMON_H)])
460 .filter(|(p, _)| p.ends_with(".h"))
461 .collect();
462 let mut included = HashSet::new();
463 let mut out = String::new();
464 for (_, source) in sources.iter().filter(|(p, _)| p.ends_with(".metal")) {
465 for line in source.lines() {
466 let header = line
467 .trim()
468 .strip_prefix("#include \"")
469 .and_then(|r| r.strip_suffix('"'));
470 match header.and_then(|h| headers.get(h).map(|text| (h, text))) {
471 Some((h, text)) => {
472 if included.insert(h) {
473 out.push_str(text);
474 out.push('\n');
475 }
476 }
477 None => {
478 out.push_str(line);
479 out.push('\n');
480 }
481 }
482 }
483 }
484 out
485}
486
487#[derive(Clone)]
489pub struct Pipeline {
490 state: Retained<ProtocolObject<dyn MTLComputePipelineState>>,
491 name: Arc<str>,
492}
493
494impl Pipeline {
495 pub fn name(&self) -> &str {
497 &self.name
498 }
499
500 pub fn max_threads_per_threadgroup(&self) -> usize {
502 self.state.maxTotalThreadsPerThreadgroup()
503 }
504
505 pub fn execution_width(&self) -> usize {
507 self.state.threadExecutionWidth()
508 }
509
510 pub fn same_as(&self, other: &Pipeline) -> bool {
512 Retained::as_ptr(&self.state) == Retained::as_ptr(&other.state)
513 }
514
515 pub(crate) fn raw(&self) -> &ProtocolObject<dyn MTLComputePipelineState> {
516 &self.state
517 }
518}
519
520impl std::fmt::Debug for Pipeline {
521 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
522 f.debug_struct("Pipeline")
523 .field("name", &self.name)
524 .finish_non_exhaustive()
525 }
526}
527
528#[cfg(test)]
529mod tests {
530 use super::inline_sources;
531
532 #[test]
533 fn inlines_each_header_once() {
534 let sources = [
535 ("a.metal", "#include \"h.h\"\nkernel A;"),
536 ("b.metal", " #include \"h.h\"\nkernel B;"),
537 ("h.h", "#pragma once\ninline int f();"),
538 ];
539 let out = inline_sources(&sources);
540 assert_eq!(out.matches("inline int f();").count(), 1);
541 assert!(out.contains("kernel A;") && out.contains("kernel B;"));
542 assert!(!out.contains("#include \"h.h\""));
543 }
544}