Merged hydrology cold start

- Shared bounds-biome memo and erosion raster without per-visit lookups
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-22 11:47:20 -04:00
commit c6d7a814e0
6 files changed
+568 -46

No files matched your search

@@ -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);
}
}
@@ -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);
}
}
}
}
}
}
@@ -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());
}
}