diff --git a/leaf-server/minecraft-patches/features/0192-Async-chunk-sending.patch b/leaf-server/minecraft-patches/features/0192-Async-chunk-sending.patch deleted file mode 100644 index 4dbebfe3b..000000000 --- a/leaf-server/minecraft-patches/features/0192-Async-chunk-sending.patch +++ /dev/null @@ -1,175 +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 7129c5e1920008ac54f3a8ac83f5589396f4e4e9..36fa105561fcee138f8e2c69c94c5340ddd330a7 100644 ---- a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java -+++ b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java -@@ -440,7 +440,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 sending -+ 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 sending - // 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 19547e22620f16d01f05bbd13a4dcb9c0df636df..3c554b57e22644f35ed6520901d9e7b3d6c96551 100644 ---- a/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java -+++ b/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java -@@ -75,6 +75,52 @@ public class ClientboundLevelChunkPacketData { - } - } - -+ // Leaf start - Async chunk sending -+ public ClientboundLevelChunkPacketData(LevelChunk levelChunk, io.papermc.paper.antixray.ChunkPacketInfo chunkPacketInfo, BlockEntity[] blockEntities, Map heightmaps) { -+ this.heightmaps = heightmaps; -+ -+ if (Thread.currentThread() instanceof org.dreeam.leaf.async.chunk.AsyncChunkSendThread) { -+ int size = calculateChunkSize(levelChunk); -+ ByteBuf buffer = Unpooled.buffer(size); -+ extractChunkData(new FriendlyByteBuf(buffer), levelChunk, chunkPacketInfo); -+ // make sure all sections is latest -+ while (size != buffer.writerIndex()) { -+ buffer.writerIndex(0); -+ size = calculateChunkSize(levelChunk); -+ extractChunkData(new FriendlyByteBuf(buffer), levelChunk, chunkPacketInfo); -+ } -+ byte[] array = it.unimi.dsi.fastutil.bytes.ByteArrays.setLength(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 sending -+ - public ClientboundLevelChunkPacketData(RegistryFriendlyByteBuf buffer, int x, int z) { - this.heightmaps = HEIGHTMAPS_STREAM_CODEC.decode(buffer); - int varInt = buffer.readVarInt(); -@@ -123,6 +169,7 @@ public class ClientboundLevelChunkPacketData { - // Paper end - Anti-Xray - Add chunk packet info - } - -+ if (Thread.currentThread() instanceof org.dreeam.leaf.async.chunk.AsyncChunkSendThread) return; // Leaf - Async chunk sending - if (buffer.writerIndex() != buffer.capacity()) { - throw new IllegalStateException("Didn't fill chunk buffer: expected " + buffer.capacity() + " bytes, got " + buffer.writerIndex()); - } -diff --git a/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java b/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java -index 9f43cfbdd49df61de869fd65fb2cbea39a563260..b81b1d5b3e8f4eee65f0f3a622724df0b3852a0b 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 heightmaps) { -+ ChunkPos pos = chunk.getPos(); -+ this.x = pos.x; -+ this.z = pos.z; -+ io.papermc.paper.antixray.ChunkPacketInfo 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 sending - - 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 644948d64791d0ffa4166375d0f4419f1ffa214a..dc0c1ec1f4d682604638b6a3442d469c73995cec 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 sending -+ 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 sending - - for (LevelChunk levelChunk : list) { - sendChunk(serverGamePacketListenerImpl, serverLevel, levelChunk); - } - -- serverGamePacketListenerImpl.send(new ClientboundChunkBatchFinishedPacket(list.size())); -+ // Leaf start - Async chunk sending -+ 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 sending - this.batchQuota = this.batchQuota - list.size(); - } - } -@@ -81,7 +97,23 @@ public class PlayerChunkSender { - public static void sendChunk(ServerGamePacketListenerImpl packetListener, ServerLevel level, LevelChunk chunk) { // Paper - rewrite chunk system - public - // Paper start - Anti-Xray - final boolean shouldModify = level.chunkPacketBlockController.shouldModify(packetListener.player, chunk); -- packetListener.send(new ClientboundLevelChunkWithLightPacket(chunk, level.getLightEngine(), null, null, shouldModify)); -+ // Leaf start - Async chunk sending -+ if (org.dreeam.leaf.config.modules.async.AsyncChunkSend.enabled) { -+ var blockEntities = chunk.blockEntities.values().toArray(new net.minecraft.world.level.block.entity.BlockEntity[0]); -+ java.util.Map heightmaps = new java.util.concurrent.ConcurrentHashMap<>(); -+ -+ for (var entry : chunk.getHeightmaps()) { -+ if (entry.getKey().sendToClient()) { -+ heightmaps.put(entry.getKey(), 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 sending - // Paper end - Anti-Xray - // Paper start - PlayerChunkLoadEvent - if (io.papermc.paper.event.packet.PlayerChunkLoadEvent.getHandlerList().getRegisteredListeners().length > 0) { diff --git a/leaf-server/minecraft-patches/features/0192-async-chunk-sender.patch b/leaf-server/minecraft-patches/features/0192-async-chunk-sender.patch new file mode 100644 index 000000000..d75c5d6f1 --- /dev/null +++ b/leaf-server/minecraft-patches/features/0192-async-chunk-sender.patch @@ -0,0 +1,248 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Taiyou06 +Date: Sun, 2 Mar 2025 21:23:20 +0100 +Subject: [PATCH] async chunk sender + +Co-authored-by: hayanesuru + +diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +index 7129c5e1920008ac54f3a8ac83f5589396f4e4e9..7975c2d6bb035d2504ce2263639098ca8052465b 100644 +--- a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +@@ -395,6 +395,7 @@ public final class RegionizedPlayerChunkLoader { + private final LongHeapPriorityQueue loadQueue = new LongHeapPriorityQueue(CLOSEST_MANHATTAN_DIST); + + private volatile boolean removed; ++ private final org.dreeam.leaf.async.chunk.AsyncChunkSender asyncSender = org.dreeam.leaf.config.modules.async.AsyncChunkSend.enabled ? new org.dreeam.leaf.async.chunk.AsyncChunkSender() : null; // Leaf - async chunk sender + + public PlayerChunkLoaderData(final ServerLevel world, final ServerPlayer player) { + this.world = world; +@@ -427,8 +428,53 @@ public final class RegionizedPlayerChunkLoader { + throw new IllegalStateException(); + } + ++ // Leaf start - async chunk sender ++ private boolean leaf$sendChunk(final int chunkX, final int chunkZ) { ++ long k = CoordinateUtils.getChunkKey(chunkX, chunkZ); ++ if (!this.sentChunks.contains(k)) { ++ if (this.asyncSender.add(k)) { ++ PlayerChunkSender.sendChunk(this.player.connection, this.world, ((ChunkSystemLevel) this.world).moonrise$getFullChunkIfLoaded(chunkX, chunkZ), this.asyncSender); ++ return true; ++ } else { ++ return false; ++ } ++ } ++ throw new IllegalStateException(); ++ } ++ private void leaf$sendChunkRaw(final net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket chunkPacket) { ++ final int chunkX = chunkPacket.getX(); ++ final int chunkZ = chunkPacket.getZ(); ++ if (this.sentChunks.add(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { ++ ((ChunkSystemChunkHolder)((ChunkSystemServerLevel)this.world).moonrise$getChunkTaskScheduler().chunkHolderManager ++ .getChunkHolder(chunkX, chunkZ).vanillaChunkHolder).moonrise$addReceivedChunk(this.player); ++ ++ final LevelChunk chunk = ((ChunkSystemLevel)this.world).moonrise$getFullChunkIfLoaded(chunkX, chunkZ); ++ ++ PlatformHooks.get().onChunkWatch(this.world, chunk, this.player); ++ this.player.connection.send(chunkPacket); ++ // 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), this.player.connection.getPlayer().getBukkitEntity()).callEvent(); ++ } ++ // Paper end - PlayerChunkLoadEvent ++ // Leaf start - Multithreaded tracker ++ if (org.dreeam.leaf.config.modules.async.MultithreadedTracker.enabled) { ++ ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk trackedChunk = chunk.moonrise$getChunkHolder().holderData.nearbyPlayers; ++ if (trackedChunk != null) { ++ trackedChunk.trackingUpdateCountAtomic.getAndIncrement(); ++ } ++ } ++ // Leaf end - Multithreaded tracker ++ return; ++ } ++ throw new IllegalStateException(); ++ } ++ // Leaf end - async chunk sender ++ + private void sendUnloadChunk(final int chunkX, final int chunkZ) { +- if (!this.sentChunks.remove(CoordinateUtils.getChunkKey(chunkX, chunkZ))) { ++ long k = CoordinateUtils.getChunkKey(chunkX, chunkZ); // Leaf - async chunk sender ++ if (this.asyncSender != null) { this.asyncSender.remove(k); } // Leaf - async chunk sender ++ if (!this.sentChunks.remove(k)) { // Leaf - async chunk sender + return; + } + this.sendUnloadChunkRaw(chunkX, chunkZ); +@@ -839,15 +885,36 @@ public final class RegionizedPlayerChunkLoader { + return; + } // else: good to dequeue and send, fall through + } +- this.sendQueue.dequeueLong(); ++ //this.sendQueue.dequeueLong(); // Leaf - async chunk sender + +- this.sendChunk(pendingSendX, pendingSendZ); ++ // Leaf start - async chunk sender ++ if (this.asyncSender != null) { ++ if (this.leaf$sendChunk(pendingSendX, pendingSendZ)) { ++ this.sendQueue.dequeueLong(); ++ } else { ++ break; ++ } ++ } else { ++ this.sendQueue.dequeueLong(); ++ this.sendChunk(pendingSendX, pendingSendZ); ++ } ++ // Leaf end - async chunk sender + + if (this.removed) { + // sendChunk may invoke plugin logic + return; + } + } ++ // Leaf start - async chunk sender ++ if (this.asyncSender != null) { ++ net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket packet; ++ while ((packet = this.asyncSender.recv()) != null) { ++ if (this.asyncSender.remove(CoordinateUtils.getChunkKey(packet.getX(), packet.getZ()))) { ++ this.leaf$sendChunkRaw(packet); ++ } ++ } ++ } ++ // Leaf end - async chunk sender + + this.flushDelayedTicketOps(); + } +@@ -1030,7 +1097,7 @@ public final class RegionizedPlayerChunkLoader { + break; + } + case CHUNK_TICKET_STAGE_GENERATED: { +- if (sendChunk && !sentChunk) { ++ if (sendChunk && !sentChunk && (this.asyncSender == null || !this.asyncSender.contains(chunk))) { // Leaf - async chunk sender + this.sendQueue.enqueue(chunk); + } + if (squareDistance <= tickViewDistance) { +@@ -1039,7 +1106,7 @@ public final class RegionizedPlayerChunkLoader { + break; + } + case CHUNK_TICKET_STAGE_TICK: { +- if (sendChunk && !sentChunk) { ++ if (sendChunk && !sentChunk && (this.asyncSender == null || !this.asyncSender.contains(chunk))) { // Leaf - async chunk sender + this.sendQueue.enqueue(chunk); + } + break; +@@ -1084,6 +1151,7 @@ public final class RegionizedPlayerChunkLoader { + this.flushDelayedTicketOps(); + + // now all tickets should be removed, which is all of our external state ++ this.asyncSender.clear(); // Leaf - async chunk sender + } + + public LongOpenHashSet getSentChunksRaw() { +diff --git a/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java b/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java +index 19547e22620f16d01f05bbd13a4dcb9c0df636df..4a0d5b4f1ce98b334e7812d3bbe12db2f7f6c958 100644 +--- a/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java ++++ b/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java +@@ -75,6 +75,37 @@ public class ClientboundLevelChunkPacketData { + } + } + ++ // Leaf start - async chunk sender ++ public ClientboundLevelChunkPacketData(LevelChunk levelChunk, io.papermc.paper.antixray.ChunkPacketInfo chunkPacketInfo, BlockEntity[] blockEntities, Map heightmaps) { ++ this.heightmaps = heightmaps; ++ int size; ++ ByteBuf buffer; ++ do { ++ buffer = Unpooled.buffer(size = calculateChunkSize(levelChunk)); ++ } while (!extractChunkData(new FriendlyByteBuf(buffer), levelChunk, chunkPacketInfo, size)); ++ this.buffer = it.unimi.dsi.fastutil.bytes.ByteArrays.setLength(buffer.array(), buffer.writerIndex()); ++ 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)); ++ } ++ ++ if (chunkPacketInfo != null) { ++ chunkPacketInfo.setBuffer(this.buffer); ++ } ++ } ++ // Leaf end - async chunk sender ++ + public ClientboundLevelChunkPacketData(RegistryFriendlyByteBuf buffer, int x, int z) { + this.heightmaps = HEIGHTMAPS_STREAM_CODEC.decode(buffer); + int varInt = buffer.readVarInt(); +@@ -127,6 +158,17 @@ public class ClientboundLevelChunkPacketData { + throw new IllegalStateException("Didn't fill chunk buffer: expected " + buffer.capacity() + " bytes, got " + buffer.writerIndex()); + } + } ++ // Leaf start - async chunk sender ++ public static boolean extractChunkData(FriendlyByteBuf buffer, LevelChunk chunk, io.papermc.paper.antixray.ChunkPacketInfo chunkPacketInfo, int expected) { ++ int chunkSectionIndex = 0; ++ for (LevelChunkSection levelChunkSection : chunk.getSections()) { ++ levelChunkSection.write(buffer, chunkPacketInfo, chunkSectionIndex); ++ chunkSectionIndex++; ++ // Paper end - Anti-Xray - Add chunk packet info ++ } ++ return buffer.writerIndex() == expected; ++ } ++ // Leaf end - async chunk sender + + public Consumer getBlockEntitiesTagsConsumer(int chunkX, int chunkZ) { + return blockEntityTagOutput -> this.getBlockEntitiesTags(blockEntityTagOutput, chunkX, chunkZ); +diff --git a/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java b/net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket.java +index 9f43cfbdd49df61de869fd65fb2cbea39a563260..03470810c8dafc87c787fbd2aff2e1d301b0862e 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 heightmaps) { ++ ChunkPos pos = chunk.getPos(); ++ this.x = pos.x; ++ this.z = pos.z; ++ io.papermc.paper.antixray.ChunkPacketInfo 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.leaf$modifyBlocks(this, chunkPacketInfo); // Paper - Anti-Xray - Modify blocks ++ } ++ // Leaf end - async chunk sender + + 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 644948d64791d0ffa4166375d0f4419f1ffa214a..342e7f6fbb53966c879b369ffde5fee5fc2f01ef 100644 +--- a/net/minecraft/server/network/PlayerChunkSender.java ++++ b/net/minecraft/server/network/PlayerChunkSender.java +@@ -96,6 +96,22 @@ public class PlayerChunkSender { + level.debugSynchronizers().startTrackingChunk(packetListener.player, chunk.getPos()); + } + ++ // Leaf start - async chunk sender ++ public static void sendChunk(ServerGamePacketListenerImpl packetListener, ServerLevel level, LevelChunk chunk, org.dreeam.leaf.async.chunk.AsyncChunkSender chunkSender) { ++ final boolean shouldModify = level.chunkPacketBlockController.shouldModify(packetListener.player, chunk); ++ net.minecraft.world.level.block.entity.BlockEntity[] blockEntities = chunk.blockEntities.values().toArray(new net.minecraft.world.level.block.entity.BlockEntity[0]); ++ java.util.Map heightmaps = new java.util.EnumMap<>(net.minecraft.world.level.levelgen.Heightmap.Types.class); ++ for (java.util.Map.Entry entry1 : chunk.getHeightmaps()) { ++ if (entry1.getKey().sendToClient()) { ++ heightmaps.put(entry1.getKey(), entry1.getValue().getRawData().clone()); ++ } ++ } ++ chunkSender.submit( ++ () -> new ClientboundLevelChunkWithLightPacket(chunk, level.getLightEngine(), null, null, shouldModify, blockEntities, heightmaps) ++ ); ++ } ++ // Leaf end - async chunk sender ++ + private List collectChunksToSend(ChunkMap chunkMap, ChunkPos chunkPos) { + int floor = Mth.floor(this.batchQuota); + List list; diff --git a/leaf-server/minecraft-patches/features/0289-Multithreaded-Tracker.patch b/leaf-server/minecraft-patches/features/0289-Multithreaded-Tracker.patch index 3789c466f..be00b2ffa 100644 --- a/leaf-server/minecraft-patches/features/0289-Multithreaded-Tracker.patch +++ b/leaf-server/minecraft-patches/features/0289-Multithreaded-Tracker.patch @@ -80,7 +80,7 @@ index 288a3eb57f3431dd624ad8a4b08684563abbc5ad..499cad369242f9ad724b3251538d62d8 if (list == null) { throw new IllegalStateException("Does not contain player " + player); diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java -index 7a40cef6c0edf4bb728f74f0c2e93c7e9f34a1b3..3ca08ab3b57c278748ad58ab03751f25ab6202e5 100644 +index d8e0ba633224cc665c7dc65463cdb27e70be73a6..4ad7cb1369f0c2a69a1199de71654f7de5c84470 100644 --- a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +++ b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java @@ -345,7 +345,7 @@ public final class RegionizedPlayerChunkLoader { @@ -92,7 +92,7 @@ index 7a40cef6c0edf4bb728f74f0c2e93c7e9f34a1b3..3ca08ab3b57c278748ad58ab03751f25 private static final byte CHUNK_TICKET_STAGE_NONE = 0; private static final byte CHUNK_TICKET_STAGE_LOADING = 1; -@@ -423,6 +423,14 @@ public final class RegionizedPlayerChunkLoader { +@@ -424,6 +424,14 @@ public final class RegionizedPlayerChunkLoader { PlatformHooks.get().onChunkWatch(this.world, chunk, this.player); PlayerChunkSender.sendChunk(this.player.connection, this.world, chunk); diff --git a/leaf-server/paper-patches/features/0048-Async-chunk-sending.patch b/leaf-server/paper-patches/features/0048-Async-chunk-sending.patch deleted file mode 100644 index 930731fef..000000000 --- a/leaf-server/paper-patches/features/0048-Async-chunk-sending.patch +++ /dev/null @@ -1,19 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: hayanesuru -Date: Fri, 2 May 2025 18:22:24 -0700 -Subject: [PATCH] Async chunk sending - - -diff --git a/src/main/java/io/papermc/paper/antixray/ChunkPacketBlockControllerAntiXray.java b/src/main/java/io/papermc/paper/antixray/ChunkPacketBlockControllerAntiXray.java -index b03b2d098c20ce53403de7b0aeb0cf67a61bf7b4..d27a17a155df98b3f35d8fc1b896c81d6c1619b2 100644 ---- a/src/main/java/io/papermc/paper/antixray/ChunkPacketBlockControllerAntiXray.java -+++ b/src/main/java/io/papermc/paper/antixray/ChunkPacketBlockControllerAntiXray.java -@@ -186,7 +186,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 sending - // Plugins? - MinecraftServer.getServer().scheduleOnMain(() -> modifyBlocks(chunkPacket, chunkPacketInfo)); - return; diff --git a/leaf-server/paper-patches/features/0048-async-chunk-sender.patch b/leaf-server/paper-patches/features/0048-async-chunk-sender.patch new file mode 100644 index 000000000..357416834 --- /dev/null +++ b/leaf-server/paper-patches/features/0048-async-chunk-sender.patch @@ -0,0 +1,50 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: hayanesuru +Date: Fri, 31 Oct 2025 00:04:42 +0900 +Subject: [PATCH] async chunk sender + + +diff --git a/src/main/java/io/papermc/paper/antixray/ChunkPacketBlockController.java b/src/main/java/io/papermc/paper/antixray/ChunkPacketBlockController.java +index 568d12d006996258e6b007622add1a059e475708..58a2f57cdbea21122ace08bb966b95c002f4892e 100644 +--- a/src/main/java/io/papermc/paper/antixray/ChunkPacketBlockController.java ++++ b/src/main/java/io/papermc/paper/antixray/ChunkPacketBlockController.java +@@ -36,6 +36,12 @@ public class ChunkPacketBlockController { + chunkPacket.setReady(true); + } + ++ // Leaf start - async chunk sender ++ public void leaf$modifyBlocks(ClientboundLevelChunkWithLightPacket chunkPacket, ChunkPacketInfo chunkPacketInfo) { ++ chunkPacket.setReady(true); ++ } ++ // Leaf end - async chunk sender ++ + public void onBlockChange(Level level, BlockPos blockPos, BlockState newBlockState, BlockState oldBlockState, @Block.UpdateFlags int flags, int maxUpdateDepth) { + + } +diff --git a/src/main/java/io/papermc/paper/antixray/ChunkPacketBlockControllerAntiXray.java b/src/main/java/io/papermc/paper/antixray/ChunkPacketBlockControllerAntiXray.java +index b03b2d098c20ce53403de7b0aeb0cf67a61bf7b4..b63ae1958bad9e373d94321444b1c47cc452196b 100644 +--- a/src/main/java/io/papermc/paper/antixray/ChunkPacketBlockControllerAntiXray.java ++++ b/src/main/java/io/papermc/paper/antixray/ChunkPacketBlockControllerAntiXray.java +@@ -200,6 +200,22 @@ public final class ChunkPacketBlockControllerAntiXray extends ChunkPacketBlockCo + executor.execute((Runnable) chunkPacketInfo); + } + ++ // Leaf start - async chunk sender ++ @Override ++ public void leaf$modifyBlocks(ClientboundLevelChunkWithLightPacket chunkPacket, ChunkPacketInfo chunkPacketInfo) { ++ if (!(chunkPacketInfo instanceof ChunkPacketInfoAntiXray)) { ++ chunkPacket.setReady(true); ++ return; ++ } ++ LevelChunk chunk = chunkPacketInfo.getChunk(); ++ int x = chunk.getPos().x; ++ int z = chunk.getPos().z; ++ Level level = chunk.getLevel(); ++ ((ChunkPacketInfoAntiXray) chunkPacketInfo).setNearbyChunks(level.getChunkIfLoaded(x - 1, z), level.getChunkIfLoaded(x + 1, z), level.getChunkIfLoaded(x, z - 1), level.getChunkIfLoaded(x, z + 1)); ++ ((Runnable) chunkPacketInfo).run(); ++ } ++ // Leaf end - async chunk sender ++ + // Actually these fields should be variables inside the obfuscate method but in sync mode or with SingleThreadExecutor in async mode it's okay (even without ThreadLocal) + // If an ExecutorService with multiple threads is used, ThreadLocal must be used here + private final ThreadLocal presetBlockStateBits = ThreadLocal.withInitial(() -> new int[getPresetBlockStatesFullLength()]); 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 index 54ee9ad00..8c6c6584b 100644 --- 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 @@ -1,27 +1,22 @@ package org.dreeam.leaf.async.chunk; -import com.google.common.util.concurrent.ThreadFactoryBuilder; import net.minecraft.util.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 final class AsyncChunkSend { public static final ExecutorService POOL = new ThreadPoolExecutor( 1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), - new ThreadFactoryBuilder() - .setPriority(Thread.NORM_PRIORITY) - .setNameFormat("Leaf Async Chunk Send Thread") - .setUncaughtExceptionHandler(Util::onThreadException) - .setThreadFactory(AsyncChunkSendThread::new) - .build(), + Thread.ofPlatform() + .priority(Thread.NORM_PRIORITY - 1) + .uncaughtExceptionHandler(Util::onThreadException) + .name("Leaf Async Chunk Sender Thread") + .factory(), new ThreadPoolExecutor.CallerRunsPolicy() ); - 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 deleted file mode 100644 index 4dae48a5e..000000000 --- a/leaf-server/src/main/java/org/dreeam/leaf/async/chunk/AsyncChunkSendThread.java +++ /dev/null @@ -1,8 +0,0 @@ -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/async/chunk/AsyncChunkSender.java b/leaf-server/src/main/java/org/dreeam/leaf/async/chunk/AsyncChunkSender.java new file mode 100644 index 000000000..14ee0ccef --- /dev/null +++ b/leaf-server/src/main/java/org/dreeam/leaf/async/chunk/AsyncChunkSender.java @@ -0,0 +1,54 @@ +package org.dreeam.leaf.async.chunk; + +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket; +import org.dreeam.leaf.util.queue.MpmcQueue; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.function.Supplier; + +@NullMarked +public final class AsyncChunkSender { + + private static final int CAPACITY = 255; + + private final MpmcQueue channel; + private final LongOpenHashSet pending; + private int size = 0; + + public AsyncChunkSender() { + this.channel = new MpmcQueue<>(ClientboundLevelChunkWithLightPacket.class, CAPACITY); + this.pending = new LongOpenHashSet(); + } + + public boolean add(long k) { + return size < CAPACITY && pending.size() < CAPACITY && pending.add(k); + } + + public boolean remove(long k) { + return pending.remove(k); + } + + public boolean contains(long k) { + return pending.contains(k); + } + + public void clear() { + pending.clear(); + while (recv() != null) ; + } + + public void submit(Supplier task) { + size++; + AsyncChunkSend.POOL.submit(() -> { + ClientboundLevelChunkWithLightPacket chunk = task.get(); + while (!channel.send(chunk)) ; + }); + } + + public @Nullable ClientboundLevelChunkWithLightPacket recv() { + size--; + return this.channel.recv(); + } +}