diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/engine/FineLatticeSource.java b/core/src/main/java/art/arcane/iris/generation/hydrology/engine/FineLatticeSource.java deleted file mode 100644 index 73db7475d..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/engine/FineLatticeSource.java +++ /dev/null @@ -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(); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/engine/HydrologyPlanners.java b/core/src/main/java/art/arcane/iris/generation/hydrology/engine/HydrologyPlanners.java index 7ff67fb26..88bea3a5e 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/engine/HydrologyPlanners.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/engine/HydrologyPlanners.java @@ -11,23 +11,21 @@ import art.arcane.iris.generation.hydrology.water.ReachShaper; import java.util.Objects; -public record HydrologyPlanners(RegionPlanner regions, WindowPlanner windows, HeightLattice windowLattice) { +public record HydrologyPlanners(RegionPlanner regions, WindowPlanner windows, HeightLattice fineLattice) { public HydrologyPlanners { Objects.requireNonNull(regions, "regions"); Objects.requireNonNull(windows, "windows"); - Objects.requireNonNull(windowLattice, "windowLattice"); + Objects.requireNonNull(fineLattice, "fineLattice"); } public static HydrologyPlanners create(HydrologySettings settings, HydrologyTerrainSource source) { Objects.requireNonNull(settings, "settings"); Objects.requireNonNull(source, "source"); HeightLattice fine = new HeightLattice(source, WindowPlanner.FINE_CELL, false, HydrologyPlanCache.FINE_LATTICE_CELLS); - HeightLattice window = new HeightLattice(new FineLatticeSource(fine, source), WindowPlanner.CELL, false, - HydrologyPlanCache.WINDOW_LATTICE_CELLS); PondPlanner ponds = new PondPlanner(settings); ReachShaper shaper = new ReachShaper(settings); RegionPlanner regions = new RegionPlanner(settings, new HeightLattice(source, RegionPlanner.CELL, true, HydrologyPlanCache.REGION_LATTICE_CELLS), fine, ponds, shaper); - return new HydrologyPlanners(regions, new WindowPlanner(settings, window, fine, ponds, shaper), window); + return new HydrologyPlanners(regions, new WindowPlanner(settings, fine, ponds, shaper), fine); } } diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/grid/HeightLattice.java b/core/src/main/java/art/arcane/iris/generation/hydrology/grid/HeightLattice.java index 1f1d5c7f4..cc1804606 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/grid/HeightLattice.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/grid/HeightLattice.java @@ -1,34 +1,59 @@ package art.arcane.iris.generation.hydrology.grid; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReferenceArray; public final class HeightLattice { + private static final int TILE_SHIFT = 5; + private static final int COLUMN_TILE_SHIFT = 3; + private static final int COLUMN_CELLS = 1 << 20; + private static final CompletableFuture READY = CompletableFuture.completedFuture(null); + private static final int[] NO_NODES = new int[0]; + private static final Tile[] NO_TILES = new Tile[0]; + private final HydrologyTerrainSource source; private final int cell; private final boolean baseOnly; - private final int capacityCells; - private final ConcurrentHashMap cells; - private final ConcurrentLinkedQueue insertionOrder; + private final int tileShift; + private final int tileMask; + private final int tileCells; + private final int capacityTiles; + private final ConcurrentHashMap tiles; + private final AtomicLong clock; private final AtomicBoolean evicting; + private final HeightLattice columns; public HeightLattice(HydrologyTerrainSource source, int cell, boolean baseOnly, int capacityCells) { + this(source, cell, baseOnly, capacityCells, TILE_SHIFT, new HeightLattice(source, 1, baseOnly, COLUMN_CELLS, COLUMN_TILE_SHIFT, null)); + } + + private HeightLattice(HydrologyTerrainSource source, int cell, boolean baseOnly, int capacityCells, int tileShift, HeightLattice columns) { if (cell <= 0 || capacityCells <= 0) { throw new IllegalArgumentException("Invalid lattice cell=" + cell + " capacityCells=" + capacityCells); } this.source = Objects.requireNonNull(source, "source"); this.cell = cell; this.baseOnly = baseOnly; - this.capacityCells = capacityCells; - this.cells = new ConcurrentHashMap<>(); - this.insertionOrder = new ConcurrentLinkedQueue<>(); + this.tileShift = tileShift; + this.tileMask = (1 << tileShift) - 1; + this.tileCells = 1 << (2 * tileShift); + this.capacityTiles = Math.ceilDiv(capacityCells, tileCells); + this.tiles = new ConcurrentHashMap<>(); + this.clock = new AtomicLong(); this.evicting = new AtomicBoolean(); + this.columns = columns; } public int cell() { @@ -67,19 +92,24 @@ public final class HeightLattice { throw new IllegalArgumentException("Column sample has " + xs.length + " x and " + zs.length + " z coordinates"); } requireCapacity(out, xs.length); - if (xs.length > 0) { - source.sample(xs, zs, out, baseOnly); + if (xs.length == 0) { + return; } + if (columns == null) { + source.sample(xs, zs, out, baseOnly); + return; + } + columns.resolve(new Scatter(xs, zs), xs.length, out); } private void resolve(Nodes nodes, int count, HydrologySampleBuffer out) { - Slot[] slots = new Slot[count]; - int unresolved = count; - while (unresolved > 0) { - CompletableFuture batch = claimAndSample(nodes, slots); - unresolved = awaitForeign(batch, slots); + Pass pass = new Pass(nodes, out, clock.incrementAndGet()); + int[] open = null; + int remaining = count; + while (remaining > 0) { + open = pass.run(open, remaining); + remaining = open == null ? 0 : open.length; } - copy(slots, out); evictOverflow(); } @@ -94,130 +124,33 @@ public final class HeightLattice { Math.multiplyExact(Math.addExact(cell0, Math.multiplyExact(extent - 1, stride)), cell); } - private CompletableFuture claimAndSample(Nodes nodes, Slot[] slots) { - CompletableFuture batch = new CompletableFuture<>(); - try { - int[] xs = new int[slots.length]; - int[] zs = new int[slots.length]; - int claimed = claim(batch, nodes, slots, xs, zs); - if (claimed > 0) { - HydrologySampleBuffer samples = new HydrologySampleBuffer(claimed); - source.sample(Arrays.copyOf(xs, claimed), Arrays.copyOf(zs, claimed), samples, baseOnly); - batch.complete(samples); - } - } catch (Throwable failure) { - try { - release(batch, nodes, slots); - } finally { - batch.completeExceptionally(failure); - } - throw failure; - } - return batch; - } - - private int claim(CompletableFuture 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 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 own, Slot[] slots) { - int unresolved = 0; - for (int index = 0; index < slots.length; index++) { - CompletableFuture batch = slots[index].batch(); - if (batch != own && !await(batch)) { - slots[index] = null; - unresolved++; - } - } - return unresolved; - } - - private static boolean await(CompletableFuture batch) { - if (!batch.isDone()) { - try { - batch.get(); - } catch (ExecutionException | CancellationException failure) { - return false; - } catch (InterruptedException interruption) { - Thread.currentThread().interrupt(); - throw new CancellationException("Hydrology lattice wait interrupted."); - } - } - return !batch.isCompletedExceptionally(); - } - - private static void copy(Slot[] slots, HydrologySampleBuffer out) { - float[] height = out.height(); - boolean[] ocean = out.ocean(); - float[] rainfall = out.rainfall(); - float[] pondDensity = out.pondDensity(); - byte[] permission = out.permission(); - String[] fluidProfile = out.fluidProfile(); - for (int index = 0; index < slots.length; index++) { - Slot slot = slots[index]; - HydrologySampleBuffer samples = slot.batch().join(); - int sample = slot.index(); - height[index] = samples.height()[sample]; - ocean[index] = samples.ocean()[sample]; - rainfall[index] = samples.rainfall()[sample]; - pondDensity[index] = samples.pondDensity()[sample]; - permission[index] = samples.permission()[sample]; - fluidProfile[index] = samples.fluidProfile()[sample]; + private Tile tile(int tileX, int tileZ) { + Long key = key(tileX, tileZ); + Tile known = tiles.get(key); + if (known != null) { + return known; } + return tiles.computeIfAbsent(key, ignored -> new Tile(tileCells)); } private void evictOverflow() { - if (cells.size() <= capacityCells || !evicting.compareAndSet(false, true)) { + if (tiles.size() <= capacityTiles || !evicting.compareAndSet(false, true)) { return; } try { - int excess = cells.size() - capacityCells; - while (excess > 0) { - Long key = insertionOrder.poll(); - if (key == null) { - return; + List> idle = new ArrayList<>(tiles.size()); + for (Map.Entry entry : tiles.entrySet()) { + if (entry.getValue().pending.get() == 0) { + idle.add(entry); } - Slot slot = cells.get(key); - if (slot == null) { - continue; - } - if (!slot.batch().isDone()) { - insertionOrder.offer(key); - excess--; - } else if (cells.remove(key, slot)) { - excess--; + } + idle.sort(Comparator.comparingLong(entry -> entry.getValue().used)); + int target = capacityTiles - (capacityTiles >> 3); + for (int index = 0; index < idle.size() && tiles.size() > target; index++) { + Map.Entry entry = idle.get(index); + Tile tile = entry.getValue(); + if (tile.pending.get() == 0) { + tiles.remove(entry.getKey(), tile); } } } finally { @@ -232,7 +165,219 @@ public final class HeightLattice { return mixed ^ (mixed >>> 31); } - private record Slot(CompletableFuture batch, int index) { + private static boolean await(CompletableFuture 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 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 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 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 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 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 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> 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 { diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/plan/HydrologyPlanCache.java b/core/src/main/java/art/arcane/iris/generation/hydrology/plan/HydrologyPlanCache.java index ec2365dc8..4269e145b 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/plan/HydrologyPlanCache.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/plan/HydrologyPlanCache.java @@ -32,10 +32,10 @@ import java.util.function.Consumer; import java.util.function.Function; public final class HydrologyPlanCache { - private static final int FRONT = 3; - public static final int WINDOW_LATTICE_CELLS = frontCells(WindowKey.SIZE, WindowPlanner.CELL); - public static final int FINE_LATTICE_CELLS = frontCells(WindowKey.SIZE, WindowPlanner.FINE_CELL); - public static final int REGION_LATTICE_CELLS = frontCells(RegionKey.SIZE, RegionPlanner.CELL); + private static final int FINE_FRONT = 9; + private static final int REGION_FRONT = 5; + public static final int FINE_LATTICE_CELLS = frontCells(FINE_FRONT, WindowKey.SIZE, WindowPlanner.FINE_CELL); + public static final int REGION_LATTICE_CELLS = frontCells(REGION_FRONT, RegionKey.SIZE, RegionPlanner.CELL); private static final int WINDOW_CAPACITY = 128; private static final int REGION_CAPACITY = 64; private static final long DEMAND = Long.MIN_VALUE; @@ -312,8 +312,8 @@ public final class HydrologyPlanCache { return Math.max(1, parallelism / 2); } - private static int frontCells(int unit, int cell) { - int cells = (FRONT + 2) * unit / cell; + private static int frontCells(int front, int unit, int cell) { + int cells = (front + 2) * unit / cell; return cells * cells; } diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/plan/WindowPlanner.java b/core/src/main/java/art/arcane/iris/generation/hydrology/plan/WindowPlanner.java index 82df753de..07238e748 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/plan/WindowPlanner.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/plan/WindowPlanner.java @@ -36,6 +36,7 @@ public final class WindowPlanner { public static final int CELL = 8; public static final int FINE_CELL = 4; private static final int WINDOW_CELLS = WindowKey.SIZE / CELL; + private static final int FINE_STRIDE = CELL / FINE_CELL; private static final int SPAN = 3 * WINDOW_CELLS; private static final int REACH_OVERHANG = 256; private static final float BASIN_DEPTH = 0.5f; @@ -43,22 +44,17 @@ public final class WindowPlanner { private static final long SALT_HEAD = 0x5354524541484dL; private final HydrologySettings settings; - private final HeightLattice lattice; private final HeightLattice fine; private final PondPlanner ponds; private final ReachShaper shaper; private final SeaInletPlanner inlets; - public WindowPlanner(HydrologySettings settings, HeightLattice lattice, HeightLattice fine, PondPlanner ponds, ReachShaper shaper) { + public WindowPlanner(HydrologySettings settings, HeightLattice fine, PondPlanner ponds, ReachShaper shaper) { this.settings = Objects.requireNonNull(settings, "settings"); - this.lattice = Objects.requireNonNull(lattice, "lattice"); this.fine = Objects.requireNonNull(fine, "fine"); this.ponds = Objects.requireNonNull(ponds, "ponds"); this.shaper = Objects.requireNonNull(shaper, "shaper"); this.inlets = new SeaInletPlanner(settings); - if (lattice.cell() != CELL) { - throw new IllegalArgumentException("Window lattice cell " + lattice.cell() + " must be " + CELL); - } if (fine.cell() != FINE_CELL) { throw new IllegalArgumentException("Window fine lattice cell " + fine.cell() + " must be " + FINE_CELL); } @@ -87,7 +83,7 @@ public final class WindowPlanner { int cellX = Math.multiplyExact(key.wx() - 1, WINDOW_CELLS); int cellZ = Math.multiplyExact(key.wz() - 1, WINDOW_CELLS); HydrologySampleBuffer samples = new HydrologySampleBuffer(SPAN * SPAN); - lattice.fill(cellX, cellZ, SPAN, SPAN, samples); + fine.fill(cellX * FINE_STRIDE, cellZ * FINE_STRIDE, SPAN, SPAN, FINE_STRIDE, samples); DemGrid raw = new DemGrid(cellX * CELL, cellZ * CELL, CELL, SPAN, SPAN); System.arraycopy(samples.height(), 0, raw.values(), 0, SPAN * SPAN); List lakes = regionLakes(plans.values()); diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/engine/FineLatticeSourceTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/engine/FineLatticeSourceTest.java deleted file mode 100644 index b22468d07..000000000 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/engine/FineLatticeSourceTest.java +++ /dev/null @@ -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; - } - } -} diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/engine/IrisHydrologyTerrainSourceTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/engine/IrisHydrologyTerrainSourceTest.java index 552d4bcbb..b411a91a2 100644 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/engine/IrisHydrologyTerrainSourceTest.java +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/engine/IrisHydrologyTerrainSourceTest.java @@ -154,10 +154,9 @@ public class IrisHydrologyTerrainSourceTest { HydrologySampleBuffer rect = new HydrologySampleBuffer(32 * 32); fine.fill(-600, 400, 32, 32, rect); assertMatchesScalar(reference, "fine rect", rect, nodes(-600, 400, 32, 1, 4), false); - HeightLattice window = new HeightLattice(new FineLatticeSource(fine, source), 8, false, 1 << 16); HydrologySampleBuffer strided = new HydrologySampleBuffer(16 * 16); - window.fill(-150, 300, 16, 16, 3, strided); - assertMatchesScalar(reference, "window strided", strided, nodes(-150, 300, 16, 3, 8), false); + fine.fill(-300, 600, 16, 16, 6, strided); + assertMatchesScalar(reference, "fine strided", strided, nodes(-300, 600, 16, 6, 4), false); HeightLattice coarse = new HeightLattice(source, 64, true, 1 << 12); HydrologySampleBuffer coarseBase = new HydrologySampleBuffer(16 * 16); coarse.fill(-8, -8, 16, 16, coarseBase); diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/grid/HeightLatticeTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/grid/HeightLatticeTest.java index 0016c469a..5e4da3c2d 100644 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/grid/HeightLatticeTest.java +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/grid/HeightLatticeTest.java @@ -287,6 +287,31 @@ public class HeightLatticeTest { assertThrows(IllegalArgumentException.class, () -> lattice.fill(cellXs, cellZs, new HydrologySampleBuffer(5))); } + @Test + public void sampledColumnsAreReadOnceAndMatchTheSource() { + CountingSource source = new CountingSource(); + HeightLattice lattice = new HeightLattice(source, 4, false, 1 << 16); + int[] xs = {5, -13, 5, 1027, -13, 6}; + int[] zs = {-2, 77, -2, 3, 77, -2}; + HydrologySampleBuffer first = new HydrologySampleBuffer(xs.length); + lattice.sampleColumns(xs, zs, first); + assertEquals(4, source.samples()); + HydrologySampleBuffer second = new HydrologySampleBuffer(xs.length); + lattice.sampleColumns(xs, zs, second); + assertEquals(4, source.samples()); + HydrologySampleBuffer expected = new HydrologySampleBuffer(xs.length); + new CountingSource().sample(xs, zs, expected, false); + for (int index = 0; index < xs.length; index++) { + assertEquals(Float.floatToIntBits(expected.height()[index]), Float.floatToIntBits(first.height()[index])); + assertEquals(Float.floatToIntBits(expected.height()[index]), Float.floatToIntBits(second.height()[index])); + assertEquals(expected.ocean()[index], second.ocean()[index]); + assertEquals(expected.permission()[index], second.permission()[index]); + assertEquals(expected.fluidProfile()[index], second.fluidProfile()[index]); + } + lattice.sampleColumns(new int[0], new int[0], new HydrologySampleBuffer(0)); + assertEquals(1, source.calls()); + } + private static void fillWindow(HeightLattice lattice, int wx, int wz, int cell, boolean baseOnly) { assertFill(lattice, cell, baseOnly, (wx - 1) * WINDOW, (wz - 1) * WINDOW, AREA, AREA); } diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/plan/HydrologyPlanCacheTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/plan/HydrologyPlanCacheTest.java index 040a6bfa4..63c7275ff 100644 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/plan/HydrologyPlanCacheTest.java +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/plan/HydrologyPlanCacheTest.java @@ -1045,7 +1045,7 @@ public class HydrologyPlanCacheTest { } private static WindowPlanner windowPlanner(HydrologySettings settings, SyntheticTerrain terrain) { - return new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.CELL, false, HydrologyPlanCache.WINDOW_LATTICE_CELLS), + return new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.FINE_CELL, false, HydrologyPlanCache.FINE_LATTICE_CELLS), new PondPlanner(settings), new ReachShaper(settings)); } diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/plan/HydrologyPlanCodecTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/plan/HydrologyPlanCodecTest.java index dba3ff4a1..274a2ffce 100644 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/plan/HydrologyPlanCodecTest.java +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/plan/HydrologyPlanCodecTest.java @@ -81,7 +81,7 @@ public class HydrologyPlanCodecTest { assertEquals(planned, HydrologyPlanCodec.decodeRegion(settings, HydrologyPlanCodec.encode(settings, planned)).orElseThrow()); regions.add(planned); } - WindowPlan planned = new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.CELL, false, 1 << 18), + WindowPlan planned = new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.FINE_CELL, false, 1 << 21), new PondPlanner(settings), new ReachShaper(settings)) .plan(LAND, regions); assertFalse(planned.reaches().isEmpty()); diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/plan/WindowPlannerTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/plan/WindowPlannerTest.java index a1829ff59..1efdfb02f 100644 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/plan/WindowPlannerTest.java +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/plan/WindowPlannerTest.java @@ -83,7 +83,7 @@ public class WindowPlannerTest { new int[]{head, head}, new float[]{2f, 2f}, new float[]{2f, 2f}, new ReachStyle[]{ReachStyle.RUN, ReachStyle.RUN}, 1000f); return new ShapedNetwork(List.of(outlet), List.of(pond.withOutletReachId(outlet.id()))); }).when(shaper).shape(any(), any(), any(), any(), any(), any(), anyLong()); - WindowPlanner planner = new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.CELL, false, 1 << 18), + WindowPlanner planner = new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.FINE_CELL, false, 1 << 18), ponds, shaper); WindowComputation settled = planner.compute(ORIGIN, noRegions(ORIGIN)); @@ -587,7 +587,7 @@ public class WindowPlannerTest { } return network; }).when(shaper).shape(any(), any(), any(), any(), any(), any(), anyLong()); - return new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.CELL, false, 1 << 18), + return new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.FINE_CELL, false, 1 << 21), new PondPlanner(settings), shaper); } @@ -715,7 +715,7 @@ public class WindowPlannerTest { } private static WindowPlanner planner(HydrologySettings settings, SyntheticTerrain terrain) { - return new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.CELL, false, 1 << 18), + return new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.FINE_CELL, false, 1 << 21), new PondPlanner(settings), new ReachShaper(settings)); } diff --git a/probe/src/main/java/art/arcane/iris/probe/HydrologyPlanHarness.java b/probe/src/main/java/art/arcane/iris/probe/HydrologyPlanHarness.java index 89d776000..ddc37a008 100644 --- a/probe/src/main/java/art/arcane/iris/probe/HydrologyPlanHarness.java +++ b/probe/src/main/java/art/arcane/iris/probe/HydrologyPlanHarness.java @@ -195,7 +195,8 @@ final class HydrologyPlanHarness { double landTiles(WindowKey key) { int cells = WindowKey.SIZE / WindowPlanner.CELL; HydrologySampleBuffer samples = new HydrologySampleBuffer(cells * cells); - planners.windowLattice().fill(key.wx() * cells, key.wz() * cells, cells, cells, samples); + int stride = WindowPlanner.CELL / WindowPlanner.FINE_CELL; + planners.fineLattice().fill(key.wx() * cells * stride, key.wz() * cells * stride, cells, cells, stride, samples); int land = 0; for (int cell = 0; cell < cells * cells; cell++) { if (!samples.ocean()[cell]) {