Merged hydrology planning improvements

- Reuse downstream route distances and primitive footprint indexes
- Bound diagnostic history and reduce containment bookkeeping
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-06 03:27:31 -04:00
commit 587e0fdee0
7 files changed
+269 -85

No files matched your search

@@ -2646,7 +2646,7 @@ public final class HydrologyPlanner {
potential,
parent,
outletIndex,
routeLengths(grid, parent),
routeLengths(parent, grid.width(), settings.routing()),
List.copyOf(outlets),
false
);
@@ -3222,31 +3222,43 @@ public final class HydrologyPlanner {
return count + (HydrologyHash.unit(stable) < remainder ? 1 : 0);
}
private int[] routeLengths(SampledGrid grid, int[] parent) {
static int[] routeLengths(int[] parent, int width, HydrologyPlannerSettings.Routing routing) {
int[] lengths = new int[parent.length];
int[] pending = new int[parent.length];
double[] distances = new double[parent.length];
Arrays.fill(distances, Double.NaN);
double diagonalDistance = StrictMath.hypot(routing.sampleSpacing(), routing.sampleSpacing());
for (int sourceIndex = 0; sourceIndex < parent.length; sourceIndex++) {
lengths[sourceIndex] = routeLength(sourceIndex, grid, parent);
if (!Double.isNaN(distances[sourceIndex])) {
continue;
}
int current = sourceIndex;
int pendingCount = 0;
while (current >= 0 && Double.isNaN(distances[current])) {
pending[pendingCount++] = current;
distances[current] = -1D;
current = parent[current];
}
if (current >= 0 && distances[current] < 0D) {
throw new IllegalStateException("Hydrology drainage routing contains a cycle.");
}
double distance = current < 0 ? 0D : distances[current];
while (pendingCount > 0) {
int node = pending[--pendingCount];
int next = parent[node];
if (next >= 0) {
boolean cardinal = node / width == next / width || node % width == next % width;
distance += cardinal ? routing.sampleSpacing() : diagonalDistance;
}
distances[node] = distance;
lengths[node] = distance > routing.maximumRouteLength()
? Integer.MAX_VALUE
: (int) StrictMath.ceil(distance);
}
}
return lengths;
}
private int routeLength(int sourceIndex, SampledGrid grid, int[] parent) {
int current = sourceIndex;
int steps = 0;
double length = 0D;
while (parent[current] >= 0) {
int next = parent[current];
GridNode currentNode = grid.node(current);
GridNode nextNode = grid.node(next);
length += StrictMath.hypot(currentNode.x() - nextNode.x(), currentNode.z() - nextNode.z());
if (length > settings.routing().maximumRouteLength() || ++steps > settings.routing().maximumRouteNodes()) {
return Integer.MAX_VALUE;
}
current = next;
}
return (int) StrictMath.ceil(length);
}
private int[] countContributions(List<Integer> sources, RoutingPlan routing) {
int[] counts = new int[routing.parent().length];
for (int source : sources) {
@@ -3,46 +3,48 @@ package art.arcane.iris.engine.hydrology;
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
public final class RiverFootprint {
private static final RiverFootprint EMPTY = new RiverFootprint(Map.of());
private final Long2ObjectLinkedOpenHashMap<HydrologyColumnSample> indexed;
private final Map<Long, HydrologyColumnSample> columns;
public RiverFootprint(Map<Long, HydrologyColumnSample> columns) {
if (columns == null) {
throw new IllegalArgumentException("columns must not be null.");
}
TreeMap<Long, HydrologyColumnSample> ordered = new TreeMap<>();
indexed = new Long2ObjectLinkedOpenHashMap<>(columns.size());
for (Map.Entry<Long, HydrologyColumnSample> entry : columns.entrySet()) {
HydrologyColumnSample sample = entry.getValue();
if (sample == null || entry.getKey() != pack(sample.x(), sample.z())) {
throw new IllegalArgumentException("Footprint keys must match their column coordinates.");
}
ordered.put(entry.getKey(), sample);
indexed.put(entry.getKey().longValue(), sample);
}
Long2ObjectLinkedOpenHashMap<HydrologyColumnSample> indexed =
new Long2ObjectLinkedOpenHashMap<>(ordered.size());
for (Map.Entry<Long, HydrologyColumnSample> entry : ordered.entrySet()) {
indexed.put(entry.getKey().longValue(), entry.getValue());
long[] order = indexed.keySet().toLongArray();
Arrays.sort(order);
for (long key : order) {
indexed.getAndMoveToLast(key);
}
columns = Collections.unmodifiableMap(indexed);
this.columns = columns;
this.columns = Collections.unmodifiableMap(indexed);
}
public static RiverFootprint empty() {
return new RiverFootprint(Map.of());
return EMPTY;
}
public Optional<HydrologyColumnSample> sample(int x, int z) {
return Optional.ofNullable(columns.get(pack(x, z)));
return Optional.ofNullable(indexed.get(pack(x, z)));
}
public HydrologyRenderSample renderSample(int x, int z) {
HydrologyColumnSample sample = columns.get(pack(x, z));
HydrologyColumnSample sample = indexed.get(pack(x, z));
return sample == null ? new HydrologyRenderSample(x, z, List.of()) : sample.renderSample();
}
@@ -61,7 +63,7 @@ public final class RiverFootprint {
if (area < columns.size()) {
for (long z = minimumZ; z < maximumZ; z++) {
for (long x = minimumX; x < maximumX; x++) {
HydrologyColumnSample sample = columns.get(pack((int) x, (int) z));
HydrologyColumnSample sample = indexed.get(pack((int) x, (int) z));
if (sample != null) {
selected.add(sample);
}
@@ -9,7 +9,6 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
@@ -1132,8 +1131,8 @@ public final class HydrologyCaveContainmentPlanner {
List<CavePosition> throat
) {
Queue<CavePosition> queue = new ArrayDeque<>();
Set<CavePosition> queued = new HashSet<>();
Set<CavePosition> resolved = new LinkedHashSet<>();
Set<CavePosition> queued = new LinkedHashSet<>();
int visitedCount = 0;
queue.add(source.target());
queued.add(source.target());
@@ -1148,10 +1147,7 @@ public final class HydrologyCaveContainmentPlanner {
if (positionRejection != HydrologyCaveRejection.NONE) {
return ComponentResult.rejected(positionRejection);
}
if (!resolved.add(position)) {
continue;
}
if (resolved.size() > settings.maxFloodVolume()) {
if (++visitedCount > settings.maxFloodVolume()) {
return ComponentResult.rejected(HydrologyCaveRejection.VOLUME_LIMIT);
}
@@ -1171,7 +1167,7 @@ public final class HydrologyCaveContainmentPlanner {
}
}
return ComponentResult.accepted(resolved);
return ComponentResult.accepted(queued);
}
private HydrologyCaveRejection addThroatContacts(
@@ -58,6 +58,8 @@ import art.arcane.iris.util.common.data.DataProvider;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.volmlib.util.math.RNG;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.ArrayList;
import java.util.Collections;
@@ -70,7 +72,6 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.nio.file.Path;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.IntConsumer;
import java.util.function.Predicate;
@@ -96,7 +97,9 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
private final Set<String> profileKeys;
private final String defaultProfileKey;
private final Object terrainSampleLock;
private final Set<HydrologyTileKey> unplannedQueries = ConcurrentHashMap.newKeySet();
private final Cache<HydrologyTileKey, Boolean> unplannedQueries = Caffeine.newBuilder()
.maximumSize(MAXIMUM_CACHE_TILES)
.build();
private final LinkedHashMap<Long, HydrologyTerrainSample> terrainSamples;
public IrisHydrologyRuntime(IrisHydrologyRuntimeContext context) {
@@ -156,7 +159,7 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
if (!planned) {
int tileSize = settings.routing().tileSize();
HydrologyTileKey key = new HydrologyTileKey(Math.floorDiv(blockX, tileSize), Math.floorDiv(blockZ, tileSize));
if (unplannedQueries.add(key)) {
if (unplannedQueries.asMap().putIfAbsent(key, Boolean.TRUE) == null) {
IrisLogging.debug("Hydrology tile %d,%d queried before it was planned; natural terrain answers until the plan lands",
key.tileX(), key.tileZ());
}
@@ -479,6 +482,7 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
@Override
public void close() {
cache.close();
unplannedQueries.invalidateAll();
routingTerrainSampler.close();
synchronized (terrainSampleLock) {
terrainSamples.clear();
@@ -91,11 +91,12 @@ public final class SurfaceFootprintCompiler {
&& column.x() == centerline.x()[0] && column.z() == centerline.z()[0];
int y = column.role() == SurfaceRole.CHANNEL ? column.headY() : column.height();
HydrologyFeatureRef feature = features.feature(segment, column.role(), source, column.x(), y, column.z(), flowX, flowZ);
String biomeOverride = pool ? poolBiome == null ? column.terrain().parentBiomeKey() : poolBiome : null;
columns.add(new SurfaceLayerColumn(
column.x(),
column.z(),
column.terrain(),
pool ? poolLayer(feature, column, course.profileKey(), poolBiome) : layer(feature, column, course.profileKey()),
layer(feature, column, course.profileKey(), biomeOverride),
column.role(),
column.apron()
));
@@ -103,40 +104,6 @@ public final class SurfaceFootprintCompiler {
return new SurfaceFootprint(columns, field.uncontainedWetCells());
}
/** A pool's bowl and rim carry the pool biome when one is configured, otherwise the surrounding biome. */
private static HydrologyColumnLayer poolLayer(
HydrologyFeatureRef feature,
SurfaceColumn column,
String profileKey,
String poolBiome
) {
HydrologyTerrainSample terrain = column.terrain();
String biome = poolBiome == null ? terrain.parentBiomeKey() : poolBiome;
boolean channel = column.role() == SurfaceRole.CHANNEL;
boolean shore = column.role() == SurfaceRole.SHORE;
return new HydrologyColumnLayer(
feature,
column.height(),
column.headY(),
column.headY(),
channel,
shore,
!channel,
channel,
false,
false,
true,
channel,
false,
profileKey,
biome,
biome,
biome,
biome,
terrain.floodedCaveBiomeKey()
);
}
private ChannelProfile poolProfile(HydraulicSegment segment, SurfaceCenterline centerline) {
int count = centerline.size();
double[] halfWidth = new double[count];
@@ -160,9 +127,15 @@ public final class SurfaceFootprintCompiler {
return null;
}
private static HydrologyColumnLayer layer(HydrologyFeatureRef feature, SurfaceColumn column, String profileKey) {
/** A pool's bowl and rim carry the pool biome when one is configured, otherwise the surrounding biome. */
private static HydrologyColumnLayer layer(
HydrologyFeatureRef feature,
SurfaceColumn column,
String profileKey,
String biomeOverride
) {
HydrologyTerrainSample terrain = column.terrain();
if (column.apron()) {
if (column.apron() && biomeOverride == null) {
return new HydrologyColumnLayer(
feature,
column.headY(),
@@ -194,10 +167,10 @@ public final class SurfaceFootprintCompiler {
channel,
false,
profileKey,
terrain.surfaceBiomeKey(),
terrain.mouthBiomeKey(),
terrain.shoreBiomeKey(),
terrain.bankBiomeKey(),
biomeOverride == null ? terrain.surfaceBiomeKey() : biomeOverride,
biomeOverride == null ? terrain.mouthBiomeKey() : biomeOverride,
biomeOverride == null ? terrain.shoreBiomeKey() : biomeOverride,
biomeOverride == null ? terrain.bankBiomeKey() : biomeOverride,
terrain.floodedCaveBiomeKey()
);
}