1use libloading::Library;
13use std::ffi::c_void;
14use std::os::raw::{c_int, c_uint};
15use std::sync::{Arc, OnceLock};
16
17pub(crate) type CudaError = c_int; pub type GraphicsResource = *mut c_void; pub(crate) type ExternalMemory = *mut c_void; pub type CudaStream = *mut c_void; #[allow(non_snake_case, dead_code)]
23pub(crate) struct CudaTable {
24 _lib: &'static Library,
25 pub graphics_gl_register_buffer:
26 unsafe extern "C" fn(*mut GraphicsResource, c_uint, c_uint) -> CudaError,
27 pub graphics_map_resources:
28 unsafe extern "C" fn(c_int, *mut GraphicsResource, *mut c_void) -> CudaError,
29 pub graphics_get_mapped_pointer:
30 unsafe extern "C" fn(*mut *mut c_void, *mut usize, GraphicsResource) -> CudaError,
31 pub graphics_unmap_resources:
32 unsafe extern "C" fn(c_int, *mut GraphicsResource, *mut c_void) -> CudaError,
33 pub graphics_unregister_resource: unsafe extern "C" fn(GraphicsResource) -> CudaError,
34 pub import_external_memory:
35 unsafe extern "C" fn(*mut ExternalMemory, *const c_void) -> CudaError,
36 pub external_memory_get_mapped_buffer:
37 unsafe extern "C" fn(*mut *mut c_void, ExternalMemory, *const c_void) -> CudaError,
38 pub destroy_external_memory: unsafe extern "C" fn(ExternalMemory) -> CudaError,
39 pub memcpy: unsafe extern "C" fn(*mut c_void, *const c_void, usize, c_int) -> CudaError,
40 pub free: unsafe extern "C" fn(*mut c_void) -> CudaError,
41 pub stream_create: unsafe extern "C" fn(*mut CudaStream) -> CudaError,
42 pub stream_synchronize: unsafe extern "C" fn(CudaStream) -> CudaError,
43 pub stream_destroy: unsafe extern "C" fn(CudaStream) -> CudaError,
44}
45
46static TABLE: OnceLock<Option<CudaTable>> = OnceLock::new();
47
48#[cfg(miri)]
59fn load() -> Option<CudaTable> {
60 None
61}
62
63#[cfg(not(miri))]
64fn load() -> Option<CudaTable> {
65 let lib = ["libcudart.so", "libcudart.so.12", "libcudart.so.11.0"]
66 .iter()
67 .find_map(|n| unsafe { Library::new(*n) }.ok())?;
68 let lib: &'static Library = Box::leak(Box::new(lib));
69 macro_rules! sym {
70 ($n:literal) => {{
71 *unsafe { lib.get(concat!($n, "\0").as_bytes()) }.ok()?
72 }};
73 }
74 Some(CudaTable {
75 _lib: lib,
76 graphics_gl_register_buffer: sym!("cudaGraphicsGLRegisterBuffer"),
77 graphics_map_resources: sym!("cudaGraphicsMapResources"),
78 graphics_get_mapped_pointer: sym!("cudaGraphicsResourceGetMappedPointer"),
79 graphics_unmap_resources: sym!("cudaGraphicsUnmapResources"),
80 graphics_unregister_resource: sym!("cudaGraphicsUnregisterResource"),
81 import_external_memory: sym!("cudaImportExternalMemory"),
82 external_memory_get_mapped_buffer: sym!("cudaExternalMemoryGetMappedBuffer"),
83 destroy_external_memory: sym!("cudaDestroyExternalMemory"),
84 memcpy: sym!("cudaMemcpy"),
85 free: sym!("cudaFree"),
86 stream_create: sym!("cudaStreamCreate"),
87 stream_synchronize: sym!("cudaStreamSynchronize"),
88 stream_destroy: sym!("cudaStreamDestroy"),
89 })
90}
91
92pub(crate) fn table() -> Option<&'static CudaTable> {
93 TABLE.get_or_init(load).as_ref()
94}
95
96pub fn is_cuda_available() -> bool {
98 table().is_some()
99}
100
101pub const CUDA_MEMCPY_DEVICE_TO_HOST: c_int = 2;
103
104pub unsafe fn memcpy_device_to_host(
114 host: *mut c_void,
115 device: *const c_void,
116 count: usize,
117) -> bool {
118 match table() {
119 Some(t) => unsafe { (t.memcpy)(host, device, count, CUDA_MEMCPY_DEVICE_TO_HOST) == 0 },
120 None => false,
121 }
122}
123
124pub fn stream_create() -> Option<CudaStream> {
129 let t = table()?;
130 let mut stream: CudaStream = std::ptr::null_mut();
131 if unsafe { (t.stream_create)(&mut stream) } != 0 {
132 return None;
133 }
134 Some(stream)
135}
136
137pub unsafe fn stream_synchronize(stream: CudaStream) -> bool {
145 match table() {
146 Some(t) => unsafe { (t.stream_synchronize)(stream) == 0 },
147 None => false,
148 }
149}
150
151pub unsafe fn stream_destroy(stream: CudaStream) {
158 if let Some(t) = table() {
159 let _ = unsafe { (t.stream_destroy)(stream) };
160 }
161}
162
163pub fn gl_register_buffer(buffer_id: u32) -> Option<usize> {
167 let t = table()?;
168 let mut res: GraphicsResource = std::ptr::null_mut();
169 let rc = unsafe { (t.graphics_gl_register_buffer)(&mut res, buffer_id, 0) };
171 if rc != 0 {
172 log::debug!("cudaGraphicsGLRegisterBuffer(buffer={buffer_id}) failed: cudaError {rc}");
173 return None;
174 }
175 Some(res as usize)
176}
177
178pub fn gl_map_resource(resource: usize) -> Option<(usize, usize)> {
180 let t = table()?;
181 let mut res = resource as GraphicsResource;
182 if unsafe { (t.graphics_map_resources)(1, &mut res, std::ptr::null_mut()) } != 0 {
183 return None;
184 }
185 let (mut ptr, mut size) = (std::ptr::null_mut::<c_void>(), 0usize);
186 if unsafe { (t.graphics_get_mapped_pointer)(&mut ptr, &mut size, res) } != 0 {
187 unsafe {
188 (t.graphics_unmap_resources)(1, &mut res, std::ptr::null_mut());
189 }
190 return None;
191 }
192 Some((ptr as usize, size))
193}
194
195pub fn gl_unmap_resource(resource: usize) {
197 if let Some(t) = table() {
198 let mut r = resource as GraphicsResource;
199 unsafe {
200 (t.graphics_unmap_resources)(1, &mut r, std::ptr::null_mut());
201 }
202 }
203}
204
205pub fn gl_unregister_resource(resource: usize) {
207 if let Some(t) = table() {
208 unsafe {
209 (t.graphics_unregister_resource)(resource as GraphicsResource);
210 }
211 }
212}
213
214#[allow(dead_code)] pub(crate) const CUDA_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD: c_uint = 1;
228
229#[allow(dead_code)] #[repr(C)]
241pub(crate) struct CudaExternalMemoryHandleDesc {
242 pub type_: c_int,
243 pub _pad0: u32,
244 pub handle_fd: c_int,
245 pub _union_rest: [u32; 3],
246 pub size: u64,
247 pub flags: c_uint,
248 pub _tail: u32,
249}
250
251#[allow(dead_code)] #[repr(C)]
260pub(crate) struct CudaExternalMemoryBufferDesc {
261 pub offset: u64,
262 pub size: u64,
263 pub flags: c_uint,
264 pub _tail: u32,
265}
266
267#[cfg(all(target_os = "linux", feature = "static"))]
288pub(crate) fn import_dma_fd(fd: i32, size: usize) -> Option<(ExternalMemory, *mut c_void)> {
289 use std::os::fd::{AsRawFd, BorrowedFd, IntoRawFd};
290 let t = table()?;
291 let dup = unsafe { BorrowedFd::borrow_raw(fd) }
300 .try_clone_to_owned()
301 .ok()?;
302 let mut desc: CudaExternalMemoryHandleDesc = unsafe { std::mem::zeroed() };
303 desc.type_ = CUDA_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD as c_int;
304 desc.handle_fd = dup.as_raw_fd();
305 desc.size = size as u64;
306 let mut ext: ExternalMemory = std::ptr::null_mut();
307 let rc = unsafe { (t.import_external_memory)(&mut ext, &desc as *const _ as *const c_void) };
308 if rc != 0 {
309 log::debug!("cudaImportExternalMemory(OpaqueFd, size={size}) failed: cudaError {rc}");
310 let raw = dup.into_raw_fd();
314 if unsafe { libc::close(raw) } != 0 {
315 log::debug!(
316 "reclaim of CUDA-import dup fd {raw} after failed import: {}",
317 std::io::Error::last_os_error()
318 );
319 }
320 return None;
321 }
322 let _ = dup.into_raw_fd();
324 let bdesc = CudaExternalMemoryBufferDesc {
325 offset: 0,
326 size: size as u64,
327 flags: 0,
328 _tail: 0,
329 };
330 let mut dptr: *mut c_void = std::ptr::null_mut();
331 if unsafe {
332 (t.external_memory_get_mapped_buffer)(&mut dptr, ext, &bdesc as *const _ as *const c_void)
333 } != 0
334 {
335 unsafe { (t.destroy_external_memory)(ext) };
336 return None;
337 }
338 Some((ext, dptr))
339}
340
341pub trait CudaGlOps: Send + Sync {
344 fn map(&self, resource: GraphicsResource) -> Option<(*mut c_void, usize)>;
345 fn unmap(&self, resource: GraphicsResource);
346 fn unregister(&self, resource: GraphicsResource);
347}
348
349enum CudaBacking {
350 #[allow(dead_code)] GlBuffer {
352 resource: GraphicsResource,
353 ops: Arc<dyn CudaGlOps>,
354 },
355 #[allow(dead_code)] ExternalMem {
357 ext_mem: ExternalMemory,
358 dptr: *mut c_void,
359 },
360}
361
362unsafe impl Send for CudaBacking {}
365unsafe impl Sync for CudaBacking {}
366
367pub struct CudaHandle {
369 kind: CudaBacking,
370 size: usize,
371}
372
373impl std::fmt::Debug for CudaHandle {
374 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
375 let kind = match &self.kind {
376 CudaBacking::GlBuffer { .. } => "GlBuffer",
377 CudaBacking::ExternalMem { .. } => "ExternalMem",
378 };
379 f.debug_struct("CudaHandle")
380 .field("kind", &kind)
381 .field("size", &self.size)
382 .finish()
383 }
384}
385
386impl CudaHandle {
387 pub fn new_gl(resource: GraphicsResource, size: usize, ops: Arc<dyn CudaGlOps>) -> Self {
391 Self {
392 kind: CudaBacking::GlBuffer { resource, ops },
393 size,
394 }
395 }
396
397 #[allow(dead_code)] pub(crate) fn new_external(ext_mem: ExternalMemory, dptr: *mut c_void, size: usize) -> Self {
399 Self {
400 kind: CudaBacking::ExternalMem { ext_mem, dptr },
401 size,
402 }
403 }
404
405 pub fn map(&self) -> Option<CudaMap<'_>> {
408 match &self.kind {
409 CudaBacking::GlBuffer { resource, ops } => {
410 let (ptr, len) = ops.map(*resource)?;
411 Some(CudaMap {
412 ptr,
413 len,
414 unmap: Some((ops.clone(), *resource)),
415 _marker: std::marker::PhantomData,
416 })
417 }
418 CudaBacking::ExternalMem { dptr, .. } => Some(CudaMap {
419 ptr: *dptr,
420 len: self.size,
421 unmap: None,
422 _marker: std::marker::PhantomData,
423 }),
424 }
425 }
426}
427
428impl Drop for CudaHandle {
429 fn drop(&mut self) {
430 match &self.kind {
431 CudaBacking::GlBuffer { resource, ops } => ops.unregister(*resource),
432 CudaBacking::ExternalMem { ext_mem, dptr: _ } => {
433 if let Some(t) = table() {
439 unsafe {
440 (t.destroy_external_memory)(*ext_mem);
441 }
442 }
443 }
444 }
445 }
446}
447
448pub struct CudaMap<'a> {
452 ptr: *mut c_void,
453 len: usize,
454 unmap: Option<(Arc<dyn CudaGlOps>, GraphicsResource)>,
455 _marker: std::marker::PhantomData<&'a ()>,
456}
457
458unsafe impl Send for CudaMap<'_> {}
462unsafe impl Sync for CudaMap<'_> {}
463
464impl CudaMap<'_> {
465 pub fn device_ptr(&self) -> *mut c_void {
467 self.ptr
468 }
469
470 pub fn len(&self) -> usize {
472 self.len
473 }
474
475 pub fn is_empty(&self) -> bool {
477 self.len == 0
478 }
479}
480
481impl Drop for CudaMap<'_> {
482 fn drop(&mut self) {
483 if let Some((ops, r)) = self.unmap.take() {
484 ops.unmap(r);
485 }
486 }
487}
488
489#[cfg(test)]
490mod ext_mem_layout {
491 use super::*;
492 #[test]
493 fn external_memory_desc_abi() {
494 assert_eq!(std::mem::size_of::<CudaExternalMemoryHandleDesc>(), 40);
495 assert_eq!(std::mem::align_of::<CudaExternalMemoryHandleDesc>(), 8);
496 let d: CudaExternalMemoryHandleDesc = unsafe { std::mem::zeroed() };
498 let base = &d as *const _ as usize;
499 assert_eq!((&d.size as *const _ as usize) - base, 24);
500 assert_eq!((&d.flags as *const _ as usize) - base, 32);
501 assert_eq!(std::mem::size_of::<CudaExternalMemoryBufferDesc>(), 24);
502 let b: CudaExternalMemoryBufferDesc = unsafe { std::mem::zeroed() };
503 let bb = &b as *const _ as usize;
504 assert_eq!((&b.size as *const _ as usize) - bb, 8);
505 }
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511 #[test]
512 fn cuda_table_loads_when_libcudart_present() {
513 let avail = is_cuda_available();
514 if avail {
515 assert!(table().is_some(), "table present when available");
516 }
517 }
519
520 #[test]
521 fn pub_primitives_degrade_without_libcudart() {
522 if is_cuda_available() {
527 return;
528 }
529 assert!(gl_register_buffer(0).is_none());
530 assert!(gl_map_resource(0).is_none());
531 gl_unmap_resource(0); gl_unregister_resource(0); assert!(!unsafe { memcpy_device_to_host(std::ptr::null_mut(), std::ptr::null(), 0) });
536 }
537}
538
539#[cfg(test)]
540mod handle_tests {
541 use super::*;
542 use std::sync::{
543 atomic::{AtomicUsize, Ordering},
544 Arc,
545 };
546 struct MockOps {
547 unmaps: Arc<AtomicUsize>,
548 unregisters: Arc<AtomicUsize>,
549 }
550 impl CudaGlOps for MockOps {
551 fn map(&self, _r: GraphicsResource) -> Option<(*mut std::ffi::c_void, usize)> {
552 Some((0x1000usize as *mut _, 4096))
553 }
554 fn unmap(&self, _r: GraphicsResource) {
555 self.unmaps.fetch_add(1, Ordering::SeqCst);
556 }
557 fn unregister(&self, _r: GraphicsResource) {
558 self.unregisters.fetch_add(1, Ordering::SeqCst);
559 }
560 }
561 #[test]
562 fn cudamap_guard_unmaps_on_drop_for_glbuffer() {
563 let unmaps = Arc::new(AtomicUsize::new(0));
564 let unregisters = Arc::new(AtomicUsize::new(0));
565 {
566 let h = CudaHandle::new_gl(
567 0x1usize as GraphicsResource,
568 4096,
569 Arc::new(MockOps {
570 unmaps: unmaps.clone(),
571 unregisters: unregisters.clone(),
572 }),
573 );
574 {
575 let m = h.map().expect("map");
576 assert_eq!(m.device_ptr() as usize, 0x1000);
577 assert_eq!(m.len(), 4096);
578 assert!(!m.is_empty());
579 }
580 assert_eq!(
582 unmaps.load(Ordering::SeqCst),
583 1,
584 "Drop must unmap a GlBuffer"
585 );
586 assert_eq!(unregisters.load(Ordering::SeqCst), 0);
587 }
588 assert_eq!(
590 unregisters.load(Ordering::SeqCst),
591 1,
592 "Dropping a GlBuffer handle must unregister"
593 );
594 }
595
596 struct NoneOps;
598 impl CudaGlOps for NoneOps {
599 fn map(&self, _r: GraphicsResource) -> Option<(*mut std::ffi::c_void, usize)> {
600 None
601 }
602 fn unmap(&self, _r: GraphicsResource) {}
603 fn unregister(&self, _r: GraphicsResource) {}
604 }
605 #[test]
606 fn glbuffer_map_returns_none_when_ops_map_fails() {
607 let h = CudaHandle::new_gl(0x9usize as GraphicsResource, 4096, Arc::new(NoneOps));
608 assert!(
609 h.map().is_none(),
610 "GlBuffer map propagates ops.map() failure"
611 );
612 }
613
614 #[test]
615 fn glbuffer_handle_debug_and_empty_map() {
616 let unmaps = Arc::new(AtomicUsize::new(0));
617 let unregisters = Arc::new(AtomicUsize::new(0));
618 let h = CudaHandle::new_gl(
619 0x2usize as GraphicsResource,
620 0,
621 Arc::new(MockOps {
622 unmaps: unmaps.clone(),
623 unregisters: unregisters.clone(),
624 }),
625 );
626 let dbg = format!("{h:?}");
627 assert!(
628 dbg.contains("GlBuffer"),
629 "debug names the backing kind: {dbg}"
630 );
631 assert!(dbg.contains("size"), "debug includes size: {dbg}");
632 }
633
634 #[test]
635 fn external_mem_map_is_persistent_and_debug_names_kind() {
636 let dptr = 0xCAFE_0000usize as *mut std::ffi::c_void;
639 let h = CudaHandle::new_external(std::ptr::null_mut(), dptr, 8192);
640 let dbg = format!("{h:?}");
641 assert!(dbg.contains("ExternalMem"), "debug names the kind: {dbg}");
642 {
643 let m = h.map().expect("ExternalMem map is always Some");
644 assert_eq!(m.device_ptr(), dptr, "persistent device ptr passthrough");
645 assert_eq!(m.len(), 8192);
646 assert!(!m.is_empty());
647 }
649 std::mem::forget(h);
652 }
653
654 #[test]
655 fn external_mem_zero_len_map_is_empty() {
656 let h = CudaHandle::new_external(std::ptr::null_mut(), std::ptr::null_mut(), 0);
657 {
658 let m = h.map().expect("map");
659 assert_eq!(m.len(), 0);
660 assert!(m.is_empty(), "zero-length mapping is empty");
661 assert!(m.device_ptr().is_null());
662 }
663 std::mem::forget(h); }
665}