Merged wave1 into terrain bake

# Conflicts:
#	core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java
#	probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-22 02:16:40 -04:00
commit d2106806a5
71 files changed
+5882 -948

No files matched your search

+5
View File
@@ -476,6 +476,11 @@ tasks.named('shadowJar', ShadowJar).configure {
if (providers.gradleProperty('irisRuntimeJava').getOrElse(rootProject.irisModTarget.java.toString()) == '21') {
exclude('art/arcane/volmlib/util/noise/NativeBillowLoader*.class')
exclude('art/arcane/volmlib/util/noise/BillowBatch*.class')
exclude('art/arcane/volmlib/util/noise/NativeNoiseLibrary*.class')
exclude('art/arcane/volmlib/util/noise/program/NativeNoise*.class')
exclude('art/arcane/volmlib/util/noise/program/NativeProgramEncoder*.class')
exclude('art/arcane/volmlib/util/noise/program/GpuNoise*.class')
exclude('art/arcane/volmlib/native/**')
}
// minimize() stays off. Caffeine reaches its 520 generated cache and node classes through
// MethodHandles.Lookup.findClass on a computed name, and the VolmLib command, GUI and hotload
+1 -1
View File
@@ -16,8 +16,8 @@ art/arcane/iris/generation/block/IrisCustomData.java
art/arcane/iris/generation/block/LegacyTileData.java
art/arcane/iris/generation/block/TileData.java
art/arcane/iris/generation/chunk/ChunkBiomeHunkView.java
art/arcane/iris/generation/chunk/ChunkDataHunkHolder.java
art/arcane/iris/generation/chunk/ChunkDataHunkView.java
art/arcane/iris/generation/chunk/ChunkDataOutput.java
art/arcane/iris/generation/chunk/ChunkHunkView.java
art/arcane/iris/generation/chunk/LinkedTerrainChunk.java
art/arcane/iris/generation/chunk/TerrainChunk.java
@@ -1,169 +0,0 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.generation.chunk;
import art.arcane.iris.platform.bukkit.nms.INMS;
import art.arcane.volmlib.nativelib.terrain.NativeBlockVolume;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.generation.block.BoundBlockState;
import art.arcane.iris.generation.block.IrisCustomData;
import art.arcane.volmlib.util.hunk.storage.StorageHunk;
import org.bukkit.block.data.BlockData;
import org.bukkit.generator.ChunkGenerator.ChunkData;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.Arrays;
/**
* Plain-array block buffer for one chunk. Stages write disjoint columns when they fan out and every hand-off
* between threads is a join, so the cells need no per-access fences. Each column tracks its highest stored y;
* a raise is a CAS so concurrent writers to one column can never lose the maximum.
*/
public class ChunkDataHunkHolder extends StorageHunk<NativeBlockState> implements NativeBlockVolume, ColumnExtent {
private static final BoundBlockState AIR = BoundBlockState.of("AIR");
private static final VarHandle COLUMN_TOPS = MethodHandles.arrayElementVarHandle(int[].class);
private final ChunkData chunk;
private final NativeBlockState[] data;
private final int[] columnTops;
private final int zStride;
public ChunkDataHunkHolder(ChunkData chunk) {
super(16, chunk.getMaxHeight() - chunk.getMinHeight(), 16);
this.chunk = chunk;
this.zStride = 16 * getHeight();
this.data = new NativeBlockState[zStride * 16];
this.columnTops = new int[256];
Arrays.fill(columnTops, -1);
}
@Override
public int getWidth() {
return 16;
}
@Override
public int getDepth() {
return 16;
}
@Override
public void setRaw(int x, int y, int z, NativeBlockState t) {
// Block-hunk contract: null means "no write", never "erase" — ChunkDataHunkView and
// the modded ModdedBlockBuffer already discard nulls, and storing one here made the
// Bukkit output diverge from the modded loaders for the same engine emission.
if (t == null) {
return;
}
data[index(x, y, z)] = t;
int column = (z << 4) | x;
int top = (int) COLUMN_TOPS.getOpaque(columnTops, column);
while (y > top) {
int witness = (int) COLUMN_TOPS.compareAndExchange(columnTops, column, top, y);
if (witness == top) {
break;
}
top = witness;
}
}
@Override
public NativeBlockState getRaw(int x, int y, int z) {
if (y < 0 || y >= getHeight()) {
return AIR.get();
}
NativeBlockState b = data[index(x, y, z)];
return b != null ? b : AIR.get();
}
@Override
public NativeBlockState getStoredRaw(int x, int y, int z) {
return data[index(x, y, z)];
}
@Override
public int highestStoredY(int x, int z) {
return (int) COLUMN_TOPS.getOpaque(columnTops, (z << 4) | x);
}
private int index(int x, int y, int z) {
return (z * zStride) + (y << 4) + x;
}
public void apply() {
applyTo(chunk);
}
public void applyTo(ChunkData target) {
if (INMS.get().applyChunkDataBlocks(target, this)) {
return;
}
int height = getHeight();
for (int x = 0; x < getWidth(); x++) {
for (int z = 0; z < getDepth(); z++) {
BlockData activeBlock = null;
int runStart = -1;
for (int y = 0; y < height; y++) {
NativeBlockState state = getStoredRaw(x, y, z);
BlockData block = state == null ? null : (BlockData) state.nativeHandle();
// Custom wrappers are not real Bukkit data; write the vanilla base like the
// NMS fast path (NMSBinding.applyChunkDataBlocks) does.
if (block instanceof IrisCustomData custom) {
block = custom.getBase();
}
if (block == null) {
flushRun(target, x, z, runStart, y, activeBlock);
activeBlock = null;
runStart = -1;
continue;
}
if (activeBlock != null && activeBlock.equals(block)) {
continue;
}
flushRun(target, x, z, runStart, y, activeBlock);
activeBlock = block;
runStart = y;
}
flushRun(target, x, z, runStart, height, activeBlock);
}
}
}
private void flushRun(ChunkData target, int x, int z, int startY, int endY, BlockData block) {
if (block == null || startY < 0 || endY <= startY) {
return;
}
int minY = target.getMinHeight();
if (endY - startY == 1) {
target.setBlock(x, startY + minY, z, block);
return;
}
target.setRegion(x, startY + minY, z, x + 1, endY + minY, z + 1, block);
}
}
@@ -0,0 +1,82 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.generation.chunk;
import art.arcane.iris.generation.block.IrisCustomData;
import art.arcane.iris.platform.bukkit.nms.INMS;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import org.bukkit.block.data.BlockData;
import org.bukkit.generator.ChunkGenerator.ChunkData;
public final class ChunkDataOutput {
private ChunkDataOutput() {
}
public static void apply(ChunkData target, SectionBuffer blocks) {
if (INMS.get().applyChunkDataBlocks(target, blocks)) {
return;
}
int height = blocks.getHeight();
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
BlockData activeBlock = null;
int runStart = -1;
for (int y = 0; y < height; y++) {
NativeBlockState state = blocks.getStoredRaw(x, y, z);
BlockData block = state == null ? null : (BlockData) state.nativeHandle();
if (block instanceof IrisCustomData custom) {
block = custom.getBase();
}
if (block == null) {
flushRun(target, x, z, runStart, y, activeBlock);
activeBlock = null;
runStart = -1;
continue;
}
if (activeBlock != null && activeBlock.equals(block)) {
continue;
}
flushRun(target, x, z, runStart, y, activeBlock);
activeBlock = block;
runStart = y;
}
flushRun(target, x, z, runStart, height, activeBlock);
}
}
}
private static void flushRun(ChunkData target, int x, int z, int startY, int endY, BlockData block) {
if (block == null || startY < 0 || endY <= startY) {
return;
}
int minY = target.getMinHeight();
if (endY - startY == 1) {
target.setBlock(x, startY + minY, z, block);
return;
}
target.setRegion(x, startY + minY, z, x + 1, endY + minY, z + 1, block);
}
}
@@ -20,7 +20,7 @@ package art.arcane.iris.generation.chunk;
import art.arcane.volmlib.util.hunk.Hunk;
public final class ColumnExtentListeningHunk<T> implements Hunk<T>, ColumnExtent {
public final class ColumnExtentListeningHunk<T> implements Hunk<T>, ColumnExtent, ColumnRunHunk<T> {
private final Hunk<T> source;
private final Listener<T> listener;
@@ -35,6 +35,12 @@ public final class ColumnExtentListeningHunk<T> implements Hunk<T>, ColumnExtent
source.setRaw(x, y, z, t);
}
@Override
public void fillColumnRun(int x, int z, int yFromInclusive, int yToInclusive, T value) {
listener.onRun(x, z, yFromInclusive, yToInclusive, value);
ColumnRunHunk.fillColumnRun(source, x, z, yFromInclusive, yToInclusive, value);
}
@Override
public T getRaw(int x, int y, int z) {
return source.getRaw(x, y, z);
@@ -63,5 +69,11 @@ public final class ColumnExtentListeningHunk<T> implements Hunk<T>, ColumnExtent
@FunctionalInterface
public interface Listener<T> {
void onWrite(int x, int y, int z, T value);
default void onRun(int x, int z, int yFromInclusive, int yToInclusive, T value) {
for (int y = yToInclusive; y >= yFromInclusive; y--) {
onWrite(x, y, z, value);
}
}
}
}
@@ -0,0 +1,36 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.generation.chunk;
import art.arcane.volmlib.util.hunk.Hunk;
public interface ColumnRunHunk<T> {
void fillColumnRun(int x, int z, int yFromInclusive, int yToInclusive, T value);
@SuppressWarnings("unchecked")
static <T> void fillColumnRun(Hunk<T> hunk, int x, int z, int yFromInclusive, int yToInclusive, T value) {
if (hunk instanceof ColumnRunHunk<?> runs) {
((ColumnRunHunk<T>) runs).fillColumnRun(x, z, yFromInclusive, yToInclusive, value);
return;
}
for (int y = yToInclusive; y >= yFromInclusive; y--) {
hunk.setRaw(x, y, z, value);
}
}
}
@@ -0,0 +1,268 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.generation.chunk;
import art.arcane.iris.generation.block.BoundBlockState;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.nativelib.terrain.NativeSectionVolume;
import art.arcane.volmlib.util.hunk.Hunk;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.Arrays;
public final class SectionBuffer implements Hunk<NativeBlockState>, NativeSectionVolume, ColumnExtent, ColumnRunHunk<NativeBlockState> {
private static final BoundBlockState AIR = BoundBlockState.of("AIR");
private static final VarHandle SECTIONS = MethodHandles.arrayElementVarHandle(short[][].class);
private static final VarHandle COLUMN_TOPS = MethodHandles.arrayElementVarHandle(int[].class);
private static final int SECTION_CELLS = 4096;
private static final int MAX_PALETTE_ID = 0xFFFF;
private static final int INITIAL_PALETTE_CAPACITY = 64;
private static final int COLUMN_SLICE_SHIFT = 5;
private final int height;
private final short[][] sections;
private final int[] columnTops;
private final Object paletteLock = new Object();
private PaletteEntry[] slots;
private NativeBlockState[] states;
private int paletteSize;
public SectionBuffer(int height) {
if (height <= 0) {
throw new IllegalArgumentException("Unsupported section buffer height " + height);
}
this.height = height;
this.sections = new short[(height + 15) >> 4][];
this.columnTops = new int[16 << COLUMN_SLICE_SHIFT];
Arrays.fill(columnTops, -1);
this.slots = new PaletteEntry[INITIAL_PALETTE_CAPACITY << 1];
this.states = new NativeBlockState[INITIAL_PALETTE_CAPACITY];
this.paletteSize = 1;
}
@Override
public int getWidth() {
return 16;
}
@Override
public int getHeight() {
return height;
}
@Override
public int getDepth() {
return 16;
}
public int paletteId(NativeBlockState state) {
if (state == null) {
return 0;
}
PaletteEntry[] table = slots;
int mask = table.length - 1;
int slot = System.identityHashCode(state) & mask;
PaletteEntry entry;
while ((entry = table[slot]) != null) {
if (entry.state == state) {
return entry.id;
}
slot = (slot + 1) & mask;
}
return insert(state);
}
@Override
public void setRaw(int x, int y, int z, NativeBlockState t) {
if (t == null) {
return;
}
writableSection(y >> 4)[((y & 15) << 8) | (z << 4) | x] = (short) paletteId(t);
raiseTop((x << COLUMN_SLICE_SHIFT) | z, y);
}
@Override
public NativeBlockState getRaw(int x, int y, int z) {
if (y < 0 || y >= height) {
return AIR.get();
}
short[] section = sections[y >> 4];
if (section != null) {
int id = section[((y & 15) << 8) | (z << 4) | x] & 0xFFFF;
if (id != 0) {
return state(id);
}
}
return AIR.get();
}
@Override
public NativeBlockState getStoredRaw(int x, int y, int z) {
if (y < 0 || y >= height) {
return null;
}
short[] section = sections[y >> 4];
if (section == null) {
return null;
}
int id = section[((y & 15) << 8) | (z << 4) | x] & 0xFFFF;
return id == 0 ? null : state(id);
}
@Override
public void fillColumnRun(int x, int z, int yFromInclusive, int yToInclusive, NativeBlockState value) {
if (value == null || yToInclusive < yFromInclusive) {
return;
}
fillColumnRun(x, z, yFromInclusive, yToInclusive, paletteId(value));
}
public void fillColumnRun(int localX, int localZ, int yFromInclusive, int yToInclusive, int paletteId) {
if (paletteId == 0 || yToInclusive < yFromInclusive) {
return;
}
short value = (short) paletteId;
int column = (localZ << 4) | localX;
int y = yFromInclusive;
while (y <= yToInclusive) {
int sectionIndex = y >> 4;
short[] section = writableSection(sectionIndex);
int last = Math.min(yToInclusive, (sectionIndex << 4) | 15);
int end = ((last & 15) << 8) | column;
for (int index = ((y & 15) << 8) | column; index <= end; index += 256) {
section[index] = value;
}
y = last + 1;
}
raiseTop((localX << COLUMN_SLICE_SHIFT) | localZ, yToInclusive);
}
@Override
public int highestStoredY(int x, int z) {
return (int) COLUMN_TOPS.getOpaque(columnTops, (x << COLUMN_SLICE_SHIFT) | z);
}
@Override
public int sectionCount() {
return sections.length;
}
@Override
public short[] section(int sectionIndex) {
return sections[sectionIndex];
}
@Override
public NativeBlockState[] palette() {
synchronized (paletteLock) {
return Arrays.copyOf(states, paletteSize);
}
}
private short[] writableSection(int sectionIndex) {
short[] section = sections[sectionIndex];
if (section != null) {
return section;
}
short[] created = new short[SECTION_CELLS];
short[] witness = (short[]) SECTIONS.compareAndExchange(sections, sectionIndex, (short[]) null, created);
return witness == null ? created : witness;
}
private void raiseTop(int column, int y) {
int top = (int) COLUMN_TOPS.getOpaque(columnTops, column);
while (y > top) {
int witness = (int) COLUMN_TOPS.compareAndExchange(columnTops, column, top, y);
if (witness == top) {
return;
}
top = witness;
}
}
private NativeBlockState state(int id) {
NativeBlockState[] current = states;
if (id < current.length) {
NativeBlockState state = current[id];
if (state != null) {
return state;
}
}
synchronized (paletteLock) {
return states[id];
}
}
private int insert(NativeBlockState state) {
synchronized (paletteLock) {
PaletteEntry[] table = slots;
int mask = table.length - 1;
int slot = System.identityHashCode(state) & mask;
PaletteEntry entry;
while ((entry = table[slot]) != null) {
if (entry.state == state) {
return entry.id;
}
slot = (slot + 1) & mask;
}
int id = paletteSize;
if (id > MAX_PALETTE_ID) {
throw new IllegalStateException("A chunk wrote more than " + MAX_PALETTE_ID + " distinct block state instances");
}
NativeBlockState[] current = states;
if (id == current.length) {
current = Arrays.copyOf(current, current.length << 1);
}
current[id] = state;
states = current;
paletteSize = id + 1;
PaletteEntry created = new PaletteEntry(state, id);
if (id << 1 >= table.length) {
slots = rehash(table, created);
} else {
table[slot] = created;
}
return id;
}
}
private static PaletteEntry[] rehash(PaletteEntry[] table, PaletteEntry created) {
PaletteEntry[] grown = new PaletteEntry[table.length << 1];
for (PaletteEntry entry : table) {
if (entry != null) {
place(grown, entry);
}
}
place(grown, created);
return grown;
}
private static void place(PaletteEntry[] table, PaletteEntry entry) {
int mask = table.length - 1;
int slot = System.identityHashCode(entry.state) & mask;
while (table[slot] != null) {
slot = (slot + 1) & mask;
}
table[slot] = entry;
}
private record PaletteEntry(NativeBlockState state, int id) {
}
}
@@ -26,7 +26,7 @@ public class MultiBurst extends MultiBurstSupport {
private final AtomicInteger parallelismBaseline = new AtomicInteger();
public MultiBurst() {
this("Iris");
this("Iris", Thread.MIN_PRIORITY, MultiBurstSupport::performanceProcessors);
}
public MultiBurst(String name) {
@@ -140,8 +140,15 @@ public class MultiBurst extends MultiBurstSupport {
static final class HydrologyBurst extends MultiBurst {
HydrologyBurst() {
super("Iris Hydrology", () -> Math.max(2,
IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism())));
super("Iris Hydrology", HydrologyBurst::plannerParallelism);
}
static int plannerParallelism() {
return plannerParallelism(MultiBurstSupport.performanceProcessors());
}
static int plannerParallelism(int performanceProcessors) {
return Math.max(4, performanceProcessors);
}
@Override
@@ -79,6 +79,14 @@ public final class IrisOreBands {
return minY <= maxY;
}
public int lowestY() {
return minY;
}
public int highestY() {
return maxY;
}
public boolean contains(int y) {
return y >= minY && y <= maxY;
}
@@ -1,6 +1,7 @@
package art.arcane.iris.generation.hydrology;
import art.arcane.iris.generation.runtime.GenerationClosedException;
import art.arcane.iris.generation.runtime.GenerationCounter;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.iris.generation.stream.ProvisionalSampling;
@@ -43,7 +44,7 @@ public final class HydrologyTileCache implements AutoCloseable {
private static final byte AROUND_EMPTY = 1;
private static final byte AROUND_PUBLISHED = 2;
private static final int MAXIMUM_PREGENERATION_TILES = 4096;
private static final int PREGENERATION_HALO_BLOCKS = 512;
private static final int PREGENERATION_HALO_BLOCKS = 256;
private static final HydrologyColumnSample[] NO_COLUMNS = new HydrologyColumnSample[CHUNK_COLUMN_COUNT];
private static final Cache<SharedTileKey, HydrologyTile> SHARED_TILES = Caffeine.newBuilder()
.maximumWeight(HydrologyCacheBudget.sharedStudioBytes())
@@ -499,6 +500,9 @@ public final class HydrologyTileCache implements AutoCloseable {
}
public Optional<HydrologyColumnSample> columnAt(int blockX, int blockZ) {
if (GenerationCounter.ENABLED) {
GenerationCounter.HYDROLOGY_LOOKUPS.increment();
}
return chunkColumns(blockX, blockZ).columnAt(blockX, blockZ);
}
@@ -507,6 +511,9 @@ public final class HydrologyTileCache implements AutoCloseable {
if (!waitsForbidden()) {
return HydrologyColumnSnapshot.ready(columnAt(blockX, blockZ).orElse(null));
}
if (GenerationCounter.ENABLED) {
GenerationCounter.HYDROLOGY_LOOKUPS.increment();
}
int chunkX = Math.floorDiv(blockX, CHUNK_SIZE);
int chunkZ = Math.floorDiv(blockZ, CHUNK_SIZE);
ChunkColumns local = localChunks(cacheEpoch.get()).get(chunkX, chunkZ);
@@ -66,6 +66,24 @@ final class CaveCarveScratch {
final int[] tileLocalX = new int[4];
final int[] tileLocalZ = new int[4];
final int[] tileTopY = new int[4];
final long[] rowCarveBits = new long[4];
final long[] cellPassBits = new long[4];
final CaveDensityLattice cellLattice = new CaveDensityLattice();
final double[] cellThresholdMin = new double[4];
final double[] cellThresholdMax = new double[4];
final double[] cellThresholdMaxBoost = new double[4];
final int[] cellBreakFloorMin = new int[4];
final int[] cellCeilingFadeStart = new int[4];
final int[] cellTopY = new int[4];
final int[] cellMinTopY = new int[4];
final boolean[] cellProtected = new boolean[4];
final int[] columnProtectStartY = new int[256];
final int[] columnProtectEndY = new int[256];
int[] rowActiveCount = new int[0];
long solidCells;
long openCells;
long mixedCells;
long exactRows;
CaveFieldModuleState[] activeModules = new CaveFieldModuleState[0];
double[] activeModuleRemainingMin = new double[0];
double[] activeModuleRemainingMax = new double[0];
@@ -79,10 +97,33 @@ final class CaveCarveScratch {
int adaptiveGeometryStep = -1;
int adaptiveGeometryAxisCells = -1;
boolean fullWeightsInitialized;
int[] latticeY = new int[0];
int[] latticeSnapY = new int[0];
double[] latticeWarpA = new double[0];
double[] latticeWarpB = new double[0];
double[] latticeWarpedX = new double[0];
double[] latticeWarpedY = new double[0];
double[] latticeWarpedZ = new double[0];
double[] latticeDensity = new double[0];
void releaseReferences() {
Arrays.fill(sectionMatter, null);
Arrays.fill(sectionSlices, null);
Arrays.fill(densityColumns, null);
cellLattice.releaseReferences();
}
void ensureLatticeBatch(int size) {
if (latticeY.length >= size) {
return;
}
latticeY = new int[size];
latticeSnapY = new int[size];
latticeWarpA = new double[size];
latticeWarpB = new double[size];
latticeWarpedX = new double[size];
latticeWarpedY = new double[size];
latticeWarpedZ = new double[size];
latticeDensity = new double[size];
}
}
@@ -0,0 +1,33 @@
package art.arcane.iris.generation.mantle;
final class CaveCellKernel {
static final int NONE = 0;
static final int ALL = 1;
static final int MIXED = 2;
private static final double GUARD_ULPS = 32D;
private static final double MAXIMUM_MAGNITUDE = 0x1p500;
private CaveCellKernel() {
}
static int decide(double d00, double d01, double d10, double d11, double thresholdLow, double thresholdHigh) {
double low = Math.min(Math.min(d00, d01), Math.min(d10, d11));
double high = Math.max(Math.max(d00, d01), Math.max(d10, d11));
double magnitude = Math.max(Math.abs(low), Math.abs(high));
if (!(magnitude < MAXIMUM_MAGNITUDE)) {
return MIXED;
}
double guard = Math.ulp(magnitude) * GUARD_ULPS;
if (high + guard <= thresholdLow) {
return ALL;
}
if (low - guard > thresholdHigh) {
return NONE;
}
return MIXED;
}
static double threshold(double localThreshold, double normalizationFactor, double inverseNormalization) {
return (localThreshold * normalizationFactor) * inverseNormalization;
}
}
@@ -5,6 +5,7 @@ import art.arcane.iris.generation.cache.ConcurrentClockCache;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReferenceArray;
@@ -22,42 +23,63 @@ final class CaveDensityGrid {
private static final AtomicInteger OWNERS = new AtomicInteger();
private final ConcurrentClockCache<Column> columns;
private final ConcurrentClockCache<Column> warpColumns;
private final ConcurrentHashMap<Object, Owner> owners = new ConcurrentHashMap<>();
CaveDensityGrid(int capacity) {
columns = new ConcurrentClockCache<>(capacity);
warpColumns = new ConcurrentClockCache<>(Math.max(1, capacity >> 2));
}
static int nextOwner() {
return OWNERS.getAndIncrement();
Owner owner(Object fieldKey) {
return owners.computeIfAbsent(fieldKey, ignored -> new Owner(OWNERS.getAndIncrement()));
}
Column column(Object owner, int ownerId, int height, int latticeX, int latticeZ) {
long key = (long) (ownerId & 0xFFFF) << 48 | (latticeX & COORDINATE_MASK) << 24 | (latticeZ & COORDINATE_MASK);
Column cached = columns.get(key);
if (cached == null) {
cached = columns.putIfAbsent(key, new Column(owner, latticeX, latticeZ, height));
}
return cached.matches(owner, latticeX, latticeZ) ? cached : null;
Column column(Owner owner, int height, int latticeX, int latticeZ) {
return column(columns, owner, height, latticeX, latticeZ);
}
Column warpColumn(Owner owner, int height, int latticeX, int latticeZ) {
return column(warpColumns, owner, height, latticeX, latticeZ);
}
void clear() {
columns.clear();
warpColumns.clear();
owners.clear();
}
private static Column column(ConcurrentClockCache<Column> cache, Owner owner, int height, int latticeX, int latticeZ) {
long key = (long) (owner.id & 0xFFFF) << 48 | (latticeX & COORDINATE_MASK) << 24 | (latticeZ & COORDINATE_MASK);
Column cached = cache.get(key);
if (cached == null) {
cached = cache.putIfAbsent(key, new Column(owner, latticeX, latticeZ, height));
}
return cached.matches(owner, latticeX, latticeZ) ? cached : null;
}
static final class Owner {
private final int id;
private Owner(int id) {
this.id = id;
}
}
static final class Column {
private final Object owner;
private final Owner owner;
private final int latticeX;
private final int latticeZ;
private final AtomicReferenceArray<double[]> segments;
private Column(Object owner, int latticeX, int latticeZ, int height) {
private Column(Owner owner, int latticeX, int latticeZ, int height) {
this.owner = owner;
this.latticeX = latticeX;
this.latticeZ = latticeZ;
segments = new AtomicReferenceArray<>(Math.ceilDiv(Math.max(1, height), SEGMENT_SIZE));
}
boolean matches(Object owner, int latticeX, int latticeZ) {
boolean matches(Owner owner, int latticeX, int latticeZ) {
return this.owner == owner && this.latticeX == latticeX && this.latticeZ == latticeZ;
}
@@ -0,0 +1,74 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.generation.mantle;
import java.util.Arrays;
final class CaveDensityLattice {
static final int CORNERS = 9;
final CaveDensityGrid.Column[] densityColumns = new CaveDensityGrid.Column[CORNERS];
final CaveDensityGrid.Column[] warpColumns = new CaveDensityGrid.Column[CORNERS];
private double[] values = new double[0];
private byte[] rowCells = new byte[0];
private short[] rowCorners = new short[0];
private int minimumY;
private int rows;
void reset(int minimumY, int rows) {
this.minimumY = minimumY;
this.rows = rows;
if (rowCells.length < rows) {
rowCells = new byte[rows];
rowCorners = new short[rows];
}
if (values.length < rows * CORNERS) {
values = new double[rows * CORNERS];
}
Arrays.fill(rowCells, 0, rows, (byte) 0);
Arrays.fill(rowCorners, 0, rows, (short) 0);
}
void markRow(int y, int cells, int corners) {
int row = y - minimumY;
rowCells[row] = (byte) cells;
rowCorners[row] = (short) corners;
}
int cells(int y) {
return rowCells[y - minimumY];
}
boolean needs(int corner, int y) {
return (rowCorners[y - minimumY] & (1 << corner)) != 0;
}
double density(int corner, int y) {
return values[corner * rows + (y - minimumY)];
}
void store(int corner, int y, double density) {
values[corner * rows + (y - minimumY)] = density;
}
void releaseReferences() {
Arrays.fill(densityColumns, null);
Arrays.fill(warpColumns, null);
}
}
@@ -0,0 +1,51 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.generation.mantle;
import art.arcane.volmlib.util.noise.program.NoiseProgram;
import art.arcane.volmlib.util.noise.program.PrimitiveKind;
record CaveFieldKey(long signature, long probe, NoiseProgram identity) {
private static final double[][] PROBE_POINTS = {
{0.5D, 3.25D, -7.75D},
{-1031.125D, 77.5D, 2049.875D},
{16D, 255D, -16D},
{123_456.375D, -40.25D, -98_765.625D}
};
static CaveFieldKey of(NoiseProgram program) {
if (!shareable(program)) {
return new CaveFieldKey(program.structuralSignature(), 0L, program);
}
long probe = 0xCBF29CE484222325L;
for (double[] point : PROBE_POINTS) {
probe = (probe ^ Double.doubleToRawLongBits(program.sampleSigned3D(point[0], point[1], point[2]))) * 0x100000001B3L;
}
return new CaveFieldKey(program.structuralSignature(), probe, null);
}
private static boolean shareable(NoiseProgram program) {
for (int node = 0; node < program.nodeCount(); node++) {
if (program.nodeOpaque(node) || program.nodeKernel(node).kind() == PrimitiveKind.GENERIC) {
return false;
}
}
return true;
}
}
@@ -20,10 +20,10 @@ package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.cave.IrisCaveFieldModule;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.volmlib.util.noise.CNG;
import art.arcane.volmlib.util.noise.program.NoiseProgram;
final class CaveFieldModuleState {
final CNG density;
final NoiseProgram density;
final int minY;
final int maxY;
final double weight;
@@ -32,7 +32,7 @@ final class CaveFieldModuleState {
final double minContribution;
final double maxContribution;
CaveFieldModuleState(IrisCaveFieldModule module, CNG density) {
CaveFieldModuleState(IrisCaveFieldModule module, NoiseProgram density) {
IrisRange range = module.getVerticalRange();
this.density = density;
this.minY = (int) Math.floor(range.getMin());
@@ -47,12 +47,12 @@ final class CaveFieldModuleState {
}
double sample(double x, int y, double z) {
double sampled = density.noiseFastSigned3D(x, y, z);
double sampled = density.sampleSigned3D(x, y, z);
return invert ? (threshold - sampled) * weight : (sampled - threshold) * weight;
}
double sample(double x, double y, double z) {
double sampled = density.noiseFastSigned3D(x, y, z);
double sampled = density.sampleSigned3D(x, y, z);
return invert ? (threshold - sampled) * weight : (sampled - threshold) * weight;
}
}
@@ -20,12 +20,15 @@ package art.arcane.iris.generation.mantle;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.GenerationCounter;
import art.arcane.iris.generation.cave.IrisCaveFieldModule;
import art.arcane.iris.generation.cave.IrisCaveProfile;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.IrisStyledRange;
import art.arcane.volmlib.util.noise.CNG;
import art.arcane.volmlib.util.noise.program.NoiseProgram;
import art.arcane.volmlib.util.noise.program.NoiseScratch;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
@@ -43,6 +46,7 @@ import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinWorkerThread;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.LongAdder;
public class IrisCaveCarver3D {
private static final byte LIQUID_AIR = 0;
@@ -62,13 +66,23 @@ public class IrisCaveCarver3D {
private static final int MAXIMUM_DENSITY_WORKERS = 4;
private static final int[] LATTICE_TILE_OFFSETS = {0, 1, 16, 17};
private static final int STANDALONE_DENSITY_GRID_COLUMNS = 1024;
private static final int[] CELL_CORNER_MASKS = {0b000011011, 0b000110110, 0b011011000, 0b110110000};
private static final long[] CELL_Z_MASKS = {0x00FF00FF00FF00FFL, 0xFF00FF00FF00FF00L};
private static final LongAdder SOLID_CELLS = new LongAdder();
private static final LongAdder OPEN_CELLS = new LongAdder();
private static final LongAdder MIXED_CELLS = new LongAdder();
private static final LongAdder EXACT_ROWS = new LongAdder();
private static final double WARP_B_OFFSET_X = 31.37D;
private static final double WARP_B_OFFSET_Y = 17.21D;
private static final double WARP_B_OFFSET_Z = 23.91D;
private static final ThreadLocal<NoiseScratch> NOISE_SCRATCH = ThreadLocal.withInitial(NoiseScratch::new);
private final Engine engine;
private final IrisData data;
private final IrisCaveProfile profile;
private final CNG baseDensity;
private final CNG detailDensity;
private final CNG warpDensity;
private final NoiseProgram baseDensity;
private final NoiseProgram detailDensity;
private final NoiseProgram warpDensity;
private final CNG surfaceBreakDensity;
private final RNG thresholdRng;
private final CaveFieldModuleState[] modules;
@@ -98,7 +112,8 @@ public class IrisCaveCarver3D {
private final ThreadLocal<Boolean> carving = ThreadLocal.withInitial(() -> false);
private final ThreadLocal<CaveCarveScratch> scratchCache = ThreadLocal.withInitial(CaveCarveScratch::new);
private final CaveDensityGrid densityGrid;
private final int densityGridOwner = CaveDensityGrid.nextOwner();
private final CaveDensityGrid.Owner densityOwner;
private final CaveDensityGrid.Owner warpOwner;
public IrisCaveCarver3D(Engine engine, IrisCaveProfile profile) {
this(engine, profile, new CaveDensityGrid(STANDALONE_DENSITY_GRID_COLUMNS));
@@ -116,9 +131,9 @@ public class IrisCaveCarver3D {
List<CaveFieldModuleState> moduleStates = new ArrayList<>();
RNG baseRng = new RNG(engine.getSeedManager().getCarve());
this.baseDensity = profile.getBaseDensityStyle().create(baseRng.nextParallelRNG(934_447), data);
this.detailDensity = profile.getDetailDensityStyle().create(baseRng.nextParallelRNG(612_991), data);
this.warpDensity = profile.getWarpStyle().create(baseRng.nextParallelRNG(770_713), data);
this.baseDensity = NoiseProgram.compileSigned3D(profile.getBaseDensityStyle().create(baseRng.nextParallelRNG(934_447), data));
this.detailDensity = NoiseProgram.compileSigned3D(profile.getDetailDensityStyle().create(baseRng.nextParallelRNG(612_991), data));
this.warpDensity = NoiseProgram.compileSigned3D(profile.getWarpStyle().create(baseRng.nextParallelRNG(770_713), data));
this.surfaceBreakDensity = profile.getSurfaceBreakStyle().create(baseRng.nextParallelRNG(341_219), data);
this.thresholdRng = baseRng.nextParallelRNG(489_112);
this.baseWeight = profile.getBaseWeight();
@@ -136,7 +151,7 @@ public class IrisCaveCarver3D {
int index = 0;
for (IrisCaveFieldModule module : profile.getModules()) {
CNG moduleDensity = module.getStyle().create(baseRng.nextParallelRNG(1_000_003L + (index * 65_537L)), data);
CaveFieldModuleState state = new CaveFieldModuleState(module, moduleDensity);
CaveFieldModuleState state = new CaveFieldModuleState(module, NoiseProgram.compileSigned3D(moduleDensity));
moduleStates.add(state);
weight += Math.abs(state.weight);
index++;
@@ -153,6 +168,25 @@ public class IrisCaveCarver3D {
verticalDensityStep = parallelDensity ? Math.max(1, engine.getDimension().getCaveDensitySamplingStep()) : 1;
worldMinimumY = engine.getWorld().minHeight();
densityMaximumY = engine.getWorld().getHeight() - 1;
CaveFieldKey warpKey = CaveFieldKey.of(warpDensity);
warpOwner = densityGrid.owner(new WarpFieldKey(warpKey));
densityOwner = densityGrid.owner(new DensityFieldKey(hasWarp, warpKey, Double.doubleToRawLongBits(warpStrength),
CaveFieldKey.of(baseDensity), Double.doubleToRawLongBits(baseWeight), CaveFieldKey.of(detailDensity),
Double.doubleToRawLongBits(detailWeight), Double.doubleToRawLongBits(inverseNormalization),
moduleFieldKeys(modules)));
}
private static List<ModuleFieldKey> moduleFieldKeys(CaveFieldModuleState[] modules) {
List<ModuleFieldKey> keys = new ArrayList<>(modules.length);
for (CaveFieldModuleState module : modules) {
keys.add(new ModuleFieldKey(CaveFieldKey.of(module.density), module.minY, module.maxY,
Double.doubleToRawLongBits(module.weight), Double.doubleToRawLongBits(module.threshold), module.invert));
}
return List.copyOf(keys);
}
public static CellCounters cellCounters() {
return new CellCounters(SOLID_CELLS.sum(), OPEN_CELLS.sum(), MIXED_CELLS.sum(), EXACT_ROWS.sum());
}
public int carve(MantleWriter writer, int chunkX, int chunkZ) {
@@ -504,7 +538,9 @@ public class IrisCaveCarver3D {
int minimumY = pass.minimumY() + index * rows;
int maximumY = Math.min(pass.maximumY(), minimumY + rows - 1);
if (minimumY <= maximumY) {
tasks.add(() -> classifyRows(pass, minimumY, maximumY, classified));
tasks.add(pass.cellKernel()
? () -> classifyCellRows(pass, minimumY, maximumY, classified)
: () -> classifyRows(pass, minimumY, maximumY, classified));
}
}
runDensityTasks(tasks, pool);
@@ -697,10 +733,25 @@ public class IrisCaveCarver3D {
int[] columns, int[] columnTops, int columnCount, double[] thresholds,
long[] fluidBoundaries, boolean[] surfaceBreakColumns, int[] surfaceBreakFloors,
double surfaceBreakBoost, double[] verticalFade, CaveCarveScratch sourceScratch,
int adaptiveStep, double adaptiveMargin, int[] fluidTops, MatterCavern[] matterByY
int adaptiveStep, double adaptiveMargin, int[] fluidTops, MatterCavern[] matterByY, boolean cellKernel
) {
}
public record CellCounters(long solidCells, long openCells, long mixedCells, long exactRows) {
public long decided() {
return solidCells + openCells;
}
public long evaluated() {
return mixedCells;
}
public String machineLine() {
return "IRIS_GENPROBE_CARVE_CELLS solid=" + solidCells + " open=" + openCells + " mixed=" + mixedCells
+ " exact_rows=" + exactRows;
}
}
private static final class DensityTask implements Runnable, ForkJoinPool.ManagedBlocker {
private final Runnable work;
private final AtomicBoolean claimed = new AtomicBoolean();
@@ -795,7 +846,7 @@ public class IrisCaveCarver3D {
byte[] classified = skipExistingCarved ? null : classifyPassParallel(new DensityPass(
x0, z0, minY, passMaxY, activeColumnIndices, activeColumnTopY, activeColumnCount,
passThreshold, surfaceFluidBoundaries, surfaceBreakColumn, surfaceBreakFloorY,
surfaceBreakThresholdBoost, verticalEdgeFade, scratch, 0, 0D, fluidMaxY, matterByY));
surfaceBreakThresholdBoost, verticalEdgeFade, scratch, 0, 0D, fluidMaxY, matterByY, false));
for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) {
int sectionMinY = Math.max(minY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex));
@@ -944,12 +995,26 @@ public class IrisCaveCarver3D {
adaptiveSampleStep,
effectiveAdaptiveSampleStep
);
boolean cellKernel = !skipExistingCarved && usesCellKernel(effectiveAdaptiveSampleStep);
if (cellKernel) {
prepareCellPlan(scratch, x0, z0, minY, passMaxY, activeColumnIndices, activeColumnCount,
surfaceFluidBoundaries, surfaceBreakThresholdBoost);
}
byte[] classified = skipExistingCarved ? null : classifyPassParallel(new DensityPass(
x0, z0, minY, passMaxY, activeColumnIndices, activeColumnTopY, activeColumnCount,
passThreshold, surfaceFluidBoundaries, surfaceBreakColumn, surfaceBreakFloorY,
surfaceBreakThresholdBoost, verticalEdgeFade, scratch, effectiveAdaptiveSampleStep, effectiveAdaptiveThresholdMargin,
fluidMaxY, matterByY));
fluidMaxY, matterByY, cellKernel));
if (cellKernel) {
if (classified == null) {
fillCellLattice(scratch, scratch, x0, z0, minY, passMaxY);
}
return emitCellPass(chunk, scratch, classified, x0, z0, minY, passMaxY, minSection, maxSection,
activeColumnIndices, activeColumnTopY, activeColumnCount, surfaceFluidBoundaries,
surfaceBreakThresholdBoost, verticalEdgeFade, fluidMaxY, matterByY, fluidSupportPlan);
}
for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) {
int sectionMinY = Math.max(minY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex));
@@ -1059,6 +1124,629 @@ public class IrisCaveCarver3D {
return adaptiveThresholdMargin + ((effectiveAdaptiveSampleStep - adaptiveSampleStep) * ADAPTIVE_DEEP_MARGIN_BOOST);
}
private boolean usesCellKernel(int effectiveAdaptiveSampleStep) {
return effectiveAdaptiveSampleStep == ADAPTIVE_DEEP_SAMPLE_STEP
&& verticalDensityStep == 1
&& parallelDensity
&& (hasWarp || !hasModules);
}
private void prepareCellPlan(
CaveCarveScratch plan,
int x0,
int z0,
int minY,
int passMaxY,
int[] activeColumnIndices,
int activeColumnCount,
long[] surfaceFluidBoundaries,
double surfaceBreakThresholdBoost
) {
int rows = passMaxY - minY + 1;
if (plan.rowActiveCount.length < rows + 1) {
plan.rowActiveCount = new int[rows + 1];
}
int[] rowActiveCount = plan.rowActiveCount;
Arrays.fill(rowActiveCount, 0, rows + 1, 0);
Arrays.fill(plan.cellPassBits, 0L);
Arrays.fill(plan.cellThresholdMin, Double.POSITIVE_INFINITY);
Arrays.fill(plan.cellThresholdMax, Double.NEGATIVE_INFINITY);
Arrays.fill(plan.cellThresholdMaxBoost, Double.NEGATIVE_INFINITY);
Arrays.fill(plan.cellBreakFloorMin, Integer.MAX_VALUE);
Arrays.fill(plan.cellCeilingFadeStart, Integer.MAX_VALUE);
Arrays.fill(plan.cellTopY, Integer.MIN_VALUE);
Arrays.fill(plan.cellMinTopY, Integer.MAX_VALUE);
Arrays.fill(plan.cellProtected, false);
prepareAdaptiveGeometry(plan, ADAPTIVE_DEEP_SAMPLE_STEP, 2, 3);
double[] passThreshold = plan.passThreshold;
for (int activeIndex = 0; activeIndex < activeColumnCount; activeIndex++) {
int column = activeColumnIndices[activeIndex];
int cell = ((column >> 7) << 1) | ((column >> 3) & 1);
int top = plan.columnMaxY[column];
double threshold = passThreshold[column];
plan.cellPassBits[column >> 6] |= 1L << column;
plan.cellThresholdMin[cell] = Math.min(plan.cellThresholdMin[cell], threshold);
plan.cellThresholdMax[cell] = Math.max(plan.cellThresholdMax[cell], threshold);
if (plan.surfaceBreakColumn[column]) {
plan.cellThresholdMaxBoost[cell] = Math.max(plan.cellThresholdMaxBoost[cell], threshold + surfaceBreakThresholdBoost);
plan.cellBreakFloorMin[cell] = Math.min(plan.cellBreakFloorMin[cell], plan.surfaceBreakFloorY[column]);
} else {
plan.cellThresholdMaxBoost[cell] = Math.max(plan.cellThresholdMaxBoost[cell], threshold);
}
if (plan.surfaceCeilingColumn[column]) {
plan.cellCeilingFadeStart[cell] = Math.min(plan.cellCeilingFadeStart[cell], top - SURFACE_CEILING_FADE_DEPTH + 1);
}
plan.cellTopY[cell] = Math.max(plan.cellTopY[cell], top);
plan.cellMinTopY[cell] = Math.min(plan.cellMinTopY[cell], top);
int protectStart = SurfaceFluidBoundaryPlan.NO_BOUNDARY;
int protectEnd = Integer.MIN_VALUE;
if (surfaceFluidBoundaries != null) {
long boundary = surfaceFluidBoundaries[column];
protectStart = SurfaceFluidBoundaryPlan.startY(boundary);
protectEnd = SurfaceFluidBoundaryPlan.endY(boundary);
}
plan.columnProtectStartY[column] = protectStart;
plan.columnProtectEndY[column] = protectEnd;
rowActiveCount[0]++;
rowActiveCount[top - minY + 1]--;
int protectedFrom = Math.max(protectStart, minY);
int protectedTo = Math.min(protectEnd, top);
if (protectedFrom <= protectedTo) {
rowActiveCount[protectedFrom - minY]--;
rowActiveCount[protectedTo - minY + 1]++;
plan.cellProtected[cell] = true;
}
}
for (int row = 1; row < rows; row++) {
rowActiveCount[row] += rowActiveCount[row - 1];
}
int usedCorners = 0;
for (int cell = 0; cell < 4; cell++) {
if (plan.cellTopY[cell] >= minY) {
usedCorners |= CELL_CORNER_MASKS[cell];
}
}
prepareCellLattice(plan, x0 >> CaveDensityGrid.LATTICE_SHIFT, z0 >> CaveDensityGrid.LATTICE_SHIFT, minY, passMaxY,
usedCorners);
}
private void prepareCellLattice(CaveCarveScratch plan, int latticeX, int latticeZ, int minY, int passMaxY, int usedCorners) {
CaveDensityLattice lattice = plan.cellLattice;
lattice.reset(minY, passMaxY - minY + 1);
for (int corner = 0; corner < CaveDensityLattice.CORNERS; corner++) {
boolean used = (usedCorners & (1 << corner)) != 0;
int cornerX = latticeX + (corner / 3);
int cornerZ = latticeZ + (corner % 3);
lattice.densityColumns[corner] = used ? densityGrid.column(densityOwner, densityMaximumY + 1, cornerX, cornerZ) : null;
lattice.warpColumns[corner] = used && hasWarp
? densityGrid.warpColumn(warpOwner, densityMaximumY + 2, cornerX, cornerZ) : null;
}
for (int y = minY; y <= passMaxY; y++) {
if (plan.rowActiveCount[y - minY] < ADAPTIVE_MIN_PLANE_COLUMNS) {
continue;
}
int cells = 0;
int corners = 0;
for (int cell = 0; cell < 4; cell++) {
if (isCellActive(plan, cell, y)) {
cells |= 1 << cell;
corners |= CELL_CORNER_MASKS[cell];
}
}
lattice.markRow(y, cells, corners);
}
}
private void fillCellLattice(CaveCarveScratch scratch, CaveCarveScratch plan, int x0, int z0, int fromY, int toY) {
CaveDensityLattice lattice = plan.cellLattice;
scratch.ensureLatticeBatch(toY - fromY + 1);
int[] latticeY = scratch.latticeY;
double[] density = scratch.latticeDensity;
long reads = 0L;
long samples = 0L;
for (int corner = 0; corner < CaveDensityLattice.CORNERS; corner++) {
CaveDensityGrid.Column column = lattice.densityColumns[corner];
int count = 0;
for (int y = fromY; y <= toY; y++) {
if (!lattice.needs(corner, y)) {
continue;
}
reads++;
double cached = column == null ? Double.NaN : column.get(y);
if (Double.isNaN(cached)) {
latticeY[count++] = y;
} else {
lattice.store(corner, y, cached);
}
}
if (count == 0) {
continue;
}
samples += count;
sampleLatticeColumn(scratch, lattice.warpColumns[corner], x0 + (corner / 3) * ADAPTIVE_DEEP_SAMPLE_STEP,
z0 + (corner % 3) * ADAPTIVE_DEEP_SAMPLE_STEP, count);
for (int index = 0; index < count; index++) {
lattice.store(corner, latticeY[index], density[index]);
if (column != null) {
column.set(latticeY[index], density[index]);
}
}
}
if (GenerationCounter.ENABLED) {
GenerationCounter.CAVE_LATTICE_READS.add(reads);
GenerationCounter.CAVE_LATTICE_SAMPLES.add(samples);
}
}
private void sampleLatticeColumn(CaveCarveScratch scratch, CaveDensityGrid.Column warpColumn, int x, int z, int count) {
int[] latticeY = scratch.latticeY;
double[] density = scratch.latticeDensity;
if (!hasWarp) {
for (int index = 0; index < count; index++) {
density[index] = sampleDensityNoWarpNoModules(x, latticeY[index], z);
}
return;
}
resolveLatticeWarp(scratch, warpColumn, x, z, count);
double[] warpedX = scratch.latticeWarpedX;
double[] warpedY = scratch.latticeWarpedY;
double[] warpedZ = scratch.latticeWarpedZ;
for (int index = 0; index < count; index++) {
density[index] = baseDensity.sampleSigned3D(warpedX[index], warpedY[index], warpedZ[index]) * baseWeight;
}
for (int index = 0; index < count; index++) {
density[index] += detailDensity.sampleSigned3D(warpedX[index], warpedY[index], warpedZ[index]) * detailWeight;
}
for (CaveFieldModuleState module : modules) {
for (int index = 0; index < count; index++) {
int y = latticeY[index];
if (y < module.minY || y > module.maxY) {
continue;
}
double moduleDensity = module.density.sampleSigned3D(warpedX[index], warpedY[index], warpedZ[index]) - module.threshold;
if (module.invert) {
moduleDensity = -moduleDensity;
}
density[index] += moduleDensity * module.weight;
}
}
for (int index = 0; index < count; index++) {
density[index] = density[index] * inverseNormalization;
}
}
private void resolveLatticeWarp(CaveCarveScratch scratch, CaveDensityGrid.Column warpColumn, int x, int z, int count) {
int[] latticeY = scratch.latticeY;
int[] snapY = scratch.latticeSnapY;
double[] warpA = scratch.latticeWarpA;
double[] warpB = scratch.latticeWarpB;
int snapCount = 0;
for (int index = 0; index < count; index++) {
int snap = snapWarp(latticeY[index]);
if (snapCount == 0 || snapY[snapCount - 1] != snap) {
snapY[snapCount++] = snap;
}
}
long computed = 0L;
int runStart = -1;
for (int snapIndex = 0; snapIndex <= snapCount; snapIndex++) {
boolean missing = false;
if (snapIndex < snapCount) {
int snap = snapY[snapIndex];
double cachedA = warpColumn == null ? Double.NaN : warpColumn.get(snap);
double cachedB = warpColumn == null ? Double.NaN : warpColumn.get(snap + 1);
missing = Double.isNaN(cachedA) || Double.isNaN(cachedB);
warpA[snapIndex] = cachedA;
warpB[snapIndex] = cachedB;
}
if (runStart >= 0 && (!missing || snapY[snapIndex] != snapY[snapIndex - 1] + warpResolution)) {
computeLatticeWarpRun(scratch, warpColumn, x, z, runStart, snapIndex);
computed += snapIndex - runStart;
runStart = -1;
}
if (missing && runStart < 0) {
runStart = snapIndex;
}
}
if (GenerationCounter.ENABLED) {
GenerationCounter.CAVE_WARP_SAMPLES.add(computed);
}
double[] warpedX = scratch.latticeWarpedX;
double[] warpedY = scratch.latticeWarpedY;
double[] warpedZ = scratch.latticeWarpedZ;
int snapIndex = 0;
for (int index = 0; index < count; index++) {
int y = latticeY[index];
int snap = snapWarp(y);
while (snapY[snapIndex] != snap) {
snapIndex++;
}
double a = warpA[snapIndex];
double b = warpB[snapIndex];
warpedX[index] = x + (a * warpStrength);
warpedY[index] = y + (b * warpStrength);
warpedZ[index] = z + ((a - b) * 0.5D * warpStrength);
}
}
private void computeLatticeWarpRun(CaveCarveScratch scratch, CaveDensityGrid.Column warpColumn, int x, int z, int from, int to) {
int[] snapY = scratch.latticeSnapY;
double[] warpA = scratch.latticeWarpA;
double[] warpB = scratch.latticeWarpB;
int length = to - from;
if (length == 1) {
warpA[from] = warpDensity.sampleSigned3D(x, snapY[from], z);
} else {
double[] column = scratch.latticeWarpedX;
warpDensity.fillColumn3D(NOISE_SCRATCH.get(), x, z, snapY[from], warpResolution, length, column);
System.arraycopy(column, 0, warpA, from, length);
}
for (int snapIndex = from; snapIndex < to; snapIndex++) {
int snap = snapY[snapIndex];
warpB[snapIndex] = warpDensity.sampleSigned3D(x + WARP_B_OFFSET_X, snap - WARP_B_OFFSET_Y, z + WARP_B_OFFSET_Z);
if (warpColumn != null) {
warpColumn.set(snap, warpA[snapIndex]);
warpColumn.set(snap + 1, warpB[snapIndex]);
}
}
}
private int emitCellPass(
MantleChunk<Matter> chunk,
CaveCarveScratch scratch,
byte[] classified,
int x0,
int z0,
int minY,
int passMaxY,
int minSection,
int maxSection,
int[] activeColumnIndices,
int[] activeColumnTopY,
int activeColumnCount,
long[] surfaceFluidBoundaries,
double surfaceBreakThresholdBoost,
double[] verticalEdgeFade,
int[] fluidMaxY,
MatterCavern[] matterByY,
CaveFluidSupportPlan fluidSupportPlan
) {
int carved = 0;
long[] rowBits = scratch.rowCarveBits;
for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) {
int sectionMinY = Math.max(minY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex));
int sectionMaxY = Math.min(passMaxY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex) + 15);
MatterSlice<MatterCavern> cavernSlice = resolveCavernSlice(scratch, chunk, sectionIndex);
for (int y = sectionMinY; y <= sectionMaxY; y++) {
int fadeIndex = y - minY;
if (scratch.rowActiveCount[fadeIndex] == 0) {
continue;
}
MatterCavern verticalMatter = matterByY[fadeIndex];
if (classified != null) {
carved += emitClassifiedRow(cavernSlice, classified, fadeIndex, y, verticalMatter, fluidSupportPlan);
continue;
}
classifyAdaptiveRow(scratch, scratch, x0, z0, y, minY, activeColumnIndices, activeColumnTopY,
activeColumnCount, surfaceFluidBoundaries, surfaceBreakThresholdBoost, verticalEdgeFade, rowBits);
for (int word = 0; word < 4; word++) {
long bits = rowBits[word];
while (bits != 0L) {
int column = (word << 6) | Long.numberOfTrailingZeros(bits);
bits &= bits - 1L;
int localX = PowerOfTwoCoordinates.unpackLocal16X(column);
int localZ = column & 15;
MatterCavern matter = verticalMatter;
if (verticalMatter != carveLava && y <= fluidMaxY[column]) {
matter = resolveMatter(scratch, verticalMatter, x0 + localX, y, z0 + localZ, column, fluidMaxY,
cellThreshold(scratch, column, y, minY, surfaceBreakThresholdBoost, verticalEdgeFade));
}
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
carved++;
}
}
}
}
flushCellCounters(scratch);
return carved;
}
private int emitClassifiedRow(MatterSlice<MatterCavern> cavernSlice, byte[] classified, int fadeIndex, int y,
MatterCavern verticalMatter, CaveFluidSupportPlan fluidSupportPlan) {
int carved = 0;
int offset = fadeIndex * 256;
for (int column = 0; column < 256; column++) {
byte flags = classified[offset + column];
if ((flags & DENSITY_CARVE) == 0) {
continue;
}
MatterCavern matter = (flags & DENSITY_FLUID) != 0 ? carveFluid : verticalMatter;
writeCavern(cavernSlice, PowerOfTwoCoordinates.unpackLocal16X(column), y, column & 15, matter, fluidSupportPlan);
carved++;
}
return carved;
}
private void classifyCellRows(DensityPass pass, int minimumY, int maximumY, byte[] classified) {
CaveCarveScratch scratch = new CaveCarveScratch();
CaveCarveScratch plan = pass.sourceScratch();
fillCellLattice(scratch, plan, pass.x(), pass.z(), minimumY, maximumY);
long[] rowBits = scratch.rowCarveBits;
for (int y = minimumY; y <= maximumY; y++) {
int fadeIndex = y - pass.minimumY();
if (plan.rowActiveCount[fadeIndex] == 0) {
continue;
}
classifyAdaptiveRow(scratch, plan, pass.x(), pass.z(), y, pass.minimumY(), pass.columns(), pass.columnTops(),
pass.columnCount(), pass.fluidBoundaries(), pass.surfaceBreakBoost(), pass.verticalFade(), rowBits);
MatterCavern verticalMatter = pass.matterByY()[fadeIndex];
int offset = fadeIndex * 256;
for (int word = 0; word < 4; word++) {
long bits = rowBits[word];
while (bits != 0L) {
int column = (word << 6) | Long.numberOfTrailingZeros(bits);
bits &= bits - 1L;
MatterCavern matter = verticalMatter;
if (verticalMatter != carveLava && y <= pass.fluidTops()[column]) {
matter = resolveMatter(scratch, verticalMatter, pass.x() + PowerOfTwoCoordinates.unpackLocal16X(column),
y, pass.z() + (column & 15), column, pass.fluidTops(),
cellThreshold(plan, column, y, pass.minimumY(), pass.surfaceBreakBoost(), pass.verticalFade()));
}
classified[offset + column] = matter == carveFluid ? DENSITY_CARVE | DENSITY_FLUID : DENSITY_CARVE;
}
}
}
flushCellCounters(scratch);
}
private void classifyAdaptiveRow(
CaveCarveScratch scratch,
CaveCarveScratch plan,
int x0,
int z0,
int y,
int minY,
int[] activeColumnIndices,
int[] activeColumnTopY,
int activeColumnCount,
long[] surfaceFluidBoundaries,
double surfaceBreakThresholdBoost,
double[] verticalEdgeFade,
long[] rowBits
) {
Arrays.fill(rowBits, 0L);
if (plan.rowActiveCount[y - minY] >= ADAPTIVE_MIN_PLANE_COLUMNS) {
classifyCellRow(scratch, plan, y, minY, surfaceBreakThresholdBoost, verticalEdgeFade, rowBits);
return;
}
int[] planeColumnIndices = scratch.planeColumnIndices;
double[] planeThresholdLimit = scratch.planeThresholdLimit;
boolean[] planeCarve = scratch.planeCarve;
int planeCount = 0;
for (int activeIndex = 0; activeIndex < activeColumnCount; activeIndex++) {
if (activeColumnTopY[activeIndex] < y) {
continue;
}
int columnIndex = activeColumnIndices[activeIndex];
if (SurfaceFluidBoundaryPlan.protects(surfaceFluidBoundaries, columnIndex, y)) {
continue;
}
planeColumnIndices[planeCount] = columnIndex;
planeThresholdLimit[planeCount] = cellLocalThreshold(plan, columnIndex, y, minY, surfaceBreakThresholdBoost,
verticalEdgeFade) * normalizationFactor;
planeCount++;
}
classifyDensityPlane(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve);
for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) {
if (planeCarve[planeIndex]) {
int columnIndex = planeColumnIndices[planeIndex];
rowBits[columnIndex >> 6] |= 1L << columnIndex;
}
}
scratch.exactRows++;
}
private void classifyCellRow(
CaveCarveScratch scratch,
CaveCarveScratch plan,
int y,
int minY,
double surfaceBreakThresholdBoost,
double[] verticalEdgeFade,
long[] rowBits
) {
CaveDensityLattice lattice = plan.cellLattice;
int activeCells = lattice.cells(y);
if (activeCells == 0) {
return;
}
int fadeIndex = y - minY;
double fade = verticalEdgeFade[fadeIndex];
double closure = plan.surfaceClosureThreshold[fadeIndex];
for (int cell = 0; cell < 4; cell++) {
if ((activeCells & (1 << cell)) == 0) {
continue;
}
int corner = ((cell >> 1) * 3) + (cell & 1);
double d00 = lattice.density(corner, y);
double d01 = lattice.density(corner + 1, y);
double d10 = lattice.density(corner + 3, y);
double d11 = lattice.density(corner + 4, y);
double low = plan.cellThresholdMin[cell] - fade;
if (y >= plan.cellCeilingFadeStart[cell]) {
low = Math.min(low, closure);
}
double high = (y >= plan.cellBreakFloorMin[cell] ? plan.cellThresholdMaxBoost[cell] : plan.cellThresholdMax[cell]) - fade;
int decision = CaveCellKernel.decide(d00, d01, d10, d11,
CaveCellKernel.threshold(low, normalizationFactor, inverseNormalization),
CaveCellKernel.threshold(high, normalizationFactor, inverseNormalization));
boolean uniform = !plan.cellProtected[cell] && y <= plan.cellMinTopY[cell];
if (decision == CaveCellKernel.NONE) {
scratch.solidCells++;
} else if (decision == CaveCellKernel.ALL) {
scratch.openCells++;
if (uniform) {
int word = (cell >> 1) << 1;
long mask = CELL_Z_MASKS[cell & 1];
rowBits[word] |= plan.cellPassBits[word] & mask;
rowBits[word + 1] |= plan.cellPassBits[word + 1] & mask;
} else {
markActiveCellColumns(plan, cell, y, rowBits);
}
} else {
scratch.mixedCells++;
if (uniform && y < plan.cellCeilingFadeStart[cell] && y < plan.cellBreakFloorMin[cell]) {
classifyPlainMixedCell(plan, cell, fade, d00, d01, d10, d11, rowBits);
} else {
classifyMixedCell(plan, cell, y, minY, surfaceBreakThresholdBoost, verticalEdgeFade, d00, d01, d10, d11, rowBits);
}
}
}
}
private void classifyMixedCell(
CaveCarveScratch plan,
int cell,
int y,
int minY,
double surfaceBreakThresholdBoost,
double[] verticalEdgeFade,
double d00,
double d01,
double d10,
double d11,
long[] rowBits
) {
double[] adaptiveTx = plan.adaptiveTx;
double[] adaptiveTz = plan.adaptiveTz;
int firstLocalX = (cell >> 1) << 3;
int firstLocalZ = (cell & 1) << 3;
for (int localX = firstLocalX; localX < firstLocalX + ADAPTIVE_DEEP_SAMPLE_STEP; localX++) {
int rowColumn = PowerOfTwoCoordinates.packLocal16(localX, firstLocalZ);
double tx = adaptiveTx[rowColumn];
double dx0 = d00 + ((d10 - d00) * tx);
double dx1 = d01 + ((d11 - d01) * tx);
double slope = dx1 - dx0;
for (int column = rowColumn; column < rowColumn + ADAPTIVE_DEEP_SAMPLE_STEP; column++) {
if (!isCellColumnActive(plan, column, y)) {
continue;
}
double predictedDensity = dx0 + (slope * adaptiveTz[column]);
if (predictedDensity <= cellThreshold(plan, column, y, minY, surfaceBreakThresholdBoost, verticalEdgeFade)) {
rowBits[column >> 6] |= 1L << column;
}
}
}
}
private void classifyPlainMixedCell(
CaveCarveScratch plan,
int cell,
double fade,
double d00,
double d01,
double d10,
double d11,
long[] rowBits
) {
double[] passThreshold = plan.passThreshold;
double[] adaptiveTx = plan.adaptiveTx;
double[] adaptiveTz = plan.adaptiveTz;
int firstLocalX = (cell >> 1) << 3;
int firstLocalZ = (cell & 1) << 3;
for (int localX = firstLocalX; localX < firstLocalX + ADAPTIVE_DEEP_SAMPLE_STEP; localX++) {
int rowColumn = PowerOfTwoCoordinates.packLocal16(localX, firstLocalZ);
double tx = adaptiveTx[rowColumn];
double dx0 = d00 + ((d10 - d00) * tx);
double dx1 = d01 + ((d11 - d01) * tx);
double slope = dx1 - dx0;
long bits = 0L;
for (int offset = 0; offset < ADAPTIVE_DEEP_SAMPLE_STEP; offset++) {
int column = rowColumn + offset;
double predictedDensity = dx0 + (slope * adaptiveTz[column]);
if (predictedDensity <= CaveCellKernel.threshold(passThreshold[column] - fade, normalizationFactor,
inverseNormalization)) {
bits |= 1L << offset;
}
}
rowBits[rowColumn >> 6] |= bits << (rowColumn & 63);
}
}
private static void markActiveCellColumns(CaveCarveScratch plan, int cell, int y, long[] rowBits) {
int firstLocalX = (cell >> 1) << 3;
int firstLocalZ = (cell & 1) << 3;
for (int localX = firstLocalX; localX < firstLocalX + ADAPTIVE_DEEP_SAMPLE_STEP; localX++) {
int rowColumn = PowerOfTwoCoordinates.packLocal16(localX, firstLocalZ);
for (int column = rowColumn; column < rowColumn + ADAPTIVE_DEEP_SAMPLE_STEP; column++) {
if (isCellColumnActive(plan, column, y)) {
rowBits[column >> 6] |= 1L << column;
}
}
}
}
private static boolean isCellActive(CaveCarveScratch plan, int cell, int y) {
if (y > plan.cellTopY[cell]) {
return false;
}
if (!plan.cellProtected[cell]) {
return true;
}
int firstLocalX = (cell >> 1) << 3;
int firstLocalZ = (cell & 1) << 3;
for (int localX = firstLocalX; localX < firstLocalX + ADAPTIVE_DEEP_SAMPLE_STEP; localX++) {
int rowColumn = PowerOfTwoCoordinates.packLocal16(localX, firstLocalZ);
for (int column = rowColumn; column < rowColumn + ADAPTIVE_DEEP_SAMPLE_STEP; column++) {
if (isCellColumnActive(plan, column, y)) {
return true;
}
}
}
return false;
}
private static boolean isCellColumnActive(CaveCarveScratch plan, int column, int y) {
return (plan.cellPassBits[column >> 6] & (1L << column)) != 0L
&& plan.columnMaxY[column] >= y
&& (y < plan.columnProtectStartY[column] || y > plan.columnProtectEndY[column]);
}
private double cellThreshold(CaveCarveScratch plan, int column, int y, int minY, double surfaceBreakThresholdBoost,
double[] verticalEdgeFade) {
return CaveCellKernel.threshold(cellLocalThreshold(plan, column, y, minY, surfaceBreakThresholdBoost, verticalEdgeFade),
normalizationFactor, inverseNormalization);
}
private double cellLocalThreshold(CaveCarveScratch plan, int column, int y, int minY, double surfaceBreakThresholdBoost,
double[] verticalEdgeFade) {
double localThreshold = plan.passThreshold[column];
if (plan.surfaceBreakColumn[column] && y >= plan.surfaceBreakFloorY[column]) {
localThreshold += surfaceBreakThresholdBoost;
}
localThreshold -= verticalEdgeFade[y - minY];
return applySurfaceCeilingFade(plan, localThreshold, column, y, minY);
}
private static void flushCellCounters(CaveCarveScratch scratch) {
if (scratch.solidCells != 0L) {
SOLID_CELLS.add(scratch.solidCells);
scratch.solidCells = 0L;
}
if (scratch.openCells != 0L) {
OPEN_CELLS.add(scratch.openCells);
scratch.openCells = 0L;
}
if (scratch.mixedCells != 0L) {
MIXED_CELLS.add(scratch.mixedCells);
scratch.mixedCells = 0L;
}
if (scratch.exactRows != 0L) {
EXACT_ROWS.add(scratch.exactRows);
scratch.exactRows = 0L;
}
}
private int carvePassLattice(
MantleChunk<Matter> chunk,
int x0,
@@ -2131,7 +2819,7 @@ public class IrisCaveCarver3D {
}
private boolean classifyDensityPointNoWarpNoModules(int x, int y, int z, double thresholdLimit) {
double density = baseDensity.noiseFastSigned3D(x, y, z) * baseWeight;
double density = baseDensity.sampleSigned3D(x, y, z) * baseWeight;
if ((density + detailMinContribution) > thresholdLimit) {
return false;
}
@@ -2139,7 +2827,7 @@ public class IrisCaveCarver3D {
return true;
}
density += detailDensity.noiseFastSigned3D(x, y, z) * detailWeight;
density += detailDensity.sampleSigned3D(x, y, z) * detailWeight;
return density <= thresholdLimit;
}
@@ -2157,7 +2845,7 @@ public class IrisCaveCarver3D {
return classifyDensityPointNoWarpNoModules(x, y, z, thresholdLimit);
}
double density = baseDensity.noiseFastSigned3D(x, y, z) * baseWeight;
double density = baseDensity.sampleSigned3D(x, y, z) * baseWeight;
if ((density + detailMinContribution + remainingMin[0]) > thresholdLimit) {
return false;
}
@@ -2165,7 +2853,7 @@ public class IrisCaveCarver3D {
return true;
}
density += detailDensity.noiseFastSigned3D(x, y, z) * detailWeight;
density += detailDensity.sampleSigned3D(x, y, z) * detailWeight;
if ((density + remainingMin[0]) > thresholdLimit) {
return false;
}
@@ -2207,7 +2895,7 @@ public class IrisCaveCarver3D {
double warpedX = x + (warpA * warpStrength);
double warpedY = y + (warpB * warpStrength);
double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength);
double density = baseDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * baseWeight;
double density = baseDensity.sampleSigned3D(warpedX, warpedY, warpedZ) * baseWeight;
if ((density + detailMinContribution) > thresholdLimit) {
return false;
}
@@ -2215,7 +2903,7 @@ public class IrisCaveCarver3D {
return true;
}
density += detailDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * detailWeight;
density += detailDensity.sampleSigned3D(warpedX, warpedY, warpedZ) * detailWeight;
return density <= thresholdLimit;
}
@@ -2243,7 +2931,7 @@ public class IrisCaveCarver3D {
double warpedX = x + (warpA * warpStrength);
double warpedY = y + (warpB * warpStrength);
double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength);
double density = baseDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * baseWeight;
double density = baseDensity.sampleSigned3D(warpedX, warpedY, warpedZ) * baseWeight;
if ((density + detailMinContribution + remainingMin[0]) > thresholdLimit) {
return false;
}
@@ -2251,7 +2939,7 @@ public class IrisCaveCarver3D {
return true;
}
density += detailDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * detailWeight;
density += detailDensity.sampleSigned3D(warpedX, warpedY, warpedZ) * detailWeight;
if ((density + remainingMin[0]) > thresholdLimit) {
return false;
}
@@ -2477,8 +3165,8 @@ public class IrisCaveCarver3D {
int latticeZ = z >> CaveDensityGrid.LATTICE_SHIFT;
int slot = ((latticeX & 3) << 2) | (latticeZ & 3);
CaveDensityGrid.Column column = parallelDensity ? scratch.densityColumns[slot] : null;
if (column == null || !column.matches(this, latticeX, latticeZ)) {
column = densityGrid.column(this, densityGridOwner, densityMaximumY + 1, latticeX, latticeZ);
if (column == null || !column.matches(densityOwner, latticeX, latticeZ)) {
column = densityGrid.column(densityOwner, densityMaximumY + 1, latticeX, latticeZ);
if (parallelDensity) {
scratch.densityColumns[slot] = column;
}
@@ -2507,8 +3195,8 @@ public class IrisCaveCarver3D {
}
private double sampleDensityNoWarpNoModules(int x, int y, int z) {
double density = baseDensity.noiseFastSigned3D(x, y, z) * baseWeight;
density += detailDensity.noiseFastSigned3D(x, y, z) * detailWeight;
double density = baseDensity.sampleSigned3D(x, y, z) * baseWeight;
density += detailDensity.sampleSigned3D(x, y, z) * detailWeight;
return density * inverseNormalization;
}
@@ -2519,11 +3207,11 @@ public class IrisCaveCarver3D {
}
CaveFieldModuleState[] localModules = scratch.activeModules;
double density = baseDensity.noiseFastSigned3D(x, y, z) * baseWeight;
density += detailDensity.noiseFastSigned3D(x, y, z) * detailWeight;
double density = baseDensity.sampleSigned3D(x, y, z) * baseWeight;
density += detailDensity.sampleSigned3D(x, y, z) * detailWeight;
for (int moduleIndex = 0; moduleIndex < activeModuleCount; moduleIndex++) {
CaveFieldModuleState module = localModules[moduleIndex];
double moduleDensity = module.density.noiseFastSigned3D(x, y, z) - module.threshold;
double moduleDensity = module.density.sampleSigned3D(x, y, z) - module.threshold;
if (module.invert) {
moduleDensity = -moduleDensity;
}
@@ -2544,8 +3232,8 @@ public class IrisCaveCarver3D {
double warpedX = x + (warpA * warpStrength);
double warpedY = y + (warpB * warpStrength);
double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength);
double density = baseDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * baseWeight;
density += detailDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * detailWeight;
double density = baseDensity.sampleSigned3D(warpedX, warpedY, warpedZ) * baseWeight;
density += detailDensity.sampleSigned3D(warpedX, warpedY, warpedZ) * detailWeight;
return density * inverseNormalization;
}
@@ -2569,11 +3257,11 @@ public class IrisCaveCarver3D {
double warpedX = x + (warpA * warpStrength);
double warpedY = y + (warpB * warpStrength);
double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength);
double density = baseDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * baseWeight;
density += detailDensity.noiseFastSigned3D(warpedX, warpedY, warpedZ) * detailWeight;
double density = baseDensity.sampleSigned3D(warpedX, warpedY, warpedZ) * baseWeight;
density += detailDensity.sampleSigned3D(warpedX, warpedY, warpedZ) * detailWeight;
for (int moduleIndex = 0; moduleIndex < activeModuleCount; moduleIndex++) {
CaveFieldModuleState module = localModules[moduleIndex];
double moduleDensity = module.density.noiseFastSigned3D(warpedX, warpedY, warpedZ) - module.threshold;
double moduleDensity = module.density.sampleSigned3D(warpedX, warpedY, warpedZ) - module.threshold;
if (module.invert) {
moduleDensity = -moduleDensity;
}
@@ -2597,8 +3285,8 @@ public class IrisCaveCarver3D {
scratch.warpCacheX[slot] = sx;
scratch.warpCacheY[slot] = sy;
scratch.warpCacheZ[slot] = sz;
scratch.warpCacheA[slot] = warpDensity.noiseFastSigned3D(sx, sy, sz);
scratch.warpCacheB[slot] = warpDensity.noiseFastSigned3D(sx + 31.37D, sy - 17.21D, sz + 23.91D);
scratch.warpCacheA[slot] = warpDensity.sampleSigned3D(sx, sy, sz);
scratch.warpCacheB[slot] = warpDensity.sampleSigned3D(sx + 31.37D, sy - 17.21D, sz + 23.91D);
return slot;
}
@@ -2714,7 +3402,7 @@ public class IrisCaveCarver3D {
private boolean isAquiferCandidate(CaveCarveScratch scratch, int x, int y, int z, double localThreshold) {
double depthFactor = Math.max(0D, Math.min(1.5D, (fluidHeight - y) / 48D));
double cutoff = 0.35D + (depthFactor * 0.2D);
if (detailDensity.noiseFastSigned3D(x, y * 0.5D, z) <= cutoff) {
if (detailDensity.sampleSigned3D(x, y * 0.5D, z) <= cutoff) {
return false;
}
return !fluidRequiresFloor || hasAquiferCupSupport(scratch, x, y, z, localThreshold);
@@ -2887,4 +3575,15 @@ public class IrisCaveCarver3D {
return verticalEdgeFade;
}
private record WarpFieldKey(CaveFieldKey field) {
}
private record ModuleFieldKey(CaveFieldKey field, int minY, int maxY, long weight, long threshold, boolean invert) {
}
private record DensityFieldKey(boolean warp, CaveFieldKey warpField, long warpStrength, CaveFieldKey base, long baseWeight,
CaveFieldKey detail, long detailWeight, long inverseNormalization,
List<ModuleFieldKey> modules) {
}
}
@@ -44,7 +44,6 @@ import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.math.RNG;
import java.io.File;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
@@ -598,7 +597,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
@Override
protected int computeRadius() {
int maxObjectExtent = 0;
Map<String, IrisBlockVector> sizeCache = new HashMap<>();
Map<String, PlacementReach.Extent> extents = new HashMap<>();
Set<String> warnedLargeObjects = new HashSet<>();
try {
IrisData data = getData();
@@ -608,15 +607,17 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
continue;
}
for (IrisFloatingChildBiomes entry : entries) {
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getFloatingObjects(), data, sizeCache, warnedLargeObjects));
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getExtraObjects(), data, sizeCache, warnedLargeObjects));
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getTopObjectOverrides(), data, sizeCache, warnedLargeObjects));
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.getBottomObjectOverrides(), data, sizeCache, warnedLargeObjects));
PlacementScan scan = new PlacementScan(data, extents, warnedLargeObjects,
entry.hasObjectShrink() ? entry.getShrinkScale().getMaxScale() : 1D);
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.getFloatingObjects(), false));
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.getExtraObjects(), true));
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.getTopObjectOverrides(), true));
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.getBottomObjectOverrides(), true));
try {
IrisBiome target = entry.getRealBiome(biome, data);
if (target != null) {
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.resolveTopObjects(target), data, sizeCache, warnedLargeObjects));
maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.resolveBottomObjects(target), data, sizeCache, warnedLargeObjects));
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.resolveTopObjects(target), true));
maxObjectExtent = Math.max(maxObjectExtent, scan.radius(entry.resolveBottomObjects(target), true));
} else {
maxObjectExtent = Math.max(maxObjectExtent, MINIMUM_PLACEMENT_RADIUS);
}
@@ -635,44 +636,66 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent {
return maxObjectExtent;
}
private int computePlacementRadius(KList<IrisObjectPlacement> placements, IrisData data,
Map<String, IrisBlockVector> sizeCache,
Set<String> warnedLargeObjects) {
if (placements == null) {
return MINIMUM_PLACEMENT_RADIUS;
private final class PlacementScan {
private final IrisData data;
private final Map<String, PlacementReach.Extent> extents;
private final Set<String> warnedLargeObjects;
private final double shrink;
private PlacementScan(IrisData data, Map<String, PlacementReach.Extent> extents, Set<String> warnedLargeObjects,
double shrink) {
this.data = data;
this.extents = extents;
this.warnedLargeObjects = warnedLargeObjects;
this.shrink = shrink;
}
if (placements.isEmpty()) {
return 0;
}
int radius = MINIMUM_PLACEMENT_RADIUS;
for (IrisObjectPlacement placement : placements) {
if (placement == null || placement.getPlace() == null) {
continue;
private int radius(KList<IrisObjectPlacement> placements, boolean anchored) {
if (placements == null) {
return MINIMUM_PLACEMENT_RADIUS;
}
for (String objectKey : placement.getPlace()) {
try {
IrisBlockVector size = sizeCache.get(objectKey);
if (size == null) {
File file = data.getObjectLoader().findFile(objectKey);
if (file == null) {
if (placements.isEmpty()) {
return 0;
}
int radius = MINIMUM_PLACEMENT_RADIUS;
for (IrisObjectPlacement placement : placements) {
if (placement == null || placement.getPlace() == null) {
continue;
}
PlacementReach.Profile profile = PlacementReach.profile(placement);
double scale = placement.getMaximumScale(getDimension());
for (String objectKey : placement.getPlace()) {
try {
PlacementReach.Extent extent = extent(objectKey);
if (extent == null) {
continue;
}
size = IrisObject.sampleSize(file);
sizeCache.put(objectKey, size);
PlacementReach.Extent scaled = extent.scaled(scale).scaled(shrink);
int reach = anchored ? profile.anchored(scaled).horizontal() : profile.of(scaled).horizontal();
if (reach > 128 && warnedLargeObjects.add(objectKey)) {
IrisLogging.warn("Floating object " + objectKey + " has a large placement reach (" + reach + " blocks) and may increase memory usage!");
}
radius = Math.max(radius, reach);
} catch (Throwable e) {
GenerationFailures.rethrowEngineFailure(e);
IrisLogging.reportError(e);
}
int reach = MantleObjectComponent.calculatePlacementReach(size, placement, placement.getMaximumScale(getDimension()));
if (reach > 128 && warnedLargeObjects.add(objectKey)) {
IrisLogging.warn("Floating object " + objectKey + " has a large placement reach (" + reach + " blocks) and may increase memory usage!");
}
radius = Math.max(radius, reach);
} catch (Throwable e) {
GenerationFailures.rethrowEngineFailure(e);
IrisLogging.reportError(e);
}
}
return radius;
}
private PlacementReach.Extent extent(String objectKey) {
if (extents.containsKey(objectKey)) {
return extents.get(objectKey);
}
IrisObject object = data.getObjectLoader().load(objectKey);
PlacementReach.Extent extent = object == null ? null : PlacementReach.Extent.of(object);
extents.put(objectKey, extent);
return extent;
}
return radius;
}
private static final class PlacementSequence {
private int ordinal;
@@ -54,7 +54,6 @@ import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
import art.arcane.iris.generation.subterrain.SubterrainPlan;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
@@ -69,12 +68,12 @@ import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.generation.block.DataProvider;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@@ -94,6 +93,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
private final ObjectSourcePlanCache sourcePlans;
private final AtomicLong lastSourcePlanStatsLog = new AtomicLong();
private volatile int collisionRuleState;
private volatile int inputReach;
public MantleObjectComponent(EngineMantle engineMantle) {
super(engineMantle, ReservedFlag.OBJECT, 2);
@@ -134,7 +134,8 @@ public class MantleObjectComponent extends IrisMantleComponent {
@Override
public int getInputRadius() {
return calculateInputRadius(getRadius(), hasCollisionRules());
int radius = getRadius();
return calculateInputRadius(radius, inputReach, hasCollisionRules());
}
@Override
@@ -297,11 +298,11 @@ public class MantleObjectComponent extends IrisMantleComponent {
return inputRadius >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) inputRadius;
}
static int calculateInputRadius(int radius, boolean sourceAnchoredCollisions) {
static int calculateInputRadius(int radius, int inputReach, boolean sourceAnchoredCollisions) {
int normalizedRadius = Math.max(0, radius);
long sourceChunkRadius = sourceChunkRadius(normalizedRadius);
long sourceLegs = sourceAnchoredCollisions ? 2L : 1L;
long inputRadius = (sourceChunkRadius * 16L * sourceLegs) + normalizedRadius + 1L;
long inputRadius = (sourceChunkRadius * 16L * sourceLegs) + Math.max(normalizedRadius, inputReach) + 1L;
return inputRadius >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) inputRadius;
}
@@ -1370,7 +1371,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
int id = rng.i(0, Integer.MAX_VALUE);
IrisObjectPlacement placement = objectPlacement.toPlacement(v.getLoadKey());
placement.setMode(ObjectPlaceMode.CENTER_HEIGHT);
placement.setRotation(buildUpsideDownRotation());
placement.setRotation(upsideDownRotation());
placement.setCarvingSupport(CarvingMode.ANYWHERE);
if (forcePlace) {
placement.setForcePlace(true);
@@ -1408,7 +1409,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
}
private IrisObjectRotation buildUpsideDownRotation() {
private static IrisObjectRotation upsideDownRotation() {
IrisObjectRotation rt = new IrisObjectRotation();
rt.setEnabled(true);
rt.setXAxis(new IrisAxisRotationClamp(true, true, 180D, 180D, 90D));
@@ -1785,135 +1786,13 @@ public class MantleObjectComponent extends IrisMantleComponent {
protected int computeRadius() {
KSet<String> warnedLargeObjects = new KSet<>();
int radius = computeDimensionRadius(getDimension(), this::getData, warnedLargeObjects);
PlacementReach reach = new ReachScan(getDimension(), this::getData, warnedLargeObjects).dimension();
UpperDimensionContext upperContext = activeUpperContext();
if (upperContext != null) {
radius = Math.max(radius, computeDimensionRadius(
upperContext.getDimension(),
upperContext,
warnedLargeObjects
));
reach = reach.union(new ReachScan(upperContext.getDimension(), upperContext, warnedLargeObjects).upperDimension());
}
return radius;
}
private int computeDimensionRadius(
IrisDimension dimension,
DataProvider dataProvider,
KSet<String> warnedLargeObjects
) {
KMap<String, IrisBlockVector> sizeCache = new KMap<>();
int radius = 0;
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
if (region == null) {
continue;
}
radius = Math.max(radius, computePlacementRadius(
region.getObjects(),
dimension,
dataProvider,
sizeCache,
warnedLargeObjects
));
radius = Math.max(radius, computeProceduralRadius(region.getProceduralObjects(), dimension, dataProvider));
}
for (IrisBiome biome : dimension.getReachableBiomes(dataProvider)) {
if (biome == null) {
continue;
}
radius = Math.max(radius, computePlacementRadius(
biome.getObjects(),
dimension,
dataProvider,
sizeCache,
warnedLargeObjects
));
radius = Math.max(radius, computeProceduralRadius(biome.getProceduralObjects(), dimension, dataProvider));
}
return radius;
}
private int computePlacementRadius(
KList<IrisObjectPlacement> placements,
IrisDimension dimension,
DataProvider dataProvider,
KMap<String, IrisBlockVector> sizeCache,
KSet<String> warnedLargeObjects
) {
int radius = 0;
for (IrisObjectPlacement placement : placements) {
if (placement == null) {
continue;
}
for (String objectKey : placement.getPlace()) {
try {
IrisBlockVector size = loadObjectSize(dataProvider, sizeCache, objectKey);
if (size == null) {
continue;
}
int reach = calculatePlacementReach(size, placement, placement.getMaximumScale(dimension));
if (reach > 128 && warnedLargeObjects.add(objectKey)) {
IrisLogging.warn("Object " + objectKey + " has a large placement reach (" + reach + " blocks) and may increase memory usage!");
}
radius = Math.max(radius, reach);
} catch (Throwable e) {
GenerationFailures.rethrowEngineFailure(e);
IrisLogging.reportError(e);
}
}
}
return radius;
}
private int computeProceduralRadius(IrisProceduralObjects procedural, IrisDimension dimension, DataProvider dataProvider) {
if (procedural == null || procedural.isEmpty()) {
return 0;
}
int radius = 0;
for (IrisProceduralPlacement placement : procedural.getAllPlacements()) {
if (placement == null) {
continue;
}
KList<IrisObject> variants = placement.getVariantObjects(dataProvider.getData());
if (variants == null) {
continue;
}
IrisObjectPlacement objectPlacement = placement.asPlacement();
for (IrisObject variant : variants) {
if (variant == null) {
continue;
}
IrisBlockVector size = new IrisBlockVector(variant.getW(), variant.getH(), variant.getD());
radius = Math.max(radius, calculatePlacementReach(size, objectPlacement, objectPlacement.getMaximumScale(dimension)));
}
}
return radius;
}
static int calculatePlacementReach(IrisBlockVector size, IrisObjectPlacement placement, double scale) {
if (size == null) {
return 0;
}
if (placement == null) {
return Math.max(Math.abs(size.getBlockX()), Math.abs(size.getBlockZ()));
}
int width = scaledDimension(size.getBlockX(), scale);
int height = scaledDimension(size.getBlockY(), scale);
int depth = scaledDimension(size.getBlockZ(), scale);
IrisObjectRotation rotation = placement.getRotation();
boolean rotateX = rotation != null && rotation.isEnabled() && rotation.getXAxis() != null && rotation.getXAxis().isEnabled();
boolean rotateZ = rotation != null && rotation.isEnabled() && rotation.getZAxis() != null && rotation.getZAxis().isEnabled();
int footprint = rotateX || rotateZ ? Math.max(width, Math.max(height, depth)) : Math.max(width, depth);
int translation = calculateTranslationReach(placement.getTranslate(), rotation, rotateX || rotateZ);
int warp = placement.getWarp() != null && !placement.getWarp().isFlat()
? (int) Math.ceil(Math.abs(placement.getWarp().getMultiplier()) / 2D)
: 0;
int vacuum = IrisObjectVacuum.isVacuumMode(placement.getMode())
? 2 * IrisObjectVacuum.resolveRadius(placement.getMode(), placement.getVacuumSettings())
: 0;
long reach = (long) footprint + translation + warp + vacuum;
return reach > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) reach;
inputReach = reach.input();
return reach.horizontal();
}
static boolean allowsObjectPlacement(
@@ -1926,9 +1805,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
if (complex == null || object == null) {
return true;
}
int reach = calculatePlacementReach(
new IrisBlockVector(object.getW(), object.getH(), object.getD()),
placement, 1D);
int reach = transitionFootprint(object, placement);
return complex.allowsNewGenerationFootprint(
saturatedOffset(anchorX, -reach),
saturatedOffset(anchorZ, -reach),
@@ -1936,20 +1813,34 @@ public class MantleObjectComponent extends IrisMantleComponent {
saturatedOffset(anchorZ, reach));
}
private static int transitionFootprint(IrisObject object, IrisObjectPlacement placement) {
int width = Math.abs(object.getW());
int height = Math.abs(object.getH());
int depth = Math.abs(object.getD());
if (placement == null) {
return Math.max(width, depth);
}
IrisObjectRotation rotation = placement.getRotation();
boolean rotateX = rotation != null && rotation.isEnabled() && rotation.getXAxis() != null && rotation.getXAxis().isEnabled();
boolean rotateZ = rotation != null && rotation.isEnabled() && rotation.getZAxis() != null && rotation.getZAxis().isEnabled();
int footprint = rotateX || rotateZ ? Math.max(width, Math.max(height, depth)) : Math.max(width, depth);
int translation = transitionTranslation(placement.getTranslate(), rotation, rotateX || rotateZ);
int warp = placement.getWarp() != null && !placement.getWarp().isFlat()
? (int) Math.ceil(Math.abs(placement.getWarp().getMultiplier()) / 2D)
: 0;
int vacuum = IrisObjectVacuum.isVacuumMode(placement.getMode())
? 2 * IrisObjectVacuum.resolveRadius(placement.getMode(), placement.getVacuumSettings())
: 0;
long reach = (long) footprint + translation + warp + vacuum;
return reach > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) reach;
}
private static int saturatedOffset(int coordinate, int offset) {
long result = (long) coordinate + offset;
return (int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, result));
}
private static int scaledDimension(int dimension, double scale) {
int absoluteDimension = Math.abs(dimension);
if (scale == 1D) {
return absoluteDimension;
}
return (int) Math.ceil(absoluteDimension * scale);
}
private static int calculateTranslationReach(IrisObjectTranslate translate, IrisObjectRotation rotation, boolean rotateVertically) {
private static int transitionTranslation(IrisObjectTranslate translate, IrisObjectRotation rotation, boolean rotateVertically) {
if (translate == null || !translate.canTranslate()) {
return 0;
}
@@ -1963,22 +1854,6 @@ public class MantleObjectComponent extends IrisMantleComponent {
return rotateY ? (int) Math.ceil(Math.hypot(x, z)) : (int) Math.max(Math.abs(x), Math.abs(z));
}
private IrisBlockVector loadObjectSize(
DataProvider dataProvider,
KMap<String, IrisBlockVector> sizeCache,
String objectKey
) {
return sizeCache.computeIfAbsent(objectKey, k -> {
try {
return IrisObject.sampleSize(dataProvider.getData().getObjectLoader().findFile(objectKey));
} catch (IOException e) {
IrisLogging.reportError(e);
}
return null;
});
}
private UpperDimensionContext activeUpperContext() {
if (!getDimension().isUpperDimensionObjects()) {
return null;
@@ -2047,6 +1922,187 @@ public class MantleObjectComponent extends IrisMantleComponent {
}
}
private static final class ReachScan {
private final IrisDimension dimension;
private final DataProvider dataProvider;
private final KSet<String> warnedLargeObjects;
private final Map<String, PlacementReach.Extent> extents;
private final Set<ObjectPlaceMode> caveVacuumModes;
private ReachScan(IrisDimension dimension, DataProvider dataProvider, KSet<String> warnedLargeObjects) {
this.dimension = dimension;
this.dataProvider = dataProvider;
this.warnedLargeObjects = warnedLargeObjects;
this.extents = new HashMap<>();
this.caveVacuumModes = caveVacuumModes(dimension, dataProvider);
}
private PlacementReach dimension() {
PlacementReach reach = PlacementReach.NONE;
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
if (region != null) {
reach = reach.union(placements(region.getObjects())).union(procedural(region.getProceduralObjects()));
}
}
for (IrisBiome biome : dimension.getReachableBiomes(dataProvider)) {
if (biome != null) {
reach = reach.union(placements(biome.getObjects())).union(procedural(biome.getProceduralObjects()));
}
}
return reach.withReadMargin(PlacementReach.supportReadMargin(dimension.getObjectSurfaceSupportBuffer()));
}
private PlacementReach upperDimension() {
PlacementReach reach = PlacementReach.NONE;
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
if (region != null) {
reach = reach.union(upperPlacements(region.getSurfaceObjects()));
}
}
for (IrisBiome biome : dimension.getReachableBiomes(dataProvider)) {
if (biome != null) {
reach = reach.union(upperPlacements(biome.getSurfaceObjects()));
}
}
return reach;
}
private PlacementReach placements(KList<IrisObjectPlacement> placements) {
PlacementReach reach = PlacementReach.NONE;
if (placements == null) {
return reach;
}
for (IrisObjectPlacement placement : placements) {
if (placement != null) {
reach = reach.union(objects(placement, profiles(placement)));
}
}
return reach;
}
private PlacementReach upperPlacements(KList<IrisObjectPlacement> placements) {
PlacementReach reach = PlacementReach.NONE;
if (placements == null) {
return reach;
}
for (IrisObjectPlacement placement : placements) {
if (placement == null) {
continue;
}
IrisObjectPlacement upper = placement.toPlacement(placement.getPlace().toArray(new String[0]));
upper.setMode(ObjectPlaceMode.CENTER_HEIGHT);
upper.setRotation(upsideDownRotation());
upper.setCarvingSupport(CarvingMode.ANYWHERE);
reach = reach.union(objects(placement, new KList<>(PlacementReach.profile(upper))));
}
return reach;
}
private PlacementReach objects(IrisObjectPlacement placement, KList<PlacementReach.Profile> profiles) {
PlacementReach reach = PlacementReach.NONE;
double scale = placement.getMaximumScale(dimension);
for (String objectKey : placement.getPlace()) {
try {
PlacementReach.Extent extent = extent(objectKey);
if (extent == null) {
continue;
}
PlacementReach.Extent scaled = extent.scaled(scale);
for (PlacementReach.Profile profile : profiles) {
PlacementReach objectReach = profile.of(scaled);
if (objectReach.horizontal() > 128 && warnedLargeObjects.add(objectKey)) {
IrisLogging.warn("Object " + objectKey + " has a large placement reach (" + objectReach.horizontal() + " blocks) and may increase memory usage!");
}
reach = reach.union(objectReach);
}
} catch (Throwable e) {
GenerationFailures.rethrowEngineFailure(e);
IrisLogging.reportError(e);
}
}
return reach;
}
private PlacementReach procedural(IrisProceduralObjects procedural) {
PlacementReach reach = PlacementReach.NONE;
if (procedural == null || procedural.isEmpty()) {
return reach;
}
for (IrisProceduralPlacement placement : procedural.getAllPlacements()) {
if (placement == null) {
continue;
}
KList<IrisObject> variants = placement.getVariantObjects(dataProvider.getData());
if (variants == null) {
continue;
}
IrisObjectPlacement objectPlacement = placement.asPlacement();
KList<PlacementReach.Profile> profiles = profiles(objectPlacement);
double scale = objectPlacement.getMaximumScale(dimension);
for (IrisObject variant : variants) {
if (variant == null) {
continue;
}
PlacementReach.Extent extent = PlacementReach.Extent.of(variant).scaled(scale);
for (PlacementReach.Profile profile : profiles) {
reach = reach.union(profile.of(extent));
}
}
}
return reach;
}
private KList<PlacementReach.Profile> profiles(IrisObjectPlacement placement) {
KList<PlacementReach.Profile> profiles = new KList<>(PlacementReach.profile(placement));
if (placement.getMode() != ObjectPlaceMode.CENTER_HEIGHT
|| placement.getCarvingSupport() == CarvingMode.SURFACE_ONLY) {
return profiles;
}
for (ObjectPlaceMode mode : caveVacuumModes) {
IrisObjectPlacement cave = placement.toPlacement(placement.getPlace().toArray(new String[0]));
cave.setMode(mode);
profiles.add(PlacementReach.profile(cave));
}
return profiles;
}
private PlacementReach.Extent extent(String objectKey) {
if (extents.containsKey(objectKey)) {
return extents.get(objectKey);
}
IrisObject object = dataProvider.getData().getObjectLoader().load(objectKey);
PlacementReach.Extent extent = object == null ? null : PlacementReach.Extent.of(object);
extents.put(objectKey, extent);
return extent;
}
private static Set<ObjectPlaceMode> caveVacuumModes(IrisDimension dimension, DataProvider dataProvider) {
Set<ObjectPlaceMode> modes = EnumSet.noneOf(ObjectPlaceMode.class);
addCaveVacuumMode(modes, dimension.getCaveProfile());
for (IrisRegion region : dimension.getAllRegions(dataProvider)) {
if (region != null) {
addCaveVacuumMode(modes, region.getCaveProfile());
}
}
for (IrisBiome biome : dimension.getReachableBiomes(dataProvider)) {
if (biome != null) {
addCaveVacuumMode(modes, biome.getCaveProfile());
}
}
return modes;
}
private static void addCaveVacuumMode(Set<ObjectPlaceMode> modes, IrisCaveProfile profile) {
if (profile == null) {
return;
}
ObjectPlaceMode mode = profile.getDefaultObjectPlaceMode();
if (mode != null && IrisObjectVacuum.isVacuumMode(mode)) {
modes.add(mode);
}
}
}
@FunctionalInterface
interface SourceChunkConsumer {
void accept(int chunkX, int chunkZ);
@@ -23,13 +23,14 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public interface MatterGenerator {
MultiBurst DISPATCHER = MultiBurst.burst;
ConcurrentHashMap<MatterTaskKey, MatterComponentTask> IN_FLIGHT_COMPONENTS = new ConcurrentHashMap<>();
int COMPONENT_SUBMISSION_BATCH_SIZE = Math.max(1, Math.min(8, Runtime.getRuntime().availableProcessors()));
int COMPONENT_SUBMISSION_LIMIT = Math.max(1, Math.min(8, Runtime.getRuntime().availableProcessors()));
long COMPONENT_TASK_POLL_MS = 1000L;
long COMPONENT_TASK_TIMEOUT_MS = Long.getLong("iris.mantle.componentTimeout", 120000L);
@@ -87,7 +88,7 @@ public interface MatterGenerator {
if (passPlans.length == 0) {
return;
}
int prefetchRadius = passPlans[0].passChunkRadius();
int prefetchRadius = phasePrefetchRadius(passPlans, phase);
LongOpenHashSet partialChunks = new LongOpenHashSet();
List<MantleComponent> requiredComponents = enabledComponents();
boolean terrainOnAccess = false;
@@ -104,8 +105,9 @@ public interface MatterGenerator {
// close() releases the cached chunks, even when a pass throws, or detached pool threads
// write into released chunks.
List<MatterComponentTask> outstandingTasks = null;
List<MatterComponentTask> submittedTasks = multicore && !DISPATCHER.ownsCurrentThread()
? new ArrayList<>(COMPONENT_SUBMISSION_BATCH_SIZE)
List<DeferredComponent> deferredComponents = null;
SubmissionWindow submissions = multicore && !DISPATCHER.ownsCurrentThread()
? new SubmissionWindow()
: null;
try {
@@ -229,15 +231,17 @@ public interface MatterGenerator {
for (int componentIndex = 0; componentIndex < eligibleComponentCount; componentIndex++) {
MantleComponent component = eligibleComponents[componentIndex];
outstandingTasks.add(runComponentAsync(
chunk, component, writer, finalPassX, finalPassZ, context, submittedTasks));
if (submittedTasks != null && submittedTasks.size() >= COMPONENT_SUBMISSION_BATCH_SIZE) {
awaitComponentTasks(submittedTasks);
}
chunk, component, writer, finalPassX, finalPassZ, context, submissions));
}
} else {
for (int componentIndex = 0; componentIndex < eligibleComponentCount; componentIndex++) {
MantleComponent component = eligibleComponents[componentIndex];
runComponentInline(chunk, component, writer, finalPassX, finalPassZ, context);
if (!tryRunComponentInline(chunk, component, writer, finalPassX, finalPassZ, context)) {
if (deferredComponents == null) {
deferredComponents = new ArrayList<>();
}
deferredComponents.add(new DeferredComponent(chunk, component, finalPassX, finalPassZ));
}
}
}
}
@@ -245,9 +249,8 @@ public interface MatterGenerator {
if (asyncComponents) {
awaitComponentTasks(outstandingTasks);
if (submittedTasks != null) {
submittedTasks.clear();
}
} else if (deferredComponents != null) {
runDeferredComponents(deferredComponents, writer, context);
}
}
@@ -320,6 +323,25 @@ public interface MatterGenerator {
return new MatterGenerationPlan(plans, writerChunkRadius * 2, phase);
}
private int phasePrefetchRadius(MatterPassPlan[] passPlans, MatterGenerationPhase phase) {
int radius = 0;
for (MatterPassPlan passPlan : passPlans) {
if (passPlan.passChunkRadius() > radius && generatesInPhase(passPlan.pass(), phase)) {
radius = passPlan.passChunkRadius();
}
}
return radius;
}
private boolean generatesInPhase(MantlePass pass, MatterGenerationPhase phase) {
for (MantleComponent component : pass.components()) {
if (phase.includes(component) && shouldGenerateComponent(component)) {
return true;
}
}
return false;
}
private List<MantleComponent> enabledComponents() {
List<MantleComponent> components = new ArrayList<>();
for (MantlePass pass : getComponents()) {
@@ -376,7 +398,7 @@ public interface MatterGenerator {
int chunkX,
int chunkZ,
ChunkContext context,
List<MatterComponentTask> submittedTasks
SubmissionWindow submissions
) {
MantleFlag flag = component.getFlag();
MatterTaskKey key = new MatterTaskKey(getMantle(), chunkX, chunkZ, flag);
@@ -386,19 +408,35 @@ public interface MatterGenerator {
return task;
}
MatterComponentTask existing = IN_FLIGHT_COMPONENTS.putIfAbsent(key, task);
if (existing != null) {
return existing;
}
try {
if (DISPATCHER.ownsCurrentThread()) {
if (submissions == null) {
MatterComponentTask existing = IN_FLIGHT_COMPONENTS.putIfAbsent(key, task);
if (existing != null) {
return existing;
}
try {
// A pool thread runs its claim inline: a task queued behind pool threads that block
// on unmanaged waits (structure builds, cache locks) could starve the whole pool.
completeComponentTask(task, chunk, component, writer, chunkX, chunkZ, context, IrisContext.get());
return task;
} catch (Throwable throwable) {
task.cancel(throwable);
throw throwable;
}
}
MatterComponentTask running = IN_FLIGHT_COMPONENTS.get(key);
if (running != null) {
return running;
}
submissions.acquire();
MatterComponentTask existing = IN_FLIGHT_COMPONENTS.putIfAbsent(key, task);
if (existing != null) {
submissions.release();
return existing;
}
try {
IrisContext callerContext = IrisContext.get();
submissions.track(task);
task.setSubmission(DISPATCHER.submit(() -> completeComponentTask(
task,
chunk,
@@ -408,7 +446,6 @@ public interface MatterGenerator {
chunkZ,
context,
callerContext)));
submittedTasks.add(task);
return task;
} catch (Throwable throwable) {
task.cancel(throwable);
@@ -416,6 +453,42 @@ public interface MatterGenerator {
}
}
private boolean tryRunComponentInline(
MantleChunk<Matter> chunk,
MantleComponent component,
MantleWriter writer,
int chunkX,
int chunkZ,
ChunkContext context
) {
MantleFlag flag = component.getFlag();
if (!chunk.tryClaim(flag)) {
return chunk.isFlagged(flag);
}
boolean generated = false;
try {
writer.withComponentPriority(component.getPriority(),
() -> component.generateLayer(writer, chunkX, chunkZ, context));
generated = true;
} finally {
if (generated) {
chunk.completeClaim(flag);
} else {
chunk.abandonClaim(flag);
}
}
return true;
}
private void runDeferredComponents(List<DeferredComponent> deferredComponents, MantleWriter writer,
ChunkContext context) {
for (DeferredComponent deferred : deferredComponents) {
runComponentInline(deferred.chunk(), deferred.component(), writer, deferred.chunkX(), deferred.chunkZ(),
context);
}
deferredComponents.clear();
}
/**
* Pass barrier. Waits for every launched task, even if one of them failed, so that no task is
* still writing through the writer once this returns. The first failure is rethrown.
@@ -698,6 +771,36 @@ public interface MatterGenerator {
}
}
final class SubmissionWindow {
private final Semaphore slots = new Semaphore(COMPONENT_SUBMISSION_LIMIT);
private volatile Throwable failure;
void acquire() {
slots.acquireUninterruptibly();
Throwable observed = failure;
if (observed != null) {
slots.release();
throw new CompletionException(observed);
}
}
void release() {
slots.release();
}
void track(MatterComponentTask task) {
task.future.whenComplete((ignored, throwable) -> {
if (throwable != null && failure == null) {
failure = throwable;
}
slots.release();
});
}
}
record DeferredComponent(MantleChunk<Matter> chunk, MantleComponent component, int chunkX, int chunkZ) {
}
record MatterPassPlan(MantlePass pass, int passChunkRadius, int downstreamBlockRadius) {
}
@@ -0,0 +1,312 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.block.VectorMap;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.structure.object.IrisObjectPlacement;
import art.arcane.iris.structure.object.IrisObjectRotation;
import art.arcane.iris.structure.object.IrisObjectTranslate;
import art.arcane.iris.structure.object.IrisObjectVacuum;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.math.Vector3i;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import java.util.Objects;
public record PlacementReach(int horizontal, int readMargin) {
public static final PlacementReach NONE = new PlacementReach(0, 0);
private static final int SPIN_PERIOD = 360;
private static final int MAXIMUM_ROTATION_KERNELS = 1024;
private static final double ROUNDING_SLACK = 1.0E-9D;
public PlacementReach {
if (horizontal < 0 || readMargin < 0) {
throw new IllegalArgumentException("Placement reach must be non-negative: " + horizontal + "+" + readMargin);
}
}
public static PlacementReach of(IrisObject object, IrisObjectPlacement placement, double scale) {
return of(Extent.of(object).scaled(scale), placement);
}
public static PlacementReach of(Extent extent, IrisObjectPlacement placement) {
return profile(placement).of(extent);
}
public static PlacementReach anchored(Extent extent, IrisObjectPlacement placement) {
return profile(placement).anchored(extent);
}
public static Profile profile(IrisObjectPlacement placement) {
return new Profile(Objects.requireNonNull(placement, "placement"));
}
public static int supportReadMargin(int surfaceSupportBuffer) {
return 1 + Math.max(0, surfaceSupportBuffer);
}
public int input() {
return saturate((long) horizontal + readMargin);
}
public PlacementReach union(PlacementReach other) {
int unionHorizontal = Math.max(horizontal, other.horizontal);
int unionInput = Math.max(input(), other.input());
return new PlacementReach(unionHorizontal, unionInput - unionHorizontal);
}
public PlacementReach withReadMargin(int minimumReadMargin) {
return minimumReadMargin <= readMargin ? this : new PlacementReach(horizontal, minimumReadMargin);
}
private static int roundedBound(double maximumMagnitude) {
return saturate((long) Math.floor(maximumMagnitude + 0.5D + ROUNDING_SLACK));
}
private static int saturate(long value) {
return value >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) Math.max(0L, value);
}
private static IrisBlockVector corner(Extent extent, int corner) {
return new IrisBlockVector(
(corner & 1) == 0 ? extent.x().minimum() : extent.x().maximum(),
(corner & 2) == 0 ? extent.y().minimum() : extent.y().maximum(),
(corner & 4) == 0 ? extent.z().minimum() : extent.z().maximum());
}
public static final class Profile {
private final IrisObjectRotation rotation;
private final IrisBlockVector translation;
private final boolean rotating;
private final boolean radial;
private final boolean vertical;
private final int[] spinsX;
private final int[] spinsY;
private final int[] spinsZ;
private final int additive;
private final int readMargin;
private Profile(IrisObjectPlacement placement) {
IrisObjectRotation configuredRotation = placement.getRotation();
IrisObjectTranslate translate = placement.getTranslate();
this.rotation = configuredRotation;
this.translation = translate != null && translate.canTranslate()
? new IrisBlockVector(translate.getX(), translate.getY(), translate.getZ())
: null;
this.rotating = configuredRotation != null && configuredRotation.canRotate();
this.spinsX = rotating ? distinctSpins(configuredRotation, Axis.X) : new int[]{0};
this.spinsY = rotating ? distinctSpins(configuredRotation, Axis.Y) : new int[]{0};
this.spinsZ = rotating ? distinctSpins(configuredRotation, Axis.Z) : new int[]{0};
long kernels = (long) spinsX.length * spinsY.length * spinsZ.length;
this.radial = placement.isRotateTowardsSlope() || (rotating && kernels > MAXIMUM_ROTATION_KERNELS);
this.vertical = placement.isRotateTowardsSlope()
|| (rotating && (configuredRotation.canRotateX() || configuredRotation.canRotateZ()));
this.additive = saturate((long) warpReach(placement.getWarp()) + vacuumReach(placement));
this.readMargin = supportReadMargin(placement.getSurfaceSupportBuffer());
}
public PlacementReach of(Extent extent) {
Objects.requireNonNull(extent, "extent");
return new PlacementReach(saturate((long) rotatedReach(extent) + additive), readMargin);
}
public PlacementReach anchored(Extent extent) {
Objects.requireNonNull(extent, "extent");
long span = Math.max(extent.x().span(), extent.z().span());
return new PlacementReach(saturate(span + additive), readMargin);
}
private int rotatedReach(Extent extent) {
if (radial) {
return radialReach(extent);
}
if (!rotating) {
return unrotatedReach(extent);
}
double maximum = 0D;
for (int spinX : spinsX) {
for (int spinY : spinsY) {
for (int spinZ : spinsZ) {
maximum = Math.max(maximum, rotatedCornerMaximum(extent, spinX, spinY, spinZ));
}
}
}
return roundedBound(maximum);
}
private double rotatedCornerMaximum(Extent extent, int spinX, int spinY, int spinZ) {
IrisBlockVector offset = translation == null ? null : rotation.rotate(translation, spinX, spinY, spinZ);
double maximum = 0D;
for (int corner = 0; corner < 8; corner++) {
IrisBlockVector rotated = rotation.rotate(corner(extent, corner), spinX, spinY, spinZ);
double x = offset == null ? rotated.getX() : rotated.getX() + offset.getX();
double z = offset == null ? rotated.getZ() : rotated.getZ() + offset.getZ();
maximum = Math.max(maximum, Math.max(Math.abs(x), Math.abs(z)));
}
return maximum;
}
private int unrotatedReach(Extent extent) {
double translateX = translation == null ? 0D : translation.getX();
double translateZ = translation == null ? 0D : translation.getZ();
double x = Math.max(Math.abs(extent.x().minimum() + translateX), Math.abs(extent.x().maximum() + translateX));
double z = Math.max(Math.abs(extent.z().minimum() + translateZ), Math.abs(extent.z().maximum() + translateZ));
return roundedBound(Math.max(x, z));
}
private int radialReach(Extent extent) {
double translateX = translation == null ? 0D : translation.getX();
double translateY = translation == null ? 0D : translation.getY();
double translateZ = translation == null ? 0D : translation.getZ();
double maximum = 0D;
for (int corner = 0; corner < 8; corner++) {
IrisBlockVector position = corner(extent, corner);
double x = position.getX() + translateX;
double y = vertical ? position.getY() + translateY : 0D;
double z = position.getZ() + translateZ;
maximum = Math.max(maximum, Math.sqrt((x * x) + (y * y) + (z * z)));
}
return roundedBound(maximum);
}
private static int warpReach(IrisGeneratorStyle warp) {
if (warp == null || warp.isFlat()) {
return 0;
}
return saturate((long) Math.ceil(Math.abs(warp.getMultiplier()) / 2D));
}
private static int vacuumReach(IrisObjectPlacement placement) {
if (!IrisObjectVacuum.isVacuumMode(placement.getMode())) {
return 0;
}
return IrisObjectVacuum.resolveRadius(placement.getMode(), placement.getVacuumSettings());
}
private static int[] distinctSpins(IrisObjectRotation rotation, Axis axis) {
boolean rotates = switch (axis) {
case X -> rotation.canRotateX();
case Y -> rotation.canRotateY();
case Z -> rotation.canRotateZ();
};
if (!rotates) {
return new int[]{0};
}
LongOpenHashSet angles = new LongOpenHashSet();
IntArrayList spins = new IntArrayList();
for (int spin = 0; spin < SPIN_PERIOD; spin++) {
double angle = switch (axis) {
case X -> rotation.getXRotation(spin);
case Y -> rotation.getYRotation(spin);
case Z -> rotation.getZRotation(spin);
};
if (angles.add(Double.doubleToLongBits(angle))) {
spins.add(spin);
}
}
return spins.toIntArray();
}
}
private enum Axis {
X,
Y,
Z
}
public record Extent(Span x, Span y, Span z) {
public Extent {
Objects.requireNonNull(x, "x");
Objects.requireNonNull(y, "y");
Objects.requireNonNull(z, "z");
}
public static Extent of(IrisObject object) {
Objects.requireNonNull(object, "object");
Vector3i center = object.getCenter();
int[] keys = keyBounds(object.getBlocks());
return new Extent(
Span.of(object.getW(), center.getBlockX(), keys[0], keys[3]),
Span.of(object.getH(), center.getBlockY(), keys[1], keys[4]),
Span.of(object.getD(), center.getBlockZ(), keys[2], keys[5]));
}
public Extent scaled(double scale) {
if (scale == 1D) {
return this;
}
return new Extent(x.scaled(scale), y.scaled(scale), z.scaled(scale));
}
private static int[] keyBounds(VectorMap<NativeBlockState> blocks) {
int[] bounds = {Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE,
Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE};
VectorMap<NativeBlockState>.Cursor cursor = blocks.cursor();
while (cursor.next()) {
IrisBlockVector key = cursor.key();
bounds[0] = Math.min(bounds[0], key.getBlockX());
bounds[1] = Math.min(bounds[1], key.getBlockY());
bounds[2] = Math.min(bounds[2], key.getBlockZ());
bounds[3] = Math.max(bounds[3], key.getBlockX());
bounds[4] = Math.max(bounds[4], key.getBlockY());
bounds[5] = Math.max(bounds[5], key.getBlockZ());
}
return bounds;
}
}
public record Span(int size, int center, int minimum, int maximum, boolean framed) {
public Span {
if (minimum > -center || maximum < size - 1 - center) {
throw new IllegalArgumentException("Span must cover the object frame: " + minimum + ".." + maximum
+ " for size " + size + " centered at " + center);
}
}
public static Span of(int size, int center, int keyMinimum, int keyMaximum) {
int frameMinimum = -center;
int frameMaximum = size - 1 - center;
boolean framed = keyMinimum >= frameMinimum && keyMaximum <= frameMaximum;
return new Span(size, center, Math.min(keyMinimum, frameMinimum), Math.max(keyMaximum, frameMaximum), framed);
}
public int span() {
return maximum - minimum;
}
public Span scaled(double scale) {
int scaledSize = (int) Math.ceil(size * scale);
int scaledCenter = scaledSize / 2;
int frameMinimum = -scaledCenter;
int frameMaximum = scaledSize - 1 - scaledCenter;
if (framed) {
return new Span(scaledSize, scaledCenter, frameMinimum, frameMaximum, true);
}
int scaledMinimum = (int) Math.floor((minimum + center) * scale) - scaledCenter;
double centerExcess = Math.max(0D, center - (size / 2D));
int scaledMaximum = (int) Math.ceil(((maximum + 1 + centerExcess) * scale) + 0.5D + ROUNDING_SLACK) - 1;
return new Span(scaledSize, scaledCenter, Math.min(scaledMinimum, frameMinimum),
Math.max(scaledMaximum, frameMaximum), false);
}
}
}
@@ -50,4 +50,18 @@ final class ChunkBlockUpdateListener implements ColumnExtentListeningHunk.Listen
}
quiet = state;
}
@Override
public void onRun(int x, int z, int yFromInclusive, int yToInclusive, NativeBlockState state) {
if (state == null || state == quiet) {
return;
}
if (B.isUpdatable(state) || state.isCustom()) {
for (int y = yToInclusive; y >= yFromInclusive; y--) {
updater.catchBlockUpdates(originX + x, y, originZ + z, state);
}
return;
}
quiet = state;
}
}
@@ -24,63 +24,65 @@ import lombok.Data;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.DoubleAdder;
import java.util.concurrent.atomic.LongAdder;
@Data
public class EngineMetrics {
private final AtomicRollingSequence total;
private final AtomicRollingSequence updates;
private final AtomicRollingSequence terrain;
private final AtomicRollingSequence biome;
private final AtomicRollingSequence parallax;
private final AtomicRollingSequence parallaxInsert;
private final AtomicRollingSequence post;
private final AtomicRollingSequence perfection;
private final AtomicRollingSequence api;
private final AtomicRollingSequence decoration;
private final AtomicRollingSequence cave;
private final AtomicRollingSequence ravine;
private final AtomicRollingSequence deposit;
private final AtomicRollingSequence carveResolve;
private final AtomicRollingSequence carveApply;
private final AtomicRollingSequence noiseHeightInterpolate;
private final AtomicRollingSequence noiseHeightGenerator;
private final AtomicRollingSequence contextPrefill;
private final AtomicRollingSequence contextPrefillHeight;
private final AtomicRollingSequence contextPrefillBiome;
private final AtomicRollingSequence contextPrefillRock;
private final AtomicRollingSequence contextPrefillFluid;
private final AtomicRollingSequence contextPrefillRegion;
private final AtomicRollingSequence contextPrefillCave;
private final AtomicRollingSequence pregenWaitPermit;
private final AtomicRollingSequence pregenWaitAdaptive;
private final StageTimer total;
private final StageTimer updates;
private final StageTimer terrain;
private final StageTimer biome;
private final StageTimer parallax;
private final StageTimer parallaxInsert;
private final StageTimer post;
private final StageTimer perfection;
private final StageTimer api;
private final StageTimer decoration;
private final StageTimer cave;
private final StageTimer ravine;
private final StageTimer deposit;
private final StageTimer carveResolve;
private final StageTimer carveApply;
private final StageTimer noiseHeightInterpolate;
private final StageTimer noiseHeightGenerator;
private final StageTimer contextPrefill;
private final StageTimer contextPrefillHeight;
private final StageTimer contextPrefillBiome;
private final StageTimer contextPrefillRock;
private final StageTimer contextPrefillFluid;
private final StageTimer contextPrefillRegion;
private final StageTimer contextPrefillCave;
private final StageTimer pregenWaitPermit;
private final StageTimer pregenWaitAdaptive;
public EngineMetrics(int mem) {
this.total = new AtomicRollingSequence(mem);
this.terrain = new AtomicRollingSequence(mem);
this.api = new AtomicRollingSequence(mem);
this.biome = new AtomicRollingSequence(mem);
this.perfection = new AtomicRollingSequence(mem);
this.parallax = new AtomicRollingSequence(mem);
this.parallaxInsert = new AtomicRollingSequence(mem);
this.post = new AtomicRollingSequence(mem);
this.decoration = new AtomicRollingSequence(mem);
this.updates = new AtomicRollingSequence(mem);
this.cave = new AtomicRollingSequence(mem);
this.ravine = new AtomicRollingSequence(mem);
this.deposit = new AtomicRollingSequence(mem);
this.carveResolve = new AtomicRollingSequence(mem);
this.carveApply = new AtomicRollingSequence(mem);
this.noiseHeightInterpolate = new AtomicRollingSequence(mem);
this.noiseHeightGenerator = new AtomicRollingSequence(mem);
this.contextPrefill = new AtomicRollingSequence(mem);
this.contextPrefillHeight = new AtomicRollingSequence(mem);
this.contextPrefillBiome = new AtomicRollingSequence(mem);
this.contextPrefillRock = new AtomicRollingSequence(mem);
this.contextPrefillFluid = new AtomicRollingSequence(mem);
this.contextPrefillRegion = new AtomicRollingSequence(mem);
this.contextPrefillCave = new AtomicRollingSequence(mem);
this.pregenWaitPermit = new AtomicRollingSequence(mem);
this.pregenWaitAdaptive = new AtomicRollingSequence(mem);
this.total = new StageTimer(mem);
this.terrain = new StageTimer(mem);
this.api = new StageTimer(mem);
this.biome = new StageTimer(mem);
this.perfection = new StageTimer(mem);
this.parallax = new StageTimer(mem);
this.parallaxInsert = new StageTimer(mem);
this.post = new StageTimer(mem);
this.decoration = new StageTimer(mem);
this.updates = new StageTimer(mem);
this.cave = new StageTimer(mem);
this.ravine = new StageTimer(mem);
this.deposit = new StageTimer(mem);
this.carveResolve = new StageTimer(mem);
this.carveApply = new StageTimer(mem);
this.noiseHeightInterpolate = new StageTimer(mem);
this.noiseHeightGenerator = new StageTimer(mem);
this.contextPrefill = new StageTimer(mem);
this.contextPrefillHeight = new StageTimer(mem);
this.contextPrefillBiome = new StageTimer(mem);
this.contextPrefillRock = new StageTimer(mem);
this.contextPrefillFluid = new StageTimer(mem);
this.contextPrefillRegion = new StageTimer(mem);
this.contextPrefillCave = new StageTimer(mem);
this.pregenWaitPermit = new StageTimer(mem);
this.pregenWaitAdaptive = new StageTimer(mem);
}
public KMap<String, Double> pull() {
@@ -134,4 +136,61 @@ public class EngineMetrics {
averages.entrySet().removeIf(entry -> !Double.isFinite(entry.getValue()) || entry.getValue() < 0D);
return Map.copyOf(averages);
}
public Map<String, StageTotal> stageTotals() {
Map<String, StageTotal> totals = new LinkedHashMap<>();
totals.put("total", total.snapshot());
totals.put("terrain", terrain.snapshot());
totals.put("biome", biome.snapshot());
totals.put("parallax", parallax.snapshot());
totals.put("parallax.insert", parallaxInsert.snapshot());
totals.put("post", post.snapshot());
totals.put("perfection", perfection.snapshot());
totals.put("decoration", decoration.snapshot());
totals.put("api", api.snapshot());
totals.put("updates", updates.snapshot());
totals.put("cave", cave.snapshot());
totals.put("ravine", ravine.snapshot());
totals.put("deposit", deposit.snapshot());
totals.put("carve.resolve", carveResolve.snapshot());
totals.put("carve.apply", carveApply.snapshot());
totals.put("noise.height.interpolate", noiseHeightInterpolate.snapshot());
totals.put("noise.height.generator", noiseHeightGenerator.snapshot());
totals.put("context.prefill", contextPrefill.snapshot());
totals.put("context.prefill.height", contextPrefillHeight.snapshot());
totals.put("context.prefill.biome", contextPrefillBiome.snapshot());
totals.put("context.prefill.rock", contextPrefillRock.snapshot());
totals.put("context.prefill.fluid", contextPrefillFluid.snapshot());
totals.put("context.prefill.region", contextPrefillRegion.snapshot());
totals.put("context.prefill.cave", contextPrefillCave.snapshot());
totals.put("pregen.wait.permit", pregenWaitPermit.snapshot());
totals.put("pregen.wait.adaptive", pregenWaitAdaptive.snapshot());
return totals;
}
public static final class StageTimer extends AtomicRollingSequence {
private final DoubleAdder totalMillis = new DoubleAdder();
private final LongAdder samples = new LongAdder();
public StageTimer(int size) {
super(size);
}
@Override
public void put(double millis) {
super.put(millis);
totalMillis.add(millis);
samples.increment();
}
public StageTotal snapshot() {
return new StageTotal(totalMillis.sum(), samples.sum());
}
}
public record StageTotal(double millis, long samples) {
public StageTotal since(StageTotal earlier) {
return new StageTotal(millis - earlier.millis(), samples - earlier.samples());
}
}
}
@@ -0,0 +1,37 @@
package art.arcane.iris.generation.runtime;
import java.util.concurrent.atomic.LongAdder;
public enum GenerationCounter {
GENERATOR_EVALUATIONS("generator_evaluations"),
CORNER_STARCASTS("corner_starcasts"),
HYDROLOGY_LOOKUPS("hydrology_lookups"),
CAVE_LATTICE_READS("cave_lattice_reads"),
CAVE_LATTICE_SAMPLES("cave_lattice_samples"),
CAVE_WARP_SAMPLES("cave_warp_samples");
public static final boolean ENABLED = Boolean.getBoolean("iris.generation.counters");
private final String key;
private final LongAdder count = new LongAdder();
GenerationCounter(String key) {
this.key = key;
}
public String key() {
return key;
}
public void add(long amount) {
count.add(amount);
}
public void increment() {
count.increment();
}
public long sum() {
return count.sum();
}
}
@@ -2212,6 +2212,9 @@ public class IrisComplex implements DataProvider {
long sharedKey = SharedCornerBounds.key(gx, gz);
long packed = shared.get(sharedKey, interpolatorIndex);
if (packed == Long.MIN_VALUE) {
if (GenerationCounter.ENABLED) {
GenerationCounter.CORNER_STARCASTS.increment();
}
packed = computePackedBounds(cache, engine, interpolator, generators, gx, gz);
if (!Double.isFinite(boundsLow(packed)) || !Double.isFinite(boundsHigh(packed))) {
// A non-finite corner is a transient sampling fault, not a fact about the terrain:
@@ -2576,6 +2579,9 @@ public class IrisComplex implements DataProvider {
return height / generators.length;
}
GeneratorEvaluations.scalar(generators.length);
if (GenerationCounter.ENABLED) {
GenerationCounter.GENERATOR_EVALUATIONS.add(generators.length);
}
for (IrisGenerator generator : generators) {
double noise = generator.getHeight(x, z, seed);
if (!Double.isFinite(noise)) {
@@ -177,7 +177,7 @@ public class IrisEngine implements Engine {
volatile GenerationHistoryRuntimeRouter generationHistoryRuntimeRouter;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private boolean generationHistoryRoutingRequired;
private volatile boolean generationHistoryRoutingRequired;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
volatile EngineTarget publishedTarget;
@@ -1011,13 +1011,7 @@ public class IrisEngine implements Engine {
}
public Optional<GenerationHistoryRuntimeRouter> getGenerationHistoryRuntimeRouter() {
GenerationHistoryRuntimeRouter router = generationHistoryRuntimeRouter;
if (router != null) {
return Optional.of(router);
}
synchronized (generationHistoryRuntimeRouterLock) {
return Optional.ofNullable(generationHistoryRuntimeRouter);
}
return Optional.ofNullable(generationHistoryRuntimeRouter);
}
public GenerationHistoryRuntimeRouter.CoordinateScope openGenerationHistoryCoordinateScope(
@@ -1028,13 +1022,14 @@ public class IrisEngine implements Engine {
if (router != null) {
return router.openCoordinateScope(blockX, blockZ);
}
synchronized (generationHistoryRuntimeRouterLock) {
router = generationHistoryRuntimeRouter;
if (router == null && generationHistoryRoutingRequired) {
throw new IllegalStateException("Iris generation-history runtime router is detached.");
}
if (!generationHistoryRoutingRequired) {
return null;
}
return router == null ? null : router.openCoordinateScope(blockX, blockZ);
router = generationHistoryRuntimeRouter;
if (router == null) {
throw new IllegalStateException("Iris generation-history runtime router is detached.");
}
return router.openCoordinateScope(blockX, blockZ);
}
public boolean hasGenerationRuntimeScope() {
@@ -79,15 +79,20 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
private static final int CAVE_BIOME_BLEND_CENTER_WEIGHT = 4;
private static final int CAVE_BIOME_BLEND_TOTAL_WEIGHT = 8;
private static final int SUBMERGED_FLOOR_SUBSTRATE_DEPTH = 8;
private static final long RANDOM_MULTIPLIER = 0x5DEECE66DL;
private static final long RANDOM_ADDEND = 0xBL;
private static final long RANDOM_MASK = (1L << 48) - 1;
private static final MatterCavern BASIC_CAVERN = new MatterCavern(true, "", (byte) 0);
private final RNG rng;
private final long caveBlendSeed;
private final NativeBlockState AIR = B.getState("CAVE_AIR");
private final NativeBlockState LAVA = B.getState("LAVA");
private final IrisDecorantActuator decorant;
public IrisCarveModifier(Engine engine) {
super(engine, "Carve");
rng = new RNG(getEngine().getSeedManager().getCarve());
caveBlendSeed = getEngine().getSeedManager().getCarve();
rng = new RNG(caveBlendSeed);
decorant = new IrisDecorantActuator(engine);
}
@@ -97,9 +102,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
PrecisionStopwatch caveStopwatch = PrecisionStopwatch.start();
Mantle<Matter> mantle = getEngine().getMantle().getMantle();
CaveInputs resolverState = new CaveInputs(getEngine());
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache = new Long2ObjectOpenHashMap<>(2048);
IrisCarveScratch scratch = SCRATCH.get();
scratch.reset();
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache = scratch.caveBiomeCache;
CarveWallBuffer walls = scratch.walls;
CarveColumnMask[] columnMasks = scratch.columnMasks;
CarveColumnMask[] boundaryMasks = scratch.boundaryMasks;
@@ -1231,7 +1236,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
return null;
}
int roll = Math.floorMod(rng.nextParallelRNG(BlockPosition.toLong(x, y, z)).nextInt(), CAVE_BIOME_BLEND_TOTAL_WEIGHT);
int roll = Math.floorMod(caveBlendRandom(caveBlendSeed, x, y, z), CAVE_BIOME_BLEND_TOTAL_WEIGHT);
if (roll < CAVE_BIOME_BLEND_CENTER_WEIGHT) {
return center;
}
@@ -1245,6 +1250,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
return neighbor != null ? neighbor : center;
}
static int caveBlendRandom(long seed, int x, int y, int z) {
long scrambled = ((seed + BlockPosition.toLong(x, y, z)) ^ RANDOM_MULTIPLIER) & RANDOM_MASK;
return (int) (((scrambled * RANDOM_MULTIPLIER + RANDOM_ADDEND) & RANDOM_MASK) >>> 16);
}
private IrisBiome sampleCaveBiome(Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, int x, int y, int z, CaveInputs resolverState) {
long key = BlockPosition.toLong(x, y, z);
IrisBiome cachedBiome = caveBiomeCache.get(key);
@@ -21,6 +21,7 @@ package art.arcane.iris.generation.stage;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import java.util.Arrays;
import java.util.HashMap;
@@ -32,6 +33,7 @@ final class IrisCarveScratch {
final int[] surfaceHeights = new int[256];
final CarveWallBuffer walls = new CarveWallBuffer(512);
final Map<String, IrisBiome> customBiomeCache = new HashMap<>();
final Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache = new Long2ObjectOpenHashMap<>(4096);
int[] upperSurfaceHeights;
boolean customCaveBiomePresent;
@@ -56,6 +58,7 @@ final class IrisCarveScratch {
}
walls.clear();
customBiomeCache.clear();
caveBiomeCache.clear();
customCaveBiomePresent = false;
}
}
@@ -36,6 +36,7 @@ import art.arcane.iris.generation.hydrology.IrisRiverMaterialConfig;
import art.arcane.iris.generation.hydrology.IrisSurfaceRiverBankConfig;
import art.arcane.iris.generation.terrain.Terrain3DColumn;
import art.arcane.iris.generation.block.BoundBlockState;
import art.arcane.iris.generation.chunk.ColumnRunHunk;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.generation.context.ChunkedDataCache;
import art.arcane.iris.generation.context.ChunkContext;
@@ -176,6 +177,15 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
private IrisOreBands regionSurfaceOres;
private IrisOreBands biomeUndergroundOres;
private IrisOreBands regionUndergroundOres;
private int surfaceOreTop;
private int surfaceOreFloor;
private int undergroundOreTop;
private int undergroundOreFloor;
private int columnX;
private int columnZ;
private NativeBlockState runState;
private int runTop = -1;
private int runBottom;
private ColumnPainter(int x, int z, Hunk<NativeBlockState> h, ChunkContext context) {
this.x = x;
@@ -260,8 +270,12 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
regionSurfaceOres = hideOres ? IrisOreBands.EMPTY : region.getSurfaceOreBands();
biomeUndergroundOres = hideOres ? IrisOreBands.EMPTY : biome.getUndergroundOreBands();
regionUndergroundOres = hideOres ? IrisOreBands.EMPTY : region.getUndergroundOreBands();
boolean hasSurfaceOres = biomeSurfaceOres.hasOres() || regionSurfaceOres.hasOres() || dimensionSurfaceOres.hasOres();
boolean hasUndergroundOres = biomeUndergroundOres.hasOres() || regionUndergroundOres.hasOres() || dimensionUndergroundOres.hasOres();
surfaceOreTop = oreTop(biomeSurfaceOres, regionSurfaceOres, dimensionSurfaceOres);
surfaceOreFloor = oreFloor(biomeSurfaceOres, regionSurfaceOres, dimensionSurfaceOres);
undergroundOreTop = oreTop(biomeUndergroundOres, regionUndergroundOres, dimensionUndergroundOres);
undergroundOreFloor = oreFloor(biomeUndergroundOres, regionUndergroundOres, dimensionUndergroundOres);
columnX = xf;
columnZ = zf;
int bedrockFloor = bedrockEnabled ? 1 : 0;
KList<NativeBlockState> blocks = null;
KList<NativeBlockState> ceilingBlocks = null;
@@ -269,7 +283,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
for (int i = topY; i >= 0; i--) {
if (i == 0 && bedrockEnabled) {
h.setRaw(xf, i, zf, bedrock);
put(i, bedrock);
continue;
}
@@ -280,16 +294,16 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
ceilingBlocks = null;
}
if (terrainSpan < 0 || i > terrainColumn.floor(terrainSpan)) {
h.setRaw(xf, i, zf, air);
put(i, air);
continue;
}
layerSurfaceY = terrainColumn.floor(terrainSpan);
layerCeilingY = terrainColumn.ceiling(terrainSpan);
}
NativeBlockState ore = hasSurfaceOres ? surfaceOre(realX, i, realZ) : null;
NativeBlockState ore = i <= surfaceOreTop && i >= surfaceOreFloor ? surfaceOre(realX, i, realZ) : null;
if (ore != null) {
h.setRaw(xf, i, zf, ore);
put(i, ore);
continue;
}
@@ -298,7 +312,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
if (hydrologyFluid == null && fblocks == null) {
fblocks = biome.generateSeaLayers(realX, realZ, rng, hf - he, data);
}
h.setRaw(xf, i, zf, HydrologyFluidLayerSelector.select(
put(i, HydrologyFluidLayerSelector.select(
fblocks,
fdepth,
fluid,
@@ -310,7 +324,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
if (i <= he) {
int depth = layerSurfaceY - i;
if (depth == 0 && mappedSurfaceBlock != null) {
h.setRaw(xf, i, zf, mappedSurfaceBlock);
put(i, mappedSurfaceBlock);
continue;
}
if (blocks == null) {
@@ -324,7 +338,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
int deepFloor = Math.max(bedrockFloor, layerCeilingY > 0 ? layerCeilingY + 2 : 0);
if (i >= deepFloor && depth >= Math.max(blocks.size(), paintDepth)) {
paintDeep(xf, zf, realX, realZ, i, deepFloor, rock, hasSurfaceOres, hasUndergroundOres);
paintDeep(realX, realZ, i, deepFloor, rock);
i = deepFloor;
continue;
}
@@ -335,7 +349,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
2, layerCeilingY, data, complex);
}
int ceilingDepth = i - layerCeilingY;
h.setRaw(xf, i, zf, ceilingBlocks.hasIndex(ceilingDepth)
put(i, ceilingBlocks.hasIndex(ceilingDepth)
? ceilingBlocks.get(ceilingDepth) : rock);
continue;
}
@@ -348,17 +362,19 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
if (riverOwned && depth <= riverBed.getPadding() && IrisProceduralBlocks.isGravityAffected(layerBlock)) {
layerBlock = riverBed.getPaddingPalette().get(rng, realX, i, realZ, data);
}
h.setRaw(xf, i, zf, layerBlock);
put(i, layerBlock);
continue;
}
ore = hasUndergroundOres ? undergroundOre(realX, i, realZ) : null;
h.setRaw(xf, i, zf, ore != null ? ore : rock);
ore = i <= undergroundOreTop && i >= undergroundOreFloor ? undergroundOre(realX, i, realZ) : null;
put(i, ore != null ? ore : rock);
}
}
flush();
if (upperContext != null) {
paintUpper(xf, zf, realX, realZ);
paintUpper(realX, realZ);
flush();
}
}
@@ -366,15 +382,78 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
* Paints the solid run from {@code top} down to {@code floor}, which lies below every biome
* layer, river material, mapped surface and cave ceiling of its span: only ores or rock land there.
*/
private void paintDeep(int xf, int zf, int realX, int realZ, int top, int floor, NativeBlockState rock,
boolean hasSurfaceOres, boolean hasUndergroundOres) {
for (int y = top; y >= floor; y--) {
NativeBlockState ore = hasSurfaceOres ? surfaceOre(realX, y, realZ) : null;
if (ore == null && hasUndergroundOres) {
private void paintDeep(int realX, int realZ, int top, int floor, NativeBlockState rock) {
int bandTop = Math.min(top, Math.max(surfaceOreTop, undergroundOreTop));
int bandFloor = Math.max(floor, Math.min(surfaceOreFloor, undergroundOreFloor));
if (bandTop < bandFloor) {
putRun(top, floor, rock);
return;
}
if (top > bandTop) {
putRun(top, bandTop + 1, rock);
}
for (int y = bandTop; y >= bandFloor; y--) {
NativeBlockState ore = y <= surfaceOreTop && y >= surfaceOreFloor ? surfaceOre(realX, y, realZ) : null;
if (ore == null && y <= undergroundOreTop && y >= undergroundOreFloor) {
ore = undergroundOre(realX, y, realZ);
}
h.setRaw(xf, y, zf, ore != null ? ore : rock);
put(y, ore != null ? ore : rock);
}
if (bandFloor > floor) {
putRun(bandFloor - 1, floor, rock);
}
}
private void put(int y, NativeBlockState state) {
putRun(y, y, state);
}
private void putRun(int top, int bottom, NativeBlockState state) {
if (runTop >= 0 && state == runState && top == runBottom - 1) {
runBottom = bottom;
return;
}
flush();
runState = state;
runTop = top;
runBottom = bottom;
}
private void flush() {
if (runTop < 0) {
return;
}
ColumnRunHunk.fillColumnRun(h, columnX, columnZ, runBottom, runTop, runState);
runTop = -1;
runState = null;
}
private static int oreTop(IrisOreBands first, IrisOreBands second, IrisOreBands third) {
int top = Integer.MIN_VALUE;
if (first.hasOres()) {
top = Math.max(top, first.highestY());
}
if (second.hasOres()) {
top = Math.max(top, second.highestY());
}
if (third.hasOres()) {
top = Math.max(top, third.highestY());
}
return top;
}
private static int oreFloor(IrisOreBands first, IrisOreBands second, IrisOreBands third) {
int floor = Integer.MAX_VALUE;
if (first.hasOres()) {
floor = Math.min(floor, first.lowestY());
}
if (second.hasOres()) {
floor = Math.min(floor, second.lowestY());
}
if (third.hasOres()) {
floor = Math.min(floor, third.lowestY());
}
return floor;
}
private NativeBlockState surfaceOre(int realX, int y, int realZ) {
@@ -405,7 +484,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
return ore;
}
private void paintUpper(int xf, int zf, int realX, int realZ) {
private void paintUpper(int realX, int realZ) {
UpperDimensionContext.Column upperColumn = upperContext.sampleColumn(realX, realZ);
int upperSurfaceY = upperColumn.surfaceY();
if (upperSurfaceY >= chunkHeight - 1) {
@@ -420,11 +499,11 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
for (int y = chunkHeight - 1; y >= upperSurfaceY; y--) {
if (y == chunkHeight - 1 && bedrockEnabled) {
h.setRaw(xf, y, zf, bedrock);
put(y, bedrock);
continue;
}
if (!upperColumn.isSolid(y)) {
h.setRaw(xf, y, zf, air);
put(y, air);
continue;
}
int faceY = upperColumn.faceY(y);
@@ -434,7 +513,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
upperBlocks = null;
}
if (y == faceY && upperMappedSurface != null) {
h.setRaw(xf, y, zf, upperMappedSurface);
put(y, upperMappedSurface);
continue;
}
int depthFromFace = y - faceY;
@@ -444,9 +523,9 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
upperContext.getData(), upperContext.getSurfaceSlopeStream(sourceSurfaceY));
}
if (upperBlocks != null && upperBlocks.hasIndex(depthFromFace)) {
h.setRaw(xf, y, zf, upperBlocks.get(depthFromFace));
put(y, upperBlocks.get(depthFromFace));
} else {
h.setRaw(xf, y, zf, upperRock);
put(y, upperRock);
}
}
}
@@ -6,14 +6,21 @@ import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReferenceArray;
import java.util.function.Predicate;
public final class SubterrainPlanner {
private static final int PLAN_CACHE_SLOTS = 4096;
private static final int TILE_CACHE_SLOTS = 1024;
private static final Comparator<SubterrainPlan> PLAN_ORDER =
Comparator.comparingInt(SubterrainPlan::priority).reversed().thenComparing(SubterrainPlan::id);
private final List<Definition> definitions;
private final long seed;
private final int minY;
private final int maxYExclusive;
private final ThreadLocal<CandidateTile> sampleTile = new ThreadLocal<>();
private final AtomicReferenceArray<CachedPlan> planCache = new AtomicReferenceArray<>(PLAN_CACHE_SLOTS);
private final AtomicReferenceArray<CandidateTile> tileCache = new AtomicReferenceArray<>(TILE_CACHE_SLOTS);
private final Predicate<SubterrainBounds> allowedBounds;
private final RegionResolver regionResolver;
@@ -70,8 +77,9 @@ public final class SubterrainPlanner {
if (minX > maxX || minZ > maxZ) {
throw new IllegalArgumentException("Subterrain query bounds are inverted");
}
List<SubterrainPlan> result = new ArrayList<>();
for (Definition definition : definitions) {
List<SubterrainPlan> result = null;
for (int definitionIndex = 0; definitionIndex < definitions.size(); definitionIndex++) {
Definition definition = definitions.get(definitionIndex);
long firstX = Math.floorDiv(minX, definition.spacing()) - 1L;
long lastX = Math.floorDiv(maxX, definition.spacing()) + 1L;
long firstZ = Math.floorDiv(minZ, definition.spacing()) - 1L;
@@ -81,14 +89,20 @@ public final class SubterrainPlanner {
}
for (long gridX = firstX; gridX <= lastX; gridX++) {
for (long gridZ = firstZ; gridZ <= lastZ; gridZ++) {
SubterrainPlan plan = create(definition, gridX, gridZ);
SubterrainPlan plan = plan(definitionIndex, definition, gridX, gridZ);
if (plan != null && plan.bounds().intersects(minX, minZ, maxX, maxZ)) {
if (result == null) {
result = new ArrayList<>();
}
result.add(plan);
}
}
}
}
result.sort(Comparator.comparingInt(SubterrainPlan::priority).reversed().thenComparing(SubterrainPlan::id));
if (result == null) {
return List.of();
}
result.sort(PLAN_ORDER);
return List.copyOf(result);
}
@@ -96,20 +110,7 @@ public final class SubterrainPlanner {
if (isEmpty() || y < minY || y >= maxYExclusive) {
return SubterrainCell.OUTSIDE;
}
int chunkX = Math.floorDiv(x, 16);
int chunkZ = Math.floorDiv(z, 16);
CandidateTile tile = sampleTile.get();
if (tile == null || tile.chunkX() != chunkX || tile.chunkZ() != chunkZ) {
long baseX = (long) chunkX * 16;
long baseZ = (long) chunkZ * 16;
tile = new CandidateTile(chunkX, chunkZ, plansForBounds(
(int) Math.max(Integer.MIN_VALUE, baseX - 1),
(int) Math.max(Integer.MIN_VALUE, baseZ - 1),
(int) Math.min(Integer.MAX_VALUE, baseX + 16),
(int) Math.min(Integer.MAX_VALUE, baseZ + 16)));
sampleTile.set(tile);
}
return sample(tile.plans(), x, y, z);
return sample(candidates(Math.floorDiv(x, 16), Math.floorDiv(z, 16)), x, y, z);
}
public SubterrainCell sample(List<SubterrainPlan> candidates, int x, int y, int z) {
@@ -127,10 +128,38 @@ public final class SubterrainPlanner {
return selected;
}
private List<SubterrainPlan> candidates(int chunkX, int chunkZ) {
int slot = (int) mix(((long) chunkX << 32) ^ (chunkZ & 0xffffffffL)) & (TILE_CACHE_SLOTS - 1);
CandidateTile tile = tileCache.getAcquire(slot);
if (tile != null && tile.chunkX() == chunkX && tile.chunkZ() == chunkZ) {
return tile.plans();
}
long baseX = (long) chunkX * 16;
long baseZ = (long) chunkZ * 16;
List<SubterrainPlan> plans = plansForBounds(
(int) Math.max(Integer.MIN_VALUE, baseX - 1),
(int) Math.max(Integer.MIN_VALUE, baseZ - 1),
(int) Math.min(Integer.MAX_VALUE, baseX + 16),
(int) Math.min(Integer.MAX_VALUE, baseZ + 16));
tileCache.setRelease(slot, new CandidateTile(chunkX, chunkZ, plans));
return plans;
}
private SubterrainPlan plan(int definitionIndex, Definition definition, long gridX, long gridZ) {
int slot = (int) mix(mix(gridX ^ definition.seedSalt()) ^ Long.rotateLeft(gridZ, 29)) & (PLAN_CACHE_SLOTS - 1);
CachedPlan cached = planCache.getAcquire(slot);
if (cached != null && cached.definition() == definitionIndex && cached.gridX() == gridX && cached.gridZ() == gridZ) {
return cached.plan();
}
SubterrainPlan plan = create(definition, gridX, gridZ);
planCache.setRelease(slot, new CachedPlan(definitionIndex, gridX, gridZ, plan));
return plan;
}
private SubterrainCell raw(List<SubterrainPlan> candidates, int x, int y, int z) {
SubterrainCell selected = SubterrainCell.OUTSIDE;
for (SubterrainPlan plan : candidates) {
SubterrainCell cell = plan.sample(x, y, z);
for (int index = 0; index < candidates.size(); index++) {
SubterrainCell cell = candidates.get(index).sample(x, y, z);
if (cell.solid() && cell.room().reservedSolid()) {
return cell;
}
@@ -212,6 +241,9 @@ public final class SubterrainPlanner {
private record CandidateTile(int chunkX, int chunkZ, List<SubterrainPlan> plans) {
}
private record CachedPlan(int definition, long gridX, long gridZ, SubterrainPlan plan) {
}
record Definition(String id, IrisSubterrainFamily family, IrisSubterrainFluid fluid, String biome, int spacing, double probability,
int length, int radius, int height, double shapeWarp, int fluidDepth, int chimneyHeight, int terraceCount,
int pillarSpacing, double formationFraction, String solid, int priority,
@@ -109,6 +109,7 @@ import java.util.concurrent.ConcurrentHashMap;
@Data
public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
private static final int CUSTOM_BIOME_KEY_CACHE_LIMIT = 4096;
private static final Map<File, IrisData> dataLoaders = new ConcurrentHashMap<>();
// Loaders (cached or detached) that currently have registered engines; see hasActiveEngines.
// Identity-keyed on purpose: Lombok's @Data hashCode over this class's mutable loader state
@@ -159,6 +160,10 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
@Setter(AccessLevel.NONE)
@EqualsAndHashCode.Exclude
private transient volatile GenerationRegistryContractFactory.CustomBiomeResourceResolver customBiomeResourceResolver;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
@EqualsAndHashCode.Exclude
private transient volatile CustomBiomeKeyCache customBiomeKeyCache;
private transient volatile Engine soleEngine;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
@@ -234,6 +239,32 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
IrisBiomeCustom requiredBiome = Objects.requireNonNull(customBiome, "customBiome");
GenerationRegistryContractFactory.CustomBiomeResourceResolver resolver = customBiomeResourceResolver;
ContentGate gate = resolver == null ? getContentGate() : null;
Object owner = resolver == null ? gate : resolver;
CustomBiomeKeyCache cache = customBiomeKeyCache;
if (cache == null || cache.owner() != owner) {
cache = new CustomBiomeKeyCache(owner, new ConcurrentHashMap<>());
customBiomeKeyCache = cache;
}
CustomBiomeKey key = new CustomBiomeKey(requiredDimension.getLoadKey(), requiredBiome);
String cached = cache.keys().get(key);
if (cached != null) {
return cached;
}
String resolved = resolveCustomBiomeResourceKey(requiredDimension, requiredBiome, resolver, gate);
if (cache.keys().size() >= CUSTOM_BIOME_KEY_CACHE_LIMIT) {
cache.keys().clear();
}
cache.keys().put(key, resolved);
return resolved;
}
private String resolveCustomBiomeResourceKey(
IrisDimension requiredDimension,
IrisBiomeCustom requiredBiome,
GenerationRegistryContractFactory.CustomBiomeResourceResolver resolver,
ContentGate gate
) {
if (resolver != null) {
try {
return resolver.resolve(
@@ -248,7 +279,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
return GenerationRegistryContractFactory.customBiomeResourceKey(
requiredDimension.getLoadKey(),
requiredBiome,
getContentGate()
gate
);
}
@@ -1035,4 +1066,29 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
return immutable;
}
}
private record CustomBiomeKeyCache(Object owner, ConcurrentHashMap<CustomBiomeKey, String> keys) {
}
private static final class CustomBiomeKey {
private final String dimensionKey;
private final IrisBiomeCustom biome;
private final int hash;
private CustomBiomeKey(String dimensionKey, IrisBiomeCustom biome) {
this.dimensionKey = dimensionKey;
this.biome = biome;
hash = 31 * Objects.hashCode(dimensionKey) + System.identityHashCode(biome);
}
@Override
public boolean equals(Object other) {
return other instanceof CustomBiomeKey key && key.biome == biome && Objects.equals(key.dimensionKey, dimensionKey);
}
@Override
public int hashCode() {
return hash;
}
}
}
@@ -20,7 +20,7 @@ package art.arcane.iris.platform.bukkit.nms;
import art.arcane.volmlib.nativelib.terrain.ServerShutdownBoundary;
import art.arcane.volmlib.nativelib.terrain.NativeBlockVolume;
import art.arcane.volmlib.nativelib.terrain.NativeSectionVolume;
import art.arcane.volmlib.nativelib.terrain.BukkitTerrainBuffer;
@@ -217,7 +217,7 @@ public interface INMSBinding extends NativeTerrainAccess {
return false;
}
default boolean applyChunkDataBlocks(ChunkGenerator.ChunkData chunkData, NativeBlockVolume data) {
default boolean applyChunkDataBlocks(ChunkGenerator.ChunkData chunkData, NativeSectionVolume data) {
return false;
}
@@ -76,7 +76,8 @@ import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.iris.generation.chunk.ColumnBiomeHunk;
import art.arcane.iris.platform.bukkit.BukkitBiome;
import art.arcane.iris.generation.chunk.ChunkDataHunkHolder;
import art.arcane.iris.generation.chunk.ChunkDataOutput;
import art.arcane.iris.generation.chunk.SectionBuffer;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.volmlib.util.io.ReactiveFolder;
@@ -1344,7 +1345,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
if (selected != null) {
selected.generateChunk(engine, TerrainChunk.create(d), x, z);
} else {
ChunkDataHunkHolder blocks = new ChunkDataHunkHolder(d);
SectionBuffer blocks = new SectionBuffer(d.getMaxHeight() - d.getMinHeight());
Hunk<NativeBiome> biomes = new ColumnBiomeHunk(d.getMaxHeight() - d.getMinHeight(), () -> BukkitBiome.of(Biome.PLAINS));
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope =
openGenerationHistoryCoordinateScope(engine, x << 4, z << 4);
@@ -1352,7 +1353,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
engine.generate(x << 4, z << 4, blocks, biomes,
initialEntryPending || studioEntryBootstrapActive.get() || engine.shouldGenerateMulticore());
blocks.apply();
ChunkDataOutput.apply(d, blocks);
if (historyScope != null) {
historyScope.claimGeneratedSemantics();
}
@@ -24,6 +24,7 @@ import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.volmlib.util.parallel.MultiBurstSupport;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
@@ -45,7 +46,7 @@ public final class PregenPerformanceProfile {
changed = true;
}
if (MultiBurst.burst.raiseParallelism(pregenBurstParallelism(Runtime.getRuntime().availableProcessors()))) {
if (MultiBurst.burst.raiseParallelism(pregenBurstParallelism(MultiBurstSupport.performanceProcessors()))) {
PARALLELISM_HOLDERS.incrementAndGet();
changed = true;
}
@@ -88,8 +89,8 @@ public final class PregenPerformanceProfile {
apply(generator == null ? null : generator.getEngine());
}
static int pregenBurstParallelism(int availableProcessors) {
return Math.max(4, availableProcessors * 2);
static int pregenBurstParallelism(int performanceProcessors) {
return Math.max(4, performanceProcessors * 2);
}
private static void prepareNoiseCacheInPlace(Engine engine) {
@@ -6,8 +6,7 @@ import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.iris.generation.chunk.ChunkDataHunkHolder;
import org.bukkit.generator.ChunkGenerator;
import art.arcane.iris.generation.chunk.SectionBuffer;
import org.junit.After;
import org.junit.ClassRule;
import org.junit.Test;
@@ -37,7 +36,7 @@ public class PlatformStateRebindTest {
"art.arcane.iris.generation.biome.IrisBiomeLayerGenerator",
"art.arcane.iris.studio.generation.BiomeBuffetGenerator",
"art.arcane.iris.generation.mantle.EngineMantle",
"art.arcane.iris.generation.chunk.ChunkDataHunkHolder");
"art.arcane.iris.generation.chunk.SectionBuffer");
@After
public void unbindPlatform() {
@@ -47,10 +46,7 @@ public class PlatformStateRebindTest {
@Test
public void chunkDataAirIsResolvedAgainstTheCurrentlyBoundPlatform() {
NativeBlockState first = bindPlatform("first");
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
when(chunk.getMinHeight()).thenReturn(-64);
when(chunk.getMaxHeight()).thenReturn(320);
ChunkDataHunkHolder holder = new ChunkDataHunkHolder(chunk);
SectionBuffer holder = new SectionBuffer(384);
assertSame(first, holder.getRaw(0, -1, 0));
@@ -1,155 +0,0 @@
package art.arcane.iris.generation.chunk;
import art.arcane.iris.platform.bukkit.nms.INMS;
import art.arcane.iris.platform.bukkit.nms.INMSBinding;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import org.bukkit.block.data.BlockData;
import org.bukkit.generator.ChunkGenerator;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class ChunkDataHunkHolderTest {
@Test
public void columnTopTracksTheHighestStoredValueAndIgnoresNulls() {
ChunkDataHunkHolder holder = holder(64);
NativeBlockState stone = mock(NativeBlockState.class);
assertEquals(-1, holder.highestStoredY(3, 7));
holder.setRaw(3, 40, 7, null);
assertEquals(-1, holder.highestStoredY(3, 7));
holder.setRaw(3, 12, 7, stone);
holder.setRaw(3, 30, 7, stone);
holder.setRaw(3, 5, 7, stone);
assertEquals(30, holder.highestStoredY(3, 7));
assertEquals(-1, holder.highestStoredY(7, 3));
assertSame(stone, holder.getStoredRaw(3, 30, 7));
assertNull(holder.getStoredRaw(3, 31, 7));
assertEquals(30, ColumnExtent.highestStoredY(new ColumnExtentListeningHunk<>(holder, (x, y, z, state) -> {
}), 3, 7));
}
@Test
public void cellsKeepTheZMajorLayout() {
ChunkDataHunkHolder holder = holder(32);
List<NativeBlockState> states = new ArrayList<>();
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < 32; y++) {
NativeBlockState state = mock(NativeBlockState.class);
states.add(state);
holder.setRaw(x, y, z, state);
}
}
}
int index = 0;
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < 32; y++) {
assertSame(states.get(index++), holder.getStoredRaw(x, y, z));
}
}
}
}
@Test
public void concurrentWritersNeverLoseTheColumnMaximum() throws InterruptedException {
for (int trial = 0; trial < 20; trial++) {
ChunkDataHunkHolder holder = holder(768);
NativeBlockState stone = mock(NativeBlockState.class);
int threads = 8;
CountDownLatch start = new CountDownLatch(1);
List<Thread> writers = new ArrayList<>();
for (int thread = 0; thread < threads; thread++) {
int offset = thread;
Thread writer = new Thread(() -> {
try {
start.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
for (int y = offset; y < 768; y += threads) {
holder.setRaw(9, y, 4, stone);
}
});
writer.start();
writers.add(writer);
}
start.countDown();
for (Thread writer : writers) {
writer.join();
}
assertEquals(767, holder.highestStoredY(9, 4));
}
}
@Test
public void fallbackAppliesExplicitCaveAirBetweenSolidRunsWithNegativeMinimumHeight() {
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
when(chunk.getMinHeight()).thenReturn(-256);
when(chunk.getMaxHeight()).thenReturn(512);
ChunkDataHunkHolder holder = new ChunkDataHunkHolder(chunk);
BlockData dirt = mock(BlockData.class);
BlockData coal = mock(BlockData.class);
BlockData caveAir = mock(BlockData.class);
NativeBlockState dirtState = state(dirt);
NativeBlockState coalState = state(coal);
NativeBlockState caveAirState = state(caveAir);
BlockData[] applied = new BlockData[768];
Arrays.fill(applied, dirt);
for (int y = 0; y <= 75; y++) {
holder.setRaw(14, y + 256, 6, y < 33 ? dirtState : y <= 54 ? caveAirState : coalState);
}
doAnswer(call -> {
int y = call.getArgument(1);
applied[y + 256] = call.getArgument(3);
return null;
}).when(chunk).setBlock(anyInt(), anyInt(), anyInt(), any(BlockData.class));
doAnswer(call -> {
int from = call.getArgument(1);
int to = call.getArgument(4);
BlockData block = call.getArgument(6);
Arrays.fill(applied, from + 256, to + 256, block);
return null;
}).when(chunk).setRegion(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(BlockData.class));
try (MockedStatic<INMS> nms = mockStatic(INMS.class)) {
nms.when(INMS::get).thenReturn(mock(INMSBinding.class));
holder.apply();
}
for (int y = -256; y < 512; y++) {
BlockData expected = y >= 33 && y <= 54 ? caveAir : y >= 55 && y <= 75 ? coal : dirt;
assertSame("column at " + y, expected, applied[y + 256]);
}
}
private static NativeBlockState state(BlockData data) {
NativeBlockState state = mock(NativeBlockState.class);
when(state.nativeHandle()).thenReturn(data);
return state;
}
private static ChunkDataHunkHolder holder(int height) {
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
when(chunk.getMinHeight()).thenReturn(0);
when(chunk.getMaxHeight()).thenReturn(height);
return new ChunkDataHunkHolder(chunk);
}
}
@@ -0,0 +1,88 @@
package art.arcane.iris.generation.chunk;
import art.arcane.iris.platform.bukkit.nms.INMS;
import art.arcane.iris.platform.bukkit.nms.INMSBinding;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import org.bukkit.block.data.BlockData;
import org.bukkit.generator.ChunkGenerator;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.Arrays;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ChunkDataOutputTest {
@Test
public void nativeWriterReceivesTheSectionBuffer() {
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
SectionBuffer blocks = new SectionBuffer(64);
INMSBinding binding = mock(INMSBinding.class);
when(binding.applyChunkDataBlocks(chunk, blocks)).thenReturn(true);
try (MockedStatic<INMS> nms = mockStatic(INMS.class)) {
nms.when(INMS::get).thenReturn(binding);
ChunkDataOutput.apply(chunk, blocks);
}
verify(binding).applyChunkDataBlocks(chunk, blocks);
verify(chunk, never()).setRegion(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(BlockData.class));
}
@Test
public void fallbackAppliesExplicitCaveAirBetweenSolidRunsWithNegativeMinimumHeight() {
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
when(chunk.getMinHeight()).thenReturn(-256);
when(chunk.getMaxHeight()).thenReturn(512);
SectionBuffer blocks = new SectionBuffer(768);
BlockData dirt = mock(BlockData.class);
BlockData coal = mock(BlockData.class);
BlockData caveAir = mock(BlockData.class);
NativeBlockState dirtState = state(dirt);
NativeBlockState coalState = state(coal);
NativeBlockState caveAirState = state(caveAir);
BlockData[] applied = new BlockData[768];
Arrays.fill(applied, dirt);
for (int y = 0; y <= 75; y++) {
blocks.setRaw(14, y + 256, 6, y < 33 ? dirtState : y <= 54 ? caveAirState : coalState);
}
doAnswer(call -> {
int y = call.getArgument(1);
applied[y + 256] = call.getArgument(3);
return null;
}).when(chunk).setBlock(anyInt(), anyInt(), anyInt(), any(BlockData.class));
doAnswer(call -> {
int from = call.getArgument(1);
int to = call.getArgument(4);
BlockData block = call.getArgument(6);
Arrays.fill(applied, from + 256, to + 256, block);
return null;
}).when(chunk).setRegion(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(BlockData.class));
try (MockedStatic<INMS> nms = mockStatic(INMS.class)) {
nms.when(INMS::get).thenReturn(mock(INMSBinding.class));
ChunkDataOutput.apply(chunk, blocks);
}
for (int y = -256; y < 512; y++) {
BlockData expected = y >= 33 && y <= 54 ? caveAir : y >= 55 && y <= 75 ? coal : dirt;
assertSame("column at " + y, expected, applied[y + 256]);
}
assertTrue(blocks.highestStoredY(14, 6) == 331);
}
private static NativeBlockState state(BlockData data) {
NativeBlockState state = mock(NativeBlockState.class);
when(state.nativeHandle()).thenReturn(data);
return state;
}
}
@@ -0,0 +1,322 @@
package art.arcane.iris.generation.chunk;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.hunk.Hunk;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class SectionBufferTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private NativeBlockState air;
@Before
public void bindPlatform() {
air = mock(NativeBlockState.class);
PlatformRegistries registries = mock(PlatformRegistries.class);
when(registries.block(anyString())).thenReturn(air);
IrisPlatform platform = mock(IrisPlatform.class);
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
}
@After
public void unbindPlatform() {
IrisPlatforms.unbind();
}
@Test
public void columnTopTracksTheHighestStoredValueAndIgnoresNulls() {
SectionBuffer buffer = new SectionBuffer(64);
NativeBlockState stone = mock(NativeBlockState.class);
assertEquals(-1, buffer.highestStoredY(3, 7));
buffer.setRaw(3, 40, 7, null);
assertEquals(-1, buffer.highestStoredY(3, 7));
buffer.setRaw(3, 12, 7, stone);
buffer.setRaw(3, 30, 7, stone);
buffer.setRaw(3, 5, 7, stone);
assertEquals(30, buffer.highestStoredY(3, 7));
assertEquals(-1, buffer.highestStoredY(7, 3));
assertSame(stone, buffer.getStoredRaw(3, 30, 7));
assertNull(buffer.getStoredRaw(3, 31, 7));
assertEquals(30, ColumnExtent.highestStoredY(new ColumnExtentListeningHunk<>(buffer, (x, y, z, state) -> {
}), 3, 7));
}
@Test
public void unwrittenAndOutOfRangeCellsReadAsAir() {
SectionBuffer buffer = new SectionBuffer(48);
NativeBlockState stone = mock(NativeBlockState.class);
buffer.setRaw(2, 20, 9, stone);
assertSame(air, buffer.getRaw(2, 21, 9));
assertSame(air, buffer.getRaw(2, 3, 9));
assertSame(air, buffer.getRaw(2, -1, 9));
assertSame(air, buffer.getRaw(2, 48, 9));
assertSame(stone, buffer.getRaw(2, 20, 9));
assertNull(buffer.getStoredRaw(2, 21, 9));
assertNull(buffer.getStoredRaw(2, 3, 9));
}
@Test
public void paletteKeepsEveryDistinctInstance() {
SectionBuffer buffer = new SectionBuffer(32);
NativeBlockState first = mock(NativeBlockState.class);
NativeBlockState second = mock(NativeBlockState.class);
when(first.matches(second)).thenReturn(true);
when(second.matches(first)).thenReturn(true);
buffer.setRaw(0, 0, 0, first);
buffer.setRaw(1, 0, 0, second);
buffer.setRaw(2, 0, 0, first);
assertSame(first, buffer.getRaw(0, 0, 0));
assertSame(second, buffer.getRaw(1, 0, 0));
assertSame(first, buffer.getRaw(2, 0, 0));
assertEquals(buffer.paletteId(first), buffer.paletteId(first));
assertNotEquals(buffer.paletteId(first), buffer.paletteId(second));
assertEquals(0, buffer.paletteId(null));
NativeBlockState[] palette = buffer.palette();
assertEquals(3, palette.length);
assertNull(palette[0]);
assertSame(first, palette[buffer.paletteId(first)]);
assertSame(second, palette[buffer.paletteId(second)]);
}
@Test
public void sectionsUseTheYzxLayoutAndStayUnallocatedUntilWritten() {
SectionBuffer buffer = new SectionBuffer(64);
NativeBlockState stone = mock(NativeBlockState.class);
NativeBlockState dirt = mock(NativeBlockState.class);
buffer.setRaw(5, 37, 11, stone);
buffer.setRaw(15, 47, 15, dirt);
assertEquals(4, buffer.sectionCount());
assertNull(buffer.section(0));
assertNull(buffer.section(1));
assertNull(buffer.section(3));
short[] section = buffer.section(2);
assertEquals(4096, section.length);
assertEquals(buffer.paletteId(stone), section[(5 << 8) | (11 << 4) | 5]);
assertEquals(buffer.paletteId(dirt), section[(15 << 8) | (15 << 4) | 15]);
int written = 0;
for (short id : section) {
written += id == 0 ? 0 : 1;
}
assertEquals(2, written);
}
@Test
public void columnRunsMatchRepeatedSingleWrites() {
Random random = new Random(41L);
List<NativeBlockState> pool = new ArrayList<>();
for (int index = 0; index < 9; index++) {
pool.add(mock(NativeBlockState.class));
}
pool.add(null);
for (int trial = 0; trial < 40; trial++) {
int height = 16 * (1 + random.nextInt(12));
SectionBuffer runs = new SectionBuffer(height);
SectionBuffer cells = new SectionBuffer(height);
for (int write = 0; write < 300; write++) {
int x = random.nextInt(16);
int z = random.nextInt(16);
int from = random.nextInt(height);
int to = Math.min(height - 1, from + random.nextInt(70));
NativeBlockState state = pool.get(random.nextInt(pool.size()));
switch (random.nextInt(3)) {
case 0 -> runs.fillColumnRun(x, z, from, to, state);
case 1 -> runs.fillColumnRun(x, z, from, to, runs.paletteId(state));
default -> ColumnRunHunk.fillColumnRun(runs, x, z, from, to, state);
}
for (int y = to; y >= from; y--) {
cells.setRaw(x, y, z, state);
}
}
assertSameContents(cells, runs);
}
}
@Test
public void runsWithAnEmptyRangeOrNoStateWriteNothing() {
SectionBuffer buffer = new SectionBuffer(32);
NativeBlockState stone = mock(NativeBlockState.class);
buffer.fillColumnRun(1, 1, 10, 9, stone);
buffer.fillColumnRun(1, 1, 3, 9, (NativeBlockState) null);
buffer.fillColumnRun(1, 1, 3, 9, 0);
assertEquals(-1, buffer.highestStoredY(1, 1));
assertNull(buffer.section(0));
assertEquals(1, buffer.palette().length);
}
@Test
public void concurrentWritersNeverLoseTheColumnMaximum() throws InterruptedException {
for (int trial = 0; trial < 20; trial++) {
SectionBuffer buffer = new SectionBuffer(768);
NativeBlockState stone = mock(NativeBlockState.class);
int threads = 8;
runConcurrently(threads, thread -> {
for (int y = thread; y < 768; y += threads) {
buffer.setRaw(9, y, 4, stone);
}
});
assertEquals(767, buffer.highestStoredY(9, 4));
}
}
@Test
public void concurrentPaletteGrowthKeepsOneIdPerInstance() throws InterruptedException {
int threads = 8;
int statesPerThread = 300;
NativeBlockState[][] states = new NativeBlockState[threads][statesPerThread];
NativeBlockState[] shared = new NativeBlockState[40];
for (int index = 0; index < shared.length; index++) {
shared[index] = mock(NativeBlockState.class);
}
for (int thread = 0; thread < threads; thread++) {
for (int index = 0; index < statesPerThread; index++) {
states[thread][index] = mock(NativeBlockState.class);
}
}
for (int trial = 0; trial < 10; trial++) {
SectionBuffer buffer = new SectionBuffer(384);
runConcurrently(threads, thread -> {
for (int index = 0; index < statesPerThread; index++) {
int x = thread * 2 + (index & 1);
int z = (index >> 1) & 15;
int y = index;
buffer.setRaw(x, y, z, states[thread][index]);
buffer.setRaw(x, y + 1, z, shared[index % shared.length]);
}
});
for (int thread = 0; thread < threads; thread++) {
for (int index = 0; index < statesPerThread; index++) {
int x = thread * 2 + (index & 1);
int z = (index >> 1) & 15;
assertSame(states[thread][index], buffer.getStoredRaw(x, index, z));
assertSame(shared[index % shared.length], buffer.getRaw(x, index + 1, z));
}
}
assertEquals(1 + threads * statesPerThread + shared.length, buffer.palette().length);
for (NativeBlockState state : shared) {
assertSame(state, buffer.palette()[buffer.paletteId(state)]);
}
}
}
@Test
public void listeningHunksForwardRunsInTopDownOrder() {
SectionBuffer buffer = new SectionBuffer(32);
NativeBlockState stone = mock(NativeBlockState.class);
List<Integer> heard = new ArrayList<>();
ColumnExtentListeningHunk<NativeBlockState> listening = new ColumnExtentListeningHunk<>(buffer,
(x, y, z, state) -> heard.add(y));
ColumnRunHunk.fillColumnRun(listening, 4, 6, 10, 13, stone);
assertEquals(List.of(13, 12, 11, 10), heard);
for (int y = 10; y <= 13; y++) {
assertSame(stone, buffer.getStoredRaw(4, y, 6));
}
assertNull(buffer.getStoredRaw(4, 9, 6));
assertEquals(13, buffer.highestStoredY(4, 6));
}
@Test
public void plainHunksReceiveRunsAsTopDownSingleWrites() {
Hunk<NativeBlockState> array = Hunk.newArrayHunk(16, 32, 16);
NativeBlockState stone = mock(NativeBlockState.class);
array.setRaw(2, 5, 2, stone);
List<Integer> order = new ArrayList<>();
Hunk<NativeBlockState> recording = new ColumnExtentListeningHunk<>(array, (x, y, z, state) -> order.add(y));
ColumnRunHunk.fillColumnRun(Hunk.newArrayHunk(16, 32, 16), 0, 0, 0, 3, stone);
ColumnRunHunk.fillColumnRun(array, 2, 2, 4, 6, null);
ColumnRunHunk.fillColumnRun(recording, 1, 1, 7, 9, stone);
assertNull(array.getRaw(2, 5, 2));
assertSame(stone, array.getRaw(1, 8, 1));
assertEquals(List.of(9, 8, 7), order);
}
private static void assertSameContents(SectionBuffer expected, SectionBuffer actual) {
assertEquals(expected.sectionCount(), actual.sectionCount());
NativeBlockState[] expectedPalette = expected.palette();
NativeBlockState[] actualPalette = actual.palette();
for (int sectionIndex = 0; sectionIndex < expected.sectionCount(); sectionIndex++) {
short[] expectedSection = expected.section(sectionIndex);
short[] actualSection = actual.section(sectionIndex);
if (expectedSection == null) {
assertNull(actualSection);
continue;
}
NativeBlockState[] expectedStates = new NativeBlockState[4096];
NativeBlockState[] actualStates = new NativeBlockState[4096];
for (int index = 0; index < 4096; index++) {
expectedStates[index] = expectedPalette[expectedSection[index] & 0xFFFF];
actualStates[index] = actualPalette[actualSection[index] & 0xFFFF];
}
assertArrayEquals(expectedStates, actualStates);
}
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
assertEquals(expected.highestStoredY(x, z), actual.highestStoredY(x, z));
}
}
assertEquals(Arrays.stream(expectedPalette).filter(state -> state != null).distinct().count(),
Arrays.stream(actualPalette).filter(state -> state != null).distinct().count());
}
private static void runConcurrently(int threads, ThreadTask task) throws InterruptedException {
CountDownLatch start = new CountDownLatch(1);
List<Thread> workers = new ArrayList<>();
for (int thread = 0; thread < threads; thread++) {
int index = thread;
Thread worker = new Thread(() -> {
try {
start.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
task.run(index);
});
worker.start();
workers.add(worker);
}
start.countDown();
for (Thread worker : workers) {
worker.join();
}
}
@FunctionalInterface
private interface ThreadTask {
void run(int thread);
}
}
@@ -12,6 +12,7 @@ import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
@@ -60,6 +61,13 @@ public class HydrologyBurstTest {
assertTrue(pool.isTerminated());
}
@Test
public void plannerPoolFollowsThePerformanceCores() {
assertEquals(12, MultiBurst.HydrologyBurst.plannerParallelism(12));
assertEquals(4, MultiBurst.HydrologyBurst.plannerParallelism(1));
assertEquals(32, MultiBurst.HydrologyBurst.plannerParallelism(32));
}
@Test
public void closedHydrologyWorkersTerminateAndReopenCreatesANewPool() throws Exception {
IrisSettings previousSettings = IrisSettings.settings;
@@ -696,6 +696,29 @@ public class HydrologyTileCacheTest {
cache.close();
}
@Test
public void pregenSpeculationStaysWithinTheGenerationReachOfTheArea() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyPlannerSettings settings = stalePrefetchSettings();
when(planner.settings()).thenReturn(settings);
ArrayList<HydrologyTileKey> planned = new ArrayList<>();
doAnswer(invocation -> {
planned.add(invocation.getArgument(0));
return tile;
}).when(planner).plan(any(HydrologyTileKey.class));
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, Runnable::run);
try (HydrologyTileCache.PregenerationScope ignored = cache.preparePregeneration(
new HydrologyTileCache.PregenerationArea(1024, 1024, 736, 736, 1311, 1311))) {
assertEquals(prefetchRectangle(0, 1, 0, 1), new HashSet<>(planned));
cache.prepareChunkColumns(800, 800);
cache.prepareChunkColumns(1300, 1300);
assertEquals(prefetchRectangle(0, 1, 0, 1), new HashSet<>(planned));
assertEquals(4, planned.size());
}
cache.close();
}
@Test
public void pregenScopesRestoreStudioPrefetchAndIgnoreSupersededClose() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
@@ -0,0 +1,106 @@
package art.arcane.iris.generation.mantle;
import org.junit.Test;
import java.util.SplittableRandom;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class CaveCellKernelTest {
private static final int TRIALS = 200_000;
@Test
public void decidedCellsAgreeWithEveryInterpolatedColumn() {
SplittableRandom random = new SplittableRandom(91_337L);
long decided = 0L;
for (int trial = 0; trial < TRIALS; trial++) {
double scale = Math.scalb(1D, random.nextInt(-30, 4));
double base = (random.nextDouble() * 2D - 1D) * scale;
double spread = random.nextInt(4) == 0 ? 0D : random.nextDouble() * scale * Math.scalb(1D, -random.nextInt(0, 40));
double d00 = base + spread * (random.nextDouble() * 2D - 1D);
double d01 = random.nextInt(8) == 0 ? d00 : base + spread * (random.nextDouble() * 2D - 1D);
double d10 = base + spread * (random.nextDouble() * 2D - 1D);
double d11 = random.nextInt(8) == 0 ? d10 : base + spread * (random.nextDouble() * 2D - 1D);
double low = Math.min(Math.min(d00, d01), Math.min(d10, d11));
double high = Math.max(Math.max(d00, d01), Math.max(d10, d11));
int mode = random.nextInt(6);
double[] thresholds = new double[64];
for (int column = 0; column < 64; column++) {
thresholds[column] = pickThreshold(random, low, high, mode);
}
double thresholdLow = Double.POSITIVE_INFINITY;
double thresholdHigh = Double.NEGATIVE_INFINITY;
for (double threshold : thresholds) {
thresholdLow = Math.min(thresholdLow, threshold);
thresholdHigh = Math.max(thresholdHigh, threshold);
}
int decision = CaveCellKernel.decide(d00, d01, d10, d11, thresholdLow, thresholdHigh);
if (decision == CaveCellKernel.MIXED) {
continue;
}
decided++;
for (int column = 0; column < 64; column++) {
double tx = (column >> 3) / 8D;
double tz = (column & 7) / 8D;
boolean carved = bilinear(d00, d01, d10, d11, tx, tz) <= thresholds[column];
assertEquals("trial " + trial + " column " + column, decision == CaveCellKernel.ALL, carved);
}
}
assertTrue("decided " + decided, decided > TRIALS / 4);
}
@Test
public void thresholdsAtTheCornerExtremesStayMixed() {
double d00 = 0.1D;
double d01 = -0.2D;
double d10 = 0.3D;
double d11 = 0.05D;
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(d00, d01, d10, d11, 0.3D, 0.3D));
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(d00, d01, d10, d11, Math.nextUp(0.3D), 0.4D));
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(d00, d01, d10, d11, -0.3D, Math.nextDown(-0.2D)));
assertEquals(CaveCellKernel.ALL, CaveCellKernel.decide(d00, d01, d10, d11, 0.3D + 1.0E-12D, 0.5D));
assertEquals(CaveCellKernel.NONE, CaveCellKernel.decide(d00, d01, d10, d11, -0.9D, -0.2D - 1.0E-12D));
}
@Test
public void nonFiniteCornersAreNeverDecided() {
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(Double.NaN, 0D, 0D, 0D, 5D, 5D));
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(0D, Double.POSITIVE_INFINITY, 0D, 0D, -5D, -5D));
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(0D, 0D, Double.NEGATIVE_INFINITY, 0D,
Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY));
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(0D, 0D, 0D, 0D, Double.NaN, 1D));
}
@Test
public void monotoneThresholdMappingPreservesBounds() {
SplittableRandom random = new SplittableRandom(7_771L);
for (int trial = 0; trial < TRIALS; trial++) {
double normalization = 0.25D + random.nextDouble() * 3D;
double inverse = 1D / normalization;
double a = (random.nextDouble() * 2D - 1D) * Math.scalb(1D, random.nextInt(-20, 3));
double b = random.nextInt(4) == 0 ? Math.nextUp(a) : a + random.nextDouble() * Math.scalb(1D, random.nextInt(-50, 0));
assertTrue(CaveCellKernel.threshold(a, normalization, inverse) <= CaveCellKernel.threshold(b, normalization, inverse));
assertEquals(Double.doubleToRawLongBits((a * normalization) * inverse),
Double.doubleToRawLongBits(CaveCellKernel.threshold(a, normalization, inverse)));
}
}
private static double pickThreshold(SplittableRandom random, double low, double high, int mode) {
double width = Math.max(high - low, Math.ulp(Math.max(Math.abs(low), Math.abs(high))));
return switch (mode) {
case 0 -> high + Math.ulp(high) * random.nextInt(0, 64);
case 1 -> low - Math.ulp(low) * random.nextInt(0, 64);
case 2 -> high + width * random.nextDouble() * 0.5D;
case 3 -> low - width * random.nextDouble() * 0.5D;
case 4 -> high + width * (1D + random.nextDouble() * 4D);
default -> low - width * (1D + random.nextDouble() * 4D);
};
}
private static double bilinear(double d00, double d01, double d10, double d11, double tx, double tz) {
double dx0 = d00 + ((d10 - d00) * tx);
double dx1 = d01 + ((d11 - d01) * tx);
return dx0 + ((dx1 - dx0) * tz);
}
}
@@ -0,0 +1,414 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.cave.IrisCaveFieldModule;
import art.arcane.iris.generation.cave.IrisCaveProfile;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.IrisStyledRange;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EngineMetrics;
import art.arcane.iris.generation.runtime.SeedManager;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.iris.world.IrisWorld;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterSlice;
import art.arcane.volmlib.util.matter.slices.CavernMatter;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.withSettings;
public class IrisCaveCarver3DCellKernelTest {
private static final long CARVE_SEED = -54_777_863_784_492_918L;
private static final int WORLD_HEIGHT = 160;
private static final int CHUNK_SPAN = 4;
@Test
public void warpModulesProfileMatchesGoldenWriteSequence() throws Exception {
assertScenario(Scenario.WARP_MODULES, 0xdf617c77d3c3cc5eL, 67798);
}
@Test
public void warpOnlyTerrainProfileMatchesGoldenWriteSequence() throws Exception {
assertScenario(Scenario.WARP_TERRAIN, 0x7e0b63cab5bb3904L, 67036);
}
@Test
public void styledThresholdBlendProfileMatchesGoldenWriteSequence() throws Exception {
assertScenario(Scenario.STYLED_BLEND, 0x25792d49143774faL, 30387);
}
@Test
public void fluidBoundaryProfileMatchesGoldenWriteSequence() throws Exception {
assertScenario(Scenario.FLUID_BOUNDARY, 0xb102145856119afeL, 17584);
}
@Test
public void noWarpModulesProfileMatchesGoldenWriteSequence() throws Exception {
assertScenario(Scenario.NO_WARP_MODULES, 0x5a83dc20c89c6398L, 47534);
}
@Test
public void verticalStepProfileMatchesGoldenWriteSequence() throws Exception {
assertScenario(Scenario.VERTICAL_STEP, 0xac0413031ed65b61L, 48011);
}
@Test
public void latticeProfileMatchesGoldenWriteSequence() throws Exception {
assertScenario(Scenario.LATTICE, 0xc7ffab155e6bdef9L, 72714);
}
@Test
public void floorFluidProfilesSharingChunksMatchGoldenWriteSequence() throws Exception {
assertScenario(Scenario.FLOOR_FLUID_LAYERED, 0xb4c0fb23e071617dL, 68747);
}
@Test
public void packStyleProfilesSharingFieldsMatchGoldenWriteSequence() throws Exception {
assertScenario(Scenario.PACK_SHARED, 0xdbe20a1ca87f9cc7L, 269035);
}
@Test
public void cellKernelDecidesCellsWithoutColumnEvaluation() throws Exception {
IrisCaveCarver3D.CellCounters before = IrisCaveCarver3D.cellCounters();
run(Scenario.WARP_MODULES, false);
IrisCaveCarver3D.CellCounters after = IrisCaveCarver3D.cellCounters();
long decided = after.decided() - before.decided();
long evaluated = after.evaluated() - before.evaluated();
assertTrue("decided=" + decided + " evaluated=" + evaluated, decided > 0L && evaluated > 0L);
}
private void assertScenario(Scenario scenario, long expectedHash, int expectedWrites) throws Exception {
Result serial = run(scenario, false);
Result parallel = run(scenario, true);
assertTrue(serial.carved > 0);
assertEquals(serial.hash, parallel.hash);
assertEquals(serial.writes, parallel.writes);
assertEquals(serial.carved, parallel.carved);
assertEquals(expectedHash, serial.hash);
assertEquals(expectedWrites, serial.writes);
}
private Result run(Scenario scenario, boolean parallel) throws Exception {
if (!parallel) {
return carveArea(scenario);
}
ForkJoinPool pool = new ForkJoinPool(4);
try {
return pool.submit(() -> carveArea(scenario)).get(120, TimeUnit.SECONDS);
} finally {
pool.shutdownNow();
}
}
private Result carveArea(Scenario scenario) {
Engine engine = createEngine(scenario);
CaveDensityGrid grid = new CaveDensityGrid(4096);
IrisCaveProfile[] profiles = scenario.profiles();
IrisCaveCarver3D[] carvers = new IrisCaveCarver3D[profiles.length];
for (int index = 0; index < profiles.length; index++) {
carvers[index] = new IrisCaveCarver3D(engine, profiles[index], grid);
}
Capture capture = new Capture();
int carved = 0;
for (int chunkX = -2; chunkX < CHUNK_SPAN - 2; chunkX++) {
for (int chunkZ = -1; chunkZ < CHUNK_SPAN - 1; chunkZ++) {
MantleWriter writer = capture.writer(chunkX, chunkZ);
CaveFluidSupportPlan supportPlan = new CaveFluidSupportPlan();
for (int index = 0; index < carvers.length; index++) {
carved += carvers[index].carve(writer, chunkX, chunkZ, scenario.weights(index, chunkX, chunkZ),
0.08D, 0.24D, scenario.worldYRange(), scenario.surfaceHeights(chunkX, chunkZ),
scenario.boundaries(chunkX, chunkZ), null, supportPlan);
}
capture.resolving = true;
supportPlan.resolve(writer.acquireChunk(chunkX, chunkZ));
capture.finishResolve();
}
}
return new Result(capture.hash, capture.writes, carved);
}
private Engine createEngine(Scenario scenario) {
Engine engine = mock(Engine.class, withSettings().stubOnly());
IrisData data = mock(IrisData.class, withSettings().stubOnly());
IrisDimension dimension = mock(IrisDimension.class, withSettings().stubOnly());
SeedManager seedManager = mock(SeedManager.class, withSettings().stubOnly());
doReturn(data).when(engine).getData();
doReturn(dimension).when(engine).getDimension();
doReturn(seedManager).when(engine).getSeedManager();
doReturn(CARVE_SEED).when(seedManager).getCarve();
doReturn(new EngineMetrics(16)).when(engine).getMetrics();
doReturn(IrisWorld.builder().minHeight(-32).maxHeight(WORLD_HEIGHT - 32).build()).when(engine).getWorld();
doReturn(WORLD_HEIGHT).when(engine).getHeight();
doReturn(110).when(engine).getHeight(anyInt(), anyInt());
doReturn(24).when(dimension).getCaveLavaHeight();
doReturn(70).when(dimension).getFluidHeight();
doReturn(scenario == Scenario.VERTICAL_STEP ? 4 : 1).when(dimension).getCaveDensitySamplingStep();
return engine;
}
private static IrisCaveProfile baseProfile(boolean warp, boolean modules) {
IrisCaveProfile profile = new IrisCaveProfile();
profile.setEnabled(true);
profile.setVerticalRange(new IrisRange(4D, 150D));
profile.setVerticalEdgeFade(14);
profile.setVerticalEdgeFadeStrength(0.21D);
profile.setBaseDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.21D));
profile.setDetailDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.43D));
profile.setWarpStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.37D));
profile.setSurfaceBreakStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.140625D));
profile.setBaseWeight(1D);
profile.setDetailWeight(0.42D);
profile.setWarpStrength(warp ? 3.7D : 0D);
profile.setDensityThreshold(new IrisStyledRange(-0.18D, -0.18D, new IrisGeneratorStyle(NoiseStyle.FLAT)));
profile.setThresholdBias(0D);
profile.setSampleStep(1);
profile.setSurfaceClearance(5);
profile.setAllowSurfaceBreak(true);
profile.setSurfaceBreakNoiseThreshold(0.3D);
profile.setSurfaceBreakDepth(14);
profile.setSurfaceBreakThresholdBoost(0.21D);
profile.setAllowFluid(false);
profile.setAllowLava(true);
KList<IrisCaveFieldModule> caveModules = new KList<>();
if (modules) {
caveModules.add(new IrisCaveFieldModule(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.33D),
0.27D, 0.05D, new IrisRange(0D, 90D), false));
caveModules.add(new IrisCaveFieldModule(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.51D),
0.19D, -0.07D, new IrisRange(40D, 150D), true));
}
profile.setModules(caveModules);
return profile;
}
private static IrisCaveProfile packProfile(NoiseStyle warp, double warpZoom, double warpStrength, NoiseStyle base,
double baseZoom, double threshold, IrisCaveFieldModule... modules) {
IrisCaveProfile profile = baseProfile(true, false);
profile.setWarpStyle(new IrisGeneratorStyle(warp).zoomed(warpZoom));
profile.setWarpStrength(warpStrength);
profile.setBaseDensityStyle(new IrisGeneratorStyle(base).zoomed(baseZoom));
profile.setDetailDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.5D));
profile.setBaseWeight(0.82D);
profile.setDetailWeight(0.15D);
profile.setDensityThreshold(new IrisStyledRange(threshold - 0.06D, threshold + 0.05D,
new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.78D)));
profile.setThresholdBias(0.12D);
profile.setSampleStep(2);
profile.setModules(new KList<>(modules));
return profile;
}
private static IrisCaveFieldModule packModule(NoiseStyle style, double zoom, double weight, double threshold,
double minY, double maxY, boolean invert) {
return new IrisCaveFieldModule(new IrisGeneratorStyle(style).zoomed(zoom), weight, threshold,
new IrisRange(minY, maxY), invert);
}
private enum Scenario {
PACK_SHARED,
WARP_MODULES,
WARP_TERRAIN,
STYLED_BLEND,
FLUID_BOUNDARY,
NO_WARP_MODULES,
VERTICAL_STEP,
LATTICE,
FLOOR_FLUID_LAYERED;
IrisCaveProfile[] profiles() {
if (this == PACK_SHARED) {
return new IrisCaveProfile[]{
packProfile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D, NoiseStyle.PERLIN_IRIS, 0.72D, -0.02D,
packModule(NoiseStyle.FRACTAL_FBM_SIMPLEX, 0.24D, 0.08D, 0.03D, 10D, 140D, false),
packModule(NoiseStyle.SIMPLEX_VASCULAR, 1.02D, 0.05D, 0.07D, 4D, 90D, true)),
packProfile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D, NoiseStyle.FRACTAL_BILLOW_SIMPLEX, 0.656D, 0.01D,
packModule(NoiseStyle.CELLULAR_BILINEAR, 1.32D, 0.06D, 0.02D, 20D, 150D, false)),
packProfile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D, NoiseStyle.PERLIN_IRIS, 0.72D, 0.04D,
packModule(NoiseStyle.FRACTAL_FBM_SIMPLEX, 0.24D, 0.08D, 0.03D, 10D, 140D, false),
packModule(NoiseStyle.SIMPLEX_VASCULAR, 1.02D, 0.05D, 0.07D, 4D, 90D, true)),
packProfile(NoiseStyle.PERLIN_IRIS, 0.35D, 2.4D, NoiseStyle.IRIS_DOUBLE, 0.68D, -0.04D,
packModule(NoiseStyle.VASCULAR_THIN, 1.28D, 0.04D, 0.05D, 8D, 120D, false))
};
}
if (this == FLOOR_FLUID_LAYERED) {
return new IrisCaveProfile[]{
baseProfile(true, true).setAllowFluid(true).setFluidRequiresFloor(true).setFluidMinDepthBelowSurface(4),
baseProfile(true, false).setAllowFluid(true).setFluidRequiresFloor(true).setFluidMinDepthBelowSurface(2)
.setAllowLava(false).setDensityThreshold(new IrisStyledRange(-0.24D, -0.08D,
new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.45D)))
};
}
return new IrisCaveProfile[]{profile()};
}
IrisCaveProfile profile() {
return switch (this) {
case WARP_MODULES, VERTICAL_STEP -> baseProfile(true, true);
case WARP_TERRAIN -> baseProfile(true, false);
case STYLED_BLEND -> baseProfile(false, false).setDensityThreshold(new IrisStyledRange(-0.32D, -0.04D,
new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.6D)));
case FLUID_BOUNDARY -> baseProfile(true, true).setAllowFluid(true).setFluidRequiresFloor(false)
.setFluidMinDepthBelowSurface(6).setAllowLava(false);
case NO_WARP_MODULES -> baseProfile(false, true);
case LATTICE -> baseProfile(true, true).setSampleStep(3);
case FLOOR_FLUID_LAYERED, PACK_SHARED -> baseProfile(true, true);
};
}
double[] weights(int layer, int chunkX, int chunkZ) {
double[] weights = new double[256];
for (int index = 0; index < 256; index++) {
int worldX = (chunkX << 4) + (index >> 4);
int worldZ = (chunkZ << 4) + (index & 15);
double blend = 0.5D + 0.6D * Math.sin(worldX * 0.11D) * Math.cos(worldZ * 0.07D);
weights[index] = switch (this) {
case PACK_SHARED -> 0.5D + 0.6D * Math.sin(worldX * 0.13D + layer * 1.7D) * Math.cos(worldZ * 0.09D - layer);
case STYLED_BLEND, FLUID_BOUNDARY -> blend;
case FLOOR_FLUID_LAYERED -> layer == 0 ? blend : 1D - blend;
default -> 1D;
};
}
return weights;
}
int[] surfaceHeights(int chunkX, int chunkZ) {
int[] heights = new int[256];
for (int index = 0; index < 256; index++) {
int worldX = (chunkX << 4) + (index >> 4);
int worldZ = (chunkZ << 4) + (index & 15);
heights[index] = this == WARP_MODULES || this == LATTICE || this == FLOOR_FLUID_LAYERED
? 118
: 96 + (int) Math.round(22D * Math.sin(worldX * 0.09D) + 15D * Math.cos(worldZ * 0.13D));
}
return heights;
}
IrisRange worldYRange() {
return this == FLUID_BOUNDARY || this == FLOOR_FLUID_LAYERED ? new IrisRange(-20D, 100D) : null;
}
long[] boundaries(int chunkX, int chunkZ) {
long[] boundaries = new long[256];
Arrays.fill(boundaries, SurfaceFluidBoundaryPlan.boundary(SurfaceFluidBoundaryPlan.NO_BOUNDARY, Integer.MIN_VALUE));
if (this != FLUID_BOUNDARY && this != WARP_TERRAIN) {
return boundaries;
}
for (int index = 0; index < 256; index++) {
int worldX = (chunkX << 4) + (index >> 4);
if (Math.floorMod(worldX + chunkZ, 5) == 0) {
boundaries[index] = SurfaceFluidBoundaryPlan.boundary(60 + Math.floorMod(index, 7), 82);
}
}
return boundaries;
}
}
private record Result(long hash, int writes, int carved) {
}
private static final class Capture {
private final Map<Long, Map<Integer, Matter>> chunks = new HashMap<>();
private final IdentityHashMap<MatterCavern, Integer> identities = new IdentityHashMap<>();
private final List<Long> resolved = new ArrayList<>();
private long hash = 0xcbf29ce484222325L;
private int writes;
private boolean resolving;
private MantleWriter writer(int chunkX, int chunkZ) {
MantleWriter writer = mock(MantleWriter.class, withSettings().stubOnly());
@SuppressWarnings("unchecked")
Mantle<Matter> mantle = mock(Mantle.class, withSettings().stubOnly());
@SuppressWarnings("unchecked")
MantleChunk<Matter> chunk = mock(MantleChunk.class, withSettings().stubOnly());
long key = ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL);
Map<Integer, Matter> sections = chunks.computeIfAbsent(key, ignored -> new HashMap<>());
doReturn(mantle).when(writer).getMantle();
doReturn(WORLD_HEIGHT).when(mantle).getWorldHeight();
doReturn(chunk).when(writer).acquireChunk(anyInt(), anyInt());
doAnswer(invocation -> sections.get(invocation.getArgument(0, Integer.class))).when(chunk).get(anyInt());
doAnswer(invocation -> sections.computeIfAbsent(invocation.getArgument(0, Integer.class),
sectionIndex -> createSection(chunkX, chunkZ, sectionIndex))).when(chunk).getOrCreate(anyInt());
return writer;
}
private Matter createSection(int chunkX, int chunkZ, int sectionIndex) {
mix(0x5EC7L);
mix(chunkX);
mix(chunkZ);
mix(sectionIndex);
Matter matter = mock(Matter.class, withSettings().stubOnly());
MatterSlice<MatterCavern> slice = new RecordingCavernMatter(this, sectionIndex);
doReturn(slice).when(matter).slice(MatterCavern.class);
doReturn(slice).when(matter).getSlice(MatterCavern.class);
return matter;
}
private void record(int sectionIndex, int x, int y, int z, MatterCavern value) {
writes++;
if (resolving) {
resolved.add(((long) sectionIndex << 20) | ((long) x << 16) | ((long) y << 12) | ((long) z << 8) | value.getLiquid());
return;
}
mix(sectionIndex);
mix((x << 8) | (y << 4) | z);
mix(value == null ? -1 : value.getLiquid());
Integer identity = identities.get(value);
if (identity == null) {
identity = identities.size();
identities.put(value, identity);
}
mix(identity);
}
private void finishResolve() {
resolving = false;
resolved.sort(Long::compare);
for (long entry : resolved) {
mix(entry);
}
resolved.clear();
}
private void mix(long value) {
hash ^= value;
hash *= 0x100000001b3L;
}
}
private static final class RecordingCavernMatter extends CavernMatter {
private final Capture capture;
private final int sectionIndex;
private RecordingCavernMatter(Capture capture, int sectionIndex) {
super(16, 16, 16);
this.capture = capture;
this.sectionIndex = sectionIndex;
}
@Override
public void set(int x, int y, int z, MatterCavern value) {
super.set(x, y, z, value);
capture.record(sectionIndex, x, y, z, value);
}
}
}
@@ -0,0 +1,163 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.cave.IrisCaveFieldModule;
import art.arcane.iris.generation.cave.IrisCaveProfile;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.IrisStyledRange;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EngineMetrics;
import art.arcane.iris.generation.runtime.SeedManager;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.iris.world.IrisWorld;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Random;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.withSettings;
public class IrisCaveCarver3DLatticeTest {
private static final long CARVE_SEED = 7_340_119_802_557L;
private static final int WORLD_HEIGHT = 384;
@Test
public void batchedLatticeColumnsMatchScalarPlaneDensityBitForBit() throws Exception {
Engine engine = createEngine();
CaveDensityGrid grid = new CaveDensityGrid(256);
IrisCaveProfile[] profiles = {
profile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D, NoiseStyle.PERLIN_IRIS, 0.72D,
module(NoiseStyle.FRACTAL_FBM_SIMPLEX, 0.24D, 10D, 140D, false),
module(NoiseStyle.CELLULAR_BILINEAR, 1.32D, 4D, 300D, true)),
profile(NoiseStyle.PERLIN_IRIS, 0.35D, 2.4D, NoiseStyle.IRIS_DOUBLE, 0.68D,
module(NoiseStyle.VASCULAR_THIN, 1.28D, 8D, 220D, false)),
profile(NoiseStyle.SIMPLEX, 0.5D, 0D, NoiseStyle.FRACTAL_BILLOW_SIMPLEX, 0.656D)
};
Method sampleLatticeColumn = IrisCaveCarver3D.class.getDeclaredMethod("sampleLatticeColumn",
CaveCarveScratch.class, CaveDensityGrid.Column.class, int.class, int.class, int.class);
sampleLatticeColumn.setAccessible(true);
Method samplePlaneDensity = IrisCaveCarver3D.class.getDeclaredMethod("samplePlaneDensity",
CaveCarveScratch.class, int.class, int.class, int.class);
samplePlaneDensity.setAccessible(true);
Field warpOwnerField = IrisCaveCarver3D.class.getDeclaredField("warpOwner");
warpOwnerField.setAccessible(true);
Random random = new Random(91_337L);
for (IrisCaveProfile profile : profiles) {
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile, grid);
CaveDensityGrid.Owner warpOwner = (CaveDensityGrid.Owner) warpOwnerField.get(carver);
CaveCarveScratch batch = new CaveCarveScratch();
CaveCarveScratch reference = new CaveCarveScratch();
for (int trial = 0; trial < 48; trial++) {
int latticeX = random.nextInt(4001) - 2000;
int latticeZ = random.nextInt(4001) - 2000;
int x = latticeX << CaveDensityGrid.LATTICE_SHIFT;
int z = latticeZ << CaveDensityGrid.LATTICE_SHIFT;
int count = selectRows(random, batch);
CaveDensityGrid.Column warpColumn = trial % 3 == 0 ? null
: grid.warpColumn(warpOwner, WORLD_HEIGHT + 1, latticeX, latticeZ);
for (int pass = 0; pass < 2; pass++) {
sampleLatticeColumn.invoke(carver, batch, warpColumn, x, z, count);
for (int index = 0; index < count; index++) {
int y = batch.latticeY[index];
double expected = (double) samplePlaneDensity.invoke(carver, reference, x, y, z);
assertEquals("x=" + x + " y=" + y + " z=" + z + " pass=" + pass,
Double.doubleToRawLongBits(expected), Double.doubleToRawLongBits(batch.latticeDensity[index]));
}
}
}
}
}
@Test
public void identicalFieldsShareGridOwnersAcrossProfiles() throws Exception {
Engine engine = createEngine();
CaveDensityGrid grid = new CaveDensityGrid(256);
Field densityOwnerField = IrisCaveCarver3D.class.getDeclaredField("densityOwner");
densityOwnerField.setAccessible(true);
Field warpOwnerField = IrisCaveCarver3D.class.getDeclaredField("warpOwner");
warpOwnerField.setAccessible(true);
IrisCaveCarver3D first = new IrisCaveCarver3D(engine, profile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D,
NoiseStyle.PERLIN_IRIS, 0.72D, module(NoiseStyle.SIMPLEX_VASCULAR, 1.02D, 4D, 90D, true)), grid);
IrisCaveCarver3D sameDensity = new IrisCaveCarver3D(engine, profile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D,
NoiseStyle.PERLIN_IRIS, 0.72D, module(NoiseStyle.SIMPLEX_VASCULAR, 1.02D, 4D, 90D, true))
.setDensityThreshold(new IrisStyledRange(0.2D, 0.2D, new IrisGeneratorStyle(NoiseStyle.FLAT))), grid);
IrisCaveCarver3D sameWarp = new IrisCaveCarver3D(engine, profile(NoiseStyle.FRACTAL_WATER, 0.44D, 1.7D,
NoiseStyle.FRACTAL_BILLOW_SIMPLEX, 0.656D), grid);
IrisCaveCarver3D otherWarp = new IrisCaveCarver3D(engine, profile(NoiseStyle.FRACTAL_WATER, 0.46D, 3.2D,
NoiseStyle.PERLIN_IRIS, 0.72D, module(NoiseStyle.SIMPLEX_VASCULAR, 1.02D, 4D, 90D, true)), grid);
IrisCaveCarver3D otherModule = new IrisCaveCarver3D(engine, profile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D,
NoiseStyle.PERLIN_IRIS, 0.72D, module(NoiseStyle.SIMPLEX_VASCULAR, 1.02D, 4D, 91D, true)), grid);
assertSame(densityOwnerField.get(first), densityOwnerField.get(sameDensity));
assertSame(warpOwnerField.get(first), warpOwnerField.get(sameDensity));
assertSame(warpOwnerField.get(first), warpOwnerField.get(sameWarp));
assertSame(warpOwnerField.get(first), warpOwnerField.get(otherModule));
assertNotSame(densityOwnerField.get(first), densityOwnerField.get(sameWarp));
assertNotSame(densityOwnerField.get(first), densityOwnerField.get(otherModule));
assertNotSame(densityOwnerField.get(first), densityOwnerField.get(otherWarp));
assertNotSame(warpOwnerField.get(first), warpOwnerField.get(otherWarp));
}
private static int selectRows(Random random, CaveCarveScratch scratch) {
int start = random.nextInt(40);
int end = start + 20 + random.nextInt(WORLD_HEIGHT - start - 20);
scratch.ensureLatticeBatch(end - start + 1);
int count = 0;
for (int y = start; y <= end; y++) {
if (random.nextInt(5) != 0) {
scratch.latticeY[count++] = y;
}
}
return count;
}
private static IrisCaveProfile profile(NoiseStyle warp, double warpZoom, double warpStrength, NoiseStyle base,
double baseZoom, IrisCaveFieldModule... modules) {
IrisCaveProfile profile = new IrisCaveProfile();
profile.setEnabled(true);
profile.setVerticalRange(new IrisRange(4D, 360D));
profile.setWarpStyle(new IrisGeneratorStyle(warp).zoomed(warpZoom));
profile.setWarpStrength(warpStrength);
profile.setBaseDensityStyle(new IrisGeneratorStyle(base).zoomed(baseZoom));
profile.setDetailDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.5D));
profile.setSurfaceBreakStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.82D));
profile.setBaseWeight(0.82D);
profile.setDetailWeight(0.15D);
profile.setDensityThreshold(new IrisStyledRange(-0.1D, 0.02D, new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.78D)));
profile.setSampleStep(2);
profile.setModules(new KList<>(modules));
return profile;
}
private static IrisCaveFieldModule module(NoiseStyle style, double zoom, double minY, double maxY, boolean invert) {
return new IrisCaveFieldModule(new IrisGeneratorStyle(style).zoomed(zoom), 0.07D, 0.04D, new IrisRange(minY, maxY), invert);
}
private static Engine createEngine() {
Engine engine = mock(Engine.class, withSettings().stubOnly());
IrisData data = mock(IrisData.class, withSettings().stubOnly());
IrisDimension dimension = mock(IrisDimension.class, withSettings().stubOnly());
SeedManager seedManager = mock(SeedManager.class, withSettings().stubOnly());
doReturn(data).when(engine).getData();
doReturn(dimension).when(engine).getDimension();
doReturn(seedManager).when(engine).getSeedManager();
doReturn(CARVE_SEED).when(seedManager).getCarve();
doReturn(new EngineMetrics(16)).when(engine).getMetrics();
doReturn(IrisWorld.builder().minHeight(-64).maxHeight(WORLD_HEIGHT - 64).build()).when(engine).getWorld();
doReturn(WORLD_HEIGHT).when(engine).getHeight();
doReturn(200).when(engine).getHeight(anyInt(), anyInt());
doReturn(20).when(dimension).getCaveLavaHeight();
doReturn(64).when(dimension).getFluidHeight();
doReturn(1).when(dimension).getCaveDensitySamplingStep();
return engine;
}
}
@@ -13,6 +13,7 @@ import art.arcane.iris.generation.noise.IrisStyledRange;
import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.volmlib.util.noise.CNG;
import art.arcane.volmlib.util.noise.program.NoiseProgram;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
@@ -148,7 +149,7 @@ public class IrisCaveCarver3DNearParityTest {
sampledY.add((int) y);
return x * 0.0001D + y * 0.01D + z * 0.0002D;
}).when(density).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
baseDensityField.set(carver, density);
baseDensityField.set(carver, NoiseProgram.compileSigned3D(density));
Method prepare = IrisCaveCarver3D.class.getDeclaredMethod("prepareVerticalDensityPlane",
CaveCarveScratch.class, int.class, int.class, int.class);
prepare.setAccessible(true);
@@ -168,7 +169,7 @@ public class IrisCaveCarver3DNearParityTest {
doReturn(1).when(dimension).getCaveDensitySamplingStep();
IrisCaveCarver3D scalar = new IrisCaveCarver3D(engine,
createProfile(false, false).setDetailWeight(0D).setAllowFluid(false));
baseDensityField.set(scalar, density);
baseDensityField.set(scalar, NoiseProgram.compileSigned3D(density));
CaveCarveScratch scalarScratch = new CaveCarveScratch();
for (int index = 0; index < 256; index++) {
scalarScratch.planeColumnIndices[index] = index;
@@ -292,10 +293,10 @@ public class IrisCaveCarver3DNearParityTest {
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile);
CNG base = mock(CNG.class, withSettings().stubOnly());
doReturn(1D).when(base).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
baseDensityField.set(carver, base);
baseDensityField.set(carver, NoiseProgram.compileSigned3D(base));
CNG module = mock(CNG.class, withSettings().stubOnly());
doReturn(-1D).when(module).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
moduleDensityField.set(((CaveFieldModuleState[]) modulesField.get(carver))[0], module);
moduleDensityField.set(((CaveFieldModuleState[]) modulesField.get(carver))[0], NoiseProgram.compileSigned3D(module));
WriterCapture capture = createWriterCapture(128);
carver.carve(capture.writer, -1, 2, fullWeights(), 0D, 0D, null, filledHeights(110));
assertEquals(512, capture.carvedCells.size());
@@ -364,13 +365,13 @@ public class IrisCaveCarver3DNearParityTest {
}
return -1D;
}).when(density).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
baseDensityField.set(carver, density);
baseDensityField.set(carver, NoiseProgram.compileSigned3D(density));
assertThrows(IllegalStateException.class, () -> carver.carve(capture.writer, 0, 0,
fullWeights(), 0D, 0D, null, filledHeights(110)));
} else {
carver.carve(capture.writer, 0, 0, fullWeights(), 0D, 0D, null, filledHeights(110));
assertFalse(capture.carvedCells.isEmpty());
}
assertFalse(capture.carvedCells.isEmpty());
Field scratchField = IrisCaveCarver3D.class.getDeclaredField("scratchCache");
scratchField.setAccessible(true);
ThreadLocal<?> scratchCache = (ThreadLocal<?>) scratchField.get(carver);
@@ -385,6 +386,12 @@ public class IrisCaveCarver3DNearParityTest {
for (CaveDensityGrid.Column column : scratch.densityColumns) {
assertNull(column);
}
for (CaveDensityGrid.Column column : scratch.cellLattice.densityColumns) {
assertNull(column);
}
for (CaveDensityGrid.Column column : scratch.cellLattice.warpColumns) {
assertNull(column);
}
}
}
@@ -439,7 +446,7 @@ public class IrisCaveCarver3DNearParityTest {
}).when(detail).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
Field detailField = IrisCaveCarver3D.class.getDeclaredField("detailDensity");
detailField.setAccessible(true);
detailField.set(carver, detail);
detailField.set(carver, NoiseProgram.compileSigned3D(detail));
WriterCapture capture = createWriterCapture(128);
try (ForkJoinPool pool = new ForkJoinPool(4)) {
assertEquals(0, pool.submit(() -> carver.carve(capture.writer, 0, 0, fullWeights(),
@@ -772,7 +779,7 @@ public class IrisCaveCarver3DNearParityTest {
rejectedDensity.put("11:10:10", -1D);
rejectedDensity.put("9:10:10", -1D);
CoordinateDensityCNG rejectedNoise = new CoordinateDensityCNG(rejectedDensity);
baseDensityField.set(carver, rejectedNoise);
baseDensityField.set(carver, NoiseProgram.compileSigned3D(rejectedNoise));
boolean rejected = (boolean) aquiferCupSupportMethod.invoke(
carver, new CaveCarveScratch(), 10, 10, 10, 0D);
@@ -781,7 +788,7 @@ public class IrisCaveCarver3DNearParityTest {
assertEquals(List.of("10:9:10", "10:8:10", "11:10:10", "9:10:10"), rejectedNoise.sampledCoordinates);
CoordinateDensityCNG acceptedNoise = new CoordinateDensityCNG(Map.of());
baseDensityField.set(carver, acceptedNoise);
baseDensityField.set(carver, NoiseProgram.compileSigned3D(acceptedNoise));
boolean accepted = (boolean) aquiferCupSupportMethod.invoke(
carver, new CaveCarveScratch(), 10, 10, 10, 0D);
@@ -1100,7 +1107,7 @@ public class IrisCaveCarver3DNearParityTest {
AtomicLong isolatedSamples = new AtomicLong();
IrisCaveCarver3D shared = new IrisCaveCarver3D(engine,
createProfile(warp, modules).setAdaptiveSampling(true), new CaveDensityGrid(4096));
baseDensityField.set(shared, new CountingCNG((CNG) baseDensityField.get(shared), sharedSamples));
baseDensityField.set(shared, NoiseProgram.compileSigned3D(new CountingCNG((NoiseProgram) baseDensityField.get(shared), sharedSamples)));
for (int chunkX = -1; chunkX <= 1; chunkX++) {
for (int chunkZ = -1; chunkZ <= 1; chunkZ++) {
WriterCapture sharedCapture = createWriterCapture(96);
@@ -1108,7 +1115,7 @@ public class IrisCaveCarver3DNearParityTest {
null, filledHeights(90));
IrisCaveCarver3D isolated = new IrisCaveCarver3D(engine,
createProfile(warp, modules).setAdaptiveSampling(true));
baseDensityField.set(isolated, new CountingCNG((CNG) baseDensityField.get(isolated), isolatedSamples));
baseDensityField.set(isolated, NoiseProgram.compileSigned3D(new CountingCNG((NoiseProgram) baseDensityField.get(isolated), isolatedSamples)));
WriterCapture isolatedCapture = createWriterCapture(96);
int isolatedCarved = isolated.carve(isolatedCapture.writer, chunkX, chunkZ, fullWeights(), 0D, 0D,
null, filledHeights(90));
@@ -1241,7 +1248,7 @@ public class IrisCaveCarver3DNearParityTest {
new IrisCaveCarver3D(engine, profile).carve(expected.writer, -1, 2,
fullWeights(), 0D, 0D, null, filledHeights(110));
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile);
CNG original = (CNG) baseDensityField.get(carver);
NoiseProgram original = (NoiseProgram) baseDensityField.get(carver);
CNG density = mock(CNG.class, withSettings().stubOnly());
AtomicBoolean entered = new AtomicBoolean();
WriterCapture nested = createWriterCapture(128);
@@ -1252,9 +1259,9 @@ public class IrisCaveCarver3DNearParityTest {
double x = invocation.getArgument(0);
double y = invocation.getArgument(1);
double z = invocation.getArgument(2);
return original.noiseFastSigned3D(x, y, z);
return original.sampleSigned3D(x, y, z);
}).when(density).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
baseDensityField.set(carver, density);
baseDensityField.set(carver, NoiseProgram.compileSigned3D(density));
WriterCapture actual = createWriterCapture(128);
carver.carve(actual.writer, -1, 2, fullWeights(), 0D, 0D, null, filledHeights(110));
assertTrue(entered.get());
@@ -1268,7 +1275,7 @@ public class IrisCaveCarver3DNearParityTest {
doAnswer(invocation -> {
lookups.incrementAndGet();
return invocation.callRealMethod();
}).when(grid).column(any(), anyInt(), anyInt(), anyInt(), anyInt());
}).when(grid).column(any(), anyInt(), anyInt(), anyInt());
return grid;
}
@@ -1292,10 +1299,10 @@ public class IrisCaveCarver3DNearParityTest {
}
private void countSkippableSamples(IrisCaveCarver3D carver, AtomicLong samples) throws Exception {
detailDensityField.set(carver, new CountingCNG((CNG) detailDensityField.get(carver), samples));
detailDensityField.set(carver, NoiseProgram.compileSigned3D(new CountingCNG((NoiseProgram) detailDensityField.get(carver), samples)));
CaveFieldModuleState[] modules = (CaveFieldModuleState[]) modulesField.get(carver);
for (CaveFieldModuleState module : modules) {
moduleDensityField.set(module, new CountingCNG((CNG) moduleDensityField.get(module), samples));
moduleDensityField.set(module, NoiseProgram.compileSigned3D(new CountingCNG((NoiseProgram) moduleDensityField.get(module), samples)));
}
}
@@ -1893,10 +1900,10 @@ public class IrisCaveCarver3DNearParityTest {
}
private static final class CountingCNG extends CNG {
private final CNG delegate;
private final NoiseProgram delegate;
private final AtomicLong samples;
private CountingCNG(CNG delegate, AtomicLong samples) {
private CountingCNG(NoiseProgram delegate, AtomicLong samples) {
super(new RNG(41_117L));
this.delegate = delegate;
this.samples = samples;
@@ -1905,7 +1912,7 @@ public class IrisCaveCarver3DNearParityTest {
@Override
public double noiseFastSigned3D(double x, double y, double z) {
samples.incrementAndGet();
return delegate.noiseFastSigned3D(x, y, z);
return delegate.sampleSigned3D(x, y, z);
}
}
@@ -31,11 +31,6 @@ import art.arcane.iris.testsupport.PlatformBinding;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
@@ -45,22 +40,12 @@ public class MantleFloatingObjectComponentBoundaryRadiusTest {
@Rule
public final PlatformBinding platform = PlatformBinding.mockPlatform();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
@SuppressWarnings("unchecked")
public void transformedFloatingPlacementExpandsOwnerRadius() throws Exception {
File objectFile = temporaryFolder.newFile("floating.iob");
try (DataOutputStream output = new DataOutputStream(new FileOutputStream(objectFile))) {
output.writeInt(1);
output.writeInt(1);
output.writeInt(1);
}
public void transformedFloatingPlacementExpandsOwnerRadius() {
IrisObjectPlacement placement = new IrisObjectPlacement()
.setPlace(new KList<>("test/floating"))
.setTranslate(new IrisObjectTranslate().setX(32));
.setTranslate(new IrisObjectTranslate().setX(-32));
IrisFloatingChildBiomes floatingChild = new IrisFloatingChildBiomes()
.setFloatingObjects(new KList<>(placement));
IrisBiome biome = new IrisBiome().setFloatingChildBiomes(new KList<>(floatingChild));
@@ -69,7 +54,7 @@ public class MantleFloatingObjectComponentBoundaryRadiusTest {
when(dimension.getReachableBiomes(org.mockito.ArgumentMatchers.any())).thenReturn(new KList<>(biome));
ResourceLoader<IrisObject> objectLoader = mock(ResourceLoader.class);
when(objectLoader.findFile("test/floating")).thenReturn(objectFile);
when(objectLoader.load("test/floating")).thenReturn(new IrisObject(1, 1, 1));
IrisData data = mock(IrisData.class);
when(data.getObjectLoader()).thenReturn(objectLoader);
@@ -79,8 +64,8 @@ public class MantleFloatingObjectComponentBoundaryRadiusTest {
when(engineMantle.getEngine()).thenReturn(engine);
when(engineMantle.getData()).thenReturn(data);
assertEquals(33, new MantleFloatingObjectComponent(engineMantle).getRadius());
assertEquals(32, new MantleFloatingObjectComponent(engineMantle).getRadius());
when(dimension.getAllObjectScaleFactor()).thenReturn(2D);
assertEquals(34, new MantleFloatingObjectComponent(engineMantle).getRadius());
assertEquals(33, new MantleFloatingObjectComponent(engineMantle).getRadius());
}
}
@@ -24,7 +24,6 @@ import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
@@ -33,13 +32,8 @@ import art.arcane.volmlib.util.matter.Matter;
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 java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@@ -60,9 +54,6 @@ public class MantleObjectComponentBoundaryRadiusTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Before
public void bindPlatform() {
IrisPlatforms.unbind();
@@ -92,7 +83,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
.setRotation(IrisObjectRotation.of(90, 0, 0))
.setTranslate(new IrisObjectTranslate().setX(3).setY(4));
assertEquals(53, MantleObjectComponent.calculatePlacementReach(new IrisBlockVector(5, 48, 7), placement, 1D));
assertEquals(27, PlacementReach.of(new IrisObject(5, 48, 7), placement, 1D).horizontal());
}
@Test
@@ -103,7 +94,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
.setMode(ObjectPlaceMode.VACUUM)
.setVacuumSettings(new IrisVacuumSettings().setRadius(12));
assertEquals(39, MantleObjectComponent.calculatePlacementReach(new IrisBlockVector(5, 4, 3), placement, placement.getMaximumScale(null)));
assertEquals(22, PlacementReach.of(new IrisObject(5, 4, 3), placement, placement.getMaximumScale(null)).horizontal());
}
@Test
@@ -126,15 +117,12 @@ public class MantleObjectComponentBoundaryRadiusTest {
public void inheritedRadiusMatchesScaledFrameAndExplicitScaleOverridesIt() {
IrisDimension dimension = new IrisDimension().setAllObjectScaleFactor(2D);
IrisObjectPlacement inherited = new IrisObjectPlacement();
IrisBlockVector size = new IrisBlockVector(8, 8, 8);
assertEquals(16, MantleObjectComponent.calculatePlacementReach(size, inherited,
inherited.getMaximumScale(dimension)));
IrisObject object = new IrisObject(8, 8, 8);
assertEquals(8, PlacementReach.of(object, inherited, inherited.getMaximumScale(dimension)).horizontal());
dimension.setAllObjectScaleFactor(0.5D);
assertEquals(4, MantleObjectComponent.calculatePlacementReach(size, inherited,
inherited.getMaximumScale(dimension)));
assertEquals(2, PlacementReach.of(object, inherited, inherited.getMaximumScale(dimension)).horizontal());
inherited.setScale(new IrisObjectScale().setSize(1D));
assertEquals(8, MantleObjectComponent.calculatePlacementReach(size, inherited,
inherited.getMaximumScale(dimension)));
assertEquals(4, PlacementReach.of(object, inherited, inherited.getMaximumScale(dimension)).horizontal());
}
@Test
@@ -183,10 +171,11 @@ public class MantleObjectComponentBoundaryRadiusTest {
@Test
public void collisionInputRadiusCoversBothSourceTraversalLegs() {
assertEquals(82, MantleObjectComponent.calculateInputRadius(33, false));
assertEquals(130, MantleObjectComponent.calculateInputRadius(33, true));
assertEquals(84, MantleObjectComponent.calculateInputRadius(33, 35, false));
assertEquals(132, MantleObjectComponent.calculateInputRadius(33, 35, true));
assertEquals(82, MantleObjectComponent.calculateInputRadius(33, 0, false));
assertEquals(Integer.MAX_VALUE,
MantleObjectComponent.calculateInputRadius(Integer.MAX_VALUE, true));
MantleObjectComponent.calculateInputRadius(Integer.MAX_VALUE, Integer.MAX_VALUE, true));
}
@Test
@@ -199,7 +188,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
for (int radius = 0; radius < 300; radius++) {
for (boolean collisions : new boolean[]{false, true}) {
assertTrue(MantleObjectComponent.calculateEagerInputRadius(radius, collisions)
<= MantleObjectComponent.calculateInputRadius(radius, collisions));
<= MantleObjectComponent.calculateInputRadius(radius, radius, collisions));
}
}
assertEquals(Integer.MAX_VALUE,
@@ -231,27 +220,25 @@ public class MantleObjectComponentBoundaryRadiusTest {
when(engineMantle.getEngine()).thenReturn(engine);
when(engineMantle.getData()).thenReturn(data);
assertEquals(53, new MantleObjectComponent(engineMantle).getRadius());
assertEquals(27, new MantleObjectComponent(engineMantle).getRadius());
}
@Test
@SuppressWarnings("unchecked")
public void upperDimensionObjectsDriveRadiusAndCollisionMode() {
IrisObjectPlacement placement = new IrisObjectPlacement()
.setPlace(new KList<>("upper/tree"))
.setRotation(IrisObjectRotation.of(90, 0, 0))
.setTranslate(new IrisObjectTranslate().setX(3).setY(4));
placement.getForbiddenCollisions().add("upper/blocker");
IrisData baseData = mock(IrisData.class);
IrisData upperData = mock(IrisData.class);
IrisProceduralPlacement proceduralPlacement = mock(IrisProceduralPlacement.class);
when(proceduralPlacement.getVariantObjects(upperData))
.thenReturn(new KList<>(new IrisObject(5, 48, 7)));
when(proceduralPlacement.asPlacement()).thenReturn(placement);
IrisProceduralObjects proceduralObjects = mock(IrisProceduralObjects.class);
when(proceduralObjects.isEmpty()).thenReturn(false);
when(proceduralObjects.getAllPlacements()).thenReturn(new KList<>(proceduralPlacement));
ResourceLoader<IrisObject> upperObjects = mock(ResourceLoader.class);
when(upperObjects.load("upper/tree")).thenReturn(new IrisObject(5, 48, 7));
when(upperData.getObjectLoader()).thenReturn(upperObjects);
IrisRegion upperRegion = mock(IrisRegion.class);
when(upperRegion.getObjects()).thenReturn(new KList<>());
when(upperRegion.getProceduralObjects()).thenReturn(proceduralObjects);
when(upperRegion.getObjects()).thenReturn(new KList<>(placement));
when(upperRegion.getSurfaceObjects()).thenReturn(new KList<>(placement));
IrisDimension baseDimension = mock(IrisDimension.class);
when(baseDimension.getAllObjectScaleFactor()).thenReturn(1D);
when(baseDimension.isUpperDimensionObjects()).thenReturn(true);
@@ -273,20 +260,13 @@ public class MantleObjectComponentBoundaryRadiusTest {
MantleObjectComponent component = new MantleObjectComponent(engineMantle);
assertEquals(53, component.getRadius());
assertEquals(182, component.getInputRadius());
assertEquals(5, component.getRadius());
assertEquals(41, component.getInputRadius());
}
@Test
@SuppressWarnings("unchecked")
public void translatedTreeCachesDistantSourcesAndSchedulesOnlyItsDestination() throws Exception {
File objectFile = temporaryFolder.newFile("tree.iob");
try (DataOutputStream output = new DataOutputStream(new FileOutputStream(objectFile))) {
output.writeInt(1);
output.writeInt(1);
output.writeInt(1);
}
public void translatedTreeCachesDistantSourcesAndSchedulesOnlyItsDestination() {
IrisObjectPlacement placement = new IrisObjectPlacement()
.setPlace(new KList<>("test/tree"))
.setTranslate(new IrisObjectTranslate().setX(32));
@@ -298,7 +278,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
when(dimension.getReachableBiomes(any())).thenReturn(new KList<>(biome));
ResourceLoader<IrisObject> objectLoader = mock(ResourceLoader.class);
when(objectLoader.findFile("test/tree")).thenReturn(objectFile);
when(objectLoader.load("test/tree")).thenReturn(new IrisObject(1, 1, 1));
IrisData data = mock(IrisData.class);
when(data.getObjectLoader()).thenReturn(objectLoader);
@@ -310,9 +290,9 @@ public class MantleObjectComponentBoundaryRadiusTest {
when(engineMantle.getData()).thenReturn(data);
MantleObjectComponent component = spy(new MantleObjectComponent(engineMantle));
assertEquals(33, component.getRadius());
assertEquals(32, component.getRadius());
assertEquals(0, component.getOutputRadius());
assertEquals(82, component.getInputRadius());
assertEquals(68, component.getInputRadius());
Mantle<Matter> mantle = mock(Mantle.class);
MantleChunk<Matter> chunk = mock(MantleChunk.class);
@@ -341,9 +321,9 @@ public class MantleObjectComponentBoundaryRadiusTest {
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
assertEquals(49, directOrigins.get());
assertEquals(25, directOrigins.get());
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
assertEquals(49, directOrigins.get());
assertEquals(25, directOrigins.get());
List<String> generatedChunks = new ArrayList<>();
doAnswer(invocation -> {
@@ -370,14 +350,14 @@ public class MantleObjectComponentBoundaryRadiusTest {
component.getRadius(),
(chunkX, chunkZ) -> replayedSources.add(chunkX + "," + chunkZ)
);
assertEquals(49, replayedSources.size());
assertEquals("-3,-3", replayedSources.getFirst());
assertEquals("-3,-2", replayedSources.get(1));
assertEquals("3,3", replayedSources.getLast());
assertEquals(25, replayedSources.size());
assertEquals("-2,-2", replayedSources.getFirst());
assertEquals("-2,-1", replayedSources.get(1));
assertEquals("2,2", replayedSources.getLast());
placement.getForbiddenCollisions().add("test/tree");
component.hotload();
assertEquals(130, component.getInputRadius());
assertEquals(100, component.getInputRadius());
}
private static final class TestMatterGenerator implements MatterGenerator {
@@ -76,7 +76,7 @@ public class MatterGeneratorConcurrencyTest {
@Test
public void externalWindowsBoundManagedBlockedComponentSubmissions() throws Exception {
GeneratorFixture fixture = new GeneratorFixture(true);
int maximum = 2 * MatterGenerator.COMPONENT_SUBMISSION_BATCH_SIZE;
int maximum = 2 * MatterGenerator.COMPONENT_SUBMISSION_LIMIT;
CountDownLatch admitted = new CountDownLatch(maximum);
CountDownLatch excess = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
@@ -115,10 +115,10 @@ public class MatterGeneratorConcurrencyTest {
}
@Test
public void externalWindowDefersSharedClaimsUntilItsOwnBatchesComplete() throws Exception {
public void externalWindowDefersSharedClaimsUntilItsOwnSubmissionsComplete() throws Exception {
GeneratorFixture fixture = new GeneratorFixture(true);
List<MatterGenerator.MatterComponentTask> shared = new ArrayList<>();
for (int z = -4; z < -4 + MatterGenerator.COMPONENT_SUBMISSION_BATCH_SIZE; z++) {
for (int z = -4; z < -4 + MatterGenerator.COMPONENT_SUBMISSION_LIMIT; z++) {
MatterGenerator.MatterTaskKey key = new MatterGenerator.MatterTaskKey(
fixture.mantle, -4, z, ReservedFlag.OBJECT);
MatterGenerator.MatterComponentTask task = new MatterGenerator.MatterComponentTask(key);
@@ -137,7 +137,7 @@ public class MatterGeneratorConcurrencyTest {
CompletableFuture<Void> generation = CompletableFuture.runAsync(
() -> generator.generateMatter(0, 0, true, fixture.context));
try {
Await.until("all privately owned component batches", Duration.ofSeconds(10L),
Await.until("all privately owned component submissions", Duration.ofSeconds(10L),
() -> completed.get() == 81 - shared.size());
assertFalse(generation.isDone());
} finally {
@@ -149,16 +149,17 @@ public class MatterGeneratorConcurrencyTest {
}
@Test
public void failedSubmissionBatchDrainsItsWritersBeforeReturning() throws Exception {
public void failedSubmissionDrainsItsWritersBeforeReturning() throws Exception {
GeneratorFixture fixture = new GeneratorFixture(true);
CountDownLatch failed = new CountDownLatch(1);
CountDownLatch started = new CountDownLatch(MatterGenerator.COMPONENT_SUBMISSION_BATCH_SIZE - 1);
CountDownLatch started = new CountDownLatch(MatterGenerator.COMPONENT_SUBMISSION_LIMIT - 1);
CountDownLatch release = new CountDownLatch(1);
IllegalStateException failure = new IllegalStateException("Component rejected its input");
RecordingComponent component = new RecordingComponent(ReservedFlag.OBJECT, 0, 64) {
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
if (x == -4 && z == -4) {
await(started);
failed.countDown();
throw failure;
}
@@ -172,7 +173,7 @@ public class MatterGeneratorConcurrencyTest {
try {
await(failed);
await(started);
if (MatterGenerator.COMPONENT_SUBMISSION_BATCH_SIZE > 1) {
if (MatterGenerator.COMPONENT_SUBMISSION_LIMIT > 1) {
assertFalse(generation.isDone());
verify(fixture.mantle.getChunk(-4, -4), never()).release();
}
@@ -186,6 +187,69 @@ public class MatterGeneratorConcurrencyTest {
assertEquals(0, activeClaims(fixture, 0, 4));
}
@Test
public void externalWindowKeepsSubmittingPastAStragglingComponent() throws Exception {
GeneratorFixture fixture = new GeneratorFixture(true);
CountDownLatch release = new CountDownLatch(1);
AtomicInteger completed = new AtomicInteger();
RecordingComponent component = new RecordingComponent(ReservedFlag.OBJECT, 0, 64) {
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
if (x == -4 && z == -4) {
awaitManaged(release);
}
completed.incrementAndGet();
}
};
TestMatterGenerator generator = fixture.generator(List.of(new MantlePass(List.of(component), 4, 0)));
CompletableFuture<Void> generation = CompletableFuture.runAsync(
() -> generator.generateMatter(0, 0, true, fixture.context));
try {
Await.until("every chunk but the straggler", Duration.ofSeconds(10L), () -> completed.get() == 80);
assertFalse(generation.isDone());
} finally {
release.countDown();
}
generation.get(10L, TimeUnit.SECONDS);
assertEquals(81, completed.get());
assertEquals(0, activeClaims(fixture, 0, 4));
}
@Test
public void inlineWindowFinishesItsOwnChunksBeforeJoiningAClaimAnotherGenerationHolds() throws Exception {
GeneratorFixture fixture = new GeneratorFixture(true);
MantleChunk<Matter> held = fixture.mantle.getChunk(1, 0);
CountDownLatch claimed = new CountDownLatch(1);
CountDownLatch releaseClaim = new CountDownLatch(1);
Set<Long> completed = ConcurrentHashMap.newKeySet();
RecordingComponent component = new RecordingComponent(ReservedFlag.OBJECT, 0, 16) {
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
completed.add((((long) x) << 32) ^ (z & 0xffffffffL));
}
};
TestMatterGenerator generator = fixture.generator(List.of(new MantlePass(List.of(component), 1, 0)));
CompletableFuture<Void> holder = CompletableFuture.runAsync(() -> {
assertTrue(held.tryClaim(ReservedFlag.OBJECT));
claimed.countDown();
await(releaseClaim);
held.completeClaim(ReservedFlag.OBJECT);
});
await(claimed);
CompletableFuture<Void> generation = CompletableFuture.runAsync(
() -> generator.generateMatter(0, 0, false, fixture.context));
try {
Await.until("the eight unclaimed chunks", Duration.ofSeconds(10L), () -> completed.size() == 8);
assertFalse(generation.isDone());
} finally {
releaseClaim.countDown();
}
holder.get(10L, TimeUnit.SECONDS);
generation.get(10L, TimeUnit.SECONDS);
assertEquals(8, completed.size());
assertTrue(held.isFlagged(ReservedFlag.OBJECT));
}
@Test
public void boundedAndReentrantGenerationMatchSerialComponentResults() throws Exception {
Map<Long, Long> serial = componentResults(false, false);
@@ -812,6 +876,25 @@ public class MatterGeneratorConcurrencyTest {
}
return null;
}).when(chunk).raiseFlagSuspend(any(), any(Runnable.class));
when(chunk.tryClaim(any())).thenAnswer(invocation -> {
if (flagged.contains(invocation.getArgument(0)) || !lock.tryLock()) {
return false;
}
if (flagged.contains(invocation.getArgument(0))) {
lock.unlock();
return false;
}
return true;
});
doAnswer(invocation -> {
flagged.add(invocation.getArgument(0));
lock.unlock();
return null;
}).when(chunk).completeClaim(any());
doAnswer(invocation -> {
lock.unlock();
return null;
}).when(chunk).abandonClaim(any());
return chunk;
}
@@ -0,0 +1,517 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.biome.IrisFloatingChildBiomes;
import art.arcane.iris.generation.decoration.IrisProceduralObjects;
import art.arcane.iris.generation.decoration.IrisProceduralPlacement;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.MeteredCache;
import art.arcane.iris.generation.runtime.PreservationRegistry;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
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.spi.PlatformRegistries;
import art.arcane.iris.structure.object.FloatingObjectFootprint;
import art.arcane.iris.structure.object.IrisAxisRotationClamp;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.structure.object.IrisObjectPlacement;
import art.arcane.iris.structure.object.IrisObjectRotation;
import art.arcane.iris.structure.object.IrisObjectScale;
import art.arcane.iris.structure.object.IrisObjectTranslate;
import art.arcane.iris.structure.object.IrisObjectPlacementScaleInterpolator;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.Vector3i;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import org.junit.After;
import org.junit.Assume;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.mockito.Answers;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.function.Consumer;
import java.util.stream.Stream;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
public class PlacementReachPackTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final int SPIN_PERIOD = 360;
private static final int MAXIMUM_CHECKED_KERNELS = 4096;
private static final String V2_HEADER = "Iris V2 IOB;";
private File pack;
private IrisData data;
private IrisDimension dimension;
@Before
public void loadPack() throws IOException {
String configured = System.getenv("IRIS_TEST_PACK");
Assume.assumeTrue(configured != null && !configured.isBlank());
pack = new File(configured).getAbsoluteFile();
Assume.assumeTrue(new File(pack, "objects").isDirectory());
File[] dimensions = new File(pack, "dimensions").listFiles((folder, name) -> name.endsWith(".json"));
Assume.assumeTrue(dimensions != null && dimensions.length > 0);
bindPlatform(Files.createTempDirectory("iris-placement-reach").toFile());
data = IrisData.get(pack);
String dimensionKey = System.getenv().getOrDefault("IRIS_TEST_PACK_DIMENSION",
dimensions[0].getName().substring(0, dimensions[0].getName().length() - 5));
dimension = data.getDimensionLoader().load(dimensionKey);
assertTrue("dimension " + dimensionKey + " must load", dimension != null);
}
@After
public void unbindPlatform() {
IrisServices.remove(PreservationRegistry.class);
IrisPlatforms.unbind();
}
@Test
public void everyObjectFileStaysInsideItsReachUnderRightAngleRotations() throws IOException {
List<IrisObjectPlacement> placements = List.of(
new IrisObjectPlacement(),
new IrisObjectPlacement().setRotation(rotation(new IrisAxisRotationClamp(true, false, 0D, 0D, 90D),
new IrisAxisRotationClamp(true, false, 0D, 0D, 90D), new IrisAxisRotationClamp())),
new IrisObjectPlacement().setRotation(rotation(new IrisAxisRotationClamp(),
new IrisAxisRotationClamp(true, false, 0D, 0D, 90D), new IrisAxisRotationClamp(true, false, 0D, 0D, 90D))),
new IrisObjectPlacement().setRotation(IrisObjectRotation.of(0D, 0D, 0D)),
new IrisObjectPlacement().setRotation(rotation(new IrisAxisRotationClamp(),
new IrisAxisRotationClamp(true, false, 0D, 0D, 45D), new IrisAxisRotationClamp()))
.setTranslate(new IrisObjectTranslate().setX(7).setZ(-3)));
List<Path> files;
try (Stream<Path> walk = Files.walk(new File(pack, "objects").toPath())) {
files = walk.filter(path -> path.toString().endsWith(".iob")).sorted().toList();
}
assertTrue(!files.isEmpty());
Statistics statistics = new Statistics();
NativeBlockState marker = mock(NativeBlockState.class, withSettings().stubOnly());
for (Path file : files) {
IrisObject object = readPositions(file, marker);
for (IrisObjectPlacement placement : placements) {
verifyCentered(file.toString(), object, placement, PlacementReach.of(object, placement, 1D), statistics);
}
}
System.out.println("PLACEMENT_REACH_FILES objects=" + files.size() + " " + statistics);
}
@Test
public void everyPackPlacementStaysInsideItsReach() {
Statistics statistics = new Statistics();
Set<IrisObjectPlacement> placements = new LinkedHashSet<>();
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
placements.addAll(region.getObjects());
}
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
placements.addAll(biome.getObjects());
}
for (IrisObjectPlacement placement : placements) {
for (String key : placement.getPlace()) {
IrisObject object = data.getObjectLoader().load(key);
if (object == null) {
continue;
}
PlacementReach reach = PlacementReach.of(object, placement, placement.getMaximumScale(dimension));
forEachVariant(object, placement, 1D, variant -> verifyCentered(key, variant, placement, reach, statistics));
}
}
for (IrisProceduralPlacement procedural : proceduralPlacements()) {
KList<IrisObject> variants = procedural.getVariantObjects(data);
if (variants == null) {
continue;
}
IrisObjectPlacement placement = procedural.asPlacement();
for (IrisObject variant : variants) {
PlacementReach reach = PlacementReach.of(variant, placement, placement.getMaximumScale(dimension));
forEachVariant(variant, placement, 1D,
scaled -> verifyCentered(variant.getLoadKey(), scaled, placement, reach, statistics));
}
}
assertTrue(statistics.checked > 0);
System.out.println("PLACEMENT_REACH_PLACEMENTS placements=" + placements.size() + " " + statistics);
}
@Test
public void floatingPlacementsStayInsideTheirReach() {
Statistics statistics = new Statistics();
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
KList<IrisFloatingChildBiomes> entries = biome.getFloatingChildBiomes();
if (entries == null) {
continue;
}
for (IrisFloatingChildBiomes entry : entries) {
double shrink = entry.hasObjectShrink() ? entry.getShrinkScale().getMaxScale() : 1D;
IrisBiome target = entry.getRealBiome(biome, data);
for (IrisObjectPlacement placement : listOrEmpty(entry.getFloatingObjects())) {
verifyFloating(placement, shrink, false, statistics);
}
List<IrisObjectPlacement> anchored = new ArrayList<>();
anchored.addAll(listOrEmpty(entry.getExtraObjects()));
anchored.addAll(listOrEmpty(entry.getTopObjectOverrides()));
anchored.addAll(listOrEmpty(entry.getBottomObjectOverrides()));
if (target != null) {
anchored.addAll(listOrEmpty(entry.resolveTopObjects(target)));
anchored.addAll(listOrEmpty(entry.resolveBottomObjects(target)));
}
for (IrisObjectPlacement placement : anchored) {
verifyFloating(placement, shrink, true, statistics);
}
}
}
System.out.println("PLACEMENT_REACH_FLOATING " + statistics);
}
@Test
public void reportsTheMantleWindow() {
Engine engine = mock(Engine.class);
when(engine.getDimension()).thenReturn(dimension);
EngineMantle engineMantle = mock(EngineMantle.class);
when(engineMantle.getEngine()).thenReturn(engine);
when(engineMantle.getData()).thenReturn(data);
MantleObjectComponent objects = new MantleObjectComponent(engineMantle);
MantleFloatingObjectComponent floating = new MantleFloatingObjectComponent(engineMantle);
int objectRadius = objects.getRadius();
int objectInput = objects.getInputRadius();
int objectEager = objects.getEagerInputRadius(objectInput);
int floatingRadius = floating.getRadius();
int floatingPassRadius = chunkRadius(floatingRadius);
int objectPassRadius = chunkRadius(floatingRadius);
int terrainRadius = chunkRadius(floatingRadius + objectEager + 1);
int writerRadius = 2 * chunkRadius(floatingRadius + objectInput + 1);
System.out.println("PLACEMENT_REACH_WINDOW objectReach=" + objectRadius
+ " objectInput=" + objectInput
+ " objectEager=" + objectEager
+ " sourceChunkRadius=" + MantleObjectComponent.sourceChunkRadius(objectRadius)
+ " floatingReach=" + floatingRadius
+ " floatingPassRadius=" + floatingPassRadius
+ " objectPassRadius=" + objectPassRadius
+ " terrainRadius=" + terrainRadius
+ " terrainWindow=" + ((2 * terrainRadius) + 1) + "x" + ((2 * terrainRadius) + 1)
+ " writerRadius=" + writerRadius);
assertTrue(objectRadius > 0);
}
private void verifyFloating(IrisObjectPlacement placement, double shrink, boolean anchored, Statistics statistics) {
if (placement == null || placement.getPlace() == null) {
return;
}
for (String key : placement.getPlace()) {
IrisObject object = data.getObjectLoader().load(key);
if (object == null) {
continue;
}
PlacementReach.Extent extent = PlacementReach.Extent.of(object)
.scaled(placement.getMaximumScale(dimension)).scaled(shrink);
if (!anchored) {
PlacementReach reach = PlacementReach.of(extent, placement);
forEachVariant(object, placement, shrink, variant -> verifyCentered(key, variant, placement, reach, statistics));
continue;
}
PlacementReach reach = PlacementReach.anchored(extent, placement);
forEachVariant(object, placement, shrink, variant -> verifyAnchored(key, variant, reach, statistics));
}
}
private void verifyAnchored(String key, IrisObject variant, PlacementReach reach, Statistics statistics) {
FloatingObjectFootprint footprint = FloatingObjectFootprint.compute(variant);
List<IrisBlockVector> positions = positions(variant);
List<IrisObjectRotation> rotations = List.of(IrisObjectRotation.of(0D, 0D, 0D),
IrisObjectRotation.xFlip180WithY(0D), IrisObjectRotation.xFlip180WithY(90D),
IrisObjectRotation.xFlip180WithY(180D), IrisObjectRotation.xFlip180WithY(270D));
for (IrisObjectRotation rotation : rotations) {
IrisBlockVector anchor = rotation.rotate(new IrisBlockVector(footprint.getTallestKx(), 0, footprint.getTallestKz()), 0, 0, 0);
int actual = 0;
for (IrisBlockVector position : positions) {
IrisBlockVector rotated = rotation.rotate(position, 0, 0, 0);
actual = Math.max(actual, Math.abs((int) Math.round(rotated.getX()) - anchor.getBlockX()));
actual = Math.max(actual, Math.abs((int) Math.round(rotated.getZ()) - anchor.getBlockZ()));
}
record(key, actual, reach.horizontal(), statistics);
}
}
private void verifyCentered(String key, IrisObject object, IrisObjectPlacement placement, PlacementReach reach,
Statistics statistics) {
IrisObjectRotation rotation = placement.getRotation();
IrisObjectTranslate translate = placement.getTranslate();
boolean translating = translate != null && translate.canTranslate();
int[] spinsX = spins(rotation, 0);
int[] spinsY = spins(rotation, 1);
int[] spinsZ = spins(rotation, 2);
List<IrisBlockVector> positions = positions(object);
long kernels = (long) spinsX.length * spinsY.length * spinsZ.length;
if (kernels > MAXIMUM_CHECKED_KERNELS) {
fail("rotation for " + key + " has " + kernels + " kernels");
}
for (int spinX : spinsX) {
for (int spinY : spinsY) {
for (int spinZ : spinsZ) {
IrisBlockVector offset = translating
? rotation.rotate(new IrisBlockVector(translate.getX(), translate.getY(), translate.getZ()), spinX, spinY, spinZ)
: null;
int actual = 0;
for (IrisBlockVector position : positions) {
IrisBlockVector moved = rotation.rotate(position, spinX, spinY, spinZ).clone();
if (offset != null) {
moved.add(offset);
}
actual = Math.max(actual, Math.abs((int) Math.round(moved.getX())));
actual = Math.max(actual, Math.abs((int) Math.round(moved.getZ())));
}
record(key, actual, reach.horizontal(), statistics);
}
}
}
}
private static IrisObject readPositions(Path file, NativeBlockState marker) throws IOException {
try (DataInputStream input = new DataInputStream(new BufferedInputStream(Files.newInputStream(file)))) {
IrisObject object = new IrisObject(input.readInt(), input.readInt(), input.readInt());
input.mark(Short.BYTES + V2_HEADER.length());
int headerLength = input.readUnsignedShort();
boolean v2 = headerLength == V2_HEADER.length()
&& V2_HEADER.equals(new String(input.readNBytes(headerLength), StandardCharsets.US_ASCII));
if (!v2) {
input.reset();
}
int paletteSize = v2 ? input.readShort() : 0;
for (int index = 0; index < paletteSize; index++) {
input.readUTF();
}
int blocks = input.readInt();
for (int index = 0; index < blocks; index++) {
IrisBlockVector position = new IrisBlockVector(input.readShort(), input.readShort(), input.readShort());
if (v2) {
input.readShort();
} else {
input.readUTF();
}
object.getBlocks().put(position, marker);
}
return object;
}
}
private static void record(String key, int actual, int reach, Statistics statistics) {
statistics.checked++;
statistics.maximumActual = Math.max(statistics.maximumActual, actual);
statistics.maximumReach = Math.max(statistics.maximumReach, reach);
statistics.slack += reach - actual;
if (actual > reach) {
fail(key + " places a block " + actual + " blocks from its origin, beyond its reach " + reach);
}
}
private static List<IrisBlockVector> positions(IrisObject object) {
List<IrisBlockVector> positions = new ArrayList<>(object.getBlocks().size() + 8);
object.getBlocks().forEach((position, state) -> positions.add(position));
Vector3i center = object.getCenter();
int[] xs = {-center.getBlockX(), object.getW() - center.getBlockX() - 1};
int[] ys = {-center.getBlockY(), object.getH() - center.getBlockY() - 1};
int[] zs = {-center.getBlockZ(), object.getD() - center.getBlockZ() - 1};
for (int x : xs) {
for (int y : ys) {
for (int z : zs) {
positions.add(new IrisBlockVector(x, y, z));
}
}
}
return positions;
}
private void forEachVariant(IrisObject object, IrisObjectPlacement placement, double shrink, Consumer<IrisObject> consumer) {
IrisObjectScale scale = placement.getScale();
IrisObjectPlacementScaleInterpolator interpolation = scale == null || scale.getInterpolation() == null
? IrisObjectPlacementScaleInterpolator.NONE : scale.getInterpolation();
for (double factor : scales(placement)) {
IrisObject variant = factor == 1D && (scale == null || !scale.shouldScale())
? object
: scale == null ? IrisObjectScale.getFixed(object, factor) : object.scaled(factor, interpolation);
consumer.accept(shrink < 1D ? variant.scaled(shrink, IrisObjectPlacementScaleInterpolator.NONE) : variant);
}
}
private double[] scales(IrisObjectPlacement placement) {
IrisObjectScale scale = placement.getScale();
if (scale == null) {
return new double[]{dimension.getAllObjectScaleFactor()};
}
if (!scale.shouldScale()) {
return new double[]{1D};
}
if (scale.getSize() != 1D) {
return new double[]{scale.getSize()};
}
if (scale.getMinimumScale() == scale.getMaximumScale()) {
return new double[]{scale.getMinimumScale()};
}
int count = Math.max(1, Math.min(scale.getVariations(), 32));
double[] scales = new double[count];
for (int index = 0; index < count; index++) {
scales[index] = scale.getMinimumScale() + (((scale.getMaximumScale() - scale.getMinimumScale()) / count) * index);
}
return scales;
}
private List<IrisProceduralPlacement> proceduralPlacements() {
List<IrisProceduralPlacement> placements = new ArrayList<>();
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
addProcedural(placements, region.getProceduralObjects());
}
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
addProcedural(placements, biome.getProceduralObjects());
}
return placements;
}
private static void addProcedural(List<IrisProceduralPlacement> placements, IrisProceduralObjects procedural) {
if (procedural != null && !procedural.isEmpty()) {
placements.addAll(procedural.getAllPlacements());
}
}
private static int[] spins(IrisObjectRotation rotation, int axis) {
if (rotation == null || !rotation.canRotate()) {
return new int[]{0};
}
boolean rotates = switch (axis) {
case 0 -> rotation.canRotateX();
case 1 -> rotation.canRotateY();
default -> rotation.canRotateZ();
};
if (!rotates) {
return new int[]{0};
}
LongOpenHashSet angles = new LongOpenHashSet();
IntArrayList spins = new IntArrayList();
for (int spin = -SPIN_PERIOD + 1; spin < SPIN_PERIOD; spin++) {
double angle = switch (axis) {
case 0 -> rotation.getXRotation(spin);
case 1 -> rotation.getYRotation(spin);
default -> rotation.getZRotation(spin);
};
if (angles.add(Double.doubleToLongBits(angle))) {
spins.add(spin);
}
}
return spins.toIntArray();
}
private static <T> List<T> listOrEmpty(KList<T> list) {
return list == null ? List.of() : list;
}
private static IrisObjectRotation rotation(IrisAxisRotationClamp x, IrisAxisRotationClamp y, IrisAxisRotationClamp z) {
IrisObjectRotation rotation = new IrisObjectRotation();
rotation.setEnabled(true);
rotation.setXAxis(x);
rotation.setYAxis(y);
rotation.setZAxis(z);
return rotation;
}
private static int chunkRadius(int blockRadius) {
return blockRadius > 0 ? Math.ceilDiv(blockRadius, 16) : 0;
}
private static void bindPlatform(File dataFolder) {
IrisPlatforms.unbind();
Map<String, NativeBlockState> states = new ConcurrentHashMap<>();
PlatformRegistries registries = mock(PlatformRegistries.class, withSettings().stubOnly());
when(registries.block(anyString())).thenAnswer(invocation -> state(states, invocation.getArgument(0)));
when(registries.blockOrNull(anyString())).thenAnswer(invocation -> state(states, invocation.getArgument(0)));
when(registries.blockOrNull(anyString(), anyBoolean())).thenAnswer(invocation -> state(states, invocation.getArgument(0)));
when(registries.air()).thenAnswer(invocation -> state(states, "minecraft:air"));
IrisPlatform platform = mock(IrisPlatform.class, Answers.CALLS_REAL_METHODS);
when(platform.dataFolder()).thenReturn(dataFolder);
when(platform.dataFile(any(String[].class))).thenAnswer(invocation -> {
Object[] path = invocation.getRawArguments();
return new File(dataFolder, String.join(File.separator, (String[]) path[0]));
});
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
IrisServices.register(PreservationRegistry.class, new IgnoredPreservation());
}
private static NativeBlockState state(Map<String, NativeBlockState> states, String key) {
return states.computeIfAbsent(key, ignored -> {
String material = materialOf(key);
boolean air = material.endsWith(":air") || material.endsWith("cave_air") || material.endsWith("void_air");
NativeBlockState state = mock(NativeBlockState.class, withSettings().stubOnly());
when(state.key()).thenReturn(key);
when(state.materialKey()).thenReturn(material);
when(state.isAir()).thenReturn(air);
when(state.isSolid()).thenReturn(!air);
when(state.isOccluding()).thenReturn(!air);
return state;
});
}
private static String materialOf(String key) {
String trimmed = key.trim().toLowerCase(Locale.ROOT);
int properties = trimmed.indexOf('[');
String head = properties < 0 ? trimmed : trimmed.substring(0, properties);
return head.indexOf(':') < 0 ? "minecraft:" + head : head;
}
private static final class IgnoredPreservation implements PreservationRegistry {
@Override
public void register(Thread thread) {
}
@Override
public void register(ExecutorService service) {
}
@Override
public void registerCache(MeteredCache cache) {
}
@Override
public void dereference() {
}
}
private static final class Statistics {
private long checked;
private long slack;
private int maximumActual;
private int maximumReach;
@Override
public String toString() {
return "checked=" + checked + " maxActual=" + maximumActual + " maxReach=" + maximumReach
+ " meanSlack=" + (checked == 0 ? 0D : (double) slack / checked);
}
}
}
@@ -0,0 +1,416 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.decoration.IrisStiltSettings;
import art.arcane.iris.generation.decoration.IrisVacuumSettings;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.structure.object.FloatingObjectFootprint;
import art.arcane.iris.structure.object.IObjectPlacer;
import art.arcane.iris.structure.object.IrisAxisRotationClamp;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.structure.object.IrisObjectPlacement;
import art.arcane.iris.structure.object.IrisObjectPlacementScaleInterpolator;
import art.arcane.iris.structure.object.IrisObjectRotation;
import art.arcane.iris.structure.object.IrisObjectScale;
import art.arcane.iris.structure.object.IrisObjectTranslate;
import art.arcane.iris.structure.object.ObjectPlaceMode;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.stream.ProceduralStream;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
public class PlacementReachTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final int ORIGIN_X = 1000;
private static final int ORIGIN_Y = 90;
private static final int ORIGIN_Z = -2000;
private static final int GROUND_Y = 64;
private final Map<String, NativeBlockState> states = new ConcurrentHashMap<>();
private IrisData data;
private IrisData objectData;
private Engine engine;
@Before
public void bindPlatform() {
IrisPlatforms.unbind();
PlatformRegistries registries = mock(PlatformRegistries.class, withSettings().stubOnly());
when(registries.block(anyString())).thenAnswer(invocation -> state(invocation.getArgument(0)));
when(registries.blockOrNull(anyString())).thenAnswer(invocation -> state(invocation.getArgument(0)));
when(registries.blockOrNull(anyString(), anyBoolean())).thenAnswer(invocation -> state(invocation.getArgument(0)));
when(registries.air()).thenAnswer(invocation -> state("minecraft:air"));
IrisPlatform platform = mock(IrisPlatform.class, withSettings().stubOnly());
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
ProceduralStream<NativeBlockState> rock = mock(ProceduralStream.class, withSettings().stubOnly());
IrisComplex complex = mock(IrisComplex.class, withSettings().stubOnly());
when(complex.getRockStream()).thenReturn(rock);
engine = mock(Engine.class, withSettings().stubOnly());
when(engine.getHeight()).thenReturn(256);
when(engine.getMinHeight()).thenReturn(0);
when(engine.getComplex()).thenReturn(complex);
data = mock(IrisData.class, withSettings().stubOnly());
when(data.getEngine()).thenReturn(engine);
objectData = mock(IrisData.class, withSettings().stubOnly());
}
@After
public void unbindPlatform() {
IrisPlatforms.unbind();
}
@Test
public void halfExtentReachIsTheCenteredFrameNotTheFullWidth() {
IrisObject object = singleBlockObject(103, 75, 73);
IrisObjectPlacement placement = new IrisObjectPlacement();
assertEquals(51, PlacementReach.of(object, placement, 1D).horizontal());
assertEquals(54, PlacementReach.of(object, placement, 1D).input());
assertEquals(51, PlacementReach.of(object, placement.setRotation(IrisObjectRotation.of(0D, 0D, 0D)), 1D).horizontal());
}
@Test
public void arbitraryAngleRotationUsesTheRotatedFrame() {
IrisObject object = singleBlockObject(21, 5, 21);
IrisObjectPlacement placement = new IrisObjectPlacement().setRotation(rotation(new IrisAxisRotationClamp(),
new IrisAxisRotationClamp(true, false, 0D, 0D, 45D), new IrisAxisRotationClamp()));
assertEquals(14, PlacementReach.of(object, placement, 1D).horizontal());
}
@Test
public void slopeRotationIsBoundedEvenWhenTheConfiguredRotationIsDisabled() {
IrisObject object = singleBlockObject(21, 9, 21);
IrisObjectPlacement placement = new IrisObjectPlacement().setRotation(IrisObjectRotation.of(0D, 0D, 0D));
assertEquals(10, PlacementReach.of(object, placement, 1D).horizontal());
assertEquals(15, PlacementReach.of(object, placement.setRotateTowardsSlope(true), 1D).horizontal());
}
@Test
public void unionKeepsTheLargestReachAndTheLargestInput() {
PlacementReach union = new PlacementReach(10, 2).union(new PlacementReach(4, 12));
assertEquals(10, union.horizontal());
assertEquals(16, union.input());
assertEquals(19, union.withReadMargin(9).input());
assertEquals(16, union.withReadMargin(1).input());
}
@Test
public void scaledSpanBoundsEveryScaledVariant() {
Random random = new Random(91L);
double[] scales = {0.01D, 0.25D, 0.45D, 0.5D, 0.75D, 0.95D, 1D, 1.3D, 1.5D, 2D, 2.6D, 3.7D, 4D};
for (int trial = 0; trial < 120; trial++) {
int outside = trial % 3 == 0 ? 1 + random.nextInt(3) : 0;
IrisObject object = randomObject(random, outside);
PlacementReach.Extent extent = PlacementReach.Extent.of(object);
for (double scale : scales) {
for (IrisObjectPlacementScaleInterpolator interpolation : List.of(IrisObjectPlacementScaleInterpolator.NONE,
IrisObjectPlacementScaleInterpolator.TRILINEAR)) {
if (interpolation != IrisObjectPlacementScaleInterpolator.NONE && scale > 2D) {
continue;
}
IrisObject scaled = object.scaled(scale, interpolation);
for (double maximum : new double[]{scale, Math.min(IrisObjectScale.MAXIMUM_FACTOR, scale * 1.4D)}) {
assertContains(extent.scaled(maximum), scaled, "scale " + scale + " bounded at " + maximum);
for (double shrink : new double[]{0.45D, 0.8D}) {
assertContains(extent.scaled(maximum).scaled(shrink),
scaled.scaled(shrink, IrisObjectPlacementScaleInterpolator.NONE),
"scale " + scale + " shrink " + shrink);
}
}
}
}
}
}
@Test
public void runnerWritesAndReadsStayInsideTheReach() {
Random random = new Random(1337L);
for (int trial = 0; trial < 260; trial++) {
IrisObject object = randomObject(random, trial % 5 == 0 ? 2 : 0);
object.setLoader(objectData);
IrisObjectPlacement placement = randomPlacement(random);
PlacementReach reach = PlacementReach.of(object, placement, placement.getMaximumScale(null));
for (int seed = 0; seed < 12; seed++) {
RNG rng = new RNG((trial * 7919L) + seed);
IrisObject variant = placement.getScale() == null ? object : placement.getScale().get(rng, object);
RecordingPlacer placer = new RecordingPlacer(engine);
variant.place(ORIGIN_X, ORIGIN_Y, ORIGIN_Z, placer, placement, rng, null, null, data);
String context = "trial " + trial + " seed " + seed + " mode " + placement.getMode()
+ " rotation " + placement.getRotation() + " translate " + placement.getTranslate()
+ " scale " + placement.getScale() + " warp " + placement.getWarp().getMultiplier();
assertTrue(context + " writes " + placer.writeReach + " > " + reach.horizontal(),
placer.writeReach <= reach.horizontal());
assertTrue(context + " reads " + placer.readReach + " > " + reach.input(),
placer.readReach <= reach.input());
}
}
}
@Test
public void anchoredReachCoversTheFloatingFootprintAnchor() {
Random random = new Random(7L);
for (int trial = 0; trial < 80; trial++) {
IrisObject object = randomObject(random, trial % 4 == 0 ? 1 : 0);
PlacementReach reach = PlacementReach.anchored(PlacementReach.Extent.of(object), new IrisObjectPlacement());
FloatingObjectFootprint footprint = FloatingObjectFootprint.compute(object);
for (IrisObjectRotation rotation : List.of(IrisObjectRotation.of(0D, 0D, 0D),
IrisObjectRotation.xFlip180WithY(0D), IrisObjectRotation.xFlip180WithY(90D),
IrisObjectRotation.xFlip180WithY(180D), IrisObjectRotation.xFlip180WithY(270D))) {
IrisBlockVector anchor = rotation.rotate(new IrisBlockVector(footprint.getTallestKx(), 0, footprint.getTallestKz()), 0, 0, 0);
object.getBlocks().forEach((position, state) -> {
IrisBlockVector rotated = rotation.rotate(position, 0, 0, 0);
assertTrue(Math.abs((int) Math.round(rotated.getX()) - anchor.getBlockX()) <= reach.horizontal());
assertTrue(Math.abs((int) Math.round(rotated.getZ()) - anchor.getBlockZ()) <= reach.horizontal());
});
}
}
}
private IrisObjectPlacement randomPlacement(Random random) {
IrisObjectPlacement placement = new IrisObjectPlacement();
placement.setRotation(randomRotation(random));
if (random.nextInt(3) > 0) {
placement.setTranslate(new IrisObjectTranslate()
.setX(random.nextInt(41) - 20)
.setY(random.nextInt(21) - 10)
.setZ(random.nextInt(41) - 20));
}
if (random.nextInt(3) == 0) {
placement.setWarp(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).setMultiplier(1D + random.nextInt(9)));
}
List<ObjectPlaceMode> modes = List.of(ObjectPlaceMode.CENTER_HEIGHT, ObjectPlaceMode.STILT, ObjectPlaceMode.FAST_STILT,
ObjectPlaceMode.CENTER_STILT, ObjectPlaceMode.ERODE_STILT, ObjectPlaceMode.ORGANIC_STILT, ObjectPlaceMode.CEILING_HANG,
ObjectPlaceMode.VACUUM, ObjectPlaceMode.VACUUM_HIGH, ObjectPlaceMode.VACUUM_FAST, ObjectPlaceMode.VACUUM_ORGANIC,
ObjectPlaceMode.VACUUM_WAVY, ObjectPlaceMode.STRUCTURE_PIECE);
placement.setMode(modes.get(random.nextInt(modes.size())));
if (random.nextInt(4) == 0) {
placement.setVacuumSettings(new IrisVacuumSettings().setRadius(1 + random.nextInt(16)));
}
if (random.nextInt(4) == 0) {
placement.setStiltSettings(new IrisStiltSettings());
}
placement.setBore(random.nextInt(4) == 0);
placement.setBoreExtendMaxY(random.nextInt(3));
placement.setSnow(random.nextInt(4) == 0 ? 0.6D : 0D);
placement.setSurfaceSupportBuffer(random.nextInt(5));
switch (random.nextInt(5)) {
case 0 -> placement.setScale(new IrisObjectScale().setSize(0.3D + (random.nextDouble() * 1.6D)));
case 1 -> placement.setScale(new IrisObjectScale().setMinimumScale(0.5D).setMaximumScale(2.3D)
.setVariations(5).setInterpolation(IrisObjectPlacementScaleInterpolator.TRILINEAR));
case 2 -> placement.setScale(new IrisObjectScale().setMinimumScale(0.4D).setMaximumScale(0.9D).setVariations(3));
default -> {
}
}
return placement;
}
private static IrisObjectRotation randomRotation(Random random) {
return switch (random.nextInt(9)) {
case 0 -> new IrisObjectRotation();
case 1 -> IrisObjectRotation.of(0D, 0D, 0D);
case 2 -> rotation(new IrisAxisRotationClamp(), new IrisAxisRotationClamp(true, false, 0D, 0D, 45D), new IrisAxisRotationClamp());
case 3 -> rotation(new IrisAxisRotationClamp(), new IrisAxisRotationClamp(true, false, 0D, 0D, 0D), new IrisAxisRotationClamp());
case 4 -> IrisObjectRotation.of(90D, 0D, 0D);
case 5 -> rotation(new IrisAxisRotationClamp(true, false, 0D, 0D, 90D),
new IrisAxisRotationClamp(true, false, 0D, 0D, 90D), new IrisAxisRotationClamp(true, false, 0D, 0D, 90D));
case 6 -> rotation(new IrisAxisRotationClamp(true, false, 0D, 0D, 0D),
new IrisAxisRotationClamp(true, false, 0D, 0D, 0D), new IrisAxisRotationClamp(true, false, 0D, 0D, 30D));
case 7 -> IrisObjectRotation.of(30D, 135D, 0D);
default -> rotation(new IrisAxisRotationClamp(), new IrisAxisRotationClamp(true, false, 20D, 110D, 15D),
new IrisAxisRotationClamp(true, true, 180D, 180D, 0D));
};
}
private IrisObject randomObject(Random random, int outside) {
int width = 1 + random.nextInt(18);
int height = 1 + random.nextInt(14);
int depth = 1 + random.nextInt(18);
IrisObject object = new IrisObject(width, height, depth);
int count = 1 + random.nextInt(Math.max(1, (width * height * depth) / 3));
for (int index = 0; index < count; index++) {
int x = random.nextInt(width) - (width / 2);
int y = random.nextInt(height) - (height / 2);
int z = random.nextInt(depth) - (depth / 2);
object.getBlocks().put(new IrisBlockVector(x, y, z), state(random.nextInt(5) == 0 ? "minecraft:oak_leaves" : "minecraft:stone"));
}
for (int index = 0; index < outside; index++) {
int x = random.nextInt(width + 10) - ((width + 10) / 2);
int y = random.nextInt(height + 6) - ((height + 6) / 2);
int z = random.nextInt(depth + 10) - ((depth + 10) / 2);
object.getBlocks().put(new IrisBlockVector(x, y, z), state("minecraft:stone"));
}
return object;
}
private IrisObject singleBlockObject(int width, int height, int depth) {
IrisObject object = new IrisObject(width, height, depth);
object.getBlocks().put(new IrisBlockVector(0, 0, 0), state("minecraft:stone"));
return object;
}
private static void assertContains(PlacementReach.Extent extent, IrisObject object, String context) {
object.getBlocks().forEach((position, state) -> {
assertTrue(context + " x " + position.getBlockX() + " outside " + extent.x(),
position.getBlockX() >= extent.x().minimum() && position.getBlockX() <= extent.x().maximum());
assertTrue(context + " y " + position.getBlockY() + " outside " + extent.y(),
position.getBlockY() >= extent.y().minimum() && position.getBlockY() <= extent.y().maximum());
assertTrue(context + " z " + position.getBlockZ() + " outside " + extent.z(),
position.getBlockZ() >= extent.z().minimum() && position.getBlockZ() <= extent.z().maximum());
});
int centerX = object.getCenter().getBlockX();
int centerZ = object.getCenter().getBlockZ();
assertTrue(context + " frame x", -centerX >= extent.x().minimum() && object.getW() - 1 - centerX <= extent.x().maximum());
assertTrue(context + " frame z", -centerZ >= extent.z().minimum() && object.getD() - 1 - centerZ <= extent.z().maximum());
}
private static IrisObjectRotation rotation(IrisAxisRotationClamp x, IrisAxisRotationClamp y, IrisAxisRotationClamp z) {
IrisObjectRotation rotation = new IrisObjectRotation();
rotation.setEnabled(true);
rotation.setXAxis(x);
rotation.setYAxis(y);
rotation.setZAxis(z);
return rotation;
}
private NativeBlockState state(String key) {
return states.computeIfAbsent(key, ignored -> {
String material = key.toLowerCase(Locale.ROOT);
boolean air = material.endsWith("air");
NativeBlockState state = mock(NativeBlockState.class, withSettings().stubOnly());
when(state.key()).thenReturn(key);
when(state.materialKey()).thenReturn(material);
when(state.isAir()).thenReturn(air);
when(state.isSolid()).thenReturn(!air);
when(state.isOccluding()).thenReturn(!air);
return state;
});
}
private static final class RecordingPlacer implements IObjectPlacer {
private final Engine engine;
private int writeReach;
private int readReach;
private RecordingPlacer(Engine engine) {
this.engine = engine;
}
private void write(int x, int z) {
writeReach = Math.max(writeReach, Math.max(Math.abs(x - ORIGIN_X), Math.abs(z - ORIGIN_Z)));
read(x, z);
}
private void read(int x, int z) {
readReach = Math.max(readReach, Math.max(Math.abs(x - ORIGIN_X), Math.abs(z - ORIGIN_Z)));
}
@Override
public int getHighest(int x, int z, IrisData data) {
read(x, z);
return GROUND_Y;
}
@Override
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
read(x, z);
return GROUND_Y;
}
@Override
public void set(int x, int y, int z, NativeBlockState d) {
write(x, z);
}
@Override
public NativeBlockState get(int x, int y, int z) {
read(x, z);
return null;
}
@Override
public boolean isPreventingDecay() {
return false;
}
@Override
public boolean isCarved(int x, int y, int z) {
read(x, z);
return false;
}
@Override
public boolean isSolid(int x, int y, int z) {
read(x, z);
return y < GROUND_Y;
}
@Override
public boolean isUnderwater(int x, int z) {
read(x, z);
return false;
}
@Override
public int getFluidHeight() {
return GROUND_Y - 1;
}
@Override
public boolean isDebugSmartBore() {
return false;
}
@Override
public void setTile(int xx, int yy, int zz, TileData tile) {
write(xx, zz);
}
@Override
public <T> void setData(int xx, int yy, int zz, T data) {
write(xx, zz);
}
@Override
public <T> T getData(int xx, int yy, int zz, Class<T> t) {
read(xx, zz);
return null;
}
@Override
public Engine getEngine() {
return engine;
}
}
}
@@ -0,0 +1,47 @@
package art.arcane.iris.generation.runtime;
import org.junit.Test;
import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertEquals;
public final class EngineMetricsStageTotalsTest {
@Test
public void stageTotalsAccumulatePastTheRollingWindow() {
EngineMetrics metrics = new EngineMetrics(2);
metrics.getTerrain().put(1.5D);
metrics.getTerrain().put(2.5D);
metrics.getTerrain().put(4D);
EngineMetrics.StageTotal terrain = metrics.stageTotals().get("terrain");
assertEquals(8D, terrain.millis(), 0D);
assertEquals(3L, terrain.samples());
assertEquals(3.25D, metrics.getTerrain().getAverage(), 0D);
}
@Test
public void stageTotalsSinceAnEarlierSnapshotCoverOnlyTheLaterPuts() {
EngineMetrics metrics = new EngineMetrics(4);
metrics.getCarveResolve().put(3D);
EngineMetrics.StageTotal before = metrics.stageTotals().get("carve.resolve");
metrics.getCarveResolve().put(5D);
metrics.getCarveResolve().put(7D);
EngineMetrics.StageTotal window = metrics.stageTotals().get("carve.resolve").since(before);
assertEquals(12D, window.millis(), 0D);
assertEquals(2L, window.samples());
}
@Test
public void stageTotalsNameEveryPulledTimer() {
EngineMetrics metrics = new EngineMetrics(1);
Map<String, EngineMetrics.StageTotal> totals = metrics.stageTotals();
assertEquals(Set.copyOf(metrics.pull().keySet()), Set.copyOf(totals.keySet()));
assertEquals(metrics.pull().size(), totals.size());
}
}
@@ -8,7 +8,6 @@ import art.arcane.iris.generation.runtime.GenerationRuntime.BiomeMaxes;
import art.arcane.iris.world.history.SavedBiomeRuntime;
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.testsupport.Await;
import art.arcane.iris.testsupport.PlatformBinding;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@@ -37,13 +36,11 @@ import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
@@ -680,30 +677,19 @@ public class IrisEngineGenerationRuntimeScopeTest {
}
@Test
public void missingRouterReadWaitsForAttachmentPublication() throws Exception {
public void missingRouterReadsDoNotWaitForTheAttachmentMonitor() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
GenerationHistoryRuntimeRouter router = historyRouter(engine);
GenerationHistoryRuntimeRouter.CoordinateScope scope = mock(GenerationHistoryRuntimeRouter.CoordinateScope.class);
when(router.openCoordinateScope(-17, 32)).thenReturn(scope);
CountDownLatch started = new CountDownLatch(1);
FutureTask<GenerationHistoryRuntimeRouter.CoordinateScope> read = new FutureTask<>(() -> {
started.countDown();
return engine.openGenerationHistoryCoordinateScope(-17, 32);
});
Thread reader = new Thread(read, "Pending history attachment");
ExecutorService reader = Executors.newSingleThreadExecutor();
try {
synchronized (engine.generationHistoryRuntimeRouterLock) {
reader.start();
assertTrue(started.await(5, TimeUnit.SECONDS));
awaitBlocked(reader);
engine.attachGenerationHistoryRuntimeRouter(router);
assertNull(reader.submit(() -> engine.openGenerationHistoryCoordinateScope(-17, 32))
.get(5, TimeUnit.SECONDS));
assertTrue(reader.submit(engine::getGenerationHistoryRuntimeRouter).get(5, TimeUnit.SECONDS).isEmpty());
}
assertSame(scope, read.get(5, TimeUnit.SECONDS));
} finally {
reader.join(5_000L);
assertTrue("Pending router read did not finish", !reader.isAlive());
reader.shutdownNow();
assertTrue(reader.awaitTermination(5, TimeUnit.SECONDS));
}
}
@@ -713,27 +699,19 @@ public class IrisEngineGenerationRuntimeScopeTest {
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
GenerationHistoryRuntimeRouter router = historyRouter(engine);
engine.attachGenerationHistoryRuntimeRouter(router);
CountDownLatch started = new CountDownLatch(1);
FutureTask<GenerationHistoryRuntimeRouter.CoordinateScope> read = new FutureTask<>(() -> {
started.countDown();
return engine.openGenerationHistoryCoordinateScope(-17, 32);
});
Thread reader = new Thread(read, "Detached history read");
engine.detachGenerationHistoryRuntimeRouter(router);
ExecutorService reader = Executors.newSingleThreadExecutor();
try {
synchronized (engine.generationHistoryRuntimeRouterLock) {
engine.detachGenerationHistoryRuntimeRouter(router);
reader.start();
assertTrue(started.await(5, TimeUnit.SECONDS));
awaitBlocked(reader);
ExecutionException failure = assertThrows(ExecutionException.class, () -> reader.submit(
() -> engine.openGenerationHistoryCoordinateScope(-17, 32)).get(5, TimeUnit.SECONDS));
assertTrue(failure.getCause() instanceof IllegalStateException);
assertEquals("Iris generation-history runtime router is detached.", failure.getCause().getMessage());
}
ExecutionException failure = assertThrows(ExecutionException.class, () -> read.get(5, TimeUnit.SECONDS));
assertTrue(failure.getCause() instanceof IllegalStateException);
assertEquals("Iris generation-history runtime router is detached.", failure.getCause().getMessage());
assertTrue(engine.getGenerationHistoryRuntimeRouter().isEmpty());
} finally {
reader.join(5_000L);
assertTrue("Detached router read did not finish", !reader.isAlive());
reader.shutdownNow();
assertTrue(reader.awaitTermination(5, TimeUnit.SECONDS));
}
verify(router, never()).openCoordinateScope(anyInt(), anyInt());
}
@@ -909,12 +887,6 @@ public class IrisEngineGenerationRuntimeScopeTest {
return router;
}
private static void awaitBlocked(Thread thread) {
Await.reached("the reader to block on the engine lifecycle lock", Duration.ofSeconds(5L),
() -> thread.getState() == Thread.State.BLOCKED || !thread.isAlive());
assertEquals(Thread.State.BLOCKED, thread.getState());
}
private static IrisEngine engine(
GenerationRuntime generationRuntime,
EngineEffects effects,
@@ -132,6 +132,9 @@ public class IrisCarveModifierBiomeBlendTest {
Field field = IrisCarveModifier.class.getDeclaredField("rng");
field.setAccessible(true);
field.set(modifier, rng);
Field seed = IrisCarveModifier.class.getDeclaredField("caveBlendSeed");
seed.setAccessible(true);
seed.setLong(modifier, SEED);
return modifier;
}
@@ -56,6 +56,9 @@ public class IrisCarveModifierBoundarySupportTest {
Field rng = IrisCarveModifier.class.getDeclaredField("rng");
rng.setAccessible(true);
rng.set(modifier, new RNG(71L));
Field seed = IrisCarveModifier.class.getDeclaredField("caveBlendSeed");
seed.setAccessible(true);
seed.setLong(modifier, 71L);
IrisMatterSupport.ensureRegistered();
Matter floorMatter = new IrisMatter(16, 16, 16);
@@ -0,0 +1,53 @@
package art.arcane.iris.generation.stage;
import art.arcane.volmlib.util.math.BlockPosition;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import java.util.SplittableRandom;
import static org.junit.Assert.assertEquals;
public class IrisCarveModifierCaveBlendRollTest {
private static final int SAMPLES = 1_000_000;
@Test
public void statelessBlendRandomMatchesParallelRngForOneMillionPoints() {
SplittableRandom random = new SplittableRandom(0x5eed_cafe_f00dL);
long[] seeds = {0L, 1L, -1L, 1337L, Long.MIN_VALUE, Long.MAX_VALUE, 0x5DEECE66DL, random.nextLong()};
for (int sample = 0; sample < SAMPLES; sample++) {
long seed = sample < seeds.length * 64 ? seeds[sample % seeds.length] : random.nextLong();
int x = coordinate(random, sample);
int y = sample % 7 == 0 ? random.nextInt() : random.nextInt(-2048, 2048);
int z = coordinate(random, sample + 3);
assertEquals(seed + " @ " + x + "," + y + "," + z,
new RNG(seed).nextParallelRNG(BlockPosition.toLong(x, y, z)).nextInt(),
IrisCarveModifier.caveBlendRandom(seed, x, y, z));
}
}
@Test
public void statelessBlendRandomMatchesParallelRngAtCoordinateExtremes() {
int[] edges = {Integer.MIN_VALUE, Integer.MIN_VALUE + 1, -33_554_433, -33_554_432, -1, 0, 1,
33_554_431, 33_554_432, Integer.MAX_VALUE - 1, Integer.MAX_VALUE};
long[] seeds = {0L, -1L, 1337L, Long.MIN_VALUE, Long.MAX_VALUE};
for (long seed : seeds) {
for (int x : edges) {
for (int y : edges) {
for (int z : edges) {
assertEquals(new RNG(seed).nextParallelRNG(BlockPosition.toLong(x, y, z)).nextInt(),
IrisCarveModifier.caveBlendRandom(seed, x, y, z));
}
}
}
}
}
private static int coordinate(SplittableRandom random, int sample) {
return switch (sample % 4) {
case 0 -> random.nextInt();
case 1 -> random.nextInt(-30_000_000, 30_000_000);
default -> random.nextInt(-65_536, 65_536);
};
}
}
@@ -1,5 +1,6 @@
package art.arcane.iris.generation.stage;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.volmlib.util.matter.MatterCavern;
import org.junit.Test;
@@ -49,6 +50,7 @@ public class IrisCarveScratchTest {
scratch.boundaryMasks[0].add(13);
scratch.walls.put(1, 12, 2, cavern);
scratch.customBiomeCache.put("cave", null);
scratch.caveBiomeCache.put(42L, new IrisBiome());
scratch.customCaveBiomePresent = true;
scratch.reset();
@@ -56,6 +58,7 @@ public class IrisCarveScratchTest {
assertTrue(scratch.columnMasks[0].isEmpty());
assertTrue(scratch.boundaryMasks[0].isEmpty());
assertTrue(scratch.customBiomeCache.isEmpty());
assertTrue(scratch.caveBiomeCache.isEmpty());
assertFalse(scratch.customCaveBiomePresent);
int[] wallCount = new int[1];
scratch.walls.forEach((x, y, z, value) -> wallCount[0]++);
@@ -19,11 +19,10 @@ import art.arcane.iris.generation.decoration.IrisDecorator;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.chunk.ChunkDataHunkHolder;
import art.arcane.iris.generation.chunk.SectionBuffer;
import art.arcane.iris.generation.chunk.ColumnExtentListeningHunk;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import org.bukkit.generator.ChunkGenerator;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
@@ -165,10 +164,7 @@ public class IrisNaturalCaveScanTest {
fullOutput.listen((x, y, z, state) -> fullWrites.add(x + ":" + y + ":" + z + ":" + state.key())), null);
Fixture bounded = new Fixture(true);
ChunkGenerator.ChunkData chunkData = mock(ChunkGenerator.ChunkData.class);
doReturn(0).when(chunkData).getMinHeight();
doReturn(32).when(chunkData).getMaxHeight();
ChunkDataHunkHolder holder = new ChunkDataHunkHolder(chunkData);
SectionBuffer holder = new SectionBuffer(32);
bounded.sparseOutput(holder);
List<String> boundedWrites = new ArrayList<>();
bounded.modifier.decorateNaturalCaves(-32, 48, new ColumnExtentListeningHunk<>(holder,
@@ -1,7 +1,7 @@
package art.arcane.iris.generation.stage;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.chunk.ChunkDataHunkHolder;
import art.arcane.iris.generation.chunk.SectionBuffer;
import art.arcane.iris.generation.chunk.ColumnExtentListeningHunk;
import art.arcane.iris.generation.concurrent.BurstExecutor;
import art.arcane.iris.generation.concurrent.MultiBurst;
@@ -15,7 +15,6 @@ import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.volmlib.util.hunk.storage.ArrayHunk;
import org.bukkit.block.data.BlockData;
import org.bukkit.generator.ChunkGenerator;
import org.junit.Rule;
import org.junit.Test;
@@ -65,8 +64,8 @@ public class IrisPerfectionModifierColumnTest {
NativeBlockState[] palette = {null, fixture.air, fixture.water, fixture.stone,
fixture.decorant, fixture.half, fixture.spike(true, false), fixture.spike(false, true)};
for (int trial = 0; trial < 8; trial++) {
ChunkDataHunkHolder bounded = holder(64);
ChunkDataHunkHolder full = holder(64);
SectionBuffer bounded = new SectionBuffer(64);
SectionBuffer full = new SectionBuffer(64);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
int top = random.nextInt(64);
@@ -91,13 +90,6 @@ public class IrisPerfectionModifierColumnTest {
}
}
private static ChunkDataHunkHolder holder(int height) {
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
when(chunk.getMinHeight()).thenReturn(0);
when(chunk.getMaxHeight()).thenReturn(height);
return new ChunkDataHunkHolder(chunk);
}
@Test
public void unrelatedTallColumnsAreScannedOnceDuringRepeatedCleanup() {
Fixture fixture = new Fixture();
@@ -1,6 +1,7 @@
package art.arcane.iris.generation.subterrain;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.volmlib.util.collection.KList;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import org.junit.Test;
@@ -10,6 +11,11 @@ import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.HashSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -441,6 +447,74 @@ public class SubterrainPlannerTest {
}
}
@Test
public void alternatingChunkSamplingResolvesEachCandidateCellOnce() {
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE).setAllowedRegions(new KList<>("test-region"));
AtomicInteger regionQueries = new AtomicInteger();
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 7123, -64, 97,
(x, z) -> {
regionQueries.incrementAndGet();
return "test-region";
}));
for (int round = 0; round < 500; round++) {
planner.sample(7, 10, 7);
planner.sample(23, 10, 7);
planner.sample(7, 10, 23);
planner.sample(-9, 10, -9);
}
int firstRound = regionQueries.get();
assertTrue("candidate cells resolved " + firstRound + " times", firstRound <= 16);
for (int round = 0; round < 500; round++) {
planner.plansForBounds(-1, -1, 16, 16);
planner.plansForBounds(15, 15, 32, 32);
}
assertEquals(firstRound, regionQueries.get());
}
@Test
public void concurrentCrossChunkSamplingMatchesAFreshPlanner() throws Exception {
List<IrisSubterrainFeature> features = List.of(compactFeature(IrisSubterrainFamily.CENOTE).setSpacing(96),
compactFeature(IrisSubterrainFamily.TRAVERTINE_TERRACES).setSpacing(160));
SubterrainPlanner shared = new SubterrainPlanner(new SubterrainPlanner.Options(features, 4242, -64, 97, (x, z) -> "test-region"));
SubterrainPlanner reference = new SubterrainPlanner(new SubterrainPlanner.Options(features, 4242, -64, 97, (x, z) -> "test-region"));
int threads = 8;
ExecutorService pool = Executors.newFixedThreadPool(threads);
try {
List<Future<Integer>> results = new ArrayList<>();
for (int thread = 0; thread < threads; thread++) {
int offset = thread;
results.add(pool.submit(() -> {
int occupied = 0;
for (int step = 0; step < 6000; step++) {
int x = Math.floorMod(step * 37 + offset * 101, 2400) - 1200;
int z = Math.floorMod(step * 53 + offset * 211, 2400) - 1200;
int y = Math.floorMod(step * 11 + offset, 150) - 60;
int chunkX = Math.floorDiv(x, 16) * 16;
int chunkZ = Math.floorDiv(z, 16) * 16;
SubterrainCell expected;
synchronized (reference) {
expected = reference.sample(reference.plansForBounds(chunkX - 1, chunkZ - 1, chunkX + 16, chunkZ + 16), x, y, z);
}
SubterrainCell actual = shared.sample(x, y, z);
assertEquals(expected, actual);
if (actual.occupied()) {
occupied++;
}
}
return occupied;
}));
}
int occupied = 0;
for (Future<Integer> result : results) {
occupied += result.get(60, TimeUnit.SECONDS);
}
assertTrue(occupied > 0);
} finally {
pool.shutdownNow();
assertTrue(pool.awaitTermination(10, TimeUnit.SECONDS));
}
}
@Test
public void immutableSnapshotsAndSeedsControlPlans() {
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE);
@@ -0,0 +1,116 @@
package art.arcane.iris.pack.loading;
import art.arcane.iris.generation.biome.IrisBiomeCustom;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.testsupport.DurabilityMode;
import art.arcane.iris.world.history.GenerationRegistryContractFactory;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
public class IrisDataCustomBiomeKeyTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void repeatedConcurrentLookupsMatchTheContentAddressedKeyPerDimension() throws Exception {
Path pack = temporaryFolder.newFolder("custom-keys").toPath().resolve("iris/pack");
writePack(pack);
IrisData data = IrisData.openDatapackCompiler(pack.toFile());
ExecutorService pool = Executors.newFixedThreadPool(8);
try {
IrisDimension overworld = data.getDimensionLoader().load("overworld");
IrisDimension second = data.getDimensionLoader().load("second");
List<IrisBiomeCustom> customs = data.getBiomeLoader().load("test").getCustomDerivitives();
assertEquals(2, customs.size());
List<String> expected = new ArrayList<>();
for (IrisDimension dimension : List.of(overworld, second)) {
for (IrisBiomeCustom custom : customs) {
expected.add(GenerationRegistryContractFactory.customBiomeResourceKey(
dimension.getLoadKey(), custom, data.getContentGate()));
}
}
assertEquals(4, expected.stream().distinct().count());
List<Future<List<String>>> results = new ArrayList<>();
for (int thread = 0; thread < 8; thread++) {
results.add(pool.submit(() -> {
List<String> seen = new ArrayList<>();
for (int round = 0; round < 200; round++) {
seen.clear();
for (IrisDimension dimension : List.of(overworld, second)) {
for (IrisBiomeCustom custom : customs) {
seen.add(data.customBiomeResourceKey(dimension, custom));
}
}
}
return List.copyOf(seen);
}));
}
for (Future<List<String>> result : results) {
assertEquals(expected, result.get(60, TimeUnit.SECONDS));
}
IrisBiomeCustom equalCopy = new IrisBiomeCustom().setId(customs.getFirst().getId());
assertEquals(GenerationRegistryContractFactory.customBiomeResourceKey(
overworld.getLoadKey(), equalCopy, data.getContentGate()),
data.customBiomeResourceKey(overworld, equalCopy));
assertNotEquals(data.customBiomeResourceKey(overworld, customs.getFirst()),
data.customBiomeResourceKey(second, customs.getFirst()));
} finally {
pool.shutdownNow();
assertTrue(pool.awaitTermination(10, TimeUnit.SECONDS));
data.close();
}
}
private static void writePack(Path root) throws Exception {
Files.createDirectories(root.resolve("dimensions"));
Files.createDirectories(root.resolve("biomes"));
for (String dimension : List.of("overworld", "second")) {
Files.writeString(root.resolve("dimensions/" + dimension + ".json"), """
{
"name": "Test Dimension",
"environment": "NORMAL",
"logicalHeight": 256,
"dimensionHeight": {
"min": -64,
"max": 320
}
}
""", StandardCharsets.UTF_8);
}
Files.writeString(root.resolve("biomes/test.json"), """
{
"name": "Test Biome",
"derivative": "minecraft:plains",
"vanillaDerivative": "minecraft:plains",
"customDerivitives": [
{
"id": "first_custom"
},
{
"id": "second_custom",
"temperature": 1.5
}
]
}
""", StandardCharsets.UTF_8);
}
}
@@ -99,7 +99,7 @@ public class PregenPerformanceProfileTest {
}
@Test
public void pregenerationWantsTwoBurstWorkersPerCore() {
public void pregenerationWantsTwoBurstWorkersPerPerformanceCore() {
assertEquals(32, PregenPerformanceProfile.pregenBurstParallelism(16));
assertEquals(4, PregenPerformanceProfile.pregenBurstParallelism(1));
}