mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +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
+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
|
||||
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());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user