diff --git a/divinemc-server/minecraft-patches/features/0052-Regionized-Chunk-Ticking.patch b/divinemc-server/minecraft-patches/features/0052-Regionized-Chunk-Ticking.patch index 4873d06..9e0171b 100644 --- a/divinemc-server/minecraft-patches/features/0052-Regionized-Chunk-Ticking.patch +++ b/divinemc-server/minecraft-patches/features/0052-Regionized-Chunk-Ticking.patch @@ -8,7 +8,7 @@ This patch adds regionized chunk ticking feature, by grouping adjacent chunks in Original idea by Dueris, modified by NONPLAYT and heavily optimized by dan28000 diff --git a/net/minecraft/network/Connection.java b/net/minecraft/network/Connection.java -index 3f85f9e9551b2eed6e66ab8036dbb1f40fb8bbac..2da4ba00963c9ff6715fe60aa2f6af724fa8ed61 100644 +index 411e1284a208ca1a097cf6eaa92e1e0d2203d83d..3f60d1b0ac91cfd3418e791222cd7267774b367a 100644 --- a/net/minecraft/network/Connection.java +++ b/net/minecraft/network/Connection.java @@ -327,7 +327,7 @@ public class Connection extends SimpleChannelInboundHandler> { @@ -20,19 +20,33 @@ index 3f85f9e9551b2eed6e66ab8036dbb1f40fb8bbac..2da4ba00963c9ff6715fe60aa2f6af72 } catch (Exception var2) { if (var2 instanceof ClosedChannelException) { LOGGER.info("Connection closed during protocol change"); +diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java +index 7ca147cf9da67c399806056e5092841f7ca32321..81a174111dc2ebc9df428a67936c302ae9ec95e3 100644 +--- a/net/minecraft/server/level/ChunkMap.java ++++ b/net/minecraft/server/level/ChunkMap.java +@@ -730,7 +730,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + void collectSpawningChunks(List output) { + final ca.spottedleaf.moonrise.common.list.ReferenceList tickingChunks = ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel)this.level).moonrise$getPlayerTickingChunks(); + +- final LevelChunk[] raw = tickingChunks.getRawDataUnchecked(); ++ final LevelChunk[] raw = tickingChunks.toArray(new LevelChunk[0]); // DivineMC - Regionized Chunk Ticking - sync fix + final int size = tickingChunks.size(); + + Objects.checkFromToIndex(0, size, raw.length); diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java -index bfc200f39a22664204b5aa66d3911abdb368e563..50daa39747a0f07c4d31a13c4410819a82d5f076 100644 +index 2039e636b1a52aff5403621e7281d618e4b87864..855ec5c636b9f8ca504425dafd49c4a71b1e2456 100644 --- a/net/minecraft/server/level/ServerChunkCache.java +++ b/net/minecraft/server/level/ServerChunkCache.java -@@ -58,6 +58,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon +@@ -56,7 +56,7 @@ import org.slf4j.Logger; + public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemServerChunkCache { // Paper - rewrite chunk system private static final Logger LOGGER = LogUtils.getLogger(); private final DistanceManager distanceManager; - private final ServerLevel level; -+ public static final Executor REGION_EXECUTOR = java.util.concurrent.Executors.newFixedThreadPool(org.bxteam.divinemc.config.DivineConfig.AsyncCategory.regionizedChunkTickingExecutorThreadCount, new org.bxteam.divinemc.util.NamedAgnosticThreadFactory<>("Region Ticking", ca.spottedleaf.moonrise.common.util.TickThread::new, org.bxteam.divinemc.config.DivineConfig.AsyncCategory.regionizedChunkTickingExecutorThreadPriority)); // DivineMC - Regionized Chunk Ticking +- private final ServerLevel level; ++ protected final ServerLevel level; // DivineMC - Regionized Chunk Ticking - private -> protected public final Thread mainThread; final ThreadedLevelLightEngine lightEngine; public final ServerChunkCache.MainThreadExecutor mainThreadProcessor; -@@ -71,8 +72,10 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon +@@ -70,8 +70,10 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon private final long[] lastChunkPos = new long[4]; private final ChunkStatus[] lastChunkStatus = new ChunkStatus[4]; private final ChunkAccess[] lastChunk = new ChunkAccess[4]; @@ -45,141 +59,12 @@ index bfc200f39a22664204b5aa66d3911abdb368e563..50daa39747a0f07c4d31a13c4410819a @Nullable @VisibleForDebug private NaturalSpawner.SpawnState lastSpawnState; -@@ -154,36 +157,253 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon - return load ? this.syncLoad(chunkX, chunkZ, toStatus) : null; - } +@@ -155,34 +157,46 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon // Paper end - rewrite chunk system -+ -+ // DivineMC start - Regionized Chunk Ticking -+ private record RegionData(it.unimi.dsi.fastutil.longs.LongOpenHashSet chunks, Set entities) { -+ public boolean isEmpty() { -+ return chunks.isEmpty(); -+ } -+ } -+ -+ private record Output(RegionData[] regions, Set entities) {} -+ -+ private Output computePlayerRegionsParallel() { -+ int tickViewDistance = level.moonrise$getViewDistanceHolder().getViewDistances().tickViewDistance(); -+ List players = new java.util.ArrayList<>(level.players); -+ int max = maxChunksForViewDistance(); -+ -+ List playerChunkSets = players.parallelStream() -+ .map(player -> { -+ ChunkPos playerChunk = player.chunkPosition(); -+ int px = playerChunk.x; -+ int pz = playerChunk.z; -+ it.unimi.dsi.fastutil.longs.LongOpenHashSet chunkKeys = new it.unimi.dsi.fastutil.longs.LongOpenHashSet(max); -+ for (int dx = -tickViewDistance; dx <= tickViewDistance; dx++) { -+ for (int dz = -tickViewDistance; dz <= tickViewDistance; dz++) { -+ long key = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(px + dx, pz + dz); -+ chunkKeys.add(key); -+ } -+ } -+ return chunkKeys; -+ }).toList(); -+ -+ List mergedRegions = new java.util.ArrayList<>(); -+ boolean[] merged = new boolean[playerChunkSets.size()]; -+ -+ for (int i = 0; i < playerChunkSets.size(); i++) { -+ if (merged[i]) continue; -+ -+ it.unimi.dsi.fastutil.longs.LongOpenHashSet region = new it.unimi.dsi.fastutil.longs.LongOpenHashSet(playerChunkSets.get(i)); -+ merged[i] = true; -+ -+ boolean madeChanges; -+ do { -+ madeChanges = false; -+ for (int j = i + 1; j < playerChunkSets.size(); j++) { -+ if (merged[j]) continue; -+ -+ it.unimi.dsi.fastutil.longs.LongOpenHashSet set = playerChunkSets.get(j); -+ -+ boolean hasIntersection = false; -+ it.unimi.dsi.fastutil.longs.LongIterator iter = set.iterator(); -+ while (iter.hasNext()) { -+ if (region.contains(iter.nextLong())) { -+ hasIntersection = true; -+ break; -+ } -+ } -+ -+ if (hasIntersection) { -+ region.addAll(set); -+ merged[j] = true; -+ madeChanges = true; -+ } -+ } -+ } while (madeChanges); -+ -+ mergedRegions.add(region); -+ } -+ -+ ObjectArrayList regions = new ObjectArrayList<>(); -+ it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap chunkToRegion = new it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap(max * mergedRegions.size()); -+ chunkToRegion.defaultReturnValue(-1); -+ for (int i = 0; i < mergedRegions.size(); i++) { -+ regions.add(new RegionData(mergedRegions.get(i), java.util.Collections.newSetFromMap(new java.util.concurrent.ConcurrentHashMap<>()))); -+ for (long key : mergedRegions.get(i)) { -+ chunkToRegion.put(key, i); -+ } -+ } -+ -+ final Set firstTick = java.util.Collections.newSetFromMap(new java.util.concurrent.ConcurrentHashMap<>()); -+ -+ synchronized (level.entityTickList.entities) { -+ final ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet.Iterator iterator = level.entityTickList.entities.iterator(); -+ try { -+ while (iterator.hasNext()) { -+ Entity entity = iterator.next(); -+ long chunkKey = entity.chunkPosition().longKey; -+ int regionIndex = chunkToRegion.get(chunkKey); -+ if (regionIndex != -1) { -+ regions.get(regionIndex).entities().add(entity); -+ } else { -+ firstTick.add(entity); -+ } -+ } -+ } finally { -+ iterator.finishedIterating(); -+ } -+ } -+ -+ return new Output(regions.toArray(new RegionData[0]), firstTick); -+ } -+ -+ // Should be max safe estimate of ticking chunks in a region -+ private int maxChunksForViewDistance() { -+ ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistances distances = level.moonrise$getViewDistanceHolder().getViewDistances(); -+ int diameter = 2 * distances.tickViewDistance() + 1; -+ return diameter * diameter; -+ } -+ -+ private void tickEntity(Entity entity) { -+ if (!entity.isRemoved()) { -+ if (!level.tickRateManager().isEntityFrozen(entity)) { -+ entity.checkDespawn(); -+ // Paper - rewrite chunk system -+ Entity vehicle = entity.getVehicle(); -+ if (vehicle != null) { -+ if (!vehicle.isRemoved() && vehicle.hasPassenger(entity)) { -+ return; -+ } -+ -+ entity.stopRiding(); -+ } -+ -+ level.guardEntityTick(level::tickNonPassenger, entity); -+ } -+ } -+ } -+ // DivineMC end - Regionized Chunk Ticking -+ // Paper start - chunk tick iteration optimisations private final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom shuffleRandom = new ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom(0L); - private void iterateTickingChunksFaster() { -+ private void iterateTickingChunksFaster(final java.util.concurrent.CompletableFuture spawns) { // DivineMC - Regionized Chunk Ticking ++ private void iterateTickingChunksFaster(final CompletableFuture spawns) { // DivineMC - Regionized Chunk Ticking final ServerLevel world = this.level; final int randomTickSpeed = world.getGameRules().getInt(GameRules.RULE_RANDOMTICKING); @@ -198,98 +83,17 @@ index bfc200f39a22664204b5aa66d3911abdb368e563..50daa39747a0f07c4d31a13c4410819a - java.util.Objects.checkFromToIndex(0, size, raw.length); - for (int i = 0; i < size; ++i) { - world.tickChunk(raw[i], randomTickSpeed); -+ // DivineMC start - Regionized Chunk Ticking -+ if (org.bxteam.divinemc.config.DivineConfig.AsyncCategory.enableRegionizedChunkTicking) { -+ final Output output = computePlayerRegionsParallel(); -+ final RegionData[] regions = output.regions(); -+ int regionCount = regions.length; -+ -+ java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(regionCount); -+ io.papermc.paper.entity.activation.ActivationRange.activateEntities(level); // Paper - EAR -+ -+ try { -+ java.util.concurrent.ForkJoinPool.managedBlock(new java.util.concurrent.ForkJoinPool.ManagedBlocker() { -+ @Override -+ public boolean block() throws InterruptedException { -+ ObjectArrayList> ticked = new ObjectArrayList<>(regionCount); -+ for (final RegionData region : regions) { -+ if (region == null || region.isEmpty()) { -+ latch.countDown(); -+ continue; -+ } -+ -+ ticked.add(java.util.concurrent.CompletableFuture.supplyAsync(() -> { -+ ObjectArrayList regionChunks = new ObjectArrayList<>(region.chunks().size()); -+ it.unimi.dsi.fastutil.longs.LongOpenHashSet regionChunksIDs = new it.unimi.dsi.fastutil.longs.LongOpenHashSet(region.chunks().size()); -+ for (long key : region.chunks()) { -+ LevelChunk chunk = fullChunks.get(key); -+ if (chunk != null) { -+ regionChunks.add(chunk); -+ regionChunksIDs.add(key); -+ } -+ } -+ -+ try { -+ for (LevelChunk chunk : regionChunks) { -+ world.tickChunk(chunk, randomTickSpeed); -+ } -+ for (net.minecraft.world.entity.Entity entity : region.entities()) { -+ tickEntity(entity); -+ } -+ } finally { -+ latch.countDown(); -+ } -+ return regionChunksIDs; -+ }, REGION_EXECUTOR)); -+ } - +- - // call mid-tick tasks for chunk system - if ((i & 7) == 0) { - // DivineMC start - Parallel world ticking - if (!org.bxteam.divinemc.config.DivineConfig.AsyncCategory.enableParallelWorldTicking) { - ((ca.spottedleaf.moonrise.patches.chunk_system.server.ChunkSystemMinecraftServer) this.level.getServer()).moonrise$executeMidTickTasks(); - continue; -+ CompletableFuture.runAsync(() -> { -+ try { -+ CompletableFuture.allOf(ticked.toArray(new CompletableFuture[0])).join(); -+ } catch (java.util.concurrent.CompletionException ex) { -+ LOGGER.error("Error during region chunk ticking", ex.getCause()); -+ } -+ -+ it.unimi.dsi.fastutil.longs.LongOpenHashSet tickedChunkKeys = new it.unimi.dsi.fastutil.longs.LongOpenHashSet(raw.length); -+ -+ for (CompletableFuture future : ticked) { -+ if (!future.isCompletedExceptionally()) { -+ try { -+ it.unimi.dsi.fastutil.longs.LongOpenHashSet regionChunks = future.join(); -+ tickedChunkKeys.addAll(regionChunks); -+ } catch (Exception e) { -+ LOGGER.error("Exception in region ticking future", e); -+ } -+ } -+ } -+ -+ for (LevelChunk chunk : raw) { -+ if (!tickedChunkKeys.contains(chunk.coordinateKey)) { -+ world.tickChunk(chunk, randomTickSpeed); -+ } -+ } -+ -+ output.entities.forEach(ServerChunkCache.this::tickEntity); -+ spawns.join(); -+ }, REGION_EXECUTOR).join(); -+ -+ latch.await(); -+ return true; -+ } -+ -+ @Override -+ public boolean isReleasable() { -+ return latch.getCount() == 0; -+ } -+ }); -+ } catch (InterruptedException ex) { -+ throw new RuntimeException("Interrupted managed block during region ticking", ex); ++ // DivineMC start - Regionized Chunk Ticking ++ if (org.bxteam.divinemc.config.DivineConfig.AsyncCategory.enableRegionizedChunkTicking) { ++ if (this instanceof org.bxteam.divinemc.async.rct.RegionizedChunkTicking rct) { ++ rct.execute(spawns, raw); + } + } else { + java.util.Objects.checkFromToIndex(0, size, raw.length); @@ -306,13 +110,17 @@ index bfc200f39a22664204b5aa66d3911abdb368e563..50daa39747a0f07c4d31a13c4410819a + // DivineMC end - Parallel world ticking } - // DivineMC end - Parallel world ticking ++ } ++ ++ if (org.bxteam.divinemc.config.DivineConfig.AsyncCategory.asyncNaturalSpawn) { ++ spawns.join(); } } + // DivineMC end - Regionized Chunk Ticking } // Paper end - chunk tick iteration optimisations -@@ -502,14 +722,21 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon +@@ -507,14 +521,21 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon long gameTime = this.level.getGameTime(); long l = gameTime - this.lastInhabitedUpdate; this.lastInhabitedUpdate = gameTime; @@ -338,7 +146,7 @@ index bfc200f39a22664204b5aa66d3911abdb368e563..50daa39747a0f07c4d31a13c4410819a // DivineMC start - Pufferfish: Optimize mob spawning if (org.bxteam.divinemc.config.DivineConfig.AsyncCategory.enableAsyncSpawning) { for (ServerPlayer player : this.level.players) { -@@ -553,14 +780,18 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon +@@ -558,14 +579,18 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon } private void broadcastChangedChunks() { @@ -363,54 +171,53 @@ index bfc200f39a22664204b5aa66d3911abdb368e563..50daa39747a0f07c4d31a13c4410819a } private void tickChunks(long timeInhabited) { -@@ -610,23 +841,28 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon +@@ -615,6 +640,24 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon filteredSpawningCategories = List.of(); } -- List list = this.spawningChunks; + // DivineMC start - Regionized Chunk Ticking -+ final java.util.concurrent.CompletableFuture spawns = java.util.concurrent.CompletableFuture.runAsync(() -> { -+ List list = this.spawningChunks; - -- try { -- this.chunkMap.collectSpawningChunks(list); -- // Paper start - chunk tick iteration optimisation -- this.shuffleRandom.setSeed(this.level.random.nextLong()); -- if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) Util.shuffle(list, this.shuffleRandom); // Paper - Optional per player mob spawns; do not need this when per-player is enabled -- // Paper end - chunk tick iteration optimisation -- -- for (LevelChunk levelChunk : list) { -- this.tickSpawningChunk(levelChunk, timeInhabited, filteredSpawningCategories, lastSpawnState); // DivineMC - Pufferfish: Optimize mob spawning -+ try { -+ this.chunkMap.collectSpawningChunks(list); -+ // Paper start - chunk tick iteration optimisation -+ this.shuffleRandom.setSeed(this.level.random.nextLong()); -+ if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) -+ Util.shuffle(list, this.shuffleRandom); // Paper - Optional per player mob spawns; do not need this when per-player is enabled -+ // Paper end - chunk tick iteration optimisation -+ -+ for (LevelChunk levelChunk : list) { -+ this.tickSpawningChunk(levelChunk, timeInhabited, filteredSpawningCategories, lastSpawnState); // DivineMC - Pufferfish: Optimize mob spawning -+ } -+ } finally { -+ list.clear(); - } -- } finally { -- list.clear(); -- } -+ }, REGION_EXECUTOR); ++ final CompletableFuture spawns; ++ if (org.bxteam.divinemc.config.DivineConfig.AsyncCategory.asyncNaturalSpawn) { ++ spawns = CompletableFuture.runAsync(() -> naturalSpawn(filteredSpawningCategories, timeInhabited), org.bxteam.divinemc.async.rct.RegionizedChunkTicking.REGION_EXECUTOR); ++ } else { ++ naturalSpawn(filteredSpawningCategories, timeInhabited); ++ spawns = new CompletableFuture<>(); ++ } + // DivineMC end - Regionized Chunk Ticking ++ ++ this.iterateTickingChunksFaster(spawns); // Paper - chunk tick iteration optimisations // DivineMC - Regionized Chunk Ticking ++ if (_boolean) { ++ this.level.tickCustomSpawners(this.spawnEnemies, this.spawnFriendlies); ++ } ++ } ++ ++ // DivineMC start - Regionized Chunk Ticking ++ private void naturalSpawn(List filteredSpawningCategories, long timeInhabited) { + List list = this.spawningChunks; + + try { +@@ -630,12 +673,12 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + } finally { + list.clear(); + } ++ } - this.iterateTickingChunksFaster(); // Paper - chunk tick iteration optimisations -+ this.iterateTickingChunksFaster(spawns); // Paper - chunk tick iteration optimisations // DivineMC - Regionized Chunk Ticking - if (_boolean) { - this.level.tickCustomSpawners(this.spawnEnemies); - } +- if (_boolean) { +- this.level.tickCustomSpawners(this.spawnEnemies, this.spawnFriendlies); +- } ++ protected net.minecraft.world.level.entity.EntityTickList getEntityTickList() { ++ return level.entityTickList; + } ++ // DivineMC end - Regionized Chunk Ticking + + private void tickSpawningChunk(LevelChunk chunk, long timeInhabited, List spawnCategories, NaturalSpawner.SpawnState spawnState) { + ChunkPos pos = chunk.getPos(); diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index d27a130f80cb8225a30b289b9d06c5c508a55ea7..f4399659d084ed9703052b3ce2c82a9e198cd514 100644 +index f9091b2daf735fd0788f8d6d60e3c812fd6dd4f2..6c59d11107958f835ebe09317ed0d129f64d4583 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java -@@ -197,7 +197,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -191,7 +191,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe private final LevelTicks blockTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded); private final LevelTicks fluidTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded); private final PathTypeCache pathTypesByPosCache = new PathTypeCache(); @@ -419,7 +226,59 @@ index d27a130f80cb8225a30b289b9d06c5c508a55ea7..f4399659d084ed9703052b3ce2c82a9e volatile boolean isUpdatingNavigations; protected final Raids raids; private final ObjectLinkedOpenHashSet blockEvents = new ObjectLinkedOpenHashSet<>(); -@@ -834,6 +834,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -640,20 +640,37 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + boolean flag = server.forceSynchronousWrites(); + DataFixer fixerUpper = server.getFixerUpper(); + // Paper - rewrite chunk system +- this.chunkSource = new ServerChunkCache( +- this, +- levelStorageAccess, +- fixerUpper, +- server.getStructureManager(), +- dispatcher, +- chunkGenerator, +- this.spigotConfig.viewDistance, // Spigot +- this.spigotConfig.simulationDistance, // Spigot +- flag, +- progressListener, +- null, // Paper - rewrite chunk system +- () -> server.overworld().getDataStorage() +- ); ++ if (org.bxteam.divinemc.config.DivineConfig.AsyncCategory.enableRegionizedChunkTicking) { ++ this.chunkSource = new org.bxteam.divinemc.async.rct.RegionizedChunkTicking(this, ++ levelStorageAccess, ++ fixerUpper, ++ server.getStructureManager(), ++ dispatcher, ++ chunkGenerator, ++ this.spigotConfig.viewDistance, // Spigot ++ this.spigotConfig.simulationDistance, // Spigot ++ flag, ++ progressListener, ++ null, // Paper - rewrite chunk system ++ () -> server.overworld().getDataStorage() ++ ); ++ } else { ++ this.chunkSource = new ServerChunkCache( ++ this, ++ levelStorageAccess, ++ fixerUpper, ++ server.getStructureManager(), ++ dispatcher, ++ chunkGenerator, ++ this.spigotConfig.viewDistance, // Spigot ++ this.spigotConfig.simulationDistance, // Spigot ++ flag, ++ progressListener, ++ null, // Paper - rewrite chunk system ++ () -> server.overworld().getDataStorage() ++ ); ++ } ++ + this.chunkSource.getGeneratorState().ensureStructuresGenerated(); + this.portalForcer = new PortalForcer(this); + this.updateSkyBrightness(); +@@ -806,6 +823,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe this.dragonFight.tick(); } @@ -433,7 +292,7 @@ index d27a130f80cb8225a30b289b9d06c5c508a55ea7..f4399659d084ed9703052b3ce2c82a9e io.papermc.paper.entity.activation.ActivationRange.activateEntities(this); // Paper - EAR this.entityTickList .forEach( -@@ -1862,22 +1869,16 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -1828,22 +1852,16 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe if (Shapes.joinIsNotEmpty(collisionShape, collisionShape1, BooleanOp.NOT_SAME)) { List list = new ObjectArrayList<>(); @@ -465,19 +324,19 @@ index d27a130f80cb8225a30b289b9d06c5c508a55ea7..f4399659d084ed9703052b3ce2c82a9e try { this.isUpdatingNavigations = true; diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java -index 49c4d40e802f8adaba52d929ba013d3953704989..9f559be50c990d15d7765827b7c4c4092f5e184a 100644 +index 22a2b6b31f6f9b9b613586f7d674302304be3232..66ba223dacefb3531c46b144c4499b2b2285eafe 100644 --- a/net/minecraft/world/level/Level.java +++ b/net/minecraft/world/level/Level.java -@@ -112,7 +112,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - .build(); - public final org.bxteam.divinemc.util.BlockEntityTickersList blockEntityTickers = new org.bxteam.divinemc.util.BlockEntityTickersList(); // DivineMC - optimize block entity removals - Fix MC-117075 - protected final CollectingNeighborUpdater neighborUpdater; +@@ -106,7 +106,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl + public static final int MIN_ENTITY_SPAWN_Y = -20000000; + public final org.bxteam.divinemc.util.BlockEntityTickersList blockEntityTickers = new org.bxteam.divinemc.util.BlockEntityTickersList(); // Paper - public // DivineMC - optimize block entity removals - Fix MC-117075 + protected final NeighborUpdater neighborUpdater; - private final List pendingBlockEntityTickers = Lists.newArrayList(); + private final List pendingBlockEntityTickers = java.util.Collections.synchronizedList(Lists.newArrayList()); // DivineMC - Regionized Chunk Ticking private boolean tickingBlockEntities; public final Thread thread; private final boolean isDebug; -@@ -144,7 +144,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl +@@ -138,7 +138,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl public boolean captureBlockStates = false; public boolean captureTreeGeneration = false; @@ -486,9 +345,9 @@ index 49c4d40e802f8adaba52d929ba013d3953704989..9f559be50c990d15d7765827b7c4c409 public Map capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper - Retain block place order when capturing blockstates @Nullable public List captureDrops; -@@ -1457,10 +1457,14 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl +@@ -1503,10 +1503,14 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl - public void tickBlockEntities() { + protected void tickBlockEntities() { this.tickingBlockEntities = true; - if (!this.pendingBlockEntityTickers.isEmpty()) { - this.blockEntityTickers.addAll(this.pendingBlockEntityTickers); @@ -505,10 +364,10 @@ index 49c4d40e802f8adaba52d929ba013d3953704989..9f559be50c990d15d7765827b7c4c409 // Spigot start boolean runsNormally = this.tickRateManager().runsNormally(); diff --git a/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java b/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java -index 879be2d05ef0fcfb8fab0c9f4e5bf66d7fce730b..ff8fde0d294c96755cdcdcef0cbf57dd259e06a7 100644 +index 028eae2f9a459b60e92f3344091083aa93b54485..51e5a54aff069cac14deef6c04899d3a469842ce 100644 --- a/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java +++ b/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java -@@ -53,7 +53,7 @@ public class CollectingNeighborUpdater implements NeighborUpdater { +@@ -46,7 +46,7 @@ public class CollectingNeighborUpdater implements NeighborUpdater { this.addAndRun(pos, new CollectingNeighborUpdater.MultiNeighborUpdate(pos.immutable(), block, orientation, facing)); } @@ -517,7 +376,7 @@ index 879be2d05ef0fcfb8fab0c9f4e5bf66d7fce730b..ff8fde0d294c96755cdcdcef0cbf57dd boolean flag = this.count > 0; boolean flag1 = this.maxChainedNeighborUpdates >= 0 && this.count >= this.maxChainedNeighborUpdates; this.count++; -@@ -72,7 +72,7 @@ public class CollectingNeighborUpdater implements NeighborUpdater { +@@ -65,7 +65,7 @@ public class CollectingNeighborUpdater implements NeighborUpdater { } } diff --git a/divinemc-server/src/main/java/org/bxteam/divinemc/async/rct/RegionData.java b/divinemc-server/src/main/java/org/bxteam/divinemc/async/rct/RegionData.java new file mode 100644 index 0000000..ceddc92 --- /dev/null +++ b/divinemc-server/src/main/java/org/bxteam/divinemc/async/rct/RegionData.java @@ -0,0 +1,12 @@ +package org.bxteam.divinemc.async.rct; + +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import net.minecraft.world.entity.Entity; + +import java.util.Set; + +record RegionData(LongOpenHashSet chunks, Set entities) { + public boolean isEmpty() { + return chunks.isEmpty(); + } +} diff --git a/divinemc-server/src/main/java/org/bxteam/divinemc/async/rct/RegionizedChunkTicking.java b/divinemc-server/src/main/java/org/bxteam/divinemc/async/rct/RegionizedChunkTicking.java new file mode 100644 index 0000000..d308a04 --- /dev/null +++ b/divinemc-server/src/main/java/org/bxteam/divinemc/async/rct/RegionizedChunkTicking.java @@ -0,0 +1,244 @@ +package org.bxteam.divinemc.async.rct; + +import ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.TickThread; +import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistances; +import com.mojang.datafixers.DataFixer; +import com.mojang.logging.LogUtils; +import io.papermc.paper.entity.activation.ActivationRange; +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.progress.ChunkProgressListener; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.entity.ChunkStatusUpdateListener; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; +import net.minecraft.world.level.storage.DimensionDataStorage; +import net.minecraft.world.level.storage.LevelStorageSource; +import org.bxteam.divinemc.config.DivineConfig; +import org.bxteam.divinemc.util.NamedAgnosticThreadFactory; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.function.Supplier; + +public final class RegionizedChunkTicking extends ServerChunkCache { + private static final Logger LOGGER = LogUtils.getLogger(); + public static final Executor REGION_EXECUTOR = Executors.newFixedThreadPool(DivineConfig.AsyncCategory.regionizedChunkTickingExecutorThreadCount, + new NamedAgnosticThreadFactory<>("Region Ticking", TickThread::new, DivineConfig.AsyncCategory.regionizedChunkTickingExecutorThreadPriority) + ); + + public RegionizedChunkTicking(final ServerLevel level, final LevelStorageSource.LevelStorageAccess levelStorageAccess, final DataFixer fixerUpper, final StructureTemplateManager structureManager, final Executor dispatcher, final ChunkGenerator generator, final int viewDistance, final int simulationDistance, final boolean sync, final ChunkProgressListener progressListener, final ChunkStatusUpdateListener chunkStatusListener, final Supplier overworldDataStorage) { + super(level, levelStorageAccess, fixerUpper, structureManager, dispatcher, generator, viewDistance, simulationDistance, sync, progressListener, chunkStatusListener, overworldDataStorage); + } + + public void execute(CompletableFuture spawns, final LevelChunk[] raw) { + final TickPair tickPair = computePlayerRegionsParallel(); + final RegionData[] regions = tickPair.regions(); + final int regionCount = regions.length; + + ActivationRange.activateEntities(level); // Paper - EAR + + final int randomTickSpeed = level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING); + ObjectArrayList> ticked = new ObjectArrayList<>(regionCount); + + for (final RegionData region : regions) { + if (region == null || region.isEmpty()) { + continue; + } + + ticked.add(tick(region, randomTickSpeed)); + } + + CompletableFuture.runAsync(() -> { + finishTicking(ticked, randomTickSpeed, raw, tickPair); + spawns.join(); + }, REGION_EXECUTOR).join(); + } + + private CompletableFuture tick(RegionData region, int randomTickSpeed) { + return CompletableFuture.supplyAsync(() -> { + ObjectArrayList regionChunks = new ObjectArrayList<>(region.chunks().size()); + LongOpenHashSet regionChunksIDs = new LongOpenHashSet(region.chunks().size()); + for (long key : region.chunks()) { + LevelChunk chunk = fullChunks.get(key); + if (chunk != null) { + regionChunks.add(chunk); + regionChunksIDs.add(key); + } + } + + for (LevelChunk chunk : regionChunks) { + level.tickChunk(chunk, randomTickSpeed); + } + for (Entity entity : region.entities()) { + tickEntity(entity); + } + + return regionChunksIDs; + }, REGION_EXECUTOR); + } + + private void finishTicking(final ObjectArrayList> ticked, final int randomTickSpeed, final LevelChunk[] raw, final TickPair tickPair) { + try { + CompletableFuture.allOf(ticked.toArray(new CompletableFuture[0])).join(); + } catch (CompletionException ex) { + LOGGER.error("Error during region chunk ticking", ex.getCause()); + } + + LongOpenHashSet tickedChunkKeys = new LongOpenHashSet(raw.length); + + for (CompletableFuture future : ticked) { + if (!future.isCompletedExceptionally()) { + try { + LongOpenHashSet regionChunks = future.join(); + tickedChunkKeys.addAll(regionChunks); + } catch (Exception e) { + LOGGER.error("Exception in region ticking future", e); + } + } + } + + for (LevelChunk chunk : raw) { + if (!tickedChunkKeys.contains(chunk.coordinateKey)) { + level.tickChunk(chunk, randomTickSpeed); + } + } + + for (Entity entity : tickPair.entities()) { + tickEntity(entity); + } + } + + private TickPair computePlayerRegionsParallel() { + int tickViewDistance = level.moonrise$getViewDistanceHolder().getViewDistances().tickViewDistance(); + List players = new ArrayList<>(level.players()); + int max = maxChunksForViewDistance(); + + List playerChunkSets = players.parallelStream() + .map(player -> { + ChunkPos playerChunk = player.chunkPosition(); + int px = playerChunk.x; + int pz = playerChunk.z; + LongOpenHashSet chunkKeys = new LongOpenHashSet(max); + for (int dx = -tickViewDistance; dx <= tickViewDistance; dx++) { + for (int dz = -tickViewDistance; dz <= tickViewDistance; dz++) { + long key = CoordinateUtils.getChunkKey(px + dx, pz + dz); + chunkKeys.add(key); + } + } + return chunkKeys; + }).toList(); + + List mergedRegions = new ArrayList<>(); + boolean[] merged = new boolean[playerChunkSets.size()]; + + for (int i = 0; i < playerChunkSets.size(); i++) { + if (merged[i]) continue; + + LongOpenHashSet region = new LongOpenHashSet(playerChunkSets.get(i)); + merged[i] = true; + + boolean madeChanges; + do { + madeChanges = false; + for (int j = i + 1; j < playerChunkSets.size(); j++) { + if (merged[j]) continue; + + LongOpenHashSet set = playerChunkSets.get(j); + + boolean hasIntersection = false; + LongIterator iter = set.iterator(); + while (iter.hasNext()) { + if (region.contains(iter.nextLong())) { + hasIntersection = true; + break; + } + } + + if (hasIntersection) { + region.addAll(set); + merged[j] = true; + madeChanges = true; + } + } + } while (madeChanges); + + mergedRegions.add(region); + } + + ObjectArrayList regions = new ObjectArrayList<>(); + Long2IntOpenHashMap chunkToRegion = new Long2IntOpenHashMap(max * mergedRegions.size()); + chunkToRegion.defaultReturnValue(-1); + for (int i = 0; i < mergedRegions.size(); i++) { + regions.add(new RegionData(mergedRegions.get(i), new ObjectOpenHashSet<>())); + for (long key : mergedRegions.get(i)) { + chunkToRegion.put(key, i); + } + } + + final Set firstTick = new ObjectOpenHashSet<>(); + + synchronized (getEntityTickList().entities) { + final IteratorSafeOrderedReferenceSet.Iterator iterator = getEntityTickList().entities.iterator(); + try { + while (iterator.hasNext()) { + Entity entity = iterator.next(); + long chunkKey = entity.chunkPosition().longKey; + int regionIndex = chunkToRegion.get(chunkKey); + if (regionIndex != -1) { + regions.get(regionIndex).entities().add(entity); + } else { + firstTick.add(entity); + } + } + } finally { + iterator.finishedIterating(); + } + } + + return new TickPair(regions.toArray(new RegionData[0]), firstTick); + } + + // Should be max safe estimate of ticking chunks in a region + private int maxChunksForViewDistance() { + ViewDistances distances = level.moonrise$getViewDistanceHolder().getViewDistances(); + int diameter = 2 * distances.tickViewDistance() + 1; + return diameter * diameter; + } + + private void tickEntity(Entity entity) { + if (!entity.isRemoved()) { + if (!level.tickRateManager().isEntityFrozen(entity)) { + entity.checkDespawn(); + // Paper - rewrite chunk system + Entity vehicle = entity.getVehicle(); + if (vehicle != null) { + if (!vehicle.isRemoved() && vehicle.hasPassenger(entity)) { + return; + } + + entity.stopRiding(); + } + + level.guardEntityTick(level::tickNonPassenger, entity); + } + } + } +} diff --git a/divinemc-server/src/main/java/org/bxteam/divinemc/async/rct/TickPair.java b/divinemc-server/src/main/java/org/bxteam/divinemc/async/rct/TickPair.java new file mode 100644 index 0000000..73c8e38 --- /dev/null +++ b/divinemc-server/src/main/java/org/bxteam/divinemc/async/rct/TickPair.java @@ -0,0 +1,7 @@ +package org.bxteam.divinemc.async.rct; + +import net.minecraft.world.entity.Entity; + +import java.util.Set; + +record TickPair(RegionData[] regions, Set entities) { } diff --git a/divinemc-server/src/main/java/org/bxteam/divinemc/config/DivineConfig.java b/divinemc-server/src/main/java/org/bxteam/divinemc/config/DivineConfig.java index cdf7099..ade282f 100644 --- a/divinemc-server/src/main/java/org/bxteam/divinemc/config/DivineConfig.java +++ b/divinemc-server/src/main/java/org/bxteam/divinemc/config/DivineConfig.java @@ -221,6 +221,7 @@ public class DivineConfig { // Async mob spawning settings public static boolean enableAsyncSpawning = true; + public static boolean asyncNaturalSpawn = true; public static void load() { parallelWorldTicking(); @@ -335,6 +336,8 @@ public class DivineConfig { private static void asyncMobSpawning() { enableAsyncSpawning = getBoolean(ConfigCategory.ASYNC.key("mob-spawning.enable"), enableAsyncSpawning, "Enables optimization that will offload much of the computational effort involved with spawning new mobs to a different thread."); + asyncNaturalSpawn = getBoolean(ConfigCategory.ASYNC.key("async-ticking-of-natural-spawns"), asyncNaturalSpawn, + "Enables offloading of natural spawning to a different thread"); } }