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
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum Constant {
33 Bool(bool),
35 Int(i32),
37}
38
39#[derive(Clone)]
41pub struct Device {
42 inner: Arc<Inner>,
43}
44
45struct Inner {
46 device: Retained<ProtocolObject<dyn MTLDevice>>,
47 queue: Retained<ProtocolObject<dyn MTLCommandQueue>>,
48 info: DeviceInfo,
49 library: OnceLock<Result<Library>>,
50 pipelines: Mutex<HashMap<String, Pipeline>>,
51 pool: Arc<Pool>,
52 in_flight: InFlight,
53}
54
55#[derive(Default)]
57pub(crate) struct InFlight {
58 count: Mutex<usize>,
59 done: Condvar,
60}
61
62impl InFlight {
63 pub(crate) fn begin(&self) {
64 *self.count.lock().unwrap_or_else(PoisonError::into_inner) += 1;
65 }
66
67 pub(crate) fn end(&self) {
68 let mut count = self.count.lock().unwrap_or_else(PoisonError::into_inner);
69 *count = count.saturating_sub(1);
70 self.done.notify_all();
71 }
72
73 fn get(&self) -> usize {
74 *self.count.lock().unwrap_or_else(PoisonError::into_inner)
75 }
76
77 fn wait_below(&self, max: usize) {
78 let mut count = self.count.lock().unwrap_or_else(PoisonError::into_inner);
79 while *count >= max.max(1) {
80 count = self
81 .done
82 .wait(count)
83 .unwrap_or_else(PoisonError::into_inner);
84 }
85 }
86}
87
88impl Device {
89 pub fn system_default() -> Option<Device> {
92 let device = MTLCreateSystemDefaultDevice()?;
93 let queue = device.newCommandQueue()?;
94 let architecture = device.architecture().name().to_string();
95 let (generation, class) = parse_architecture(&architecture);
96 let info = DeviceInfo {
97 name: device.name().to_string(),
98 architecture,
99 generation,
100 class,
101 recommended_working_set: device.recommendedMaxWorkingSetSize(),
102 max_threads_per_threadgroup: device.maxThreadsPerThreadgroup().width,
103 };
104 let ram = objc2_foundation::NSProcessInfo::processInfo().physicalMemory();
105 let options = objc2_metal::MTLResourceOptions::StorageModeShared;
106 let placeholder = device.newBufferWithLength_options(16, options);
107 let residency = residency::Residency::new(residency::attach(&device, &queue));
108 let pool = Arc::new(Pool::new(
109 info.recommended_working_set,
110 ram,
111 placeholder,
112 residency,
113 ));
114 Some(Device {
115 inner: Arc::new(Inner {
116 device,
117 queue,
118 info,
119 library: OnceLock::new(),
120 pipelines: Mutex::new(HashMap::new()),
121 pool,
122 in_flight: InFlight::default(),
123 }),
124 })
125 }
126
127 pub fn shared() -> Option<&'static Device> {
130 static SHARED: OnceLock<Option<Device>> = OnceLock::new();
131 SHARED.get_or_init(Device::system_default).as_ref()
132 }
133
134 pub fn memory(&self) -> MemoryStats {
136 self.inner.pool.stats()
137 }
138
139 pub fn set_memory_limit(&self, bytes: usize) -> usize {
141 self.inner.pool.set_memory_limit(bytes)
142 }
143
144 pub fn set_cache_limit(&self, bytes: usize) -> usize {
146 self.inner.pool.set_cache_limit(bytes)
147 }
148
149 pub fn set_wired_limit(&self, bytes: usize) -> Result<usize> {
157 if bytes as u64 > self.inner.info.recommended_working_set {
158 return Err(GpuError::InvalidArgument(format!(
159 "wired limit {bytes} exceeds the recommended working set ({} bytes)",
160 self.inner.info.recommended_working_set
161 )));
162 }
163 Ok(self.inner.pool.set_wired_limit(bytes))
164 }
165
166 pub fn can_wire(&self) -> bool {
169 self.inner.pool.can_wire()
170 }
171
172 pub fn clear_cache(&self) {
174 self.inner.pool.clear_cache();
175 }
176
177 pub fn in_flight(&self) -> usize {
179 self.inner.in_flight.get()
180 }
181
182 pub fn wait_for_capacity(&self, max: usize) {
185 self.inner.in_flight.wait_below(max);
186 }
187
188 pub(crate) fn flights(&self) -> &InFlight {
189 &self.inner.in_flight
190 }
191
192 pub fn reset_peak(&self) {
194 self.inner.pool.reset_peak();
195 }
196
197 pub(crate) fn pool(&self) -> &Arc<Pool> {
198 &self.inner.pool
199 }
200
201 pub fn info(&self) -> &DeviceInfo {
203 &self.inner.info
204 }
205
206 pub fn pipeline(&self, name: &str) -> Result<Pipeline> {
209 self.pipeline_with(name, &[])
210 }
211
212 pub fn pipeline_with(&self, name: &str, constants: &[(usize, bool)]) -> Result<Pipeline> {
215 let constants: Vec<(usize, Constant)> = constants
216 .iter()
217 .map(|&(i, v)| (i, Constant::Bool(v)))
218 .collect();
219 self.pipeline_with_constants(name, &constants)
220 }
221
222 pub fn pipeline_with_constants(
225 &self,
226 name: &str,
227 constants: &[(usize, Constant)],
228 ) -> Result<Pipeline> {
229 let key = constants.iter().fold(name.to_owned(), |mut k, (i, v)| {
230 match v {
231 Constant::Bool(b) => k.push_str(&format!("#{i}={}", u8::from(*b))),
232 Constant::Int(n) => k.push_str(&format!("#{i}=i{n}")),
233 }
234 k
235 });
236 let mut cache = self
237 .inner
238 .pipelines
239 .lock()
240 .unwrap_or_else(PoisonError::into_inner);
241 if let Some(p) = cache.get(&key) {
242 return Ok(p.clone());
243 }
244 let library = self.library()?;
245 let ns_name = NSString::from_str(name);
246 let function = if constants.is_empty() {
247 library
248 .newFunctionWithName(&ns_name)
249 .ok_or_else(|| GpuError::MissingKernel(name.to_owned()))?
250 } else {
251 let values = MTLFunctionConstantValues::new();
252 for &(index, value) in constants {
253 match value {
254 Constant::Bool(b) => unsafe {
257 values.setConstantValue_type_atIndex(
258 NonNull::from(&b).cast(),
259 MTLDataType::Bool,
260 index,
261 );
262 },
263 Constant::Int(n) => unsafe {
265 values.setConstantValue_type_atIndex(
266 NonNull::from(&n).cast(),
267 MTLDataType::Int,
268 index,
269 );
270 },
271 }
272 }
273 library
274 .newFunctionWithName_constantValues_error(&ns_name, &values)
275 .map_err(|e| GpuError::Pipeline {
276 name: key.clone(),
277 message: e.localizedDescription().to_string(),
278 })?
279 };
280 let state = self
281 .inner
282 .device
283 .newComputePipelineStateWithFunction_error(&function)
284 .map_err(|e| GpuError::Pipeline {
285 name: key.clone(),
286 message: e.localizedDescription().to_string(),
287 })?;
288 let pipeline = Pipeline {
289 state,
290 name: key.as_str().into(),
291 };
292 cache.insert(key, pipeline.clone());
293 Ok(pipeline)
294 }
295
296 pub fn pipeline_from_source(
301 &self,
302 name: &str,
303 source: impl FnOnce() -> String,
304 ) -> Result<Pipeline> {
305 let mut cache = self
306 .inner
307 .pipelines
308 .lock()
309 .unwrap_or_else(PoisonError::into_inner);
310 if let Some(p) = cache.get(name) {
311 return Ok(p.clone());
312 }
313 let failed = |message: String| GpuError::Pipeline {
314 name: name.to_owned(),
315 message,
316 };
317 let source = inline_sources(&[("generated.metal", &source())]);
318 let options = objc2_metal::MTLCompileOptions::new();
319 options.setMathMode(objc2_metal::MTLMathMode::Safe);
320 let library = self
321 .inner
322 .device
323 .newLibraryWithSource_options_error(&NSString::from_str(&source), Some(&options))
324 .map_err(|e| failed(e.localizedDescription().to_string()))?;
325 let function = library
326 .newFunctionWithName(&NSString::from_str(name))
327 .ok_or_else(|| GpuError::MissingKernel(name.to_owned()))?;
328 let state = self
329 .inner
330 .device
331 .newComputePipelineStateWithFunction_error(&function)
332 .map_err(|e| failed(e.localizedDescription().to_string()))?;
333 let pipeline = Pipeline {
334 state,
335 name: name.into(),
336 };
337 cache.insert(name.to_owned(), pipeline.clone());
338 Ok(pipeline)
339 }
340
341 fn library(&self) -> Result<&Library> {
343 self.inner
344 .library
345 .get_or_init(|| load_library(&self.inner.device))
346 .as_ref()
347 .map_err(Clone::clone)
348 }
349
350 pub(crate) fn raw(&self) -> &ProtocolObject<dyn MTLDevice> {
351 &self.inner.device
352 }
353
354 pub(crate) fn queue(&self) -> &ProtocolObject<dyn MTLCommandQueue> {
355 &self.inner.queue
356 }
357}
358
359impl std::fmt::Debug for Device {
360 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
361 f.debug_struct("Device")
362 .field("info", &self.inner.info)
363 .finish_non_exhaustive()
364 }
365}
366
367fn load_library(device: &ProtocolObject<dyn MTLDevice>) -> Result<Library> {
370 let error = |e: Retained<objc2_foundation::NSError>| {
371 GpuError::LibraryLoad(e.localizedDescription().to_string())
372 };
373 match nuvec_kernels::MODE {
374 MetallibMode::Precompiled => {
375 let bytes = nuvec_kernels::metallib().ok_or(GpuError::NoKernels)?;
376 device
377 .newLibraryWithData_error(&dispatch2::DispatchData::from_static_bytes(bytes))
378 .map_err(error)
379 }
380 MetallibMode::RuntimeSource => {
381 let source = inline_sources(nuvec_kernels::sources());
382 let options = objc2_metal::MTLCompileOptions::new();
384 options.setMathMode(objc2_metal::MTLMathMode::Safe);
385 device
386 .newLibraryWithSource_options_error(&NSString::from_str(&source), Some(&options))
387 .map_err(error)
388 }
389 _ => Err(GpuError::NoKernels),
390 }
391}
392
393fn inline_sources(sources: &[(&str, &str)]) -> String {
396 let headers: HashMap<&str, &str> = nuvec_kernels::sources()
397 .iter()
398 .chain(sources)
399 .copied()
400 .chain([("common.h", nuvec_kernels::COMMON_H)])
401 .filter(|(p, _)| p.ends_with(".h"))
402 .collect();
403 let mut included = HashSet::new();
404 let mut out = String::new();
405 for (_, source) in sources.iter().filter(|(p, _)| p.ends_with(".metal")) {
406 for line in source.lines() {
407 let header = line
408 .trim()
409 .strip_prefix("#include \"")
410 .and_then(|r| r.strip_suffix('"'));
411 match header.and_then(|h| headers.get(h).map(|text| (h, text))) {
412 Some((h, text)) => {
413 if included.insert(h) {
414 out.push_str(text);
415 out.push('\n');
416 }
417 }
418 None => {
419 out.push_str(line);
420 out.push('\n');
421 }
422 }
423 }
424 }
425 out
426}
427
428#[derive(Clone)]
430pub struct Pipeline {
431 state: Retained<ProtocolObject<dyn MTLComputePipelineState>>,
432 name: Arc<str>,
433}
434
435impl Pipeline {
436 pub fn name(&self) -> &str {
438 &self.name
439 }
440
441 pub fn max_threads_per_threadgroup(&self) -> usize {
443 self.state.maxTotalThreadsPerThreadgroup()
444 }
445
446 pub fn execution_width(&self) -> usize {
448 self.state.threadExecutionWidth()
449 }
450
451 pub fn same_as(&self, other: &Pipeline) -> bool {
453 Retained::as_ptr(&self.state) == Retained::as_ptr(&other.state)
454 }
455
456 pub(crate) fn raw(&self) -> &ProtocolObject<dyn MTLComputePipelineState> {
457 &self.state
458 }
459}
460
461impl std::fmt::Debug for Pipeline {
462 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
463 f.debug_struct("Pipeline")
464 .field("name", &self.name)
465 .finish_non_exhaustive()
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use super::inline_sources;
472
473 #[test]
474 fn inlines_each_header_once() {
475 let sources = [
476 ("a.metal", "#include \"h.h\"\nkernel A;"),
477 ("b.metal", " #include \"h.h\"\nkernel B;"),
478 ("h.h", "#pragma once\ninline int f();"),
479 ];
480 let out = inline_sources(&sources);
481 assert_eq!(out.matches("inline int f();").count(), 1);
482 assert!(out.contains("kernel A;") && out.contains("kernel B;"));
483 assert!(!out.contains("#include \"h.h\""));
484 }
485}