diff --git a/game/client/src/main/java/net/betterthanadventure/sound/ChannelLWJGL3OpenAL.java b/game/client/src/main/java/net/betterthanadventure/sound/ChannelLWJGL3OpenAL.java index 3e2816412..95ff68c8a 100644 --- a/game/client/src/main/java/net/betterthanadventure/sound/ChannelLWJGL3OpenAL.java +++ b/game/client/src/main/java/net/betterthanadventure/sound/ChannelLWJGL3OpenAL.java @@ -108,15 +108,13 @@ public class ChannelLWJGL3OpenAL extends Channel { IntBuffer var2; if (var4 > 0) { var2 = BufferUtils.createIntBuffer(var4); - AL10.alGenBuffers(var2); - if (this.errorCheck(this.checkALError(), "Error clearing stream buffers in method 'preLoadBuffers'")) { - return false; - } - AL10.alSourceUnqueueBuffers(this.ALSource.get(0), var2); if (this.errorCheck(this.checkALError(), "Error unqueuing stream buffers in method 'preLoadBuffers'")) { return false; } + + AL10.alDeleteBuffers(var2); + this.checkALError(); } if (var3) { @@ -197,11 +195,6 @@ public class ChannelLWJGL3OpenAL extends Channel { IntBuffer var3; if (var4 > 0) { var3 = BufferUtils.createIntBuffer(var4); - AL10.alGenBuffers(var3); - if (this.errorCheck(this.checkALError(), "Error clearing stream buffers in method 'feedRawAudioData'")) { - return -1; - } - AL10.alSourceUnqueueBuffers(this.ALSource.get(0), var3); if (this.errorCheck(this.checkALError(), "Error unqueuing stream buffers in method 'feedRawAudioData'")) { return -1; @@ -256,6 +249,9 @@ public class ChannelLWJGL3OpenAL extends Channel { if (this.checkALError()) { return; } + + AL10.alDeleteBuffers(var2); + this.checkALError(); } } diff --git a/game/client/src/main/java/net/betterthanadventure/sound/LibraryLWJGL3OpenAL.java b/game/client/src/main/java/net/betterthanadventure/sound/LibraryLWJGL3OpenAL.java index 1275cb860..7bb8be0db 100644 --- a/game/client/src/main/java/net/betterthanadventure/sound/LibraryLWJGL3OpenAL.java +++ b/game/client/src/main/java/net/betterthanadventure/sound/LibraryLWJGL3OpenAL.java @@ -12,8 +12,10 @@ import javax.sound.sampled.AudioFormat; import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.nio.IntBuffer; +import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Set; @@ -278,7 +280,35 @@ public class LibraryLWJGL3OpenAL extends Library { } public void unloadSound(String var1) { - this.ALBufferMap.remove(var1); + IntBuffer buffer = this.ALBufferMap != null ? this.ALBufferMap.remove(var1) : null; + if (buffer != null) { + int bufferId = buffer.get(0); + + // OpenAL refuses to delete a buffer that is still attached to a source :P + for (Object channel : this.normalChannels) { + if (channel instanceof ChannelLWJGL3OpenAL alChannel && alChannel.ALSource != null) { + int alSource = alChannel.ALSource.get(0); + if (AL10.alGetSourcei(alSource, AL10.AL_BUFFER) == bufferId) { + AL10.alSourceStop(alSource); + AL10.alSourcei(alSource, AL10.AL_BUFFER, 0); + } + } + } + + // sources for this sound would keep referencing the deleted buffer without + List staleSources = new ArrayList<>(); + for (Object entry : this.sourceMap.entrySet()) { + Map.Entry sourceEntry = (Map.Entry) entry; + Source source = (Source) sourceEntry.getValue(); + if (source != null && !source.toStream && source.filenameURL != null && var1.equals(source.filenameURL.getFilename())) { + staleSources.add((String) sourceEntry.getKey()); + } + } + staleSources.forEach(this::removeSource); + + AL10.alDeleteBuffers(buffer); + this.checkALError(); + } super.unloadSound(var1); } 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 8b5ec6ebb..a3f739aa7 100644 --- a/game/client/src/main/java/net/minecraft/client/Minecraft.java +++ b/game/client/src/main/java/net/minecraft/client/Minecraft.java @@ -2145,15 +2145,8 @@ public class Minecraft if (this.currentWorld != null) { this.currentWorld.saveWorldIndirectly(this.loadingScreen); ChunkLoader loader = this.currentWorld.getLevelStorage().getChunkLoader(this.currentWorld.dimension); - if (loader != null) { - while (loader.isSaving()) { - try { - Thread.sleep(1); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - } + if (loader != null) loader.awaitSaving(); + this.currentWorld.getChunkProvider().unloadAllChunks(); this.currentWorld.onUnload(); this.currentWorld.close(); diff --git a/game/client/src/main/java/net/minecraft/client/gui/ScreenConsole.java b/game/client/src/main/java/net/minecraft/client/gui/ScreenConsole.java index c6bcae7f4..d7981d1dd 100644 --- a/game/client/src/main/java/net/minecraft/client/gui/ScreenConsole.java +++ b/game/client/src/main/java/net/minecraft/client/gui/ScreenConsole.java @@ -388,12 +388,18 @@ public class ScreenConsole extends Screen { private final @NotNull LogRenderer logRenderer; private final @NotNull InputRenderer inputRenderer; + private static @Nullable FontRenderer sharedMonospaceFont; private final FontRenderer monospaceFont; public ScreenConsole(final @Nullable Screen parent) { super(parent); - this.monospaceFont = new FontRendererDefault(Fonts.MONOSPACE); - this.monospaceFont.init(); + + if (sharedMonospaceFont == null) { + sharedMonospaceFont = new FontRendererDefault(Fonts.MONOSPACE); + sharedMonospaceFont.init(); + } + + this.monospaceFont = sharedMonospaceFont; this.logRenderer = new LogRenderer(this.monospaceFont); this.inputRenderer = new InputRenderer(this.monospaceFont); } 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 new file mode 100644 index 000000000..30aa664c7 --- /dev/null +++ b/game/core/src/main/java/net/minecraft/core/util/collection/Lockfree.java @@ -0,0 +1,135 @@ +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 187b281b6..62e4b3149 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,6 +3,7 @@ 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 { @@ -40,4 +41,8 @@ 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 df0140e39..dfc29cef3 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,33 +1,29 @@ 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.LinkedList; -import java.util.Queue; -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.LockSupport; public class ChunkLoaderRegionAsync implements ChunkLoader { private static final Logger LOGGER = LogUtils.getLogger(); - 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(); + + private static final ChunkSaveThread thread; + static { + thread = new ChunkSaveThread(); + thread.start(); System.out.println("Spawned new chunk save thread!"); } @@ -48,78 +44,65 @@ public class ChunkLoaderRegionAsync public void saveChunk(World world, Chunk chunk) throws IOException { - lockChunksToSave.lock(); - try - { - if (!chunksToSave.contains(chunk)) { - chunksToSave.add(chunk); - worldsToSave.add(world); - chunkLoaders.add(chunkLoaderRegion); - notEmpty.signal(); - } - } - finally - { - lockChunksToSave.unlock(); - } + thread.push(this.chunkLoaderRegion, world, chunk); } @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 !chunksToSave.isEmpty(); + return !thread.isJobless(); } private static class ChunkSaveThread extends Thread { - public volatile boolean shouldStop = false; + 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(); + } @Override public void run() { - 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) { + for (;;) { + Entry entry; + this.isAsleep.setRelease(true); + while ((entry = entries.poll()) == null) LockSupport.park(); + this.isAsleep.setRelease(false); + try { entry.saveChunk(); } + catch (Throwable e) { LOGGER.error("Exception while attempting to save chunks in async loader", e); - } finally { - lockChunksToSave.unlock(); } + chunksSet.remove(entry.chunk); } } }