diff options
Diffstat (limited to 'patches/server/0009-MC-Utils.patch')
-rw-r--r-- | patches/server/0009-MC-Utils.patch | 2347 |
1 files changed, 77 insertions, 2270 deletions
diff --git a/patches/server/0009-MC-Utils.patch b/patches/server/0009-MC-Utils.patch index 1d50fab8f5..c12a8e7c5c 100644 --- a/patches/server/0009-MC-Utils.patch +++ b/patches/server/0009-MC-Utils.patch @@ -12,456 +12,6 @@ public net.minecraft.server.level.ServerChunkCache mainThreadProcessor public net.minecraft.server.level.ServerChunkCache$MainThreadExecutor public net.minecraft.world.level.chunk.LevelChunkSection states -diff --git a/src/main/java/com/destroystokyo/paper/util/concurrent/WeakSeqLock.java b/src/main/java/com/destroystokyo/paper/util/concurrent/WeakSeqLock.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4029dc68cf35d63aa70c4a76c35bf65a7fc6358f ---- /dev/null -+++ b/src/main/java/com/destroystokyo/paper/util/concurrent/WeakSeqLock.java -@@ -0,0 +1,68 @@ -+package com.destroystokyo.paper.util.concurrent; -+ -+import java.util.concurrent.atomic.AtomicLong; -+ -+/** -+ * copied from https://github.com/Spottedleaf/ConcurrentUtil/blob/master/src/main/java/ca/spottedleaf/concurrentutil/lock/WeakSeqLock.java -+ * @author Spottedleaf -+ */ -+public final class WeakSeqLock { -+ // TODO when the switch to J11 is made, nuke this class from orbit -+ -+ protected final AtomicLong lock = new AtomicLong(); -+ -+ public WeakSeqLock() { -+ //VarHandle.storeStoreFence(); // warn: usages must be checked to ensure this behaviour isn't needed -+ } -+ -+ public void acquireWrite() { -+ // must be release-type write -+ this.lock.lazySet(this.lock.get() + 1); -+ } -+ -+ public boolean canRead(final long read) { -+ return (read & 1) == 0; -+ } -+ -+ public boolean tryAcquireWrite() { -+ this.acquireWrite(); -+ return true; -+ } -+ -+ public void releaseWrite() { -+ // must be acquire-type write -+ final long lock = this.lock.get(); // volatile here acts as store-store -+ this.lock.lazySet(lock + 1); -+ } -+ -+ public void abortWrite() { -+ // must be acquire-type write -+ final long lock = this.lock.get(); // volatile here acts as store-store -+ this.lock.lazySet(lock ^ 1); -+ } -+ -+ public long acquireRead() { -+ int failures = 0; -+ long curr; -+ -+ for (curr = this.lock.get(); !this.canRead(curr); curr = this.lock.get()) { -+ // without j11, our only backoff is the yield() call... -+ -+ if (++failures > 5_000) { /* TODO determine a threshold */ -+ Thread.yield(); -+ } -+ /* Better waiting is beyond the scope of this lock; if it is needed the lock is being misused */ -+ } -+ -+ //VarHandle.loadLoadFence(); // volatile acts as the load-load barrier -+ return curr; -+ } -+ -+ public boolean tryReleaseRead(final long read) { -+ return this.lock.get() == read; // volatile acts as the load-load barrier -+ } -+ -+ public long getSequentialCounter() { -+ return this.lock.get(); -+ } -+} -diff --git a/src/main/java/com/destroystokyo/paper/util/map/QueuedChangesMapLong2Int.java b/src/main/java/com/destroystokyo/paper/util/map/QueuedChangesMapLong2Int.java -new file mode 100644 -index 0000000000000000000000000000000000000000..59868f37d14bbc0ece0836095cdad148778995e6 ---- /dev/null -+++ b/src/main/java/com/destroystokyo/paper/util/map/QueuedChangesMapLong2Int.java -@@ -0,0 +1,162 @@ -+package com.destroystokyo.paper.util.map; -+ -+import com.destroystokyo.paper.util.concurrent.WeakSeqLock; -+import it.unimi.dsi.fastutil.longs.Long2IntMap; -+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; -+import it.unimi.dsi.fastutil.longs.LongIterator; -+import it.unimi.dsi.fastutil.longs.LongOpenHashSet; -+import it.unimi.dsi.fastutil.objects.ObjectIterator; -+ -+/** -+ * @author Spottedleaf -+ */ -+public class QueuedChangesMapLong2Int { -+ -+ protected final Long2IntOpenHashMap updatingMap; -+ protected final Long2IntOpenHashMap visibleMap; -+ protected final Long2IntOpenHashMap queuedPuts; -+ protected final LongOpenHashSet queuedRemove; -+ -+ protected int queuedDefaultReturnValue; -+ -+ // we use a seqlock as writes are not common. -+ protected final WeakSeqLock updatingMapSeqLock = new WeakSeqLock(); -+ -+ public QueuedChangesMapLong2Int() { -+ this(16, 0.75f); -+ } -+ -+ public QueuedChangesMapLong2Int(final int capacity, final float loadFactor) { -+ this.updatingMap = new Long2IntOpenHashMap(capacity, loadFactor); -+ this.visibleMap = new Long2IntOpenHashMap(capacity, loadFactor); -+ this.queuedPuts = new Long2IntOpenHashMap(); -+ this.queuedRemove = new LongOpenHashSet(); -+ } -+ -+ public void queueDefaultReturnValue(final int dfl) { -+ this.queuedDefaultReturnValue = dfl; -+ this.updatingMap.defaultReturnValue(dfl); -+ } -+ -+ public int queueUpdate(final long k, final int v) { -+ this.queuedRemove.remove(k); -+ this.queuedPuts.put(k, v); -+ -+ return this.updatingMap.put(k, v); -+ } -+ -+ public int queueRemove(final long k) { -+ this.queuedPuts.remove(k); -+ this.queuedRemove.add(k); -+ -+ return this.updatingMap.remove(k); -+ } -+ -+ public int getUpdating(final long k) { -+ return this.updatingMap.get(k); -+ } -+ -+ public int getVisible(final long k) { -+ return this.visibleMap.get(k); -+ } -+ -+ public int getVisibleAsync(final long k) { -+ long readlock; -+ int ret = 0; -+ -+ do { -+ readlock = this.updatingMapSeqLock.acquireRead(); -+ try { -+ ret = this.visibleMap.get(k); -+ } catch (final Throwable thr) { -+ if (thr instanceof ThreadDeath) { -+ throw (ThreadDeath)thr; -+ } -+ // ignore... -+ continue; -+ } -+ -+ } while (!this.updatingMapSeqLock.tryReleaseRead(readlock)); -+ -+ return ret; -+ } -+ -+ public boolean performUpdates() { -+ this.updatingMapSeqLock.acquireWrite(); -+ this.visibleMap.defaultReturnValue(this.queuedDefaultReturnValue); -+ this.updatingMapSeqLock.releaseWrite(); -+ -+ if (this.queuedPuts.isEmpty() && this.queuedRemove.isEmpty()) { -+ return false; -+ } -+ -+ // update puts -+ final ObjectIterator<Long2IntMap.Entry> iterator0 = this.queuedPuts.long2IntEntrySet().fastIterator(); -+ while (iterator0.hasNext()) { -+ final Long2IntMap.Entry entry = iterator0.next(); -+ final long key = entry.getLongKey(); -+ final int val = entry.getIntValue(); -+ -+ this.updatingMapSeqLock.acquireWrite(); -+ try { -+ this.visibleMap.put(key, val); -+ } finally { -+ this.updatingMapSeqLock.releaseWrite(); -+ } -+ } -+ -+ this.queuedPuts.clear(); -+ -+ final LongIterator iterator1 = this.queuedRemove.iterator(); -+ while (iterator1.hasNext()) { -+ final long key = iterator1.nextLong(); -+ -+ this.updatingMapSeqLock.acquireWrite(); -+ try { -+ this.visibleMap.remove(key); -+ } finally { -+ this.updatingMapSeqLock.releaseWrite(); -+ } -+ } -+ -+ this.queuedRemove.clear(); -+ -+ return true; -+ } -+ -+ public boolean performUpdatesLockMap() { -+ this.updatingMapSeqLock.acquireWrite(); -+ try { -+ this.visibleMap.defaultReturnValue(this.queuedDefaultReturnValue); -+ -+ if (this.queuedPuts.isEmpty() && this.queuedRemove.isEmpty()) { -+ return false; -+ } -+ -+ // update puts -+ final ObjectIterator<Long2IntMap.Entry> iterator0 = this.queuedPuts.long2IntEntrySet().fastIterator(); -+ while (iterator0.hasNext()) { -+ final Long2IntMap.Entry entry = iterator0.next(); -+ final long key = entry.getLongKey(); -+ final int val = entry.getIntValue(); -+ -+ this.visibleMap.put(key, val); -+ } -+ -+ this.queuedPuts.clear(); -+ -+ final LongIterator iterator1 = this.queuedRemove.iterator(); -+ while (iterator1.hasNext()) { -+ final long key = iterator1.nextLong(); -+ -+ this.visibleMap.remove(key); -+ } -+ -+ this.queuedRemove.clear(); -+ -+ return true; -+ } finally { -+ this.updatingMapSeqLock.releaseWrite(); -+ } -+ } -+} -diff --git a/src/main/java/com/destroystokyo/paper/util/map/QueuedChangesMapLong2Object.java b/src/main/java/com/destroystokyo/paper/util/map/QueuedChangesMapLong2Object.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7bab31a312463cc963d9621cdc543a281459bd32 ---- /dev/null -+++ b/src/main/java/com/destroystokyo/paper/util/map/QueuedChangesMapLong2Object.java -@@ -0,0 +1,202 @@ -+package com.destroystokyo.paper.util.map; -+ -+import com.destroystokyo.paper.util.concurrent.WeakSeqLock; -+import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; -+import it.unimi.dsi.fastutil.longs.Long2ObjectMap; -+import it.unimi.dsi.fastutil.objects.ObjectBidirectionalIterator; -+import java.util.ArrayList; -+import java.util.Collection; -+import java.util.List; -+ -+/** -+ * @author Spottedleaf -+ */ -+public class QueuedChangesMapLong2Object<V> { -+ -+ protected static final Object REMOVED = new Object(); -+ -+ protected final Long2ObjectLinkedOpenHashMap<V> updatingMap; -+ protected final Long2ObjectLinkedOpenHashMap<V> visibleMap; -+ protected final Long2ObjectLinkedOpenHashMap<Object> queuedChanges; -+ -+ // we use a seqlock as writes are not common. -+ protected final WeakSeqLock updatingMapSeqLock = new WeakSeqLock(); -+ -+ public QueuedChangesMapLong2Object() { -+ this(16, 0.75f); // dfl for fastutil -+ } -+ -+ public QueuedChangesMapLong2Object(final int capacity, final float loadFactor) { -+ this.updatingMap = new Long2ObjectLinkedOpenHashMap<>(capacity, loadFactor); -+ this.visibleMap = new Long2ObjectLinkedOpenHashMap<>(capacity, loadFactor); -+ this.queuedChanges = new Long2ObjectLinkedOpenHashMap<>(); -+ } -+ -+ public V queueUpdate(final long k, final V value) { -+ this.queuedChanges.put(k, value); -+ return this.updatingMap.put(k, value); -+ } -+ -+ public V queueRemove(final long k) { -+ this.queuedChanges.put(k, REMOVED); -+ return this.updatingMap.remove(k); -+ } -+ -+ public V getUpdating(final long k) { -+ return this.updatingMap.get(k); -+ } -+ -+ public boolean updatingContainsKey(final long k) { -+ return this.updatingMap.containsKey(k); -+ } -+ -+ public V getVisible(final long k) { -+ return this.visibleMap.get(k); -+ } -+ -+ public boolean visibleContainsKey(final long k) { -+ return this.visibleMap.containsKey(k); -+ } -+ -+ public V getVisibleAsync(final long k) { -+ long readlock; -+ V ret = null; -+ -+ do { -+ readlock = this.updatingMapSeqLock.acquireRead(); -+ -+ try { -+ ret = this.visibleMap.get(k); -+ } catch (final Throwable thr) { -+ if (thr instanceof ThreadDeath) { -+ throw (ThreadDeath)thr; -+ } -+ // ignore... -+ continue; -+ } -+ -+ } while (!this.updatingMapSeqLock.tryReleaseRead(readlock)); -+ -+ return ret; -+ } -+ -+ public boolean visibleContainsKeyAsync(final long k) { -+ long readlock; -+ boolean ret = false; -+ -+ do { -+ readlock = this.updatingMapSeqLock.acquireRead(); -+ -+ try { -+ ret = this.visibleMap.containsKey(k); -+ } catch (final Throwable thr) { -+ if (thr instanceof ThreadDeath) { -+ throw (ThreadDeath)thr; -+ } -+ // ignore... -+ continue; -+ } -+ -+ } while (!this.updatingMapSeqLock.tryReleaseRead(readlock)); -+ -+ return ret; -+ } -+ -+ public Long2ObjectLinkedOpenHashMap<V> getVisibleMap() { -+ return this.visibleMap; -+ } -+ -+ public Long2ObjectLinkedOpenHashMap<V> getUpdatingMap() { -+ return this.updatingMap; -+ } -+ -+ public int getVisibleSize() { -+ return this.visibleMap.size(); -+ } -+ -+ public int getVisibleSizeAsync() { -+ long readlock; -+ int ret; -+ -+ do { -+ readlock = this.updatingMapSeqLock.acquireRead(); -+ ret = this.visibleMap.size(); -+ } while (!this.updatingMapSeqLock.tryReleaseRead(readlock)); -+ -+ return ret; -+ } -+ -+ // unlike mojang's impl this cannot be used async since it's not a view of an immutable map -+ public Collection<V> getUpdatingValues() { -+ return this.updatingMap.values(); -+ } -+ -+ public List<V> getUpdatingValuesCopy() { -+ return new ArrayList<>(this.updatingMap.values()); -+ } -+ -+ // unlike mojang's impl this cannot be used async since it's not a view of an immutable map -+ public Collection<V> getVisibleValues() { -+ return this.visibleMap.values(); -+ } -+ -+ public List<V> getVisibleValuesCopy() { -+ return new ArrayList<>(this.visibleMap.values()); -+ } -+ -+ public boolean performUpdates() { -+ if (this.queuedChanges.isEmpty()) { -+ return false; -+ } -+ -+ final ObjectBidirectionalIterator<Long2ObjectMap.Entry<Object>> iterator = this.queuedChanges.long2ObjectEntrySet().fastIterator(); -+ while (iterator.hasNext()) { -+ final Long2ObjectMap.Entry<Object> entry = iterator.next(); -+ final long key = entry.getLongKey(); -+ final Object val = entry.getValue(); -+ -+ this.updatingMapSeqLock.acquireWrite(); -+ try { -+ if (val == REMOVED) { -+ this.visibleMap.remove(key); -+ } else { -+ this.visibleMap.put(key, (V)val); -+ } -+ } finally { -+ this.updatingMapSeqLock.releaseWrite(); -+ } -+ } -+ -+ this.queuedChanges.clear(); -+ return true; -+ } -+ -+ public boolean performUpdatesLockMap() { -+ if (this.queuedChanges.isEmpty()) { -+ return false; -+ } -+ -+ final ObjectBidirectionalIterator<Long2ObjectMap.Entry<Object>> iterator = this.queuedChanges.long2ObjectEntrySet().fastIterator(); -+ -+ try { -+ this.updatingMapSeqLock.acquireWrite(); -+ -+ while (iterator.hasNext()) { -+ final Long2ObjectMap.Entry<Object> entry = iterator.next(); -+ final long key = entry.getLongKey(); -+ final Object val = entry.getValue(); -+ -+ if (val == REMOVED) { -+ this.visibleMap.remove(key); -+ } else { -+ this.visibleMap.put(key, (V)val); -+ } -+ } -+ } finally { -+ this.updatingMapSeqLock.releaseWrite(); -+ } -+ -+ this.queuedChanges.clear(); -+ return true; -+ } -+} diff --git a/src/main/java/com/destroystokyo/paper/util/maplist/ChunkList.java b/src/main/java/com/destroystokyo/paper/util/maplist/ChunkList.java new file mode 100644 index 0000000000000000000000000000000000000000..554f4d4e63c1431721989e6f502a32ccc53a8807 @@ -2178,495 +1728,12 @@ index 46cab7a8c7b87ab01b26074b04f5a02b3907cfc4..49019b4a9bc4e634d54a9b0acaf9229a + } + // Paper end } -diff --git a/src/main/java/io/papermc/paper/chunk/SingleThreadChunkRegionManager.java b/src/main/java/io/papermc/paper/chunk/SingleThreadChunkRegionManager.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a5f706d6f716b2a463ae58adcde69d9e665c7733 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/chunk/SingleThreadChunkRegionManager.java -@@ -0,0 +1,477 @@ -+package io.papermc.paper.chunk; -+ -+import io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet; -+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; -+import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet; -+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; -+import io.papermc.paper.util.MCUtil; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.world.level.ChunkPos; -+import java.util.ArrayList; -+import java.util.Arrays; -+import java.util.Iterator; -+import java.util.List; -+import java.util.function.Supplier; -+ -+public final class SingleThreadChunkRegionManager { -+ -+ protected final int regionSectionMergeRadius; -+ protected final int regionSectionChunkSize; -+ public final int regionChunkShift; // log2(REGION_CHUNK_SIZE) -+ -+ public final ServerLevel world; -+ public final String name; -+ -+ protected final Long2ObjectOpenHashMap<RegionSection> regionsBySection = new Long2ObjectOpenHashMap<>(); -+ protected final ReferenceLinkedOpenHashSet<Region> needsRecalculation = new ReferenceLinkedOpenHashSet<>(); -+ protected final int minSectionRecalcCount; -+ protected final double maxDeadRegionPercent; -+ protected final Supplier<RegionData> regionDataSupplier; -+ protected final Supplier<RegionSectionData> regionSectionDataSupplier; -+ -+ public SingleThreadChunkRegionManager(final ServerLevel world, final int minSectionRecalcCount, -+ final double maxDeadRegionPercent, final int sectionMergeRadius, -+ final int regionSectionChunkShift, -+ final String name, final Supplier<RegionData> regionDataSupplier, -+ final Supplier<RegionSectionData> regionSectionDataSupplier) { -+ this.regionSectionMergeRadius = sectionMergeRadius; -+ this.regionSectionChunkSize = 1 << regionSectionChunkShift; -+ this.regionChunkShift = regionSectionChunkShift; -+ this.world = world; -+ this.name = name; -+ this.minSectionRecalcCount = Math.max(2, minSectionRecalcCount); -+ this.maxDeadRegionPercent = maxDeadRegionPercent; -+ this.regionDataSupplier = regionDataSupplier; -+ this.regionSectionDataSupplier = regionSectionDataSupplier; -+ } -+ -+ // tested via https://gist.github.com/Spottedleaf/aa7ade3451c37b4cac061fc77074db2f -+ -+ /* -+ protected void check() { -+ ReferenceOpenHashSet<Region<T>> checked = new ReferenceOpenHashSet<>(); -+ -+ for (RegionSection<T> section : this.regionsBySection.values()) { -+ if (!checked.add(section.region)) { -+ section.region.check(); -+ } -+ } -+ for (Region<T> region : this.needsRecalculation) { -+ region.check(); -+ } -+ } -+ */ -+ -+ protected void addToRecalcQueue(final Region region) { -+ this.needsRecalculation.add(region); -+ } -+ -+ protected void removeFromRecalcQueue(final Region region) { -+ this.needsRecalculation.remove(region); -+ } -+ -+ public RegionSection getRegionSection(final int chunkX, final int chunkZ) { -+ return this.regionsBySection.get(MCUtil.getCoordinateKey(chunkX >> this.regionChunkShift, chunkZ >> this.regionChunkShift)); -+ } -+ -+ public Region getRegion(final int chunkX, final int chunkZ) { -+ final RegionSection section = this.regionsBySection.get(MCUtil.getCoordinateKey(chunkX >> regionChunkShift, chunkZ >> regionChunkShift)); -+ return section != null ? section.region : null; -+ } -+ -+ private final List<Region> toMerge = new ArrayList<>(); -+ -+ protected RegionSection getOrCreateAndMergeSection(final int sectionX, final int sectionZ, final RegionSection force) { -+ final long sectionKey = MCUtil.getCoordinateKey(sectionX, sectionZ); -+ -+ if (force == null) { -+ RegionSection region = this.regionsBySection.get(sectionKey); -+ if (region != null) { -+ return region; -+ } -+ } -+ -+ int mergeCandidateSectionSize = -1; -+ Region mergeIntoCandidate = null; -+ -+ // find optimal candidate to merge into -+ -+ final int minX = sectionX - this.regionSectionMergeRadius; -+ final int maxX = sectionX + this.regionSectionMergeRadius; -+ final int minZ = sectionZ - this.regionSectionMergeRadius; -+ final int maxZ = sectionZ + this.regionSectionMergeRadius; -+ for (int currX = minX; currX <= maxX; ++currX) { -+ for (int currZ = minZ; currZ <= maxZ; ++currZ) { -+ final RegionSection section = this.regionsBySection.get(MCUtil.getCoordinateKey(currX, currZ)); -+ if (section == null) { -+ continue; -+ } -+ final Region region = section.region; -+ if (region.dead) { -+ throw new IllegalStateException("Dead region should not be in live region manager state: " + region); -+ } -+ final int sections = region.sections.size(); -+ -+ if (sections > mergeCandidateSectionSize) { -+ mergeCandidateSectionSize = sections; -+ mergeIntoCandidate = region; -+ } -+ this.toMerge.add(region); -+ } -+ } -+ -+ // merge -+ if (mergeIntoCandidate != null) { -+ for (int i = 0; i < this.toMerge.size(); ++i) { -+ final Region region = this.toMerge.get(i); -+ if (region.dead || mergeIntoCandidate == region) { -+ continue; -+ } -+ region.mergeInto(mergeIntoCandidate); -+ } -+ this.toMerge.clear(); -+ } else { -+ mergeIntoCandidate = new Region(this); -+ } -+ -+ final RegionSection section; -+ if (force == null) { -+ this.regionsBySection.put(sectionKey, section = new RegionSection(sectionKey, this)); -+ } else { -+ final RegionSection existing = this.regionsBySection.putIfAbsent(sectionKey, force); -+ if (existing != null) { -+ throw new IllegalStateException("Attempting to override section '" + existing.toStringWithRegion() + -+ ", with " + force.toStringWithRegion()); -+ } -+ -+ section = force; -+ } -+ -+ mergeIntoCandidate.addRegionSection(section); -+ //mergeIntoCandidate.check(); -+ //this.check(); -+ -+ return section; -+ } -+ -+ public void addChunk(final int chunkX, final int chunkZ) { -+ this.getOrCreateAndMergeSection(chunkX >> this.regionChunkShift, chunkZ >> this.regionChunkShift, null).addChunk(chunkX, chunkZ); -+ } -+ -+ public void removeChunk(final int chunkX, final int chunkZ) { -+ final RegionSection section = this.regionsBySection.get( -+ MCUtil.getCoordinateKey(chunkX >> this.regionChunkShift, chunkZ >> this.regionChunkShift) -+ ); -+ if (section != null) { -+ section.removeChunk(chunkX, chunkZ); -+ } else { -+ throw new IllegalStateException("Cannot remove chunk at (" + chunkX + "," + chunkZ + ") from region state, section does not exist"); -+ } -+ } -+ -+ public void recalculateRegions() { -+ for (int i = 0, len = this.needsRecalculation.size(); i < len; ++i) { -+ final Region region = this.needsRecalculation.removeFirst(); -+ -+ this.recalculateRegion(region); -+ //this.check(); -+ } -+ } -+ -+ protected void recalculateRegion(final Region region) { -+ region.markedForRecalc = false; -+ //region.check(); -+ // clear unused regions -+ for (final Iterator<RegionSection> iterator = region.deadSections.iterator(); iterator.hasNext();) { -+ final RegionSection deadSection = iterator.next(); -+ -+ if (deadSection.hasChunks()) { -+ throw new IllegalStateException("Dead section '" + deadSection.toStringWithRegion() + "' is marked dead but has chunks!"); -+ } -+ if (!region.removeRegionSection(deadSection)) { -+ throw new IllegalStateException("Region " + region + " has inconsistent state, it should contain section " + deadSection); -+ } -+ if (!this.regionsBySection.remove(deadSection.regionCoordinate, deadSection)) { -+ throw new IllegalStateException("Cannot remove dead section '" + -+ deadSection.toStringWithRegion() + "' from section state! State at section coordinate: " + -+ this.regionsBySection.get(deadSection.regionCoordinate)); -+ } -+ } -+ region.deadSections.clear(); -+ -+ // implicitly cover cases where size == 0 -+ if (region.sections.size() < this.minSectionRecalcCount) { -+ //region.check(); -+ return; -+ } -+ -+ // run a test to see if we actually need to recalculate -+ // TODO -+ -+ // destroy and rebuild the region -+ region.dead = true; -+ -+ // destroy region state -+ for (final Iterator<RegionSection> iterator = region.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { -+ final RegionSection aliveSection = iterator.next(); -+ if (!aliveSection.hasChunks()) { -+ throw new IllegalStateException("Alive section '" + aliveSection.toStringWithRegion() + "' has no chunks!"); -+ } -+ if (!this.regionsBySection.remove(aliveSection.regionCoordinate, aliveSection)) { -+ throw new IllegalStateException("Cannot remove alive section '" + -+ aliveSection.toStringWithRegion() + "' from section state! State at section coordinate: " + -+ this.regionsBySection.get(aliveSection.regionCoordinate)); -+ } -+ } -+ -+ // rebuild regions -+ for (final Iterator<RegionSection> iterator = region.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { -+ final RegionSection aliveSection = iterator.next(); -+ this.getOrCreateAndMergeSection(aliveSection.getSectionX(), aliveSection.getSectionZ(), aliveSection); -+ } -+ } -+ -+ public static final class Region { -+ protected final IteratorSafeOrderedReferenceSet<RegionSection> sections = new IteratorSafeOrderedReferenceSet<>(); -+ protected final ReferenceOpenHashSet<RegionSection> deadSections = new ReferenceOpenHashSet<>(16, 0.7f); -+ protected boolean dead; -+ protected boolean markedForRecalc; -+ -+ public final SingleThreadChunkRegionManager regionManager; -+ public final RegionData regionData; -+ -+ protected Region(final SingleThreadChunkRegionManager regionManager) { -+ this.regionManager = regionManager; -+ this.regionData = regionManager.regionDataSupplier.get(); -+ } -+ -+ public IteratorSafeOrderedReferenceSet.Iterator<RegionSection> getSections() { -+ return this.sections.iterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); -+ } -+ -+ protected final double getDeadSectionPercent() { -+ return (double)this.deadSections.size() / (double)this.sections.size(); -+ } -+ -+ /* -+ protected void check() { -+ if (this.dead) { -+ throw new IllegalStateException("Dead region!"); -+ } -+ for (final Iterator<RegionSection<T>> iterator = this.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { -+ final RegionSection<T> section = iterator.next(); -+ if (section.region != this) { -+ throw new IllegalStateException("Region section must point to us!"); -+ } -+ if (this.regionManager.regionsBySection.get(section.regionCoordinate) != section) { -+ throw new IllegalStateException("Region section must match the regionmanager state!"); -+ } -+ } -+ } -+ */ -+ -+ // note: it is not true that the region at this point is not in any region. use the region field on the section -+ // to see if it is currently in another region. -+ protected final boolean addRegionSection(final RegionSection section) { -+ if (!this.sections.add(section)) { -+ return false; -+ } -+ -+ section.sectionData.addToRegion(section, section.region, this); -+ -+ section.region = this; -+ return true; -+ } -+ -+ protected final boolean removeRegionSection(final RegionSection section) { -+ if (!this.sections.remove(section)) { -+ return false; -+ } -+ -+ section.sectionData.removeFromRegion(section, this); -+ -+ return true; -+ } -+ -+ protected void mergeInto(final Region mergeTarget) { -+ if (this == mergeTarget) { -+ throw new IllegalStateException("Cannot merge a region onto itself"); -+ } -+ if (this.dead) { -+ throw new IllegalStateException("Source region is dead! Source " + this + ", target " + mergeTarget); -+ } else if (mergeTarget.dead) { -+ throw new IllegalStateException("Target region is dead! Source " + this + ", target " + mergeTarget); -+ } -+ this.dead = true; -+ if (this.markedForRecalc) { -+ this.regionManager.removeFromRecalcQueue(this); -+ } -+ -+ for (final Iterator<RegionSection> iterator = this.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { -+ final RegionSection section = iterator.next(); -+ -+ if (!mergeTarget.addRegionSection(section)) { -+ throw new IllegalStateException("Target cannot contain source's sections! Source " + this + ", target " + mergeTarget); -+ } -+ } -+ -+ for (final RegionSection deadSection : this.deadSections) { -+ if (!this.sections.contains(deadSection)) { -+ throw new IllegalStateException("Source region does not even contain its own dead sections! Missing " + deadSection + " from region " + this); -+ } -+ mergeTarget.deadSections.add(deadSection); -+ } -+ //mergeTarget.check(); -+ } -+ -+ protected void markSectionAlive(final RegionSection section) { -+ this.deadSections.remove(section); -+ if (this.markedForRecalc && (this.sections.size() < this.regionManager.minSectionRecalcCount || this.getDeadSectionPercent() < this.regionManager.maxDeadRegionPercent)) { -+ this.regionManager.removeFromRecalcQueue(this); -+ this.markedForRecalc = false; -+ } -+ } -+ -+ protected void markSectionDead(final RegionSection section) { -+ this.deadSections.add(section); -+ if (!this.markedForRecalc && (this.sections.size() >= this.regionManager.minSectionRecalcCount || this.sections.size() == this.deadSections.size()) && this.getDeadSectionPercent() >= this.regionManager.maxDeadRegionPercent) { -+ this.regionManager.addToRecalcQueue(this); -+ this.markedForRecalc = true; -+ } -+ } -+ -+ @Override -+ public String toString() { -+ final StringBuilder ret = new StringBuilder(128); -+ -+ ret.append("Region{"); -+ ret.append("dead=").append(this.dead).append(','); -+ ret.append("markedForRecalc=").append(this.markedForRecalc).append(','); -+ -+ ret.append("sectionCount=").append(this.sections.size()).append(','); -+ ret.append("sections=["); -+ for (final Iterator<RegionSection> iterator = this.sections.unsafeIterator(IteratorSafeOrderedReferenceSet.ITERATOR_FLAG_SEE_ADDITIONS); iterator.hasNext();) { -+ final RegionSection section = iterator.next(); -+ ret.append(section); -+ if (iterator.hasNext()) { -+ ret.append(','); -+ } -+ } -+ ret.append(']'); -+ -+ ret.append('}'); -+ return ret.toString(); -+ } -+ } -+ -+ public static final class RegionSection { -+ protected final long regionCoordinate; -+ protected final long[] chunksBitset; -+ protected int chunkCount; -+ protected Region region; -+ -+ public final SingleThreadChunkRegionManager regionManager; -+ public final RegionSectionData sectionData; -+ -+ protected RegionSection(final long regionCoordinate, final SingleThreadChunkRegionManager regionManager) { -+ this.regionCoordinate = regionCoordinate; -+ this.regionManager = regionManager; -+ this.chunksBitset = new long[Math.max(1, regionManager.regionSectionChunkSize * regionManager.regionSectionChunkSize / Long.SIZE)]; -+ this.sectionData = regionManager.regionSectionDataSupplier.get(); -+ } -+ -+ public int getSectionX() { -+ return MCUtil.getCoordinateX(this.regionCoordinate); -+ } -+ -+ public int getSectionZ() { -+ return MCUtil.getCoordinateZ(this.regionCoordinate); -+ } -+ -+ public Region getRegion() { -+ return this.region; -+ } -+ -+ private int getChunkIndex(final int chunkX, final int chunkZ) { -+ return (chunkX & (this.regionManager.regionSectionChunkSize - 1)) | ((chunkZ & (this.regionManager.regionSectionChunkSize - 1)) << this.regionManager.regionChunkShift); -+ } -+ -+ protected boolean hasChunks() { -+ return this.chunkCount != 0; -+ } -+ -+ protected void addChunk(final int chunkX, final int chunkZ) { -+ final int index = this.getChunkIndex(chunkX, chunkZ); -+ final long bitset = this.chunksBitset[index >>> 6]; // index / Long.SIZE -+ final long after = this.chunksBitset[index >>> 6] = bitset | (1L << (index & (Long.SIZE - 1))); -+ if (after == bitset) { -+ throw new IllegalStateException("Cannot add a chunk to a section which already has the chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString()); -+ } -+ if (++this.chunkCount != 1) { -+ return; -+ } -+ this.region.markSectionAlive(this); -+ } -+ -+ protected void removeChunk(final int chunkX, final int chunkZ) { -+ final int index = this.getChunkIndex(chunkX, chunkZ); -+ final long before = this.chunksBitset[index >>> 6]; // index / Long.SIZE -+ final long bitset = this.chunksBitset[index >>> 6] = before & ~(1L << (index & (Long.SIZE - 1))); -+ if (before == bitset) { -+ throw new IllegalStateException("Cannot remove a chunk from a section which does not have that chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString()); -+ } -+ if (--this.chunkCount != 0) { -+ return; -+ } -+ this.region.markSectionDead(this); -+ } -+ -+ @Override -+ public String toString() { -+ return "RegionSection{" + -+ "regionCoordinate=" + new ChunkPos(this.regionCoordinate).toString() + "," + -+ "chunkCount=" + this.chunkCount + "," + -+ "chunksBitset=" + toString(this.chunksBitset) + "," + -+ "hash=" + this.hashCode() + -+ "}"; -+ } -+ -+ public String toStringWithRegion() { -+ return "RegionSection{" + -+ "regionCoordinate=" + new ChunkPos(this.regionCoordinate).toString() + "," + -+ "chunkCount=" + this.chunkCount + "," + -+ "chunksBitset=" + toString(this.chunksBitset) + "," + -+ "hash=" + this.hashCode() + "," + -+ "region=" + this.region + -+ "}"; -+ } -+ -+ private static String toString(final long[] array) { -+ final StringBuilder ret = new StringBuilder(); -+ for (final long value : array) { -+ // zero pad the hex string -+ final char[] zeros = new char[Long.SIZE / 4]; -+ Arrays.fill(zeros, '0'); -+ final String string = Long.toHexString(value); -+ System.arraycopy(string.toCharArray(), 0, zeros, zeros.length - string.length(), string.length()); -+ -+ ret.append(zeros); -+ } -+ -+ return ret.toString(); -+ } -+ } -+ -+ public static interface RegionData { -+ -+ } -+ -+ public static interface RegionSectionData { -+ -+ public void removeFromRegion(final RegionSection section, final Region from); -+ -+ // removal from the old region is handled via removeFromRegion -+ public void addToRegion(final RegionSection section, final Region oldRegion, final Region newRegion); -+ -+ } -+} diff --git a/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java new file mode 100644 -index 0000000000000000000000000000000000000000..0d0cb3e63acd5156b6f9d6d78cc949b0af36a77b +index 0000000000000000000000000000000000000000..a79abe9b26f68d573812e91554124783075ae17a --- /dev/null +++ b/src/main/java/io/papermc/paper/chunk/system/ChunkSystem.java -@@ -0,0 +1,303 @@ +@@ -0,0 +1,297 @@ +package io.papermc.paper.chunk.system; + +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; @@ -2720,12 +1787,16 @@ index 0000000000000000000000000000000000000000..0d0cb3e63acd5156b6f9d6d78cc949b0 + } + scheduleChunkLoad(level, chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> { + if (chunk == null) { -+ onComplete.accept(null); ++ if (onComplete != null) { ++ onComplete.accept(null); ++ } + } else { + if (chunk.getPersistedStatus().isOrAfter(toStatus)) { + scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete); + } else { -+ onComplete.accept(null); ++ if (onComplete != null) { ++ onComplete.accept(null); ++ } + } + } + }); @@ -2757,8 +1828,6 @@ index 0000000000000000000000000000000000000000..0d0cb3e63acd5156b6f9d6d78cc949b0 + if (onComplete != null) { + onComplete.accept(chunk); + } -+ } catch (final ThreadDeath death) { -+ throw death; + } catch (final Throwable thr) { + LOGGER.error("Exception handling chunk load callback", thr); + SneakyThrow.sneaky(thr); @@ -2825,8 +1894,6 @@ index 0000000000000000000000000000000000000000..0d0cb3e63acd5156b6f9d6d78cc949b0 + if (onComplete != null) { + onComplete.accept(chunk); + } -+ } catch (final ThreadDeath death) { -+ throw death; + } catch (final Throwable thr) { + LOGGER.error("Exception handling chunk load callback", thr); + SneakyThrow.sneaky(thr); @@ -2905,21 +1972,15 @@ index 0000000000000000000000000000000000000000..0d0cb3e63acd5156b6f9d6d78cc949b0 + } + + public static void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) { -+ final ChunkMap chunkMap = level.chunkSource.chunkMap; -+ for (int index = 0, len = chunkMap.regionManagers.size(); index < len; ++index) { -+ chunkMap.regionManagers.get(index).addChunk(holder.getPos().x, holder.getPos().z); -+ } ++ + } + + public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) { -+ final ChunkMap chunkMap = level.chunkSource.chunkMap; -+ for (int index = 0, len = chunkMap.regionManagers.size(); index < len; ++index) { -+ chunkMap.regionManagers.get(index).removeChunk(holder.getPos().x, holder.getPos().z); -+ } ++ + } + + public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) { -+ chunk.playerChunk = holder; ++ + } + + public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) { @@ -3368,14 +2429,16 @@ index 0000000000000000000000000000000000000000..16785bd5c0524f6bad0691ca7ecd4514 +} diff --git a/src/main/java/io/papermc/paper/util/IntervalledCounter.java b/src/main/java/io/papermc/paper/util/IntervalledCounter.java new file mode 100644 -index 0000000000000000000000000000000000000000..cea9c098ade00ee87b8efc8164ab72f5279758f0 +index 0000000000000000000000000000000000000000..c90acc3bde887b9c8f8d49fcc3195657c721bc14 --- /dev/null +++ b/src/main/java/io/papermc/paper/util/IntervalledCounter.java -@@ -0,0 +1,115 @@ +@@ -0,0 +1,128 @@ +package io.papermc.paper.util; + +public final class IntervalledCounter { + ++ private static final int INITIAL_SIZE = 8; ++ + protected long[] times; + protected long[] counts; + protected final long interval; @@ -3385,8 +2448,8 @@ index 0000000000000000000000000000000000000000..cea9c098ade00ee87b8efc8164ab72f5 + protected int tail; // exclusive + + public IntervalledCounter(final long interval) { -+ this.times = new long[8]; -+ this.counts = new long[8]; ++ this.times = new long[INITIAL_SIZE]; ++ this.counts = new long[INITIAL_SIZE]; + this.interval = interval; + } + @@ -3441,13 +2504,13 @@ index 0000000000000000000000000000000000000000..cea9c098ade00ee87b8efc8164ab72f5 + this.tail = nextTail; + } + -+ public void updateAndAdd(final int count) { ++ public void updateAndAdd(final long count) { + final long currTime = System.nanoTime(); + this.updateCurrentTime(currTime); + this.addTime(currTime, count); + } + -+ public void updateAndAdd(final int count, final long currTime) { ++ public void updateAndAdd(final long count, final long currTime) { + this.updateCurrentTime(currTime); + this.addTime(currTime, count); + } @@ -3467,9 +2530,12 @@ index 0000000000000000000000000000000000000000..cea9c098ade00ee87b8efc8164ab72f5 + this.tail = size; + + if (tail >= head) { ++ // sequentially ordered from [head, tail) + System.arraycopy(oldElements, head, newElements, 0, size); + System.arraycopy(oldCounts, head, newCounts, 0, size); + } else { ++ // ordered from [head, length) ++ // then followed by [0, tail) + System.arraycopy(oldElements, head, newElements, 0, oldElements.length - head); + System.arraycopy(oldElements, 0, newElements, oldElements.length - head, tail); + @@ -3480,19 +2546,27 @@ index 0000000000000000000000000000000000000000..cea9c098ade00ee87b8efc8164ab72f5 + + // returns in units per second + public double getRate() { -+ return this.size() / (this.interval * 1.0e-9); ++ return (double)this.sum / ((double)this.interval * 1.0E-9); + } + -+ public long size() { ++ public long getInterval() { ++ return this.interval; ++ } ++ ++ public long getSum() { + return this.sum; + } ++ ++ public int totalDataPoints() { ++ return this.tail >= this.head ? (this.tail - this.head) : (this.tail + (this.counts.length - this.head)); ++ } +} diff --git a/src/main/java/io/papermc/paper/util/MCUtil.java b/src/main/java/io/papermc/paper/util/MCUtil.java new file mode 100644 -index 0000000000000000000000000000000000000000..eb36bef19e6729c1cc44aefa927317963aba929e +index 0000000000000000000000000000000000000000..c6c723d9378c593c8608d5940f63c98dff097cd0 --- /dev/null +++ b/src/main/java/io/papermc/paper/util/MCUtil.java -@@ -0,0 +1,554 @@ +@@ -0,0 +1,550 @@ +package io.papermc.paper.util; + +import com.google.common.collect.ImmutableList; @@ -4020,10 +3094,6 @@ index 0000000000000000000000000000000000000000..eb36bef19e6729c1cc44aefa92731796 + } + } + -+ public static int getTicketLevelFor(net.minecraft.world.level.chunk.status.ChunkStatus status) { -+ return net.minecraft.server.level.ChunkMap.MAX_VIEW_DISTANCE + io.papermc.paper.chunk.system.ChunkSystem.getDistance(status); -+ } -+ + @NotNull + public static <T> List<T> copyListAndAdd(@NotNull final List<T> original, + @NotNull final T newElement) { @@ -4469,1033 +3539,6 @@ index 0000000000000000000000000000000000000000..0fd814f1d65c111266a2b20f86561839 + } + } +} -diff --git a/src/main/java/io/papermc/paper/util/misc/Delayed26WayDistancePropagator3D.java b/src/main/java/io/papermc/paper/util/misc/Delayed26WayDistancePropagator3D.java -new file mode 100644 -index 0000000000000000000000000000000000000000..470402573bc31106d5a63e415b958fb7f9c36aa9 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/util/misc/Delayed26WayDistancePropagator3D.java -@@ -0,0 +1,297 @@ -+package io.papermc.paper.util.misc; -+ -+import io.papermc.paper.util.CoordinateUtils; -+import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; -+import it.unimi.dsi.fastutil.longs.LongIterator; -+import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; -+ -+public final class Delayed26WayDistancePropagator3D { -+ -+ // this map is considered "stale" unless updates are propagated. -+ protected final Delayed8WayDistancePropagator2D.LevelMap levels = new Delayed8WayDistancePropagator2D.LevelMap(8192*2, 0.6f); -+ -+ // this map is never stale -+ protected final Long2ByteOpenHashMap sources = new Long2ByteOpenHashMap(4096, 0.6f); -+ -+ // Generally updates to positions are made close to other updates, so we link to decrease cache misses when -+ // propagating updates -+ protected final LongLinkedOpenHashSet updatedSources = new LongLinkedOpenHashSet(); -+ -+ @FunctionalInterface -+ public static interface LevelChangeCallback { -+ -+ /** -+ * This can be called for intermediate updates. So do not rely on newLevel being close to or -+ * the exact level that is expected after a full propagation has occured. -+ */ -+ public void onLevelUpdate(final long coordinate, final byte oldLevel, final byte newLevel); -+ -+ } -+ -+ protected final LevelChangeCallback changeCallback; -+ -+ public Delayed26WayDistancePropagator3D() { -+ this(null); -+ } -+ -+ public Delayed26WayDistancePropagator3D(final LevelChangeCallback changeCallback) { -+ this.changeCallback = changeCallback; -+ } -+ -+ public int getLevel(final long pos) { -+ return this.levels.get(pos); -+ } -+ -+ public int getLevel(final int x, final int y, final int z) { -+ return this.levels.get(CoordinateUtils.getChunkSectionKey(x, y, z)); -+ } -+ -+ public void setSource(final int x, final int y, final int z, final int level) { -+ this.setSource(CoordinateUtils.getChunkSectionKey(x, y, z), level); -+ } -+ -+ public void setSource(final long coordinate, final int level) { -+ if ((level & 63) != level || level == 0) { -+ throw new IllegalArgumentException("Level must be in (0, 63], not " + level); -+ } -+ -+ final byte byteLevel = (byte)level; -+ final byte oldLevel = this.sources.put(coordinate, byteLevel); -+ -+ if (oldLevel == byteLevel) { -+ return; // nothing to do -+ } -+ -+ // queue to update later -+ this.updatedSources.add(coordinate); -+ } -+ -+ public void removeSource(final int x, final int y, final int z) { -+ this.removeSource(CoordinateUtils.getChunkSectionKey(x, y, z)); -+ } -+ -+ public void removeSource(final long coordinate) { -+ if (this.sources.remove(coordinate) != 0) { -+ this.updatedSources.add(coordinate); -+ } -+ } -+ -+ // queues used for BFS propagating levels -+ protected final Delayed8WayDistancePropagator2D.WorkQueue[] levelIncreaseWorkQueues = new Delayed8WayDistancePropagator2D.WorkQueue[64]; -+ { -+ for (int i = 0; i < this.levelIncreaseWorkQueues.length; ++i) { -+ this.levelIncreaseWorkQueues[i] = new Delayed8WayDistancePropagator2D.WorkQueue(); -+ } -+ } -+ protected final Delayed8WayDistancePropagator2D.WorkQueue[] levelRemoveWorkQueues = new Delayed8WayDistancePropagator2D.WorkQueue[64]; -+ { -+ for (int i = 0; i < this.levelRemoveWorkQueues.length; ++i) { -+ this.levelRemoveWorkQueues[i] = new Delayed8WayDistancePropagator2D.WorkQueue(); -+ } -+ } -+ protected long levelIncreaseWorkQueueBitset; -+ protected long levelRemoveWorkQueueBitset; -+ -+ protected final void addToIncreaseWorkQueue(final long coordinate, final byte level) { -+ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[level]; -+ queue.queuedCoordinates.enqueue(coordinate); -+ queue.queuedLevels.enqueue(level); -+ -+ this.levelIncreaseWorkQueueBitset |= (1L << level); -+ } -+ -+ protected final void addToIncreaseWorkQueue(final long coordinate, final byte index, final byte level) { -+ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[index]; -+ queue.queuedCoordinates.enqueue(coordinate); -+ queue.queuedLevels.enqueue(level); -+ -+ this.levelIncreaseWorkQueueBitset |= (1L << index); -+ } -+ -+ protected final void addToRemoveWorkQueue(final long coordinate, final byte level) { -+ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelRemoveWorkQueues[level]; -+ queue.queuedCoordinates.enqueue(coordinate); -+ queue.queuedLevels.enqueue(level); -+ -+ this.levelRemoveWorkQueueBitset |= (1L << level); -+ } -+ -+ public boolean propagateUpdates() { -+ if (this.updatedSources.isEmpty()) { -+ return false; -+ } -+ -+ boolean ret = false; -+ -+ for (final LongIterator iterator = this.updatedSources.iterator(); iterator.hasNext();) { -+ final long coordinate = iterator.nextLong(); -+ -+ final byte currentLevel = this.levels.get(coordinate); -+ final byte updatedSource = this.sources.get(coordinate); -+ -+ if (currentLevel == updatedSource) { -+ continue; -+ } -+ ret = true; -+ -+ if (updatedSource > currentLevel) { -+ // level increase -+ this.addToIncreaseWorkQueue(coordinate, updatedSource); -+ } else { -+ // level decrease -+ this.addToRemoveWorkQueue(coordinate, currentLevel); -+ // if the current coordinate is a source, then the decrease propagation will detect that and queue -+ // the source propagation -+ } -+ } -+ -+ this.updatedSources.clear(); -+ -+ // propagate source level increases first for performance reasons (in crowded areas hopefully the additions -+ // make the removes remove less) -+ this.propagateIncreases(); -+ -+ // now we propagate the decreases (which will then re-propagate clobbered sources) -+ this.propagateDecreases(); -+ -+ return ret; -+ } -+ -+ protected void propagateIncreases() { -+ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset); -+ this.levelIncreaseWorkQueueBitset != 0L; -+ this.levelIncreaseWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset)) { -+ -+ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelIncreaseWorkQueues[queueIndex]; -+ while (!queue.queuedLevels.isEmpty()) { -+ final long coordinate = queue.queuedCoordinates.removeFirstLong(); -+ byte level = queue.queuedLevels.removeFirstByte(); -+ -+ final boolean neighbourCheck = level < 0; -+ -+ final byte currentLevel; -+ if (neighbourCheck) { -+ level = (byte)-level; -+ currentLevel = this.levels.get(coordinate); -+ } else { -+ currentLevel = this.levels.putIfGreater(coordinate, level); -+ } -+ -+ if (neighbourCheck) { -+ // used when propagating from decrease to indicate that this level needs to check its neighbours -+ // this means the level at coordinate could be equal, but would still need neighbours checked -+ -+ if (currentLevel != level) { -+ // something caused the level to change, which means something propagated to it (which means -+ // us propagating here is redundant), or something removed the level (which means we -+ // cannot propagate further) -+ continue; -+ } -+ } else if (currentLevel >= level) { -+ // something higher/equal propagated -+ continue; -+ } -+ if (this.changeCallback != null) { -+ this.changeCallback.onLevelUpdate(coordinate, currentLevel, level); -+ } -+ -+ if (level == 1) { -+ // can't propagate 0 to neighbours -+ continue; -+ } -+ -+ // propagate to neighbours -+ final byte neighbourLevel = (byte)(level - 1); -+ final int x = CoordinateUtils.getChunkSectionX(coordinate); -+ final int y = CoordinateUtils.getChunkSectionY(coordinate); -+ final int z = CoordinateUtils.getChunkSectionZ(coordinate); -+ -+ for (int dy = -1; dy <= 1; ++dy) { -+ for (int dz = -1; dz <= 1; ++dz) { -+ for (int dx = -1; dx <= 1; ++dx) { -+ if ((dy | dz | dx) == 0) { -+ // already propagated to coordinate -+ continue; -+ } -+ -+ // sure we can check the neighbour level in the map right now and avoid a propagation, -+ // but then we would still have to recheck it when popping the value off of the queue! -+ // so just avoid the double lookup -+ final long neighbourCoordinate = CoordinateUtils.getChunkSectionKey(dx + x, dy + y, dz + z); -+ this.addToIncreaseWorkQueue(neighbourCoordinate, neighbourLevel); -+ } -+ } -+ } -+ } -+ } -+ } -+ -+ protected void propagateDecreases() { -+ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset); -+ this.levelRemoveWorkQueueBitset != 0L; -+ this.levelRemoveWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset)) { -+ -+ final Delayed8WayDistancePropagator2D.WorkQueue queue = this.levelRemoveWorkQueues[queueIndex]; -+ while (!queue.queuedLevels.isEmpty()) { -+ final long coordinate = queue.queuedCoordinates.removeFirstLong(); -+ final byte level = queue.queuedLevels.removeFirstByte(); -+ -+ final byte currentLevel = this.levels.removeIfGreaterOrEqual(coordinate, level); -+ if (currentLevel == 0) { -+ // something else removed -+ continue; -+ } -+ -+ if (currentLevel > level) { -+ // something higher propagated here or we hit the propagation of another source -+ // in the second case we need to re-propagate because we could have just clobbered another source's -+ // propagation -+ this.addToIncreaseWorkQueue(coordinate, currentLevel, (byte)-currentLevel); // indicate to the increase code that the level's neighbours need checking -+ continue; -+ } -+ -+ if (this.changeCallback != null) { -+ this.changeCallback.onLevelUpdate(coordinate, currentLevel, (byte)0); -+ } -+ -+ final byte source = this.sources.get(coordinate); -+ if (source != 0) { -+ // must re-propagate source later -+ this.addToIncreaseWorkQueue(coordinate, source); -+ } -+ -+ if (level == 0) { -+ // can't propagate -1 to neighbours -+ // we have to check neighbours for removing 1 just in case the neighbour is 2 -+ continue; -+ } -+ -+ // propagate to neighbours -+ final byte neighbourLevel = (byte)(level - 1); -+ final int x = CoordinateUtils.getChunkSectionX(coordinate); -+ final int y = CoordinateUtils.getChunkSectionY(coordinate); -+ final int z = CoordinateUtils.getChunkSectionZ(coordinate); -+ -+ for (int dy = -1; dy <= 1; ++dy) { -+ for (int dz = -1; dz <= 1; ++dz) { -+ for (int dx = -1; dx <= 1; ++dx) { -+ if ((dy | dz | dx) == 0) { -+ // already propagated to coordinate -+ continue; -+ } -+ -+ // sure we can check the neighbour level in the map right now and avoid a propagation, -+ // but then we would still have to recheck it when popping the value off of the queue! -+ // so just avoid the double lookup -+ final long neighbourCoordinate = CoordinateUtils.getChunkSectionKey(dx + x, dy + y, dz + z); -+ this.addToRemoveWorkQueue(neighbourCoordinate, neighbourLevel); -+ } -+ } -+ } -+ } -+ } -+ -+ // propagate sources we clobbered in the process -+ this.propagateIncreases(); -+ } -+} -diff --git a/src/main/java/io/papermc/paper/util/misc/Delayed8WayDistancePropagator2D.java b/src/main/java/io/papermc/paper/util/misc/Delayed8WayDistancePropagator2D.java -new file mode 100644 -index 0000000000000000000000000000000000000000..808d1449ac44ae86a650932365081fbaf178d141 ---- /dev/null -+++ b/src/main/java/io/papermc/paper/util/misc/Delayed8WayDistancePropagator2D.java -@@ -0,0 +1,718 @@ -+package io.papermc.paper.util.misc; -+ -+import it.unimi.dsi.fastutil.HashCommon; -+import it.unimi.dsi.fastutil.bytes.ByteArrayFIFOQueue; -+import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; -+import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; -+import it.unimi.dsi.fastutil.longs.LongIterator; -+import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet; -+import io.papermc.paper.util.MCUtil; -+ -+public final class Delayed8WayDistancePropagator2D { -+ -+ // Test -+ /* -+ protected static void test(int x, int z, com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap<Ticket> reference, Delayed8WayDistancePropagator2D test) { -+ int got = test.getLevel(x, z); -+ -+ int expect = 0; -+ Object[] nearest = reference.getObjectsInRange(x, z) == null ? null : reference.getObjectsInRange(x, z).getBackingSet(); -+ if (nearest != null) { -+ for (Object _obj : nearest) { -+ if (_obj instanceof Ticket) { -+ Ticket ticket = (Ticket)_obj; -+ long ticketCoord = reference.getLastCoordinate(ticket); -+ int viewDistance = reference.getLastViewDistance(ticket); -+ int distance = Math.max(com.destroystokyo.paper.util.math.IntegerUtil.branchlessAbs(MCUtil.getCoordinateX(ticketCoord) - x), -+ com.destroystokyo.paper.util.math.IntegerUtil.branchlessAbs(MCUtil.getCoordinateZ(ticketCoord) - z)); -+ int level = viewDistance - distance; -+ if (level > expect) { -+ expect = level; -+ } -+ } -+ } -+ } -+ -+ if (expect != got) { -+ throw new IllegalStateException("Expected " + expect + " at pos (" + x + "," + z + ") but got " + got); -+ } -+ } -+ -+ static class Ticket { -+ -+ int x; -+ int z; -+ -+ final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<Ticket> empty -+ = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<>(this); -+ -+ } -+ -+ public static void main(final String[] args) { -+ com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap<Ticket> reference = new com.destroystokyo.paper.util.misc.DistanceTrackingAreaMap<Ticket>() { -+ @Override -+ protected com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<Ticket> getEmptySetFor(Ticket object) { -+ return object.empty; -+ } -+ }; -+ Delayed8WayDistancePropagator2D test = new Delayed8WayDistancePropagator2D(); -+ -+ final int maxDistance = 64; -+ // test origin -+ { -+ Ticket originTicket = new Ticket(); -+ int originDistance = 31; -+ // test single source -+ reference.add(originTicket, 0, 0, originDistance); -+ test.setSource(0, 0, originDistance); test.propagateUpdates(); // set and propagate -+ for (int dx = -originDistance; dx <= originDistance; ++dx) { -+ for (int dz = -originDistance; dz <= originDistance; ++dz) { -+ test(dx, dz, reference, test); -+ } -+ } -+ // test single source decrease -+ reference.update(originTicket, 0, 0, originDistance/2); -+ test.setSource(0, 0, originDistance/2); test.propagateUpdates(); // set and propagate -+ for (int dx = -originDistance; dx <= originDistance; ++dx) { -+ for (int dz = -originDistance; dz <= originDistance; ++dz) { -+ test(dx, dz, reference, test); -+ } -+ } -+ // test source increase -+ originDistance = 2*originDistance; -+ reference.update(originTicket, 0, 0, originDistance); -+ test.setSource(0, 0, originDistance); test.propagateUpdates(); // set and propagate -+ for (int dx = -4*originDistance; dx <= 4*originDistance; ++dx) { -+ for (int dz = -4*originDistance; dz <= 4*originDistance; ++dz) { -+ test(dx, dz, reference, test); -+ } -+ } -+ -+ reference.remove(originTicket); -+ test.removeSource(0, 0); test.propagateUpdates(); -+ } -+ -+ // test multiple sources at origin -+ { -+ int originDistance = 31; -+ java.util.List<Ticket> list = new java.util.ArrayList<>(); -+ for (int i = 0; i < 10; ++i) { -+ Ticket a = new Ticket(); -+ list.add(a); -+ a.x = (i & 1) == 1 ? -i : i; -+ a.z = (i & 1) == 1 ? -i : i; -+ } -+ for (Ticket ticket : list) { -+ reference.add(ticket, ticket.x, ticket.z, originDistance); -+ test.setSource(ticket.x, ticket.z, originDistance); -+ } -+ test.propagateUpdates(); -+ -+ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) { -+ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) { -+ test(dx, dz, reference, test); -+ } -+ } -+ -+ // test ticket level decrease -+ -+ for (Ticket ticket : list) { -+ reference.update(ticket, ticket.x, ticket.z, originDistance/2); -+ test.setSource(ticket.x, ticket.z, originDistance/2); -+ } -+ test.propagateUpdates(); -+ -+ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) { -+ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) { -+ test(dx, dz, reference, test); -+ } -+ } -+ -+ // test ticket level increase -+ -+ for (Ticket ticket : list) { -+ reference.update(ticket, ticket.x, ticket.z, originDistance*2); -+ test.setSource(ticket.x, ticket.z, originDistance*2); -+ } -+ test.propagateUpdates(); -+ -+ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) { -+ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) { -+ test(dx, dz, reference, test); -+ } -+ } -+ -+ // test ticket remove -+ for (int i = 0, len = list.size(); i < len; ++i) { -+ if ((i & 3) != 0) { -+ continue; -+ } -+ Ticket ticket = list.get(i); -+ reference.remove(ticket); -+ test.removeSource(ticket.x, ticket.z); -+ } -+ test.propagateUpdates(); -+ -+ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) { -+ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) { -+ test(dx, dz, reference, test); -+ } -+ } -+ } -+ -+ // now test at coordinate offsets -+ // test offset -+ { -+ Ticket originTicket = new Ticket(); -+ int originDistance = 31; -+ int offX = 54432; -+ int offZ = -134567; -+ // test single source -+ reference.add(originTicket, offX, offZ, originDistance); -+ test.setSource(offX, offZ, originDistance); test.propagateUpdates(); // set and propagate -+ for (int dx = -originDistance; dx <= originDistance; ++dx) { -+ for (int dz = -originDistance; dz <= originDistance; ++dz) { -+ test(dx + offX, dz + offZ, reference, test); -+ } -+ } -+ // test single source decrease -+ reference.update(originTicket, offX, offZ, originDistance/2); -+ test.setSource(offX, offZ, originDistance/2); test.propagateUpdates(); // set and propagate -+ for (int dx = -originDistance; dx <= originDistance; ++dx) { -+ for (int dz = -originDistance; dz <= originDistance; ++dz) { -+ test(dx + offX, dz + offZ, reference, test); -+ } -+ } -+ // test source increase -+ originDistance = 2*originDistance; -+ reference.update(originTicket, offX, offZ, originDistance); -+ test.setSource(offX, offZ, originDistance); test.propagateUpdates(); // set and propagate -+ for (int dx = -4*originDistance; dx <= 4*originDistance; ++dx) { -+ for (int dz = -4*originDistance; dz <= 4*originDistance; ++dz) { -+ test(dx + offX, dz + offZ, reference, test); -+ } -+ } -+ -+ reference.remove(originTicket); -+ test.removeSource(offX, offZ); test.propagateUpdates(); -+ } -+ -+ // test multiple sources at origin -+ { -+ int originDistance = 31; -+ int offX = 54432; -+ int offZ = -134567; -+ java.util.List<Ticket> list = new java.util.ArrayList<>(); -+ for (int i = 0; i < 10; ++i) { -+ Ticket a = new Ticket(); -+ list.add(a); -+ a.x = offX + ((i & 1) == 1 ? -i : i); -+ a.z = offZ + ((i & 1) == 1 ? -i : i); -+ } -+ for (Ticket ticket : list) { -+ reference.add(ticket, ticket.x, ticket.z, originDistance); -+ test.setSource(ticket.x, ticket.z, originDistance); -+ } -+ test.propagateUpdates(); -+ -+ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) { -+ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) { -+ test(dx, dz, reference, test); -+ } -+ } -+ -+ // test ticket level decrease -+ -+ for (Ticket ticket : list) { -+ reference.update(ticket, ticket.x, ticket.z, originDistance/2); -+ test.setSource(ticket.x, ticket.z, originDistance/2); -+ } -+ test.propagateUpdates(); -+ -+ for (int dx = -8*originDistance; dx <= 8*originDistance; ++dx) { -+ for (int dz = -8*originDistance; dz <= 8*originDistance; ++dz) { -+ test(dx, dz, reference, test); -+ } -+ } -+ -+ // test ticket level increase -+ -+ for (Ticket ticket : list) { -+ reference.update(ticket, ticket.x, ticket.z, originDistance*2); -+ test.setSource(ticket.x, ticket.z, originDistance*2); -+ } -+ test.propagateUpdates(); -+ -+ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) { -+ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) { -+ test(dx, dz, reference, test); -+ } -+ } -+ -+ // test ticket remove -+ for (int i = 0, len = list.size(); i < len; ++i) { -+ if ((i & 3) != 0) { -+ continue; -+ } -+ Ticket ticket = list.get(i); -+ reference.remove(ticket); -+ test.removeSource(ticket.x, ticket.z); -+ } -+ test.propagateUpdates(); -+ -+ for (int dx = -16*originDistance; dx <= 16*originDistance; ++dx) { -+ for (int dz = -16*originDistance; dz <= 16*originDistance; ++dz) { -+ test(dx, dz, reference, test); -+ } -+ } -+ } -+ } -+ */ -+ -+ // this map is considered "stale" unless updates are propagated. -+ protected final LevelMap levels = new LevelMap(8192*2, 0.6f); -+ -+ // this map is never stale -+ protected final Long2ByteOpenHashMap sources = new Long2ByteOpenHashMap(4096, 0.6f); -+ -+ // Generally updates to positions are made close to other updates, so we link to decrease cache misses when -+ // propagating updates -+ protected final LongLinkedOpenHashSet updatedSources = new LongLinkedOpenHashSet(); -+ -+ @FunctionalInterface -+ public static interface LevelChangeCallback { -+ -+ /** -+ * This can be called for intermediate updates. So do not rely on newLevel being close to or -+ * the exact level that is expected after a full propagation has occured. -+ */ -+ public void onLevelUpdate(final long coordinate, final byte oldLevel, final byte newLevel); -+ -+ } -+ -+ protected final LevelChangeCallback changeCallback; -+ -+ public Delayed8WayDistancePropagator2D() { -+ this(null); -+ } -+ -+ public Delayed8WayDistancePropagator2D(final LevelChangeCallback changeCallback) { -+ this.changeCallback = changeCallback; -+ } -+ -+ public int getLevel(final long pos) { -+ return this.levels.get(pos); -+ } -+ -+ public int getLevel(final int x, final int z) { -+ return this.levels.get(MCUtil.getCoordinateKey(x, z)); -+ } -+ -+ public void setSource(final int x, final int z, final int level) { -+ this.setSource(MCUtil.getCoordinateKey(x, z), level); -+ } -+ -+ public void setSource(final long coordinate, final int level) { -+ if ((level & 63) != level || level == 0) { -+ throw new IllegalArgumentException("Level must be in (0, 63], not " + level); -+ } -+ -+ final byte byteLevel = (byte)level; -+ final byte oldLevel = this.sources.put(coordinate, byteLevel); -+ -+ if (oldLevel == byteLevel) { -+ return; // nothing to do -+ } -+ -+ // queue to update later -+ this.updatedSources.add(coordinate); -+ } -+ -+ public void removeSource(final int x, final int z) { -+ this.removeSource(MCUtil.getCoordinateKey(x, z)); -+ } -+ -+ public void removeSource(final long coordinate) { -+ if (this.sources.remove(coordinate) != 0) { -+ this.updatedSources.add(coordinate); -+ } -+ } -+ -+ // queues used for BFS propagating levels -+ protected final WorkQueue[] levelIncreaseWorkQueues = new WorkQueue[64]; -+ { -+ for (int i = 0; i < this.levelIncreaseWorkQueues.length; ++i) { -+ this.levelIncreaseWorkQueues[i] = new WorkQueue(); -+ } -+ } -+ protected final WorkQueue[] levelRemoveWorkQueues = new WorkQueue[64]; -+ { -+ for (int i = 0; i < this.levelRemoveWorkQueues.length; ++i) { -+ this.levelRemoveWorkQueues[i] = new WorkQueue(); -+ } -+ } -+ protected long levelIncreaseWorkQueueBitset; -+ protected long levelRemoveWorkQueueBitset; -+ -+ protected final void addToIncreaseWorkQueue(final long coordinate, final byte level) { -+ final WorkQueue queue = this.levelIncreaseWorkQueues[level]; -+ queue.queuedCoordinates.enqueue(coordinate); -+ queue.queuedLevels.enqueue(level); -+ -+ this.levelIncreaseWorkQueueBitset |= (1L << level); -+ } -+ -+ protected final void addToIncreaseWorkQueue(final long coordinate, final byte index, final byte level) { -+ final WorkQueue queue = this.levelIncreaseWorkQueues[index]; -+ queue.queuedCoordinates.enqueue(coordinate); -+ queue.queuedLevels.enqueue(level); -+ -+ this.levelIncreaseWorkQueueBitset |= (1L << index); -+ } -+ -+ protected final void addToRemoveWorkQueue(final long coordinate, final byte level) { -+ final WorkQueue queue = this.levelRemoveWorkQueues[level]; -+ queue.queuedCoordinates.enqueue(coordinate); -+ queue.queuedLevels.enqueue(level); -+ -+ this.levelRemoveWorkQueueBitset |= (1L << level); -+ } -+ -+ public boolean propagateUpdates() { -+ if (this.updatedSources.isEmpty()) { -+ return false; -+ } -+ -+ boolean ret = false; -+ -+ for (final LongIterator iterator = this.updatedSources.iterator(); iterator.hasNext();) { -+ final long coordinate = iterator.nextLong(); -+ -+ final byte currentLevel = this.levels.get(coordinate); -+ final byte updatedSource = this.sources.get(coordinate); -+ -+ if (currentLevel == updatedSource) { -+ continue; -+ } -+ ret = true; -+ -+ if (updatedSource > currentLevel) { -+ // level increase -+ this.addToIncreaseWorkQueue(coordinate, updatedSource); -+ } else { -+ // level decrease -+ this.addToRemoveWorkQueue(coordinate, currentLevel); -+ // if the current coordinate is a source, then the decrease propagation will detect that and queue -+ // the source propagation -+ } -+ } -+ -+ this.updatedSources.clear(); -+ -+ // propagate source level increases first for performance reasons (in crowded areas hopefully the additions -+ // make the removes remove less) -+ this.propagateIncreases(); -+ -+ // now we propagate the decreases (which will then re-propagate clobbered sources) -+ this.propagateDecreases(); -+ -+ return ret; -+ } -+ -+ protected void propagateIncreases() { -+ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset); -+ this.levelIncreaseWorkQueueBitset != 0L; -+ this.levelIncreaseWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelIncreaseWorkQueueBitset)) { -+ -+ final WorkQueue queue = this.levelIncreaseWorkQueues[queueIndex]; -+ while (!queue.queuedLevels.isEmpty()) { -+ final long coordinate = queue.queuedCoordinates.removeFirstLong(); -+ byte level = queue.queuedLevels.removeFirstByte(); -+ -+ final boolean neighbourCheck = level < 0; -+ -+ final byte currentLevel; -+ if (neighbourCheck) { -+ level = (byte)-level; -+ currentLevel = this.levels.get(coordinate); -+ } else { -+ currentLevel = this.levels.putIfGreater(coordinate, level); -+ } -+ -+ if (neighbourCheck) { -+ // used when propagating from decrease to indicate that this level needs to check its neighbours -+ // this means the level at coordinate could be equal, but would still need neighbours checked -+ -+ if (currentLevel != level) { -+ // something caused the level to change, which means something propagated to it (which means -+ // us propagating here is redundant), or something removed the level (which means we -+ // cannot propagate further) -+ continue; -+ } -+ } else if (currentLevel >= level) { -+ // something higher/equal propagated -+ continue; -+ } -+ if (this.changeCallback != null) { -+ this.changeCallback.onLevelUpdate(coordinate, currentLevel, level); -+ } -+ -+ if (level == 1) { -+ // can't propagate 0 to neighbours -+ continue; -+ } -+ -+ // propagate to neighbours -+ final byte neighbourLevel = (byte)(level - 1); -+ final int x = (int)coordinate; -+ final int z = (int)(coordinate >>> 32); -+ -+ for (int dx = -1; dx <= 1; ++dx) { -+ for (int dz = -1; dz <= 1; ++dz) { -+ if ((dx | dz) == 0) { -+ // already propagated to coordinate -+ continue; -+ } -+ -+ // sure we can check the neighbour level in the map right now and avoid a propagation, -+ // but then we would still have to recheck it when popping the value off of the queue! -+ // so just avoid the double lookup -+ final long neighbourCoordinate = MCUtil.getCoordinateKey(x + dx, z + dz); -+ this.addToIncreaseWorkQueue(neighbourCoordinate, neighbourLevel); -+ } -+ } -+ } -+ } -+ } -+ -+ protected void propagateDecreases() { -+ for (int queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset); -+ this.levelRemoveWorkQueueBitset != 0L; -+ this.levelRemoveWorkQueueBitset ^= (1L << queueIndex), queueIndex = 63 ^ Long.numberOfLeadingZeros(this.levelRemoveWorkQueueBitset)) { -+ -+ final WorkQueue queue = this.levelRemoveWorkQueues[queueIndex]; -+ while (!queue.queuedLevels.isEmpty()) { -+ final long coordinate = queue.queuedCoordinates.removeFirstLong(); -+ final byte level = queue.queuedLevels.removeFirstByte(); -+ -+ final byte currentLevel = this.levels.removeIfGreaterOrEqual(coordinate, level); -+ if (currentLevel == 0) { -+ // something else removed -+ continue; -+ } -+ -+ if (currentLevel > level) { -+ // something higher propagated here or we hit the propagation of another source -+ // in the second case we need to re-propagate because we could have just clobbered another source's -+ // propagation -+ this.addToIncreaseWorkQueue(coordinate, currentLevel, (byte)-currentLevel); // indicate to the increase code that the level's neighbours need checking -+ continue; -+ } -+ -+ if (this.changeCallback != null) { -+ this.changeCallback.onLevelUpdate(coordinate, currentLevel, (byte)0); -+ } -+ -+ final byte source = this.sources.get(coordinate); -+ if (source != 0) { -+ // must re-propagate source later -+ this.addToIncreaseWorkQueue(coordinate, source); -+ } -+ -+ if (level == 0) { -+ // can't propagate -1 to neighbours -+ // we have to check neighbours for removing 1 just in case the neighbour is 2 -+ continue; -+ } -+ -+ // propagate to neighbours -+ final byte neighbourLevel = (byte)(level - 1); -+ final int x = (int)coordinate; -+ final int z = (int)(coordinate >>> 32); -+ -+ for (int dx = -1; dx <= 1; ++dx) { -+ for (int dz = -1; dz <= 1; ++dz) { -+ if ((dx | dz) == 0) { -+ // already propagated to coordinate -+ continue; -+ } -+ -+ // sure we can check the neighbour level in the map right now and avoid a propagation, -+ // but then we would still have to recheck it when popping the value off of the queue! -+ // so just avoid the double lookup -+ final long neighbourCoordinate = MCUtil.getCoordinateKey(x + dx, z + dz); -+ this.addToRemoveWorkQueue(neighbourCoordinate, neighbourLevel); -+ } -+ } -+ } -+ } -+ -+ // propagate sources we clobbered in the process -+ this.propagateIncreases(); -+ } -+ -+ protected static final class LevelMap extends Long2ByteOpenHashMap { -+ public LevelMap() { -+ super(); -+ } -+ -+ public LevelMap(final int expected, final float loadFactor) { -+ super(expected, loadFactor); -+ } -+ -+ // copied from superclass -+ private int find(final long k) { -+ if (k == 0L) { -+ return this.containsNullKey ? this.n : -(this.n + 1); -+ } else { -+ final long[] key = this.key; -+ long curr; -+ int pos; -+ if ((curr = key[pos = (int)HashCommon.mix(k) & this.mask]) == 0L) { -+ return -(pos + 1); -+ } else if (k == curr) { -+ return pos; -+ } else { -+ while((curr = key[pos = pos + 1 & this.mask]) != 0L) { -+ if (k == curr) { -+ return pos; -+ } -+ } -+ -+ return -(pos + 1); -+ } -+ } -+ } -+ -+ // copied from superclass -+ private void insert(final int pos, final long k, final byte v) { -+ if (pos == this.n) { -+ this.containsNullKey = true; -+ } -+ -+ this.key[pos] = k; -+ this.value[pos] = v; -+ if (this.size++ >= this.maxFill) { -+ this.rehash(HashCommon.arraySize(this.size + 1, this.f)); -+ } -+ } -+ -+ // copied from superclass -+ public byte putIfGreater(final long key, final byte value) { -+ final int pos = this.find(key); -+ if (pos < 0) { -+ if (this.defRetValue < value) { -+ this.insert(-pos - 1, key, value); -+ } -+ return this.defRetValue; -+ } else { -+ final byte curr = this.value[pos]; -+ if (value > curr) { -+ this.value[pos] = value; -+ return curr; -+ } -+ return curr; -+ } -+ } -+ -+ // copied from superclass -+ private void removeEntry(final int pos) { -+ --this.size; -+ this.shiftKeys(pos); -+ if (this.n > this.minN && this.size < this.maxFill / 4 && this.n > 16) { -+ this.rehash(this.n / 2); -+ } -+ } -+ -+ // copied from superclass -+ private void removeNullEntry() { -+ this.containsNullKey = false; -+ --this.size; -+ if (this.n > this.minN && this.size < this.maxFill / 4 && this.n > 16) { -+ this.rehash(this.n / 2); -+ } -+ } -+ -+ // copied from superclass -+ public byte removeIfGreaterOrEqual(final long key, final byte value) { -+ if (key == 0L) { -+ if (!this.containsNullKey) { -+ return this.defRetValue; -+ } -+ final byte current = this.value[this.n]; -+ if (value >= current) { -+ this.removeNullEntry(); -+ return current; -+ } -+ return current; -+ } else { -+ long[] keys = this.key; -+ byte[] values = this.value; -+ long curr; -+ int pos; -+ if ((curr = keys[pos = (int)HashCommon.mix(key) & this.mask]) == 0L) { -+ return this.defRetValue; -+ } else if (key == curr) { -+ final byte current = values[pos]; -+ if (value >= current) { -+ this.removeEntry(pos); -+ return current; -+ } -+ return current; -+ } else { -+ while((curr = keys[pos = pos + 1 & this.mask]) != 0L) { -+ if (key == curr) { -+ final byte current = values[pos]; -+ if (value >= current) { -+ this.removeEntry(pos); -+ return current; -+ } -+ return current; -+ } -+ } -+ -+ return this.defRetValue; -+ } -+ } -+ } -+ } -+ -+ protected static final class WorkQueue { -+ -+ public final NoResizeLongArrayFIFODeque queuedCoordinates = new NoResizeLongArrayFIFODeque(); -+ public final NoResizeByteArrayFIFODeque queuedLevels = new NoResizeByteArrayFIFODeque(); -+ -+ } -+ -+ protected static final class NoResizeLongArrayFIFODeque extends LongArrayFIFOQueue { -+ -+ /** -+ * Assumes non-empty. If empty, undefined behaviour. -+ */ -+ public long removeFirstLong() { -+ // copied from superclass -+ long t = this.array[this.start]; -+ if (++this.start == this.length) { -+ this.start = 0; -+ } -+ -+ return t; -+ } -+ } -+ -+ protected static final class NoResizeByteArrayFIFODeque extends ByteArrayFIFOQueue { -+ -+ /** -+ * Assumes non-empty. If empty, undefined behaviour. -+ */ -+ public byte removeFirstByte() { -+ // copied from superclass -+ byte t = this.array[this.start]; -+ if (++this.start == this.length) { -+ this.start = 0; -+ } -+ -+ return t; -+ } -+ } -+} diff --git a/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java b/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java new file mode 100644 index 0000000000000000000000000000000000000000..c3ce8a42dddd76b7189ad5685b23f9d9f8ccadb3 @@ -6081,7 +4124,7 @@ index 3e5a85a7ad6149b04622c254fbc2e174896a4128..3f662692ed4846e026a9d48595e7b3b2 + } diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 40adb6117b9e0d5f70103113202a07715e403e2a..b1325e090f2c7aff31d27fc38ca7173efe31ed7c 100644 +index 40adb6117b9e0d5f70103113202a07715e403e2a..9eb987f9d86396d6b7e9d4f3834bea3326640ac7 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -310,6 +310,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa @@ -6119,18 +4162,6 @@ index 40adb6117b9e0d5f70103113202a07715e403e2a..b1325e090f2c7aff31d27fc38ca7173e this.profiler.push("tallying"); long j = Util.getNanos() - i; int k = this.tickCount % 100; -@@ -1470,6 +1475,11 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa - try { - worldserver.timings.doTick.startTiming(); // Spigot - worldserver.tick(shouldKeepTicking); -+ // Paper start -+ for (final io.papermc.paper.chunk.SingleThreadChunkRegionManager regionManager : worldserver.getChunkSource().chunkMap.regionManagers) { -+ regionManager.recalculateRegions(); -+ } -+ // Paper end - worldserver.timings.doTick.stopTiming(); // Spigot - } catch (Throwable throwable) { - CrashReport crashreport = CrashReport.forThrowable(throwable, "Exception ticking world"); diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java index f40a2f348c45a29168ca3d4eef07b5b628060bee..c643bb0daa5cd264fd6ebab7acf0a2bdd7fe7029 100644 --- a/src/main/java/net/minecraft/server/level/ChunkHolder.java @@ -6347,10 +4378,10 @@ index f40a2f348c45a29168ca3d4eef07b5b628060bee..c643bb0daa5cd264fd6ebab7acf0a2bd + // Paper end } diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java -index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b8ef8fe7f 100644 +index 5b920beb39dad8d392b4e5e12a89880720e41942..d3caadb3e884d7d0468daf5eff9abd6629ac4b49 100644 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java +++ b/src/main/java/net/minecraft/server/level/ChunkMap.java -@@ -170,6 +170,62 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -170,6 +170,37 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider }; // CraftBukkit end @@ -6379,31 +4410,6 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b + } + // Paper end + // Paper start -+ public final List<io.papermc.paper.chunk.SingleThreadChunkRegionManager> regionManagers = new java.util.ArrayList<>(); -+ public final io.papermc.paper.chunk.SingleThreadChunkRegionManager dataRegionManager; -+ -+ public static final class DataRegionData implements io.papermc.paper.chunk.SingleThreadChunkRegionManager.RegionData { -+ } -+ -+ public static final class DataRegionSectionData implements io.papermc.paper.chunk.SingleThreadChunkRegionManager.RegionSectionData { -+ -+ @Override -+ public void removeFromRegion(final io.papermc.paper.chunk.SingleThreadChunkRegionManager.RegionSection section, -+ final io.papermc.paper.chunk.SingleThreadChunkRegionManager.Region from) { -+ final DataRegionSectionData sectionData = (DataRegionSectionData)section.sectionData; -+ final DataRegionData fromData = (DataRegionData)from.regionData; -+ } -+ -+ @Override -+ public void addToRegion(final io.papermc.paper.chunk.SingleThreadChunkRegionManager.RegionSection section, -+ final io.papermc.paper.chunk.SingleThreadChunkRegionManager.Region oldRegion, -+ final io.papermc.paper.chunk.SingleThreadChunkRegionManager.Region newRegion) { -+ final DataRegionSectionData sectionData = (DataRegionSectionData)section.sectionData; -+ final DataRegionData oldRegionData = oldRegion == null ? null : (DataRegionData)oldRegion.regionData; -+ final DataRegionData newRegionData = (DataRegionData)newRegion.regionData; -+ } -+ } -+ + public final ChunkHolder getUnloadingChunkHolder(int chunkX, int chunkZ) { + return this.pendingUnloads.get(io.papermc.paper.util.CoordinateUtils.getChunkKey(chunkX, chunkZ)); + } @@ -6413,13 +4419,11 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop<Runnable> mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory, int viewDistance, boolean dsync) { super(new RegionStorageInfo(session.getLevelId(), world.dimension(), "chunk"), session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync); this.visibleChunkMap = this.updatingChunkMap.clone(); -@@ -221,8 +277,24 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -221,8 +252,22 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.poiManager = new PoiManager(new RegionStorageInfo(session.getLevelId(), world.dimension(), "poi"), path.resolve("poi"), dataFixer, dsync, iregistrycustom, world.getServer(), world); this.setServerViewDistance(viewDistance); this.worldGenContext = new WorldGenContext(world, chunkGenerator, structureTemplateManager, this.lightEngine, this.mainThreadMailbox); + // Paper start -+ this.dataRegionManager = new io.papermc.paper.chunk.SingleThreadChunkRegionManager(this.level, 2, (1.0 / 3.0), 1, 6, "Data", DataRegionData::new, DataRegionSectionData::new); -+ this.regionManagers.add(this.dataRegionManager); + this.nearbyPlayers = new io.papermc.paper.util.player.NearbyPlayers(this.level); + // Paper end + } @@ -6438,7 +4442,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b protected ChunkGenerator generator() { return this.worldGenContext.generator(); } -@@ -378,9 +450,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -378,9 +423,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider }; stringbuilder.append("Updating:").append(System.lineSeparator()); @@ -6450,7 +4454,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b CrashReport crashreport = CrashReport.forThrowable(exception, "Chunk loading"); CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Chunk loading"); -@@ -422,8 +494,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -422,8 +467,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider holder.setTicketLevel(level); } else { holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this.queueSorter, this); @@ -6465,7 +4469,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b this.updatingChunkMap.put(pos, holder); this.modified = true; } -@@ -445,7 +523,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -445,7 +496,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider protected void saveAllChunks(boolean flush) { if (flush) { @@ -6474,7 +4478,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b MutableBoolean mutableboolean = new MutableBoolean(); do { -@@ -468,7 +546,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -468,7 +519,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider }); this.flushWorker(); } else { @@ -6483,7 +4487,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b } } -@@ -487,7 +565,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -487,7 +538,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } public boolean hasWork() { @@ -6492,7 +4496,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b } private void processUnloads(BooleanSupplier shouldKeepTicking) { -@@ -504,6 +582,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -504,6 +555,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } this.updatingChunkMap.remove(j); @@ -6500,7 +4504,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b this.pendingUnloads.put(j, playerchunk); this.modified = true; ++i; -@@ -523,7 +602,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -523,7 +575,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } int l = 0; @@ -6509,7 +4513,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b while (l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) { if (this.saveChunkIfNeeded((ChunkHolder) objectiterator.next())) { -@@ -541,7 +620,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -541,7 +593,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } else { ChunkAccess ichunkaccess = holder.getLatestChunk(); @@ -6522,7 +4526,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b LevelChunk chunk; if (ichunkaccess instanceof LevelChunk) { -@@ -559,7 +642,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -559,7 +615,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.lightEngine.tryScheduleUpdate(); this.progressListener.onStatusChange(ichunkaccess.getPos(), (ChunkStatus) null); this.chunkSaveCooldowns.remove(ichunkaccess.getPos().toLong()); @@ -6533,7 +4537,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b } }; -@@ -896,7 +981,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -896,7 +954,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } } @@ -6542,7 +4546,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b int j = Mth.clamp(watchDistance, 2, 32); if (j != this.serverViewDistance) { -@@ -913,7 +998,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -913,7 +971,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } @@ -6551,7 +4555,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b return Mth.clamp(player.requestedViewDistance(), 2, this.serverViewDistance); } -@@ -942,7 +1027,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -942,7 +1000,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } public int size() { @@ -6560,7 +4564,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b } public DistanceManager getDistanceManager() { -@@ -950,19 +1035,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -950,19 +1008,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } protected Iterable<ChunkHolder> getChunks() { @@ -6585,7 +4589,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b Optional<ChunkAccess> optional = Optional.ofNullable(playerchunk.getLatestChunk()); Optional<LevelChunk> optional1 = optional.flatMap((ichunkaccess) -> { return ichunkaccess instanceof LevelChunk ? Optional.of((LevelChunk) ichunkaccess) : Optional.empty(); -@@ -1083,6 +1168,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -1083,6 +1141,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider player.setChunkTrackingView(ChunkTrackingView.EMPTY); this.updateChunkTracking(player); @@ -6593,7 +4597,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b } else { SectionPos sectionposition = player.getLastSectionPos(); -@@ -1091,6 +1177,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -1091,6 +1150,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.distanceManager.removePlayer(sectionposition, player); } @@ -6601,7 +4605,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b this.applyChunkTrackingView(player, ChunkTrackingView.EMPTY); } -@@ -1142,6 +1229,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -1142,6 +1202,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.updateChunkTracking(player); } @@ -6609,7 +4613,7 @@ index 5b920beb39dad8d392b4e5e12a89880720e41942..319f51eb8adde7584c74780ac0539f4b } private void updateChunkTracking(ServerPlayer player) { -@@ -1385,10 +1473,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -1385,10 +1446,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider }); } @@ -6655,7 +4659,7 @@ index b6cc33943fe7e4667944f3e6f868b3033ea9ca18..27065ffc5473c518acee3a3096b83fac while (objectiterator.hasNext()) { diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -index d39268911ed7c4d60ee6a82178be23245aae58c4..ab57071cc6ce8b79d883f2426855c1abf577e90d 100644 +index d39268911ed7c4d60ee6a82178be23245aae58c4..e9f53f57c363a32106880ea9aad0ccf5a7342509 100644 --- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java +++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java @@ -48,6 +48,7 @@ import net.minecraft.world.level.storage.LevelStorageSource; @@ -6666,81 +4670,30 @@ index d39268911ed7c4d60ee6a82178be23245aae58c4..ab57071cc6ce8b79d883f2426855c1ab private static final List<ChunkStatus> CHUNK_STATUSES = ChunkStatus.getStatusList(); private final DistanceManager distanceManager; final ServerLevel level; -@@ -66,6 +67,14 @@ public class ServerChunkCache extends ChunkSource { +@@ -66,6 +67,12 @@ public class ServerChunkCache extends ChunkSource { @Nullable @VisibleForDebug private NaturalSpawner.SpawnState lastSpawnState; + // Paper start + public final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<LevelChunk> tickingChunks = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true); + public final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<LevelChunk> entityTickingChunks = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true); -+ final com.destroystokyo.paper.util.concurrent.WeakSeqLock loadedChunkMapSeqLock = new com.destroystokyo.paper.util.concurrent.WeakSeqLock(); -+ final it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<LevelChunk> loadedChunkMap = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>(8192, 0.5f); ++ private final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<net.minecraft.world.level.chunk.LevelChunk> fullChunks = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>(); + long chunkFutureAwaitCounter; -+ private final LevelChunk[] lastLoadedChunks = new LevelChunk[4 * 4]; + // Paper end public ServerChunkCache(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, int simulationDistance, boolean dsync, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory) { this.level = world; -@@ -91,6 +100,124 @@ public class ServerChunkCache extends ChunkSource { +@@ -91,6 +98,54 @@ public class ServerChunkCache extends ChunkSource { return chunk.getFullChunkNow() != null; } // CraftBukkit end + // Paper start -+ private static int getChunkCacheKey(int x, int z) { -+ return x & 3 | ((z & 3) << 2); -+ } -+ + public void addLoadedChunk(LevelChunk chunk) { -+ this.loadedChunkMapSeqLock.acquireWrite(); -+ try { -+ this.loadedChunkMap.put(chunk.coordinateKey, chunk); -+ } finally { -+ this.loadedChunkMapSeqLock.releaseWrite(); -+ } -+ -+ // rewrite cache if we have to -+ // we do this since we also cache null chunks -+ int cacheKey = getChunkCacheKey(chunk.locX, chunk.locZ); -+ -+ this.lastLoadedChunks[cacheKey] = chunk; ++ this.fullChunks.put(chunk.coordinateKey, chunk); + } + + public void removeLoadedChunk(LevelChunk chunk) { -+ this.loadedChunkMapSeqLock.acquireWrite(); -+ try { -+ this.loadedChunkMap.remove(chunk.coordinateKey); -+ } finally { -+ this.loadedChunkMapSeqLock.releaseWrite(); -+ } -+ -+ // rewrite cache if we have to -+ // we do this since we also cache null chunks -+ int cacheKey = getChunkCacheKey(chunk.locX, chunk.locZ); -+ -+ LevelChunk cachedChunk = this.lastLoadedChunks[cacheKey]; -+ if (cachedChunk != null && cachedChunk.coordinateKey == chunk.coordinateKey) { -+ this.lastLoadedChunks[cacheKey] = null; -+ } -+ } -+ -+ public final LevelChunk getChunkAtIfLoadedMainThread(int x, int z) { -+ int cacheKey = getChunkCacheKey(x, z); -+ -+ LevelChunk cachedChunk = this.lastLoadedChunks[cacheKey]; -+ if (cachedChunk != null && cachedChunk.locX == x & cachedChunk.locZ == z) { -+ return cachedChunk; -+ } -+ -+ long chunkKey = ChunkPos.asLong(x, z); -+ -+ cachedChunk = this.loadedChunkMap.get(chunkKey); -+ // Skipping a null check to avoid extra instructions to improve inline capability -+ this.lastLoadedChunks[cacheKey] = cachedChunk; -+ return cachedChunk; -+ } -+ -+ public final LevelChunk getChunkAtIfLoadedMainThreadNoCache(int x, int z) { -+ return this.loadedChunkMap.get(ChunkPos.asLong(x, z)); ++ this.fullChunks.remove(chunk.coordinateKey); + } + + @Nullable @@ -6779,34 +4732,13 @@ index d39268911ed7c4d60ee6a82178be23245aae58c4..ab57071cc6ce8b79d883f2426855c1ab + + @Nullable + public LevelChunk getChunkAtIfLoadedImmediately(int x, int z) { -+ long k = ChunkPos.asLong(x, z); -+ -+ if (Thread.currentThread() == this.mainThread) { -+ return this.getChunkAtIfLoadedMainThread(x, z); -+ } -+ -+ LevelChunk ret = null; -+ long readlock; -+ do { -+ readlock = this.loadedChunkMapSeqLock.acquireRead(); -+ try { -+ ret = this.loadedChunkMap.get(k); -+ } catch (Throwable thr) { -+ if (thr instanceof ThreadDeath) { -+ throw (ThreadDeath)thr; -+ } -+ // re-try, this means a CME occurred... -+ continue; -+ } -+ } while (!this.loadedChunkMapSeqLock.tryReleaseRead(readlock)); -+ -+ return ret; ++ return this.fullChunks.get(ChunkPos.asLong(x, z)); + } + // Paper end @Override public ThreadedLevelLightEngine getLightEngine() { -@@ -286,7 +413,7 @@ public class ServerChunkCache extends ChunkSource { +@@ -286,7 +341,7 @@ public class ServerChunkCache extends ChunkSource { return this.mainThreadProcessor.pollTask(); } @@ -6815,7 +4747,7 @@ index d39268911ed7c4d60ee6a82178be23245aae58c4..ab57071cc6ce8b79d883f2426855c1ab boolean flag = this.distanceManager.runAllUpdates(this.chunkMap); boolean flag1 = this.chunkMap.promoteChunkMap(); -@@ -299,6 +426,12 @@ public class ServerChunkCache extends ChunkSource { +@@ -299,6 +354,12 @@ public class ServerChunkCache extends ChunkSource { } } @@ -7466,120 +5398,21 @@ index a52077f0d93c94b0ea644bc14b9b28e84fd1b154..dcc0acd259920463a4464213b9a5e793 @Nullable @Override diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -index b537e7a079497db428db405edfccde74f32f4208..7898e1aaf82f096fa74bd3f5859f0f4303ea677f 100644 +index b537e7a079497db428db405edfccde74f32f4208..c664021dbfffcf0db3247041270ce9a1ee6940de 100644 --- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -@@ -116,6 +116,109 @@ public class LevelChunk extends ChunkAccess { +@@ -116,6 +116,10 @@ public class LevelChunk extends ChunkAccess { public boolean needsDecoration; // CraftBukkit end + // Paper start -+ public @Nullable net.minecraft.server.level.ChunkHolder playerChunk; -+ -+ static final int NEIGHBOUR_CACHE_RADIUS = 3; -+ public static int getNeighbourCacheRadius() { -+ return NEIGHBOUR_CACHE_RADIUS; -+ } -+ + boolean loadedTicketLevel; -+ private long neighbourChunksLoadedBitset; -+ private final LevelChunk[] loadedNeighbourChunks = new LevelChunk[(NEIGHBOUR_CACHE_RADIUS * 2 + 1) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)]; -+ -+ private static int getNeighbourIndex(final int relativeX, final int relativeZ) { -+ // index = (relativeX + NEIGHBOUR_CACHE_RADIUS) + (relativeZ + NEIGHBOUR_CACHE_RADIUS) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1) -+ // optimised variant of the above by moving some of the ops to compile time -+ return relativeX + (relativeZ * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)) + (NEIGHBOUR_CACHE_RADIUS + NEIGHBOUR_CACHE_RADIUS * ((NEIGHBOUR_CACHE_RADIUS * 2 + 1))); -+ } -+ -+ public final LevelChunk getRelativeNeighbourIfLoaded(final int relativeX, final int relativeZ) { -+ return this.loadedNeighbourChunks[getNeighbourIndex(relativeX, relativeZ)]; -+ } -+ -+ public final boolean isNeighbourLoaded(final int relativeX, final int relativeZ) { -+ return (this.neighbourChunksLoadedBitset & (1L << getNeighbourIndex(relativeX, relativeZ))) != 0; -+ } -+ -+ public final void setNeighbourLoaded(final int relativeX, final int relativeZ, final LevelChunk chunk) { -+ if (chunk == null) { -+ throw new IllegalArgumentException("Chunk must be non-null, neighbour: (" + relativeX + "," + relativeZ + "), chunk: " + this.chunkPos); -+ } -+ final long before = this.neighbourChunksLoadedBitset; -+ final int index = getNeighbourIndex(relativeX, relativeZ); -+ this.loadedNeighbourChunks[index] = chunk; -+ this.neighbourChunksLoadedBitset |= (1L << index); -+ this.onNeighbourChange(before, this.neighbourChunksLoadedBitset); -+ } -+ -+ public final void setNeighbourUnloaded(final int relativeX, final int relativeZ) { -+ final long before = this.neighbourChunksLoadedBitset; -+ final int index = getNeighbourIndex(relativeX, relativeZ); -+ this.loadedNeighbourChunks[index] = null; -+ this.neighbourChunksLoadedBitset &= ~(1L << index); -+ this.onNeighbourChange(before, this.neighbourChunksLoadedBitset); -+ } -+ -+ public final void resetNeighbours() { -+ final long before = this.neighbourChunksLoadedBitset; -+ this.neighbourChunksLoadedBitset = 0L; -+ java.util.Arrays.fill(this.loadedNeighbourChunks, null); -+ this.onNeighbourChange(before, 0L); -+ } -+ -+ protected void onNeighbourChange(final long bitsetBefore, final long bitsetAfter) { -+ -+ } -+ -+ public final boolean isAnyNeighborsLoaded() { -+ return neighbourChunksLoadedBitset != 0; -+ } -+ public final boolean areNeighboursLoaded(final int radius) { -+ return LevelChunk.areNeighboursLoaded(this.neighbourChunksLoadedBitset, radius); -+ } -+ -+ public static boolean areNeighboursLoaded(final long bitset, final int radius) { -+ // index = relativeX + (relativeZ * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)) + (NEIGHBOUR_CACHE_RADIUS + NEIGHBOUR_CACHE_RADIUS * ((NEIGHBOUR_CACHE_RADIUS * 2 + 1))) -+ switch (radius) { -+ case 0: { -+ return (bitset & (1L << getNeighbourIndex(0, 0))) != 0; -+ } -+ case 1: { -+ long mask = 0L; -+ for (int dx = -1; dx <= 1; ++dx) { -+ for (int dz = -1; dz <= 1; ++dz) { -+ mask |= (1L << getNeighbourIndex(dx, dz)); -+ } -+ } -+ return (bitset & mask) == mask; -+ } -+ case 2: { -+ long mask = 0L; -+ for (int dx = -2; dx <= 2; ++dx) { -+ for (int dz = -2; dz <= 2; ++dz) { -+ mask |= (1L << getNeighbourIndex(dx, dz)); -+ } -+ } -+ return (bitset & mask) == mask; -+ } -+ case 3: { -+ long mask = 0L; -+ for (int dx = -3; dx <= 3; ++dx) { -+ for (int dz = -3; dz <= 3; ++dz) { -+ mask |= (1L << getNeighbourIndex(dx, dz)); -+ } -+ } -+ return (bitset & mask) == mask; -+ } -+ -+ default: -+ throw new IllegalArgumentException("Radius not recognized: " + radius); -+ } -+ } + // Paper end + public LevelChunk(ServerLevel world, ProtoChunk protoChunk, @Nullable LevelChunk.PostLoadProcessor entityLoader) { this(world, protoChunk.getPos(), protoChunk.getUpgradeData(), protoChunk.unpackBlockTicks(), protoChunk.unpackFluidTicks(), protoChunk.getInhabitedTime(), protoChunk.getSections(), entityLoader, protoChunk.getBlendingData()); Iterator iterator = protoChunk.getBlockEntities().values().iterator(); -@@ -181,8 +284,25 @@ public class LevelChunk extends ChunkAccess { +@@ -181,8 +185,25 @@ public class LevelChunk extends ChunkAccess { } } @@ -7605,7 +5438,7 @@ index b537e7a079497db428db405edfccde74f32f4208..7898e1aaf82f096fa74bd3f5859f0f43 int i = pos.getX(); int j = pos.getY(); int k = pos.getZ(); -@@ -224,6 +344,18 @@ public class LevelChunk extends ChunkAccess { +@@ -224,6 +245,18 @@ public class LevelChunk extends ChunkAccess { } } @@ -7624,51 +5457,25 @@ index b537e7a079497db428db405edfccde74f32f4208..7898e1aaf82f096fa74bd3f5859f0f43 @Override public FluidState getFluidState(BlockPos pos) { return this.getFluidState(pos.getX(), pos.getY(), pos.getZ()); -@@ -549,7 +681,25 @@ public class LevelChunk extends ChunkAccess { +@@ -549,7 +582,11 @@ public class LevelChunk extends ChunkAccess { // CraftBukkit start public void loadCallback() { -+ // Paper start - neighbour cache -+ int chunkX = this.chunkPos.x; -+ int chunkZ = this.chunkPos.z; -+ net.minecraft.server.level.ServerChunkCache chunkProvider = this.level.getChunkSource(); -+ for (int dx = -NEIGHBOUR_CACHE_RADIUS; dx <= NEIGHBOUR_CACHE_RADIUS; ++dx) { -+ for (int dz = -NEIGHBOUR_CACHE_RADIUS; dz <= NEIGHBOUR_CACHE_RADIUS; ++dz) { -+ LevelChunk neighbour = chunkProvider.getChunkAtIfLoadedMainThreadNoCache(chunkX + dx, chunkZ + dz); -+ if (neighbour != null) { -+ neighbour.setNeighbourLoaded(-dx, -dz, this); -+ // should be in cached already -+ this.setNeighbourLoaded(dx, dz, neighbour); -+ } -+ } -+ } -+ this.setNeighbourLoaded(0, 0, this); ++ // Paper start + this.loadedTicketLevel = true; -+ // Paper end - neighbour cache ++ // Paper end org.bukkit.Server server = this.level.getCraftServer(); + this.level.getChunkSource().addLoadedChunk(this); // Paper if (server != null) { /* * If it's a new world, the first few chunks are generated inside -@@ -590,6 +740,22 @@ public class LevelChunk extends ChunkAccess { +@@ -590,6 +627,10 @@ public class LevelChunk extends ChunkAccess { server.getPluginManager().callEvent(unloadEvent); // note: saving can be prevented, but not forced if no saving is actually required this.mustNotSave = !unloadEvent.isSaveChunk(); + this.level.getChunkSource().removeLoadedChunk(this); // Paper -+ // Paper start - neighbour cache -+ int chunkX = this.chunkPos.x; -+ int chunkZ = this.chunkPos.z; -+ net.minecraft.server.level.ServerChunkCache chunkProvider = this.level.getChunkSource(); -+ for (int dx = -NEIGHBOUR_CACHE_RADIUS; dx <= NEIGHBOUR_CACHE_RADIUS; ++dx) { -+ for (int dz = -NEIGHBOUR_CACHE_RADIUS; dz <= NEIGHBOUR_CACHE_RADIUS; ++dz) { -+ LevelChunk neighbour = chunkProvider.getChunkAtIfLoadedMainThreadNoCache(chunkX + dx, chunkZ + dz); -+ if (neighbour != null) { -+ neighbour.setNeighbourUnloaded(-dx, -dz); -+ } -+ } -+ } ++ // Paper start + this.loadedTicketLevel = false; -+ this.resetNeighbours(); + // Paper end } @@ -8150,7 +5957,7 @@ index e08d4a45e313ef1b9005ef00ee0185a188171207..2fc68d129e2fdfd51e310ea5bdfb8332 public static byte toLegacyData(BlockState data) { diff --git a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java -index 5fd6eb754c4edebed6798c65b06507a4e89ca48f..0794d92c42b0db6b367505ae28f09f1fd39fa312 100644 +index 5fd6eb754c4edebed6798c65b06507a4e89ca48f..524b51a0ab808a0629c871ad813115abd4b49dbd 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java +++ b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java @@ -58,6 +58,7 @@ import net.minecraft.world.phys.shapes.VoxelShape; @@ -8170,19 +5977,19 @@ index 5fd6eb754c4edebed6798c65b06507a4e89ca48f..0794d92c42b0db6b367505ae28f09f1f + @Nullable + @Override + public BlockState getBlockStateIfLoaded(final BlockPos blockposition) { -+ return null; ++ return this.handle.getBlockStateIfLoaded(blockposition); + } + + @Nullable + @Override + public FluidState getFluidIfLoaded(final BlockPos blockposition) { -+ return null; ++ return this.handle.getFluidIfLoaded(blockposition); + } + + @Nullable + @Override + public ChunkAccess getChunkIfLoadedImmediately(final int x, final int z) { -+ return null; ++ return this.handle.getChunkIfLoadedImmediately(x, z); + } + // Paper end } |