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:
parent
cad4800835
commit
54980fa73a
11 changed files with 677 additions and 466 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -4619,6 +4619,7 @@ dependencies = [
|
|||
"image",
|
||||
"indicatif",
|
||||
"interfrost",
|
||||
"libc",
|
||||
"md-5 0.10.6",
|
||||
"oneclient_auth",
|
||||
"oneclient_cluster",
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
601
packages/oneclient_core/src/game/gpu/linux.rs
Normal file
601
packages/oneclient_core/src/game/gpu/linux.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
30
packages/oneclient_core/src/game/gpu/mod.rs
Normal file
30
packages/oneclient_core/src/game/gpu/mod.rs
Normal 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;
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
mod analytics;
|
||||
mod error;
|
||||
pub mod fabric;
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
mod gpu;
|
||||
mod heal;
|
||||
mod launch;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue