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/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/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..409ce9990 --- /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(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 f44e2d2c2..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 @@ -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, @@ -27,9 +34,24 @@ 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(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(); - 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/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, 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..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 @@ -1,7 +1,12 @@ 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.serialization.Codec; +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; @@ -9,6 +14,7 @@ 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; @@ -18,33 +24,26 @@ 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.slf4j.Logger; +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; -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; @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 private Long2ObjectMap> storage; - @Shadow - protected abstract Optional get(long pos); - @Shadow @Final protected LevelHeightAccessor levelHeightAccessor; @@ -52,6 +51,18 @@ public abstract class SectionStorageMixin implements RegionBasedStorageSectio @Shadow protected abstract void unpackChunk(ChunkPos chunkPos); + @Shadow + @Final + private Codec

codec; + + @Shadow + @Final + static Logger LOGGER; + + @Shadow + @Final + private Function packer; + private Long2ObjectOpenHashMap columns; @SuppressWarnings("rawtypes") @@ -105,6 +116,33 @@ 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$removeColumn(long chunkPos) { + 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); + + 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, @@ -182,7 +220,7 @@ protected R computeNext() { } @Override - public Optional lithium$getElementAt(long sectionPos) { + public Optional lithium$uncheckedGetElementAt(long sectionPos) { return this.storage.get(sectionPos); } @@ -195,4 +233,55 @@ protected R computeNext() { public int lithium$getChunkYMaxInclusive() { 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. + */ + + + /** + * @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()) + ) + ) + ); + } + } 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..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); } } @@ -112,7 +117,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) ); 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..1a59ce86f --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/SectionStorageMixin.java @@ -0,0 +1,186 @@ +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; +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; +import java.util.concurrent.CompletableFuture; + +@Mixin(SectionStorage.class) +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 + 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 + * Warning: This has more checks than vanilla to match vanilla behavior. + * Please use other methods if it is performance sensitive. + */ + @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; + } + } + } + + /** + * 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 + this.lithium$removeSectionWithoutUpdatingColumn(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/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..11a56d15e --- /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,60 @@ +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, RegionBasedStorageSectionExtended { + @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 = 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 = 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/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/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/poi_unloading/ChunkMapMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/poi_unloading/ChunkMapMixin.java new file mode 100644 index 000000000..609288c6e --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/poi_unloading/ChunkMapMixin.java @@ -0,0 +1,32 @@ +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; +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; +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 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/ai/poi/reduce_poi_memory/poi_unloading/PoiManagerMixin.java b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/poi_unloading/PoiManagerMixin.java new file mode 100644 index 000000000..75973f38a --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/lithium/mixin/ai/poi/reduce_poi_memory/poi_unloading/PoiManagerMixin.java @@ -0,0 +1,23 @@ +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; +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(); + + // Keep portal loaded POIs to improve performance and reduce observable behavior changes + @Override + public boolean lithium$shouldUnloadChunkPOIs(long chunkPos) { + return !loadedChunks.contains(chunkPos); + } +} 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..1e8689eb7 --- /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,8 @@ +@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/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 ff352d65c..c606c7bae 100644 --- a/common/src/main/resources/lithium.mixins.json +++ b/common/src/main/resources/lithium.mixins.json @@ -22,6 +22,10 @@ "ai.poi.SectionStorageMixin", "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", @@ -168,6 +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", + "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 6a5a2d834..4e2970882 100644 --- a/lithium-fabric-mixin-config.md +++ b/lithium-fabric-mixin-config.md @@ -41,6 +41,20 @@ 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.reduce_poi_memory.check_consistency_with_blocks` +(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. +Requirements: +- `mixin.ai.poi.fast_portals=true` + ### `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 c3d920b38..5f6cf632c 100644 --- a/lithium-neoforge-mixin-config.md +++ b/lithium-neoforge-mixin-config.md @@ -41,6 +41,20 @@ 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.reduce_poi_memory.check_consistency_with_blocks` +(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. +Requirements: +- `mixin.ai.poi.fast_portals=true` + ### `mixin.ai.poi.tasks` (default: `true`) Mob Tasks which search for POIs use the optimized POI search