From 942376481bf775324ce6349766200b5f044bffa0 Mon Sep 17 00:00:00 2001 From: hayanesuru Date: Sat, 14 Jun 2025 22:58:48 +0900 Subject: [PATCH] reduce LevelChunkSection#tickingBlocks memory overhead --- .../features/0191-optimize-random-tick.patch | 130 ++++++++----- .../dreeam/leaf/world/RandomTickSystem.java | 173 ++++++++++++++++-- 2 files changed, 244 insertions(+), 59 deletions(-) diff --git a/leaf-server/minecraft-patches/features/0191-optimize-random-tick.patch b/leaf-server/minecraft-patches/features/0191-optimize-random-tick.patch index d8a31024..10f08105 100644 --- a/leaf-server/minecraft-patches/features/0191-optimize-random-tick.patch +++ b/leaf-server/minecraft-patches/features/0191-optimize-random-tick.patch @@ -4,6 +4,18 @@ Date: Fri, 6 Jun 2025 20:46:10 +0900 Subject: [PATCH] optimize random tick +diff --git a/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java b/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java +index 0d1443a113c07d7655e7b927a899447f70db8fa9..5f219ce1e386b29616694aef223136eec0ec80de 100644 +--- a/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java ++++ b/ca/spottedleaf/moonrise/patches/block_counting/BlockCountingChunkSection.java +@@ -6,6 +6,6 @@ public interface BlockCountingChunkSection { + + public boolean moonrise$hasSpecialCollidingBlocks(); + +- public ShortList moonrise$getTickingBlockList(); ++ // public ShortList moonrise$getTickingBlockList(); // Leaf - optimize random tick + + } diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java index 15bbd1f7f2a90b4b5427026d622764bb1c92d465..bc37fa20143eda45f9df07e382f2feca3909abce 100644 --- a/net/minecraft/server/level/ServerChunkCache.java @@ -17,9 +29,27 @@ index 15bbd1f7f2a90b4b5427026d622764bb1c92d465..bc37fa20143eda45f9df07e382f2feca if (flagAndHasNaturalSpawn) { // Gale - MultiPaper - skip unnecessary mob spawning computations this.level.tickCustomSpawners(this.spawnEnemies, this.spawnFriendlies); diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index eb849c57992658005e0f514c6f7923f8ca43bebf..cc6fd8533fce1839600488b996f9179d1399380d 100644 +index eb849c57992658005e0f514c6f7923f8ca43bebf..1591f04b34a9807f689a076112ef34060a600fc4 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java +@@ -1097,7 +1097,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + final net.minecraft.world.level.chunk.PalettedContainer states = section.states; + // Leaf end - Micro optimizations for random tick + +- final ca.spottedleaf.moonrise.common.list.ShortList tickList = section.moonrise$getTickingBlockList(); // Leaf - Micro optimizations for random tick - no redundant cast ++ final org.dreeam.leaf.world.RandomTickSystem.TickingBlockSet tickList = section.tickingBlocks; // Leaf - Micro optimizations for random tick - no redundant cast // Leaf - optimize random tick + + for (int i = 0; i < tickSpeed; ++i) { + final int index = simpleRandom.nextInt() & ((16 * 16 * 16) - 1); +@@ -1107,7 +1107,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + continue; + } + +- final int location = tickList.getRaw(index); // Leaf - Micro optimizations for random tick - no unnecessary operations ++ final int location = tickList.get(simpleRandom); // Leaf - Micro optimizations for random tick - no unnecessary operations // Leaf - optimize random tick + final BlockState state = states.get(location); + + // do not use a mutable pos, as some random tick implementations store the input without calling immutable()! @@ -1128,6 +1128,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe private int currentIceAndSnowTick = 0; protected void resetIceAndSnowTick() { this.currentIceAndSnowTick = this.simpleRandom.nextInt(16); } // Gale - Airplane - optimize random calls in chunk ticking @@ -39,59 +69,22 @@ index eb849c57992658005e0f514c6f7923f8ca43bebf..cc6fd8533fce1839600488b996f9179d } diff --git a/net/minecraft/world/level/chunk/LevelChunk.java b/net/minecraft/world/level/chunk/LevelChunk.java -index a90bf0d80ae4dac9b19b8e467b402917cc19a271..44b672671e0bea6a5f91e9b8573f9a8225a20f6a 100644 +index a90bf0d80ae4dac9b19b8e467b402917cc19a271..165cdb884c6e0aa80d035fc0b2e95c7dc311761e 100644 --- a/net/minecraft/world/level/chunk/LevelChunk.java +++ b/net/minecraft/world/level/chunk/LevelChunk.java -@@ -149,6 +149,48 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p +@@ -149,6 +149,11 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p } // Gale end - Airplane - optimize random calls in chunk ticking - instead of using a random every time the chunk is ticked, define when lightning strikes preemptively + // Leaf start - optimize random tick -+ private boolean leaf$tickingBlocksDirty = true; -+ private int leaf$tickingBlocksCount; -+ private int leaf$firstTickingSectionIndex = -1; -+ public final int leaf$tickingBlocksCount() { -+ if (!leaf$tickingBlocksDirty) { -+ return leaf$tickingBlocksCount; -+ } -+ leaf$tickingBlocksDirty = false; -+ int sum = 0; -+ leaf$firstTickingSectionIndex = -1; -+ for (int i = 0; i < sections.length; i++) { -+ LevelChunkSection section = sections[i]; -+ int size = section.moonrise$getTickingBlockList().size(); -+ if (size != 0 && leaf$firstTickingSectionIndex == -1) { -+ leaf$firstTickingSectionIndex = i; -+ } -+ sum += size; -+ } -+ leaf$tickingBlocksCount = sum; -+ return sum; -+ } -+ public final java.util.OptionalLong leaf$getTickingPos(int idx) { -+ if (leaf$firstTickingSectionIndex != -1) { -+ for (int i = leaf$firstTickingSectionIndex; i < sections.length; i++) { -+ LevelChunkSection section = sections[i]; -+ var l = section.moonrise$getTickingBlockList(); -+ int size = l.size(); -+ if (idx < size) { -+ short loc = l.getRaw(idx); -+ int x = (loc & 15) | (chunkPos.x << 4); -+ int y = (loc >>> 8) | ((getMinSectionY() + i) << 4); -+ int z = ((loc >>> 4) & 15) | (chunkPos.z << 4); -+ return java.util.OptionalLong.of(BlockPos.asLong(x, y, z)); -+ } -+ idx -= size; -+ } -+ } -+ leaf$tickingBlocksDirty = true; -+ return java.util.OptionalLong.empty(); -+ } ++ public boolean leaf$tickingBlocksDirty = true; ++ public int leaf$tickingBlocksCount; ++ public int leaf$firstTickingSectionIndex = -1; + // Leaf end - optimize random tick public LevelChunk(Level level, ChunkPos pos) { this(level, pos, UpgradeData.EMPTY, new LevelChunkTicks<>(), new LevelChunkTicks<>(), 0L, null, null, null); } -@@ -417,6 +459,11 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p +@@ -417,6 +422,11 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p if (blockState == state) { return null; } else { @@ -103,3 +96,52 @@ index a90bf0d80ae4dac9b19b8e467b402917cc19a271..44b672671e0bea6a5f91e9b8573f9a82 Block block = state.getBlock(); this.heightmaps.get(Heightmap.Types.MOTION_BLOCKING).update(i, y, i2, state); this.heightmaps.get(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES).update(i, y, i2, state); +diff --git a/net/minecraft/world/level/chunk/LevelChunkSection.java b/net/minecraft/world/level/chunk/LevelChunkSection.java +index 82f723ab2e92036c56ce6f09109a9c0e25dc025e..889fb5dda79a062f4dee242c2a32640e801fd397 100644 +--- a/net/minecraft/world/level/chunk/LevelChunkSection.java ++++ b/net/minecraft/world/level/chunk/LevelChunkSection.java +@@ -37,17 +37,22 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_ + private boolean isClient; + private static final short CLIENT_FORCED_SPECIAL_COLLIDING_BLOCKS = (short)9999; + private short specialCollidingBlocks; +- private final ca.spottedleaf.moonrise.common.list.ShortList tickingBlocks = new ca.spottedleaf.moonrise.common.list.ShortList(); ++ // private final ca.spottedleaf.moonrise.common.list.ShortList tickingBlocks = new ca.spottedleaf.moonrise.common.list.ShortList(); // Leaf - optimize random tick ++ public final org.dreeam.leaf.world.RandomTickSystem.TickingBlockSet tickingBlocks = new org.dreeam.leaf.world.RandomTickSystem.TickingBlockSet(); // Leaf - optimize random tick + + @Override + public final boolean moonrise$hasSpecialCollidingBlocks() { + return this.specialCollidingBlocks != 0; + } + ++ // Leaf start - optimize random tick ++ /* + @Override + public final ca.spottedleaf.moonrise.common.list.ShortList moonrise$getTickingBlockList() { + return this.tickingBlocks; + } ++ */ ++ // Leaf end - optimize random tick + // Paper end - block counting + + private LevelChunkSection(LevelChunkSection section) { +@@ -123,7 +128,7 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_ + final boolean oldTicking = oldState.isRandomlyTicking(); + final boolean newTicking = newState.isRandomlyTicking(); + if (oldTicking != newTicking) { +- final ca.spottedleaf.moonrise.common.list.ShortList tickingBlocks = this.tickingBlocks; ++ final org.dreeam.leaf.world.RandomTickSystem.TickingBlockSet tickingBlocks = this.tickingBlocks; // Leaf - optimize random tick + final short position = (short)(x | (z << 4) | (y << (4+4))); + + if (oldTicking) { +@@ -236,9 +241,9 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_ + final short[] raw = coordinates.elements(); + final int rawLen = raw.length; + +- final ca.spottedleaf.moonrise.common.list.ShortList tickingBlocks = this.tickingBlocks; ++ final org.dreeam.leaf.world.RandomTickSystem.TickingBlockSet tickingBlocks = this.tickingBlocks; // Leaf - optimize random tick + +- tickingBlocks.setMinCapacity(Math.min((rawLen + tickingBlocks.size()) * 3 / 2, 16*16*16)); ++ // tickingBlocks.setMinCapacity(Math.min((rawLen + tickingBlocks.size()) * 3 / 2, 16*16*16)); // Leaf - optimize random tick + + java.util.Objects.checkFromToIndex(0, paletteCount, raw.length); + for (int i = 0; i < paletteCount; ++i) { diff --git a/leaf-server/src/main/java/org/dreeam/leaf/world/RandomTickSystem.java b/leaf-server/src/main/java/org/dreeam/leaf/world/RandomTickSystem.java index f54a1e73..d111954c 100644 --- a/leaf-server/src/main/java/org/dreeam/leaf/world/RandomTickSystem.java +++ b/leaf-server/src/main/java/org/dreeam/leaf/world/RandomTickSystem.java @@ -1,14 +1,16 @@ package org.dreeam.leaf.world; +import it.unimi.dsi.fastutil.HashCommon; import it.unimi.dsi.fastutil.longs.LongArrayList; import net.minecraft.core.BlockPos; import net.minecraft.server.level.ServerLevel; -import net.minecraft.util.RandomSource; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.levelgen.BitRandomSource; import net.minecraft.world.level.material.FluidState; -import java.util.OptionalLong; +import java.util.Arrays; public final class RandomTickSystem { private static final long SCALE = 0x100000L; @@ -99,12 +101,35 @@ public final class RandomTickSystem { return world.chunkSource.getChunkAtIfLoadedImmediately((int) packed, (int) (packed >> 32)); } - private static void tickBlock(ServerLevel world, LevelChunk chunk, RandomSource random) { - OptionalLong optionalPos = chunk.leaf$getTickingPos(random.nextInt(chunk.leaf$tickingBlocksCount())); - if (optionalPos.isEmpty()) { + private static void tickBlock(ServerLevel world, LevelChunk chunk, BitRandomSource random) { + if (chunk.leaf$firstTickingSectionIndex == -1) { return; } - BlockPos pos = BlockPos.of(optionalPos.getAsLong()); + int idx = random.nextInt(chunk.leaf$tickingBlocksCount); + LevelChunkSection[] sections = chunk.getSections(); + int cx = chunk.locX; + int cz = chunk.locZ; + BlockPos pos = null; + for (int i = chunk.leaf$firstTickingSectionIndex; i < sections.length; i++) { + LevelChunkSection section = sections[i]; + var l = section.tickingBlocks; + int size = l.size(); + if (idx < size) { + short loc = l.get(random); + int x = (loc & 15) | (cx << 4); + int y = (loc >>> 8) | ((chunk.getMinSectionY() + i) << 4); + int z = ((loc >>> 4) & 15) | (cz << 4); + pos = new BlockPos(x, y, z); + break; + } + idx -= size; + } + if (pos == null) { + chunk.leaf$tickingBlocksDirty = true; + chunk.leaf$firstTickingSectionIndex = -1; + return; + } + BlockState state = chunk.getBlockStateFinal(pos.getX(), pos.getY(), pos.getZ()); state.randomTick(world, pos, random); @@ -118,7 +143,7 @@ public final class RandomTickSystem { } public void tickChunk( - RandomSource random, + BitRandomSource random, LevelChunk chunk, long tickSpeed ) { @@ -128,14 +153,30 @@ public final class RandomTickSystem { } else { this.bits += BITS_STEP; } - if ((this.cacheRandom & (TICK_MASK << bits)) == 0L) { - int count = chunk.leaf$tickingBlocksCount(); - if (count != 0L) { - long weight = (TICK_MUL * tickSpeed * count * SCALE) / CHUNK_BLOCKS; - samples.add(chunk.getPos().longKey); - weights.add(weight); - weightsSum += weight; + if ((this.cacheRandom & (TICK_MASK << bits)) != 0L) { + return; + } + if (chunk.leaf$tickingBlocksDirty) { + chunk.leaf$tickingBlocksDirty = false; + int sum = 0; + chunk.leaf$firstTickingSectionIndex = -1; + LevelChunkSection[] sections = chunk.getSections(); + for (int i = 0; i < sections.length; i++) { + LevelChunkSection section = sections[i]; + int size = section.tickingBlocks.size(); + if (size != 0 && chunk.leaf$firstTickingSectionIndex == -1) { + chunk.leaf$firstTickingSectionIndex = i; + } + sum += size; } + chunk.leaf$tickingBlocksCount = sum; + } + int count = chunk.leaf$tickingBlocksCount; + if (count != 0L) { + long weight = (TICK_MUL * tickSpeed * count * SCALE) / CHUNK_BLOCKS; + samples.add(chunk.getPos().longKey); + weights.add(weight); + weightsSum += weight; } } @@ -148,7 +189,7 @@ public final class RandomTickSystem { * * @see java.util.random.RandomGenerator#nextLong(long) nextLong(bound) */ - public static long boundedNextLong(RandomSource rng, long bound) { + public static long boundedNextLong(BitRandomSource rng, long bound) { final long m = bound - 1; long r = rng.nextLong(); if ((bound & m) == 0L) { @@ -161,4 +202,106 @@ public final class RandomTickSystem { } return r; } + + public static final class TickingBlockSet { + private static final short EMPTY = -1; + private static final short[] EMPTY_ARRAY = {}; + private static final int DEFAULT_CAP = 8; + + private short[] a = EMPTY_ARRAY; + private int size; + private int bits; + + public void clear() { + a = EMPTY_ARRAY; + size = 0; + bits = 0; + } + + /// @param n {@code n >= 0 && n <= 4096} + public boolean add(short n) { + if (a == EMPTY_ARRAY) { + a = new short[DEFAULT_CAP]; + Arrays.fill(a, EMPTY); + bits = Integer.numberOfTrailingZeros(DEFAULT_CAP); + } + return addShort(n); + } + + /// @param n {@code n >= 0 && n <= 4096} + private boolean addShort(short n) { + if (size >= a.length >>> 1) { + resize(a.length << 1); + } + int i = HashCommon.mix(n) & (a.length - 1); + int start = i; + do { + if (a[i] == n) { + return false; + } + if (a[i] == EMPTY) { + a[i] = n; + size++; + return true; + } + i = (i + 1) & (a.length - 1); + } while (i != start); + return false; + } + + /// @param n {@code n >= 0 && n <= 4096} + public boolean remove(short n) { + if (size == 0) { + return false; + } + int i = HashCommon.mix(n) & (a.length - 1); + int start = i; + do { + if (a[i] == n) { + a[i] = EMPTY; + size--; + i = (i + 1) & (a.length - 1); + while (a[i] != EMPTY) { + short rehash = a[i]; + a[i] = EMPTY; + size--; + addShort(rehash); + i = (i + 1) & (a.length - 1); + } + return true; + } + if (a[i] == EMPTY) { + return false; + } + i = (i + 1) & (a.length - 1); + } while (i != start); + return false; + } + + public short get(BitRandomSource rand) { + if (size == 0) return EMPTY; + while (true) { + int i = rand.next(bits); + if (a[i] != EMPTY) return a[i]; + } + } + + public int size() { + return size; + } + + private void resize(int cap) { + short[] o = a; + a = new short[cap]; + Arrays.fill(a, EMPTY); + size = 0; + bits = Integer.numberOfTrailingZeros(cap); + + for (short val : o) { + if (val != EMPTY) { + addShort(val); + } + } + } + } }