diff --git a/src/infra/logging/tests.rs b/src/infra/logging/tests.rs index f14e2238..a361134b 100644 --- a/src/infra/logging/tests.rs +++ b/src/infra/logging/tests.rs @@ -130,6 +130,7 @@ mod tests { vkd3d_enabled: false, graphics_backend_policy: crate::models::GraphicsBackendPolicy::WineD3D, d3d12_policy: crate::models::D3D12ProviderPolicy::Auto, + gpu_selection: crate::models::GpuSelection::Auto, }; let warnings = check_environment_sanity(&env, "Wine", Some(&config)); diff --git a/src/infra/runners/wine_tkg.rs b/src/infra/runners/wine_tkg.rs index 3fdc1e92..63781530 100644 --- a/src/infra/runners/wine_tkg.rs +++ b/src/infra/runners/wine_tkg.rs @@ -308,30 +308,38 @@ impl Runner for WineTkgRunner { .unwrap_or("wine") }; - // Resolve graphics backend policy + // Resolve graphics backend policy using authoritative pipeline resolutions match glc.graphics_backend_policy { crate::models::GraphicsBackendPolicy::Auto => { - let components = crate::utils::detect_runner_components( - &crate::utils::resolve_runner(proton, &library_root), - Some(&game_wineprefix), - ); - - if components.dxvk.is_some() { + let has_dxvk = ctx.dll_resolutions.iter().any(|r| { + matches!(r.name.as_str(), "d3d11" | "d3d9" | "dxgi") && + matches!(r.chosen_provider, crate::launch::dll_provider_resolver::DllProvider::Runner | crate::launch::dll_provider_resolver::DllProvider::GameLocal) + }); + if has_dxvk { glc.dxvk_enabled = true; } - // Respect d3d12_policy when auto-detecting VKD3D variant + match glc.d3d12_policy { crate::models::D3D12ProviderPolicy::Vkd3dWine => { - if components.vkd3d.is_some() { + let has_vkd3d_wine = ctx.dll_resolutions.iter().any(|r| { + r.name == "d3d12" && + matches!(r.chosen_provider, crate::launch::dll_provider_resolver::DllProvider::Runner) && + r.chosen_path.as_ref().map(|p| !p.to_string_lossy().contains("vkd3d-proton")).unwrap_or(false) + }); + if has_vkd3d_wine { glc.vkd3d_enabled = true; } } _ => { - // Auto or Vkd3dProton: prefer proton, fall back to wine VKD3D - if components.vkd3d_proton.is_some() { - glc.vkd3d_proton_enabled = true; - } else if components.vkd3d.is_some() { - glc.vkd3d_enabled = true; + let d3d12_res = ctx.dll_resolutions.iter().find(|r| r.name == "d3d12"); + if let Some(res) = d3d12_res { + if matches!(res.chosen_provider, crate::launch::dll_provider_resolver::DllProvider::Runner | crate::launch::dll_provider_resolver::DllProvider::GameLocal) { + if res.chosen_path.as_ref().map(|p| p.to_string_lossy().contains("vkd3d-proton")).unwrap_or(false) { + glc.vkd3d_proton_enabled = true; + } else { + glc.vkd3d_enabled = true; + } + } } } } @@ -340,20 +348,17 @@ impl Runner for WineTkgRunner { glc.dxvk_enabled = false; glc.vkd3d_proton_enabled = false; glc.vkd3d_enabled = false; - // force_builtin_d3d handles the rest below } crate::models::GraphicsBackendPolicy::DXVK => { glc.dxvk_enabled = true; } crate::models::GraphicsBackendPolicy::VKD3D => { - // Consult d3d12_policy to select the right implementation match glc.d3d12_policy { crate::models::D3D12ProviderPolicy::Vkd3dWine => { glc.vkd3d_enabled = true; glc.vkd3d_proton_enabled = false; } _ => { - // Auto or Vkd3dProton glc.vkd3d_proton_enabled = true; } } @@ -453,8 +458,11 @@ impl Runner for WineTkgRunner { env.insert("XDG_RUNTIME_DIR".to_string(), xdg_runtime); } - // Auto-inject PRIME/Optimus GPU selection (user env vars can still override below) - for (k, v) in crate::utils::detect_prime_env() { + // Auto-inject GPU selection (user env vars can still override below) + let gpu_selection = ctx.user_config.as_ref() + .map(|c| c.graphics_layers.gpu_selection.clone()) + .unwrap_or_default(); + for (k, v) in crate::utils::get_gpu_selection_env(&gpu_selection) { env.entry(k).or_insert(v); } diff --git a/src/launch/dll_provider_resolver.rs b/src/launch/dll_provider_resolver.rs index ac060ce7..969701ff 100644 --- a/src/launch/dll_provider_resolver.rs +++ b/src/launch/dll_provider_resolver.rs @@ -92,15 +92,20 @@ impl DllProviderResolver { let subdirs = [ "files/lib/wine/dxvk", "files/lib/wine/vkd3d", + "files/lib/wine/vkd3d-proton", "files/lib64/wine/dxvk", "files/lib64/wine/vkd3d", + "files/lib64/wine/vkd3d-proton", "files/lib/vkd3d", "dist/lib/wine/dxvk", "dist/lib/wine/vkd3d", + "dist/lib/wine/vkd3d-proton", "lib/wine/dxvk", "lib/wine/vkd3d", + "lib/wine/vkd3d-proton", "lib64/wine/dxvk", "lib64/wine/vkd3d", + "lib64/wine/vkd3d-proton", ]; for s in subdirs { roots.push(runner_root.join(s)); } report.scan_roots = roots; @@ -391,10 +396,12 @@ mod tests { components.vkd3d_proton = Some(crate::utils::ComponentInfo { version: "2.10".into(), source: crate::utils::ComponentSource::BundledWithRunner, + path: None, }); components.vkd3d = Some(crate::utils::ComponentInfo { version: "1.8".into(), source: crate::utils::ComponentSource::BundledWithRunner, + path: None, }); let resolver = DllProviderResolver::new(); diff --git a/src/launch/pipeline.rs b/src/launch/pipeline.rs index 9faa8963..56015413 100644 --- a/src/launch/pipeline.rs +++ b/src/launch/pipeline.rs @@ -27,6 +27,7 @@ pub struct GraphicsStackInfo { pub graphics_stack_evidence: Vec, // e.g. ["DXVK: v2.3.1"] pub graphics_stack_confidence: String, // "low" | "medium" | "high" pub override_policy: String, // e.g. "Native-only" + pub gpu_selection: String, // e.g. "Manual: AMD (0x1002 0x73ff)" pub dll_providers: HashMap, // e.g. {"d3d11": "Runner", "d3d9": "GameLocal"} } @@ -431,6 +432,11 @@ impl LaunchPipeline { ctx.graphics_stack.graphics_stack_expected = format!("{} [Policy: {}]", expected.join(", "), policy); ctx.graphics_stack.override_policy = "Native-preferred".to_string(); } + + ctx.graphics_stack.gpu_selection = match &config.graphics_layers.gpu_selection { + crate::models::GpuSelection::Auto => "Auto".to_string(), + crate::models::GpuSelection::Manual { index, name } => format!("Manual: {} (#{})", name, index), + }; } } diff --git a/src/models.rs b/src/models.rs index 3fee080c..9056b897 100644 --- a/src/models.rs +++ b/src/models.rs @@ -61,6 +61,16 @@ pub enum D3D12ProviderPolicy { Vkd3dWine, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub enum GpuSelection { + #[default] + Auto, + Manual { + index: usize, + name: String, + }, +} + #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct GraphicsLayerConfig { #[serde(default)] @@ -73,6 +83,8 @@ pub struct GraphicsLayerConfig { pub graphics_backend_policy: GraphicsBackendPolicy, #[serde(default)] pub d3d12_policy: D3D12ProviderPolicy, + #[serde(default)] + pub gpu_selection: GpuSelection, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] diff --git a/src/ui.rs b/src/ui.rs index ed2df8a9..07c80828 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1208,31 +1208,16 @@ impl SteamLauncher { ui.add_space(8.0); ui.separator(); - ui.heading("Graphics Layers (per-prefix)"); + ui.heading("Graphics Compatibility"); ui.label( egui::RichText::new( - "Copies DLLs into this game's WINEPREFIX. Required for DX12 on wine-tkg.", + "Configures the translation stack and GPU for this game.", ) .weak() .italics(), ); ui.add_space(4.0); - let user_cfg_for_prefix = self.user_configs.get(&game.app_id).cloned().unwrap_or_default(); - let game_prefix = if user_cfg_for_prefix.use_steam_runtime { - // Shared mode → master prefix; PerGame → game's own prefix - crate::utils::steam_wineprefix_for_game( - &self.launcher_config, - game.app_id, - &self.user_configs, - ) - } else { - std::path::PathBuf::from(&self.launcher_config.steam_library_path) - .join("steamapps/compatdata") - .join(game.app_id.to_string()) - .join("pfx") - }; - let mut user_cfg_gl = self.user_configs.get(&game.app_id).cloned().unwrap_or_default(); let glc = &mut user_cfg_gl.graphics_layers; let mut gl_changed = false; @@ -1278,250 +1263,58 @@ impl SteamLauncher { }); } - ui.add_space(8.0); - - // Detect what's already in the prefix for status display - let dxvk_in_prefix = game_prefix.join("drive_c/windows/system32/d3d11.dll").exists(); - let d3d12_dll = game_prefix.join("drive_c/windows/system32/d3d12.dll"); - let mut vkd3dp_in_prefix = false; - let mut vkd3dw_in_prefix = false; - - if d3d12_dll.exists() { - // This is a bit slow to do every frame, but fine for small DLLs - // In a real app we'd cache this. - let is_proton = crate::utils::detect_runner_components(&PathBuf::new(), Some(&game_prefix)).vkd3d_proton.is_some(); - if is_proton { - vkd3dp_in_prefix = true; - } else { - vkd3dw_in_prefix = true; - } - } - - egui::Grid::new("graphics_layer_grid") - .num_columns(4) - .spacing([8.0, 6.0]) - .show(ui, |ui| { - // ── DXVK ────────────────────────────────────────────── - ui.label("DXVK (DX8-11):"); - let dxvk_avail = - crate::utils::find_layer_source(&crate::utils::GraphicsLayer::Dxvk).is_some(); - - if dxvk_in_prefix { - ui.colored_label(egui::Color32::GREEN, "installed"); - } else { - ui.colored_label(egui::Color32::GRAY, "not installed"); - } - - if ui - .add_enabled( - dxvk_avail && game_prefix.exists(), - egui::Button::new(if dxvk_in_prefix { "Reinstall" } else { "Install" }), - ) - .clicked() - { - match crate::utils::install_layer_into_prefix( - &crate::utils::GraphicsLayer::Dxvk, - &game_prefix, - ) { - Ok(dlls) => { - self.status = format!("DXVK installed: {}", dlls.join(", ")); - glc.dxvk_enabled = true; - gl_changed = true; - } - Err(e) => self.status = format!("DXVK install failed: {e}"), - } - } - - if ui.add_enabled(dxvk_in_prefix, egui::Button::new("Remove")).clicked() { - match crate::utils::remove_layer_from_prefix( - &crate::utils::GraphicsLayer::Dxvk, - &game_prefix, - ) { - Ok(()) => { - self.status = "DXVK removed".to_string(); - glc.dxvk_enabled = false; - gl_changed = true; - } - Err(e) => self.status = format!("DXVK remove failed: {e}"), - } - } - - if !dxvk_avail { - ui.colored_label( - egui::Color32::YELLOW, - "(not found system-wide — install via package manager)", - ); - } else { - ui.label(""); - } - ui.end_row(); - - // Override toggle (independent of install status) - ui.label(""); - if ui - .checkbox(&mut glc.dxvk_enabled, "Manual DXVK Overrides (advanced)") - .on_hover_text( - "Forces d3d9=n,b d3d11=n,b dxgi=n,b regardless of policy choice. Requires DLLs to be installed above.", - ) - .changed() - { - gl_changed = true; - } - ui.label(""); - ui.label(""); - ui.label(""); - ui.end_row(); - - ui.add_space(4.0); - ui.end_row(); - - // ── VKD3D-Proton ─────────────────────────────────────── - ui.label("VKD3D-Proton (DX12):"); - let vkd3dp_avail = - crate::utils::find_layer_source(&crate::utils::GraphicsLayer::Vkd3dProton) - .is_some(); - - if vkd3dp_in_prefix { - ui.colored_label(egui::Color32::GREEN, "installed"); - } else { - ui.colored_label(egui::Color32::GRAY, "not installed"); - } - - if ui - .add_enabled( - vkd3dp_avail && game_prefix.exists(), - egui::Button::new(if vkd3dp_in_prefix { "Reinstall" } else { "Install" }), - ) - .clicked() - { - match crate::utils::install_layer_into_prefix( - &crate::utils::GraphicsLayer::Vkd3dProton, - &game_prefix, - ) { - Ok(dlls) => { - self.status = format!("VKD3D-Proton installed: {}", dlls.join(", ")); - glc.vkd3d_proton_enabled = true; - gl_changed = true; - } - Err(e) => self.status = format!("VKD3D-Proton install failed: {e}"), - } - } - - if ui.add_enabled(vkd3dp_in_prefix, egui::Button::new("Remove")).clicked() { - match crate::utils::remove_layer_from_prefix( - &crate::utils::GraphicsLayer::Vkd3dProton, - &game_prefix, - ) { - Ok(()) => { - self.status = "VKD3D-Proton removed".to_string(); - glc.vkd3d_proton_enabled = false; - gl_changed = true; - } - Err(e) => self.status = format!("VKD3D-Proton remove failed: {e}"), - } - } - - if !vkd3dp_avail { - ui.colored_label( - egui::Color32::YELLOW, - "(not found — install vkd3d-proton via package manager)", - ); - } else { - ui.label(""); - } - ui.end_row(); - - ui.label(""); - if ui - .checkbox( - &mut glc.vkd3d_proton_enabled, - "Manual VKD3D-Proton Overrides", - ) - .on_hover_text("Forces d3d12=n,b regardless of policy choice. Required for DX12 games on wine-tkg.") - .changed() - { - gl_changed = true; - } - ui.label(""); - ui.label(""); - ui.label(""); - ui.end_row(); - - ui.add_space(4.0); - ui.end_row(); - - // ── VKD3D (Wine) ─────────────────────────────────────── - ui.label("VKD3D (Wine):"); - let vkd3dw_avail = - crate::utils::find_layer_source(&crate::utils::GraphicsLayer::Vkd3d) - .is_some(); - - if vkd3dw_in_prefix { - ui.colored_label(egui::Color32::GREEN, "installed"); - } else { - ui.colored_label(egui::Color32::GRAY, "not installed"); - } + ui.horizontal(|ui| { + ui.label("Preferred GPU:"); + let gpus = crate::utils::list_available_gpus(); + let current_gpu_text = match &glc.gpu_selection { + crate::models::GpuSelection::Auto => "Auto (System Default)".to_string(), + crate::models::GpuSelection::Manual { name, .. } => name.clone(), + }; - if ui - .add_enabled( - vkd3dw_avail && game_prefix.exists(), - egui::Button::new(if vkd3dw_in_prefix { "Reinstall" } else { "Install" }), - ) - .clicked() - { - match crate::utils::install_layer_into_prefix( - &crate::utils::GraphicsLayer::Vkd3d, - &game_prefix, - ) { - Ok(dlls) => { - self.status = format!("VKD3D (Wine) installed: {}", dlls.join(", ")); - glc.vkd3d_enabled = true; - gl_changed = true; - } - Err(e) => self.status = format!("VKD3D (Wine) install failed: {e}"), + egui::ComboBox::from_id_salt("gpu_selection_selector") + .selected_text(current_gpu_text) + .show_ui(ui, |ui| { + if ui.selectable_value(&mut glc.gpu_selection, crate::models::GpuSelection::Auto, "Auto (System Default)").clicked() { + gl_changed = true; } - } - - if ui.add_enabled(vkd3dw_in_prefix, egui::Button::new("Remove")).clicked() { - match crate::utils::remove_layer_from_prefix( - &crate::utils::GraphicsLayer::Vkd3d, - &game_prefix, - ) { - Ok(()) => { - self.status = "VKD3D (Wine) removed".to_string(); - glc.vkd3d_enabled = false; + for (index, name) in gpus { + if ui.selectable_value( + &mut glc.gpu_selection, + crate::models::GpuSelection::Manual { index, name: name.clone() }, + name + ).clicked() { gl_changed = true; } - Err(e) => self.status = format!("VKD3D (Wine) remove failed: {e}"), } - } + }); + }); - if !vkd3dw_avail { - ui.colored_label( - egui::Color32::YELLOW, - "(not found — install libvkd3d via package manager)", - ); - } else { - ui.label(""); - } - ui.end_row(); + ui.add_space(8.0); + ui.label(egui::RichText::new("Manual Overrides (advanced)").strong()); - ui.label(""); - if ui - .checkbox( - &mut glc.vkd3d_enabled, - "Manual VKD3D (Wine) Overrides", - ) - .on_hover_text("Forces d3d12=n,b regardless of policy choice.") - .changed() - { - gl_changed = true; - } - ui.label(""); - ui.label(""); - ui.label(""); - ui.end_row(); - }); + ui.horizontal(|ui| { + if ui + .checkbox(&mut glc.dxvk_enabled, "DXVK") + .on_hover_text("Forces d3d9=n,b d3d11=n,b dxgi=n,b regardless of policy choice.") + .changed() + { + gl_changed = true; + } + if ui + .checkbox(&mut glc.vkd3d_proton_enabled, "VKD3D-Proton") + .on_hover_text("Forces d3d12=n,b regardless of policy choice.") + .changed() + { + gl_changed = true; + } + if ui + .checkbox(&mut glc.vkd3d_enabled, "VKD3D (Wine)") + .on_hover_text("Forces d3d12=n,b regardless of policy choice.") + .changed() + { + gl_changed = true; + } + }); if gl_changed { self.user_configs.insert(game.app_id, user_cfg_gl); @@ -2504,10 +2297,13 @@ impl eframe::App for SteamLauncher { } else { format!("{}", c.source) }; - ui.colored_label( + let label = ui.colored_label( egui::Color32::GRAY, format!("({})", source_text), ); + if let Some(path) = &c.path { + label.on_hover_text(path.to_string_lossy()); + } } None => { ui.colored_label(egui::Color32::GRAY, "not found"); diff --git a/src/utils.rs b/src/utils.rs index 6dcbf57d..58798c3a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -124,6 +124,7 @@ pub struct RunnerComponents { pub struct ComponentInfo { pub version: String, pub source: ComponentSource, + pub path: Option, } #[derive(Debug, Clone, PartialEq)] @@ -178,6 +179,72 @@ pub fn detect_runner_components( } } +/// Discovers available GPUs on the system via DRI devices. +pub fn list_available_gpus() -> Vec<(usize, String)> { + let mut gpus = Vec::new(); + if let Ok(entries) = std::fs::read_dir("/dev/dri") { + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + if name.starts_with("renderD") { + if let Ok(index) = name.trim_start_matches("renderD").parse::() { + let device_name = get_gpu_name_from_sysfs(index).unwrap_or_else(|| format!("GPU #{}", index - 128)); + gpus.push((index, device_name)); + } + } + } + } + gpus.sort_by_key(|g| g.0); + gpus +} + +fn get_gpu_name_from_sysfs(index: usize) -> Option { + let base = format!("/sys/class/drm/renderD{}", index); + let vendor_path = format!("{}/device/vendor", base); + let device_path = format!("{}/device/device", base); + + let vendor = std::fs::read_to_string(vendor_path).ok()?.trim().to_string(); + let device = std::fs::read_to_string(device_path).ok()?.trim().to_string(); + + let vendor_name = match vendor.as_str() { + "0x10de" => "NVIDIA", + "0x1002" => "AMD", + "0x8086" => "Intel", + _ => "Unknown", + }; + + Some(format!("{} ({} {})", vendor_name, vendor, device)) +} + +pub fn get_gpu_selection_env(selection: &crate::models::GpuSelection) -> std::collections::HashMap { + let mut env = std::collections::HashMap::new(); + + match selection { + crate::models::GpuSelection::Auto => { + // Fallback to legacy prime detection + for (k, v) in detect_prime_env() { + env.insert(k, v); + } + } + crate::models::GpuSelection::Manual { index, name } => { + let dri_index = index.saturating_sub(128); + env.insert("DRI_PRIME".to_string(), dri_index.to_string()); + + if name.to_uppercase().contains("NVIDIA") { + env.insert("__NV_PRIME_RENDER_OFFLOAD".to_string(), "1".to_string()); + env.insert("__NV_PRIME_RENDER_OFFLOAD_PROVIDER".to_string(), "NVIDIA-G0".to_string()); + env.insert("__GLX_VENDOR_LIBRARY_NAME".to_string(), "nvidia".to_string()); + env.insert("__VK_LAYER_NV_optimus".to_string(), "NVIDIA_only".to_string()); + } + + // MESA_VK_DEVICE_SELECT for Vulkan selection on Mesa drivers + // Format: vendor_id:device_id or just an index + env.insert("MESA_VK_DEVICE_SELECT".to_string(), dri_index.to_string()); + } + } + + env +} + /// Detects NVIDIA Optimus / hybrid graphics and returns the env vars needed /// to force the discrete NVIDIA GPU. Returns empty map on non-hybrid systems. pub fn detect_prime_env() -> std::collections::HashMap { @@ -265,6 +332,7 @@ fn detect_vkd3d_proton(root: &Path, prefix: Option<&Path>) -> Option) -> Option) -> Option { return Some(ComponentInfo { version, source: ComponentSource::InstalledInPrefix, + path: Some(p), }); } } @@ -379,12 +449,14 @@ fn detect_vkd3d(root: &Path, prefix: Option<&Path>) -> Option { // ── Shared helpers ──────────────────────────────────────────────────────────── fn check_bundled(root: &Path, dll_candidates: &[&str], version_files: &[&str]) -> Option { - let found_dll = dll_candidates.iter().find(|rel| root.join(rel).exists()); - if let Some(rel) = found_dll { - tracing::debug!("Found bundled component DLL at: {}", root.join(rel).display()); + let found_rel = dll_candidates.iter().find(|rel| root.join(rel).exists()); + let dll_path = if let Some(rel) = found_rel { + let p = root.join(rel); + tracing::debug!("Found bundled component DLL at: {}", p.display()); + Some(p) } else { return None; - } + }; let version = version_files .iter() @@ -411,6 +483,7 @@ fn check_bundled(root: &Path, dll_candidates: &[&str], version_files: &[&str]) - Some(ComponentInfo { version, source: ComponentSource::BundledWithRunner, + path: dll_path, }) } @@ -428,6 +501,7 @@ fn check_prefix(prefix: &Path, dll_candidates: &[&str], _name: &str) -> Option Option { return Some(ComponentInfo { version, source: ComponentSource::SystemWide, + path: Some(p.to_path_buf()), }); } }