From 1a914a404a33ad89bdbe0dc93023385cb4e03f12 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Fri, 30 Oct 2026 20:25:50 -0400 Subject: [PATCH] Sped up column scans - Section buffers resolve air once per buffer instead of on every empty read - Section buffer writes remember the last palette entry they resolved - Added bulk column reads to section buffers and the update-listening hunk - Natural cave decoration, perfection and transition geometry capture read whole columns at once - Iris execution contexts bind through a thread slot table instead of a ThreadLocal - Semantic capture tracks resolved cave cells in a fixed bitmap instead of a BitSet - Added tests for bulk column reads and for contexts when threads outnumber the slot table --- .../chunk/ColumnExtentListeningHunk.java | 7 +- .../iris/generation/chunk/ColumnReadHunk.java | 37 ++++++++++ .../iris/generation/chunk/SectionBuffer.java | 49 ++++++++++++- .../iris/generation/context/IrisContext.java | 69 ++++++++++++++++--- .../generation/stage/IrisCarveModifier.java | 13 +++- .../stage/IrisPerfectionModifier.java | 22 ++++-- .../stage/IrisTransitionGeometryActuator.java | 15 +++- .../history/GenerationSemanticCapture.java | 29 ++++++-- .../block/PlatformStateRebindTest.java | 5 +- .../generation/chunk/SectionBufferTest.java | 36 ++++++++++ .../context/IrisContextIsolationTest.java | 38 ++++++++++ 11 files changed, 288 insertions(+), 32 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/generation/chunk/ColumnReadHunk.java 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..f3734a605 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,23 @@ public class IrisCarveModifier extends EngineAssignedModifier int height = output.getHeight(); ColumnExtent extent = output instanceof ColumnExtent columnExtent ? columnExtent : null; SolidMemo solids = new SolidMemo(); + NativeBlockState[] column = new NativeBlockState[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); + if (top >= 1) { + ColumnReadHunk.readColumn(output, localX, localZ, 0, Math.min(top, height - 1), column, 0); + } 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[y]); boolean decorated = false; if (solid) { if (floor >= 0) { @@ -1122,11 +1127,15 @@ public class IrisCarveModifier extends EngineAssignedModifier if (extent != null) { top = extent.highestStoredY(localX, localZ); } + int refreshTop = Math.min(top, height - 1); + if (refreshTop >= y) { + ColumnReadHunk.readColumn(output, localX, localZ, y, refreshTop, column, y); + } } floor = -1; } } else if (floor < 0 && (previousKnown ? previousSolid - : solids.test(output.getRaw(localX, y - 1, localZ)))) { + : solids.test(column[y - 1]))) { floor = y; } previousSolid = 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..24cc11c37 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,28 @@ 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; for (int y = top; y >= 0; y--) { - NativeBlockState block = output.get(x, y, z); + NativeBlockState block = column[y]; int flags = 0; if (block != null) { if (block != classified) { @@ -133,6 +138,9 @@ public class IrisPerfectionModifier extends EngineAssignedModifier 0) { + ColumnReadHunk.readColumn(output, x, z, 0, y - 1, column, 0); + } } } boolean now = (flags & SOLID) != 0; @@ -233,11 +241,11 @@ 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/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/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);