mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-11-01 08:50:06 +00:00
Merge branch 'perf-raster' into perf-review
This commit is contained in:
commit
e9c18cdc08
13 files changed
+463
-54
No files matched your search
+40
-2
@@ -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<LastRaster> last;
|
||||
private final Set<Long> queued;
|
||||
private final ConcurrentHashMap<Long, CompletableFuture<ChunkWaterRaster>> 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<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) {
|
||||
try {
|
||||
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 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<Pond> ponds;
|
||||
private final List<Pond> lakes;
|
||||
private final List<SeaInlet> 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<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.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<Reach> reaches = new ArrayList<>();
|
||||
List<Pond> ponds = new ArrayList<>();
|
||||
List<SeaInlet> 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<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);
|
||||
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<WindowKey, IndexedWindow> windowsByKey(int chunkX, int chunkZ, List<IndexedWindow> windows) {
|
||||
|
||||
@@ -37,7 +37,7 @@ public final class ChunkRasterizer {
|
||||
List<RegionPlan> 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);
|
||||
|
||||
+78
-18
@@ -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<HydrologyTier> tiers, LongPalette features, Palette<String> 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<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-15
@@ -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<HydrologyTier> tierOut = new ChunkWaterRaster.Palette<>(new HydrologyTier[columns]);
|
||||
ChunkWaterRaster.LongPalette featureOut = new ChunkWaterRaster.LongPalette();
|
||||
ChunkWaterRaster.Palette<String> 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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<Field> 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);
|
||||
}
|
||||
|
||||
+6
-1
@@ -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<? extends CarvedTerrain> 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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+28
@@ -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<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)
|
||||
public void failedRasterDoesNotAdvancePreviewRevision() throws Exception {
|
||||
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);
|
||||
}
|
||||
}
|
||||
+26
-6
@@ -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<Long> 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<Long, HydrologyCaveCell> fill(HydrologySettings settings, VoxelWorld world, int chunkX, int chunkZ) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user