Tiled hydrology lattice

- Stored lattice samples in compact 32x32 tiles with least-recently-used tile eviction
- Cached sampled pond rim columns in a block-resolution tile cache
- Read window coarse nodes from the fine lattice at stride 2
- Removed the window lattice and FineLatticeSource
- Sized the fine lattice for an eleven-window span and the region lattice for a seven-region span
- Updated lattice, planner and probe harness callers for the single fine lattice
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-30 20:54:41 -04:00
1 parent c700138b66
commit 8773d4b008
12 files changed
+323 -292

No files matched your search

@@ -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);
} }
} }
@@ -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());
@@ -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;
}
}
}
@@ -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]) {