Memoized bake planes

- Planes fill lazily per column with ready bits and batch only missing columns on full-chunk requests
- Natural fills batch only the base heights Terrain3D still needs and reuse the sparse memo
- Bounds lattice and hydrology planner reads stay out of the bake
- Bake capacity is eight times the noise cache, covering the measured working set
- Generator evaluation counters and a bake census print from the probe
- Parity test compares batched chunk base heights and evaluation counts with the scalar path
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-21 23:22:16 -04:00
1 parent 037156ab7d
commit 1acda886b3
12 files changed
+666 -291

No files matched your search

@@ -1,16 +1,26 @@
package art.arcane.iris.generation.bake; package art.arcane.iris.generation.bake;
public enum BakePlane { public enum BakePlane {
REGION, REGION(false),
BASE_BIOME, BASE_BIOME(false),
BASE_HEIGHT, BASE_HEIGHT(true),
NATURAL_HEIGHT, NATURAL_HEIGHT(true),
NATURAL_TRUE_BIOME, NATURAL_TRUE_BIOME(false),
HEIGHT, HEIGHT(true),
TRUE_BIOME, TRUE_BIOME(false),
ROCK, ROCK(false),
FLUID, FLUID(false),
CAVE_BIOME; CAVE_BIOME(false);
static final BakePlane[] PLANES = values(); static final BakePlane[] PLANES = values();
private final boolean doubles;
BakePlane(boolean doubles) {
this.doubles = doubles;
}
public boolean doubles() {
return doubles;
}
} }
@@ -1,29 +1,25 @@
package art.arcane.iris.generation.bake; package art.arcane.iris.generation.bake;
import art.arcane.iris.generation.cache.ConcurrentClockCache; import art.arcane.iris.generation.cache.ConcurrentClockCache;
import art.arcane.iris.generation.stream.ProvisionalSampling;
import java.util.Objects; import java.util.Objects;
import java.util.function.DoubleSupplier;
import java.util.function.Supplier; import java.util.function.Supplier;
public final class ChunkBakeCache implements BakeCache { public final class ChunkBakeCache implements BakeCache {
private static final ThreadLocal<int[]> DEPTH = ThreadLocal.withInitial(() -> new int[2]); private static final ThreadLocal<int[]> SPARSE = ThreadLocal.withInitial(() -> new int[1]);
private static final int BAKING = 0;
private static final int SPARSE = 1;
private final PlaneSource source; private final PlaneSource source;
private final PlaneSource tracked;
private volatile ConcurrentClockCache<ChunkPlanes> entries; private volatile ConcurrentClockCache<ChunkPlanes> entries;
public ChunkBakeCache(PlaneSource source, int capacityChunks) { public ChunkBakeCache(PlaneSource source, int capacityChunks) {
this.source = Objects.requireNonNull(source, "Bake plane source"); this.source = Objects.requireNonNull(source, "Bake plane source");
this.tracked = new TrackedSource(source);
this.entries = new ConcurrentClockCache<>(Math.max(16, capacityChunks)); this.entries = new ConcurrentClockCache<>(Math.max(16, capacityChunks));
} }
@Override @Override
public ColumnBake get(int chunkX, int chunkZ) { public ColumnBake get(int chunkX, int chunkZ) {
return planes(chunkX, chunkZ).columnBake(tracked); return planes(chunkX, chunkZ).columnBake(source);
} }
@Override @Override
@@ -50,6 +46,19 @@ public final class ChunkBakeCache implements BakeCache {
entries.clear(); entries.clear();
} }
public long[] census() {
long[] counts = new long[BakePlane.PLANES.length + 1];
entries.forEach(planes -> {
counts[BakePlane.PLANES.length]++;
for (BakePlane plane : BakePlane.PLANES) {
if (planes.planeIfPresent(plane) != null) {
counts[plane.ordinal()]++;
}
}
});
return counts;
}
public ChunkPlanes planes(int chunkX, int chunkZ) { public ChunkPlanes planes(int chunkX, int chunkZ) {
ConcurrentClockCache<ChunkPlanes> current = entries; ConcurrentClockCache<ChunkPlanes> current = entries;
long key = key(chunkX, chunkZ); long key = key(chunkX, chunkZ);
@@ -61,38 +70,35 @@ public final class ChunkBakeCache implements BakeCache {
return entries.get(key(chunkX, chunkZ)); return entries.get(key(chunkX, chunkZ));
} }
public Object bake(BakePlane plane, int chunkX, int chunkZ) { public ChunkPlanes.Plane planeIfPresent(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)); ChunkPlanes planes = entries.get(key(chunkX, chunkZ));
if (planes != null) { return planes == null ? null : planes.planeIfPresent(plane);
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() { public ChunkPlanes.Plane plane(BakePlane plane, int chunkX, int chunkZ) {
int[] depth = DEPTH.get(); return planes(chunkX, chunkZ).plane(plane);
if (depth[SPARSE] > 0 || source.mainThread()) { }
return false;
public Object complete(BakePlane plane, int chunkX, int chunkZ) {
return planes(chunkX, chunkZ).complete(plane, source);
}
public boolean memoAllowed() {
return SPARSE.get()[0] == 0 && !source.mainThread();
}
public void storeDouble(BakePlane plane, int blockX, int blockZ, double value, long revision) {
if (revision != ProvisionalSampling.revision()) {
return;
} }
return depth[BAKING] > 0 || source.generationContext(); planes(blockX >> 4, blockZ >> 4).plane(plane).storeDouble(((blockZ & 15) << 4) | (blockX & 15), value);
} }
public boolean bakingAllowed() { public void storeObject(BakePlane plane, int blockX, int blockZ, Object value, long revision) {
return !inSparseScope() && !source.mainThread(); if (revision != ProvisionalSampling.revision()) {
} return;
}
public static boolean inSparseScope() { planes(blockX >> 4, blockZ >> 4).plane(plane).storeObject(((blockZ & 15) << 4) | (blockX & 15), value);
return DEPTH.get()[SPARSE] > 0;
} }
public static <T> T sparse(Supplier<T> action) { public static <T> T sparse(Supplier<T> action) {
@@ -104,73 +110,19 @@ public final class ChunkBakeCache implements BakeCache {
} }
} }
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() { public static void enterSparse() {
DEPTH.get()[SPARSE]++; SPARSE.get()[0]++;
} }
public static void exitSparse() { public static void exitSparse() {
DEPTH.get()[SPARSE]--; SPARSE.get()[0]--;
}
public static boolean inSparseScope() {
return SPARSE.get()[0] > 0;
} }
public static long key(int chunkX, int chunkZ) { public static long key(int chunkX, int chunkZ) {
return ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL); 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();
}
}
} }
@@ -2,12 +2,17 @@ package art.arcane.iris.generation.bake;
import art.arcane.iris.generation.stream.ProvisionalSampling; import art.arcane.iris.generation.stream.ProvisionalSampling;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicReferenceArray; import java.util.concurrent.atomic.AtomicReferenceArray;
public final class ChunkPlanes { public final class ChunkPlanes {
private static final VarHandle READY = MethodHandles.arrayElementVarHandle(long[].class);
private final int chunkX; private final int chunkX;
private final int chunkZ; private final int chunkZ;
private final AtomicReferenceArray<Object> planes = new AtomicReferenceArray<>(BakePlane.PLANES.length); private final AtomicReferenceArray<Plane> planes = new AtomicReferenceArray<>(BakePlane.PLANES.length);
private volatile ColumnBake columnBake; private volatile ColumnBake columnBake;
ChunkPlanes(int chunkX, int chunkZ) { ChunkPlanes(int chunkX, int chunkZ) {
@@ -23,40 +28,50 @@ public final class ChunkPlanes {
return chunkZ; return chunkZ;
} }
public Object published(BakePlane plane) { public Plane planeIfPresent(BakePlane kind) {
Object state = planes.getAcquire(plane.ordinal()); return planes.getAcquire(kind.ordinal());
return state instanceof Pending ? null : state;
} }
Object plane(BakePlane plane, PlaneSource source) { public Object published(BakePlane kind) {
int slot = plane.ordinal(); Plane plane = planes.getAcquire(kind.ordinal());
return plane != null && plane.complete ? plane.values : null;
}
public Plane plane(BakePlane kind) {
int slot = kind.ordinal();
Plane plane = planes.getAcquire(slot);
if (plane != null) {
return plane;
}
Plane created = new Plane(kind);
Plane witness = planes.compareAndExchangeRelease(slot, null, created);
return witness == null ? created : witness;
}
Object complete(BakePlane kind, PlaneSource source) {
Plane plane = plane(kind);
while (true) { while (true) {
Object state = planes.getAcquire(slot); if (plane.complete) {
if (state != null && !(state instanceof Pending)) { return plane.values;
return state;
} }
if (state instanceof Pending pending) { Pending running = plane.filling.get();
if (pending.owner == Thread.currentThread()) { if (running != null) {
return source.compute(plane, chunkX, chunkZ); if (running.owner == Thread.currentThread()) {
return plane.unpublishedFill(source, chunkX, chunkZ);
} }
pending.await(); running.await();
continue; continue;
} }
Pending pending = new Pending(Thread.currentThread()); Pending pending = new Pending(Thread.currentThread());
if (!planes.compareAndSet(slot, null, pending)) { if (!plane.filling.compareAndSet(null, pending)) {
continue; continue;
} }
Object computed = null;
boolean publishable = false;
try { try {
long revision = ProvisionalSampling.revision(); return plane.fill(source, chunkX, chunkZ);
computed = source.compute(plane, chunkX, chunkZ);
publishable = revision == ProvisionalSampling.revision();
} finally { } finally {
planes.setRelease(slot, publishable ? computed : null); plane.filling.set(null);
pending.complete(); pending.complete();
} }
return computed;
} }
} }
@@ -77,6 +92,97 @@ public final class ChunkPlanes {
return bake; return bake;
} }
public static final class Plane {
private final BakePlane kind;
private final Object values;
private final long[] ready = new long[4];
private final AtomicReference<Pending> filling = new AtomicReference<>();
private volatile boolean complete;
private Plane(BakePlane kind) {
this.kind = kind;
this.values = kind.doubles() ? new double[256] : new Object[256];
}
public boolean ready(int index) {
return complete || ((long) READY.getAcquire(ready, index >>> 6) & (1L << (index & 63))) != 0L;
}
public double doubleAt(int index) {
return ((double[]) values)[index];
}
public Object objectAt(int index) {
return ((Object[]) values)[index];
}
public void storeDouble(int index, double value) {
((double[]) values)[index] = value;
markReady(index);
}
public void storeObject(int index, Object value) {
((Object[]) values)[index] = value;
markReady(index);
}
private void markReady(int index) {
READY.getAndBitwiseOrRelease(ready, index >>> 6, 1L << (index & 63));
}
private long[] missing() {
long[] missing = new long[4];
for (int word = 0; word < 4; word++) {
missing[word] = ~(long) READY.getAcquire(ready, word);
}
return missing;
}
private Object fill(PlaneSource source, int chunkX, int chunkZ) {
long[] missing = missing();
Object computed = kind.doubles() ? new double[256] : new Object[256];
long revision = ProvisionalSampling.revision();
source.fill(kind, chunkX, chunkZ, missing, computed);
if (revision != ProvisionalSampling.revision()) {
return merge(missing, computed);
}
for (int index = 0; index < 256; index++) {
if ((missing[index >>> 6] & (1L << (index & 63))) != 0L) {
if (kind.doubles()) {
((double[]) values)[index] = ((double[]) computed)[index];
} else {
((Object[]) values)[index] = ((Object[]) computed)[index];
}
}
}
for (int word = 0; word < 4; word++) {
READY.setRelease(ready, word, -1L);
}
complete = true;
return values;
}
private Object unpublishedFill(PlaneSource source, int chunkX, int chunkZ) {
long[] missing = missing();
Object computed = kind.doubles() ? new double[256] : new Object[256];
source.fill(kind, chunkX, chunkZ, missing, computed);
return merge(missing, computed);
}
private Object merge(long[] missing, Object computed) {
for (int index = 0; index < 256; index++) {
if ((missing[index >>> 6] & (1L << (index & 63))) == 0L) {
if (kind.doubles()) {
((double[]) computed)[index] = ((double[]) values)[index];
} else {
((Object[]) computed)[index] = ((Object[]) values)[index];
}
}
}
return computed;
}
}
private static final class Pending { private static final class Pending {
private final Thread owner; private final Thread owner;
private boolean done; private boolean done;
@@ -0,0 +1,48 @@
package art.arcane.iris.generation.bake;
import java.util.concurrent.atomic.LongAdder;
public final class GeneratorEvaluations {
private static final LongAdder BAKED_CHUNKS = new LongAdder();
private static final LongAdder BAKED = new LongAdder();
private static final LongAdder BATCHED = new LongAdder();
private static final LongAdder SCALAR = new LongAdder();
private GeneratorEvaluations() {
}
public static void bakedChunk(long evaluations) {
BAKED_CHUNKS.increment();
BAKED.add(evaluations);
}
public static void batched(long evaluations) {
BATCHED.add(evaluations);
}
public static void scalar(int evaluations) {
SCALAR.add(evaluations);
}
public static Snapshot snapshot() {
return new Snapshot(BAKED_CHUNKS.sum(), BAKED.sum(), BATCHED.sum(), SCALAR.sum());
}
public static void reset() {
BAKED_CHUNKS.reset();
BAKED.reset();
BATCHED.reset();
SCALAR.reset();
}
public record Snapshot(long bakedChunks, long bakedEvaluations, long batchedEvaluations, long scalarEvaluations) {
public double bakedEvaluationsPerChunk() {
return bakedChunks == 0L ? 0D : (double) bakedEvaluations / bakedChunks;
}
public Snapshot minus(Snapshot earlier) {
return new Snapshot(bakedChunks - earlier.bakedChunks, bakedEvaluations - earlier.bakedEvaluations,
batchedEvaluations - earlier.batchedEvaluations, scalarEvaluations - earlier.scalarEvaluations);
}
}
}
@@ -1,11 +1,9 @@
package art.arcane.iris.generation.bake; package art.arcane.iris.generation.bake;
public interface PlaneSource { public interface PlaneSource {
Object compute(BakePlane plane, int chunkX, int chunkZ); void fill(BakePlane plane, int chunkX, int chunkZ, long[] missing, Object values);
ColumnBake assemble(ChunkPlanes planes); ColumnBake assemble(ChunkPlanes planes);
boolean mainThread(); boolean mainThread();
boolean generationContext();
} }
@@ -6,7 +6,6 @@ import art.arcane.iris.generation.noise.TerrainBatchScratch;
import art.arcane.volmlib.util.math.M; import art.arcane.volmlib.util.math.M;
import java.util.Arrays; import java.util.Arrays;
import java.util.concurrent.atomic.LongAdder;
public final class TerrainProgram { public final class TerrainProgram {
private static final int SKIPPED = 0; private static final int SKIPPED = 0;
@@ -16,7 +15,6 @@ public final class TerrainProgram {
private final IrisGenerator[][] generators; private final IrisGenerator[][] generators;
private final GeneratorProgram[][] programs; private final GeneratorProgram[][] programs;
private final long heightSeed; private final long heightSeed;
private final LongAdder evaluations = new LongAdder();
private TerrainProgram(IrisGenerator[][] generators, GeneratorProgram[][] programs, long heightSeed) { private TerrainProgram(IrisGenerator[][] generators, GeneratorProgram[][] programs, long heightSeed) {
this.generators = generators; this.generators = generators;
@@ -44,19 +42,11 @@ public final class TerrainProgram {
return generators[group].length; return generators[group].length;
} }
public long evaluations() { public long accumulate(int group, TerrainBatchScratch scratch, double[] xs, double[] zs, int count,
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) { double[] lows, double[] highs, double[] heights) {
IrisGenerator[] members = generators[group]; IrisGenerator[] members = generators[group];
if (members.length == 0 || count == 0) { if (members.length == 0 || count == 0) {
return; return 0L;
} }
int mark = scratch.mark(); int mark = scratch.mark();
int indexMark = scratch.indexMark(); int indexMark = scratch.indexMark();
@@ -87,7 +77,7 @@ public final class TerrainProgram {
} }
if (activeCount == 0) { if (activeCount == 0) {
scratch.resetIndices(indexMark); scratch.resetIndices(indexMark);
return; return 0L;
} }
double[] activeX = scratch.buffer(activeCount); double[] activeX = scratch.buffer(activeCount);
double[] activeZ = scratch.buffer(activeCount); double[] activeZ = scratch.buffer(activeCount);
@@ -129,8 +119,8 @@ public final class TerrainProgram {
for (int index = 0; index < activeCount; index++) { for (int index = 0; index < activeCount; index++) {
heights[activeIndex[index]] += sums[index] / members.length; heights[activeIndex[index]] += sums[index] / members.length;
} }
evaluations.add((long) activeCount * members.length);
scratch.reset(mark); scratch.reset(mark);
scratch.resetIndices(indexMark); scratch.resetIndices(indexMark);
return (long) activeCount * members.length;
} }
} }
@@ -23,6 +23,7 @@ import art.arcane.iris.generation.bake.BakeRegistry;
import art.arcane.iris.generation.bake.ChunkBakeCache; import art.arcane.iris.generation.bake.ChunkBakeCache;
import art.arcane.iris.generation.bake.ChunkPlanes; import art.arcane.iris.generation.bake.ChunkPlanes;
import art.arcane.iris.generation.bake.ColumnBake; import art.arcane.iris.generation.bake.ColumnBake;
import art.arcane.iris.generation.bake.GeneratorEvaluations;
import art.arcane.iris.generation.bake.PlaneSource; import art.arcane.iris.generation.bake.PlaneSource;
import art.arcane.iris.generation.bake.SparseHeightMemo; import art.arcane.iris.generation.bake.SparseHeightMemo;
import art.arcane.iris.generation.bake.TerrainProgram; import art.arcane.iris.generation.bake.TerrainProgram;
@@ -406,7 +407,7 @@ public class IrisComplex implements DataProvider {
return value; return value;
}); });
bakeCache = new ChunkBakeCache(new BakeSource(), cacheSize); bakeCache = new ChunkBakeCache(new BakeSource(), cacheSize * 8);
rockSource = engine.getDimension().getRockPalette().getLayerGenerator(rng.nextParallelRNG(45), data).stream() rockSource = engine.getDimension().getRockPalette().getLayerGenerator(rng.nextParallelRNG(45), data).stream()
.select(engine.getDimension().getRockPalette().getBlockData(data)); .select(engine.getDimension().getRockPalette().getBlockData(data));
rockStream = new BakedStream<>(bakeCache, BakePlane.ROCK, rockSource); rockStream = new BakedStream<>(bakeCache, BakePlane.ROCK, rockSource);
@@ -844,6 +845,7 @@ public class IrisComplex implements DataProvider {
zs[index] = RiverFootprint.unpackZ(missing[index]); zs[index] = RiverFootprint.unpackZ(missing[index]);
} }
TerrainProgram program = terrainProgram(); TerrainProgram program = terrainProgram();
long evaluations = 0L;
for (int group = 0; group < frozenInterpolators.length; group++) { for (int group = 0; group < frozenInterpolators.length; group++) {
if (frozenGenerators[group].length == 0) { if (frozenGenerators[group].length == 0) {
continue; continue;
@@ -854,8 +856,9 @@ public class IrisComplex implements DataProvider {
lows[index] = bounds.min(); lows[index] = bounds.min();
highs[index] = bounds.max(); highs[index] = bounds.max();
} }
program.accumulate(group, scratch, xs, zs, size, lows, highs, values); evaluations += program.accumulate(group, scratch, xs, zs, size, lows, highs, values);
} }
GeneratorEvaluations.batched(evaluations);
for (int index = 0; index < size; index++) { for (int index = 0; index < size; index++) {
values[index] = finishBaseHeight(xs[index], zs[index], values[index]); values[index] = finishBaseHeight(xs[index], zs[index], values[index]);
} }
@@ -865,15 +868,16 @@ public class IrisComplex implements DataProvider {
} }
} }
private double[] bakeBaseHeights(int chunkX, int chunkZ) { private void bakeBaseHeights(int chunkX, int chunkZ, long[] missing, double[] values) {
double[] values = new double[256];
int originX = chunkX << 4; int originX = chunkX << 4;
int originZ = chunkZ << 4; int originZ = chunkZ << 4;
if (!supportsChunkHeightBatch()) { if (!supportsChunkHeightBatch()) {
for (int index = 0; index < 256; index++) { for (int index = 0; index < 256; index++) {
values[index] = sampleUnblendedNaturalTerrainHeight(originX + (index & 15), originZ + (index >> 4)); if ((missing[index >>> 6] & (1L << (index & 63))) != 0L) {
values[index] = sampleUnblendedNaturalTerrainHeight(originX + (index & 15), originZ + (index >> 4));
}
} }
return values; return;
} }
TerrainBatchScratch scratch = BATCH_SCRATCH.get(); TerrainBatchScratch scratch = BATCH_SCRATCH.get();
int mark = scratch.mark(); int mark = scratch.mark();
@@ -883,44 +887,55 @@ public class IrisComplex implements DataProvider {
double[] zs = scratch.buffer(256); double[] zs = scratch.buffer(256);
double[] lows = scratch.buffer(256); double[] lows = scratch.buffer(256);
double[] highs = scratch.buffer(256); double[] highs = scratch.buffer(256);
double[] heights = scratch.buffer(256);
int[] columns = scratch.indices(256);
int count = 0;
for (int index = 0; index < 256; index++) { for (int index = 0; index < 256; index++) {
xs[index] = originX + (index & 15); if ((missing[index >>> 6] & (1L << (index & 63))) != 0L) {
zs[index] = originZ + (index >> 4); xs[count] = originX + (index & 15);
zs[count] = originZ + (index >> 4);
heights[count] = 0D;
columns[count++] = index;
}
}
if (count == 0) {
return;
} }
TerrainProgram program = terrainProgram(); TerrainProgram program = terrainProgram();
GridBoundsCache cache = gridBoundsCache.get(); GridBoundsCache cache = gridBoundsCache.get();
int grid = biomeBoundsSamplingStep; int grid = biomeBoundsSamplingStep;
boolean corners = ChunkCorners.supports(grid); boolean corners = ChunkCorners.supports(grid);
long evaluations = 0L;
for (int group = 0; group < frozenInterpolators.length; group++) { for (int group = 0; group < frozenInterpolators.length; group++) {
if (frozenGenerators[group].length == 0) { if (frozenGenerators[group].length == 0) {
continue; continue;
} }
if (corners) { if (corners) {
if (!chunkCornerBounds(cache, group, grid, originX, originZ, xs, zs, lows, highs)) { if (!chunkCornerBounds(cache, group, grid, originX, originZ, xs, zs, count, lows, highs)) {
continue; continue;
} }
} else { } else {
for (int index = 0; index < 256; index++) { for (int index = 0; index < count; index++) {
NoiseBounds bounds = gridSampleBounds(terrainEngine, frozenInterpolators[group], group, NoiseBounds bounds = gridSampleBounds(terrainEngine, frozenInterpolators[group], group,
frozenGenerators[group], xs[index], zs[index]); frozenGenerators[group], xs[index], zs[index]);
lows[index] = bounds.min(); lows[index] = bounds.min();
highs[index] = bounds.max(); highs[index] = bounds.max();
} }
} }
program.accumulate(group, scratch, xs, zs, 256, lows, highs, values); evaluations += program.accumulate(group, scratch, xs, zs, count, lows, highs, heights);
} }
for (int index = 0; index < 256; index++) { for (int index = 0; index < count; index++) {
values[index] = finishBaseHeight(xs[index], zs[index], values[index]); values[columns[index]] = finishBaseHeight(xs[index], zs[index], heights[index]);
} }
GeneratorEvaluations.bakedChunk(evaluations);
} finally { } finally {
scratch.reset(mark); scratch.reset(mark);
scratch.resetIndices(indexMark); scratch.resetIndices(indexMark);
} }
return values;
} }
private boolean chunkCornerBounds(GridBoundsCache cache, int group, int grid, int originX, int originZ, private boolean chunkCornerBounds(GridBoundsCache cache, int group, int grid, int originX, int originZ,
double[] xs, double[] zs, double[] lows, double[] highs) { double[] xs, double[] zs, int count, double[] lows, double[] highs) {
int cells = 16 / grid; int cells = 16 / grid;
int stride = cells + 1; int stride = cells + 1;
long[] packed = new long[stride * stride]; long[] packed = new long[stride * stride];
@@ -936,7 +951,7 @@ public class IrisComplex implements DataProvider {
if (silent) { if (silent) {
return false; return false;
} }
for (int index = 0; index < 256; index++) { for (int index = 0; index < count; index++) {
double x = xs[index]; double x = xs[index];
double z = zs[index]; double z = zs[index];
int gx = ((int) Math.floor(x)) & ~(grid - 1); int gx = ((int) Math.floor(x)) & ~(grid - 1);
@@ -967,6 +982,62 @@ public class IrisComplex implements DataProvider {
return true; return true;
} }
private void basePlane(int chunkX, int chunkZ, long[] missing, double[] values) {
int originX = chunkX << 4;
int originZ = chunkZ << 4;
long[] compute = missing.clone();
for (int index = 0; index < 256; index++) {
if ((compute[index >>> 6] & (1L << (index & 63))) == 0L) {
continue;
}
double memoized = sparseBaseHeights.get(originX + (index & 15), originZ + (index >> 4));
if (!Double.isNaN(memoized)) {
values[index] = memoized;
compute[index >>> 6] &= ~(1L << (index & 63));
}
}
bakeBaseHeights(chunkX, chunkZ, compute, values);
}
private void prefetchBaseHeights(int chunkX, int chunkZ, long[] naturalMissing) {
ChunkPlanes.Plane base = bakeCache.plane(BakePlane.BASE_HEIGHT, chunkX, chunkZ);
int originX = chunkX << 4;
int originZ = chunkZ << 4;
long[] compute = new long[4];
boolean any = false;
for (int index = 0; index < 256; index++) {
if ((naturalMissing[index >>> 6] & (1L << (index & 63))) == 0L || base.ready(index)) {
continue;
}
int x = originX + (index & 15);
int z = originZ + (index >> 4);
if (terrain3D != null && !terrain3D.requiresBaseHeight(x, z)) {
continue;
}
double memoized = sparseBaseHeights.get(x, z);
if (!Double.isNaN(memoized)) {
base.storeDouble(index, memoized);
continue;
}
compute[index >>> 6] |= 1L << (index & 63);
any = true;
}
if (!any) {
return;
}
double[] values = new double[256];
long revision = ProvisionalSampling.revision();
bakeBaseHeights(chunkX, chunkZ, compute, values);
if (revision != ProvisionalSampling.revision()) {
return;
}
for (int index = 0; index < 256; index++) {
if ((compute[index >>> 6] & (1L << (index & 63))) != 0L) {
base.storeDouble(index, values[index]);
}
}
}
private boolean supportsChunkHeightBatch() { private boolean supportsChunkHeightBatch() {
return terrainEngine.getDimension().getTerrainSamplingStep() == 1 && biomeBuffetGenerators.isEmpty() return terrainEngine.getDimension().getTerrainSamplingStep() == 1 && biomeBuffetGenerators.isEmpty()
&& (biomeBuffet == null || biomeBuffet.cells().isEmpty()); && (biomeBuffet == null || biomeBuffet.cells().isEmpty());
@@ -1012,9 +1083,9 @@ public class IrisComplex implements DataProvider {
for (int index = 0; index < count; index++) { for (int index = 0; index < count; index++) {
int x = RiverFootprint.unpackX(coordinates[index]); int x = RiverFootprint.unpackX(coordinates[index]);
int z = RiverFootprint.unpackZ(coordinates[index]); int z = RiverFootprint.unpackZ(coordinates[index]);
if (publishedPlane(BakePlane.NATURAL_HEIGHT, x, z) != null if (bakedColumn(BakePlane.NATURAL_HEIGHT, x, z)
|| terrain3D != null && !terrain3D.requiresBaseHeight(x, z) || terrain3D != null && !terrain3D.requiresBaseHeight(x, z)
|| publishedPlane(BakePlane.BASE_HEIGHT, x, z) != null || bakedColumn(BakePlane.BASE_HEIGHT, x, z)
|| !Double.isNaN(sparseBaseHeights.get(x, z))) { || !Double.isNaN(sparseBaseHeights.get(x, z))) {
continue; continue;
} }
@@ -1027,22 +1098,14 @@ public class IrisComplex implements DataProvider {
sparseBaseHeights.put(RiverFootprint.unpackX(required[index]), RiverFootprint.unpackZ(required[index]), baseHeights[index]); sparseBaseHeights.put(RiverFootprint.unpackX(required[index]), RiverFootprint.unpackZ(required[index]), baseHeights[index]);
} }
} }
BakedDoubleStream naturalHeights = (BakedDoubleStream) naturalHeightStream;
for (int index = 0; index < count; index++) { for (int index = 0; index < count; index++) {
heights[index] = naturalHeights.sparse(RiverFootprint.unpackX(coordinates[index]), RiverFootprint.unpackZ(coordinates[index])); heights[index] = naturalHeightStream.getDouble(RiverFootprint.unpackX(coordinates[index]), RiverFootprint.unpackZ(coordinates[index]));
} }
} }
@SuppressWarnings("unchecked") private boolean bakedColumn(BakePlane plane, int blockX, int blockZ) {
private IrisBiome sparseBaseBiome(double x, double z) { ChunkPlanes.Plane values = bakeCache.planeIfPresent(plane, blockX >> 4, blockZ >> 4);
return baseBiomeStream instanceof BakedStream<?> baked return values != null && values.ready(((blockZ & 15) << 4) | (blockX & 15));
? ((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 { private final class HydrologyHeightProvider implements IrisHydrologyNaturalHeightProvider {
@@ -1050,7 +1113,12 @@ public class IrisComplex implements DataProvider {
@Override @Override
public double sample(int blockX, int blockZ) { public double sample(int blockX, int blockZ) {
return ((BakedDoubleStream) naturalHeightStream).sparse(blockX, blockZ); ChunkBakeCache.enterSparse();
try {
return naturalHeightStream.getDouble(blockX, blockZ);
} finally {
ChunkBakeCache.exitSparse();
}
} }
@Override @Override
@@ -1075,19 +1143,19 @@ public class IrisComplex implements DataProvider {
private final class BakeSource implements PlaneSource { private final class BakeSource implements PlaneSource {
@Override @Override
public Object compute(BakePlane plane, int chunkX, int chunkZ) { public void fill(BakePlane plane, int chunkX, int chunkZ, long[] missing, Object values) {
return switch (plane) { switch (plane) {
case REGION -> objectPlane(regionSource, chunkX, chunkZ); case REGION -> objectPlane(regionSource, chunkX, chunkZ, missing, (Object[]) values);
case BASE_BIOME -> objectPlane(baseBiomeSource, chunkX, chunkZ); case BASE_BIOME -> objectPlane(baseBiomeSource, chunkX, chunkZ, missing, (Object[]) values);
case BASE_HEIGHT -> bakeBaseHeights(chunkX, chunkZ); case BASE_HEIGHT -> basePlane(chunkX, chunkZ, missing, (double[]) values);
case NATURAL_HEIGHT -> doublePlane(naturalHeightSource, chunkX, chunkZ); case NATURAL_HEIGHT -> naturalPlane(chunkX, chunkZ, missing, (double[]) values);
case NATURAL_TRUE_BIOME -> objectPlane(naturalTrueBiomeSource, chunkX, chunkZ); case NATURAL_TRUE_BIOME -> objectPlane(naturalTrueBiomeSource, chunkX, chunkZ, missing, (Object[]) values);
case HEIGHT -> heightPlane(chunkX, chunkZ); case HEIGHT -> heightPlane(chunkX, chunkZ, missing, (double[]) values);
case TRUE_BIOME -> objectPlane(trueBiomeSource, chunkX, chunkZ); case TRUE_BIOME -> objectPlane(trueBiomeSource, chunkX, chunkZ, missing, (Object[]) values);
case ROCK -> objectPlane(rockSource, chunkX, chunkZ); case ROCK -> objectPlane(rockSource, chunkX, chunkZ, missing, (Object[]) values);
case FLUID -> objectPlane(fluidSource, chunkX, chunkZ); case FLUID -> objectPlane(fluidSource, chunkX, chunkZ, missing, (Object[]) values);
case CAVE_BIOME -> objectPlane(caveSource, chunkX, chunkZ); case CAVE_BIOME -> objectPlane(caveSource, chunkX, chunkZ, missing, (Object[]) values);
}; }
} }
@Override @Override
@@ -1100,52 +1168,50 @@ public class IrisComplex implements DataProvider {
return terrainEngine.getPlatformHooks().isMainThread(); return terrainEngine.getPlatformHooks().isMainThread();
} }
@Override private void objectPlane(ProceduralStream<?> source, int chunkX, int chunkZ, long[] missing, Object[] values) {
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 originX = chunkX << 4;
int originZ = chunkZ << 4; int originZ = chunkZ << 4;
for (int index = 0; index < 256; index++) { for (int index = 0; index < 256; index++) {
values[index] = source.get(originX + (index & 15), originZ + (index >> 4)); if ((missing[index >>> 6] & (1L << (index & 63))) != 0L) {
values[index] = source.get(originX + (index & 15), originZ + (index >> 4));
}
} }
return values;
} }
private double[] heightPlane(int chunkX, int chunkZ) { private void naturalPlane(int chunkX, int chunkZ, long[] missing, double[] values) {
double[] values = new double[256]; prefetchBaseHeights(chunkX, chunkZ, missing);
int originX = chunkX << 4;
int originZ = chunkZ << 4;
for (int index = 0; index < 256; index++) {
if ((missing[index >>> 6] & (1L << (index & 63))) != 0L) {
values[index] = naturalHeightSource.getDouble(originX + (index & 15), originZ + (index >> 4));
}
}
}
private void heightPlane(int chunkX, int chunkZ, long[] missing, double[] values) {
int originX = chunkX << 4; int originX = chunkX << 4;
int originZ = chunkZ << 4; int originZ = chunkZ << 4;
boolean main = terrainEngine.getPlatformHooks().isMainThread(); boolean main = terrainEngine.getPlatformHooks().isMainThread();
for (int index = 0; index < 256; index++) { if (!main && !ChunkBakeCache.inSparseScope()) {
int x = originX + (index & 15); bakeCache.complete(BakePlane.NATURAL_HEIGHT, chunkX, chunkZ);
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++) { for (int index = 0; index < 256; index++) {
values[index] = source.getDouble(originX + (index & 15), originZ + (index >> 4)); if ((missing[index >>> 6] & (1L << (index & 63))) != 0L) {
int x = originX + (index & 15);
int z = originZ + (index >> 4);
values[index] = main ? nonblockingTerrainHeight(x, z) : resolveHydrologyTerrainHeight(x, z);
}
} }
return values;
} }
} }
public Object bakePlane(BakePlane plane, int chunkX, int chunkZ) { public Object bakePlane(BakePlane plane, int chunkX, int chunkZ) {
return bakeCache.bake(plane, chunkX, chunkZ); return bakeCache.complete(plane, chunkX, chunkZ);
}
public long[] bakeCensus() {
return bakeCache.census();
} }
public ColumnBake columnBake(int chunkX, int chunkZ) { public ColumnBake columnBake(int chunkX, int chunkZ) {
@@ -1177,7 +1243,7 @@ public class IrisComplex implements DataProvider {
} }
private ColumnBake assembleColumnBake(int chunkX, int chunkZ) { private ColumnBake assembleColumnBake(int chunkX, int chunkZ) {
double[] height = (double[]) bakeCache.bake(BakePlane.HEIGHT, chunkX, chunkZ); double[] height = (double[]) bakeCache.complete(BakePlane.HEIGHT, chunkX, chunkZ);
int[] rounded = new int[256]; int[] rounded = new int[256];
SimdSupport.kernels().roundToInt(height, rounded, 256); SimdSupport.kernels().roundToInt(height, rounded, 256);
int originX = chunkX << 4; int originX = chunkX << 4;
@@ -1197,17 +1263,17 @@ public class IrisComplex implements DataProvider {
} }
} }
return new ColumnBake(new ColumnBake.Planes(chunkX, chunkZ, return new ColumnBake(new ColumnBake.Planes(chunkX, chunkZ,
ids((Object[]) bakeCache.bake(BakePlane.REGION, chunkX, chunkZ), regionRegistry), ids((Object[]) bakeCache.complete(BakePlane.REGION, chunkX, chunkZ), regionRegistry),
ids((Object[]) bakeCache.bake(BakePlane.BASE_BIOME, chunkX, chunkZ), biomeRegistry), ids((Object[]) bakeCache.complete(BakePlane.BASE_BIOME, chunkX, chunkZ), biomeRegistry),
ids((Object[]) bakeCache.bake(BakePlane.TRUE_BIOME, chunkX, chunkZ), biomeRegistry), ids((Object[]) bakeCache.complete(BakePlane.TRUE_BIOME, chunkX, chunkZ), biomeRegistry),
ids((Object[]) bakeCache.bake(BakePlane.CAVE_BIOME, chunkX, chunkZ), biomeRegistry), ids((Object[]) bakeCache.complete(BakePlane.CAVE_BIOME, chunkX, chunkZ), biomeRegistry),
(double[]) bakeCache.bake(BakePlane.BASE_HEIGHT, chunkX, chunkZ), (double[]) bakeCache.complete(BakePlane.BASE_HEIGHT, chunkX, chunkZ),
(double[]) bakeCache.bake(BakePlane.NATURAL_HEIGHT, chunkX, chunkZ), (double[]) bakeCache.complete(BakePlane.NATURAL_HEIGHT, chunkX, chunkZ),
height, height,
rounded, rounded,
fluidSurface, fluidSurface,
ids((Object[]) bakeCache.bake(BakePlane.ROCK, chunkX, chunkZ), blockRegistry), ids((Object[]) bakeCache.complete(BakePlane.ROCK, chunkX, chunkZ), blockRegistry),
ids((Object[]) bakeCache.bake(BakePlane.FLUID, chunkX, chunkZ), blockRegistry), ids((Object[]) bakeCache.complete(BakePlane.FLUID, chunkX, chunkZ), blockRegistry),
terrainColumns, terrainColumns,
apron)); apron));
} }
@@ -2496,6 +2562,7 @@ public class IrisComplex implements DataProvider {
} }
return height / generators.length; return height / generators.length;
} }
GeneratorEvaluations.scalar(generators.length);
for (IrisGenerator generator : generators) { for (IrisGenerator generator : generators) {
double noise = generator.getHeight(x, z, seed); double noise = generator.getHeight(x, z, seed);
if (!Double.isFinite(noise)) { if (!Double.isFinite(noise)) {
@@ -2834,7 +2901,12 @@ public class IrisComplex implements DataProvider {
double sampleZ = integerCoordinates ? (int) zz : zz; double sampleZ = integerCoordinates ? (int) zz : zz;
IrisBiome bx = sampleCache.get(sampleX, sampleZ); IrisBiome bx = sampleCache.get(sampleX, sampleZ);
if (bx == null) { if (bx == null) {
bx = complex.sparseBaseBiome(xx, zz); ChunkBakeCache.enterSparse();
try {
bx = complex.baseBiomeStream.get(xx, zz);
} finally {
ChunkBakeCache.exitSparse();
}
sampleCache.put(sampleX, sampleZ, bx); sampleCache.put(sampleX, sampleZ, bx);
} }
@@ -37,21 +37,18 @@ public final class BakedDoubleStream extends BasicStream<Double> implements Chun
public double getDouble(double x, double z) { public double getDouble(double x, double z) {
int blockX = (int) x; int blockX = (int) x;
int blockZ = (int) z; int blockZ = (int) z;
Object values = cache.read(plane, blockX >> 4, blockZ >> 4); int index = ((blockZ & 15) << 4) | (blockX & 15);
return values == null ? scalar.getDouble(blockX, blockZ) : ((double[]) values)[((blockZ & 15) << 4) | (blockX & 15)]; ChunkPlanes.Plane values = cache.planeIfPresent(plane, blockX >> 4, blockZ >> 4);
} if (values != null && values.ready(index)) {
return values.doubleAt(index);
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(); if (!cache.memoAllowed()) {
try {
return scalar.getDouble(blockX, blockZ); return scalar.getDouble(blockX, blockZ);
} finally {
ChunkBakeCache.exitSparse();
} }
long revision = ProvisionalSampling.revision();
double value = scalar.getDouble(blockX, blockZ);
cache.storeDouble(plane, blockX, blockZ, value, revision);
return value;
} }
@Override @Override
@@ -81,15 +78,14 @@ public final class BakedDoubleStream extends BasicStream<Double> implements Chun
} }
int chunkX = worldX >> 4; int chunkX = worldX >> 4;
int chunkZ = worldZ >> 4; int chunkZ = worldZ >> 4;
Object values = cache.bakingAllowed() ? cache.bake(plane, chunkX, chunkZ) : publishedPlane(chunkX, chunkZ); if (cache.memoAllowed()) {
if (values != null) { System.arraycopy(cache.complete(plane, chunkX, chunkZ), 0, target, 0, 256);
System.arraycopy(values, 0, target, 0, 256);
return; return;
} }
int originX = chunkX << 4; int originX = chunkX << 4;
int originZ = chunkZ << 4; int originZ = chunkZ << 4;
for (int index = 0; index < 256; index++) { for (int index = 0; index < 256; index++) {
target[index] = scalar.getDouble(originX + (index & 15), originZ + (index >> 4)); target[index] = getDouble(originX + (index & 15), originZ + (index >> 4));
} }
} }
@@ -101,9 +97,4 @@ public final class BakedDoubleStream extends BasicStream<Double> implements Chun
target[index] = values[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);
}
} }
@@ -33,10 +33,7 @@ public final class BakedHeightStream extends BasicStream<Double> implements Chun
if (mainThread.getAsBoolean()) { if (mainThread.getAsBoolean()) {
return nonblocking.noise(x, z); return nonblocking.noise(x, z);
} }
int blockX = (int) x; return column((int) x, (int) z, !ChunkBakeCache.inSparseScope());
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 @Override
@@ -65,18 +62,15 @@ public final class BakedHeightStream extends BasicStream<Double> implements Chun
throw new IllegalArgumentException("Expected a 16x16 target array."); throw new IllegalArgumentException("Expected a 16x16 target array.");
} }
boolean main = mainThread.getAsBoolean(); boolean main = mainThread.getAsBoolean();
if (!main && (worldX & 15) == 0 && (worldZ & 15) == 0) { boolean memo = !main && !ChunkBakeCache.inSparseScope();
Object values = ChunkBakeCache.inSparseScope() ? published(worldX >> 4, worldZ >> 4) if (memo && (worldX & 15) == 0 && (worldZ & 15) == 0) {
: cache.bake(BakePlane.HEIGHT, worldX >> 4, worldZ >> 4); System.arraycopy(cache.complete(BakePlane.HEIGHT, worldX >> 4, worldZ >> 4), 0, target, 0, 256);
if (values != null) { return;
System.arraycopy(values, 0, target, 0, 256);
return;
}
} }
for (int index = 0; index < 256; index++) { for (int index = 0; index < 256; index++) {
int x = worldX + (index & 15); int x = worldX + (index & 15);
int z = worldZ + (index >> 4); int z = worldZ + (index >> 4);
target[index] = main ? nonblocking.noise(x, z) : column(x, z); target[index] = main ? nonblocking.noise(x, z) : column(x, z, memo);
} }
} }
@@ -89,13 +83,18 @@ public final class BakedHeightStream extends BasicStream<Double> implements Chun
} }
} }
private double column(int x, int z) { private double column(int x, int z, boolean memo) {
Object values = published(x >> 4, z >> 4); int index = ((z & 15) << 4) | (x & 15);
return values == null ? resolved.noise(x, z) : ((double[]) values)[((z & 15) << 4) | (x & 15)]; ChunkPlanes.Plane values = cache.planeIfPresent(BakePlane.HEIGHT, x >> 4, z >> 4);
} if (values != null && values.ready(index)) {
return values.doubleAt(index);
private Object published(int chunkX, int chunkZ) { }
ChunkPlanes planes = cache.planesIfPresent(chunkX, chunkZ); if (!memo) {
return planes == null ? null : planes.published(BakePlane.HEIGHT); return resolved.noise(x, z);
}
long revision = ProvisionalSampling.revision();
double value = resolved.noise(x, z);
cache.storeDouble(BakePlane.HEIGHT, x, z, value, revision);
return value;
} }
} }
@@ -33,22 +33,18 @@ public final class BakedStream<T> extends BasicStream<T> implements ChunkFillabl
public T get(double x, double z) { public T get(double x, double z) {
int blockX = (int) x; int blockX = (int) x;
int blockZ = (int) z; int blockZ = (int) z;
Object values = cache.read(plane, blockX >> 4, blockZ >> 4); int index = ((blockZ & 15) << 4) | (blockX & 15);
return values == null ? scalar.get(blockX, blockZ) : (T) ((Object[]) values)[((blockZ & 15) << 4) | (blockX & 15)]; ChunkPlanes.Plane values = cache.planeIfPresent(plane, blockX >> 4, blockZ >> 4);
} if (values != null && values.ready(index)) {
return (T) values.objectAt(index);
@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(); if (!cache.memoAllowed()) {
try {
return scalar.get(blockX, blockZ); return scalar.get(blockX, blockZ);
} finally {
ChunkBakeCache.exitSparse();
} }
long revision = ProvisionalSampling.revision();
T value = scalar.get(blockX, blockZ);
cache.storeObject(plane, blockX, blockZ, value, revision);
return value;
} }
@Override @Override
@@ -70,20 +66,14 @@ public final class BakedStream<T> extends BasicStream<T> implements ChunkFillabl
public void fillChunkRaw(int worldX, int worldZ, Object[] target) { public void fillChunkRaw(int worldX, int worldZ, Object[] target) {
int chunkX = worldX >> 4; int chunkX = worldX >> 4;
int chunkZ = worldZ >> 4; int chunkZ = worldZ >> 4;
Object values = cache.bakingAllowed() ? cache.bake(plane, chunkX, chunkZ) : publishedPlane(chunkX, chunkZ); if (cache.memoAllowed()) {
if (values != null) { System.arraycopy(cache.complete(plane, chunkX, chunkZ), 0, target, 0, 256);
System.arraycopy(values, 0, target, 0, 256);
return; return;
} }
int originX = chunkX << 4; int originX = chunkX << 4;
int originZ = chunkZ << 4; int originZ = chunkZ << 4;
for (int index = 0; index < 256; index++) { for (int index = 0; index < 256; index++) {
target[index] = scalar.get(originX + (index & 15), originZ + (index >> 4)); target[index] = 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,204 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.generation.bake.BakePlane;
import art.arcane.iris.generation.bake.GeneratorEvaluations;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.image.IrisImageMapApplication;
import art.arcane.iris.generation.image.IrisImageMapRuntime;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.MockedStatic;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.SplittableRandom;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Answers.CALLS_REAL_METHODS;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class IrisComplexBakeParityTest {
@ClassRule public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
private IrisSettings previousSettings;
@Before
public void prepareRuntime() {
previousSettings = IrisSettings.settings;
IrisSettings.settings = new IrisSettings();
IrisServices.register(PreservationRegistry.class, mock(PreservationRegistry.class));
IrisPlatform platform = mock(IrisPlatform.class);
when(platform.platformName()).thenReturn("bukkit");
IrisPlatforms.bind(platform);
}
@After
public void releaseRuntime() {
IrisPlatforms.unbind();
IrisServices.remove(PreservationRegistry.class);
IrisSettings.settings = previousSettings;
}
@Test
public void batchedChunkBaseHeightsEqualScalarHeightsBitForBit() throws Exception {
try (Statics ignored = new Statics(); Fixture batched = new Fixture(pack()); Fixture scalar = new Fixture(pack())) {
SplittableRandom random = new SplittableRandom(0xBA5EL);
for (int chunk = 0; chunk < 48; chunk++) {
int chunkX = random.nextInt(-400, 400);
int chunkZ = random.nextInt(-400, 400);
double[] baked = (double[]) batched.complex.bakePlane(BakePlane.BASE_HEIGHT, chunkX, chunkZ);
for (int index = 0; index < 256; index++) {
int x = (chunkX << 4) + (index & 15);
int z = (chunkZ << 4) + (index >> 4);
assertEquals("chunk " + chunkX + "," + chunkZ + " column " + index,
Double.doubleToRawLongBits(scalar.complex.getBaseTerrainHeightStream().getDouble(x, z)),
Double.doubleToRawLongBits(baked[index]));
}
}
}
}
@Test
public void batchedChunksEvaluateExactlyTheScalarGeneratorSet() throws Exception {
try (Statics ignored = new Statics(); Fixture batched = new Fixture(pack()); Fixture scalar = new Fixture(pack())) {
SplittableRandom random = new SplittableRandom(0xC0DEL);
long totalBatched = 0L;
for (int chunk = 0; chunk < 32; chunk++) {
int chunkX = random.nextInt(-400, 400);
int chunkZ = random.nextInt(-400, 400);
GeneratorEvaluations.Snapshot before = GeneratorEvaluations.snapshot();
batched.complex.bakePlane(BakePlane.BASE_HEIGHT, chunkX, chunkZ);
GeneratorEvaluations.Snapshot afterBatch = GeneratorEvaluations.snapshot();
for (int index = 0; index < 256; index++) {
scalar.complex.getBaseTerrainHeightStream().getDouble((chunkX << 4) + (index & 15), (chunkZ << 4) + (index >> 4));
}
GeneratorEvaluations.Snapshot afterScalar = GeneratorEvaluations.snapshot();
long batchedEvaluations = afterBatch.minus(before).bakedEvaluations();
assertEquals(0L, afterBatch.minus(before).scalarEvaluations());
assertEquals("chunk " + chunkX + "," + chunkZ, batchedEvaluations,
afterScalar.minus(afterBatch).scalarEvaluations());
totalBatched += batchedEvaluations;
}
assertTrue(totalBatched > 0L);
}
}
private Path pack() throws Exception {
Path pack = temporaryFolder.newFolder().toPath();
Files.createDirectories(pack.resolve("dimensions"));
Files.createDirectories(pack.resolve("regions"));
Files.createDirectories(pack.resolve("biomes"));
Files.createDirectories(pack.resolve("generators"));
Files.writeString(pack.resolve("dimensions/main.json"), """
{"regions":["alpha","beta"],"landChance":0.55,"regionZoom":0.2,"biomeZoom":0.15,
"carvingEnabled":false,"hydrology":{"rivers":{"enabled":false}}}
""");
Files.writeString(pack.resolve("regions/alpha.json"),
"{\"landBiomes\":[\"peaks\",\"meadow\",\"dunes\"],\"seaBiomes\":[\"sea\"],\"rarity\":1}");
Files.writeString(pack.resolve("regions/beta.json"),
"{\"landBiomes\":[\"meadow\",\"spires\"],\"seaBiomes\":[\"sea\"],\"rarity\":2}");
generator(pack, "mountain", "BILINEAR_STARCAST_9", 52, 0.5D, """
[{"style":{"style":"FRACTAL_SMOKE","zoom":1},"seed":3356}]""", "");
generator(pack, "magnet", "BILINEAR_STARCAST_9", 52, 0.5D, """
[{"style":{"style":"FRACTAL_SMOKE","zoom":0.6},"seed":911}]""", "");
generator(pack, "plain", "BILINEAR_STARCAST_9", 12, 0.5D, """
[{"style":{"style":"IRIS_DOUBLE","zoom":0.87,"fracture":{"style":"NOWHERE","zoom":0.195,"multiplier":18}},"seed":10056,"bezier":true}]""", "");
generator(pack, "cliffs", "BILINEAR_STARCAST_9", 12, 0.5D, """
[{"style":{"style":"GLOB","zoom":0.4,"exponent":0.7,"fracture":{"style":"NOWHERE","zoom":0.1,"multiplier":5}},"negative":true,"seed":10056}]""",
",\"cliffHeightMax\":80,\"cliffHeightMin\":35,\"cliffHeightGenerator\":{\"seed\":2348,\"style\":{\"style\":\"CELLULAR_HEIGHT\"},\"zoom\":0.4}");
generator(pack, "dunes", "BILINEAR_STARCAST_9", 23, 1D, """
[{"style":{"style":"SIMPLEX","zoom":2},"seed":77,"fracture":[{"style":{"style":"PERLIN"},"seed":5,"opacity":2.5}]}]""", "");
generator(pack, "spikes", "STARCAST_12", 42, 0D, """
[{"style":{"style":"CELLULAR","zoom":0.5},"seed":4}]""", "");
biome(pack, "peaks", "[{\"generator\":\"mountain\",\"min\":40,\"max\":120},{\"generator\":\"spikes\",\"min\":0,\"max\":20}]",
",\"children\":[\"meadow\"],\"childShrinkFactor\":1.5");
biome(pack, "meadow", "[{\"generator\":\"plain\",\"min\":2,\"max\":14},{\"generator\":\"cliffs\",\"min\":0,\"max\":9}]", "");
biome(pack, "dunes", "[{\"generator\":\"dunes\",\"min\":3,\"max\":30},{\"generator\":\"magnet\",\"min\":-4,\"max\":6}]", "");
biome(pack, "spires", "[{\"generator\":\"spikes\",\"min\":10,\"max\":60},{\"generator\":\"plain\",\"min\":1,\"max\":4}]", "");
biome(pack, "sea", "[{\"generator\":\"mountain\",\"min\":-32,\"max\":-10}]", "");
return pack;
}
private static void generator(Path pack, String key, String function, double scale, double detail,
String composite, String extra) throws Exception {
Files.writeString(pack.resolve("generators/" + key + ".json"), "{\"surfaceDetail\":" + detail
+ ",\"interpolator\":{\"function\":\"" + function + "\",\"horizontalScale\":" + scale + "}"
+ ",\"seed\":" + Math.abs(key.hashCode()) + ",\"composite\":" + composite.strip() + extra + "}");
}
private static void biome(Path pack, String key, String generators, String extra) throws Exception {
Files.writeString(pack.resolve("biomes/" + key + ".json"), "{\"generators\":" + generators + extra + "}");
}
private static final class Statics implements AutoCloseable {
private final MockedStatic<B> blocks = mockStatic(B.class);
private final MockedStatic<IrisImageMapRuntime> imageRuntimes = mockStatic(IrisImageMapRuntime.class);
private Statics() {
NativeBlockState block = mock(NativeBlockState.class);
when(block.isFluid()).thenReturn(true);
blocks.when(() -> B.getState(anyString())).thenReturn(block);
blocks.when(() -> B.getStateOrNull(anyString(), eq(false))).thenReturn(block);
IrisImageMapRuntime images = mock(IrisImageMapRuntime.class);
when(images.has(IrisImageMapApplication.BIOME)).thenReturn(false);
when(images.sampleTerrainHeight(anyDouble(), anyDouble(), anyDouble()))
.thenAnswer(invocation -> invocation.<Double>getArgument(2));
imageRuntimes.when(() -> IrisImageMapRuntime.compile(any(Engine.class))).thenReturn(images);
}
@Override
public void close() {
imageRuntimes.close();
blocks.close();
}
}
private static final class Fixture implements AutoCloseable {
private final Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
private final EnginePlatformHooks hooks = mock(EnginePlatformHooks.class);
private final IrisData data;
private final IrisComplex complex;
private Fixture(Path pack) {
data = IrisData.openRuntime(pack.toFile());
IrisDimension dimension = data.getDimensionLoader().load("main");
doReturn(data).when(engine).getData();
doReturn(dimension).when(engine).getDimension();
doReturn(384).when(engine).getHeight();
doReturn(-64).when(engine).getMinHeight();
doReturn(320).when(engine).getMaxHeight();
when(hooks.isMainThread()).thenReturn(false);
doReturn(hooks).when(engine).getPlatformHooks();
doReturn(new SeedManager(1337L)).when(engine).getSeedManager();
complex = new IrisComplex(engine);
doReturn(complex).when(engine).getComplex();
}
@Override
public void close() {
complex.close();
data.close();
}
}
}
@@ -18,6 +18,8 @@
package art.arcane.iris.probe; package art.arcane.iris.probe;
import art.arcane.iris.generation.bake.BakePlane;
import art.arcane.iris.generation.bake.GeneratorEvaluations;
import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.PackValidationResult; import art.arcane.iris.pack.PackValidationResult;
import art.arcane.iris.pack.PackValidator; import art.arcane.iris.pack.PackValidator;
@@ -553,9 +555,11 @@ public final class GenerationProbe {
List<ChunkCoordinate> coordinates, OutcomeSink sink) throws IOException { List<ChunkCoordinate> coordinates, OutcomeSink sink) throws IOException {
int totalChunks = coordinates.size(); int totalChunks = coordinates.size();
long measuredStartEpochMillis = 0L; long measuredStartEpochMillis = 0L;
GeneratorEvaluations.Snapshot measuredStartEvaluations = GeneratorEvaluations.snapshot();
for (int index = 0; index < totalChunks; index++) { for (int index = 0; index < totalChunks; index++) {
if (index == configuration.warmupChunks()) { if (index == configuration.warmupChunks()) {
measuredStartEpochMillis = System.currentTimeMillis(); measuredStartEpochMillis = System.currentTimeMillis();
measuredStartEvaluations = GeneratorEvaluations.snapshot();
} }
drainReported(); drainReported();
ChunkOutcome outcome = generateChunk(engine, index, coordinates.get(index), configuration.multicore()); ChunkOutcome outcome = generateChunk(engine, index, coordinates.get(index), configuration.multicore());
@@ -563,12 +567,22 @@ public final class GenerationProbe {
sink.accept(outcome, index >= configuration.warmupChunks()); sink.accept(outcome, index >= configuration.warmupChunks());
printProgress(configuration, index + 1, totalChunks); printProgress(configuration, index + 1, totalChunks);
} }
printWindow(measuredStartEpochMillis, System.currentTimeMillis()); printWindow(engine, measuredStartEpochMillis, System.currentTimeMillis(), measuredStartEvaluations);
} }
private static void printWindow(long startEpochMillis, long endEpochMillis) { private static void printWindow(Engine engine, long startEpochMillis, long endEpochMillis, GeneratorEvaluations.Snapshot startEvaluations) {
System.out.println("IRIS_GENPROBE_WINDOW phase=measured start_epoch_ms=" + startEpochMillis System.out.println("IRIS_GENPROBE_WINDOW phase=measured start_epoch_ms=" + startEpochMillis
+ " end_epoch_ms=" + endEpochMillis); + " end_epoch_ms=" + endEpochMillis);
GeneratorEvaluations.Snapshot evaluations = GeneratorEvaluations.snapshot().minus(startEvaluations);
System.out.printf(Locale.ROOT, "IRIS_GENPROBE_GENERATORS baked_chunks=%d baked_evaluations=%d evaluations_per_baked_chunk=%.3f batched_evaluations=%d scalar_evaluations=%d%n",
evaluations.bakedChunks(), evaluations.bakedEvaluations(), evaluations.bakedEvaluationsPerChunk(),
evaluations.batchedEvaluations(), evaluations.scalarEvaluations());
long[] census = engine.getComplex().bakeCensus();
StringBuilder planes = new StringBuilder("IRIS_GENPROBE_BAKE entries=").append(census[BakePlane.values().length]);
for (BakePlane plane : BakePlane.values()) {
planes.append(' ').append(plane.name().toLowerCase(Locale.ROOT)).append('=').append(census[plane.ordinal()]);
}
System.out.println(planes);
} }
private static void generateParallel(Engine engine, ProbeConfiguration configuration, ProbeOptions options, private static void generateParallel(Engine engine, ProbeConfiguration configuration, ProbeOptions options,
@@ -594,10 +608,11 @@ public final class GenerationProbe {
List<Integer> measured = spiralOrder(coordinates, configuration.warmupChunks(), List<Integer> measured = spiralOrder(coordinates, configuration.warmupChunks(),
configuration.centerChunkX(), configuration.centerChunkZ()); configuration.centerChunkX(), configuration.centerChunkZ());
long measuredStartEpochMillis = System.currentTimeMillis(); long measuredStartEpochMillis = System.currentTimeMillis();
GeneratorEvaluations.Snapshot measuredStartEvaluations = GeneratorEvaluations.snapshot();
long measuredStarted = System.nanoTime(); long measuredStarted = System.nanoTime();
runPhase(executor, engine, configuration, coordinates, measured, outcomes); runPhase(executor, engine, configuration, coordinates, measured, outcomes);
measuredWallNanos = System.nanoTime() - measuredStarted; measuredWallNanos = System.nanoTime() - measuredStarted;
printWindow(measuredStartEpochMillis, System.currentTimeMillis()); printWindow(engine, measuredStartEpochMillis, System.currentTimeMillis(), measuredStartEvaluations);
unattributed.addAll(drainReported()); unattributed.addAll(drainReported());
System.out.println("[genprobe] measured progress: " + configuration.measuredChunks() + "/" System.out.println("[genprobe] measured progress: " + configuration.measuredChunks() + "/"
+ configuration.measuredChunks()); + configuration.measuredChunks());