diff --git a/core/build.gradle b/core/build.gradle
index ba53e472a..457e53dff 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -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
diff --git a/core/purity-allowlist.txt b/core/purity-allowlist.txt
index 0d263819a..838d61c6f 100644
--- a/core/purity-allowlist.txt
+++ b/core/purity-allowlist.txt
@@ -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
diff --git a/core/src/main/java/art/arcane/iris/generation/chunk/ChunkDataHunkHolder.java b/core/src/main/java/art/arcane/iris/generation/chunk/ChunkDataHunkHolder.java
deleted file mode 100644
index 157fc8fad..000000000
--- a/core/src/main/java/art/arcane/iris/generation/chunk/ChunkDataHunkHolder.java
+++ /dev/null
@@ -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 .
- */
-
-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 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);
- }
-}
diff --git a/core/src/main/java/art/arcane/iris/generation/chunk/ChunkDataOutput.java b/core/src/main/java/art/arcane/iris/generation/chunk/ChunkDataOutput.java
new file mode 100644
index 000000000..7d4eb3e52
--- /dev/null
+++ b/core/src/main/java/art/arcane/iris/generation/chunk/ChunkDataOutput.java
@@ -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 .
+ */
+
+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);
+ }
+}
diff --git a/core/src/main/java/art/arcane/iris/generation/chunk/ColumnExtentListeningHunk.java b/core/src/main/java/art/arcane/iris/generation/chunk/ColumnExtentListeningHunk.java
index f2d32dd3b..0498d0973 100644
--- a/core/src/main/java/art/arcane/iris/generation/chunk/ColumnExtentListeningHunk.java
+++ b/core/src/main/java/art/arcane/iris/generation/chunk/ColumnExtentListeningHunk.java
@@ -20,7 +20,7 @@ package art.arcane.iris.generation.chunk;
import art.arcane.volmlib.util.hunk.Hunk;
-public final class ColumnExtentListeningHunk implements Hunk, ColumnExtent {
+public final class ColumnExtentListeningHunk implements Hunk, ColumnExtent, ColumnRunHunk {
private final Hunk source;
private final Listener listener;
@@ -35,6 +35,12 @@ public final class ColumnExtentListeningHunk implements Hunk, 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 implements Hunk, ColumnExtent
@FunctionalInterface
public interface Listener {
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);
+ }
+ }
}
}
diff --git a/core/src/main/java/art/arcane/iris/generation/chunk/ColumnRunHunk.java b/core/src/main/java/art/arcane/iris/generation/chunk/ColumnRunHunk.java
new file mode 100644
index 000000000..363e7a8a3
--- /dev/null
+++ b/core/src/main/java/art/arcane/iris/generation/chunk/ColumnRunHunk.java
@@ -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 .
+ */
+
+package art.arcane.iris.generation.chunk;
+
+import art.arcane.volmlib.util.hunk.Hunk;
+
+public interface ColumnRunHunk {
+ void fillColumnRun(int x, int z, int yFromInclusive, int yToInclusive, T value);
+
+ @SuppressWarnings("unchecked")
+ static void fillColumnRun(Hunk hunk, int x, int z, int yFromInclusive, int yToInclusive, T value) {
+ if (hunk instanceof ColumnRunHunk> runs) {
+ ((ColumnRunHunk) runs).fillColumnRun(x, z, yFromInclusive, yToInclusive, value);
+ return;
+ }
+ for (int y = yToInclusive; y >= yFromInclusive; y--) {
+ hunk.setRaw(x, y, z, value);
+ }
+ }
+}
diff --git a/core/src/main/java/art/arcane/iris/generation/chunk/SectionBuffer.java b/core/src/main/java/art/arcane/iris/generation/chunk/SectionBuffer.java
new file mode 100644
index 000000000..8a6898911
--- /dev/null
+++ b/core/src/main/java/art/arcane/iris/generation/chunk/SectionBuffer.java
@@ -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 .
+ */
+
+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, NativeSectionVolume, ColumnExtent, ColumnRunHunk {
+ 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) {
+ }
+}
diff --git a/core/src/main/java/art/arcane/iris/generation/concurrent/MultiBurst.java b/core/src/main/java/art/arcane/iris/generation/concurrent/MultiBurst.java
index 0969ba03d..66fe2961c 100644
--- a/core/src/main/java/art/arcane/iris/generation/concurrent/MultiBurst.java
+++ b/core/src/main/java/art/arcane/iris/generation/concurrent/MultiBurst.java
@@ -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
diff --git a/core/src/main/java/art/arcane/iris/generation/decoration/IrisOreBands.java b/core/src/main/java/art/arcane/iris/generation/decoration/IrisOreBands.java
index b70f0681e..a15ab4ca2 100644
--- a/core/src/main/java/art/arcane/iris/generation/decoration/IrisOreBands.java
+++ b/core/src/main/java/art/arcane/iris/generation/decoration/IrisOreBands.java
@@ -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;
}
diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyTileCache.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyTileCache.java
index bc8178418..7f025926b 100644
--- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyTileCache.java
+++ b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyTileCache.java
@@ -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 SHARED_TILES = Caffeine.newBuilder()
.maximumWeight(HydrologyCacheBudget.sharedStudioBytes())
@@ -499,6 +500,9 @@ public final class HydrologyTileCache implements AutoCloseable {
}
public Optional 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);
diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/CaveCarveScratch.java b/core/src/main/java/art/arcane/iris/generation/mantle/CaveCarveScratch.java
index dd456abf5..5fd6a127c 100644
--- a/core/src/main/java/art/arcane/iris/generation/mantle/CaveCarveScratch.java
+++ b/core/src/main/java/art/arcane/iris/generation/mantle/CaveCarveScratch.java
@@ -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];
}
}
diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/CaveCellKernel.java b/core/src/main/java/art/arcane/iris/generation/mantle/CaveCellKernel.java
new file mode 100644
index 000000000..6eb42c753
--- /dev/null
+++ b/core/src/main/java/art/arcane/iris/generation/mantle/CaveCellKernel.java
@@ -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;
+ }
+}
diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/CaveDensityGrid.java b/core/src/main/java/art/arcane/iris/generation/mantle/CaveDensityGrid.java
index 824523a64..eb49c899c 100644
--- a/core/src/main/java/art/arcane/iris/generation/mantle/CaveDensityGrid.java
+++ b/core/src/main/java/art/arcane/iris/generation/mantle/CaveDensityGrid.java
@@ -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 columns;
+ private final ConcurrentClockCache warpColumns;
+ private final ConcurrentHashMap 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 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 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;
}
diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/CaveDensityLattice.java b/core/src/main/java/art/arcane/iris/generation/mantle/CaveDensityLattice.java
new file mode 100644
index 000000000..10aadb38d
--- /dev/null
+++ b/core/src/main/java/art/arcane/iris/generation/mantle/CaveDensityLattice.java
@@ -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 .
+ */
+
+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);
+ }
+}
diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/CaveFieldKey.java b/core/src/main/java/art/arcane/iris/generation/mantle/CaveFieldKey.java
new file mode 100644
index 000000000..7278e09db
--- /dev/null
+++ b/core/src/main/java/art/arcane/iris/generation/mantle/CaveFieldKey.java
@@ -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 .
+ */
+
+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;
+ }
+}
diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/CaveFieldModuleState.java b/core/src/main/java/art/arcane/iris/generation/mantle/CaveFieldModuleState.java
index 66e60be24..c963f2af2 100644
--- a/core/src/main/java/art/arcane/iris/generation/mantle/CaveFieldModuleState.java
+++ b/core/src/main/java/art/arcane/iris/generation/mantle/CaveFieldModuleState.java
@@ -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;
}
}
diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/IrisCaveCarver3D.java b/core/src/main/java/art/arcane/iris/generation/mantle/IrisCaveCarver3D.java
index 8bce7fdc5..fa348442e 100644
--- a/core/src/main/java/art/arcane/iris/generation/mantle/IrisCaveCarver3D.java
+++ b/core/src/main/java/art/arcane/iris/generation/mantle/IrisCaveCarver3D.java
@@ -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 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 carving = ThreadLocal.withInitial(() -> false);
private final ThreadLocal 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 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 moduleFieldKeys(CaveFieldModuleState[] modules) {
+ List 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 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 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 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 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 modules) {
+ }
}
diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/MantleFloatingObjectComponent.java b/core/src/main/java/art/arcane/iris/generation/mantle/MantleFloatingObjectComponent.java
index 40e050675..c2d897124 100644
--- a/core/src/main/java/art/arcane/iris/generation/mantle/MantleFloatingObjectComponent.java
+++ b/core/src/main/java/art/arcane/iris/generation/mantle/MantleFloatingObjectComponent.java
@@ -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 sizeCache = new HashMap<>();
+ Map extents = new HashMap<>();
Set 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 placements, IrisData data,
- Map sizeCache,
- Set warnedLargeObjects) {
- if (placements == null) {
- return MINIMUM_PLACEMENT_RADIUS;
+ private final class PlacementScan {
+ private final IrisData data;
+ private final Map extents;
+ private final Set warnedLargeObjects;
+ private final double shrink;
+
+ private PlacementScan(IrisData data, Map extents, Set 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 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;
diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/MantleObjectComponent.java b/core/src/main/java/art/arcane/iris/generation/mantle/MantleObjectComponent.java
index f64111b18..913c726f8 100644
--- a/core/src/main/java/art/arcane/iris/generation/mantle/MantleObjectComponent.java
+++ b/core/src/main/java/art/arcane/iris/generation/mantle/MantleObjectComponent.java
@@ -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 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 warnedLargeObjects
- ) {
- KMap 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 placements,
- IrisDimension dimension,
- DataProvider dataProvider,
- KMap sizeCache,
- KSet 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 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 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 warnedLargeObjects;
+ private final Map extents;
+ private final Set caveVacuumModes;
+
+ private ReachScan(IrisDimension dimension, DataProvider dataProvider, KSet 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 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 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 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 variants = placement.getVariantObjects(dataProvider.getData());
+ if (variants == null) {
+ continue;
+ }
+ IrisObjectPlacement objectPlacement = placement.asPlacement();
+ KList 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 profiles(IrisObjectPlacement placement) {
+ KList 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 caveVacuumModes(IrisDimension dimension, DataProvider dataProvider) {
+ Set 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 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);
diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/MatterGenerator.java b/core/src/main/java/art/arcane/iris/generation/mantle/MatterGenerator.java
index 5b42014ed..19db47d82 100644
--- a/core/src/main/java/art/arcane/iris/generation/mantle/MatterGenerator.java
+++ b/core/src/main/java/art/arcane/iris/generation/mantle/MatterGenerator.java
@@ -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 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 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 outstandingTasks = null;
- List submittedTasks = multicore && !DISPATCHER.ownsCurrentThread()
- ? new ArrayList<>(COMPONENT_SUBMISSION_BATCH_SIZE)
+ List 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 enabledComponents() {
List components = new ArrayList<>();
for (MantlePass pass : getComponents()) {
@@ -376,7 +398,7 @@ public interface MatterGenerator {
int chunkX,
int chunkZ,
ChunkContext context,
- List 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 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 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 chunk, MantleComponent component, int chunkX, int chunkZ) {
+ }
+
record MatterPassPlan(MantlePass pass, int passChunkRadius, int downstreamBlockRadius) {
}
diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/PlacementReach.java b/core/src/main/java/art/arcane/iris/generation/mantle/PlacementReach.java
new file mode 100644
index 000000000..e7cf808b3
--- /dev/null
+++ b/core/src/main/java/art/arcane/iris/generation/mantle/PlacementReach.java
@@ -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 .
+ */
+
+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 blocks) {
+ int[] bounds = {Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE,
+ Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE};
+ VectorMap.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);
+ }
+ }
+}
diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/ChunkBlockUpdateListener.java b/core/src/main/java/art/arcane/iris/generation/runtime/ChunkBlockUpdateListener.java
index 2860edbd1..f060a53a6 100644
--- a/core/src/main/java/art/arcane/iris/generation/runtime/ChunkBlockUpdateListener.java
+++ b/core/src/main/java/art/arcane/iris/generation/runtime/ChunkBlockUpdateListener.java
@@ -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;
+ }
}
diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/EngineMetrics.java b/core/src/main/java/art/arcane/iris/generation/runtime/EngineMetrics.java
index 95d7e08fa..711ddf35b 100644
--- a/core/src/main/java/art/arcane/iris/generation/runtime/EngineMetrics.java
+++ b/core/src/main/java/art/arcane/iris/generation/runtime/EngineMetrics.java
@@ -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 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 stageTotals() {
+ Map 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());
+ }
+ }
}
diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/GenerationCounter.java b/core/src/main/java/art/arcane/iris/generation/runtime/GenerationCounter.java
new file mode 100644
index 000000000..df740c7f2
--- /dev/null
+++ b/core/src/main/java/art/arcane/iris/generation/runtime/GenerationCounter.java
@@ -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();
+ }
+}
diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java b/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java
index 494328d5e..c995c2e27 100644
--- a/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java
+++ b/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java
@@ -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)) {
diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java b/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java
index 2db689d42..709d50b7c 100644
--- a/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java
+++ b/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java
@@ -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 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() {
diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java
index bc0d71c76..a5df09420 100644
--- a/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java
+++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java
@@ -79,15 +79,20 @@ public class IrisCarveModifier extends EngineAssignedModifier
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
PrecisionStopwatch caveStopwatch = PrecisionStopwatch.start();
Mantle mantle = getEngine().getMantle().getMantle();
CaveInputs resolverState = new CaveInputs(getEngine());
- Long2ObjectOpenHashMap caveBiomeCache = new Long2ObjectOpenHashMap<>(2048);
IrisCarveScratch scratch = SCRATCH.get();
scratch.reset();
+ Long2ObjectOpenHashMap caveBiomeCache = scratch.caveBiomeCache;
CarveWallBuffer walls = scratch.walls;
CarveColumnMask[] columnMasks = scratch.columnMasks;
CarveColumnMask[] boundaryMasks = scratch.boundaryMasks;
@@ -1231,7 +1236,7 @@ public class IrisCarveModifier extends EngineAssignedModifier
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
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 caveBiomeCache, int x, int y, int z, CaveInputs resolverState) {
long key = BlockPosition.toLong(x, y, z);
IrisBiome cachedBiome = caveBiomeCache.get(key);
diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveScratch.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveScratch.java
index e75522d11..6dfd76d6f 100644
--- a/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveScratch.java
+++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveScratch.java
@@ -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 customBiomeCache = new HashMap<>();
+ final Long2ObjectOpenHashMap caveBiomeCache = new Long2ObjectOpenHashMap<>(4096);
int[] upperSurfaceHeights;
boolean customCaveBiomePresent;
@@ -56,6 +58,7 @@ final class IrisCarveScratch {
}
walls.clear();
customBiomeCache.clear();
+ caveBiomeCache.clear();
customCaveBiomePresent = false;
}
}
diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuator.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuator.java
index 7c39f3338..3e1ed620b 100644
--- a/core/src/main/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuator.java
+++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuator.java
@@ -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 h, ChunkContext context) {
this.x = x;
@@ -260,8 +270,12 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator blocks = null;
KList ceilingBlocks = null;
@@ -269,7 +283,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator= 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 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 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= 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= 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= chunkHeight - 1) {
@@ -420,11 +499,11 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator= 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 PLAN_ORDER =
+ Comparator.comparingInt(SubterrainPlan::priority).reversed().thenComparing(SubterrainPlan::id);
+
private final List definitions;
private final long seed;
private final int minY;
private final int maxYExclusive;
- private final ThreadLocal sampleTile = new ThreadLocal<>();
+ private final AtomicReferenceArray planCache = new AtomicReferenceArray<>(PLAN_CACHE_SLOTS);
+ private final AtomicReferenceArray tileCache = new AtomicReferenceArray<>(TILE_CACHE_SLOTS);
private final Predicate 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 result = new ArrayList<>();
- for (Definition definition : definitions) {
+ List 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 candidates, int x, int y, int z) {
@@ -127,10 +128,38 @@ public final class SubterrainPlanner {
return selected;
}
+ private List 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 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 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 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,
diff --git a/core/src/main/java/art/arcane/iris/pack/loading/IrisData.java b/core/src/main/java/art/arcane/iris/pack/loading/IrisData.java
index d7f2bd104..8188eb058 100644
--- a/core/src/main/java/art/arcane/iris/pack/loading/IrisData.java
+++ b/core/src/main/java/art/arcane/iris/pack/loading/IrisData.java
@@ -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 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 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;
+ }
+ }
}
diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/nms/INMSBinding.java b/core/src/main/java/art/arcane/iris/platform/bukkit/nms/INMSBinding.java
index d0d8b1cb6..41f69e15a 100644
--- a/core/src/main/java/art/arcane/iris/platform/bukkit/nms/INMSBinding.java
+++ b/core/src/main/java/art/arcane/iris/platform/bukkit/nms/INMSBinding.java
@@ -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;
}
diff --git a/core/src/main/java/art/arcane/iris/platform/generation/BukkitChunkGenerator.java b/core/src/main/java/art/arcane/iris/platform/generation/BukkitChunkGenerator.java
index 06de2087b..2fe8b87dd 100644
--- a/core/src/main/java/art/arcane/iris/platform/generation/BukkitChunkGenerator.java
+++ b/core/src/main/java/art/arcane/iris/platform/generation/BukkitChunkGenerator.java
@@ -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 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();
}
diff --git a/core/src/main/java/art/arcane/iris/world/pregen/PregenPerformanceProfile.java b/core/src/main/java/art/arcane/iris/world/pregen/PregenPerformanceProfile.java
index c5d5b10b2..3fc3c3b2c 100644
--- a/core/src/main/java/art/arcane/iris/world/pregen/PregenPerformanceProfile.java
+++ b/core/src/main/java/art/arcane/iris/world/pregen/PregenPerformanceProfile.java
@@ -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) {
diff --git a/core/src/test/java/art/arcane/iris/generation/block/PlatformStateRebindTest.java b/core/src/test/java/art/arcane/iris/generation/block/PlatformStateRebindTest.java
index a0af9659f..f149111e8 100644
--- a/core/src/test/java/art/arcane/iris/generation/block/PlatformStateRebindTest.java
+++ b/core/src/test/java/art/arcane/iris/generation/block/PlatformStateRebindTest.java
@@ -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));
diff --git a/core/src/test/java/art/arcane/iris/generation/chunk/ChunkDataHunkHolderTest.java b/core/src/test/java/art/arcane/iris/generation/chunk/ChunkDataHunkHolderTest.java
deleted file mode 100644
index 020f6db50..000000000
--- a/core/src/test/java/art/arcane/iris/generation/chunk/ChunkDataHunkHolderTest.java
+++ /dev/null
@@ -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 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 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 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);
- }
-}
diff --git a/core/src/test/java/art/arcane/iris/generation/chunk/ChunkDataOutputTest.java b/core/src/test/java/art/arcane/iris/generation/chunk/ChunkDataOutputTest.java
new file mode 100644
index 000000000..e98b40f8b
--- /dev/null
+++ b/core/src/test/java/art/arcane/iris/generation/chunk/ChunkDataOutputTest.java
@@ -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 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 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;
+ }
+}
diff --git a/core/src/test/java/art/arcane/iris/generation/chunk/SectionBufferTest.java b/core/src/test/java/art/arcane/iris/generation/chunk/SectionBufferTest.java
new file mode 100644
index 000000000..75ab6a014
--- /dev/null
+++ b/core/src/test/java/art/arcane/iris/generation/chunk/SectionBufferTest.java
@@ -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 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 heard = new ArrayList<>();
+ ColumnExtentListeningHunk 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 array = Hunk.newArrayHunk(16, 32, 16);
+ NativeBlockState stone = mock(NativeBlockState.class);
+ array.setRaw(2, 5, 2, stone);
+ List order = new ArrayList<>();
+ Hunk 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 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);
+ }
+}
diff --git a/core/src/test/java/art/arcane/iris/generation/concurrent/HydrologyBurstTest.java b/core/src/test/java/art/arcane/iris/generation/concurrent/HydrologyBurstTest.java
index 7aa6ababd..bd0aca932 100644
--- a/core/src/test/java/art/arcane/iris/generation/concurrent/HydrologyBurstTest.java
+++ b/core/src/test/java/art/arcane/iris/generation/concurrent/HydrologyBurstTest.java
@@ -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;
diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyTileCacheTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyTileCacheTest.java
index f9b670632..817a6c7e7 100644
--- a/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyTileCacheTest.java
+++ b/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyTileCacheTest.java
@@ -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 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);
diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/CaveCellKernelTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/CaveCellKernelTest.java
new file mode 100644
index 000000000..4df05e6af
--- /dev/null
+++ b/core/src/test/java/art/arcane/iris/generation/mantle/CaveCellKernelTest.java
@@ -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);
+ }
+}
diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DCellKernelTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DCellKernelTest.java
new file mode 100644
index 000000000..e0a4f8f57
--- /dev/null
+++ b/core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DCellKernelTest.java
@@ -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 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> chunks = new HashMap<>();
+ private final IdentityHashMap identities = new IdentityHashMap<>();
+ private final List 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 mantle = mock(Mantle.class, withSettings().stubOnly());
+ @SuppressWarnings("unchecked")
+ MantleChunk chunk = mock(MantleChunk.class, withSettings().stubOnly());
+ long key = ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL);
+ Map 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 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);
+ }
+ }
+}
diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DLatticeTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DLatticeTest.java
new file mode 100644
index 000000000..db789b87f
--- /dev/null
+++ b/core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DLatticeTest.java
@@ -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;
+ }
+}
diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DNearParityTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DNearParityTest.java
index ced1764da..cbaf46887 100644
--- a/core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DNearParityTest.java
+++ b/core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DNearParityTest.java
@@ -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);
}
}
diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/MantleFloatingObjectComponentBoundaryRadiusTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/MantleFloatingObjectComponentBoundaryRadiusTest.java
index 8f65ca73a..56dc96eaa 100644
--- a/core/src/test/java/art/arcane/iris/generation/mantle/MantleFloatingObjectComponentBoundaryRadiusTest.java
+++ b/core/src/test/java/art/arcane/iris/generation/mantle/MantleFloatingObjectComponentBoundaryRadiusTest.java
@@ -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 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());
}
}
diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/MantleObjectComponentBoundaryRadiusTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/MantleObjectComponentBoundaryRadiusTest.java
index 09467ff87..9e374d6f8 100644
--- a/core/src/test/java/art/arcane/iris/generation/mantle/MantleObjectComponentBoundaryRadiusTest.java
+++ b/core/src/test/java/art/arcane/iris/generation/mantle/MantleObjectComponentBoundaryRadiusTest.java
@@ -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 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 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 mantle = mock(Mantle.class);
MantleChunk 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 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 {
diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/MatterGeneratorConcurrencyTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/MatterGeneratorConcurrencyTest.java
index ad9cf6d22..d330fe25c 100644
--- a/core/src/test/java/art/arcane/iris/generation/mantle/MatterGeneratorConcurrencyTest.java
+++ b/core/src/test/java/art/arcane/iris/generation/mantle/MatterGeneratorConcurrencyTest.java
@@ -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 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 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 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 held = fixture.mantle.getChunk(1, 0);
+ CountDownLatch claimed = new CountDownLatch(1);
+ CountDownLatch releaseClaim = new CountDownLatch(1);
+ Set 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 holder = CompletableFuture.runAsync(() -> {
+ assertTrue(held.tryClaim(ReservedFlag.OBJECT));
+ claimed.countDown();
+ await(releaseClaim);
+ held.completeClaim(ReservedFlag.OBJECT);
+ });
+ await(claimed);
+ CompletableFuture 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 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;
}
diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/PlacementReachPackTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/PlacementReachPackTest.java
new file mode 100644
index 000000000..6ee5a3425
--- /dev/null
+++ b/core/src/test/java/art/arcane/iris/generation/mantle/PlacementReachPackTest.java
@@ -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 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 files;
+ try (Stream 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 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 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 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 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 positions = positions(variant);
+ List 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 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 positions(IrisObject object) {
+ List 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 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 proceduralPlacements() {
+ List 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 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 List listOrEmpty(KList 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 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 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);
+ }
+ }
+}
diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/PlacementReachTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/PlacementReachTest.java
new file mode 100644
index 000000000..f32449792
--- /dev/null
+++ b/core/src/test/java/art/arcane/iris/generation/mantle/PlacementReachTest.java
@@ -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 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 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 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 void setData(int xx, int yy, int zz, T data) {
+ write(xx, zz);
+ }
+
+ @Override
+ public T getData(int xx, int yy, int zz, Class t) {
+ read(xx, zz);
+ return null;
+ }
+
+ @Override
+ public Engine getEngine() {
+ return engine;
+ }
+ }
+}
diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/EngineMetricsStageTotalsTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/EngineMetricsStageTotalsTest.java
new file mode 100644
index 000000000..731687e79
--- /dev/null
+++ b/core/src/test/java/art/arcane/iris/generation/runtime/EngineMetricsStageTotalsTest.java
@@ -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 totals = metrics.stageTotals();
+
+ assertEquals(Set.copyOf(metrics.pull().keySet()), Set.copyOf(totals.keySet()));
+ assertEquals(metrics.pull().size(), totals.size());
+ }
+}
diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineGenerationRuntimeScopeTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineGenerationRuntimeScopeTest.java
index 2e351e5c1..9b0f9177b 100644
--- a/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineGenerationRuntimeScopeTest.java
+++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineGenerationRuntimeScopeTest.java
@@ -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 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 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,
diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierBiomeBlendTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierBiomeBlendTest.java
index 3a12db6f7..d0ab77646 100644
--- a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierBiomeBlendTest.java
+++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierBiomeBlendTest.java
@@ -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;
}
diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierBoundarySupportTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierBoundarySupportTest.java
index e0bbe9760..11b96dbce 100644
--- a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierBoundarySupportTest.java
+++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierBoundarySupportTest.java
@@ -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);
diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierCaveBlendRollTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierCaveBlendRollTest.java
new file mode 100644
index 000000000..fc018ff8c
--- /dev/null
+++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveModifierCaveBlendRollTest.java
@@ -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);
+ };
+ }
+}
diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveScratchTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveScratchTest.java
index b979d3f63..bfe93fb0c 100644
--- a/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveScratchTest.java
+++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisCarveScratchTest.java
@@ -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]++);
diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisNaturalCaveScanTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisNaturalCaveScanTest.java
index 34e934a63..1144815d2 100644
--- a/core/src/test/java/art/arcane/iris/generation/stage/IrisNaturalCaveScanTest.java
+++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisNaturalCaveScanTest.java
@@ -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 boundedWrites = new ArrayList<>();
bounded.modifier.decorateNaturalCaves(-32, 48, new ColumnExtentListeningHunk<>(holder,
diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisPerfectionModifierColumnTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisPerfectionModifierColumnTest.java
index ea1a7ec84..c91f8340e 100644
--- a/core/src/test/java/art/arcane/iris/generation/stage/IrisPerfectionModifierColumnTest.java
+++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisPerfectionModifierColumnTest.java
@@ -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();
diff --git a/core/src/test/java/art/arcane/iris/generation/subterrain/SubterrainPlannerTest.java b/core/src/test/java/art/arcane/iris/generation/subterrain/SubterrainPlannerTest.java
index 43c8b2378..6cb0f2535 100644
--- a/core/src/test/java/art/arcane/iris/generation/subterrain/SubterrainPlannerTest.java
+++ b/core/src/test/java/art/arcane/iris/generation/subterrain/SubterrainPlannerTest.java
@@ -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 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> 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 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);
diff --git a/core/src/test/java/art/arcane/iris/pack/loading/IrisDataCustomBiomeKeyTest.java b/core/src/test/java/art/arcane/iris/pack/loading/IrisDataCustomBiomeKeyTest.java
new file mode 100644
index 000000000..09924af9a
--- /dev/null
+++ b/core/src/test/java/art/arcane/iris/pack/loading/IrisDataCustomBiomeKeyTest.java
@@ -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 customs = data.getBiomeLoader().load("test").getCustomDerivitives();
+ assertEquals(2, customs.size());
+ List 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>> results = new ArrayList<>();
+ for (int thread = 0; thread < 8; thread++) {
+ results.add(pool.submit(() -> {
+ List 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> 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);
+ }
+}
diff --git a/core/src/test/java/art/arcane/iris/world/pregen/PregenPerformanceProfileTest.java b/core/src/test/java/art/arcane/iris/world/pregen/PregenPerformanceProfileTest.java
index 96c0e407f..21f574cc0 100644
--- a/core/src/test/java/art/arcane/iris/world/pregen/PregenPerformanceProfileTest.java
+++ b/core/src/test/java/art/arcane/iris/world/pregen/PregenPerformanceProfileTest.java
@@ -99,7 +99,7 @@ public class PregenPerformanceProfileTest {
}
@Test
- public void pregenerationWantsTwoBurstWorkersPerCore() {
+ public void pregenerationWantsTwoBurstWorkersPerPerformanceCore() {
assertEquals(32, PregenPerformanceProfile.pregenBurstParallelism(16));
assertEquals(4, PregenPerformanceProfile.pregenBurstParallelism(1));
}
diff --git a/probe/build.gradle b/probe/build.gradle
index bf17231f1..926526037 100644
--- a/probe/build.gradle
+++ b/probe/build.gradle
@@ -171,10 +171,19 @@ tasks.register('genProbe', JavaExec) {
systemProperty('iris.probe.expectedBackend', providers.gradleProperty('probeExpectedBackend').orElse('').get())
systemProperty('iris.probe.requireNativeSamples', providers.gradleProperty('probeRequireNativeSamples').orElse('false').get())
if (providers.gradleProperty('probeNativeLibrary').isPresent()) {
- systemProperty('volmlib.noise.nativeBillowLibrary', providers.gradleProperty('probeNativeLibrary').get())
+ systemProperty('volmlib.native.library', providers.gradleProperty('probeNativeLibrary').get())
+ }
+ if (providers.gradleProperty('probeNativePolicy').isPresent()) {
+ systemProperty('volmlib.native', providers.gradleProperty('probeNativePolicy').get())
}
systemProperty('iris.probe.threads', providers.gradleProperty('probeThreads').orElse('1').get())
systemProperty('iris.probe.pregenProfile', providers.gradleProperty('probePregenProfile').orElse('false').get())
+ systemProperty('iris.probe.hydrologyPreplan', providers.gradleProperty('probeHydrologyPreplan').orElse('false').get())
+ systemProperty('iris.probe.historyRouter', providers.gradleProperty('probeHistoryRouter').orElse('false').get())
+ systemProperty('iris.probe.productionBuffers', providers.gradleProperty('probeProductionBuffers').orElse('false').get())
+ String probeCounters = providers.gradleProperty('probeCounters').orElse('false').get()
+ systemProperty('iris.generation.counters', probeCounters)
+ systemProperty('volmlib.cache.countContention', probeCounters)
dependsOn(':core:compileJava')
doFirst {
if (!probePack.isPresent() || probePack.get().isBlank()) {
diff --git a/probe/src/main/java/art/arcane/iris/probe/ChunkDump.java b/probe/src/main/java/art/arcane/iris/probe/ChunkDump.java
index 5c3b1e85e..6d072caae 100644
--- a/probe/src/main/java/art/arcane/iris/probe/ChunkDump.java
+++ b/probe/src/main/java/art/arcane/iris/probe/ChunkDump.java
@@ -20,6 +20,7 @@ package art.arcane.iris.probe;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
+import art.arcane.volmlib.nativelib.terrain.NativeBlockVolume;
import art.arcane.volmlib.util.hunk.Hunk;
import java.io.BufferedInputStream;
@@ -84,11 +85,12 @@ public final class ChunkDump {
char[] biomeIndices = new char[size];
PaletteBuilder blockPalette = new PaletteBuilder();
PaletteBuilder biomePalette = new PaletteBuilder();
+ NativeBlockVolume stored = blocks instanceof NativeBlockVolume volume ? volume : null;
int index = 0;
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < height; y++) {
- NativeBlockState state = blocks.get(x, y, z);
+ NativeBlockState state = stored == null ? blocks.get(x, y, z) : stored.getStoredRaw(x, y, z);
NativeBiome biome = biomes.get(x, y, z);
blockIndices[index] = blockPalette.indexOf(state, state == null ? NULL_BLOCK_KEY : null);
biomeIndices[index] = biomePalette.indexOf(biome, biome == null ? NULL_BIOME_KEY : null);
diff --git a/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java b/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java
index d94371ffe..81b78ae28 100644
--- a/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java
+++ b/probe/src/main/java/art/arcane/iris/probe/GenerationProbe.java
@@ -20,6 +20,8 @@ package art.arcane.iris.probe;
import art.arcane.iris.generation.bake.BakePlane;
import art.arcane.iris.generation.bake.GeneratorEvaluations;
+import art.arcane.iris.generation.mantle.IrisCaveCarver3D;
+import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.PackValidationResult;
import art.arcane.iris.pack.PackValidator;
@@ -40,6 +42,9 @@ import art.arcane.iris.generation.hydrology.HydrologyTileCache;
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.world.IrisWorld;
+import art.arcane.iris.world.history.GenerationHistory;
+import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
+import art.arcane.iris.world.history.IrisBoundarySignatureSampler;
import art.arcane.iris.world.pregen.PregenPerformanceProfile;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
@@ -63,6 +68,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -170,7 +176,8 @@ public final class GenerationProbe {
}
}
- record ProbeOptions(int threads, Path dumpFile, boolean pregenProfile) {
+ record ProbeOptions(int threads, Path dumpFile, boolean pregenProfile, boolean hydrologyPreplan,
+ boolean historyRouter, boolean productionBuffers, boolean counters) {
ProbeOptions {
if (threads < 1) {
throw new IllegalArgumentException("Thread count must be at least 1.");
@@ -185,7 +192,15 @@ public final class GenerationProbe {
return new ProbeOptions(
Integer.parseInt(System.getProperty("iris.probe.threads", "1").trim()),
dump.isEmpty() ? null : Path.of(dump),
- Boolean.parseBoolean(System.getProperty("iris.probe.pregenProfile", "false").trim()));
+ booleanProperty("iris.probe.pregenProfile"),
+ booleanProperty("iris.probe.hydrologyPreplan"),
+ booleanProperty("iris.probe.historyRouter"),
+ booleanProperty("iris.probe.productionBuffers"),
+ booleanProperty("iris.generation.counters"));
+ }
+
+ private static boolean booleanProperty(String key) {
+ return Boolean.parseBoolean(System.getProperty(key, "false").trim());
}
boolean parallel() {
@@ -304,6 +319,7 @@ public final class GenerationProbe {
try {
ProbeResult result = run(configuration, options);
System.out.println(result.machineLine());
+ System.out.println(IrisCaveCarver3D.cellCounters().machineLine());
exitCode = result.failedChunks() == 0 ? 0 : 1;
} catch (Throwable e) {
System.out.println("[genprobe] FAIL: probe execution failed");
@@ -356,7 +372,7 @@ public final class GenerationProbe {
Throwable executionFailure = null;
try {
File pack = clonePack(configuration.packSource(), workRoot);
- configureProbeRuntime(workRoot);
+ configureProbeRuntime(workRoot, options.historyRouter());
validatePack(pack);
System.out.println("[genprobe] pack: " + configuration.packSource().getAbsolutePath());
@@ -369,29 +385,48 @@ public final class GenerationProbe {
System.out.println("[genprobe] studio: " + configuration.studio());
System.out.println("[genprobe] threads: " + options.threads());
System.out.println("[genprobe] pregen profile: " + options.pregenProfile());
+ System.out.println("[genprobe] hydrology preplan: " + options.hydrologyPreplan());
+ System.out.println("[genprobe] history router: " + options.historyRouter());
+ System.out.println("[genprobe] production buffers: " + options.productionBuffers());
+ System.out.println("[genprobe] counters: " + options.counters());
System.out.println("[genprobe] dump file: " + (options.dumpFile() == null ? "none" : options.dumpFile()));
long engineStart = System.nanoTime();
+ File worldFolder = new File(workRoot, "world");
data = IrisData.get(pack);
- IrisDimension dimension = data.getDimensionLoader().load(configuration.dimensionKey());
- if (dimension == null) {
- throw new IllegalStateException("Dimension '" + configuration.dimensionKey()
- + "' did not load from " + pack.getAbsolutePath());
+ IrisDimension dimension = loadDimension(data, configuration.dimensionKey());
+ GenerationHistory history = null;
+ if (options.historyRouter()) {
+ history = HeadlessGenerationHistorySession.create(new RealPackProbeSupport.HistoryRequest(
+ worldFolder.toPath(), data, dimension, configuration.seed()), DataVersion.getLatest().get());
+ data.close();
+ data = IrisData.openRuntime(history.activePackRoot().toFile());
+ data.bindGenerationRegistryContract(history.activeEpoch().registryContract());
+ dimension = loadDimension(data, configuration.dimensionKey());
}
IrisWorld world = IrisWorld.builder()
.platformIdentity("iris:probe")
.name("probe")
.seed(configuration.seed())
- .worldFolder(new File(workRoot, "world"))
+ .worldFolder(worldFolder)
.minHeight(dimension.getMinHeight())
.maxHeight(dimension.getMaxHeight())
.build();
EngineTarget target = new EngineTarget(world, dimension, data);
- engine = new IrisEngine(
- target,
- configuration.studio()
- ? IrisEngine.InitializationMode.STUDIO
- : IrisEngine.InitializationMode.RUNTIME);
+ IrisEngine.InitializationMode mode = configuration.studio()
+ ? IrisEngine.InitializationMode.STUDIO
+ : IrisEngine.InitializationMode.RUNTIME;
+ GenerationHistoryRuntimeRouter router = null;
+ if (history == null) {
+ engine = new IrisEngine(target, mode);
+ } else {
+ IrisEngine historyEngine = new IrisEngine(target, mode,
+ history.paths().activationMantleRoot(history.activeActivation().activationId()),
+ history.activeEpoch().kernelVersion(), null);
+ engine = historyEngine;
+ router = GenerationHistoryRuntimeRouter.attach(historyEngine, history, IrisBoundarySignatureSampler.INSTANCE);
+ router.preloadActiveRuntimes();
+ }
long engineReadyNanos = System.nanoTime() - engineStart;
List initNoise = settleAndDrain();
@@ -400,9 +435,16 @@ public final class GenerationProbe {
+ " seed=" + engine.getSeedManager().getSeed()
+ " minY=" + engine.getMinHeight() + " maxY=" + engine.getMaxHeight()
+ " timeMs=" + String.format(Locale.ROOT, "%.3f", nanosToMillis(engineReadyNanos)));
+ System.out.println("IRIS_GENPROBE_MANTLE version=1 radius=" + engine.getMantle().getRadius()
+ + " real_radius=" + engine.getMantle().getRealRadius());
+ ProbeChunkGenerator generator = new ProbeChunkGenerator(engine, router, options.productionBuffers());
long nativeSamplesBefore = FractalBillowSimplexNoise.nativeSampleCount();
- GenerationResult generation = generateWithProfile(engine, configuration, options);
+ GenerationResult generation = generateWithProfile(generator, configuration, options);
verifyBackend(nativeSamplesBefore);
+ if (options.productionBuffers()) {
+ System.out.println("IRIS_GENPROBE_BUFFERS version=1 mode=production biome_expanded_chunks="
+ + generator.expandedBiomeHunks());
+ }
String status = generation.failedChunks() == 0 ? "PASS" : "FAIL";
printGenerationFailures(generation);
return new ProbeResult(
@@ -447,7 +489,16 @@ public final class GenerationProbe {
}
}
- private static void configureProbeRuntime(File workRoot) {
+ private static IrisDimension loadDimension(IrisData data, String dimensionKey) {
+ IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
+ if (dimension == null) {
+ throw new IllegalStateException("Dimension '" + dimensionKey + "' did not load from "
+ + data.getDataFolder().getAbsolutePath());
+ }
+ return dimension;
+ }
+
+ private static void configureProbeRuntime(File workRoot, boolean generationRegistry) {
REPORTED.clear();
StubPlatform.bindGenerationStateHandlers();
StubPlatform.verbose(false);
@@ -458,7 +509,8 @@ public final class GenerationProbe {
IrisServices.register(EngineEffectsProvider.class, (EngineEffectsProvider) InertEffects::new);
IrisServices.register(EnginePlatformHooks.class, new InertPlatformHooks());
IrisPlatforms.unbind();
- IrisPlatforms.bind(new StubPlatform(new File(workRoot, "platform-data")));
+ IrisPlatforms.bind(new StubPlatform(new File(workRoot, "platform-data"),
+ generationRegistry ? ProbeGenerationRegistry.INSTANCE : null));
}
private static void validatePack(File pack) {
@@ -475,21 +527,35 @@ public final class GenerationProbe {
System.out.println("[genprobe] offline pack validation: PASS");
}
- private static GenerationResult generateWithProfile(Engine engine, ProbeConfiguration configuration,
+ private static GenerationResult generateWithProfile(ProbeChunkGenerator generator, ProbeConfiguration configuration,
ProbeOptions options) throws Exception {
+ Engine engine = generator.engine();
if (!options.pregenProfile()) {
- return generate(engine, configuration, options);
+ preplanHydrology(engine, configuration, options);
+ return generate(generator, configuration, options);
}
PregenPerformanceProfile.apply(engine);
IrisHydrologyRuntime hydrology = engine.getComplex().getHydrologyRuntime();
try (HydrologyTileCache.PregenerationScope ignored = hydrology == null ? null
: hydrology.preparePregeneration(pregenerationArea(configuration))) {
- return generate(engine, configuration, options);
+ preplanHydrology(engine, configuration, options);
+ return generate(generator, configuration, options);
} finally {
PregenPerformanceProfile.restore();
}
}
+ private static void preplanHydrology(Engine engine, ProbeConfiguration configuration, ProbeOptions options)
+ throws Exception {
+ if (!options.hydrologyPreplan()) {
+ return;
+ }
+ Optional preplan = ProbeHydrologyPreplan.run(engine, configuration);
+ printDistinctCauses("hydrology preplan reported errors", drainReported());
+ System.out.println(preplan.map(ProbeHydrologyPreplan::machineLine)
+ .orElse("IRIS_GENPROBE_HYDROLOGY_PREPLAN version=1 tiles=0 skipped=no-hydrology"));
+ }
+
static HydrologyTileCache.PregenerationArea pregenerationArea(ProbeConfiguration configuration) {
List coordinates = scheduleCoordinates(
configuration.warmupChunks() + configuration.measuredChunks(),
@@ -533,8 +599,9 @@ public final class GenerationProbe {
return Math.atan2(coordinate.z() - centerChunkZ, coordinate.x() - centerChunkX);
}
- private static GenerationResult generate(Engine engine, ProbeConfiguration configuration,
+ private static GenerationResult generate(ProbeChunkGenerator generator, ProbeConfiguration configuration,
ProbeOptions options) throws Exception {
+ Engine engine = generator.engine();
int totalChunks = configuration.warmupChunks() + configuration.measuredChunks();
List coordinates = scheduleCoordinates(
totalChunks, configuration.centerChunkX(), configuration.centerChunkZ());
@@ -543,31 +610,44 @@ public final class GenerationProbe {
engine.getMinHeight(), height);
try (OutcomeSink sink = new OutcomeSink(options.dumpFile(), header, configuration.measuredChunks())) {
if (options.parallel()) {
- generateParallel(engine, configuration, options, coordinates, sink);
+ generateParallel(generator, configuration, options, coordinates, sink);
} else {
- generateSequential(engine, configuration, coordinates, sink);
+ generateSequential(generator, configuration, options, coordinates, sink);
}
return sink.result(configuration);
}
}
- private static void generateSequential(Engine engine, ProbeConfiguration configuration,
- List coordinates, OutcomeSink sink) throws IOException {
+ private static void generateSequential(ProbeChunkGenerator generator, ProbeConfiguration configuration,
+ ProbeOptions options, List coordinates,
+ OutcomeSink sink) throws IOException {
int totalChunks = coordinates.size();
long measuredStartEpochMillis = 0L;
+ ProbeWindowMetrics measuredStart = null;
GeneratorEvaluations.Snapshot measuredStartEvaluations = GeneratorEvaluations.snapshot();
for (int index = 0; index < totalChunks; index++) {
if (index == configuration.warmupChunks()) {
measuredStartEpochMillis = System.currentTimeMillis();
measuredStartEvaluations = GeneratorEvaluations.snapshot();
+ measuredStart = ProbeWindowMetrics.capture(generator.engine(), options.counters());
}
drainReported();
- ChunkOutcome outcome = generateChunk(engine, index, coordinates.get(index), configuration.multicore());
+ ChunkOutcome outcome = generator.generate(index, coordinates.get(index), configuration.multicore());
outcome.failures().addAll(drainReported());
sink.accept(outcome, index >= configuration.warmupChunks());
printProgress(configuration, index + 1, totalChunks);
}
- printWindow(engine, measuredStartEpochMillis, System.currentTimeMillis(), measuredStartEvaluations);
+ printWindow(generator.engine(), measuredStartEpochMillis, System.currentTimeMillis(), measuredStartEvaluations);
+ printWindowMetrics(measuredStart, ProbeWindowMetrics.capture(generator.engine(), options.counters()),
+ configuration.measuredChunks(), options);
+ }
+
+ private static void printWindowMetrics(ProbeWindowMetrics start, ProbeWindowMetrics end, int chunks,
+ ProbeOptions options) {
+ System.out.println(ProbeWindowMetrics.stagesLine(start, end, chunks, options.threads()));
+ if (options.counters()) {
+ System.out.println(ProbeWindowMetrics.countersLine(start, end, chunks));
+ }
}
private static void printWindow(Engine engine, long startEpochMillis, long endEpochMillis, GeneratorEvaluations.Snapshot startEvaluations) {
@@ -585,8 +665,9 @@ public final class GenerationProbe {
System.out.println(planes);
}
- private static void generateParallel(Engine engine, ProbeConfiguration configuration, ProbeOptions options,
- List coordinates, OutcomeSink sink) throws Exception {
+ private static void generateParallel(ProbeChunkGenerator generator, ProbeConfiguration configuration,
+ ProbeOptions options, List coordinates,
+ OutcomeSink sink) throws Exception {
ChunkOutcome[] outcomes = new ChunkOutcome[coordinates.size()];
ExecutorService executor = Executors.newFixedThreadPool(options.threads());
long warmupWallNanos;
@@ -599,7 +680,7 @@ public final class GenerationProbe {
warmup.add(index);
}
long warmupStarted = System.nanoTime();
- runPhase(executor, engine, configuration, coordinates, warmup, outcomes);
+ runPhase(executor, generator, configuration, coordinates, warmup, outcomes);
warmupWallNanos = System.nanoTime() - warmupStarted;
unattributed.addAll(drainReported());
System.out.println("[genprobe] warmup complete: " + configuration.warmupChunks() + " chunks on "
@@ -607,12 +688,15 @@ public final class GenerationProbe {
List measured = spiralOrder(coordinates, configuration.warmupChunks(),
configuration.centerChunkX(), configuration.centerChunkZ());
+ ProbeWindowMetrics measuredStart = ProbeWindowMetrics.capture(generator.engine(), options.counters());
long measuredStartEpochMillis = System.currentTimeMillis();
GeneratorEvaluations.Snapshot measuredStartEvaluations = GeneratorEvaluations.snapshot();
long measuredStarted = System.nanoTime();
- runPhase(executor, engine, configuration, coordinates, measured, outcomes);
+ runPhase(executor, generator, configuration, coordinates, measured, outcomes);
measuredWallNanos = System.nanoTime() - measuredStarted;
- printWindow(engine, measuredStartEpochMillis, System.currentTimeMillis(), measuredStartEvaluations);
+ printWindow(generator.engine(), measuredStartEpochMillis, System.currentTimeMillis(), measuredStartEvaluations);
+ printWindowMetrics(measuredStart, ProbeWindowMetrics.capture(generator.engine(), options.counters()),
+ configuration.measuredChunks(), options);
unattributed.addAll(drainReported());
System.out.println("[genprobe] measured progress: " + configuration.measuredChunks() + "/"
+ configuration.measuredChunks());
@@ -646,13 +730,13 @@ public final class GenerationProbe {
System.out.println(throughput.machineLine());
}
- private static void runPhase(ExecutorService executor, Engine engine, ProbeConfiguration configuration,
- List coordinates, List order,
- ChunkOutcome[] outcomes) throws Exception {
+ private static void runPhase(ExecutorService executor, ProbeChunkGenerator generator,
+ ProbeConfiguration configuration, List coordinates,
+ List order, ChunkOutcome[] outcomes) throws Exception {
List> futures = new ArrayList<>(order.size());
for (int index : order) {
ChunkCoordinate coordinate = coordinates.get(index);
- futures.add(executor.submit(() -> generateChunk(engine, index, coordinate, configuration.multicore())));
+ futures.add(executor.submit(() -> generator.generate(index, coordinate, configuration.multicore())));
}
for (Future future : futures) {
ChunkOutcome outcome;
@@ -672,21 +756,6 @@ public final class GenerationProbe {
}
}
- private static ChunkOutcome generateChunk(Engine engine, int index, ChunkCoordinate coordinate,
- boolean multicore) {
- int height = engine.getTarget().getHeight();
- List failures = new ArrayList<>();
- Hunk blocks = Hunk.newArrayHunk(16, height, 16);
- Hunk biomes = Hunk.newArrayHunk(16, height, 16);
- long started = System.nanoTime();
- try {
- engine.generate(coordinate.x() << 4, coordinate.z() << 4, blocks, biomes, multicore);
- } catch (Throwable e) {
- failures.add(e);
- }
- return new ChunkOutcome(index, coordinate, System.nanoTime() - started, blocks, biomes, failures);
- }
-
private static void printProgress(ProbeConfiguration configuration, int completed, int totalChunks) {
if (completed == configuration.warmupChunks()) {
System.out.println("[genprobe] warmup complete: " + configuration.warmupChunks() + " chunks");
diff --git a/probe/src/main/java/art/arcane/iris/probe/ProbeChunkGenerator.java b/probe/src/main/java/art/arcane/iris/probe/ProbeChunkGenerator.java
new file mode 100644
index 000000000..64db59352
--- /dev/null
+++ b/probe/src/main/java/art/arcane/iris/probe/ProbeChunkGenerator.java
@@ -0,0 +1,99 @@
+/*
+ * Iris is a World Generator for Minecraft 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 .
+ */
+
+package art.arcane.iris.probe;
+
+import art.arcane.iris.generation.chunk.SectionBuffer;
+import art.arcane.iris.generation.context.IrisContext;
+import art.arcane.iris.generation.runtime.Engine;
+import art.arcane.iris.generation.runtime.GenerationSessionLease;
+import art.arcane.iris.spi.IrisPlatforms;
+import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
+import art.arcane.volmlib.nativelib.terrain.NativeBiome;
+import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
+import art.arcane.volmlib.util.hunk.Hunk;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+final class ProbeChunkGenerator {
+ private final Engine engine;
+ private final GenerationHistoryRuntimeRouter router;
+ private final boolean productionBuffers;
+ private final AtomicInteger expandedBiomeHunks = new AtomicInteger();
+
+ ProbeChunkGenerator(Engine engine, GenerationHistoryRuntimeRouter router, boolean productionBuffers) {
+ this.engine = engine;
+ this.router = router;
+ this.productionBuffers = productionBuffers;
+ }
+
+ Engine engine() {
+ return engine;
+ }
+
+ int expandedBiomeHunks() {
+ return expandedBiomeHunks.get();
+ }
+
+ GenerationProbe.ChunkOutcome generate(int index, GenerationProbe.ChunkCoordinate coordinate, boolean multicore) {
+ int height = engine.getTarget().getHeight();
+ List failures = new ArrayList<>();
+ Hunk blocks = newBlocks(height);
+ Hunk biomes = newBiomes(height);
+ long started = System.nanoTime();
+ try {
+ generateInto(coordinate.x() << 4, coordinate.z() << 4, blocks, biomes, multicore);
+ } catch (Throwable e) {
+ failures.add(e);
+ }
+ long elapsed = System.nanoTime() - started;
+ if (biomes instanceof ProductionBiomeHunk production && production.expanded()) {
+ expandedBiomeHunks.incrementAndGet();
+ }
+ return new GenerationProbe.ChunkOutcome(index, coordinate, elapsed, blocks, biomes, failures);
+ }
+
+ private Hunk newBlocks(int height) {
+ return productionBuffers ? new SectionBuffer(height) : Hunk.newArrayHunk(16, height, 16);
+ }
+
+ private Hunk newBiomes(int height) {
+ return productionBuffers ? new ProductionBiomeHunk(height, ProbeChunkGenerator::plainsBiome)
+ : Hunk.newArrayHunk(16, height, 16);
+ }
+
+ private void generateInto(int blockX, int blockZ, Hunk blocks, Hunk biomes,
+ boolean multicore) throws Exception {
+ if (router == null) {
+ engine.generate(blockX, blockZ, blocks, biomes, multicore);
+ return;
+ }
+ try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = router.openCoordinateScope(blockX, blockZ);
+ GenerationSessionLease lease = engine.acquireGenerationLease("probe_terrain_stage");
+ IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
+ engine.generate(blockX, blockZ, blocks, biomes, multicore);
+ historyScope.claimGeneratedSemantics();
+ }
+ }
+
+ private static NativeBiome plainsBiome() {
+ return IrisPlatforms.get().registries().biome("minecraft:plains");
+ }
+}
diff --git a/probe/src/main/java/art/arcane/iris/probe/ProbeGenerationRegistry.java b/probe/src/main/java/art/arcane/iris/probe/ProbeGenerationRegistry.java
new file mode 100644
index 000000000..644845015
--- /dev/null
+++ b/probe/src/main/java/art/arcane/iris/probe/ProbeGenerationRegistry.java
@@ -0,0 +1,51 @@
+/*
+ * Iris is a World Generator for Minecraft 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 .
+ */
+
+package art.arcane.iris.probe;
+
+import art.arcane.iris.spi.PlatformGenerationRegistry;
+
+import java.util.List;
+import java.util.Locale;
+
+final class ProbeGenerationRegistry implements PlatformGenerationRegistry {
+ static final ProbeGenerationRegistry INSTANCE = new ProbeGenerationRegistry();
+
+ private ProbeGenerationRegistry() {
+ }
+
+ @Override
+ public String runtimeIdentity() {
+ return "iris-probe";
+ }
+
+ @Override
+ public List legacyCustomBiomeResourceKeys(String packName, String dimensionKey, String customBiomeId) {
+ return List.of(dimensionKey.toLowerCase(Locale.ROOT) + ":" + customBiomeId.toLowerCase(Locale.ROOT));
+ }
+
+ @Override
+ public String dimensionTypeResourceKey(String packName, String dimensionKey, String dimensionTypeKey) {
+ return "iris:" + dimensionTypeKey.toLowerCase(Locale.ROOT);
+ }
+
+ @Override
+ public Definition definition(String registryKey, String resourceKey) {
+ return Definition.resourceIdentity(registryKey, resourceKey);
+ }
+}
diff --git a/probe/src/main/java/art/arcane/iris/probe/ProbeHydrologyPreplan.java b/probe/src/main/java/art/arcane/iris/probe/ProbeHydrologyPreplan.java
new file mode 100644
index 000000000..3df8d2bd2
--- /dev/null
+++ b/probe/src/main/java/art/arcane/iris/probe/ProbeHydrologyPreplan.java
@@ -0,0 +1,109 @@
+/*
+ * Iris is a World Generator for Minecraft 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 .
+ */
+
+package art.arcane.iris.probe;
+
+import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings;
+import art.arcane.iris.generation.hydrology.HydrologyTileCache;
+import art.arcane.iris.generation.hydrology.HydrologyTileKey;
+import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
+import art.arcane.iris.generation.runtime.Engine;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+record ProbeHydrologyPreplan(int tiles, int tileSize, int publicationRadius, int reachChunks, long elapsedNanos) {
+ private static final int PLANNING_THREADS = 4;
+
+ static Optional run(Engine engine, GenerationProbe.ProbeConfiguration configuration)
+ throws Exception {
+ IrisHydrologyRuntime hydrology = engine.getComplex().getHydrologyRuntime();
+ if (hydrology == null) {
+ return Optional.empty();
+ }
+ HydrologyPlannerSettings settings = hydrology.settings();
+ int tileSize = settings.routing().tileSize();
+ int publicationRadius = settings.publicationRadius();
+ int reachChunks = Math.max(engine.getMantle().getRadius(), engine.getMantle().getRealRadius());
+ List keys = tiles(configuration, tileSize, publicationRadius, reachChunks);
+ long started = System.nanoTime();
+ ExecutorService executor = Executors.newFixedThreadPool(Math.min(keys.size(), PLANNING_THREADS));
+ try {
+ List> plans = new ArrayList<>(keys.size());
+ for (HydrologyTileKey key : keys) {
+ plans.add(executor.submit(() -> hydrology.tile(key)));
+ }
+ for (Future> plan : plans) {
+ await(plan);
+ }
+ } finally {
+ executor.shutdown();
+ }
+ return Optional.of(new ProbeHydrologyPreplan(keys.size(), tileSize, publicationRadius, reachChunks,
+ System.nanoTime() - started));
+ }
+
+ static List tiles(GenerationProbe.ProbeConfiguration configuration, int tileSize,
+ int publicationRadius, int reachChunks) {
+ HydrologyTileCache.PregenerationArea area = GenerationProbe.pregenerationArea(configuration);
+ long reach = (long) reachChunks * 16L + publicationRadius;
+ int minimumX = Math.toIntExact(Math.floorDiv(area.minimumBlockX() - reach, tileSize)) - 1;
+ int minimumZ = Math.toIntExact(Math.floorDiv(area.minimumBlockZ() - reach, tileSize)) - 1;
+ int maximumX = Math.toIntExact(Math.floorDiv(area.maximumBlockX() + reach, tileSize)) + 1;
+ int maximumZ = Math.toIntExact(Math.floorDiv(area.maximumBlockZ() + reach, tileSize)) + 1;
+ int centerX = Math.floorDiv(area.centerBlockX(), tileSize);
+ int centerZ = Math.floorDiv(area.centerBlockZ(), tileSize);
+ List keys = new ArrayList<>((maximumX - minimumX + 1) * (maximumZ - minimumZ + 1));
+ for (int tileZ = minimumZ; tileZ <= maximumZ; tileZ++) {
+ for (int tileX = minimumX; tileX <= maximumX; tileX++) {
+ keys.add(new HydrologyTileKey(tileX, tileZ));
+ }
+ }
+ keys.sort(Comparator.comparingInt((HydrologyTileKey key) ->
+ Math.max(Math.abs(key.tileX() - centerX), Math.abs(key.tileZ() - centerZ))));
+ return keys;
+ }
+
+ String machineLine() {
+ return String.format(Locale.ROOT,
+ "IRIS_GENPROBE_HYDROLOGY_PREPLAN version=1 tiles=%d tile_size=%d publication_radius=%d reach_chunks=%d planned_ms=%.3f",
+ tiles, tileSize, publicationRadius, reachChunks, elapsedNanos / 1_000_000D);
+ }
+
+ private static void await(Future> plan) throws Exception {
+ try {
+ plan.get();
+ } catch (ExecutionException failure) {
+ Throwable cause = failure.getCause();
+ if (cause instanceof Error error) {
+ throw error;
+ }
+ if (cause instanceof Exception exception) {
+ throw exception;
+ }
+ throw new IllegalStateException(cause);
+ }
+ }
+}
diff --git a/probe/src/main/java/art/arcane/iris/probe/ProbeWindowMetrics.java b/probe/src/main/java/art/arcane/iris/probe/ProbeWindowMetrics.java
new file mode 100644
index 000000000..bc5872682
--- /dev/null
+++ b/probe/src/main/java/art/arcane/iris/probe/ProbeWindowMetrics.java
@@ -0,0 +1,67 @@
+/*
+ * Iris is a World Generator for Minecraft 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 .
+ */
+
+package art.arcane.iris.probe;
+
+import art.arcane.iris.generation.runtime.Engine;
+import art.arcane.iris.generation.runtime.EngineMetrics;
+import art.arcane.iris.generation.runtime.GenerationCounter;
+import art.arcane.volmlib.util.cache.CacheContention;
+
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+
+record ProbeWindowMetrics(Map stages, Map counters) {
+ static ProbeWindowMetrics capture(Engine engine, boolean counters) {
+ return new ProbeWindowMetrics(engine.getMetrics().stageTotals(), counters ? counterValues() : Map.of());
+ }
+
+ static String stagesLine(ProbeWindowMetrics start, ProbeWindowMetrics end, int chunks, int threads) {
+ StringBuilder line = new StringBuilder(512);
+ line.append("IRIS_GENPROBE_STAGES version=1 chunks=").append(chunks).append(" threads=").append(threads);
+ for (Map.Entry entry : end.stages().entrySet()) {
+ EngineMetrics.StageTotal earlier = start.stages().get(entry.getKey());
+ EngineMetrics.StageTotal window = earlier == null ? entry.getValue() : entry.getValue().since(earlier);
+ line.append(' ').append(entry.getKey()).append('=')
+ .append(String.format(Locale.ROOT, "%.3f", window.millis()));
+ }
+ return line.toString();
+ }
+
+ static String countersLine(ProbeWindowMetrics start, ProbeWindowMetrics end, int chunks) {
+ StringBuilder line = new StringBuilder(256);
+ line.append("IRIS_GENPROBE_COUNTERS version=1 chunks=").append(chunks);
+ for (Map.Entry entry : end.counters().entrySet()) {
+ long window = entry.getValue() - start.counters().getOrDefault(entry.getKey(), 0L);
+ line.append(' ').append(entry.getKey()).append('=').append(window)
+ .append(' ').append(entry.getKey()).append("_per_chunk=")
+ .append(String.format(Locale.ROOT, "%.1f", chunks == 0 ? 0D : window / (double) chunks));
+ }
+ return line.toString();
+ }
+
+ private static Map counterValues() {
+ Map values = new LinkedHashMap<>();
+ for (GenerationCounter counter : GenerationCounter.values()) {
+ values.put(counter.key(), counter.sum());
+ }
+ values.put("cache_duplicate_computations", CacheContention.duplicateComputations());
+ return values;
+ }
+}
diff --git a/probe/src/main/java/art/arcane/iris/probe/ProductionBiomeHunk.java b/probe/src/main/java/art/arcane/iris/probe/ProductionBiomeHunk.java
new file mode 100644
index 000000000..dbd0be877
--- /dev/null
+++ b/probe/src/main/java/art/arcane/iris/probe/ProductionBiomeHunk.java
@@ -0,0 +1,112 @@
+/*
+ * Iris is a World Generator for Minecraft 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 .
+ */
+
+package art.arcane.iris.probe;
+
+import art.arcane.volmlib.nativelib.terrain.NativeBiome;
+import art.arcane.volmlib.util.hunk.HunkMutationSupport;
+import art.arcane.volmlib.util.hunk.storage.StorageHunk;
+
+import java.util.function.Supplier;
+
+final class ProductionBiomeHunk extends StorageHunk {
+ private static final int CHUNK_SIZE = 16;
+
+ private final NativeBiome[] columns = new NativeBiome[CHUNK_SIZE * CHUNK_SIZE];
+ private final Supplier fallback;
+ private NativeBiome[] cells;
+ private volatile NativeBiome defaultBiome;
+
+ ProductionBiomeHunk(int height, Supplier fallback) {
+ super(CHUNK_SIZE, height, CHUNK_SIZE);
+ this.fallback = fallback;
+ }
+
+ @Override
+ public void set(int x1, int y1, int z1, int x2, int y2, int z2, NativeBiome biome) {
+ if (x1 == x2 && z1 == z2 && y1 == 0 && y2 == getHeight() - 1) {
+ fillColumn(x1, z1, biome);
+ return;
+ }
+ HunkMutationSupport.setRangeInclusive(this, x1, y1, z1, x2, y2, z2, biome);
+ }
+
+ @Override
+ public void setRaw(int x, int y, int z, NativeBiome biome) {
+ NativeBiome[] storage = cells == null ? expand() : cells;
+ storage[cellIndex(x, y, z)] = biome;
+ }
+
+ @Override
+ public NativeBiome getRaw(int x, int y, int z) {
+ NativeBiome[] storage = cells;
+ NativeBiome biome = storage == null ? columns[columnIndex(x, z)] : storage[cellIndex(x, y, z)];
+ if (biome != null) {
+ return biome;
+ }
+ NativeBiome resolved = defaultBiome;
+ if (resolved == null) {
+ resolved = fallback.get();
+ defaultBiome = resolved;
+ }
+ return resolved;
+ }
+
+ boolean expanded() {
+ return cells != null;
+ }
+
+ private void fillColumn(int x, int z, NativeBiome biome) {
+ NativeBiome[] storage = cells;
+ if (storage == null) {
+ columns[columnIndex(x, z)] = biome;
+ return;
+ }
+ int stride = CHUNK_SIZE * CHUNK_SIZE;
+ int index = columnIndex(x, z);
+ for (int y = 0; y < getHeight(); y++) {
+ storage[index] = biome;
+ index += stride;
+ }
+ }
+
+ private NativeBiome[] expand() {
+ NativeBiome[] storage = new NativeBiome[CHUNK_SIZE * getHeight() * CHUNK_SIZE];
+ int stride = CHUNK_SIZE * CHUNK_SIZE;
+ for (int column = 0; column < stride; column++) {
+ NativeBiome biome = columns[column];
+ if (biome == null) {
+ continue;
+ }
+ for (int index = column; index < storage.length; index += stride) {
+ storage[index] = biome;
+ }
+ }
+ cells = storage;
+ return storage;
+ }
+
+ private static int columnIndex(int x, int z) {
+ return ((z & (CHUNK_SIZE - 1)) * CHUNK_SIZE) + (x & (CHUNK_SIZE - 1));
+ }
+
+ private int cellIndex(int x, int y, int z) {
+ int clampedY = Math.max(0, Math.min(getHeight() - 1, y));
+ return (clampedY * CHUNK_SIZE * CHUNK_SIZE) + columnIndex(x, z);
+ }
+}
diff --git a/probe/src/main/java/art/arcane/iris/probe/StubPlatform.java b/probe/src/main/java/art/arcane/iris/probe/StubPlatform.java
index 65c658e28..d7dd3a673 100644
--- a/probe/src/main/java/art/arcane/iris/probe/StubPlatform.java
+++ b/probe/src/main/java/art/arcane/iris/probe/StubPlatform.java
@@ -26,6 +26,7 @@ import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.LogLevel;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.iris.spi.PlatformBiomeWriter;
+import art.arcane.iris.spi.PlatformGenerationRegistry;
import art.arcane.volmlib.nativelib.terrain.NativeBlockProperty;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.nativelib.entity.NativeEntityType;
@@ -78,7 +79,7 @@ public final class StubPlatform implements IrisPlatform {
return StubBlockState.of(key);
}
- private final StubRegistries registries = new StubRegistries();
+ private final StubRegistries registries;
private final StubScheduler scheduler = new StubScheduler();
private final StubStructureHooks structureHooks = new StubStructureHooks();
private final StubBiomeWriter biomeWriter = new StubBiomeWriter();
@@ -89,7 +90,12 @@ public final class StubPlatform implements IrisPlatform {
}
public StubPlatform(File dataFolder) {
+ this(dataFolder, null);
+ }
+
+ public StubPlatform(File dataFolder, PlatformGenerationRegistry generationRegistry) {
this.dataFolder = dataFolder;
+ this.registries = new StubRegistries(generationRegistry);
}
private static final class StubBlockState implements NativeBlockState {
@@ -620,6 +626,20 @@ public final class StubPlatform implements IrisPlatform {
}
private static final class StubRegistries implements PlatformRegistries {
+ private final PlatformGenerationRegistry generationRegistry;
+
+ private StubRegistries(PlatformGenerationRegistry generationRegistry) {
+ this.generationRegistry = generationRegistry;
+ }
+
+ @Override
+ public PlatformGenerationRegistry generationRegistry() {
+ if (generationRegistry == null) {
+ return PlatformRegistries.super.generationRegistry();
+ }
+ return generationRegistry;
+ }
+
@Override
public NativeBlockState decodeBlockState(String key) {
return blockOrNull(key);
diff --git a/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java b/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java
index 716aca6b8..54d240378 100644
--- a/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java
+++ b/probe/src/test/java/art/arcane/iris/probe/GenerationProbeTest.java
@@ -1,15 +1,20 @@
package art.arcane.iris.probe;
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
+import art.arcane.iris.generation.hydrology.HydrologyTileKey;
+import art.arcane.iris.generation.runtime.EngineMetrics;
import org.junit.Test;
import java.io.File;
import java.nio.file.Path;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
@@ -128,7 +133,96 @@ public final class GenerationProbeTest {
GenerationProbe.ProbeOptions defaults = GenerationProbe.ProbeOptions.fromSystemProperties();
assertEquals(1, defaults.threads());
assertNull(defaults.dumpFile());
- assertThrows(IllegalArgumentException.class, () -> new GenerationProbe.ProbeOptions(0, null, false));
+ assertThrows(IllegalArgumentException.class,
+ () -> new GenerationProbe.ProbeOptions(0, null, false, false, false, false, false));
+ assertFalse(defaults.hydrologyPreplan());
+ assertFalse(defaults.historyRouter());
+ assertFalse(defaults.productionBuffers());
+ assertFalse(defaults.counters());
+ }
+
+ @Test
+ public void readsHarnessFidelityOptionsFromSystemProperties() {
+ System.setProperty("iris.probe.hydrologyPreplan", "true");
+ System.setProperty("iris.probe.historyRouter", "true");
+ System.setProperty("iris.probe.productionBuffers", "true");
+ System.setProperty("iris.generation.counters", "true");
+ try {
+ GenerationProbe.ProbeOptions options = GenerationProbe.ProbeOptions.fromSystemProperties();
+ assertTrue(options.hydrologyPreplan());
+ assertTrue(options.historyRouter());
+ assertTrue(options.productionBuffers());
+ assertTrue(options.counters());
+ assertFalse(options.parallel());
+ } finally {
+ System.clearProperty("iris.probe.hydrologyPreplan");
+ System.clearProperty("iris.probe.historyRouter");
+ System.clearProperty("iris.probe.productionBuffers");
+ System.clearProperty("iris.generation.counters");
+ }
+ }
+
+ @Test
+ public void stageLineReportsOnlyTheMeasuredWindow() {
+ Map before = new LinkedHashMap<>();
+ before.put("total", new EngineMetrics.StageTotal(100D, 10L));
+ before.put("context.prefill.height", new EngineMetrics.StageTotal(0D, 0L));
+ Map after = new LinkedHashMap<>();
+ after.put("total", new EngineMetrics.StageTotal(350.25D, 20L));
+ after.put("context.prefill.height", new EngineMetrics.StageTotal(12.5D, 10L));
+
+ String line = ProbeWindowMetrics.stagesLine(new ProbeWindowMetrics(before, Map.of()),
+ new ProbeWindowMetrics(after, Map.of()), 10, 1);
+
+ assertEquals("IRIS_GENPROBE_STAGES version=1 chunks=10 threads=1 total=250.250 context.prefill.height=12.500", line);
+ }
+
+ @Test
+ public void counterLineReportsWindowTotalsAndPerChunkRates() {
+ Map before = new LinkedHashMap<>();
+ before.put("generator_evaluations", 1_000L);
+ before.put("corner_starcasts", 0L);
+ Map after = new LinkedHashMap<>();
+ after.put("generator_evaluations", 5_000L);
+ after.put("corner_starcasts", 330L);
+
+ String line = ProbeWindowMetrics.countersLine(new ProbeWindowMetrics(Map.of(), before),
+ new ProbeWindowMetrics(Map.of(), after), 2);
+
+ assertEquals("IRIS_GENPROBE_COUNTERS version=1 chunks=2 generator_evaluations=4000"
+ + " generator_evaluations_per_chunk=2000.0 corner_starcasts=330 corner_starcasts_per_chunk=165.0", line);
+ }
+
+ @Test
+ public void hydrologyPreplanCoversEveryTileAScheduledChunkComposesFromPlusTheNeighbourRing() {
+ GenerationProbe.ProbeConfiguration configuration = new GenerationProbe.ProbeConfiguration(
+ new File("/tmp/pack"), "overworld", 256, 1024, 2048, 2048, false, false, 1337L);
+ int tileSize = 512;
+ int publicationRadius = 96;
+ int reachChunks = 13;
+ List keys = ProbeHydrologyPreplan.tiles(configuration, tileSize, publicationRadius, reachChunks);
+ Set planned = new HashSet<>(keys);
+
+ assertEquals(keys.size(), planned.size());
+ for (GenerationProbe.ChunkCoordinate chunk : GenerationProbe.scheduleCoordinates(1280, 2048, 2048)) {
+ for (int offsetX = -reachChunks; offsetX <= reachChunks; offsetX++) {
+ for (int offsetZ = -reachChunks; offsetZ <= reachChunks; offsetZ++) {
+ long minimumX = (long) (chunk.x() + offsetX) * 16L;
+ long minimumZ = (long) (chunk.z() + offsetZ) * 16L;
+ int firstX = Math.floorDiv(Math.toIntExact(minimumX - publicationRadius), tileSize) - 1;
+ int lastX = Math.floorDiv(Math.toIntExact(minimumX + 15L + publicationRadius), tileSize) + 1;
+ int firstZ = Math.floorDiv(Math.toIntExact(minimumZ - publicationRadius), tileSize) - 1;
+ int lastZ = Math.floorDiv(Math.toIntExact(minimumZ + 15L + publicationRadius), tileSize) + 1;
+ for (int tileX = firstX; tileX <= lastX; tileX++) {
+ for (int tileZ = firstZ; tileZ <= lastZ; tileZ++) {
+ assertTrue(planned.contains(new HydrologyTileKey(tileX, tileZ)));
+ }
+ }
+ }
+ }
+ }
+ HydrologyTileKey first = keys.getFirst();
+ assertEquals(new HydrologyTileKey(Math.floorDiv(2048 * 16 + 8, tileSize), Math.floorDiv(2048 * 16 + 8, tileSize)), first);
}
@Test
diff --git a/probe/src/test/java/art/arcane/iris/probe/ProductionHunkTest.java b/probe/src/test/java/art/arcane/iris/probe/ProductionHunkTest.java
new file mode 100644
index 000000000..1917afcd1
--- /dev/null
+++ b/probe/src/test/java/art/arcane/iris/probe/ProductionHunkTest.java
@@ -0,0 +1,127 @@
+package art.arcane.iris.probe;
+
+import art.arcane.iris.generation.chunk.SectionBuffer;
+import art.arcane.volmlib.nativelib.terrain.NativeBiome;
+import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
+import art.arcane.volmlib.util.hunk.Hunk;
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+public final class ProductionHunkTest {
+ private static final int HEIGHT = 24;
+
+ @Test
+ public void blockHunkIgnoresNullWritesAndTracksTheHighestStoredCellPerColumn() {
+ SectionBuffer blocks = new SectionBuffer(HEIGHT);
+ NativeBlockState stone = StubPlatform.blockStateForTest("minecraft:stone");
+ NativeBlockState dirt = StubPlatform.blockStateForTest("minecraft:dirt");
+
+ blocks.setRaw(3, 5, 7, stone);
+ blocks.setRaw(3, 9, 7, dirt);
+ blocks.setRaw(3, 2, 7, stone);
+ blocks.setRaw(3, 9, 7, null);
+
+ assertSame(dirt, blocks.getStoredRaw(3, 9, 7));
+ assertSame(stone, blocks.getStoredRaw(3, 5, 7));
+ assertNull(blocks.getStoredRaw(3, 6, 7));
+ assertEquals(9, blocks.highestStoredY(3, 7));
+ assertEquals(-1, blocks.highestStoredY(7, 3));
+ }
+
+ @Test
+ public void dumpCaptureOfTheBlockHunkMatchesAnArrayHunkHoldingTheSameWrites() {
+ SectionBuffer production = new SectionBuffer(HEIGHT);
+ Hunk array = Hunk.newArrayHunk(16, HEIGHT, 16);
+ Hunk biomes = Hunk.newArrayHunk(16, HEIGHT, 16);
+ NativeBlockState stone = StubPlatform.blockStateForTest("minecraft:stone");
+ NativeBlockState water = StubPlatform.blockStateForTest("minecraft:water");
+ for (int x = 0; x < 16; x++) {
+ for (int z = 0; z < 16; z++) {
+ int top = (x * 7 + z * 3) % HEIGHT;
+ for (int y = 0; y <= top; y++) {
+ NativeBlockState state = y == top ? water : stone;
+ production.setRaw(x, y, z, state);
+ array.setRaw(x, y, z, state);
+ }
+ }
+ }
+
+ ChunkDump.ChunkSnapshot fromProduction = ChunkDump.capture(4, -9, production, biomes, HEIGHT);
+ ChunkDump.ChunkSnapshot fromArray = ChunkDump.capture(4, -9, array, biomes, HEIGHT);
+
+ assertArrayEquals(fromArray.blockPalette(), fromProduction.blockPalette());
+ assertArrayEquals(fromArray.blocks(), fromProduction.blocks());
+ assertTrue(List.of(fromProduction.blockPalette()).contains(ChunkDump.NULL_BLOCK_KEY));
+ }
+
+ @Test
+ public void biomeHunkKeepsWholeColumnWritesPerColumnAndReadsLikeAnArrayHunk() {
+ ProductionBiomeHunk production = new ProductionBiomeHunk(HEIGHT, () -> biome("minecraft:plains"));
+ Hunk array = Hunk.newArrayHunk(16, HEIGHT, 16);
+ NativeBiome forest = biome("minecraft:forest");
+ NativeBiome desert = biome("minecraft:desert");
+ for (int x = 0; x < 16; x++) {
+ for (int z = 0; z < 16; z++) {
+ NativeBiome column = ((x + z) & 1) == 0 ? forest : desert;
+ production.set(x, 0, z, x, HEIGHT - 1, z, column);
+ array.set(x, 0, z, x, HEIGHT - 1, z, column);
+ }
+ }
+
+ assertFalse(production.expanded());
+ assertSameCells(array, production);
+
+ production.setRaw(2, 4, 9, desert);
+ array.setRaw(2, 4, 9, desert);
+ production.set(5, 0, 5, 5, HEIGHT - 1, 5, desert);
+ array.set(5, 0, 5, 5, HEIGHT - 1, 5, desert);
+
+ assertTrue(production.expanded());
+ assertSameCells(array, production);
+ }
+
+ @Test
+ public void biomeHunkReadsTheFallbackOnlyForUnwrittenCells() {
+ NativeBiome plains = biome("minecraft:plains");
+ ProductionBiomeHunk production = new ProductionBiomeHunk(HEIGHT, () -> plains);
+ NativeBiome forest = biome("minecraft:forest");
+ production.set(1, 0, 1, 1, HEIGHT - 1, 1, forest);
+
+ assertSame(forest, production.getRaw(1, HEIGHT - 1, 1));
+ assertSame(plains, production.getRaw(0, 0, 0));
+ }
+
+ private static void assertSameCells(Hunk expected, Hunk actual) {
+ for (int x = 0; x < 16; x++) {
+ for (int z = 0; z < 16; z++) {
+ for (int y = 0; y < HEIGHT; y++) {
+ assertSame(expected.get(x, y, z), actual.get(x, y, z));
+ }
+ }
+ }
+ }
+
+ private static NativeBiome biome(String key) {
+ return new TestBiome(key);
+ }
+
+ private record TestBiome(String key) implements NativeBiome {
+ @Override
+ public String namespace() {
+ return key.substring(0, key.indexOf(':'));
+ }
+
+ @Override
+ public Object nativeHandle() {
+ return key;
+ }
+ }
+}