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