diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dc478189..90c0a0a0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,10 +32,3 @@ jobs: with: files: 'bin/*' repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Add changelog to release - uses: irongut/EditRelease@v1.2.0 - with: - token: ${{ secrets.GITHUB_TOKEN }} - id: ${{ github.event.release.id }} - replacebody: true - files: "CHANGELOG.md" diff --git a/annotation-processor/build.gradle b/annotation-processor/build.gradle index beb7ea1b..2921bf3d 100644 --- a/annotation-processor/build.gradle +++ b/annotation-processor/build.gradle @@ -30,7 +30,7 @@ dependencies { } tasks.withType(JavaCompile) { - options.release = 21 + options.release = 17 } shadowJar { diff --git a/annotation-processor/src/main/java/org/fury_phoenix/mixinAp/annotation/ClientMixinValidator.java b/annotation-processor/src/main/java/org/fury_phoenix/mixinAp/annotation/ClientMixinValidator.java index 2d76bcef..0db91f28 100644 --- a/annotation-processor/src/main/java/org/fury_phoenix/mixinAp/annotation/ClientMixinValidator.java +++ b/annotation-processor/src/main/java/org/fury_phoenix/mixinAp/annotation/ClientMixinValidator.java @@ -90,24 +90,19 @@ public class ClientMixinValidator { } private boolean targetsClient(Object classTarget) { - return switch (classTarget) { - case TypeElement te -> - isClientMarked(te); - case TypeMirror tm -> { - var el = types.asElement(tm); - yield el != null ? targetsClient(el) : warn("TypeMirror of " + tm); - } - // If you're using a dollar sign in class names you are insane - case String s -> { - var te = - elemUtils.getTypeElement(toSourceString(s.split("\\$")[0])); - yield te != null ? targetsClient(te) : warn(s); - } - default -> - throw new IllegalArgumentException("Unhandled type: " + if (classTarget instanceof TypeElement te) { + return isClientMarked(te); + } else if (classTarget instanceof TypeMirror tm) { + var el = types.asElement(tm); + return el != null ? targetsClient(el) : warn("TypeMirror of " + tm); + } else if (classTarget instanceof String s) { + var te = elemUtils.getTypeElement(toSourceString(s.split("\\$")[0])); + return te != null ? targetsClient(te) : warn(s); + } else { + throw new IllegalArgumentException("Unhandled type: " + classTarget.getClass() + "\n" + "Stringified contents: " + classTarget.toString()); - }; + } } private boolean isClientMarked(TypeElement te) { diff --git a/build.gradle.kts b/build.gradle.kts index 37773c84..892d56d8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -89,9 +89,6 @@ tasks.named("jar") { } java { - toolchain { - languageVersion = JavaLanguageVersion.of(25) - } val curSourceCompatLevel = JavaVersion.VERSION_25 sourceCompatibility = curSourceCompatLevel targetCompatibility = curSourceCompatLevel @@ -212,7 +209,8 @@ tasks.named("build") { } publishMods { - file.set(tasks.named(finalJarTask).get().outputs.files.singleFile) + file.set(tasks.named(finalJarTask).flatMap { it.archiveFile }) + displayName.set(tasks.named(finalJarTask).flatMap { it.archiveFileName }) changelog = "Please check the [GitHub wiki](https://github.com/embeddedt/ModernFix/wiki/Changelog) for major changes." type = STABLE @@ -225,7 +223,7 @@ publishMods { minecraftVersions.add(minecraft_version) } modrinth { - projectId = "modernfix" + projectId = "nmDcB62a" accessToken = providers.environmentVariable("MODRINTH_TOKEN") minecraftVersions.add(minecraft_version) } diff --git a/neoforge/gradle.properties b/neoforge/gradle.properties deleted file mode 100644 index 2914393d..00000000 --- a/neoforge/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -loom.platform=neoforge \ No newline at end of file diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/chunk_deadlock/ChunkMapLoadMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/chunk_deadlock/ChunkMapLoadMixin.java new file mode 100644 index 00000000..236d20c0 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/chunk_deadlock/ChunkMapLoadMixin.java @@ -0,0 +1,90 @@ +package org.embeddedt.modernfix.common.mixin.bugfix.chunk_deadlock; + +import com.llamalad7.mixinextras.sugar.Local; +import net.minecraft.CrashReport; +import net.minecraft.ReportedException; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatusTasks; +import net.minecraft.world.level.chunk.status.WorldGenContext; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +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.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.Supplier; + +@Mixin(ChunkStatusTasks.class) +public abstract class ChunkMapLoadMixin { + @Unique + private static final ThreadLocal> MFIX_SURROGATE_FUTURE = new ThreadLocal<>(); + + /** + * @author embeddedt + * @reason This redirect makes several changes to how full chunk promotion works. First of all, promotion runs + * directly in the context of the main thread executor, rather than going through the priority sorter. + * This change allows attempts to load other chunks from within the promotion lambda to succeed (important + * for bad EntityJoinLevelEvent implementations to not deadlock the game). Second, it slightly alters the + * semantics of protoChunkToFullChunk so that the FULL chunk future will be completed before postload + * callbacks finish running. This change allows attempts to load the _same_ chunk in the promotion lambda to + * succeed, as otherwise the future would block waiting for itself to complete. + * + *

This is a cleaner version of a similar trick used in ModernFix versions for 1.16, which deferred specifically + * entity addition to happen outside the futures. + */ + @Redirect(method = "full", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;supplyAsync(Ljava/util/function/Supplier;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;", ordinal = 0)) + private static CompletableFuture createSurrogateFuture(Supplier supplier, Executor executor, + @Local(ordinal = 0, argsOnly = true) WorldGenContext worldGenContext) { + var surrogate = new CompletableFuture(); + // Unlike vanilla, we execute the promotion lambda in mainThreadExecutor, rather than within the context + // of the task sorter. Doing this avoids deadlocking the sorter if a blocking chunk load is attempted + // during chunk promotion. We still initially compose the future through the sorter's executor to stop promotion + // from running earlier than it would in vanilla. + var mainThreadExecutor = ((ServerChunkCacheAccessor) worldGenContext.level().getChunkSource()).mfix$getMainThreadProcessor(); + CompletableFuture.runAsync(() -> {}, executor).thenApplyAsync($ -> { + // running on thread that executes lambda body + MFIX_SURROGATE_FUTURE.set(surrogate); + try { + return supplier.get(); + } finally { + MFIX_SURROGATE_FUTURE.remove(); + } + }, mainThreadExecutor).whenComplete((either, throwable) -> { + if (throwable != null) { + if (!surrogate.isDone()) { + surrogate.completeExceptionally(throwable); + } else { + // The chunk has already become visible at FULL status, so we + // track the exception ourselves and manually rethrow it at the right point + // to trigger a server crash + var exc = new ReportedException(CrashReport.forThrowable(throwable, "Exception during promotion of chunk to FULL status")); + mainThreadExecutor.schedule(() -> { + throw exc; + }); + } + } else { + surrogate.complete(either); + } + }); + // Return the surrogate + return surrogate; + } + + /** + * @author embeddedt + * @reason Complete the surrogate future as soon as basic promotion is done, and before we start loading entities + * & block entities. This allows EntityJoinLevelEvent to read the current chunk. + */ + @Inject(method = "lambda$full$0", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/LevelChunk;runPostLoad()V")) + private static void completeSurrogateFuture(CallbackInfoReturnable cir, @Local(ordinal = 0) LevelChunk levelChunk) { + var future = MFIX_SURROGATE_FUTURE.get(); + if (future != null) { + future.complete(levelChunk); + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/chunk_deadlock/ServerChunkCacheAccessor.java b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/chunk_deadlock/ServerChunkCacheAccessor.java new file mode 100644 index 00000000..304f023a --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/chunk_deadlock/ServerChunkCacheAccessor.java @@ -0,0 +1,11 @@ +package org.embeddedt.modernfix.common.mixin.bugfix.chunk_deadlock; + +import net.minecraft.server.level.ServerChunkCache; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(ServerChunkCache.class) +public interface ServerChunkCacheAccessor { + @Accessor("mainThreadProcessor") + ServerChunkCache.MainThreadExecutor mfix$getMainThreadProcessor(); +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/concurrency/ReloadableResourceManagerMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/concurrency/ReloadableResourceManagerMixin.java new file mode 100644 index 00000000..9623584e --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/bugfix/concurrency/ReloadableResourceManagerMixin.java @@ -0,0 +1,45 @@ +package org.embeddedt.modernfix.common.mixin.bugfix.concurrency; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.client.Minecraft; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.resources.PreparableReloadListener; +import net.minecraft.server.packs.resources.ReloadableResourceManager; +import org.embeddedt.modernfix.ModernFix; +import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.embeddedt.modernfix.neoforge.init.ModernFixForge; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(ReloadableResourceManager.class) +@ClientOnlyMixin +public abstract class ReloadableResourceManagerMixin { + @Shadow + @Final + private PackType type; + + @Shadow + public abstract void registerReloadListener(PreparableReloadListener listener); + + /** + * @author embeddedt + * @reason complain loudly when reload listeners are being registered too late in a way that would cause + * concurrency issues, and prevent them from crashing the game + */ + @WrapMethod(method = "registerReloadListener") + private void checkCallingThread(PreparableReloadListener listener, Operation original) { + if (ModernFixForge.registryEventsFired && this.type == PackType.CLIENT_RESOURCES + && (Object)this == Minecraft.getInstance().getResourceManager() + && !Minecraft.getInstance().isSameThread()) { + ModernFix.LOGGER.error("A mod is calling registerReloadListener at the wrong time. This will cause random concurrency crashes when ModernFix is not installed. Please report this to them. If you are a modder, refer to https://github.com/embeddedt/ModernFix/wiki/registerReloadListener-called-on-wrong-thread for more information.", new Exception("registerReloadListener called on wrong thread")); + // Defer the call onto the main client thread. There is a decent chance the mod's listener will be + // ignored in this case, but it is more predictable than allowing them to randomly crash the game. + Minecraft.getInstance().schedule(() -> this.registerReloadListener(listener)); + return; + } + + original.call(listener); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/core/BootstrapMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/core/BootstrapMixin.java index ddaad6f2..172abed1 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/core/BootstrapMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/core/BootstrapMixin.java @@ -1,8 +1,8 @@ package org.embeddedt.modernfix.common.mixin.core; import net.minecraft.server.Bootstrap; -import org.embeddedt.modernfix.util.TimeFormatter; import org.slf4j.Logger; +import org.embeddedt.modernfix.util.TimeFormatter; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.MixinEnvironment; @@ -23,6 +23,7 @@ public class BootstrapMixin { private static void doModernFixBootstrap(CallbackInfo ci) { if(!isBootstrapped) { LOGGER.info("ModernFix reached bootstrap stage ({} after launch)", TimeFormatter.formatNanos(ManagementFactory.getRuntimeMXBean().getUptime() * 1000L * 1000L)); + if (Boolean.getBoolean("modernfix.auditMixinsAtStart")) { MixinEnvironment.getCurrentEnvironment().audit(); } diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/core/GameDataMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/core/GameDataMixin.java new file mode 100644 index 00000000..806285a2 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/core/GameDataMixin.java @@ -0,0 +1,16 @@ +package org.embeddedt.modernfix.common.mixin.core; + +import net.neoforged.neoforge.registries.GameData; +import org.embeddedt.modernfix.neoforge.init.ModernFixForge; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(value = GameData.class, remap = false) +public class GameDataMixin { + @Inject(method = "postRegisterEvents", at = @At("RETURN")) + private static void markPosted(CallbackInfo ci) { + ModernFixForge.registryEventsFired = true; + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierBuilderMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierBuilderMixin.java new file mode 100644 index 00000000..116e954d --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierBuilderMixin.java @@ -0,0 +1,31 @@ +package org.embeddedt.modernfix.common.mixin.perf.attribute_supplier_dedup; + +import net.minecraft.core.Holder; +import net.minecraft.world.entity.ai.attributes.Attribute; +import net.minecraft.world.entity.ai.attributes.AttributeInstance; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import org.embeddedt.modernfix.entity.AttributeInstanceTemplates; +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.callback.CallbackInfoReturnable; + +import java.util.Map; + +@Mixin(AttributeSupplier.Builder.class) +public class AttributeSupplierBuilderMixin { + @Shadow + @Final + private Map, AttributeInstance> builder; + + /** + * @author embeddedt + * @reason canonicalize identical AttributeInstance templates, many entities are created with the same values + */ + @Inject(method = "build", at = @At(value = "NEW", target = "(Ljava/util/Map;)Lnet/minecraft/world/entity/ai/attributes/AttributeSupplier;")) + private void deduplicateInstances(CallbackInfoReturnable cir) { + this.builder.replaceAll((a, i) -> AttributeInstanceTemplates.intern(i)); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierMixin.java new file mode 100644 index 00000000..c214cba0 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/attribute_supplier_dedup/AttributeSupplierMixin.java @@ -0,0 +1,33 @@ +package org.embeddedt.modernfix.common.mixin.perf.attribute_supplier_dedup; + +import net.minecraft.core.Holder; +import net.minecraft.world.entity.ai.attributes.Attribute; +import net.minecraft.world.entity.ai.attributes.AttributeInstance; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +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.Map; + +@Mixin(AttributeSupplier.class) +public class AttributeSupplierMixin { + @Shadow + @Final + @Mutable + private Map, AttributeInstance> instances; + + /** + * @author embeddedt + * @reason Java 9's Map.of() implementation is significantly more compact than ImmutableMap, and we do not + * care about insertion order in this context + */ + @Inject(method = "", at = @At("RETURN")) + private void useCompactJavaMap(Map, AttributeInstance> instances, CallbackInfo ci) { + this.instances = Map.copyOf(this.instances); + } +} \ No newline at end of file diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ChunkGeneratorMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ChunkGeneratorMixin.java new file mode 100644 index 00000000..0c1edbe5 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ChunkGeneratorMixin.java @@ -0,0 +1,168 @@ +package org.embeddedt.modernfix.common.mixin.perf.cache_strongholds; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.core.Holder; +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.*; +import net.minecraft.resources.RegistryOps; +import net.minecraft.util.Util; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; +import net.minecraft.world.level.levelgen.structure.StructureSet; +import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement; +import org.embeddedt.modernfix.ModernFix; +import org.embeddedt.modernfix.duck.IChunkGenerator; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.lang.ref.SoftReference; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +@Mixin(ChunkGeneratorStructureState.class) +public class ChunkGeneratorMixin implements IChunkGenerator { + @Shadow + @Final + private long concentricRingsSeed; + + @Shadow + @Final + private BiomeSource biomeSource; + + private Path mfix$dimensionPath; + private RegistryAccess.Frozen mfix$registryAccess; + private SoftReference>> mfix$cachedPositions = new SoftReference<>(null); + + private static final String CACHE_FILENAME = "mfix_stronghold_cache_v2.nbt"; + + @Override + public void mfix$setStrongholdCachePath(Path cachePath, RegistryAccess.Frozen registryAccess) { + this.mfix$dimensionPath = cachePath; + this.mfix$registryAccess = registryAccess; + } + + @WrapMethod(method = "generateRingPositions") + private CompletableFuture> modernfix$cacheRingPositions(Holder structureSet, + ConcentricRingsStructurePlacement placement, + Operation>> original) { + if (this.mfix$registryAccess == null || this.mfix$dimensionPath == null) { + return original.call(structureSet, placement); + } + + String cacheKey = mfix$makeCacheKey(placement); + + // Try reading from cache + List cached = mfix$readFromCache(cacheKey); + if (cached != null) { + ModernFix.LOGGER.debug("Using cached stronghold positions for {}", cacheKey); + return CompletableFuture.completedFuture(List.copyOf(cached)); + } + + return original.call(structureSet, placement).thenApplyAsync(positions -> { + mfix$writeToCache(cacheKey, positions); + return positions; + }, Util.ioPool()); + } + + private String mfix$makeCacheKey(ConcentricRingsStructurePlacement placement) { + RegistryOps ops = RegistryOps.create(NbtOps.INSTANCE, this.mfix$registryAccess); + String placementKey = ConcentricRingsStructurePlacement.CODEC.codec().encodeStart(ops, placement) + .result().map(Tag::toString).orElse(null); + String biomeSourceKey = BiomeSource.CODEC.encodeStart(ops, this.biomeSource) + .result().map(Tag::toString).orElse(null); + if (placementKey == null || biomeSourceKey == null) { + ModernFix.LOGGER.warn("Failed to create cache key for concentric structure placement"); + return null; + } + String data = placementKey + ";biomes=" + biomeSourceKey + ";seed=" + this.concentricRingsSeed; + try { + byte[] hash = MessageDigest.getInstance("SHA-256").digest(data.getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(64); + for (byte b : hash) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } catch (NoSuchAlgorithmException e) { + return null; + } + } + + private synchronized List mfix$readFromCache(String cacheKey) { + Map> cache = mfix$getOrLoadCache(); + return cache.get(cacheKey); + } + + private synchronized void mfix$writeToCache(String cacheKey, List positions) { + Map> cache = mfix$getOrLoadCache(); + cache.put(cacheKey, List.copyOf(positions)); + mfix$cachedPositions = new SoftReference<>(cache); + mfix$saveCacheFile(cache); + } + + private Map> mfix$getOrLoadCache() { + Map> cache = mfix$cachedPositions.get(); + if (cache != null) { + return cache; + } + cache = mfix$loadCacheFile(); + mfix$cachedPositions = new SoftReference<>(cache); + return cache; + } + + private Map> mfix$loadCacheFile() { + Path file = mfix$dimensionPath.resolve(CACHE_FILENAME); + if (!Files.exists(file)) { + return new HashMap<>(); + } + try { + CompoundTag root = NbtIo.readCompressed(file, NbtAccounter.unlimitedHeap()); + Map> result = new HashMap<>(); + for (String key : root.keySet()) { + root.getIntArray(key).ifPresent(data -> { + if (data.length >= 2 && data.length % 2 == 0) { + List positions = new ArrayList<>(data.length / 2); + for (int i = 0; i < data.length; i += 2) { + positions.add(new ChunkPos(data[i], data[i + 1])); + } + result.put(key, positions); + } + }); + } + return result; + } catch (Exception e) { + ModernFix.LOGGER.warn("Failed to read stronghold cache, will recompute", e); + return new HashMap<>(); + } + } + + private void mfix$saveCacheFile(Map> cache) { + CompoundTag root = new CompoundTag(); + for (var entry : cache.entrySet()) { + List positions = entry.getValue(); + int[] data = new int[positions.size() * 2]; + for (int i = 0; i < positions.size(); i++) { + ChunkPos pos = positions.get(i); + data[i * 2] = pos.x(); + data[i * 2 + 1] = pos.z(); + } + root.putIntArray(entry.getKey(), data); + } + Path file = mfix$dimensionPath.resolve(CACHE_FILENAME); + try { + NbtIo.writeCompressed(root, file); + } catch (Exception e) { + ModernFix.LOGGER.warn("Failed to write stronghold cache", e); + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ServerLevelMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ServerLevelMixin.java new file mode 100644 index 00000000..8505303e --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/cache_strongholds/ServerLevelMixin.java @@ -0,0 +1,30 @@ +package org.embeddedt.modernfix.common.mixin.perf.cache_strongholds; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; +import net.minecraft.world.level.storage.LevelStorageSource; +import org.embeddedt.modernfix.duck.IChunkGenerator; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(ServerLevel.class) +public class ServerLevelMixin { + /** + * @author embeddedt + * @reason Make the dimension path accessible to ChunkGeneratorStructureState. + */ + @WrapOperation(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/ChunkGeneratorStructureState;ensureStructuresGenerated()V")) + private void setCachePath(ChunkGeneratorStructureState instance, Operation original, + @Local(ordinal = 0, argsOnly = true) LevelStorageSource.LevelStorageAccess levelStorageAccess, + @Local(ordinal = 0, argsOnly = true) ResourceKey dimension, + @Local(ordinal = 0, argsOnly = true) MinecraftServer server) { + ((IChunkGenerator)instance).mfix$setStrongholdCachePath(levelStorageAccess.getDimensionPath(dimension), server.registryAccess()); + original.call(instance); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_imposterprotochunks/ChunkAccessMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_imposterprotochunks/ChunkAccessMixin.java new file mode 100644 index 00000000..0343f25b --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_imposterprotochunks/ChunkAccessMixin.java @@ -0,0 +1,22 @@ +package org.embeddedt.modernfix.common.mixin.perf.compact_imposterprotochunks; + +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.lighting.ChunkSkyLightSources; +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; + +@Mixin(ChunkAccess.class) +public class ChunkAccessMixin { + @Shadow + @Final + @Mutable + protected LevelChunkSection[] sections; + + @Shadow + protected ChunkSkyLightSources skyLightSources; +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_imposterprotochunks/ImposterProtoChunkMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_imposterprotochunks/ImposterProtoChunkMixin.java new file mode 100644 index 00000000..36829c70 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_imposterprotochunks/ImposterProtoChunkMixin.java @@ -0,0 +1,22 @@ +package org.embeddedt.modernfix.common.mixin.perf.compact_imposterprotochunks; + +import net.minecraft.world.level.chunk.ImposterProtoChunk; +import net.minecraft.world.level.chunk.LevelChunk; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ImposterProtoChunk.class) +public abstract class ImposterProtoChunkMixin extends ChunkAccessMixin { + /** + * @author embeddedt + * @reason ImposterProtoChunks allocate their own LevelChunkSection objects etc. which wastes quite + * a bit of memory + */ + @Inject(method = "", at = @At("RETURN")) + private void replaceDuplicateObjects(LevelChunk wrapped, boolean allowWrites, CallbackInfo ci) { + this.sections = wrapped.getSections(); + this.skyLightSources = wrapped.getSkyLightSources(); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_mojang_registries/BlockStateDataMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_mojang_registries/BlockStateDataMixin.java index fed1cfd0..65e1e597 100644 --- a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_mojang_registries/BlockStateDataMixin.java +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compact_mojang_registries/BlockStateDataMixin.java @@ -36,7 +36,7 @@ public class BlockStateDataMixin { t = compactTag(ct); } t = TAG_INTERNER.addOrGet(t); - entries[i++] = Map.entry(key, t); + entries[i++] = Map.entry(key.intern(), t); } return new CompoundTag(Map.ofEntries(entries)); } diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compress_unihex_font/GlyphProviderTypeMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compress_unihex_font/GlyphProviderTypeMixin.java new file mode 100644 index 00000000..bdb2db07 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/compress_unihex_font/GlyphProviderTypeMixin.java @@ -0,0 +1,20 @@ +package org.embeddedt.modernfix.common.mixin.perf.compress_unihex_font; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.mojang.serialization.MapCodec; +import net.minecraft.client.gui.font.providers.GlyphProviderDefinition; +import net.minecraft.client.gui.font.providers.GlyphProviderType; +import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.embeddedt.modernfix.render.font.LazyGlyphProvider; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(GlyphProviderType.class) +@ClientOnlyMixin +public class GlyphProviderTypeMixin { + @ModifyExpressionValue(method = "", at = @At(value = "FIELD", opcode = Opcodes.GETSTATIC, target = "Lnet/minecraft/client/gui/font/providers/UnihexProvider$Definition;CODEC:Lcom/mojang/serialization/MapCodec;")) + private static MapCodec lazyUnihex(MapCodec codec) { + return LazyGlyphProvider.wrap(codec); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/encoder_cache_leak/ClientConfigurationPacketListenerImplMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/encoder_cache_leak/ClientConfigurationPacketListenerImplMixin.java new file mode 100644 index 00000000..deb20085 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/encoder_cache_leak/ClientConfigurationPacketListenerImplMixin.java @@ -0,0 +1,21 @@ +package org.embeddedt.modernfix.common.mixin.perf.encoder_cache_leak; + +import net.minecraft.client.multiplayer.ClientConfigurationPacketListenerImpl; +import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientConfigurationPacketListenerImpl.class) +@ClientOnlyMixin +public class ClientConfigurationPacketListenerImplMixin { + /** + * @author embeddedt + * @reason Reset the encoder cache after configuration finishes as the registries are now changing. + */ + @Inject(method = "handleConfigurationFinished", at = @At("RETURN")) + private void resetEncoderCache(CallbackInfo ci) { + ((EncoderCacheAccessor)DataComponentsAccessor.mfix$getCache()).mfix$getCache().invalidateAll(); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/encoder_cache_leak/MinecraftMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/encoder_cache_leak/MinecraftMixin.java new file mode 100644 index 00000000..91fe6326 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/encoder_cache_leak/MinecraftMixin.java @@ -0,0 +1,25 @@ +package org.embeddedt.modernfix.common.mixin.perf.encoder_cache_leak; + +import net.minecraft.client.Minecraft; +import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Minecraft.class) +@ClientOnlyMixin +public class MinecraftMixin { + /** + * @author embeddedt + * @reason Make sure the encoder cache is cleared when the client disconnects, as it retains strong references + * to registries. + */ + @Inject(method = { + "disconnect(Lnet/minecraft/client/gui/screens/Screen;Z)V", + "clearClientLevel(Lnet/minecraft/client/gui/screens/Screen;)V" + }, at = @At("RETURN")) + private void clearEncoderCache(CallbackInfo ci) { + ((EncoderCacheAccessor)DataComponentsAccessor.mfix$getCache()).mfix$getCache().invalidateAll(); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/BiomeManagerAccessor.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/BiomeManagerAccessor.java new file mode 100644 index 00000000..7a6cf192 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/BiomeManagerAccessor.java @@ -0,0 +1,14 @@ +package org.embeddedt.modernfix.common.mixin.perf.optimize_surface_rules; + +import net.minecraft.world.level.biome.BiomeManager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(BiomeManager.class) +public interface BiomeManagerAccessor { + @Accessor("biomeZoomSeed") + long mfix$getZoomSeed(); + + @Accessor("noiseBiomeSource") + BiomeManager.NoiseBiomeSource mfix$getBiomeSource(); +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SequenceRuleSourceMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SequenceRuleSourceMixin.java new file mode 100644 index 00000000..a599aa7b --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SequenceRuleSourceMixin.java @@ -0,0 +1,19 @@ +package org.embeddedt.modernfix.common.mixin.perf.optimize_surface_rules; + +import net.minecraft.world.level.levelgen.SurfaceRules; +import org.embeddedt.modernfix.world.gen.SurfaceRuleOptimizer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(targets = {"net/minecraft/world/level/levelgen/SurfaceRules$SequenceRuleSource"}) +public class SequenceRuleSourceMixin { + @Inject(method = "apply(Lnet/minecraft/world/level/levelgen/SurfaceRules$Context;)Lnet/minecraft/world/level/levelgen/SurfaceRules$SurfaceRule;", at = @At("HEAD"), cancellable = true) + private void optimizeApply(SurfaceRules.Context context, CallbackInfoReturnable cir) { + var optimized = SurfaceRuleOptimizer.optimizeSequenceRule((SurfaceRules.SequenceRuleSource)(Object) this, context); + if (optimized != null) { + cir.setReturnValue(optimized); + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SurfaceSystemMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SurfaceSystemMixin.java new file mode 100644 index 00000000..aa26d345 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/optimize_surface_rules/SurfaceSystemMixin.java @@ -0,0 +1,80 @@ +package org.embeddedt.modernfix.common.mixin.perf.optimize_surface_rules; + +import com.llamalad7.mixinextras.sugar.Local; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.chunk.BlockColumn; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.levelgen.NoiseChunk; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.SurfaceRules; +import net.minecraft.world.level.levelgen.SurfaceSystem; +import net.minecraft.world.level.levelgen.WorldGenerationContext; +import org.embeddedt.modernfix.world.gen.ChunkBiomeLookup; +import org.embeddedt.modernfix.world.gen.PrefetchingBlockColumn; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.function.Function; + +@Mixin(SurfaceSystem.class) +public class SurfaceSystemMixin { + private static final ThreadLocal MFIX_LOOKUP_CACHE = ThreadLocal.withInitial(ChunkBiomeLookup::new); + private static final ThreadLocal MFIX_BLOCK_COLUMN = new ThreadLocal<>(); + + @ModifyArg(method = "buildSurface", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/levelgen/SurfaceRules$Context;(Lnet/minecraft/world/level/levelgen/SurfaceSystem;Lnet/minecraft/world/level/levelgen/RandomState;Lnet/minecraft/world/level/chunk/ChunkAccess;Lnet/minecraft/world/level/levelgen/NoiseChunk;Ljava/util/function/Function;Lnet/minecraft/core/Registry;Lnet/minecraft/world/level/levelgen/WorldGenerationContext;)V"), index = 4) + private Function> useFasterLookup(Function> biomeGetter, + @Local(ordinal = 0, argsOnly = true) BiomeManager manager, + @Local(ordinal = 0, argsOnly = true) ChunkAccess chunk, + @Share("chunkBiomeLookup") LocalRef lookupRef) { + var lookup = MFIX_LOOKUP_CACHE.get(); + BiomeManagerAccessor accessor = (BiomeManagerAccessor)manager; + lookup.prepare(accessor.mfix$getBiomeSource(), accessor.mfix$getZoomSeed(), chunk, manager); + lookupRef.set(lookup); + return lookup; + } + + @Inject(method = "buildSurface", at = @At("RETURN")) + private void finishAndDisposeLookups(RandomState randomState, BiomeManager biomeManager, Registry biomes, boolean p_224652_, WorldGenerationContext context, ChunkAccess chunk, NoiseChunk noiseChunk, SurfaceRules.RuleSource ruleSource, CallbackInfo ci) { + MFIX_LOOKUP_CACHE.get().dispose(); + var column = MFIX_BLOCK_COLUMN.get(); + if (column != null) { + column.dispose(); + } + } + + @Redirect(method = "buildSurface", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/biome/BiomeManager;getBiome(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/core/Holder;")) + private Holder useFasterLookup(BiomeManager instance, BlockPos pos, @Share("chunkBiomeLookup") LocalRef lookupRef) { + return lookupRef.get().apply(pos); + } + + @Inject(method = "buildSurface", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/levelgen/SurfaceRules$Context;(Lnet/minecraft/world/level/levelgen/SurfaceSystem;Lnet/minecraft/world/level/levelgen/RandomState;Lnet/minecraft/world/level/chunk/ChunkAccess;Lnet/minecraft/world/level/levelgen/NoiseChunk;Ljava/util/function/Function;Lnet/minecraft/core/Registry;Lnet/minecraft/world/level/levelgen/WorldGenerationContext;)V")) + private void captureRealBlockColumn(CallbackInfo ci, @Local(ordinal = 0) LocalRef column, + @Local(ordinal = 0, argsOnly = true) ChunkAccess chunk, + @Share("prefetchColumn") LocalRef prefetchRef) { + var prefetchingBlockColumn = MFIX_BLOCK_COLUMN.get(); + if (prefetchingBlockColumn == null || prefetchingBlockColumn.getExpectedHeight() != chunk.getHeight()) { + prefetchingBlockColumn = new PrefetchingBlockColumn(chunk.getHeight()); + MFIX_BLOCK_COLUMN.set(prefetchingBlockColumn); + } + column.set(prefetchingBlockColumn); + prefetchRef.set(prefetchingBlockColumn); + } + + @Inject(method = "buildSurface", at = @At(value = "INVOKE", target = "Lnet/minecraft/core/BlockPos$MutableBlockPos;setZ(I)Lnet/minecraft/core/BlockPos$MutableBlockPos;", ordinal = 0, shift = At.Shift.AFTER)) + private void prefetchBlockArray(RandomState randomState, BiomeManager biomeManager, Registry biomes, boolean p_224652_, + WorldGenerationContext context, ChunkAccess chunk, NoiseChunk noiseChunk, SurfaceRules.RuleSource ruleSource, CallbackInfo ci, + @Local(ordinal = 0) BlockColumn column, + @Local(ordinal = 0) BlockPos.MutableBlockPos cursor) { + ((PrefetchingBlockColumn)column).prefetch(chunk, cursor.getX() & 15, cursor.getZ() & 15); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkHolderMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkHolderMixin.java new file mode 100644 index 00000000..604804d4 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkHolderMixin.java @@ -0,0 +1,74 @@ +package org.embeddedt.modernfix.common.mixin.perf.release_protochunks; + +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkLevel; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.server.level.GenerationChunkHolder; +import net.minecraft.world.level.ChunkPos; +import org.embeddedt.modernfix.duck.release_protochunks.IClearableChunkHolder; +import org.embeddedt.modernfix.duck.release_protochunks.ISuspendedHolderTrackingChunkMap; +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.callback.CallbackInfo; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +@Mixin(ChunkHolder.class) +public abstract class ChunkHolderMixin extends GenerationChunkHolder implements IClearableChunkHolder { + @Shadow + private CompletableFuture saveSync; + + @Shadow + private int ticketLevel; + + @Shadow + @Final + private ChunkHolder.PlayerProvider playerProvider; + + public ChunkHolderMixin(ChunkPos pos) { + super(pos); + } + + @Override + public void mfix$resetProtoChunkFutures() { + int len = this.futures.length(); + for (int i = 0; i < len; i++) { + this.futures.set(i, null); + } + this.saveSync = CompletableFuture.completedFuture(null); + this.startedWork.set(null); + } + + /* + * The methods below trigger the ChunkMap to check whether this holder can be "suspended" (have its ProtoChunk-only + * futures cleared) each time a new version of the chunkToSave future has completed. The ChunkMap itself + * also verifies that all conditions are still met for suspension in case the holder has become necessary + * again in the meantime. + */ + + @Inject(method = "addSaveDependency", at = @At("RETURN")) + private void recheckSuspensionAfterNeighbor(CompletableFuture future, CallbackInfo ci) { + this.mfix$markAsNeedingProtoChunkDrop(); + } + + @Inject(method = "updateFutures", at = @At("RETURN")) + private void markForSuspensionOnDemotion(ChunkMap chunkMap, Executor executor, CallbackInfo ci) { + this.mfix$markAsNeedingProtoChunkDrop(); + } + + private void mfix$markAsNeedingProtoChunkDrop() { + if (!ChunkLevel.fullStatus(this.ticketLevel).isOrAfter(FullChunkStatus.FULL) + && ChunkLevel.isLoaded(this.ticketLevel)) { + // register for suspension check when chain completes + var map = ((ISuspendedHolderTrackingChunkMap)this.playerProvider); + this.saveSync.whenCompleteAsync((r, e) -> { + map.mfix$markForSuspensionCheck(this.pos); + }, map.mfix$getMainThreadExecutor()); + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkMapMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkMapMixin.java new file mode 100644 index 00000000..675f0ef3 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/release_protochunks/ChunkMapMixin.java @@ -0,0 +1,107 @@ +package org.embeddedt.modernfix.common.mixin.perf.release_protochunks; + +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkLevel; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.util.thread.BlockableEventLoop; +import net.minecraft.world.level.ChunkPos; +import org.embeddedt.modernfix.duck.release_protochunks.IClearableChunkHolder; +import org.embeddedt.modernfix.duck.release_protochunks.ISuspendedHolderTrackingChunkMap; +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.callback.CallbackInfo; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.BooleanSupplier; + +@Mixin(ChunkMap.class) +public abstract class ChunkMapMixin implements ISuspendedHolderTrackingChunkMap { + + private static final int MFIX$TICKS_TO_WAIT_BEFORE_SUSPENDING = 100; + + @Shadow + @Final + public Long2ObjectLinkedOpenHashMap updatingChunkMap; + + @Shadow + @Final + private BlockableEventLoop mainThreadExecutor; + + @Shadow + protected abstract void lambda$scheduleUnload$0(ChunkHolder holder, CompletableFuture future, long chunkPos); + + @Shadow + @Final + public Long2ObjectLinkedOpenHashMap pendingUnloads; + + private final Long2IntOpenHashMap mfix$protoChunksToDrop = new Long2IntOpenHashMap(); + + private int mfix$dropTickCounter = 0; + + /** + * @author embeddedt + * @reason We keep track of ChunkHolders that only contain protochunks, and are not loaded to a full status. + * This hook unloads their contents once there are no generation tasks actively relying on the chunk. + */ + @Inject(method = "processUnloads(Ljava/util/function/BooleanSupplier;)V", at = @At("RETURN")) + private void dropProtoChunks(BooleanSupplier hasMoreTime, CallbackInfo ci) { + int suspended = 0; + int iterations = 0; + mfix$dropTickCounter++; + var dropIterator = mfix$protoChunksToDrop.long2IntEntrySet().fastIterator(); + while (dropIterator.hasNext() && suspended < 50 && iterations < 500 && (hasMoreTime.getAsBoolean() || mfix$protoChunksToDrop.size() > 1000)) { + iterations++; + var entry = dropIterator.next(); + long pos = entry.getLongKey(); + ChunkHolder holder = this.updatingChunkMap.get(pos); + if (holder == null // already removed + || ChunkLevel.fullStatus(holder.getTicketLevel()).isOrAfter(FullChunkStatus.FULL) // promoted to FULL + || !ChunkLevel.isLoaded(holder.getTicketLevel()) // is going to be dropped through normal code path + ) { + dropIterator.remove(); + continue; + } + + if (!holder.isReadyForSaving() // saveSync dependencies have not completed or chunk is still being referenced by another chunk for generation + ) { + // Not safe to suspend yet, reset timer + entry.setValue(mfix$dropTickCounter); + continue; + } + + if ((mfix$dropTickCounter - entry.getIntValue()) < MFIX$TICKS_TO_WAIT_BEFORE_SUSPENDING) { + // Chunk has not been idle for long enough, wait + continue; + } + + // All generation work done, so we can suspend and remove from set + dropIterator.remove(); + + // Execute the logic inside scheduleUnload() inline, without delegating to a queue + // When this returns it is safe to release any data the ChunkHolder holds + this.pendingUnloads.put(pos, holder); + this.lambda$scheduleUnload$0(holder, holder.getSaveSyncFuture(), pos); + + ((IClearableChunkHolder)holder).mfix$resetProtoChunkFutures(); + + suspended++; + } + } + + @Override + public void mfix$markForSuspensionCheck(ChunkPos pos) { + this.mfix$protoChunksToDrop.put(pos.pack(), this.mfix$dropTickCounter); + } + + @Override + public Executor mfix$getMainThreadExecutor() { + return this.mainThreadExecutor; + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/IntegratedServerMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/IntegratedServerMixin.java new file mode 100644 index 00000000..9d8ac304 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/IntegratedServerMixin.java @@ -0,0 +1,70 @@ +package org.embeddedt.modernfix.common.mixin.perf.suspend_integrated_server_during_load; + +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.mojang.datafixers.DataFixer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.server.IntegratedServer; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.Services; +import net.minecraft.server.WorldStem; +import net.minecraft.server.level.progress.LevelLoadListener; +import net.minecraft.server.packs.repository.PackRepository; +import net.minecraft.world.level.gamerules.GameRules; +import net.minecraft.world.level.storage.LevelStorageSource; +import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.embeddedt.modernfix.duck.suspend_integrated_server_during_load.IDeferrableIntegratedServer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; + +import java.net.Proxy; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BooleanSupplier; + +@Mixin(IntegratedServer.class) +@ClientOnlyMixin +public abstract class IntegratedServerMixin extends MinecraftServer implements IDeferrableIntegratedServer { + @Shadow + private boolean paused; + + private final AtomicBoolean mfix$hasPrimaryClientJoined = new AtomicBoolean(false); + + public IntegratedServerMixin(Thread serverThread, LevelStorageSource.LevelStorageAccess storageSource, PackRepository packRepository, WorldStem worldStem, Optional gameRules, Proxy proxy, DataFixer fixerUpper, Services services, LevelLoadListener levelLoadListener, boolean propagatesCrashes) { + super(serverThread, storageSource, packRepository, worldStem, gameRules, proxy, fixerUpper, services, levelLoadListener, propagatesCrashes); + } + + /** + * @author embeddedt + * @reason Wait to be finished processing all expensive packets (recipes, tags, etc.) + * before continuing to tick the integrated server. + */ + @WrapOperation(method = "tickServer", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;isPaused()Z", ordinal = 0)) + private boolean preventTicks(Minecraft instance, Operation original) { + return !mfix$hasPrimaryClientJoined.get() || original.call(instance); + } + + /** + * @author embeddedt + * @reason If waiting for a client connection to exist, we only need to tick the server connection, + * not the whole server as vanilla does. + */ + @WrapWithCondition(method = "tickServer", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;tickServer(Ljava/util/function/BooleanSupplier;)V", ordinal = 0)) + private boolean preventRunningFullServerTick(MinecraftServer server, BooleanSupplier hasTimeLeft) { + if (this.paused && !mfix$hasPrimaryClientJoined.get()) { + var conn = this.getConnection(); + if (conn != null) { + conn.tick(); + } + return false; + } + return true; + } + + @Override + public void mfix$markClientLoadFinished() { + mfix$hasPrimaryClientJoined.set(true); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/PlayerListMixin.java b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/PlayerListMixin.java new file mode 100644 index 00000000..f3cb710a --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/common/mixin/perf/suspend_integrated_server_during_load/PlayerListMixin.java @@ -0,0 +1,23 @@ +package org.embeddedt.modernfix.common.mixin.perf.suspend_integrated_server_during_load; + +import net.minecraft.network.Connection; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.CommonListenerCookie; +import net.minecraft.server.players.PlayerList; +import org.embeddedt.modernfix.annotation.ClientOnlyMixin; +import org.embeddedt.modernfix.neoforge.packet.ClientLoadFinishedPayload; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(PlayerList.class) +@ClientOnlyMixin +public class PlayerListMixin { + @Inject(method = "placeNewPlayer", at = @At("RETURN")) + private void sendConfigFinishedSentinelPacket(Connection connection, ServerPlayer player, CommonListenerCookie cookie, CallbackInfo ci) { + if (connection.isMemoryConnection()) { + player.connection.send(ClientLoadFinishedPayload.INSTANCE); + } + } +} 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 c757ad99..ccd16151 100644 --- a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java +++ b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java @@ -173,6 +173,7 @@ 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) @@ -182,6 +183,7 @@ public class ModernFixEarlyConfig { .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) .put("mixin.feature.warn_missing_perf_mods", true) .put("mixin.feature.spark_profile_launch", false) diff --git a/src/main/java/org/embeddedt/modernfix/duck/IChunkGenerator.java b/src/main/java/org/embeddedt/modernfix/duck/IChunkGenerator.java index 5f48ba9e..3cf83acc 100644 --- a/src/main/java/org/embeddedt/modernfix/duck/IChunkGenerator.java +++ b/src/main/java/org/embeddedt/modernfix/duck/IChunkGenerator.java @@ -1,7 +1,9 @@ package org.embeddedt.modernfix.duck; -import net.minecraft.server.level.ServerLevel; +import net.minecraft.core.RegistryAccess; + +import java.nio.file.Path; public interface IChunkGenerator { - void mfix$setAssociatedServerLevel(ServerLevel level); + void mfix$setStrongholdCachePath(Path cachePath, RegistryAccess.Frozen registryAccess); } diff --git a/src/main/java/org/embeddedt/modernfix/duck/ISpawnTrackingMinecraftServer.java b/src/main/java/org/embeddedt/modernfix/duck/ISpawnTrackingMinecraftServer.java new file mode 100644 index 00000000..46fb5b0b --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/duck/ISpawnTrackingMinecraftServer.java @@ -0,0 +1,10 @@ +package org.embeddedt.modernfix.duck; + +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import org.apache.commons.lang3.tuple.Pair; + +public interface ISpawnTrackingMinecraftServer { + Pair, ChunkPos> mfix$getInitialStartTicketLocation(); +} diff --git a/src/main/java/org/embeddedt/modernfix/duck/release_protochunks/IClearableChunkHolder.java b/src/main/java/org/embeddedt/modernfix/duck/release_protochunks/IClearableChunkHolder.java new file mode 100644 index 00000000..cb7d22a6 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/duck/release_protochunks/IClearableChunkHolder.java @@ -0,0 +1,5 @@ +package org.embeddedt.modernfix.duck.release_protochunks; + +public interface IClearableChunkHolder { + void mfix$resetProtoChunkFutures(); +} diff --git a/src/main/java/org/embeddedt/modernfix/duck/release_protochunks/ISuspendedHolderTrackingChunkMap.java b/src/main/java/org/embeddedt/modernfix/duck/release_protochunks/ISuspendedHolderTrackingChunkMap.java new file mode 100644 index 00000000..2fb171fb --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/duck/release_protochunks/ISuspendedHolderTrackingChunkMap.java @@ -0,0 +1,11 @@ +package org.embeddedt.modernfix.duck.release_protochunks; + +import net.minecraft.world.level.ChunkPos; + +import java.util.concurrent.Executor; + +public interface ISuspendedHolderTrackingChunkMap { + void mfix$markForSuspensionCheck(ChunkPos pos); + + Executor mfix$getMainThreadExecutor(); +} diff --git a/src/main/java/org/embeddedt/modernfix/duck/suspend_integrated_server_during_load/IDeferrableIntegratedServer.java b/src/main/java/org/embeddedt/modernfix/duck/suspend_integrated_server_during_load/IDeferrableIntegratedServer.java new file mode 100644 index 00000000..7988b007 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/duck/suspend_integrated_server_during_load/IDeferrableIntegratedServer.java @@ -0,0 +1,10 @@ +package org.embeddedt.modernfix.duck.suspend_integrated_server_during_load; + +import net.minecraft.resources.Identifier; +import org.embeddedt.modernfix.ModernFix; + +public interface IDeferrableIntegratedServer { + Identifier CLIENT_LOAD_SENTINEL = Identifier.fromNamespaceAndPath(ModernFix.MODID, "mark_client_load_finished"); + + void mfix$markClientLoadFinished(); +} diff --git a/src/main/java/org/embeddedt/modernfix/entity/AttributeInstanceTemplates.java b/src/main/java/org/embeddedt/modernfix/entity/AttributeInstanceTemplates.java new file mode 100644 index 00000000..46e1328a --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/entity/AttributeInstanceTemplates.java @@ -0,0 +1,42 @@ +package org.embeddedt.modernfix.entity; + +import it.unimi.dsi.fastutil.Hash; +import it.unimi.dsi.fastutil.objects.ObjectOpenCustomHashSet; +import net.minecraft.world.entity.ai.attributes.AttributeInstance; + +public class AttributeInstanceTemplates { + private static final ObjectOpenCustomHashSet INTERNER = new ObjectOpenCustomHashSet<>(new Hash.Strategy<>() { + @Override + public int hashCode(AttributeInstance o) { + if (o == null) { + return 0; + } + int h = System.identityHashCode(o.getAttribute()); + h = 31 * h + Double.hashCode(o.getBaseValue()); + h = 31 * h + o.getModifiers().hashCode(); + return h; + } + + @Override + public boolean equals(AttributeInstance a, AttributeInstance b) { + if (a == b) { + return true; + } + if (a == null || b == null) { + return false; + } + return a.getAttribute() == b.getAttribute() + && a.getBaseValue() == b.getBaseValue() + && a.getModifiers().equals(b.getModifiers()); + } + }); + + public static AttributeInstance intern(AttributeInstance a) { + if (a == null || a.getClass() != AttributeInstance.class) { + return a; + } + synchronized (INTERNER) { + return INTERNER.addOrGet(a); + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/neoforge/init/ModernFixForge.java b/src/main/java/org/embeddedt/modernfix/neoforge/init/ModernFixForge.java index 72520a99..e0695d1f 100644 --- a/src/main/java/org/embeddedt/modernfix/neoforge/init/ModernFixForge.java +++ b/src/main/java/org/embeddedt/modernfix/neoforge/init/ModernFixForge.java @@ -1,6 +1,7 @@ package org.embeddedt.modernfix.neoforge.init; import com.google.common.collect.ImmutableList; +import net.minecraft.client.Minecraft; import net.minecraft.core.registries.Registries; import net.minecraft.resources.Identifier; import net.minecraft.world.item.Item; @@ -17,10 +18,14 @@ import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.event.server.ServerStartedEvent; import net.neoforged.neoforge.event.server.ServerStoppedEvent; import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import net.neoforged.neoforge.network.registration.HandlerThread; +import net.neoforged.neoforge.network.registration.PayloadRegistrar; import net.neoforged.neoforge.registries.RegisterEvent; import org.apache.commons.lang3.tuple.Pair; import org.embeddedt.modernfix.ModernFix; import org.embeddedt.modernfix.core.ModernFixMixinPlugin; +import org.embeddedt.modernfix.duck.suspend_integrated_server_during_load.IDeferrableIntegratedServer; +import org.embeddedt.modernfix.neoforge.packet.ClientLoadFinishedPayload; import java.util.List; @@ -28,6 +33,7 @@ import java.util.List; public class ModernFixForge { private static ModernFix commonMod; public static boolean launchDone = false; + public static boolean registryEventsFired = false; public ModernFixForge(ModContainer modContainer, IEventBus modBus) { commonMod = new ModernFix(); @@ -74,7 +80,19 @@ public class ModernFixForge { } private void registerNetworkChannel(final RegisterPayloadHandlersEvent event) { + final PayloadRegistrar registrar = event.registrar("1").executesOn(HandlerThread.MAIN).optional(); + registrar.playToClient( + ClientLoadFinishedPayload.TYPE, + ClientLoadFinishedPayload.STREAM_CODEC, + (payload, ctx) -> { + ctx.enqueueWork(() -> { + Minecraft mc = Minecraft.getInstance(); + if (mc.hasSingleplayerServer()) { + ((IDeferrableIntegratedServer)mc.getSingleplayerServer()).mfix$markClientLoadFinished(); + } + }); + }); } @SubscribeEvent(priority = EventPriority.LOWEST) diff --git a/src/main/java/org/embeddedt/modernfix/neoforge/packet/ClientLoadFinishedPayload.java b/src/main/java/org/embeddedt/modernfix/neoforge/packet/ClientLoadFinishedPayload.java new file mode 100644 index 00000000..66797ed2 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/neoforge/packet/ClientLoadFinishedPayload.java @@ -0,0 +1,18 @@ +package org.embeddedt.modernfix.neoforge.packet; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import org.embeddedt.modernfix.duck.suspend_integrated_server_during_load.IDeferrableIntegratedServer; + +public enum ClientLoadFinishedPayload implements CustomPacketPayload { + INSTANCE; + + public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(IDeferrableIntegratedServer.CLIENT_LOAD_SENTINEL); + public static final StreamCodec STREAM_CODEC = StreamCodec.unit(INSTANCE); + + @Override + public Type type() { + return TYPE; + } +} diff --git a/src/main/java/org/embeddedt/modernfix/render/font/LazyGlyphProvider.java b/src/main/java/org/embeddedt/modernfix/render/font/LazyGlyphProvider.java new file mode 100644 index 00000000..064dfe64 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/render/font/LazyGlyphProvider.java @@ -0,0 +1,99 @@ +package org.embeddedt.modernfix.render.font; + +import com.mojang.blaze3d.font.GlyphProvider; +import com.mojang.blaze3d.font.UnbakedGlyph; +import com.mojang.datafixers.util.Either; +import com.mojang.serialization.MapCodec; +import it.unimi.dsi.fastutil.ints.IntSet; +import net.minecraft.client.gui.font.providers.GlyphProviderDefinition; +import net.minecraft.client.gui.font.providers.GlyphProviderType; +import net.minecraft.server.packs.resources.ResourceManager; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.lang.ref.SoftReference; +import java.util.function.Function; + +public class LazyGlyphProvider implements GlyphProvider { + private final GlyphProviderDefinition.Loader loader; + private final ResourceManager manager; + + private SoftReference innerProvider = new SoftReference<>(null); + + LazyGlyphProvider(GlyphProviderDefinition.Loader loader, ResourceManager manager) { + this.loader = loader; + this.manager = manager; + } + + @Override + public void close() { + // best effort + var prov = innerProvider.get(); + if (prov != null) { + prov.close(); + } + } + + private synchronized @Nullable GlyphProvider getGlyphProvider() { + GlyphProvider prov = innerProvider.get(); + if (prov == null) { + try { + prov = this.loader.load(this.manager); + } catch (IOException e) { + return null; + } + innerProvider = new SoftReference<>(prov); + } + return prov; + } + + @Override + public @Nullable UnbakedGlyph getGlyph(int character) { + var prov = getGlyphProvider(); + if (prov != null) { + return prov.getGlyph(character); + } else { + return null; + } + } + + @Override + public IntSet getSupportedGlyphs() { + var prov = getGlyphProvider(); + if (prov != null) { + return prov.getSupportedGlyphs(); + } else { + return IntSet.of(); + } + } + + private static class Definition implements GlyphProviderDefinition { + private final GlyphProviderDefinition delegate; + + public Definition(GlyphProviderDefinition delegate) { + this.delegate = delegate; + } + + @Override + public GlyphProviderType type() { + return this.delegate.type(); + } + + @Override + public Either unpack() { + return this.delegate.unpack().mapBoth( + loader -> resourceManager -> new LazyGlyphProvider(loader, resourceManager), + Function.identity() + ); + } + + @SuppressWarnings("unchecked") + public T delegate() { + return (T)this.delegate; + } + } + + public static MapCodec wrap(MapCodec codec) { + return codec.xmap(Definition::new, Definition::delegate); + } +} diff --git a/src/main/java/org/embeddedt/modernfix/spark/SparkLaunchProfiler.java b/src/main/java/org/embeddedt/modernfix/spark/SparkLaunchProfiler.java index c251c99e..7c3d9be3 100644 --- a/src/main/java/org/embeddedt/modernfix/spark/SparkLaunchProfiler.java +++ b/src/main/java/org/embeddedt/modernfix/spark/SparkLaunchProfiler.java @@ -36,11 +36,26 @@ public class SparkLaunchProfiler { private static ExecutorService executor = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryBuilder().setDaemon(true).setNameFormat("spark-modernfix-async-worker").build()); private static final SparkPlatform platform = new SparkPlatform(new ModernFixSparkPlugin()); + private static final String ALLOW_SPARK_PROFILING_PROP = "modernfix.allowSparkProfiling"; private static final boolean USE_JAVA_SAMPLER_FOR_LAUNCH = !Boolean.getBoolean("modernfix.profileWithAsyncSampler"); + private static final boolean ALLOW_SPARK_PROFILING = Boolean.getBoolean(ALLOW_SPARK_PROFILING_PROP); private static final int SAMPLING_INTERVAL = Integer.getInteger("modernfix.profileSamplingIntervalMicroseconds", 4000); private static final String THREAD_GROUPER = System.getProperty("modernfix.profileSamplingThreadGrouper", "by-pool"); + private static boolean checkSparkProfilingAllowed() { + if (!ALLOW_SPARK_PROFILING) { + ModernFixMixinPlugin.instance.logger.fatal("To reduce excessive load on the Spark servers, you must set " + + "-D{}=true in your JVM arguments for profiling to proceed. Please do " + + "this and relaunch the game.", ALLOW_SPARK_PROFILING_PROP); + return false; + } + return true; + } + public static void start(String key) { + if (!checkSparkProfilingAllowed()) { + return; + } if (!ongoingSamplers.containsKey(key)) { Sampler sampler; SamplerSettings settings = new SamplerSettings(SAMPLING_INTERVAL, ThreadDumper.ALL, ThreadGrouper.parseConfigSetting(THREAD_GROUPER).get(), -1, false, true); diff --git a/src/main/java/org/embeddedt/modernfix/world/gen/ChunkBiomeLookup.java b/src/main/java/org/embeddedt/modernfix/world/gen/ChunkBiomeLookup.java new file mode 100644 index 00000000..ac0cc43e --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/world/gen/ChunkBiomeLookup.java @@ -0,0 +1,262 @@ +package org.embeddedt.modernfix.world.gen; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.QuartPos; +import net.minecraft.util.LinearCongruentialGenerator; +import net.minecraft.util.Mth; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.chunk.ChunkAccess; + +import java.util.Arrays; +import java.util.function.Function; + +/** + * Drop-in replacement for {@code biomeManager::getBiome} in SurfaceSystem.buildSurface. + * + *

Pre-computes the Voronoi bias (fiddle) values and quart-resolution biome data for an + * entire chunk, then uses two optimizations: + *

    + *
  • Uniform check: If all 8 Voronoi candidate cells for a given quart position + * hold the same biome, the Voronoi computation is skipped entirely.
  • + *
  • Pre-computed bias: When the Voronoi is needed, the 48 LCG operations per block + * are replaced by array lookups of pre-computed fiddle values.
  • + *
+ */ +public class ChunkBiomeLookup implements Function> { + @SuppressWarnings("unchecked") + private Holder[] biomes = new Holder[0]; + private double[] biasX = new double[0], biasY = new double[0], biasZ = new double[0]; + private boolean[] uniform = new boolean[0]; + + private int qMinX, qMinY, qMinZ; + private int sizeX, sizeY, sizeZ; + + private BiomeManager fallbackManager; + + /** + * Pre-compute biome and bias data for the given chunk. Must be called before any + * {@link #getBiome} calls for positions within this chunk. + * + * @param source the underlying quart-resolution biome source (e.g. the chunk) + * @param biomeZoomSeed the obfuscated biome zoom seed from BiomeManager + */ + @SuppressWarnings("unchecked") + public void prepare(BiomeManager.NoiseBiomeSource source, long biomeZoomSeed, ChunkAccess chunk, BiomeManager fallback) { + int chunkMinX = chunk.getPos().getMinBlockX(); + int chunkMinZ = chunk.getPos().getMinBlockZ(); + int minBuildHeight = chunk.getMinY(); + int maxBuildHeight = minBuildHeight + chunk.getHeight(); // exclusive + + // BiomeManager.getBiome subtracts a 2-block offset before converting to quart coords, + // then considers quart and quart+1 as the 8 Voronoi candidates. + int biomeOffset = 2; + int minBlockX = chunkMinX - biomeOffset; + int maxBlockX = chunkMinX + 15 - biomeOffset; + int minBlockZ = chunkMinZ - biomeOffset; + int maxBlockZ = chunkMinZ + 15 - biomeOffset; + int minBlockY = minBuildHeight - biomeOffset; + int maxBlockY = maxBuildHeight - 1 - biomeOffset; + + // Quart range: fromBlock(min) to fromBlock(max) + 1 (for the +1 Voronoi candidate) + this.qMinX = QuartPos.fromBlock(minBlockX); + int qMaxX = QuartPos.fromBlock(maxBlockX) + 1; + this.qMinZ = QuartPos.fromBlock(minBlockZ); + int qMaxZ = QuartPos.fromBlock(maxBlockZ) + 1; + this.qMinY = QuartPos.fromBlock(minBlockY); + int qMaxY = QuartPos.fromBlock(maxBlockY) + 1; + + this.sizeX = qMaxX - qMinX + 1; // always 6 for 16-wide chunks + this.sizeY = qMaxY - qMinY + 1; + this.sizeZ = qMaxZ - qMinZ + 1; + + int totalCells = sizeX * sizeY * sizeZ; + + // Reuse arrays across chunks if large enough + if (biomes.length < totalCells) { + biomes = new Holder[totalCells]; + biasX = new double[totalCells]; + biasY = new double[totalCells]; + biasZ = new double[totalCells]; + uniform = new boolean[totalCells]; + } + + // Fetch quart-resolution biomes + boolean isSingleBiome = !fetchBiomes(source); + + if (isSingleBiome) { + // All cells hold the same biome, so no need to do expensive computations. + Arrays.fill(uniform, 0, totalCells, true); + } else { + this.computeUniformity(); + this.computeBiases(biomeZoomSeed); + } + + this.fallbackManager = fallback; + } + + public void dispose() { + // Make sure we do not retain strong references to the biome holders + Arrays.fill(biomes, null); + } + + private boolean fetchBiomes(BiomeManager.NoiseBiomeSource source) { + var biomes = this.biomes; + Holder firstSeen = null; + boolean seenMultiple = false; + for (int rx = 0; rx < sizeX; rx++) { + int wx = qMinX + rx; + for (int rz = 0; rz < sizeZ; rz++) { + int wz = qMinZ + rz; + for (int ry = 0; ry < sizeY; ry++) { + int wy = qMinY + ry; + var biome = source.getNoiseBiome(wx, wy, wz); + biomes[index(rx, ry, rz)] = biome; + if (biome != firstSeen) { + if (firstSeen == null) { + firstSeen = biome; + } else { + seenMultiple = true; + } + } + } + } + } + return seenMultiple; + } + + private void computeUniformity() { + // For each quart position, check if all 8 Voronoi candidates hold the same biome. + // If so, the Voronoi result is guaranteed to be that biome regardless of fractional + // position, so we can skip the distance computation entirely. + var uniform = this.uniform; + int sizeX = this.sizeX, sizeY = this.sizeY, sizeZ = this.sizeZ; + + for (int rx = 0; rx < sizeX - 1; rx++) { + for (int rz = 0; rz < sizeZ - 1; rz++) { + for (int ry = 0; ry < sizeY - 1; ry++) { + uniform[index(rx, ry, rz)] = isUniform(rx, ry, rz); + } + } + } + } + + private void computeBiases(long biomeZoomSeed) { + int sizeX = this.sizeX, sizeY = this.sizeY, sizeZ = this.sizeZ; + int qMinX = this.qMinX, qMinY = this.qMinY, qMinZ = this.qMinZ; + + // Pre-compute bias (fiddle) values for the Voronoi distance computation. + for (int rx = 0; rx < sizeX; rx++) { + int wx = qMinX + rx; + for (int rz = 0; rz < sizeZ; rz++) { + int wz = qMinZ + rz; + for (int ry = 0; ry < sizeY; ry++) { + computeBias(index(rx, ry, rz), biomeZoomSeed, wx, qMinY + ry, wz); + } + } + } + } + + private void computeBias(int idx, long seed, int x, int y, int z) { + // Reproduces the LCG chain from BiomeManager.getFiddledDistance exactly + long s = LinearCongruentialGenerator.next(seed, x); + s = LinearCongruentialGenerator.next(s, y); + s = LinearCongruentialGenerator.next(s, z); + s = LinearCongruentialGenerator.next(s, x); + s = LinearCongruentialGenerator.next(s, y); + s = LinearCongruentialGenerator.next(s, z); + biasX[idx] = getFiddle(s); + s = LinearCongruentialGenerator.next(s, seed); + biasY[idx] = getFiddle(s); + s = LinearCongruentialGenerator.next(s, seed); + biasZ[idx] = getFiddle(s); + } + + private static double getFiddle(long seed) { + double d = (double) Math.floorMod(seed >> 24, 1024) / 1024.0D; + return (d - 0.5D) * 0.9D; + } + + private boolean isUniform(int rx, int ry, int rz) { + var biomes = this.biomes; + Holder ref = biomes[index(rx, ry, rz)]; + for (int dx = 0; dx <= 1; dx++) { + for (int dy = 0; dy <= 1; dy++) { + for (int dz = 0; dz <= 1; dz++) { + if (biomes[index(rx + dx, ry + dy, rz + dz)] != ref) { + return false; + } + } + } + } + return true; + } + + private int index(int rx, int ry, int rz) { + return (rx * sizeY + ry) * sizeZ + rz; + } + + @Override + public Holder apply(BlockPos pos) { + return getBiome(pos); + } + + public Holder getBiome(BlockPos pos) { + int i = pos.getX() - 2; + int j = pos.getY() - 2; + int k = pos.getZ() - 2; + + int rx = QuartPos.fromBlock(i) - qMinX; + int ry = QuartPos.fromBlock(j) - qMinY; + int rz = QuartPos.fromBlock(k) - qMinZ; + + if (rx < 0 || rx >= sizeX - 1 || ry < 0 || ry >= sizeY - 1 || rz < 0 || rz >= sizeZ - 1) { + return fallbackManager.getBiome(pos); + } + + int baseIdx = index(rx, ry, rz); + if (uniform[baseIdx]) { + return biomes[baseIdx]; + } + + return getBiomeWithVoronoi(i, j, k, rx, ry, rz); + } + + private Holder getBiomeWithVoronoi(int i, int j, int k, int rx, int ry, int rz) { + var biasX = this.biasX; + var biasY = this.biasY; + var biasZ = this.biasZ; + + double d0 = (double) QuartPos.quartLocal(i) / 4.0D; + double d1 = (double) QuartPos.quartLocal(j) / 4.0D; + double d2 = (double) QuartPos.quartLocal(k) / 4.0D; + + int closestIdx = 0; + double closestDist = Double.POSITIVE_INFINITY; + + for (int c = 0; c < 8; c++) { + boolean fx = (c & 4) == 0; + boolean fy = (c & 2) == 0; + boolean fz = (c & 1) == 0; + + int idx = index( + rx + (fx ? 0 : 1), + ry + (fy ? 0 : 1), + rz + (fz ? 0 : 1) + ); + + double dx = (fx ? d0 : d0 - 1.0D) + biasX[idx]; + double dy = (fy ? d1 : d1 - 1.0D) + biasY[idx]; + double dz = (fz ? d2 : d2 - 1.0D) + biasZ[idx]; + double dist = Mth.square(dx) + Mth.square(dy) + Mth.square(dz); + + if (dist < closestDist) { + closestDist = dist; + closestIdx = idx; + } + } + + return biomes[closestIdx]; + } +} diff --git a/src/main/java/org/embeddedt/modernfix/world/gen/PrefetchingBlockColumn.java b/src/main/java/org/embeddedt/modernfix/world/gen/PrefetchingBlockColumn.java new file mode 100644 index 00000000..ce2eb988 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/world/gen/PrefetchingBlockColumn.java @@ -0,0 +1,126 @@ +package org.embeddedt.modernfix.world.gen; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.BlockColumn; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.levelgen.Heightmap; + +/** + * Wraps a BlockColumn and prefetches all block states for a column into an array. + * + *

Writes bypass {@link ChunkAccess#setBlockState} and go directly to the section, + * skipping heightmap and light updates that are unnecessary during the surface building + * stage (which runs before {@code INITIALIZE_LIGHT}). + */ +public class PrefetchingBlockColumn implements BlockColumn { + private static final BlockState AIR = Blocks.AIR.defaultBlockState(); + private static final BlockState VOID_AIR = Blocks.VOID_AIR.defaultBlockState(); + + private final BlockState[] states; + private final BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + + private ChunkAccess chunk; + private LevelChunkSection[] sections; + private int minBuildHeight; + private int localX, localZ; + + public PrefetchingBlockColumn(int height) { + this.states = new BlockState[height]; + } + + public int getExpectedHeight() { + return this.states.length; + } + + /** + * Prefetch all block states for the column at the given local XZ coordinates. + * Must be called before any getBlock/setBlock calls for this column. + */ + public void prefetch(ChunkAccess chunk, int localX, int localZ) { + if (chunk.getHeight() != this.states.length) { + throw new IllegalStateException(); + } + this.chunk = chunk; + this.sections = chunk.getSections(); + this.minBuildHeight = chunk.getMinY(); + this.localX = localX; + this.localZ = localZ; + var sections = this.sections; + var states = this.states; + int offset = 0; + for (LevelChunkSection section : sections) { + if (section.hasOnlyAir()) { + for (int y = 0; y < 16; y++) { + states[offset + y] = AIR; + } + } else { + var container = section.getStates(); + for (int y = 0; y < 16; y++) { + states[offset + y] = container.get(localX, y, localZ); + } + } + offset += 16; + } + } + + /** + * Clear cached references to allow GC of the chunk between uses. + */ + public void dispose() { + this.chunk = null; + this.sections = null; + } + + @Override + public BlockState getBlock(int y) { + int idx = y - minBuildHeight; + if (idx >= 0 && idx < states.length) { + return states[idx]; + } + return VOID_AIR; + } + + private void markPostprocessing(int y) { + cursor.set( + SectionPos.sectionToBlockCoord(chunk.getPos().x(), localX), + y, + SectionPos.sectionToBlockCoord(chunk.getPos().z(), localZ) + ); + chunk.markPosForPostprocessing(cursor); + } + + private void updateHeightmap(Heightmap.Types type, int y, BlockState newState) { + chunk.getOrCreateHeightmapUnprimed(type).update(localX, y, localZ, newState); + } + + @Override + public void setBlock(int y, BlockState state) { + int idx = y - minBuildHeight; + var states = this.states; + if (idx >= 0 && idx < states.length) { + BlockState oldState = states[idx]; + if (oldState == state) { + return; + } + states[idx] = state; + // Write directly to the section, bypassing ProtoChunk.setBlockState which + // does expensive heightmap updates that are not needed during surface building. + int sectionIdx = idx >> 4; + sections[sectionIdx].setBlockState(localX, y & 15, localZ, state, false); + if (!state.getFluidState().isEmpty()) { + markPostprocessing(y); + } + // Update heightmaps if the air/motion-blocking properties changed. + if (oldState.isAir() != state.isAir()) { + updateHeightmap(Heightmap.Types.WORLD_SURFACE_WG, y, state); + } + if (oldState.blocksMotion() != state.blocksMotion()) { + updateHeightmap(Heightmap.Types.OCEAN_FLOOR_WG, y, state); + } + } + } +} diff --git a/src/main/java/org/embeddedt/modernfix/world/gen/SurfaceRuleOptimizer.java b/src/main/java/org/embeddedt/modernfix/world/gen/SurfaceRuleOptimizer.java new file mode 100644 index 00000000..42492fa1 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/world/gen/SurfaceRuleOptimizer.java @@ -0,0 +1,100 @@ +package org.embeddedt.modernfix.world.gen; + +import it.unimi.dsi.fastutil.objects.Reference2ObjectMaps; +import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; +import net.minecraft.core.Holder; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.SurfaceRules; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class SurfaceRuleOptimizer { + public static @Nullable SurfaceRules.SurfaceRule optimizeSequenceRule(SurfaceRules.SequenceRuleSource source, SurfaceRules.Context context) { + // First pass: collect which biomes appear and count biome-gated branches + Reference2ObjectOpenHashMap, List> perBiomeSources = new Reference2ObjectOpenHashMap<>(); + int biomeGatedBranches = 0; + for (var innerSource : source.sequence()) { + if (innerSource instanceof SurfaceRules.TestRuleSource testRuleSource + && testRuleSource.ifTrue() instanceof SurfaceRules.BiomeConditionSource biomeConditionSource) { + biomeGatedBranches++; + for (var biome : biomeConditionSource.biomes) { + perBiomeSources.putIfAbsent(biome, new ArrayList<>()); + } + } + } + if (biomeGatedBranches < 3) { + return null; + } + // Second pass: build per-biome source lists preserving original interleaving order + List noMatchSources = new ArrayList<>(); + for (var innerSource : source.sequence()) { + if (innerSource instanceof SurfaceRules.TestRuleSource testRuleSource + && testRuleSource.ifTrue() instanceof SurfaceRules.BiomeConditionSource biomeConditionSource) { + // Add the inner rule (condition stripped) only to the matching biomes' lists + for (var biome : biomeConditionSource.biomes) { + perBiomeSources.get(biome).add(testRuleSource.thenRun()); + } + } else { + // Non-biome-gated rule: add to every biome list and the no-match list + for (var list : perBiomeSources.values()) { + list.add(innerSource); + } + noMatchSources.add(innerSource); + } + } + // Compile all source lists into rule lists + Reference2ObjectOpenHashMap, List> compiledBiomeMatch = new Reference2ObjectOpenHashMap<>(perBiomeSources.size()); + Reference2ObjectMaps.fastForEach(perBiomeSources, entry -> { + List compiled = new ArrayList<>(entry.getValue().size()); + for (var src : entry.getValue()) { + compiled.add(src.apply(context)); + } + compiledBiomeMatch.put(entry.getKey(), List.copyOf(compiled)); + }); + List compiledNoMatch = new ArrayList<>(noMatchSources.size()); + for (var src : noMatchSources) { + compiledNoMatch.add(src.apply(context)); + } + return new OptimizedBiomeLookupSequenceRule(compiledBiomeMatch, List.copyOf(compiledNoMatch), context); + } + + public record OptimizedBiomeLookupSequenceRule( + Map, List> rulesForBiomeMatch, + List rulesForNoBiomeMatch, + SurfaceRules.Context context + ) implements SurfaceRules.SurfaceRule { + @Override + public @Nullable BlockState tryApply(int x, int y, int z) { + var biome = context.biome.get(); + var key = (biome instanceof Holder.Reference ref) ? ref.key() : biome.unwrapKey().orElseThrow(); + var ruleList = rulesForBiomeMatch.getOrDefault(key, rulesForNoBiomeMatch); + //noinspection ForLoopReplaceableByForEach + for (int i = 0; i < ruleList.size(); i++) { + var rule = ruleList.get(i); + var state = rule.tryApply(x, y, z); + if (state != null) { + return state; + } + } + return null; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + OptimizedBiomeLookupSequenceRule that = (OptimizedBiomeLookupSequenceRule) o; + return rulesForBiomeMatch.equals(that.rulesForBiomeMatch) && rulesForNoBiomeMatch.equals(that.rulesForNoBiomeMatch); + } + + @Override + public int hashCode() { + return Objects.hash(rulesForBiomeMatch, rulesForNoBiomeMatch); + } + } +} diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg index 6730f70c..a90bfbfd 100644 --- a/src/main/resources/META-INF/accesstransformer.cfg +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -5,9 +5,19 @@ public net.minecraft.client.renderer.block.model.multipart.MultiPart definition public net.minecraft.client.resources.model.ModelBakery$ModelBakerImpl public net.minecraft.client.resources.model.ModelBakery$ModelBakerImpl (Lnet/minecraft/client/resources/model/ModelBakery;Lnet/minecraft/client/resources/model/ModelBakery$TextureGetter;Lnet/minecraft/client/resources/model/ModelResourceLocation;)V public net.minecraft.world.level.levelgen.SurfaceRules$SequenceRule +public net.minecraft.world.level.levelgen.SurfaceRules$SequenceRuleSource +public net.minecraft.world.level.levelgen.SurfaceRules$SequenceRuleSource (Ljava/util/List;)V +public net.minecraft.world.level.levelgen.SurfaceRules$TestRuleSource +public net.minecraft.world.level.levelgen.SurfaceRules$TestRuleSource (Lnet/minecraft/world/level/levelgen/SurfaceRules$ConditionSource;Lnet/minecraft/world/level/levelgen/SurfaceRules$RuleSource;)V +public net.minecraft.world.level.levelgen.SurfaceRules$BiomeConditionSource +public net.minecraft.world.level.levelgen.SurfaceRules$BiomeConditionSource (Ljava/util/List;)V +public net.minecraft.world.level.levelgen.SurfaceRules$BiomeConditionSource biomes +public net.minecraft.world.level.levelgen.SurfaceRules$Context biome public net.minecraft.client.renderer.block.model.BlockModel GSON public net.minecraft.server.packs.resources.ProfiledReloadInstance$State reloadNanos public net.minecraft.server.packs.resources.ProfiledReloadInstance$State preparationNanos +public net.minecraft.client.resources.model.ModelBakery$BlockStateDefinitionException +public net.minecraft.world.item.crafting.Ingredient$TagValue f_43959_ public net.minecraft.server.MinecraftServer$ReloadableResources public net.minecraft.resources.ResourceKey (Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)V public net.minecraft.server.level.ServerChunkCache$MainThreadExecutor @@ -53,10 +63,13 @@ public net.minecraft.server.level.ChunkMap pendingUnloads public net.minecraft.world.level.levelgen.DensityFunctions$MulOrAdd$Type public net.minecraft.client.renderer.entity.EnderDragonRenderer$DragonModel entity public net.minecraft.client.KeyMapping ALL +protected net.minecraft.server.level.GenerationChunkHolder futures +protected net.minecraft.server.level.GenerationChunkHolder startedWork + public net.minecraft.client.resources.model.BlockStateModelLoader$LoadedBlockModelDefinition public net.minecraft.client.resources.model.ModelManager$ResolvedModels public net.minecraft.client.resources.model.ModelManager$ResolvedModels (Lnet/minecraft/client/resources/model/ResolvedModel;Ljava/util/Map;)V public net.minecraft.client.resources.model.ModelDiscovery$ModelWrapper public net.minecraft.client.resources.model.ModelDiscovery$ModelWrapper ModelWrapper(Lnet/minecraft/resources/Identifier;Lnet/minecraft/client/resources/model/UnbakedModel;Z)V -public net.minecraft.client.resources.model.ModelDiscovery createAndQueueWrapper(Lnet/minecraft/resources/Identifier;Lnet/minecraft/client/resources/model/UnbakedModel;)Lnet/minecraft/client/resources/model/ModelDiscovery$ModelWrapper; \ No newline at end of file +public net.minecraft.client.resources.model.ModelDiscovery createAndQueueWrapper(Lnet/minecraft/resources/Identifier;Lnet/minecraft/client/resources/model/UnbakedModel;)Lnet/minecraft/client/resources/model/ModelDiscovery$ModelWrapper;