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 3640dfa0..228627f1 100644 --- a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java +++ b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java @@ -72,6 +72,7 @@ public class ModernFixEarlyConfig { this.addMixinRule("perf.patchouli_deduplicate_books", modPresent("patchouli")); this.addMixinRule("perf.datapack_reload_exceptions", true); this.addMixinRule("perf.faster_texture_stitching", true); + this.addMixinRule("perf.faster_texture_loading", true); /* off by default in 1.18 because it doesn't work as well */ this.addMixinRule("perf.faster_singleplayer_load", false); /* Keep this off if JEI/REI isn't installed to prevent breaking vanilla gameplay */ diff --git a/src/main/java/org/embeddedt/modernfix/mixin/perf/faster_texture_loading/TextureAtlasMixin.java b/src/main/java/org/embeddedt/modernfix/mixin/perf/faster_texture_loading/TextureAtlasMixin.java new file mode 100644 index 00000000..79ce9970 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/mixin/perf/faster_texture_loading/TextureAtlasMixin.java @@ -0,0 +1,110 @@ +package org.embeddedt.modernfix.mixin.perf.faster_texture_loading; + +import com.mojang.blaze3d.platform.NativeImage; +import com.mojang.datafixers.util.Pair; +import net.minecraft.client.renderer.texture.MissingTextureAtlasSprite; +import net.minecraft.client.renderer.texture.TextureAtlas; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.resources.metadata.animation.AnimationMetadataSection; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraftforge.client.ForgeHooksClient; +import net.minecraftforge.fml.ModLoader; +import org.apache.commons.lang3.tuple.Triple; +import org.embeddedt.modernfix.ModernFix; +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.CallbackInfoReturnable; + +import java.io.IOException; +import java.io.InputStream; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; + +@Mixin(TextureAtlas.class) +public abstract class TextureAtlasMixin { + @Shadow protected abstract ResourceLocation getResourceLocation(ResourceLocation location); + + @Shadow protected abstract Collection getBasicSpriteInfos(ResourceManager resourceManager, Set spriteLocations); + + private Map> loadedImages; + private boolean usingFasterLoad; + /** + * @author embeddedt + * @reason simplify texture loading by loading whole image once, avoid slow PngInfo code + */ + @Redirect(method = "prepareToStitch", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/texture/TextureAtlas;getBasicSpriteInfos(Lnet/minecraft/server/packs/resources/ResourceManager;Ljava/util/Set;)Ljava/util/Collection;")) + private Collection loadImages(TextureAtlas atlas, ResourceManager manager, Set imageLocations) { + usingFasterLoad = ModLoader.isLoadingStateValid(); + // bail if Forge is erroring to avoid AT crashes + if(!usingFasterLoad) { + return getBasicSpriteInfos(manager, imageLocations); + } + List> futures = new ArrayList<>(); + ConcurrentLinkedQueue results = new ConcurrentLinkedQueue<>(); + loadedImages = new ConcurrentHashMap<>(); + for(ResourceLocation location : imageLocations) { + if(MissingTextureAtlasSprite.getLocation().equals(location)) + continue; + futures.add(CompletableFuture.runAsync(() -> { + try { + ResourceLocation fileLocation = this.getResourceLocation(location); + Resource resource = manager.getResource(fileLocation); + NativeImage image = NativeImage.read(resource.getInputStream()); + AnimationMetadataSection animData = resource.getMetadata(AnimationMetadataSection.SERIALIZER); + if (animData == null) { + animData = AnimationMetadataSection.EMPTY; + } + Pair dimensions = animData.getFrameSize(image.getWidth(), image.getHeight()); + loadedImages.put(location, Pair.of(resource, image)); + results.add(new TextureAtlasSprite.Info(location, dimensions.getFirst(), dimensions.getSecond(), animData)); + } catch(IOException e) { + ModernFix.LOGGER.error("Using missing texture, unable to load {} : {}", location, e); + } catch(RuntimeException e) { + ModernFix.LOGGER.error("Unable to parse metadata from {} : {}", location, e); + } + }, ModernFix.resourceReloadExecutor())); + } + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + return results; + } + + @Inject(method = "prepareToStitch", at = @At("RETURN")) + private void clearLoadedImages(CallbackInfoReturnable cir) { + loadedImages = Collections.emptyMap(); + } + + @Inject(method = "load(Lnet/minecraft/server/packs/resources/ResourceManager;Lnet/minecraft/client/renderer/texture/TextureAtlasSprite$Info;IIIII)Lnet/minecraft/client/renderer/texture/TextureAtlasSprite;", + at = @At("HEAD"), cancellable = true) + private void loadFromExisting(ResourceManager resourceManager, TextureAtlasSprite.Info spriteInfo, int storageX, int storageY, int mipLevel, int x, int y, CallbackInfoReturnable cir) { + if(!usingFasterLoad) + return; + Pair pair = loadedImages.get(spriteInfo.name()); + if(pair == null) { + ModernFix.LOGGER.error("Texture {} was not loaded in early stage", spriteInfo.name()); + cir.setReturnValue(null); + } else { + TextureAtlasSprite sprite = null; + try { + sprite = ForgeHooksClient.loadTextureAtlasSprite((TextureAtlas)(Object)this, resourceManager, spriteInfo, pair.getFirst(), storageX, storageY, x, y, mipLevel, pair.getSecond()); + if(sprite == null) + sprite = new TextureAtlasSprite((TextureAtlas)(Object)this, spriteInfo, mipLevel, storageX, storageY, x, y, pair.getSecond()); + } catch(RuntimeException e) { + ModernFix.LOGGER.error("Error loading texture {}: {}", spriteInfo.name(), e); + } finally { + try { + pair.getFirst().close(); + } catch(IOException ignored) { + // not much we can do + } + } + cir.setReturnValue(sprite); + } + } +} diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg index 01c9dd6b..c1e448c8 100644 --- a/src/main/resources/META-INF/accesstransformer.cfg +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -25,3 +25,4 @@ public net.minecraft.world.level.block.state.BlockBehaviour$Properties f_60889_ public net.minecraft.world.level.block.state.BlockBehaviour$Properties f_60888_ # destroyTime public net.minecraft.server.level.ServerChunkCache$MainThreadExecutor public net.minecraft.nbt.CompoundTag (Ljava/util/Map;)V # +public net.minecraft.client.renderer.texture.TextureAtlasSprite (Lnet/minecraft/client/renderer/texture/TextureAtlas;Lnet/minecraft/client/renderer/texture/TextureAtlasSprite$Info;IIIIILcom/mojang/blaze3d/platform/NativeImage;)V # diff --git a/src/main/resources/modernfix.mixins.json b/src/main/resources/modernfix.mixins.json index 3e397be6..6dfac2a9 100644 --- a/src/main/resources/modernfix.mixins.json +++ b/src/main/resources/modernfix.mixins.json @@ -72,6 +72,7 @@ "perf.cache_model_materials.BlockModelMixin", "perf.cache_model_materials.MultipartMixin", "perf.faster_texture_stitching.StitcherMixin", + "perf.faster_texture_loading.TextureAtlasMixin", "devenv.MinecraftMixin", "devenv.NarratorMixin" ],