// Oak Video Editor - Non-Linear Video Editor // Copyright (C) 2026 Oak Team // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . //! M5: zero-copy hardware-decode import. //! //! A decoded hardware surface (VAAPI/NVDEC/D3D11VA/VideoToolbox) is //! imported into the engine's wgpu device as a pair of planar textures //! (luma + interleaved chroma) instead of being downloaded with //! `av_hwframe_transfer_data`. The render side turns the planes into //! working-space RGBA with the M2 YUV→RGB pass; the CPU staging path //! stays the per-frame fallback. //! //! Platform surface extraction lives here (FFmpeg types); the raw //! Vulkan/Metal import lives in `oak_core::backend::external`. //! //! ## Counters //! //! - [`HW_IMPORTS`] — frames that took the zero-copy path. //! - [`HW_IMPORT_FALLBACKS`] — attempts that were tried and failed. //! - [`HW_IMPORT_UNSUPPORTED`] — frames that skipped the import because //! no import path exists (NVDEC, a platform switch off, an //! unimportable layout); not failures. //! - `hwdecode::HW_TRANSFERS` — frames downloaded to system memory; a //! zero-copy hardware frame must not move it. use std::sync::Arc; use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; use ffmpeg::ffi as sys; use ffmpeg_next as ffmpeg; use oak_core::backend::{GpuContext, YuvTransform}; #[cfg(target_os = "linux")] use oak_core::backend::ImportGuard; use oak_core::colormath::YuvMatrix; use oak_core::texture::PlanarFormat; /// Frames that were imported to the GPU instead of downloaded (M5 /// acceptance: the hardware path keeps `hwdecode::HW_TRANSFERS` at 0). pub static HW_IMPORTS: AtomicU64 = AtomicU64::new(0); /// Import attempts that were actually attempted and failed (per-frame /// fallback; feeds the consecutive-failure breaker). "Unsupported" /// outcomes are deliberately NOT counted here — a missing import path is /// not a failure (M5 audit). pub static HW_IMPORT_FALLBACKS: AtomicU64 = AtomicU64::new(0); /// Frames that took the staging path because no import path exists for /// this device/machine/configuration: NVDEC/CUDA has no public handle /// export, a per-platform switch is off, or the surface layout is not /// importable. Observability only: neither counted as a fallback nor fed /// to the breaker. pub static HW_IMPORT_UNSUPPORTED: AtomicU64 = AtomicU64::new(0); /// Consecutive import failures after which the process stops attempting /// (the platform combination is evidently unsupported on this machine). /// A single failure does not disable anything — the next frame is /// attempted afresh, as the per-frame fallback contract requires. const STICKY_FAILURE_LIMIT: u32 = 8; static CONSECUTIVE_FAILURES: AtomicU32 = AtomicU32::new(0); /// The config key of the zero-copy import switch (1 = import, 0 = /// staging). Default ON; `OAK_GPU_IMPORT=0` overrides both. pub const CONFIG_KEY_GPU_DECODE_IMPORT: &str = "GpuDecodeImport"; /// The per-platform import switches (each platform's import is an /// independent, revertable row of design §3.6). pub const CONFIG_KEY_VAAPI_IMPORT: &str = "GpuDecodeImportVaapi"; /// Windows D3D11VA import switch. pub const CONFIG_KEY_D3D11_IMPORT: &str = "GpuDecodeImportD3d11"; /// macOS VideoToolbox import switch. pub const CONFIG_KEY_VIDEOTOOLBOX_IMPORT: &str = "GpuDecodeImportVideotoolbox"; /// Whether the zero-copy hardware import is preferred. `OAK_GPU_IMPORT=0` /// force-disables it (diagnostics/CI without importable surfaces), same /// convention as `OAK_HWACCEL`. pub fn gpu_import_enabled() -> bool { if let Ok(v) = std::env::var("OAK_GPU_IMPORT") { return v != "0"; } switch_from_config(CONFIG_KEY_GPU_DECODE_IMPORT) } /// One platform's import switch (global import switch AND platform /// switch). `env` overrides the config key, like the global switch. fn platform_import_enabled(env: &str, config: &str) -> bool { if let Ok(v) = std::env::var(env) { return v != "0"; } switch_from_config(config) } fn switch_from_config(key: &str) -> bool { match oak_core::configstore::ConfigStore::instance().get(None, key) { Ok(value) => value != "false", Err(_) => true, } } /// Reset the import counters (tests). pub fn reset_import_counters() { HW_IMPORTS.store(0, Ordering::Relaxed); HW_IMPORT_FALLBACKS.store(0, Ordering::Relaxed); HW_IMPORT_UNSUPPORTED.store(0, Ordering::Relaxed); CONSECUTIVE_FAILURES.store(0, Ordering::Relaxed); } /// Everything the import needs from the decoder's frame and colorimetry. pub struct HwImportRequest<'a> { /// The decoded frame (a hardware surface when import is possible). /// Borrowed for the duration of the attempt; the import guard takes /// its own reference to the underlying surface. pub frame: *const sys::AVFrame, /// Lifetime marker for the borrowed frame. pub _marker: std::marker::PhantomData<&'a ()>, /// The session's hardware device type (`None` = software decode). pub device_type: Option, /// Frame dimensions (luma). pub luma_size: (u32, u32), /// The frame's YUV→RGB luma matrix. pub matrix: YuvMatrix, /// Full/limited range. pub full_range: bool, /// Source color-primaries code point. pub color_primaries: i32, /// Source transfer-characteristic code point. pub color_trc: i32, } /// A successfully imported hardware frame: the plane tokens plus the /// colorimetry the render side needs to resolve them to working-space /// RGBA. pub struct ImportedHwFrame { /// Luma plane token. pub y: u64, /// Interleaved chroma plane token. pub uv: u64, /// Plane layout (bit depth / interleave). pub format: PlanarFormat, /// Luma dimensions. pub width: u32, /// Frame height. pub height: u32, /// The GPU device that owns the planes. pub ctx: Arc, /// Extra lifetime guard for platforms whose GPU texture types cannot /// carry a drop callback (macOS): the decoder's pixel buffer. pub keep_alive: Option>, } impl ImportedHwFrame { /// The YUV→RGB transform for the planes (the bit depth picks the /// exact limited-range expansion: 8 for NV12, 16 for P010). pub fn transform(&self, matrix: YuvMatrix, full_range: bool) -> YuvTransform { YuvTransform::from_matrix_depth(matrix, full_range, self.format.bit_depth()) } } /// Try to import `req.frame` as a pair of planar GPU textures. `None` /// means "use the staging path for this frame" — the caller must then /// call the CPU retrieval (the decoder session cache still holds the /// decoded surface, so no re-decode happens). pub fn try_import_hw_frame(req: &HwImportRequest<'_>) -> Option { if !gpu_import_enabled() { return None; } if CONSECUTIVE_FAILURES.load(Ordering::Relaxed) >= STICKY_FAILURE_LIMIT { return None; } let device_type = req.device_type?; // Only a host-installed device (the app's UI/render context) may take // the frame: a lazily created worker/CLI context must keep decoding on // the CPU path. Checked before touching the frame so the common // "no import here" case costs nothing. if !GpuContext::host_gpu_installed() { return None; } if !crate::hwdecode::is_hw_format(frame_format(req.frame)) { return None; } let Some(ctx) = GpuContext::shared() else { // The host slot was cleared between the two checks (shutdown): // nothing was attempted, so this is not a failure. HW_IMPORT_UNSUPPORTED.fetch_add(1, Ordering::Relaxed); return None; }; match platform_import(req, &ctx, device_type) { ImportOutcome::Imported { y, uv, format, keep_alive, } => { CONSECUTIVE_FAILURES.store(0, Ordering::Relaxed); HW_IMPORTS.fetch_add(1, Ordering::Relaxed); Some(ImportedHwFrame { y, uv, format, width: req.luma_size.0, height: req.luma_size.1, ctx, keep_alive, }) } ImportOutcome::Unsupported => { HW_IMPORT_UNSUPPORTED.fetch_add(1, Ordering::Relaxed); None } ImportOutcome::Failed => { bump_fallback(); None } } } /// Outcome of one platform import attempt. enum ImportOutcome { /// The planes were imported; the tokens and keep-alive guard. Imported { y: u64, uv: u64, format: PlanarFormat, keep_alive: Option>, }, /// No import path exists for this device/machine/configuration (the /// frame takes the staging path without being a failure). Unsupported, /// The import path exists and the attempt failed: per-frame fallback, /// counted and fed to the consecutive-failure breaker. Failed, } fn bump_fallback() { HW_IMPORT_FALLBACKS.fetch_add(1, Ordering::Relaxed); CONSECUTIVE_FAILURES.fetch_add(1, Ordering::Relaxed); } /// The frame's raw pixel format (hardware variants included). fn frame_format(frame: *const sys::AVFrame) -> sys::AVPixelFormat { // SAFETY: plain read of the frame's format field. unsafe { std::mem::transmute::((*frame).format) } } #[cfg(target_os = "linux")] fn platform_import( req: &HwImportRequest<'_>, ctx: &Arc, device_type: sys::AVHWDeviceType, ) -> ImportOutcome { use sys::AVHWDeviceType; if device_type != AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI { // CUDA/NVDEC surfaces have no public fd/handle export; the // staging path is the honest fallback (design §3.6) — NOT a // failure. return ImportOutcome::Unsupported; } if !platform_import_enabled("OAK_GPU_IMPORT_VAAPI", CONFIG_KEY_VAAPI_IMPORT) { return ImportOutcome::Unsupported; } vaapi_import(req, ctx) } #[cfg(target_os = "windows")] fn platform_import( req: &HwImportRequest<'_>, ctx: &Arc, device_type: sys::AVHWDeviceType, ) -> ImportOutcome { if device_type != sys::AVHWDeviceType::AV_HWDEVICE_TYPE_D3D11VA { return ImportOutcome::Unsupported; } if !platform_import_enabled("OAK_GPU_IMPORT_D3D11", CONFIG_KEY_D3D11_IMPORT) { return ImportOutcome::Unsupported; } d3d11_import(req, ctx) } #[cfg(target_os = "macos")] fn platform_import( req: &HwImportRequest<'_>, ctx: &Arc, device_type: sys::AVHWDeviceType, ) -> ImportOutcome { if device_type != sys::AVHWDeviceType::AV_HWDEVICE_TYPE_VIDEOTOOLBOX { return ImportOutcome::Unsupported; } if !platform_import_enabled("OAK_GPU_IMPORT_VIDEOTOOLBOX", CONFIG_KEY_VIDEOTOOLBOX_IMPORT) { return ImportOutcome::Unsupported; } videotoolbox_import(req, ctx) } #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))] fn platform_import( _req: &HwImportRequest<'_>, _ctx: &Arc, _device_type: sys::AVHWDeviceType, ) -> ImportOutcome { ImportOutcome::Unsupported } // --------------------------------------------------------------------------- // Linux: VAAPI surface -> DRM PRIME descriptor -> DMA-BUF plane imports // --------------------------------------------------------------------------- /// DRM format fourcc values (little-endian `fourcc_code`). #[cfg(target_os = "linux")] mod drm_fourcc { pub const R8: u32 = 0x2020_3852; // 'R','8',' ',' ' pub const R16: u32 = 0x2036_3152; // 'R','1','6',' ' pub const GR88: u32 = 0x3838_5252; // 'R','R','8','8' pub const RG88: u32 = 0x3838_4752; // 'R','G','8','8' pub const RG1616: u32 = 0x3631_4752; // 'R','G','1','6' } /// Map a VAAPI frame to its DRM PRIME descriptor (FFmpeg calls /// `vaExportSurfaceHandle` internally; no CPU download happens). #[cfg(target_os = "linux")] fn vaapi_import(req: &HwImportRequest<'_>, ctx: &Arc) -> ImportOutcome { let mut drm_frame = ffmpeg::frame::Video::empty(); // SAFETY: `drm_frame` is a valid AVFrame; the map only writes into it // and takes references to the source frame's hwframe mapping. let rc = unsafe { let raw = drm_frame.as_mut_ptr(); (*raw).format = sys::AVPixelFormat::AV_PIX_FMT_DRM_PRIME as i32; sys::av_hwframe_map(raw, req.frame, sys::AV_HWFRAME_MAP_READ as i32) }; if rc < 0 { // The VAAPI session exists but the surface could not be exported // as DRM PRIME: a real failure (the breaker bounds the retries). return ImportOutcome::Failed; } // SAFETY: on success `data[0]` is the descriptor FFmpeg allocated for // this mapping; it stays valid until `drm_frame` is unref'd. let desc = unsafe { &*((*drm_frame.as_ptr()).data[0] as *const sys::AVDRMFrameDescriptor) }; if desc.nb_layers != 2 { return ImportOutcome::Unsupported; } let layer_format = |i: usize| desc.layers[i].format; let (format, y_fourcc, uv_fourcc) = match (layer_format(0), layer_format(1)) { (a, b) if a == drm_fourcc::R8 && (b == drm_fourcc::GR88 || b == drm_fourcc::RG88) => { (PlanarFormat::Nv12, a, b) } (a, b) if a == drm_fourcc::R16 && b == drm_fourcc::RG1616 => { (PlanarFormat::P010, a, b) } _ => return ImportOutcome::Unsupported, }; let _ = (y_fourcc, uv_fourcc); for layer in &desc.layers[..desc.nb_layers as usize] { if layer.nb_planes != 1 { return ImportOutcome::Unsupported; } } let (lw, lh) = req.luma_size; if lw == 0 || lh == 0 { return ImportOutcome::Unsupported; } let (cw, ch) = (lw.div_ceil(2), lh.div_ceil(2)); let plane = |layer: usize| { let l = &desc.layers[layer]; let p = &l.planes[0]; let obj = &desc.objects[p.object_index as usize]; oak_core::backend::external::DmaBufPlane { fd: obj.fd, offset: p.offset as u64, pitch: p.pitch as u64, modifier: obj.format_modifier, } }; // Shared keep-alive: the source frame pins the VAAPI surface until // the last plane texture is destroyed (the dmabuf fds Vulkan imported // keep the buffer itself alive even after the mapping drops). let Some(keep) = crate::ffmpeg::RefFrame::clone_raw(req.frame) else { return ImportOutcome::Failed; }; let keep = Arc::new(keep); let y_guard = keep.clone(); let y = match ctx.import_dmabuf_plane(lw, lh, format.y_format(), plane(0), guard(y_guard)) { Ok(y) => y, Err(oak_core::error::Error::State) | Err(oak_core::error::Error::Invalid) => { return ImportOutcome::Unsupported } Err(_) => return ImportOutcome::Failed, }; let uv_guard = keep.clone(); match ctx.import_dmabuf_plane(cw, ch, format.uv_format(), plane(1), guard(uv_guard)) { Ok(uv) => ImportOutcome::Imported { y, uv, format, keep_alive: None, }, Err(_) => { ctx.destroy_texture(y); ImportOutcome::Failed } } } /// A keep-alive import guard holding one `Arc` of the decoder frame /// (Linux/VAAPI path; Windows attaches its guard inline and macOS keeps /// the frame on the planar texture). #[cfg(target_os = "linux")] fn guard(value: Arc) -> Option { Some(Box::new(move || drop(value))) } // --------------------------------------------------------------------------- // Windows: D3D11VA texture -> shared NT handle -> Vulkan import // --------------------------------------------------------------------------- /// Import a D3D11VA texture (Windows): one NV12/P010 texture shared as an /// NT handle, imported as a single multi-planar image whose two plane /// views feed the planar YUV→RGB pass. The decoder's frame reference is /// kept alive by the texture's drop guard. #[cfg(target_os = "windows")] fn d3d11_import(req: &HwImportRequest<'_>, ctx: &Arc) -> ImportOutcome { use windows::core::Interface; use windows::Win32::Foundation::CloseHandle; use windows::Win32::Graphics::Direct3D11::{ID3D11Texture2D, D3D11_TEXTURE2D_DESC}; use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_NV12, DXGI_FORMAT_P010}; use windows::Win32::Graphics::Dxgi::{IDXGIResource1, DXGI_SHARED_RESOURCE_READ}; // AV_PIX_FMT_D3D11: data[0] = ID3D11Texture2D*, data[1] = subresource // index. D3D11VA frames cycle through the decoder's surface ARRAY, so // the slice index is part of the frame identity — the import selects // it via an array-layer view (M5 audit). let texture_ptr = unsafe { (*req.frame).data[0] } as *mut std::ffi::c_void; if texture_ptr.is_null() { return ImportOutcome::Unsupported; } let slice = unsafe { (*req.frame).data[1] } as usize as u32; // SAFETY: FFmpeg owns the texture reference for the frame's lifetime. let texture: &ID3D11Texture2D = unsafe { &*(texture_ptr as *const ID3D11Texture2D) }; let mut desc = D3D11_TEXTURE2D_DESC::default(); unsafe { texture.GetDesc(&mut desc) }; let array_size = desc.ArraySize.max(1); if slice >= array_size { return ImportOutcome::Unsupported; } let (texture_format, planar) = match desc.Format { DXGI_FORMAT_NV12 => (oak_core::wgpu::TextureFormat::NV12, PlanarFormat::Nv12), DXGI_FORMAT_P010 => (oak_core::wgpu::TextureFormat::P010, PlanarFormat::P010), _ => return ImportOutcome::Unsupported, }; // SAFETY: the D3D11 texture supports IDXGIResource1 (every shared // texture does); a cast failure means it is not a shareable resource. let Ok(resource) = texture.cast::() else { return ImportOutcome::Unsupported; }; // Read-only NT handle; Vulkan references the resource, so the handle // is closed again right after the import. let handle = unsafe { resource.CreateSharedHandle( None, DXGI_SHARED_RESOURCE_READ.0, None::<&windows::core::PCWSTR>, ) }; let Ok(handle) = handle else { return ImportOutcome::Failed; }; let Some(keep) = crate::ffmpeg::RefFrame::clone_raw(req.frame) else { unsafe { let _ = CloseHandle(handle); } return ImportOutcome::Failed; }; let (w, h) = req.luma_size; let guard: Arc = Arc::new(keep); let result = ctx.import_d3d11_shared_texture( handle.0 as isize, w, h, texture_format, array_size, slice, Some(Box::new(move || drop(guard))), ); unsafe { let _ = CloseHandle(handle); } match result { Ok((y, uv)) => ImportOutcome::Imported { y, uv, format: planar, keep_alive: None, }, // State/Invalid are capability gaps (missing device feature, wrong // format), not failed attempts. Err(oak_core::error::Error::State) | Err(oak_core::error::Error::Invalid) => { ImportOutcome::Unsupported } Err(_) => ImportOutcome::Failed, } } // --------------------------------------------------------------------------- // macOS: VideoToolbox CVPixelBuffer -> IOSurface -> Metal textures // --------------------------------------------------------------------------- /// `CVPixelBufferGetIOSurface` (CoreVideo). #[cfg(target_os = "macos")] #[link(name = "CoreVideo", kind = "framework")] extern "C" { fn CVPixelBufferGetIOSurface(pixel_buffer: *mut std::ffi::c_void) -> *mut std::ffi::c_void; } /// Import a VideoToolbox frame (macOS): its CVPixelBuffer's IOSurface is /// wrapped in one Metal texture per plane. Metal textures cannot carry a /// drop callback, so the decoder's frame reference travels back in /// `ImportedHwFrame::keep_alive` and is held by the planar texture. #[cfg(target_os = "macos")] fn videotoolbox_import(req: &HwImportRequest<'_>, ctx: &Arc) -> ImportOutcome { // AV_PIX_FMT_VIDEOTOOLBOX keeps the CVPixelBuffer in data[3]. let pixel_buffer = unsafe { (*req.frame).data[3] } as *mut std::ffi::c_void; if pixel_buffer.is_null() { return ImportOutcome::Unsupported; } let surface = unsafe { CVPixelBufferGetIOSurface(pixel_buffer) }; if surface.is_null() { return ImportOutcome::Unsupported; } let frames = unsafe { (*req.frame).hw_frames_ctx }; if frames.is_null() { return ImportOutcome::Unsupported; } // SAFETY: `hw_frames_ctx` is an AVHWFramesContext for this frame. let sw_format = unsafe { (*(*frames).data as *const sys::AVHWFramesContext).sw_format }; let format = match sw_format { sys::AVPixelFormat::AV_PIX_FMT_NV12 => PlanarFormat::Nv12, sys::AVPixelFormat::AV_PIX_FMT_P010LE | sys::AVPixelFormat::AV_PIX_FMT_P010BE => { PlanarFormat::P010 } _ => return ImportOutcome::Unsupported, }; let (w, h) = req.luma_size; let (cw, ch) = (w.div_ceil(2), h.div_ceil(2)); // SAFETY: `surface` is the CVPixelBuffer's IOSurface and the decoder // frame reference is kept alive by the import guard (planar texture). let Ok(y) = (unsafe { ctx.import_iosurface_plane(surface, 0, w, h, format.y_format()) }) else { return ImportOutcome::Failed; }; let uv = match unsafe { ctx.import_iosurface_plane(surface, 1, cw, ch, format.uv_format()) } { Ok(uv) => uv, Err(_) => { ctx.destroy_texture(y); return ImportOutcome::Failed; } }; let Some(keep) = crate::ffmpeg::RefFrame::clone_raw(req.frame) else { ctx.destroy_texture(y); ctx.destroy_texture(uv); return ImportOutcome::Failed; }; ImportOutcome::Imported { y, uv, format, keep_alive: Some(Arc::new(keep)), } } #[cfg(test)] mod tests { use super::*; fn empty_frame() -> ffmpeg::frame::Video { ffmpeg::frame::Video::empty() } fn request(frame: &ffmpeg::frame::Video, device_type: sys::AVHWDeviceType) -> HwImportRequest<'_> { HwImportRequest { frame: unsafe { frame.as_ptr() }, _marker: std::marker::PhantomData, device_type: Some(device_type), luma_size: (64, 64), matrix: YuvMatrix::Bt709, full_range: false, color_primaries: 1, color_trc: 1, } } /// Test-only RAII override for a process-global environment variable: /// restores the previous value (or absence) on drop, so a panicking /// assertion cannot leak the override into the next serialized test and /// an externally preset value survives the test. struct EnvVarGuard { name: &'static str, previous: Option, } impl EnvVarGuard { /// Set `name=value` for the guard's lifetime. fn set(name: &'static str, value: &str) -> Self { let previous = std::env::var_os(name); std::env::set_var(name, value); Self { name, previous } } /// Remove `name` for the guard's lifetime. fn remove(name: &'static str) -> Self { let previous = std::env::var_os(name); std::env::remove_var(name); Self { name, previous } } } impl Drop for EnvVarGuard { fn drop(&mut self) { match self.previous.take() { Some(value) => std::env::set_var(self.name, value), None => std::env::remove_var(self.name), } } } /// Without a host-installed device the import declines before any /// frame work (and is not counted as a fallback: nothing failed). #[test] fn import_requires_a_host_installed_context() { let _g = crate::lock_tests(); reset_import_counters(); assert!(!GpuContext::host_gpu_installed()); let frame = empty_frame(); let req = request(&frame, sys::AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI); assert!(try_import_hw_frame(&req).is_none()); assert_eq!(HW_IMPORTS.load(Ordering::Relaxed), 0); assert_eq!(HW_IMPORT_FALLBACKS.load(Ordering::Relaxed), 0); assert_eq!(HW_IMPORT_UNSUPPORTED.load(Ordering::Relaxed), 0); } /// The counter classification (M5 audit): a device type with no import /// path (NVDEC/CUDA) is "unsupported", never a failure — it must not /// feed the consecutive-failure breaker or `HW_IMPORT_FALLBACKS`. /// Exercised at the platform-function level so no real GPU is needed. /// /// (`platform_import` is private; this test lives in the same module.) #[cfg(target_os = "linux")] #[test] fn non_importable_device_type_is_unsupported_not_failed() { let _g = crate::lock_tests(); // A context is required by the platform function's signature; // the non-VAAPI device check happens before any of its fields are // touched, so a lazily-created local context is never used. let Some(ctx) = GpuContext::create(oak_core::backend::BackendKind::Auto) else { eprintln!("no GPU adapter; skipping classification test"); return; }; let frame = empty_frame(); let req = request(&frame, sys::AVHWDeviceType::AV_HWDEVICE_TYPE_CUDA); assert!( matches!( platform_import(&req, &ctx, sys::AVHWDeviceType::AV_HWDEVICE_TYPE_CUDA), ImportOutcome::Unsupported ), "CUDA/NVDEC has no export path and must classify as unsupported" ); } /// The `OAK_GPU_IMPORT=0` escape hatch disables the path outright, and /// removing the override falls back to the default-on config. The /// override is RAII-scoped, so an externally preset value survives the /// test instead of being clobbered by `remove_var`. #[test] fn import_switch_disables_the_path() { let _g = crate::lock_tests(); let previous = std::env::var_os("OAK_GPU_IMPORT"); { let _off = EnvVarGuard::set("OAK_GPU_IMPORT", "0"); assert!(!gpu_import_enabled(), "explicit 0 disables the import path"); } assert_eq!( std::env::var_os("OAK_GPU_IMPORT"), previous, "the guard restores the externally preset value" ); if previous.is_none() { // No override: the config-store default keeps the import path // on. (With an external value the caller's setting governs, so // asserting the default would be wrong.) let _unset = EnvVarGuard::remove("OAK_GPU_IMPORT"); assert!( gpu_import_enabled(), "unset falls back to the default-on config" ); } } /// Plane layouts carry the expansion depth the YUV transform needs. #[test] fn planar_formats_carry_their_bit_depth() { assert_eq!(PlanarFormat::Nv12.bit_depth(), 8); assert_eq!(PlanarFormat::P010.bit_depth(), 16); assert_eq!(PlanarFormat::Nv12.name(), "nv12"); } }