diff --git a/core/src/main/java/art/arcane/iris/generation/chunk/ColumnExtentListeningHunk.java b/core/src/main/java/art/arcane/iris/generation/chunk/ColumnExtentListeningHunk.java index 0498d0973..9c7aa0b30 100644 --- a/core/src/main/java/art/arcane/iris/generation/chunk/ColumnExtentListeningHunk.java +++ b/core/src/main/java/art/arcane/iris/generation/chunk/ColumnExtentListeningHunk.java @@ -20,7 +20,7 @@ package art.arcane.iris.generation.chunk; import art.arcane.volmlib.util.hunk.Hunk; -public final class ColumnExtentListeningHunk implements Hunk, ColumnExtent, ColumnRunHunk { +public final class ColumnExtentListeningHunk implements Hunk, ColumnExtent, ColumnRunHunk, ColumnReadHunk { private final Hunk source; private final Listener listener; @@ -46,6 +46,11 @@ public final class ColumnExtentListeningHunk implements Hunk, ColumnExtent return source.getRaw(x, y, z); } + @Override + public void readColumn(int x, int z, int yFromInclusive, int yToInclusive, T[] target, int targetOffset) { + ColumnReadHunk.readColumn(source, x, z, yFromInclusive, yToInclusive, target, targetOffset); + } + @Override public int getWidth() { return source.getWidth(); diff --git a/core/src/main/java/art/arcane/iris/generation/chunk/ColumnReadHunk.java b/core/src/main/java/art/arcane/iris/generation/chunk/ColumnReadHunk.java new file mode 100644 index 000000000..dbc90ce0d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/chunk/ColumnReadHunk.java @@ -0,0 +1,37 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.generation.chunk; + +import art.arcane.volmlib.util.hunk.Hunk; + +public interface ColumnReadHunk { + void readColumn(int x, int z, int yFromInclusive, int yToInclusive, T[] target, int targetOffset); + + @SuppressWarnings("unchecked") + static void readColumn(Hunk hunk, int x, int z, int yFromInclusive, int yToInclusive, T[] target, + int targetOffset) { + if (hunk instanceof ColumnReadHunk reads) { + ((ColumnReadHunk) reads).readColumn(x, z, yFromInclusive, yToInclusive, target, targetOffset); + return; + } + for (int y = yFromInclusive; y <= yToInclusive; y++) { + target[targetOffset + y - yFromInclusive] = hunk.getRaw(x, y, z); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/chunk/SectionBuffer.java b/core/src/main/java/art/arcane/iris/generation/chunk/SectionBuffer.java index 8a6898911..01e9710e7 100644 --- a/core/src/main/java/art/arcane/iris/generation/chunk/SectionBuffer.java +++ b/core/src/main/java/art/arcane/iris/generation/chunk/SectionBuffer.java @@ -27,7 +27,8 @@ import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; import java.util.Arrays; -public final class SectionBuffer implements Hunk, NativeSectionVolume, ColumnExtent, ColumnRunHunk { +public final class SectionBuffer implements Hunk, NativeSectionVolume, ColumnExtent, ColumnRunHunk, + ColumnReadHunk { private static final BoundBlockState AIR = BoundBlockState.of("AIR"); private static final VarHandle SECTIONS = MethodHandles.arrayElementVarHandle(short[][].class); private static final VarHandle COLUMN_TOPS = MethodHandles.arrayElementVarHandle(int[].class); @@ -43,6 +44,8 @@ public final class SectionBuffer implements Hunk, NativeSectio private PaletteEntry[] slots; private NativeBlockState[] states; private int paletteSize; + private NativeBlockState air; + private PaletteEntry lastEntry; public SectionBuffer(int height) { if (height <= 0) { @@ -76,12 +79,17 @@ public final class SectionBuffer implements Hunk, NativeSectio if (state == null) { return 0; } + PaletteEntry last = lastEntry; + if (last != null && last.state == state) { + return last.id; + } PaletteEntry[] table = slots; int mask = table.length - 1; int slot = System.identityHashCode(state) & mask; PaletteEntry entry; while ((entry = table[slot]) != null) { if (entry.state == state) { + lastEntry = entry; return entry.id; } slot = (slot + 1) & mask; @@ -101,7 +109,7 @@ public final class SectionBuffer implements Hunk, NativeSectio @Override public NativeBlockState getRaw(int x, int y, int z) { if (y < 0 || y >= height) { - return AIR.get(); + return air(); } short[] section = sections[y >> 4]; if (section != null) { @@ -110,7 +118,7 @@ public final class SectionBuffer implements Hunk, NativeSectio return state(id); } } - return AIR.get(); + return air(); } @Override @@ -126,6 +134,32 @@ public final class SectionBuffer implements Hunk, NativeSectio return id == 0 ? null : state(id); } + @Override + public void readColumn(int x, int z, int yFromInclusive, int yToInclusive, NativeBlockState[] target, int targetOffset) { + int column = (z << 4) | x; + int shift = targetOffset - yFromInclusive; + int y = yFromInclusive; + while (y <= yToInclusive) { + if (y < 0 || y >= height) { + target[shift + y] = air(); + y++; + continue; + } + int sectionIndex = y >> 4; + int last = Math.min(Math.min(yToInclusive, height - 1), (sectionIndex << 4) | 15); + short[] section = sections[sectionIndex]; + if (section == null) { + Arrays.fill(target, shift + y, shift + last + 1, air()); + } else { + for (int row = y; row <= last; row++) { + int id = section[((row & 15) << 8) | column] & 0xFFFF; + target[shift + row] = id == 0 ? air() : state(id); + } + } + y = last + 1; + } + } + @Override public void fillColumnRun(int x, int z, int yFromInclusive, int yToInclusive, NativeBlockState value) { if (value == null || yToInclusive < yFromInclusive) { @@ -176,6 +210,15 @@ public final class SectionBuffer implements Hunk, NativeSectio } } + private NativeBlockState air() { + NativeBlockState resolved = air; + if (resolved == null) { + resolved = AIR.get(); + air = resolved; + } + return resolved; + } + private short[] writableSection(int sectionIndex) { short[] section = sections[sectionIndex]; if (section != null) { diff --git a/core/src/main/java/art/arcane/iris/generation/context/IrisContext.java b/core/src/main/java/art/arcane/iris/generation/context/IrisContext.java index 2131b4609..54c00606d 100644 --- a/core/src/main/java/art/arcane/iris/generation/context/IrisContext.java +++ b/core/src/main/java/art/arcane/iris/generation/context/IrisContext.java @@ -28,9 +28,14 @@ import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.volmlib.util.collection.KMap; import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReferenceArray; public final class IrisContext { - private static final ThreadLocal CONTEXT = new ThreadLocal<>(); + private static final int SLOTS = 512; + private static final AtomicReferenceArray BINDINGS = new AtomicReferenceArray<>(SLOTS); + private static final ThreadLocal OVERFLOW = new ThreadLocal<>(); + private static final AtomicInteger OVERFLOW_BINDINGS = new AtomicInteger(); private final Engine engine; private final ChunkContext chunkContext; private final long generationSessionId; @@ -42,7 +47,8 @@ public final class IrisContext { } public static IrisContext get() { - return CONTEXT.get(); + Binding binding = binding(Thread.currentThread()); + return binding == null ? null : binding.context; } public static IrisContext require() { @@ -55,9 +61,10 @@ public final class IrisContext { public static Scope open(Engine engine, long generationSessionId, ChunkContext chunkContext) { Thread thread = Thread.currentThread(); - IrisContext previous = CONTEXT.get(); + Binding binding = bind(thread); + IrisContext previous = binding.context; IrisContext installed = new IrisContext(engine, generationSessionId, chunkContext); - CONTEXT.set(installed); + binding.context = installed; return new Scope(thread, previous, installed); } @@ -99,6 +106,42 @@ public final class IrisContext { .qput("queued", mantle.getUnloadRegionCount())); } + private static Binding binding(Thread thread) { + Binding binding = BINDINGS.getPlain(slot(thread)); + if (binding != null && binding.owner == thread) { + return binding; + } + return OVERFLOW_BINDINGS.get() == 0 ? null : OVERFLOW.get(); + } + + private static Binding bind(Thread thread) { + Binding binding = binding(thread); + if (binding != null) { + return binding; + } + binding = new Binding(thread); + if (!BINDINGS.compareAndSet(slot(thread), null, binding)) { + binding.overflow = true; + OVERFLOW.set(binding); + OVERFLOW_BINDINGS.incrementAndGet(); + } + return binding; + } + + private static void release(Binding binding) { + binding.context = null; + if (binding.overflow) { + OVERFLOW.remove(); + OVERFLOW_BINDINGS.decrementAndGet(); + return; + } + BINDINGS.compareAndSet(slot(binding.owner), binding, null); + } + + private static int slot(Thread thread) { + return (int) thread.threadId() & (SLOTS - 1); + } + public static final class Scope implements NativeGenerationScope { private final Thread owner; private final IrisContext previous; @@ -119,16 +162,26 @@ public final class IrisContext { if (Thread.currentThread() != owner) { throw new IllegalStateException("Iris execution context scope closed from a different thread."); } - IrisContext current = CONTEXT.get(); - if (current != installed) { + Binding binding = binding(owner); + if (binding == null || binding.context != installed) { throw new IllegalStateException("Iris execution context scopes must close in LIFO order."); } if (previous == null) { - CONTEXT.remove(); + release(binding); } else { - CONTEXT.set(previous); + binding.context = previous; } closed = true; } } + + private static final class Binding { + private final Thread owner; + private boolean overflow; + private IrisContext context; + + private Binding(Thread owner) { + this.owner = owner; + } + } } diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java index 62bed2da5..dc69b9b06 100644 --- a/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java +++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java @@ -49,6 +49,7 @@ import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell; import art.arcane.iris.generation.hydrology.cave.HydrologyCaveStorage; import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster; import art.arcane.iris.generation.chunk.ColumnExtent; +import art.arcane.iris.generation.chunk.ColumnReadHunk; import art.arcane.iris.generation.context.ChunkContext; import art.arcane.iris.generation.context.IrisContext; import art.arcane.iris.generation.block.B; @@ -1086,19 +1087,21 @@ public class IrisCarveModifier extends EngineAssignedModifier int height = output.getHeight(); ColumnExtent extent = output instanceof ColumnExtent columnExtent ? columnExtent : null; SolidMemo solids = new SolidMemo(); + ColumnWindow column = new ColumnWindow(output, height); for (int localX = 0; localX < width; localX++) { for (int localZ = 0; localZ < depth; localZ++) { int worldX = blockX + localX; int worldZ = blockZ + localZ; int floor = -1; int top = extent == null ? height - 1 : extent.highestStoredY(localX, localZ); + column.begin(localX, localZ, top); boolean previousKnown = false; boolean previousSolid = false; for (int y = 1; y < height; y++) { if (y > top) { break; } - boolean solid = solids.test(output.getRaw(localX, y, localZ)); + boolean solid = solids.test(column.get(y)); boolean decorated = false; if (solid) { if (floor >= 0) { @@ -1122,11 +1125,12 @@ public class IrisCarveModifier extends EngineAssignedModifier if (extent != null) { top = extent.highestStoredY(localX, localZ); } + column.begin(localX, localZ, top); } floor = -1; } } else if (floor < 0 && (previousKnown ? previousSolid - : solids.test(output.getRaw(localX, y - 1, localZ)))) { + : solids.test(column.get(y - 1)))) { floor = y; } previousSolid = solid; @@ -1472,6 +1476,43 @@ public class IrisCarveModifier extends EngineAssignedModifier private record CaveColumn(long key, IrisBiome surface, IrisBiome cave, int surfaceY, int minimumDepth) { } + private static final class ColumnWindow { + private static final int ROWS = 16; + + private final Hunk output; + private final NativeBlockState[] states; + private int x; + private int z; + private int limit; + private int from; + private int to; + + private ColumnWindow(Hunk output, int height) { + this.output = output; + this.states = new NativeBlockState[height]; + } + + private void begin(int localX, int localZ, int top) { + x = localX; + z = localZ; + limit = Math.min(top, states.length - 1); + from = 0; + to = -1; + } + + private NativeBlockState get(int y) { + if (y < from || y > to) { + if (y > limit) { + return output.getRaw(x, y, z); + } + from = y; + to = Math.min(limit, y + ROWS - 1); + ColumnReadHunk.readColumn(output, x, z, from, to, states, from); + } + return states[y]; + } + } + private static final class SolidMemo { private NativeBlockState state; private boolean solid; diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisPerfectionModifier.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisPerfectionModifier.java index 9cf870cc3..53d3aa6b8 100644 --- a/core/src/main/java/art/arcane/iris/generation/stage/IrisPerfectionModifier.java +++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisPerfectionModifier.java @@ -19,6 +19,7 @@ package art.arcane.iris.generation.stage; import art.arcane.iris.generation.chunk.ColumnExtent; +import art.arcane.iris.generation.chunk.ColumnReadHunk; import art.arcane.iris.generation.decoration.IrisSpeleothems; import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.EngineAssignedModifier; @@ -105,24 +106,29 @@ public class IrisPerfectionModifier extends EngineAssignedModifier output, int x, int z, IntArrayList surfaces, - StateFlags states, NativeBlockState air, NativeBlockState water) { + StateFlags states, NativeBlockState air, NativeBlockState water, + NativeBlockState[] column) { do { surfaces.clear(); - int top = getHeight(output, x, z, states); + int highest = Math.min(output.getHeight() - 1, ColumnExtent.highestStoredY(output, x, z)); + ColumnReadHunk.readColumn(output, x, z, 0, Math.max(0, highest), column, 0); + int top = getHeight(column, highest, states); boolean inside = true; surfaces.add(top); NativeBlockState classified = null; int classifiedFlags = 0; + boolean columnRead = true; for (int y = top; y >= 0; y--) { - NativeBlockState block = output.get(x, y, z); + NativeBlockState block = columnRead ? column[y] : output.get(x, y, z); int flags = 0; if (block != null) { if (block != classified) { @@ -133,6 +139,7 @@ public class IrisPerfectionModifier extends EngineAssignedModifier output, int x, int z, StateFlags states) { + private static int getHeight(NativeBlockState[] column, int highest, StateFlags states) { NativeBlockState classified = null; int classifiedFlags = 0; - for (int i = Math.min(output.getHeight() - 1, ColumnExtent.highestStoredY(output, x, z)); i >= 0; i--) { - NativeBlockState state = output.get(x, i, z); + for (int i = highest; i >= 0; i--) { + NativeBlockState state = column[i]; if (state == null) { continue; } diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisTransitionGeometryActuator.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisTransitionGeometryActuator.java index a7c1c4fa4..227b3a8be 100644 --- a/core/src/main/java/art/arcane/iris/generation/stage/IrisTransitionGeometryActuator.java +++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisTransitionGeometryActuator.java @@ -1,5 +1,6 @@ package art.arcane.iris.generation.stage; +import art.arcane.iris.generation.chunk.ColumnReadHunk; import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.EngineAssignedComponent; import art.arcane.iris.generation.runtime.EngineStage; @@ -111,6 +112,7 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen private final ChunkContext context; private final Map voxels = new IdentityHashMap<>(); private NativeBlockState air; + private NativeBlockState[] column = new NativeBlockState[0]; private NativeBlockState lastState; private BoundaryColumnGeometry.Voxel lastVoxel; @@ -132,10 +134,14 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen return; } int base = minimumY - this.minimumY; - NativeBlockState runState = stateAt(localX, base, localZ); + if (column.length < height) { + column = new NativeBlockState[height]; + } + ColumnReadHunk.readColumn(blocks, localX, localZ, base, base + height - 1, column, 0); + NativeBlockState runState = orAir(column[0]); int start = 0; for (int offset = 1; offset < height; offset++) { - NativeBlockState state = stateAt(localX, base + offset, localZ); + NativeBlockState state = orAir(column[offset]); if (state != runState) { runs.add(start, offset, voxelOf(runState)); start = offset; @@ -146,7 +152,10 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen } private NativeBlockState stateAt(int localX, int offset, int localZ) { - NativeBlockState state = blocks.getRaw(localX, offset, localZ); + return orAir(blocks.getRaw(localX, offset, localZ)); + } + + private NativeBlockState orAir(NativeBlockState state) { if (state != null) { return state; } diff --git a/core/src/main/java/art/arcane/iris/world/history/GenerationHistory.java b/core/src/main/java/art/arcane/iris/world/history/GenerationHistory.java index 83857db64..7731c1774 100644 --- a/core/src/main/java/art/arcane/iris/world/history/GenerationHistory.java +++ b/core/src/main/java/art/arcane/iris/world/history/GenerationHistory.java @@ -26,6 +26,8 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.LockSupport; import java.util.function.Predicate; public final class GenerationHistory { @@ -34,6 +36,7 @@ public final class GenerationHistory { private static final int MAXIMUM_CACHED_TRANSITION_PLANS = 4; private static final int MAXIMUM_SEMANTIC_BATCH = 32; private static final int MAXIMUM_PENDING_SEMANTIC_CLAIMS = 128; + private static final long SEMANTIC_CLAIM_PARK_NANOS = 10_000_000L; private final GenerationHistoryPaths paths; private final GenerationPackRepository packs; @@ -50,6 +53,7 @@ public final class GenerationHistory { private final Map transitionPlanCache; private final ArrayBlockingQueue pendingSemanticClaims = new ArrayBlockingQueue<>(MAXIMUM_PENDING_SEMANTIC_CLAIMS); + private final AtomicBoolean semanticWriter = new AtomicBoolean(); private GenerationHistory( GenerationHistoryPaths paths, @@ -776,12 +780,26 @@ public final class GenerationHistory { persistPendingSemanticClaims(); } } - synchronized (this) { - while (!pending.claim.completed) { - persistPendingSemanticClaims(); + while (!pending.settled) { + if (!semanticWriter.compareAndSet(false, true)) { + LockSupport.parkNanos(pending, SEMANTIC_CLAIM_PARK_NANOS); + continue; + } + try { + synchronized (this) { + while (!pending.claim.completed) { + persistPendingSemanticClaims(); + } + } + } finally { + semanticWriter.set(false); + PendingSemanticClaim next = pendingSemanticClaims.peek(); + if (next != null) { + LockSupport.unpark(next.owner); + } } - return pending.claim.result(); } + return pending.claim.result(); } private void persistPendingSemanticClaims() { @@ -814,10 +832,15 @@ public final class GenerationHistory { } while ((current = pendingSemanticClaims.poll()) != null) { reported |= current.claim.fail(failure); + current.settle(); } if (!reported) { throw failure; } + } finally { + for (PendingSemanticClaim pending : batch) { + pending.settle(); + } } } @@ -1354,11 +1377,21 @@ public final class GenerationHistory { private static final class PendingSemanticClaim { private final GenerationStage stage; private final GenerationSemanticIndex.Claim claim; + private final Thread owner = Thread.currentThread(); + private volatile boolean settled; private PendingSemanticClaim(GenerationStage stage, ChunkGenerationSemantics update) { this.stage = stage; this.claim = new GenerationSemanticIndex.Claim(update); } + + private void settle() { + if (!claim.completed || settled) { + return; + } + settled = true; + LockSupport.unpark(owner); + } } public static final class GenerationStage implements AutoCloseable { diff --git a/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticCapture.java b/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticCapture.java index 673cd4c79..b6ae00d61 100644 --- a/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticCapture.java +++ b/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticCapture.java @@ -35,7 +35,7 @@ import art.arcane.volmlib.util.matter.MatterCavern; import art.arcane.volmlib.util.matter.MatterStructurePOI; import java.util.ArrayDeque; -import java.util.BitSet; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -238,7 +238,7 @@ public final class GenerationSemanticCapture { Mantle mantle = engine.getMantle().getMantle(); int minimumX = Math.multiplyExact(chunkX, CHUNK_SIZE); int minimumZ = Math.multiplyExact(chunkZ, CHUNK_SIZE); - BitSet resolvedCaves = new BitSet(); + CavePositions resolvedCaves = new CavePositions(); CaveBiomes caveBiomes = new CaveBiomes(engine, scoped); mantle.iterateChunk(chunkX, chunkZ, MatterCavern.class, (localX, y, localZ, cavern) -> { if (!caveSpace.test(localX, y, localZ)) { @@ -370,17 +370,15 @@ public final class GenerationSemanticCapture { private static void captureCave( CaveBiomes caveBiomes, ChunkGenerationSemantics.Builder semantics, - BitSet resolvedCaves, + CavePositions resolvedCaves, int blockX, int y, int blockZ, String explicitBiomeKey ) { - int position = (y << 8) | ((blockX & 15) << 4) | (blockZ & 15); - if (resolvedCaves.get(position)) { + if (!resolvedCaves.add((y << 8) | ((blockX & 15) << 4) | (blockZ & 15))) { return; } - resolvedCaves.set(position); if (explicitBiomeKey != null && !explicitBiomeKey.isBlank()) { caveBiomes.add(semantics, explicitBiomeKey); return; @@ -493,6 +491,25 @@ public final class GenerationSemanticCapture { } } + private static final class CavePositions { + private static final int INITIAL_WORDS = 1536; + + private long[] words = new long[INITIAL_WORDS]; + + private boolean add(int position) { + int word = position >>> 6; + if (word >= words.length) { + words = Arrays.copyOf(words, Math.max(word + 1, words.length << 1)); + } + long bit = 1L << position; + if ((words[word] & bit) != 0L) { + return false; + } + words[word] |= bit; + return true; + } + } + private static final class CaveBiomes { private final Engine engine; private final boolean scoped; diff --git a/core/src/main/java/art/arcane/iris/world/history/SavedBiomeStore.java b/core/src/main/java/art/arcane/iris/world/history/SavedBiomeStore.java index 22d0240e8..23a6520e1 100644 --- a/core/src/main/java/art/arcane/iris/world/history/SavedBiomeStore.java +++ b/core/src/main/java/art/arcane/iris/world/history/SavedBiomeStore.java @@ -29,6 +29,8 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.LockSupport; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.CRC32; @@ -49,6 +51,7 @@ public final class SavedBiomeStore { private static final int MAXIMUM_CACHED_CHUNKS = 128; private static final long MAXIMUM_CACHED_BYTES = 64L * 1024L * 1024L; private static final int MAXIMUM_WRITE_BATCH = 64; + private static final long WRITE_PARK_NANOS = 10_000_000L; private static final ThreadLocal DEFLATERS = ThreadLocal.withInitial(() -> new Deflater(Deflater.BEST_SPEED)); private final Path dimensionRoot; @@ -132,19 +135,34 @@ public final class SavedBiomeStore { .putInt(body.length).put(body).putInt(checksum(body)).array(); PendingWrite pending = new PendingWrite(required, record); lock.pending.add(pending); - synchronized (lock) { - while (!pending.completed) { - persistPending(lock); + while (!pending.settled) { + if (!lock.writer.compareAndSet(false, true)) { + LockSupport.parkNanos(pending, WRITE_PARK_NANOS); + continue; } - if (pending.failure != null) { - throw pending.failure; + try { + synchronized (lock) { + while (!pending.completed) { + persistPending(lock); + } + } + } finally { + lock.writer.set(false); + PendingWrite next = lock.pending.peek(); + if (next != null) { + LockSupport.unpark(next.owner); + } } - return pending.persisted; } + if (pending.failure != null) { + throw pending.failure; + } + return pending.persisted; } private void persistPending(WriteStripe stripe) { Map> batches = new LinkedHashMap<>(); + List polled = new ArrayList<>(MAXIMUM_WRITE_BATCH); PendingWrite grouping = null; try { for (int count = 0; count < MAXIMUM_WRITE_BATCH; count++) { @@ -152,6 +170,7 @@ public final class SavedBiomeStore { if (grouping == null) { break; } + polled.add(grouping); long regionKey = key(grouping.chunk.chunkX() >> 5, grouping.chunk.chunkZ() >> 5); batches.computeIfAbsent(regionKey, ignored -> new ArrayList<>()).add(grouping); grouping = null; @@ -175,6 +194,10 @@ public final class SavedBiomeStore { } } throw failure; + } finally { + for (PendingWrite pending : polled) { + pending.settle(); + } } } @@ -822,19 +845,30 @@ public final class SavedBiomeStore { private static final class WriteStripe { private final ConcurrentLinkedQueue pending = new ConcurrentLinkedQueue<>(); + private final AtomicBoolean writer = new AtomicBoolean(); } private static final class PendingWrite { private final SavedBiomeChunk chunk; private final byte[] record; + private final Thread owner = Thread.currentThread(); private boolean completed; private boolean persisted; private IOException failure; + private volatile boolean settled; private PendingWrite(SavedBiomeChunk chunk, byte[] record) { this.chunk = chunk; this.record = record; } + + private void settle() { + if (!completed || settled) { + return; + } + settled = true; + LockSupport.unpark(owner); + } } private static final class DecodeBudget { diff --git a/core/src/test/java/art/arcane/iris/generation/block/PlatformStateRebindTest.java b/core/src/test/java/art/arcane/iris/generation/block/PlatformStateRebindTest.java index f149111e8..e4ef5651c 100644 --- a/core/src/test/java/art/arcane/iris/generation/block/PlatformStateRebindTest.java +++ b/core/src/test/java/art/arcane/iris/generation/block/PlatformStateRebindTest.java @@ -44,7 +44,7 @@ public class PlatformStateRebindTest { } @Test - public void chunkDataAirIsResolvedAgainstTheCurrentlyBoundPlatform() { + public void chunkDataAirIsResolvedAgainstThePlatformBoundWhenTheBufferFirstReadsIt() { NativeBlockState first = bindPlatform("first"); SectionBuffer holder = new SectionBuffer(384); @@ -53,7 +53,8 @@ public class PlatformStateRebindTest { IrisPlatforms.unbind(); NativeBlockState second = bindPlatform("second"); - assertSame(second, holder.getRaw(0, -1, 0)); + assertSame(first, holder.getRaw(0, -1, 0)); + assertSame(second, new SectionBuffer(384).getRaw(0, -1, 0)); } @Test diff --git a/core/src/test/java/art/arcane/iris/generation/chunk/SectionBufferTest.java b/core/src/test/java/art/arcane/iris/generation/chunk/SectionBufferTest.java index 75ab6a014..e2e212840 100644 --- a/core/src/test/java/art/arcane/iris/generation/chunk/SectionBufferTest.java +++ b/core/src/test/java/art/arcane/iris/generation/chunk/SectionBufferTest.java @@ -264,6 +264,42 @@ public class SectionBufferTest { assertEquals(List.of(9, 8, 7), order); } + @Test + public void columnReadsMatchSingleReadsAcrossEmptySectionsAndBufferEdges() { + SectionBuffer buffer = new SectionBuffer(40); + NativeBlockState stone = mock(NativeBlockState.class); + NativeBlockState dirt = mock(NativeBlockState.class); + buffer.setRaw(3, 2, 9, stone); + buffer.setRaw(3, 33, 9, dirt); + buffer.setRaw(3, 39, 9, stone); + ColumnExtentListeningHunk listening = new ColumnExtentListeningHunk<>(buffer, (x, y, z, state) -> { + }); + NativeBlockState[] column = new NativeBlockState[52]; + + ColumnReadHunk.readColumn(listening, 3, 9, -3, 44, column, 4); + + for (int y = -3; y <= 44; y++) { + assertSame("y=" + y, buffer.getRaw(3, y, 9), column[4 + y + 3]); + } + assertNull(column[0]); + assertSame(dirt, column[4 + 33 + 3]); + } + + @Test + public void plainHunksAnswerColumnReadsWithSingleReads() { + Hunk array = Hunk.newArrayHunk(16, 32, 16); + NativeBlockState stone = mock(NativeBlockState.class); + array.setRaw(5, 7, 1, stone); + NativeBlockState[] column = new NativeBlockState[10]; + + ColumnReadHunk.readColumn(array, 5, 1, 6, 8, column, 2); + + assertNull(column[2]); + assertSame(stone, column[3]); + assertNull(column[4]); + assertNull(column[5]); + } + private static void assertSameContents(SectionBuffer expected, SectionBuffer actual) { assertEquals(expected.sectionCount(), actual.sectionCount()); NativeBlockState[] expectedPalette = expected.palette(); diff --git a/core/src/test/java/art/arcane/iris/generation/context/IrisContextIsolationTest.java b/core/src/test/java/art/arcane/iris/generation/context/IrisContextIsolationTest.java index 02530c7c3..8085c02a3 100644 --- a/core/src/test/java/art/arcane/iris/generation/context/IrisContextIsolationTest.java +++ b/core/src/test/java/art/arcane/iris/generation/context/IrisContextIsolationTest.java @@ -3,6 +3,8 @@ package art.arcane.iris.generation.context; import art.arcane.iris.generation.runtime.Engine; import org.junit.Test; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.TimeUnit; @@ -50,6 +52,42 @@ public class IrisContextIsolationTest { assertNull(rightAfterClose.get()); } + @Test + public void contextsStayIsolatedWhenThreadsOutnumberTheBindingSlots() throws Exception { + Engine engine = mock(Engine.class); + int threads = 640; + CyclicBarrier allBound = new CyclicBarrier(threads); + AtomicReference failure = new AtomicReference<>(); + List workers = new ArrayList<>(threads); + for (int index = 0; index < threads; index++) { + long session = index + 1L; + Thread worker = new Thread(() -> { + try (IrisContext.Scope ignored = IrisContext.open(engine, session, null)) { + allBound.await(30L, TimeUnit.SECONDS); + if (IrisContext.get().getGenerationSessionId() != session) { + failure.compareAndSet(null, new AssertionError("Thread observed session " + + IrisContext.get().getGenerationSessionId() + " instead of " + session)); + } + allBound.await(30L, TimeUnit.SECONDS); + } catch (Throwable thrown) { + failure.compareAndSet(null, thrown); + } + if (IrisContext.get() != null) { + failure.compareAndSet(null, new AssertionError("Context survived its scope")); + } + }, "iris-context-crowd-" + index); + workers.add(worker); + worker.start(); + } + for (Thread worker : workers) { + worker.join(60000L); + assertFalse("Context worker did not finish.", worker.isAlive()); + } + + assertNull(failure.get()); + assertNull(IrisContext.get()); + } + @Test public void scopeRebindsWorkerWhenEngineChanges() throws Exception { Engine firstEngine = mock(Engine.class); diff --git a/core/src/test/java/art/arcane/iris/world/history/GenerationHistoryAdmissionTest.java b/core/src/test/java/art/arcane/iris/world/history/GenerationHistoryAdmissionTest.java index 15455769d..1cdbada60 100644 --- a/core/src/test/java/art/arcane/iris/world/history/GenerationHistoryAdmissionTest.java +++ b/core/src/test/java/art/arcane/iris/world/history/GenerationHistoryAdmissionTest.java @@ -250,7 +250,7 @@ public class GenerationHistoryAdmissionTest { .addSurfaceBiome("iris:conflict").seal().build() : claim(stage); results.add(executor.submit(() -> persistCounting(history, stage, update, appends, forces))); - awaitBlockedOnHistory(history, workers, index + 1); + awaitQueuedOnHistory(history, workers, index + 1); } stages.get(5).close(); } @@ -340,7 +340,7 @@ public class GenerationHistoryAdmissionTest { for (int index = 0; index < stages.size(); index++) { GenerationHistory.GenerationStage stage = stages.get(index); results.add(executor.submit(() -> persistFailingRegion(history, stage, failure))); - awaitBlockedOnHistory(history, workers, index + 1); + awaitQueuedOnHistory(history, workers, index + 1); } } assertTrue(results.getFirst().get(5, TimeUnit.SECONDS)); @@ -404,21 +404,27 @@ public class GenerationHistoryAdmissionTest { } } - private static void awaitBlockedOnHistory(GenerationHistory history, List workers, int count) { - Await.until("semantic claim callers to queue behind the History monitor", Duration.ofSeconds(5L), - () -> blockedOnHistory(history, workers) == count); + private static void awaitQueuedOnHistory(GenerationHistory history, List workers, int count) { + Await.until("semantic claim callers to queue behind the History writer", Duration.ofSeconds(5L), + () -> queuedOnHistory(history, workers) == count); } - private static int blockedOnHistory(GenerationHistory history, List workers) { - int blocked = 0; + private static int queuedOnHistory(GenerationHistory history, List workers) { + int queued = 0; for (Thread worker : workers) { ThreadInfo info = ManagementFactory.getThreadMXBean().getThreadInfo(worker.threadId()); - if (info != null && info.getThreadState() == Thread.State.BLOCKED - && info.getLockInfo().getIdentityHashCode() == System.identityHashCode(history)) { - blocked++; + if (info == null || info.getLockInfo() == null) { + continue; + } + boolean writer = info.getThreadState() == Thread.State.BLOCKED + && info.getLockInfo().getIdentityHashCode() == System.identityHashCode(history); + boolean follower = info.getThreadState() == Thread.State.TIMED_WAITING + && info.getLockInfo().getClassName().endsWith("GenerationHistory$PendingSemanticClaim"); + if (writer || follower) { + queued++; } } - return blocked; + return queued; } private static boolean persistPaused(GenerationHistory history, GenerationHistory.GenerationStage stage,