diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/feature/cause_lag_by_disabling_threads/UtilMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/feature/cause_lag_by_disabling_threads/UtilMixin.java index a8b452bd..a9d494f4 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/feature/cause_lag_by_disabling_threads/UtilMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/feature/cause_lag_by_disabling_threads/UtilMixin.java @@ -1,7 +1,7 @@ package org.embeddedt.modernfix.common.mixin.feature.cause_lag_by_disabling_threads; import net.minecraft.Util; -import org.embeddedt.modernfix.util.DirectExecutorService; +import org.embeddedt.modernfix.util.SingleThreadedWorkerService; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mutable; @@ -12,5 +12,5 @@ import java.util.concurrent.ExecutorService; @Mixin(Util.class) public class UtilMixin { @Shadow @Final @Mutable - private static final ExecutorService BACKGROUND_EXECUTOR = new DirectExecutorService(); + private static final ExecutorService BACKGROUND_EXECUTOR = new SingleThreadedWorkerService(); } diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_entity_models/CubeDefinitionMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_entity_models/CubeDefinitionMixin.java new file mode 100644 index 00000000..29ee0411 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_entity_models/CubeDefinitionMixin.java @@ -0,0 +1,40 @@ +package org.embeddedt.modernfix.common.mixin.perf.compact_entity_models; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import net.minecraft.client.model.geom.ModelPart; +import net.minecraft.client.model.geom.builders.CubeDefinition; +import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +@Mixin(CubeDefinition.class) +@ClientOnlyMixin +public class CubeDefinitionMixin { + @Unique + private static final ConcurrentHashMap, ModelPart.Cube> MFIX_CUBE_CACHE = new ConcurrentHashMap<>(); + + /** + * @author embeddedt + * @reason deduplicate creation of Cube objects + */ + @WrapOperation(method = "bake", at = @At(value = "NEW", target = "(IIFFFFFFFFFZFFLjava/util/Set;)Lnet/minecraft/client/model/geom/ModelPart$Cube;")) + private ModelPart.Cube modernfix$deduplicateCube(int texCoordU, int texCoordV, float originX, float originY, float originZ, + float dimensionX, float dimensionY, float dimensionZ, float gtowX, + float growY, float growZ, boolean mirror, float texScaleU, + float texScaleV, Set visibleFaces, + Operation original) { + List cacheKey = List.of(texCoordU, texCoordV, originX, originY, originZ, dimensionX, dimensionY, dimensionZ, gtowX, growY, growZ, mirror, texScaleU, texScaleV, visibleFaces); + var cube = MFIX_CUBE_CACHE.get(cacheKey); + if (cube == null) { + cube = original.call((Object[])cacheKey.toArray()); + MFIX_CUBE_CACHE.put(cacheKey, cube); + } + return cube; + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_languages/ClientLanguageMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_languages/ClientLanguageMixin.java new file mode 100644 index 00000000..01595bdc --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_languages/ClientLanguageMixin.java @@ -0,0 +1,57 @@ +package org.embeddedt.modernfix.common.mixin.perf.dynamic_languages; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import net.minecraft.client.resources.language.ClientLanguage; +import net.minecraft.server.packs.resources.Resource; +import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.embeddedt.modernfix.dynamiclanguages.DynamicLanguageMap; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArg; + +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; + +/** + * Modifies the language system to load/unload the contents of language entries based on GC pressure. + */ +@Mixin(ClientLanguage.class) +@ClientOnlyMixin +public class ClientLanguageMixin { + private static final ThreadLocal MFIX_MODIFY_APPEND_SEMANTICS = ThreadLocal.withInitial(() -> Boolean.FALSE); + + /** + * @author embeddedt + * @reason modify the semantics of appendFrom so that it's used to do a prepass + */ + @ModifyArg(method = "appendFrom", at = @At(value = "INVOKE", target = "Lnet/minecraft/locale/Language;loadFromJson(Ljava/io/InputStream;Ljava/util/function/BiConsumer;)V"), index = 1) + private static BiConsumer changeSemanticsOfConsumer(BiConsumer consumer, @Local(ordinal = 0, argsOnly = true) Map destinationMap, @Local(ordinal = 0) Resource resource) { + return MFIX_MODIFY_APPEND_SEMANTICS.get() ? ((k, v) -> destinationMap.put(k, resource)) : consumer; + } + + /** + * @author embeddedt + * @reason collect resources that own keys with a prepass + */ + @WrapOperation(method = "loadFrom", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/language/ClientLanguage;appendFrom(Ljava/lang/String;Ljava/util/List;Ljava/util/Map;)V")) + private static void trackEntrySource(String languageName, List resources, Map destinationMap, Operation original) { + MFIX_MODIFY_APPEND_SEMANTICS.set(true); + try { + original.call(languageName, resources, destinationMap); + } finally { + MFIX_MODIFY_APPEND_SEMANTICS.remove(); + } + } + + /** + * @author embeddedt + * @reason figure out which keys are dynamically loaded and which are injected by mixins + */ + @ModifyArg(method = "loadFrom", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/language/ClientLanguage;(Ljava/util/Map;Z)V"), index = 0) + private static Map modifyLanguageMap(Map storage) { + return DynamicLanguageMap.forStorage(Map.copyOf(storage)); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/forge_registry_alloc/DebugLevelSourceMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/forge_registry_alloc/DebugLevelSourceMixin.java new file mode 100644 index 00000000..ecf240c1 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/forge_registry_alloc/DebugLevelSourceMixin.java @@ -0,0 +1,38 @@ +package org.embeddedt.modernfix.common.mixin.perf.forge_registry_alloc; + +import net.minecraft.world.level.levelgen.DebugLevelSource; +import net.neoforged.neoforge.registries.GameData; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import java.util.AbstractList; +import java.util.stream.Collector; +import java.util.stream.Stream; + +@Mixin(DebugLevelSource.class) +public class DebugLevelSourceMixin { + /** + * @author embeddedt + * @reason Reuse the existing blockstate list held by Forge instead of making a new one + */ + @Redirect(method = "initValidStates", at = @At(value = "INVOKE", target = "Ljava/util/stream/Stream;collect(Ljava/util/stream/Collector;)Ljava/lang/Object;", ordinal = 0), remap = false) + private static Object getStateList(Stream instance, Collector arCollector) { + var idMapper = GameData.getBlockStateIDMap(); + return new AbstractList<>() { + @Override + public int size() { + return idMapper.size(); + } + + @Override + public Object get(int index) { + var o = idMapper.byId(index); + if (o == null) { + throw new IndexOutOfBoundsException(); + } + return o; + } + }; + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/safety/LivingEntityRendererMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/safety/LivingEntityRendererMixin.java index 8e165b7b..c6dac5de 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/safety/LivingEntityRendererMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/safety/LivingEntityRendererMixin.java @@ -1,28 +1,34 @@ package org.embeddedt.modernfix.common.mixin.safety; +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.client.Minecraft; +import net.minecraft.client.model.EntityModel; import net.minecraft.client.renderer.entity.LivingEntityRenderer; import net.minecraft.client.renderer.entity.layers.RenderLayer; +import net.minecraft.world.entity.Entity; +import org.embeddedt.modernfix.ModernFix; import org.embeddedt.modernfix.annotation.ClientOnlyMixin; -import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Mutable; 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 java.util.Collections; -import java.util.List; @Mixin(LivingEntityRenderer.class) @ClientOnlyMixin -public class LivingEntityRendererMixin { - @Shadow @Final @Mutable - protected List> layers; +public abstract class LivingEntityRendererMixin> { + @Shadow + public abstract boolean addLayer(RenderLayer layer); - @Inject(method = "", at = @At("RETURN")) - private void synchronizeLayerList(CallbackInfo ci) { - /* allows buggy mods to call addLayer concurrently, order is not deterministic but can't fix that */ - this.layers = Collections.synchronizedList(layers); + /** + * @author embeddedt + * @reason avoid CMEs from buggy mods calling addLayer on wrong thread + */ + @WrapMethod(method = "addLayer") + private boolean handleOffThreadLayerAdd(RenderLayer layer, Operation original) { + if (!Minecraft.getInstance().isSameThread()) { + ModernFix.LOGGER.error("LivingEntityRenderer.addLayer called on wrong thread", new Exception()); + Minecraft.getInstance().tell(() -> this.addLayer(layer)); + return true; + } + return original.call(layer); } } 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 ccd16151..00563452 100644 --- a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java +++ b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java @@ -173,15 +173,12 @@ public class ModernFixEarlyConfig { .put("mixin.feature.blockentity_incorrect_thread", false) .put("mixin.perf.clear_mixin_classinfo", false) .put("mixin.perf.deduplicate_climate_parameters", false) - .put("mixin.perf.faster_capabilities.bytecode_analysis", false) .put("mixin.bugfix.packet_leak", false) .put("mixin.perf.deduplicate_location", false) .put("mixin.perf.dynamic_entity_renderers", false) .put("mixin.feature.integrated_server_watchdog", true) .put("mixin.perf.faster_item_rendering", false) - .put("mixin.perf.ingredient_item_deduplication", false) .put("mixin.feature.spam_thread_dump", false) - .put("mixin.feature.disable_unihex_font", false) .put("mixin.feature.remove_chat_signing", false) .put("mixin.bugfix.skip_redundant_saves", false) .put("mixin.feature.snapshot_easter_egg", true) @@ -193,6 +190,12 @@ public class ModernFixEarlyConfig { .putConditionally(() -> !isFabric, "mixin.bugfix.fix_config_crashes", true) .putConditionally(() -> !isFabric, "mixin.feature.registry_event_progress", true) .putConditionally(() -> isFabric, "mixin.perf.clear_fabric_mapping_tables", false) + // Beta (promote on next release) + .put("mixin.perf.compact_entity_models", false) + .put("mixin.perf.dynamic_languages", false) + .put("mixin.perf.faster_capabilities.bytecode_analysis", false) + .put("mixin.perf.ingredient_item_deduplication", false) + // END .build(); private ModernFixEarlyConfig(File file) { @@ -236,7 +239,7 @@ public class ModernFixEarlyConfig { disableIfModPresent("mixin.bugfix.item_cache_flag", "lithium", "canary", "radium"); // DimThread makes changes to the server chunk manager (understandably), C2ME probably does the same disableIfModPresent("mixin.bugfix.chunk_deadlock", "c2me", "dimthread"); - disableIfModPresent("mixin.perf.reuse_datapacks", "tac"); + disableIfModPresent("mixin.perf.release_protochunks", "c2me"); disableIfModPresent("mixin.launch.class_search_cache", "optifine"); disableIfModPresent("mixin.perf.faster_texture_stitching", "optifine"); disableIfModPresent("mixin.bugfix.entity_pose_stack", "optifine"); diff --git a/src/main/java/org/embeddedt/modernfix/dynamiclanguages/DynamicLanguageMap.java b/src/main/java/org/embeddedt/modernfix/dynamiclanguages/DynamicLanguageMap.java new file mode 100644 index 00000000..a3471627 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/dynamiclanguages/DynamicLanguageMap.java @@ -0,0 +1,42 @@ +package org.embeddedt.modernfix.dynamiclanguages; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.Maps; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.locale.Language; +import net.minecraft.server.packs.resources.Resource; +import org.embeddedt.modernfix.ModernFix; + +import java.io.IOException; +import java.util.Map; + +public class DynamicLanguageMap { + public static Map forStorage(Map storage) { + LoadingCache> languageFileContents = CacheBuilder.newBuilder() + .softValues() + .build(new CacheLoader<>() { + @Override + public Map load(Resource resource) throws Exception { + Map data = new Object2ObjectOpenHashMap<>(); + try (var stream = resource.open()) { + Language.loadFromJson(stream, data::put); + } catch (IOException e) { + ModernFix.LOGGER.error("Error loading language data from {}", resource.sourcePackId(), e); + } + return data; + } + }); + return Maps.asMap(storage.keySet(), k -> { + var value = storage.get(k); + if (value instanceof Resource r) { + return languageFileContents.getUnchecked(r).getOrDefault(k, ""); + } else if (value instanceof String s) { + return s; + } else { + return null; + } + }); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/util/DirectExecutorService.java b/src/main/java/org/embeddedt/modernfix/util/DirectExecutorService.java deleted file mode 100644 index 83b5d0aa..00000000 --- a/src/main/java/org/embeddedt/modernfix/util/DirectExecutorService.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.embeddedt.modernfix.util; - -import org.jetbrains.annotations.NotNull; - -import java.util.List; -import java.util.concurrent.AbstractExecutorService; -import java.util.concurrent.TimeUnit; - -public class DirectExecutorService extends AbstractExecutorService { - private boolean isShutdown; - - @Override - public void shutdown() { - isShutdown = true; - } - - @NotNull - @Override - public List shutdownNow() { - isShutdown = true; - return List.of(); - } - - @Override - public boolean isShutdown() { - return isShutdown; - } - - @Override - public boolean isTerminated() { - return isShutdown; - } - - @Override - public boolean awaitTermination(long timeout, @NotNull TimeUnit unit) throws InterruptedException { - return true; - } - - @Override - public void execute(@NotNull Runnable command) { - command.run(); - } -} diff --git a/src/main/java/org/embeddedt/modernfix/util/SingleThreadedWorkerService.java b/src/main/java/org/embeddedt/modernfix/util/SingleThreadedWorkerService.java new file mode 100644 index 00000000..1682ece6 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/util/SingleThreadedWorkerService.java @@ -0,0 +1,68 @@ +package org.embeddedt.modernfix.util; + +import net.minecraft.util.thread.ProcessorMailbox; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Like {@link Executors#newSingleThreadExecutor()}, but handles the case where the background executor schedules + * a task to itself and waits for it the way a direct executor would. + */ +public class SingleThreadedWorkerService extends AbstractExecutorService { + private final AtomicReference thread = new AtomicReference<>(); + private final ExecutorService executorService; + + public SingleThreadedWorkerService() { + this.executorService = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "Worker-Main"); + t.setPriority(Thread.MIN_PRIORITY); + thread.set(t); + return t; + }); + } + + @Override + public void shutdown() { + executorService.shutdown(); + } + + @NotNull + @Override + public List shutdownNow() { + return executorService.shutdownNow(); + } + + @Override + public boolean isShutdown() { + return executorService.isShutdown(); + } + + @Override + public boolean isTerminated() { + return executorService.isTerminated(); + } + + @Override + public boolean awaitTermination(long timeout, @NotNull TimeUnit unit) throws InterruptedException { + return executorService.awaitTermination(timeout, unit); + } + + private static boolean isForcedAsyncCommand(Runnable command) { + return command instanceof ProcessorMailbox; + } + + @Override + public void execute(@NotNull Runnable command) { + if (!isForcedAsyncCommand(command) && Thread.currentThread() == thread.get()) { + command.run(); + } else { + executorService.execute(command); + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/world/gen/ChunkBiomeLookup.java b/src/main/java/org/embeddedt/modernfix/world/gen/ChunkBiomeLookup.java index 8c368c92..03c9b6bb 100644 --- a/src/main/java/org/embeddedt/modernfix/world/gen/ChunkBiomeLookup.java +++ b/src/main/java/org/embeddedt/modernfix/world/gen/ChunkBiomeLookup.java @@ -99,6 +99,7 @@ public class ChunkBiomeLookup implements Function> { public void dispose() { // Make sure we do not retain strong references to the biome holders Arrays.fill(biomes, null); + this.fallbackManager = null; } private boolean fetchBiomes(BiomeManager.NoiseBiomeSource source) {