diff --git a/src/main/java/me/jellysquid/mods/sodium/client/util/workarounds/DriverWorkarounds.java b/src/main/java/me/jellysquid/mods/sodium/client/util/workarounds/DriverWorkarounds.java index 3915346b00..097c372d37 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/util/workarounds/DriverWorkarounds.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/util/workarounds/DriverWorkarounds.java @@ -1,6 +1,6 @@ package me.jellysquid.mods.sodium.client.util.workarounds; -import me.jellysquid.mods.sodium.client.util.workarounds.platform.NVIDIAWorkarounds; +import me.jellysquid.mods.sodium.client.util.workarounds.platform.NvidiaWorkarounds; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,13 +11,13 @@ public static void beforeContextCreation() { LOGGER.info("Checking for any workarounds that need to be applied prior to context creation..."); if (Workarounds.isWorkaroundEnabled(Workarounds.Reference.NVIDIA_BAD_DRIVER_SETTINGS)) { - NVIDIAWorkarounds.install(); + NvidiaWorkarounds.install(); } } public static void afterContextCreation() { if (Workarounds.isWorkaroundEnabled(Workarounds.Reference.NVIDIA_BAD_DRIVER_SETTINGS)) { - NVIDIAWorkarounds.uninstall(); + NvidiaWorkarounds.uninstall(); } } } diff --git a/src/main/java/me/jellysquid/mods/sodium/client/util/workarounds/platform/NVIDIAWorkarounds.java b/src/main/java/me/jellysquid/mods/sodium/client/util/workarounds/platform/NVIDIAWorkarounds.java deleted file mode 100644 index 23519ab227..0000000000 --- a/src/main/java/me/jellysquid/mods/sodium/client/util/workarounds/platform/NVIDIAWorkarounds.java +++ /dev/null @@ -1,225 +0,0 @@ -package me.jellysquid.mods.sodium.client.util.workarounds.platform; - -import net.minecraft.util.Util; -import org.lwjgl.system.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.nio.BufferOverflowException; -import java.nio.ByteBuffer; -import java.util.Objects; - -public class NVIDIAWorkarounds { - private static final Logger LOGGER = LoggerFactory.getLogger("Sodium-CommandLineWorkaround"); - - private static Hook ACTIVE_HOOK; - - public static class Hook { - // Reference to the shared library so that it doesn't get unloaded. - private SharedLibrary sharedLibrary; - - // Pointer into the command-line arguments stored within the Windows process structure - // We do not own this memory, and it should not be freed. - private final ByteBuffer commandLineBuffer; - - // function pointer to GetCommandLineW in Kernel32 - private final long pfnGetCommandLineW; - - // function pointer to SetEnvironmentVariableW in Kernel32 - private final long pfnSetEnvironmentVariable; - - // The original command-line the process was started with. - private final String originalCommandLine; - - // True if the command line arguments in memory have been modified - private boolean modifiedCommandLine; - // True if the environment variables have been modified - private boolean modifiedEnvironmentVariables; - - public Hook() { - this.sharedLibrary = Library.loadNative("me.jellyquid.mods.sodium", "kernel32"); - - try { - this.pfnGetCommandLineW = APIUtil.apiGetFunctionAddress(this.sharedLibrary, "GetCommandLineW"); - this.pfnSetEnvironmentVariable = APIUtil.apiGetFunctionAddress(this.sharedLibrary, "SetEnvironmentVariableW"); - - var lpwstrCommandLine = JNI.callP(this.pfnGetCommandLineW); - - this.originalCommandLine = MemoryUtil.memUTF16(lpwstrCommandLine); - - // This should remain valid for the lifetime of the process. - this.commandLineBuffer = MemoryUtil.memByteBuffer(lpwstrCommandLine, - MemoryUtil.memLengthUTF16(this.originalCommandLine, true)); - } catch (Throwable t) { - this.sharedLibrary.close(); - throw t; // re-throw the exception - } - } - - public void install() { - // Check that the library is still loaded, otherwise the function pointers are invalid. - if (this.sharedLibrary == null) { - throw new IllegalStateException("Library is unloaded"); - } - - this.modifyCommandLineArguments(); - this.modifyEnvironmentVariables(); - } - - private void modifyCommandLineArguments() { - if (this.modifiedCommandLine) { - return; - } - - LOGGER.info("... Modifying process command line arguments... (forces NVIDIA drivers to not apply broken optimizations)"); - - // The NVIDIA drivers rely on parsing the command line arguments to detect Minecraft. If we destroy those, - // then it shouldn't be able to detect us anymore. - var modifiedCmdline = "net.caffeinemc.sodium"; // Honestly, even giving the drivers this much might be a mistake. - var modifiedCmdlineLen = MemoryUtil.memLengthUTF16(modifiedCmdline, true); - - if (modifiedCmdlineLen > this.commandLineBuffer.remaining()) { - // We can never write a string which is larger than what we were given, as there - // may not be enough space remaining. Realistically, this should never happen, since - // our identifying string is very short, and the command line is *at least* going to contain - // the class entrypoint. - throw new BufferOverflowException(); - } - - // Write the new command line arguments into the process structure. - // The Windows API documentation explicitly says this is forbidden, but it *does* give us a pointer - // directly into the PEB structure, so... - // Must be null-terminated (as it was given to us). - MemoryUtil.memUTF16(modifiedCmdline, true, - this.commandLineBuffer); - - // Make sure we can actually see our changes in the process structure - // We don't know if this could ever actually happen, but since we're doing something pretty hacky - // it's not out of the question that Windows might try to prevent it in a newer version. - // - // NOTE: When reading it back, we pass the memory address by itself instead of the ByteBuffer. This - // results in it reading up to the null terminator, rather than the end of the buffer. - if (!Objects.equals(modifiedCmdline, MemoryUtil.memUTF16(MemoryUtil.memAddress(this.commandLineBuffer)))) { - throw new RuntimeException("Sanity check failed, the command line arguments did not appear to change"); - } - - this.modifiedCommandLine = true; - } - - private void modifyEnvironmentVariables() { - if (this.modifiedEnvironmentVariables) { - return; - } - - LOGGER.info("... Modifying process environment variables... (forces NVIDIA drivers to use dedicated GPU when available)"); - - // Since we broke the driver's ability to find Minecraft, it won't use the dedicated GPU any longer. - // We need to update the environment variables to ensure it picks the dedicated GPU. - try (MemoryStack stack = MemoryStack.stackPush()) { - var name = "SHIM_MCCOMPAT"; // control var for multi-GPU systems - var value = "0x800000001"; // use dedicated GPU - - var lpName = stack.malloc(16, MemoryUtil.memLengthUTF16(name, true)); - MemoryUtil.memUTF16(name, true, lpName); - - var lpValue = stack.malloc(16, MemoryUtil.memLengthUTF16(value, true)); - MemoryUtil.memUTF16(value, true, lpValue); - - // We don't care about the return value. - JNI.callJJI(MemoryUtil.memAddress0(lpName), MemoryUtil.memAddress(lpValue), this.pfnSetEnvironmentVariable); - } - - this.modifiedEnvironmentVariables = true; - } - - public void uninstall() { - if (this.modifiedCommandLine) { - this.restoreCommandLine(); - } - - if (this.modifiedEnvironmentVariables) { - this.restoreEnvironmentVariables(); - } - - if (this.sharedLibrary != null) { - this.sharedLibrary.close(); - this.sharedLibrary = null; - } - } - - private void restoreCommandLine() { - // Restore the original value of the command line arguments - // Must be null-terminated (as it was given to us) - MemoryUtil.memUTF16(this.originalCommandLine, true, - this.commandLineBuffer); - - this.modifiedCommandLine = false; - } - - - private void restoreEnvironmentVariables() { - try (MemoryStack stack = MemoryStack.stackPush()) { - var name = "SHIM_MCCOMPAT"; // control var for multi-GPU systems - - var lpName = stack.malloc(16, MemoryUtil.memLengthUTF16(name, true)); - MemoryUtil.memUTF16(name, true, lpName); - - // Passing NULL as the value will unset the environment variable. - // We don't care about the return value. - JNI.callJJI(MemoryUtil.memAddress0(lpName), MemoryUtil.NULL, this.pfnSetEnvironmentVariable); - } - - this.modifiedEnvironmentVariables = false; - } - } - - public static void install() { - if (ACTIVE_HOOK != null) { - return; - } - - LOGGER.info("Attempting to apply workarounds for the NVIDIA Graphics Driver..."); - LOGGER.info("If the game crashes immediately after this point, please make a bug report: https://github.com/CaffeineMC/sodium-fabric/issues"); - - if (Util.getOperatingSystem() == Util.OperatingSystem.LINUX) { - setLinuxDisableEnv(); - return; - } else if (Util.getOperatingSystem() != Util.OperatingSystem.WINDOWS) { - return; - } - - try { - ACTIVE_HOOK = new Hook(); - ACTIVE_HOOK.install(); - - LOGGER.info("Successfully applied workarounds for the NVIDIA Graphics Driver!"); - } catch (Throwable t) { - LOGGER.error("Failure while applying workarounds", t); - - LOGGER.error("READ ME! The workarounds for the NVIDIA Graphics Driver did not apply correctly!"); - LOGGER.error("READ ME! You are very likely going to run into unexplained crashes and severe performance issues!"); - LOGGER.error("READ ME! Please see this issue for more information: https://github.com/CaffeineMC/sodium-fabric/issues/1816"); - } - } - - public static void setLinuxDisableEnv() { - try (SharedLibrary sharedLibrary = Library.loadNative("me.jellyquid.mods.sodium", "libc.so.6")) { - long pfnSetenv = APIUtil.apiGetFunctionAddress(sharedLibrary, "setenv"); - try (var stack = MemoryStack.stackPush()) { - JNI.callPPI(MemoryUtil.memAddress0(stack.UTF8("__GL_THREADED_OPTIMIZATIONS")), MemoryUtil.memAddress0(stack.UTF8("0")), 1, pfnSetenv); - } - } catch (Throwable t) { - LOGGER.error("Failure while applying workarounds", t); - LOGGER.error("READ ME! The workarounds for the NVIDIA Graphics Driver did not apply correctly!"); - } - } - - public static void uninstall() { - if (ACTIVE_HOOK == null) { - return; - } - - ACTIVE_HOOK.uninstall(); - ACTIVE_HOOK = null; - } -} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/util/workarounds/platform/NvidiaWorkarounds.java b/src/main/java/me/jellysquid/mods/sodium/client/util/workarounds/platform/NvidiaWorkarounds.java new file mode 100644 index 0000000000..168981fe1b --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/util/workarounds/platform/NvidiaWorkarounds.java @@ -0,0 +1,52 @@ +package me.jellysquid.mods.sodium.client.util.workarounds.platform; + +import me.jellysquid.mods.sodium.client.util.workarounds.platform.linux.LibC; +import me.jellysquid.mods.sodium.client.util.workarounds.platform.windows.Kernel32; +import me.jellysquid.mods.sodium.client.util.workarounds.platform.windows.WindowsProcessHacks; +import net.minecraft.util.Util; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class NvidiaWorkarounds { + private static final Logger LOGGER = LoggerFactory.getLogger("Sodium-NvidiaWorkarounds"); + + public static void install() { + LOGGER.warn("Attempting to apply workarounds for the NVIDIA Graphics Driver..."); + LOGGER.warn("If the game crashes immediately after this point, please make a bug report: https://github.com/CaffeineMC/sodium-fabric/issues"); + + try { + switch (Util.getOperatingSystem()) { + case WINDOWS -> { + // The NVIDIA drivers rely on parsing the command line arguments to detect Minecraft. If we destroy those, + // then it shouldn't be able to detect us anymore. + WindowsProcessHacks.setCommandLine("net.caffeinemc.sodium"); + + // Ensures that Minecraft will run on the dedicated GPU, since the drivers can no longer detect it + Kernel32.setEnvironmentVariable("SHIM_MCCOMPAT", "0x800000001"); + } + case LINUX -> { + // Unlike Windows, we don't need to hide ourselves from the driver. We can just request that + // it not use threaded optimizations instead. + LibC.setEnvironmentVariable("__GL_THREADED_OPTIMIZATIONS", "0"); + } + } + + LOGGER.info("... Successfully applied workarounds for the NVIDIA Graphics Driver!"); + } catch (Throwable t) { + LOGGER.error("Failure while applying workarounds", t); + + LOGGER.error("READ ME! The workarounds for the NVIDIA Graphics Driver did not apply correctly!"); + LOGGER.error("READ ME! You are very likely going to run into unexplained crashes and severe performance issues!"); + LOGGER.error("READ ME! Please see this issue for more information: https://github.com/CaffeineMC/sodium-fabric/issues/1816"); + } + } + + public static void uninstall() { + switch (Util.getOperatingSystem()) { + case WINDOWS -> { + WindowsProcessHacks.resetCommandLine(); + } + case LINUX -> { } + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/util/workarounds/platform/linux/LibC.java b/src/main/java/me/jellysquid/mods/sodium/client/util/workarounds/platform/linux/LibC.java new file mode 100644 index 0000000000..87fb7532ca --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/util/workarounds/platform/linux/LibC.java @@ -0,0 +1,23 @@ +package me.jellysquid.mods.sodium.client.util.workarounds.platform.linux; + +import org.jetbrains.annotations.Nullable; +import org.lwjgl.system.*; + +public class LibC { + private static final SharedLibrary LIBRARY = Library.loadNative("me.jellyquid.mods.sodium", "libc.so.6"); + + private static final long PFN_setenv; + + static { + PFN_setenv = APIUtil.apiGetFunctionAddress(LIBRARY, "setenv"); + } + + public static void setEnvironmentVariable(String name, @Nullable String value) { + try (var stack = MemoryStack.stackPush()) { + var nameBuf = stack.UTF8(name); + var valueBuf = value != null ? stack.UTF8(value) : null; + + JNI.callPPI(MemoryUtil.memAddress(nameBuf), MemoryUtil.memAddressSafe(valueBuf), 1 /* replace */, PFN_setenv); + } + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/util/workarounds/platform/windows/Kernel32.java b/src/main/java/me/jellysquid/mods/sodium/client/util/workarounds/platform/windows/Kernel32.java new file mode 100644 index 0000000000..08dd2cd46d --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/util/workarounds/platform/windows/Kernel32.java @@ -0,0 +1,38 @@ +package me.jellysquid.mods.sodium.client.util.workarounds.platform.windows; + +import org.jetbrains.annotations.Nullable; +import org.lwjgl.system.*; + +import java.nio.ByteBuffer; + +public class Kernel32 { + private static final SharedLibrary LIBRARY = Library.loadNative("me.jellyquid.mods.sodium", "kernel32"); + + private static final long PFN_GetCommandLineW; + private static final long PFN_SetEnvironmentVariableW; + + static { + PFN_GetCommandLineW = APIUtil.apiGetFunctionAddress(LIBRARY, "GetCommandLineW"); + PFN_SetEnvironmentVariableW = APIUtil.apiGetFunctionAddress(LIBRARY, "SetEnvironmentVariableW"); + } + + public static void setEnvironmentVariable(String name, @Nullable String value) { + try (MemoryStack stack = MemoryStack.stackPush()) { + ByteBuffer lpNameBuf = stack.malloc(16, MemoryUtil.memLengthUTF16(name, true)); + MemoryUtil.memUTF16(name, true, lpNameBuf); + + ByteBuffer lpValueBuf = null; + + if (value != null) { + lpValueBuf = stack.malloc(16, MemoryUtil.memLengthUTF16(value, true)); + MemoryUtil.memUTF16(value, true, lpValueBuf); + } + + JNI.callJJI(MemoryUtil.memAddress0(lpNameBuf), MemoryUtil.memAddressSafe(lpValueBuf), PFN_SetEnvironmentVariableW); + } + } + + public static long getCommandLine() { + return JNI.callP(PFN_GetCommandLineW); + } +} diff --git a/src/main/java/me/jellysquid/mods/sodium/client/util/workarounds/platform/windows/WindowsProcessHacks.java b/src/main/java/me/jellysquid/mods/sodium/client/util/workarounds/platform/windows/WindowsProcessHacks.java new file mode 100644 index 0000000000..c45f274dd8 --- /dev/null +++ b/src/main/java/me/jellysquid/mods/sodium/client/util/workarounds/platform/windows/WindowsProcessHacks.java @@ -0,0 +1,80 @@ +package me.jellysquid.mods.sodium.client.util.workarounds.platform.windows; + +import org.lwjgl.system.MemoryUtil; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.util.Objects; + +public class WindowsProcessHacks { + private static CommandLineHook ACTIVE_COMMAND_LINE_HOOK; + + public static void setCommandLine(String modifiedCmdline) { + if (ACTIVE_COMMAND_LINE_HOOK != null) { + throw new IllegalStateException("Command line is already modified"); + } + + // Pointer into the command-line arguments stored within the Windows process structure + // We do not own this memory, and it should not be freed. + var pCmdline = Kernel32.getCommandLine(); + + // The original command-line the process was started with. + var cmdline = MemoryUtil.memUTF16(pCmdline); + var cmdlineLen = MemoryUtil.memLengthUTF16(cmdline, true); + + if (MemoryUtil.memLengthUTF16(modifiedCmdline, true) > cmdlineLen) { + // We can never write a string which is larger than what we were given, as there + // may not be enough space remaining. Realistically, this should never happen, since + // our identifying string is very short, and the command line is *at least* going to contain + // the class entrypoint. + throw new BufferOverflowException(); + } + + ByteBuffer buffer = MemoryUtil.memByteBuffer(pCmdline, cmdlineLen); + + // Write the new command line arguments into the process structure. + // The Windows API documentation explicitly says this is forbidden, but it *does* give us a pointer + // directly into the PEB structure, so... + MemoryUtil.memUTF16(modifiedCmdline, true, buffer); + + // Make sure we can actually see our changes in the process structure + // We don't know if this could ever actually happen, but since we're doing something pretty hacky + // it's not out of the question that Windows might try to prevent it in a newer version. + if (!Objects.equals(modifiedCmdline, MemoryUtil.memUTF16(pCmdline))) { + throw new RuntimeException("Sanity check failed, the command line arguments did not appear to change"); + } + + ACTIVE_COMMAND_LINE_HOOK = new CommandLineHook(cmdline, buffer); + } + + public static void resetCommandLine() { + if (ACTIVE_COMMAND_LINE_HOOK != null) { + ACTIVE_COMMAND_LINE_HOOK.uninstall(); + ACTIVE_COMMAND_LINE_HOOK = null; + } + } + + private static class CommandLineHook { + private final String cmdline; + private final ByteBuffer cmdlineBuf; + + private boolean active; + + private CommandLineHook(String cmdline, ByteBuffer cmdlineBuf) { + this.cmdline = cmdline; + this.cmdlineBuf = cmdlineBuf; + } + + public void uninstall() { + if (this.active) { + throw new IllegalStateException(); + } + + // Restore the original value of the command line arguments + // Must be null-terminated (as it was given to us) + MemoryUtil.memUTF16(this.cmdline, true, this.cmdlineBuf); + + this.active = false; + } + } +}