From 5cd3b40add730d50d3ff13d6d80d96ec6fd92463 Mon Sep 17 00:00:00 2001 From: embeddedt <42941056+embeddedt@users.noreply.github.com> Date: Wed, 22 Feb 2023 20:12:06 -0500 Subject: [PATCH] Add 1.18 version of resource pack optimization --- .../core/config/ModernFixEarlyConfig.java | 1 + .../PathResourcePackMixin.java | 149 ++++++++++++++++++ .../VanillaPackResourcesMixin.java | 73 +++++++++ src/main/resources/modernfix.mixins.json | 2 + 4 files changed, 225 insertions(+) create mode 100644 src/main/java/org/embeddedt/modernfix/mixin/perf/modern_resourcepacks/PathResourcePackMixin.java create mode 100644 src/main/java/org/embeddedt/modernfix/mixin/perf/modern_resourcepacks/VanillaPackResourcesMixin.java diff --git a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java index 3d957100..607290f7 100644 --- a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java +++ b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java @@ -17,6 +17,7 @@ public class ModernFixEarlyConfig { // Defines the default rules which can be configured by the user or other mods. // You must manually add a rule for any new mixins not covered by an existing package rule. this.addMixinRule("core", true); // TODO: Don't actually allow the user to disable this + this.addMixinRule("perf.modern_resourcepacks", true); this.addMixinRule("feature.measure_time", true); this.addMixinRule("feature.reduce_loading_screen_freezes", false); this.addMixinRule("perf.remove_biome_temperature_cache", true); diff --git a/src/main/java/org/embeddedt/modernfix/mixin/perf/modern_resourcepacks/PathResourcePackMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/perf/modern_resourcepacks/PathResourcePackMixin.java new file mode 100644 index 00000000..dd64c22d --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/mixin/perf/modern_resourcepacks/PathResourcePackMixin.java @@ -0,0 +1,149 @@ +package org.embeddedt.modernfix.mixin.perf.modern_resourcepacks; + +import com.google.common.base.Joiner; +import net.minecraft.server.packs.PackType; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.fml.loading.moddiscovery.ModFile; +import net.minecraftforge.resource.PathResourcePack; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Mixin(PathResourcePack.class) +public abstract class PathResourcePackMixin { + @Shadow public abstract Set getNamespaces(PackType type); + + @Shadow protected abstract Path resolve(String... paths); + + private EnumMap> namespacesByType; + private EnumMap>> rootListingByNamespaceAndType; + private boolean hasGeneratedListings = false; + private Set containedPaths; + + private FileSystem resourcePackFS; + + private static Joiner slashJoiner = Joiner.on('/'); + + @Inject(method = "", at = @At("TAIL")) + private void cacheResources(String packName, Path source, CallbackInfo ci) { + this.resourcePackFS = source.getFileSystem(); + this.namespacesByType = new EnumMap<>(PackType.class); + } + + private boolean generateResourceCache() { + synchronized (this.namespacesByType) { + if(!this.namespacesByType.containsKey(PackType.CLIENT_RESOURCES) || !this.namespacesByType.containsKey(PackType.SERVER_DATA)) + return false; + } + if(hasGeneratedListings) + return true; + EnumMap>> rootListingByNamespaceAndType = new EnumMap<>(PackType.class); + HashSet containedPaths = new HashSet<>(); + for(PackType type : PackType.values()) { + Set namespaces; + synchronized (this.namespacesByType) { + namespaces = this.namespacesByType.get(type); + } + HashMap> rootListingForNamespaces = new HashMap<>(); + for(String namespace : namespaces) { + try { + Path root = this.resolve(type.getDirectory(), namespace).toAbsolutePath(); + try (Stream stream = Files.walk(root)) { + ArrayList rootListingPaths = new ArrayList<>(); + stream + .map(path -> root.relativize(path.toAbsolutePath())) + .filter(this::isValidCachedResourcePath) + .forEach(path -> { + if(!path.toString().endsWith(".mcmeta")) + rootListingPaths.add(path); + String mergedPath = slashJoiner.join(type.getDirectory(), namespace, path); + containedPaths.add(mergedPath); + }); + rootListingPaths.trimToSize(); + rootListingForNamespaces.put(namespace, rootListingPaths); + } + } catch(IOException e) { + rootListingForNamespaces.put(namespace, Collections.emptyList()); + } + } + rootListingByNamespaceAndType.put(type, rootListingForNamespaces); + } + this.rootListingByNamespaceAndType = rootListingByNamespaceAndType; + this.containedPaths = containedPaths; + this.hasGeneratedListings = true; + return true; + } + + private boolean isValidCachedResourcePath(Path path) { + String str = path.toString(); + for(int i = 0; i < str.length(); i++) { + if(!ResourceLocation.validPathChar(str.charAt(i))) { + return false; + } + } + return true; + } + + @Inject(method = "getNamespaces", at = @At("HEAD"), cancellable = true) + private void useCacheForNamespaces(PackType type, CallbackInfoReturnable> cir) { + Set cachedNamespaces; + synchronized (this.namespacesByType) { + cachedNamespaces = this.namespacesByType.get(type); + } + if(cachedNamespaces != null) { + cir.setReturnValue(cachedNamespaces); + } + } + + @Inject(method = "getNamespaces", at = @At("TAIL")) + private void storeCacheForNamespaces(PackType type, CallbackInfoReturnable> cir) { + synchronized (this.namespacesByType) { + this.namespacesByType.put(type, cir.getReturnValue()); + } + } + + @Inject(method = "hasResource(Ljava/lang/String;)Z", at = @At(value = "HEAD"), cancellable = true) + private void useCacheForExistence(String path, CallbackInfoReturnable cir) { + if(!this.generateResourceCache()) { + return; + } + cir.setReturnValue(this.containedPaths.contains(path)); + } + + /** + * @author embeddedt + * @reason Use cached listing of mod resources + */ + @Inject(method = "getResources", at = @At("HEAD"), cancellable = true) + public void getResources(PackType type, String resourceNamespace, String pathIn, int maxDepth, Predicate filter, CallbackInfoReturnable> cir) + { + if(!this.generateResourceCache()) { + return; + } + Path inputPath = this.resourcePackFS.getPath(pathIn); + Collection cachedListing = this.rootListingByNamespaceAndType.get(type).getOrDefault(resourceNamespace, Collections.emptyList()).stream(). + filter(path -> path.getNameCount() <= maxDepth). // Make sure the depth is within bounds + filter(path -> path.startsWith(inputPath)). // Make sure the target path is inside this one + filter(path -> filter.test(path.getFileName().toString())). // Test the file name against the predicate + // Finally we need to form the RL, so use the first name as the domain, and the rest as the path + // It is VERY IMPORTANT that we do not rely on Path.toString as this is inconsistent between operating systems + // Join the path names ourselves to force forward slashes + map(path -> new ResourceLocation(resourceNamespace, slashJoiner.join(path))). + collect(Collectors.toList()); + cir.setReturnValue(cachedListing); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/mixin/perf/modern_resourcepacks/VanillaPackResourcesMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/perf/modern_resourcepacks/VanillaPackResourcesMixin.java new file mode 100644 index 00000000..3eec88fb --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/mixin/perf/modern_resourcepacks/VanillaPackResourcesMixin.java @@ -0,0 +1,73 @@ +package org.embeddedt.modernfix.mixin.perf.modern_resourcepacks; + +import com.google.common.base.Joiner; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.LoadingCache; +import net.minecraft.server.packs.PackType; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.VanillaPackResources; +import net.minecraft.server.packs.metadata.pack.PackMetadataSection; +import org.apache.commons.lang3.tuple.Pair; +import org.embeddedt.modernfix.FileWalker; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.io.IOException; +import java.nio.file.*; +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.stream.Stream; + +@Mixin(VanillaPackResources.class) +public class VanillaPackResourcesMixin { + @Shadow @Final private static Map ROOT_DIR_BY_TYPE; + private static LoadingCache, List> pathStreamLoadingCache = CacheBuilder.newBuilder() + .build(FileWalker.INSTANCE); + + private static Set containedPaths = null; + + @Inject(method = "", at = @At("TAIL")) + private void cacheContainedPaths(PackMetadataSection arg, String[] p_i47912_1_, CallbackInfo ci) { + if(containedPaths != null) + return; + containedPaths = new HashSet<>(); + Joiner slashJoiner = Joiner.on('/'); + for(PackType type : PackType.values()) { + Path root = ROOT_DIR_BY_TYPE.get(type); + if(root == null) + throw new IllegalStateException("No filesystem for vanilla " + type.name() + " assets"); + try { + try(Stream stream = Files.walk(root)) { + stream + .map(path -> root.relativize(path.toAbsolutePath())) + .forEach(path -> containedPaths.add(slashJoiner.join(type.getDirectory(), path))); + } + } catch(IOException e) { + e.printStackTrace(); + } + } + } + + @Redirect(method = "getResources(Ljava/util/Collection;ILjava/lang/String;Ljava/nio/file/Path;Ljava/lang/String;Ljava/util/function/Predicate;)V", at = @At(value = "INVOKE", target = "Ljava/nio/file/Files;walk(Ljava/nio/file/Path;I[Ljava/nio/file/FileVisitOption;)Ljava/util/stream/Stream;")) + private static Stream useCacheForLoading(Path path, int maxDepth, FileVisitOption[] fileVisitOptions) throws IOException { + try { + return pathStreamLoadingCache.get(Pair.of(path, maxDepth)).stream(); + } catch (ExecutionException e) { + if(e.getCause() instanceof IOException) /* generally always should be */ + throw (IOException)e.getCause(); + else + throw new IOException(e); + } + } + + @Inject(method = "hasResource", at = @At(value = "INVOKE", target = "Ljava/lang/Class;getResource(Ljava/lang/String;)Ljava/net/URL;"), cancellable = true) + private void useCacheForExistence(PackType type, ResourceLocation location, CallbackInfoReturnable cir) { + cir.setReturnValue(containedPaths.contains(type.getDirectory() + "/" + location.getNamespace() + "/" + location.getPath())); + } +} diff --git a/src/main/resources/modernfix.mixins.json b/src/main/resources/modernfix.mixins.json index 0a9d3b6c..0c6b3800 100644 --- a/src/main/resources/modernfix.mixins.json +++ b/src/main/resources/modernfix.mixins.json @@ -7,6 +7,8 @@ "refmap": "modernfix.refmap.json", "mixins": [ "bugfix.edge_chunk_not_saved.ChunkManagerMixin", + "perf.modern_resourcepacks.PathResourcePackMixin", + "perf.modern_resourcepacks.VanillaPackResourcesMixin", "perf.remove_biome_temperature_cache.BiomeMixin", "perf.reduce_blockstate_cache_rebuilds.GameDataMixin", "perf.reduce_blockstate_cache_rebuilds.BlockCallbacksMixin",