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
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-30 20:25:50 -04:00
1 parent c700138b66
commit 1a914a404a
11 files changed
+288 -32

No files matched your search

@@ -20,7 +20,7 @@ package art.arcane.iris.generation.chunk;
import art.arcane.volmlib.util.hunk.Hunk; import art.arcane.volmlib.util.hunk.Hunk;
public final class ColumnExtentListeningHunk<T> implements Hunk<T>, ColumnExtent, ColumnRunHunk<T> { public final class ColumnExtentListeningHunk<T> implements Hunk<T>, ColumnExtent, ColumnRunHunk<T>, ColumnReadHunk<T> {
private final Hunk<T> source; private final Hunk<T> source;
private final Listener<T> listener; private final Listener<T> listener;
@@ -46,6 +46,11 @@ public final class ColumnExtentListeningHunk<T> implements Hunk<T>, ColumnExtent
return source.getRaw(x, y, z); 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 @Override
public int getWidth() { public int getWidth() {
return source.getWidth(); return source.getWidth();
@@ -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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.generation.chunk;
import art.arcane.volmlib.util.hunk.Hunk;
public interface ColumnReadHunk<T> {
void readColumn(int x, int z, int yFromInclusive, int yToInclusive, T[] target, int targetOffset);
@SuppressWarnings("unchecked")
static <T> void readColumn(Hunk<T> hunk, int x, int z, int yFromInclusive, int yToInclusive, T[] target,
int targetOffset) {
if (hunk instanceof ColumnReadHunk<?> reads) {
((ColumnReadHunk<T>) 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);
}
}
}
@@ -27,7 +27,8 @@ import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle; import java.lang.invoke.VarHandle;
import java.util.Arrays; import java.util.Arrays;
public final class SectionBuffer implements Hunk<NativeBlockState>, NativeSectionVolume, ColumnExtent, ColumnRunHunk<NativeBlockState> { public final class SectionBuffer implements Hunk<NativeBlockState>, NativeSectionVolume, ColumnExtent, ColumnRunHunk<NativeBlockState>,
ColumnReadHunk<NativeBlockState> {
private static final BoundBlockState AIR = BoundBlockState.of("AIR"); private static final BoundBlockState AIR = BoundBlockState.of("AIR");
private static final VarHandle SECTIONS = MethodHandles.arrayElementVarHandle(short[][].class); private static final VarHandle SECTIONS = MethodHandles.arrayElementVarHandle(short[][].class);
private static final VarHandle COLUMN_TOPS = MethodHandles.arrayElementVarHandle(int[].class); private static final VarHandle COLUMN_TOPS = MethodHandles.arrayElementVarHandle(int[].class);
@@ -43,6 +44,8 @@ public final class SectionBuffer implements Hunk<NativeBlockState>, NativeSectio
private PaletteEntry[] slots; private PaletteEntry[] slots;
private NativeBlockState[] states; private NativeBlockState[] states;
private int paletteSize; private int paletteSize;
private NativeBlockState air;
private PaletteEntry lastEntry;
public SectionBuffer(int height) { public SectionBuffer(int height) {
if (height <= 0) { if (height <= 0) {
@@ -76,12 +79,17 @@ public final class SectionBuffer implements Hunk<NativeBlockState>, NativeSectio
if (state == null) { if (state == null) {
return 0; return 0;
} }
PaletteEntry last = lastEntry;
if (last != null && last.state == state) {
return last.id;
}
PaletteEntry[] table = slots; PaletteEntry[] table = slots;
int mask = table.length - 1; int mask = table.length - 1;
int slot = System.identityHashCode(state) & mask; int slot = System.identityHashCode(state) & mask;
PaletteEntry entry; PaletteEntry entry;
while ((entry = table[slot]) != null) { while ((entry = table[slot]) != null) {
if (entry.state == state) { if (entry.state == state) {
lastEntry = entry;
return entry.id; return entry.id;
} }
slot = (slot + 1) & mask; slot = (slot + 1) & mask;
@@ -101,7 +109,7 @@ public final class SectionBuffer implements Hunk<NativeBlockState>, NativeSectio
@Override @Override
public NativeBlockState getRaw(int x, int y, int z) { public NativeBlockState getRaw(int x, int y, int z) {
if (y < 0 || y >= height) { if (y < 0 || y >= height) {
return AIR.get(); return air();
} }
short[] section = sections[y >> 4]; short[] section = sections[y >> 4];
if (section != null) { if (section != null) {
@@ -110,7 +118,7 @@ public final class SectionBuffer implements Hunk<NativeBlockState>, NativeSectio
return state(id); return state(id);
} }
} }
return AIR.get(); return air();
} }
@Override @Override
@@ -126,6 +134,32 @@ public final class SectionBuffer implements Hunk<NativeBlockState>, NativeSectio
return id == 0 ? null : state(id); 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 @Override
public void fillColumnRun(int x, int z, int yFromInclusive, int yToInclusive, NativeBlockState value) { public void fillColumnRun(int x, int z, int yFromInclusive, int yToInclusive, NativeBlockState value) {
if (value == null || yToInclusive < yFromInclusive) { if (value == null || yToInclusive < yFromInclusive) {
@@ -176,6 +210,15 @@ public final class SectionBuffer implements Hunk<NativeBlockState>, NativeSectio
} }
} }
private NativeBlockState air() {
NativeBlockState resolved = air;
if (resolved == null) {
resolved = AIR.get();
air = resolved;
}
return resolved;
}
private short[] writableSection(int sectionIndex) { private short[] writableSection(int sectionIndex) {
short[] section = sections[sectionIndex]; short[] section = sections[sectionIndex];
if (section != null) { if (section != null) {
@@ -28,9 +28,14 @@ import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KMap;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReferenceArray;
public final class IrisContext { public final class IrisContext {
private static final ThreadLocal<IrisContext> CONTEXT = new ThreadLocal<>(); private static final int SLOTS = 512;
private static final AtomicReferenceArray<Binding> BINDINGS = new AtomicReferenceArray<>(SLOTS);
private static final ThreadLocal<Binding> OVERFLOW = new ThreadLocal<>();
private static final AtomicInteger OVERFLOW_BINDINGS = new AtomicInteger();
private final Engine engine; private final Engine engine;
private final ChunkContext chunkContext; private final ChunkContext chunkContext;
private final long generationSessionId; private final long generationSessionId;
@@ -42,7 +47,8 @@ public final class IrisContext {
} }
public static IrisContext get() { public static IrisContext get() {
return CONTEXT.get(); Binding binding = binding(Thread.currentThread());
return binding == null ? null : binding.context;
} }
public static IrisContext require() { public static IrisContext require() {
@@ -55,9 +61,10 @@ public final class IrisContext {
public static Scope open(Engine engine, long generationSessionId, ChunkContext chunkContext) { public static Scope open(Engine engine, long generationSessionId, ChunkContext chunkContext) {
Thread thread = Thread.currentThread(); Thread thread = Thread.currentThread();
IrisContext previous = CONTEXT.get(); Binding binding = bind(thread);
IrisContext previous = binding.context;
IrisContext installed = new IrisContext(engine, generationSessionId, chunkContext); IrisContext installed = new IrisContext(engine, generationSessionId, chunkContext);
CONTEXT.set(installed); binding.context = installed;
return new Scope(thread, previous, installed); return new Scope(thread, previous, installed);
} }
@@ -99,6 +106,42 @@ public final class IrisContext {
.qput("queued", mantle.getUnloadRegionCount())); .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 { public static final class Scope implements NativeGenerationScope {
private final Thread owner; private final Thread owner;
private final IrisContext previous; private final IrisContext previous;
@@ -119,16 +162,26 @@ public final class IrisContext {
if (Thread.currentThread() != owner) { if (Thread.currentThread() != owner) {
throw new IllegalStateException("Iris execution context scope closed from a different thread."); throw new IllegalStateException("Iris execution context scope closed from a different thread.");
} }
IrisContext current = CONTEXT.get(); Binding binding = binding(owner);
if (current != installed) { if (binding == null || binding.context != installed) {
throw new IllegalStateException("Iris execution context scopes must close in LIFO order."); throw new IllegalStateException("Iris execution context scopes must close in LIFO order.");
} }
if (previous == null) { if (previous == null) {
CONTEXT.remove(); release(binding);
} else { } else {
CONTEXT.set(previous); binding.context = previous;
} }
closed = true; closed = true;
} }
} }
private static final class Binding {
private final Thread owner;
private boolean overflow;
private IrisContext context;
private Binding(Thread owner) {
this.owner = owner;
}
}
} }
@@ -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.cave.HydrologyCaveStorage;
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster; import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
import art.arcane.iris.generation.chunk.ColumnExtent; 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.ChunkContext;
import art.arcane.iris.generation.context.IrisContext; import art.arcane.iris.generation.context.IrisContext;
import art.arcane.iris.generation.block.B; import art.arcane.iris.generation.block.B;
@@ -1086,19 +1087,23 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
int height = output.getHeight(); int height = output.getHeight();
ColumnExtent extent = output instanceof ColumnExtent columnExtent ? columnExtent : null; ColumnExtent extent = output instanceof ColumnExtent columnExtent ? columnExtent : null;
SolidMemo solids = new SolidMemo(); SolidMemo solids = new SolidMemo();
NativeBlockState[] column = new NativeBlockState[height];
for (int localX = 0; localX < width; localX++) { for (int localX = 0; localX < width; localX++) {
for (int localZ = 0; localZ < depth; localZ++) { for (int localZ = 0; localZ < depth; localZ++) {
int worldX = blockX + localX; int worldX = blockX + localX;
int worldZ = blockZ + localZ; int worldZ = blockZ + localZ;
int floor = -1; int floor = -1;
int top = extent == null ? height - 1 : extent.highestStoredY(localX, localZ); 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 previousKnown = false;
boolean previousSolid = false; boolean previousSolid = false;
for (int y = 1; y < height; y++) { for (int y = 1; y < height; y++) {
if (y > top) { if (y > top) {
break; break;
} }
boolean solid = solids.test(output.getRaw(localX, y, localZ)); boolean solid = solids.test(column[y]);
boolean decorated = false; boolean decorated = false;
if (solid) { if (solid) {
if (floor >= 0) { if (floor >= 0) {
@@ -1122,11 +1127,15 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
if (extent != null) { if (extent != null) {
top = extent.highestStoredY(localX, localZ); 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; floor = -1;
} }
} else if (floor < 0 && (previousKnown ? previousSolid } else if (floor < 0 && (previousKnown ? previousSolid
: solids.test(output.getRaw(localX, y - 1, localZ)))) { : solids.test(column[y - 1]))) {
floor = y; floor = y;
} }
previousSolid = solid; previousSolid = solid;
@@ -19,6 +19,7 @@
package art.arcane.iris.generation.stage; package art.arcane.iris.generation.stage;
import art.arcane.iris.generation.chunk.ColumnExtent; 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.decoration.IrisSpeleothems;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EngineAssignedModifier; import art.arcane.iris.generation.runtime.EngineAssignedModifier;
@@ -105,24 +106,28 @@ public class IrisPerfectionModifier extends EngineAssignedModifier<NativeBlockSt
StateFlags states = new StateFlags(); StateFlags states = new StateFlags();
NativeBlockState air = AIR.get(); NativeBlockState air = AIR.get();
NativeBlockState water = WATER.get(); NativeBlockState water = WATER.get();
NativeBlockState[] column = new NativeBlockState[output.getHeight()];
for (int x = firstSlice; x < lastSlice; x++) { for (int x = firstSlice; x < lastSlice; x++) {
for (int z = 0; z < 16; z++) { for (int z = 0; z < 16; z++) {
perfectColumn(output, x, z, surfaces, states, air, water); perfectColumn(output, x, z, surfaces, states, air, water, column);
} }
} }
} }
private static void perfectColumn(Hunk<NativeBlockState> output, int x, int z, IntArrayList surfaces, private static void perfectColumn(Hunk<NativeBlockState> output, int x, int z, IntArrayList surfaces,
StateFlags states, NativeBlockState air, NativeBlockState water) { StateFlags states, NativeBlockState air, NativeBlockState water,
NativeBlockState[] column) {
do { do {
surfaces.clear(); 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; boolean inside = true;
surfaces.add(top); surfaces.add(top);
NativeBlockState classified = null; NativeBlockState classified = null;
int classifiedFlags = 0; int classifiedFlags = 0;
for (int y = top; y >= 0; y--) { for (int y = top; y >= 0; y--) {
NativeBlockState block = output.get(x, y, z); NativeBlockState block = column[y];
int flags = 0; int flags = 0;
if (block != null) { if (block != null) {
if (block != classified) { if (block != classified) {
@@ -133,6 +138,9 @@ public class IrisPerfectionModifier extends EngineAssignedModifier<NativeBlockSt
if ((flags & SPIKE) != 0) { if ((flags & SPIKE) != 0) {
block = normalizeSpike(block, output, x, z, y, air, water); block = normalizeSpike(block, output, x, z, y, air, water);
flags = block == null ? 0 : states.of(block); flags = block == null ? 0 : states.of(block);
if (y > 0) {
ColumnReadHunk.readColumn(output, x, z, 0, y - 1, column, 0);
}
} }
} }
boolean now = (flags & SOLID) != 0; boolean now = (flags & SOLID) != 0;
@@ -233,11 +241,11 @@ public class IrisPerfectionModifier extends EngineAssignedModifier<NativeBlockSt
burst.complete(); burst.complete();
} }
private static int getHeight(Hunk<NativeBlockState> output, int x, int z, StateFlags states) { private static int getHeight(NativeBlockState[] column, int highest, StateFlags states) {
NativeBlockState classified = null; NativeBlockState classified = null;
int classifiedFlags = 0; int classifiedFlags = 0;
for (int i = Math.min(output.getHeight() - 1, ColumnExtent.highestStoredY(output, x, z)); i >= 0; i--) { for (int i = highest; i >= 0; i--) {
NativeBlockState state = output.get(x, i, z); NativeBlockState state = column[i];
if (state == null) { if (state == null) {
continue; continue;
} }
@@ -1,5 +1,6 @@
package art.arcane.iris.generation.stage; 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.Engine;
import art.arcane.iris.generation.runtime.EngineAssignedComponent; import art.arcane.iris.generation.runtime.EngineAssignedComponent;
import art.arcane.iris.generation.runtime.EngineStage; import art.arcane.iris.generation.runtime.EngineStage;
@@ -111,6 +112,7 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
private final ChunkContext context; private final ChunkContext context;
private final Map<NativeBlockState, BoundaryColumnGeometry.Voxel> voxels = new IdentityHashMap<>(); private final Map<NativeBlockState, BoundaryColumnGeometry.Voxel> voxels = new IdentityHashMap<>();
private NativeBlockState air; private NativeBlockState air;
private NativeBlockState[] column = new NativeBlockState[0];
private NativeBlockState lastState; private NativeBlockState lastState;
private BoundaryColumnGeometry.Voxel lastVoxel; private BoundaryColumnGeometry.Voxel lastVoxel;
@@ -132,10 +134,14 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
return; return;
} }
int base = minimumY - this.minimumY; 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; int start = 0;
for (int offset = 1; offset < height; offset++) { for (int offset = 1; offset < height; offset++) {
NativeBlockState state = stateAt(localX, base + offset, localZ); NativeBlockState state = orAir(column[offset]);
if (state != runState) { if (state != runState) {
runs.add(start, offset, voxelOf(runState)); runs.add(start, offset, voxelOf(runState));
start = offset; start = offset;
@@ -146,7 +152,10 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
} }
private NativeBlockState stateAt(int localX, int offset, int localZ) { 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) { if (state != null) {
return state; return state;
} }
@@ -35,7 +35,7 @@ import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterStructurePOI; import art.arcane.volmlib.util.matter.MatterStructurePOI;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.BitSet; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@@ -238,7 +238,7 @@ public final class GenerationSemanticCapture {
Mantle<Matter> mantle = engine.getMantle().getMantle(); Mantle<Matter> mantle = engine.getMantle().getMantle();
int minimumX = Math.multiplyExact(chunkX, CHUNK_SIZE); int minimumX = Math.multiplyExact(chunkX, CHUNK_SIZE);
int minimumZ = Math.multiplyExact(chunkZ, CHUNK_SIZE); int minimumZ = Math.multiplyExact(chunkZ, CHUNK_SIZE);
BitSet resolvedCaves = new BitSet(); CavePositions resolvedCaves = new CavePositions();
CaveBiomes caveBiomes = new CaveBiomes(engine, scoped); CaveBiomes caveBiomes = new CaveBiomes(engine, scoped);
mantle.iterateChunk(chunkX, chunkZ, MatterCavern.class, (localX, y, localZ, cavern) -> { mantle.iterateChunk(chunkX, chunkZ, MatterCavern.class, (localX, y, localZ, cavern) -> {
if (!caveSpace.test(localX, y, localZ)) { if (!caveSpace.test(localX, y, localZ)) {
@@ -370,17 +370,15 @@ public final class GenerationSemanticCapture {
private static void captureCave( private static void captureCave(
CaveBiomes caveBiomes, CaveBiomes caveBiomes,
ChunkGenerationSemantics.Builder semantics, ChunkGenerationSemantics.Builder semantics,
BitSet resolvedCaves, CavePositions resolvedCaves,
int blockX, int blockX,
int y, int y,
int blockZ, int blockZ,
String explicitBiomeKey String explicitBiomeKey
) { ) {
int position = (y << 8) | ((blockX & 15) << 4) | (blockZ & 15); if (!resolvedCaves.add((y << 8) | ((blockX & 15) << 4) | (blockZ & 15))) {
if (resolvedCaves.get(position)) {
return; return;
} }
resolvedCaves.set(position);
if (explicitBiomeKey != null && !explicitBiomeKey.isBlank()) { if (explicitBiomeKey != null && !explicitBiomeKey.isBlank()) {
caveBiomes.add(semantics, explicitBiomeKey); caveBiomes.add(semantics, explicitBiomeKey);
return; 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 static final class CaveBiomes {
private final Engine engine; private final Engine engine;
private final boolean scoped; private final boolean scoped;
@@ -44,7 +44,7 @@ public class PlatformStateRebindTest {
} }
@Test @Test
public void chunkDataAirIsResolvedAgainstTheCurrentlyBoundPlatform() { public void chunkDataAirIsResolvedAgainstThePlatformBoundWhenTheBufferFirstReadsIt() {
NativeBlockState first = bindPlatform("first"); NativeBlockState first = bindPlatform("first");
SectionBuffer holder = new SectionBuffer(384); SectionBuffer holder = new SectionBuffer(384);
@@ -53,7 +53,8 @@ public class PlatformStateRebindTest {
IrisPlatforms.unbind(); IrisPlatforms.unbind();
NativeBlockState second = bindPlatform("second"); 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 @Test
@@ -264,6 +264,42 @@ public class SectionBufferTest {
assertEquals(List.of(9, 8, 7), order); 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<NativeBlockState> 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<NativeBlockState> 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) { private static void assertSameContents(SectionBuffer expected, SectionBuffer actual) {
assertEquals(expected.sectionCount(), actual.sectionCount()); assertEquals(expected.sectionCount(), actual.sectionCount());
NativeBlockState[] expectedPalette = expected.palette(); NativeBlockState[] expectedPalette = expected.palette();
@@ -3,6 +3,8 @@ package art.arcane.iris.generation.context;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import org.junit.Test; import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier; import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -50,6 +52,42 @@ public class IrisContextIsolationTest {
assertNull(rightAfterClose.get()); assertNull(rightAfterClose.get());
} }
@Test
public void contextsStayIsolatedWhenThreadsOutnumberTheBindingSlots() throws Exception {
Engine engine = mock(Engine.class);
int threads = 640;
CyclicBarrier allBound = new CyclicBarrier(threads);
AtomicReference<Throwable> failure = new AtomicReference<>();
List<Thread> 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 @Test
public void scopeRebindsWorkerWhenEngineChanges() throws Exception { public void scopeRebindsWorkerWhenEngineChanges() throws Exception {
Engine firstEngine = mock(Engine.class); Engine firstEngine = mock(Engine.class);