fix: linux use dedicated GPU setting (#816)

* forgot to add this

* fix complex type warning in test

* fix(linux): ask the driver whether a card is discrete
This commit is contained in:
LynithDev 2026-09-13 01:36:47 +02:00 • committed by GitHub
parent cad4800835
commit 54980fa73a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 677 additions and 466 deletions

1
Cargo.lock generated
View file

@ -4619,6 +4619,7 @@ dependencies = [
"image",
"indicatif",
"interfrost",
"libc",
"md-5 0.10.6",
"oneclient_auth",
"oneclient_cluster",

View file

@ -113,6 +113,7 @@ dunce = {version = "=1.0.5"}
htmd = {version = "=0.5.4"}
indicatif = {version = "=0.18.4", features = ["tokio"]}
junction = {version = "=2.0.0"}
libc = {version = "=0.2.189"}
notify = {version = "=8.2.0"}
parking_lot = {version = "=0.12.5"}
pulldown-cmark = {version = "=0.13.4", default-features = false}

View file

@ -1,24 +1,19 @@
use freya::prelude::*;
use oneclient_common::Patch;
use oneclient_core::settings::{PackageUpdateMode, ProfileUpdate, Resolution};
#[cfg(windows)]
use oneclient_core::settings::LauncherSettings;
#[cfg(any(target_os = "linux", windows))]
use oneclient_core::settings::SettingsOsExtra;
use super::settings_page;
use crate::components::{
Dropdown, Icon, IconType, TextInput, memory_field, toggle, validate_number,
};
#[cfg(windows)]
#[cfg(any(target_os = "linux", windows))]
use crate::components::toggle_controlled;
use crate::hooks::{use_dispatch, use_settings_snapshot};
use crate::theme::colors;
use crate::view::app::settings::{section_header, settings_row};
#[cfg(target_os = "linux")]
use crate::components::toggle_controlled;
#[cfg(target_os = "linux")]
use oneclient_core::settings::SettingsOsExtra;
#[derive(PartialEq)]
pub struct SettingsMinecraft;
@ -151,15 +146,7 @@ impl Component for SettingsMinecraft {
.width(Size::px(220.)),
));
#[cfg(windows)]
let page = page.child(settings_row(
IconType::Rocket02,
"Prefer Dedicated GPU",
"Ask Windows to run Java on the high-performance GPU.",
discrete_gpu_field(settings, dispatch.clone()),
));
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", windows))]
let page = page
.child(section_header("GRAPHICS"))
.child(discrete_gpu_row(profile.os_extra.clone(), dispatch));
@ -168,7 +155,7 @@ impl Component for SettingsMinecraft {
}
}
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", windows))]
fn discrete_gpu_row(
os_extra: Option<SettingsOsExtra>,
dispatch: crate::Actions,
@ -231,20 +218,6 @@ fn build_update(
}
}
/// A launcher setting rather than a profile field, so it bypasses [`build_update`]
#[cfg(windows)]
fn discrete_gpu_field(settings: LauncherSettings, dispatch: crate::Actions) -> impl IntoElement {
let on = settings.use_discrete_gpu;
let on_toggle: EventHandler<()> = (move |()| {
let mut next = settings.clone();
next.use_discrete_gpu = !on;
dispatch.set_settings(next);
})
.into();
toggle_controlled(on, on_toggle)
}
/// Dispatched separately from [`build_update`] which debounces keystrokes a dropdown has no intermediate states
fn update_mode_field(
selected: PackageUpdateMode,

View file

@ -26,37 +26,25 @@ pub struct GameSettingsProfile {
pub use oneclient_common::Resolution;
pub use oneclient_common::domain::PackageUpdateMode;
cfg_select! {
target_os = "linux" => {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct SettingsOsExtra {
pub enable_gamemode: Option<bool>,
pub use_discrete_gpu: Option<bool>,
/// Keys this build has no field for, kept verbatim so a profile edited
/// on one OS does not clear another OS's settings
#[serde(flatten)]
pub unknown: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct SettingsOsExtra {
#[cfg(target_os = "linux")]
pub enable_gamemode: Option<bool>,
#[cfg(any(target_os = "linux", windows))]
pub use_discrete_gpu: Option<bool>,
#[serde(flatten)]
pub unknown: serde_json::Map<String, serde_json::Value>,
}
impl Default for SettingsOsExtra {
fn default() -> Self {
Self {
enable_gamemode: Some(true),
use_discrete_gpu: Some(false),
unknown: serde_json::Map::new(),
}
}
}
}
_ => {
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct SettingsOsExtra {
/// Keys this build has no field for, kept verbatim so a profile edited
/// on one OS does not clear another OS's settings
#[serde(flatten)]
pub unknown: serde_json::Map<String, serde_json::Value>,
impl Default for SettingsOsExtra {
fn default() -> Self {
Self {
#[cfg(target_os = "linux")]
enable_gamemode: Some(true),
#[cfg(any(target_os = "linux", windows))]
use_discrete_gpu: Some(true),
unknown: serde_json::Map::new(),
}
}
}
@ -79,6 +67,17 @@ impl GameSettingsProfile {
}
}
pub fn use_discrete_gpu(&self) -> bool {
cfg_select! {
any(target_os = "linux", target_os = "windows") => self
.os_extra
.as_ref()
.and_then(|extra| extra.use_discrete_gpu)
.unwrap_or(false),
_ => false
}
}
pub fn is_global(&self) -> bool {
self.name == GLOBAL_PROFILE_NAME
}

View file

@ -92,3 +92,6 @@ sysinfo.workspace = true
trash.workspace = true
url.workspace = true
uuid.workspace = true
[target.'cfg(target_os = "linux")'.dependencies]
libc.workspace = true

View file

@ -1,362 +0,0 @@
use std::path::Path;
const VENDOR_NVIDIA: u16 = 0x10de;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Gpu {
pub index: u32,
pub vendor_id: u16,
/// The card the firmware booted with
/// `None` on the non-PCI platforms that never publish the attribute
pub boot_vga: Option<bool>,
pub pci_address: Option<String>,
/// `device/class`; display controllers are `0x03xxxx`
pub class: Option<u32>,
pub has_render_node: bool,
}
impl Gpu {
/// A server's BMC display adapter is a display controller with no render node, and naming it in `DRI_PRIME` drops the game to software rendering
fn can_render(&self) -> bool {
self.has_render_node && self.class.is_none_or(|class| class >> 16 == 0x03)
}
}
/// Empty whenever offload does not apply
pub fn offload_env(gpus: &[Gpu]) -> Vec<(&'static str, String)> {
if gpus.len() < 2 {
return Vec::new();
}
let Some(boot) = gpus.iter().find(|gpu| gpu.boot_vga == Some(true)) else {
return Vec::new();
};
if boot.vendor_id == VENDOR_NVIDIA {
return Vec::new();
}
let Some(target) = gpus
.iter()
.find(|gpu| gpu.boot_vga != Some(true) && gpu.can_render())
else {
return Vec::new();
};
// `prime-run` sets only the NVIDIA variables; `DRI_PRIME` alongside them names a
// card the proprietary driver owns and steers any Mesa path still in play onto nouveau
if target.vendor_id == VENDOR_NVIDIA {
return vec![
("__NV_PRIME_RENDER_OFFLOAD", "1".to_string()),
("__VK_LAYER_NV_optimus", "NVIDIA_only".to_string()),
("__GLX_VENDOR_LIBRARY_NAME", "nvidia".to_string()),
];
}
let prime = target
.pci_address
.as_deref()
.and_then(pci_tag)
.unwrap_or_else(|| "1".to_string());
vec![("DRI_PRIME", prime)]
}
#[cfg(target_os = "linux")]
pub fn detect() -> Vec<Gpu> {
read_drm_devices(Path::new("/sys/class/drm"))
}
fn read_drm_devices(root: &Path) -> Vec<Gpu> {
let Ok(entries) = std::fs::read_dir(root) else {
return Vec::new();
};
let mut gpus = Vec::new();
for entry in entries.flatten() {
let name = entry.file_name();
let Some(name) = name.to_str() else {
continue;
};
// `card0` is a device; `card0-HDMI-A-1` is a connector hanging off it
let Some(index) = name.strip_prefix("card").and_then(|n| n.parse::<u32>().ok()) else {
continue;
};
let device = entry.path().join("device");
let Some(vendor_id) = read_hex(&device.join("vendor")) else {
continue;
};
gpus.push(Gpu {
index,
vendor_id,
boot_vga: read_flag(&device.join("boot_vga")),
pci_address: std::fs::read_link(&device)
.ok()
.and_then(|link| link.file_name()?.to_str().map(str::to_string)),
class: read_class(&device.join("class")),
has_render_node: has_render_node(&device.join("drm")),
});
}
gpus.sort_by_key(|gpu| gpu.index);
gpus
}
fn read_hex(path: &Path) -> Option<u16> {
let raw = std::fs::read_to_string(path).ok()?;
let raw = raw.trim();
u16::from_str_radix(raw.strip_prefix("0x").unwrap_or(raw), 16).ok()
}
fn read_flag(path: &Path) -> Option<bool> {
Some(std::fs::read_to_string(path).ok()?.trim() == "1")
}
/// sysfs spells the PCI class as `0x030000`
fn read_class(path: &Path) -> Option<u32> {
let raw = std::fs::read_to_string(path).ok()?;
let raw = raw.trim();
u32::from_str_radix(raw.strip_prefix("0x").unwrap_or(raw), 16).ok()
}
/// The `drm` directory on the device holds its own `cardN` plus a `renderD*` whenever the card can be rendered on
fn has_render_node(drm: &Path) -> bool {
let Ok(entries) = std::fs::read_dir(drm) else {
return false;
};
entries.flatten().any(|entry| {
entry
.file_name()
.to_str()
.is_some_and(|name| name.starts_with("renderD"))
})
}
/// Mesa builds its tag as `pci-%04x_%02x_%02x_%1u`, which is the sysfs `0000:01:00.0` spelling with the separators swapped
fn pci_tag(address: &str) -> Option<String> {
let (domain, rest) = address.split_once(':')?;
let (bus, rest) = rest.split_once(':')?;
let (device, function) = rest.split_once('.')?;
let widths = [(domain, 4), (bus, 2), (device, 2), (function, 1)];
if widths
.iter()
.any(|(part, width)| part.len() != *width || !part.bytes().all(|b| b.is_ascii_hexdigit()))
{
return None;
}
Some(format!("pci-{domain}_{bus}_{device}_{function}"))
}
#[cfg(test)]
mod tests {
use super::*;
const NVIDIA: u16 = 0x10de;
const AMD: u16 = 0x1002;
const INTEL: u16 = 0x8086;
const ASPEED: u16 = 0x1a03;
fn gpu(index: u32, vendor_id: u16, boot_vga: Option<bool>) -> Gpu {
Gpu {
index,
vendor_id,
boot_vga,
pci_address: None,
class: Some(0x03_0000),
has_render_node: true,
}
}
#[test]
fn a_lone_card_is_never_offloaded_from() {
for vendor in [AMD, NVIDIA, INTEL] {
assert!(offload_env(&[gpu(0, vendor, Some(true))]).is_empty());
}
assert!(offload_env(&[]).is_empty());
}
#[test]
fn an_all_amd_hybrid_never_names_nvidia() {
let env = offload_env(&[gpu(0, AMD, Some(true)), gpu(1, AMD, Some(false))]);
assert_eq!(env, vec![("DRI_PRIME", "1".to_string())]);
}
#[test]
fn an_intel_amd_hybrid_never_names_nvidia() {
let env = offload_env(&[gpu(0, INTEL, Some(true)), gpu(1, AMD, Some(false))]);
assert_eq!(env, vec![("DRI_PRIME", "1".to_string())]);
}
#[test]
fn an_optimus_laptop_gets_the_nvidia_variables() {
let env = offload_env(&[gpu(0, INTEL, Some(true)), gpu(1, NVIDIA, Some(false))]);
assert_eq!(
env,
vec![
("__NV_PRIME_RENDER_OFFLOAD", "1".to_string()),
("__VK_LAYER_NV_optimus", "NVIDIA_only".to_string()),
("__GLX_VENDOR_LIBRARY_NAME", "nvidia".to_string()),
]
);
assert!(
!env.iter().any(|(key, _)| *key == "DRI_PRIME"),
"the proprietary driver owns the card DRI_PRIME would name"
);
}
#[test]
fn a_bmc_display_adapter_is_never_the_offload_target() {
// Workstation: iGPU on the display, the BMC's ASPEED at a lower card index
// than the real dGPU, and no render node on the BMC
let mut bmc = gpu(1, ASPEED, Some(false));
bmc.has_render_node = false;
let mut dgpu = gpu(2, AMD, Some(false));
dgpu.pci_address = Some("0000:c1:00.0".to_string());
let env = offload_env(&[gpu(0, INTEL, Some(true)), bmc, dgpu]);
assert_eq!(env, vec![("DRI_PRIME", "pci-0000_c1_00_0".to_string())]);
}
#[test]
fn a_card_that_is_not_a_display_controller_is_never_the_offload_target() {
let mut compute = gpu(1, AMD, Some(false));
compute.class = Some(0x12_0000);
assert!(offload_env(&[gpu(0, INTEL, Some(true)), compute]).is_empty());
}
#[test]
fn nothing_happens_when_the_discrete_card_already_drives_the_display() {
// Desktop with the monitor on the NVIDIA card and the iGPU still on.
// `DRI_PRIME` would move Mesa onto the integrated one
let env = offload_env(&[gpu(0, NVIDIA, Some(true)), gpu(1, INTEL, Some(false))]);
assert!(env.is_empty());
}
#[test]
fn nothing_happens_when_no_card_claims_to_be_the_boot_gpu() {
let env = offload_env(&[gpu(0, INTEL, None), gpu(1, NVIDIA, None)]);
assert!(env.is_empty());
}
#[test]
fn the_target_card_is_named_by_pci_address_when_sysfs_offers_one() {
let mut target = gpu(1, AMD, Some(false));
target.pci_address = Some("0000:01:00.0".to_string());
let env = offload_env(&[gpu(0, INTEL, Some(true)), target]);
assert_eq!(env[0], ("DRI_PRIME", "pci-0000_01_00_0".to_string()));
}
#[test]
fn a_malformed_pci_address_falls_back_to_the_ordinal() {
let mut target = gpu(1, AMD, Some(false));
target.pci_address = Some("not-an-address".to_string());
let env = offload_env(&[gpu(0, AMD, Some(true)), target]);
assert_eq!(env[0], ("DRI_PRIME", "1".to_string()));
}
#[test]
fn pci_tags_match_the_shape_mesa_builds() {
assert_eq!(pci_tag("0000:01:00.0").as_deref(), Some("pci-0000_01_00_0"));
assert_eq!(pci_tag("10000:af:1f.7").as_deref(), None, "domain is 4 wide");
assert_eq!(pci_tag("0000:1:00.0"), None, "bus is 2 wide");
assert_eq!(pci_tag("0000:01:00"), None, "no function");
assert_eq!(pci_tag("0000:0g:00.0"), None, "not hex");
}
fn write(path: &Path, contents: &str) {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, contents).unwrap();
}
#[test]
fn reads_vendor_boot_flag_and_address_out_of_a_sysfs_tree() {
let scratch = polyio::testing::ScratchDir::new("gpu-sysfs");
let drm = scratch.join("drm");
// sysfs spells these with a trailing newline
write(&drm.join("card0").join("device").join("vendor"), "0x8086\n");
write(&drm.join("card0").join("device").join("boot_vga"), "1\n");
write(&drm.join("card0").join("device").join("class"), "0x030000\n");
write(
&drm.join("card0")
.join("device")
.join("drm")
.join("renderD128")
.join("dev"),
"226:128\n",
);
write(&drm.join("card1").join("device").join("vendor"), "0x10de\n");
write(&drm.join("card1").join("device").join("boot_vga"), "0\n");
write(&drm.join("card1").join("device").join("class"), "0x030000\n");
write(
&drm.join("card1")
.join("device")
.join("drm")
.join("renderD129")
.join("dev"),
"226:129\n",
);
let gpus = read_drm_devices(&drm);
assert_eq!(gpus.len(), 2);
assert_eq!(gpus[0].index, 0);
assert_eq!(gpus[0].vendor_id, INTEL);
assert_eq!(gpus[0].boot_vga, Some(true));
assert_eq!(gpus[1].vendor_id, NVIDIA);
assert_eq!(gpus[1].boot_vga, Some(false));
assert_eq!(gpus[1].class, Some(0x03_0000));
assert!(gpus[1].has_render_node);
// and the whole point: this pair earns the NVIDIA variables
assert_eq!(offload_env(&gpus).len(), 3);
}
#[test]
fn skips_connectors_and_anything_without_a_vendor() {
let scratch = polyio::testing::ScratchDir::new("gpu-connectors");
let drm = scratch.join("drm");
write(&drm.join("card0").join("device").join("vendor"), "0x1002\n");
// a connector, not a device
write(
&drm.join("card0-HDMI-A-1").join("device").join("vendor"),
"0x1002\n",
);
// renderD128 sits alongside the cards and is the same device again
write(&drm.join("renderD128").join("device").join("vendor"), "0x1002\n");
// present but unreadable as a device
std::fs::create_dir_all(drm.join("card9")).unwrap();
let gpus = read_drm_devices(&drm);
assert_eq!(gpus.len(), 1, "one card, counted once");
assert_eq!(gpus[0].vendor_id, AMD);
}
#[test]
fn a_missing_drm_tree_is_not_an_error() {
let scratch = polyio::testing::ScratchDir::new("gpu-missing");
assert!(read_drm_devices(&scratch.join("nope")).is_empty());
}
}

View file

@ -0,0 +1,601 @@
use std::path::{Path, PathBuf};
const INTEL_INTEGRATED: &str = "0000:00:02.0";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Driver {
AmdGpu,
Radeon,
Nouveau,
Nvidia,
I915,
Xe,
Other,
}
impl Driver {
fn from_name(name: Option<&str>) -> Self {
match name {
Some("amdgpu") => Self::AmdGpu,
Some("radeon") => Self::Radeon,
Some("nouveau") => Self::Nouveau,
Some("nvidia") => Self::Nvidia,
Some("i915") => Self::I915,
Some("xe") => Self::Xe,
Some(other) => {
tracing::warn!(driver = other, "unknown graphics driver");
Self::Other
}
None => Self::Other,
}
}
fn vulkan_icd(self) -> Option<&'static str> {
match self {
Self::AmdGpu | Self::Radeon => Some("*radeon*,*amd*"),
Self::Nvidia => Some("*nvidia*"),
Self::I915 | Self::Xe => Some("*intel*"),
Self::Nouveau | Self::Other => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Gpu {
pub pci_address: String,
pub boot_vga: Option<bool>,
pub driver: Driver,
pub render_node: Option<PathBuf>,
pub discrete: bool,
}
impl Gpu {
fn nvidia_proprietary(&self) -> bool {
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![
("__NV_PRIME_RENDER_OFFLOAD", "1".to_string()),
("__VK_LAYER_NV_optimus", "NVIDIA_only".to_string()),
("__GLX_VENDOR_LIBRARY_NAME", "nvidia".to_string()),
]
} else {
let prime = pci_tag(&best.pci_address).unwrap_or_else(|| "1".to_string());
vec![("DRI_PRIME", prime)]
};
if let Some(icd) = best.driver.vulkan_icd() {
env.push(("VK_LOADER_DRIVERS_SELECT", icd.to_string()));
}
env
}
#[cfg(target_os = "linux")]
pub fn detect() -> Vec<Gpu> {
let mut gpus = read_pci_devices(Path::new("/sys/bus/pci/devices"));
for gpu in &mut gpus {
gpu.discrete = probe_discrete(gpu);
}
gpus
}
fn read_pci_devices(root: &Path) -> Vec<Gpu> {
let Ok(entries) = std::fs::read_dir(root) else {
return Vec::new();
};
let mut gpus = Vec::new();
for entry in entries.flatten() {
let name = entry.file_name();
let Some(address) = name.to_str() else {
continue;
};
let device = entry.path();
if read_class(&device.join("class")).is_none_or(|class| class >> 16 != 0x03) {
continue;
}
gpus.push(Gpu {
pci_address: address.to_string(),
boot_vga: read_flag(&device.join("boot_vga")),
driver: Driver::from_name(read_link_name(&device.join("driver")).as_deref()),
render_node: render_node(&device.join("drm")),
discrete: false,
});
}
gpus.sort_by(|a, b| a.pci_address.cmp(&b.pci_address));
gpus
}
fn read_flag(path: &Path) -> Option<bool> {
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 = raw.trim();
u32::from_str_radix(raw.strip_prefix("0x").unwrap_or(raw), 16).ok()
}
fn read_link_name(path: &Path) -> Option<String> {
let link = std::fs::read_link(path).ok()?;
Some(link.file_name()?.to_str()?.to_string())
}
fn render_node(drm: &Path) -> Option<PathBuf> {
let entries = std::fs::read_dir(drm).ok()?;
entries.flatten().find_map(|entry| {
let name = entry.file_name();
name.to_str()?
.starts_with("renderD")
.then(|| Path::new("/dev/dri").join(&name))
})
}
fn pci_tag(address: &str) -> Option<String> {
let (domain, rest) = address.split_once(':')?;
let (bus, rest) = rest.split_once(':')?;
let (device, function) = rest.split_once('.')?;
let widths = [(domain, 4), (bus, 2), (device, 2), (function, 1)];
if widths
.iter()
.any(|(part, width)| part.len() != *width || !part.bytes().all(|b| b.is_ascii_hexdigit()))
{
return None;
}
Some(format!("pci-{domain}_{bus}_{device}_{function}"))
}
fn probe_discrete(gpu: &Gpu) -> bool {
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 {
probe.unwrap_or_else(|error| {
tracing::warn!(
card = gpu.pci_address,
?error,
"no discreteness answer; assuming integrated"
);
false
})
}
#[cfg(not(target_os = "linux"))]
mod drm {
use super::Gpu;
fn unavailable() -> std::io::Result<bool> {
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)]
mod tests {
use super::*;
const AMD_INTEGRATED: &str = "0000:0c:00.0";
fn gpu(address: &str, driver: Driver, boot_vga: Option<bool>) -> Gpu {
let discrete = match driver {
Driver::Nvidia => true,
Driver::I915 => address != INTEL_INTEGRATED,
_ => boot_vga == Some(false),
};
Gpu {
pci_address: address.to_string(),
boot_vga,
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]
fn pci_tags_match_the_shape_mesa_builds() {
assert_eq!(pci_tag("0000:01:00.0").as_deref(), Some("pci-0000_01_00_0"));
assert_eq!(pci_tag("10000:af:1f.7").as_deref(), None, "domain is 4 wide");
assert_eq!(pci_tag("0000:1:00.0"), None, "bus is 2 wide");
assert_eq!(pci_tag("0000:01:00"), None, "no function");
assert_eq!(pci_tag("0000:0g:00.0"), None, "not hex");
}
#[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"
);
}
#[test]
fn the_cards_we_can_place_without_asking_the_driver() {
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"
);
}
#[test]
fn an_unknown_module_name_is_not_fatal() {
assert_eq!(Driver::from_name(Some("asahi")), Driver::Other);
assert_eq!(Driver::from_name(None), Driver::Other);
}
fn write(path: &Path, contents: &str) {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, contents).unwrap();
}
fn write_card(bus: &Path, address: &str, boot_vga: &str, driver: &str) {
let device = bus.join(address);
write(&device.join("boot_vga"), &format!("{boot_vga}\n"));
write(&device.join("class"), "0x030000\n");
let target = bus.join("drivers").join(driver);
std::fs::create_dir_all(&target).unwrap();
std::os::unix::fs::symlink(&target, device.join("driver")).unwrap();
}
#[test]
fn reads_boot_flag_driver_and_render_node_out_of_a_sysfs_tree() {
let scratch = polyio::testing::ScratchDir::new("gpu-sysfs");
let bus = scratch.join("devices");
write_card(&bus, INTEL_INTEGRATED, "1", "i915");
write(
&bus.join(INTEL_INTEGRATED)
.join("drm")
.join("renderD128")
.join("dev"),
"226:128\n",
);
write_card(&bus, "0000:01:00.0", "0", "nvidia");
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:02:00.0").join("class"), "0x020000\n");
std::fs::create_dir_all(bus.join("0000:03:00.0")).unwrap();
let gpus = read_pci_devices(&bus);
assert_eq!(gpus.len(), 1, "one card, counted once");
assert_eq!(gpus[0].driver, Driver::AmdGpu);
}
#[test]
fn a_missing_pci_tree_is_not_an_error() {
let scratch = polyio::testing::ScratchDir::new("gpu-missing");
assert!(read_pci_devices(&scratch.join("nope")).is_empty());
}
}

View file

@ -0,0 +1,30 @@
#[cfg(any(target_os = "linux", test))]
mod linux;
use tokio::process::Command;
#[allow(unused_variables)]
pub async fn prefer_discrete(command: &mut Command, java_path: &str) {
#[cfg(target_os = "linux")]
{
let gpus = linux::detect();
let env = linux::offload_env(&gpus);
if env.is_empty() {
tracing::info!(
gpus = gpus.len(),
"discrete GPU was requested but nothing here is a valid offload target; \
leaving the renderer alone"
);
return;
}
for (key, value) in env {
tracing::debug!(key, value, "offloading the game to the discrete GPU");
command.env(key, value);
}
}
#[cfg(windows)]
oneclient_java::prefer_dedicated_gpu(std::path::Path::new(java_path)).await;
}

View file

@ -341,14 +341,15 @@ async fn start(
);
tracing::debug!(cluster_id, ?jvm_args, main_class = %version_info.main_class, "jvm arguments");
let use_discrete_gpu = state.settings.read().use_discrete_gpu;
if use_discrete_gpu {
oneclient_java::prefer_dedicated_gpu(std::path::Path::new(&java.absolute_path)).await;
let mut command = base_command(&profile, &java.absolute_path);
if profile.use_discrete_gpu() {
crate::game::gpu::prefer_discrete(&mut command, &java.absolute_path).await;
}
let mut command = base_command(&profile, &java.absolute_path);
apply_env(&mut command, &profile);
command
apply_env(&mut command, &profile);
command
.args(jvm_args)
.arg(&version_info.main_class)
.args(mc_args)
@ -683,9 +684,6 @@ fn base_command(profile: &GameSettingsProfile, java_path: &str) -> Command {
fn apply_env(command: &mut Command, profile: &GameSettingsProfile) {
command.env_remove("_JAVA_OPTIONS");
#[cfg(target_os = "linux")]
apply_discrete_gpu(command, profile);
if let Some(env) = &profile.launch_env {
for pair in env.split_whitespace() {
if let Some((key, value)) = pair.split_once('=') {
@ -695,36 +693,6 @@ fn apply_env(command: &mut Command, profile: &GameSettingsProfile) {
}
}
#[cfg(target_os = "linux")]
fn apply_discrete_gpu(command: &mut Command, profile: &GameSettingsProfile) {
let requested = profile
.os_extra
.as_ref()
.and_then(|extra| extra.use_discrete_gpu)
.unwrap_or(false);
if !requested {
return;
}
let gpus = crate::game::gpu::detect();
let env = crate::game::gpu::offload_env(&gpus);
if env.is_empty() {
tracing::info!(
gpus = gpus.len(),
"discrete GPU was requested but nothing here is a valid offload target; \
leaving the renderer alone"
);
return;
}
for (key, value) in env {
tracing::debug!(key, value, "offloading the game to the discrete GPU");
command.env(key, value);
}
}
#[tracing::instrument(skip(cwd), fields(hook), level = "debug")]
async fn run_hook(hook: Option<&str>, cwd: &Path) {
let Some(hook) = hook.map(str::trim).filter(|h| !h.is_empty()) else {

View file

@ -1,7 +1,6 @@
mod analytics;
mod error;
pub mod fabric;
#[cfg(any(target_os = "linux", test))]
mod gpu;
mod heal;
mod launch;

View file

@ -42,7 +42,6 @@ pub struct LauncherSettings {
pub auto_update: bool,
pub crash_reporting: bool,
pub enable_gamemode: bool,
pub use_discrete_gpu: bool,
pub discord_enabled: bool,
pub max_concurrent_requests: usize,
pub global_game_settings: GameSettingsProfile,
@ -86,7 +85,6 @@ impl Default for LauncherSettings {
crash_reporting: true,
discord_enabled: true,
enable_gamemode: false,
use_discrete_gpu: false,
max_concurrent_requests: 25,
global_game_settings: GameSettingsProfile::default_global_profile(),
allow_parallel_running_clusters: false,