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:
24
build.gradle
24
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
132
src/main/java/com/replaymod/render/utils/Lwjgl3Loader.java
Normal file
132
src/main/java/com/replaymod/render/utils/Lwjgl3Loader.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,6 @@
|
||||
//#endif
|
||||
//#if MC>=11400
|
||||
"Mixin_PreserveDepthDuringHandRendering",
|
||||
"Mixin_WindowsWorkaroundForTinyEXRNatives",
|
||||
//#endif
|
||||
"GameRendererAccessor",
|
||||
"MainWindowAccessor",
|
||||
|
||||
Reference in New Issue
Block a user