Skip to main content

screencapturekit/
metal.rs

1//! Metal texture helpers for `IOSurface`
2//!
3//! This module provides utilities for creating Metal textures from `IOSurface`
4//! with zero-copy GPU access. This is the most efficient way to use captured
5//! frames with Metal rendering.
6//!
7//! ## Features
8//!
9//! - Zero-copy texture creation from `IOSurface`
10//! - Automatic pixel format detection and Metal format mapping
11//! - Multi-plane support for YCbCr formats (420v, 420f)
12//! - Native Metal device and texture types (no external crate needed)
13//! - Embedded Metal shaders for common rendering scenarios
14//!
15//! ## When to Use
16//!
17//! Use this module when you need:
18//! - **Real-time rendering** - Display captured frames in a Metal view
19//! - **GPU processing** - Apply compute shaders to captured content
20//! - **Zero-copy performance** - Avoid CPU-GPU memory transfers
21//!
22//! For CPU-based processing, use [`CVPixelBuffer`](crate::cv::CVPixelBuffer) with lock guards instead.
23//!
24//! ## Workflow
25//!
26//! 1. Get `IOSurface` from captured frame via [`CMSampleBufferExt::pixel_buffer()`](crate::cm::CMSampleBufferExt::pixel_buffer)
27//! 2. Create Metal textures with [`IOSurface::create_metal_textures()`](crate::cm::IOSurface::create_metal_textures)
28//! 3. Render using the built-in shaders or your own
29//!
30//! ## Example
31//!
32//! ```no_run
33//! use screencapturekit::cm::{CMSampleBuffer, CMSampleBufferExt, IOSurface};
34//! use screencapturekit::metal::{IOSurfaceMetalExt, MetalDevice};
35//!
36//! // Get the system default Metal device
37//! let device = MetalDevice::system_default().expect("No Metal device");
38//!
39//! // In your frame handler
40//! fn handle_frame(sample: &CMSampleBuffer, device: &MetalDevice) {
41//!     if let Some(pixel_buffer) = sample.pixel_buffer() {
42//!         if let Some(surface) = pixel_buffer.io_surface() {
43//!             // Create textures directly - no closures or factories needed
44//!             if let Some(textures) = surface.create_metal_textures(device) {
45//!                 if textures.is_ycbcr() {
46//!                     // Use YCbCr shader with plane0 (Y) and plane1 (CbCr)
47//!                     println!("YCbCr texture: {}x{}",
48//!                         textures.plane0.width(), textures.plane0.height());
49//!                 } else {
50//!                     // Use single-plane shader (BGRA, l10r)
51//!                     println!("Single-plane texture: {}x{}",
52//!                         textures.plane0.width(), textures.plane0.height());
53//!                 }
54//!             }
55//!         }
56//!     }
57//! }
58//! ```
59//!
60//! ## Built-in Shaders
61//!
62//! The [`SHADER_SOURCE`] constant contains Metal shaders for common rendering scenarios:
63//!
64//! | Function | Description |
65//! |----------|-------------|
66//! | `vertex_fullscreen` | Aspect-ratio-preserving fullscreen quad |
67//! | `fragment_textured` | BGRA/L10R single-texture rendering |
68//! | `fragment_ycbcr` | YCbCr biplanar (420v/420f) to RGB conversion |
69//! | `vertex_colored` / `fragment_colored` | UI overlay rendering |
70
71use std::ffi::{c_void, CStr};
72use std::marker::PhantomData;
73use std::ptr::NonNull;
74use std::sync::{Arc, Mutex, PoisonError};
75
76use crate::cm::IOSurface;
77use crate::FourCharCode;
78
79/// Pixel format constants using [`FourCharCode`]
80///
81/// These match the values returned by `IOSurface::pixel_format()`.
82pub mod pixel_format {
83    use crate::FourCharCode;
84
85    /// BGRA 8-bit per channel (32-bit total)
86    pub const BGRA: FourCharCode = FourCharCode::from_bytes(*b"BGRA");
87
88    /// 10-bit RGB (ARGB2101010, also known as l10r)
89    pub const L10R: FourCharCode = FourCharCode::from_bytes(*b"l10r");
90
91    /// YCbCr 4:2:0 biplanar, video range
92    pub const YCBCR_420V: FourCharCode = FourCharCode::from_bytes(*b"420v");
93
94    /// YCbCr 4:2:0 biplanar, full range
95    pub const YCBCR_420F: FourCharCode = FourCharCode::from_bytes(*b"420f");
96
97    /// Check if a pixel format is a YCbCr biplanar format
98    ///
99    /// Accepts either a `FourCharCode` or a raw `u32`.
100    #[must_use]
101    pub fn is_ycbcr_biplanar(format: impl Into<FourCharCode>) -> bool {
102        let f = format.into();
103        f.equals(YCBCR_420V) || f.equals(YCBCR_420F)
104    }
105
106    /// Check if a pixel format uses full range (vs video range)
107    ///
108    /// Accepts either a `FourCharCode` or a raw `u32`.
109    #[must_use]
110    pub fn is_full_range(format: impl Into<FourCharCode>) -> bool {
111        format.into().equals(YCBCR_420F)
112    }
113}
114
115/// Metal pixel format enum matching `MTLPixelFormat` values
116///
117/// This provides a Rust-native enum for common Metal pixel formats used in screen capture.
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
119#[repr(u64)]
120pub enum MetalPixelFormat {
121    /// 8-bit normalized unsigned integer per channel (BGRA order)
122    BGRA8Unorm = 80,
123    /// 10-bit RGB with 2-bit alpha (BGR order)
124    BGR10A2Unorm = 94,
125    /// 8-bit normalized unsigned integer (single channel, for Y plane)
126    R8Unorm = 10,
127    /// 8-bit normalized unsigned integer per channel (two channels, for `CbCr` plane)
128    RG8Unorm = 30,
129}
130
131impl MetalPixelFormat {
132    /// Get the raw `MTLPixelFormat` value
133    #[must_use]
134    pub const fn raw(self) -> u64 {
135        self as u64
136    }
137
138    /// Create from a raw `MTLPixelFormat` value
139    #[must_use]
140    pub const fn from_raw(value: u64) -> Option<Self> {
141        match value {
142            80 => Some(Self::BGRA8Unorm),
143            94 => Some(Self::BGR10A2Unorm),
144            10 => Some(Self::R8Unorm),
145            30 => Some(Self::RG8Unorm),
146            _ => None,
147        }
148    }
149}
150
151/// Information about an `IOSurface` for Metal texture creation
152#[derive(Debug, Clone)]
153pub struct IOSurfaceInfo {
154    /// Width in pixels
155    pub width: usize,
156    /// Height in pixels
157    pub height: usize,
158    /// Bytes per row
159    pub bytes_per_row: usize,
160    /// Pixel format
161    pub pixel_format: FourCharCode,
162    /// Number of planes (0 for single-plane formats, 2 for YCbCr biplanar)
163    pub plane_count: usize,
164    /// Per-plane information
165    pub planes: Vec<PlaneInfo>,
166}
167
168/// Information about a single plane within an `IOSurface`
169#[derive(Debug, Clone)]
170pub struct PlaneInfo {
171    /// Plane index
172    pub index: usize,
173    /// Width in pixels
174    pub width: usize,
175    /// Height in pixels
176    pub height: usize,
177    /// Bytes per row
178    pub bytes_per_row: usize,
179}
180
181/// Metal texture descriptor parameters for creating textures from `IOSurface`
182///
183/// This provides the information needed to configure a Metal `MTLTextureDescriptor`.
184#[derive(Debug, Clone, Copy)]
185pub struct TextureParams {
186    /// Width in pixels
187    pub width: usize,
188    /// Height in pixels
189    pub height: usize,
190    /// Recommended Metal pixel format
191    pub format: MetalPixelFormat,
192    /// Plane index for multi-planar surfaces
193    pub plane: usize,
194}
195
196impl TextureParams {
197    /// Get the raw `MTLPixelFormat` value for use with Metal APIs
198    #[must_use]
199    pub const fn metal_pixel_format(&self) -> u64 {
200        self.format.raw()
201    }
202}
203
204/// Result of creating Metal textures from an `IOSurface`
205#[derive(Debug)]
206pub struct CapturedTextures<T> {
207    /// Primary texture (BGRA/L10R for single-plane, Y plane for YCbCr)
208    pub plane0: T,
209    /// Secondary texture (`CbCr` plane for YCbCr formats)
210    pub plane1: Option<T>,
211    /// The pixel format of the source surface
212    pub pixel_format: FourCharCode,
213    /// Width in pixels
214    pub width: usize,
215    /// Height in pixels
216    pub height: usize,
217}
218
219impl<T> CapturedTextures<T> {
220    /// Check if this capture uses a YCbCr biplanar format
221    #[must_use]
222    pub fn is_ycbcr(&self) -> bool {
223        pixel_format::is_ycbcr_biplanar(self.pixel_format)
224    }
225}
226
227/// Metal shader source for rendering captured frames
228///
229/// This shader supports:
230/// - BGRA and BGR10A2 single-plane formats
231/// - YCbCr 4:2:0 biplanar formats (420v and 420f)
232/// - Aspect-ratio-preserving fullscreen quad
233///
234/// ## Uniforms
235///
236/// The shader expects a `Uniforms` buffer:
237/// - `viewport_size: float2` - Current viewport dimensions
238/// - `texture_size: float2` - Source texture dimensions
239/// - `time: float` - Animation time (optional)
240/// - `pixel_format: uint` - `FourCC` pixel format code
241///
242/// ## Usage
243///
244/// 1. Compile shader with `device.new_library_with_source(SHADER_SOURCE, ...)`
245/// 2. Create pipeline with `vertex_fullscreen` + `fragment_textured` (for BGRA/L10R)
246/// 3. Or use `vertex_fullscreen` + `fragment_ycbcr` (for 420v/420f)
247/// 4. Bind plane0 to texture slot 0, plane1 to texture slot 1 (for YCbCr)
248pub const SHADER_SOURCE: &str = r"
249#include <metal_stdlib>
250using namespace metal;
251
252struct Uniforms {
253    float2 viewport_size;
254    float2 texture_size;
255    float time;
256    uint pixel_format;
257    float padding[2];
258};
259
260struct TexturedVertexOut {
261    float4 position [[position]];
262    float2 texcoord;
263};
264
265// Fullscreen quad vertex shader with aspect ratio correction
266vertex TexturedVertexOut vertex_fullscreen(uint vid [[vertex_id]], constant Uniforms& uniforms [[buffer(0)]]) {
267    TexturedVertexOut out;
268    float va = uniforms.viewport_size.x / uniforms.viewport_size.y;
269    float ta = uniforms.texture_size.x / uniforms.texture_size.y;
270    float sx = ta > va ? 1.0 : ta / va;
271    float sy = ta > va ? va / ta : 1.0;
272    float2 positions[4] = { float2(-sx, -sy), float2(sx, -sy), float2(-sx, sy), float2(sx, sy) };
273    float2 texcoords[4] = { float2(0.0, 1.0), float2(1.0, 1.0), float2(0.0, 0.0), float2(1.0, 0.0) };
274    out.position = float4(positions[vid], 0.0, 1.0);
275    out.texcoord = texcoords[vid];
276    return out;
277}
278
279// BGRA/RGB texture fragment shader
280fragment float4 fragment_textured(TexturedVertexOut in [[stage_in]], texture2d<float> tex [[texture(0)]]) {
281    constexpr sampler s(mag_filter::linear, min_filter::linear);
282    return tex.sample(s, in.texcoord);
283}
284
285// YCbCr to RGB conversion (BT.709 matrix for HD video)
286float4 ycbcr_to_rgb(float y, float2 cbcr, bool full_range) {
287    float y_adj = full_range ? y : (y - 16.0/255.0) * (255.0/219.0);
288    float2 cbcr_adj = full_range
289        ? cbcr - 0.5
290        : (cbcr - 16.0/255.0) * (255.0/224.0) - 0.5;
291    float cb = cbcr_adj.x;
292    float cr = cbcr_adj.y;
293    // BT.709 conversion matrix
294    float r = y_adj + 1.5748 * cr;
295    float g = y_adj - 0.1873 * cb - 0.4681 * cr;
296    float b = y_adj + 1.8556 * cb;
297    return float4(saturate(float3(r, g, b)), 1.0);
298}
299
300// YCbCr biplanar (420v/420f) fragment shader
301fragment float4 fragment_ycbcr(TexturedVertexOut in [[stage_in]], 
302    texture2d<float> y_tex [[texture(0)]], 
303    texture2d<float> cbcr_tex [[texture(1)]],
304    constant Uniforms& uniforms [[buffer(0)]]) {
305    constexpr sampler s(mag_filter::linear, min_filter::linear);
306    float y = y_tex.sample(s, in.texcoord).r;
307    float2 cbcr = cbcr_tex.sample(s, in.texcoord).rg;
308    bool full_range = (uniforms.pixel_format == 0x34323066); // '420f'
309    return ycbcr_to_rgb(y, cbcr, full_range);
310}
311
312// Colored vertex input/output for UI overlays
313struct ColoredVertex {
314    float2 position [[attribute(0)]];
315    float4 color [[attribute(1)]];
316};
317
318struct ColoredVertexOut {
319    float4 position [[position]];
320    float4 color;
321};
322
323// Colored vertex shader for UI elements (position in pixels, converted to NDC)
324vertex ColoredVertexOut vertex_colored(ColoredVertex in [[stage_in]], constant Uniforms& uniforms [[buffer(1)]]) {
325    ColoredVertexOut out;
326    float2 ndc = (in.position / uniforms.viewport_size) * 2.0 - 1.0;
327    ndc.y = -ndc.y;
328    out.position = float4(ndc, 0.0, 1.0);
329    out.color = in.color;
330    return out;
331}
332
333// Colored fragment shader for UI elements
334fragment float4 fragment_colored(ColoredVertexOut in [[stage_in]]) {
335    return in.color;
336}
337";
338
339/// Uniforms structure for Metal shaders
340///
341/// This matches the layout expected by `SHADER_SOURCE`.
342#[repr(C)]
343#[derive(Debug, Clone, Copy, Default)]
344pub struct Uniforms {
345    /// Viewport width and height
346    pub viewport_size: [f32; 2],
347    /// Texture width and height
348    pub texture_size: [f32; 2],
349    /// Animation time (optional)
350    pub time: f32,
351    /// Pixel format (raw u32 for GPU compatibility)
352    pub pixel_format: u32,
353    /// Padding for alignment
354    #[doc(hidden)]
355    pub _padding: [f32; 2],
356}
357
358impl Uniforms {
359    /// Encoded size of one uniforms value.
360    pub const BYTE_LEN: usize = 32;
361
362    /// Create uniforms for a given viewport and texture size
363    #[must_use]
364    pub fn new(
365        viewport_width: f32,
366        viewport_height: f32,
367        texture_width: f32,
368        texture_height: f32,
369    ) -> Self {
370        Self {
371            viewport_size: [viewport_width, viewport_height],
372            texture_size: [texture_width, texture_height],
373            time: 0.0,
374            pixel_format: 0,
375            _padding: [0.0; 2],
376        }
377    }
378
379    /// Create uniforms from viewport size and captured textures
380    ///
381    /// Automatically extracts texture dimensions and pixel format.
382    ///
383    /// # Example
384    ///
385    /// ```no_run
386    /// use screencapturekit::metal::{IOSurfaceMetalExt, MetalDevice, Uniforms};
387    /// use screencapturekit::cm::IOSurface;
388    ///
389    /// fn example(surface: &IOSurface, device: &MetalDevice) {
390    ///     if let Some(textures) = surface.create_metal_textures(device) {
391    ///         let uniforms = Uniforms::from_captured_textures(1920.0, 1080.0, &textures);
392    ///     }
393    /// }
394    /// ```
395    #[must_use]
396    #[allow(clippy::cast_precision_loss)] // Screen dimensions will fit in f32
397    pub fn from_captured_textures<T>(
398        viewport_width: f32,
399        viewport_height: f32,
400        textures: &CapturedTextures<T>,
401    ) -> Self {
402        Self {
403            viewport_size: [viewport_width, viewport_height],
404            texture_size: [textures.width as f32, textures.height as f32],
405            time: 0.0,
406            pixel_format: textures.pixel_format.as_u32(),
407            _padding: [0.0; 2],
408        }
409    }
410
411    /// Set the pixel format
412    ///
413    /// Accepts either a `FourCharCode` or a raw `u32`:
414    /// ```no_run
415    /// use screencapturekit::metal::{Uniforms, pixel_format};
416    ///
417    /// let uniforms = Uniforms::new(1920.0, 1080.0, 1920.0, 1080.0)
418    ///     .with_pixel_format(pixel_format::BGRA);
419    /// ```
420    #[must_use]
421    pub fn with_pixel_format(mut self, format: impl Into<FourCharCode>) -> Self {
422        self.pixel_format = format.into().as_u32();
423        self
424    }
425
426    /// Set the animation time
427    #[must_use]
428    pub fn with_time(mut self, time: f32) -> Self {
429        self.time = time;
430        self
431    }
432
433    /// Encode this value as initialized native-endian GPU bytes.
434    #[must_use]
435    #[allow(clippy::used_underscore_binding)]
436    pub fn to_bytes(self) -> [u8; Self::BYTE_LEN] {
437        let words = [
438            self.viewport_size[0].to_bits(),
439            self.viewport_size[1].to_bits(),
440            self.texture_size[0].to_bits(),
441            self.texture_size[1].to_bits(),
442            self.time.to_bits(),
443            self.pixel_format,
444            self._padding[0].to_bits(),
445            self._padding[1].to_bits(),
446        ];
447        let mut bytes = [0_u8; Self::BYTE_LEN];
448        for (chunk, word) in bytes.chunks_exact_mut(4).zip(words) {
449            chunk.copy_from_slice(&word.to_ne_bytes());
450        }
451        bytes
452    }
453}
454
455/// Errors raised before an invalid Metal operation reaches the native API.
456#[derive(Debug, Clone, PartialEq, Eq)]
457pub enum MetalError {
458    /// A Rust `usize` cannot be represented by Swift's signed `Int`.
459    SwiftIntOverflow {
460        /// Argument name.
461        argument: &'static str,
462        /// Rejected value.
463        value: usize,
464    },
465    /// A binding index exceeds the native slot count.
466    IndexOutOfRange {
467        /// Argument name.
468        argument: &'static str,
469        /// Rejected index.
470        index: usize,
471        /// Exclusive upper bound.
472        max_exclusive: usize,
473    },
474    /// The Swift bridge rejected an operation after defensive validation.
475    NativeCallRejected {
476        /// Operation name.
477        operation: &'static str,
478    },
479    /// The command buffer was already committed.
480    CommandBufferAlreadyCommitted,
481    /// A command encoder is still active on the command buffer.
482    CommandBufferHasActiveEncoder,
483    /// The command buffer already has an active render encoder.
484    RenderEncoderAlreadyActive,
485    /// Native render-encoder creation failed.
486    RenderEncoderCreationFailed,
487    /// The render encoder has already ended.
488    RenderEncoderEnded,
489    UnknownPixelFormat {
490        raw: u64,
491    },
492}
493
494impl std::fmt::Display for MetalError {
495    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
496        match self {
497            Self::SwiftIntOverflow { argument, value } => {
498                write!(f, "{argument} value {value} exceeds Swift Int")
499            }
500            Self::IndexOutOfRange {
501                argument,
502                index,
503                max_exclusive,
504            } => write!(f, "{argument} index {index} is outside 0..{max_exclusive}"),
505            Self::NativeCallRejected { operation } => {
506                write!(f, "native Metal rejected {operation}")
507            }
508            Self::CommandBufferAlreadyCommitted => f.write_str("command buffer already committed"),
509            Self::CommandBufferHasActiveEncoder => {
510                f.write_str("command buffer has an active encoder")
511            }
512            Self::RenderEncoderAlreadyActive => {
513                f.write_str("command buffer already has an active render encoder")
514            }
515            Self::RenderEncoderCreationFailed => {
516                f.write_str("native render command encoder creation failed")
517            }
518            Self::RenderEncoderEnded => f.write_str("render command encoder already ended"),
519            Self::UnknownPixelFormat { raw } => {
520                write!(
521                    f,
522                    "texture pixel format {raw} is not a known MetalPixelFormat"
523                )
524            }
525        }
526    }
527}
528
529impl std::error::Error for MetalError {}
530
531// MARK: - FFI Declarations
532
533#[link(name = "Metal", kind = "framework")]
534extern "C" {}
535
536#[link(name = "QuartzCore", kind = "framework")]
537extern "C" {}
538
539extern "C" {
540    // Device
541    fn metal_create_system_default_device() -> *mut c_void;
542    fn metal_device_release(device: *mut c_void);
543    fn metal_device_get_name(device: *mut c_void) -> *const std::ffi::c_char;
544    fn metal_device_retain(device: *mut c_void) -> *mut c_void;
545    fn metal_string_free(ptr: *mut std::ffi::c_char);
546    fn metal_device_create_command_queue(device: *mut c_void) -> *mut c_void;
547    fn metal_device_create_render_pipeline_state(
548        device: *mut c_void,
549        desc: *mut c_void,
550    ) -> *mut c_void;
551
552    // Texture
553    fn metal_create_texture_from_iosurface(
554        device: *mut c_void,
555        iosurface: *mut c_void,
556        plane: usize,
557        width: usize,
558        height: usize,
559        pixel_format: u64,
560    ) -> *mut c_void;
561    fn metal_texture_release(texture: *mut c_void);
562    fn metal_texture_retain(texture: *mut c_void) -> *mut c_void;
563    fn metal_texture_get_width(texture: *mut c_void) -> usize;
564    fn metal_texture_get_height(texture: *mut c_void) -> usize;
565    fn metal_texture_get_pixel_format(texture: *mut c_void) -> u64;
566
567    // Command Queue
568    fn metal_command_queue_release(queue: *mut c_void);
569    fn metal_command_queue_command_buffer(queue: *mut c_void) -> *mut c_void;
570
571    // Library/Function
572    fn metal_device_create_library_with_source(
573        device: *mut c_void,
574        source: *const std::ffi::c_char,
575        error_out: *mut *const std::ffi::c_char,
576    ) -> *mut c_void;
577    fn metal_library_release(library: *mut c_void);
578    fn metal_library_get_function(
579        library: *mut c_void,
580        name: *const std::ffi::c_char,
581    ) -> *mut c_void;
582    fn metal_function_release(function: *mut c_void);
583
584    // Buffer
585    fn metal_device_create_buffer(device: *mut c_void, length: usize, options: u64) -> *mut c_void;
586    fn metal_buffer_contents(buffer: *mut c_void) -> *mut c_void;
587    fn metal_buffer_length(buffer: *mut c_void) -> usize;
588    fn metal_buffer_did_modify_range(buffer: *mut c_void, location: usize, length: usize);
589    fn metal_buffer_release(buffer: *mut c_void);
590
591    // Layer
592    fn metal_layer_create() -> *mut c_void;
593    fn metal_layer_set_device(layer: *mut c_void, device: *mut c_void);
594    fn metal_layer_set_pixel_format(layer: *mut c_void, format: u64);
595    fn metal_layer_set_drawable_size(layer: *mut c_void, width: f64, height: f64);
596    fn metal_layer_set_presents_with_transaction(layer: *mut c_void, value: bool);
597    fn metal_layer_next_drawable(layer: *mut c_void) -> *mut c_void;
598    fn metal_layer_release(layer: *mut c_void);
599
600    // Drawable
601    fn metal_drawable_texture(drawable: *mut c_void) -> *mut c_void;
602    fn metal_drawable_release(drawable: *mut c_void);
603
604    // Command Buffer
605    fn metal_command_buffer_present_drawable(cmd_buffer: *mut c_void, drawable: *mut c_void);
606    fn metal_command_buffer_commit(cmd_buffer: *mut c_void);
607    fn metal_command_buffer_retain(cmd_buffer: *mut c_void) -> *mut c_void;
608    fn metal_command_buffer_release(cmd_buffer: *mut c_void);
609
610    // Render Pass
611    fn metal_render_pass_descriptor_create() -> *mut c_void;
612    fn metal_render_pass_set_color_attachment_texture(
613        desc: *mut c_void,
614        index: usize,
615        texture: *mut c_void,
616    ) -> bool;
617    fn metal_render_pass_set_color_attachment_load_action(
618        desc: *mut c_void,
619        index: usize,
620        action: u64,
621    ) -> bool;
622    fn metal_render_pass_set_color_attachment_store_action(
623        desc: *mut c_void,
624        index: usize,
625        action: u64,
626    ) -> bool;
627    fn metal_render_pass_set_color_attachment_clear_color(
628        desc: *mut c_void,
629        index: usize,
630        r: f64,
631        g: f64,
632        b: f64,
633        a: f64,
634    ) -> bool;
635    fn metal_render_pass_descriptor_release(desc: *mut c_void);
636
637    // Vertex Descriptor
638    fn metal_vertex_descriptor_create() -> *mut c_void;
639    fn metal_vertex_descriptor_set_attribute(
640        desc: *mut c_void,
641        index: usize,
642        format: u64,
643        offset: usize,
644        buffer_index: usize,
645    ) -> bool;
646    fn metal_vertex_descriptor_set_layout(
647        desc: *mut c_void,
648        buffer_index: usize,
649        stride: usize,
650        step_function: u64,
651    ) -> bool;
652    fn metal_vertex_descriptor_release(desc: *mut c_void);
653
654    // Render Pipeline Descriptor
655    fn metal_render_pipeline_descriptor_create() -> *mut c_void;
656    fn metal_render_pipeline_descriptor_set_vertex_function(
657        desc: *mut c_void,
658        function: *mut c_void,
659    );
660    fn metal_render_pipeline_descriptor_set_fragment_function(
661        desc: *mut c_void,
662        function: *mut c_void,
663    );
664    fn metal_render_pipeline_descriptor_set_vertex_descriptor(
665        desc: *mut c_void,
666        vertex_descriptor: *mut c_void,
667    );
668    fn metal_render_pipeline_descriptor_set_color_attachment_pixel_format(
669        desc: *mut c_void,
670        index: usize,
671        format: u64,
672    ) -> bool;
673    fn metal_render_pipeline_descriptor_set_blending_enabled(
674        desc: *mut c_void,
675        index: usize,
676        enabled: bool,
677    ) -> bool;
678    fn metal_render_pipeline_descriptor_set_blend_operations(
679        desc: *mut c_void,
680        index: usize,
681        rgb_op: u64,
682        alpha_op: u64,
683    ) -> bool;
684    fn metal_render_pipeline_descriptor_set_blend_factors(
685        desc: *mut c_void,
686        index: usize,
687        src_rgb: u64,
688        dst_rgb: u64,
689        src_alpha: u64,
690        dst_alpha: u64,
691    ) -> bool;
692    fn metal_render_pipeline_descriptor_release(desc: *mut c_void);
693    fn metal_render_pipeline_state_release(state: *mut c_void);
694
695    // Render Command Encoder
696    fn metal_command_buffer_render_command_encoder(
697        cmd_buffer: *mut c_void,
698        render_pass: *mut c_void,
699    ) -> *mut c_void;
700    fn metal_render_encoder_set_pipeline_state(encoder: *mut c_void, state: *mut c_void);
701    fn metal_render_encoder_set_vertex_buffer(
702        encoder: *mut c_void,
703        buffer: *mut c_void,
704        offset: usize,
705        index: usize,
706    ) -> bool;
707    fn metal_render_encoder_set_fragment_buffer(
708        encoder: *mut c_void,
709        buffer: *mut c_void,
710        offset: usize,
711        index: usize,
712    ) -> bool;
713    fn metal_render_encoder_set_fragment_texture(
714        encoder: *mut c_void,
715        texture: *mut c_void,
716        index: usize,
717    ) -> bool;
718    fn metal_render_encoder_draw_primitives(
719        encoder: *mut c_void,
720        primitive_type: u64,
721        vertex_start: usize,
722        vertex_count: usize,
723    ) -> bool;
724    fn metal_render_encoder_end_encoding(encoder: *mut c_void);
725    fn metal_render_encoder_retain(encoder: *mut c_void) -> *mut c_void;
726    fn metal_render_encoder_release(encoder: *mut c_void);
727
728    // NSView helpers
729    fn nsview_set_wants_layer(view: *mut c_void);
730    fn nsview_set_layer(view: *mut c_void, layer: *mut c_void);
731}
732
733// MARK: - Metal Device
734
735/// A Metal device (GPU)
736///
737/// This is a wrapper around `MTLDevice` that provides safe access to Metal functionality.
738#[derive(Debug)]
739pub struct MetalDevice {
740    ptr: NonNull<c_void>,
741    /// Whether this wrapper owns a `+1` reference that must be released on drop.
742    /// `false` for borrowed devices created via [`Self::from_ptr`].
743    owned: bool,
744}
745
746impl MetalDevice {
747    /// Get the system default Metal device
748    ///
749    /// Returns `None` if no Metal device is available.
750    #[must_use]
751    pub fn system_default() -> Option<Self> {
752        let ptr = unsafe { metal_create_system_default_device() };
753        NonNull::new(ptr).map(|ptr| Self { ptr, owned: true })
754    }
755
756    /// Create a `MetalDevice` from a raw `MTLDevice` pointer
757    ///
758    /// This is useful when you already have a device from another source
759    /// (e.g., the `metal` crate) and want to use it for texture creation.
760    ///
761    /// # Safety
762    ///
763    /// The pointer must be a valid `MTLDevice` pointer. The wrapper *borrows*
764    /// the device: it will NOT be released when this wrapper is dropped. Use
765    /// [`Self::from_ptr_retained`] if you want the wrapper to hold its own
766    /// reference.
767    #[must_use]
768    pub unsafe fn from_ptr(ptr: *mut c_void) -> Option<Self> {
769        NonNull::new(ptr).map(|ptr| Self { ptr, owned: false })
770    }
771
772    /// Create a `MetalDevice` from a raw `MTLDevice` pointer, retaining it
773    ///
774    /// The wrapper takes its own `+1` reference (via an Objective-C `retain`)
775    /// and releases it on drop, so the caller keeps ownership of their
776    /// reference.
777    ///
778    /// # Safety
779    ///
780    /// The pointer must be a valid `MTLDevice` pointer.
781    #[must_use]
782    pub unsafe fn from_ptr_retained(ptr: *mut c_void) -> Option<Self> {
783        if ptr.is_null() {
784            return None;
785        }
786        let retained = unsafe { metal_device_retain(ptr) };
787        NonNull::new(retained).map(|ptr| Self { ptr, owned: true })
788    }
789
790    /// Get the name of this device
791    #[must_use]
792    pub fn name(&self) -> String {
793        unsafe {
794            let name_ptr = metal_device_get_name(self.ptr.as_ptr());
795            if name_ptr.is_null() {
796                return String::new();
797            }
798            let name = CStr::from_ptr(name_ptr).to_string_lossy().into_owned();
799            // `metal_device_get_name` returns a malloc'd copy we own.
800            metal_string_free(name_ptr.cast_mut());
801            name
802        }
803    }
804
805    /// Create a command queue for this device
806    #[must_use]
807    pub fn create_command_queue(&self) -> Option<MetalCommandQueue> {
808        let ptr = unsafe { metal_device_create_command_queue(self.ptr.as_ptr()) };
809        NonNull::new(ptr).map(|ptr| MetalCommandQueue { ptr })
810    }
811
812    /// Create a shader library from source code
813    ///
814    /// # Errors
815    /// Returns an error message if shader compilation fails.
816    pub fn create_library_with_source(&self, source: &str) -> Result<MetalLibrary, String> {
817        use std::ffi::CString;
818        let source_c = CString::new(source).map_err(|e| e.to_string())?;
819        let mut error_ptr: *const std::ffi::c_char = std::ptr::null();
820
821        let ptr = unsafe {
822            metal_device_create_library_with_source(
823                self.ptr.as_ptr(),
824                source_c.as_ptr(),
825                &raw mut error_ptr,
826            )
827        };
828
829        NonNull::new(ptr).map_or_else(
830            || {
831                let error = if error_ptr.is_null() {
832                    "Unknown shader compilation error".to_string()
833                } else {
834                    let msg = unsafe { CStr::from_ptr(error_ptr).to_string_lossy().into_owned() };
835                    // `errorOut` is a malloc'd copy we own.
836                    unsafe { metal_string_free(error_ptr.cast_mut()) };
837                    msg
838                };
839                Err(error)
840            },
841            |ptr| Ok(MetalLibrary { ptr }),
842        )
843    }
844
845    /// Create a buffer
846    ///
847    /// Returns `None` if `length` exceeds `isize::MAX`: the Swift bridge takes
848    /// a signed `Int`, so such a length would arrive as a negative size.
849    #[must_use]
850    pub fn create_buffer(&self, length: usize, options: ResourceOptions) -> Option<MetalBuffer> {
851        let length = checked_swift_int(length)?;
852        let ptr = unsafe { metal_device_create_buffer(self.ptr.as_ptr(), length, options.0) };
853        NonNull::new(ptr).map(|ptr| MetalBuffer { ptr })
854    }
855
856    /// Create a buffer and populate it with initialized bytes.
857    ///
858    /// ```
859    /// use screencapturekit::metal::{MetalDevice, Uniforms};
860    ///
861    /// let device = MetalDevice::system_default().expect("Metal device");
862    /// let bytes = Uniforms::new(1920.0, 1080.0, 1280.0, 720.0).to_bytes();
863    /// let buffer = device.create_buffer_with_bytes(&bytes).expect("buffer");
864    /// assert_eq!(buffer.length(), Uniforms::BYTE_LEN);
865    /// ```
866    ///
867    #[must_use]
868    pub fn create_buffer_with_bytes(&self, data: &[u8]) -> Option<MetalBuffer> {
869        let buffer =
870            self.create_buffer(data.len(), ResourceOptions::CPU_CACHE_MODE_DEFAULT_CACHE)?;
871        if data.is_empty() {
872            return Some(buffer);
873        }
874        let destination = NonNull::new(buffer.contents().cast::<u8>())?;
875        unsafe {
876            std::ptr::copy_nonoverlapping(data.as_ptr(), destination.as_ptr(), data.len());
877        }
878        Some(buffer)
879    }
880
881    /// Create a buffer by copying the object representation of `data`.
882    ///
883    /// Prefer [`Self::create_buffer_with_bytes`] and an explicit encoder such
884    /// as [`Uniforms::to_bytes`].
885    ///
886    /// # Safety
887    ///
888    /// Every byte in `T`, including padding, must be initialized. Its layout
889    /// and native-endian representation must match the GPU consumer, and it
890    /// must not contain references, pointers, or ownership-bearing handles
891    /// that the GPU could interpret or outlive.
892    #[must_use]
893    pub unsafe fn create_buffer_with_data<T: Copy>(&self, data: &T) -> Option<MetalBuffer> {
894        let size = std::mem::size_of::<T>();
895        let bytes =
896            unsafe { std::slice::from_raw_parts(std::ptr::from_ref(data).cast::<u8>(), size) };
897        self.create_buffer_with_bytes(bytes)
898    }
899
900    /// Create a render pipeline state from a descriptor
901    #[must_use]
902    pub fn create_render_pipeline_state(
903        &self,
904        descriptor: &MetalRenderPipelineDescriptor,
905    ) -> Option<MetalRenderPipelineState> {
906        let ptr = unsafe {
907            metal_device_create_render_pipeline_state(self.ptr.as_ptr(), descriptor.as_ptr())
908        };
909        NonNull::new(ptr).map(|ptr| MetalRenderPipelineState { ptr })
910    }
911
912    /// Get the raw pointer to the underlying `MTLDevice`
913    #[must_use]
914    pub fn as_ptr(&self) -> *mut c_void {
915        self.ptr.as_ptr()
916    }
917
918    /// Borrow this device as an `apple_metal::MetalDevice` for interop with the
919    /// lightweight `apple-metal` crate.
920    ///
921    /// The returned guard references the same `MTLDevice` instance and does
922    /// not release it on drop, so it must not outlive this [`MetalDevice`].
923    /// That is enforced by the borrow: `apple_metal::ManuallyDropDevice` has no
924    /// lifetime of its own, so returning it directly let callers keep a
925    /// dangling `MTLDevice` handle after the owner was dropped.
926    ///
927    /// Deref to reach the `apple_metal::MetalDevice` API.
928    #[must_use]
929    pub fn as_apple_metal(&self) -> BorrowedAppleMetalDevice<'_> {
930        BorrowedAppleMetalDevice {
931            inner: unsafe { apple_metal::MetalDevice::from_raw_borrowed(self.ptr.as_ptr()) },
932            _owner: PhantomData,
933        }
934    }
935}
936
937/// An `apple_metal::MetalDevice` borrowed from an SCK [`MetalDevice`].
938///
939/// Returned by [`MetalDevice::as_apple_metal`]. Dereferences to
940/// [`apple_metal::MetalDevice`] and does not release the underlying
941/// `MTLDevice`; the borrow keeps the owning [`MetalDevice`] alive.
942pub struct BorrowedAppleMetalDevice<'a> {
943    inner: apple_metal::ManuallyDropDevice,
944    _owner: PhantomData<&'a MetalDevice>,
945}
946
947impl std::ops::Deref for BorrowedAppleMetalDevice<'_> {
948    type Target = apple_metal::MetalDevice;
949
950    fn deref(&self) -> &Self::Target {
951        &self.inner
952    }
953}
954
955impl std::fmt::Debug for BorrowedAppleMetalDevice<'_> {
956    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
957        f.debug_struct("BorrowedAppleMetalDevice").finish()
958    }
959}
960
961impl Drop for MetalDevice {
962    fn drop(&mut self) {
963        if self.owned {
964            unsafe { metal_device_release(self.ptr.as_ptr()) }
965        }
966    }
967}
968
969// SAFETY: `MTLDevice` is documented by Apple as thread-safe; the wrapper holds
970// only a retained pointer with atomic ObjC reference counting.
971unsafe impl Send for MetalDevice {}
972unsafe impl Sync for MetalDevice {}
973
974// MARK: - Metal Texture
975
976/// A Metal texture
977///
978/// This is a wrapper around `MTLTexture` that provides safe access.
979#[derive(Debug)]
980pub struct MetalTexture {
981    ptr: NonNull<c_void>,
982}
983
984impl MetalTexture {
985    /// Get the width of this texture
986    #[must_use]
987    pub fn width(&self) -> usize {
988        unsafe { metal_texture_get_width(self.ptr.as_ptr()) }
989    }
990
991    /// Get the height of this texture
992    #[must_use]
993    pub fn height(&self) -> usize {
994        unsafe { metal_texture_get_height(self.ptr.as_ptr()) }
995    }
996
997    /// Get the pixel format of this texture
998    #[allow(clippy::missing_errors_doc)]
999    pub fn pixel_format(&self) -> Result<MetalPixelFormat, MetalError> {
1000        let raw = unsafe { metal_texture_get_pixel_format(self.ptr.as_ptr()) };
1001        MetalPixelFormat::from_raw(raw).ok_or(MetalError::UnknownPixelFormat { raw })
1002    }
1003
1004    /// Get the raw pointer to the underlying `MTLTexture`
1005    #[must_use]
1006    pub fn as_ptr(&self) -> *mut c_void {
1007        self.ptr.as_ptr()
1008    }
1009}
1010
1011impl Clone for MetalTexture {
1012    fn clone(&self) -> Self {
1013        let ptr = unsafe { metal_texture_retain(self.ptr.as_ptr()) };
1014        // `metal_texture_retain` is an Objective-C `retain`, which returns the
1015        // same (non-null) pointer for a live object. Fall back to `self.ptr`
1016        // rather than panicking on the unreachable null case (Clone must be
1017        // infallible).
1018        Self {
1019            ptr: NonNull::new(ptr).unwrap_or(self.ptr),
1020        }
1021    }
1022}
1023
1024impl Drop for MetalTexture {
1025    fn drop(&mut self) {
1026        unsafe { metal_texture_release(self.ptr.as_ptr()) }
1027    }
1028}
1029
1030// SAFETY: `MTLTexture` is documented by Apple as thread-safe for the read-only
1031// access exposed here; the wrapper holds only a retained pointer with atomic
1032// ObjC reference counting.
1033unsafe impl Send for MetalTexture {}
1034unsafe impl Sync for MetalTexture {}
1035
1036// MARK: - Metal Command Queue
1037
1038/// A Metal command queue
1039#[derive(Debug)]
1040pub struct MetalCommandQueue {
1041    ptr: NonNull<c_void>,
1042}
1043
1044impl MetalCommandQueue {
1045    /// Create a command buffer
1046    #[must_use]
1047    pub fn command_buffer(&self) -> Option<MetalCommandBuffer> {
1048        let ptr = unsafe { metal_command_queue_command_buffer(self.ptr.as_ptr()) };
1049        NonNull::new(ptr).map(|ptr| MetalCommandBuffer {
1050            ptr,
1051            state: Arc::new(Mutex::new(CommandBufferState::default())),
1052        })
1053    }
1054
1055    /// Get the raw pointer to the underlying `MTLCommandQueue`
1056    #[must_use]
1057    pub fn as_ptr(&self) -> *mut c_void {
1058        self.ptr.as_ptr()
1059    }
1060}
1061
1062impl Drop for MetalCommandQueue {
1063    fn drop(&mut self) {
1064        unsafe { metal_command_queue_release(self.ptr.as_ptr()) }
1065    }
1066}
1067
1068// SAFETY: `MTLCommandQueue` is documented by Apple as thread-safe; the wrapper
1069// holds only a retained pointer with atomic ObjC reference counting.
1070unsafe impl Send for MetalCommandQueue {}
1071unsafe impl Sync for MetalCommandQueue {}
1072
1073// MARK: - Metal Library
1074
1075/// A Metal shader library
1076#[derive(Debug)]
1077pub struct MetalLibrary {
1078    ptr: NonNull<c_void>,
1079}
1080
1081impl MetalLibrary {
1082    /// Get a function from this library by name
1083    #[must_use]
1084    pub fn get_function(&self, name: &str) -> Option<MetalFunction> {
1085        use std::ffi::CString;
1086        let name_c = CString::new(name).ok()?;
1087        let ptr = unsafe { metal_library_get_function(self.ptr.as_ptr(), name_c.as_ptr()) };
1088        NonNull::new(ptr).map(|ptr| MetalFunction { ptr })
1089    }
1090
1091    /// Get the raw pointer to the underlying `MTLLibrary`
1092    #[must_use]
1093    pub fn as_ptr(&self) -> *mut c_void {
1094        self.ptr.as_ptr()
1095    }
1096}
1097
1098impl Drop for MetalLibrary {
1099    fn drop(&mut self) {
1100        unsafe { metal_library_release(self.ptr.as_ptr()) }
1101    }
1102}
1103
1104// SAFETY: `MTLLibrary` is documented by Apple as thread-safe; the wrapper holds
1105// only a retained pointer with atomic ObjC reference counting.
1106unsafe impl Send for MetalLibrary {}
1107unsafe impl Sync for MetalLibrary {}
1108
1109// MARK: - Metal Function
1110
1111/// A Metal shader function
1112#[derive(Debug)]
1113pub struct MetalFunction {
1114    ptr: NonNull<c_void>,
1115}
1116
1117impl MetalFunction {
1118    /// Get the raw pointer to the underlying `MTLFunction`
1119    #[must_use]
1120    pub fn as_ptr(&self) -> *mut c_void {
1121        self.ptr.as_ptr()
1122    }
1123}
1124
1125impl Drop for MetalFunction {
1126    fn drop(&mut self) {
1127        unsafe { metal_function_release(self.ptr.as_ptr()) }
1128    }
1129}
1130
1131// SAFETY: `MTLFunction` is documented by Apple as thread-safe; the wrapper holds
1132// only a retained pointer with atomic ObjC reference counting.
1133unsafe impl Send for MetalFunction {}
1134unsafe impl Sync for MetalFunction {}
1135
1136// MARK: - Metal Buffer
1137
1138/// A Metal buffer for vertex/uniform data
1139#[derive(Debug)]
1140pub struct MetalBuffer {
1141    ptr: NonNull<c_void>,
1142}
1143
1144/// Resource options for buffer creation
1145#[derive(Debug, Clone, Copy, Default)]
1146pub struct ResourceOptions(u64);
1147
1148impl ResourceOptions {
1149    /// CPU cache mode default, storage mode shared
1150    pub const CPU_CACHE_MODE_DEFAULT_CACHE: Self = Self(0);
1151    /// Storage mode shared (CPU and GPU can access)
1152    pub const STORAGE_MODE_SHARED: Self = Self(0);
1153    /// Storage mode managed (CPU writes, GPU reads)
1154    pub const STORAGE_MODE_MANAGED: Self = Self(1 << 4);
1155}
1156
1157impl MetalBuffer {
1158    /// Get a pointer to the buffer contents
1159    #[must_use]
1160    pub fn contents(&self) -> *mut c_void {
1161        unsafe { metal_buffer_contents(self.ptr.as_ptr()) }
1162    }
1163
1164    /// Get the length of the buffer in bytes
1165    #[must_use]
1166    pub fn length(&self) -> usize {
1167        unsafe { metal_buffer_length(self.ptr.as_ptr()) }
1168    }
1169
1170    /// Notify that a range of the buffer was modified (for managed storage mode)
1171    ///
1172    /// The call is skipped when the range cannot be expressed as the Swift
1173    /// bridge's signed `Int` pair — Swift reconstructs `start ..< end` and
1174    /// traps on an overflowing or reversed range.
1175    pub fn did_modify_range(&self, range: std::ops::Range<usize>) {
1176        if range.end < range.start
1177            || checked_swift_int(range.start).is_none()
1178            || checked_swift_int(range.end).is_none()
1179        {
1180            return;
1181        }
1182        unsafe { metal_buffer_did_modify_range(self.ptr.as_ptr(), range.start, range.len()) }
1183    }
1184
1185    /// Get the raw pointer
1186    #[must_use]
1187    pub fn as_ptr(&self) -> *mut c_void {
1188        self.ptr.as_ptr()
1189    }
1190}
1191
1192impl Drop for MetalBuffer {
1193    fn drop(&mut self) {
1194        unsafe { metal_buffer_release(self.ptr.as_ptr()) }
1195    }
1196}
1197
1198// SAFETY: `MTLBuffer` is documented by Apple as thread-safe. Note that `Sync`
1199// exposes `contents()` as a raw `*mut c_void`, not a Rust `&mut`; callers that
1200// write to the GPU buffer from multiple threads are responsible for their own
1201// synchronization (the standard GPU-programming contract).
1202unsafe impl Send for MetalBuffer {}
1203unsafe impl Sync for MetalBuffer {}
1204
1205// MARK: - Metal Layer
1206
1207/// A `CAMetalLayer` for rendering to a window
1208#[derive(Debug)]
1209pub struct MetalLayer {
1210    ptr: NonNull<c_void>,
1211}
1212
1213impl MetalLayer {
1214    /// Create a new Metal layer
1215    ///
1216    /// # Panics
1217    /// Panics if layer creation fails (should not happen on macOS with Metal support).
1218    #[must_use]
1219    pub fn new() -> Self {
1220        let ptr = unsafe { metal_layer_create() };
1221        Self {
1222            ptr: NonNull::new(ptr).expect("metal_layer_create returned null"),
1223        }
1224    }
1225
1226    /// Set the device for this layer
1227    pub fn set_device(&self, device: &MetalDevice) {
1228        unsafe { metal_layer_set_device(self.ptr.as_ptr(), device.as_ptr()) }
1229    }
1230
1231    /// Set the pixel format
1232    pub fn set_pixel_format(&self, format: MTLPixelFormat) {
1233        unsafe { metal_layer_set_pixel_format(self.ptr.as_ptr(), format.raw()) }
1234    }
1235
1236    /// Set the drawable size
1237    pub fn set_drawable_size(&self, width: f64, height: f64) {
1238        unsafe { metal_layer_set_drawable_size(self.ptr.as_ptr(), width, height) }
1239    }
1240
1241    /// Set whether to present with transaction
1242    pub fn set_presents_with_transaction(&self, value: bool) {
1243        unsafe { metal_layer_set_presents_with_transaction(self.ptr.as_ptr(), value) }
1244    }
1245
1246    /// Get the next drawable
1247    #[must_use]
1248    pub fn next_drawable(&self) -> Option<MetalDrawable> {
1249        let ptr = unsafe { metal_layer_next_drawable(self.ptr.as_ptr()) };
1250        NonNull::new(ptr).map(|ptr| MetalDrawable { ptr })
1251    }
1252
1253    /// Get the raw pointer (for attaching to a view)
1254    #[must_use]
1255    pub fn as_ptr(&self) -> *mut c_void {
1256        self.ptr.as_ptr()
1257    }
1258}
1259
1260impl Default for MetalLayer {
1261    fn default() -> Self {
1262        Self::new()
1263    }
1264}
1265
1266impl Drop for MetalLayer {
1267    fn drop(&mut self) {
1268        unsafe { metal_layer_release(self.ptr.as_ptr()) }
1269    }
1270}
1271
1272// MARK: - Metal Drawable
1273
1274/// A drawable from a Metal layer
1275#[derive(Debug)]
1276pub struct MetalDrawable {
1277    ptr: NonNull<c_void>,
1278}
1279
1280impl MetalDrawable {
1281    /// Get the texture for this drawable
1282    ///
1283    /// # Panics
1284    /// Panics if the drawable has no texture (should not happen for valid drawables).
1285    #[must_use]
1286    pub fn texture(&self) -> MetalTexture {
1287        let ptr = unsafe { metal_drawable_texture(self.ptr.as_ptr()) };
1288        // Null-check the borrowed pointer BEFORE retaining it.
1289        let ptr = NonNull::new(ptr).expect("drawable texture is null");
1290        // Texture is borrowed from the drawable; retain so MetalTexture owns it.
1291        let retained = unsafe { metal_texture_retain(ptr.as_ptr()) };
1292        MetalTexture {
1293            ptr: NonNull::new(retained).unwrap_or(ptr),
1294        }
1295    }
1296
1297    /// Get the raw pointer
1298    #[must_use]
1299    pub fn as_ptr(&self) -> *mut c_void {
1300        self.ptr.as_ptr()
1301    }
1302}
1303
1304impl Drop for MetalDrawable {
1305    fn drop(&mut self) {
1306        unsafe { metal_drawable_release(self.ptr.as_ptr()) }
1307    }
1308}
1309
1310// MARK: - Command Buffer
1311
1312#[derive(Debug, Default)]
1313struct CommandBufferState {
1314    committed: bool,
1315    encoder_active: bool,
1316}
1317
1318/// A Metal command buffer
1319#[derive(Debug)]
1320pub struct MetalCommandBuffer {
1321    ptr: NonNull<c_void>,
1322    state: Arc<Mutex<CommandBufferState>>,
1323}
1324
1325impl MetalCommandBuffer {
1326    /// Create a render command encoder
1327    ///
1328    /// # Errors
1329    ///
1330    /// Returns an error after commit, while another encoder is active, or
1331    /// when native encoder creation fails.
1332    pub fn render_command_encoder(
1333        &self,
1334        render_pass: &MetalRenderPassDescriptor,
1335    ) -> Result<MetalRenderCommandEncoder, MetalError> {
1336        let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
1337        if state.committed {
1338            return Err(MetalError::CommandBufferAlreadyCommitted);
1339        }
1340        if state.encoder_active {
1341            return Err(MetalError::RenderEncoderAlreadyActive);
1342        }
1343        let ptr = unsafe {
1344            metal_command_buffer_render_command_encoder(self.ptr.as_ptr(), render_pass.as_ptr())
1345        };
1346        let ptr = NonNull::new(ptr).ok_or(MetalError::RenderEncoderCreationFailed)?;
1347        state.encoder_active = true;
1348        drop(state);
1349        Ok(MetalRenderCommandEncoder {
1350            ptr,
1351            state: Arc::new(RenderEncoderState {
1352                ended: Mutex::new(false),
1353                command: Arc::clone(&self.state),
1354            }),
1355        })
1356    }
1357
1358    /// Present a drawable
1359    ///
1360    /// # Errors
1361    ///
1362    /// Returns an error if the command buffer was already committed.
1363    pub fn present_drawable(&self, drawable: &MetalDrawable) -> Result<(), MetalError> {
1364        let state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
1365        if state.committed {
1366            return Err(MetalError::CommandBufferAlreadyCommitted);
1367        }
1368        unsafe { metal_command_buffer_present_drawable(self.ptr.as_ptr(), drawable.as_ptr()) }
1369        drop(state);
1370        Ok(())
1371    }
1372
1373    /// Commit the command buffer
1374    ///
1375    /// # Errors
1376    ///
1377    /// Returns an error if it was already committed or an encoder is active.
1378    pub fn commit(&self) -> Result<(), MetalError> {
1379        let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
1380        if state.committed {
1381            return Err(MetalError::CommandBufferAlreadyCommitted);
1382        }
1383        if state.encoder_active {
1384            return Err(MetalError::CommandBufferHasActiveEncoder);
1385        }
1386        unsafe { metal_command_buffer_commit(self.ptr.as_ptr()) }
1387        state.committed = true;
1388        drop(state);
1389        Ok(())
1390    }
1391
1392    /// Get the raw pointer
1393    #[must_use]
1394    pub fn as_ptr(&self) -> *mut c_void {
1395        self.ptr.as_ptr()
1396    }
1397}
1398
1399impl Clone for MetalCommandBuffer {
1400    fn clone(&self) -> Self {
1401        let ptr = unsafe { metal_command_buffer_retain(self.ptr.as_ptr()) };
1402        Self {
1403            ptr: NonNull::new(ptr).unwrap_or(self.ptr),
1404            state: Arc::clone(&self.state),
1405        }
1406    }
1407}
1408
1409impl Drop for MetalCommandBuffer {
1410    fn drop(&mut self) {
1411        unsafe { metal_command_buffer_release(self.ptr.as_ptr()) }
1412    }
1413}
1414
1415// MARK: - Render Pass Descriptor
1416
1417/// A render pass descriptor
1418#[derive(Debug)]
1419pub struct MetalRenderPassDescriptor {
1420    ptr: NonNull<c_void>,
1421}
1422
1423/// Load action for render pass attachments
1424#[derive(Debug, Clone, Copy, Default)]
1425#[repr(u64)]
1426pub enum MTLLoadAction {
1427    /// Don't care about existing contents
1428    DontCare = 0,
1429    /// Load existing contents
1430    Load = 1,
1431    /// Clear to a value
1432    #[default]
1433    Clear = 2,
1434}
1435
1436/// Store action for render pass attachments
1437#[derive(Debug, Clone, Copy, Default)]
1438#[repr(u64)]
1439pub enum MTLStoreAction {
1440    /// Don't care about storing
1441    DontCare = 0,
1442    /// Store the results
1443    #[default]
1444    Store = 1,
1445}
1446
1447/// Pixel format
1448#[derive(Debug, Clone, Copy, Default)]
1449#[repr(u64)]
1450pub enum MTLPixelFormat {
1451    /// Invalid format
1452    Invalid = 0,
1453    /// BGRA 8-bit unsigned normalized
1454    #[default]
1455    BGRA8Unorm = 80,
1456    /// BGR 10-bit, A 2-bit unsigned normalized
1457    BGR10A2Unorm = 94,
1458    /// R 8-bit unsigned normalized
1459    R8Unorm = 10,
1460    /// RG 8-bit unsigned normalized
1461    RG8Unorm = 30,
1462}
1463
1464impl MTLPixelFormat {
1465    /// Get the raw value
1466    #[must_use]
1467    pub const fn raw(self) -> u64 {
1468        self as u64
1469    }
1470}
1471
1472/// Vertex format for vertex attributes
1473#[derive(Debug, Clone, Copy, Default)]
1474#[repr(u64)]
1475pub enum MTLVertexFormat {
1476    /// Invalid format
1477    Invalid = 0,
1478    /// Two 32-bit floats
1479    #[default]
1480    Float2 = 29,
1481    /// Three 32-bit floats
1482    Float3 = 30,
1483    /// Four 32-bit floats
1484    Float4 = 31,
1485}
1486
1487impl MTLVertexFormat {
1488    /// Get the raw value
1489    #[must_use]
1490    pub const fn raw(self) -> u64 {
1491        self as u64
1492    }
1493}
1494
1495/// Vertex step function
1496#[derive(Debug, Clone, Copy, Default)]
1497#[repr(u64)]
1498pub enum MTLVertexStepFunction {
1499    /// Constant value (same for all vertices)
1500    Constant = 0,
1501    /// Step once per vertex (default)
1502    #[default]
1503    PerVertex = 1,
1504    /// Step once per instance
1505    PerInstance = 2,
1506}
1507
1508impl MTLVertexStepFunction {
1509    /// Get the raw value
1510    #[must_use]
1511    pub const fn raw(self) -> u64 {
1512        self as u64
1513    }
1514}
1515
1516/// Primitive type for drawing
1517#[derive(Debug, Clone, Copy, Default)]
1518#[repr(u64)]
1519pub enum MTLPrimitiveType {
1520    /// Points
1521    Point = 0,
1522    /// Lines
1523    Line = 1,
1524    /// Line strip
1525    LineStrip = 2,
1526    /// Triangles
1527    #[default]
1528    Triangle = 3,
1529    /// Triangle strip
1530    TriangleStrip = 4,
1531}
1532
1533impl MTLPrimitiveType {
1534    /// Get the raw value
1535    #[must_use]
1536    pub const fn raw(self) -> u64 {
1537        self as u64
1538    }
1539}
1540
1541/// Blend operation
1542#[derive(Debug, Clone, Copy, Default)]
1543#[repr(u64)]
1544pub enum MTLBlendOperation {
1545    /// Add source and destination
1546    #[default]
1547    Add = 0,
1548    /// Subtract destination from source
1549    Subtract = 1,
1550    /// Subtract source from destination
1551    ReverseSubtract = 2,
1552    /// Minimum of source and destination
1553    Min = 3,
1554    /// Maximum of source and destination
1555    Max = 4,
1556}
1557
1558/// Blend factor
1559#[derive(Debug, Clone, Copy, Default)]
1560#[repr(u64)]
1561pub enum MTLBlendFactor {
1562    /// 0
1563    Zero = 0,
1564    /// 1
1565    #[default]
1566    One = 1,
1567    /// Source color
1568    SourceColor = 2,
1569    /// 1 - source color
1570    OneMinusSourceColor = 3,
1571    /// Source alpha
1572    SourceAlpha = 4,
1573    /// 1 - source alpha
1574    OneMinusSourceAlpha = 5,
1575    /// Destination color
1576    DestinationColor = 6,
1577    /// 1 - destination color
1578    OneMinusDestinationColor = 7,
1579    /// Destination alpha
1580    DestinationAlpha = 8,
1581    /// 1 - destination alpha
1582    OneMinusDestinationAlpha = 9,
1583}
1584
1585impl MetalRenderPassDescriptor {
1586    /// Create a new render pass descriptor
1587    ///
1588    /// # Panics
1589    /// Panics if descriptor creation fails (should not happen).
1590    #[must_use]
1591    pub fn new() -> Self {
1592        let ptr = unsafe { metal_render_pass_descriptor_create() };
1593        Self {
1594            ptr: NonNull::new(ptr).expect("render pass descriptor create failed"),
1595        }
1596    }
1597
1598    /// Set the texture for a color attachment
1599    ///
1600    /// # Errors
1601    ///
1602    /// Returns an error when `index` is not a native color-attachment slot.
1603    pub fn set_color_attachment_texture(
1604        &self,
1605        index: usize,
1606        texture: &MetalTexture,
1607    ) -> Result<(), MetalError> {
1608        let index = checked_slot("color attachment", index, MAX_COLOR_ATTACHMENTS)?;
1609        let accepted = unsafe {
1610            metal_render_pass_set_color_attachment_texture(
1611                self.ptr.as_ptr(),
1612                index,
1613                texture.as_ptr(),
1614            )
1615        };
1616        bridge_result(accepted, "set render-pass color texture")
1617    }
1618
1619    /// Set the load action for a color attachment
1620    ///
1621    /// # Errors
1622    ///
1623    /// Returns an error when `index` is not a native color-attachment slot.
1624    pub fn set_color_attachment_load_action(
1625        &self,
1626        index: usize,
1627        action: MTLLoadAction,
1628    ) -> Result<(), MetalError> {
1629        let index = checked_slot("color attachment", index, MAX_COLOR_ATTACHMENTS)?;
1630        let accepted = unsafe {
1631            metal_render_pass_set_color_attachment_load_action(
1632                self.ptr.as_ptr(),
1633                index,
1634                action as u64,
1635            )
1636        };
1637        bridge_result(accepted, "set render-pass load action")
1638    }
1639
1640    /// Set the store action for a color attachment
1641    ///
1642    /// # Errors
1643    ///
1644    /// Returns an error when `index` is not a native color-attachment slot.
1645    pub fn set_color_attachment_store_action(
1646        &self,
1647        index: usize,
1648        action: MTLStoreAction,
1649    ) -> Result<(), MetalError> {
1650        let index = checked_slot("color attachment", index, MAX_COLOR_ATTACHMENTS)?;
1651        let accepted = unsafe {
1652            metal_render_pass_set_color_attachment_store_action(
1653                self.ptr.as_ptr(),
1654                index,
1655                action as u64,
1656            )
1657        };
1658        bridge_result(accepted, "set render-pass store action")
1659    }
1660
1661    /// Set the clear color for a color attachment
1662    ///
1663    /// # Errors
1664    ///
1665    /// Returns an error when `index` is not a native color-attachment slot.
1666    pub fn set_color_attachment_clear_color(
1667        &self,
1668        index: usize,
1669        r: f64,
1670        g: f64,
1671        b: f64,
1672        a: f64,
1673    ) -> Result<(), MetalError> {
1674        let index = checked_slot("color attachment", index, MAX_COLOR_ATTACHMENTS)?;
1675        let accepted = unsafe {
1676            metal_render_pass_set_color_attachment_clear_color(self.ptr.as_ptr(), index, r, g, b, a)
1677        };
1678        bridge_result(accepted, "set render-pass clear color")
1679    }
1680
1681    /// Get the raw pointer
1682    #[must_use]
1683    pub fn as_ptr(&self) -> *mut c_void {
1684        self.ptr.as_ptr()
1685    }
1686}
1687
1688impl Default for MetalRenderPassDescriptor {
1689    fn default() -> Self {
1690        Self::new()
1691    }
1692}
1693
1694impl Drop for MetalRenderPassDescriptor {
1695    fn drop(&mut self) {
1696        unsafe { metal_render_pass_descriptor_release(self.ptr.as_ptr()) }
1697    }
1698}
1699
1700// MARK: - Vertex Descriptor
1701
1702/// A vertex descriptor for specifying vertex buffer layout
1703#[derive(Debug)]
1704pub struct MetalVertexDescriptor {
1705    ptr: NonNull<c_void>,
1706}
1707
1708impl MetalVertexDescriptor {
1709    /// Create a new vertex descriptor
1710    ///
1711    /// # Panics
1712    /// Panics if descriptor creation fails (should not happen).
1713    #[must_use]
1714    pub fn new() -> Self {
1715        let ptr = unsafe { metal_vertex_descriptor_create() };
1716        Self {
1717            ptr: NonNull::new(ptr).expect("vertex descriptor create failed"),
1718        }
1719    }
1720
1721    /// Set an attribute's format, offset, and buffer index
1722    ///
1723    /// # Errors
1724    ///
1725    /// Returns an error for an invalid attribute/buffer slot or an offset that
1726    /// cannot be represented by Swift's `Int`.
1727    pub fn set_attribute(
1728        &self,
1729        index: usize,
1730        format: MTLVertexFormat,
1731        offset: usize,
1732        buffer_index: usize,
1733    ) -> Result<(), MetalError> {
1734        let index = checked_slot("vertex attribute", index, MAX_VERTEX_ATTRIBUTES)?;
1735        let offset = checked_swift_value("vertex attribute offset", offset)?;
1736        let buffer_index = checked_slot("vertex buffer", buffer_index, MAX_BUFFER_BINDINGS)?;
1737        let accepted = unsafe {
1738            metal_vertex_descriptor_set_attribute(
1739                self.ptr.as_ptr(),
1740                index,
1741                format.raw(),
1742                offset,
1743                buffer_index,
1744            )
1745        };
1746        bridge_result(accepted, "set vertex attribute")
1747    }
1748
1749    /// Set a buffer layout's stride and step function
1750    ///
1751    /// # Errors
1752    ///
1753    /// Returns an error for an invalid buffer slot or a stride that cannot be
1754    /// represented by Swift's `Int`.
1755    pub fn set_layout(
1756        &self,
1757        buffer_index: usize,
1758        stride: usize,
1759        step_function: MTLVertexStepFunction,
1760    ) -> Result<(), MetalError> {
1761        let buffer_index = checked_slot("vertex buffer", buffer_index, MAX_BUFFER_BINDINGS)?;
1762        let stride = checked_swift_value("vertex stride", stride)?;
1763        let accepted = unsafe {
1764            metal_vertex_descriptor_set_layout(
1765                self.ptr.as_ptr(),
1766                buffer_index,
1767                stride,
1768                step_function.raw(),
1769            )
1770        };
1771        bridge_result(accepted, "set vertex layout")
1772    }
1773
1774    /// Get the raw pointer
1775    #[must_use]
1776    pub fn as_ptr(&self) -> *mut c_void {
1777        self.ptr.as_ptr()
1778    }
1779}
1780
1781impl Default for MetalVertexDescriptor {
1782    fn default() -> Self {
1783        Self::new()
1784    }
1785}
1786
1787impl Drop for MetalVertexDescriptor {
1788    fn drop(&mut self) {
1789        unsafe { metal_vertex_descriptor_release(self.ptr.as_ptr()) }
1790    }
1791}
1792
1793// MARK: - Render Pipeline Descriptor
1794
1795/// A render pipeline descriptor
1796#[derive(Debug)]
1797pub struct MetalRenderPipelineDescriptor {
1798    ptr: NonNull<c_void>,
1799}
1800
1801impl MetalRenderPipelineDescriptor {
1802    /// Create a new render pipeline descriptor
1803    ///
1804    /// # Panics
1805    /// Panics if descriptor creation fails (should not happen).
1806    #[must_use]
1807    pub fn new() -> Self {
1808        let ptr = unsafe { metal_render_pipeline_descriptor_create() };
1809        Self {
1810            ptr: NonNull::new(ptr).expect("render pipeline descriptor create failed"),
1811        }
1812    }
1813
1814    /// Set the vertex function
1815    pub fn set_vertex_function(&self, function: &MetalFunction) {
1816        unsafe {
1817            metal_render_pipeline_descriptor_set_vertex_function(
1818                self.ptr.as_ptr(),
1819                function.as_ptr(),
1820            );
1821        }
1822    }
1823
1824    /// Set the fragment function
1825    pub fn set_fragment_function(&self, function: &MetalFunction) {
1826        unsafe {
1827            metal_render_pipeline_descriptor_set_fragment_function(
1828                self.ptr.as_ptr(),
1829                function.as_ptr(),
1830            );
1831        }
1832    }
1833
1834    /// Set the vertex descriptor for vertex buffer layout
1835    pub fn set_vertex_descriptor(&self, descriptor: &MetalVertexDescriptor) {
1836        unsafe {
1837            metal_render_pipeline_descriptor_set_vertex_descriptor(
1838                self.ptr.as_ptr(),
1839                descriptor.as_ptr(),
1840            );
1841        }
1842    }
1843
1844    /// Set color attachment pixel format
1845    ///
1846    /// # Errors
1847    ///
1848    /// Returns an error when `index` is not a native color-attachment slot.
1849    pub fn set_color_attachment_pixel_format(
1850        &self,
1851        index: usize,
1852        format: MTLPixelFormat,
1853    ) -> Result<(), MetalError> {
1854        let index = checked_slot("color attachment", index, MAX_COLOR_ATTACHMENTS)?;
1855        let accepted = unsafe {
1856            metal_render_pipeline_descriptor_set_color_attachment_pixel_format(
1857                self.ptr.as_ptr(),
1858                index,
1859                format.raw(),
1860            )
1861        };
1862        bridge_result(accepted, "set pipeline color format")
1863    }
1864
1865    /// Set blending enabled for a color attachment
1866    ///
1867    /// # Errors
1868    ///
1869    /// Returns an error when `index` is not a native color-attachment slot.
1870    pub fn set_blending_enabled(&self, index: usize, enabled: bool) -> Result<(), MetalError> {
1871        let index = checked_slot("color attachment", index, MAX_COLOR_ATTACHMENTS)?;
1872        let accepted = unsafe {
1873            metal_render_pipeline_descriptor_set_blending_enabled(self.ptr.as_ptr(), index, enabled)
1874        };
1875        bridge_result(accepted, "set pipeline blending")
1876    }
1877
1878    /// Set blend operations
1879    ///
1880    /// # Errors
1881    ///
1882    /// Returns an error when `index` is not a native color-attachment slot.
1883    pub fn set_blend_operations(
1884        &self,
1885        index: usize,
1886        rgb_op: MTLBlendOperation,
1887        alpha_op: MTLBlendOperation,
1888    ) -> Result<(), MetalError> {
1889        let index = checked_slot("color attachment", index, MAX_COLOR_ATTACHMENTS)?;
1890        let accepted = unsafe {
1891            metal_render_pipeline_descriptor_set_blend_operations(
1892                self.ptr.as_ptr(),
1893                index,
1894                rgb_op as u64,
1895                alpha_op as u64,
1896            )
1897        };
1898        bridge_result(accepted, "set pipeline blend operations")
1899    }
1900
1901    /// Set blend factors
1902    ///
1903    /// # Errors
1904    ///
1905    /// Returns an error when `index` is not a native color-attachment slot.
1906    pub fn set_blend_factors(
1907        &self,
1908        index: usize,
1909        src_rgb: MTLBlendFactor,
1910        dst_rgb: MTLBlendFactor,
1911        src_alpha: MTLBlendFactor,
1912        dst_alpha: MTLBlendFactor,
1913    ) -> Result<(), MetalError> {
1914        let index = checked_slot("color attachment", index, MAX_COLOR_ATTACHMENTS)?;
1915        let accepted = unsafe {
1916            metal_render_pipeline_descriptor_set_blend_factors(
1917                self.ptr.as_ptr(),
1918                index,
1919                src_rgb as u64,
1920                dst_rgb as u64,
1921                src_alpha as u64,
1922                dst_alpha as u64,
1923            )
1924        };
1925        bridge_result(accepted, "set pipeline blend factors")
1926    }
1927
1928    /// Get the raw pointer
1929    #[must_use]
1930    pub fn as_ptr(&self) -> *mut c_void {
1931        self.ptr.as_ptr()
1932    }
1933}
1934
1935impl Default for MetalRenderPipelineDescriptor {
1936    fn default() -> Self {
1937        Self::new()
1938    }
1939}
1940
1941impl Drop for MetalRenderPipelineDescriptor {
1942    fn drop(&mut self) {
1943        unsafe { metal_render_pipeline_descriptor_release(self.ptr.as_ptr()) }
1944    }
1945}
1946
1947// MARK: - Render Pipeline State
1948
1949/// A compiled render pipeline state
1950#[derive(Debug)]
1951pub struct MetalRenderPipelineState {
1952    ptr: NonNull<c_void>,
1953}
1954
1955impl MetalRenderPipelineState {
1956    /// Get the raw pointer
1957    #[must_use]
1958    pub fn as_ptr(&self) -> *mut c_void {
1959        self.ptr.as_ptr()
1960    }
1961}
1962
1963impl Drop for MetalRenderPipelineState {
1964    fn drop(&mut self) {
1965        unsafe { metal_render_pipeline_state_release(self.ptr.as_ptr()) }
1966    }
1967}
1968
1969// SAFETY: `MTLRenderPipelineState` is documented by Apple as thread-safe; the
1970// wrapper holds only a retained pointer with atomic ObjC reference counting.
1971unsafe impl Send for MetalRenderPipelineState {}
1972unsafe impl Sync for MetalRenderPipelineState {}
1973
1974// MARK: - Render Command Encoder
1975
1976#[derive(Debug)]
1977struct RenderEncoderState {
1978    ended: Mutex<bool>,
1979    command: Arc<Mutex<CommandBufferState>>,
1980}
1981
1982impl RenderEncoderState {
1983    fn with_active(
1984        &self,
1985        operation: impl FnOnce() -> Result<(), MetalError>,
1986    ) -> Result<(), MetalError> {
1987        let ended = self.ended.lock().unwrap_or_else(PoisonError::into_inner);
1988        if *ended {
1989            return Err(MetalError::RenderEncoderEnded);
1990        }
1991        let result = operation();
1992        drop(ended);
1993        result
1994    }
1995
1996    fn end(&self, encoder: NonNull<c_void>) -> Result<(), MetalError> {
1997        let mut ended = self.ended.lock().unwrap_or_else(PoisonError::into_inner);
1998        if *ended {
1999            return Err(MetalError::RenderEncoderEnded);
2000        }
2001        unsafe { metal_render_encoder_end_encoding(encoder.as_ptr()) }
2002        *ended = true;
2003        drop(ended);
2004        let mut command = self.command.lock().unwrap_or_else(PoisonError::into_inner);
2005        command.encoder_active = false;
2006        drop(command);
2007        Ok(())
2008    }
2009
2010    fn end_on_drop(&self, encoder: NonNull<c_void>) {
2011        let mut ended = self.ended.lock().unwrap_or_else(PoisonError::into_inner);
2012        if *ended {
2013            return;
2014        }
2015        unsafe { metal_render_encoder_end_encoding(encoder.as_ptr()) }
2016        *ended = true;
2017        drop(ended);
2018        let mut command = self.command.lock().unwrap_or_else(PoisonError::into_inner);
2019        command.encoder_active = false;
2020        drop(command);
2021    }
2022}
2023
2024/// A render command encoder
2025///
2026/// Dropping the final retained handle ends encoding automatically so the
2027/// parent command buffer cannot be left permanently uncommittable.
2028#[derive(Debug)]
2029pub struct MetalRenderCommandEncoder {
2030    ptr: NonNull<c_void>,
2031    state: Arc<RenderEncoderState>,
2032}
2033
2034impl MetalRenderCommandEncoder {
2035    /// Set the render pipeline state
2036    ///
2037    /// # Errors
2038    ///
2039    /// Returns an error after encoding has ended.
2040    pub fn set_render_pipeline_state(
2041        &self,
2042        pipeline: &MetalRenderPipelineState,
2043    ) -> Result<(), MetalError> {
2044        self.state.with_active(|| {
2045            unsafe {
2046                metal_render_encoder_set_pipeline_state(self.ptr.as_ptr(), pipeline.as_ptr());
2047            }
2048            Ok(())
2049        })
2050    }
2051
2052    /// Set a vertex buffer
2053    ///
2054    /// # Errors
2055    ///
2056    /// Returns an error after encoding has ended or for an invalid offset or
2057    /// buffer binding index.
2058    pub fn set_vertex_buffer(
2059        &self,
2060        buffer: &MetalBuffer,
2061        offset: usize,
2062        index: usize,
2063    ) -> Result<(), MetalError> {
2064        self.state.with_active(|| {
2065            let offset = checked_swift_value("vertex buffer offset", offset)?;
2066            let index = checked_slot("vertex buffer", index, MAX_BUFFER_BINDINGS)?;
2067            let accepted = unsafe {
2068                metal_render_encoder_set_vertex_buffer(
2069                    self.ptr.as_ptr(),
2070                    buffer.as_ptr(),
2071                    offset,
2072                    index,
2073                )
2074            };
2075            bridge_result(accepted, "set vertex buffer")
2076        })
2077    }
2078
2079    /// Set a fragment buffer
2080    ///
2081    /// # Errors
2082    ///
2083    /// Returns an error after encoding has ended or for an invalid offset or
2084    /// buffer binding index.
2085    pub fn set_fragment_buffer(
2086        &self,
2087        buffer: &MetalBuffer,
2088        offset: usize,
2089        index: usize,
2090    ) -> Result<(), MetalError> {
2091        self.state.with_active(|| {
2092            let offset = checked_swift_value("fragment buffer offset", offset)?;
2093            let index = checked_slot("fragment buffer", index, MAX_BUFFER_BINDINGS)?;
2094            let accepted = unsafe {
2095                metal_render_encoder_set_fragment_buffer(
2096                    self.ptr.as_ptr(),
2097                    buffer.as_ptr(),
2098                    offset,
2099                    index,
2100                )
2101            };
2102            bridge_result(accepted, "set fragment buffer")
2103        })
2104    }
2105
2106    /// Set a fragment texture
2107    ///
2108    /// # Errors
2109    ///
2110    /// Returns an error after encoding has ended or for an invalid texture
2111    /// binding index.
2112    pub fn set_fragment_texture(
2113        &self,
2114        texture: &MetalTexture,
2115        index: usize,
2116    ) -> Result<(), MetalError> {
2117        self.state.with_active(|| {
2118            let index = checked_slot("fragment texture", index, MAX_TEXTURE_BINDINGS)?;
2119            let accepted = unsafe {
2120                metal_render_encoder_set_fragment_texture(
2121                    self.ptr.as_ptr(),
2122                    texture.as_ptr(),
2123                    index,
2124                )
2125            };
2126            bridge_result(accepted, "set fragment texture")
2127        })
2128    }
2129
2130    /// Draw primitives
2131    ///
2132    /// # Errors
2133    ///
2134    /// Returns an error after encoding has ended or when the vertex range
2135    /// cannot be represented by Swift's `Int`.
2136    pub fn draw_primitives(
2137        &self,
2138        primitive_type: MTLPrimitiveType,
2139        vertex_start: usize,
2140        vertex_count: usize,
2141    ) -> Result<(), MetalError> {
2142        self.state.with_active(|| {
2143            let vertex_start = checked_swift_value("vertex start", vertex_start)?;
2144            let vertex_count = checked_swift_value("vertex count", vertex_count)?;
2145            let accepted = unsafe {
2146                metal_render_encoder_draw_primitives(
2147                    self.ptr.as_ptr(),
2148                    primitive_type.raw(),
2149                    vertex_start,
2150                    vertex_count,
2151                )
2152            };
2153            bridge_result(accepted, "draw primitives")
2154        })
2155    }
2156
2157    /// End encoding
2158    ///
2159    /// # Errors
2160    ///
2161    /// Returns an error if encoding already ended through this handle or one
2162    /// of its retained clones.
2163    pub fn end_encoding(&self) -> Result<(), MetalError> {
2164        self.state.end(self.ptr)
2165    }
2166
2167    /// Get the raw pointer
2168    #[must_use]
2169    pub fn as_ptr(&self) -> *mut c_void {
2170        self.ptr.as_ptr()
2171    }
2172}
2173
2174impl Clone for MetalRenderCommandEncoder {
2175    fn clone(&self) -> Self {
2176        let ptr = unsafe { metal_render_encoder_retain(self.ptr.as_ptr()) };
2177        Self {
2178            ptr: NonNull::new(ptr).unwrap_or(self.ptr),
2179            state: Arc::clone(&self.state),
2180        }
2181    }
2182}
2183
2184impl Drop for MetalRenderCommandEncoder {
2185    fn drop(&mut self) {
2186        if Arc::strong_count(&self.state) == 1 {
2187            self.state.end_on_drop(self.ptr);
2188        }
2189        unsafe { metal_render_encoder_release(self.ptr.as_ptr()) }
2190    }
2191}
2192
2193// MARK: - IOSurface Metal Extension
2194
2195/// Result of creating Metal textures from an `IOSurface`
2196pub type MetalCapturedTextures = CapturedTextures<MetalTexture>;
2197
2198/// Extension trait that adds Metal-related convenience methods to
2199/// `apple_cf::iosurface::IOSurface`.
2200///
2201/// It's a trait (rather than inherent impls) because Rust's orphan rules
2202/// forbid inherent impls on out-of-crate types.
2203///
2204/// Bring this trait into scope to call `info()`, `texture_params()`,
2205/// `metal_textures(...)`, `create_metal_textures(...)`, etc. on any
2206/// `IOSurface`.
2207pub trait IOSurfaceMetalExt {
2208    /// Detailed information about this surface for Metal texture creation.
2209    fn info(&self) -> IOSurfaceInfo;
2210    /// Whether this surface uses a YCbCr biplanar format.
2211    fn is_ycbcr_biplanar(&self) -> bool;
2212    /// Texture params (one per plane) needed to create matching Metal textures.
2213    fn texture_params(&self) -> Vec<TextureParams>;
2214    /// Generic texture creation via user-supplied closure.
2215    fn metal_textures<T, F>(&self, create_texture: F) -> Option<CapturedTextures<T>>
2216    where
2217        F: Fn(&TextureParams, *const c_void) -> Option<T>;
2218    /// Convenience: create concrete `MetalTexture`s using a `MetalDevice`.
2219    fn create_metal_textures(&self, device: &MetalDevice) -> Option<MetalCapturedTextures>;
2220}
2221
2222impl IOSurfaceMetalExt for IOSurface {
2223    /// Get detailed information about this `IOSurface` for Metal texture creation
2224    fn info(&self) -> IOSurfaceInfo {
2225        let width = self.width();
2226        let height = self.height();
2227        let bytes_per_row = self.bytes_per_row();
2228        let pix_format: FourCharCode = self.pixel_format().into();
2229        let plane_count = self.plane_count();
2230
2231        let planes = if plane_count > 0 {
2232            (0..plane_count)
2233                .map(|i| PlaneInfo {
2234                    index: i,
2235                    width: self.width_of_plane(i),
2236                    height: self.height_of_plane(i),
2237                    bytes_per_row: self.bytes_per_row_of_plane(i),
2238                })
2239                .collect()
2240        } else {
2241            vec![]
2242        };
2243
2244        IOSurfaceInfo {
2245            width,
2246            height,
2247            bytes_per_row,
2248            pixel_format: pix_format,
2249            plane_count,
2250            planes,
2251        }
2252    }
2253
2254    /// Check if this `IOSurface` uses a YCbCr biplanar format
2255    fn is_ycbcr_biplanar(&self) -> bool {
2256        pixel_format::is_ycbcr_biplanar(self.pixel_format())
2257    }
2258
2259    /// Get texture parameters for creating Metal textures from this `IOSurface`
2260    ///
2261    /// Returns texture parameters for each plane needed to render this surface.
2262    /// - Single-plane formats (BGRA, L10R): Returns 1 texture param
2263    /// - YCbCr biplanar formats: Returns 2 texture params (Y and `CbCr` planes)
2264    fn texture_params(&self) -> Vec<TextureParams> {
2265        let pix_format: FourCharCode = self.pixel_format().into();
2266        let plane_count = self.plane_count();
2267
2268        if pix_format == pixel_format::BGRA {
2269            vec![TextureParams {
2270                width: self.width(),
2271                height: self.height(),
2272                format: MetalPixelFormat::BGRA8Unorm,
2273                plane: 0,
2274            }]
2275        } else if pix_format == pixel_format::L10R {
2276            vec![TextureParams {
2277                width: self.width(),
2278                height: self.height(),
2279                format: MetalPixelFormat::BGR10A2Unorm,
2280                plane: 0,
2281            }]
2282        } else if pixel_format::is_ycbcr_biplanar(pix_format) && plane_count >= 2 {
2283            vec![
2284                // Plane 0: Y (luminance) - R8Unorm
2285                TextureParams {
2286                    width: self.width_of_plane(0),
2287                    height: self.height_of_plane(0),
2288                    format: MetalPixelFormat::R8Unorm,
2289                    plane: 0,
2290                },
2291                // Plane 1: CbCr (chrominance) - RG8Unorm
2292                TextureParams {
2293                    width: self.width_of_plane(1),
2294                    height: self.height_of_plane(1),
2295                    format: MetalPixelFormat::RG8Unorm,
2296                    plane: 1,
2297                },
2298            ]
2299        } else {
2300            // Fallback to BGRA
2301            vec![TextureParams {
2302                width: self.width(),
2303                height: self.height(),
2304                format: MetalPixelFormat::BGRA8Unorm,
2305                plane: 0,
2306            }]
2307        }
2308    }
2309
2310    /// Create Metal textures from this `IOSurface` using a closure
2311    ///
2312    /// This is a zero-copy operation - the textures share memory with the `IOSurface`.
2313    ///
2314    /// The closure receives `TextureParams` and the raw `IOSurfaceRef` pointer,
2315    /// and should return the created texture.
2316    ///
2317    /// # Example
2318    ///
2319    /// ```no_run
2320    /// use screencapturekit::cm::IOSurface;
2321    /// use screencapturekit::metal::IOSurfaceMetalExt;
2322    /// use std::ffi::c_void;
2323    ///
2324    /// fn example(surface: &IOSurface) {
2325    ///     let textures = surface.metal_textures(|params, _iosurface_ptr| {
2326    ///         // Create Metal texture using params.width, params.height, params.format
2327    ///         // Return Some(texture) or None
2328    ///         Some(()) // placeholder
2329    ///     });
2330    ///
2331    ///     if let Some(textures) = textures {
2332    ///         if textures.is_ycbcr() {
2333    ///             // Use YCbCr shader with plane0 (Y) and plane1 (CbCr)
2334    ///         }
2335    ///     }
2336    /// }
2337    /// ```
2338    ///
2339    /// # Safety
2340    ///
2341    /// The closure receives a raw `IOSurfaceRef` pointer. The pointer is valid
2342    /// for the duration of the closure call.
2343    fn metal_textures<T, F>(&self, create_texture: F) -> Option<CapturedTextures<T>>
2344    where
2345        F: Fn(&TextureParams, *const c_void) -> Option<T>,
2346    {
2347        let width = self.width();
2348        let height = self.height();
2349        let pix_format: FourCharCode = self.pixel_format().into();
2350
2351        if width == 0 || height == 0 {
2352            return None;
2353        }
2354
2355        let iosurface_ptr = self.as_ptr();
2356        let params = self.texture_params();
2357
2358        if params.len() == 1 {
2359            // Single-plane format
2360            let texture = create_texture(&params[0], iosurface_ptr)?;
2361            Some(CapturedTextures {
2362                plane0: texture,
2363                plane1: None,
2364                pixel_format: pix_format,
2365                width,
2366                height,
2367            })
2368        } else if params.len() >= 2 {
2369            // YCbCr biplanar format
2370            let y_texture = create_texture(&params[0], iosurface_ptr)?;
2371            let uv_texture = create_texture(&params[1], iosurface_ptr)?;
2372            Some(CapturedTextures {
2373                plane0: y_texture,
2374                plane1: Some(uv_texture),
2375                pixel_format: pix_format,
2376                width,
2377                height,
2378            })
2379        } else {
2380            None
2381        }
2382    }
2383
2384    /// Create Metal textures from this `IOSurface` using the provided device
2385    ///
2386    /// This is a zero-copy operation - the textures share memory with the `IOSurface`.
2387    ///
2388    /// # Example
2389    ///
2390    /// ```no_run
2391    /// use screencapturekit::metal::{IOSurfaceMetalExt, MetalDevice};
2392    /// use screencapturekit::cm::IOSurface;
2393    ///
2394    /// fn example(surface: &IOSurface) {
2395    ///     let device = MetalDevice::system_default().expect("No Metal device");
2396    ///     if let Some(textures) = surface.create_metal_textures(&device) {
2397    ///         if textures.is_ycbcr() {
2398    ///             // Use YCbCr shader with plane0 (Y) and plane1 (CbCr)
2399    ///         }
2400    ///     }
2401    /// }
2402    /// ```
2403    fn create_metal_textures(&self, device: &MetalDevice) -> Option<MetalCapturedTextures> {
2404        let width = self.width();
2405        let height = self.height();
2406        let pix_format: FourCharCode = self.pixel_format().into();
2407
2408        if width == 0 || height == 0 {
2409            return None;
2410        }
2411
2412        let params = self.texture_params();
2413
2414        if params.len() == 1 {
2415            // Single-plane format
2416            let texture = create_texture_for_plane(self, device, &params[0])?;
2417            Some(CapturedTextures {
2418                plane0: texture,
2419                plane1: None,
2420                pixel_format: pix_format,
2421                width,
2422                height,
2423            })
2424        } else if params.len() >= 2 {
2425            // YCbCr biplanar format
2426            let y_texture = create_texture_for_plane(self, device, &params[0])?;
2427            let uv_texture = create_texture_for_plane(self, device, &params[1])?;
2428            Some(CapturedTextures {
2429                plane0: y_texture,
2430                plane1: Some(uv_texture),
2431                pixel_format: pix_format,
2432                width,
2433                height,
2434            })
2435        } else {
2436            None
2437        }
2438    }
2439}
2440
2441/// Private helper used by `create_metal_textures` to build a `MetalTexture`
2442/// for one plane of the surface. Was previously an inherent method on
2443/// `IOSurface`; lives here as a free function now that `IOSurface` is
2444/// defined in `apple-cf`.
2445fn create_texture_for_plane(
2446    surface: &IOSurface,
2447    device: &MetalDevice,
2448    params: &TextureParams,
2449) -> Option<MetalTexture> {
2450    // The Swift bridge takes `Int`; a plane/extent above `isize::MAX` would
2451    // arrive negative and index out of bounds inside Metal.
2452    checked_swift_int(params.plane)?;
2453    checked_swift_int(params.width)?;
2454    checked_swift_int(params.height)?;
2455    let ptr = unsafe {
2456        metal_create_texture_from_iosurface(
2457            device.as_ptr(),
2458            surface.as_ptr(),
2459            params.plane,
2460            params.width,
2461            params.height,
2462            params.format.raw(),
2463        )
2464    };
2465    NonNull::new(ptr).map(|ptr| MetalTexture { ptr })
2466}
2467
2468/// Reject `usize` values the Swift bridge cannot represent.
2469///
2470/// Every size/index parameter crosses the boundary as Swift's signed `Int`.
2471/// The two types are the same width, so anything above `isize::MAX` silently
2472/// becomes a negative length or index on the Swift side.
2473const fn checked_swift_int(value: usize) -> Option<usize> {
2474    if value > isize::MAX as usize {
2475        None
2476    } else {
2477        Some(value)
2478    }
2479}
2480
2481const MAX_COLOR_ATTACHMENTS: usize = 8;
2482const MAX_VERTEX_ATTRIBUTES: usize = 31;
2483const MAX_BUFFER_BINDINGS: usize = 31;
2484const MAX_TEXTURE_BINDINGS: usize = 128;
2485
2486fn checked_swift_value(argument: &'static str, value: usize) -> Result<usize, MetalError> {
2487    checked_swift_int(value).ok_or(MetalError::SwiftIntOverflow { argument, value })
2488}
2489
2490fn checked_slot(
2491    argument: &'static str,
2492    index: usize,
2493    max_exclusive: usize,
2494) -> Result<usize, MetalError> {
2495    let index = checked_swift_value(argument, index)?;
2496    if index >= max_exclusive {
2497        Err(MetalError::IndexOutOfRange {
2498            argument,
2499            index,
2500            max_exclusive,
2501        })
2502    } else {
2503        Ok(index)
2504    }
2505}
2506
2507fn bridge_result(accepted: bool, operation: &'static str) -> Result<(), MetalError> {
2508    if accepted {
2509        Ok(())
2510    } else {
2511        Err(MetalError::NativeCallRejected { operation })
2512    }
2513}
2514
2515// MARK: - Autorelease Pool
2516
2517#[link(name = "Foundation", kind = "framework")]
2518extern "C" {
2519    fn objc_autoreleasePoolPush() -> *mut c_void;
2520    fn objc_autoreleasePoolPop(pool: *mut c_void);
2521}
2522
2523/// RAII guard that pops the pushed autorelease pool on drop, including while
2524/// unwinding.
2525struct AutoreleasePoolGuard {
2526    pool: *mut c_void,
2527}
2528
2529impl Drop for AutoreleasePoolGuard {
2530    fn drop(&mut self) {
2531        unsafe { objc_autoreleasePoolPop(self.pool) }
2532    }
2533}
2534
2535/// Execute a closure within an autorelease pool
2536///
2537/// This is equivalent to `@autoreleasepool { ... }` in Objective-C/Swift.
2538/// Use this when running code that creates temporary Objective-C objects
2539/// that need to be released promptly.
2540///
2541/// The pool is popped by an RAII guard, so it is balanced even if `f` panics.
2542/// Leaking the push would corrupt the thread's pool stack for every later
2543/// pool on that thread, not just this one.
2544///
2545/// # Example
2546///
2547/// ```no_run
2548/// use screencapturekit::metal::autoreleasepool;
2549///
2550/// autoreleasepool(|| {
2551///     // Code that creates temporary Objective-C objects
2552///     println!("Inside autorelease pool");
2553/// });
2554/// ```
2555pub fn autoreleasepool<F, R>(f: F) -> R
2556where
2557    F: FnOnce() -> R,
2558{
2559    let _guard = AutoreleasePoolGuard {
2560        pool: unsafe { objc_autoreleasePoolPush() },
2561    };
2562    f()
2563}
2564
2565// MARK: - NSView Helpers
2566
2567/// Set up an `NSView` for Metal rendering
2568///
2569/// This sets `wantsLayer = YES` and assigns the Metal layer to the view.
2570///
2571/// # Safety
2572///
2573/// The `view` pointer must be a valid `NSView` pointer.
2574///
2575/// # Example
2576///
2577/// ```no_run
2578/// use screencapturekit::metal::{setup_metal_view, MetalLayer};
2579/// use std::ffi::c_void;
2580///
2581/// fn example(ns_view: *mut c_void) {
2582///     let layer = MetalLayer::new();
2583///     unsafe { setup_metal_view(ns_view, &layer); }
2584/// }
2585/// ```
2586pub unsafe fn setup_metal_view(view: *mut c_void, layer: &MetalLayer) {
2587    unsafe {
2588        nsview_set_wants_layer(view);
2589        nsview_set_layer(view, layer.as_ptr());
2590    }
2591}