From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Mon, 27 Jan 2020 21:28:00 -0800
Subject: [PATCH] Optimise random block ticking

Massive performance improvement for random block ticking.
The performance increase comes from the fact that the vast
majority of attempted block ticks (~95% in my testing) fail
because the randomly selected block is not tickable.

Now only tickable blocks are targeted, however this means that
the maximum number of block ticks occurs per chunk. However,
not all chunks are going to be targeted. The percent chance
of a chunk being targeted is based on how many tickable blocks
are in the chunk.
This means that while block ticks are spread out less, the
total number of blocks ticked per world tick remains the same.
Therefore, the chance of a random tickable block being ticked
remains the same.

1.17: The IBlockDataList util class needs to be redone to support variable height limits

diff --git a/src/main/java/com/destroystokyo/paper/util/math/ThreadUnsafeRandom.java b/src/main/java/com/destroystokyo/paper/util/math/ThreadUnsafeRandom.java
new file mode 100644
index 0000000000000000000000000000000000000000..3edc8e52e06a62ce9f8cc734fd7458b37cfaad91
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/util/math/ThreadUnsafeRandom.java
@@ -0,0 +1,46 @@
+package com.destroystokyo.paper.util.math;
+
+import java.util.Random;
+
+public final class ThreadUnsafeRandom extends Random {
+
+    // See javadoc and internal comments for java.util.Random where these values come from, how they are used, and the author for them.
+    private static final long multiplier = 0x5DEECE66DL;
+    private static final long addend = 0xBL;
+    private static final long mask = (1L << 48) - 1;
+
+    private static long initialScramble(long seed) {
+        return (seed ^ multiplier) & mask;
+    }
+
+    private long seed;
+
+    @Override
+    public void setSeed(long seed) {
+        // note: called by Random constructor
+        this.seed = initialScramble(seed);
+    }
+
+    @Override
+    protected int next(int bits) {
+        // avoid the expensive CAS logic used by superclass
+        return (int) (((this.seed = this.seed * multiplier + addend) & mask) >>> (48 - bits));
+    }
+
+    // Taken from
+    // https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/
+    // https://github.com/lemire/Code-used-on-Daniel-Lemire-s-blog/blob/master/2016/06/25/fastrange.c
+    // Original license is public domain
+    public static int fastRandomBounded(final long randomInteger, final long limit) {
+        // randomInteger must be [0, pow(2, 32))
+        // limit must be [0, pow(2, 32))
+        return (int)((randomInteger * limit) >>> 32);
+    }
+
+    @Override
+    public int nextInt(int bound) {
+        // yes this breaks random's spec
+        // however there's nothing that uses this class that relies on it
+        return fastRandomBounded(this.next(32) & 0xFFFFFFFFL, bound);
+    }
+}
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
index 3b7585760483b077783a28de8d04ba438eb25c16..5f499f2d8e62fc6f28c180c857582bd6c895c98c 100644
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
@@ -673,7 +673,12 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl
         });
     }
 
-    public void tickChunk(LevelChunk chunk, int randomTickSpeed) {
+    // Paper start - optimise random block ticking
+    private final BlockPos.MutableBlockPos chunkTickMutablePosition = new BlockPos.MutableBlockPos();
+    private final com.destroystokyo.paper.util.math.ThreadUnsafeRandom randomTickRandom = new com.destroystokyo.paper.util.math.ThreadUnsafeRandom();
+    // Paper end
+
+    public void tickChunk(LevelChunk chunk, int randomTickSpeed) { final int randomTickSpeed1 = randomTickSpeed; // Paper
         ChunkPos chunkcoordintpair = chunk.getPos();
         boolean flag = this.isRaining();
         int j = chunkcoordintpair.getMinBlockX();
@@ -681,10 +686,10 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl
         ProfilerFiller gameprofilerfiller = this.getProfiler();
 
         gameprofilerfiller.push("thunder");
-        BlockPos blockposition;
+        final BlockPos.MutableBlockPos blockposition = this.chunkTickMutablePosition; // Paper - use mutable to reduce allocation rate, final to force compile fail on change
 
         if (!this.paperConfig.disableThunder && flag && this.isThundering() && this.random.nextInt(100000) == 0) { // Paper - Disable thunder
-            blockposition = this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15));
+            blockposition.set(this.findLightningTargetAround(this.getBlockRandomPos(j, 0, k, 15))); // Paper
             if (this.isRainingAt(blockposition)) {
                 DifficultyInstance difficultydamagescaler = this.getCurrentDifficultyAt(blockposition);
                 boolean flag1 = this.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && this.random.nextDouble() < (double) difficultydamagescaler.getEffectiveDifficulty() * paperConfig.skeleHorseSpawnChance && !this.getBlockState(blockposition.below()).is(Blocks.LIGHTNING_ROD); // Paper
@@ -707,66 +712,81 @@ public class ServerLevel extends net.minecraft.world.level.Level implements Worl
         }
 
         gameprofilerfiller.popPush("iceandsnow");
-        if (!this.paperConfig.disableIceAndSnow && this.random.nextInt(16) == 0) { // Paper - Disable ice and snow
-            blockposition = this.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, this.getBlockRandomPos(j, 0, k, 15));
-            BlockPos blockposition1 = blockposition.below();
+        if (!this.paperConfig.disableIceAndSnow && this.randomTickRandom.nextInt(16) == 0) { // Paper - Disable ice and snow // Paper - optimise random ticking
+            // Paper start - optimise chunk ticking
+            this.getRandomBlockPosition(j, 0, k, 15, blockposition);
+            int normalY = chunk.getHighestBlockY(Heightmap.Types.MOTION_BLOCKING, blockposition.getX() & 15, blockposition.getZ() & 15);
+            int downY = normalY - 1;
+            blockposition.setY(normalY);
+            // Paper end
             Biome biomebase = this.getBiome(blockposition);
 
-            if (biomebase.shouldFreeze((LevelReader) this, blockposition1)) {
-                org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, blockposition1, Blocks.ICE.defaultBlockState(), null); // CraftBukkit
+            // Paper start - optimise chunk ticking
+            blockposition.setY(downY);
+            if (biomebase.shouldFreeze(this, blockposition)) {
+                org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, blockposition, Blocks.ICE.defaultBlockState(), null); // CraftBukkit
+                // Paper end
             }
 
             if (flag) {
+                blockposition.setY(normalY); // Paper
                 if (biomebase.shouldSnow(this, blockposition)) {
                     org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(this, blockposition, Blocks.SNOW.defaultBlockState(), null); // CraftBukkit
                 }
 
-                BlockState iblockdata = this.getBlockState(blockposition1);
+                blockposition.setY(downY); // Paper
+                BlockState iblockdata = this.getBlockState(blockposition); // Paper
                 Biome.Precipitation biomebase_precipitation = this.getBiome(blockposition).getPrecipitation();
 
-                if (biomebase_precipitation == Biome.Precipitation.RAIN && biomebase.isColdEnoughToSnow(blockposition1)) {
+                if (biomebase_precipitation == Biome.Precipitation.RAIN && biomebase.isColdEnoughToSnow(blockposition)) { // Paper
                     biomebase_precipitation = Biome.Precipitation.SNOW;
                 }
 
-                iblockdata.getBlock().handlePrecipitation(iblockdata, (net.minecraft.world.level.Level) this, blockposition1, biomebase_precipitation);
+                iblockdata.getBlock().handlePrecipitation(iblockdata, (net.minecraft.world.level.Level) this, blockposition, biomebase_precipitation); // Paper
             }
         }
 
-        gameprofilerfiller.popPush("tickBlocks");
-        timings.chunkTicksBlocks.startTiming(); // Paper
+        // Paper start - optimise random block ticking
+        gameprofilerfiller.pop();
         if (randomTickSpeed > 0) {
-            LevelChunkSection[] achunksection = chunk.getSections();
-            int l = achunksection.length;
-
-            for (int i1 = 0; i1 < l; ++i1) {
-                LevelChunkSection chunksection = achunksection[i1];
+            gameprofilerfiller.push("randomTick");
+            timings.chunkTicksBlocks.startTiming(); // Paper
 
-                if (chunksection != LevelChunk.EMPTY_SECTION && chunksection.isRandomlyTicking()) {
-                    int j1 = chunksection.bottomBlockY();
+            LevelChunkSection[] sections = chunk.getSections();
 
-                    for (int k1 = 0; k1 < randomTickSpeed; ++k1) {
-                        BlockPos blockposition2 = this.getBlockRandomPos(j, j1, k, 15);
+            for (int sectionIndex = 0; sectionIndex < 16; ++sectionIndex) {
+                LevelChunkSection section = sections[sectionIndex];
+                if (section == null || section.tickingList.size() == 0) {
+                    continue;
+                }
 
-                        gameprofilerfiller.push("randomTick");
-                        BlockState iblockdata1 = chunksection.getBlockState(blockposition2.getX() - j, blockposition2.getY() - j1, blockposition2.getZ() - k);
+                int yPos = sectionIndex << 4;
+                for (int a = 0; a < randomTickSpeed1; ++a) {
+                    int tickingBlocks = section.tickingList.size();
+                    int index = this.randomTickRandom.nextInt(16 * 16 * 16);
+                    if (index >= tickingBlocks) {
+                        continue;
+                    }
 
-                        if (iblockdata1.isRandomlyTicking()) {
-                            iblockdata1.randomTick(this, blockposition2, this.random);
-                        }
+                    long raw = section.tickingList.getRaw(index);
+                    int location = com.destroystokyo.paper.util.maplist.IBlockDataList.getLocationFromRaw(raw);
+                    int randomX = location & 15;
+                    int randomY = ((location >>> (4 + 4)) & 255) | yPos;
+                    int randomZ = (location >>> 4) & 15;
 
-                        FluidState fluid = iblockdata1.getFluidState();
+                    BlockPos blockposition2 = blockposition.setValues(j + randomX, randomY, k + randomZ);
+                    BlockState iblockdata = com.destroystokyo.paper.util.maplist.IBlockDataList.getBlockDataFromRaw(raw);
 
-                        if (fluid.isRandomlyTicking()) {
-                            fluid.randomTick(this, blockposition2, this.random);
-                        }
+                    iblockdata.randomTick(this, blockposition2, this.randomTickRandom);
 
-                        gameprofilerfiller.pop();
-                    }
+                    // We drop the fluid tick since LAVA is ALREADY TICKED by the above method.
+                    // TODO CHECK ON UPDATE
                 }
             }
+            gameprofilerfiller.pop();
+            timings.chunkTicksBlocks.stopTiming(); // Paper
+            // Paper end
         }
-        timings.chunkTicksBlocks.stopTiming(); // Paper
-        gameprofilerfiller.pop();
     }
 
     private Optional<BlockPos> findLightningRod(BlockPos pos) {
diff --git a/src/main/java/net/minecraft/util/BitStorage.java b/src/main/java/net/minecraft/util/BitStorage.java
index 9b955a027bd2c3cbcfa659a41a6687221c5fea63..6c036335b28258cd8c268173d73707af00d12bf9 100644
--- a/src/main/java/net/minecraft/util/BitStorage.java
+++ b/src/main/java/net/minecraft/util/BitStorage.java
@@ -105,4 +105,32 @@ public class BitStorage {
         }
 
     }
+
+    // Paper start
+    public final void forEach(DataBitConsumer consumer) {
+        int i = 0;
+        long[] along = this.data;
+        int j = along.length;
+
+        for (int k = 0; k < j; ++k) {
+            long l = along[k];
+
+            for (int i1 = 0; i1 < this.valuesPerLong; ++i1) {
+                consumer.accept(i, (int) (l & this.mask));
+                l >>= this.bits;
+                ++i;
+                if (i >= this.size) {
+                    return;
+                }
+            }
+        }
+    }
+
+    @FunctionalInterface
+    public static interface DataBitConsumer {
+
+        void accept(int location, int data);
+
+    }
+    // Paper end
 }
diff --git a/src/main/java/net/minecraft/world/entity/animal/Turtle.java b/src/main/java/net/minecraft/world/entity/animal/Turtle.java
index 925f16d5eb092518ef774f69a8d99689feb0f5d7..01d8af06f19427354cac95d691e65d31253fef94 100644
--- a/src/main/java/net/minecraft/world/entity/animal/Turtle.java
+++ b/src/main/java/net/minecraft/world/entity/animal/Turtle.java
@@ -91,7 +91,7 @@ public class Turtle extends Animal {
     }
 
     public void setHomePos(BlockPos pos) {
-        this.entityData.set(Turtle.HOME_POS, pos);
+        this.entityData.set(Turtle.HOME_POS, pos.immutable()); // Paper - called with mutablepos...
     }
 
     public BlockPos getHomePos() { // Paper - public
diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
index 02c4396f3f42c1ec387eae9b2f7815f6e9f9e1c4..1e373db7080bd4fa5c62188e3ddb3e5206e9b5b1 100644
--- a/src/main/java/net/minecraft/world/level/Level.java
+++ b/src/main/java/net/minecraft/world/level/Level.java
@@ -1307,10 +1307,18 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
     public abstract TagContainer getTagManager();
 
     public BlockPos getBlockRandomPos(int x, int y, int z, int l) {
+        // Paper start - allow use of mutable pos
+        BlockPos.MutableBlockPos ret = new BlockPos.MutableBlockPos();
+        this.getRandomBlockPosition(x, y, z, l, ret);
+        return ret.immutable();
+    }
+    public final BlockPos.MutableBlockPos getRandomBlockPosition(int i, int j, int k, int l, BlockPos.MutableBlockPos out) {
+        // Paper end
         this.randValue = this.randValue * 3 + 1013904223;
         int i1 = this.randValue >> 2;
 
-        return new BlockPos(x + (i1 & 15), y + (i1 >> 16 & l), z + (i1 >> 8 & 15));
+        out.setValues(i + (i1 & 15), j + (i1 >> 16 & l), k + (i1 >> 8 & 15)); // Paper - change to setValues call
+        return out; // Paper
     }
 
     public boolean noSave() {
diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
index a088cb005525fda2c9d5521ab3bac43cfa38a393..1782be43f1dbe2776abe5087d305e271c62285dd 100644
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
@@ -568,6 +568,7 @@ public class LevelChunk implements ChunkAccess {
     @Override
     public void addEntity(Entity entity) {}
 
+    public final int getHighestBlockY(Heightmap.Types heightmap_type, int i, int j) { return this.getHeight(heightmap_type, i, j) + 1; } // Paper - sort of an obfhelper, but without -1
     @Override
     public int getHeight(Heightmap.Types type, int x, int z) {
         return ((Heightmap) this.heightmaps.get(type)).getFirstAvailable(x & 15, z & 15) - 1;
diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
index ec8b67c1b024df38d5e1ad81acff33537ae25626..739abc73020e8a41a99fa52907843efe07af9b4e 100644
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunkSection.java
@@ -14,11 +14,12 @@ public class LevelChunkSection {
     public static final int SECTION_HEIGHT = 16;
     public static final int SECTION_SIZE = 4096;
     public static final Palette<BlockState> GLOBAL_BLOCKSTATE_PALETTE = new GlobalPalette<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState());
-    private final int bottomBlockY;
+    final int bottomBlockY; // Paper - private -> package-private
     short nonEmptyBlockCount; // Paper - package-private
-    private short tickingBlockCount;
+    short tickingBlockCount; // Paper - private -> package-private
     private short tickingFluidCount;
     final PalettedContainer<BlockState> states; // Paper - package-private
+    public final com.destroystokyo.paper.util.maplist.IBlockDataList tickingList = new com.destroystokyo.paper.util.maplist.IBlockDataList(); // Paper
 
     // Paper start - Anti-Xray - Add parameters
     @Deprecated public LevelChunkSection(int yOffset) { this(yOffset, null, null, true); } // Notice for updates: Please make sure this constructor isn't used anywhere
@@ -79,6 +80,9 @@ public class LevelChunkSection {
             --this.nonEmptyBlockCount;
             if (blockState.isRandomlyTicking()) {
                 --this.tickingBlockCount;
+                // Paper start
+                this.tickingList.remove(x, y, z);
+                // Paper end
             }
         }
 
@@ -90,6 +94,9 @@ public class LevelChunkSection {
             ++this.nonEmptyBlockCount;
             if (state.isRandomlyTicking()) {
                 ++this.tickingBlockCount;
+                // Paper start
+                this.tickingList.add(x, y, z, state);
+                // Paper end
             }
         }
 
@@ -125,22 +132,28 @@ public class LevelChunkSection {
     }
 
     public void recalcBlockCounts() {
+        // Paper start
+        this.tickingList.clear();
+        // Paper end
         this.nonEmptyBlockCount = 0;
         this.tickingBlockCount = 0;
         this.tickingFluidCount = 0;
-        this.states.count((state, count) -> {
+        this.states.forEachLocation((state, location) -> { // Paper
             FluidState fluidState = state.getFluidState();
             if (!state.isAir()) {
-                this.nonEmptyBlockCount = (short)(this.nonEmptyBlockCount + count);
+                this.nonEmptyBlockCount = (short)(this.nonEmptyBlockCount + 1); // Paper
                 if (state.isRandomlyTicking()) {
-                    this.tickingBlockCount = (short)(this.tickingBlockCount + count);
+                    // Paper start
+                    this.tickingBlockCount = (short)(this.tickingBlockCount + 1);
+                    this.tickingList.add(location, state);
+                    // Paper end
                 }
             }
 
             if (!fluidState.isEmpty()) {
-                this.nonEmptyBlockCount = (short)(this.nonEmptyBlockCount + count);
+                this.nonEmptyBlockCount = (short) (this.nonEmptyBlockCount + 1); // Paper
                 if (fluidState.isRandomlyTicking()) {
-                    this.tickingFluidCount = (short)(this.tickingFluidCount + count);
+                    this.tickingFluidCount = (short) (this.tickingFluidCount + 1); // Paper
                 }
             }
 
diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
index b5b0dbbb21f15a61017d8fc936feed30c2b193dc..2bedc0f38ef16e922197a6f8e4c17aeb9cab92fd 100644
--- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
+++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java
@@ -314,6 +314,14 @@ public class PalettedContainer<T> implements PaletteResize<T> {
         });
     }
 
+    // Paper start
+    public void forEachLocation(PalettedContainer.CountConsumer<T> datapaletteblock_a) {
+        this.getDataBits().forEach((int location, int data) -> {
+            datapaletteblock_a.accept(this.getDataPalette().getObject(data), location);
+        });
+    }
+    // Paper end
+
     @FunctionalInterface
     public interface CountConsumer<T> {
         void accept(T object, int count);
