Merged wave1 into terrain bake

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

No files matched your search

@@ -6,8 +6,7 @@ import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.iris.generation.chunk.ChunkDataHunkHolder;
import org.bukkit.generator.ChunkGenerator;
import art.arcane.iris.generation.chunk.SectionBuffer;
import org.junit.After;
import org.junit.ClassRule;
import org.junit.Test;
@@ -37,7 +36,7 @@ public class PlatformStateRebindTest {
"art.arcane.iris.generation.biome.IrisBiomeLayerGenerator",
"art.arcane.iris.studio.generation.BiomeBuffetGenerator",
"art.arcane.iris.generation.mantle.EngineMantle",
"art.arcane.iris.generation.chunk.ChunkDataHunkHolder");
"art.arcane.iris.generation.chunk.SectionBuffer");
@After
public void unbindPlatform() {
@@ -47,10 +46,7 @@ public class PlatformStateRebindTest {
@Test
public void chunkDataAirIsResolvedAgainstTheCurrentlyBoundPlatform() {
NativeBlockState first = bindPlatform("first");
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
when(chunk.getMinHeight()).thenReturn(-64);
when(chunk.getMaxHeight()).thenReturn(320);
ChunkDataHunkHolder holder = new ChunkDataHunkHolder(chunk);
SectionBuffer holder = new SectionBuffer(384);
assertSame(first, holder.getRaw(0, -1, 0));
@@ -1,155 +0,0 @@
package art.arcane.iris.generation.chunk;
import art.arcane.iris.platform.bukkit.nms.INMS;
import art.arcane.iris.platform.bukkit.nms.INMSBinding;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import org.bukkit.block.data.BlockData;
import org.bukkit.generator.ChunkGenerator;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class ChunkDataHunkHolderTest {
@Test
public void columnTopTracksTheHighestStoredValueAndIgnoresNulls() {
ChunkDataHunkHolder holder = holder(64);
NativeBlockState stone = mock(NativeBlockState.class);
assertEquals(-1, holder.highestStoredY(3, 7));
holder.setRaw(3, 40, 7, null);
assertEquals(-1, holder.highestStoredY(3, 7));
holder.setRaw(3, 12, 7, stone);
holder.setRaw(3, 30, 7, stone);
holder.setRaw(3, 5, 7, stone);
assertEquals(30, holder.highestStoredY(3, 7));
assertEquals(-1, holder.highestStoredY(7, 3));
assertSame(stone, holder.getStoredRaw(3, 30, 7));
assertNull(holder.getStoredRaw(3, 31, 7));
assertEquals(30, ColumnExtent.highestStoredY(new ColumnExtentListeningHunk<>(holder, (x, y, z, state) -> {
}), 3, 7));
}
@Test
public void cellsKeepTheZMajorLayout() {
ChunkDataHunkHolder holder = holder(32);
List<NativeBlockState> states = new ArrayList<>();
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < 32; y++) {
NativeBlockState state = mock(NativeBlockState.class);
states.add(state);
holder.setRaw(x, y, z, state);
}
}
}
int index = 0;
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < 32; y++) {
assertSame(states.get(index++), holder.getStoredRaw(x, y, z));
}
}
}
}
@Test
public void concurrentWritersNeverLoseTheColumnMaximum() throws InterruptedException {
for (int trial = 0; trial < 20; trial++) {
ChunkDataHunkHolder holder = holder(768);
NativeBlockState stone = mock(NativeBlockState.class);
int threads = 8;
CountDownLatch start = new CountDownLatch(1);
List<Thread> writers = new ArrayList<>();
for (int thread = 0; thread < threads; thread++) {
int offset = thread;
Thread writer = new Thread(() -> {
try {
start.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
for (int y = offset; y < 768; y += threads) {
holder.setRaw(9, y, 4, stone);
}
});
writer.start();
writers.add(writer);
}
start.countDown();
for (Thread writer : writers) {
writer.join();
}
assertEquals(767, holder.highestStoredY(9, 4));
}
}
@Test
public void fallbackAppliesExplicitCaveAirBetweenSolidRunsWithNegativeMinimumHeight() {
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
when(chunk.getMinHeight()).thenReturn(-256);
when(chunk.getMaxHeight()).thenReturn(512);
ChunkDataHunkHolder holder = new ChunkDataHunkHolder(chunk);
BlockData dirt = mock(BlockData.class);
BlockData coal = mock(BlockData.class);
BlockData caveAir = mock(BlockData.class);
NativeBlockState dirtState = state(dirt);
NativeBlockState coalState = state(coal);
NativeBlockState caveAirState = state(caveAir);
BlockData[] applied = new BlockData[768];
Arrays.fill(applied, dirt);
for (int y = 0; y <= 75; y++) {
holder.setRaw(14, y + 256, 6, y < 33 ? dirtState : y <= 54 ? caveAirState : coalState);
}
doAnswer(call -> {
int y = call.getArgument(1);
applied[y + 256] = call.getArgument(3);
return null;
}).when(chunk).setBlock(anyInt(), anyInt(), anyInt(), any(BlockData.class));
doAnswer(call -> {
int from = call.getArgument(1);
int to = call.getArgument(4);
BlockData block = call.getArgument(6);
Arrays.fill(applied, from + 256, to + 256, block);
return null;
}).when(chunk).setRegion(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(BlockData.class));
try (MockedStatic<INMS> nms = mockStatic(INMS.class)) {
nms.when(INMS::get).thenReturn(mock(INMSBinding.class));
holder.apply();
}
for (int y = -256; y < 512; y++) {
BlockData expected = y >= 33 && y <= 54 ? caveAir : y >= 55 && y <= 75 ? coal : dirt;
assertSame("column at " + y, expected, applied[y + 256]);
}
}
private static NativeBlockState state(BlockData data) {
NativeBlockState state = mock(NativeBlockState.class);
when(state.nativeHandle()).thenReturn(data);
return state;
}
private static ChunkDataHunkHolder holder(int height) {
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
when(chunk.getMinHeight()).thenReturn(0);
when(chunk.getMaxHeight()).thenReturn(height);
return new ChunkDataHunkHolder(chunk);
}
}
@@ -0,0 +1,88 @@
package art.arcane.iris.generation.chunk;
import art.arcane.iris.platform.bukkit.nms.INMS;
import art.arcane.iris.platform.bukkit.nms.INMSBinding;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import org.bukkit.block.data.BlockData;
import org.bukkit.generator.ChunkGenerator;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.Arrays;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ChunkDataOutputTest {
@Test
public void nativeWriterReceivesTheSectionBuffer() {
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
SectionBuffer blocks = new SectionBuffer(64);
INMSBinding binding = mock(INMSBinding.class);
when(binding.applyChunkDataBlocks(chunk, blocks)).thenReturn(true);
try (MockedStatic<INMS> nms = mockStatic(INMS.class)) {
nms.when(INMS::get).thenReturn(binding);
ChunkDataOutput.apply(chunk, blocks);
}
verify(binding).applyChunkDataBlocks(chunk, blocks);
verify(chunk, never()).setRegion(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(BlockData.class));
}
@Test
public void fallbackAppliesExplicitCaveAirBetweenSolidRunsWithNegativeMinimumHeight() {
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
when(chunk.getMinHeight()).thenReturn(-256);
when(chunk.getMaxHeight()).thenReturn(512);
SectionBuffer blocks = new SectionBuffer(768);
BlockData dirt = mock(BlockData.class);
BlockData coal = mock(BlockData.class);
BlockData caveAir = mock(BlockData.class);
NativeBlockState dirtState = state(dirt);
NativeBlockState coalState = state(coal);
NativeBlockState caveAirState = state(caveAir);
BlockData[] applied = new BlockData[768];
Arrays.fill(applied, dirt);
for (int y = 0; y <= 75; y++) {
blocks.setRaw(14, y + 256, 6, y < 33 ? dirtState : y <= 54 ? caveAirState : coalState);
}
doAnswer(call -> {
int y = call.getArgument(1);
applied[y + 256] = call.getArgument(3);
return null;
}).when(chunk).setBlock(anyInt(), anyInt(), anyInt(), any(BlockData.class));
doAnswer(call -> {
int from = call.getArgument(1);
int to = call.getArgument(4);
BlockData block = call.getArgument(6);
Arrays.fill(applied, from + 256, to + 256, block);
return null;
}).when(chunk).setRegion(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(BlockData.class));
try (MockedStatic<INMS> nms = mockStatic(INMS.class)) {
nms.when(INMS::get).thenReturn(mock(INMSBinding.class));
ChunkDataOutput.apply(chunk, blocks);
}
for (int y = -256; y < 512; y++) {
BlockData expected = y >= 33 && y <= 54 ? caveAir : y >= 55 && y <= 75 ? coal : dirt;
assertSame("column at " + y, expected, applied[y + 256]);
}
assertTrue(blocks.highestStoredY(14, 6) == 331);
}
private static NativeBlockState state(BlockData data) {
NativeBlockState state = mock(NativeBlockState.class);
when(state.nativeHandle()).thenReturn(data);
return state;
}
}
@@ -0,0 +1,322 @@
package art.arcane.iris.generation.chunk;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.hunk.Hunk;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class SectionBufferTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private NativeBlockState air;
@Before
public void bindPlatform() {
air = mock(NativeBlockState.class);
PlatformRegistries registries = mock(PlatformRegistries.class);
when(registries.block(anyString())).thenReturn(air);
IrisPlatform platform = mock(IrisPlatform.class);
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
}
@After
public void unbindPlatform() {
IrisPlatforms.unbind();
}
@Test
public void columnTopTracksTheHighestStoredValueAndIgnoresNulls() {
SectionBuffer buffer = new SectionBuffer(64);
NativeBlockState stone = mock(NativeBlockState.class);
assertEquals(-1, buffer.highestStoredY(3, 7));
buffer.setRaw(3, 40, 7, null);
assertEquals(-1, buffer.highestStoredY(3, 7));
buffer.setRaw(3, 12, 7, stone);
buffer.setRaw(3, 30, 7, stone);
buffer.setRaw(3, 5, 7, stone);
assertEquals(30, buffer.highestStoredY(3, 7));
assertEquals(-1, buffer.highestStoredY(7, 3));
assertSame(stone, buffer.getStoredRaw(3, 30, 7));
assertNull(buffer.getStoredRaw(3, 31, 7));
assertEquals(30, ColumnExtent.highestStoredY(new ColumnExtentListeningHunk<>(buffer, (x, y, z, state) -> {
}), 3, 7));
}
@Test
public void unwrittenAndOutOfRangeCellsReadAsAir() {
SectionBuffer buffer = new SectionBuffer(48);
NativeBlockState stone = mock(NativeBlockState.class);
buffer.setRaw(2, 20, 9, stone);
assertSame(air, buffer.getRaw(2, 21, 9));
assertSame(air, buffer.getRaw(2, 3, 9));
assertSame(air, buffer.getRaw(2, -1, 9));
assertSame(air, buffer.getRaw(2, 48, 9));
assertSame(stone, buffer.getRaw(2, 20, 9));
assertNull(buffer.getStoredRaw(2, 21, 9));
assertNull(buffer.getStoredRaw(2, 3, 9));
}
@Test
public void paletteKeepsEveryDistinctInstance() {
SectionBuffer buffer = new SectionBuffer(32);
NativeBlockState first = mock(NativeBlockState.class);
NativeBlockState second = mock(NativeBlockState.class);
when(first.matches(second)).thenReturn(true);
when(second.matches(first)).thenReturn(true);
buffer.setRaw(0, 0, 0, first);
buffer.setRaw(1, 0, 0, second);
buffer.setRaw(2, 0, 0, first);
assertSame(first, buffer.getRaw(0, 0, 0));
assertSame(second, buffer.getRaw(1, 0, 0));
assertSame(first, buffer.getRaw(2, 0, 0));
assertEquals(buffer.paletteId(first), buffer.paletteId(first));
assertNotEquals(buffer.paletteId(first), buffer.paletteId(second));
assertEquals(0, buffer.paletteId(null));
NativeBlockState[] palette = buffer.palette();
assertEquals(3, palette.length);
assertNull(palette[0]);
assertSame(first, palette[buffer.paletteId(first)]);
assertSame(second, palette[buffer.paletteId(second)]);
}
@Test
public void sectionsUseTheYzxLayoutAndStayUnallocatedUntilWritten() {
SectionBuffer buffer = new SectionBuffer(64);
NativeBlockState stone = mock(NativeBlockState.class);
NativeBlockState dirt = mock(NativeBlockState.class);
buffer.setRaw(5, 37, 11, stone);
buffer.setRaw(15, 47, 15, dirt);
assertEquals(4, buffer.sectionCount());
assertNull(buffer.section(0));
assertNull(buffer.section(1));
assertNull(buffer.section(3));
short[] section = buffer.section(2);
assertEquals(4096, section.length);
assertEquals(buffer.paletteId(stone), section[(5 << 8) | (11 << 4) | 5]);
assertEquals(buffer.paletteId(dirt), section[(15 << 8) | (15 << 4) | 15]);
int written = 0;
for (short id : section) {
written += id == 0 ? 0 : 1;
}
assertEquals(2, written);
}
@Test
public void columnRunsMatchRepeatedSingleWrites() {
Random random = new Random(41L);
List<NativeBlockState> pool = new ArrayList<>();
for (int index = 0; index < 9; index++) {
pool.add(mock(NativeBlockState.class));
}
pool.add(null);
for (int trial = 0; trial < 40; trial++) {
int height = 16 * (1 + random.nextInt(12));
SectionBuffer runs = new SectionBuffer(height);
SectionBuffer cells = new SectionBuffer(height);
for (int write = 0; write < 300; write++) {
int x = random.nextInt(16);
int z = random.nextInt(16);
int from = random.nextInt(height);
int to = Math.min(height - 1, from + random.nextInt(70));
NativeBlockState state = pool.get(random.nextInt(pool.size()));
switch (random.nextInt(3)) {
case 0 -> runs.fillColumnRun(x, z, from, to, state);
case 1 -> runs.fillColumnRun(x, z, from, to, runs.paletteId(state));
default -> ColumnRunHunk.fillColumnRun(runs, x, z, from, to, state);
}
for (int y = to; y >= from; y--) {
cells.setRaw(x, y, z, state);
}
}
assertSameContents(cells, runs);
}
}
@Test
public void runsWithAnEmptyRangeOrNoStateWriteNothing() {
SectionBuffer buffer = new SectionBuffer(32);
NativeBlockState stone = mock(NativeBlockState.class);
buffer.fillColumnRun(1, 1, 10, 9, stone);
buffer.fillColumnRun(1, 1, 3, 9, (NativeBlockState) null);
buffer.fillColumnRun(1, 1, 3, 9, 0);
assertEquals(-1, buffer.highestStoredY(1, 1));
assertNull(buffer.section(0));
assertEquals(1, buffer.palette().length);
}
@Test
public void concurrentWritersNeverLoseTheColumnMaximum() throws InterruptedException {
for (int trial = 0; trial < 20; trial++) {
SectionBuffer buffer = new SectionBuffer(768);
NativeBlockState stone = mock(NativeBlockState.class);
int threads = 8;
runConcurrently(threads, thread -> {
for (int y = thread; y < 768; y += threads) {
buffer.setRaw(9, y, 4, stone);
}
});
assertEquals(767, buffer.highestStoredY(9, 4));
}
}
@Test
public void concurrentPaletteGrowthKeepsOneIdPerInstance() throws InterruptedException {
int threads = 8;
int statesPerThread = 300;
NativeBlockState[][] states = new NativeBlockState[threads][statesPerThread];
NativeBlockState[] shared = new NativeBlockState[40];
for (int index = 0; index < shared.length; index++) {
shared[index] = mock(NativeBlockState.class);
}
for (int thread = 0; thread < threads; thread++) {
for (int index = 0; index < statesPerThread; index++) {
states[thread][index] = mock(NativeBlockState.class);
}
}
for (int trial = 0; trial < 10; trial++) {
SectionBuffer buffer = new SectionBuffer(384);
runConcurrently(threads, thread -> {
for (int index = 0; index < statesPerThread; index++) {
int x = thread * 2 + (index & 1);
int z = (index >> 1) & 15;
int y = index;
buffer.setRaw(x, y, z, states[thread][index]);
buffer.setRaw(x, y + 1, z, shared[index % shared.length]);
}
});
for (int thread = 0; thread < threads; thread++) {
for (int index = 0; index < statesPerThread; index++) {
int x = thread * 2 + (index & 1);
int z = (index >> 1) & 15;
assertSame(states[thread][index], buffer.getStoredRaw(x, index, z));
assertSame(shared[index % shared.length], buffer.getRaw(x, index + 1, z));
}
}
assertEquals(1 + threads * statesPerThread + shared.length, buffer.palette().length);
for (NativeBlockState state : shared) {
assertSame(state, buffer.palette()[buffer.paletteId(state)]);
}
}
}
@Test
public void listeningHunksForwardRunsInTopDownOrder() {
SectionBuffer buffer = new SectionBuffer(32);
NativeBlockState stone = mock(NativeBlockState.class);
List<Integer> heard = new ArrayList<>();
ColumnExtentListeningHunk<NativeBlockState> listening = new ColumnExtentListeningHunk<>(buffer,
(x, y, z, state) -> heard.add(y));
ColumnRunHunk.fillColumnRun(listening, 4, 6, 10, 13, stone);
assertEquals(List.of(13, 12, 11, 10), heard);
for (int y = 10; y <= 13; y++) {
assertSame(stone, buffer.getStoredRaw(4, y, 6));
}
assertNull(buffer.getStoredRaw(4, 9, 6));
assertEquals(13, buffer.highestStoredY(4, 6));
}
@Test
public void plainHunksReceiveRunsAsTopDownSingleWrites() {
Hunk<NativeBlockState> array = Hunk.newArrayHunk(16, 32, 16);
NativeBlockState stone = mock(NativeBlockState.class);
array.setRaw(2, 5, 2, stone);
List<Integer> order = new ArrayList<>();
Hunk<NativeBlockState> recording = new ColumnExtentListeningHunk<>(array, (x, y, z, state) -> order.add(y));
ColumnRunHunk.fillColumnRun(Hunk.newArrayHunk(16, 32, 16), 0, 0, 0, 3, stone);
ColumnRunHunk.fillColumnRun(array, 2, 2, 4, 6, null);
ColumnRunHunk.fillColumnRun(recording, 1, 1, 7, 9, stone);
assertNull(array.getRaw(2, 5, 2));
assertSame(stone, array.getRaw(1, 8, 1));
assertEquals(List.of(9, 8, 7), order);
}
private static void assertSameContents(SectionBuffer expected, SectionBuffer actual) {
assertEquals(expected.sectionCount(), actual.sectionCount());
NativeBlockState[] expectedPalette = expected.palette();
NativeBlockState[] actualPalette = actual.palette();
for (int sectionIndex = 0; sectionIndex < expected.sectionCount(); sectionIndex++) {
short[] expectedSection = expected.section(sectionIndex);
short[] actualSection = actual.section(sectionIndex);
if (expectedSection == null) {
assertNull(actualSection);
continue;
}
NativeBlockState[] expectedStates = new NativeBlockState[4096];
NativeBlockState[] actualStates = new NativeBlockState[4096];
for (int index = 0; index < 4096; index++) {
expectedStates[index] = expectedPalette[expectedSection[index] & 0xFFFF];
actualStates[index] = actualPalette[actualSection[index] & 0xFFFF];
}
assertArrayEquals(expectedStates, actualStates);
}
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
assertEquals(expected.highestStoredY(x, z), actual.highestStoredY(x, z));
}
}
assertEquals(Arrays.stream(expectedPalette).filter(state -> state != null).distinct().count(),
Arrays.stream(actualPalette).filter(state -> state != null).distinct().count());
}
private static void runConcurrently(int threads, ThreadTask task) throws InterruptedException {
CountDownLatch start = new CountDownLatch(1);
List<Thread> workers = new ArrayList<>();
for (int thread = 0; thread < threads; thread++) {
int index = thread;
Thread worker = new Thread(() -> {
try {
start.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
task.run(index);
});
worker.start();
workers.add(worker);
}
start.countDown();
for (Thread worker : workers) {
worker.join();
}
}
@FunctionalInterface
private interface ThreadTask {
void run(int thread);
}
}
@@ -12,6 +12,7 @@ import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
@@ -60,6 +61,13 @@ public class HydrologyBurstTest {
assertTrue(pool.isTerminated());
}
@Test
public void plannerPoolFollowsThePerformanceCores() {
assertEquals(12, MultiBurst.HydrologyBurst.plannerParallelism(12));
assertEquals(4, MultiBurst.HydrologyBurst.plannerParallelism(1));
assertEquals(32, MultiBurst.HydrologyBurst.plannerParallelism(32));
}
@Test
public void closedHydrologyWorkersTerminateAndReopenCreatesANewPool() throws Exception {
IrisSettings previousSettings = IrisSettings.settings;
@@ -696,6 +696,29 @@ public class HydrologyTileCacheTest {
cache.close();
}
@Test
public void pregenSpeculationStaysWithinTheGenerationReachOfTheArea() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyPlannerSettings settings = stalePrefetchSettings();
when(planner.settings()).thenReturn(settings);
ArrayList<HydrologyTileKey> planned = new ArrayList<>();
doAnswer(invocation -> {
planned.add(invocation.getArgument(0));
return tile;
}).when(planner).plan(any(HydrologyTileKey.class));
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, Runnable::run);
try (HydrologyTileCache.PregenerationScope ignored = cache.preparePregeneration(
new HydrologyTileCache.PregenerationArea(1024, 1024, 736, 736, 1311, 1311))) {
assertEquals(prefetchRectangle(0, 1, 0, 1), new HashSet<>(planned));
cache.prepareChunkColumns(800, 800);
cache.prepareChunkColumns(1300, 1300);
assertEquals(prefetchRectangle(0, 1, 0, 1), new HashSet<>(planned));
assertEquals(4, planned.size());
}
cache.close();
}
@Test
public void pregenScopesRestoreStudioPrefetchAndIgnoreSupersededClose() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
@@ -0,0 +1,106 @@
package art.arcane.iris.generation.mantle;
import org.junit.Test;
import java.util.SplittableRandom;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class CaveCellKernelTest {
private static final int TRIALS = 200_000;
@Test
public void decidedCellsAgreeWithEveryInterpolatedColumn() {
SplittableRandom random = new SplittableRandom(91_337L);
long decided = 0L;
for (int trial = 0; trial < TRIALS; trial++) {
double scale = Math.scalb(1D, random.nextInt(-30, 4));
double base = (random.nextDouble() * 2D - 1D) * scale;
double spread = random.nextInt(4) == 0 ? 0D : random.nextDouble() * scale * Math.scalb(1D, -random.nextInt(0, 40));
double d00 = base + spread * (random.nextDouble() * 2D - 1D);
double d01 = random.nextInt(8) == 0 ? d00 : base + spread * (random.nextDouble() * 2D - 1D);
double d10 = base + spread * (random.nextDouble() * 2D - 1D);
double d11 = random.nextInt(8) == 0 ? d10 : base + spread * (random.nextDouble() * 2D - 1D);
double low = Math.min(Math.min(d00, d01), Math.min(d10, d11));
double high = Math.max(Math.max(d00, d01), Math.max(d10, d11));
int mode = random.nextInt(6);
double[] thresholds = new double[64];
for (int column = 0; column < 64; column++) {
thresholds[column] = pickThreshold(random, low, high, mode);
}
double thresholdLow = Double.POSITIVE_INFINITY;
double thresholdHigh = Double.NEGATIVE_INFINITY;
for (double threshold : thresholds) {
thresholdLow = Math.min(thresholdLow, threshold);
thresholdHigh = Math.max(thresholdHigh, threshold);
}
int decision = CaveCellKernel.decide(d00, d01, d10, d11, thresholdLow, thresholdHigh);
if (decision == CaveCellKernel.MIXED) {
continue;
}
decided++;
for (int column = 0; column < 64; column++) {
double tx = (column >> 3) / 8D;
double tz = (column & 7) / 8D;
boolean carved = bilinear(d00, d01, d10, d11, tx, tz) <= thresholds[column];
assertEquals("trial " + trial + " column " + column, decision == CaveCellKernel.ALL, carved);
}
}
assertTrue("decided " + decided, decided > TRIALS / 4);
}
@Test
public void thresholdsAtTheCornerExtremesStayMixed() {
double d00 = 0.1D;
double d01 = -0.2D;
double d10 = 0.3D;
double d11 = 0.05D;
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(d00, d01, d10, d11, 0.3D, 0.3D));
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(d00, d01, d10, d11, Math.nextUp(0.3D), 0.4D));
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(d00, d01, d10, d11, -0.3D, Math.nextDown(-0.2D)));
assertEquals(CaveCellKernel.ALL, CaveCellKernel.decide(d00, d01, d10, d11, 0.3D + 1.0E-12D, 0.5D));
assertEquals(CaveCellKernel.NONE, CaveCellKernel.decide(d00, d01, d10, d11, -0.9D, -0.2D - 1.0E-12D));
}
@Test
public void nonFiniteCornersAreNeverDecided() {
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(Double.NaN, 0D, 0D, 0D, 5D, 5D));
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(0D, Double.POSITIVE_INFINITY, 0D, 0D, -5D, -5D));
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(0D, 0D, Double.NEGATIVE_INFINITY, 0D,
Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY));
assertEquals(CaveCellKernel.MIXED, CaveCellKernel.decide(0D, 0D, 0D, 0D, Double.NaN, 1D));
}
@Test
public void monotoneThresholdMappingPreservesBounds() {
SplittableRandom random = new SplittableRandom(7_771L);
for (int trial = 0; trial < TRIALS; trial++) {
double normalization = 0.25D + random.nextDouble() * 3D;
double inverse = 1D / normalization;
double a = (random.nextDouble() * 2D - 1D) * Math.scalb(1D, random.nextInt(-20, 3));
double b = random.nextInt(4) == 0 ? Math.nextUp(a) : a + random.nextDouble() * Math.scalb(1D, random.nextInt(-50, 0));
assertTrue(CaveCellKernel.threshold(a, normalization, inverse) <= CaveCellKernel.threshold(b, normalization, inverse));
assertEquals(Double.doubleToRawLongBits((a * normalization) * inverse),
Double.doubleToRawLongBits(CaveCellKernel.threshold(a, normalization, inverse)));
}
}
private static double pickThreshold(SplittableRandom random, double low, double high, int mode) {
double width = Math.max(high - low, Math.ulp(Math.max(Math.abs(low), Math.abs(high))));
return switch (mode) {
case 0 -> high + Math.ulp(high) * random.nextInt(0, 64);
case 1 -> low - Math.ulp(low) * random.nextInt(0, 64);
case 2 -> high + width * random.nextDouble() * 0.5D;
case 3 -> low - width * random.nextDouble() * 0.5D;
case 4 -> high + width * (1D + random.nextDouble() * 4D);
default -> low - width * (1D + random.nextDouble() * 4D);
};
}
private static double bilinear(double d00, double d01, double d10, double d11, double tx, double tz) {
double dx0 = d00 + ((d10 - d00) * tx);
double dx1 = d01 + ((d11 - d01) * tx);
return dx0 + ((dx1 - dx0) * tz);
}
}
@@ -0,0 +1,414 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.cave.IrisCaveFieldModule;
import art.arcane.iris.generation.cave.IrisCaveProfile;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.IrisStyledRange;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EngineMetrics;
import art.arcane.iris.generation.runtime.SeedManager;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.iris.world.IrisWorld;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterSlice;
import art.arcane.volmlib.util.matter.slices.CavernMatter;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.withSettings;
public class IrisCaveCarver3DCellKernelTest {
private static final long CARVE_SEED = -54_777_863_784_492_918L;
private static final int WORLD_HEIGHT = 160;
private static final int CHUNK_SPAN = 4;
@Test
public void warpModulesProfileMatchesGoldenWriteSequence() throws Exception {
assertScenario(Scenario.WARP_MODULES, 0xdf617c77d3c3cc5eL, 67798);
}
@Test
public void warpOnlyTerrainProfileMatchesGoldenWriteSequence() throws Exception {
assertScenario(Scenario.WARP_TERRAIN, 0x7e0b63cab5bb3904L, 67036);
}
@Test
public void styledThresholdBlendProfileMatchesGoldenWriteSequence() throws Exception {
assertScenario(Scenario.STYLED_BLEND, 0x25792d49143774faL, 30387);
}
@Test
public void fluidBoundaryProfileMatchesGoldenWriteSequence() throws Exception {
assertScenario(Scenario.FLUID_BOUNDARY, 0xb102145856119afeL, 17584);
}
@Test
public void noWarpModulesProfileMatchesGoldenWriteSequence() throws Exception {
assertScenario(Scenario.NO_WARP_MODULES, 0x5a83dc20c89c6398L, 47534);
}
@Test
public void verticalStepProfileMatchesGoldenWriteSequence() throws Exception {
assertScenario(Scenario.VERTICAL_STEP, 0xac0413031ed65b61L, 48011);
}
@Test
public void latticeProfileMatchesGoldenWriteSequence() throws Exception {
assertScenario(Scenario.LATTICE, 0xc7ffab155e6bdef9L, 72714);
}
@Test
public void floorFluidProfilesSharingChunksMatchGoldenWriteSequence() throws Exception {
assertScenario(Scenario.FLOOR_FLUID_LAYERED, 0xb4c0fb23e071617dL, 68747);
}
@Test
public void packStyleProfilesSharingFieldsMatchGoldenWriteSequence() throws Exception {
assertScenario(Scenario.PACK_SHARED, 0xdbe20a1ca87f9cc7L, 269035);
}
@Test
public void cellKernelDecidesCellsWithoutColumnEvaluation() throws Exception {
IrisCaveCarver3D.CellCounters before = IrisCaveCarver3D.cellCounters();
run(Scenario.WARP_MODULES, false);
IrisCaveCarver3D.CellCounters after = IrisCaveCarver3D.cellCounters();
long decided = after.decided() - before.decided();
long evaluated = after.evaluated() - before.evaluated();
assertTrue("decided=" + decided + " evaluated=" + evaluated, decided > 0L && evaluated > 0L);
}
private void assertScenario(Scenario scenario, long expectedHash, int expectedWrites) throws Exception {
Result serial = run(scenario, false);
Result parallel = run(scenario, true);
assertTrue(serial.carved > 0);
assertEquals(serial.hash, parallel.hash);
assertEquals(serial.writes, parallel.writes);
assertEquals(serial.carved, parallel.carved);
assertEquals(expectedHash, serial.hash);
assertEquals(expectedWrites, serial.writes);
}
private Result run(Scenario scenario, boolean parallel) throws Exception {
if (!parallel) {
return carveArea(scenario);
}
ForkJoinPool pool = new ForkJoinPool(4);
try {
return pool.submit(() -> carveArea(scenario)).get(120, TimeUnit.SECONDS);
} finally {
pool.shutdownNow();
}
}
private Result carveArea(Scenario scenario) {
Engine engine = createEngine(scenario);
CaveDensityGrid grid = new CaveDensityGrid(4096);
IrisCaveProfile[] profiles = scenario.profiles();
IrisCaveCarver3D[] carvers = new IrisCaveCarver3D[profiles.length];
for (int index = 0; index < profiles.length; index++) {
carvers[index] = new IrisCaveCarver3D(engine, profiles[index], grid);
}
Capture capture = new Capture();
int carved = 0;
for (int chunkX = -2; chunkX < CHUNK_SPAN - 2; chunkX++) {
for (int chunkZ = -1; chunkZ < CHUNK_SPAN - 1; chunkZ++) {
MantleWriter writer = capture.writer(chunkX, chunkZ);
CaveFluidSupportPlan supportPlan = new CaveFluidSupportPlan();
for (int index = 0; index < carvers.length; index++) {
carved += carvers[index].carve(writer, chunkX, chunkZ, scenario.weights(index, chunkX, chunkZ),
0.08D, 0.24D, scenario.worldYRange(), scenario.surfaceHeights(chunkX, chunkZ),
scenario.boundaries(chunkX, chunkZ), null, supportPlan);
}
capture.resolving = true;
supportPlan.resolve(writer.acquireChunk(chunkX, chunkZ));
capture.finishResolve();
}
}
return new Result(capture.hash, capture.writes, carved);
}
private Engine createEngine(Scenario scenario) {
Engine engine = mock(Engine.class, withSettings().stubOnly());
IrisData data = mock(IrisData.class, withSettings().stubOnly());
IrisDimension dimension = mock(IrisDimension.class, withSettings().stubOnly());
SeedManager seedManager = mock(SeedManager.class, withSettings().stubOnly());
doReturn(data).when(engine).getData();
doReturn(dimension).when(engine).getDimension();
doReturn(seedManager).when(engine).getSeedManager();
doReturn(CARVE_SEED).when(seedManager).getCarve();
doReturn(new EngineMetrics(16)).when(engine).getMetrics();
doReturn(IrisWorld.builder().minHeight(-32).maxHeight(WORLD_HEIGHT - 32).build()).when(engine).getWorld();
doReturn(WORLD_HEIGHT).when(engine).getHeight();
doReturn(110).when(engine).getHeight(anyInt(), anyInt());
doReturn(24).when(dimension).getCaveLavaHeight();
doReturn(70).when(dimension).getFluidHeight();
doReturn(scenario == Scenario.VERTICAL_STEP ? 4 : 1).when(dimension).getCaveDensitySamplingStep();
return engine;
}
private static IrisCaveProfile baseProfile(boolean warp, boolean modules) {
IrisCaveProfile profile = new IrisCaveProfile();
profile.setEnabled(true);
profile.setVerticalRange(new IrisRange(4D, 150D));
profile.setVerticalEdgeFade(14);
profile.setVerticalEdgeFadeStrength(0.21D);
profile.setBaseDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.21D));
profile.setDetailDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.43D));
profile.setWarpStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.37D));
profile.setSurfaceBreakStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.140625D));
profile.setBaseWeight(1D);
profile.setDetailWeight(0.42D);
profile.setWarpStrength(warp ? 3.7D : 0D);
profile.setDensityThreshold(new IrisStyledRange(-0.18D, -0.18D, new IrisGeneratorStyle(NoiseStyle.FLAT)));
profile.setThresholdBias(0D);
profile.setSampleStep(1);
profile.setSurfaceClearance(5);
profile.setAllowSurfaceBreak(true);
profile.setSurfaceBreakNoiseThreshold(0.3D);
profile.setSurfaceBreakDepth(14);
profile.setSurfaceBreakThresholdBoost(0.21D);
profile.setAllowFluid(false);
profile.setAllowLava(true);
KList<IrisCaveFieldModule> caveModules = new KList<>();
if (modules) {
caveModules.add(new IrisCaveFieldModule(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.33D),
0.27D, 0.05D, new IrisRange(0D, 90D), false));
caveModules.add(new IrisCaveFieldModule(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.51D),
0.19D, -0.07D, new IrisRange(40D, 150D), true));
}
profile.setModules(caveModules);
return profile;
}
private static IrisCaveProfile packProfile(NoiseStyle warp, double warpZoom, double warpStrength, NoiseStyle base,
double baseZoom, double threshold, IrisCaveFieldModule... modules) {
IrisCaveProfile profile = baseProfile(true, false);
profile.setWarpStyle(new IrisGeneratorStyle(warp).zoomed(warpZoom));
profile.setWarpStrength(warpStrength);
profile.setBaseDensityStyle(new IrisGeneratorStyle(base).zoomed(baseZoom));
profile.setDetailDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.5D));
profile.setBaseWeight(0.82D);
profile.setDetailWeight(0.15D);
profile.setDensityThreshold(new IrisStyledRange(threshold - 0.06D, threshold + 0.05D,
new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.78D)));
profile.setThresholdBias(0.12D);
profile.setSampleStep(2);
profile.setModules(new KList<>(modules));
return profile;
}
private static IrisCaveFieldModule packModule(NoiseStyle style, double zoom, double weight, double threshold,
double minY, double maxY, boolean invert) {
return new IrisCaveFieldModule(new IrisGeneratorStyle(style).zoomed(zoom), weight, threshold,
new IrisRange(minY, maxY), invert);
}
private enum Scenario {
PACK_SHARED,
WARP_MODULES,
WARP_TERRAIN,
STYLED_BLEND,
FLUID_BOUNDARY,
NO_WARP_MODULES,
VERTICAL_STEP,
LATTICE,
FLOOR_FLUID_LAYERED;
IrisCaveProfile[] profiles() {
if (this == PACK_SHARED) {
return new IrisCaveProfile[]{
packProfile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D, NoiseStyle.PERLIN_IRIS, 0.72D, -0.02D,
packModule(NoiseStyle.FRACTAL_FBM_SIMPLEX, 0.24D, 0.08D, 0.03D, 10D, 140D, false),
packModule(NoiseStyle.SIMPLEX_VASCULAR, 1.02D, 0.05D, 0.07D, 4D, 90D, true)),
packProfile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D, NoiseStyle.FRACTAL_BILLOW_SIMPLEX, 0.656D, 0.01D,
packModule(NoiseStyle.CELLULAR_BILINEAR, 1.32D, 0.06D, 0.02D, 20D, 150D, false)),
packProfile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D, NoiseStyle.PERLIN_IRIS, 0.72D, 0.04D,
packModule(NoiseStyle.FRACTAL_FBM_SIMPLEX, 0.24D, 0.08D, 0.03D, 10D, 140D, false),
packModule(NoiseStyle.SIMPLEX_VASCULAR, 1.02D, 0.05D, 0.07D, 4D, 90D, true)),
packProfile(NoiseStyle.PERLIN_IRIS, 0.35D, 2.4D, NoiseStyle.IRIS_DOUBLE, 0.68D, -0.04D,
packModule(NoiseStyle.VASCULAR_THIN, 1.28D, 0.04D, 0.05D, 8D, 120D, false))
};
}
if (this == FLOOR_FLUID_LAYERED) {
return new IrisCaveProfile[]{
baseProfile(true, true).setAllowFluid(true).setFluidRequiresFloor(true).setFluidMinDepthBelowSurface(4),
baseProfile(true, false).setAllowFluid(true).setFluidRequiresFloor(true).setFluidMinDepthBelowSurface(2)
.setAllowLava(false).setDensityThreshold(new IrisStyledRange(-0.24D, -0.08D,
new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.45D)))
};
}
return new IrisCaveProfile[]{profile()};
}
IrisCaveProfile profile() {
return switch (this) {
case WARP_MODULES, VERTICAL_STEP -> baseProfile(true, true);
case WARP_TERRAIN -> baseProfile(true, false);
case STYLED_BLEND -> baseProfile(false, false).setDensityThreshold(new IrisStyledRange(-0.32D, -0.04D,
new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.6D)));
case FLUID_BOUNDARY -> baseProfile(true, true).setAllowFluid(true).setFluidRequiresFloor(false)
.setFluidMinDepthBelowSurface(6).setAllowLava(false);
case NO_WARP_MODULES -> baseProfile(false, true);
case LATTICE -> baseProfile(true, true).setSampleStep(3);
case FLOOR_FLUID_LAYERED, PACK_SHARED -> baseProfile(true, true);
};
}
double[] weights(int layer, int chunkX, int chunkZ) {
double[] weights = new double[256];
for (int index = 0; index < 256; index++) {
int worldX = (chunkX << 4) + (index >> 4);
int worldZ = (chunkZ << 4) + (index & 15);
double blend = 0.5D + 0.6D * Math.sin(worldX * 0.11D) * Math.cos(worldZ * 0.07D);
weights[index] = switch (this) {
case PACK_SHARED -> 0.5D + 0.6D * Math.sin(worldX * 0.13D + layer * 1.7D) * Math.cos(worldZ * 0.09D - layer);
case STYLED_BLEND, FLUID_BOUNDARY -> blend;
case FLOOR_FLUID_LAYERED -> layer == 0 ? blend : 1D - blend;
default -> 1D;
};
}
return weights;
}
int[] surfaceHeights(int chunkX, int chunkZ) {
int[] heights = new int[256];
for (int index = 0; index < 256; index++) {
int worldX = (chunkX << 4) + (index >> 4);
int worldZ = (chunkZ << 4) + (index & 15);
heights[index] = this == WARP_MODULES || this == LATTICE || this == FLOOR_FLUID_LAYERED
? 118
: 96 + (int) Math.round(22D * Math.sin(worldX * 0.09D) + 15D * Math.cos(worldZ * 0.13D));
}
return heights;
}
IrisRange worldYRange() {
return this == FLUID_BOUNDARY || this == FLOOR_FLUID_LAYERED ? new IrisRange(-20D, 100D) : null;
}
long[] boundaries(int chunkX, int chunkZ) {
long[] boundaries = new long[256];
Arrays.fill(boundaries, SurfaceFluidBoundaryPlan.boundary(SurfaceFluidBoundaryPlan.NO_BOUNDARY, Integer.MIN_VALUE));
if (this != FLUID_BOUNDARY && this != WARP_TERRAIN) {
return boundaries;
}
for (int index = 0; index < 256; index++) {
int worldX = (chunkX << 4) + (index >> 4);
if (Math.floorMod(worldX + chunkZ, 5) == 0) {
boundaries[index] = SurfaceFluidBoundaryPlan.boundary(60 + Math.floorMod(index, 7), 82);
}
}
return boundaries;
}
}
private record Result(long hash, int writes, int carved) {
}
private static final class Capture {
private final Map<Long, Map<Integer, Matter>> chunks = new HashMap<>();
private final IdentityHashMap<MatterCavern, Integer> identities = new IdentityHashMap<>();
private final List<Long> resolved = new ArrayList<>();
private long hash = 0xcbf29ce484222325L;
private int writes;
private boolean resolving;
private MantleWriter writer(int chunkX, int chunkZ) {
MantleWriter writer = mock(MantleWriter.class, withSettings().stubOnly());
@SuppressWarnings("unchecked")
Mantle<Matter> mantle = mock(Mantle.class, withSettings().stubOnly());
@SuppressWarnings("unchecked")
MantleChunk<Matter> chunk = mock(MantleChunk.class, withSettings().stubOnly());
long key = ((long) chunkX << 32) ^ (chunkZ & 0xffffffffL);
Map<Integer, Matter> sections = chunks.computeIfAbsent(key, ignored -> new HashMap<>());
doReturn(mantle).when(writer).getMantle();
doReturn(WORLD_HEIGHT).when(mantle).getWorldHeight();
doReturn(chunk).when(writer).acquireChunk(anyInt(), anyInt());
doAnswer(invocation -> sections.get(invocation.getArgument(0, Integer.class))).when(chunk).get(anyInt());
doAnswer(invocation -> sections.computeIfAbsent(invocation.getArgument(0, Integer.class),
sectionIndex -> createSection(chunkX, chunkZ, sectionIndex))).when(chunk).getOrCreate(anyInt());
return writer;
}
private Matter createSection(int chunkX, int chunkZ, int sectionIndex) {
mix(0x5EC7L);
mix(chunkX);
mix(chunkZ);
mix(sectionIndex);
Matter matter = mock(Matter.class, withSettings().stubOnly());
MatterSlice<MatterCavern> slice = new RecordingCavernMatter(this, sectionIndex);
doReturn(slice).when(matter).slice(MatterCavern.class);
doReturn(slice).when(matter).getSlice(MatterCavern.class);
return matter;
}
private void record(int sectionIndex, int x, int y, int z, MatterCavern value) {
writes++;
if (resolving) {
resolved.add(((long) sectionIndex << 20) | ((long) x << 16) | ((long) y << 12) | ((long) z << 8) | value.getLiquid());
return;
}
mix(sectionIndex);
mix((x << 8) | (y << 4) | z);
mix(value == null ? -1 : value.getLiquid());
Integer identity = identities.get(value);
if (identity == null) {
identity = identities.size();
identities.put(value, identity);
}
mix(identity);
}
private void finishResolve() {
resolving = false;
resolved.sort(Long::compare);
for (long entry : resolved) {
mix(entry);
}
resolved.clear();
}
private void mix(long value) {
hash ^= value;
hash *= 0x100000001b3L;
}
}
private static final class RecordingCavernMatter extends CavernMatter {
private final Capture capture;
private final int sectionIndex;
private RecordingCavernMatter(Capture capture, int sectionIndex) {
super(16, 16, 16);
this.capture = capture;
this.sectionIndex = sectionIndex;
}
@Override
public void set(int x, int y, int z, MatterCavern value) {
super.set(x, y, z, value);
capture.record(sectionIndex, x, y, z, value);
}
}
}
@@ -0,0 +1,163 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.cave.IrisCaveFieldModule;
import art.arcane.iris.generation.cave.IrisCaveProfile;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.IrisStyledRange;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EngineMetrics;
import art.arcane.iris.generation.runtime.SeedManager;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.iris.world.IrisWorld;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Random;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.withSettings;
public class IrisCaveCarver3DLatticeTest {
private static final long CARVE_SEED = 7_340_119_802_557L;
private static final int WORLD_HEIGHT = 384;
@Test
public void batchedLatticeColumnsMatchScalarPlaneDensityBitForBit() throws Exception {
Engine engine = createEngine();
CaveDensityGrid grid = new CaveDensityGrid(256);
IrisCaveProfile[] profiles = {
profile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D, NoiseStyle.PERLIN_IRIS, 0.72D,
module(NoiseStyle.FRACTAL_FBM_SIMPLEX, 0.24D, 10D, 140D, false),
module(NoiseStyle.CELLULAR_BILINEAR, 1.32D, 4D, 300D, true)),
profile(NoiseStyle.PERLIN_IRIS, 0.35D, 2.4D, NoiseStyle.IRIS_DOUBLE, 0.68D,
module(NoiseStyle.VASCULAR_THIN, 1.28D, 8D, 220D, false)),
profile(NoiseStyle.SIMPLEX, 0.5D, 0D, NoiseStyle.FRACTAL_BILLOW_SIMPLEX, 0.656D)
};
Method sampleLatticeColumn = IrisCaveCarver3D.class.getDeclaredMethod("sampleLatticeColumn",
CaveCarveScratch.class, CaveDensityGrid.Column.class, int.class, int.class, int.class);
sampleLatticeColumn.setAccessible(true);
Method samplePlaneDensity = IrisCaveCarver3D.class.getDeclaredMethod("samplePlaneDensity",
CaveCarveScratch.class, int.class, int.class, int.class);
samplePlaneDensity.setAccessible(true);
Field warpOwnerField = IrisCaveCarver3D.class.getDeclaredField("warpOwner");
warpOwnerField.setAccessible(true);
Random random = new Random(91_337L);
for (IrisCaveProfile profile : profiles) {
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile, grid);
CaveDensityGrid.Owner warpOwner = (CaveDensityGrid.Owner) warpOwnerField.get(carver);
CaveCarveScratch batch = new CaveCarveScratch();
CaveCarveScratch reference = new CaveCarveScratch();
for (int trial = 0; trial < 48; trial++) {
int latticeX = random.nextInt(4001) - 2000;
int latticeZ = random.nextInt(4001) - 2000;
int x = latticeX << CaveDensityGrid.LATTICE_SHIFT;
int z = latticeZ << CaveDensityGrid.LATTICE_SHIFT;
int count = selectRows(random, batch);
CaveDensityGrid.Column warpColumn = trial % 3 == 0 ? null
: grid.warpColumn(warpOwner, WORLD_HEIGHT + 1, latticeX, latticeZ);
for (int pass = 0; pass < 2; pass++) {
sampleLatticeColumn.invoke(carver, batch, warpColumn, x, z, count);
for (int index = 0; index < count; index++) {
int y = batch.latticeY[index];
double expected = (double) samplePlaneDensity.invoke(carver, reference, x, y, z);
assertEquals("x=" + x + " y=" + y + " z=" + z + " pass=" + pass,
Double.doubleToRawLongBits(expected), Double.doubleToRawLongBits(batch.latticeDensity[index]));
}
}
}
}
}
@Test
public void identicalFieldsShareGridOwnersAcrossProfiles() throws Exception {
Engine engine = createEngine();
CaveDensityGrid grid = new CaveDensityGrid(256);
Field densityOwnerField = IrisCaveCarver3D.class.getDeclaredField("densityOwner");
densityOwnerField.setAccessible(true);
Field warpOwnerField = IrisCaveCarver3D.class.getDeclaredField("warpOwner");
warpOwnerField.setAccessible(true);
IrisCaveCarver3D first = new IrisCaveCarver3D(engine, profile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D,
NoiseStyle.PERLIN_IRIS, 0.72D, module(NoiseStyle.SIMPLEX_VASCULAR, 1.02D, 4D, 90D, true)), grid);
IrisCaveCarver3D sameDensity = new IrisCaveCarver3D(engine, profile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D,
NoiseStyle.PERLIN_IRIS, 0.72D, module(NoiseStyle.SIMPLEX_VASCULAR, 1.02D, 4D, 90D, true))
.setDensityThreshold(new IrisStyledRange(0.2D, 0.2D, new IrisGeneratorStyle(NoiseStyle.FLAT))), grid);
IrisCaveCarver3D sameWarp = new IrisCaveCarver3D(engine, profile(NoiseStyle.FRACTAL_WATER, 0.44D, 1.7D,
NoiseStyle.FRACTAL_BILLOW_SIMPLEX, 0.656D), grid);
IrisCaveCarver3D otherWarp = new IrisCaveCarver3D(engine, profile(NoiseStyle.FRACTAL_WATER, 0.46D, 3.2D,
NoiseStyle.PERLIN_IRIS, 0.72D, module(NoiseStyle.SIMPLEX_VASCULAR, 1.02D, 4D, 90D, true)), grid);
IrisCaveCarver3D otherModule = new IrisCaveCarver3D(engine, profile(NoiseStyle.FRACTAL_WATER, 0.44D, 3.2D,
NoiseStyle.PERLIN_IRIS, 0.72D, module(NoiseStyle.SIMPLEX_VASCULAR, 1.02D, 4D, 91D, true)), grid);
assertSame(densityOwnerField.get(first), densityOwnerField.get(sameDensity));
assertSame(warpOwnerField.get(first), warpOwnerField.get(sameDensity));
assertSame(warpOwnerField.get(first), warpOwnerField.get(sameWarp));
assertSame(warpOwnerField.get(first), warpOwnerField.get(otherModule));
assertNotSame(densityOwnerField.get(first), densityOwnerField.get(sameWarp));
assertNotSame(densityOwnerField.get(first), densityOwnerField.get(otherModule));
assertNotSame(densityOwnerField.get(first), densityOwnerField.get(otherWarp));
assertNotSame(warpOwnerField.get(first), warpOwnerField.get(otherWarp));
}
private static int selectRows(Random random, CaveCarveScratch scratch) {
int start = random.nextInt(40);
int end = start + 20 + random.nextInt(WORLD_HEIGHT - start - 20);
scratch.ensureLatticeBatch(end - start + 1);
int count = 0;
for (int y = start; y <= end; y++) {
if (random.nextInt(5) != 0) {
scratch.latticeY[count++] = y;
}
}
return count;
}
private static IrisCaveProfile profile(NoiseStyle warp, double warpZoom, double warpStrength, NoiseStyle base,
double baseZoom, IrisCaveFieldModule... modules) {
IrisCaveProfile profile = new IrisCaveProfile();
profile.setEnabled(true);
profile.setVerticalRange(new IrisRange(4D, 360D));
profile.setWarpStyle(new IrisGeneratorStyle(warp).zoomed(warpZoom));
profile.setWarpStrength(warpStrength);
profile.setBaseDensityStyle(new IrisGeneratorStyle(base).zoomed(baseZoom));
profile.setDetailDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.5D));
profile.setSurfaceBreakStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.82D));
profile.setBaseWeight(0.82D);
profile.setDetailWeight(0.15D);
profile.setDensityThreshold(new IrisStyledRange(-0.1D, 0.02D, new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.78D)));
profile.setSampleStep(2);
profile.setModules(new KList<>(modules));
return profile;
}
private static IrisCaveFieldModule module(NoiseStyle style, double zoom, double minY, double maxY, boolean invert) {
return new IrisCaveFieldModule(new IrisGeneratorStyle(style).zoomed(zoom), 0.07D, 0.04D, new IrisRange(minY, maxY), invert);
}
private static Engine createEngine() {
Engine engine = mock(Engine.class, withSettings().stubOnly());
IrisData data = mock(IrisData.class, withSettings().stubOnly());
IrisDimension dimension = mock(IrisDimension.class, withSettings().stubOnly());
SeedManager seedManager = mock(SeedManager.class, withSettings().stubOnly());
doReturn(data).when(engine).getData();
doReturn(dimension).when(engine).getDimension();
doReturn(seedManager).when(engine).getSeedManager();
doReturn(CARVE_SEED).when(seedManager).getCarve();
doReturn(new EngineMetrics(16)).when(engine).getMetrics();
doReturn(IrisWorld.builder().minHeight(-64).maxHeight(WORLD_HEIGHT - 64).build()).when(engine).getWorld();
doReturn(WORLD_HEIGHT).when(engine).getHeight();
doReturn(200).when(engine).getHeight(anyInt(), anyInt());
doReturn(20).when(dimension).getCaveLavaHeight();
doReturn(64).when(dimension).getFluidHeight();
doReturn(1).when(dimension).getCaveDensitySamplingStep();
return engine;
}
}
@@ -13,6 +13,7 @@ import art.arcane.iris.generation.noise.IrisStyledRange;
import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.volmlib.util.noise.CNG;
import art.arcane.volmlib.util.noise.program.NoiseProgram;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
@@ -148,7 +149,7 @@ public class IrisCaveCarver3DNearParityTest {
sampledY.add((int) y);
return x * 0.0001D + y * 0.01D + z * 0.0002D;
}).when(density).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
baseDensityField.set(carver, density);
baseDensityField.set(carver, NoiseProgram.compileSigned3D(density));
Method prepare = IrisCaveCarver3D.class.getDeclaredMethod("prepareVerticalDensityPlane",
CaveCarveScratch.class, int.class, int.class, int.class);
prepare.setAccessible(true);
@@ -168,7 +169,7 @@ public class IrisCaveCarver3DNearParityTest {
doReturn(1).when(dimension).getCaveDensitySamplingStep();
IrisCaveCarver3D scalar = new IrisCaveCarver3D(engine,
createProfile(false, false).setDetailWeight(0D).setAllowFluid(false));
baseDensityField.set(scalar, density);
baseDensityField.set(scalar, NoiseProgram.compileSigned3D(density));
CaveCarveScratch scalarScratch = new CaveCarveScratch();
for (int index = 0; index < 256; index++) {
scalarScratch.planeColumnIndices[index] = index;
@@ -292,10 +293,10 @@ public class IrisCaveCarver3DNearParityTest {
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile);
CNG base = mock(CNG.class, withSettings().stubOnly());
doReturn(1D).when(base).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
baseDensityField.set(carver, base);
baseDensityField.set(carver, NoiseProgram.compileSigned3D(base));
CNG module = mock(CNG.class, withSettings().stubOnly());
doReturn(-1D).when(module).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
moduleDensityField.set(((CaveFieldModuleState[]) modulesField.get(carver))[0], module);
moduleDensityField.set(((CaveFieldModuleState[]) modulesField.get(carver))[0], NoiseProgram.compileSigned3D(module));
WriterCapture capture = createWriterCapture(128);
carver.carve(capture.writer, -1, 2, fullWeights(), 0D, 0D, null, filledHeights(110));
assertEquals(512, capture.carvedCells.size());
@@ -364,13 +365,13 @@ public class IrisCaveCarver3DNearParityTest {
}
return -1D;
}).when(density).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
baseDensityField.set(carver, density);
baseDensityField.set(carver, NoiseProgram.compileSigned3D(density));
assertThrows(IllegalStateException.class, () -> carver.carve(capture.writer, 0, 0,
fullWeights(), 0D, 0D, null, filledHeights(110)));
} else {
carver.carve(capture.writer, 0, 0, fullWeights(), 0D, 0D, null, filledHeights(110));
assertFalse(capture.carvedCells.isEmpty());
}
assertFalse(capture.carvedCells.isEmpty());
Field scratchField = IrisCaveCarver3D.class.getDeclaredField("scratchCache");
scratchField.setAccessible(true);
ThreadLocal<?> scratchCache = (ThreadLocal<?>) scratchField.get(carver);
@@ -385,6 +386,12 @@ public class IrisCaveCarver3DNearParityTest {
for (CaveDensityGrid.Column column : scratch.densityColumns) {
assertNull(column);
}
for (CaveDensityGrid.Column column : scratch.cellLattice.densityColumns) {
assertNull(column);
}
for (CaveDensityGrid.Column column : scratch.cellLattice.warpColumns) {
assertNull(column);
}
}
}
@@ -439,7 +446,7 @@ public class IrisCaveCarver3DNearParityTest {
}).when(detail).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
Field detailField = IrisCaveCarver3D.class.getDeclaredField("detailDensity");
detailField.setAccessible(true);
detailField.set(carver, detail);
detailField.set(carver, NoiseProgram.compileSigned3D(detail));
WriterCapture capture = createWriterCapture(128);
try (ForkJoinPool pool = new ForkJoinPool(4)) {
assertEquals(0, pool.submit(() -> carver.carve(capture.writer, 0, 0, fullWeights(),
@@ -772,7 +779,7 @@ public class IrisCaveCarver3DNearParityTest {
rejectedDensity.put("11:10:10", -1D);
rejectedDensity.put("9:10:10", -1D);
CoordinateDensityCNG rejectedNoise = new CoordinateDensityCNG(rejectedDensity);
baseDensityField.set(carver, rejectedNoise);
baseDensityField.set(carver, NoiseProgram.compileSigned3D(rejectedNoise));
boolean rejected = (boolean) aquiferCupSupportMethod.invoke(
carver, new CaveCarveScratch(), 10, 10, 10, 0D);
@@ -781,7 +788,7 @@ public class IrisCaveCarver3DNearParityTest {
assertEquals(List.of("10:9:10", "10:8:10", "11:10:10", "9:10:10"), rejectedNoise.sampledCoordinates);
CoordinateDensityCNG acceptedNoise = new CoordinateDensityCNG(Map.of());
baseDensityField.set(carver, acceptedNoise);
baseDensityField.set(carver, NoiseProgram.compileSigned3D(acceptedNoise));
boolean accepted = (boolean) aquiferCupSupportMethod.invoke(
carver, new CaveCarveScratch(), 10, 10, 10, 0D);
@@ -1100,7 +1107,7 @@ public class IrisCaveCarver3DNearParityTest {
AtomicLong isolatedSamples = new AtomicLong();
IrisCaveCarver3D shared = new IrisCaveCarver3D(engine,
createProfile(warp, modules).setAdaptiveSampling(true), new CaveDensityGrid(4096));
baseDensityField.set(shared, new CountingCNG((CNG) baseDensityField.get(shared), sharedSamples));
baseDensityField.set(shared, NoiseProgram.compileSigned3D(new CountingCNG((NoiseProgram) baseDensityField.get(shared), sharedSamples)));
for (int chunkX = -1; chunkX <= 1; chunkX++) {
for (int chunkZ = -1; chunkZ <= 1; chunkZ++) {
WriterCapture sharedCapture = createWriterCapture(96);
@@ -1108,7 +1115,7 @@ public class IrisCaveCarver3DNearParityTest {
null, filledHeights(90));
IrisCaveCarver3D isolated = new IrisCaveCarver3D(engine,
createProfile(warp, modules).setAdaptiveSampling(true));
baseDensityField.set(isolated, new CountingCNG((CNG) baseDensityField.get(isolated), isolatedSamples));
baseDensityField.set(isolated, NoiseProgram.compileSigned3D(new CountingCNG((NoiseProgram) baseDensityField.get(isolated), isolatedSamples)));
WriterCapture isolatedCapture = createWriterCapture(96);
int isolatedCarved = isolated.carve(isolatedCapture.writer, chunkX, chunkZ, fullWeights(), 0D, 0D,
null, filledHeights(90));
@@ -1241,7 +1248,7 @@ public class IrisCaveCarver3DNearParityTest {
new IrisCaveCarver3D(engine, profile).carve(expected.writer, -1, 2,
fullWeights(), 0D, 0D, null, filledHeights(110));
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile);
CNG original = (CNG) baseDensityField.get(carver);
NoiseProgram original = (NoiseProgram) baseDensityField.get(carver);
CNG density = mock(CNG.class, withSettings().stubOnly());
AtomicBoolean entered = new AtomicBoolean();
WriterCapture nested = createWriterCapture(128);
@@ -1252,9 +1259,9 @@ public class IrisCaveCarver3DNearParityTest {
double x = invocation.getArgument(0);
double y = invocation.getArgument(1);
double z = invocation.getArgument(2);
return original.noiseFastSigned3D(x, y, z);
return original.sampleSigned3D(x, y, z);
}).when(density).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
baseDensityField.set(carver, density);
baseDensityField.set(carver, NoiseProgram.compileSigned3D(density));
WriterCapture actual = createWriterCapture(128);
carver.carve(actual.writer, -1, 2, fullWeights(), 0D, 0D, null, filledHeights(110));
assertTrue(entered.get());
@@ -1268,7 +1275,7 @@ public class IrisCaveCarver3DNearParityTest {
doAnswer(invocation -> {
lookups.incrementAndGet();
return invocation.callRealMethod();
}).when(grid).column(any(), anyInt(), anyInt(), anyInt(), anyInt());
}).when(grid).column(any(), anyInt(), anyInt(), anyInt());
return grid;
}
@@ -1292,10 +1299,10 @@ public class IrisCaveCarver3DNearParityTest {
}
private void countSkippableSamples(IrisCaveCarver3D carver, AtomicLong samples) throws Exception {
detailDensityField.set(carver, new CountingCNG((CNG) detailDensityField.get(carver), samples));
detailDensityField.set(carver, NoiseProgram.compileSigned3D(new CountingCNG((NoiseProgram) detailDensityField.get(carver), samples)));
CaveFieldModuleState[] modules = (CaveFieldModuleState[]) modulesField.get(carver);
for (CaveFieldModuleState module : modules) {
moduleDensityField.set(module, new CountingCNG((CNG) moduleDensityField.get(module), samples));
moduleDensityField.set(module, NoiseProgram.compileSigned3D(new CountingCNG((NoiseProgram) moduleDensityField.get(module), samples)));
}
}
@@ -1893,10 +1900,10 @@ public class IrisCaveCarver3DNearParityTest {
}
private static final class CountingCNG extends CNG {
private final CNG delegate;
private final NoiseProgram delegate;
private final AtomicLong samples;
private CountingCNG(CNG delegate, AtomicLong samples) {
private CountingCNG(NoiseProgram delegate, AtomicLong samples) {
super(new RNG(41_117L));
this.delegate = delegate;
this.samples = samples;
@@ -1905,7 +1912,7 @@ public class IrisCaveCarver3DNearParityTest {
@Override
public double noiseFastSigned3D(double x, double y, double z) {
samples.incrementAndGet();
return delegate.noiseFastSigned3D(x, y, z);
return delegate.sampleSigned3D(x, y, z);
}
}
@@ -31,11 +31,6 @@ import art.arcane.iris.testsupport.PlatformBinding;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
@@ -45,22 +40,12 @@ public class MantleFloatingObjectComponentBoundaryRadiusTest {
@Rule
public final PlatformBinding platform = PlatformBinding.mockPlatform();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
@SuppressWarnings("unchecked")
public void transformedFloatingPlacementExpandsOwnerRadius() throws Exception {
File objectFile = temporaryFolder.newFile("floating.iob");
try (DataOutputStream output = new DataOutputStream(new FileOutputStream(objectFile))) {
output.writeInt(1);
output.writeInt(1);
output.writeInt(1);
}
public void transformedFloatingPlacementExpandsOwnerRadius() {
IrisObjectPlacement placement = new IrisObjectPlacement()
.setPlace(new KList<>("test/floating"))
.setTranslate(new IrisObjectTranslate().setX(32));
.setTranslate(new IrisObjectTranslate().setX(-32));
IrisFloatingChildBiomes floatingChild = new IrisFloatingChildBiomes()
.setFloatingObjects(new KList<>(placement));
IrisBiome biome = new IrisBiome().setFloatingChildBiomes(new KList<>(floatingChild));
@@ -69,7 +54,7 @@ public class MantleFloatingObjectComponentBoundaryRadiusTest {
when(dimension.getReachableBiomes(org.mockito.ArgumentMatchers.any())).thenReturn(new KList<>(biome));
ResourceLoader<IrisObject> objectLoader = mock(ResourceLoader.class);
when(objectLoader.findFile("test/floating")).thenReturn(objectFile);
when(objectLoader.load("test/floating")).thenReturn(new IrisObject(1, 1, 1));
IrisData data = mock(IrisData.class);
when(data.getObjectLoader()).thenReturn(objectLoader);
@@ -79,8 +64,8 @@ public class MantleFloatingObjectComponentBoundaryRadiusTest {
when(engineMantle.getEngine()).thenReturn(engine);
when(engineMantle.getData()).thenReturn(data);
assertEquals(33, new MantleFloatingObjectComponent(engineMantle).getRadius());
assertEquals(32, new MantleFloatingObjectComponent(engineMantle).getRadius());
when(dimension.getAllObjectScaleFactor()).thenReturn(2D);
assertEquals(34, new MantleFloatingObjectComponent(engineMantle).getRadius());
assertEquals(33, new MantleFloatingObjectComponent(engineMantle).getRadius());
}
}
@@ -24,7 +24,6 @@ import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
@@ -33,13 +32,8 @@ import art.arcane.volmlib.util.matter.Matter;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@@ -60,9 +54,6 @@ public class MantleObjectComponentBoundaryRadiusTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Before
public void bindPlatform() {
IrisPlatforms.unbind();
@@ -92,7 +83,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
.setRotation(IrisObjectRotation.of(90, 0, 0))
.setTranslate(new IrisObjectTranslate().setX(3).setY(4));
assertEquals(53, MantleObjectComponent.calculatePlacementReach(new IrisBlockVector(5, 48, 7), placement, 1D));
assertEquals(27, PlacementReach.of(new IrisObject(5, 48, 7), placement, 1D).horizontal());
}
@Test
@@ -103,7 +94,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
.setMode(ObjectPlaceMode.VACUUM)
.setVacuumSettings(new IrisVacuumSettings().setRadius(12));
assertEquals(39, MantleObjectComponent.calculatePlacementReach(new IrisBlockVector(5, 4, 3), placement, placement.getMaximumScale(null)));
assertEquals(22, PlacementReach.of(new IrisObject(5, 4, 3), placement, placement.getMaximumScale(null)).horizontal());
}
@Test
@@ -126,15 +117,12 @@ public class MantleObjectComponentBoundaryRadiusTest {
public void inheritedRadiusMatchesScaledFrameAndExplicitScaleOverridesIt() {
IrisDimension dimension = new IrisDimension().setAllObjectScaleFactor(2D);
IrisObjectPlacement inherited = new IrisObjectPlacement();
IrisBlockVector size = new IrisBlockVector(8, 8, 8);
assertEquals(16, MantleObjectComponent.calculatePlacementReach(size, inherited,
inherited.getMaximumScale(dimension)));
IrisObject object = new IrisObject(8, 8, 8);
assertEquals(8, PlacementReach.of(object, inherited, inherited.getMaximumScale(dimension)).horizontal());
dimension.setAllObjectScaleFactor(0.5D);
assertEquals(4, MantleObjectComponent.calculatePlacementReach(size, inherited,
inherited.getMaximumScale(dimension)));
assertEquals(2, PlacementReach.of(object, inherited, inherited.getMaximumScale(dimension)).horizontal());
inherited.setScale(new IrisObjectScale().setSize(1D));
assertEquals(8, MantleObjectComponent.calculatePlacementReach(size, inherited,
inherited.getMaximumScale(dimension)));
assertEquals(4, PlacementReach.of(object, inherited, inherited.getMaximumScale(dimension)).horizontal());
}
@Test
@@ -183,10 +171,11 @@ public class MantleObjectComponentBoundaryRadiusTest {
@Test
public void collisionInputRadiusCoversBothSourceTraversalLegs() {
assertEquals(82, MantleObjectComponent.calculateInputRadius(33, false));
assertEquals(130, MantleObjectComponent.calculateInputRadius(33, true));
assertEquals(84, MantleObjectComponent.calculateInputRadius(33, 35, false));
assertEquals(132, MantleObjectComponent.calculateInputRadius(33, 35, true));
assertEquals(82, MantleObjectComponent.calculateInputRadius(33, 0, false));
assertEquals(Integer.MAX_VALUE,
MantleObjectComponent.calculateInputRadius(Integer.MAX_VALUE, true));
MantleObjectComponent.calculateInputRadius(Integer.MAX_VALUE, Integer.MAX_VALUE, true));
}
@Test
@@ -199,7 +188,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
for (int radius = 0; radius < 300; radius++) {
for (boolean collisions : new boolean[]{false, true}) {
assertTrue(MantleObjectComponent.calculateEagerInputRadius(radius, collisions)
<= MantleObjectComponent.calculateInputRadius(radius, collisions));
<= MantleObjectComponent.calculateInputRadius(radius, radius, collisions));
}
}
assertEquals(Integer.MAX_VALUE,
@@ -231,27 +220,25 @@ public class MantleObjectComponentBoundaryRadiusTest {
when(engineMantle.getEngine()).thenReturn(engine);
when(engineMantle.getData()).thenReturn(data);
assertEquals(53, new MantleObjectComponent(engineMantle).getRadius());
assertEquals(27, new MantleObjectComponent(engineMantle).getRadius());
}
@Test
@SuppressWarnings("unchecked")
public void upperDimensionObjectsDriveRadiusAndCollisionMode() {
IrisObjectPlacement placement = new IrisObjectPlacement()
.setPlace(new KList<>("upper/tree"))
.setRotation(IrisObjectRotation.of(90, 0, 0))
.setTranslate(new IrisObjectTranslate().setX(3).setY(4));
placement.getForbiddenCollisions().add("upper/blocker");
IrisData baseData = mock(IrisData.class);
IrisData upperData = mock(IrisData.class);
IrisProceduralPlacement proceduralPlacement = mock(IrisProceduralPlacement.class);
when(proceduralPlacement.getVariantObjects(upperData))
.thenReturn(new KList<>(new IrisObject(5, 48, 7)));
when(proceduralPlacement.asPlacement()).thenReturn(placement);
IrisProceduralObjects proceduralObjects = mock(IrisProceduralObjects.class);
when(proceduralObjects.isEmpty()).thenReturn(false);
when(proceduralObjects.getAllPlacements()).thenReturn(new KList<>(proceduralPlacement));
ResourceLoader<IrisObject> upperObjects = mock(ResourceLoader.class);
when(upperObjects.load("upper/tree")).thenReturn(new IrisObject(5, 48, 7));
when(upperData.getObjectLoader()).thenReturn(upperObjects);
IrisRegion upperRegion = mock(IrisRegion.class);
when(upperRegion.getObjects()).thenReturn(new KList<>());
when(upperRegion.getProceduralObjects()).thenReturn(proceduralObjects);
when(upperRegion.getObjects()).thenReturn(new KList<>(placement));
when(upperRegion.getSurfaceObjects()).thenReturn(new KList<>(placement));
IrisDimension baseDimension = mock(IrisDimension.class);
when(baseDimension.getAllObjectScaleFactor()).thenReturn(1D);
when(baseDimension.isUpperDimensionObjects()).thenReturn(true);
@@ -273,20 +260,13 @@ public class MantleObjectComponentBoundaryRadiusTest {
MantleObjectComponent component = new MantleObjectComponent(engineMantle);
assertEquals(53, component.getRadius());
assertEquals(182, component.getInputRadius());
assertEquals(5, component.getRadius());
assertEquals(41, component.getInputRadius());
}
@Test
@SuppressWarnings("unchecked")
public void translatedTreeCachesDistantSourcesAndSchedulesOnlyItsDestination() throws Exception {
File objectFile = temporaryFolder.newFile("tree.iob");
try (DataOutputStream output = new DataOutputStream(new FileOutputStream(objectFile))) {
output.writeInt(1);
output.writeInt(1);
output.writeInt(1);
}
public void translatedTreeCachesDistantSourcesAndSchedulesOnlyItsDestination() {
IrisObjectPlacement placement = new IrisObjectPlacement()
.setPlace(new KList<>("test/tree"))
.setTranslate(new IrisObjectTranslate().setX(32));
@@ -298,7 +278,7 @@ public class MantleObjectComponentBoundaryRadiusTest {
when(dimension.getReachableBiomes(any())).thenReturn(new KList<>(biome));
ResourceLoader<IrisObject> objectLoader = mock(ResourceLoader.class);
when(objectLoader.findFile("test/tree")).thenReturn(objectFile);
when(objectLoader.load("test/tree")).thenReturn(new IrisObject(1, 1, 1));
IrisData data = mock(IrisData.class);
when(data.getObjectLoader()).thenReturn(objectLoader);
@@ -310,9 +290,9 @@ public class MantleObjectComponentBoundaryRadiusTest {
when(engineMantle.getData()).thenReturn(data);
MantleObjectComponent component = spy(new MantleObjectComponent(engineMantle));
assertEquals(33, component.getRadius());
assertEquals(32, component.getRadius());
assertEquals(0, component.getOutputRadius());
assertEquals(82, component.getInputRadius());
assertEquals(68, component.getInputRadius());
Mantle<Matter> mantle = mock(Mantle.class);
MantleChunk<Matter> chunk = mock(MantleChunk.class);
@@ -341,9 +321,9 @@ public class MantleObjectComponentBoundaryRadiusTest {
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
assertEquals(49, directOrigins.get());
assertEquals(25, directOrigins.get());
component.generateLayer(directWriter, 0, 0, mock(ChunkContext.class));
assertEquals(49, directOrigins.get());
assertEquals(25, directOrigins.get());
List<String> generatedChunks = new ArrayList<>();
doAnswer(invocation -> {
@@ -370,14 +350,14 @@ public class MantleObjectComponentBoundaryRadiusTest {
component.getRadius(),
(chunkX, chunkZ) -> replayedSources.add(chunkX + "," + chunkZ)
);
assertEquals(49, replayedSources.size());
assertEquals("-3,-3", replayedSources.getFirst());
assertEquals("-3,-2", replayedSources.get(1));
assertEquals("3,3", replayedSources.getLast());
assertEquals(25, replayedSources.size());
assertEquals("-2,-2", replayedSources.getFirst());
assertEquals("-2,-1", replayedSources.get(1));
assertEquals("2,2", replayedSources.getLast());
placement.getForbiddenCollisions().add("test/tree");
component.hotload();
assertEquals(130, component.getInputRadius());
assertEquals(100, component.getInputRadius());
}
private static final class TestMatterGenerator implements MatterGenerator {
@@ -76,7 +76,7 @@ public class MatterGeneratorConcurrencyTest {
@Test
public void externalWindowsBoundManagedBlockedComponentSubmissions() throws Exception {
GeneratorFixture fixture = new GeneratorFixture(true);
int maximum = 2 * MatterGenerator.COMPONENT_SUBMISSION_BATCH_SIZE;
int maximum = 2 * MatterGenerator.COMPONENT_SUBMISSION_LIMIT;
CountDownLatch admitted = new CountDownLatch(maximum);
CountDownLatch excess = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
@@ -115,10 +115,10 @@ public class MatterGeneratorConcurrencyTest {
}
@Test
public void externalWindowDefersSharedClaimsUntilItsOwnBatchesComplete() throws Exception {
public void externalWindowDefersSharedClaimsUntilItsOwnSubmissionsComplete() throws Exception {
GeneratorFixture fixture = new GeneratorFixture(true);
List<MatterGenerator.MatterComponentTask> shared = new ArrayList<>();
for (int z = -4; z < -4 + MatterGenerator.COMPONENT_SUBMISSION_BATCH_SIZE; z++) {
for (int z = -4; z < -4 + MatterGenerator.COMPONENT_SUBMISSION_LIMIT; z++) {
MatterGenerator.MatterTaskKey key = new MatterGenerator.MatterTaskKey(
fixture.mantle, -4, z, ReservedFlag.OBJECT);
MatterGenerator.MatterComponentTask task = new MatterGenerator.MatterComponentTask(key);
@@ -137,7 +137,7 @@ public class MatterGeneratorConcurrencyTest {
CompletableFuture<Void> generation = CompletableFuture.runAsync(
() -> generator.generateMatter(0, 0, true, fixture.context));
try {
Await.until("all privately owned component batches", Duration.ofSeconds(10L),
Await.until("all privately owned component submissions", Duration.ofSeconds(10L),
() -> completed.get() == 81 - shared.size());
assertFalse(generation.isDone());
} finally {
@@ -149,16 +149,17 @@ public class MatterGeneratorConcurrencyTest {
}
@Test
public void failedSubmissionBatchDrainsItsWritersBeforeReturning() throws Exception {
public void failedSubmissionDrainsItsWritersBeforeReturning() throws Exception {
GeneratorFixture fixture = new GeneratorFixture(true);
CountDownLatch failed = new CountDownLatch(1);
CountDownLatch started = new CountDownLatch(MatterGenerator.COMPONENT_SUBMISSION_BATCH_SIZE - 1);
CountDownLatch started = new CountDownLatch(MatterGenerator.COMPONENT_SUBMISSION_LIMIT - 1);
CountDownLatch release = new CountDownLatch(1);
IllegalStateException failure = new IllegalStateException("Component rejected its input");
RecordingComponent component = new RecordingComponent(ReservedFlag.OBJECT, 0, 64) {
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
if (x == -4 && z == -4) {
await(started);
failed.countDown();
throw failure;
}
@@ -172,7 +173,7 @@ public class MatterGeneratorConcurrencyTest {
try {
await(failed);
await(started);
if (MatterGenerator.COMPONENT_SUBMISSION_BATCH_SIZE > 1) {
if (MatterGenerator.COMPONENT_SUBMISSION_LIMIT > 1) {
assertFalse(generation.isDone());
verify(fixture.mantle.getChunk(-4, -4), never()).release();
}
@@ -186,6 +187,69 @@ public class MatterGeneratorConcurrencyTest {
assertEquals(0, activeClaims(fixture, 0, 4));
}
@Test
public void externalWindowKeepsSubmittingPastAStragglingComponent() throws Exception {
GeneratorFixture fixture = new GeneratorFixture(true);
CountDownLatch release = new CountDownLatch(1);
AtomicInteger completed = new AtomicInteger();
RecordingComponent component = new RecordingComponent(ReservedFlag.OBJECT, 0, 64) {
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
if (x == -4 && z == -4) {
awaitManaged(release);
}
completed.incrementAndGet();
}
};
TestMatterGenerator generator = fixture.generator(List.of(new MantlePass(List.of(component), 4, 0)));
CompletableFuture<Void> generation = CompletableFuture.runAsync(
() -> generator.generateMatter(0, 0, true, fixture.context));
try {
Await.until("every chunk but the straggler", Duration.ofSeconds(10L), () -> completed.get() == 80);
assertFalse(generation.isDone());
} finally {
release.countDown();
}
generation.get(10L, TimeUnit.SECONDS);
assertEquals(81, completed.get());
assertEquals(0, activeClaims(fixture, 0, 4));
}
@Test
public void inlineWindowFinishesItsOwnChunksBeforeJoiningAClaimAnotherGenerationHolds() throws Exception {
GeneratorFixture fixture = new GeneratorFixture(true);
MantleChunk<Matter> held = fixture.mantle.getChunk(1, 0);
CountDownLatch claimed = new CountDownLatch(1);
CountDownLatch releaseClaim = new CountDownLatch(1);
Set<Long> completed = ConcurrentHashMap.newKeySet();
RecordingComponent component = new RecordingComponent(ReservedFlag.OBJECT, 0, 16) {
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
completed.add((((long) x) << 32) ^ (z & 0xffffffffL));
}
};
TestMatterGenerator generator = fixture.generator(List.of(new MantlePass(List.of(component), 1, 0)));
CompletableFuture<Void> holder = CompletableFuture.runAsync(() -> {
assertTrue(held.tryClaim(ReservedFlag.OBJECT));
claimed.countDown();
await(releaseClaim);
held.completeClaim(ReservedFlag.OBJECT);
});
await(claimed);
CompletableFuture<Void> generation = CompletableFuture.runAsync(
() -> generator.generateMatter(0, 0, false, fixture.context));
try {
Await.until("the eight unclaimed chunks", Duration.ofSeconds(10L), () -> completed.size() == 8);
assertFalse(generation.isDone());
} finally {
releaseClaim.countDown();
}
holder.get(10L, TimeUnit.SECONDS);
generation.get(10L, TimeUnit.SECONDS);
assertEquals(8, completed.size());
assertTrue(held.isFlagged(ReservedFlag.OBJECT));
}
@Test
public void boundedAndReentrantGenerationMatchSerialComponentResults() throws Exception {
Map<Long, Long> serial = componentResults(false, false);
@@ -812,6 +876,25 @@ public class MatterGeneratorConcurrencyTest {
}
return null;
}).when(chunk).raiseFlagSuspend(any(), any(Runnable.class));
when(chunk.tryClaim(any())).thenAnswer(invocation -> {
if (flagged.contains(invocation.getArgument(0)) || !lock.tryLock()) {
return false;
}
if (flagged.contains(invocation.getArgument(0))) {
lock.unlock();
return false;
}
return true;
});
doAnswer(invocation -> {
flagged.add(invocation.getArgument(0));
lock.unlock();
return null;
}).when(chunk).completeClaim(any());
doAnswer(invocation -> {
lock.unlock();
return null;
}).when(chunk).abandonClaim(any());
return chunk;
}
@@ -0,0 +1,517 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.biome.IrisFloatingChildBiomes;
import art.arcane.iris.generation.decoration.IrisProceduralObjects;
import art.arcane.iris.generation.decoration.IrisProceduralPlacement;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.MeteredCache;
import art.arcane.iris.generation.runtime.PreservationRegistry;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.structure.object.FloatingObjectFootprint;
import art.arcane.iris.structure.object.IrisAxisRotationClamp;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.structure.object.IrisObjectPlacement;
import art.arcane.iris.structure.object.IrisObjectRotation;
import art.arcane.iris.structure.object.IrisObjectScale;
import art.arcane.iris.structure.object.IrisObjectTranslate;
import art.arcane.iris.structure.object.IrisObjectPlacementScaleInterpolator;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.Vector3i;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import org.junit.After;
import org.junit.Assume;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.mockito.Answers;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.function.Consumer;
import java.util.stream.Stream;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
public class PlacementReachPackTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final int SPIN_PERIOD = 360;
private static final int MAXIMUM_CHECKED_KERNELS = 4096;
private static final String V2_HEADER = "Iris V2 IOB;";
private File pack;
private IrisData data;
private IrisDimension dimension;
@Before
public void loadPack() throws IOException {
String configured = System.getenv("IRIS_TEST_PACK");
Assume.assumeTrue(configured != null && !configured.isBlank());
pack = new File(configured).getAbsoluteFile();
Assume.assumeTrue(new File(pack, "objects").isDirectory());
File[] dimensions = new File(pack, "dimensions").listFiles((folder, name) -> name.endsWith(".json"));
Assume.assumeTrue(dimensions != null && dimensions.length > 0);
bindPlatform(Files.createTempDirectory("iris-placement-reach").toFile());
data = IrisData.get(pack);
String dimensionKey = System.getenv().getOrDefault("IRIS_TEST_PACK_DIMENSION",
dimensions[0].getName().substring(0, dimensions[0].getName().length() - 5));
dimension = data.getDimensionLoader().load(dimensionKey);
assertTrue("dimension " + dimensionKey + " must load", dimension != null);
}
@After
public void unbindPlatform() {
IrisServices.remove(PreservationRegistry.class);
IrisPlatforms.unbind();
}
@Test
public void everyObjectFileStaysInsideItsReachUnderRightAngleRotations() throws IOException {
List<IrisObjectPlacement> placements = List.of(
new IrisObjectPlacement(),
new IrisObjectPlacement().setRotation(rotation(new IrisAxisRotationClamp(true, false, 0D, 0D, 90D),
new IrisAxisRotationClamp(true, false, 0D, 0D, 90D), new IrisAxisRotationClamp())),
new IrisObjectPlacement().setRotation(rotation(new IrisAxisRotationClamp(),
new IrisAxisRotationClamp(true, false, 0D, 0D, 90D), new IrisAxisRotationClamp(true, false, 0D, 0D, 90D))),
new IrisObjectPlacement().setRotation(IrisObjectRotation.of(0D, 0D, 0D)),
new IrisObjectPlacement().setRotation(rotation(new IrisAxisRotationClamp(),
new IrisAxisRotationClamp(true, false, 0D, 0D, 45D), new IrisAxisRotationClamp()))
.setTranslate(new IrisObjectTranslate().setX(7).setZ(-3)));
List<Path> files;
try (Stream<Path> walk = Files.walk(new File(pack, "objects").toPath())) {
files = walk.filter(path -> path.toString().endsWith(".iob")).sorted().toList();
}
assertTrue(!files.isEmpty());
Statistics statistics = new Statistics();
NativeBlockState marker = mock(NativeBlockState.class, withSettings().stubOnly());
for (Path file : files) {
IrisObject object = readPositions(file, marker);
for (IrisObjectPlacement placement : placements) {
verifyCentered(file.toString(), object, placement, PlacementReach.of(object, placement, 1D), statistics);
}
}
System.out.println("PLACEMENT_REACH_FILES objects=" + files.size() + " " + statistics);
}
@Test
public void everyPackPlacementStaysInsideItsReach() {
Statistics statistics = new Statistics();
Set<IrisObjectPlacement> placements = new LinkedHashSet<>();
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
placements.addAll(region.getObjects());
}
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
placements.addAll(biome.getObjects());
}
for (IrisObjectPlacement placement : placements) {
for (String key : placement.getPlace()) {
IrisObject object = data.getObjectLoader().load(key);
if (object == null) {
continue;
}
PlacementReach reach = PlacementReach.of(object, placement, placement.getMaximumScale(dimension));
forEachVariant(object, placement, 1D, variant -> verifyCentered(key, variant, placement, reach, statistics));
}
}
for (IrisProceduralPlacement procedural : proceduralPlacements()) {
KList<IrisObject> variants = procedural.getVariantObjects(data);
if (variants == null) {
continue;
}
IrisObjectPlacement placement = procedural.asPlacement();
for (IrisObject variant : variants) {
PlacementReach reach = PlacementReach.of(variant, placement, placement.getMaximumScale(dimension));
forEachVariant(variant, placement, 1D,
scaled -> verifyCentered(variant.getLoadKey(), scaled, placement, reach, statistics));
}
}
assertTrue(statistics.checked > 0);
System.out.println("PLACEMENT_REACH_PLACEMENTS placements=" + placements.size() + " " + statistics);
}
@Test
public void floatingPlacementsStayInsideTheirReach() {
Statistics statistics = new Statistics();
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
KList<IrisFloatingChildBiomes> entries = biome.getFloatingChildBiomes();
if (entries == null) {
continue;
}
for (IrisFloatingChildBiomes entry : entries) {
double shrink = entry.hasObjectShrink() ? entry.getShrinkScale().getMaxScale() : 1D;
IrisBiome target = entry.getRealBiome(biome, data);
for (IrisObjectPlacement placement : listOrEmpty(entry.getFloatingObjects())) {
verifyFloating(placement, shrink, false, statistics);
}
List<IrisObjectPlacement> anchored = new ArrayList<>();
anchored.addAll(listOrEmpty(entry.getExtraObjects()));
anchored.addAll(listOrEmpty(entry.getTopObjectOverrides()));
anchored.addAll(listOrEmpty(entry.getBottomObjectOverrides()));
if (target != null) {
anchored.addAll(listOrEmpty(entry.resolveTopObjects(target)));
anchored.addAll(listOrEmpty(entry.resolveBottomObjects(target)));
}
for (IrisObjectPlacement placement : anchored) {
verifyFloating(placement, shrink, true, statistics);
}
}
}
System.out.println("PLACEMENT_REACH_FLOATING " + statistics);
}
@Test
public void reportsTheMantleWindow() {
Engine engine = mock(Engine.class);
when(engine.getDimension()).thenReturn(dimension);
EngineMantle engineMantle = mock(EngineMantle.class);
when(engineMantle.getEngine()).thenReturn(engine);
when(engineMantle.getData()).thenReturn(data);
MantleObjectComponent objects = new MantleObjectComponent(engineMantle);
MantleFloatingObjectComponent floating = new MantleFloatingObjectComponent(engineMantle);
int objectRadius = objects.getRadius();
int objectInput = objects.getInputRadius();
int objectEager = objects.getEagerInputRadius(objectInput);
int floatingRadius = floating.getRadius();
int floatingPassRadius = chunkRadius(floatingRadius);
int objectPassRadius = chunkRadius(floatingRadius);
int terrainRadius = chunkRadius(floatingRadius + objectEager + 1);
int writerRadius = 2 * chunkRadius(floatingRadius + objectInput + 1);
System.out.println("PLACEMENT_REACH_WINDOW objectReach=" + objectRadius
+ " objectInput=" + objectInput
+ " objectEager=" + objectEager
+ " sourceChunkRadius=" + MantleObjectComponent.sourceChunkRadius(objectRadius)
+ " floatingReach=" + floatingRadius
+ " floatingPassRadius=" + floatingPassRadius
+ " objectPassRadius=" + objectPassRadius
+ " terrainRadius=" + terrainRadius
+ " terrainWindow=" + ((2 * terrainRadius) + 1) + "x" + ((2 * terrainRadius) + 1)
+ " writerRadius=" + writerRadius);
assertTrue(objectRadius > 0);
}
private void verifyFloating(IrisObjectPlacement placement, double shrink, boolean anchored, Statistics statistics) {
if (placement == null || placement.getPlace() == null) {
return;
}
for (String key : placement.getPlace()) {
IrisObject object = data.getObjectLoader().load(key);
if (object == null) {
continue;
}
PlacementReach.Extent extent = PlacementReach.Extent.of(object)
.scaled(placement.getMaximumScale(dimension)).scaled(shrink);
if (!anchored) {
PlacementReach reach = PlacementReach.of(extent, placement);
forEachVariant(object, placement, shrink, variant -> verifyCentered(key, variant, placement, reach, statistics));
continue;
}
PlacementReach reach = PlacementReach.anchored(extent, placement);
forEachVariant(object, placement, shrink, variant -> verifyAnchored(key, variant, reach, statistics));
}
}
private void verifyAnchored(String key, IrisObject variant, PlacementReach reach, Statistics statistics) {
FloatingObjectFootprint footprint = FloatingObjectFootprint.compute(variant);
List<IrisBlockVector> positions = positions(variant);
List<IrisObjectRotation> rotations = List.of(IrisObjectRotation.of(0D, 0D, 0D),
IrisObjectRotation.xFlip180WithY(0D), IrisObjectRotation.xFlip180WithY(90D),
IrisObjectRotation.xFlip180WithY(180D), IrisObjectRotation.xFlip180WithY(270D));
for (IrisObjectRotation rotation : rotations) {
IrisBlockVector anchor = rotation.rotate(new IrisBlockVector(footprint.getTallestKx(), 0, footprint.getTallestKz()), 0, 0, 0);
int actual = 0;
for (IrisBlockVector position : positions) {
IrisBlockVector rotated = rotation.rotate(position, 0, 0, 0);
actual = Math.max(actual, Math.abs((int) Math.round(rotated.getX()) - anchor.getBlockX()));
actual = Math.max(actual, Math.abs((int) Math.round(rotated.getZ()) - anchor.getBlockZ()));
}
record(key, actual, reach.horizontal(), statistics);
}
}
private void verifyCentered(String key, IrisObject object, IrisObjectPlacement placement, PlacementReach reach,
Statistics statistics) {
IrisObjectRotation rotation = placement.getRotation();
IrisObjectTranslate translate = placement.getTranslate();
boolean translating = translate != null && translate.canTranslate();
int[] spinsX = spins(rotation, 0);
int[] spinsY = spins(rotation, 1);
int[] spinsZ = spins(rotation, 2);
List<IrisBlockVector> positions = positions(object);
long kernels = (long) spinsX.length * spinsY.length * spinsZ.length;
if (kernels > MAXIMUM_CHECKED_KERNELS) {
fail("rotation for " + key + " has " + kernels + " kernels");
}
for (int spinX : spinsX) {
for (int spinY : spinsY) {
for (int spinZ : spinsZ) {
IrisBlockVector offset = translating
? rotation.rotate(new IrisBlockVector(translate.getX(), translate.getY(), translate.getZ()), spinX, spinY, spinZ)
: null;
int actual = 0;
for (IrisBlockVector position : positions) {
IrisBlockVector moved = rotation.rotate(position, spinX, spinY, spinZ).clone();
if (offset != null) {
moved.add(offset);
}
actual = Math.max(actual, Math.abs((int) Math.round(moved.getX())));
actual = Math.max(actual, Math.abs((int) Math.round(moved.getZ())));
}
record(key, actual, reach.horizontal(), statistics);
}
}
}
}
private static IrisObject readPositions(Path file, NativeBlockState marker) throws IOException {
try (DataInputStream input = new DataInputStream(new BufferedInputStream(Files.newInputStream(file)))) {
IrisObject object = new IrisObject(input.readInt(), input.readInt(), input.readInt());
input.mark(Short.BYTES + V2_HEADER.length());
int headerLength = input.readUnsignedShort();
boolean v2 = headerLength == V2_HEADER.length()
&& V2_HEADER.equals(new String(input.readNBytes(headerLength), StandardCharsets.US_ASCII));
if (!v2) {
input.reset();
}
int paletteSize = v2 ? input.readShort() : 0;
for (int index = 0; index < paletteSize; index++) {
input.readUTF();
}
int blocks = input.readInt();
for (int index = 0; index < blocks; index++) {
IrisBlockVector position = new IrisBlockVector(input.readShort(), input.readShort(), input.readShort());
if (v2) {
input.readShort();
} else {
input.readUTF();
}
object.getBlocks().put(position, marker);
}
return object;
}
}
private static void record(String key, int actual, int reach, Statistics statistics) {
statistics.checked++;
statistics.maximumActual = Math.max(statistics.maximumActual, actual);
statistics.maximumReach = Math.max(statistics.maximumReach, reach);
statistics.slack += reach - actual;
if (actual > reach) {
fail(key + " places a block " + actual + " blocks from its origin, beyond its reach " + reach);
}
}
private static List<IrisBlockVector> positions(IrisObject object) {
List<IrisBlockVector> positions = new ArrayList<>(object.getBlocks().size() + 8);
object.getBlocks().forEach((position, state) -> positions.add(position));
Vector3i center = object.getCenter();
int[] xs = {-center.getBlockX(), object.getW() - center.getBlockX() - 1};
int[] ys = {-center.getBlockY(), object.getH() - center.getBlockY() - 1};
int[] zs = {-center.getBlockZ(), object.getD() - center.getBlockZ() - 1};
for (int x : xs) {
for (int y : ys) {
for (int z : zs) {
positions.add(new IrisBlockVector(x, y, z));
}
}
}
return positions;
}
private void forEachVariant(IrisObject object, IrisObjectPlacement placement, double shrink, Consumer<IrisObject> consumer) {
IrisObjectScale scale = placement.getScale();
IrisObjectPlacementScaleInterpolator interpolation = scale == null || scale.getInterpolation() == null
? IrisObjectPlacementScaleInterpolator.NONE : scale.getInterpolation();
for (double factor : scales(placement)) {
IrisObject variant = factor == 1D && (scale == null || !scale.shouldScale())
? object
: scale == null ? IrisObjectScale.getFixed(object, factor) : object.scaled(factor, interpolation);
consumer.accept(shrink < 1D ? variant.scaled(shrink, IrisObjectPlacementScaleInterpolator.NONE) : variant);
}
}
private double[] scales(IrisObjectPlacement placement) {
IrisObjectScale scale = placement.getScale();
if (scale == null) {
return new double[]{dimension.getAllObjectScaleFactor()};
}
if (!scale.shouldScale()) {
return new double[]{1D};
}
if (scale.getSize() != 1D) {
return new double[]{scale.getSize()};
}
if (scale.getMinimumScale() == scale.getMaximumScale()) {
return new double[]{scale.getMinimumScale()};
}
int count = Math.max(1, Math.min(scale.getVariations(), 32));
double[] scales = new double[count];
for (int index = 0; index < count; index++) {
scales[index] = scale.getMinimumScale() + (((scale.getMaximumScale() - scale.getMinimumScale()) / count) * index);
}
return scales;
}
private List<IrisProceduralPlacement> proceduralPlacements() {
List<IrisProceduralPlacement> placements = new ArrayList<>();
for (IrisRegion region : dimension.getAllRegions(() -> data)) {
addProcedural(placements, region.getProceduralObjects());
}
for (IrisBiome biome : dimension.getReachableBiomes(() -> data)) {
addProcedural(placements, biome.getProceduralObjects());
}
return placements;
}
private static void addProcedural(List<IrisProceduralPlacement> placements, IrisProceduralObjects procedural) {
if (procedural != null && !procedural.isEmpty()) {
placements.addAll(procedural.getAllPlacements());
}
}
private static int[] spins(IrisObjectRotation rotation, int axis) {
if (rotation == null || !rotation.canRotate()) {
return new int[]{0};
}
boolean rotates = switch (axis) {
case 0 -> rotation.canRotateX();
case 1 -> rotation.canRotateY();
default -> rotation.canRotateZ();
};
if (!rotates) {
return new int[]{0};
}
LongOpenHashSet angles = new LongOpenHashSet();
IntArrayList spins = new IntArrayList();
for (int spin = -SPIN_PERIOD + 1; spin < SPIN_PERIOD; spin++) {
double angle = switch (axis) {
case 0 -> rotation.getXRotation(spin);
case 1 -> rotation.getYRotation(spin);
default -> rotation.getZRotation(spin);
};
if (angles.add(Double.doubleToLongBits(angle))) {
spins.add(spin);
}
}
return spins.toIntArray();
}
private static <T> List<T> listOrEmpty(KList<T> list) {
return list == null ? List.of() : list;
}
private static IrisObjectRotation rotation(IrisAxisRotationClamp x, IrisAxisRotationClamp y, IrisAxisRotationClamp z) {
IrisObjectRotation rotation = new IrisObjectRotation();
rotation.setEnabled(true);
rotation.setXAxis(x);
rotation.setYAxis(y);
rotation.setZAxis(z);
return rotation;
}
private static int chunkRadius(int blockRadius) {
return blockRadius > 0 ? Math.ceilDiv(blockRadius, 16) : 0;
}
private static void bindPlatform(File dataFolder) {
IrisPlatforms.unbind();
Map<String, NativeBlockState> states = new ConcurrentHashMap<>();
PlatformRegistries registries = mock(PlatformRegistries.class, withSettings().stubOnly());
when(registries.block(anyString())).thenAnswer(invocation -> state(states, invocation.getArgument(0)));
when(registries.blockOrNull(anyString())).thenAnswer(invocation -> state(states, invocation.getArgument(0)));
when(registries.blockOrNull(anyString(), anyBoolean())).thenAnswer(invocation -> state(states, invocation.getArgument(0)));
when(registries.air()).thenAnswer(invocation -> state(states, "minecraft:air"));
IrisPlatform platform = mock(IrisPlatform.class, Answers.CALLS_REAL_METHODS);
when(platform.dataFolder()).thenReturn(dataFolder);
when(platform.dataFile(any(String[].class))).thenAnswer(invocation -> {
Object[] path = invocation.getRawArguments();
return new File(dataFolder, String.join(File.separator, (String[]) path[0]));
});
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
IrisServices.register(PreservationRegistry.class, new IgnoredPreservation());
}
private static NativeBlockState state(Map<String, NativeBlockState> states, String key) {
return states.computeIfAbsent(key, ignored -> {
String material = materialOf(key);
boolean air = material.endsWith(":air") || material.endsWith("cave_air") || material.endsWith("void_air");
NativeBlockState state = mock(NativeBlockState.class, withSettings().stubOnly());
when(state.key()).thenReturn(key);
when(state.materialKey()).thenReturn(material);
when(state.isAir()).thenReturn(air);
when(state.isSolid()).thenReturn(!air);
when(state.isOccluding()).thenReturn(!air);
return state;
});
}
private static String materialOf(String key) {
String trimmed = key.trim().toLowerCase(Locale.ROOT);
int properties = trimmed.indexOf('[');
String head = properties < 0 ? trimmed : trimmed.substring(0, properties);
return head.indexOf(':') < 0 ? "minecraft:" + head : head;
}
private static final class IgnoredPreservation implements PreservationRegistry {
@Override
public void register(Thread thread) {
}
@Override
public void register(ExecutorService service) {
}
@Override
public void registerCache(MeteredCache cache) {
}
@Override
public void dereference() {
}
}
private static final class Statistics {
private long checked;
private long slack;
private int maximumActual;
private int maximumReach;
@Override
public String toString() {
return "checked=" + checked + " maxActual=" + maximumActual + " maxReach=" + maximumReach
+ " meanSlack=" + (checked == 0 ? 0D : (double) slack / checked);
}
}
}
@@ -0,0 +1,416 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.decoration.IrisStiltSettings;
import art.arcane.iris.generation.decoration.IrisVacuumSettings;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.structure.object.FloatingObjectFootprint;
import art.arcane.iris.structure.object.IObjectPlacer;
import art.arcane.iris.structure.object.IrisAxisRotationClamp;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.structure.object.IrisObjectPlacement;
import art.arcane.iris.structure.object.IrisObjectPlacementScaleInterpolator;
import art.arcane.iris.structure.object.IrisObjectRotation;
import art.arcane.iris.structure.object.IrisObjectScale;
import art.arcane.iris.structure.object.IrisObjectTranslate;
import art.arcane.iris.structure.object.ObjectPlaceMode;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.stream.ProceduralStream;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
public class PlacementReachTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final int ORIGIN_X = 1000;
private static final int ORIGIN_Y = 90;
private static final int ORIGIN_Z = -2000;
private static final int GROUND_Y = 64;
private final Map<String, NativeBlockState> states = new ConcurrentHashMap<>();
private IrisData data;
private IrisData objectData;
private Engine engine;
@Before
public void bindPlatform() {
IrisPlatforms.unbind();
PlatformRegistries registries = mock(PlatformRegistries.class, withSettings().stubOnly());
when(registries.block(anyString())).thenAnswer(invocation -> state(invocation.getArgument(0)));
when(registries.blockOrNull(anyString())).thenAnswer(invocation -> state(invocation.getArgument(0)));
when(registries.blockOrNull(anyString(), anyBoolean())).thenAnswer(invocation -> state(invocation.getArgument(0)));
when(registries.air()).thenAnswer(invocation -> state("minecraft:air"));
IrisPlatform platform = mock(IrisPlatform.class, withSettings().stubOnly());
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
ProceduralStream<NativeBlockState> rock = mock(ProceduralStream.class, withSettings().stubOnly());
IrisComplex complex = mock(IrisComplex.class, withSettings().stubOnly());
when(complex.getRockStream()).thenReturn(rock);
engine = mock(Engine.class, withSettings().stubOnly());
when(engine.getHeight()).thenReturn(256);
when(engine.getMinHeight()).thenReturn(0);
when(engine.getComplex()).thenReturn(complex);
data = mock(IrisData.class, withSettings().stubOnly());
when(data.getEngine()).thenReturn(engine);
objectData = mock(IrisData.class, withSettings().stubOnly());
}
@After
public void unbindPlatform() {
IrisPlatforms.unbind();
}
@Test
public void halfExtentReachIsTheCenteredFrameNotTheFullWidth() {
IrisObject object = singleBlockObject(103, 75, 73);
IrisObjectPlacement placement = new IrisObjectPlacement();
assertEquals(51, PlacementReach.of(object, placement, 1D).horizontal());
assertEquals(54, PlacementReach.of(object, placement, 1D).input());
assertEquals(51, PlacementReach.of(object, placement.setRotation(IrisObjectRotation.of(0D, 0D, 0D)), 1D).horizontal());
}
@Test
public void arbitraryAngleRotationUsesTheRotatedFrame() {
IrisObject object = singleBlockObject(21, 5, 21);
IrisObjectPlacement placement = new IrisObjectPlacement().setRotation(rotation(new IrisAxisRotationClamp(),
new IrisAxisRotationClamp(true, false, 0D, 0D, 45D), new IrisAxisRotationClamp()));
assertEquals(14, PlacementReach.of(object, placement, 1D).horizontal());
}
@Test
public void slopeRotationIsBoundedEvenWhenTheConfiguredRotationIsDisabled() {
IrisObject object = singleBlockObject(21, 9, 21);
IrisObjectPlacement placement = new IrisObjectPlacement().setRotation(IrisObjectRotation.of(0D, 0D, 0D));
assertEquals(10, PlacementReach.of(object, placement, 1D).horizontal());
assertEquals(15, PlacementReach.of(object, placement.setRotateTowardsSlope(true), 1D).horizontal());
}
@Test
public void unionKeepsTheLargestReachAndTheLargestInput() {
PlacementReach union = new PlacementReach(10, 2).union(new PlacementReach(4, 12));
assertEquals(10, union.horizontal());
assertEquals(16, union.input());
assertEquals(19, union.withReadMargin(9).input());
assertEquals(16, union.withReadMargin(1).input());
}
@Test
public void scaledSpanBoundsEveryScaledVariant() {
Random random = new Random(91L);
double[] scales = {0.01D, 0.25D, 0.45D, 0.5D, 0.75D, 0.95D, 1D, 1.3D, 1.5D, 2D, 2.6D, 3.7D, 4D};
for (int trial = 0; trial < 120; trial++) {
int outside = trial % 3 == 0 ? 1 + random.nextInt(3) : 0;
IrisObject object = randomObject(random, outside);
PlacementReach.Extent extent = PlacementReach.Extent.of(object);
for (double scale : scales) {
for (IrisObjectPlacementScaleInterpolator interpolation : List.of(IrisObjectPlacementScaleInterpolator.NONE,
IrisObjectPlacementScaleInterpolator.TRILINEAR)) {
if (interpolation != IrisObjectPlacementScaleInterpolator.NONE && scale > 2D) {
continue;
}
IrisObject scaled = object.scaled(scale, interpolation);
for (double maximum : new double[]{scale, Math.min(IrisObjectScale.MAXIMUM_FACTOR, scale * 1.4D)}) {
assertContains(extent.scaled(maximum), scaled, "scale " + scale + " bounded at " + maximum);
for (double shrink : new double[]{0.45D, 0.8D}) {
assertContains(extent.scaled(maximum).scaled(shrink),
scaled.scaled(shrink, IrisObjectPlacementScaleInterpolator.NONE),
"scale " + scale + " shrink " + shrink);
}
}
}
}
}
}
@Test
public void runnerWritesAndReadsStayInsideTheReach() {
Random random = new Random(1337L);
for (int trial = 0; trial < 260; trial++) {
IrisObject object = randomObject(random, trial % 5 == 0 ? 2 : 0);
object.setLoader(objectData);
IrisObjectPlacement placement = randomPlacement(random);
PlacementReach reach = PlacementReach.of(object, placement, placement.getMaximumScale(null));
for (int seed = 0; seed < 12; seed++) {
RNG rng = new RNG((trial * 7919L) + seed);
IrisObject variant = placement.getScale() == null ? object : placement.getScale().get(rng, object);
RecordingPlacer placer = new RecordingPlacer(engine);
variant.place(ORIGIN_X, ORIGIN_Y, ORIGIN_Z, placer, placement, rng, null, null, data);
String context = "trial " + trial + " seed " + seed + " mode " + placement.getMode()
+ " rotation " + placement.getRotation() + " translate " + placement.getTranslate()
+ " scale " + placement.getScale() + " warp " + placement.getWarp().getMultiplier();
assertTrue(context + " writes " + placer.writeReach + " > " + reach.horizontal(),
placer.writeReach <= reach.horizontal());
assertTrue(context + " reads " + placer.readReach + " > " + reach.input(),
placer.readReach <= reach.input());
}
}
}
@Test
public void anchoredReachCoversTheFloatingFootprintAnchor() {
Random random = new Random(7L);
for (int trial = 0; trial < 80; trial++) {
IrisObject object = randomObject(random, trial % 4 == 0 ? 1 : 0);
PlacementReach reach = PlacementReach.anchored(PlacementReach.Extent.of(object), new IrisObjectPlacement());
FloatingObjectFootprint footprint = FloatingObjectFootprint.compute(object);
for (IrisObjectRotation rotation : List.of(IrisObjectRotation.of(0D, 0D, 0D),
IrisObjectRotation.xFlip180WithY(0D), IrisObjectRotation.xFlip180WithY(90D),
IrisObjectRotation.xFlip180WithY(180D), IrisObjectRotation.xFlip180WithY(270D))) {
IrisBlockVector anchor = rotation.rotate(new IrisBlockVector(footprint.getTallestKx(), 0, footprint.getTallestKz()), 0, 0, 0);
object.getBlocks().forEach((position, state) -> {
IrisBlockVector rotated = rotation.rotate(position, 0, 0, 0);
assertTrue(Math.abs((int) Math.round(rotated.getX()) - anchor.getBlockX()) <= reach.horizontal());
assertTrue(Math.abs((int) Math.round(rotated.getZ()) - anchor.getBlockZ()) <= reach.horizontal());
});
}
}
}
private IrisObjectPlacement randomPlacement(Random random) {
IrisObjectPlacement placement = new IrisObjectPlacement();
placement.setRotation(randomRotation(random));
if (random.nextInt(3) > 0) {
placement.setTranslate(new IrisObjectTranslate()
.setX(random.nextInt(41) - 20)
.setY(random.nextInt(21) - 10)
.setZ(random.nextInt(41) - 20));
}
if (random.nextInt(3) == 0) {
placement.setWarp(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).setMultiplier(1D + random.nextInt(9)));
}
List<ObjectPlaceMode> modes = List.of(ObjectPlaceMode.CENTER_HEIGHT, ObjectPlaceMode.STILT, ObjectPlaceMode.FAST_STILT,
ObjectPlaceMode.CENTER_STILT, ObjectPlaceMode.ERODE_STILT, ObjectPlaceMode.ORGANIC_STILT, ObjectPlaceMode.CEILING_HANG,
ObjectPlaceMode.VACUUM, ObjectPlaceMode.VACUUM_HIGH, ObjectPlaceMode.VACUUM_FAST, ObjectPlaceMode.VACUUM_ORGANIC,
ObjectPlaceMode.VACUUM_WAVY, ObjectPlaceMode.STRUCTURE_PIECE);
placement.setMode(modes.get(random.nextInt(modes.size())));
if (random.nextInt(4) == 0) {
placement.setVacuumSettings(new IrisVacuumSettings().setRadius(1 + random.nextInt(16)));
}
if (random.nextInt(4) == 0) {
placement.setStiltSettings(new IrisStiltSettings());
}
placement.setBore(random.nextInt(4) == 0);
placement.setBoreExtendMaxY(random.nextInt(3));
placement.setSnow(random.nextInt(4) == 0 ? 0.6D : 0D);
placement.setSurfaceSupportBuffer(random.nextInt(5));
switch (random.nextInt(5)) {
case 0 -> placement.setScale(new IrisObjectScale().setSize(0.3D + (random.nextDouble() * 1.6D)));
case 1 -> placement.setScale(new IrisObjectScale().setMinimumScale(0.5D).setMaximumScale(2.3D)
.setVariations(5).setInterpolation(IrisObjectPlacementScaleInterpolator.TRILINEAR));
case 2 -> placement.setScale(new IrisObjectScale().setMinimumScale(0.4D).setMaximumScale(0.9D).setVariations(3));
default -> {
}
}
return placement;
}
private static IrisObjectRotation randomRotation(Random random) {
return switch (random.nextInt(9)) {
case 0 -> new IrisObjectRotation();
case 1 -> IrisObjectRotation.of(0D, 0D, 0D);
case 2 -> rotation(new IrisAxisRotationClamp(), new IrisAxisRotationClamp(true, false, 0D, 0D, 45D), new IrisAxisRotationClamp());
case 3 -> rotation(new IrisAxisRotationClamp(), new IrisAxisRotationClamp(true, false, 0D, 0D, 0D), new IrisAxisRotationClamp());
case 4 -> IrisObjectRotation.of(90D, 0D, 0D);
case 5 -> rotation(new IrisAxisRotationClamp(true, false, 0D, 0D, 90D),
new IrisAxisRotationClamp(true, false, 0D, 0D, 90D), new IrisAxisRotationClamp(true, false, 0D, 0D, 90D));
case 6 -> rotation(new IrisAxisRotationClamp(true, false, 0D, 0D, 0D),
new IrisAxisRotationClamp(true, false, 0D, 0D, 0D), new IrisAxisRotationClamp(true, false, 0D, 0D, 30D));
case 7 -> IrisObjectRotation.of(30D, 135D, 0D);
default -> rotation(new IrisAxisRotationClamp(), new IrisAxisRotationClamp(true, false, 20D, 110D, 15D),
new IrisAxisRotationClamp(true, true, 180D, 180D, 0D));
};
}
private IrisObject randomObject(Random random, int outside) {
int width = 1 + random.nextInt(18);
int height = 1 + random.nextInt(14);
int depth = 1 + random.nextInt(18);
IrisObject object = new IrisObject(width, height, depth);
int count = 1 + random.nextInt(Math.max(1, (width * height * depth) / 3));
for (int index = 0; index < count; index++) {
int x = random.nextInt(width) - (width / 2);
int y = random.nextInt(height) - (height / 2);
int z = random.nextInt(depth) - (depth / 2);
object.getBlocks().put(new IrisBlockVector(x, y, z), state(random.nextInt(5) == 0 ? "minecraft:oak_leaves" : "minecraft:stone"));
}
for (int index = 0; index < outside; index++) {
int x = random.nextInt(width + 10) - ((width + 10) / 2);
int y = random.nextInt(height + 6) - ((height + 6) / 2);
int z = random.nextInt(depth + 10) - ((depth + 10) / 2);
object.getBlocks().put(new IrisBlockVector(x, y, z), state("minecraft:stone"));
}
return object;
}
private IrisObject singleBlockObject(int width, int height, int depth) {
IrisObject object = new IrisObject(width, height, depth);
object.getBlocks().put(new IrisBlockVector(0, 0, 0), state("minecraft:stone"));
return object;
}
private static void assertContains(PlacementReach.Extent extent, IrisObject object, String context) {
object.getBlocks().forEach((position, state) -> {
assertTrue(context + " x " + position.getBlockX() + " outside " + extent.x(),
position.getBlockX() >= extent.x().minimum() && position.getBlockX() <= extent.x().maximum());
assertTrue(context + " y " + position.getBlockY() + " outside " + extent.y(),
position.getBlockY() >= extent.y().minimum() && position.getBlockY() <= extent.y().maximum());
assertTrue(context + " z " + position.getBlockZ() + " outside " + extent.z(),
position.getBlockZ() >= extent.z().minimum() && position.getBlockZ() <= extent.z().maximum());
});
int centerX = object.getCenter().getBlockX();
int centerZ = object.getCenter().getBlockZ();
assertTrue(context + " frame x", -centerX >= extent.x().minimum() && object.getW() - 1 - centerX <= extent.x().maximum());
assertTrue(context + " frame z", -centerZ >= extent.z().minimum() && object.getD() - 1 - centerZ <= extent.z().maximum());
}
private static IrisObjectRotation rotation(IrisAxisRotationClamp x, IrisAxisRotationClamp y, IrisAxisRotationClamp z) {
IrisObjectRotation rotation = new IrisObjectRotation();
rotation.setEnabled(true);
rotation.setXAxis(x);
rotation.setYAxis(y);
rotation.setZAxis(z);
return rotation;
}
private NativeBlockState state(String key) {
return states.computeIfAbsent(key, ignored -> {
String material = key.toLowerCase(Locale.ROOT);
boolean air = material.endsWith("air");
NativeBlockState state = mock(NativeBlockState.class, withSettings().stubOnly());
when(state.key()).thenReturn(key);
when(state.materialKey()).thenReturn(material);
when(state.isAir()).thenReturn(air);
when(state.isSolid()).thenReturn(!air);
when(state.isOccluding()).thenReturn(!air);
return state;
});
}
private static final class RecordingPlacer implements IObjectPlacer {
private final Engine engine;
private int writeReach;
private int readReach;
private RecordingPlacer(Engine engine) {
this.engine = engine;
}
private void write(int x, int z) {
writeReach = Math.max(writeReach, Math.max(Math.abs(x - ORIGIN_X), Math.abs(z - ORIGIN_Z)));
read(x, z);
}
private void read(int x, int z) {
readReach = Math.max(readReach, Math.max(Math.abs(x - ORIGIN_X), Math.abs(z - ORIGIN_Z)));
}
@Override
public int getHighest(int x, int z, IrisData data) {
read(x, z);
return GROUND_Y;
}
@Override
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
read(x, z);
return GROUND_Y;
}
@Override
public void set(int x, int y, int z, NativeBlockState d) {
write(x, z);
}
@Override
public NativeBlockState get(int x, int y, int z) {
read(x, z);
return null;
}
@Override
public boolean isPreventingDecay() {
return false;
}
@Override
public boolean isCarved(int x, int y, int z) {
read(x, z);
return false;
}
@Override
public boolean isSolid(int x, int y, int z) {
read(x, z);
return y < GROUND_Y;
}
@Override
public boolean isUnderwater(int x, int z) {
read(x, z);
return false;
}
@Override
public int getFluidHeight() {
return GROUND_Y - 1;
}
@Override
public boolean isDebugSmartBore() {
return false;
}
@Override
public void setTile(int xx, int yy, int zz, TileData tile) {
write(xx, zz);
}
@Override
public <T> void setData(int xx, int yy, int zz, T data) {
write(xx, zz);
}
@Override
public <T> T getData(int xx, int yy, int zz, Class<T> t) {
read(xx, zz);
return null;
}
@Override
public Engine getEngine() {
return engine;
}
}
}
@@ -0,0 +1,47 @@
package art.arcane.iris.generation.runtime;
import org.junit.Test;
import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertEquals;
public final class EngineMetricsStageTotalsTest {
@Test
public void stageTotalsAccumulatePastTheRollingWindow() {
EngineMetrics metrics = new EngineMetrics(2);
metrics.getTerrain().put(1.5D);
metrics.getTerrain().put(2.5D);
metrics.getTerrain().put(4D);
EngineMetrics.StageTotal terrain = metrics.stageTotals().get("terrain");
assertEquals(8D, terrain.millis(), 0D);
assertEquals(3L, terrain.samples());
assertEquals(3.25D, metrics.getTerrain().getAverage(), 0D);
}
@Test
public void stageTotalsSinceAnEarlierSnapshotCoverOnlyTheLaterPuts() {
EngineMetrics metrics = new EngineMetrics(4);
metrics.getCarveResolve().put(3D);
EngineMetrics.StageTotal before = metrics.stageTotals().get("carve.resolve");
metrics.getCarveResolve().put(5D);
metrics.getCarveResolve().put(7D);
EngineMetrics.StageTotal window = metrics.stageTotals().get("carve.resolve").since(before);
assertEquals(12D, window.millis(), 0D);
assertEquals(2L, window.samples());
}
@Test
public void stageTotalsNameEveryPulledTimer() {
EngineMetrics metrics = new EngineMetrics(1);
Map<String, EngineMetrics.StageTotal> totals = metrics.stageTotals();
assertEquals(Set.copyOf(metrics.pull().keySet()), Set.copyOf(totals.keySet()));
assertEquals(metrics.pull().size(), totals.size());
}
}
@@ -8,7 +8,6 @@ import art.arcane.iris.generation.runtime.GenerationRuntime.BiomeMaxes;
import art.arcane.iris.world.history.SavedBiomeRuntime;
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.testsupport.Await;
import art.arcane.iris.testsupport.PlatformBinding;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@@ -37,13 +36,11 @@ import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
@@ -680,30 +677,19 @@ public class IrisEngineGenerationRuntimeScopeTest {
}
@Test
public void missingRouterReadWaitsForAttachmentPublication() throws Exception {
public void missingRouterReadsDoNotWaitForTheAttachmentMonitor() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
GenerationHistoryRuntimeRouter router = historyRouter(engine);
GenerationHistoryRuntimeRouter.CoordinateScope scope = mock(GenerationHistoryRuntimeRouter.CoordinateScope.class);
when(router.openCoordinateScope(-17, 32)).thenReturn(scope);
CountDownLatch started = new CountDownLatch(1);
FutureTask<GenerationHistoryRuntimeRouter.CoordinateScope> read = new FutureTask<>(() -> {
started.countDown();
return engine.openGenerationHistoryCoordinateScope(-17, 32);
});
Thread reader = new Thread(read, "Pending history attachment");
ExecutorService reader = Executors.newSingleThreadExecutor();
try {
synchronized (engine.generationHistoryRuntimeRouterLock) {
reader.start();
assertTrue(started.await(5, TimeUnit.SECONDS));
awaitBlocked(reader);
engine.attachGenerationHistoryRuntimeRouter(router);
assertNull(reader.submit(() -> engine.openGenerationHistoryCoordinateScope(-17, 32))
.get(5, TimeUnit.SECONDS));
assertTrue(reader.submit(engine::getGenerationHistoryRuntimeRouter).get(5, TimeUnit.SECONDS).isEmpty());
}
assertSame(scope, read.get(5, TimeUnit.SECONDS));
} finally {
reader.join(5_000L);
assertTrue("Pending router read did not finish", !reader.isAlive());
reader.shutdownNow();
assertTrue(reader.awaitTermination(5, TimeUnit.SECONDS));
}
}
@@ -713,27 +699,19 @@ public class IrisEngineGenerationRuntimeScopeTest {
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
GenerationHistoryRuntimeRouter router = historyRouter(engine);
engine.attachGenerationHistoryRuntimeRouter(router);
CountDownLatch started = new CountDownLatch(1);
FutureTask<GenerationHistoryRuntimeRouter.CoordinateScope> read = new FutureTask<>(() -> {
started.countDown();
return engine.openGenerationHistoryCoordinateScope(-17, 32);
});
Thread reader = new Thread(read, "Detached history read");
engine.detachGenerationHistoryRuntimeRouter(router);
ExecutorService reader = Executors.newSingleThreadExecutor();
try {
synchronized (engine.generationHistoryRuntimeRouterLock) {
engine.detachGenerationHistoryRuntimeRouter(router);
reader.start();
assertTrue(started.await(5, TimeUnit.SECONDS));
awaitBlocked(reader);
ExecutionException failure = assertThrows(ExecutionException.class, () -> reader.submit(
() -> engine.openGenerationHistoryCoordinateScope(-17, 32)).get(5, TimeUnit.SECONDS));
assertTrue(failure.getCause() instanceof IllegalStateException);
assertEquals("Iris generation-history runtime router is detached.", failure.getCause().getMessage());
}
ExecutionException failure = assertThrows(ExecutionException.class, () -> read.get(5, TimeUnit.SECONDS));
assertTrue(failure.getCause() instanceof IllegalStateException);
assertEquals("Iris generation-history runtime router is detached.", failure.getCause().getMessage());
assertTrue(engine.getGenerationHistoryRuntimeRouter().isEmpty());
} finally {
reader.join(5_000L);
assertTrue("Detached router read did not finish", !reader.isAlive());
reader.shutdownNow();
assertTrue(reader.awaitTermination(5, TimeUnit.SECONDS));
}
verify(router, never()).openCoordinateScope(anyInt(), anyInt());
}
@@ -909,12 +887,6 @@ public class IrisEngineGenerationRuntimeScopeTest {
return router;
}
private static void awaitBlocked(Thread thread) {
Await.reached("the reader to block on the engine lifecycle lock", Duration.ofSeconds(5L),
() -> thread.getState() == Thread.State.BLOCKED || !thread.isAlive());
assertEquals(Thread.State.BLOCKED, thread.getState());
}
private static IrisEngine engine(
GenerationRuntime generationRuntime,
EngineEffects effects,
@@ -132,6 +132,9 @@ public class IrisCarveModifierBiomeBlendTest {
Field field = IrisCarveModifier.class.getDeclaredField("rng");
field.setAccessible(true);
field.set(modifier, rng);
Field seed = IrisCarveModifier.class.getDeclaredField("caveBlendSeed");
seed.setAccessible(true);
seed.setLong(modifier, SEED);
return modifier;
}
@@ -56,6 +56,9 @@ public class IrisCarveModifierBoundarySupportTest {
Field rng = IrisCarveModifier.class.getDeclaredField("rng");
rng.setAccessible(true);
rng.set(modifier, new RNG(71L));
Field seed = IrisCarveModifier.class.getDeclaredField("caveBlendSeed");
seed.setAccessible(true);
seed.setLong(modifier, 71L);
IrisMatterSupport.ensureRegistered();
Matter floorMatter = new IrisMatter(16, 16, 16);
@@ -0,0 +1,53 @@
package art.arcane.iris.generation.stage;
import art.arcane.volmlib.util.math.BlockPosition;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import java.util.SplittableRandom;
import static org.junit.Assert.assertEquals;
public class IrisCarveModifierCaveBlendRollTest {
private static final int SAMPLES = 1_000_000;
@Test
public void statelessBlendRandomMatchesParallelRngForOneMillionPoints() {
SplittableRandom random = new SplittableRandom(0x5eed_cafe_f00dL);
long[] seeds = {0L, 1L, -1L, 1337L, Long.MIN_VALUE, Long.MAX_VALUE, 0x5DEECE66DL, random.nextLong()};
for (int sample = 0; sample < SAMPLES; sample++) {
long seed = sample < seeds.length * 64 ? seeds[sample % seeds.length] : random.nextLong();
int x = coordinate(random, sample);
int y = sample % 7 == 0 ? random.nextInt() : random.nextInt(-2048, 2048);
int z = coordinate(random, sample + 3);
assertEquals(seed + " @ " + x + "," + y + "," + z,
new RNG(seed).nextParallelRNG(BlockPosition.toLong(x, y, z)).nextInt(),
IrisCarveModifier.caveBlendRandom(seed, x, y, z));
}
}
@Test
public void statelessBlendRandomMatchesParallelRngAtCoordinateExtremes() {
int[] edges = {Integer.MIN_VALUE, Integer.MIN_VALUE + 1, -33_554_433, -33_554_432, -1, 0, 1,
33_554_431, 33_554_432, Integer.MAX_VALUE - 1, Integer.MAX_VALUE};
long[] seeds = {0L, -1L, 1337L, Long.MIN_VALUE, Long.MAX_VALUE};
for (long seed : seeds) {
for (int x : edges) {
for (int y : edges) {
for (int z : edges) {
assertEquals(new RNG(seed).nextParallelRNG(BlockPosition.toLong(x, y, z)).nextInt(),
IrisCarveModifier.caveBlendRandom(seed, x, y, z));
}
}
}
}
}
private static int coordinate(SplittableRandom random, int sample) {
return switch (sample % 4) {
case 0 -> random.nextInt();
case 1 -> random.nextInt(-30_000_000, 30_000_000);
default -> random.nextInt(-65_536, 65_536);
};
}
}
@@ -1,5 +1,6 @@
package art.arcane.iris.generation.stage;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.volmlib.util.matter.MatterCavern;
import org.junit.Test;
@@ -49,6 +50,7 @@ public class IrisCarveScratchTest {
scratch.boundaryMasks[0].add(13);
scratch.walls.put(1, 12, 2, cavern);
scratch.customBiomeCache.put("cave", null);
scratch.caveBiomeCache.put(42L, new IrisBiome());
scratch.customCaveBiomePresent = true;
scratch.reset();
@@ -56,6 +58,7 @@ public class IrisCarveScratchTest {
assertTrue(scratch.columnMasks[0].isEmpty());
assertTrue(scratch.boundaryMasks[0].isEmpty());
assertTrue(scratch.customBiomeCache.isEmpty());
assertTrue(scratch.caveBiomeCache.isEmpty());
assertFalse(scratch.customCaveBiomePresent);
int[] wallCount = new int[1];
scratch.walls.forEach((x, y, z, value) -> wallCount[0]++);
@@ -19,11 +19,10 @@ import art.arcane.iris.generation.decoration.IrisDecorator;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.chunk.ChunkDataHunkHolder;
import art.arcane.iris.generation.chunk.SectionBuffer;
import art.arcane.iris.generation.chunk.ColumnExtentListeningHunk;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import org.bukkit.generator.ChunkGenerator;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
@@ -165,10 +164,7 @@ public class IrisNaturalCaveScanTest {
fullOutput.listen((x, y, z, state) -> fullWrites.add(x + ":" + y + ":" + z + ":" + state.key())), null);
Fixture bounded = new Fixture(true);
ChunkGenerator.ChunkData chunkData = mock(ChunkGenerator.ChunkData.class);
doReturn(0).when(chunkData).getMinHeight();
doReturn(32).when(chunkData).getMaxHeight();
ChunkDataHunkHolder holder = new ChunkDataHunkHolder(chunkData);
SectionBuffer holder = new SectionBuffer(32);
bounded.sparseOutput(holder);
List<String> boundedWrites = new ArrayList<>();
bounded.modifier.decorateNaturalCaves(-32, 48, new ColumnExtentListeningHunk<>(holder,
@@ -1,7 +1,7 @@
package art.arcane.iris.generation.stage;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.chunk.ChunkDataHunkHolder;
import art.arcane.iris.generation.chunk.SectionBuffer;
import art.arcane.iris.generation.chunk.ColumnExtentListeningHunk;
import art.arcane.iris.generation.concurrent.BurstExecutor;
import art.arcane.iris.generation.concurrent.MultiBurst;
@@ -15,7 +15,6 @@ import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.volmlib.util.hunk.storage.ArrayHunk;
import org.bukkit.block.data.BlockData;
import org.bukkit.generator.ChunkGenerator;
import org.junit.Rule;
import org.junit.Test;
@@ -65,8 +64,8 @@ public class IrisPerfectionModifierColumnTest {
NativeBlockState[] palette = {null, fixture.air, fixture.water, fixture.stone,
fixture.decorant, fixture.half, fixture.spike(true, false), fixture.spike(false, true)};
for (int trial = 0; trial < 8; trial++) {
ChunkDataHunkHolder bounded = holder(64);
ChunkDataHunkHolder full = holder(64);
SectionBuffer bounded = new SectionBuffer(64);
SectionBuffer full = new SectionBuffer(64);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
int top = random.nextInt(64);
@@ -91,13 +90,6 @@ public class IrisPerfectionModifierColumnTest {
}
}
private static ChunkDataHunkHolder holder(int height) {
ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class);
when(chunk.getMinHeight()).thenReturn(0);
when(chunk.getMaxHeight()).thenReturn(height);
return new ChunkDataHunkHolder(chunk);
}
@Test
public void unrelatedTallColumnsAreScannedOnceDuringRepeatedCleanup() {
Fixture fixture = new Fixture();
@@ -1,6 +1,7 @@
package art.arcane.iris.generation.subterrain;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.volmlib.util.collection.KList;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import org.junit.Test;
@@ -10,6 +11,11 @@ import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.HashSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -441,6 +447,74 @@ public class SubterrainPlannerTest {
}
}
@Test
public void alternatingChunkSamplingResolvesEachCandidateCellOnce() {
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE).setAllowedRegions(new KList<>("test-region"));
AtomicInteger regionQueries = new AtomicInteger();
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 7123, -64, 97,
(x, z) -> {
regionQueries.incrementAndGet();
return "test-region";
}));
for (int round = 0; round < 500; round++) {
planner.sample(7, 10, 7);
planner.sample(23, 10, 7);
planner.sample(7, 10, 23);
planner.sample(-9, 10, -9);
}
int firstRound = regionQueries.get();
assertTrue("candidate cells resolved " + firstRound + " times", firstRound <= 16);
for (int round = 0; round < 500; round++) {
planner.plansForBounds(-1, -1, 16, 16);
planner.plansForBounds(15, 15, 32, 32);
}
assertEquals(firstRound, regionQueries.get());
}
@Test
public void concurrentCrossChunkSamplingMatchesAFreshPlanner() throws Exception {
List<IrisSubterrainFeature> features = List.of(compactFeature(IrisSubterrainFamily.CENOTE).setSpacing(96),
compactFeature(IrisSubterrainFamily.TRAVERTINE_TERRACES).setSpacing(160));
SubterrainPlanner shared = new SubterrainPlanner(new SubterrainPlanner.Options(features, 4242, -64, 97, (x, z) -> "test-region"));
SubterrainPlanner reference = new SubterrainPlanner(new SubterrainPlanner.Options(features, 4242, -64, 97, (x, z) -> "test-region"));
int threads = 8;
ExecutorService pool = Executors.newFixedThreadPool(threads);
try {
List<Future<Integer>> results = new ArrayList<>();
for (int thread = 0; thread < threads; thread++) {
int offset = thread;
results.add(pool.submit(() -> {
int occupied = 0;
for (int step = 0; step < 6000; step++) {
int x = Math.floorMod(step * 37 + offset * 101, 2400) - 1200;
int z = Math.floorMod(step * 53 + offset * 211, 2400) - 1200;
int y = Math.floorMod(step * 11 + offset, 150) - 60;
int chunkX = Math.floorDiv(x, 16) * 16;
int chunkZ = Math.floorDiv(z, 16) * 16;
SubterrainCell expected;
synchronized (reference) {
expected = reference.sample(reference.plansForBounds(chunkX - 1, chunkZ - 1, chunkX + 16, chunkZ + 16), x, y, z);
}
SubterrainCell actual = shared.sample(x, y, z);
assertEquals(expected, actual);
if (actual.occupied()) {
occupied++;
}
}
return occupied;
}));
}
int occupied = 0;
for (Future<Integer> result : results) {
occupied += result.get(60, TimeUnit.SECONDS);
}
assertTrue(occupied > 0);
} finally {
pool.shutdownNow();
assertTrue(pool.awaitTermination(10, TimeUnit.SECONDS));
}
}
@Test
public void immutableSnapshotsAndSeedsControlPlans() {
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE);
@@ -0,0 +1,116 @@
package art.arcane.iris.pack.loading;
import art.arcane.iris.generation.biome.IrisBiomeCustom;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.testsupport.DurabilityMode;
import art.arcane.iris.world.history.GenerationRegistryContractFactory;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
public class IrisDataCustomBiomeKeyTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void repeatedConcurrentLookupsMatchTheContentAddressedKeyPerDimension() throws Exception {
Path pack = temporaryFolder.newFolder("custom-keys").toPath().resolve("iris/pack");
writePack(pack);
IrisData data = IrisData.openDatapackCompiler(pack.toFile());
ExecutorService pool = Executors.newFixedThreadPool(8);
try {
IrisDimension overworld = data.getDimensionLoader().load("overworld");
IrisDimension second = data.getDimensionLoader().load("second");
List<IrisBiomeCustom> customs = data.getBiomeLoader().load("test").getCustomDerivitives();
assertEquals(2, customs.size());
List<String> expected = new ArrayList<>();
for (IrisDimension dimension : List.of(overworld, second)) {
for (IrisBiomeCustom custom : customs) {
expected.add(GenerationRegistryContractFactory.customBiomeResourceKey(
dimension.getLoadKey(), custom, data.getContentGate()));
}
}
assertEquals(4, expected.stream().distinct().count());
List<Future<List<String>>> results = new ArrayList<>();
for (int thread = 0; thread < 8; thread++) {
results.add(pool.submit(() -> {
List<String> seen = new ArrayList<>();
for (int round = 0; round < 200; round++) {
seen.clear();
for (IrisDimension dimension : List.of(overworld, second)) {
for (IrisBiomeCustom custom : customs) {
seen.add(data.customBiomeResourceKey(dimension, custom));
}
}
}
return List.copyOf(seen);
}));
}
for (Future<List<String>> result : results) {
assertEquals(expected, result.get(60, TimeUnit.SECONDS));
}
IrisBiomeCustom equalCopy = new IrisBiomeCustom().setId(customs.getFirst().getId());
assertEquals(GenerationRegistryContractFactory.customBiomeResourceKey(
overworld.getLoadKey(), equalCopy, data.getContentGate()),
data.customBiomeResourceKey(overworld, equalCopy));
assertNotEquals(data.customBiomeResourceKey(overworld, customs.getFirst()),
data.customBiomeResourceKey(second, customs.getFirst()));
} finally {
pool.shutdownNow();
assertTrue(pool.awaitTermination(10, TimeUnit.SECONDS));
data.close();
}
}
private static void writePack(Path root) throws Exception {
Files.createDirectories(root.resolve("dimensions"));
Files.createDirectories(root.resolve("biomes"));
for (String dimension : List.of("overworld", "second")) {
Files.writeString(root.resolve("dimensions/" + dimension + ".json"), """
{
"name": "Test Dimension",
"environment": "NORMAL",
"logicalHeight": 256,
"dimensionHeight": {
"min": -64,
"max": 320
}
}
""", StandardCharsets.UTF_8);
}
Files.writeString(root.resolve("biomes/test.json"), """
{
"name": "Test Biome",
"derivative": "minecraft:plains",
"vanillaDerivative": "minecraft:plains",
"customDerivitives": [
{
"id": "first_custom"
},
{
"id": "second_custom",
"temperature": 1.5
}
]
}
""", StandardCharsets.UTF_8);
}
}
@@ -99,7 +99,7 @@ public class PregenPerformanceProfileTest {
}
@Test
public void pregenerationWantsTwoBurstWorkersPerCore() {
public void pregenerationWantsTwoBurstWorkersPerPerformanceCore() {
assertEquals(32, PregenPerformanceProfile.pregenBurstParallelism(16));
assertEquals(4, PregenPerformanceProfile.pregenBurstParallelism(1));
}