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