From a53ddc7f8cf51634c1e690bc4d286a7859891e60 Mon Sep 17 00:00:00 2001 From: hayanesuru Date: Wed, 7 May 2025 02:28:16 +0900 Subject: [PATCH] update async chunk send (#307) * cleanup * Async Chunk Send --------- Co-authored-by: Taiyou06 --- .../features/0132-Async-chunk-send.patch | 172 ++++++++++++++++++ .../features/0132-Async-chunk-sending.patch | 108 ----------- .../features/0036-async-chunk-send.patch | 19 ++ .../dreeam/leaf/async/AsyncChunkSending.java | 9 - .../leaf/async/chunk/AsyncChunkSend.java | 25 +++ .../async/chunk/AsyncChunkSendThread.java | 7 + .../config/modules/async/AsyncChunkSend.java | 7 + 7 files changed, 230 insertions(+), 117 deletions(-) create mode 100644 leaf-server/minecraft-patches/features/0132-Async-chunk-send.patch delete mode 100644 leaf-server/minecraft-patches/features/0132-Async-chunk-sending.patch create mode 100644 leaf-server/paper-patches/features/0036-async-chunk-send.patch delete mode 100644 leaf-server/src/main/java/org/dreeam/leaf/async/AsyncChunkSending.java create mode 100644 leaf-server/src/main/java/org/dreeam/leaf/async/chunk/AsyncChunkSend.java create mode 100644 leaf-server/src/main/java/org/dreeam/leaf/async/chunk/AsyncChunkSendThread.java diff --git a/leaf-server/minecraft-patches/features/0132-Async-chunk-send.patch b/leaf-server/minecraft-patches/features/0132-Async-chunk-send.patch new file mode 100644 index 00000000..196e225f --- /dev/null +++ b/leaf-server/minecraft-patches/features/0132-Async-chunk-send.patch @@ -0,0 +1,172 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Taiyou06 +Date: Sun, 2 Mar 2025 21:23:20 +0100 +Subject: [PATCH] Async chunk send + + +diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +index a35e9fae8f8da0c42f0616c4f78dc396492673aa..2fef24acfaceab21aad6be50e6b29701fa460bfb 100644 +--- a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +@@ -438,7 +438,15 @@ public final class RegionizedPlayerChunkLoader { + // Note: drop isAlive() check so that chunks properly unload client-side when the player dies + ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager + .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$removeReceivedChunk(this.player); +- this.player.connection.send(new ClientboundForgetLevelChunkPacket(new ChunkPos(chunkX, chunkZ))); ++ // Leaf start - Async chunk send ++ if (org.dreeam.leaf.config.modules.async.AsyncChunkSend.enabled) { ++ org.dreeam.leaf.async.chunk.AsyncChunkSend.POOL.execute( ++ () -> this.player.connection.send(new ClientboundForgetLevelChunkPacket(new ChunkPos(chunkX, chunkZ))) ++ ); ++ } else { ++ this.player.connection.send(new ClientboundForgetLevelChunkPacket(new ChunkPos(chunkX, chunkZ))); ++ } ++ // Leaf end - Async chunk send + // Paper start - PlayerChunkUnloadEvent + if (io.papermc.paper.event.packet.PlayerChunkUnloadEvent.getHandlerList().getRegisteredListeners().length > 0) { + new io.papermc.paper.event.packet.PlayerChunkUnloadEvent(player.getBukkitEntity().getWorld().getChunkAt(new ChunkPos(chunkX, chunkZ).longKey), player.getBukkitEntity()).callEvent(); +diff --git a/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java b/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java +index 9e321ef1c3d5803519b243685f4ee598dc0cf640..641b9c8bcf92d01cf40df8fb6d658b9cec37c6bc 100644 +--- a/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java ++++ b/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java +@@ -73,6 +73,44 @@ public class ClientboundLevelChunkPacketData { + this.blockEntitiesData.add(ClientboundLevelChunkPacketData.BlockEntityInfo.create(entryx.getValue())); + } + } ++ // Leaf start - Async chunk send ++ public ClientboundLevelChunkPacketData(LevelChunk levelChunk, io.papermc.paper.antixray.ChunkPacketInfo chunkPacketInfo, BlockEntity[] blockEntities, CompoundTag heightmaps) { ++ this.heightmaps = heightmaps; ++ ++ if (Thread.currentThread() instanceof org.dreeam.leaf.async.chunk.AsyncChunkSendThread) { ++ var buffer = new io.netty.buffer.UnpooledByteBufAllocator(false).buffer(calculateChunkSize(levelChunk)); ++ extractChunkData(new FriendlyByteBuf(buffer), levelChunk, chunkPacketInfo); ++ var array = it.unimi.dsi.fastutil.bytes.ByteArrays.trim(buffer.array(), buffer.writerIndex()); ++ if (chunkPacketInfo != null) { ++ chunkPacketInfo.setBuffer(array); ++ } ++ this.buffer = array; ++ } else { ++ this.buffer = new byte[calculateChunkSize(levelChunk)]; ++ // Paper start - Anti-Xray - Add chunk packet info ++ if (chunkPacketInfo != null) { ++ chunkPacketInfo.setBuffer(this.buffer); ++ } ++ extractChunkData(new FriendlyByteBuf(this.getWriteBuffer()), levelChunk, chunkPacketInfo); ++ } ++ ++ this.blockEntitiesData = Lists.newArrayList(); ++ int totalTileEntities = 0; // Paper - Handle oversized block entities in chunks ++ ++ for (BlockEntity blockEntity : blockEntities) { ++ // Paper start - Handle oversized block entities in chunks ++ if (++totalTileEntities > BLOCK_ENTITY_LIMIT) { ++ net.minecraft.network.protocol.Packet packet = blockEntity.getUpdatePacket(); ++ if (packet != null) { ++ this.extraPackets.add(packet); ++ continue; ++ } ++ } ++ // Paper end - Handle oversized block entities in chunks ++ this.blockEntitiesData.add(ClientboundLevelChunkPacketData.BlockEntityInfo.create(blockEntity)); ++ } ++ } ++ // Leaf end - Async chunk send + + public ClientboundLevelChunkPacketData(RegistryFriendlyByteBuf buffer, int x, int z) { + this.heightmaps = buffer.readNbt(); +diff --git a/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java b/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java +index 8578d1f78ddd1bb75f3230f04bfaa35af9f5f822..6878cb15fdec7d2438e87ed7dc5fc50b2edbda9f 100644 +--- a/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java ++++ b/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java +@@ -44,6 +44,17 @@ public class ClientboundLevelChunkWithLightPacket implements Packet chunkPacketInfo = modifyBlocks ? chunk.getLevel().chunkPacketBlockController.getChunkPacketInfo(this, chunk) : null; // Paper - Ant-Xray ++ this.chunkData = new ClientboundLevelChunkPacketData(chunk, chunkPacketInfo, blockEntities, heightmaps); // Paper - Anti-Xray ++ this.lightData = new ClientboundLightUpdatePacketData(pos, lightEngine, skyLight, blockLight); ++ chunk.getLevel().chunkPacketBlockController.modifyBlocks(this, chunkPacketInfo); // Paper - Anti-Xray - Modify blocks ++ } ++ // Leaf end - Async chunk send + + private ClientboundLevelChunkWithLightPacket(RegistryFriendlyByteBuf buffer) { + this.x = buffer.readInt(); +diff --git a/net/minecraft/server/network/PlayerChunkSender.java b/net/minecraft/server/network/PlayerChunkSender.java +index 14878690a88fd4de3e2c127086607e6c819c636c..64a8b50bfac66f75d8c87d9e6e4000dc621437dd 100644 +--- a/net/minecraft/server/network/PlayerChunkSender.java ++++ b/net/minecraft/server/network/PlayerChunkSender.java +@@ -64,13 +64,29 @@ public class PlayerChunkSender { + if (!list.isEmpty()) { + ServerGamePacketListenerImpl serverGamePacketListenerImpl = player.connection; + this.unacknowledgedBatches++; +- serverGamePacketListenerImpl.send(ClientboundChunkBatchStartPacket.INSTANCE); ++ // Leaf start - Async chunk send ++ if (org.dreeam.leaf.config.modules.async.AsyncChunkSend.enabled) { ++ org.dreeam.leaf.async.chunk.AsyncChunkSend.POOL.execute( ++ () -> serverGamePacketListenerImpl.send(ClientboundChunkBatchStartPacket.INSTANCE) ++ ); ++ } else { ++ serverGamePacketListenerImpl.send(ClientboundChunkBatchStartPacket.INSTANCE); ++ } ++ // Leaf end - Async chunk send + + for (LevelChunk levelChunk : list) { + sendChunk(serverGamePacketListenerImpl, serverLevel, levelChunk); + } + +- serverGamePacketListenerImpl.send(new ClientboundChunkBatchFinishedPacket(list.size())); ++ // Leaf start - Async chunk send ++ if (org.dreeam.leaf.config.modules.async.AsyncChunkSend.enabled) { ++ org.dreeam.leaf.async.chunk.AsyncChunkSend.POOL.execute( ++ () -> serverGamePacketListenerImpl.send(new ClientboundChunkBatchFinishedPacket(list.size())) ++ ); ++ } else { ++ serverGamePacketListenerImpl.send(new ClientboundChunkBatchFinishedPacket(list.size())); ++ } ++ // Leaf end - Async chunk send + this.batchQuota = this.batchQuota - list.size(); + } + } +@@ -81,7 +97,23 @@ 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)); ++ // Leaf start - Async chunk send ++ if (org.dreeam.leaf.config.modules.async.AsyncChunkSend.enabled) { ++ var blockEntities = chunk.blockEntities.values().toArray(new net.minecraft.world.level.block.entity.BlockEntity[0]); ++ var heightmaps = new net.minecraft.nbt.CompoundTag(); ++ ++ for (var entry : chunk.getHeightmaps()) { ++ if (entry.getKey().sendToClient()) { ++ heightmaps.put(entry.getKey().getSerializationKey(), new net.minecraft.nbt.LongArrayTag(entry.getValue().getRawData())); ++ } ++ } ++ org.dreeam.leaf.async.chunk.AsyncChunkSend.POOL.execute( ++ () -> packetListener.send(new ClientboundLevelChunkWithLightPacket(chunk, level.getLightEngine(), null, null, shouldModify, blockEntities, heightmaps)) ++ ); ++ } else { ++ packetListener.send(new ClientboundLevelChunkWithLightPacket(chunk, level.getLightEngine(), null, null, shouldModify)); ++ } ++ // Leaf end - Async chunk send + // Paper end - Anti-Xray + // Paper start - PlayerChunkLoadEvent + if (io.papermc.paper.event.packet.PlayerChunkLoadEvent.getHandlerList().getRegisteredListeners().length > 0) { +diff --git a/net/minecraft/world/level/chunk/LevelChunkSection.java b/net/minecraft/world/level/chunk/LevelChunkSection.java +index b8ac6a9ba7b56ccd034757f7d135d272b8e69e90..e307e618775acb2052593e16d6ff2a5a9edbac4a 100644 +--- a/net/minecraft/world/level/chunk/LevelChunkSection.java ++++ b/net/minecraft/world/level/chunk/LevelChunkSection.java +@@ -18,7 +18,7 @@ public class LevelChunkSection implements ca.spottedleaf.moonrise.patches.block_ + public static final int SECTION_HEIGHT = 16; + public static final int SECTION_SIZE = 4096; + public static final int BIOME_CONTAINER_BITS = 2; +- short nonEmptyBlockCount; // Paper - package private ++ volatile short nonEmptyBlockCount; // Paper - package private // Leaf - volatile + private short tickingBlockCount; + private short tickingFluidCount; + private boolean isRandomlyTickingBlocksStatus; // Leaf - Cache random tick block status diff --git a/leaf-server/minecraft-patches/features/0132-Async-chunk-sending.patch b/leaf-server/minecraft-patches/features/0132-Async-chunk-sending.patch deleted file mode 100644 index e8b716e1..00000000 --- a/leaf-server/minecraft-patches/features/0132-Async-chunk-sending.patch +++ /dev/null @@ -1,108 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Taiyou06 -Date: Sun, 2 Mar 2025 21:23:20 +0100 -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 a35e9fae8f8da0c42f0616c4f78dc396492673aa..af49117695c0785033b984ff91550e2fccbbc5e6 100644 ---- a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java -+++ b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java -@@ -411,19 +411,91 @@ public final class RegionizedPlayerChunkLoader { - this.delayedTicketOps.addLast(op); - } - -+ // Leaf start - Async chunk sending -+ /** -+ * Sends a chunk to the player. -+ * If async chunk sending is enabled, this will prepare and send the chunk packet asynchronously. -+ * Otherwise, it will use the synchronous chunk sending implementation. -+ */ - private void sendChunk(final int chunkX, final int chunkZ) { -- if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { -- ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager -- .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$addReceivedChunk(this.player); -+ final long chunkKey = CoordinateUtils.getChunkKey(chunkX, chunkZ); - -- final LevelChunk chunk = ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(chunkX, chunkZ); -+ if (!this.sentChunks.add(chunkKey)) { -+ // Already in our sent list - silently return instead of throwing an exception -+ return; -+ } -+ -+ // Get the chunk now, as we need it for both sync and async paths -+ final LevelChunk chunk = ((ChunkSystemLevel) this.world).moonrise$getFullChunkIfLoaded(chunkX, chunkZ); -+ if (chunk == null) { -+ // Handle case where chunk is no longer loaded -+ this.sentChunks.remove(chunkKey); -+ return; -+ } -+ -+ // Try to mark the chunk as received by this player -+ try { -+ // This part needs to remain on the main thread as it affects shared state -+ ((ChunkSystemServerLevel) this.world).moonrise$getChunkTaskScheduler().chunkHolderManager -+ .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder.moonrise$addReceivedChunk(this.player); - -+ // Call onChunkWatch on the main thread as it might affect server state - PlatformHooks.get().onChunkWatch(this.world, chunk, this.player); -- PlayerChunkSender.sendChunk(this.player.connection, this.world, chunk); -+ } catch (IllegalStateException e) { -+ // This happens if the chunk was already marked as received by this player -+ // Just remove it from our sent list and return -+ this.sentChunks.remove(chunkKey); - return; - } -- throw new IllegalStateException(); -+ -+ // Check if async chunk sending is enabled -+ if (org.dreeam.leaf.config.modules.async.AsyncChunkSend.enabled) { -+ // Async implementation -+ net.minecraft.Util.backgroundExecutor().execute(() -> { -+ try { -+ final net.minecraft.server.network.ServerGamePacketListenerImpl connection = this.player.connection; -+ final ServerLevel serverLevel = this.world; -+ -+ // Create the packet with anti-xray control flag -+ final net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket packet = new net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket( -+ chunk, serverLevel.getLightEngine(), null, null, -+ serverLevel.chunkPacketBlockController.shouldModify(this.player, chunk) -+ ); -+ -+ // Let the main thread handle the anti-xray processing -+ serverLevel.getServer().execute(() -> { -+ if (this.removed || !this.sentChunks.contains(chunkKey)) { -+ return; -+ } -+ -+ // This will trigger anti-xray processing and mark the packet as ready when done -+ // The packet automatically handles readiness -+ // Send the packet (which will be held until ready by the network layer) -+ connection.send(packet); -+ -+ // Fire events and send POI packets -+ if (io.papermc.paper.event.packet.PlayerChunkLoadEvent.getHandlerList().getRegisteredListeners().length > 0) { -+ new io.papermc.paper.event.packet.PlayerChunkLoadEvent( -+ new org.bukkit.craftbukkit.CraftChunk(chunk), -+ this.player.getBukkitEntity() -+ ).callEvent(); -+ } -+ -+ net.minecraft.network.protocol.game.DebugPackets.sendPoiPacketsForChunk(serverLevel, chunk.getPos()); -+ }); -+ } catch (Exception e) { -+ org.dreeam.leaf.async.AsyncChunkSending.LOGGER.error("Failed to send chunk asynchronously!", e); -+ -+ if (!this.removed) { -+ this.sentChunks.remove(chunkKey); -+ } -+ } -+ }); -+ } else { -+ PlayerChunkSender.sendChunk(this.player.connection, this.world, chunk); -+ } - } -+ // Leaf end - Async chunk sending - - private void sendUnloadChunk(final int chunkX, final int chunkZ) { - if (!this.sentChunks.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { diff --git a/leaf-server/paper-patches/features/0036-async-chunk-send.patch b/leaf-server/paper-patches/features/0036-async-chunk-send.patch new file mode 100644 index 00000000..a0008705 --- /dev/null +++ b/leaf-server/paper-patches/features/0036-async-chunk-send.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: hayanesuru +Date: Fri, 2 May 2025 18:22:24 -0700 +Subject: [PATCH] async chunk send + + +diff --git a/src/main/java/io/papermc/paper/antixray/ChunkPacketBlockControllerAntiXray.java b/src/main/java/io/papermc/paper/antixray/ChunkPacketBlockControllerAntiXray.java +index ee2d3a54d760f9c26542eab03c51651a30e279a0..0a4382315ee62e8defa50b3e8018a8f97687c139 100644 +--- a/src/main/java/io/papermc/paper/antixray/ChunkPacketBlockControllerAntiXray.java ++++ b/src/main/java/io/papermc/paper/antixray/ChunkPacketBlockControllerAntiXray.java +@@ -185,7 +185,7 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo + return; + } + +- if (!Bukkit.isPrimaryThread()) { ++ if (!Bukkit.isPrimaryThread() && !(Thread.currentThread() instanceof org.dreeam.leaf.async.chunk.AsyncChunkSendThread)) { // Leaf - Async chunk send + // Plugins? + MinecraftServer.getServer().scheduleOnMain(() -> modifyBlocks(chunkPacket, chunkPacketInfo)); + return; diff --git a/leaf-server/src/main/java/org/dreeam/leaf/async/AsyncChunkSending.java b/leaf-server/src/main/java/org/dreeam/leaf/async/AsyncChunkSending.java deleted file mode 100644 index a54cefb3..00000000 --- a/leaf-server/src/main/java/org/dreeam/leaf/async/AsyncChunkSending.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.dreeam.leaf.async; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -public class AsyncChunkSending { - - public static final Logger LOGGER = LogManager.getLogger(AsyncChunkSending.class.getSimpleName()); -} diff --git a/leaf-server/src/main/java/org/dreeam/leaf/async/chunk/AsyncChunkSend.java b/leaf-server/src/main/java/org/dreeam/leaf/async/chunk/AsyncChunkSend.java new file mode 100644 index 00000000..d64f225d --- /dev/null +++ b/leaf-server/src/main/java/org/dreeam/leaf/async/chunk/AsyncChunkSend.java @@ -0,0 +1,25 @@ +package org.dreeam.leaf.async.chunk; + +import net.minecraft.Util; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class AsyncChunkSend { + public static final ExecutorService POOL = new ThreadPoolExecutor( + 1, 1, 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(), + new com.google.common.util.concurrent.ThreadFactoryBuilder() + .setPriority(Thread.NORM_PRIORITY - 2) + .setNameFormat("Leaf Async Chunk Send Thread") + .setUncaughtExceptionHandler(Util::onThreadException) + .setThreadFactory(AsyncChunkSendThread::new) + .build(), + new ThreadPoolExecutor.DiscardPolicy() + ); + public static final Logger LOGGER = LogManager.getLogger("Leaf Async Chunk Send"); +} diff --git a/leaf-server/src/main/java/org/dreeam/leaf/async/chunk/AsyncChunkSendThread.java b/leaf-server/src/main/java/org/dreeam/leaf/async/chunk/AsyncChunkSendThread.java new file mode 100644 index 00000000..7e9f8bcb --- /dev/null +++ b/leaf-server/src/main/java/org/dreeam/leaf/async/chunk/AsyncChunkSendThread.java @@ -0,0 +1,7 @@ +package org.dreeam.leaf.async.chunk; + +public class AsyncChunkSendThread extends Thread { + protected AsyncChunkSendThread(Runnable task) { + super(task); + } +} diff --git a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncChunkSend.java b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncChunkSend.java index 89ef74ed..5313cb62 100644 --- a/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncChunkSend.java +++ b/leaf-server/src/main/java/org/dreeam/leaf/config/modules/async/AsyncChunkSend.java @@ -10,6 +10,7 @@ public class AsyncChunkSend extends ConfigModules { } public static boolean enabled = false; + private static boolean asyncChunkSendInitialized; @Override public void onLoaded() { @@ -20,6 +21,12 @@ public class AsyncChunkSend extends ConfigModules { 使区块数据包准备和发送异步化以提高服务器性能. 当许多玩家同时加载区块时, 这可以显著减少主线程负载."""); + if (asyncChunkSendInitialized) { + config.getConfigSection(getBasePath()); + return; + } + asyncChunkSendInitialized = true; + enabled = config.getBoolean(getBasePath() + ".enabled", enabled); } }