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

@@ -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();