Skip to main content

apple_mps/
filters.rs

1use crate::ffi;
2use crate::image::{Image, ImageRegion};
3use apple_metal::{CommandBuffer, MetalBuffer, MetalDevice, MetalTexture};
4use core::ffi::c_void;
5use core::ptr;
6
7macro_rules! opaque_handle {
8    ($name:ident) => {
9        pub struct $name {
10            ptr: *mut c_void,
11        }
12
13        // SAFETY: MPS filter handles are opaque pointers to thread-safe Swift/ObjC objects.
14        unsafe impl Send for $name {}
15        // SAFETY: MPS filter handles are opaque pointers to thread-safe Swift/ObjC objects.
16        unsafe impl Sync for $name {}
17
18        impl Drop for $name {
19            fn drop(&mut self) {
20                if !self.ptr.is_null() {
21                    // SAFETY: `ptr` is a +1 retained Swift/ObjC object pointer owned by this wrapper.
22                    unsafe { ffi::mps_object_release(self.ptr) };
23                    self.ptr = ptr::null_mut();
24                }
25            }
26        }
27
28        impl $name {
29            #[must_use]
30            pub const fn as_ptr(&self) -> *mut c_void {
31                self.ptr
32            }
33        }
34    };
35}
36
37macro_rules! impl_unary_methods {
38    ($name:ident) => {
39        impl $name {
40            /// Encode the filter against `MPSImage` inputs/outputs.
41            pub fn encode_image(
42                &self,
43                command_buffer: &CommandBuffer,
44                source: &Image,
45                destination: &Image,
46            ) {
47                // SAFETY: All handles come from safe wrappers and remain alive for the call.
48                unsafe {
49                    ffi::mps_unary_encode_image(
50                        self.ptr,
51                        command_buffer.as_ptr(),
52                        source.as_ptr(),
53                        destination.as_ptr(),
54                    )
55                };
56            }
57
58            /// Encode the filter directly against `MTLTexture` inputs/outputs.
59            pub fn encode_texture(
60                &self,
61                command_buffer: &CommandBuffer,
62                source: &MetalTexture,
63                destination: &MetalTexture,
64            ) {
65                // SAFETY: All handles come from safe wrappers and remain alive for the call.
66                unsafe {
67                    ffi::mps_unary_encode_texture(
68                        self.ptr,
69                        command_buffer.as_ptr(),
70                        source.as_ptr(),
71                        destination.as_ptr(),
72                    )
73                };
74            }
75
76            /// Configure the kernel's edge mode.
77            pub fn set_edge_mode(&self, edge_mode: usize) {
78                // SAFETY: The kernel pointer is valid for the duration of the call.
79                unsafe { ffi::mps_unary_set_edge_mode(self.ptr, edge_mode) };
80            }
81
82            /// Restrict writes to a destination clip rectangle.
83            pub fn set_clip_rect(&self, region: ImageRegion) {
84                // SAFETY: The kernel pointer is valid for the duration of the call.
85                unsafe {
86                    ffi::mps_unary_set_clip_rect(
87                        self.ptr,
88                        region.x,
89                        region.y,
90                        region.z,
91                        region.width,
92                        region.height,
93                        region.depth,
94                    )
95                };
96            }
97        }
98    };
99}
100
101macro_rules! impl_binary_methods {
102    ($name:ident) => {
103        impl $name {
104            /// Encode the filter against `MPSImage` inputs/outputs.
105            pub fn encode_image(
106                &self,
107                command_buffer: &CommandBuffer,
108                primary: &Image,
109                secondary: &Image,
110                destination: &Image,
111            ) {
112                // SAFETY: All handles come from safe wrappers and remain alive for the call.
113                unsafe {
114                    ffi::mps_binary_encode_image(
115                        self.ptr,
116                        command_buffer.as_ptr(),
117                        primary.as_ptr(),
118                        secondary.as_ptr(),
119                        destination.as_ptr(),
120                    )
121                };
122            }
123
124            /// Encode the filter directly against `MTLTexture` inputs/outputs.
125            pub fn encode_texture(
126                &self,
127                command_buffer: &CommandBuffer,
128                primary: &MetalTexture,
129                secondary: &MetalTexture,
130                destination: &MetalTexture,
131            ) {
132                // SAFETY: All handles come from safe wrappers and remain alive for the call.
133                unsafe {
134                    ffi::mps_binary_encode_texture(
135                        self.ptr,
136                        command_buffer.as_ptr(),
137                        primary.as_ptr(),
138                        secondary.as_ptr(),
139                        destination.as_ptr(),
140                    )
141                };
142            }
143
144            /// Configure the primary input edge mode.
145            pub fn set_primary_edge_mode(&self, edge_mode: usize) {
146                // SAFETY: The kernel pointer is valid for the duration of the call.
147                unsafe { ffi::mps_binary_set_primary_edge_mode(self.ptr, edge_mode) };
148            }
149
150            /// Configure the secondary input edge mode.
151            pub fn set_secondary_edge_mode(&self, edge_mode: usize) {
152                // SAFETY: The kernel pointer is valid for the duration of the call.
153                unsafe { ffi::mps_binary_set_secondary_edge_mode(self.ptr, edge_mode) };
154            }
155
156            /// Restrict writes to a destination clip rectangle.
157            pub fn set_clip_rect(&self, region: ImageRegion) {
158                // SAFETY: The kernel pointer is valid for the duration of the call.
159                unsafe {
160                    ffi::mps_binary_set_clip_rect(
161                        self.ptr,
162                        region.x,
163                        region.y,
164                        region.z,
165                        region.width,
166                        region.height,
167                        region.depth,
168                    )
169                };
170            }
171        }
172    };
173}
174
175/// `MPSScaleTransform` values used by resampling kernels.
176#[derive(Debug, Clone, Copy)]
177pub struct ScaleTransform {
178    pub scale_x: f64,
179    pub scale_y: f64,
180    pub translate_x: f64,
181    pub translate_y: f64,
182}
183
184/// Plain-Rust configuration for `MPSImageHistogramInfo`.
185#[derive(Debug, Clone, Copy)]
186pub struct HistogramInfo {
187    pub number_of_entries: usize,
188    pub histogram_for_alpha: bool,
189    pub min_pixel_value: [f32; 4],
190    pub max_pixel_value: [f32; 4],
191}
192
193opaque_handle!(ImageGaussianBlur);
194impl ImageGaussianBlur {
195    #[must_use]
196    pub fn new(device: &MetalDevice, sigma: f32) -> Option<Self> {
197        // SAFETY: `device` exposes a valid `MTLDevice` pointer.
198        let ptr = unsafe { ffi::mps_image_gaussian_blur_new(device.as_ptr(), sigma) };
199        if ptr.is_null() {
200            None
201        } else {
202            Some(Self { ptr })
203        }
204    }
205}
206impl_unary_methods!(ImageGaussianBlur);
207
208opaque_handle!(ImageBox);
209impl ImageBox {
210    #[must_use]
211    pub fn new(device: &MetalDevice, kernel_width: usize, kernel_height: usize) -> Option<Self> {
212        // SAFETY: `device` exposes a valid `MTLDevice` pointer.
213        let ptr = unsafe { ffi::mps_image_box_new(device.as_ptr(), kernel_width, kernel_height) };
214        if ptr.is_null() {
215            None
216        } else {
217            Some(Self { ptr })
218        }
219    }
220}
221impl_unary_methods!(ImageBox);
222
223opaque_handle!(ImageSobel);
224impl ImageSobel {
225    #[must_use]
226    pub fn new(device: &MetalDevice) -> Option<Self> {
227        // SAFETY: `device` exposes a valid `MTLDevice` pointer.
228        let ptr = unsafe { ffi::mps_image_sobel_new(device.as_ptr(), core::ptr::null()) };
229        if ptr.is_null() {
230            None
231        } else {
232            Some(Self { ptr })
233        }
234    }
235
236    #[must_use]
237    pub fn with_transform(device: &MetalDevice, transform: [f32; 3]) -> Option<Self> {
238        // SAFETY: `transform` lives for the duration of the FFI call.
239        let ptr = unsafe { ffi::mps_image_sobel_new(device.as_ptr(), transform.as_ptr()) };
240        if ptr.is_null() {
241            None
242        } else {
243            Some(Self { ptr })
244        }
245    }
246}
247impl_unary_methods!(ImageSobel);
248
249opaque_handle!(ImageMedian);
250impl ImageMedian {
251    #[must_use]
252    pub fn new(device: &MetalDevice, kernel_diameter: usize) -> Option<Self> {
253        // SAFETY: `device` exposes a valid `MTLDevice` pointer.
254        let ptr = unsafe { ffi::mps_image_median_new(device.as_ptr(), kernel_diameter) };
255        if ptr.is_null() {
256            None
257        } else {
258            Some(Self { ptr })
259        }
260    }
261}
262impl_unary_methods!(ImageMedian);
263
264opaque_handle!(ImageConvolution);
265impl ImageConvolution {
266    #[must_use]
267    pub fn new(
268        device: &MetalDevice,
269        kernel_width: usize,
270        kernel_height: usize,
271        weights: &[f32],
272    ) -> Option<Self> {
273        if weights.len() != kernel_width.saturating_mul(kernel_height) {
274            return None;
275        }
276
277        // SAFETY: `weights` lives for the duration of the FFI call.
278        let ptr = unsafe {
279            ffi::mps_image_convolution_new(
280                device.as_ptr(),
281                kernel_width,
282                kernel_height,
283                weights.as_ptr(),
284            )
285        };
286        if ptr.is_null() {
287            None
288        } else {
289            Some(Self { ptr })
290        }
291    }
292}
293impl_unary_methods!(ImageConvolution);
294
295opaque_handle!(ImageBilinearScale);
296impl ImageBilinearScale {
297    #[must_use]
298    pub fn new(device: &MetalDevice) -> Option<Self> {
299        // SAFETY: `device` exposes a valid `MTLDevice` pointer.
300        let ptr = unsafe { ffi::mps_image_bilinear_scale_new(device.as_ptr()) };
301        if ptr.is_null() {
302            None
303        } else {
304            Some(Self { ptr })
305        }
306    }
307
308    /// Override the default fit-to-destination scale transform.
309    pub fn set_scale_transform(&self, transform: ScaleTransform) {
310        // SAFETY: The kernel pointer is valid for the duration of the call.
311        unsafe {
312            ffi::mps_image_scale_set_transform(
313                self.ptr,
314                transform.scale_x,
315                transform.scale_y,
316                transform.translate_x,
317                transform.translate_y,
318            );
319        };
320    }
321}
322impl_unary_methods!(ImageBilinearScale);
323
324opaque_handle!(ImageLanczosScale);
325impl ImageLanczosScale {
326    #[must_use]
327    pub fn new(device: &MetalDevice) -> Option<Self> {
328        // SAFETY: `device` exposes a valid `MTLDevice` pointer.
329        let ptr = unsafe { ffi::mps_image_lanczos_scale_new(device.as_ptr()) };
330        if ptr.is_null() {
331            None
332        } else {
333            Some(Self { ptr })
334        }
335    }
336
337    /// Override the default fit-to-destination scale transform.
338    pub fn set_scale_transform(&self, transform: ScaleTransform) {
339        // SAFETY: The kernel pointer is valid for the duration of the call.
340        unsafe {
341            ffi::mps_image_scale_set_transform(
342                self.ptr,
343                transform.scale_x,
344                transform.scale_y,
345                transform.translate_x,
346                transform.translate_y,
347            );
348        };
349    }
350}
351impl_unary_methods!(ImageLanczosScale);
352
353opaque_handle!(ImageThresholdBinary);
354impl ImageThresholdBinary {
355    #[must_use]
356    pub fn new(device: &MetalDevice, threshold_value: f32, maximum_value: f32) -> Option<Self> {
357        // SAFETY: `device` exposes a valid `MTLDevice` pointer.
358        let ptr = unsafe {
359            ffi::mps_image_threshold_binary_new(
360                device.as_ptr(),
361                threshold_value,
362                maximum_value,
363                core::ptr::null(),
364            )
365        };
366        if ptr.is_null() {
367            None
368        } else {
369            Some(Self { ptr })
370        }
371    }
372
373    #[must_use]
374    pub fn with_transform(
375        device: &MetalDevice,
376        threshold_value: f32,
377        maximum_value: f32,
378        transform: [f32; 3],
379    ) -> Option<Self> {
380        // SAFETY: `transform` lives for the duration of the FFI call.
381        let ptr = unsafe {
382            ffi::mps_image_threshold_binary_new(
383                device.as_ptr(),
384                threshold_value,
385                maximum_value,
386                transform.as_ptr(),
387            )
388        };
389        if ptr.is_null() {
390            None
391        } else {
392            Some(Self { ptr })
393        }
394    }
395}
396impl_unary_methods!(ImageThresholdBinary);
397
398opaque_handle!(ImageHistogram);
399impl ImageHistogram {
400    #[must_use]
401    pub fn new(device: &MetalDevice, info: HistogramInfo) -> Option<Self> {
402        // SAFETY: `info` arrays live for the duration of the FFI call.
403        let ptr = unsafe {
404            ffi::mps_image_histogram_new(
405                device.as_ptr(),
406                info.number_of_entries,
407                info.histogram_for_alpha,
408                info.min_pixel_value.as_ptr(),
409                info.max_pixel_value.as_ptr(),
410            )
411        };
412        if ptr.is_null() {
413            None
414        } else {
415            Some(Self { ptr })
416        }
417    }
418
419    /// Encode a histogram pass using an `MPSImage` source.
420    pub fn encode_image(
421        &self,
422        command_buffer: &CommandBuffer,
423        source: &Image,
424        histogram_buffer: &MetalBuffer,
425        histogram_offset: usize,
426    ) {
427        // SAFETY: All handles come from safe wrappers and remain alive for the call.
428        unsafe {
429            ffi::mps_image_histogram_encode_image(
430                self.ptr,
431                command_buffer.as_ptr(),
432                source.as_ptr(),
433                histogram_buffer.as_ptr(),
434                histogram_offset,
435            );
436        };
437    }
438
439    /// Encode a histogram pass using a raw `MTLTexture` source.
440    pub fn encode_texture(
441        &self,
442        command_buffer: &CommandBuffer,
443        source: &MetalTexture,
444        histogram_buffer: &MetalBuffer,
445        histogram_offset: usize,
446    ) {
447        // SAFETY: All handles come from safe wrappers and remain alive for the call.
448        unsafe {
449            ffi::mps_image_histogram_encode_texture(
450                self.ptr,
451                command_buffer.as_ptr(),
452                source.as_ptr(),
453                histogram_buffer.as_ptr(),
454                histogram_offset,
455            );
456        };
457    }
458
459    /// Report the minimum output buffer size for the given source pixel format.
460    #[must_use]
461    pub fn histogram_size_for_source_format(&self, source_format: usize) -> usize {
462        // SAFETY: The histogram pointer is valid for the duration of the call.
463        unsafe { ffi::mps_image_histogram_size_for_source_format(self.ptr, source_format) }
464    }
465}
466
467opaque_handle!(ImageStatisticsMinAndMax);
468impl ImageStatisticsMinAndMax {
469    #[must_use]
470    pub fn new(device: &MetalDevice) -> Option<Self> {
471        // SAFETY: `device` exposes a valid `MTLDevice` pointer.
472        let ptr = unsafe { ffi::mps_image_statistics_min_max_new(device.as_ptr()) };
473        if ptr.is_null() {
474            None
475        } else {
476            Some(Self { ptr })
477        }
478    }
479}
480impl_unary_methods!(ImageStatisticsMinAndMax);
481
482opaque_handle!(ImageStatisticsMean);
483impl ImageStatisticsMean {
484    #[must_use]
485    pub fn new(device: &MetalDevice) -> Option<Self> {
486        // SAFETY: `device` exposes a valid `MTLDevice` pointer.
487        let ptr = unsafe { ffi::mps_image_statistics_mean_new(device.as_ptr()) };
488        if ptr.is_null() {
489            None
490        } else {
491            Some(Self { ptr })
492        }
493    }
494}
495impl_unary_methods!(ImageStatisticsMean);
496
497opaque_handle!(ImageReduceRowMin);
498impl ImageReduceRowMin {
499    #[must_use]
500    pub fn new(device: &MetalDevice) -> Option<Self> {
501        // SAFETY: `device` exposes a valid `MTLDevice` pointer.
502        let ptr = unsafe { ffi::mps_image_reduce_row_min_new(device.as_ptr()) };
503        if ptr.is_null() {
504            None
505        } else {
506            Some(Self { ptr })
507        }
508    }
509}
510impl_unary_methods!(ImageReduceRowMin);
511
512opaque_handle!(ImageReduceRowMax);
513impl ImageReduceRowMax {
514    #[must_use]
515    pub fn new(device: &MetalDevice) -> Option<Self> {
516        // SAFETY: `device` exposes a valid `MTLDevice` pointer.
517        let ptr = unsafe { ffi::mps_image_reduce_row_max_new(device.as_ptr()) };
518        if ptr.is_null() {
519            None
520        } else {
521            Some(Self { ptr })
522        }
523    }
524}
525impl_unary_methods!(ImageReduceRowMax);
526
527opaque_handle!(ImageReduceRowMean);
528impl ImageReduceRowMean {
529    #[must_use]
530    pub fn new(device: &MetalDevice) -> Option<Self> {
531        // SAFETY: `device` exposes a valid `MTLDevice` pointer.
532        let ptr = unsafe { ffi::mps_image_reduce_row_mean_new(device.as_ptr()) };
533        if ptr.is_null() {
534            None
535        } else {
536            Some(Self { ptr })
537        }
538    }
539}
540impl_unary_methods!(ImageReduceRowMean);
541
542opaque_handle!(ImageReduceRowSum);
543impl ImageReduceRowSum {
544    #[must_use]
545    pub fn new(device: &MetalDevice) -> Option<Self> {
546        // SAFETY: `device` exposes a valid `MTLDevice` pointer.
547        let ptr = unsafe { ffi::mps_image_reduce_row_sum_new(device.as_ptr()) };
548        if ptr.is_null() {
549            None
550        } else {
551            Some(Self { ptr })
552        }
553    }
554}
555impl_unary_methods!(ImageReduceRowSum);
556
557opaque_handle!(ImageAdd);
558impl ImageAdd {
559    #[must_use]
560    pub fn new(device: &MetalDevice) -> Option<Self> {
561        // SAFETY: `device` exposes a valid `MTLDevice` pointer.
562        let ptr = unsafe { ffi::mps_image_add_new(device.as_ptr()) };
563        if ptr.is_null() {
564            None
565        } else {
566            Some(Self { ptr })
567        }
568    }
569
570    /// Set `primaryScale`, `secondaryScale`, and `bias` in one call.
571    pub fn set_scales(&self, primary_scale: f32, secondary_scale: f32, bias: f32) {
572        // SAFETY: The kernel pointer is valid for the duration of the call.
573        unsafe {
574            ffi::mps_image_arithmetic_set_scales_bias(
575                self.ptr,
576                primary_scale,
577                secondary_scale,
578                bias,
579            );
580        };
581    }
582
583    /// Clamp arithmetic results to the closed interval `[minimum_value, maximum_value]`.
584    pub fn set_clamp(&self, minimum_value: f32, maximum_value: f32) {
585        // SAFETY: The kernel pointer is valid for the duration of the call.
586        unsafe { ffi::mps_image_arithmetic_set_clamp(self.ptr, minimum_value, maximum_value) };
587    }
588}
589impl_binary_methods!(ImageAdd);
590
591/// Convenience wrapper for `scale-and-add` semantics implemented with `MPSImageAdd`.
592pub struct ImageScaleAndAdd {
593    inner: ImageAdd,
594}
595
596impl ImageScaleAndAdd {
597    /// Build an image add kernel with non-unit primary/secondary scales.
598    #[must_use]
599    pub fn new(
600        device: &MetalDevice,
601        primary_scale: f32,
602        secondary_scale: f32,
603        bias: f32,
604    ) -> Option<Self> {
605        let inner = ImageAdd::new(device)?;
606        inner.set_scales(primary_scale, secondary_scale, bias);
607        Some(Self { inner })
608    }
609
610    #[must_use]
611    pub const fn as_ptr(&self) -> *mut c_void {
612        self.inner.as_ptr()
613    }
614
615    pub fn encode_image(
616        &self,
617        command_buffer: &CommandBuffer,
618        primary: &Image,
619        secondary: &Image,
620        destination: &Image,
621    ) {
622        self.inner
623            .encode_image(command_buffer, primary, secondary, destination);
624    }
625
626    pub fn encode_texture(
627        &self,
628        command_buffer: &CommandBuffer,
629        primary: &MetalTexture,
630        secondary: &MetalTexture,
631        destination: &MetalTexture,
632    ) {
633        self.inner
634            .encode_texture(command_buffer, primary, secondary, destination);
635    }
636
637    pub fn set_primary_edge_mode(&self, edge_mode: usize) {
638        self.inner.set_primary_edge_mode(edge_mode);
639    }
640
641    pub fn set_secondary_edge_mode(&self, edge_mode: usize) {
642        self.inner.set_secondary_edge_mode(edge_mode);
643    }
644
645    pub fn set_clip_rect(&self, region: ImageRegion) {
646        self.inner.set_clip_rect(region);
647    }
648
649    pub fn set_clamp(&self, minimum_value: f32, maximum_value: f32) {
650        self.inner.set_clamp(minimum_value, maximum_value);
651    }
652}