From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Spottedleaf 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. 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 67b9daee8d7e55fdf2015e6616f393f176b1ca96..ecb9c5a1958c89494417bdb3e6c6363f3fc84534 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java @@ -664,7 +664,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(); @@ -672,10 +677,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 @@ -698,66 +703,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 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 e638d982b4bd1d261a7282cad6dab98ad0b55213..e305173fd1652a8b88ae8a9b94d0fae083e2da95 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 07e81fa1119bba4981e34e70b9e67f43280f8071..5fccec12c0325dd9873905c5c3559128c3b4d9ad 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java @@ -1299,10 +1299,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 922026da8c234427e0322443004d3c32993adfce..88b053d8181d2a5abdb2c5527529a81855e1de7c 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 5fd66020a937b641e2a060cf38df731a43f3bf55..b5497272bc03a290298b5a829bdf653ac986866b 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 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 states; // Paper - package-private + public final com.destroystokyo.paper.util.maplist.IBlockDataList tickingList = new com.destroystokyo.paper.util.maplist.IBlockDataList(); // Paper public LevelChunkSection(int yOffset) { this(yOffset, (short)0, (short)0, (short)0); @@ -70,6 +71,9 @@ public class LevelChunkSection { --this.nonEmptyBlockCount; if (blockState.isRandomlyTicking()) { --this.tickingBlockCount; + // Paper start + this.tickingList.remove(x, y, z); + // Paper end } } @@ -81,6 +85,9 @@ public class LevelChunkSection { ++this.nonEmptyBlockCount; if (state.isRandomlyTicking()) { ++this.tickingBlockCount; + // Paper start + this.tickingList.add(x, y, z, state); + // Paper end } } @@ -116,22 +123,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 5ea60bbb56450502f1ceb41959239ab579458ac2..5ac948b5b82f3144cdf402af440251cb8c7369d7 100644 --- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java +++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java @@ -259,6 +259,14 @@ public class PalettedContainer implements PaletteResize { }); } + // Paper start + public void forEachLocation(PalettedContainer.CountConsumer datapaletteblock_a) { + this.getDataBits().forEach((int location, int data) -> { + datapaletteblock_a.accept(this.getDataPalette().getObject(data), location); + }); + } + // Paper end + @FunctionalInterface public interface CountConsumer { void accept(T object, int count);