diff --git a/common/addons/library-image/build.gradle.kts b/common/addons/library-image/build.gradle.kts index e39ddc12a..703657471 100644 --- a/common/addons/library-image/build.gradle.kts +++ b/common/addons/library-image/build.gradle.kts @@ -1,4 +1,4 @@ -version = version("1.0.0") +version = version("1.0.1") dependencies { compileOnlyApi(project(":common:addons:manifest-addon-loader")) diff --git a/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/ImageLibraryAddon.java b/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/ImageLibraryAddon.java index 9ef07e187..0e9739588 100644 --- a/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/ImageLibraryAddon.java +++ b/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/ImageLibraryAddon.java @@ -7,6 +7,11 @@ import java.util.function.Supplier; import com.dfsek.terra.addons.image.colorsampler.ColorSampler; import com.dfsek.terra.addons.image.config.ColorLoader; import com.dfsek.terra.addons.image.config.ColorLoader.ColorString; +import com.dfsek.terra.addons.image.config.ImageLibraryPackConfigTemplate; +import com.dfsek.terra.addons.image.config.noisesampler.ChannelNoiseSamplerTemplate; +import com.dfsek.terra.addons.image.config.noisesampler.DistanceTransformNoiseSamplerTemplate; +import com.dfsek.terra.addons.image.config.image.ImageTemplate; +import com.dfsek.terra.addons.image.config.image.StitchedImageTemplate; import com.dfsek.terra.addons.image.config.colorsampler.ConstantColorSamplerTemplate; import com.dfsek.terra.addons.image.config.colorsampler.image.SingleImageColorSamplerTemplate; import com.dfsek.terra.addons.image.config.colorsampler.image.TileImageColorSamplerTemplate; @@ -52,6 +57,10 @@ public class ImageLibraryAddon implements AddonInitializer { .getHandler(FunctionalEventHandler.class) .register(addon, ConfigPackPreLoadEvent.class) .priority(10) + .then(event -> { + ImageLibraryPackConfigTemplate config = event.loadTemplate(new ImageLibraryPackConfigTemplate()); + event.getPack().getContext().put(config); + }) .then(event -> { ConfigPack pack = event.getPack(); CheckedRegistry>> imageRegistry = pack.getOrCreateRegistry(IMAGE_REGISTRY_KEY); diff --git a/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/config/ImageLibraryPackConfigTemplate.java b/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/config/ImageLibraryPackConfigTemplate.java new file mode 100644 index 000000000..b3a78caed --- /dev/null +++ b/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/config/ImageLibraryPackConfigTemplate.java @@ -0,0 +1,37 @@ +package com.dfsek.terra.addons.image.config; + +import com.dfsek.tectonic.api.config.template.ConfigTemplate; + +import com.dfsek.tectonic.api.config.template.annotations.Default; +import com.dfsek.tectonic.api.config.template.annotations.Description; +import com.dfsek.tectonic.api.config.template.annotations.Value; + +import com.dfsek.terra.api.properties.Properties; + +public class ImageLibraryPackConfigTemplate implements ConfigTemplate, Properties { + // TODO - These would be better as plugin wide config parameters in config.yml + + @Value("images.cache.load-on-use") + @Description("If set to true, images will load into memory upon use rather than on pack load.") + @Default + private boolean loadOnUse = false; + + @Value("images.cache.timeout") + @Description("How many seconds to keep images loaded in the image cache for. " + + "If set to a number greater than 0, images will be removed from memory if not used after the timeout, otherwise images will stay loaded in memory. " + + "Setting the timeout to greater than 0 will trade decreased memory consumption when not performing any image reads for a period of time for extra processing time required to perform cache lookups.") + @Default + private int cacheTimeout = 0; + + public boolean loadOnUse() { + return loadOnUse; + } + + public boolean unloadOnTimeout() { + return cacheTimeout > 0; + } + + public int getCacheTimeout() { + return cacheTimeout; + } +} diff --git a/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/config/image/ImageCache.java b/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/config/image/ImageCache.java index 5f96ea390..7558d4bfa 100644 --- a/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/config/image/ImageCache.java +++ b/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/config/image/ImageCache.java @@ -3,44 +3,60 @@ package com.dfsek.terra.addons.image.config.image; import javax.imageio.ImageIO; import java.io.FileNotFoundException; import java.io.IOException; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import com.dfsek.terra.addons.image.config.ImageLibraryPackConfigTemplate; import com.dfsek.terra.addons.image.image.BufferedImageWrapper; import com.dfsek.terra.addons.image.image.Image; +import com.dfsek.terra.addons.image.image.SuppliedImage; import com.dfsek.terra.api.config.ConfigPack; import com.dfsek.terra.api.config.Loader; import com.dfsek.terra.api.properties.Properties; +import com.dfsek.terra.api.util.generic.Lazy; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; + /* * Cache prevents configs from loading the same image multiple times into memory */ -record ImageCache(ConcurrentHashMap map) implements Properties { +record ImageCache(LoadingCache cache) implements Properties { public static Image load(String path, ConfigPack pack, Loader files) throws IOException { - ImageCache cache; + ImageLibraryPackConfigTemplate config = pack.getContext().get(ImageLibraryPackConfigTemplate.class); + ImageCache images; if(!pack.getContext().has(ImageCache.class)) { - cache = new ImageCache(new ConcurrentHashMap<>()); - pack.getContext().put(cache); - } else { - cache = pack.getContext().get(ImageCache.class); + var cacheBuilder = Caffeine.newBuilder(); + if (config.unloadOnTimeout()) cacheBuilder.expireAfterAccess(config.getCacheTimeout(), TimeUnit.SECONDS); + images = new ImageCache(cacheBuilder.build(s -> loadImage(s, files))); + pack.getContext().put(images); + } else images = pack.getContext().get(ImageCache.class); + + if (config.loadOnUse()) { + if(config.unloadOnTimeout()) { // Grab directly from cache if images are to unload on timeout + return new SuppliedImage(() -> images.cache.get(path)); + } else { + // If images do not time out, image can be lazily loaded once instead of performing cache lookups for each image operation + Lazy lazyImage = Lazy.lazy(() -> images.cache.get(path)); + return new SuppliedImage(lazyImage::value); + } } - if(cache.map.containsKey(path)) { - return cache.map.get(path); - } else { - try { - BufferedImageWrapper image = new BufferedImageWrapper(ImageIO.read(files.get(path))); - cache.map.put(path, image); - return image; - } catch(IllegalArgumentException e) { - throw new IllegalArgumentException("Unable to load image (image might be too large?)", e); - } catch(IOException e) { - if(e instanceof FileNotFoundException) { - // Rethrow using nicer message - throw new IOException("Unable to load image: No such file or directory: " + path, e); - } - throw new IOException("Unable to load image", e); + return images.cache.get(path); + } + + private static Image loadImage(String path, Loader files) throws IOException { + try { + return new BufferedImageWrapper(ImageIO.read(files.get(path))); + } catch(IllegalArgumentException e) { + throw new IllegalArgumentException("Unable to load image (image might be too large?)", e); + } catch(IOException e) { + if(e instanceof FileNotFoundException) { + // Rethrow using nicer message + throw new IOException("Unable to load image: No such file or directory: " + path, e); } + throw new IOException("Unable to load image", e); } } } diff --git a/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/image/SuppliedImage.java b/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/image/SuppliedImage.java new file mode 100644 index 000000000..99fcbfcd3 --- /dev/null +++ b/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/image/SuppliedImage.java @@ -0,0 +1,27 @@ +package com.dfsek.terra.addons.image.image; + +import java.util.function.Supplier; + +public class SuppliedImage implements Image { + + private final Supplier imageSupplier; + + public SuppliedImage(Supplier imageSupplier) { + this.imageSupplier = imageSupplier; + } + + @Override + public int getRGB(int x, int y) { + return imageSupplier.get().getRGB(x, y); + } + + @Override + public int getWidth() { + return imageSupplier.get().getWidth(); + } + + @Override + public int getHeight() { + return imageSupplier.get().getHeight(); + } +}