From ada1659caacfcdbce7832cf958d3bd58a388995e Mon Sep 17 00:00:00 2001 From: jcw780 <30562395+jcw780@users.noreply.github.com> Date: Mon, 19 Jan 2026 21:15:06 -0500 Subject: [PATCH 01/22] saving changes --- .../RegionBasedStorageSectionExtended.java | 2 +- .../iterator/NearbyPointOfInterestStream.java | 4 +-- .../SphereChunkOrderedPoiSetSpliterator.java | 4 +-- .../lithium/mixin/ai/poi/PoiManagerMixin.java | 2 +- .../mixin/ai/poi/SectionStorageMixin.java | 34 ++++++++++++++++--- 5 files changed, 35 insertions(+), 11 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java index f44e2d2c2..b229bc2e4 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java @@ -8,7 +8,7 @@ import java.util.Optional; import java.util.function.Predicate; -public interface RegionBasedStorageSectionExtended { +public interface RegionBasedStorageSectionExtended { U lithium$getFirstInRangeInChunkColumn(int chunkX, int chunkZ, long deltaYSqMargin, diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/iterator/NearbyPointOfInterestStream.java b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/iterator/NearbyPointOfInterestStream.java index ea266272b..6252f0168 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/iterator/NearbyPointOfInterestStream.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/iterator/NearbyPointOfInterestStream.java @@ -92,7 +92,7 @@ public class NearbyPointOfInterestStream extends Spliterators.AbstractSpliterato }; - private final RegionBasedStorageSectionExtended storage; + private final RegionBasedStorageSectionExtended storage; private final Predicate> typeSelector; private final PoiManager.Occupancy occupationStatus; @@ -142,7 +142,7 @@ public NearbyPointOfInterestStream( PoiManager.Occupancy status, @Nullable Predicate afterSortingPredicate, BlockPos origin, int radius, - RegionBasedStorageSectionExtended storage, + RegionBasedStorageSectionExtended storage, BiPredicate distanceLimit, Comparator sortOrder ) { diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/iterator/SphereChunkOrderedPoiSetSpliterator.java b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/iterator/SphereChunkOrderedPoiSetSpliterator.java index 39752aea2..bc08afdb2 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/iterator/SphereChunkOrderedPoiSetSpliterator.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/iterator/SphereChunkOrderedPoiSetSpliterator.java @@ -20,7 +20,7 @@ import java.util.function.Predicate; public class SphereChunkOrderedPoiSetSpliterator extends Spliterators.AbstractSpliterator { - private final RegionBasedStorageSectionExtended storage; + private final RegionBasedStorageSectionExtended storage; private final int chunkYMin; private final int chunkLimit; @@ -40,7 +40,7 @@ public class SphereChunkOrderedPoiSetSpliterator extends Spliterators.AbstractSp int nextPoiSectionIndex; Iterator sectionIterator; - public SphereChunkOrderedPoiSetSpliterator(int radius, BlockPos origin, RegionBasedStorageSectionExtended storage, Predicate> typeFilter, PoiManager.Occupancy status) { + public SphereChunkOrderedPoiSetSpliterator(int radius, BlockPos origin, RegionBasedStorageSectionExtended storage, Predicate> typeFilter, PoiManager.Occupancy status) { super((long) ((origin.getX() + radius + 1 >> 4) - (origin.getX() - radius - 1 >> 4) + 1) * ((origin.getZ() + radius + 1 >> 4) - (origin.getZ() - radius - 1 >> 4) + 1), Spliterator.ORDERED); this.storage = storage; this.chunkYMin = this.storage.lithium$getChunkYMin(); diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/PoiManagerMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/PoiManagerMixin.java index bd389a155..6da3f3a05 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/PoiManagerMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/PoiManagerMixin.java @@ -36,7 +36,7 @@ import java.util.stream.StreamSupport; @Mixin(PoiManager.class) -public abstract class PoiManagerMixin extends SectionStorage implements PointOfInterestStorageExtended, RegionBasedStorageSectionExtended { +public abstract class PoiManagerMixin extends SectionStorage implements PointOfInterestStorageExtended, RegionBasedStorageSectionExtended { public PoiManagerMixin(SimpleRegionStorage simpleRegionStorage, Codec codec, Function function, BiFunction biFunction, Function function2, RegistryAccess registryAccess, ChunkIOErrorReporter chunkIOErrorReporter, LevelHeightAccessor levelHeightAccessor) { diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java index d9a93a167..83e92cc83 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java @@ -2,6 +2,7 @@ import com.google.common.collect.AbstractIterator; import com.mojang.serialization.Codec; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import net.caffeinemc.mods.lithium.common.util.Distances; @@ -13,15 +14,14 @@ import net.minecraft.core.Holder; import net.minecraft.core.RegistryAccess; import net.minecraft.core.SectionPos; +import net.minecraft.world.entity.ai.village.poi.PoiSection; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.LevelHeightAccessor; import net.minecraft.world.level.chunk.storage.ChunkIOErrorReporter; import net.minecraft.world.level.chunk.storage.SectionStorage; import net.minecraft.world.level.chunk.storage.SimpleRegionStorage; -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Mutable; -import org.spongepowered.asm.mixin.Shadow; +import org.jspecify.annotations.Nullable; +import org.spongepowered.asm.mixin.*; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @@ -36,7 +36,7 @@ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") // We don't get a choice, this is Minecraft's doing! @Mixin(SectionStorage.class) -public abstract class SectionStorageMixin implements RegionBasedStorageSectionExtended { +public abstract class SectionStorageMixin implements RegionBasedStorageSectionExtended { @Mutable @Shadow @Final @@ -195,4 +195,28 @@ protected R computeNext() { public int lithium$getChunkYMaxInclusive() { return Pos.SectionYCoord.getMaxYSectionInclusive(this.levelHeightAccessor); } + + @Overwrite + private void unpackChunk(ChunkPos chunkPos, + @Nullable SectionStorage.PackedChunk

packedChunk) { + if (packedChunk == null) { + for (int i = this.levelHeightAccessor.getMinSectionY(); i <= this.levelHeightAccessor.getMaxSectionY(); i++) { + this.storage.put(getKey(chunkPos, i), Optional.empty()); + } + } else { + boolean bl = packedChunk.versionChanged(); + + for (int j = this.levelHeightAccessor.getMinSectionY(); j <= this.levelHeightAccessor.getMaxSectionY(); j++) { + long l = getKey(chunkPos, j); + Optional optional = Optional.ofNullable(packedChunk.sectionsByY.get(j)).map(object -> this.unpacker.apply(object, (Runnable)() -> this.setDirty(l))); + this.storage.put(l, optional); + optional.ifPresent(object -> { + this.onSectionLoad(l); + if (bl) { + this.setDirty(l); + } + }); + } + } + } } From 3c94af11de47687339495e25870c03ad4a643e4d Mon Sep 17 00:00:00 2001 From: jcw780 <30562395+jcw780@users.noreply.github.com> Date: Sun, 25 Jan 2026 11:33:49 -0500 Subject: [PATCH 02/22] plausibly working --- .../mixin/ai/poi/SectionStorageMixin.java | 179 +++++++++++++++--- 1 file changed, 153 insertions(+), 26 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java index 83e92cc83..4bb3390b4 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java @@ -1,8 +1,13 @@ package net.caffeinemc.mods.lithium.mixin.ai.poi; import com.google.common.collect.AbstractIterator; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.mojang.logging.LogUtils; import com.mojang.serialization.Codec; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.Dynamic; +import com.mojang.serialization.DynamicOps; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import net.caffeinemc.mods.lithium.common.util.Distances; @@ -10,26 +15,27 @@ import net.caffeinemc.mods.lithium.common.util.collections.ListeningLong2ObjectOpenHashMap; import net.caffeinemc.mods.lithium.common.util.functions.FunLongAnd5; import net.caffeinemc.mods.lithium.common.world.interests.RegionBasedStorageSectionExtended; +import net.minecraft.SharedConstants; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; import net.minecraft.core.RegistryAccess; import net.minecraft.core.SectionPos; -import net.minecraft.world.entity.ai.village.poi.PoiSection; +import net.minecraft.util.Util; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.LevelHeightAccessor; import net.minecraft.world.level.chunk.storage.ChunkIOErrorReporter; import net.minecraft.world.level.chunk.storage.SectionStorage; import net.minecraft.world.level.chunk.storage.SimpleRegionStorage; import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; import org.spongepowered.asm.mixin.*; import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Coerce; import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import java.util.BitSet; -import java.util.Collections; -import java.util.Objects; -import java.util.Optional; +import java.util.*; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; @@ -42,8 +48,8 @@ public abstract class SectionStorageMixin implements RegionBasedStorageSec @Final private Long2ObjectMap> storage; - @Shadow - protected abstract Optional get(long pos); + //@Shadow + //protected abstract Optional get(long pos); @Shadow @Final @@ -52,6 +58,21 @@ public abstract class SectionStorageMixin implements RegionBasedStorageSec @Shadow protected abstract void unpackChunk(ChunkPos chunkPos); + @Shadow + @Final + private Codec

codec; + + @Shadow + @Final + static Logger LOGGER; + + @Shadow + @Final + private Function packer; + + @Shadow + protected abstract boolean outsideStoredRange(long l); + private Long2ObjectOpenHashMap columns; @SuppressWarnings("rawtypes") @@ -196,27 +217,133 @@ protected R computeNext() { return Pos.SectionYCoord.getMaxYSectionInclusive(this.levelHeightAccessor); } + /** + * These mixins are to remove Optional.empty() sections stored in the storage hashmap. + * This is done to mitigate a memory leak in vanilla as these are never unloaded. The long keys will eventually + * build up as more chunks have been loaded. + * The new logic utilizes the Lithium columns lookup for populated sections instead of Optional.empty() stored + * instead the storage hashmap. + */ + @Inject(method = "unpackChunk(Lnet/minecraft/world/level/ChunkPos;Lnet/minecraft/world/level/chunk/storage/SectionStorage$PackedChunk;)V", at= @At(value = "HEAD")) + private void initializeColumnBitset(ChunkPos chunkPos, @Coerce Object ignored, CallbackInfo ci) { + final long pos = chunkPos.toLong(); + BitSet flags = this.columns.get(pos); + + if (flags == null) { + this.columns.put(pos, new BitSet(Pos.SectionYIndex.getNumYSections(this.levelHeightAccessor))); + } + } + + @Redirect(method = "unpackChunk(Lnet/minecraft/world/level/ChunkPos;Lnet/minecraft/world/level/chunk/storage/SectionStorage$PackedChunk;)V", at = @At(value = "INVOKE", target = "Lit/unimi/dsi/fastutil/longs/Long2ObjectMap;put(JLjava/lang/Object;)Ljava/lang/Object;")) + private Object removeOptionalEmpties(Long2ObjectMap instance, long l, Object o) { + if(o.equals(Optional.empty())) { + return null; + } + return instance.put(l, o); + } + + /** + * @author jcw780 + * @reason Match vanilla returns after removal of Optional.empty() sections from storage + */ @Overwrite - private void unpackChunk(ChunkPos chunkPos, - @Nullable SectionStorage.PackedChunk

packedChunk) { - if (packedChunk == null) { - for (int i = this.levelHeightAccessor.getMinSectionY(); i <= this.levelHeightAccessor.getMaxSectionY(); i++) { - this.storage.put(getKey(chunkPos, i), Optional.empty()); - } + @Nullable + public Optional get(long l) { // Has to be public even though it is protected in vanilla for some reason + // For some reason PoiManager::isVillageCenter updates called from SectionTracker::getComputedLevel can be out of bounds + final int columnIndex = Pos.SectionYIndex.fromSectionCoord(this.levelHeightAccessor, SectionPos.y(l)); + if (columnIndex < 0 || columnIndex >= Pos.SectionYIndex.getNumYSections(this.levelHeightAccessor)) { + return Optional.empty(); + } + + final int x = SectionPos.x(l); + final int z = SectionPos.z(l); + final BitSet flags = this.columns.get(ChunkPos.asLong(x, z)); + if (flags == null) { + return Optional.empty(); + } + + if (!flags.get(columnIndex)) { + return Optional.empty(); + } + + return this.storage.get(l); + } + + /** + * @author jcw780 + * @reason Match vanilla returns after removal of Optional.empty() sections from storage + */ + @Overwrite + public Optional getOrLoad(long l) { + if (this.outsideStoredRange(l)) { + return Optional.empty(); } else { - boolean bl = packedChunk.versionChanged(); - - for (int j = this.levelHeightAccessor.getMinSectionY(); j <= this.levelHeightAccessor.getMaxSectionY(); j++) { - long l = getKey(chunkPos, j); - Optional optional = Optional.ofNullable(packedChunk.sectionsByY.get(j)).map(object -> this.unpacker.apply(object, (Runnable)() -> this.setDirty(l))); - this.storage.put(l, optional); - optional.ifPresent(object -> { - this.onSectionLoad(l); - if (bl) { - this.setDirty(l); - } - }); + ChunkPos chunkPos = SectionPos.of(l).chunk(); + BitSet column = this.columns.get(chunkPos.toLong()); + + if (column == null) { + this.unpackChunk(chunkPos); + column = this.columns.get(chunkPos.toLong()); + if (column == null) { + throw new IllegalStateException("Column failed to generate for chunk"); + } + } + + final int columnIndex = Pos.SectionYIndex.fromSectionCoord(this.levelHeightAccessor, SectionPos.y(l)); + + if (!column.get(columnIndex)) { + return Optional.empty(); + } + + Optional optional = this.storage.get(l); + if (optional == null) { + throw Util.pauseInIde(new IllegalStateException()); + } else { + return optional; + } + + } + } + + /** + * @author jcw780 + * @reason Use Lithium columns look up in write chunk + */ + @Overwrite + private com.mojang.serialization.Dynamic writeChunk(ChunkPos chunkPos, DynamicOps dynamicOps) { + Map map = Maps.newHashMap(); + final int chunkX = chunkPos.x; + final int chunkZ = chunkPos.z; + BitSet sectionsWithPOI = this.columns.get(chunkPos.toLong()); + + if (sectionsWithPOI != null) { + int nextBit = sectionsWithPOI.nextSetBit(0); + while (nextBit >= 0) { + final int chunkY = Pos.SectionYCoord.fromSectionIndex(this.levelHeightAccessor, nextBit); + Optional next = this.storage.get(SectionPos.asLong(chunkX, chunkY, chunkZ)); + + // Find and advance to the next set bit + nextBit = sectionsWithPOI.nextSetBit(nextBit + 1); + + if (next.isPresent()) { + DataResult dataResult = this.codec.encodeStart(dynamicOps, (P)this.packer.apply(next.get())); + String string = Integer.toString(chunkY); + dataResult.resultOrPartial(LOGGER::error).ifPresent(object -> map.put(dynamicOps.createString(string), object)); + } } } + + return new Dynamic<>( + dynamicOps, + dynamicOps.createMap( + ImmutableMap.of( + dynamicOps.createString("Sections"), + dynamicOps.createMap(map), + dynamicOps.createString("DataVersion"), + dynamicOps.createInt(SharedConstants.getCurrentVersion().dataVersion().version()) + ) + ) + ); } + } From fbf2b065ad84460f19fa27c1b98cbee7f762ac2b Mon Sep 17 00:00:00 2001 From: jcw780 <30562395+jcw780@users.noreply.github.com> Date: Sun, 25 Jan 2026 18:01:04 -0500 Subject: [PATCH 03/22] minor section storage changes --- .../mixin/ai/poi/SectionStorageMixin.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java index 4bb3390b4..a49fac81a 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java @@ -251,17 +251,22 @@ private Object removeOptionalEmpties(Long2ObjectMap instance, long l, Object o) public Optional get(long l) { // Has to be public even though it is protected in vanilla for some reason // For some reason PoiManager::isVillageCenter updates called from SectionTracker::getComputedLevel can be out of bounds final int columnIndex = Pos.SectionYIndex.fromSectionCoord(this.levelHeightAccessor, SectionPos.y(l)); + + //Out of range sections will not be in the storage if (columnIndex < 0 || columnIndex >= Pos.SectionYIndex.getNumYSections(this.levelHeightAccessor)) { - return Optional.empty(); + return null; } final int x = SectionPos.x(l); final int z = SectionPos.z(l); final BitSet flags = this.columns.get(ChunkPos.asLong(x, z)); + + //If there are no flags, then the chunk was never loaded - so the section will not be in storage if (flags == null) { - return Optional.empty(); + return null; } + //Sections without POI sections are stored as Optional.empty() in vanilla if (!flags.get(columnIndex)) { return Optional.empty(); } @@ -272,6 +277,8 @@ public Optional get(long l) { // Has to be public even though it is protected /** * @author jcw780 * @reason Match vanilla returns after removal of Optional.empty() sections from storage + * Warning: This has more checks than vanilla to match vanilla behavior. + * Please use other methods if it is performance sensitive. */ @Overwrite public Optional getOrLoad(long l) { @@ -279,18 +286,11 @@ public Optional getOrLoad(long l) { return Optional.empty(); } else { ChunkPos chunkPos = SectionPos.of(l).chunk(); - BitSet column = this.columns.get(chunkPos.toLong()); - - if (column == null) { - this.unpackChunk(chunkPos); - column = this.columns.get(chunkPos.toLong()); - if (column == null) { - throw new IllegalStateException("Column failed to generate for chunk"); - } - } + BitSet column = this.lithium$getNonEmptyPOISections(chunkPos.x, chunkPos.z); final int columnIndex = Pos.SectionYIndex.fromSectionCoord(this.levelHeightAccessor, SectionPos.y(l)); + //Sections without POI sections are stored as Optional.empty() in vanilla if (!column.get(columnIndex)) { return Optional.empty(); } From 46b1979739a20ea68d8e6441c0eb13416a2e5ff3 Mon Sep 17 00:00:00 2001 From: jcw780 <30562395+jcw780@users.noreply.github.com> Date: Sun, 25 Jan 2026 18:06:08 -0500 Subject: [PATCH 04/22] revert additional template parameter in RegionBasedStorageSectionExtended --- .../world/interests/RegionBasedStorageSectionExtended.java | 2 +- .../world/interests/iterator/NearbyPointOfInterestStream.java | 4 ++-- .../iterator/SphereChunkOrderedPoiSetSpliterator.java | 4 ++-- .../caffeinemc/mods/lithium/mixin/ai/poi/PoiManagerMixin.java | 2 +- .../mods/lithium/mixin/ai/poi/SectionStorageMixin.java | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java index b229bc2e4..f44e2d2c2 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java @@ -8,7 +8,7 @@ import java.util.Optional; import java.util.function.Predicate; -public interface RegionBasedStorageSectionExtended { +public interface RegionBasedStorageSectionExtended { U lithium$getFirstInRangeInChunkColumn(int chunkX, int chunkZ, long deltaYSqMargin, diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/iterator/NearbyPointOfInterestStream.java b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/iterator/NearbyPointOfInterestStream.java index 6252f0168..ea266272b 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/iterator/NearbyPointOfInterestStream.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/iterator/NearbyPointOfInterestStream.java @@ -92,7 +92,7 @@ public class NearbyPointOfInterestStream extends Spliterators.AbstractSpliterato }; - private final RegionBasedStorageSectionExtended storage; + private final RegionBasedStorageSectionExtended storage; private final Predicate> typeSelector; private final PoiManager.Occupancy occupationStatus; @@ -142,7 +142,7 @@ public NearbyPointOfInterestStream( PoiManager.Occupancy status, @Nullable Predicate afterSortingPredicate, BlockPos origin, int radius, - RegionBasedStorageSectionExtended storage, + RegionBasedStorageSectionExtended storage, BiPredicate distanceLimit, Comparator sortOrder ) { diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/iterator/SphereChunkOrderedPoiSetSpliterator.java b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/iterator/SphereChunkOrderedPoiSetSpliterator.java index bc08afdb2..39752aea2 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/iterator/SphereChunkOrderedPoiSetSpliterator.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/iterator/SphereChunkOrderedPoiSetSpliterator.java @@ -20,7 +20,7 @@ import java.util.function.Predicate; public class SphereChunkOrderedPoiSetSpliterator extends Spliterators.AbstractSpliterator { - private final RegionBasedStorageSectionExtended storage; + private final RegionBasedStorageSectionExtended storage; private final int chunkYMin; private final int chunkLimit; @@ -40,7 +40,7 @@ public class SphereChunkOrderedPoiSetSpliterator extends Spliterators.AbstractSp int nextPoiSectionIndex; Iterator sectionIterator; - public SphereChunkOrderedPoiSetSpliterator(int radius, BlockPos origin, RegionBasedStorageSectionExtended storage, Predicate> typeFilter, PoiManager.Occupancy status) { + public SphereChunkOrderedPoiSetSpliterator(int radius, BlockPos origin, RegionBasedStorageSectionExtended storage, Predicate> typeFilter, PoiManager.Occupancy status) { super((long) ((origin.getX() + radius + 1 >> 4) - (origin.getX() - radius - 1 >> 4) + 1) * ((origin.getZ() + radius + 1 >> 4) - (origin.getZ() - radius - 1 >> 4) + 1), Spliterator.ORDERED); this.storage = storage; this.chunkYMin = this.storage.lithium$getChunkYMin(); diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/PoiManagerMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/PoiManagerMixin.java index 6da3f3a05..bd389a155 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/PoiManagerMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/PoiManagerMixin.java @@ -36,7 +36,7 @@ import java.util.stream.StreamSupport; @Mixin(PoiManager.class) -public abstract class PoiManagerMixin extends SectionStorage implements PointOfInterestStorageExtended, RegionBasedStorageSectionExtended { +public abstract class PoiManagerMixin extends SectionStorage implements PointOfInterestStorageExtended, RegionBasedStorageSectionExtended { public PoiManagerMixin(SimpleRegionStorage simpleRegionStorage, Codec codec, Function function, BiFunction biFunction, Function function2, RegistryAccess registryAccess, ChunkIOErrorReporter chunkIOErrorReporter, LevelHeightAccessor levelHeightAccessor) { diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java index a49fac81a..46034b408 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java @@ -42,7 +42,7 @@ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") // We don't get a choice, this is Minecraft's doing! @Mixin(SectionStorage.class) -public abstract class SectionStorageMixin implements RegionBasedStorageSectionExtended { +public abstract class SectionStorageMixin implements RegionBasedStorageSectionExtended { @Mutable @Shadow @Final From a2ea3e8e23dbfc1f7679827b0a4ff8ed7ac6d4dd Mon Sep 17 00:00:00 2001 From: jcw780 <30562395+jcw780@users.noreply.github.com> Date: Sun, 25 Jan 2026 23:08:54 -0500 Subject: [PATCH 05/22] minor renaming and commenting --- .../world/interests/RegionBasedStorageSectionExtended.java | 2 +- .../world/interests/iterator/NearbyPointOfInterestStream.java | 2 +- .../mods/lithium/mixin/ai/poi/SectionStorageMixin.java | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java index f44e2d2c2..5f9cddf66 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java @@ -31,5 +31,5 @@ public interface RegionBasedStorageSectionExtended { int lithium$getChunkYMaxInclusive(); - Optional lithium$getElementAt(long sectionPos); + Optional lithium$uncheckedGetElementAt(long sectionPos); } diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/iterator/NearbyPointOfInterestStream.java b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/iterator/NearbyPointOfInterestStream.java index ea266272b..05e23e880 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/iterator/NearbyPointOfInterestStream.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/iterator/NearbyPointOfInterestStream.java @@ -250,7 +250,7 @@ public boolean tryAdvance(Consumer action) { while (!this.isSectionListEmpty() && this.minCollectedElementDistanceSq >= this.getMinimumNextPotentialDistanceSq()) { final long sectionPos = queuedPOISections.get(this.queuedSectionsSearched++).sectionPos; this.nextSectionDistanceSq = this.getNextSectionDistanceSq(); - final Optional poiSection = this.storage.lithium$getElementAt(sectionPos); + final Optional poiSection = this.storage.lithium$uncheckedGetElementAt(sectionPos); //noinspection OptionalIsPresent if (poiSection.isPresent()) { ((PointOfInterestSetExtended) poiSection.get()).lithium$collectMatchingPoints(this.typeSelector, this.occupationStatus, this); diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java index 46034b408..8cff9efec 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java @@ -203,7 +203,7 @@ protected R computeNext() { } @Override - public Optional lithium$getElementAt(long sectionPos) { + public Optional lithium$uncheckedGetElementAt(long sectionPos) { return this.storage.get(sectionPos); } @@ -254,6 +254,7 @@ public Optional get(long l) { // Has to be public even though it is protected //Out of range sections will not be in the storage if (columnIndex < 0 || columnIndex >= Pos.SectionYIndex.getNumYSections(this.levelHeightAccessor)) { + //noinspection OptionalAssignedToNull return null; } @@ -263,6 +264,7 @@ public Optional get(long l) { // Has to be public even though it is protected //If there are no flags, then the chunk was never loaded - so the section will not be in storage if (flags == null) { + //noinspection OptionalAssignedToNull return null; } From 3a05e69e6c44ce52906b51fc2e4a8ed34b173eb3 Mon Sep 17 00:00:00 2001 From: jcw780 <30562395+jcw780@users.noreply.github.com> Date: Sun, 25 Jan 2026 23:30:13 -0500 Subject: [PATCH 06/22] favor loaded case for get getOrLoad overwrites --- .../mixin/ai/poi/SectionStorageMixin.java | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java index 8cff9efec..f55695aaa 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java @@ -250,30 +250,39 @@ private Object removeOptionalEmpties(Long2ObjectMap instance, long l, Object o) @Nullable public Optional get(long l) { // Has to be public even though it is protected in vanilla for some reason // For some reason PoiManager::isVillageCenter updates called from SectionTracker::getComputedLevel can be out of bounds - final int columnIndex = Pos.SectionYIndex.fromSectionCoord(this.levelHeightAccessor, SectionPos.y(l)); - - //Out of range sections will not be in the storage + final int y = SectionPos.y(l); + final int columnIndex = Pos.SectionYIndex.fromSectionCoord(this.levelHeightAccessor, y); + // Out of range sections will not be in the storage if (columnIndex < 0 || columnIndex >= Pos.SectionYIndex.getNumYSections(this.levelHeightAccessor)) { //noinspection OptionalAssignedToNull return null; } + // Move the access to the front to improve best case (already loaded) performance + Optional result = this.storage.get(l); + + // noinspection OptionalAssignedToNull + if (result != null) { + return result; + } + final int x = SectionPos.x(l); final int z = SectionPos.z(l); final BitSet flags = this.columns.get(ChunkPos.asLong(x, z)); - //If there are no flags, then the chunk was never loaded - so the section will not be in storage + // If there are no flags, then the chunk was never loaded - so the section will not be in storage if (flags == null) { - //noinspection OptionalAssignedToNull + // noinspection OptionalAssignedToNull return null; } - //Sections without POI sections are stored as Optional.empty() in vanilla + // Sections without POI sections are stored as Optional.empty() in vanilla if (!flags.get(columnIndex)) { return Optional.empty(); } - return this.storage.get(l); + // Section marked as having a POI Section is not in the storage hashmap - this should not happen + throw new IllegalStateException(String.format("Section %d %d %d missing from storage despite being marked as present", x, y, z)); } /** @@ -287,6 +296,13 @@ public Optional getOrLoad(long l) { if (this.outsideStoredRange(l)) { return Optional.empty(); } else { + Optional optional = this.storage.get(l); + + // noinspection OptionalAssignedToNull + if (optional != null) { // Section is in the storage - return early - doing this since most of the time it will be loaded + return optional; + } + ChunkPos chunkPos = SectionPos.of(l).chunk(); BitSet column = this.lithium$getNonEmptyPOISections(chunkPos.x, chunkPos.z); @@ -297,13 +313,13 @@ public Optional getOrLoad(long l) { return Optional.empty(); } - Optional optional = this.storage.get(l); + optional = this.storage.get(l); + // noinspection OptionalAssignedToNull if (optional == null) { throw Util.pauseInIde(new IllegalStateException()); } else { return optional; } - } } From c8a2f1ddd560ec544754b3545659b4477252fd99 Mon Sep 17 00:00:00 2001 From: jcw780 <30562395+jcw780@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:34:10 -0500 Subject: [PATCH 07/22] refactor memory reduction patches into own folder --- .../RegionBasedStorageSectionExtended.java | 7 + .../mixin/ai/poi/SectionStorageMixin.java | 120 +++------------- .../SectionStorageMixin.java | 136 ++++++++++++++++++ .../poi/reduce_poi_memory/package-info.java | 4 + common/src/main/resources/lithium.mixins.json | 17 +-- lithium-fabric-mixin-config.md | 4 + 6 files changed, 177 insertions(+), 111 deletions(-) create mode 100644 common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/SectionStorageMixin.java create mode 100644 common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/package-info.java diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java index 5f9cddf66..ede26c010 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java @@ -3,6 +3,8 @@ import net.caffeinemc.mods.lithium.common.util.functions.FunLongAnd5; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; +import net.minecraft.world.level.ChunkPos; +import org.jspecify.annotations.Nullable; import java.util.BitSet; import java.util.Optional; @@ -10,6 +12,11 @@ public interface RegionBasedStorageSectionExtended { + @Nullable + public BitSet lithium$getColumn(long chunkPos); + + BitSet lithium$getOrAddColumnIfNull(long chunkPos); + U lithium$getFirstInRangeInChunkColumn(int chunkX, int chunkZ, long deltaYSqMargin, BlockPos center, long radiusSq, diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java index f55695aaa..af3c4830d 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java @@ -48,9 +48,6 @@ public abstract class SectionStorageMixin implements RegionBasedStorageSec @Final private Long2ObjectMap> storage; - //@Shadow - //protected abstract Optional get(long pos); - @Shadow @Final protected LevelHeightAccessor levelHeightAccessor; @@ -70,9 +67,6 @@ public abstract class SectionStorageMixin implements RegionBasedStorageSec @Final private Function packer; - @Shadow - protected abstract boolean outsideStoredRange(long l); - private Long2ObjectOpenHashMap columns; @SuppressWarnings("rawtypes") @@ -126,6 +120,23 @@ private void onEntryAdded(long key, Optional value) { flags.set(y, value.isPresent()); } + @Override + @Nullable + public BitSet lithium$getColumn(long chunkPos) { + return this.columns.get(chunkPos); + } + + @Override + public BitSet lithium$getOrAddColumnIfNull(long chunkPos) { + BitSet column = this.columns.get(chunkPos); + + if (column == null) { + this.columns.put(chunkPos, column = new BitSet(Pos.SectionYIndex.getNumYSections(this.levelHeightAccessor))); + } + + return column; + } + @Override public U lithium$getFirstInRangeInChunkColumn(int chunkX, int chunkZ, long deltaYSqMargin, @@ -224,104 +235,7 @@ protected R computeNext() { * The new logic utilizes the Lithium columns lookup for populated sections instead of Optional.empty() stored * instead the storage hashmap. */ - @Inject(method = "unpackChunk(Lnet/minecraft/world/level/ChunkPos;Lnet/minecraft/world/level/chunk/storage/SectionStorage$PackedChunk;)V", at= @At(value = "HEAD")) - private void initializeColumnBitset(ChunkPos chunkPos, @Coerce Object ignored, CallbackInfo ci) { - final long pos = chunkPos.toLong(); - BitSet flags = this.columns.get(pos); - if (flags == null) { - this.columns.put(pos, new BitSet(Pos.SectionYIndex.getNumYSections(this.levelHeightAccessor))); - } - } - - @Redirect(method = "unpackChunk(Lnet/minecraft/world/level/ChunkPos;Lnet/minecraft/world/level/chunk/storage/SectionStorage$PackedChunk;)V", at = @At(value = "INVOKE", target = "Lit/unimi/dsi/fastutil/longs/Long2ObjectMap;put(JLjava/lang/Object;)Ljava/lang/Object;")) - private Object removeOptionalEmpties(Long2ObjectMap instance, long l, Object o) { - if(o.equals(Optional.empty())) { - return null; - } - return instance.put(l, o); - } - - /** - * @author jcw780 - * @reason Match vanilla returns after removal of Optional.empty() sections from storage - */ - @Overwrite - @Nullable - public Optional get(long l) { // Has to be public even though it is protected in vanilla for some reason - // For some reason PoiManager::isVillageCenter updates called from SectionTracker::getComputedLevel can be out of bounds - final int y = SectionPos.y(l); - final int columnIndex = Pos.SectionYIndex.fromSectionCoord(this.levelHeightAccessor, y); - // Out of range sections will not be in the storage - if (columnIndex < 0 || columnIndex >= Pos.SectionYIndex.getNumYSections(this.levelHeightAccessor)) { - //noinspection OptionalAssignedToNull - return null; - } - - // Move the access to the front to improve best case (already loaded) performance - Optional result = this.storage.get(l); - - // noinspection OptionalAssignedToNull - if (result != null) { - return result; - } - - final int x = SectionPos.x(l); - final int z = SectionPos.z(l); - final BitSet flags = this.columns.get(ChunkPos.asLong(x, z)); - - // If there are no flags, then the chunk was never loaded - so the section will not be in storage - if (flags == null) { - // noinspection OptionalAssignedToNull - return null; - } - - // Sections without POI sections are stored as Optional.empty() in vanilla - if (!flags.get(columnIndex)) { - return Optional.empty(); - } - - // Section marked as having a POI Section is not in the storage hashmap - this should not happen - throw new IllegalStateException(String.format("Section %d %d %d missing from storage despite being marked as present", x, y, z)); - } - - /** - * @author jcw780 - * @reason Match vanilla returns after removal of Optional.empty() sections from storage - * Warning: This has more checks than vanilla to match vanilla behavior. - * Please use other methods if it is performance sensitive. - */ - @Overwrite - public Optional getOrLoad(long l) { - if (this.outsideStoredRange(l)) { - return Optional.empty(); - } else { - Optional optional = this.storage.get(l); - - // noinspection OptionalAssignedToNull - if (optional != null) { // Section is in the storage - return early - doing this since most of the time it will be loaded - return optional; - } - - ChunkPos chunkPos = SectionPos.of(l).chunk(); - BitSet column = this.lithium$getNonEmptyPOISections(chunkPos.x, chunkPos.z); - - final int columnIndex = Pos.SectionYIndex.fromSectionCoord(this.levelHeightAccessor, SectionPos.y(l)); - - //Sections without POI sections are stored as Optional.empty() in vanilla - if (!column.get(columnIndex)) { - return Optional.empty(); - } - - optional = this.storage.get(l); - // noinspection OptionalAssignedToNull - if (optional == null) { - throw Util.pauseInIde(new IllegalStateException()); - } else { - return optional; - } - } - } /** * @author jcw780 diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/SectionStorageMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/SectionStorageMixin.java new file mode 100644 index 000000000..83b9ee252 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/SectionStorageMixin.java @@ -0,0 +1,136 @@ +package net.caffeinemc.mods.lithium.mixin.ai.poi.reduce_poi_memory; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import net.caffeinemc.mods.lithium.common.util.Pos; +import net.caffeinemc.mods.lithium.common.world.interests.RegionBasedStorageSectionExtended; +import net.minecraft.core.SectionPos; +import net.minecraft.util.Util; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.chunk.storage.SectionStorage; +import org.jspecify.annotations.Nullable; +import org.spongepowered.asm.mixin.*; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Coerce; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.BitSet; +import java.util.Optional; + +@Mixin(SectionStorage.class) +public abstract class SectionStorageMixin implements RegionBasedStorageSectionExtended { + @Mutable + @Shadow + @Final + private Long2ObjectMap> storage; + + @Shadow + @Final + protected LevelHeightAccessor levelHeightAccessor; + + @Shadow + protected abstract boolean outsideStoredRange(long l); + + /* Since we are removing Optional.empty() sections, we use the Lithium column lookup in its place. + * Because these are normally added whenever a new entry is added, we would have missing columns when a whole chunk + * lacks POI sections. + * This means we must always add the column whenever a chunk is loaded. + */ + @Inject(method = "unpackChunk(Lnet/minecraft/world/level/ChunkPos;Lnet/minecraft/world/level/chunk/storage/SectionStorage$PackedChunk;)V", at= @At(value = "HEAD")) + private void initializeColumnBitset(ChunkPos chunkPos, @Coerce Object ignored, CallbackInfo ci) { + final long pos = chunkPos.toLong(); + this.lithium$getOrAddColumnIfNull(pos); + } + + // Do not add Optional.empty() sections into the map + @Redirect(method = "unpackChunk(Lnet/minecraft/world/level/ChunkPos;Lnet/minecraft/world/level/chunk/storage/SectionStorage$PackedChunk;)V", at = @At(value = "INVOKE", target = "Lit/unimi/dsi/fastutil/longs/Long2ObjectMap;put(JLjava/lang/Object;)Ljava/lang/Object;")) + private Object removeOptionalEmpties(Long2ObjectMap instance, long l, Object o) { + if(o.equals(Optional.empty())) { + return null; + } + return instance.put(l, o); + } + + /** + * @author jcw780 + * @reason Match vanilla returns after removal of Optional.empty() sections from storage + */ + @Overwrite + @Nullable + public Optional get(long l) { // Has to be public even though it is protected in vanilla for some reason + // For some reason PoiManager::isVillageCenter updates called from SectionTracker::getComputedLevel can be out of bounds + final int y = SectionPos.y(l); + final int columnIndex = Pos.SectionYIndex.fromSectionCoord(this.levelHeightAccessor, y); + // Out of range sections will not be in the storage + if (columnIndex < 0 || columnIndex >= Pos.SectionYIndex.getNumYSections(this.levelHeightAccessor)) { + //noinspection OptionalAssignedToNull + return null; + } + + // Move the access to the front to improve best case (already loaded) performance + Optional result = this.storage.get(l); + + // noinspection OptionalAssignedToNull + if (result != null) { + return result; + } + + final int x = SectionPos.x(l); + final int z = SectionPos.z(l); + final BitSet flags = this.lithium$getColumn(ChunkPos.asLong(x, z)); + + // If there are no flags, then the chunk was never loaded - so the section will not be in storage + if (flags == null) { + // noinspection OptionalAssignedToNull + return null; + } + + // Sections without POI sections are stored as Optional.empty() in vanilla + if (!flags.get(columnIndex)) { + return Optional.empty(); + } + + // Section marked as having a POI Section is not in the storage hashmap - this should not happen + throw new IllegalStateException(String.format("Section %d %d %d missing from storage despite being marked as present", x, y, z)); + } + + /** + * @author jcw780 + * @reason Match vanilla returns after removal of Optional.empty() sections from storage + * Warning: This has more checks than vanilla to match vanilla behavior. + * Please use other methods if it is performance sensitive. + */ + @Overwrite + public Optional getOrLoad(long l) { + if (this.outsideStoredRange(l)) { + return Optional.empty(); + } else { + Optional optional = this.storage.get(l); + + // noinspection OptionalAssignedToNull + if (optional != null) { // Section is in the storage - return early - doing this since most of the time it will be loaded + return optional; + } + + ChunkPos chunkPos = SectionPos.of(l).chunk(); + BitSet column = this.lithium$getNonEmptyPOISections(chunkPos.x, chunkPos.z); + + final int columnIndex = Pos.SectionYIndex.fromSectionCoord(this.levelHeightAccessor, SectionPos.y(l)); + + //Sections without POI sections are stored as Optional.empty() in vanilla + if (!column.get(columnIndex)) { + return Optional.empty(); + } + + optional = this.storage.get(l); + // noinspection OptionalAssignedToNull + if (optional == null) { + throw Util.pauseInIde(new IllegalStateException()); + } else { + return optional; + } + } + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/package-info.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/package-info.java new file mode 100644 index 000000000..0c450180f --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/package-info.java @@ -0,0 +1,4 @@ +@MixinConfigOption(description = "Reduce memory consumption of POI system") +package net.caffeinemc.mods.lithium.mixin.ai.poi.reduce_poi_memory; + +import net.caffeinemc.gradle.MixinConfigOption; \ No newline at end of file diff --git a/common/src/main/resources/lithium.mixins.json b/common/src/main/resources/lithium.mixins.json index b9b0ceac9..e3d55a26c 100644 --- a/common/src/main/resources/lithium.mixins.json +++ b/common/src/main/resources/lithium.mixins.json @@ -1,12 +1,12 @@ { - "package" : "net.caffeinemc.mods.lithium.mixin", - "required" : true, - "compatibilityLevel" : "JAVA_21", - "plugin" : "net.caffeinemc.mods.lithium.mixin.LithiumMixinPlugin", - "injectors" : { - "defaultRequire" : 1 + "package": "net.caffeinemc.mods.lithium.mixin", + "required": true, + "compatibilityLevel": "JAVA_21", + "plugin": "net.caffeinemc.mods.lithium.mixin.LithiumMixinPlugin", + "injectors": { + "defaultRequire": 1 }, - "mixins" : [ + "mixins": [ "ai.non_poi_block_search.HoglinSpecificSensorMixin", "ai.non_poi_block_search.MoveToBlockGoalMixin", "ai.non_poi_block_search.PiglinSpecificSensorMixin", @@ -22,6 +22,7 @@ "ai.poi.SectionStorageMixin", "ai.poi.fast_portals.PoiManagerMixin", "ai.poi.fast_portals.PortalForcerMixin", + "ai.poi.reduce_poi_memory.SectionStorageMixin", "ai.poi.tasks.AcquirePoiMixin", "ai.poi.tasks.LocateHidingPlaceMixin", "ai.poi.tasks.RaiderEntityAttackHomeGoalMixin", @@ -257,7 +258,7 @@ "world.temperature_cache.BiomeMixin", "world.tick_scheduler.LevelChunkTicksMixin" ], - "client" : [ + "client": [ "chunk.entity_class_groups.ClientLevelMixin", "debug.palette.ClientBoundLevelChunkPacketDataAccessor", "debug.palette.ClientPacketListenerMixin", diff --git a/lithium-fabric-mixin-config.md b/lithium-fabric-mixin-config.md index 5d9dcda38..7e3d86065 100644 --- a/lithium-fabric-mixin-config.md +++ b/lithium-fabric-mixin-config.md @@ -41,6 +41,10 @@ Implements a faster POI search (default: `true`) Portal search uses the faster POI search and optimized loaded state caching +### `mixin.ai.poi.reduce_poi_memory` +(default: `true`) +Reduce memory consumption of POI system + ### `mixin.ai.poi.tasks` (default: `true`) Mob Tasks which search for POIs use the optimized POI search From b530c402ff3d5ccb963fa1e697dfc3ea33b781bb Mon Sep 17 00:00:00 2001 From: jcw780 <30562395+jcw780@users.noreply.github.com> Date: Tue, 27 Jan 2026 18:29:08 -0500 Subject: [PATCH 08/22] updating comments for reduce_poi_memory section storage get method overwrite --- .../mixin/ai/poi/reduce_poi_memory/SectionStorageMixin.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/SectionStorageMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/SectionStorageMixin.java index 83b9ee252..6bb310c11 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/SectionStorageMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/SectionStorageMixin.java @@ -56,6 +56,8 @@ private Object removeOptionalEmpties(Long2ObjectMap instance, long l, Object o) /** * @author jcw780 * @reason Match vanilla returns after removal of Optional.empty() sections from storage + * Warning: This has more checks than vanilla to match vanilla behavior. + * Please use other methods if it is performance sensitive. */ @Overwrite @Nullable From 596e37b3d2166d1980d468e125659c6e8fbab667 Mon Sep 17 00:00:00 2001 From: jcw780 <30562395+jcw780@users.noreply.github.com> Date: Tue, 27 Jan 2026 22:13:25 -0500 Subject: [PATCH 09/22] added fallback getInChunk optimization to avoid successive getOrLoad calls --- .../SingleChunkPointOfInterestStream.java | 84 +++++++++++++++++++ .../lithium/mixin/ai/poi/PoiManagerMixin.java | 13 +++ 2 files changed, 97 insertions(+) create mode 100644 common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/iterator/SingleChunkPointOfInterestStream.java diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/iterator/SingleChunkPointOfInterestStream.java b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/iterator/SingleChunkPointOfInterestStream.java new file mode 100644 index 000000000..f8ca329b2 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/iterator/SingleChunkPointOfInterestStream.java @@ -0,0 +1,84 @@ +package net.caffeinemc.mods.lithium.common.world.interests.iterator; + +import net.caffeinemc.mods.lithium.common.world.interests.PointOfInterestSetExtended; +import net.caffeinemc.mods.lithium.common.world.interests.RegionBasedStorageSectionExtended; +import net.minecraft.core.Holder; +import net.minecraft.core.SectionPos; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.entity.ai.village.poi.PoiRecord; +import net.minecraft.world.entity.ai.village.poi.PoiSection; +import net.minecraft.world.entity.ai.village.poi.PoiType; +import net.minecraft.world.level.ChunkPos; + +import java.util.BitSet; +import java.util.Iterator; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Stream-less fallback for PoiManager::getInChunk in case search is not otherwise optimized + * @author jcw780 + */ +public class SingleChunkPointOfInterestStream extends Spliterators.AbstractSpliterator { + final int chunkX; + final int chunkZ; + final int chunkYMin; + RegionBasedStorageSectionExtended storage; + final BitSet chunkPoiSections; + int nextPoiSectionIndex; + Iterator sectionIterator; + private final Predicate> typeFilter; + private final PoiManager.Occupancy status; + + public SingleChunkPointOfInterestStream(Predicate> typeFilter, ChunkPos chunkPos, PoiManager.Occupancy status, RegionBasedStorageSectionExtended storage) { + super(Long.MAX_VALUE, Spliterator.ORDERED); + this.chunkX = chunkPos.x; + this.chunkZ = chunkPos.z; + this.storage = storage; + + this.chunkYMin = this.storage.lithium$getChunkYMin(); + + this.chunkPoiSections = this.storage.lithium$getNonEmptyPOISections(this.chunkX, this.chunkZ); + this.typeFilter = typeFilter; + this.status = status; + } + + @Override + public boolean tryAdvance(Consumer action) { + do { + while (this.sectionIterator != null && this.sectionIterator.hasNext()) { + PoiRecord next = this.sectionIterator.next(); + if (status.getTest().test(next)) { + action.accept(next); + return true; + } + } + + } while (this.nextSection()); + return false; + } + + private boolean nextSection() { + int nextSectionIndex; + while ((nextSectionIndex = this.chunkPoiSections.nextSetBit(this.nextPoiSectionIndex)) != -1) { + this.nextPoiSectionIndex = nextSectionIndex + 1; + int chunkY = nextSectionIndex + this.chunkYMin; + this.sectionIterator = this.getSectionIterator(this.chunkX, chunkY, this.chunkZ); + if (this.sectionIterator != null && this.sectionIterator.hasNext()) { + return true; + } + } + return false; + } + + private Iterator getSectionIterator(int chunkX, int chunkY, int chunkZ) { + // Note: chunk is already "POI loaded" by lithium$getNonEmptyPOISections + PoiSection poiSection = this.storage.lithium$uncheckedGetElementAt(SectionPos.asLong(chunkX, chunkY, chunkZ)).orElse(null); + if (poiSection == null) { + return null; + } + return ((PointOfInterestSetExtended) poiSection).lithium$iterate(this.typeFilter); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/PoiManagerMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/PoiManagerMixin.java index bd389a155..2c1f00dad 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/PoiManagerMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/PoiManagerMixin.java @@ -8,6 +8,7 @@ import net.caffeinemc.mods.lithium.common.world.interests.PointOfInterestStorageExtended; import net.caffeinemc.mods.lithium.common.world.interests.RegionBasedStorageSectionExtended; import net.caffeinemc.mods.lithium.common.world.interests.iterator.NearbyPointOfInterestStream; +import net.caffeinemc.mods.lithium.common.world.interests.iterator.SingleChunkPointOfInterestStream; import net.caffeinemc.mods.lithium.common.world.interests.iterator.SinglePointOfInterestTypeFilter; import net.caffeinemc.mods.lithium.common.world.interests.iterator.SphereChunkOrderedPoiSetSpliterator; import net.minecraft.core.BlockPos; @@ -15,10 +16,12 @@ import net.minecraft.core.RegistryAccess; import net.minecraft.core.SectionPos; import net.minecraft.util.RandomSource; +import net.minecraft.util.VisibleForDebug; import net.minecraft.world.entity.ai.village.poi.PoiManager; import net.minecraft.world.entity.ai.village.poi.PoiRecord; import net.minecraft.world.entity.ai.village.poi.PoiSection; import net.minecraft.world.entity.ai.village.poi.PoiType; +import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.LevelHeightAccessor; import net.minecraft.world.level.border.WorldBorder; import net.minecraft.world.level.chunk.storage.ChunkIOErrorReporter; @@ -240,6 +243,16 @@ public Stream getInRange(Predicate> predicate, BlockP return StreamSupport.stream(new SphereChunkOrderedPoiSetSpliterator(radius, center, this, predicate, status), false); } + /** + * @author jcw780 + * @reason Fall back to column checked accesses in case POI search is otherwise unoptimized + */ + @VisibleForDebug + @Overwrite + public Stream getInChunk(Predicate> predicate, ChunkPos chunkPos, PoiManager.Occupancy occupancy) { + return StreamSupport.stream(new SingleChunkPointOfInterestStream(predicate, chunkPos, occupancy, this), false); + } + @Override public Optional lithium$findNearestForPortalLogic(BlockPos origin, int radius, Holder type, PoiManager.Occupancy status, From 7810df197a9cf0514d6df02acb951d11ec5b22e5 Mon Sep 17 00:00:00 2001 From: jcw780 <30562395+jcw780@users.noreply.github.com> Date: Wed, 28 Jan 2026 00:21:52 -0500 Subject: [PATCH 10/22] begin work on poi unloading --- .../ListeningLong2ObjectOpenHashMap.java | 7 ++ .../common/world/interests/PoiUnloading.java | 8 ++ .../RegionBasedStorageSectionExtended.java | 8 ++ .../mixin/ai/poi/SectionStorageMixin.java | 9 ++- .../world/poi_unloading/ChunkMapMixin.java | 27 +++++++ .../world/poi_unloading/PoiManagerMixin.java | 22 ++++++ .../poi_unloading/SectionStorageMixin.java | 73 +++++++++++++++++++ .../world/poi_unloading/package-info.java | 14 ++++ .../src/main/resources/lithium.accesswidener | 1 + common/src/main/resources/lithium.mixins.json | 19 +++-- lithium-fabric-mixin-config.md | 7 ++ 11 files changed, 183 insertions(+), 12 deletions(-) create mode 100644 common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/PoiUnloading.java create mode 100644 common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/ChunkMapMixin.java create mode 100644 common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/PoiManagerMixin.java create mode 100644 common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/SectionStorageMixin.java create mode 100644 common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/package-info.java diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/common/util/collections/ListeningLong2ObjectOpenHashMap.java b/common/src/main/java/net/caffeinemc/mods/lithium/common/util/collections/ListeningLong2ObjectOpenHashMap.java index ce2ad563a..4428f8f7d 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/common/util/collections/ListeningLong2ObjectOpenHashMap.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/common/util/collections/ListeningLong2ObjectOpenHashMap.java @@ -40,6 +40,13 @@ public V remove(long k) { return ret; } + /** + * Only use this if you are sure you are replicating the intended listening effect + */ + public V removeSilently(long k) { + return super.remove(k); + } + public interface Callback { void apply(long key, V value); } diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/PoiUnloading.java b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/PoiUnloading.java new file mode 100644 index 000000000..55c239488 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/PoiUnloading.java @@ -0,0 +1,8 @@ +package net.caffeinemc.mods.lithium.common.world.interests; + +import net.minecraft.world.level.ChunkPos; + +public interface PoiUnloading { + boolean lithium$shouldUnloadChunkPOIs(ChunkPos chunkPos); + void lithium$unloadChunkPOIs(ChunkPos chunkPos); +} diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java index ede26c010..00cc90551 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java @@ -34,6 +34,14 @@ public interface RegionBasedStorageSectionExtended { BitSet lithium$getNonEmptyPOISections(int chunkX, int chunkZ); + /** + * Manually remove a chunk section column bitset - only used when unloading chunk with no POISections + * + * @param chunkPos + * @return + */ + BitSet lithium$removeColumn(ChunkPos chunkPos); + int lithium$getChunkYMin(); int lithium$getChunkYMaxInclusive(); diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java index af3c4830d..d2b8bb41f 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java @@ -3,7 +3,6 @@ import com.google.common.collect.AbstractIterator; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; -import com.mojang.logging.LogUtils; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import com.mojang.serialization.Dynamic; @@ -20,7 +19,6 @@ import net.minecraft.core.Holder; import net.minecraft.core.RegistryAccess; import net.minecraft.core.SectionPos; -import net.minecraft.util.Util; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.LevelHeightAccessor; import net.minecraft.world.level.chunk.storage.ChunkIOErrorReporter; @@ -30,9 +28,7 @@ import org.slf4j.Logger; import org.spongepowered.asm.mixin.*; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Coerce; import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import java.util.*; @@ -126,6 +122,11 @@ private void onEntryAdded(long key, Optional value) { return this.columns.get(chunkPos); } + @Override + public BitSet lithium$removeColumn(ChunkPos chunkPos) { + return this.columns.remove(chunkPos.toLong()); + } + @Override public BitSet lithium$getOrAddColumnIfNull(long chunkPos) { BitSet column = this.columns.get(chunkPos); diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/ChunkMapMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/ChunkMapMixin.java new file mode 100644 index 000000000..c3dac12bf --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/ChunkMapMixin.java @@ -0,0 +1,27 @@ +package net.caffeinemc.mods.lithium.mixin.minimal_nonvanilla.world.poi_unloading; + +import net.caffeinemc.mods.lithium.common.world.interests.PoiUnloading; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.concurrent.CompletableFuture; + +@Mixin(ChunkMap.class) +public abstract class ChunkMapMixin { + @Shadow + @Final + private PoiManager poiManager; + + // Inject after the chunkMap save because PoiManager::flush is called + @Inject(method = "method_60440", at = @At(value = "INVOKE", target = "net/minecraft/server/level/ChunkMap.save (Lnet/minecraft/world/level/chunk/ChunkAccess;)Z", shift = At.Shift.AFTER)) + void unloadPOI(ChunkHolder chunkHolder, CompletableFuture completableFuture, long l, CallbackInfo ci) { + ((PoiUnloading) this.poiManager).lithium$unloadChunkPOIs(chunkHolder.getPos()); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/PoiManagerMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/PoiManagerMixin.java new file mode 100644 index 000000000..3131c7cf8 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/PoiManagerMixin.java @@ -0,0 +1,22 @@ +package net.caffeinemc.mods.lithium.mixin.minimal_nonvanilla.world.poi_unloading; + +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongSet; +import net.caffeinemc.mods.lithium.common.world.interests.PoiUnloading; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.level.ChunkPos; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(PoiManager.class) +public abstract class PoiManagerMixin implements PoiUnloading { + @Shadow + @Final + private LongSet loadedChunks = new LongOpenHashSet(); + + @Override + public boolean lithium$shouldUnloadChunkPOIs(ChunkPos chunkPos) { + return !loadedChunks.contains(chunkPos.toLong()); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/SectionStorageMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/SectionStorageMixin.java new file mode 100644 index 000000000..8af42e83d --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/SectionStorageMixin.java @@ -0,0 +1,73 @@ +package net.caffeinemc.mods.lithium.mixin.minimal_nonvanilla.world.poi_unloading; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.LongSet; +import net.caffeinemc.mods.lithium.common.util.Pos; +import net.caffeinemc.mods.lithium.common.util.collections.ListeningLong2ObjectOpenHashMap; +import net.caffeinemc.mods.lithium.common.world.interests.PoiUnloading; +import net.caffeinemc.mods.lithium.common.world.interests.RegionBasedStorageSectionExtended; +import net.minecraft.core.SectionPos; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.chunk.storage.SectionStorage; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.BitSet; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +@Mixin(SectionStorage.class) +public abstract class SectionStorageMixin implements PoiUnloading, RegionBasedStorageSectionExtended { + @Shadow + protected abstract void writeChunk(ChunkPos chunkPos); + + @Shadow + @Final + Object loadLock; + + @Shadow + @Final + private LongSet loadedChunks; + + @Shadow + @Final + private Long2ObjectMap>>> pendingLoads; + + @Shadow + @Final + private Long2ObjectMap> storage; + + @Shadow + @Final + protected LevelHeightAccessor levelHeightAccessor; + + @Override + public void lithium$unloadChunkPOIs(ChunkPos chunkPos) { + if (!this.lithium$shouldUnloadChunkPOIs(chunkPos)) { + return; + } + + // Note: shouldn't need to write chunks since it's after PoiManager::flush in chunk unload + + // Remove column bitset + BitSet chunkSections = this.lithium$removeColumn(chunkPos); + if (chunkSections == null) { + return; + } + + final int chunkYMin = this.lithium$getChunkYMin(); + int nextSectionY = 0; + while ((nextSectionY = chunkSections.nextSetBit(nextSectionY)) != -1) { + ((ListeningLong2ObjectOpenHashMap)this.storage).removeSilently( + SectionPos.asLong(chunkPos.x, chunkYMin + nextSectionY, chunkPos.z) + ); + } + + synchronized (this.loadLock) { + this.loadedChunks.remove(chunkPos.toLong()); + this.pendingLoads.remove(chunkPos.toLong()); + } + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/package-info.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/package-info.java new file mode 100644 index 000000000..ade57c330 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/package-info.java @@ -0,0 +1,14 @@ +@MixinConfigOption( + description = "Unload non-portal forced POIs. " + + "Note: This will cause extra chunk loading when portals run ensureLoadedAndValid into unload areas." + + "However, this will only be detectable during the first portal load into an area with valid POI sections that were loaded by non-portal means and then unloaded.", + depends = { + @MixinConfigDependency(dependencyPath = "mixin.ai.poi"), + @MixinConfigDependency(dependencyPath = "mixin.ai.poi.reduce_poi_memory"), + } +) + +package net.caffeinemc.mods.lithium.mixin.minimal_nonvanilla.world.poi_unloading; + +import net.caffeinemc.gradle.MixinConfigDependency; +import net.caffeinemc.gradle.MixinConfigOption; diff --git a/common/src/main/resources/lithium.accesswidener b/common/src/main/resources/lithium.accesswidener index 2b62871c9..b46307260 100644 --- a/common/src/main/resources/lithium.accesswidener +++ b/common/src/main/resources/lithium.accesswidener @@ -23,6 +23,7 @@ accessible field net/minecraft/server/level/ChunkMap level Lnet/minecraft/server accessible field net/minecraft/world/level/chunk/Strategy LINEAR_PALETTE_FACTORY Lnet/minecraft/world/level/chunk/Palette$Factory; accessible field net/minecraft/world/level/chunk/Strategy SINGLE_VALUE_PALETTE_FACTORY Lnet/minecraft/world/level/chunk/Palette$Factory; accessible class net/minecraft/world/level/chunk/LevelChunk$RebindableTickingBlockEntityWrapper +accessible class net/minecraft/world/level/chunk/storage/SectionStorage$PackedChunk accessible method net/minecraft/world/level/chunk/storage/SectionStorage getOrLoad (J)Ljava/util/Optional; accessible method net/minecraft/world/entity/ai/village/poi/PoiRecord acquireTicket ()Z accessible class net/minecraft/world/entity/ai/behavior/AcquirePoi$JitteredLinearRetry \ No newline at end of file diff --git a/common/src/main/resources/lithium.mixins.json b/common/src/main/resources/lithium.mixins.json index e3d55a26c..b2d9d887e 100644 --- a/common/src/main/resources/lithium.mixins.json +++ b/common/src/main/resources/lithium.mixins.json @@ -1,12 +1,12 @@ { - "package": "net.caffeinemc.mods.lithium.mixin", - "required": true, - "compatibilityLevel": "JAVA_21", - "plugin": "net.caffeinemc.mods.lithium.mixin.LithiumMixinPlugin", - "injectors": { - "defaultRequire": 1 + "package" : "net.caffeinemc.mods.lithium.mixin", + "required" : true, + "compatibilityLevel" : "JAVA_21", + "plugin" : "net.caffeinemc.mods.lithium.mixin.LithiumMixinPlugin", + "injectors" : { + "defaultRequire" : 1 }, - "mixins": [ + "mixins" : [ "ai.non_poi_block_search.HoglinSpecificSensorMixin", "ai.non_poi_block_search.MoveToBlockGoalMixin", "ai.non_poi_block_search.PiglinSpecificSensorMixin", @@ -169,6 +169,9 @@ "minimal_nonvanilla.world.block_entity_ticking.support_cache.DirectBlockEntityTickInvokerMixin", "minimal_nonvanilla.world.block_entity_ticking.support_cache.LevelChunkMixin", "minimal_nonvanilla.world.expiring_chunk_tickets.TicketStorageMixin", + "minimal_nonvanilla.world.poi_unloading.ChunkMapMixin", + "minimal_nonvanilla.world.poi_unloading.PoiManagerMixin", + "minimal_nonvanilla.world.poi_unloading.SectionStorageMixin", "shapes.blockstate_cache.BlockMixin", "shapes.lazy_shape_context.EntityCollisionContextMixin", "shapes.optimized_matching.ShapesMixin", @@ -258,7 +261,7 @@ "world.temperature_cache.BiomeMixin", "world.tick_scheduler.LevelChunkTicksMixin" ], - "client": [ + "client" : [ "chunk.entity_class_groups.ClientLevelMixin", "debug.palette.ClientBoundLevelChunkPacketDataAccessor", "debug.palette.ClientPacketListenerMixin", diff --git a/lithium-fabric-mixin-config.md b/lithium-fabric-mixin-config.md index 7e3d86065..3e453f484 100644 --- a/lithium-fabric-mixin-config.md +++ b/lithium-fabric-mixin-config.md @@ -510,6 +510,13 @@ Requirements: (default: `true`) Only check positions with expiring tickets during ticket expiration. Can cause reordering of chunks unloading. The chunk unloading order in vanilla is predictable, but depends on the hash of the chunk position of the tickets and the hashes of the other chunk tickets, and the order of creation of the chunk tickets when hash collisions occur. No known contraptions depend on the unload order. +### `mixin.minimal_nonvanilla.world.poi_unloading` +(default: `true`) +Unload non-portal forced POIs. Note: This will cause extra chunk loading when portals run ensureLoadedAndValid into unload areas.However, this will only be detectable during the first portal load into an area with valid POI sections that were loaded by non-portal means and then unloaded. +Requirements: +- `mixin.ai.poi=true` +- `mixin.ai.poi.reduce_poi_memory=true` + ### `mixin.shapes` (default: `true`) Various VoxelShape optimizations From 239d00deb1760e04dea0dc39c334c99449dee25d Mon Sep 17 00:00:00 2001 From: jcw780 <30562395+jcw780@users.noreply.github.com> Date: Wed, 28 Jan 2026 11:03:22 -0500 Subject: [PATCH 11/22] update neoforge config --- lithium-neoforge-mixin-config.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lithium-neoforge-mixin-config.md b/lithium-neoforge-mixin-config.md index c1943eed1..005360aac 100644 --- a/lithium-neoforge-mixin-config.md +++ b/lithium-neoforge-mixin-config.md @@ -41,6 +41,10 @@ Implements a faster POI search (default: `true`) Portal search uses the faster POI search and optimized loaded state caching +### `mixin.ai.poi.reduce_poi_memory` +(default: `true`) +Reduce memory consumption of POI system + ### `mixin.ai.poi.tasks` (default: `true`) Mob Tasks which search for POIs use the optimized POI search @@ -487,6 +491,13 @@ Requirements: (default: `true`) Only check positions with expiring tickets during ticket expiration. Can cause reordering of chunks unloading. The chunk unloading order in vanilla is predictable, but depends on the hash of the chunk position of the tickets and the hashes of the other chunk tickets, and the order of creation of the chunk tickets when hash collisions occur. No known contraptions depend on the unload order. +### `mixin.minimal_nonvanilla.world.poi_unloading` +(default: `true`) +Unload non-portal forced POIs. Note: This will cause extra chunk loading when portals run ensureLoadedAndValid into unload areas.However, this will only be detectable during the first portal load into an area with valid POI sections that were loaded by non-portal means and then unloaded. +Requirements: +- `mixin.ai.poi=true` +- `mixin.ai.poi.reduce_poi_memory=true` + ### `mixin.shapes` (default: `true`) Various VoxelShape optimizations From c06df97e7cddeed1698d364b023b00faa77fdafd Mon Sep 17 00:00:00 2001 From: jcw780 <30562395+jcw780@users.noreply.github.com> Date: Wed, 28 Jan 2026 11:03:41 -0500 Subject: [PATCH 12/22] fix poi unloading infinite loop --- .../world/poi_unloading/SectionStorageMixin.java | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/SectionStorageMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/SectionStorageMixin.java index 8af42e83d..131958ed3 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/SectionStorageMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/SectionStorageMixin.java @@ -2,7 +2,6 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.LongSet; -import net.caffeinemc.mods.lithium.common.util.Pos; import net.caffeinemc.mods.lithium.common.util.collections.ListeningLong2ObjectOpenHashMap; import net.caffeinemc.mods.lithium.common.world.interests.PoiUnloading; import net.caffeinemc.mods.lithium.common.world.interests.RegionBasedStorageSectionExtended; @@ -39,10 +38,6 @@ public abstract class SectionStorageMixin implements PoiUnloading, RegionBase @Final private Long2ObjectMap> storage; - @Shadow - @Final - protected LevelHeightAccessor levelHeightAccessor; - @Override public void lithium$unloadChunkPOIs(ChunkPos chunkPos) { if (!this.lithium$shouldUnloadChunkPOIs(chunkPos)) { @@ -58,9 +53,9 @@ public abstract class SectionStorageMixin implements PoiUnloading, RegionBase } final int chunkYMin = this.lithium$getChunkYMin(); - int nextSectionY = 0; - while ((nextSectionY = chunkSections.nextSetBit(nextSectionY)) != -1) { - ((ListeningLong2ObjectOpenHashMap)this.storage).removeSilently( + int nextSectionY = -1; + while ((nextSectionY = chunkSections.nextSetBit(nextSectionY + 1)) != -1) { + ((ListeningLong2ObjectOpenHashMap>)this.storage).removeSilently( SectionPos.asLong(chunkPos.x, chunkYMin + nextSectionY, chunkPos.z) ); } From 8910d54aa5f553d76d3072b11821c552155384e2 Mon Sep 17 00:00:00 2001 From: jcw780 <30562395+jcw780@users.noreply.github.com> Date: Thu, 29 Jan 2026 10:47:34 -0500 Subject: [PATCH 13/22] make poi unloading remove from loaded chunk even if column is somehow null --- .../poi_unloading/SectionStorageMixin.java | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/SectionStorageMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/SectionStorageMixin.java index 131958ed3..535547e6a 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/SectionStorageMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/SectionStorageMixin.java @@ -19,12 +19,9 @@ @Mixin(SectionStorage.class) public abstract class SectionStorageMixin implements PoiUnloading, RegionBasedStorageSectionExtended { - @Shadow - protected abstract void writeChunk(ChunkPos chunkPos); - @Shadow @Final - Object loadLock; + private Object loadLock; @Shadow @Final @@ -48,16 +45,16 @@ public abstract class SectionStorageMixin implements PoiUnloading, RegionBase // Remove column bitset BitSet chunkSections = this.lithium$removeColumn(chunkPos); - if (chunkSections == null) { - return; - } - - final int chunkYMin = this.lithium$getChunkYMin(); - int nextSectionY = -1; - while ((nextSectionY = chunkSections.nextSetBit(nextSectionY + 1)) != -1) { - ((ListeningLong2ObjectOpenHashMap>)this.storage).removeSilently( - SectionPos.asLong(chunkPos.x, chunkYMin + nextSectionY, chunkPos.z) - ); + if (chunkSections != null) { + // This relies on the reduce poi memory optimizations so we only need to remove sections with a POISection + final int chunkYMin = this.lithium$getChunkYMin(); + int nextSectionY = -1; + while ((nextSectionY = chunkSections.nextSetBit(nextSectionY + 1)) != -1) { + // Column is already removed so we do not need to update the column in the columns hashmap + ((ListeningLong2ObjectOpenHashMap>)this.storage).removeSilently( + SectionPos.asLong(chunkPos.x, chunkYMin + nextSectionY, chunkPos.z) + ); + } } synchronized (this.loadLock) { From 0b89120aad900c433c9d3d3539b92b375a7796af Mon Sep 17 00:00:00 2001 From: jcw780 <30562395+jcw780@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:53:53 -0500 Subject: [PATCH 14/22] optimize check PoiManager::checkConsistencyWithBlocks when using reduced_poi_memory --- .../world/interests/PoiCheckConsistency.java | 11 ++++ .../PoiManagerMixin.java | 59 +++++++++++++++++++ .../SectionStorageMixin.java | 32 ++++++++++ .../SerializableChunkDataMixin.java | 40 +++++++++++++ .../package-info.java | 5 ++ common/src/main/resources/lithium.mixins.json | 3 + lithium-fabric-mixin-config.md | 4 ++ lithium-neoforge-mixin-config.md | 4 ++ 8 files changed, 158 insertions(+) create mode 100644 common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/PoiCheckConsistency.java create mode 100644 common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/check_consistency_with_blocks/PoiManagerMixin.java create mode 100644 common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/check_consistency_with_blocks/SectionStorageMixin.java create mode 100644 common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/check_consistency_with_blocks/SerializableChunkDataMixin.java create mode 100644 common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/check_consistency_with_blocks/package-info.java diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/PoiCheckConsistency.java b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/PoiCheckConsistency.java new file mode 100644 index 000000000..e18b057b0 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/PoiCheckConsistency.java @@ -0,0 +1,11 @@ +package net.caffeinemc.mods.lithium.common.world.interests; + +import net.minecraft.core.SectionPos; +import net.minecraft.world.level.chunk.LevelChunkSection; + +import java.util.BitSet; + +public interface PoiCheckConsistency { + R lithium$createSectionAndInsert(long l); + void lithium$CheckConsistencyWithBlocks(SectionPos sectionPos, LevelChunkSection levelChunkSection, BitSet column); +} diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/check_consistency_with_blocks/PoiManagerMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/check_consistency_with_blocks/PoiManagerMixin.java new file mode 100644 index 000000000..fedb8fb18 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/check_consistency_with_blocks/PoiManagerMixin.java @@ -0,0 +1,59 @@ +package net.caffeinemc.mods.lithium.mixin.ai.poi.reduce_poi_memory.check_consistency_with_blocks; + +import net.caffeinemc.mods.lithium.common.world.interests.PoiCheckConsistency; +import net.caffeinemc.mods.lithium.common.world.interests.RegionBasedStorageSectionExtended; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.SectionPos; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.entity.ai.village.poi.PoiSection; +import net.minecraft.world.entity.ai.village.poi.PoiType; +import net.minecraft.world.level.chunk.LevelChunkSection; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.BitSet; +import java.util.Optional; +import java.util.function.BiConsumer; + +@Mixin(PoiManager.class) +public abstract class PoiManagerMixin implements PoiCheckConsistency { + @Shadow + protected abstract void updateFromSection(LevelChunkSection levelChunkSection, SectionPos sectionPos, BiConsumer> biConsumer); + + @Shadow + private static boolean mayHavePoi(LevelChunkSection levelChunkSection) { + return false; + } + + /** + * Copy the logic of PoiManager::checkConsistencyWithBlocks except using Lithium columns lookup + * @param sectionPos + * @param levelChunkSection + * @param column + */ + @Override + public void lithium$CheckConsistencyWithBlocks(SectionPos sectionPos, LevelChunkSection levelChunkSection, BitSet column) { + final int chunkYMin = ((RegionBasedStorageSectionExtended) this).lithium$getChunkYMin(); + if (column.get(sectionPos.y() - chunkYMin)) { + // Chunk should already be unpacked - do not need to check further + Optional optional = ((RegionBasedStorageSectionExtended) this).lithium$uncheckedGetElementAt(sectionPos.asLong()); + if (optional.isPresent()) { + PoiSection section = optional.get(); + section.refresh(biConsumer -> { + if (mayHavePoi(levelChunkSection)) { + this.updateFromSection(levelChunkSection, sectionPos, biConsumer); + } + }); + } else { + throw new IllegalStateException(String.format("Section %d %d %d is missing from storage despite being marked as present", sectionPos.x(), sectionPos.y(), sectionPos.z())); + } + } else { + if (mayHavePoi(levelChunkSection)) { + // Section is empty - always create new PoiSection + PoiSection section = this.lithium$createSectionAndInsert(sectionPos.asLong()); + this.updateFromSection(levelChunkSection, sectionPos, section::add); + } + } + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/check_consistency_with_blocks/SectionStorageMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/check_consistency_with_blocks/SectionStorageMixin.java new file mode 100644 index 000000000..2569402a2 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/check_consistency_with_blocks/SectionStorageMixin.java @@ -0,0 +1,32 @@ +package net.caffeinemc.mods.lithium.mixin.ai.poi.reduce_poi_memory.check_consistency_with_blocks; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import net.caffeinemc.mods.lithium.common.world.interests.PoiCheckConsistency; +import net.minecraft.world.level.chunk.storage.SectionStorage; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Optional; +import java.util.function.Function; + +@Mixin(SectionStorage.class) +public abstract class SectionStorageMixin implements PoiCheckConsistency { + @Shadow + @Final + private Long2ObjectMap> storage; + + @Shadow + @Final + private Function factory; + + @Shadow + protected abstract void setDirty(long l); + + @Override + public R lithium$createSectionAndInsert(long l) { + R object = this.factory.apply(() -> this.setDirty(l)); + this.storage.put(l, Optional.of(object)); + return object; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/check_consistency_with_blocks/SerializableChunkDataMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/check_consistency_with_blocks/SerializableChunkDataMixin.java new file mode 100644 index 000000000..6546c8fba --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/check_consistency_with_blocks/SerializableChunkDataMixin.java @@ -0,0 +1,40 @@ +package net.caffeinemc.mods.lithium.mixin.ai.poi.reduce_poi_memory.check_consistency_with_blocks; + +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; +import net.caffeinemc.mods.lithium.common.world.interests.PoiCheckConsistency; +import net.caffeinemc.mods.lithium.common.world.interests.RegionBasedStorageSectionExtended; +import net.minecraft.core.SectionPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.entity.ai.village.poi.PoiSection; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.storage.RegionStorageInfo; +import net.minecraft.world.level.chunk.storage.SerializableChunkData; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.BitSet; + +@Mixin(SerializableChunkData.class) +public abstract class SerializableChunkDataMixin { + // Ensure chunk is "POI-loaded" and retrieve lookup column for reuse to reduce hashmap calls since all calls in the method + // are in the same chunk. + @Inject(method = "read", at= @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerLevel;palettedContainerFactory()Lnet/minecraft/world/level/chunk/PalettedContainerFactory;", shift = At.Shift.AFTER)) + private void generateColumn(ServerLevel serverLevel, PoiManager poiManager, RegionStorageInfo regionStorageInfo, + ChunkPos chunkPos, CallbackInfoReturnable cir, @Share("column") LocalRef column) { + column.set(((RegionBasedStorageSectionExtended) poiManager).lithium$getNonEmptyPOISections(chunkPos.x, chunkPos.z)); + } + + // Use specialized method to take advantage of acquired column + @Redirect(method = "read", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/ai/village/poi/PoiManager;checkConsistencyWithBlocks(Lnet/minecraft/core/SectionPos;Lnet/minecraft/world/level/chunk/LevelChunkSection;)V")) + private void redirectCheckConsistencyWithBlocks(PoiManager poiManager, SectionPos sectionPos, + LevelChunkSection levelChunkSection, @Share("column") LocalRef column) { + ((PoiCheckConsistency) poiManager).lithium$CheckConsistencyWithBlocks(sectionPos, levelChunkSection, column.get()); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/check_consistency_with_blocks/package-info.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/check_consistency_with_blocks/package-info.java new file mode 100644 index 000000000..0245ccdd1 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/check_consistency_with_blocks/package-info.java @@ -0,0 +1,5 @@ +@MixinConfigOption(description = "Optimize checkConsistencyWithBlocks when using reduce_poi_memory optimizations") + +package net.caffeinemc.mods.lithium.mixin.ai.poi.reduce_poi_memory.check_consistency_with_blocks; + +import net.caffeinemc.gradle.MixinConfigOption; \ No newline at end of file diff --git a/common/src/main/resources/lithium.mixins.json b/common/src/main/resources/lithium.mixins.json index b2d9d887e..7502e8f06 100644 --- a/common/src/main/resources/lithium.mixins.json +++ b/common/src/main/resources/lithium.mixins.json @@ -23,6 +23,9 @@ "ai.poi.fast_portals.PoiManagerMixin", "ai.poi.fast_portals.PortalForcerMixin", "ai.poi.reduce_poi_memory.SectionStorageMixin", + "ai.poi.reduce_poi_memory.check_consistency_with_blocks.PoiManagerMixin", + "ai.poi.reduce_poi_memory.check_consistency_with_blocks.SectionStorageMixin", + "ai.poi.reduce_poi_memory.check_consistency_with_blocks.SerializableChunkDataMixin", "ai.poi.tasks.AcquirePoiMixin", "ai.poi.tasks.LocateHidingPlaceMixin", "ai.poi.tasks.RaiderEntityAttackHomeGoalMixin", diff --git a/lithium-fabric-mixin-config.md b/lithium-fabric-mixin-config.md index 3e453f484..1a823dc9f 100644 --- a/lithium-fabric-mixin-config.md +++ b/lithium-fabric-mixin-config.md @@ -45,6 +45,10 @@ Portal search uses the faster POI search and optimized loaded state caching (default: `true`) Reduce memory consumption of POI system +### `mixin.ai.poi.reduce_poi_memory.check_consistency_with_blocks` +(default: `true`) +Optimize checkConsistencyWithBlocks when using reduce_poi_memory optimizations + ### `mixin.ai.poi.tasks` (default: `true`) Mob Tasks which search for POIs use the optimized POI search diff --git a/lithium-neoforge-mixin-config.md b/lithium-neoforge-mixin-config.md index 005360aac..ea5b1e21f 100644 --- a/lithium-neoforge-mixin-config.md +++ b/lithium-neoforge-mixin-config.md @@ -45,6 +45,10 @@ Portal search uses the faster POI search and optimized loaded state caching (default: `true`) Reduce memory consumption of POI system +### `mixin.ai.poi.reduce_poi_memory.check_consistency_with_blocks` +(default: `true`) +Optimize checkConsistencyWithBlocks when using reduce_poi_memory optimizations + ### `mixin.ai.poi.tasks` (default: `true`) Mob Tasks which search for POIs use the optimized POI search From a09d38a8a6a24664bc1eb1c1562cc0af353ece5d Mon Sep 17 00:00:00 2001 From: jcw780 <30562395+jcw780@users.noreply.github.com> Date: Thu, 29 Jan 2026 21:40:28 -0500 Subject: [PATCH 15/22] cleanup + better mixin description of poi unloading --- .../world/poi_unloading/SectionStorageMixin.java | 1 - .../minimal_nonvanilla/world/poi_unloading/package-info.java | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/SectionStorageMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/SectionStorageMixin.java index 535547e6a..8c1e99a84 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/SectionStorageMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/SectionStorageMixin.java @@ -7,7 +7,6 @@ import net.caffeinemc.mods.lithium.common.world.interests.RegionBasedStorageSectionExtended; import net.minecraft.core.SectionPos; import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.LevelHeightAccessor; import net.minecraft.world.level.chunk.storage.SectionStorage; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/package-info.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/package-info.java index ade57c330..e670f496f 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/package-info.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/package-info.java @@ -1,7 +1,8 @@ @MixinConfigOption( description = "Unload non-portal forced POIs. " + "Note: This will cause extra chunk loading when portals run ensureLoadedAndValid into unload areas." + - "However, this will only be detectable during the first portal load into an area with valid POI sections that were loaded by non-portal means and then unloaded.", + "However, this will only be detectable during the first portal load into an area with valid POI " + + "sections that were loaded before by non-portal means and then unloaded. No known use is affected by this.", depends = { @MixinConfigDependency(dependencyPath = "mixin.ai.poi"), @MixinConfigDependency(dependencyPath = "mixin.ai.poi.reduce_poi_memory"), From 35b50d71e8b48ea456f1fa1ca1acfb5a611ae87a Mon Sep 17 00:00:00 2001 From: jcw780 <30562395+jcw780@users.noreply.github.com> Date: Thu, 29 Jan 2026 21:44:41 -0500 Subject: [PATCH 16/22] minor description edit --- .../minimal_nonvanilla/world/poi_unloading/package-info.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/package-info.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/package-info.java index e670f496f..085483a0a 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/package-info.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/package-info.java @@ -1,6 +1,6 @@ @MixinConfigOption( description = "Unload non-portal forced POIs. " + - "Note: This will cause extra chunk loading when portals run ensureLoadedAndValid into unload areas." + + "Note: This will cause extra chunk loading when portals run ensureLoadedAndValid into unload areas. " + "However, this will only be detectable during the first portal load into an area with valid POI " + "sections that were loaded before by non-portal means and then unloaded. No known use is affected by this.", depends = { From 71faf2e35b9186d78c21852d791a672a99715507 Mon Sep 17 00:00:00 2001 From: jcw780 <30562395+jcw780@users.noreply.github.com> Date: Fri, 30 Jan 2026 23:11:32 -0500 Subject: [PATCH 17/22] refactor unloading code for easier multi-loader support + minor changes --- .../common/world/interests/PoiUnloading.java | 4 +- .../RegionBasedStorageSectionExtended.java | 2 +- .../mixin/ai/poi/SectionStorageMixin.java | 4 +- .../SectionStorageMixin.java | 52 ++++++++++++++- .../world/poi_unloading/ChunkMapMixin.java | 9 ++- .../world/poi_unloading/PoiManagerMixin.java | 5 +- .../poi_unloading/SectionStorageMixin.java | 64 ------------------- common/src/main/resources/lithium.mixins.json | 1 - lithium-fabric-mixin-config.md | 2 +- lithium-neoforge-mixin-config.md | 2 +- 10 files changed, 68 insertions(+), 77 deletions(-) delete mode 100644 common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/SectionStorageMixin.java diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/PoiUnloading.java b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/PoiUnloading.java index 55c239488..409ce9990 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/PoiUnloading.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/PoiUnloading.java @@ -3,6 +3,6 @@ import net.minecraft.world.level.ChunkPos; public interface PoiUnloading { - boolean lithium$shouldUnloadChunkPOIs(ChunkPos chunkPos); - void lithium$unloadChunkPOIs(ChunkPos chunkPos); + boolean lithium$shouldUnloadChunkPOIs(long chunkPos); + void lithium$unloadChunkPOIs(long chunkPos); } diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java index 00cc90551..4120b2822 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java @@ -40,7 +40,7 @@ public interface RegionBasedStorageSectionExtended { * @param chunkPos * @return */ - BitSet lithium$removeColumn(ChunkPos chunkPos); + BitSet lithium$removeColumn(long chunkPos); int lithium$getChunkYMin(); diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java index d2b8bb41f..bb80960f5 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java @@ -123,8 +123,8 @@ private void onEntryAdded(long key, Optional value) { } @Override - public BitSet lithium$removeColumn(ChunkPos chunkPos) { - return this.columns.remove(chunkPos.toLong()); + public BitSet lithium$removeColumn(long chunkPos) { + return this.columns.remove(chunkPos); } @Override diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/SectionStorageMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/SectionStorageMixin.java index 6bb310c11..b5ea5ae7f 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/SectionStorageMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/SectionStorageMixin.java @@ -1,7 +1,10 @@ package net.caffeinemc.mods.lithium.mixin.ai.poi.reduce_poi_memory; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.LongSet; import net.caffeinemc.mods.lithium.common.util.Pos; +import net.caffeinemc.mods.lithium.common.util.collections.ListeningLong2ObjectOpenHashMap; +import net.caffeinemc.mods.lithium.common.world.interests.PoiUnloading; import net.caffeinemc.mods.lithium.common.world.interests.RegionBasedStorageSectionExtended; import net.minecraft.core.SectionPos; import net.minecraft.util.Util; @@ -18,9 +21,22 @@ import java.util.BitSet; import java.util.Optional; +import java.util.concurrent.CompletableFuture; @Mixin(SectionStorage.class) -public abstract class SectionStorageMixin implements RegionBasedStorageSectionExtended { +public abstract class SectionStorageMixin implements RegionBasedStorageSectionExtended, PoiUnloading { + @Shadow + @Final + private Object loadLock; + + @Shadow + @Final + private LongSet loadedChunks; + + @Shadow + @Final + private Long2ObjectMap>>> pendingLoads; + @Mutable @Shadow @Final @@ -135,4 +151,38 @@ public Optional getOrLoad(long l) { } } } + + /** + * This is for unloading all the points of interest in the chunk using the new storage logic. + * This greatly improves performance since most sections will not have points of interest sections. + * Intended for use by Lithium's minimal_nonvanilla.poi_unloading or as part of Neoforge default behavior + * @param chunkPos + */ + @Override + public void lithium$unloadChunkPOIs(long chunkPos) { + // Make sure the injection happens after PoiManager::flush in chunk unload + // Then we will not need to call flush separately + + // Remove column bitset + BitSet chunkSections = this.lithium$removeColumn(chunkPos); + if (chunkSections != null) { + // This relies on the reduce poi memory optimizations so we only need to remove sections with a POISection + final int chunkYMin = this.lithium$getChunkYMin(); + final int x = ChunkPos.getX(chunkPos); + final int z = ChunkPos.getZ(chunkPos); + + int nextSectionY = -1; + while ((nextSectionY = chunkSections.nextSetBit(nextSectionY + 1)) != -1) { + // Column is already removed so we do not need to update the column in the columns hashmap + ((ListeningLong2ObjectOpenHashMap>)this.storage).removeSilently( + SectionPos.asLong(x, chunkYMin + nextSectionY, z) + ); + } + } + + synchronized (this.loadLock) { + this.loadedChunks.remove(chunkPos); + this.pendingLoads.remove(chunkPos); + } + } } diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/ChunkMapMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/ChunkMapMixin.java index c3dac12bf..3b0f5d0d0 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/ChunkMapMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/ChunkMapMixin.java @@ -4,6 +4,9 @@ import net.minecraft.server.level.ChunkHolder; import net.minecraft.server.level.ChunkMap; import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.level.ChunkPos; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -21,7 +24,9 @@ public abstract class ChunkMapMixin { // Inject after the chunkMap save because PoiManager::flush is called @Inject(method = "method_60440", at = @At(value = "INVOKE", target = "net/minecraft/server/level/ChunkMap.save (Lnet/minecraft/world/level/chunk/ChunkAccess;)Z", shift = At.Shift.AFTER)) - void unloadPOI(ChunkHolder chunkHolder, CompletableFuture completableFuture, long l, CallbackInfo ci) { - ((PoiUnloading) this.poiManager).lithium$unloadChunkPOIs(chunkHolder.getPos()); + void unloadChunkPOIs(ChunkHolder chunkHolder, CompletableFuture completableFuture, long chunkPos, CallbackInfo ci) { + if (((PoiUnloading) this.poiManager).lithium$shouldUnloadChunkPOIs(chunkPos)) { + ((PoiUnloading) this.poiManager).lithium$unloadChunkPOIs(chunkPos); + } } } diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/PoiManagerMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/PoiManagerMixin.java index 3131c7cf8..549868667 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/PoiManagerMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/PoiManagerMixin.java @@ -15,8 +15,9 @@ public abstract class PoiManagerMixin implements PoiUnloading { @Final private LongSet loadedChunks = new LongOpenHashSet(); + // Keep portal loaded POIs to improve performance and reduce observable behavior changes @Override - public boolean lithium$shouldUnloadChunkPOIs(ChunkPos chunkPos) { - return !loadedChunks.contains(chunkPos.toLong()); + public boolean lithium$shouldUnloadChunkPOIs(long chunkPos) { + return !loadedChunks.contains(chunkPos); } } diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/SectionStorageMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/SectionStorageMixin.java deleted file mode 100644 index 8c1e99a84..000000000 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/SectionStorageMixin.java +++ /dev/null @@ -1,64 +0,0 @@ -package net.caffeinemc.mods.lithium.mixin.minimal_nonvanilla.world.poi_unloading; - -import it.unimi.dsi.fastutil.longs.Long2ObjectMap; -import it.unimi.dsi.fastutil.longs.LongSet; -import net.caffeinemc.mods.lithium.common.util.collections.ListeningLong2ObjectOpenHashMap; -import net.caffeinemc.mods.lithium.common.world.interests.PoiUnloading; -import net.caffeinemc.mods.lithium.common.world.interests.RegionBasedStorageSectionExtended; -import net.minecraft.core.SectionPos; -import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.chunk.storage.SectionStorage; -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; - -import java.util.BitSet; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; - -@Mixin(SectionStorage.class) -public abstract class SectionStorageMixin implements PoiUnloading, RegionBasedStorageSectionExtended { - @Shadow - @Final - private Object loadLock; - - @Shadow - @Final - private LongSet loadedChunks; - - @Shadow - @Final - private Long2ObjectMap>>> pendingLoads; - - @Shadow - @Final - private Long2ObjectMap> storage; - - @Override - public void lithium$unloadChunkPOIs(ChunkPos chunkPos) { - if (!this.lithium$shouldUnloadChunkPOIs(chunkPos)) { - return; - } - - // Note: shouldn't need to write chunks since it's after PoiManager::flush in chunk unload - - // Remove column bitset - BitSet chunkSections = this.lithium$removeColumn(chunkPos); - if (chunkSections != null) { - // This relies on the reduce poi memory optimizations so we only need to remove sections with a POISection - final int chunkYMin = this.lithium$getChunkYMin(); - int nextSectionY = -1; - while ((nextSectionY = chunkSections.nextSetBit(nextSectionY + 1)) != -1) { - // Column is already removed so we do not need to update the column in the columns hashmap - ((ListeningLong2ObjectOpenHashMap>)this.storage).removeSilently( - SectionPos.asLong(chunkPos.x, chunkYMin + nextSectionY, chunkPos.z) - ); - } - } - - synchronized (this.loadLock) { - this.loadedChunks.remove(chunkPos.toLong()); - this.pendingLoads.remove(chunkPos.toLong()); - } - } -} diff --git a/common/src/main/resources/lithium.mixins.json b/common/src/main/resources/lithium.mixins.json index 7502e8f06..058694d3e 100644 --- a/common/src/main/resources/lithium.mixins.json +++ b/common/src/main/resources/lithium.mixins.json @@ -174,7 +174,6 @@ "minimal_nonvanilla.world.expiring_chunk_tickets.TicketStorageMixin", "minimal_nonvanilla.world.poi_unloading.ChunkMapMixin", "minimal_nonvanilla.world.poi_unloading.PoiManagerMixin", - "minimal_nonvanilla.world.poi_unloading.SectionStorageMixin", "shapes.blockstate_cache.BlockMixin", "shapes.lazy_shape_context.EntityCollisionContextMixin", "shapes.optimized_matching.ShapesMixin", diff --git a/lithium-fabric-mixin-config.md b/lithium-fabric-mixin-config.md index 1a823dc9f..c90230ef7 100644 --- a/lithium-fabric-mixin-config.md +++ b/lithium-fabric-mixin-config.md @@ -516,7 +516,7 @@ Only check positions with expiring tickets during ticket expiration. Can cause r ### `mixin.minimal_nonvanilla.world.poi_unloading` (default: `true`) -Unload non-portal forced POIs. Note: This will cause extra chunk loading when portals run ensureLoadedAndValid into unload areas.However, this will only be detectable during the first portal load into an area with valid POI sections that were loaded by non-portal means and then unloaded. +Unload non-portal forced POIs. Note: This will cause extra chunk loading when portals run ensureLoadedAndValid into unload areas. However, this will only be detectable during the first portal load into an area with valid POI sections that were loaded before by non-portal means and then unloaded. No known use is affected by this. Requirements: - `mixin.ai.poi=true` - `mixin.ai.poi.reduce_poi_memory=true` diff --git a/lithium-neoforge-mixin-config.md b/lithium-neoforge-mixin-config.md index ea5b1e21f..cd4310a80 100644 --- a/lithium-neoforge-mixin-config.md +++ b/lithium-neoforge-mixin-config.md @@ -497,7 +497,7 @@ Only check positions with expiring tickets during ticket expiration. Can cause r ### `mixin.minimal_nonvanilla.world.poi_unloading` (default: `true`) -Unload non-portal forced POIs. Note: This will cause extra chunk loading when portals run ensureLoadedAndValid into unload areas.However, this will only be detectable during the first portal load into an area with valid POI sections that were loaded by non-portal means and then unloaded. +Unload non-portal forced POIs. Note: This will cause extra chunk loading when portals run ensureLoadedAndValid into unload areas. However, this will only be detectable during the first portal load into an area with valid POI sections that were loaded before by non-portal means and then unloaded. No known use is affected by this. Requirements: - `mixin.ai.poi=true` - `mixin.ai.poi.reduce_poi_memory=true` From 7a3fb83115cd7e65fdb4e722c519aec80f76dfec Mon Sep 17 00:00:00 2001 From: jcw780 <30562395+jcw780@users.noreply.github.com> Date: Fri, 6 Feb 2026 22:21:20 -0500 Subject: [PATCH 18/22] fix develop merge --- .../mods/lithium/mixin/ai/poi/fast_portals/PoiManagerMixin.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/fast_portals/PoiManagerMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/fast_portals/PoiManagerMixin.java index 210e4a515..fc879c90a 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/fast_portals/PoiManagerMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/fast_portals/PoiManagerMixin.java @@ -112,7 +112,7 @@ private static long packYX(long y, long x) { int setSectionIndex = -1; while ((setSectionIndex = column.nextSetBit(setSectionIndex + 1)) != -1 && setSectionIndex < lowestUnsetSection) { - Optional section = this.lithium$getElementAt( + Optional section = this.lithium$uncheckedGetElementAt( SectionPos.asLong(x, Pos.SectionYCoord.fromSectionIndex(worldView, setSectionIndex), z) ); From 7762c9de00c4419bd8921f3e28fefd4e5f0d7aad Mon Sep 17 00:00:00 2001 From: jcw780 <30562395+jcw780@users.noreply.github.com> Date: Sat, 7 Feb 2026 16:35:31 -0500 Subject: [PATCH 19/22] refactor storage silent removals --- .../interests/RegionBasedStorageSectionExtended.java | 7 +++++++ .../mods/lithium/mixin/ai/poi/SectionStorageMixin.java | 5 +++++ .../ai/poi/reduce_poi_memory/SectionStorageMixin.java | 4 +--- .../check_consistency_with_blocks/PoiManagerMixin.java | 9 +++++---- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java index 4120b2822..4092f93b4 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/common/world/interests/RegionBasedStorageSectionExtended.java @@ -42,6 +42,13 @@ public interface RegionBasedStorageSectionExtended { */ BitSet lithium$removeColumn(long chunkPos); + /** + * Remove a section from the storage without updating the columns map - column adjustment must be handled manually + * @param l + * @return + */ + Optional lithium$removeSectionWithoutUpdatingColumn(long l); + int lithium$getChunkYMin(); int lithium$getChunkYMaxInclusive(); diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java index bb80960f5..8b7822a9d 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/SectionStorageMixin.java @@ -127,6 +127,11 @@ private void onEntryAdded(long key, Optional value) { return this.columns.remove(chunkPos); } + @Override + public Optional lithium$removeSectionWithoutUpdatingColumn (long sectionPos) { + return ((ListeningLong2ObjectOpenHashMap>) this.storage).removeSilently(sectionPos); + } + @Override public BitSet lithium$getOrAddColumnIfNull(long chunkPos) { BitSet column = this.columns.get(chunkPos); diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/SectionStorageMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/SectionStorageMixin.java index b5ea5ae7f..1a59ce86f 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/SectionStorageMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/SectionStorageMixin.java @@ -174,9 +174,7 @@ public Optional getOrLoad(long l) { int nextSectionY = -1; while ((nextSectionY = chunkSections.nextSetBit(nextSectionY + 1)) != -1) { // Column is already removed so we do not need to update the column in the columns hashmap - ((ListeningLong2ObjectOpenHashMap>)this.storage).removeSilently( - SectionPos.asLong(x, chunkYMin + nextSectionY, z) - ); + this.lithium$removeSectionWithoutUpdatingColumn(SectionPos.asLong(x, chunkYMin + nextSectionY, z)); } } diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/check_consistency_with_blocks/PoiManagerMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/check_consistency_with_blocks/PoiManagerMixin.java index fedb8fb18..11a56d15e 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/check_consistency_with_blocks/PoiManagerMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/check_consistency_with_blocks/PoiManagerMixin.java @@ -17,7 +17,7 @@ import java.util.function.BiConsumer; @Mixin(PoiManager.class) -public abstract class PoiManagerMixin implements PoiCheckConsistency { +public abstract class PoiManagerMixin implements PoiCheckConsistency, RegionBasedStorageSectionExtended { @Shadow protected abstract void updateFromSection(LevelChunkSection levelChunkSection, SectionPos sectionPos, BiConsumer> biConsumer); @@ -34,10 +34,11 @@ private static boolean mayHavePoi(LevelChunkSection levelChunkSection) { */ @Override public void lithium$CheckConsistencyWithBlocks(SectionPos sectionPos, LevelChunkSection levelChunkSection, BitSet column) { - final int chunkYMin = ((RegionBasedStorageSectionExtended) this).lithium$getChunkYMin(); - if (column.get(sectionPos.y() - chunkYMin)) { + final int chunkYMin = this.lithium$getChunkYMin(); + final int currentYSectionIndex = sectionPos.y() - chunkYMin; + if (column.get(currentYSectionIndex)) { // Chunk should already be unpacked - do not need to check further - Optional optional = ((RegionBasedStorageSectionExtended) this).lithium$uncheckedGetElementAt(sectionPos.asLong()); + Optional optional = this.lithium$uncheckedGetElementAt(sectionPos.asLong()); if (optional.isPresent()) { PoiSection section = optional.get(); section.refresh(biConsumer -> { From dbcc08e2b76f18ff0b33aa6921830a0eaecf26d2 Mon Sep 17 00:00:00 2001 From: jcw780 <30562395+jcw780@users.noreply.github.com> Date: Sun, 8 Feb 2026 22:38:59 -0500 Subject: [PATCH 20/22] move poi unloading to reduce poi memory --- .../poi_unloading/ChunkMapMixin.java | 2 +- .../poi_unloading/PoiManagerMixin.java | 2 +- .../poi_unloading/package-info.java | 7 +++++++ .../world/poi_unloading/package-info.java | 15 --------------- common/src/main/resources/lithium.mixins.json | 4 ++-- lithium-fabric-mixin-config.md | 11 ++++------- lithium-neoforge-mixin-config.md | 11 ++++------- 7 files changed, 19 insertions(+), 33 deletions(-) rename common/src/main/java/net/caffeinemc/mods/lithium/mixin/{minimal_nonvanilla/world => ai/poi/reduce_poi_memory}/poi_unloading/ChunkMapMixin.java (94%) rename common/src/main/java/net/caffeinemc/mods/lithium/mixin/{minimal_nonvanilla/world => ai/poi/reduce_poi_memory}/poi_unloading/PoiManagerMixin.java (90%) create mode 100644 common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/poi_unloading/package-info.java delete mode 100644 common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/package-info.java diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/ChunkMapMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/poi_unloading/ChunkMapMixin.java similarity index 94% rename from common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/ChunkMapMixin.java rename to common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/poi_unloading/ChunkMapMixin.java index 3b0f5d0d0..609288c6e 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/ChunkMapMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/poi_unloading/ChunkMapMixin.java @@ -1,4 +1,4 @@ -package net.caffeinemc.mods.lithium.mixin.minimal_nonvanilla.world.poi_unloading; +package net.caffeinemc.mods.lithium.mixin.ai.poi.reduce_poi_memory.poi_unloading; import net.caffeinemc.mods.lithium.common.world.interests.PoiUnloading; import net.minecraft.server.level.ChunkHolder; diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/PoiManagerMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/poi_unloading/PoiManagerMixin.java similarity index 90% rename from common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/PoiManagerMixin.java rename to common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/poi_unloading/PoiManagerMixin.java index 549868667..75973f38a 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/PoiManagerMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/poi_unloading/PoiManagerMixin.java @@ -1,4 +1,4 @@ -package net.caffeinemc.mods.lithium.mixin.minimal_nonvanilla.world.poi_unloading; +package net.caffeinemc.mods.lithium.mixin.ai.poi.reduce_poi_memory.poi_unloading; import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import it.unimi.dsi.fastutil.longs.LongSet; diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/poi_unloading/package-info.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/poi_unloading/package-info.java new file mode 100644 index 000000000..f82881835 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/poi_unloading/package-info.java @@ -0,0 +1,7 @@ +@MixinConfigOption( + description = "Unload non-portal forced POIs." +) + +package net.caffeinemc.mods.lithium.mixin.ai.poi.reduce_poi_memory.poi_unloading; + +import net.caffeinemc.gradle.MixinConfigOption; diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/package-info.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/package-info.java deleted file mode 100644 index 085483a0a..000000000 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/minimal_nonvanilla/world/poi_unloading/package-info.java +++ /dev/null @@ -1,15 +0,0 @@ -@MixinConfigOption( - description = "Unload non-portal forced POIs. " + - "Note: This will cause extra chunk loading when portals run ensureLoadedAndValid into unload areas. " + - "However, this will only be detectable during the first portal load into an area with valid POI " + - "sections that were loaded before by non-portal means and then unloaded. No known use is affected by this.", - depends = { - @MixinConfigDependency(dependencyPath = "mixin.ai.poi"), - @MixinConfigDependency(dependencyPath = "mixin.ai.poi.reduce_poi_memory"), - } -) - -package net.caffeinemc.mods.lithium.mixin.minimal_nonvanilla.world.poi_unloading; - -import net.caffeinemc.gradle.MixinConfigDependency; -import net.caffeinemc.gradle.MixinConfigOption; diff --git a/common/src/main/resources/lithium.mixins.json b/common/src/main/resources/lithium.mixins.json index a6f5b4bef..c606c7bae 100644 --- a/common/src/main/resources/lithium.mixins.json +++ b/common/src/main/resources/lithium.mixins.json @@ -172,8 +172,8 @@ "minimal_nonvanilla.world.block_entity_ticking.support_cache.DirectBlockEntityTickInvokerMixin", "minimal_nonvanilla.world.block_entity_ticking.support_cache.LevelChunkMixin", "minimal_nonvanilla.world.expiring_chunk_tickets.TicketStorageMixin", - "minimal_nonvanilla.world.poi_unloading.ChunkMapMixin", - "minimal_nonvanilla.world.poi_unloading.PoiManagerMixin", + "ai.poi.reduce_poi_memory.poi_unloading.ChunkMapMixin", + "ai.poi.reduce_poi_memory.poi_unloading.PoiManagerMixin", "shapes.blockstate_cache.BlockMixin", "shapes.lazy_shape_context.EntityCollisionContextMixin", "shapes.optimized_matching.ShapesMixin", diff --git a/lithium-fabric-mixin-config.md b/lithium-fabric-mixin-config.md index 6a500503b..a53efd02f 100644 --- a/lithium-fabric-mixin-config.md +++ b/lithium-fabric-mixin-config.md @@ -49,6 +49,10 @@ Reduce memory consumption of POI system (default: `true`) Optimize checkConsistencyWithBlocks when using reduce_poi_memory optimizations +### `mixin.ai.poi.reduce_poi_memory.poi_unloading` +(default: `true`) +Unload non-portal forced POIs. + ### `mixin.ai.poi.tasks` (default: `true`) Mob Tasks which search for POIs use the optimized POI search @@ -514,13 +518,6 @@ Requirements: (default: `true`) Only check positions with expiring tickets during ticket expiration. Can cause reordering of chunks unloading. The chunk unloading order in vanilla is predictable, but depends on the hash of the chunk position of the tickets and the hashes of the other chunk tickets, and the order of creation of the chunk tickets when hash collisions occur. No known contraptions depend on the unload order. -### `mixin.minimal_nonvanilla.world.poi_unloading` -(default: `true`) -Unload non-portal forced POIs. Note: This will cause extra chunk loading when portals run ensureLoadedAndValid into unload areas. However, this will only be detectable during the first portal load into an area with valid POI sections that were loaded before by non-portal means and then unloaded. No known use is affected by this. -Requirements: -- `mixin.ai.poi=true` -- `mixin.ai.poi.reduce_poi_memory=true` - ### `mixin.shapes` (default: `true`) Various VoxelShape optimizations diff --git a/lithium-neoforge-mixin-config.md b/lithium-neoforge-mixin-config.md index fb7994781..cf29a1533 100644 --- a/lithium-neoforge-mixin-config.md +++ b/lithium-neoforge-mixin-config.md @@ -49,6 +49,10 @@ Reduce memory consumption of POI system (default: `true`) Optimize checkConsistencyWithBlocks when using reduce_poi_memory optimizations +### `mixin.ai.poi.reduce_poi_memory.poi_unloading` +(default: `true`) +Unload non-portal forced POIs. + ### `mixin.ai.poi.tasks` (default: `true`) Mob Tasks which search for POIs use the optimized POI search @@ -495,13 +499,6 @@ Requirements: (default: `true`) Only check positions with expiring tickets during ticket expiration. Can cause reordering of chunks unloading. The chunk unloading order in vanilla is predictable, but depends on the hash of the chunk position of the tickets and the hashes of the other chunk tickets, and the order of creation of the chunk tickets when hash collisions occur. No known contraptions depend on the unload order. -### `mixin.minimal_nonvanilla.world.poi_unloading` -(default: `true`) -Unload non-portal forced POIs. Note: This will cause extra chunk loading when portals run ensureLoadedAndValid into unload areas. However, this will only be detectable during the first portal load into an area with valid POI sections that were loaded before by non-portal means and then unloaded. No known use is affected by this. -Requirements: -- `mixin.ai.poi=true` -- `mixin.ai.poi.reduce_poi_memory=true` - ### `mixin.shapes` (default: `true`) Various VoxelShape optimizations From da7c3cde3d5c81fde6eff8183310108a76bca253 Mon Sep 17 00:00:00 2001 From: jcw780 <30562395+jcw780@users.noreply.github.com> Date: Sun, 8 Feb 2026 22:39:31 -0500 Subject: [PATCH 21/22] minor formatting change --- .../ai/poi/reduce_poi_memory/poi_unloading/package-info.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/poi_unloading/package-info.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/poi_unloading/package-info.java index f82881835..b7926169a 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/poi_unloading/package-info.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/poi_unloading/package-info.java @@ -1,6 +1,4 @@ -@MixinConfigOption( - description = "Unload non-portal forced POIs." -) +@MixinConfigOption(description = "Unload non-portal forced POIs.") package net.caffeinemc.mods.lithium.mixin.ai.poi.reduce_poi_memory.poi_unloading; From 2d2eb597da07c34c876a11668750abc588333082 Mon Sep 17 00:00:00 2001 From: jcw780 <30562395+jcw780@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:01:32 -0500 Subject: [PATCH 22/22] modify fast_portals ensureLoadedAndValid optimization to mitigate poi unloading edgecase --- .../lithium/mixin/ai/poi/fast_portals/PoiManagerMixin.java | 7 ++++++- .../poi/reduce_poi_memory/poi_unloading/package-info.java | 5 ++++- lithium-fabric-mixin-config.md | 4 +++- lithium-neoforge-mixin-config.md | 4 +++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/fast_portals/PoiManagerMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/fast_portals/PoiManagerMixin.java index fc879c90a..2d841f845 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/fast_portals/PoiManagerMixin.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/fast_portals/PoiManagerMixin.java @@ -78,7 +78,12 @@ public void ensureLoadedAndValid(LevelReader worldView, BlockPos pos, int radius int loadingChunkCounter = 0; for (int x = chunkX - chunkRadius, xMax = chunkX + chunkRadius; x <= xMax; x++) { final int lowestSection = this.lithium$getLowestEmptyOrInvalidSection(worldView, x, z); - if (lowestSection < maxYSectionIndexExclusive && loadedChunks.add(ChunkPos.asLong(x, z))) { + // Flip the check for poi unloading so portal chunk POIs are never unloaded + // This prevents an edgecase where all sections are loaded and valid in chunk being unloaded despite + // being portal loaded - which creates additional lag on subsequent tp's after unloading. + // Note: This technically means that loaded chunks may have different contents but literally nothing + // else in minecraft uses this and it does not affect the behavior of this method. + if (loadedChunks.add(ChunkPos.asLong(x, z)) && lowestSection < maxYSectionIndexExclusive) { sectionsYXPacked[loadingChunkCounter++] = packYX(lowestSection, x); } } diff --git a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/poi_unloading/package-info.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/poi_unloading/package-info.java index b7926169a..1e8689eb7 100644 --- a/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/poi_unloading/package-info.java +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/poi_unloading/package-info.java @@ -1,5 +1,8 @@ -@MixinConfigOption(description = "Unload non-portal forced POIs.") +@MixinConfigOption(description = "Unload non-portal forced POIs.", + depends = @MixinConfigDependency(dependencyPath = "mixin.ai.poi.fast_portals") +) package net.caffeinemc.mods.lithium.mixin.ai.poi.reduce_poi_memory.poi_unloading; +import net.caffeinemc.gradle.MixinConfigDependency; import net.caffeinemc.gradle.MixinConfigOption; diff --git a/lithium-fabric-mixin-config.md b/lithium-fabric-mixin-config.md index a53efd02f..4e2970882 100644 --- a/lithium-fabric-mixin-config.md +++ b/lithium-fabric-mixin-config.md @@ -51,7 +51,9 @@ Optimize checkConsistencyWithBlocks when using reduce_poi_memory optimizations ### `mixin.ai.poi.reduce_poi_memory.poi_unloading` (default: `true`) -Unload non-portal forced POIs. +Unload non-portal forced POIs. +Requirements: +- `mixin.ai.poi.fast_portals=true` ### `mixin.ai.poi.tasks` (default: `true`) diff --git a/lithium-neoforge-mixin-config.md b/lithium-neoforge-mixin-config.md index cf29a1533..5f6cf632c 100644 --- a/lithium-neoforge-mixin-config.md +++ b/lithium-neoforge-mixin-config.md @@ -51,7 +51,9 @@ Optimize checkConsistencyWithBlocks when using reduce_poi_memory optimizations ### `mixin.ai.poi.reduce_poi_memory.poi_unloading` (default: `true`) -Unload non-portal forced POIs. +Unload non-portal forced POIs. +Requirements: +- `mixin.ai.poi.fast_portals=true` ### `mixin.ai.poi.tasks` (default: `true`)