9
0
mirror of https://github.com/Winds-Studio/Leaf.git synced 2025-12-25 09:59:15 +00:00

optimize chunk map (#438)

* rebase

* optimize LivingEntity#travel

* cleanup

* Replace fluid height map

* reuse array list in Entity#collide

* cleanup

* fix fire and liquid collision shape

* fix checkInside

* inline betweenClosed

* cleanup

* optimize getOnPos

* optimize equals in getOnPos

* update mainSupportingBlockPos on dirty

* cleanup

* rename

* merge same patch

* rebase and remove properly

* [ci skip] cleanup

* rebase and rebuild

* fix async locator

* remove async locator

* cleanup

* rebase

---------

Co-authored-by: Taiyou06 <kaandindar21@gmail.com>
This commit is contained in:
hayanesuru
2025-08-20 03:48:26 +09:00
committed by GitHub
parent 55de442b70
commit 23b7b02eee
163 changed files with 2158 additions and 146 deletions

View File

@@ -3,7 +3,6 @@ package org.dreeam.leaf.async;
import net.minecraft.server.MinecraftServer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dreeam.leaf.async.locate.AsyncLocator;
import org.dreeam.leaf.async.path.AsyncPathProcessor;
import org.dreeam.leaf.async.tracker.AsyncTracker;
@@ -48,14 +47,5 @@ public class ShutdownExecutors {
} catch (InterruptedException ignored) {
}
}
if (AsyncLocator.LOCATING_EXECUTOR_SERVICE != null) {
LOGGER.info("Waiting for structure locating executor to shutdown...");
AsyncLocator.LOCATING_EXECUTOR_SERVICE.shutdown();
try {
AsyncLocator.LOCATING_EXECUTOR_SERVICE.awaitTermination(60L, TimeUnit.SECONDS);
} catch (InterruptedException ignored) {
}
}
}
}

View File

@@ -1,168 +0,0 @@
package org.dreeam.leaf.async.locate;
import ca.spottedleaf.moonrise.common.util.TickThread;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.TagKey;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.structure.Structure;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
// Original project: https://github.com/thebrightspark/AsyncLocator
public class AsyncLocator {
public static final Logger LOGGER = LogManager.getLogger("Leaf Async Locator");
public static final ExecutorService LOCATING_EXECUTOR_SERVICE;
private AsyncLocator() {
}
public static class AsyncLocatorThread extends TickThread {
private static final AtomicInteger THREAD_COUNTER = new AtomicInteger(0);
public AsyncLocatorThread(Runnable run, String name) {
super(run, name, THREAD_COUNTER.incrementAndGet());
}
@Override
public void run() {
super.run();
}
}
static {
LOCATING_EXECUTOR_SERVICE = !org.dreeam.leaf.config.modules.async.AsyncLocator.enabled ? null : new ThreadPoolExecutor(
1,
org.dreeam.leaf.config.modules.async.AsyncLocator.asyncLocatorThreads,
org.dreeam.leaf.config.modules.async.AsyncLocator.asyncLocatorKeepalive,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
new ThreadFactoryBuilder()
.setThreadFactory(
r -> new AsyncLocatorThread(r, "Leaf Async Locator Thread") {
@Override
public void run() {
r.run();
}
}
)
.setNameFormat("Leaf Async Locator Thread - %d")
.setPriority(Thread.NORM_PRIORITY - 2)
.build()
);
}
/**
* Queues a task to locate a feature using {@link ServerLevel#findNearestMapStructure(TagKey, BlockPos, int, boolean)}
* and returns a {@link LocateTask} with the futures for it.
*/
public static LocateTask<BlockPos> locate(
ServerLevel level,
TagKey<Structure> structureTag,
BlockPos pos,
int searchRadius,
boolean skipKnownStructures
) {
CompletableFuture<BlockPos> completableFuture = new CompletableFuture<>();
Future<?> future = LOCATING_EXECUTOR_SERVICE.submit(
() -> doLocateLevel(completableFuture, level, structureTag, pos, searchRadius, skipKnownStructures)
);
return new LocateTask<>(level.getServer(), completableFuture, future);
}
/**
* Queues a task to locate a feature using
* {@link ChunkGenerator#findNearestMapStructure(ServerLevel, HolderSet, BlockPos, int, boolean)} and returns a
* {@link LocateTask} with the futures for it.
*/
public static LocateTask<Pair<BlockPos, Holder<Structure>>> locate(
ServerLevel level,
HolderSet<Structure> structureSet,
BlockPos pos,
int searchRadius,
boolean skipKnownStructures
) {
CompletableFuture<Pair<BlockPos, Holder<Structure>>> completableFuture = new CompletableFuture<>();
Future<?> future = LOCATING_EXECUTOR_SERVICE.submit(
() -> doLocateChunkGenerator(completableFuture, level, structureSet, pos, searchRadius, skipKnownStructures)
);
return new LocateTask<>(level.getServer(), completableFuture, future);
}
private static void doLocateLevel(
CompletableFuture<BlockPos> completableFuture,
ServerLevel level,
TagKey<Structure> structureTag,
BlockPos pos,
int searchRadius,
boolean skipExistingChunks
) {
BlockPos foundPos = level.findNearestMapStructure(structureTag, pos, searchRadius, skipExistingChunks);
completableFuture.complete(foundPos);
}
private static void doLocateChunkGenerator(
CompletableFuture<Pair<BlockPos, Holder<Structure>>> completableFuture,
ServerLevel level,
HolderSet<Structure> structureSet,
BlockPos pos,
int searchRadius,
boolean skipExistingChunks
) {
Pair<BlockPos, Holder<Structure>> foundPair = level.getChunkSource().getGenerator()
.findNearestMapStructure(level, structureSet, pos, searchRadius, skipExistingChunks);
completableFuture.complete(foundPair);
}
/**
* Holder of the futures for an async locate task as well as providing some helper functions.
* The completableFuture will be completed once the call to
* {@link ServerLevel#findNearestMapStructure(TagKey, BlockPos, int, boolean)} has completed, and will hold the
* result of it.
* The taskFuture is the future for the {@link Runnable} itself in the executor service.
*/
public record LocateTask<T>(MinecraftServer server, CompletableFuture<T> completableFuture, Future<?> taskFuture) {
/**
* Helper function that calls {@link CompletableFuture#thenAccept(Consumer)} with the given action.
* Bear in mind that the action will be executed from the task's thread. If you intend to change any game data,
* it's strongly advised you use {@link #thenOnServerThread(Consumer)} instead so that it's queued and executed
* on the main server thread instead.
*/
public LocateTask<T> then(Consumer<T> action) {
completableFuture.thenAccept(action);
return this;
}
/**
* Helper function that calls {@link CompletableFuture#thenAccept(Consumer)} with the given action on the server
* thread.
*/
public LocateTask<T> thenOnServerThread(Consumer<T> action) {
completableFuture.thenAccept(pos -> server.scheduleOnMain(() -> action.accept(pos)));
return this;
}
/**
* Helper function that cancels both completableFuture and taskFuture.
*/
public void cancel() {
taskFuture.cancel(true);
completableFuture.cancel(false);
}
}
}

View File

@@ -1,39 +0,0 @@
package org.dreeam.leaf.config.modules.async;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
import org.dreeam.leaf.config.LeafConfig;
public class AsyncLocator extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.ASYNC.getBaseKeyName() + ".async-locator";
}
public static boolean enabled = false;
public static int asyncLocatorThreads = 0;
public static int asyncLocatorKeepalive = 60;
@Override
public void onLoaded() {
config.addCommentRegionBased(getBasePath(), """
Whether or not asynchronous locator should be enabled.
This offloads structure locating to other threads.
Only for locate command, dolphin treasure finding and eye of ender currently.""",
"""
是否启用异步结构搜索.
目前可用于 /locate 指令, 海豚寻宝和末影之眼.""");
enabled = config.getBoolean(getBasePath() + ".enabled", enabled);
asyncLocatorThreads = config.getInt(getBasePath() + ".threads", asyncLocatorThreads);
asyncLocatorKeepalive = config.getInt(getBasePath() + ".keepalive", asyncLocatorKeepalive);
if (asyncLocatorThreads <= 0) {
asyncLocatorThreads = 1;
}
if (!enabled) {
asyncLocatorThreads = 0;
} else {
LeafConfig.LOGGER.info("Using {} threads for Async Locator", asyncLocatorThreads);
}
}
}

View File

@@ -0,0 +1,162 @@
package org.dreeam.leaf.util.map;
import it.unimi.dsi.fastutil.objects.AbstractObject2DoubleMap;
import it.unimi.dsi.fastutil.objects.AbstractObjectSet;
import it.unimi.dsi.fastutil.objects.ObjectIterator;
import it.unimi.dsi.fastutil.objects.ObjectSet;
import net.minecraft.tags.FluidTags;
import net.minecraft.tags.TagKey;
import net.minecraft.world.level.material.Fluid;
import java.util.NoSuchElementException;
public final class FluidHeightMap extends AbstractObject2DoubleMap<TagKey<Fluid>> {
private double water = 0.0;
private double lava = 0.0;
@Override
public int size() {
return 2;
}
@Override
public ObjectSet<Entry<TagKey<Fluid>>> object2DoubleEntrySet() {
return new EntrySet();
}
@Override
public double getDouble(Object k) {
return k == FluidTags.WATER ? water : k == FluidTags.LAVA ? lava : 0.0;
}
@Override
public double put(TagKey<Fluid> k, double v) {
if (k == FluidTags.WATER) {
double prev = this.water;
this.water = v;
return prev;
} else if (k == FluidTags.LAVA) {
double prev = this.lava;
this.lava = v;
return prev;
}
return 0.0;
}
@Override
public void clear() {
this.water = 0.0;
this.lava = 0.0;
}
private final class EntrySet extends AbstractObjectSet<Entry<TagKey<Fluid>>> {
@Override
public ObjectIterator<Entry<TagKey<Fluid>>> iterator() {
return new EntryIterator();
}
@Override
public int size() {
return 2;
}
@Override
public boolean contains(Object o) {
if (!(o instanceof Entry<?> entry)) {
return false;
}
Object key = entry.getKey();
if (key == FluidTags.WATER) {
return entry.getDoubleValue() == water;
} else if (key == FluidTags.LAVA) {
return entry.getDoubleValue() == lava;
}
return false;
}
@Override
public boolean remove(final Object o) {
if (!(o instanceof Entry<?> entry)) {
return false;
}
Object key = entry.getKey();
if (key == FluidTags.WATER) {
water = 0.0;
return true;
} else if (key == FluidTags.LAVA) {
lava = 0.0;
return true;
}
return false;
}
}
private final class EntryIterator implements ObjectIterator<Entry<TagKey<Fluid>>> {
private int index = 0;
private Entry<TagKey<Fluid>> entry = null;
@Override
public boolean hasNext() {
return index < 2;
}
@Override
public Entry<TagKey<Fluid>> next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
if (index == 0) {
index++;
return entry = new DoubleEntry(FluidTags.WATER);
} else {
index++;
return entry = new DoubleEntry(FluidTags.LAVA);
}
}
@Override
public void remove() {
if (entry == null) {
throw new IllegalStateException();
}
TagKey<Fluid> key = entry.getKey();
if (key == FluidTags.WATER) {
water = 0.0;
} else if (key == FluidTags.LAVA) {
lava = 0.0;
}
entry = null;
}
}
private final class DoubleEntry implements Entry<TagKey<Fluid>> {
private final TagKey<Fluid> key;
public DoubleEntry(TagKey<Fluid> key) {
this.key = key;
}
@Override
public TagKey<Fluid> getKey() {
return key;
}
@Override
public double getDoubleValue() {
return key == FluidTags.WATER ? water : lava;
}
@Override
public double setValue(double value) {
double prev;
if (key == FluidTags.WATER) {
prev = water;
water = value;
} else {
prev = lava;
lava = value;
}
return prev;
}
}
}

View File

@@ -0,0 +1,304 @@
package org.dreeam.leaf.world;
import ca.spottedleaf.moonrise.common.util.TickThread;
import it.unimi.dsi.fastutil.HashCommon;
import net.minecraft.server.level.ServerLevel;
import java.util.Arrays;
import java.util.concurrent.Future;
/// Optimized chunk map for main thread.
/// - Single-entry cache: Maintains a fast-access entry cache for the most recently accessed item
/// - Thread safety: Enforces single-threaded access with runtime checks
///
/// This map is designed to be accessed from a single thread at a time.
/// All mutating operations will throw [IllegalStateException]
/// if the current thread is not the owning thread.
///
/// @see it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap
public final class ChunkCache<V> {
private static final long EMPTY_KEY = Long.MIN_VALUE;
private static final float FACTOR = 0.5F;
private static final int MIN_N = HashCommon.arraySize(1024, FACTOR);
private long k1 = EMPTY_KEY;
private V v1 = null;
private Thread thread;
private transient long[] key;
private transient V[] value;
private transient int mask;
private transient boolean containsNullKey;
private transient int n;
private transient int maxFill;
private int size;
public ChunkCache(Thread thread) {
n = MIN_N;
mask = n - 1;
maxFill = HashCommon.maxFill(n, FACTOR);
key = new long[n + 1];
//noinspection unchecked
value = (V[]) new Object[n + 1];
this.thread = thread;
}
/// Retrieves the value associated with the specified key.
///
/// This method implements a single-entry cache optimization:
///
/// If the requested key matches the most recently accessed key,
/// the cached value is returned immediately without a hash map lookup.
///
/// This method does not perform thread checks for performance reasons.
///
/// # Safety
///
/// The caller must ensure that the current thread is the owning thread.
///
/// @param k The key whose associated value is to be returned
/// @return The value associated with the key, or `null` if no mapping exists
/// @implNote This method updates the single-entry cache on successful lookups
/// @see #isSameThread()
public V get(long k) {
long k1 = this.k1;
V v1 = this.v1;
if (k1 == k && v1 != null) {
return v1;
}
if (k == 0L) {
if (this.containsNullKey) {
this.k1 = k;
return this.v1 = this.value[this.n];
} else {
return null;
}
} else {
long curr;
final long[] key = this.key;
int pos;
if ((curr = key[pos = (int) HashCommon.mix(k) & this.mask]) == 0) {
return null;
} else if (k == curr) {
this.k1 = k;
return this.v1 = this.value[pos];
} else {
while (true) {
if ((curr = key[pos = pos + 1 & this.mask]) == 0) {
return null;
}
if (k == curr) {
this.k1 = k;
return this.v1 = this.value[pos];
}
}
}
}
}
/// Removes the mapping for the specified key from this map.
///
/// If the removed key matches the cached key, the single-entry cache is invalidated.
///
/// @param k The key whose mapping is to be removed
/// @return The previous value associated with the key, or `null` if no mapping existed
/// @throws IllegalStateException If the current thread is not the owning thread
public V remove(long k) {
// Safety: throws IllegalStateException for all non-owning threads
ensureSameThread();
if (k == k1) {
v1 = null;
k1 = EMPTY_KEY;
}
if (((k) == (0))) {
if (containsNullKey) return removeNullEntry();
return null;
}
long curr;
final long[] key = this.key;
int pos;
if (((curr = key[pos = (int) HashCommon.mix((k)) & mask]) == (0))) return null;
if (((k) == (curr))) return removeEntry(pos);
while (true) {
if (((curr = key[pos = (pos + 1) & mask]) == (0))) return null;
if (((k) == (curr))) return removeEntry(pos);
}
}
/// Associates the specified entry in this map.
///
/// If the key matches the cached key, the single-entry cache is invalidated.
///
/// @param k The key with which the specified value is to be associated
/// @param levelChunk The value to be associated with the specified key
/// @return The previous value associated with the key, or null if no mapping existed
/// @throws IllegalStateException If the current thread is not the owning thread
public V put(long k, V levelChunk) {
// Safety: throws IllegalStateException for all non-owning threads
ensureSameThread();
if (k == k1) {
v1 = null;
k1 = EMPTY_KEY;
}
final int pos = find(k);
if (pos < 0) {
insert(-pos - 1, k, levelChunk);
return null;
}
final V oldValue = value[pos];
value[pos] = levelChunk;
return oldValue;
}
/// Removes all elements from this map.
///
/// This method also clears the single-entry cache.
///
/// @throws IllegalStateException If the current thread is not the owning thread
public void clear() {
// Safety: throws IllegalStateException for all non-owning threads
ensureSameThread();
v1 = null;
k1 = EMPTY_KEY;
if (size == 0) return;
size = 0;
containsNullKey = false;
Arrays.fill(key, (0));
Arrays.fill(value, null);
}
/// Changes the owning thread of this map to the current thread.
///
/// # Safety
///
/// The caller must ensure proper happens before relationships
/// when transferring ownership between threads.
///
/// This should be done through proper synchronization mechanisms like
/// [Thread#join()] or [Future#get()].
///
/// @implNote This method does not perform synchronization
public void setThread() {
this.thread = Thread.currentThread();
}
/// Checks if the current thread is the same as the owning thread.
///
/// @return The current thread owns this map
public boolean isSameThread() {
return Thread.currentThread() == this.thread;
}
public boolean isSameThreadFor(ServerLevel serverLevel, int chunkX, int chunkZ) {
return Thread.currentThread() == this.thread && TickThread.isTickThreadFor(serverLevel, chunkX, chunkZ);
}
/// Ensure that the current thread is the owning thread.
///
/// @throws IllegalStateException If the current thread is not the owning thread
/// @see #isSameThread()
/// @see #setThread()
public void ensureSameThread() {
if (!isSameThread()) {
throw new IllegalStateException("Thread failed main thread check: Cannot update chunk status asynchronously, context=thread=" + Thread.currentThread().getName());
}
}
// from fastutil
private V removeEntry(final int pos) {
final V oldValue = value[pos];
value[pos] = null;
size--;
shiftKeys(pos);
if (n > MIN_N && size < maxFill / 4 && n > it.unimi.dsi.fastutil.Hash.DEFAULT_INITIAL_SIZE) {
rehash(n / 2);
}
return oldValue;
}
// from fastutil
private void shiftKeys(int pos) {
int last, slot;
long curr;
final long[] key = this.key;
final V[] value = this.value;
for (;;) {
pos = ((last = pos) + 1) & mask;
for (;;) {
if (((curr = key[pos]) == (0))) {
key[last] = (0);
value[last] = null;
return;
}
slot = (int) HashCommon.mix((curr)) & mask;
if (last <= pos ? last >= slot || slot > pos : last >= slot && slot > pos) break;
pos = (pos + 1) & mask;
}
key[last] = curr;
value[last] = value[pos];
}
}
// from fastutil
private V removeNullEntry() {
containsNullKey = false;
final V oldValue = value[n];
value[n] = null;
size--;
if (n > MIN_N && size < maxFill / 4 && n > it.unimi.dsi.fastutil.Hash.DEFAULT_INITIAL_SIZE) rehash(n / 2);
return oldValue;
}
// from fastutil
private void rehash(final int newN) {
final long[] key = this.key;
final V[] value = this.value;
final int mask = newN - 1;
final long[] newKey = new long[newN + 1];
//noinspection unchecked
final V[] newValue = (V[])new Object[newN + 1];
int i = n, pos;
for (int j = realSize(); j-- != 0;) {
//noinspection StatementWithEmptyBody
while (((key[--i]) == (0)));
if (!((newKey[pos = (int) HashCommon.mix((key[i])) & mask]) == (0)))
//noinspection StatementWithEmptyBody
while (!((newKey[pos = (pos + 1) & mask]) == (0)));
newKey[pos] = key[i];
newValue[pos] = value[i];
}
newValue[newN] = value[n];
n = newN;
this.mask = mask;
maxFill = HashCommon.maxFill(n, FACTOR);
this.key = newKey;
this.value = newValue;
}
// from fastutil
private int realSize() {
return containsNullKey ? size - 1 : size;
}
// from fastutil
private int find(final long k) {
if (((k) == (0))) return containsNullKey ? n : -(n + 1);
long curr;
final long[] key = this.key;
int pos;
if (((curr = key[pos = (int) HashCommon.mix((k)) & mask]) == (0))) return -(pos + 1);
if (((k) == (curr))) return pos;
while (true) {
if (((curr = key[pos = (pos + 1) & mask]) == (0))) return -(pos + 1);
if (((k) == (curr))) return pos;
}
}
// from fastutil
private void insert(final int pos, final long k, final V v) {
if (pos == n) containsNullKey = true;
key[pos] = k;
value[pos] = v;
if (size++ >= maxFill) rehash(HashCommon.arraySize(size + 1, FACTOR));
}
}

View File

@@ -0,0 +1,21 @@
package org.dreeam.leaf.world;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.shapes.VoxelShape;
public record EntityCollisionCache(
ObjectArrayList<VoxelShape> potentialCollisionsVoxel,
ObjectArrayList<AABB> potentialCollisionsBB,
ObjectArrayList<AABB> entityAABBs
) {
public EntityCollisionCache() {
this(new ObjectArrayList<>(), new ObjectArrayList<>(), new ObjectArrayList<>());
}
public void clear() {
potentialCollisionsVoxel.clear();
potentialCollisionsBB.clear();
entityAABBs.clear();
}
}