Plain chunk output buffer

- ChunkDataHunkHolder stores cells in a plain array and tracks each column's highest stored y
- natural cave decoration and perfection stop scanning above the column top
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-28 11:11:11 -04:00
1 parent 8b5c109d55
commit bc618e20ec
9 files changed
+331 -10

No files matched your search

@@ -23,19 +23,35 @@ 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.AtomicHunk;
import art.arcane.volmlib.util.hunk.storage.StorageHunk;
import org.bukkit.block.data.BlockData;
import org.bukkit.generator.ChunkGenerator.ChunkData;
@SuppressWarnings("ClassCanBeRecord")
public class ChunkDataHunkHolder extends AtomicHunk<NativeBlockState> implements NativeBlockVolume {
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.Arrays;
/**
* Plain-array block buffer for one chunk. Stages write disjoint columns when they fan out and every hand-off
* between threads is a join, so the cells need no per-access fences. Each column tracks its highest stored y;
* a raise is a CAS so concurrent writers to one column can never lose the maximum.
*/
public class ChunkDataHunkHolder extends StorageHunk<NativeBlockState> implements NativeBlockVolume, ColumnExtent {
private static final BoundBlockState AIR = BoundBlockState.of("AIR");
private static final VarHandle COLUMN_TOPS = MethodHandles.arrayElementVarHandle(int[].class);
private final ChunkData chunk;
private final NativeBlockState[] data;
private final int[] columnTops;
private final int zStride;
public ChunkDataHunkHolder(ChunkData chunk) {
super(16, chunk.getMaxHeight() - chunk.getMinHeight(), 16);
this.chunk = chunk;
this.zStride = 16 * getHeight();
this.data = new NativeBlockState[zStride * 16];
this.columnTops = new int[256];
Arrays.fill(columnTops, -1);
}
@Override
@@ -56,7 +72,16 @@ public class ChunkDataHunkHolder extends AtomicHunk<NativeBlockState> implements
if (t == null) {
return;
}
super.setRaw(x, y, z, t);
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
@@ -65,13 +90,23 @@ public class ChunkDataHunkHolder extends AtomicHunk<NativeBlockState> implements
return AIR.get();
}
NativeBlockState b = super.getRaw(x, y, z);
NativeBlockState b = data[index(x, y, z)];
return b != null ? b : AIR.get();
}
@Override
public NativeBlockState getStoredRaw(int x, int y, int z) {
return super.getRaw(x, y, 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() {
@@ -90,7 +125,7 @@ public class ChunkDataHunkHolder extends AtomicHunk<NativeBlockState> implements
int runStart = -1;
for (int y = 0; y < height; y++) {
NativeBlockState state = super.getRaw(x, y, z);
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.
@@ -0,0 +1,33 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.generation.chunk;
import art.arcane.volmlib.util.hunk.Hunk;
/**
* A block volume that knows, per column, the highest y it holds a value for. Every cell above that y reads as
* unwritten, so column scans can stop there without changing what they observe.
*/
public interface ColumnExtent {
int highestStoredY(int x, int z);
static int highestStoredY(Hunk<?> hunk, int x, int z) {
return hunk instanceof ColumnExtent extent ? extent.highestStoredY(x, z) : hunk.getHeight() - 1;
}
}
@@ -0,0 +1,37 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2026 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.generation.chunk;
import art.arcane.volmlib.util.function.Consumer4;
import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.volmlib.util.hunk.view.ListeningHunk;
public final class ColumnExtentListeningHunk<T> extends ListeningHunk<T> implements ColumnExtent {
private final Hunk<T> source;
public ColumnExtentListeningHunk(Hunk<T> source, Consumer4<Integer, Integer, Integer, T> listener) {
super(source, listener);
this.source = source;
}
@Override
public int highestStoredY(int x, int z) {
return ColumnExtent.highestStoredY(source, x, z);
}
}
@@ -48,6 +48,7 @@ import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.atomics.AtomicRollingSequence;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.iris.generation.chunk.ColumnExtentListeningHunk;
import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.hunk.Hunk;
@@ -1465,7 +1466,7 @@ public class IrisEngine implements Engine {
IrisContext.Scope generationScope = IrisContext.open(this, lease.sessionId(), null)) {
getEngineData().getStatistics().generatedChunk();
PrecisionStopwatch p = PrecisionStopwatch.start();
Hunk<NativeBlockState> blocks = vblocks.listen((xx, y, zz, t) -> catchBlockUpdates(x + xx, y, z + zz, t));
Hunk<NativeBlockState> blocks = new ColumnExtentListeningHunk<>(vblocks, (xx, y, zz, t) -> catchBlockUpdates(x + xx, y, z + zz, t));
if (getDimension().isDebugChunkCrossSections() && ((x >> 4) % getDimension().getDebugCrossSectionsMod() == 0 || (z >> 4) % getDimension().getDebugCrossSectionsMod() == 0)) {
NativeBlockState crossSection = B.getState("CRYING_OBSIDIAN");
@@ -40,6 +40,7 @@ import art.arcane.iris.generation.decoration.IrisProceduralBlocks;
import art.arcane.iris.generation.hydrology.IrisRiverMaterialConfig;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.generation.chunk.ColumnExtent;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.iris.generation.block.B;
@@ -896,12 +897,16 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
int width = output.getWidth();
int depth = output.getDepth();
int height = output.getHeight();
ColumnExtent extent = output instanceof ColumnExtent columnExtent ? columnExtent : null;
for (int localX = 0; localX < width; localX++) {
for (int localZ = 0; localZ < depth; localZ++) {
int worldX = blockX + localX;
int worldZ = blockZ + localZ;
int floor = -1;
for (int y = 1; y < height; y++) {
if (extent != null && y > extent.highestStoredY(localX, localZ)) {
break;
}
NativeBlockState state = output.getRaw(localX, y, localZ);
if (B.isSolid(state)) {
if (floor >= 0) {
@@ -18,6 +18,7 @@
package art.arcane.iris.generation.stage;
import art.arcane.iris.generation.chunk.ColumnExtent;
import art.arcane.iris.generation.decoration.IrisSpeleothems;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EngineAssignedModifier;
@@ -203,7 +204,7 @@ public class IrisPerfectionModifier extends EngineAssignedModifier<NativeBlockSt
}
private int getHeight(Hunk<NativeBlockState> output, int x, int z) {
for (int i = output.getHeight() - 1; i >= 0; i--) {
for (int i = Math.min(output.getHeight() - 1, ColumnExtent.highestStoredY(output, x, z)); i >= 0; i--) {
NativeBlockState b = output.get(x, i, z);
if (b != null) {
@@ -0,0 +1,99 @@
package art.arcane.iris.generation.chunk;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import org.bukkit.generator.ChunkGenerator;
import org.junit.Test;
import java.util.ArrayList;
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.Mockito.mock;
import static org.mockito.Mockito.when;
public class ChunkDataHunkHolderTest {
@Test
public void columnTopTracksTheHighestStoredValueAndIgnoresNulls() {
ChunkDataHunkHolder holder = holder(64);
NativeBlockState stone = mock(NativeBlockState.class);
assertEquals(-1, holder.highestStoredY(3, 7));
holder.setRaw(3, 40, 7, null);
assertEquals(-1, holder.highestStoredY(3, 7));
holder.setRaw(3, 12, 7, stone);
holder.setRaw(3, 30, 7, stone);
holder.setRaw(3, 5, 7, stone);
assertEquals(30, holder.highestStoredY(3, 7));
assertEquals(-1, holder.highestStoredY(7, 3));
assertSame(stone, holder.getStoredRaw(3, 30, 7));
assertNull(holder.getStoredRaw(3, 31, 7));
assertEquals(30, ColumnExtent.highestStoredY(new ColumnExtentListeningHunk<>(holder, (x, y, z, state) -> {
}), 3, 7));
}
@Test
public void cellsKeepTheZMajorLayout() {
ChunkDataHunkHolder holder = holder(32);
List<NativeBlockState> states = new ArrayList<>();
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < 32; y++) {
NativeBlockState state = mock(NativeBlockState.class);
states.add(state);
holder.setRaw(x, y, z, state);
}
}
}
int index = 0;
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < 32; y++) {
assertSame(states.get(index++), holder.getStoredRaw(x, y, z));
}
}
}
}
@Test
public void concurrentWritersNeverLoseTheColumnMaximum() throws InterruptedException {
for (int trial = 0; trial < 20; trial++) {
ChunkDataHunkHolder holder = holder(768);
NativeBlockState stone = mock(NativeBlockState.class);
int threads = 8;
CountDownLatch start = new CountDownLatch(1);
List<Thread> writers = new ArrayList<>();
for (int thread = 0; thread < threads; thread++) {
int offset = thread;
Thread writer = new Thread(() -> {
try {
start.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
for (int y = offset; y < 768; y += threads) {
holder.setRaw(9, y, 4, stone);
}
});
writer.start();
writers.add(writer);
}
start.countDown();
for (Thread writer : writers) {
writer.join();
}
assertEquals(767, holder.highestStoredY(9, 4));
}
}
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);
}
}
@@ -12,6 +12,11 @@ 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.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;
@@ -91,6 +96,46 @@ public class IrisNaturalCaveScanTest {
verify(fixture.chunk).release();
}
@Test
public void columnExtentScanMatchesFullScanWhenDecorationBuildsAboveTheColumnTop() throws Exception {
IrisPlatforms.bind(mock(IrisPlatform.class));
try {
Fixture full = new Fixture(true);
Hunk<NativeBlockState> fullOutput = full.sparseOutput(Hunk.newArrayHunk(16, 32, 16));
List<String> fullWrites = new ArrayList<>();
full.modifier.decorateNaturalCaves(-32, 48,
fullOutput.listen((x, y, z, state) -> fullWrites.add(x + ":" + y + ":" + z + ":" + state.key())));
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);
bounded.sparseOutput(holder);
List<String> boundedWrites = new ArrayList<>();
bounded.modifier.decorateNaturalCaves(-32, 48, new ColumnExtentListeningHunk<>(holder,
(x, y, z, state) -> boundedWrites.add(x + ":" + y + ":" + z + ":" + state.key())));
assertTrue("fixture must decorate a zone above the initial column top",
full.calls.contains("floor:0:0:27:1"));
assertEquals(full.calls, bounded.calls);
assertEquals(fullWrites, boundedWrites);
assertEquals(full.markers, bounded.markers);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < 32; y++) {
NativeBlockState expected = fullOutput.getRaw(x, y, z);
NativeBlockState actual = holder.getStoredRaw(x, y, z);
assertEquals("block " + x + ":" + y + ":" + z,
expected == null ? null : expected.key(), actual == null ? null : actual.key());
}
}
}
} finally {
IrisPlatforms.unbind();
}
}
private static void verifyScan(int width, int depth) throws Exception {
Fixture fixture = new Fixture();
Hunk<NativeBlockState> output = fixture.output(width, depth);
@@ -169,8 +214,12 @@ public class IrisNaturalCaveScanTest {
private final List<String> calls = new ArrayList<>();
private final List<String> markers = new ArrayList<>();
@SuppressWarnings("unchecked")
private Fixture() throws Exception {
this(false);
}
@SuppressWarnings("unchecked")
private Fixture(boolean buildAboveTop) throws Exception {
Engine engine = mock(Engine.class);
EngineMantle engineMantle = mock(EngineMantle.class);
Mantle<Matter> mantle = mock(Mantle.class);
@@ -211,6 +260,9 @@ public class IrisNaturalCaveScanTest {
for (int height = 20; height <= 22; height++) {
output.setRaw(x, height, z, air);
}
if (buildAboveTop) {
output.setRaw(x, 31, z, stone);
}
}
return null;
}).when(surface).decorate(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(), eq(biome), any(), anyInt(), anyInt());
@@ -225,6 +277,20 @@ public class IrisNaturalCaveScanTest {
}).when(roof).decorate(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(), eq(biome), any(), anyInt(), anyInt());
}
private <H extends Hunk<NativeBlockState>> H sparseOutput(H output) {
for (int x = 0; x < output.getWidth(); x++) {
for (int z = 0; z < output.getDepth(); z++) {
for (int y = 0; y < 32; y++) {
NativeBlockState state = initialState(this, y);
if (state != air) {
output.setRaw(x, y, z, state);
}
}
}
}
return output;
}
private Hunk<NativeBlockState> output(int width, int depth) {
Hunk<NativeBlockState> output = Hunk.newArrayHunk(width, 32, depth);
for (int x = 0; x < width; x++) {
@@ -1,6 +1,8 @@
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.ColumnExtentListeningHunk;
import art.arcane.iris.generation.concurrent.BurstExecutor;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.iris.generation.decoration.IrisProceduralBlocks;
@@ -13,6 +15,7 @@ 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;
@@ -24,6 +27,7 @@ import java.util.Random;
import java.util.concurrent.ExecutorService;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
@@ -55,6 +59,46 @@ public class IrisPerfectionModifierColumnTest {
}
}
@Test
public void columnExtentTopSearchMatchesFullHeightSearch() {
Fixture fixture = new Fixture();
Random random = new Random(55031L);
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);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
int top = random.nextInt(64);
for (int y = 0; y <= top; y++) {
NativeBlockState state = y == 0 ? fixture.stone : palette[random.nextInt(palette.length)];
bounded.setRaw(x, y, z, state);
full.setRaw(x, y, z, state);
}
}
}
fixture.modifier.onModify(0, 0, new ColumnExtentListeningHunk<>(bounded, (x, y, z, state) -> {
}), false, null);
fixture.modifier.onModify(0, 0, full.listen((x, y, z, state) -> {
}), false, null);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < 64; y++) {
assertSame("voxel " + x + "," + y + "," + z, full.getStoredRaw(x, y, z), bounded.getStoredRaw(x, y, z));
}
}
}
}
}
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();