From a99b5fd8c66f2ee7c8cc047850714e28438a48fb Mon Sep 17 00:00:00 2001 From: Bacteriawa Date: Sun, 11 May 2025 00:57:37 +0800 Subject: [PATCH] Faster chunk serialization --- ...ecessary-calculations-if-player-is-n.patch | 2 +- ...0062-Gale-Faster-chunk-serialization.patch | 216 ++++++++++++++++++ .../world/chunk/LithiumHashPalette.java.patch | 212 +++++++++++++++++ 3 files changed, 429 insertions(+), 1 deletion(-) create mode 100644 luminol-server/minecraft-patches/features/0062-Gale-Faster-chunk-serialization.patch create mode 100644 luminol-server/paper-patches/files/src/main/java/net/caffeinemc/mods/lithium/common/world/chunk/LithiumHashPalette.java.patch diff --git a/luminol-server/minecraft-patches/features/0061-Lithium-Skip-unnecessary-calculations-if-player-is-n.patch b/luminol-server/minecraft-patches/features/0061-Lithium-Skip-unnecessary-calculations-if-player-is-n.patch index 1d64ca5..1129920 100644 --- a/luminol-server/minecraft-patches/features/0061-Lithium-Skip-unnecessary-calculations-if-player-is-n.patch +++ b/luminol-server/minecraft-patches/features/0061-Lithium-Skip-unnecessary-calculations-if-player-is-n.patch @@ -1,5 +1,5 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: kidofcubes +From: Bacteriawa Date: Fri, 8 Nov 2024 00:22:44 +0800 Subject: [PATCH] Lithium: Skip unnecessary calculations if player is not flying or swing diff --git a/luminol-server/minecraft-patches/features/0062-Gale-Faster-chunk-serialization.patch b/luminol-server/minecraft-patches/features/0062-Gale-Faster-chunk-serialization.patch new file mode 100644 index 0000000..89e6689 --- /dev/null +++ b/luminol-server/minecraft-patches/features/0062-Gale-Faster-chunk-serialization.patch @@ -0,0 +1,216 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Bacteriawa +Date: Wed, 30 Nov 2022 21:51:16 +0100 +Subject: [PATCH] Gale: Faster chunk serialization + + +diff --git a/net/minecraft/util/BitStorage.java b/net/minecraft/util/BitStorage.java +index 02502d50f0255f5bbcc0ecb965abb48cc1a112da..322a1ba06d6aed44ec67dc3f1831ac6b05c82fe0 100644 +--- a/net/minecraft/util/BitStorage.java ++++ b/net/minecraft/util/BitStorage.java +@@ -21,6 +21,8 @@ public interface BitStorage extends ca.spottedleaf.moonrise.patches.block_counti + + BitStorage copy(); + ++ void compact(net.minecraft.world.level.chunk.Palette srcPalette, net.minecraft.world.level.chunk.Palette dstPalette, short[] out); // Gale - Lithium - faster chunk serialization ++ + // Paper start - block counting + // provide default impl in case mods implement this... + @Override +diff --git a/net/minecraft/util/SimpleBitStorage.java b/net/minecraft/util/SimpleBitStorage.java +index e6306a68c8652d4c5d22d5ecb1416f5f931f76ee..2a3d34733b61c73729daa4da61f33e2c2e7b6c72 100644 +--- a/net/minecraft/util/SimpleBitStorage.java ++++ b/net/minecraft/util/SimpleBitStorage.java +@@ -465,4 +465,36 @@ public class SimpleBitStorage implements BitStorage { + super(message); + } + } ++ ++ // Gale start - Lithium - faster chunk serialization ++ @Override ++ public void compact(net.minecraft.world.level.chunk.Palette srcPalette, net.minecraft.world.level.chunk.Palette dstPalette, short[] out) { ++ if (this.size >= Short.MAX_VALUE) throw new IllegalStateException("Array too large"); ++ if (this.size != out.length) throw new IllegalStateException("Array size mismatch"); ++ ++ short[] mappings = new short[(int) (this.mask + 1)]; ++ ++ int idx = 0; ++ ++ for (long word : this.data) { ++ long bits = word; ++ ++ for (int elementIdx = 0; elementIdx < this.valuesPerLong; ++elementIdx) { ++ int value = (int) (bits & this.mask); ++ int remappedId = mappings[value]; ++ ++ if (remappedId == 0) { ++ remappedId = dstPalette.idFor(srcPalette.valueFor(value)) + 1; ++ mappings[value] = (short) remappedId; ++ } ++ ++ out[idx] = (short) (remappedId - 1); ++ bits >>= this.bits; ++ ++ ++idx; ++ ++ if (idx >= this.size) return; ++ } ++ } ++ } // Gale end - Lithium - faster chunk serialization + } +diff --git a/net/minecraft/util/ZeroBitStorage.java b/net/minecraft/util/ZeroBitStorage.java +index 09fd99c9cbd23b5f3c899bfb00c9b89651948ed8..90a85a00c6208d2db65cafb164cd95e6128b6dc4 100644 +--- a/net/minecraft/util/ZeroBitStorage.java ++++ b/net/minecraft/util/ZeroBitStorage.java +@@ -19,6 +19,8 @@ public class ZeroBitStorage implements BitStorage { + return 0; + } + ++ @Override public void compact(net.minecraft.world.level.chunk.Palette srcPalette, net.minecraft.world.level.chunk.Palette dstPalette, short[] out) {} // Gale - Lithium - faster chunk serialization ++ + @Override + public final void set(int index, int value) { // Paper - Perf: Optimize SimpleBitStorage + //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage +diff --git a/net/minecraft/world/level/chunk/PaletteResize.java b/net/minecraft/world/level/chunk/PaletteResize.java +index c723606fa0be811e580ba47de8c9c575583cc930..60d3176477c201643e1657751fcffad511b2994f 100644 +--- a/net/minecraft/world/level/chunk/PaletteResize.java ++++ b/net/minecraft/world/level/chunk/PaletteResize.java +@@ -1,5 +1,5 @@ + package net.minecraft.world.level.chunk; + +-interface PaletteResize { ++public interface PaletteResize { // Gale - Lithium - faster chunk serialization - package -> public + int onResize(int bits, T objectAdded); + } +diff --git a/net/minecraft/world/level/chunk/PalettedContainer.java b/net/minecraft/world/level/chunk/PalettedContainer.java +index f5da433050fd3060e0335d4002d520ebe8cd691f..5a6e699df0a177ea6a919cad609a508678bff823 100644 +--- a/net/minecraft/world/level/chunk/PalettedContainer.java ++++ b/net/minecraft/world/level/chunk/PalettedContainer.java +@@ -25,6 +25,21 @@ import net.minecraft.util.ThreadingDetector; + import net.minecraft.util.ZeroBitStorage; + + public class PalettedContainer implements PaletteResize, PalettedContainerRO { ++ // Gale start - Lithium - faster chunk serialization ++ private static final ThreadLocal CACHED_ARRAY_4096 = ThreadLocal.withInitial(() -> new short[4096]); ++ private static final ThreadLocal CACHED_ARRAY_64 = ThreadLocal.withInitial(() -> new short[64]); ++ private Optional asOptional(long[] data) { ++ return Optional.of(Arrays.stream(data)); ++ } ++ private short[] getOrCreate(int size) { ++ return switch (size) { ++ case 64 -> CACHED_ARRAY_64.get(); ++ case 4096 -> CACHED_ARRAY_4096.get(); ++ default -> new short[size]; ++ }; ++ } ++ // Gale end - Lithium - faster chunk serialization ++ + private static final int MIN_PALETTE_BITS = 0; + private final PaletteResize dummyPaletteResize = (bits, objectAdded) -> 0; + public final IdMap registry; +@@ -344,28 +359,53 @@ public class PalettedContainer implements PaletteResize, PalettedContainer + public synchronized PalettedContainerRO.PackedData pack(IdMap registry, PalettedContainer.Strategy strategy) { // Paper - synchronize + this.acquire(); + +- PalettedContainerRO.PackedData var12; ++ // Gale start - Lithium - faster chunk serialization ++ Optional data = Optional.empty(); ++ List elements = null; + try { +- HashMapPalette hashMapPalette = new HashMapPalette<>(registry, this.data.storage.getBits(), this.dummyPaletteResize); +- int size = strategy.size(); +- int[] ints = new int[size]; +- this.data.storage.unpack(ints); +- swapPalette(ints, id -> hashMapPalette.idFor(this.data.palette.valueFor(id))); +- int i = strategy.calculateBitsForSerialization(registry, hashMapPalette.getSize()); +- Optional optional; +- if (i != 0) { +- SimpleBitStorage simpleBitStorage = new SimpleBitStorage(i, size, ints); +- optional = Optional.of(Arrays.stream(simpleBitStorage.getRaw())); +- } else { +- optional = Optional.empty(); ++ // The palette that will be serialized ++ net.caffeinemc.mods.lithium.common.world.chunk.LithiumHashPalette hashPalette = null; ++ ++ final Palette palette = this.data.palette(); ++ final BitStorage storage = this.data.storage(); ++ if (storage instanceof ZeroBitStorage || palette.getSize() == 1) { ++ // If the palette only contains one entry, don't attempt to repack it. ++ elements = List.of(palette.valueFor(0)); ++ } else if (palette instanceof net.caffeinemc.mods.lithium.common.world.chunk.LithiumHashPalette lithiumHashPalette) { ++ hashPalette = lithiumHashPalette; + } + +- var12 = new PalettedContainerRO.PackedData<>(hashMapPalette.getEntries(), optional); ++ if (elements == null) { ++ var compactedPalette = new net.caffeinemc.mods.lithium.common.world.chunk.LithiumHashPalette<>(registry, storage.getBits(), this.dummyPaletteResize); ++ short[] array = this.getOrCreate(strategy.size()); ++ ++ storage.compact(this.data.palette(), compactedPalette, array); ++ ++ // If the palette didn't change during compaction, do a simple copy of the data array ++ if (hashPalette != null && hashPalette.getSize() == compactedPalette.getSize() && storage.getBits() == strategy.calculateBitsForSerialization(registry, hashPalette.getSize())) { // paletteSize can de-sync from palette - see https://github.com/CaffeineMC/lithium-fabric/issues/279 ++ data = this.asOptional(storage.getRaw().clone()); ++ elements = hashPalette.getElements(); ++ } else { ++ int bits = strategy.calculateBitsForSerialization(registry, compactedPalette.getSize()); ++ if (bits != 0) { ++ // Re-pack the integer array as the palette has changed size ++ SimpleBitStorage copy = new SimpleBitStorage(bits, array.length); ++ for (int i = 0; i < array.length; ++i) ++ copy.set(i, array[i]); ++ ++ // We don't need to clone the data array as we are the sole owner of it ++ data = this.asOptional(copy.getRaw()); ++ } ++ ++ elements = compactedPalette.getElements(); ++ } ++ } + } finally { + this.release(); + } + +- return var12; ++ return new PalettedContainerRO.PackedData<>(elements, data); ++ // Gale end - Lithium - faster chunk serialization + } + + private static void swapPalette(int[] bits, IntUnaryOperator operator) { +@@ -405,13 +445,30 @@ public class PalettedContainer implements PaletteResize, PalettedContainer + + @Override + public void count(PalettedContainer.CountConsumer countConsumer) { +- if (this.data.palette.getSize() == 1) { +- countConsumer.accept(this.data.palette.valueFor(0), this.data.storage.getSize()); +- } else { +- Int2IntOpenHashMap map = new Int2IntOpenHashMap(); +- this.data.storage.getAll(id -> map.addTo(id, 1)); +- map.int2IntEntrySet().forEach(idEntry -> countConsumer.accept(this.data.palette.valueFor(idEntry.getIntKey()), idEntry.getIntValue())); ++ // Gale start - Lithium - faster chunk serialization ++ int len = this.data.palette().getSize(); ++ ++ // Do not allocate huge arrays if we're using a large palette ++ if (len > 4096) { ++ // VanillaCopy ++ if (this.data.palette.getSize() == 1) { ++ countConsumer.accept(this.data.palette.valueFor(0), this.data.storage.getSize()); ++ } else { ++ Int2IntOpenHashMap map = new Int2IntOpenHashMap(); ++ this.data.storage.getAll(id -> map.addTo(id, 1)); ++ map.int2IntEntrySet().forEach(idEntry -> countConsumer.accept(this.data.palette.valueFor(idEntry.getIntKey()), idEntry.getIntValue())); ++ } ++ } ++ short[] counts = new short[len]; ++ this.data.storage().getAll(i -> counts[i]++); ++ ++ for (int i = 0; i < counts.length; i++) { ++ T obj = this.data.palette().valueFor(i); ++ ++ if (obj != null) ++ countConsumer.accept(obj, counts[i]); + } ++ // Gale end - Lithium - faster chunk serialization + } + + record Configuration(Palette.Factory factory, int bits) { diff --git a/luminol-server/paper-patches/files/src/main/java/net/caffeinemc/mods/lithium/common/world/chunk/LithiumHashPalette.java.patch b/luminol-server/paper-patches/files/src/main/java/net/caffeinemc/mods/lithium/common/world/chunk/LithiumHashPalette.java.patch new file mode 100644 index 0000000..917611c --- /dev/null +++ b/luminol-server/paper-patches/files/src/main/java/net/caffeinemc/mods/lithium/common/world/chunk/LithiumHashPalette.java.patch @@ -0,0 +1,212 @@ +--- /dev/null ++++ b/src/main/java/net/caffeinemc/mods/lithium/common/world/chunk/LithiumHashPalette.java +@@ -1,0 +_,209 @@ ++// Lithium - faster chunk serialization ++ ++package net.caffeinemc.mods.lithium.common.world.chunk; ++ ++import it.unimi.dsi.fastutil.HashCommon; ++import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; ++import net.minecraft.CrashReport; ++import net.minecraft.CrashReportCategory; ++import net.minecraft.ReportedException; ++import net.minecraft.core.IdMap; ++import net.minecraft.network.FriendlyByteBuf; ++import net.minecraft.network.VarInt; ++import net.minecraft.world.level.chunk.MissingPaletteEntryException; ++import net.minecraft.world.level.chunk.Palette; ++import net.minecraft.world.level.chunk.PaletteResize; ++import org.jetbrains.annotations.NotNull; ++ ++import java.util.Arrays; ++import java.util.List; ++import java.util.function.Predicate; ++ ++import static it.unimi.dsi.fastutil.Hash.FAST_LOAD_FACTOR; ++ ++/** ++ * Generally provides better performance over the vanilla {@link net.minecraft.world.level.chunk.HashMapPalette} when calling ++ * {@link LithiumHashPalette#idFor(Object)} through using a faster backing map and reducing pointer chasing. ++ */ ++public class LithiumHashPalette implements Palette { ++ private static final int ABSENT_VALUE = -1; ++ ++ private final IdMap idList; ++ private final PaletteResize resizeHandler; ++ private final int indexBits; ++ ++ private final Reference2IntOpenHashMap table; ++ private T[] entries; ++ private int size = 0; ++ ++ private LithiumHashPalette(IdMap idList, PaletteResize resizeHandler, int indexBits, T[] entries, Reference2IntOpenHashMap table, int size) { ++ this.idList = idList; ++ this.resizeHandler = resizeHandler; ++ this.indexBits = indexBits; ++ this.entries = entries; ++ this.table = table; ++ this.size = size; ++ } ++ ++ public LithiumHashPalette(IdMap idList, int bits, PaletteResize resizeHandler, List list) { ++ this(idList, bits, resizeHandler); ++ ++ for (T t : list) { ++ this.addEntry(t); ++ } ++ } ++ ++ @SuppressWarnings("unchecked") ++ public LithiumHashPalette(IdMap idList, int bits, PaletteResize resizeHandler) { ++ this.idList = idList; ++ this.indexBits = bits; ++ this.resizeHandler = resizeHandler; ++ ++ int capacity = 1 << bits; ++ ++ this.entries = (T[]) new Object[capacity]; ++ this.table = new Reference2IntOpenHashMap<>(capacity, FAST_LOAD_FACTOR); ++ this.table.defaultReturnValue(ABSENT_VALUE); ++ } ++ ++ @Override ++ public int idFor(@NotNull T obj) { ++ int id = this.table.getInt(obj); ++ ++ if (id == ABSENT_VALUE) { ++ id = this.computeEntry(obj); ++ } ++ ++ return id; ++ } ++ ++ @Override ++ public boolean maybeHas(@NotNull Predicate predicate) { ++ for (int i = 0; i < this.size; ++i) { ++ if (predicate.test(this.entries[i])) { ++ return true; ++ } ++ } ++ ++ return false; ++ } ++ ++ private int computeEntry(T obj) { ++ int id = this.addEntry(obj); ++ ++ if (id >= 1 << this.indexBits) { ++ if (this.resizeHandler == null) { ++ throw new IllegalStateException("Cannot grow"); ++ } else { ++ id = this.resizeHandler.onResize(this.indexBits + 1, obj); ++ } ++ } ++ ++ return id; ++ } ++ ++ private int addEntry(T obj) { ++ int nextId = this.size; ++ ++ if (nextId >= this.entries.length) { ++ this.resize(this.size); ++ } ++ ++ this.table.put(obj, nextId); ++ this.entries[nextId] = obj; ++ ++ this.size++; ++ ++ return nextId; ++ } ++ ++ private void resize(int neededCapacity) { ++ this.entries = Arrays.copyOf(this.entries, HashCommon.nextPowerOfTwo(neededCapacity + 1)); ++ } ++ ++ @Override ++ public @NotNull T valueFor(int id) { ++ T[] entries = this.entries; ++ ++ T entry = null; ++ if (id >= 0 && id < entries.length) { ++ entry = entries[id]; ++ } ++ ++ if (entry != null) { ++ return entry; ++ } else { ++ throw this.missingPaletteEntryCrash(id); ++ } ++ } ++ ++ private ReportedException missingPaletteEntryCrash(int id) { ++ try { ++ throw new MissingPaletteEntryException(id); ++ } catch (MissingPaletteEntryException e) { ++ CrashReport crashReport = CrashReport.forThrowable(e, "[Lithium] Getting Palette Entry"); ++ CrashReportCategory crashReportCategory = crashReport.addCategory("Chunk section"); ++ crashReportCategory.setDetail("IndexBits", this.indexBits); ++ crashReportCategory.setDetail("Entries", this.entries.length + " Elements: " + Arrays.toString(this.entries)); ++ crashReportCategory.setDetail("Table", this.table.size() + " Elements: " + this.table); ++ return new ReportedException(crashReport); ++ } ++ } ++ ++ @Override ++ public void read(FriendlyByteBuf buf) { ++ this.clear(); ++ ++ int entryCount = buf.readVarInt(); ++ ++ for (int i = 0; i < entryCount; ++i) { ++ this.addEntry(this.idList.byIdOrThrow(buf.readVarInt())); ++ } ++ } ++ ++ @Override ++ public void write(FriendlyByteBuf buf) { ++ int size = this.size; ++ buf.writeVarInt(size); ++ ++ for (int i = 0; i < size; ++i) { ++ buf.writeVarInt(this.idList.getId(this.valueFor(i))); ++ } ++ } ++ ++ @Override ++ public int getSerializedSize() { ++ int size = VarInt.getByteSize(this.size); ++ ++ for (int i = 0; i < this.size; ++i) { ++ size += VarInt.getByteSize(this.idList.getId(this.valueFor(i))); ++ } ++ ++ return size; ++ } ++ ++ @Override ++ public int getSize() { ++ return this.size; ++ } ++ ++ @Override ++ public @NotNull Palette copy(@NotNull PaletteResize resizeHandler) { ++ return new LithiumHashPalette<>(this.idList, resizeHandler, this.indexBits, this.entries.clone(), this.table.clone(), this.size); ++ } ++ ++ private void clear() { ++ Arrays.fill(this.entries, null); ++ this.table.clear(); ++ this.size = 0; ++ } ++ ++ public List getElements() { ++ T[] copy = Arrays.copyOf(this.entries, this.size); ++ return Arrays.asList(copy); ++ } ++ ++ public static Palette create(int bits, IdMap idList, PaletteResize listener, List list) { ++ return new LithiumHashPalette<>(idList, bits, listener, list); ++ } ++}