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:
Brian Neumann-Fopiano committed 2026-10-22 10:02:53 -04:00
1 parent af90765a92
commit 1fe26e6288
6 files changed
+568 -46

No files matched your search

@@ -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
public void uncachedBiomeStreamsPreserveFractionalSamplesWithinOneColumn() throws Exception {
IrisBiome first = new IrisBiome();
@@ -248,7 +308,19 @@ public class IrisComplexBoundsBiomeCacheTest {
}
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());
}
}