Merge branch 'perf-raster' into perf-review

This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-30 21:29:13 -04:00
commit e9c18cdc08
13 files changed
+463 -54

No files matched your search

@@ -57,7 +57,7 @@ import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
public final class HydrologyRuntime { public final class HydrologyRuntime {
private static final int RASTER_CAPACITY = 1024; private static final int RASTER_CAPACITY = 8192;
private static final int KEEP_SCOPES = 2; private static final int KEEP_SCOPES = 2;
private static final int CHUNKS_PER_WINDOW = WindowKey.SIZE >> 4; private static final int CHUNKS_PER_WINDOW = WindowKey.SIZE >> 4;
public static final int MAX_PREGENERATION_REGIONS = 4096; public static final int MAX_PREGENERATION_REGIONS = 4096;
@@ -73,6 +73,7 @@ public final class HydrologyRuntime {
private final RasterLru rasters; private final RasterLru rasters;
private final ThreadLocal<LastRaster> last; private final ThreadLocal<LastRaster> last;
private final Set<Long> queued; private final Set<Long> queued;
private final ConcurrentHashMap<Long, CompletableFuture<ChunkWaterRaster>> building;
private final RasterFailures failures; private final RasterFailures failures;
private final RasterFailures windowPreviewFailures; private final RasterFailures windowPreviewFailures;
private final RasterFailures regionPreviewFailures; private final RasterFailures regionPreviewFailures;
@@ -96,6 +97,7 @@ public final class HydrologyRuntime {
this.rasters = null; this.rasters = null;
this.last = null; this.last = null;
this.queued = null; this.queued = null;
this.building = null;
this.failures = null; this.failures = null;
this.windowPreviewFailures = null; this.windowPreviewFailures = null;
this.regionPreviewFailures = null; this.regionPreviewFailures = null;
@@ -123,6 +125,7 @@ public final class HydrologyRuntime {
this.rasters = new RasterLru(); this.rasters = new RasterLru();
this.last = ThreadLocal.withInitial(LastRaster::new); this.last = ThreadLocal.withInitial(LastRaster::new);
this.queued = ConcurrentHashMap.newKeySet(); this.queued = ConcurrentHashMap.newKeySet();
this.building = new ConcurrentHashMap<>();
this.failures = new RasterFailures(ticker); this.failures = new RasterFailures(ticker);
this.windowPreviewFailures = new RasterFailures(ticker); this.windowPreviewFailures = new RasterFailures(ticker);
this.regionPreviewFailures = new RasterFailures(ticker); this.regionPreviewFailures = new RasterFailures(ticker);
@@ -238,7 +241,7 @@ public final class HydrologyRuntime {
requireOpen(); requireOpen();
long key = key(chunkX, chunkZ); long key = key(chunkX, chunkZ);
ChunkWaterRaster known = remembered(key, slot); ChunkWaterRaster known = remembered(key, slot);
return known != null ? known : remember(key, planned(chunkX, chunkZ, key)); return known != null ? known : build(chunkX, chunkZ, key, slot);
} }
public ChunkWaterRaster rasterSnapshot(int chunkX, int chunkZ) { public ChunkWaterRaster rasterSnapshot(int chunkX, int chunkZ) {
@@ -525,6 +528,41 @@ public final class HydrologyRuntime {
} }
} }
private ChunkWaterRaster build(int chunkX, int chunkZ, long key, LastRaster slot) {
CompletableFuture<ChunkWaterRaster> claim = new CompletableFuture<>();
CompletableFuture<ChunkWaterRaster> running = building.putIfAbsent(key, claim);
if (running != null) {
ChunkWaterRaster built = built(running);
slot.remember(key, built);
return built;
}
try {
ChunkWaterRaster known = remembered(key, slot);
ChunkWaterRaster raster = known != null ? known : remember(key, planned(chunkX, chunkZ, key));
claim.complete(raster);
return raster;
} catch (RuntimeException | Error failure) {
claim.completeExceptionally(failure);
throw failure;
} finally {
building.remove(key, claim);
}
}
private static ChunkWaterRaster built(CompletableFuture<ChunkWaterRaster> running) {
try {
return running.join();
} catch (CompletionException failure) {
if (failure.getCause() instanceof RuntimeException cause) {
throw cause;
}
if (failure.getCause() instanceof Error cause) {
throw cause;
}
throw failure;
}
}
private ChunkWaterRaster planned(int chunkX, int chunkZ, long chunk) { private ChunkWaterRaster planned(int chunkX, int chunkZ, long chunk) {
try { try {
List<WindowKey> windowKeys = neighbourhood(chunkX, chunkZ); List<WindowKey> windowKeys = neighbourhood(chunkX, chunkZ);
@@ -12,6 +12,7 @@ import art.arcane.iris.generation.hydrology.water.PondMask;
import art.arcane.iris.generation.hydrology.water.Reach; import art.arcane.iris.generation.hydrology.water.Reach;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@@ -28,17 +29,20 @@ final class ChunkFeatures {
private final List<Pond> ponds; private final List<Pond> ponds;
private final List<Pond> lakes; private final List<Pond> lakes;
private final List<SeaInlet> inlets; private final List<SeaInlet> inlets;
private final int[] inletBoxes;
private final int[] lakeBoxes; private final int[] lakeBoxes;
private final ReachSegments segments = new ReachSegments(SEGMENT_CAPACITY); private final ReachSegments segments = new ReachSegments(SEGMENT_CAPACITY);
private final MaskEdges pondShores = new MaskEdges(MaskEdges.Kind.WATER); private final MaskEdges pondShores = new MaskEdges(MaskEdges.Kind.WATER);
private final MaskEdges pondBanks = new MaskEdges(MaskEdges.Kind.BANK); private final MaskEdges pondBanks = new MaskEdges(MaskEdges.Kind.BANK);
private final MaskEdges lakeReach = new MaskEdges(MaskEdges.Kind.GROWN_WATER); private final MaskEdges lakeReach = new MaskEdges(MaskEdges.Kind.GROWN_WATER);
private boolean inert;
private ChunkFeatures(List<Reach> reaches, List<Pond> ponds, List<Pond> lakes, List<SeaInlet> inlets) { private ChunkFeatures(List<Reach> reaches, List<Pond> ponds, List<Pond> lakes, List<SeaInlet> inlets, int[] inletBoxes) {
this.reaches = reaches; this.reaches = reaches;
this.ponds = ponds; this.ponds = ponds;
this.lakes = lakes; this.lakes = lakes;
this.inlets = inlets; this.inlets = inlets;
this.inletBoxes = inletBoxes;
this.lakeBoxes = new int[4 * lakes.size()]; this.lakeBoxes = new int[4 * lakes.size()];
} }
@@ -48,12 +52,15 @@ final class ChunkFeatures {
List<Reach> reaches = new ArrayList<>(); List<Reach> reaches = new ArrayList<>();
List<Pond> ponds = new ArrayList<>(); List<Pond> ponds = new ArrayList<>();
List<SeaInlet> inlets = new ArrayList<>(); List<SeaInlet> inlets = new ArrayList<>();
int[] inletBoxes = new int[0];
for (IndexedWindow window : windows) { for (IndexedWindow window : windows) {
for (SeaInlet inlet : window.plan().inlets()) { for (SeaInlet inlet : window.plan().inlets()) {
int[] bounds = SeaInletRasterizer.bounds(inlet); int[] bounds = SeaInletRasterizer.bounds(inlet);
int padding = RiffleProfile.RADIUS + 1; int padding = RiffleProfile.RADIUS + 1;
if (bounds[0] <= chunkX * CHUNK + CHUNK - 1 + padding && bounds[2] >= chunkX * CHUNK - padding if (bounds[0] <= chunkX * CHUNK + CHUNK - 1 + padding && bounds[2] >= chunkX * CHUNK - padding
&& bounds[1] <= chunkZ * CHUNK + CHUNK - 1 + padding && bounds[3] >= chunkZ * CHUNK - padding) { && bounds[1] <= chunkZ * CHUNK + CHUNK - 1 + padding && bounds[3] >= chunkZ * CHUNK - padding) {
inletBoxes = Arrays.copyOf(inletBoxes, inletBoxes.length + 4);
System.arraycopy(bounds, 0, inletBoxes, 4 * inlets.size(), 4);
inlets.add(inlet); inlets.add(inlet);
} }
} }
@@ -64,17 +71,22 @@ final class ChunkFeatures {
} }
} }
List<Pond> lakes = lakes(chunkX, chunkZ, halo, lakeRegionPlans, lakeRegions); List<Pond> lakes = lakes(chunkX, chunkZ, halo, lakeRegionPlans, lakeRegions);
ChunkFeatures features = new ChunkFeatures(reaches, ponds, lakes, inlets); ChunkFeatures features = new ChunkFeatures(reaches, ponds, lakes, inlets, inletBoxes);
features.index(settings, chunkX, chunkZ); features.index(settings, chunkX, chunkZ);
return features; return features;
} }
boolean empty() { boolean inert() {
return reaches.isEmpty() && ponds.isEmpty() && lakes.isEmpty() && inlets.isEmpty(); return inert;
} }
SeaInlet openInletAt(int x, int z, int[] column) { SeaInlet openInletAt(int x, int z, int[] column) {
for (SeaInlet inlet : inlets) { for (int index = 0; index < inlets.size(); index++) {
int box = 4 * index;
if (x < inletBoxes[box] || z < inletBoxes[box + 1] || x > inletBoxes[box + 2] || z > inletBoxes[box + 3]) {
continue;
}
SeaInlet inlet = inlets.get(index);
if (SeaInletRasterizer.column(inlet, x, z, column) && column[1] == SeaInlet.OPEN) { if (SeaInletRasterizer.column(inlet, x, z, column) && column[1] == SeaInlet.OPEN) {
return inlet; return inlet;
} }
@@ -179,6 +191,8 @@ final class ChunkFeatures {
for (Reach reach : reaches) { for (Reach reach : reaches) {
segments.addNear(reach, minX - 1f, minZ - 1f, maxX + 1f, maxZ + 1f, outward); segments.addNear(reach, minX - 1f, minZ - 1f, maxX + 1f, maxZ + 1f, outward);
} }
int riffle = RiffleProfile.RADIUS + 1;
segments.coverChannels(minX - riffle, minZ - riffle, maxX + riffle, maxZ + riffle);
HydrologySettings.PondShore shore = settings.ponds().shore(); HydrologySettings.PondShore shore = settings.ponds().shore();
int reach = Math.max(shore.taper(), ChunkWaterRaster.MAX_SHORE_DISTANCE) + 1; int reach = Math.max(shore.taper(), ChunkWaterRaster.MAX_SHORE_DISTANCE) + 1;
int shelf = shore.shelf() + 1; int shelf = shore.shelf() + 1;
@@ -192,6 +206,39 @@ final class ChunkFeatures {
lakeReach.collect(mask, minX - shoreReach, minZ - shoreReach, maxX + shoreReach, maxZ + shoreReach); lakeReach.collect(mask, minX - shoreReach, minZ - shoreReach, maxX + shoreReach, maxZ + shoreReach);
System.arraycopy(grownBox(mask), 0, lakeBoxes, 4 * index, 4); System.arraycopy(grownBox(mask), 0, lakeBoxes, 4 * index, 4);
} }
inert = segments.isEmpty() && !inletsTouch(minX - 1, minZ - 1, maxX + 1, maxZ + 1)
&& !pondsTouch(minX - 1 - reach, minZ - 1 - reach, maxX + 1 + reach, maxZ + 1 + reach)
&& !lakesTouch(minX - 2 - shoreReach, minZ - 2 - shoreReach, maxX + 2 + shoreReach, maxZ + 2 + shoreReach);
}
private boolean inletsTouch(int minX, int minZ, int maxX, int maxZ) {
for (int box = 0; box < inletBoxes.length; box += 4) {
if (inletBoxes[box] <= maxX && inletBoxes[box + 2] >= minX && inletBoxes[box + 1] <= maxZ && inletBoxes[box + 3] >= minZ) {
return true;
}
}
return false;
}
private boolean pondsTouch(int minX, int minZ, int maxX, int maxZ) {
for (Pond pond : ponds) {
PondMask mask = pond.mask();
int fromX = mask.originX() - (mask.step() >> 1);
int fromZ = mask.originZ() - (mask.step() >> 1);
if (fromX <= maxX && fromX + mask.width() * mask.step() > minX && fromZ <= maxZ && fromZ + mask.depth() * mask.step() > minZ) {
return true;
}
}
return false;
}
private boolean lakesTouch(int minX, int minZ, int maxX, int maxZ) {
for (int box = 0; box < lakeBoxes.length; box += 4) {
if (lakeBoxes[box] <= maxX && lakeBoxes[box + 2] > minX && lakeBoxes[box + 1] <= maxZ && lakeBoxes[box + 3] > minZ) {
return true;
}
}
return false;
} }
private static Map<WindowKey, IndexedWindow> windowsByKey(int chunkX, int chunkZ, List<IndexedWindow> windows) { private static Map<WindowKey, IndexedWindow> windowsByKey(int chunkX, int chunkZ, List<IndexedWindow> windows) {
@@ -37,7 +37,7 @@ public final class ChunkRasterizer {
List<RegionPlan> lakeRegionPlans) { List<RegionPlan> lakeRegionPlans) {
Objects.requireNonNull(naturalHeight, "naturalHeight"); Objects.requireNonNull(naturalHeight, "naturalHeight");
ChunkFeatures features = ChunkFeatures.gather(settings, chunkX, chunkZ, halo, windows, lakeRegionPlans, lakeRegions(chunkX, chunkZ)); ChunkFeatures features = ChunkFeatures.gather(settings, chunkX, chunkZ, halo, windows, lakeRegionPlans, lakeRegions(chunkX, chunkZ));
if (features.empty()) { if (features.inert()) {
return ChunkWaterRaster.EMPTY; return ChunkWaterRaster.EMPTY;
} }
return new RasterComposer(settings, features, chunkX, chunkZ).compose(naturalHeight); return new RasterComposer(settings, features, chunkX, chunkZ).compose(naturalHeight);
@@ -2,6 +2,8 @@ package art.arcane.iris.generation.hydrology.raster;
import art.arcane.iris.generation.hydrology.water.HydrologyTier; import art.arcane.iris.generation.hydrology.water.HydrologyTier;
import java.util.Arrays;
public final class ChunkWaterRaster { public final class ChunkWaterRaster {
public static final int MAX_SHORE_DISTANCE = 32; public static final int MAX_SHORE_DISTANCE = 32;
public static final ChunkWaterRaster PROVISIONAL = new ChunkWaterRaster(true); public static final ChunkWaterRaster PROVISIONAL = new ChunkWaterRaster(true);
@@ -13,10 +15,13 @@ public final class ChunkWaterRaster {
private final int[] terrain; private final int[] terrain;
private final int[] head; private final int[] head;
private final byte[] role; private final byte[] role;
private final HydrologyTier[] tier; private final byte[] tier;
private final long[] featureId; private final HydrologyTier[] tiers;
private final String[] fluid; private final byte[] feature;
private final int[] caveCeiling; private final long[] features;
private final byte[] fluid;
private final String[] fluids;
private final boolean caveMask;
private final byte[] shoreDistance; private final byte[] shoreDistance;
private ChunkWaterRaster(boolean provisional) { private ChunkWaterRaster(boolean provisional) {
@@ -25,26 +30,32 @@ public final class ChunkWaterRaster {
this.head = null; this.head = null;
this.role = null; this.role = null;
this.tier = null; this.tier = null;
this.featureId = null; this.tiers = null;
this.feature = null;
this.features = null;
this.fluid = null; this.fluid = null;
this.caveCeiling = null; this.fluids = null;
this.caveMask = false;
this.shoreDistance = null; this.shoreDistance = null;
} }
ChunkWaterRaster(int[] terrain, int[] head, byte[] role, HydrologyTier[] tier, long[] featureId, String[] fluid, int[] caveCeiling, ChunkWaterRaster(int[] terrain, int[] head, byte[] role, Palette<HydrologyTier> tiers, LongPalette features, Palette<String> fluids,
byte[] shoreDistance) { boolean caveMask, byte[] shoreDistance) {
if (terrain.length != COLUMNS || head.length != COLUMNS || role.length != COLUMNS || tier.length != COLUMNS if (terrain.length != COLUMNS || head.length != COLUMNS || role.length != COLUMNS || tiers.indices.length != COLUMNS
|| featureId.length != COLUMNS || fluid.length != COLUMNS || caveCeiling.length != COLUMNS || shoreDistance.length != COLUMNS) { || features.indices.length != COLUMNS || fluids.indices.length != COLUMNS || shoreDistance.length != COLUMNS) {
throw new IllegalArgumentException("A chunk water raster needs " + COLUMNS + " entries per array"); throw new IllegalArgumentException("A chunk water raster needs " + COLUMNS + " entries per array");
} }
this.provisional = false; this.provisional = false;
this.terrain = terrain; this.terrain = terrain;
this.head = head; this.head = head;
this.role = role; this.role = role;
this.tier = tier; this.tier = tiers.indices;
this.featureId = featureId; this.tiers = tiers.values();
this.fluid = fluid; this.feature = features.indices;
this.caveCeiling = caveCeiling; this.features = features.values();
this.fluid = fluids.indices;
this.fluids = fluids.values();
this.caveMask = caveMask;
this.shoreDistance = shoreDistance; this.shoreDistance = shoreDistance;
} }
@@ -68,22 +79,22 @@ public final class ChunkWaterRaster {
public HydrologyTier tier(int lx, int lz) { public HydrologyTier tier(int lx, int lz) {
int index = index(lx, lz); int index = index(lx, lz);
return tier == null ? null : tier[index]; return tier == null ? null : tiers[tier[index] & 0xFF];
} }
public long featureId(int lx, int lz) { public long featureId(int lx, int lz) {
int index = index(lx, lz); int index = index(lx, lz);
return featureId == null ? 0L : featureId[index]; return feature == null ? 0L : features[feature[index] & 0xFF];
} }
public String fluid(int lx, int lz) { public String fluid(int lx, int lz) {
int index = index(lx, lz); int index = index(lx, lz);
return fluid == null ? null : fluid[index]; return fluid == null ? null : fluids[fluid[index] & 0xFF];
} }
public int caveCeiling(int lx, int lz) { public int caveCeiling(int lx, int lz) {
int index = index(lx, lz); int index = index(lx, lz);
return caveCeiling == null ? Integer.MAX_VALUE : caveCeiling[index]; return caveMask && head[index] != Integer.MIN_VALUE ? terrain[index] - 1 : Integer.MAX_VALUE;
} }
public int shoreDistance(int lx, int lz) { public int shoreDistance(int lx, int lz) {
@@ -105,4 +116,53 @@ public final class ChunkWaterRaster {
} }
return lz << 4 | lx; return lz << 4 | lx;
} }
static final class Palette<T> {
private final byte[] indices = new byte[COLUMNS];
private final T[] seen;
private int size;
Palette(T[] seen) {
if (seen.length != COLUMNS) {
throw new IllegalArgumentException("A raster palette needs room for " + COLUMNS + " values");
}
this.seen = seen;
}
void set(int column, T value) {
int found = 0;
while (found < size && seen[found] != value) {
found++;
}
if (found == size) {
seen[size++] = value;
}
indices[column] = (byte) found;
}
private T[] values() {
return Arrays.copyOf(seen, size);
}
}
static final class LongPalette {
private final byte[] indices = new byte[COLUMNS];
private final long[] seen = new long[COLUMNS];
private int size;
void set(int column, long value) {
int found = 0;
while (found < size && seen[found] != value) {
found++;
}
if (found == size) {
seen[size++] = value;
}
indices[column] = (byte) found;
}
private long[] values() {
return Arrays.copyOf(seen, size);
}
}
} }
@@ -385,34 +385,37 @@ final class RasterComposer {
private ChunkWaterRaster pack() { private ChunkWaterRaster pack() {
int columns = ChunkWaterRaster.COLUMNS; int columns = ChunkWaterRaster.COLUMNS;
boolean affected = false;
for (int lz = 0; lz < CHUNK && !affected; lz++) {
for (int lx = 0; lx < CHUNK && !affected; lx++) {
int cell = (lz + 1) * GRID + lx + 1;
affected = role[cell] != WaterRole.NONE || terrain[cell] != natural[cell] || shore(cell) < ChunkWaterRaster.MAX_SHORE_DISTANCE;
}
}
if (!affected) {
return ChunkWaterRaster.EMPTY;
}
int[] terrainOut = new int[columns]; int[] terrainOut = new int[columns];
int[] headOut = new int[columns]; int[] headOut = new int[columns];
byte[] roleOut = new byte[columns]; byte[] roleOut = new byte[columns];
HydrologyTier[] tierOut = new HydrologyTier[columns]; ChunkWaterRaster.Palette<HydrologyTier> tierOut = new ChunkWaterRaster.Palette<>(new HydrologyTier[columns]);
long[] featureOut = new long[columns]; ChunkWaterRaster.LongPalette featureOut = new ChunkWaterRaster.LongPalette();
String[] fluidOut = new String[columns]; ChunkWaterRaster.Palette<String> fluidOut = new ChunkWaterRaster.Palette<>(new String[columns]);
int[] caveOut = new int[columns];
byte[] shoreOut = new byte[columns]; byte[] shoreOut = new byte[columns];
boolean caveMask = settings.deformation().caveMask();
boolean affected = false;
for (int lz = 0; lz < CHUNK; lz++) { for (int lz = 0; lz < CHUNK; lz++) {
for (int lx = 0; lx < CHUNK; lx++) { for (int lx = 0; lx < CHUNK; lx++) {
int cell = (lz + 1) * GRID + lx + 1; int cell = (lz + 1) * GRID + lx + 1;
int index = lz << 4 | lx; int index = lz << 4 | lx;
int shore = shore(cell);
terrainOut[index] = terrain[cell]; terrainOut[index] = terrain[cell];
headOut[index] = head[cell]; headOut[index] = head[cell];
roleOut[index] = (byte) role[cell].ordinal(); roleOut[index] = (byte) role[cell].ordinal();
tierOut[index] = tier[cell]; tierOut.set(index, tier[cell]);
featureOut[index] = feature[cell]; featureOut.set(index, feature[cell]);
fluidOut[index] = fluid[cell]; fluidOut.set(index, fluid[cell]);
caveOut[index] = caveMask && head[cell] != Integer.MIN_VALUE ? terrain[cell] - 1 : Integer.MAX_VALUE; shoreOut[index] = (byte) shore(cell);
shoreOut[index] = (byte) shore;
affected |= role[cell] != WaterRole.NONE || terrain[cell] != natural[cell] || shore < ChunkWaterRaster.MAX_SHORE_DISTANCE;
} }
} }
return affected ? new ChunkWaterRaster(terrainOut, headOut, roleOut, tierOut, featureOut, fluidOut, caveOut, shoreOut) return new ChunkWaterRaster(terrainOut, headOut, roleOut, tierOut, featureOut, fluidOut, settings.deformation().caveMask(), shoreOut);
: ChunkWaterRaster.EMPTY;
} }
private int shore(int cell) { private int shore(int cell) {
@@ -6,6 +6,8 @@ import java.util.Arrays;
final class ReachSegments { final class ReachSegments {
private static final int RUN = 16; private static final int RUN = 16;
private static final int COVER_SHIFT = 2;
private static final float COVER_MARGIN = 1f;
private Reach[] reaches; private Reach[] reaches;
private int[] segments; private int[] segments;
@@ -24,6 +26,13 @@ final class ReachSegments {
private int[] runBands = new int[8]; private int[] runBands = new int[8];
private int runs; private int runs;
private boolean sealed; private boolean sealed;
private boolean covered;
private int coverX;
private int coverZ;
private int coverWidth;
private int coverDepth;
private int[] coverStarts;
private int[] coverEntries;
ReachSegments(int capacity) { ReachSegments(int capacity) {
int initial = Math.max(1, capacity); int initial = Math.max(1, capacity);
@@ -71,6 +80,19 @@ final class ReachSegments {
} }
} }
boolean isEmpty() {
return size == 0;
}
void coverChannels(int minX, int minZ, int maxX, int maxZ) {
covered = true;
coverX = minX;
coverZ = minZ;
coverWidth = ((maxX - minX) >> COVER_SHIFT) + 1;
coverDepth = ((maxZ - minZ) >> COVER_SHIFT) + 1;
sealed = false;
}
void nearest(double x, double z, Scope scope, ReachCursor cursor) { void nearest(double x, double z, Scope scope, ReachCursor cursor) {
if (!sealed) { if (!sealed) {
seal(); seal();
@@ -89,6 +111,16 @@ final class ReachSegments {
hint(channelHint, x, z, unbounded, cursor); hint(channelHint, x, z, unbounded, cursor);
hint(bankHint, x, z, unbounded, cursor); hint(bankHint, x, z, unbounded, cursor);
hint(edgeHint, x, z, unbounded, cursor); hint(edgeHint, x, z, unbounded, cursor);
int cell = scope == Scope.CHANNEL ? coveredCell(x, z) : -1;
if (cell >= 0) {
for (int entry = coverStarts[cell]; entry < coverStarts[cell + 1]; entry++) {
int index = coverEntries[entry];
if (index != channelHint && index != bankHint && index != edgeHint && !beyondSegment(index, x, z, scope, cursor)) {
evaluate(index, x, z, false, cursor);
}
}
return;
}
int first = 0; int first = 0;
for (int run = 0; run < runs; run++) { for (int run = 0; run < runs; run++) {
int end = runEnds[run]; int end = runEnds[run];
@@ -99,9 +131,7 @@ final class ReachSegments {
continue; continue;
} }
for (int index = first; index < end; index++) { for (int index = first; index < end; index++) {
if (!beyond(x, z, Math.min(startX[index], endX[index]), Math.min(startZ[index], endZ[index]), Math.max(startX[index], endX[index]), if (!beyondSegment(index, x, z, scope, cursor) && index != channelHint && index != bankHint && index != edgeHint) {
Math.max(startZ[index], endZ[index]), limit(radius[index], bands[index], scope, cursor))
&& index != channelHint && index != bankHint && index != edgeHint) {
evaluate(index, x, z, unbounded, cursor); evaluate(index, x, z, unbounded, cursor);
} }
} }
@@ -109,6 +139,23 @@ final class ReachSegments {
} }
} }
private int coveredCell(double x, double z) {
if (coverStarts == null) {
return -1;
}
int cellX = ((int) Math.floor(x) - coverX) >> COVER_SHIFT;
int cellZ = ((int) Math.floor(z) - coverZ) >> COVER_SHIFT;
if (cellX < 0 || cellZ < 0 || cellX >= coverWidth || cellZ >= coverDepth) {
return -1;
}
return cellZ * coverWidth + cellX;
}
private boolean beyondSegment(int index, double x, double z, Scope scope, ReachCursor cursor) {
return beyond(x, z, Math.min(startX[index], endX[index]), Math.min(startZ[index], endZ[index]), Math.max(startX[index], endX[index]),
Math.max(startZ[index], endZ[index]), limit(radius[index], bands[index], scope, cursor));
}
private void hint(int index, double x, double z, boolean unbounded, ReachCursor cursor) { private void hint(int index, double x, double z, boolean unbounded, ReachCursor cursor) {
if (index >= 0 && index < size) { if (index >= 0 && index < size) {
evaluate(index, x, z, unbounded, cursor); evaluate(index, x, z, unbounded, cursor);
@@ -171,9 +218,56 @@ final class ReachSegments {
addRun(start, end); addRun(start, end);
start = end; start = end;
} }
if (covered) {
cover();
}
sealed = true; sealed = true;
} }
private void cover() {
int cells = coverWidth * coverDepth;
int[] starts = new int[cells + 1];
int[] range = new int[4];
for (int index = 0; index < size; index++) {
if (coverRange(index, range)) {
for (int cellZ = range[1]; cellZ <= range[3]; cellZ++) {
for (int cellX = range[0]; cellX <= range[2]; cellX++) {
starts[cellZ * coverWidth + cellX + 1]++;
}
}
}
}
for (int cell = 0; cell < cells; cell++) {
starts[cell + 1] += starts[cell];
}
int[] entries = new int[starts[cells]];
int[] fill = Arrays.copyOf(starts, cells);
for (int index = 0; index < size; index++) {
if (coverRange(index, range)) {
for (int cellZ = range[1]; cellZ <= range[3]; cellZ++) {
for (int cellX = range[0]; cellX <= range[2]; cellX++) {
entries[fill[cellZ * coverWidth + cellX]++] = index;
}
}
}
}
coverStarts = starts;
coverEntries = entries;
}
private boolean coverRange(int index, int[] range) {
float reach = radius[index] + COVER_MARGIN;
range[0] = Math.max(0, coverCell(Math.min(startX[index], endX[index]) - reach, coverX));
range[1] = Math.max(0, coverCell(Math.min(startZ[index], endZ[index]) - reach, coverZ));
range[2] = Math.min(coverWidth - 1, coverCell(Math.max(startX[index], endX[index]) + reach, coverX));
range[3] = Math.min(coverDepth - 1, coverCell(Math.max(startZ[index], endZ[index]) + reach, coverZ));
return range[0] <= range[2] && range[1] <= range[3];
}
private static int coverCell(float block, int origin) {
return ((int) Math.floor(block) - origin) >> COVER_SHIFT;
}
private void addRun(int start, int end) { private void addRun(int start, int end) {
if (runs == runEnds.length) { if (runs == runEnds.length) {
runEnds = Arrays.copyOf(runEnds, 2 * runs); runEnds = Arrays.copyOf(runEnds, 2 * runs);
@@ -15,6 +15,7 @@ final class RiffleProfile {
private static final int LIMIT = 10 * RADIUS; private static final int LIMIT = 10 * RADIUS;
private static final int DRY = Integer.MIN_VALUE; private static final int DRY = Integer.MIN_VALUE;
private static final int[] SIDES = {1, -1, GRID, -GRID}; private static final int[] SIDES = {1, -1, GRID, -GRID};
private static final int[] REMAINING = remaining();
private final List<Field> fields = new ArrayList<>(); private final List<Field> fields = new ArrayList<>();
@@ -85,6 +86,20 @@ final class RiffleProfile {
return (int) Math.max(original, Math.min(head - 1L, Math.round(raised))); return (int) Math.max(original, Math.min(head - 1L, Math.round(raised)));
} }
private static int[] remaining() {
int[] remaining = new int[GRID * GRID];
int low = PADDING - 1;
int high = PADDING + CHUNK;
for (int z = 0; z < GRID; z++) {
for (int x = 0; x < GRID; x++) {
int outsideX = Math.max(0, Math.max(low - x, x - high));
int outsideZ = Math.max(0, Math.max(low - z, z - high));
remaining[z * GRID + x] = 10 * Math.max(outsideX, outsideZ);
}
}
return remaining;
}
private record Field(int crest, int[] distances) { private record Field(int crest, int[] distances) {
} }
@@ -107,6 +122,9 @@ final class RiffleProfile {
} }
private void offer(int cell, int distance) { private void offer(int cell, int distance) {
if (distance + REMAINING[cell] >= LIMIT) {
return;
}
if (distances[cell] < LIMIT) { if (distances[cell] < LIMIT) {
remove(cell); remove(cell);
} }
@@ -10,6 +10,7 @@ import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import java.util.Arrays; import java.util.Arrays;
import java.util.Set; import java.util.Set;
import java.util.function.Supplier;
public final class CavePoolFiller { public final class CavePoolFiller {
public static final int REACH = HydrologySettings.MAX_CAVE_POOL_RADIUS + 1; public static final int REACH = HydrologySettings.MAX_CAVE_POOL_RADIUS + 1;
@@ -40,15 +41,19 @@ public final class CavePoolFiller {
this.density = settings.density(); this.density = settings.density();
} }
public void fill(int chunkX, int chunkZ, CarvedTerrain carved, MantleWriter writer, long seed) { public void fill(int chunkX, int chunkZ, Supplier<? extends CarvedTerrain> terrain, MantleWriter writer, long seed) {
if (!pools.enabled() || density == 0f) { if (!pools.enabled() || density == 0f) {
return; return;
} }
int attempts = attempts(seed, chunkX, chunkZ); int attempts = attempts(seed, chunkX, chunkZ);
CarvedTerrain carved = null;
for (int attempt = 0; attempt < attempts; attempt++) { for (int attempt = 0; attempt < attempts; attempt++) {
if (density < 1f && CellHash.unit(CellHash.mix(seed, SALT_DENSITY + attempt, chunkX, chunkZ)) >= density) { if (density < 1f && CellHash.unit(CellHash.mix(seed, SALT_DENSITY + attempt, chunkX, chunkZ)) >= density) {
continue; continue;
} }
if (carved == null) {
carved = terrain.get();
}
long column = CellHash.mix(seed, SALT_COLUMN + attempt, chunkX, chunkZ); long column = CellHash.mix(seed, SALT_COLUMN + attempt, chunkX, chunkZ);
int x = (chunkX << 4) + (int) (column & 15); int x = (chunkX << 4) + (int) (column & 15);
int z = (chunkZ << 4) + (int) ((column >>> 4) & 15); int z = (chunkZ << 4) + (int) ((column >>> 4) & 15);
@@ -85,8 +85,9 @@ public final class MantleHydrologyComponent extends IrisMantleComponent {
generateFalls(writer, complex, runtime, own, chunkX, chunkZ); generateFalls(writer, complex, runtime, own, chunkX, chunkZ);
if (poolsActive()) { if (poolsActive()) {
HydrologySettings settings = runtime.settings(); HydrologySettings settings = runtime.settings();
new CavePoolFiller(settings).fill(chunkX, chunkZ, Engine engine = getEngineMantle().getEngine();
new MantleCarvedTerrain(writer, complex, getEngineMantle().getEngine(), chunkX, chunkZ), writer, settings.seed()); new CavePoolFiller(settings).fill(chunkX, chunkZ, () -> new MantleCarvedTerrain(writer, complex, engine, chunkX, chunkZ), writer,
settings.seed());
} }
} }
@@ -216,6 +216,34 @@ public class HydrologyRuntimeTest {
assertEquals(planned + 1L, runtime.previewRevision()); assertEquals(planned + 1L, runtime.previewRevision());
} }
@Test(timeout = 180_000L)
public void concurrentDemandsShareOneRasterBuild() throws Exception {
FakePlanners planners = new FakePlanners(settings(0, false));
HydrologyRuntime runtime = runtime(planners, () -> false);
runtime.prepareWindowsAround(0, 0, 0, 0, 1).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
CountDownLatch gate = new CountDownLatch(1);
planners.naturalGate = gate;
AtomicReference<ChunkWaterRaster> first = new AtomicReference<>();
AtomicReference<ChunkWaterRaster> second = new AtomicReference<>();
Thread leader = new Thread(() -> first.set(runtime.raster(0, 0)));
Thread follower = new Thread(() -> second.set(runtime.raster(0, 0)));
try {
leader.start();
await(() -> planners.naturalSamples.get() > 0, "the first demand starts sampling");
follower.start();
await(() -> follower.getState() == Thread.State.WAITING || follower.getState() == Thread.State.TIMED_WAITING,
"the second demand waits");
assertEquals(0, planners.naturalSamplesOn(follower));
} finally {
gate.countDown();
leader.join(TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS));
follower.join(TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS));
}
assertFalse(first.get().provisional());
assertSame(first.get(), second.get());
assertEquals(0, planners.naturalSamplesOn(follower));
}
@Test(timeout = 180_000L) @Test(timeout = 180_000L)
public void failedRasterDoesNotAdvancePreviewRevision() throws Exception { public void failedRasterDoesNotAdvancePreviewRevision() throws Exception {
FakePlanners planners = new FakePlanners(settings(0, false)); FakePlanners planners = new FakePlanners(settings(0, false));
@@ -0,0 +1,95 @@
package art.arcane.iris.generation.hydrology.raster;
import art.arcane.iris.generation.hydrology.config.IrisBankStyle;
import art.arcane.iris.generation.hydrology.config.IrisHydrologyMaterials;
import art.arcane.iris.generation.hydrology.water.HydrologyTier;
import art.arcane.iris.generation.hydrology.water.Reach;
import art.arcane.iris.generation.hydrology.water.ReachStyle;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.SplittableRandom;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class ReachSegmentsTest {
private static final HydrologyTier TIER = new HydrologyTier("test", 0f, 1f, 4f, 1f, 0.4f, 1, true, 6, 4, IrisBankStyle.SMOOTH,
false, new IrisHydrologyMaterials(), "water", List.of());
@Test
public void coveredChannelLookupsMatchTheFullScan() {
SplittableRandom random = new SplittableRandom(1337L);
int hits = 0;
for (int trial = 0; trial < 40; trial++) {
int originX = random.nextInt(-2000, 2000) << 4;
int originZ = random.nextInt(-2000, 2000) << 4;
List<Reach> reaches = new ArrayList<>();
int count = 1 + random.nextInt(12);
for (int walk = 0; walk < count; walk++) {
reaches.add(randomWalk(trial * 100L + walk, originX + 8, originZ + 8, random));
}
ReachSegments scanned = segments(reaches, originX, originZ);
ReachSegments covered = segments(reaches, originX, originZ);
covered.coverChannels(originX - 17, originZ - 17, originX + 32, originZ + 32);
ReachCursor scan = new ReachCursor();
ReachCursor cover = new ReachCursor();
for (int z = originZ - 20; z <= originZ + 35; z++) {
for (int x = originX - 20; x <= originX + 35; x++) {
ReachSegments.Scope scope = random.nextInt(4) == 0 ? ReachSegments.Scope.SHORE : ReachSegments.Scope.CHANNEL;
scanned.nearest(x, z, scope, scan);
covered.nearest(x, z, scope, cover);
String at = scope + " at " + x + "," + z;
assertSameChoice(at + " channel", scan.channel, cover.channel);
assertSameChoice(at + " bank", scan.bank, cover.bank);
assertSameChoice(at + " edge", scan.edge, cover.edge);
if (scan.channel.reach != null) {
hits++;
}
}
}
}
assertTrue("random walks cross the covered area, hits=" + hits, hits > 1000);
}
private static void assertSameChoice(String what, ReachCursor.Choice expected, ReachCursor.Choice actual) {
assertSame(what, expected.reach, actual.reach);
assertEquals(what, expected.segment, actual.segment);
assertEquals(what, expected.index, actual.index);
assertEquals(what, Double.doubleToLongBits(expected.t), Double.doubleToLongBits(actual.t));
assertEquals(what, Double.doubleToLongBits(expected.distance), Double.doubleToLongBits(actual.distance));
}
private static ReachSegments segments(List<Reach> reaches, int originX, int originZ) {
ReachSegments segments = new ReachSegments(16);
for (Reach reach : reaches) {
segments.addNear(reach, originX - 1f, originZ - 1f, originX + 16f, originZ + 16f, ChunkWaterRaster.MAX_SHORE_DISTANCE + 1f);
}
return segments;
}
private static Reach randomWalk(long id, int centerX, int centerZ, SplittableRandom random) {
int points = 2 + random.nextInt(30);
float[] xs = new float[points];
float[] zs = new float[points];
float[] halfWidth = new float[points];
xs[0] = centerX + (float) random.nextDouble(-60D, 60D);
zs[0] = centerZ + (float) random.nextDouble(-60D, 60D);
halfWidth[0] = (float) random.nextDouble(0.5D, 9D);
for (int point = 1; point < points; point++) {
xs[point] = xs[point - 1] + (float) random.nextDouble(-12D, 12D);
zs[point] = zs[point - 1] + (float) random.nextDouble(-12D, 12D);
halfWidth[point] = random.nextInt(8) == 0 ? halfWidth[point - 1] : (float) random.nextDouble(0.5D, 9D);
}
int[] grade = new int[points];
float[] depth = new float[points];
ReachStyle[] style = new ReachStyle[points];
Arrays.fill(grade, 64);
Arrays.fill(depth, 1f);
Arrays.fill(style, ReachStyle.RUN);
return new Reach(id, TIER, -1L, xs, zs, grade, halfWidth, depth, style, 100f);
}
}
@@ -15,6 +15,7 @@ import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.IntFunction; import java.util.function.IntFunction;
@@ -50,18 +51,37 @@ public class CavePoolFillerTest {
return 0; return 0;
}); });
for (int chunkX = 0; chunkX < 1024; chunkX++) { for (int chunkX = 0; chunkX < 1024; chunkX++) {
filler.fill(chunkX, 0, carved, writer, SEED); filler.fill(chunkX, 0, () -> carved, writer, SEED);
} }
assertTrue("half density samples approximately half the candidate columns", columns.size() > 3600 && columns.size() < 4500); assertTrue("half density samples approximately half the candidate columns", columns.size() > 3600 && columns.size() < 4500);
Set<Long> repeated = new HashSet<>(columns); Set<Long> repeated = new HashSet<>(columns);
columns.clear(); columns.clear();
for (int chunkX = 1023; chunkX >= 0; chunkX--) { for (int chunkX = 1023; chunkX >= 0; chunkX--) {
filler.fill(chunkX, 0, carved, writer, SEED); filler.fill(chunkX, 0, () -> carved, writer, SEED);
} }
assertEquals(repeated, columns); assertEquals(repeated, columns);
verifyNoInteractions(writer); verifyNoInteractions(writer);
} }
@Test
public void chunksWithoutAPoolSiteNeverOpenTheirCaves() {
IrisHydrology config = new IrisHydrology().setDensity(0.5D);
config.getUnderground().getCavePools().setDensity(0.05D);
CavePoolFiller filler = new CavePoolFiller(HydrologySettings.compile(config, SEED, 63, 0, 384));
CarvedTerrain carved = mock(CarvedTerrain.class);
MantleWriter writer = mock(MantleWriter.class);
AtomicInteger opened = new AtomicInteger();
int chunks = 1024;
for (int chunkX = 0; chunkX < chunks; chunkX++) {
filler.fill(chunkX, 0, () -> {
opened.incrementAndGet();
return carved;
}, writer, SEED);
}
assertTrue("only chunks drawing a pool site open their caves, opened " + opened.get(), opened.get() > 0 && opened.get() < chunks / 8);
verifyNoInteractions(writer);
}
@Test @Test
public void zeroGlobalDensityDoesNotReadCaves() { public void zeroGlobalDensityDoesNotReadCaves() {
IrisHydrology config = new IrisHydrology().setDensity(0D); IrisHydrology config = new IrisHydrology().setDensity(0D);
@@ -69,7 +89,7 @@ public class CavePoolFillerTest {
CavePoolFiller filler = new CavePoolFiller(HydrologySettings.compile(config, SEED, 63, 0, 384)); CavePoolFiller filler = new CavePoolFiller(HydrologySettings.compile(config, SEED, 63, 0, 384));
CarvedTerrain carved = mock(CarvedTerrain.class); CarvedTerrain carved = mock(CarvedTerrain.class);
MantleWriter writer = mock(MantleWriter.class); MantleWriter writer = mock(MantleWriter.class);
filler.fill(0, 0, carved, writer, SEED); filler.fill(0, 0, () -> carved, writer, SEED);
verifyNoInteractions(carved, writer); verifyNoInteractions(carved, writer);
} }
@@ -143,10 +163,10 @@ public class CavePoolFillerTest {
MantleWriter forwardWriter = writer(forward); MantleWriter forwardWriter = writer(forward);
MantleWriter reverseWriter = writer(reverse); MantleWriter reverseWriter = writer(reverse);
for (int index = 0; index < 9; index++) { for (int index = 0; index < 9; index++) {
new CavePoolFiller(settings).fill(index % 3 - 1, index / 3 - 1, world, forwardWriter, SEED); new CavePoolFiller(settings).fill(index % 3 - 1, index / 3 - 1, () -> world, forwardWriter, SEED);
} }
for (int index = 8; index >= 0; index--) { for (int index = 8; index >= 0; index--) {
new CavePoolFiller(settings).fill(index % 3 - 1, index / 3 - 1, world, reverseWriter, SEED); new CavePoolFiller(settings).fill(index % 3 - 1, index / 3 - 1, () -> world, reverseWriter, SEED);
} }
assertFalse(forward.isEmpty()); assertFalse(forward.isEmpty());
@@ -284,7 +304,7 @@ public class CavePoolFillerTest {
private static Map<Long, HydrologyCaveCell> fill(HydrologySettings settings, VoxelWorld world, int chunkX, int chunkZ) { private static Map<Long, HydrologyCaveCell> fill(HydrologySettings settings, VoxelWorld world, int chunkX, int chunkZ) {
Map<Long, HydrologyCaveCell> written = new HashMap<>(); Map<Long, HydrologyCaveCell> written = new HashMap<>();
new CavePoolFiller(settings).fill(chunkX, chunkZ, world, writer(written), SEED); new CavePoolFiller(settings).fill(chunkX, chunkZ, () -> world, writer(written), SEED);
return written; return written;
} }
@@ -324,7 +324,7 @@ public class MantleCarvedTerrainTest {
for (SeaInlet inlet : complex.getHydrologyRuntime().inlets(0, chunkZ)) { for (SeaInlet inlet : complex.getHydrologyRuntime().inlets(0, chunkZ)) {
rasterizer.write(inlet, 0, chunkZ, writer, HydrologyCaveCell.SEA_FLUID, "", SeaInletRasterizer.SubterrainFluid.NOWHERE); rasterizer.write(inlet, 0, chunkZ, writer, HydrologyCaveCell.SEA_FLUID, "", SeaInletRasterizer.SubterrainFluid.NOWHERE);
} }
new CavePoolFiller(settings).fill(0, chunkZ, new MantleCarvedTerrain(writer, complex, engine, 0, chunkZ), writer, SEED); new CavePoolFiller(settings).fill(0, chunkZ, () -> new MantleCarvedTerrain(writer, complex, engine, 0, chunkZ), writer, SEED);
} }
return cells; return cells;
} }