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/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/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/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 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/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/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/probe/src/main/java/art/arcane/iris/probe/ProbeChunkGenerator.java b/probe/src/main/java/art/arcane/iris/probe/ProbeChunkGenerator.java
index efbb08746..64db59352 100644
--- a/probe/src/main/java/art/arcane/iris/probe/ProbeChunkGenerator.java
+++ b/probe/src/main/java/art/arcane/iris/probe/ProbeChunkGenerator.java
@@ -18,6 +18,7 @@
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;
@@ -70,7 +71,7 @@ final class ProbeChunkGenerator {
}
private Hunk newBlocks(int height) {
- return productionBuffers ? new ProductionBlockHunk(height) : Hunk.newArrayHunk(16, height, 16);
+ return productionBuffers ? new SectionBuffer(height) : Hunk.newArrayHunk(16, height, 16);
}
private Hunk newBiomes(int height) {
diff --git a/probe/src/main/java/art/arcane/iris/probe/ProductionBlockHunk.java b/probe/src/main/java/art/arcane/iris/probe/ProductionBlockHunk.java
deleted file mode 100644
index 91201e3cd..000000000
--- a/probe/src/main/java/art/arcane/iris/probe/ProductionBlockHunk.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * 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.block.BoundBlockState;
-import art.arcane.iris.generation.chunk.ColumnExtent;
-import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
-import art.arcane.volmlib.nativelib.terrain.NativeBlockVolume;
-import art.arcane.volmlib.util.hunk.storage.StorageHunk;
-
-import java.lang.invoke.MethodHandles;
-import java.lang.invoke.VarHandle;
-import java.util.Arrays;
-
-final class ProductionBlockHunk 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 NativeBlockState[] data;
- private final int[] columnTops;
- private final int zStride;
-
- ProductionBlockHunk(int height) {
- super(16, height, 16);
- this.zStride = 16 * height;
- this.data = new NativeBlockState[zStride * 16];
- this.columnTops = new int[256];
- Arrays.fill(columnTops, -1);
- }
-
- @Override
- public void setRaw(int x, int y, int z, NativeBlockState t) {
- 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 state = data[index(x, y, z)];
- return state != null ? state : 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;
- }
-}
diff --git a/probe/src/test/java/art/arcane/iris/probe/ProductionHunkTest.java b/probe/src/test/java/art/arcane/iris/probe/ProductionHunkTest.java
index 1a21fb510..1917afcd1 100644
--- a/probe/src/test/java/art/arcane/iris/probe/ProductionHunkTest.java
+++ b/probe/src/test/java/art/arcane/iris/probe/ProductionHunkTest.java
@@ -1,5 +1,6 @@
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;
@@ -19,7 +20,7 @@ public final class ProductionHunkTest {
@Test
public void blockHunkIgnoresNullWritesAndTracksTheHighestStoredCellPerColumn() {
- ProductionBlockHunk blocks = new ProductionBlockHunk(HEIGHT);
+ SectionBuffer blocks = new SectionBuffer(HEIGHT);
NativeBlockState stone = StubPlatform.blockStateForTest("minecraft:stone");
NativeBlockState dirt = StubPlatform.blockStateForTest("minecraft:dirt");
@@ -37,7 +38,7 @@ public final class ProductionHunkTest {
@Test
public void dumpCaptureOfTheBlockHunkMatchesAnArrayHunkHoldingTheSameWrites() {
- ProductionBlockHunk production = new ProductionBlockHunk(HEIGHT);
+ 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");