Skip to main content

mtl_gpu/capture/
mod.rs

1//! Metal GPU capture facilities for debugging.
2//!
3//! Corresponds to `Metal/MTLCaptureManager.hpp` and `Metal/MTLCaptureScope.hpp`.
4//!
5//! The capture API allows you to programmatically capture GPU workloads
6//! for debugging in Xcode or saving to a GPU trace document.
7//!
8//! # Example
9//!
10//! ```ignore
11//! use mtl_gpu::capture::CaptureManager;
12//! use mtl_gpu::device::system_default;
13//!
14//! let device = system_default().expect("no Metal device");
15//! let manager = CaptureManager::shared();
16//!
17//! // Start capturing with default settings
18//! manager.start_capture_with_device(&device);
19//!
20//! // ... perform GPU work ...
21//!
22//! manager.stop_capture();
23//! ```
24
25use std::ffi::c_void;
26use std::ptr::NonNull;
27
28use mtl_foundation::{Referencing, Url};
29use mtl_sys::{msg_send_0, msg_send_1, msg_send_2, sel};
30
31use crate::CommandQueue;
32use crate::Device;
33use crate::enums::{CaptureDestination, CaptureError};
34
35// ============================================================================
36// CaptureDescriptor
37// ============================================================================
38
39/// Descriptor for configuring a GPU capture session.
40///
41/// C++ equivalent: `MTL::CaptureDescriptor`
42#[repr(transparent)]
43pub struct CaptureDescriptor(NonNull<c_void>);
44
45impl CaptureDescriptor {
46    /// Allocate a new capture descriptor.
47    ///
48    /// C++ equivalent: `static CaptureDescriptor* alloc()`
49    pub fn alloc() -> Option<Self> {
50        unsafe {
51            let cls = mtl_sys::Class::get("MTLCaptureDescriptor")?;
52            let ptr: *mut c_void = msg_send_0(cls.as_ptr(), sel!(alloc));
53            Self::from_raw(ptr)
54        }
55    }
56
57    /// Initialize an allocated capture descriptor.
58    ///
59    /// C++ equivalent: `CaptureDescriptor* init()`
60    pub fn init(&self) -> Option<Self> {
61        unsafe {
62            let ptr: *mut c_void = msg_send_0(self.as_ptr(), sel!(init));
63            Self::from_raw(ptr)
64        }
65    }
66
67    /// Create a new capture descriptor.
68    pub fn new() -> Option<Self> {
69        Self::alloc()?.init()
70    }
71
72    /// Create from a raw pointer.
73    ///
74    /// # Safety
75    ///
76    /// The pointer must be a valid Metal capture descriptor.
77    #[inline]
78    pub unsafe fn from_raw(ptr: *mut c_void) -> Option<Self> {
79        NonNull::new(ptr).map(Self)
80    }
81
82    /// Get the raw pointer.
83    #[inline]
84    pub fn as_raw(&self) -> *mut c_void {
85        self.0.as_ptr()
86    }
87
88    /// Get the object to capture.
89    ///
90    /// C++ equivalent: `NS::Object* captureObject() const`
91    pub fn capture_object(&self) -> Option<*mut c_void> {
92        unsafe {
93            let ptr: *mut c_void = msg_send_0(self.as_ptr(), sel!(captureObject));
94            if ptr.is_null() { None } else { Some(ptr) }
95        }
96    }
97
98    /// Set the object to capture (Device, CommandQueue, or CaptureScope).
99    ///
100    /// C++ equivalent: `void setCaptureObject(NS::Object*)`
101    pub fn set_capture_object(&self, object: *const c_void) {
102        unsafe {
103            msg_send_1::<(), *const c_void>(self.as_ptr(), sel!(setCaptureObject:), object);
104        }
105    }
106
107    /// Set a device as the capture target.
108    pub fn set_capture_device(&self, device: &Device) {
109        self.set_capture_object(device.as_ptr());
110    }
111
112    /// Set a command queue as the capture target.
113    pub fn set_capture_command_queue(&self, queue: &CommandQueue) {
114        self.set_capture_object(queue.as_ptr());
115    }
116
117    /// Set a capture scope as the capture target.
118    pub fn set_capture_scope(&self, scope: &CaptureScope) {
119        self.set_capture_object(scope.as_ptr());
120    }
121
122    /// Get the capture destination.
123    ///
124    /// C++ equivalent: `CaptureDestination destination() const`
125    #[inline]
126    pub fn destination(&self) -> CaptureDestination {
127        unsafe { msg_send_0(self.as_ptr(), sel!(destination)) }
128    }
129
130    /// Set the capture destination.
131    ///
132    /// C++ equivalent: `void setDestination(CaptureDestination)`
133    #[inline]
134    pub fn set_destination(&self, destination: CaptureDestination) {
135        unsafe {
136            msg_send_1::<(), CaptureDestination>(self.as_ptr(), sel!(setDestination:), destination);
137        }
138    }
139
140    /// Get the output URL for trace document captures.
141    ///
142    /// C++ equivalent: `NS::URL* outputURL() const`
143    pub fn output_url(&self) -> Option<Url> {
144        unsafe {
145            let ptr: *mut c_void = msg_send_0(self.as_ptr(), sel!(outputURL));
146            if ptr.is_null() {
147                return None;
148            }
149            msg_send_0::<*mut c_void>(ptr as *const c_void, sel!(retain));
150            Url::from_ptr(ptr)
151        }
152    }
153
154    /// Set the output URL for trace document captures.
155    ///
156    /// C++ equivalent: `void setOutputURL(const NS::URL*)`
157    pub fn set_output_url(&self, url: &Url) {
158        unsafe {
159            msg_send_1::<(), *const c_void>(self.as_ptr(), sel!(setOutputURL:), url.as_ptr());
160        }
161    }
162}
163
164impl Default for CaptureDescriptor {
165    fn default() -> Self {
166        Self::new().expect("failed to create CaptureDescriptor")
167    }
168}
169
170impl Clone for CaptureDescriptor {
171    fn clone(&self) -> Self {
172        unsafe {
173            let ptr: *mut c_void = msg_send_0(self.as_ptr(), sel!(copy));
174            Self::from_raw(ptr).expect("failed to copy CaptureDescriptor")
175        }
176    }
177}
178
179impl Drop for CaptureDescriptor {
180    fn drop(&mut self) {
181        unsafe {
182            msg_send_0::<()>(self.as_ptr(), sel!(release));
183        }
184    }
185}
186
187impl Referencing for CaptureDescriptor {
188    #[inline]
189    fn as_ptr(&self) -> *const c_void {
190        self.0.as_ptr()
191    }
192}
193
194unsafe impl Send for CaptureDescriptor {}
195unsafe impl Sync for CaptureDescriptor {}
196
197impl std::fmt::Debug for CaptureDescriptor {
198    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199        f.debug_struct("CaptureDescriptor")
200            .field("destination", &self.destination())
201            .finish()
202    }
203}
204
205// ============================================================================
206// CaptureScope
207// ============================================================================
208
209/// A capture scope that defines the boundaries of a GPU capture.
210///
211/// C++ equivalent: `MTL::CaptureScope`
212#[repr(transparent)]
213pub struct CaptureScope(NonNull<c_void>);
214
215impl CaptureScope {
216    /// Create from a raw pointer.
217    ///
218    /// # Safety
219    ///
220    /// The pointer must be a valid Metal capture scope.
221    #[inline]
222    pub unsafe fn from_raw(ptr: *mut c_void) -> Option<Self> {
223        NonNull::new(ptr).map(Self)
224    }
225
226    /// Get the raw pointer.
227    #[inline]
228    pub fn as_raw(&self) -> *mut c_void {
229        self.0.as_ptr()
230    }
231
232    /// Get the device associated with this capture scope.
233    ///
234    /// C++ equivalent: `Device* device() const`
235    pub fn device(&self) -> Device {
236        unsafe {
237            let ptr: *mut c_void = msg_send_0(self.as_ptr(), sel!(device));
238            msg_send_0::<*mut c_void>(ptr as *const c_void, sel!(retain));
239            Device::from_raw(ptr).expect("capture scope has no device")
240        }
241    }
242
243    /// Get the command queue associated with this capture scope.
244    ///
245    /// C++ equivalent: `CommandQueue* commandQueue() const`
246    pub fn command_queue(&self) -> Option<CommandQueue> {
247        unsafe {
248            let ptr: *mut c_void = msg_send_0(self.as_ptr(), sel!(commandQueue));
249            if ptr.is_null() {
250                return None;
251            }
252            msg_send_0::<*mut c_void>(ptr as *const c_void, sel!(retain));
253            CommandQueue::from_raw(ptr)
254        }
255    }
256
257    /// Get the label for this capture scope.
258    ///
259    /// C++ equivalent: `NS::String* label() const`
260    pub fn label(&self) -> Option<String> {
261        unsafe {
262            let ptr: *mut c_void = msg_send_0(self.as_ptr(), sel!(label));
263            if ptr.is_null() {
264                return None;
265            }
266            let utf8_ptr: *const std::ffi::c_char =
267                mtl_sys::msg_send_0(ptr as *const c_void, sel!(UTF8String));
268            if utf8_ptr.is_null() {
269                return None;
270            }
271            let c_str = std::ffi::CStr::from_ptr(utf8_ptr);
272            Some(c_str.to_string_lossy().into_owned())
273        }
274    }
275
276    /// Set the label for this capture scope.
277    ///
278    /// C++ equivalent: `void setLabel(const NS::String*)`
279    pub fn set_label(&self, label: &str) {
280        if let Some(ns_label) = mtl_foundation::String::from_str(label) {
281            unsafe {
282                msg_send_1::<(), *const c_void>(self.as_ptr(), sel!(setLabel:), ns_label.as_ptr());
283            }
284        }
285    }
286
287    /// Begin the capture scope.
288    ///
289    /// C++ equivalent: `void beginScope()`
290    #[inline]
291    pub fn begin_scope(&self) {
292        unsafe {
293            msg_send_0::<()>(self.as_ptr(), sel!(beginScope));
294        }
295    }
296
297    /// End the capture scope.
298    ///
299    /// C++ equivalent: `void endScope()`
300    #[inline]
301    pub fn end_scope(&self) {
302        unsafe {
303            msg_send_0::<()>(self.as_ptr(), sel!(endScope));
304        }
305    }
306}
307
308impl Clone for CaptureScope {
309    fn clone(&self) -> Self {
310        unsafe {
311            msg_send_0::<*mut c_void>(self.as_ptr(), sel!(retain));
312        }
313        Self(self.0)
314    }
315}
316
317impl Drop for CaptureScope {
318    fn drop(&mut self) {
319        unsafe {
320            msg_send_0::<()>(self.as_ptr(), sel!(release));
321        }
322    }
323}
324
325impl Referencing for CaptureScope {
326    #[inline]
327    fn as_ptr(&self) -> *const c_void {
328        self.0.as_ptr()
329    }
330}
331
332unsafe impl Send for CaptureScope {}
333unsafe impl Sync for CaptureScope {}
334
335impl std::fmt::Debug for CaptureScope {
336    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
337        f.debug_struct("CaptureScope")
338            .field("label", &self.label())
339            .finish()
340    }
341}
342
343// ============================================================================
344// CaptureManager
345// ============================================================================
346
347/// The singleton manager for GPU capture operations.
348///
349/// C++ equivalent: `MTL::CaptureManager`
350///
351/// Use `CaptureManager::shared()` to get the singleton instance.
352#[repr(transparent)]
353pub struct CaptureManager(NonNull<c_void>);
354
355impl CaptureManager {
356    /// Get the shared capture manager singleton.
357    ///
358    /// Returns `None` if capture is not supported on this system.
359    ///
360    /// C++ equivalent: `static CaptureManager* sharedCaptureManager()`
361    pub fn shared() -> Option<Self> {
362        unsafe {
363            let cls = mtl_sys::Class::get("MTLCaptureManager")?;
364            let ptr: *mut c_void = msg_send_0(cls.as_ptr(), sel!(sharedCaptureManager));
365            if ptr.is_null() {
366                return None;
367            }
368            // The shared manager is a singleton, retain it for our reference
369            msg_send_0::<*mut c_void>(ptr as *const c_void, sel!(retain));
370            Self::from_raw(ptr)
371        }
372    }
373
374    /// Create from a raw pointer.
375    ///
376    /// # Safety
377    ///
378    /// The pointer must be a valid Metal capture manager.
379    #[inline]
380    pub unsafe fn from_raw(ptr: *mut c_void) -> Option<Self> {
381        NonNull::new(ptr).map(Self)
382    }
383
384    /// Get the raw pointer.
385    #[inline]
386    pub fn as_raw(&self) -> *mut c_void {
387        self.0.as_ptr()
388    }
389
390    /// Check if a capture is currently in progress.
391    ///
392    /// C++ equivalent: `bool isCapturing() const`
393    #[inline]
394    pub fn is_capturing(&self) -> bool {
395        unsafe { msg_send_0(self.as_ptr(), sel!(isCapturing)) }
396    }
397
398    /// Get the default capture scope.
399    ///
400    /// C++ equivalent: `CaptureScope* defaultCaptureScope() const`
401    pub fn default_capture_scope(&self) -> Option<CaptureScope> {
402        unsafe {
403            let ptr: *mut c_void = msg_send_0(self.as_ptr(), sel!(defaultCaptureScope));
404            if ptr.is_null() {
405                return None;
406            }
407            msg_send_0::<*mut c_void>(ptr as *const c_void, sel!(retain));
408            CaptureScope::from_raw(ptr)
409        }
410    }
411
412    /// Set the default capture scope.
413    ///
414    /// C++ equivalent: `void setDefaultCaptureScope(const CaptureScope*)`
415    pub fn set_default_capture_scope(&self, scope: &CaptureScope) {
416        unsafe {
417            msg_send_1::<(), *const c_void>(
418                self.as_ptr(),
419                sel!(setDefaultCaptureScope:),
420                scope.as_ptr(),
421            );
422        }
423    }
424
425    /// Create a new capture scope for a device.
426    ///
427    /// C++ equivalent: `CaptureScope* newCaptureScope(const Device*)`
428    pub fn new_capture_scope_with_device(&self, device: &Device) -> Option<CaptureScope> {
429        unsafe {
430            let ptr: *mut c_void = msg_send_1(
431                self.as_ptr(),
432                sel!(newCaptureScopeWithDevice:),
433                device.as_ptr(),
434            );
435            CaptureScope::from_raw(ptr)
436        }
437    }
438
439    /// Create a new capture scope for a command queue.
440    ///
441    /// C++ equivalent: `CaptureScope* newCaptureScope(const CommandQueue*)`
442    pub fn new_capture_scope_with_command_queue(
443        &self,
444        queue: &CommandQueue,
445    ) -> Option<CaptureScope> {
446        unsafe {
447            let ptr: *mut c_void = msg_send_1(
448                self.as_ptr(),
449                sel!(newCaptureScopeWithCommandQueue:),
450                queue.as_ptr(),
451            );
452            CaptureScope::from_raw(ptr)
453        }
454    }
455
456    /// Check if a capture destination is supported.
457    ///
458    /// C++ equivalent: `bool supportsDestination(CaptureDestination)`
459    #[inline]
460    pub fn supports_destination(&self, destination: CaptureDestination) -> bool {
461        unsafe { msg_send_1(self.as_ptr(), sel!(supportsDestination:), destination) }
462    }
463
464    /// Start a capture with a descriptor.
465    ///
466    /// Returns `Ok(())` on success or `Err(CaptureError)` on failure.
467    ///
468    /// C++ equivalent: `bool startCapture(const CaptureDescriptor*, NS::Error**)`
469    pub fn start_capture(&self, descriptor: &CaptureDescriptor) -> Result<(), CaptureError> {
470        unsafe {
471            let mut error: *mut c_void = std::ptr::null_mut();
472            let success: bool = msg_send_2(
473                self.as_ptr(),
474                sel!(startCaptureWithDescriptor: error:),
475                descriptor.as_ptr(),
476                &mut error as *mut *mut c_void,
477            );
478            if success {
479                Ok(())
480            } else {
481                // Try to get the error code
482                if !error.is_null() {
483                    let code: mtl_foundation::Integer =
484                        mtl_sys::msg_send_0(error as *const c_void, sel!(code));
485                    Err(CaptureError(code))
486                } else {
487                    Err(CaptureError::NOT_SUPPORTED)
488                }
489            }
490        }
491    }
492
493    /// Start a capture for a device (simplified API).
494    ///
495    /// C++ equivalent: `void startCapture(const Device*)`
496    #[inline]
497    pub fn start_capture_with_device(&self, device: &Device) {
498        unsafe {
499            msg_send_1::<(), *const c_void>(
500                self.as_ptr(),
501                sel!(startCaptureWithDevice:),
502                device.as_ptr(),
503            );
504        }
505    }
506
507    /// Start a capture for a command queue (simplified API).
508    ///
509    /// C++ equivalent: `void startCapture(const CommandQueue*)`
510    #[inline]
511    pub fn start_capture_with_command_queue(&self, queue: &CommandQueue) {
512        unsafe {
513            msg_send_1::<(), *const c_void>(
514                self.as_ptr(),
515                sel!(startCaptureWithCommandQueue:),
516                queue.as_ptr(),
517            );
518        }
519    }
520
521    /// Start a capture for a capture scope (simplified API).
522    ///
523    /// C++ equivalent: `void startCapture(const CaptureScope*)`
524    #[inline]
525    pub fn start_capture_with_scope(&self, scope: &CaptureScope) {
526        unsafe {
527            msg_send_1::<(), *const c_void>(
528                self.as_ptr(),
529                sel!(startCaptureWithScope:),
530                scope.as_ptr(),
531            );
532        }
533    }
534
535    /// Stop the current capture.
536    ///
537    /// C++ equivalent: `void stopCapture()`
538    #[inline]
539    pub fn stop_capture(&self) {
540        unsafe {
541            msg_send_0::<()>(self.as_ptr(), sel!(stopCapture));
542        }
543    }
544}
545
546impl Clone for CaptureManager {
547    fn clone(&self) -> Self {
548        unsafe {
549            msg_send_0::<*mut c_void>(self.as_ptr(), sel!(retain));
550        }
551        Self(self.0)
552    }
553}
554
555impl Drop for CaptureManager {
556    fn drop(&mut self) {
557        unsafe {
558            msg_send_0::<()>(self.as_ptr(), sel!(release));
559        }
560    }
561}
562
563impl Referencing for CaptureManager {
564    #[inline]
565    fn as_ptr(&self) -> *const c_void {
566        self.0.as_ptr()
567    }
568}
569
570unsafe impl Send for CaptureManager {}
571unsafe impl Sync for CaptureManager {}
572
573impl std::fmt::Debug for CaptureManager {
574    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
575        f.debug_struct("CaptureManager")
576            .field("is_capturing", &self.is_capturing())
577            .finish()
578    }
579}
580
581#[cfg(test)]
582mod tests {
583    use super::*;
584
585    #[test]
586    fn test_capture_descriptor_size() {
587        assert_eq!(
588            std::mem::size_of::<CaptureDescriptor>(),
589            std::mem::size_of::<*mut c_void>()
590        );
591    }
592
593    #[test]
594    fn test_capture_scope_size() {
595        assert_eq!(
596            std::mem::size_of::<CaptureScope>(),
597            std::mem::size_of::<*mut c_void>()
598        );
599    }
600
601    #[test]
602    fn test_capture_manager_size() {
603        assert_eq!(
604            std::mem::size_of::<CaptureManager>(),
605            std::mem::size_of::<*mut c_void>()
606        );
607    }
608
609    #[test]
610    fn test_shared_capture_manager() {
611        // CaptureManager may not be available in all environments
612        if let Some(manager) = CaptureManager::shared() {
613            // Should not be capturing by default
614            assert!(!manager.is_capturing());
615        }
616    }
617
618    // Note: test_capture_descriptor_creation removed because CaptureDescriptor
619    // is not available in all test environments (e.g., headless CI).
620}