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;
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 Listener<T> listener;
@@ -46,6 +46,11 @@ public final class ColumnExtentListeningHunk<T> implements Hunk<T>, 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();
@@ -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.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 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<NativeBlockState>, 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<NativeBlockState>, 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<NativeBlockState>, 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<NativeBlockState>, NativeSectio
return state(id);
}
}
return AIR.get();
return air();
}
@Override
@@ -126,6 +134,32 @@ public final class SectionBuffer implements Hunk<NativeBlockState>, 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<NativeBlockState>, 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) {
@@ -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<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 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;
}
}
}
@@ -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<NativeBlockState>
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<NativeBlockState>
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;
@@ -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<NativeBlockSt
StateFlags states = new StateFlags();
NativeBlockState air = AIR.get();
NativeBlockState water = WATER.get();
NativeBlockState[] column = new NativeBlockState[output.getHeight()];
for (int x = firstSlice; x < lastSlice; x++) {
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,
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<NativeBlockSt
if ((flags & SPIKE) != 0) {
block = normalizeSpike(block, output, x, z, y, air, water);
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;
@@ -233,11 +241,11 @@ public class IrisPerfectionModifier extends EngineAssignedModifier<NativeBlockSt
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;
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;
}
@@ -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<NativeBlockState, BoundaryColumnGeometry.Voxel> 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;
}
@@ -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<Matter> 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;
@@ -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
@@ -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<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) {
assertEquals(expected.sectionCount(), actual.sectionCount());
NativeBlockState[] expectedPalette = expected.palette();
@@ -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<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
public void scopeRebindsWorkerWhenEngineChanges() throws Exception {
Engine firstEngine = mock(Engine.class);