mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 14:00:26 +00:00
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:
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;
|
||||
|
||||
public enum BakePlane {
|
||||
REGION,
|
||||
BASE_BIOME,
|
||||
BASE_HEIGHT,
|
||||
NATURAL_HEIGHT,
|
||||
NATURAL_TRUE_BIOME,
|
||||
HEIGHT,
|
||||
TRUE_BIOME,
|
||||
ROCK,
|
||||
FLUID,
|
||||
CAVE_BIOME;
|
||||
REGION(false),
|
||||
BASE_BIOME(false),
|
||||
BASE_HEIGHT(true),
|
||||
NATURAL_HEIGHT(true),
|
||||
NATURAL_TRUE_BIOME(false),
|
||||
HEIGHT(true),
|
||||
TRUE_BIOME(false),
|
||||
ROCK(false),
|
||||
FLUID(false),
|
||||
CAVE_BIOME(false);
|
||||
|
||||
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;
|
||||
|
||||
import art.arcane.iris.generation.cache.ConcurrentClockCache;
|
||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||
|
||||
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 static final ThreadLocal<int[]> SPARSE = ThreadLocal.withInitial(() -> new int[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);
|
||||
return planes(chunkX, chunkZ).columnBake(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -50,6 +46,19 @@ public final class ChunkBakeCache implements BakeCache {
|
||||
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) {
|
||||
ConcurrentClockCache<ChunkPlanes> current = entries;
|
||||
long key = key(chunkX, chunkZ);
|
||||
@@ -61,38 +70,35 @@ public final class ChunkBakeCache implements BakeCache {
|
||||
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) {
|
||||
public ChunkPlanes.Plane planeIfPresent(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);
|
||||
return planes == null ? null : planes.planeIfPresent(plane);
|
||||
}
|
||||
|
||||
public boolean denseReadsAllowed() {
|
||||
int[] depth = DEPTH.get();
|
||||
if (depth[SPARSE] > 0 || source.mainThread()) {
|
||||
return false;
|
||||
public ChunkPlanes.Plane plane(BakePlane plane, int chunkX, int chunkZ) {
|
||||
return planes(chunkX, chunkZ).plane(plane);
|
||||
}
|
||||
|
||||
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() {
|
||||
return !inSparseScope() && !source.mainThread();
|
||||
}
|
||||
|
||||
public static boolean inSparseScope() {
|
||||
return DEPTH.get()[SPARSE] > 0;
|
||||
public void storeObject(BakePlane plane, int blockX, int blockZ, Object value, long revision) {
|
||||
if (revision != ProvisionalSampling.revision()) {
|
||||
return;
|
||||
}
|
||||
planes(blockX >> 4, blockZ >> 4).plane(plane).storeObject(((blockZ & 15) << 4) | (blockX & 15), value);
|
||||
}
|
||||
|
||||
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() {
|
||||
DEPTH.get()[SPARSE]++;
|
||||
SPARSE.get()[0]++;
|
||||
}
|
||||
|
||||
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) {
|
||||
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 java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.VarHandle;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.atomic.AtomicReferenceArray;
|
||||
|
||||
public final class ChunkPlanes {
|
||||
private static final VarHandle READY = MethodHandles.arrayElementVarHandle(long[].class);
|
||||
|
||||
private final int chunkX;
|
||||
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;
|
||||
|
||||
ChunkPlanes(int chunkX, int chunkZ) {
|
||||
@@ -23,40 +28,50 @@ public final class ChunkPlanes {
|
||||
return chunkZ;
|
||||
}
|
||||
|
||||
public Object published(BakePlane plane) {
|
||||
Object state = planes.getAcquire(plane.ordinal());
|
||||
return state instanceof Pending ? null : state;
|
||||
public Plane planeIfPresent(BakePlane kind) {
|
||||
return planes.getAcquire(kind.ordinal());
|
||||
}
|
||||
|
||||
Object plane(BakePlane plane, PlaneSource source) {
|
||||
int slot = plane.ordinal();
|
||||
public Object published(BakePlane kind) {
|
||||
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) {
|
||||
Object state = planes.getAcquire(slot);
|
||||
if (state != null && !(state instanceof Pending)) {
|
||||
return state;
|
||||
if (plane.complete) {
|
||||
return plane.values;
|
||||
}
|
||||
if (state instanceof Pending pending) {
|
||||
if (pending.owner == Thread.currentThread()) {
|
||||
return source.compute(plane, chunkX, chunkZ);
|
||||
Pending running = plane.filling.get();
|
||||
if (running != null) {
|
||||
if (running.owner == Thread.currentThread()) {
|
||||
return plane.unpublishedFill(source, chunkX, chunkZ);
|
||||
}
|
||||
pending.await();
|
||||
running.await();
|
||||
continue;
|
||||
}
|
||||
Pending pending = new Pending(Thread.currentThread());
|
||||
if (!planes.compareAndSet(slot, null, pending)) {
|
||||
if (!plane.filling.compareAndSet(null, pending)) {
|
||||
continue;
|
||||
}
|
||||
Object computed = null;
|
||||
boolean publishable = false;
|
||||
try {
|
||||
long revision = ProvisionalSampling.revision();
|
||||
computed = source.compute(plane, chunkX, chunkZ);
|
||||
publishable = revision == ProvisionalSampling.revision();
|
||||
return plane.fill(source, chunkX, chunkZ);
|
||||
} finally {
|
||||
planes.setRelease(slot, publishable ? computed : null);
|
||||
plane.filling.set(null);
|
||||
pending.complete();
|
||||
}
|
||||
return computed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +92,97 @@ public final class ChunkPlanes {
|
||||
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 final Thread owner;
|
||||
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;
|
||||
|
||||
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);
|
||||
|
||||
boolean mainThread();
|
||||
|
||||
boolean generationContext();
|
||||
}
|
||||
@@ -6,7 +6,6 @@ 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;
|
||||
@@ -16,7 +15,6 @@ public final class TerrainProgram {
|
||||
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;
|
||||
@@ -44,19 +42,11 @@ public final class TerrainProgram {
|
||||
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,
|
||||
public long 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;
|
||||
return 0L;
|
||||
}
|
||||
int mark = scratch.mark();
|
||||
int indexMark = scratch.indexMark();
|
||||
@@ -87,7 +77,7 @@ public final class TerrainProgram {
|
||||
}
|
||||
if (activeCount == 0) {
|
||||
scratch.resetIndices(indexMark);
|
||||
return;
|
||||
return 0L;
|
||||
}
|
||||
double[] activeX = scratch.buffer(activeCount);
|
||||
double[] activeZ = scratch.buffer(activeCount);
|
||||
@@ -129,8 +119,8 @@ public final class TerrainProgram {
|
||||
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);
|
||||
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.ChunkPlanes;
|
||||
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.SparseHeightMemo;
|
||||
import art.arcane.iris.generation.bake.TerrainProgram;
|
||||
@@ -406,7 +407,7 @@ public class IrisComplex implements DataProvider {
|
||||
|
||||
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()
|
||||
.select(engine.getDimension().getRockPalette().getBlockData(data));
|
||||
rockStream = new BakedStream<>(bakeCache, BakePlane.ROCK, rockSource);
|
||||
@@ -844,6 +845,7 @@ public class IrisComplex implements DataProvider {
|
||||
zs[index] = RiverFootprint.unpackZ(missing[index]);
|
||||
}
|
||||
TerrainProgram program = terrainProgram();
|
||||
long evaluations = 0L;
|
||||
for (int group = 0; group < frozenInterpolators.length; group++) {
|
||||
if (frozenGenerators[group].length == 0) {
|
||||
continue;
|
||||
@@ -854,8 +856,9 @@ public class IrisComplex implements DataProvider {
|
||||
lows[index] = bounds.min();
|
||||
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++) {
|
||||
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) {
|
||||
double[] values = new double[256];
|
||||
private void bakeBaseHeights(int chunkX, int chunkZ, long[] missing, double[] values) {
|
||||
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));
|
||||
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();
|
||||
int mark = scratch.mark();
|
||||
@@ -883,44 +887,55 @@ public class IrisComplex implements DataProvider {
|
||||
double[] zs = scratch.buffer(256);
|
||||
double[] lows = 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++) {
|
||||
xs[index] = originX + (index & 15);
|
||||
zs[index] = originZ + (index >> 4);
|
||||
if ((missing[index >>> 6] & (1L << (index & 63))) != 0L) {
|
||||
xs[count] = originX + (index & 15);
|
||||
zs[count] = originZ + (index >> 4);
|
||||
heights[count] = 0D;
|
||||
columns[count++] = index;
|
||||
}
|
||||
}
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
TerrainProgram program = terrainProgram();
|
||||
GridBoundsCache cache = gridBoundsCache.get();
|
||||
int grid = biomeBoundsSamplingStep;
|
||||
boolean corners = ChunkCorners.supports(grid);
|
||||
long evaluations = 0L;
|
||||
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)) {
|
||||
if (!chunkCornerBounds(cache, group, grid, originX, originZ, xs, zs, count, lows, highs)) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
for (int index = 0; index < 256; index++) {
|
||||
for (int index = 0; index < count; 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);
|
||||
evaluations += program.accumulate(group, scratch, xs, zs, count, lows, highs, heights);
|
||||
}
|
||||
for (int index = 0; index < 256; index++) {
|
||||
values[index] = finishBaseHeight(xs[index], zs[index], values[index]);
|
||||
for (int index = 0; index < count; index++) {
|
||||
values[columns[index]] = finishBaseHeight(xs[index], zs[index], heights[index]);
|
||||
}
|
||||
GeneratorEvaluations.bakedChunk(evaluations);
|
||||
} 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) {
|
||||
double[] xs, double[] zs, int count, double[] lows, double[] highs) {
|
||||
int cells = 16 / grid;
|
||||
int stride = cells + 1;
|
||||
long[] packed = new long[stride * stride];
|
||||
@@ -936,7 +951,7 @@ public class IrisComplex implements DataProvider {
|
||||
if (silent) {
|
||||
return false;
|
||||
}
|
||||
for (int index = 0; index < 256; index++) {
|
||||
for (int index = 0; index < count; index++) {
|
||||
double x = xs[index];
|
||||
double z = zs[index];
|
||||
int gx = ((int) Math.floor(x)) & ~(grid - 1);
|
||||
@@ -967,6 +982,62 @@ public class IrisComplex implements DataProvider {
|
||||
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() {
|
||||
return terrainEngine.getDimension().getTerrainSamplingStep() == 1 && biomeBuffetGenerators.isEmpty()
|
||||
&& (biomeBuffet == null || biomeBuffet.cells().isEmpty());
|
||||
@@ -1012,9 +1083,9 @@ public class IrisComplex implements DataProvider {
|
||||
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
|
||||
if (bakedColumn(BakePlane.NATURAL_HEIGHT, 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))) {
|
||||
continue;
|
||||
}
|
||||
@@ -1027,22 +1098,14 @@ public class IrisComplex implements DataProvider {
|
||||
sparseBaseHeights.put(RiverFootprint.unpackX(required[index]), RiverFootprint.unpackZ(required[index]), baseHeights[index]);
|
||||
}
|
||||
}
|
||||
BakedDoubleStream naturalHeights = (BakedDoubleStream) naturalHeightStream;
|
||||
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 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 boolean bakedColumn(BakePlane plane, int blockX, int blockZ) {
|
||||
ChunkPlanes.Plane values = bakeCache.planeIfPresent(plane, blockX >> 4, blockZ >> 4);
|
||||
return values != null && values.ready(((blockZ & 15) << 4) | (blockX & 15));
|
||||
}
|
||||
|
||||
private final class HydrologyHeightProvider implements IrisHydrologyNaturalHeightProvider {
|
||||
@@ -1050,7 +1113,12 @@ public class IrisComplex implements DataProvider {
|
||||
|
||||
@Override
|
||||
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
|
||||
@@ -1075,19 +1143,19 @@ public class IrisComplex implements DataProvider {
|
||||
|
||||
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);
|
||||
};
|
||||
public void fill(BakePlane plane, int chunkX, int chunkZ, long[] missing, Object values) {
|
||||
switch (plane) {
|
||||
case REGION -> objectPlane(regionSource, chunkX, chunkZ, missing, (Object[]) values);
|
||||
case BASE_BIOME -> objectPlane(baseBiomeSource, chunkX, chunkZ, missing, (Object[]) values);
|
||||
case BASE_HEIGHT -> basePlane(chunkX, chunkZ, missing, (double[]) values);
|
||||
case NATURAL_HEIGHT -> naturalPlane(chunkX, chunkZ, missing, (double[]) values);
|
||||
case NATURAL_TRUE_BIOME -> objectPlane(naturalTrueBiomeSource, chunkX, chunkZ, missing, (Object[]) values);
|
||||
case HEIGHT -> heightPlane(chunkX, chunkZ, missing, (double[]) values);
|
||||
case TRUE_BIOME -> objectPlane(trueBiomeSource, chunkX, chunkZ, missing, (Object[]) values);
|
||||
case ROCK -> objectPlane(rockSource, chunkX, chunkZ, missing, (Object[]) values);
|
||||
case FLUID -> objectPlane(fluidSource, chunkX, chunkZ, missing, (Object[]) values);
|
||||
case CAVE_BIOME -> objectPlane(caveSource, chunkX, chunkZ, missing, (Object[]) values);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1100,52 +1168,50 @@ public class IrisComplex implements DataProvider {
|
||||
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];
|
||||
private void objectPlane(ProceduralStream<?> source, int chunkX, int chunkZ, long[] missing, Object[] values) {
|
||||
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));
|
||||
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) {
|
||||
double[] values = new double[256];
|
||||
private void naturalPlane(int chunkX, int chunkZ, long[] missing, double[] values) {
|
||||
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 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);
|
||||
if (!main && !ChunkBakeCache.inSparseScope()) {
|
||||
bakeCache.complete(BakePlane.NATURAL_HEIGHT, chunkX, chunkZ);
|
||||
}
|
||||
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));
|
||||
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) {
|
||||
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) {
|
||||
@@ -1177,7 +1243,7 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
|
||||
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];
|
||||
SimdSupport.kernels().roundToInt(height, rounded, 256);
|
||||
int originX = chunkX << 4;
|
||||
@@ -1197,17 +1263,17 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
}
|
||||
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),
|
||||
ids((Object[]) bakeCache.complete(BakePlane.REGION, chunkX, chunkZ), regionRegistry),
|
||||
ids((Object[]) bakeCache.complete(BakePlane.BASE_BIOME, chunkX, chunkZ), biomeRegistry),
|
||||
ids((Object[]) bakeCache.complete(BakePlane.TRUE_BIOME, chunkX, chunkZ), biomeRegistry),
|
||||
ids((Object[]) bakeCache.complete(BakePlane.CAVE_BIOME, chunkX, chunkZ), biomeRegistry),
|
||||
(double[]) bakeCache.complete(BakePlane.BASE_HEIGHT, chunkX, chunkZ),
|
||||
(double[]) bakeCache.complete(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),
|
||||
ids((Object[]) bakeCache.complete(BakePlane.ROCK, chunkX, chunkZ), blockRegistry),
|
||||
ids((Object[]) bakeCache.complete(BakePlane.FLUID, chunkX, chunkZ), blockRegistry),
|
||||
terrainColumns,
|
||||
apron));
|
||||
}
|
||||
@@ -2496,6 +2562,7 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
return height / generators.length;
|
||||
}
|
||||
GeneratorEvaluations.scalar(generators.length);
|
||||
for (IrisGenerator generator : generators) {
|
||||
double noise = generator.getHeight(x, z, seed);
|
||||
if (!Double.isFinite(noise)) {
|
||||
@@ -2834,7 +2901,12 @@ public class IrisComplex implements DataProvider {
|
||||
double sampleZ = integerCoordinates ? (int) zz : zz;
|
||||
IrisBiome bx = sampleCache.get(sampleX, sampleZ);
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,21 +37,18 @@ public final class BakedDoubleStream extends BasicStream<Double> implements Chun
|
||||
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)];
|
||||
int index = ((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);
|
||||
}
|
||||
ChunkBakeCache.enterSparse();
|
||||
try {
|
||||
if (!cache.memoAllowed()) {
|
||||
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
|
||||
@@ -81,15 +78,14 @@ public final class BakedDoubleStream extends BasicStream<Double> implements Chun
|
||||
}
|
||||
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);
|
||||
if (cache.memoAllowed()) {
|
||||
System.arraycopy(cache.complete(plane, chunkX, chunkZ), 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));
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
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()) {
|
||||
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)];
|
||||
return column((int) x, (int) z, !ChunkBakeCache.inSparseScope());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -65,18 +62,15 @@ public final class BakedHeightStream extends BasicStream<Double> implements Chun
|
||||
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;
|
||||
}
|
||||
boolean memo = !main && !ChunkBakeCache.inSparseScope();
|
||||
if (memo && (worldX & 15) == 0 && (worldZ & 15) == 0) {
|
||||
System.arraycopy(cache.complete(BakePlane.HEIGHT, worldX >> 4, worldZ >> 4), 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);
|
||||
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) {
|
||||
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);
|
||||
private double column(int x, int z, boolean memo) {
|
||||
int index = ((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);
|
||||
}
|
||||
if (!memo) {
|
||||
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) {
|
||||
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)];
|
||||
int index = ((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);
|
||||
}
|
||||
ChunkBakeCache.enterSparse();
|
||||
try {
|
||||
if (!cache.memoAllowed()) {
|
||||
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
|
||||
@@ -70,20 +66,14 @@ public final class BakedStream<T> extends BasicStream<T> implements ChunkFillabl
|
||||
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);
|
||||
if (cache.memoAllowed()) {
|
||||
System.arraycopy(cache.complete(plane, chunkX, chunkZ), 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));
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user