mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 14:00:26 +00:00
Shared bounds biomes and paged erosion raster
- Bounds starcasts share whole-column base biome samples across threads through a heap-sized lock-free memo - Erosion raster geometry tracks station owners in paged local arrays and builds its index map in first-visit order
This commit is contained in:
1 parent
af90765a92
commit
1fe26e6288
6 files changed
+568
-46
No files matched your search
+80
-38
@@ -18,6 +18,7 @@ import it.unimi.dsi.fastutil.longs.LongIterator;
|
|||||||
import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
|
import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
|
||||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public final class ErosionFieldCompiler {
|
public final class ErosionFieldCompiler {
|
||||||
@@ -159,44 +160,7 @@ public final class ErosionFieldCompiler {
|
|||||||
IntArrayList stations = geometry.stations;
|
IntArrayList stations = geometry.stations;
|
||||||
DoubleArrayList distance = geometry.distance;
|
DoubleArrayList distance = geometry.distance;
|
||||||
if (!geometry.prepared) {
|
if (!geometry.prepared) {
|
||||||
for (int station = 0; station < count; station++) {
|
rasterize(centerline, prepared.radii(), count, samplingBounds, indices, stations, distance);
|
||||||
int radius = prepared.radii()[station];
|
|
||||||
int stationX = centerline.x()[station];
|
|
||||||
int stationZ = centerline.z()[station];
|
|
||||||
if (samplingBounds != null && !samplingBounds.intersects(stationX, stationZ, radius)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
int minimumZ = samplingBounds == null ? -radius : Math.max(-radius, samplingBounds.minimumZ() - stationZ);
|
|
||||||
int maximumZ = samplingBounds == null ? radius : Math.min(radius, samplingBounds.maximumZ() - stationZ);
|
|
||||||
int minimumX = samplingBounds == null ? -radius : Math.max(-radius, samplingBounds.minimumX() - stationX);
|
|
||||||
int maximumX = samplingBounds == null ? radius : Math.min(radius, samplingBounds.maximumX() - stationX);
|
|
||||||
for (int deltaZ = minimumZ; deltaZ <= maximumZ; deltaZ++) {
|
|
||||||
for (int deltaX = minimumX; deltaX <= maximumX; deltaX++) {
|
|
||||||
int cellX = stationX + deltaX;
|
|
||||||
int cellZ = stationZ + deltaZ;
|
|
||||||
double cellDistance = centerline.distanceToSegment(station, cellX, cellZ, SurfaceDistanceTable.SHARED);
|
|
||||||
if (cellDistance > radius) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
long key = RiverFootprint.pack(cellX, cellZ);
|
|
||||||
int index = indices.putIfAbsent(key, stations.size());
|
|
||||||
if (index < 0) {
|
|
||||||
stations.add(station);
|
|
||||||
distance.add(cellDistance);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
int existing = stations.getInt(index);
|
|
||||||
double existingDistance = distance.getDouble(index);
|
|
||||||
// A cell on a station's cross-section row ties between the two segments meeting
|
|
||||||
// there; the later station owns it so the row uses the head its own ring bounded.
|
|
||||||
if (cellDistance < existingDistance - EPSILON
|
|
||||||
|| Math.abs(cellDistance - existingDistance) <= EPSILON && station > existing) {
|
|
||||||
stations.set(index, station);
|
|
||||||
distance.set(index, cellDistance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
geometry.prepared = true;
|
geometry.prepared = true;
|
||||||
}
|
}
|
||||||
int oceanStart = oceanStart(centerline, valley, terminal);
|
int oceanStart = oceanStart(centerline, valley, terminal);
|
||||||
@@ -912,12 +876,90 @@ public final class ErosionFieldCompiler {
|
|||||||
return 1D - t * t * (3D - 2D * t);
|
return 1D - t * t * (3D - 2D * t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void rasterize(SurfaceCenterline centerline, int[] radii, int count, SurfaceBounds samplingBounds,
|
||||||
|
Long2IntOpenHashMap indices, IntArrayList stations, DoubleArrayList distance) {
|
||||||
|
CellPages pages = new CellPages();
|
||||||
|
LongArrayList order = new LongArrayList();
|
||||||
|
for (int station = 0; station < count; station++) {
|
||||||
|
int radius = radii[station];
|
||||||
|
int stationX = centerline.x()[station];
|
||||||
|
int stationZ = centerline.z()[station];
|
||||||
|
if (samplingBounds != null && !samplingBounds.intersects(stationX, stationZ, radius)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int minimumZ = samplingBounds == null ? -radius : Math.max(-radius, samplingBounds.minimumZ() - stationZ);
|
||||||
|
int maximumZ = samplingBounds == null ? radius : Math.min(radius, samplingBounds.maximumZ() - stationZ);
|
||||||
|
int minimumX = samplingBounds == null ? -radius : Math.max(-radius, samplingBounds.minimumX() - stationX);
|
||||||
|
int maximumX = samplingBounds == null ? radius : Math.min(radius, samplingBounds.maximumX() - stationX);
|
||||||
|
for (int deltaZ = minimumZ; deltaZ <= maximumZ; deltaZ++) {
|
||||||
|
for (int deltaX = minimumX; deltaX <= maximumX; deltaX++) {
|
||||||
|
int cellX = stationX + deltaX;
|
||||||
|
int cellZ = stationZ + deltaZ;
|
||||||
|
double cellDistance = centerline.distanceToSegment(station, cellX, cellZ, SurfaceDistanceTable.SHARED);
|
||||||
|
if (cellDistance > radius) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int[] page = pages.page(cellX, cellZ);
|
||||||
|
int offset = CellPages.offset(cellX, cellZ);
|
||||||
|
int index = page[offset];
|
||||||
|
if (index < 0) {
|
||||||
|
page[offset] = stations.size();
|
||||||
|
stations.add(station);
|
||||||
|
distance.add(cellDistance);
|
||||||
|
order.add(RiverFootprint.pack(cellX, cellZ));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int existing = stations.getInt(index);
|
||||||
|
double existingDistance = distance.getDouble(index);
|
||||||
|
// A cell on a station's cross-section row ties between the two segments meeting
|
||||||
|
// there; the later station owns it so the row uses the head its own ring bounded.
|
||||||
|
if (cellDistance < existingDistance - EPSILON
|
||||||
|
|| Math.abs(cellDistance - existingDistance) <= EPSILON && station > existing) {
|
||||||
|
stations.set(index, station);
|
||||||
|
distance.set(index, cellDistance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int index = 0; index < order.size(); index++) {
|
||||||
|
indices.put(order.getLong(index), index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static double side(SurfaceCenterline centerline, int station, int cellX, int cellZ) {
|
private static double side(SurfaceCenterline centerline, int station, int cellX, int cellZ) {
|
||||||
double deltaX = cellX - centerline.x()[station];
|
double deltaX = cellX - centerline.x()[station];
|
||||||
double deltaZ = cellZ - centerline.z()[station];
|
double deltaZ = cellZ - centerline.z()[station];
|
||||||
return deltaX * centerline.normalX(station) + deltaZ * centerline.normalZ(station);
|
return deltaX * centerline.normalX(station) + deltaZ * centerline.normalZ(station);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class CellPages {
|
||||||
|
private static final int SHIFT = 6;
|
||||||
|
private static final int MASK = (1 << SHIFT) - 1;
|
||||||
|
private final Long2ObjectOpenHashMap<int[]> pages = new Long2ObjectOpenHashMap<>();
|
||||||
|
private long currentKey;
|
||||||
|
private int[] current;
|
||||||
|
|
||||||
|
private int[] page(int x, int z) {
|
||||||
|
long key = RiverFootprint.pack(x >> SHIFT, z >> SHIFT);
|
||||||
|
if (current != null && key == currentKey) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
int[] page = pages.get(key);
|
||||||
|
if (page == null) {
|
||||||
|
page = new int[1 << (SHIFT * 2)];
|
||||||
|
Arrays.fill(page, -1);
|
||||||
|
pages.put(key, page);
|
||||||
|
}
|
||||||
|
current = page;
|
||||||
|
currentKey = key;
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int offset(int x, int z) {
|
||||||
|
return ((z & MASK) << SHIFT) | (x & MASK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final class RasterGeometry {
|
private static final class RasterGeometry {
|
||||||
private final Long2IntOpenHashMap indices = new Long2IntOpenHashMap();
|
private final Long2IntOpenHashMap indices = new Long2IntOpenHashMap();
|
||||||
private final IntArrayList stations = new IntArrayList();
|
private final IntArrayList stations = new IntArrayList();
|
||||||
|
|||||||
@@ -128,8 +128,8 @@ import java.nio.file.Path;
|
|||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "terrainProgram", "bakeCache", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"})
|
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "sharedBoundsBiomes", "terrainProgram", "bakeCache", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"})
|
||||||
@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "terrainProgram", "bakeCache", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"})
|
@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "sharedBoundsBiomes", "terrainProgram", "bakeCache", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"})
|
||||||
public class IrisComplex implements DataProvider {
|
public class IrisComplex implements DataProvider {
|
||||||
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
|
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
|
||||||
private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
|
private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
|
||||||
@@ -163,6 +163,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
@Getter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE)
|
||||||
private final int biomeBoundsSamplingStep;
|
private final int biomeBoundsSamplingStep;
|
||||||
private transient volatile SharedCornerBounds sharedCornerBounds;
|
private transient volatile SharedCornerBounds sharedCornerBounds;
|
||||||
|
private transient volatile SharedBoundsBiomes sharedBoundsBiomes;
|
||||||
@Getter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE)
|
||||||
@Setter(AccessLevel.NONE)
|
@Setter(AccessLevel.NONE)
|
||||||
private transient volatile TerrainProgram terrainProgram;
|
private transient volatile TerrainProgram terrainProgram;
|
||||||
@@ -2054,6 +2055,21 @@ public class IrisComplex implements DataProvider {
|
|||||||
return shared;
|
return shared;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SharedBoundsBiomes sharedBoundsBiomes(ProceduralStream<IrisBiome> source) {
|
||||||
|
SharedBoundsBiomes shared = sharedBoundsBiomes;
|
||||||
|
if (shared != null && shared.source() == source) {
|
||||||
|
return shared;
|
||||||
|
}
|
||||||
|
synchronized (this) {
|
||||||
|
shared = sharedBoundsBiomes;
|
||||||
|
if (shared == null || shared.source() != source) {
|
||||||
|
shared = new SharedBoundsBiomes(source, SharedBoundsBiomes.capacityFor(Runtime.getRuntime().maxMemory()));
|
||||||
|
sharedBoundsBiomes = shared;
|
||||||
|
}
|
||||||
|
return shared;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private long computePackedBounds(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, IrisGenerator[] generators, int gx, int gz) {
|
private long computePackedBounds(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, IrisGenerator[] generators, int gx, int gz) {
|
||||||
NoiseBounds bounds = sampleBoundsRaw(cache, engine, interpolator, generators, gx, gz);
|
NoiseBounds bounds = sampleBoundsRaw(cache, engine, interpolator, generators, gx, gz);
|
||||||
return (((long) Float.floatToRawIntBits((float) bounds.min())) << 32) | (Float.floatToRawIntBits((float) bounds.max()) & 0xFFFFFFFFL);
|
return (((long) Float.floatToRawIntBits((float) bounds.min())) << 32) | (Float.floatToRawIntBits((float) bounds.max()) & 0xFFFFFFFFL);
|
||||||
@@ -2688,6 +2704,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
private Engine engine;
|
private Engine engine;
|
||||||
private IrisGenerator[] generators;
|
private IrisGenerator[] generators;
|
||||||
private IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds;
|
private IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds;
|
||||||
|
private SharedBoundsBiomes sharedBiomes;
|
||||||
private boolean integerCoordinates;
|
private boolean integerCoordinates;
|
||||||
private boolean inUse;
|
private boolean inUse;
|
||||||
|
|
||||||
@@ -2702,6 +2719,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
this.cachedBounds = cachedBounds;
|
this.cachedBounds = cachedBounds;
|
||||||
this.integerCoordinates = complex.baseBiomeStream instanceof BakedStream<?>
|
this.integerCoordinates = complex.baseBiomeStream instanceof BakedStream<?>
|
||||||
|| complex.baseBiomeStream instanceof CachedStream2D<?>;
|
|| complex.baseBiomeStream instanceof CachedStream2D<?>;
|
||||||
|
this.sharedBiomes = integerCoordinates ? complex.sharedBoundsBiomes(complex.baseBiomeStream) : null;
|
||||||
this.inUse = true;
|
this.inUse = true;
|
||||||
if (sampleSource.get() != complex.baseBiomeStream) {
|
if (sampleSource.get() != complex.baseBiomeStream) {
|
||||||
sampleCache.clear();
|
sampleCache.clear();
|
||||||
@@ -2718,6 +2736,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
engine = null;
|
engine = null;
|
||||||
generators = null;
|
generators = null;
|
||||||
cachedBounds = null;
|
cachedBounds = null;
|
||||||
|
sharedBiomes = null;
|
||||||
inUse = false;
|
inUse = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2728,11 +2747,17 @@ public class IrisComplex implements DataProvider {
|
|||||||
double sampleZ = integerCoordinates ? (int) zz : zz;
|
double sampleZ = integerCoordinates ? (int) zz : zz;
|
||||||
IrisBiome bx = sampleCache.get(sampleX, sampleZ);
|
IrisBiome bx = sampleCache.get(sampleX, sampleZ);
|
||||||
if (bx == null) {
|
if (bx == null) {
|
||||||
ChunkBakeCache.enterSparse();
|
bx = sharedBiomes == null ? null : sharedBiomes.get((int) sampleX, (int) sampleZ);
|
||||||
try {
|
if (bx == null) {
|
||||||
bx = complex.baseBiomeStream.get(xx, zz);
|
ChunkBakeCache.enterSparse();
|
||||||
} finally {
|
try {
|
||||||
ChunkBakeCache.exitSparse();
|
bx = complex.baseBiomeStream.get(xx, zz);
|
||||||
|
} finally {
|
||||||
|
ChunkBakeCache.exitSparse();
|
||||||
|
}
|
||||||
|
if (sharedBiomes != null) {
|
||||||
|
sharedBiomes.put((int) sampleX, (int) sampleZ, bx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sampleCache.put(sampleX, sampleZ, bx);
|
sampleCache.put(sampleX, sampleZ, bx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package art.arcane.iris.generation.runtime;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
|
|
||||||
|
import java.lang.invoke.MethodHandles;
|
||||||
|
import java.lang.invoke.VarHandle;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
final class SharedBoundsBiomes {
|
||||||
|
static final int MINIMUM_CAPACITY = 1 << 16;
|
||||||
|
static final int MAXIMUM_CAPACITY = 1 << 21;
|
||||||
|
static final long BYTES_PER_ENTRY = 16L;
|
||||||
|
static final long HEAP_SHARE = 256L;
|
||||||
|
private static final VarHandle INTS = MethodHandles.arrayElementVarHandle(int[].class);
|
||||||
|
private static final VarHandle LONGS = MethodHandles.arrayElementVarHandle(long[].class);
|
||||||
|
private static final VarHandle OBJECTS = MethodHandles.arrayElementVarHandle(Object[].class);
|
||||||
|
|
||||||
|
private final Object source;
|
||||||
|
private final long[] keys;
|
||||||
|
private final Object[] values;
|
||||||
|
private final int[] stamps;
|
||||||
|
private final int mask;
|
||||||
|
|
||||||
|
SharedBoundsBiomes(Object source, int capacity) {
|
||||||
|
if (capacity < 2 || Integer.bitCount(capacity) != 1) {
|
||||||
|
throw new IllegalArgumentException("capacity must be a power of two of at least 2");
|
||||||
|
}
|
||||||
|
this.source = Objects.requireNonNull(source, "source");
|
||||||
|
this.keys = new long[capacity];
|
||||||
|
this.values = new Object[capacity];
|
||||||
|
this.stamps = new int[capacity];
|
||||||
|
this.mask = capacity - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int capacityFor(long maxMemory) {
|
||||||
|
long entries = Math.max(0L, maxMemory) / HEAP_SHARE / BYTES_PER_ENTRY;
|
||||||
|
long bounded = Math.max(MINIMUM_CAPACITY, Math.min(MAXIMUM_CAPACITY, entries));
|
||||||
|
return Integer.highestOneBit((int) bounded);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object source() {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
int slot(int x, int z) {
|
||||||
|
long mixed = key(x, z);
|
||||||
|
mixed = (mixed ^ (mixed >>> 30)) * 0xBF58476D1CE4E5B9L;
|
||||||
|
mixed = (mixed ^ (mixed >>> 27)) * 0x94D049BB133111EBL;
|
||||||
|
return (int) ((mixed ^ (mixed >>> 31)) & mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
IrisBiome get(int x, int z) {
|
||||||
|
long key = key(x, z);
|
||||||
|
int slot = slot(x, z);
|
||||||
|
int before = (int) INTS.getAcquire(stamps, slot);
|
||||||
|
if (before == 0 || (before & 1) != 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
long storedKey = (long) LONGS.getAcquire(keys, slot);
|
||||||
|
Object value = OBJECTS.getAcquire(values, slot);
|
||||||
|
VarHandle.loadLoadFence();
|
||||||
|
int after = (int) INTS.getAcquire(stamps, slot);
|
||||||
|
if (before != after || storedKey != key) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (IrisBiome) value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void put(int x, int z, IrisBiome biome) {
|
||||||
|
if (biome == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int slot = slot(x, z);
|
||||||
|
int stamp = (int) INTS.getAcquire(stamps, slot);
|
||||||
|
if ((stamp & 1) != 0 || !INTS.compareAndSet(stamps, slot, stamp, stamp + 1)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LONGS.setRelease(keys, slot, key(x, z));
|
||||||
|
OBJECTS.setRelease(values, slot, biome);
|
||||||
|
INTS.setRelease(stamps, slot, stamp + 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long key(int x, int z) {
|
||||||
|
return ((long) x << 32) | (z & 0xFFFFFFFFL);
|
||||||
|
}
|
||||||
|
}
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
package art.arcane.iris.generation.hydrology.surface;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.hydrology.HydrologyPoint;
|
||||||
|
import art.arcane.iris.generation.hydrology.RiverFootprint;
|
||||||
|
import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
|
||||||
|
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||||
|
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||||
|
import it.unimi.dsi.fastutil.longs.LongIterator;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.SplittableRandom;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class ErosionRasterGeometryTest {
|
||||||
|
private static final double EPSILON = 1.0E-9D;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rasterMatchesTheReferenceOwnersDistancesAndIterationOrder() {
|
||||||
|
SplittableRandom random = new SplittableRandom(0x5EED_E05EL);
|
||||||
|
int compared = 0;
|
||||||
|
for (int trial = 0; trial < 160; trial++) {
|
||||||
|
SurfaceCenterline centerline = randomCenterline(random);
|
||||||
|
int count = Math.max(1, centerline.size() - random.nextInt(Math.min(4, centerline.size())));
|
||||||
|
int[] radii = new int[count];
|
||||||
|
for (int station = 0; station < count; station++) {
|
||||||
|
radii[station] = 1 + random.nextInt(trial % 3 == 0 ? 48 : 12);
|
||||||
|
}
|
||||||
|
SurfaceBounds bounds = trial % 4 == 0 ? null : randomBounds(random, centerline);
|
||||||
|
Long2IntOpenHashMap expectedIndices = new Long2IntOpenHashMap();
|
||||||
|
expectedIndices.defaultReturnValue(-1);
|
||||||
|
IntArrayList expectedStations = new IntArrayList();
|
||||||
|
DoubleArrayList expectedDistances = new DoubleArrayList();
|
||||||
|
reference(centerline, radii, count, bounds, expectedIndices, expectedStations, expectedDistances);
|
||||||
|
Long2IntOpenHashMap actualIndices = new Long2IntOpenHashMap();
|
||||||
|
actualIndices.defaultReturnValue(-1);
|
||||||
|
IntArrayList actualStations = new IntArrayList();
|
||||||
|
DoubleArrayList actualDistances = new DoubleArrayList();
|
||||||
|
ErosionFieldCompiler.rasterize(centerline, radii, count, bounds, actualIndices, actualStations, actualDistances);
|
||||||
|
|
||||||
|
assertEquals(expectedIndices.size(), actualIndices.size());
|
||||||
|
LongIterator expectedKeys = expectedIndices.keySet().iterator();
|
||||||
|
LongIterator actualKeys = actualIndices.keySet().iterator();
|
||||||
|
while (expectedKeys.hasNext()) {
|
||||||
|
long expectedKey = expectedKeys.nextLong();
|
||||||
|
long actualKey = actualKeys.nextLong();
|
||||||
|
assertEquals(expectedKey, actualKey);
|
||||||
|
int expectedIndex = expectedIndices.get(expectedKey);
|
||||||
|
int actualIndex = actualIndices.get(actualKey);
|
||||||
|
assertEquals(expectedIndex, actualIndex);
|
||||||
|
assertEquals(expectedStations.getInt(expectedIndex), actualStations.getInt(actualIndex));
|
||||||
|
assertEquals(Double.doubleToLongBits(expectedDistances.getDouble(expectedIndex)),
|
||||||
|
Double.doubleToLongBits(actualDistances.getDouble(actualIndex)));
|
||||||
|
compared++;
|
||||||
|
}
|
||||||
|
assertEquals(expectedStations.size(), actualStations.size());
|
||||||
|
}
|
||||||
|
assertTrue(compared > 100_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SurfaceCenterline randomCenterline(SplittableRandom random) {
|
||||||
|
int originX = random.nextInt(-40_000, 40_000);
|
||||||
|
int originZ = random.nextInt(-40_000, 40_000);
|
||||||
|
int points = 2 + random.nextInt(6);
|
||||||
|
List<HydrologyPoint> path = new ArrayList<>(points);
|
||||||
|
int x = originX;
|
||||||
|
int z = originZ;
|
||||||
|
for (int point = 0; point < points; point++) {
|
||||||
|
path.add(new HydrologyPoint(x, 64, z));
|
||||||
|
x += random.nextInt(-90, 91);
|
||||||
|
z += random.nextInt(-90, 91);
|
||||||
|
}
|
||||||
|
return SurfaceCenterline.densify(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SurfaceBounds randomBounds(SplittableRandom random, SurfaceCenterline centerline) {
|
||||||
|
int station = random.nextInt(centerline.size());
|
||||||
|
int centerX = centerline.x()[station];
|
||||||
|
int centerZ = centerline.z()[station];
|
||||||
|
int halfWidth = 4 + random.nextInt(60);
|
||||||
|
int halfDepth = 4 + random.nextInt(60);
|
||||||
|
return new SurfaceBounds(centerX - halfWidth, centerZ - halfDepth, centerX + halfWidth, centerZ + halfDepth)
|
||||||
|
.expand(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void reference(SurfaceCenterline centerline, int[] radii, int count, SurfaceBounds samplingBounds,
|
||||||
|
Long2IntOpenHashMap indices, IntArrayList stations, DoubleArrayList distance) {
|
||||||
|
for (int station = 0; station < count; station++) {
|
||||||
|
int radius = radii[station];
|
||||||
|
int stationX = centerline.x()[station];
|
||||||
|
int stationZ = centerline.z()[station];
|
||||||
|
if (samplingBounds != null && !samplingBounds.intersects(stationX, stationZ, radius)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int minimumZ = samplingBounds == null ? -radius : Math.max(-radius, samplingBounds.minimumZ() - stationZ);
|
||||||
|
int maximumZ = samplingBounds == null ? radius : Math.min(radius, samplingBounds.maximumZ() - stationZ);
|
||||||
|
int minimumX = samplingBounds == null ? -radius : Math.max(-radius, samplingBounds.minimumX() - stationX);
|
||||||
|
int maximumX = samplingBounds == null ? radius : Math.min(radius, samplingBounds.maximumX() - stationX);
|
||||||
|
for (int deltaZ = minimumZ; deltaZ <= maximumZ; deltaZ++) {
|
||||||
|
for (int deltaX = minimumX; deltaX <= maximumX; deltaX++) {
|
||||||
|
int cellX = stationX + deltaX;
|
||||||
|
int cellZ = stationZ + deltaZ;
|
||||||
|
double cellDistance = centerline.distanceToSegment(station, cellX, cellZ, SurfaceDistanceTable.SHARED);
|
||||||
|
if (cellDistance > radius) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
long key = RiverFootprint.pack(cellX, cellZ);
|
||||||
|
int index = indices.putIfAbsent(key, stations.size());
|
||||||
|
if (index < 0) {
|
||||||
|
stations.add(station);
|
||||||
|
distance.add(cellDistance);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int existing = stations.getInt(index);
|
||||||
|
double existingDistance = distance.getDouble(index);
|
||||||
|
if (cellDistance < existingDistance - EPSILON
|
||||||
|
|| Math.abs(cellDistance - existingDistance) <= EPSILON && station > existing) {
|
||||||
|
stations.set(index, station);
|
||||||
|
distance.set(index, cellDistance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+73
-1
@@ -71,6 +71,66 @@ public class IrisComplexBoundsBiomeCacheTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void wholeColumnSamplesAreSharedAcrossThreadMemos() throws Exception {
|
||||||
|
IrisBiome first = new IrisBiome();
|
||||||
|
IrisBiome second = new IrisBiome();
|
||||||
|
ProceduralStream<IrisBiome> source = ProceduralStream.of(
|
||||||
|
(x, z) -> x.intValue() < 0 || z.intValue() < 0 ? first : second,
|
||||||
|
Interpolated.of(value -> 0D, value -> first));
|
||||||
|
try (MockedStatic<IrisServices> services = mockStatic(IrisServices.class)) {
|
||||||
|
services.when(() -> IrisServices.get(PreservationRegistry.class))
|
||||||
|
.thenReturn(mock(PreservationRegistry.class));
|
||||||
|
CachedStream2D<IrisBiome> stream = spy(new CachedStream2D<>("bounds", mock(Engine.class), source, 16));
|
||||||
|
Fixture fixture = new Fixture(stream);
|
||||||
|
IrisInterpolator interpolator = new IrisInterpolator()
|
||||||
|
.setFunction(InterpolationMethod.STARCAST_12).setHorizontalScale(1.25D);
|
||||||
|
fixture.setBounds(interpolator, first, -15.25D, 63.5D);
|
||||||
|
fixture.setBounds(interpolator, second, 7.75D, 127.25D);
|
||||||
|
Object otherThread = fixture.newGridCache();
|
||||||
|
for (int z = -2; z <= 2; z++) {
|
||||||
|
for (int x = -2; x <= 2; x++) {
|
||||||
|
fixture.sample(interpolator, x, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
long firstReads = streamReads(stream);
|
||||||
|
for (int z = -2; z <= 2; z++) {
|
||||||
|
for (int x = -2; x <= 2; x++) {
|
||||||
|
NoiseBounds expected = interpolator.interpolateBounds(x, z, (xx, zz) ->
|
||||||
|
(int) xx < 0 || (int) zz < 0
|
||||||
|
? new NoiseBounds(-15.25D, 63.5D) : new NoiseBounds(7.75D, 127.25D));
|
||||||
|
assertBits(expected, fixture.sample(otherThread, interpolator, x, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(firstReads > 0L);
|
||||||
|
assertEquals(firstReads, streamReads(stream));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void replacingAWholeColumnStreamDropsSharedSamples() throws Exception {
|
||||||
|
IrisBiome first = new IrisBiome();
|
||||||
|
IrisBiome second = new IrisBiome();
|
||||||
|
try (MockedStatic<IrisServices> services = mockStatic(IrisServices.class)) {
|
||||||
|
services.when(() -> IrisServices.get(PreservationRegistry.class))
|
||||||
|
.thenReturn(mock(PreservationRegistry.class));
|
||||||
|
Fixture fixture = new Fixture(new CachedStream2D<>("bounds", mock(Engine.class), constant(first), 16));
|
||||||
|
IrisInterpolator interpolator = new IrisInterpolator();
|
||||||
|
fixture.setBounds(interpolator, first, -10D, 20D);
|
||||||
|
fixture.setBounds(interpolator, second, 50D, 80D);
|
||||||
|
|
||||||
|
assertBits(new NoiseBounds(-10D, 20D), fixture.sample(interpolator, 0, 0));
|
||||||
|
fixture.complex.setBaseBiomeStream(new CachedStream2D<>("bounds", mock(Engine.class), constant(second), 16));
|
||||||
|
assertBits(new NoiseBounds(50D, 80D), fixture.sample(fixture.newGridCache(), interpolator, 0, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long streamReads(CachedStream2D<IrisBiome> stream) {
|
||||||
|
return mockingDetails(stream).getInvocations().stream()
|
||||||
|
.filter(invocation -> invocation.getMethod().getName().equals("get"))
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void uncachedBiomeStreamsPreserveFractionalSamplesWithinOneColumn() throws Exception {
|
public void uncachedBiomeStreamsPreserveFractionalSamplesWithinOneColumn() throws Exception {
|
||||||
IrisBiome first = new IrisBiome();
|
IrisBiome first = new IrisBiome();
|
||||||
@@ -248,7 +308,19 @@ public class IrisComplexBoundsBiomeCacheTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private NoiseBounds sample(IrisInterpolator interpolator, double x, double z) throws ReflectiveOperationException {
|
private NoiseBounds sample(IrisInterpolator interpolator, double x, double z) throws ReflectiveOperationException {
|
||||||
return (NoiseBounds) sample.invoke(complex, cache, null, interpolator, new IrisGenerator[0], x, z);
|
return sample(cache, interpolator, x, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
private NoiseBounds sample(Object gridCache, IrisInterpolator interpolator, double x, double z)
|
||||||
|
throws ReflectiveOperationException {
|
||||||
|
return (NoiseBounds) sample.invoke(complex, gridCache, null, interpolator, new IrisGenerator[0], x, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object newGridCache() throws ReflectiveOperationException {
|
||||||
|
Class<?> cacheClass = Class.forName("art.arcane.iris.generation.runtime.IrisComplex$GridBoundsCache");
|
||||||
|
Constructor<?> constructor = cacheClass.getDeclaredConstructor();
|
||||||
|
constructor.setAccessible(true);
|
||||||
|
return constructor.newInstance();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
package art.arcane.iris.generation.runtime;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class SharedBoundsBiomesTest {
|
||||||
|
private static final Object SOURCE = new Object();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void storedColumnsComeBackAndUnknownColumnsMiss() {
|
||||||
|
SharedBoundsBiomes cache = new SharedBoundsBiomes(SOURCE, 1 << 10);
|
||||||
|
IrisBiome biome = new IrisBiome();
|
||||||
|
|
||||||
|
assertNull(cache.get(12, -40));
|
||||||
|
cache.put(12, -40, biome);
|
||||||
|
|
||||||
|
assertSame(biome, cache.get(12, -40));
|
||||||
|
assertNull(cache.get(-40, 12));
|
||||||
|
assertSame(SOURCE, cache.source());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void originMissesUntilItIsStored() {
|
||||||
|
SharedBoundsBiomes cache = new SharedBoundsBiomes(SOURCE, 2);
|
||||||
|
IrisBiome biome = new IrisBiome();
|
||||||
|
|
||||||
|
assertNull(cache.get(0, 0));
|
||||||
|
cache.put(0, 0, biome);
|
||||||
|
assertSame(biome, cache.get(0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void nullBiomesAreNeverStored() {
|
||||||
|
SharedBoundsBiomes cache = new SharedBoundsBiomes(SOURCE, 2);
|
||||||
|
IrisBiome biome = new IrisBiome();
|
||||||
|
cache.put(3, 4, biome);
|
||||||
|
cache.put(3, 4, null);
|
||||||
|
|
||||||
|
assertSame(biome, cache.get(3, 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fullCoordinateDomainKeepsDistinctColumns() {
|
||||||
|
int[] coordinates = {0, 1, -1, 1 << 21, -(1 << 21), 30_000_000, -30_000_000,
|
||||||
|
Integer.MIN_VALUE, Integer.MAX_VALUE};
|
||||||
|
IrisBiome biome = new IrisBiome();
|
||||||
|
for (int x : coordinates) {
|
||||||
|
for (int z : coordinates) {
|
||||||
|
SharedBoundsBiomes cache = new SharedBoundsBiomes(SOURCE, 2);
|
||||||
|
cache.put(x, z, biome);
|
||||||
|
for (int otherX : coordinates) {
|
||||||
|
for (int otherZ : coordinates) {
|
||||||
|
if (x == otherX && z == otherZ) {
|
||||||
|
assertSame(biome, cache.get(otherX, otherZ));
|
||||||
|
} else {
|
||||||
|
assertNull(cache.get(otherX, otherZ));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void aCollidingColumnReplacesTheOldOne() {
|
||||||
|
SharedBoundsBiomes cache = new SharedBoundsBiomes(SOURCE, 2);
|
||||||
|
int colliding = 0;
|
||||||
|
for (int x = 1; x < 100_000; x++) {
|
||||||
|
if (cache.slot(x, 0) == cache.slot(0, 0)) {
|
||||||
|
colliding = x;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertNotEquals(0, colliding);
|
||||||
|
IrisBiome first = new IrisBiome();
|
||||||
|
IrisBiome second = new IrisBiome();
|
||||||
|
|
||||||
|
cache.put(0, 0, first);
|
||||||
|
cache.put(colliding, 0, second);
|
||||||
|
|
||||||
|
assertNull(cache.get(0, 0));
|
||||||
|
assertSame(second, cache.get(colliding, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void capacityFollowsTheHeapWithinItsBounds() {
|
||||||
|
long megabyte = 1024L * 1024L;
|
||||||
|
assertEquals(SharedBoundsBiomes.MINIMUM_CAPACITY, SharedBoundsBiomes.capacityFor(0L));
|
||||||
|
assertEquals(SharedBoundsBiomes.MINIMUM_CAPACITY, SharedBoundsBiomes.capacityFor(256L * megabyte));
|
||||||
|
assertEquals(1 << 20, SharedBoundsBiomes.capacityFor(6L * 1024L * megabyte));
|
||||||
|
assertEquals(SharedBoundsBiomes.MAXIMUM_CAPACITY, SharedBoundsBiomes.capacityFor(64L * 1024L * megabyte));
|
||||||
|
long previous = 0L;
|
||||||
|
for (long heap = 64L * megabyte; heap <= 128L * 1024L * megabyte; heap *= 2L) {
|
||||||
|
int capacity = SharedBoundsBiomes.capacityFor(heap);
|
||||||
|
assertEquals(1, Integer.bitCount(capacity));
|
||||||
|
assertTrue(capacity >= previous);
|
||||||
|
assertTrue((long) capacity * SharedBoundsBiomes.BYTES_PER_ENTRY <= Math.max(heap / SharedBoundsBiomes.HEAP_SHARE,
|
||||||
|
(long) SharedBoundsBiomes.MINIMUM_CAPACITY * SharedBoundsBiomes.BYTES_PER_ENTRY));
|
||||||
|
previous = capacity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void concurrentReadersNeverSeeAnotherColumnsBiome() throws InterruptedException {
|
||||||
|
SharedBoundsBiomes cache = new SharedBoundsBiomes(SOURCE, 8);
|
||||||
|
int[] coordinates = {0, 1 << 21, -(1 << 21), 30_000_000, -30_000_000,
|
||||||
|
Integer.MIN_VALUE, Integer.MAX_VALUE};
|
||||||
|
IrisBiome[][] biomes = new IrisBiome[coordinates.length][coordinates.length];
|
||||||
|
for (int x = 0; x < coordinates.length; x++) {
|
||||||
|
for (int z = 0; z < coordinates.length; z++) {
|
||||||
|
biomes[x][z] = new IrisBiome();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AtomicBoolean torn = new AtomicBoolean();
|
||||||
|
AtomicInteger hits = new AtomicInteger();
|
||||||
|
CountDownLatch start = new CountDownLatch(1);
|
||||||
|
List<Thread> threads = new ArrayList<>();
|
||||||
|
for (int worker = 0; worker < 4; worker++) {
|
||||||
|
int seed = worker;
|
||||||
|
threads.add(new Thread(() -> {
|
||||||
|
try {
|
||||||
|
start.await();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
torn.set(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long state = 0x9E3779B97F4A7C15L * (seed + 1);
|
||||||
|
for (int step = 0; step < 200_000; step++) {
|
||||||
|
state ^= state << 13;
|
||||||
|
state ^= state >>> 7;
|
||||||
|
state ^= state << 17;
|
||||||
|
int xi = (int) ((state & Long.MAX_VALUE) % coordinates.length);
|
||||||
|
int zi = (int) ((state >>> 10) % coordinates.length);
|
||||||
|
cache.put(coordinates[xi], coordinates[zi], biomes[xi][zi]);
|
||||||
|
IrisBiome actual = cache.get(coordinates[xi], coordinates[zi]);
|
||||||
|
if (actual != null) {
|
||||||
|
hits.incrementAndGet();
|
||||||
|
if (actual != biomes[xi][zi]) {
|
||||||
|
torn.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
for (Thread thread : threads) {
|
||||||
|
thread.start();
|
||||||
|
}
|
||||||
|
start.countDown();
|
||||||
|
for (Thread thread : threads) {
|
||||||
|
thread.join();
|
||||||
|
}
|
||||||
|
assertTrue(hits.get() > 0);
|
||||||
|
assertFalse(torn.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user