Batched terrain noise

- Hydrology grids carry uncached heights through bounded noise batches
- Pure generator layers resolve seed contexts once per batch
- Surface smoothing and volumetric terrain retain scalar output
- Provisional terrain samples stay out of routing caches
- Tests cover exact parity, scalar fallbacks, batch limits and cached misses
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-13 14:57:12 -04:00
1 parent 0e90d38acb
commit 0d34fae316
12 files changed
+820 -33

No files matched your search

@@ -1,6 +1,7 @@
package art.arcane.iris.generation.hydrology.runtime;
import art.arcane.iris.generation.hydrology.HydrologyRoutingTerrainSampler;
import art.arcane.iris.generation.stream.ProvisionalSampling;
import art.arcane.iris.generation.hydrology.HydrologyTerrainSample;
import art.arcane.iris.generation.hydrology.RiverFootprint;
import art.arcane.iris.generation.concurrent.MultiBurst;
@@ -40,6 +41,132 @@ import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
public class IrisHydrologyRoutingTerrainSamplerTest {
@Test
public void provisionalOceanClassificationIsReevaluated() {
AtomicInteger calls = new AtomicInteger();
IrisHydrologyRoutingTerrainSampler sampler = new IrisHydrologyRoutingTerrainSampler(
new IrisHydrologyRoutingTerrainSampler.Sources((x, z, height) -> {
throw new AssertionError("Classification must not load a basis");
}, (x, z) -> {
if (calls.incrementAndGet() == 1) {
ProvisionalSampling.mark();
return 100D;
}
return 40D;
}, (x, z) -> true, 60), IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(16));
assertEquals(IrisHydrologyRoutingTerrainSampler.NaturalClassification.LAND, sampler.classifyNatural(2, 3));
assertEquals(0, sampler.oceanClassificationCacheSize());
assertEquals(IrisHydrologyRoutingTerrainSampler.NaturalClassification.OCEAN, sampler.classifyNatural(2, 3));
assertEquals(1, sampler.oceanClassificationCacheSize());
}
@Test
public void bulkHeightProviderReceivesOnlyUniqueUncachedCoordinates() {
ArrayList<Long> requested = new ArrayList<>();
IrisHydrologyNaturalHeightProvider heights = new IrisHydrologyNaturalHeightProvider() {
@Override
public double sample(int x, int z) {
return 100D + Math.floorMod(x + z, 17);
}
@Override
public boolean supportsBatch() {
return true;
}
@Override
public void sampleBatch(long[] coordinates, int count, double[] target) {
for (int index = 0; index < count; index++) {
requested.add(coordinates[index]);
target[index] = sample(RiverFootprint.unpackX(coordinates[index]), RiverFootprint.unpackZ(coordinates[index]));
}
}
};
IrisHydrologyRoutingTerrainSampler sampler = new IrisHydrologyRoutingTerrainSampler(
new IrisHydrologyRoutingTerrainSampler.Sources((x, z, height) ->
new IrisHydrologyRoutingTerrainSampler.TerrainBasis(height,
HydrologyTerrainSample.openLand((int) height, 0D, "land")), heights, (x, z) -> false, 60),
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(8192));
sampler.sampleBasisWithoutSlope(-1, 5);
sampler.sampleLandHeight(6, -7);
long[] coordinates = {RiverFootprint.pack(-1, 5), RiverFootprint.pack(6, -7),
RiverFootprint.pack(-9, 19), RiverFootprint.pack(-9, 19),
RiverFootprint.pack(Integer.MAX_VALUE, Integer.MIN_VALUE)};
for (int repeat = 0; repeat < 2; repeat++) {
HydrologyTerrainSample[] samples = sampler.sampleBasisWithoutSlopeBatch(coordinates, coordinates.length);
for (int index = 0; index < coordinates.length; index++) {
assertEquals((int) heights.sample(RiverFootprint.unpackX(coordinates[index]),
RiverFootprint.unpackZ(coordinates[index])), samples[index].naturalHeight());
}
}
assertEquals(List.of(coordinates[2], coordinates[4]), requested);
}
@Test
public void gridUsesBoundedHeightSlices() {
ArrayList<Integer> sizes = new ArrayList<>();
IrisHydrologyNaturalHeightProvider heights = new IrisHydrologyNaturalHeightProvider() {
@Override
public double sample(int x, int z) {
throw new AssertionError("Expected bulk height sampling");
}
@Override
public boolean supportsBatch() {
return true;
}
@Override
public void sampleBatch(long[] coordinates, int count, double[] target) {
sizes.add(count);
for (int index = 0; index < count; index++) {
target[index] = 100D;
}
}
};
IrisHydrologyRoutingTerrainSampler sampler = new IrisHydrologyRoutingTerrainSampler(
new IrisHydrologyRoutingTerrainSampler.Sources((x, z, height) ->
new IrisHydrologyRoutingTerrainSampler.TerrainBasis(height,
HydrologyTerrainSample.openLand((int) height, 0D, "land")), heights, (x, z) -> false, 60),
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(8192));
HydrologyTerrainSample[] samples = sampler.sampleGrid(new HydrologyRoutingTerrainSampler.GridRequest(-1024, -4096, 65, 8));
assertEquals(65 * 65, samples.length);
assertEquals(List.of(4096, 260), sizes);
for (HydrologyTerrainSample sample : samples) {
assertEquals(100, sample.naturalHeight());
assertEquals(0D, sample.slope(), 0D);
}
}
@Test
public void provisionalBulkAndScalarHeightsNeverBecomeRoutingCacheEntries() {
AtomicInteger calls = new AtomicInteger();
IrisHydrologyNaturalHeightProvider heights = new IrisHydrologyNaturalHeightProvider() {
@Override
public double sample(int x, int z) {
ProvisionalSampling.mark();
return 100D + calls.incrementAndGet();
}
@Override
public boolean supportsBatch() {
return true;
}
};
IrisHydrologyRoutingTerrainSampler sampler = new IrisHydrologyRoutingTerrainSampler(
new IrisHydrologyRoutingTerrainSampler.Sources((x, z, height) ->
new IrisHydrologyRoutingTerrainSampler.TerrainBasis(height,
HydrologyTerrainSample.openLand((int) height, 0D, "land")), heights, (x, z) -> false, 60),
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(16));
long[] coordinates = {RiverFootprint.pack(4, -3)};
assertEquals(101, sampler.sampleBasisWithoutSlopeBatch(coordinates, 1)[0].naturalHeight());
assertEquals(102, sampler.sampleBasisWithoutSlopeBatch(coordinates, 1)[0].naturalHeight());
assertEquals(103, sampler.sampleBasisWithoutSlope(4, -3).naturalHeight());
assertEquals(104, sampler.sampleBasisWithoutSlope(4, -3).naturalHeight());
assertEquals(0, sampler.basisCacheSize());
assertEquals(0, sampler.naturalHeightCacheSize());
}
@Test
public void boundedBasisBatchUsesScopedWorkersAndDrainsNestedPool() throws Exception {
ThreadLocal<Integer> scope = new ThreadLocal<>();
@@ -0,0 +1,129 @@
package art.arcane.iris.generation.noise;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
public class IrisGeneratorBatchTest {
private static final double[] XS = {-0D, 0D, -12D, -5.75D, 18D, 18D, 8191.25D, -29999999.5D, 2147483647D};
private static final double[] ZS = {0D, -0D, 6D, 2.5D, -6D, -6D, -4096.75D, 29999998D, -2147483648D};
@Test
public void batchPreservesRawBitsAcrossStylesSeedsAndSurfaceDetails() {
NoiseStyle[] styles = {NoiseStyle.SIMPLEX, NoiseStyle.IRIS, NoiseStyle.FRACTAL_SMOKE,
NoiseStyle.CLOVER, NoiseStyle.CELLULAR, NoiseStyle.FLAT};
long[] seeds = {0L, -1337L, Long.MAX_VALUE - 37L, Long.MIN_VALUE + 23L};
for (NoiseStyle style : styles) {
for (double detail : new double[]{0D, 0.5D, 1D}) {
for (boolean multiply : new boolean[]{false, true}) {
IrisGenerator scalar = generator(style, detail, multiply);
IrisGenerator batch = generator(style, detail, multiply);
assertTrue(batch.supportsBatch());
double[] sampled = new double[XS.length];
for (long seed : seeds) {
batch.getHeights(XS, ZS, seed, sampled, sampled.length);
for (int index = 0; index < sampled.length; index++) {
assertEquals(style + " detail=" + detail + " seed=" + seed + " index=" + index,
Double.doubleToRawLongBits(scalar.getHeight(XS[index], ZS[index], seed)),
Double.doubleToRawLongBits(sampled[index]));
}
}
}
}
}
}
@Test
public void unroundedCompositeBatchRetainsEveryNoiseBit() {
for (NoiseStyle style : new NoiseStyle[]{NoiseStyle.SIMPLEX, NoiseStyle.IRIS, NoiseStyle.CLOVER}) {
for (double detail : new double[]{0D, 0.5D, 1D}) {
IrisGenerator scalar = generator(style, detail, false).setCliffHeightMax(0D).setCellFractureHeight(0D);
IrisGenerator batch = generator(style, detail, false).setCliffHeightMax(0D).setCellFractureHeight(0D);
double[] values = new double[XS.length];
for (long seed : new long[]{-1337L, Long.MAX_VALUE - 37L}) {
batch.getHeights(XS, ZS, seed, values, XS.length);
for (int index = 0; index < XS.length; index++) {
assertEquals(Double.doubleToRawLongBits(scalar.getHeight(XS[index], ZS[index], seed)),
Double.doubleToRawLongBits(values[index]));
}
}
}
}
}
@Test
public void resolvesLayerContextOncePerBatch() {
IrisNoiseGenerator layer = spy(new IrisNoiseGenerator().setStyle(NoiseStyle.SIMPLEX.style()));
IrisGenerator generator = new IrisGenerator().setComposite(new KList<>(layer));
double[] sampled = new double[XS.length];
generator.getHeights(XS, ZS, 99L, sampled, XS.length);
verify(layer, times(1)).getGenerator(anyLong(), isNull(), isNull());
clearInvocations(layer);
for (int index = 0; index < XS.length; index++) {
generator.getHeight(XS[index], ZS[index], 99L);
}
verify(layer, times(XS.length)).getGenerator(anyLong(), isNull(), isNull());
}
@Test
public void customGeneratorFallbackKeepsCoordinateOrder() {
List<Double> sampled = new ArrayList<>();
IrisGenerator generator = new IrisGenerator() {
@Override
public double getHeight(double x, double z, long seed) {
sampled.add(x);
return sampled.size();
}
};
assertFalse(generator.supportsBatch());
double[] values = new double[XS.length];
generator.getHeights(XS, ZS, 0L, values, values.length);
for (int index = 0; index < values.length; index++) {
assertEquals(Double.valueOf(XS[index]), sampled.get(index));
assertEquals(index + 1D, values[index], 0D);
}
assertThrows(IllegalArgumentException.class,
() -> generator.getHeights(new double[4097], new double[4097], 0L, new double[4097], 4097));
}
@Test
public void expressionsAndCustomStylesKeepScalarSampling() {
assertFalse(new IrisGenerator().setComposite(new KList<>(new IrisNoiseGenerator()
.setStyle(new IrisGeneratorStyle().setExpression("height")))).supportsBatch());
assertFalse(new IrisGenerator().setComposite(new KList<>(new IrisNoiseGenerator()
.setStyle(new IrisGeneratorStyle() {}))).supportsBatch());
}
private static IrisGenerator generator(NoiseStyle style, double detail, boolean multiply) {
IrisNoiseGenerator layer = new IrisNoiseGenerator().setStyle(style.style()).setSeed(-831L)
.setZoom(0.73D).setOffsetX(3.25D).setOffsetZ(-1.75D).setOpacity(0.7D)
.setNegative(true).setExponent(1.37D).setOffsetY(0.03D).setParametric(true);
IrisNoiseGenerator node = layer;
for (int depth = 0; depth < 5; depth++) {
IrisNoiseGenerator child = new IrisNoiseGenerator().setStyle(NoiseStyle.SIMPLEX.style())
.setSeed(depth * 7L).setOpacity(3.1D).setZoom(1.7D);
node.setFracture(new KList<>(child));
node = child;
}
return new IrisGenerator().setComposite(new KList<>(layer,
new IrisNoiseGenerator().setStyle(NoiseStyle.SIMPLEX.style()).setOpacity(0.2D)))
.setSurfaceDetail(detail).setMultiplicitive(multiply).setSeed(-93L).setZoom(1.3D)
.setOffsetX(7.25D).setOffsetZ(-9.125D).setOpacity(0.93D)
.setCliffHeightMin(3.5D).setCliffHeightMax(12.25D)
.setCliffHeightGenerator(new IrisNoiseGenerator().setStyle(NoiseStyle.SIMPLEX.style()))
.setCellFractureHeight(0.25D);
}
}
@@ -0,0 +1,78 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.generation.noise.IrisGenerator;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.IrisNoiseGenerator;
import art.arcane.iris.generation.noise.IrisShapedGeneratorStyle;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisComplexHeightBatchTest {
@Test
public void standardSurfaceGraphsCanBatchAndCoarseSamplingKeepsScalarPath() throws Exception {
IrisDimension dimension = new IrisDimension();
IrisGenerator generator = new IrisGenerator().setSurfaceDetail(0.5D)
.setComposite(new KList<>(new IrisNoiseGenerator().setStyle(NoiseStyle.FRACTAL_SMOKE.style())
.setFracture(new KList<>(new IrisNoiseGenerator().setStyle(NoiseStyle.IRIS.style())))));
IrisComplex complex = complex(dimension, generator);
assertTrue(eligible(complex));
for (int step : new int[]{2, 4, 8}) {
dimension.setTerrainSamplingStep(step);
assertFalse(eligible(complex));
}
dimension.setTerrainSamplingStep(1);
assertTrue(eligible(complex));
generator.getComposite().getFirst().setStyle(new IrisGeneratorStyle().setExpression("height"));
assertFalse(eligible(complex));
}
@Test
public void statefulBiomeAndOverlaySourcesKeepScalarPath() throws Exception {
IrisDimension dimension = new IrisDimension();
IrisComplex complex = complex(dimension, new IrisGenerator()
.setComposite(new KList<>(new IrisNoiseGenerator().setStyle(NoiseStyle.SIMPLEX.style()))));
set(complex, "reuseNaturalBaseBiome", false);
assertFalse(eligible(complex));
set(complex, "reuseNaturalBaseBiome", true);
dimension.setOverlayNoise(new KList<>(new IrisShapedGeneratorStyle()
.setGenerator(new IrisGeneratorStyle() {})));
assertFalse(eligible(complex));
dimension.getOverlayNoise().getFirst().setGenerator(NoiseStyle.SIMPLEX.style());
assertTrue(eligible(complex));
}
private static IrisComplex complex(IrisDimension dimension, IrisGenerator generator) throws Exception {
Engine engine = mock(Engine.class);
when(engine.getDimension()).thenReturn(dimension);
IrisComplex complex = mock(IrisComplex.class, CALLS_REAL_METHODS);
set(complex, "terrainEngine", engine);
set(complex, "biomeBuffetGenerators", Map.of());
set(complex, "reuseNaturalBaseBiome", true);
set(complex, "frozenGenerators", new IrisGenerator[][]{{generator}});
return complex;
}
private static boolean eligible(IrisComplex complex) throws Exception {
Method method = IrisComplex.class.getDeclaredMethod("supportsHeightBatch");
method.setAccessible(true);
return (boolean) method.invoke(complex);
}
private static void set(IrisComplex complex, String name, Object value) throws Exception {
Field field = IrisComplex.class.getDeclaredField(name);
field.setAccessible(true);
field.set(complex, value);
}
}