Skip to main content

edgefirst_tensor/
cuda.rs

1// SPDX-FileCopyrightText: Copyright 2025 Au-Zone Technologies
2// SPDX-License-Identifier: Apache-2.0
3
4//! Optional CUDA Runtime interop, loaded via dlopen (no link-time dependency).
5//! Absent libcudart ⇒ every CUDA path degrades to `None`.
6//!
7//! The intended client pattern is a fast-fail + host fallback: call
8//! [`Tensor::cuda_map`](crate::Tensor::cuda_map) (or [`TensorDyn::cuda_map`](crate::TensorDyn::cuda_map))
9//! first; if it returns `None` (no CUDA handle attached or libcudart absent),
10//! fall back to the host mapping via `Tensor::map`. This keeps hot paths
11//! zero-copy on CUDA-capable hardware while remaining correct on CPU-only targets.
12use libloading::Library;
13#[cfg(target_os = "windows")]
14use std::ffi::c_char;
15use std::ffi::c_void;
16use std::os::raw::{c_int, c_uint};
17#[cfg(all(target_os = "windows", feature = "static"))]
18use std::os::windows::io::{AsRawHandle, OwnedHandle, RawHandle};
19use std::sync::{Arc, OnceLock};
20#[cfg(target_os = "windows")]
21use windows::core::PCWSTR;
22#[cfg(target_os = "windows")]
23use windows::Win32::System::LibraryLoader::{GetModuleFileNameW, GetModuleHandleW};
24
25pub(crate) type CudaError = c_int; // cudaSuccess == 0
26pub type GraphicsResource = *mut c_void; // cudaGraphicsResource_t
27pub(crate) type ExternalMemory = *mut c_void; // cudaExternalMemory_t
28pub type CudaStream = *mut c_void; // cudaStream_t
29#[cfg(target_os = "windows")]
30pub(crate) type MipmappedArray = *mut c_void; // cudaMipmappedArray_t
31#[cfg(target_os = "windows")]
32pub(crate) type CudaArray = *mut c_void; // cudaArray_t
33#[cfg(target_os = "windows")]
34pub(crate) type ExternalSemaphore = *mut c_void; // cudaExternalSemaphore_t
35
36#[allow(non_snake_case, dead_code)]
37pub(crate) struct CudaTable {
38    // Owns the loaded module so it stays mapped for as long as the function
39    // pointers below are callable. `CudaTable` only ever lives inside the
40    // `'static TABLE` OnceLock (never dropped for the life of the process),
41    // so this need not be leaked separately.
42    _lib: Library,
43    /// The runtime `load()` actually opened: a `CUDA_PATH\bin\...` candidate
44    /// is recorded verbatim (loading a full path is unambiguous); a bare
45    /// name (Windows) is resolved to the absolute path Windows mapped via
46    /// `GetModuleHandleW`/`GetModuleFileNameW`, falling back to the bare
47    /// name if that resolution fails. On Linux this is the soname that
48    /// opened. Several toolkits on a dev box can share a bare DLL/soname, so
49    /// this is what makes "which runtime is CUDA actually using" answerable.
50    pub runtime_path: std::path::PathBuf,
51    pub graphics_gl_register_buffer:
52        unsafe extern "C" fn(*mut GraphicsResource, c_uint, c_uint) -> CudaError,
53    pub graphics_map_resources:
54        unsafe extern "C" fn(c_int, *mut GraphicsResource, *mut c_void) -> CudaError,
55    pub graphics_get_mapped_pointer:
56        unsafe extern "C" fn(*mut *mut c_void, *mut usize, GraphicsResource) -> CudaError,
57    pub graphics_unmap_resources:
58        unsafe extern "C" fn(c_int, *mut GraphicsResource, *mut c_void) -> CudaError,
59    pub graphics_unregister_resource: unsafe extern "C" fn(GraphicsResource) -> CudaError,
60    pub import_external_memory:
61        unsafe extern "C" fn(*mut ExternalMemory, *const c_void) -> CudaError,
62    pub external_memory_get_mapped_buffer:
63        unsafe extern "C" fn(*mut *mut c_void, ExternalMemory, *const c_void) -> CudaError,
64    pub destroy_external_memory: unsafe extern "C" fn(ExternalMemory) -> CudaError,
65    pub memcpy: unsafe extern "C" fn(*mut c_void, *const c_void, usize, c_int) -> CudaError,
66    pub free: unsafe extern "C" fn(*mut c_void) -> CudaError,
67    pub stream_create: unsafe extern "C" fn(*mut CudaStream) -> CudaError,
68    pub stream_synchronize: unsafe extern "C" fn(CudaStream) -> CudaError,
69    pub stream_destroy: unsafe extern "C" fn(CudaStream) -> CudaError,
70    // D3D11 texture interop (Task C2 consumes these; only meaningful where D3D11
71    // exists, so Windows-only both here and in `load_table`).
72    #[cfg(target_os = "windows")]
73    pub d3d11_get_device: unsafe extern "C" fn(*mut c_int, *mut c_void) -> CudaError,
74    #[cfg(target_os = "windows")]
75    pub set_device: unsafe extern "C" fn(c_int) -> CudaError,
76    #[cfg(target_os = "windows")]
77    pub external_memory_get_mapped_mipmapped_array:
78        unsafe extern "C" fn(*mut MipmappedArray, ExternalMemory, *const c_void) -> CudaError,
79    #[cfg(target_os = "windows")]
80    pub get_mipmapped_array_level:
81        unsafe extern "C" fn(*mut CudaArray, MipmappedArray, c_uint) -> CudaError,
82    #[cfg(target_os = "windows")]
83    pub free_mipmapped_array: unsafe extern "C" fn(MipmappedArray) -> CudaError,
84    // The async spellings, not the plain ones: a device-to-device
85    // `cudaMemcpy2D*` runs on the legacy default stream, and synchronising
86    // that is a device-wide barrier that also waits behind every other stream
87    // this crate hands out (the codec's, for one). These take the handle's own
88    // stream, so the wait, the copy and the synchronisation are one sequence
89    // on one stream.
90    #[cfg(target_os = "windows")]
91    pub memcpy_2d_from_array_async: unsafe extern "C" fn(
92        *mut c_void,
93        usize,
94        CudaArray,
95        usize,
96        usize,
97        usize,
98        usize,
99        c_int,
100        CudaStream,
101    ) -> CudaError,
102    #[cfg(target_os = "windows")]
103    pub memcpy_2d_to_array_async: unsafe extern "C" fn(
104        CudaArray,
105        usize,
106        usize,
107        *const c_void,
108        usize,
109        usize,
110        usize,
111        c_int,
112        CudaStream,
113    ) -> CudaError,
114    #[cfg(target_os = "windows")]
115    pub malloc: unsafe extern "C" fn(*mut *mut c_void, usize) -> CudaError,
116    #[cfg(target_os = "windows")]
117    pub import_external_semaphore:
118        unsafe extern "C" fn(*mut ExternalSemaphore, *const c_void) -> CudaError,
119    #[cfg(target_os = "windows")]
120    pub wait_external_semaphores_async: unsafe extern "C" fn(
121        *const ExternalSemaphore,
122        *const c_void,
123        c_uint,
124        CudaStream,
125    ) -> CudaError,
126    #[cfg(target_os = "windows")]
127    pub destroy_external_semaphore: unsafe extern "C" fn(ExternalSemaphore) -> CudaError,
128    #[cfg(target_os = "windows")]
129    pub get_error_string: unsafe extern "C" fn(CudaError) -> *const c_char,
130}
131
132static TABLE: OnceLock<Option<CudaTable>> = OnceLock::new();
133
134// Miri cannot execute `dlopen` -- it is a foreign function Miri's
135// interpreter refuses outright ("can't call foreign function `dlopen`"),
136// not merely a syscall it emulates differently. `Tensor::new` calls
137// `try_init_dma_cuda` unconditionally on Linux (`lib.rs`), which calls
138// `is_cuda_available` -> `table` -> `load` before it even checks whether
139// the tensor is DMA-backed, so this fires on every tensor construction
140// under Miri, DMA or not. Reporting "unavailable" here is not suppressing
141// a finding: this module's own contract is "absent libcudart => every CUDA
142// path degrades to `None`" (see the module doc), and under Miri libcudart
143// is, honestly, never going to be found.
144#[cfg(miri)]
145fn load() -> Option<CudaTable> {
146    None
147}
148
149#[cfg(all(not(miri), not(target_os = "windows")))]
150fn load() -> Option<CudaTable> {
151    for name in ["libcudart.so", "libcudart.so.12", "libcudart.so.11.0"] {
152        // SAFETY: `name` is one of the fixed soname literals above. Loading
153        // a shared object runs its init routines, an unavoidable side effect
154        // of dynamic loading that this whole module's design accepts (see
155        // the module doc: "absent libcudart => every CUDA path degrades to
156        // `None`" is the only contract offered).
157        let Ok(lib) = (unsafe { Library::new(name) }) else {
158            continue;
159        };
160        // `load_table` takes ownership of `lib`; if a required symbol is
161        // missing it returns `None` and its local `lib` parameter drops
162        // (unloading this soname), so the next candidate is tried rather
163        // than disabling CUDA outright for a stale library earlier in the
164        // search order.
165        if let Some(table) = load_table(lib, std::path::PathBuf::from(name)) {
166            log::debug!("CUDA runtime loaded from {}", table.runtime_path.display());
167            return Some(table);
168        }
169    }
170    None
171}
172
173#[cfg(all(not(miri), target_os = "windows"))]
174fn load() -> Option<CudaTable> {
175    // Newest runtime first; consumers ship their own, the HAL only needs one
176    // that resolves the interop entry points. `cudart64_110.dll` is not on
177    // PATH/System32 by default, so also try CUDA_PATH\bin (where the
178    // installer puts it) for each candidate name. `is_full_path` marks the
179    // `CUDA_PATH\bin` entries: loading a full path is unambiguous, so their
180    // `runtime_path` is the candidate itself; a bare name resolves through
181    // the OS's DLL search order and needs `resolve_loaded_module_path` to
182    // learn which physical file actually opened.
183    let names = ["cudart64_13.dll", "cudart64_12.dll", "cudart64_110.dll"];
184    let mut candidates: Vec<(std::path::PathBuf, bool)> = names
185        .iter()
186        .map(|n| (std::path::PathBuf::from(n), false))
187        .collect();
188    if let Ok(cuda_path) = std::env::var("CUDA_PATH") {
189        for n in names {
190            let full = std::path::Path::new(&cuda_path).join("bin").join(n);
191            candidates.push((full, true));
192        }
193    }
194    for (candidate, is_full_path) in &candidates {
195        // SAFETY: `candidate` is a `.dll` filename or full path built above.
196        // Loading a module runs its init routines, an unavoidable side
197        // effect of dynamic loading that this whole module's design accepts
198        // (see the module doc: "absent libcudart => every CUDA path
199        // degrades to `None`" is the only contract offered).
200        let Ok(lib) = (unsafe { Library::new(candidate) }) else {
201            continue;
202        };
203        let runtime_path = if *is_full_path {
204            candidate.clone()
205        } else {
206            resolve_loaded_module_path(candidate.as_os_str()).unwrap_or_else(|| candidate.clone())
207        };
208        // `load_table` takes ownership of `lib`; if a required symbol is
209        // missing it returns `None` and its local `lib` parameter drops
210        // (unloading this candidate), so the next one is tried rather than
211        // disabling CUDA outright for a stale toolkit earlier on PATH.
212        if let Some(table) = load_table(lib, runtime_path) {
213            log::debug!("CUDA runtime loaded from {}", table.runtime_path.display());
214            return Some(table);
215        }
216    }
217    None
218}
219
220/// Resolves a bare DLL name (no directory) to the absolute path Windows
221/// actually mapped, via `GetModuleHandleW` + `GetModuleFileNameW`. Callers
222/// only use this immediately after `Library::new` already succeeded for the
223/// same name, so the module is already loaded: `GetModuleHandleW` looks up
224/// its existing handle, it does not load a second copy or re-run init
225/// routines. Returns `None` on any resolution failure; the caller falls back
226/// to the bare name it tried.
227#[cfg(target_os = "windows")]
228fn resolve_loaded_module_path(name: &std::ffi::OsStr) -> Option<std::path::PathBuf> {
229    use std::os::windows::ffi::{OsStrExt, OsStringExt};
230    let wide: Vec<u16> = name.encode_wide().chain(std::iter::once(0)).collect();
231    // SAFETY: `wide` is a NUL-terminated UTF-16 buffer kept alive for the
232    // call. `GetModuleHandleW` only resolves an already-loaded module's
233    // handle by name -- it neither loads nor initializes anything.
234    let handle = unsafe { GetModuleHandleW(PCWSTR(wide.as_ptr())) }.ok()?;
235    let mut buf = [0u16; 1024];
236    // SAFETY: `handle` was just returned by a successful `GetModuleHandleW`
237    // for a module already loaded in this process; `buf` is a correctly
238    // sized mutable out-buffer for `GetModuleFileNameW` to write into.
239    let len = unsafe { GetModuleFileNameW(Some(handle), &mut buf) } as usize;
240    // `len == buf.len()` is truncation: the path did not fit and Windows did
241    // not terminate it, so the prefix would name a different file.
242    (len > 0 && len < buf.len())
243        .then(|| std::path::PathBuf::from(std::ffi::OsString::from_wide(&buf[..len])))
244}
245
246/// Resolves every interop symbol shared by both platforms' `load()`. Returns
247/// `None` and logs the missing symbol's name if any required entry point is
248/// absent from `lib` -- including the D3D11 entries on Windows, where they
249/// are not optional. Takes ownership of `lib`: on success it is stored in
250/// the returned `CudaTable`; on failure it is simply dropped here (unloading
251/// the module), so the caller is free to try its next candidate.
252#[cfg(not(miri))]
253fn load_table(lib: Library, runtime_path: std::path::PathBuf) -> Option<CudaTable> {
254    macro_rules! sym {
255        ($n:literal) => {{
256            // SAFETY: `$n` is one of the fixed `cuda*` symbol-name literals
257            // below; `lib` is the module `load()` just opened, and the
258            // resulting function pointer is only ever called through the
259            // `unsafe extern "C" fn` field type it is stored into, at each
260            // call site's own risk.
261            match unsafe { lib.get(concat!($n, "\0").as_bytes()) } {
262                Ok(s) => *s,
263                Err(_) => {
264                    log::debug!("CUDA runtime missing required symbol {}", $n);
265                    return None;
266                }
267            }
268        }};
269    }
270    // `cudaWaitExternalSemaphoresAsync` gained a `_v2` export when the ABI of
271    // `cudaExternalSemaphoreWaitParams` changed (the `reserved` tail this
272    // module's `CudaExternalSemaphoreWaitParams` already carries); prefer it
273    // and fall back to the unsuffixed name for older runtimes that only have
274    // the v1 ABI symbol under the original name. The unsuffixed symbol on a
275    // pre-11.2 runtime expects the shorter v1 params layout
276    // (`cudaExternalSemaphoreWaitParams_v1`), whose `flags` sits at offset 32
277    // where v2 has `params_reserved[0]`. A v1 runtime therefore reads a
278    // different zeroed word as `flags` rather than omitting a field, and both
279    // words are zero here: `fence.value` is at offset 0 in either layout and
280    // is the only field this module sets.
281    #[cfg(target_os = "windows")]
282    macro_rules! sym_versioned {
283        ($v2:literal, $v1:literal) => {{
284            // SAFETY: `$v2`/`$v1` are fixed symbol-name literals; `lib` is
285            // the module `load()` just opened. Same call-site-risk contract
286            // as `sym!` above.
287            let found = unsafe { lib.get(concat!($v2, "\0").as_bytes()) }
288                .or_else(|_| unsafe { lib.get(concat!($v1, "\0").as_bytes()) });
289            match found {
290                Ok(s) => *s,
291                Err(_) => {
292                    log::debug!("CUDA runtime missing required symbol {} (or {})", $v2, $v1);
293                    return None;
294                }
295            }
296        }};
297    }
298    Some(CudaTable {
299        runtime_path,
300        graphics_gl_register_buffer: sym!("cudaGraphicsGLRegisterBuffer"),
301        graphics_map_resources: sym!("cudaGraphicsMapResources"),
302        graphics_get_mapped_pointer: sym!("cudaGraphicsResourceGetMappedPointer"),
303        graphics_unmap_resources: sym!("cudaGraphicsUnmapResources"),
304        graphics_unregister_resource: sym!("cudaGraphicsUnregisterResource"),
305        import_external_memory: sym!("cudaImportExternalMemory"),
306        external_memory_get_mapped_buffer: sym!("cudaExternalMemoryGetMappedBuffer"),
307        destroy_external_memory: sym!("cudaDestroyExternalMemory"),
308        memcpy: sym!("cudaMemcpy"),
309        free: sym!("cudaFree"),
310        stream_create: sym!("cudaStreamCreate"),
311        stream_synchronize: sym!("cudaStreamSynchronize"),
312        stream_destroy: sym!("cudaStreamDestroy"),
313        #[cfg(target_os = "windows")]
314        d3d11_get_device: sym!("cudaD3D11GetDevice"),
315        #[cfg(target_os = "windows")]
316        set_device: sym!("cudaSetDevice"),
317        #[cfg(target_os = "windows")]
318        external_memory_get_mapped_mipmapped_array: sym!(
319            "cudaExternalMemoryGetMappedMipmappedArray"
320        ),
321        #[cfg(target_os = "windows")]
322        get_mipmapped_array_level: sym!("cudaGetMipmappedArrayLevel"),
323        #[cfg(target_os = "windows")]
324        free_mipmapped_array: sym!("cudaFreeMipmappedArray"),
325        #[cfg(target_os = "windows")]
326        memcpy_2d_from_array_async: sym!("cudaMemcpy2DFromArrayAsync"),
327        #[cfg(target_os = "windows")]
328        memcpy_2d_to_array_async: sym!("cudaMemcpy2DToArrayAsync"),
329        #[cfg(target_os = "windows")]
330        malloc: sym!("cudaMalloc"),
331        #[cfg(target_os = "windows")]
332        import_external_semaphore: sym!("cudaImportExternalSemaphore"),
333        #[cfg(target_os = "windows")]
334        wait_external_semaphores_async: sym_versioned!(
335            "cudaWaitExternalSemaphoresAsync_v2",
336            "cudaWaitExternalSemaphoresAsync"
337        ),
338        #[cfg(target_os = "windows")]
339        destroy_external_semaphore: sym!("cudaDestroyExternalSemaphore"),
340        #[cfg(target_os = "windows")]
341        get_error_string: sym!("cudaGetErrorString"),
342        // Moved last: every `sym!`/`sym_versioned!` call above borrows
343        // `lib`, so the field that takes ownership of it must come after
344        // all of them in the struct literal (fields evaluate in the order
345        // written, not declaration order).
346        _lib: lib,
347    })
348}
349
350/// `cudaGetErrorString(rc)` with the numeric code kept alongside it, for the
351/// D3D11 arm's diagnostics. Falls back to the bare code if the runtime hands
352/// back no string.
353#[cfg(all(target_os = "windows", feature = "static"))]
354fn err_str(t: &CudaTable, rc: CudaError) -> String {
355    // SAFETY: `cudaGetErrorString` returns a pointer to a NUL-terminated
356    // string the runtime owns for the life of the process, or null for a code
357    // it does not know.
358    let p = unsafe { (t.get_error_string)(rc) };
359    if p.is_null() {
360        return format!("cudaError {rc}");
361    }
362    // SAFETY: `p` is the non-null NUL-terminated string from the call above.
363    let text = unsafe { std::ffi::CStr::from_ptr(p) }.to_string_lossy();
364    format!("{text} (cudaError {rc})")
365}
366
367pub(crate) fn table() -> Option<&'static CudaTable> {
368    TABLE.get_or_init(load).as_ref()
369}
370
371/// True iff libcudart loaded and all interop symbols resolved. Cached, cheap.
372pub fn is_cuda_available() -> bool {
373    table().is_some()
374}
375
376/// The runtime `load()` opened, if any -- see `CudaTable::runtime_path` for
377/// what this points to on each platform. `None` iff no runtime loaded.
378pub fn runtime_path() -> Option<&'static std::path::Path> {
379    table().map(|t| t.runtime_path.as_path())
380}
381
382/// `cudaMemcpyDeviceToHost` — copies from device (GPU) to host (CPU).
383pub const CUDA_MEMCPY_DEVICE_TO_HOST: c_int = 2;
384
385/// `cudaMemcpyHostToDevice` — copies from host (CPU) to device (GPU).
386pub const CUDA_MEMCPY_HOST_TO_DEVICE: c_int = 1;
387
388/// Copy `count` bytes from a CUDA device pointer to host. Returns `false` on
389/// failure or if libcudart is unavailable.
390///
391/// # Safety
392///
393/// The caller must ensure:
394/// - `host` points to at least `count` bytes of writable memory.
395/// - `device` points to at least `count` bytes of valid CUDA device memory
396///   (i.e. obtained from a `CudaMap::device_ptr()` while the map is live).
397pub unsafe fn memcpy_device_to_host(
398    host: *mut c_void,
399    device: *const c_void,
400    count: usize,
401) -> bool {
402    match table() {
403        Some(t) => unsafe { (t.memcpy)(host, device, count, CUDA_MEMCPY_DEVICE_TO_HOST) == 0 },
404        None => false,
405    }
406}
407
408/// Copy `count` bytes from host to a CUDA device pointer. Returns `false` on
409/// failure or if libcudart is unavailable.
410///
411/// The counterpart of [`memcpy_device_to_host`], for filling a writable
412/// mapping ([`CudaHandle::map_mut`]) from host bytes.
413///
414/// # Safety
415///
416/// The caller must ensure:
417/// - `host` points to at least `count` bytes of readable memory.
418/// - `device` points to at least `count` bytes of valid CUDA device memory
419///   (i.e. obtained from a `CudaMap::device_ptr()` while the map is live).
420pub unsafe fn memcpy_host_to_device(
421    device: *mut c_void,
422    host: *const c_void,
423    count: usize,
424) -> bool {
425    match table() {
426        Some(t) => unsafe { (t.memcpy)(device, host, count, CUDA_MEMCPY_HOST_TO_DEVICE) == 0 },
427        None => false,
428    }
429}
430
431/// Create a CUDA stream. Returns `None` if libcudart is unavailable or stream
432/// creation fails. The returned stream must be released with
433/// [`stream_destroy`]. Intended for clients (e.g. the codec's nvJPEG backend)
434/// that submit async device work and synchronise on it.
435pub fn stream_create() -> Option<CudaStream> {
436    let t = table()?;
437    let mut stream: CudaStream = std::ptr::null_mut();
438    if unsafe { (t.stream_create)(&mut stream) } != 0 {
439        return None;
440    }
441    Some(stream)
442}
443
444/// Block until all work submitted to `stream` completes. Returns `false` on
445/// failure or if libcudart is unavailable.
446///
447/// # Safety
448///
449/// `stream` must be a live stream returned by [`stream_create`] and not yet
450/// destroyed.
451pub unsafe fn stream_synchronize(stream: CudaStream) -> bool {
452    match table() {
453        Some(t) => unsafe { (t.stream_synchronize)(stream) == 0 },
454        None => false,
455    }
456}
457
458/// Destroy a CUDA stream. No-op if libcudart is unavailable.
459///
460/// # Safety
461///
462/// `stream` must be a live stream returned by [`stream_create`] and must not be
463/// used after this call.
464pub unsafe fn stream_destroy(stream: CudaStream) {
465    if let Some(t) = table() {
466        let _ = unsafe { (t.stream_destroy)(stream) };
467    }
468}
469
470/// Register a GL buffer (PBO) with CUDA. Returns the resource as `usize`
471/// (pointer) or `None`. MUST be called on the thread where the GL context is
472/// current.
473pub fn gl_register_buffer(buffer_id: u32) -> Option<usize> {
474    let t = table()?;
475    let mut res: GraphicsResource = std::ptr::null_mut();
476    // cudaGraphicsRegisterFlagsNone = 0
477    let rc = unsafe { (t.graphics_gl_register_buffer)(&mut res, buffer_id, 0) };
478    if rc != 0 {
479        log::debug!("cudaGraphicsGLRegisterBuffer(buffer={buffer_id}) failed: cudaError {rc}");
480        return None;
481    }
482    Some(res as usize)
483}
484
485/// Map a registered resource → `(device ptr as usize, size)`. GL-thread only.
486pub fn gl_map_resource(resource: usize) -> Option<(usize, usize)> {
487    let t = table()?;
488    let mut res = resource as GraphicsResource;
489    if unsafe { (t.graphics_map_resources)(1, &mut res, std::ptr::null_mut()) } != 0 {
490        return None;
491    }
492    let (mut ptr, mut size) = (std::ptr::null_mut::<c_void>(), 0usize);
493    if unsafe { (t.graphics_get_mapped_pointer)(&mut ptr, &mut size, res) } != 0 {
494        unsafe {
495            (t.graphics_unmap_resources)(1, &mut res, std::ptr::null_mut());
496        }
497        return None;
498    }
499    Some((ptr as usize, size))
500}
501
502/// Unmap a previously mapped resource. GL-thread only.
503pub fn gl_unmap_resource(resource: usize) {
504    if let Some(t) = table() {
505        let mut r = resource as GraphicsResource;
506        unsafe {
507            (t.graphics_unmap_resources)(1, &mut r, std::ptr::null_mut());
508        }
509    }
510}
511
512/// Unregister a previously registered resource. GL-thread only.
513pub fn gl_unregister_resource(resource: usize) {
514    if let Some(t) = table() {
515        unsafe {
516            (t.graphics_unregister_resource)(resource as GraphicsResource);
517        }
518    }
519}
520
521// =============================================================================
522// DMA-BUF → CUDA external memory import (thread-independent; no GL context).
523//
524// ABI verified against CUDA 12.6 driver_types.h, LP64, stable across CUDA
525// 11/12.  The structs are layout-asserted in the `ext_mem_layout` test module
526// below — no host with both /dev/dma_heap and CUDA is available in CI, so
527// runtime validation is deferred to on-target testing (orin-nano, gpu-probe O5
528// already confirmed cudaImportExternalMemory(OpaqueFd) works on Orin).
529// =============================================================================
530
531/// `cudaExternalMemoryHandleTypeOpaqueFd` — the only handle type used for
532/// Linux DMA-BUF fds. Value verified vs. driver_types.h for CUDA 11/12.
533#[allow(dead_code)] // only reached on Linux DMA tensors; kept cross-platform + ABI-tested
534pub(crate) const CUDA_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD: c_uint = 1;
535
536/// FFI mirror of `cudaExternalMemoryHandleDesc` (driver_types.h, LP64).
537///
538/// Layout (size 40, align 8):
539/// - `type_`       → int       @ 0
540/// - `_pad0`       → u32       @ 4  (align the union to 8)
541/// - `handle_fd`   → int       @ 8  (first member of the 16-byte union)
542/// - `_union_rest` → [u32; 3]  @ 12 (pads union to 16 bytes; ends at 24)
543/// - `size`        → u64       @ 24
544/// - `flags`       → c_uint    @ 32
545/// - `_tail`       → u32       @ 36 (struct size 40)
546#[allow(dead_code)] // only reached on Linux DMA tensors; kept cross-platform + ABI-tested
547#[repr(C)]
548pub(crate) struct CudaExternalMemoryHandleDesc {
549    pub type_: c_int,
550    pub _pad0: u32,
551    pub handle_fd: c_int,
552    pub _union_rest: [u32; 3],
553    pub size: u64,
554    pub flags: c_uint,
555    pub _tail: u32,
556}
557
558/// FFI mirror of `cudaExternalMemoryBufferDesc` (driver_types.h, LP64).
559///
560/// Layout (size 24, align 8):
561/// - `offset` → u64    @ 0
562/// - `size`   → u64    @ 8
563/// - `flags`  → c_uint @ 16
564/// - `_tail`  → u32    @ 20 (struct size 24)
565#[allow(dead_code)] // only reached on Linux DMA tensors; kept cross-platform + ABI-tested
566#[repr(C)]
567pub(crate) struct CudaExternalMemoryBufferDesc {
568    pub offset: u64,
569    pub size: u64,
570    pub flags: c_uint,
571    pub _tail: u32,
572}
573
574/// Import a DMA-BUF fd as CUDA external memory and map it to a device pointer.
575///
576/// Thread-independent — no GL context is required. `cudaImportExternalMemory`
577/// with OpaqueFd takes ownership of the fd it is given, so this function dups
578/// the caller's `fd` and hands CUDA the dup; the caller's `fd` is therefore
579/// untouched and remains owned by the caller. Returns `(ext_mem_handle,
580/// device_ptr)` on success, or `None` on any failure (missing libcudart,
581/// unsupported platform, dup failure, or driver error). The returned handle
582/// must be destroyed via `cudaDestroyExternalMemory` (done by [`CudaHandle`]
583/// drop), which also closes the dup'd fd.
584///
585/// # RUNTIME-UNVALIDATED
586/// No test platform has both `/dev/dma_heap` and a CUDA device. ABI is
587/// layout-asserted vs. CUDA 12.6 `driver_types.h`; the mechanism is proven
588/// by gpu-probe O5 on Orin. Best-effort: returns `None` on failure.
589// `static`-only: the sole caller, `Tensor::try_init_dma_cuda` (`lib.rs`), is
590// itself `#[cfg(feature = "static")]` (it reads `self.storage:
591// TensorStorage`, a `static`-only field). The rest of this module
592// (`CudaHandle`, `CudaMap`, `gl_register_buffer`, ...) stays available under
593// `dynamic` -- `dynamic_tensor::Tensor::cuda`/`cuda_map` return their types.
594#[cfg(all(target_os = "linux", feature = "static"))]
595pub(crate) fn import_dma_fd(fd: i32, size: usize) -> Option<(ExternalMemory, *mut c_void)> {
596    use std::os::fd::{AsRawFd, BorrowedFd, IntoRawFd};
597    let t = table()?;
598    // cudaExternalMemoryHandleTypeOpaqueFd TAKES OWNERSHIP of the fd on a
599    // successful import (CUDA closes it at cudaDestroyExternalMemory). The
600    // caller's fd is owned by TensorStorage::Dma and closed on tensor drop,
601    // so hand CUDA a dup to avoid a double-close.
602    // Keep the dup as OwnedFd until CUDA tells us whether it consumed it.
603    // `into_raw_fd` before the call left a window where a failed import that
604    // still closed the fd (observed on discrete NVIDIA + /dev/dma_heap) made
605    // `OwnedFd::from_raw_fd` + drop trip Rust's IO-safety abort.
606    let dup = unsafe { BorrowedFd::borrow_raw(fd) }
607        .try_clone_to_owned()
608        .ok()?;
609    let mut desc: CudaExternalMemoryHandleDesc = unsafe { std::mem::zeroed() };
610    desc.type_ = CUDA_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD as c_int;
611    desc.handle_fd = dup.as_raw_fd();
612    desc.size = size as u64;
613    let mut ext: ExternalMemory = std::ptr::null_mut();
614    let rc = unsafe { (t.import_external_memory)(&mut ext, &desc as *const _ as *const c_void) };
615    if rc != 0 {
616        log::debug!("cudaImportExternalMemory(OpaqueFd, size={size}) failed: cudaError {rc}");
617        // CUDA docs: a failed import does not consume the fd. Discrete NVIDIA
618        // drivers have been observed to close it anyway; reclaim via libc so
619        // an already-closed dup does not trip Rust's IO-safety abort.
620        let raw = dup.into_raw_fd();
621        if unsafe { libc::close(raw) } != 0 {
622            log::debug!(
623                "reclaim of CUDA-import dup fd {raw} after failed import: {}",
624                std::io::Error::last_os_error()
625            );
626        }
627        return None;
628    }
629    // Success: CUDA now owns the dup; it is closed by cudaDestroyExternalMemory.
630    let _ = dup.into_raw_fd();
631    let bdesc = CudaExternalMemoryBufferDesc {
632        offset: 0,
633        size: size as u64,
634        flags: 0,
635        _tail: 0,
636    };
637    let mut dptr: *mut c_void = std::ptr::null_mut();
638    if unsafe {
639        (t.external_memory_get_mapped_buffer)(&mut dptr, ext, &bdesc as *const _ as *const c_void)
640    } != 0
641    {
642        unsafe { (t.destroy_external_memory)(ext) };
643        return None;
644    }
645    Some((ext, dptr))
646}
647
648// =============================================================================
649// D3D11 texture → CUDA external memory import (Windows only; Task C2 is the
650// consumer -- `import_d3d11_texture`, the fence wait, and the array copies).
651//
652// Field offsets verified against this box's CUDA 11.8 `driver_types.h` by a
653// compiled offsetof program (`cl /nologo /I"%CUDA_PATH%\include" layouts.c`,
654// MSVC/VS 2026, x64); see the `d3d11_desc_layout` test module below for the
655// printed numbers.
656//
657// Layout rule: the three structs below (all but `cudaExternalSemaphoreWaitParams`,
658// which already matches 11.8) are laid out as the newest known toolkit
659// defines them, not as 11.8 defines them, because `load()` tries
660// `cudart64_13.dll` and `cudart64_12.dll` before `cudart64_110.dll` and a
661// newer runtime reads the struct size and trailing fields it expects, not
662// the ones 11.8 has. Each struct's own doc comment gives its exact offsets,
663// size and padding.
664// =============================================================================
665
666/// `cudaExternalMemoryHandleTypeD3D11Resource`.
667#[allow(dead_code)] // unused under the dynamic backend (the D3D11 arm is static-only)
668#[cfg(target_os = "windows")]
669pub(crate) const CUDA_EXTERNAL_MEMORY_HANDLE_TYPE_D3D11_RESOURCE: c_int = 5;
670/// `cudaExternalSemaphoreHandleTypeD3D11Fence`.
671#[allow(dead_code)] // unused under the dynamic backend (the D3D11 arm is static-only)
672#[cfg(target_os = "windows")]
673pub(crate) const CUDA_EXTERNAL_SEMAPHORE_HANDLE_TYPE_D3D11_FENCE: c_int = 4;
674/// `cudaExternalMemoryDedicated` — the imported resource is a dedicated
675/// allocation (required for D3D11 textures, as opposed to a suballocated
676/// buffer region).
677#[allow(dead_code)] // unused under the dynamic backend (the D3D11 arm is static-only)
678pub(crate) const CUDA_EXTERNAL_MEMORY_DEDICATED: c_uint = 1;
679/// `cudaMemcpyDeviceToDevice` — both endpoints of the copy are CUDA device
680/// memory (the imported array and the tensor's linear device buffer).
681#[allow(dead_code)] // unused under the dynamic backend (the D3D11 arm is static-only)
682pub(crate) const CUDA_MEMCPY_DEVICE_TO_DEVICE: c_int = 3;
683/// `cudaChannelFormatKindUnsigned` — integer texture formats (RGBA8, R8).
684#[allow(dead_code)] // unused under the dynamic backend (the D3D11 arm is static-only)
685pub(crate) const CUDA_CHANNEL_FORMAT_KIND_UNSIGNED: c_int = 1;
686/// `cudaChannelFormatKindFloat` — float texture formats (F16, F32).
687#[allow(dead_code)] // unused under the dynamic backend (the D3D11 arm is static-only)
688pub(crate) const CUDA_CHANNEL_FORMAT_KIND_FLOAT: c_int = 2;
689
690/// `cudaExternalMemoryHandleDesc` with the `win32` arm of the handle union
691/// populated (`type_`, `win32_handle`/`win32_name`, `size`, `flags`), laid
692/// out as CUDA 12.x/13.x's `driver_types.h` defines it (see the section
693/// comment above).
694///
695/// Field offsets `type_` @0, `win32_handle` @8, `win32_name` @16, `size`
696/// @24, `flags` @32 are measured against this box's CUDA 11.8
697/// `driver_types.h` (identical in 12.x/13.x, which only append `reserved`).
698/// Total size 104, align 8: `reserved[16]` @36..100, tail padding to 104
699/// (12.x/13.x add this trailing array; 11.8 does not, but ignores it since
700/// 11.8 only reads its own 40-byte prefix).
701#[allow(dead_code)] // unused under the dynamic backend (the D3D11 arm is static-only)
702#[cfg(target_os = "windows")]
703#[repr(C)]
704pub(crate) struct CudaExternalMemoryHandleDescWin32 {
705    pub type_: c_int,
706    pub _pad0: u32,
707    pub win32_handle: *mut c_void,
708    pub win32_name: *const c_void,
709    pub size: u64,
710    pub flags: c_uint,
711    pub reserved: [u32; 16],
712    pub _tail: u32,
713}
714
715/// `cudaChannelFormatDesc` (driver_types.h; size 20, align 4). Generic across
716/// platforms; only reached via the Windows mipmapped-array desc for now.
717#[allow(dead_code)] // unused under the dynamic backend (the D3D11 arm is static-only)
718#[repr(C)]
719#[derive(Clone, Copy)]
720pub(crate) struct CudaChannelFormatDesc {
721    pub x: c_int,
722    pub y: c_int,
723    pub z: c_int,
724    pub w: c_int,
725    pub f: c_int,
726}
727
728/// `cudaExtent` (driver_types.h; size 24, align 8). Generic across platforms;
729/// only reached via the Windows mipmapped-array desc for now.
730#[allow(dead_code)] // unused under the dynamic backend (the D3D11 arm is static-only)
731#[repr(C)]
732#[derive(Clone, Copy)]
733pub(crate) struct CudaExtent {
734    pub width: usize,
735    pub height: usize,
736    pub depth: usize,
737}
738
739/// `cudaExternalMemoryMipmappedArrayDesc`, laid out as CUDA 12.x/13.x's
740/// `driver_types.h` defines it (see the section comment above).
741///
742/// Field offsets `offset` @0, `format_desc` @8, `extent` @32, `flags` @56,
743/// `num_levels` @60 are measured against this box's CUDA 11.8
744/// `driver_types.h` (identical in 12.x/13.x, which only append `reserved`).
745/// Total size 128, align 8: `reserved[16]` @64..128, no further padding
746/// needed (12.x/13.x add this trailing array; 11.8 does not, but ignores it
747/// since 11.8 only reads its own 64-byte prefix).
748#[allow(dead_code)] // unused under the dynamic backend (the D3D11 arm is static-only)
749#[cfg(target_os = "windows")]
750#[repr(C)]
751pub(crate) struct CudaExternalMemoryMipmappedArrayDesc {
752    pub offset: u64,
753    pub format_desc: CudaChannelFormatDesc,
754    pub _pad0: u32,
755    pub extent: CudaExtent,
756    pub flags: c_uint,
757    pub num_levels: c_uint,
758    pub reserved: [u32; 16],
759}
760
761/// `cudaExternalSemaphoreHandleDesc` with the `win32` arm of the handle
762/// union populated, laid out as CUDA 12.x/13.x's `driver_types.h` defines it
763/// (see the section comment above).
764///
765/// Field offsets `type_` @0, `win32_handle` @8, `win32_name` @16, `flags`
766/// @24 are measured against this box's CUDA 11.8 `driver_types.h` (identical
767/// in 12.x/13.x, which only append `reserved`). Total size 96, align 8:
768/// `reserved[16]` @28..92, tail padding to 96 (12.x/13.x add this trailing
769/// array; 11.8 does not, but ignores it since 11.8 only reads its own
770/// 32-byte prefix).
771#[allow(dead_code)] // unused under the dynamic backend (the D3D11 arm is static-only)
772#[cfg(target_os = "windows")]
773#[repr(C)]
774pub(crate) struct CudaExternalSemaphoreHandleDesc {
775    pub type_: c_int,
776    pub _pad0: u32,
777    pub win32_handle: *mut c_void,
778    pub win32_name: *const c_void,
779    pub flags: c_uint,
780    pub reserved: [u32; 16],
781    pub _tail: u32,
782}
783
784/// `cudaExternalSemaphoreWaitParams`.
785///
786/// Layout verified against CUDA 11.8 `driver_types.h` (size 144, align 8):
787/// `params.fence.value` @0, `params.nvSciSync` @8, `params.keyedMutex.key`
788/// @16, `params.keyedMutex.timeoutMs` @24, `params.reserved[10]` @32,
789/// `flags` @72, `reserved[16]` @76, tail padding to 144. This struct already
790/// carries the reserved arrays in CUDA 11.8, so its size and offsets match
791/// the task brief's guess exactly (unlike the three structs above).
792#[allow(dead_code)] // unused under the dynamic backend (the D3D11 arm is static-only)
793#[cfg(target_os = "windows")]
794#[repr(C)]
795pub(crate) struct CudaExternalSemaphoreWaitParams {
796    pub fence_value: u64,
797    pub nv_sci_sync: u64,
798    pub keyed_mutex_key: u64,
799    pub keyed_mutex_timeout_ms: c_uint,
800    pub _pad0: u32,
801    pub params_reserved: [u32; 10],
802    pub flags: c_uint,
803    pub reserved: [u32; 16],
804    pub _tail: u32,
805}
806
807/// The CUDA side of one D3D11 texture tensor: the imported external memory,
808/// the level-0 array it maps to, and the linear device buffer every map copies
809/// through.
810///
811/// A CUDA array is opaque, tiled memory -- no consumer can index it as a
812/// device pointer -- so a map turns it into the tensor's tight rows with one
813/// `cudaMemcpy2DFromArray`, and a writable map's release turns them back with
814/// one `cudaMemcpy2DToArray`. The linear buffer is allocated on the first map
815/// and then reused, so a per-frame consumer pays one `cudaMalloc` in total.
816///
817/// `static`-only for the same reason `d3d11::texture` is: `dynamic` forwards
818/// every tensor call to the C ABI and never constructs a texture storage, so
819/// nothing there can produce this backing.
820#[cfg(all(target_os = "windows", feature = "static"))]
821struct D3d11External {
822    ext_mem: ExternalMemory,
823    mipmapped: MipmappedArray,
824    level0: CudaArray,
825    /// The linear buffer, allocated on first use: `map` takes `&self`, so the
826    /// slot is behind a lock, which also serialises two concurrent maps that
827    /// would otherwise copy into one buffer at once.
828    linear: std::sync::Mutex<Option<*mut c_void>>,
829    bytes: usize,
830    row_bytes: usize,
831    rows: usize,
832    stream: CudaStream,
833    /// The CUDA device the D3D11 adapter maps to. Set again before every copy
834    /// and before the release: `cudaSetDevice` is per-thread state, so a
835    /// consumer thread that last used another GPU would otherwise allocate the
836    /// linear buffer on it and fail to use this handle's stream.
837    device: c_int,
838    /// The process fence imported as an external semaphore, shared by every
839    /// D3D11 import in this copy of the crate (see [`shared_semaphore`]).
840    ///
841    /// `None` when the device has no shared fence or the import failed. A map
842    /// of a tensor with **no** recorded write then copies without waiting,
843    /// which is correct -- there is nothing to wait for. A map of a tensor
844    /// that *does* carry a recorded write declines instead: it has something
845    /// to wait for and no way to, and the caller's host-map fallback is
846    /// ordered by the D3D11 immediate context.
847    semaphore: Option<&'static SharedSemaphore>,
848    /// The newest D3D11 fence value recorded on the tensor, shared with the
849    /// storage's own slot: a map waits for it before reading the array.
850    last_write: Arc<std::sync::atomic::AtomicU64>,
851}
852
853/// Imports a D3D11 texture's shared NT handle as CUDA external memory, maps
854/// its level-0 array and imports `fence_handle` as an external semaphore so
855/// maps can order their copies after the D3D11 work a producer signalled.
856///
857/// `handle` stays owned by the caller: CUDA duplicates a Win32 handle it
858/// imports rather than taking it over, unlike the OpaqueFd path in
859/// `import_dma_fd`. `fence_handle` is a duplicate this backing takes over and
860/// closes on drop, for the same reason -- nothing else holds it.
861///
862/// Returns `None` on any failure (no CUDA device for the adapter, as on WARP;
863/// a DXGI format with no channel description; a driver error), releasing
864/// everything it had already created. The tensor is then simply CUDA-less.
865#[cfg(all(target_os = "windows", feature = "static"))]
866pub(crate) fn import_d3d11_texture(
867    handle: RawHandle,
868    layout: crate::d3d11_layout::D3d11ImageLayout,
869    adapter: *mut c_void,
870    fence_handle: Option<OwnedHandle>,
871    last_write: Arc<std::sync::atomic::AtomicU64>,
872) -> Option<CudaHandle> {
873    let t = table()?;
874    // Every texture tensor is created on the one process D3D11 device, so
875    // every import resolves the same ordinal and selects the same CUDA
876    // device; caching it keeps `cudaD3D11GetDevice` off the allocation path.
877    // The ordinal itself is cached, not just the verdict, because every map
878    // and every release sets it again on its own thread.
879    static CUDA_DEVICE_SELECTED: OnceLock<Option<c_int>> = OnceLock::new();
880    let device = (*CUDA_DEVICE_SELECTED.get_or_init(|| {
881        let mut dev = -1;
882        // SAFETY: `adapter` is the process device's live `IDXGIAdapter*` and
883        // `dev` is a valid out-parameter.
884        let rc = unsafe { (t.d3d11_get_device)(&mut dev, adapter) };
885        if rc != 0 {
886            log::debug!(
887                "cudaD3D11GetDevice for the process adapter: {}; D3D11 texture tensors \
888                 will have no CUDA handle",
889                err_str(t, rc)
890            );
891            return None;
892        }
893        // SAFETY: `dev` is the ordinal the call above filled in.
894        let rc = unsafe { (t.set_device)(dev) };
895        if rc != 0 {
896            log::debug!("cudaSetDevice({dev}): {}", err_str(t, rc));
897            return None;
898        }
899        Some(dev)
900    }))?;
901    // The channel description of the texture's DXGI format: one component's
902    // width in bits and how many components a texel carries. Only the formats
903    // `crate::d3d11_layout` allocates are listed; anything else declines the
904    // import rather than guessing a description the driver would reject.
905    let (bits, kind, channels) = match layout.dxgi_format {
906        crate::d3d11_layout::DXGI_FORMAT_R8_UNORM => (8, CUDA_CHANNEL_FORMAT_KIND_UNSIGNED, 1),
907        crate::d3d11_layout::DXGI_FORMAT_R8G8_UNORM => (8, CUDA_CHANNEL_FORMAT_KIND_UNSIGNED, 2),
908        crate::d3d11_layout::DXGI_FORMAT_R8G8B8A8_UNORM
909        | crate::d3d11_layout::DXGI_FORMAT_B8G8R8A8_UNORM => {
910            (8, CUDA_CHANNEL_FORMAT_KIND_UNSIGNED, 4)
911        }
912        crate::d3d11_layout::DXGI_FORMAT_R16G16B16A16_FLOAT => {
913            (16, CUDA_CHANNEL_FORMAT_KIND_FLOAT, 4)
914        }
915        crate::d3d11_layout::DXGI_FORMAT_R32G32B32A32_FLOAT => {
916            (32, CUDA_CHANNEL_FORMAT_KIND_FLOAT, 4)
917        }
918        other => {
919            log::debug!("no CUDA channel format for DXGI format {other}");
920            return None;
921        }
922    };
923    let ch = |i: c_int| if i < channels { bits } else { 0 };
924    // SAFETY: every field is an integer, a nested POD struct or an array of
925    // those, so all-zero is a valid bit pattern; the named fields below are
926    // then set. Zeroing is what makes the 12.x/13.x `reserved` tail
927    // well-defined when an 11.8 runtime is the one loaded.
928    let mut md: CudaExternalMemoryMipmappedArrayDesc = unsafe { std::mem::zeroed() };
929    md.format_desc = CudaChannelFormatDesc {
930        x: ch(0),
931        y: ch(1),
932        z: ch(2),
933        w: ch(3),
934        f: kind,
935    };
936    md.extent = CudaExtent {
937        width: layout.texture_width,
938        height: layout.texture_height,
939        depth: 0,
940    };
941    md.num_levels = 1;
942    let (ext, mm) = import_and_map_array(t, handle, &md, layout)?;
943    let mut level0: CudaArray = std::ptr::null_mut();
944    // SAFETY: `mm` is the array just mapped, which has the one level `md`
945    // asked for, and `level0` is a valid out-parameter.
946    let rc = unsafe { (t.get_mipmapped_array_level)(&mut level0, mm, 0) };
947    let stream = if rc == 0 && !level0.is_null() {
948        stream_create()
949    } else {
950        None
951    };
952    let Some(stream) = stream else {
953        if rc != 0 {
954            log::debug!("cudaGetMipmappedArrayLevel(0): {}", err_str(t, rc));
955        } else if level0.is_null() {
956            log::debug!("cudaGetMipmappedArrayLevel(0) reported success but left a null array");
957        } else {
958            log::debug!("cudaStreamCreate for an imported D3D11 texture failed");
959        }
960        // SAFETY: both objects were created above and nothing else holds them;
961        // the array is released before the memory it was mapped from.
962        unsafe {
963            (t.free_mipmapped_array)(mm);
964            (t.destroy_external_memory)(ext);
965        }
966        return None;
967    };
968    let semaphore = fence_handle.and_then(|fh| shared_semaphore(t, fh));
969    Some(CudaHandle {
970        size: layout.tight_bytes(),
971        kind: CudaBacking::D3d11(D3d11External {
972            ext_mem: ext,
973            mipmapped: mm,
974            level0,
975            linear: std::sync::Mutex::new(None),
976            bytes: layout.tight_bytes(),
977            row_bytes: layout.tight_row_bytes(),
978            rows: layout.texture_height,
979            stream,
980            device,
981            semaphore,
982            last_write,
983        }),
984    })
985}
986
987/// The process fence imported as a CUDA external semaphore, with the
988/// duplicate of its NT handle CUDA needs kept open beside it.
989#[cfg(all(target_os = "windows", feature = "static"))]
990struct SharedSemaphore {
991    sem: ExternalSemaphore,
992    /// CUDA duplicates a Win32 handle it imports rather than taking it over,
993    /// but the source handle must stay valid for the import; held for the
994    /// process lifetime because the semaphore is.
995    _handle: OwnedHandle,
996}
997
998// SAFETY: a CUDA external-semaphore handle is an opaque runtime object bound
999// to a device, not to a thread, and every use here passes it to a runtime call
1000// that is itself thread-safe. `OwnedHandle` is `Send + Sync`. The struct is
1001// written once inside a `OnceLock` initialiser and only read afterwards.
1002#[cfg(all(target_os = "windows", feature = "static"))]
1003unsafe impl Send for SharedSemaphore {}
1004#[cfg(all(target_os = "windows", feature = "static"))]
1005unsafe impl Sync for SharedSemaphore {}
1006
1007/// Imports the process D3D11 fence as an external semaphore, once.
1008///
1009/// Every texture tensor in this copy of the crate records its completions on
1010/// the one process fence, so importing per tensor would duplicate the same NT
1011/// handle and import the same object again for every allocation -- a 30-frame
1012/// pool pays 30 of each for no benefit. The semaphore and its handle live for
1013/// the process, like the device and the fence they name, so neither is
1014/// destroyed on a handle's drop.
1015///
1016/// The CUDA *stream* is deliberately not hoisted with it: it stays per handle
1017/// so a map never waits behind another consumer's unrelated work.
1018///
1019/// `fence_handle` is consumed by the first caller and dropped by the rest.
1020#[cfg(all(target_os = "windows", feature = "static"))]
1021fn shared_semaphore(t: &CudaTable, fence_handle: OwnedHandle) -> Option<&'static SharedSemaphore> {
1022    static IMPORTED: OnceLock<Option<SharedSemaphore>> = OnceLock::new();
1023    IMPORTED
1024        .get_or_init(move || {
1025            // SAFETY: same all-zero reasoning as the descriptors above.
1026            let mut sd: CudaExternalSemaphoreHandleDesc = unsafe { std::mem::zeroed() };
1027            sd.type_ = CUDA_EXTERNAL_SEMAPHORE_HANDLE_TYPE_D3D11_FENCE;
1028            sd.win32_handle = fence_handle.as_raw_handle();
1029            let mut sem: ExternalSemaphore = std::ptr::null_mut();
1030            // SAFETY: `sd` names the live shared handle `fence_handle` owns,
1031            // which outlives the call, and `sem` is a valid out-parameter.
1032            let rc = unsafe {
1033                (t.import_external_semaphore)(&mut sem, &sd as *const _ as *const c_void)
1034            };
1035            if rc != 0 {
1036                // The duplicate closes with `fence_handle` here. Tensors that
1037                // carry a recorded write then decline their CUDA map rather
1038                // than copy unordered; see `D3d11External::semaphore`.
1039                log::debug!(
1040                    "cudaImportExternalSemaphore(D3D11Fence): {}",
1041                    err_str(t, rc)
1042                );
1043                return None;
1044            }
1045            Some(SharedSemaphore {
1046                sem,
1047                _handle: fence_handle,
1048            })
1049        })
1050        .as_ref()
1051}
1052
1053/// Whether a copy issued now would be ordered behind the producer's recorded
1054/// D3D11 write: either nothing is recorded, or a wait for it was queued on the
1055/// same stream.
1056///
1057/// A recorded write with no wait behind it is the one case that must not
1058/// proceed. The copy would run against a texture the producer's GPU work is
1059/// still writing -- timing-dependent, silent, and invisible below `warn`. It is
1060/// the hazard `validate_descriptor` refuses a descriptor for, so a map declines
1061/// here for the same reason: the documented fallback is the host map, which
1062/// goes through the D3D11 immediate context and *is* ordered.
1063#[cfg(all(target_os = "windows", feature = "static"))]
1064fn d3d11_copy_is_ordered(recorded: u64, wait_queued: bool) -> bool {
1065    recorded == 0 || wait_queued
1066}
1067
1068/// Set once a D3D11 allocation has proved that this adapter's driver pads its
1069/// textures past their padding-free size, so later imports skip the tight rung
1070/// of [`import_and_map_array`]'s ladder instead of paying a doomed import each
1071/// time.
1072#[cfg(all(target_os = "windows", feature = "static"))]
1073static CUDA_IMPORT_SIZE_PADDED: std::sync::atomic::AtomicBool =
1074    std::sync::atomic::AtomicBool::new(false);
1075
1076/// Imports the texture as external memory and maps `md` out of it, returning
1077/// the pair on success.
1078///
1079/// The declared size is the one thing here that cannot be read off the
1080/// texture: D3D11 has no allocation-size query, and the driver pads a
1081/// texture's rows and its row count for tiling, so a texture's real
1082/// allocation is larger than its padding-free bytes at most geometries. This
1083/// box's RTX 3070 maps a 256x128 RGBA8 texture from the tight 131072 bytes but
1084/// rejects a 1920x1080 one (`cudaErrorInvalidValue` from
1085/// `cudaExternalMemoryGetMappedMipmappedArray`, not from the import, which
1086/// accepts any non-zero size) until the declaration covers the padding.
1087///
1088/// So the declaration climbs a short ladder -- the exact bytes, then 2x, then
1089/// 4x -- and stops at the first rung the driver maps. Over-declaring is what
1090/// the probe's S5.4 measured as harmless (a 2x declaration of a 256x128
1091/// texture mapped and copied the same 0-mismatch bytes as the tight one), and
1092/// it is safe by construction: with `cudaExternalMemoryDedicated` the mapping
1093/// covers the whole resource, no address is derived from this number, and the
1094/// array's own extent bounds every copy.
1095///
1096/// The first rung is floored at one 4 KiB page: no GPU allocation is smaller
1097/// than a page, so a 1x1 RGBA texture's four bytes are provably too few to
1098/// describe one, and 4x of too few is still too few. Measured on this box --
1099/// 1x1 and 4x4 RGBA and 7x3 R8 map at the floor or its doublings and at
1100/// nothing the unfloored ladder reaches; every image-sized texture is far
1101/// above the floor and unaffected by it.
1102///
1103/// Whether the tight rung works is a property of the adapter, not of the
1104/// texture, so the first allocation that has to climb records it in
1105/// [`CUDA_IMPORT_SIZE_PADDED`] and later ones start one rung up.
1106#[cfg(all(target_os = "windows", feature = "static"))]
1107fn import_and_map_array(
1108    t: &CudaTable,
1109    handle: RawHandle,
1110    md: &CudaExternalMemoryMipmappedArrayDesc,
1111    layout: crate::d3d11_layout::D3d11ImageLayout,
1112) -> Option<(ExternalMemory, MipmappedArray)> {
1113    /// The ladder's floor, one page.
1114    const PAGE: usize = 4096;
1115    let tight = layout.tight_bytes();
1116    let start = usize::from(CUDA_IMPORT_SIZE_PADDED.load(std::sync::atomic::Ordering::Relaxed));
1117    for multiple in [1usize, 2, 4].into_iter().skip(start) {
1118        let size = tight.max(PAGE).saturating_mul(multiple);
1119        // SAFETY: every field is an integer or a raw pointer, so all-zero is a
1120        // valid bit pattern; the named fields below are then set.
1121        let mut hd: CudaExternalMemoryHandleDescWin32 = unsafe { std::mem::zeroed() };
1122        hd.type_ = CUDA_EXTERNAL_MEMORY_HANDLE_TYPE_D3D11_RESOURCE;
1123        hd.win32_handle = handle;
1124        hd.size = size as u64;
1125        hd.flags = CUDA_EXTERNAL_MEMORY_DEDICATED;
1126        let mut ext: ExternalMemory = std::ptr::null_mut();
1127        // SAFETY: `hd` is a fully initialised descriptor naming a live shared
1128        // NT handle of a D3D11 texture, and `ext` is a valid out-parameter.
1129        let rc = unsafe { (t.import_external_memory)(&mut ext, &hd as *const _ as *const c_void) };
1130        if rc != 0 {
1131            log::debug!(
1132                "cudaImportExternalMemory(D3D11Resource, size {size}): {}",
1133                err_str(t, rc)
1134            );
1135            continue;
1136        }
1137        let mut mm: MipmappedArray = std::ptr::null_mut();
1138        // SAFETY: `ext` is the live import, `md` a fully initialised
1139        // descriptor of the texture's own geometry, and `mm` a valid
1140        // out-parameter.
1141        let rc = unsafe {
1142            (t.external_memory_get_mapped_mipmapped_array)(
1143                &mut mm,
1144                ext,
1145                md as *const _ as *const c_void,
1146            )
1147        };
1148        if rc == 0 {
1149            // Logged once, by the allocation that discovers the padding.
1150            if multiple > 1
1151                && !CUDA_IMPORT_SIZE_PADDED.swap(true, std::sync::atomic::Ordering::Relaxed)
1152            {
1153                log::debug!(
1154                    "this adapter pads D3D11 allocations: a {tight}-byte texture mapped \
1155                     only when declared at {multiple}x ({size} bytes), so later imports \
1156                     start there"
1157                );
1158            }
1159            return Some((ext, mm));
1160        }
1161        log::debug!(
1162            "cudaExternalMemoryGetMappedMipmappedArray over {size} declared bytes: {}",
1163            err_str(t, rc)
1164        );
1165        // SAFETY: `ext` is the import made just above and nothing else holds it.
1166        unsafe { (t.destroy_external_memory)(ext) };
1167    }
1168    None
1169}
1170
1171/// Routes `cudaGraphicsMapResources`/Unmap/Unregister through the GL worker
1172/// thread (the GL context must be current there). Implemented by the image crate.
1173pub trait CudaGlOps: Send + Sync {
1174    fn map(&self, resource: GraphicsResource) -> Option<(*mut c_void, usize)>;
1175    fn unmap(&self, resource: GraphicsResource);
1176    fn unregister(&self, resource: GraphicsResource);
1177}
1178
1179enum CudaBacking {
1180    #[allow(dead_code)] // consumed by C3/C4
1181    GlBuffer {
1182        resource: GraphicsResource,
1183        ops: Arc<dyn CudaGlOps>,
1184    },
1185    #[allow(dead_code)] // consumed by C3/C4
1186    ExternalMem {
1187        ext_mem: ExternalMemory,
1188        dptr: *mut c_void,
1189    },
1190    #[cfg(all(target_os = "windows", feature = "static"))]
1191    D3d11(D3d11External),
1192}
1193
1194// SAFETY: CUDA handles/ptrs are process-global; GlBuffer routes to the GL
1195// worker; ExternalMem ptr is valid via the per-device primary context.
1196unsafe impl Send for CudaBacking {}
1197unsafe impl Sync for CudaBacking {}
1198
1199/// CUDA registration for a GPU-backed tensor. Held as `Option` on the tensor.
1200pub struct CudaHandle {
1201    kind: CudaBacking,
1202    size: usize,
1203}
1204
1205impl std::fmt::Debug for CudaHandle {
1206    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1207        let kind = match &self.kind {
1208            CudaBacking::GlBuffer { .. } => "GlBuffer",
1209            CudaBacking::ExternalMem { .. } => "ExternalMem",
1210            #[cfg(all(target_os = "windows", feature = "static"))]
1211            CudaBacking::D3d11(_) => "D3d11",
1212        };
1213        f.debug_struct("CudaHandle")
1214            .field("kind", &kind)
1215            .field("size", &self.size)
1216            .finish()
1217    }
1218}
1219
1220impl CudaHandle {
1221    /// Construct a GL-buffer-backed CUDA handle. `resource` is the
1222    /// `cudaGraphicsResource_t` returned by [`gl_register_buffer`]; `ops`
1223    /// routes map/unmap/unregister back to the GL-context thread.
1224    pub fn new_gl(resource: GraphicsResource, size: usize, ops: Arc<dyn CudaGlOps>) -> Self {
1225        Self {
1226            kind: CudaBacking::GlBuffer { resource, ops },
1227            size,
1228        }
1229    }
1230
1231    #[allow(dead_code)] // consumed by C3/C4
1232    pub(crate) fn new_external(ext_mem: ExternalMemory, dptr: *mut c_void, size: usize) -> Self {
1233        Self {
1234            kind: CudaBacking::ExternalMem { ext_mem, dptr },
1235            size,
1236        }
1237    }
1238
1239    /// Map to a device pointer. `GlBuffer` routes to the GL worker;
1240    /// `ExternalMem` is persistent (no per-call map/unmap); `D3d11` copies the
1241    /// texture's array into its linear buffer, after waiting for the newest
1242    /// fence value recorded on the tensor.
1243    ///
1244    /// The `D3d11` mapping is the texture's *tight* rows: `len()` is
1245    /// `width * bytes_per_texel * height` and row `n` starts at
1246    /// `n * width * bytes_per_texel`, whatever the tensor's
1247    /// [`row_stride`](crate::Tensor::row_stride) reports -- that is the D3D11
1248    /// staging pitch a CPU map sees, a different number on a padded backing.
1249    /// A consumer that strides the device pointer by `row_stride()` walks off
1250    /// the end of the mapping.
1251    ///
1252    /// Semi-planar textures (NV12, NV16, NV24) are the exception: the texture
1253    /// is as wide as its pitch by construction, so the mapping equals
1254    /// `row_stride() x combined_height` bytes and striding by `row_stride()`
1255    /// is exactly right.
1256    ///
1257    /// One `D3d11` handle has one linear buffer, so two live maps of the same
1258    /// tensor hand out the same device pointer and the second map's copy
1259    /// refreshes what the first is reading. That is the same aliasing the GL
1260    /// and external-memory backings have always had -- one buffer per handle,
1261    /// not one per map. A `map()` taken while a [`map_mut`](Self::map_mut)
1262    /// guard is live is the case that also loses data: it refreshes the buffer
1263    /// under the writer, and the writer's release then publishes the array's
1264    /// pre-write contents back into the texture. Callers serialise the two.
1265    pub fn map(&self) -> Option<CudaMap<'_>> {
1266        match &self.kind {
1267            CudaBacking::GlBuffer { resource, ops } => {
1268                let (ptr, len) = ops.map(*resource)?;
1269                Some(CudaMap {
1270                    ptr,
1271                    len,
1272                    release: Release::GlUnmap(ops.clone(), *resource),
1273                    _marker: std::marker::PhantomData,
1274                })
1275            }
1276            CudaBacking::ExternalMem { dptr, .. } => Some(CudaMap {
1277                ptr: *dptr,
1278                len: self.size,
1279                release: Release::None,
1280                _marker: std::marker::PhantomData,
1281            }),
1282            #[cfg(all(target_os = "windows", feature = "static"))]
1283            CudaBacking::D3d11(x) => Some(CudaMap {
1284                ptr: self.d3d11_copy_in(x)?,
1285                len: x.bytes,
1286                release: Release::None,
1287                _marker: std::marker::PhantomData,
1288            }),
1289        }
1290    }
1291
1292    /// Map to a device pointer for writing.
1293    ///
1294    /// The GL and external-memory backings alias the same device memory for
1295    /// reads and writes -- a GL buffer's CUDA mapping and an imported
1296    /// external-memory allocation are each one address the caller may write
1297    /// through -- so for them this is [`map`](Self::map). A `D3d11` texture is
1298    /// the backing that does distinguish the two: its linear buffer is a copy
1299    /// of an opaque array, so the map's release copies it back into the
1300    /// texture and synchronises, and later D3D11 and GL work on the immediate
1301    /// context is ordered after that write.
1302    ///
1303    /// The `D3d11` mapping is the texture's *tight* rows: `len()` is
1304    /// `width * bytes_per_texel * height` and row `n` starts at
1305    /// `n * width * bytes_per_texel`, whatever the tensor's
1306    /// [`row_stride`](crate::Tensor::row_stride) reports -- that is the D3D11
1307    /// staging pitch a CPU map sees, a different number on a padded backing.
1308    /// A consumer that strides the device pointer by `row_stride()` walks off
1309    /// the end of the mapping.
1310    ///
1311    /// Semi-planar textures (NV12, NV16, NV24) are the exception: the texture
1312    /// is as wide as its pitch by construction, so the mapping equals
1313    /// `row_stride() x combined_height` bytes and striding by `row_stride()`
1314    /// is exactly right.
1315    pub fn map_mut(&self) -> Option<CudaMap<'_>> {
1316        #[cfg(all(target_os = "windows", feature = "static"))]
1317        if let CudaBacking::D3d11(x) = &self.kind {
1318            return Some(CudaMap {
1319                ptr: self.d3d11_copy_in(x)?,
1320                len: x.bytes,
1321                release: Release::D3d11Writeback {
1322                    level0: x.level0,
1323                    row_bytes: x.row_bytes,
1324                    rows: x.rows,
1325                    stream: x.stream,
1326                    device: x.device,
1327                },
1328                _marker: std::marker::PhantomData,
1329            });
1330        }
1331        self.map()
1332    }
1333
1334    /// Waits for the tensor's newest recorded D3D11 write, then refreshes the
1335    /// linear buffer from the texture's array and returns its device pointer.
1336    ///
1337    /// The wait, the copy and the synchronisation all run on this backing's
1338    /// own stream, so the copy is ordered behind the D3D11 signal (the
1339    /// sequence gpu-probe S5.6 measured as the one that never reads a stale
1340    /// frame) without a device-wide barrier: a legacy-stream synchronisation
1341    /// would also wait behind streams this crate hands to other consumers.
1342    ///
1343    /// A recorded fence value the producer never signals blocks the
1344    /// synchronisation below until it is. That is inherent to waiting on a
1345    /// fence: a value is a promise of work, and `set_gpu_write` is the
1346    /// producer's statement that it queued it.
1347    ///
1348    /// Answers `None` when the wait cannot be issued at all
1349    /// ([`d3d11_copy_is_ordered`]), like every other failure here.
1350    #[cfg(all(target_os = "windows", feature = "static"))]
1351    fn d3d11_copy_in(&self, x: &D3d11External) -> Option<*mut c_void> {
1352        let t = table()?;
1353        // Per-thread state: a consumer thread that last used another GPU would
1354        // otherwise allocate on it and be unable to use this handle's stream.
1355        // SAFETY: `x.device` is the ordinal `import_d3d11_texture` cached for
1356        // the process D3D11 adapter.
1357        let rc = unsafe { (t.set_device)(x.device) };
1358        if rc != 0 {
1359            log::debug!("cudaSetDevice({}): {}", x.device, err_str(t, rc));
1360            return None;
1361        }
1362        let mut guard = x.linear.lock().unwrap_or_else(|e| e.into_inner());
1363        if guard.is_none() {
1364            let mut p: *mut c_void = std::ptr::null_mut();
1365            // SAFETY: `p` is a valid out-parameter and `x.bytes` the texture's
1366            // padding-free byte count.
1367            let rc = unsafe { (t.malloc)(&mut p, x.bytes) };
1368            if rc != 0 {
1369                log::debug!("cudaMalloc({}): {}", x.bytes, err_str(t, rc));
1370                return None;
1371            }
1372            *guard = Some(p);
1373        }
1374        let dptr = guard.unwrap();
1375        let value = x.last_write.load(std::sync::atomic::Ordering::Acquire);
1376        let mut queued = false;
1377        if let (Some(shared), true) = (x.semaphore, value != 0) {
1378            // SAFETY: every field is an integer or an array of integers, so
1379            // all-zero is valid; only `fence_value` is then set.
1380            let mut wp: CudaExternalSemaphoreWaitParams = unsafe { std::mem::zeroed() };
1381            wp.fence_value = value;
1382            // SAFETY: `shared.sem` is the process semaphore, live for the
1383            // process, `wp` a fully initialised wait parameter for the one
1384            // semaphore passed, and `x.stream` this backing's live stream.
1385            let rc = unsafe {
1386                (t.wait_external_semaphores_async)(
1387                    &shared.sem,
1388                    &wp as *const _ as *const c_void,
1389                    1,
1390                    x.stream,
1391                )
1392            };
1393            queued = rc == 0;
1394            if rc != 0 {
1395                log::warn!(
1396                    "cudaWaitExternalSemaphoresAsync({value}): {}",
1397                    err_str(t, rc)
1398                );
1399            }
1400        }
1401        if !d3d11_copy_is_ordered(value, queued) {
1402            log::warn!(
1403                "CUDA map of a D3D11 texture with a recorded write ({value}) this handle cannot \
1404                 wait for ({}); declining the device mapping so the caller falls back to the \
1405                 host map, which the immediate context orders",
1406                if x.semaphore.is_some() {
1407                    "the wait could not be queued"
1408                } else {
1409                    "no external semaphore was imported"
1410                }
1411            );
1412            return None;
1413        }
1414        // One device copy turns the tiled array into the tensor's tight rows,
1415        // on the same stream as the wait above so it runs behind it.
1416        // SAFETY: `dptr` holds `x.rows * x.row_bytes == x.bytes` bytes,
1417        // `x.level0` is the live level-0 array of the imported texture, which
1418        // is `x.row_bytes` wide and `x.rows` tall, and `x.stream` is this
1419        // backing's live stream.
1420        let rc = unsafe {
1421            (t.memcpy_2d_from_array_async)(
1422                dptr,
1423                x.row_bytes,
1424                x.level0,
1425                0,
1426                0,
1427                x.row_bytes,
1428                x.rows,
1429                CUDA_MEMCPY_DEVICE_TO_DEVICE,
1430                x.stream,
1431            )
1432        };
1433        if rc != 0 {
1434            log::warn!("cudaMemcpy2DFromArrayAsync: {}", err_str(t, rc));
1435            return None;
1436        }
1437        // SAFETY: `x.stream` is this backing's live stream.
1438        let rc = unsafe { (t.stream_synchronize)(x.stream) };
1439        if rc != 0 {
1440            log::warn!(
1441                "cudaStreamSynchronize after the array copy: {}",
1442                err_str(t, rc)
1443            );
1444            return None;
1445        }
1446        Some(dptr)
1447    }
1448}
1449
1450impl Drop for CudaHandle {
1451    fn drop(&mut self) {
1452        match &self.kind {
1453            CudaBacking::GlBuffer { resource, ops } => ops.unregister(*resource),
1454            CudaBacking::ExternalMem { ext_mem, dptr: _ } => {
1455                // The device pointer comes from `cudaExternalMemoryGetMappedBuffer`,
1456                // which CUDA frees together with the external-memory object. Calling
1457                // `cudaFree` on such a pointer is explicitly disallowed and corrupts
1458                // the driver's bookkeeping (risking a double-free when the handle is
1459                // destroyed), so only destroy the external-memory object here.
1460                if let Some(t) = table() {
1461                    unsafe {
1462                        (t.destroy_external_memory)(*ext_mem);
1463                    }
1464                }
1465            }
1466            // Everything `import_d3d11_texture` created for *this* handle,
1467            // released in reverse: the linear buffer (this one IS a
1468            // `cudaMalloc` pointer, unlike `ExternalMem`'s), then the stream,
1469            // then the array before the external memory it was mapped from.
1470            // The external semaphore is not here: it is the process import
1471            // (`shared_semaphore`) and outlives every handle.
1472            #[cfg(all(target_os = "windows", feature = "static"))]
1473            CudaBacking::D3d11(x) => {
1474                let Some(t) = table() else { return };
1475                // The handle can be dropped on any thread; the objects below
1476                // all belong to the D3D11 adapter's device.
1477                // SAFETY: `x.device` is the ordinal `import_d3d11_texture`
1478                // cached for that adapter.
1479                let rc = unsafe { (t.set_device)(x.device) };
1480                if rc != 0 {
1481                    log::debug!(
1482                        "cudaSetDevice({}) before releasing a D3D11 import: {}",
1483                        x.device,
1484                        err_str(t, rc)
1485                    );
1486                }
1487                let linear = x.linear.lock().unwrap_or_else(|e| e.into_inner()).take();
1488                // SAFETY: every object below was created by
1489                // `import_d3d11_texture` for this handle, which is being
1490                // dropped, so nothing else can hold or use them.
1491                unsafe {
1492                    if let Some(p) = linear {
1493                        (t.free)(p);
1494                    }
1495                    (t.stream_destroy)(x.stream);
1496                    (t.free_mipmapped_array)(x.mipmapped);
1497                    (t.destroy_external_memory)(x.ext_mem);
1498                }
1499            }
1500        }
1501    }
1502}
1503
1504/// What a [`CudaMap`] does when it drops: nothing for a persistent mapping,
1505/// a GL unmap, a write-back into the D3D11 texture's array, or -- on the
1506/// `dynamic` backend -- the C library's own unmap of a mapping it handed out.
1507enum Release {
1508    None,
1509    GlUnmap(Arc<dyn CudaGlOps>, GraphicsResource),
1510    #[cfg(feature = "dynamic")]
1511    FfiUnmap(*mut c_void),
1512    #[cfg(all(target_os = "windows", feature = "static"))]
1513    D3d11Writeback {
1514        level0: CudaArray,
1515        row_bytes: usize,
1516        rows: usize,
1517        stream: CudaStream,
1518        device: c_int,
1519    },
1520}
1521
1522/// Scoped CUDA device-pointer mapping. `Drop` unmaps a `GlBuffer` (so GL may
1523/// reuse the PBO for the next `convert()` call) and copies a writable `D3d11`
1524/// mapping back into the texture. `ExternalMem` mappings are persistent —
1525/// `Drop` is a no-op.
1526///
1527/// A `D3d11` mapping is the texture's tight rows: [`len`](Self::len) is
1528/// `width * bytes_per_texel * height` and rows are `width * bytes_per_texel`
1529/// apart, which is *not* the tensor's `row_stride()` (the D3D11 staging pitch
1530/// a CPU map sees) on a padded backing. A semi-planar texture is as wide as
1531/// its own pitch, so there the two numbers agree and the mapping is
1532/// `row_stride() x combined_height` bytes.
1533pub struct CudaMap<'a> {
1534    ptr: *mut c_void,
1535    len: usize,
1536    release: Release,
1537    _marker: std::marker::PhantomData<&'a ()>,
1538}
1539
1540// SAFETY: the mapped device pointer is process-global and valid cross-thread
1541// via the per-device CUDA primary context; the routed CudaGlOps is Send+Sync.
1542// A `Release::FfiUnmap` handle is likewise cross-thread: it owns a retain on a
1543// refcounted `ef_tensor`, whose retain and free are atomic and whose accessors
1544// are documented safe to call from any thread (`crates/tensor-capi/src/
1545// handle.rs`, `ef_tensor_retain` and `ef_tensor_set_colorimetry`'s
1546// Concurrency note). Required so callers can hold the guard on a separate
1547// inference thread.
1548unsafe impl Send for CudaMap<'_> {}
1549unsafe impl Sync for CudaMap<'_> {}
1550
1551impl CudaMap<'_> {
1552    /// Raw device pointer to the mapped buffer.
1553    pub fn device_ptr(&self) -> *mut c_void {
1554        self.ptr
1555    }
1556
1557    /// Length of the mapping in bytes.
1558    pub fn len(&self) -> usize {
1559        self.len
1560    }
1561
1562    /// Returns `true` if the mapping covers zero bytes.
1563    pub fn is_empty(&self) -> bool {
1564        self.len == 0
1565    }
1566
1567    /// Adopt the opaque map `ef_tensor_cuda_map`/`_map_mut` returned.
1568    ///
1569    /// The `dynamic` backend's tensors are allocated inside
1570    /// `libedgefirst_tensor`, which attaches their [`CudaHandle`] to its own
1571    /// `Tensor` -- an object the Rust-side wrapper does not hold -- so the
1572    /// mapping is taken and released across the ABI instead. The C map holds
1573    /// its own retain on the tensor handle, so this guard stays valid for as
1574    /// long as it lives, and dropping it releases both.
1575    ///
1576    /// Returns `None` (having released `raw`) when the map carries no device
1577    /// pointer, so a caller never sees a mapping it cannot read.
1578    ///
1579    /// # Safety
1580    /// `raw` must be a live, non-NULL map from `ef_tensor_cuda_map` or
1581    /// `ef_tensor_cuda_map_mut` that no one else will release: ownership
1582    /// passes to the guard returned here, which calls `ef_tensor_cuda_unmap`
1583    /// on it exactly once -- immediately when there is no device pointer,
1584    /// otherwise on drop. A second release is a double free of the map and of
1585    /// the tensor retain it holds.
1586    #[cfg(feature = "dynamic")]
1587    pub(crate) unsafe fn from_ffi(raw: *mut c_void) -> Option<Self> {
1588        let mut len = 0usize;
1589        // SAFETY: `raw` is a live map by this function's contract, and `len`
1590        // is a live local; the accessor takes either kind of map.
1591        let ptr = unsafe { edgefirst_tensor_ffi::ef_tensor_cuda_device_ptr(raw, &mut len) };
1592        if ptr.is_null() {
1593            // SAFETY: as above -- releasing, exactly once, a map whose pointer
1594            // is unusable, before ownership reaches any caller.
1595            unsafe { edgefirst_tensor_ffi::ef_tensor_cuda_unmap(raw) };
1596            return None;
1597        }
1598        Some(CudaMap {
1599            ptr,
1600            len,
1601            release: Release::FfiUnmap(raw),
1602            _marker: std::marker::PhantomData,
1603        })
1604    }
1605}
1606
1607impl Drop for CudaMap<'_> {
1608    fn drop(&mut self) {
1609        // Taken, not borrowed, so the release happens exactly once even if a
1610        // future `CudaMap` gains a way to be released early.
1611        match std::mem::replace(&mut self.release, Release::None) {
1612            Release::None => {}
1613            Release::GlUnmap(ops, r) => ops.unmap(r),
1614            // The write-back a writable D3D11 map owes happens inside the C
1615            // library, on the `CudaMap` this handle wraps there.
1616            #[cfg(feature = "dynamic")]
1617            Release::FfiUnmap(h) => {
1618                // SAFETY: the handle came from `ef_tensor_cuda_map`/`_map_mut`
1619                // and is released exactly once -- `replace` above took it.
1620                unsafe { edgefirst_tensor_ffi::ef_tensor_cuda_unmap(h) }
1621            }
1622            #[cfg(all(target_os = "windows", feature = "static"))]
1623            Release::D3d11Writeback {
1624                level0,
1625                row_bytes,
1626                rows,
1627                stream,
1628                device,
1629            } => {
1630                let Some(t) = table() else { return };
1631                // The guard can be dropped on any thread; the stream and the
1632                // array belong to the D3D11 adapter's device.
1633                // SAFETY: `device` is the ordinal `import_d3d11_texture`
1634                // cached for that adapter.
1635                let rc = unsafe { (t.set_device)(device) };
1636                if rc != 0 {
1637                    // The write-back is what publishes `cuda_map_mut`'s
1638                    // buffer; `Drop` cannot propagate, so this line is the
1639                    // whole report that the caller's writes were lost.
1640                    log::warn!(
1641                        "cudaSetDevice({device}) before the D3D11 write-back: {}",
1642                        err_str(t, rc)
1643                    );
1644                    return;
1645                }
1646                // SAFETY: `self.ptr` is the linear buffer this map was built
1647                // over, holding `rows * row_bytes` bytes, `level0` the live
1648                // level-0 array of the same geometry, and `stream` the
1649                // backing's live stream, which outlives this map.
1650                let rc = unsafe {
1651                    (t.memcpy_2d_to_array_async)(
1652                        level0,
1653                        0,
1654                        0,
1655                        self.ptr,
1656                        row_bytes,
1657                        row_bytes,
1658                        rows,
1659                        CUDA_MEMCPY_DEVICE_TO_DEVICE,
1660                        stream,
1661                    )
1662                };
1663                if rc != 0 {
1664                    log::warn!("cudaMemcpy2DToArrayAsync: {}", err_str(t, rc));
1665                }
1666                // Synchronised here, not left in flight: the copy above is
1667                // asynchronous, and later D3D11 and GL work on the immediate
1668                // context has no other ordering against this write.
1669                // SAFETY: `stream` is the backing's live stream.
1670                let rc = unsafe { (t.stream_synchronize)(stream) };
1671                if rc != 0 {
1672                    log::warn!(
1673                        "cudaStreamSynchronize after the write-back: {}",
1674                        err_str(t, rc)
1675                    );
1676                }
1677            }
1678        }
1679    }
1680}
1681
1682#[cfg(test)]
1683mod ext_mem_layout {
1684    use super::*;
1685    #[test]
1686    fn external_memory_desc_abi() {
1687        assert_eq!(std::mem::size_of::<CudaExternalMemoryHandleDesc>(), 40);
1688        assert_eq!(std::mem::align_of::<CudaExternalMemoryHandleDesc>(), 8);
1689        // size field at offset 24, flags at 32 (verified vs driver_types.h)
1690        let d: CudaExternalMemoryHandleDesc = unsafe { std::mem::zeroed() };
1691        let base = &d as *const _ as usize;
1692        assert_eq!((&d.size as *const _ as usize) - base, 24);
1693        assert_eq!((&d.flags as *const _ as usize) - base, 32);
1694        assert_eq!(std::mem::size_of::<CudaExternalMemoryBufferDesc>(), 24);
1695        let b: CudaExternalMemoryBufferDesc = unsafe { std::mem::zeroed() };
1696        let bb = &b as *const _ as usize;
1697        assert_eq!((&b.size as *const _ as usize) - bb, 8);
1698    }
1699}
1700
1701#[cfg(all(test, target_os = "windows", feature = "static"))]
1702mod d3d11_ordering {
1703    use super::d3d11_copy_is_ordered;
1704
1705    /// The rule the D2D copy is gated on. A recorded write with no wait behind
1706    /// it is the one combination that must decline: copying would read a
1707    /// texture the producer is still writing, silently and only sometimes,
1708    /// while `None` sends the caller to the host map the immediate context
1709    /// orders.
1710    #[test]
1711    fn a_recorded_write_with_no_wait_behind_it_declines_the_copy() {
1712        assert!(d3d11_copy_is_ordered(0, false), "nothing to wait for");
1713        assert!(d3d11_copy_is_ordered(0, true));
1714        assert!(d3d11_copy_is_ordered(42, true), "the wait was queued");
1715        assert!(
1716            !d3d11_copy_is_ordered(42, false),
1717            "no semaphore, or the wait failed to queue"
1718        );
1719    }
1720}
1721
1722#[cfg(all(test, target_os = "windows"))]
1723mod d3d11_desc_layout {
1724    use super::*;
1725    fn off<T, F>(base: &T, field: &F) -> usize {
1726        (field as *const F as usize) - (base as *const T as usize)
1727    }
1728
1729    // Field offsets measured against this box's CUDA 11.8 driver_types.h
1730    // with a compiled offsetof program (`cl /nologo /I"%CUDA_PATH%\include"
1731    // layouts.c`, MSVC from VS 2026's dev shell, x64):
1732    //   handle_desc 40 handle@8 size@24 flags@32
1733    //   mip_desc 64 format@8 extent@32 flags@56 levels@60
1734    //   sem_desc 32 handle@8 flags@24
1735    //   wait_params 144 value@0 flags@72
1736    // These offsets are asserted below and hold in both 11.8 and 12.x/13.x
1737    // (the newer headers only append fields after them).
1738    //
1739    // The sizes asserted below (104/128/96/144) are the CUDA 12.x/13.x
1740    // sizes, not the 11.8 sizes just measured (40/64/32/144) -- see the
1741    // struct docs and the section comment above `CudaExternalMemoryHandleDescWin32`
1742    // for why: the loader prefers a 12.x/13.x runtime when present, and that
1743    // runtime reads `unsigned int reserved[16]` fields these three structs
1744    // don't have in 11.8's header. No 12.x/13.x driver_types.h exists on this
1745    // box to run the offsetof program against, so 104/128/96 are derived
1746    // arithmetically here: each struct's measured 11.8 layout, plus 64 bytes
1747    // for `reserved[16]`, plus padding to the next multiple of 8 (this
1748    // module's structs are all 8-aligned, from their `u64`/pointer fields):
1749    //   handle_desc: 36 (fields end) + 64 (reserved) = 100 -> pad to 104
1750    //   mip_desc:    64 (fields end) + 64 (reserved) = 128 -> already aligned
1751    //   sem_desc:    28 (fields end) + 64 (reserved) = 92  -> pad to 96
1752    // `cudaExternalSemaphoreWaitParams` already carries its reserved arrays
1753    // in 11.8, so 144 is both the 11.8 and the 12.x/13.x size.
1754    #[test]
1755    fn external_memory_and_semaphore_descs_match_driver_types_h() {
1756        assert_eq!(
1757            std::mem::size_of::<CudaExternalMemoryHandleDescWin32>(),
1758            104
1759        );
1760        // SAFETY: every field of `CudaExternalMemoryHandleDescWin32` is an
1761        // integer, raw pointer or fixed-size array of those -- all-zero is a
1762        // valid bit pattern for each, so a zeroed instance is well-defined;
1763        // this test only reads field offsets and the struct's size, never
1764        // dereferences the null `win32_handle`/`win32_name` pointers.
1765        let d: CudaExternalMemoryHandleDescWin32 = unsafe { std::mem::zeroed() };
1766        assert_eq!(off(&d, &d.win32_handle), 8);
1767        assert_eq!(off(&d, &d.size), 24);
1768        assert_eq!(off(&d, &d.flags), 32);
1769        assert_eq!(
1770            std::mem::size_of::<CudaExternalMemoryMipmappedArrayDesc>(),
1771            128
1772        );
1773        // SAFETY: same reasoning as `d` above -- every field is an integer,
1774        // nested POD struct or array of those, so all-zero is valid.
1775        let m: CudaExternalMemoryMipmappedArrayDesc = unsafe { std::mem::zeroed() };
1776        assert_eq!(off(&m, &m.format_desc), 8);
1777        assert_eq!(off(&m, &m.extent), 32);
1778        assert_eq!(off(&m, &m.flags), 56);
1779        assert_eq!(off(&m, &m.num_levels), 60);
1780        assert_eq!(std::mem::size_of::<CudaExternalSemaphoreHandleDesc>(), 96);
1781        // SAFETY: same reasoning as `d` above; the null `win32_handle`/
1782        // `win32_name` pointers are never dereferenced.
1783        let s: CudaExternalSemaphoreHandleDesc = unsafe { std::mem::zeroed() };
1784        assert_eq!(off(&s, &s.win32_handle), 8);
1785        assert_eq!(off(&s, &s.flags), 24);
1786        assert_eq!(std::mem::size_of::<CudaExternalSemaphoreWaitParams>(), 144);
1787        // SAFETY: same reasoning as `d` above -- every field is an integer
1788        // or fixed-size array of integers, so all-zero is valid.
1789        let w: CudaExternalSemaphoreWaitParams = unsafe { std::mem::zeroed() };
1790        assert_eq!(off(&w, &w.fence_value), 0);
1791        assert_eq!(off(&w, &w.flags), 72);
1792    }
1793}
1794
1795#[cfg(test)]
1796mod tests {
1797    use super::*;
1798    #[test]
1799    fn cuda_table_loads_when_libcudart_present() {
1800        let avail = is_cuda_available();
1801        if avail {
1802            assert!(table().is_some(), "table present when available");
1803            let path = runtime_path().expect("runtime_path set whenever the table loaded");
1804            assert!(
1805                !path.as_os_str().is_empty(),
1806                "runtime_path must name the DLL/soname the loader opened"
1807            );
1808            // Printed so the report's "which DLL did the loader open" claim
1809            // is reproducible straight from this test's output.
1810            eprintln!("[cuda] runtime_path = {}", path.display());
1811        }
1812        // total + non-panicking either way
1813    }
1814
1815    #[test]
1816    fn pub_primitives_degrade_without_libcudart() {
1817        // On hosts without libcudart (all CI coverage lanes), the public CUDA
1818        // primitives must degrade cleanly — None / false / no-op, never panic.
1819        // Skip on a CUDA host, where these would touch the real driver without
1820        // a current GL context.
1821        if is_cuda_available() {
1822            return;
1823        }
1824        assert!(gl_register_buffer(0).is_none());
1825        assert!(gl_map_resource(0).is_none());
1826        gl_unmap_resource(0); // no-op without a table — must not panic
1827        gl_unregister_resource(0); // no-op without a table — must not panic
1828                                   // SAFETY: with no libcudart, memcpy_device_to_host returns false before
1829                                   // touching the pointers, so null/zero args are sound here.
1830        assert!(!unsafe { memcpy_device_to_host(std::ptr::null_mut(), std::ptr::null(), 0) });
1831    }
1832}
1833
1834#[cfg(test)]
1835mod handle_tests {
1836    use super::*;
1837    use std::sync::{
1838        atomic::{AtomicUsize, Ordering},
1839        Arc,
1840    };
1841    struct MockOps {
1842        unmaps: Arc<AtomicUsize>,
1843        unregisters: Arc<AtomicUsize>,
1844    }
1845    impl CudaGlOps for MockOps {
1846        fn map(&self, _r: GraphicsResource) -> Option<(*mut std::ffi::c_void, usize)> {
1847            Some((0x1000usize as *mut _, 4096))
1848        }
1849        fn unmap(&self, _r: GraphicsResource) {
1850            self.unmaps.fetch_add(1, Ordering::SeqCst);
1851        }
1852        fn unregister(&self, _r: GraphicsResource) {
1853            self.unregisters.fetch_add(1, Ordering::SeqCst);
1854        }
1855    }
1856    #[test]
1857    fn cudamap_guard_unmaps_on_drop_for_glbuffer() {
1858        let unmaps = Arc::new(AtomicUsize::new(0));
1859        let unregisters = Arc::new(AtomicUsize::new(0));
1860        {
1861            let h = CudaHandle::new_gl(
1862                0x1usize as GraphicsResource,
1863                4096,
1864                Arc::new(MockOps {
1865                    unmaps: unmaps.clone(),
1866                    unregisters: unregisters.clone(),
1867                }),
1868            );
1869            {
1870                let m = h.map().expect("map");
1871                assert_eq!(m.device_ptr() as usize, 0x1000);
1872                assert_eq!(m.len(), 4096);
1873                assert!(!m.is_empty());
1874            }
1875            // CudaMap dropped → exactly one unmap; handle still alive → no unregister yet.
1876            assert_eq!(
1877                unmaps.load(Ordering::SeqCst),
1878                1,
1879                "Drop must unmap a GlBuffer"
1880            );
1881            assert_eq!(unregisters.load(Ordering::SeqCst), 0);
1882        }
1883        // CudaHandle dropped → exactly one unregister.
1884        assert_eq!(
1885            unregisters.load(Ordering::SeqCst),
1886            1,
1887            "Dropping a GlBuffer handle must unregister"
1888        );
1889    }
1890
1891    /// A GlBuffer handle whose ops.map() fails yields None from CudaHandle::map.
1892    struct NoneOps;
1893    impl CudaGlOps for NoneOps {
1894        fn map(&self, _r: GraphicsResource) -> Option<(*mut std::ffi::c_void, usize)> {
1895            None
1896        }
1897        fn unmap(&self, _r: GraphicsResource) {}
1898        fn unregister(&self, _r: GraphicsResource) {}
1899    }
1900    #[test]
1901    fn glbuffer_map_returns_none_when_ops_map_fails() {
1902        let h = CudaHandle::new_gl(0x9usize as GraphicsResource, 4096, Arc::new(NoneOps));
1903        assert!(
1904            h.map().is_none(),
1905            "GlBuffer map propagates ops.map() failure"
1906        );
1907    }
1908
1909    #[test]
1910    fn glbuffer_handle_debug_and_empty_map() {
1911        let unmaps = Arc::new(AtomicUsize::new(0));
1912        let unregisters = Arc::new(AtomicUsize::new(0));
1913        let h = CudaHandle::new_gl(
1914            0x2usize as GraphicsResource,
1915            0,
1916            Arc::new(MockOps {
1917                unmaps: unmaps.clone(),
1918                unregisters: unregisters.clone(),
1919            }),
1920        );
1921        let dbg = format!("{h:?}");
1922        assert!(
1923            dbg.contains("GlBuffer"),
1924            "debug names the backing kind: {dbg}"
1925        );
1926        assert!(dbg.contains("size"), "debug includes size: {dbg}");
1927    }
1928
1929    #[test]
1930    fn external_mem_map_is_persistent_and_debug_names_kind() {
1931        // ExternalMem handle: map() returns the persistent device ptr directly
1932        // (no GL routing, unmap is a no-op). Construct with a synthetic ptr.
1933        let dptr = 0xCAFE_0000usize as *mut std::ffi::c_void;
1934        let h = CudaHandle::new_external(std::ptr::null_mut(), dptr, 8192);
1935        let dbg = format!("{h:?}");
1936        assert!(dbg.contains("ExternalMem"), "debug names the kind: {dbg}");
1937        {
1938            let m = h.map().expect("ExternalMem map is always Some");
1939            assert_eq!(m.device_ptr(), dptr, "persistent device ptr passthrough");
1940            assert_eq!(m.len(), 8192);
1941            assert!(!m.is_empty());
1942            // CudaMap drops here: ExternalMem mapping has unmap=None → no-op, safe.
1943        }
1944        // HOST-SAFETY: dropping `h` would call the real cudaDestroyExternalMemory
1945        // on this synthetic handle (libcudart is present on dev hosts). Forget it.
1946        std::mem::forget(h);
1947    }
1948
1949    #[test]
1950    fn external_mem_zero_len_map_is_empty() {
1951        let h = CudaHandle::new_external(std::ptr::null_mut(), std::ptr::null_mut(), 0);
1952        {
1953            let m = h.map().expect("map");
1954            assert_eq!(m.len(), 0);
1955            assert!(m.is_empty(), "zero-length mapping is empty");
1956            assert!(m.device_ptr().is_null());
1957        }
1958        std::mem::forget(h); // HOST-SAFETY: avoid real cudaDestroyExternalMemory
1959    }
1960}