diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java index a2d85d03fb6..ce7d1af2e4a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl; +import com.sun.jna.Pointer; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; @@ -46,6 +47,8 @@ import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.JarUtils; import org.jackhuang.hmcl.util.platform.*; +import org.jackhuang.hmcl.util.platform.windows.Gdi32; +import org.jackhuang.hmcl.util.platform.windows.User32; import javax.swing.*; import java.awt.*; @@ -376,6 +379,10 @@ private static void setupJavaFXVMOptions() { setUpAnimationFrameRate: { + if (System.getProperty("javafx.animation.pulse") != null) { + break setUpAnimationFrameRate; + } + String animationFrameRate = System.getenv("HMCL_ANIMATION_FRAME_RATE"); if (animationFrameRate != null) { LOG.info("HMCL_ANIMATION_FRAME_RATE: " + animationFrameRate); @@ -390,6 +397,30 @@ private static void setupJavaFXVMOptions() { } break setUpAnimationFrameRate; } + + // To avoid prematurely loading FXUtils, we only check if animationDisabled has been explicitly set to true + if (!Boolean.TRUE.equals(settings().animationDisabledProperty().get())) { + if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) { + if (NativeUtils.USE_JNA && Gdi32.INSTANCE != null && User32.INSTANCE != null) { + Pointer pointer = User32.INSTANCE.GetDC(Pointer.NULL); + if (pointer != null && !Pointer.NULL.equals(pointer)) { + try { + int refreshRate = Gdi32.INSTANCE.GetDeviceCaps(pointer, Gdi32.VREFRESH); + + if (refreshRate > 0) { + LOG.info("Detected refresh rate: " + refreshRate + "Hz"); + + if (refreshRate >= 90) { + System.getProperties().putIfAbsent("javafx.animation.pulse", String.valueOf(refreshRate)); + } + } + } finally { + User32.INSTANCE.ReleaseDC(Pointer.NULL, pointer); + } + } + } + } + } } String uiScale = System.getProperty("hmcl.uiScale", System.getenv("HMCL_UI_SCALE")); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/Gdi32.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/Gdi32.java new file mode 100644 index 00000000000..6118b5d0725 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/Gdi32.java @@ -0,0 +1,48 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.util.platform.windows; + +import com.sun.jna.Pointer; +import com.sun.jna.win32.StdCallLibrary; +import org.jackhuang.hmcl.util.platform.NativeUtils; +import org.jetbrains.annotations.NotNullByDefault; +import org.jetbrains.annotations.Nullable; + +/// Provides mappings for the GDI functions used by HMCL. +/// +/// @author Glavo +@NotNullByDefault +public interface Gdi32 extends StdCallLibrary { + + /// The loaded GDI library, or `null` when JNA is unavailable or the current platform is not Windows. + @Nullable Gdi32 INSTANCE = NativeUtils.USE_JNA && com.sun.jna.Platform.isWindows() + ? NativeUtils.load("gdi32", Gdi32.class) + : null; + + /// The [GetDeviceCaps](https://learn.microsoft.com/windows/win32/api/wingdi/nf-wingdi-getdevicecaps) + /// index for the current vertical refresh rate, in hertz. + int VREFRESH = 116; + + /// Retrieves device-specific information for the supplied device context. + /// + /// @param hdc the device context to query + /// @param index the capability index to retrieve + /// @return the value of the requested capability + /// @see GetDeviceCaps function + int GetDeviceCaps(Pointer hdc, int index); +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/User32.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/User32.java new file mode 100644 index 00000000000..cf35f181b59 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/windows/User32.java @@ -0,0 +1,51 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.util.platform.windows; + +import com.sun.jna.Pointer; +import com.sun.jna.win32.StdCallLibrary; +import org.jackhuang.hmcl.util.platform.NativeUtils; +import org.jetbrains.annotations.NotNullByDefault; +import org.jetbrains.annotations.Nullable; + +/// Provides mappings for the user interface functions used by HMCL. +/// +/// @author Glavo +@NotNullByDefault +public interface User32 extends StdCallLibrary { + + /// The loaded User32 library, or `null` when JNA is unavailable or the current platform is not Windows. + @Nullable User32 INSTANCE = NativeUtils.USE_JNA && com.sun.jna.Platform.isWindows() + ? NativeUtils.load("user32", User32.class) + : null; + + /// Retrieves a device context for a window or for the entire screen. + /// + /// @param hWnd the target window, or `null` to retrieve the screen device context + /// @return the device context, or `null` if the function fails + /// @see GetDC function + @Nullable Pointer GetDC(@Nullable Pointer hWnd); + + /// Releases a device context retrieved by {@link #GetDC(Pointer)}. + /// + /// @param hWnd the same window passed to {@link #GetDC(Pointer)}, or `null` for a screen device context + /// @param hDC the device context to release + /// @return `1` if the device context was released, or `0` if it was not released + /// @see ReleaseDC function + int ReleaseDC(@Nullable Pointer hWnd, Pointer hDC); +}