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}