From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Samsuik <40902469+Samsuik@users.noreply.github.com> Date: Thu, 16 Nov 2023 13:38:06 +0000 Subject: [PATCH] Cache Vanillia and Eigen Redstone diff --git a/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java b/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java index 9f17170179cc99d84ad25a1e838aff3d8cc66f93..a1fb1e286d66dde55682d899ded0e5d24715b642 100644 --- a/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java +++ b/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java @@ -662,6 +662,7 @@ public class RedstoneWireTurbo { // restores old behavior, at the cost of bypassing the // max-chained-neighbor-updates server property. worldIn.getBlockState(upd.self).handleNeighborChanged(worldIn, upd.self, wire, upd.parent, false); + worldIn.redstoneTracker.trackUpdate(upd.self, upd.currentState, upd.parent); // Sakura } } @@ -777,6 +778,17 @@ public class RedstoneWireTurbo { // already on-going graph walk being performed by breadthFirstWalk. return scheduleReentrantNeighborChanged(worldIn, pos, newState, source); } + + // Sakura start + int oldPower = state.getValue(RedStoneWireBlock.POWER); + int newPower = newState.getValue(RedStoneWireBlock.POWER); + if (worldIn.redstoneTracker.applyFromCache(pos, oldPower, newPower)) { + return newState; + } else { + worldIn.redstoneTracker.beginTracking(pos, oldPower, newPower); + } + // Sakura end + // If there are no on-going walks through redstone wire, then start a new walk. // If the source of the block update to the redstone wire at 'pos' is known, we can use @@ -806,6 +818,8 @@ public class RedstoneWireTurbo { // updates in a breadth first order out from the initial update received for the block at 'pos'. breadthFirstWalk(worldIn); + worldIn.redstoneTracker.endTracking(); // Sakura - we're done with the breadth walk + // With the whole search completed, clear the list of all known blocks. // We do not want to keep around state information that may be changed by other code. // In theory, we could cache the neighbor block positions, but that is a separate @@ -913,6 +927,7 @@ public class RedstoneWireTurbo { // bypass the new neighbor update stack. if (worldIn.setBlock(upd.self, state, Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS)) updateNeighborShapes(worldIn, upd.self, state); + worldIn.redstoneTracker.trackState(upd.self, state); // Sakura } } diff --git a/src/main/java/me/samsuik/sakura/redstone/RedstoneTracker.java b/src/main/java/me/samsuik/sakura/redstone/RedstoneTracker.java new file mode 100644 index 0000000000000000000000000000000000000000..4f5af6e241b0194ed982144fd9320315f68d7b00 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/redstone/RedstoneTracker.java @@ -0,0 +1,285 @@ +package me.samsuik.sakura.redstone; + +import it.unimi.dsi.fastutil.HashCommon; +import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectMaps; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import me.samsuik.sakura.utils.objects.Expiry; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.*; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.redstone.InstantNeighborUpdater; +import net.minecraft.world.level.redstone.NeighborUpdater; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +public final class RedstoneTracker { + + private static final int DEPTH_LIMIT = 512; + + // this is a fucking horrible hack, you could make a method in block + // then override it but that is far more work than I am willing right now. + private static final Predicate TECHNICAL_BLOCK = (block) -> { + return block instanceof BaseEntityBlock + || block instanceof DirectionalBlock + || block instanceof HorizontalDirectionalBlock + || block instanceof GameMasterBlock + || block instanceof BaseRailBlock // rails themselves don't require it but it's variants do + || block instanceof NoteBlock + || block instanceof DoorBlock + || block instanceof RedstoneLampBlock + || block instanceof RedstoneTorchBlock + || block instanceof RedStoneWireBlock; // required to update any nearby redstone that isn't the line currently being tested + }; + + private final Level level; + private final InstantNeighborUpdater updater; + private final Long2ObjectOpenHashMap wireCaches = new Long2ObjectOpenHashMap<>(); + // important locations, wires and adjacent + private final Long2ObjectMap> adjacent = new Long2ObjectOpenHashMap<>(); + // last block of physics + private final Long2ObjectMap> updates = new Long2ObjectOpenHashMap<>(); + + private ConnectedWires tracking; + private int depth = 0; + + public RedstoneTracker(Level level) { + this.level = level; + this.updater = new InstantNeighborUpdater(level); + } + + private static long cacheKey(BlockPos position, int previous, int next) { + int powerState = 1 + previous + (next * 31); // previous and next only go up to 15 + long powerHash = HashCommon.murmurHash3((long) powerState); // long for a better hash + return position.asLong() ^ powerHash; + } + + private boolean stillValid(BlockPos position, BlockState previous, BlockState next) { + return previous.getBlock() == next.getBlock() || previous.isSignalSource() == next.isSignalSource() + && previous.isRedstoneConductor(this.level, position) == next.isRedstoneConductor(this.level, position); + } + + public void updateNeighbors(BlockPos position, Block self) { + this.updater.updateNeighborsAtExceptFromFacing(position, self, null); + } + + public boolean applyFromCache(BlockPos position, int previous, int next) { + if (this.level.localConfig().config(position).redstoneCache && this.depth == 0) { + long key = cacheKey(position, previous, next); + RedstoneCache cache = this.wireCaches.get(key); + + if (cache != null && !cache.expiry().isExpired(MinecraftServer.currentTickLong)) { + cache.apply(this.level); + return true; + } + } + + return false; + } + + public void invalidate(BlockPos position, BlockState previous, BlockState next) { + if (!this.stillValid(position, previous, next)) { + long packed = position.asLong(); + this.invalidate(this.adjacent.get(packed)); + + if (TECHNICAL_BLOCK.test(next.getBlock())) { + this.invalidate(this.updates.get(packed)); + } + } + } + + private void invalidate(List caches) { + if (caches != null) { + caches.forEach(cache -> cache.expiry().refresh(-200)); + } + } + + public void trackState(BlockPos position, BlockState state) { + if (this.tracking != null) { + this.tracking.wireUpdate(position, state); + } + } + + public void trackUpdates(BlockPos position, Block block) { + if (this.tracking != null) { + this.tracking.updateNeighbors(position, block); + } + } + + public void trackUpdate(BlockPos position, BlockState state, BlockPos parent) { + if (this.tracking != null) { + this.tracking.updates().put(position.asLong(), new UpdateState(state.getBlock(), parent)); + } + } + + public boolean isTracked(BlockPos position) { + List wires = this.adjacent.get(position.asLong()); + return wires != null && wires.stream().anyMatch(cache -> !cache.expiry().isExpired(MinecraftServer.currentTickLong)); + } + + public boolean isTracking() { + return this.tracking != null && this.depth != 0 && this.depth < DEPTH_LIMIT; + } + + public void beginTracking(BlockPos position, int previous, int next) { + if (this.level.localConfig().config(position).redstoneCache && ++this.depth == 1) { + long key = cacheKey(position, previous, next); + + if (this.wireCaches.containsKey(key)) { + return; // cache already exists + } + + this.tracking = new ConnectedWires( + position, key, + new Object2ObjectOpenHashMap<>(), + new Long2ObjectLinkedOpenHashMap<>() + ); + } + } + + public void endTracking() { + if (this.depth > 0 && --this.depth == 0 && this.tracking != null) { + this.createRedstoneCache(); + this.tracking = null; + } + } + + private void createRedstoneCache() { + ConnectedWires connected = this.tracking; + List updates = new ArrayList<>(connected.updates().size() / 2); + + Long2ObjectMaps.fastForEach(connected.updates(), entry -> { + BlockPos position = BlockPos.of(entry.getLongKey()); + UpdateState update = entry.getValue(); + + // do not update wires that are connected + if (connected.wires().containsKey(position)) return; + + // filter out blocks that do not need updates + BlockState state = this.level.getBlockState(position); + if (TECHNICAL_BLOCK.test(state.getBlock())) { + updates.add(new RedstoneUpdate(position, state, update)); + } + }); + + List wires = new ArrayList<>(connected.wires().size()); + + connected.wires().forEach((pos, state) -> { + // is it even worth taking the hit here + boolean shapeUpdate = updates.stream() + .filter(update -> update.state().is(Blocks.OBSERVER)) + .anyMatch(update -> pos.distManhattan(update.position()) == 1); + + wires.add(new RedstoneWire(pos, state, shapeUpdate)); + }); + + RedstoneCache redstoneCache = new RedstoneCache( + wires, updates, connected, + new Expiry(MinecraftServer.currentTickLong, 200) + ); + + // put the newly created redstone cache into use + this.wireCaches.put(connected.key(), redstoneCache); + + // used for cache invalidation + connected.updates().keySet().forEach(update -> { + BlockPos position = BlockPos.of(update); + + if (wires.stream().anyMatch(wire -> wire.position().distManhattan(position) == 1)) { + this.adjacent.computeIfAbsent(update, key -> new ArrayList<>(1)) + .add(redstoneCache); + } else { + this.updates.computeIfAbsent(update, key -> new ArrayList<>(1)) + .add(redstoneCache); + } + }); + + wires.forEach(wire -> { + this.adjacent.computeIfAbsent(wire.position().asLong(), key -> new ArrayList<>(1)) + .add(redstoneCache); + }); + } + + public void expire(long tick) { + if (tick % 100 != 0) return; + + this.wireCaches.values().removeIf(cache -> { + if (cache.expiry().isExpired(tick)) { + removeExpiredCache(cache); + return true; + } + + return false; + }); + } + + private void removeExpiredCache(RedstoneCache cache) { + cache.connected().updates().keySet().forEach(update -> { + this.removeFromCaches(update, cache); + }); + + cache.connected().wires().keySet().forEach(wire -> { + this.removeFromCaches(wire.asLong(), cache); + }); + } + + private void removeFromCaches(long packed, RedstoneCache cache) { + this.removeCache(this.adjacent, packed, cache); + this.removeCache(this.updates, packed, cache); + } + + private void removeCache(Long2ObjectMap> locationMap, long packed, RedstoneCache cache) { + List caches = locationMap.get(packed); + + // in case something goes wrong + if (caches == null) return; + + caches.remove(cache); + + if (caches.isEmpty()) { + locationMap.remove(packed); + } + } + + private record UpdateState(Block block, BlockPos position) {} + private record RedstoneWire(BlockPos position, BlockState state, boolean shapeUpdate) {} + private record RedstoneUpdate(BlockPos position, BlockState state, UpdateState parent) {} + + private record RedstoneCache(List wires, List updates, ConnectedWires connected, Expiry expiry) { + public void apply(Level level) { + // Apply cached wire changes + wires.forEach(wire -> level.setBlock(wire.position(), wire.state(), wire.shapeUpdate() ? 2 : 18)); + + // Now update the neighbors. We have to do this after applying wire changes + // in case the block we're affecting here, checks the surrounding power level. + updates.forEach(update -> { + level.neighborChanged(update.position(), update.parent().block(), update.parent().position()); + }); + + // Refresh so the cache won't expire + expiry.refresh(MinecraftServer.currentTickLong); + } + } + + private record ConnectedWires(BlockPos source, long key, Object2ObjectMap wires, Long2ObjectMap updates) { + public void updateNeighbors(BlockPos position, Block block) { + for (Direction direction : NeighborUpdater.UPDATE_ORDER) { + BlockPos neighbor = position.relative(direction); + this.updates.put(neighbor.asLong(), new UpdateState(block, position)); + } + } + + public void wireUpdate(BlockPos position, BlockState state) { + this.wires.put(position, state); + } + } + +} diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java index 545dee7863095b70474abcc87c9c9c0f15a016f8..a826251f1d5f8ad670ee824bfcd093e6ab281bed 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -1820,6 +1820,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop resourcekey, RegistryAccess iregistrycustom, Holder holder, Supplier supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function paperWorldConfigCreator, Supplier sakuraWorldConfigCreator, java.util.concurrent.Executor executor) { // Sakura - sakura configuration files// Paper - create paper world config & Anti-Xray this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot @@ -1036,6 +1037,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl } else { BlockState iblockdata2 = this.getBlockState(pos); + this.redstoneTracker.invalidate(pos, iblockdata1, state); // Sakura /* if (iblockdata2 == iblockdata) { if (iblockdata1 != iblockdata2) { diff --git a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java index c131734cad123a35456d18f8a161f77a4ac9ac99..88ddd1747d9786210e8faf412b3b0363df4bab43 100644 --- a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java +++ b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java @@ -381,7 +381,15 @@ public class RedStoneWireBlock extends Block { } if (oldPower != i) { // CraftBukkit end + // Sakura start + if (world.redstoneTracker.applyFromCache(pos, oldPower, i)) { + return; + } + + world.redstoneTracker.beginTracking(pos, oldPower, i); if (world.getBlockState(pos) == state) { + world.redstoneTracker.trackState(pos, state.setValue(RedStoneWireBlock.POWER, i)); + // Sakura end world.setBlock(pos, (BlockState) state.setValue(RedStoneWireBlock.POWER, i), 2); } @@ -402,8 +410,17 @@ public class RedStoneWireBlock extends Block { while (iterator.hasNext()) { BlockPos blockposition1 = (BlockPos) iterator.next(); - world.updateNeighborsAt(blockposition1, this); + // Sakura start + world.redstoneTracker.trackUpdates(blockposition1, this); + if (world.redstoneTracker.isTracking()) { + world.redstoneTracker.updateNeighbors(blockposition1, this); + } else { + world.updateNeighborsAt(blockposition1, this); + } + // Sakura end } + + world.redstoneTracker.endTracking(); // Sakura } } @@ -559,7 +576,22 @@ public class RedStoneWireBlock extends Block { if (this.shouldSignal && direction != Direction.DOWN) { int i = (Integer) state.getValue(RedStoneWireBlock.POWER); - return i == 0 ? 0 : (direction != Direction.UP && !((RedstoneSide) this.getConnectionState(world, state, pos).getValue((Property) RedStoneWireBlock.PROPERTY_BY_DIRECTION.get(direction.getOpposite()))).isConnected() ? 0 : i); + // Sakura start - use redstone cache to avoid recalculating wires + if (i == 0) { + return 0; + } else if (direction == Direction.UP) { + return i; + } else { + final BlockState wireState; + if (world instanceof Level level && level.redstoneTracker.isTracked(pos)) { + wireState = state; // cached wire + } else { + wireState = this.getConnectionState(world, state, pos); + } + return !((RedstoneSide) wireState.getValue((Property) RedStoneWireBlock.PROPERTY_BY_DIRECTION.get(direction.getOpposite()))).isConnected() ? 0 : i; + } + //return i == 0 ? 0 : (direction != Direction.UP && !((RedstoneSide) this.getConnectionState(world, state, pos).getValue((Property) RedStoneWireBlock.PROPERTY_BY_DIRECTION.get(direction.getOpposite()))).isConnected() ? 0 : i); + // Sakura end - use redstone cache to avoid recalculating wires } else { return 0; }