feat: prefer the dedicated gpu on windows (#792)

This commit is contained in:
Julian Chang 2026-09-04 17:54:31 +02:00 • committed by LynithDev
parent e40c0185c4
commit 86145c7180
6 changed files with 168 additions and 2 deletions

View file

@ -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,

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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::*;

View file

@ -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,