From 86145c7180e49b264cc47457fc38818dc3aadddb Mon Sep 17 00:00:00 2001 From: Julian Chang Date: Fri, 4 Sep 2026 17:54:31 +0200 Subject: [PATCH] feat: prefer the dedicated gpu on windows (#792) --- .../src/view/app/settings/minecraft.rs | 29 +++- packages/oneclient_core/src/game/launch.rs | 5 + .../oneclient_core/src/settings/launcher.rs | 2 + packages/oneclient_java/src/lib.rs | 2 +- packages/oneclient_java/src/platform.rs | 130 ++++++++++++++++++ packages/oneclient_java/src/service.rs | 2 + 6 files changed, 168 insertions(+), 2 deletions(-) diff --git a/packages/oneclient_app/src/view/app/settings/minecraft.rs b/packages/oneclient_app/src/view/app/settings/minecraft.rs index 6d914d72..19d7de5b 100644 --- a/packages/oneclient_app/src/view/app/settings/minecraft.rs +++ b/packages/oneclient_app/src/view/app/settings/minecraft.rs @@ -1,11 +1,15 @@ use freya::prelude::*; use oneclient_common::Patch; use oneclient_core::settings::{PackageUpdateMode, ProfileUpdate, Resolution}; +#[cfg(windows)] +use oneclient_core::settings::LauncherSettings; use super::settings_page; use crate::components::{ Dropdown, Icon, IconType, TextInput, memory_field, toggle, validate_number, }; +#[cfg(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}; @@ -20,7 +24,8 @@ pub struct SettingsMinecraft; impl Component for SettingsMinecraft { fn render(&self) -> impl IntoElement { - let profile = use_settings_snapshot().settings.global_game_settings; + let settings = use_settings_snapshot().settings; + let profile = settings.global_game_settings.clone(); let dispatch = use_dispatch(); let fullscreen = use_state({ @@ -146,6 +151,14 @@ 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")] let page = page .child(section_header("GRAPHICS")) @@ -218,6 +231,20 @@ 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, diff --git a/packages/oneclient_core/src/game/launch.rs b/packages/oneclient_core/src/game/launch.rs index fd08f19d..d43804f4 100644 --- a/packages/oneclient_core/src/game/launch.rs +++ b/packages/oneclient_core/src/game/launch.rs @@ -294,6 +294,11 @@ pub async fn launch_cluster( ); 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); apply_env(&mut command, &profile); command diff --git a/packages/oneclient_core/src/settings/launcher.rs b/packages/oneclient_core/src/settings/launcher.rs index 6c279618..c334f152 100644 --- a/packages/oneclient_core/src/settings/launcher.rs +++ b/packages/oneclient_core/src/settings/launcher.rs @@ -42,6 +42,7 @@ 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, @@ -82,6 +83,7 @@ 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, diff --git a/packages/oneclient_java/src/lib.rs b/packages/oneclient_java/src/lib.rs index 847ea4cf..26b3cbbf 100644 --- a/packages/oneclient_java/src/lib.rs +++ b/packages/oneclient_java/src/lib.rs @@ -18,7 +18,7 @@ pub use data::{JavaPackage, JavaRuntime, PackageArchive, java_executable_relativ pub use error::{JavaError, JavaResult}; pub use install::{install_package, is_launcher_managed}; pub use locate::{LocatedJava, best_for_major, locate_java}; -pub use platform::{HostArch, HostOs, HostTarget}; +pub use platform::{HostArch, HostOs, HostTarget, prefer_dedicated_gpu}; pub use service::{ AvailableJava, INSTALLABLE_MAJORS, JAVA_CHOICE_DOWNLOAD, JAVA_CHOICE_FOLDER, JAVA_VENDOR_HINT, JavaService, diff --git a/packages/oneclient_java/src/platform.rs b/packages/oneclient_java/src/platform.rs index af8d3f0c..8e23395c 100644 --- a/packages/oneclient_java/src/platform.rs +++ b/packages/oneclient_java/src/platform.rs @@ -76,6 +76,136 @@ impl HostTarget { } } +#[cfg(windows)] +const GPU_PREFERENCES_KEY: &str = r"HKCU\Software\Microsoft\DirectX\UserGpuPreferences"; + +/// A JVM is started through either stub so both are registered +#[cfg(windows)] +const GPU_PREFERENCE_EXECUTABLES: [&str; 2] = ["javaw.exe", "java.exe"]; + +/// Absolute so a `reg.exe` earlier in the search order cannot run in its place +#[cfg(windows)] +fn reg_command() -> tokio::process::Command { + const NO_WINDOW: u32 = 0x0800_0000; + + let system_root = std::env::var_os("SystemRoot").unwrap_or_else(|| r"C:\Windows".into()); + let exe = std::path::Path::new(&system_root) + .join("System32") + .join("reg.exe"); + + let mut command = tokio::process::Command::new(exe); + command.creation_flags(NO_WINDOW); + command +} + +#[cfg(windows)] +fn registered_value<'a>(query: &'a str, name: &str) -> Option<&'a str> { + query.lines().find_map(|line| { + let (value_name, data) = line.split_once("REG_SZ")?; + if value_name.trim().eq_ignore_ascii_case(name) { + Some(data.trim()) + } else { + None + } + }) +} + +/// 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)] +fn has_explicit_preference(data: &str) -> bool { + 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 { return }; + + let mut targets = Vec::new(); + for exe in GPU_PREFERENCE_EXECUTABLES { + let path = dir.join(exe); + if polyio::try_exists(&path).await.unwrap_or(false) { + targets.push(path); + } + } + + if targets.is_empty() { + return; + } + + // One query for the whole key instead of one per executable + let query = match reg_command() + .args(["query", GPU_PREFERENCES_KEY]) + .output() + .await + { + Ok(out) => String::from_utf8_lossy(&out.stdout).into_owned(), + Err(err) => { + tracing::warn!(%err, "could not run reg.exe"); + return; + } + }; + + for path in targets { + if registered_value(&query, &path.to_string_lossy()).is_some_and(has_explicit_preference) { + continue; + } + + let added = reg_command() + .args(["add", GPU_PREFERENCES_KEY, "/v"]) + .arg(&path) + .args(["/t", "REG_SZ", "/d", "GpuPreference=2;", "/f"]) + .output() + .await; + + match added { + Ok(out) if out.status.success() => { + tracing::info!(path = %path.display(), "registered JVM for high-performance GPU"); + } + Ok(out) => tracing::warn!( + path = %path.display(), + stderr = %String::from_utf8_lossy(&out.stderr).trim(), + "could not set GPU preference" + ), + Err(err) => tracing::warn!(%err, "could not run reg.exe"), + } + } +} + +/// Runtimes install into version-stamped folders so without this the key keeps +/// a dead entry for every upgrade +#[cfg(windows)] +pub async fn forget_dedicated_gpu(java_path: &std::path::Path) { + let Some(dir) = java_path.parent() else { return }; + + for exe in GPU_PREFERENCE_EXECUTABLES { + let path = dir.join(exe); + let removed = reg_command() + .args(["delete", GPU_PREFERENCES_KEY, "/v"]) + .arg(&path) + .arg("/f") + .output() + .await; + + match removed { + Ok(out) if out.status.success() => { + tracing::info!(path = %path.display(), "removed GPU preference"); + } + // A value that was never written is reported as an error + Ok(_) => {} + Err(err) => tracing::warn!(%err, "could not run reg.exe"), + } + } +} + +#[cfg(not(windows))] +pub async fn prefer_dedicated_gpu(_java_path: &std::path::Path) {} + +#[cfg(not(windows))] +pub async fn forget_dedicated_gpu(_java_path: &std::path::Path) {} + #[cfg(test)] mod tests { use super::*; diff --git a/packages/oneclient_java/src/service.rs b/packages/oneclient_java/src/service.rs index 1aa42d9a..50e94a48 100644 --- a/packages/oneclient_java/src/service.rs +++ b/packages/oneclient_java/src/service.rs @@ -156,6 +156,8 @@ impl JavaService { pub async fn remove_runtime(&self, absolute_path: &str) -> JavaResult<()> { self.store.delete_by_path(absolute_path).await?; + crate::platform::forget_dedicated_gpu(Path::new(absolute_path)).await; + let removed_files = match crate::install::remove_installed_package(Path::new(absolute_path)).await { Ok(removed) => removed,