Merge branch 'perf-plan' into perf-review

This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-30 21:29:13 -04:00
commit 8e625d4547
20 files changed
+457 -296

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()) {
@@ -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;
@@ -77,6 +78,7 @@ public final class HydrologyRuntime {
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;
@@ -99,6 +101,7 @@ public final class HydrologyRuntime {
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;
@@ -125,6 +128,7 @@ public final class HydrologyRuntime {
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;
@@ -354,8 +358,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);
} }
} }
@@ -667,7 +691,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 +699,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 +796,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());
@@ -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++) {
@@ -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();
@@ -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;
@@ -414,6 +415,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));
} }
@@ -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]) {