aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorisXander <xander@isxander.dev>2023-08-17 14:19:32 +0100
committerisXander <xander@isxander.dev>2023-08-17 14:19:32 +0100
commit39bc5b5d8b8e6d4369ea71a7787907521e11ad34 (patch)
treed56be6ac071094646829061e2d835e6e952df47c
parent9ffd159faa215256161c3af9c57bd0b742b6d818 (diff)
downloadYetAnotherConfigLib-39bc5b5d8b8e6d4369ea71a7787907521e11ad34.tar.gz
YetAnotherConfigLib-39bc5b5d8b8e6d4369ea71a7787907521e11ad34.tar.bz2
YetAnotherConfigLib-39bc5b5d8b8e6d4369ea71a7787907521e11ad34.zip
Re-write image renderer handling to be threadsafe (relates to #101)
-rw-r--r--common/src/main/java/dev/isxander/yacl3/api/OptionDescription.java2
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/GsonConfigInstance.java14
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/OverrideImage.java2
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/EmptyCustomImageFactory.java2
-rw-r--r--common/src/main/java/dev/isxander/yacl3/gui/ImageRenderer.java388
-rw-r--r--common/src/main/java/dev/isxander/yacl3/gui/OptionDescriptionWidget.java3
-rw-r--r--common/src/main/java/dev/isxander/yacl3/gui/image/ImageRenderer.java11
-rw-r--r--common/src/main/java/dev/isxander/yacl3/gui/image/ImageRendererFactory.java24
-rw-r--r--common/src/main/java/dev/isxander/yacl3/gui/image/ImageRendererManager.java76
-rw-r--r--common/src/main/java/dev/isxander/yacl3/gui/image/impl/AnimatedDynamicTextureImage.java282
-rw-r--r--common/src/main/java/dev/isxander/yacl3/gui/image/impl/DynamicTextureImage.java62
-rw-r--r--common/src/main/java/dev/isxander/yacl3/gui/image/impl/ResourceTextureImage.java46
-rw-r--r--common/src/main/java/dev/isxander/yacl3/impl/OptionDescriptionImpl.java53
-rw-r--r--common/src/main/java/dev/isxander/yacl3/impl/utils/YACLConstants.java5
-rw-r--r--common/src/main/java/dev/isxander/yacl3/mixin/MinecraftMixin.java9
15 files changed, 544 insertions, 435 deletions
diff --git a/common/src/main/java/dev/isxander/yacl3/api/OptionDescription.java b/common/src/main/java/dev/isxander/yacl3/api/OptionDescription.java
index 40f1d68..7336379 100644
--- a/common/src/main/java/dev/isxander/yacl3/api/OptionDescription.java
+++ b/common/src/main/java/dev/isxander/yacl3/api/OptionDescription.java
@@ -1,6 +1,6 @@
package dev.isxander.yacl3.api;
-import dev.isxander.yacl3.gui.ImageRenderer;
+import dev.isxander.yacl3.gui.image.ImageRenderer;
import dev.isxander.yacl3.impl.OptionDescriptionImpl;
import net.minecraft.network.chat.CommonComponents;
import net.minecraft.network.chat.Component;
diff --git a/common/src/main/java/dev/isxander/yacl3/config/GsonConfigInstance.java b/common/src/main/java/dev/isxander/yacl3/config/GsonConfigInstance.java
index 0d7b289..acbf338 100644
--- a/common/src/main/java/dev/isxander/yacl3/config/GsonConfigInstance.java
+++ b/common/src/main/java/dev/isxander/yacl3/config/GsonConfigInstance.java
@@ -22,6 +22,20 @@ import java.util.function.UnaryOperator;
*
* @param <T> config data type
* @deprecated upgrade to config v2 {@link dev.isxander.yacl3.config.v2.api.ConfigClassHandler} with {@link dev.isxander.yacl3.config.v2.api.serializer.GsonConfigSerializerBuilder}
+ * <pre>
+ * {@code
+ * public class MyConfig {
+ * public static ConfigClassHandler<MyConfig> HANDLER = ConfigClassHandler.createBuilder(MyConfig.class)
+ * .id(new ResourceLocation("modid", "config"))
+ * .serializer(config -> GsonConfigSerializerBuilder.create(config)
+ * .setPath(FabricLoader.getInstance().getConfigDir().resolve("my_mod.json")
+ * .build())
+ * .build();
+ *
+ * @SerialEntry public boolean myBoolean = true;
+ * }
+ * }
+ * </pre>
*/
@Deprecated
public class GsonConfigInstance<T> extends ConfigInstance<T> {
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/OverrideImage.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/OverrideImage.java
index 649121a..5a33884 100644
--- a/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/OverrideImage.java
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/OverrideImage.java
@@ -2,7 +2,7 @@ package dev.isxander.yacl3.config.v2.api.autogen;
import dev.isxander.yacl3.config.v2.api.ConfigField;
import dev.isxander.yacl3.config.v2.impl.autogen.EmptyCustomImageFactory;
-import dev.isxander.yacl3.gui.ImageRenderer;
+import dev.isxander.yacl3.gui.image.ImageRenderer;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/EmptyCustomImageFactory.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/EmptyCustomImageFactory.java
index f6949e7..421de82 100644
--- a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/EmptyCustomImageFactory.java
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/EmptyCustomImageFactory.java
@@ -3,7 +3,7 @@ package dev.isxander.yacl3.config.v2.impl.autogen;
import dev.isxander.yacl3.config.v2.api.ConfigField;
import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess;
import dev.isxander.yacl3.config.v2.api.autogen.OverrideImage;
-import dev.isxander.yacl3.gui.ImageRenderer;
+import dev.isxander.yacl3.gui.image.ImageRenderer;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
diff --git a/common/src/main/java/dev/isxander/yacl3/gui/ImageRenderer.java b/common/src/main/java/dev/isxander/yacl3/gui/ImageRenderer.java
deleted file mode 100644
index 9617c58..0000000
--- a/common/src/main/java/dev/isxander/yacl3/gui/ImageRenderer.java
+++ /dev/null
@@ -1,388 +0,0 @@
-package dev.isxander.yacl3.gui;
-
-import com.mojang.blaze3d.Blaze3D;
-import com.mojang.blaze3d.platform.NativeImage;
-import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi;
-import dev.isxander.yacl3.impl.utils.YACLConstants;
-import net.minecraft.CrashReport;
-import net.minecraft.CrashReportCategory;
-import net.minecraft.ReportedException;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.gui.GuiGraphics;
-import net.minecraft.client.renderer.texture.DynamicTexture;
-import net.minecraft.client.renderer.texture.TextureManager;
-import net.minecraft.resources.ResourceLocation;
-import net.minecraft.server.packs.resources.Resource;
-import net.minecraft.server.packs.resources.ResourceManager;
-import net.minecraft.util.FastColor;
-
-import javax.imageio.ImageIO;
-import javax.imageio.ImageReader;
-import javax.imageio.metadata.IIOMetadata;
-import javax.imageio.metadata.IIOMetadataNode;
-import java.awt.*;
-import java.awt.image.BufferedImage;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.reflect.Field;
-import java.nio.file.Path;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.Supplier;
-import java.util.stream.IntStream;
-
-public interface ImageRenderer {
- int render(GuiGraphics graphics, int x, int y, int renderWidth);
-
- void close();
-
- default void tick() {}
-
- Map<ResourceLocation, CompletableFuture<Optional<ImageRenderer>>> CACHE = new ConcurrentHashMap<>();
-
- static CompletableFuture<Optional<ImageRenderer>> getOrMakeAsync(ResourceLocation id, Supplier<Optional<ImageRenderer>> factory) {
- return CACHE.computeIfAbsent(id, key -> CompletableFuture.supplyAsync(factory, YACLConstants.SINGLE_THREAD_EXECUTOR));
- }
-
- static CompletableFuture<Optional<ImageRenderer>> getOrMakeSync(ResourceLocation id, Supplier<Optional<ImageRenderer>> factory) {
- return CACHE.computeIfAbsent(id, key -> CompletableFuture.completedFuture(factory.get()));
- }
-
- static void closeAll() {
- CACHE.values().forEach(future -> future.thenAccept(opt -> opt.ifPresent(ImageRenderer::close)));
- CACHE.clear();
- }
-
- class TextureBacked implements ImageRenderer {
- private final ResourceLocation location;
- private final int width, height;
- private final int textureWidth, textureHeight;
- private final float u, v;
-
- public TextureBacked(ResourceLocation location, float u, float v, int width, int height, int textureWidth, int textureHeight) {
- this.location = location;
- this.width = width;
- this.height = height;
- this.textureWidth = textureWidth;
- this.textureHeight = textureHeight;
- this.u = u;
- this.v = v;
- }
-
- @Override
- public int render(GuiGraphics graphics, int x, int y, int renderWidth) {
- float ratio = renderWidth / (float)this.width;
- int targetHeight = (int) (this.height * ratio);
-
- graphics.pose().pushPose();
- graphics.pose().translate(x, y, 0);
- graphics.pose().scale(ratio, ratio, 1);
- graphics.blit(location, 0, 0, this.u, this.v, this.width, this.height, this.textureWidth, this.textureHeight);
- graphics.pose().popPose();
-
- return targetHeight;
- }
-
- @Override
- public void close() {
-
- }
- }
-
- class NativeImageBacked implements ImageRenderer {
- protected static final TextureManager textureManager = Minecraft.getInstance().getTextureManager();
-
- protected NativeImage image;
- protected DynamicTexture texture;
- protected final ResourceLocation uniqueLocation;
- protected final int width, height;
-
- public NativeImageBacked(NativeImage image, ResourceLocation uniqueLocation) {
- this.image = image;
- this.texture = new DynamicTexture(image);
- this.uniqueLocation = uniqueLocation;
- textureManager.register(this.uniqueLocation, this.texture);
- this.width = image.getWidth();
- this.height = image.getHeight();
- }
-
- private NativeImageBacked(Path imagePath, ResourceLocation uniqueLocation) throws IOException {
- this.uniqueLocation = uniqueLocation;
- this.image = NativeImage.read(new FileInputStream(imagePath.toFile()));
- this.width = image.getWidth();
- this.height = image.getHeight();
- this.texture = new DynamicTexture(image);
- textureManager.register(this.uniqueLocation, this.texture);
- }
-
- public static Optional<ImageRenderer> createFromPath(Path path, ResourceLocation uniqueLocation) {
- try {
- return Optional.of(new NativeImageBacked(path, uniqueLocation));
- } catch (IOException e) {
- e.printStackTrace();
- return Optional.empty();
- }
- }
-
- @Override
- public int render(GuiGraphics graphics, int x, int y, int renderWidth) {
- if (image == null) return 0;
-
- float ratio = renderWidth / (float)this.width;
- int targetHeight = (int) (this.height * ratio);
-
- graphics.pose().pushPose();
- graphics.pose().translate(x, y, 0);
- graphics.pose().scale(ratio, ratio, 1);
- graphics.blit(uniqueLocation, 0, 0, 0, 0, this.width, this.height, this.width, this.height);
- graphics.pose().popPose();
-
- return targetHeight;
- }
-
- @Override
- public void close() {
- image.close();
- image = null;
- texture = null;
- textureManager.release(uniqueLocation);
- }
- }
-
- class AnimatedNativeImageBacked extends NativeImageBacked {
- private int currentFrame;
- private double lastFrameTime;
-
- private final double[] frameDelays;
- private final int frameCount;
-
- private final int packCols, packRows;
- private final int frameWidth, frameHeight;
-
- public AnimatedNativeImageBacked(NativeImage image, int frameWidth, int frameHeight, int frameCount, double[] frameDelayMS, int packCols, int packRows, ResourceLocation uniqueLocation) {
- super(image, uniqueLocation);
- this.frameWidth = frameWidth;
- this.frameHeight = frameHeight;
- this.frameCount = frameCount;
- this.frameDelays = frameDelayMS;
- this.packCols = packCols;
- this.packRows = packRows;
- }
-
- public static AnimatedNativeImageBacked createGIFFromTexture(ResourceLocation textureLocation) throws IOException {
- ResourceManager resourceManager = Minecraft.getInstance().getResourceManager();
- Resource resource = resourceManager.getResource(textureLocation).orElseThrow();
-
- return createGIF(resource.open(), textureLocation);
- }
-
- public static AnimatedNativeImageBacked createWEBPFromTexture(ResourceLocation textureLocation) throws IOException {
- ResourceManager resourceManager = Minecraft.getInstance().getResourceManager();
- Resource resource = resourceManager.getResource(textureLocation).orElseThrow();
-
- return createWEBP(resource.open(), textureLocation);
- }
-
- public static AnimatedNativeImageBacked createGIF(InputStream is, ResourceLocation uniqueLocation) {
- try (is) {
- ImageReader reader = ImageIO.getImageReadersBySuffix("gif").next();
- reader.setInput(ImageIO.createImageInputStream(is));
-
- AnimFrameProvider animFrameFunction = i -> {
- IIOMetadata metadata = reader.getImageMetadata(i);
- String metaFormatName = metadata.getNativeMetadataFormatName();
- IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(metaFormatName);
- IIOMetadataNode graphicsControlExtensionNode = (IIOMetadataNode) root.getElementsByTagName("GraphicControlExtension").item(0);
- int delay = Integer.parseInt(graphicsControlExtensionNode.getAttribute("delayTime")) * 10;
-
- return new AnimFrame(delay, 0, 0);
- };
-
- return createFromImageReader(reader, animFrameFunction, uniqueLocation);
- } catch (Exception e) {
- CrashReport crashReport = CrashReport.forThrowable(e, "Failed to load GIF image");
- CrashReportCategory category = crashReport.addCategory("YACL Gui");
- category.setDetail("Image identifier", uniqueLocation.toString());
- throw new ReportedException(crashReport);
- }
- }
-
- public static AnimatedNativeImageBacked createWEBP(InputStream is, ResourceLocation uniqueLocation) {
- try (is) {
- ImageReader reader = new WebPImageReaderSpi().createReaderInstance();
- reader.setInput(ImageIO.createImageInputStream(is));
-
- int numImages = reader.getNumImages(true); // Force reading of all frames
- AnimFrameProvider animFrameFunction = i -> null;
- if (numImages > 1) {
- // WebP reader does not expose frame delay, prepare for reflection hell
- Class<?> webpReaderClass = Class.forName("com.twelvemonkeys.imageio.plugins.webp.WebPImageReader");
- Field framesField = webpReaderClass.getDeclaredField("frames");
- framesField.setAccessible(true);
- List<?> frames = (List<?>) framesField.get(reader);
-
- Class<?> animationFrameClass = Class.forName("com.twelvemonkeys.imageio.plugins.webp.AnimationFrame");
- Field durationField = animationFrameClass.getDeclaredField("duration");
- durationField.setAccessible(true);
- Field boundsField = animationFrameClass.getDeclaredField("bounds");
- boundsField.setAccessible(true);
-
- animFrameFunction = i -> {
- Rectangle bounds = (Rectangle) boundsField.get(frames.get(i));
- return new AnimFrame((int) durationField.get(frames.get(i)), bounds.x, bounds.y);
- };
- // that was fun
- }
-
- return createFromImageReader(reader, animFrameFunction, uniqueLocation);
- } catch (Throwable e) {
- CrashReport crashReport = CrashReport.forThrowable(e, "Failed to load WEBP image");
- CrashReportCategory category = crashReport.addCategory("YACL Gui");
- category.setDetail("Image identifier", uniqueLocation.toString());
- throw new ReportedException(crashReport);
- }
- }
-
- private static AnimatedNativeImageBacked createFromImageReader(ImageReader reader, AnimFrameProvider animationProvider, ResourceLocation uniqueLocation) throws Exception {
- if (reader.isSeekForwardOnly()) {
- throw new RuntimeException("Image reader is not seekable");
- }
-
- int frameCount = reader.getNumImages(true);
-
- // Because this is being backed into a texture atlas, we need a maximum dimension
- // so you can get the texture atlas size.
- // Smaller frames are given black borders
- int frameWidth = IntStream.range(0, frameCount).map(i -> {
- try {
- return reader.getWidth(i);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }).max().orElseThrow();
- int frameHeight = IntStream.range(0, frameCount).map(i -> {
- try {
- return reader.getHeight(i);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }).max().orElseThrow();
-
- // Packs the frames into an optimal 1:1 texture.
- // OpenGL can only have texture axis with a max of 32768 pixels,
- // and packing them to that length is not efficient, apparently.
- double ratio = frameWidth / (double)frameHeight;
- int cols = (int)Math.ceil(Math.sqrt(frameCount) / Math.sqrt(ratio));
- int rows = (int)Math.ceil(frameCount / (double)cols);
-
- NativeImage image = new NativeImage(NativeImage.Format.RGBA, frameWidth * cols, frameHeight * rows, false);
-
-// // Fill whole atlas with black, as each frame may have different dimensions
-// // that would cause borders of transparent pixels to appear around the frames
-// for (int x = 0; x < frameWidth * cols; x++) {
-// for (int y = 0; y < frameHeight * rows; y++) {
-// image.setPixelRGBA(x, y, 0xFF000000);
-// }
-// }
-
- BufferedImage bi = null;
- Graphics2D graphics = null;
-
- // each frame may have a different delay
- double[] frameDelays = new double[frameCount];
-
- for (int i = 0; i < frameCount; i++) {
- AnimFrame frame = animationProvider.get(i);
- if (frameCount > 1) // frame will be null if not animation
- frameDelays[i] = frame.durationMS;
-
- if (bi == null) {
- // first frame...
- bi = reader.read(i);
- graphics = bi.createGraphics();
- } else {
- // WebP reader sometimes provides delta frames, (only the pixels that changed since the last frame)
- // so instead of overwriting the image every frame, we draw delta frames on top of the previous frame
- // to keep a complete image.
- BufferedImage deltaFrame = reader.read(i);
- graphics.drawImage(deltaFrame, frame.xOffset, frame.yOffset, null);
- }
-
- // Each frame may have different dimensions, so we need to center them.
- int xOffset = (frameWidth - bi.getWidth()) / 2;
- int yOffset = (frameHeight - bi.getHeight()) / 2;
-
- for (int w = 0; w < bi.getWidth(); w++) {
- for (int h = 0; h < bi.getHeight(); h++) {
- int rgb = bi.getRGB(w, h);
- int r = FastColor.ARGB32.red(rgb);
- int g = FastColor.ARGB32.green(rgb);
- int b = FastColor.ARGB32.blue(rgb);
- int a = FastColor.ARGB32.alpha(rgb);
-
- int col = i % cols;
- int row = (int) Math.floor(i / (double)cols);
-
- image.setPixelRGBA(
- frameWidth * col + w + xOffset,
- frameHeight * row + h + yOffset,
- FastColor.ABGR32.color(a, b, g, r) // NativeImage uses ABGR for some reason
- );
- }
- }
- }
-
- if (graphics != null)
- graphics.dispose();
- reader.dispose();
-
- return new AnimatedNativeImageBacked(image, frameWidth, frameHeight, frameCount, frameDelays, cols, rows, uniqueLocation);
- }
-
- @Override
- public int render(GuiGraphics graphics, int x, int y, int renderWidth) {
- if (image == null) return 0;
-
- float ratio = renderWidth / (float)frameWidth;
- int targetHeight = (int) (frameHeight * ratio);
-
- int currentCol = currentFrame % packCols;
- int currentRow = (int) Math.floor(currentFrame / (double)packCols);
-
- graphics.pose().pushPose();
- graphics.pose().translate(x, y, 0);
- graphics.pose().scale(ratio, ratio, 1);
- graphics.blit(
- uniqueLocation,
- 0, 0,
- frameWidth * currentCol, frameHeight * currentRow,
- frameWidth, frameHeight,
- this.width, this.height
- );
- graphics.pose().popPose();
-
- if (frameCount > 1) {
- double timeMS = Blaze3D.getTime() * 1000;
- if (lastFrameTime == 0) lastFrameTime = timeMS;
- if (timeMS - lastFrameTime >= frameDelays[currentFrame]) {
- currentFrame++;
- lastFrameTime = timeMS;
- }
- if (currentFrame >= frameCount - 1)
- currentFrame = 0;
- }
-
- return targetHeight;
- }
-
- @FunctionalInterface
- private interface AnimFrameProvider {
- AnimFrame get(int frame) throws Exception;
- }
- private record AnimFrame(int durationMS, int xOffset, int yOffset) {}
- }
-}
diff --git a/common/src/main/java/dev/isxander/yacl3/gui/OptionDescriptionWidget.java b/common/src/main/java/dev/isxander/yacl3/gui/OptionDescriptionWidget.java
index d0db6e4..0732c5f 100644
--- a/common/src/main/java/dev/isxander/yacl3/gui/OptionDescriptionWidget.java
+++ b/common/src/main/java/dev/isxander/yacl3/gui/OptionDescriptionWidget.java
@@ -2,6 +2,7 @@ package dev.isxander.yacl3.gui;
import com.mojang.blaze3d.Blaze3D;
import com.mojang.blaze3d.platform.InputConstants;
+import dev.isxander.yacl3.gui.image.ImageRenderer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.ComponentPath;
import net.minecraft.client.gui.Font;
@@ -76,7 +77,7 @@ public class OptionDescriptionWidget extends AbstractWidget {
if (description.description().image().isDone()) {
var image = description.description().image().join();
if (image.isPresent()) {
- y += image.get().render(graphics, getX(), y, getWidth()) + 5;
+ y += image.get().render(graphics, getX(), y, getWidth(), delta) + 5;
}
}
diff --git a/common/src/main/java/dev/isxander/yacl3/gui/image/ImageRenderer.java b/common/src/main/java/dev/isxander/yacl3/gui/image/ImageRenderer.java
new file mode 100644
index 0000000..d3fb4bf
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/gui/image/ImageRenderer.java
@@ -0,0 +1,11 @@
+package dev.isxander.yacl3.gui.image;
+
+import net.minecraft.client.gui.GuiGraphics;
+
+public interface ImageRenderer {
+ int render(GuiGraphics graphics, int x, int y, int renderWidth, float tickDelta);
+
+ void close();
+
+ default void tick() {}
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/gui/image/ImageRendererFactory.java b/common/src/main/java/dev/isxander/yacl3/gui/image/ImageRendererFactory.java
new file mode 100644
index 0000000..480d8b1
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/gui/image/ImageRendererFactory.java
@@ -0,0 +1,24 @@
+package dev.isxander.yacl3.gui.image;
+
+public interface ImageRendererFactory<T extends ImageRenderer> {
+ /**
+ * Prepares the image. This can be run off-thread,
+ * and should NOT contain any GL calls whatsoever.
+ */
+ ImageSupplier<T> prepareImage() throws Exception;
+
+ default boolean requiresOffThreadPreparation() {
+ return true;
+ }
+
+ interface ImageSupplier<T extends ImageRenderer> {
+ T completeImage() throws Exception;
+ }
+
+ interface OnThread<T extends ImageRenderer> extends ImageRendererFactory<T> {
+ @Override
+ default boolean requiresOffThreadPreparation() {
+ return false;
+ }
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/gui/image/ImageRendererManager.java b/common/src/main/java/dev/isxander/yacl3/gui/image/ImageRendererManager.java
new file mode 100644
index 0000000..b1f133a
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/gui/image/ImageRendererManager.java
@@ -0,0 +1,76 @@
+package dev.isxander.yacl3.gui.image;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+import dev.isxander.yacl3.impl.utils.YACLConstants;
+import net.minecraft.resources.ResourceLocation;
+
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.*;
+
+public class ImageRendererManager {
+ private static final ExecutorService SINGLE_THREAD_EXECUTOR = Executors.newSingleThreadExecutor(task -> new Thread(task, "YACL Image Prep"));
+
+ private static final Map<ResourceLocation, CompletableFuture<ImageRenderer>> IMAGE_CACHE = new ConcurrentHashMap<>();
+ private static final Queue<FactoryIDPair<?>> CREATION_QUEUE = new ConcurrentLinkedQueue<>();
+
+ public static <T extends ImageRenderer> CompletableFuture<T> registerImage(ResourceLocation id, ImageRendererFactory<T> factory) {
+ SINGLE_THREAD_EXECUTOR.submit(() -> {
+ try {
+ ImageRendererFactory.ImageSupplier<T> supplier = factory.prepareImage();
+ CREATION_QUEUE.add(new FactoryIDPair<>(id, supplier));
+ } catch (Exception e) {
+ YACLConstants.LOGGER.error("Failed to prepare image '{}'", id, e);
+ IMAGE_CACHE.remove(id);
+ }
+ });
+
+ var future = new CompletableFuture<ImageRenderer>();
+ IMAGE_CACHE.put(id, future);
+ return (CompletableFuture<T>) future;
+ }
+
+ public static void pollImageFactories() {
+ RenderSystem.assertOnRenderThread();
+
+ while (!CREATION_QUEUE.isEmpty()) {
+ FactoryIDPair<?> pair = CREATION_QUEUE.poll();
+
+ // sanity check - this should never happen
+ if (!IMAGE_CACHE.containsKey(pair.id())) {
+ YACLConstants.LOGGER.error("Tried to finalise image '{}' but it was not found in cache.", pair.id());
+ continue;
+ }
+
+ ImageRenderer image;
+ try {
+ image = pair.supplier().completeImage();
+ } catch (Exception e) {
+ YACLConstants.LOGGER.error("Failed to create image '{}'", pair.id(), e);
+ continue;
+ }
+
+ CompletableFuture<ImageRenderer> future = IMAGE_CACHE.get(pair.id());
+ // another sanity check - this should never happen
+ if (future.isDone()) {
+ YACLConstants.LOGGER.error("Image '{}' was already completed", pair.id());
+ continue;
+ }
+
+ future.complete(image);
+ }
+ }
+
+ public static void closeAll() {
+ SINGLE_THREAD_EXECUTOR.shutdownNow();
+ CREATION_QUEUE.clear();
+ IMAGE_CACHE.values().forEach(future -> {
+ if (future.isDone()) {
+ future.join().close();
+ }
+ });
+ IMAGE_CACHE.clear();
+ }
+
+ private record FactoryIDPair<T extends ImageRenderer>(ResourceLocation id, ImageRendererFactory.ImageSupplier<T> supplier) {}
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/gui/image/impl/AnimatedDynamicTextureImage.java b/common/src/main/java/dev/isxander/yacl3/gui/image/impl/AnimatedDynamicTextureImage.java
new file mode 100644
index 0000000..edf92a5
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/gui/image/impl/AnimatedDynamicTextureImage.java
@@ -0,0 +1,282 @@
+package dev.isxander.yacl3.gui.image.impl;
+
+import com.mojang.blaze3d.Blaze3D;
+import com.mojang.blaze3d.platform.NativeImage;
+import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi;
+import dev.isxander.yacl3.gui.image.ImageRendererFactory;
+import dev.isxander.yacl3.impl.utils.YACLConstants;
+import net.minecraft.CrashReport;
+import net.minecraft.CrashReportCategory;
+import net.minecraft.ReportedException;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.GuiGraphics;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.packs.resources.Resource;
+import net.minecraft.server.packs.resources.ResourceManager;
+import net.minecraft.util.FastColor;
+
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReader;
+import javax.imageio.metadata.IIOMetadata;
+import javax.imageio.metadata.IIOMetadataNode;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.stream.IntStream;
+
+public class AnimatedDynamicTextureImage extends DynamicTextureImage {
+ private int currentFrame;
+ private double lastFrameTime;
+
+ private final double[] frameDelays;
+ private final int frameCount;
+
+ private final int packCols, packRows;
+ private final int frameWidth, frameHeight;
+
+ public AnimatedDynamicTextureImage(NativeImage image, int frameWidth, int frameHeight, int frameCount, double[] frameDelayMS, int packCols, int packRows, ResourceLocation uniqueLocation) {
+ super(image, uniqueLocation);
+ this.frameWidth = frameWidth;
+ this.frameHeight = frameHeight;
+ this.frameCount = frameCount;
+ this.frameDelays = frameDelayMS;
+ this.packCols = packCols;
+ this.packRows = packRows;
+ }
+
+ @Override
+ public int render(GuiGraphics graphics, int x, int y, int renderWidth, float tickDelta) {
+ if (image == null) return 0;
+
+ float ratio = renderWidth / (float)frameWidth;
+ int targetHeight = (int) (frameHeight * ratio);
+
+ int currentCol = currentFrame % packCols;
+ int currentRow = (int) Math.floor(currentFrame / (double)packCols);
+
+ graphics.pose().pushPose();
+ graphics.pose().translate(x, y, 0);
+ graphics.pose().scale(ratio, ratio, 1);
+ graphics.blit(
+ uniqueLocation,
+ 0, 0,
+ frameWidth * currentCol, frameHeight * currentRow,
+ frameWidth, frameHeight,
+ this.width, this.height
+ );
+ graphics.pose().popPose();
+
+ if (frameCount > 1) {
+ double timeMS = Blaze3D.getTime() * 1000;
+ if (lastFrameTime == 0) lastFrameTime = timeMS;
+ if (timeMS - lastFrameTime >= frameDelays[currentFrame]) {
+ currentFrame++;
+ lastFrameTime = timeMS;
+ }
+ if (currentFrame >= frameCount - 1)
+ currentFrame = 0;
+ }
+
+ return targetHeight;
+ }
+
+ public static ImageRendererFactory<AnimatedDynamicTextureImage> createGIFFromTexture(ResourceLocation textureLocation) {
+ return () -> {
+ ResourceManager resourceManager = Minecraft.getInstance().getResourceManager();
+ Resource resource = resourceManager.getResource(textureLocation).orElseThrow();
+
+ return createGIFSupplier(resource.open(), textureLocation);
+ };
+ }
+
+ public static ImageRendererFactory<AnimatedDynamicTextureImage> createGIFFromPath(Path path, ResourceLocation uniqueLocation) {
+ return () -> createGIFSupplier(new FileInputStream(path.toFile()), uniqueLocation);
+ }
+
+ public static ImageRendererFactory<AnimatedDynamicTextureImage> createWEBPFromTexture(ResourceLocation textureLocation) {
+ return () -> {
+ ResourceManager resourceManager = Minecraft.getInstance().getResourceManager();
+ Resource resource = resourceManager.getResource(textureLocation).orElseThrow();
+
+ return createWEBPSupplier(resource.open(), textureLocation);
+ };
+ }
+
+ public static ImageRendererFactory<AnimatedDynamicTextureImage> createWEBPFromPath(Path path, ResourceLocation uniqueLocation) {
+ return () -> createWEBPSupplier(new FileInputStream(path.toFile()), uniqueLocation);
+ }
+
+ private static ImageRendererFactory.ImageSupplier<AnimatedDynamicTextureImage> createGIFSupplier(InputStream is, ResourceLocation uniqueLocation) {
+ try (is) {
+ ImageReader reader = ImageIO.getImageReadersBySuffix("gif").next();
+ reader.setInput(ImageIO.createImageInputStream(is));
+
+ AnimFrameProvider animFrameFunction = i -> {
+ IIOMetadata metadata = reader.getImageMetadata(i);
+ String metaFormatName = metadata.getNativeMetadataFormatName();
+ IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(metaFormatName);
+ IIOMetadataNode graphicsControlExtensionNode = (IIOMetadataNode) root.getElementsByTagName("GraphicControlExtension").item(0);
+ int delay = Integer.parseInt(graphicsControlExtensionNode.getAttribute("delayTime")) * 10;
+
+ return new AnimFrame(delay, 0, 0);
+ };
+
+ return createFromImageReader(reader, animFrameFunction, uniqueLocation);
+ } catch (Exception e) {
+ CrashReport crashReport = CrashReport.forThrowable(e, "Failed to load GIF image");
+ CrashReportCategory category = crashReport.addCategory("YACL Gui");
+ category.setDetail("Image identifier", uniqueLocation.toString());
+ throw new ReportedException(crashReport);
+ }
+ }
+
+ private static ImageRendererFactory.ImageSupplier<AnimatedDynamicTextureImage> createWEBPSupplier(InputStream is, ResourceLocation uniqueLocation) {
+ try (is) {
+ ImageReader reader = new WebPImageReaderSpi().createReaderInstance();
+ reader.setInput(ImageIO.createImageInputStream(is));
+
+ int numImages = reader.getNumImages(true); // Force reading of all frames
+ AnimFrameProvider animFrameFunction = i -> null;
+ if (numImages > 1) {
+ // WebP reader does not expose frame delay, prepare for reflection hell
+ Class<?> webpReaderClass = Class.forName("com.twelvemonkeys.imageio.plugins.webp.WebPImageReader");
+ Field framesField = webpReaderClass.getDeclaredField("frames");
+ framesField.setAccessible(true);
+ java.util.List<?> frames = (List<?>) framesField.get(reader);
+
+ Class<?> animationFrameClass = Class.forName("com.twelvemonkeys.imageio.plugins.webp.AnimationFrame");
+ Field durationField = animationFrameClass.getDeclaredField("duration");
+ durationField.setAccessible(true);
+ Field boundsField = animationFrameClass.getDeclaredField("bounds");
+ boundsField.setAccessible(true);
+
+ animFrameFunction = i -> {
+ Rectangle bounds = (Rectangle) boundsField.get(frames.get(i));
+ return new AnimFrame((int) durationField.get(frames.get(i)), bounds.x, bounds.y);
+ };
+ // that was fun
+ }
+
+ return createFromImageReader(reader, animFrameFunction, uniqueLocation);
+ } catch (Throwable e) {
+ CrashReport crashReport = CrashReport.forThrowable(e, "Failed to load WEBP image");
+ CrashReportCategory category = crashReport.addCategory("YACL Gui");
+ category.setDetail("Image identifier", uniqueLocation.toString());
+ throw new ReportedException(crashReport);
+ }
+ }
+
+ private static ImageRendererFactory.ImageSupplier<AnimatedDynamicTextureImage> createFromImageReader(ImageReader reader, AnimFrameProvider animationProvider, ResourceLocation uniqueLocation) throws Exception {
+ YACLConstants.LOGGER.info("Thread 1: {}", Thread.currentThread().getName());
+
+ if (reader.isSeekForwardOnly()) {
+ throw new RuntimeException("Image reader is not seekable");
+ }
+
+ int frameCount = reader.getNumImages(true);
+
+ // Because this is being backed into a texture atlas, we need a maximum dimension
+ // so you can get the texture atlas size.
+ // Smaller frames are given black borders
+ int frameWidth = IntStream.range(0, frameCount).map(i -> {
+ try {
+ return reader.getWidth(i);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }).max().orElseThrow();
+ int frameHeight = IntStream.range(0, frameCount).map(i -> {
+ try {
+ return reader.getHeight(i);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }).max().orElseThrow();
+
+ // Packs the frames into an optimal 1:1 texture.
+ // OpenGL can only have texture axis with a max of 32768 pixels,
+ // and packing them to that length is not efficient, apparently.
+ double ratio = frameWidth / (double)frameHeight;
+ int cols = (int)Math.ceil(Math.sqrt(frameCount) / Math.sqrt(ratio));
+ int rows = (int)Math.ceil(frameCount / (double)cols);
+
+ NativeImage image = new NativeImage(NativeImage.Format.RGBA, frameWidth * cols, frameHeight * rows, false);
+
+// // Fill whole atlas with black, as each frame may have different dimensions
+// // that would cause borders of transparent pixels to appear around the frames
+// for (int x = 0; x < frameWidth * cols; x++) {
+// for (int y = 0; y < frameHeight * rows; y++) {
+// image.setPixelRGBA(x, y, 0xFF000000);
+// }
+// }
+
+ BufferedImage bi = null;
+ Graphics2D graphics = null;
+
+ // each frame may have a different delay
+ double[] frameDelays = new double[frameCount];
+
+ for (int i = 0; i < frameCount; i++) {
+ AnimFrame frame = animationProvider.get(i);
+ if (frameCount > 1) // frame will be null if not animation
+ frameDelays[i] = frame.durationMS;
+
+ if (bi == null) {
+ // first frame...
+ bi = reader.read(i);
+ graphics = bi.createGraphics();
+ } else {
+ // WebP reader sometimes provides delta frames, (only the pixels that changed since the last frame)
+ // so instead of overwriting the image every frame, we draw delta frames on top of the previous frame
+ // to keep a complete image.
+ BufferedImage deltaFrame = reader.read(i);
+ graphics.drawImage(deltaFrame, frame.xOffset, frame.yOffset, null);
+ }
+
+ // Each frame may have different dimensions, so we need to center them.
+ int xOffset = (frameWidth - bi.getWidth()) / 2;
+ int yOffset = (frameHeight - bi.getHeight()) / 2;
+
+ for (int w = 0; w < bi.getWidth(); w++) {
+ for (int h = 0; h < bi.getHeight(); h++) {
+ int rgb = bi.getRGB(w, h);
+ int r = FastColor.ARGB32.red(rgb);
+ int g = FastColor.ARGB32.green(rgb);
+ int b = FastColor.ARGB32.blue(rgb);
+ int a = FastColor.ARGB32.alpha(rgb);
+
+ int col = i % cols;
+ int row = (int) Math.floor(i / (double)cols);
+
+ image.setPixelRGBA(
+ frameWidth * col + w + xOffset,
+ frameHeight * row + h + yOffset,
+ FastColor.ABGR32.color(a, b, g, r) // NativeImage uses ABGR for some reason
+ );
+ }
+ }
+ }
+
+ if (graphics != null)
+ graphics.dispose();
+ reader.dispose();
+
+ return () -> {
+ YACLConstants.LOGGER.info("Thread 2: {}", Thread.currentThread().getName());
+ return new AnimatedDynamicTextureImage(image, frameWidth, frameHeight, frameCount, frameDelays, cols, rows, uniqueLocation);
+ };
+ }
+
+ @FunctionalInterface
+ private interface AnimFrameProvider {
+ AnimFrame get(int frame) throws Exception;
+ }
+
+ private record AnimFrame(int durationMS, int xOffset, int yOffset) {}
+
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/gui/image/impl/DynamicTextureImage.java b/common/src/main/java/dev/isxander/yacl3/gui/image/impl/DynamicTextureImage.java
new file mode 100644
index 0000000..e0449e9
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/gui/image/impl/DynamicTextureImage.java
@@ -0,0 +1,62 @@
+package dev.isxander.yacl3.gui.image.impl;
+
+import com.mojang.blaze3d.platform.NativeImage;
+import com.mojang.blaze3d.systems.RenderSystem;
+import dev.isxander.yacl3.gui.image.ImageRenderer;
+import dev.isxander.yacl3.gui.image.ImageRendererFactory;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.GuiGraphics;
+import net.minecraft.client.renderer.texture.DynamicTexture;
+import net.minecraft.client.renderer.texture.TextureManager;
+import net.minecraft.resources.ResourceLocation;
+
+import java.io.FileInputStream;
+import java.nio.file.Path;
+
+public class DynamicTextureImage implements ImageRenderer {
+ protected static final TextureManager textureManager = Minecraft.getInstance().getTextureManager();
+
+ protected NativeImage image;
+ protected DynamicTexture texture;
+ protected final ResourceLocation uniqueLocation;
+ protected final int width, height;
+
+ public DynamicTextureImage(NativeImage image, ResourceLocation location) {
+ RenderSystem.assertOnRenderThread();
+
+ this.image = image;
+ this.texture = new DynamicTexture(image);
+ this.uniqueLocation = location;
+ textureManager.register(this.uniqueLocation, this.texture);
+ this.width = image.getWidth();
+ this.height = image.getHeight();
+ }
+
+ @Override
+ public int render(GuiGraphics graphics, int x, int y, int renderWidth, float tickDelta) {
+ if (image == null) return 0;
+
+ float ratio = renderWidth / (float)this.width;
+ int targetHeight = (int) (this.height * ratio);
+
+ graphics.pose().pushPose();
+ graphics.pose().translate(x, y, 0);
+ graphics.pose().scale(ratio, ratio, 1);
+ graphics.blit(uniqueLocation, 0, 0, 0, 0, this.width, this.height, this.width, this.height);
+ graphics.pose().popPose();
+
+ return targetHeight;
+ }
+
+ @Override
+ public void close() {
+ image.close();
+ image = null;
+ texture = null;
+ textureManager.release(uniqueLocation);
+ }
+
+ public static ImageRendererFactory<DynamicTextureImage> fromPath(Path imagePath, ResourceLocation location) {
+ return (ImageRendererFactory.OnThread<DynamicTextureImage>) () -> () -> new DynamicTextureImage(NativeImage.read(new FileInputStream(imagePath.toFile())), location);
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/gui/image/impl/ResourceTextureImage.java b/common/src/main/java/dev/isxander/yacl3/gui/image/impl/ResourceTextureImage.java
new file mode 100644
index 0000000..8c2b3b5
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/gui/image/impl/ResourceTextureImage.java
@@ -0,0 +1,46 @@
+package dev.isxander.yacl3.gui.image.impl;
+
+import dev.isxander.yacl3.gui.image.ImageRenderer;
+import dev.isxander.yacl3.gui.image.ImageRendererFactory;
+import net.minecraft.client.gui.GuiGraphics;
+import net.minecraft.resources.ResourceLocation;
+
+public class ResourceTextureImage implements ImageRenderer {
+ private final ResourceLocation location;
+ private final int width, height;
+ private final int textureWidth, textureHeight;
+ private final float u, v;
+
+ public ResourceTextureImage(ResourceLocation location, float u, float v, int width, int height, int textureWidth, int textureHeight) {
+ this.location = location;
+ this.width = width;
+ this.height = height;
+ this.textureWidth = textureWidth;
+ this.textureHeight = textureHeight;
+ this.u = u;
+ this.v = v;
+ }
+
+ @Override
+ public int render(GuiGraphics graphics, int x, int y, int renderWidth, float tickDelta) {
+ float ratio = renderWidth / (float)this.width;
+ int targetHeight = (int) (this.height * ratio);
+
+ graphics.pose().pushPose();
+ graphics.pose().translate(x, y, 0);
+ graphics.pose().scale(ratio, ratio, 1);
+ graphics.blit(location, 0, 0, this.u, this.v, this.width, this.height, this.textureWidth, this.textureHeight);
+ graphics.pose().popPose();
+
+ return targetHeight;
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ public static ImageRendererFactory<ResourceTextureImage> createFactory(ResourceLocation location, float u, float v, int width, int height, int textureWidth, int textureHeight) {
+ return (ImageRendererFactory.OnThread<ResourceTextureImage>) () -> () -> new ResourceTextureImage(location, u, v, width, height, textureWidth, textureHeight);
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/impl/OptionDescriptionImpl.java b/common/src/main/java/dev/isxander/yacl3/impl/OptionDescriptionImpl.java
index 9ea9456..3a3008c 100644
--- a/common/src/main/java/dev/isxander/yacl3/impl/OptionDescriptionImpl.java
+++ b/common/src/main/java/dev/isxander/yacl3/impl/OptionDescriptionImpl.java
@@ -1,7 +1,11 @@
package dev.isxander.yacl3.impl;
import dev.isxander.yacl3.api.OptionDescription;
-import dev.isxander.yacl3.gui.ImageRenderer;
+import dev.isxander.yacl3.gui.image.ImageRenderer;
+import dev.isxander.yacl3.gui.image.ImageRendererManager;
+import dev.isxander.yacl3.gui.image.impl.AnimatedDynamicTextureImage;
+import dev.isxander.yacl3.gui.image.impl.DynamicTextureImage;
+import dev.isxander.yacl3.gui.image.impl.ResourceTextureImage;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.resources.ResourceLocation;
@@ -37,7 +41,7 @@ public record OptionDescriptionImpl(Component text, CompletableFuture<Optional<I
Validate.isTrue(width > 0, "Width must be greater than 0!");
Validate.isTrue(height > 0, "Height must be greater than 0!");
- this.image = ImageRenderer.getOrMakeSync(image, () -> Optional.of(new ImageRenderer.TextureBacked(image, 0, 0, width, height, width, height)));
+ this.image = ImageRendererManager.registerImage(image, ResourceTextureImage.createFactory(image, 0, 0, width, height, width, height)).thenApply(Optional::of);
imageUnset = false;
return this;
}
@@ -48,7 +52,7 @@ public record OptionDescriptionImpl(Component text, CompletableFuture<Optional<I
Validate.isTrue(width > 0, "Width must be greater than 0!");
Validate.isTrue(height > 0, "Height must be greater than 0!");
- this.image = ImageRenderer.getOrMakeSync(image, () -> Optional.of(new ImageRenderer.TextureBacked(image, u, v, width, height, textureWidth, textureHeight)));
+ this.image = ImageRendererManager.registerImage(image, ResourceTextureImage.createFactory(image, u, v, width, height, textureWidth, textureHeight)).thenApply(Optional::of);
imageUnset = false;
return this;
}
@@ -56,7 +60,8 @@ public record OptionDescriptionImpl(Component text, CompletableFuture<Optional<I
@Override
public Builder image(Path path, ResourceLocation uniqueLocation) {
Validate.isTrue(imageUnset, "Image already set!");
- this.image = ImageRenderer.getOrMakeAsync(uniqueLocation, () -> ImageRenderer.NativeImageBacked.createFromPath(path, uniqueLocation));
+
+ this.image = ImageRendererManager.registerImage(uniqueLocation, DynamicTextureImage.fromPath(path, uniqueLocation)).thenApply(Optional::of);
imageUnset = false;
return this;
}
@@ -64,14 +69,8 @@ public record OptionDescriptionImpl(Component text, CompletableFuture<Optional<I
@Override
public Builder gifImage(ResourceLocation image) {
Validate.isTrue(imageUnset, "Image already set!");
- this.image = ImageRenderer.getOrMakeAsync(image, () -> {
- try {
- return Optional.of(ImageRenderer.AnimatedNativeImageBacked.createGIFFromTexture(image));
- } catch (IOException e) {
- e.printStackTrace();
- return Optional.empty();
- }
- });
+
+ this.image = ImageRendererManager.registerImage(image, AnimatedDynamicTextureImage.createGIFFromTexture(image)).thenApply(Optional::of);
imageUnset = false;
return this;
}
@@ -79,14 +78,8 @@ public record OptionDescriptionImpl(Component text, CompletableFuture<Optional<I
@Override
public Builder gifImage(Path path, ResourceLocation uniqueLocation) {
Validate.isTrue(imageUnset, "Image already set!");
- this.image = ImageRenderer.getOrMakeAsync(uniqueLocation, () -> {
- try {
- return Optional.of(ImageRenderer.AnimatedNativeImageBacked.createGIF(new FileInputStream(path.toFile()), uniqueLocation));
- } catch (IOException e) {
- e.printStackTrace();
- return Optional.empty();
- }
- });
+
+ this.image = ImageRendererManager.registerImage(uniqueLocation, AnimatedDynamicTextureImage.createGIFFromPath(path, uniqueLocation)).thenApply(Optional::of);
imageUnset = false;
return this;
}
@@ -94,14 +87,8 @@ public record OptionDescriptionImpl(Component text, CompletableFuture<Optional<I
@Override
public Builder webpImage(ResourceLocation image) {
Validate.isTrue(imageUnset, "Image already set!");
- this.image = ImageRenderer.getOrMakeAsync(image, () -> {
- try {
- return Optional.of(ImageRenderer.AnimatedNativeImageBacked.createWEBPFromTexture(image));
- } catch (IOException e) {
- e.printStackTrace();
- return Optional.empty();
- }
- });
+
+ this.image = ImageRendererManager.registerImage(image, AnimatedDynamicTextureImage.createWEBPFromTexture(image)).thenApply(Optional::of);
imageUnset = false;
return this;
}
@@ -109,14 +96,8 @@ public record OptionDescriptionImpl(Component text, CompletableFuture<Optional<I
@Override
public Builder webpImage(Path path, ResourceLocation uniqueLocation) {
Validate.isTrue(imageUnset, "Image already set!");
- this.image = ImageRenderer.getOrMakeAsync(uniqueLocation, () -> {
- try {
- return Optional.of(ImageRenderer.AnimatedNativeImageBacked.createWEBP(new FileInputStream(path.toFile()), uniqueLocation));
- } catch (IOException e) {
- e.printStackTrace();
- return Optional.empty();
- }
- });
+
+ this.image = ImageRendererManager.registerImage(uniqueLocation, AnimatedDynamicTextureImage.createWEBPFromPath(path, uniqueLocation)).thenApply(Optional::of);
imageUnset = false;
return this;
}
diff --git a/common/src/main/java/dev/isxander/yacl3/impl/utils/YACLConstants.java b/common/src/main/java/dev/isxander/yacl3/impl/utils/YACLConstants.java
index 9570b02..5ff1b79 100644
--- a/common/src/main/java/dev/isxander/yacl3/impl/utils/YACLConstants.java
+++ b/common/src/main/java/dev/isxander/yacl3/impl/utils/YACLConstants.java
@@ -3,11 +3,6 @@ package dev.isxander.yacl3.impl.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
public class YACLConstants {
public static final Logger LOGGER = LoggerFactory.getLogger("YetAnotherConfigLib");
-
- public static final ExecutorService SINGLE_THREAD_EXECUTOR = Executors.newSingleThreadExecutor();
}
diff --git a/common/src/main/java/dev/isxander/yacl3/mixin/MinecraftMixin.java b/common/src/main/java/dev/isxander/yacl3/mixin/MinecraftMixin.java
index 0681213..0b228a1 100644
--- a/common/src/main/java/dev/isxander/yacl3/mixin/MinecraftMixin.java
+++ b/common/src/main/java/dev/isxander/yacl3/mixin/MinecraftMixin.java
@@ -1,6 +1,6 @@
package dev.isxander.yacl3.mixin;
-import dev.isxander.yacl3.gui.ImageRenderer;
+import dev.isxander.yacl3.gui.image.ImageRendererManager;
import net.minecraft.client.Minecraft;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@@ -11,6 +11,11 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
public class MinecraftMixin {
@Inject(method = "destroy", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;close()V", shift = At.Shift.BEFORE))
private void closeImages(CallbackInfo ci) {
- ImageRenderer.closeAll();
+ ImageRendererManager.closeAll();
+ }
+
+ @Inject(method = "runTick", at = @At(value = "HEAD"))
+ private void finaliseImages(boolean tick, CallbackInfo ci) {
+ ImageRendererManager.pollImageFactories();
}
}