Merge branch 'perf-gen' into perf-review

This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-30 21:42:42 -04:00
commit b475d20aec
14 files changed
+413 -53

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,21 @@ 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();
ColumnWindow column = new ColumnWindow(output, 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);
column.begin(localX, localZ, top);
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.get(y));
boolean decorated = false; boolean decorated = false;
if (solid) { if (solid) {
if (floor >= 0) { if (floor >= 0) {
@@ -1122,11 +1125,12 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
if (extent != null) { if (extent != null) {
top = extent.highestStoredY(localX, localZ); top = extent.highestStoredY(localX, localZ);
} }
column.begin(localX, localZ, top);
} }
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.get(y - 1)))) {
floor = y; floor = y;
} }
previousSolid = solid; previousSolid = solid;
@@ -1472,6 +1476,43 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
private record CaveColumn(long key, IrisBiome surface, IrisBiome cave, int surfaceY, int minimumDepth) { 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<NativeBlockState> output;
private final NativeBlockState[] states;
private int x;
private int z;
private int limit;
private int from;
private int to;
private ColumnWindow(Hunk<NativeBlockState> 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 static final class SolidMemo {
private NativeBlockState state; private NativeBlockState state;
private boolean solid; private boolean 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,29 @@ 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;
boolean columnRead = true;
for (int y = top; y >= 0; y--) { 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; int flags = 0;
if (block != null) { if (block != null) {
if (block != classified) { if (block != classified) {
@@ -133,6 +139,7 @@ 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);
columnRead = false;
} }
} }
boolean now = (flags & SOLID) != 0; boolean now = (flags & SOLID) != 0;
@@ -233,11 +240,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;
} }
@@ -26,6 +26,8 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;
import java.util.function.Predicate; import java.util.function.Predicate;
public final class GenerationHistory { 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_CACHED_TRANSITION_PLANS = 4;
private static final int MAXIMUM_SEMANTIC_BATCH = 32; private static final int MAXIMUM_SEMANTIC_BATCH = 32;
private static final int MAXIMUM_PENDING_SEMANTIC_CLAIMS = 128; 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 GenerationHistoryPaths paths;
private final GenerationPackRepository packs; private final GenerationPackRepository packs;
@@ -50,6 +53,7 @@ public final class GenerationHistory {
private final Map<Long, TransitionGenerationPlan> transitionPlanCache; private final Map<Long, TransitionGenerationPlan> transitionPlanCache;
private final ArrayBlockingQueue<PendingSemanticClaim> pendingSemanticClaims = private final ArrayBlockingQueue<PendingSemanticClaim> pendingSemanticClaims =
new ArrayBlockingQueue<>(MAXIMUM_PENDING_SEMANTIC_CLAIMS); new ArrayBlockingQueue<>(MAXIMUM_PENDING_SEMANTIC_CLAIMS);
private final AtomicBoolean semanticWriter = new AtomicBoolean();
private GenerationHistory( private GenerationHistory(
GenerationHistoryPaths paths, GenerationHistoryPaths paths,
@@ -776,12 +780,26 @@ public final class GenerationHistory {
persistPendingSemanticClaims(); persistPendingSemanticClaims();
} }
} }
synchronized (this) { while (!pending.settled) {
while (!pending.claim.completed) { if (!semanticWriter.compareAndSet(false, true)) {
persistPendingSemanticClaims(); 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() { private void persistPendingSemanticClaims() {
@@ -814,10 +832,15 @@ public final class GenerationHistory {
} }
while ((current = pendingSemanticClaims.poll()) != null) { while ((current = pendingSemanticClaims.poll()) != null) {
reported |= current.claim.fail(failure); reported |= current.claim.fail(failure);
current.settle();
} }
if (!reported) { if (!reported) {
throw failure; throw failure;
} }
} finally {
for (PendingSemanticClaim pending : batch) {
pending.settle();
}
} }
} }
@@ -1354,11 +1377,21 @@ public final class GenerationHistory {
private static final class PendingSemanticClaim { private static final class PendingSemanticClaim {
private final GenerationStage stage; private final GenerationStage stage;
private final GenerationSemanticIndex.Claim claim; private final GenerationSemanticIndex.Claim claim;
private final Thread owner = Thread.currentThread();
private volatile boolean settled;
private PendingSemanticClaim(GenerationStage stage, ChunkGenerationSemantics update) { private PendingSemanticClaim(GenerationStage stage, ChunkGenerationSemantics update) {
this.stage = stage; this.stage = stage;
this.claim = new GenerationSemanticIndex.Claim(update); 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 { public static final class GenerationStage implements AutoCloseable {
@@ -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;
@@ -29,6 +29,8 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue; 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.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.zip.CRC32; import java.util.zip.CRC32;
@@ -49,6 +51,7 @@ public final class SavedBiomeStore {
private static final int MAXIMUM_CACHED_CHUNKS = 128; private static final int MAXIMUM_CACHED_CHUNKS = 128;
private static final long MAXIMUM_CACHED_BYTES = 64L * 1024L * 1024L; private static final long MAXIMUM_CACHED_BYTES = 64L * 1024L * 1024L;
private static final int MAXIMUM_WRITE_BATCH = 64; private static final int MAXIMUM_WRITE_BATCH = 64;
private static final long WRITE_PARK_NANOS = 10_000_000L;
private static final ThreadLocal<Deflater> DEFLATERS = ThreadLocal.withInitial(() -> new Deflater(Deflater.BEST_SPEED)); private static final ThreadLocal<Deflater> DEFLATERS = ThreadLocal.withInitial(() -> new Deflater(Deflater.BEST_SPEED));
private final Path dimensionRoot; private final Path dimensionRoot;
@@ -132,19 +135,34 @@ public final class SavedBiomeStore {
.putInt(body.length).put(body).putInt(checksum(body)).array(); .putInt(body.length).put(body).putInt(checksum(body)).array();
PendingWrite pending = new PendingWrite(required, record); PendingWrite pending = new PendingWrite(required, record);
lock.pending.add(pending); lock.pending.add(pending);
synchronized (lock) { while (!pending.settled) {
while (!pending.completed) { if (!lock.writer.compareAndSet(false, true)) {
persistPending(lock); LockSupport.parkNanos(pending, WRITE_PARK_NANOS);
continue;
} }
if (pending.failure != null) { try {
throw pending.failure; 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) { private void persistPending(WriteStripe stripe) {
Map<Long, List<PendingWrite>> batches = new LinkedHashMap<>(); Map<Long, List<PendingWrite>> batches = new LinkedHashMap<>();
List<PendingWrite> polled = new ArrayList<>(MAXIMUM_WRITE_BATCH);
PendingWrite grouping = null; PendingWrite grouping = null;
try { try {
for (int count = 0; count < MAXIMUM_WRITE_BATCH; count++) { for (int count = 0; count < MAXIMUM_WRITE_BATCH; count++) {
@@ -152,6 +170,7 @@ public final class SavedBiomeStore {
if (grouping == null) { if (grouping == null) {
break; break;
} }
polled.add(grouping);
long regionKey = key(grouping.chunk.chunkX() >> 5, grouping.chunk.chunkZ() >> 5); long regionKey = key(grouping.chunk.chunkX() >> 5, grouping.chunk.chunkZ() >> 5);
batches.computeIfAbsent(regionKey, ignored -> new ArrayList<>()).add(grouping); batches.computeIfAbsent(regionKey, ignored -> new ArrayList<>()).add(grouping);
grouping = null; grouping = null;
@@ -175,6 +194,10 @@ public final class SavedBiomeStore {
} }
} }
throw failure; throw failure;
} finally {
for (PendingWrite pending : polled) {
pending.settle();
}
} }
} }
@@ -822,19 +845,30 @@ public final class SavedBiomeStore {
private static final class WriteStripe { private static final class WriteStripe {
private final ConcurrentLinkedQueue<PendingWrite> pending = new ConcurrentLinkedQueue<>(); private final ConcurrentLinkedQueue<PendingWrite> pending = new ConcurrentLinkedQueue<>();
private final AtomicBoolean writer = new AtomicBoolean();
} }
private static final class PendingWrite { private static final class PendingWrite {
private final SavedBiomeChunk chunk; private final SavedBiomeChunk chunk;
private final byte[] record; private final byte[] record;
private final Thread owner = Thread.currentThread();
private boolean completed; private boolean completed;
private boolean persisted; private boolean persisted;
private IOException failure; private IOException failure;
private volatile boolean settled;
private PendingWrite(SavedBiomeChunk chunk, byte[] record) { private PendingWrite(SavedBiomeChunk chunk, byte[] record) {
this.chunk = chunk; this.chunk = chunk;
this.record = record; this.record = record;
} }
private void settle() {
if (!completed || settled) {
return;
}
settled = true;
LockSupport.unpark(owner);
}
} }
private static final class DecodeBudget { private static final class DecodeBudget {
@@ -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);
@@ -250,7 +250,7 @@ public class GenerationHistoryAdmissionTest {
.addSurfaceBiome("iris:conflict").seal().build() .addSurfaceBiome("iris:conflict").seal().build()
: claim(stage); : claim(stage);
results.add(executor.submit(() -> persistCounting(history, stage, update, appends, forces))); results.add(executor.submit(() -> persistCounting(history, stage, update, appends, forces)));
awaitBlockedOnHistory(history, workers, index + 1); awaitQueuedOnHistory(history, workers, index + 1);
} }
stages.get(5).close(); stages.get(5).close();
} }
@@ -340,7 +340,7 @@ public class GenerationHistoryAdmissionTest {
for (int index = 0; index < stages.size(); index++) { for (int index = 0; index < stages.size(); index++) {
GenerationHistory.GenerationStage stage = stages.get(index); GenerationHistory.GenerationStage stage = stages.get(index);
results.add(executor.submit(() -> persistFailingRegion(history, stage, failure))); 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)); assertTrue(results.getFirst().get(5, TimeUnit.SECONDS));
@@ -404,21 +404,27 @@ public class GenerationHistoryAdmissionTest {
} }
} }
private static void awaitBlockedOnHistory(GenerationHistory history, List<Thread> workers, int count) { private static void awaitQueuedOnHistory(GenerationHistory history, List<Thread> workers, int count) {
Await.until("semantic claim callers to queue behind the History monitor", Duration.ofSeconds(5L), Await.until("semantic claim callers to queue behind the History writer", Duration.ofSeconds(5L),
() -> blockedOnHistory(history, workers) == count); () -> queuedOnHistory(history, workers) == count);
} }
private static int blockedOnHistory(GenerationHistory history, List<Thread> workers) { private static int queuedOnHistory(GenerationHistory history, List<Thread> workers) {
int blocked = 0; int queued = 0;
for (Thread worker : workers) { for (Thread worker : workers) {
ThreadInfo info = ManagementFactory.getThreadMXBean().getThreadInfo(worker.threadId()); ThreadInfo info = ManagementFactory.getThreadMXBean().getThreadInfo(worker.threadId());
if (info != null && info.getThreadState() == Thread.State.BLOCKED if (info == null || info.getLockInfo() == null) {
&& info.getLockInfo().getIdentityHashCode() == System.identityHashCode(history)) { continue;
blocked++; }
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, private static boolean persistPaused(GenerationHistory history, GenerationHistory.GenerationStage stage,