mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-20 23:36:52 +00:00
Merged generation optimizations
- Generation skips empty floating work and repeated river lookups - Height prefill uses batched cache access - Deep fixed fracture chains reuse exact samples
This commit is contained in:
commit
2e3a41f262
12 files changed
+1229
-66
No files matched your search
+14
-3
@@ -53,6 +53,7 @@ import java.util.Set;
|
||||
|
||||
@ComponentFlag(ReservedFlag.FLOATING_OBJECT)
|
||||
public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
private static final int MINIMUM_PLACEMENT_RADIUS = 16;
|
||||
private static final int INVERTED_PICK_ATTEMPTS = 8;
|
||||
private static final IrisObjectRotation ROTATION_NONE = IrisObjectRotation.of(0, 0, 0);
|
||||
|
||||
@@ -62,6 +63,9 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
|
||||
@Override
|
||||
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
|
||||
if (getRadius() == 0) {
|
||||
return;
|
||||
}
|
||||
IrisComplex complex = context.getComplex();
|
||||
if (!complex.allowsNewGenerationChunk(x, z)) {
|
||||
return;
|
||||
@@ -593,7 +597,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
|
||||
@Override
|
||||
protected int computeRadius() {
|
||||
int maxObjectExtent = 16;
|
||||
int maxObjectExtent = 0;
|
||||
Map<String, IrisBlockVector> sizeCache = new HashMap<>();
|
||||
Set<String> warnedLargeObjects = new HashSet<>();
|
||||
try {
|
||||
@@ -613,16 +617,20 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
if (target != null) {
|
||||
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.resolveTopObjects(target), data, sizeCache, warnedLargeObjects));
|
||||
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.resolveBottomObjects(target), data, sizeCache, warnedLargeObjects));
|
||||
} else {
|
||||
maxObjectExtent = Math.max(maxObjectExtent, MINIMUM_PLACEMENT_RADIUS);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
maxObjectExtent = Math.max(maxObjectExtent, MINIMUM_PLACEMENT_RADIUS);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
GenerationFailures.rethrowEngineFailure(e);
|
||||
IrisLogging.reportError(e);
|
||||
maxObjectExtent = Math.max(maxObjectExtent, MINIMUM_PLACEMENT_RADIUS);
|
||||
}
|
||||
return maxObjectExtent;
|
||||
}
|
||||
@@ -630,10 +638,13 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
|
||||
private int computePlacementRadius(KList<IrisObjectPlacement> placements, IrisData data,
|
||||
Map<String, IrisBlockVector> sizeCache,
|
||||
Set<String> warnedLargeObjects) {
|
||||
int radius = 0;
|
||||
if (placements == null) {
|
||||
return radius;
|
||||
return MINIMUM_PLACEMENT_RADIUS;
|
||||
}
|
||||
if (placements.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
int radius = MINIMUM_PLACEMENT_RADIUS;
|
||||
for (IrisObjectPlacement placement : placements) {
|
||||
if (placement == null || placement.getPlace() == null) {
|
||||
continue;
|
||||
|
||||
@@ -31,6 +31,14 @@ import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.interpolation.IrisInterpolation;
|
||||
import art.arcane.volmlib.util.noise.CNG;
|
||||
import art.arcane.volmlib.util.noise.FlatNoise;
|
||||
import art.arcane.volmlib.util.noise.FractalBillowPerlinNoise;
|
||||
import art.arcane.volmlib.util.noise.FractalBillowSimplexNoise;
|
||||
import art.arcane.volmlib.util.noise.FractalFBMSimplexNoise;
|
||||
import art.arcane.volmlib.util.noise.NoiseGenerator;
|
||||
import art.arcane.volmlib.util.noise.OffsetNoiseGenerator;
|
||||
import art.arcane.volmlib.util.noise.PerlinNoise;
|
||||
import art.arcane.volmlib.util.noise.SimplexNoise;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
@@ -50,6 +58,9 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
public class IrisNoiseGenerator {
|
||||
private static final int GENERATOR_CACHE_SIZE = 32;
|
||||
private static final long GENERATOR_SEED_SALT = 33_955_677L;
|
||||
private static final int MINIMUM_FRACTURE_DEPTH = 4;
|
||||
private static final int MAXIMUM_FRACTURE_NODES = 32;
|
||||
private static final ThreadLocal<FractureSamples> FRACTURE_SAMPLES = ThreadLocal.withInitial(FractureSamples::new);
|
||||
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
@@ -135,6 +146,20 @@ public class IrisNoiseGenerator {
|
||||
return offsetY;
|
||||
}
|
||||
|
||||
if (hasDeepFracture()) {
|
||||
FractureSamples samples = FRACTURE_SAMPLES.get();
|
||||
if (!samples.active) {
|
||||
samples.active = true;
|
||||
try {
|
||||
if (samples.prepare(this, superSeed, data, engine)) {
|
||||
return samples.sample(xv, zv);
|
||||
}
|
||||
} finally {
|
||||
samples.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double x = xv;
|
||||
double z = zv;
|
||||
int g = 33;
|
||||
@@ -148,17 +173,7 @@ public class IrisNoiseGenerator {
|
||||
g += 819;
|
||||
}
|
||||
|
||||
CNG cng = getGenerator(superSeed, data, engine);
|
||||
double sampleX = (x / zoom) + offsetX;
|
||||
double sampleZ = (z / zoom) + offsetZ;
|
||||
double n = cng.noiseFast2D(sampleX, sampleZ) * opacity;
|
||||
n = negative ? (-n + opacity) : n;
|
||||
n = (exponent != 1 ? n < 0 ? -Math.pow(-n, exponent) : Math.pow(n, exponent) : n) + offsetY;
|
||||
n = parametric ? IrisInterpolation.parametric(n, 2D) : n;
|
||||
n = bezier ? IrisInterpolation.bezier(n) : n;
|
||||
n = sinCentered ? IrisInterpolation.sinCenter(n) : n;
|
||||
|
||||
return n;
|
||||
return sample(getGenerator(superSeed, data, engine), x, z);
|
||||
}
|
||||
|
||||
public KList<IrisNoiseGenerator> getAllComposites() {
|
||||
@@ -173,6 +188,65 @@ public class IrisNoiseGenerator {
|
||||
return g;
|
||||
}
|
||||
|
||||
private double sample(CNG cng, double x, double z) {
|
||||
double sampleX = (x / zoom) + offsetX;
|
||||
double sampleZ = (z / zoom) + offsetZ;
|
||||
double n = cng.noiseFast2D(sampleX, sampleZ) * opacity;
|
||||
n = negative ? (-n + opacity) : n;
|
||||
n = (exponent != 1 ? n < 0 ? -Math.pow(-n, exponent) : Math.pow(n, exponent) : n) + offsetY;
|
||||
n = parametric ? IrisInterpolation.parametric(n, 2D) : n;
|
||||
n = bezier ? IrisInterpolation.bezier(n) : n;
|
||||
n = sinCentered ? IrisInterpolation.sinCenter(n) : n;
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
private boolean hasDeepFracture() {
|
||||
IrisNoiseGenerator node = this;
|
||||
for (int depth = 0; depth < MINIMUM_FRACTURE_DEPTH; depth++) {
|
||||
if (node.getClass() != IrisNoiseGenerator.class || !node.enabled || node.fracture.size() != 1) {
|
||||
return false;
|
||||
}
|
||||
node = node.fracture.get(0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean hasPureStyle() {
|
||||
return style != null && style.getClass() == IrisGeneratorStyle.class
|
||||
&& style.getExpression() == null && style.getImageMap() == null
|
||||
&& style.getFracture() == null && style.getCellularFrequency() == 0D
|
||||
&& style.getCacheSize() == 0;
|
||||
}
|
||||
|
||||
private static boolean isPureGenerator(CNG cng) {
|
||||
for (int depth = 0; depth < MAXIMUM_FRACTURE_NODES; depth++) {
|
||||
if (cng.getClass() != CNG.class || cng.getChildren() != null
|
||||
|| cng.getCustomGenerator() != null || cng.getInjector() != CNG.ADD
|
||||
|| !isPureBase(cng.getGenerator())) {
|
||||
return false;
|
||||
}
|
||||
cng = cng.getFracture();
|
||||
if (cng == null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isPureBase(NoiseGenerator generator) {
|
||||
for (int depth = 0; depth < MAXIMUM_FRACTURE_NODES && generator != null; depth++) {
|
||||
Class<?> type = generator.getClass();
|
||||
if (type != OffsetNoiseGenerator.class) {
|
||||
return type == SimplexNoise.class || type == PerlinNoise.class || type == FlatNoise.class
|
||||
|| type == FractalBillowPerlinNoise.class || type == FractalBillowSimplexNoise.class
|
||||
|| type == FractalFBMSimplexNoise.class;
|
||||
}
|
||||
generator = ((OffsetNoiseGenerator) generator).getBase();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static final class GeneratorKey {
|
||||
private final IrisData data;
|
||||
private final Engine engine;
|
||||
@@ -208,6 +282,59 @@ public class IrisNoiseGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class FractureSamples {
|
||||
private final IrisNoiseGenerator[] nodes = new IrisNoiseGenerator[MAXIMUM_FRACTURE_NODES];
|
||||
private final CNG[] generators = new CNG[MAXIMUM_FRACTURE_NODES];
|
||||
private int size;
|
||||
private boolean active;
|
||||
|
||||
private boolean prepare(IrisNoiseGenerator node, long superSeed, IrisData data, Engine engine) {
|
||||
while (size < MAXIMUM_FRACTURE_NODES) {
|
||||
if (node.getClass() != IrisNoiseGenerator.class || !node.enabled || !node.hasPureStyle()
|
||||
|| node.fracture.size() > 1) {
|
||||
return false;
|
||||
}
|
||||
CNG generator = node.getGenerator(superSeed, data, engine);
|
||||
if (!isPureGenerator(generator)) {
|
||||
return false;
|
||||
}
|
||||
nodes[size] = node;
|
||||
generators[size++] = generator;
|
||||
if (node.fracture.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
superSeed += node.seed + 33;
|
||||
node = node.fracture.get(0);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private double sample(double x, double z) {
|
||||
int leaf = size - 1;
|
||||
double forward = nodes[leaf].sample(generators[leaf], x, z);
|
||||
double reverse = nodes[leaf].sample(generators[leaf], z, x);
|
||||
for (int index = leaf - 1; index >= 0; index--) {
|
||||
double offset = nodes[index + 1].opacity / 2D;
|
||||
double forwardX = x + (forward - offset);
|
||||
double forwardZ = z + (reverse - offset);
|
||||
forward = nodes[index].sample(generators[index], forwardX, forwardZ);
|
||||
if (index > 0) {
|
||||
reverse = nodes[index].sample(generators[index], forwardZ, forwardX);
|
||||
}
|
||||
}
|
||||
return forward;
|
||||
}
|
||||
|
||||
private void clear() {
|
||||
for (int index = 0; index < size; index++) {
|
||||
nodes[index] = null;
|
||||
generators[index] = null;
|
||||
}
|
||||
size = 0;
|
||||
active = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CachedGenerator {
|
||||
private final GeneratorKey key;
|
||||
private final CNG generator;
|
||||
|
||||
@@ -125,7 +125,7 @@ public class IrisComplex implements DataProvider {
|
||||
private static final int GRID_BOUNDS_CACHE_SIZE = 8192;
|
||||
private static final int STUDIO_NOISE_CACHE_SIZE = 32_768;
|
||||
private static final int TERRAIN_COLUMN_CACHE_SIZE = 524_288;
|
||||
/** One million corners: about 16 MB, roughly a 4000 by 4000 block area at the 4-block grid. */
|
||||
/** One million corners: about 20 MB, roughly a 4000 by 4000 block area at the 4-block grid. */
|
||||
private static final int SHARED_CORNER_BOUNDS_CAPACITY = 1 << 20;
|
||||
/** The slope streams measure the rise across a run of this many blocks, so gradient is slope over run. */
|
||||
private static final double SLOPE_RUN = 3D;
|
||||
@@ -148,7 +148,7 @@ public class IrisComplex implements DataProvider {
|
||||
private final transient ThreadLocal<GridBoundsCache> gridBoundsCache = ThreadLocal.withInitial(GridBoundsCache::new);
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final int biomeBoundsSamplingStep;
|
||||
private transient volatile SharedCornerBounds sharedCornerBounds = new SharedCornerBounds(SHARED_CORNER_BOUNDS_CAPACITY);
|
||||
private transient volatile SharedCornerBounds sharedCornerBounds;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient IrisInterpolator[] frozenInterpolators;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@@ -407,11 +407,13 @@ public class IrisComplex implements DataProvider {
|
||||
GenerationStreams.cache2D(bridgeStream.convertAware2D((t, x, z) -> inferredStreams.get(t).get(x, z))
|
||||
.convertAware2D(this::implode), "baseBiomeStream", engine, cacheSize);
|
||||
baseBiomeStream = focusedBiomes != null || !imageMapRuntime.has(IrisImageMapApplication.BIOME)
|
||||
? proceduralBaseBiomeStream : GenerationStreams.cache2D(proceduralBaseBiomeStream
|
||||
.convertAware2D((biome, x, z) -> {
|
||||
? proceduralBaseBiomeStream : GenerationStreams.cache2D(ProceduralStream.of((x, z) -> {
|
||||
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
||||
return mapped == null ? biome : mapped;
|
||||
}), "imageMappedBaseBiomeStream", engine, cacheSize);
|
||||
return mapped == null ? proceduralBaseBiomeStream.get(x, z) : mapped;
|
||||
}, (x, y, z) -> {
|
||||
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
||||
return mapped == null ? proceduralBaseBiomeStream.get(x, y, z) : mapped;
|
||||
}, proceduralBaseBiomeStream), "imageMappedBaseBiomeStream", engine, cacheSize);
|
||||
proceduralTerrainHeight = new ProceduralTerrainHeightSampler(
|
||||
(x, z) -> getHeight(engine, x, z, engine.getSeedManager().getHeight()),
|
||||
engine.getDimension().getTerrainSamplingStep());
|
||||
@@ -441,14 +443,9 @@ public class IrisComplex implements DataProvider {
|
||||
: GenerationStreams.cache2DDouble(ProceduralStream.ofDouble((x, z) -> transitionDisplacement.height(
|
||||
blockCoordinate(x), blockCoordinate(z), unblendedNaturalHeightStream.getDouble(x, z))),
|
||||
"naturalHeightStream", engine, cacheSize);
|
||||
naturalTrueBiomeStream = focusedBiomes != null ? GenerationStreams.cache2D(focusedBiomes, "naturalTrueBiomeStream-focus", engine, cacheSize) : GenerationStreams.cache2D(naturalHeightStream
|
||||
.convertAware2D((h, x, z) -> {
|
||||
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
||||
return mapped == null
|
||||
? fixBiomeType(h, baseBiomeStream.get(x, z), regionStream.get(x, z), x, z,
|
||||
transitionFluidHeight(x, z, fluidHeight))
|
||||
: mapped;
|
||||
}), "naturalTrueBiomeStream", engine, cacheSize);
|
||||
naturalTrueBiomeStream = focusedBiomes != null ? GenerationStreams.cache2D(focusedBiomes, "naturalTrueBiomeStream-focus", engine, cacheSize)
|
||||
: GenerationStreams.cache2D(ProceduralStream.of(this::sampleNaturalTrueBiome, this::sampleNaturalTrueBiome,
|
||||
Interpolated.of(value -> 0D, value -> null)), "naturalTrueBiomeStream", engine, cacheSize);
|
||||
IrisHydrology configuredHydrology = engine.getDimension().getHydrology();
|
||||
if (hydrologyActive(configuredHydrology)) {
|
||||
hydrologyRuntime = new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext(
|
||||
@@ -477,10 +474,8 @@ public class IrisComplex implements DataProvider {
|
||||
Math.max(4096, cacheSize), data, banks3D));
|
||||
}
|
||||
}
|
||||
cachedHeightStream = new CachedDoubleStream2D("heightStream", engine,
|
||||
ProceduralStream.ofDouble(this::resolveHydrologyTerrainHeight), cacheSize);
|
||||
heightStream = ProceduralStream.ofDouble((x, z) -> terrainEngine.getPlatformHooks().isMainThread()
|
||||
? nonblockingTerrainHeight(x, z) : cachedHeightStream.getDouble(x, z));
|
||||
cachedHeightStream = new TerrainHeightStream(engine, cacheSize);
|
||||
heightStream = cachedHeightStream;
|
||||
roundedHeighteightStream = GenerationStreams.contextInjecting(heightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||
.round();
|
||||
slopeStream = GenerationStreams.contextInjecting(heightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||
@@ -1038,6 +1033,22 @@ public class IrisComplex implements DataProvider {
|
||||
return sample.terrainHeight();
|
||||
}
|
||||
|
||||
private IrisBiome sampleNaturalTrueBiome(double x, double z) {
|
||||
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
||||
return mapped == null
|
||||
? fixBiomeType(naturalHeightStream.getDouble(x, z), baseBiomeStream.get(x, z), regionStream.get(x, z), x, z,
|
||||
transitionFluidHeight(x, z, fluidHeight))
|
||||
: mapped;
|
||||
}
|
||||
|
||||
private IrisBiome sampleNaturalTrueBiome(double x, double y, double z) {
|
||||
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
||||
return mapped == null
|
||||
? fixBiomeType(naturalHeightStream.getDouble(x, y, z), baseBiomeStream.get(x, z), regionStream.get(x, z), x, z,
|
||||
transitionFluidHeight(x, z, fluidHeight))
|
||||
: mapped;
|
||||
}
|
||||
|
||||
private double nonblockingTerrainHeight(double x, double z) {
|
||||
HydrologyColumnSnapshot snapshot = sampleHydrologySnapshot(blockCoordinate(x), blockCoordinate(z));
|
||||
if (!snapshot.available() || snapshot.column() == null) {
|
||||
@@ -1693,12 +1704,17 @@ public class IrisComplex implements DataProvider {
|
||||
return packed;
|
||||
}
|
||||
|
||||
/** The field initializer covers every real complex; an instance built without it gets one here. */
|
||||
/** Coarse bounds allocate their shared table on the first corner sample. */
|
||||
private SharedCornerBounds sharedCornerBounds() {
|
||||
SharedCornerBounds shared = sharedCornerBounds;
|
||||
if (shared == null) {
|
||||
shared = new SharedCornerBounds(SHARED_CORNER_BOUNDS_CAPACITY);
|
||||
sharedCornerBounds = shared;
|
||||
synchronized (this) {
|
||||
shared = sharedCornerBounds;
|
||||
if (shared == null) {
|
||||
shared = new SharedCornerBounds(SHARED_CORNER_BOUNDS_CAPACITY);
|
||||
sharedCornerBounds = shared;
|
||||
}
|
||||
}
|
||||
}
|
||||
return shared;
|
||||
}
|
||||
@@ -2214,6 +2230,62 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private final class TerrainHeightStream extends CachedDoubleStream2D {
|
||||
private TerrainHeightStream(Engine engine, int cacheSize) {
|
||||
super("heightStream", engine, ProceduralStream.ofDouble(IrisComplex.this::resolveHydrologyTerrainHeight), cacheSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getDouble(double x, double z) {
|
||||
return terrainEngine.getPlatformHooks().isMainThread()
|
||||
? nonblockingTerrainHeight(x, z) : super.getDouble(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double get(double x, double y, double z) {
|
||||
return getDouble(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getDouble(double x, double y, double z) {
|
||||
return getDouble(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fillChunkDoubles(int worldX, int worldZ, double[] target) {
|
||||
boolean mainThread = terrainEngine.getPlatformHooks().isMainThread();
|
||||
if (!mainThread && (worldX & 15) == 0 && (worldZ & 15) == 0) {
|
||||
super.fillChunkDoubles(worldX, worldZ, target);
|
||||
return;
|
||||
}
|
||||
if (target == null || target.length != 256) {
|
||||
throw new IllegalArgumentException("Expected a 16x16 target array.");
|
||||
}
|
||||
for (int index = 0; index < 256; index++) {
|
||||
int x = worldX + (index & 15);
|
||||
int z = worldZ + (index >> 4);
|
||||
target[index] = mainThread ? nonblockingTerrainHeight(x, z) : super.getDouble(x, z);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fillChunkRaw(int worldX, int worldZ, Object[] target) {
|
||||
boolean mainThread = terrainEngine.getPlatformHooks().isMainThread();
|
||||
if (!mainThread && (worldX & 15) == 0 && (worldZ & 15) == 0) {
|
||||
super.fillChunkRaw(worldX, worldZ, target);
|
||||
return;
|
||||
}
|
||||
if (target == null || target.length != 256) {
|
||||
throw new IllegalArgumentException("Expected a 16x16 target array.");
|
||||
}
|
||||
for (int index = 0; index < 256; index++) {
|
||||
int x = worldX + (index & 15);
|
||||
int z = worldZ + (index >> 4);
|
||||
target[index] = mainThread ? nonblockingTerrainHeight(x, z) : super.getDouble(x, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static final class ChildSelectionPlan {
|
||||
private final IrisRaritySelection<IrisBiome> selection;
|
||||
|
||||
|
||||
@@ -54,43 +54,44 @@ public class IrisPostModifier extends EngineAssignedModifier<NativeBlockState> {
|
||||
int depth = output.getDepth();
|
||||
int planeWidth = width + 2;
|
||||
int[] heights = heightPlane(x, z, width, depth, planeWidth);
|
||||
boolean[] rivers = riverPlane(getComplex().getRiverCarveWeightStream(), x, z, width, depth);
|
||||
IrisDimension dimension = getDimension();
|
||||
boolean walls = dimension.isPostProcessingWalls();
|
||||
boolean slabs = dimension.isPostProcessingSlabs();
|
||||
for (int i = 0; i < width; i++) {
|
||||
for (int j = 0; j < depth; j++) {
|
||||
post(i, j, sync, i + x, j + z, context, heights, planeWidth, walls, slabs);
|
||||
post(i, j, sync, i + x, j + z, context, heights, rivers, planeWidth, walls, slabs);
|
||||
}
|
||||
}
|
||||
|
||||
getEngine().getMetrics().getPost().put(p.getMilliseconds());
|
||||
}
|
||||
|
||||
static boolean[] riverPlane(ProceduralStream<Double> carveWeights, int x, int z, int width, int depth) {
|
||||
int planeWidth = width + 2;
|
||||
boolean[] rivers = new boolean[planeWidth * (depth + 2)];
|
||||
for (int j = -1; j <= depth; j++) {
|
||||
boolean edge = j == -1 || j == depth;
|
||||
int from = edge ? 0 : -1;
|
||||
int to = edge ? width - 1 : width;
|
||||
int row = (j + 1) * planeWidth;
|
||||
for (int i = from; i <= to; i++) {
|
||||
rivers[row + i + 1] = carveWeights.get(x + i, z + j) > 0D;
|
||||
}
|
||||
}
|
||||
return rivers;
|
||||
}
|
||||
|
||||
static boolean skipsHydrologyColumn(boolean[] rivers, int center, int planeWidth) {
|
||||
return rivers[center] || rivers[center + 1] || rivers[center - 1]
|
||||
|| rivers[center + planeWidth] || rivers[center - planeWidth];
|
||||
}
|
||||
|
||||
/**
|
||||
* Every column reads its own height plus its four neighbours, so adjacent columns would otherwise
|
||||
* resolve the same height stream entry up to five times. Resolve the padded plane once instead. The
|
||||
* four diagonal corners are never read, so they are left unresolved.
|
||||
*/
|
||||
private boolean riverColumn(int x, int z) {
|
||||
ProceduralStream<Double> carveWeights = getComplex().getRiverCarveWeightStream();
|
||||
return skipsHydrologyColumn(new double[] {
|
||||
carveWeights.get(x, z),
|
||||
carveWeights.get(x + 1, z),
|
||||
carveWeights.get(x - 1, z),
|
||||
carveWeights.get(x, z + 1),
|
||||
carveWeights.get(x, z - 1)
|
||||
});
|
||||
}
|
||||
|
||||
static boolean skipsHydrologyColumn(double[] carveWeights) {
|
||||
for (double weight : carveWeights) {
|
||||
if (weight > 0D) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private int[] heightPlane(int x, int z, int width, int depth, int planeWidth) {
|
||||
int[] heights = new int[planeWidth * (depth + 2)];
|
||||
EngineMantle mantle = getEngine().getMantle();
|
||||
@@ -109,7 +110,7 @@ public class IrisPostModifier extends EngineAssignedModifier<NativeBlockState> {
|
||||
return heights;
|
||||
}
|
||||
|
||||
private void post(int currentPostX, int currentPostZ, Hunk<NativeBlockState> currentData, int x, int z, ChunkContext context, int[] heights, int planeWidth, boolean walls, boolean slabs) {
|
||||
private void post(int currentPostX, int currentPostZ, Hunk<NativeBlockState> currentData, int x, int z, ChunkContext context, int[] heights, boolean[] rivers, int planeWidth, boolean walls, boolean slabs) {
|
||||
// x/z are world coordinates, the hunk is indexed relative to this chunk origin.
|
||||
int originX = x - currentPostX;
|
||||
int originZ = z - currentPostZ;
|
||||
@@ -120,7 +121,7 @@ public class IrisPostModifier extends EngineAssignedModifier<NativeBlockState> {
|
||||
int hc = heights[center - 1];
|
||||
int hd = heights[center - planeWidth];
|
||||
int fluidHeight = (int) Math.round(getComplex().getRiverWaterSurfaceStream().get(x, z));
|
||||
boolean river = riverColumn(x, z);
|
||||
boolean river = skipsHydrologyColumn(rivers, center, planeWidth);
|
||||
|
||||
// Floating Nibs
|
||||
int g = 0;
|
||||
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
package art.arcane.iris.generation.mantle;
|
||||
|
||||
import art.arcane.iris.generation.biome.FloatingIslandBoundarySampler;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.biome.IrisFloatingChildBiomes;
|
||||
import art.arcane.iris.generation.context.ChunkContext;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.runtime.SeedManager;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.pack.loading.ResourceLoader;
|
||||
import art.arcane.iris.structure.object.IrisObject;
|
||||
import art.arcane.iris.structure.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.testsupport.PlatformBinding;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleDataAdapter;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleHooks;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class MantleFloatingObjectComponentEmptyTest {
|
||||
@ClassRule
|
||||
public static final PlatformBinding PLATFORM = PlatformBinding.mockPlatform();
|
||||
|
||||
@Test
|
||||
public void absentFloatingObjectsDoNotReadColumnsOrWriteMatter() {
|
||||
Fixture fixture = new Fixture(new IrisBiome());
|
||||
MantleWriter writer = mock(MantleWriter.class);
|
||||
ChunkContext context = mock(ChunkContext.class);
|
||||
|
||||
assertEquals(0, fixture.component.getRadius());
|
||||
fixture.component.generateLayer(writer, 4, -6, context);
|
||||
|
||||
verifyNoInteractions(writer, context);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void floatingTerrainWithoutObjectsHasNoObjectReach() {
|
||||
Fixture fixture = new Fixture(floatingBiome(new IrisFloatingChildBiomes()));
|
||||
|
||||
assertEquals(0, fixture.component.getRadius());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void explicitPlacementListsKeepTheirMinimumReachWhenObjectsAreMissing() {
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement().setPlace(new KList<>("test/missing"));
|
||||
List<IrisFloatingChildBiomes> entries = List.of(
|
||||
new IrisFloatingChildBiomes().setFloatingObjects(new KList<>(placement)),
|
||||
new IrisFloatingChildBiomes().setExtraObjects(new KList<>(placement)),
|
||||
new IrisFloatingChildBiomes().setTopObjectOverrides(new KList<>(placement)),
|
||||
new IrisFloatingChildBiomes().setBottomObjectOverrides(new KList<>(placement))
|
||||
);
|
||||
|
||||
for (IrisFloatingChildBiomes entry : entries) {
|
||||
Fixture fixture = new Fixture(floatingBiome(entry));
|
||||
assertEquals(16, fixture.component.getRadius());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedParentObjectsKeepTheirMinimumReach() {
|
||||
IrisBiome parent = floatingBiome(new IrisFloatingChildBiomes())
|
||||
.setObjects(new KList<>(new IrisObjectPlacement()));
|
||||
Fixture fixture = new Fixture(parent);
|
||||
|
||||
assertEquals(16, fixture.component.getRadius());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inheritedTargetObjectsKeepTheirMinimumReach() {
|
||||
IrisFloatingChildBiomes entry = new IrisFloatingChildBiomes().setBiome("test/target");
|
||||
Fixture fixture = new Fixture(floatingBiome(entry));
|
||||
IrisBiome target = new IrisBiome().setObjects(new KList<>(new IrisObjectPlacement()));
|
||||
when(fixture.biomes.load("test/target")).thenReturn(target);
|
||||
|
||||
assertEquals(16, fixture.component.getRadius());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void disabledInheritanceLeavesObjectlessFloatingTerrainEmpty() {
|
||||
IrisBiome parent = floatingBiome(new IrisFloatingChildBiomes().setInheritObjects(false))
|
||||
.setObjects(new KList<>(new IrisObjectPlacement()));
|
||||
Fixture fixture = new Fixture(parent);
|
||||
|
||||
assertEquals(0, fixture.component.getRadius());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hotloadRechecksObjectsAcrossPackChanges() {
|
||||
Fixture fixture = new Fixture(new IrisBiome());
|
||||
assertEquals(0, fixture.component.getRadius());
|
||||
|
||||
IrisBiome active = floatingBiome(new IrisFloatingChildBiomes()
|
||||
.setExtraObjects(new KList<>(new IrisObjectPlacement())));
|
||||
when(fixture.dimension.getReachableBiomes(any())).thenReturn(new KList<>(active));
|
||||
fixture.component.hotload();
|
||||
assertEquals(16, fixture.component.getRadius());
|
||||
|
||||
when(fixture.dimension.getReachableBiomes(any())).thenReturn(new KList<>(new IrisBiome()));
|
||||
fixture.component.hotload();
|
||||
assertEquals(0, fixture.component.getRadius());
|
||||
assertEquals(0, fixture.component.getRadius());
|
||||
verify(fixture.dimension, times(3)).getReachableBiomes(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedReachabilityScanKeepsTheComponentActive() {
|
||||
Fixture fixture = new Fixture(new IrisBiome());
|
||||
when(fixture.dimension.getReachableBiomes(any())).thenThrow(new IllegalStateException("Unavailable biome"));
|
||||
|
||||
assertEquals(16, fixture.component.getRadius());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedTargetResolutionKeepsTheComponentActive() {
|
||||
IrisFloatingChildBiomes entry = spy(new IrisFloatingChildBiomes());
|
||||
Fixture fixture = new Fixture(floatingBiome(entry));
|
||||
doThrow(new IllegalStateException("Unavailable floating biome")).when(entry).getRealBiome(any(), any());
|
||||
|
||||
assertEquals(16, fixture.component.getRadius());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unresolvedTargetKeepsTheComponentActive() {
|
||||
IrisFloatingChildBiomes entry = spy(new IrisFloatingChildBiomes());
|
||||
Fixture fixture = new Fixture(floatingBiome(entry));
|
||||
doReturn(null).when(entry).getRealBiome(any(), any());
|
||||
|
||||
assertEquals(16, fixture.component.getRadius());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unresolvedPlacementListKeepsTheComponentActive() {
|
||||
Fixture fixture = new Fixture(floatingBiome(new IrisFloatingChildBiomes().setExtraObjects(null)));
|
||||
|
||||
assertEquals(16, fixture.component.getRadius());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyComponentShrinksTheWindowAndPreservesCompletionFlags() {
|
||||
Fixture compact = new Fixture(new IrisBiome());
|
||||
Fixture conservative = new Fixture(new IrisBiome());
|
||||
MantleFloatingObjectComponent conservativeComponent = new ConservativeFloatingObjects(conservative.engineMantle);
|
||||
|
||||
GenerationResult compactResult = compact.generate(compact.component);
|
||||
GenerationResult conservativeResult = conservative.generate(conservativeComponent);
|
||||
|
||||
assertEquals(9, compactResult.carveCalls());
|
||||
assertEquals(25, conservativeResult.carveCalls());
|
||||
assertEquals(0, compact.columnReads.get());
|
||||
assertEquals(2304, conservative.columnReads.get());
|
||||
assertEquals(9, compactResult.loadedChunks());
|
||||
assertEquals(25, conservativeResult.loadedChunks());
|
||||
for (MantleFlag flag : List.of(ReservedFlag.CARVED, ReservedFlag.FLOATING_OBJECT, MantleFlag.PLANNED)) {
|
||||
assertTrue(compactResult.center().isFlagged(flag));
|
||||
assertEquals(conservativeResult.center().isFlagged(flag), compactResult.center().isFlagged(flag));
|
||||
}
|
||||
}
|
||||
|
||||
private static IrisBiome floatingBiome(IrisFloatingChildBiomes entry) {
|
||||
return new IrisBiome().setFloatingChildBiomes(new KList<>(entry));
|
||||
}
|
||||
|
||||
private static final class Fixture {
|
||||
private final IrisDimension dimension = mock(IrisDimension.class);
|
||||
private final Engine engine = mock(Engine.class);
|
||||
private final EngineMantle engineMantle = mock(EngineMantle.class);
|
||||
private final IrisComplex complex = mock(IrisComplex.class);
|
||||
private final ChunkContext context = mock(ChunkContext.class);
|
||||
private final ResourceLoader<IrisBiome> biomes;
|
||||
private final AtomicInteger columnReads = new AtomicInteger();
|
||||
private final MantleFloatingObjectComponent component;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Fixture(IrisBiome biome) {
|
||||
biomes = mock(ResourceLoader.class);
|
||||
ResourceLoader<IrisObject> objects = mock(ResourceLoader.class);
|
||||
IrisData data = mock(IrisData.class);
|
||||
when(data.getBiomeLoader()).thenReturn(biomes);
|
||||
when(data.getObjectLoader()).thenReturn(objects);
|
||||
when(dimension.isUseMantle()).thenReturn(true);
|
||||
when(dimension.getReachableBiomes(any())).thenReturn(new KList<>(biome));
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
when(engine.getHeight()).thenReturn(256);
|
||||
when(engine.getSeedManager()).thenReturn(mock(SeedManager.class));
|
||||
when(engine.getMantle()).thenReturn(engineMantle);
|
||||
when(engineMantle.getEngine()).thenReturn(engine);
|
||||
when(engineMantle.getData()).thenReturn(data);
|
||||
when(engineMantle.getComplex()).thenReturn(complex);
|
||||
when(complex.allowsNewGenerationChunk(anyInt(), anyInt())).thenReturn(true);
|
||||
when(complex.allowsMantleChunkWrite(anyInt(), anyInt())).thenReturn(true);
|
||||
when(context.getComplex()).thenReturn(complex);
|
||||
when(context.getFloatingIslandBoundarySampler()).thenReturn(new FloatingIslandBoundarySampler((x, z) -> {
|
||||
columnReads.incrementAndGet();
|
||||
return biome;
|
||||
}));
|
||||
component = new MantleFloatingObjectComponent(engineMantle);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private GenerationResult generate(MantleFloatingObjectComponent floating) {
|
||||
Map<Long, MantleChunk<Matter>> chunks = new HashMap<>();
|
||||
MantleDataAdapter<Matter> adapter = mock(MantleDataAdapter.class);
|
||||
Mantle<Matter> mantle = mock(Mantle.class);
|
||||
when(mantle.useChunk(anyInt(), anyInt())).thenAnswer(invocation -> {
|
||||
int x = invocation.getArgument(0);
|
||||
int z = invocation.getArgument(1);
|
||||
long key = ((long) x << 32) ^ (z & 0xffffffffL);
|
||||
return chunks.computeIfAbsent(key, ignored -> new MantleChunk<>(16, x, z, adapter, MantleHooks.NONE)).use();
|
||||
});
|
||||
MantleComponent carving = mock(MantleComponent.class);
|
||||
when(carving.isEnabled()).thenReturn(true);
|
||||
when(carving.getFlag()).thenReturn(ReservedFlag.CARVED);
|
||||
when(carving.getOutputRadius()).thenReturn(1);
|
||||
when(carving.getGenerationPhase()).thenReturn(MatterGenerationPhase.TERRAIN);
|
||||
when(carving.getPrerequisiteFlags()).thenReturn(MantleComponent.EMPTY_PREREQUISITES);
|
||||
AtomicInteger carveCalls = new AtomicInteger();
|
||||
doAnswer(invocation -> {
|
||||
carveCalls.incrementAndGet();
|
||||
return null;
|
||||
}).when(carving).generateLayer(any(), anyInt(), anyInt(), any());
|
||||
MatterGenerator generator = mock(MatterGenerator.class, CALLS_REAL_METHODS);
|
||||
when(generator.getEngine()).thenReturn(engine);
|
||||
when(generator.getMantle()).thenReturn(mantle);
|
||||
when(generator.getComponents()).thenReturn(List.of(
|
||||
new MantlePass(List.of(carving), 0, 0),
|
||||
new MantlePass(List.of(floating), 0, 0)
|
||||
));
|
||||
|
||||
generator.generateMatter(0, 0, false, context);
|
||||
|
||||
return new GenerationResult(chunks.get(0L), carveCalls.get(), chunks.size());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ConservativeFloatingObjects extends MantleFloatingObjectComponent {
|
||||
private ConservativeFloatingObjects(EngineMantle engineMantle) {
|
||||
super(engineMantle);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int computeRadius() {
|
||||
return Math.max(16, super.computeRadius());
|
||||
}
|
||||
}
|
||||
|
||||
private record GenerationResult(MantleChunk<Matter> center, int carveCalls, int loadedChunks) {
|
||||
}
|
||||
}
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
package art.arcane.iris.generation.noise;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.interpolation.IrisInterpolation;
|
||||
import art.arcane.volmlib.util.noise.CNG;
|
||||
import art.arcane.volmlib.util.noise.NoiseGenerator;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
public final class IrisNoiseGeneratorFractureTest {
|
||||
@Test
|
||||
public void deepChainsMatchRecursiveSamplingBits() {
|
||||
Random coordinates = new Random(718311L);
|
||||
for (NoiseStyle style : List.of(NoiseStyle.SIMPLEX, NoiseStyle.PERLIN, NoiseStyle.IRIS,
|
||||
NoiseStyle.FRACTAL_BILLOW_SIMPLEX, NoiseStyle.FRACTAL_FBM_SIMPLEX)) {
|
||||
for (int depth : new int[]{0, 1, 3, 4, 5, 8}) {
|
||||
IrisNoiseGenerator root = chain(depth, style).getFirst();
|
||||
for (long seed : new long[]{0L, -4193L, Long.MIN_VALUE, Long.MAX_VALUE}) {
|
||||
assertParity(root, seed, 0D, -0D, null, null);
|
||||
assertParity(root, seed, -0D, 0D, null, null);
|
||||
assertParity(root, seed, -30_000_000D, 29_999_999.75D, null, null);
|
||||
assertParity(root, seed, Math.nextDown(-64D), Math.nextUp(64D), null, null);
|
||||
for (int index = 0; index < 8; index++) {
|
||||
assertParity(root, seed, coordinates.nextDouble() * 4096D - 2048D,
|
||||
coordinates.nextDouble() * 4096D - 2048D, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fourFracturesReducePrimitiveSamplesFromThirtyOneToNine() {
|
||||
List<IrisNoiseGenerator> chain = chain(4, NoiseStyle.SIMPLEX);
|
||||
List<NoiseGenerator> bases = spyBases(chain, 71L);
|
||||
reference(chain.getFirst(), 71L, 13.25D, -21.75D, null, null);
|
||||
for (int index = 0; index < bases.size(); index++) {
|
||||
verify(bases.get(index), times(1 << index)).noise(anyDouble(), anyDouble());
|
||||
clearInvocations(bases.get(index));
|
||||
}
|
||||
|
||||
chain.getFirst().getNoise(71L, 13.25D, -21.75D, null);
|
||||
for (int index = 0; index < bases.size(); index++) {
|
||||
verify(bases.get(index), times(index == 0 ? 1 : 2)).noise(anyDouble(), anyDouble());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shallowChainsKeepTheirSamplingCounts() {
|
||||
List<IrisNoiseGenerator> chain = chain(3, NoiseStyle.SIMPLEX);
|
||||
List<NoiseGenerator> bases = spyBases(chain, 71L);
|
||||
chain.getFirst().getNoise(71L, 13.25D, -21.75D, null);
|
||||
for (int index = 0; index < bases.size(); index++) {
|
||||
verify(bases.get(index), times(1 << index)).noise(anyDouble(), anyDouble());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void changedParametersAndFracturesRemainVisible() {
|
||||
List<IrisNoiseGenerator> chain = chain(5, NoiseStyle.IRIS);
|
||||
IrisNoiseGenerator root = chain.getFirst();
|
||||
assertParity(root, 19L, 31D, -79D, null, null);
|
||||
chain.get(3).setOffsetX(-319D).setOffsetZ(17D).setOffsetY(-0.7D)
|
||||
.setSeed(Long.MAX_VALUE).setOpacity(0.35D).setZoom(17D);
|
||||
assertParity(root, 19L, 31D, -79D, null, null);
|
||||
chain.get(4).setEnabled(false);
|
||||
assertParity(root, 19L, 31D, -79D, null, null);
|
||||
chain.get(4).setEnabled(true);
|
||||
chain.get(2).getFracture().add(new IrisNoiseGenerator().setStyle(NoiseStyle.PERLIN.style()));
|
||||
assertParity(root, 19L, 31D, -79D, null, null);
|
||||
chain.get(2).getFracture().removeLast();
|
||||
chain.getLast().getFracture().add(new IrisNoiseGenerator().setStyle(NoiseStyle.SIMPLEX.style()));
|
||||
assertParity(root, 19L, 31D, -79D, null, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void statefulRuntimeGeneratorsKeepAllEvaluations() {
|
||||
List<IrisNoiseGenerator> chain = chain(4, NoiseStyle.SIMPLEX);
|
||||
long leafSeed = childSeed(chain, 23L, chain.size() - 1);
|
||||
StatefulNoise stateful = new StatefulNoise();
|
||||
chain.getLast().getGenerator(leafSeed, null, null).setGenerator(stateful);
|
||||
double expected = reference(chain.getFirst(), 23L, 17D, -31D, null, null);
|
||||
assertEquals(16, stateful.samples);
|
||||
stateful.samples = 0;
|
||||
double actual = chain.getFirst().getNoise(23L, 17D, -31D, null);
|
||||
assertBits(expected, actual);
|
||||
assertEquals(16, stateful.samples);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expressionConfigurationDoesNotReuseEvenPreviouslyCachedPureNoise() {
|
||||
List<IrisNoiseGenerator> chain = chain(4, NoiseStyle.SIMPLEX);
|
||||
List<NoiseGenerator> bases = spyBases(chain, 71L);
|
||||
chain.getLast().getStyle().setExpression("custom");
|
||||
chain.getFirst().getNoise(71L, 13.25D, -21.75D, null);
|
||||
verify(bases.getLast(), times(16)).noise(anyDouble(), anyDouble());
|
||||
chain.getLast().getStyle().setExpression(null).setImageMap("map");
|
||||
clearInvocations(bases.getLast());
|
||||
chain.getFirst().getNoise(71L, 13.25D, -21.75D, null);
|
||||
verify(bases.getLast(), times(16)).noise(anyDouble(), anyDouble());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customStylesKeepTheScalarPath() {
|
||||
List<IrisNoiseGenerator> chain = chain(4, NoiseStyle.SIMPLEX);
|
||||
List<NoiseGenerator> bases = spyBases(chain, 71L);
|
||||
chain.getLast().setStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX) {
|
||||
});
|
||||
chain.getFirst().getNoise(71L, 13.25D, -21.75D, null);
|
||||
verify(bases.getLast(), times(16)).noise(anyDouble(), anyDouble());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedSamplingReleasesTheThreadScratch() {
|
||||
List<IrisNoiseGenerator> chain = chain(4, NoiseStyle.SIMPLEX);
|
||||
List<NoiseGenerator> bases = spyBases(chain, 71L);
|
||||
doThrow(new IllegalStateException("sample failed")).doCallRealMethod()
|
||||
.when(bases.getLast()).noise(anyDouble(), anyDouble());
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> chain.getFirst().getNoise(71L, 13.25D, -21.75D, null));
|
||||
clearInvocations(bases.getLast());
|
||||
chain.getFirst().getNoise(71L, 13.25D, -21.75D, null);
|
||||
verify(bases.getLast(), times(2)).noise(anyDouble(), anyDouble());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void threadsAndEngineIdentitiesRemainIndependent() throws Exception {
|
||||
IrisNoiseGenerator root = chain(5, NoiseStyle.IRIS).getFirst();
|
||||
IrisData data = mock(IrisData.class);
|
||||
Engine firstEngine = mock(Engine.class);
|
||||
Engine secondEngine = mock(Engine.class);
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
ExecutorService executor = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<?> first = executor.submit(() -> sampleConcurrently(root, data, firstEngine, 47L, start));
|
||||
Future<?> second = executor.submit(() -> sampleConcurrently(root, data, secondEngine, -131L, start));
|
||||
start.countDown();
|
||||
first.get(15L, TimeUnit.SECONDS);
|
||||
second.get(15L, TimeUnit.SECONDS);
|
||||
} finally {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
assertParity(root, 47L, 17D, 19D, data, secondEngine);
|
||||
assertParity(root, 47L, 17D, 19D, data, firstEngine);
|
||||
}
|
||||
|
||||
private static void sampleConcurrently(IrisNoiseGenerator root, IrisData data, Engine engine,
|
||||
long seed, CountDownLatch start) {
|
||||
try {
|
||||
assertTrue(start.await(5L, TimeUnit.SECONDS));
|
||||
} catch (InterruptedException failure) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new AssertionError(failure);
|
||||
}
|
||||
for (int index = 0; index < 64; index++) {
|
||||
assertParity(root, seed, index * 13.75D, -index * 2.25D, data, engine);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<IrisNoiseGenerator> chain(int depth, NoiseStyle style) {
|
||||
List<IrisNoiseGenerator> nodes = new ArrayList<>(depth + 1);
|
||||
for (int index = 0; index <= depth; index++) {
|
||||
IrisNoiseGenerator node = new IrisNoiseGenerator().setStyle(style.style())
|
||||
.setSeed(index * 713L).setZoom(0.75D + index).setOpacity(0.63D + index * 0.01D)
|
||||
.setOffsetX(index * -0.2D).setOffsetY(index * -0.03D).setOffsetZ(index * 0.4D)
|
||||
.setNegative(index % 2 == 0).setExponent(index % 3 == 0 ? 1.25D : 1D)
|
||||
.setParametric(index % 3 == 1).setBezier(index % 3 == 2)
|
||||
.setSinCentered(index % 4 == 1);
|
||||
if (!nodes.isEmpty()) {
|
||||
nodes.getLast().setFracture(new KList<>(node));
|
||||
}
|
||||
nodes.add(node);
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
private static List<NoiseGenerator> spyBases(List<IrisNoiseGenerator> nodes, long superSeed) {
|
||||
List<NoiseGenerator> bases = new ArrayList<>(nodes.size());
|
||||
for (IrisNoiseGenerator node : nodes) {
|
||||
CNG generator = node.getGenerator(superSeed, null, null);
|
||||
NoiseGenerator base = spy(generator.getGenerator());
|
||||
generator.setGenerator(base);
|
||||
bases.add(base);
|
||||
superSeed += node.getSeed() + 33;
|
||||
}
|
||||
return bases;
|
||||
}
|
||||
|
||||
private static long childSeed(List<IrisNoiseGenerator> nodes, long seed, int index) {
|
||||
for (int child = 0; child < index; child++) {
|
||||
seed += nodes.get(child).getSeed() + 33;
|
||||
}
|
||||
return seed;
|
||||
}
|
||||
|
||||
private static void assertParity(IrisNoiseGenerator node, long seed, double x, double z,
|
||||
IrisData data, Engine engine) {
|
||||
assertBits(reference(node, seed, x, z, data, engine), node.getNoise(seed, x, z, data, engine));
|
||||
}
|
||||
|
||||
private static void assertBits(double expected, double actual) {
|
||||
assertEquals(Double.doubleToRawLongBits(expected), Double.doubleToRawLongBits(actual));
|
||||
}
|
||||
|
||||
private static double reference(IrisNoiseGenerator node, long superSeed, double xv, double zv,
|
||||
IrisData data, Engine engine) {
|
||||
if (!node.isEnabled()) {
|
||||
return node.getOffsetY();
|
||||
}
|
||||
double x = xv;
|
||||
double z = zv;
|
||||
int salt = 33;
|
||||
for (IrisNoiseGenerator child : node.getFracture()) {
|
||||
if (child.isEnabled()) {
|
||||
double offset = child.getOpacity() / 2D;
|
||||
x += reference(child, superSeed + node.getSeed() + salt, xv, zv, data, engine) - offset;
|
||||
z += reference(child, superSeed + node.getSeed() + salt, zv, xv, data, engine) - offset;
|
||||
}
|
||||
salt += 819;
|
||||
}
|
||||
double sampleX = (x / node.getZoom()) + node.getOffsetX();
|
||||
double sampleZ = (z / node.getZoom()) + node.getOffsetZ();
|
||||
double result = node.getGenerator(superSeed, data, engine).noiseFast2D(sampleX, sampleZ) * node.getOpacity();
|
||||
result = node.isNegative() ? -result + node.getOpacity() : result;
|
||||
result = (node.getExponent() != 1D ? result < 0D ? -Math.pow(-result, node.getExponent())
|
||||
: Math.pow(result, node.getExponent()) : result) + node.getOffsetY();
|
||||
result = node.isParametric() ? IrisInterpolation.parametric(result, 2D) : result;
|
||||
result = node.isBezier() ? IrisInterpolation.bezier(result) : result;
|
||||
return node.isSinCentered() ? IrisInterpolation.sinCenter(result) : result;
|
||||
}
|
||||
|
||||
private static final class StatefulNoise implements NoiseGenerator {
|
||||
private int samples;
|
||||
|
||||
@Override
|
||||
public double noise(double x) {
|
||||
return ++samples * 0.01D;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double noise(double x, double z) {
|
||||
return ++samples * 0.01D;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double noise(double x, double y, double z) {
|
||||
return ++samples * 0.01D;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,12 +84,14 @@ public class IrisComplexBiomeBuffetTest {
|
||||
IrisComplex complex = new IrisComplex(engine);
|
||||
doReturn(complex).when(engine).getComplex();
|
||||
try {
|
||||
assertNull(complex.getSharedCornerBounds());
|
||||
assertEquals("alpha", complex.getTrueBiomeStream().get(15, 0).getLoadKey());
|
||||
assertEquals("beta", complex.getTrueBiomeStream().get(16, 0).getLoadKey());
|
||||
assertEquals("gamma", complex.getTrueBiomeStream().get(0, 16).getLoadKey());
|
||||
assertEquals(137D, complex.getBaseTerrainHeightStream().getDouble(15, 0), 0D);
|
||||
assertEquals(147D, complex.getBaseTerrainHeightStream().getDouble(16, 0), 0D);
|
||||
assertEquals(157D, complex.getBaseTerrainHeightStream().getDouble(0, 16), 0D);
|
||||
assertNull(complex.getSharedCornerBounds());
|
||||
assertEquals("owned", complex.getRegionStream().get(16, 0).getLoadKey());
|
||||
assertEquals("iris-focus/", complex.getRegionStream().get(0, 16).getLoadKey().substring(0, 11));
|
||||
assertEquals("alpha", dimension.getFocus());
|
||||
|
||||
+32
@@ -13,20 +13,52 @@ import org.junit.runners.Parameterized.Parameters;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.Answers.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
@RunWith(Enclosed.class)
|
||||
public class IrisComplexGridBoundsCacheTest {
|
||||
public static class CornerCache {
|
||||
@Test(timeout = 10_000L)
|
||||
public void concurrentFirstSamplesShareOneCornerTable() throws Exception {
|
||||
IrisComplex complex = createComplex();
|
||||
Method sharedCache = IrisComplex.class.getDeclaredMethod("sharedCornerBounds");
|
||||
sharedCache.setAccessible(true);
|
||||
CountDownLatch entered = new CountDownLatch(4);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
try (ExecutorService readers = Executors.newFixedThreadPool(4)) {
|
||||
List<Future<SharedCornerBounds>> results = new ArrayList<>();
|
||||
for (int index = 0; index < 4; index++) {
|
||||
results.add(readers.submit(() -> {
|
||||
entered.countDown();
|
||||
assertTrue(release.await(5L, TimeUnit.SECONDS));
|
||||
return (SharedCornerBounds) sharedCache.invoke(complex);
|
||||
}));
|
||||
}
|
||||
assertTrue(entered.await(5L, TimeUnit.SECONDS));
|
||||
release.countDown();
|
||||
SharedCornerBounds expected = results.getFirst().get(5L, TimeUnit.SECONDS);
|
||||
for (Future<SharedCornerBounds> result : results) {
|
||||
assertSame(expected, result.get(5L, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void gridBoundsCacheIsIsolatedPerComplex() throws Exception {
|
||||
IrisComplex first = createComplex();
|
||||
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.block.B;
|
||||
import art.arcane.iris.generation.context.ChunkedDoubleDataCache;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSnapshot;
|
||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.generation.image.IrisImageMapApplication;
|
||||
import art.arcane.iris.generation.image.IrisImageMapRuntime;
|
||||
import art.arcane.iris.generation.stream.ChunkFillableDoubleStream2D;
|
||||
import art.arcane.iris.generation.stream.ChunkFillableStream2D;
|
||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.Answers.CALLS_REAL_METHODS;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisComplexHeightStreamTest {
|
||||
@ClassRule public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
@Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
private IrisSettings previousSettings;
|
||||
|
||||
@Before
|
||||
public void prepareRuntime() {
|
||||
previousSettings = IrisSettings.settings;
|
||||
IrisSettings.settings = new IrisSettings();
|
||||
IrisServices.register(PreservationRegistry.class, mock(PreservationRegistry.class));
|
||||
IrisPlatform platform = mock(IrisPlatform.class);
|
||||
when(platform.platformName()).thenReturn("bukkit");
|
||||
IrisPlatforms.bind(platform);
|
||||
}
|
||||
|
||||
@After
|
||||
public void releaseRuntime() {
|
||||
IrisPlatforms.unbind();
|
||||
IrisServices.remove(PreservationRegistry.class);
|
||||
IrisSettings.settings = previousSettings;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sharedCornersRemainUnallocatedUntilTerrainSamplesThem() throws Exception {
|
||||
try (Fixture fixture = new Fixture(pack())) {
|
||||
assertNull(fixture.complex.getSharedCornerBounds());
|
||||
fixture.complex.getBaseTerrainHeightStream().getDouble(0, 0);
|
||||
assertNotNull(fixture.complex.getSharedCornerBounds());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mappedBiomesSkipProceduralSelectionAndNaturalHeight() throws Exception {
|
||||
try (Fixture fixture = new Fixture(pack())) {
|
||||
IrisBiome mapped = new IrisBiome();
|
||||
when(fixture.images.sampleBiome(anyDouble(), anyDouble())).thenReturn(mapped);
|
||||
MeteredCache bridge = (MeteredCache) fixture.complex.getBridgeStream();
|
||||
MeteredCache regions = (MeteredCache) fixture.complex.getRegionStream();
|
||||
MeteredCache heights = (MeteredCache) fixture.complex.getNaturalHeightStream();
|
||||
assertSame(mapped, fixture.complex.getBaseBiomeStream().get(-16, 32));
|
||||
assertSame(mapped, fixture.complex.getBaseBiomeStream().get(-16.75, 10, 32.5));
|
||||
assertSame(mapped, fixture.complex.getNaturalTrueBiomeStream().get(-16, 32));
|
||||
assertSame(mapped, fixture.complex.getNaturalTrueBiomeStream().get(-16.75, 10, 32.5));
|
||||
assertEquals(0L, bridge.getSize());
|
||||
assertEquals(0L, regions.getSize());
|
||||
assertEquals(0L, heights.getSize());
|
||||
assertNull(fixture.complex.getSharedCornerBounds());
|
||||
|
||||
when(fixture.images.sampleBiome(anyDouble(), anyDouble())).thenReturn(null);
|
||||
assertEquals("plain", fixture.complex.getBaseBiomeStream().get(16, 32).getLoadKey());
|
||||
assertEquals("plain", fixture.complex.getNaturalTrueBiomeStream().get(16, 32).getLoadKey());
|
||||
assertNotNull(fixture.complex.getSharedCornerBounds());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void workerPrefillUsesOneThreadCheckAndRetainsRoundedValues() throws Exception {
|
||||
try (Fixture fixture = new Fixture(pack())) {
|
||||
AtomicInteger samples = fixture.installNaturalHeight();
|
||||
ChunkedDoubleDataCache cache = new ChunkedDoubleDataCache(fixture.complex.getRawHeightStream(), -16, 32);
|
||||
int[] rounded = new int[256];
|
||||
clearInvocations(fixture.hooks);
|
||||
cache.fillRounded(rounded);
|
||||
verify(fixture.hooks, times(1)).isMainThread();
|
||||
assertEquals(256, samples.get());
|
||||
for (int index = 0; index < 256; index++) {
|
||||
int x = index & 15;
|
||||
int z = index >> 4;
|
||||
double expected = height(-16 + x, 32 + z);
|
||||
assertBits(expected, cache.getDouble(x, z));
|
||||
assertEquals((int) Math.round(expected), rounded[index]);
|
||||
}
|
||||
clearInvocations(fixture.hooks);
|
||||
cache.fill();
|
||||
verify(fixture.hooks, times(1)).isMainThread();
|
||||
assertEquals(256, samples.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mainThreadPrefillNeverWaitsOrConsumesCachedHydrology() throws Exception {
|
||||
try (Fixture fixture = new Fixture(pack())) {
|
||||
fixture.installNaturalHeight();
|
||||
IrisHydrologyRuntime hydrology = mock(IrisHydrologyRuntime.class);
|
||||
HydrologyColumnSample planned = mock(HydrologyColumnSample.class);
|
||||
when(planned.terrainHeight()).thenReturn(100);
|
||||
when(hydrology.sample(anyDouble(), anyDouble())).thenAnswer(invocation -> {
|
||||
if (fixture.mainThread.get()) {
|
||||
throw new AssertionError("Main-thread sampling entered blocking hydrology");
|
||||
}
|
||||
return Optional.of(planned);
|
||||
});
|
||||
when(hydrology.sampleSnapshot(anyInt(), anyInt())).thenAnswer(invocation -> {
|
||||
ProvisionalSampling.mark();
|
||||
return HydrologyColumnSnapshot.unavailable();
|
||||
});
|
||||
fixture.complex.setHydrologyRuntime(hydrology);
|
||||
ProceduralStream<Double> stream = fixture.complex.getRawHeightStream();
|
||||
ChunkFillableDoubleStream2D bulk = (ChunkFillableDoubleStream2D) stream;
|
||||
double[] values = new double[256];
|
||||
fixture.mainThread.set(true);
|
||||
assertThrows(ProvisionalSampling.Unmemoizable.class, () -> ProvisionalSampling.memoizable(() -> {
|
||||
bulk.fillChunkDoubles(-16, 32, values);
|
||||
return values[0];
|
||||
}));
|
||||
assertBits(height(-16, 32), values[0]);
|
||||
fixture.mainThread.set(false);
|
||||
bulk.fillChunkDoubles(-16, 32, values);
|
||||
assertBits(100D, values[0]);
|
||||
assertBits(100D, values[255]);
|
||||
verify(hydrology, times(256)).sample(anyDouble(), anyDouble());
|
||||
|
||||
fixture.mainThread.set(true);
|
||||
bulk.fillChunkDoubles(-16, 32, values);
|
||||
assertBits(height(-16, 32), values[0]);
|
||||
assertBits(height(-16.75, 32.5), stream.getDouble(-16.75, 10, 32.5));
|
||||
assertBits(height(-16.75, 32.5), stream.get(-16.75, 10, 32.5));
|
||||
Object[] boxed = new Object[256];
|
||||
((ChunkFillableStream2D) stream).fillChunkRaw(-16, 32, boxed);
|
||||
assertBits(height(-1, 47), (Double) boxed[255]);
|
||||
verify(hydrology, times(256)).sample(anyDouble(), anyDouble());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unalignedPrefillAndThreeDimensionalReadsRetainColumnCoordinates() throws Exception {
|
||||
try (Fixture fixture = new Fixture(pack())) {
|
||||
fixture.installNaturalHeight();
|
||||
ProceduralStream<Double> stream = fixture.complex.getRawHeightStream();
|
||||
double[] values = new double[256];
|
||||
((ChunkFillableDoubleStream2D) stream).fillChunkDoubles(-17, 31, values);
|
||||
for (int index = 0; index < 256; index++) {
|
||||
assertBits(height(-17 + (index & 15), 31 + (index >> 4)), values[index]);
|
||||
}
|
||||
assertBits(height(-16, 32), stream.getDouble(-16.75, 10, 32.5));
|
||||
assertBits(height(-16, 32), stream.get(-16.75, 10, 32.5));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void workerPrefillDoesNotMemoizeProvisionalNaturalValues() throws Exception {
|
||||
try (Fixture fixture = new Fixture(pack())) {
|
||||
AtomicBoolean ready = new AtomicBoolean();
|
||||
AtomicInteger samples = new AtomicInteger();
|
||||
fixture.complex.setNaturalHeightStream(ProceduralStream.ofDouble((x, z) -> {
|
||||
samples.incrementAndGet();
|
||||
if (!ready.get()) {
|
||||
ProvisionalSampling.mark();
|
||||
return -1D;
|
||||
}
|
||||
return height(x, z);
|
||||
}));
|
||||
ChunkFillableDoubleStream2D stream = (ChunkFillableDoubleStream2D) fixture.complex.getRawHeightStream();
|
||||
double[] values = new double[256];
|
||||
stream.fillChunkDoubles(-16, 32, values);
|
||||
for (double value : values) {
|
||||
assertBits(-1D, value);
|
||||
}
|
||||
ready.set(true);
|
||||
int before = samples.get();
|
||||
stream.fillChunkDoubles(-16, 32, values);
|
||||
assertEquals(before + 256, samples.get());
|
||||
for (int index = 0; index < 256; index++) {
|
||||
assertBits(height(-16 + (index & 15), 32 + (index >> 4)), values[index]);
|
||||
}
|
||||
stream.fillChunkDoubles(-16, 32, values);
|
||||
assertEquals(before + 256, samples.get());
|
||||
}
|
||||
}
|
||||
|
||||
private Path pack() throws Exception {
|
||||
Path pack = temporaryFolder.newFolder().toPath();
|
||||
Files.createDirectories(pack.resolve("dimensions"));
|
||||
Files.createDirectories(pack.resolve("regions"));
|
||||
Files.createDirectories(pack.resolve("biomes"));
|
||||
Files.createDirectories(pack.resolve("generators"));
|
||||
Files.writeString(pack.resolve("dimensions/main.json"), """
|
||||
{"regions":["plain"],"landChance":1,
|
||||
"carvingEnabled":false,"hydrology":{"rivers":{"enabled":false}}}
|
||||
""");
|
||||
Files.writeString(pack.resolve("regions/plain.json"), "{\"landBiomes\":[\"plain\"]}");
|
||||
Files.writeString(pack.resolve("generators/flat.json"), "{\"composite\":[]}");
|
||||
Files.writeString(pack.resolve("biomes/plain.json"),
|
||||
"{\"generators\":[{\"generator\":\"flat\",\"min\":10,\"max\":10}]}");
|
||||
return pack;
|
||||
}
|
||||
|
||||
private static double height(double x, double z) {
|
||||
return x * 0.125D - z * 0.03125D;
|
||||
}
|
||||
|
||||
private static void assertBits(double expected, double actual) {
|
||||
assertEquals(Double.doubleToRawLongBits(expected), Double.doubleToRawLongBits(actual));
|
||||
}
|
||||
|
||||
private static final class Fixture implements AutoCloseable {
|
||||
private final Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
|
||||
private final EnginePlatformHooks hooks = mock(EnginePlatformHooks.class);
|
||||
private final AtomicBoolean mainThread = new AtomicBoolean();
|
||||
private final MockedStatic<B> blocks = mockStatic(B.class);
|
||||
private final MockedStatic<IrisImageMapRuntime> imageRuntimes = mockStatic(IrisImageMapRuntime.class);
|
||||
private final IrisImageMapRuntime images = mock(IrisImageMapRuntime.class);
|
||||
private final IrisData data;
|
||||
private final IrisComplex complex;
|
||||
|
||||
private Fixture(Path pack) {
|
||||
NativeBlockState block = mock(NativeBlockState.class);
|
||||
when(block.isFluid()).thenReturn(true);
|
||||
blocks.when(() -> B.getState(anyString())).thenReturn(block);
|
||||
blocks.when(() -> B.getStateOrNull(anyString(), eq(false))).thenReturn(block);
|
||||
imageRuntimes.when(() -> IrisImageMapRuntime.compile(engine)).thenReturn(images);
|
||||
when(images.has(IrisImageMapApplication.BIOME)).thenReturn(true);
|
||||
when(images.sampleTerrainHeight(anyDouble(), anyDouble(), anyDouble()))
|
||||
.thenAnswer(invocation -> invocation.<Double>getArgument(2));
|
||||
data = IrisData.openRuntime(pack.toFile());
|
||||
IrisDimension dimension = data.getDimensionLoader().load("main");
|
||||
doReturn(data).when(engine).getData();
|
||||
doReturn(dimension).when(engine).getDimension();
|
||||
doReturn(384).when(engine).getHeight();
|
||||
doReturn(-64).when(engine).getMinHeight();
|
||||
doReturn(320).when(engine).getMaxHeight();
|
||||
when(hooks.isMainThread()).thenAnswer(ignored -> mainThread.get());
|
||||
doReturn(hooks).when(engine).getPlatformHooks();
|
||||
doReturn(new SeedManager(1337L)).when(engine).getSeedManager();
|
||||
complex = new IrisComplex(engine);
|
||||
doReturn(complex).when(engine).getComplex();
|
||||
}
|
||||
|
||||
private AtomicInteger installNaturalHeight() {
|
||||
AtomicInteger samples = new AtomicInteger();
|
||||
complex.setNaturalHeightStream(ProceduralStream.ofDouble((x, z) -> {
|
||||
samples.incrementAndGet();
|
||||
return height(x, z);
|
||||
}));
|
||||
return samples;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
complex.close();
|
||||
data.close();
|
||||
} finally {
|
||||
imageRuntimes.close();
|
||||
blocks.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+71
-4
@@ -1,20 +1,87 @@
|
||||
package art.arcane.iris.generation.stage;
|
||||
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisPostModifierHydrologyTest {
|
||||
@Test
|
||||
public void columnsTouchingARiverFootprintSkipThePostPasses() {
|
||||
assertTrue(IrisPostModifier.skipsHydrologyColumn(new double[] {1D, 0D, 0D, 0D, 0D}));
|
||||
assertTrue(IrisPostModifier.skipsHydrologyColumn(new double[] {0D, 0D, 0.5D, 0D, 0D}));
|
||||
assertTrue(IrisPostModifier.skipsHydrologyColumn(new double[] {0D, 0D, 0D, 0D, 0.75D}));
|
||||
for (int index : new int[] {4, 3, 5, 1, 7}) {
|
||||
boolean[] rivers = new boolean[9];
|
||||
rivers[index] = true;
|
||||
assertTrue(IrisPostModifier.skipsHydrologyColumn(rivers, 4, 3));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void columnsAwayFromRiversKeepThePostPasses() {
|
||||
assertFalse(IrisPostModifier.skipsHydrologyColumn(new double[] {0D, 0D, 0D, 0D, 0D}));
|
||||
boolean[] rivers = new boolean[9];
|
||||
rivers[0] = true;
|
||||
rivers[2] = true;
|
||||
rivers[6] = true;
|
||||
rivers[8] = true;
|
||||
assertFalse(IrisPostModifier.skipsHydrologyColumn(rivers, 4, 3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void paddedPlaneSamplesEachRequiredCoordinateOnce() {
|
||||
Map<Long, Integer> samples = new HashMap<>();
|
||||
ProceduralStream<Double> stream = ProceduralStream.ofDouble((x, z) -> {
|
||||
long key = ((long) (int) x << 32) | ((int) z & 0xffffffffL);
|
||||
samples.merge(key, 1, Integer::sum);
|
||||
return weight((int) x, (int) z);
|
||||
});
|
||||
boolean[] rivers = IrisPostModifier.riverPlane(stream, -32, 48, 16, 16);
|
||||
assertEquals(320, samples.size());
|
||||
for (int count : samples.values()) {
|
||||
assertEquals(1, count);
|
||||
}
|
||||
assertPlaneMatchesPointSampling(rivers, -32, 48, 16, 16);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rectangularPlanesPreservePositiveWeightRules() {
|
||||
ProceduralStream<Double> stream = ProceduralStream.ofDouble((x, z) -> weight((int) x, (int) z));
|
||||
for (int width : new int[] {1, 3, 16}) {
|
||||
for (int depth : new int[] {1, 5, 16}) {
|
||||
boolean[] rivers = IrisPostModifier.riverPlane(stream, -19, -37, width, depth);
|
||||
assertPlaneMatchesPointSampling(rivers, -19, -37, width, depth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertPlaneMatchesPointSampling(boolean[] rivers, int x, int z, int width, int depth) {
|
||||
int planeWidth = width + 2;
|
||||
for (int localX = 0; localX < width; localX++) {
|
||||
for (int localZ = 0; localZ < depth; localZ++) {
|
||||
int worldX = x + localX;
|
||||
int worldZ = z + localZ;
|
||||
boolean expected = weight(worldX, worldZ) > 0D || weight(worldX + 1, worldZ) > 0D
|
||||
|| weight(worldX - 1, worldZ) > 0D || weight(worldX, worldZ + 1) > 0D
|
||||
|| weight(worldX, worldZ - 1) > 0D;
|
||||
assertEquals(expected, IrisPostModifier.skipsHydrologyColumn(rivers,
|
||||
(localZ + 1) * planeWidth + localX + 1, planeWidth));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static double weight(int x, int z) {
|
||||
return switch (Math.floorMod(x * 31 + z * 17, 8)) {
|
||||
case 0 -> Double.NaN;
|
||||
case 1 -> -1D;
|
||||
case 2 -> 0D;
|
||||
case 3 -> -0D;
|
||||
case 4 -> Double.MIN_VALUE;
|
||||
case 5 -> 1D;
|
||||
case 6 -> Double.POSITIVE_INFINITY;
|
||||
default -> Double.NEGATIVE_INFINITY;
|
||||
};
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -91,9 +91,9 @@ public class IrisPostModifierSlabSupportTest {
|
||||
heights[2 * PLANE_WIDTH + 3] = SURFACE + 1;
|
||||
|
||||
Method post = IrisPostModifier.class.getDeclaredMethod("post", int.class, int.class, Hunk.class,
|
||||
int.class, int.class, ChunkContext.class, int[].class, int.class, boolean.class, boolean.class);
|
||||
int.class, int.class, ChunkContext.class, int[].class, boolean[].class, int.class, boolean.class, boolean.class);
|
||||
post.setAccessible(true);
|
||||
post.invoke(modifier, 1, 1, output, 1, 1, context, heights, PLANE_WIDTH, false, true);
|
||||
post.invoke(modifier, 1, 1, output, 1, 1, context, heights, new boolean[heights.length], PLANE_WIDTH, false, true);
|
||||
return output.get(1, SURFACE + 1, 1);
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -123,9 +123,10 @@ public class IrisPostModifierWallShoreTest {
|
||||
heights[5] = SURFACE - 4;
|
||||
|
||||
Method post = IrisPostModifier.class.getDeclaredMethod("post", int.class, int.class, Hunk.class,
|
||||
int.class, int.class, ChunkContext.class, int[].class, int.class, boolean.class, boolean.class);
|
||||
int.class, int.class, ChunkContext.class, int[].class, boolean[].class, int.class, boolean.class, boolean.class);
|
||||
post.setAccessible(true);
|
||||
post.invoke(modifier, 0, 0, data, 0, 0, context, heights, PLANE_WIDTH, walls, false);
|
||||
boolean[] rivers = IrisPostModifier.riverPlane(carveWeights, 0, 0, PLANE_WIDTH - 2, PLANE_WIDTH - 2);
|
||||
post.invoke(modifier, 0, 0, data, 0, 0, context, heights, rivers, PLANE_WIDTH, walls, false);
|
||||
return data.get(0, SURFACE, 0);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user