Skip to main content

oxicuda_memory/
host_registered.rs

1//! Host-registered memory for DMA access.
2//!
3//! [`RegisteredMemory<T>`] wraps `cuMemHostRegister` / `cuMemHostUnregister`
4//! to register existing host allocations with the CUDA driver, enabling DMA
5//! transfers without an intermediate staging copy.
6//!
7//! Unlike [`PinnedBuffer`](crate::PinnedBuffer), which allocates *new*
8//! page-locked memory, `RegisteredMemory` works with memory that has
9//! already been allocated (e.g. a `Vec<T>`, a slice from a memory-mapped
10//! file, etc.).
11//!
12//! # Lifetime
13//!
14//! The caller must ensure the underlying allocation outlives the
15//! `RegisteredMemory` handle.  The handle borrows (but does NOT own) the
16//! memory.  On [`Drop`], only `cuMemHostUnregister` is called — the
17//! original allocation is untouched.
18//!
19//! # Example
20//!
21//! ```rust,no_run
22//! # use oxicuda_memory::host_registered::{register_vec, RegisterFlags};
23//! let mut data = vec![0.0f32; 1024];
24//! let reg = register_vec(&mut data, RegisterFlags::DEFAULT)?;
25//! assert_eq!(reg.len(), 1024);
26//! // `data` is now DMA-accessible; use `reg.device_ptr()` on the GPU side.
27//! drop(reg); // cuMemHostUnregister is called here
28//! # Ok::<(), oxicuda_driver::error::CudaError>(())
29//! ```
30
31use std::fmt;
32use std::ops::{BitAnd, BitOr, Deref, DerefMut};
33
34use oxicuda_driver::error::{CudaError, CudaResult};
35use oxicuda_driver::ffi::{
36    CU_MEMHOSTREGISTER_DEVICEMAP, CU_MEMHOSTREGISTER_IOMEMORY, CU_MEMHOSTREGISTER_PORTABLE,
37    CU_MEMHOSTREGISTER_READ_ONLY, CUdeviceptr,
38};
39
40#[cfg(not(target_os = "macos"))]
41use oxicuda_driver::ffi;
42#[cfg(not(target_os = "macos"))]
43use oxicuda_driver::loader::try_driver;
44#[cfg(not(target_os = "macos"))]
45use std::ffi::c_void;
46
47// ---------------------------------------------------------------------------
48// RegisterFlags
49// ---------------------------------------------------------------------------
50
51/// Bitflags controlling how `cuMemHostRegister` registers host memory.
52#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
53pub struct RegisterFlags(u32);
54
55impl RegisterFlags {
56    /// Memory is portable across CUDA contexts.
57    pub const PORTABLE: Self = Self(CU_MEMHOSTREGISTER_PORTABLE);
58
59    /// Memory is mapped into the device address space, enabling zero-copy
60    /// access via `cuMemHostGetDevicePointer`.
61    pub const DEVICE_MAP: Self = Self(CU_MEMHOSTREGISTER_DEVICEMAP);
62
63    /// Pointer refers to I/O memory (not system RAM).
64    pub const IO_MEMORY: Self = Self(CU_MEMHOSTREGISTER_IOMEMORY);
65
66    /// Memory will not be written by the GPU (read-only hint).
67    pub const READ_ONLY: Self = Self(CU_MEMHOSTREGISTER_READ_ONLY);
68
69    /// The recommended default: portable + device-mapped.
70    pub const DEFAULT: Self = Self(CU_MEMHOSTREGISTER_PORTABLE | CU_MEMHOSTREGISTER_DEVICEMAP);
71
72    /// No flags set.
73    pub const NONE: Self = Self(0);
74
75    /// Returns the raw `u32` flag value.
76    #[inline]
77    pub const fn bits(self) -> u32 {
78        self.0
79    }
80
81    /// Creates a `RegisterFlags` from a raw `u32` value.
82    #[inline]
83    pub const fn from_bits(bits: u32) -> Self {
84        Self(bits)
85    }
86
87    /// Returns `true` if `self` contains all flags in `other`.
88    #[inline]
89    pub const fn contains(self, other: Self) -> bool {
90        (self.0 & other.0) == other.0
91    }
92}
93
94impl BitOr for RegisterFlags {
95    type Output = Self;
96
97    #[inline]
98    fn bitor(self, rhs: Self) -> Self {
99        Self(self.0 | rhs.0)
100    }
101}
102
103impl BitAnd for RegisterFlags {
104    type Output = Self;
105
106    #[inline]
107    fn bitand(self, rhs: Self) -> Self {
108        Self(self.0 & rhs.0)
109    }
110}
111
112impl fmt::Display for RegisterFlags {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        let mut parts = Vec::new();
115        if self.contains(Self::PORTABLE) {
116            parts.push("PORTABLE");
117        }
118        if self.contains(Self::DEVICE_MAP) {
119            parts.push("DEVICE_MAP");
120        }
121        if self.contains(Self::IO_MEMORY) {
122            parts.push("IO_MEMORY");
123        }
124        if self.contains(Self::READ_ONLY) {
125            parts.push("READ_ONLY");
126        }
127        if parts.is_empty() {
128            write!(f, "NONE")
129        } else {
130            write!(f, "{}", parts.join(" | "))
131        }
132    }
133}
134
135// ---------------------------------------------------------------------------
136// RegisteredMemory<T>
137// ---------------------------------------------------------------------------
138
139/// RAII handle for host memory registered with the CUDA driver.
140///
141/// The handle borrows a raw pointer to existing host memory and registers
142/// it via `cuMemHostRegister_v2`.  On [`Drop`], `cuMemHostUnregister` is
143/// called to undo the registration.  The underlying allocation is **not**
144/// freed — that responsibility remains with the original owner.
145///
146/// # Safety invariant
147///
148/// The memory range `[ptr, ptr + len)` must remain valid and not be freed
149/// for the entire lifetime of this handle.
150pub struct RegisteredMemory<T: Copy> {
151    /// Borrowed pointer to the host allocation (NOT owned).
152    ptr: *mut T,
153    /// Number of `T` elements.
154    len: usize,
155    /// Flags used during registration.
156    flags: RegisterFlags,
157    /// Device-visible pointer obtained from registration (if DEVICE_MAP).
158    device_ptr: CUdeviceptr,
159}
160
161// SAFETY: The registered host memory is not thread-local; it is accessible
162// from any thread once registered with the CUDA driver.
163unsafe impl<T: Copy + Send> Send for RegisteredMemory<T> {}
164unsafe impl<T: Copy + Sync> Sync for RegisteredMemory<T> {}
165
166impl<T: Copy> RegisteredMemory<T> {
167    /// Returns a raw const pointer to the registered memory.
168    #[inline]
169    pub fn as_ptr(&self) -> *const T {
170        self.ptr
171    }
172
173    /// Returns a raw mutable pointer to the registered memory.
174    #[inline]
175    pub fn as_mut_ptr(&mut self) -> *mut T {
176        self.ptr
177    }
178
179    /// Returns the device-visible pointer for the registered memory.
180    ///
181    /// This is only meaningful when the `DEVICE_MAP` flag was set.
182    #[inline]
183    pub fn device_ptr(&self) -> CUdeviceptr {
184        self.device_ptr
185    }
186
187    /// Returns the number of `T` elements in the registered range.
188    #[inline]
189    pub fn len(&self) -> usize {
190        self.len
191    }
192
193    /// Returns `true` if the registered range contains zero elements.
194    #[inline]
195    pub fn is_empty(&self) -> bool {
196        self.len == 0
197    }
198
199    /// Returns the flags used when the memory was registered.
200    #[inline]
201    pub fn flags(&self) -> RegisterFlags {
202        self.flags
203    }
204
205    /// Returns a shared slice over the registered memory.
206    #[inline]
207    pub fn as_slice(&self) -> &[T] {
208        // SAFETY: the caller guaranteed the memory is valid for `self.len`
209        // elements, and we have `&self` so no mutable alias exists.
210        unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
211    }
212
213    /// Returns a mutable slice over the registered memory.
214    #[inline]
215    pub fn as_mut_slice(&mut self) -> &mut [T] {
216        // SAFETY: the caller guaranteed the memory is valid for `self.len`
217        // elements, and we have `&mut self` so no other alias exists.
218        unsafe { std::slice::from_raw_parts_mut(self.ptr, self.len) }
219    }
220}
221
222impl<T: Copy> Deref for RegisteredMemory<T> {
223    type Target = [T];
224
225    #[inline]
226    fn deref(&self) -> &[T] {
227        self.as_slice()
228    }
229}
230
231impl<T: Copy> DerefMut for RegisteredMemory<T> {
232    #[inline]
233    fn deref_mut(&mut self) -> &mut [T] {
234        self.as_mut_slice()
235    }
236}
237
238impl<T: Copy> Drop for RegisteredMemory<T> {
239    fn drop(&mut self) {
240        #[cfg(not(target_os = "macos"))]
241        {
242            if let Ok(api) = try_driver() {
243                let rc = unsafe { (api.cu_mem_host_unregister)(self.ptr.cast::<c_void>()) };
244                if rc != 0 {
245                    tracing::warn!(
246                        cuda_error = rc,
247                        len = self.len,
248                        "cuMemHostUnregister failed during RegisteredMemory drop"
249                    );
250                }
251            }
252        }
253    }
254}
255
256// ---------------------------------------------------------------------------
257// Public registration functions
258// ---------------------------------------------------------------------------
259
260/// Registers an existing host memory range with the CUDA driver for DMA.
261///
262/// # Safety contract (upheld by the caller)
263///
264/// * `ptr` must point to a valid allocation of at least `len * size_of::<T>()` bytes.
265/// * The allocation must remain valid for the lifetime of the returned handle.
266///
267/// # Errors
268///
269/// * [`CudaError::InvalidValue`] if `len` is zero or the byte size overflows.
270/// * [`CudaError::NotSupported`] on macOS.
271/// * Other driver errors from `cuMemHostRegister_v2`.
272pub fn register<T: Copy>(
273    ptr: *mut T,
274    len: usize,
275    flags: RegisterFlags,
276) -> CudaResult<RegisteredMemory<T>> {
277    if len == 0 {
278        return Err(CudaError::InvalidValue);
279    }
280    if ptr.is_null() {
281        return Err(CudaError::InvalidValue);
282    }
283    let byte_size = len
284        .checked_mul(std::mem::size_of::<T>())
285        .ok_or(CudaError::InvalidValue)?;
286
287    #[cfg(target_os = "macos")]
288    {
289        // On macOS there is no CUDA driver.  Return a synthetic handle so
290        // that unit tests can exercise the API surface without a GPU.
291        let _ = byte_size;
292        Ok(RegisteredMemory {
293            ptr,
294            len,
295            flags,
296            device_ptr: ptr as CUdeviceptr,
297        })
298    }
299
300    #[cfg(not(target_os = "macos"))]
301    {
302        let api = try_driver()?;
303
304        // Register the host memory range.
305        let rc =
306            unsafe { (api.cu_mem_host_register_v2)(ptr.cast::<c_void>(), byte_size, flags.bits()) };
307        oxicuda_driver::check(rc)?;
308
309        // If DEVICE_MAP is set, obtain the device pointer.
310        let device_ptr = if flags.contains(RegisterFlags::DEVICE_MAP) {
311            let mut dptr: CUdeviceptr = 0;
312            let rc2 = unsafe {
313                (api.cu_mem_host_get_device_pointer_v2)(&mut dptr, ptr.cast::<c_void>(), 0)
314            };
315            if let Err(e) = oxicuda_driver::check(rc2) {
316                // The host pages are already registered at this point; if we
317                // returned directly here without unregistering, the
318                // registration (and its pinned pages) would leak since the
319                // caller never receives a `RegisteredMemory` handle to drop.
320                let rc_unreg = unsafe { (api.cu_mem_host_unregister)(ptr.cast::<c_void>()) };
321                if rc_unreg != 0 {
322                    tracing::warn!(
323                        cuda_error = rc_unreg,
324                        "cuMemHostUnregister failed while rolling back after \
325                         cuMemHostGetDevicePointer error"
326                    );
327                }
328                return Err(e);
329            }
330            dptr
331        } else {
332            0
333        };
334
335        Ok(RegisteredMemory {
336            ptr,
337            len,
338            flags,
339            device_ptr,
340        })
341    }
342}
343
344/// Convenience: registers a mutable slice with the CUDA driver.
345///
346/// # Errors
347///
348/// Same as [`register`].
349pub fn register_slice<T: Copy>(
350    slice: &mut [T],
351    flags: RegisterFlags,
352) -> CudaResult<RegisteredMemory<T>> {
353    register(slice.as_mut_ptr(), slice.len(), flags)
354}
355
356/// Convenience: registers a `Vec<T>` with the CUDA driver.
357///
358/// The `Vec` must not be reallocated (e.g. via `push`, `resize`) while the
359/// returned handle is alive, as that would invalidate the registered pointer.
360///
361/// # Errors
362///
363/// Same as [`register`].
364pub fn register_vec<T: Copy>(
365    vec: &mut Vec<T>,
366    flags: RegisterFlags,
367) -> CudaResult<RegisteredMemory<T>> {
368    register(vec.as_mut_ptr(), vec.len(), flags)
369}
370
371// ---------------------------------------------------------------------------
372// Pointer query
373// ---------------------------------------------------------------------------
374
375/// The type of memory backing a registered pointer.
376#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
377pub enum RegisteredMemoryType {
378    /// Host (system) memory.
379    Host,
380    /// Device (GPU) memory.
381    Device,
382    /// Unified (managed) memory.
383    Unified,
384    /// Pointer is not registered with CUDA.
385    Unregistered,
386}
387
388impl fmt::Display for RegisteredMemoryType {
389    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390        match self {
391            Self::Host => write!(f, "Host"),
392            Self::Device => write!(f, "Device"),
393            Self::Unified => write!(f, "Unified"),
394            Self::Unregistered => write!(f, "Unregistered"),
395        }
396    }
397}
398
399/// Information about a pointer registered with the CUDA driver.
400#[derive(Debug, Clone, Copy)]
401pub struct RegisteredPointerInfo {
402    /// Device pointer corresponding to the registered host pointer.
403    pub device_ptr: CUdeviceptr,
404    /// Whether the memory is managed (unified).
405    pub is_managed: bool,
406    /// The type of memory backing the pointer.
407    pub memory_type: RegisteredMemoryType,
408}
409
410/// Queries the CUDA driver for information about a registered pointer.
411///
412/// # Errors
413///
414/// * [`CudaError::NotSupported`] on macOS.
415/// * [`CudaError::InvalidValue`] if the pointer is not known to the driver.
416/// * Other driver errors from `cuPointerGetAttribute`.
417pub fn query_registered_pointer_info(ptr: *const u8) -> CudaResult<RegisteredPointerInfo> {
418    if ptr.is_null() {
419        return Err(CudaError::InvalidValue);
420    }
421
422    #[cfg(target_os = "macos")]
423    {
424        // Synthetic response for macOS tests.
425        Ok(RegisteredPointerInfo {
426            device_ptr: ptr as CUdeviceptr,
427            is_managed: false,
428            memory_type: RegisteredMemoryType::Host,
429        })
430    }
431
432    #[cfg(not(target_os = "macos"))]
433    {
434        let api = try_driver()?;
435        let dev_ptr_val = ptr as CUdeviceptr;
436
437        // Query memory type.
438        let mut mem_type: u32 = 0;
439        let rc = unsafe {
440            (api.cu_pointer_get_attribute)(
441                (&mut mem_type as *mut u32).cast::<c_void>(),
442                ffi::CU_POINTER_ATTRIBUTE_MEMORY_TYPE,
443                dev_ptr_val,
444            )
445        };
446        let memory_type = if rc != 0 {
447            // If the query fails, the pointer is likely unregistered.
448            RegisteredMemoryType::Unregistered
449        } else {
450            match mem_type {
451                ffi::CU_MEMORYTYPE_HOST => RegisteredMemoryType::Host,
452                ffi::CU_MEMORYTYPE_DEVICE => RegisteredMemoryType::Device,
453                ffi::CU_MEMORYTYPE_UNIFIED => RegisteredMemoryType::Unified,
454                _ => RegisteredMemoryType::Unregistered,
455            }
456        };
457
458        // Query is_managed.
459        let mut managed: u32 = 0;
460        let rc2 = unsafe {
461            (api.cu_pointer_get_attribute)(
462                (&mut managed as *mut u32).cast::<c_void>(),
463                ffi::CU_POINTER_ATTRIBUTE_IS_MANAGED,
464                dev_ptr_val,
465            )
466        };
467        let is_managed = rc2 == 0 && managed != 0;
468
469        // Query device pointer.
470        let mut dptr: CUdeviceptr = 0;
471        let rc3 = unsafe {
472            (api.cu_pointer_get_attribute)(
473                (&mut dptr as *mut CUdeviceptr).cast::<c_void>(),
474                ffi::CU_POINTER_ATTRIBUTE_DEVICE_POINTER,
475                dev_ptr_val,
476            )
477        };
478        if rc3 != 0 {
479            dptr = 0;
480        }
481
482        Ok(RegisteredPointerInfo {
483            device_ptr: dptr,
484            is_managed,
485            memory_type,
486        })
487    }
488}
489
490// ---------------------------------------------------------------------------
491// Tests
492// ---------------------------------------------------------------------------
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497
498    // -- RegisterFlags tests -----------------------------------------------
499
500    #[test]
501    fn flags_default_contains_portable_and_device_map() {
502        assert!(RegisterFlags::DEFAULT.contains(RegisterFlags::PORTABLE));
503        assert!(RegisterFlags::DEFAULT.contains(RegisterFlags::DEVICE_MAP));
504        assert!(!RegisterFlags::DEFAULT.contains(RegisterFlags::IO_MEMORY));
505        assert!(!RegisterFlags::DEFAULT.contains(RegisterFlags::READ_ONLY));
506    }
507
508    #[test]
509    fn flags_bitor_combines() {
510        let combined = RegisterFlags::PORTABLE | RegisterFlags::READ_ONLY;
511        assert!(combined.contains(RegisterFlags::PORTABLE));
512        assert!(combined.contains(RegisterFlags::READ_ONLY));
513        assert!(!combined.contains(RegisterFlags::IO_MEMORY));
514    }
515
516    #[test]
517    fn flags_bitand_intersects() {
518        let a = RegisterFlags::PORTABLE | RegisterFlags::DEVICE_MAP;
519        let b = RegisterFlags::PORTABLE | RegisterFlags::READ_ONLY;
520        let intersected = a & b;
521        assert!(intersected.contains(RegisterFlags::PORTABLE));
522        assert!(!intersected.contains(RegisterFlags::DEVICE_MAP));
523        assert!(!intersected.contains(RegisterFlags::READ_ONLY));
524    }
525
526    #[test]
527    fn flags_display() {
528        assert_eq!(RegisterFlags::NONE.to_string(), "NONE");
529        assert_eq!(RegisterFlags::PORTABLE.to_string(), "PORTABLE");
530        let default_str = RegisterFlags::DEFAULT.to_string();
531        assert!(default_str.contains("PORTABLE"));
532        assert!(default_str.contains("DEVICE_MAP"));
533    }
534
535    #[test]
536    fn flags_bits_roundtrip() {
537        let flags = RegisterFlags::PORTABLE | RegisterFlags::IO_MEMORY;
538        let bits = flags.bits();
539        assert_eq!(RegisterFlags::from_bits(bits), flags);
540    }
541
542    #[test]
543    fn flags_none_is_zero() {
544        assert_eq!(RegisterFlags::NONE.bits(), 0);
545    }
546
547    // -- RegisteredMemoryType tests ----------------------------------------
548
549    #[test]
550    fn memory_type_display() {
551        assert_eq!(RegisteredMemoryType::Host.to_string(), "Host");
552        assert_eq!(RegisteredMemoryType::Device.to_string(), "Device");
553        assert_eq!(RegisteredMemoryType::Unified.to_string(), "Unified");
554        assert_eq!(
555            RegisteredMemoryType::Unregistered.to_string(),
556            "Unregistered"
557        );
558    }
559
560    #[test]
561    fn memory_type_equality() {
562        assert_eq!(RegisteredMemoryType::Host, RegisteredMemoryType::Host);
563        assert_ne!(RegisteredMemoryType::Host, RegisteredMemoryType::Device);
564    }
565
566    // -- register / RegisteredMemory tests ---------------------------------
567
568    #[test]
569    fn register_zero_len_fails() {
570        let mut buf = [0u8; 16];
571        let result = register(buf.as_mut_ptr(), 0, RegisterFlags::DEFAULT);
572        assert!(matches!(result, Err(CudaError::InvalidValue)));
573    }
574
575    #[test]
576    fn register_null_ptr_fails() {
577        let result = register::<u8>(std::ptr::null_mut(), 10, RegisterFlags::DEFAULT);
578        assert!(matches!(result, Err(CudaError::InvalidValue)));
579    }
580
581    #[test]
582    fn register_slice_zero_len_fails() {
583        let mut empty: [f32; 0] = [];
584        let result = register_slice(&mut empty, RegisterFlags::DEFAULT);
585        assert!(matches!(result, Err(CudaError::InvalidValue)));
586    }
587
588    #[test]
589    fn register_vec_zero_len_fails() {
590        let mut v: Vec<i32> = Vec::new();
591        let result = register_vec(&mut v, RegisterFlags::DEFAULT);
592        assert!(matches!(result, Err(CudaError::InvalidValue)));
593    }
594
595    #[test]
596    fn query_null_ptr_fails() {
597        let result = query_registered_pointer_info(std::ptr::null());
598        assert!(matches!(result, Err(CudaError::InvalidValue)));
599    }
600
601    // -- macOS synthetic tests (these run on all platforms for validation) --
602
603    #[cfg(target_os = "macos")]
604    mod macos_tests {
605        use super::*;
606
607        #[test]
608        fn register_slice_succeeds_on_macos() {
609            let mut data = vec![1.0f32, 2.0, 3.0, 4.0];
610            let reg = register_slice(data.as_mut_slice(), RegisterFlags::DEFAULT);
611            let reg = reg.ok();
612            assert!(reg.is_some());
613            let reg = reg.inspect(|r| {
614                assert_eq!(r.len(), 4);
615                assert!(!r.is_empty());
616                assert_eq!(r.flags(), RegisterFlags::DEFAULT);
617                assert_eq!(r.as_slice(), &[1.0, 2.0, 3.0, 4.0]);
618            });
619            drop(reg);
620        }
621
622        #[test]
623        fn register_vec_succeeds_on_macos() {
624            let mut v = vec![10u32, 20, 30];
625            let reg = register_vec(&mut v, RegisterFlags::PORTABLE);
626            assert!(reg.is_ok());
627            if let Ok(r) = reg {
628                assert_eq!(r.len(), 3);
629                assert_eq!(r.flags(), RegisterFlags::PORTABLE);
630                assert_ne!(r.device_ptr(), 0);
631            }
632        }
633
634        #[test]
635        fn registered_memory_deref_works() {
636            let mut data = vec![100i64, 200, 300];
637            let reg = register_vec(&mut data, RegisterFlags::DEFAULT);
638            assert!(reg.is_ok());
639            if let Ok(r) = reg {
640                // Deref to &[T]
641                let slice: &[i64] = &r;
642                assert_eq!(slice.len(), 3);
643                assert_eq!(slice[0], 100);
644                assert_eq!(slice[2], 300);
645            }
646        }
647
648        #[test]
649        fn registered_memory_deref_mut_works() {
650            let mut data = vec![1u8, 2, 3, 4, 5];
651            let reg = register_slice(&mut data, RegisterFlags::DEFAULT);
652            assert!(reg.is_ok());
653            if let Ok(mut r) = reg {
654                r[0] = 99;
655                assert_eq!(r[0], 99);
656                let mslice: &mut [u8] = &mut r;
657                mslice[4] = 88;
658                assert_eq!(mslice[4], 88);
659            }
660        }
661
662        #[test]
663        fn query_pointer_info_on_macos() {
664            let data = [42u8; 64];
665            let info = query_registered_pointer_info(data.as_ptr());
666            assert!(info.is_ok());
667            if let Ok(info) = info {
668                assert!(!info.is_managed);
669                assert_eq!(info.memory_type, RegisteredMemoryType::Host);
670                assert_ne!(info.device_ptr, 0);
671            }
672        }
673
674        #[test]
675        fn registered_memory_as_ptr_mut_ptr() {
676            let mut data = vec![5.0f64; 10];
677            let original_ptr = data.as_mut_ptr();
678            let reg = register_vec(&mut data, RegisterFlags::DEFAULT);
679            assert!(reg.is_ok());
680            if let Ok(mut r) = reg {
681                assert_eq!(r.as_ptr(), original_ptr as *const f64);
682                assert_eq!(r.as_mut_ptr(), original_ptr);
683            }
684        }
685    }
686
687    // -- GPU integration tests (require real hardware) ---------------------
688
689    #[cfg(feature = "gpu-tests")]
690    mod gpu_tests {
691        use super::*;
692
693        #[test]
694        fn register_and_unregister_on_gpu() {
695            // cuMemHostRegister requires an active CUDA context bound to the
696            // calling thread.  Create one via Context::new (which calls
697            // cuCtxCreate, making the context current).  Skip the test if no
698            // GPU or driver is available.
699            if oxicuda_driver::init().is_err() || oxicuda_driver::Device::count().unwrap_or(0) == 0
700            {
701                return;
702            }
703            let Ok(dev) = oxicuda_driver::Device::get(0) else {
704                return;
705            };
706            let Ok(_ctx) = oxicuda_driver::Context::new(&dev) else {
707                return;
708            };
709            // _ctx keeps the CUDA context alive and current for this thread.
710
711            let mut data = vec![0.0f32; 4096];
712            let reg = register_vec(&mut data, RegisterFlags::DEFAULT);
713            assert!(reg.is_ok(), "registration failed: {:?}", reg.err());
714            if let Ok(r) = reg {
715                assert_eq!(r.len(), 4096);
716                assert!(r.device_ptr() != 0, "device_ptr should be non-zero");
717            }
718        }
719    }
720}