diff --git a/src/main/java/com/mitchej123/hodgepodge/config/SpeedupsConfig.java b/src/main/java/com/mitchej123/hodgepodge/config/SpeedupsConfig.java index 64a6d5a8..f751abd9 100644 --- a/src/main/java/com/mitchej123/hodgepodge/config/SpeedupsConfig.java +++ b/src/main/java/com/mitchej123/hodgepodge/config/SpeedupsConfig.java @@ -40,6 +40,28 @@ public class SpeedupsConfig { @Config.DefaultBoolean(true) public static boolean speedupChunkProviderClient; + @Config.Comment("Rewrites internal cache methods to be safer and faster. Experimental, use at your own risk!") + @Config.DefaultBoolean(false) + @Config.RequiresMcRestart + public static boolean fastIntCache; + + @Config.Comment("Removes hard caps on chunk handling speed. Experimental and probably incompatible with hybrid servers!") + @Config.DefaultBoolean(false) + @Config.RequiresMcRestart + public static boolean fastChunkHandling; + + @Config.Comment("The maximum speed of chunkloading per player, in chunks/tick. High values may overload clients! Only active with fastChunkHandling.\n" + + "For reference: Vanilla is 5, or 100 chunks/sec. At 32 render distance = 4225 chunks, loading the world would take 42.25 seconds.") + @Config.DefaultInt(50) + @Config.RangeInt(min = 5) + public static int maxSendSpeed; + + @Config.Comment("The maximum speed of chunk unloading, in chunks/tick. High values may overload servers! Only active with fastChunkHandling.\n" + + "For reference: Vanilla is 100, or 2000 chunks/sec. At 32 render distance = 4225 chunks, unloading the world would take 2.1125 seconds.") + @Config.DefaultInt(220) + @Config.RangeInt(min = 100) + public static int maxUnloadSpeed; + // Biomes O' Plenty @Config.Comment("Speedup biome fog rendering in BiomesOPlenty") diff --git a/src/main/java/com/mitchej123/hodgepodge/hax/FastIntCache.java b/src/main/java/com/mitchej123/hodgepodge/hax/FastIntCache.java new file mode 100644 index 00000000..62ffd6fd --- /dev/null +++ b/src/main/java/com/mitchej123/hodgepodge/hax/FastIntCache.java @@ -0,0 +1,32 @@ +package com.mitchej123.hodgepodge.hax; + +import java.util.List; +import java.util.Map; + +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; + +public class FastIntCache { + + private static final int SMALLEST = 256; + private static final int MIN_LEVEL = 32 - Integer.numberOfLeadingZeros(SMALLEST - 1); + + private static final Map> cachedObjects = new Int2ObjectOpenHashMap<>(); + + public static synchronized int[] getCache(int size) { + + // Get the smallest power of two larger than or equal to the number + final int level = (size <= SMALLEST) ? MIN_LEVEL : 32 - Integer.numberOfLeadingZeros(size - 1); + + final List caches = cachedObjects.computeIfAbsent(level, i -> new ObjectArrayList<>()); + + if (caches.isEmpty()) return new int[2 << (level - 1)]; + return caches.remove(caches.size() - 1); + } + + public static synchronized void releaseCache(int[] cache) { + + final int level = (cache.length <= SMALLEST) ? MIN_LEVEL : 32 - Integer.numberOfLeadingZeros(cache.length - 1); + cachedObjects.computeIfAbsent(level, i -> new ObjectArrayList<>()).add(cache); + } +} diff --git a/src/main/java/com/mitchej123/hodgepodge/mixins/Mixins.java b/src/main/java/com/mitchej123/hodgepodge/mixins/Mixins.java index e9621d13..493b1ca1 100644 --- a/src/main/java/com/mitchej123/hodgepodge/mixins/Mixins.java +++ b/src/main/java/com/mitchej123/hodgepodge/mixins/Mixins.java @@ -454,6 +454,16 @@ public enum Mixins { new Builder("Memory fixes").setPhase(Phase.EARLY).setSide(Side.CLIENT).addTargetedMod(TargetedMod.VANILLA) .addMixinClasses("memory.MixinFMLClientHandler").setApplyIf(() -> FixesConfig.enableMemoryFixes)), + FAST_INT_CACHE(new Builder("Rewrite internal caching methods to be safer and faster").setPhase(Phase.EARLY) + .setSide(Side.BOTH).addTargetedMod(TargetedMod.VANILLA) + .addMixinClasses("minecraft.fastload.MixinIntCache", "minecraft.fastload.MixinWorldChunkManager") + .setApplyIf(() -> SpeedupsConfig.fastIntCache)), + + FAST_CHUNK_LOADING(new Builder("Invasively accelerates chunk handling").setPhase(Phase.EARLY).setSide(Side.BOTH) + .addTargetedMod(TargetedMod.VANILLA) + .addMixinClasses("minecraft.fastload.MixinEntityPlayerMP", "minecraft.fastload.MixinChunkProviderServer") + .setApplyIf(() -> SpeedupsConfig.fastChunkHandling)), + MEMORY_FIXES_IC2(new Builder("Removes allocation spam from the Direction.applyTo method").setPhase(Phase.LATE) .setSide(Side.BOTH).addMixinClasses("ic2.MixinDirection_Memory") .setApplyIf(() -> FixesConfig.enableMemoryFixes).addTargetedMod(TargetedMod.IC2)), diff --git a/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinChunkProviderServer.java b/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinChunkProviderServer.java new file mode 100644 index 00000000..b0667fb9 --- /dev/null +++ b/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinChunkProviderServer.java @@ -0,0 +1,18 @@ +package com.mitchej123.hodgepodge.mixins.early.minecraft.fastload; + +import net.minecraft.world.gen.ChunkProviderServer; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.ModifyConstant; + +import com.mitchej123.hodgepodge.config.SpeedupsConfig; + +@Mixin(ChunkProviderServer.class) +public class MixinChunkProviderServer { + + @ModifyConstant(method = "unloadQueuedChunks", constant = @Constant(intValue = 100, ordinal = 0)) + private int hodgepodge$fastChunkDiscard(int original) { + return SpeedupsConfig.maxUnloadSpeed; + } +} diff --git a/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinEntityPlayerMP.java b/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinEntityPlayerMP.java new file mode 100644 index 00000000..458d34d2 --- /dev/null +++ b/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinEntityPlayerMP.java @@ -0,0 +1,143 @@ +package com.mitchej123.hodgepodge.mixins.early.minecraft.fastload; + +import java.util.Iterator; +import java.util.List; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.network.NetHandlerPlayServer; +import net.minecraft.network.play.server.S26PacketMapChunkBulk; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.world.ChunkCoordIntPair; +import net.minecraft.world.World; +import net.minecraft.world.WorldServer; +import net.minecraft.world.chunk.Chunk; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.world.ChunkWatchEvent; + +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +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.callback.CallbackInfo; + +import com.mitchej123.hodgepodge.config.SpeedupsConfig; +import com.mojang.authlib.GameProfile; + +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.ObjectImmutableList; + +@Mixin(EntityPlayerMP.class) +public abstract class MixinEntityPlayerMP extends EntityPlayer { + + @Shadow + @Final + public List loadedChunks; + + @Shadow + public NetHandlerPlayServer playerNetServerHandler; + + @Shadow + protected abstract void func_147097_b(TileEntity te); + + // Cache these lists so they don't have to be reallocated every onUpdate + @Unique + private final ObjectArrayList> hodgepodge$chunkSends = new ObjectArrayList<>(); + @Unique + private final ObjectArrayList hodgepodge$rollingChunks = new ObjectArrayList<>(); + @Unique + private final ObjectArrayList hodgepodge$rollingTEs = new ObjectArrayList<>(); + + @Shadow + public abstract WorldServer getServerForPlayer(); + + public MixinEntityPlayerMP(World world, GameProfile profile) { + super(world, profile); + } + + /** + * This injects just before where vanilla handles chunk sending and returns before then. This will mess up any + * mixins that target the tail of this function, but other solutions are similarly invasive. + */ + @Inject( + method = "onUpdate", + at = @At( + value = "FIELD", + target = "Lnet/minecraft/entity/player/EntityPlayerMP;loadedChunks:Ljava/util/List;", + shift = At.Shift.BEFORE, + ordinal = 0), + cancellable = true) + private void hodgepodge$replaceChunkSending(CallbackInfo ci) { + if (loadedChunks.isEmpty()) { + ci.cancel(); + return; + } + + int numChunks = 0; + final Iterator allChunks = loadedChunks.iterator(); + final int chunksPerPacket = S26PacketMapChunkBulk.func_149258_c(); + + // Gather chunks to send, unload chunks outside of range + while (allChunks.hasNext() && numChunks < SpeedupsConfig.maxSendSpeed) { + final ChunkCoordIntPair ccip = allChunks.next(); + if (ccip == null) { + allChunks.remove(); + continue; + } + + if (!worldObj.blockExists(ccip.chunkXPos << 4, 0, ccip.chunkZPos << 4)) { + continue; + } + + final Chunk chunk = worldObj.getChunkFromChunkCoords(ccip.chunkXPos, ccip.chunkZPos); + if (!chunk.func_150802_k()) { // chunk.isNotEmpty() + continue; + } + + // If there's enough to fill a packet, overflow it + if (hodgepodge$rollingChunks.size() == chunksPerPacket) { + hodgepodge$chunkSends.add(new ObjectImmutableList<>(hodgepodge$rollingChunks)); + hodgepodge$rollingChunks.clear(); + } + hodgepodge$rollingChunks.add(chunk); + hodgepodge$rollingTEs.addAll( + ((WorldServer) worldObj).func_147486_a( + ccip.chunkXPos * 16, + 0, + ccip.chunkZPos * 16, + ccip.chunkXPos * 16 + 15, + 256, + ccip.chunkZPos * 16 + 15)); + allChunks.remove(); + numChunks++; + } + + // Since it only gets cleared when it's full, rollingChunks always holds the last ones + hodgepodge$chunkSends.add(new ObjectImmutableList<>(hodgepodge$rollingChunks)); + + if (numChunks > 0) { + for (int i = 0; i < hodgepodge$chunkSends.size(); ++i) { + playerNetServerHandler.sendPacket(new S26PacketMapChunkBulk(hodgepodge$chunkSends.get(i))); + } + + for (TileEntity tileentity : hodgepodge$rollingTEs) { + func_147097_b(tileentity); + } + + for (int i = 0; i < numChunks; ++i) { + final int div = i / chunksPerPacket; + final int rem = i % chunksPerPacket; + final Chunk chunk = hodgepodge$chunkSends.get(div).get(rem); + getServerForPlayer().getEntityTracker().func_85172_a((EntityPlayerMP) (Object) this, chunk); + MinecraftForge.EVENT_BUS + .post(new ChunkWatchEvent.Watch(chunk.getChunkCoordIntPair(), (EntityPlayerMP) (Object) this)); + } + } + hodgepodge$chunkSends.clear(); + hodgepodge$rollingChunks.clear(); + hodgepodge$rollingTEs.clear(); + ci.cancel(); + } +} diff --git a/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinIntCache.java b/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinIntCache.java new file mode 100644 index 00000000..b9a2cee7 --- /dev/null +++ b/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinIntCache.java @@ -0,0 +1,22 @@ +package com.mitchej123.hodgepodge.mixins.early.minecraft.fastload; + +import net.minecraft.world.gen.layer.IntCache; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; + +import com.mitchej123.hodgepodge.hax.FastIntCache; + +@Mixin(IntCache.class) +public class MixinIntCache { + + /** + * @author ah-OOG-ah + * @reason The old methods are non-threadsafe and barely safe at all - they recycle all allocated instances instead + * of explicitly releasing them. + */ + @Overwrite + public static synchronized int[] getIntCache(int size) { + return FastIntCache.getCache(size); + } +} diff --git a/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinWorldChunkManager.java b/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinWorldChunkManager.java new file mode 100644 index 00000000..3c48a448 --- /dev/null +++ b/src/main/java/com/mitchej123/hodgepodge/mixins/early/minecraft/fastload/MixinWorldChunkManager.java @@ -0,0 +1,187 @@ +package com.mitchej123.hodgepodge.mixins.early.minecraft.fastload; + +import java.util.List; +import java.util.Random; + +import net.minecraft.crash.CrashReport; +import net.minecraft.crash.CrashReportCategory; +import net.minecraft.util.ReportedException; +import net.minecraft.world.ChunkPosition; +import net.minecraft.world.biome.BiomeGenBase; +import net.minecraft.world.biome.WorldChunkManager; +import net.minecraft.world.gen.layer.GenLayer; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import com.llamalad7.mixinextras.sugar.Local; +import com.mitchej123.hodgepodge.hax.FastIntCache; + +@Mixin(WorldChunkManager.class) +public class MixinWorldChunkManager { + + @Shadow + private GenLayer genBiomes; + + @Redirect( + method = { "getRainfall", "getBiomesForGeneration", "areBiomesViable", + "getBiomeGenAt([Lnet/minecraft/world/biome/BiomeGenBase;IIIIZ)[Lnet/minecraft/world/biome/BiomeGenBase;", + "findBiomePosition" }, + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/gen/layer/IntCache;resetIntCache()V")) + private void hodgepodge$nukeIntCache() {} + + @Inject( + method = "getRainfall", + at = @At( + value = "INVOKE_ASSIGN", + target = "Lnet/minecraft/world/gen/layer/GenLayer;getInts(IIII)[I", + shift = At.Shift.AFTER), + cancellable = true) + private void hodgepodge$recycleCacheRain(float[] downfalls, int x, int z, int width, int height, + CallbackInfoReturnable cir, @Local(name = "aint") int[] ints) { + + for (int i = 0; i < width * height; ++i) { + try { + float f = (float) BiomeGenBase.getBiome(ints[i]).getIntRainfall() / 65536.0F; + + if (f > 1.0F) { + f = 1.0F; + } + + downfalls[i] = f; + } catch (Throwable throwable) { + CrashReport crashreport = CrashReport.makeCrashReport(throwable, "Invalid Biome id"); + CrashReportCategory crashreportcategory = crashreport.makeCategory("DownfallBlock"); + crashreportcategory.addCrashSection("biome id", i); + crashreportcategory.addCrashSection("downfalls[] size", downfalls.length); + crashreportcategory.addCrashSection("x", x); + crashreportcategory.addCrashSection("z", z); + crashreportcategory.addCrashSection("w", width); + crashreportcategory.addCrashSection("h", height); + throw new ReportedException(crashreport); + } + } + + FastIntCache.releaseCache(ints); + cir.setReturnValue(downfalls); + } + + @Inject( + method = "getBiomesForGeneration", + at = @At( + value = "INVOKE_ASSIGN", + target = "Lnet/minecraft/world/gen/layer/GenLayer;getInts(IIII)[I", + shift = At.Shift.AFTER), + cancellable = true) + private void hodgepodge$recycleCacheBiomes(BiomeGenBase[] biomes, int x, int z, int width, int height, + CallbackInfoReturnable cir, @Local(name = "aint") int[] ints) { + + try { + for (int i = 0; i < width * height; ++i) { + biomes[i] = BiomeGenBase.getBiome(ints[i]); + } + + FastIntCache.releaseCache(ints); + cir.setReturnValue(biomes); + } catch (Throwable throwable) { + CrashReport crashreport = CrashReport.makeCrashReport(throwable, "Invalid Biome id"); + CrashReportCategory crashreportcategory = crashreport.makeCategory("RawBiomeBlock"); + crashreportcategory.addCrashSection("biomes[] size", biomes.length); + crashreportcategory.addCrashSection("x", x); + crashreportcategory.addCrashSection("z", z); + crashreportcategory.addCrashSection("w", width); + crashreportcategory.addCrashSection("h", height); + throw new ReportedException(crashreport); + } + } + + @Inject( + method = "getBiomeGenAt([Lnet/minecraft/world/biome/BiomeGenBase;IIIIZ)[Lnet/minecraft/world/biome/BiomeGenBase;", + at = @At( + value = "INVOKE_ASSIGN", + target = "Lnet/minecraft/world/gen/layer/GenLayer;getInts(IIII)[I", + shift = At.Shift.AFTER), + cancellable = true) + private void hodgepodge$recycleCacheBiomeAt(BiomeGenBase[] biomes, int p_76931_2_, int p_76931_3_, int width, + int height, boolean p_76931_6_, CallbackInfoReturnable cir, + @Local(name = "aint") int[] ints) { + + for (int i = 0; i < width * height; ++i) { + biomes[i] = BiomeGenBase.getBiome(ints[i]); + } + + FastIntCache.releaseCache(ints); + cir.setReturnValue(biomes); + } + + @Inject( + method = "areBiomesViable", + at = @At( + value = "INVOKE_ASSIGN", + target = "Lnet/minecraft/world/gen/layer/GenLayer;getInts(IIII)[I", + shift = At.Shift.AFTER), + cancellable = true) + private void hodgepodge$recycleCacheViable(int x, int z, int radius, List allowed, + CallbackInfoReturnable cir, @Local(name = "l1") int areaWidth, @Local(name = "i2") int areaHeight, + @Local(ordinal = 0) int[] cache) { + + try { + for (int i = 0; i < areaWidth * areaHeight; ++i) { + BiomeGenBase biomegenbase = BiomeGenBase.getBiome(cache[i]); + + if (!allowed.contains(biomegenbase)) { + + FastIntCache.releaseCache(cache); + cir.setReturnValue(false); + return; + } + } + + FastIntCache.releaseCache(cache); + cir.setReturnValue(true); + } catch (Throwable throwable) { + CrashReport crashreport = CrashReport.makeCrashReport(throwable, "Invalid Biome id"); + CrashReportCategory crashreportcategory = crashreport.makeCategory("Layer"); + crashreportcategory.addCrashSection("Layer", this.genBiomes.toString()); + crashreportcategory.addCrashSection("x", x); + crashreportcategory.addCrashSection("z", z); + crashreportcategory.addCrashSection("radius", radius); + crashreportcategory.addCrashSection("allowed", allowed); + throw new ReportedException(crashreport); + } + } + + @Inject( + method = "findBiomePosition", + at = @At( + value = "INVOKE_ASSIGN", + target = "Lnet/minecraft/world/gen/layer/GenLayer;getInts(IIII)[I", + shift = At.Shift.AFTER), + cancellable = true) + private void hodgepodge$recycleCacheFindBiome(int x, int z, int radius, List p_150795_4_, + Random p_150795_5_, CallbackInfoReturnable cir, @Local(name = "l1") int l1, + @Local(name = "i2") int i2, @Local(name = "l") int l, @Local(name = "i1") int i1, + @Local(ordinal = 0) int[] ints) { + + ChunkPosition chunkposition = null; + int j2 = 0; + + for (int i = 0; i < l1 * i2; ++i) { + int l2 = l + i % l1 << 2; + int i3 = i1 + i / l1 << 2; + BiomeGenBase biomegenbase = BiomeGenBase.getBiome(ints[i]); + + if (p_150795_4_.contains(biomegenbase) && (chunkposition == null || p_150795_5_.nextInt(j2 + 1) == 0)) { + chunkposition = new ChunkPosition(l2, 0, i3); + ++j2; + } + } + + FastIntCache.releaseCache(ints); + cir.setReturnValue(chunkposition); + } +} diff --git a/src/main/resources/META-INF/hodgepodge_at.cfg b/src/main/resources/META-INF/hodgepodge_at.cfg index f1bc32a1..529105e2 100644 --- a/src/main/resources/META-INF/hodgepodge_at.cfg +++ b/src/main/resources/META-INF/hodgepodge_at.cfg @@ -21,3 +21,5 @@ public net.minecraft.client.resources.FileResourcePack field_110600_d # resource public net.minecraft.client.resources.SimpleReloadableResourceManager field_110548_a # domainResourceManagers public net.minecraft.entity.EntityList field_75624_e # classToIDMapping public net.minecraft.world.WorldServer func_73053_d()V # wakeAllPlayers() + +public net.minecraft.server.management.PlayerManager$PlayerInstance