From 0b19b8f13de4cc4c5e40e9b31caf6bf6d36e7965 Mon Sep 17 00:00:00 2001 From: NONPLAYT <76615486+NONPLAYT@users.noreply.github.com> Date: Mon, 3 Mar 2025 19:36:45 +0300 Subject: [PATCH] Async Chunk Sending --- .../features/0038-Async-Chunk-Sending.patch | 60 +++++++++++++++++++ .../org/bxteam/divinemc/DivineConfig.java | 22 +++++++ .../server/chunk/ChunkSendingExecutor.java | 47 +++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 divinemc-server/minecraft-patches/features/0038-Async-Chunk-Sending.patch create mode 100644 divinemc-server/src/main/java/org/bxteam/divinemc/server/chunk/ChunkSendingExecutor.java diff --git a/divinemc-server/minecraft-patches/features/0038-Async-Chunk-Sending.patch b/divinemc-server/minecraft-patches/features/0038-Async-Chunk-Sending.patch new file mode 100644 index 0000000..b2033f6 --- /dev/null +++ b/divinemc-server/minecraft-patches/features/0038-Async-Chunk-Sending.patch @@ -0,0 +1,60 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: NONPLAYT <76615486+NONPLAYT@users.noreply.github.com> +Date: Mon, 3 Mar 2025 19:29:13 +0300 +Subject: [PATCH] Async Chunk Sending + + +diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +index f81cc357618c70f2fcf0bc24b0b25be566ffffcc..287bc85b80059fe5711cec25220d2d7bab472a16 100644 +--- a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +@@ -823,8 +823,11 @@ public final class RegionizedPlayerChunkLoader { + } + + // try to pull sending chunks +- final long maxSends = Math.max(0L, Math.min(MAX_RATE, Integer.MAX_VALUE)); // note: no logic to track concurrent sends +- final int maxSendsThisTick = Math.min((int)this.chunkSendLimiter.takeAllocation(time, sendRate, maxSends), this.sendQueue.size()); ++ // DivineMC start - Async Chunk Sending ++ final int maxSendsThisTick = org.bxteam.divinemc.DivineConfig.asyncChunkSendingRateLimitChunkSends == -1 ++ ? this.sendQueue.size() ++ : org.bxteam.divinemc.DivineConfig.asyncChunkSendingRateLimitChunkSends; ++ // DivineMC end - Async Chunk Sending + // we do not return sends that we took from the allocation back because we want to limit the max send rate, not target it + for (int i = 0; i < maxSendsThisTick; ++i) { + final long pendingSend = this.sendQueue.firstLong(); +diff --git a/net/minecraft/server/network/PlayerChunkSender.java b/net/minecraft/server/network/PlayerChunkSender.java +index 14878690a88fd4de3e2c127086607e6c819c636c..4723ce85ebcd3740245607348a525f292ac1e2f3 100644 +--- a/net/minecraft/server/network/PlayerChunkSender.java ++++ b/net/minecraft/server/network/PlayerChunkSender.java +@@ -80,16 +80,21 @@ public class PlayerChunkSender { + + // Paper start - Anti-Xray + public static void sendChunk(ServerGamePacketListenerImpl packetListener, ServerLevel level, LevelChunk chunk) { +- final boolean shouldModify = level.chunkPacketBlockController.shouldModify(packetListener.player, chunk); +- packetListener.send(new ClientboundLevelChunkWithLightPacket(chunk, level.getLightEngine(), null, null, shouldModify)); +- // Paper end - Anti-Xray +- // Paper start - PlayerChunkLoadEvent +- if (io.papermc.paper.event.packet.PlayerChunkLoadEvent.getHandlerList().getRegisteredListeners().length > 0) { +- new io.papermc.paper.event.packet.PlayerChunkLoadEvent(new org.bukkit.craftbukkit.CraftChunk(chunk), packetListener.getPlayer().getBukkitEntity()).callEvent(); +- } +- // Paper end - PlayerChunkLoadEvent +- ChunkPos pos = chunk.getPos(); +- DebugPackets.sendPoiPacketsForChunk(level, pos); ++ // DivineMC start - Async Chunk Sending ++ org.bxteam.divinemc.server.chunk.ChunkSendingExecutor.execute(() -> { ++ level.getChunk(chunk.getPos().x, chunk.getPos().z, net.minecraft.world.level.chunk.status.ChunkStatus.FULL); // DivineMC - ensure chunk is loaded ++ final boolean shouldModify = level.chunkPacketBlockController.shouldModify(packetListener.player, chunk); ++ packetListener.send(new ClientboundLevelChunkWithLightPacket(chunk, level.getLightEngine(), null, null, shouldModify)); ++ // Paper end - Anti-Xray ++ // Paper start - PlayerChunkLoadEvent ++ if (io.papermc.paper.event.packet.PlayerChunkLoadEvent.getHandlerList().getRegisteredListeners().length > 0) { ++ new io.papermc.paper.event.packet.PlayerChunkLoadEvent(new org.bukkit.craftbukkit.CraftChunk(chunk), packetListener.getPlayer().getBukkitEntity()).callEvent(); ++ } ++ // Paper end - PlayerChunkLoadEvent ++ ChunkPos pos = chunk.getPos(); ++ DebugPackets.sendPoiPacketsForChunk(level, pos); ++ }, level); ++ // DivineMC end - Async Chunk Sending + } + + private List collectChunksToSend(ChunkMap chunkMap, ChunkPos chunkPos) { diff --git a/divinemc-server/src/main/java/org/bxteam/divinemc/DivineConfig.java b/divinemc-server/src/main/java/org/bxteam/divinemc/DivineConfig.java index b17af25..8cbe76b 100644 --- a/divinemc-server/src/main/java/org/bxteam/divinemc/DivineConfig.java +++ b/divinemc-server/src/main/java/org/bxteam/divinemc/DivineConfig.java @@ -235,6 +235,28 @@ public class DivineConfig { "modpacks where many structure mods are using very high weight values in their template pools."); } + public static boolean asyncChunkSendingEnabled = true; + public static int asyncChunkSendingThreadCount = 1; + public static boolean asyncChunkSendingUseVirtualThreads = false; + public static int asyncChunkSendingRateLimitChunkSends = 50; + private void asyncChunkSending() { + asyncChunkSendingEnabled = getBoolean("settings.async-chunk-sending.enable", asyncChunkSendingEnabled, + "Enables chunk sending runs off-main thread."); + + asyncChunkSendingThreadCount = getInt("settings.async-chunk-sending.thread-count", asyncChunkSendingThreadCount, + "Amount of threads to use for async chunk sending. (If use-virtual-threads is enabled, this will be ignored)"); + + asyncChunkSendingUseVirtualThreads = getBoolean("settings.async-chunk-sending.use-virtual-threads", asyncChunkSendingUseVirtualThreads, + "Similar to the 'virtual-thread' options. This will use the virtual thread executor for chunk sending."); + + asyncChunkSendingRateLimitChunkSends = getInt("settings.async-chunk-sending.rate-limit-chunk-sends", asyncChunkSendingRateLimitChunkSends, + "DivineMC have optimization patches that speed ups world generation,", + "so chunk loading/generating may be faster and with this, server can spam a ton of chunk packets to the client on server join.", + "This setting will limit the amount of chunk packets sent to the client per tick, allowing a smoother ping when sending chunks on join", + "", + "Set to -1 to disable rate limiting (not recommended)"); + } + public static boolean enableRegionizedChunkTicking = false; public static int regionizedChunkTickingExecutorThreadCount = 4; public static int regionizedChunkTickingExecutorThreadPriority = Thread.NORM_PRIORITY; diff --git a/divinemc-server/src/main/java/org/bxteam/divinemc/server/chunk/ChunkSendingExecutor.java b/divinemc-server/src/main/java/org/bxteam/divinemc/server/chunk/ChunkSendingExecutor.java new file mode 100644 index 0000000..50c9aff --- /dev/null +++ b/divinemc-server/src/main/java/org/bxteam/divinemc/server/chunk/ChunkSendingExecutor.java @@ -0,0 +1,47 @@ +package org.bxteam.divinemc.server.chunk; + +import ca.spottedleaf.moonrise.common.util.TickThread; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import org.bxteam.divinemc.DivineConfig; +import org.bxteam.divinemc.util.NamedAgnosticThreadFactory; +import org.jetbrains.annotations.NotNull; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class ChunkSendingExecutor { + private static final ExecutorService SERVICE = DivineConfig.asyncChunkSendingEnabled + ? DivineConfig.asyncChunkSendingUseVirtualThreads ? + Executors.newVirtualThreadPerTaskExecutor() : + Executors.newFixedThreadPool( + DivineConfig.asyncChunkSendingThreadCount, + new NamedAgnosticThreadFactory<>("chunk_sending", TickThread::new, Thread.NORM_PRIORITY) + ) + : null; + + public static void execute(Runnable runnable, ServerLevel level) { + runnable = wrapRunnable(runnable, level); + if (DivineConfig.asyncChunkSendingEnabled) { + SERVICE.submit(runnable); + } else { + runnable.run(); + } + } + + private static @NotNull Runnable wrapRunnable(Runnable runnable, final ServerLevel level) { + return () -> { + try { + runnable.run(); + } catch (Throwable throwable) { + MinecraftServer.LOGGER.warn("Failed to send chunk data! Retrying..."); + level.getServer().scheduleOnMain(() -> { + try { + runnable.run(); + } catch (Throwable failed) { + MinecraftServer.LOGGER.error("Failed to send chunk data! (2nd attempt). Logging error log", failed); + } + }); + } + }; + } +}