diff --git a/game/client/src/main/java/net/minecraft/client/Minecraft.java b/game/client/src/main/java/net/minecraft/client/Minecraft.java index a3f739aa7..8b5ec6ebb 100644 --- a/game/client/src/main/java/net/minecraft/client/Minecraft.java +++ b/game/client/src/main/java/net/minecraft/client/Minecraft.java @@ -2145,8 +2145,15 @@ public class Minecraft if (this.currentWorld != null) { this.currentWorld.saveWorldIndirectly(this.loadingScreen); ChunkLoader loader = this.currentWorld.getLevelStorage().getChunkLoader(this.currentWorld.dimension); - if (loader != null) loader.awaitSaving(); - + if (loader != null) { + while (loader.isSaving()) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } this.currentWorld.getChunkProvider().unloadAllChunks(); this.currentWorld.onUnload(); this.currentWorld.close(); diff --git a/game/core/src/main/java/net/minecraft/core/util/collection/Lockfree.java b/game/core/src/main/java/net/minecraft/core/util/collection/Lockfree.java deleted file mode 100644 index 30aa664c7..000000000 --- a/game/core/src/main/java/net/minecraft/core/util/collection/Lockfree.java +++ /dev/null @@ -1,135 +0,0 @@ -package net.minecraft.core.util.collection; - -import java.lang.invoke.MethodHandles; -import java.lang.invoke.VarHandle; -import java.util.Arrays; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public final class Lockfree { - private static final VarHandle _LONG_ARRAY = MethodHandles.arrayElementVarHandle(long[].class); - private static final VarHandle _OBJECT_ARRAY = MethodHandles.arrayElementVarHandle(Object[].class); - private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); - - private static @NotNull VarHandle fieldHandle( - final @NotNull Class container, - final @NotNull String name, - final @NotNull Class type - ) { - try { - return LOOKUP.findVarHandle(container, name, type); - } catch (ReflectiveOperationException e) { - throw new ExceptionInInitializerError(e); - } - } - - private static long atomicPopLowestSetBit(final long[] arr, final int i) { - for (long loc = (long) _LONG_ARRAY.getAcquire(arr, i), loc2;; loc = loc2) { - if (loc == 0L) return 0L; - - final long bit = loc & -loc; - assert bit != 0; - - loc2 = (long) _LONG_ARRAY.getAndBitwiseAnd(arr, i, ~bit); - if ((loc2 & bit) != 0) return bit; - } - } - - private static int compareAndIncr(final VarHandle field, final Object container, final int expected) { - final int loc = (int) field.compareAndExchange(container, expected, expected + 1); - return (loc == expected) ? expected + 1 : expected; - } - - public static final class BitsetMappedBuffer { - private final int len64, len64Mask; - private final long[] rMask, wMask; - private final Object[] buffer; - - private @Deprecated @SuppressWarnings("unused") int boundLog2 = 0; - - private @Deprecated @SuppressWarnings("unused") int rHint, wHint; - private @Deprecated @SuppressWarnings("unused") int size; - - private static final VarHandle _R_HINT = fieldHandle(BitsetMappedBuffer.class, "rHint", int.class); - private static final VarHandle _W_HINT = fieldHandle(BitsetMappedBuffer.class, "wHint", int.class); - private static final VarHandle _SIZE = fieldHandle(BitsetMappedBuffer.class, "size", int.class); - - public BitsetMappedBuffer(final int capacity) { - assert capacity >= 0; - - this.len64 = Math.max(1, Integer.highestOneBit(capacity-1) >>> 5); - this.len64Mask = this.len64 - 1; - - this.buffer = new Object[this.len64 << 6]; - this.rMask = new long[this.len64]; - this.wMask = new long[this.len64]; - - Arrays.fill(this.wMask, ~0L); - } - - public int size() { - return (int) _SIZE.getAcquire(this); - } - - public void clear() { - for (int i = 0; i < this.len64; i++) _LONG_ARRAY.setRelease(this.rMask, i, 0L); - for (int i = 0; i < this.len64; i++) _LONG_ARRAY.setRelease(this.wMask, i, ~0L); - _R_HINT.setRelease(this, 0); - _W_HINT.setRelease(this, 0); - } - - public void clearRelease() { - for (int i = 0; i < this.len64; i++) _LONG_ARRAY.setRelease(this.rMask, i, 0L); - for (int i = 0; i < this.len64; i++) _LONG_ARRAY.setRelease(this.wMask, i, 0L); - Arrays.fill(this.buffer, null); - for (int i = 0; i < this.len64; i++) _LONG_ARRAY.setRelease(this.wMask, i, ~0L); - _R_HINT.setRelease(this, 0); - _W_HINT.setRelease(this, 0); - } - - public @Nullable T poll() { - for (int i = 0, hint = (int) _R_HINT.getAcquire(this); i <= this.len64; i++, hint = compareAndIncr(_R_HINT, this, hint)) { - final int j = hint & this.len64Mask; - final long rsv = atomicPopLowestSetBit(this.rMask, j); - if (rsv == 0L) continue; - - - final var elem = (T) _OBJECT_ARRAY.getAcquire(this.buffer, j*64 + Long.numberOfTrailingZeros(rsv)); - assert elem != null; - - _LONG_ARRAY.getAndBitwiseOr(this.wMask, j, rsv); - _SIZE.getAndAddRelease(this, -1); - - return elem; - } - return null; - } - - public boolean push(final @NotNull T elem) { - assert elem != null; - - for (int i = 0, hint = (int) _W_HINT.getAcquire(this); i <= this.len64; i++, hint = compareAndIncr(_W_HINT, this, hint)) { - final int j = hint & this.len64Mask; - final long rsv = atomicPopLowestSetBit(this.wMask, j); - if (rsv == 0L) continue; - - _OBJECT_ARRAY.setRelease(this.buffer, j*64 + Long.numberOfTrailingZeros(rsv), elem); - - _LONG_ARRAY.getAndBitwiseOr(this.rMask, j, rsv); - _SIZE.getAndAddRelease(this, 1); - - return true; - } - return false; - } - - public void copy(final @NotNull T[] arr) { - for (int i = 0; i < arr.length && i < this.len64*64; i++) { - final var elem = (T) _OBJECT_ARRAY.getAcquire(this.buffer, i); - if (elem == null) break; - - } - } - } -} diff --git a/game/core/src/main/java/net/minecraft/core/world/chunk/ChunkLoader.java b/game/core/src/main/java/net/minecraft/core/world/chunk/ChunkLoader.java index 62e4b3149..187b281b6 100644 --- a/game/core/src/main/java/net/minecraft/core/world/chunk/ChunkLoader.java +++ b/game/core/src/main/java/net/minecraft/core/world/chunk/ChunkLoader.java @@ -3,7 +3,6 @@ package net.minecraft.core.world.chunk; import net.minecraft.core.world.World; import java.io.IOException; -import java.util.concurrent.locks.LockSupport; public interface ChunkLoader { @@ -41,8 +40,4 @@ public interface ChunkLoader * @return true if busy saving */ boolean isSaving(); - - default void awaitSaving() { - while (this.isSaving()) LockSupport.parkNanos(1000_000); - } } diff --git a/game/core/src/main/java/net/minecraft/core/world/chunk/ChunkLoaderRegionAsync.java b/game/core/src/main/java/net/minecraft/core/world/chunk/ChunkLoaderRegionAsync.java index dfc29cef3..df0140e39 100644 --- a/game/core/src/main/java/net/minecraft/core/world/chunk/ChunkLoaderRegionAsync.java +++ b/game/core/src/main/java/net/minecraft/core/world/chunk/ChunkLoaderRegionAsync.java @@ -1,29 +1,33 @@ package net.minecraft.core.world.chunk; import com.mojang.logging.LogUtils; - -import net.minecraft.core.util.collection.Lockfree; import net.minecraft.core.world.World; - -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import java.io.File; import java.io.IOException; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.locks.LockSupport; +import java.util.LinkedList; +import java.util.Queue; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; public class ChunkLoaderRegionAsync implements ChunkLoader { private static final Logger LOGGER = LogUtils.getLogger(); - - private static final ChunkSaveThread thread; - static { - thread = new ChunkSaveThread(); - thread.start(); + private static final Queue chunksToSave = new LinkedList<>(); + private static final Queue worldsToSave = new LinkedList<>(); + private static final Queue chunkLoaders = new LinkedList<>(); + private static final Lock lockChunksToSave = new ReentrantLock(); + private static final Condition notEmpty = lockChunksToSave.newCondition(); + private static final ChunkSaveThread chunkSaveThread; + + static + { + chunkSaveThread = new ChunkSaveThread(); + chunkSaveThread.setName("Chunk Save Thread"); + chunkSaveThread.start(); System.out.println("Spawned new chunk save thread!"); } @@ -44,65 +48,78 @@ public class ChunkLoaderRegionAsync public void saveChunk(World world, Chunk chunk) throws IOException { - thread.push(this.chunkLoaderRegion, world, chunk); + lockChunksToSave.lock(); + try + { + if (!chunksToSave.contains(chunk)) { + chunksToSave.add(chunk); + worldsToSave.add(world); + chunkLoaders.add(chunkLoaderRegion); + notEmpty.signal(); + } + } + finally + { + lockChunksToSave.unlock(); + } } @Override public boolean chunkExists(World world, int x, int z) { + //chunks waiting in the save queue exist + lockChunksToSave.lock(); + try + { + java.util.Iterator chunkIterator = chunksToSave.iterator(); + java.util.Iterator worldIterator = worldsToSave.iterator(); + while (chunkIterator.hasNext() && worldIterator.hasNext()) + { + Chunk chunk = chunkIterator.next(); + World chunkWorld = worldIterator.next(); + if (chunkWorld == world && chunk.pos.x == x && chunk.pos.z == z) + { + return true; + } + } + } + finally + { + lockChunksToSave.unlock(); + } return chunkLoaderRegion.chunkExists(world, x, z); } @Override public boolean isSaving() { - return !thread.isJobless(); + return !chunksToSave.isEmpty(); } private static class ChunkSaveThread extends Thread { - private final AtomicBoolean isAsleep = new AtomicBoolean(false); - - private final Set chunksSet = ConcurrentHashMap.newKeySet(); - private final Lockfree.BitsetMappedBuffer entries = new Lockfree.BitsetMappedBuffer<>(64); - - private final record Entry(@NotNull ChunkLoaderRegion loader, @NotNull World world, @NotNull Chunk chunk) { - public void saveChunk() throws IOException { - this.loader.saveChunk(this.world, this.chunk); - } - } - - public ChunkSaveThread() { - this.setName("Chunk Save Thread"); - this.setDaemon(true); - } - - public void push( - final @NotNull ChunkLoaderRegion loader, - final @NotNull World world, final @NotNull Chunk chunk - ) { - if (chunksSet.add(chunk)) { - while (!entries.push(new Entry(loader, world, chunk))); - if (this.isAsleep.getAcquire()) LockSupport.unpark(this); - } - } - - public boolean isJobless() { - return this.chunksSet.isEmpty(); - } + public volatile boolean shouldStop = false; @Override public void run() { - for (;;) { - Entry entry; - this.isAsleep.setRelease(true); - while ((entry = entries.poll()) == null) LockSupport.park(); - this.isAsleep.setRelease(false); - try { entry.saveChunk(); } - catch (Throwable e) { + while (!shouldStop) + { + // Save exactly one chunk from queue + lockChunksToSave.lock(); + try { + if (!chunksToSave.isEmpty()) { + Chunk chunk = chunksToSave.remove(); + World world = worldsToSave.remove(); + ChunkLoaderRegion chunkLoaderRegion = chunkLoaders.remove(); + chunkLoaderRegion.saveChunk(world, chunk); + } else { + notEmpty.await(); + } + } catch (IOException | InterruptedException e) { LOGGER.error("Exception while attempting to save chunks in async loader", e); + } finally { + lockChunksToSave.unlock(); } - chunksSet.remove(entry.chunk); } } }