mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-11-01 17:00:26 +00:00
Merge branch 'perf-dem' into perf-review
This commit is contained in:
commit
b10966af47
10 files changed
+628
-71
No files matched your search
@@ -91,7 +91,6 @@ import art.arcane.volmlib.util.interpolation.NoiseBoundsProvider;
|
||||
import art.arcane.volmlib.util.noise.CNG;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import art.arcane.volmlib.util.stream.interpolation.Interpolated;
|
||||
import it.unimi.dsi.fastutil.longs.Long2DoubleOpenHashMap;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
@@ -120,10 +119,11 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "sharedBoundsBiomes", "terrainProgram", "bakeCache", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyRuntime", "shoreBiomes", "waterVegetation", "effectiveChunks"})
|
||||
@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "sharedBoundsBiomes", "terrainProgram", "bakeCache", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyRuntime", "shoreBiomes", "waterVegetation", "effectiveChunks"})
|
||||
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "sharedBaseHeights", "sharedBoundsBiomes", "regionBounds", "contextRegionStream", "terrainProgram", "bakeCache", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyRuntime", "shoreBiomes", "waterVegetation", "effectiveChunks"})
|
||||
@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "sharedBaseHeights", "sharedBoundsBiomes", "regionBounds", "contextRegionStream", "terrainProgram", "bakeCache", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyRuntime", "shoreBiomes", "waterVegetation", "effectiveChunks"})
|
||||
public class IrisComplex implements DataProvider {
|
||||
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D);
|
||||
private static final long NEGATIVE_ZERO_BITS = Double.doubleToRawLongBits(-0D);
|
||||
private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
|
||||
private static final int GRID_BOUNDS_CACHE_SIZE = 8192;
|
||||
private static final int STUDIO_NOISE_CACHE_SIZE = 32_768;
|
||||
@@ -146,7 +146,6 @@ public class IrisComplex implements DataProvider {
|
||||
Comparator.nullsFirst(Comparator.naturalOrder())
|
||||
);
|
||||
private static final ThreadLocal<TerrainBatchScratch> BATCH_SCRATCH = ThreadLocal.withInitial(TerrainBatchScratch::new);
|
||||
private static final ThreadLocal<HydrologyBaseHeights> HYDROLOGY_BASE_HEIGHTS = new ThreadLocal<>();
|
||||
private static final InferredType[] INFERRED_BIOME_PREPARATION_ORDER = {
|
||||
InferredType.LAND,
|
||||
InferredType.CAVE,
|
||||
@@ -160,6 +159,7 @@ public class IrisComplex implements DataProvider {
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final int biomeBoundsSamplingStep;
|
||||
private transient volatile SharedCornerBounds sharedCornerBounds;
|
||||
private transient volatile SharedBaseHeights sharedBaseHeights;
|
||||
private transient volatile SharedBoundsBiomes sharedBoundsBiomes;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
@@ -259,6 +259,10 @@ public class IrisComplex implements DataProvider {
|
||||
private IrisBiome focusBiome;
|
||||
private IrisRegion focusRegion;
|
||||
private Map<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient RegionBounds regionBounds;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient ProceduralStream<IrisRegion> contextRegionStream;
|
||||
private Set<IrisBiome> generatorBiomes;
|
||||
@Setter(AccessLevel.NONE)
|
||||
private final transient HydrologyRuntime hydrologyRuntime;
|
||||
@@ -386,6 +390,9 @@ public class IrisComplex implements DataProvider {
|
||||
frozenGenerators[frozenIndex] = generatorGroups[frozenIndex].generators();
|
||||
}
|
||||
generatorBounds = buildGeneratorBounds(engine);
|
||||
regionBounds = focusedBiomes == null && !imageMapRuntime.has(IrisImageMapApplication.BIOME)
|
||||
? RegionBounds.compile(preparedRegions, frozenInterpolators.length, new BoundsReach(), this::groupBounds)
|
||||
: null;
|
||||
biomeBuffetGenerators = compileBuffetGeneratorGroups();
|
||||
chunkHeightBatch = engine.getDimension().getTerrainSamplingStep() == 1 && biomeBuffetGenerators.isEmpty()
|
||||
&& (biomeBuffet == null || biomeBuffet.cells().isEmpty());
|
||||
@@ -417,20 +424,21 @@ public class IrisComplex implements DataProvider {
|
||||
: RegionSelection.select(new RegionSelection.Options(
|
||||
engine.getDimension(), data, regionStyleStream, imageMapRuntime, null));
|
||||
regionStream = focusedRegions != null ? focusedRegions : new BakedStream<>(bakeCache, BakePlane.REGION, regionSource);
|
||||
caveSource = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||
contextRegionStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z));
|
||||
caveSource = contextRegionStream
|
||||
.convert((r) -> createInferredBiomeStream(r, InferredType.CAVE))
|
||||
.convertAware2D(ProceduralStream::get);
|
||||
caveBiomeStream = new BakedStream<>(bakeCache, BakePlane.CAVE_BIOME, caveSource);
|
||||
inferredStreams.put(InferredType.CAVE, caveBiomeStream);
|
||||
landBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||
landBiomeStream = contextRegionStream
|
||||
.convert((r) -> createInferredBiomeStream(r, InferredType.LAND))
|
||||
.convertAware2D(ProceduralStream::get);
|
||||
inferredStreams.put(InferredType.LAND, landBiomeStream);
|
||||
seaBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||
seaBiomeStream = contextRegionStream
|
||||
.convert((r) -> createInferredBiomeStream(r, InferredType.SEA))
|
||||
.convertAware2D(ProceduralStream::get);
|
||||
inferredStreams.put(InferredType.SEA, seaBiomeStream);
|
||||
shoreBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
|
||||
shoreBiomeStream = contextRegionStream
|
||||
.convert((r) -> createInferredBiomeStream(r, InferredType.SHORE))
|
||||
.convertAware2D(ProceduralStream::get);
|
||||
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
|
||||
@@ -812,17 +820,35 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
return;
|
||||
}
|
||||
SharedBaseHeights memo = sharedBaseHeights();
|
||||
TerrainBatchScratch scratch = BATCH_SCRATCH.get();
|
||||
int mark = scratch.mark();
|
||||
int indexMark = scratch.indexMark();
|
||||
try {
|
||||
double[] batchXs = scratch.buffer(count);
|
||||
double[] batchZs = scratch.buffer(count);
|
||||
double[] computed = scratch.buffer(count);
|
||||
int[] slots = scratch.indices(count);
|
||||
int missing = 0;
|
||||
for (int index = 0; index < count; index++) {
|
||||
batchXs[index] = xs[from + index];
|
||||
batchZs[index] = zs[from + index];
|
||||
int x = xs[from + index];
|
||||
int z = zs[from + index];
|
||||
double known = memo.get(x, z);
|
||||
if (Double.isNaN(known)) {
|
||||
batchXs[missing] = x;
|
||||
batchZs[missing] = z;
|
||||
slots[missing++] = index;
|
||||
} else {
|
||||
heights[index] = known;
|
||||
}
|
||||
}
|
||||
if (missing > 0) {
|
||||
batchBaseHeights(scratch, batchXs, batchZs, missing, computed);
|
||||
for (int index = 0; index < missing; index++) {
|
||||
heights[slots[index]] = computed[index];
|
||||
memo.put((int) batchXs[index], (int) batchZs[index], computed[index]);
|
||||
}
|
||||
}
|
||||
batchBaseHeights(scratch, batchXs, batchZs, count, heights);
|
||||
} finally {
|
||||
scratch.reset(mark);
|
||||
scratch.resetIndices(indexMark);
|
||||
@@ -830,23 +856,9 @@ public class IrisComplex implements DataProvider {
|
||||
if (baseOnly || terrain3D == null && transitionDisplacement == null) {
|
||||
return;
|
||||
}
|
||||
Long2DoubleOpenHashMap known = new Long2DoubleOpenHashMap(count);
|
||||
known.defaultReturnValue(Double.NaN);
|
||||
for (int index = 0; index < count; index++) {
|
||||
known.put(hydrologyColumnKey(xs[from + index], zs[from + index]), heights[index]);
|
||||
}
|
||||
HYDROLOGY_BASE_HEIGHTS.set(new HydrologyBaseHeights(this, known));
|
||||
try {
|
||||
for (int index = 0; index < count; index++) {
|
||||
heights[index] = naturalHeightStream.getDouble(xs[from + index], zs[from + index]);
|
||||
}
|
||||
} finally {
|
||||
HYDROLOGY_BASE_HEIGHTS.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private static long hydrologyColumnKey(int x, int z) {
|
||||
return ((long) x << 32) | (z & 0xFFFFFFFFL);
|
||||
}
|
||||
|
||||
private void bakeBaseHeights(int chunkX, int chunkZ, long[] missing, double[] values) {
|
||||
@@ -1082,15 +1094,21 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
|
||||
private double sampleUnblendedNaturalTerrainHeight(double x, double z) {
|
||||
HydrologyBaseHeights batched = HYDROLOGY_BASE_HEIGHTS.get();
|
||||
if (batched != null && batched.owner() == this && (int) x == x && (int) z == z) {
|
||||
double known = batched.heights().get(hydrologyColumnKey((int) x, (int) z));
|
||||
if (!memoColumn(x) || !memoColumn(z)) {
|
||||
return imageMapRuntime.sampleTerrainHeight(x, z, proceduralTerrainHeight.sample(x, z));
|
||||
}
|
||||
SharedBaseHeights memo = sharedBaseHeights();
|
||||
double known = memo.get((int) x, (int) z);
|
||||
if (!Double.isNaN(known)) {
|
||||
return known;
|
||||
}
|
||||
double height = imageMapRuntime.sampleTerrainHeight(x, z, proceduralTerrainHeight.sample(x, z));
|
||||
memo.put((int) x, (int) z, height);
|
||||
return height;
|
||||
}
|
||||
double proceduralHeight = proceduralTerrainHeight.sample(x, z);
|
||||
return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight);
|
||||
|
||||
private static boolean memoColumn(double coordinate) {
|
||||
return (int) coordinate == coordinate && Double.doubleToRawLongBits(coordinate) != NEGATIVE_ZERO_BITS;
|
||||
}
|
||||
|
||||
static double calculateNaturalSlope(double naturalHeight, double easternHeight, double southernHeight) {
|
||||
@@ -1885,7 +1903,7 @@ public class IrisComplex implements DataProvider {
|
||||
int grid = biomeBoundsSamplingStep;
|
||||
GridBoundsCache cache = gridBoundsCache.get();
|
||||
if (grid <= 1) {
|
||||
return sampleBoundsRaw(cache, engine, interpolator, generators, x, z);
|
||||
return sampleBoundsRaw(cache, engine, interpolator, interpolatorIndex, generators, x, z);
|
||||
}
|
||||
|
||||
int xi = (int) Math.floor(x);
|
||||
@@ -1941,7 +1959,7 @@ public class IrisComplex implements DataProvider {
|
||||
if (GenerationCounter.ENABLED) {
|
||||
GenerationCounter.CORNER_STARCASTS.increment();
|
||||
}
|
||||
packed = computePackedBounds(cache, engine, interpolator, generators, gx, gz);
|
||||
packed = computePackedBounds(cache, engine, interpolator, interpolatorIndex, generators, gx, gz);
|
||||
if (!Double.isFinite(boundsLow(packed)) || !Double.isFinite(boundsHigh(packed))) {
|
||||
// A non-finite corner is a transient sampling fault, not a fact about the terrain:
|
||||
// hand it back uncached so the next sample recomputes it instead of pinning it.
|
||||
@@ -1972,6 +1990,20 @@ public class IrisComplex implements DataProvider {
|
||||
return shared;
|
||||
}
|
||||
|
||||
private SharedBaseHeights sharedBaseHeights() {
|
||||
SharedBaseHeights shared = sharedBaseHeights;
|
||||
if (shared == null) {
|
||||
synchronized (this) {
|
||||
shared = sharedBaseHeights;
|
||||
if (shared == null) {
|
||||
shared = new SharedBaseHeights(SharedBaseHeights.capacityFor(Runtime.getRuntime().maxMemory()));
|
||||
sharedBaseHeights = shared;
|
||||
}
|
||||
}
|
||||
}
|
||||
return shared;
|
||||
}
|
||||
|
||||
private SharedBoundsBiomes sharedBoundsBiomes(ProceduralStream<IrisBiome> source) {
|
||||
SharedBoundsBiomes shared = sharedBoundsBiomes;
|
||||
if (shared != null && shared.source() == source) {
|
||||
@@ -1987,8 +2019,9 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private long computePackedBounds(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, IrisGenerator[] generators, int gx, int gz) {
|
||||
NoiseBounds bounds = sampleBoundsRaw(cache, engine, interpolator, generators, gx, gz);
|
||||
private long computePackedBounds(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, int interpolatorIndex,
|
||||
IrisGenerator[] generators, int gx, int gz) {
|
||||
NoiseBounds bounds = sampleBoundsRaw(cache, engine, interpolator, interpolatorIndex, generators, gx, gz);
|
||||
return (((long) Float.floatToRawIntBits((float) bounds.min())) << 32) | (Float.floatToRawIntBits((float) bounds.max()) & 0xFFFFFFFFL);
|
||||
}
|
||||
|
||||
@@ -2006,10 +2039,11 @@ public class IrisComplex implements DataProvider {
|
||||
return a + ((b - a) * fz);
|
||||
}
|
||||
|
||||
private NoiseBounds sampleBoundsRaw(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, IrisGenerator[] generators, double x, double z) {
|
||||
private NoiseBounds sampleBoundsRaw(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, int interpolatorIndex,
|
||||
IrisGenerator[] generators, double x, double z) {
|
||||
IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds = generatorBounds.get(interpolator);
|
||||
BoundsSampler sampler = cache.sampler.isInUse() ? new BoundsSampler() : cache.sampler;
|
||||
sampler.bind(this, engine, generators, cachedBounds);
|
||||
sampler.bind(this, engine, interpolatorIndex, generators, cachedBounds);
|
||||
|
||||
try {
|
||||
return interpolator.interpolateBounds(x, z, sampler);
|
||||
@@ -2297,6 +2331,11 @@ public class IrisComplex implements DataProvider {
|
||||
return reached;
|
||||
}
|
||||
|
||||
private NoiseBounds groupBounds(int group, IrisBiome biome) {
|
||||
GeneratorBounds cached = generatorBounds.get(frozenInterpolators[group]).get(biome);
|
||||
return (cached == null ? computeGeneratorBounds(frozenGenerators[group], biome) : cached).noiseBounds;
|
||||
}
|
||||
|
||||
static double averageGeneratorHeights(
|
||||
IrisGenerator[] generators,
|
||||
double low,
|
||||
@@ -2604,6 +2643,32 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private final class BoundsReach implements RegionBounds.Reach {
|
||||
@Override
|
||||
public List<IrisBiome> picks(IrisRegion region, InferredType type) {
|
||||
return loadInferredBiomes(biomeKeys(region, type), type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<IrisBiome> descendants(IrisBiome biome) {
|
||||
Set<IrisBiome> reached = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
ArrayDeque<IrisBiome> pending = new ArrayDeque<>();
|
||||
pending.add(biome);
|
||||
while (!pending.isEmpty()) {
|
||||
IrisBiome next = pending.poll();
|
||||
if (!reached.add(next)) {
|
||||
continue;
|
||||
}
|
||||
for (IrisBiome child : next.getRealChildren(IrisComplex.this)) {
|
||||
if (child != null) {
|
||||
pending.add(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
return reached;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable per-thread bounds provider. Holds the biome memo and the lazily computed generator
|
||||
* bounds for one sampleBoundsRaw pass so the pass allocates nothing; {@link #bind}
|
||||
@@ -2619,9 +2684,12 @@ public class IrisComplex implements DataProvider {
|
||||
private final IdentityHashMap<IrisBiome, GeneratorBounds> localBounds = new IdentityHashMap<>(8);
|
||||
private IrisComplex complex;
|
||||
private Engine engine;
|
||||
private int group;
|
||||
private IrisGenerator[] generators;
|
||||
private IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds;
|
||||
private SharedBoundsBiomes sharedBiomes;
|
||||
private BakedStream<IrisBiome> bakedBiomes;
|
||||
private RegionBounds regional;
|
||||
private boolean integerCoordinates;
|
||||
private boolean inUse;
|
||||
|
||||
@@ -2629,14 +2697,19 @@ public class IrisComplex implements DataProvider {
|
||||
return inUse;
|
||||
}
|
||||
|
||||
private void bind(IrisComplex complex, Engine engine, IrisGenerator[] generators, IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds) {
|
||||
private void bind(IrisComplex complex, Engine engine, int group, IrisGenerator[] generators,
|
||||
IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds) {
|
||||
this.complex = complex;
|
||||
this.engine = engine;
|
||||
this.group = group;
|
||||
this.generators = generators;
|
||||
this.cachedBounds = cachedBounds;
|
||||
this.integerCoordinates = complex.baseBiomeStream instanceof BakedStream<?>
|
||||
|| complex.baseBiomeStream instanceof CachedStream2D<?>;
|
||||
this.sharedBiomes = integerCoordinates ? complex.sharedBoundsBiomes(complex.baseBiomeStream) : null;
|
||||
this.bakedBiomes = complex.regionBounds != null && complex.baseBiomeStream instanceof BakedStream<IrisBiome> baked
|
||||
? baked : null;
|
||||
this.regional = bakedBiomes == null ? null : complex.regionBounds;
|
||||
this.inUse = true;
|
||||
if (sampleSource.get() != complex.baseBiomeStream) {
|
||||
sampleCache.clear();
|
||||
@@ -2654,6 +2727,8 @@ public class IrisComplex implements DataProvider {
|
||||
generators = null;
|
||||
cachedBounds = null;
|
||||
sharedBiomes = null;
|
||||
bakedBiomes = null;
|
||||
regional = null;
|
||||
inUse = false;
|
||||
}
|
||||
|
||||
@@ -2662,25 +2737,23 @@ public class IrisComplex implements DataProvider {
|
||||
try {
|
||||
double sampleX = integerCoordinates ? (int) xx : xx;
|
||||
double sampleZ = integerCoordinates ? (int) zz : zz;
|
||||
IrisBiome bx = sampleCache.get(sampleX, sampleZ);
|
||||
if (bx == null) {
|
||||
bx = sharedBiomes == null ? null : sharedBiomes.get((int) sampleX, (int) sampleZ);
|
||||
if (bx == null) {
|
||||
Object known = sampleCache.get(sampleX, sampleZ);
|
||||
if (known == null && sharedBiomes != null) {
|
||||
known = sharedBiomes.get((int) sampleX, (int) sampleZ);
|
||||
if (known != null) {
|
||||
sampleCache.put(sampleX, sampleZ, known);
|
||||
}
|
||||
}
|
||||
if (known instanceof IrisBiome biome) {
|
||||
return bounds(biome);
|
||||
}
|
||||
ChunkBakeCache.enterSparse();
|
||||
try {
|
||||
bx = complex.baseBiomeStream.get(xx, zz);
|
||||
return regional == null ? bounds(remember(sampleX, sampleZ, complex.baseBiomeStream.get(xx, zz)))
|
||||
: regionalBounds(known, sampleX, sampleZ);
|
||||
} finally {
|
||||
ChunkBakeCache.exitSparse();
|
||||
}
|
||||
if (sharedBiomes != null) {
|
||||
sharedBiomes.put((int) sampleX, (int) sampleZ, bx);
|
||||
}
|
||||
}
|
||||
sampleCache.put(sampleX, sampleZ, bx);
|
||||
}
|
||||
|
||||
GeneratorBounds bounds = complex.resolveGeneratorBounds(engine, generators, bx, cachedBounds, localBounds);
|
||||
return bounds.noiseBounds;
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
long now = System.currentTimeMillis();
|
||||
@@ -2696,6 +2769,49 @@ public class IrisComplex implements DataProvider {
|
||||
|
||||
return ZERO_NOISE_BOUNDS;
|
||||
}
|
||||
|
||||
private NoiseBounds regionalBounds(Object known, double x, double z) {
|
||||
RegionBounds.Pick pick = known instanceof RegionBounds.Pick picked ? picked : null;
|
||||
IrisRegion region = pick != null ? pick.region() : known instanceof IrisRegion cached ? cached : null;
|
||||
if (region == null) {
|
||||
IrisBiome baked = bakedBiomes.baked((int) x, (int) z);
|
||||
if (baked != null) {
|
||||
return bounds(remember(x, z, baked));
|
||||
}
|
||||
region = remember(x, z, complex.contextRegionStream.get(x, z));
|
||||
}
|
||||
RegionBounds.Constants constants = regional.of(region);
|
||||
NoiseBounds constant = constants == null ? null : constants.any(group);
|
||||
if (constant != null) {
|
||||
return constant;
|
||||
}
|
||||
InferredType type = pick != null ? pick.type() : complex.bridgeStream.get(x, z);
|
||||
constant = constants == null ? null : constants.typed(type, group);
|
||||
if (constant != null) {
|
||||
return constant;
|
||||
}
|
||||
IrisBiome picked = pick != null ? pick.biome() : complex.createInferredBiomeStream(region, type).get(x, z);
|
||||
constant = regional.pick(picked, group);
|
||||
if (constant != null) {
|
||||
if (pick == null) {
|
||||
remember(x, z, new RegionBounds.Pick(region, type, picked));
|
||||
}
|
||||
return constant;
|
||||
}
|
||||
return bounds(remember(x, z, complex.implode(picked, x, z)));
|
||||
}
|
||||
|
||||
private NoiseBounds bounds(IrisBiome biome) {
|
||||
return complex.resolveGeneratorBounds(engine, generators, biome, cachedBounds, localBounds).noiseBounds;
|
||||
}
|
||||
|
||||
private <T> T remember(double x, double z, T sample) {
|
||||
if (sharedBiomes != null) {
|
||||
sharedBiomes.put((int) x, (int) z, sample);
|
||||
}
|
||||
sampleCache.put(x, z, sample);
|
||||
return sample;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2705,13 +2821,13 @@ public class IrisComplex implements DataProvider {
|
||||
private static final class CoordinateBiomeCache {
|
||||
private final long[] xBits;
|
||||
private final long[] zBits;
|
||||
private final IrisBiome[] values;
|
||||
private final Object[] values;
|
||||
private final int mask;
|
||||
|
||||
private CoordinateBiomeCache(int capacity) {
|
||||
xBits = new long[capacity];
|
||||
zBits = new long[capacity];
|
||||
values = new IrisBiome[capacity];
|
||||
values = new Object[capacity];
|
||||
mask = capacity - 1;
|
||||
}
|
||||
|
||||
@@ -2719,20 +2835,20 @@ public class IrisComplex implements DataProvider {
|
||||
Arrays.fill(values, null);
|
||||
}
|
||||
|
||||
private IrisBiome get(double x, double z) {
|
||||
private Object get(double x, double z) {
|
||||
long xb = Double.doubleToLongBits(x);
|
||||
long zb = Double.doubleToLongBits(z);
|
||||
int slot = mix(xb, zb) & mask;
|
||||
return xBits[slot] == xb && zBits[slot] == zb ? values[slot] : null;
|
||||
}
|
||||
|
||||
private void put(double x, double z, IrisBiome biome) {
|
||||
private void put(double x, double z, Object sample) {
|
||||
long xb = Double.doubleToLongBits(x);
|
||||
long zb = Double.doubleToLongBits(z);
|
||||
int slot = mix(xb, zb) & mask;
|
||||
xBits[slot] = xb;
|
||||
zBits[slot] = zb;
|
||||
values[slot] = biome;
|
||||
values[slot] = sample;
|
||||
}
|
||||
|
||||
private int mix(long xb, long zb) {
|
||||
@@ -2784,7 +2900,4 @@ public class IrisComplex implements DataProvider {
|
||||
transitionDisplacement.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private record HydrologyBaseHeights(IrisComplex owner, Long2DoubleOpenHashMap heights) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.terrain.InferredType;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.volmlib.util.interpolation.NoiseBounds;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
final class RegionBounds {
|
||||
private final Map<IrisRegion, Constants> regions;
|
||||
private final Map<IrisBiome, NoiseBounds[]> picks;
|
||||
|
||||
private RegionBounds(Map<IrisRegion, Constants> regions, Map<IrisBiome, NoiseBounds[]> picks) {
|
||||
this.regions = regions;
|
||||
this.picks = picks;
|
||||
}
|
||||
|
||||
static RegionBounds compile(Collection<IrisRegion> regions, int groups, Reach reach, GroupBounds bounds) {
|
||||
IdentityHashMap<IrisRegion, Constants> compiled = new IdentityHashMap<>(Math.max(16, regions.size() * 2));
|
||||
IdentityHashMap<IrisBiome, NoiseBounds[]> picks = new IdentityHashMap<>();
|
||||
for (IrisRegion region : regions) {
|
||||
if (compiled.containsKey(region)) {
|
||||
continue;
|
||||
}
|
||||
NoiseBounds[] land = typeConstants(reach.picks(region, InferredType.LAND), groups, reach, bounds, picks);
|
||||
NoiseBounds[] sea = typeConstants(reach.picks(region, InferredType.SEA), groups, reach, bounds, picks);
|
||||
NoiseBounds[] any = new NoiseBounds[groups];
|
||||
for (int group = 0; group < groups; group++) {
|
||||
any[group] = land[group] != null && sea[group] != null && sameBits(land[group], sea[group]) ? land[group] : null;
|
||||
}
|
||||
compiled.put(region, new Constants(any, land, sea));
|
||||
}
|
||||
return new RegionBounds(compiled, picks);
|
||||
}
|
||||
|
||||
Constants of(IrisRegion region) {
|
||||
return regions.get(region);
|
||||
}
|
||||
|
||||
NoiseBounds pick(IrisBiome picked, int group) {
|
||||
NoiseBounds[] constants = picks.get(picked);
|
||||
return constants == null ? null : constants[group];
|
||||
}
|
||||
|
||||
private static NoiseBounds[] typeConstants(List<IrisBiome> picked, int groups, Reach reach, GroupBounds bounds,
|
||||
Map<IrisBiome, NoiseBounds[]> picks) {
|
||||
if (picked.isEmpty()) {
|
||||
return new NoiseBounds[groups];
|
||||
}
|
||||
Set<IrisBiome> reached = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
for (IrisBiome biome : picked) {
|
||||
Collection<IrisBiome> descendants = reach.descendants(biome);
|
||||
reached.addAll(descendants);
|
||||
if (!picks.containsKey(biome)) {
|
||||
picks.put(biome, constants(descendants, groups, bounds));
|
||||
}
|
||||
}
|
||||
return constants(reached, groups, bounds);
|
||||
}
|
||||
|
||||
private static NoiseBounds[] constants(Collection<IrisBiome> biomes, int groups, GroupBounds bounds) {
|
||||
NoiseBounds[] constants = new NoiseBounds[groups];
|
||||
for (int group = 0; group < groups; group++) {
|
||||
constants[group] = constant(biomes, group, bounds);
|
||||
}
|
||||
return constants;
|
||||
}
|
||||
|
||||
private static NoiseBounds constant(Collection<IrisBiome> biomes, int group, GroupBounds bounds) {
|
||||
NoiseBounds first = null;
|
||||
for (IrisBiome biome : biomes) {
|
||||
NoiseBounds next = bounds.of(group, biome);
|
||||
if (first == null) {
|
||||
first = next;
|
||||
} else if (!sameBits(first, next)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
private static boolean sameBits(NoiseBounds first, NoiseBounds second) {
|
||||
return Double.doubleToRawLongBits(first.min()) == Double.doubleToRawLongBits(second.min())
|
||||
&& Double.doubleToRawLongBits(first.max()) == Double.doubleToRawLongBits(second.max());
|
||||
}
|
||||
|
||||
record Constants(NoiseBounds[] any, NoiseBounds[] land, NoiseBounds[] sea) {
|
||||
NoiseBounds any(int group) {
|
||||
return any[group];
|
||||
}
|
||||
|
||||
NoiseBounds typed(InferredType type, int group) {
|
||||
return switch (type) {
|
||||
case LAND -> land[group];
|
||||
case SEA -> sea[group];
|
||||
case SHORE, CAVE -> null;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
record Pick(IrisRegion region, InferredType type, IrisBiome biome) {
|
||||
}
|
||||
|
||||
interface Reach {
|
||||
List<IrisBiome> picks(IrisRegion region, InferredType type);
|
||||
|
||||
Collection<IrisBiome> descendants(IrisBiome biome);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface GroupBounds {
|
||||
NoiseBounds of(int group, IrisBiome biome);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.VarHandle;
|
||||
|
||||
final class SharedBaseHeights {
|
||||
static final int MINIMUM_CAPACITY = 1 << 16;
|
||||
static final int MAXIMUM_CAPACITY = 1 << 21;
|
||||
static final long BYTES_PER_ENTRY = 20L;
|
||||
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 final long[] keys;
|
||||
private final long[] values;
|
||||
private final int[] stamps;
|
||||
private final int mask;
|
||||
|
||||
SharedBaseHeights(int capacity) {
|
||||
if (capacity < 2 || Integer.bitCount(capacity) != 1) {
|
||||
throw new IllegalArgumentException("capacity must be a power of two of at least 2");
|
||||
}
|
||||
this.keys = new long[capacity];
|
||||
this.values = new long[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);
|
||||
}
|
||||
|
||||
double get(int x, int z) {
|
||||
long key = key(x, z);
|
||||
int slot = slot(key);
|
||||
int before = (int) INTS.getAcquire(stamps, slot);
|
||||
if (before == 0 || (before & 1) != 0) {
|
||||
return Double.NaN;
|
||||
}
|
||||
long storedKey = (long) LONGS.getAcquire(keys, slot);
|
||||
long value = (long) LONGS.getAcquire(values, slot);
|
||||
VarHandle.loadLoadFence();
|
||||
int after = (int) INTS.getAcquire(stamps, slot);
|
||||
if (before != after || storedKey != key) {
|
||||
return Double.NaN;
|
||||
}
|
||||
return Double.longBitsToDouble(value);
|
||||
}
|
||||
|
||||
void put(int x, int z, double height) {
|
||||
if (!Double.isFinite(height)) {
|
||||
return;
|
||||
}
|
||||
long key = key(x, z);
|
||||
int slot = slot(key);
|
||||
int stamp = (int) INTS.getAcquire(stamps, slot);
|
||||
if ((stamp & 1) != 0 || !INTS.compareAndSet(stamps, slot, stamp, stamp + 1)) {
|
||||
return;
|
||||
}
|
||||
LONGS.setRelease(keys, slot, key);
|
||||
LONGS.setRelease(values, slot, Double.doubleToRawLongBits(height));
|
||||
INTS.setRelease(stamps, slot, stamp + 2);
|
||||
}
|
||||
|
||||
private int slot(long key) {
|
||||
long mixed = (key ^ (key >>> 30)) * 0xBF58476D1CE4E5B9L;
|
||||
mixed = (mixed ^ (mixed >>> 27)) * 0x94D049BB133111EBL;
|
||||
return (int) ((mixed ^ (mixed >>> 31)) & mask);
|
||||
}
|
||||
|
||||
private static long key(int x, int z) {
|
||||
return ((long) x << 32) | (z & 0xFFFFFFFFL);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
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;
|
||||
@@ -49,7 +47,7 @@ final class SharedBoundsBiomes {
|
||||
return (int) ((mixed ^ (mixed >>> 31)) & mask);
|
||||
}
|
||||
|
||||
IrisBiome get(int x, int z) {
|
||||
Object get(int x, int z) {
|
||||
long key = key(x, z);
|
||||
int slot = slot(x, z);
|
||||
int before = (int) INTS.getAcquire(stamps, slot);
|
||||
@@ -63,11 +61,11 @@ final class SharedBoundsBiomes {
|
||||
if (before != after || storedKey != key) {
|
||||
return null;
|
||||
}
|
||||
return (IrisBiome) value;
|
||||
return value;
|
||||
}
|
||||
|
||||
void put(int x, int z, IrisBiome biome) {
|
||||
if (biome == null) {
|
||||
void put(int x, int z, Object sample) {
|
||||
if (sample == null) {
|
||||
return;
|
||||
}
|
||||
int slot = slot(x, z);
|
||||
@@ -76,7 +74,7 @@ final class SharedBoundsBiomes {
|
||||
return;
|
||||
}
|
||||
LONGS.setRelease(keys, slot, key(x, z));
|
||||
OBJECTS.setRelease(values, slot, biome);
|
||||
OBJECTS.setRelease(values, slot, sample);
|
||||
INTS.setRelease(stamps, slot, stamp + 2);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,13 @@ public final class BakedStream<T> extends BasicStream<T> implements ChunkFillabl
|
||||
return value;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public T baked(int blockX, int blockZ) {
|
||||
int index = ((blockZ & 15) << 4) | (blockX & 15);
|
||||
ChunkPlanes.Plane values = cache.planeIfPresent(plane, blockX >> 4, blockZ >> 4);
|
||||
return values != null && values.ready(index) ? (T) values.objectAt(index) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T get(double x, double y, double z) {
|
||||
return scalar.get(x, y, z);
|
||||
|
||||
@@ -22,11 +22,13 @@ import org.junit.rules.TemporaryFolder;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.SplittableRandom;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Answers.CALLS_REAL_METHODS;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -81,6 +83,24 @@ public class IrisComplexBakeParityTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void regionConstantBoundsEqualFullBiomeBoundsBitForBit() throws Exception {
|
||||
try (Statics ignored = new Statics(); Fixture shortcut = new Fixture(pack()); Fixture full = new Fixture(pack())) {
|
||||
Field regionBounds = IrisComplex.class.getDeclaredField("regionBounds");
|
||||
regionBounds.setAccessible(true);
|
||||
assertNotNull(regionBounds.get(shortcut.complex));
|
||||
regionBounds.set(full.complex, null);
|
||||
SplittableRandom random = new SplittableRandom(0x5EEDL);
|
||||
for (int column = 0; column < 2048; column++) {
|
||||
int x = random.nextInt(-6400, 6400);
|
||||
int z = random.nextInt(-6400, 6400);
|
||||
assertEquals("column " + x + "," + z,
|
||||
Double.doubleToRawLongBits(full.complex.getBaseTerrainHeightStream().getDouble(x, z)),
|
||||
Double.doubleToRawLongBits(shortcut.complex.getBaseTerrainHeightStream().getDouble(x, z)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void batchedChunksEvaluateExactlyTheScalarGeneratorSet() throws Exception {
|
||||
try (Statics ignored = new Statics(); Fixture batched = new Fixture(pack()); Fixture scalar = new Fixture(pack())) {
|
||||
|
||||
+4
-4
@@ -243,7 +243,7 @@ public class IrisComplexBoundsBiomeCacheTest {
|
||||
constructor.setAccessible(true);
|
||||
Object cache = constructor.newInstance(4096);
|
||||
Method get = type.getDeclaredMethod("get", double.class, double.class);
|
||||
Method put = type.getDeclaredMethod("put", double.class, double.class, IrisBiome.class);
|
||||
Method put = type.getDeclaredMethod("put", double.class, double.class, Object.class);
|
||||
get.setAccessible(true);
|
||||
put.setAccessible(true);
|
||||
IrisBiome first = new IrisBiome();
|
||||
@@ -266,7 +266,7 @@ public class IrisComplexBoundsBiomeCacheTest {
|
||||
}
|
||||
Field values = type.getDeclaredField("values");
|
||||
values.setAccessible(true);
|
||||
assertEquals(4096, ((IrisBiome[]) values.get(cache)).length);
|
||||
assertEquals(4096, ((Object[]) values.get(cache)).length);
|
||||
}
|
||||
|
||||
private static ProceduralStream<IrisBiome> constant(IrisBiome biome) {
|
||||
@@ -295,7 +295,7 @@ public class IrisComplexBoundsBiomeCacheTest {
|
||||
constructor.setAccessible(true);
|
||||
cache = constructor.newInstance();
|
||||
sample = IrisComplex.class.getDeclaredMethod("sampleBoundsRaw", cacheClass, Engine.class,
|
||||
IrisInterpolator.class, IrisGenerator[].class, double.class, double.class);
|
||||
IrisInterpolator.class, int.class, IrisGenerator[].class, double.class, double.class);
|
||||
sample.setAccessible(true);
|
||||
boundsConstructor = Class.forName("art.arcane.iris.generation.runtime.IrisComplex$GeneratorBounds")
|
||||
.getDeclaredConstructor(double.class, double.class);
|
||||
@@ -313,7 +313,7 @@ public class IrisComplexBoundsBiomeCacheTest {
|
||||
|
||||
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);
|
||||
return (NoiseBounds) sample.invoke(complex, gridCache, null, interpolator, 0, new IrisGenerator[0], x, z);
|
||||
}
|
||||
|
||||
private Object newGridCache() throws ReflectiveOperationException {
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.terrain.InferredType;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.volmlib.util.interpolation.NoiseBounds;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
|
||||
public class RegionBoundsTest {
|
||||
private static final NoiseBounds FLAT = new NoiseBounds(0D, 0D);
|
||||
private static final NoiseBounds HILLS = new NoiseBounds(4D, 30D);
|
||||
|
||||
@Test
|
||||
public void groupsEveryReachableBiomeAgreesOnAreConstantForTheRegion() {
|
||||
IrisBiome plains = new IrisBiome();
|
||||
IrisBiome forest = new IrisBiome();
|
||||
IrisBiome ocean = new IrisBiome();
|
||||
IrisRegion region = new IrisRegion();
|
||||
Map<IrisBiome, NoiseBounds[]> bounds = new IdentityHashMap<>();
|
||||
bounds.put(plains, new NoiseBounds[]{FLAT, HILLS});
|
||||
bounds.put(forest, new NoiseBounds[]{new NoiseBounds(0D, 0D), new NoiseBounds(2D, 30D)});
|
||||
bounds.put(ocean, new NoiseBounds[]{FLAT, HILLS});
|
||||
|
||||
RegionBounds compiled = RegionBounds.compile(List.of(region), 2,
|
||||
new Fixed(Map.of(InferredType.LAND, List.of(plains, forest), InferredType.SEA, List.of(ocean)), Map.of()),
|
||||
(group, biome) -> bounds.get(biome)[group]);
|
||||
RegionBounds.Constants constants = compiled.of(region);
|
||||
|
||||
assertBits(FLAT, constants.any(0));
|
||||
assertBits(FLAT, constants.typed(InferredType.LAND, 0));
|
||||
assertBits(FLAT, constants.typed(InferredType.SEA, 0));
|
||||
assertNull(constants.any(1));
|
||||
assertNull(constants.typed(InferredType.LAND, 1));
|
||||
assertSame(HILLS, constants.typed(InferredType.SEA, 1));
|
||||
assertSame(HILLS, compiled.pick(plains, 1));
|
||||
assertBits(new NoiseBounds(2D, 30D), compiled.pick(forest, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void picksAreConstantOnlyWhenEveryDescendantAgrees() {
|
||||
IrisBiome steppe = new IrisBiome();
|
||||
IrisBiome meadow = new IrisBiome();
|
||||
IrisBiome mesa = new IrisBiome();
|
||||
IrisBiome canyon = new IrisBiome();
|
||||
IrisRegion region = new IrisRegion();
|
||||
Map<IrisBiome, NoiseBounds> bounds = new IdentityHashMap<>();
|
||||
bounds.put(steppe, FLAT);
|
||||
bounds.put(meadow, new NoiseBounds(0D, 0D));
|
||||
bounds.put(mesa, FLAT);
|
||||
bounds.put(canyon, HILLS);
|
||||
|
||||
RegionBounds compiled = RegionBounds.compile(List.of(region), 1,
|
||||
new Fixed(Map.of(InferredType.LAND, List.of(steppe, mesa), InferredType.SEA, List.of()),
|
||||
descendants(steppe, List.of(steppe, meadow), mesa, List.of(mesa, canyon))),
|
||||
(group, biome) -> bounds.get(biome));
|
||||
|
||||
assertNull(compiled.of(region).typed(InferredType.LAND, 0));
|
||||
assertSame(FLAT, compiled.pick(steppe, 0));
|
||||
assertNull(compiled.pick(mesa, 0));
|
||||
assertNull(compiled.pick(canyon, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void landAndSeaConstantsOnlyMergeWhenTheirBitsMatch() {
|
||||
IrisBiome beach = new IrisBiome();
|
||||
IrisBiome reef = new IrisBiome();
|
||||
IrisRegion region = new IrisRegion();
|
||||
Map<IrisBiome, NoiseBounds> bounds = new IdentityHashMap<>();
|
||||
bounds.put(beach, new NoiseBounds(0D, 0D));
|
||||
bounds.put(reef, new NoiseBounds(-0D, 0D));
|
||||
|
||||
RegionBounds.Constants constants = RegionBounds.compile(List.of(region), 1,
|
||||
new Fixed(Map.of(InferredType.LAND, List.of(beach), InferredType.SEA, List.of(reef)), Map.of()),
|
||||
(group, biome) -> bounds.get(biome)).of(region);
|
||||
|
||||
assertNull(constants.any(0));
|
||||
assertSame(bounds.get(beach), constants.typed(InferredType.LAND, 0));
|
||||
assertSame(bounds.get(reef), constants.typed(InferredType.SEA, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyBiomeListsAndUnknownRegionsHaveNoConstant() {
|
||||
IrisRegion region = new IrisRegion();
|
||||
IrisBiome only = new IrisBiome();
|
||||
RegionBounds compiled = RegionBounds.compile(List.of(region), 1,
|
||||
new Fixed(Map.of(InferredType.LAND, List.of(only), InferredType.SEA, List.of()), Map.of()),
|
||||
(group, biome) -> FLAT);
|
||||
RegionBounds.Constants constants = compiled.of(region);
|
||||
|
||||
assertSame(FLAT, constants.typed(InferredType.LAND, 0));
|
||||
assertNull(constants.typed(InferredType.SEA, 0));
|
||||
assertNull(constants.any(0));
|
||||
assertNull(constants.typed(InferredType.SHORE, 0));
|
||||
assertNull(constants.typed(InferredType.CAVE, 0));
|
||||
assertNull(compiled.of(new IrisRegion()));
|
||||
assertNull(compiled.of(null));
|
||||
assertNull(compiled.pick(new IrisBiome(), 0));
|
||||
assertNull(compiled.pick(null, 0));
|
||||
}
|
||||
|
||||
private static Map<IrisBiome, List<IrisBiome>> descendants(IrisBiome first, List<IrisBiome> firstDescendants,
|
||||
IrisBiome second, List<IrisBiome> secondDescendants) {
|
||||
Map<IrisBiome, List<IrisBiome>> descendants = new IdentityHashMap<>();
|
||||
descendants.put(first, firstDescendants);
|
||||
descendants.put(second, secondDescendants);
|
||||
return descendants;
|
||||
}
|
||||
|
||||
private static void assertBits(NoiseBounds expected, NoiseBounds actual) {
|
||||
assertEquals(Double.doubleToRawLongBits(expected.min()), Double.doubleToRawLongBits(actual.min()));
|
||||
assertEquals(Double.doubleToRawLongBits(expected.max()), Double.doubleToRawLongBits(actual.max()));
|
||||
}
|
||||
|
||||
private record Fixed(Map<InferredType, List<IrisBiome>> picks, Map<IrisBiome, List<IrisBiome>> descendants)
|
||||
implements RegionBounds.Reach {
|
||||
@Override
|
||||
public List<IrisBiome> picks(IrisRegion region, InferredType type) {
|
||||
return picks.get(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<IrisBiome> descendants(IrisBiome biome) {
|
||||
List<IrisBiome> known = descendants.get(biome);
|
||||
return known == null ? List.of(biome) : known;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class SharedBaseHeightsTest {
|
||||
@Test
|
||||
public void storedColumnsComeBackWithTheirExactBits() {
|
||||
SharedBaseHeights cache = new SharedBaseHeights(1 << 10);
|
||||
double height = Math.nextUp(71.25D);
|
||||
|
||||
assertTrue(Double.isNaN(cache.get(12, -40)));
|
||||
cache.put(12, -40, height);
|
||||
|
||||
assertEquals(Double.doubleToRawLongBits(height), Double.doubleToRawLongBits(cache.get(12, -40)));
|
||||
assertTrue(Double.isNaN(cache.get(-40, 12)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void originMissesUntilItIsStored() {
|
||||
SharedBaseHeights cache = new SharedBaseHeights(2);
|
||||
|
||||
assertTrue(Double.isNaN(cache.get(0, 0)));
|
||||
cache.put(0, 0, -0D);
|
||||
assertEquals(Double.doubleToRawLongBits(-0D), Double.doubleToRawLongBits(cache.get(0, 0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonFiniteHeightsAreNeverStored() {
|
||||
SharedBaseHeights cache = new SharedBaseHeights(2);
|
||||
cache.put(3, 4, 64D);
|
||||
cache.put(3, 4, Double.NaN);
|
||||
cache.put(3, 4, Double.POSITIVE_INFINITY);
|
||||
|
||||
assertEquals(64D, cache.get(3, 4), 0D);
|
||||
}
|
||||
|
||||
@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};
|
||||
for (int x : coordinates) {
|
||||
for (int z : coordinates) {
|
||||
SharedBaseHeights cache = new SharedBaseHeights(2);
|
||||
cache.put(x, z, 12D);
|
||||
for (int otherX : coordinates) {
|
||||
for (int otherZ : coordinates) {
|
||||
if (x == otherX && z == otherZ) {
|
||||
assertEquals(12D, cache.get(otherX, otherZ), 0D);
|
||||
} else {
|
||||
assertTrue(Double.isNaN(cache.get(otherX, otherZ)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aCollidingColumnReplacesTheOldOne() {
|
||||
SharedBaseHeights cache = new SharedBaseHeights(2);
|
||||
cache.put(5, 9, 40D);
|
||||
cache.put(6, 9, 41D);
|
||||
cache.put(7, 9, 42D);
|
||||
|
||||
int stored = 0;
|
||||
for (int x = 5; x <= 7; x++) {
|
||||
double value = cache.get(x, 9);
|
||||
if (!Double.isNaN(value)) {
|
||||
assertEquals(35D + x, value, 0D);
|
||||
stored++;
|
||||
}
|
||||
}
|
||||
assertTrue(stored >= 1 && stored <= 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void capacityFollowsTheHeapWithinBounds() {
|
||||
assertEquals(SharedBaseHeights.MINIMUM_CAPACITY, SharedBaseHeights.capacityFor(0L));
|
||||
assertEquals(SharedBaseHeights.MAXIMUM_CAPACITY, SharedBaseHeights.capacityFor(Long.MAX_VALUE));
|
||||
int capacity = SharedBaseHeights.capacityFor(6L << 30);
|
||||
assertEquals(1, Integer.bitCount(capacity));
|
||||
assertTrue(capacity >= SharedBaseHeights.MINIMUM_CAPACITY && capacity <= SharedBaseHeights.MAXIMUM_CAPACITY);
|
||||
}
|
||||
}
|
||||
@@ -146,7 +146,7 @@ public class SharedBoundsBiomesTest {
|
||||
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]);
|
||||
Object actual = cache.get(coordinates[xi], coordinates[zi]);
|
||||
if (actual != null) {
|
||||
hits.incrementAndGet();
|
||||
if (actual != biomes[xi][zi]) {
|
||||
|
||||
Reference in new issue
Block a user