Added chunk terrain bake

- Compiled generator programs evaluate layer trees in batches through NoiseProgram
- TerrainProgram accumulates interpolator group means over chunk columns
- ChunkBakeCache holds compute-once per-chunk planes with provisional results never published
- Height, biome, region, rock, fluid and cave streams read the bake as thin facades
- Sparse hydrology and bounds reads stay scalar with a direct-mapped base height memo
- ColumnBake assembles id planes, heights, fluid surface, columns and the apron on demand
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-21 21:52:07 -04:00
1 parent 2471066937
commit 037156ab7d
22 files changed
+2032 -331

No files matched your search

@@ -0,0 +1,7 @@
package art.arcane.iris.generation.bake;
public interface BakeCache {
ColumnBake get(int chunkX, int chunkZ);
ColumnBake getIfPresent(int chunkX, int chunkZ);
}
@@ -0,0 +1,16 @@
package art.arcane.iris.generation.bake;
public enum BakePlane {
REGION,
BASE_BIOME,
BASE_HEIGHT,
NATURAL_HEIGHT,
NATURAL_TRUE_BIOME,
HEIGHT,
TRUE_BIOME,
ROCK,
FLUID,
CAVE_BIOME;
static final BakePlane[] PLANES = values();
}
@@ -0,0 +1,41 @@
package art.arcane.iris.generation.bake;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.Objects;
public final class BakeRegistry<T> {
private final Object lock = new Object();
private volatile IdentityHashMap<T, Integer> ids = new IdentityHashMap<>();
private volatile Object[] values = new Object[16];
private int size;
public short id(T value) {
Objects.requireNonNull(value, "Registered value");
Integer known = ids.get(value);
if (known != null) {
return known.shortValue();
}
synchronized (lock) {
Integer existing = ids.get(value);
if (existing != null) {
return existing.shortValue();
}
if (size == Short.MAX_VALUE) {
throw new IllegalStateException("Bake registry is full");
}
Object[] grown = size == values.length ? Arrays.copyOf(values, size * 2) : values;
grown[size] = value;
values = grown;
IdentityHashMap<T, Integer> next = new IdentityHashMap<>(ids);
next.put(value, size);
ids = next;
return (short) size++;
}
}
@SuppressWarnings("unchecked")
public T value(int id) {
return (T) values[id];
}
}
@@ -0,0 +1,176 @@
package art.arcane.iris.generation.bake;
import art.arcane.iris.generation.cache.ConcurrentClockCache;
import java.util.Objects;
import java.util.function.DoubleSupplier;
import java.util.function.Supplier;
public final class ChunkBakeCache implements BakeCache {
private static final ThreadLocal<int[]> DEPTH = ThreadLocal.withInitial(() -> new int[2]);
private static final int BAKING = 0;
private static final int SPARSE = 1;
private final PlaneSource source;
private final PlaneSource tracked;
private volatile ConcurrentClockCache<ChunkPlanes> entries;
public ChunkBakeCache(PlaneSource source, int capacityChunks) {
this.source = Objects.requireNonNull(source, "Bake plane source");
this.tracked = new TrackedSource(source);
this.entries = new ConcurrentClockCache<>(Math.max(16, capacityChunks));
}
@Override
public ColumnBake get(int chunkX, int chunkZ) {
return planes(chunkX, chunkZ).columnBake(tracked);
}
@Override
public ColumnBake getIfPresent(int chunkX, int chunkZ) {
ChunkPlanes planes = planesIfPresent(chunkX, chunkZ);
return planes == null ? null : planes.columnBake();
}
public int capacity() {
return entries.capacity();
}
public synchronized void ensureCapacity(int chunks) {
ConcurrentClockCache<ChunkPlanes> current = entries;
if (current.capacity() >= chunks) {
return;
}
ConcurrentClockCache<ChunkPlanes> resized = new ConcurrentClockCache<>(chunks);
current.forEach(planes -> resized.putIfAbsent(key(planes.chunkX(), planes.chunkZ()), planes));
entries = resized;
}
public void clear() {
entries.clear();
}
public ChunkPlanes planes(int chunkX, int chunkZ) {
ConcurrentClockCache<ChunkPlanes> current = entries;
long key = key(chunkX, chunkZ);
ChunkPlanes existing = current.get(key);
return existing != null ? existing : current.putIfAbsent(key, new ChunkPlanes(chunkX, chunkZ));
}
public ChunkPlanes planesIfPresent(int chunkX, int chunkZ) {
return entries.get(key(chunkX, chunkZ));
}
public Object bake(BakePlane plane, int chunkX, int chunkZ) {
return planes(chunkX, chunkZ).plane(plane, tracked);
}
public Object read(BakePlane plane, int chunkX, int chunkZ) {
ChunkPlanes planes = entries.get(key(chunkX, chunkZ));
if (planes != null) {
Object published = planes.published(plane);
if (published != null) {
return published;
}
}
if (!denseReadsAllowed()) {
return null;
}
return (planes != null ? planes : planes(chunkX, chunkZ)).plane(plane, tracked);
}
public boolean denseReadsAllowed() {
int[] depth = DEPTH.get();
if (depth[SPARSE] > 0 || source.mainThread()) {
return false;
}
return depth[BAKING] > 0 || source.generationContext();
}
public boolean bakingAllowed() {
return !inSparseScope() && !source.mainThread();
}
public static boolean inSparseScope() {
return DEPTH.get()[SPARSE] > 0;
}
public static <T> T sparse(Supplier<T> action) {
enterSparse();
try {
return action.get();
} finally {
exitSparse();
}
}
public static double sparseDouble(DoubleSupplier action) {
enterSparse();
try {
return action.getAsDouble();
} finally {
exitSparse();
}
}
public static void sparse(Runnable action) {
enterSparse();
try {
action.run();
} finally {
exitSparse();
}
}
public static void enterSparse() {
DEPTH.get()[SPARSE]++;
}
public static void exitSparse() {
DEPTH.get()[SPARSE]--;
}
public static long key(int chunkX, int chunkZ) {
return ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL);
}
private static final class TrackedSource implements PlaneSource {
private final PlaneSource delegate;
private TrackedSource(PlaneSource delegate) {
this.delegate = delegate;
}
@Override
public Object compute(BakePlane plane, int chunkX, int chunkZ) {
int[] depth = DEPTH.get();
depth[BAKING]++;
try {
return delegate.compute(plane, chunkX, chunkZ);
} finally {
depth[BAKING]--;
}
}
@Override
public ColumnBake assemble(ChunkPlanes planes) {
int[] depth = DEPTH.get();
depth[BAKING]++;
try {
return delegate.assemble(planes);
} finally {
depth[BAKING]--;
}
}
@Override
public boolean mainThread() {
return delegate.mainThread();
}
@Override
public boolean generationContext() {
return delegate.generationContext();
}
}
}
@@ -0,0 +1,107 @@
package art.arcane.iris.generation.bake;
import art.arcane.iris.generation.stream.ProvisionalSampling;
import java.util.concurrent.atomic.AtomicReferenceArray;
public final class ChunkPlanes {
private final int chunkX;
private final int chunkZ;
private final AtomicReferenceArray<Object> planes = new AtomicReferenceArray<>(BakePlane.PLANES.length);
private volatile ColumnBake columnBake;
ChunkPlanes(int chunkX, int chunkZ) {
this.chunkX = chunkX;
this.chunkZ = chunkZ;
}
public int chunkX() {
return chunkX;
}
public int chunkZ() {
return chunkZ;
}
public Object published(BakePlane plane) {
Object state = planes.getAcquire(plane.ordinal());
return state instanceof Pending ? null : state;
}
Object plane(BakePlane plane, PlaneSource source) {
int slot = plane.ordinal();
while (true) {
Object state = planes.getAcquire(slot);
if (state != null && !(state instanceof Pending)) {
return state;
}
if (state instanceof Pending pending) {
if (pending.owner == Thread.currentThread()) {
return source.compute(plane, chunkX, chunkZ);
}
pending.await();
continue;
}
Pending pending = new Pending(Thread.currentThread());
if (!planes.compareAndSet(slot, null, pending)) {
continue;
}
Object computed = null;
boolean publishable = false;
try {
long revision = ProvisionalSampling.revision();
computed = source.compute(plane, chunkX, chunkZ);
publishable = revision == ProvisionalSampling.revision();
} finally {
planes.setRelease(slot, publishable ? computed : null);
pending.complete();
}
return computed;
}
}
ColumnBake columnBake() {
return columnBake;
}
ColumnBake columnBake(PlaneSource source) {
ColumnBake bake = columnBake;
if (bake != null) {
return bake;
}
long revision = ProvisionalSampling.revision();
bake = source.assemble(this);
if (revision == ProvisionalSampling.revision()) {
columnBake = bake;
}
return bake;
}
private static final class Pending {
private final Thread owner;
private boolean done;
private Pending(Thread owner) {
this.owner = owner;
}
private synchronized void complete() {
done = true;
notifyAll();
}
private synchronized void await() {
boolean interrupted = false;
while (!done) {
try {
wait();
} catch (InterruptedException e) {
interrupted = true;
}
}
if (interrupted) {
Thread.currentThread().interrupt();
}
}
}
}
@@ -0,0 +1,55 @@
package art.arcane.iris.generation.bake;
import art.arcane.iris.generation.terrain.Terrain3DColumn;
public final class ColumnBake {
public static final int APRON_WIDTH = 18;
public final int chunkX;
public final int chunkZ;
public final short[] regionId;
public final short[] baseBiomeId;
public final short[] trueBiomeId;
public final short[] caveBiomeId;
public final double[] baseHeight;
public final double[] naturalHeight;
public final double[] height;
public final int[] roundedHeight;
public final float[] fluidSurface;
public final short[] rockState;
public final short[] fluidState;
public final Terrain3DColumn[] terrainColumns;
public final double[] apronHeight;
public ColumnBake(Planes planes) {
this.chunkX = planes.chunkX();
this.chunkZ = planes.chunkZ();
this.regionId = planes.regionId();
this.baseBiomeId = planes.baseBiomeId();
this.trueBiomeId = planes.trueBiomeId();
this.caveBiomeId = planes.caveBiomeId();
this.baseHeight = planes.baseHeight();
this.naturalHeight = planes.naturalHeight();
this.height = planes.height();
this.roundedHeight = planes.roundedHeight();
this.fluidSurface = planes.fluidSurface();
this.rockState = planes.rockState();
this.fluidState = planes.fluidState();
this.terrainColumns = planes.terrainColumns();
this.apronHeight = planes.apronHeight();
}
public static int index(int localX, int localZ) {
return (localZ << 4) | localX;
}
public static int apronIndex(int localX, int localZ) {
return (localZ + 1) * APRON_WIDTH + localX + 1;
}
public record Planes(int chunkX, int chunkZ, short[] regionId, short[] baseBiomeId, short[] trueBiomeId,
short[] caveBiomeId, double[] baseHeight, double[] naturalHeight, double[] height,
int[] roundedHeight, float[] fluidSurface, short[] rockState, short[] fluidState,
Terrain3DColumn[] terrainColumns, double[] apronHeight) {
}
}
@@ -0,0 +1,11 @@
package art.arcane.iris.generation.bake;
public interface PlaneSource {
Object compute(BakePlane plane, int chunkX, int chunkZ);
ColumnBake assemble(ChunkPlanes planes);
boolean mainThread();
boolean generationContext();
}
@@ -0,0 +1,41 @@
package art.arcane.iris.generation.bake;
import art.arcane.volmlib.util.cache.CacheKey;
import java.util.concurrent.atomic.AtomicReferenceArray;
public final class SparseHeightMemo {
private final AtomicReferenceArray<Node> nodes;
private final int mask;
public SparseHeightMemo(int capacity) {
if (capacity <= 0 || Integer.bitCount(capacity) != 1) {
throw new IllegalArgumentException("Sparse height memo capacity must be a power of two");
}
nodes = new AtomicReferenceArray<>(capacity);
mask = capacity - 1;
}
public double get(int x, int z) {
long key = ((long) x << 32) ^ (z & 0xffffffffL);
Node node = nodes.getAcquire((int) CacheKey.mix(key) & mask);
return node != null && node.key == key ? node.height : Double.NaN;
}
public void put(int x, int z, double height) {
if (!Double.isFinite(height)) {
return;
}
long key = ((long) x << 32) ^ (z & 0xffffffffL);
nodes.setRelease((int) CacheKey.mix(key) & mask, new Node(key, height));
}
public void clear() {
for (int index = 0; index < nodes.length(); index++) {
nodes.setRelease(index, null);
}
}
private record Node(long key, double height) {
}
}
@@ -0,0 +1,136 @@
package art.arcane.iris.generation.bake;
import art.arcane.iris.generation.noise.GeneratorProgram;
import art.arcane.iris.generation.noise.IrisGenerator;
import art.arcane.iris.generation.noise.TerrainBatchScratch;
import art.arcane.volmlib.util.math.M;
import java.util.Arrays;
import java.util.concurrent.atomic.LongAdder;
public final class TerrainProgram {
private static final int SKIPPED = 0;
private static final int CONSTANT = 1;
private static final int ACTIVE = 2;
private final IrisGenerator[][] generators;
private final GeneratorProgram[][] programs;
private final long heightSeed;
private final LongAdder evaluations = new LongAdder();
private TerrainProgram(IrisGenerator[][] generators, GeneratorProgram[][] programs, long heightSeed) {
this.generators = generators;
this.programs = programs;
this.heightSeed = heightSeed;
}
public static TerrainProgram compile(IrisGenerator[][] generators, long heightSeed) {
GeneratorProgram[][] programs = new GeneratorProgram[generators.length][];
for (int group = 0; group < generators.length; group++) {
programs[group] = new GeneratorProgram[generators[group].length];
for (int index = 0; index < generators[group].length; index++) {
IrisGenerator generator = generators[group][index];
programs[group][index] = generator.supportsBatch() ? generator.compileProgram(heightSeed) : null;
}
}
return new TerrainProgram(generators, programs, heightSeed);
}
public int groups() {
return generators.length;
}
public int groupSize(int group) {
return generators[group].length;
}
public long evaluations() {
return evaluations.sum();
}
public void countScalarEvaluations(int count) {
evaluations.add(count);
}
public void accumulate(int group, TerrainBatchScratch scratch, double[] xs, double[] zs, int count,
double[] lows, double[] highs, double[] heights) {
IrisGenerator[] members = generators[group];
if (members.length == 0 || count == 0) {
return;
}
int mark = scratch.mark();
int indexMark = scratch.indexMark();
int[] modes = scratch.indices(count);
int activeCount = 0;
for (int index = 0; index < count; index++) {
double low = lows[index];
double high = highs[index];
if (low == 0D && high == 0D) {
modes[index] = SKIPPED;
continue;
}
if (!Double.isFinite(low) || !Double.isFinite(high)) {
throw new IllegalStateException("Terrain generator bounds were not finite at " + xs[index] + "," + zs[index]
+ " (generator=" + members[0].getLoadKey() + ", bounds=" + low + ".." + high + ").");
}
if (low == high) {
modes[index] = CONSTANT;
double sum = 0D;
for (int member = 0; member < members.length; member++) {
sum += low;
}
heights[index] += sum / members.length;
continue;
}
modes[index] = ACTIVE;
activeCount++;
}
if (activeCount == 0) {
scratch.resetIndices(indexMark);
return;
}
double[] activeX = scratch.buffer(activeCount);
double[] activeZ = scratch.buffer(activeCount);
double[] activeLow = scratch.buffer(activeCount);
double[] activeHigh = scratch.buffer(activeCount);
int[] activeIndex = scratch.indices(activeCount);
int cursor = 0;
for (int index = 0; index < count; index++) {
if (modes[index] == ACTIVE) {
activeX[cursor] = xs[index];
activeZ[cursor] = zs[index];
activeLow[cursor] = lows[index];
activeHigh[cursor] = highs[index];
activeIndex[cursor++] = index;
}
}
double[] sums = scratch.buffer(activeCount);
double[] noise = scratch.buffer(activeCount);
Arrays.fill(sums, 0, activeCount, 0D);
GeneratorProgram[] compiled = programs[group];
for (int member = 0; member < members.length; member++) {
GeneratorProgram program = compiled[member];
if (program != null) {
program.heights(scratch, activeX, activeZ, activeCount, noise);
} else {
for (int index = 0; index < activeCount; index++) {
noise[index] = members[member].getHeight(activeX[index], activeZ[index], heightSeed);
}
}
for (int index = 0; index < activeCount; index++) {
if (!Double.isFinite(noise[index])) {
throw new IllegalStateException("Terrain generator noise was not finite at " + activeX[index] + "," + activeZ[index]
+ " (generator=" + members[member].getLoadKey() + ", noise=" + noise[index]
+ ", bounds=" + activeLow[index] + ".." + activeHigh[index] + ").");
}
sums[index] += M.lerp(activeLow[index], activeHigh[index], noise[index]);
}
}
for (int index = 0; index < activeCount; index++) {
heights[activeIndex[index]] += sums[index] / members.length;
}
evaluations.add((long) activeCount * members.length);
scratch.reset(mark);
scratch.resetIndices(indexMark);
}
}
@@ -0,0 +1,201 @@
package art.arcane.iris.generation.noise;
import art.arcane.volmlib.util.interpolation.IrisInterpolation;
import java.util.Arrays;
public final class GeneratorProgram {
private static final int SURFACE_GRID = 6;
private final IrisGenerator generator;
private final long superSeed;
private final NoiseLayerProgram[] layers;
private final double totalOpacity;
private final NoiseLayerProgram cliffs;
private final boolean multiplicitive;
private final double opacity;
private final double offsetX;
private final double offsetZ;
private final double zoom;
private final double surfaceDetail;
private final double cliffHeightMin;
private final double cliffHeightMax;
private final boolean cellCracks;
GeneratorProgram(IrisGenerator generator, long superSeed, NoiseLayerProgram[] layers, double totalOpacity, NoiseLayerProgram cliffs) {
this.generator = generator;
this.superSeed = superSeed;
this.layers = layers;
this.totalOpacity = totalOpacity;
this.cliffs = cliffs;
this.multiplicitive = generator.isMultiplicitive();
this.opacity = generator.getOpacity();
this.offsetX = generator.getOffsetX();
this.offsetZ = generator.getOffsetZ();
this.zoom = generator.getZoom();
this.surfaceDetail = generator.getSurfaceDetail();
this.cliffHeightMin = generator.getCliffHeightMin();
this.cliffHeightMax = generator.getCliffHeightMax();
this.cellCracks = generator.getCellFractureHeight() != 0;
}
public IrisGenerator generator() {
return generator;
}
public void heights(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
if (count == 0) {
return;
}
if (layers.length == 0) {
Arrays.fill(out, 0, count, 0D);
return;
}
if (surfaceDetail == 1D) {
sample(scratch, xs, zs, count, out);
return;
}
surfaceHeights(scratch, xs, zs, count, out);
}
private void surfaceHeights(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
int mark = scratch.mark();
int indexMark = scratch.indexMark();
int[] northWest = scratch.indices(count);
int[] northEast = scratch.indices(count);
int[] southWest = scratch.indices(count);
int[] southEast = scratch.indices(count);
double[] fractionX = scratch.buffer(count);
double[] fractionZ = scratch.buffer(count);
scratch.clearLattice(count * 4);
int detailed = 0;
for (int index = 0; index < count; index++) {
double lowerX = Math.floor(xs[index] / SURFACE_GRID) * SURFACE_GRID;
double lowerZ = Math.floor(zs[index] / SURFACE_GRID) * SURFACE_GRID;
double dx = (xs[index] - lowerX) / SURFACE_GRID;
double dz = (zs[index] - lowerZ) / SURFACE_GRID;
fractionX[index] = dx;
fractionZ[index] = dz;
northWest[index] = scratch.lattice(lowerX, lowerZ);
northEast[index] = dx == 0D ? -1 : scratch.lattice(lowerX + SURFACE_GRID, lowerZ);
southWest[index] = dz == 0D ? -1 : scratch.lattice(lowerX, lowerZ + SURFACE_GRID);
southEast[index] = dx == 0D || dz == 0D ? -1 : scratch.lattice(lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID);
if (dx != 0D || dz != 0D) {
detailed++;
}
}
int nodes = scratch.latticeCount();
double[] lattice = scratch.buffer(nodes);
resolveLattice(scratch, nodes, lattice);
double[] detail = null;
int[] detailIndex = null;
if (surfaceDetail != 0D && detailed > 0) {
double[] detailX = scratch.buffer(detailed);
double[] detailZ = scratch.buffer(detailed);
detail = scratch.buffer(detailed);
detailIndex = scratch.indices(count);
int cursor = 0;
for (int index = 0; index < count; index++) {
if (fractionX[index] != 0D || fractionZ[index] != 0D) {
detailX[cursor] = xs[index];
detailZ[cursor] = zs[index];
detailIndex[index] = cursor++;
}
}
sample(scratch, detailX, detailZ, detailed, detail);
}
for (int index = 0; index < count; index++) {
double dx = fractionX[index];
double dz = fractionZ[index];
double nw = lattice[northWest[index]];
if (dx == 0D && dz == 0D) {
out[index] = nw;
continue;
}
double north = dx == 0D ? nw : IrisInterpolation.lerp(nw, lattice[northEast[index]], dx);
double smooth = north;
if (dz != 0D) {
double sw = lattice[southWest[index]];
double south = dx == 0D ? sw : IrisInterpolation.lerp(sw, lattice[southEast[index]], dx);
smooth = IrisInterpolation.lerp(north, south, dz);
}
out[index] = surfaceDetail == 0D ? smooth : smooth + (detail[detailIndex[index]] - smooth) * surfaceDetail;
}
scratch.resetIndices(indexMark);
scratch.reset(mark);
}
private void resolveLattice(TerrainBatchScratch scratch, int nodes, double[] lattice) {
IrisGenerator.SurfaceCacheStripe[] stripes = generator.surfaceStripes();
int indexMark = scratch.indexMark();
int mark = scratch.mark();
int[] missing = scratch.indices(nodes);
double[] missingX = scratch.buffer(nodes);
double[] missingZ = scratch.buffer(nodes);
int missingCount = 0;
for (int node = 0; node < nodes; node++) {
double x = scratch.latticeX(node);
double z = scratch.latticeZ(node);
if (x == (int) x && z == (int) z && IrisGenerator.cachedSurface(stripes, x, z, superSeed, lattice, node)) {
continue;
}
missing[missingCount] = node;
missingX[missingCount] = x;
missingZ[missingCount++] = z;
}
if (missingCount > 0) {
double[] sampled = scratch.buffer(missingCount);
sample(scratch, missingX, missingZ, missingCount, sampled);
for (int index = 0; index < missingCount; index++) {
int node = missing[index];
lattice[node] = sampled[index];
if (missingX[index] == (int) missingX[index] && missingZ[index] == (int) missingZ[index]) {
IrisGenerator.storeSurface(stripes, missingX[index], missingZ[index], superSeed, sampled[index]);
}
}
}
scratch.reset(mark);
scratch.resetIndices(indexMark);
}
private void sample(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
int mark = scratch.mark();
double[] sampleX = scratch.buffer(count);
double[] sampleZ = scratch.buffer(count);
double[] noise = scratch.buffer(count);
for (int index = 0; index < count; index++) {
sampleX[index] = (xs[index] + offsetX) / zoom;
sampleZ[index] = (zs[index] + offsetZ) / zoom;
}
Arrays.fill(out, 0, count, multiplicitive ? 1D : 0D);
for (NoiseLayerProgram layer : layers) {
layer.evaluate(scratch, sampleX, sampleZ, count, noise);
if (multiplicitive) {
for (int index = 0; index < count; index++) {
out[index] *= noise[index];
}
} else {
for (int index = 0; index < count; index++) {
out[index] += noise[index];
}
}
}
for (int index = 0; index < count; index++) {
double value = multiplicitive ? out[index] * opacity : (out[index] / totalOpacity) * opacity;
out[index] = Double.isNaN(value) ? 0D : value;
}
if (cliffs != null) {
cliffs.evaluate(scratch, sampleX, sampleZ, count, noise);
for (int index = 0; index < count; index++) {
double height = IrisInterpolation.lerp(cliffHeightMin, cliffHeightMax, noise[index]);
out[index] = (Math.round((out[index] * 255D) / height) * height) / 255D;
}
}
if (cellCracks) {
for (int index = 0; index < count; index++) {
out[index] = generator.cell(xs[index], zs[index], out[index], 0D);
}
}
scratch.reset(mark);
}
}
@@ -41,10 +41,7 @@ import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.DoubleBinaryOperator;
@Accessors(chain = true)
@NoArgsConstructor
@@ -211,7 +208,7 @@ public class IrisGenerator extends IrisRegistrant {
if (surfaceDetail == 1D) {
return sampleHeight(rx, rz, superSeed, data, engine);
}
return surfaceHeight(rx, rz, superSeed, data, engine, null);
return surfaceHeight(rx, rz, superSeed, data, engine);
}
public boolean supportsBatch() {
@@ -227,90 +224,84 @@ public class IrisGenerator extends IrisRegistrant {
return !hasCliffs() || cliffHeightGenerator.supportsBatch();
}
public void getHeights(double[] xs, double[] zs, long superSeed, double[] target, int count) {
Objects.checkFromIndexSize(0, count, xs.length);
Objects.checkFromIndexSize(0, count, zs.length);
Objects.checkFromIndexSize(0, count, target.length);
if (count > 4096) {
throw new IllegalArgumentException("Generator batch exceeds 4096 samples");
public GeneratorProgram compileProgram(long superSeed) {
if (!Double.isFinite(surfaceDetail) || surfaceDetail < 0D || surfaceDetail > 1D) {
throw new IllegalArgumentException("Generator surfaceDetail must be finite and between 0 and 1");
}
if (count < 8 || !supportsBatch()) {
for (int index = 0; index < count; index++) {
target[index] = getHeight(xs[index], zs[index], superSeed);
}
return;
}
if (composite.isEmpty()) {
Arrays.fill(target, 0, count, 0D);
return;
if (!supportsBatch()) {
throw new IllegalStateException("Generator " + getLoadKey() + " cannot be compiled into a batch program");
}
IrisData data = getLoader();
Engine engine = data == null ? null : data.getEngine();
DoubleBinaryOperator prepared = prepareHeight(superSeed, data, engine);
for (int index = 0; index < count; index++) {
target[index] = surfaceDetail == 1D ? prepared.applyAsDouble(xs[index], zs[index])
: surfaceHeight(xs[index], zs[index], superSeed, data, engine, prepared);
}
}
private DoubleBinaryOperator prepareHeight(long superSeed, IrisData data, Engine engine) {
int hc = (int) ((cliffHeightMin * 10) + 10 + cliffHeightMax * getSeed() + offsetX + offsetZ);
DoubleBinaryOperator[] layers = new DoubleBinaryOperator[composite.size()];
NoiseLayerProgram[] layers = new NoiseLayerProgram[composite.size()];
double totalOpacity = 0D;
for (int index = 0; index < layers.length; index++) {
IrisNoiseGenerator layer = composite.get(index);
layers[index] = layer.prepareBatch(getSeed() + superSeed + hc, data, engine);
layers[index] = layer.compileProgram(getSeed() + superSeed + hc, data, engine);
if (!multiplicitive) {
totalOpacity += layer.getOpacity();
}
}
double sumOpacity = totalOpacity;
double cliffSeed = (double) (superSeed + 294596 + hc) - 34857;
DoubleBinaryOperator cliffs = hasCliffs() ? cliffHeightGenerator.prepareBatch(
(long) (getSeed() + cliffSeed + hc), data, engine) : null;
return (rx, rz) -> {
double sampleX = (rx + offsetX) / zoom;
double sampleZ = (rz + offsetZ) / zoom;
double h = multiplicitive ? 1D : 0D;
for (DoubleBinaryOperator layer : layers) {
double sampled = layer.applyAsDouble(sampleX, sampleZ);
h = multiplicitive ? h * sampled : h + sampled;
}
double value = multiplicitive ? h * opacity : (h / sumOpacity) * opacity;
if (Double.isNaN(value)) {
value = 0D;
}
if (cliffs != null) {
double height = IrisInterpolation.lerp(cliffHeightMin, cliffHeightMax,
cliffs.applyAsDouble(sampleX, sampleZ));
value = (Math.round((value * 255D) / height) * height) / 255D;
}
return hasCellCracks() ? cell(rx, rz, value, superSeed + 48622 + hc) : value;
};
NoiseLayerProgram cliffs = hasCliffs()
? cliffHeightGenerator.compileProgram((long) (getSeed() + cliffSeed + hc), data, engine)
: null;
return new GeneratorProgram(this, superSeed, layers, totalOpacity, cliffs);
}
private double surfaceHeight(double rx, double rz, long superSeed, IrisData data, Engine engine,
DoubleBinaryOperator prepared) {
SurfaceCacheStripe[] surfaceStripes() {
IrisData data = getLoader();
return surfaceCache(data, data == null ? null : data.getEngine()).stripes();
}
static boolean cachedSurface(SurfaceCacheStripe[] stripes, double x, double z, long superSeed, double[] target, int index) {
long key = ((long) (int) x << 32) | ((int) z & 0xffffffffL);
long mixed = surfaceMix(key);
SurfaceSample cached = stripes[(int) mixed & (SURFACE_CACHE_STRIPES - 1)]
.get(key, superSeed, (int) (mixed >>> 4) & (SURFACE_CACHE_STRIPE_SIZE - 1));
if (cached == null) {
return false;
}
target[index] = cached.height();
return true;
}
static void storeSurface(SurfaceCacheStripe[] stripes, double x, double z, long superSeed, double height) {
long key = ((long) (int) x << 32) | ((int) z & 0xffffffffL);
long mixed = surfaceMix(key);
stripes[(int) mixed & (SURFACE_CACHE_STRIPES - 1)]
.put((int) (mixed >>> 4) & (SURFACE_CACHE_STRIPE_SIZE - 1), new SurfaceSample(key, superSeed, height));
}
private static long surfaceMix(long key) {
long mixed = key ^ (key >>> 33);
mixed *= 0xff51afd7ed558ccdL;
mixed ^= mixed >>> 33;
return mixed;
}
private double surfaceHeight(double rx, double rz, long superSeed, IrisData data, Engine engine) {
SurfaceCacheStripe[] stripes = surfaceCache(data, engine).stripes();
double lowerX = Math.floor(rx / SURFACE_GRID) * SURFACE_GRID;
double lowerZ = Math.floor(rz / SURFACE_GRID) * SURFACE_GRID;
double dx = (rx - lowerX) / SURFACE_GRID;
double dz = (rz - lowerZ) / SURFACE_GRID;
double northWest = cachedSurfaceHeight(lowerX, lowerZ, superSeed, stripes, data, engine, prepared);
double northWest = cachedSurfaceHeight(lowerX, lowerZ, superSeed, stripes, data, engine);
if (dx == 0D && dz == 0D) {
return northWest;
}
double north = dx == 0D ? northWest : IrisInterpolation.lerp(northWest,
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ, superSeed, stripes, data, engine, prepared), dx);
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ, superSeed, stripes, data, engine), dx);
double smooth = north;
if (dz != 0D) {
double southWest = cachedSurfaceHeight(lowerX, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine, prepared);
double southWest = cachedSurfaceHeight(lowerX, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine);
double south = dx == 0D ? southWest : IrisInterpolation.lerp(southWest,
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine, prepared), dx);
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine), dx);
smooth = IrisInterpolation.lerp(north, south, dz);
}
return surfaceDetail == 0D ? smooth
: smooth + ((prepared == null ? sampleHeight(rx, rz, superSeed, data, engine) : prepared.applyAsDouble(rx, rz)) - smooth) * surfaceDetail;
: smooth + (sampleHeight(rx, rz, superSeed, data, engine) - smooth) * surfaceDetail;
}
private double sampleHeight(double rx, double rz, long superSeed, IrisData data, Engine engine) {
@@ -350,21 +341,19 @@ public class IrisGenerator extends IrisRegistrant {
return v;
}
private double cachedSurfaceHeight(double x, double z, long superSeed, SurfaceCacheStripe[] stripes, IrisData data, Engine engine, DoubleBinaryOperator prepared) {
private double cachedSurfaceHeight(double x, double z, long superSeed, SurfaceCacheStripe[] stripes, IrisData data, Engine engine) {
if (x != (int) x || z != (int) z) {
return prepared == null ? sampleHeight(x, z, superSeed, data, engine) : prepared.applyAsDouble(x, z);
return sampleHeight(x, z, superSeed, data, engine);
}
long key = ((long) (int) x << 32) | ((int) z & 0xffffffffL);
long mixed = key ^ (key >>> 33);
mixed *= 0xff51afd7ed558ccdL;
mixed ^= mixed >>> 33;
long mixed = surfaceMix(key);
SurfaceCacheStripe stripe = stripes[(int) mixed & (SURFACE_CACHE_STRIPES - 1)];
int slot = (int) (mixed >>> 4) & (SURFACE_CACHE_STRIPE_SIZE - 1);
SurfaceSample cached = stripe.get(key, superSeed, slot);
if (cached != null) {
return cached.height();
}
double height = prepared == null ? sampleHeight(x, z, superSeed, data, engine) : prepared.applyAsDouble(x, z);
double height = sampleHeight(x, z, superSeed, data, engine);
stripe.put(slot, new SurfaceSample(key, superSeed, height));
return height;
}
@@ -457,7 +446,7 @@ public class IrisGenerator extends IrisRegistrant {
private record SurfaceSample(long key, long seed, double height) {
}
private static final class SurfaceCacheStripe {
static final class SurfaceCacheStripe {
private final AtomicReferenceArray<SurfaceSample> entries = new AtomicReferenceArray<>(SURFACE_CACHE_STRIPE_SIZE);
private SurfaceSample get(long key, long seed, int slot) {
@@ -39,6 +39,7 @@ 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 art.arcane.volmlib.util.noise.program.NoiseProgram;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -47,8 +48,8 @@ import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.DoubleBinaryOperator;
@Snippet("generator")
@Accessors(chain = true)
@@ -198,44 +199,29 @@ public class IrisNoiseGenerator {
return true;
}
DoubleBinaryOperator prepareBatch(long superSeed, IrisData data, Engine engine) {
NoiseLayerProgram compileProgram(long superSeed, IrisData data, Engine engine) {
if (!enabled) {
double value = offsetY;
return (x, z) -> value;
return NoiseLayerProgram.disabled(this);
}
if (hasDeepFracture()) {
FractureSamples samples = new FractureSamples();
if (samples.prepare(this, superSeed, data, engine)) {
return samples::sample;
int enabledChildren = 0;
for (IrisNoiseGenerator child : fracture) {
if (child.isEnabled()) {
enabledChildren++;
}
}
DoubleBinaryOperator[] children = new DoubleBinaryOperator[fracture.size()];
double[] offsets = new double[fracture.size()];
NoiseLayerProgram[] children = new NoiseLayerProgram[enabledChildren];
double[] offsets = new double[enabledChildren];
int salt = 33;
for (int index = 0; index < children.length; index++) {
IrisNoiseGenerator child = fracture.get(index);
int compiled = 0;
for (IrisNoiseGenerator child : fracture) {
if (child.isEnabled()) {
children[index] = child.prepareBatch(superSeed + seed + salt, data, engine);
offsets[index] = child.getOpacity() / 2D;
children[compiled] = child.compileProgram(superSeed + seed + salt, data, engine);
offsets[compiled++] = child.getOpacity() / 2D;
}
salt += 819;
}
CNG generator = getGenerator(superSeed, data, engine);
return (x, z) -> samplePrepared(generator, children, offsets, x, z);
}
private double samplePrepared(CNG generator, DoubleBinaryOperator[] children, double[] offsets,
double xv, double zv) {
double x = xv;
double z = zv;
for (int index = 0; index < children.length; index++) {
DoubleBinaryOperator child = children[index];
if (child != null) {
x += child.applyAsDouble(xv, zv) - offsets[index];
z += child.applyAsDouble(zv, xv) - offsets[index];
}
}
return sample(generator, x, z);
CNG generator = Objects.requireNonNull(getGenerator(superSeed, data, engine), "Noise layer generator");
return NoiseLayerProgram.of(this, NoiseProgram.compile2D(generator), children, offsets);
}
public KList<IrisNoiseGenerator> getAllComposites() {
@@ -0,0 +1,137 @@
package art.arcane.iris.generation.noise;
import art.arcane.volmlib.util.interpolation.IrisInterpolation;
import art.arcane.volmlib.util.noise.program.NoiseProgram;
import java.util.Arrays;
public final class NoiseLayerProgram {
private final boolean enabled;
private final double offsetY;
private final NoiseProgram program;
private final double zoom;
private final double opacity;
private final double offsetX;
private final double offsetZ;
private final double exponent;
private final boolean negative;
private final boolean parametric;
private final boolean bezier;
private final boolean sinCentered;
private final NoiseLayerProgram[] children;
private final double[] childOffsets;
private NoiseLayerProgram(IrisNoiseGenerator layer, NoiseProgram program, NoiseLayerProgram[] children, double[] childOffsets) {
this.enabled = program != null;
this.offsetY = layer.getOffsetY();
this.program = program;
this.zoom = layer.getZoom();
this.opacity = layer.getOpacity();
this.offsetX = layer.getOffsetX();
this.offsetZ = layer.getOffsetZ();
this.exponent = layer.getExponent();
this.negative = layer.isNegative();
this.parametric = layer.isParametric();
this.bezier = layer.isBezier();
this.sinCentered = layer.isSinCentered();
this.children = children;
this.childOffsets = childOffsets;
}
static NoiseLayerProgram disabled(IrisNoiseGenerator layer) {
return new NoiseLayerProgram(layer, null, new NoiseLayerProgram[0], new double[0]);
}
static NoiseLayerProgram of(IrisNoiseGenerator layer, NoiseProgram program, NoiseLayerProgram[] children, double[] childOffsets) {
return new NoiseLayerProgram(layer, program, children, childOffsets);
}
public void evaluate(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) {
if (!enabled) {
Arrays.fill(out, 0, count, offsetY);
return;
}
int mark = scratch.mark();
double[] sampleX = scratch.buffer(count);
double[] sampleZ = scratch.buffer(count);
if (children.length == 0) {
for (int index = 0; index < count; index++) {
sampleX[index] = (xs[index] / zoom) + offsetX;
sampleZ[index] = (zs[index] / zoom) + offsetZ;
}
} else {
System.arraycopy(xs, 0, sampleX, 0, count);
System.arraycopy(zs, 0, sampleZ, 0, count);
double[] forward = scratch.buffer(count);
double[] reverse = scratch.buffer(count);
for (int child = 0; child < children.length; child++) {
children[child].evaluatePair(scratch, xs, zs, count, forward, reverse);
double offset = childOffsets[child];
for (int index = 0; index < count; index++) {
sampleX[index] += forward[index] - offset;
sampleZ[index] += reverse[index] - offset;
}
}
for (int index = 0; index < count; index++) {
sampleX[index] = (sampleX[index] / zoom) + offsetX;
sampleZ[index] = (sampleZ[index] / zoom) + offsetZ;
}
}
program.fill2D(scratch.noise(), sampleX, sampleZ, count, out);
for (int index = 0; index < count; index++) {
out[index] = post(out[index]);
}
scratch.reset(mark);
}
void evaluatePair(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] forward, double[] reverse) {
if (!enabled) {
Arrays.fill(forward, 0, count, offsetY);
Arrays.fill(reverse, 0, count, offsetY);
return;
}
int mark = scratch.mark();
int pairs = count * 2;
double[] sampleX = scratch.buffer(pairs);
double[] sampleZ = scratch.buffer(pairs);
System.arraycopy(xs, 0, sampleX, 0, count);
System.arraycopy(zs, 0, sampleZ, 0, count);
System.arraycopy(zs, 0, sampleX, count, count);
System.arraycopy(xs, 0, sampleZ, count, count);
if (children.length > 0) {
double[] childForward = scratch.buffer(count);
double[] childReverse = scratch.buffer(count);
for (int child = 0; child < children.length; child++) {
children[child].evaluatePair(scratch, xs, zs, count, childForward, childReverse);
double offset = childOffsets[child];
for (int index = 0; index < count; index++) {
sampleX[index] += childForward[index] - offset;
sampleZ[index] += childReverse[index] - offset;
sampleX[count + index] += childReverse[index] - offset;
sampleZ[count + index] += childForward[index] - offset;
}
}
}
for (int index = 0; index < pairs; index++) {
sampleX[index] = (sampleX[index] / zoom) + offsetX;
sampleZ[index] = (sampleZ[index] / zoom) + offsetZ;
}
double[] values = scratch.buffer(pairs);
program.fill2D(scratch.noise(), sampleX, sampleZ, pairs, values);
for (int index = 0; index < count; index++) {
forward[index] = post(values[index]);
reverse[index] = post(values[count + index]);
}
scratch.reset(mark);
}
private double post(double raw) {
double n = raw * 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;
}
}
@@ -0,0 +1,115 @@
package art.arcane.iris.generation.noise;
import art.arcane.volmlib.util.noise.program.NoiseScratch;
import java.util.Arrays;
public final class TerrainBatchScratch {
private static final int MINIMUM_BUFFER = 256;
private final NoiseScratch noise = new NoiseScratch();
private double[][] buffers = new double[32][];
private int[][] indexBuffers = new int[8][];
private int top;
private int indexTop;
private long[] latticeX = new long[64];
private long[] latticeZ = new long[64];
private int[] latticeSlots = new int[64];
private int latticeCapacity = 64;
private int latticeCount;
public NoiseScratch noise() {
return noise;
}
public int mark() {
return top;
}
public int indexMark() {
return indexTop;
}
public void reset(int mark) {
top = mark;
}
public void resetIndices(int mark) {
indexTop = mark;
}
public double[] buffer(int length) {
if (top == buffers.length) {
buffers = Arrays.copyOf(buffers, top * 2);
}
double[] buffer = buffers[top];
if (buffer == null || buffer.length < length) {
buffer = new double[Math.max(length, MINIMUM_BUFFER)];
buffers[top] = buffer;
}
top++;
return buffer;
}
public int[] indices(int length) {
if (indexTop == indexBuffers.length) {
indexBuffers = Arrays.copyOf(indexBuffers, indexTop * 2);
}
int[] buffer = indexBuffers[indexTop];
if (buffer == null || buffer.length < length) {
buffer = new int[Math.max(length, MINIMUM_BUFFER)];
indexBuffers[indexTop] = buffer;
}
indexTop++;
return buffer;
}
void clearLattice(int maximumEntries) {
int capacity = Integer.highestOneBit(Math.max(16, maximumEntries * 2) - 1) << 1;
if (latticeSlots.length < capacity) {
latticeSlots = new int[capacity];
} else {
Arrays.fill(latticeSlots, 0, capacity, 0);
}
latticeCapacity = capacity;
latticeCount = 0;
}
int lattice(double x, double z) {
long xBits = Double.doubleToRawLongBits(x);
long zBits = Double.doubleToRawLongBits(z);
long mixed = xBits * 0x9E3779B97F4A7C15L ^ Long.rotateLeft(zBits * 0xC2B2AE3D27D4EB4FL, 31);
mixed ^= mixed >>> 29;
int mask = latticeCapacity - 1;
int slot = (int) mixed & mask;
while (true) {
int stored = latticeSlots[slot];
if (stored == 0) {
if (latticeCount == latticeX.length) {
latticeX = Arrays.copyOf(latticeX, latticeCount * 2);
latticeZ = Arrays.copyOf(latticeZ, latticeCount * 2);
}
latticeX[latticeCount] = xBits;
latticeZ[latticeCount] = zBits;
latticeSlots[slot] = ++latticeCount;
return latticeCount - 1;
}
if (latticeX[stored - 1] == xBits && latticeZ[stored - 1] == zBits) {
return stored - 1;
}
slot = (slot + 1) & mask;
}
}
int latticeCount() {
return latticeCount;
}
double latticeX(int index) {
return Double.longBitsToDouble(latticeX[index]);
}
double latticeZ(int index) {
return Double.longBitsToDouble(latticeZ[index]);
}
}
@@ -18,6 +18,19 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.generation.bake.BakePlane;
import art.arcane.iris.generation.bake.BakeRegistry;
import art.arcane.iris.generation.bake.ChunkBakeCache;
import art.arcane.iris.generation.bake.ChunkPlanes;
import art.arcane.iris.generation.bake.ColumnBake;
import art.arcane.iris.generation.bake.PlaneSource;
import art.arcane.iris.generation.bake.SparseHeightMemo;
import art.arcane.iris.generation.bake.TerrainProgram;
import art.arcane.iris.generation.noise.TerrainBatchScratch;
import art.arcane.iris.generation.simd.SimdSupport;
import art.arcane.iris.generation.stream.BakedDoubleStream;
import art.arcane.iris.generation.stream.BakedHeightStream;
import art.arcane.iris.generation.stream.BakedStream;
import art.arcane.iris.generation.stream.GenerationStreams;
import art.arcane.iris.generation.stream.CachedDoubleStream2D;
import art.arcane.iris.generation.stream.CachedStream2D;
@@ -118,14 +131,15 @@ import java.nio.file.Path;
import java.util.function.BiFunction;
@Data
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"})
@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"})
@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "terrainProgram", "bakeCache", "sparseBaseHeights", "biomeRegistry", "regionRegistry", "blockRegistry", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"})
@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "terrainProgram", "bakeCache", "sparseBaseHeights", "biomeRegistry", "regionRegistry", "blockRegistry", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"})
public class IrisComplex implements DataProvider {
private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 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;
private static final int TERRAIN_COLUMN_CACHE_SIZE = 524_288;
private static final int SPARSE_BASE_HEIGHT_CAPACITY = 1 << 17;
/** 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. */
@@ -141,6 +155,7 @@ public class IrisComplex implements DataProvider {
IrisGenerator::getLoadKey,
Comparator.nullsFirst(Comparator.naturalOrder())
);
private static final ThreadLocal<TerrainBatchScratch> BATCH_SCRATCH = ThreadLocal.withInitial(TerrainBatchScratch::new);
private static final InferredType[] INFERRED_BIOME_PREPARATION_ORDER = {
InferredType.LAND,
InferredType.CAVE,
@@ -153,6 +168,35 @@ public class IrisComplex implements DataProvider {
private final int biomeBoundsSamplingStep;
private transient volatile SharedCornerBounds sharedCornerBounds;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient volatile TerrainProgram terrainProgram;
@Getter(AccessLevel.NONE)
private final transient ChunkBakeCache bakeCache;
@Getter(AccessLevel.NONE)
private final transient SparseHeightMemo sparseBaseHeights = new SparseHeightMemo(SPARSE_BASE_HEIGHT_CAPACITY);
@Getter(AccessLevel.NONE)
private final transient BakeRegistry<IrisBiome> biomeRegistry = new BakeRegistry<>();
@Getter(AccessLevel.NONE)
private final transient BakeRegistry<IrisRegion> regionRegistry = new BakeRegistry<>();
@Getter(AccessLevel.NONE)
private final transient BakeRegistry<NativeBlockState> blockRegistry = new BakeRegistry<>();
@Getter(AccessLevel.NONE)
private final transient ProceduralStream<IrisRegion> regionSource;
@Getter(AccessLevel.NONE)
private final transient ProceduralStream<IrisBiome> baseBiomeSource;
@Getter(AccessLevel.NONE)
private final transient ProceduralStream<IrisBiome> caveSource;
@Getter(AccessLevel.NONE)
private final transient ProceduralStream<NativeBlockState> rockSource;
@Getter(AccessLevel.NONE)
private final transient ProceduralStream<NativeBlockState> fluidSource;
@Getter(AccessLevel.NONE)
private final transient ProceduralStream<Double> naturalHeightSource;
@Getter(AccessLevel.NONE)
private final transient ProceduralStream<IrisBiome> naturalTrueBiomeSource;
@Getter(AccessLevel.NONE)
private final transient ProceduralStream<IrisBiome> trueBiomeSource;
@Getter(AccessLevel.NONE)
private final transient IrisInterpolator[] frozenInterpolators;
@Getter(AccessLevel.NONE)
private final transient IrisGenerator[][] frozenGenerators;
@@ -213,9 +257,6 @@ public class IrisComplex implements DataProvider {
private ProceduralStream<Double> unblendedNaturalHeightStream;
private final TransitionDisplacementField transitionDisplacement;
private ProceduralStream<Double> heightStream;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private CachedDoubleStream2D cachedHeightStream;
private ProceduralStream<Integer> roundedHeighteightStream;
private ProceduralStream<Double> maxHeightStream;
private ProceduralStream<Double> overlayStream;
@@ -365,54 +406,58 @@ public class IrisComplex implements DataProvider {
return value;
});
rockStream = engine.getDimension().getRockPalette().getLayerGenerator(rng.nextParallelRNG(45), data).stream()
bakeCache = new ChunkBakeCache(new BakeSource(), cacheSize);
rockSource = engine.getDimension().getRockPalette().getLayerGenerator(rng.nextParallelRNG(45), data).stream()
.select(engine.getDimension().getRockPalette().getBlockData(data));
fluidStream = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream()
rockStream = new BakedStream<>(bakeCache, BakePlane.ROCK, rockSource);
fluidSource = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream()
.select(engine.getDimension().getFluidPalette().getBlockData(data));
fluidStream = new BakedStream<>(bakeCache, BakePlane.FLUID, fluidSource);
hydrologyFluidStreams = createHydrologyFluidStreams(engine.getDimension().getHydrology());
regionStyleStream = engine.getDimension().getRegionStyle().create(rng.nextParallelRNG(883), getData()).stream()
.zoom(engine.getDimension().getRegionZoom());
regionIdentityStream = regionStyleStream.fit(Integer.MIN_VALUE, Integer.MAX_VALUE);
regionStream = focusedRegions != null ? focusedRegions
: GenerationStreams.cache2D(RegionSelection.select(new RegionSelection.Options(
engine.getDimension(), data, regionStyleStream, imageMapRuntime, null)),
"regionStream", engine, cacheSize);
caveBiomeStream = GenerationStreams.cache2D(GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
regionSource = focusedRegions != null ? focusedRegions
: 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))
.convert((r) -> createInferredBiomeStream(r, InferredType.CAVE))
.convertAware2D(ProceduralStream::get), "caveBiomeStream", engine, cacheSize);
.convertAware2D(ProceduralStream::get);
caveBiomeStream = new BakedStream<>(bakeCache, BakePlane.CAVE_BIOME, caveSource);
inferredStreams.put(InferredType.CAVE, caveBiomeStream);
landBiomeStream = GenerationStreams.cache2D(GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
landBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
.convert((r) -> createInferredBiomeStream(r, InferredType.LAND))
.convertAware2D(ProceduralStream::get), "landBiomeStream", engine, cacheSize);
.convertAware2D(ProceduralStream::get);
inferredStreams.put(InferredType.LAND, landBiomeStream);
seaBiomeStream = GenerationStreams.cache2D(GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
seaBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
.convert((r) -> createInferredBiomeStream(r, InferredType.SEA))
.convertAware2D(ProceduralStream::get), "seaBiomeStream", engine, cacheSize);
.convertAware2D(ProceduralStream::get);
inferredStreams.put(InferredType.SEA, seaBiomeStream);
shoreBiomeStream = GenerationStreams.cache2D(GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
shoreBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z))
.convert((r) -> createInferredBiomeStream(r, InferredType.SHORE))
.convertAware2D(ProceduralStream::get), "shoreBiomeStream", engine, cacheSize);
.convertAware2D(ProceduralStream::get);
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
bridgeStream = focusedBiomes != null ? focusedBiomes.convert(IrisBiome::getInferredType) :
GenerationStreams.cache2D(ContinentGenerator.create(engine.getDimension(), rng, getData()).stream()
.convert((v) -> v >= engine.getDimension().getLandChance() ? InferredType.SEA : InferredType.LAND), "bridgeStream", engine, cacheSize);
ContinentGenerator.create(engine.getDimension(), rng, getData()).stream()
.convert((v) -> v >= engine.getDimension().getLandChance() ? InferredType.SEA : InferredType.LAND);
ProceduralStream<IrisBiome> proceduralBaseBiomeStream = focusedBiomes != null ? focusedBiomes :
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(ProceduralStream.of((x, z) -> {
bridgeStream.convertAware2D((t, x, z) -> inferredStreams.get(t).get(x, z))
.convertAware2D(this::implode);
baseBiomeSource = focusedBiomes != null || !imageMapRuntime.has(IrisImageMapApplication.BIOME)
? proceduralBaseBiomeStream : ProceduralStream.of((x, z) -> {
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
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);
}, proceduralBaseBiomeStream);
baseBiomeStream = focusedBiomes != null ? focusedBiomes : new BakedStream<>(bakeCache, BakePlane.BASE_BIOME, baseBiomeSource);
proceduralTerrainHeight = new ProceduralTerrainHeightSampler(
(x, z) -> getHeight(engine, x, z, engine.getSeedManager().getHeight()),
engine.getDimension().getTerrainSamplingStep());
baseTerrainHeightStream = GenerationStreams.cache2DDouble(
ProceduralStream.ofDouble(this::sampleUnblendedNaturalTerrainHeight),
"baseTerrainHeightStream", engine, cacheSize);
baseTerrainHeightStream = new BakedDoubleStream(bakeCache, BakePlane.BASE_HEIGHT,
ProceduralStream.ofDouble(this::sparseBaseTerrainHeight));
boolean terrain3DEnabled = false;
for (IrisBiome biome : generatorBiomes) {
if (biome.getTerrain3D() != null) {
@@ -427,18 +472,20 @@ public class IrisComplex implements DataProvider {
new Terrain3DRuntime.Options(engine.getSeedManager().getTerrain(), engine.getHeight(),
fluidHeight, data, terrain3DEnabled, TERRAIN_COLUMN_CACHE_SIZE));
unblendedNaturalHeightStream = terrain3DEnabled
? GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::sampleTerrain3DHeight), "unblendedNaturalHeightStream", engine, cacheSize)
? ProceduralStream.ofDouble(this::sampleTerrain3DHeight)
: baseTerrainHeightStream;
transitionDisplacement = transitionGenerationPlan == null ? null : new TransitionDisplacementField(
transitionGenerationPlan, new TransitionDisplacementField.Sources(
(x, z) -> unblendedNaturalHeightStream.getDouble(x, z), engine.getHeight()));
naturalHeightStream = transitionDisplacement == null ? unblendedNaturalHeightStream
: 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(ProceduralStream.of(this::sampleNaturalTrueBiome, this::sampleNaturalTrueBiome,
Interpolated.of(value -> 0D, value -> null)), "naturalTrueBiomeStream", engine, cacheSize);
naturalHeightSource = transitionDisplacement == null
? unblendedNaturalHeightStream
: ProceduralStream.ofDouble((x, z) -> transitionDisplacement.height(
blockCoordinate(x), blockCoordinate(z), unblendedNaturalHeightStream.getDouble(x, z)));
naturalHeightStream = new BakedDoubleStream(bakeCache, BakePlane.NATURAL_HEIGHT, naturalHeightSource);
naturalTrueBiomeSource = focusedBiomes != null ? focusedBiomes
: ProceduralStream.of(this::sampleNaturalTrueBiome, this::sampleNaturalTrueBiome,
Interpolated.of(value -> 0D, value -> null));
naturalTrueBiomeStream = new BakedStream<>(bakeCache, BakePlane.NATURAL_TRUE_BIOME, naturalTrueBiomeSource);
IrisHydrology configuredHydrology = engine.getDimension().getHydrology();
if (hydrologyActive(configuredHydrology)) {
hydrologyRuntime = new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext(
@@ -446,9 +493,9 @@ public class IrisComplex implements DataProvider {
engine.getHeight(),
engine.getDimension(),
data,
(x, z, naturalHeight) -> sampleHydrologyNatural(engine, x, z, naturalHeight),
(x, z, naturalHeight) -> ChunkBakeCache.sparse(() -> sampleHydrologyNatural(engine, x, z, naturalHeight)),
new HydrologyHeightProvider(),
(x, z) -> describeNaturalHeight(engine, x, z),
(x, z) -> ChunkBakeCache.sparse(() -> describeNaturalHeight(engine, x, z)),
this::sampleNaturalOcean,
footprint -> new MantleHydrologyCaveVoxelView(engine, this, footprint),
() -> engine.getPlatformHooks().isMainThread(),
@@ -467,8 +514,8 @@ public class IrisComplex implements DataProvider {
Math.max(4096, cacheSize), data, banks3D));
}
}
cachedHeightStream = new TerrainHeightStream(engine, cacheSize);
heightStream = cachedHeightStream;
heightStream = new BakedHeightStream(bakeCache, () -> terrainEngine.getPlatformHooks().isMainThread(),
this::nonblockingTerrainHeight, this::resolveHydrologyTerrainHeight);
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))
@@ -476,8 +523,9 @@ public class IrisComplex implements DataProvider {
naturalSlopeStream = GenerationStreams.cache2DDouble(naturalHeightStream.slope(3), "naturalSlopeStream", engine, cacheSize);
// Terrain3D column heights resolve the surface biome, so the shore band reads the pre-terrain3D slope or it recurses.
shoreSlopeStream = terrain3DEnabled ? baseTerrainHeightStream.slope(3) : naturalSlopeStream;
trueBiomeStream = focusedBiomes != null ? GenerationStreams.cache2D(focusedBiomes, "trueBiomeStream-focus", engine, cacheSize) : GenerationStreams.cache2D(heightStream
.convertAware2D((terrainHeight, x, z) -> resolveHydrologySurfaceBiome(terrainHeight, x, z)), "trueBiomeStream", engine, cacheSize);
trueBiomeSource = focusedBiomes != null ? focusedBiomes
: heightStream.convertAware2D((terrainHeight, x, z) -> resolveHydrologySurfaceBiome(terrainHeight, x, z));
trueBiomeStream = new BakedStream<>(bakeCache, BakePlane.TRUE_BIOME, trueBiomeSource);
riverDistanceStream = GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::resolveHydrologyDistance), "riverDistanceStream", engine, cacheSize);
riverFlowStream = GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::resolveHydrologyFlow), "riverFlowStream", engine, cacheSize);
riverCarveWeightStream = GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::resolveHydrologyCarveWeight), "riverCarveWeightStream", engine, cacheSize);
@@ -518,17 +566,11 @@ public class IrisComplex implements DataProvider {
return engine.isStudio() && !detached ? Math.max(configuredSize, STUDIO_NOISE_CACHE_SIZE) : configuredSize;
}
public synchronized void ensureTerrainNoiseCacheSize(int minimumChunks) {
public void ensureTerrainNoiseCacheSize(int minimumChunks) {
if (minimumChunks <= 0) {
throw new IllegalArgumentException("Terrain noise cache size must be positive");
}
CachedDoubleStream2D naturalCache = (CachedDoubleStream2D) naturalHeightStream;
if (naturalCache.getMaxSize() < minimumChunks * 256L) {
naturalCache.setMaximumChunks(minimumChunks);
}
if (cachedHeightStream.getMaxSize() < minimumChunks * 256L) {
cachedHeightStream.setMaximumChunks(minimumChunks);
}
bakeCache.ensureCapacity(minimumChunks);
}
void expandHydrologyNoiseCaches() {
@@ -787,96 +829,220 @@ public class IrisComplex implements DataProvider {
return true;
}
private void sampleBaseHeightBatch(long[] coordinates, int count, double[] target) {
((CachedDoubleStream2D) baseTerrainHeightStream).getBatch(coordinates, count, target, this::resolveBaseHeightBatch);
private void resolveBaseHeightBatch(long[] missing, int size, double[] values) {
TerrainBatchScratch scratch = BATCH_SCRATCH.get();
int mark = scratch.mark();
int indexMark = scratch.indexMark();
try {
double[] xs = scratch.buffer(size);
double[] zs = scratch.buffer(size);
double[] lows = scratch.buffer(size);
double[] highs = scratch.buffer(size);
Arrays.fill(values, 0, size, 0D);
for (int index = 0; index < size; index++) {
xs[index] = RiverFootprint.unpackX(missing[index]);
zs[index] = RiverFootprint.unpackZ(missing[index]);
}
TerrainProgram program = terrainProgram();
for (int group = 0; group < frozenInterpolators.length; group++) {
if (frozenGenerators[group].length == 0) {
continue;
}
for (int index = 0; index < size; index++) {
NoiseBounds bounds = gridSampleBounds(terrainEngine, frozenInterpolators[group], group,
frozenGenerators[group], xs[index], zs[index]);
lows[index] = bounds.min();
highs[index] = bounds.max();
}
program.accumulate(group, scratch, xs, zs, size, lows, highs, values);
}
for (int index = 0; index < size; index++) {
values[index] = finishBaseHeight(xs[index], zs[index], values[index]);
}
} finally {
scratch.reset(mark);
scratch.resetIndices(indexMark);
}
}
private void resolveBaseHeightBatch(long[] missing, int size, double[] values) {
double[] xs = new double[size];
double[] zs = new double[size];
double[] lows = new double[size];
double[] highs = new double[size];
double[] activeXs = new double[size];
double[] activeZs = new double[size];
double[] noise = new double[size];
double[] groupHeights = new double[size];
int[] active = new int[size];
for (int index = 0; index < size; index++) {
xs[index] = RiverFootprint.unpackX(missing[index]);
zs[index] = RiverFootprint.unpackZ(missing[index]);
private double[] bakeBaseHeights(int chunkX, int chunkZ) {
double[] values = new double[256];
int originX = chunkX << 4;
int originZ = chunkZ << 4;
if (!supportsChunkHeightBatch()) {
for (int index = 0; index < 256; index++) {
values[index] = sampleUnblendedNaturalTerrainHeight(originX + (index & 15), originZ + (index >> 4));
}
return values;
}
long seed = terrainEngine.getSeedManager().getHeight() + 239945;
for (int group = 0; group < frozenInterpolators.length; group++) {
IrisGenerator[] generators = frozenGenerators[group];
if (generators.length == 0) {
TerrainBatchScratch scratch = BATCH_SCRATCH.get();
int mark = scratch.mark();
int indexMark = scratch.indexMark();
try {
double[] xs = scratch.buffer(256);
double[] zs = scratch.buffer(256);
double[] lows = scratch.buffer(256);
double[] highs = scratch.buffer(256);
for (int index = 0; index < 256; index++) {
xs[index] = originX + (index & 15);
zs[index] = originZ + (index >> 4);
}
TerrainProgram program = terrainProgram();
GridBoundsCache cache = gridBoundsCache.get();
int grid = biomeBoundsSamplingStep;
boolean corners = ChunkCorners.supports(grid);
for (int group = 0; group < frozenInterpolators.length; group++) {
if (frozenGenerators[group].length == 0) {
continue;
}
if (corners) {
if (!chunkCornerBounds(cache, group, grid, originX, originZ, xs, zs, lows, highs)) {
continue;
}
} else {
for (int index = 0; index < 256; index++) {
NoiseBounds bounds = gridSampleBounds(terrainEngine, frozenInterpolators[group], group,
frozenGenerators[group], xs[index], zs[index]);
lows[index] = bounds.min();
highs[index] = bounds.max();
}
}
program.accumulate(group, scratch, xs, zs, 256, lows, highs, values);
}
for (int index = 0; index < 256; index++) {
values[index] = finishBaseHeight(xs[index], zs[index], values[index]);
}
} finally {
scratch.reset(mark);
scratch.resetIndices(indexMark);
}
return values;
}
private boolean chunkCornerBounds(GridBoundsCache cache, int group, int grid, int originX, int originZ,
double[] xs, double[] zs, double[] lows, double[] highs) {
int cells = 16 / grid;
int stride = cells + 1;
long[] packed = new long[stride * stride];
boolean silent = true;
for (int row = 0; row < stride; row++) {
for (int column = 0; column < stride; column++) {
long corner = cornerBounds(cache, terrainEngine, frozenInterpolators[group], group,
frozenGenerators[group], originX + column * grid, originZ + row * grid);
packed[row * stride + column] = corner;
silent &= silentBounds(corner);
}
}
if (silent) {
return false;
}
for (int index = 0; index < 256; index++) {
double x = xs[index];
double z = zs[index];
int gx = ((int) Math.floor(x)) & ~(grid - 1);
int gz = ((int) Math.floor(z)) & ~(grid - 1);
double fx = (x - gx) / grid;
double fz = (z - gz) / grid;
int cornerIndex = ((gz - originZ) / grid) * stride + (gx - originX) / grid;
long b00 = packed[cornerIndex];
if (fx == 0D && fz == 0D) {
lows[index] = boundsLow(b00);
highs[index] = boundsHigh(b00);
} else if (fz == 0D) {
long b10 = packed[cornerIndex + 1];
lows[index] = biLerp(boundsLow(b00), boundsLow(b10), boundsLow(b00), boundsLow(b10), fx, fz);
highs[index] = biLerp(boundsHigh(b00), boundsHigh(b10), boundsHigh(b00), boundsHigh(b10), fx, fz);
} else if (fx == 0D) {
long b01 = packed[cornerIndex + stride];
lows[index] = biLerp(boundsLow(b00), boundsLow(b00), boundsLow(b01), boundsLow(b01), fx, fz);
highs[index] = biLerp(boundsHigh(b00), boundsHigh(b00), boundsHigh(b01), boundsHigh(b01), fx, fz);
} else {
long b10 = packed[cornerIndex + 1];
long b01 = packed[cornerIndex + stride];
long b11 = packed[cornerIndex + stride + 1];
lows[index] = biLerp(boundsLow(b00), boundsLow(b10), boundsLow(b01), boundsLow(b11), fx, fz);
highs[index] = biLerp(boundsHigh(b00), boundsHigh(b10), boundsHigh(b01), boundsHigh(b11), fx, fz);
}
}
return true;
}
private boolean supportsChunkHeightBatch() {
return terrainEngine.getDimension().getTerrainSamplingStep() == 1 && biomeBuffetGenerators.isEmpty()
&& (biomeBuffet == null || biomeBuffet.cells().isEmpty());
}
private double finishBaseHeight(double x, double z, double interpolated) {
double height = Math.max(Math.min(interpolated + fluidHeight + overlayStream.get(x, z), terrainEngine.getHeight()), 0);
return imageMapRuntime.sampleTerrainHeight(x, z, height);
}
private TerrainProgram terrainProgram() {
TerrainProgram program = terrainProgram;
if (program == null) {
synchronized (this) {
program = terrainProgram;
if (program == null) {
program = TerrainProgram.compile(frozenGenerators, terrainEngine.getSeedManager().getHeight() + 239945);
terrainProgram = program;
}
}
}
return program;
}
private double sparseBaseTerrainHeight(double x, double z) {
int blockX = (int) x;
int blockZ = (int) z;
if (x != blockX || z != blockZ) {
return sampleUnblendedNaturalTerrainHeight(x, z);
}
double memoized = sparseBaseHeights.get(blockX, blockZ);
if (!Double.isNaN(memoized)) {
return memoized;
}
double height = sampleUnblendedNaturalTerrainHeight(x, z);
sparseBaseHeights.put(blockX, blockZ, height);
return height;
}
private void sampleNaturalBatch(long[] coordinates, int count, double[] heights) {
long[] required = new long[count];
int requiredCount = 0;
for (int index = 0; index < count; index++) {
int x = RiverFootprint.unpackX(coordinates[index]);
int z = RiverFootprint.unpackZ(coordinates[index]);
if (publishedPlane(BakePlane.NATURAL_HEIGHT, x, z) != null
|| terrain3D != null && !terrain3D.requiresBaseHeight(x, z)
|| publishedPlane(BakePlane.BASE_HEIGHT, x, z) != null
|| !Double.isNaN(sparseBaseHeights.get(x, z))) {
continue;
}
int activeCount = 0;
Arrays.fill(groupHeights, 0D);
for (int index = 0; index < size; index++) {
NoiseBounds bounds = gridSampleBounds(terrainEngine, frozenInterpolators[group], group,
generators, xs[index], zs[index]);
lows[index] = bounds.min();
highs[index] = bounds.max();
if (!Double.isFinite(lows[index]) || !Double.isFinite(highs[index])) {
averageGeneratorHeights(generators, lows[index], highs[index], xs[index], zs[index], seed);
}
if (lows[index] != highs[index]) {
active[activeCount] = index;
activeXs[activeCount] = xs[index];
activeZs[activeCount++] = zs[index];
} else {
for (int generator = 0; generator < generators.length; generator++) {
groupHeights[index] += lows[index];
}
}
}
for (IrisGenerator generator : generators) {
if (activeCount == 0) {
break;
}
generator.getHeights(activeXs, activeZs, seed, noise, activeCount);
for (int sample = 0; sample < activeCount; sample++) {
int index = active[sample];
if (!Double.isFinite(noise[sample])) {
throw new IllegalStateException("Terrain generator noise was not finite at " + xs[index] + "," + zs[index]
+ " (generator=" + generator.getLoadKey() + ", noise=" + noise[sample]
+ ", bounds=" + lows[index] + ".." + highs[index] + ").");
}
groupHeights[index] += M.lerp(lows[index], highs[index], noise[sample]);
}
}
for (int index = 0; index < size; index++) {
values[index] += groupHeights[index] / generators.length;
required[requiredCount++] = coordinates[index];
}
if (requiredCount > 0) {
double[] baseHeights = new double[requiredCount];
resolveBaseHeightBatch(required, requiredCount, baseHeights);
for (int index = 0; index < requiredCount; index++) {
sparseBaseHeights.put(RiverFootprint.unpackX(required[index]), RiverFootprint.unpackZ(required[index]), baseHeights[index]);
}
}
for (int index = 0; index < size; index++) {
double height = Math.max(Math.min(values[index] + fluidHeight + overlayStream.get(xs[index], zs[index]),
terrainEngine.getHeight()), 0);
values[index] = imageMapRuntime.sampleTerrainHeight(xs[index], zs[index], height);
BakedDoubleStream naturalHeights = (BakedDoubleStream) naturalHeightStream;
for (int index = 0; index < count; index++) {
heights[index] = naturalHeights.sparse(RiverFootprint.unpackX(coordinates[index]), RiverFootprint.unpackZ(coordinates[index]));
}
}
private void sampleUnblendedHeightBatch(long[] coordinates, int count, double[] target) {
if (terrain3D == null) {
sampleBaseHeightBatch(coordinates, count, target);
return;
}
((CachedDoubleStream2D) unblendedNaturalHeightStream).getBatch(coordinates, count, target, (missing, size, values) -> {
long[] required = new long[size];
int requiredCount = 0;
for (int index = 0; index < size; index++) {
if (terrain3D.requiresBaseHeight(RiverFootprint.unpackX(missing[index]), RiverFootprint.unpackZ(missing[index]))) {
required[requiredCount++] = missing[index];
}
}
if (requiredCount > 0) {
sampleBaseHeightBatch(required, requiredCount, new double[requiredCount]);
}
for (int index = 0; index < size; index++) {
values[index] = sampleTerrain3DHeight(RiverFootprint.unpackX(missing[index]), RiverFootprint.unpackZ(missing[index]));
}
});
@SuppressWarnings("unchecked")
private IrisBiome sparseBaseBiome(double x, double z) {
return baseBiomeStream instanceof BakedStream<?> baked
? ((BakedStream<IrisBiome>) baked).sparse((int) x, (int) z)
: baseBiomeStream.get(x, z);
}
private Object publishedPlane(BakePlane plane, int blockX, int blockZ) {
ChunkPlanes planes = bakeCache.planesIfPresent(blockX >> 4, blockZ >> 4);
return planes == null ? null : planes.published(plane);
}
private final class HydrologyHeightProvider implements IrisHydrologyNaturalHeightProvider {
@@ -884,7 +1050,7 @@ public class IrisComplex implements DataProvider {
@Override
public double sample(int blockX, int blockZ) {
return naturalHeightStream.getDouble(blockX, blockZ);
return ((BakedDoubleStream) naturalHeightStream).sparse(blockX, blockZ);
}
@Override
@@ -896,20 +1062,165 @@ public class IrisComplex implements DataProvider {
public void sampleBatch(long[] coordinates, int count, double[] heights) {
if (!batch) {
IrisHydrologyNaturalHeightProvider.super.sampleBatch(coordinates, count, heights);
} else if (transitionDisplacement == null) {
sampleUnblendedHeightBatch(coordinates, count, heights);
} else {
((CachedDoubleStream2D) naturalHeightStream).getBatch(coordinates, count, heights, (missing, size, values) -> {
sampleUnblendedHeightBatch(missing, size, values);
for (int index = 0; index < size; index++) {
values[index] = transitionDisplacement.height(RiverFootprint.unpackX(missing[index]),
RiverFootprint.unpackZ(missing[index]), values[index]);
}
});
return;
}
ChunkBakeCache.enterSparse();
try {
sampleNaturalBatch(coordinates, count, heights);
} finally {
ChunkBakeCache.exitSparse();
}
}
}
private final class BakeSource implements PlaneSource {
@Override
public Object compute(BakePlane plane, int chunkX, int chunkZ) {
return switch (plane) {
case REGION -> objectPlane(regionSource, chunkX, chunkZ);
case BASE_BIOME -> objectPlane(baseBiomeSource, chunkX, chunkZ);
case BASE_HEIGHT -> bakeBaseHeights(chunkX, chunkZ);
case NATURAL_HEIGHT -> doublePlane(naturalHeightSource, chunkX, chunkZ);
case NATURAL_TRUE_BIOME -> objectPlane(naturalTrueBiomeSource, chunkX, chunkZ);
case HEIGHT -> heightPlane(chunkX, chunkZ);
case TRUE_BIOME -> objectPlane(trueBiomeSource, chunkX, chunkZ);
case ROCK -> objectPlane(rockSource, chunkX, chunkZ);
case FLUID -> objectPlane(fluidSource, chunkX, chunkZ);
case CAVE_BIOME -> objectPlane(caveSource, chunkX, chunkZ);
};
}
@Override
public ColumnBake assemble(ChunkPlanes planes) {
return assembleColumnBake(planes.chunkX(), planes.chunkZ());
}
@Override
public boolean mainThread() {
return terrainEngine.getPlatformHooks().isMainThread();
}
@Override
public boolean generationContext() {
IrisContext context = IrisContext.get();
if (context == null || context.getEngine() != terrainEngine) {
return false;
}
ChunkContext chunk = context.getChunkContext();
return chunk != null && chunk.getComplex() == IrisComplex.this;
}
private Object[] objectPlane(ProceduralStream<?> source, int chunkX, int chunkZ) {
Object[] values = new Object[256];
int originX = chunkX << 4;
int originZ = chunkZ << 4;
for (int index = 0; index < 256; index++) {
values[index] = source.get(originX + (index & 15), originZ + (index >> 4));
}
return values;
}
private double[] heightPlane(int chunkX, int chunkZ) {
double[] values = new double[256];
int originX = chunkX << 4;
int originZ = chunkZ << 4;
boolean main = terrainEngine.getPlatformHooks().isMainThread();
for (int index = 0; index < 256; index++) {
int x = originX + (index & 15);
int z = originZ + (index >> 4);
values[index] = main ? nonblockingTerrainHeight(x, z) : resolveHydrologyTerrainHeight(x, z);
}
return values;
}
private double[] doublePlane(ProceduralStream<Double> source, int chunkX, int chunkZ) {
double[] values = new double[256];
int originX = chunkX << 4;
int originZ = chunkZ << 4;
for (int index = 0; index < 256; index++) {
values[index] = source.getDouble(originX + (index & 15), originZ + (index >> 4));
}
return values;
}
}
public Object bakePlane(BakePlane plane, int chunkX, int chunkZ) {
return bakeCache.bake(plane, chunkX, chunkZ);
}
public ColumnBake columnBake(int chunkX, int chunkZ) {
return bakeCache.get(chunkX, chunkZ);
}
public ColumnBake columnBakeIfPresent(int chunkX, int chunkZ) {
return bakeCache.getIfPresent(chunkX, chunkZ);
}
public IrisBiome biome(int id) {
return id < 0 ? null : biomeRegistry.value(id);
}
public short biomeId(IrisBiome biome) {
return biome == null ? -1 : biomeRegistry.id(biome);
}
public IrisRegion region(int id) {
return id < 0 ? null : regionRegistry.value(id);
}
public short regionId(IrisRegion region) {
return region == null ? -1 : regionRegistry.id(region);
}
public NativeBlockState blockState(int id) {
return id < 0 ? null : blockRegistry.value(id);
}
private ColumnBake assembleColumnBake(int chunkX, int chunkZ) {
double[] height = (double[]) bakeCache.bake(BakePlane.HEIGHT, chunkX, chunkZ);
int[] rounded = new int[256];
SimdSupport.kernels().roundToInt(height, rounded, 256);
int originX = chunkX << 4;
int originZ = chunkZ << 4;
float[] fluidSurface = new float[256];
Terrain3DColumn[] terrainColumns = new Terrain3DColumn[256];
for (int index = 0; index < 256; index++) {
int x = originX + (index & 15);
int z = originZ + (index >> 4);
fluidSurface[index] = (float) riverWaterSurfaceStream.getDouble(x, z);
terrainColumns[index] = terrainColumn(x, z);
}
double[] apron = new double[ColumnBake.APRON_WIDTH * ColumnBake.APRON_WIDTH];
for (int row = 0; row < ColumnBake.APRON_WIDTH; row++) {
for (int column = 0; column < ColumnBake.APRON_WIDTH; column++) {
apron[row * ColumnBake.APRON_WIDTH + column] = heightStream.getDouble(originX - 1 + column, originZ - 1 + row);
}
}
return new ColumnBake(new ColumnBake.Planes(chunkX, chunkZ,
ids((Object[]) bakeCache.bake(BakePlane.REGION, chunkX, chunkZ), regionRegistry),
ids((Object[]) bakeCache.bake(BakePlane.BASE_BIOME, chunkX, chunkZ), biomeRegistry),
ids((Object[]) bakeCache.bake(BakePlane.TRUE_BIOME, chunkX, chunkZ), biomeRegistry),
ids((Object[]) bakeCache.bake(BakePlane.CAVE_BIOME, chunkX, chunkZ), biomeRegistry),
(double[]) bakeCache.bake(BakePlane.BASE_HEIGHT, chunkX, chunkZ),
(double[]) bakeCache.bake(BakePlane.NATURAL_HEIGHT, chunkX, chunkZ),
height,
rounded,
fluidSurface,
ids((Object[]) bakeCache.bake(BakePlane.ROCK, chunkX, chunkZ), blockRegistry),
ids((Object[]) bakeCache.bake(BakePlane.FLUID, chunkX, chunkZ), blockRegistry),
terrainColumns,
apron));
}
@SuppressWarnings("unchecked")
private static <T> short[] ids(Object[] values, BakeRegistry<T> registry) {
short[] ids = new short[values.length];
for (int index = 0; index < values.length; index++) {
ids[index] = values[index] == null ? -1 : registry.id((T) values[index]);
}
return ids;
}
private double sampleUnblendedNaturalTerrainHeight(double x, double z) {
double proceduralHeight = proceduralTerrainHeight.sample(x, z);
return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight);
@@ -2495,7 +2806,8 @@ public class IrisComplex implements DataProvider {
this.engine = engine;
this.generators = generators;
this.cachedBounds = cachedBounds;
this.integerCoordinates = complex.baseBiomeStream instanceof CachedStream2D<?>;
this.integerCoordinates = complex.baseBiomeStream instanceof BakedStream<?>
|| complex.baseBiomeStream instanceof CachedStream2D<?>;
this.inUse = true;
if (sampleSource.get() != complex.baseBiomeStream) {
sampleCache.clear();
@@ -2522,7 +2834,7 @@ public class IrisComplex implements DataProvider {
double sampleZ = integerCoordinates ? (int) zz : zz;
IrisBiome bx = sampleCache.get(sampleX, sampleZ);
if (bx == null) {
bx = complex.baseBiomeStream.get(xx, zz);
bx = complex.sparseBaseBiome(xx, zz);
sampleCache.put(sampleX, sampleZ, bx);
}
@@ -2592,62 +2904,6 @@ 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;
@@ -2698,6 +2954,8 @@ public class IrisComplex implements DataProvider {
hydrologyRuntime.close();
}
releaseHydrologyNoiseCaches();
bakeCache.clear();
sparseBaseHeights.clear();
if (transitionDisplacement != null) {
transitionDisplacement.clear();
}
@@ -0,0 +1,109 @@
package art.arcane.iris.generation.stream;
import art.arcane.iris.generation.bake.BakePlane;
import art.arcane.iris.generation.bake.ChunkBakeCache;
import art.arcane.iris.generation.bake.ChunkPlanes;
import art.arcane.volmlib.util.stream.BasicStream;
import art.arcane.volmlib.util.stream.ProceduralStream;
import java.util.Objects;
public final class BakedDoubleStream extends BasicStream<Double> implements ChunkFillableStream2D, ChunkFillableDoubleStream2D {
private final ChunkBakeCache cache;
private final BakePlane plane;
private final ProceduralStream<Double> scalar;
public BakedDoubleStream(ChunkBakeCache cache, BakePlane plane, ProceduralStream<Double> scalar) {
super();
this.cache = Objects.requireNonNull(cache, "Bake cache");
this.plane = Objects.requireNonNull(plane, "Bake plane");
this.scalar = Objects.requireNonNull(scalar, "Scalar source");
}
public BakePlane plane() {
return plane;
}
public ProceduralStream<Double> scalar() {
return scalar;
}
@Override
public Double get(double x, double z) {
return getDouble(x, z);
}
@Override
public double getDouble(double x, double z) {
int blockX = (int) x;
int blockZ = (int) z;
Object values = cache.read(plane, blockX >> 4, blockZ >> 4);
return values == null ? scalar.getDouble(blockX, blockZ) : ((double[]) values)[((blockZ & 15) << 4) | (blockX & 15)];
}
public double sparse(int blockX, int blockZ) {
Object published = publishedPlane(blockX >> 4, blockZ >> 4);
if (published != null) {
return ((double[]) published)[((blockZ & 15) << 4) | (blockX & 15)];
}
ChunkBakeCache.enterSparse();
try {
return scalar.getDouble(blockX, blockZ);
} finally {
ChunkBakeCache.exitSparse();
}
}
@Override
public Double get(double x, double y, double z) {
return scalar.get(x, y, z);
}
@Override
public double getDouble(double x, double y, double z) {
return scalar.getDouble(x, y, z);
}
@Override
public double toDouble(Double t) {
return t;
}
@Override
public Double fromDouble(double d) {
return d;
}
@Override
public void fillChunkDoubles(int worldX, int worldZ, double[] target) {
if (target == null || target.length != 256) {
throw new IllegalArgumentException("Expected a 16x16 target array.");
}
int chunkX = worldX >> 4;
int chunkZ = worldZ >> 4;
Object values = cache.bakingAllowed() ? cache.bake(plane, chunkX, chunkZ) : publishedPlane(chunkX, chunkZ);
if (values != null) {
System.arraycopy(values, 0, target, 0, 256);
return;
}
int originX = chunkX << 4;
int originZ = chunkZ << 4;
for (int index = 0; index < 256; index++) {
target[index] = scalar.getDouble(originX + (index & 15), originZ + (index >> 4));
}
}
@Override
public void fillChunkRaw(int worldX, int worldZ, Object[] target) {
double[] values = new double[256];
fillChunkDoubles(worldX, worldZ, values);
for (int index = 0; index < 256; index++) {
target[index] = values[index];
}
}
private Object publishedPlane(int chunkX, int chunkZ) {
ChunkPlanes planes = cache.planesIfPresent(chunkX, chunkZ);
return planes == null ? null : planes.published(plane);
}
}
@@ -0,0 +1,101 @@
package art.arcane.iris.generation.stream;
import art.arcane.iris.generation.bake.BakePlane;
import art.arcane.iris.generation.bake.ChunkBakeCache;
import art.arcane.iris.generation.bake.ChunkPlanes;
import art.arcane.volmlib.util.function.NoiseProvider;
import art.arcane.volmlib.util.stream.BasicStream;
import java.util.Objects;
import java.util.function.BooleanSupplier;
public final class BakedHeightStream extends BasicStream<Double> implements ChunkFillableStream2D, ChunkFillableDoubleStream2D {
private final ChunkBakeCache cache;
private final BooleanSupplier mainThread;
private final NoiseProvider nonblocking;
private final NoiseProvider resolved;
public BakedHeightStream(ChunkBakeCache cache, BooleanSupplier mainThread, NoiseProvider nonblocking, NoiseProvider resolved) {
super();
this.cache = Objects.requireNonNull(cache, "Bake cache");
this.mainThread = Objects.requireNonNull(mainThread, "Main thread check");
this.nonblocking = Objects.requireNonNull(nonblocking, "Nonblocking height");
this.resolved = Objects.requireNonNull(resolved, "Resolved height");
}
@Override
public Double get(double x, double z) {
return getDouble(x, z);
}
@Override
public double getDouble(double x, double z) {
if (mainThread.getAsBoolean()) {
return nonblocking.noise(x, z);
}
int blockX = (int) x;
int blockZ = (int) z;
Object values = cache.read(BakePlane.HEIGHT, blockX >> 4, blockZ >> 4);
return values == null ? resolved.noise(blockX, blockZ) : ((double[]) values)[((blockZ & 15) << 4) | (blockX & 15)];
}
@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 double toDouble(Double t) {
return t;
}
@Override
public Double fromDouble(double d) {
return d;
}
@Override
public void fillChunkDoubles(int worldX, int worldZ, double[] target) {
if (target == null || target.length != 256) {
throw new IllegalArgumentException("Expected a 16x16 target array.");
}
boolean main = mainThread.getAsBoolean();
if (!main && (worldX & 15) == 0 && (worldZ & 15) == 0) {
Object values = ChunkBakeCache.inSparseScope() ? published(worldX >> 4, worldZ >> 4)
: cache.bake(BakePlane.HEIGHT, worldX >> 4, worldZ >> 4);
if (values != null) {
System.arraycopy(values, 0, target, 0, 256);
return;
}
}
for (int index = 0; index < 256; index++) {
int x = worldX + (index & 15);
int z = worldZ + (index >> 4);
target[index] = main ? nonblocking.noise(x, z) : column(x, z);
}
}
@Override
public void fillChunkRaw(int worldX, int worldZ, Object[] target) {
double[] values = new double[256];
fillChunkDoubles(worldX, worldZ, values);
for (int index = 0; index < 256; index++) {
target[index] = values[index];
}
}
private double column(int x, int z) {
Object values = published(x >> 4, z >> 4);
return values == null ? resolved.noise(x, z) : ((double[]) values)[((z & 15) << 4) | (x & 15)];
}
private Object published(int chunkX, int chunkZ) {
ChunkPlanes planes = cache.planesIfPresent(chunkX, chunkZ);
return planes == null ? null : planes.published(BakePlane.HEIGHT);
}
}
@@ -0,0 +1,89 @@
package art.arcane.iris.generation.stream;
import art.arcane.iris.generation.bake.BakePlane;
import art.arcane.iris.generation.bake.ChunkBakeCache;
import art.arcane.iris.generation.bake.ChunkPlanes;
import art.arcane.volmlib.util.stream.BasicStream;
import art.arcane.volmlib.util.stream.ProceduralStream;
import java.util.Objects;
public final class BakedStream<T> extends BasicStream<T> implements ChunkFillableStream2D {
private final ChunkBakeCache cache;
private final BakePlane plane;
private final ProceduralStream<T> scalar;
public BakedStream(ChunkBakeCache cache, BakePlane plane, ProceduralStream<T> scalar) {
super();
this.cache = Objects.requireNonNull(cache, "Bake cache");
this.plane = Objects.requireNonNull(plane, "Bake plane");
this.scalar = Objects.requireNonNull(scalar, "Scalar source");
}
public BakePlane plane() {
return plane;
}
public ProceduralStream<T> scalar() {
return scalar;
}
@Override
@SuppressWarnings("unchecked")
public T get(double x, double z) {
int blockX = (int) x;
int blockZ = (int) z;
Object values = cache.read(plane, blockX >> 4, blockZ >> 4);
return values == null ? scalar.get(blockX, blockZ) : (T) ((Object[]) values)[((blockZ & 15) << 4) | (blockX & 15)];
}
@SuppressWarnings("unchecked")
public T sparse(int blockX, int blockZ) {
Object published = publishedPlane(blockX >> 4, blockZ >> 4);
if (published != null) {
return (T) ((Object[]) published)[((blockZ & 15) << 4) | (blockX & 15)];
}
ChunkBakeCache.enterSparse();
try {
return scalar.get(blockX, blockZ);
} finally {
ChunkBakeCache.exitSparse();
}
}
@Override
public T get(double x, double y, double z) {
return scalar.get(x, y, z);
}
@Override
public double toDouble(T t) {
return scalar.toDouble(t);
}
@Override
public T fromDouble(double d) {
return scalar.fromDouble(d);
}
@Override
public void fillChunkRaw(int worldX, int worldZ, Object[] target) {
int chunkX = worldX >> 4;
int chunkZ = worldZ >> 4;
Object values = cache.bakingAllowed() ? cache.bake(plane, chunkX, chunkZ) : publishedPlane(chunkX, chunkZ);
if (values != null) {
System.arraycopy(values, 0, target, 0, 256);
return;
}
int originX = chunkX << 4;
int originZ = chunkZ << 4;
for (int index = 0; index < 256; index++) {
target[index] = scalar.get(originX + (index & 15), originZ + (index >> 4));
}
}
private Object publishedPlane(int chunkX, int chunkZ) {
ChunkPlanes planes = cache.planesIfPresent(chunkX, chunkZ);
return planes == null ? null : planes.published(plane);
}
}
@@ -0,0 +1,140 @@
package art.arcane.iris.generation.noise;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.util.SplittableRandom;
import static org.junit.Assert.assertEquals;
public class GeneratorProgramParityTest {
private static final NoiseStyle[] STYLES = {
NoiseStyle.SIMPLEX, NoiseStyle.PERLIN, NoiseStyle.IRIS, NoiseStyle.IRIS_HALF, NoiseStyle.FRACTAL_SMOKE,
NoiseStyle.FRACTAL_WATER, NoiseStyle.CLOVER, NoiseStyle.CELLULAR, NoiseStyle.NOWHERE,
NoiseStyle.CELLULAR_IRIS_DOUBLE, NoiseStyle.SIMPLEX_VASCULAR, NoiseStyle.FLAT, NoiseStyle.STATIC,
NoiseStyle.GLOB_IRIS_THICK, NoiseStyle.FRACTAL_BILLOW_PERLIN, NoiseStyle.PERLIN_IRIS
};
@Test
public void programHeightsMatchScalarHeightsBitForBit() {
SplittableRandom random = new SplittableRandom(0x5EED1E55L);
TerrainBatchScratch scratch = new TerrainBatchScratch();
for (int generatorIndex = 0; generatorIndex < 160; generatorIndex++) {
long shapeSeed = random.nextLong();
IrisGenerator scalar = generator(new SplittableRandom(shapeSeed));
IrisGenerator batch = generator(new SplittableRandom(shapeSeed));
long superSeed = random.nextLong();
GeneratorProgram program = batch.compileProgram(superSeed);
int count = 1 + random.nextInt(600);
double[] xs = new double[count];
double[] zs = new double[count];
double[] out = new double[count];
fillCoordinates(random, xs, zs, count);
program.heights(scratch, xs, zs, count, out);
for (int index = 0; index < count; index++) {
assertEquals("generator " + generatorIndex + " point " + xs[index] + "," + zs[index],
Double.doubleToRawLongBits(scalar.getHeight(xs[index], zs[index], superSeed)),
Double.doubleToRawLongBits(out[index]));
}
}
}
@Test
public void chunkGridsMatchScalarHeightsBitForBit() {
SplittableRandom random = new SplittableRandom(0xC0FFEEL);
TerrainBatchScratch scratch = new TerrainBatchScratch();
double[] xs = new double[256];
double[] zs = new double[256];
double[] out = new double[256];
for (int generatorIndex = 0; generatorIndex < 60; generatorIndex++) {
long shapeSeed = random.nextLong();
IrisGenerator scalar = generator(new SplittableRandom(shapeSeed));
IrisGenerator batch = generator(new SplittableRandom(shapeSeed));
long superSeed = random.nextLong();
GeneratorProgram program = batch.compileProgram(superSeed);
for (int chunk = 0; chunk < 4; chunk++) {
int originX = random.nextInt(-200_000, 200_000) << 4;
int originZ = random.nextInt(-200_000, 200_000) << 4;
for (int index = 0; index < 256; index++) {
xs[index] = originX + (index & 15);
zs[index] = originZ + (index >> 4);
}
program.heights(scratch, xs, zs, 256, out);
for (int index = 0; index < 256; index++) {
assertEquals(Double.doubleToRawLongBits(scalar.getHeight(xs[index], zs[index], superSeed)),
Double.doubleToRawLongBits(out[index]));
}
}
}
}
private static void fillCoordinates(SplittableRandom random, double[] xs, double[] zs, int count) {
for (int index = 0; index < count; index++) {
xs[index] = coordinate(random);
zs[index] = random.nextInt(5) == 0 ? xs[index] : coordinate(random);
}
}
private static double coordinate(SplittableRandom random) {
return switch (random.nextInt(6)) {
case 0 -> random.nextInt(-4096, 4096);
case 1 -> random.nextInt(-30_000_000, 30_000_000);
case 2 -> random.nextInt(-512, 512) + random.nextDouble();
case 3 -> random.nextInt(-64, 64) * 6;
case 4 -> random.nextDouble(-30_000_000D, 30_000_000D);
default -> random.nextInt(-16, 16) * 0.25D;
};
}
private static IrisGenerator generator(SplittableRandom random) {
int layers = 1 + random.nextInt(3);
KList<IrisNoiseGenerator> composite = new KList<>();
for (int layer = 0; layer < layers; layer++) {
composite.add(layer(random, 0));
}
double[] details = {0D, 0.25D, 0.5D, 1D};
IrisGenerator generator = new IrisGenerator().setComposite(composite)
.setSurfaceDetail(details[random.nextInt(details.length)])
.setMultiplicitive(random.nextInt(4) == 0)
.setSeed(random.nextLong(-5000L, 5000L))
.setZoom(0.2D + random.nextDouble() * 3D)
.setOffsetX(random.nextInt(-40, 40) * 0.25D)
.setOffsetZ(random.nextInt(-40, 40) * 0.25D)
.setOpacity(0.1D + random.nextDouble());
if (random.nextInt(3) == 0) {
generator.setCliffHeightMin(random.nextDouble() * 4D).setCliffHeightMax(4D + random.nextDouble() * 12D)
.setCliffHeightGenerator(layer(random, 2));
}
if (random.nextInt(5) == 0) {
generator.setCellFractureHeight(random.nextDouble()).setCellFractureZoom(0.5D + random.nextDouble());
}
return generator;
}
private static IrisNoiseGenerator layer(SplittableRandom random, int depth) {
IrisNoiseGenerator layer = new IrisNoiseGenerator()
.setStyle(STYLES[random.nextInt(STYLES.length)].style())
.setSeed(random.nextLong(-100_000L, 100_000L))
.setZoom(0.3D + random.nextDouble() * 4D)
.setOpacity(0.05D + random.nextDouble())
.setOffsetX(random.nextInt(-20, 20) * 0.5D)
.setOffsetZ(random.nextInt(-20, 20) * 0.5D)
.setOffsetY(random.nextInt(4) == 0 ? random.nextDouble() * 0.1D : 0D)
.setNegative(random.nextInt(4) == 0)
.setExponent(random.nextInt(4) == 0 ? 0.5D + random.nextDouble() : 1D)
.setParametric(random.nextInt(6) == 0)
.setBezier(random.nextInt(6) == 0)
.setSinCentered(random.nextInt(8) == 0)
.setOctaves(random.nextInt(5) == 0 ? 2 : 1)
.setEnabled(depth == 0 || random.nextInt(8) != 0);
if (depth < 4 && random.nextInt(3) != 0) {
int children = random.nextInt(4) == 0 ? 2 + random.nextInt(2) : 1;
KList<IrisNoiseGenerator> fracture = new KList<>();
for (int child = 0; child < children; child++) {
fracture.add(layer(random, depth + 1));
}
layer.setFracture(fracture);
}
return layer;
}
}
@@ -3,9 +3,6 @@ package art.arcane.iris.generation.noise;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
@@ -34,7 +31,7 @@ public class IrisGeneratorBatchTest {
assertTrue(batch.supportsBatch());
double[] sampled = new double[XS.length];
for (long seed : seeds) {
batch.getHeights(XS, ZS, seed, sampled, sampled.length);
batch.compileProgram(seed).heights(new TerrainBatchScratch(), XS, ZS, sampled.length, sampled);
for (int index = 0; index < sampled.length; index++) {
assertEquals(style + " detail=" + detail + " seed=" + seed + " index=" + index,
Double.doubleToRawLongBits(scalar.getHeight(XS[index], ZS[index], seed)),
@@ -54,7 +51,7 @@ public class IrisGeneratorBatchTest {
IrisGenerator batch = generator(style, detail, false).setCliffHeightMax(0D).setCellFractureHeight(0D);
double[] values = new double[XS.length];
for (long seed : new long[]{-1337L, Long.MAX_VALUE - 37L}) {
batch.getHeights(XS, ZS, seed, values, XS.length);
batch.compileProgram(seed).heights(new TerrainBatchScratch(), XS, ZS, XS.length, values);
for (int index = 0; index < XS.length; index++) {
assertEquals(Double.doubleToRawLongBits(scalar.getHeight(XS[index], ZS[index], seed)),
Double.doubleToRawLongBits(values[index]));
@@ -65,13 +62,15 @@ public class IrisGeneratorBatchTest {
}
@Test
public void resolvesLayerContextOncePerBatch() {
public void resolvesLayerContextOncePerCompile() {
IrisNoiseGenerator layer = spy(new IrisNoiseGenerator().setStyle(NoiseStyle.SIMPLEX.style()));
IrisGenerator generator = new IrisGenerator().setComposite(new KList<>(layer));
double[] sampled = new double[XS.length];
generator.getHeights(XS, ZS, 99L, sampled, XS.length);
GeneratorProgram program = generator.compileProgram(99L);
verify(layer, times(1)).getGenerator(anyLong(), isNull(), isNull());
clearInvocations(layer);
program.heights(new TerrainBatchScratch(), XS, ZS, XS.length, sampled);
verify(layer, times(0)).getGenerator(anyLong(), isNull(), isNull());
for (int index = 0; index < XS.length; index++) {
generator.getHeight(XS[index], ZS[index], 99L);
}
@@ -79,24 +78,15 @@ public class IrisGeneratorBatchTest {
}
@Test
public void customGeneratorFallbackKeepsCoordinateOrder() {
List<Double> sampled = new ArrayList<>();
public void customGeneratorsCannotCompile() {
IrisGenerator generator = new IrisGenerator() {
@Override
public double getHeight(double x, double z, long seed) {
sampled.add(x);
return sampled.size();
return x;
}
};
assertFalse(generator.supportsBatch());
double[] values = new double[XS.length];
generator.getHeights(XS, ZS, 0L, values, values.length);
for (int index = 0; index < values.length; index++) {
assertEquals(Double.valueOf(XS[index]), sampled.get(index));
assertEquals(index + 1D, values[index], 0D);
}
assertThrows(IllegalArgumentException.class,
() -> generator.getHeights(new double[4097], new double[4097], 0L, new double[4097], 4097));
assertThrows(IllegalStateException.class, () -> generator.compileProgram(0L));
}
@Test
@@ -44,6 +44,7 @@ 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.atMost;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -89,16 +90,10 @@ public class IrisComplexHeightStreamTest {
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);
@@ -116,7 +111,7 @@ public class IrisComplexHeightStreamTest {
int[] rounded = new int[256];
clearInvocations(fixture.hooks);
cache.fillRounded(rounded);
verify(fixture.hooks, times(1)).isMainThread();
verify(fixture.hooks, atMost(2)).isMainThread();
assertEquals(256, samples.get());
for (int index = 0; index < 256; index++) {
int x = index & 15;
@@ -127,7 +122,7 @@ public class IrisComplexHeightStreamTest {
}
clearInvocations(fixture.hooks);
cache.fill();
verify(fixture.hooks, times(1)).isMainThread();
verify(fixture.hooks, atMost(2)).isMainThread();
assertEquals(256, samples.get());
}
}
@@ -2,6 +2,9 @@ package art.arcane.iris.generation.runtime;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.world.pregen.PregenPerformanceProfile;
import art.arcane.iris.generation.bake.ChunkBakeCache;
import art.arcane.iris.generation.bake.PlaneSource;
import art.arcane.iris.generation.bake.SparseHeightMemo;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.hydrology.HydrologyPlanner;
import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings;
@@ -48,17 +51,14 @@ public class IrisComplexHydrologyNoiseCacheTest {
try (Fixture fixture = new Fixture(1_024)) {
ProceduralStream<Double> wrapper = fixture.complex.getRawHeightStream();
assertEquals(13D, fixture.height.getDouble(18, 5), 0D);
assertEquals(13D, fixture.rawHeight.getDouble(18, 5), 0D);
PregenPerformanceProfile.apply(fixture.engine);
assertEquals(4_096L * 256L, fixture.height.getMaxSize());
assertEquals(4_096L * 256L, fixture.rawHeight.getMaxSize());
assertTrue(fixture.bakes.capacity() >= 4_096);
assertEquals(1_024L * 256L, fixture.biomes.getMaxSize());
assertSame(wrapper, fixture.complex.getRawHeightStream());
assertEquals(47D, wrapper.getDouble(18, 5), 0D);
assertEquals(13D, fixture.height.getDouble(18, 5), 0D);
assertEquals(13D, fixture.rawHeight.getDouble(18, 5), 0D);
} finally {
PregenPerformanceProfile.restore();
IrisSettings.settings = previousSettings;
@@ -75,7 +75,7 @@ public class IrisComplexHydrologyNoiseCacheTest {
fixture.complex.ensureTerrainNoiseCacheSize(4_096);
fixture.assertCapacities(32_768);
assertEquals(4_096L * 256L, fixture.rawHeight.getMaxSize());
assertTrue(fixture.bakes.capacity() >= 4_096);
assertEquals(reservedBytes, budget.reservedBytes());
fixture.complex.close();
assertEquals(0L, budget.reservedBytes());
@@ -88,7 +88,7 @@ public class IrisComplexHydrologyNoiseCacheTest {
fixture.complex.ensureTerrainNoiseCacheSize(4_096);
fixture.assertCapacities(65_536);
assertEquals(65_536L * 256L, fixture.rawHeight.getMaxSize());
assertTrue(fixture.bakes.capacity() >= 65_536);
}
}
@@ -297,7 +297,7 @@ public class IrisComplexHydrologyNoiseCacheTest {
private final CachedStream2D<IrisRegion> regions;
private final CachedStream2D<InferredType> bridge;
private final CachedDoubleStream2D height;
private final CachedDoubleStream2D rawHeight;
private final ChunkBakeCache bakes;
private Fixture(int capacity) throws Exception {
services.when(() -> IrisServices.get(PreservationRegistry.class)).thenReturn(mock(PreservationRegistry.class));
@@ -305,10 +305,11 @@ public class IrisComplexHydrologyNoiseCacheTest {
regions = objectCache(capacity);
bridge = objectCache(capacity);
height = heightCache(capacity);
rawHeight = heightCache(capacity);
bakes = new ChunkBakeCache(mock(PlaneSource.class), capacity);
complex.setNaturalHeightStream(height);
complex.setHeightStream(ProceduralStream.ofDouble((x, z) -> 47D));
setField(complex, "cachedHeightStream", rawHeight);
setField(complex, "bakeCache", bakes);
setField(complex, "sparseBaseHeights", new SparseHeightMemo(16));
when(engine.getComplex()).thenReturn(complex);
complex.setBaseBiomeStream(biomes);
complex.setRegionStream(regions);