Skip to main content

grafting_frame/
lib.rs

1// Copyright 2026 Mark Alan Boykin
2// This Source Code Form is subject to the terms of the Mozilla Public
3// License, v. 2.0. If a copy of the MPL was not distributed with this
4// file, You can obtain one at https://mozilla.org/MPL/2.0/.
5// SPDX-License-Identifier: MPL-2.0
6
7//! Move-owned native image and synchronization custody.
8//!
9//! This crate is the producer/host boundary beneath `grafting`. It deliberately
10//! has no dependency on wgpu, wgpu-hal, Vulkan bindings, or an engine contract.
11//! Import policy and GPU API transitions belong in an importer above this crate.
12
13use std::fmt;
14
15/// Pixel dimensions of one producer frame.
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub struct FrameSize {
18    pub width: u32,
19    pub height: u32,
20}
21
22impl FrameSize {
23    pub const fn new(width: u32, height: u32) -> Self {
24        Self { width, height }
25    }
26}
27
28/// Four-channel, eight-bit pixel layouts supported by the native-frame seam.
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
30#[non_exhaustive]
31pub enum PixelFormat {
32    Rgba8Unorm,
33    Rgba8UnormSrgb,
34    Bgra8Unorm,
35    Bgra8UnormSrgb,
36}
37
38/// Resource-independent facts that remain valid after an image is imported.
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub struct FrameMetadata {
41    pub size: FrameSize,
42    pub format: PixelFormat,
43    pub generation: u64,
44}
45
46/// Stable discriminator for host routing before a platform downcast.
47#[derive(Clone, Copy, Debug, Eq, PartialEq)]
48#[non_exhaustive]
49pub enum OwnedNativeFrameKind {
50    Dx12SharedTexture,
51    DmaBufImage,
52    MetalTexture,
53    IoSurface,
54}
55
56/// One move-owned producer frame.
57///
58/// Dropping a frame releases every image and synchronization resource still in
59/// Rust custody. Importers take this value by value when an operating-system or
60/// driver import consumes any contained descriptor.
61pub struct OwnedNativeFrame {
62    metadata: FrameMetadata,
63    image: OwnedNativeImage,
64    sync: FrameSync,
65}
66
67impl OwnedNativeFrame {
68    pub fn new(metadata: FrameMetadata, image: OwnedNativeImage, sync: FrameSync) -> Self {
69        Self {
70            metadata,
71            image,
72            sync,
73        }
74    }
75
76    pub fn metadata(&self) -> FrameMetadata {
77        self.metadata
78    }
79    pub fn kind(&self) -> OwnedNativeFrameKind {
80        self.image.kind()
81    }
82    pub fn image(&self) -> &OwnedNativeImage {
83        &self.image
84    }
85    pub fn sync(&self) -> &FrameSync {
86        &self.sync
87    }
88    pub fn into_parts(self) -> (FrameMetadata, OwnedNativeImage, FrameSync) {
89        (self.metadata, self.image, self.sync)
90    }
91}
92
93impl fmt::Debug for OwnedNativeFrame {
94    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
95        formatter
96            .debug_struct("OwnedNativeFrame")
97            .field("metadata", &self.metadata)
98            .field("kind", &self.kind())
99            .field("sync", &self.sync)
100            .finish_non_exhaustive()
101    }
102}
103
104/// Platform image custody. Variants exist only where their native ownership
105/// type exists.
106#[non_exhaustive]
107pub enum OwnedNativeImage {
108    #[cfg(target_os = "windows")]
109    Dx12SharedTexture(Dx12SharedTexture),
110    #[cfg(target_os = "linux")]
111    DmaBufImage(DmaBufImage),
112    #[cfg(target_vendor = "apple")]
113    MetalTexture(MetalTexture),
114    #[cfg(target_vendor = "apple")]
115    IoSurface(IoSurface),
116}
117
118impl OwnedNativeImage {
119    pub fn kind(&self) -> OwnedNativeFrameKind {
120        // Match the value: on targets with no native variants the value is
121        // uninhabited, while a reference is still considered inhabited.
122        match *self {
123            #[cfg(target_os = "windows")]
124            Self::Dx12SharedTexture(_) => OwnedNativeFrameKind::Dx12SharedTexture,
125            #[cfg(target_os = "linux")]
126            Self::DmaBufImage(_) => OwnedNativeFrameKind::DmaBufImage,
127            #[cfg(target_vendor = "apple")]
128            Self::MetalTexture(_) => OwnedNativeFrameKind::MetalTexture,
129            #[cfg(target_vendor = "apple")]
130            Self::IoSurface(_) => OwnedNativeFrameKind::IoSurface,
131        }
132    }
133}
134
135/// Synchronization custody paired with an image.
136///
137/// `None` means the producer and consumer have another documented ordering
138/// relationship, such as a shared queue. GL flush policy remains in `grafting`
139/// because it is not an owned native synchronization resource.
140#[derive(Debug)]
141#[non_exhaustive]
142pub enum FrameSync {
143    None,
144    #[cfg(target_os = "windows")]
145    Dx12Fence {
146        fence: Dx12SharedFence,
147        value: u64,
148    },
149    #[cfg(target_os = "linux")]
150    VulkanSemaphore(std::os::fd::OwnedFd),
151    #[cfg(target_vendor = "apple")]
152    MetalSharedEvent {
153        event: objc2::rc::Retained<objc2::runtime::ProtocolObject<dyn objc2_metal::MTLSharedEvent>>,
154        value: u64,
155    },
156}
157
158#[cfg(target_os = "linux")]
159mod linux {
160    use std::os::fd::{FromRawFd, OwnedFd, RawFd};
161
162    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
163    pub struct DmaBufPlane {
164        pub buffer_index: u32,
165        pub offset: u64,
166        pub stride: u64,
167    }
168
169    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
170    #[non_exhaustive]
171    pub enum DmaBufError {
172        NoBuffers,
173        NoPlanes,
174        InvalidBufferIndex { plane: usize, buffer_index: u32 },
175        ZeroStride { plane: usize },
176    }
177
178    impl std::fmt::Display for DmaBufError {
179        fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180            match self {
181                Self::NoBuffers => formatter.write_str("DMABUF image has no owned buffers"),
182                Self::NoPlanes => formatter.write_str("DMABUF image has no planes"),
183                Self::InvalidBufferIndex {
184                    plane,
185                    buffer_index,
186                } => write!(
187                    formatter,
188                    "DMABUF plane {plane} references absent buffer {buffer_index}"
189                ),
190                Self::ZeroStride { plane } => {
191                    write!(formatter, "DMABUF plane {plane} has a zero stride")
192                }
193            }
194        }
195    }
196
197    impl std::error::Error for DmaBufError {}
198
199    /// Move-owned DMABUF descriptor table and DRM plane layout. Several planes
200    /// may reference one buffer entry.
201    pub struct DmaBufImage {
202        buffers: Vec<OwnedFd>,
203        planes: Vec<DmaBufPlane>,
204        drm_format: u32,
205        drm_modifier: u64,
206    }
207
208    impl DmaBufImage {
209        pub fn new(
210            buffers: Vec<OwnedFd>,
211            planes: Vec<DmaBufPlane>,
212            drm_format: u32,
213            drm_modifier: u64,
214        ) -> Result<Self, DmaBufError> {
215            if buffers.is_empty() {
216                return Err(DmaBufError::NoBuffers);
217            }
218            if planes.is_empty() {
219                return Err(DmaBufError::NoPlanes);
220            }
221            for (plane_index, plane) in planes.iter().enumerate() {
222                if plane.buffer_index as usize >= buffers.len() {
223                    return Err(DmaBufError::InvalidBufferIndex {
224                        plane: plane_index,
225                        buffer_index: plane.buffer_index,
226                    });
227                }
228                if plane.stride == 0 {
229                    return Err(DmaBufError::ZeroStride { plane: plane_index });
230                }
231            }
232            Ok(Self {
233                buffers,
234                planes,
235                drm_format,
236                drm_modifier,
237            })
238        }
239
240        /// # Safety
241        /// Every entry in `buffers` must be a valid, open descriptor uniquely
242        /// owned by the caller. This takes responsibility for closing all of them.
243        pub unsafe fn from_raw_owned_parts(
244            buffers: Vec<RawFd>,
245            planes: Vec<DmaBufPlane>,
246            drm_format: u32,
247            drm_modifier: u64,
248        ) -> Result<Self, DmaBufError> {
249            let buffers = buffers
250                .into_iter()
251                .map(|fd| unsafe { OwnedFd::from_raw_fd(fd) })
252                .collect();
253            Self::new(buffers, planes, drm_format, drm_modifier)
254        }
255
256        pub fn buffers(&self) -> &[OwnedFd] {
257            &self.buffers
258        }
259        pub fn planes(&self) -> &[DmaBufPlane] {
260            &self.planes
261        }
262        pub fn drm_format(&self) -> u32 {
263            self.drm_format
264        }
265        pub fn drm_modifier(&self) -> u64 {
266            self.drm_modifier
267        }
268        pub fn into_parts(self) -> (Vec<OwnedFd>, Vec<DmaBufPlane>, u32, u64) {
269            (
270                self.buffers,
271                self.planes,
272                self.drm_format,
273                self.drm_modifier,
274            )
275        }
276        pub fn into_buffers(self) -> Vec<OwnedFd> {
277            self.buffers
278        }
279    }
280}
281
282#[cfg(target_os = "linux")]
283pub use linux::{DmaBufError, DmaBufImage, DmaBufPlane};
284
285#[cfg(target_os = "windows")]
286mod windows_frame {
287    use std::{
288        os::windows::io::{AsHandle, BorrowedHandle, OwnedHandle},
289        sync::{
290            Arc,
291            atomic::{AtomicU64, Ordering},
292        },
293    };
294
295    static NEXT_ALLOCATION_KEY: AtomicU64 = AtomicU64::new(1);
296    static NEXT_FENCE_KEY: AtomicU64 = AtomicU64::new(1);
297
298    #[derive(Clone, Debug)]
299    pub struct Dx12SharedResource {
300        handle: Arc<OwnedHandle>,
301        allocation_key: u64,
302    }
303
304    impl Dx12SharedResource {
305        pub fn from_owned_handle(handle: OwnedHandle) -> Self {
306            Self {
307                handle: Arc::new(handle),
308                allocation_key: NEXT_ALLOCATION_KEY.fetch_add(1, Ordering::Relaxed),
309            }
310        }
311        pub fn allocation_key(&self) -> u64 {
312            self.allocation_key
313        }
314        pub fn as_handle(&self) -> BorrowedHandle<'_> {
315            self.handle.as_handle()
316        }
317    }
318
319    #[derive(Debug)]
320    pub struct Dx12SharedTexture {
321        resource: Dx12SharedResource,
322    }
323
324    impl Dx12SharedTexture {
325        pub fn new(resource: Dx12SharedResource) -> Self {
326            Self { resource }
327        }
328        pub fn allocation_key(&self) -> u64 {
329            self.resource.allocation_key()
330        }
331        pub fn resource(&self) -> &Dx12SharedResource {
332            &self.resource
333        }
334        pub fn into_resource(self) -> Dx12SharedResource {
335            self.resource
336        }
337    }
338
339    #[derive(Clone, Debug)]
340    pub struct Dx12SharedFence {
341        handle: Arc<OwnedHandle>,
342        fence_key: u64,
343    }
344
345    impl Dx12SharedFence {
346        pub fn from_owned_handle(handle: OwnedHandle) -> Self {
347            Self {
348                handle: Arc::new(handle),
349                fence_key: NEXT_FENCE_KEY.fetch_add(1, Ordering::Relaxed),
350            }
351        }
352        pub fn fence_key(&self) -> u64 {
353            self.fence_key
354        }
355        pub fn as_handle(&self) -> BorrowedHandle<'_> {
356            self.handle.as_handle()
357        }
358    }
359}
360
361#[cfg(target_os = "windows")]
362pub use windows_frame::{Dx12SharedFence, Dx12SharedResource, Dx12SharedTexture};
363
364#[cfg(target_vendor = "apple")]
365mod apple {
366    use objc2::{rc::Retained, runtime::ProtocolObject};
367    use objc2_core_foundation::CFRetained;
368    use objc2_io_surface::IOSurfaceRef;
369    use objc2_metal::MTLTexture;
370
371    pub struct MetalTexture {
372        texture: Retained<ProtocolObject<dyn MTLTexture>>,
373    }
374    impl MetalTexture {
375        pub fn new(texture: Retained<ProtocolObject<dyn MTLTexture>>) -> Self {
376            Self { texture }
377        }
378        pub fn texture(&self) -> &ProtocolObject<dyn MTLTexture> {
379            &self.texture
380        }
381        pub fn into_retained(self) -> Retained<ProtocolObject<dyn MTLTexture>> {
382            self.texture
383        }
384    }
385
386    pub struct IoSurface {
387        surface: CFRetained<IOSurfaceRef>,
388    }
389    impl IoSurface {
390        pub fn new(surface: CFRetained<IOSurfaceRef>) -> Self {
391            Self { surface }
392        }
393        pub fn surface(&self) -> &IOSurfaceRef {
394            &self.surface
395        }
396        pub fn into_retained(self) -> CFRetained<IOSurfaceRef> {
397            self.surface
398        }
399    }
400}
401
402#[cfg(target_vendor = "apple")]
403pub use apple::{IoSurface, MetalTexture};
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408    use static_assertions::assert_not_impl_any;
409
410    assert_not_impl_any!(OwnedNativeFrame: Copy, Clone);
411    assert_not_impl_any!(OwnedNativeImage: Copy, Clone);
412    assert_not_impl_any!(FrameSync: Copy, Clone);
413
414    #[test]
415    fn metadata_is_resource_free_and_copyable() {
416        let metadata = FrameMetadata {
417            size: FrameSize::new(960, 640),
418            format: PixelFormat::Bgra8Unorm,
419            generation: 42,
420        };
421        assert_eq!(metadata, metadata);
422        assert_eq!(metadata.size.width, 960);
423    }
424
425    #[cfg(target_os = "windows")]
426    #[test]
427    fn dx12_custody_clones_one_allocation_identity() {
428        use std::os::windows::io::{FromRawHandle, OwnedHandle};
429
430        let raw = unsafe {
431            windows::Win32::System::Threading::CreateEventW(None, true, false, None)
432                .expect("create test handle")
433        };
434        let owned = unsafe { OwnedHandle::from_raw_handle(raw.0) };
435        let resource = Dx12SharedResource::from_owned_handle(owned);
436        let clone = resource.clone();
437        assert_eq!(resource.allocation_key(), clone.allocation_key());
438
439        let texture = Dx12SharedTexture::new(clone);
440        assert_eq!(texture.allocation_key(), resource.allocation_key());
441    }
442
443    #[cfg(target_os = "linux")]
444    #[test]
445    fn invalid_dmabuf_layout_closes_taken_descriptor() {
446        use std::{io::Write, os::fd::IntoRawFd, os::unix::net::UnixStream};
447
448        let (taken, mut peer) = UnixStream::pair().expect("create descriptor pair");
449        let raw = taken.into_raw_fd();
450        let result = unsafe {
451            DmaBufImage::from_raw_owned_parts(
452                vec![raw],
453                vec![DmaBufPlane {
454                    buffer_index: 1,
455                    offset: 0,
456                    stride: 4,
457                }],
458                0,
459                0,
460            )
461        };
462        let error = match result {
463            Err(error) => error,
464            Ok(_) => panic!("invalid table must fail"),
465        };
466        assert!(matches!(error, DmaBufError::InvalidBufferIndex { .. }));
467        assert!(peer.write_all(b"closed").is_err());
468    }
469}