diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/engine/HydrologyRuntime.java b/core/src/main/java/art/arcane/iris/generation/hydrology/engine/HydrologyRuntime.java index 8c38c8c2f..c2536f5b8 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/engine/HydrologyRuntime.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/engine/HydrologyRuntime.java @@ -57,7 +57,7 @@ import java.util.function.Predicate; import java.util.function.Supplier; 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 CHUNKS_PER_WINDOW = WindowKey.SIZE >> 4; public static final int MAX_PREGENERATION_REGIONS = 4096; @@ -73,6 +73,7 @@ public final class HydrologyRuntime { private final RasterLru rasters; private final ThreadLocal last; private final Set queued; + private final ConcurrentHashMap> building; private final RasterFailures failures; private final RasterFailures windowPreviewFailures; private final RasterFailures regionPreviewFailures; @@ -96,6 +97,7 @@ public final class HydrologyRuntime { this.rasters = null; this.last = null; this.queued = null; + this.building = null; this.failures = null; this.windowPreviewFailures = null; this.regionPreviewFailures = null; @@ -123,6 +125,7 @@ public final class HydrologyRuntime { this.rasters = new RasterLru(); this.last = ThreadLocal.withInitial(LastRaster::new); this.queued = ConcurrentHashMap.newKeySet(); + this.building = new ConcurrentHashMap<>(); this.failures = new RasterFailures(ticker); this.windowPreviewFailures = new RasterFailures(ticker); this.regionPreviewFailures = new RasterFailures(ticker); @@ -238,7 +241,7 @@ public final class HydrologyRuntime { requireOpen(); long key = key(chunkX, chunkZ); 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) { @@ -525,6 +528,41 @@ public final class HydrologyRuntime { } } + private ChunkWaterRaster build(int chunkX, int chunkZ, long key, LastRaster slot) { + CompletableFuture claim = new CompletableFuture<>(); + CompletableFuture 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 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) { try { List windowKeys = neighbourhood(chunkX, chunkZ); diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkFeatures.java b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkFeatures.java index 3a0eb132b..11fd7ba6c 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkFeatures.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkFeatures.java @@ -12,6 +12,7 @@ import art.arcane.iris.generation.hydrology.water.PondMask; import art.arcane.iris.generation.hydrology.water.Reach; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -28,17 +29,20 @@ final class ChunkFeatures { private final List ponds; private final List lakes; private final List inlets; + private final int[] inletBoxes; private final int[] lakeBoxes; private final ReachSegments segments = new ReachSegments(SEGMENT_CAPACITY); private final MaskEdges pondShores = new MaskEdges(MaskEdges.Kind.WATER); private final MaskEdges pondBanks = new MaskEdges(MaskEdges.Kind.BANK); private final MaskEdges lakeReach = new MaskEdges(MaskEdges.Kind.GROWN_WATER); + private boolean inert; - private ChunkFeatures(List reaches, List ponds, List lakes, List inlets) { + private ChunkFeatures(List reaches, List ponds, List lakes, List inlets, int[] inletBoxes) { this.reaches = reaches; this.ponds = ponds; this.lakes = lakes; this.inlets = inlets; + this.inletBoxes = inletBoxes; this.lakeBoxes = new int[4 * lakes.size()]; } @@ -48,12 +52,15 @@ final class ChunkFeatures { List reaches = new ArrayList<>(); List ponds = new ArrayList<>(); List inlets = new ArrayList<>(); + int[] inletBoxes = new int[0]; for (IndexedWindow window : windows) { for (SeaInlet inlet : window.plan().inlets()) { int[] bounds = SeaInletRasterizer.bounds(inlet); int padding = RiffleProfile.RADIUS + 1; 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) { + inletBoxes = Arrays.copyOf(inletBoxes, inletBoxes.length + 4); + System.arraycopy(bounds, 0, inletBoxes, 4 * inlets.size(), 4); inlets.add(inlet); } } @@ -64,17 +71,22 @@ final class ChunkFeatures { } } List 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); return features; } - boolean empty() { - return reaches.isEmpty() && ponds.isEmpty() && lakes.isEmpty() && inlets.isEmpty(); + boolean inert() { + return inert; } 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) { return inlet; } @@ -179,6 +191,8 @@ final class ChunkFeatures { for (Reach reach : reaches) { 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(); int reach = Math.max(shore.taper(), ChunkWaterRaster.MAX_SHORE_DISTANCE) + 1; int shelf = shore.shelf() + 1; @@ -192,6 +206,39 @@ final class ChunkFeatures { lakeReach.collect(mask, minX - shoreReach, minZ - shoreReach, maxX + shoreReach, maxZ + shoreReach); 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 windowsByKey(int chunkX, int chunkZ, List windows) { diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkRasterizer.java b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkRasterizer.java index 369be027d..9a27928ad 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkRasterizer.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkRasterizer.java @@ -37,7 +37,7 @@ public final class ChunkRasterizer { List lakeRegionPlans) { Objects.requireNonNull(naturalHeight, "naturalHeight"); ChunkFeatures features = ChunkFeatures.gather(settings, chunkX, chunkZ, halo, windows, lakeRegionPlans, lakeRegions(chunkX, chunkZ)); - if (features.empty()) { + if (features.inert()) { return ChunkWaterRaster.EMPTY; } return new RasterComposer(settings, features, chunkX, chunkZ).compose(naturalHeight); diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkWaterRaster.java b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkWaterRaster.java index 524bcef74..02c963754 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkWaterRaster.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkWaterRaster.java @@ -2,6 +2,8 @@ package art.arcane.iris.generation.hydrology.raster; import art.arcane.iris.generation.hydrology.water.HydrologyTier; +import java.util.Arrays; + public final class ChunkWaterRaster { public static final int MAX_SHORE_DISTANCE = 32; public static final ChunkWaterRaster PROVISIONAL = new ChunkWaterRaster(true); @@ -13,10 +15,13 @@ public final class ChunkWaterRaster { private final int[] terrain; private final int[] head; private final byte[] role; - private final HydrologyTier[] tier; - private final long[] featureId; - private final String[] fluid; - private final int[] caveCeiling; + private final byte[] tier; + private final HydrologyTier[] tiers; + private final byte[] feature; + private final long[] features; + private final byte[] fluid; + private final String[] fluids; + private final boolean caveMask; private final byte[] shoreDistance; private ChunkWaterRaster(boolean provisional) { @@ -25,26 +30,32 @@ public final class ChunkWaterRaster { this.head = null; this.role = null; this.tier = null; - this.featureId = null; + this.tiers = null; + this.feature = null; + this.features = null; this.fluid = null; - this.caveCeiling = null; + this.fluids = null; + this.caveMask = false; this.shoreDistance = null; } - ChunkWaterRaster(int[] terrain, int[] head, byte[] role, HydrologyTier[] tier, long[] featureId, String[] fluid, int[] caveCeiling, - byte[] shoreDistance) { - if (terrain.length != COLUMNS || head.length != COLUMNS || role.length != COLUMNS || tier.length != COLUMNS - || featureId.length != COLUMNS || fluid.length != COLUMNS || caveCeiling.length != COLUMNS || shoreDistance.length != COLUMNS) { + ChunkWaterRaster(int[] terrain, int[] head, byte[] role, Palette tiers, LongPalette features, Palette fluids, + boolean caveMask, byte[] shoreDistance) { + if (terrain.length != COLUMNS || head.length != COLUMNS || role.length != COLUMNS || tiers.indices.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"); } this.provisional = false; this.terrain = terrain; this.head = head; this.role = role; - this.tier = tier; - this.featureId = featureId; - this.fluid = fluid; - this.caveCeiling = caveCeiling; + this.tier = tiers.indices; + this.tiers = tiers.values(); + this.feature = features.indices; + this.features = features.values(); + this.fluid = fluids.indices; + this.fluids = fluids.values(); + this.caveMask = caveMask; this.shoreDistance = shoreDistance; } @@ -68,22 +79,22 @@ public final class ChunkWaterRaster { public HydrologyTier tier(int lx, int 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) { 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) { 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) { 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) { @@ -105,4 +116,53 @@ public final class ChunkWaterRaster { } return lz << 4 | lx; } + + static final class Palette { + 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); + } + } } diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/RasterComposer.java b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/RasterComposer.java index 36dec7012..bb377ec85 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/RasterComposer.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/RasterComposer.java @@ -385,34 +385,37 @@ final class RasterComposer { private ChunkWaterRaster pack() { 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[] headOut = new int[columns]; byte[] roleOut = new byte[columns]; - HydrologyTier[] tierOut = new HydrologyTier[columns]; - long[] featureOut = new long[columns]; - String[] fluidOut = new String[columns]; - int[] caveOut = new int[columns]; + ChunkWaterRaster.Palette tierOut = new ChunkWaterRaster.Palette<>(new HydrologyTier[columns]); + ChunkWaterRaster.LongPalette featureOut = new ChunkWaterRaster.LongPalette(); + ChunkWaterRaster.Palette fluidOut = new ChunkWaterRaster.Palette<>(new String[columns]); byte[] shoreOut = new byte[columns]; - boolean caveMask = settings.deformation().caveMask(); - boolean affected = false; for (int lz = 0; lz < CHUNK; lz++) { for (int lx = 0; lx < CHUNK; lx++) { int cell = (lz + 1) * GRID + lx + 1; int index = lz << 4 | lx; - int shore = shore(cell); terrainOut[index] = terrain[cell]; headOut[index] = head[cell]; roleOut[index] = (byte) role[cell].ordinal(); - tierOut[index] = tier[cell]; - featureOut[index] = feature[cell]; - fluidOut[index] = fluid[cell]; - caveOut[index] = caveMask && head[cell] != Integer.MIN_VALUE ? terrain[cell] - 1 : Integer.MAX_VALUE; - shoreOut[index] = (byte) shore; - affected |= role[cell] != WaterRole.NONE || terrain[cell] != natural[cell] || shore < ChunkWaterRaster.MAX_SHORE_DISTANCE; + tierOut.set(index, tier[cell]); + featureOut.set(index, feature[cell]); + fluidOut.set(index, fluid[cell]); + shoreOut[index] = (byte) shore(cell); } } - return affected ? new ChunkWaterRaster(terrainOut, headOut, roleOut, tierOut, featureOut, fluidOut, caveOut, shoreOut) - : ChunkWaterRaster.EMPTY; + return new ChunkWaterRaster(terrainOut, headOut, roleOut, tierOut, featureOut, fluidOut, settings.deformation().caveMask(), shoreOut); } private int shore(int cell) { diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ReachSegments.java b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ReachSegments.java index 7661088e5..60bebde2d 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ReachSegments.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ReachSegments.java @@ -6,6 +6,8 @@ import java.util.Arrays; final class ReachSegments { private static final int RUN = 16; + private static final int COVER_SHIFT = 2; + private static final float COVER_MARGIN = 1f; private Reach[] reaches; private int[] segments; @@ -24,6 +26,13 @@ final class ReachSegments { private int[] runBands = new int[8]; private int runs; 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) { 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) { if (!sealed) { seal(); @@ -89,6 +111,16 @@ final class ReachSegments { hint(channelHint, x, z, unbounded, cursor); hint(bankHint, 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; for (int run = 0; run < runs; run++) { int end = runEnds[run]; @@ -99,9 +131,7 @@ final class ReachSegments { continue; } 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]), - Math.max(startZ[index], endZ[index]), limit(radius[index], bands[index], scope, cursor)) - && index != channelHint && index != bankHint && index != edgeHint) { + if (!beyondSegment(index, x, z, scope, cursor) && index != channelHint && index != bankHint && index != edgeHint) { 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) { if (index >= 0 && index < size) { evaluate(index, x, z, unbounded, cursor); @@ -171,9 +218,56 @@ final class ReachSegments { addRun(start, end); start = end; } + if (covered) { + cover(); + } 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) { if (runs == runEnds.length) { runEnds = Arrays.copyOf(runEnds, 2 * runs); diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/RiffleProfile.java b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/RiffleProfile.java index 75587a877..d56581969 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/RiffleProfile.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/RiffleProfile.java @@ -15,6 +15,7 @@ final class RiffleProfile { private static final int LIMIT = 10 * RADIUS; private static final int DRY = Integer.MIN_VALUE; private static final int[] SIDES = {1, -1, GRID, -GRID}; + private static final int[] REMAINING = remaining(); private final List fields = new ArrayList<>(); @@ -85,6 +86,20 @@ final class RiffleProfile { 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) { } @@ -107,6 +122,9 @@ final class RiffleProfile { } private void offer(int cell, int distance) { + if (distance + REMAINING[cell] >= LIMIT) { + return; + } if (distances[cell] < LIMIT) { remove(cell); } diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/underground/CavePoolFiller.java b/core/src/main/java/art/arcane/iris/generation/hydrology/underground/CavePoolFiller.java index d1bf23062..fb303b98c 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/underground/CavePoolFiller.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/underground/CavePoolFiller.java @@ -10,6 +10,7 @@ import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import java.util.Arrays; import java.util.Set; +import java.util.function.Supplier; public final class CavePoolFiller { public static final int REACH = HydrologySettings.MAX_CAVE_POOL_RADIUS + 1; @@ -40,15 +41,19 @@ public final class CavePoolFiller { 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 terrain, MantleWriter writer, long seed) { if (!pools.enabled() || density == 0f) { return; } int attempts = attempts(seed, chunkX, chunkZ); + CarvedTerrain carved = null; for (int attempt = 0; attempt < attempts; attempt++) { if (density < 1f && CellHash.unit(CellHash.mix(seed, SALT_DENSITY + attempt, chunkX, chunkZ)) >= density) { continue; } + if (carved == null) { + carved = terrain.get(); + } long column = CellHash.mix(seed, SALT_COLUMN + attempt, chunkX, chunkZ); int x = (chunkX << 4) + (int) (column & 15); int z = (chunkZ << 4) + (int) ((column >>> 4) & 15); diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/MantleHydrologyComponent.java b/core/src/main/java/art/arcane/iris/generation/mantle/MantleHydrologyComponent.java index 5c3397042..4ac0b4c3d 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/MantleHydrologyComponent.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/MantleHydrologyComponent.java @@ -85,8 +85,9 @@ public final class MantleHydrologyComponent extends IrisMantleComponent { generateFalls(writer, complex, runtime, own, chunkX, chunkZ); if (poolsActive()) { HydrologySettings settings = runtime.settings(); - new CavePoolFiller(settings).fill(chunkX, chunkZ, - new MantleCarvedTerrain(writer, complex, getEngineMantle().getEngine(), chunkX, chunkZ), writer, settings.seed()); + Engine engine = getEngineMantle().getEngine(); + new CavePoolFiller(settings).fill(chunkX, chunkZ, () -> new MantleCarvedTerrain(writer, complex, engine, chunkX, chunkZ), writer, + settings.seed()); } } diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/engine/HydrologyRuntimeTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/engine/HydrologyRuntimeTest.java index 7e38638fa..cbf79be3f 100644 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/engine/HydrologyRuntimeTest.java +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/engine/HydrologyRuntimeTest.java @@ -216,6 +216,34 @@ public class HydrologyRuntimeTest { 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 first = new AtomicReference<>(); + AtomicReference 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) public void failedRasterDoesNotAdvancePreviewRevision() throws Exception { FakePlanners planners = new FakePlanners(settings(0, false)); diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/raster/ReachSegmentsTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/raster/ReachSegmentsTest.java new file mode 100644 index 000000000..299c4178d --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/raster/ReachSegmentsTest.java @@ -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 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 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); + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/underground/CavePoolFillerTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/underground/CavePoolFillerTest.java index 2d6b4cc30..860215a9a 100644 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/underground/CavePoolFillerTest.java +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/underground/CavePoolFillerTest.java @@ -15,6 +15,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.IntFunction; @@ -50,18 +51,37 @@ public class CavePoolFillerTest { return 0; }); 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); Set repeated = new HashSet<>(columns); columns.clear(); for (int chunkX = 1023; chunkX >= 0; chunkX--) { - filler.fill(chunkX, 0, carved, writer, SEED); + filler.fill(chunkX, 0, () -> carved, writer, SEED); } assertEquals(repeated, columns); 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 public void zeroGlobalDensityDoesNotReadCaves() { IrisHydrology config = new IrisHydrology().setDensity(0D); @@ -69,7 +89,7 @@ public class CavePoolFillerTest { CavePoolFiller filler = new CavePoolFiller(HydrologySettings.compile(config, SEED, 63, 0, 384)); CarvedTerrain carved = mock(CarvedTerrain.class); MantleWriter writer = mock(MantleWriter.class); - filler.fill(0, 0, carved, writer, SEED); + filler.fill(0, 0, () -> carved, writer, SEED); verifyNoInteractions(carved, writer); } @@ -143,10 +163,10 @@ public class CavePoolFillerTest { MantleWriter forwardWriter = writer(forward); MantleWriter reverseWriter = writer(reverse); 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--) { - 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()); @@ -284,7 +304,7 @@ public class CavePoolFillerTest { private static Map fill(HydrologySettings settings, VoxelWorld world, int chunkX, int chunkZ) { Map 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; } diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/MantleCarvedTerrainTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/MantleCarvedTerrainTest.java index 22603f411..de5db777c 100644 --- a/core/src/test/java/art/arcane/iris/generation/mantle/MantleCarvedTerrainTest.java +++ b/core/src/test/java/art/arcane/iris/generation/mantle/MantleCarvedTerrainTest.java @@ -324,7 +324,7 @@ public class MantleCarvedTerrainTest { for (SeaInlet inlet : complex.getHydrologyRuntime().inlets(0, chunkZ)) { 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; }