fix: url opening, dgpu dropdown instead of switcher, invisible icons (#941)
This commit is contained in:
parent
abf708fdb4
commit
e7ddd85960
15 changed files with 277 additions and 547 deletions
7
.changeset/choose_which_gpu_the_game_renders_on.md
Normal file
7
.changeset/choose_which_gpu_the_game_renders_on.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
default: minor
|
||||||
|
---
|
||||||
|
|
||||||
|
# Choose which GPU the game renders on
|
||||||
|
|
||||||
|
The discrete GPU toggle is now a picker. Linux lists each graphics card by name, Windows offers integrated or dedicated.
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
default: patch
|
||||||
|
---
|
||||||
|
|
||||||
|
# Icons show on Linux systems with a comma decimal locale
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
default: patch
|
||||||
|
---
|
||||||
|
|
||||||
|
# Open folder and open in browser work again on Linux
|
||||||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -4928,6 +4928,7 @@ dependencies = [
|
||||||
"freya-skia-safe",
|
"freya-skia-safe",
|
||||||
"freya-testing",
|
"freya-testing",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"gtk-sys",
|
||||||
"image",
|
"image",
|
||||||
"libloading 0.8.9",
|
"libloading 0.8.9",
|
||||||
"mimalloc",
|
"mimalloc",
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ mimalloc = "=0.1.52"
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
libloading = "=0.8.9"
|
libloading = "=0.8.9"
|
||||||
|
gtk-sys = "=0.18.2"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows = { version = "=0.62.2", features = [
|
windows = { version = "=0.62.2", features = [
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,11 @@ impl App for OneClientApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
unsafe {
|
||||||
|
gtk_sys::gtk_disable_setlocale();
|
||||||
|
}
|
||||||
|
|
||||||
let cli = cli::parse();
|
let cli = cli::parse();
|
||||||
|
|
||||||
let mut builder = Builder::new_multi_thread();
|
let mut builder = Builder::new_multi_thread();
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,28 @@ fn has_allowed_scheme(url: &str) -> bool {
|
||||||
.any(|allowed| scheme.eq_ignore_ascii_case(allowed))
|
.any(|allowed| scheme.eq_ignore_ascii_case(allowed))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn open_target(target: &str) {
|
||||||
|
use std::process::Stdio;
|
||||||
|
|
||||||
|
let spawned = open::commands(target).into_iter().find_map(|mut command| {
|
||||||
|
command
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.spawn()
|
||||||
|
.ok()
|
||||||
|
});
|
||||||
|
|
||||||
|
match spawned {
|
||||||
|
Some(mut child) => {
|
||||||
|
std::thread::spawn(move || child.wait());
|
||||||
|
}
|
||||||
|
None => tracing::warn!("failed to open {target}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
fn open_target(target: &str) {
|
fn open_target(target: &str) {
|
||||||
if let Err(err) = open::that_detached(target) {
|
if let Err(err) = open::that_detached(target) {
|
||||||
tracing::warn!("failed to open {target}: {err}");
|
tracing::warn!("failed to open {target}: {err}");
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
use freya::prelude::*;
|
use freya::prelude::*;
|
||||||
use oneclient_common::Patch;
|
use oneclient_common::Patch;
|
||||||
#[cfg(any(target_os = "linux", windows))]
|
use oneclient_core::settings::{
|
||||||
use oneclient_core::settings::SettingsOsExtra;
|
GameSettingsProfile, PackageUpdateMode, ProfileUpdate, Resolution, SettingsOsExtra,
|
||||||
use oneclient_core::settings::{GameSettingsProfile, PackageUpdateMode, ProfileUpdate, Resolution};
|
};
|
||||||
|
|
||||||
use super::settings_page;
|
use super::settings_page;
|
||||||
use crate::components::{
|
use crate::components::{
|
||||||
|
|
@ -65,11 +65,11 @@ impl Component for SettingsMinecraft {
|
||||||
move || v
|
move || v
|
||||||
});
|
});
|
||||||
|
|
||||||
#[cfg(any(target_os = "linux", windows))]
|
let gpu = use_state({
|
||||||
let discrete_gpu = use_state({
|
let v = profile.gpu().map(str::to_owned);
|
||||||
let v = profile.use_discrete_gpu();
|
|
||||||
move || v
|
move || v
|
||||||
});
|
});
|
||||||
|
let gpus = use_state(oneclient_core::game::gpu::detect);
|
||||||
|
|
||||||
let mut first = use_state(|| true);
|
let mut first = use_state(|| true);
|
||||||
use_side_effect(move || {
|
use_side_effect(move || {
|
||||||
|
|
@ -84,22 +84,17 @@ impl Component for SettingsMinecraft {
|
||||||
&post_exit_command.read(),
|
&post_exit_command.read(),
|
||||||
*update_mode.read(),
|
*update_mode.read(),
|
||||||
);
|
);
|
||||||
#[cfg(any(target_os = "linux", windows))]
|
let selected_gpu = gpu.read().clone();
|
||||||
let gpu = *discrete_gpu.read();
|
|
||||||
if *first.peek() {
|
if *first.peek() {
|
||||||
first.set(false);
|
first.set(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(target_os = "linux", windows))]
|
let update = with_gpu(update, selected_gpu);
|
||||||
let update = with_discrete_gpu(update, gpu);
|
|
||||||
|
|
||||||
dispatch.update_global_profile(update);
|
dispatch.update_global_profile(update);
|
||||||
});
|
});
|
||||||
|
|
||||||
#[cfg(any(target_os = "linux", windows))]
|
|
||||||
let discrete_gpu_default = SettingsOsExtra::default().use_discrete_gpu.unwrap_or(true);
|
|
||||||
|
|
||||||
let page = settings_page()
|
let page = settings_page()
|
||||||
.child(section_header("GAME"))
|
.child(section_header("GAME"))
|
||||||
.child(settings_row(
|
.child(settings_row(
|
||||||
|
|
@ -211,34 +206,56 @@ impl Component for SettingsMinecraft {
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|
||||||
#[cfg(any(target_os = "linux", windows))]
|
let page = if gpus.read().is_empty() {
|
||||||
let page = page
|
page
|
||||||
.child(section_header("GRAPHICS"))
|
} else {
|
||||||
.child(settings_row(
|
page.child(section_header("GRAPHICS")).child(settings_row(
|
||||||
IconType::Rocket02,
|
IconType::Rocket02,
|
||||||
"Use Discrete GPU",
|
"GPU",
|
||||||
"Render the game on the dedicated graphics card. Does nothing on a machine with only one GPU, and draws noticeably more power on a laptop.",
|
"The graphics card the game renders on. System default leaves the choice to your drivers.",
|
||||||
resettable(toggle(discrete_gpu), discrete_gpu, discrete_gpu_default),
|
resettable(gpu_field(gpu, &gpus.read()), gpu, None),
|
||||||
));
|
))
|
||||||
|
};
|
||||||
|
|
||||||
page.into_element()
|
page.into_element()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(target_os = "linux", windows))]
|
fn with_gpu(mut update: ProfileUpdate, gpu: Option<String>) -> ProfileUpdate {
|
||||||
fn with_discrete_gpu(mut update: ProfileUpdate, on: bool) -> ProfileUpdate {
|
|
||||||
let base = crate::launcher::state()
|
let base = crate::launcher::state()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|state| state.settings.read().global_game_settings.os_extra.clone())
|
.and_then(|state| state.settings.read().global_game_settings.os_extra.clone())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
update.os_extra = Patch::Set(SettingsOsExtra {
|
update.os_extra = Patch::Set(SettingsOsExtra { gpu, ..base });
|
||||||
use_discrete_gpu: Some(on),
|
|
||||||
..base
|
|
||||||
});
|
|
||||||
update
|
update
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn gpu_field(
|
||||||
|
mut selected: State<Option<String>>,
|
||||||
|
gpus: &[oneclient_core::game::gpu::Gpu],
|
||||||
|
) -> impl IntoElement {
|
||||||
|
const SYSTEM_DEFAULT: &str = "System default";
|
||||||
|
|
||||||
|
let current = selected
|
||||||
|
.read()
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|id| gpus.iter().find(|gpu| gpu.id == id))
|
||||||
|
.map_or(SYSTEM_DEFAULT.to_string(), |gpu| gpu.name.clone());
|
||||||
|
|
||||||
|
let options = std::iter::once(SYSTEM_DEFAULT.to_string())
|
||||||
|
.chain(gpus.iter().map(|gpu| gpu.name.clone()))
|
||||||
|
.collect();
|
||||||
|
let ids: Vec<String> = gpus.iter().map(|gpu| gpu.id.clone()).collect();
|
||||||
|
|
||||||
|
Dropdown::new(current, options)
|
||||||
|
.width(Size::px(220.))
|
||||||
|
.height(Size::px(34.))
|
||||||
|
.on_select(move |idx: usize| {
|
||||||
|
selected.set(idx.checked_sub(1).and_then(|i| ids.get(i).cloned()));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn build_update(
|
fn build_update(
|
||||||
fullscreen: bool,
|
fullscreen: bool,
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,8 @@ pub use oneclient_common::domain::PackageUpdateMode;
|
||||||
pub struct SettingsOsExtra {
|
pub struct SettingsOsExtra {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub enable_gamemode: Option<bool>,
|
pub enable_gamemode: Option<bool>,
|
||||||
#[cfg(any(target_os = "linux", windows))]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub use_discrete_gpu: Option<bool>,
|
pub gpu: Option<String>,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub unknown: serde_json::Map<String, serde_json::Value>,
|
pub unknown: serde_json::Map<String, serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
@ -42,8 +42,7 @@ impl Default for SettingsOsExtra {
|
||||||
Self {
|
Self {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
enable_gamemode: Some(true),
|
enable_gamemode: Some(true),
|
||||||
#[cfg(any(target_os = "linux", windows))]
|
gpu: None,
|
||||||
use_discrete_gpu: Some(true),
|
|
||||||
unknown: serde_json::Map::new(),
|
unknown: serde_json::Map::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -67,15 +66,8 @@ impl GameSettingsProfile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn use_discrete_gpu(&self) -> bool {
|
pub fn gpu(&self) -> Option<&str> {
|
||||||
cfg_select! {
|
self.os_extra.as_ref()?.gpu.as_deref()
|
||||||
any(target_os = "linux", target_os = "windows") => self
|
|
||||||
.os_extra
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|extra| extra.use_discrete_gpu)
|
|
||||||
.unwrap_or(true),
|
|
||||||
_ => false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_global(&self) -> bool {
|
pub fn is_global(&self) -> bool {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::Path;
|
||||||
|
|
||||||
const INTEL_INTEGRATED: &str = "0000:00:02.0";
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum Driver {
|
pub enum Driver {
|
||||||
|
|
@ -41,53 +39,25 @@ impl Driver {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Gpu {
|
pub struct Card {
|
||||||
pub pci_address: String,
|
pub pci_address: String,
|
||||||
pub boot_vga: Option<bool>,
|
pub name: String,
|
||||||
pub driver: Driver,
|
pub driver: Driver,
|
||||||
pub render_node: Option<PathBuf>,
|
|
||||||
pub discrete: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Gpu {
|
pub fn offload_env(card: &Card) -> Vec<(&'static str, String)> {
|
||||||
fn nvidia_proprietary(&self) -> bool {
|
let mut env = if card.driver == Driver::Nvidia {
|
||||||
self.driver == Driver::Nvidia
|
|
||||||
}
|
|
||||||
|
|
||||||
fn can_render(&self) -> bool {
|
|
||||||
self.render_node.is_some() || self.nvidia_proprietary()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn priority(&self) -> u32 {
|
|
||||||
u32::from(self.discrete) * 2 + u32::from(self.boot_vga == Some(true))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn offload_env(gpus: &[Gpu]) -> Vec<(&'static str, String)> {
|
|
||||||
let Some(best) = gpus
|
|
||||||
.iter()
|
|
||||||
.filter(|gpu| gpu.can_render())
|
|
||||||
.max_by_key(|gpu| gpu.priority())
|
|
||||||
else {
|
|
||||||
return Vec::new();
|
|
||||||
};
|
|
||||||
|
|
||||||
if !best.discrete || best.boot_vga == Some(true) {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut env = if best.nvidia_proprietary() {
|
|
||||||
vec![
|
vec![
|
||||||
("__NV_PRIME_RENDER_OFFLOAD", "1".to_string()),
|
("__NV_PRIME_RENDER_OFFLOAD", "1".to_string()),
|
||||||
("__VK_LAYER_NV_optimus", "NVIDIA_only".to_string()),
|
("__VK_LAYER_NV_optimus", "NVIDIA_only".to_string()),
|
||||||
("__GLX_VENDOR_LIBRARY_NAME", "nvidia".to_string()),
|
("__GLX_VENDOR_LIBRARY_NAME", "nvidia".to_string()),
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
let prime = pci_tag(&best.pci_address).unwrap_or_else(|| "1".to_string());
|
let prime = format!("pci-{}", card.pci_address.replace([':', '.'], "_"));
|
||||||
vec![("DRI_PRIME", prime)]
|
vec![("DRI_PRIME", prime)]
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(icd) = best.driver.vulkan_icd() {
|
if let Some(icd) = card.driver.vulkan_icd() {
|
||||||
env.push(("VK_LOADER_DRIVERS_SELECT", icd.to_string()));
|
env.push(("VK_LOADER_DRIVERS_SELECT", icd.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,22 +65,25 @@ pub fn offload_env(gpus: &[Gpu]) -> Vec<(&'static str, String)> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub fn detect() -> Vec<Gpu> {
|
pub fn detect() -> Vec<Card> {
|
||||||
let mut gpus = read_pci_devices(Path::new("/sys/bus/pci/devices"));
|
let ids = [
|
||||||
|
"/usr/share/hwdata/pci.ids",
|
||||||
|
"/usr/share/misc/pci.ids",
|
||||||
|
"/usr/share/pci.ids",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.find_map(|path| std::fs::read_to_string(path).ok())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
for gpu in &mut gpus {
|
read_pci_devices(Path::new("/sys/bus/pci/devices"), &ids)
|
||||||
gpu.discrete = probe_discrete(gpu);
|
|
||||||
}
|
|
||||||
|
|
||||||
gpus
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_pci_devices(root: &Path) -> Vec<Gpu> {
|
fn read_pci_devices(root: &Path, ids: &str) -> Vec<Card> {
|
||||||
let Ok(entries) = std::fs::read_dir(root) else {
|
let Ok(entries) = std::fs::read_dir(root) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut gpus = Vec::new();
|
let mut cards = Vec::new();
|
||||||
|
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let name = entry.file_name();
|
let name = entry.file_name();
|
||||||
|
|
@ -120,28 +93,30 @@ fn read_pci_devices(root: &Path) -> Vec<Gpu> {
|
||||||
|
|
||||||
let device = entry.path();
|
let device = entry.path();
|
||||||
|
|
||||||
if read_class(&device.join("class")).is_none_or(|class| class >> 16 != 0x03) {
|
if read_hex(&device.join("class")).is_none_or(|class| class >> 16 != 0x03) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
gpus.push(Gpu {
|
let driver = Driver::from_name(read_link_name(&device.join("driver")).as_deref());
|
||||||
|
if driver != Driver::Nvidia && !has_render_node(&device.join("drm")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let vendor = read_hex(&device.join("vendor")).unwrap_or(0);
|
||||||
|
let model = read_hex(&device.join("device")).unwrap_or(0);
|
||||||
|
|
||||||
|
cards.push(Card {
|
||||||
pci_address: address.to_string(),
|
pci_address: address.to_string(),
|
||||||
boot_vga: read_flag(&device.join("boot_vga")),
|
name: display_name(ids, vendor, model, address),
|
||||||
driver: Driver::from_name(read_link_name(&device.join("driver")).as_deref()),
|
driver,
|
||||||
render_node: render_node(&device.join("drm")),
|
|
||||||
discrete: false,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
gpus.sort_by(|a, b| a.pci_address.cmp(&b.pci_address));
|
cards.sort_by(|a, b| a.pci_address.cmp(&b.pci_address));
|
||||||
gpus
|
cards
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_flag(path: &Path) -> Option<bool> {
|
fn read_hex(path: &Path) -> Option<u32> {
|
||||||
Some(std::fs::read_to_string(path).ok()?.trim() == "1")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_class(path: &Path) -> Option<u32> {
|
|
||||||
let raw = std::fs::read_to_string(path).ok()?;
|
let raw = std::fs::read_to_string(path).ok()?;
|
||||||
let raw = raw.trim();
|
let raw = raw.trim();
|
||||||
u32::from_str_radix(raw.strip_prefix("0x").unwrap_or(raw), 16).ok()
|
u32::from_str_radix(raw.strip_prefix("0x").unwrap_or(raw), 16).ok()
|
||||||
|
|
@ -152,410 +127,95 @@ fn read_link_name(path: &Path) -> Option<String> {
|
||||||
Some(link.file_name()?.to_str()?.to_string())
|
Some(link.file_name()?.to_str()?.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_node(drm: &Path) -> Option<PathBuf> {
|
fn has_render_node(drm: &Path) -> bool {
|
||||||
let entries = std::fs::read_dir(drm).ok()?;
|
std::fs::read_dir(drm).is_ok_and(|entries| {
|
||||||
|
entries
|
||||||
entries.flatten().find_map(|entry| {
|
.flatten()
|
||||||
let name = entry.file_name();
|
.any(|entry| entry.file_name().to_string_lossy().starts_with("renderD"))
|
||||||
name.to_str()?
|
|
||||||
.starts_with("renderD")
|
|
||||||
.then(|| Path::new("/dev/dri").join(&name))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pci_tag(address: &str) -> Option<String> {
|
fn display_name(ids: &str, vendor: u32, model: u32, address: &str) -> String {
|
||||||
let (domain, rest) = address.split_once(':')?;
|
let brand = match vendor {
|
||||||
let (bus, rest) = rest.split_once(':')?;
|
0x10de => "NVIDIA",
|
||||||
let (device, function) = rest.split_once('.')?;
|
0x1002 => "AMD",
|
||||||
|
0x8086 => "Intel",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
|
||||||
let widths = [(domain, 4), (bus, 2), (device, 2), (function, 1)];
|
let model = pci_ids_model(ids, vendor, model).map_or_else(
|
||||||
if widths
|
|| format!("GPU {address}"),
|
||||||
.iter()
|
|name| {
|
||||||
.any(|(part, width)| part.len() != *width || !part.bytes().all(|b| b.is_ascii_hexdigit()))
|
name.rsplit_once('[')
|
||||||
{
|
.and_then(|(_, marketing)| marketing.strip_suffix(']'))
|
||||||
return None;
|
.unwrap_or(name)
|
||||||
}
|
.to_string()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
Some(format!("pci-{domain}_{bus}_{device}_{function}"))
|
if brand.is_empty() {
|
||||||
}
|
model
|
||||||
|
} else {
|
||||||
fn probe_discrete(gpu: &Gpu) -> bool {
|
format!("{brand} {model}")
|
||||||
match gpu.driver {
|
|
||||||
Driver::Nvidia => true,
|
|
||||||
Driver::AmdGpu => answered(gpu, drm::amdgpu_is_discrete(gpu)),
|
|
||||||
Driver::Xe => answered(gpu, drm::xe_is_discrete(gpu)),
|
|
||||||
Driver::I915 => gpu.pci_address != INTEL_INTEGRATED,
|
|
||||||
Driver::Nouveau | Driver::Radeon | Driver::Other => gpu.boot_vga == Some(false),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn answered(gpu: &Gpu, probe: std::io::Result<bool>) -> bool {
|
fn pci_ids_model(ids: &str, vendor: u32, model: u32) -> Option<&str> {
|
||||||
probe.unwrap_or_else(|error| {
|
let vendor = format!("{vendor:04x} ");
|
||||||
tracing::warn!(
|
let model = format!("\t{model:04x} ");
|
||||||
card = gpu.pci_address,
|
|
||||||
?error,
|
|
||||||
"no discreteness answer; assuming integrated"
|
|
||||||
);
|
|
||||||
false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
ids.lines()
|
||||||
mod drm {
|
.skip_while(|line| !line.starts_with(&vendor))
|
||||||
use super::Gpu;
|
.skip(1)
|
||||||
|
.take_while(|line| line.starts_with('\t') || line.starts_with('#'))
|
||||||
fn unavailable() -> std::io::Result<bool> {
|
.find_map(|line| line.strip_prefix(&model))
|
||||||
Err(std::io::Error::from(std::io::ErrorKind::Unsupported))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn amdgpu_is_discrete(_gpu: &Gpu) -> std::io::Result<bool> {
|
|
||||||
unavailable()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn xe_is_discrete(_gpu: &Gpu) -> std::io::Result<bool> {
|
|
||||||
unavailable()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
mod drm {
|
|
||||||
use std::fs::File;
|
|
||||||
use std::os::fd::AsRawFd;
|
|
||||||
|
|
||||||
use super::Gpu;
|
|
||||||
|
|
||||||
const DRM_IOCTL_BASE: u32 = b'd' as u32;
|
|
||||||
const DRM_COMMAND_BASE: u32 = 0x40;
|
|
||||||
|
|
||||||
fn open(gpu: &Gpu) -> std::io::Result<Option<File>> {
|
|
||||||
let Some(node) = &gpu.render_node else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
std::fs::OpenOptions::new()
|
|
||||||
.read(true)
|
|
||||||
.write(true)
|
|
||||||
.open(node)
|
|
||||||
.map(Some)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check(result: i32) -> std::io::Result<()> {
|
|
||||||
if result == 0 {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(std::io::Error::last_os_error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn command_write<T>(file: &File, index: u32, data: &T) -> std::io::Result<()> {
|
|
||||||
let request = libc::_IOW::<T>(DRM_IOCTL_BASE, DRM_COMMAND_BASE + index);
|
|
||||||
check(unsafe { libc::ioctl(file.as_raw_fd(), request, std::ptr::from_ref(data)) })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn command_read_write<T>(file: &File, index: u32, data: &mut T) -> std::io::Result<()> {
|
|
||||||
let request = libc::_IOWR::<T>(DRM_IOCTL_BASE, DRM_COMMAND_BASE + index);
|
|
||||||
check(unsafe { libc::ioctl(file.as_raw_fd(), request, std::ptr::from_mut(data)) })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn amdgpu_is_discrete(gpu: &Gpu) -> std::io::Result<bool> {
|
|
||||||
let Some(file) = open(gpu)? else {
|
|
||||||
return Ok(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
struct InfoDevice {
|
|
||||||
_before_ids_flags: [u32; 34],
|
|
||||||
ids_flags: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
struct Info {
|
|
||||||
return_pointer: u64,
|
|
||||||
return_size: u32,
|
|
||||||
query: u32,
|
|
||||||
_query_args: [u32; 4],
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut result = InfoDevice {
|
|
||||||
_before_ids_flags: [0; 34],
|
|
||||||
ids_flags: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const AMDGPU_INFO_DEV_INFO: u32 = 0x16;
|
|
||||||
let query = Info {
|
|
||||||
return_pointer: std::ptr::from_mut(&mut result) as u64,
|
|
||||||
return_size: size_of_val(&result) as u32,
|
|
||||||
query: AMDGPU_INFO_DEV_INFO,
|
|
||||||
_query_args: [0; 4],
|
|
||||||
};
|
|
||||||
|
|
||||||
const DRM_AMDGPU_INFO: u32 = 0x05;
|
|
||||||
command_write(&file, DRM_AMDGPU_INFO, &query)?;
|
|
||||||
|
|
||||||
const AMDGPU_IDS_FLAGS_FUSION: u64 = 0x1;
|
|
||||||
Ok(result.ids_flags & AMDGPU_IDS_FLAGS_FUSION == 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn xe_is_discrete(gpu: &Gpu) -> std::io::Result<bool> {
|
|
||||||
let Some(file) = open(gpu)? else {
|
|
||||||
return Ok(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
struct DeviceQuery {
|
|
||||||
extensions: u64,
|
|
||||||
query: u32,
|
|
||||||
size: u32,
|
|
||||||
data: u64,
|
|
||||||
_reserved: [u64; 2],
|
|
||||||
}
|
|
||||||
|
|
||||||
const DRM_XE_DEVICE_QUERY: u32 = 0x00;
|
|
||||||
const DRM_XE_DEVICE_QUERY_CONFIG: u32 = 0x2;
|
|
||||||
|
|
||||||
let mut query = DeviceQuery {
|
|
||||||
extensions: 0,
|
|
||||||
query: DRM_XE_DEVICE_QUERY_CONFIG,
|
|
||||||
size: 0,
|
|
||||||
data: 0,
|
|
||||||
_reserved: [0; 2],
|
|
||||||
};
|
|
||||||
|
|
||||||
command_read_write(&file, DRM_XE_DEVICE_QUERY, &mut query)?;
|
|
||||||
|
|
||||||
const INFO_OFFSET: usize = 1;
|
|
||||||
const CONFIG_FLAGS: usize = 1;
|
|
||||||
let words = (query.size as usize).div_ceil(size_of::<u64>());
|
|
||||||
|
|
||||||
if words <= INFO_OFFSET + CONFIG_FLAGS {
|
|
||||||
tracing::warn!(size = query.size, "xe config too short to hold its flags");
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut data = vec![0_u64; words];
|
|
||||||
query.data = data.as_mut_ptr() as u64;
|
|
||||||
|
|
||||||
command_read_write(&file, DRM_XE_DEVICE_QUERY, &mut query)?;
|
|
||||||
|
|
||||||
const DRM_XE_QUERY_CONFIG_FLAG_HAS_VRAM: u64 = 1 << 0;
|
|
||||||
Ok(data[INFO_OFFSET + CONFIG_FLAGS] & DRM_XE_QUERY_CONFIG_FLAG_HAS_VRAM != 0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
const AMD_INTEGRATED: &str = "0000:0c:00.0";
|
const IDS: &str = "\
|
||||||
|
# comment
|
||||||
|
10de NVIDIA Corporation
|
||||||
|
\t25a2 GA107M [GeForce RTX 3050 Mobile]
|
||||||
|
\t\t1043 1e1f GA107M [GeForce RTX 3050 Mobile]
|
||||||
|
\t1234 Plain Name
|
||||||
|
1002 Advanced Micro Devices, Inc. [AMD/ATI]
|
||||||
|
\t25a2 Not The Nvidia Card
|
||||||
|
8086 Intel Corporation
|
||||||
|
";
|
||||||
|
|
||||||
fn gpu(address: &str, driver: Driver, boot_vga: Option<bool>) -> Gpu {
|
fn card(address: &str, driver: Driver) -> Card {
|
||||||
let discrete = match driver {
|
Card {
|
||||||
Driver::Nvidia => true,
|
|
||||||
Driver::I915 => address != INTEL_INTEGRATED,
|
|
||||||
_ => boot_vga == Some(false),
|
|
||||||
};
|
|
||||||
|
|
||||||
Gpu {
|
|
||||||
pci_address: address.to_string(),
|
pci_address: address.to_string(),
|
||||||
boot_vga,
|
name: String::new(),
|
||||||
driver,
|
driver,
|
||||||
render_node: Some(PathBuf::from("/dev/dri/renderD128")),
|
|
||||||
discrete,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nvidia_offload() -> Vec<(&'static str, String)> {
|
|
||||||
vec![
|
|
||||||
("__NV_PRIME_RENDER_OFFLOAD", "1".to_string()),
|
|
||||||
("__VK_LAYER_NV_optimus", "NVIDIA_only".to_string()),
|
|
||||||
("__GLX_VENDOR_LIBRARY_NAME", "nvidia".to_string()),
|
|
||||||
("VK_LOADER_DRIVERS_SELECT", "*nvidia*".to_string()),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dri_prime(card: &str, icd: &str) -> Vec<(&'static str, String)> {
|
|
||||||
vec![
|
|
||||||
("DRI_PRIME", card.to_string()),
|
|
||||||
("VK_LOADER_DRIVERS_SELECT", icd.to_string()),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn offload_env_names_the_right_card_for_every_pairing() {
|
|
||||||
let igpu = || gpu(INTEL_INTEGRATED, Driver::I915, Some(true));
|
|
||||||
let dgpu = || gpu("0000:01:00.0", Driver::Nvidia, Some(false));
|
|
||||||
|
|
||||||
type Case = (&'static str, Vec<Gpu>, Vec<(&'static str, String)>);
|
|
||||||
let amd = "*radeon*,*amd*";
|
|
||||||
|
|
||||||
let cases: Vec<Case> = vec![
|
|
||||||
("no cards at all", vec![], vec![]),
|
|
||||||
(
|
|
||||||
"a lone amd card",
|
|
||||||
vec![gpu("0000:01:00.0", Driver::AmdGpu, Some(true))],
|
|
||||||
vec![],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"a lone nvidia card",
|
|
||||||
vec![gpu("0000:01:00.0", Driver::Nvidia, Some(true))],
|
|
||||||
vec![],
|
|
||||||
),
|
|
||||||
("a lone intel card", vec![igpu()], vec![]),
|
|
||||||
(
|
|
||||||
"an all-amd hybrid never names nvidia",
|
|
||||||
vec![
|
|
||||||
gpu(AMD_INTEGRATED, Driver::AmdGpu, Some(true)),
|
|
||||||
gpu("0000:01:00.0", Driver::AmdGpu, Some(false)),
|
|
||||||
],
|
|
||||||
dri_prime("pci-0000_01_00_0", amd),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"an intel + amd hybrid never names nvidia",
|
|
||||||
vec![igpu(), gpu("0000:01:00.0", Driver::AmdGpu, Some(false))],
|
|
||||||
dri_prime("pci-0000_01_00_0", amd),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"an optimus laptop gets the nvidia variables and no DRI_PRIME",
|
|
||||||
vec![igpu(), dgpu()],
|
|
||||||
nvidia_offload(),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"the same laptop on nouveau gets DRI_PRIME and no ICD filter",
|
|
||||||
vec![
|
|
||||||
igpu(),
|
|
||||||
Gpu {
|
|
||||||
driver: Driver::Nouveau,
|
|
||||||
..dgpu()
|
|
||||||
},
|
|
||||||
],
|
|
||||||
vec![("DRI_PRIME", "pci-0000_01_00_0".to_string())],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"an nvidia card without a render node is still a target",
|
|
||||||
vec![
|
|
||||||
igpu(),
|
|
||||||
Gpu {
|
|
||||||
render_node: None,
|
|
||||||
..dgpu()
|
|
||||||
},
|
|
||||||
],
|
|
||||||
nvidia_offload(),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"an unbound card is never the target",
|
|
||||||
vec![
|
|
||||||
igpu(),
|
|
||||||
Gpu {
|
|
||||||
driver: Driver::Other,
|
|
||||||
render_node: None,
|
|
||||||
..dgpu()
|
|
||||||
},
|
|
||||||
],
|
|
||||||
vec![],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"a bmc display adapter is skipped for the real card behind it",
|
|
||||||
vec![
|
|
||||||
igpu(),
|
|
||||||
Gpu {
|
|
||||||
render_node: None,
|
|
||||||
..gpu("0000:03:00.0", Driver::Other, Some(false))
|
|
||||||
},
|
|
||||||
gpu("0000:c1:00.0", Driver::AmdGpu, Some(false)),
|
|
||||||
],
|
|
||||||
dri_prime("pci-0000_c1_00_0", amd),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"the discrete card wins over a renderable card at a lower address",
|
|
||||||
vec![
|
|
||||||
igpu(),
|
|
||||||
Gpu {
|
|
||||||
discrete: false,
|
|
||||||
..gpu("0000:03:00.0", Driver::AmdGpu, Some(false))
|
|
||||||
},
|
|
||||||
gpu("0000:c1:00.0", Driver::AmdGpu, Some(false)),
|
|
||||||
],
|
|
||||||
dri_prime("pci-0000_c1_00_0", amd),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"nothing happens when the discrete card already drives the display",
|
|
||||||
vec![
|
|
||||||
gpu("0000:01:00.0", Driver::Nvidia, Some(true)),
|
|
||||||
gpu(INTEL_INTEGRATED, Driver::I915, Some(false)),
|
|
||||||
],
|
|
||||||
vec![],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"the driver still finds the discrete card when nothing publishes boot_vga",
|
|
||||||
vec![
|
|
||||||
gpu(INTEL_INTEGRATED, Driver::I915, None),
|
|
||||||
gpu("0000:01:00.0", Driver::Nvidia, None),
|
|
||||||
],
|
|
||||||
nvidia_offload(),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"a malformed pci address falls back to the ordinal",
|
|
||||||
vec![
|
|
||||||
gpu(AMD_INTEGRATED, Driver::AmdGpu, Some(true)),
|
|
||||||
gpu("not-an-address", Driver::AmdGpu, Some(false)),
|
|
||||||
],
|
|
||||||
dri_prime("1", amd),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (name, gpus, expected) in cases {
|
|
||||||
assert_eq!(offload_env(&gpus), expected, "{name}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pci_tags_match_the_shape_mesa_builds() {
|
fn offload_env_names_the_chosen_card() {
|
||||||
assert_eq!(pci_tag("0000:01:00.0").as_deref(), Some("pci-0000_01_00_0"));
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
pci_tag("10000:af:1f.7").as_deref(),
|
offload_env(&card("0000:01:00.0", Driver::Nvidia)),
|
||||||
None,
|
vec![
|
||||||
"domain is 4 wide"
|
("__NV_PRIME_RENDER_OFFLOAD", "1".to_string()),
|
||||||
|
("__VK_LAYER_NV_optimus", "NVIDIA_only".to_string()),
|
||||||
|
("__GLX_VENDOR_LIBRARY_NAME", "nvidia".to_string()),
|
||||||
|
("VK_LOADER_DRIVERS_SELECT", "*nvidia*".to_string()),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
assert_eq!(pci_tag("0000:1:00.0"), None, "bus is 2 wide");
|
assert_eq!(
|
||||||
assert_eq!(pci_tag("0000:01:00"), None, "no function");
|
offload_env(&card("0000:c1:00.0", Driver::AmdGpu)),
|
||||||
assert_eq!(pci_tag("0000:0g:00.0"), None, "not hex");
|
vec![
|
||||||
}
|
("DRI_PRIME", "pci-0000_c1_00_0".to_string()),
|
||||||
|
("VK_LOADER_DRIVERS_SELECT", "*radeon*,*amd*".to_string()),
|
||||||
#[test]
|
],
|
||||||
fn a_driver_that_will_not_answer_is_never_called_discrete() {
|
|
||||||
let mut apu = gpu(AMD_INTEGRATED, Driver::AmdGpu, Some(false));
|
|
||||||
apu.discrete = probe_discrete(&apu);
|
|
||||||
assert!(!apu.discrete);
|
|
||||||
|
|
||||||
let mut dgpu = gpu("0000:03:00.0", Driver::AmdGpu, Some(true));
|
|
||||||
dgpu.discrete = probe_discrete(&dgpu);
|
|
||||||
assert!(!dgpu.discrete);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
offload_env(&[apu, dgpu]).is_empty(),
|
|
||||||
"knowing nothing, leave the renderer alone"
|
|
||||||
);
|
);
|
||||||
}
|
assert_eq!(
|
||||||
|
offload_env(&card("0000:01:00.0", Driver::Nouveau)),
|
||||||
#[test]
|
vec![("DRI_PRIME", "pci-0000_01_00_0".to_string())],
|
||||||
fn the_cards_we_can_place_without_asking_the_driver() {
|
"nouveau gets no ICD filter",
|
||||||
assert!(probe_discrete(&gpu(
|
|
||||||
"0000:01:00.0",
|
|
||||||
Driver::Nvidia,
|
|
||||||
Some(false)
|
|
||||||
)));
|
|
||||||
assert!(!probe_discrete(&gpu(
|
|
||||||
INTEL_INTEGRATED,
|
|
||||||
Driver::I915,
|
|
||||||
Some(true)
|
|
||||||
)));
|
|
||||||
assert!(
|
|
||||||
probe_discrete(&gpu("0000:03:00.0", Driver::I915, Some(false))),
|
|
||||||
"an arc board does not sit at the integrated address"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
probe_discrete(&gpu("0000:01:00.0", Driver::Nouveau, Some(false))),
|
|
||||||
"no query, so boot_vga is all we have"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -570,10 +230,11 @@ mod tests {
|
||||||
std::fs::write(path, contents).unwrap();
|
std::fs::write(path, contents).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_card(bus: &Path, address: &str, boot_vga: &str, driver: &str) {
|
fn write_card(bus: &Path, address: &str, vendor: &str, model: &str, driver: &str) {
|
||||||
let device = bus.join(address);
|
let device = bus.join(address);
|
||||||
write(&device.join("boot_vga"), &format!("{boot_vga}\n"));
|
|
||||||
write(&device.join("class"), "0x030000\n");
|
write(&device.join("class"), "0x030000\n");
|
||||||
|
write(&device.join("vendor"), &format!("0x{vendor}\n"));
|
||||||
|
write(&device.join("device"), &format!("0x{model}\n"));
|
||||||
|
|
||||||
let target = bus.join("drivers").join(driver);
|
let target = bus.join("drivers").join(driver);
|
||||||
std::fs::create_dir_all(&target).unwrap();
|
std::fs::create_dir_all(&target).unwrap();
|
||||||
|
|
@ -581,58 +242,41 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reads_boot_flag_driver_and_render_node_out_of_a_sysfs_tree() {
|
fn lists_the_cards_that_can_render_out_of_a_sysfs_tree() {
|
||||||
let scratch = polyio::testing::ScratchDir::new("gpu-sysfs");
|
let scratch = polyio::testing::ScratchDir::new("gpu-sysfs");
|
||||||
let bus = scratch.join("devices");
|
let bus = scratch.join("devices");
|
||||||
|
|
||||||
write_card(&bus, INTEL_INTEGRATED, "1", "i915");
|
write_card(&bus, "0000:00:02.0", "8086", "9a49", "i915");
|
||||||
write(
|
write(
|
||||||
&bus.join(INTEL_INTEGRATED)
|
&bus.join("0000:00:02.0")
|
||||||
.join("drm")
|
.join("drm")
|
||||||
.join("renderD128")
|
.join("renderD128")
|
||||||
.join("dev"),
|
.join("dev"),
|
||||||
"226:128\n",
|
"226:128\n",
|
||||||
);
|
);
|
||||||
write_card(&bus, "0000:01:00.0", "0", "nvidia");
|
write_card(&bus, "0000:01:00.0", "10de", "25a2", "nvidia");
|
||||||
|
write_card(&bus, "0000:03:00.0", "1a03", "2000", "ast");
|
||||||
let gpus = read_pci_devices(&bus);
|
|
||||||
|
|
||||||
assert_eq!(gpus.len(), 2);
|
|
||||||
assert_eq!(gpus[0].pci_address, INTEL_INTEGRATED);
|
|
||||||
assert_eq!(gpus[0].boot_vga, Some(true));
|
|
||||||
assert_eq!(gpus[0].driver, Driver::I915);
|
|
||||||
assert_eq!(
|
|
||||||
gpus[0].render_node,
|
|
||||||
Some(PathBuf::from("/dev/dri/renderD128")),
|
|
||||||
"the node the ioctl probes open"
|
|
||||||
);
|
|
||||||
assert_eq!(gpus[1].boot_vga, Some(false));
|
|
||||||
assert_eq!(gpus[1].driver, Driver::Nvidia);
|
|
||||||
assert_eq!(gpus[1].render_node, None, "no nvidia-drm in this tree");
|
|
||||||
|
|
||||||
assert!(gpus.iter().all(|gpu| !gpu.discrete));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn skips_everything_on_the_bus_that_is_not_a_display_controller() {
|
|
||||||
let scratch = polyio::testing::ScratchDir::new("gpu-bus");
|
|
||||||
let bus = scratch.join("devices");
|
|
||||||
|
|
||||||
write_card(&bus, "0000:01:00.0", "1", "amdgpu");
|
|
||||||
write(&bus.join("0000:01:00.1").join("class"), "0x040300\n");
|
write(&bus.join("0000:01:00.1").join("class"), "0x040300\n");
|
||||||
write(&bus.join("0000:02:00.0").join("class"), "0x020000\n");
|
write(&bus.join("0000:02:00.0").join("class"), "0x020000\n");
|
||||||
std::fs::create_dir_all(bus.join("0000:03:00.0")).unwrap();
|
std::fs::create_dir_all(bus.join("0000:04:00.0")).unwrap();
|
||||||
|
|
||||||
let gpus = read_pci_devices(&bus);
|
let gpus = read_pci_devices(&bus, IDS);
|
||||||
|
|
||||||
assert_eq!(gpus.len(), 1, "one card, counted once");
|
assert_eq!(
|
||||||
assert_eq!(gpus[0].driver, Driver::AmdGpu);
|
gpus.len(),
|
||||||
|
2,
|
||||||
|
"no bmc adapter, audio, nic or classless device"
|
||||||
|
);
|
||||||
|
assert_eq!(gpus[0].pci_address, "0000:00:02.0");
|
||||||
|
assert_eq!(gpus[0].driver, Driver::I915);
|
||||||
|
assert_eq!(gpus[1].driver, Driver::Nvidia, "needs no render node");
|
||||||
|
assert_eq!(gpus[1].name, "NVIDIA GeForce RTX 3050 Mobile");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_missing_pci_tree_is_not_an_error() {
|
fn a_missing_pci_tree_is_not_an_error() {
|
||||||
let scratch = polyio::testing::ScratchDir::new("gpu-missing");
|
let scratch = polyio::testing::ScratchDir::new("gpu-missing");
|
||||||
|
|
||||||
assert!(read_pci_devices(&scratch.join("nope")).is_empty());
|
assert!(read_pci_devices(&scratch.join("nope"), "").is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,69 @@
|
||||||
#[cfg(any(target_os = "linux", test))]
|
#[cfg(any(target_os = "linux", test))]
|
||||||
mod linux;
|
mod linux;
|
||||||
|
|
||||||
use tokio::process::Command;
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Gpu {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
const WINDOWS_PREFERENCES: [(&str, &str, u8); 2] = [
|
||||||
|
("integrated", "Integrated GPU", 1),
|
||||||
|
("dedicated", "Dedicated GPU", 2),
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn detect() -> Vec<Gpu> {
|
||||||
|
cfg_select! {
|
||||||
|
target_os = "linux" => linux::detect()
|
||||||
|
.into_iter()
|
||||||
|
.map(|card| Gpu {
|
||||||
|
id: card.pci_address,
|
||||||
|
name: card.name,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
windows => WINDOWS_PREFERENCES
|
||||||
|
.iter()
|
||||||
|
.map(|(id, name, _)| Gpu {
|
||||||
|
id: (*id).to_string(),
|
||||||
|
name: (*name).to_string(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
_ => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
pub async fn prefer_discrete(command: &mut Command, java_path: &str) {
|
pub async fn select(command: &mut tokio::process::Command, java_path: &str, id: &str) {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
let gpus = linux::detect();
|
let cards = tokio::task::spawn_blocking(linux::detect)
|
||||||
let env = linux::offload_env(&gpus);
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
if env.is_empty() {
|
let Some(card) = cards.into_iter().find(|card| card.pci_address == id) else {
|
||||||
tracing::info!(
|
tracing::warn!(id, "the selected GPU is gone; leaving the renderer alone");
|
||||||
gpus = gpus.len(),
|
|
||||||
"discrete GPU was requested but nothing here is a valid offload target; \
|
|
||||||
leaving the renderer alone"
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
};
|
||||||
|
|
||||||
for (key, value) in env {
|
for (key, value) in linux::offload_env(&card) {
|
||||||
tracing::debug!(key, value, "offloading the game to the discrete GPU");
|
tracing::debug!(
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
gpu = card.name,
|
||||||
|
"rendering the game on the selected GPU"
|
||||||
|
);
|
||||||
command.env(key, value);
|
command.env(key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
oneclient_java::prefer_dedicated_gpu(std::path::Path::new(java_path)).await;
|
{
|
||||||
|
let Some((_, _, preference)) = WINDOWS_PREFERENCES.iter().find(|(known, ..)| *known == id)
|
||||||
|
else {
|
||||||
|
tracing::warn!(id, "unknown GPU preference; leaving the renderer alone");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
oneclient_java::set_gpu_preference(std::path::Path::new(java_path), *preference).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -382,8 +382,8 @@ async fn start(
|
||||||
|
|
||||||
let (mut command, wrapper) = base_command(&profile, &java.absolute_path);
|
let (mut command, wrapper) = base_command(&profile, &java.absolute_path);
|
||||||
|
|
||||||
if profile.use_discrete_gpu() {
|
if let Some(gpu) = profile.gpu() {
|
||||||
crate::game::gpu::prefer_discrete(&mut command, &java.absolute_path).await;
|
crate::game::gpu::select(&mut command, &java.absolute_path, gpu).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_env(&mut command, &profile);
|
apply_env(&mut command, &profile);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
mod analytics;
|
mod analytics;
|
||||||
mod error;
|
mod error;
|
||||||
pub mod fabric;
|
pub mod fabric;
|
||||||
mod gpu;
|
pub mod gpu;
|
||||||
mod heal;
|
mod heal;
|
||||||
mod launch;
|
mod launch;
|
||||||
mod log_replay;
|
mod log_replay;
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ pub use data::{JavaPackage, JavaRuntime, PackageArchive, java_executable_relativ
|
||||||
pub use error::{JavaError, JavaResult};
|
pub use error::{JavaError, JavaResult};
|
||||||
pub use install::{install_package, is_launcher_managed};
|
pub use install::{install_package, is_launcher_managed};
|
||||||
pub use locate::{LocatedJava, best_for_major, locate_java};
|
pub use locate::{LocatedJava, best_for_major, locate_java};
|
||||||
pub use platform::{HostArch, HostOs, HostTarget, prefer_dedicated_gpu};
|
pub use platform::{HostArch, HostOs, HostTarget, set_gpu_preference};
|
||||||
pub use service::{
|
pub use service::{
|
||||||
AvailableJava, INSTALLABLE_MAJORS, JAVA_CHOICE_DOWNLOAD, JAVA_CHOICE_FOLDER, JAVA_VENDOR_HINT,
|
AvailableJava, INSTALLABLE_MAJORS, JAVA_CHOICE_DOWNLOAD, JAVA_CHOICE_FOLDER, JAVA_VENDOR_HINT,
|
||||||
JavaService,
|
JavaService,
|
||||||
|
|
|
||||||
|
|
@ -110,17 +110,8 @@ fn registered_value<'a>(query: &'a str, name: &str) -> Option<&'a str> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Windows writes `GpuPreference=0;` ("let Windows decide") for any app added
|
|
||||||
/// through Settings, so only 1 or 2 is a choice worth leaving alone
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
fn has_explicit_preference(data: &str) -> bool {
|
pub async fn set_gpu_preference(java_path: &std::path::Path, preference: u8) {
|
||||||
data.split(';')
|
|
||||||
.find_map(|part| part.trim().strip_prefix("GpuPreference="))
|
|
||||||
.is_some_and(|preference| matches!(preference.trim(), "1" | "2"))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
pub async fn prefer_dedicated_gpu(java_path: &std::path::Path) {
|
|
||||||
let Some(dir) = java_path.parent() else {
|
let Some(dir) = java_path.parent() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
@ -150,21 +141,22 @@ pub async fn prefer_dedicated_gpu(java_path: &std::path::Path) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let data = format!("GpuPreference={preference};");
|
||||||
for path in targets {
|
for path in targets {
|
||||||
if registered_value(&query, &path.to_string_lossy()).is_some_and(has_explicit_preference) {
|
if registered_value(&query, &path.to_string_lossy()) == Some(data.as_str()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let added = reg_command()
|
let added = reg_command()
|
||||||
.args(["add", GPU_PREFERENCES_KEY, "/v"])
|
.args(["add", GPU_PREFERENCES_KEY, "/v"])
|
||||||
.arg(&path)
|
.arg(&path)
|
||||||
.args(["/t", "REG_SZ", "/d", "GpuPreference=2;", "/f"])
|
.args(["/t", "REG_SZ", "/d", data.as_str(), "/f"])
|
||||||
.output()
|
.output()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match added {
|
match added {
|
||||||
Ok(out) if out.status.success() => {
|
Ok(out) if out.status.success() => {
|
||||||
tracing::info!(path = %path.display(), "registered JVM for high-performance GPU");
|
tracing::info!(path = %path.display(), preference, "registered JVM GPU preference");
|
||||||
}
|
}
|
||||||
Ok(out) => tracing::warn!(
|
Ok(out) => tracing::warn!(
|
||||||
path = %path.display(),
|
path = %path.display(),
|
||||||
|
|
@ -205,7 +197,7 @@ pub async fn forget_dedicated_gpu(java_path: &std::path::Path) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
pub async fn prefer_dedicated_gpu(_java_path: &std::path::Path) {}
|
pub async fn set_gpu_preference(_java_path: &std::path::Path, _preference: u8) {}
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
pub async fn forget_dedicated_gpu(_java_path: &std::path::Path) {}
|
pub async fn forget_dedicated_gpu(_java_path: &std::path::Path) {}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue