mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-11-01 17:00:26 +00:00
Merge branch 'perf-gen' into perf-review
This commit is contained in:
commit
b475d20aec
14 files changed
+405
-45
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;
|
||||
}
|
||||
|
||||
+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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
while (!pending.settled) {
|
||||
if (!semanticWriter.compareAndSet(false, true)) {
|
||||
LockSupport.parkNanos(pending, SEMANTIC_CLAIM_PARK_NANOS);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
synchronized (this) {
|
||||
while (!pending.claim.completed) {
|
||||
persistPendingSemanticClaims();
|
||||
}
|
||||
return pending.claim.result();
|
||||
}
|
||||
} finally {
|
||||
semanticWriter.set(false);
|
||||
PendingSemanticClaim next = pendingSemanticClaims.peek();
|
||||
if (next != null) {
|
||||
LockSupport.unpark(next.owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
while (!pending.settled) {
|
||||
if (!lock.writer.compareAndSet(false, true)) {
|
||||
LockSupport.parkNanos(pending, WRITE_PARK_NANOS);
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
+17
-11
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user