Compare commits

...
13 Commits
Author SHA1 Message Date
Glavo c9b1cfec5a [release/3.13] 修复 CurseForge 源中内容不显示 Minecraft 26.x 版本的问题 (#6003)
https://github.com/HMCL-dev/HMCL/pull/5994
2026-04-25 20:50:08 +08:00
辞庐 82460ebbcb fix: 下载页面标签显示未进行 shouldDisplayCategory 判断 (#5946) 2026-04-12 21:50:40 +08:00
辞庐 3e8a9bf1c4 清理 DecoratorSkin 冗余 separator (#5947) 2026-04-12 21:50:26 +08:00
辞庐andGlavo a2db925e86 优化 MenuSeparator 颜色 (#5944)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-04-12 21:44:42 +08:00
辞庐 fe70cc3daf fix: 模组名字过长导致下载页面布局异常 (#5943) 2026-04-12 21:40:05 +08:00
gwdsirandGlavo 5991112cd8 .gitignore 增加 /.gemini/ (#5927)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-04-12 21:37:19 +08:00
辞庐 084d91c62a 增大自动安装界面加载器名称版本号间距 (#5945) 2026-04-12 21:03:01 +08:00
辞庐 4963f7a5f0 上传微软账户皮肤时校验大小 (#5949) 2026-04-12 20:57:03 +08:00
Glavo f42d5219c2 支持更多 Vulkan 渲染器 (#5940) 2026-04-12 20:41:39 +08:00
Glavo 101a9b5d5e 禁止在 LogWindow 中显示横向滚动条 (#5935) 2026-04-11 00:43:35 +08:00
Glavo 7cd79a4e2a Update versions.txt to 26.1.2 (#5934) 2026-04-10 22:49:40 +08:00
Glavoand3gf8jv4dv 30c3acfb23 增强对 Vulkan 的支持 (#5930)
1. 优化渲染器选项描述;
2. 添加 Vulkan 相关的渲染器选项。

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2026-04-10 22:19:51 +08:00
Glavo 31f04db555 支持将新格式快照版本号翻译为文言 (#5931) 2026-04-09 00:16:57 +08:00
36 changed files with 1073 additions and 144 deletions

No files matched your search

+5 -1
View File
@@ -64,4 +64,8 @@ language-subtag-registry
# test
/hmcl.json
/.hmcl.json
/.hmcl/
/.hmcl/
# AI
/.gemini/
!/.gemini/config.yaml
@@ -430,6 +430,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
.setNativesDirType(vs.getNativesDirType())
.setNativesDir(vs.getNativesDir())
.setProcessPriority(vs.getProcessPriority())
.setGraphicsBackend(vs.getGraphicsBackend())
.setRenderer(vs.getRenderer())
.setEnableDebugLogOutput(vs.isEnableDebugLogOutput())
.setUseNativeGLFW(vs.isUseNativeGLFW())
@@ -173,27 +173,27 @@ public final class LauncherHelper {
}
}),
Task.composeAsync(() -> {
Renderer renderer = setting.getRenderer();
if (renderer != Renderer.DEFAULT && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.SYSTEM_VERSION);
if (lib == null)
return null;
Path file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
if (file.toAbsolutePath().toString().indexOf('=') >= 0) {
LOG.warning("Invalid character '=' in the libraries directory path, unable to attach software renderer loader");
return null;
}
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS
|| !(setting.getRenderer() instanceof Renderer.Driver renderer)
|| renderer.mesaDriverName() == null)
return null;
String agent = FileUtils.getAbsolutePath(file) + "=" + renderer.name().toLowerCase(Locale.ROOT);
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.SYSTEM_VERSION);
if (lib == null)
return null;
Path file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
if (file.toAbsolutePath().toString().indexOf('=') >= 0) {
LOG.warning("Invalid character '=' in the libraries directory path, unable to attach software renderer loader");
return null;
}
if (GameLibrariesTask.shouldDownloadLibrary(repository, version.get(), lib, integrityCheck)) {
return new LibraryDownloadTask(dependencyManager, file, lib)
.thenRunAsync(() -> javaAgents.add(agent));
} else {
javaAgents.add(agent);
return null;
}
String agent = FileUtils.getAbsolutePath(file) + "=" + renderer.mesaDriverName();
if (GameLibrariesTask.shouldDownloadLibrary(repository, version.get(), lib, integrityCheck)) {
return new LibraryDownloadTask(dependencyManager, file, lib)
.thenRunAsync(() -> javaAgents.add(agent));
} else {
javaAgents.add(agent);
return null;
}
})
@@ -232,6 +232,7 @@ public final class LauncherHelper {
if (quickPlayOption != null) {
launchOptionsBuilder.setQuickPlayOption(quickPlayOption);
}
LaunchOptions launchOptions = launchOptionsBuilder.create();
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModsDirectory(selectedVersion), 10));
@@ -603,6 +603,20 @@ public final class VersionSetting implements Cloneable, Observable {
processPriorityProperty.set(processPriority);
}
private final ObjectProperty<GraphicsAPI> graphicsBackend = new SimpleObjectProperty<>(this, "graphicsBackend", GraphicsAPI.DEFAULT);
public ObjectProperty<GraphicsAPI> graphicsBackendProperty() {
return graphicsBackend;
}
public GraphicsAPI getGraphicsBackend() {
return graphicsBackendProperty().get();
}
public void setGraphicsBackend(GraphicsAPI api) {
graphicsBackendProperty().set(api);
}
private final ObjectProperty<Renderer> rendererProperty = new SimpleObjectProperty<>(this, "renderer", Renderer.DEFAULT);
public Renderer getRenderer() {
@@ -820,8 +834,9 @@ public final class VersionSetting implements Cloneable, Observable {
}
obj.addProperty("java", java);
obj.addProperty("graphicsBackend", src.getGraphicsBackend().name());
obj.addProperty("renderer", src.getRenderer().name());
if (src.getRenderer() == Renderer.LLVMPIPE)
if (src.getRenderer() == Renderer.OpenGL.LLVMPIPE)
obj.addProperty("useSoftwareRenderer", true);
return obj;
@@ -894,16 +909,19 @@ public final class VersionSetting implements Cloneable, Observable {
}
vs.setRenderer(Optional.ofNullable(obj.get("renderer")).map(JsonElement::getAsString)
.map(Renderer::of).orElseGet(() -> {
boolean useSoftwareRenderer = Optional.ofNullable(obj.get("useSoftwareRenderer")).map(JsonElement::getAsBoolean).orElse(false);
return useSoftwareRenderer ? Renderer.OpenGL.LLVMPIPE : Renderer.DEFAULT;
}));
vs.setGraphicsBackend(Optional.ofNullable(obj.get("graphicsBackend")).map(JsonElement::getAsString)
.flatMap(name -> {
try {
return Optional.of(Renderer.valueOf(name.toUpperCase(Locale.ROOT)));
return Optional.of(GraphicsAPI.valueOf(name.toUpperCase(Locale.ROOT)));
} catch (IllegalArgumentException ignored) {
return Optional.empty();
}
}).orElseGet(() -> {
boolean useSoftwareRenderer = Optional.ofNullable(obj.get("useSoftwareRenderer")).map(JsonElement::getAsBoolean).orElse(false);
return useSoftwareRenderer ? Renderer.LLVMPIPE : Renderer.DEFAULT;
}));
}).orElseGet(() -> vs.getRenderer() instanceof Renderer.Driver renderer ? renderer.api() : GraphicsAPI.DEFAULT));
return vs;
}
@@ -170,6 +170,7 @@ public final class LogWindow extends Stage {
LogWindowImpl() {
getStyleClass().add("log-window");
listView.getStyleClass().add("no-horizontal-scrollbar");
listView.getProperties().put("no-smooth-scrolling", true);
listView.setItems(FXCollections.observableList(new CircularArrayList<>(logs.size())));
@@ -164,6 +164,9 @@ public class AccountListItem extends RadioButton {
if (skinImg.isError()) {
throw new InvalidSkinException("Failed to read skin image", skinImg.getException());
}
if (skinImg.getWidth() != 64 || (skinImg.getHeight() != 32 && skinImg.getHeight() != 64)) {
throw new InvalidSkinException("Invalid skin size");
}
NormalizedSkin skin = new NormalizedSkin(skinImg);
String model = skin.isSlim() ? "slim" : "";
LOG.info("Uploading skin [" + selectedFile + "], model [" + model + "]");
@@ -28,6 +28,7 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.input.*;
import javafx.scene.layout.StackPane;
@@ -49,6 +50,8 @@ public class LineSelectButton<T> extends LineButton {
private static final PseudoClass SELECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("selected");
private JFXPopup popup;
@SuppressWarnings({"FieldCanBeLocal", "unused"})
private ObservableList<Node> popupItems; // keep a reference
public LineSelectButton() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
@@ -87,7 +90,7 @@ public class LineSelectButton<T> extends LineButton {
ripplerContainer.addEventFilter(ScrollEvent.ANY, ignored -> popup.hide());
Bindings.bindContent(popupMenu.getContent(), MappedObservableList.create(itemsProperty(), item -> {
Bindings.bindContent(popupMenu.getContent(), popupItems = MappedObservableList.create(itemsProperty(), item -> {
VBox vbox = new VBox();
var itemTitleLabel = new Label();
@@ -19,8 +19,8 @@ package org.jackhuang.hmcl.ui.construct;
import javafx.geometry.Insets;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import org.jackhuang.hmcl.theme.Themes;
public class MenuSeparator extends StackPane {
@@ -28,7 +28,7 @@ public class MenuSeparator extends StackPane {
Rectangle rect = new Rectangle();
rect.widthProperty().bind(widthProperty().add(-14));
rect.setHeight(1);
rect.setFill(Color.GRAY);
rect.fillProperty().bind(Themes.colorSchemeProperty().getOutlineVariant());
maxHeightProperty().set(10);
setPadding(new Insets(3));
getChildren().setAll(rect);
@@ -147,6 +147,10 @@ public class TwoLineListItem extends VBox {
subtitleProperty().set(subtitle);
}
public HBox getFirstLine() {
return firstLine;
}
public Label getTitleLabel() {
return lblTitle;
}
@@ -168,6 +172,7 @@ public class TwoLineListItem extends VBox {
Bindings.bindContent(tagsBox.getChildren(), tags);
var scrollPane = new ScrollPane(tagsBox);
scrollPane.setMinSize(0, 0);
HBox.setHgrow(scrollPane, Priority.ALWAYS);
lblTitle.setMinWidth(Label.USE_PREF_SIZE);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
@@ -353,12 +353,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
refreshNavButton.onActionProperty().bind(skinnable.onRefreshNavButtonActionProperty());
skinnable.forbidDraggingWindow(refreshNavButton);
Rectangle separator = new Rectangle();
separator.visibleProperty().bind(refreshNavButton.visibleProperty());
separator.heightProperty().bind(navBar.heightProperty());
separator.setFill(Color.GRAY);
navRight.getChildren().setAll(refreshNavButton, separator);
navRight.getChildren().setAll(refreshNavButton);
navBar.setRight(navRight);
}
}
@@ -25,6 +25,7 @@ import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.game.GraphicsAPI;
import org.jackhuang.hmcl.game.NativesDirectoryType;
import org.jackhuang.hmcl.game.Renderer;
import org.jackhuang.hmcl.setting.Profile;
@@ -36,6 +37,7 @@ import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.nio.file.FileSystems;
@@ -69,6 +71,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
private final ComponentSublist nativesDirSublist;
private final MultiFileItem<NativesDirectoryType> nativesDirItem;
private final MultiFileItem.FileOption<NativesDirectoryType> nativesDirCustomOption;
private final LineSelectButton<GraphicsAPI> graphicsBackendPane;
private final LineSelectButton<Renderer> rendererPane;
public AdvancedVersionSettingPage(Profile profile, @Nullable String versionId, VersionSetting versionSetting) {
@@ -79,6 +82,10 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
versionId == null ? i18n("settings.advanced") : i18n("settings.advanced.title", versionId)
));
@Nullable GameVersionNumber gameVersion = versionId != null
? GameVersionNumber.asGameVersion(profile.getRepository().getGameVersion(versionId))
: null;
this.getStyleClass().add("gray-background");
ScrollPane scrollPane = new ScrollPane();
@@ -185,6 +192,25 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
nativesDirHint.setText(i18n("settings.advanced.natives_directory.hint"));
nativesDirItem.getChildren().add(nativesDirHint);
graphicsBackendPane = new LineSelectButton<>();
graphicsBackendPane.setTitle(i18n("settings.advanced.graphics_backend"));
graphicsBackendPane.setConverter(backend -> i18n("settings.advanced.graphics_backend." + backend.name().toLowerCase(Locale.ROOT)));
graphicsBackendPane.setDescriptionConverter(backend -> switch (backend) {
case DEFAULT -> i18n("settings.advanced.graphics_backend.default.desc");
case OPENGL -> i18n("settings.advanced.graphics_backend.opengl.desc");
case VULKAN -> {
if (gameVersion == null)
yield i18n("settings.advanced.graphics_backend.vulkan.desc.global");
else if (gameVersion.compareTo("26.2-snapshot-2") < 0)
yield i18n("settings.advanced.graphics_backend.vulkan.desc.unsupported");
else
yield i18n("settings.advanced.graphics_backend.vulkan.desc");
}
default -> null;
});
graphicsBackendPane.setValue(GraphicsAPI.DEFAULT);
graphicsBackendPane.setItems(GraphicsAPI.values());
rendererPane = new LineSelectButton<>();
rendererPane.setTitle(i18n("settings.advanced.renderer"));
rendererPane.setConverter(e -> i18n("settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT)));
@@ -192,7 +218,23 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
String bundleKey = "settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT) + ".desc";
return I18n.hasKey(bundleKey) ? i18n(bundleKey) : null;
});
rendererPane.setItems(Renderer.values());
rendererPane.setValue(Renderer.DEFAULT);
FXUtils.onChangeAndOperate(graphicsBackendPane.valueProperty(), backend -> {
if (backend == null) { // unbind
return;
}
rendererPane.setItems(Renderer.getSupported(backend));
if (backend == GraphicsAPI.DEFAULT) {
rendererPane.setDisable(true);
rendererPane.setValue(Renderer.DEFAULT);
} else {
rendererPane.setDisable(false);
if (!(rendererPane.getValue() instanceof Renderer.Driver driver) || driver.api() != backend)
rendererPane.setValue(Renderer.DEFAULT);
}
});
noJVMArgsPane = new LineToggleButton();
noJVMArgsPane.setTitle(i18n("settings.advanced.no_jvm_args"));
@@ -219,7 +261,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
useNativeOpenALPane.setSubtitle(i18n("settings.advanced.linux_freebsd_only"));
workaroundPane.getContent().setAll(
nativesDirSublist, rendererPane, noJVMArgsPane, noOptimizingJVMArgsPane, noGameCheckPane,
nativesDirSublist, graphicsBackendPane, rendererPane, noJVMArgsPane, noOptimizingJVMArgsPane, noGameCheckPane,
noJVMCheckPane, noNativesPatchPane
);
@@ -252,6 +294,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
FXUtils.bindString(txtPreLaunchCommand, versionSetting.preLaunchCommandProperty());
FXUtils.bindString(txtPostExitCommand, versionSetting.postExitCommandProperty());
rendererPane.valueProperty().bindBidirectional(versionSetting.rendererProperty());
graphicsBackendPane.valueProperty().bindBidirectional(versionSetting.graphicsBackendProperty());
noGameCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckGameProperty());
noJVMCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckJVMProperty());
noJVMArgsPane.selectedProperty().bindBidirectional(versionSetting.noJVMArgsProperty());
@@ -294,6 +337,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
FXUtils.unbind(txtPreLaunchCommand, versionSetting.preLaunchCommandProperty());
FXUtils.unbind(txtPostExitCommand, versionSetting.postExitCommandProperty());
rendererPane.valueProperty().unbindBidirectional(versionSetting.rendererProperty());
graphicsBackendPane.valueProperty().unbindBidirectional(versionSetting.graphicsBackendProperty());
noGameCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckGameProperty());
noJVMCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckJVMProperty());
noJVMArgsPane.selectedProperty().unbindBidirectional(versionSetting.noJVMArgsProperty());
@@ -225,8 +225,10 @@ public class DownloadPage extends Control implements DecoratorPage {
content.setSubtitle(getSkinnable().addon.getDescription());
content.getSubtitleLabel().setWrapText(true);
getSkinnable().addon.getCategories().stream()
.filter(category -> getSkinnable().page.shouldDisplayCategory(category))
.map(category -> getSkinnable().page.getLocalizedCategory(category))
.forEach(content::addTag);
content.getFirstLine().setMinWidth(0);
descriptionPane.getChildren().add(content);
if (getSkinnable().mod != null) {
@@ -189,15 +189,16 @@ public final class NativePatcher {
return version.setLibraries(newLibraries);
}
public static @Nullable Library getWindowsMesaLoader(@NotNull JavaRuntime javaVersion, @NotNull Renderer renderer, @NotNull OSVersion windowsVersion) {
/// @see <a href="https://github.com/HMCL-dev/mesa-loader-windows">Java Mesa Loader for Windows</a>
public static @Nullable Library getWindowsMesaLoader(@NotNull JavaRuntime java, @NotNull Renderer renderer, @NotNull OSVersion windowsVersion) {
if (renderer == Renderer.DEFAULT)
return null;
if (windowsVersion.isAtLeast(OSVersion.WINDOWS_10)) {
return getNatives(javaVersion.getPlatform()).get("mesa-loader");
return getNatives(java.getPlatform()).get("mesa-loader");
} else if (windowsVersion.isAtLeast(OSVersion.WINDOWS_7)) {
if (renderer == Renderer.LLVMPIPE)
return getNatives(javaVersion.getPlatform()).get("software-renderer-loader");
if (renderer == Renderer.OpenGL.LLVMPIPE)
return getNatives(java.getPlatform()).get("software-renderer-loader");
else
return null;
} else {
@@ -96,6 +96,10 @@ public class Translator_lzh extends Translator {
case GA -> {
// do nothing
}
case SNAPSHOT -> {
builder.append("之快照");
appendDigitByDigit(builder, release.getEaVersion().toString());
}
case PRE_RELEASE -> {
builder.append("之預");
appendDigitByDigit(builder, release.getEaVersion().toString());
+1 -1
View File
@@ -414,7 +414,7 @@
}
.installer-item-wrapper .installer-item:list-item > .installer-item-name {
-fx-pref-width: 80px;
-fx-pref-width: 100px;
}
.installer-item-wrapper .installer-item:list-item > .installer-item-status {
@@ -1337,6 +1337,15 @@ settings.advanced.dont_patch_natives=Do not attempt to automatically replace nat
settings.advanced.environment_variables=Environment Variables
settings.advanced.game_dir.default=Default (".minecraft/")
settings.advanced.game_dir.independent=Isolated (".minecraft/versions/<instance name>/", except for assets and libraries)
settings.advanced.graphics_backend=Graphics API
settings.advanced.graphics_backend.default=Default
settings.advanced.graphics_backend.default.desc=Follow the game's settings
settings.advanced.graphics_backend.opengl=OpenGL
settings.advanced.graphics_backend.opengl.desc=Use OpenGL for rendering
settings.advanced.graphics_backend.vulkan=Vulkan
settings.advanced.graphics_backend.vulkan.desc=Use Vulkan for rendering
settings.advanced.graphics_backend.vulkan.desc.global=Use Vulkan for rendering (Since Minecraft 26.2)
settings.advanced.graphics_backend.vulkan.desc.unsupported=Use Vulkan for rendering (not supported for the current game version)
settings.advanced.java_permanent_generation_space=PermGen Space
settings.advanced.java_permanent_generation_space.prompt=in MiB
settings.advanced.jvm=JVM Options
@@ -1379,13 +1388,47 @@ settings.advanced.post_exit_command=Post-exit Command
settings.advanced.post_exit_command.prompt=Commands to execute after the game exits
settings.advanced.renderer=Renderer
settings.advanced.renderer.default=Default
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (Poor performance and compatibility)
settings.advanced.renderer.default.desc=Use System Default
# Vulkan Renderers
settings.advanced.renderer.lavapipe=Mesa Lavapipe
settings.advanced.renderer.lavapipe.desc=Software Vulkan Renderer
settings.advanced.renderer.dozen=Mesa Dozen
settings.advanced.renderer.dozen.desc=Vulkan renderer based on DirectX 12 (Experimental)
settings.advanced.renderer.nvidia_vulkan=NVIDIA
settings.advanced.renderer.nvidia_vulkan.desc=Official Vulkan driver for NVIDIA GPUs
settings.advanced.renderer.nvidia_nvk=NVIDIA (NVK)
settings.advanced.renderer.nvidia_nvk.desc=Open-source Vulkan driver for NVIDIA GPUs
settings.advanced.renderer.amdvlk=AMD
settings.advanced.renderer.amdvlk.desc=Official Vulkan driver for AMD GPUs
settings.advanced.renderer.amd_radv=AMD (RADV)
settings.advanced.renderer.amd_radv.desc=Open-source Vulkan driver for AMD GPUs
settings.advanced.renderer.intel_vulkan=Intel
settings.advanced.renderer.intel_vulkan.desc=Official Vulkan driver for Intel GPUs
settings.advanced.renderer.intel_anv=Intel (ANV)
settings.advanced.renderer.intel_anv.desc=Open-source Vulkan driver for Intel GPUs
settings.advanced.renderer.intel_hasvk=Intel (HASVK)
settings.advanced.renderer.intel_hasvk.desc=Open-source Vulkan driver for older Intel GPUs
settings.advanced.renderer.qualcomm=Qualcomm
settings.advanced.renderer.qualcomm.desc=Official Vulkan driver for Qualcomm Adreno GPUs
settings.advanced.renderer.turnip=Qualcomm (Turnip)
settings.advanced.renderer.turnip.desc=Open-source Vulkan driver for Qualcomm Adreno GPUs
settings.advanced.renderer.moltenvk=MoltenVK
settings.advanced.renderer.moltenvk.desc=Vulkan renderer based on Metal
settings.advanced.renderer.kosmickrisp=KosmicKrisp
settings.advanced.renderer.kosmickrisp.desc=Vulkan renderer based on Metal 4 (Experimental)
settings.advanced.renderer.powervr=PowerVR
settings.advanced.renderer.powervr.desc=Open-source Vulkan driver for PowerVR GPUs (Experimental)
settings.advanced.renderer.panvk=PanVK
settings.advanced.renderer.panvk.desc=Open-source Vulkan driver for Arm Mali GPUs (Experimental)
settings.advanced.renderer.v3dv=V3DV
settings.advanced.renderer.v3dv.desc=Open-source Vulkan driver for Raspberry Pi 4/5 GPUs (Experimental)
# OpenGL Renderers
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=Software (Poor performance, best compatibility)
settings.advanced.renderer.llvmpipe.desc=Software OpenGL renderer
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=OpenGL renderer based on DirectX 12
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (Best performance, poor compatibility)
settings.advanced.renderer.zink.desc=OpenGL renderer based on Vulkan
settings.advanced.server_ip=Server Address
settings.advanced.server_ip.prompt=Automatically join after launching the game
settings.advanced.unsupported_system_options=Settings not applicable to the current system
@@ -1364,13 +1364,9 @@ settings.advanced.post_exit_command=أمر ما بعد الإغلاق
settings.advanced.post_exit_command.prompt=أوامر تُنفَّذ بعد إغلاق اللعبة
settings.advanced.renderer=المُصيِّر
settings.advanced.renderer.default=افتراضي
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (أداء وتوافق ضعيفان)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=برمجي (أداء ضعيف، توافق أفضل)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (أفضل أداء، توافق ضعيف)
settings.advanced.server_ip=عنوان الخادم
settings.advanced.server_ip.prompt=الانضمام تلقائياً بعد تشغيل اللعبة
settings.advanced.unsupported_system_options=إعدادات غير مطبّقة على النظام الحالي
@@ -1268,13 +1268,9 @@ settings.advanced.post_exit_command=Comando post-salida
settings.advanced.post_exit_command.prompt=El comando se ejecuta después de que el juego se detenga
settings.advanced.renderer=Renderizador
settings.advanced.renderer.default=Por defecto
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (Rendimiento y compatibilidad deficientes)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=Software (Bajo rendimiento, máxima compatibilidad)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (Máximo rendimiento, baja compatibilidad)
settings.advanced.server_ip=Dirección del servidor
settings.advanced.server_ip.prompt=Entrar automáticamente después de ejecutar el juego
settings.advanced.unsupported_system_options=Configuración no aplicable al sistema actual
@@ -792,13 +792,9 @@ settings.advanced.post_exit_command=終了後のコマンド
settings.advanced.post_exit_command.prompt=ゲーム終了後に実行されます
settings.advanced.renderer=レンダラー
settings.advanced.renderer.default=デフォルト
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (低パフォーマンス、低互換性)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=ソフトウェア (低速だが、互換性は最高)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (最高パフォーマンス、低互換性)
settings.advanced.server_ip=サーバーアドレス
settings.advanced.server_ip.prompt=ゲームの起動時にサーバーに参加する
settings.advanced.unsupported_system_options=サポートされていないシステムオプション
@@ -1085,13 +1085,9 @@ settings.advanced.post_exit_command=令於戲訖
settings.advanced.post_exit_command.prompt=將後戲訖而行
settings.advanced.renderer=繪器
settings.advanced.renderer.default=本
settings.advanced.renderer.default.desc=開圖庫
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (效與所適皆差,勘誤之用)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=軟繪器 (效差,所適至)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (效至,所適差)
settings.advanced.server_ip=伺服器之址
settings.advanced.server_ip.prompt=本之址,啟訖徑入是伺服器
settings.advanced.unsupported_system_options=不逮今之械網所選
@@ -1259,13 +1259,9 @@ settings.advanced.post_exit_command=Команда после выхода
settings.advanced.post_exit_command.prompt=Команды, которые необходимо выполнить после выхода из игры
settings.advanced.renderer=Рендерер
settings.advanced.renderer.default=По умолчанию
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (Низкая производительность и совместимость)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=ПО (Низкая производительность, лучшая совместимость)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (Лучшая производительность, низкая совместимость)
settings.advanced.server_ip=Адрес сервера
settings.advanced.server_ip.prompt=Присоединяться к серверу при запуске игры
settings.advanced.unsupported_system_options=Настройки, не применимые к текущей системе
@@ -1206,13 +1206,9 @@ settings.advanced.post_exit_command=Команда після виходу
settings.advanced.post_exit_command.prompt=Команди для виконання після виходу з гри
settings.advanced.renderer=Рендерер
settings.advanced.renderer.default=По умолчанию
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (Погана продуктивність та сумісність)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=Програмний (Погана продуктивність, найкраща сумісність)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (Найкраща продуктивність, погана сумісність)
settings.advanced.server_ip=Адреса сервера
settings.advanced.server_ip.prompt=Автоматично приєднатися після запуску гри
settings.advanced.unsupported_system_options=Налаштування, що не застосовуються до поточної системи
@@ -1127,6 +1127,15 @@ settings.advanced.dont_patch_natives=不嘗試自動取代本機庫
settings.advanced.environment_variables=環境變數
settings.advanced.game_dir.default=預設 (".minecraft/")
settings.advanced.game_dir.independent=各實例獨立 (".minecraft/versions/<實例名>/",除 assets、libraries 外)
settings.advanced.graphics_backend=圖形 API
settings.advanced.graphics_backend.default=預設
settings.advanced.graphics_backend.default.desc=遵循遊戲設定
settings.advanced.graphics_backend.opengl=OpenGL
settings.advanced.graphics_backend.opengl.desc=使用 OpenGL 渲染
settings.advanced.graphics_backend.vulkan=Vulkan
settings.advanced.graphics_backend.vulkan.desc=使用 Vulkan 渲染
settings.advanced.graphics_backend.vulkan.desc.global=使用 Vulkan 渲染 (適用於 Minecraft 26.2 及更高版本)
settings.advanced.graphics_backend.vulkan.desc.unsupported=使用 Vulkan 渲染 (不適用於當前遊戲版本)
settings.advanced.java_permanent_generation_space=記憶體永久儲存區域
settings.advanced.java_permanent_generation_space.prompt=單位 MiB
settings.advanced.jvm=Java 虛擬機設定
@@ -1164,15 +1173,49 @@ settings.advanced.process_priority.high=高
settings.advanced.process_priority.high.desc=優先保證遊戲執行,但可能會導致其他程式卡頓
settings.advanced.post_exit_command=遊戲結束後執行指令
settings.advanced.post_exit_command.prompt=將在遊戲結束後呼叫使用
settings.advanced.renderer=繪製器
settings.advanced.renderer=繪製器/驅動
settings.advanced.renderer.default=預設
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (效能與相容性較差,用於除錯)
settings.advanced.renderer.default.desc=使用系統預設繪製器
# Vulkan Renderers
settings.advanced.renderer.lavapipe=Mesa Lavapipe
settings.advanced.renderer.lavapipe.desc=軟體 Vulkan 繪製器
settings.advanced.renderer.dozen=Mesa Dozen
settings.advanced.renderer.dozen.desc=基於 DirectX 12 的 Vulkan 繪製器 (實驗性)
settings.advanced.renderer.nvidia_vulkan=NVIDIA
settings.advanced.renderer.nvidia_vulkan.desc=NVIDIA GPU 的官方 Vulkan 驅動
settings.advanced.renderer.nvidia_nvk=NVIDIA (NVK)
settings.advanced.renderer.nvidia_nvk.desc=NVIDIA GPU 的開源 Vulkan 驅動
settings.advanced.renderer.amdvlk=AMD
settings.advanced.renderer.amdvlk.desc=AMD GPU 的官方 Vulkan 驅動
settings.advanced.renderer.amd_radv=AMD (RADV)
settings.advanced.renderer.amd_radv.desc=AMD GPU 的開源 Vulkan 驅動
settings.advanced.renderer.intel_vulkan=Intel
settings.advanced.renderer.intel_vulkan.desc=Intel GPU 的官方 Vulkan 驅動
settings.advanced.renderer.intel_anv=Intel (ANV)
settings.advanced.renderer.intel_anv.desc=Intel GPU 的開源 Vulkan 驅動
settings.advanced.renderer.intel_hasvk=Intel (HASVK)
settings.advanced.renderer.intel_hasvk.desc=Intel 舊 GPU 的開源 Vulkan 驅動
settings.advanced.renderer.qualcomm=高通
settings.advanced.renderer.qualcomm.desc=高通 Adreno GPU 的官方 Vulkan 驅動
settings.advanced.renderer.turnip=高通 (Turnip)
settings.advanced.renderer.turnip.desc=高通 Adreno GPU 的開源 Vulkan 驅動
settings.advanced.renderer.moltenvk=MoltenVK
settings.advanced.renderer.moltenvk.desc=基於 Metal 的 Vulkan 繪製器
settings.advanced.renderer.kosmickrisp=KosmicKrisp
settings.advanced.renderer.kosmickrisp.desc=基於 Metal 4 的 Vulkan 繪製器 (實驗性)
settings.advanced.renderer.powervr=PowerVR
settings.advanced.renderer.powervr.desc=PowerVR GPU 的開源 Vulkan 驅動 (實驗性)
settings.advanced.renderer.panvk=PanVK
settings.advanced.renderer.panvk.desc=Arm Mali GPU 的開源 Vulkan 驅動 (實驗性)
settings.advanced.renderer.v3dv=V3DV
settings.advanced.renderer.v3dv.desc=樹莓派 4/5 GPU 的開源 Vulkan 驅動 (實驗性)
# OpenGL Renderers
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=軟繪製器 (效能較差,相容性最好)
settings.advanced.renderer.llvmpipe.desc=軟體 OpenGL 繪製器
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=基於 DirectX 12 的 OpenGL 繪製器
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (效能最好,相容性較差)
settings.advanced.renderer.zink.desc=基於 Vulkan 的 OpenGL 繪製器
settings.advanced.server_ip=伺服器位址
settings.advanced.server_ip.prompt=預設,啟動遊戲後直接進入對應伺服器
settings.advanced.unsupported_system_options=不適用於目前系統的選項
@@ -1132,6 +1132,15 @@ settings.advanced.dont_patch_natives=不尝试自动替换本地库
settings.advanced.environment_variables=环境变量
settings.advanced.game_dir.default=默认 (".minecraft/")
settings.advanced.game_dir.independent=各实例独立 (存放在 ".minecraft/versions/<实例名>/",除 assets、libraries 外)
settings.advanced.graphics_backend=图形 API
settings.advanced.graphics_backend.default=默认
settings.advanced.graphics_backend.default.desc=遵循游戏设置
settings.advanced.graphics_backend.opengl=OpenGL
settings.advanced.graphics_backend.opengl.desc=使用 OpenGL 渲染
settings.advanced.graphics_backend.vulkan=Vulkan
settings.advanced.graphics_backend.vulkan.desc=使用 Vulkan 渲染
settings.advanced.graphics_backend.vulkan.desc.global=使用 Vulkan 渲染 (适用于 Minecraft 26.2 及更高版本)
settings.advanced.graphics_backend.vulkan.desc.unsupported=使用 Vulkan 渲染 (不适用于当前游戏版本)
settings.advanced.java_permanent_generation_space=内存永久保存区域
settings.advanced.java_permanent_generation_space.prompt=单位 MiB
settings.advanced.jvm=Java 虚拟机设置
@@ -1169,15 +1178,49 @@ settings.advanced.process_priority.high=高
settings.advanced.process_priority.high.desc=优先保证游戏运行,但可能会导致其他程序卡顿
settings.advanced.post_exit_command=游戏结束后执行命令
settings.advanced.post_exit_command.prompt=将在游戏结束后调用
settings.advanced.renderer=渲染器
settings.advanced.renderer=渲染器/驱动
settings.advanced.renderer.default=默认
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (性能与兼容性较差,用于调试)
settings.advanced.renderer.default.desc=使用系统默认渲染器
# Vulkan Renderers
settings.advanced.renderer.lavapipe=Mesa Lavapipe
settings.advanced.renderer.lavapipe.desc=软件 Vulkan 渲染器
settings.advanced.renderer.dozen=Mesa Dozen
settings.advanced.renderer.dozen.desc=基于 DirectX 12 的 Vulkan 渲染器 (实验性)
settings.advanced.renderer.nvidia_vulkan=NVIDIA
settings.advanced.renderer.nvidia_vulkan.desc=NVIDIA GPU 的官方 Vulkan 驱动
settings.advanced.renderer.nvidia_nvk=NVIDIA (NVK)
settings.advanced.renderer.nvidia_nvk.desc=NVIDIA GPU 的开源 Vulkan 驱动
settings.advanced.renderer.amdvlk=AMD
settings.advanced.renderer.amdvlk.desc=AMD GPU 的官方 Vulkan 驱动
settings.advanced.renderer.amd_radv=AMD (RADV)
settings.advanced.renderer.amd_radv.desc=AMD GPU 的开源 Vulkan 驱动
settings.advanced.renderer.intel_vulkan=Intel
settings.advanced.renderer.intel_vulkan.desc=Intel GPU 的官方 Vulkan 驱动
settings.advanced.renderer.intel_anv=Intel (ANV)
settings.advanced.renderer.intel_anv.desc=Intel GPU 的开源 Vulkan 驱动
settings.advanced.renderer.intel_hasvk=Intel (HASVK)
settings.advanced.renderer.intel_hasvk.desc=Intel 旧 GPU 的开源 Vulkan 驱动
settings.advanced.renderer.qualcomm=高通
settings.advanced.renderer.qualcomm.desc=高通 Adreno GPU 的官方 Vulkan 驱动
settings.advanced.renderer.turnip=高通 (Turnip)
settings.advanced.renderer.turnip.desc=高通 Adreno GPU 的开源 Vulkan 驱动
settings.advanced.renderer.moltenvk=MoltenVK
settings.advanced.renderer.moltenvk.desc=基于 Metal 的 Vulkan 渲染器
settings.advanced.renderer.kosmickrisp=KosmicKrisp
settings.advanced.renderer.kosmickrisp.desc=基于 Metal 4 的 Vulkan 渲染器 (实验性)
settings.advanced.renderer.powervr=PowerVR
settings.advanced.renderer.powervr.desc=PowerVR GPU 的开源 Vulkan 驱动 (实验性)
settings.advanced.renderer.panvk=PanVK
settings.advanced.renderer.panvk.desc=Arm Mali GPU 的开源 Vulkan 驱动 (实验性)
settings.advanced.renderer.v3dv=V3DV
settings.advanced.renderer.v3dv.desc=树莓派 4/5 GPU 的开源 Vulkan 驱动 (实验性)
# OpenGL Renderers
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=软渲染器 (性能较差,兼容性最好)
settings.advanced.renderer.llvmpipe.desc=软件 OpenGL 渲染器
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=基于 DirectX 12 的 OpenGL 渲染器
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (性能最好,兼容性较差)
settings.advanced.renderer.zink.desc=基于 Vulkan 的 OpenGL 渲染器
settings.advanced.server_ip=服务器地址
settings.advanced.server_ip.prompt=默认,启动游戏后可以直接进入对应服务器
settings.advanced.unsupported_system_options=不适用于当前系统的选项
+15 -15
View File
@@ -4403,13 +4403,13 @@
},
"windows-x86_64": {
"mesa-loader": {
"name": "org.glavo:mesa-loader-windows:25.0.3:x64",
"name": "org.glavo:mesa-loader-windows:26.0.4:x64",
"downloads": {
"artifact": {
"path": "org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-x64.jar",
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-x64.jar",
"sha1": "b2552fcc8c98e95b4559c39f2ff87cdb3aaaa513",
"size": 27971214
"path": "org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-x64.jar",
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-x64.jar",
"sha1": "f8da709c59ef61f531c91434ca0e3b4f39202981",
"size": 49873846
}
}
},
@@ -4427,13 +4427,13 @@
},
"windows-x86": {
"mesa-loader": {
"name": "org.glavo:mesa-loader-windows:25.0.3:x86",
"name": "org.glavo:mesa-loader-windows:26.0.4:x86",
"downloads": {
"artifact": {
"path": "org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-x86.jar",
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-x86.jar",
"sha1": "6fd0fd7da88ca6636f735e1e0f6feb7f62f59715",
"size": 23311299
"path": "org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-x86.jar",
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-x86.jar",
"sha1": "ac6afaa8baa7c17468267c09e77e1296ee92d5ed",
"size": 41742113
}
}
}
@@ -4816,13 +4816,13 @@
"com.mojang:text2speech:1.12.4:natives": null,
"com.mojang:text2speech:1.13.9:natives-windows": null,
"mesa-loader": {
"name": "org.glavo:mesa-loader-windows:25.0.3:arm64",
"name": "org.glavo:mesa-loader-windows:26.0.4:arm64",
"downloads": {
"artifact": {
"path": "org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-arm64.jar",
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-arm64.jar",
"sha1": "40ca5de3351bf48b7b9efa5d2598d5b6b4d1ab8e",
"size": 24905270
"path": "org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-arm64.jar",
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-arm64.jar",
"sha1": "5c761a344700a07eaded51c5cf0cde36ee614706",
"size": 43504189
}
}
}
@@ -0,0 +1,33 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
import java.util.Locale;
/// The Graphics API.
public enum GraphicsAPI {
DEFAULT,
OPENGL,
VULKAN;
private final String minecraftArg = name().toLowerCase(Locale.ROOT);
public String getMinecraftArg() {
return minecraftArg;
}
}
@@ -56,6 +56,7 @@ public class LaunchOptions implements Serializable {
private NativesDirectoryType nativesDirType;
private String nativesDir;
private ProcessPriority processPriority = ProcessPriority.NORMAL;
private GraphicsAPI graphicsBackend = GraphicsAPI.DEFAULT;
private Renderer renderer = Renderer.DEFAULT;
private boolean useNativeGLFW;
private boolean useNativeOpenAL;
@@ -244,7 +245,11 @@ public class LaunchOptions implements Serializable {
return processPriority;
}
public Renderer getRenderer() {
public @NotNull GraphicsAPI getGraphicsBackend() {
return graphicsBackend;
}
public @NotNull Renderer getRenderer() {
return renderer;
}
@@ -435,8 +440,13 @@ public class LaunchOptions implements Serializable {
return this;
}
public Builder setRenderer(@NotNull Renderer renderer) {
options.renderer = renderer;
public Builder setGraphicsBackend(GraphicsAPI backend) {
options.graphicsBackend = Objects.requireNonNullElse(backend, GraphicsAPI.DEFAULT);
return this;
}
public Builder setRenderer(Renderer renderer) {
options.renderer = Objects.requireNonNullElse(renderer, Renderer.DEFAULT);
return this;
}
@@ -17,9 +17,529 @@
*/
package org.jackhuang.hmcl.game;
public enum Renderer {
DEFAULT,
ZINK,
LLVMPIPE,
D3D12
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.platform.hardware.GraphicsCard;
import org.jackhuang.hmcl.util.platform.hardware.HardwareVendor;
import org.jackhuang.hmcl.util.platform.macos.HomebrewUtils;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// @author Glavo
@NotNullByDefault
public sealed interface Renderer permits Renderer.Default, Renderer.Driver, Renderer.Unknown {
/// Default renderer.
Default DEFAULT = new Default();
/// Parse a renderer from a string.
static Renderer of(String name) {
if ("DEFAULT".equalsIgnoreCase(name) || name.isBlank())
return DEFAULT;
String upper = name.toUpperCase(Locale.ROOT).trim();
try {
return OpenGL.valueOf(upper);
} catch (IllegalArgumentException ignored) {
}
try {
return Vulkan.valueOf(upper);
} catch (IllegalArgumentException ignored) {
}
return new Unknown(name);
}
/// Get all supported renderers for a given graphics API.
static @Unmodifiable List<Renderer> getSupported(GraphicsAPI api) {
return switch (api) {
case DEFAULT -> List.of(DEFAULT);
case VULKAN -> Vulkan.Holder.SUPPORTED;
case OPENGL -> OpenGL.SUPPORTED;
};
}
String name();
final class Default implements Renderer {
private Default() {
}
@Override
public String name() {
return "DEFAULT";
}
}
sealed interface Driver extends Renderer permits Vulkan, OpenGL {
/// The graphics API that this driver belongs to.
GraphicsAPI api();
/// If this driver can be loaded via [mesa-loader-windows](https://github.com/HMCL-dev/mesa-loader-windows),
/// return the driver name, otherwise return `null`.
@Contract(pure = true)
default @Nullable String mesaDriverName() {
return null;
}
}
enum Vulkan implements Driver {
/// Mesa Lavapipe driver.
///
/// It is a software rasterizer.
///
/// @see <a href="https://docs.mesa3d.org/drivers/llvmpipe.html">LLVMpipe - The Mesa 3D Graphics Library</a>
LAVAPIPE("lvp") {
@Override
public String mesaDriverName() {
return "lavapipe";
}
},
/// Mesa Dozen driver.
///
/// It is a Vulkan driver based on DirectX 12.
///
/// ## Note
/// Currently, Dozen does not support the VK_KHR_push_descriptor feature, so it cannot launch Minecraft 26.2
/// Using Dozen can run Minecraft 1.21.11 + VulkanMod, but it will cause the game to crash after playing for a while
DOZEN("dzn") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() == OperatingSystem.WINDOWS;
}
@Override
public String mesaDriverName() {
return "dzn";
}
},
/// NVIDIA Vulkan driver.
///
/// It is a Vulkan driver for NVIDIA GPUs.
///
/// @see <a href="https://developer.nvidia.com/vulkan">Vulkan Open Standard Modern GPU API | NVIDIA Developer</a>
NVIDIA_VULKAN("nvidia") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return Vulkan.hasCard(cards, HardwareVendor.NVIDIA);
}
},
/// Mesa NVK driver.
///
/// It is a Vulkan driver for NVIDIA GPUs.
///
/// @see <a href="https://docs.mesa3d.org/drivers/nvk.html">NVK - The Mesa 3D Graphics Library</a>
NVIDIA_NVK("nouveau") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() == OperatingSystem.LINUX && Vulkan.hasCard(cards, HardwareVendor.NVIDIA);
}
},
/// AMD Open Source Driver for Vulkan
///
/// It is a Vulkan driver for AMD GPUs.
///
/// @see <a href="https://github.com/GPUOpen-Drivers/AMDVLK">GPUOpen-Drivers/AMDVLK - GitHub</a>
AMDVLK("amd") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() == OperatingSystem.LINUX && Vulkan.hasCard(cards, HardwareVendor.AMD);
}
},
/// Mesa RADV driver.
///
/// It is a Vulkan driver for AMD GCN/RDNA GPUs.
///
/// @see <a href="https://docs.mesa3d.org/drivers/radv.html">RADV - The Mesa 3D Graphics Library</a>
AMD_RADV("radeon") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return Vulkan.hasCard(cards, HardwareVendor.AMD);
}
},
/// Intel Vulkan driver.
///
/// It is a Vulkan driver for Intel GPUs.
INTEL_VULKAN("ig") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() == OperatingSystem.WINDOWS && Vulkan.hasCard(cards, HardwareVendor.INTEL);
}
},
/// Mesa ANV driver.
///
/// It is a Vulkan driver for Intel GPUs.
///
/// @see <a href="https://docs.mesa3d.org/drivers/anv.html">ANV - The Mesa 3D Graphics Library</a>
INTEL_ANV("intel") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() != OperatingSystem.WINDOWS && Vulkan.hasCard(cards, HardwareVendor.INTEL);
}
},
/// Intel HASVK driver.
///
/// It is a Vulkan driver for Intel Gen7 (Ivy Bridge / Haswell) and Gen8 (Broadwell) graphics.
///
/// @see <a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/18208">intel: split vulkan driver between gfx7/8 and above</a>
INTEL_HASVK("intel_hasvk") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() != OperatingSystem.WINDOWS
&& cards != null
&& cards.stream().anyMatch(card -> card.getVendor() == HardwareVendor.INTEL && card.getName().startsWith("Intel HD Graphics "));
}
},
/// Qualcomm Vulkan driver.
///
/// It is a Vulkan driver for Qualcomm Adreno GPUs.
QUALCOMM("qc") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.equals(Platform.WINDOWS_ARM64);
}
},
/// Mesa Turnip driver.
///
/// It is a Vulkan driver for Qualcomm Adreno GPUs.
///
/// @see <a href="https://docs.mesa3d.org/drivers/freedreno.html">Freedreno - The Mesa 3D Graphics Library</a>
TURNIP("freedreno") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() != OperatingSystem.WINDOWS && platform.arch().isArm();
}
},
/// MoltenVK driver.
///
/// It is a Vulkan driver for macOS, iOS, tvOS, and visionOS.
///
/// @see <a href="https://github.com/KhronosGroup/MoltenVK">KhronosGroup/MoltenVK - GitHub</a>
MOLTENVK("MoltenVK") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() == OperatingSystem.MACOS;
}
},
/// Mesa KosmicKrisp driver.
///
/// It is a Vulkan driver for Apple Silicon hardware.
///
/// @see <a href="https://docs.mesa3d.org/drivers/kosmickrisp.html">KosmicKrisp - The Mesa 3D Graphics Library</a>
KOSMICKRISP("kosmickrisp_mesa") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() == OperatingSystem.MACOS && platform.arch() == Architecture.ARM64;
}
},
/// Mesa PowerVR driver.
///
/// It is a Vulkan driver for Imagination Technologies PowerVR GPUs.
///
/// @see <a href="https://docs.mesa3d.org/drivers/powervr.html">PowerVR - The Mesa 3D Graphics Library</a>
POWERVR("powervr") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() == OperatingSystem.LINUX && Vulkan.hasCard(cards, HardwareVendor.IMG);
}
},
/// Mesa PanVK driver.
///
/// It is a Vulkan driver for ARM Mali GPUs.
///
/// @see <a href="https://docs.mesa3d.org/drivers/panfrost.html">Panfrost - The Mesa 3D Graphics Library</a>
PANVK("panfrost") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() == OperatingSystem.LINUX && platform.arch().isArm();
}
},
/// Mesa V3DV driver.
///
/// It is a Vulkan driver for the Raspberry Pi 4 and Raspberry Pi 5.
///
/// @see <a href="https://docs.mesa3d.org/drivers/v3dv.html">V3DV - The Mesa 3D Graphics Library</a>
V3DV("broadcom") {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() == OperatingSystem.LINUX && platform.arch().isArm()
&& Vulkan.hasCard(cards, HardwareVendor.BROADCOM);
}
};
private static final class Holder {
static final List<Renderer> SUPPORTED;
static final Map<Vulkan, Path> DRIVER_TO_ICD_FILE;
static {
var driverToIcdFile = new EnumMap<Vulkan, Path>(Vulkan.class);
var supported = new LinkedHashSet<Renderer>();
// Helper
String archName = switch (Architecture.SYSTEM_ARCH) {
case X86 -> "i686";
case X86_64 -> "x86_64";
case ARM64 -> "aarch64";
default -> Architecture.SYSTEM_ARCH.getCheckedName();
};
var icdFileNamePattern = Pattern.compile("(?<name>[a-zA-Z0-9_-]+)_icd(?:\\." + Pattern.quote(archName) + ")?\\.json");
Map<String, Vulkan> icdNameToDriver = Stream.of(values()).collect(Collectors.toMap(Vulkan::icdName, Function.identity()));
supported.add(DEFAULT);
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
supported.addAll(List.of(LAVAPIPE, DOZEN));
List<GraphicsCard> graphicsCards = SystemInfo.getGraphicsCards();
if (graphicsCards != null) {
EnumSet<Vulkan> foundSupported = EnumSet.noneOf(Vulkan.class);
for (GraphicsCard card : graphicsCards) {
if (!card.getVulkanDriverFiles().isEmpty()) {
for (Path icdFile : card.getVulkanDriverFiles()) {
String fileName = FileUtils.getName(icdFile);
if (!fileName.endsWith(".json"))
continue;
Vulkan driver;
Matcher matcher = icdFileNamePattern.matcher(fileName);
if (matcher.matches()) {
String icdName = matcher.group("name");
driver = icdNameToDriver.get(icdName);
} else {
switch (fileName.substring(0, fileName.length() - ".json".length())) {
case "igvk64" -> {
if (Architecture.SYSTEM_ARCH.getBits() == Bits.BIT_64)
driver = INTEL_VULKAN;
else
continue;
}
case "igvk32" -> {
if (Architecture.SYSTEM_ARCH.getBits() == Bits.BIT_32)
driver = INTEL_VULKAN;
else
continue;
}
case "nv-vk64" -> {
if (Architecture.SYSTEM_ARCH.getBits() == Bits.BIT_64)
driver = NVIDIA_VULKAN;
else
continue;
}
case "nv-vk32" -> {
if (Architecture.SYSTEM_ARCH.getBits() == Bits.BIT_32)
driver = NVIDIA_VULKAN;
else
continue;
}
case "amd-vulkan64" -> {
if (Architecture.SYSTEM_ARCH.getBits() == Bits.BIT_64)
driver = AMDVLK;
else
continue;
}
case "amd-vulkan32" -> {
if (Architecture.SYSTEM_ARCH.getBits() == Bits.BIT_32)
driver = AMDVLK;
else
continue;
}
case "qcvk_icd_arm64x" -> {
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
driver = QUALCOMM;
else
continue;
}
default -> {
continue;
}
}
}
driverToIcdFile.putIfAbsent(driver, icdFile);
foundSupported.add(driver);
}
}
}
supported.addAll(foundSupported);
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
// LWJGL integrates MoltenVK, so it is always available
supported.add(MOLTENVK);
// We need libvulkan.1.dylib to load custom Vulkan drivers
if (Files.isRegularFile(HomebrewUtils.LIB_VULKAN)) {
Path kosmickrispIcd = HomebrewUtils.HOMEBREW_PREFIX.resolve("share/vulkan/icd.d/kosmickrisp_mesa_icd." + archName + ".json");
if (Files.isRegularFile(kosmickrispIcd)) {
driverToIcdFile.put(KOSMICKRISP, kosmickrispIcd);
supported.add(KOSMICKRISP);
}
Path lvpIcd = HomebrewUtils.HOMEBREW_PREFIX.resolve("share/vulkan/icd.d/lvp_icd." + archName + ".json");
if (Files.isRegularFile(lvpIcd)) {
driverToIcdFile.put(LAVAPIPE, lvpIcd);
supported.add(LAVAPIPE);
}
}
} else {
List<Path> icdDirs = switch (OperatingSystem.CURRENT_OS) {
case LINUX -> List.of(
Path.of("/usr/share/vulkan/icd.d"),
Path.of("/etc/vulkan/icd.d")
);
case FREEBSD -> List.of(Path.of("/usr/local/share/vulkan/icd.d"));
default -> List.of();
};
EnumSet<Vulkan> foundSupported = EnumSet.noneOf(Vulkan.class);
for (Path icdDir : icdDirs) {
if (!Files.isDirectory(icdDir))
continue;
try (Stream<Path> stream = Files.list(icdDir)) {
for (Path icdFile : Lang.toIterable(stream)) {
String fileName = icdFile.getFileName().toString();
Matcher matcher = icdFileNamePattern.matcher(fileName);
if (matcher.matches()) {
String icdName = matcher.group("name");
Vulkan driver = icdNameToDriver.get(icdName);
if (driver != null) {
driverToIcdFile.put(driver, icdFile);
if (driver.isSupported(Platform.CURRENT_PLATFORM, SystemInfo.getGraphicsCards())) {
foundSupported.add(driver);
}
}
}
}
} catch (IOException e) {
LOG.warning("Failed to read Vulkan ICD files in " + icdDir, e);
}
}
supported.addAll(foundSupported);
}
SUPPORTED = List.copyOf(supported);
DRIVER_TO_ICD_FILE = Collections.unmodifiableMap(driverToIcdFile);
}
}
private final String icdName;
Vulkan(String icdName) {
this.icdName = icdName;
}
private static boolean hasCard(@Nullable List<GraphicsCard> cards, HardwareVendor vendor) {
return cards != null && cards.stream().anyMatch(card -> card.getVendor() == vendor);
}
@Override
public GraphicsAPI api() {
return GraphicsAPI.VULKAN;
}
@Contract(pure = true)
public String icdName() {
return icdName;
}
/// Get the path to the ICD file for this driver.
///
/// If the ICD file does not exist, return `null`.
@Contract(pure = true)
public @Nullable Path icdFile() {
return Holder.DRIVER_TO_ICD_FILE.get(this);
}
}
enum OpenGL implements Driver {
/// @see <a href="https://docs.mesa3d.org/drivers/llvmpipe.html">LLVMpipe - The Mesa 3D Graphics Library</a>
LLVMPIPE {
@Override
public String mesaDriverName() {
return "llvmpipe";
}
},
ZINK {
@Override
public String mesaDriverName() {
return "zink";
}
},
D3D12 {
@Override
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return platform.os() == OperatingSystem.WINDOWS;
}
@Override
public String mesaDriverName() {
return "d3d12";
}
};
static final List<Renderer> SUPPORTED =
Stream.concat(
Stream.of(DEFAULT),
Stream.of(OpenGL.values()).filter(it -> it.isSupported(
Platform.CURRENT_PLATFORM,
null // We don't need to pass graphics cards for OpenGL yet
))
).toList();
@Override
public GraphicsAPI api() {
return GraphicsAPI.OPENGL;
}
}
/// Unknown renderer.
record Unknown(String name) implements Renderer {
}
/// Whether this renderer is supported on the given platform and graphics cards.
///
/// If `cards` is `null`, it means that the graphics cards are unknown.
default boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
return true;
}
}
@@ -27,6 +27,7 @@ import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.Unzipper;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.platform.macos.HomebrewUtils;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
@@ -104,7 +105,7 @@ public class DefaultLauncher extends Launcher {
// Executable
if (StringUtils.isNotBlank(options.getWrapper()))
res.addAllWithoutParsing(StringUtils.tokenize(options.getWrapper(), getEnvVars()));
res.addAllWithoutParsing(StringUtils.tokenize(options.getWrapper(), getEnvVars(nativeFolder)));
res.add(options.getJava().getBinary().toString());
@@ -265,6 +266,21 @@ public class DefaultLauncher extends Launcher {
res.addDefault("-Dfml.ignorePatchDiscrepancies=", "true");
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
&& options.getRenderer() instanceof Renderer.Driver renderer
&& renderer.mesaDriverName() != null) {
res.addDefault("-Dorg.glavo.mesa.loader.nativeDir=", FileUtils.getAbsolutePath(nativeFolder.resolve("mesa-loader")));
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
&& options.getJava().getArchitecture() == Architecture.SYSTEM_ARCH
&& options.getRenderer() instanceof Renderer.Vulkan vulkanDriver
&& vulkanDriver.icdFile() != null) {
if (Files.isRegularFile(HomebrewUtils.LIB_VULKAN)) {
res.addDefault("-Dorg.lwjgl.vulkan.libname=", FileUtils.getAbsolutePath(HomebrewUtils.LIB_VULKAN));
}
}
Set<String> classpath = repository.getClasspath(version);
if (analyzer.has(LibraryAnalyzer.LibraryType.CLEANROOM)) {
@@ -395,6 +411,12 @@ public class DefaultLauncher extends Launcher {
}
}
if (options.getGraphicsBackend() != GraphicsAPI.DEFAULT
&& gameVersion.isPresent() && GameVersionNumber.compare(gameVersion.get(), "26.2-snapshot-2") >= 0) {
res.add("--graphicsBackend");
res.add(options.getGraphicsBackend().getMinecraftArg());
}
res.addAllWithoutParsing(Arguments.parseStringArguments(options.getGameArguments(), configuration));
res.removeIf(it -> getForbiddens().containsKey(it) && getForbiddens().get(it).get());
@@ -564,8 +586,8 @@ public class DefaultLauncher extends Launcher {
Path runDirectory = repository.getRunDirectory(version.getId());
if (StringUtils.isNotBlank(options.getPreLaunchCommand())) {
ProcessBuilder builder = new ProcessBuilder(StringUtils.tokenize(options.getPreLaunchCommand(), getEnvVars())).directory(runDirectory.toFile());
builder.environment().putAll(getEnvVars());
ProcessBuilder builder = new ProcessBuilder(StringUtils.tokenize(options.getPreLaunchCommand(), getEnvVars(nativeFolder))).directory(runDirectory.toFile());
builder.environment().putAll(getEnvVars(nativeFolder));
SystemUtils.callExternalProcess(builder);
}
@@ -578,7 +600,7 @@ public class DefaultLauncher extends Launcher {
Path appdata = options.getGameDir().toAbsolutePath().getParent();
if (appdata != null) builder.environment().put("APPDATA", appdata.toString());
builder.environment().putAll(getEnvVars());
builder.environment().putAll(getEnvVars(nativeFolder));
process = builder.start();
} catch (IOException e) {
throw new ProcessCreationException(e);
@@ -586,12 +608,13 @@ public class DefaultLauncher extends Launcher {
ManagedProcess p = new ManagedProcess(process, rawCommandLine);
if (listener != null)
startMonitors(p, listener, command.encoding, daemon);
startMonitors(p, nativeFolder, listener, command.encoding, daemon);
return p;
}
private Map<String, String> getEnvVars() {
private Map<String, String> getEnvVars(Path nativeFolder) {
String versionName = Optional.ofNullable(options.getVersionName()).orElse(version.getId());
Map<String, String> env = new LinkedHashMap<>();
env.put("INST_NAME", versionName);
env.put("INST_ID", versionName);
@@ -599,18 +622,32 @@ public class DefaultLauncher extends Launcher {
env.put("INST_MC_DIR", FileUtils.getAbsolutePath(repository.getRunDirectory(version.getId())));
env.put("INST_JAVA", options.getJava().getBinary().toString());
Renderer renderer = options.getRenderer();
if (renderer != Renderer.DEFAULT) {
if (options.getRenderer() instanceof Renderer.Driver driver) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
if (renderer != Renderer.LLVMPIPE)
env.put("GALLIUM_DRIVER", renderer.name().toLowerCase(Locale.ROOT));
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
env.put("__GLX_VENDOR_LIBRARY_NAME", "mesa");
switch (renderer) {
case LLVMPIPE:
if (driver.mesaDriverName() != null) {
if (driver instanceof Renderer.OpenGL && driver != Renderer.OpenGL.LLVMPIPE)
env.put("GALLIUM_DRIVER", driver.mesaDriverName());
else if (driver instanceof Renderer.Vulkan vulkanDriver) {
String icdFile = FileUtils.getAbsolutePath(nativeFolder.resolve("mesa-loader/" + vulkanDriver.icdName() + "_icd.json"));
env.put("VK_ICD_FILENAMES", icdFile);
env.put("VK_DRIVER_FILES", icdFile);
}
} else if (driver instanceof Renderer.Vulkan vulkanDriver
&& vulkanDriver.icdFile() != null
&& options.getJava().getArchitecture() == Architecture.SYSTEM_ARCH) {
String icdFile = FileUtils.getAbsolutePath(vulkanDriver.icdFile());
env.put("VK_ICD_FILENAMES", icdFile);
env.put("VK_DRIVER_FILES", icdFile);
}
} else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
if (driver instanceof Renderer.OpenGL oglDriver) {
if (oglDriver == Renderer.OpenGL.LLVMPIPE) {
env.put("__GLX_VENDOR_LIBRARY_NAME", "mesa");
env.put("LIBGL_ALWAYS_SOFTWARE", "1");
break;
case ZINK:
} else if (oglDriver == Renderer.OpenGL.ZINK) {
env.put("__GLX_VENDOR_LIBRARY_NAME", "mesa");
env.put("MESA_LOADER_DRIVER_OVERRIDE", "zink");
/*
* The amdgpu DDX is missing support for modifiers, causing Zink to fail.
@@ -619,7 +656,23 @@ public class DefaultLauncher extends Launcher {
* Link: https://gitlab.freedesktop.org/mesa/mesa/-/issues/10093
*/
env.put("LIBGL_KOPPER_DRI2", "1");
break;
}
} else if (driver instanceof Renderer.Vulkan vulkanDriver
&& options.getJava().getArchitecture() == Architecture.SYSTEM_ARCH) {
if (vulkanDriver.icdFile() != null) {
String absolutePath = FileUtils.getAbsolutePath(vulkanDriver.icdFile());
env.put("VK_ICD_FILENAMES", absolutePath);
env.put("VK_DRIVER_FILES", absolutePath);
}
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
&& options.getJava().getArchitecture() == Architecture.SYSTEM_ARCH) {
if (driver instanceof Renderer.Vulkan vulkanDriver
&& vulkanDriver != Renderer.Vulkan.MOLTENVK
&& vulkanDriver.icdFile() != null) {
String absolutePath = FileUtils.getAbsolutePath(vulkanDriver.icdFile());
env.put("VK_ICD_FILENAMES", absolutePath);
env.put("VK_DRIVER_FILES", absolutePath);
}
}
}
@@ -677,7 +730,7 @@ public class DefaultLauncher extends Launcher {
final Command commandLine = generateCommandLine(nativeFolder);
final String command = usePowerShell ? null : commandLine.commandLine.toString();
Map<String, String> envVars = getEnvVars();
Map<String, String> envVars = getEnvVars(nativeFolder);
if (isWindows && !usePowerShell) {
// https://stackoverflow.com/a/28452546
@@ -820,7 +873,7 @@ public class DefaultLauncher extends Launcher {
throw new ExecutionPolicyLimitException();
}
private void startMonitors(ManagedProcess managedProcess, ProcessListener processListener, Charset encoding, boolean isDaemon) {
private void startMonitors(ManagedProcess managedProcess, Path nativeFolder, ProcessListener processListener, Charset encoding, boolean isDaemon) {
processListener.setProcess(managedProcess);
Thread stdout = Lang.thread(new StreamPump(managedProcess.getProcess().getInputStream(), it -> {
processListener.onLog(it, false);
@@ -837,8 +890,8 @@ public class DefaultLauncher extends Launcher {
if (StringUtils.isNotBlank(options.getPostExitCommand())) {
try {
ProcessBuilder builder = new ProcessBuilder(StringUtils.tokenize(options.getPostExitCommand(), getEnvVars())).directory(options.getGameDir().toFile());
builder.environment().putAll(getEnvVars());
ProcessBuilder builder = new ProcessBuilder(StringUtils.tokenize(options.getPostExitCommand(), getEnvVars(nativeFolder))).directory(options.getGameDir().toFile());
builder.environment().putAll(getEnvVars(nativeFolder));
SystemUtils.callExternalProcess(builder);
} catch (Throwable e) {
LOG.warning("An Exception happened while running exit command.", e);
@@ -24,6 +24,7 @@ import org.jackhuang.hmcl.mod.RemoteModRepository;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
@@ -585,7 +586,7 @@ public class CurseAddon implements RemoteMod.IMod {
}
return RemoteMod.Dependency.ofGeneral(RELATION_TYPE.get(dependency.getRelationType()), CurseForgeRemoteModRepository.MODS, Integer.toString(dependency.getModId()));
}).distinct().filter(Objects::nonNull).collect(Collectors.toList()),
gameVersions.stream().filter(ver -> ver.startsWith("1.") || ver.contains("w")).collect(Collectors.toList()),
gameVersions.stream().filter(GameVersionNumber::isKnown).toList(),
gameVersions.stream().flatMap(version -> {
if ("fabric".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.FABRIC);
else if ("forge".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.FORGE);
@@ -86,6 +86,10 @@ public enum Architecture {
return this == X86 || this == X86_64;
}
public boolean isArm() {
return this == ARM32 || this == ARM64;
}
public static final Architecture CURRENT_ARCH;
public static final Architecture SYSTEM_ARCH;
@@ -18,18 +18,22 @@
package org.jackhuang.hmcl.util.platform.hardware;
import org.jackhuang.hmcl.util.StringUtils;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnknownNullability;
import org.jetbrains.annotations.Unmodifiable;
import java.nio.file.Path;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author Glavo
*/
/// @author Glavo
@NotNullByDefault
public final class GraphicsCard {
public static String cleanName(String name) {
@UnknownNullability
public static String cleanName(@UnknownNullability String name) {
if (name == null)
return null;
@@ -58,13 +62,15 @@ public final class GraphicsCard {
private final @Nullable Type type;
private final @Nullable String driver;
private final @Nullable String driverVersion;
private final @Unmodifiable List<Path> vulkanDriverFiles;
private GraphicsCard(String name, @Nullable HardwareVendor vendor, @Nullable Type type, @Nullable String driver, @Nullable String driverVersion) {
private GraphicsCard(String name, @Nullable HardwareVendor vendor, @Nullable Type type, @Nullable String driver, @Nullable String driverVersion, List<Path> vulkanDriverFiles) {
this.name = Objects.requireNonNull(name);
this.vendor = vendor;
this.type = type;
this.driver = driver;
this.driverVersion = driverVersion;
this.vulkanDriverFiles = vulkanDriverFiles;
}
public String getName() {
@@ -79,6 +85,10 @@ public final class GraphicsCard {
return driverVersion;
}
public List<Path> getVulkanDriverFiles() {
return vulkanDriverFiles;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder(name);
@@ -96,11 +106,12 @@ public final class GraphicsCard {
}
public static final class Builder {
private String name;
private HardwareVendor vendor;
private Type type;
private String driver;
private String driverVersion;
private @Nullable String name;
private @Nullable HardwareVendor vendor;
private @Nullable Type type;
private @Nullable String driver;
private @Nullable String driverVersion;
private @Nullable List<Path> vulkanDriverFiles;
public GraphicsCard build() {
String name = this.name;
@@ -111,10 +122,12 @@ public final class GraphicsCard {
name = "Unknown";
}
return new GraphicsCard(name, vendor, type, driver, driverVersion);
List<Path> vulkanDriverFiles = Objects.requireNonNullElse(this.vulkanDriverFiles, List.of());
return new GraphicsCard(name, vendor, type, driver, driverVersion, List.copyOf(vulkanDriverFiles));
}
public String getName() {
public @Nullable String getName() {
return name;
}
@@ -123,7 +136,7 @@ public final class GraphicsCard {
return this;
}
public HardwareVendor getVendor() {
public @Nullable HardwareVendor getVendor() {
return vendor;
}
@@ -132,7 +145,7 @@ public final class GraphicsCard {
return this;
}
public Type getType() {
public @Nullable Type getType() {
return type;
}
@@ -141,7 +154,7 @@ public final class GraphicsCard {
return this;
}
public String getDriver() {
public @Nullable String getDriver() {
return driver;
}
@@ -150,7 +163,7 @@ public final class GraphicsCard {
return this;
}
public String getDriverVersion() {
public @Nullable String getDriverVersion() {
return driverVersion;
}
@@ -158,5 +171,14 @@ public final class GraphicsCard {
this.driverVersion = driverVersion;
return this;
}
public @Nullable List<Path> getVulkanDriverFiles() {
return vulkanDriverFiles;
}
public Builder setVulkanDriverFiles(@Nullable List<Path> files) {
this.vulkanDriverFiles = files;
return this;
}
}
}
@@ -0,0 +1,60 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.platform.macos;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.NotNullByDefault;
import java.nio.file.Path;
/// @author Glavo
@NotNullByDefault
public final class HomebrewUtils {
/// The path to Homebrew prefix.
///
/// - For macOS x86_64, it is `/usr/local`;
/// - For macOS ARM64, it is `/opt/homebrew`;
/// - For other operating systems, it is undefined.
public static final Path HOMEBREW_PREFIX;
/// The path to `libvulkan.1.dylib`.
///
/// For non-macOS operating systems, it is undefined.
public static final Path LIB_VULKAN;
static {
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
// TODO: custom Homebrew prefix for macOS
HOMEBREW_PREFIX = Architecture.SYSTEM_ARCH.isX86()
? Path.of("/usr/local")
: Path.of("/opt/homebrew");
LIB_VULKAN = HOMEBREW_PREFIX.resolve("lib/libvulkan.1.dylib");
} else {
// For other operating systems, we don't need Homebrew.
var placeholder = Path.of("");
HOMEBREW_PREFIX = placeholder;
LIB_VULKAN = placeholder;
}
}
private HomebrewUtils() {
}
}
@@ -28,8 +28,11 @@ import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -48,6 +51,25 @@ final class WindowsGPUDetector {
}
}
private static @Nullable List<Path> parseVulkanDriverFiles(Object vulkanDriverFiles) {
if (vulkanDriverFiles instanceof String file) {
try {
return List.of(Path.of(file));
} catch (InvalidPathException e) {
LOG.warning("Failed to parse Vulkan ICD file path: " + file, e);
return null;
}
} else if (vulkanDriverFiles instanceof String[] files) {
try {
return Stream.of(files).map(Path::of).toList();
} catch (InvalidPathException e) {
LOG.warning("Failed to parse Vulkan ICD file paths: " + Arrays.toString(files), e);
return null;
}
} else
return null;
}
private static List<GraphicsCard> detectByCim() {
try {
String getCimInstance = OperatingSystem.SYSTEM_VERSION.getVersion().startsWith("6.1")
@@ -116,6 +138,7 @@ final class WindowsGPUDetector {
String vendor = regValueToString(reg.queryValue(hkey, subkey, "ProviderName"));
String driverVersion = regValueToString(reg.queryValue(hkey, subkey, "DriverVersion"));
String dacType = regValueToString(reg.queryValue(hkey, subkey, "HardwareInformation.DacType"));
Object vulkanDriverName = reg.queryValue(hkey, subkey, "VulkanDriverName");
GraphicsCard.Builder builder = GraphicsCard.builder();
if (name != null)
@@ -126,6 +149,8 @@ final class WindowsGPUDetector {
builder.setDriverVersion(driverVersion);
if (dacType != null)
builder.setType(fromDacType(dacType));
if (vulkanDriverName != null)
builder.setVulkanDriverFiles(parseVulkanDriverFiles(vulkanDriverName));
result.add(builder.build());
}
return result;
@@ -95,6 +95,18 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
return VersionRange.atMost(asGameVersion(maximum));
}
/// Determines whether the given version string corresponds to a known Minecraft version.
///
/// If the version string cannot be parsed as any known version type (release, snapshot, pre-release, etc.)
/// and is not a known April Fools snapshot, it is considered an unknown version.
///
/// @param version the version string to check, e.g. `"1.21.4"`, `"25w14craftmine"`, etc.
/// @return {@code true} if the version string corresponds to a known Minecraft version, {@code false} otherwise
/// @see #asGameVersion(String)
public static boolean isKnown(String version) {
return !(asGameVersion(version) instanceof Special special && special.prev == null);
}
final String value;
final String normalized;
@@ -151,7 +163,7 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
///
/// ```java
/// GameVersionNumber.asVersion("...").isAtLeast("1.13", "17w43a");
///```
/// ```
///
/// @param strictReleaseVersion When `strictReleaseVersion` is `false`, `releaseVersion` is considered less than
/// its corresponding pre/rc versions.
@@ -914,4 +914,6 @@
26.1
26.1.1-rc-1
26.1.1
26.1.2-rc-1
26.1.2
26w14a