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