Pregen optimization pass

- Hydrology height lattice stores samples in tiles with least-recently-used eviction
- Evicted lattice cells are no longer sampled twice
- Pond rim columns go through the lattice column cache
- Window planning reads its coarse nodes from the fine lattice
- Prefetch stays inside the pregeneration area while a pregeneration runs
- Sea inlet splat compares squared distances
- Bounds sampling answers a column from region constants when every reachable biome shares the bounds
- Base terrain heights are shared through a per-column memo
- Chunk water rasters use per-chunk palettes and a larger cache
- Raster channel lookups use a 4-block segment grid
- Chunks with no water in reach return the empty raster directly
- Riffle spreads stop at cells that cannot reach the chunk
- Cave pool terrain is built only for chunks with a pool site
- Concurrent raster builds for one chunk share a single build
- Section buffers resolve air once and support bulk column reads
- Cave decoration, perfection and transition capture read bounded column windows
- Generation history claims and saved biome writes elect one writer per batch
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-30 22:34:31 -04:00
commit 6ceda7c53b
55 files changed
+1968 -481

No files matched your search

@@ -348,6 +348,11 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
return false;
}
@Override
public void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
hydrology.bound(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
}
@Override
public void onRegionsAhead(List<long[]> regions) {
if (!closing.get()) {
@@ -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;
}
}
}
@@ -1,53 +0,0 @@
package art.arcane.iris.generation.hydrology.engine;
import art.arcane.iris.generation.hydrology.grid.HeightLattice;
import art.arcane.iris.generation.hydrology.grid.HydrologySampleBuffer;
import art.arcane.iris.generation.hydrology.grid.HydrologyTerrainSource;
import java.util.Objects;
final class FineLatticeSource implements HydrologyTerrainSource {
private final HeightLattice fine;
private final HydrologyTerrainSource terrain;
FineLatticeSource(HeightLattice fine, HydrologyTerrainSource terrain) {
this.fine = Objects.requireNonNull(fine, "fine");
this.terrain = Objects.requireNonNull(terrain, "terrain");
}
@Override
public void sample(int[] xs, int[] zs, HydrologySampleBuffer out, boolean baseOnly) {
if (baseOnly) {
throw new IllegalArgumentException("The fine lattice holds natural heights only");
}
if (xs.length != zs.length) {
throw new IllegalArgumentException("Fine lattice read has " + xs.length + " x and " + zs.length + " z coordinates");
}
int cell = fine.cell();
int[] cellXs = new int[xs.length];
int[] cellZs = new int[zs.length];
for (int index = 0; index < xs.length; index++) {
if (Math.floorMod(xs[index], cell) != 0 || Math.floorMod(zs[index], cell) != 0) {
throw new IllegalArgumentException("Column " + xs[index] + "," + zs[index] + " is not a node of the " + cell + "-block fine lattice");
}
cellXs[index] = Math.floorDiv(xs[index], cell);
cellZs[index] = Math.floorDiv(zs[index], cell);
}
fine.fill(cellXs, cellZs, out);
}
@Override
public int seaLevel() {
return terrain.seaLevel();
}
@Override
public int minY() {
return terrain.minY();
}
@Override
public int maxY() {
return terrain.maxY();
}
}
@@ -11,23 +11,21 @@ import art.arcane.iris.generation.hydrology.water.ReachShaper;
import java.util.Objects;
public record HydrologyPlanners(RegionPlanner regions, WindowPlanner windows, HeightLattice windowLattice) {
public record HydrologyPlanners(RegionPlanner regions, WindowPlanner windows, HeightLattice fineLattice) {
public HydrologyPlanners {
Objects.requireNonNull(regions, "regions");
Objects.requireNonNull(windows, "windows");
Objects.requireNonNull(windowLattice, "windowLattice");
Objects.requireNonNull(fineLattice, "fineLattice");
}
public static HydrologyPlanners create(HydrologySettings settings, HydrologyTerrainSource source) {
Objects.requireNonNull(settings, "settings");
Objects.requireNonNull(source, "source");
HeightLattice fine = new HeightLattice(source, WindowPlanner.FINE_CELL, false, HydrologyPlanCache.FINE_LATTICE_CELLS);
HeightLattice window = new HeightLattice(new FineLatticeSource(fine, source), WindowPlanner.CELL, false,
HydrologyPlanCache.WINDOW_LATTICE_CELLS);
PondPlanner ponds = new PondPlanner(settings);
ReachShaper shaper = new ReachShaper(settings);
RegionPlanner regions = new RegionPlanner(settings,
new HeightLattice(source, RegionPlanner.CELL, true, HydrologyPlanCache.REGION_LATTICE_CELLS), fine, ponds, shaper);
return new HydrologyPlanners(regions, new WindowPlanner(settings, window, fine, ponds, shaper), window);
return new HydrologyPlanners(regions, new WindowPlanner(settings, fine, ponds, shaper), fine);
}
}
@@ -18,6 +18,12 @@ public final class HydrologyPregeneration implements AutoCloseable {
}
}
public synchronized void bound(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
if (!closed) {
runtime.boundPregeneration(epoch, minRegionX, minRegionZ, maxRegionX, maxRegionZ);
}
}
@Override
public synchronized void close() {
if (!closed) {
@@ -36,6 +36,7 @@ import java.io.UncheckedIOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
@@ -56,7 +57,7 @@ import java.util.function.Predicate;
import java.util.function.Supplier;
public final class HydrologyRuntime {
private static final int RASTER_CAPACITY = 1024;
private static final int RASTER_CAPACITY = 8192;
private static final int KEEP_SCOPES = 2;
private static final int CHUNKS_PER_WINDOW = WindowKey.SIZE >> 4;
public static final int MAX_PREGENERATION_REGIONS = 4096;
@@ -72,11 +73,13 @@ public final class HydrologyRuntime {
private final RasterLru rasters;
private final ThreadLocal<LastRaster> last;
private final Set<Long> queued;
private final ConcurrentHashMap<Long, CompletableFuture<ChunkWaterRaster>> building;
private final RasterFailures failures;
private final RasterFailures windowPreviewFailures;
private final RasterFailures regionPreviewFailures;
private final Set<Object> previews;
private final LinkedHashMap<WindowKey, WindowPrefetch> prefetches;
private final Map<Object, PregenArea> pregenAreas;
private final HydrologyBinding binding;
private final Function<PreparedHydrologyCacheIdentity, HydrologyPlanStore> stores;
private final boolean caveFluidPolicies;
@@ -94,11 +97,13 @@ public final class HydrologyRuntime {
this.rasters = null;
this.last = null;
this.queued = null;
this.building = null;
this.failures = null;
this.windowPreviewFailures = null;
this.regionPreviewFailures = null;
this.previews = null;
this.prefetches = null;
this.pregenAreas = null;
this.binding = HydrologyBinding.UNBOUND;
this.stores = identity -> null;
this.caveFluidPolicies = false;
@@ -120,11 +125,13 @@ public final class HydrologyRuntime {
this.rasters = new RasterLru();
this.last = ThreadLocal.withInitial(LastRaster::new);
this.queued = ConcurrentHashMap.newKeySet();
this.building = new ConcurrentHashMap<>();
this.failures = new RasterFailures(ticker);
this.windowPreviewFailures = new RasterFailures(ticker);
this.regionPreviewFailures = new RasterFailures(ticker);
this.previews = new HashSet<>();
this.prefetches = new LinkedHashMap<>(PREFETCH_CAPACITY * 2, 0.75f, true);
this.pregenAreas = new HashMap<>();
this.binding = Objects.requireNonNull(binding, "binding");
this.stores = Objects.requireNonNull(stores, "stores");
this.caveFluidPolicies = caveFluidPolicies;
@@ -234,7 +241,7 @@ public final class HydrologyRuntime {
requireOpen();
long key = key(chunkX, chunkZ);
ChunkWaterRaster known = remembered(key, slot);
return known != null ? known : remember(key, planned(chunkX, chunkZ, key));
return known != null ? known : build(chunkX, chunkZ, key, slot);
}
public ChunkWaterRaster rasterSnapshot(int chunkX, int chunkZ) {
@@ -354,8 +361,28 @@ public final class HydrologyRuntime {
}
}
void boundPregeneration(Object epoch, int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
Objects.requireNonNull(epoch, "epoch");
if (maxRegionX < minRegionX || maxRegionZ < minRegionZ) {
throw new IllegalArgumentException("Invalid pregeneration bounds " + minRegionX + "," + minRegionZ + " to " + maxRegionX + ","
+ maxRegionZ);
}
if (cache == null) {
return;
}
synchronized (prefetches) {
pregenAreas.put(epoch, new PregenArea(minRegionX - 1, minRegionZ - 1, maxRegionX + 1, maxRegionZ + 1));
prefetches.clear();
}
}
void cancelPregeneration(Object epoch) {
if (cache != null) {
synchronized (prefetches) {
if (pregenAreas.remove(epoch) != null) {
prefetches.clear();
}
}
cache.cancel(epoch);
}
}
@@ -501,6 +528,41 @@ public final class HydrologyRuntime {
}
}
private ChunkWaterRaster build(int chunkX, int chunkZ, long key, LastRaster slot) {
CompletableFuture<ChunkWaterRaster> claim = new CompletableFuture<>();
CompletableFuture<ChunkWaterRaster> running = building.putIfAbsent(key, claim);
if (running != null) {
ChunkWaterRaster built = built(running);
slot.remember(key, built);
return built;
}
try {
ChunkWaterRaster known = remembered(key, slot);
ChunkWaterRaster raster = known != null ? known : remember(key, planned(chunkX, chunkZ, key));
claim.complete(raster);
return raster;
} catch (RuntimeException | Error failure) {
claim.completeExceptionally(failure);
throw failure;
} finally {
building.remove(key, claim);
}
}
private static ChunkWaterRaster built(CompletableFuture<ChunkWaterRaster> running) {
try {
return running.join();
} catch (CompletionException failure) {
if (failure.getCause() instanceof RuntimeException cause) {
throw cause;
}
if (failure.getCause() instanceof Error cause) {
throw cause;
}
throw failure;
}
}
private ChunkWaterRaster planned(int chunkX, int chunkZ, long chunk) {
try {
List<WindowKey> windowKeys = neighbourhood(chunkX, chunkZ);
@@ -667,7 +729,7 @@ public final class HydrologyRuntime {
&& (!known.prepared().isDone() || known.evictions() == evictions)) {
return;
}
CompletableFuture<Void> prepared = cache.prepareWindowsAround(chunkX, chunkZ, chunkX, chunkZ, 1 + settings.prefetchRings());
CompletableFuture<Void> prepared = prepareAround(chunkX, chunkZ, owner, 1 + settings.prefetchRings());
prefetches.put(owner, new WindowPrefetch(prepared, evictions));
if (prefetches.size() > PREFETCH_CAPACITY) {
prefetches.pollFirstEntry();
@@ -675,6 +737,24 @@ public final class HydrologyRuntime {
}
}
private CompletableFuture<Void> prepareAround(int chunkX, int chunkZ, WindowKey owner, int rings) {
PregenArea covering = null;
for (PregenArea area : pregenAreas.values()) {
if (area.contains(owner)) {
covering = covering == null ? area : covering.union(area);
}
}
if (covering == null) {
return cache.prepareWindowsAround(chunkX, chunkZ, chunkX, chunkZ, rings);
}
int minX = Math.max(owner.wx() - rings, covering.minX());
int minZ = Math.max(owner.wz() - rings, covering.minZ());
int maxX = Math.min(owner.wx() + rings, covering.maxX());
int maxZ = Math.min(owner.wz() + rings, covering.maxZ());
return cache.prepareWindowsAround(minX * CHUNKS_PER_WINDOW, minZ * CHUNKS_PER_WINDOW, (maxX + 1) * CHUNKS_PER_WINDOW - 1,
(maxZ + 1) * CHUNKS_PER_WINDOW - 1, 0);
}
private static List<WindowKey> neighbourhood(int chunkX, int chunkZ) {
WindowKey own = WindowKey.containing(chunkX << 4, chunkZ << 4);
List<WindowKey> keys = new ArrayList<>(9);
@@ -754,6 +834,16 @@ public final class HydrologyRuntime {
private record WindowPrefetch(CompletableFuture<Void> prepared, long evictions) {
}
private record PregenArea(int minX, int minZ, int maxX, int maxZ) {
private boolean contains(WindowKey key) {
return key.wx() > minX && key.wx() < maxX && key.wz() > minZ && key.wz() < maxZ;
}
private PregenArea union(PregenArea other) {
return new PregenArea(Math.min(minX, other.minX), Math.min(minZ, other.minZ), Math.max(maxX, other.maxX), Math.max(maxZ, other.maxZ));
}
}
private static final class LastRaster {
private long key;
private ChunkWaterRaster raster;
@@ -1,34 +1,59 @@
package art.arcane.iris.generation.hydrology.grid;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReferenceArray;
public final class HeightLattice {
private static final int TILE_SHIFT = 5;
private static final int COLUMN_TILE_SHIFT = 3;
private static final int COLUMN_CELLS = 1 << 20;
private static final CompletableFuture<Void> READY = CompletableFuture.completedFuture(null);
private static final int[] NO_NODES = new int[0];
private static final Tile[] NO_TILES = new Tile[0];
private final HydrologyTerrainSource source;
private final int cell;
private final boolean baseOnly;
private final int capacityCells;
private final ConcurrentHashMap<Long, Slot> cells;
private final ConcurrentLinkedQueue<Long> insertionOrder;
private final int tileShift;
private final int tileMask;
private final int tileCells;
private final int capacityTiles;
private final ConcurrentHashMap<Long, Tile> tiles;
private final AtomicLong clock;
private final AtomicBoolean evicting;
private final HeightLattice columns;
public HeightLattice(HydrologyTerrainSource source, int cell, boolean baseOnly, int capacityCells) {
this(source, cell, baseOnly, capacityCells, TILE_SHIFT, new HeightLattice(source, 1, baseOnly, COLUMN_CELLS, COLUMN_TILE_SHIFT, null));
}
private HeightLattice(HydrologyTerrainSource source, int cell, boolean baseOnly, int capacityCells, int tileShift, HeightLattice columns) {
if (cell <= 0 || capacityCells <= 0) {
throw new IllegalArgumentException("Invalid lattice cell=" + cell + " capacityCells=" + capacityCells);
}
this.source = Objects.requireNonNull(source, "source");
this.cell = cell;
this.baseOnly = baseOnly;
this.capacityCells = capacityCells;
this.cells = new ConcurrentHashMap<>();
this.insertionOrder = new ConcurrentLinkedQueue<>();
this.tileShift = tileShift;
this.tileMask = (1 << tileShift) - 1;
this.tileCells = 1 << (2 * tileShift);
this.capacityTiles = Math.ceilDiv(capacityCells, tileCells);
this.tiles = new ConcurrentHashMap<>();
this.clock = new AtomicLong();
this.evicting = new AtomicBoolean();
this.columns = columns;
}
public int cell() {
@@ -67,19 +92,24 @@ public final class HeightLattice {
throw new IllegalArgumentException("Column sample has " + xs.length + " x and " + zs.length + " z coordinates");
}
requireCapacity(out, xs.length);
if (xs.length > 0) {
source.sample(xs, zs, out, baseOnly);
if (xs.length == 0) {
return;
}
if (columns == null) {
source.sample(xs, zs, out, baseOnly);
return;
}
columns.resolve(new Scatter(xs, zs), xs.length, out);
}
private void resolve(Nodes nodes, int count, HydrologySampleBuffer out) {
Slot[] slots = new Slot[count];
int unresolved = count;
while (unresolved > 0) {
CompletableFuture<HydrologySampleBuffer> batch = claimAndSample(nodes, slots);
unresolved = awaitForeign(batch, slots);
Pass pass = new Pass(nodes, out, clock.incrementAndGet());
int[] open = null;
int remaining = count;
while (remaining > 0) {
open = pass.run(open, remaining);
remaining = open == null ? 0 : open.length;
}
copy(slots, out);
evictOverflow();
}
@@ -94,130 +124,33 @@ public final class HeightLattice {
Math.multiplyExact(Math.addExact(cell0, Math.multiplyExact(extent - 1, stride)), cell);
}
private CompletableFuture<HydrologySampleBuffer> claimAndSample(Nodes nodes, Slot[] slots) {
CompletableFuture<HydrologySampleBuffer> batch = new CompletableFuture<>();
try {
int[] xs = new int[slots.length];
int[] zs = new int[slots.length];
int claimed = claim(batch, nodes, slots, xs, zs);
if (claimed > 0) {
HydrologySampleBuffer samples = new HydrologySampleBuffer(claimed);
source.sample(Arrays.copyOf(xs, claimed), Arrays.copyOf(zs, claimed), samples, baseOnly);
batch.complete(samples);
}
} catch (Throwable failure) {
try {
release(batch, nodes, slots);
} finally {
batch.completeExceptionally(failure);
}
throw failure;
}
return batch;
}
private int claim(CompletableFuture<HydrologySampleBuffer> batch, Nodes nodes, Slot[] slots, int[] xs, int[] zs) {
int claimed = 0;
for (int index = 0; index < slots.length; index++) {
if (slots[index] != null) {
continue;
}
int cellX = nodes.cellX(index);
int cellZ = nodes.cellZ(index);
Long key = key(cellX, cellZ);
Slot existing = cells.get(key);
if (existing == null) {
Slot fresh = new Slot(batch, claimed);
existing = cells.putIfAbsent(key, fresh);
if (existing == null) {
slots[index] = fresh;
insertionOrder.offer(key);
xs[claimed] = cellX * cell;
zs[claimed] = cellZ * cell;
claimed++;
continue;
}
}
slots[index] = existing;
}
return claimed;
}
private void release(CompletableFuture<HydrologySampleBuffer> batch, Nodes nodes, Slot[] slots) {
for (int index = 0; index < slots.length; index++) {
Slot slot = slots[index];
if (slot != null && slot.batch() == batch) {
cells.remove(key(nodes.cellX(index), nodes.cellZ(index)), slot);
}
}
}
private static int awaitForeign(CompletableFuture<HydrologySampleBuffer> own, Slot[] slots) {
int unresolved = 0;
for (int index = 0; index < slots.length; index++) {
CompletableFuture<HydrologySampleBuffer> batch = slots[index].batch();
if (batch != own && !await(batch)) {
slots[index] = null;
unresolved++;
}
}
return unresolved;
}
private static boolean await(CompletableFuture<HydrologySampleBuffer> batch) {
if (!batch.isDone()) {
try {
batch.get();
} catch (ExecutionException | CancellationException failure) {
return false;
} catch (InterruptedException interruption) {
Thread.currentThread().interrupt();
throw new CancellationException("Hydrology lattice wait interrupted.");
}
}
return !batch.isCompletedExceptionally();
}
private static void copy(Slot[] slots, HydrologySampleBuffer out) {
float[] height = out.height();
boolean[] ocean = out.ocean();
float[] rainfall = out.rainfall();
float[] pondDensity = out.pondDensity();
byte[] permission = out.permission();
String[] fluidProfile = out.fluidProfile();
for (int index = 0; index < slots.length; index++) {
Slot slot = slots[index];
HydrologySampleBuffer samples = slot.batch().join();
int sample = slot.index();
height[index] = samples.height()[sample];
ocean[index] = samples.ocean()[sample];
rainfall[index] = samples.rainfall()[sample];
pondDensity[index] = samples.pondDensity()[sample];
permission[index] = samples.permission()[sample];
fluidProfile[index] = samples.fluidProfile()[sample];
private Tile tile(int tileX, int tileZ) {
Long key = key(tileX, tileZ);
Tile known = tiles.get(key);
if (known != null) {
return known;
}
return tiles.computeIfAbsent(key, ignored -> new Tile(tileCells));
}
private void evictOverflow() {
if (cells.size() <= capacityCells || !evicting.compareAndSet(false, true)) {
if (tiles.size() <= capacityTiles || !evicting.compareAndSet(false, true)) {
return;
}
try {
int excess = cells.size() - capacityCells;
while (excess > 0) {
Long key = insertionOrder.poll();
if (key == null) {
return;
List<Map.Entry<Long, Tile>> idle = new ArrayList<>(tiles.size());
for (Map.Entry<Long, Tile> entry : tiles.entrySet()) {
if (entry.getValue().pending.get() == 0) {
idle.add(entry);
}
Slot slot = cells.get(key);
if (slot == null) {
continue;
}
if (!slot.batch().isDone()) {
insertionOrder.offer(key);
excess--;
} else if (cells.remove(key, slot)) {
excess--;
}
idle.sort(Comparator.comparingLong(entry -> entry.getValue().used));
int target = capacityTiles - (capacityTiles >> 3);
for (int index = 0; index < idle.size() && tiles.size() > target; index++) {
Map.Entry<Long, Tile> entry = idle.get(index);
Tile tile = entry.getValue();
if (tile.pending.get() == 0) {
tiles.remove(entry.getKey(), tile);
}
}
} finally {
@@ -232,7 +165,219 @@ public final class HeightLattice {
return mixed ^ (mixed >>> 31);
}
private record Slot(CompletableFuture<HydrologySampleBuffer> batch, int index) {
private static boolean await(CompletableFuture<Void> batch) {
if (!batch.isDone()) {
try {
batch.get();
} catch (ExecutionException | CancellationException failure) {
return false;
} catch (InterruptedException interruption) {
Thread.currentThread().interrupt();
throw new CancellationException("Hydrology lattice wait interrupted.");
}
}
return !batch.isCompletedExceptionally();
}
private static void copy(HydrologySampleBuffer samples, int sample, HydrologySampleBuffer out, int index) {
out.height()[index] = samples.height()[sample];
out.ocean()[index] = samples.ocean()[sample];
out.rainfall()[index] = samples.rainfall()[sample];
out.pondDensity()[index] = samples.pondDensity()[sample];
out.permission()[index] = samples.permission()[sample];
out.fluidProfile()[index] = samples.fluidProfile()[sample];
}
private final class Pass {
private final Nodes nodes;
private final HydrologySampleBuffer out;
private final long stamp;
private int[] claimedIndex = NO_NODES;
private Tile[] claimedTile = NO_TILES;
private int[] claimedLocal = NO_NODES;
private int claimed;
private int[] waitingIndex = NO_NODES;
private Tile[] waitingTile = NO_TILES;
private int[] waitingLocal = NO_NODES;
private int waiting;
private Tile current;
private int currentX;
private int currentZ;
private Pass(Nodes nodes, HydrologySampleBuffer out, long stamp) {
this.nodes = nodes;
this.out = out;
this.stamp = stamp;
}
private int[] run(int[] open, int remaining) {
claimed = 0;
waiting = 0;
CompletableFuture<Void> batch = new CompletableFuture<>();
for (int position = 0; position < remaining; position++) {
visit(open == null ? position : open[position], batch);
}
if (claimed > 0) {
sample(batch);
}
return awaitWaiting();
}
private void visit(int index, CompletableFuture<Void> batch) {
int cellX = nodes.cellX(index);
int cellZ = nodes.cellZ(index);
Tile tile = tileOf(cellX >> tileShift, cellZ >> tileShift);
int local = ((cellZ & tileMask) << tileShift) | (cellX & tileMask);
while (true) {
CompletableFuture<Void> mark = tile.marks.get(local);
if (mark == READY) {
tile.copy(local, out, index);
return;
}
if (mark != null) {
addWaiting(index, tile, local);
return;
}
if (tile.marks.compareAndSet(local, null, batch)) {
tile.pending.incrementAndGet();
addClaimed(index, tile, local);
return;
}
}
}
private Tile tileOf(int tileX, int tileZ) {
if (current == null || currentX != tileX || currentZ != tileZ) {
current = tile(tileX, tileZ);
currentX = tileX;
currentZ = tileZ;
current.used = stamp;
}
return current;
}
private void sample(CompletableFuture<Void> batch) {
try {
int[] xs = new int[claimed];
int[] zs = new int[claimed];
for (int position = 0; position < claimed; position++) {
xs[position] = nodes.cellX(claimedIndex[position]) * cell;
zs[position] = nodes.cellZ(claimedIndex[position]) * cell;
}
HydrologySampleBuffer samples = new HydrologySampleBuffer(claimed);
source.sample(xs, zs, samples, baseOnly);
for (int position = 0; position < claimed; position++) {
Tile tile = claimedTile[position];
int local = claimedLocal[position];
tile.store(local, samples, position);
tile.marks.set(local, READY);
tile.pending.decrementAndGet();
copy(samples, position, out, claimedIndex[position]);
}
batch.complete(null);
} catch (Throwable failure) {
try {
release(batch);
} finally {
batch.completeExceptionally(failure);
}
throw failure;
}
}
private void release(CompletableFuture<Void> batch) {
for (int position = 0; position < claimed; position++) {
Tile tile = claimedTile[position];
if (tile.marks.compareAndSet(claimedLocal[position], batch, null)) {
tile.pending.decrementAndGet();
}
}
}
private int[] awaitWaiting() {
int[] open = null;
int unresolved = 0;
for (int position = 0; position < waiting; position++) {
Tile tile = waitingTile[position];
int local = waitingLocal[position];
CompletableFuture<Void> mark = tile.marks.get(local);
if (mark != READY && (mark == null || !await(mark) || tile.marks.get(local) != READY)) {
if (open == null) {
open = new int[waiting - position];
}
open[unresolved++] = waitingIndex[position];
continue;
}
tile.copy(local, out, waitingIndex[position]);
}
return open == null ? null : Arrays.copyOf(open, unresolved);
}
private void addClaimed(int index, Tile tile, int local) {
if (claimed == claimedIndex.length) {
int grown = Math.max(16, claimed * 2);
claimedIndex = Arrays.copyOf(claimedIndex, grown);
claimedTile = Arrays.copyOf(claimedTile, grown);
claimedLocal = Arrays.copyOf(claimedLocal, grown);
}
claimedIndex[claimed] = index;
claimedTile[claimed] = tile;
claimedLocal[claimed] = local;
claimed++;
}
private void addWaiting(int index, Tile tile, int local) {
if (waiting == waitingIndex.length) {
int grown = Math.max(16, waiting * 2);
waitingIndex = Arrays.copyOf(waitingIndex, grown);
waitingTile = Arrays.copyOf(waitingTile, grown);
waitingLocal = Arrays.copyOf(waitingLocal, grown);
}
waitingIndex[waiting] = index;
waitingTile[waiting] = tile;
waitingLocal[waiting] = local;
waiting++;
}
}
private static final class Tile {
private final float[] height;
private final boolean[] ocean;
private final float[] rainfall;
private final float[] pondDensity;
private final byte[] permission;
private final String[] fluidProfile;
private final AtomicReferenceArray<CompletableFuture<Void>> marks;
private final AtomicInteger pending = new AtomicInteger();
private volatile long used;
private Tile(int cells) {
this.height = new float[cells];
this.ocean = new boolean[cells];
this.rainfall = new float[cells];
this.pondDensity = new float[cells];
this.permission = new byte[cells];
this.fluidProfile = new String[cells];
this.marks = new AtomicReferenceArray<>(cells);
}
private void store(int local, HydrologySampleBuffer samples, int sample) {
height[local] = samples.height()[sample];
ocean[local] = samples.ocean()[sample];
rainfall[local] = samples.rainfall()[sample];
pondDensity[local] = samples.pondDensity()[sample];
permission[local] = samples.permission()[sample];
fluidProfile[local] = samples.fluidProfile()[sample];
}
private void copy(int local, HydrologySampleBuffer out, int index) {
out.height()[index] = height[local];
out.ocean()[index] = ocean[local];
out.rainfall()[index] = rainfall[local];
out.pondDensity()[index] = pondDensity[local];
out.permission()[index] = permission[local];
out.fluidProfile()[index] = fluidProfile[local];
}
}
private interface Nodes {
@@ -32,10 +32,10 @@ import java.util.function.Consumer;
import java.util.function.Function;
public final class HydrologyPlanCache {
private static final int FRONT = 3;
public static final int WINDOW_LATTICE_CELLS = frontCells(WindowKey.SIZE, WindowPlanner.CELL);
public static final int FINE_LATTICE_CELLS = frontCells(WindowKey.SIZE, WindowPlanner.FINE_CELL);
public static final int REGION_LATTICE_CELLS = frontCells(RegionKey.SIZE, RegionPlanner.CELL);
private static final int FINE_FRONT = 9;
private static final int REGION_FRONT = 5;
public static final int FINE_LATTICE_CELLS = frontCells(FINE_FRONT, WindowKey.SIZE, WindowPlanner.FINE_CELL);
public static final int REGION_LATTICE_CELLS = frontCells(REGION_FRONT, RegionKey.SIZE, RegionPlanner.CELL);
private static final int WINDOW_CAPACITY = 128;
private static final int REGION_CAPACITY = 64;
private static final long DEMAND = Long.MIN_VALUE;
@@ -312,8 +312,8 @@ public final class HydrologyPlanCache {
return Math.max(1, parallelism / 2);
}
private static int frontCells(int unit, int cell) {
int cells = (FRONT + 2) * unit / cell;
private static int frontCells(int front, int unit, int cell) {
int cells = (front + 2) * unit / cell;
return cells * cells;
}
@@ -36,6 +36,7 @@ public final class WindowPlanner {
public static final int CELL = 8;
public static final int FINE_CELL = 4;
private static final int WINDOW_CELLS = WindowKey.SIZE / CELL;
private static final int FINE_STRIDE = CELL / FINE_CELL;
private static final int SPAN = 3 * WINDOW_CELLS;
private static final int REACH_OVERHANG = 256;
private static final float BASIN_DEPTH = 0.5f;
@@ -43,22 +44,17 @@ public final class WindowPlanner {
private static final long SALT_HEAD = 0x5354524541484dL;
private final HydrologySettings settings;
private final HeightLattice lattice;
private final HeightLattice fine;
private final PondPlanner ponds;
private final ReachShaper shaper;
private final SeaInletPlanner inlets;
public WindowPlanner(HydrologySettings settings, HeightLattice lattice, HeightLattice fine, PondPlanner ponds, ReachShaper shaper) {
public WindowPlanner(HydrologySettings settings, HeightLattice fine, PondPlanner ponds, ReachShaper shaper) {
this.settings = Objects.requireNonNull(settings, "settings");
this.lattice = Objects.requireNonNull(lattice, "lattice");
this.fine = Objects.requireNonNull(fine, "fine");
this.ponds = Objects.requireNonNull(ponds, "ponds");
this.shaper = Objects.requireNonNull(shaper, "shaper");
this.inlets = new SeaInletPlanner(settings);
if (lattice.cell() != CELL) {
throw new IllegalArgumentException("Window lattice cell " + lattice.cell() + " must be " + CELL);
}
if (fine.cell() != FINE_CELL) {
throw new IllegalArgumentException("Window fine lattice cell " + fine.cell() + " must be " + FINE_CELL);
}
@@ -87,7 +83,7 @@ public final class WindowPlanner {
int cellX = Math.multiplyExact(key.wx() - 1, WINDOW_CELLS);
int cellZ = Math.multiplyExact(key.wz() - 1, WINDOW_CELLS);
HydrologySampleBuffer samples = new HydrologySampleBuffer(SPAN * SPAN);
lattice.fill(cellX, cellZ, SPAN, SPAN, samples);
fine.fill(cellX * FINE_STRIDE, cellZ * FINE_STRIDE, SPAN, SPAN, FINE_STRIDE, samples);
DemGrid raw = new DemGrid(cellX * CELL, cellZ * CELL, CELL, SPAN, SPAN);
System.arraycopy(samples.height(), 0, raw.values(), 0, SPAN * SPAN);
List<Pond> lakes = regionLakes(plans.values());
@@ -12,6 +12,7 @@ import art.arcane.iris.generation.hydrology.water.PondMask;
import art.arcane.iris.generation.hydrology.water.Reach;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -28,17 +29,20 @@ final class ChunkFeatures {
private final List<Pond> ponds;
private final List<Pond> lakes;
private final List<SeaInlet> inlets;
private final int[] inletBoxes;
private final int[] lakeBoxes;
private final ReachSegments segments = new ReachSegments(SEGMENT_CAPACITY);
private final MaskEdges pondShores = new MaskEdges(MaskEdges.Kind.WATER);
private final MaskEdges pondBanks = new MaskEdges(MaskEdges.Kind.BANK);
private final MaskEdges lakeReach = new MaskEdges(MaskEdges.Kind.GROWN_WATER);
private boolean inert;
private ChunkFeatures(List<Reach> reaches, List<Pond> ponds, List<Pond> lakes, List<SeaInlet> inlets) {
private ChunkFeatures(List<Reach> reaches, List<Pond> ponds, List<Pond> lakes, List<SeaInlet> inlets, int[] inletBoxes) {
this.reaches = reaches;
this.ponds = ponds;
this.lakes = lakes;
this.inlets = inlets;
this.inletBoxes = inletBoxes;
this.lakeBoxes = new int[4 * lakes.size()];
}
@@ -48,12 +52,15 @@ final class ChunkFeatures {
List<Reach> reaches = new ArrayList<>();
List<Pond> ponds = new ArrayList<>();
List<SeaInlet> inlets = new ArrayList<>();
int[] inletBoxes = new int[0];
for (IndexedWindow window : windows) {
for (SeaInlet inlet : window.plan().inlets()) {
int[] bounds = SeaInletRasterizer.bounds(inlet);
int padding = RiffleProfile.RADIUS + 1;
if (bounds[0] <= chunkX * CHUNK + CHUNK - 1 + padding && bounds[2] >= chunkX * CHUNK - padding
&& bounds[1] <= chunkZ * CHUNK + CHUNK - 1 + padding && bounds[3] >= chunkZ * CHUNK - padding) {
inletBoxes = Arrays.copyOf(inletBoxes, inletBoxes.length + 4);
System.arraycopy(bounds, 0, inletBoxes, 4 * inlets.size(), 4);
inlets.add(inlet);
}
}
@@ -64,17 +71,22 @@ final class ChunkFeatures {
}
}
List<Pond> lakes = lakes(chunkX, chunkZ, halo, lakeRegionPlans, lakeRegions);
ChunkFeatures features = new ChunkFeatures(reaches, ponds, lakes, inlets);
ChunkFeatures features = new ChunkFeatures(reaches, ponds, lakes, inlets, inletBoxes);
features.index(settings, chunkX, chunkZ);
return features;
}
boolean empty() {
return reaches.isEmpty() && ponds.isEmpty() && lakes.isEmpty() && inlets.isEmpty();
boolean inert() {
return inert;
}
SeaInlet openInletAt(int x, int z, int[] column) {
for (SeaInlet inlet : inlets) {
for (int index = 0; index < inlets.size(); index++) {
int box = 4 * index;
if (x < inletBoxes[box] || z < inletBoxes[box + 1] || x > inletBoxes[box + 2] || z > inletBoxes[box + 3]) {
continue;
}
SeaInlet inlet = inlets.get(index);
if (SeaInletRasterizer.column(inlet, x, z, column) && column[1] == SeaInlet.OPEN) {
return inlet;
}
@@ -179,6 +191,8 @@ final class ChunkFeatures {
for (Reach reach : reaches) {
segments.addNear(reach, minX - 1f, minZ - 1f, maxX + 1f, maxZ + 1f, outward);
}
int riffle = RiffleProfile.RADIUS + 1;
segments.coverChannels(minX - riffle, minZ - riffle, maxX + riffle, maxZ + riffle);
HydrologySettings.PondShore shore = settings.ponds().shore();
int reach = Math.max(shore.taper(), ChunkWaterRaster.MAX_SHORE_DISTANCE) + 1;
int shelf = shore.shelf() + 1;
@@ -192,6 +206,39 @@ final class ChunkFeatures {
lakeReach.collect(mask, minX - shoreReach, minZ - shoreReach, maxX + shoreReach, maxZ + shoreReach);
System.arraycopy(grownBox(mask), 0, lakeBoxes, 4 * index, 4);
}
inert = segments.isEmpty() && !inletsTouch(minX - 1, minZ - 1, maxX + 1, maxZ + 1)
&& !pondsTouch(minX - 1 - reach, minZ - 1 - reach, maxX + 1 + reach, maxZ + 1 + reach)
&& !lakesTouch(minX - 2 - shoreReach, minZ - 2 - shoreReach, maxX + 2 + shoreReach, maxZ + 2 + shoreReach);
}
private boolean inletsTouch(int minX, int minZ, int maxX, int maxZ) {
for (int box = 0; box < inletBoxes.length; box += 4) {
if (inletBoxes[box] <= maxX && inletBoxes[box + 2] >= minX && inletBoxes[box + 1] <= maxZ && inletBoxes[box + 3] >= minZ) {
return true;
}
}
return false;
}
private boolean pondsTouch(int minX, int minZ, int maxX, int maxZ) {
for (Pond pond : ponds) {
PondMask mask = pond.mask();
int fromX = mask.originX() - (mask.step() >> 1);
int fromZ = mask.originZ() - (mask.step() >> 1);
if (fromX <= maxX && fromX + mask.width() * mask.step() > minX && fromZ <= maxZ && fromZ + mask.depth() * mask.step() > minZ) {
return true;
}
}
return false;
}
private boolean lakesTouch(int minX, int minZ, int maxX, int maxZ) {
for (int box = 0; box < lakeBoxes.length; box += 4) {
if (lakeBoxes[box] <= maxX && lakeBoxes[box + 2] > minX && lakeBoxes[box + 1] <= maxZ && lakeBoxes[box + 3] > minZ) {
return true;
}
}
return false;
}
private static Map<WindowKey, IndexedWindow> windowsByKey(int chunkX, int chunkZ, List<IndexedWindow> windows) {
@@ -37,7 +37,7 @@ public final class ChunkRasterizer {
List<RegionPlan> lakeRegionPlans) {
Objects.requireNonNull(naturalHeight, "naturalHeight");
ChunkFeatures features = ChunkFeatures.gather(settings, chunkX, chunkZ, halo, windows, lakeRegionPlans, lakeRegions(chunkX, chunkZ));
if (features.empty()) {
if (features.inert()) {
return ChunkWaterRaster.EMPTY;
}
return new RasterComposer(settings, features, chunkX, chunkZ).compose(naturalHeight);
@@ -2,6 +2,8 @@ package art.arcane.iris.generation.hydrology.raster;
import art.arcane.iris.generation.hydrology.water.HydrologyTier;
import java.util.Arrays;
public final class ChunkWaterRaster {
public static final int MAX_SHORE_DISTANCE = 32;
public static final ChunkWaterRaster PROVISIONAL = new ChunkWaterRaster(true);
@@ -13,10 +15,13 @@ public final class ChunkWaterRaster {
private final int[] terrain;
private final int[] head;
private final byte[] role;
private final HydrologyTier[] tier;
private final long[] featureId;
private final String[] fluid;
private final int[] caveCeiling;
private final byte[] tier;
private final HydrologyTier[] tiers;
private final byte[] feature;
private final long[] features;
private final byte[] fluid;
private final String[] fluids;
private final boolean caveMask;
private final byte[] shoreDistance;
private ChunkWaterRaster(boolean provisional) {
@@ -25,26 +30,32 @@ public final class ChunkWaterRaster {
this.head = null;
this.role = null;
this.tier = null;
this.featureId = null;
this.tiers = null;
this.feature = null;
this.features = null;
this.fluid = null;
this.caveCeiling = null;
this.fluids = null;
this.caveMask = false;
this.shoreDistance = null;
}
ChunkWaterRaster(int[] terrain, int[] head, byte[] role, HydrologyTier[] tier, long[] featureId, String[] fluid, int[] caveCeiling,
byte[] shoreDistance) {
if (terrain.length != COLUMNS || head.length != COLUMNS || role.length != COLUMNS || tier.length != COLUMNS
|| featureId.length != COLUMNS || fluid.length != COLUMNS || caveCeiling.length != COLUMNS || shoreDistance.length != COLUMNS) {
ChunkWaterRaster(int[] terrain, int[] head, byte[] role, Palette<HydrologyTier> tiers, LongPalette features, Palette<String> fluids,
boolean caveMask, byte[] shoreDistance) {
if (terrain.length != COLUMNS || head.length != COLUMNS || role.length != COLUMNS || tiers.indices.length != COLUMNS
|| features.indices.length != COLUMNS || fluids.indices.length != COLUMNS || shoreDistance.length != COLUMNS) {
throw new IllegalArgumentException("A chunk water raster needs " + COLUMNS + " entries per array");
}
this.provisional = false;
this.terrain = terrain;
this.head = head;
this.role = role;
this.tier = tier;
this.featureId = featureId;
this.fluid = fluid;
this.caveCeiling = caveCeiling;
this.tier = tiers.indices;
this.tiers = tiers.values();
this.feature = features.indices;
this.features = features.values();
this.fluid = fluids.indices;
this.fluids = fluids.values();
this.caveMask = caveMask;
this.shoreDistance = shoreDistance;
}
@@ -68,22 +79,22 @@ public final class ChunkWaterRaster {
public HydrologyTier tier(int lx, int lz) {
int index = index(lx, lz);
return tier == null ? null : tier[index];
return tier == null ? null : tiers[tier[index] & 0xFF];
}
public long featureId(int lx, int lz) {
int index = index(lx, lz);
return featureId == null ? 0L : featureId[index];
return feature == null ? 0L : features[feature[index] & 0xFF];
}
public String fluid(int lx, int lz) {
int index = index(lx, lz);
return fluid == null ? null : fluid[index];
return fluid == null ? null : fluids[fluid[index] & 0xFF];
}
public int caveCeiling(int lx, int lz) {
int index = index(lx, lz);
return caveCeiling == null ? Integer.MAX_VALUE : caveCeiling[index];
return caveMask && head[index] != Integer.MIN_VALUE ? terrain[index] - 1 : Integer.MAX_VALUE;
}
public int shoreDistance(int lx, int lz) {
@@ -105,4 +116,53 @@ public final class ChunkWaterRaster {
}
return lz << 4 | lx;
}
static final class Palette<T> {
private final byte[] indices = new byte[COLUMNS];
private final T[] seen;
private int size;
Palette(T[] seen) {
if (seen.length != COLUMNS) {
throw new IllegalArgumentException("A raster palette needs room for " + COLUMNS + " values");
}
this.seen = seen;
}
void set(int column, T value) {
int found = 0;
while (found < size && seen[found] != value) {
found++;
}
if (found == size) {
seen[size++] = value;
}
indices[column] = (byte) found;
}
private T[] values() {
return Arrays.copyOf(seen, size);
}
}
static final class LongPalette {
private final byte[] indices = new byte[COLUMNS];
private final long[] seen = new long[COLUMNS];
private int size;
void set(int column, long value) {
int found = 0;
while (found < size && seen[found] != value) {
found++;
}
if (found == size) {
seen[size++] = value;
}
indices[column] = (byte) found;
}
private long[] values() {
return Arrays.copyOf(seen, size);
}
}
}
@@ -385,34 +385,37 @@ final class RasterComposer {
private ChunkWaterRaster pack() {
int columns = ChunkWaterRaster.COLUMNS;
boolean affected = false;
for (int lz = 0; lz < CHUNK && !affected; lz++) {
for (int lx = 0; lx < CHUNK && !affected; lx++) {
int cell = (lz + 1) * GRID + lx + 1;
affected = role[cell] != WaterRole.NONE || terrain[cell] != natural[cell] || shore(cell) < ChunkWaterRaster.MAX_SHORE_DISTANCE;
}
}
if (!affected) {
return ChunkWaterRaster.EMPTY;
}
int[] terrainOut = new int[columns];
int[] headOut = new int[columns];
byte[] roleOut = new byte[columns];
HydrologyTier[] tierOut = new HydrologyTier[columns];
long[] featureOut = new long[columns];
String[] fluidOut = new String[columns];
int[] caveOut = new int[columns];
ChunkWaterRaster.Palette<HydrologyTier> tierOut = new ChunkWaterRaster.Palette<>(new HydrologyTier[columns]);
ChunkWaterRaster.LongPalette featureOut = new ChunkWaterRaster.LongPalette();
ChunkWaterRaster.Palette<String> fluidOut = new ChunkWaterRaster.Palette<>(new String[columns]);
byte[] shoreOut = new byte[columns];
boolean caveMask = settings.deformation().caveMask();
boolean affected = false;
for (int lz = 0; lz < CHUNK; lz++) {
for (int lx = 0; lx < CHUNK; lx++) {
int cell = (lz + 1) * GRID + lx + 1;
int index = lz << 4 | lx;
int shore = shore(cell);
terrainOut[index] = terrain[cell];
headOut[index] = head[cell];
roleOut[index] = (byte) role[cell].ordinal();
tierOut[index] = tier[cell];
featureOut[index] = feature[cell];
fluidOut[index] = fluid[cell];
caveOut[index] = caveMask && head[cell] != Integer.MIN_VALUE ? terrain[cell] - 1 : Integer.MAX_VALUE;
shoreOut[index] = (byte) shore;
affected |= role[cell] != WaterRole.NONE || terrain[cell] != natural[cell] || shore < ChunkWaterRaster.MAX_SHORE_DISTANCE;
tierOut.set(index, tier[cell]);
featureOut.set(index, feature[cell]);
fluidOut.set(index, fluid[cell]);
shoreOut[index] = (byte) shore(cell);
}
}
return affected ? new ChunkWaterRaster(terrainOut, headOut, roleOut, tierOut, featureOut, fluidOut, caveOut, shoreOut)
: ChunkWaterRaster.EMPTY;
return new ChunkWaterRaster(terrainOut, headOut, roleOut, tierOut, featureOut, fluidOut, settings.deformation().caveMask(), shoreOut);
}
private int shore(int cell) {
@@ -6,6 +6,8 @@ import java.util.Arrays;
final class ReachSegments {
private static final int RUN = 16;
private static final int COVER_SHIFT = 2;
private static final float COVER_MARGIN = 1f;
private Reach[] reaches;
private int[] segments;
@@ -24,6 +26,13 @@ final class ReachSegments {
private int[] runBands = new int[8];
private int runs;
private boolean sealed;
private boolean covered;
private int coverX;
private int coverZ;
private int coverWidth;
private int coverDepth;
private int[] coverStarts;
private int[] coverEntries;
ReachSegments(int capacity) {
int initial = Math.max(1, capacity);
@@ -71,6 +80,19 @@ final class ReachSegments {
}
}
boolean isEmpty() {
return size == 0;
}
void coverChannels(int minX, int minZ, int maxX, int maxZ) {
covered = true;
coverX = minX;
coverZ = minZ;
coverWidth = ((maxX - minX) >> COVER_SHIFT) + 1;
coverDepth = ((maxZ - minZ) >> COVER_SHIFT) + 1;
sealed = false;
}
void nearest(double x, double z, Scope scope, ReachCursor cursor) {
if (!sealed) {
seal();
@@ -89,6 +111,16 @@ final class ReachSegments {
hint(channelHint, x, z, unbounded, cursor);
hint(bankHint, x, z, unbounded, cursor);
hint(edgeHint, x, z, unbounded, cursor);
int cell = scope == Scope.CHANNEL ? coveredCell(x, z) : -1;
if (cell >= 0) {
for (int entry = coverStarts[cell]; entry < coverStarts[cell + 1]; entry++) {
int index = coverEntries[entry];
if (index != channelHint && index != bankHint && index != edgeHint && !beyondSegment(index, x, z, scope, cursor)) {
evaluate(index, x, z, false, cursor);
}
}
return;
}
int first = 0;
for (int run = 0; run < runs; run++) {
int end = runEnds[run];
@@ -99,9 +131,7 @@ final class ReachSegments {
continue;
}
for (int index = first; index < end; index++) {
if (!beyond(x, z, Math.min(startX[index], endX[index]), Math.min(startZ[index], endZ[index]), Math.max(startX[index], endX[index]),
Math.max(startZ[index], endZ[index]), limit(radius[index], bands[index], scope, cursor))
&& index != channelHint && index != bankHint && index != edgeHint) {
if (!beyondSegment(index, x, z, scope, cursor) && index != channelHint && index != bankHint && index != edgeHint) {
evaluate(index, x, z, unbounded, cursor);
}
}
@@ -109,6 +139,23 @@ final class ReachSegments {
}
}
private int coveredCell(double x, double z) {
if (coverStarts == null) {
return -1;
}
int cellX = ((int) Math.floor(x) - coverX) >> COVER_SHIFT;
int cellZ = ((int) Math.floor(z) - coverZ) >> COVER_SHIFT;
if (cellX < 0 || cellZ < 0 || cellX >= coverWidth || cellZ >= coverDepth) {
return -1;
}
return cellZ * coverWidth + cellX;
}
private boolean beyondSegment(int index, double x, double z, Scope scope, ReachCursor cursor) {
return beyond(x, z, Math.min(startX[index], endX[index]), Math.min(startZ[index], endZ[index]), Math.max(startX[index], endX[index]),
Math.max(startZ[index], endZ[index]), limit(radius[index], bands[index], scope, cursor));
}
private void hint(int index, double x, double z, boolean unbounded, ReachCursor cursor) {
if (index >= 0 && index < size) {
evaluate(index, x, z, unbounded, cursor);
@@ -171,9 +218,56 @@ final class ReachSegments {
addRun(start, end);
start = end;
}
if (covered) {
cover();
}
sealed = true;
}
private void cover() {
int cells = coverWidth * coverDepth;
int[] starts = new int[cells + 1];
int[] range = new int[4];
for (int index = 0; index < size; index++) {
if (coverRange(index, range)) {
for (int cellZ = range[1]; cellZ <= range[3]; cellZ++) {
for (int cellX = range[0]; cellX <= range[2]; cellX++) {
starts[cellZ * coverWidth + cellX + 1]++;
}
}
}
}
for (int cell = 0; cell < cells; cell++) {
starts[cell + 1] += starts[cell];
}
int[] entries = new int[starts[cells]];
int[] fill = Arrays.copyOf(starts, cells);
for (int index = 0; index < size; index++) {
if (coverRange(index, range)) {
for (int cellZ = range[1]; cellZ <= range[3]; cellZ++) {
for (int cellX = range[0]; cellX <= range[2]; cellX++) {
entries[fill[cellZ * coverWidth + cellX]++] = index;
}
}
}
}
coverStarts = starts;
coverEntries = entries;
}
private boolean coverRange(int index, int[] range) {
float reach = radius[index] + COVER_MARGIN;
range[0] = Math.max(0, coverCell(Math.min(startX[index], endX[index]) - reach, coverX));
range[1] = Math.max(0, coverCell(Math.min(startZ[index], endZ[index]) - reach, coverZ));
range[2] = Math.min(coverWidth - 1, coverCell(Math.max(startX[index], endX[index]) + reach, coverX));
range[3] = Math.min(coverDepth - 1, coverCell(Math.max(startZ[index], endZ[index]) + reach, coverZ));
return range[0] <= range[2] && range[1] <= range[3];
}
private static int coverCell(float block, int origin) {
return ((int) Math.floor(block) - origin) >> COVER_SHIFT;
}
private void addRun(int start, int end) {
if (runs == runEnds.length) {
runEnds = Arrays.copyOf(runEnds, 2 * runs);
@@ -15,6 +15,7 @@ final class RiffleProfile {
private static final int LIMIT = 10 * RADIUS;
private static final int DRY = Integer.MIN_VALUE;
private static final int[] SIDES = {1, -1, GRID, -GRID};
private static final int[] REMAINING = remaining();
private final List<Field> fields = new ArrayList<>();
@@ -85,6 +86,20 @@ final class RiffleProfile {
return (int) Math.max(original, Math.min(head - 1L, Math.round(raised)));
}
private static int[] remaining() {
int[] remaining = new int[GRID * GRID];
int low = PADDING - 1;
int high = PADDING + CHUNK;
for (int z = 0; z < GRID; z++) {
for (int x = 0; x < GRID; x++) {
int outsideX = Math.max(0, Math.max(low - x, x - high));
int outsideZ = Math.max(0, Math.max(low - z, z - high));
remaining[z * GRID + x] = 10 * Math.max(outsideX, outsideZ);
}
}
return remaining;
}
private record Field(int crest, int[] distances) {
}
@@ -107,6 +122,9 @@ final class RiffleProfile {
}
private void offer(int cell, int distance) {
if (distance + REMAINING[cell] >= LIMIT) {
return;
}
if (distances[cell] < LIMIT) {
remove(cell);
}
@@ -10,6 +10,7 @@ import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import java.util.Arrays;
import java.util.Set;
import java.util.function.Supplier;
public final class CavePoolFiller {
public static final int REACH = HydrologySettings.MAX_CAVE_POOL_RADIUS + 1;
@@ -40,15 +41,19 @@ public final class CavePoolFiller {
this.density = settings.density();
}
public void fill(int chunkX, int chunkZ, CarvedTerrain carved, MantleWriter writer, long seed) {
public void fill(int chunkX, int chunkZ, Supplier<? extends CarvedTerrain> terrain, MantleWriter writer, long seed) {
if (!pools.enabled() || density == 0f) {
return;
}
int attempts = attempts(seed, chunkX, chunkZ);
CarvedTerrain carved = null;
for (int attempt = 0; attempt < attempts; attempt++) {
if (density < 1f && CellHash.unit(CellHash.mix(seed, SALT_DENSITY + attempt, chunkX, chunkZ)) >= density) {
continue;
}
if (carved == null) {
carved = terrain.get();
}
long column = CellHash.mix(seed, SALT_COLUMN + attempt, chunkX, chunkZ);
int x = (chunkX << 4) + (int) (column & 15);
int z = (chunkZ << 4) + (int) ((column >>> 4) & 15);
@@ -33,6 +33,7 @@ public final class SeaInletPlanner {
private static final long SALT_HEADROOM = 0x53454148454144L;
private static final long SALT_DEPTH = 0x53454144455054L;
private static final long SALT_DENSITY = 0x53454144454e53L;
private static final double SQUARED_MARGIN = 1e-9D;
private final HydrologySettings settings;
@@ -403,22 +404,36 @@ public final class SeaInletPlanner {
double dx = bx - ax;
double dz = bz - az;
double span = dx * dx + dz * dz;
double outside = radius > 0f ? squaredAbove(radius) : Double.POSITIVE_INFINITY;
double inside = radius > 0f ? squaredBelow(radius) : Double.NEGATIVE_INFINITY;
double wetOutside = wetRadius > 0f ? squaredAbove(wetRadius) : Double.POSITIVE_INFINITY;
double wetInside = wetRadius > 0f ? squaredBelow(wetRadius) : Double.NEGATIVE_INFINITY;
for (int j = minJ; j <= maxJ; j++) {
double pz = grid.originZ() + (double) j * cell;
for (int i = minI; i <= maxI; i++) {
double px = grid.originX() + (double) i * cell;
double t = span <= 0D ? 0D : Math.clamp(((px - ax) * dx + (pz - az) * dz) / span, 0D, 1D);
double distance = Math.hypot(px - (ax + t * dx), pz - (az + t * dz));
if (distance > radius) {
double ex = px - (ax + t * dx);
double ez = pz - (az + t * dz);
double squared = ex * ex + ez * ez;
if (squared > outside || !(squared < inside) && Math.hypot(ex, ez) > radius) {
continue;
}
int index = j * grid.width() + i;
cover[index] = Math.min(cover[index], bed);
wet[index] |= distance <= wetRadius;
wet[index] |= squared < wetInside || !(squared > wetOutside) && Math.hypot(ex, ez) <= wetRadius;
}
}
}
private static double squaredAbove(float radius) {
return (double) radius * radius * (1D + SQUARED_MARGIN);
}
private static double squaredBelow(float radius) {
return (double) radius * radius * (1D - SQUARED_MARGIN);
}
private static float pathLength(DemGrid grid, List<Integer> path) {
float length = 0f;
for (int station = 1; station < path.size(); station++) {
@@ -85,8 +85,9 @@ public final class MantleHydrologyComponent extends IrisMantleComponent {
generateFalls(writer, complex, runtime, own, chunkX, chunkZ);
if (poolsActive()) {
HydrologySettings settings = runtime.settings();
new CavePoolFiller(settings).fill(chunkX, chunkZ,
new MantleCarvedTerrain(writer, complex, getEngineMantle().getEngine(), chunkX, chunkZ), writer, settings.seed());
Engine engine = getEngineMantle().getEngine();
new CavePoolFiller(settings).fill(chunkX, chunkZ, () -> new MantleCarvedTerrain(writer, complex, engine, chunkX, chunkZ), writer,
settings.seed());
}
}
@@ -91,7 +91,6 @@ import art.arcane.volmlib.util.interpolation.NoiseBoundsProvider;
import art.arcane.volmlib.util.noise.CNG;
import art.arcane.volmlib.util.stream.ProceduralStream;
import art.arcane.volmlib.util.stream.interpolation.Interpolated;
import it.unimi.dsi.fastutil.longs.Long2DoubleOpenHashMap;
import lombok.AccessLevel;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -120,10 +119,11 @@ import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiFunction;
@Data
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "sharedBoundsBiomes", "terrainProgram", "bakeCache", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyRuntime", "shoreBiomes", "waterVegetation", "effectiveChunks"})
@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "sharedBoundsBiomes", "terrainProgram", "bakeCache", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyRuntime", "shoreBiomes", "waterVegetation", "effectiveChunks"})
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "sharedBaseHeights", "sharedBoundsBiomes", "regionBounds", "contextRegionStream", "terrainProgram", "bakeCache", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyRuntime", "shoreBiomes", "waterVegetation", "effectiveChunks"})
@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "sharedBaseHeights", "sharedBoundsBiomes", "regionBounds", "contextRegionStream", "terrainProgram", "bakeCache", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyRuntime", "shoreBiomes", "waterVegetation", "effectiveChunks"})
public class IrisComplex implements DataProvider {
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
private static final long NEGATIVE_ZERO_BITS = Double.doubleToRawLongBits(-0D);
private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
private static final int GRID_BOUNDS_CACHE_SIZE = 8192;
private static final int STUDIO_NOISE_CACHE_SIZE = 32_768;
@@ -146,7 +146,6 @@ public class IrisComplex implements DataProvider {
Comparator.nullsFirst(Comparator.naturalOrder())
);
private static final ThreadLocal<TerrainBatchScratch> BATCH_SCRATCH = ThreadLocal.withInitial(TerrainBatchScratch::new);
private static final ThreadLocal<HydrologyBaseHeights> HYDROLOGY_BASE_HEIGHTS = new ThreadLocal<>();
private static final InferredType[] INFERRED_BIOME_PREPARATION_ORDER = {
InferredType.LAND,
InferredType.CAVE,
@@ -160,6 +159,7 @@ public class IrisComplex implements DataProvider {
@Getter(AccessLevel.NONE)
private final int biomeBoundsSamplingStep;
private transient volatile SharedCornerBounds sharedCornerBounds;
private transient volatile SharedBaseHeights sharedBaseHeights;
private transient volatile SharedBoundsBiomes sharedBoundsBiomes;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
@@ -259,6 +259,10 @@ public class IrisComplex implements DataProvider {
private IrisBiome focusBiome;
private IrisRegion focusRegion;
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds;
@Getter(AccessLevel.NONE)
private final transient RegionBounds regionBounds;
@Getter(AccessLevel.NONE)
private final transient ProceduralStream<IrisRegion> contextRegionStream;
private Set<IrisBiome> generatorBiomes;
@Setter(AccessLevel.NONE)
private final transient HydrologyRuntime hydrologyRuntime;
@@ -386,6 +390,9 @@ public class IrisComplex implements DataProvider {
frozenGenerators[frozenIndex] = generatorGroups[frozenIndex].generators();
}
generatorBounds = buildGeneratorBounds(engine);
regionBounds = focusedBiomes == null && !imageMapRuntime.has(IrisImageMapApplication.BIOME)
? RegionBounds.compile(preparedRegions, frozenInterpolators.length, new BoundsReach(), this::groupBounds)
: null;
biomeBuffetGenerators = compileBuffetGeneratorGroups();
chunkHeightBatch = engine.getDimension().getTerrainSamplingStep() == 1 && biomeBuffetGenerators.isEmpty()
&& (biomeBuffet == null || biomeBuffet.cells().isEmpty());
@@ -417,20 +424,21 @@ public class IrisComplex implements DataProvider {
: RegionSelection.select(new RegionSelection.Options(
engine.getDimension(), data, regionStyleStream, imageMapRuntime, null));
regionStream = focusedRegions != null ? focusedRegions : new BakedStream<>(bakeCache, BakePlane.REGION, regionSource);
caveSource = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
contextRegionStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z));
caveSource = contextRegionStream
.convert((r) -> createInferredBiomeStream(r, InferredType.CAVE))
.convertAware2D(ProceduralStream::get);
caveBiomeStream = new BakedStream<>(bakeCache, BakePlane.CAVE_BIOME, caveSource);
inferredStreams.put(InferredType.CAVE, caveBiomeStream);
landBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
landBiomeStream = contextRegionStream
.convert((r) -> createInferredBiomeStream(r, InferredType.LAND))
.convertAware2D(ProceduralStream::get);
inferredStreams.put(InferredType.LAND, landBiomeStream);
seaBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
seaBiomeStream = contextRegionStream
.convert((r) -> createInferredBiomeStream(r, InferredType.SEA))
.convertAware2D(ProceduralStream::get);
inferredStreams.put(InferredType.SEA, seaBiomeStream);
shoreBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
shoreBiomeStream = contextRegionStream
.convert((r) -> createInferredBiomeStream(r, InferredType.SHORE))
.convertAware2D(ProceduralStream::get);
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
@@ -812,17 +820,35 @@ public class IrisComplex implements DataProvider {
}
return;
}
SharedBaseHeights memo = sharedBaseHeights();
TerrainBatchScratch scratch = BATCH_SCRATCH.get();
int mark = scratch.mark();
int indexMark = scratch.indexMark();
try {
double[] batchXs = scratch.buffer(count);
double[] batchZs = scratch.buffer(count);
double[] computed = scratch.buffer(count);
int[] slots = scratch.indices(count);
int missing = 0;
for (int index = 0; index < count; index++) {
batchXs[index] = xs[from + index];
batchZs[index] = zs[from + index];
int x = xs[from + index];
int z = zs[from + index];
double known = memo.get(x, z);
if (Double.isNaN(known)) {
batchXs[missing] = x;
batchZs[missing] = z;
slots[missing++] = index;
} else {
heights[index] = known;
}
}
if (missing > 0) {
batchBaseHeights(scratch, batchXs, batchZs, missing, computed);
for (int index = 0; index < missing; index++) {
heights[slots[index]] = computed[index];
memo.put((int) batchXs[index], (int) batchZs[index], computed[index]);
}
}
batchBaseHeights(scratch, batchXs, batchZs, count, heights);
} finally {
scratch.reset(mark);
scratch.resetIndices(indexMark);
@@ -830,23 +856,9 @@ public class IrisComplex implements DataProvider {
if (baseOnly || terrain3D == null && transitionDisplacement == null) {
return;
}
Long2DoubleOpenHashMap known = new Long2DoubleOpenHashMap(count);
known.defaultReturnValue(Double.NaN);
for (int index = 0; index < count; index++) {
known.put(hydrologyColumnKey(xs[from + index], zs[from + index]), heights[index]);
heights[index] = naturalHeightStream.getDouble(xs[from + index], zs[from + index]);
}
HYDROLOGY_BASE_HEIGHTS.set(new HydrologyBaseHeights(this, known));
try {
for (int index = 0; index < count; index++) {
heights[index] = naturalHeightStream.getDouble(xs[from + index], zs[from + index]);
}
} finally {
HYDROLOGY_BASE_HEIGHTS.remove();
}
}
private static long hydrologyColumnKey(int x, int z) {
return ((long) x << 32) | (z & 0xFFFFFFFFL);
}
private void bakeBaseHeights(int chunkX, int chunkZ, long[] missing, double[] values) {
@@ -1082,15 +1094,21 @@ public class IrisComplex implements DataProvider {
}
private double sampleUnblendedNaturalTerrainHeight(double x, double z) {
HydrologyBaseHeights batched = HYDROLOGY_BASE_HEIGHTS.get();
if (batched != null && batched.owner() == this && (int) x == x && (int) z == z) {
double known = batched.heights().get(hydrologyColumnKey((int) x, (int) z));
if (!Double.isNaN(known)) {
return known;
}
if (!memoColumn(x) || !memoColumn(z)) {
return imageMapRuntime.sampleTerrainHeight(x, z, proceduralTerrainHeight.sample(x, z));
}
double proceduralHeight = proceduralTerrainHeight.sample(x, z);
return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight);
SharedBaseHeights memo = sharedBaseHeights();
double known = memo.get((int) x, (int) z);
if (!Double.isNaN(known)) {
return known;
}
double height = imageMapRuntime.sampleTerrainHeight(x, z, proceduralTerrainHeight.sample(x, z));
memo.put((int) x, (int) z, height);
return height;
}
private static boolean memoColumn(double coordinate) {
return (int) coordinate == coordinate && Double.doubleToRawLongBits(coordinate) != NEGATIVE_ZERO_BITS;
}
static double calculateNaturalSlope(double naturalHeight, double easternHeight, double southernHeight) {
@@ -1885,7 +1903,7 @@ public class IrisComplex implements DataProvider {
int grid = biomeBoundsSamplingStep;
GridBoundsCache cache = gridBoundsCache.get();
if (grid <= 1) {
return sampleBoundsRaw(cache, engine, interpolator, generators, x, z);
return sampleBoundsRaw(cache, engine, interpolator, interpolatorIndex, generators, x, z);
}
int xi = (int) Math.floor(x);
@@ -1941,7 +1959,7 @@ public class IrisComplex implements DataProvider {
if (GenerationCounter.ENABLED) {
GenerationCounter.CORNER_STARCASTS.increment();
}
packed = computePackedBounds(cache, engine, interpolator, generators, gx, gz);
packed = computePackedBounds(cache, engine, interpolator, interpolatorIndex, generators, gx, gz);
if (!Double.isFinite(boundsLow(packed)) || !Double.isFinite(boundsHigh(packed))) {
// A non-finite corner is a transient sampling fault, not a fact about the terrain:
// hand it back uncached so the next sample recomputes it instead of pinning it.
@@ -1972,6 +1990,20 @@ public class IrisComplex implements DataProvider {
return shared;
}
private SharedBaseHeights sharedBaseHeights() {
SharedBaseHeights shared = sharedBaseHeights;
if (shared == null) {
synchronized (this) {
shared = sharedBaseHeights;
if (shared == null) {
shared = new SharedBaseHeights(SharedBaseHeights.capacityFor(Runtime.getRuntime().maxMemory()));
sharedBaseHeights = shared;
}
}
}
return shared;
}
private SharedBoundsBiomes sharedBoundsBiomes(ProceduralStream<IrisBiome> source) {
SharedBoundsBiomes shared = sharedBoundsBiomes;
if (shared != null && shared.source() == source) {
@@ -1987,8 +2019,9 @@ public class IrisComplex implements DataProvider {
}
}
private long computePackedBounds(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, IrisGenerator[] generators, int gx, int gz) {
NoiseBounds bounds = sampleBoundsRaw(cache, engine, interpolator, generators, gx, gz);
private long computePackedBounds(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, int interpolatorIndex,
IrisGenerator[] generators, int gx, int gz) {
NoiseBounds bounds = sampleBoundsRaw(cache, engine, interpolator, interpolatorIndex, generators, gx, gz);
return (((long) Float.floatToRawIntBits((float) bounds.min())) << 32) | (Float.floatToRawIntBits((float) bounds.max()) & 0xFFFFFFFFL);
}
@@ -2006,10 +2039,11 @@ public class IrisComplex implements DataProvider {
return a + ((b - a) * fz);
}
private NoiseBounds sampleBoundsRaw(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, IrisGenerator[] generators, double x, double z) {
private NoiseBounds sampleBoundsRaw(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, int interpolatorIndex,
IrisGenerator[] generators, double x, double z) {
IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds = generatorBounds.get(interpolator);
BoundsSampler sampler = cache.sampler.isInUse() ? new BoundsSampler() : cache.sampler;
sampler.bind(this, engine, generators, cachedBounds);
sampler.bind(this, engine, interpolatorIndex, generators, cachedBounds);
try {
return interpolator.interpolateBounds(x, z, sampler);
@@ -2297,6 +2331,11 @@ public class IrisComplex implements DataProvider {
return reached;
}
private NoiseBounds groupBounds(int group, IrisBiome biome) {
GeneratorBounds cached = generatorBounds.get(frozenInterpolators[group]).get(biome);
return (cached == null ? computeGeneratorBounds(frozenGenerators[group], biome) : cached).noiseBounds;
}
static double averageGeneratorHeights(
IrisGenerator[] generators,
double low,
@@ -2604,6 +2643,32 @@ public class IrisComplex implements DataProvider {
}
}
private final class BoundsReach implements RegionBounds.Reach {
@Override
public List<IrisBiome> picks(IrisRegion region, InferredType type) {
return loadInferredBiomes(biomeKeys(region, type), type);
}
@Override
public Collection<IrisBiome> descendants(IrisBiome biome) {
Set<IrisBiome> reached = Collections.newSetFromMap(new IdentityHashMap<>());
ArrayDeque<IrisBiome> pending = new ArrayDeque<>();
pending.add(biome);
while (!pending.isEmpty()) {
IrisBiome next = pending.poll();
if (!reached.add(next)) {
continue;
}
for (IrisBiome child : next.getRealChildren(IrisComplex.this)) {
if (child != null) {
pending.add(child);
}
}
}
return reached;
}
}
/**
* Reusable per-thread bounds provider. Holds the biome memo and the lazily computed generator
* bounds for one sampleBoundsRaw pass so the pass allocates nothing; {@link #bind}
@@ -2619,9 +2684,12 @@ public class IrisComplex implements DataProvider {
private final IdentityHashMap<IrisBiome, GeneratorBounds> localBounds = new IdentityHashMap<>(8);
private IrisComplex complex;
private Engine engine;
private int group;
private IrisGenerator[] generators;
private IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds;
private SharedBoundsBiomes sharedBiomes;
private BakedStream<IrisBiome> bakedBiomes;
private RegionBounds regional;
private boolean integerCoordinates;
private boolean inUse;
@@ -2629,14 +2697,19 @@ public class IrisComplex implements DataProvider {
return inUse;
}
private void bind(IrisComplex complex, Engine engine, IrisGenerator[] generators, IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds) {
private void bind(IrisComplex complex, Engine engine, int group, IrisGenerator[] generators,
IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds) {
this.complex = complex;
this.engine = engine;
this.group = group;
this.generators = generators;
this.cachedBounds = cachedBounds;
this.integerCoordinates = complex.baseBiomeStream instanceof BakedStream<?>
|| complex.baseBiomeStream instanceof CachedStream2D<?>;
this.sharedBiomes = integerCoordinates ? complex.sharedBoundsBiomes(complex.baseBiomeStream) : null;
this.bakedBiomes = complex.regionBounds != null && complex.baseBiomeStream instanceof BakedStream<IrisBiome> baked
? baked : null;
this.regional = bakedBiomes == null ? null : complex.regionBounds;
this.inUse = true;
if (sampleSource.get() != complex.baseBiomeStream) {
sampleCache.clear();
@@ -2654,6 +2727,8 @@ public class IrisComplex implements DataProvider {
generators = null;
cachedBounds = null;
sharedBiomes = null;
bakedBiomes = null;
regional = null;
inUse = false;
}
@@ -2662,25 +2737,23 @@ public class IrisComplex implements DataProvider {
try {
double sampleX = integerCoordinates ? (int) xx : xx;
double sampleZ = integerCoordinates ? (int) zz : zz;
IrisBiome bx = sampleCache.get(sampleX, sampleZ);
if (bx == null) {
bx = sharedBiomes == null ? null : sharedBiomes.get((int) sampleX, (int) sampleZ);
if (bx == null) {
ChunkBakeCache.enterSparse();
try {
bx = complex.baseBiomeStream.get(xx, zz);
} finally {
ChunkBakeCache.exitSparse();
}
if (sharedBiomes != null) {
sharedBiomes.put((int) sampleX, (int) sampleZ, bx);
}
Object known = sampleCache.get(sampleX, sampleZ);
if (known == null && sharedBiomes != null) {
known = sharedBiomes.get((int) sampleX, (int) sampleZ);
if (known != null) {
sampleCache.put(sampleX, sampleZ, known);
}
sampleCache.put(sampleX, sampleZ, bx);
}
GeneratorBounds bounds = complex.resolveGeneratorBounds(engine, generators, bx, cachedBounds, localBounds);
return bounds.noiseBounds;
if (known instanceof IrisBiome biome) {
return bounds(biome);
}
ChunkBakeCache.enterSparse();
try {
return regional == null ? bounds(remember(sampleX, sampleZ, complex.baseBiomeStream.get(xx, zz)))
: regionalBounds(known, sampleX, sampleZ);
} finally {
ChunkBakeCache.exitSparse();
}
} catch (Throwable e) {
GenerationFailures.rethrowEngineFailure(e);
long now = System.currentTimeMillis();
@@ -2696,6 +2769,49 @@ public class IrisComplex implements DataProvider {
return ZERO_NOISE_BOUNDS;
}
private NoiseBounds regionalBounds(Object known, double x, double z) {
RegionBounds.Pick pick = known instanceof RegionBounds.Pick picked ? picked : null;
IrisRegion region = pick != null ? pick.region() : known instanceof IrisRegion cached ? cached : null;
if (region == null) {
IrisBiome baked = bakedBiomes.baked((int) x, (int) z);
if (baked != null) {
return bounds(remember(x, z, baked));
}
region = remember(x, z, complex.contextRegionStream.get(x, z));
}
RegionBounds.Constants constants = regional.of(region);
NoiseBounds constant = constants == null ? null : constants.any(group);
if (constant != null) {
return constant;
}
InferredType type = pick != null ? pick.type() : complex.bridgeStream.get(x, z);
constant = constants == null ? null : constants.typed(type, group);
if (constant != null) {
return constant;
}
IrisBiome picked = pick != null ? pick.biome() : complex.createInferredBiomeStream(region, type).get(x, z);
constant = regional.pick(picked, group);
if (constant != null) {
if (pick == null) {
remember(x, z, new RegionBounds.Pick(region, type, picked));
}
return constant;
}
return bounds(remember(x, z, complex.implode(picked, x, z)));
}
private NoiseBounds bounds(IrisBiome biome) {
return complex.resolveGeneratorBounds(engine, generators, biome, cachedBounds, localBounds).noiseBounds;
}
private <T> T remember(double x, double z, T sample) {
if (sharedBiomes != null) {
sharedBiomes.put((int) x, (int) z, sample);
}
sampleCache.put(x, z, sample);
return sample;
}
}
/**
@@ -2705,13 +2821,13 @@ public class IrisComplex implements DataProvider {
private static final class CoordinateBiomeCache {
private final long[] xBits;
private final long[] zBits;
private final IrisBiome[] values;
private final Object[] values;
private final int mask;
private CoordinateBiomeCache(int capacity) {
xBits = new long[capacity];
zBits = new long[capacity];
values = new IrisBiome[capacity];
values = new Object[capacity];
mask = capacity - 1;
}
@@ -2719,20 +2835,20 @@ public class IrisComplex implements DataProvider {
Arrays.fill(values, null);
}
private IrisBiome get(double x, double z) {
private Object get(double x, double z) {
long xb = Double.doubleToLongBits(x);
long zb = Double.doubleToLongBits(z);
int slot = mix(xb, zb) & mask;
return xBits[slot] == xb && zBits[slot] == zb ? values[slot] : null;
}
private void put(double x, double z, IrisBiome biome) {
private void put(double x, double z, Object sample) {
long xb = Double.doubleToLongBits(x);
long zb = Double.doubleToLongBits(z);
int slot = mix(xb, zb) & mask;
xBits[slot] = xb;
zBits[slot] = zb;
values[slot] = biome;
values[slot] = sample;
}
private int mix(long xb, long zb) {
@@ -2784,7 +2900,4 @@ public class IrisComplex implements DataProvider {
transitionDisplacement.clear();
}
}
private record HydrologyBaseHeights(IrisComplex owner, Long2DoubleOpenHashMap heights) {
}
}
@@ -0,0 +1,120 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.InferredType;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.volmlib.util.interpolation.NoiseBounds;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
final class RegionBounds {
private final Map<IrisRegion, Constants> regions;
private final Map<IrisBiome, NoiseBounds[]> picks;
private RegionBounds(Map<IrisRegion, Constants> regions, Map<IrisBiome, NoiseBounds[]> picks) {
this.regions = regions;
this.picks = picks;
}
static RegionBounds compile(Collection<IrisRegion> regions, int groups, Reach reach, GroupBounds bounds) {
IdentityHashMap<IrisRegion, Constants> compiled = new IdentityHashMap<>(Math.max(16, regions.size() * 2));
IdentityHashMap<IrisBiome, NoiseBounds[]> picks = new IdentityHashMap<>();
for (IrisRegion region : regions) {
if (compiled.containsKey(region)) {
continue;
}
NoiseBounds[] land = typeConstants(reach.picks(region, InferredType.LAND), groups, reach, bounds, picks);
NoiseBounds[] sea = typeConstants(reach.picks(region, InferredType.SEA), groups, reach, bounds, picks);
NoiseBounds[] any = new NoiseBounds[groups];
for (int group = 0; group < groups; group++) {
any[group] = land[group] != null && sea[group] != null && sameBits(land[group], sea[group]) ? land[group] : null;
}
compiled.put(region, new Constants(any, land, sea));
}
return new RegionBounds(compiled, picks);
}
Constants of(IrisRegion region) {
return regions.get(region);
}
NoiseBounds pick(IrisBiome picked, int group) {
NoiseBounds[] constants = picks.get(picked);
return constants == null ? null : constants[group];
}
private static NoiseBounds[] typeConstants(List<IrisBiome> picked, int groups, Reach reach, GroupBounds bounds,
Map<IrisBiome, NoiseBounds[]> picks) {
if (picked.isEmpty()) {
return new NoiseBounds[groups];
}
Set<IrisBiome> reached = Collections.newSetFromMap(new IdentityHashMap<>());
for (IrisBiome biome : picked) {
Collection<IrisBiome> descendants = reach.descendants(biome);
reached.addAll(descendants);
if (!picks.containsKey(biome)) {
picks.put(biome, constants(descendants, groups, bounds));
}
}
return constants(reached, groups, bounds);
}
private static NoiseBounds[] constants(Collection<IrisBiome> biomes, int groups, GroupBounds bounds) {
NoiseBounds[] constants = new NoiseBounds[groups];
for (int group = 0; group < groups; group++) {
constants[group] = constant(biomes, group, bounds);
}
return constants;
}
private static NoiseBounds constant(Collection<IrisBiome> biomes, int group, GroupBounds bounds) {
NoiseBounds first = null;
for (IrisBiome biome : biomes) {
NoiseBounds next = bounds.of(group, biome);
if (first == null) {
first = next;
} else if (!sameBits(first, next)) {
return null;
}
}
return first;
}
private static boolean sameBits(NoiseBounds first, NoiseBounds second) {
return Double.doubleToRawLongBits(first.min()) == Double.doubleToRawLongBits(second.min())
&& Double.doubleToRawLongBits(first.max()) == Double.doubleToRawLongBits(second.max());
}
record Constants(NoiseBounds[] any, NoiseBounds[] land, NoiseBounds[] sea) {
NoiseBounds any(int group) {
return any[group];
}
NoiseBounds typed(InferredType type, int group) {
return switch (type) {
case LAND -> land[group];
case SEA -> sea[group];
case SHORE, CAVE -> null;
};
}
}
record Pick(IrisRegion region, InferredType type, IrisBiome biome) {
}
interface Reach {
List<IrisBiome> picks(IrisRegion region, InferredType type);
Collection<IrisBiome> descendants(IrisBiome biome);
}
@FunctionalInterface
interface GroupBounds {
NoiseBounds of(int group, IrisBiome biome);
}
}
@@ -0,0 +1,76 @@
package art.arcane.iris.generation.runtime;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
final class SharedBaseHeights {
static final int MINIMUM_CAPACITY = 1 << 16;
static final int MAXIMUM_CAPACITY = 1 << 21;
static final long BYTES_PER_ENTRY = 20L;
static final long HEAP_SHARE = 256L;
private static final VarHandle INTS = MethodHandles.arrayElementVarHandle(int[].class);
private static final VarHandle LONGS = MethodHandles.arrayElementVarHandle(long[].class);
private final long[] keys;
private final long[] values;
private final int[] stamps;
private final int mask;
SharedBaseHeights(int capacity) {
if (capacity < 2 || Integer.bitCount(capacity) != 1) {
throw new IllegalArgumentException("capacity must be a power of two of at least 2");
}
this.keys = new long[capacity];
this.values = new long[capacity];
this.stamps = new int[capacity];
this.mask = capacity - 1;
}
static int capacityFor(long maxMemory) {
long entries = Math.max(0L, maxMemory) / HEAP_SHARE / BYTES_PER_ENTRY;
long bounded = Math.max(MINIMUM_CAPACITY, Math.min(MAXIMUM_CAPACITY, entries));
return Integer.highestOneBit((int) bounded);
}
double get(int x, int z) {
long key = key(x, z);
int slot = slot(key);
int before = (int) INTS.getAcquire(stamps, slot);
if (before == 0 || (before & 1) != 0) {
return Double.NaN;
}
long storedKey = (long) LONGS.getAcquire(keys, slot);
long value = (long) LONGS.getAcquire(values, slot);
VarHandle.loadLoadFence();
int after = (int) INTS.getAcquire(stamps, slot);
if (before != after || storedKey != key) {
return Double.NaN;
}
return Double.longBitsToDouble(value);
}
void put(int x, int z, double height) {
if (!Double.isFinite(height)) {
return;
}
long key = key(x, z);
int slot = slot(key);
int stamp = (int) INTS.getAcquire(stamps, slot);
if ((stamp & 1) != 0 || !INTS.compareAndSet(stamps, slot, stamp, stamp + 1)) {
return;
}
LONGS.setRelease(keys, slot, key);
LONGS.setRelease(values, slot, Double.doubleToRawLongBits(height));
INTS.setRelease(stamps, slot, stamp + 2);
}
private int slot(long key) {
long mixed = (key ^ (key >>> 30)) * 0xBF58476D1CE4E5B9L;
mixed = (mixed ^ (mixed >>> 27)) * 0x94D049BB133111EBL;
return (int) ((mixed ^ (mixed >>> 31)) & mask);
}
private static long key(int x, int z) {
return ((long) x << 32) | (z & 0xFFFFFFFFL);
}
}
@@ -1,7 +1,5 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.generation.biome.IrisBiome;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.Objects;
@@ -49,7 +47,7 @@ final class SharedBoundsBiomes {
return (int) ((mixed ^ (mixed >>> 31)) & mask);
}
IrisBiome get(int x, int z) {
Object get(int x, int z) {
long key = key(x, z);
int slot = slot(x, z);
int before = (int) INTS.getAcquire(stamps, slot);
@@ -63,11 +61,11 @@ final class SharedBoundsBiomes {
if (before != after || storedKey != key) {
return null;
}
return (IrisBiome) value;
return value;
}
void put(int x, int z, IrisBiome biome) {
if (biome == null) {
void put(int x, int z, Object sample) {
if (sample == null) {
return;
}
int slot = slot(x, z);
@@ -76,7 +74,7 @@ final class SharedBoundsBiomes {
return;
}
LONGS.setRelease(keys, slot, key(x, z));
OBJECTS.setRelease(values, slot, biome);
OBJECTS.setRelease(values, slot, sample);
INTS.setRelease(stamps, slot, stamp + 2);
}
@@ -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;
}
@@ -39,6 +39,13 @@ public final class BakedStream<T> extends BasicStream<T> implements ChunkFillabl
return value;
}
@SuppressWarnings("unchecked")
public T baked(int blockX, int blockZ) {
int index = ((blockZ & 15) << 4) | (blockX & 15);
ChunkPlanes.Plane values = cache.planeIfPresent(plane, blockX >> 4, blockZ >> 4);
return values != null && values.ready(index) ? (T) values.objectAt(index) : null;
}
@Override
public T get(double x, double y, double z) {
return scalar.get(x, y, z);
@@ -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 {
@@ -268,6 +268,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
@Override
public void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
retention = createRetention(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
hydrology.bound(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
}
private PregenChunkRetention createRetention(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
@@ -219,6 +219,11 @@ public class MedievalPregenMethod implements PregeneratorMethod {
return false;
}
@Override
public void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
hydrology.bound(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
}
@Override
public void onRegionsAhead(List<long[]> regions) {
if (!closing.get()) {
@@ -13,6 +13,7 @@ public final class PregenHydrology implements AutoCloseable {
private final Supplier<Engine> engine;
private HydrologyRuntime owner;
private HydrologyPregeneration epoch;
private int[] bounds;
private boolean closed;
public PregenHydrology(Supplier<Engine> engine) {
@@ -28,12 +29,21 @@ public final class PregenHydrology implements AutoCloseable {
withdraw();
owner = runtime;
epoch = runtime == null ? null : runtime.openPregeneration();
applyBounds();
}
if (epoch != null) {
epoch.prepare(regions);
}
}
public synchronized void bound(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
if (closed) {
return;
}
bounds = new int[]{minRegionX, minRegionZ, maxRegionX, maxRegionZ};
applyBounds();
}
@Override
public synchronized void close() {
closed = true;
@@ -41,6 +51,12 @@ public final class PregenHydrology implements AutoCloseable {
owner = null;
}
private void applyBounds() {
if (epoch != null && bounds != null) {
epoch.bound(bounds[0], bounds[1], bounds[2], bounds[3]);
}
}
private void withdraw() {
if (epoch != null) {
epoch.close();
@@ -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);
@@ -1,80 +0,0 @@
package art.arcane.iris.generation.hydrology.engine;
import art.arcane.iris.generation.hydrology.grid.HeightLattice;
import art.arcane.iris.generation.hydrology.grid.HydrologySampleBuffer;
import art.arcane.iris.generation.hydrology.grid.HydrologyTerrainSource;
import org.junit.Test;
import java.util.concurrent.atomic.AtomicLong;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
public class FineLatticeSourceTest {
@Test
public void windowLatticeReadsItsNodesFromTheFineLattice() {
CountingTerrain terrain = new CountingTerrain();
HeightLattice fine = new HeightLattice(terrain, 4, false, 1 << 16);
HeightLattice window = new HeightLattice(new FineLatticeSource(fine, terrain), 8, false, 1 << 16);
fine.fill(0, 0, 64, 64, new HydrologySampleBuffer(64 * 64));
assertEquals(4096L, terrain.samples.get());
HydrologySampleBuffer held = new HydrologySampleBuffer(32 * 32);
window.fill(0, 0, 32, 32, held);
assertEquals(4096L, terrain.samples.get());
assertHeights(held, 0, 0, 32);
HydrologySampleBuffer fresh = new HydrologySampleBuffer(16 * 16);
window.fill(32, 0, 16, 16, fresh);
assertEquals(4096L + 256L, terrain.samples.get());
assertHeights(fresh, 32, 0, 16);
fine.fill(64, 0, 32, 32, new HydrologySampleBuffer(32 * 32));
assertEquals(4096L + 1024L, terrain.samples.get());
FineLatticeSource source = new FineLatticeSource(fine, terrain);
assertThrows(IllegalArgumentException.class, () -> source.sample(new int[]{8}, new int[]{8}, new HydrologySampleBuffer(1), true));
assertThrows(IllegalArgumentException.class, () -> source.sample(new int[]{6}, new int[]{8}, new HydrologySampleBuffer(1), false));
}
private static void assertHeights(HydrologySampleBuffer samples, int cellX0, int cellZ0, int side) {
for (int index = 0; index < side * side; index++) {
int x = (cellX0 + index % side) * 8;
int z = (cellZ0 + index / side) * 8;
assertEquals(x + "," + z, Float.floatToRawIntBits(CountingTerrain.height(x, z)), Float.floatToRawIntBits(samples.height()[index]));
}
}
private static final class CountingTerrain implements HydrologyTerrainSource {
private final AtomicLong samples = new AtomicLong();
private static float height(int x, int z) {
return 64f + (float) Math.sin(x * 0.013D) * 9f + (float) Math.cos(z * 0.021D) * 7f;
}
@Override
public void sample(int[] xs, int[] zs, HydrologySampleBuffer out, boolean baseOnly) {
samples.addAndGet(xs.length);
for (int index = 0; index < xs.length; index++) {
out.height()[index] = height(xs[index], zs[index]);
out.permission()[index] = HydrologySampleBuffer.ALLOW_STREAMS | HydrologySampleBuffer.ALLOW_PONDS;
out.rainfall()[index] = 1f;
out.pondDensity()[index] = 1f;
}
}
@Override
public int seaLevel() {
return 40;
}
@Override
public int minY() {
return 0;
}
@Override
public int maxY() {
return 384;
}
}
}
@@ -67,6 +67,7 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BooleanSupplier;
import java.util.stream.Stream;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -215,6 +216,34 @@ public class HydrologyRuntimeTest {
assertEquals(planned + 1L, runtime.previewRevision());
}
@Test(timeout = 180_000L)
public void concurrentDemandsShareOneRasterBuild() throws Exception {
FakePlanners planners = new FakePlanners(settings(0, false));
HydrologyRuntime runtime = runtime(planners, () -> false);
runtime.prepareWindowsAround(0, 0, 0, 0, 1).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
CountDownLatch gate = new CountDownLatch(1);
planners.naturalGate = gate;
AtomicReference<ChunkWaterRaster> first = new AtomicReference<>();
AtomicReference<ChunkWaterRaster> second = new AtomicReference<>();
Thread leader = new Thread(() -> first.set(runtime.raster(0, 0)));
Thread follower = new Thread(() -> second.set(runtime.raster(0, 0)));
try {
leader.start();
await(() -> planners.naturalSamples.get() > 0, "the first demand starts sampling");
follower.start();
await(() -> follower.getState() == Thread.State.WAITING || follower.getState() == Thread.State.TIMED_WAITING,
"the second demand waits");
assertEquals(0, planners.naturalSamplesOn(follower));
} finally {
gate.countDown();
leader.join(TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS));
follower.join(TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS));
}
assertFalse(first.get().provisional());
assertSame(first.get(), second.get());
assertEquals(0, planners.naturalSamplesOn(follower));
}
@Test(timeout = 180_000L)
public void failedRasterDoesNotAdvancePreviewRevision() throws Exception {
FakePlanners planners = new FakePlanners(settings(0, false));
@@ -414,6 +443,35 @@ public class HydrologyRuntimeTest {
assertEquals(3, prefetches.get());
}
@Test(timeout = 180_000L)
public void boundedPregenerationPrefetchesOnlyWindowsItsChunksNeed() {
FakePlanners planners = new FakePlanners(settings());
HydrologyPlanCache cache = spy(new HydrologyPlanCache(planners.settings, planners::region, planners::window,
pool(planners, 2), () -> false));
List<int[]> prepared = Collections.synchronizedList(new ArrayList<>());
doAnswer(invocation -> {
prepared.add(new int[]{invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2), invocation.getArgument(3),
invocation.getArgument(4)});
return CompletableFuture.completedFuture(null);
}).when(cache).prepareWindowsAround(anyInt(), anyInt(), anyInt(), anyInt(), anyInt());
HydrologyRuntime runtime = new HydrologyRuntime(planners.settings, cache, planners::natural, () -> false, planners.ticker::get);
runtimes.add(runtime);
int chunks = WindowKey.SIZE >> 4;
HydrologyPregeneration pregeneration = runtime.openPregeneration();
pregeneration.bound(0, 0, 1, 1);
runtime.raster(0, 0);
assertArrayEquals(new int[]{-chunks, -chunks, 3 * chunks - 1, 3 * chunks - 1, 0}, prepared.getLast());
runtime.raster(chunks + 5, chunks + 5);
assertArrayEquals(new int[]{-chunks, -chunks, 3 * chunks - 1, 3 * chunks - 1, 0}, prepared.getLast());
runtime.raster(-3 * chunks, 0);
assertArrayEquals(new int[]{-3 * chunks, 0, -3 * chunks, 0, 2}, prepared.getLast());
pregeneration.close();
runtime.raster(1, 1);
assertArrayEquals(new int[]{1, 1, 1, 1, 2}, prepared.getLast());
}
@Test(timeout = 180_000L)
public void preparePregenerationPlansRegionsInOrder() throws Exception {
FakePlanners planners = new FakePlanners(settings());
@@ -154,10 +154,9 @@ public class IrisHydrologyTerrainSourceTest {
HydrologySampleBuffer rect = new HydrologySampleBuffer(32 * 32);
fine.fill(-600, 400, 32, 32, rect);
assertMatchesScalar(reference, "fine rect", rect, nodes(-600, 400, 32, 1, 4), false);
HeightLattice window = new HeightLattice(new FineLatticeSource(fine, source), 8, false, 1 << 16);
HydrologySampleBuffer strided = new HydrologySampleBuffer(16 * 16);
window.fill(-150, 300, 16, 16, 3, strided);
assertMatchesScalar(reference, "window strided", strided, nodes(-150, 300, 16, 3, 8), false);
fine.fill(-300, 600, 16, 16, 6, strided);
assertMatchesScalar(reference, "fine strided", strided, nodes(-300, 600, 16, 6, 4), false);
HeightLattice coarse = new HeightLattice(source, 64, true, 1 << 12);
HydrologySampleBuffer coarseBase = new HydrologySampleBuffer(16 * 16);
coarse.fill(-8, -8, 16, 16, coarseBase);
@@ -287,6 +287,31 @@ public class HeightLatticeTest {
assertThrows(IllegalArgumentException.class, () -> lattice.fill(cellXs, cellZs, new HydrologySampleBuffer(5)));
}
@Test
public void sampledColumnsAreReadOnceAndMatchTheSource() {
CountingSource source = new CountingSource();
HeightLattice lattice = new HeightLattice(source, 4, false, 1 << 16);
int[] xs = {5, -13, 5, 1027, -13, 6};
int[] zs = {-2, 77, -2, 3, 77, -2};
HydrologySampleBuffer first = new HydrologySampleBuffer(xs.length);
lattice.sampleColumns(xs, zs, first);
assertEquals(4, source.samples());
HydrologySampleBuffer second = new HydrologySampleBuffer(xs.length);
lattice.sampleColumns(xs, zs, second);
assertEquals(4, source.samples());
HydrologySampleBuffer expected = new HydrologySampleBuffer(xs.length);
new CountingSource().sample(xs, zs, expected, false);
for (int index = 0; index < xs.length; index++) {
assertEquals(Float.floatToIntBits(expected.height()[index]), Float.floatToIntBits(first.height()[index]));
assertEquals(Float.floatToIntBits(expected.height()[index]), Float.floatToIntBits(second.height()[index]));
assertEquals(expected.ocean()[index], second.ocean()[index]);
assertEquals(expected.permission()[index], second.permission()[index]);
assertEquals(expected.fluidProfile()[index], second.fluidProfile()[index]);
}
lattice.sampleColumns(new int[0], new int[0], new HydrologySampleBuffer(0));
assertEquals(1, source.calls());
}
private static void fillWindow(HeightLattice lattice, int wx, int wz, int cell, boolean baseOnly) {
assertFill(lattice, cell, baseOnly, (wx - 1) * WINDOW, (wz - 1) * WINDOW, AREA, AREA);
}
@@ -1045,7 +1045,7 @@ public class HydrologyPlanCacheTest {
}
private static WindowPlanner windowPlanner(HydrologySettings settings, SyntheticTerrain terrain) {
return new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.CELL, false, HydrologyPlanCache.WINDOW_LATTICE_CELLS),
return new WindowPlanner(settings,
new HeightLattice(terrain, WindowPlanner.FINE_CELL, false, HydrologyPlanCache.FINE_LATTICE_CELLS), new PondPlanner(settings),
new ReachShaper(settings));
}
@@ -81,7 +81,7 @@ public class HydrologyPlanCodecTest {
assertEquals(planned, HydrologyPlanCodec.decodeRegion(settings, HydrologyPlanCodec.encode(settings, planned)).orElseThrow());
regions.add(planned);
}
WindowPlan planned = new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.CELL, false, 1 << 18),
WindowPlan planned = new WindowPlanner(settings,
new HeightLattice(terrain, WindowPlanner.FINE_CELL, false, 1 << 21), new PondPlanner(settings), new ReachShaper(settings))
.plan(LAND, regions);
assertFalse(planned.reaches().isEmpty());
@@ -83,7 +83,7 @@ public class WindowPlannerTest {
new int[]{head, head}, new float[]{2f, 2f}, new float[]{2f, 2f}, new ReachStyle[]{ReachStyle.RUN, ReachStyle.RUN}, 1000f);
return new ShapedNetwork(List.of(outlet), List.of(pond.withOutletReachId(outlet.id())));
}).when(shaper).shape(any(), any(), any(), any(), any(), any(), anyLong());
WindowPlanner planner = new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.CELL, false, 1 << 18),
WindowPlanner planner = new WindowPlanner(settings,
new HeightLattice(terrain, WindowPlanner.FINE_CELL, false, 1 << 18), ponds, shaper);
WindowComputation settled = planner.compute(ORIGIN, noRegions(ORIGIN));
@@ -587,7 +587,7 @@ public class WindowPlannerTest {
}
return network;
}).when(shaper).shape(any(), any(), any(), any(), any(), any(), anyLong());
return new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.CELL, false, 1 << 18),
return new WindowPlanner(settings,
new HeightLattice(terrain, WindowPlanner.FINE_CELL, false, 1 << 21), new PondPlanner(settings), shaper);
}
@@ -715,7 +715,7 @@ public class WindowPlannerTest {
}
private static WindowPlanner planner(HydrologySettings settings, SyntheticTerrain terrain) {
return new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.CELL, false, 1 << 18),
return new WindowPlanner(settings,
new HeightLattice(terrain, WindowPlanner.FINE_CELL, false, 1 << 21), new PondPlanner(settings), new ReachShaper(settings));
}
@@ -0,0 +1,95 @@
package art.arcane.iris.generation.hydrology.raster;
import art.arcane.iris.generation.hydrology.config.IrisBankStyle;
import art.arcane.iris.generation.hydrology.config.IrisHydrologyMaterials;
import art.arcane.iris.generation.hydrology.water.HydrologyTier;
import art.arcane.iris.generation.hydrology.water.Reach;
import art.arcane.iris.generation.hydrology.water.ReachStyle;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.SplittableRandom;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class ReachSegmentsTest {
private static final HydrologyTier TIER = new HydrologyTier("test", 0f, 1f, 4f, 1f, 0.4f, 1, true, 6, 4, IrisBankStyle.SMOOTH,
false, new IrisHydrologyMaterials(), "water", List.of());
@Test
public void coveredChannelLookupsMatchTheFullScan() {
SplittableRandom random = new SplittableRandom(1337L);
int hits = 0;
for (int trial = 0; trial < 40; trial++) {
int originX = random.nextInt(-2000, 2000) << 4;
int originZ = random.nextInt(-2000, 2000) << 4;
List<Reach> reaches = new ArrayList<>();
int count = 1 + random.nextInt(12);
for (int walk = 0; walk < count; walk++) {
reaches.add(randomWalk(trial * 100L + walk, originX + 8, originZ + 8, random));
}
ReachSegments scanned = segments(reaches, originX, originZ);
ReachSegments covered = segments(reaches, originX, originZ);
covered.coverChannels(originX - 17, originZ - 17, originX + 32, originZ + 32);
ReachCursor scan = new ReachCursor();
ReachCursor cover = new ReachCursor();
for (int z = originZ - 20; z <= originZ + 35; z++) {
for (int x = originX - 20; x <= originX + 35; x++) {
ReachSegments.Scope scope = random.nextInt(4) == 0 ? ReachSegments.Scope.SHORE : ReachSegments.Scope.CHANNEL;
scanned.nearest(x, z, scope, scan);
covered.nearest(x, z, scope, cover);
String at = scope + " at " + x + "," + z;
assertSameChoice(at + " channel", scan.channel, cover.channel);
assertSameChoice(at + " bank", scan.bank, cover.bank);
assertSameChoice(at + " edge", scan.edge, cover.edge);
if (scan.channel.reach != null) {
hits++;
}
}
}
}
assertTrue("random walks cross the covered area, hits=" + hits, hits > 1000);
}
private static void assertSameChoice(String what, ReachCursor.Choice expected, ReachCursor.Choice actual) {
assertSame(what, expected.reach, actual.reach);
assertEquals(what, expected.segment, actual.segment);
assertEquals(what, expected.index, actual.index);
assertEquals(what, Double.doubleToLongBits(expected.t), Double.doubleToLongBits(actual.t));
assertEquals(what, Double.doubleToLongBits(expected.distance), Double.doubleToLongBits(actual.distance));
}
private static ReachSegments segments(List<Reach> reaches, int originX, int originZ) {
ReachSegments segments = new ReachSegments(16);
for (Reach reach : reaches) {
segments.addNear(reach, originX - 1f, originZ - 1f, originX + 16f, originZ + 16f, ChunkWaterRaster.MAX_SHORE_DISTANCE + 1f);
}
return segments;
}
private static Reach randomWalk(long id, int centerX, int centerZ, SplittableRandom random) {
int points = 2 + random.nextInt(30);
float[] xs = new float[points];
float[] zs = new float[points];
float[] halfWidth = new float[points];
xs[0] = centerX + (float) random.nextDouble(-60D, 60D);
zs[0] = centerZ + (float) random.nextDouble(-60D, 60D);
halfWidth[0] = (float) random.nextDouble(0.5D, 9D);
for (int point = 1; point < points; point++) {
xs[point] = xs[point - 1] + (float) random.nextDouble(-12D, 12D);
zs[point] = zs[point - 1] + (float) random.nextDouble(-12D, 12D);
halfWidth[point] = random.nextInt(8) == 0 ? halfWidth[point - 1] : (float) random.nextDouble(0.5D, 9D);
}
int[] grade = new int[points];
float[] depth = new float[points];
ReachStyle[] style = new ReachStyle[points];
Arrays.fill(grade, 64);
Arrays.fill(depth, 1f);
Arrays.fill(style, ReachStyle.RUN);
return new Reach(id, TIER, -1L, xs, zs, grade, halfWidth, depth, style, 100f);
}
}
@@ -15,6 +15,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.IntFunction;
@@ -50,18 +51,37 @@ public class CavePoolFillerTest {
return 0;
});
for (int chunkX = 0; chunkX < 1024; chunkX++) {
filler.fill(chunkX, 0, carved, writer, SEED);
filler.fill(chunkX, 0, () -> carved, writer, SEED);
}
assertTrue("half density samples approximately half the candidate columns", columns.size() > 3600 && columns.size() < 4500);
Set<Long> repeated = new HashSet<>(columns);
columns.clear();
for (int chunkX = 1023; chunkX >= 0; chunkX--) {
filler.fill(chunkX, 0, carved, writer, SEED);
filler.fill(chunkX, 0, () -> carved, writer, SEED);
}
assertEquals(repeated, columns);
verifyNoInteractions(writer);
}
@Test
public void chunksWithoutAPoolSiteNeverOpenTheirCaves() {
IrisHydrology config = new IrisHydrology().setDensity(0.5D);
config.getUnderground().getCavePools().setDensity(0.05D);
CavePoolFiller filler = new CavePoolFiller(HydrologySettings.compile(config, SEED, 63, 0, 384));
CarvedTerrain carved = mock(CarvedTerrain.class);
MantleWriter writer = mock(MantleWriter.class);
AtomicInteger opened = new AtomicInteger();
int chunks = 1024;
for (int chunkX = 0; chunkX < chunks; chunkX++) {
filler.fill(chunkX, 0, () -> {
opened.incrementAndGet();
return carved;
}, writer, SEED);
}
assertTrue("only chunks drawing a pool site open their caves, opened " + opened.get(), opened.get() > 0 && opened.get() < chunks / 8);
verifyNoInteractions(writer);
}
@Test
public void zeroGlobalDensityDoesNotReadCaves() {
IrisHydrology config = new IrisHydrology().setDensity(0D);
@@ -69,7 +89,7 @@ public class CavePoolFillerTest {
CavePoolFiller filler = new CavePoolFiller(HydrologySettings.compile(config, SEED, 63, 0, 384));
CarvedTerrain carved = mock(CarvedTerrain.class);
MantleWriter writer = mock(MantleWriter.class);
filler.fill(0, 0, carved, writer, SEED);
filler.fill(0, 0, () -> carved, writer, SEED);
verifyNoInteractions(carved, writer);
}
@@ -143,10 +163,10 @@ public class CavePoolFillerTest {
MantleWriter forwardWriter = writer(forward);
MantleWriter reverseWriter = writer(reverse);
for (int index = 0; index < 9; index++) {
new CavePoolFiller(settings).fill(index % 3 - 1, index / 3 - 1, world, forwardWriter, SEED);
new CavePoolFiller(settings).fill(index % 3 - 1, index / 3 - 1, () -> world, forwardWriter, SEED);
}
for (int index = 8; index >= 0; index--) {
new CavePoolFiller(settings).fill(index % 3 - 1, index / 3 - 1, world, reverseWriter, SEED);
new CavePoolFiller(settings).fill(index % 3 - 1, index / 3 - 1, () -> world, reverseWriter, SEED);
}
assertFalse(forward.isEmpty());
@@ -284,7 +304,7 @@ public class CavePoolFillerTest {
private static Map<Long, HydrologyCaveCell> fill(HydrologySettings settings, VoxelWorld world, int chunkX, int chunkZ) {
Map<Long, HydrologyCaveCell> written = new HashMap<>();
new CavePoolFiller(settings).fill(chunkX, chunkZ, world, writer(written), SEED);
new CavePoolFiller(settings).fill(chunkX, chunkZ, () -> world, writer(written), SEED);
return written;
}
@@ -324,7 +324,7 @@ public class MantleCarvedTerrainTest {
for (SeaInlet inlet : complex.getHydrologyRuntime().inlets(0, chunkZ)) {
rasterizer.write(inlet, 0, chunkZ, writer, HydrologyCaveCell.SEA_FLUID, "", SeaInletRasterizer.SubterrainFluid.NOWHERE);
}
new CavePoolFiller(settings).fill(0, chunkZ, new MantleCarvedTerrain(writer, complex, engine, 0, chunkZ), writer, SEED);
new CavePoolFiller(settings).fill(0, chunkZ, () -> new MantleCarvedTerrain(writer, complex, engine, 0, chunkZ), writer, SEED);
}
return cells;
}
@@ -22,11 +22,13 @@ import org.junit.rules.TemporaryFolder;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.SplittableRandom;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Answers.CALLS_REAL_METHODS;
import static org.mockito.ArgumentMatchers.any;
@@ -81,6 +83,24 @@ public class IrisComplexBakeParityTest {
}
}
@Test
public void regionConstantBoundsEqualFullBiomeBoundsBitForBit() throws Exception {
try (Statics ignored = new Statics(); Fixture shortcut = new Fixture(pack()); Fixture full = new Fixture(pack())) {
Field regionBounds = IrisComplex.class.getDeclaredField("regionBounds");
regionBounds.setAccessible(true);
assertNotNull(regionBounds.get(shortcut.complex));
regionBounds.set(full.complex, null);
SplittableRandom random = new SplittableRandom(0x5EEDL);
for (int column = 0; column < 2048; column++) {
int x = random.nextInt(-6400, 6400);
int z = random.nextInt(-6400, 6400);
assertEquals("column " + x + "," + z,
Double.doubleToRawLongBits(full.complex.getBaseTerrainHeightStream().getDouble(x, z)),
Double.doubleToRawLongBits(shortcut.complex.getBaseTerrainHeightStream().getDouble(x, z)));
}
}
}
@Test
public void batchedChunksEvaluateExactlyTheScalarGeneratorSet() throws Exception {
try (Statics ignored = new Statics(); Fixture batched = new Fixture(pack()); Fixture scalar = new Fixture(pack())) {
@@ -243,7 +243,7 @@ public class IrisComplexBoundsBiomeCacheTest {
constructor.setAccessible(true);
Object cache = constructor.newInstance(4096);
Method get = type.getDeclaredMethod("get", double.class, double.class);
Method put = type.getDeclaredMethod("put", double.class, double.class, IrisBiome.class);
Method put = type.getDeclaredMethod("put", double.class, double.class, Object.class);
get.setAccessible(true);
put.setAccessible(true);
IrisBiome first = new IrisBiome();
@@ -266,7 +266,7 @@ public class IrisComplexBoundsBiomeCacheTest {
}
Field values = type.getDeclaredField("values");
values.setAccessible(true);
assertEquals(4096, ((IrisBiome[]) values.get(cache)).length);
assertEquals(4096, ((Object[]) values.get(cache)).length);
}
private static ProceduralStream<IrisBiome> constant(IrisBiome biome) {
@@ -295,7 +295,7 @@ public class IrisComplexBoundsBiomeCacheTest {
constructor.setAccessible(true);
cache = constructor.newInstance();
sample = IrisComplex.class.getDeclaredMethod("sampleBoundsRaw", cacheClass, Engine.class,
IrisInterpolator.class, IrisGenerator[].class, double.class, double.class);
IrisInterpolator.class, int.class, IrisGenerator[].class, double.class, double.class);
sample.setAccessible(true);
boundsConstructor = Class.forName("art.arcane.iris.generation.runtime.IrisComplex$GeneratorBounds")
.getDeclaredConstructor(double.class, double.class);
@@ -313,7 +313,7 @@ public class IrisComplexBoundsBiomeCacheTest {
private NoiseBounds sample(Object gridCache, IrisInterpolator interpolator, double x, double z)
throws ReflectiveOperationException {
return (NoiseBounds) sample.invoke(complex, gridCache, null, interpolator, new IrisGenerator[0], x, z);
return (NoiseBounds) sample.invoke(complex, gridCache, null, interpolator, 0, new IrisGenerator[0], x, z);
}
private Object newGridCache() throws ReflectiveOperationException {
@@ -0,0 +1,136 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.terrain.InferredType;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.volmlib.util.interpolation.NoiseBounds;
import org.junit.Test;
import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
public class RegionBoundsTest {
private static final NoiseBounds FLAT = new NoiseBounds(0D, 0D);
private static final NoiseBounds HILLS = new NoiseBounds(4D, 30D);
@Test
public void groupsEveryReachableBiomeAgreesOnAreConstantForTheRegion() {
IrisBiome plains = new IrisBiome();
IrisBiome forest = new IrisBiome();
IrisBiome ocean = new IrisBiome();
IrisRegion region = new IrisRegion();
Map<IrisBiome, NoiseBounds[]> bounds = new IdentityHashMap<>();
bounds.put(plains, new NoiseBounds[]{FLAT, HILLS});
bounds.put(forest, new NoiseBounds[]{new NoiseBounds(0D, 0D), new NoiseBounds(2D, 30D)});
bounds.put(ocean, new NoiseBounds[]{FLAT, HILLS});
RegionBounds compiled = RegionBounds.compile(List.of(region), 2,
new Fixed(Map.of(InferredType.LAND, List.of(plains, forest), InferredType.SEA, List.of(ocean)), Map.of()),
(group, biome) -> bounds.get(biome)[group]);
RegionBounds.Constants constants = compiled.of(region);
assertBits(FLAT, constants.any(0));
assertBits(FLAT, constants.typed(InferredType.LAND, 0));
assertBits(FLAT, constants.typed(InferredType.SEA, 0));
assertNull(constants.any(1));
assertNull(constants.typed(InferredType.LAND, 1));
assertSame(HILLS, constants.typed(InferredType.SEA, 1));
assertSame(HILLS, compiled.pick(plains, 1));
assertBits(new NoiseBounds(2D, 30D), compiled.pick(forest, 1));
}
@Test
public void picksAreConstantOnlyWhenEveryDescendantAgrees() {
IrisBiome steppe = new IrisBiome();
IrisBiome meadow = new IrisBiome();
IrisBiome mesa = new IrisBiome();
IrisBiome canyon = new IrisBiome();
IrisRegion region = new IrisRegion();
Map<IrisBiome, NoiseBounds> bounds = new IdentityHashMap<>();
bounds.put(steppe, FLAT);
bounds.put(meadow, new NoiseBounds(0D, 0D));
bounds.put(mesa, FLAT);
bounds.put(canyon, HILLS);
RegionBounds compiled = RegionBounds.compile(List.of(region), 1,
new Fixed(Map.of(InferredType.LAND, List.of(steppe, mesa), InferredType.SEA, List.of()),
descendants(steppe, List.of(steppe, meadow), mesa, List.of(mesa, canyon))),
(group, biome) -> bounds.get(biome));
assertNull(compiled.of(region).typed(InferredType.LAND, 0));
assertSame(FLAT, compiled.pick(steppe, 0));
assertNull(compiled.pick(mesa, 0));
assertNull(compiled.pick(canyon, 0));
}
@Test
public void landAndSeaConstantsOnlyMergeWhenTheirBitsMatch() {
IrisBiome beach = new IrisBiome();
IrisBiome reef = new IrisBiome();
IrisRegion region = new IrisRegion();
Map<IrisBiome, NoiseBounds> bounds = new IdentityHashMap<>();
bounds.put(beach, new NoiseBounds(0D, 0D));
bounds.put(reef, new NoiseBounds(-0D, 0D));
RegionBounds.Constants constants = RegionBounds.compile(List.of(region), 1,
new Fixed(Map.of(InferredType.LAND, List.of(beach), InferredType.SEA, List.of(reef)), Map.of()),
(group, biome) -> bounds.get(biome)).of(region);
assertNull(constants.any(0));
assertSame(bounds.get(beach), constants.typed(InferredType.LAND, 0));
assertSame(bounds.get(reef), constants.typed(InferredType.SEA, 0));
}
@Test
public void emptyBiomeListsAndUnknownRegionsHaveNoConstant() {
IrisRegion region = new IrisRegion();
IrisBiome only = new IrisBiome();
RegionBounds compiled = RegionBounds.compile(List.of(region), 1,
new Fixed(Map.of(InferredType.LAND, List.of(only), InferredType.SEA, List.of()), Map.of()),
(group, biome) -> FLAT);
RegionBounds.Constants constants = compiled.of(region);
assertSame(FLAT, constants.typed(InferredType.LAND, 0));
assertNull(constants.typed(InferredType.SEA, 0));
assertNull(constants.any(0));
assertNull(constants.typed(InferredType.SHORE, 0));
assertNull(constants.typed(InferredType.CAVE, 0));
assertNull(compiled.of(new IrisRegion()));
assertNull(compiled.of(null));
assertNull(compiled.pick(new IrisBiome(), 0));
assertNull(compiled.pick(null, 0));
}
private static Map<IrisBiome, List<IrisBiome>> descendants(IrisBiome first, List<IrisBiome> firstDescendants,
IrisBiome second, List<IrisBiome> secondDescendants) {
Map<IrisBiome, List<IrisBiome>> descendants = new IdentityHashMap<>();
descendants.put(first, firstDescendants);
descendants.put(second, secondDescendants);
return descendants;
}
private static void assertBits(NoiseBounds expected, NoiseBounds actual) {
assertEquals(Double.doubleToRawLongBits(expected.min()), Double.doubleToRawLongBits(actual.min()));
assertEquals(Double.doubleToRawLongBits(expected.max()), Double.doubleToRawLongBits(actual.max()));
}
private record Fixed(Map<InferredType, List<IrisBiome>> picks, Map<IrisBiome, List<IrisBiome>> descendants)
implements RegionBounds.Reach {
@Override
public List<IrisBiome> picks(IrisRegion region, InferredType type) {
return picks.get(type);
}
@Override
public Collection<IrisBiome> descendants(IrisBiome biome) {
List<IrisBiome> known = descendants.get(biome);
return known == null ? List.of(biome) : known;
}
}
}
@@ -0,0 +1,87 @@
package art.arcane.iris.generation.runtime;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class SharedBaseHeightsTest {
@Test
public void storedColumnsComeBackWithTheirExactBits() {
SharedBaseHeights cache = new SharedBaseHeights(1 << 10);
double height = Math.nextUp(71.25D);
assertTrue(Double.isNaN(cache.get(12, -40)));
cache.put(12, -40, height);
assertEquals(Double.doubleToRawLongBits(height), Double.doubleToRawLongBits(cache.get(12, -40)));
assertTrue(Double.isNaN(cache.get(-40, 12)));
}
@Test
public void originMissesUntilItIsStored() {
SharedBaseHeights cache = new SharedBaseHeights(2);
assertTrue(Double.isNaN(cache.get(0, 0)));
cache.put(0, 0, -0D);
assertEquals(Double.doubleToRawLongBits(-0D), Double.doubleToRawLongBits(cache.get(0, 0)));
}
@Test
public void nonFiniteHeightsAreNeverStored() {
SharedBaseHeights cache = new SharedBaseHeights(2);
cache.put(3, 4, 64D);
cache.put(3, 4, Double.NaN);
cache.put(3, 4, Double.POSITIVE_INFINITY);
assertEquals(64D, cache.get(3, 4), 0D);
}
@Test
public void fullCoordinateDomainKeepsDistinctColumns() {
int[] coordinates = {0, 1, -1, 1 << 21, -(1 << 21), 30_000_000, -30_000_000,
Integer.MIN_VALUE, Integer.MAX_VALUE};
for (int x : coordinates) {
for (int z : coordinates) {
SharedBaseHeights cache = new SharedBaseHeights(2);
cache.put(x, z, 12D);
for (int otherX : coordinates) {
for (int otherZ : coordinates) {
if (x == otherX && z == otherZ) {
assertEquals(12D, cache.get(otherX, otherZ), 0D);
} else {
assertTrue(Double.isNaN(cache.get(otherX, otherZ)));
}
}
}
}
}
}
@Test
public void aCollidingColumnReplacesTheOldOne() {
SharedBaseHeights cache = new SharedBaseHeights(2);
cache.put(5, 9, 40D);
cache.put(6, 9, 41D);
cache.put(7, 9, 42D);
int stored = 0;
for (int x = 5; x <= 7; x++) {
double value = cache.get(x, 9);
if (!Double.isNaN(value)) {
assertEquals(35D + x, value, 0D);
stored++;
}
}
assertTrue(stored >= 1 && stored <= 2);
}
@Test
public void capacityFollowsTheHeapWithinBounds() {
assertEquals(SharedBaseHeights.MINIMUM_CAPACITY, SharedBaseHeights.capacityFor(0L));
assertEquals(SharedBaseHeights.MAXIMUM_CAPACITY, SharedBaseHeights.capacityFor(Long.MAX_VALUE));
int capacity = SharedBaseHeights.capacityFor(6L << 30);
assertEquals(1, Integer.bitCount(capacity));
assertTrue(capacity >= SharedBaseHeights.MINIMUM_CAPACITY && capacity <= SharedBaseHeights.MAXIMUM_CAPACITY);
}
}
@@ -146,7 +146,7 @@ public class SharedBoundsBiomesTest {
int xi = (int) ((state & Long.MAX_VALUE) % coordinates.length);
int zi = (int) ((state >>> 10) % coordinates.length);
cache.put(coordinates[xi], coordinates[zi], biomes[xi][zi]);
IrisBiome actual = cache.get(coordinates[xi], coordinates[zi]);
Object actual = cache.get(coordinates[xi], coordinates[zi]);
if (actual != null) {
hits.incrementAndGet();
if (actual != biomes[xi][zi]) {
@@ -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,
@@ -195,7 +195,8 @@ final class HydrologyPlanHarness {
double landTiles(WindowKey key) {
int cells = WindowKey.SIZE / WindowPlanner.CELL;
HydrologySampleBuffer samples = new HydrologySampleBuffer(cells * cells);
planners.windowLattice().fill(key.wx() * cells, key.wz() * cells, cells, cells, samples);
int stride = WindowPlanner.CELL / WindowPlanner.FINE_CELL;
planners.fineLattice().fill(key.wx() * cells * stride, key.wz() * cells * stride, cells, cells, stride, samples);
int land = 0;
for (int cell = 0; cell < cells * cells; cell++) {
if (!samples.ocean()[cell]) {