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; return false;
} }
@Override
public void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
hydrology.bound(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
}
@Override @Override
public void onRegionsAhead(List<long[]> regions) { public void onRegionsAhead(List<long[]> regions) {
if (!closing.get()) { if (!closing.get()) {
@@ -20,7 +20,7 @@ package art.arcane.iris.generation.chunk;
import art.arcane.volmlib.util.hunk.Hunk; import art.arcane.volmlib.util.hunk.Hunk;
public final class ColumnExtentListeningHunk<T> implements Hunk<T>, ColumnExtent, ColumnRunHunk<T> { public final class ColumnExtentListeningHunk<T> implements Hunk<T>, ColumnExtent, ColumnRunHunk<T>, ColumnReadHunk<T> {
private final Hunk<T> source; private final Hunk<T> source;
private final Listener<T> listener; private final Listener<T> listener;
@@ -46,6 +46,11 @@ public final class ColumnExtentListeningHunk<T> implements Hunk<T>, ColumnExtent
return source.getRaw(x, y, z); return source.getRaw(x, y, z);
} }
@Override
public void readColumn(int x, int z, int yFromInclusive, int yToInclusive, T[] target, int targetOffset) {
ColumnReadHunk.readColumn(source, x, z, yFromInclusive, yToInclusive, target, targetOffset);
}
@Override @Override
public int getWidth() { public int getWidth() {
return source.getWidth(); return source.getWidth();
@@ -0,0 +1,37 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.generation.chunk;
import art.arcane.volmlib.util.hunk.Hunk;
public interface ColumnReadHunk<T> {
void readColumn(int x, int z, int yFromInclusive, int yToInclusive, T[] target, int targetOffset);
@SuppressWarnings("unchecked")
static <T> void readColumn(Hunk<T> hunk, int x, int z, int yFromInclusive, int yToInclusive, T[] target,
int targetOffset) {
if (hunk instanceof ColumnReadHunk<?> reads) {
((ColumnReadHunk<T>) reads).readColumn(x, z, yFromInclusive, yToInclusive, target, targetOffset);
return;
}
for (int y = yFromInclusive; y <= yToInclusive; y++) {
target[targetOffset + y - yFromInclusive] = hunk.getRaw(x, y, z);
}
}
}
@@ -27,7 +27,8 @@ import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle; import java.lang.invoke.VarHandle;
import java.util.Arrays; import java.util.Arrays;
public final class SectionBuffer implements Hunk<NativeBlockState>, NativeSectionVolume, ColumnExtent, ColumnRunHunk<NativeBlockState> { public final class SectionBuffer implements Hunk<NativeBlockState>, NativeSectionVolume, ColumnExtent, ColumnRunHunk<NativeBlockState>,
ColumnReadHunk<NativeBlockState> {
private static final BoundBlockState AIR = BoundBlockState.of("AIR"); private static final BoundBlockState AIR = BoundBlockState.of("AIR");
private static final VarHandle SECTIONS = MethodHandles.arrayElementVarHandle(short[][].class); private static final VarHandle SECTIONS = MethodHandles.arrayElementVarHandle(short[][].class);
private static final VarHandle COLUMN_TOPS = MethodHandles.arrayElementVarHandle(int[].class); private static final VarHandle COLUMN_TOPS = MethodHandles.arrayElementVarHandle(int[].class);
@@ -43,6 +44,8 @@ public final class SectionBuffer implements Hunk<NativeBlockState>, NativeSectio
private PaletteEntry[] slots; private PaletteEntry[] slots;
private NativeBlockState[] states; private NativeBlockState[] states;
private int paletteSize; private int paletteSize;
private NativeBlockState air;
private PaletteEntry lastEntry;
public SectionBuffer(int height) { public SectionBuffer(int height) {
if (height <= 0) { if (height <= 0) {
@@ -76,12 +79,17 @@ public final class SectionBuffer implements Hunk<NativeBlockState>, NativeSectio
if (state == null) { if (state == null) {
return 0; return 0;
} }
PaletteEntry last = lastEntry;
if (last != null && last.state == state) {
return last.id;
}
PaletteEntry[] table = slots; PaletteEntry[] table = slots;
int mask = table.length - 1; int mask = table.length - 1;
int slot = System.identityHashCode(state) & mask; int slot = System.identityHashCode(state) & mask;
PaletteEntry entry; PaletteEntry entry;
while ((entry = table[slot]) != null) { while ((entry = table[slot]) != null) {
if (entry.state == state) { if (entry.state == state) {
lastEntry = entry;
return entry.id; return entry.id;
} }
slot = (slot + 1) & mask; slot = (slot + 1) & mask;
@@ -101,7 +109,7 @@ public final class SectionBuffer implements Hunk<NativeBlockState>, NativeSectio
@Override @Override
public NativeBlockState getRaw(int x, int y, int z) { public NativeBlockState getRaw(int x, int y, int z) {
if (y < 0 || y >= height) { if (y < 0 || y >= height) {
return AIR.get(); return air();
} }
short[] section = sections[y >> 4]; short[] section = sections[y >> 4];
if (section != null) { if (section != null) {
@@ -110,7 +118,7 @@ public final class SectionBuffer implements Hunk<NativeBlockState>, NativeSectio
return state(id); return state(id);
} }
} }
return AIR.get(); return air();
} }
@Override @Override
@@ -126,6 +134,32 @@ public final class SectionBuffer implements Hunk<NativeBlockState>, NativeSectio
return id == 0 ? null : state(id); return id == 0 ? null : state(id);
} }
@Override
public void readColumn(int x, int z, int yFromInclusive, int yToInclusive, NativeBlockState[] target, int targetOffset) {
int column = (z << 4) | x;
int shift = targetOffset - yFromInclusive;
int y = yFromInclusive;
while (y <= yToInclusive) {
if (y < 0 || y >= height) {
target[shift + y] = air();
y++;
continue;
}
int sectionIndex = y >> 4;
int last = Math.min(Math.min(yToInclusive, height - 1), (sectionIndex << 4) | 15);
short[] section = sections[sectionIndex];
if (section == null) {
Arrays.fill(target, shift + y, shift + last + 1, air());
} else {
for (int row = y; row <= last; row++) {
int id = section[((row & 15) << 8) | column] & 0xFFFF;
target[shift + row] = id == 0 ? air() : state(id);
}
}
y = last + 1;
}
}
@Override @Override
public void fillColumnRun(int x, int z, int yFromInclusive, int yToInclusive, NativeBlockState value) { public void fillColumnRun(int x, int z, int yFromInclusive, int yToInclusive, NativeBlockState value) {
if (value == null || yToInclusive < yFromInclusive) { if (value == null || yToInclusive < yFromInclusive) {
@@ -176,6 +210,15 @@ public final class SectionBuffer implements Hunk<NativeBlockState>, NativeSectio
} }
} }
private NativeBlockState air() {
NativeBlockState resolved = air;
if (resolved == null) {
resolved = AIR.get();
air = resolved;
}
return resolved;
}
private short[] writableSection(int sectionIndex) { private short[] writableSection(int sectionIndex) {
short[] section = sections[sectionIndex]; short[] section = sections[sectionIndex];
if (section != null) { if (section != null) {
@@ -28,9 +28,14 @@ import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KMap;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReferenceArray;
public final class IrisContext { public final class IrisContext {
private static final ThreadLocal<IrisContext> CONTEXT = new ThreadLocal<>(); private static final int SLOTS = 512;
private static final AtomicReferenceArray<Binding> BINDINGS = new AtomicReferenceArray<>(SLOTS);
private static final ThreadLocal<Binding> OVERFLOW = new ThreadLocal<>();
private static final AtomicInteger OVERFLOW_BINDINGS = new AtomicInteger();
private final Engine engine; private final Engine engine;
private final ChunkContext chunkContext; private final ChunkContext chunkContext;
private final long generationSessionId; private final long generationSessionId;
@@ -42,7 +47,8 @@ public final class IrisContext {
} }
public static IrisContext get() { public static IrisContext get() {
return CONTEXT.get(); Binding binding = binding(Thread.currentThread());
return binding == null ? null : binding.context;
} }
public static IrisContext require() { public static IrisContext require() {
@@ -55,9 +61,10 @@ public final class IrisContext {
public static Scope open(Engine engine, long generationSessionId, ChunkContext chunkContext) { public static Scope open(Engine engine, long generationSessionId, ChunkContext chunkContext) {
Thread thread = Thread.currentThread(); Thread thread = Thread.currentThread();
IrisContext previous = CONTEXT.get(); Binding binding = bind(thread);
IrisContext previous = binding.context;
IrisContext installed = new IrisContext(engine, generationSessionId, chunkContext); IrisContext installed = new IrisContext(engine, generationSessionId, chunkContext);
CONTEXT.set(installed); binding.context = installed;
return new Scope(thread, previous, installed); return new Scope(thread, previous, installed);
} }
@@ -99,6 +106,42 @@ public final class IrisContext {
.qput("queued", mantle.getUnloadRegionCount())); .qput("queued", mantle.getUnloadRegionCount()));
} }
private static Binding binding(Thread thread) {
Binding binding = BINDINGS.getPlain(slot(thread));
if (binding != null && binding.owner == thread) {
return binding;
}
return OVERFLOW_BINDINGS.get() == 0 ? null : OVERFLOW.get();
}
private static Binding bind(Thread thread) {
Binding binding = binding(thread);
if (binding != null) {
return binding;
}
binding = new Binding(thread);
if (!BINDINGS.compareAndSet(slot(thread), null, binding)) {
binding.overflow = true;
OVERFLOW.set(binding);
OVERFLOW_BINDINGS.incrementAndGet();
}
return binding;
}
private static void release(Binding binding) {
binding.context = null;
if (binding.overflow) {
OVERFLOW.remove();
OVERFLOW_BINDINGS.decrementAndGet();
return;
}
BINDINGS.compareAndSet(slot(binding.owner), binding, null);
}
private static int slot(Thread thread) {
return (int) thread.threadId() & (SLOTS - 1);
}
public static final class Scope implements NativeGenerationScope { public static final class Scope implements NativeGenerationScope {
private final Thread owner; private final Thread owner;
private final IrisContext previous; private final IrisContext previous;
@@ -119,16 +162,26 @@ public final class IrisContext {
if (Thread.currentThread() != owner) { if (Thread.currentThread() != owner) {
throw new IllegalStateException("Iris execution context scope closed from a different thread."); throw new IllegalStateException("Iris execution context scope closed from a different thread.");
} }
IrisContext current = CONTEXT.get(); Binding binding = binding(owner);
if (current != installed) { if (binding == null || binding.context != installed) {
throw new IllegalStateException("Iris execution context scopes must close in LIFO order."); throw new IllegalStateException("Iris execution context scopes must close in LIFO order.");
} }
if (previous == null) { if (previous == null) {
CONTEXT.remove(); release(binding);
} else { } else {
CONTEXT.set(previous); binding.context = previous;
} }
closed = true; closed = true;
} }
} }
private static final class Binding {
private final Thread owner;
private boolean overflow;
private IrisContext context;
private Binding(Thread owner) {
this.owner = owner;
}
}
} }
@@ -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; import java.util.Objects;
public record HydrologyPlanners(RegionPlanner regions, WindowPlanner windows, HeightLattice windowLattice) { public record HydrologyPlanners(RegionPlanner regions, WindowPlanner windows, HeightLattice fineLattice) {
public HydrologyPlanners { public HydrologyPlanners {
Objects.requireNonNull(regions, "regions"); Objects.requireNonNull(regions, "regions");
Objects.requireNonNull(windows, "windows"); Objects.requireNonNull(windows, "windows");
Objects.requireNonNull(windowLattice, "windowLattice"); Objects.requireNonNull(fineLattice, "fineLattice");
} }
public static HydrologyPlanners create(HydrologySettings settings, HydrologyTerrainSource source) { public static HydrologyPlanners create(HydrologySettings settings, HydrologyTerrainSource source) {
Objects.requireNonNull(settings, "settings"); Objects.requireNonNull(settings, "settings");
Objects.requireNonNull(source, "source"); Objects.requireNonNull(source, "source");
HeightLattice fine = new HeightLattice(source, WindowPlanner.FINE_CELL, false, HydrologyPlanCache.FINE_LATTICE_CELLS); 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); PondPlanner ponds = new PondPlanner(settings);
ReachShaper shaper = new ReachShaper(settings); ReachShaper shaper = new ReachShaper(settings);
RegionPlanner regions = new RegionPlanner(settings, RegionPlanner regions = new RegionPlanner(settings,
new HeightLattice(source, RegionPlanner.CELL, true, HydrologyPlanCache.REGION_LATTICE_CELLS), fine, ponds, shaper); 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 @Override
public synchronized void close() { public synchronized void close() {
if (!closed) { if (!closed) {
@@ -36,6 +36,7 @@ import java.io.UncheckedIOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@@ -56,7 +57,7 @@ import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
public final class HydrologyRuntime { 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 KEEP_SCOPES = 2;
private static final int CHUNKS_PER_WINDOW = WindowKey.SIZE >> 4; private static final int CHUNKS_PER_WINDOW = WindowKey.SIZE >> 4;
public static final int MAX_PREGENERATION_REGIONS = 4096; public static final int MAX_PREGENERATION_REGIONS = 4096;
@@ -72,11 +73,13 @@ public final class HydrologyRuntime {
private final RasterLru rasters; private final RasterLru rasters;
private final ThreadLocal<LastRaster> last; private final ThreadLocal<LastRaster> last;
private final Set<Long> queued; private final Set<Long> queued;
private final ConcurrentHashMap<Long, CompletableFuture<ChunkWaterRaster>> building;
private final RasterFailures failures; private final RasterFailures failures;
private final RasterFailures windowPreviewFailures; private final RasterFailures windowPreviewFailures;
private final RasterFailures regionPreviewFailures; private final RasterFailures regionPreviewFailures;
private final Set<Object> previews; private final Set<Object> previews;
private final LinkedHashMap<WindowKey, WindowPrefetch> prefetches; private final LinkedHashMap<WindowKey, WindowPrefetch> prefetches;
private final Map<Object, PregenArea> pregenAreas;
private final HydrologyBinding binding; private final HydrologyBinding binding;
private final Function<PreparedHydrologyCacheIdentity, HydrologyPlanStore> stores; private final Function<PreparedHydrologyCacheIdentity, HydrologyPlanStore> stores;
private final boolean caveFluidPolicies; private final boolean caveFluidPolicies;
@@ -94,11 +97,13 @@ public final class HydrologyRuntime {
this.rasters = null; this.rasters = null;
this.last = null; this.last = null;
this.queued = null; this.queued = null;
this.building = null;
this.failures = null; this.failures = null;
this.windowPreviewFailures = null; this.windowPreviewFailures = null;
this.regionPreviewFailures = null; this.regionPreviewFailures = null;
this.previews = null; this.previews = null;
this.prefetches = null; this.prefetches = null;
this.pregenAreas = null;
this.binding = HydrologyBinding.UNBOUND; this.binding = HydrologyBinding.UNBOUND;
this.stores = identity -> null; this.stores = identity -> null;
this.caveFluidPolicies = false; this.caveFluidPolicies = false;
@@ -120,11 +125,13 @@ public final class HydrologyRuntime {
this.rasters = new RasterLru(); this.rasters = new RasterLru();
this.last = ThreadLocal.withInitial(LastRaster::new); this.last = ThreadLocal.withInitial(LastRaster::new);
this.queued = ConcurrentHashMap.newKeySet(); this.queued = ConcurrentHashMap.newKeySet();
this.building = new ConcurrentHashMap<>();
this.failures = new RasterFailures(ticker); this.failures = new RasterFailures(ticker);
this.windowPreviewFailures = new RasterFailures(ticker); this.windowPreviewFailures = new RasterFailures(ticker);
this.regionPreviewFailures = new RasterFailures(ticker); this.regionPreviewFailures = new RasterFailures(ticker);
this.previews = new HashSet<>(); this.previews = new HashSet<>();
this.prefetches = new LinkedHashMap<>(PREFETCH_CAPACITY * 2, 0.75f, true); this.prefetches = new LinkedHashMap<>(PREFETCH_CAPACITY * 2, 0.75f, true);
this.pregenAreas = new HashMap<>();
this.binding = Objects.requireNonNull(binding, "binding"); this.binding = Objects.requireNonNull(binding, "binding");
this.stores = Objects.requireNonNull(stores, "stores"); this.stores = Objects.requireNonNull(stores, "stores");
this.caveFluidPolicies = caveFluidPolicies; this.caveFluidPolicies = caveFluidPolicies;
@@ -234,7 +241,7 @@ public final class HydrologyRuntime {
requireOpen(); requireOpen();
long key = key(chunkX, chunkZ); long key = key(chunkX, chunkZ);
ChunkWaterRaster known = remembered(key, slot); 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) { 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) { void cancelPregeneration(Object epoch) {
if (cache != null) { if (cache != null) {
synchronized (prefetches) {
if (pregenAreas.remove(epoch) != null) {
prefetches.clear();
}
}
cache.cancel(epoch); 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) { private ChunkWaterRaster planned(int chunkX, int chunkZ, long chunk) {
try { try {
List<WindowKey> windowKeys = neighbourhood(chunkX, chunkZ); List<WindowKey> windowKeys = neighbourhood(chunkX, chunkZ);
@@ -667,7 +729,7 @@ public final class HydrologyRuntime {
&& (!known.prepared().isDone() || known.evictions() == evictions)) { && (!known.prepared().isDone() || known.evictions() == evictions)) {
return; 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)); prefetches.put(owner, new WindowPrefetch(prepared, evictions));
if (prefetches.size() > PREFETCH_CAPACITY) { if (prefetches.size() > PREFETCH_CAPACITY) {
prefetches.pollFirstEntry(); 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) { private static List<WindowKey> neighbourhood(int chunkX, int chunkZ) {
WindowKey own = WindowKey.containing(chunkX << 4, chunkZ << 4); WindowKey own = WindowKey.containing(chunkX << 4, chunkZ << 4);
List<WindowKey> keys = new ArrayList<>(9); List<WindowKey> keys = new ArrayList<>(9);
@@ -754,6 +834,16 @@ public final class HydrologyRuntime {
private record WindowPrefetch(CompletableFuture<Void> prepared, long evictions) { 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 static final class LastRaster {
private long key; private long key;
private ChunkWaterRaster raster; private ChunkWaterRaster raster;
@@ -1,34 +1,59 @@
package art.arcane.iris.generation.hydrology.grid; package art.arcane.iris.generation.hydrology.grid;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.CancellationException; import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean; 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 { 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 HydrologyTerrainSource source;
private final int cell; private final int cell;
private final boolean baseOnly; private final boolean baseOnly;
private final int capacityCells; private final int tileShift;
private final ConcurrentHashMap<Long, Slot> cells; private final int tileMask;
private final ConcurrentLinkedQueue<Long> insertionOrder; private final int tileCells;
private final int capacityTiles;
private final ConcurrentHashMap<Long, Tile> tiles;
private final AtomicLong clock;
private final AtomicBoolean evicting; private final AtomicBoolean evicting;
private final HeightLattice columns;
public HeightLattice(HydrologyTerrainSource source, int cell, boolean baseOnly, int capacityCells) { 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) { if (cell <= 0 || capacityCells <= 0) {
throw new IllegalArgumentException("Invalid lattice cell=" + cell + " capacityCells=" + capacityCells); throw new IllegalArgumentException("Invalid lattice cell=" + cell + " capacityCells=" + capacityCells);
} }
this.source = Objects.requireNonNull(source, "source"); this.source = Objects.requireNonNull(source, "source");
this.cell = cell; this.cell = cell;
this.baseOnly = baseOnly; this.baseOnly = baseOnly;
this.capacityCells = capacityCells; this.tileShift = tileShift;
this.cells = new ConcurrentHashMap<>(); this.tileMask = (1 << tileShift) - 1;
this.insertionOrder = new ConcurrentLinkedQueue<>(); this.tileCells = 1 << (2 * tileShift);
this.capacityTiles = Math.ceilDiv(capacityCells, tileCells);
this.tiles = new ConcurrentHashMap<>();
this.clock = new AtomicLong();
this.evicting = new AtomicBoolean(); this.evicting = new AtomicBoolean();
this.columns = columns;
} }
public int cell() { 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"); throw new IllegalArgumentException("Column sample has " + xs.length + " x and " + zs.length + " z coordinates");
} }
requireCapacity(out, xs.length); requireCapacity(out, xs.length);
if (xs.length > 0) { if (xs.length == 0) {
source.sample(xs, zs, out, baseOnly); 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) { private void resolve(Nodes nodes, int count, HydrologySampleBuffer out) {
Slot[] slots = new Slot[count]; Pass pass = new Pass(nodes, out, clock.incrementAndGet());
int unresolved = count; int[] open = null;
while (unresolved > 0) { int remaining = count;
CompletableFuture<HydrologySampleBuffer> batch = claimAndSample(nodes, slots); while (remaining > 0) {
unresolved = awaitForeign(batch, slots); open = pass.run(open, remaining);
remaining = open == null ? 0 : open.length;
} }
copy(slots, out);
evictOverflow(); evictOverflow();
} }
@@ -94,130 +124,33 @@ public final class HeightLattice {
Math.multiplyExact(Math.addExact(cell0, Math.multiplyExact(extent - 1, stride)), cell); Math.multiplyExact(Math.addExact(cell0, Math.multiplyExact(extent - 1, stride)), cell);
} }
private CompletableFuture<HydrologySampleBuffer> claimAndSample(Nodes nodes, Slot[] slots) { private Tile tile(int tileX, int tileZ) {
CompletableFuture<HydrologySampleBuffer> batch = new CompletableFuture<>(); Long key = key(tileX, tileZ);
try { Tile known = tiles.get(key);
int[] xs = new int[slots.length]; if (known != null) {
int[] zs = new int[slots.length]; return known;
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];
} }
return tiles.computeIfAbsent(key, ignored -> new Tile(tileCells));
} }
private void evictOverflow() { private void evictOverflow() {
if (cells.size() <= capacityCells || !evicting.compareAndSet(false, true)) { if (tiles.size() <= capacityTiles || !evicting.compareAndSet(false, true)) {
return; return;
} }
try { try {
int excess = cells.size() - capacityCells; List<Map.Entry<Long, Tile>> idle = new ArrayList<>(tiles.size());
while (excess > 0) { for (Map.Entry<Long, Tile> entry : tiles.entrySet()) {
Long key = insertionOrder.poll(); if (entry.getValue().pending.get() == 0) {
if (key == null) { idle.add(entry);
return;
} }
Slot slot = cells.get(key); }
if (slot == null) { idle.sort(Comparator.comparingLong(entry -> entry.getValue().used));
continue; int target = capacityTiles - (capacityTiles >> 3);
} for (int index = 0; index < idle.size() && tiles.size() > target; index++) {
if (!slot.batch().isDone()) { Map.Entry<Long, Tile> entry = idle.get(index);
insertionOrder.offer(key); Tile tile = entry.getValue();
excess--; if (tile.pending.get() == 0) {
} else if (cells.remove(key, slot)) { tiles.remove(entry.getKey(), tile);
excess--;
} }
} }
} finally { } finally {
@@ -232,7 +165,219 @@ public final class HeightLattice {
return mixed ^ (mixed >>> 31); 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 { private interface Nodes {
@@ -32,10 +32,10 @@ import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
public final class HydrologyPlanCache { public final class HydrologyPlanCache {
private static final int FRONT = 3; private static final int FINE_FRONT = 9;
public static final int WINDOW_LATTICE_CELLS = frontCells(WindowKey.SIZE, WindowPlanner.CELL); private static final int REGION_FRONT = 5;
public static final int FINE_LATTICE_CELLS = frontCells(WindowKey.SIZE, WindowPlanner.FINE_CELL); public static final int FINE_LATTICE_CELLS = frontCells(FINE_FRONT, WindowKey.SIZE, WindowPlanner.FINE_CELL);
public static final int REGION_LATTICE_CELLS = frontCells(RegionKey.SIZE, RegionPlanner.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 WINDOW_CAPACITY = 128;
private static final int REGION_CAPACITY = 64; private static final int REGION_CAPACITY = 64;
private static final long DEMAND = Long.MIN_VALUE; private static final long DEMAND = Long.MIN_VALUE;
@@ -312,8 +312,8 @@ public final class HydrologyPlanCache {
return Math.max(1, parallelism / 2); return Math.max(1, parallelism / 2);
} }
private static int frontCells(int unit, int cell) { private static int frontCells(int front, int unit, int cell) {
int cells = (FRONT + 2) * unit / cell; int cells = (front + 2) * unit / cell;
return cells * cells; return cells * cells;
} }
@@ -36,6 +36,7 @@ public final class WindowPlanner {
public static final int CELL = 8; public static final int CELL = 8;
public static final int FINE_CELL = 4; public static final int FINE_CELL = 4;
private static final int WINDOW_CELLS = WindowKey.SIZE / CELL; 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 SPAN = 3 * WINDOW_CELLS;
private static final int REACH_OVERHANG = 256; private static final int REACH_OVERHANG = 256;
private static final float BASIN_DEPTH = 0.5f; private static final float BASIN_DEPTH = 0.5f;
@@ -43,22 +44,17 @@ public final class WindowPlanner {
private static final long SALT_HEAD = 0x5354524541484dL; private static final long SALT_HEAD = 0x5354524541484dL;
private final HydrologySettings settings; private final HydrologySettings settings;
private final HeightLattice lattice;
private final HeightLattice fine; private final HeightLattice fine;
private final PondPlanner ponds; private final PondPlanner ponds;
private final ReachShaper shaper; private final ReachShaper shaper;
private final SeaInletPlanner inlets; 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.settings = Objects.requireNonNull(settings, "settings");
this.lattice = Objects.requireNonNull(lattice, "lattice");
this.fine = Objects.requireNonNull(fine, "fine"); this.fine = Objects.requireNonNull(fine, "fine");
this.ponds = Objects.requireNonNull(ponds, "ponds"); this.ponds = Objects.requireNonNull(ponds, "ponds");
this.shaper = Objects.requireNonNull(shaper, "shaper"); this.shaper = Objects.requireNonNull(shaper, "shaper");
this.inlets = new SeaInletPlanner(settings); 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) { if (fine.cell() != FINE_CELL) {
throw new IllegalArgumentException("Window fine lattice cell " + fine.cell() + " must be " + 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 cellX = Math.multiplyExact(key.wx() - 1, WINDOW_CELLS);
int cellZ = Math.multiplyExact(key.wz() - 1, WINDOW_CELLS); int cellZ = Math.multiplyExact(key.wz() - 1, WINDOW_CELLS);
HydrologySampleBuffer samples = new HydrologySampleBuffer(SPAN * SPAN); 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); DemGrid raw = new DemGrid(cellX * CELL, cellZ * CELL, CELL, SPAN, SPAN);
System.arraycopy(samples.height(), 0, raw.values(), 0, SPAN * SPAN); System.arraycopy(samples.height(), 0, raw.values(), 0, SPAN * SPAN);
List<Pond> lakes = regionLakes(plans.values()); 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 art.arcane.iris.generation.hydrology.water.Reach;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@@ -28,17 +29,20 @@ final class ChunkFeatures {
private final List<Pond> ponds; private final List<Pond> ponds;
private final List<Pond> lakes; private final List<Pond> lakes;
private final List<SeaInlet> inlets; private final List<SeaInlet> inlets;
private final int[] inletBoxes;
private final int[] lakeBoxes; private final int[] lakeBoxes;
private final ReachSegments segments = new ReachSegments(SEGMENT_CAPACITY); private final ReachSegments segments = new ReachSegments(SEGMENT_CAPACITY);
private final MaskEdges pondShores = new MaskEdges(MaskEdges.Kind.WATER); private final MaskEdges pondShores = new MaskEdges(MaskEdges.Kind.WATER);
private final MaskEdges pondBanks = new MaskEdges(MaskEdges.Kind.BANK); private final MaskEdges pondBanks = new MaskEdges(MaskEdges.Kind.BANK);
private final MaskEdges lakeReach = new MaskEdges(MaskEdges.Kind.GROWN_WATER); 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.reaches = reaches;
this.ponds = ponds; this.ponds = ponds;
this.lakes = lakes; this.lakes = lakes;
this.inlets = inlets; this.inlets = inlets;
this.inletBoxes = inletBoxes;
this.lakeBoxes = new int[4 * lakes.size()]; this.lakeBoxes = new int[4 * lakes.size()];
} }
@@ -48,12 +52,15 @@ final class ChunkFeatures {
List<Reach> reaches = new ArrayList<>(); List<Reach> reaches = new ArrayList<>();
List<Pond> ponds = new ArrayList<>(); List<Pond> ponds = new ArrayList<>();
List<SeaInlet> inlets = new ArrayList<>(); List<SeaInlet> inlets = new ArrayList<>();
int[] inletBoxes = new int[0];
for (IndexedWindow window : windows) { for (IndexedWindow window : windows) {
for (SeaInlet inlet : window.plan().inlets()) { for (SeaInlet inlet : window.plan().inlets()) {
int[] bounds = SeaInletRasterizer.bounds(inlet); int[] bounds = SeaInletRasterizer.bounds(inlet);
int padding = RiffleProfile.RADIUS + 1; int padding = RiffleProfile.RADIUS + 1;
if (bounds[0] <= chunkX * CHUNK + CHUNK - 1 + padding && bounds[2] >= chunkX * CHUNK - padding 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) { && 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); inlets.add(inlet);
} }
} }
@@ -64,17 +71,22 @@ final class ChunkFeatures {
} }
} }
List<Pond> lakes = lakes(chunkX, chunkZ, halo, lakeRegionPlans, lakeRegions); 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); features.index(settings, chunkX, chunkZ);
return features; return features;
} }
boolean empty() { boolean inert() {
return reaches.isEmpty() && ponds.isEmpty() && lakes.isEmpty() && inlets.isEmpty(); return inert;
} }
SeaInlet openInletAt(int x, int z, int[] column) { 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) { if (SeaInletRasterizer.column(inlet, x, z, column) && column[1] == SeaInlet.OPEN) {
return inlet; return inlet;
} }
@@ -179,6 +191,8 @@ final class ChunkFeatures {
for (Reach reach : reaches) { for (Reach reach : reaches) {
segments.addNear(reach, minX - 1f, minZ - 1f, maxX + 1f, maxZ + 1f, outward); 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(); HydrologySettings.PondShore shore = settings.ponds().shore();
int reach = Math.max(shore.taper(), ChunkWaterRaster.MAX_SHORE_DISTANCE) + 1; int reach = Math.max(shore.taper(), ChunkWaterRaster.MAX_SHORE_DISTANCE) + 1;
int shelf = shore.shelf() + 1; int shelf = shore.shelf() + 1;
@@ -192,6 +206,39 @@ final class ChunkFeatures {
lakeReach.collect(mask, minX - shoreReach, minZ - shoreReach, maxX + shoreReach, maxZ + shoreReach); lakeReach.collect(mask, minX - shoreReach, minZ - shoreReach, maxX + shoreReach, maxZ + shoreReach);
System.arraycopy(grownBox(mask), 0, lakeBoxes, 4 * index, 4); 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) { private static Map<WindowKey, IndexedWindow> windowsByKey(int chunkX, int chunkZ, List<IndexedWindow> windows) {
@@ -37,7 +37,7 @@ public final class ChunkRasterizer {
List<RegionPlan> lakeRegionPlans) { List<RegionPlan> lakeRegionPlans) {
Objects.requireNonNull(naturalHeight, "naturalHeight"); Objects.requireNonNull(naturalHeight, "naturalHeight");
ChunkFeatures features = ChunkFeatures.gather(settings, chunkX, chunkZ, halo, windows, lakeRegionPlans, lakeRegions(chunkX, chunkZ)); ChunkFeatures features = ChunkFeatures.gather(settings, chunkX, chunkZ, halo, windows, lakeRegionPlans, lakeRegions(chunkX, chunkZ));
if (features.empty()) { if (features.inert()) {
return ChunkWaterRaster.EMPTY; return ChunkWaterRaster.EMPTY;
} }
return new RasterComposer(settings, features, chunkX, chunkZ).compose(naturalHeight); 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 art.arcane.iris.generation.hydrology.water.HydrologyTier;
import java.util.Arrays;
public final class ChunkWaterRaster { public final class ChunkWaterRaster {
public static final int MAX_SHORE_DISTANCE = 32; public static final int MAX_SHORE_DISTANCE = 32;
public static final ChunkWaterRaster PROVISIONAL = new ChunkWaterRaster(true); public static final ChunkWaterRaster PROVISIONAL = new ChunkWaterRaster(true);
@@ -13,10 +15,13 @@ public final class ChunkWaterRaster {
private final int[] terrain; private final int[] terrain;
private final int[] head; private final int[] head;
private final byte[] role; private final byte[] role;
private final HydrologyTier[] tier; private final byte[] tier;
private final long[] featureId; private final HydrologyTier[] tiers;
private final String[] fluid; private final byte[] feature;
private final int[] caveCeiling; private final long[] features;
private final byte[] fluid;
private final String[] fluids;
private final boolean caveMask;
private final byte[] shoreDistance; private final byte[] shoreDistance;
private ChunkWaterRaster(boolean provisional) { private ChunkWaterRaster(boolean provisional) {
@@ -25,26 +30,32 @@ public final class ChunkWaterRaster {
this.head = null; this.head = null;
this.role = null; this.role = null;
this.tier = null; this.tier = null;
this.featureId = null; this.tiers = null;
this.feature = null;
this.features = null;
this.fluid = null; this.fluid = null;
this.caveCeiling = null; this.fluids = null;
this.caveMask = false;
this.shoreDistance = null; this.shoreDistance = null;
} }
ChunkWaterRaster(int[] terrain, int[] head, byte[] role, HydrologyTier[] tier, long[] featureId, String[] fluid, int[] caveCeiling, ChunkWaterRaster(int[] terrain, int[] head, byte[] role, Palette<HydrologyTier> tiers, LongPalette features, Palette<String> fluids,
byte[] shoreDistance) { boolean caveMask, byte[] shoreDistance) {
if (terrain.length != COLUMNS || head.length != COLUMNS || role.length != COLUMNS || tier.length != COLUMNS if (terrain.length != COLUMNS || head.length != COLUMNS || role.length != COLUMNS || tiers.indices.length != COLUMNS
|| featureId.length != COLUMNS || fluid.length != COLUMNS || caveCeiling.length != COLUMNS || shoreDistance.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"); throw new IllegalArgumentException("A chunk water raster needs " + COLUMNS + " entries per array");
} }
this.provisional = false; this.provisional = false;
this.terrain = terrain; this.terrain = terrain;
this.head = head; this.head = head;
this.role = role; this.role = role;
this.tier = tier; this.tier = tiers.indices;
this.featureId = featureId; this.tiers = tiers.values();
this.fluid = fluid; this.feature = features.indices;
this.caveCeiling = caveCeiling; this.features = features.values();
this.fluid = fluids.indices;
this.fluids = fluids.values();
this.caveMask = caveMask;
this.shoreDistance = shoreDistance; this.shoreDistance = shoreDistance;
} }
@@ -68,22 +79,22 @@ public final class ChunkWaterRaster {
public HydrologyTier tier(int lx, int lz) { public HydrologyTier tier(int lx, int lz) {
int index = index(lx, 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) { public long featureId(int lx, int lz) {
int index = index(lx, 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) { public String fluid(int lx, int lz) {
int index = index(lx, 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) { public int caveCeiling(int lx, int lz) {
int index = index(lx, 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) { public int shoreDistance(int lx, int lz) {
@@ -105,4 +116,53 @@ public final class ChunkWaterRaster {
} }
return lz << 4 | lx; 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() { private ChunkWaterRaster pack() {
int columns = ChunkWaterRaster.COLUMNS; 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[] terrainOut = new int[columns];
int[] headOut = new int[columns]; int[] headOut = new int[columns];
byte[] roleOut = new byte[columns]; byte[] roleOut = new byte[columns];
HydrologyTier[] tierOut = new HydrologyTier[columns]; ChunkWaterRaster.Palette<HydrologyTier> tierOut = new ChunkWaterRaster.Palette<>(new HydrologyTier[columns]);
long[] featureOut = new long[columns]; ChunkWaterRaster.LongPalette featureOut = new ChunkWaterRaster.LongPalette();
String[] fluidOut = new String[columns]; ChunkWaterRaster.Palette<String> fluidOut = new ChunkWaterRaster.Palette<>(new String[columns]);
int[] caveOut = new int[columns];
byte[] shoreOut = new byte[columns]; byte[] shoreOut = new byte[columns];
boolean caveMask = settings.deformation().caveMask();
boolean affected = false;
for (int lz = 0; lz < CHUNK; lz++) { for (int lz = 0; lz < CHUNK; lz++) {
for (int lx = 0; lx < CHUNK; lx++) { for (int lx = 0; lx < CHUNK; lx++) {
int cell = (lz + 1) * GRID + lx + 1; int cell = (lz + 1) * GRID + lx + 1;
int index = lz << 4 | lx; int index = lz << 4 | lx;
int shore = shore(cell);
terrainOut[index] = terrain[cell]; terrainOut[index] = terrain[cell];
headOut[index] = head[cell]; headOut[index] = head[cell];
roleOut[index] = (byte) role[cell].ordinal(); roleOut[index] = (byte) role[cell].ordinal();
tierOut[index] = tier[cell]; tierOut.set(index, tier[cell]);
featureOut[index] = feature[cell]; featureOut.set(index, feature[cell]);
fluidOut[index] = fluid[cell]; fluidOut.set(index, fluid[cell]);
caveOut[index] = caveMask && head[cell] != Integer.MIN_VALUE ? terrain[cell] - 1 : Integer.MAX_VALUE; shoreOut[index] = (byte) shore(cell);
shoreOut[index] = (byte) shore;
affected |= role[cell] != WaterRole.NONE || terrain[cell] != natural[cell] || shore < ChunkWaterRaster.MAX_SHORE_DISTANCE;
} }
} }
return affected ? new ChunkWaterRaster(terrainOut, headOut, roleOut, tierOut, featureOut, fluidOut, caveOut, shoreOut) return new ChunkWaterRaster(terrainOut, headOut, roleOut, tierOut, featureOut, fluidOut, settings.deformation().caveMask(), shoreOut);
: ChunkWaterRaster.EMPTY;
} }
private int shore(int cell) { private int shore(int cell) {
@@ -6,6 +6,8 @@ import java.util.Arrays;
final class ReachSegments { final class ReachSegments {
private static final int RUN = 16; private static final int RUN = 16;
private static final int COVER_SHIFT = 2;
private static final float COVER_MARGIN = 1f;
private Reach[] reaches; private Reach[] reaches;
private int[] segments; private int[] segments;
@@ -24,6 +26,13 @@ final class ReachSegments {
private int[] runBands = new int[8]; private int[] runBands = new int[8];
private int runs; private int runs;
private boolean sealed; 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) { ReachSegments(int capacity) {
int initial = Math.max(1, 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) { void nearest(double x, double z, Scope scope, ReachCursor cursor) {
if (!sealed) { if (!sealed) {
seal(); seal();
@@ -89,6 +111,16 @@ final class ReachSegments {
hint(channelHint, x, z, unbounded, cursor); hint(channelHint, x, z, unbounded, cursor);
hint(bankHint, x, z, unbounded, cursor); hint(bankHint, x, z, unbounded, cursor);
hint(edgeHint, 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; int first = 0;
for (int run = 0; run < runs; run++) { for (int run = 0; run < runs; run++) {
int end = runEnds[run]; int end = runEnds[run];
@@ -99,9 +131,7 @@ final class ReachSegments {
continue; continue;
} }
for (int index = first; index < end; index++) { 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]), if (!beyondSegment(index, x, z, scope, cursor) && index != channelHint && index != bankHint && index != edgeHint) {
Math.max(startZ[index], endZ[index]), limit(radius[index], bands[index], scope, cursor))
&& index != channelHint && index != bankHint && index != edgeHint) {
evaluate(index, x, z, unbounded, cursor); 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) { private void hint(int index, double x, double z, boolean unbounded, ReachCursor cursor) {
if (index >= 0 && index < size) { if (index >= 0 && index < size) {
evaluate(index, x, z, unbounded, cursor); evaluate(index, x, z, unbounded, cursor);
@@ -171,9 +218,56 @@ final class ReachSegments {
addRun(start, end); addRun(start, end);
start = end; start = end;
} }
if (covered) {
cover();
}
sealed = true; 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) { private void addRun(int start, int end) {
if (runs == runEnds.length) { if (runs == runEnds.length) {
runEnds = Arrays.copyOf(runEnds, 2 * runs); runEnds = Arrays.copyOf(runEnds, 2 * runs);
@@ -15,6 +15,7 @@ final class RiffleProfile {
private static final int LIMIT = 10 * RADIUS; private static final int LIMIT = 10 * RADIUS;
private static final int DRY = Integer.MIN_VALUE; private static final int DRY = Integer.MIN_VALUE;
private static final int[] SIDES = {1, -1, GRID, -GRID}; private static final int[] SIDES = {1, -1, GRID, -GRID};
private static final int[] REMAINING = remaining();
private final List<Field> fields = new ArrayList<>(); 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))); 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) { private record Field(int crest, int[] distances) {
} }
@@ -107,6 +122,9 @@ final class RiffleProfile {
} }
private void offer(int cell, int distance) { private void offer(int cell, int distance) {
if (distance + REMAINING[cell] >= LIMIT) {
return;
}
if (distances[cell] < LIMIT) { if (distances[cell] < LIMIT) {
remove(cell); remove(cell);
} }
@@ -10,6 +10,7 @@ import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import java.util.Arrays; import java.util.Arrays;
import java.util.Set; import java.util.Set;
import java.util.function.Supplier;
public final class CavePoolFiller { public final class CavePoolFiller {
public static final int REACH = HydrologySettings.MAX_CAVE_POOL_RADIUS + 1; public static final int REACH = HydrologySettings.MAX_CAVE_POOL_RADIUS + 1;
@@ -40,15 +41,19 @@ public final class CavePoolFiller {
this.density = settings.density(); 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) { if (!pools.enabled() || density == 0f) {
return; return;
} }
int attempts = attempts(seed, chunkX, chunkZ); int attempts = attempts(seed, chunkX, chunkZ);
CarvedTerrain carved = null;
for (int attempt = 0; attempt < attempts; attempt++) { for (int attempt = 0; attempt < attempts; attempt++) {
if (density < 1f && CellHash.unit(CellHash.mix(seed, SALT_DENSITY + attempt, chunkX, chunkZ)) >= density) { if (density < 1f && CellHash.unit(CellHash.mix(seed, SALT_DENSITY + attempt, chunkX, chunkZ)) >= density) {
continue; continue;
} }
if (carved == null) {
carved = terrain.get();
}
long column = CellHash.mix(seed, SALT_COLUMN + attempt, chunkX, chunkZ); long column = CellHash.mix(seed, SALT_COLUMN + attempt, chunkX, chunkZ);
int x = (chunkX << 4) + (int) (column & 15); int x = (chunkX << 4) + (int) (column & 15);
int z = (chunkZ << 4) + (int) ((column >>> 4) & 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_HEADROOM = 0x53454148454144L;
private static final long SALT_DEPTH = 0x53454144455054L; private static final long SALT_DEPTH = 0x53454144455054L;
private static final long SALT_DENSITY = 0x53454144454e53L; private static final long SALT_DENSITY = 0x53454144454e53L;
private static final double SQUARED_MARGIN = 1e-9D;
private final HydrologySettings settings; private final HydrologySettings settings;
@@ -403,22 +404,36 @@ public final class SeaInletPlanner {
double dx = bx - ax; double dx = bx - ax;
double dz = bz - az; double dz = bz - az;
double span = dx * dx + dz * dz; 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++) { for (int j = minJ; j <= maxJ; j++) {
double pz = grid.originZ() + (double) j * cell; double pz = grid.originZ() + (double) j * cell;
for (int i = minI; i <= maxI; i++) { for (int i = minI; i <= maxI; i++) {
double px = grid.originX() + (double) i * cell; double px = grid.originX() + (double) i * cell;
double t = span <= 0D ? 0D : Math.clamp(((px - ax) * dx + (pz - az) * dz) / span, 0D, 1D); 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)); double ex = px - (ax + t * dx);
if (distance > radius) { double ez = pz - (az + t * dz);
double squared = ex * ex + ez * ez;
if (squared > outside || !(squared < inside) && Math.hypot(ex, ez) > radius) {
continue; continue;
} }
int index = j * grid.width() + i; int index = j * grid.width() + i;
cover[index] = Math.min(cover[index], bed); 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) { private static float pathLength(DemGrid grid, List<Integer> path) {
float length = 0f; float length = 0f;
for (int station = 1; station < path.size(); station++) { 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); generateFalls(writer, complex, runtime, own, chunkX, chunkZ);
if (poolsActive()) { if (poolsActive()) {
HydrologySettings settings = runtime.settings(); HydrologySettings settings = runtime.settings();
new CavePoolFiller(settings).fill(chunkX, chunkZ, Engine engine = getEngineMantle().getEngine();
new MantleCarvedTerrain(writer, complex, getEngineMantle().getEngine(), chunkX, chunkZ), writer, settings.seed()); 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.noise.CNG;
import art.arcane.volmlib.util.stream.ProceduralStream; import art.arcane.volmlib.util.stream.ProceduralStream;
import art.arcane.volmlib.util.stream.interpolation.Interpolated; import art.arcane.volmlib.util.stream.interpolation.Interpolated;
import it.unimi.dsi.fastutil.longs.Long2DoubleOpenHashMap;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@@ -120,10 +119,11 @@ import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiFunction; import java.util.function.BiFunction;
@Data @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"}) @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", "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", "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 { public class IrisComplex implements DataProvider {
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D); 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 AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
private static final int GRID_BOUNDS_CACHE_SIZE = 8192; private static final int GRID_BOUNDS_CACHE_SIZE = 8192;
private static final int STUDIO_NOISE_CACHE_SIZE = 32_768; private static final int STUDIO_NOISE_CACHE_SIZE = 32_768;
@@ -146,7 +146,6 @@ public class IrisComplex implements DataProvider {
Comparator.nullsFirst(Comparator.naturalOrder()) Comparator.nullsFirst(Comparator.naturalOrder())
); );
private static final ThreadLocal<TerrainBatchScratch> BATCH_SCRATCH = ThreadLocal.withInitial(TerrainBatchScratch::new); 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 = { private static final InferredType[] INFERRED_BIOME_PREPARATION_ORDER = {
InferredType.LAND, InferredType.LAND,
InferredType.CAVE, InferredType.CAVE,
@@ -160,6 +159,7 @@ public class IrisComplex implements DataProvider {
@Getter(AccessLevel.NONE) @Getter(AccessLevel.NONE)
private final int biomeBoundsSamplingStep; private final int biomeBoundsSamplingStep;
private transient volatile SharedCornerBounds sharedCornerBounds; private transient volatile SharedCornerBounds sharedCornerBounds;
private transient volatile SharedBaseHeights sharedBaseHeights;
private transient volatile SharedBoundsBiomes sharedBoundsBiomes; private transient volatile SharedBoundsBiomes sharedBoundsBiomes;
@Getter(AccessLevel.NONE) @Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
@@ -259,6 +259,10 @@ public class IrisComplex implements DataProvider {
private IrisBiome focusBiome; private IrisBiome focusBiome;
private IrisRegion focusRegion; private IrisRegion focusRegion;
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds; 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; private Set<IrisBiome> generatorBiomes;
@Setter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
private final transient HydrologyRuntime hydrologyRuntime; private final transient HydrologyRuntime hydrologyRuntime;
@@ -386,6 +390,9 @@ public class IrisComplex implements DataProvider {
frozenGenerators[frozenIndex] = generatorGroups[frozenIndex].generators(); frozenGenerators[frozenIndex] = generatorGroups[frozenIndex].generators();
} }
generatorBounds = buildGeneratorBounds(engine); generatorBounds = buildGeneratorBounds(engine);
regionBounds = focusedBiomes == null && !imageMapRuntime.has(IrisImageMapApplication.BIOME)
? RegionBounds.compile(preparedRegions, frozenInterpolators.length, new BoundsReach(), this::groupBounds)
: null;
biomeBuffetGenerators = compileBuffetGeneratorGroups(); biomeBuffetGenerators = compileBuffetGeneratorGroups();
chunkHeightBatch = engine.getDimension().getTerrainSamplingStep() == 1 && biomeBuffetGenerators.isEmpty() chunkHeightBatch = engine.getDimension().getTerrainSamplingStep() == 1 && biomeBuffetGenerators.isEmpty()
&& (biomeBuffet == null || biomeBuffet.cells().isEmpty()); && (biomeBuffet == null || biomeBuffet.cells().isEmpty());
@@ -417,20 +424,21 @@ public class IrisComplex implements DataProvider {
: RegionSelection.select(new RegionSelection.Options( : RegionSelection.select(new RegionSelection.Options(
engine.getDimension(), data, regionStyleStream, imageMapRuntime, null)); engine.getDimension(), data, regionStyleStream, imageMapRuntime, null));
regionStream = focusedRegions != null ? focusedRegions : new BakedStream<>(bakeCache, BakePlane.REGION, regionSource); 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)) .convert((r) -> createInferredBiomeStream(r, InferredType.CAVE))
.convertAware2D(ProceduralStream::get); .convertAware2D(ProceduralStream::get);
caveBiomeStream = new BakedStream<>(bakeCache, BakePlane.CAVE_BIOME, caveSource); caveBiomeStream = new BakedStream<>(bakeCache, BakePlane.CAVE_BIOME, caveSource);
inferredStreams.put(InferredType.CAVE, caveBiomeStream); 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)) .convert((r) -> createInferredBiomeStream(r, InferredType.LAND))
.convertAware2D(ProceduralStream::get); .convertAware2D(ProceduralStream::get);
inferredStreams.put(InferredType.LAND, landBiomeStream); 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)) .convert((r) -> createInferredBiomeStream(r, InferredType.SEA))
.convertAware2D(ProceduralStream::get); .convertAware2D(ProceduralStream::get);
inferredStreams.put(InferredType.SEA, seaBiomeStream); 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)) .convert((r) -> createInferredBiomeStream(r, InferredType.SHORE))
.convertAware2D(ProceduralStream::get); .convertAware2D(ProceduralStream::get);
inferredStreams.put(InferredType.SHORE, shoreBiomeStream); inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
@@ -812,17 +820,35 @@ public class IrisComplex implements DataProvider {
} }
return; return;
} }
SharedBaseHeights memo = sharedBaseHeights();
TerrainBatchScratch scratch = BATCH_SCRATCH.get(); TerrainBatchScratch scratch = BATCH_SCRATCH.get();
int mark = scratch.mark(); int mark = scratch.mark();
int indexMark = scratch.indexMark(); int indexMark = scratch.indexMark();
try { try {
double[] batchXs = scratch.buffer(count); double[] batchXs = scratch.buffer(count);
double[] batchZs = 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++) { for (int index = 0; index < count; index++) {
batchXs[index] = xs[from + index]; int x = xs[from + index];
batchZs[index] = zs[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 { } finally {
scratch.reset(mark); scratch.reset(mark);
scratch.resetIndices(indexMark); scratch.resetIndices(indexMark);
@@ -830,23 +856,9 @@ public class IrisComplex implements DataProvider {
if (baseOnly || terrain3D == null && transitionDisplacement == null) { if (baseOnly || terrain3D == null && transitionDisplacement == null) {
return; return;
} }
Long2DoubleOpenHashMap known = new Long2DoubleOpenHashMap(count);
known.defaultReturnValue(Double.NaN);
for (int index = 0; index < count; index++) { 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) { 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) { private double sampleUnblendedNaturalTerrainHeight(double x, double z) {
HydrologyBaseHeights batched = HYDROLOGY_BASE_HEIGHTS.get(); if (!memoColumn(x) || !memoColumn(z)) {
if (batched != null && batched.owner() == this && (int) x == x && (int) z == z) { return imageMapRuntime.sampleTerrainHeight(x, z, proceduralTerrainHeight.sample(x, z));
double known = batched.heights().get(hydrologyColumnKey((int) x, (int) z));
if (!Double.isNaN(known)) {
return known;
}
} }
double proceduralHeight = proceduralTerrainHeight.sample(x, z); SharedBaseHeights memo = sharedBaseHeights();
return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight); 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) { static double calculateNaturalSlope(double naturalHeight, double easternHeight, double southernHeight) {
@@ -1885,7 +1903,7 @@ public class IrisComplex implements DataProvider {
int grid = biomeBoundsSamplingStep; int grid = biomeBoundsSamplingStep;
GridBoundsCache cache = gridBoundsCache.get(); GridBoundsCache cache = gridBoundsCache.get();
if (grid <= 1) { 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); int xi = (int) Math.floor(x);
@@ -1941,7 +1959,7 @@ public class IrisComplex implements DataProvider {
if (GenerationCounter.ENABLED) { if (GenerationCounter.ENABLED) {
GenerationCounter.CORNER_STARCASTS.increment(); 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))) { if (!Double.isFinite(boundsLow(packed)) || !Double.isFinite(boundsHigh(packed))) {
// A non-finite corner is a transient sampling fault, not a fact about the terrain: // 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. // 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; 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) { private SharedBoundsBiomes sharedBoundsBiomes(ProceduralStream<IrisBiome> source) {
SharedBoundsBiomes shared = sharedBoundsBiomes; SharedBoundsBiomes shared = sharedBoundsBiomes;
if (shared != null && shared.source() == source) { 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) { private long computePackedBounds(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, int interpolatorIndex,
NoiseBounds bounds = sampleBoundsRaw(cache, engine, interpolator, generators, gx, gz); 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); 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); 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); IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds = generatorBounds.get(interpolator);
BoundsSampler sampler = cache.sampler.isInUse() ? new BoundsSampler() : cache.sampler; BoundsSampler sampler = cache.sampler.isInUse() ? new BoundsSampler() : cache.sampler;
sampler.bind(this, engine, generators, cachedBounds); sampler.bind(this, engine, interpolatorIndex, generators, cachedBounds);
try { try {
return interpolator.interpolateBounds(x, z, sampler); return interpolator.interpolateBounds(x, z, sampler);
@@ -2297,6 +2331,11 @@ public class IrisComplex implements DataProvider {
return reached; 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( static double averageGeneratorHeights(
IrisGenerator[] generators, IrisGenerator[] generators,
double low, 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 * 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} * 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 final IdentityHashMap<IrisBiome, GeneratorBounds> localBounds = new IdentityHashMap<>(8);
private IrisComplex complex; private IrisComplex complex;
private Engine engine; private Engine engine;
private int group;
private IrisGenerator[] generators; private IrisGenerator[] generators;
private IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds; private IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds;
private SharedBoundsBiomes sharedBiomes; private SharedBoundsBiomes sharedBiomes;
private BakedStream<IrisBiome> bakedBiomes;
private RegionBounds regional;
private boolean integerCoordinates; private boolean integerCoordinates;
private boolean inUse; private boolean inUse;
@@ -2629,14 +2697,19 @@ public class IrisComplex implements DataProvider {
return inUse; 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.complex = complex;
this.engine = engine; this.engine = engine;
this.group = group;
this.generators = generators; this.generators = generators;
this.cachedBounds = cachedBounds; this.cachedBounds = cachedBounds;
this.integerCoordinates = complex.baseBiomeStream instanceof BakedStream<?> this.integerCoordinates = complex.baseBiomeStream instanceof BakedStream<?>
|| complex.baseBiomeStream instanceof CachedStream2D<?>; || complex.baseBiomeStream instanceof CachedStream2D<?>;
this.sharedBiomes = integerCoordinates ? complex.sharedBoundsBiomes(complex.baseBiomeStream) : null; 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; this.inUse = true;
if (sampleSource.get() != complex.baseBiomeStream) { if (sampleSource.get() != complex.baseBiomeStream) {
sampleCache.clear(); sampleCache.clear();
@@ -2654,6 +2727,8 @@ public class IrisComplex implements DataProvider {
generators = null; generators = null;
cachedBounds = null; cachedBounds = null;
sharedBiomes = null; sharedBiomes = null;
bakedBiomes = null;
regional = null;
inUse = false; inUse = false;
} }
@@ -2662,25 +2737,23 @@ public class IrisComplex implements DataProvider {
try { try {
double sampleX = integerCoordinates ? (int) xx : xx; double sampleX = integerCoordinates ? (int) xx : xx;
double sampleZ = integerCoordinates ? (int) zz : zz; double sampleZ = integerCoordinates ? (int) zz : zz;
IrisBiome bx = sampleCache.get(sampleX, sampleZ); Object known = sampleCache.get(sampleX, sampleZ);
if (bx == null) { if (known == null && sharedBiomes != null) {
bx = sharedBiomes == null ? null : sharedBiomes.get((int) sampleX, (int) sampleZ); known = sharedBiomes.get((int) sampleX, (int) sampleZ);
if (bx == null) { if (known != null) {
ChunkBakeCache.enterSparse(); sampleCache.put(sampleX, sampleZ, known);
try {
bx = complex.baseBiomeStream.get(xx, zz);
} finally {
ChunkBakeCache.exitSparse();
}
if (sharedBiomes != null) {
sharedBiomes.put((int) sampleX, (int) sampleZ, bx);
}
} }
sampleCache.put(sampleX, sampleZ, bx);
} }
if (known instanceof IrisBiome biome) {
GeneratorBounds bounds = complex.resolveGeneratorBounds(engine, generators, bx, cachedBounds, localBounds); return bounds(biome);
return bounds.noiseBounds; }
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) { } catch (Throwable e) {
GenerationFailures.rethrowEngineFailure(e); GenerationFailures.rethrowEngineFailure(e);
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
@@ -2696,6 +2769,49 @@ public class IrisComplex implements DataProvider {
return ZERO_NOISE_BOUNDS; 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 static final class CoordinateBiomeCache {
private final long[] xBits; private final long[] xBits;
private final long[] zBits; private final long[] zBits;
private final IrisBiome[] values; private final Object[] values;
private final int mask; private final int mask;
private CoordinateBiomeCache(int capacity) { private CoordinateBiomeCache(int capacity) {
xBits = new long[capacity]; xBits = new long[capacity];
zBits = new long[capacity]; zBits = new long[capacity];
values = new IrisBiome[capacity]; values = new Object[capacity];
mask = capacity - 1; mask = capacity - 1;
} }
@@ -2719,20 +2835,20 @@ public class IrisComplex implements DataProvider {
Arrays.fill(values, null); Arrays.fill(values, null);
} }
private IrisBiome get(double x, double z) { private Object get(double x, double z) {
long xb = Double.doubleToLongBits(x); long xb = Double.doubleToLongBits(x);
long zb = Double.doubleToLongBits(z); long zb = Double.doubleToLongBits(z);
int slot = mix(xb, zb) & mask; int slot = mix(xb, zb) & mask;
return xBits[slot] == xb && zBits[slot] == zb ? values[slot] : null; 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 xb = Double.doubleToLongBits(x);
long zb = Double.doubleToLongBits(z); long zb = Double.doubleToLongBits(z);
int slot = mix(xb, zb) & mask; int slot = mix(xb, zb) & mask;
xBits[slot] = xb; xBits[slot] = xb;
zBits[slot] = zb; zBits[slot] = zb;
values[slot] = biome; values[slot] = sample;
} }
private int mix(long xb, long zb) { private int mix(long xb, long zb) {
@@ -2784,7 +2900,4 @@ public class IrisComplex implements DataProvider {
transitionDisplacement.clear(); 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; package art.arcane.iris.generation.runtime;
import art.arcane.iris.generation.biome.IrisBiome;
import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle; import java.lang.invoke.VarHandle;
import java.util.Objects; import java.util.Objects;
@@ -49,7 +47,7 @@ final class SharedBoundsBiomes {
return (int) ((mixed ^ (mixed >>> 31)) & mask); return (int) ((mixed ^ (mixed >>> 31)) & mask);
} }
IrisBiome get(int x, int z) { Object get(int x, int z) {
long key = key(x, z); long key = key(x, z);
int slot = slot(x, z); int slot = slot(x, z);
int before = (int) INTS.getAcquire(stamps, slot); int before = (int) INTS.getAcquire(stamps, slot);
@@ -63,11 +61,11 @@ final class SharedBoundsBiomes {
if (before != after || storedKey != key) { if (before != after || storedKey != key) {
return null; return null;
} }
return (IrisBiome) value; return value;
} }
void put(int x, int z, IrisBiome biome) { void put(int x, int z, Object sample) {
if (biome == null) { if (sample == null) {
return; return;
} }
int slot = slot(x, z); int slot = slot(x, z);
@@ -76,7 +74,7 @@ final class SharedBoundsBiomes {
return; return;
} }
LONGS.setRelease(keys, slot, key(x, z)); LONGS.setRelease(keys, slot, key(x, z));
OBJECTS.setRelease(values, slot, biome); OBJECTS.setRelease(values, slot, sample);
INTS.setRelease(stamps, slot, stamp + 2); 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.cave.HydrologyCaveStorage;
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster; import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
import art.arcane.iris.generation.chunk.ColumnExtent; import art.arcane.iris.generation.chunk.ColumnExtent;
import art.arcane.iris.generation.chunk.ColumnReadHunk;
import art.arcane.iris.generation.context.ChunkContext; import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.context.IrisContext; import art.arcane.iris.generation.context.IrisContext;
import art.arcane.iris.generation.block.B; import art.arcane.iris.generation.block.B;
@@ -1086,19 +1087,21 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
int height = output.getHeight(); int height = output.getHeight();
ColumnExtent extent = output instanceof ColumnExtent columnExtent ? columnExtent : null; ColumnExtent extent = output instanceof ColumnExtent columnExtent ? columnExtent : null;
SolidMemo solids = new SolidMemo(); SolidMemo solids = new SolidMemo();
ColumnWindow column = new ColumnWindow(output, height);
for (int localX = 0; localX < width; localX++) { for (int localX = 0; localX < width; localX++) {
for (int localZ = 0; localZ < depth; localZ++) { for (int localZ = 0; localZ < depth; localZ++) {
int worldX = blockX + localX; int worldX = blockX + localX;
int worldZ = blockZ + localZ; int worldZ = blockZ + localZ;
int floor = -1; int floor = -1;
int top = extent == null ? height - 1 : extent.highestStoredY(localX, localZ); int top = extent == null ? height - 1 : extent.highestStoredY(localX, localZ);
column.begin(localX, localZ, top);
boolean previousKnown = false; boolean previousKnown = false;
boolean previousSolid = false; boolean previousSolid = false;
for (int y = 1; y < height; y++) { for (int y = 1; y < height; y++) {
if (y > top) { if (y > top) {
break; break;
} }
boolean solid = solids.test(output.getRaw(localX, y, localZ)); boolean solid = solids.test(column.get(y));
boolean decorated = false; boolean decorated = false;
if (solid) { if (solid) {
if (floor >= 0) { if (floor >= 0) {
@@ -1122,11 +1125,12 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
if (extent != null) { if (extent != null) {
top = extent.highestStoredY(localX, localZ); top = extent.highestStoredY(localX, localZ);
} }
column.begin(localX, localZ, top);
} }
floor = -1; floor = -1;
} }
} else if (floor < 0 && (previousKnown ? previousSolid } else if (floor < 0 && (previousKnown ? previousSolid
: solids.test(output.getRaw(localX, y - 1, localZ)))) { : solids.test(column.get(y - 1)))) {
floor = y; floor = y;
} }
previousSolid = solid; previousSolid = solid;
@@ -1472,6 +1476,43 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
private record CaveColumn(long key, IrisBiome surface, IrisBiome cave, int surfaceY, int minimumDepth) { private record CaveColumn(long key, IrisBiome surface, IrisBiome cave, int surfaceY, int minimumDepth) {
} }
private static final class ColumnWindow {
private static final int ROWS = 16;
private final Hunk<NativeBlockState> output;
private final NativeBlockState[] states;
private int x;
private int z;
private int limit;
private int from;
private int to;
private ColumnWindow(Hunk<NativeBlockState> output, int height) {
this.output = output;
this.states = new NativeBlockState[height];
}
private void begin(int localX, int localZ, int top) {
x = localX;
z = localZ;
limit = Math.min(top, states.length - 1);
from = 0;
to = -1;
}
private NativeBlockState get(int y) {
if (y < from || y > to) {
if (y > limit) {
return output.getRaw(x, y, z);
}
from = y;
to = Math.min(limit, y + ROWS - 1);
ColumnReadHunk.readColumn(output, x, z, from, to, states, from);
}
return states[y];
}
}
private static final class SolidMemo { private static final class SolidMemo {
private NativeBlockState state; private NativeBlockState state;
private boolean solid; private boolean solid;
@@ -19,6 +19,7 @@
package art.arcane.iris.generation.stage; package art.arcane.iris.generation.stage;
import art.arcane.iris.generation.chunk.ColumnExtent; import art.arcane.iris.generation.chunk.ColumnExtent;
import art.arcane.iris.generation.chunk.ColumnReadHunk;
import art.arcane.iris.generation.decoration.IrisSpeleothems; import art.arcane.iris.generation.decoration.IrisSpeleothems;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EngineAssignedModifier; import art.arcane.iris.generation.runtime.EngineAssignedModifier;
@@ -105,24 +106,29 @@ public class IrisPerfectionModifier extends EngineAssignedModifier<NativeBlockSt
StateFlags states = new StateFlags(); StateFlags states = new StateFlags();
NativeBlockState air = AIR.get(); NativeBlockState air = AIR.get();
NativeBlockState water = WATER.get(); NativeBlockState water = WATER.get();
NativeBlockState[] column = new NativeBlockState[output.getHeight()];
for (int x = firstSlice; x < lastSlice; x++) { for (int x = firstSlice; x < lastSlice; x++) {
for (int z = 0; z < 16; z++) { for (int z = 0; z < 16; z++) {
perfectColumn(output, x, z, surfaces, states, air, water); perfectColumn(output, x, z, surfaces, states, air, water, column);
} }
} }
} }
private static void perfectColumn(Hunk<NativeBlockState> output, int x, int z, IntArrayList surfaces, private static void perfectColumn(Hunk<NativeBlockState> output, int x, int z, IntArrayList surfaces,
StateFlags states, NativeBlockState air, NativeBlockState water) { StateFlags states, NativeBlockState air, NativeBlockState water,
NativeBlockState[] column) {
do { do {
surfaces.clear(); surfaces.clear();
int top = getHeight(output, x, z, states); int highest = Math.min(output.getHeight() - 1, ColumnExtent.highestStoredY(output, x, z));
ColumnReadHunk.readColumn(output, x, z, 0, Math.max(0, highest), column, 0);
int top = getHeight(column, highest, states);
boolean inside = true; boolean inside = true;
surfaces.add(top); surfaces.add(top);
NativeBlockState classified = null; NativeBlockState classified = null;
int classifiedFlags = 0; int classifiedFlags = 0;
boolean columnRead = true;
for (int y = top; y >= 0; y--) { for (int y = top; y >= 0; y--) {
NativeBlockState block = output.get(x, y, z); NativeBlockState block = columnRead ? column[y] : output.get(x, y, z);
int flags = 0; int flags = 0;
if (block != null) { if (block != null) {
if (block != classified) { if (block != classified) {
@@ -133,6 +139,7 @@ public class IrisPerfectionModifier extends EngineAssignedModifier<NativeBlockSt
if ((flags & SPIKE) != 0) { if ((flags & SPIKE) != 0) {
block = normalizeSpike(block, output, x, z, y, air, water); block = normalizeSpike(block, output, x, z, y, air, water);
flags = block == null ? 0 : states.of(block); flags = block == null ? 0 : states.of(block);
columnRead = false;
} }
} }
boolean now = (flags & SOLID) != 0; boolean now = (flags & SOLID) != 0;
@@ -233,11 +240,11 @@ public class IrisPerfectionModifier extends EngineAssignedModifier<NativeBlockSt
burst.complete(); burst.complete();
} }
private static int getHeight(Hunk<NativeBlockState> output, int x, int z, StateFlags states) { private static int getHeight(NativeBlockState[] column, int highest, StateFlags states) {
NativeBlockState classified = null; NativeBlockState classified = null;
int classifiedFlags = 0; int classifiedFlags = 0;
for (int i = Math.min(output.getHeight() - 1, ColumnExtent.highestStoredY(output, x, z)); i >= 0; i--) { for (int i = highest; i >= 0; i--) {
NativeBlockState state = output.get(x, i, z); NativeBlockState state = column[i];
if (state == null) { if (state == null) {
continue; continue;
} }
@@ -1,5 +1,6 @@
package art.arcane.iris.generation.stage; package art.arcane.iris.generation.stage;
import art.arcane.iris.generation.chunk.ColumnReadHunk;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EngineAssignedComponent; import art.arcane.iris.generation.runtime.EngineAssignedComponent;
import art.arcane.iris.generation.runtime.EngineStage; import art.arcane.iris.generation.runtime.EngineStage;
@@ -111,6 +112,7 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
private final ChunkContext context; private final ChunkContext context;
private final Map<NativeBlockState, BoundaryColumnGeometry.Voxel> voxels = new IdentityHashMap<>(); private final Map<NativeBlockState, BoundaryColumnGeometry.Voxel> voxels = new IdentityHashMap<>();
private NativeBlockState air; private NativeBlockState air;
private NativeBlockState[] column = new NativeBlockState[0];
private NativeBlockState lastState; private NativeBlockState lastState;
private BoundaryColumnGeometry.Voxel lastVoxel; private BoundaryColumnGeometry.Voxel lastVoxel;
@@ -132,10 +134,14 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
return; return;
} }
int base = minimumY - this.minimumY; int base = minimumY - this.minimumY;
NativeBlockState runState = stateAt(localX, base, localZ); if (column.length < height) {
column = new NativeBlockState[height];
}
ColumnReadHunk.readColumn(blocks, localX, localZ, base, base + height - 1, column, 0);
NativeBlockState runState = orAir(column[0]);
int start = 0; int start = 0;
for (int offset = 1; offset < height; offset++) { for (int offset = 1; offset < height; offset++) {
NativeBlockState state = stateAt(localX, base + offset, localZ); NativeBlockState state = orAir(column[offset]);
if (state != runState) { if (state != runState) {
runs.add(start, offset, voxelOf(runState)); runs.add(start, offset, voxelOf(runState));
start = offset; start = offset;
@@ -146,7 +152,10 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
} }
private NativeBlockState stateAt(int localX, int offset, int localZ) { private NativeBlockState stateAt(int localX, int offset, int localZ) {
NativeBlockState state = blocks.getRaw(localX, offset, localZ); return orAir(blocks.getRaw(localX, offset, localZ));
}
private NativeBlockState orAir(NativeBlockState state) {
if (state != null) { if (state != null) {
return state; return state;
} }
@@ -39,6 +39,13 @@ public final class BakedStream<T> extends BasicStream<T> implements ChunkFillabl
return value; 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 @Override
public T get(double x, double y, double z) { public T get(double x, double y, double z) {
return scalar.get(x, y, z); return scalar.get(x, y, z);
@@ -26,6 +26,8 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;
import java.util.function.Predicate; import java.util.function.Predicate;
public final class GenerationHistory { public final class GenerationHistory {
@@ -34,6 +36,7 @@ public final class GenerationHistory {
private static final int MAXIMUM_CACHED_TRANSITION_PLANS = 4; private static final int MAXIMUM_CACHED_TRANSITION_PLANS = 4;
private static final int MAXIMUM_SEMANTIC_BATCH = 32; private static final int MAXIMUM_SEMANTIC_BATCH = 32;
private static final int MAXIMUM_PENDING_SEMANTIC_CLAIMS = 128; private static final int MAXIMUM_PENDING_SEMANTIC_CLAIMS = 128;
private static final long SEMANTIC_CLAIM_PARK_NANOS = 10_000_000L;
private final GenerationHistoryPaths paths; private final GenerationHistoryPaths paths;
private final GenerationPackRepository packs; private final GenerationPackRepository packs;
@@ -50,6 +53,7 @@ public final class GenerationHistory {
private final Map<Long, TransitionGenerationPlan> transitionPlanCache; private final Map<Long, TransitionGenerationPlan> transitionPlanCache;
private final ArrayBlockingQueue<PendingSemanticClaim> pendingSemanticClaims = private final ArrayBlockingQueue<PendingSemanticClaim> pendingSemanticClaims =
new ArrayBlockingQueue<>(MAXIMUM_PENDING_SEMANTIC_CLAIMS); new ArrayBlockingQueue<>(MAXIMUM_PENDING_SEMANTIC_CLAIMS);
private final AtomicBoolean semanticWriter = new AtomicBoolean();
private GenerationHistory( private GenerationHistory(
GenerationHistoryPaths paths, GenerationHistoryPaths paths,
@@ -776,12 +780,26 @@ public final class GenerationHistory {
persistPendingSemanticClaims(); persistPendingSemanticClaims();
} }
} }
synchronized (this) { while (!pending.settled) {
while (!pending.claim.completed) { if (!semanticWriter.compareAndSet(false, true)) {
persistPendingSemanticClaims(); LockSupport.parkNanos(pending, SEMANTIC_CLAIM_PARK_NANOS);
continue;
}
try {
synchronized (this) {
while (!pending.claim.completed) {
persistPendingSemanticClaims();
}
}
} finally {
semanticWriter.set(false);
PendingSemanticClaim next = pendingSemanticClaims.peek();
if (next != null) {
LockSupport.unpark(next.owner);
}
} }
return pending.claim.result();
} }
return pending.claim.result();
} }
private void persistPendingSemanticClaims() { private void persistPendingSemanticClaims() {
@@ -814,10 +832,15 @@ public final class GenerationHistory {
} }
while ((current = pendingSemanticClaims.poll()) != null) { while ((current = pendingSemanticClaims.poll()) != null) {
reported |= current.claim.fail(failure); reported |= current.claim.fail(failure);
current.settle();
} }
if (!reported) { if (!reported) {
throw failure; throw failure;
} }
} finally {
for (PendingSemanticClaim pending : batch) {
pending.settle();
}
} }
} }
@@ -1354,11 +1377,21 @@ public final class GenerationHistory {
private static final class PendingSemanticClaim { private static final class PendingSemanticClaim {
private final GenerationStage stage; private final GenerationStage stage;
private final GenerationSemanticIndex.Claim claim; private final GenerationSemanticIndex.Claim claim;
private final Thread owner = Thread.currentThread();
private volatile boolean settled;
private PendingSemanticClaim(GenerationStage stage, ChunkGenerationSemantics update) { private PendingSemanticClaim(GenerationStage stage, ChunkGenerationSemantics update) {
this.stage = stage; this.stage = stage;
this.claim = new GenerationSemanticIndex.Claim(update); this.claim = new GenerationSemanticIndex.Claim(update);
} }
private void settle() {
if (!claim.completed || settled) {
return;
}
settled = true;
LockSupport.unpark(owner);
}
} }
public static final class GenerationStage implements AutoCloseable { public static final class GenerationStage implements AutoCloseable {
@@ -35,7 +35,7 @@ import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterStructurePOI; import art.arcane.volmlib.util.matter.MatterStructurePOI;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.BitSet; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@@ -238,7 +238,7 @@ public final class GenerationSemanticCapture {
Mantle<Matter> mantle = engine.getMantle().getMantle(); Mantle<Matter> mantle = engine.getMantle().getMantle();
int minimumX = Math.multiplyExact(chunkX, CHUNK_SIZE); int minimumX = Math.multiplyExact(chunkX, CHUNK_SIZE);
int minimumZ = Math.multiplyExact(chunkZ, CHUNK_SIZE); int minimumZ = Math.multiplyExact(chunkZ, CHUNK_SIZE);
BitSet resolvedCaves = new BitSet(); CavePositions resolvedCaves = new CavePositions();
CaveBiomes caveBiomes = new CaveBiomes(engine, scoped); CaveBiomes caveBiomes = new CaveBiomes(engine, scoped);
mantle.iterateChunk(chunkX, chunkZ, MatterCavern.class, (localX, y, localZ, cavern) -> { mantle.iterateChunk(chunkX, chunkZ, MatterCavern.class, (localX, y, localZ, cavern) -> {
if (!caveSpace.test(localX, y, localZ)) { if (!caveSpace.test(localX, y, localZ)) {
@@ -370,17 +370,15 @@ public final class GenerationSemanticCapture {
private static void captureCave( private static void captureCave(
CaveBiomes caveBiomes, CaveBiomes caveBiomes,
ChunkGenerationSemantics.Builder semantics, ChunkGenerationSemantics.Builder semantics,
BitSet resolvedCaves, CavePositions resolvedCaves,
int blockX, int blockX,
int y, int y,
int blockZ, int blockZ,
String explicitBiomeKey String explicitBiomeKey
) { ) {
int position = (y << 8) | ((blockX & 15) << 4) | (blockZ & 15); if (!resolvedCaves.add((y << 8) | ((blockX & 15) << 4) | (blockZ & 15))) {
if (resolvedCaves.get(position)) {
return; return;
} }
resolvedCaves.set(position);
if (explicitBiomeKey != null && !explicitBiomeKey.isBlank()) { if (explicitBiomeKey != null && !explicitBiomeKey.isBlank()) {
caveBiomes.add(semantics, explicitBiomeKey); caveBiomes.add(semantics, explicitBiomeKey);
return; return;
@@ -493,6 +491,25 @@ public final class GenerationSemanticCapture {
} }
} }
private static final class CavePositions {
private static final int INITIAL_WORDS = 1536;
private long[] words = new long[INITIAL_WORDS];
private boolean add(int position) {
int word = position >>> 6;
if (word >= words.length) {
words = Arrays.copyOf(words, Math.max(word + 1, words.length << 1));
}
long bit = 1L << position;
if ((words[word] & bit) != 0L) {
return false;
}
words[word] |= bit;
return true;
}
}
private static final class CaveBiomes { private static final class CaveBiomes {
private final Engine engine; private final Engine engine;
private final boolean scoped; private final boolean scoped;
@@ -29,6 +29,8 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.zip.CRC32; import java.util.zip.CRC32;
@@ -49,6 +51,7 @@ public final class SavedBiomeStore {
private static final int MAXIMUM_CACHED_CHUNKS = 128; private static final int MAXIMUM_CACHED_CHUNKS = 128;
private static final long MAXIMUM_CACHED_BYTES = 64L * 1024L * 1024L; private static final long MAXIMUM_CACHED_BYTES = 64L * 1024L * 1024L;
private static final int MAXIMUM_WRITE_BATCH = 64; private static final int MAXIMUM_WRITE_BATCH = 64;
private static final long WRITE_PARK_NANOS = 10_000_000L;
private static final ThreadLocal<Deflater> DEFLATERS = ThreadLocal.withInitial(() -> new Deflater(Deflater.BEST_SPEED)); private static final ThreadLocal<Deflater> DEFLATERS = ThreadLocal.withInitial(() -> new Deflater(Deflater.BEST_SPEED));
private final Path dimensionRoot; private final Path dimensionRoot;
@@ -132,19 +135,34 @@ public final class SavedBiomeStore {
.putInt(body.length).put(body).putInt(checksum(body)).array(); .putInt(body.length).put(body).putInt(checksum(body)).array();
PendingWrite pending = new PendingWrite(required, record); PendingWrite pending = new PendingWrite(required, record);
lock.pending.add(pending); lock.pending.add(pending);
synchronized (lock) { while (!pending.settled) {
while (!pending.completed) { if (!lock.writer.compareAndSet(false, true)) {
persistPending(lock); LockSupport.parkNanos(pending, WRITE_PARK_NANOS);
continue;
} }
if (pending.failure != null) { try {
throw pending.failure; synchronized (lock) {
while (!pending.completed) {
persistPending(lock);
}
}
} finally {
lock.writer.set(false);
PendingWrite next = lock.pending.peek();
if (next != null) {
LockSupport.unpark(next.owner);
}
} }
return pending.persisted;
} }
if (pending.failure != null) {
throw pending.failure;
}
return pending.persisted;
} }
private void persistPending(WriteStripe stripe) { private void persistPending(WriteStripe stripe) {
Map<Long, List<PendingWrite>> batches = new LinkedHashMap<>(); Map<Long, List<PendingWrite>> batches = new LinkedHashMap<>();
List<PendingWrite> polled = new ArrayList<>(MAXIMUM_WRITE_BATCH);
PendingWrite grouping = null; PendingWrite grouping = null;
try { try {
for (int count = 0; count < MAXIMUM_WRITE_BATCH; count++) { for (int count = 0; count < MAXIMUM_WRITE_BATCH; count++) {
@@ -152,6 +170,7 @@ public final class SavedBiomeStore {
if (grouping == null) { if (grouping == null) {
break; break;
} }
polled.add(grouping);
long regionKey = key(grouping.chunk.chunkX() >> 5, grouping.chunk.chunkZ() >> 5); long regionKey = key(grouping.chunk.chunkX() >> 5, grouping.chunk.chunkZ() >> 5);
batches.computeIfAbsent(regionKey, ignored -> new ArrayList<>()).add(grouping); batches.computeIfAbsent(regionKey, ignored -> new ArrayList<>()).add(grouping);
grouping = null; grouping = null;
@@ -175,6 +194,10 @@ public final class SavedBiomeStore {
} }
} }
throw failure; throw failure;
} finally {
for (PendingWrite pending : polled) {
pending.settle();
}
} }
} }
@@ -822,19 +845,30 @@ public final class SavedBiomeStore {
private static final class WriteStripe { private static final class WriteStripe {
private final ConcurrentLinkedQueue<PendingWrite> pending = new ConcurrentLinkedQueue<>(); private final ConcurrentLinkedQueue<PendingWrite> pending = new ConcurrentLinkedQueue<>();
private final AtomicBoolean writer = new AtomicBoolean();
} }
private static final class PendingWrite { private static final class PendingWrite {
private final SavedBiomeChunk chunk; private final SavedBiomeChunk chunk;
private final byte[] record; private final byte[] record;
private final Thread owner = Thread.currentThread();
private boolean completed; private boolean completed;
private boolean persisted; private boolean persisted;
private IOException failure; private IOException failure;
private volatile boolean settled;
private PendingWrite(SavedBiomeChunk chunk, byte[] record) { private PendingWrite(SavedBiomeChunk chunk, byte[] record) {
this.chunk = chunk; this.chunk = chunk;
this.record = record; this.record = record;
} }
private void settle() {
if (!completed || settled) {
return;
}
settled = true;
LockSupport.unpark(owner);
}
} }
private static final class DecodeBudget { private static final class DecodeBudget {
@@ -268,6 +268,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
@Override @Override
public void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) { public void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
retention = createRetention(minRegionX, minRegionZ, maxRegionX, maxRegionZ); retention = createRetention(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
hydrology.bound(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
} }
private PregenChunkRetention createRetention(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) { private PregenChunkRetention createRetention(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
@@ -219,6 +219,11 @@ public class MedievalPregenMethod implements PregeneratorMethod {
return false; return false;
} }
@Override
public void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
hydrology.bound(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
}
@Override @Override
public void onRegionsAhead(List<long[]> regions) { public void onRegionsAhead(List<long[]> regions) {
if (!closing.get()) { if (!closing.get()) {
@@ -13,6 +13,7 @@ public final class PregenHydrology implements AutoCloseable {
private final Supplier<Engine> engine; private final Supplier<Engine> engine;
private HydrologyRuntime owner; private HydrologyRuntime owner;
private HydrologyPregeneration epoch; private HydrologyPregeneration epoch;
private int[] bounds;
private boolean closed; private boolean closed;
public PregenHydrology(Supplier<Engine> engine) { public PregenHydrology(Supplier<Engine> engine) {
@@ -28,12 +29,21 @@ public final class PregenHydrology implements AutoCloseable {
withdraw(); withdraw();
owner = runtime; owner = runtime;
epoch = runtime == null ? null : runtime.openPregeneration(); epoch = runtime == null ? null : runtime.openPregeneration();
applyBounds();
} }
if (epoch != null) { if (epoch != null) {
epoch.prepare(regions); 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 @Override
public synchronized void close() { public synchronized void close() {
closed = true; closed = true;
@@ -41,6 +51,12 @@ public final class PregenHydrology implements AutoCloseable {
owner = null; owner = null;
} }
private void applyBounds() {
if (epoch != null && bounds != null) {
epoch.bound(bounds[0], bounds[1], bounds[2], bounds[3]);
}
}
private void withdraw() { private void withdraw() {
if (epoch != null) { if (epoch != null) {
epoch.close(); epoch.close();
@@ -44,7 +44,7 @@ public class PlatformStateRebindTest {
} }
@Test @Test
public void chunkDataAirIsResolvedAgainstTheCurrentlyBoundPlatform() { public void chunkDataAirIsResolvedAgainstThePlatformBoundWhenTheBufferFirstReadsIt() {
NativeBlockState first = bindPlatform("first"); NativeBlockState first = bindPlatform("first");
SectionBuffer holder = new SectionBuffer(384); SectionBuffer holder = new SectionBuffer(384);
@@ -53,7 +53,8 @@ public class PlatformStateRebindTest {
IrisPlatforms.unbind(); IrisPlatforms.unbind();
NativeBlockState second = bindPlatform("second"); NativeBlockState second = bindPlatform("second");
assertSame(second, holder.getRaw(0, -1, 0)); assertSame(first, holder.getRaw(0, -1, 0));
assertSame(second, new SectionBuffer(384).getRaw(0, -1, 0));
} }
@Test @Test
@@ -264,6 +264,42 @@ public class SectionBufferTest {
assertEquals(List.of(9, 8, 7), order); assertEquals(List.of(9, 8, 7), order);
} }
@Test
public void columnReadsMatchSingleReadsAcrossEmptySectionsAndBufferEdges() {
SectionBuffer buffer = new SectionBuffer(40);
NativeBlockState stone = mock(NativeBlockState.class);
NativeBlockState dirt = mock(NativeBlockState.class);
buffer.setRaw(3, 2, 9, stone);
buffer.setRaw(3, 33, 9, dirt);
buffer.setRaw(3, 39, 9, stone);
ColumnExtentListeningHunk<NativeBlockState> listening = new ColumnExtentListeningHunk<>(buffer, (x, y, z, state) -> {
});
NativeBlockState[] column = new NativeBlockState[52];
ColumnReadHunk.readColumn(listening, 3, 9, -3, 44, column, 4);
for (int y = -3; y <= 44; y++) {
assertSame("y=" + y, buffer.getRaw(3, y, 9), column[4 + y + 3]);
}
assertNull(column[0]);
assertSame(dirt, column[4 + 33 + 3]);
}
@Test
public void plainHunksAnswerColumnReadsWithSingleReads() {
Hunk<NativeBlockState> array = Hunk.newArrayHunk(16, 32, 16);
NativeBlockState stone = mock(NativeBlockState.class);
array.setRaw(5, 7, 1, stone);
NativeBlockState[] column = new NativeBlockState[10];
ColumnReadHunk.readColumn(array, 5, 1, 6, 8, column, 2);
assertNull(column[2]);
assertSame(stone, column[3]);
assertNull(column[4]);
assertNull(column[5]);
}
private static void assertSameContents(SectionBuffer expected, SectionBuffer actual) { private static void assertSameContents(SectionBuffer expected, SectionBuffer actual) {
assertEquals(expected.sectionCount(), actual.sectionCount()); assertEquals(expected.sectionCount(), actual.sectionCount());
NativeBlockState[] expectedPalette = expected.palette(); NativeBlockState[] expectedPalette = expected.palette();
@@ -3,6 +3,8 @@ package art.arcane.iris.generation.context;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import org.junit.Test; import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier; import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -50,6 +52,42 @@ public class IrisContextIsolationTest {
assertNull(rightAfterClose.get()); assertNull(rightAfterClose.get());
} }
@Test
public void contextsStayIsolatedWhenThreadsOutnumberTheBindingSlots() throws Exception {
Engine engine = mock(Engine.class);
int threads = 640;
CyclicBarrier allBound = new CyclicBarrier(threads);
AtomicReference<Throwable> failure = new AtomicReference<>();
List<Thread> workers = new ArrayList<>(threads);
for (int index = 0; index < threads; index++) {
long session = index + 1L;
Thread worker = new Thread(() -> {
try (IrisContext.Scope ignored = IrisContext.open(engine, session, null)) {
allBound.await(30L, TimeUnit.SECONDS);
if (IrisContext.get().getGenerationSessionId() != session) {
failure.compareAndSet(null, new AssertionError("Thread observed session "
+ IrisContext.get().getGenerationSessionId() + " instead of " + session));
}
allBound.await(30L, TimeUnit.SECONDS);
} catch (Throwable thrown) {
failure.compareAndSet(null, thrown);
}
if (IrisContext.get() != null) {
failure.compareAndSet(null, new AssertionError("Context survived its scope"));
}
}, "iris-context-crowd-" + index);
workers.add(worker);
worker.start();
}
for (Thread worker : workers) {
worker.join(60000L);
assertFalse("Context worker did not finish.", worker.isAlive());
}
assertNull(failure.get());
assertNull(IrisContext.get());
}
@Test @Test
public void scopeRebindsWorkerWhenEngineChanges() throws Exception { public void scopeRebindsWorkerWhenEngineChanges() throws Exception {
Engine firstEngine = mock(Engine.class); Engine firstEngine = mock(Engine.class);
@@ -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.function.BooleanSupplier;
import java.util.stream.Stream; import java.util.stream.Stream;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
@@ -215,6 +216,34 @@ public class HydrologyRuntimeTest {
assertEquals(planned + 1L, runtime.previewRevision()); 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) @Test(timeout = 180_000L)
public void failedRasterDoesNotAdvancePreviewRevision() throws Exception { public void failedRasterDoesNotAdvancePreviewRevision() throws Exception {
FakePlanners planners = new FakePlanners(settings(0, false)); FakePlanners planners = new FakePlanners(settings(0, false));
@@ -414,6 +443,35 @@ public class HydrologyRuntimeTest {
assertEquals(3, prefetches.get()); 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) @Test(timeout = 180_000L)
public void preparePregenerationPlansRegionsInOrder() throws Exception { public void preparePregenerationPlansRegionsInOrder() throws Exception {
FakePlanners planners = new FakePlanners(settings()); FakePlanners planners = new FakePlanners(settings());
@@ -154,10 +154,9 @@ public class IrisHydrologyTerrainSourceTest {
HydrologySampleBuffer rect = new HydrologySampleBuffer(32 * 32); HydrologySampleBuffer rect = new HydrologySampleBuffer(32 * 32);
fine.fill(-600, 400, 32, 32, rect); fine.fill(-600, 400, 32, 32, rect);
assertMatchesScalar(reference, "fine rect", rect, nodes(-600, 400, 32, 1, 4), false); 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); HydrologySampleBuffer strided = new HydrologySampleBuffer(16 * 16);
window.fill(-150, 300, 16, 16, 3, strided); fine.fill(-300, 600, 16, 16, 6, strided);
assertMatchesScalar(reference, "window strided", strided, nodes(-150, 300, 16, 3, 8), false); assertMatchesScalar(reference, "fine strided", strided, nodes(-300, 600, 16, 6, 4), false);
HeightLattice coarse = new HeightLattice(source, 64, true, 1 << 12); HeightLattice coarse = new HeightLattice(source, 64, true, 1 << 12);
HydrologySampleBuffer coarseBase = new HydrologySampleBuffer(16 * 16); HydrologySampleBuffer coarseBase = new HydrologySampleBuffer(16 * 16);
coarse.fill(-8, -8, 16, 16, coarseBase); coarse.fill(-8, -8, 16, 16, coarseBase);
@@ -287,6 +287,31 @@ public class HeightLatticeTest {
assertThrows(IllegalArgumentException.class, () -> lattice.fill(cellXs, cellZs, new HydrologySampleBuffer(5))); 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) { 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); 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) { 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 HeightLattice(terrain, WindowPlanner.FINE_CELL, false, HydrologyPlanCache.FINE_LATTICE_CELLS), new PondPlanner(settings),
new ReachShaper(settings)); new ReachShaper(settings));
} }
@@ -81,7 +81,7 @@ public class HydrologyPlanCodecTest {
assertEquals(planned, HydrologyPlanCodec.decodeRegion(settings, HydrologyPlanCodec.encode(settings, planned)).orElseThrow()); assertEquals(planned, HydrologyPlanCodec.decodeRegion(settings, HydrologyPlanCodec.encode(settings, planned)).orElseThrow());
regions.add(planned); 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)) new HeightLattice(terrain, WindowPlanner.FINE_CELL, false, 1 << 21), new PondPlanner(settings), new ReachShaper(settings))
.plan(LAND, regions); .plan(LAND, regions);
assertFalse(planned.reaches().isEmpty()); 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); 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()))); return new ShapedNetwork(List.of(outlet), List.of(pond.withOutletReachId(outlet.id())));
}).when(shaper).shape(any(), any(), any(), any(), any(), any(), anyLong()); }).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); new HeightLattice(terrain, WindowPlanner.FINE_CELL, false, 1 << 18), ponds, shaper);
WindowComputation settled = planner.compute(ORIGIN, noRegions(ORIGIN)); WindowComputation settled = planner.compute(ORIGIN, noRegions(ORIGIN));
@@ -587,7 +587,7 @@ public class WindowPlannerTest {
} }
return network; return network;
}).when(shaper).shape(any(), any(), any(), any(), any(), any(), anyLong()); }).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); 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) { 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)); 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.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.IntFunction; import java.util.function.IntFunction;
@@ -50,18 +51,37 @@ public class CavePoolFillerTest {
return 0; return 0;
}); });
for (int chunkX = 0; chunkX < 1024; chunkX++) { 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); assertTrue("half density samples approximately half the candidate columns", columns.size() > 3600 && columns.size() < 4500);
Set<Long> repeated = new HashSet<>(columns); Set<Long> repeated = new HashSet<>(columns);
columns.clear(); columns.clear();
for (int chunkX = 1023; chunkX >= 0; chunkX--) { for (int chunkX = 1023; chunkX >= 0; chunkX--) {
filler.fill(chunkX, 0, carved, writer, SEED); filler.fill(chunkX, 0, () -> carved, writer, SEED);
} }
assertEquals(repeated, columns); assertEquals(repeated, columns);
verifyNoInteractions(writer); 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 @Test
public void zeroGlobalDensityDoesNotReadCaves() { public void zeroGlobalDensityDoesNotReadCaves() {
IrisHydrology config = new IrisHydrology().setDensity(0D); IrisHydrology config = new IrisHydrology().setDensity(0D);
@@ -69,7 +89,7 @@ public class CavePoolFillerTest {
CavePoolFiller filler = new CavePoolFiller(HydrologySettings.compile(config, SEED, 63, 0, 384)); CavePoolFiller filler = new CavePoolFiller(HydrologySettings.compile(config, SEED, 63, 0, 384));
CarvedTerrain carved = mock(CarvedTerrain.class); CarvedTerrain carved = mock(CarvedTerrain.class);
MantleWriter writer = mock(MantleWriter.class); MantleWriter writer = mock(MantleWriter.class);
filler.fill(0, 0, carved, writer, SEED); filler.fill(0, 0, () -> carved, writer, SEED);
verifyNoInteractions(carved, writer); verifyNoInteractions(carved, writer);
} }
@@ -143,10 +163,10 @@ public class CavePoolFillerTest {
MantleWriter forwardWriter = writer(forward); MantleWriter forwardWriter = writer(forward);
MantleWriter reverseWriter = writer(reverse); MantleWriter reverseWriter = writer(reverse);
for (int index = 0; index < 9; index++) { 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--) { 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()); assertFalse(forward.isEmpty());
@@ -284,7 +304,7 @@ public class CavePoolFillerTest {
private static Map<Long, HydrologyCaveCell> fill(HydrologySettings settings, VoxelWorld world, int chunkX, int chunkZ) { private static Map<Long, HydrologyCaveCell> fill(HydrologySettings settings, VoxelWorld world, int chunkX, int chunkZ) {
Map<Long, HydrologyCaveCell> written = new HashMap<>(); 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; return written;
} }
@@ -324,7 +324,7 @@ public class MantleCarvedTerrainTest {
for (SeaInlet inlet : complex.getHydrologyRuntime().inlets(0, chunkZ)) { for (SeaInlet inlet : complex.getHydrologyRuntime().inlets(0, chunkZ)) {
rasterizer.write(inlet, 0, chunkZ, writer, HydrologyCaveCell.SEA_FLUID, "", SeaInletRasterizer.SubterrainFluid.NOWHERE); 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; return cells;
} }
@@ -22,11 +22,13 @@ import org.junit.rules.TemporaryFolder;
import org.mockito.MockedStatic; import org.mockito.MockedStatic;
import org.mockito.Mockito; import org.mockito.Mockito;
import java.lang.reflect.Field;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.SplittableRandom; import java.util.SplittableRandom;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.Answers.CALLS_REAL_METHODS; import static org.mockito.Answers.CALLS_REAL_METHODS;
import static org.mockito.ArgumentMatchers.any; 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 @Test
public void batchedChunksEvaluateExactlyTheScalarGeneratorSet() throws Exception { public void batchedChunksEvaluateExactlyTheScalarGeneratorSet() throws Exception {
try (Statics ignored = new Statics(); Fixture batched = new Fixture(pack()); Fixture scalar = new Fixture(pack())) { 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); constructor.setAccessible(true);
Object cache = constructor.newInstance(4096); Object cache = constructor.newInstance(4096);
Method get = type.getDeclaredMethod("get", double.class, double.class); 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); get.setAccessible(true);
put.setAccessible(true); put.setAccessible(true);
IrisBiome first = new IrisBiome(); IrisBiome first = new IrisBiome();
@@ -266,7 +266,7 @@ public class IrisComplexBoundsBiomeCacheTest {
} }
Field values = type.getDeclaredField("values"); Field values = type.getDeclaredField("values");
values.setAccessible(true); values.setAccessible(true);
assertEquals(4096, ((IrisBiome[]) values.get(cache)).length); assertEquals(4096, ((Object[]) values.get(cache)).length);
} }
private static ProceduralStream<IrisBiome> constant(IrisBiome biome) { private static ProceduralStream<IrisBiome> constant(IrisBiome biome) {
@@ -295,7 +295,7 @@ public class IrisComplexBoundsBiomeCacheTest {
constructor.setAccessible(true); constructor.setAccessible(true);
cache = constructor.newInstance(); cache = constructor.newInstance();
sample = IrisComplex.class.getDeclaredMethod("sampleBoundsRaw", cacheClass, Engine.class, 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); sample.setAccessible(true);
boundsConstructor = Class.forName("art.arcane.iris.generation.runtime.IrisComplex$GeneratorBounds") boundsConstructor = Class.forName("art.arcane.iris.generation.runtime.IrisComplex$GeneratorBounds")
.getDeclaredConstructor(double.class, double.class); .getDeclaredConstructor(double.class, double.class);
@@ -313,7 +313,7 @@ public class IrisComplexBoundsBiomeCacheTest {
private NoiseBounds sample(Object gridCache, IrisInterpolator interpolator, double x, double z) private NoiseBounds sample(Object gridCache, IrisInterpolator interpolator, double x, double z)
throws ReflectiveOperationException { 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 { 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 xi = (int) ((state & Long.MAX_VALUE) % coordinates.length);
int zi = (int) ((state >>> 10) % coordinates.length); int zi = (int) ((state >>> 10) % coordinates.length);
cache.put(coordinates[xi], coordinates[zi], biomes[xi][zi]); 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) { if (actual != null) {
hits.incrementAndGet(); hits.incrementAndGet();
if (actual != biomes[xi][zi]) { if (actual != biomes[xi][zi]) {
@@ -250,7 +250,7 @@ public class GenerationHistoryAdmissionTest {
.addSurfaceBiome("iris:conflict").seal().build() .addSurfaceBiome("iris:conflict").seal().build()
: claim(stage); : claim(stage);
results.add(executor.submit(() -> persistCounting(history, stage, update, appends, forces))); results.add(executor.submit(() -> persistCounting(history, stage, update, appends, forces)));
awaitBlockedOnHistory(history, workers, index + 1); awaitQueuedOnHistory(history, workers, index + 1);
} }
stages.get(5).close(); stages.get(5).close();
} }
@@ -340,7 +340,7 @@ public class GenerationHistoryAdmissionTest {
for (int index = 0; index < stages.size(); index++) { for (int index = 0; index < stages.size(); index++) {
GenerationHistory.GenerationStage stage = stages.get(index); GenerationHistory.GenerationStage stage = stages.get(index);
results.add(executor.submit(() -> persistFailingRegion(history, stage, failure))); results.add(executor.submit(() -> persistFailingRegion(history, stage, failure)));
awaitBlockedOnHistory(history, workers, index + 1); awaitQueuedOnHistory(history, workers, index + 1);
} }
} }
assertTrue(results.getFirst().get(5, TimeUnit.SECONDS)); assertTrue(results.getFirst().get(5, TimeUnit.SECONDS));
@@ -404,21 +404,27 @@ public class GenerationHistoryAdmissionTest {
} }
} }
private static void awaitBlockedOnHistory(GenerationHistory history, List<Thread> workers, int count) { private static void awaitQueuedOnHistory(GenerationHistory history, List<Thread> workers, int count) {
Await.until("semantic claim callers to queue behind the History monitor", Duration.ofSeconds(5L), Await.until("semantic claim callers to queue behind the History writer", Duration.ofSeconds(5L),
() -> blockedOnHistory(history, workers) == count); () -> queuedOnHistory(history, workers) == count);
} }
private static int blockedOnHistory(GenerationHistory history, List<Thread> workers) { private static int queuedOnHistory(GenerationHistory history, List<Thread> workers) {
int blocked = 0; int queued = 0;
for (Thread worker : workers) { for (Thread worker : workers) {
ThreadInfo info = ManagementFactory.getThreadMXBean().getThreadInfo(worker.threadId()); ThreadInfo info = ManagementFactory.getThreadMXBean().getThreadInfo(worker.threadId());
if (info != null && info.getThreadState() == Thread.State.BLOCKED if (info == null || info.getLockInfo() == null) {
&& info.getLockInfo().getIdentityHashCode() == System.identityHashCode(history)) { continue;
blocked++; }
boolean writer = info.getThreadState() == Thread.State.BLOCKED
&& info.getLockInfo().getIdentityHashCode() == System.identityHashCode(history);
boolean follower = info.getThreadState() == Thread.State.TIMED_WAITING
&& info.getLockInfo().getClassName().endsWith("GenerationHistory$PendingSemanticClaim");
if (writer || follower) {
queued++;
} }
} }
return blocked; return queued;
} }
private static boolean persistPaused(GenerationHistory history, GenerationHistory.GenerationStage stage, private static boolean persistPaused(GenerationHistory history, GenerationHistory.GenerationStage stage,
@@ -195,7 +195,8 @@ final class HydrologyPlanHarness {
double landTiles(WindowKey key) { double landTiles(WindowKey key) {
int cells = WindowKey.SIZE / WindowPlanner.CELL; int cells = WindowKey.SIZE / WindowPlanner.CELL;
HydrologySampleBuffer samples = new HydrologySampleBuffer(cells * cells); 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; int land = 0;
for (int cell = 0; cell < cells * cells; cell++) { for (int cell = 0; cell < cells * cells; cell++) {
if (!samples.ocean()[cell]) { if (!samples.ocean()[cell]) {