diff --git a/core/src/main/java/art/arcane/iris/generation/bake/BakeCache.java b/core/src/main/java/art/arcane/iris/generation/bake/BakeCache.java new file mode 100644 index 000000000..5054c537c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/bake/BakeCache.java @@ -0,0 +1,7 @@ +package art.arcane.iris.generation.bake; + +public interface BakeCache { + ColumnBake get(int chunkX, int chunkZ); + + ColumnBake getIfPresent(int chunkX, int chunkZ); +} diff --git a/core/src/main/java/art/arcane/iris/generation/bake/BakePlane.java b/core/src/main/java/art/arcane/iris/generation/bake/BakePlane.java new file mode 100644 index 000000000..a915f3222 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/bake/BakePlane.java @@ -0,0 +1,26 @@ +package art.arcane.iris.generation.bake; + +public enum BakePlane { + 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; + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/bake/BakeRegistry.java b/core/src/main/java/art/arcane/iris/generation/bake/BakeRegistry.java new file mode 100644 index 000000000..205982885 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/bake/BakeRegistry.java @@ -0,0 +1,41 @@ +package art.arcane.iris.generation.bake; + +import java.util.Arrays; +import java.util.IdentityHashMap; +import java.util.Objects; + +public final class BakeRegistry { + private final Object lock = new Object(); + private volatile IdentityHashMap ids = new IdentityHashMap<>(); + private volatile Object[] values = new Object[16]; + private int size; + + public short id(T value) { + Objects.requireNonNull(value, "Registered value"); + Integer known = ids.get(value); + if (known != null) { + return known.shortValue(); + } + synchronized (lock) { + Integer existing = ids.get(value); + if (existing != null) { + return existing.shortValue(); + } + if (size == Short.MAX_VALUE) { + throw new IllegalStateException("Bake registry is full"); + } + Object[] grown = size == values.length ? Arrays.copyOf(values, size * 2) : values; + grown[size] = value; + values = grown; + IdentityHashMap next = new IdentityHashMap<>(ids); + next.put(value, size); + ids = next; + return (short) size++; + } + } + + @SuppressWarnings("unchecked") + public T value(int id) { + return (T) values[id]; + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/bake/ChunkBakeCache.java b/core/src/main/java/art/arcane/iris/generation/bake/ChunkBakeCache.java new file mode 100644 index 000000000..b1466b5a7 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/bake/ChunkBakeCache.java @@ -0,0 +1,153 @@ +package art.arcane.iris.generation.bake; + +import art.arcane.iris.generation.cache.ConcurrentClockCache; +import art.arcane.iris.generation.stream.ProvisionalSampling; + +import java.util.Objects; + +public final class ChunkBakeCache implements BakeCache { + private static final ThreadLocal SPARSE = ThreadLocal.withInitial(() -> new int[1]); + + private final PlaneSource source; + private final ThreadLocal recent = ThreadLocal.withInitial(Recent::new); + private volatile ConcurrentClockCache entries; + + public ChunkBakeCache(PlaneSource source, int capacityChunks) { + this.source = Objects.requireNonNull(source, "Bake plane source"); + this.entries = new ConcurrentClockCache<>(Math.max(16, capacityChunks)); + } + + @Override + public ColumnBake get(int chunkX, int chunkZ) { + return planes(chunkX, chunkZ).columnBake(source); + } + + @Override + public ColumnBake getIfPresent(int chunkX, int chunkZ) { + ChunkPlanes planes = planesIfPresent(chunkX, chunkZ); + return planes == null ? null : planes.columnBake(); + } + + public int capacity() { + return entries.capacity(); + } + + public synchronized void ensureCapacity(int chunks) { + ConcurrentClockCache current = entries; + if (current.capacity() >= chunks) { + return; + } + ConcurrentClockCache resized = new ConcurrentClockCache<>(chunks); + current.forEach(planes -> resized.putIfAbsent(key(planes.chunkX(), planes.chunkZ()), planes)); + entries = resized; + } + + public void clear() { + ConcurrentClockCache current = entries; + entries = new ConcurrentClockCache<>(current.capacity()); + current.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 current = entries; + long key = key(chunkX, chunkZ); + ChunkPlanes existing = current.get(key); + return existing != null ? existing : current.putIfAbsent(key, new ChunkPlanes(chunkX, chunkZ)); + } + + public ChunkPlanes planesIfPresent(int chunkX, int chunkZ) { + return entries.get(key(chunkX, chunkZ)); + } + + public ChunkPlanes.Plane planeIfPresent(BakePlane plane, int chunkX, int chunkZ) { + long key = key(chunkX, chunkZ); + Recent last = recent.get(); + ConcurrentClockCache current = entries; + ChunkPlanes planes; + if (last.key == key && last.entries == current) { + planes = last.planes; + } else { + planes = current.get(key); + if (planes == null) { + return null; + } + last.key = key; + last.planes = planes; + last.entries = current; + } + return planes.planeIfPresent(plane); + } + + 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 remembered(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; + } + remembered(blockX >> 4, blockZ >> 4).plane(plane).storeDouble(((blockZ & 15) << 4) | (blockX & 15), value); + } + + public void storeObject(BakePlane plane, int blockX, int blockZ, Object value, long revision) { + if (revision != ProvisionalSampling.revision()) { + return; + } + remembered(blockX >> 4, blockZ >> 4).plane(plane).storeObject(((blockZ & 15) << 4) | (blockX & 15), value); + } + + private ChunkPlanes remembered(int chunkX, int chunkZ) { + ConcurrentClockCache current = entries; + long key = key(chunkX, chunkZ); + ChunkPlanes existing = current.get(key); + ChunkPlanes planes = existing != null ? existing : current.putIfAbsent(key, new ChunkPlanes(chunkX, chunkZ)); + Recent last = recent.get(); + last.key = key; + last.planes = planes; + last.entries = current; + return planes; + } + + public static void enterSparse() { + SPARSE.get()[0]++; + } + + public static void exitSparse() { + 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 Recent { + private long key; + private ChunkPlanes planes; + private ConcurrentClockCache entries; + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/bake/ChunkPlanes.java b/core/src/main/java/art/arcane/iris/generation/bake/ChunkPlanes.java new file mode 100644 index 000000000..6a72dd756 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/bake/ChunkPlanes.java @@ -0,0 +1,208 @@ +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 planes = new AtomicReferenceArray<>(BakePlane.PLANES.length); + private volatile ColumnBake columnBake; + + ChunkPlanes(int chunkX, int chunkZ) { + this.chunkX = chunkX; + this.chunkZ = chunkZ; + } + + public int chunkX() { + return chunkX; + } + + public int chunkZ() { + return chunkZ; + } + + public Plane planeIfPresent(BakePlane kind) { + return planes.getAcquire(kind.ordinal()); + } + + 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) { + if (plane.complete) { + return plane.values; + } + Pending running = plane.filling.get(); + if (running != null) { + if (running.owner == Thread.currentThread()) { + return plane.unpublishedFill(source, chunkX, chunkZ); + } + running.await(); + continue; + } + Pending pending = new Pending(Thread.currentThread()); + if (!plane.filling.compareAndSet(null, pending)) { + continue; + } + try { + return plane.fill(source, chunkX, chunkZ); + } finally { + plane.filling.set(null); + pending.complete(); + } + } + } + + ColumnBake columnBake() { + return columnBake; + } + + ColumnBake columnBake(PlaneSource source) { + ColumnBake bake = columnBake; + if (bake != null) { + return bake; + } + long revision = ProvisionalSampling.revision(); + bake = source.assemble(this); + if (revision == ProvisionalSampling.revision()) { + columnBake = bake; + } + return bake; + } + + public static final class Plane { + private final BakePlane kind; + private final Object values; + private final long[] ready = new long[4]; + private final AtomicReference 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; + + private Pending(Thread owner) { + this.owner = owner; + } + + private synchronized void complete() { + done = true; + notifyAll(); + } + + private synchronized void await() { + boolean interrupted = false; + while (!done) { + try { + wait(); + } catch (InterruptedException e) { + interrupted = true; + } + } + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/bake/ColumnBake.java b/core/src/main/java/art/arcane/iris/generation/bake/ColumnBake.java new file mode 100644 index 000000000..749263925 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/bake/ColumnBake.java @@ -0,0 +1,55 @@ +package art.arcane.iris.generation.bake; + +import art.arcane.iris.generation.terrain.Terrain3DColumn; + +public final class ColumnBake { + public static final int APRON_WIDTH = 18; + + public final int chunkX; + public final int chunkZ; + public final short[] regionId; + public final short[] baseBiomeId; + public final short[] trueBiomeId; + public final short[] caveBiomeId; + public final double[] baseHeight; + public final double[] naturalHeight; + public final double[] height; + public final int[] roundedHeight; + public final float[] fluidSurface; + public final short[] rockState; + public final short[] fluidState; + public final Terrain3DColumn[] terrainColumns; + public final double[] apronHeight; + + public ColumnBake(Planes planes) { + this.chunkX = planes.chunkX(); + this.chunkZ = planes.chunkZ(); + this.regionId = planes.regionId(); + this.baseBiomeId = planes.baseBiomeId(); + this.trueBiomeId = planes.trueBiomeId(); + this.caveBiomeId = planes.caveBiomeId(); + this.baseHeight = planes.baseHeight(); + this.naturalHeight = planes.naturalHeight(); + this.height = planes.height(); + this.roundedHeight = planes.roundedHeight(); + this.fluidSurface = planes.fluidSurface(); + this.rockState = planes.rockState(); + this.fluidState = planes.fluidState(); + this.terrainColumns = planes.terrainColumns(); + this.apronHeight = planes.apronHeight(); + } + + public static int index(int localX, int localZ) { + return (localZ << 4) | localX; + } + + public static int apronIndex(int localX, int localZ) { + return (localZ + 1) * APRON_WIDTH + localX + 1; + } + + public record Planes(int chunkX, int chunkZ, short[] regionId, short[] baseBiomeId, short[] trueBiomeId, + short[] caveBiomeId, double[] baseHeight, double[] naturalHeight, double[] height, + int[] roundedHeight, float[] fluidSurface, short[] rockState, short[] fluidState, + Terrain3DColumn[] terrainColumns, double[] apronHeight) { + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/bake/GeneratorEvaluations.java b/core/src/main/java/art/arcane/iris/generation/bake/GeneratorEvaluations.java new file mode 100644 index 000000000..7466bed71 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/bake/GeneratorEvaluations.java @@ -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); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/bake/PlaneSource.java b/core/src/main/java/art/arcane/iris/generation/bake/PlaneSource.java new file mode 100644 index 000000000..5e98ffd08 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/bake/PlaneSource.java @@ -0,0 +1,9 @@ +package art.arcane.iris.generation.bake; + +public interface PlaneSource { + void fill(BakePlane plane, int chunkX, int chunkZ, long[] missing, Object values); + + ColumnBake assemble(ChunkPlanes planes); + + boolean mainThread(); +} diff --git a/core/src/main/java/art/arcane/iris/generation/bake/TerrainProgram.java b/core/src/main/java/art/arcane/iris/generation/bake/TerrainProgram.java new file mode 100644 index 000000000..a67f63719 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/bake/TerrainProgram.java @@ -0,0 +1,161 @@ +package art.arcane.iris.generation.bake; + +import art.arcane.iris.generation.noise.GeneratorProgram; +import art.arcane.iris.generation.noise.IrisGenerator; +import art.arcane.iris.generation.noise.TerrainBatchScratch; +import art.arcane.volmlib.util.math.M; + +import java.util.Arrays; + +public final class TerrainProgram { + private static final int SKIPPED = 0; + private static final int CONSTANT = 1; + private static final int ACTIVE = 2; + + private final IrisGenerator[][] generators; + private final GeneratorProgram[][] programs; + private final long heightSeed; + + private TerrainProgram(IrisGenerator[][] generators, GeneratorProgram[][] programs, long heightSeed) { + this.generators = generators; + this.programs = programs; + this.heightSeed = heightSeed; + } + + public static TerrainProgram compile(IrisGenerator[][] generators, long heightSeed) { + GeneratorProgram[][] programs = new GeneratorProgram[generators.length][]; + for (int group = 0; group < generators.length; group++) { + programs[group] = new GeneratorProgram[generators[group].length]; + for (int index = 0; index < generators[group].length; index++) { + IrisGenerator generator = generators[group][index]; + programs[group][index] = generator.supportsBatch() ? generator.compileProgram(heightSeed) : null; + } + } + return new TerrainProgram(generators, programs, heightSeed); + } + + public long heightSeed() { + return heightSeed; + } + + public double average(int group, TerrainBatchScratch scratch, double low, double high, double x, double z) { + IrisGenerator[] members = generators[group]; + if (members.length == 0) { + return 0D; + } + if (!Double.isFinite(low) || !Double.isFinite(high)) { + throw new IllegalStateException("Terrain generator bounds were not finite at " + x + "," + z + + " (generator=" + members[0].getLoadKey() + ", bounds=" + low + ".." + high + ")."); + } + double height = 0D; + if (low == high) { + for (int member = 0; member < members.length; member++) { + height += low; + } + return height / members.length; + } + GeneratorEvaluations.scalar(members.length); + GeneratorProgram[] compiled = programs[group]; + for (int member = 0; member < members.length; member++) { + GeneratorProgram program = compiled[member]; + double noise = program != null ? program.height(scratch, x, z) : members[member].getHeight(x, z, heightSeed); + if (!Double.isFinite(noise)) { + throw new IllegalStateException("Terrain generator noise was not finite at " + x + "," + z + + " (generator=" + members[member].getLoadKey() + ", noise=" + noise + + ", bounds=" + low + ".." + high + ")."); + } + height += M.lerp(low, high, noise); + } + return height / members.length; + } + + public int groups() { + return generators.length; + } + + public int groupSize(int group) { + return generators[group].length; + } + + 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 0L; + } + int mark = scratch.mark(); + int indexMark = scratch.indexMark(); + int[] modes = scratch.indices(count); + int activeCount = 0; + for (int index = 0; index < count; index++) { + double low = lows[index]; + double high = highs[index]; + if (low == 0D && high == 0D) { + modes[index] = SKIPPED; + continue; + } + if (!Double.isFinite(low) || !Double.isFinite(high)) { + throw new IllegalStateException("Terrain generator bounds were not finite at " + xs[index] + "," + zs[index] + + " (generator=" + members[0].getLoadKey() + ", bounds=" + low + ".." + high + ")."); + } + if (low == high) { + modes[index] = CONSTANT; + double sum = 0D; + for (int member = 0; member < members.length; member++) { + sum += low; + } + heights[index] += sum / members.length; + continue; + } + modes[index] = ACTIVE; + activeCount++; + } + if (activeCount == 0) { + scratch.resetIndices(indexMark); + return 0L; + } + double[] activeX = scratch.buffer(activeCount); + double[] activeZ = scratch.buffer(activeCount); + double[] activeLow = scratch.buffer(activeCount); + double[] activeHigh = scratch.buffer(activeCount); + int[] activeIndex = scratch.indices(activeCount); + int cursor = 0; + for (int index = 0; index < count; index++) { + if (modes[index] == ACTIVE) { + activeX[cursor] = xs[index]; + activeZ[cursor] = zs[index]; + activeLow[cursor] = lows[index]; + activeHigh[cursor] = highs[index]; + activeIndex[cursor++] = index; + } + } + double[] sums = scratch.buffer(activeCount); + double[] noise = scratch.buffer(activeCount); + Arrays.fill(sums, 0, activeCount, 0D); + GeneratorProgram[] compiled = programs[group]; + for (int member = 0; member < members.length; member++) { + GeneratorProgram program = compiled[member]; + if (program != null) { + program.heights(scratch, activeX, activeZ, activeCount, noise); + } else { + for (int index = 0; index < activeCount; index++) { + noise[index] = members[member].getHeight(activeX[index], activeZ[index], heightSeed); + } + } + for (int index = 0; index < activeCount; index++) { + if (!Double.isFinite(noise[index])) { + throw new IllegalStateException("Terrain generator noise was not finite at " + activeX[index] + "," + activeZ[index] + + " (generator=" + members[member].getLoadKey() + ", noise=" + noise[index] + + ", bounds=" + activeLow[index] + ".." + activeHigh[index] + ")."); + } + sums[index] += M.lerp(activeLow[index], activeHigh[index], noise[index]); + } + } + for (int index = 0; index < activeCount; index++) { + heights[activeIndex[index]] += sums[index] / members.length; + } + scratch.reset(mark); + scratch.resetIndices(indexMark); + return (long) activeCount * members.length; + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/biome/IrisBiome.java b/core/src/main/java/art/arcane/iris/generation/biome/IrisBiome.java index e70c2ef91..b1e0cd05e 100644 --- a/core/src/main/java/art/arcane/iris/generation/biome/IrisBiome.java +++ b/core/src/main/java/art/arcane/iris/generation/biome/IrisBiome.java @@ -515,7 +515,10 @@ public class IrisBiome extends IrisRegistrant implements Rarity { } public CNG getChildrenGenerator(RNG random, int sig, double scale) { - long seed = deriveChildStyleSeed(random, sig).getSeed(); + return getChildrenGenerator(deriveChildStyleSeed(random, sig).getSeed(), scale); + } + + public CNG getChildrenGenerator(long seed, double scale) { IrisData data = getLoader(); Engine engine = data == null ? null : data.getEngine(); CachedChildGenerator recent = recentChildGenerator; @@ -533,6 +536,10 @@ public class IrisBiome extends IrisRegistrant implements Rarity { return random.nextParallelRNG(signature * CHILDREN_SEED_SALT); } + public static long childStyleSeed(RNG random, int signature) { + return deriveChildStyleSeed(random, signature).getSeed(); + } + public KList generateLayers(IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex) { return IrisBiomeLayerGenerator.generateLayers(this, dim, wx, wz, random, maxDepth, height, rdata, complex); } diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/CaveObjectPlacementTransaction.java b/core/src/main/java/art/arcane/iris/generation/mantle/CaveObjectPlacementTransaction.java index b53862f49..e8a47f754 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/CaveObjectPlacementTransaction.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/CaveObjectPlacementTransaction.java @@ -30,27 +30,41 @@ import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell; import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.util.math.BlockPosition; import art.arcane.iris.generation.block.B; -import art.arcane.volmlib.util.collection.KList; import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import org.jetbrains.annotations.Nullable; -import java.util.HashMap; -import java.util.Map; +import java.util.Arrays; final class CaveObjectPlacementTransaction implements IObjectPlacer { private static final int CACHE_MISS = Integer.MIN_VALUE; + private static final int POSITION_BITS = 21; + private static final int POSITION_LIMIT = 1 << (POSITION_BITS - 1); + private static final long POSITION_MASK = (1L << POSITION_BITS) - 1L; private final IObjectPlacer delegate; private final Engine engine; private final int anchorY; private final int minDepthBelowSurface; private final int worldHeight; - private final KList mutations; - private final Map bufferedBlocks; + private static final byte BLOCK = 0; + private static final byte TILE = 1; + private static final byte DATA = 2; + private int[] mutationX = new int[64]; + private int[] mutationY = new int[64]; + private int[] mutationZ = new int[64]; + private Object[] mutationValues = new Object[64]; + private byte[] mutationKinds = new byte[64]; + private boolean[] mutationCanopies = new boolean[64]; + private int mutationCount; + private final Long2ObjectOpenHashMap bufferedBlocks; private final Long2IntOpenHashMap surfaceHeights; private final Long2IntOpenHashMap caveCeilings; private int blockWrites; private BlockPosition canopyPosition; + private boolean originKnown; + private int originX; + private int originZ; CaveObjectPlacementTransaction(IObjectPlacer delegate, int anchorY, int minDepthBelowSurface) { this.delegate = delegate; @@ -58,8 +72,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer { this.anchorY = anchorY; this.minDepthBelowSurface = Math.max(0, minDepthBelowSurface); this.worldHeight = engine == null ? 0 : engine.getHeight(); - this.mutations = new KList<>(); - this.bufferedBlocks = new HashMap<>(); + this.bufferedBlocks = new Long2ObjectOpenHashMap<>(); this.surfaceHeights = new Long2IntOpenHashMap(); this.caveCeilings = new Long2IntOpenHashMap(); this.surfaceHeights.defaultReturnValue(CACHE_MISS); @@ -73,28 +86,41 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer { } IrisComplex complex = engine == null ? null : engine.getComplex(); - for (BufferedMutation mutation : mutations) { + boolean previousChecked = false; + int previousX = 0; + int previousY = 0; + int previousZ = 0; + for (int index = 0; index < mutationCount; index++) { + int x = mutationX[index]; + int y = mutationY[index]; + int z = mutationZ[index]; + if (previousChecked && x == previousX && y == previousY && z == previousZ) { + continue; + } if (complex != null - && !complex.allowsNewDiscreteContentAt(mutation.x(), mutation.z())) { + && !complex.allowsNewDiscreteContentAt(x, z)) { discard(); return CommitResult.REJECTED_TRANSITION; } - if (!isWithinBounds(mutation.x(), mutation.y(), mutation.z())) { + if (!isWithinBounds(x, y, z)) { discard(); return CommitResult.REJECTED_BOUNDS; } - SubterrainCell feature = engine.getSubterrainCell(mutation.x(), mutation.y(), mutation.z()); + SubterrainCell feature = engine.getSubterrainCell(x, y, z); if (SubterrainRasterizer.protectsPlacement(feature) - || feature != null && feature.solid() && delegate.isSolid(mutation.x(), mutation.y(), mutation.z())) { + || feature != null && feature.solid() && delegate.isSolid(x, y, z)) { discard(); return CommitResult.REJECTED_SUBTERRAIN; } - HydrologyCaveCell hydrology = delegate.getData( - mutation.x(), mutation.y(), mutation.z(), HydrologyCaveCell.class); + HydrologyCaveCell hydrology = delegate.getData(x, y, z, HydrologyCaveCell.class); if (hydrology != null && hydrology.protectsPlacement()) { discard(); return CommitResult.REJECTED_HYDROLOGY; } + previousChecked = true; + previousX = x; + previousY = y; + previousZ = z; } applyMutations(); @@ -106,18 +132,26 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer { boolean canopyOpen = false; boolean skipCanopy = false; try { - for (BufferedMutation mutation : mutations) { - if (mutation.value() instanceof NativeBlockState) { + for (int index = 0; index < mutationCount; index++) { + int x = mutationX[index]; + int y = mutationY[index]; + int z = mutationZ[index]; + Object value = mutationValues[index]; + boolean canopy = mutationCanopies[index]; + if (value instanceof NativeBlockState) { if (canopyOpen) { delegate.endTreeCanopy(); canopyOpen = false; } - skipCanopy = mutation.canopy() - && !delegate.beginTreeCanopy(new BlockPosition(mutation.x(), mutation.y(), mutation.z())); - canopyOpen = mutation.canopy() && !skipCanopy; + skipCanopy = canopy && !delegate.beginTreeCanopy(new BlockPosition(x, y, z)); + canopyOpen = canopy && !skipCanopy; } - if (!mutation.canopy() || !skipCanopy) { - mutation.apply(delegate); + if (!canopy || !skipCanopy) { + switch (mutationKinds[index]) { + case BLOCK -> delegate.set(x, y, z, (NativeBlockState) value); + case TILE -> delegate.setTile(x, y, z, (TileData) value); + default -> delegate.setData(x, y, z, value); + } } } } finally { @@ -128,11 +162,31 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer { } void discard() { - mutations.clear(); + Arrays.fill(mutationValues, 0, mutationCount, null); + mutationCount = 0; bufferedBlocks.clear(); blockWrites = 0; } + private void buffer(byte kind, int x, int y, int z, Object value) { + if (mutationCount == mutationValues.length) { + int capacity = mutationCount * 2; + mutationX = Arrays.copyOf(mutationX, capacity); + mutationY = Arrays.copyOf(mutationY, capacity); + mutationZ = Arrays.copyOf(mutationZ, capacity); + mutationValues = Arrays.copyOf(mutationValues, capacity); + mutationKinds = Arrays.copyOf(mutationKinds, capacity); + mutationCanopies = Arrays.copyOf(mutationCanopies, capacity); + } + mutationX[mutationCount] = x; + mutationY[mutationCount] = y; + mutationZ[mutationCount] = z; + mutationValues[mutationCount] = value; + mutationKinds[mutationCount] = kind; + mutationCanopies[mutationCount] = isCanopy(x, y, z); + mutationCount++; + } + int getCaveCeiling(int x, int z) { long key = Cache.key(x, z); int cached = caveCeilings.get(key); @@ -164,8 +218,8 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer { if (state == null) { return; } - mutations.add(new BlockMutation(x, y, z, state, isCanopy(x, y, z))); - bufferedBlocks.put(new PositionKey(x, y, z), state); + buffer(BLOCK, x, y, z, state); + bufferedBlocks.put(bufferKey(x, y, z), state); blockWrites++; } @@ -190,7 +244,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer { @Override public NativeBlockState get(int x, int y, int z) { - NativeBlockState buffered = bufferedBlocks.get(new PositionKey(x, y, z)); + NativeBlockState buffered = bufferedBlocks.isEmpty() ? null : bufferedBlocks.get(bufferKey(x, y, z)); return buffered == null ? delegate.get(x, y, z) : buffered; } @@ -211,7 +265,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer { @Override public boolean isSolid(int x, int y, int z) { - NativeBlockState buffered = bufferedBlocks.get(new PositionKey(x, y, z)); + NativeBlockState buffered = bufferedBlocks.isEmpty() ? null : bufferedBlocks.get(bufferKey(x, y, z)); return buffered == null ? delegate.isSolid(x, y, z) : B.isSolid(buffered); } @@ -242,7 +296,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer { @Override public void setTile(int x, int y, int z, TileData tile) { if (tile != null) { - mutations.add(new TileMutation(x, y, z, tile, isCanopy(x, y, z))); + buffer(TILE, x, y, z, tile); } } @@ -251,21 +305,20 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer { if (data == null) { return; } - mutations.add(new DataMutation(x, y, z, data, isCanopy(x, y, z))); + buffer(DATA, x, y, z, data); if (data instanceof NativeBlockState state) { - bufferedBlocks.put(new PositionKey(x, y, z), state); + bufferedBlocks.put(bufferKey(x, y, z), state); blockWrites++; } } @Override public @Nullable T getData(int x, int y, int z, Class type) { - for (int i = mutations.size() - 1; i >= 0; i--) { - BufferedMutation mutation = mutations.get(i); - if (mutation.x() != x || mutation.y() != y || mutation.z() != z) { + for (int index = mutationCount - 1; index >= 0; index--) { + if (mutationX[index] != x || mutationY[index] != y || mutationZ[index] != z) { continue; } - Object value = mutation.value(); + Object value = mutationValues[index]; if (type.isInstance(value)) { return type.cast(value); } @@ -278,6 +331,22 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer { return engine; } + private long bufferKey(int x, int y, int z) { + if (!originKnown) { + originX = x; + originZ = z; + originKnown = true; + } + int dx = x - originX; + int dz = z - originZ; + if (dx < -POSITION_LIMIT || dx >= POSITION_LIMIT || dz < -POSITION_LIMIT || dz >= POSITION_LIMIT + || y < -POSITION_LIMIT || y >= POSITION_LIMIT) { + throw new IllegalStateException("Cave object write at " + x + "," + y + "," + z + + " is too far from its first write at " + originX + "," + originZ); + } + return ((dx & POSITION_MASK) << (POSITION_BITS * 2)) | ((dz & POSITION_MASK) << POSITION_BITS) | (y & POSITION_MASK); + } + private boolean isWithinBounds(int x, int y, int z) { if (engine == null || y < 0 || y >= worldHeight) { return false; @@ -325,42 +394,4 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer { REJECTED_HYDROLOGY, REJECTED_SUBTERRAIN } - - private interface BufferedMutation { - int x(); - - int y(); - - int z(); - - Object value(); - - boolean canopy(); - - void apply(IObjectPlacer placer); - } - - private record BlockMutation(int x, int y, int z, NativeBlockState value, boolean canopy) implements BufferedMutation { - @Override - public void apply(IObjectPlacer placer) { - placer.set(x, y, z, value); - } - } - - private record TileMutation(int x, int y, int z, TileData value, boolean canopy) implements BufferedMutation { - @Override - public void apply(IObjectPlacer placer) { - placer.setTile(x, y, z, value); - } - } - - private record DataMutation(int x, int y, int z, Object value, boolean canopy) implements BufferedMutation { - @Override - public void apply(IObjectPlacer placer) { - placer.setData(x, y, z, value); - } - } - - private record PositionKey(int x, int y, int z) { - } } diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/MantleObjectComponent.java b/core/src/main/java/art/arcane/iris/generation/mantle/MantleObjectComponent.java index 913c726f8..3d452b5ab 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/MantleObjectComponent.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/MantleObjectComponent.java @@ -88,6 +88,8 @@ public class MantleObjectComponent extends IrisMantleComponent { private static final Map CAVE_REJECT_LOG_STATE = new ConcurrentHashMap<>(); private static final Set MISSING_LOAD_KEY_WARNED = ConcurrentHashMap.newKeySet(); private static final long SOURCE_PLAN_STATS_INTERVAL_MS = 30_000L; + private static final int MAXIMUM_TREE_MATERIALS = 8192; + private static final Map TREE_MATERIALS = new ConcurrentHashMap<>(); private final Object collisionRuleLock; private final ObjectSourcePlanCache sourcePlans; @@ -124,7 +126,14 @@ public class MantleObjectComponent extends IrisMantleComponent { } private static void writeTreeMaterial(IObjectPlacer placer, int x, int y, int z, NativeBlockState data) { - placer.setData(x, y, z, TreeBlockMaterial.of(data)); + TreeBlockMaterial material = TREE_MATERIALS.get(data); + if (material == null) { + material = TreeBlockMaterial.of(data); + if (TREE_MATERIALS.size() < MAXIMUM_TREE_MATERIALS) { + TREE_MATERIALS.putIfAbsent(data, material); + } + } + placer.setData(x, y, z, material); } @Override @@ -157,19 +166,23 @@ public class MantleObjectComponent extends IrisMantleComponent { ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, x, z); int radius = getRadius(); int sourceChunkRadius = sourceChunkRadius(radius); - replaySourceChunks( - x, - z, - radius, - (sourceX, sourceZ) -> { - ObjectSourcePlan plan = sourcePlans.acquire(sourceX, sourceZ, x, z, sourceChunkRadius, - () -> buildSourcePlan(writer, sourceX, sourceZ, context)); - if (plan != null) { - transaction.apply(plan); - plan.persistContinuations(writer, sourceX, sourceZ, x, z); - } - } - ); + int side = (sourceChunkRadius * 2) + 1; + ObjectSourcePlan[] plans = new ObjectSourcePlan[side * side]; + int[] slot = new int[1]; + replaySourceChunks(x, z, radius, (sourceX, sourceZ) -> plans[slot[0]++] = sourcePlans.tryAcquire( + sourceX, sourceZ, x, z, sourceChunkRadius, () -> buildSourcePlan(writer, sourceX, sourceZ, context))); + slot[0] = 0; + replaySourceChunks(x, z, radius, (sourceX, sourceZ) -> { + ObjectSourcePlan plan = plans[slot[0]++]; + if (plan == ObjectSourcePlanCache.BUSY) { + plan = sourcePlans.acquire(sourceX, sourceZ, x, z, sourceChunkRadius, + () -> buildSourcePlan(writer, sourceX, sourceZ, context)); + } + if (plan != null) { + transaction.apply(plan); + plan.persistContinuations(writer, sourceX, sourceZ, x, z); + } + }); transaction.commit(); logSourcePlanStats(); } diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/MantleWriter.java b/core/src/main/java/art/arcane/iris/generation/mantle/MantleWriter.java index bb94e272f..0a88389d7 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/MantleWriter.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/MantleWriter.java @@ -244,7 +244,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { synchronized (chunk) { Matter matter = chunk.getOrCreate(y >> 4); if ((t instanceof NativeBlockState || t instanceof MatterCavern) - && (hasProtectedHydrology(matter, x, y, z) || protectsSubterrainPlacement(x, y, z))) { + && hasProtectedHydrology(matter, x, y, z)) { return; } if (t instanceof NativeBlockState) { diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/MutationLog.java b/core/src/main/java/art/arcane/iris/generation/mantle/MutationLog.java new file mode 100644 index 000000000..2480cf184 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/mantle/MutationLog.java @@ -0,0 +1,89 @@ +package art.arcane.iris.generation.mantle; + +import art.arcane.volmlib.nativelib.terrain.NativeBlockState; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +final class MutationLog { + private static final byte CUSTOM = 1; + private static final byte CANOPY = 2; + + private int size; + private int[] x = new int[32]; + private int[] y = new int[32]; + private int[] z = new int[32]; + private Object[] values = new Object[32]; + private byte[] flags = new byte[32]; + + static MutationLog of(List mutations) { + MutationLog log = new MutationLog(); + for (ObjectDestinationTransaction.Mutation mutation : mutations) { + Objects.requireNonNull(mutation, "mutation"); + ObjectDestinationTransaction.DataKey key = mutation.key(); + switch (mutation) { + case ObjectDestinationTransaction.SetMutation set -> { + Object value = Objects.requireNonNull(set.value(), "mutation value"); + if (ObjectSourcePlan.typeOf(value) != key.type()) { + throw new IllegalArgumentException("Object mutation type " + key.type().getName() + + " does not match its value " + value.getClass().getName()); + } + log.add(key.x(), key.y(), key.z(), value, false, set.canopy()); + } + case ObjectDestinationTransaction.CustomBlockMutation block -> log.add(key.x(), key.y(), key.z(), + Objects.requireNonNull(block.state(), "custom block state"), true, block.canopy()); + } + } + return log; + } + + void add(int blockX, int blockY, int blockZ, Object value, boolean custom, boolean canopy) { + if (size == values.length) { + int capacity = size * 2; + x = Arrays.copyOf(x, capacity); + y = Arrays.copyOf(y, capacity); + z = Arrays.copyOf(z, capacity); + values = Arrays.copyOf(values, capacity); + flags = Arrays.copyOf(flags, capacity); + } + x[size] = blockX; + y[size] = blockY; + z[size] = blockZ; + values[size] = value; + flags[size] = (byte) ((custom ? CUSTOM : 0) | (canopy ? CANOPY : 0)); + size++; + } + + int size() { + return size; + } + + int x(int index) { + return x[index]; + } + + int y(int index) { + return y[index]; + } + + int z(int index) { + return z[index]; + } + + Object value(int index) { + return values[index]; + } + + boolean custom(int index) { + return (flags[index] & CUSTOM) != 0; + } + + boolean canopy(int index) { + return (flags[index] & CANOPY) != 0; + } + + Class type(int index) { + return custom(index) ? NativeBlockState.class : ObjectSourcePlan.typeOf(values[index]); + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/ObjectDestinationTransaction.java b/core/src/main/java/art/arcane/iris/generation/mantle/ObjectDestinationTransaction.java index f1012ec84..546c14200 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/ObjectDestinationTransaction.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/ObjectDestinationTransaction.java @@ -8,7 +8,6 @@ import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial; import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell; import art.arcane.iris.generation.subterrain.SubterrainRasterizer; import art.arcane.iris.generation.block.TileData; -import art.arcane.iris.structure.object.IObjectPlacer; import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.util.math.BlockPosition; import art.arcane.iris.generation.block.B; @@ -18,10 +17,11 @@ import org.jetbrains.annotations.Nullable; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; import java.util.ArrayList; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -32,7 +32,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer { private final int destinationChunkX; private final int destinationChunkZ; private final int worldHeight; - private final List mutations; + private final MutationLog log = new MutationLog(); private final List placements = new ArrayList<>(); private final Long2ObjectOpenHashMap> overlay; private BlockPosition canopyPosition; @@ -40,13 +40,17 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer { private int hydrologyGateChunkX; private int hydrologyGateChunkZ; private boolean hydrologyGateOpen; + private boolean subterrainKnown; + private int subterrainX; + private int subterrainY; + private int subterrainZ; + private boolean subterrainProtects; ObjectDestinationTransaction(MantleWriter writer, int destinationChunkX, int destinationChunkZ) { this.writer = writer; this.destinationChunkX = destinationChunkX; this.destinationChunkZ = destinationChunkZ; this.worldHeight = writer.getMantle().getWorldHeight(); - this.mutations = new ArrayList<>(); this.overlay = new Long2ObjectOpenHashMap<>(); } @@ -55,11 +59,11 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer { } int mutationCheckpoint() { - return mutations.size(); + return log.size(); } ObjectSourcePlan sourcePlanSince(int checkpoint) { - if (checkpoint < 0 || checkpoint > mutations.size()) { + if (checkpoint < 0 || checkpoint > log.size()) { throw new IllegalArgumentException("Mutation checkpoint is outside the transaction"); } ArrayList accepted = new ArrayList<>(); @@ -68,65 +72,91 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer { accepted.add(new PlacementRange(placement.start() - checkpoint, placement.end() - checkpoint)); } } - return new ObjectSourcePlan(mutations.subList(checkpoint, mutations.size()), accepted); + return new ObjectSourcePlan(log, checkpoint, accepted); } @Override public int beginObjectPlacement() { - return mutations.size(); + return log.size(); } @Override public void endObjectPlacement(int checkpoint) { - if (checkpoint >= 0 && checkpoint < mutations.size()) { - placements.add(new PlacementRange(checkpoint, mutations.size())); + if (checkpoint >= 0 && checkpoint < log.size()) { + placements.add(new PlacementRange(checkpoint, log.size())); } } void apply(ObjectSourcePlan plan) { + int slot = plan.destinationSlot(destinationChunkX, destinationChunkZ); + if (slot < 0) { + return; + } + int baseX = destinationChunkX << 4; + int baseZ = destinationChunkZ << 4; boolean skipCanopy = false; - for (Mutation mutation : plan.mutationsFor(destinationChunkX, destinationChunkZ)) { - if (mutation.key().type() == NativeBlockState.class) { - NativeBlockState existing = mutation.canopy() ? get(mutation.key().x(), mutation.key().y(), mutation.key().z()) : null; + int end = plan.slotEnd(slot); + for (int index = plan.slotStart(slot); index < end; index++) { + int cell = plan.cellAt(index); + int x = baseX | ObjectSourcePlan.cellX(cell); + int y = ObjectSourcePlan.cellY(cell); + int z = baseZ | ObjectSourcePlan.cellZ(cell); + Object value = plan.valueAt(index); + boolean custom = ObjectSourcePlan.isCustom(cell); + boolean canopy = ObjectSourcePlan.isCanopy(cell); + if (custom || value instanceof NativeBlockState) { + NativeBlockState existing = canopy ? get(x, y, z) : null; skipCanopy = existing != null && existing.isSolid(); } - if (mutation.canopy() && skipCanopy) { + if (canopy && skipCanopy) { continue; } - switch (mutation) { - case SetMutation set -> setData(set.key().x(), set.key().y(), set.key().z(), set.value(), set); - case CustomBlockMutation custom -> set(custom.key().x(), custom.key().y(), custom.key().z(), custom.state(), custom); + if (custom) { + set(x, y, z, (NativeBlockState) value, true, canopy); + } else { + setData(x, y, z, value, true, canopy); } } } private void commitFenced() { - LinkedHashMap originals = new LinkedHashMap<>(); + CommitJournal journal = new CommitJournal(); try { - for (Mutation mutation : mutations) { - if (!isDestination(mutation.x(), mutation.z())) { + for (int position = 0; position < log.size(); position++) { + int x = log.x(position); + int z = log.z(position); + if (!isDestination(x, z)) { continue; } - captureOriginal(originals, mutation.key()); - if (mutation.key().type() == NativeBlockState.class) { - TileWrapper tile = writer.getDataIfPresent( - mutation.key().x(), mutation.key().y(), mutation.key().z(), TileWrapper.class); - if (tile != null) { - originals.putIfAbsent(new DataKey( - mutation.key().x(), mutation.key().y(), mutation.key().z(), TileWrapper.class), tile); + int y = log.y(position); + Object value = log.value(position); + boolean custom = log.custom(position); + Class type = log.type(position); + if (!journal.captured(x, y, z, type)) { + journal.capture(x, y, z, type, publishedOrCleared(x, y, z, type)); + } + if (type == NativeBlockState.class) { + if (!journal.captured(x, y, z, TileWrapper.class)) { + TileWrapper tile = writer.getDataIfPresent(x, y, z, TileWrapper.class); + if (tile != null) { + journal.capture(x, y, z, TileWrapper.class, tile); + } } - Identifier identifier = writer.getDataIfPresent( - mutation.key().x(), mutation.key().y(), mutation.key().z(), Identifier.class); - if (identifier != null || mutation instanceof CustomBlockMutation) { - originals.putIfAbsent(new DataKey( - mutation.key().x(), mutation.key().y(), mutation.key().z(), Identifier.class), - identifier == null ? CLEARED : identifier); + if (!journal.captured(x, y, z, Identifier.class)) { + Identifier identifier = writer.getDataIfPresent(x, y, z, Identifier.class); + if (identifier != null || custom) { + journal.capture(x, y, z, Identifier.class, identifier == null ? CLEARED : identifier); + } } } - mutation.apply(writer); + if (custom) { + writer.set(x, y, z, (NativeBlockState) value); + } else { + writer.setData(x, y, z, value); + } } } catch (Throwable failure) { - rollback(originals, failure); + journal.rollback(failure); if (failure instanceof RuntimeException runtimeException) { throw runtimeException; } @@ -149,10 +179,10 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer { @Override public void set(int x, int y, int z, NativeBlockState state) { - set(x, y, z, state, null); + set(x, y, z, state, false, false); } - private void set(int x, int y, int z, NativeBlockState state, CustomBlockMutation replayed) { + private void set(int x, int y, int z, NativeBlockState state, boolean replayed, boolean replayedCanopy) { if (state == null) { return; } @@ -167,8 +197,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer { cell.block = baseState; cell.tile = CLEARED; cell.identifier = identifier; - mutations.add(replayed != null - ? replayed : new CustomBlockMutation(new DataKey(x, y, z, NativeBlockState.class), state, isCanopy(x, y, z))); + log.add(x, y, z, state, true, replayed ? replayedCanopy : isCanopy(x, y, z)); return; } setData(x, y, z, state); @@ -262,10 +291,10 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer { @Override public void setData(int x, int y, int z, T data) { - setData(x, y, z, data, null); + setData(x, y, z, data, false, false); } - private void setData(int x, int y, int z, Object data, SetMutation replayed) { + private void setData(int x, int y, int z, Object data, boolean replayed, boolean replayedCanopy) { if (data == null || y < 0 || y >= worldHeight || protectsSubterrain(x, y, z)) { return; @@ -285,8 +314,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer { } else { cell.put(type, data); } - mutations.add(replayed != null - ? replayed : new SetMutation(new DataKey(x, y, z, type), data, isCanopy(x, y, z))); + log.add(x, y, z, data, false, replayed ? replayedCanopy : isCanopy(x, y, z)); } @Override @@ -368,18 +396,6 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer { return value == null ? CLEARED : value; } - private void captureOriginal(LinkedHashMap originals, DataKey key) { - originals.computeIfAbsent( - key, - candidate -> publishedOrCleared( - candidate.x(), - candidate.y(), - candidate.z(), - candidate.type() - ) - ); - } - private boolean isDestination(int x, int z) { return (x >> 4) == destinationChunkX && (z >> 4) == destinationChunkZ; } @@ -396,8 +412,17 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer { } private boolean protectsSubterrain(int x, int y, int z) { + if (subterrainKnown && x == subterrainX && y == subterrainY && z == subterrainZ) { + return subterrainProtects; + } Engine engine = getEngine(); - return engine != null && SubterrainRasterizer.protectsPlacement(engine.getSubterrainCell(x, y, z)); + boolean protects = engine != null && SubterrainRasterizer.protectsPlacement(engine.getSubterrainCell(x, y, z)); + subterrainX = x; + subterrainY = y; + subterrainZ = z; + subterrainProtects = protects; + subterrainKnown = true; + return protects; } private boolean hasProtectedHydrology(int x, int y, int z) { @@ -419,22 +444,44 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer { return hydrologyGateOpen; } - private void rollback(LinkedHashMap originals, Throwable failure) { - ArrayList> entries = new ArrayList<>(originals.entrySet()); - for (int index = entries.size() - 1; index >= 0; index--) { - Map.Entry entry = entries.get(index); - DataKey key = entry.getKey(); - try { - writer.restoreData(key.x(), key.y(), key.z(), key.type(), - entry.getValue() == CLEARED ? null : entry.getValue()); - } catch (Throwable rollbackFailure) { - if (rollbackFailure != failure) { - failure.addSuppressed(rollbackFailure); + private final class CommitJournal { + private final Reference2IntOpenHashMap> typeIds = new Reference2IntOpenHashMap<>(); + private final LongOpenHashSet keys = new LongOpenHashSet(); + private final ArrayList originals = new ArrayList<>(); + + private long key(int x, int y, int z, Class type) { + int typeId = typeIds.computeIfAbsent(type, (Class ignored) -> typeIds.size()); + return ((long) typeId << 32) | ((long) y << 8) | ((long) (z & 15) << 4) | (x & 15); + } + + private boolean captured(int x, int y, int z, Class type) { + return keys.contains(key(x, y, z, type)); + } + + private void capture(int x, int y, int z, Class type, Object value) { + if (keys.add(key(x, y, z, type))) { + originals.add(new Original(x, y, z, type, value)); + } + } + + private void rollback(Throwable failure) { + for (int index = originals.size() - 1; index >= 0; index--) { + Original original = originals.get(index); + try { + writer.restoreData(original.x(), original.y(), original.z(), original.type(), + original.value() == CLEARED ? null : original.value()); + } catch (Throwable rollbackFailure) { + if (rollbackFailure != failure) { + failure.addSuppressed(rollbackFailure); + } } } } } + private record Original(int x, int y, int z, Class type, Object value) { + } + private static final class OverlayCell { private Object block; private Object hydrology; @@ -506,8 +553,6 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer { DataKey key(); boolean canopy(); - - void apply(IObjectPlacer placer); } record SetMutation(DataKey key, Object value, boolean canopy) implements Mutation { @@ -520,11 +565,6 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer { public int z() { return key.z(); } - - @Override - public void apply(IObjectPlacer placer) { - placer.setData(key.x(), key.y(), key.z(), value); - } } record CustomBlockMutation(DataKey key, NativeBlockState state, boolean canopy) implements Mutation { @@ -537,10 +577,5 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer { public int z() { return key.z(); } - - @Override - public void apply(IObjectPlacer placer) { - placer.set(key.x(), key.y(), key.z(), state); - } } } diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/ObjectSourcePlan.java b/core/src/main/java/art/arcane/iris/generation/mantle/ObjectSourcePlan.java index 236c1b90c..ab7e12def 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/ObjectSourcePlan.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/ObjectSourcePlan.java @@ -20,7 +20,6 @@ import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Objects; final class ObjectSourcePlan { static final ObjectSourcePlan EMPTY = new ObjectSourcePlan(List.of()); @@ -45,18 +44,21 @@ final class ObjectSourcePlan { private final int estimatedRetainedBytes; ObjectSourcePlan(List mutations) { - this(mutations, List.of()); + this(MutationLog.of(mutations), 0, List.of()); } ObjectSourcePlan(List mutations, List accepted) { - int count = mutations.size(); + this(MutationLog.of(mutations), 0, accepted); + } + + ObjectSourcePlan(MutationLog log, int from, List accepted) { + int count = log.size() - from; long[] mutationDestinations = new long[count]; Long2IntOpenHashMap destinationSlots = new Long2IntOpenHashMap(); destinationSlots.defaultReturnValue(-1); LongArrayList firstSeen = new LongArrayList(); for (int index = 0; index < count; index++) { - ObjectDestinationTransaction.Mutation mutation = Objects.requireNonNull(mutations.get(index), "mutation"); - long destination = destinationKey(mutation.x() >> 4, mutation.z() >> 4); + long destination = destinationKey(log.x(from + index) >> 4, log.z(from + index) >> 4); mutationDestinations[index] = destination; if (destinationSlots.putIfAbsent(destination, firstSeen.size()) == -1) { firstSeen.add(destination); @@ -80,38 +82,23 @@ final class ObjectSourcePlan { valueIndexes = new int[count]; ValueTable table = new ValueTable(); for (int index = 0; index < count; index++) { - ObjectDestinationTransaction.Mutation mutation = mutations.get(index); + int logged = from + index; int position = cursor[destinationSlots.get(mutationDestinations[index])]++; positions[index] = position; - ObjectDestinationTransaction.DataKey key = mutation.key(); - if (key.y() < 0 || key.y() > MAXIMUM_Y) { - throw new IllegalArgumentException("Object mutation height is outside the plan range: " + key.y()); + int y = log.y(logged); + if (y < 0 || y > MAXIMUM_Y) { + throw new IllegalArgumentException("Object mutation height is outside the plan range: " + y); } - Object value; - int custom; - switch (mutation) { - case ObjectDestinationTransaction.SetMutation set -> { - value = Objects.requireNonNull(set.value(), "mutation value"); - if (typeOf(value) != key.type()) { - throw new IllegalArgumentException("Object mutation type " + key.type().getName() - + " does not match its value " + value.getClass().getName()); - } - custom = 0; - } - case ObjectDestinationTransaction.CustomBlockMutation block -> { - value = Objects.requireNonNull(block.state(), "custom block state"); - custom = CUSTOM_BIT; - } - } - cells[position] = (key.y() << Y_SHIFT) | custom | (mutation.canopy() ? CANOPY_BIT : 0) | ((key.z() & 15) << 4) | (key.x() & 15); - valueIndexes[position] = table.indexOf(value); + cells[position] = (y << Y_SHIFT) | (log.custom(logged) ? CUSTOM_BIT : 0) | (log.canopy(logged) ? CANOPY_BIT : 0) + | ((log.z(logged) & 15) << 4) | (log.x(logged) & 15); + valueIndexes[position] = table.indexOf(log.value(logged)); } values = table.values.toArray(); ArrayList crossing = new ArrayList<>(); for (int ordinal = 0; ordinal < accepted.size(); ordinal++) { ObjectDestinationTransaction.PlacementRange range = accepted.get(ordinal); - Placement placement = crossingPlacement(ordinal, range.start(), range.end(), mutations, mutationDestinations, destinationSlots, positions); + Placement placement = crossingPlacement(ordinal, range.start(), range.end(), log, from, mutationDestinations, destinationSlots, positions); if (placement != null) { crossing.add(placement); } @@ -147,12 +134,13 @@ final class ObjectSourcePlan { int ordinal, int start, int end, - List mutations, + MutationLog log, + int from, long[] mutationDestinations, Long2IntOpenHashMap destinationSlots, int[] positions ) { - if (start < 0 || end > mutations.size() || start >= end) { + if (start < 0 || end > log.size() - from || start >= end) { throw new IllegalArgumentException("Object placement range is outside the plan"); } long first = mutationDestinations[start]; @@ -172,11 +160,12 @@ final class ObjectSourcePlan { int maximumX = Integer.MIN_VALUE; int maximumZ = Integer.MIN_VALUE; for (int mutationIndex = start; mutationIndex < end; mutationIndex++) { - ObjectDestinationTransaction.Mutation mutation = mutations.get(mutationIndex); - minimumX = Math.min(minimumX, mutation.x()); - minimumZ = Math.min(minimumZ, mutation.z()); - maximumX = Math.max(maximumX, mutation.x()); - maximumZ = Math.max(maximumZ, mutation.z()); + int mutationX = log.x(from + mutationIndex); + int mutationZ = log.z(from + mutationIndex); + minimumX = Math.min(minimumX, mutationX); + minimumZ = Math.min(minimumZ, mutationZ); + maximumX = Math.max(maximumX, mutationX); + maximumZ = Math.max(maximumZ, mutationZ); long destination = mutationDestinations[mutationIndex]; int touchedIndex = touchedIndexes.get(destination); if (touchedIndex < 0) { @@ -216,6 +205,55 @@ final class ObjectSourcePlan { return destinations.length == 0; } + int slotStart(int slot) { + return offsets[slot]; + } + + int slotEnd(int slot) { + return offsets[slot + 1]; + } + + int cellAt(int index) { + return cells[index]; + } + + Object valueAt(int index) { + return values[valueIndexes[index]]; + } + + static int cellX(int cell) { + return cell & 15; + } + + static int cellZ(int cell) { + return (cell >>> 4) & 15; + } + + static int cellY(int cell) { + return cell >>> Y_SHIFT; + } + + static boolean isCustom(int cell) { + return (cell & CUSTOM_BIT) != 0; + } + + static boolean isCanopy(int cell) { + return (cell & CANOPY_BIT) != 0; + } + + ObjectDestinationTransaction.Mutation mutationAt(int chunkX, int chunkZ, int index) { + int cell = cells[index]; + int x = (chunkX << 4) | cellX(cell); + int z = (chunkZ << 4) | cellZ(cell); + int y = cellY(cell); + Object value = valueAt(index); + return isCustom(cell) + ? new ObjectDestinationTransaction.CustomBlockMutation( + new ObjectDestinationTransaction.DataKey(x, y, z, NativeBlockState.class), (NativeBlockState) value, isCanopy(cell)) + : new ObjectDestinationTransaction.SetMutation( + new ObjectDestinationTransaction.DataKey(x, y, z, typeOf(value)), value, isCanopy(cell)); + } + List mutationsFor(int chunkX, int chunkZ) { int slot = destinationSlot(chunkX, chunkZ); return slot < 0 ? List.of() : decode(chunkX, chunkZ, offsets[slot], offsets[slot + 1] - offsets[slot]); @@ -266,19 +304,8 @@ final class ObjectSourcePlan { private List decode(int chunkX, int chunkZ, int start, int length) { ObjectDestinationTransaction.Mutation[] decoded = new ObjectDestinationTransaction.Mutation[length]; - int baseX = chunkX << 4; - int baseZ = chunkZ << 4; for (int index = 0; index < length; index++) { - int cell = cells[start + index]; - int x = baseX | (cell & 15); - int z = baseZ | ((cell >>> 4) & 15); - int y = cell >>> Y_SHIFT; - Object value = values[valueIndexes[start + index]]; - decoded[index] = (cell & CUSTOM_BIT) != 0 - ? new ObjectDestinationTransaction.CustomBlockMutation( - new ObjectDestinationTransaction.DataKey(x, y, z, NativeBlockState.class), (NativeBlockState) value, (cell & CANOPY_BIT) != 0) - : new ObjectDestinationTransaction.SetMutation( - new ObjectDestinationTransaction.DataKey(x, y, z, typeOf(value)), value, (cell & CANOPY_BIT) != 0); + decoded[index] = mutationAt(chunkX, chunkZ, start + index); } return List.of(decoded); } diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/ObjectSourcePlanCache.java b/core/src/main/java/art/arcane/iris/generation/mantle/ObjectSourcePlanCache.java index 4b73e6dce..daa1a1580 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/ObjectSourcePlanCache.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/ObjectSourcePlanCache.java @@ -4,6 +4,7 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import art.arcane.volmlib.util.cache.CacheKey; +import java.util.List; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -14,6 +15,7 @@ import java.util.function.Supplier; import static art.arcane.iris.generation.cache.Cache.key; final class ObjectSourcePlanCache { + static final ObjectSourcePlan BUSY = new ObjectSourcePlan(List.of()); private static final long MAXIMUM_RETAINED_BYTES = 512L * 1024L * 1024L; private static final int ENTRY_BYTES = 64; @@ -68,6 +70,19 @@ final class ObjectSourcePlanCache { return plan; } + ObjectSourcePlan tryAcquire(int sourceChunkX, int sourceChunkZ, int destinationChunkX, int destinationChunkZ, + int sourceChunkRadius, Supplier builder) { + State current = state; + long source = CacheKey.mix(key(sourceChunkX, sourceChunkZ)); + if (current.plans.getIfPresent(source) == null) { + Pending pending = current.pending.get(source); + if (pending != null && pending.owner != Thread.currentThread()) { + return BUSY; + } + } + return acquire(sourceChunkX, sourceChunkZ, destinationChunkX, destinationChunkZ, sourceChunkRadius, builder); + } + private ObjectSourcePlan claim(State current, long source, Entry entry, int destinationChunkX, int destinationChunkZ, Supplier builder) { int slot = entry.slot(destinationChunkX, destinationChunkZ); diff --git a/core/src/main/java/art/arcane/iris/generation/noise/GeneratorProgram.java b/core/src/main/java/art/arcane/iris/generation/noise/GeneratorProgram.java new file mode 100644 index 000000000..360746480 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/noise/GeneratorProgram.java @@ -0,0 +1,289 @@ +package art.arcane.iris.generation.noise; + +import art.arcane.volmlib.util.interpolation.IrisInterpolation; + +import java.util.Arrays; + +public final class GeneratorProgram { + private static final int SURFACE_GRID = 6; + + private final IrisGenerator generator; + private final long superSeed; + private final NoiseLayerProgram[] layers; + private final double totalOpacity; + private final NoiseLayerProgram cliffs; + private final boolean multiplicitive; + private final double opacity; + private final double offsetX; + private final double offsetZ; + private final double zoom; + private final double surfaceDetail; + private final double cliffHeightMin; + private final double cliffHeightMax; + private final boolean cellCracks; + + GeneratorProgram(IrisGenerator generator, long superSeed, NoiseLayerProgram[] layers, double totalOpacity, NoiseLayerProgram cliffs) { + this.generator = generator; + this.superSeed = superSeed; + this.layers = layers; + this.totalOpacity = totalOpacity; + this.cliffs = cliffs; + this.multiplicitive = generator.isMultiplicitive(); + this.opacity = generator.getOpacity(); + this.offsetX = generator.getOffsetX(); + this.offsetZ = generator.getOffsetZ(); + this.zoom = generator.getZoom(); + this.surfaceDetail = generator.getSurfaceDetail(); + this.cliffHeightMin = generator.getCliffHeightMin(); + this.cliffHeightMax = generator.getCliffHeightMax(); + this.cellCracks = generator.getCellFractureHeight() != 0; + } + + public IrisGenerator generator() { + return generator; + } + + public void heights(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) { + if (count == 0) { + return; + } + if (layers.length == 0) { + Arrays.fill(out, 0, count, 0D); + return; + } + if (surfaceDetail == 1D) { + sample(scratch, xs, zs, count, out); + return; + } + surfaceHeights(scratch, xs, zs, count, out); + } + + public double height(TerrainBatchScratch scratch, double x, double z) { + if (layers.length == 0) { + return 0D; + } + if (surfaceDetail == 1D) { + return sample(scratch, x, z); + } + double lowerX = Math.floor(x / SURFACE_GRID) * SURFACE_GRID; + double lowerZ = Math.floor(z / SURFACE_GRID) * SURFACE_GRID; + double dx = (x - lowerX) / SURFACE_GRID; + double dz = (z - lowerZ) / SURFACE_GRID; + IrisGenerator.SurfaceCacheStripe[] stripes = generator.surfaceStripes(); + double northWest = lattice(scratch, stripes, lowerX, lowerZ); + if (dx == 0D && dz == 0D) { + return northWest; + } + double north = dx == 0D ? northWest + : IrisInterpolation.lerp(northWest, lattice(scratch, stripes, lowerX + SURFACE_GRID, lowerZ), dx); + double smooth = north; + if (dz != 0D) { + double southWest = lattice(scratch, stripes, lowerX, lowerZ + SURFACE_GRID); + double south = dx == 0D ? southWest + : IrisInterpolation.lerp(southWest, lattice(scratch, stripes, lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID), dx); + smooth = IrisInterpolation.lerp(north, south, dz); + } + return surfaceDetail == 0D ? smooth : smooth + (sample(scratch, x, z) - smooth) * surfaceDetail; + } + + private double lattice(TerrainBatchScratch scratch, IrisGenerator.SurfaceCacheStripe[] stripes, double x, double z) { + if (x != (int) x || z != (int) z) { + return sample(scratch, x, z); + } + int mark = scratch.mark(); + double[] cached = scratch.buffer(1); + boolean hit = IrisGenerator.cachedSurface(stripes, x, z, superSeed, cached, 0); + double value = cached[0]; + scratch.reset(mark); + if (hit) { + return value; + } + double height = sample(scratch, x, z); + IrisGenerator.storeSurface(stripes, x, z, superSeed, height); + return height; + } + + private double sample(TerrainBatchScratch scratch, double x, double z) { + double sampleX = (x + offsetX) / zoom; + double sampleZ = (z + offsetZ) / zoom; + double value = multiplicitive ? 1D : 0D; + for (NoiseLayerProgram layer : layers) { + double noise = layer.sample(scratch, sampleX, sampleZ); + value = multiplicitive ? value * noise : value + noise; + } + value = multiplicitive ? value * opacity : (value / totalOpacity) * opacity; + if (Double.isNaN(value)) { + value = 0D; + } + if (cliffs != null) { + double height = IrisInterpolation.lerp(cliffHeightMin, cliffHeightMax, cliffs.sample(scratch, sampleX, sampleZ)); + value = (Math.round((value * 255D) / height) * height) / 255D; + } + return cellCracks ? generator.cell(x, z, value, 0D) : value; + } + + private void surfaceHeights(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) { + int mark = scratch.mark(); + int indexMark = scratch.indexMark(); + int[] northWest = scratch.indices(count); + int[] northEast = scratch.indices(count); + int[] southWest = scratch.indices(count); + int[] southEast = scratch.indices(count); + double[] fractionX = scratch.buffer(count); + double[] fractionZ = scratch.buffer(count); + scratch.clearLattice(count * 4); + int detailed = 0; + double previousX = Double.NaN; + double previousZ = Double.NaN; + int cellNorthWest = -1; + int cellNorthEast = -1; + int cellSouthWest = -1; + int cellSouthEast = -1; + for (int index = 0; index < count; index++) { + double lowerX = Math.floor(xs[index] / SURFACE_GRID) * SURFACE_GRID; + double lowerZ = Math.floor(zs[index] / SURFACE_GRID) * SURFACE_GRID; + double dx = (xs[index] - lowerX) / SURFACE_GRID; + double dz = (zs[index] - lowerZ) / SURFACE_GRID; + fractionX[index] = dx; + fractionZ[index] = dz; + if (Double.doubleToRawLongBits(lowerX) != Double.doubleToRawLongBits(previousX) + || Double.doubleToRawLongBits(lowerZ) != Double.doubleToRawLongBits(previousZ)) { + previousX = lowerX; + previousZ = lowerZ; + cellNorthWest = scratch.lattice(lowerX, lowerZ); + cellNorthEast = -1; + cellSouthWest = -1; + cellSouthEast = -1; + } + if (dx != 0D && cellNorthEast < 0) { + cellNorthEast = scratch.lattice(lowerX + SURFACE_GRID, lowerZ); + } + if (dz != 0D && cellSouthWest < 0) { + cellSouthWest = scratch.lattice(lowerX, lowerZ + SURFACE_GRID); + } + if (dx != 0D && dz != 0D && cellSouthEast < 0) { + cellSouthEast = scratch.lattice(lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID); + } + northWest[index] = cellNorthWest; + northEast[index] = dx == 0D ? -1 : cellNorthEast; + southWest[index] = dz == 0D ? -1 : cellSouthWest; + southEast[index] = dx == 0D || dz == 0D ? -1 : cellSouthEast; + if (dx != 0D || dz != 0D) { + detailed++; + } + } + int nodes = scratch.latticeCount(); + double[] lattice = scratch.buffer(nodes); + resolveLattice(scratch, nodes, lattice); + double[] detail = null; + int[] detailIndex = null; + if (surfaceDetail != 0D && detailed > 0) { + double[] detailX = scratch.buffer(detailed); + double[] detailZ = scratch.buffer(detailed); + detail = scratch.buffer(detailed); + detailIndex = scratch.indices(count); + int cursor = 0; + for (int index = 0; index < count; index++) { + if (fractionX[index] != 0D || fractionZ[index] != 0D) { + detailX[cursor] = xs[index]; + detailZ[cursor] = zs[index]; + detailIndex[index] = cursor++; + } + } + sample(scratch, detailX, detailZ, detailed, detail); + } + for (int index = 0; index < count; index++) { + double dx = fractionX[index]; + double dz = fractionZ[index]; + double nw = lattice[northWest[index]]; + if (dx == 0D && dz == 0D) { + out[index] = nw; + continue; + } + double north = dx == 0D ? nw : IrisInterpolation.lerp(nw, lattice[northEast[index]], dx); + double smooth = north; + if (dz != 0D) { + double sw = lattice[southWest[index]]; + double south = dx == 0D ? sw : IrisInterpolation.lerp(sw, lattice[southEast[index]], dx); + smooth = IrisInterpolation.lerp(north, south, dz); + } + out[index] = surfaceDetail == 0D ? smooth : smooth + (detail[detailIndex[index]] - smooth) * surfaceDetail; + } + scratch.resetIndices(indexMark); + scratch.reset(mark); + } + + private void resolveLattice(TerrainBatchScratch scratch, int nodes, double[] lattice) { + IrisGenerator.SurfaceCacheStripe[] stripes = generator.surfaceStripes(); + int indexMark = scratch.indexMark(); + int mark = scratch.mark(); + int[] missing = scratch.indices(nodes); + double[] missingX = scratch.buffer(nodes); + double[] missingZ = scratch.buffer(nodes); + int missingCount = 0; + for (int node = 0; node < nodes; node++) { + double x = scratch.latticeX(node); + double z = scratch.latticeZ(node); + if (x == (int) x && z == (int) z && IrisGenerator.cachedSurface(stripes, x, z, superSeed, lattice, node)) { + continue; + } + missing[missingCount] = node; + missingX[missingCount] = x; + missingZ[missingCount++] = z; + } + if (missingCount > 0) { + double[] sampled = scratch.buffer(missingCount); + sample(scratch, missingX, missingZ, missingCount, sampled); + for (int index = 0; index < missingCount; index++) { + int node = missing[index]; + lattice[node] = sampled[index]; + if (missingX[index] == (int) missingX[index] && missingZ[index] == (int) missingZ[index]) { + IrisGenerator.storeSurface(stripes, missingX[index], missingZ[index], superSeed, sampled[index]); + } + } + } + scratch.reset(mark); + scratch.resetIndices(indexMark); + } + + private void sample(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) { + int mark = scratch.mark(); + double[] sampleX = scratch.buffer(count); + double[] sampleZ = scratch.buffer(count); + double[] noise = scratch.buffer(count); + for (int index = 0; index < count; index++) { + sampleX[index] = (xs[index] + offsetX) / zoom; + sampleZ[index] = (zs[index] + offsetZ) / zoom; + } + Arrays.fill(out, 0, count, multiplicitive ? 1D : 0D); + for (NoiseLayerProgram layer : layers) { + layer.evaluate(scratch, sampleX, sampleZ, count, noise); + if (multiplicitive) { + for (int index = 0; index < count; index++) { + out[index] *= noise[index]; + } + } else { + for (int index = 0; index < count; index++) { + out[index] += noise[index]; + } + } + } + for (int index = 0; index < count; index++) { + double value = multiplicitive ? out[index] * opacity : (out[index] / totalOpacity) * opacity; + out[index] = Double.isNaN(value) ? 0D : value; + } + if (cliffs != null) { + cliffs.evaluate(scratch, sampleX, sampleZ, count, noise); + for (int index = 0; index < count; index++) { + double height = IrisInterpolation.lerp(cliffHeightMin, cliffHeightMax, noise[index]); + out[index] = (Math.round((out[index] * 255D) / height) * height) / 255D; + } + } + if (cellCracks) { + for (int index = 0; index < count; index++) { + out[index] = generator.cell(xs[index], zs[index], out[index], 0D); + } + } + scratch.reset(mark); + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/noise/IrisGenerator.java b/core/src/main/java/art/arcane/iris/generation/noise/IrisGenerator.java index 6ff6032fb..dd5ebd912 100644 --- a/core/src/main/java/art/arcane/iris/generation/noise/IrisGenerator.java +++ b/core/src/main/java/art/arcane/iris/generation/noise/IrisGenerator.java @@ -41,10 +41,7 @@ import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; -import java.util.Arrays; import java.util.List; -import java.util.Objects; -import java.util.function.DoubleBinaryOperator; @Accessors(chain = true) @NoArgsConstructor @@ -211,7 +208,7 @@ public class IrisGenerator extends IrisRegistrant { if (surfaceDetail == 1D) { return sampleHeight(rx, rz, superSeed, data, engine); } - return surfaceHeight(rx, rz, superSeed, data, engine, null); + return surfaceHeight(rx, rz, superSeed, data, engine); } public boolean supportsBatch() { @@ -227,90 +224,84 @@ public class IrisGenerator extends IrisRegistrant { return !hasCliffs() || cliffHeightGenerator.supportsBatch(); } - public void getHeights(double[] xs, double[] zs, long superSeed, double[] target, int count) { - Objects.checkFromIndexSize(0, count, xs.length); - Objects.checkFromIndexSize(0, count, zs.length); - Objects.checkFromIndexSize(0, count, target.length); - if (count > 4096) { - throw new IllegalArgumentException("Generator batch exceeds 4096 samples"); + public GeneratorProgram compileProgram(long superSeed) { + if (!Double.isFinite(surfaceDetail) || surfaceDetail < 0D || surfaceDetail > 1D) { + throw new IllegalArgumentException("Generator surfaceDetail must be finite and between 0 and 1"); } - if (count < 8 || !supportsBatch()) { - for (int index = 0; index < count; index++) { - target[index] = getHeight(xs[index], zs[index], superSeed); - } - return; - } - if (composite.isEmpty()) { - Arrays.fill(target, 0, count, 0D); - return; + if (!supportsBatch()) { + throw new IllegalStateException("Generator " + getLoadKey() + " cannot be compiled into a batch program"); } IrisData data = getLoader(); Engine engine = data == null ? null : data.getEngine(); - DoubleBinaryOperator prepared = prepareHeight(superSeed, data, engine); - for (int index = 0; index < count; index++) { - target[index] = surfaceDetail == 1D ? prepared.applyAsDouble(xs[index], zs[index]) - : surfaceHeight(xs[index], zs[index], superSeed, data, engine, prepared); - } - } - - private DoubleBinaryOperator prepareHeight(long superSeed, IrisData data, Engine engine) { int hc = (int) ((cliffHeightMin * 10) + 10 + cliffHeightMax * getSeed() + offsetX + offsetZ); - DoubleBinaryOperator[] layers = new DoubleBinaryOperator[composite.size()]; + NoiseLayerProgram[] layers = new NoiseLayerProgram[composite.size()]; double totalOpacity = 0D; for (int index = 0; index < layers.length; index++) { IrisNoiseGenerator layer = composite.get(index); - layers[index] = layer.prepareBatch(getSeed() + superSeed + hc, data, engine); + layers[index] = layer.compileProgram(getSeed() + superSeed + hc, data, engine); if (!multiplicitive) { totalOpacity += layer.getOpacity(); } } - double sumOpacity = totalOpacity; double cliffSeed = (double) (superSeed + 294596 + hc) - 34857; - DoubleBinaryOperator cliffs = hasCliffs() ? cliffHeightGenerator.prepareBatch( - (long) (getSeed() + cliffSeed + hc), data, engine) : null; - return (rx, rz) -> { - double sampleX = (rx + offsetX) / zoom; - double sampleZ = (rz + offsetZ) / zoom; - double h = multiplicitive ? 1D : 0D; - for (DoubleBinaryOperator layer : layers) { - double sampled = layer.applyAsDouble(sampleX, sampleZ); - h = multiplicitive ? h * sampled : h + sampled; - } - double value = multiplicitive ? h * opacity : (h / sumOpacity) * opacity; - if (Double.isNaN(value)) { - value = 0D; - } - if (cliffs != null) { - double height = IrisInterpolation.lerp(cliffHeightMin, cliffHeightMax, - cliffs.applyAsDouble(sampleX, sampleZ)); - value = (Math.round((value * 255D) / height) * height) / 255D; - } - return hasCellCracks() ? cell(rx, rz, value, superSeed + 48622 + hc) : value; - }; + NoiseLayerProgram cliffs = hasCliffs() + ? cliffHeightGenerator.compileProgram((long) (getSeed() + cliffSeed + hc), data, engine) + : null; + return new GeneratorProgram(this, superSeed, layers, totalOpacity, cliffs); } - private double surfaceHeight(double rx, double rz, long superSeed, IrisData data, Engine engine, - DoubleBinaryOperator prepared) { + SurfaceCacheStripe[] surfaceStripes() { + IrisData data = getLoader(); + return surfaceCache(data, data == null ? null : data.getEngine()).stripes(); + } + + static boolean cachedSurface(SurfaceCacheStripe[] stripes, double x, double z, long superSeed, double[] target, int index) { + long key = ((long) (int) x << 32) | ((int) z & 0xffffffffL); + long mixed = surfaceMix(key); + SurfaceSample cached = stripes[(int) mixed & (SURFACE_CACHE_STRIPES - 1)] + .get(key, superSeed, (int) (mixed >>> 4) & (SURFACE_CACHE_STRIPE_SIZE - 1)); + if (cached == null) { + return false; + } + target[index] = cached.height(); + return true; + } + + static void storeSurface(SurfaceCacheStripe[] stripes, double x, double z, long superSeed, double height) { + long key = ((long) (int) x << 32) | ((int) z & 0xffffffffL); + long mixed = surfaceMix(key); + stripes[(int) mixed & (SURFACE_CACHE_STRIPES - 1)] + .put((int) (mixed >>> 4) & (SURFACE_CACHE_STRIPE_SIZE - 1), new SurfaceSample(key, superSeed, height)); + } + + private static long surfaceMix(long key) { + long mixed = key ^ (key >>> 33); + mixed *= 0xff51afd7ed558ccdL; + mixed ^= mixed >>> 33; + return mixed; + } + + private double surfaceHeight(double rx, double rz, long superSeed, IrisData data, Engine engine) { SurfaceCacheStripe[] stripes = surfaceCache(data, engine).stripes(); double lowerX = Math.floor(rx / SURFACE_GRID) * SURFACE_GRID; double lowerZ = Math.floor(rz / SURFACE_GRID) * SURFACE_GRID; double dx = (rx - lowerX) / SURFACE_GRID; double dz = (rz - lowerZ) / SURFACE_GRID; - double northWest = cachedSurfaceHeight(lowerX, lowerZ, superSeed, stripes, data, engine, prepared); + double northWest = cachedSurfaceHeight(lowerX, lowerZ, superSeed, stripes, data, engine); if (dx == 0D && dz == 0D) { return northWest; } double north = dx == 0D ? northWest : IrisInterpolation.lerp(northWest, - cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ, superSeed, stripes, data, engine, prepared), dx); + cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ, superSeed, stripes, data, engine), dx); double smooth = north; if (dz != 0D) { - double southWest = cachedSurfaceHeight(lowerX, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine, prepared); + double southWest = cachedSurfaceHeight(lowerX, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine); double south = dx == 0D ? southWest : IrisInterpolation.lerp(southWest, - cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine, prepared), dx); + cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine), dx); smooth = IrisInterpolation.lerp(north, south, dz); } return surfaceDetail == 0D ? smooth - : smooth + ((prepared == null ? sampleHeight(rx, rz, superSeed, data, engine) : prepared.applyAsDouble(rx, rz)) - smooth) * surfaceDetail; + : smooth + (sampleHeight(rx, rz, superSeed, data, engine) - smooth) * surfaceDetail; } private double sampleHeight(double rx, double rz, long superSeed, IrisData data, Engine engine) { @@ -350,21 +341,19 @@ public class IrisGenerator extends IrisRegistrant { return v; } - private double cachedSurfaceHeight(double x, double z, long superSeed, SurfaceCacheStripe[] stripes, IrisData data, Engine engine, DoubleBinaryOperator prepared) { + private double cachedSurfaceHeight(double x, double z, long superSeed, SurfaceCacheStripe[] stripes, IrisData data, Engine engine) { if (x != (int) x || z != (int) z) { - return prepared == null ? sampleHeight(x, z, superSeed, data, engine) : prepared.applyAsDouble(x, z); + return sampleHeight(x, z, superSeed, data, engine); } long key = ((long) (int) x << 32) | ((int) z & 0xffffffffL); - long mixed = key ^ (key >>> 33); - mixed *= 0xff51afd7ed558ccdL; - mixed ^= mixed >>> 33; + long mixed = surfaceMix(key); SurfaceCacheStripe stripe = stripes[(int) mixed & (SURFACE_CACHE_STRIPES - 1)]; int slot = (int) (mixed >>> 4) & (SURFACE_CACHE_STRIPE_SIZE - 1); SurfaceSample cached = stripe.get(key, superSeed, slot); if (cached != null) { return cached.height(); } - double height = prepared == null ? sampleHeight(x, z, superSeed, data, engine) : prepared.applyAsDouble(x, z); + double height = sampleHeight(x, z, superSeed, data, engine); stripe.put(slot, new SurfaceSample(key, superSeed, height)); return height; } @@ -457,7 +446,7 @@ public class IrisGenerator extends IrisRegistrant { private record SurfaceSample(long key, long seed, double height) { } - private static final class SurfaceCacheStripe { + static final class SurfaceCacheStripe { private final AtomicReferenceArray entries = new AtomicReferenceArray<>(SURFACE_CACHE_STRIPE_SIZE); private SurfaceSample get(long key, long seed, int slot) { diff --git a/core/src/main/java/art/arcane/iris/generation/noise/IrisNoiseGenerator.java b/core/src/main/java/art/arcane/iris/generation/noise/IrisNoiseGenerator.java index 2793c4c23..1ea234e8c 100644 --- a/core/src/main/java/art/arcane/iris/generation/noise/IrisNoiseGenerator.java +++ b/core/src/main/java/art/arcane/iris/generation/noise/IrisNoiseGenerator.java @@ -39,6 +39,7 @@ import art.arcane.volmlib.util.noise.NoiseGenerator; import art.arcane.volmlib.util.noise.OffsetNoiseGenerator; import art.arcane.volmlib.util.noise.PerlinNoise; import art.arcane.volmlib.util.noise.SimplexNoise; +import art.arcane.volmlib.util.noise.program.NoiseProgram; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Data; @@ -47,8 +48,8 @@ import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.Accessors; +import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.DoubleBinaryOperator; @Snippet("generator") @Accessors(chain = true) @@ -198,44 +199,29 @@ public class IrisNoiseGenerator { return true; } - DoubleBinaryOperator prepareBatch(long superSeed, IrisData data, Engine engine) { + NoiseLayerProgram compileProgram(long superSeed, IrisData data, Engine engine) { if (!enabled) { - double value = offsetY; - return (x, z) -> value; + return NoiseLayerProgram.disabled(this); } - if (hasDeepFracture()) { - FractureSamples samples = new FractureSamples(); - if (samples.prepare(this, superSeed, data, engine)) { - return samples::sample; + int enabledChildren = 0; + for (IrisNoiseGenerator child : fracture) { + if (child.isEnabled()) { + enabledChildren++; } } - DoubleBinaryOperator[] children = new DoubleBinaryOperator[fracture.size()]; - double[] offsets = new double[fracture.size()]; + NoiseLayerProgram[] children = new NoiseLayerProgram[enabledChildren]; + double[] offsets = new double[enabledChildren]; int salt = 33; - for (int index = 0; index < children.length; index++) { - IrisNoiseGenerator child = fracture.get(index); + int compiled = 0; + for (IrisNoiseGenerator child : fracture) { if (child.isEnabled()) { - children[index] = child.prepareBatch(superSeed + seed + salt, data, engine); - offsets[index] = child.getOpacity() / 2D; + children[compiled] = child.compileProgram(superSeed + seed + salt, data, engine); + offsets[compiled++] = child.getOpacity() / 2D; } salt += 819; } - CNG generator = getGenerator(superSeed, data, engine); - return (x, z) -> samplePrepared(generator, children, offsets, x, z); - } - - private double samplePrepared(CNG generator, DoubleBinaryOperator[] children, double[] offsets, - double xv, double zv) { - double x = xv; - double z = zv; - for (int index = 0; index < children.length; index++) { - DoubleBinaryOperator child = children[index]; - if (child != null) { - x += child.applyAsDouble(xv, zv) - offsets[index]; - z += child.applyAsDouble(zv, xv) - offsets[index]; - } - } - return sample(generator, x, z); + CNG generator = Objects.requireNonNull(getGenerator(superSeed, data, engine), "Noise layer generator"); + return NoiseLayerProgram.of(this, NoiseProgram.compile2D(generator), children, offsets); } public KList getAllComposites() { diff --git a/core/src/main/java/art/arcane/iris/generation/noise/NoiseLayerProgram.java b/core/src/main/java/art/arcane/iris/generation/noise/NoiseLayerProgram.java new file mode 100644 index 000000000..e9bd1df41 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/noise/NoiseLayerProgram.java @@ -0,0 +1,184 @@ +package art.arcane.iris.generation.noise; + +import art.arcane.volmlib.util.interpolation.IrisInterpolation; +import art.arcane.volmlib.util.noise.program.NoiseProgram; + +import java.util.Arrays; + +public final class NoiseLayerProgram { + private final boolean enabled; + private final double offsetY; + private final NoiseProgram program; + private final double zoom; + private final double opacity; + private final double offsetX; + private final double offsetZ; + private final double exponent; + private final boolean negative; + private final boolean parametric; + private final boolean bezier; + private final boolean sinCentered; + private final NoiseLayerProgram[] children; + private final double[] childOffsets; + + private NoiseLayerProgram(IrisNoiseGenerator layer, NoiseProgram program, NoiseLayerProgram[] children, double[] childOffsets) { + this.enabled = program != null; + this.offsetY = layer.getOffsetY(); + this.program = program; + this.zoom = layer.getZoom(); + this.opacity = layer.getOpacity(); + this.offsetX = layer.getOffsetX(); + this.offsetZ = layer.getOffsetZ(); + this.exponent = layer.getExponent(); + this.negative = layer.isNegative(); + this.parametric = layer.isParametric(); + this.bezier = layer.isBezier(); + this.sinCentered = layer.isSinCentered(); + this.children = children; + this.childOffsets = childOffsets; + } + + static NoiseLayerProgram disabled(IrisNoiseGenerator layer) { + return new NoiseLayerProgram(layer, null, new NoiseLayerProgram[0], new double[0]); + } + + static NoiseLayerProgram of(IrisNoiseGenerator layer, NoiseProgram program, NoiseLayerProgram[] children, double[] childOffsets) { + return new NoiseLayerProgram(layer, program, children, childOffsets); + } + + public void evaluate(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) { + if (!enabled) { + Arrays.fill(out, 0, count, offsetY); + return; + } + int mark = scratch.mark(); + double[] sampleX = scratch.buffer(count); + double[] sampleZ = scratch.buffer(count); + if (children.length == 0) { + for (int index = 0; index < count; index++) { + sampleX[index] = (xs[index] / zoom) + offsetX; + sampleZ[index] = (zs[index] / zoom) + offsetZ; + } + } else { + System.arraycopy(xs, 0, sampleX, 0, count); + System.arraycopy(zs, 0, sampleZ, 0, count); + double[] forward = scratch.buffer(count); + double[] reverse = scratch.buffer(count); + for (int child = 0; child < children.length; child++) { + children[child].evaluatePair(scratch, xs, zs, count, forward, reverse); + double offset = childOffsets[child]; + for (int index = 0; index < count; index++) { + sampleX[index] += forward[index] - offset; + sampleZ[index] += reverse[index] - offset; + } + } + for (int index = 0; index < count; index++) { + sampleX[index] = (sampleX[index] / zoom) + offsetX; + sampleZ[index] = (sampleZ[index] / zoom) + offsetZ; + } + } + program.fill2D(scratch.noise(), sampleX, sampleZ, count, out); + for (int index = 0; index < count; index++) { + out[index] = post(out[index]); + } + scratch.reset(mark); + } + + void evaluatePair(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] forward, double[] reverse) { + if (!enabled) { + Arrays.fill(forward, 0, count, offsetY); + Arrays.fill(reverse, 0, count, offsetY); + return; + } + int mark = scratch.mark(); + int pairs = count * 2; + double[] sampleX = scratch.buffer(pairs); + double[] sampleZ = scratch.buffer(pairs); + System.arraycopy(xs, 0, sampleX, 0, count); + System.arraycopy(zs, 0, sampleZ, 0, count); + System.arraycopy(zs, 0, sampleX, count, count); + System.arraycopy(xs, 0, sampleZ, count, count); + if (children.length > 0) { + double[] childForward = scratch.buffer(count); + double[] childReverse = scratch.buffer(count); + for (int child = 0; child < children.length; child++) { + children[child].evaluatePair(scratch, xs, zs, count, childForward, childReverse); + double offset = childOffsets[child]; + for (int index = 0; index < count; index++) { + sampleX[index] += childForward[index] - offset; + sampleZ[index] += childReverse[index] - offset; + sampleX[count + index] += childReverse[index] - offset; + sampleZ[count + index] += childForward[index] - offset; + } + } + } + for (int index = 0; index < pairs; index++) { + sampleX[index] = (sampleX[index] / zoom) + offsetX; + sampleZ[index] = (sampleZ[index] / zoom) + offsetZ; + } + double[] values = scratch.buffer(pairs); + program.fill2D(scratch.noise(), sampleX, sampleZ, pairs, values); + for (int index = 0; index < count; index++) { + forward[index] = post(values[index]); + reverse[index] = post(values[count + index]); + } + scratch.reset(mark); + } + + public double sample(TerrainBatchScratch scratch, double x, double z) { + if (!enabled) { + return offsetY; + } + double sampleX = x; + double sampleZ = z; + if (children.length > 0) { + int mark = scratch.mark(); + double[] pair = scratch.buffer(2); + for (int child = 0; child < children.length; child++) { + children[child].samplePair(scratch, x, z, pair); + double offset = childOffsets[child]; + sampleX += pair[0] - offset; + sampleZ += pair[1] - offset; + } + scratch.reset(mark); + } + return post(program.sample2D((sampleX / zoom) + offsetX, (sampleZ / zoom) + offsetZ)); + } + + private void samplePair(TerrainBatchScratch scratch, double x, double z, double[] out) { + if (!enabled) { + out[0] = offsetY; + out[1] = offsetY; + return; + } + double forwardX = x; + double forwardZ = z; + double reverseX = z; + double reverseZ = x; + if (children.length > 0) { + int mark = scratch.mark(); + double[] pair = scratch.buffer(2); + for (int child = 0; child < children.length; child++) { + children[child].samplePair(scratch, x, z, pair); + double offset = childOffsets[child]; + forwardX += pair[0] - offset; + forwardZ += pair[1] - offset; + reverseX += pair[1] - offset; + reverseZ += pair[0] - offset; + } + scratch.reset(mark); + } + out[0] = post(program.sample2D((forwardX / zoom) + offsetX, (forwardZ / zoom) + offsetZ)); + out[1] = post(program.sample2D((reverseX / zoom) + offsetX, (reverseZ / zoom) + offsetZ)); + } + + private double post(double raw) { + double n = raw * opacity; + n = negative ? (-n + opacity) : n; + n = (exponent != 1 ? n < 0 ? -Math.pow(-n, exponent) : Math.pow(n, exponent) : n) + offsetY; + n = parametric ? IrisInterpolation.parametric(n, 2D) : n; + n = bezier ? IrisInterpolation.bezier(n) : n; + n = sinCentered ? IrisInterpolation.sinCenter(n) : n; + return n; + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/noise/TerrainBatchScratch.java b/core/src/main/java/art/arcane/iris/generation/noise/TerrainBatchScratch.java new file mode 100644 index 000000000..9cdaaa6eb --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/noise/TerrainBatchScratch.java @@ -0,0 +1,115 @@ +package art.arcane.iris.generation.noise; + +import art.arcane.volmlib.util.noise.program.NoiseScratch; + +import java.util.Arrays; + +public final class TerrainBatchScratch { + private static final int MINIMUM_BUFFER = 256; + + private final NoiseScratch noise = new NoiseScratch(); + private double[][] buffers = new double[32][]; + private int[][] indexBuffers = new int[8][]; + private int top; + private int indexTop; + private long[] latticeX = new long[64]; + private long[] latticeZ = new long[64]; + private int[] latticeSlots = new int[64]; + private int latticeCapacity = 64; + private int latticeCount; + + public NoiseScratch noise() { + return noise; + } + + public int mark() { + return top; + } + + public int indexMark() { + return indexTop; + } + + public void reset(int mark) { + top = mark; + } + + public void resetIndices(int mark) { + indexTop = mark; + } + + public double[] buffer(int length) { + if (top == buffers.length) { + buffers = Arrays.copyOf(buffers, top * 2); + } + double[] buffer = buffers[top]; + if (buffer == null || buffer.length < length) { + buffer = new double[Math.max(length, MINIMUM_BUFFER)]; + buffers[top] = buffer; + } + top++; + return buffer; + } + + public int[] indices(int length) { + if (indexTop == indexBuffers.length) { + indexBuffers = Arrays.copyOf(indexBuffers, indexTop * 2); + } + int[] buffer = indexBuffers[indexTop]; + if (buffer == null || buffer.length < length) { + buffer = new int[Math.max(length, MINIMUM_BUFFER)]; + indexBuffers[indexTop] = buffer; + } + indexTop++; + return buffer; + } + + void clearLattice(int maximumEntries) { + int capacity = Integer.highestOneBit(Math.max(16, maximumEntries * 2) - 1) << 1; + if (latticeSlots.length < capacity) { + latticeSlots = new int[capacity]; + } else { + Arrays.fill(latticeSlots, 0, capacity, 0); + } + latticeCapacity = capacity; + latticeCount = 0; + } + + int lattice(double x, double z) { + long xBits = Double.doubleToRawLongBits(x); + long zBits = Double.doubleToRawLongBits(z); + long mixed = xBits * 0x9E3779B97F4A7C15L ^ Long.rotateLeft(zBits * 0xC2B2AE3D27D4EB4FL, 31); + mixed ^= mixed >>> 29; + int mask = latticeCapacity - 1; + int slot = (int) mixed & mask; + while (true) { + int stored = latticeSlots[slot]; + if (stored == 0) { + if (latticeCount == latticeX.length) { + latticeX = Arrays.copyOf(latticeX, latticeCount * 2); + latticeZ = Arrays.copyOf(latticeZ, latticeCount * 2); + } + latticeX[latticeCount] = xBits; + latticeZ[latticeCount] = zBits; + latticeSlots[slot] = ++latticeCount; + return latticeCount - 1; + } + if (latticeX[stored - 1] == xBits && latticeZ[stored - 1] == zBits) { + return stored - 1; + } + slot = (slot + 1) & mask; + } + } + + int latticeCount() { + return latticeCount; + } + + double latticeX(int index) { + return Double.longBitsToDouble(latticeX[index]); + } + + double latticeZ(int index) { + return Double.longBitsToDouble(latticeZ[index]); + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java b/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java index d7145e8eb..c995c2e27 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java @@ -18,7 +18,21 @@ package art.arcane.iris.generation.runtime; +import art.arcane.iris.generation.bake.BakePlane; +import art.arcane.iris.generation.bake.BakeRegistry; +import art.arcane.iris.generation.bake.ChunkBakeCache; +import art.arcane.iris.generation.bake.ChunkPlanes; +import art.arcane.iris.generation.bake.ColumnBake; +import art.arcane.iris.generation.bake.GeneratorEvaluations; +import art.arcane.iris.generation.bake.PlaneSource; +import art.arcane.iris.generation.bake.TerrainProgram; +import art.arcane.iris.generation.noise.TerrainBatchScratch; +import art.arcane.iris.generation.simd.SimdSupport; +import art.arcane.iris.generation.stream.BakedDoubleStream; +import art.arcane.iris.generation.stream.BakedHeightStream; +import art.arcane.iris.generation.stream.BakedStream; import art.arcane.iris.generation.stream.GenerationStreams; +import art.arcane.iris.generation.stream.RarityStream; import art.arcane.iris.generation.stream.CachedDoubleStream2D; import art.arcane.iris.generation.stream.CachedStream2D; import art.arcane.iris.generation.stream.ProvisionalSampling; @@ -118,14 +132,16 @@ import java.nio.file.Path; import java.util.function.BiFunction; @Data -@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"}) -@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"}) +@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "terrainProgram", "bakeCache", "biomeRegistry", "regionRegistry", "blockRegistry", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"}) +@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "terrainProgram", "bakeCache", "biomeRegistry", "regionRegistry", "blockRegistry", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "hydrologyRuntime", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyBanks3D"}) public class IrisComplex implements DataProvider { private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D); private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L); private static final int GRID_BOUNDS_CACHE_SIZE = 8192; private static final int STUDIO_NOISE_CACHE_SIZE = 32_768; private static final int TERRAIN_COLUMN_CACHE_SIZE = 524_288; + private static final int BAKE_CHUNKS_PER_NOISE_CHUNK = 8; + private static final int HYDROLOGY_BAKE_BUDGET_SHARES = 2; /** One million corners: about 20 MB, roughly a 4000 by 4000 block area at the 4-block grid. */ private static final int SHARED_CORNER_BOUNDS_CAPACITY = 1 << 20; /** The slope streams measure the rise across a run of this many blocks, so gradient is slope over run. */ @@ -141,6 +157,7 @@ public class IrisComplex implements DataProvider { IrisGenerator::getLoadKey, Comparator.nullsFirst(Comparator.naturalOrder()) ); + private static final ThreadLocal BATCH_SCRATCH = ThreadLocal.withInitial(TerrainBatchScratch::new); private static final InferredType[] INFERRED_BIOME_PREPARATION_ORDER = { InferredType.LAND, InferredType.CAVE, @@ -153,6 +170,37 @@ public class IrisComplex implements DataProvider { private final int biomeBoundsSamplingStep; private transient volatile SharedCornerBounds sharedCornerBounds; @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + private transient volatile TerrainProgram terrainProgram; + @Getter(AccessLevel.NONE) + private final transient ChunkBakeCache bakeCache; + @Getter(AccessLevel.NONE) + private final transient BakeRegistry biomeRegistry = new BakeRegistry<>(); + @Getter(AccessLevel.NONE) + private final transient BakeRegistry regionRegistry = new BakeRegistry<>(); + @Getter(AccessLevel.NONE) + private final transient BakeRegistry blockRegistry = new BakeRegistry<>(); + @Getter(AccessLevel.NONE) + private final transient long childStyleSeed; + @Getter(AccessLevel.NONE) + private final transient Map> inferredByType; + @Getter(AccessLevel.NONE) + private final transient ProceduralStream regionSource; + @Getter(AccessLevel.NONE) + private final transient ProceduralStream baseBiomeSource; + @Getter(AccessLevel.NONE) + private final transient ProceduralStream caveSource; + @Getter(AccessLevel.NONE) + private final transient ProceduralStream rockSource; + @Getter(AccessLevel.NONE) + private final transient ProceduralStream fluidSource; + @Getter(AccessLevel.NONE) + private final transient ProceduralStream naturalHeightSource; + @Getter(AccessLevel.NONE) + private final transient ProceduralStream naturalTrueBiomeSource; + @Getter(AccessLevel.NONE) + private final transient ProceduralStream trueBiomeSource; + @Getter(AccessLevel.NONE) private final transient IrisInterpolator[] frozenInterpolators; @Getter(AccessLevel.NONE) private final transient IrisGenerator[][] frozenGenerators; @@ -170,6 +218,9 @@ public class IrisComplex implements DataProvider { @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) private transient boolean hydrologyNoiseCachesClosed; + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + private transient HydrologyNoiseCacheBudget.Reservation hydrologyBakeReservation; private RNG rng; private double fluidHeight; private IrisData data; @@ -213,9 +264,6 @@ public class IrisComplex implements DataProvider { private ProceduralStream unblendedNaturalHeightStream; private final TransitionDisplacementField transitionDisplacement; private ProceduralStream heightStream; - @Getter(AccessLevel.NONE) - @Setter(AccessLevel.NONE) - private CachedDoubleStream2D cachedHeightStream; private ProceduralStream roundedHeighteightStream; private ProceduralStream maxHeightStream; private ProceduralStream overlayStream; @@ -279,6 +327,7 @@ public class IrisComplex implements DataProvider { IrisBiome emptyBiome = new IrisBiome().setInferredType(InferredType.CAVE); UUID focusUUID = UUID.nameUUIDFromBytes("focus".getBytes()); this.rng = new RNG(engine.getSeedManager().getComplex()); + childStyleSeed = IrisBiome.childStyleSeed(rng, 123); this.data = engine.getData(); imageMapRuntime = IrisImageMapRuntime.compile(engine); double height = engine.getMaxHeight(); @@ -292,7 +341,8 @@ public class IrisComplex implements DataProvider { boolean buffetActive = biomeBuffet != null && !biomeBuffet.cells().isEmpty(); focusBiome = buffetActive ? null : compatUsable(engine.getFocus()); focusRegion = buffetActive ? null : compatUsable(engine.getFocusRegion()); - Map> inferredStreams = new HashMap<>(); + Map> inferredStreams = new EnumMap<>(InferredType.class); + inferredByType = inferredStreams; KList preparedRegions = new KList<>(); if (focusBiome != null) { @@ -365,54 +415,59 @@ public class IrisComplex implements DataProvider { return value; }); - rockStream = engine.getDimension().getRockPalette().getLayerGenerator(rng.nextParallelRNG(45), data).stream() + bakeCache = new ChunkBakeCache(new BakeSource(), cacheSize * BAKE_CHUNKS_PER_NOISE_CHUNK); + rockSource = engine.getDimension().getRockPalette().getLayerGenerator(rng.nextParallelRNG(45), data).stream() .select(engine.getDimension().getRockPalette().getBlockData(data)); - fluidStream = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream() + rockStream = new BakedStream<>(bakeCache, BakePlane.ROCK, rockSource); + fluidSource = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream() .select(engine.getDimension().getFluidPalette().getBlockData(data)); + fluidStream = new BakedStream<>(bakeCache, BakePlane.FLUID, fluidSource); hydrologyFluidStreams = createHydrologyFluidStreams(engine.getDimension().getHydrology()); regionStyleStream = engine.getDimension().getRegionStyle().create(rng.nextParallelRNG(883), getData()).stream() .zoom(engine.getDimension().getRegionZoom()); regionIdentityStream = regionStyleStream.fit(Integer.MIN_VALUE, Integer.MAX_VALUE); - regionStream = focusedRegions != null ? focusedRegions - : GenerationStreams.cache2D(RegionSelection.select(new RegionSelection.Options( - engine.getDimension(), data, regionStyleStream, imageMapRuntime, null)), - "regionStream", engine, cacheSize); - caveBiomeStream = GenerationStreams.cache2D(GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)) + regionSource = focusedRegions != null ? focusedRegions + : RegionSelection.select(new RegionSelection.Options( + engine.getDimension(), data, regionStyleStream, imageMapRuntime, null)); + regionStream = focusedRegions != null ? focusedRegions : new BakedStream<>(bakeCache, BakePlane.REGION, regionSource); + caveSource = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)) .convert((r) -> createInferredBiomeStream(r, InferredType.CAVE)) - .convertAware2D(ProceduralStream::get), "caveBiomeStream", engine, cacheSize); + .convertAware2D(ProceduralStream::get); + caveBiomeStream = new BakedStream<>(bakeCache, BakePlane.CAVE_BIOME, caveSource); inferredStreams.put(InferredType.CAVE, caveBiomeStream); - landBiomeStream = GenerationStreams.cache2D(GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)) + landBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)) .convert((r) -> createInferredBiomeStream(r, InferredType.LAND)) - .convertAware2D(ProceduralStream::get), "landBiomeStream", engine, cacheSize); + .convertAware2D(ProceduralStream::get); inferredStreams.put(InferredType.LAND, landBiomeStream); - seaBiomeStream = GenerationStreams.cache2D(GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)) + seaBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)) .convert((r) -> createInferredBiomeStream(r, InferredType.SEA)) - .convertAware2D(ProceduralStream::get), "seaBiomeStream", engine, cacheSize); + .convertAware2D(ProceduralStream::get); inferredStreams.put(InferredType.SEA, seaBiomeStream); - shoreBiomeStream = GenerationStreams.cache2D(GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)) + shoreBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)) .convert((r) -> createInferredBiomeStream(r, InferredType.SHORE)) - .convertAware2D(ProceduralStream::get), "shoreBiomeStream", engine, cacheSize); + .convertAware2D(ProceduralStream::get); inferredStreams.put(InferredType.SHORE, shoreBiomeStream); bridgeStream = focusedBiomes != null ? focusedBiomes.convert(IrisBiome::getInferredType) : - GenerationStreams.cache2D(ContinentGenerator.create(engine.getDimension(), rng, getData()).stream() - .convert((v) -> v >= engine.getDimension().getLandChance() ? InferredType.SEA : InferredType.LAND), "bridgeStream", engine, cacheSize); - ProceduralStream proceduralBaseBiomeStream = focusedBiomes != null ? focusedBiomes : - GenerationStreams.cache2D(bridgeStream.convertAware2D((t, x, z) -> inferredStreams.get(t).get(x, z)) - .convertAware2D(this::implode), "baseBiomeStream", engine, cacheSize); - baseBiomeStream = focusedBiomes != null || !imageMapRuntime.has(IrisImageMapApplication.BIOME) - ? proceduralBaseBiomeStream : GenerationStreams.cache2D(ProceduralStream.of((x, z) -> { + ContinentGenerator.create(engine.getDimension(), rng, getData()).stream() + .convert((v) -> v >= engine.getDimension().getLandChance() ? InferredType.SEA : InferredType.LAND); + ProceduralStream proceduralBaseBiomeStream = focusedBiomes != null ? focusedBiomes + : ProceduralStream.of(this::proceduralBaseBiome, + (x, y, z) -> implode(inferredByType.get(bridgeStream.get(x, y, z)).get(x, z), x, z), + Interpolated.of(value -> 0D, value -> null)); + baseBiomeSource = focusedBiomes != null || !imageMapRuntime.has(IrisImageMapApplication.BIOME) + ? proceduralBaseBiomeStream : ProceduralStream.of((x, z) -> { IrisBiome mapped = imageMapRuntime.sampleBiome(x, z); return mapped == null ? proceduralBaseBiomeStream.get(x, z) : mapped; }, (x, y, z) -> { IrisBiome mapped = imageMapRuntime.sampleBiome(x, z); return mapped == null ? proceduralBaseBiomeStream.get(x, y, z) : mapped; - }, proceduralBaseBiomeStream), "imageMappedBaseBiomeStream", engine, cacheSize); + }, proceduralBaseBiomeStream); + baseBiomeStream = focusedBiomes != null ? focusedBiomes : new BakedStream<>(bakeCache, BakePlane.BASE_BIOME, baseBiomeSource); proceduralTerrainHeight = new ProceduralTerrainHeightSampler( (x, z) -> getHeight(engine, x, z, engine.getSeedManager().getHeight()), engine.getDimension().getTerrainSamplingStep()); - baseTerrainHeightStream = GenerationStreams.cache2DDouble( - ProceduralStream.ofDouble(this::sampleUnblendedNaturalTerrainHeight), - "baseTerrainHeightStream", engine, cacheSize); + baseTerrainHeightStream = new BakedDoubleStream(bakeCache, BakePlane.BASE_HEIGHT, + ProceduralStream.ofDouble(this::sampleUnblendedNaturalTerrainHeight)); boolean terrain3DEnabled = false; for (IrisBiome biome : generatorBiomes) { if (biome.getTerrain3D() != null) { @@ -427,18 +482,20 @@ public class IrisComplex implements DataProvider { new Terrain3DRuntime.Options(engine.getSeedManager().getTerrain(), engine.getHeight(), fluidHeight, data, terrain3DEnabled, TERRAIN_COLUMN_CACHE_SIZE)); unblendedNaturalHeightStream = terrain3DEnabled - ? GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::sampleTerrain3DHeight), "unblendedNaturalHeightStream", engine, cacheSize) + ? ProceduralStream.ofDouble(this::sampleTerrain3DHeight) : baseTerrainHeightStream; transitionDisplacement = transitionGenerationPlan == null ? null : new TransitionDisplacementField( transitionGenerationPlan, new TransitionDisplacementField.Sources( (x, z) -> unblendedNaturalHeightStream.getDouble(x, z), engine.getHeight())); - naturalHeightStream = transitionDisplacement == null ? unblendedNaturalHeightStream - : GenerationStreams.cache2DDouble(ProceduralStream.ofDouble((x, z) -> transitionDisplacement.height( - blockCoordinate(x), blockCoordinate(z), unblendedNaturalHeightStream.getDouble(x, z))), - "naturalHeightStream", engine, cacheSize); - naturalTrueBiomeStream = focusedBiomes != null ? GenerationStreams.cache2D(focusedBiomes, "naturalTrueBiomeStream-focus", engine, cacheSize) - : GenerationStreams.cache2D(ProceduralStream.of(this::sampleNaturalTrueBiome, this::sampleNaturalTrueBiome, - Interpolated.of(value -> 0D, value -> null)), "naturalTrueBiomeStream", engine, cacheSize); + naturalHeightSource = transitionDisplacement == null + ? unblendedNaturalHeightStream + : ProceduralStream.ofDouble((x, z) -> transitionDisplacement.height( + blockCoordinate(x), blockCoordinate(z), unblendedNaturalHeightStream.getDouble(x, z))); + naturalHeightStream = new BakedDoubleStream(bakeCache, BakePlane.NATURAL_HEIGHT, naturalHeightSource); + naturalTrueBiomeSource = focusedBiomes != null ? focusedBiomes + : ProceduralStream.of(this::sampleNaturalTrueBiome, this::sampleNaturalTrueBiome, + Interpolated.of(value -> 0D, value -> null)); + naturalTrueBiomeStream = new BakedStream<>(bakeCache, BakePlane.NATURAL_TRUE_BIOME, naturalTrueBiomeSource); IrisHydrology configuredHydrology = engine.getDimension().getHydrology(); if (hydrologyActive(configuredHydrology)) { hydrologyRuntime = new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext( @@ -467,8 +524,8 @@ public class IrisComplex implements DataProvider { Math.max(4096, cacheSize), data, banks3D)); } } - cachedHeightStream = new TerrainHeightStream(engine, cacheSize); - heightStream = cachedHeightStream; + heightStream = new BakedHeightStream(bakeCache, () -> terrainEngine.getPlatformHooks().isMainThread(), + this::nonblockingTerrainHeight, this::resolveHydrologyTerrainHeight); roundedHeighteightStream = GenerationStreams.contextInjecting(heightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z)) .round(); slopeStream = GenerationStreams.contextInjecting(heightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z)) @@ -476,8 +533,9 @@ public class IrisComplex implements DataProvider { naturalSlopeStream = GenerationStreams.cache2DDouble(naturalHeightStream.slope(3), "naturalSlopeStream", engine, cacheSize); // Terrain3D column heights resolve the surface biome, so the shore band reads the pre-terrain3D slope or it recurses. shoreSlopeStream = terrain3DEnabled ? baseTerrainHeightStream.slope(3) : naturalSlopeStream; - trueBiomeStream = focusedBiomes != null ? GenerationStreams.cache2D(focusedBiomes, "trueBiomeStream-focus", engine, cacheSize) : GenerationStreams.cache2D(heightStream - .convertAware2D((terrainHeight, x, z) -> resolveHydrologySurfaceBiome(terrainHeight, x, z)), "trueBiomeStream", engine, cacheSize); + trueBiomeSource = focusedBiomes != null ? focusedBiomes + : heightStream.convertAware2D((terrainHeight, x, z) -> resolveHydrologySurfaceBiome(terrainHeight, x, z)); + trueBiomeStream = new BakedStream<>(bakeCache, BakePlane.TRUE_BIOME, trueBiomeSource); riverDistanceStream = GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::resolveHydrologyDistance), "riverDistanceStream", engine, cacheSize); riverFlowStream = GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::resolveHydrologyFlow), "riverFlowStream", engine, cacheSize); riverCarveWeightStream = GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::resolveHydrologyCarveWeight), "riverCarveWeightStream", engine, cacheSize); @@ -518,17 +576,11 @@ public class IrisComplex implements DataProvider { return engine.isStudio() && !detached ? Math.max(configuredSize, STUDIO_NOISE_CACHE_SIZE) : configuredSize; } - public synchronized void ensureTerrainNoiseCacheSize(int minimumChunks) { + public void ensureTerrainNoiseCacheSize(int minimumChunks) { if (minimumChunks <= 0) { throw new IllegalArgumentException("Terrain noise cache size must be positive"); } - CachedDoubleStream2D naturalCache = (CachedDoubleStream2D) naturalHeightStream; - if (naturalCache.getMaxSize() < minimumChunks * 256L) { - naturalCache.setMaximumChunks(minimumChunks); - } - if (cachedHeightStream.getMaxSize() < minimumChunks * 256L) { - cachedHeightStream.setMaximumChunks(minimumChunks); - } + bakeCache.ensureCapacity(minimumChunks * BAKE_CHUNKS_PER_NOISE_CHUNK); } void expandHydrologyNoiseCaches() { @@ -578,6 +630,27 @@ public class IrisComplex implements DataProvider { hydrologyNoiseCaches = List.of(); } + synchronized void reserveHydrologyBake(HydrologyNoiseCacheBudget budget) { + if (detached || hydrologyRuntime == null || hydrologyNoiseCachesClosed || hydrologyBakeReservation != null) { + return; + } + int capacity = bakeCache.capacity(); + int requested = HydrologyNoiseCacheBudget.MAXIMUM_CHUNKS - capacity; + if (requested <= 0) { + return; + } + hydrologyBakeReservation = budget.reserve(HYDROLOGY_BAKE_BUDGET_SHARES, requested); + bakeCache.ensureCapacity(capacity + hydrologyBakeReservation.additionalChunks()); + } + + private synchronized void releaseHydrologyBake() { + if (hydrologyBakeReservation == null) { + return; + } + hydrologyBakeReservation.close(); + hydrologyBakeReservation = null; + } + void enablePreparedHydrologyCache(String runtimeIdentity, Path persistentRoot) { if (hydrologyRuntime != null) { hydrologyRuntime.enableSharedCache(runtimeIdentity, persistentRoot); @@ -787,106 +860,261 @@ public class IrisComplex implements DataProvider { return true; } - private void sampleBaseHeightBatch(long[] coordinates, int count, double[] target) { - ((CachedDoubleStream2D) baseTerrainHeightStream).getBatch(coordinates, count, target, this::resolveBaseHeightBatch); - } - private void resolveBaseHeightBatch(long[] missing, int size, double[] values) { - double[] xs = new double[size]; - double[] zs = new double[size]; - double[] lows = new double[size]; - double[] highs = new double[size]; - double[] activeXs = new double[size]; - double[] activeZs = new double[size]; - double[] noise = new double[size]; - double[] groupHeights = new double[size]; - int[] active = new int[size]; - for (int index = 0; index < size; index++) { - xs[index] = RiverFootprint.unpackX(missing[index]); - zs[index] = RiverFootprint.unpackZ(missing[index]); - } - long seed = terrainEngine.getSeedManager().getHeight() + 239945; - for (int group = 0; group < frozenInterpolators.length; group++) { - IrisGenerator[] generators = frozenGenerators[group]; - if (generators.length == 0) { - continue; - } - int activeCount = 0; - Arrays.fill(groupHeights, 0D); + TerrainBatchScratch scratch = BATCH_SCRATCH.get(); + int mark = scratch.mark(); + int indexMark = scratch.indexMark(); + try { + double[] xs = scratch.buffer(size); + double[] zs = scratch.buffer(size); + double[] lows = scratch.buffer(size); + double[] highs = scratch.buffer(size); + Arrays.fill(values, 0, size, 0D); for (int index = 0; index < size; index++) { - NoiseBounds bounds = gridSampleBounds(terrainEngine, frozenInterpolators[group], group, - generators, xs[index], zs[index]); - lows[index] = bounds.min(); - highs[index] = bounds.max(); - if (!Double.isFinite(lows[index]) || !Double.isFinite(highs[index])) { - averageGeneratorHeights(generators, lows[index], highs[index], xs[index], zs[index], seed); - } - if (lows[index] != highs[index]) { - active[activeCount] = index; - activeXs[activeCount] = xs[index]; - activeZs[activeCount++] = zs[index]; - } else { - for (int generator = 0; generator < generators.length; generator++) { - groupHeights[index] += lows[index]; - } - } + xs[index] = RiverFootprint.unpackX(missing[index]); + zs[index] = RiverFootprint.unpackZ(missing[index]); } - for (IrisGenerator generator : generators) { - if (activeCount == 0) { - break; + TerrainProgram program = terrainProgram(); + long evaluations = 0L; + for (int group = 0; group < frozenInterpolators.length; group++) { + if (frozenGenerators[group].length == 0) { + continue; } - generator.getHeights(activeXs, activeZs, seed, noise, activeCount); - if (GenerationCounter.ENABLED) { - GenerationCounter.GENERATOR_EVALUATIONS.add(activeCount); - } - for (int sample = 0; sample < activeCount; sample++) { - int index = active[sample]; - if (!Double.isFinite(noise[sample])) { - throw new IllegalStateException("Terrain generator noise was not finite at " + xs[index] + "," + zs[index] - + " (generator=" + generator.getLoadKey() + ", noise=" + noise[sample] - + ", bounds=" + lows[index] + ".." + highs[index] + ")."); - } - groupHeights[index] += M.lerp(lows[index], highs[index], noise[sample]); + for (int index = 0; index < size; index++) { + NoiseBounds bounds = gridSampleBounds(terrainEngine, frozenInterpolators[group], group, + frozenGenerators[group], xs[index], zs[index]); + lows[index] = bounds.min(); + highs[index] = bounds.max(); } + evaluations += program.accumulate(group, scratch, xs, zs, size, lows, highs, values); } + GeneratorEvaluations.batched(evaluations); for (int index = 0; index < size; index++) { - values[index] += groupHeights[index] / generators.length; + values[index] = finishBaseHeight(xs[index], zs[index], values[index]); } - } - for (int index = 0; index < size; index++) { - double height = Math.max(Math.min(values[index] + fluidHeight + overlayStream.get(xs[index], zs[index]), - terrainEngine.getHeight()), 0); - values[index] = imageMapRuntime.sampleTerrainHeight(xs[index], zs[index], height); + } finally { + scratch.reset(mark); + scratch.resetIndices(indexMark); } } - private void sampleUnblendedHeightBatch(long[] coordinates, int count, double[] target) { - if (terrain3D == null) { - sampleBaseHeightBatch(coordinates, count, target); + 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++) { + if ((missing[index >>> 6] & (1L << (index & 63))) != 0L) { + values[index] = sampleUnblendedNaturalTerrainHeight(originX + (index & 15), originZ + (index >> 4)); + } + } return; } - ((CachedDoubleStream2D) unblendedNaturalHeightStream).getBatch(coordinates, count, target, (missing, size, values) -> { - long[] required = new long[size]; - int requiredCount = 0; - for (int index = 0; index < size; index++) { - if (terrain3D.requiresBaseHeight(RiverFootprint.unpackX(missing[index]), RiverFootprint.unpackZ(missing[index]))) { - required[requiredCount++] = missing[index]; + TerrainBatchScratch scratch = BATCH_SCRATCH.get(); + int mark = scratch.mark(); + int indexMark = scratch.indexMark(); + try { + double[] xs = scratch.buffer(256); + double[] zs = scratch.buffer(256); + double[] lows = scratch.buffer(256); + double[] highs = scratch.buffer(256); + double[] heights = scratch.buffer(256); + int[] columns = scratch.indices(256); + int count = 0; + for (int index = 0; index < 256; index++) { + 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 (requiredCount > 0) { - sampleBaseHeightBatch(required, requiredCount, new double[requiredCount]); + if (count == 0) { + return; } - for (int index = 0; index < size; index++) { - values[index] = sampleTerrain3DHeight(RiverFootprint.unpackX(missing[index]), RiverFootprint.unpackZ(missing[index])); + 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, count, lows, highs)) { + continue; + } + } else { + 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(); + } + } + evaluations += program.accumulate(group, scratch, xs, zs, count, lows, highs, heights); } - }); + 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); + } + } + + private boolean chunkCornerBounds(GridBoundsCache cache, int group, int grid, int originX, int originZ, + double[] xs, double[] zs, int count, double[] lows, double[] highs) { + int cells = 16 / grid; + int stride = cells + 1; + long[] packed = new long[stride * stride]; + boolean silent = true; + for (int row = 0; row < stride; row++) { + for (int column = 0; column < stride; column++) { + long corner = cornerBounds(cache, terrainEngine, frozenInterpolators[group], group, + frozenGenerators[group], originX + column * grid, originZ + row * grid); + packed[row * stride + column] = corner; + silent &= silentBounds(corner); + } + } + if (silent) { + return false; + } + for (int index = 0; index < count; index++) { + double x = xs[index]; + double z = zs[index]; + int gx = ((int) Math.floor(x)) & ~(grid - 1); + int gz = ((int) Math.floor(z)) & ~(grid - 1); + double fx = (x - gx) / grid; + double fz = (z - gz) / grid; + int cornerIndex = ((gz - originZ) / grid) * stride + (gx - originX) / grid; + long b00 = packed[cornerIndex]; + if (fx == 0D && fz == 0D) { + lows[index] = boundsLow(b00); + highs[index] = boundsHigh(b00); + } else if (fz == 0D) { + long b10 = packed[cornerIndex + 1]; + lows[index] = biLerp(boundsLow(b00), boundsLow(b10), boundsLow(b00), boundsLow(b10), fx, fz); + highs[index] = biLerp(boundsHigh(b00), boundsHigh(b10), boundsHigh(b00), boundsHigh(b10), fx, fz); + } else if (fx == 0D) { + long b01 = packed[cornerIndex + stride]; + lows[index] = biLerp(boundsLow(b00), boundsLow(b00), boundsLow(b01), boundsLow(b01), fx, fz); + highs[index] = biLerp(boundsHigh(b00), boundsHigh(b00), boundsHigh(b01), boundsHigh(b01), fx, fz); + } else { + long b10 = packed[cornerIndex + 1]; + long b01 = packed[cornerIndex + stride]; + long b11 = packed[cornerIndex + stride + 1]; + lows[index] = biLerp(boundsLow(b00), boundsLow(b10), boundsLow(b01), boundsLow(b11), fx, fz); + highs[index] = biLerp(boundsHigh(b00), boundsHigh(b10), boundsHigh(b01), boundsHigh(b11), fx, fz); + } + } + return true; + } + + private 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; + } + 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()); + } + + private double finishBaseHeight(double x, double z, double interpolated) { + double height = Math.max(Math.min(interpolated + fluidHeight + overlayStream.get(x, z), terrainEngine.getHeight()), 0); + return imageMapRuntime.sampleTerrainHeight(x, z, height); + } + + private TerrainProgram terrainProgram() { + TerrainProgram program = terrainProgram; + if (program == null) { + synchronized (this) { + program = terrainProgram; + if (program == null) { + program = TerrainProgram.compile(frozenGenerators, terrainEngine.getSeedManager().getHeight() + 239945); + terrainProgram = program; + } + } + } + return program; + } + + private void sampleNaturalBatch(long[] coordinates, int count, double[] heights) { + long[] required = new long[count]; + int requiredCount = 0; + for (int index = 0; index < count; index++) { + int x = RiverFootprint.unpackX(coordinates[index]); + int z = RiverFootprint.unpackZ(coordinates[index]); + if (bakedColumn(BakePlane.NATURAL_HEIGHT, x, z) + || terrain3D != null && !terrain3D.requiresBaseHeight(x, z) + || bakedColumn(BakePlane.BASE_HEIGHT, x, z)) { + continue; + } + required[requiredCount++] = coordinates[index]; + } + if (requiredCount > 0) { + double[] baseHeights = new double[requiredCount]; + long revision = ProvisionalSampling.revision(); + resolveBaseHeightBatch(required, requiredCount, baseHeights); + if (bakeCache.memoAllowed()) { + for (int index = 0; index < requiredCount; index++) { + bakeCache.storeDouble(BakePlane.BASE_HEIGHT, RiverFootprint.unpackX(required[index]), + RiverFootprint.unpackZ(required[index]), baseHeights[index], revision); + } + } + } + for (int index = 0; index < count; index++) { + heights[index] = naturalHeightStream.getDouble(RiverFootprint.unpackX(coordinates[index]), RiverFootprint.unpackZ(coordinates[index])); + } + } + + private IrisBiome proceduralBaseBiome(double x, double z) { + return implode(inferredByType.get(bridgeStream.get(x, z)).get(x, z), x, z); + } + + 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 { private final boolean batch = supportsHeightBatch(); + private volatile boolean bakeReserved; @Override public double sample(int blockX, int blockZ) { + reserveBake(); return naturalHeightStream.getDouble(blockX, blockZ); } @@ -899,18 +1127,165 @@ public class IrisComplex implements DataProvider { public void sampleBatch(long[] coordinates, int count, double[] heights) { if (!batch) { IrisHydrologyNaturalHeightProvider.super.sampleBatch(coordinates, count, heights); - } else if (transitionDisplacement == null) { - sampleUnblendedHeightBatch(coordinates, count, heights); - } else { - ((CachedDoubleStream2D) naturalHeightStream).getBatch(coordinates, count, heights, (missing, size, values) -> { - sampleUnblendedHeightBatch(missing, size, values); - for (int index = 0; index < size; index++) { - values[index] = transitionDisplacement.height(RiverFootprint.unpackX(missing[index]), - RiverFootprint.unpackZ(missing[index]), values[index]); - } - }); + return; + } + reserveBake(); + sampleNaturalBatch(coordinates, count, heights); + } + + private void reserveBake() { + if (bakeReserved) { + return; + } + bakeReserved = true; + reserveHydrologyBake(HydrologyNoiseCacheBudget.SHARED); + } + } + + private final class BakeSource implements PlaneSource { + @Override + 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 -> bakeBaseHeights(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 + public ColumnBake assemble(ChunkPlanes planes) { + return assembleColumnBake(planes.chunkX(), planes.chunkZ()); + } + + @Override + public boolean mainThread() { + return terrainEngine.getPlatformHooks().isMainThread(); + } + + 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++) { + if ((missing[index >>> 6] & (1L << (index & 63))) != 0L) { + values[index] = source.get(originX + (index & 15), originZ + (index >> 4)); + } + } + } + + 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(); + if (!main && !ChunkBakeCache.inSparseScope()) { + bakeCache.complete(BakePlane.NATURAL_HEIGHT, chunkX, chunkZ); + } + for (int index = 0; index < 256; index++) { + 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); + } + } + } + } + + public Object bakePlane(BakePlane plane, int chunkX, int chunkZ) { + return bakeCache.complete(plane, chunkX, chunkZ); + } + + public long[] bakeCensus() { + return bakeCache.census(); + } + + public ColumnBake columnBake(int chunkX, int chunkZ) { + return bakeCache.get(chunkX, chunkZ); + } + + public ColumnBake columnBakeIfPresent(int chunkX, int chunkZ) { + return bakeCache.getIfPresent(chunkX, chunkZ); + } + + public IrisBiome biome(int id) { + return id < 0 ? null : biomeRegistry.value(id); + } + + public short biomeId(IrisBiome biome) { + return biome == null ? -1 : biomeRegistry.id(biome); + } + + public IrisRegion region(int id) { + return id < 0 ? null : regionRegistry.value(id); + } + + public short regionId(IrisRegion region) { + return region == null ? -1 : regionRegistry.id(region); + } + + public NativeBlockState blockState(int id) { + return id < 0 ? null : blockRegistry.value(id); + } + + private ColumnBake assembleColumnBake(int chunkX, int chunkZ) { + double[] height = (double[]) bakeCache.complete(BakePlane.HEIGHT, chunkX, chunkZ); + int[] rounded = new int[256]; + SimdSupport.kernels().roundToInt(height, rounded, 256); + int originX = chunkX << 4; + int originZ = chunkZ << 4; + float[] fluidSurface = new float[256]; + Terrain3DColumn[] terrainColumns = new Terrain3DColumn[256]; + for (int index = 0; index < 256; index++) { + int x = originX + (index & 15); + int z = originZ + (index >> 4); + fluidSurface[index] = (float) riverWaterSurfaceStream.getDouble(x, z); + terrainColumns[index] = terrainColumn(x, z); + } + double[] apron = new double[ColumnBake.APRON_WIDTH * ColumnBake.APRON_WIDTH]; + for (int row = 0; row < ColumnBake.APRON_WIDTH; row++) { + for (int column = 0; column < ColumnBake.APRON_WIDTH; column++) { + apron[row * ColumnBake.APRON_WIDTH + column] = heightStream.getDouble(originX - 1 + column, originZ - 1 + row); + } + } + return new ColumnBake(new ColumnBake.Planes(chunkX, chunkZ, + ids((Object[]) bakeCache.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.complete(BakePlane.ROCK, chunkX, chunkZ), blockRegistry), + ids((Object[]) bakeCache.complete(BakePlane.FLUID, chunkX, chunkZ), blockRegistry), + terrainColumns, + apron)); + } + + @SuppressWarnings("unchecked") + private static short[] ids(Object[] values, BakeRegistry registry) { + short[] ids = new short[values.length]; + for (int index = 0; index < values.length; index++) { + ids[index] = values[index] == null ? -1 : registry.id((T) values[index]); + } + return ids; } private double sampleUnblendedNaturalTerrainHeight(double x, double z) { @@ -993,29 +1368,29 @@ public class IrisComplex implements DataProvider { IrisBiome emptyBiome ) { return switch (inferredType) { - case CAVE -> engine.getDimension().getCaveBiomeStyle() + case CAVE -> new RarityStream<>(engine.getDimension().getCaveBiomeStyle() .create(rng.nextParallelRNG(InferredType.CAVE.ordinal()), getData()).stream() .zoom(engine.getDimension().getBiomeZoom()) - .zoom(region.getCaveBiomeZoom()) - .selectRarity(loadInferredBiomes(region.getCaveBiomes(), InferredType.CAVE)) + .zoom(region.getCaveBiomeZoom()), + loadInferredBiomes(region.getCaveBiomes(), InferredType.CAVE)) .onNull(emptyBiome); - case LAND -> engine.getDimension().getLandBiomeStyle() + case LAND -> new RarityStream<>(engine.getDimension().getLandBiomeStyle() .create(rng.nextParallelRNG(InferredType.LAND.ordinal()), getData()).stream() .zoom(engine.getDimension().getBiomeZoom()) .zoom(engine.getDimension().getLandZoom()) - .zoom(region.getLandBiomeZoom()) - .selectRarity(loadInferredBiomes(region.getLandBiomes(), InferredType.LAND)); - case SEA -> engine.getDimension().getSeaBiomeStyle() + .zoom(region.getLandBiomeZoom()), + loadInferredBiomes(region.getLandBiomes(), InferredType.LAND)); + case SEA -> new RarityStream<>(engine.getDimension().getSeaBiomeStyle() .create(rng.nextParallelRNG(InferredType.SEA.ordinal()), getData()).stream() .zoom(engine.getDimension().getBiomeZoom()) .zoom(engine.getDimension().getSeaZoom()) - .zoom(region.getSeaBiomeZoom()) - .selectRarity(loadInferredBiomes(region.getSeaBiomes(), InferredType.SEA)); - case SHORE -> engine.getDimension().getShoreBiomeStyle() + .zoom(region.getSeaBiomeZoom()), + loadInferredBiomes(region.getSeaBiomes(), InferredType.SEA)); + case SHORE -> new RarityStream<>(engine.getDimension().getShoreBiomeStyle() .create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), getData()).stream() .zoom(engine.getDimension().getBiomeZoom()) - .zoom(region.getShoreBiomeZoom()) - .selectRarity(loadInferredBiomes(region.getShoreBiomes(), InferredType.SHORE)); + .zoom(region.getShoreBiomeZoom()), + loadInferredBiomes(region.getShoreBiomes(), InferredType.SHORE)); }; } @@ -1646,7 +2021,7 @@ public class IrisComplex implements DataProvider { return biomeKey == null ? sample.parentBiomeKey() : biomeKey; } - private IrisBiome fixBiomeType(Double height, IrisBiome biome, IrisRegion region, Double x, Double z, double fluidHeight) { + private IrisBiome fixBiomeType(double height, IrisBiome biome, IrisRegion region, double x, double z, double fluidHeight) { IrisBiome resolved = resolveSurfaceBiome( height, biome, @@ -1762,7 +2137,19 @@ public class IrisComplex implements DataProvider { double hi = sampledBounds.max(); double lo = sampledBounds.min(); - return averageGeneratorHeights(generators, lo, hi, x, z, seed + 239945); + return groupHeight(interpolatorIndex, generators, lo, hi, x, z, seed + 239945); + } + + private double groupHeight(int group, IrisGenerator[] generators, double low, double high, double x, double z, long heightSeed) { + TerrainProgram program = terrainProgram; + if (program == null && terrainEngine != null) { + program = terrainProgram(); + } + if (program == null || program.heightSeed() != heightSeed || group >= program.groups() + || program.groupSize(group) != generators.length || generators != frozenGenerators[group]) { + return averageGeneratorHeights(generators, low, high, x, z, heightSeed); + } + return program.average(group, BATCH_SCRATCH.get(), low, high, x, z); } private NoiseBounds gridSampleBounds(Engine engine, IrisInterpolator interpolator, int interpolatorIndex, IrisGenerator[] generators, double x, double z) { @@ -1965,7 +2352,7 @@ public class IrisComplex implements DataProvider { lo = biLerp(boundsLow(b00), boundsLow(b10), boundsLow(b01), boundsLow(b11), fx, fz); hi = biLerp(boundsHigh(b00), boundsHigh(b10), boundsHigh(b01), boundsHigh(b11), fx, fz); } - h += averageGeneratorHeights(generators, lo, hi, x, z, heightSeed); + h += groupHeight(interpolatorIndex, generators, lo, hi, x, z, heightSeed); } return h; @@ -2191,6 +2578,7 @@ public class IrisComplex implements DataProvider { } return height / generators.length; } + GeneratorEvaluations.scalar(generators.length); if (GenerationCounter.ENABLED) { GenerationCounter.GENERATOR_EVALUATIONS.add(generators.length); } @@ -2264,7 +2652,7 @@ public class IrisComplex implements DataProvider { return computed; } - private IrisBiome implode(IrisBiome b, Double x, Double z) { + private IrisBiome implode(IrisBiome b, double x, double z) { if (b.getChildren().isEmpty()) { return b; } @@ -2272,7 +2660,7 @@ public class IrisComplex implements DataProvider { return implode(b, x, z, 3); } - private IrisBiome implode(IrisBiome b, Double x, Double z, int max) { + private IrisBiome implode(IrisBiome b, double x, double z, int max) { if (max < 0) { return b; } @@ -2281,7 +2669,7 @@ public class IrisComplex implements DataProvider { return b; } - CNG childCell = b.getChildrenGenerator(rng, 123, b.getChildShrinkFactor()); + CNG childCell = b.getChildrenGenerator(childStyleSeed, b.getChildShrinkFactor()); ChildSelectionPlan childSelectionPlan = resolveChildSelectionPlan(b); IrisBiome biome = childSelectionPlan.select(childCell, x, z).withInferredType(b.getInferredType()); if (biome == b && hasFixedNoise(b.getChildStyle())) { @@ -2504,7 +2892,8 @@ public class IrisComplex implements DataProvider { this.engine = engine; this.generators = generators; this.cachedBounds = cachedBounds; - this.integerCoordinates = complex.baseBiomeStream instanceof CachedStream2D; + this.integerCoordinates = complex.baseBiomeStream instanceof BakedStream + || complex.baseBiomeStream instanceof CachedStream2D; this.inUse = true; if (sampleSource.get() != complex.baseBiomeStream) { sampleCache.clear(); @@ -2531,7 +2920,12 @@ public class IrisComplex implements DataProvider { double sampleZ = integerCoordinates ? (int) zz : zz; IrisBiome bx = sampleCache.get(sampleX, sampleZ); if (bx == null) { - bx = complex.baseBiomeStream.get(xx, zz); + ChunkBakeCache.enterSparse(); + try { + bx = complex.baseBiomeStream.get(xx, zz); + } finally { + ChunkBakeCache.exitSparse(); + } sampleCache.put(sampleX, sampleZ, bx); } @@ -2601,62 +2995,6 @@ public class IrisComplex implements DataProvider { } } - private final class TerrainHeightStream extends CachedDoubleStream2D { - private TerrainHeightStream(Engine engine, int cacheSize) { - super("heightStream", engine, ProceduralStream.ofDouble(IrisComplex.this::resolveHydrologyTerrainHeight), cacheSize); - } - - @Override - public double getDouble(double x, double z) { - return terrainEngine.getPlatformHooks().isMainThread() - ? nonblockingTerrainHeight(x, z) : super.getDouble(x, z); - } - - @Override - public Double get(double x, double y, double z) { - return getDouble(x, z); - } - - @Override - public double getDouble(double x, double y, double z) { - return getDouble(x, z); - } - - @Override - public void fillChunkDoubles(int worldX, int worldZ, double[] target) { - boolean mainThread = terrainEngine.getPlatformHooks().isMainThread(); - if (!mainThread && (worldX & 15) == 0 && (worldZ & 15) == 0) { - super.fillChunkDoubles(worldX, worldZ, target); - return; - } - if (target == null || target.length != 256) { - throw new IllegalArgumentException("Expected a 16x16 target array."); - } - for (int index = 0; index < 256; index++) { - int x = worldX + (index & 15); - int z = worldZ + (index >> 4); - target[index] = mainThread ? nonblockingTerrainHeight(x, z) : super.getDouble(x, z); - } - } - - @Override - public void fillChunkRaw(int worldX, int worldZ, Object[] target) { - boolean mainThread = terrainEngine.getPlatformHooks().isMainThread(); - if (!mainThread && (worldX & 15) == 0 && (worldZ & 15) == 0) { - super.fillChunkRaw(worldX, worldZ, target); - return; - } - if (target == null || target.length != 256) { - throw new IllegalArgumentException("Expected a 16x16 target array."); - } - for (int index = 0; index < 256; index++) { - int x = worldX + (index & 15); - int z = worldZ + (index >> 4); - target[index] = mainThread ? nonblockingTerrainHeight(x, z) : super.getDouble(x, z); - } - } - } - static final class ChildSelectionPlan { private final IrisRaritySelection selection; @@ -2707,6 +3045,8 @@ public class IrisComplex implements DataProvider { hydrologyRuntime.close(); } releaseHydrologyNoiseCaches(); + releaseHydrologyBake(); + bakeCache.clear(); if (transitionDisplacement != null) { transitionDisplacement.clear(); } diff --git a/core/src/main/java/art/arcane/iris/generation/stream/BakedDoubleStream.java b/core/src/main/java/art/arcane/iris/generation/stream/BakedDoubleStream.java new file mode 100644 index 000000000..5578bb160 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/stream/BakedDoubleStream.java @@ -0,0 +1,92 @@ +package art.arcane.iris.generation.stream; + +import art.arcane.iris.generation.bake.BakePlane; +import art.arcane.iris.generation.bake.ChunkBakeCache; +import art.arcane.iris.generation.bake.ChunkPlanes; +import art.arcane.volmlib.util.stream.BasicStream; +import art.arcane.volmlib.util.stream.ProceduralStream; + +import java.util.Objects; + +public final class BakedDoubleStream extends BasicStream implements ChunkFillableStream2D, ChunkFillableDoubleStream2D { + private final ChunkBakeCache cache; + private final BakePlane plane; + private final ProceduralStream scalar; + + public BakedDoubleStream(ChunkBakeCache cache, BakePlane plane, ProceduralStream scalar) { + super(); + this.cache = Objects.requireNonNull(cache, "Bake cache"); + this.plane = Objects.requireNonNull(plane, "Bake plane"); + this.scalar = Objects.requireNonNull(scalar, "Scalar source"); + } + + @Override + public Double get(double x, double z) { + return getDouble(x, z); + } + + @Override + public double getDouble(double x, double z) { + int blockX = (int) x; + int blockZ = (int) z; + 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); + } + if (!cache.memoAllowed()) { + return scalar.getDouble(blockX, blockZ); + } + long revision = ProvisionalSampling.revision(); + double value = scalar.getDouble(blockX, blockZ); + cache.storeDouble(plane, blockX, blockZ, value, revision); + return value; + } + + @Override + public Double get(double x, double y, double z) { + return scalar.get(x, y, z); + } + + @Override + public double getDouble(double x, double y, double z) { + return scalar.getDouble(x, y, z); + } + + @Override + public double toDouble(Double t) { + return t; + } + + @Override + public Double fromDouble(double d) { + return d; + } + + @Override + public void fillChunkDoubles(int worldX, int worldZ, double[] target) { + if (target == null || target.length != 256) { + throw new IllegalArgumentException("Expected a 16x16 target array."); + } + int chunkX = worldX >> 4; + int chunkZ = worldZ >> 4; + 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] = getDouble(originX + (index & 15), originZ + (index >> 4)); + } + } + + @Override + public void fillChunkRaw(int worldX, int worldZ, Object[] target) { + double[] values = new double[256]; + fillChunkDoubles(worldX, worldZ, values); + for (int index = 0; index < 256; index++) { + target[index] = values[index]; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/stream/BakedHeightStream.java b/core/src/main/java/art/arcane/iris/generation/stream/BakedHeightStream.java new file mode 100644 index 000000000..70efa9736 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/stream/BakedHeightStream.java @@ -0,0 +1,100 @@ +package art.arcane.iris.generation.stream; + +import art.arcane.iris.generation.bake.BakePlane; +import art.arcane.iris.generation.bake.ChunkBakeCache; +import art.arcane.iris.generation.bake.ChunkPlanes; +import art.arcane.volmlib.util.function.NoiseProvider; +import art.arcane.volmlib.util.stream.BasicStream; + +import java.util.Objects; +import java.util.function.BooleanSupplier; + +public final class BakedHeightStream extends BasicStream implements ChunkFillableStream2D, ChunkFillableDoubleStream2D { + private final ChunkBakeCache cache; + private final BooleanSupplier mainThread; + private final NoiseProvider nonblocking; + private final NoiseProvider resolved; + + public BakedHeightStream(ChunkBakeCache cache, BooleanSupplier mainThread, NoiseProvider nonblocking, NoiseProvider resolved) { + super(); + this.cache = Objects.requireNonNull(cache, "Bake cache"); + this.mainThread = Objects.requireNonNull(mainThread, "Main thread check"); + this.nonblocking = Objects.requireNonNull(nonblocking, "Nonblocking height"); + this.resolved = Objects.requireNonNull(resolved, "Resolved height"); + } + + @Override + public Double get(double x, double z) { + return getDouble(x, z); + } + + @Override + public double getDouble(double x, double z) { + if (mainThread.getAsBoolean()) { + return nonblocking.noise(x, z); + } + return column((int) x, (int) z, !ChunkBakeCache.inSparseScope()); + } + + @Override + public Double get(double x, double y, double z) { + return getDouble(x, z); + } + + @Override + public double getDouble(double x, double y, double z) { + return getDouble(x, z); + } + + @Override + public double toDouble(Double t) { + return t; + } + + @Override + public Double fromDouble(double d) { + return d; + } + + @Override + public void fillChunkDoubles(int worldX, int worldZ, double[] target) { + if (target == null || target.length != 256) { + throw new IllegalArgumentException("Expected a 16x16 target array."); + } + boolean main = mainThread.getAsBoolean(); + 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, memo); + } + } + + @Override + public void fillChunkRaw(int worldX, int worldZ, Object[] target) { + double[] values = new double[256]; + fillChunkDoubles(worldX, worldZ, values); + for (int index = 0; index < 256; index++) { + target[index] = values[index]; + } + } + + private double column(int x, int z, 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; + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/stream/BakedStream.java b/core/src/main/java/art/arcane/iris/generation/stream/BakedStream.java new file mode 100644 index 000000000..1a0594b73 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/stream/BakedStream.java @@ -0,0 +1,71 @@ +package art.arcane.iris.generation.stream; + +import art.arcane.iris.generation.bake.BakePlane; +import art.arcane.iris.generation.bake.ChunkBakeCache; +import art.arcane.iris.generation.bake.ChunkPlanes; +import art.arcane.volmlib.util.stream.BasicStream; +import art.arcane.volmlib.util.stream.ProceduralStream; + +import java.util.Objects; + +public final class BakedStream extends BasicStream implements ChunkFillableStream2D { + private final ChunkBakeCache cache; + private final BakePlane plane; + private final ProceduralStream scalar; + + public BakedStream(ChunkBakeCache cache, BakePlane plane, ProceduralStream scalar) { + super(); + this.cache = Objects.requireNonNull(cache, "Bake cache"); + this.plane = Objects.requireNonNull(plane, "Bake plane"); + this.scalar = Objects.requireNonNull(scalar, "Scalar source"); + } + + @Override + @SuppressWarnings("unchecked") + public T get(double x, double z) { + int blockX = (int) x; + int blockZ = (int) z; + 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); + } + if (!cache.memoAllowed()) { + return scalar.get(blockX, blockZ); + } + long revision = ProvisionalSampling.revision(); + T value = scalar.get(blockX, blockZ); + cache.storeObject(plane, blockX, blockZ, value, revision); + return value; + } + + @Override + public T get(double x, double y, double z) { + return scalar.get(x, y, z); + } + + @Override + public double toDouble(T t) { + return scalar.toDouble(t); + } + + @Override + public T fromDouble(double d) { + return scalar.fromDouble(d); + } + + @Override + public void fillChunkRaw(int worldX, int worldZ, Object[] target) { + int chunkX = worldX >> 4; + int chunkZ = worldZ >> 4; + 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] = get(originX + (index & 15), originZ + (index >> 4)); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/stream/RarityStream.java b/core/src/main/java/art/arcane/iris/generation/stream/RarityStream.java new file mode 100644 index 000000000..2d85e06da --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/stream/RarityStream.java @@ -0,0 +1,64 @@ +package art.arcane.iris.generation.stream; + +import art.arcane.volmlib.util.math.Rarity; +import art.arcane.volmlib.util.stream.BasicStream; +import art.arcane.volmlib.util.stream.ProceduralStream; + +import java.util.List; +import java.util.Objects; + +public final class RarityStream extends BasicStream { + private final ProceduralStream noise; + private final Object[] options; + private final double[] cumulative; + + public RarityStream(ProceduralStream noise, List options) { + super(); + this.noise = Objects.requireNonNull(noise, "Rarity noise"); + this.options = options.toArray(); + this.cumulative = new double[this.options.length]; + double buffer = 0; + for (int index = 0; index < this.options.length; index++) { + buffer += 1d / Rarity.get(this.options[index]); + cumulative[index] = buffer; + } + } + + @Override + public T get(double x, double z) { + return pick(noise.getDouble(x, z)); + } + + @Override + public T get(double x, double y, double z) { + return pick(noise.getDouble(x, y, z)); + } + + @Override + public double toDouble(T t) { + return 0; + } + + @Override + public T fromDouble(double d) { + return null; + } + + @SuppressWarnings("unchecked") + public T pick(double noiseValue) { + int count = options.length; + if (count == 0) { + return null; + } + if (count == 1) { + return (T) options[0]; + } + double threshold = cumulative[count - 1] * noiseValue; + for (int index = 0; index < count; index++) { + if (cumulative[index] >= threshold) { + return (T) options[index]; + } + } + return (T) options[count - 1]; + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/terrain/RegionSelection.java b/core/src/main/java/art/arcane/iris/generation/terrain/RegionSelection.java index 99a1d2678..9c6234f9c 100644 --- a/core/src/main/java/art/arcane/iris/generation/terrain/RegionSelection.java +++ b/core/src/main/java/art/arcane/iris/generation/terrain/RegionSelection.java @@ -3,6 +3,7 @@ package art.arcane.iris.generation.terrain; import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.image.IrisImageMapRuntime; import art.arcane.iris.generation.image.IrisImageMapApplication; +import art.arcane.iris.generation.stream.RarityStream; import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.studio.generation.BiomeBuffetLayout; import art.arcane.volmlib.util.collection.KList; @@ -24,7 +25,7 @@ public final class RegionSelection { regions.add(region); } } - ProceduralStream selected = options.style().selectRarity(regions); + ProceduralStream selected = new RarityStream<>(options.style(), regions); if (!options.imageMaps().has(IrisImageMapApplication.REGION)) { return selected; } diff --git a/core/src/main/java/art/arcane/iris/structure/object/IrisObject.java b/core/src/main/java/art/arcane/iris/structure/object/IrisObject.java index 239bb6e3a..9b0d1fd52 100644 --- a/core/src/main/java/art/arcane/iris/structure/object/IrisObject.java +++ b/core/src/main/java/art/arcane/iris/structure/object/IrisObject.java @@ -61,6 +61,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.Map; import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.BiConsumer; @@ -144,6 +145,7 @@ public class IrisObject extends IrisRegistrant { protected transient AtomicCache aabb = new AtomicCache<>(); transient final AtomicCache> surfaceSupportOffsets = new AtomicCache<>(); transient final AtomicCache floatingFootprint = new AtomicCache<>(); + transient final ConcurrentHashMap placementBakes = new ConcurrentHashMap<>(); @Getter @Setter private transient TreeGrounding treeGrounding; diff --git a/core/src/main/java/art/arcane/iris/structure/object/IrisObjectPlacementRunner.java b/core/src/main/java/art/arcane/iris/structure/object/IrisObjectPlacementRunner.java index 6a7a4b651..36d05ab82 100644 --- a/core/src/main/java/art/arcane/iris/structure/object/IrisObjectPlacementRunner.java +++ b/core/src/main/java/art/arcane/iris/structure/object/IrisObjectPlacementRunner.java @@ -30,7 +30,6 @@ import art.arcane.iris.generation.decoration.IrisVacuumSettings; import art.arcane.iris.world.entity.IrisMarker; import art.arcane.iris.pack.loading.IrisData; -import art.arcane.iris.generation.runtime.GenerationFailures; import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.Engine; import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume; @@ -52,6 +51,7 @@ import art.arcane.volmlib.util.matter.MatterMarker; import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap; import java.util.Comparator; +import java.util.IdentityHashMap; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -475,127 +475,53 @@ final class IrisObjectPlacementRunner { int vacuumLowest = Integer.MAX_VALUE; int vacuumHighest = Integer.MIN_VALUE; self.readLock.lock(); - - KMap markers = null; - try { - VectorMap blocks = self.blocks; - VectorMap states = self.states; - TreeGrounding tree = self.getTreeGrounding(); - // Zero-tile objects are the common case: skip the two Key allocations VectorMap#get needs to prove null. - boolean hasStates = !states.isEmpty(); + ObjectPlacementBake bake = ObjectPlacementBake.of(self, spin, ceilingHang, translating ? translateOffset : null); + PlacementStates palette = new PlacementStates(bake.palette(), config.getRotation(), spinx, spiny, spinz, + preventingDecay, placer.isDebugSmartBore(), excludedStiltMaterials); + double[] exact = warped || hasEdits || eroding ? bake.exact(self, spin) : null; + String[] markers = config.getMarkers().isNotEmpty() && placer.getEngine() != null + ? resolveMarkers(config, rng, x, yv, z, rdata, bake) : null; + int[] tileBlocks = bake.tileBlocks(); + TileData[] tileValues = bake.tileValues(); + int tileCursor = 0; + int count = bake.count(); - if (config.getMarkers().isNotEmpty() && placer.getEngine() != null) { - markers = new KMap<>(); - KList list = StreamSupport.stream(blocks.keys().spliterator(), false) - .collect(KList.collector()); - // Marker selection persists into the mantle, so it must be seed-deterministic. - // Derive a side stream keyed on the placement position instead of consuming the - // caller's rng: consuming draws there would shift every later placement in the - // chunk and invalidate existing worlds/goldenhashes. - RNG markerRng = rng.nextParallelRNG(((((long) x) << 32) | (z & 0xFFFFFFFFL)) * 31L + yv); - - int markerIndex = 0; - for (IrisObjectMarker j : config.getMarkers()) { - IrisMarker marker = self.getLoader().getMarkerLoader().load(j.getMarker()); - int markerSalt = markerIndex++; - - if (marker == null || marker.isCompatExcluded()) { - continue; - } - - int max = j.getMaximumMarkers(); - for (IrisBlockVector i : list.shuffleCopy(markerRng.nextParallelRNG(markerSalt))) { - if (max <= 0) { - break; - } - - NativeBlockState data = blocks.get(i); - if (data == null) { - continue; - } - - for (NativeBlockState k : j.getMark(rdata)) { - if (max <= 0) { - break; - } - - if (j.isExact() ? k.matches(data) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(data))) { - boolean a = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 1, 0))); - boolean fff = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 2, 0))); - - if (!marker.isEmptyAbove() || (a && fff)) { - markers.put(i, j.getMarker()); - max--; - } - } - } - } - } - } - - VectorMap.Cursor cursor = blocks.cursor(); - while (cursor.next()) { - IrisBlockVector g = cursor.key(); - NativeBlockState d; + for (int index = 0; index < count; index++) { + int entry = bake.paletteIndex(index); TileData tile = null; + if (tileBlocks != null && tileCursor < tileBlocks.length && tileBlocks[tileCursor] == index) { + tile = tileValues[tileCursor++]; + } - try { - d = cursor.value(); - if (hasStates) { - tile = states.get(g); + if (stilting && palette.stiltLayer(entry)) { + int layer = bake.layerY(index); + if (layer < lowest) { + lowest = layer; } - } catch (Throwable e) { - GenerationFailures.rethrowEngineFailure(e); - IrisLogging.reportError(e); - IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (cme)"); - d = IrisObject.States.air(); - } - - if (d == null) { - IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (null)"); - d = IrisObject.States.air(); - } - - if (placer.isDebugSmartBore() && IrisObject.States.vair().equals(d)) { - d = IrisObject.States.vairDebug(); - } - - NativeBlockState data = d; - IrisBlockVector i = g.clone(); - spin.rotate(i); - if (ceilingHang) { - i.setY(-i.getBlockY()); - } - if (translating) { - i.add(translateOffset); - } - - if (stilting && IrisObjectShaping.isStiltLayerBlock(data)) { - if (i.getBlockY() < lowest) { - lowest = i.getBlockY(); - } - if (i.getBlockY() > topLayer) { - topLayer = i.getBlockY(); + if (layer > topLayer) { + topLayer = layer; } } - if (preventingDecay && IrisProceduralBlocks.hasProperty(data, "distance") && "false".equals(IrisProceduralBlocks.propertyValue(data, "persistent"))) { - data = data.withProperty("persistent", "true"); - } - + NativeBlockState data = palette.decayed(entry); + boolean edited = false; if (hasEdits) { + double exactX = exact[index * 3]; + double exactY = exact[index * 3 + 1]; + double exactZ = exact[index * 3 + 2]; for (IrisObjectReplace j : config.getEdit()) { if (rng.chance(j.getChance())) { for (NativeBlockState k : j.getFind(rdata)) { if (j.isExact() ? k.matches(data) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(data))) { - NativeBlockState newData = j.getReplace(rng, i.getX() + x, i.getY() + y, i.getZ() + z, rdata); + NativeBlockState newData = j.getReplace(rng, exactX + x, exactY + y, exactZ + z, rdata); boolean sameMaterial = IrisObjectShaping.materialKey(newData).equals(IrisObjectShaping.materialKey(data)); if (sameMaterial && !(newData.isCustom() || data.isCustom())) data = BlockDataMergeSupport.merge(data, newData); else data = newData; + edited = true; Optional t = j.getReplace().getTile(rng, x, y, z, rdata); if (t.isPresent()) { @@ -609,18 +535,21 @@ final class IrisObjectPlacementRunner { } } - data = config.getRotation().rotate(data, spinx, spiny, spinz); - if (data == null) { + StateFacts facts = edited ? palette.factsOf(data) : palette.facts(entry); + NativeBlockState rotated = facts.rotated(); + if (rotated == null) { continue; } - xx = x + (int) Math.round(i.getX()); - - int yy = y + (int) Math.round(i.getY()); - zz = z + (int) Math.round(i.getZ()); + xx = x + bake.offsetX(index); + int yy = y + bake.offsetY(index); + zz = z + bake.offsetZ(index); if (warped) { - xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getX() + x, i.getY() + y, i.getZ() + z); - zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getZ() + z, i.getY() + y, i.getX() + x); + double exactX = exact[index * 3]; + double exactY = exact[index * 3 + 1]; + double exactZ = exact[index * 3 + 2]; + xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, exactX + x, exactY + y, exactZ + z); + zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, exactZ + z, exactY + y, exactX + x); } if (paint) { @@ -628,8 +557,8 @@ final class IrisObjectPlacementRunner { if (surface == IrisPaintSurfaceProjection.MISSING) { continue; } - if (!B.isVineBlock(data)) { - yy = (int) Math.round(i.getY()) + Math.floorDiv(self.h, 2) + surface + yrand; + if (!B.isVineBlock(rotated)) { + yy = bake.offsetY(index) + Math.floorDiv(self.h, 2) + surface + yrand; } } @@ -637,13 +566,18 @@ final class IrisObjectPlacementRunner { continue; } - boolean canopy = tree != null && tree.isCanopy(g.getBlockX(), g.getBlockY(), g.getBlockZ()); + boolean canopy = bake.canopy(index); if (canopy && !placer.beginTreeCanopy(new BlockPosition(xx, yy, zz))) { continue; } try { - data = normalizePlacedWaterlogging(data, placer, xx, yy, zz, waterlogCandidate); + data = rotated; + if (facts.waterloggable()) { + data = waterlogCandidate || facts.authoredWaterlogged() + ? IrisProceduralBlocks.normalizeWaterlogging(data, placer.get(xx, yy, zz), waterlogCandidate) + : facts.dry(); + } if (!rawStructurePiece && B.isVineBlock(data)) { data = attachVineFaces(placer, data, xx, yy, zz); @@ -652,9 +586,7 @@ final class IrisObjectPlacementRunner { // Short-circuit order matters for cost only: the mantle read is paid solely // for vine blocks. Both operands are pure, so the value is unchanged. boolean wouldReplace = !rawStructurePiece && B.isVineBlock(data) && B.isSolid(placer.get(xx, yy, zz)); - String material = IrisObjectShaping.materialKey(data); - boolean air = material.equals("minecraft:air") || material.equals("minecraft:cave_air"); - boolean place = shouldPlaceObjectBlock(rawStructurePiece, air, wouldReplace); + boolean place = shouldPlaceObjectBlock(rawStructurePiece, facts.air(), wouldReplace); if (data.isCustom() || place) { placer.set(xx, yy, zz, data); @@ -668,8 +600,8 @@ final class IrisObjectPlacementRunner { if (tile != null) { placer.setTile(xx, yy, zz, tile); } - if (markers != null && markers.containsKey(g)) { - placer.setData(xx, yy, zz, new MatterMarker(markers.get(g))); + if (markers != null && markers[index] != null) { + placer.setData(xx, yy, zz, new MatterMarker(markers[index])); } if (listener != null) { listener.accept(new BlockPosition(xx, yy, zz), data); @@ -687,27 +619,21 @@ final class IrisObjectPlacementRunner { } } } - } finally { - self.readLock.unlock(); - } - if (grounding != null) { - for (TreeGrounding.Extension extension : grounding.extensions()) { - for (int rootY = extension.topY(); rootY >= extension.bottomY(); rootY--) { - NativeBlockState rootState = normalizePlacedWaterlogging(extension.state(), placer, - extension.x(), rootY, extension.z(), waterlogCandidate); - placer.set(extension.x(), rootY, extension.z(), rootState); - if (listener != null) { - listener.accept(new BlockPosition(extension.x(), rootY, extension.z()), rootState); + if (grounding != null) { + for (TreeGrounding.Extension extension : grounding.extensions()) { + for (int rootY = extension.topY(); rootY >= extension.bottomY(); rootY--) { + NativeBlockState rootState = normalizePlacedWaterlogging(extension.state(), placer, + extension.x(), rootY, extension.z(), waterlogCandidate); + placer.set(extension.x(), rootY, extension.z(), rootState); + if (listener != null) { + listener.accept(new BlockPosition(extension.x(), rootY, extension.z()), rootState); + } } } } - } - if (stilting) { - self.readLock.lock(); - try { - VectorMap blocks = self.blocks; + if (stilting) { IrisStiltSettings settings = stiltSettings; double erodeCentroidX = 0; @@ -715,93 +641,51 @@ final class IrisObjectPlacementRunner { double erodeMaxDist = 1; if (eroding) { int centroidCount = 0; - VectorMap.Cursor centroidCursor = blocks.cursor(); - while (centroidCursor.next()) { - IrisBlockVector rot = centroidCursor.key().clone(); - spin.rotate(rot); - if (translating) { - rot.add(translateOffset); - } - if (rot.getBlockY() == lowest) { - NativeBlockState bd = centroidCursor.value(); - if (bd != null && IrisObjectShaping.shouldStilt(bd, excludedStiltMaterials)) { - erodeCentroidX += rot.getX(); - erodeCentroidZ += rot.getZ(); - centroidCount++; - } + for (int index = 0; index < count; index++) { + if (bake.layerY(index) == lowest && palette.shouldStilt(bake.paletteIndex(index))) { + erodeCentroidX += exact[index * 3]; + erodeCentroidZ += exact[index * 3 + 2]; + centroidCount++; } } if (centroidCount > 0) { erodeCentroidX /= centroidCount; erodeCentroidZ /= centroidCount; } - VectorMap.Cursor spreadCursor = blocks.cursor(); - while (spreadCursor.next()) { - IrisBlockVector rot = spreadCursor.key().clone(); - spin.rotate(rot); - if (translating) { - rot.add(translateOffset); - } - if (rot.getBlockY() == lowest) { - NativeBlockState bd = spreadCursor.value(); - if (bd != null && IrisObjectShaping.shouldStilt(bd, excludedStiltMaterials)) { - double dx = rot.getX() - erodeCentroidX; - double dz = rot.getZ() - erodeCentroidZ; - double dist = Math.sqrt(dx * dx + dz * dz); - if (dist > erodeMaxDist) { - erodeMaxDist = dist; - } + for (int index = 0; index < count; index++) { + if (bake.layerY(index) == lowest && palette.shouldStilt(bake.paletteIndex(index))) { + double dx = exact[index * 3] - erodeCentroidX; + double dz = exact[index * 3 + 2] - erodeCentroidZ; + double dist = Math.sqrt(dx * dx + dz * dz); + if (dist > erodeMaxDist) { + erodeMaxDist = dist; } } } } - VectorMap.Cursor stiltCursor = blocks.cursor(); - while (stiltCursor.next()) { - IrisBlockVector g = stiltCursor.key(); - NativeBlockState sourceData; - try { - sourceData = stiltCursor.value(); - } catch (Throwable e) { - GenerationFailures.rethrowEngineFailure(e); - IrisLogging.reportError(e); - IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (stilt cme)"); - sourceData = IrisObject.States.air(); - } - - if (sourceData == null) { - IrisLogging.warn("Failed to read block node " + g.getBlockX() + "," + g.getBlockY() + "," + g.getBlockZ() + " in object " + self.getLoadKey() + " (stilt null)"); - sourceData = IrisObject.States.air(); - } - - if (!IrisObjectShaping.shouldStilt(sourceData, excludedStiltMaterials)) { + int targetLayer = ceilingHang ? topLayer : lowest; + for (int index = 0; index < count; index++) { + int entry = bake.paletteIndex(index); + if (!palette.shouldStilt(entry)) { continue; } - NativeBlockState d = sourceData; + NativeBlockState d; if (settings != null && settings.getPalette() != null) { d = config.getStiltSettings().getPalette().get(rng, x, y, z, rdata); } else { - String mat = IrisObjectShaping.materialKey(d); - if (mat.equals("minecraft:grass_block") || mat.equals("minecraft:mycelium") || mat.equals("minecraft:podzol") || mat.equals("minecraft:dirt_path")) { - d = IrisObject.States.dirt(); - } + d = palette.stiltDefault(entry); } - IrisBlockVector i = g.clone(); - spin.rotate(i); - if (ceilingHang) { - i.setY(-i.getBlockY()); - } - if (translating) { - i.add(translateOffset); - } - int targetLayer = ceilingHang ? topLayer : lowest; - if (i.getBlockY() != targetLayer) + if (bake.layerY(index) != targetLayer) continue; - d = config.getRotation().rotate(d, spinx, spiny, spinz); + d = palette.rotate(d); if (hasEdits) { + double exactX = exact[index * 3]; + double exactY = exact[index * 3 + 1]; + double exactZ = exact[index * 3 + 2]; for (IrisObjectReplace j : config.getEdit()) { if (rng.chance(j.getChance())) { for (NativeBlockState k : j.getFind(rdata)) { @@ -809,7 +693,7 @@ final class IrisObjectPlacementRunner { continue; } if (j.isExact() ? k.matches(d) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(d))) { - NativeBlockState newData = j.getReplace(rng, i.getX() + x, i.getY() + y, i.getZ() + z, rdata); + NativeBlockState newData = j.getReplace(rng, exactX + x, exactY + y, exactZ + z, rdata); if (IrisObjectShaping.materialKey(newData).equals(IrisObjectShaping.materialKey(d))) { d = BlockDataMergeSupport.merge(d, newData); @@ -822,15 +706,18 @@ final class IrisObjectPlacementRunner { } } - if (d == null || !IrisObjectShaping.shouldStilt(d, excludedStiltMaterials)) + if (d == null || !palette.shouldStiltPlaced(d)) continue; - xx = x + (int) Math.round(i.getX()); - zz = z + (int) Math.round(i.getZ()); + xx = x + bake.offsetX(index); + zz = z + bake.offsetZ(index); if (warped) { - xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getX() + x, i.getY() + y, i.getZ() + z); - zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, i.getZ() + z, i.getY() + y, i.getX() + x); + double exactX = exact[index * 3]; + double exactY = exact[index * 3 + 1]; + double exactZ = exact[index * 3 + 2]; + xx += surfaceWarp.fitDouble(-warpHalf, warpHalf, exactX + x, exactY + y, exactZ + z); + zz += surfaceWarp.fitDouble(-warpHalf, warpHalf, exactZ + z, exactY + y, exactX + x); } if (organic) { @@ -891,8 +778,6 @@ final class IrisObjectPlacementRunner { int highest = placer.getHighest(xx, zz, self.getLoader(), true); - - int lowerBound = highest - 1; if (settings != null) { lowerBound -= config.getStiltSettings().getOverStilt() - rng.i(0, config.getStiltSettings().getYRand()); @@ -901,8 +786,8 @@ final class IrisObjectPlacementRunner { } if (eroding) { - double dx = i.getX() - erodeCentroidX; - double dz = i.getZ() - erodeCentroidZ; + double dx = exact[index * 3] - erodeCentroidX; + double dz = exact[index * 3 + 2] - erodeCentroidZ; double normalizedDist = Math.sqrt(dx * dx + dz * dz) / erodeMaxDist; normalizedDist = Math.min(normalizedDist, 1.0); int totalDepth = (lowest + y) - lowerBound; @@ -933,11 +818,10 @@ final class IrisObjectPlacementRunner { } placer.set(xx, j, zz, normalizePlacedWaterlogging(d, placer, xx, j, zz, waterlogCandidate)); } - } - } finally { - self.readLock.unlock(); } + } finally { + self.readLock.unlock(); } if (vacuuming && vacuumLowest != Integer.MAX_VALUE && placer.getEngine() != null) { @@ -974,9 +858,76 @@ final class IrisObjectPlacementRunner { if (state == null || !IrisProceduralBlocks.hasProperty(state, "waterlogged")) { return state; } + if (!enabled && !"true".equals(IrisProceduralBlocks.propertyValue(state, "waterlogged"))) { + return IrisProceduralBlocks.normalizeWaterlogging(state, null, false); + } return IrisProceduralBlocks.normalizeWaterlogging(state, placer.get(x, y, z), enabled); } + private String[] resolveMarkers(IrisObjectPlacement config, RNG rng, int x, int yv, int z, IrisData rdata, + ObjectPlacementBake bake) { + self.readLock.lock(); + try { + VectorMap blocks = self.blocks; + KMap markers = new KMap<>(); + KList list = StreamSupport.stream(blocks.keys().spliterator(), false) + .collect(KList.collector()); + // Marker selection persists into the mantle, so it must be seed-deterministic. + // Derive a side stream keyed on the placement position instead of consuming the + // caller's rng: consuming draws there would shift every later placement in the + // chunk and invalidate existing worlds/goldenhashes. + RNG markerRng = rng.nextParallelRNG(((((long) x) << 32) | (z & 0xFFFFFFFFL)) * 31L + yv); + + int markerIndex = 0; + for (IrisObjectMarker j : config.getMarkers()) { + IrisMarker marker = self.getLoader().getMarkerLoader().load(j.getMarker()); + int markerSalt = markerIndex++; + + if (marker == null || marker.isCompatExcluded()) { + continue; + } + + int max = j.getMaximumMarkers(); + for (IrisBlockVector i : list.shuffleCopy(markerRng.nextParallelRNG(markerSalt))) { + if (max <= 0) { + break; + } + + NativeBlockState data = blocks.get(i); + if (data == null) { + continue; + } + + for (NativeBlockState k : j.getMark(rdata)) { + if (max <= 0) { + break; + } + + if (j.isExact() ? k.matches(data) : IrisObjectShaping.materialKey(k).equals(IrisObjectShaping.materialKey(data))) { + boolean a = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 1, 0))); + boolean fff = !blocks.containsKey((IrisBlockVector) i.clone().add(new IrisBlockVector(0, 2, 0))); + + if (!marker.isEmptyAbove() || (a && fff)) { + markers.put(i, j.getMarker()); + max--; + } + } + } + } + } + + String[] resolved = new String[bake.count()]; + VectorMap.Cursor cursor = blocks.cursor(); + int index = 0; + while (index < resolved.length && cursor.next()) { + resolved[index++] = markers.get(cursor.key()); + } + return resolved; + } finally { + self.readLock.unlock(); + } + } + private KList transformedPaintSupport(SpinKernel spin, IrisBlockVector translateOffset, CNG warp, double half, int x, int y, int z) { KList offsets = new KList<>(); @@ -1337,152 +1288,159 @@ final class IrisObjectPlacementRunner { return result; } - /** - * Precomputed vector rotation for a single placement. - *

- * This mirrors {@link IrisObjectRotation#rotate(IrisBlockVector, int, int, int)} branch for branch, with two - * differences: it mutates the vector handed to it instead of cloning, and the per-axis angle plus its cosine - * and sine are resolved once instead of once per block. The angle is a pure function of the axis clamp and the - * spin, both fixed for the placement, and Math.cos/Math.sin are pure, so every produced double is identical to - * the value the per-call form would have produced. - */ - private static final class SpinKernel { - private static final int MODE_NONE = 0; - private static final int MODE_FLIP = 1; - private static final int MODE_QUARTER = 2; - private static final int MODE_THREE_QUARTER = 3; - private static final int MODE_ANGLE = 4; + private record StateFacts(NativeBlockState rotated, boolean waterloggable, boolean authoredWaterlogged, + NativeBlockState dry, boolean air) { + private static final StateFacts ROTATED_AWAY = new StateFacts(null, false, false, null, false); + } - private final boolean rotates; - private final int xMode; - private final int yMode; - private final int zMode; - private final double xCos; - private final double xSin; - private final double yCos; - private final double ySin; - private final double zCos; - private final double zSin; + private static final class PlacementStates { + private static final Object ROTATED_AWAY = new Object(); + private static final byte UNKNOWN = 0; + private static final byte NO = 1; + private static final byte YES = 2; - private SpinKernel(IrisObjectRotation rotation, int spinx, int spiny, int spinz) { - rotates = rotation.canRotate(); - xMode = rotation.canRotateX() ? modeOf(rotation.getXAxis()) : MODE_NONE; - zMode = rotation.canRotateZ() ? modeOf(rotation.getZAxis()) : MODE_NONE; - yMode = rotation.canRotateY() ? modeOf(rotation.getYAxis()) : MODE_NONE; + private final NativeBlockState[] palette; + private final IrisObjectRotation rotation; + private final int spinx; + private final int spiny; + private final int spinz; + private final boolean preventingDecay; + private final boolean debugSmartBore; + private final Set excludedStiltMaterials; + private final NativeBlockState[] decayed; + private final StateFacts[] facts; + private final byte[] stiltLayers; + private final byte[] stilts; + private final NativeBlockState[] stiltDefaults; + private IdentityHashMap rotations; + private IdentityHashMap editedFacts; + private IdentityHashMap placedStilts; - double xAngle = xMode == MODE_ANGLE ? rotation.getXRotation(spinx) : 0; - double zAngle = zMode == MODE_ANGLE ? rotation.getZRotation(spinz) : 0; - double yAngle = yMode == MODE_ANGLE ? rotation.getYRotation(spiny) : 0; - xCos = Math.cos(xAngle); - xSin = Math.sin(xAngle); - zCos = Math.cos(zAngle); - zSin = Math.sin(zAngle); - yCos = Math.cos(yAngle); - ySin = Math.sin(yAngle); + private PlacementStates(NativeBlockState[] palette, IrisObjectRotation rotation, int spinx, int spiny, int spinz, + boolean preventingDecay, boolean debugSmartBore, Set excludedStiltMaterials) { + this.palette = palette; + this.rotation = rotation; + this.spinx = spinx; + this.spiny = spiny; + this.spinz = spinz; + this.preventingDecay = preventingDecay; + this.debugSmartBore = debugSmartBore; + this.excludedStiltMaterials = excludedStiltMaterials; + this.decayed = new NativeBlockState[palette.length]; + this.facts = new StateFacts[palette.length]; + this.stiltLayers = new byte[palette.length]; + this.stilts = new byte[palette.length]; + this.stiltDefaults = new NativeBlockState[palette.length]; } - private static int modeOf(IrisAxisRotationClamp clamp) { - if (!clamp.isLocked()) { - return MODE_ANGLE; - } - - if (Math.abs(clamp.getMax()) % 360D == 180D) { - return MODE_FLIP; - } - - if (clamp.getMax() % 360D == 90D || clamp.getMax() % 360D == -270D) { - return MODE_QUARTER; - } - - if (clamp.getMax() == -90D || clamp.getMax() % 360D == 270D) { - return MODE_THREE_QUARTER; - } - - return MODE_ANGLE; + private NativeBlockState source(int entry) { + NativeBlockState state = palette[entry]; + return debugSmartBore && IrisObject.States.vair().equals(state) ? IrisObject.States.vairDebug() : state; } - /** - * Rotates in place. Axis order (X, then Z, then Y) is load bearing. - */ - private void rotate(IrisBlockVector v) { - if (!rotates) { - return; + private boolean stiltLayer(int entry) { + byte known = stiltLayers[entry]; + if (known == UNKNOWN) { + known = IrisObjectShaping.isStiltLayerBlock(source(entry)) ? YES : NO; + stiltLayers[entry] = known; } + return known == YES; + } - switch (xMode) { - case MODE_FLIP -> { - v.setZ(-v.getZ()); - v.setY(-v.getY()); - } - case MODE_QUARTER -> { - double z = v.getZ(); - v.setZ(v.getY()); - v.setY(-z); - } - case MODE_THREE_QUARTER -> { - double z = v.getZ(); - v.setZ(-v.getY()); - v.setY(z); - } - case MODE_ANGLE -> { - double y = xCos * v.getY() - xSin * v.getZ(); - double z = xSin * v.getY() + xCos * v.getZ(); - v.setY(y); - v.setZ(z); - } - default -> { - } + private NativeBlockState decayed(int entry) { + NativeBlockState resolved = decayed[entry]; + if (resolved == null) { + NativeBlockState data = source(entry); + resolved = preventingDecay && IrisProceduralBlocks.hasProperty(data, "distance") + && "false".equals(IrisProceduralBlocks.propertyValue(data, "persistent")) + ? data.withProperty("persistent", "true") : data; + decayed[entry] = resolved; } + return resolved; + } - switch (zMode) { - case MODE_FLIP -> { - v.setY(-v.getY()); - v.setX(-v.getX()); - } - case MODE_QUARTER -> { - double y = v.getY(); - v.setY(v.getX()); - v.setX(-y); - } - case MODE_THREE_QUARTER -> { - double y = v.getY(); - v.setY(-v.getX()); - v.setX(y); - } - case MODE_ANGLE -> { - double x = zCos * v.getX() - zSin * v.getY(); - double y = zSin * v.getX() + zCos * v.getY(); - v.setX(x); - v.setY(y); - } - default -> { - } + private StateFacts facts(int entry) { + StateFacts resolved = facts[entry]; + if (resolved == null) { + resolved = computeFacts(decayed(entry)); + facts[entry] = resolved; } + return resolved; + } - switch (yMode) { - case MODE_FLIP -> { - v.setX(-v.getX()); - v.setZ(-v.getZ()); - } - case MODE_QUARTER -> { - double x = v.getX(); - v.setX(v.getZ()); - v.setZ(-x); - } - case MODE_THREE_QUARTER -> { - double x = v.getX(); - v.setX(-v.getZ()); - v.setZ(x); - } - case MODE_ANGLE -> { - double x = yCos * v.getX() + ySin * v.getZ(); - double z = -ySin * v.getX() + yCos * v.getZ(); - v.setX(x); - v.setZ(z); - } - default -> { - } + private StateFacts factsOf(NativeBlockState state) { + if (editedFacts == null) { + editedFacts = new IdentityHashMap<>(); } + StateFacts resolved = editedFacts.get(state); + if (resolved == null) { + resolved = computeFacts(state); + editedFacts.put(state, resolved); + } + return resolved; + } + + private StateFacts computeFacts(NativeBlockState state) { + NativeBlockState rotated = rotate(state); + if (rotated == null) { + return StateFacts.ROTATED_AWAY; + } + boolean waterloggable = IrisProceduralBlocks.hasProperty(rotated, "waterlogged"); + boolean authored = waterloggable && "true".equals(IrisProceduralBlocks.propertyValue(rotated, "waterlogged")); + NativeBlockState dry = waterloggable ? IrisProceduralBlocks.normalizeWaterlogging(rotated, null, false) : rotated; + String material = IrisObjectShaping.materialKey(rotated); + boolean air = material.equals("minecraft:air") || material.equals("minecraft:cave_air"); + return new StateFacts(rotated, waterloggable, authored, dry, air); + } + + private NativeBlockState rotate(NativeBlockState state) { + if (state == null) { + return null; + } + if (rotations == null) { + rotations = new IdentityHashMap<>(); + } + Object memo = rotations.get(state); + if (memo == null) { + NativeBlockState rotated = rotation.rotate(state, spinx, spiny, spinz); + memo = rotated == null ? ROTATED_AWAY : rotated; + rotations.put(state, memo); + } + return memo == ROTATED_AWAY ? null : (NativeBlockState) memo; + } + + private boolean shouldStilt(int entry) { + byte known = stilts[entry]; + if (known == UNKNOWN) { + known = IrisObjectShaping.shouldStilt(palette[entry], excludedStiltMaterials) ? YES : NO; + stilts[entry] = known; + } + return known == YES; + } + + private NativeBlockState stiltDefault(int entry) { + NativeBlockState resolved = stiltDefaults[entry]; + if (resolved == null) { + NativeBlockState state = palette[entry]; + String material = IrisObjectShaping.materialKey(state); + resolved = material.equals("minecraft:grass_block") || material.equals("minecraft:mycelium") + || material.equals("minecraft:podzol") || material.equals("minecraft:dirt_path") + ? IrisObject.States.dirt() : state; + stiltDefaults[entry] = resolved; + } + return resolved; + } + + private boolean shouldStiltPlaced(NativeBlockState state) { + if (placedStilts == null) { + placedStilts = new IdentityHashMap<>(); + } + Boolean known = placedStilts.get(state); + if (known == null) { + known = IrisObjectShaping.shouldStilt(state, excludedStiltMaterials); + placedStilts.put(state, known); + } + return known; } } diff --git a/core/src/main/java/art/arcane/iris/structure/object/ObjectPlacementBake.java b/core/src/main/java/art/arcane/iris/structure/object/ObjectPlacementBake.java new file mode 100644 index 000000000..58470a119 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/structure/object/ObjectPlacementBake.java @@ -0,0 +1,259 @@ +package art.arcane.iris.structure.object; + +import art.arcane.iris.generation.block.TileData; +import art.arcane.iris.generation.block.VectorMap; +import art.arcane.iris.generation.decoration.tree.TreeGrounding; +import art.arcane.iris.generation.geometry.IrisBlockVector; +import art.arcane.volmlib.nativelib.terrain.NativeBlockState; +import it.unimi.dsi.fastutil.ints.IntArrayList; + +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +final class ObjectPlacementBake { + private static final int MAXIMUM_VARIANTS_PER_OBJECT = 32; + + private final long blocksRevision; + private final long statesRevision; + private final TreeGrounding grounding; + private final int count; + private final int[] offsetX; + private final int[] offsetY; + private final int[] offsetZ; + private final int[] layerY; + private final short[] paletteIndexes; + private final NativeBlockState[] palette; + private final int[] tileBlocks; + private final TileData[] tileValues; + private final long[] canopy; + private final Key key; + private volatile double[] exact; + + private ObjectPlacementBake(Key key, long blocksRevision, long statesRevision, TreeGrounding grounding, int count, + int[] offsetX, int[] offsetY, int[] offsetZ, int[] layerY, short[] paletteIndexes, + NativeBlockState[] palette, int[] tileBlocks, TileData[] tileValues, long[] canopy) { + this.key = key; + this.blocksRevision = blocksRevision; + this.statesRevision = statesRevision; + this.grounding = grounding; + this.count = count; + this.offsetX = offsetX; + this.offsetY = offsetY; + this.offsetZ = offsetZ; + this.layerY = layerY; + this.paletteIndexes = paletteIndexes; + this.palette = palette; + this.tileBlocks = tileBlocks; + this.tileValues = tileValues; + this.canopy = canopy; + } + + static ObjectPlacementBake of(IrisObject object, SpinKernel spin, boolean ceilingHang, IrisBlockVector translateOffset) { + Key key = Key.of(spin, ceilingHang, translateOffset); + ConcurrentHashMap variants = object.placementBakes; + ObjectPlacementBake cached = variants.get(key); + if (cached != null && cached.matches(object)) { + return cached; + } + ObjectPlacementBake built = bake(object, key, spin, ceilingHang, translateOffset); + if (built.matches(object)) { + if (variants.size() >= MAXIMUM_VARIANTS_PER_OBJECT) { + variants.clear(); + } + variants.put(key, built); + } + return built; + } + + private static ObjectPlacementBake bake(IrisObject object, Key key, SpinKernel spin, boolean ceilingHang, + IrisBlockVector translateOffset) { + object.readLock.lock(); + try { + VectorMap blocks = object.blocks; + VectorMap states = object.states; + long blocksRevision = blocks.modificationRevision(); + long statesRevision = states.modificationRevision(); + TreeGrounding grounding = object.getTreeGrounding(); + boolean hasStates = !states.isEmpty(); + IntArrayList x = new IntArrayList(); + IntArrayList y = new IntArrayList(); + IntArrayList z = new IntArrayList(); + IntArrayList layer = new IntArrayList(); + IntArrayList indexes = new IntArrayList(); + IntArrayList tileBlocks = new IntArrayList(); + List tileValues = new ArrayList<>(); + List palette = new ArrayList<>(); + IdentityHashMap paletteSlots = new IdentityHashMap<>(); + IntArrayList canopyBlocks = new IntArrayList(); + boolean layerDiffers = false; + IrisBlockVector rotated = new IrisBlockVector(0, 0, 0); + VectorMap.Cursor cursor = blocks.cursor(); + int index = 0; + while (cursor.next()) { + IrisBlockVector source = cursor.key(); + NativeBlockState state = cursor.value(); + if (state == null) { + state = IrisObject.States.air(); + } + transform(source, rotated, spin, ceilingHang, translateOffset); + int roundedY = (int) Math.round(rotated.getY()); + int floorY = rotated.getBlockY(); + x.add((int) Math.round(rotated.getX())); + y.add(roundedY); + z.add((int) Math.round(rotated.getZ())); + layer.add(floorY); + layerDiffers |= floorY != roundedY; + Integer slot = paletteSlots.get(state); + if (slot == null) { + slot = palette.size(); + palette.add(state); + paletteSlots.put(state, slot); + } + indexes.add(slot); + if (hasStates) { + TileData tile = states.get(source); + if (tile != null) { + tileBlocks.add(index); + tileValues.add(tile); + } + } + if (grounding != null && grounding.isCanopy(source.getBlockX(), source.getBlockY(), source.getBlockZ())) { + canopyBlocks.add(index); + } + index++; + } + if (palette.size() > Short.MAX_VALUE) { + throw new IllegalStateException("Object " + object.getLoadKey() + " has more than " + + Short.MAX_VALUE + " distinct block states"); + } + short[] paletteIndexes = new short[index]; + for (int block = 0; block < index; block++) { + paletteIndexes[block] = (short) indexes.getInt(block); + } + long[] canopy = null; + if (!canopyBlocks.isEmpty()) { + canopy = new long[(index + 63) >>> 6]; + for (int block : canopyBlocks) { + canopy[block >>> 6] |= 1L << (block & 63); + } + } + return new ObjectPlacementBake(key, blocksRevision, statesRevision, grounding, index, + x.toIntArray(), y.toIntArray(), z.toIntArray(), layerDiffers ? layer.toIntArray() : null, + paletteIndexes, palette.toArray(new NativeBlockState[0]), + tileBlocks.isEmpty() ? null : tileBlocks.toIntArray(), + tileValues.isEmpty() ? null : tileValues.toArray(new TileData[0]), canopy); + } finally { + object.readLock.unlock(); + } + } + + private static void transform(IrisBlockVector source, IrisBlockVector target, SpinKernel spin, boolean ceilingHang, + IrisBlockVector translateOffset) { + target.setX(source.getX()); + target.setY(source.getY()); + target.setZ(source.getZ()); + spin.rotate(target); + if (ceilingHang) { + target.setY(-target.getBlockY()); + } + if (translateOffset != null) { + target.add(translateOffset); + } + } + + boolean matches(IrisObject object) { + return object.blocks.modificationRevision() == blocksRevision + && object.states.modificationRevision() == statesRevision + && object.getTreeGrounding() == grounding; + } + + int count() { + return count; + } + + int offsetX(int index) { + return offsetX[index]; + } + + int offsetY(int index) { + return offsetY[index]; + } + + int offsetZ(int index) { + return offsetZ[index]; + } + + int layerY(int index) { + return layerY == null ? offsetY[index] : layerY[index]; + } + + int paletteIndex(int index) { + return paletteIndexes[index]; + } + + NativeBlockState[] palette() { + return palette; + } + + int[] tileBlocks() { + return tileBlocks; + } + + TileData[] tileValues() { + return tileValues; + } + + boolean canopy(int index) { + return canopy != null && (canopy[index >>> 6] & (1L << (index & 63))) != 0L; + } + + double[] exact(IrisObject object, SpinKernel spin) { + double[] resolved = exact; + if (resolved != null) { + return resolved; + } + object.readLock.lock(); + try { + resolved = new double[count * 3]; + IrisBlockVector rotated = new IrisBlockVector(0, 0, 0); + VectorMap.Cursor cursor = object.blocks.cursor(); + int index = 0; + while (cursor.next() && index < count) { + transform(cursor.key(), rotated, spin, key.ceilingHang(), key.translateOffset()); + resolved[index * 3] = rotated.getX(); + resolved[index * 3 + 1] = rotated.getY(); + resolved[index * 3 + 2] = rotated.getZ(); + index++; + } + if (index != count) { + throw new IllegalStateException("Object " + object.getLoadKey() + " changed while its placement bake was read"); + } + } finally { + object.readLock.unlock(); + } + exact = resolved; + return resolved; + } + + record Key(boolean rotates, int xMode, int yMode, int zMode, long xCos, long xSin, long yCos, long ySin, + long zCos, long zSin, boolean ceilingHang, boolean translating, long translateX, long translateY, + long translateZ) { + static Key of(SpinKernel spin, boolean ceilingHang, IrisBlockVector translateOffset) { + boolean translating = translateOffset != null; + return new Key(spin.rotates, spin.xMode, spin.yMode, spin.zMode, + Double.doubleToLongBits(spin.xCos), Double.doubleToLongBits(spin.xSin), + Double.doubleToLongBits(spin.yCos), Double.doubleToLongBits(spin.ySin), + Double.doubleToLongBits(spin.zCos), Double.doubleToLongBits(spin.zSin), ceilingHang, translating, + translating ? Double.doubleToLongBits(translateOffset.getX()) : 0L, + translating ? Double.doubleToLongBits(translateOffset.getY()) : 0L, + translating ? Double.doubleToLongBits(translateOffset.getZ()) : 0L); + } + + IrisBlockVector translateOffset() { + return translating ? new IrisBlockVector(Double.longBitsToDouble(translateX), + Double.longBitsToDouble(translateY), Double.longBitsToDouble(translateZ)) : null; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/structure/object/SpinKernel.java b/core/src/main/java/art/arcane/iris/structure/object/SpinKernel.java new file mode 100644 index 000000000..abaa1598c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/structure/object/SpinKernel.java @@ -0,0 +1,152 @@ +package art.arcane.iris.structure.object; + +import art.arcane.iris.generation.geometry.IrisBlockVector; + +/** + * Precomputed vector rotation for a single placement. + *

+ * This mirrors {@link IrisObjectRotation#rotate(IrisBlockVector, int, int, int)} branch for branch, with two + * differences: it mutates the vector handed to it instead of cloning, and the per-axis angle plus its cosine + * and sine are resolved once instead of once per block. The angle is a pure function of the axis clamp and the + * spin, both fixed for the placement, and Math.cos/Math.sin are pure, so every produced double is identical to + * the value the per-call form would have produced. + */ +final class SpinKernel { + static final int MODE_NONE = 0; + static final int MODE_FLIP = 1; + static final int MODE_QUARTER = 2; + static final int MODE_THREE_QUARTER = 3; + static final int MODE_ANGLE = 4; + + final boolean rotates; + final int xMode; + final int yMode; + final int zMode; + final double xCos; + final double xSin; + final double yCos; + final double ySin; + final double zCos; + final double zSin; + + SpinKernel(IrisObjectRotation rotation, int spinx, int spiny, int spinz) { + rotates = rotation.canRotate(); + xMode = rotation.canRotateX() ? modeOf(rotation.getXAxis()) : MODE_NONE; + zMode = rotation.canRotateZ() ? modeOf(rotation.getZAxis()) : MODE_NONE; + yMode = rotation.canRotateY() ? modeOf(rotation.getYAxis()) : MODE_NONE; + + double xAngle = xMode == MODE_ANGLE ? rotation.getXRotation(spinx) : 0; + double zAngle = zMode == MODE_ANGLE ? rotation.getZRotation(spinz) : 0; + double yAngle = yMode == MODE_ANGLE ? rotation.getYRotation(spiny) : 0; + xCos = Math.cos(xAngle); + xSin = Math.sin(xAngle); + zCos = Math.cos(zAngle); + zSin = Math.sin(zAngle); + yCos = Math.cos(yAngle); + ySin = Math.sin(yAngle); + } + + private static int modeOf(IrisAxisRotationClamp clamp) { + if (!clamp.isLocked()) { + return MODE_ANGLE; + } + + if (Math.abs(clamp.getMax()) % 360D == 180D) { + return MODE_FLIP; + } + + if (clamp.getMax() % 360D == 90D || clamp.getMax() % 360D == -270D) { + return MODE_QUARTER; + } + + if (clamp.getMax() == -90D || clamp.getMax() % 360D == 270D) { + return MODE_THREE_QUARTER; + } + + return MODE_ANGLE; + } + + /** + * Rotates in place. Axis order (X, then Z, then Y) is load bearing. + */ + void rotate(IrisBlockVector v) { + if (!rotates) { + return; + } + + switch (xMode) { + case MODE_FLIP -> { + v.setZ(-v.getZ()); + v.setY(-v.getY()); + } + case MODE_QUARTER -> { + double z = v.getZ(); + v.setZ(v.getY()); + v.setY(-z); + } + case MODE_THREE_QUARTER -> { + double z = v.getZ(); + v.setZ(-v.getY()); + v.setY(z); + } + case MODE_ANGLE -> { + double y = xCos * v.getY() - xSin * v.getZ(); + double z = xSin * v.getY() + xCos * v.getZ(); + v.setY(y); + v.setZ(z); + } + default -> { + } + } + + switch (zMode) { + case MODE_FLIP -> { + v.setY(-v.getY()); + v.setX(-v.getX()); + } + case MODE_QUARTER -> { + double y = v.getY(); + v.setY(v.getX()); + v.setX(-y); + } + case MODE_THREE_QUARTER -> { + double y = v.getY(); + v.setY(-v.getX()); + v.setX(y); + } + case MODE_ANGLE -> { + double x = zCos * v.getX() - zSin * v.getY(); + double y = zSin * v.getX() + zCos * v.getY(); + v.setX(x); + v.setY(y); + } + default -> { + } + } + + switch (yMode) { + case MODE_FLIP -> { + v.setX(-v.getX()); + v.setZ(-v.getZ()); + } + case MODE_QUARTER -> { + double x = v.getX(); + v.setX(v.getZ()); + v.setZ(-x); + } + case MODE_THREE_QUARTER -> { + double x = v.getX(); + v.setX(-v.getZ()); + v.setZ(x); + } + case MODE_ANGLE -> { + double x = yCos * v.getX() + ySin * v.getZ(); + double z = -ySin * v.getX() + yCos * v.getZ(); + v.setX(x); + v.setZ(z); + } + default -> { + } + } + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/MutationLogTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/MutationLogTest.java new file mode 100644 index 000000000..9a47e059d --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/mantle/MutationLogTest.java @@ -0,0 +1,81 @@ +package art.arcane.iris.generation.mantle; + +import art.arcane.iris.testsupport.KeyedBlockState; +import art.arcane.volmlib.nativelib.terrain.NativeBlockState; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class MutationLogTest { + @Test + public void logKeepsPositionsValuesAndFlagsInOrder() { + NativeBlockState state = new KeyedBlockState("minecraft:stone"); + MutationLog log = MutationLog.of(List.of( + new ObjectDestinationTransaction.SetMutation(new ObjectDestinationTransaction.DataKey(3, 70, -2, String.class), "marker", true), + new ObjectDestinationTransaction.CustomBlockMutation(new ObjectDestinationTransaction.DataKey(-17, 4, 40, NativeBlockState.class), state, false))); + for (int index = 0; index < 100; index++) { + log.add(index, index + 1, -index, "grow" + index, (index & 1) == 0, (index & 2) == 0); + } + + assertEquals(102, log.size()); + assertEquals(3, log.x(0)); + assertEquals(70, log.y(0)); + assertEquals(-2, log.z(0)); + assertEquals("marker", log.value(0)); + assertFalse(log.custom(0)); + assertTrue(log.canopy(0)); + assertSame(String.class, log.type(0)); + assertSame(state, log.value(1)); + assertTrue(log.custom(1)); + assertFalse(log.canopy(1)); + assertSame(NativeBlockState.class, log.type(1)); + assertEquals(99, log.x(101)); + assertEquals("grow99", log.value(101)); + assertFalse(log.custom(101)); + assertTrue(log.canopy(98)); + assertFalse(log.canopy(100)); + } + + @Test + public void mismatchedMutationTypesAreRejected() { + assertThrows(IllegalArgumentException.class, () -> MutationLog.of(List.of( + new ObjectDestinationTransaction.SetMutation(new ObjectDestinationTransaction.DataKey(0, 4, 0, Integer.class), "text", false)))); + } + + @Test + public void planSlicesMatchThePlanBuiltFromTheSameMutations() { + MutationLog log = new MutationLog(); + List tail = new ArrayList<>(); + for (int index = 0; index < 64; index++) { + int x = (index * 7) - 30; + int y = 4 + index; + int z = (index * 5) - 20; + String value = "value" + (index % 9); + boolean canopy = index % 4 == 0; + log.add(x, y, z, value, false, canopy); + if (index >= 10) { + tail.add(new ObjectDestinationTransaction.SetMutation(new ObjectDestinationTransaction.DataKey(x, y, z, String.class), value, canopy)); + } + } + ObjectSourcePlan sliced = new ObjectSourcePlan(log, 10, List.of()); + ObjectSourcePlan expected = new ObjectSourcePlan(tail); + + assertEquals(expected.destinationCount(), sliced.destinationCount()); + int total = 0; + for (int chunkX = -3; chunkX <= 30; chunkX++) { + for (int chunkZ = -3; chunkZ <= 30; chunkZ++) { + List mutations = sliced.mutationsFor(chunkX, chunkZ); + assertEquals(expected.mutationsFor(chunkX, chunkZ), mutations); + total += mutations.size(); + } + } + assertEquals(54, total); + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/ObjectSourcePlanCacheTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/ObjectSourcePlanCacheTest.java index 25d05d0b2..9c88b387e 100644 --- a/core/src/test/java/art/arcane/iris/generation/mantle/ObjectSourcePlanCacheTest.java +++ b/core/src/test/java/art/arcane/iris/generation/mantle/ObjectSourcePlanCacheTest.java @@ -163,6 +163,44 @@ public class ObjectSourcePlanCacheTest { } } + @Test + public void tryAcquireReportsBusySourcesWithoutConsumingThem() throws Exception { + ObjectSourcePlanCache cache = new ObjectSourcePlanCache(1024L * 1024L); + ObjectSourcePlan plan = planTouching(new int[]{0, 0}, new int[]{1, 0}); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + AtomicInteger builds = new AtomicInteger(); + ExecutorService executor = Executors.newFixedThreadPool(1); + try { + Future owner = executor.submit(() -> cache.acquire(0, 0, 0, 0, 7, () -> { + builds.incrementAndGet(); + entered.countDown(); + await(release); + return plan; + })); + assertTrue(entered.await(5L, TimeUnit.SECONDS)); + + assertSame(ObjectSourcePlanCache.BUSY, cache.tryAcquire(0, 0, 1, 0, 7, () -> { + builds.incrementAndGet(); + return plan; + })); + ObjectSourcePlan unrelated = planTouching(new int[]{5, 5}); + assertSame(unrelated, cache.tryAcquire(5, 5, 5, 5, 7, () -> unrelated)); + release.countDown(); + assertSame(plan, owner.get(5L, TimeUnit.SECONDS)); + + assertSame(plan, cache.tryAcquire(0, 0, 1, 0, 7, () -> { + builds.incrementAndGet(); + return plan; + })); + assertEquals(1, builds.get()); + assertEquals(2L, cache.stats().drained()); + } finally { + release.countDown(); + executor.shutdownNow(); + } + } + @Test public void concurrentDestinationsShareOneSourceBuild() throws Exception { ObjectSourcePlanCache cache = new ObjectSourcePlanCache(1024L * 1024L); diff --git a/core/src/test/java/art/arcane/iris/generation/noise/GeneratorProgramParityTest.java b/core/src/test/java/art/arcane/iris/generation/noise/GeneratorProgramParityTest.java new file mode 100644 index 000000000..016d1c6cb --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/noise/GeneratorProgramParityTest.java @@ -0,0 +1,160 @@ +package art.arcane.iris.generation.noise; + +import art.arcane.volmlib.util.collection.KList; +import org.junit.Test; + +import java.util.SplittableRandom; + +import static org.junit.Assert.assertEquals; + +public class GeneratorProgramParityTest { + private static final NoiseStyle[] STYLES = { + NoiseStyle.SIMPLEX, NoiseStyle.PERLIN, NoiseStyle.IRIS, NoiseStyle.IRIS_HALF, NoiseStyle.FRACTAL_SMOKE, + NoiseStyle.FRACTAL_WATER, NoiseStyle.CLOVER, NoiseStyle.CELLULAR, NoiseStyle.NOWHERE, + NoiseStyle.CELLULAR_IRIS_DOUBLE, NoiseStyle.SIMPLEX_VASCULAR, NoiseStyle.FLAT, NoiseStyle.STATIC, + NoiseStyle.GLOB_IRIS_THICK, NoiseStyle.FRACTAL_BILLOW_PERLIN, NoiseStyle.PERLIN_IRIS + }; + + @Test + public void programHeightsMatchScalarHeightsBitForBit() { + SplittableRandom random = new SplittableRandom(0x5EED1E55L); + TerrainBatchScratch scratch = new TerrainBatchScratch(); + for (int generatorIndex = 0; generatorIndex < 160; generatorIndex++) { + long shapeSeed = random.nextLong(); + IrisGenerator scalar = generator(new SplittableRandom(shapeSeed)); + IrisGenerator batch = generator(new SplittableRandom(shapeSeed)); + long superSeed = random.nextLong(); + GeneratorProgram program = batch.compileProgram(superSeed); + int count = 1 + random.nextInt(600); + double[] xs = new double[count]; + double[] zs = new double[count]; + double[] out = new double[count]; + fillCoordinates(random, xs, zs, count); + program.heights(scratch, xs, zs, count, out); + for (int index = 0; index < count; index++) { + assertEquals("generator " + generatorIndex + " point " + xs[index] + "," + zs[index], + Double.doubleToRawLongBits(scalar.getHeight(xs[index], zs[index], superSeed)), + Double.doubleToRawLongBits(out[index])); + } + } + } + + @Test + public void singlePointProgramHeightsMatchScalarHeightsBitForBit() { + SplittableRandom random = new SplittableRandom(0xFEEDL); + TerrainBatchScratch scratch = new TerrainBatchScratch(); + for (int generatorIndex = 0; generatorIndex < 160; generatorIndex++) { + long shapeSeed = random.nextLong(); + IrisGenerator scalar = generator(new SplittableRandom(shapeSeed)); + IrisGenerator compiled = generator(new SplittableRandom(shapeSeed)); + long superSeed = random.nextLong(); + GeneratorProgram program = compiled.compileProgram(superSeed); + for (int sample = 0; sample < 400; sample++) { + double x = coordinate(random); + double z = random.nextInt(5) == 0 ? x : coordinate(random); + assertEquals("generator " + generatorIndex + " point " + x + "," + z, + Double.doubleToRawLongBits(scalar.getHeight(x, z, superSeed)), + Double.doubleToRawLongBits(program.height(scratch, x, z))); + } + } + } + + @Test + public void chunkGridsMatchScalarHeightsBitForBit() { + SplittableRandom random = new SplittableRandom(0xC0FFEEL); + TerrainBatchScratch scratch = new TerrainBatchScratch(); + double[] xs = new double[256]; + double[] zs = new double[256]; + double[] out = new double[256]; + for (int generatorIndex = 0; generatorIndex < 60; generatorIndex++) { + long shapeSeed = random.nextLong(); + IrisGenerator scalar = generator(new SplittableRandom(shapeSeed)); + IrisGenerator batch = generator(new SplittableRandom(shapeSeed)); + long superSeed = random.nextLong(); + GeneratorProgram program = batch.compileProgram(superSeed); + for (int chunk = 0; chunk < 4; chunk++) { + int originX = random.nextInt(-200_000, 200_000) << 4; + int originZ = random.nextInt(-200_000, 200_000) << 4; + for (int index = 0; index < 256; index++) { + xs[index] = originX + (index & 15); + zs[index] = originZ + (index >> 4); + } + program.heights(scratch, xs, zs, 256, out); + for (int index = 0; index < 256; index++) { + assertEquals(Double.doubleToRawLongBits(scalar.getHeight(xs[index], zs[index], superSeed)), + Double.doubleToRawLongBits(out[index])); + } + } + } + } + + private static void fillCoordinates(SplittableRandom random, double[] xs, double[] zs, int count) { + for (int index = 0; index < count; index++) { + xs[index] = coordinate(random); + zs[index] = random.nextInt(5) == 0 ? xs[index] : coordinate(random); + } + } + + private static double coordinate(SplittableRandom random) { + return switch (random.nextInt(6)) { + case 0 -> random.nextInt(-4096, 4096); + case 1 -> random.nextInt(-30_000_000, 30_000_000); + case 2 -> random.nextInt(-512, 512) + random.nextDouble(); + case 3 -> random.nextInt(-64, 64) * 6; + case 4 -> random.nextDouble(-30_000_000D, 30_000_000D); + default -> random.nextInt(-16, 16) * 0.25D; + }; + } + + private static IrisGenerator generator(SplittableRandom random) { + int layers = 1 + random.nextInt(3); + KList composite = new KList<>(); + for (int layer = 0; layer < layers; layer++) { + composite.add(layer(random, 0)); + } + double[] details = {0D, 0.25D, 0.5D, 1D}; + IrisGenerator generator = new IrisGenerator().setComposite(composite) + .setSurfaceDetail(details[random.nextInt(details.length)]) + .setMultiplicitive(random.nextInt(4) == 0) + .setSeed(random.nextLong(-5000L, 5000L)) + .setZoom(0.2D + random.nextDouble() * 3D) + .setOffsetX(random.nextInt(-40, 40) * 0.25D) + .setOffsetZ(random.nextInt(-40, 40) * 0.25D) + .setOpacity(0.1D + random.nextDouble()); + if (random.nextInt(3) == 0) { + generator.setCliffHeightMin(random.nextDouble() * 4D).setCliffHeightMax(4D + random.nextDouble() * 12D) + .setCliffHeightGenerator(layer(random, 2)); + } + if (random.nextInt(5) == 0) { + generator.setCellFractureHeight(random.nextDouble()).setCellFractureZoom(0.5D + random.nextDouble()); + } + return generator; + } + + private static IrisNoiseGenerator layer(SplittableRandom random, int depth) { + IrisNoiseGenerator layer = new IrisNoiseGenerator() + .setStyle(STYLES[random.nextInt(STYLES.length)].style()) + .setSeed(random.nextLong(-100_000L, 100_000L)) + .setZoom(0.3D + random.nextDouble() * 4D) + .setOpacity(0.05D + random.nextDouble()) + .setOffsetX(random.nextInt(-20, 20) * 0.5D) + .setOffsetZ(random.nextInt(-20, 20) * 0.5D) + .setOffsetY(random.nextInt(4) == 0 ? random.nextDouble() * 0.1D : 0D) + .setNegative(random.nextInt(4) == 0) + .setExponent(random.nextInt(4) == 0 ? 0.5D + random.nextDouble() : 1D) + .setParametric(random.nextInt(6) == 0) + .setBezier(random.nextInt(6) == 0) + .setSinCentered(random.nextInt(8) == 0) + .setOctaves(random.nextInt(5) == 0 ? 2 : 1) + .setEnabled(depth == 0 || random.nextInt(8) != 0); + if (depth < 4 && random.nextInt(3) != 0) { + int children = random.nextInt(4) == 0 ? 2 + random.nextInt(2) : 1; + KList fracture = new KList<>(); + for (int child = 0; child < children; child++) { + fracture.add(layer(random, depth + 1)); + } + layer.setFracture(fracture); + } + return layer; + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/noise/IrisGeneratorBatchTest.java b/core/src/test/java/art/arcane/iris/generation/noise/IrisGeneratorBatchTest.java index 7d984d9da..144f8da56 100644 --- a/core/src/test/java/art/arcane/iris/generation/noise/IrisGeneratorBatchTest.java +++ b/core/src/test/java/art/arcane/iris/generation/noise/IrisGeneratorBatchTest.java @@ -3,9 +3,6 @@ package art.arcane.iris.generation.noise; import art.arcane.volmlib.util.collection.KList; import org.junit.Test; -import java.util.ArrayList; -import java.util.List; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; @@ -34,7 +31,7 @@ public class IrisGeneratorBatchTest { assertTrue(batch.supportsBatch()); double[] sampled = new double[XS.length]; for (long seed : seeds) { - batch.getHeights(XS, ZS, seed, sampled, sampled.length); + batch.compileProgram(seed).heights(new TerrainBatchScratch(), XS, ZS, sampled.length, sampled); for (int index = 0; index < sampled.length; index++) { assertEquals(style + " detail=" + detail + " seed=" + seed + " index=" + index, Double.doubleToRawLongBits(scalar.getHeight(XS[index], ZS[index], seed)), @@ -54,7 +51,7 @@ public class IrisGeneratorBatchTest { IrisGenerator batch = generator(style, detail, false).setCliffHeightMax(0D).setCellFractureHeight(0D); double[] values = new double[XS.length]; for (long seed : new long[]{-1337L, Long.MAX_VALUE - 37L}) { - batch.getHeights(XS, ZS, seed, values, XS.length); + batch.compileProgram(seed).heights(new TerrainBatchScratch(), XS, ZS, XS.length, values); for (int index = 0; index < XS.length; index++) { assertEquals(Double.doubleToRawLongBits(scalar.getHeight(XS[index], ZS[index], seed)), Double.doubleToRawLongBits(values[index])); @@ -65,13 +62,15 @@ public class IrisGeneratorBatchTest { } @Test - public void resolvesLayerContextOncePerBatch() { + public void resolvesLayerContextOncePerCompile() { IrisNoiseGenerator layer = spy(new IrisNoiseGenerator().setStyle(NoiseStyle.SIMPLEX.style())); IrisGenerator generator = new IrisGenerator().setComposite(new KList<>(layer)); double[] sampled = new double[XS.length]; - generator.getHeights(XS, ZS, 99L, sampled, XS.length); + GeneratorProgram program = generator.compileProgram(99L); verify(layer, times(1)).getGenerator(anyLong(), isNull(), isNull()); clearInvocations(layer); + program.heights(new TerrainBatchScratch(), XS, ZS, XS.length, sampled); + verify(layer, times(0)).getGenerator(anyLong(), isNull(), isNull()); for (int index = 0; index < XS.length; index++) { generator.getHeight(XS[index], ZS[index], 99L); } @@ -79,24 +78,15 @@ public class IrisGeneratorBatchTest { } @Test - public void customGeneratorFallbackKeepsCoordinateOrder() { - List sampled = new ArrayList<>(); + public void customGeneratorsCannotCompile() { IrisGenerator generator = new IrisGenerator() { @Override public double getHeight(double x, double z, long seed) { - sampled.add(x); - return sampled.size(); + return x; } }; assertFalse(generator.supportsBatch()); - double[] values = new double[XS.length]; - generator.getHeights(XS, ZS, 0L, values, values.length); - for (int index = 0; index < values.length; index++) { - assertEquals(Double.valueOf(XS[index]), sampled.get(index)); - assertEquals(index + 1D, values[index], 0D); - } - assertThrows(IllegalArgumentException.class, - () -> generator.getHeights(new double[4097], new double[4097], 0L, new double[4097], 4097)); + assertThrows(IllegalStateException.class, () -> generator.compileProgram(0L)); } @Test diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexBakeParityTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexBakeParityTest.java new file mode 100644 index 000000000..ec36ad135 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexBakeParityTest.java @@ -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 blocks = mockStatic(B.class); + private final MockedStatic 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.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(); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexHeightStreamTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexHeightStreamTest.java index 1e0dbbac0..8f0829a1c 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexHeightStreamTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexHeightStreamTest.java @@ -44,6 +44,7 @@ import static org.mockito.ArgumentMatchers.anyDouble; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -89,16 +90,10 @@ public class IrisComplexHeightStreamTest { try (Fixture fixture = new Fixture(pack())) { IrisBiome mapped = new IrisBiome(); when(fixture.images.sampleBiome(anyDouble(), anyDouble())).thenReturn(mapped); - MeteredCache bridge = (MeteredCache) fixture.complex.getBridgeStream(); - MeteredCache regions = (MeteredCache) fixture.complex.getRegionStream(); - MeteredCache heights = (MeteredCache) fixture.complex.getNaturalHeightStream(); assertSame(mapped, fixture.complex.getBaseBiomeStream().get(-16, 32)); assertSame(mapped, fixture.complex.getBaseBiomeStream().get(-16.75, 10, 32.5)); assertSame(mapped, fixture.complex.getNaturalTrueBiomeStream().get(-16, 32)); assertSame(mapped, fixture.complex.getNaturalTrueBiomeStream().get(-16.75, 10, 32.5)); - assertEquals(0L, bridge.getSize()); - assertEquals(0L, regions.getSize()); - assertEquals(0L, heights.getSize()); assertNull(fixture.complex.getSharedCornerBounds()); when(fixture.images.sampleBiome(anyDouble(), anyDouble())).thenReturn(null); @@ -116,7 +111,7 @@ public class IrisComplexHeightStreamTest { int[] rounded = new int[256]; clearInvocations(fixture.hooks); cache.fillRounded(rounded); - verify(fixture.hooks, times(1)).isMainThread(); + verify(fixture.hooks, atMost(2)).isMainThread(); assertEquals(256, samples.get()); for (int index = 0; index < 256; index++) { int x = index & 15; @@ -127,7 +122,7 @@ public class IrisComplexHeightStreamTest { } clearInvocations(fixture.hooks); cache.fill(); - verify(fixture.hooks, times(1)).isMainThread(); + verify(fixture.hooks, atMost(2)).isMainThread(); assertEquals(256, samples.get()); } } diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexHydrologyNoiseCacheTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexHydrologyNoiseCacheTest.java index be0ae4f35..e18d0bc1e 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexHydrologyNoiseCacheTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexHydrologyNoiseCacheTest.java @@ -2,6 +2,8 @@ package art.arcane.iris.generation.runtime; import art.arcane.iris.configuration.IrisSettings; import art.arcane.iris.world.pregen.PregenPerformanceProfile; +import art.arcane.iris.generation.bake.ChunkBakeCache; +import art.arcane.iris.generation.bake.PlaneSource; import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.hydrology.HydrologyPlanner; import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings; @@ -48,17 +50,14 @@ public class IrisComplexHydrologyNoiseCacheTest { try (Fixture fixture = new Fixture(1_024)) { ProceduralStream wrapper = fixture.complex.getRawHeightStream(); assertEquals(13D, fixture.height.getDouble(18, 5), 0D); - assertEquals(13D, fixture.rawHeight.getDouble(18, 5), 0D); PregenPerformanceProfile.apply(fixture.engine); - assertEquals(4_096L * 256L, fixture.height.getMaxSize()); - assertEquals(4_096L * 256L, fixture.rawHeight.getMaxSize()); + assertTrue(fixture.bakes.capacity() >= 4_096); assertEquals(1_024L * 256L, fixture.biomes.getMaxSize()); assertSame(wrapper, fixture.complex.getRawHeightStream()); assertEquals(47D, wrapper.getDouble(18, 5), 0D); assertEquals(13D, fixture.height.getDouble(18, 5), 0D); - assertEquals(13D, fixture.rawHeight.getDouble(18, 5), 0D); } finally { PregenPerformanceProfile.restore(); IrisSettings.settings = previousSettings; @@ -75,7 +74,7 @@ public class IrisComplexHydrologyNoiseCacheTest { fixture.complex.ensureTerrainNoiseCacheSize(4_096); fixture.assertCapacities(32_768); - assertEquals(4_096L * 256L, fixture.rawHeight.getMaxSize()); + assertTrue(fixture.bakes.capacity() >= 4_096); assertEquals(reservedBytes, budget.reservedBytes()); fixture.complex.close(); assertEquals(0L, budget.reservedBytes()); @@ -88,7 +87,7 @@ public class IrisComplexHydrologyNoiseCacheTest { fixture.complex.ensureTerrainNoiseCacheSize(4_096); fixture.assertCapacities(65_536); - assertEquals(65_536L * 256L, fixture.rawHeight.getMaxSize()); + assertTrue(fixture.bakes.capacity() >= 65_536); } } @@ -229,11 +228,54 @@ public class IrisComplexHydrologyNoiseCacheTest { HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE); setField(fixture.complex, "detached", true); fixture.complex.expandHydrologyNoiseCaches(budget); + fixture.complex.reserveHydrologyBake(budget); fixture.assertCapacities(1_024); + assertEquals(1_024, fixture.bakes.capacity()); setField(fixture.complex, "detached", false); fixture.complex.setHydrologyRuntime(null); fixture.complex.expandHydrologyNoiseCaches(budget); + fixture.complex.reserveHydrologyBake(budget); fixture.assertCapacities(1_024); + assertEquals(1_024, fixture.bakes.capacity()); + assertEquals(0L, budget.reservedBytes()); + } + } + + @Test + public void plannerReservationGrowsTheBakeToTheHydrologyTarget() throws Exception { + try (Fixture fixture = new Fixture(1_024)) { + HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE); + fixture.complex.reserveHydrologyBake(budget); + fixture.complex.reserveHydrologyBake(budget); + assertEquals(32_768, fixture.bakes.capacity()); + assertEquals(2L * (32_768L - 1_024L) * 4_096L, budget.reservedBytes()); + fixture.complex.ensureTerrainNoiseCacheSize(4_096); + assertEquals(32_768, fixture.bakes.capacity()); + fixture.complex.close(); + assertEquals(0L, budget.reservedBytes()); + fixture.complex.reserveHydrologyBake(budget); + assertEquals(0L, budget.reservedBytes()); + } + } + + @Test + public void plannerReservationTakesOnlyTheGrantedShareOfTheBudget() throws Exception { + try (Fixture fixture = new Fixture(1_024)) { + HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(2L * 80L * 4_096L + 4_095L); + fixture.complex.reserveHydrologyBake(budget); + assertEquals(1_104, fixture.bakes.capacity()); + assertEquals(2L * 80L * 4_096L, budget.reservedBytes()); + fixture.complex.close(); + assertEquals(0L, budget.reservedBytes()); + } + } + + @Test + public void plannerReservationLeavesLargerBakesUnchanged() throws Exception { + try (Fixture fixture = new Fixture(65_536)) { + HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE); + fixture.complex.reserveHydrologyBake(budget); + assertEquals(65_536, fixture.bakes.capacity()); assertEquals(0L, budget.reservedBytes()); } } @@ -297,7 +339,7 @@ public class IrisComplexHydrologyNoiseCacheTest { private final CachedStream2D regions; private final CachedStream2D bridge; private final CachedDoubleStream2D height; - private final CachedDoubleStream2D rawHeight; + private final ChunkBakeCache bakes; private Fixture(int capacity) throws Exception { services.when(() -> IrisServices.get(PreservationRegistry.class)).thenReturn(mock(PreservationRegistry.class)); @@ -305,10 +347,10 @@ public class IrisComplexHydrologyNoiseCacheTest { regions = objectCache(capacity); bridge = objectCache(capacity); height = heightCache(capacity); - rawHeight = heightCache(capacity); + bakes = new ChunkBakeCache(mock(PlaneSource.class), capacity); complex.setNaturalHeightStream(height); complex.setHeightStream(ProceduralStream.ofDouble((x, z) -> 47D)); - setField(complex, "cachedHeightStream", rawHeight); + setField(complex, "bakeCache", bakes); when(engine.getComplex()).thenReturn(complex); complex.setBaseBiomeStream(biomes); complex.setRegionStream(regions); diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexSelfSelectionTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexSelfSelectionTest.java index 4d68fd070..b0e7aedd6 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexSelfSelectionTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexSelfSelectionTest.java @@ -83,7 +83,7 @@ public class IrisComplexSelfSelectionTest { Field field = IrisComplex.class.getDeclaredField("childSelectionPlans"); field.setAccessible(true); field.set(complex, plans); - implode = IrisComplex.class.getDeclaredMethod("implode", IrisBiome.class, Double.class, Double.class, int.class); + implode = IrisComplex.class.getDeclaredMethod("implode", IrisBiome.class, double.class, double.class, int.class); implode.setAccessible(true); } @@ -106,7 +106,7 @@ public class IrisComplexSelfSelectionTest { } @Override - public CNG getChildrenGenerator(RNG rng, int signature, double scale) { + public CNG getChildrenGenerator(long seed, double scale) { return noise; } } diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierInferenceIsolationTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierInferenceIsolationTest.java index ca9f39398..d0eb081fb 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierInferenceIsolationTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierInferenceIsolationTest.java @@ -97,11 +97,11 @@ public class IrisCarveModifierInferenceIsolationTest { doReturn(0D).when(region).getShoreHeight(anyDouble(), anyDouble()); Method fixBiomeType = IrisComplex.class.getDeclaredMethod( "fixBiomeType", - Double.class, + double.class, IrisBiome.class, IrisRegion.class, - Double.class, - Double.class, + double.class, + double.class, double.class ); fixBiomeType.setAccessible(true); diff --git a/core/src/test/java/art/arcane/iris/generation/stream/RarityStreamTest.java b/core/src/test/java/art/arcane/iris/generation/stream/RarityStreamTest.java new file mode 100644 index 000000000..c3dee77dd --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/stream/RarityStreamTest.java @@ -0,0 +1,52 @@ +package art.arcane.iris.generation.stream; + +import art.arcane.volmlib.util.math.Rarity; +import art.arcane.volmlib.util.stream.ProceduralStream; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.SplittableRandom; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +public class RarityStreamTest { + @Test + public void picksMatchRarityStreamSelectionForEveryNoiseValue() { + SplittableRandom random = new SplittableRandom(4242L); + for (int trial = 0; trial < 200; trial++) { + List