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 fa062e6543e8a0377e3d4715996955dba005ee80..ca8b5066e2db3dc5df97091fa8821493d91949da 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).neighborChanged(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..c4150b062ee1a15f1938fea2da926699edb804a9 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/redstone/RedstoneTracker.java @@ -0,0 +1,283 @@ +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) { + return position.asLong() ^ HashCommon.mix(previous + next * 31); + } + + 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.sakuraConfig().technical.redstone.redstoneCache) { + long key = cacheKey(position, previous, next); + RedstoneCache cache = this.wireCaches.get(key); + + if (this.depth == 0 && cache != null && !cache.expiry().isExpired(MinecraftServer.currentTickLong)) { + cache.apply(level); + return true; + } + } + + return false; + } + + public void invalidate(BlockPos position, BlockState previous, BlockState next) { + if (!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 isTracking() { + return this.depth != 0 && this.depth < DEPTH_LIMIT; + } + + public void beginTracking(BlockPos position, int previous, int next) { + if (++this.depth == 1 && this.level.sakuraConfig().technical.redstone.redstoneCache) { + long key = cacheKey(position, previous, next); + + if (this.wireCaches.containsKey(key)) { + // cache already exists + return; + } + + this.tracking = new ConnectedWires( + position, key, + new Object2ObjectOpenHashMap<>(), + new Long2ObjectLinkedOpenHashMap<>() + ); + } + } + + public void endTracking() { + if (--this.depth == 0) { + if (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 622ab8f82f1b641f32912788ce44381b88f46093..4d16f579d7c73e2374230df519ca3f56c7c14dd5 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -1751,6 +1751,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop predicate, List into, int limit, int search) { @@ -1009,6 +1010,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable { } 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 b5a71fd4e2f55bf036c2c697da5d50cc90fc657c..93df0a39de85a5fa4b0ab680954405171646915d 100644 --- a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java +++ b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java @@ -382,7 +382,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); } @@ -403,8 +411,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 } }