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