mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-11-01 08:50:06 +00:00
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:
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;
|
||||
}
|
||||
|
||||
+12
-3
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user