From 68dc0b16ea7da887519d28b0d8bdec40ef4e102b Mon Sep 17 00:00:00 2001 From: quadsteel Date: Sat, 19 Sep 2026 16:30:37 +0300 Subject: [PATCH 1/2] streamer(vulkan): prioritize capable dGPUs and fix AppImage GLib clash - Filter and score Vulkan physical devices based on video decode queue isolation and PRIME offload variables instead of indexing device 0. - Fall through to alternative physical devices when queue isolation fails. - Exclude host GLib and GObject binaries from the AppImage packaging pipeline to resolve host VA-API driver dlopen symbol clashes. - Ensure proper sync object drain before Vulkan teardown under Wayland. --- .github/workflows/qt-build.yml | 4 + .github/workflows/qt-release-candidate.yml | 4 + .../src/presentation.rs | 14 +- .../src/vulkan_device.rs | 320 ++++++++++++++++-- .../rendering/LinuxVulkanGraphics.cpp | 8 + 5 files changed, 313 insertions(+), 37 deletions(-) diff --git a/.github/workflows/qt-build.yml b/.github/workflows/qt-build.yml index 75899723f..25b284fb7 100644 --- a/.github/workflows/qt-build.yml +++ b/.github/workflows/qt-build.yml @@ -459,6 +459,10 @@ jobs: --icon-file build/AppDir/usr/share/icons/hicolor/scalable/apps/io.github.opencloudgaming.OpenNOW.svg \ --library "$(pkg-config --variable=libdir libva)/libva.so.2" \ --library "$(pkg-config --variable=libdir libva-drm)/libva-drm.so.2" \ + --exclude-library libglib-2.0.so.0 \ + --exclude-library libgobject-2.0.so.0 \ + --exclude-library libgio-2.0.so.0 \ + --exclude-library libgmodule-2.0.so.0 \ --plugin qt \ --output appimage test -f build/AppDir/usr/lib/libva.so.2 diff --git a/.github/workflows/qt-release-candidate.yml b/.github/workflows/qt-release-candidate.yml index c3143e5dd..5beb356a8 100644 --- a/.github/workflows/qt-release-candidate.yml +++ b/.github/workflows/qt-release-candidate.yml @@ -217,6 +217,10 @@ jobs: --icon-file build/AppDir/usr/share/icons/hicolor/scalable/apps/io.github.opencloudgaming.OpenNOW.svg \ --library "$(pkg-config --variable=libdir libva)/libva.so.2" \ --library "$(pkg-config --variable=libdir libva-drm)/libva-drm.so.2" \ + --exclude-library libglib-2.0.so.0 \ + --exclude-library libgobject-2.0.so.0 \ + --exclude-library libgio-2.0.so.0 \ + --exclude-library libgmodule-2.0.so.0 \ --plugin qt --output appimage test -f build/AppDir/usr/lib/libva.so.2 test -f build/AppDir/usr/lib/libva-drm.so.2 diff --git a/native/opennow-streamer/crates/opennow-streamer-platform-linux/src/presentation.rs b/native/opennow-streamer/crates/opennow-streamer-platform-linux/src/presentation.rs index 146a7ff57..e2b231b36 100644 --- a/native/opennow-streamer/crates/opennow-streamer-platform-linux/src/presentation.rs +++ b/native/opennow-streamer/crates/opennow-streamer-platform-linux/src/presentation.rs @@ -2013,7 +2013,19 @@ fn select_device( ) -> Result<(vk::PhysicalDevice, u32)> { let devices = unsafe { instance.enumerate_physical_devices() } .map_err(|error| vk_error("enumerate physical devices", error))?; - for device in devices { + let mut ranked: Vec<(usize, vk::PhysicalDevice, i64)> = devices + .into_iter() + .enumerate() + .map(|(index, device)| { + let props = unsafe { instance.get_physical_device_properties(device) }; + let queues = unsafe { instance.get_physical_device_queue_family_properties(device) }; + let score = crate::vulkan_device::score_physical_device(index, &props, &queues); + (index, device, score) + }) + .collect(); + ranked.sort_by(|a, b| b.2.cmp(&a.2)); + + for (_index, device, _score) in ranked { if !supports_swapchain(instance, device) .map_err(|error| vk_error("enumerate device extensions", error))? { diff --git a/native/opennow-streamer/crates/opennow-streamer-platform-linux/src/vulkan_device.rs b/native/opennow-streamer/crates/opennow-streamer-platform-linux/src/vulkan_device.rs index 0a440e338..c257efbcc 100644 --- a/native/opennow-streamer/crates/opennow-streamer-platform-linux/src/vulkan_device.rs +++ b/native/opennow-streamer/crates/opennow-streamer-platform-linux/src/vulkan_device.rs @@ -133,10 +133,127 @@ fn format_profile_index(codec: VideoCodec, pixel_format: PixelFormat) -> Option< Some(profile_index(codec, pixel_format.is_ten_bit()) + offset) } +#[cfg(feature = "vulkan")] +pub(crate) fn score_physical_device( + index: usize, + properties: &ash::vk::PhysicalDeviceProperties, + queue_families: &[ash::vk::QueueFamilyProperties], +) -> i64 { + let mut score = 0i64; + + // 1. Explicit user overrides (highest priority) + if let Ok(override_index) = std::env::var("OPENNOW_VK_DEVICE_INDEX") { + if let Ok(parsed) = override_index.trim().parse::() { + if parsed == index { + score += 100_000; + } + } + } + + if let Ok(override_vendor) = std::env::var("OPENNOW_VK_VENDOR_ID") { + let trimmed = override_vendor.trim(); + let parsed = if trimmed.starts_with("0x") || trimmed.starts_with("0X") { + u32::from_str_radix(&trimmed[2..], 16).ok() + } else { + trimmed.parse::().ok() + }; + if parsed == Some(properties.vendor_id) { + score += 50_000; + } + } + + // 2. PRIME / offload environment variables + let nv_prime = std::env::var("__NV_PRIME_RENDER_OFFLOAD") + .map(|v| v.trim() == "1") + .unwrap_or(false); + if nv_prime && properties.vendor_id == 0x10de { + score += 20_000; + } + + let dri_prime = std::env::var("DRI_PRIME") + .map(|v| v.trim() == "1") + .unwrap_or(false); + if dri_prime + && (properties.device_type == ash::vk::PhysicalDeviceType::DISCRETE_GPU + || properties.vendor_id != 0x8086) + { + score += 15_000; + } + + // 3. Discrete GPU preference over Integrated / CPU + match properties.device_type { + ash::vk::PhysicalDeviceType::DISCRETE_GPU => score += 3_000, + ash::vk::PhysicalDeviceType::INTEGRATED_GPU => score += 500, + ash::vk::PhysicalDeviceType::VIRTUAL_GPU => score += 200, + ash::vk::PhysicalDeviceType::CPU => score -= 1_000, + _ => {} + } + + // 4. Queue capabilities & queue isolation evaluation + let has_graphics = queue_families + .iter() + .any(|q| q.queue_flags.contains(ash::vk::QueueFlags::GRAPHICS)); + if !has_graphics { + score -= 50_000; + return score; + } + + let has_video_decode = queue_families + .iter() + .any(|q| q.queue_flags.contains(ash::vk::QueueFlags::VIDEO_DECODE_KHR)); + if has_video_decode { + score += 1_500; + } + + let has_compute = queue_families + .iter() + .any(|q| q.queue_flags.contains(ash::vk::QueueFlags::COMPUTE)); + let has_transfer = queue_families + .iter() + .any(|q| q.queue_flags.contains(ash::vk::QueueFlags::TRANSFER)); + if has_compute { + score += 500; + } + if has_transfer { + score += 200; + } + + // Isolation check: + // Either the graphics queue family has count > 1 (can split Qt queue from native), + // or if count == 1, there must be separate compute and decode queue families. + let can_isolate = queue_families.iter().any(|q| { + q.queue_flags.contains(ash::vk::QueueFlags::GRAPHICS) && q.queue_count > 1 + }) || (has_compute && has_video_decode); + + if can_isolate { + score += 2_000; + } else { + score -= 5_000; + } + + score +} + #[cfg(all(feature = "ffmpeg", feature = "vulkan"))] impl Drop for SharedVulkanDevice { fn drop(&mut self) { - unsafe { ffmpeg_next::ffi::av_buffer_unref(&mut self.device) }; + unsafe { + if !self.device.is_null() && self.info.device != 0 && self.info.instance != 0 { + use ash::vk::Handle as _; + if let Ok(entry) = ash::Entry::load() { + let instance = ash::Instance::load( + entry.static_fn(), + ash::vk::Instance::from_raw(self.info.instance as u64), + ); + let logical = ash::Device::load( + instance.fp_v1_0(), + ash::vk::Device::from_raw(self.info.device as u64), + ); + let _ = logical.device_wait_idle(); + } + } + ffmpeg_next::ffi::av_buffer_unref(&mut self.device); + } } } @@ -147,19 +264,8 @@ mod implementation { use ffmpeg_next::ffi; use std::{ffi::CStr, ptr}; - pub(super) fn create() -> Result> { - ffmpeg_next::init() - .map_err(|error| Error::backend(Subsystem::Ffmpeg, error.to_string()))?; - let mut device = ptr::null_mut(); + fn build_options(hdr_colorspace: bool) -> *mut ffi::AVDictionary { let mut options = ptr::null_mut(); - let entry = unsafe { ash::Entry::load() } - .map_err(|error| Error::unavailable(Subsystem::Vulkan, error.to_string()))?; - let extensions = unsafe { entry.enumerate_instance_extension_properties(None) } - .map_err(|error| Error::unavailable(Subsystem::Vulkan, error.to_string()))?; - let hdr_colorspace = extensions.iter().any(|extension| unsafe { - CStr::from_ptr(extension.extension_name.as_ptr()) - == ash::ext::swapchain_colorspace::NAME - }); let instance_extensions = if hdr_colorspace { c"VK_KHR_surface+VK_KHR_xlib_surface+VK_KHR_xcb_surface+VK_KHR_wayland_surface+VK_EXT_swapchain_colorspace" } else { @@ -180,33 +286,106 @@ mod implementation { ); ffi::av_dict_set(&mut options, c"limit_queues".as_ptr(), c"2".as_ptr(), 0); } - let result = unsafe { - ffi::av_hwdevice_ctx_create( - &mut device, - ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_VULKAN, - ptr::null(), - options, - 0, - ) + options + } + + pub(super) fn create() -> Result> { + ffmpeg_next::init() + .map_err(|error| Error::backend(Subsystem::Ffmpeg, error.to_string()))?; + let entry = unsafe { ash::Entry::load() } + .map_err(|error| Error::unavailable(Subsystem::Vulkan, error.to_string()))?; + let extensions = unsafe { entry.enumerate_instance_extension_properties(None) } + .map_err(|error| Error::unavailable(Subsystem::Vulkan, error.to_string()))?; + let hdr_colorspace = extensions.iter().any(|extension| unsafe { + CStr::from_ptr(extension.extension_name.as_ptr()) + == ash::ext::swapchain_colorspace::NAME + }); + + // Enumerate and score candidate physical devices using a temporary instance. + let mut candidates: Vec<(usize, String, i64)> = Vec::new(); + let app_info = vk::ApplicationInfo::default().api_version(vk::API_VERSION_1_1); + let instance_create_info = vk::InstanceCreateInfo::default().application_info(&app_info); + if let Ok(temp_instance) = unsafe { entry.create_instance(&instance_create_info, None) } { + if let Ok(devices) = unsafe { temp_instance.enumerate_physical_devices() } { + for (index, &device) in devices.iter().enumerate() { + let props = unsafe { temp_instance.get_physical_device_properties(device) }; + let queues = unsafe { temp_instance.get_physical_device_queue_family_properties(device) }; + let adapter = unsafe { CStr::from_ptr(props.device_name.as_ptr()) } + .to_string_lossy() + .into_owned(); + let score = score_physical_device(index, &props, &queues); + candidates.push((index, adapter, score)); + } + } + unsafe { temp_instance.destroy_instance(None) }; + } + candidates.sort_by(|a, b| b.2.cmp(&a.2)); + + let candidate_indices: Vec<(Option, String)> = if candidates.is_empty() { + vec![(None, "default".to_string())] + } else { + candidates + .into_iter() + .map(|(idx, name, score)| (Some(idx), format!("{name} (index {idx}, score {score})"))) + .collect() }; - unsafe { ffi::av_dict_free(&mut options) }; - if result < 0 || device.is_null() { - return Err(Error::unavailable( - Subsystem::Vulkan, - format!( - "shared Vulkan device creation failed: {}", + + let mut last_error = None; + for (candidate_idx, candidate_name) in candidate_indices { + let mut device = ptr::null_mut(); + let mut options = unsafe { build_options(hdr_colorspace) }; + + let device_param = candidate_idx.map(|idx| std::ffi::CString::new(idx.to_string()).unwrap()); + let device_param_ptr = device_param.as_ref().map(|s| s.as_ptr()).unwrap_or(ptr::null()); + + let result = unsafe { + ffi::av_hwdevice_ctx_create( + &mut device, + ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_VULKAN, + device_param_ptr, + options, + 0, + ) + }; + unsafe { ffi::av_dict_free(&mut options) }; + + if result < 0 || device.is_null() { + let err_msg = format!( + "candidate Vulkan device {candidate_name} creation failed: {}", ffmpeg_next::Error::from(result) - ), - )); - } - let initialized = unsafe { initialize(device) }; - match initialized { - Ok(owner) => Ok(Arc::new(owner)), - Err(error) => { - unsafe { ffi::av_buffer_unref(&mut device) }; - Err(error) + ); + eprintln!("{err_msg}"); + last_error = Some(Error::unavailable(Subsystem::Vulkan, err_msg)); + continue; + } + + let initialized = unsafe { initialize(device) }; + match initialized { + Ok(owner) => return Ok(Arc::new(owner)), + Err(error) => { + eprintln!("candidate Vulkan device {candidate_name} rejected: {error}"); + unsafe { + let context = &mut *((*device).data.cast::()); + let vulkan = &mut *context.hwctx.cast::(); + if !vulkan.act_dev.is_null() && !vulkan.inst.is_null() { + let inst = ash::Instance::load(entry.static_fn(), vk::Instance::from_raw(vulkan.inst as u64)); + let log_dev = ash::Device::load(inst.fp_v1_0(), vk::Device::from_raw(vulkan.act_dev as u64)); + let _ = log_dev.device_wait_idle(); + } + ffi::av_buffer_unref(&mut device); + } + last_error = Some(error); + continue; + } } } + + Err(last_error.unwrap_or_else(|| { + Error::unavailable( + Subsystem::Vulkan, + "shared Vulkan device creation failed: no suitable candidate device found", + ) + })) } unsafe fn initialize(device: *mut ffi::AVBufferRef) -> Result { @@ -840,4 +1019,73 @@ mod tests { assert!(!owner.codec_support(VideoCodec::H265, true)); assert!(!owner.supports(VideoCodec::H265, true, 1920, 1080)); } + + #[test] + #[cfg(feature = "vulkan")] + fn score_physical_device_prioritizes_capable_discrete_gpu() { + let intel_props = ash::vk::PhysicalDeviceProperties { + vendor_id: 0x8086, + device_type: ash::vk::PhysicalDeviceType::INTEGRATED_GPU, + ..Default::default() + }; + let intel_queues = vec![ash::vk::QueueFamilyProperties { + queue_flags: ash::vk::QueueFlags::GRAPHICS | ash::vk::QueueFlags::COMPUTE, + queue_count: 1, + ..Default::default() + }]; + + let nvidia_props = ash::vk::PhysicalDeviceProperties { + vendor_id: 0x10de, + device_type: ash::vk::PhysicalDeviceType::DISCRETE_GPU, + ..Default::default() + }; + let nvidia_queues = vec![ + ash::vk::QueueFamilyProperties { + queue_flags: ash::vk::QueueFlags::GRAPHICS + | ash::vk::QueueFlags::COMPUTE + | ash::vk::QueueFlags::TRANSFER, + queue_count: 8, + ..Default::default() + }, + ash::vk::QueueFamilyProperties { + queue_flags: ash::vk::QueueFlags::VIDEO_DECODE_KHR, + queue_count: 1, + ..Default::default() + }, + ]; + + let intel_score = score_physical_device(0, &intel_props, &intel_queues); + let nvidia_score = score_physical_device(1, &nvidia_props, &nvidia_queues); + assert!(nvidia_score > intel_score); + assert!(intel_score < 0); + assert!(nvidia_score > 0); + } + + #[test] + #[cfg(feature = "vulkan")] + fn score_physical_device_respects_user_overrides() { + let intel_props = ash::vk::PhysicalDeviceProperties { + vendor_id: 0x8086, + device_type: ash::vk::PhysicalDeviceType::INTEGRATED_GPU, + ..Default::default() + }; + let intel_queues = vec![ash::vk::QueueFamilyProperties { + queue_flags: ash::vk::QueueFlags::GRAPHICS | ash::vk::QueueFlags::COMPUTE, + queue_count: 2, + ..Default::default() + }]; + + // Test vendor override + unsafe { std::env::set_var("OPENNOW_VK_VENDOR_ID", "0x8086") }; + let score_with_vendor = score_physical_device(0, &intel_props, &intel_queues); + unsafe { std::env::remove_var("OPENNOW_VK_VENDOR_ID") }; + let score_without_vendor = score_physical_device(0, &intel_props, &intel_queues); + assert!(score_with_vendor >= score_without_vendor + 50_000); + + // Test index override + unsafe { std::env::set_var("OPENNOW_VK_DEVICE_INDEX", "0") }; + let score_with_index = score_physical_device(0, &intel_props, &intel_queues); + unsafe { std::env::remove_var("OPENNOW_VK_DEVICE_INDEX") }; + assert!(score_with_index >= score_without_vendor + 100_000); + } } diff --git a/opennow-qt/src/streaming/rendering/LinuxVulkanGraphics.cpp b/opennow-qt/src/streaming/rendering/LinuxVulkanGraphics.cpp index 69d162e9e..e00976c0e 100644 --- a/opennow-qt/src/streaming/rendering/LinuxVulkanGraphics.cpp +++ b/opennow-qt/src/streaming/rendering/LinuxVulkanGraphics.cpp @@ -19,6 +19,13 @@ Device::~Device() void Device::reset() { + if (m_info.device && m_instance.isValid()) { + auto pfnWaitIdle = reinterpret_cast( + m_instance.getInstanceProcAddr("vkDeviceWaitIdle")); + if (pfnWaitIdle) { + pfnWaitIdle(reinterpret_cast(m_info.device)); + } + } m_instance.destroy(); if (m_device) m_api.destroy(m_device); m_device = nullptr; @@ -93,6 +100,7 @@ bool Device::adopt(QQuickWindow *window) if (!m_instance.supportsPresent(reinterpret_cast(m_info.physical_device), m_info.graphics_queue_family_index, window)) { m_lastError = QStringLiteral("The native Vulkan graphics queue cannot present to this window system."); + window->setVulkanInstance(nullptr); return false; } window->setGraphicsDevice(QQuickGraphicsDevice::fromDeviceObjects( From 9d3fc438128ce0328db4e3e608497c20687c6d96 Mon Sep 17 00:00:00 2001 From: zortos293 <65777760+zortos293@users.noreply.github.com> Date: Sat, 19 Sep 2026 17:51:58 +0000 Subject: [PATCH 2/2] fix(test): isolate Vulkan device scoring preferences from process env Score computation now takes an explicit VulkanDevicePreferences value, with a thin from_env wrapper preserving the OPENNOW_VK_* / PRIME behavior. The unit tests exercise the pure function directly, so they no longer mutate process-wide env vars and cannot flake when cargo runs them in parallel. Also addresses review nits: sort_by_key, drop the redundant unsafe around the safe options builder, and fall back to default device selection if the index string ever fails CString conversion instead of unwrapping. --- .../src/presentation.rs | 2 +- .../src/vulkan_device.rs | 167 ++++++++++++------ 2 files changed, 117 insertions(+), 52 deletions(-) diff --git a/native/opennow-streamer/crates/opennow-streamer-platform-linux/src/presentation.rs b/native/opennow-streamer/crates/opennow-streamer-platform-linux/src/presentation.rs index e2b231b36..a936e6c99 100644 --- a/native/opennow-streamer/crates/opennow-streamer-platform-linux/src/presentation.rs +++ b/native/opennow-streamer/crates/opennow-streamer-platform-linux/src/presentation.rs @@ -2023,7 +2023,7 @@ fn select_device( (index, device, score) }) .collect(); - ranked.sort_by(|a, b| b.2.cmp(&a.2)); + ranked.sort_by_key(|candidate| std::cmp::Reverse(candidate.2)); for (_index, device, _score) in ranked { if !supports_swapchain(instance, device) diff --git a/native/opennow-streamer/crates/opennow-streamer-platform-linux/src/vulkan_device.rs b/native/opennow-streamer/crates/opennow-streamer-platform-linux/src/vulkan_device.rs index c257efbcc..bed08b36a 100644 --- a/native/opennow-streamer/crates/opennow-streamer-platform-linux/src/vulkan_device.rs +++ b/native/opennow-streamer/crates/opennow-streamer-platform-linux/src/vulkan_device.rs @@ -133,47 +133,82 @@ fn format_profile_index(codec: VideoCodec, pixel_format: PixelFormat) -> Option< Some(profile_index(codec, pixel_format.is_ten_bit()) + offset) } +#[cfg(feature = "vulkan")] +#[derive(Debug, Clone, Copy, Default)] +pub(crate) struct VulkanDevicePreferences { + device_index: Option, + vendor_id: Option, + nv_prime: bool, + dri_prime: bool, +} + +#[cfg(feature = "vulkan")] +impl VulkanDevicePreferences { + fn from_env() -> Self { + let device_index = std::env::var("OPENNOW_VK_DEVICE_INDEX") + .ok() + .and_then(|value| value.trim().parse::().ok()); + let vendor_id = std::env::var("OPENNOW_VK_VENDOR_ID") + .ok() + .and_then(|value| { + let trimmed = value.trim(); + if trimmed.starts_with("0x") || trimmed.starts_with("0X") { + u32::from_str_radix(&trimmed[2..], 16).ok() + } else { + trimmed.parse::().ok() + } + }); + let nv_prime = std::env::var("__NV_PRIME_RENDER_OFFLOAD") + .map(|v| v.trim() == "1") + .unwrap_or(false); + let dri_prime = std::env::var("DRI_PRIME") + .map(|v| v.trim() == "1") + .unwrap_or(false); + Self { + device_index, + vendor_id, + nv_prime, + dri_prime, + } + } +} + #[cfg(feature = "vulkan")] pub(crate) fn score_physical_device( index: usize, properties: &ash::vk::PhysicalDeviceProperties, queue_families: &[ash::vk::QueueFamilyProperties], +) -> i64 { + score_physical_device_with_preferences( + index, + properties, + queue_families, + &VulkanDevicePreferences::from_env(), + ) +} + +#[cfg(feature = "vulkan")] +pub(crate) fn score_physical_device_with_preferences( + index: usize, + properties: &ash::vk::PhysicalDeviceProperties, + queue_families: &[ash::vk::QueueFamilyProperties], + preferences: &VulkanDevicePreferences, ) -> i64 { let mut score = 0i64; // 1. Explicit user overrides (highest priority) - if let Ok(override_index) = std::env::var("OPENNOW_VK_DEVICE_INDEX") { - if let Ok(parsed) = override_index.trim().parse::() { - if parsed == index { - score += 100_000; - } - } + if preferences.device_index == Some(index) { + score += 100_000; } - - if let Ok(override_vendor) = std::env::var("OPENNOW_VK_VENDOR_ID") { - let trimmed = override_vendor.trim(); - let parsed = if trimmed.starts_with("0x") || trimmed.starts_with("0X") { - u32::from_str_radix(&trimmed[2..], 16).ok() - } else { - trimmed.parse::().ok() - }; - if parsed == Some(properties.vendor_id) { - score += 50_000; - } + if preferences.vendor_id == Some(properties.vendor_id) { + score += 50_000; } // 2. PRIME / offload environment variables - let nv_prime = std::env::var("__NV_PRIME_RENDER_OFFLOAD") - .map(|v| v.trim() == "1") - .unwrap_or(false); - if nv_prime && properties.vendor_id == 0x10de { + if preferences.nv_prime && properties.vendor_id == 0x10de { score += 20_000; } - - let dri_prime = std::env::var("DRI_PRIME") - .map(|v| v.trim() == "1") - .unwrap_or(false); - if dri_prime + if preferences.dri_prime && (properties.device_type == ash::vk::PhysicalDeviceType::DISCRETE_GPU || properties.vendor_id != 0x8086) { @@ -198,9 +233,10 @@ pub(crate) fn score_physical_device( return score; } - let has_video_decode = queue_families - .iter() - .any(|q| q.queue_flags.contains(ash::vk::QueueFlags::VIDEO_DECODE_KHR)); + let has_video_decode = queue_families.iter().any(|q| { + q.queue_flags + .contains(ash::vk::QueueFlags::VIDEO_DECODE_KHR) + }); if has_video_decode { score += 1_500; } @@ -221,9 +257,10 @@ pub(crate) fn score_physical_device( // Isolation check: // Either the graphics queue family has count > 1 (can split Qt queue from native), // or if count == 1, there must be separate compute and decode queue families. - let can_isolate = queue_families.iter().any(|q| { - q.queue_flags.contains(ash::vk::QueueFlags::GRAPHICS) && q.queue_count > 1 - }) || (has_compute && has_video_decode); + let can_isolate = queue_families + .iter() + .any(|q| q.queue_flags.contains(ash::vk::QueueFlags::GRAPHICS) && q.queue_count > 1) + || (has_compute && has_video_decode); if can_isolate { score += 2_000; @@ -309,7 +346,9 @@ mod implementation { if let Ok(devices) = unsafe { temp_instance.enumerate_physical_devices() } { for (index, &device) in devices.iter().enumerate() { let props = unsafe { temp_instance.get_physical_device_properties(device) }; - let queues = unsafe { temp_instance.get_physical_device_queue_family_properties(device) }; + let queues = unsafe { + temp_instance.get_physical_device_queue_family_properties(device) + }; let adapter = unsafe { CStr::from_ptr(props.device_name.as_ptr()) } .to_string_lossy() .into_owned(); @@ -319,24 +358,30 @@ mod implementation { } unsafe { temp_instance.destroy_instance(None) }; } - candidates.sort_by(|a, b| b.2.cmp(&a.2)); + candidates.sort_by_key(|candidate| std::cmp::Reverse(candidate.2)); let candidate_indices: Vec<(Option, String)> = if candidates.is_empty() { vec![(None, "default".to_string())] } else { candidates .into_iter() - .map(|(idx, name, score)| (Some(idx), format!("{name} (index {idx}, score {score})"))) + .map(|(idx, name, score)| { + (Some(idx), format!("{name} (index {idx}, score {score})")) + }) .collect() }; let mut last_error = None; for (candidate_idx, candidate_name) in candidate_indices { let mut device = ptr::null_mut(); - let mut options = unsafe { build_options(hdr_colorspace) }; + let mut options = build_options(hdr_colorspace); - let device_param = candidate_idx.map(|idx| std::ffi::CString::new(idx.to_string()).unwrap()); - let device_param_ptr = device_param.as_ref().map(|s| s.as_ptr()).unwrap_or(ptr::null()); + let device_param = + candidate_idx.and_then(|idx| std::ffi::CString::new(idx.to_string()).ok()); + let device_param_ptr = device_param + .as_ref() + .map(|s| s.as_ptr()) + .unwrap_or(ptr::null()); let result = unsafe { ffi::av_hwdevice_ctx_create( @@ -368,8 +413,14 @@ mod implementation { let context = &mut *((*device).data.cast::()); let vulkan = &mut *context.hwctx.cast::(); if !vulkan.act_dev.is_null() && !vulkan.inst.is_null() { - let inst = ash::Instance::load(entry.static_fn(), vk::Instance::from_raw(vulkan.inst as u64)); - let log_dev = ash::Device::load(inst.fp_v1_0(), vk::Device::from_raw(vulkan.act_dev as u64)); + let inst = ash::Instance::load( + entry.static_fn(), + vk::Instance::from_raw(vulkan.inst as u64), + ); + let log_dev = ash::Device::load( + inst.fp_v1_0(), + vk::Device::from_raw(vulkan.act_dev as u64), + ); let _ = log_dev.device_wait_idle(); } ffi::av_buffer_unref(&mut device); @@ -1054,8 +1105,11 @@ mod tests { }, ]; - let intel_score = score_physical_device(0, &intel_props, &intel_queues); - let nvidia_score = score_physical_device(1, &nvidia_props, &nvidia_queues); + let preferences = VulkanDevicePreferences::default(); + let intel_score = + score_physical_device_with_preferences(0, &intel_props, &intel_queues, &preferences); + let nvidia_score = + score_physical_device_with_preferences(1, &nvidia_props, &nvidia_queues, &preferences); assert!(nvidia_score > intel_score); assert!(intel_score < 0); assert!(nvidia_score > 0); @@ -1075,17 +1129,28 @@ mod tests { ..Default::default() }]; - // Test vendor override - unsafe { std::env::set_var("OPENNOW_VK_VENDOR_ID", "0x8086") }; - let score_with_vendor = score_physical_device(0, &intel_props, &intel_queues); - unsafe { std::env::remove_var("OPENNOW_VK_VENDOR_ID") }; - let score_without_vendor = score_physical_device(0, &intel_props, &intel_queues); + let baseline = VulkanDevicePreferences::default(); + let score_without_vendor = + score_physical_device_with_preferences(0, &intel_props, &intel_queues, &baseline); + + let vendor_override = VulkanDevicePreferences { + vendor_id: Some(0x8086), + ..Default::default() + }; + let score_with_vendor = score_physical_device_with_preferences( + 0, + &intel_props, + &intel_queues, + &vendor_override, + ); assert!(score_with_vendor >= score_without_vendor + 50_000); - // Test index override - unsafe { std::env::set_var("OPENNOW_VK_DEVICE_INDEX", "0") }; - let score_with_index = score_physical_device(0, &intel_props, &intel_queues); - unsafe { std::env::remove_var("OPENNOW_VK_DEVICE_INDEX") }; + let index_override = VulkanDevicePreferences { + device_index: Some(0), + ..Default::default() + }; + let score_with_index = + score_physical_device_with_preferences(0, &intel_props, &intel_queues, &index_override); assert!(score_with_index >= score_without_vendor + 100_000); } }