diff --git a/build.gradle b/build.gradle index 573347ee..40ad0f52 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,5 @@ import com.replaymod.gradle.preprocess.PreprocessTask +import static gg.essential.gradle.util.PrebundleKt.prebundle buildscript { def mcVersion @@ -18,17 +19,17 @@ buildscript { name = "fabric" url = "https://maven.fabricmc.net/" } - if (!fabric) { - maven { - name = "forge" - url = "https://maven.minecraftforge.net" - } + maven { + name = "forge" + url = "https://maven.minecraftforge.net" } maven { name = "sonatype" url = "https://oss.sonatype.org/content/repositories/snapshots/" } maven { url 'https://jitpack.io' } + maven { url "https://maven.architectury.dev" } + maven { url "https://repo.essential.gg/repository/maven-public" } } dependencies { @@ -48,6 +49,7 @@ buildscript { } else { classpath 'com.github.ReplayMod:ForgeGradle:a8a9e0ca:all' // FG 1.2 } + classpath 'gg.essential:essential-gradle-toolkit:0.1.10' } } @@ -323,13 +325,13 @@ dependencies { shadow 'com.google.api-client:google-api-client-java6:1.20.0', shadeExclusions shadow 'com.google.oauth-client:google-oauth-client-jetty:1.20.0' - if (mcVersion >= 11400) { // need lwjgl 3 - for (suffix in ['', ':natives-linux', ':natives-windows', ':natives-macos']) { - shadow('org.lwjgl:lwjgl-tinyexr:3.2.2' + suffix) { - exclude group: 'org.lwjgl', module: 'lwjgl' // comes with MC - } - } + def lwjgl = configurations.create("lwjgl") + for (suffix in ['', ':natives-linux', ':natives-windows', ':natives-macos', ':natives-macos-arm64']) { + add(lwjgl.name, 'org.lwjgl:lwjgl:3.3.1' + suffix) + add(lwjgl.name, 'org.lwjgl:lwjgl-tinyexr:3.3.1' + suffix) } + compileOnly('org.lwjgl:lwjgl-tinyexr:3.3.1') + shadow(prebundle(project, lwjgl, "com/replaymod/render/utils/lwjgl.jar", {})) if (mcVersion < 11200) { // The version which MC ships is too old, we'll need to ship our own diff --git a/src/main/java/com/replaymod/render/EXRWriter.java b/src/main/java/com/replaymod/render/EXRWriter.java index 0bf981d8..17124ccc 100644 --- a/src/main/java/com/replaymod/render/EXRWriter.java +++ b/src/main/java/com/replaymod/render/EXRWriter.java @@ -1,4 +1,3 @@ -//#if MC>=11400 package com.replaymod.render; import com.replaymod.core.versions.MCVer; @@ -6,6 +5,7 @@ import com.replaymod.render.frame.BitmapFrame; import com.replaymod.render.rendering.Channel; import com.replaymod.render.rendering.FrameConsumer; import com.replaymod.render.utils.ByteBufferPool; +import com.replaymod.render.utils.Lwjgl3Loader; import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import net.minecraft.util.crash.CrashReport; import org.lwjgl.PointerBuffer; @@ -27,6 +27,14 @@ import static org.lwjgl.util.tinyexr.TinyEXR.*; public class EXRWriter implements FrameConsumer { + public static FrameConsumer create(Path outputFolder, boolean keepAlpha) { + return Lwjgl3Loader.createFrameConsumer( + EXRWriter.class, + new Class[]{ Path.class, boolean.class }, + new Object[]{ outputFolder, keepAlpha } + ); + } + private final Path outputFolder; private final boolean keepAlpha; @@ -128,4 +136,3 @@ public class EXRWriter implements FrameConsumer { public void close() { } } -//#endif diff --git a/src/main/java/com/replaymod/render/RenderSettings.java b/src/main/java/com/replaymod/render/RenderSettings.java index b358b718..f5e92ffb 100644 --- a/src/main/java/com/replaymod/render/RenderSettings.java +++ b/src/main/java/com/replaymod/render/RenderSettings.java @@ -105,13 +105,6 @@ public class RenderSettings { public boolean isSupported() { if (this == BLEND) { return RenderMethod.BLEND.isSupported(); - } else if (this == EXR) { - // Need LJWGL 3 - //#if MC>=11400 - return true; - //#else - //$$ return false; - //#endif } else { return true; } diff --git a/src/main/java/com/replaymod/render/mixin/Mixin_WindowsWorkaroundForTinyEXRNatives.java b/src/main/java/com/replaymod/render/mixin/Mixin_WindowsWorkaroundForTinyEXRNatives.java deleted file mode 100644 index 3df81c08..00000000 --- a/src/main/java/com/replaymod/render/mixin/Mixin_WindowsWorkaroundForTinyEXRNatives.java +++ /dev/null @@ -1,81 +0,0 @@ -//#if MC>=11400 -package com.replaymod.render.mixin; - -import org.lwjgl.system.Library; -import org.lwjgl.system.Platform; -import org.lwjgl.util.tinyexr.TinyEXR; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.ModifyArg; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.util.function.Consumer; -import java.util.regex.Pattern; - -/** - * It appears like natives on Windows cannot be loaded if one of their dependencies has already been loaded by a - * different class loader. In our case we cannot load tinyexr (on the knot class loader) because lwjgl has already - * been loaded on the system class loader. - * - * If we force the tinyexr native to load on the system class loader (by calling `Library.loadSystem(absPath)`), - * it'll load but we'll get an error when we call any of the native methods. - * - * We can't really load TinyEXR itself via the system class loader because Java does not provide any methods for - * modifying the system class path at runtime and we'd have to use JVM-specific hacks. - * - * Strangely, if we use System.loadLibrary instead of System.load, then it all just works. This mixin implements - * that workaround by finding MC's natives folder, extracting the dll from our jar into that folder and then replacing - * the context class passed to Library.loadSystem (which it uses to find dlls in jars) with Library (which is on the - * system class loader) so it cannot find the dll in our jar and falls back to using System.loadLibrary. - */ -@Mixin(value = TinyEXR.class, remap = false) -public class Mixin_WindowsWorkaroundForTinyEXRNatives { - private static final String LOAD_SYSTEM_CONSUMERS = "Lorg/lwjgl/system/Library;loadSystem(Ljava/util/function/Consumer;Ljava/util/function/Consumer;Ljava/lang/Class;Ljava/lang/String;)V"; - - @ModifyArg(method = "", at = @At(value = "INVOKE", target = LOAD_SYSTEM_CONSUMERS)) - private static Class uglyWindowsHacks(Consumer load, Consumer loadLibrary, Class context, String name) throws IOException { - if (Platform.get() != Platform.WINDOWS) { - return context; // works out of the box on linux - } - - name = System.mapLibraryName(name); - - URL libURL = context.getClassLoader().getResource(name); - if (libURL == null) { - throw new UnsatisfiedLinkError("Failed to locate library: " + name); - } - - String lwjglLibName = Library.JNI_LIBRARY_NAME; - if (!lwjglLibName.endsWith(".dll")) { - lwjglLibName = System.mapLibraryName(lwjglLibName); - } - - String paths = System.getProperty("java.library.path"); - Path nativesDir = null; - for (String dir : Pattern.compile(File.pathSeparator).split(paths)) { - Path path = Paths.get(dir); - if (Files.isReadable(path.resolve(lwjglLibName))) { - nativesDir = path; - break; - } - } - if (nativesDir == null) { - throw new UnsatisfiedLinkError("Failed to locate natives folder in " + paths); - } - - Path libPath = nativesDir.resolve(name); - try (InputStream source = libURL.openStream()) { - Files.copy(source, libPath, StandardCopyOption.REPLACE_EXISTING); - } - - return Library.class; - } -} -//#endif diff --git a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java index a6b31504..ff8048d2 100644 --- a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java +++ b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java @@ -8,6 +8,7 @@ import com.replaymod.core.versions.MCVer; import com.replaymod.pathing.player.AbstractTimelinePlayer; import com.replaymod.pathing.properties.TimestampProperty; import com.replaymod.render.CameraPathExporter; +import com.replaymod.render.EXRWriter; import com.replaymod.render.PNGWriter; import com.replaymod.render.RenderSettings; import com.replaymod.render.ReplayModRender; @@ -55,7 +56,6 @@ import org.lwjgl.opengl.GL11; //#endif //#if MC>=11400 -import com.replaymod.render.EXRWriter; import net.minecraft.client.gui.screen.Screen; import java.util.concurrent.CompletableFuture; //#else @@ -129,11 +129,7 @@ public class VideoRenderer implements RenderInfo { } else { FrameConsumer frameConsumer; if (settings.getEncodingPreset() == RenderSettings.EncodingPreset.EXR) { - //#if MC>=11400 - frameConsumer = new EXRWriter(settings.getOutputFile().toPath(), settings.isIncludeAlphaChannel()); - //#else - //$$ throw new UnsupportedOperationException("EXR requires LWJGL3"); - //#endif + frameConsumer = EXRWriter.create(settings.getOutputFile().toPath(), settings.isIncludeAlphaChannel()); } else if (settings.getEncodingPreset() == RenderSettings.EncodingPreset.PNG) { frameConsumer = new PNGWriter(settings.getOutputFile().toPath(), settings.isIncludeAlphaChannel()); } else { diff --git a/src/main/java/com/replaymod/render/utils/Lwjgl3Loader.java b/src/main/java/com/replaymod/render/utils/Lwjgl3Loader.java new file mode 100644 index 00000000..8c832acd --- /dev/null +++ b/src/main/java/com/replaymod/render/utils/Lwjgl3Loader.java @@ -0,0 +1,132 @@ +package com.replaymod.render.utils; + +import com.replaymod.core.ReplayMod; +import com.replaymod.render.rendering.Frame; +import com.replaymod.render.rendering.FrameConsumer; +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.ProtectionDomain; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; + +public class Lwjgl3Loader extends URLClassLoader { + static { registerAsParallelCapable(); } + private static Path tempJarFile; + private static Lwjgl3Loader instance; + + private final Set implClasses = new CopyOnWriteArraySet<>(); + + private Lwjgl3Loader(Path jarFile) throws IOException, ReflectiveOperationException { + super(new URL[] { jarFile.toUri().toURL() }, Lwjgl3Loader.class.getClassLoader()); + + // Need to use a different directory for natives than MC because native files can only be loaded once + Path nativesDir = ReplayMod.instance.folders.getCacheFolder().resolve("lwjgl-natives"); + + Class configClass = Class.forName("org.lwjgl.system.Configuration", true, this); + Object extractDirField = configClass.getField("SHARED_LIBRARY_EXTRACT_DIRECTORY").get(null); + Method setMethod = configClass.getMethod("set", Object.class); + setMethod.invoke(extractDirField, nativesDir.toAbsolutePath().toString()); + } + + private boolean canBeSharedWithMc(String name) { + if (name.startsWith("org.lwjgl.")) { + return false; // MC may have a different version + } + for (String implClass : implClasses) { + if (name.startsWith(implClass)) { + return false; // depends on above lwjgl + } + } + return true; + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (!canBeSharedWithMc(name)) { + synchronized (getClassLoadingLock(name)) { + Class cls = findLoadedClass(name); + if (cls == null) { + cls = findClass(name); + } + if (resolve) { + resolveClass(cls); + } + return cls; + } + } else { + return super.loadClass(name, resolve); + } + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + try { + return super.findClass(name); + } catch (ClassNotFoundException e) { + String path = name.replace('.', '/').concat(".class"); + URL url = getParent().getResource(path); + if (url == null) { + throw e; + } + try { + byte[] bytes = IOUtils.toByteArray(url); + return defineClass(name, bytes, 0, bytes.length, (ProtectionDomain) null); + } catch (IOException e1) { + throw new ClassNotFoundException(name, e1); + } + } + } + + private static synchronized Path getJarFile() throws IOException { + if (tempJarFile == null) { + Path jarFile = Files.createTempFile("replaymod-lwjgl", ".jar"); + jarFile.toFile().deleteOnExit(); + try (InputStream in = Lwjgl3Loader.class.getResourceAsStream("lwjgl.jar")) { + if (in == null) { + throw new IOException("Failed to find embedded lwjgl.jar file."); + } + Files.copy(in, jarFile, REPLACE_EXISTING); + } + tempJarFile = jarFile; + } + return tempJarFile; + } + + public static synchronized Lwjgl3Loader instance() { + if (instance == null) { + try { + instance = new Lwjgl3Loader(getJarFile()); + } catch (IOException | ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + return instance; + } + + @SuppressWarnings("unchecked") + public static

FrameConsumer

createFrameConsumer( + Class> implClass, + Class[] parameterTypes, + Object[] args + ) { + try { + Lwjgl3Loader loader = instance(); + loader.implClasses.add(implClass.getName()); + Class realClass = Class.forName(implClass.getName(), true, loader); + Constructor constructor = realClass.getConstructor(parameterTypes); + return (FrameConsumer

) constructor.newInstance(args); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/resources/mixins.render.replaymod.json b/src/main/resources/mixins.render.replaymod.json index ff833a38..8bd2c8b6 100644 --- a/src/main/resources/mixins.render.replaymod.json +++ b/src/main/resources/mixins.render.replaymod.json @@ -36,7 +36,6 @@ //#endif //#if MC>=11400 "Mixin_PreserveDepthDuringHandRendering", - "Mixin_WindowsWorkaroundForTinyEXRNatives", //#endif "GameRendererAccessor", "MainWindowAccessor",