mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 14:00:26 +00:00
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:
1 parent
0e90d38acb
commit
0d34fae316
12 files changed
+820
-33
No files matched your search
+127
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user