From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Aikar Date: Sat, 11 Apr 2020 03:56:07 -0400 Subject: [PATCH] Implement Chunk Priority / Urgency System for Chunks Mark chunks that are blocking main thread for world generation as urgent Implements a general priority system so that chunks that are sorted in the generator queues can prioritize certain chunks over another. Urgent chunks will jump to the front of the line, ensuring that a sync chunk load on an ungenerated chunk does not lag the server for a long period of time if the servers generator queues are filled with lots of chunks already. This massively reduces the lag spikes from sync chunk gens. Then we further prioritize loading order so nearby chunks have higher priority than distant chunks, reducing the pressure a high no tick view distance holds on you. Chunks in front of the player have higher priority, to help with fast traveling players keep up with their movement. diff --git a/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java index 18ae2e2b339d357fbe0f6f2b18bc14c0dfe4c222..3b7ba9c755c82a6f086d5542d32b3567c0f98b99 100644 --- a/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java +++ b/src/main/java/com/destroystokyo/paper/io/chunk/ChunkTaskManager.java @@ -108,7 +108,7 @@ public final class ChunkTaskManager { } static void dumpChunkInfo(Set seenChunks, ChunkHolder chunkHolder, int x, int z) { - dumpChunkInfo(seenChunks, chunkHolder, x, z, 0, 1); + dumpChunkInfo(seenChunks, chunkHolder, x, z, 0, 4); // Paper - 1->4 } static void dumpChunkInfo(Set seenChunks, ChunkHolder chunkHolder, int x, int z, int indent, int maxDepth) { @@ -129,6 +129,31 @@ public final class ChunkTaskManager { PaperFileIOThread.LOGGER.log(Level.ERROR, indentStr + "Chunk Status - " + ((chunk == null) ? "null chunk" : chunk.getStatus().toString())); PaperFileIOThread.LOGGER.log(Level.ERROR, indentStr + "Chunk Ticket Status - " + ChunkHolder.getStatus(chunkHolder.getTicketLevel())); PaperFileIOThread.LOGGER.log(Level.ERROR, indentStr + "Chunk Holder Status - " + ((holderStatus == null) ? "null" : holderStatus.toString())); + // Paper start + PaperFileIOThread.LOGGER.log(Level.ERROR, indentStr + "Chunk Holder Priority - " + chunkHolder.queueLevel); + + if (!chunkHolder.neighbors.isEmpty()) { + if (indent >= maxDepth) { + PaperFileIOThread.LOGGER.log(Level.ERROR, indentStr + "Chunk Neighbors: (Can't show, too deeply nested)"); + return; + } + PaperFileIOThread.LOGGER.log(Level.ERROR, indentStr + "Chunk Neighbors: "); + for (ChunkHolder neighbor : chunkHolder.neighbors.keySet()) { + ChunkStatus status = neighbor.getChunkHolderStatus(); + if (status != null && status.isOrAfter(ChunkHolder.getStatus(neighbor.getTicketLevel()))) { + continue; + } + int nx = neighbor.pos.x; + int nz = neighbor.pos.z; + if (seenChunks.contains(neighbor)) { + PaperFileIOThread.LOGGER.log(Level.ERROR, indentStr + " " + nx + "," + nz + " in " + chunkHolder.getWorld().getWorld().getName() + " (CIRCULAR)"); + continue; + } + PaperFileIOThread.LOGGER.log(Level.ERROR, indentStr + " " + nx + "," + nz + " in " + chunkHolder.getWorld().getWorld().getName() + ":"); + dumpChunkInfo(seenChunks, neighbor, nx, nz, indent + 1, maxDepth); + } + } + // Paper end } } diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java index 2d5b8e35d52b0dfd075af81a3a936d8a21053b31..9ddedd310eb0323a5a09f51a61bfb7b36503be93 100644 --- a/src/main/java/net/minecraft/server/MCUtil.java +++ b/src/main/java/net/minecraft/server/MCUtil.java @@ -671,6 +671,7 @@ public final class MCUtil { chunkData.addProperty("x", playerChunk.pos.x); chunkData.addProperty("z", playerChunk.pos.z); chunkData.addProperty("ticket-level", playerChunk.getTicketLevel()); + chunkData.addProperty("priority", playerChunk.queueLevel); // Paper - priority chunkData.addProperty("state", ChunkHolder.getFullChunkStatus(playerChunk.getTicketLevel()).toString()); chunkData.addProperty("queued-for-unload", chunkMap.toDrop.contains(playerChunk.pos.longKey)); chunkData.addProperty("status", status == null ? "unloaded" : status.toString()); diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java index 8245e07f6ecfd9dd997c8525b52c6eadd392e6f1..a0334dde56829fd895323f4cab2ab55d5d5ecf30 100644 --- a/src/main/java/net/minecraft/server/level/ChunkHolder.java +++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java @@ -60,7 +60,7 @@ public class ChunkHolder { private final DebugBuffer chunkToSaveHistory; public int oldTicketLevel; private int ticketLevel; - private int queueLevel; + public volatile int queueLevel; // Paper - private->public, make volatile since this is concurrently accessed public final ChunkPos pos; private boolean hasChangedSections; private final ShortSet[] changedBlocksPerSection; @@ -75,6 +75,7 @@ public class ChunkHolder { boolean isUpdateQueued = false; // Paper private final ChunkMap chunkMap; // Paper + public ServerLevel getWorld() { return chunkMap.level; } // Paper // Paper start - no-tick view distance public final LevelChunk getSendingChunk() { // it's important that we use getChunkAtIfLoadedImmediately to mirror the chunk sending logic used @@ -100,6 +101,134 @@ public class ChunkHolder { } // Paper end + // Paper start - Chunk gen/load priority system + volatile int neighborPriority = -1; + volatile int priorityBoost = 0; + public final java.util.concurrent.ConcurrentHashMap neighbors = new java.util.concurrent.ConcurrentHashMap<>(); + public final it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap neighborPriorities = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>(); + + private int getDemandedPriority() { + int priority = neighborPriority; // if we have a neighbor priority, use it + int myPriority = getMyPriority(); + + if (priority == -1 || (ticketLevel <= 33 && priority > myPriority)) { + priority = myPriority; + } + + return Math.max(1, Math.min(Math.max(ticketLevel, ChunkMap.MAX_CHUNK_DISTANCE), priority)); + } + + private int getMyPriority() { + if (priorityBoost == DistanceManager.URGENT_PRIORITY) { + return 2; // Urgent - ticket level isn't always 31 so 33-30 = 3, but allow 1 more tasks to go below this for dependents + } + return ticketLevel - priorityBoost; + } + + private int getNeighborsPriority() { + return (neighborPriorities.isEmpty() ? getMyPriority() : getDemandedPriority()) + 1; + } + + public void onNeighborRequest(ChunkHolder neighbor, ChunkStatus status) { + neighbor.setNeighborPriority(this, getNeighborsPriority()); + this.neighbors.compute(neighbor, (playerChunk, currentWantedStatus) -> { + if (currentWantedStatus == null || !currentWantedStatus.isOrAfter(status)) { + //System.out.println(this + " request " + neighbor + " at " + status + " currently " + currentWantedStatus); + return status; + } else { + //System.out.println(this + " requested " + neighbor + " at " + status + " but thats lower than other wanted status " + currentWantedStatus); + return currentWantedStatus; + } + }); + + } + + public void onNeighborDone(ChunkHolder neighbor, ChunkStatus chunkstatus, ChunkAccess chunk) { + this.neighbors.compute(neighbor, (playerChunk, wantedStatus) -> { + if (wantedStatus != null && chunkstatus.isOrAfter(wantedStatus)) { + //System.out.println(this + " neighbor done at " + neighbor + " for status " + chunkstatus + " wanted " + wantedStatus); + neighbor.removeNeighborPriority(this); + return null; + } else { + //System.out.println(this + " neighbor finished our previous request at " + neighbor + " for status " + chunkstatus + " but we now want instead " + wantedStatus); + return wantedStatus; + } + }); + } + + private void removeNeighborPriority(ChunkHolder requester) { + synchronized (neighborPriorities) { + neighborPriorities.remove(requester.pos.toLong()); + recalcNeighborPriority(); + } + checkPriority(); + } + + + private void setNeighborPriority(ChunkHolder requester, int priority) { + synchronized (neighborPriorities) { + neighborPriorities.put(requester.pos.toLong(), Integer.valueOf(priority)); + recalcNeighborPriority(); + } + checkPriority(); + } + + private void recalcNeighborPriority() { + neighborPriority = -1; + if (!neighborPriorities.isEmpty()) { + synchronized (neighborPriorities) { + for (Integer neighbor : neighborPriorities.values()) { + if (neighbor < neighborPriority || neighborPriority == -1) { + neighborPriority = neighbor; + } + } + } + } + } + private void checkPriority() { + if (queueLevel != getDemandedPriority()) this.chunkMap.queueHolderUpdate(this); + } + + public final double getDistance(ServerPlayer player) { + return getDistance(player.getX(), player.getZ()); + } + public final double getDistance(double blockX, double blockZ) { + int cx = net.minecraft.server.MCUtil.fastFloor(blockX) >> 4; + int cz = net.minecraft.server.MCUtil.fastFloor(blockZ) >> 4; + final double x = pos.x - cx; + final double z = pos.z - cz; + return (x * x) + (z * z); + } + + public final double getDistanceFrom(BlockPos pos) { + return getDistance(pos.getX(), pos.getZ()); + } + + public static ChunkStatus getNextStatus(ChunkStatus status) { + if (status == ChunkStatus.FULL) { + return status; + } + return CHUNK_STATUSES.get(status.getIndex() + 1); + } + public CompletableFuture> getStatusFutureUncheckedMain(ChunkStatus chunkstatus) { + return ensureMain(getFutureIfPresentUnchecked(chunkstatus)); + } + public CompletableFuture ensureMain(CompletableFuture future) { + return future.thenApplyAsync(r -> r, chunkMap.mainInvokingExecutor); + } + + @Override + public String toString() { + return "PlayerChunk{" + + "location=" + pos + + ", ticketLevel=" + ticketLevel + "/" + getStatus(this.ticketLevel) + + ", chunkHolderStatus=" + getChunkHolderStatus() + + ", neighborPriority=" + getNeighborsPriority() + + ", priority=(" + ticketLevel + " - " + priorityBoost +" vs N " + neighborPriority + ") = " + getDemandedPriority() + " A " + queueLevel + + '}'; + } + // Paper end + // Paper start - optimise isOutsideOfRange // cached here to avoid a map lookup com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playersInMobSpawnRange; @@ -470,12 +599,25 @@ public class ChunkHolder { }); } + // Paper start + private boolean loadCallbackScheduled = false; + private boolean unloadCallbackScheduled = false; + static void ensureTickThread(final String reason) { + // TODO REMOVE + if (!org.bukkit.Bukkit.isPrimaryThread()) { + MinecraftServer.LOGGER.fatal("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable()); + throw new IllegalStateException(reason); + } + } + // Paper end + private void demoteFullChunk(ChunkMap playerchunkmap, ChunkHolder.FullChunkStatus playerchunk_state) { this.pendingFullStateConfirmation.cancel(false); playerchunkmap.onFullChunkStatusChange(this.pos, playerchunk_state); } protected void updateFutures(ChunkMap chunkStorage, Executor executor) { + ensureTickThread("Async ticket level update"); // Paper ChunkStatus chunkstatus = ChunkHolder.getStatus(this.oldTicketLevel); ChunkStatus chunkstatus1 = ChunkHolder.getStatus(this.ticketLevel); boolean flag = this.oldTicketLevel <= ChunkMap.MAX_CHUNK_DISTANCE; @@ -486,9 +628,22 @@ public class ChunkHolder { // ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins. if (playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && !playerchunk_state1.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { this.getFutureIfPresentUnchecked(ChunkStatus.FULL).thenAccept((either) -> { + ensureTickThread("Async full status chunk future completion"); // Paper LevelChunk chunk = (LevelChunk)either.left().orElse(null); - if (chunk != null) { + if (chunk != null && chunk.wasLoadCallbackInvoked() && ChunkHolder.this.ticketLevel > 33) { // Paper - only invoke unload if load was called + // Paper start - only schedule once, now the future is no longer completed as RIGHT if unloaded... + if (ChunkHolder.this.unloadCallbackScheduled) { + return; + } + ChunkHolder.this.unloadCallbackScheduled = true; + // Paper end - only schedule once, now the future is no longer completed as RIGHT if unloaded... chunkStorage.callbackExecutor.execute(() -> { + // Paper start - only schedule once, now the future is no longer completed as RIGHT if unloaded... + ChunkHolder.this.unloadCallbackScheduled = false; + if (ChunkHolder.this.ticketLevel <= 33) { + return; + } + // Paper end - only schedule once, now the future is no longer completed as RIGHT if unloaded... // Minecraft will apply the chunks tick lists to the world once the chunk got loaded, and then store the tick // lists again inside the chunk once the chunk becomes inaccessible and set the chunk's needsSaving flag. // These actions may however happen deferred, so we manually set the needsSaving flag already here. @@ -545,12 +700,14 @@ public class ChunkHolder { this.scheduleFullChunkPromotion(chunkStorage, this.fullChunkFuture, executor, ChunkHolder.FullChunkStatus.BORDER); // Paper start - cache ticking ready status this.fullChunkFuture.thenAccept(either -> { + ensureTickThread("Async full chunk future completion"); // Paper final Optional left = either.left(); if (left.isPresent() && ChunkHolder.this.fullChunkCreateCount == expectCreateCount) { // note: Here is a very good place to add callbacks to logic waiting on this. LevelChunk fullChunk = either.left().get(); ChunkHolder.this.isFullChunkReady = true; fullChunk.playerChunk = ChunkHolder.this; + this.chunkMap.distanceManager.clearPriorityTickets(pos); } }); this.updateChunkToSave(this.fullChunkFuture, "full"); @@ -575,6 +732,7 @@ public class ChunkHolder { this.scheduleFullChunkPromotion(chunkStorage, this.tickingChunkFuture, executor, ChunkHolder.FullChunkStatus.TICKING); // Paper start - cache ticking ready status this.tickingChunkFuture.thenAccept(either -> { + ensureTickThread("Async full chunk future completion"); // Paper either.ifLeft(chunk -> { // note: Here is a very good place to add callbacks to logic waiting on this. ChunkHolder.this.isTickingReady = true; @@ -607,6 +765,7 @@ public class ChunkHolder { this.scheduleFullChunkPromotion(chunkStorage, this.entityTickingChunkFuture, executor, ChunkHolder.FullChunkStatus.ENTITY_TICKING); // Paper start - cache ticking ready status this.entityTickingChunkFuture.thenAccept(either -> { + ensureTickThread("Async full chunk future completion"); // Paper either.ifLeft(chunk -> { ChunkHolder.this.isEntityTickingReady = true; }); @@ -624,16 +783,42 @@ public class ChunkHolder { this.demoteFullChunk(chunkStorage, playerchunk_state1); } - this.onLevelChange.onLevelChange(this.pos, this::getQueueLevel, this.ticketLevel, this::setQueueLevel); + //this.onLevelChange.onLevelChange(this.pos, this::getQueueLevel, this.ticketLevel, this::setQueueLevel); + // Paper start - raise IO/load priority if priority changes, use our preferred priority + priorityBoost = chunkMap.distanceManager.getChunkPriority(pos); + int priority = getDemandedPriority(); + if (this.queueLevel > priority) { + int ioPriority = com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY; + if (priority <= 10) { + ioPriority = com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY; + } else if (priority <= 20) { + ioPriority = com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGH_PRIORITY; + } + chunkMap.level.asyncChunkTaskManager.raisePriority(pos.x, pos.z, ioPriority); + } + if (this.queueLevel != priority) { + this.onLevelChange.onLevelChange(this.pos, () -> this.queueLevel, priority, p -> this.queueLevel = p); // use preferred priority + int neighborsPriority = getNeighborsPriority(); + this.neighbors.forEach((neighbor, neighborDesired) -> neighbor.setNeighborPriority(this, neighborsPriority)); + } + // Paper end this.oldTicketLevel = this.ticketLevel; // CraftBukkit start // ChunkLoadEvent: Called after the chunk is loaded: isChunkLoaded returns true and chunk is ready to be modified by plugins. if (!playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.BORDER) && playerchunk_state1.isOrAfter(ChunkHolder.FullChunkStatus.BORDER)) { this.getFutureIfPresentUnchecked(ChunkStatus.FULL).thenAccept((either) -> { + ensureTickThread("Async full status chunk future completion"); // Paper LevelChunk chunk = (LevelChunk)either.left().orElse(null); - if (chunk != null) { + if (chunk != null && ChunkHolder.this.oldTicketLevel <= 33 && !chunk.wasLoadCallbackInvoked()) { // Paper - ensure ticket level is set to loaded before calling, as now this can complete with ticket level > 33 + // Paper start - only schedule once, now the future is no longer completed as RIGHT if unloaded... + if (ChunkHolder.this.loadCallbackScheduled) { + return; + } + ChunkHolder.this.loadCallbackScheduled = true; + // Paper end - only schedule once, now the future is no longer completed as RIGHT if unloaded... chunkStorage.callbackExecutor.execute(() -> { - chunk.loadCallback(); + ChunkHolder.this.loadCallbackScheduled = false; // Paper - only schedule once, now the future is no longer completed as RIGHT if unloaded... + if (ChunkHolder.this.oldTicketLevel <= 33) chunk.loadCallback(); // Paper " }); } }).exceptionally((throwable) -> { diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java index 12f9f512dc8b9e094dab2caec7b6ddaf2a378ee9..5b1b91ab4d351f47a05a73cf65539b0bc92df13b 100644 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java +++ b/src/main/java/net/minecraft/server/level/ChunkMap.java @@ -149,6 +149,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider public final ServerLevel level; private final ThreadedLevelLightEngine lightEngine; private final BlockableEventLoop mainThreadExecutor; + final java.util.concurrent.Executor mainInvokingExecutor; // Paper public final ChunkGenerator generator; public final Supplier overworldDataStorage; private final PoiManager poiManager; @@ -341,6 +342,15 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.level = world; this.generator = chunkGenerator; this.mainThreadExecutor = mainThreadExecutor; + // Paper start + this.mainInvokingExecutor = (run) -> { + if (MCUtil.isMainThread()) { + run.run(); + } else { + mainThreadExecutor.execute(run); + } + }; + // Paper end ProcessorMailbox threadedmailbox = ProcessorMailbox.create(executor, "worldgen"); Objects.requireNonNull(mainThreadExecutor); @@ -436,6 +446,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.playerViewDistanceTickMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets, (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { + checkHighPriorityChunks(player); if (newState.size() != 1) { return; } @@ -454,7 +465,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } ChunkPos chunkPos = new ChunkPos(rangeX, rangeZ); ChunkMap.this.level.getChunkSource().removeTicketAtLevel(TicketType.PLAYER, chunkPos, 31, chunkPos); // entity ticking level, TODO check on update - }); + // Paper start + ChunkMap.this.level.getChunkSource().clearPriorityTickets(chunkPos); + }, + (player, prevPos, newPos) -> { + player.lastHighPriorityChecked = -1; // reset and recheck + checkHighPriorityChunks(player); + }); + // Paper end this.playerViewDistanceNoTickMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets); this.playerViewDistanceBroadcastMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets, (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, @@ -472,6 +490,116 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider // Paper end - no-tick view distance } + // Paper start - Chunk Prioritization + public void queueHolderUpdate(ChunkHolder playerchunk) { + Runnable runnable = () -> { + if (isUnloading(playerchunk)) { + return; // unloaded + } + distanceManager.pendingChunkUpdates.add(playerchunk); + if (!distanceManager.pollingPendingChunkUpdates) { + level.getChunkSource().runDistanceManagerUpdates(); + } + }; + if (MCUtil.isMainThread()) { + // We can't use executor here because it will not execute tasks if its currently in the middle of executing tasks... + runnable.run(); + } else { + mainThreadExecutor.execute(runnable); + } + } + + private boolean isUnloading(ChunkHolder playerchunk) { + return playerchunk == null || toDrop.contains(playerchunk.pos.toLong()); + } + + private void updateChunkPriorityMap(it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap map, long chunk, int level) { + int prev = map.getOrDefault(chunk, -1); + if (level > prev) { + map.put(chunk, level); + } + } + + public void checkHighPriorityChunks(ServerPlayer player) { + int currentTick = MinecraftServer.currentTick; + if (currentTick - player.lastHighPriorityChecked < 20 || !player.isRealPlayer) { // weed out fake players + return; + } + player.lastHighPriorityChecked = currentTick; + it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap priorities = new it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap(); + + int viewDistance = getEffectiveNoTickViewDistance(); + net.minecraft.core.BlockPos.MutableBlockPos pos = new net.minecraft.core.BlockPos.MutableBlockPos(); + + // Prioritize circular near + double playerChunkX = Mth.floor(player.getX()) >> 4; + double playerChunkZ = Mth.floor(player.getZ()) >> 4; + pos.set(player.getX(), 0, player.getZ()); + double twoThirdModifier = 2D / 3D; + MCUtil.getSpiralOutChunks(pos, Math.min(6, viewDistance)).forEach(coord -> { + if (shouldSkipPrioritization(coord)) return; + + double dist = MCUtil.distance(playerChunkX, 0, playerChunkZ, coord.x, 0, coord.z); + // Prioritize immediate + if (dist <= 4) { + updateChunkPriorityMap(priorities, coord.toLong(), (int) (27 - dist)); + return; + } + + // Prioritize nearby chunks + updateChunkPriorityMap(priorities, coord.toLong(), (int) (20 - dist * twoThirdModifier)); + }); + + // Prioritize Frustum near 3 + ChunkPos front3 = player.getChunkInFront(3); + pos.set(front3.x << 4, 0, front3.z << 4); + MCUtil.getSpiralOutChunks(pos, Math.min(5, viewDistance)).forEach(coord -> { + if (shouldSkipPrioritization(coord)) return; + + double dist = MCUtil.distance(playerChunkX, 0, playerChunkZ, coord.x, 0, coord.z); + updateChunkPriorityMap(priorities, coord.toLong(), (int) (25 - dist * twoThirdModifier)); + }); + + // Prioritize Frustum near 5 + if (viewDistance > 4) { + ChunkPos front5 = player.getChunkInFront(5); + pos.set(front5.x << 4, 0, front5.z << 4); + MCUtil.getSpiralOutChunks(pos, 4).forEach(coord -> { + if (shouldSkipPrioritization(coord)) return; + + double dist = MCUtil.distance(playerChunkX, 0, playerChunkZ, coord.x, 0, coord.z); + updateChunkPriorityMap(priorities, coord.toLong(), (int) (25 - dist * twoThirdModifier)); + }); + } + + // Prioritize Frustum far 7 + if (viewDistance > 6) { + ChunkPos front7 = player.getChunkInFront(7); + pos.set(front7.x << 4, 0, front7.z << 4); + MCUtil.getSpiralOutChunks(pos, 3).forEach(coord -> { + if (shouldSkipPrioritization(coord)) { + return; + } + double dist = MCUtil.distance(playerChunkX, 0, playerChunkZ, coord.x, 0, coord.z); + updateChunkPriorityMap(priorities, coord.toLong(), (int) (25 - dist * twoThirdModifier)); + }); + } + + if (priorities.isEmpty()) return; + distanceManager.delayDistanceManagerTick = true; + priorities.long2IntEntrySet().fastForEach(entry -> distanceManager.markHighPriority(new ChunkPos(entry.getLongKey()), entry.getIntValue())); + distanceManager.delayDistanceManagerTick = false; + level.getChunkSource().runDistanceManagerUpdates(); + + } + + private boolean shouldSkipPrioritization(ChunkPos coord) { + if (playerViewDistanceNoTickMap.getObjectsInRange(coord.toLong()) == null) return true; + ChunkHolder chunk = getUpdatingChunkIfPresent(coord.toLong()); + return chunk != null && (chunk.isFullChunkReady()); + } + // Paper end + // Paper start public void updatePlayerMobTypeMap(Entity entity) { if (!this.level.paperConfig.perPlayerMobSpawns) { @@ -630,6 +758,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider List>> list = Lists.newArrayList(); int j = centerChunk.x; int k = centerChunk.z; + ChunkHolder requestingNeighbor = getUpdatingChunkIfPresent(centerChunk.toLong()); // Paper for (int l = -margin; l <= margin; ++l) { for (int i1 = -margin; i1 <= margin; ++i1) { @@ -648,6 +777,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider ChunkStatus chunkstatus = (ChunkStatus) distanceToStatus.apply(j1); CompletableFuture> completablefuture = playerchunk.getOrScheduleFuture(chunkstatus, this); + // Paper start + if (requestingNeighbor != null && requestingNeighbor != playerchunk && !completablefuture.isDone()) { + requestingNeighbor.onNeighborRequest(playerchunk, chunkstatus); + completablefuture.thenAccept(either -> { + requestingNeighbor.onNeighborDone(playerchunk, chunkstatus, either.left().orElse(null)); + }); + } + // Paper end list.add(completablefuture); } @@ -1016,11 +1153,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider if (requiredStatus == ChunkStatus.EMPTY) { return this.scheduleChunkLoad(chunkcoordintpair); } else { + // Paper start - revert 1.17 chunk system changes + CompletableFuture> future = holder.getOrScheduleFuture(requiredStatus.getParent(), this); + return future.thenComposeAsync((either) -> { + Optional optional = either.left(); + if (!optional.isPresent()) { + return CompletableFuture.completedFuture(either); + } + // Paper end - revert 1.17 chunk system changes if (requiredStatus == ChunkStatus.LIGHT) { this.distanceManager.addTicket(TicketType.LIGHT, chunkcoordintpair, 33 + ChunkStatus.getDistance(ChunkStatus.LIGHT), chunkcoordintpair); } - Optional optional = ((Either) holder.getOrScheduleFuture(requiredStatus.getParent(), this).getNow(ChunkHolder.UNLOADED_CHUNK)).left(); + // Paper - revert 1.17 chunk system changes if (optional.isPresent() && ((ChunkAccess) optional.get()).getStatus().isOrAfter(requiredStatus)) { CompletableFuture> completablefuture = requiredStatus.load(this.level, this.structureManager, this.lightEngine, (ichunkaccess) -> { @@ -1032,6 +1177,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } else { return this.scheduleChunkGeneration(holder, requiredStatus); } + }, this.mainThreadExecutor).thenComposeAsync(CompletableFuture::completedFuture, this.mainThreadExecutor); // Paper - revert 1.17 chunk system changes } } @@ -1091,14 +1237,24 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider }; CompletableFuture chunkSaveFuture = this.level.asyncChunkTaskManager.getChunkSaveFuture(pos.x, pos.z); + // Paper start + ChunkHolder playerChunk = getUpdatingChunkIfPresent(pos.toLong()); + int chunkPriority = playerChunk != null ? playerChunk.queueLevel : 33; + int priority = com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY; + + if (chunkPriority <= 10) { + priority = com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY; + } else if (chunkPriority <= 20) { + priority = com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGH_PRIORITY; + } + boolean isHighestPriority = priority == com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY; + // Paper end if (chunkSaveFuture != null) { - this.level.asyncChunkTaskManager.scheduleChunkLoad(pos.x, pos.z, - com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGH_PRIORITY, chunkHolderConsumer, false, chunkSaveFuture); - this.level.asyncChunkTaskManager.raisePriority(pos.x, pos.z, com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGH_PRIORITY); + this.level.asyncChunkTaskManager.scheduleChunkLoad(pos.x, pos.z, priority, chunkHolderConsumer, isHighestPriority, chunkSaveFuture); // Paper } else { - this.level.asyncChunkTaskManager.scheduleChunkLoad(pos.x, pos.z, - com.destroystokyo.paper.io.PrioritizedTaskQueue.NORMAL_PRIORITY, chunkHolderConsumer, false); + this.level.asyncChunkTaskManager.scheduleChunkLoad(pos.x, pos.z, priority, chunkHolderConsumer, isHighestPriority); // Paper } + this.level.asyncChunkTaskManager.raisePriority(pos.x, pos.z, priority); // Paper return ret; // Paper end } @@ -1147,7 +1303,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.releaseLightTicket(chunkcoordintpair); return CompletableFuture.completedFuture(Either.right(playerchunk_failure)); }); - }, executor); + }, executor).thenComposeAsync((either) -> { // Paper start - force competion on the main thread + return CompletableFuture.completedFuture(either); + }, this.mainThreadExecutor); // use the main executor, we want to ensure only one chunk callback can be completed per runnable execute + // Paper end - force competion on the main thread } protected void releaseLightTicket(ChunkPos pos) { @@ -1230,7 +1389,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider long i = playerchunk.getPos().toLong(); Objects.requireNonNull(playerchunk); - mailbox.tell(ChunkTaskPriorityQueueSorter.message(runnable, i, playerchunk::getTicketLevel)); + mailbox.tell(ChunkTaskPriorityQueueSorter.message(runnable, i, () -> 1)); // Paper - final loads are always urgent! }); } diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java index d94241bcca4f2fd5e464a860bd356af504dc68b7..1cc4e0a1f3d8235ef88b48e01ca8b78a263d2676 100644 --- a/src/main/java/net/minecraft/server/level/DistanceManager.java +++ b/src/main/java/net/minecraft/server/level/DistanceManager.java @@ -98,6 +98,7 @@ public abstract class DistanceManager { } private static int getTicketLevelAt(SortedArraySet> arraysetsorted) { + org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::getLowestTicketLevel"); // Paper return !arraysetsorted.isEmpty() ? ((Ticket) arraysetsorted.first()).getTicketLevel() : ChunkMap.MAX_CHUNK_DISTANCE + 1; } @@ -111,6 +112,7 @@ public abstract class DistanceManager { public boolean runAllUpdates(ChunkMap playerchunkmap) { //this.f.a(); // Paper - no longer used + org.spigotmc.AsyncCatcher.catchOp("DistanceManagerTick"); // Paper this.playerTicketManager.runAllUpdates(); int i = Integer.MAX_VALUE - this.ticketTracker.runDistanceUpdates(Integer.MAX_VALUE); boolean flag = i != 0; @@ -121,11 +123,13 @@ public abstract class DistanceManager { // Paper start if (!this.pendingChunkUpdates.isEmpty()) { + this.pollingPendingChunkUpdates = true; try { // Paper - Chunk priority while(!this.pendingChunkUpdates.isEmpty()) { ChunkHolder remove = this.pendingChunkUpdates.remove(); remove.isUpdateQueued = false; remove.updateFutures(playerchunkmap, this.mainThreadExecutor); } + } finally { this.pollingPendingChunkUpdates = false; } // Paper - Chunk priority // Paper end return true; } else { @@ -161,8 +165,10 @@ public abstract class DistanceManager { return flag; } } + boolean pollingPendingChunkUpdates = false; // Paper - Chunk priority boolean addTicket(long i, Ticket ticket) { // CraftBukkit - void -> boolean + org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::addTicket"); // Paper SortedArraySet> arraysetsorted = this.getTickets(i); int j = DistanceManager.getTicketLevelAt(arraysetsorted); Ticket ticket1 = (Ticket) arraysetsorted.addOrGet(ticket); // CraftBukkit - decompile error @@ -176,7 +182,9 @@ public abstract class DistanceManager { } boolean removeTicket(long i, Ticket ticket) { // CraftBukkit - void -> boolean + org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::removeTicket"); // Paper SortedArraySet> arraysetsorted = this.getTickets(i); + int oldLevel = getTicketLevelAt(arraysetsorted); // Paper boolean removed = false; // CraftBukkit if (arraysetsorted.remove(ticket)) { @@ -208,7 +216,12 @@ public abstract class DistanceManager { this.tickets.remove(i); } - this.ticketTracker.update(i, DistanceManager.getTicketLevelAt(arraysetsorted), false); + // Paper start - Chunk priority + int newLevel = getTicketLevelAt(arraysetsorted); + if (newLevel > oldLevel) { + this.ticketTracker.update(i, newLevel, false); + } + // Paper end return removed; // CraftBukkit } @@ -250,6 +263,136 @@ public abstract class DistanceManager { }); } + // Paper start - Chunk priority + public static final int PRIORITY_TICKET_LEVEL = ChunkMap.MAX_CHUNK_DISTANCE; + public static final int URGENT_PRIORITY = 29; + public boolean delayDistanceManagerTick = false; + public boolean markUrgent(ChunkPos coords) { + return addPriorityTicket(coords, TicketType.URGENT, URGENT_PRIORITY); + } + public boolean markHighPriority(ChunkPos coords, int priority) { + priority = Math.min(URGENT_PRIORITY - 1, Math.max(1, priority)); + return addPriorityTicket(coords, TicketType.PRIORITY, priority); + } + + public void markAreaHighPriority(ChunkPos center, int priority, int radius) { + delayDistanceManagerTick = true; + priority = Math.min(URGENT_PRIORITY - 1, Math.max(1, priority)); + int finalPriority = priority; + net.minecraft.server.MCUtil.getSpiralOutChunks(center.getWorldPosition(), radius).forEach(coords -> { + addPriorityTicket(coords, TicketType.PRIORITY, finalPriority); + }); + delayDistanceManagerTick = false; + chunkMap.level.getChunkSource().runDistanceManagerUpdates(); + } + + public void clearAreaPriorityTickets(ChunkPos center, int radius) { + delayDistanceManagerTick = true; + net.minecraft.server.MCUtil.getSpiralOutChunks(center.getWorldPosition(), radius).forEach(coords -> { + this.removeTicket(coords.toLong(), new Ticket(TicketType.PRIORITY, PRIORITY_TICKET_LEVEL, coords)); + }); + delayDistanceManagerTick = false; + chunkMap.level.getChunkSource().runDistanceManagerUpdates(); + } + + private boolean hasPlayerTicket(ChunkPos coords, int level) { + SortedArraySet> tickets = this.tickets.get(coords.toLong()); + if (tickets == null || tickets.isEmpty()) { + return false; + } + for (Ticket ticket : tickets) { + if (ticket.getType() == TicketType.PLAYER && ticket.getTicketLevel() == level) { + return true; + } + } + + return false; + } + + private boolean addPriorityTicket(ChunkPos coords, TicketType ticketType, int priority) { + org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::addPriorityTicket"); + long pair = coords.toLong(); + ChunkHolder chunk = chunkMap.getUpdatingChunkIfPresent(pair); + boolean needsTicket = chunkMap.playerViewDistanceNoTickMap.getObjectsInRange(pair) != null && !hasPlayerTicket(coords, 33); + + if (needsTicket) { + Ticket ticket = new Ticket<>(TicketType.PLAYER, 33, coords); + this.ticketsToRelease.add(pair); + addTicket(pair, ticket); + } + if ((chunk != null && chunk.isFullChunkReady())) { + if (needsTicket) { + chunkMap.level.getChunkSource().runDistanceManagerUpdates(); + } + return needsTicket; + } + + boolean success; + if (!(success = updatePriorityTicket(coords, ticketType, priority))) { + Ticket ticket = new Ticket(ticketType, PRIORITY_TICKET_LEVEL, coords); + ticket.priority = priority; + success = this.addTicket(pair, ticket); + } else { + if (chunk == null) { + chunk = chunkMap.getUpdatingChunkIfPresent(pair); + } + chunkMap.queueHolderUpdate(chunk); + } + + //chunkMap.world.getWorld().spawnParticle(priority <= 15 ? org.bukkit.Particle.EXPLOSION_HUGE : org.bukkit.Particle.EXPLOSION_NORMAL, chunkMap.world.getWorld().getPlayers(), null, coords.x << 4, 70, coords.z << 4, 2, 0, 0, 0, 1, null, true); + + chunkMap.level.getChunkSource().runDistanceManagerUpdates(); + + return success; + } + + private boolean updatePriorityTicket(ChunkPos coords, TicketType type, int priority) { + SortedArraySet> tickets = this.tickets.get(coords.toLong()); + if (tickets == null) { + return false; + } + for (Ticket ticket : tickets) { + if (ticket.getType() == type) { + // We only support increasing, not decreasing, too complicated + ticket.setCreatedTick(this.ticketTickCounter); + ticket.priority = Math.max(ticket.priority, priority); + return true; + } + } + + return false; + } + + public int getChunkPriority(ChunkPos coords) { + org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::getChunkPriority"); + SortedArraySet> tickets = this.tickets.get(coords.toLong()); + if (tickets == null) { + return 0; + } + for (Ticket ticket : tickets) { + if (ticket.getType() == TicketType.URGENT) { + return URGENT_PRIORITY; + } + } + for (Ticket ticket : tickets) { + if (ticket.getType() == TicketType.PRIORITY && ticket.priority > 0) { + return ticket.priority; + } + } + return 0; + } + + public void clearPriorityTickets(ChunkPos coords) { + org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::clearPriority"); + this.removeTicket(coords.toLong(), new Ticket(TicketType.PRIORITY, PRIORITY_TICKET_LEVEL, coords)); + } + + public void clearUrgent(ChunkPos coords) { + org.spigotmc.AsyncCatcher.catchOp("ChunkMapDistance::clearUrgent"); + this.removeTicket(coords.toLong(), new Ticket(TicketType.URGENT, PRIORITY_TICKET_LEVEL, coords)); + } + // Paper end + protected void updateChunkForced(ChunkPos pos, boolean forced) { Ticket ticket = new Ticket<>(TicketType.FORCED, 31, pos); @@ -516,41 +659,68 @@ public abstract class DistanceManager { public void updateViewDistance(int watchDistance) { ObjectIterator objectiterator = this.chunks.long2ByteEntrySet().iterator(); + // Paper start - set the view distance before scheduling chunk loads/unloads + int lastViewDistance = this.viewDistance; + this.viewDistance = watchDistance; + // Paper end while (objectiterator.hasNext()) { it.unimi.dsi.fastutil.longs.Long2ByteMap.Entry it_unimi_dsi_fastutil_longs_long2bytemap_entry = (it.unimi.dsi.fastutil.longs.Long2ByteMap.Entry) objectiterator.next(); byte b0 = it_unimi_dsi_fastutil_longs_long2bytemap_entry.getByteValue(); long j = it_unimi_dsi_fastutil_longs_long2bytemap_entry.getLongKey(); - this.onLevelChange(j, b0, this.haveTicketFor(b0), b0 <= watchDistance - 2); + this.onLevelChange(j, b0, b0 <= lastViewDistance - 2, this.haveTicketFor(b0)); // Paper } - this.viewDistance = watchDistance; + // this.viewDistance = watchDistance; // Paper - view distance is now set further up } private void onLevelChange(long pos, int distance, boolean oldWithinViewDistance, boolean withinViewDistance) { if (oldWithinViewDistance != withinViewDistance) { - Ticket ticket = new Ticket<>(TicketType.PLAYER, 33, new ChunkPos(pos)); // Paper - no-tick view distance + ChunkPos coords = new ChunkPos(pos); // Paper - reuse variable + Ticket ticket = new Ticket<>(TicketType.PLAYER, 33, coords); // Paper - no-tick view distance if (withinViewDistance) { + scheduleChunkLoad(pos, net.minecraft.server.MinecraftServer.currentTick, distance, (priority) -> { // Paper - smarter ticket delay based on frustum and distance + // Paper start - recheck its still valid if not cancel + if (!isChunkInRange(pos)) { + DistanceManager.this.ticketThrottlerReleaser.tell(ChunkTaskPriorityQueueSorter.release(() -> { + DistanceManager.this.mainThreadExecutor.execute(() -> { + DistanceManager.this.removeTicket(pos, ticket); + DistanceManager.this.clearPriorityTickets(coords); + }); + }, pos, false)); + return; + } + // abort early if we got a ticket already + if (hasPlayerTicket(coords, 33)) return; + // skip player ticket throttle for near chunks + if (priority <= 3) { + DistanceManager.this.addTicket(pos, ticket); + DistanceManager.this.ticketsToRelease.add(pos); + return; + } + // Paper end DistanceManager.this.ticketThrottlerInput.tell(ChunkTaskPriorityQueueSorter.message(() -> { DistanceManager.this.mainThreadExecutor.execute(() -> { - if (this.haveTicketFor(this.getLevel(pos))) { + if (isChunkInRange(pos)) { if (!hasPlayerTicket(coords, 33)) { // Paper - high priority might of already added it DistanceManager.this.addTicket(pos, ticket); DistanceManager.this.ticketsToRelease.add(pos); - } else { + }} else { // Paper DistanceManager.this.ticketThrottlerReleaser.tell(ChunkTaskPriorityQueueSorter.release(() -> { }, pos, false)); } }); }, pos, () -> { - return distance; + return Math.min(ChunkMap.MAX_CHUNK_DISTANCE, priority); // Paper - Chunk priority })); + }); // Paper } else { DistanceManager.this.ticketThrottlerReleaser.tell(ChunkTaskPriorityQueueSorter.release(() -> { DistanceManager.this.mainThreadExecutor.execute(() -> { DistanceManager.this.removeTicket(pos, ticket); + DistanceManager.this.clearPriorityTickets(coords); // Paper - Chunk priority }); }, pos, true)); } @@ -592,5 +762,100 @@ public abstract class DistanceManager { private boolean haveTicketFor(int distance) { return distance <= this.viewDistance - 2; } + + // Paper start - smart scheduling of player tickets + private boolean isChunkInRange(long i) { + return this.haveTicketFor(this.getLevel(i)); + } + public void scheduleChunkLoad(long i, long startTick, int initialDistance, java.util.function.Consumer task) { + long elapsed = net.minecraft.server.MinecraftServer.currentTick - startTick; + ChunkPos chunkPos = new ChunkPos(i); + ChunkHolder updatingChunk = chunkMap.getUpdatingChunkIfPresent(i); + if ((updatingChunk != null && updatingChunk.isFullChunkReady()) || !isChunkInRange(i) || getChunkPriority(chunkPos) > 0) { // Copied from above + // no longer needed + task.accept(1); + return; + } + + int desireDelay = 0; + double minDist = Double.MAX_VALUE; + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet players = chunkMap.playerViewDistanceNoTickMap.getObjectsInRange(i); + if (elapsed == 0 && initialDistance <= 4) { + // Aim for no delay on initial 6 chunk radius tickets save on performance of the below code to only > 6 + minDist = initialDistance; + } else if (players != null) { + Object[] backingSet = players.getBackingSet(); + + net.minecraft.core.BlockPos blockPos = chunkPos.getWorldPosition(); + + boolean isFront = false; + net.minecraft.core.BlockPos.MutableBlockPos pos = new net.minecraft.core.BlockPos.MutableBlockPos(); + for (int index = 0, len = backingSet.length; index < len; ++index) { + if (!(backingSet[index] instanceof ServerPlayer)) { + continue; + } + ServerPlayer player = (ServerPlayer) backingSet[index]; + + ChunkPos pointInFront = player.getChunkInFront(5); + pos.set(pointInFront.x << 4, 0, pointInFront.z << 4); + double frontDist = net.minecraft.server.MCUtil.distanceSq(pos, blockPos); + + pos.set(player.getX(), 0, player.getZ()); + double center = net.minecraft.server.MCUtil.distanceSq(pos, blockPos); + + double dist = Math.min(frontDist, center); + if (!isFront) { + ChunkPos pointInBack = player.getChunkInFront(-7); + pos.set(pointInBack.x << 4, 0, pointInBack.z << 4); + double backDist = net.minecraft.server.MCUtil.distanceSq(pos, blockPos); + if (frontDist < backDist) { + isFront = true; + } + } + if (dist < minDist) { + minDist = dist; + } + } + if (minDist == Double.MAX_VALUE) { + minDist = 15; + } else { + minDist = Math.sqrt(minDist) / 16; + } + if (minDist > 4) { + int desiredTimeDelayMax = isFront ? + (minDist < 10 ? 7 : 15) : // Front + (minDist < 10 ? 15 : 45); // Back + desireDelay += (desiredTimeDelayMax * 20) * (minDist / 32); + } + } else { + minDist = initialDistance; + desireDelay = 1; + } + long delay = desireDelay - elapsed; + if (delay <= 0 && minDist > 4 && minDist < Double.MAX_VALUE) { + boolean hasAnyNeighbor = false; + for (int x = -1; x <= 1; x++) { + for (int z = -1; z <= 1; z++) { + if (x == 0 && z == 0) continue; + long pair = ChunkPos.asLong(chunkPos.x + x, chunkPos.z + z); + ChunkHolder neighbor = chunkMap.getUpdatingChunkIfPresent(pair); + ChunkStatus current = neighbor != null ? neighbor.getChunkHolderStatus() : null; + if (current != null && current.isOrAfter(ChunkStatus.LIGHT)) { + hasAnyNeighbor = true; + } + } + } + if (!hasAnyNeighbor) { + delay += 20; + } + } + if (delay <= 0) { + task.accept((int) minDist); + } else { + int taskDelay = (int) Math.min(delay, minDist >= 10 ? 40 : (minDist < 6 ? 5 : 20)); + net.minecraft.server.MCUtil.scheduleTask(taskDelay, () -> scheduleChunkLoad(i, startTick, initialDistance, task), "Player Ticket Delayer"); + } + } + // Paper end } } diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java index 6d947343b6ff2f86a23ea669e371d5ecd9e903c0..d6f0c82b503a0fa73f6589fc4c331d27fbe27638 100644 --- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java +++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java @@ -449,6 +449,26 @@ public class ServerChunkCache extends ChunkSource { public void removeTicketAtLevel(TicketType ticketType, ChunkPos chunkPos, int ticketLevel, T identifier) { this.distanceManager.removeTicketAtLevel(ticketType, chunkPos, ticketLevel, identifier); } + + public boolean markUrgent(ChunkPos coords) { + return this.distanceManager.markUrgent(coords); + } + + public boolean markHighPriority(ChunkPos coords, int priority) { + return this.distanceManager.markHighPriority(coords, priority); + } + + public void markAreaHighPriority(ChunkPos center, int priority, int radius) { + this.distanceManager.markAreaHighPriority(center, priority, radius); + } + + public void clearAreaPriorityTickets(ChunkPos center, int radius) { + this.distanceManager.clearAreaPriorityTickets(center, radius); + } + + public void clearPriorityTickets(ChunkPos coords) { + this.distanceManager.clearPriorityTickets(coords); + } // Paper end - async chunk io @Nullable @@ -489,6 +509,8 @@ public class ServerChunkCache extends ChunkSource { Objects.requireNonNull(completablefuture); if (!completablefuture.isDone()) { // Paper // Paper start - async chunk io/loading + ChunkPos pair = new ChunkPos(x1, z1); // Paper - Chunk priority + this.distanceManager.markUrgent(pair); // Paper - Chunk priority this.level.asyncChunkTaskManager.raisePriority(x1, z1, com.destroystokyo.paper.io.PrioritizedTaskQueue.HIGHEST_PRIORITY); com.destroystokyo.paper.io.chunk.ChunkTaskManager.pushChunkWait(this.level, x1, z1); // Paper end @@ -497,6 +519,8 @@ public class ServerChunkCache extends ChunkSource { chunkproviderserver_a.managedBlock(completablefuture::isDone); com.destroystokyo.paper.io.chunk.ChunkTaskManager.popChunkWait(); // Paper - async chunk debug this.level.timings.syncChunkLoad.stopTiming(); // Paper + this.distanceManager.clearPriorityTickets(pair); // Paper - Chunk priority + this.distanceManager.clearUrgent(pair); // Paper - Chunk priority } // Paper ichunkaccess = (ChunkAccess) ((Either) completablefuture.join()).map((ichunkaccess1) -> { return ichunkaccess1; @@ -570,10 +594,12 @@ public class ServerChunkCache extends ChunkSource { if (flag && !currentlyUnloading) { // CraftBukkit end this.distanceManager.addTicket(TicketType.UNKNOWN, chunkcoordintpair, l, chunkcoordintpair); + if (isUrgent) this.distanceManager.markUrgent(chunkcoordintpair); // Paper - Chunk priority if (this.chunkAbsent(playerchunk, l)) { ProfilerFiller gameprofilerfiller = this.level.getProfiler(); gameprofilerfiller.push("chunkLoad"); + distanceManager.delayDistanceManagerTick = false; // Paper - Chunk priority - ensure this is never false this.runDistanceManagerUpdates(); playerchunk = this.getVisibleChunkIfPresent(k); gameprofilerfiller.pop(); @@ -582,8 +608,13 @@ public class ServerChunkCache extends ChunkSource { } } } - - return this.chunkAbsent(playerchunk, l) ? ChunkHolder.UNLOADED_CHUNK_FUTURE : playerchunk.getOrScheduleFuture(chunkstatus, this.chunkMap); + // Paper start - Chunk priority + CompletableFuture> future = this.chunkAbsent(playerchunk, l) ? ChunkHolder.UNLOADED_CHUNK_FUTURE : playerchunk.getOrScheduleFuture(chunkstatus, this.chunkMap); + if (isUrgent) { + future.thenAccept(either -> this.distanceManager.clearUrgent(chunkcoordintpair)); + } + return future; + // Paper end } private boolean chunkAbsent(@Nullable ChunkHolder holder, int maxLevel) { @@ -635,6 +666,7 @@ public class ServerChunkCache extends ChunkSource { } public boolean runDistanceManagerUpdates() { + if (distanceManager.delayDistanceManagerTick) return false; // Paper - Chunk priority boolean flag = this.distanceManager.runAllUpdates(this.chunkMap); boolean flag1 = this.chunkMap.promoteChunkMap(); diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java index cf09bd17b9d2be04f79edef6debdd815b5f7f86c..6cf83d9b1e43ade17cb67843dbdc11937eda1e08 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayer.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java @@ -189,6 +189,14 @@ public class ServerPlayer extends Player { private int lastRecordedArmor = Integer.MIN_VALUE; private int lastRecordedLevel = Integer.MIN_VALUE; private int lastRecordedExperience = Integer.MIN_VALUE; + // Paper start - Chunk priority + public long lastHighPriorityChecked; + public void forceCheckHighPriority() { + lastHighPriorityChecked = -1; + getLevel().getChunkSource().chunkMap.checkHighPriorityChunks(this); + } + public boolean isRealPlayer; + // Paper end private float lastSentHealth = -1.0E8F; private int lastSentFood = -99999999; private boolean lastFoodSaturationZero = true; @@ -325,6 +333,21 @@ public class ServerPlayer extends Player { this.maxHealthCache = this.getMaxHealth(); this.cachedSingleMobDistanceMap = new com.destroystokyo.paper.util.PooledHashSets.PooledObjectLinkedOpenHashSet<>(this); // Paper } + // Paper start - Chunk priority + public BlockPos getPointInFront(double inFront) { + double rads = Math.toRadians(net.minecraft.server.MCUtil.normalizeYaw(this.yRot + 90)); // MC rotates yaw 90 for some odd reason + final double x = getX() + inFront * Math.cos(rads); + final double z = getZ() + inFront * Math.sin(rads); + return new BlockPos(x, getY(), z); + } + + public ChunkPos getChunkInFront(double inFront) { + double rads = Math.toRadians(net.minecraft.server.MCUtil.normalizeYaw(this.yRot + 90)); // MC rotates yaw 90 for some odd reason + final double x = getX() + (inFront * 16) * Math.cos(rads); + final double z = getZ() + (inFront * 16) * Math.sin(rads); + return new ChunkPos(Mth.floor(x) >> 4, Mth.floor(z) >> 4); + } + // Paper end // Yes, this doesn't match Vanilla, but it's the best we can do for now. // If this is an issue, PRs are welcome @@ -646,6 +669,7 @@ public class ServerPlayer extends Player { if (valid && !this.isSpectator() || !this.touchingUnloadedChunk()) { // Paper - don't tick dead players that are not in the world currently (pending respawn) super.tick(); } + if (valid && isAlive() && connection != null) ((ServerLevel)level).getChunkSource().chunkMap.checkHighPriorityChunks(this); // Paper - Chunk priority for (int i = 0; i < this.getInventory().getContainerSize(); ++i) { ItemStack itemstack = this.getInventory().getItem(i); diff --git a/src/main/java/net/minecraft/server/level/Ticket.java b/src/main/java/net/minecraft/server/level/Ticket.java index f1128f0d4a9a0241ac6c9bc18dd13b431c616bb1..2b2b7851d5f68bcdb41d58bcc64740ba58bf1ef4 100644 --- a/src/main/java/net/minecraft/server/level/Ticket.java +++ b/src/main/java/net/minecraft/server/level/Ticket.java @@ -8,6 +8,7 @@ public final class Ticket implements Comparable> { public final T key; public long createdTick; public long delayUnloadBy; // Paper + public int priority; // Paper - Chunk priority protected Ticket(TicketType type, int level, T argument) { this.type = type; diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java index 8770fe0db46b01e8b608637df4f1a669a3f4cdde..3c1698ba0d3bc412ab957777d9b5211dbc555208 100644 --- a/src/main/java/net/minecraft/server/level/TicketType.java +++ b/src/main/java/net/minecraft/server/level/TicketType.java @@ -9,6 +9,8 @@ import net.minecraft.world.level.ChunkPos; public class TicketType { public static final TicketType FUTURE_AWAIT = create("future_await", Long::compareTo); // Paper public static final TicketType ASYNC_LOAD = create("async_load", Long::compareTo); // Paper + public static final TicketType PRIORITY = create("priority", Comparator.comparingLong(ChunkPos::toLong), 300); // Paper + public static final TicketType URGENT = create("urgent", Comparator.comparingLong(ChunkPos::toLong), 300); // Paper private final String name; private final Comparator comparator; diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java index ae3465cd5be426f3bddc3552b6aad6cad8c8ee25..91df850e66aa14b13a679357e02e2f74dc653628 100644 --- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java @@ -1545,6 +1545,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser this.awaitingTeleportTime = this.tickCount; this.player.absMoveTo(d0, d1, d2, f, f1); + this.player.forceCheckHighPriority(); // Paper this.player.connection.send(new ClientboundPlayerPositionPacket(d0 - d3, d1 - d4, d2 - d5, f - f2, f1 - f3, set, this.awaitingTeleport, flag)); } diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java index e0c9857a922d8a3f421d7df0f056b82c4775390e..fefd3e4b7d9b2ec77dd2bc26298c3732d651df32 100644 --- a/src/main/java/net/minecraft/server/players/PlayerList.java +++ b/src/main/java/net/minecraft/server/players/PlayerList.java @@ -175,6 +175,7 @@ public abstract class PlayerList { } public void placeNewPlayer(Connection connection, ServerPlayer player) { + player.isRealPlayer = true; // Paper - Chunk priority ServerPlayer prev = pendingPlayers.put(player.getUUID(), player);// Paper if (prev != null) { disconnectPendingPlayer(prev); @@ -286,8 +287,8 @@ public abstract class PlayerList { net.minecraft.server.level.ChunkMap playerChunkMap = worldserver1.getChunkSource().chunkMap; net.minecraft.server.level.DistanceManager distanceManager = playerChunkMap.distanceManager; distanceManager.addTicketAtLevel(net.minecraft.server.level.TicketType.LOGIN, pos, 31, pos.toLong()); - worldserver1.getChunkSource().runDistanceManagerUpdates(); - worldserver1.getChunkSource().getChunkAtAsynchronously(chunkX, chunkZ, true, true).thenApply(chunk -> { + worldserver1.getChunkSource().markAreaHighPriority(pos, 28, 3); // Paper - Chunk priority + worldserver1.getChunkSource().getChunkAtAsynchronously(chunkX, chunkZ, true, false).thenApply(chunk -> { // Paper - Chunk priority net.minecraft.server.level.ChunkHolder updatingChunk = playerChunkMap.getUpdatingChunkIfPresent(pos.toLong()); if (updatingChunk != null) { return updatingChunk.getEntityTickingChunkFuture(); @@ -893,6 +894,7 @@ public abstract class PlayerList { // CraftBukkit end worldserver1.getChunkSource().addRegionTicket(net.minecraft.server.level.TicketType.POST_TELEPORT, new net.minecraft.world.level.ChunkPos(location.getBlockX() >> 4, location.getBlockZ() >> 4), 1, entityplayer.getId()); // Paper + entityplayer1.forceCheckHighPriority(); // Player - Chunk priority while (avoidSuffocation && !worldserver1.noCollision(entityplayer1) && entityplayer1.getY() < (double) worldserver1.getMaxBuildHeight()) { entityplayer1.setPos(entityplayer1.getX(), entityplayer1.getY() + 1.0D, entityplayer1.getZ()); } diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java index 73fc8ebd2ec207db2efc545f6bb304d53880b78f..e1b6a390105232500d7146cc456bf30912934904 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java @@ -223,7 +223,7 @@ public abstract class Entity implements Nameable, EntityAccess, CommandSource, n private Vec3 position; private BlockPos blockPosition; private Vec3 deltaMovement; - private float yRot; + public float yRot; // Paper - private->public private float xRot; public float yRotO; public float xRotO; 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 a84b75a53a0324fab9aeb9b80bf74eb0a84ecd2e..d580f0375cce5e995c024f1b0cd4843b5718121c 100644 --- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java @@ -192,7 +192,7 @@ public class LevelChunk implements ChunkAccess { return NEIGHBOUR_CACHE_RADIUS; } - boolean loadedTicketLevel; + boolean loadedTicketLevel; public final boolean wasLoadCallbackInvoked() { return this.loadedTicketLevel; } // Paper - public accessor private long neighbourChunksLoadedBitset; private final LevelChunk[] loadedNeighbourChunks = new LevelChunk[(NEIGHBOUR_CACHE_RADIUS * 2 + 1) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)]; @@ -794,6 +794,7 @@ public class LevelChunk implements ChunkAccess { // CraftBukkit start public void loadCallback() { + if (this.loadedTicketLevel) { LOGGER.error("Double calling chunk load!", new Throwable()); } // Paper // Paper start - neighbour cache int chunkX = this.chunkPos.x; int chunkZ = this.chunkPos.z; @@ -848,6 +849,7 @@ public class LevelChunk implements ChunkAccess { } public void unloadCallback() { + if (!this.loadedTicketLevel) { LOGGER.error("Double calling chunk unload!", new Throwable()); } // Paper org.bukkit.Server server = this.level.getCraftServer(); org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(this.bukkitChunk, this.isUnsaved()); server.getPluginManager().callEvent(unloadEvent); diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java index 496bb91da1138fb6a4e004da1eab43e588c82592..ff1cb5f679f57988eb30b77fbcc5de2699a90dca 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java @@ -1912,6 +1912,12 @@ public class CraftWorld extends CraftRegionAccessor implements World { return future; } + // Paper start - Chunk priority + if (!urgent) { + // If not urgent, at least use a slightly boosted priority + world.getChunkSource().markHighPriority(new ChunkPos(x, z), 1); + } + // Paper end return this.world.getChunkSource().getChunkAtAsynchronously(x, z, gen, urgent).thenComposeAsync((either) -> { net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk) either.left().orElse(null); if (chunk != null) addTicket(x, z); // Paper diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java index a3951039eeb1f5d3529213ca02d11c97d499be38..9ce4ebc9a0ddeefdc9fb3a7bd91be77aeb075011 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java @@ -895,6 +895,16 @@ public class CraftPlayer extends CraftHumanEntity implements Player { throw new UnsupportedOperationException("Cannot set rotation of players. Consider teleporting instead."); } + // Paper start - Chunk priority + @Override + public java.util.concurrent.CompletableFuture teleportAsync(Location loc, @javax.annotation.Nonnull PlayerTeleportEvent.TeleportCause cause) { + ((CraftWorld)loc.getWorld()).getHandle().getChunkSource().markAreaHighPriority( + new net.minecraft.world.level.ChunkPos(net.minecraft.util.Mth.floor(loc.getX()) >> 4, + net.minecraft.util.Mth.floor(loc.getZ()) >> 4), 28, 3); // Load area high priority + return super.teleportAsync(loc, cause); + } + // Paper end + @Override public boolean teleport(Location location, PlayerTeleportEvent.TeleportCause cause) { Preconditions.checkArgument(location != null, "location");