Conversation
There was a problem hiding this comment.
Pull request overview
Adds an optional Windows-only launch behavior to prefer the high-performance GPU by temporarily writing UserGpuPreferences registry values, exposed via a new launcher setting.
Changes:
- Extend the Windows registry helper (WinReg/JNA) to support setting and deleting REG_SZ values.
- Add a new persisted config flag and settings UI toggle for “high-performance GPU” on Windows.
- Hook game launch flow to create (and later remove) the relevant
UserGpuPreferencesregistry entry.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinReg.java | Adds setValue / deleteValue APIs and JNA implementations for registry writes. |
| HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/WinConstants.java | Adds registry access/constants needed for write/delete operations. |
| HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/Advapi32.java | Adds JNA signatures for RegCreateKeyExW / RegSetValueExW / RegDeleteValueW. |
| HMCL/src/main/resources/assets/lang/I18N*.properties | Adds i18n strings for the new Windows GPU preference setting. |
| HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java | Adds a new toggle to the settings UI bound to the new config property. |
| HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java | Persists the new windowsHighPerformance flag. |
| HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java | Writes/deletes the GPU preference registry value around the launch lifecycle. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| windowsHighPerformance.setTitle(i18n("settings.launcher.windows_gpu_preferences")); | ||
| windowsHighPerformance.selectedProperty().bindBidirectional(config().windowsHighPerformanceProperty()); | ||
|
|
||
| settingsPane.getContent().add(windowsHighPerformance); |
There was a problem hiding this comment.
This Windows-specific toggle is currently shown on all platforms. Since enabling it on non-Windows can’t have an effect (and currently can even trigger Windows-registry code paths during launch), consider only adding this control when OperatingSystem.CURRENT_OS == WINDOWS, or disabling it with an explanatory subtitle when not on Windows.
| settingsPane.getContent().add(windowsHighPerformance); | |
| if (System.getProperty("os.name").toLowerCase(Locale.ROOT).startsWith("windows")) { | |
| settingsPane.getContent().add(windowsHighPerformance); | |
| } |
| public abstract boolean setValue(HKEY root, String key, String valueName, String value); | ||
|
|
||
| public abstract boolean deleteValue(HKEY root, String key, String valueName); |
There was a problem hiding this comment.
New WinReg mutation APIs (setValue/deleteValue) are introduced, but the existing WinRegTest only covers querying. Please add JUnit coverage for setting a REG_SZ value and deleting it (including verifying behavior when the value/key does not exist), so regressions in JNA signatures/encoding are caught.
| Object current = WinReg.INSTANCE.queryValue( | ||
| WinReg.HKEY.HKEY_CURRENT_USER, | ||
| "Software\\Microsoft\\DirectX\\UserGpuPreferences", | ||
| FileUtils.getAbsolutePath(javaVersionRef.get().getBinary()) | ||
| ); | ||
| if (current instanceof String) { | ||
| gpuRegAlreadyExistsed = true; | ||
| } else { | ||
| WinReg.INSTANCE.setValue( | ||
| WinReg.HKEY.HKEY_CURRENT_USER, | ||
| "Software\\Microsoft\\DirectX\\UserGpuPreferences", | ||
| FileUtils.getAbsolutePath(javaVersionRef.get().getBinary()), | ||
| "GpuPreference=2;" | ||
| ); |
There was a problem hiding this comment.
WinReg.INSTANCE can be null (e.g., non-Windows or JNA disabled). This code calls WinReg.INSTANCE.queryValue/setValue without checking for null or OperatingSystem.WINDOWS, which can cause an immediate NPE when the setting is enabled. Please guard this block with an OS check and a WinReg reg = WinReg.INSTANCE; if (reg == null) { ... } fallback (and consider logging if the registry write is unavailable).
| Object current = WinReg.INSTANCE.queryValue( | |
| WinReg.HKEY.HKEY_CURRENT_USER, | |
| "Software\\Microsoft\\DirectX\\UserGpuPreferences", | |
| FileUtils.getAbsolutePath(javaVersionRef.get().getBinary()) | |
| ); | |
| if (current instanceof String) { | |
| gpuRegAlreadyExistsed = true; | |
| } else { | |
| WinReg.INSTANCE.setValue( | |
| WinReg.HKEY.HKEY_CURRENT_USER, | |
| "Software\\Microsoft\\DirectX\\UserGpuPreferences", | |
| FileUtils.getAbsolutePath(javaVersionRef.get().getBinary()), | |
| "GpuPreference=2;" | |
| ); | |
| if (!SYSTEM_PLATFORM.isWindows()) { | |
| LOG.warning("Windows high performance GPU setting is enabled, but current platform is not Windows; skipping registry configuration."); | |
| } else { | |
| WinReg reg = WinReg.INSTANCE; | |
| if (reg == null) { | |
| LOG.warning("Windows registry access (WinReg) is unavailable; cannot apply high performance GPU preference."); | |
| } else { | |
| Object current = reg.queryValue( | |
| WinReg.HKEY.HKEY_CURRENT_USER, | |
| "Software\\Microsoft\\DirectX\\UserGpuPreferences", | |
| FileUtils.getAbsolutePath(javaVersionRef.get().getBinary()) | |
| ); | |
| if (current instanceof String) { | |
| gpuRegAlreadyExistsed = true; | |
| } else { | |
| reg.setValue( | |
| WinReg.HKEY.HKEY_CURRENT_USER, | |
| "Software\\Microsoft\\DirectX\\UserGpuPreferences", | |
| FileUtils.getAbsolutePath(javaVersionRef.get().getBinary()), | |
| "GpuPreference=2;" | |
| ); | |
| } | |
| } |
| WinReg.INSTANCE.deleteValue( | ||
| WinReg.HKEY.HKEY_CURRENT_USER, | ||
| "Software\\Microsoft\\DirectX\\UserGpuPreferences", | ||
| process.getProcess().info().command().orElseThrow() | ||
| ); |
There was a problem hiding this comment.
The registry value name used for cleanup should match exactly the value name used when setting it. Here the value name is derived from javaVersionRef.get().getBinary() during setValue, but cleanup uses process.getProcess().info().command().orElseThrow(), which may differ (quoting/canonicalization) or be empty and throw, leaving the registry entry behind or breaking finishLaunch. Store the exact value name string you used for setValue and reuse it for deletion, and avoid orElseThrow() here.
| WinReg.INSTANCE.deleteValue( | |
| WinReg.HKEY.HKEY_CURRENT_USER, | |
| "Software\\Microsoft\\DirectX\\UserGpuPreferences", | |
| process.getProcess().info().command().orElseThrow() | |
| ); | |
| String valueName = null; | |
| JavaRuntime javaRuntime = javaVersionRef.get(); | |
| if (javaRuntime != null && javaRuntime.getBinary() != null) { | |
| valueName = javaRuntime.getBinary().toString(); | |
| } | |
| if (valueName != null) { | |
| WinReg.INSTANCE.deleteValue( | |
| WinReg.HKEY.HKEY_CURRENT_USER, | |
| "Software\\Microsoft\\DirectX\\UserGpuPreferences", | |
| valueName | |
| ); | |
| } |
| try { | ||
| Thread.sleep(5000L); | ||
| } catch (InterruptedException ignored) {} | ||
| WinReg.INSTANCE.deleteValue( | ||
| WinReg.HKEY.HKEY_CURRENT_USER, | ||
| "Software\\Microsoft\\DirectX\\UserGpuPreferences", | ||
| process.getProcess().info().command().orElseThrow() | ||
| ); |
There was a problem hiding this comment.
finishLaunch() performs a 5s Thread.sleep and ignores interrupts. This method is invoked from onLog() while holding lock (see onLog around the finishLaunch() call), so sleeping here can block log processing and hold the lock for 5 seconds. Consider scheduling the delayed registry cleanup asynchronously (after releasing the lock), and if catching InterruptedException, restore the interrupt flag with Thread.currentThread().interrupt().
| try { | |
| Thread.sleep(5000L); | |
| } catch (InterruptedException ignored) {} | |
| WinReg.INSTANCE.deleteValue( | |
| WinReg.HKEY.HKEY_CURRENT_USER, | |
| "Software\\Microsoft\\DirectX\\UserGpuPreferences", | |
| process.getProcess().info().command().orElseThrow() | |
| ); | |
| CompletableFuture.runAsync(() -> { | |
| try { | |
| Thread.sleep(5000L); | |
| } catch (InterruptedException e) { | |
| Thread.currentThread().interrupt(); | |
| return; | |
| } | |
| WinReg.INSTANCE.deleteValue( | |
| WinReg.HKEY.HKEY_CURRENT_USER, | |
| "Software\\Microsoft\\DirectX\\UserGpuPreferences", | |
| process.getProcess().info().command().orElseThrow() | |
| ); | |
| }); |
| private boolean showLogs; | ||
| private QuickPlayOption quickPlayOption; | ||
| private boolean disableOfflineSkin = false; | ||
| private boolean gpuRegAlreadyExistsed = false; |
There was a problem hiding this comment.
gpuRegAlreadyExistsed is written during the async launch task and read later from the process listener thread; without any synchronization it may not be safely published between threads. Make this flag volatile/AtomicBoolean, or move it (and any related state like the registry value name) into the HMCLProcessListener instance where it’s used.
| private boolean gpuRegAlreadyExistsed = false; | |
| private volatile boolean gpuRegAlreadyExistsed = false; |
| private boolean showLogs; | ||
| private QuickPlayOption quickPlayOption; | ||
| private boolean disableOfflineSkin = false; | ||
| private boolean gpuRegAlreadyExistsed = false; |
There was a problem hiding this comment.
Typo in new field name: gpuRegAlreadyExistsed is grammatically incorrect and makes the intent harder to read. Rename to something like gpuRegAlreadyExisted or gpuRegValueAlreadyExists.
| private boolean gpuRegAlreadyExistsed = false; | |
| private boolean gpuRegValueAlreadyExists = false; |
No description provided.