Load tinyexr in its own class loader

That way we can simply use whatever version we want and aren't tied to the
version of lwjgl which MC provides.

This fixes the OpenEXR export on 1.19 crashing either due to the windows
workaround or due to differing lwjgl versions (depending on OS).
And also enables OpenEXR export on MC versions using lwjgl2 (1.12.2 and below).
This commit is contained in:
Jonas Herzig
2022-06-26 11:57:24 +02:00
parent 814d0b7c17
commit 40f07279cf
7 changed files with 156 additions and 108 deletions

View File

@@ -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

View File

@@ -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<BitmapFrame> {
public static FrameConsumer<BitmapFrame> 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<BitmapFrame> {
public void close() {
}
}
//#endif

View File

@@ -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;
}

View File

@@ -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 = "<clinit>", at = @At(value = "INVOKE", target = LOAD_SYSTEM_CONSUMERS))
private static Class<?> uglyWindowsHacks(Consumer<String> load, Consumer<String> 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

View File

@@ -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<BitmapFrame> 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 {

View File

@@ -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<String> 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 <P extends Frame> FrameConsumer<P> createFrameConsumer(
Class<? extends FrameConsumer<P>> 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<P>) constructor.newInstance(args);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -36,7 +36,6 @@
//#endif
//#if MC>=11400
"Mixin_PreserveDepthDuringHandRendering",
"Mixin_WindowsWorkaroundForTinyEXRNatives",
//#endif
"GameRendererAccessor",
"MainWindowAccessor",