gpu_handle_types/device_handles.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Typed newtypes for device-context handles.
4//!
5//! Rather than a bare `*mut c_void` payload behind one blanket
6//! `unsafe impl Send` on an enum, each handle kind gets its own newtype
7//! with one typed `try_from_raw(unsafe { ... })` constructor, so the
8//! caller-side invariants (null rejection, Send/Sync contract) are
9//! asserted once at construction rather than on every pattern-match arm.
10//! [`crate::ExternalQueue`] carries them, as can an interop layer's
11//! sync-target enum.
12//!
13//! All payloads stay raw (`*mut c_void`). Pulling `ash` / `objc2` /
14//! `windows` into `gpu-handle-types` would force every downstream crate
15//! to link the transitive toolchains regardless of platform.
16//!
17//! Each newtype is `Copy` — the wrapper carries no ownership, just the
18//! pointer's bit-pattern. Borrowed via `&'a CudaContext` etc. into
19//! [`crate::ExternalQueue`] (or a sync target); the caller's
20//! producer-side `Arc<...>` (or equivalent) keeps the underlying device
21//! alive for the duration of the borrow.
22
23use core::ffi::c_void;
24use core::fmt;
25
26use crate::gpu_resource::InvalidHandleError;
27
28macro_rules! define_raw_handle {
29 (
30 $(#[$attr:meta])*
31 $name:ident, $payload_label:expr
32 ) => {
33 $(#[$attr])*
34 #[derive(Copy, Clone)]
35 #[repr(transparent)]
36 pub struct $name {
37 ptr: *mut c_void,
38 }
39
40 impl $name {
41 #[doc = concat!("Wrap a raw `", $payload_label, "` in the typed newtype, rejecting null.")]
42 ///
43 /// This is a **borrow**, not a transfer of ownership. The
44 /// newtype is `Copy` and stores nothing but the pointer's
45 /// bit-pattern — no `Arc`, no keep-alive, no `Drop`. It never
46 /// retains, releases, destroys or closes the underlying
47 /// object, so its lifetime stays entirely the caller's (or
48 /// the caller's own producer's) to manage.
49 ///
50 /// # Safety
51 ///
52 #[doc = concat!(
53 "* `ptr` must be a live `", $payload_label, "` — that exact API kind, not \
54 merely a non-null pointer. The only check performed is the null rejection \
55 below; the value is otherwise stored verbatim and later handed to the bridge \
56 dispatcher, which calls into the owning API through it."
57 )]
58 /// * The referenced object must stay alive, and must not be
59 /// destroyed, for as long as this value or **any** `Copy` of
60 /// it exists — in particular for the whole of every
61 /// [`crate::ExternalQueue`] (or sync-target) borrow that
62 /// names it. Because `Copy` duplicates the pointer with no
63 /// refcount bump, destroying the object while a copy
64 /// survives is a use-after-free the borrow checker cannot
65 /// see. Callers anchor this with the `Arc<…>` (or
66 /// equivalent) they already hold on the producer side.
67 /// * The object must tolerate use from a thread other than the
68 /// one that created it, and concurrent use from several:
69 #[doc = concat!(" `", stringify!($name), "` is `Send + Sync`, so a copy may be moved to \
70 and shared with the interop worker threads. Where the API forbids that — \
71 recording into one `id<MTLCommandBuffer>` from two threads, or a GL context \
72 that is current on exactly one thread — the caller must confine the value \
73 itself; nothing here enforces it.")]
74 /// * `ptr` may be null: that is reported as
75 /// [`InvalidHandleError::NullPointer`] rather than being
76 /// undefined behaviour, so a null check is *not* part of
77 /// the caller's obligation.
78 #[inline]
79 pub unsafe fn try_from_raw(ptr: *mut c_void) -> Result<Self, InvalidHandleError> {
80 if ptr.is_null() {
81 return Err(InvalidHandleError::NullPointer($payload_label));
82 }
83 Ok(Self { ptr })
84 }
85
86 #[inline]
87 pub fn as_raw(&self) -> *mut c_void {
88 self.ptr
89 }
90 }
91
92 impl fmt::Debug for $name {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 f.debug_struct(stringify!($name)).finish_non_exhaustive()
95 }
96 }
97
98 // SAFETY: caller asserts the platform permits cross-thread
99 // sharing of the handle (see `try_from_raw`'s `# Safety`). Not
100 // enforced statically.
101 unsafe impl Send for $name {}
102 unsafe impl Sync for $name {}
103 };
104}
105
106define_raw_handle! {
107 /// CUDA `CUcontext` borrowed for the duration of a CUDA sync-target
108 /// dispatch.
109 CudaContext, "CUcontext"
110}
111
112define_raw_handle! {
113 /// CUDA `CUstream` borrowed by `ExternalQueue::CudaStream`. The
114 /// stream's lifetime is anchored by the caller's CUDA context.
115 CudaStreamRef, "CUstream"
116}
117
118define_raw_handle! {
119 /// OpenCL `cl_context` borrowed for an OpenCL sync-target
120 /// dispatch. The platform/device parentage is rederived inside the
121 /// bridge via `clGetContextInfo`.
122 ClContext, "cl_context"
123}
124
125define_raw_handle! {
126 /// OpenCL `cl_command_queue` borrowed by `ExternalQueue::OpenClQueue`.
127 ClQueueRef, "cl_command_queue"
128}
129
130define_raw_handle! {
131 /// `ID3D11Device*` borrowed for a D3D11 sync-target dispatch.
132 /// Caller has set `D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX` or the
133 /// multithread-protect flag — that's the cross-thread contract.
134 D3D11Device, "ID3D11Device*"
135}
136
137define_raw_handle! {
138 /// `ID3D12Device*` borrowed for a D3D12 sync-target dispatch.
139 /// LUID-matched against the source `VkDevice` / Vulkan instance by
140 /// the bridge dispatcher.
141 D3D12Device, "ID3D12Device*"
142}
143
144define_raw_handle! {
145 /// `VkInstance` borrowed for a Vulkan sync-target dispatch.
146 /// Required even for device-level work because ash resolves device
147 /// fn pointers through `vkGetDeviceProcAddr`, which is itself an
148 /// instance-level command (`vkGetInstanceProcAddr(NULL, ...)`
149 /// returns NULL).
150 VkInstance, "VkInstance"
151}
152
153define_raw_handle! {
154 /// `VkDevice` borrowed for a Vulkan sync-target dispatch.
155 VkDevice, "VkDevice"
156}
157
158define_raw_handle! {
159 /// `id<MTLDevice>` borrowed for a Metal sync-target dispatch.
160 MtlDevice, "id<MTLDevice>"
161}
162
163define_raw_handle! {
164 /// `id<MTLCommandBuffer>` borrowed by
165 /// `ExternalQueue::MetalCommandBuffer`. Recording into the same
166 /// command buffer from multiple threads is undefined, and nothing
167 /// here prevents it — the borrow is shared and the newtype is `Copy`,
168 /// so confining recording to one thread is the caller's obligation.
169 MtlCommandBufferRef, "id<MTLCommandBuffer>"
170}
171
172define_raw_handle! {
173 /// Platform-native OpenGL context handle (`EGLContext` / `HGLRC` /
174 /// `CGLContextObj`). The `GlBackend` flavour travels alongside it in
175 /// an OpenGL sync target so the bridge dispatcher resolves the
176 /// matching proc-address loader without re-probing.
177 GlContextRef, "EGLContext/HGLRC/CGLContextObj"
178}