Expanded terrain generation tooling

- Standalone Minecraft 26.3 generation uses native registries, biome planning and structure planning with bounded concurrency
- Offline export publishes fresh TERRAIN checkpoints for native server completion
- Checkpoints retain mantle data, immutable generation history and captured server configuration
- Output publication uses staged directories, durability checks and atomic moves
- Offline distribution tasks provide an installed launcher and reusable native fixtures
- Dimension settings control terrain, biome-bound and cave-density sampling with unchanged defaults
- Pack validation and schemas enforce the supported sampling intervals
- Biome layer caches isolate data, engine and seed identities
- Floating biome palettes keep stable thickness patterns across generation orders
- Hydrology batches terrain samples within the owning generation runtime
- Hydrology planners share bounded basis caches and reuse raster geometry and distance samples
- Demand planning admits new tiles as active plans finish
- Volumetric height queries avoid unnecessary density evaluation and bound cached anchors
- Cave generation reuses chunk-scoped inputs and semantic capture
- Object replay indexes mutations by destination and reuses resolved records
- Source-plan caches coordinate concurrent builds and reduce overlay allocation
- Prerequisite carving reads preserve the underlying hydrology before object placement
- Tree material decoding preserves stored native block identities
- Engine shutdown drains generation outside the lifecycle lock and coordinates runtime retirement
- Pregeneration sizes temporary worker boosts from processor and heap capacity
- Pregeneration restores native worker counts after drain and failure paths
- Native structure queries retain per-origin coordination without broad window locks
- Saved biome writes drain pending batches under their write stripe
- Boundary column lookups avoid repeated voxel decoding
- Modded registry consumers use explicit VolmLib providers and version-specific native source overlays
- Regression tests cover generation parity, cache isolation, concurrency, shutdown, persistence and offline checkpoints
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-23 07:00:55 -04:00
1 parent 98ddb6644d
commit ca9f28c9b0
142 files changed
+11159 -773

No files matched your search

@@ -11,7 +11,6 @@ import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
@@ -28,7 +27,6 @@ public class IrisBiomeCeilingLayerTest {
new IrisDimension(), 13, -27, new RNG(1), 8, 64, null, null);
assertEquals(List.of(first, first, second, third, third), result);
assertNull(biome.getLayerHeightGenerators().getIfPresent());
}
@Test
@@ -45,8 +43,8 @@ public class IrisBiomeCeilingLayerTest {
assertEquals(firstCeiling.size(), secondCeiling.size());
}
first.getLayerHeightGenerators(rng, null);
assertNotSame(first.getLayerHeightGenerators().getIfPresent().get(0),
first.getLayerCeilingHeightGenerators().getIfPresent().get(0));
assertNotSame(first.getLayerHeightGenerators(rng, null).get(0),
first.getLayerCeilingHeightGenerators().get(first.getCaveCeilingLayers(), 7235, rng, null).get(0));
}
@Test
@@ -0,0 +1,179 @@
package art.arcane.iris.generation.biome;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.noise.CNG;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
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.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisBiomeLayerHeightCacheTest {
private static final long TERRAIN_SEED = 7191L;
private static final long CARVE_SEED = -872315L;
private static final long FLOATING_SEED = (TERRAIN_SEED ^ 0x7EB0A73F1DCE514DL)
+ (0x7A4E ^ "magnetics/glass-shard".hashCode());
@Test
public void layerHeightUsesEachCallerSeedInEitherOrder() {
IrisBiomePaletteLayer layer = layer();
assertNoise(layer().getHeightGenerator(new RNG(CARVE_SEED), null),
layer.getHeightGenerator(new RNG(CARVE_SEED), null));
assertNoise(layer().getHeightGenerator(new RNG(FLOATING_SEED), null),
layer.getHeightGenerator(new RNG(FLOATING_SEED), null));
IrisBiomePaletteLayer reversed = layer();
assertNoise(layer.getHeightGenerator(new RNG(FLOATING_SEED), null),
reversed.getHeightGenerator(new RNG(FLOATING_SEED), null));
assertNoise(layer.getHeightGenerator(new RNG(CARVE_SEED), null),
reversed.getHeightGenerator(new RNG(CARVE_SEED), null));
}
@Test
public void sharedSurfaceCarveAndFloatingBiomeIgnoresWarmOrder() {
IrisBiome biome = biome();
long[] seeds = {FLOATING_SEED, CARVE_SEED, TERRAIN_SEED, FLOATING_SEED + 733L};
for (long seed : seeds) {
assertGenerators(biome().getLayerHeightGenerators(new RNG(seed), null),
biome.getLayerHeightGenerators(new RNG(seed), null));
}
IrisBiome reversed = biome();
for (int i = seeds.length - 1; i >= 0; i--) {
assertGenerators(biome.getLayerHeightGenerators(new RNG(seeds[i]), null),
reversed.getLayerHeightGenerators(new RNG(seeds[i]), null));
}
assertNotEquals(biome.getLayerHeightGenerators(new RNG(TERRAIN_SEED), null).get(0).noise(13, -29),
biome.getLayerHeightGenerators(new RNG(FLOATING_SEED), null).get(0).noise(13, -29), 0D);
}
@Test
public void concurrentSurfaceCarveAndFloatingCallersMatchIsolatedBiomes() throws Exception {
IrisBiome shared = biome();
ExecutorService executor = Executors.newFixedThreadPool(8);
CountDownLatch start = new CountDownLatch(1);
List<Future<?>> tasks = new ArrayList<>();
long[] seeds = {TERRAIN_SEED, CARVE_SEED, FLOATING_SEED, FLOATING_SEED + 733L};
try {
for (int worker = 0; worker < 8; worker++) {
long seed = seeds[worker % seeds.length];
KList<CNG> expected = biome().getLayerHeightGenerators(new RNG(seed), null);
tasks.add(executor.submit(() -> {
assertTrue(start.await(5, TimeUnit.SECONDS));
for (int sample = 0; sample < 30; sample++) {
assertGenerators(expected, shared.getLayerHeightGenerators(new RNG(seed), null));
}
return null;
}));
}
start.countDown();
for (Future<?> task : tasks) {
task.get(10, TimeUnit.SECONDS);
}
} finally {
executor.shutdownNow();
}
}
@Test
public void runtimeAndDataIdentitiesDoNotReuseHeightGenerators() {
IrisBiome biome = biome();
IrisData data = mock(IrisData.class);
IrisData otherData = mock(IrisData.class);
Engine first = mock(Engine.class);
Engine second = mock(Engine.class);
when(data.getEngine()).thenReturn(first);
when(otherData.getEngine()).thenReturn(first);
RNG rng = new RNG(TERRAIN_SEED);
KList<CNG> original = biome.getLayerHeightGenerators(rng, data);
assertSame(original, biome.getLayerHeightGenerators(rng, data));
KList<CNG> other = biome.getLayerHeightGenerators(rng, otherData);
assertNotSame(original, other);
assertNotSame(original.get(0), other.get(0));
assertGenerators(original, other);
when(data.getEngine()).thenReturn(second);
KList<CNG> replacement = biome.getLayerHeightGenerators(rng, data);
assertNotSame(original, replacement);
assertNotSame(original.get(0), replacement.get(0));
assertGenerators(original, replacement);
when(data.getEngine()).thenReturn(first);
assertSame(original, biome.getLayerHeightGenerators(rng, data));
}
@Test
public void materialBandsMatchIsolatedGenerationAcrossSurfaceSeaAndCeiling() {
NativeBlockState first = mock(NativeBlockState.class);
NativeBlockState second = mock(NativeBlockState.class);
IrisBiome shared = materialBiome(first, second);
IrisDimension dimension = new IrisDimension();
long[] seeds = {FLOATING_SEED, TERRAIN_SEED, CARVE_SEED, FLOATING_SEED + 733L};
for (long seed : seeds) {
IrisBiome isolated = materialBiome(first, second);
RNG rng = new RNG(seed);
for (int x = -65; x <= 65; x += 7) {
assertEquals(isolated.generateLayers(dimension, x, -x, rng, 64, 120, null, null),
shared.generateLayers(dimension, x, -x, rng, 64, 120, null, null));
assertEquals(isolated.generateCeilingLayers(dimension, x, -x, rng, 64, 120, null, null),
shared.generateCeilingLayers(dimension, x, -x, rng, 64, 120, null, null));
assertEquals(isolated.generateSeaLayers(x, -x, rng, 64, null),
shared.generateSeaLayers(x, -x, rng, 64, null));
}
}
}
@Test
public void evictionRecomputesTheSameNoise() {
IrisBiome biome = biome();
KList<CNG> original = biome.getLayerHeightGenerators(new RNG(TERRAIN_SEED), null);
for (int seed = 0; seed < 96; seed++) {
biome.getLayerHeightGenerators(new RNG(seed), null);
}
KList<CNG> recomputed = biome.getLayerHeightGenerators(new RNG(TERRAIN_SEED), null);
assertNotSame(original, recomputed);
assertNotSame(original.get(0), recomputed.get(0));
assertGenerators(original, recomputed);
}
private static IrisBiomePaletteLayer layer() {
return new IrisBiomePaletteLayer().zero().setMinHeight(1).setMaxHeight(12);
}
private static IrisBiome biome() {
return new IrisBiome().setLayers(new KList<>(layer(), layer().setMinHeight(3).setMaxHeight(5)));
}
private static IrisBiome materialBiome(NativeBlockState first, NativeBlockState second) {
IrisBiomePaletteLayer upper = IrisBiomeCeilingLayerTest.layer(first, 1).setMaxHeight(5);
IrisBiomePaletteLayer lower = IrisBiomeCeilingLayerTest.layer(second, 3).setMaxHeight(12);
KList<IrisBiomePaletteLayer> layers = new KList<>(upper, lower);
return new IrisBiome().setLayers(layers).setCaveCeilingLayers(layers).setSeaLayers(layers);
}
private static void assertGenerators(KList<CNG> expected, KList<CNG> actual) {
assertEquals(expected.size(), actual.size());
for (int index = 0; index < expected.size(); index++) {
assertNoise(expected.get(index), actual.get(index));
}
}
private static void assertNoise(CNG expected, CNG actual) {
for (int x = -97; x <= 97; x += 13) {
assertEquals(expected.noise(x, x * -3.25), actual.noise(x, x * -3.25), 0D);
}
}
}
@@ -0,0 +1,156 @@
package art.arcane.iris.generation.chunk;
import art.arcane.iris.platform.bukkit.BukkitBiome;
import art.arcane.iris.testsupport.BukkitTestServer;
import art.arcane.iris.world.history.BoundaryColumnGeometry;
import art.arcane.iris.world.history.NativeTerrainReceipt;
import art.arcane.iris.world.history.SavedTerrainChunk;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import io.papermc.paper.InternalAPIBridge;
import io.papermc.paper.registry.RegistryAccess;
import net.kyori.adventure.key.Key;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.block.Biome;
import org.bukkit.generator.ChunkGenerator.ChunkData;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Proxy;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.RETURNS_DEFAULTS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class LinkedTerrainChunkTest {
@BeforeClass
public static void initializeBiomes() throws Exception {
BukkitTestServer.install();
AtomicReference<Object> registered = new AtomicReference<>();
RegistryAccess access = mock(RegistryAccess.class, invocation -> {
if (invocation.getMethod().getName().equals("getRegistry")) {
if (registered.get() == null) {
registered.set(Proxy.newProxyInstance(Registry.class.getClassLoader(), new Class<?>[]{Registry.class},
(proxy, method, arguments) -> method.getName().equals("getOrThrow")
? biome((Key) arguments[0]) : null));
}
return registered.get();
}
return RETURNS_DEFAULTS.answer(invocation);
});
InternalAPIBridge bridge = mock(InternalAPIBridge.class, invocation ->
invocation.getMethod().getName().equals("constructLegacyCustomBiome")
? biome(Key.key("minecraft:custom")) : RETURNS_DEFAULTS.answer(invocation));
try (MockedStatic<RegistryAccess> registries = mockStatic(RegistryAccess.class);
MockedStatic<InternalAPIBridge> internals = mockStatic(InternalAPIBridge.class)) {
registries.when(RegistryAccess::registryAccess).thenReturn(access);
internals.when(InternalAPIBridge::get).thenReturn(bridge);
Class.forName(Registry.class.getName());
Class.forName(Biome.class.getName());
}
}
@Test
public void biomeReadsReuseCanonicalHandlesAndPreserveReceiptBytes() throws Exception {
LinkedTerrainChunk chunk = chunk();
NativeBiome plains = BukkitBiome.of(Biome.PLAINS);
NativeBiome desert = BukkitBiome.of(Biome.DESERT);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
chunk.fillBiomeColumn(x, z, plains);
}
}
NativeBiome alternate = mock(NativeBiome.class);
when(alternate.nativeHandle()).thenReturn(Biome.DESERT);
when(alternate.key()).thenReturn("ignored:alias");
chunk.setBiome(0, -12, 0, alternate);
chunk.setBiome(17, 100, -1, desert);
AtomicInteger conversions = new AtomicInteger();
try (MockedStatic<BukkitBiome> wrappers = mockStatic(BukkitBiome.class, invocation -> {
conversions.incrementAndGet();
return CALLS_REAL_METHODS.answer(invocation);
})) {
assertSame(desert, chunk.getBiome(0, -12, 0));
assertSame(desert, chunk.getBiome(1, -1, 15));
assertSame(plains, chunk.getBiome(0, -100, 0));
for (boolean boundary : new boolean[]{false, true}) {
SavedTerrainChunk.VoxelSource actual = source((x, y, z) -> chunk.getBiome(x, y, z).key());
SavedTerrainChunk.VoxelSource expected = source((x, y, z) ->
x == 0 && y == -12 && z == 0 || x == 1 && y == -1 && z == 15
? "minecraft:desert" : "minecraft:plains");
SavedTerrainChunk captured = capture(actual, boundary);
assertArrayEquals(NativeTerrainReceipt.encode(capture(expected, boundary), 7L, "epoch"),
NativeTerrainReceipt.encode(captured, 7L, "epoch"));
}
assertEquals(0, conversions.get());
}
}
@Test
public void emptyColumnsResolveTheDefaultBiomeOnlyOnce() {
LinkedTerrainChunk chunk = chunk();
NativeBiome plains = BukkitBiome.of(Biome.PLAINS);
AtomicInteger conversions = new AtomicInteger();
try (MockedStatic<BukkitBiome> wrappers = mockStatic(BukkitBiome.class, invocation -> {
conversions.incrementAndGet();
return CALLS_REAL_METHODS.answer(invocation);
})) {
for (int y = -32; y < 16; y++) {
assertSame(plains, chunk.getBiome(0, y, 0));
}
assertEquals(1, conversions.get());
}
}
private static LinkedTerrainChunk chunk() {
ChunkData data = mock(ChunkData.class);
when(data.getMinHeight()).thenReturn(-16);
when(data.getMaxHeight()).thenReturn(0);
return new LinkedTerrainChunk(data);
}
private static SavedTerrainChunk capture(SavedTerrainChunk.VoxelSource source, boolean boundary) throws Exception {
return boundary ? SavedTerrainChunk.captureBoundary(-1, 2, -16, 16, "minecraft:noise", source)
: SavedTerrainChunk.capture(-1, 2, -16, 16, "minecraft:noise", source);
}
private static SavedTerrainChunk.VoxelSource source(BiomeKey source) {
BoundaryColumnGeometry.Voxel stone = new BoundaryColumnGeometry.Voxel(
"minecraft:stone", BoundaryColumnGeometry.Phase.SOLID, "", false);
return new SavedTerrainChunk.VoxelSource() {
@Override
public BoundaryColumnGeometry.Voxel voxel(int x, int y, int z) {
return stone;
}
@Override
public String biome(int x, int y, int z) {
return source.get(x, y, z);
}
};
}
private static Biome biome(Key key) {
NamespacedKey namespaced = new NamespacedKey(key.namespace(), key.value());
return (Biome) Proxy.newProxyInstance(Biome.class.getClassLoader(), new Class<?>[]{Biome.class},
(proxy, method, arguments) -> switch (method.getName()) {
case "getKey" -> namespaced;
case "hashCode" -> namespaced.hashCode();
case "equals" -> proxy == arguments[0];
case "toString", "name" -> key.value();
default -> null;
});
}
private interface BiomeKey {
String get(int x, int y, int z);
}
}
@@ -1,11 +1,42 @@
package art.arcane.iris.generation.decoration.tree;
import org.junit.Test;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.junit.Assert.assertTrue;
public class TreeBlockMaterialTest {
@Test
public void nativeMaterialUsesTheMemoizedStringWithoutReadingTheStateKey() {
NativeBlockState state = mock(NativeBlockState.class);
String material = new String("minecraft:oak_log");
when(state.materialKey()).thenReturn(material);
TreeBlockMaterial first = TreeBlockMaterial.of(state);
TreeBlockMaterial second = TreeBlockMaterial.of(state);
assertSame(material, first.materialKey());
assertSame(first.materialKey(), second.materialKey());
verify(state, never()).key();
assertEquals(TreeBlockMaterial.of("minecraft:oak_log[axis=x]"), first);
}
@Test
public void nativeMaterialFallsBackToTheStateKeyWhenNotMemoized() {
NativeBlockState state = mock(NativeBlockState.class);
when(state.key()).thenReturn("minecraft:oak_log[axis=z]");
assertEquals(TreeBlockMaterial.of("minecraft:oak_log"), TreeBlockMaterial.of(state));
verify(state).key();
}
@Test
public void materialComparisonIgnoresBlockStateProperties() {
TreeBlockMaterial expected = TreeBlockMaterial.of("minecraft:oak_log[axis=y]");
@@ -0,0 +1,158 @@
package art.arcane.iris.generation.hydrology;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
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.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class HydrologyBasisCacheTest {
@Test
public void batchLookupPreservesNullAndFirstPublishedSample() {
HydrologyBasisCache cache = new HydrologyBasisCache(1);
HydrologyTerrainSample[] samples = new HydrologyTerrainSample[1];
long key = RiverFootprint.pack(-17, Integer.MAX_VALUE);
assertTrue(!cache.findWithoutSlope(key, samples, 0));
assertNull(cache.rememberWithoutSlope(key, null));
assertTrue(cache.findWithoutSlope(key, samples, 0));
assertNull(samples[0]);
HydrologyTerrainSample later = HydrologyTerrainSample.openLand(73, 0D, "land");
assertNull(cache.rememberWithoutSlope(key, later));
cache.rememberWithoutSlope(key + 1, later);
assertTrue(!cache.findWithoutSlope(key, samples, 0));
assertEquals(later, cache.rememberWithoutSlope(key + 1, null));
}
@Test
public void channelsNullsFailuresAndEvictionPreserveSamples() {
CountingSampler sampler = new CountingSampler();
HydrologyBasisCache cache = new HydrologyBasisCache(1);
assertEquals(3D, cache.sample(1, -2, sampler).slope(), 0D);
assertEquals(0D, cache.sampleWithoutSlope(1, -2, sampler).slope(), 0D);
assertEquals(3D, cache.sample(1, -2, sampler).slope(), 0D);
assertEquals(0D, cache.sampleWithoutSlope(1, -2, sampler).slope(), 0D);
assertEquals(1, sampler.fullCalls.get());
assertEquals(1, sampler.slopeFreeCalls.get());
assertNull(cache.sampleWithoutSlope(Integer.MIN_VALUE, Integer.MAX_VALUE, sampler));
assertNull(cache.sampleWithoutSlope(Integer.MIN_VALUE, Integer.MAX_VALUE, sampler));
assertEquals(2, sampler.slopeFreeCalls.get());
assertThrows(IllegalStateException.class, () -> cache.sampleWithoutSlope(99, 0, sampler));
assertEquals(87, cache.sampleWithoutSlope(99, 0, sampler).naturalHeight());
assertEquals(87, cache.sampleWithoutSlope(99, 0, sampler).naturalHeight());
assertEquals(4, sampler.slopeFreeCalls.get());
assertEquals(87, cache.sampleWithoutSlope(1, -2, sampler).naturalHeight());
assertEquals(5, sampler.slopeFreeCalls.get());
}
@Test
public void plannerTrialsPreserveLocalSlopePrecedenceAndOwnerIsolation() {
CountingSampler natural = new CountingSampler();
HydrologyTerrainSampler detailed = (x, z) -> {
throw new AssertionError("Basis caching must not sample caves");
};
HydrologyPlannerSettings settings = HydrologyPlannerSettings.defaults();
HydrologyPlanner planner = new HydrologyPlanner(23L, settings, detailed, natural,
HydrologyGeometrySampler.deterministic(detailed), -4096,
footprint -> new HydrologyTerrainCaveVoxelView(detailed, settings.seaLevel(), -4096, 4096));
planner.planningSamples.set(new HydrologyPlanner.PlanningSamples());
try {
assertEquals(3D, planner.sampleBasis(1, 1).slope(), 0D);
assertEquals(0D, planner.sampleBasisWithoutSlope(2, 1).slope(), 0D);
HydrologyBasisCache shared = planner.planningSamples.get()
.fallbackBasis(new Long2ObjectOpenHashMap<>());
for (int trial = 0; trial < 4; trial++) {
planner.planningSamples.set(new HydrologyPlanner.PlanningSamples(null, shared));
assertEquals(0D, planner.sampleBasisWithoutSlope(1, 1).slope(), 0D);
assertEquals(3D, planner.sampleBasis(1, 1).slope(), 0D);
assertEquals(3D, planner.sampleBasisWithoutSlope(1, 1).slope(), 0D);
assertEquals(0D, planner.sampleBasisWithoutSlope(2, 1).slope(), 0D);
}
assertEquals(1, natural.fullCalls.get());
assertEquals(2, natural.slopeFreeCalls.get());
planner.planningSamples.set(new HydrologyPlanner.PlanningSamples());
assertEquals(0D, planner.sampleBasisWithoutSlope(1, 1).slope(), 0D);
assertEquals(3, natural.slopeFreeCalls.get());
} finally {
planner.planningSamples.remove();
}
}
@Test
public void parallelTrialsComputeEachCoordinateOnce() throws Exception {
CountingSampler sampler = new CountingSampler();
HydrologyBasisCache cache = new HydrologyBasisCache();
CountDownLatch ready = new CountDownLatch(4);
CountDownLatch start = new CountDownLatch(1);
ExecutorService workers = Executors.newFixedThreadPool(4);
try {
List<Future<List<HydrologyTerrainSample>>> results = new ArrayList<>();
for (int trial = 0; trial < 4; trial++) {
results.add(workers.submit(() -> {
ready.countDown();
assertTrue(start.await(5, TimeUnit.SECONDS));
List<HydrologyTerrainSample> values = new ArrayList<>();
for (int x = -128; x < 0; x++) {
values.add(cache.sampleWithoutSlope(x, -17, sampler));
}
return values;
}));
}
assertTrue(ready.await(5, TimeUnit.SECONDS));
start.countDown();
List<HydrologyTerrainSample> expected = results.getFirst().get(5, TimeUnit.SECONDS);
for (Future<List<HydrologyTerrainSample>> result : results) {
assertEquals(expected, result.get(5, TimeUnit.SECONDS));
}
assertEquals(128, sampler.slopeFreeCalls.get());
} finally {
start.countDown();
workers.shutdownNow();
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
}
}
private static final class CountingSampler implements HydrologyNaturalTerrainSampler {
private final AtomicInteger fullCalls = new AtomicInteger();
private final AtomicInteger slopeFreeCalls = new AtomicInteger();
private final AtomicInteger failures = new AtomicInteger();
@Override
public HydrologyTerrainSample sampleBasis(int x, int z) {
fullCalls.incrementAndGet();
return HydrologyTerrainSample.openLand(87, 3D, "land");
}
@Override
public HydrologyTerrainSample sampleBasisWithoutSlope(int x, int z) {
slopeFreeCalls.incrementAndGet();
if (x == Integer.MIN_VALUE) {
return null;
}
if (x == 99 && failures.getAndIncrement() == 0) {
throw new IllegalStateException("retry");
}
return HydrologyTerrainSample.openLand(87, 0D, "land");
}
@Override
public HydrologyTerrainSample[] sampleGrid(GridRequest request) {
throw new AssertionError("Basis caching must not sample grids");
}
@Override
public NaturalClassification classifyNatural(int x, int z) {
throw new AssertionError("Basis caching must not classify separately");
}
}
}
@@ -34,6 +34,7 @@ public class HydrologyDiagnosticCacheTest {
@Test
public void terrainLoadsLeaveDiagnosticsLazyAndQueriesRetainImmutableResults() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
when(planner.settings()).thenReturn(HydrologyPlannerSettings.defaults());
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyTileKey key = new HydrologyTileKey(0, 0);
HydrologyDiagnosticCandidate candidate = mock(HydrologyDiagnosticCandidate.class);
@@ -59,6 +60,7 @@ public class HydrologyDiagnosticCacheTest {
@Test
public void forbiddenThreadsCanReadOnlyCompleteCachedDiagnostics() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
when(planner.settings()).thenReturn(HydrologyPlannerSettings.defaults());
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyTileKey key = new HydrologyTileKey(0, 0);
AtomicBoolean forbidden = new AtomicBoolean(true);
@@ -77,6 +79,7 @@ public class HydrologyDiagnosticCacheTest {
@Test
public void interruptedWaiterLeavesTheSharedDiagnosticLoadRunning() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
when(planner.settings()).thenReturn(HydrologyPlannerSettings.defaults());
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyTileKey key = new HydrologyTileKey(0, 0);
List<HydrologyDiagnosticCandidate> expected = List.of(mock(HydrologyDiagnosticCandidate.class));
@@ -127,6 +130,7 @@ public class HydrologyDiagnosticCacheTest {
@Test
public void clearDoesNotPublishAnOlderDiagnosticResultIntoTheNewEpoch() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
when(planner.settings()).thenReturn(HydrologyPlannerSettings.defaults());
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyTileKey key = new HydrologyTileKey(0, 0);
List<HydrologyDiagnosticCandidate> old = List.of(mock(HydrologyDiagnosticCandidate.class));
@@ -166,6 +170,7 @@ public class HydrologyDiagnosticCacheTest {
@Test
public void closeCancelsQueuedDiagnosticsAndDrainsActiveDiagnostics() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
when(planner.settings()).thenReturn(HydrologyPlannerSettings.defaults());
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyTileKey firstKey = new HydrologyTileKey(0, 0);
HydrologyTileKey secondKey = new HydrologyTileKey(1, 0);
@@ -213,6 +218,7 @@ public class HydrologyDiagnosticCacheTest {
@Test
public void aDiagnosticLoadCannotCloseItsOwnCache() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
when(planner.settings()).thenReturn(HydrologyPlannerSettings.defaults());
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyTileKey key = new HydrologyTileKey(0, 0);
when(planner.plan(key)).thenReturn(tile);
@@ -229,6 +235,7 @@ public class HydrologyDiagnosticCacheTest {
@Test
public void diagnosticFailureIsRetriedAndCompletedListsRemainBounded() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
when(planner.settings()).thenReturn(HydrologyPlannerSettings.defaults());
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
when(planner.diagnosticCandidates(tile)).thenThrow(new IllegalStateException("expected")).thenReturn(List.of());
@@ -113,6 +113,34 @@ public class HydrologyFootprintCompilerTest {
}
}
@Test
public void fallbackCompilersShareBasisWithoutChangingValidationOrFootprints() {
HydraulicSegment segment = new HydraulicSegment(
9001L, 9000L, HydrologyFeatureType.SURFACE_POOL, 70, 70, 6, 3, false, false,
List.of(new HydrologyPoint(-12, 70, -3), new HydrologyPoint(32, 70, 5)),
HydraulicChannelProfile.uniform(6, 3));
RiverCourse course = course(9000L, RiverCourseType.SURFACE, segment);
SlopeCountingSampler sampler = new SlopeCountingSampler(true);
HydrologyFootprintCompiler owner = new HydrologyFootprintCompiler(
HydrologyPlannerSettings.defaults(),
new HydrologyFootprintCompiler.Sampling(sampler, request -> request.minimum(), sampler));
HydrologyFootprintCompiler.ValidationRaster expectedValidation = owner.compileValidation(List.of(course));
RiverFootprint expected = owner.compile(List.of(course));
int ownerCalls = sampler.slopeFreeCalls;
assertTrue(ownerCalls > 0);
HydrologyBasisCache shared = new HydrologyBasisCache();
shared.seedWithoutSlope(owner.terrainBases);
for (int trial = 0; trial < 3; trial++) {
HydrologyFootprintCompiler alternative = new HydrologyFootprintCompiler(
HydrologyPlannerSettings.defaults(),
new HydrologyFootprintCompiler.Sampling(sampler, request -> request.minimum(), sampler));
alternative.sharedBasis = shared;
assertEquals(expectedValidation.columns(), alternative.compileValidation(List.of(course)).columns());
assertEquals(expected, alternative.compile(List.of(course)));
}
assertEquals(ownerCalls, sampler.slopeFreeCalls);
}
@Test
public void surfaceChannelNeverRaisesTerrainOrPublishesUnsupportedFluid() {
HydraulicSegment segment = new HydraulicSegment(
@@ -536,11 +564,14 @@ public class HydrologyFootprintCompilerTest {
);
RiverCourse course = course(42L, RiverCourseType.SURFACE, segment);
HydrologyTerrainSampler terrain = (int x, int z) -> HydrologyTerrainSample.openLand(80, 1D, "parent");
RiverFootprint footprint = new HydrologyFootprintCompiler(
HydrologyFootprintCompiler compiler = new HydrologyFootprintCompiler(
HydrologyPlannerSettings.defaults(),
terrain,
request -> request.minimum()
).compile(List.of(course));
);
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED,
compiler.surfaceFootprintForPublication(course).rejection());
RiverFootprint footprint = compiler.compile(List.of(course));
int channelEdge = 0;
while (layerAt(footprint, channelEdge + 1, 0).channel()) {
@@ -143,7 +143,7 @@ public class HydrologyPlannerRoutingSamplerTest {
assertEquals(6, heightCalls.get());
HydrologyLandHeightCache shared = planner.planningSamples.get().fallbackLandHeights();
for (int trial = 0; trial < 3; trial++) {
planner.planningSamples.set(new HydrologyPlanner.PlanningSamples(shared));
planner.planningSamples.set(new HydrologyPlanner.PlanningSamples(shared, null));
assertEquals(Double.NaN, planner.sampleLandHeight(0, -17), 0D);
assertEquals(0D, planner.sampleLandHeight(1, -17), 0D);
assertEquals(Integer.MIN_VALUE, planner.sampleLandHeight(2, -17), 0D);
@@ -2118,13 +2118,15 @@ public class HydrologyPlannerTest {
}
@Test
public void fallbackAlternatesCoastalAndInlandTrials() {
public void fallbackAlternatesCoastalAndInlandTrials() throws Exception {
// An island whose coast is walled by a berm between the lattice columns: every mouth course
// needs a cut deeper than the channel may make, so the surface routing falls back. The coast
// offers more fallback mouths than the trial budget, so a sinkhole only gets its trial when
// the fallback alternates coastal and inland candidates.
HydrologyPlannerSettings settings = standardSettings(1D, 0D, true, true, List.of());
AtomicInteger terrainSamples = new AtomicInteger();
HydrologyTerrainSampler island = (int x, int z) -> {
terrainSamples.incrementAndGet();
if (x < -16 || x > 144 || z < -16 || z > 144) {
return oceanTerrain();
}
@@ -2135,6 +2137,7 @@ public class HydrologyPlannerTest {
};
HydrologyTile tile = new HydrologyPlanner(885L, settings, island, solidCaveView()).plan(TILE);
assertTrue("Fallback terrain samples: " + terrainSamples.get(), terrainSamples.get() < 37_500);
assertFalse("diagnostics=" + tile.localDiagnosticCandidates(), surfaceCourses(tile).isEmpty());
assertTrue(tile.localDiagnosticCandidates().stream().anyMatch((HydrologyDiagnosticCandidate candidate) ->
@@ -2144,6 +2147,46 @@ public class HydrologyPlannerTest {
assertFalse("outlet=" + outlet, outlet.directOcean());
assertTrue(course.surfaceSinkholeContinuation());
}
HydrologyNaturalTerrainSampler natural = new HydrologyNaturalTerrainSampler() {
@Override
public HydrologyTerrainSample sampleBasis(int x, int z) {
return island.sample(x, z);
}
@Override
public HydrologyTerrainSample[] sampleGrid(GridRequest request) {
HydrologyTerrainSample[] samples = new HydrologyTerrainSample[request.width() * request.width()];
for (int z = 0; z < request.width(); z++) {
for (int x = 0; x < request.width(); x++) {
samples[z * request.width() + x] = island.sample(
request.minimumX() + x * request.spacing(), request.minimumZ() + z * request.spacing());
}
}
return samples;
}
@Override
public NaturalClassification classifyNatural(int x, int z) {
return island.sample(x, z).ocean() ? NaturalClassification.OCEAN : NaturalClassification.LAND;
}
@Override
public boolean supportsSharedGridSamples() {
return true;
}
};
HydrologyPlanner sharedPlanner = new HydrologyPlanner(885L, settings, island, natural,
HydrologyGeometrySampler.deterministic(island), Integer.MIN_VALUE, footprint -> solidCaveView());
ForkJoinPool workers = new ForkJoinPool(4);
try {
HydrologyTile shared = workers.submit(() -> sharedPlanner.plan(TILE)).get(30, TimeUnit.SECONDS);
assertTileContentsEqual(tile, shared);
assertEquals(tile.courses(), shared.courses());
assertEquals(tile.localDiagnosticCandidates(), shared.localDiagnosticCandidates());
} finally {
workers.shutdownNow();
}
}
private HydrologyPlannerSettings mixedCoastSettings(int maximumCoastalPerTile) {
@@ -40,7 +40,9 @@ public class HydrologyPlanningAdmissionIntegrationTest {
AtomicInteger peak = new AtomicInteger();
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyPlanner firstPlanner = mock(HydrologyPlanner.class);
when(firstPlanner.settings()).thenReturn(HydrologyPlannerSettings.defaults());
HydrologyPlanner secondPlanner = mock(HydrologyPlanner.class);
when(secondPlanner.settings()).thenReturn(HydrologyPlannerSettings.defaults());
HydrologyTileKey key = new HydrologyTileKey(0, 0);
when(firstPlanner.plan(key)).thenAnswer(invocation -> {
peak.accumulateAndGet(active.incrementAndGet(), Math::max);
@@ -86,6 +88,7 @@ public class HydrologyPlanningAdmissionIntegrationTest {
ExecutorService workers = Executors.newSingleThreadExecutor();
ExecutorService callers = Executors.newFixedThreadPool(2);
HydrologyPlanner planner = mock(HydrologyPlanner.class);
when(planner.settings()).thenReturn(HydrologyPlannerSettings.defaults());
HydrologyTileCache cache = new HydrologyTileCache(planner, 4, workers);
HydrologyTileKey key = new HydrologyTileKey(0, 0);
try {
@@ -101,6 +104,7 @@ public class HydrologyPlanningAdmissionIntegrationTest {
releaseRoots(held);
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyPlanner nextPlanner = mock(HydrologyPlanner.class);
when(nextPlanner.settings()).thenReturn(HydrologyPlannerSettings.defaults());
when(nextPlanner.plan(key)).thenReturn(tile);
try (HydrologyTileCache next = new HydrologyTileCache(nextPlanner, 4, workers)) {
assertSame(tile, callers.submit(() -> next.get(key)).get(5L, TimeUnit.SECONDS));
@@ -280,6 +280,7 @@ public class HydrologyTileCacheTest {
@Test
public void interruptedWaiterExitsWithoutCancellingTheSharedPlan() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
when(planner.settings()).thenReturn(emptySettings());
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyTileKey key = new HydrologyTileKey(0, 0);
CountDownLatch started = new CountDownLatch(1);
@@ -331,6 +332,7 @@ public class HydrologyTileCacheTest {
@Test
public void closeDrainsBackgroundPlanningAndRejectsNewDemand() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
when(planner.settings()).thenReturn(emptySettings());
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyTileKey key = new HydrologyTileKey(0, 0);
CountDownLatch started = new CountDownLatch(1);
@@ -402,6 +404,7 @@ public class HydrologyTileCacheTest {
@Test
public void closingFromAnOwnedLoadFailsWithoutDeadlockingOrClosingTheCache() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
when(planner.settings()).thenReturn(emptySettings());
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyTileCache cache = new HydrologyTileCache(planner, 4);
HydrologyTileKey key = new HydrologyTileKey(0, 0);
@@ -707,6 +710,7 @@ public class HydrologyTileCacheTest {
@Test
public void equivalentStudioRuntimesReuseCompletedTiles() {
HydrologyPlanner firstPlanner = mock(HydrologyPlanner.class);
when(firstPlanner.settings()).thenReturn(emptySettings());
HydrologyPlanner secondPlanner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyTileKey key = new HydrologyTileKey(2, -3);
@@ -739,7 +743,9 @@ public class HydrologyTileCacheTest {
@Test
public void failedPlansDoNotEnterTheSharedStudioCache() {
HydrologyPlanner failingPlanner = mock(HydrologyPlanner.class);
when(failingPlanner.settings()).thenReturn(emptySettings());
HydrologyPlanner succeedingPlanner = mock(HydrologyPlanner.class);
when(succeedingPlanner.settings()).thenReturn(emptySettings());
HydrologyTile emptyTile = mock(HydrologyTile.class);
HydrologyTile plannedTile = mock(HydrologyTile.class);
HydrologyTileKey key = new HydrologyTileKey(-4, 5);
@@ -759,6 +765,7 @@ public class HydrologyTileCacheTest {
@Test
public void closeWaitsForActivePlansWithoutPublishingTheirSharedTiles() throws Exception {
HydrologyPlanner closingPlanner = mock(HydrologyPlanner.class);
when(closingPlanner.settings()).thenReturn(emptySettings());
HydrologyPlanner succeedingPlanner = mock(HydrologyPlanner.class);
HydrologyTile lateTile = mock(HydrologyTile.class);
HydrologyTile plannedTile = mock(HydrologyTile.class);
@@ -956,6 +963,106 @@ public class HydrologyTileCacheTest {
}
}
@Test
public void singleColumnContributionReusesItsImmutableSample() {
HydrologyColumnSample sample = compositionSample(90, List.of(compositionLayer(11L, 80)));
try (HydrologyTileCache cache = compositionCache(Map.of(new HydrologyTileKey(-1, 0), sample))) {
assertSame(sample, cache.columnAt(-1, 0).orElseThrow());
assertTrue(cache.columnAt(-2, 0).isEmpty());
}
}
@Test
public void singleColumnContributionPreservesCanonicalLayerOrder() {
HydrologyColumnSample sample = compositionSample(90,
List.of(compositionLayer(11L, 80), compositionLayer(12L, 78)));
try (HydrologyTileCache cache = compositionCache(Map.of(new HydrologyTileKey(-1, 0), sample))) {
assertSame(sample, cache.columnAt(-1, 0).orElseThrow());
}
}
@Test
public void singleColumnContributionDeduplicatesIdenticalFeatureIds() {
HydrologyColumnLayer layer = compositionLayer(11L, 80);
HydrologyColumnSample sample = compositionSample(90, List.of(layer, layer));
try (HydrologyTileCache cache = compositionCache(Map.of(new HydrologyTileKey(-1, 0), sample))) {
assertEquals(compositionSample(90, List.of(layer)), cache.columnAt(-1, 0).orElseThrow());
}
}
@Test
public void singleColumnContributionRejectsConflictingFeatureIds() {
HydrologyColumnSample sample = compositionSample(90,
List.of(compositionLayer(11L, 80), compositionLayer(11L, 79)));
try (HydrologyTileCache cache = compositionCache(Map.of(new HydrologyTileKey(-1, 0), sample))) {
IllegalStateException failure = assertThrows(IllegalStateException.class, () -> cache.columnAt(-1, 0));
assertEquals("Hydrology feature id collision at -1,0.", failure.getMessage());
}
}
@Test
public void overlappingColumnContributionsMergeAndDeduplicateLayers() {
HydrologyColumnLayer shared = compositionLayer(11L, 80);
HydrologyColumnLayer additional = compositionLayer(12L, 78);
HydrologyColumnSample first = compositionSample(90, List.of(shared));
HydrologyColumnSample second = compositionSample(90, List.of(additional, shared));
try (HydrologyTileCache cache = compositionCache(Map.of(
new HydrologyTileKey(-1, 0), first, new HydrologyTileKey(0, 0), second))) {
assertEquals(compositionSample(90, List.of(shared, additional)), cache.columnAt(-1, 0).orElseThrow());
}
}
@Test
public void overlappingColumnContributionsRejectConflictingFeatureIds() {
HydrologyColumnSample first = compositionSample(90, List.of(compositionLayer(11L, 80)));
HydrologyColumnSample second = compositionSample(90, List.of(compositionLayer(11L, 79)));
try (HydrologyTileCache cache = compositionCache(Map.of(
new HydrologyTileKey(-1, 0), first, new HydrologyTileKey(0, 0), second))) {
IllegalStateException failure = assertThrows(IllegalStateException.class, () -> cache.columnAt(-1, 0));
assertEquals("Hydrology feature id collision at -1,0.", failure.getMessage());
}
}
@Test
public void overlappingColumnContributionsRejectDifferentNaturalTerrain() {
HydrologyColumnSample first = compositionSample(90, List.of(compositionLayer(11L, 80)));
HydrologyColumnSample second = compositionSample(91, List.of(compositionLayer(12L, 78)));
try (HydrologyTileCache cache = compositionCache(Map.of(
new HydrologyTileKey(-1, 0), first, new HydrologyTileKey(0, 0), second))) {
IllegalStateException failure = assertThrows(IllegalStateException.class, () -> cache.columnAt(-1, 0));
assertEquals("Hydrology plans disagree on terrain metadata at -1,0.", failure.getMessage());
}
}
private HydrologyTileCache compositionCache(Map<HydrologyTileKey, HydrologyColumnSample> samples) {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
when(planner.settings()).thenReturn(featureSettings());
when(planner.plan(any(HydrologyTileKey.class))).thenAnswer(invocation -> {
HydrologyTileKey key = invocation.getArgument(0);
HydrologyColumnSample sample = samples.get(key);
RiverFootprint footprint = sample == null ? RiverFootprint.empty()
: new RiverFootprint(Map.of(RiverFootprint.pack(sample.x(), sample.z()), sample));
HydrologyTile tile = mock(HydrologyTile.class);
when(tile.footprint()).thenReturn(footprint);
when(tile.columnAt(anyInt(), anyInt())).thenAnswer(query ->
footprint.sample(query.getArgument(0), query.getArgument(1)));
return tile;
});
return new HydrologyTileCache(planner, 64);
}
private static HydrologyColumnSample compositionSample(int naturalHeight, List<HydrologyColumnLayer> layers) {
return new HydrologyColumnSample(-1, 0, naturalHeight, 63, false, "parent", layers);
}
private static HydrologyColumnLayer compositionLayer(long featureId, int bedY) {
HydrologyFeatureRef feature = new HydrologyFeatureRef(featureId, HydrologyFeatureType.SURFACE_POOL,
1L, 1L, -1, 82, 0, 1, 0, false);
return new HydrologyColumnLayer(feature, bedY, 82, 82,
true, false, false, true, false, false, true, true, false,
"default", "river", "mouth", "shore", "bank", "cave");
}
@Test
public void concurrentDifferentTileAndCompositeQueriesRemainDeterministic() throws Exception {
HydrologyPlanner planner = new HydrologyPlanner(1211L, featureSettings(), this::featureTerrain);
@@ -1338,6 +1445,7 @@ public class HydrologyTileCacheTest {
private void assertConcurrentTilePlanning(List<HydrologyTileKey> keys) throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
when(planner.settings()).thenReturn(emptySettings());
HydrologyTile tile = mock(HydrologyTile.class);
CountDownLatch started = new CountDownLatch(Math.min(keys.size(), HydrologyPlanningAdmission.maximumRoots()));
CountDownLatch release = new CountDownLatch(1);
@@ -1449,6 +1557,7 @@ public class HydrologyTileCacheTest {
@Test
public void clearDoesNotJoinOrPublishAnOldTilePlan() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
when(planner.settings()).thenReturn(emptySettings());
HydrologyTile oldTile = mock(HydrologyTile.class);
HydrologyTile newTile = mock(HydrologyTile.class);
HydrologyTileKey key = new HydrologyTileKey(0, 0);
@@ -0,0 +1,151 @@
package art.arcane.iris.generation.hydrology;
import org.junit.Test;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
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;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class HydrologyTileDemandWindowTest {
@Test
public void completedSlotsRefillBeforeTheSlowestTileFinishes() throws Exception {
assumeTrue(HydrologyPlanningAdmission.maximumRoots() >= 2);
HydrologyPlanner planner = planner();
HydrologyTile first = mock(HydrologyTile.class);
HydrologyTile second = mock(HydrologyTile.class);
HydrologyTile third = mock(HydrologyTile.class);
List<HydrologyTile> expected = List.of(first, second, third);
CountDownLatch firstStarted = new CountDownLatch(1);
CountDownLatch releaseFirst = new CountDownLatch(1);
CountDownLatch thirdStarted = new CountDownLatch(1);
AtomicInteger active = new AtomicInteger();
AtomicInteger peak = new AtomicInteger();
doAnswer(invocation -> {
HydrologyTileKey key = invocation.getArgument(0);
peak.accumulateAndGet(active.incrementAndGet(), Math::max);
try {
if (key.tileX() == 0) {
firstStarted.countDown();
assertTrue(releaseFirst.await(5, TimeUnit.SECONDS));
} else {
assertTrue(firstStarted.await(5, TimeUnit.SECONDS));
if (key.tileX() == 2) {
thirdStarted.countDown();
}
}
return expected.get(key.tileX());
} finally {
active.decrementAndGet();
}
}).when(planner).plan(any(HydrologyTileKey.class));
ExecutorService workers = Executors.newFixedThreadPool(3);
ExecutorService caller = Executors.newSingleThreadExecutor();
HydrologyTileCache cache = new HydrologyTileCache(planner, 4, workers);
try {
Future<List<HydrologyTile>> result = caller.submit(() -> cache.tiles(keys()));
assertTrue("a completed slot must admit the third tile while the first is blocked",
thirdStarted.await(5, TimeUnit.SECONDS));
assertFalse(result.isDone());
releaseFirst.countDown();
assertEquals(expected, result.get(5, TimeUnit.SECONDS));
assertEquals(2, peak.get());
} finally {
releaseFirst.countDown();
caller.shutdownNow();
workers.shutdownNow();
assertTrue(caller.awaitTermination(5, TimeUnit.SECONDS));
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
cache.close();
}
}
@Test
public void cancellationStopsRefillWithoutCancellingSharedSiblingWork() throws Exception {
assumeTrue(HydrologyPlanningAdmission.maximumRoots() >= 2);
HydrologyPlanner planner = planner();
HydrologyTile tile = mock(HydrologyTile.class);
CountDownLatch firstStarted = new CountDownLatch(1);
CountDownLatch releaseFirst = new CountDownLatch(1);
AtomicInteger planned = new AtomicInteger();
doAnswer(invocation -> {
HydrologyTileKey key = invocation.getArgument(0);
planned.incrementAndGet();
if (key.tileX() == 0) {
firstStarted.countDown();
assertTrue(releaseFirst.await(5, TimeUnit.SECONDS));
return tile;
}
assertTrue(firstStarted.await(5, TimeUnit.SECONDS));
throw new CancellationException("Cancelled demand tile");
}).when(planner).plan(any(HydrologyTileKey.class));
ExecutorService workers = Executors.newFixedThreadPool(3);
ExecutorService callers = Executors.newFixedThreadPool(2);
HydrologyTileCache cache = new HydrologyTileCache(planner, 4, workers);
try {
Future<List<HydrologyTile>> result = callers.submit(() -> cache.tiles(keys()));
ExecutionException failure = assertThrows(ExecutionException.class,
() -> result.get(5, TimeUnit.SECONDS));
assertTrue(failure.getCause() instanceof CancellationException);
assertEquals(2, planned.get());
Future<HydrologyTile> shared = callers.submit(() -> cache.get(keys().getFirst()));
releaseFirst.countDown();
assertSame(tile, shared.get(5, TimeUnit.SECONDS));
assertEquals(2, planned.get());
} finally {
releaseFirst.countDown();
callers.shutdownNow();
workers.shutdownNow();
assertTrue(callers.awaitTermination(5, TimeUnit.SECONDS));
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
cache.close();
}
}
@Test
public void capturedCachedTilesSurviveInvalidationDuringAnotherPlan() {
HydrologyPlanner planner = planner();
HydrologyTile cached = mock(HydrologyTile.class);
HydrologyTile generated = mock(HydrologyTile.class);
HydrologyTileKey cachedKey = new HydrologyTileKey(2, 0);
when(planner.plan(cachedKey)).thenReturn(cached);
HydrologyTileCache cache = new HydrologyTileCache(planner, 4, Runnable::run);
try {
assertSame(cached, cache.get(cachedKey));
doAnswer(invocation -> {
cache.clear();
return generated;
}).when(planner).plan(keys().getFirst());
when(planner.plan(cachedKey)).thenThrow(new AssertionError("Captured tile was replanned"));
assertEquals(List.of(generated, cached), cache.tiles(List.of(keys().getFirst(), cachedKey)));
} finally {
cache.close();
}
}
private static HydrologyPlanner planner() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
when(planner.settings()).thenReturn(HydrologyPlannerSettings.defaults());
return planner;
}
private static List<HydrologyTileKey> keys() {
return List.of(new HydrologyTileKey(0, 0), new HydrologyTileKey(1, 0), new HydrologyTileKey(2, 0));
}
}
@@ -2,6 +2,7 @@ package art.arcane.iris.generation.hydrology.runtime;
import art.arcane.iris.generation.hydrology.HydrologyRoutingTerrainSampler;
import art.arcane.iris.generation.hydrology.HydrologyTerrainSample;
import art.arcane.iris.generation.hydrology.RiverFootprint;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.runtime.Engine;
@@ -26,6 +27,9 @@ import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ExecutionException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
@@ -36,6 +40,75 @@ import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
public class IrisHydrologyRoutingTerrainSamplerTest {
@Test
public void boundedBasisBatchUsesScopedWorkersAndDrainsNestedPool() throws Exception {
ThreadLocal<Integer> scope = new ThreadLocal<>();
AtomicInteger opened = new AtomicInteger();
AtomicInteger closed = new AtomicInteger();
AtomicInteger samples = new AtomicInteger();
CountDownLatch simultaneous = new CountDownLatch(2);
ForkJoinPool pool = new ForkJoinPool(2);
try {
IrisHydrologyRoutingTerrainSampler sampler = new IrisHydrologyRoutingTerrainSampler(
new IrisHydrologyRoutingTerrainSampler.Sources((x, z, height) -> {
assertEquals(Integer.valueOf(7), scope.get());
if (x >= 10_000) {
throw new IllegalStateException("terrain failure");
}
simultaneous.countDown();
try {
assertTrue(simultaneous.await(5, TimeUnit.SECONDS));
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IllegalStateException(failure);
}
samples.incrementAndGet();
return new IrisHydrologyRoutingTerrainSampler.TerrainBasis(height,
HydrologyTerrainSample.openLand((int) height, 0D, "land"));
}, (x, z) -> 100 + Math.floorMod(x + z, 17), (x, z) -> false, 60),
new IrisHydrologyRoutingTerrainSampler.SamplingOptions(8192, 2, pool, () -> () -> {
Integer previous = scope.get();
scope.set(7);
opened.incrementAndGet();
return () -> {
if (previous == null) {
scope.remove();
} else {
scope.set(previous);
}
closed.incrementAndGet();
};
}));
long[] coordinates = new long[4096];
for (int index = 0; index < coordinates.length; index++) {
coordinates[index] = RiverFootprint.pack(index - 2048, -index);
}
HydrologyTerrainSample[] result = pool.submit(() -> sampler.sampleBasisWithoutSlopeBatch(
coordinates, coordinates.length)).get(10, TimeUnit.SECONDS);
for (int index = 0; index < result.length; index++) {
assertEquals(100 + Math.floorMod(-2048, 17), result[index].naturalHeight());
}
assertEquals(coordinates.length, samples.get());
assertEquals(2, opened.get());
assertEquals(opened.get(), closed.get());
assertEquals(null, pool.submit(scope::get).get(5, TimeUnit.SECONDS));
assertThrows(IllegalArgumentException.class,
() -> sampler.sampleBasisWithoutSlopeBatch(new long[4097], 4097));
long[] failureCoordinates = new long[4096];
for (int index = 0; index < failureCoordinates.length; index++) {
failureCoordinates[index] = RiverFootprint.pack(10_000 + index, -index);
}
assertThrows(ExecutionException.class, () -> pool.submit(() -> sampler.sampleBasisWithoutSlopeBatch(
failureCoordinates, failureCoordinates.length)).get(10, TimeUnit.SECONDS));
assertEquals(4, opened.get());
assertEquals(opened.get(), closed.get());
assertEquals(null, pool.submit(scope::get).get(5, TimeUnit.SECONDS));
} finally {
pool.shutdownNow();
assertTrue(pool.awaitTermination(5, TimeUnit.SECONDS));
}
}
@Test
public void bankHeightsAtSeaLevelSkipOceanIntentSampling() {
AtomicInteger classifierCalls = new AtomicInteger();
@@ -2,6 +2,12 @@ package art.arcane.iris.generation.hydrology.surface;
import art.arcane.iris.generation.hydrology.policy.SurfaceRiverPolicy;
import art.arcane.iris.generation.hydrology.HydraulicChannelProfile;
import art.arcane.iris.generation.hydrology.HydraulicSegment;
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
import art.arcane.iris.generation.hydrology.HydrologySurfaceDropRaster;
import art.arcane.iris.generation.hydrology.RiverCourse;
import art.arcane.iris.generation.hydrology.RiverCourseType;
import art.arcane.iris.generation.hydrology.HydrologyGeometrySampler;
import art.arcane.iris.generation.hydrology.HydrologyCandidateRejection;
import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings;
@@ -13,10 +19,18 @@ import art.arcane.iris.generation.hydrology.IrisRiverBedProfile;
import art.arcane.iris.generation.hydrology.IrisRiverBlendStyle;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HexFormat;
import java.util.HashSet;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.OptionalLong;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.IntBinaryOperator;
@@ -33,6 +47,223 @@ public class ErosionFieldCompilerTest {
default -> request.minimum();
};
@Test
public void wetIncisionPreflightMatchesFullVerdictsAcrossPondsCrossingsStepsAndPolicies() {
int accepted = 0;
int unsupported = 0;
for (int scenario = 0; scenario < 24; scenario++) {
int fixture = scenario;
boolean crossing = scenario % 3 == 1;
boolean coastal = scenario % 6 == 5;
HydrologyPlannerSettings.Ponds ponds = scenario % 4 == 0 ? noPonds()
: new HydrologyPlannerSettings.Ponds(new HydrologyPlannerSettings.Pond(true, 3, 8, 3),
new HydrologyPlannerSettings.Pond(true, 3, 7, 2));
HydrologyPlannerSettings.Erosion erosion = shapedErosion(
IrisRiverBlendStyle.values()[scenario % IrisRiverBlendStyle.values().length], 4, 0.5D,
IrisRiverBedProfile.values()[scenario % IrisRiverBedProfile.values().length], 1D, 2D);
HydrologyPlannerSettings.Surface surface = surfaceWith(scenario % 2 == 0 ? 0D : 0.6D,
0, 1.5D, erosion, ponds, HydrologyPlannerSettings.Channel.defaults(),
HydrologyPlannerSettings.Flow.defaults());
HydrologyTerrainSampler terrain = (x, z) -> {
if (coastal && x > 12) {
return HydrologyTerrainSample.ocean(50, "ocean");
}
int natural = coastal ? SEA_LEVEL : 90;
if (fixture >= 12 && x >= -2 && x <= 2) {
natural += 30;
}
if (fixture % 4 == 2 && z > 4) {
natural += 12;
}
HydrologyTerrainSample sample = HydrologyTerrainSample.openLand(natural, 0D, "land")
.withErosion(fixture % 7 != 0).withShoreWidth(z < 0 ? 0.5D : 3D);
return fixture % 3 == 2 ? sample.withSurfacePolicy(new SurfaceRiverPolicy(
"local", null, null, null, null, null, null, x < 0 ? 3 : 16)) : sample;
};
List<HydrologyPoint> path = crossing
? List.of(new HydrologyPoint(-18, 90, -8), new HydrologyPoint(18, 90, 8),
new HydrologyPoint(-18, 90, 8), new HydrologyPoint(18, 90, -8))
: List.of(new HydrologyPoint(-18, 90, 0), new HydrologyPoint(18, 90, 0));
SurfaceCenterline centerline = SurfaceCenterline.densify(path);
double[] widths = new double[centerline.size()];
double[] depths = new double[centerline.size()];
double[] multipliers = new double[centerline.size()];
int[] heads = new int[centerline.size()];
Arrays.fill(widths, 3D);
Arrays.fill(depths, 3D);
Arrays.fill(multipliers, 1D);
for (int station = 0; station < heads.length; station++) {
heads[station] = coastal ? SEA_LEVEL : 89 - (scenario % 4 == 3 ? station / 8 : 0);
}
ChannelProfile channel = new ChannelProfile(widths, depths, multipliers);
ValleyProfile valley = ValleyProfile.fromHeads(heads, heads.length);
SurfaceTerminal terminal = coastal ? SurfaceTerminal.OCEAN_MOUTH : SurfaceTerminal.SINKHOLE;
HydrologyPlannerSettings settings = rasterSettings(surface);
HydrologySurfaceDropRaster drops = HydrologySurfaceDropRaster.empty();
if (scenario % 4 == 3 && !coastal) {
HydraulicSegment fall = new HydraulicSegment(2L, 1L, HydrologyFeatureType.WATERFALL,
89, 81, 6, 3, true, true,
List.of(new HydrologyPoint(-5, 89, 0), new HydrologyPoint(-4, 81, 0)),
HydraulicChannelProfile.uniform(6, 3));
RiverCourse course = new RiverCourse(1L, RiverCourseType.SURFACE, OptionalLong.of(9L),
OptionalLong.of(8L), "water", 1, List.of(), List.of(fall));
drops = HydrologySurfaceDropRaster.compile(settings, terrain, CONSTANT_GEOMETRY, course);
assertTrue("Drop raster scenario " + scenario, !drops.columns().isEmpty());
}
SurfaceRasterContext context = new SurfaceRasterContext(null, drops,
scenario % 4 == 1 ? new SurfaceRunBoundary(
new SurfaceRunBoundary.Plane(-18, 0, 1, 0, 10D),
new SurfaceRunBoundary.Plane(18, 0, 1, 0, 10D)) : SurfaceRunBoundary.unbounded());
ErosionFieldCompiler compiler = new ErosionFieldCompiler(settings, terrain);
ErosionFieldCompiler.PreparedField prepared = compiler.prepare(centerline, channel, valley,
terminal, coastal ? Math.max(0, centerline.size() - 12) : centerline.size());
for (long seed = 0; seed < 4; seed++) {
ErosionField full = compiler.compile(seed, centerline, channel, valley, terminal, 8,
ponds, context, prepared);
ErosionField publication = compiler.compileForPublication(seed, centerline, channel, valley,
terminal, 8, ponds, context, prepared);
assertEquals("scenario=" + scenario + " seed=" + seed, full.rejection(), publication.rejection());
if (full.rejection() == HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED) {
unsupported++;
for (SurfaceColumn wet : publication.columns().values()) {
if (wet.role() == SurfaceRole.CHANNEL) {
assertEquals(full.column(wet.x(), wet.z()), wet);
}
}
} else {
assertEqualFields(full, publication);
if (full.rejection() == null) {
accepted++;
}
}
}
}
assertTrue("Accepted oracle cases: " + accepted, accepted >= 8);
assertTrue("Unsupported oracle cases: " + unsupported, unsupported >= 8);
}
@Test
public void rasterBatchesPreserveExactDemandAndColumnOrder() {
Map<Long, Integer> serialCalls = new HashMap<>();
Map<Long, Integer> batchCalls = new HashMap<>();
AtomicInteger batches = new AtomicInteger();
HydrologyTerrainSampler serial = (x, z) -> {
serialCalls.merge(RiverFootprint.pack(x, z), 1, Integer::sum);
return HydrologyTerrainSample.openLand(80 + Math.floorMod(z, 7), 0D, "land");
};
HydrologyTerrainSampler batched = new HydrologyTerrainSampler() {
@Override
public HydrologyTerrainSample sample(int x, int z) {
batchCalls.merge(RiverFootprint.pack(x, z), 1, Integer::sum);
return HydrologyTerrainSample.openLand(80 + Math.floorMod(z, 7), 0D, "land");
}
public HydrologyTerrainSample[] sampleBatch(long[] coordinates, int count) {
assertTrue(count > 0 && count <= 4096);
batches.incrementAndGet();
HydrologyTerrainSample[] samples = new HydrologyTerrainSample[count];
for (int index = 0; index < count; index++) {
samples[index] = sample(RiverFootprint.unpackX(coordinates[index]), RiverFootprint.unpackZ(coordinates[index]));
}
return samples;
}
};
ErosionField expected = compile(serial, 300, SurfaceTerminal.SINKHOLE, 40).field();
ErosionField actual = compile(batched, 300, SurfaceTerminal.SINKHOLE, 40).field();
assertEquals(new ArrayList<>(expected.columns().values()), new ArrayList<>(actual.columns().values()));
assertEquals(expected.rejection(), actual.rejection());
assertEquals(expected.rejectionDetail(), actual.rejectionDetail());
assertEquals(expected.uncontainedWetCells(), actual.uncontainedWetCells());
assertEquals(expected.bankExcavation(), actual.bankExcavation());
assertEquals(serialCalls, batchCalls);
assertTrue(batches.get() > 1);
}
@Test
public void pondExclusionsPreserveCompleteAndClippedFieldsWithFewerTerrainSamples() throws Exception {
List<String> fingerprints = new ArrayList<>();
List<Integer> sampleCounts = new ArrayList<>();
for (int scenario = 0; scenario < 8; scenario++) {
double shore = switch (scenario) {
case 0 -> Math.nextDown(1.75D);
case 1 -> 1.75D;
case 2 -> Math.nextUp(1.75D);
default -> 1.5D;
};
boolean enabled = scenario != 4;
boolean erode = scenario != 5;
boolean coastal = scenario == 7;
HydrologyPlannerSettings.Erosion defaults = HydrologyPlannerSettings.Erosion.defaults();
HydrologyPlannerSettings.Erosion erosion = new HydrologyPlannerSettings.Erosion(enabled,
defaults.smoothingRadius(), defaults.thalwegFraction(), defaults.blendCurve(), defaults.bedNoise(),
defaults.style(), defaults.terraceSteps(), defaults.cliffFraction(), defaults.bedProfile(),
defaults.shoreRise(), defaults.blendBaseWidth(),
new HydrologyPlannerSettings.Excavation(16, scenario == 6 ? 5 : 48, 256));
HydrologyPlannerSettings.Ponds ponds = new HydrologyPlannerSettings.Ponds(
new HydrologyPlannerSettings.Pond(true, 6, 6, 3),
new HydrologyPlannerSettings.Pond(true, 5, 5, 2));
HydrologyPlannerSettings.Surface surface = surfaceWith(scenario == 3 ? 0.65D : 0D, 0, shore,
erosion, ponds, HydrologyPlannerSettings.Channel.defaults(), HydrologyPlannerSettings.Flow.defaults());
HydrologyTerrainSampler terrain = (x, z) -> coastal && x >= 4
? HydrologyTerrainSample.ocean(50, "ocean")
: HydrologyTerrainSample.openLand(coastal ? SEA_LEVEL : 80 + Math.max(0, z) / 3, 0D, "land")
.withErosion(erode);
SurfaceCenterline centerline = SurfaceCenterline.densify(List.of(
new HydrologyPoint(-12, 80, 0), new HydrologyPoint(12, 80, 0)));
double[] widths = new double[centerline.size()];
double[] depths = new double[centerline.size()];
double[] multipliers = new double[centerline.size()];
int[] heads = new int[centerline.size()];
Arrays.fill(widths, 3D);
Arrays.fill(depths, 2D);
Arrays.fill(multipliers, 1D);
Arrays.fill(heads, coastal ? SEA_LEVEL : 80);
MessageDigest digest = MessageDigest.getInstance("SHA-256");
AtomicInteger samples = new AtomicInteger();
ErosionField complete = null;
for (SurfaceBounds bounds : new SurfaceBounds[]{null, new SurfaceBounds(-20, -12, -1, 12),
new SurfaceBounds(0, -12, 20, 12)}) {
ErosionField field = new ErosionFieldCompiler(rasterSettings(surface), (x, z) -> {
samples.incrementAndGet();
return terrain.sample(x, z);
}).compile(1234L, centerline, new ChannelProfile(widths, depths, multipliers),
ValleyProfile.fromHeads(heads, coastal ? 17 : heads.length),
coastal ? SurfaceTerminal.OCEAN_MOUTH : SurfaceTerminal.SINKHOLE, 8, ponds,
SurfaceRasterContext.bounded(bounds));
List<SurfaceColumn> ordered = new ArrayList<>(field.columns().values());
ordered.sort(Comparator.comparingLong(column -> RiverFootprint.pack(column.x(), column.z())));
for (SurfaceColumn column : ordered) {
digest.update(column.toString().getBytes(StandardCharsets.UTF_8));
if (bounds != null && bounds.contains(column.x(), column.z())) {
assertEquals(complete.column(column.x(), column.z()), column);
}
}
digest.update((field.rejection() + ":" + field.rejectionDetail() + ":"
+ field.uncontainedWetCells() + ":" + field.bankExcavation()).getBytes(StandardCharsets.UTF_8));
if (bounds == null) {
complete = field;
}
}
fingerprints.add(HexFormat.of().formatHex(digest.digest()));
sampleCounts.add(samples.get());
}
assertEquals(List.of(
"66595850077d44389c5c78594276de4568f85f08854d3333137dfc05ebf3455a",
"66595850077d44389c5c78594276de4568f85f08854d3333137dfc05ebf3455a",
"66595850077d44389c5c78594276de4568f85f08854d3333137dfc05ebf3455a",
"744b15e684ba996f9c730ab1409ada2643985c902a6da1aadb5cc2b4882d640b",
"2e7e4af1e433ccd0a7392f0d2e93bb85ac122088cd2cf8d55107f7dcfb4ec283",
"7beee0b78c37b1287510fa0719375e14e0f0013eb8af81df8e20a3b5628612b7",
"ad35016194318efa42dc862df4e2bb3d9ec398d3bacec79588576d74bfd1723a",
"a2fe6d20c87be6b6ebf1f4503da75ee767e65d5c8e8c43d5c344957f48f3085a"), fingerprints);
int[] previousSamples = {16724, 16724, 16724, 19594, 15164, 15464, 16724, 7367};
int[] maximumSamples = {15500, 15500, 15500, 17000, 3000, 3300, 5000, 7367};
for (int scenario = 0; scenario < previousSamples.length; scenario++) {
assertTrue("scenario " + scenario + " samples " + sampleCounts.get(scenario)
+ "/" + previousSamples[scenario], sampleCounts.get(scenario) <= maximumSamples[scenario]);
}
}
@Test
public void geometricExclusionAvoidsTerrainSamplesWithoutChangingAsymmetricBanks() {
HydrologyPlannerSettings.Surface surface = zeroRoughnessSurface();
@@ -0,0 +1,89 @@
package art.arcane.iris.generation.hydrology.surface;
import org.junit.Test;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
public class SurfaceDistanceTableTest {
@Test
public void latticeResidualsReuseExactDistancesIncludingSignedZero() {
SurfaceDistanceTable table = new SurfaceDistanceTable(32, StrictMath::hypot);
for (int x = -66; x <= 66; x++) {
for (int z = -66; z <= 66; z++) {
equalBits(StrictMath.hypot(x / 2D, z / 2D), table.hypot(x / 2D, z / 2D));
}
}
double[] special = {-0D, 0D, Double.MIN_VALUE, Double.MAX_VALUE, Double.NaN,
Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, 0.25D, 31.75D, 32.5D};
for (double x : special) {
for (double z : special) {
equalBits(StrictMath.hypot(x, z), table.hypot(x, z));
}
}
}
@Test
public void scalarProjectionMatchesAcrossDirectionsAndExtremeCoordinates() {
SurfaceDistanceTable table = new SurfaceDistanceTable(64, StrictMath::hypot);
int[] origins = {Integer.MIN_VALUE, Integer.MIN_VALUE + 64, -30000000, -1, 0,
30000000, Integer.MAX_VALUE - 64, Integer.MAX_VALUE};
for (int origin : origins) {
for (int directionX = -1; directionX <= 1; directionX++) {
for (int directionZ = -1; directionZ <= 1; directionZ++) {
SurfaceCenterline centerline = segment(origin, -origin, origin + directionX, -origin + directionZ);
for (int station = 0; station < 2; station++) {
for (int x = -34; x <= 34; x++) {
for (int z = -34; z <= 34; z++) {
double pointX = origin + x;
double pointZ = -origin + z;
equalBits(centerline.distanceToSegment(station, pointX, pointZ),
centerline.distanceToSegment(station, pointX, pointZ, table));
}
}
}
}
}
}
SurfaceCenterline arbitrary = segment(-1700000000, 1500000000, 1900000000, -1600000000);
for (int x = -80; x <= 80; x++) {
equalBits(arbitrary.distanceToSegment(0, x + 0.125D, x - 0.75D),
arbitrary.distanceToSegment(0, x + 0.125D, x - 0.75D, table));
}
}
@Test
public void repeatedStationSquaresOnlyCalculateTheBoundedTable() {
AtomicInteger calculations = new AtomicInteger();
SurfaceDistanceTable table = new SurfaceDistanceTable(33, (x, z) -> {
calculations.incrementAndGet();
return StrictMath.hypot(x, z);
});
int initialization = calculations.get();
assertEquals(34 * 34 + 33, initialization);
long samples = 0;
for (int station = 0; station < 1000; station++) {
SurfaceCenterline centerline = segment(station, -station, station + 1, -station + 1);
for (int x = -32; x <= 32; x++) {
for (int z = -32; z <= 32; z++) {
centerline.distanceToSegment(0, station + x, -station + z, table);
samples++;
}
}
}
assertEquals(4_225_000L, samples);
assertEquals(initialization, calculations.get());
table.hypot(34D, 0D);
assertEquals(initialization + 1, calculations.get());
}
private static SurfaceCenterline segment(int x, int z, int endX, int endZ) {
return new SurfaceCenterline(new int[]{x, endX}, new int[]{z, endZ},
new double[2], new double[2], new int[2]);
}
private static void equalBits(double expected, double actual) {
assertEquals(Double.doubleToLongBits(expected), Double.doubleToLongBits(actual));
}
}
@@ -37,6 +37,27 @@ public class SurfaceFootprintCompilerTest {
default -> request.minimum();
};
@Test
public void impossibleWetIncisionRejectsBeforeSamplingWideBanks() {
AtomicInteger bankSamples = new AtomicInteger();
HydrologyTerrainSampler sampler = (x, z) -> {
if (Math.abs(z) > 8) {
bankSamples.incrementAndGet();
}
return HydrologyTerrainSample.openLand(x >= 60 && x <= 68 ? 170 : 90, 0D, "land");
};
RiverCourse course = course(List.of(segment(1L, HydrologyFeatureType.SURFACE_POOL,
89, 89, points(0, 200, 89))));
SurfaceFootprint footprint = compiler(sampler).compileForPublication(course);
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, footprint.rejection());
assertTrue("Wide-bank terrain samples: " + bankSamples.get(), bankSamples.get() < 1_000);
SurfaceFootprint complete = compiler(sampler).compile(course);
assertEquals(complete.rejection(), footprint.rejection());
assertTrue(complete.columns().size() > footprint.columns().size());
}
@Test
public void mouthUsesConnectedFloodedLandWithoutChangingTerrainMetadataOrOwningWater() {
int seaLevel = HydrologyPlannerSettings.defaults().seaLevel();
@@ -2,6 +2,7 @@ package art.arcane.iris.generation.image;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.ProceduralTerrainHeightSampler;
import art.arcane.volmlib.util.collection.KList;
import com.google.gson.Gson;
import org.junit.Rule;
@@ -48,6 +49,33 @@ public class IrisImageMapRuntimeTest {
assertEquals(first.hash(), second.hash());
}
@Test
public void heightImagesRetainPixelDetailAfterProceduralLatticeSampling() throws Exception {
File pack = temporaryFolder.newFolder("sampled-height-pack");
writeMap(pack, "terrain", "terrain", grayscale(0, 255, 0, 255), new IrisImageMap()
.setSource("terrain")
.setType(IrisImageMapType.GRAYSCALE_HEIGHT)
.setMinimumHeight(-64D)
.setMaximumHeight(320D)
.setOutOfBounds(IrisImageMapOutOfBounds.CLAMP));
IrisDimension dimension = new IrisDimension();
dimension.getImageMaps().add(new IrisImageMapBinding()
.setKey("terrain")
.setMap("terrain")
.setApplication(IrisImageMapApplication.TERRAIN_HEIGHT));
ProceduralTerrainHeightSampler terrain = new ProceduralTerrainHeightSampler((x, z) -> x + 100D, 4);
IrisData data = IrisData.openDatapackCompiler(pack);
try {
IrisImageMapRuntime runtime = IrisImageMapRuntime.compile(data, dimension, -64);
for (int x = 0; x < 4; x++) {
assertEquals(x % 2 == 0 ? 0D : 384D,
runtime.sampleTerrainHeight(x, 0D, terrain.sample(x, 0D)), EPSILON);
}
} finally {
data.close();
}
}
@Test
public void composesNamedMasksInDeclarationOrder() throws Exception {
File pack = temporaryFolder.newFolder("mask-pack");
@@ -126,6 +126,176 @@ public class IrisCaveCarver3DNearParityTest {
carveForcedAirField.setAccessible(true);
}
@Test
public void verticalDensityPlanesUseGlobalAnchorsAndReduceSamplesAcrossChunkEdges() throws Exception {
Engine engine = createEngine(128, 110);
IrisDimension dimension = engine.getDimension();
doReturn(4).when(dimension).getCaveDensitySamplingStep();
doReturn(IrisWorld.builder().minHeight(-63).maxHeight(65).build()).when(engine).getWorld();
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine,
createProfile(false, false).setDetailWeight(0D).setAllowFluid(false));
AtomicInteger calls = new AtomicInteger();
Set<Integer> sampledY = new HashSet<>();
CNG density = mock(CNG.class);
doAnswer(invocation -> {
calls.incrementAndGet();
double x = invocation.getArgument(0);
double y = invocation.getArgument(1);
double z = invocation.getArgument(2);
sampledY.add((int) y);
return x * 0.0001D + y * 0.01D + z * 0.0002D;
}).when(density).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
baseDensityField.set(carver, density);
Method prepare = IrisCaveCarver3D.class.getDeclaredMethod("prepareVerticalDensityPlane",
CaveCarveScratch.class, int.class, int.class, int.class);
prepare.setAccessible(true);
CaveCarveScratch scratch = new CaveCarveScratch();
for (int y = 0; y < 64; y++) {
prepare.invoke(carver, scratch, -16, 32, y);
for (int x = 0; x < 3; x++) {
for (int z = 0; z < 3; z++) {
assertEquals((-16 + x * 8) * 0.0001D + y * 0.01D + (32 + z * 8) * 0.0002D,
scratch.adaptivePlaneDensity[x * 3 + z], 1.0E-14D);
}
}
}
assertEquals(153, calls.get());
assertFalse(sampledY.contains(1));
int interpolatedCalls = calls.get();
doReturn(1).when(dimension).getCaveDensitySamplingStep();
IrisCaveCarver3D scalar = new IrisCaveCarver3D(engine,
createProfile(false, false).setDetailWeight(0D).setAllowFluid(false));
baseDensityField.set(scalar, density);
CaveCarveScratch scalarScratch = new CaveCarveScratch();
for (int index = 0; index < 256; index++) {
scalarScratch.planeColumnIndices[index] = index;
}
Method classify = IrisCaveCarver3D.class.getDeclaredMethod("classifyDensityPlaneAdaptive",
CaveCarveScratch.class, int.class, int.class, int.class, int[].class, double[].class,
int.class, boolean[].class, int.class, double.class);
classify.setAccessible(true);
calls.set(0);
for (int y = 0; y < 64; y++) {
classify.invoke(scalar, scalarScratch, -16, 32, y, scalarScratch.planeColumnIndices,
scalarScratch.planeThresholdLimit, 256, scalarScratch.planeCarve, 8, 0.04D);
}
assertEquals(576, calls.get());
assertTrue(interpolatedCalls < calls.get() * 0.3D);
assertTrue(sampledY.containsAll(List.of(0, 3, 7, 63)));
CaveCarveScratch neighboring = new CaveCarveScratch();
prepare.invoke(carver, neighboring, 0, 32, 63);
for (int z = 0; z < 3; z++) {
assertEquals(scratch.adaptivePlaneDensity[6 + z], neighboring.adaptivePlaneDensity[z], 0D);
}
prepare.invoke(carver, scratch, 48, -32, 9);
assertEquals(48 * 0.0001D + 0.09D - 32 * 0.0002D, scratch.adaptivePlaneDensity[0], 1.0E-14D);
}
@Test
public void expressionStylesKeepTheOriginalSamplingAndCarvingBehavior() throws Exception {
CNG density = mock(CNG.class);
AtomicInteger calls = new AtomicInteger();
doAnswer(invocation -> {
calls.incrementAndGet();
double y = invocation.getArgument(1);
return Math.sin(y * 0.2D);
}).when(density).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
IrisGeneratorStyle style = mock(IrisGeneratorStyle.class);
doReturn("density_expression").when(style).getExpression();
doReturn(density).when(style).create(any(RNG.class), any(IrisData.class));
IrisCaveProfile profile = createProfile(false, false).setBaseDensityStyle(style)
.setDetailWeight(0D).setAllowFluid(false)
.setDensityThreshold(new IrisStyledRange(0.15D, 0.15D, new IrisGeneratorStyle(NoiseStyle.FLAT)));
WriterCapture expected = null;
int expectedSamples = 0;
for (int step : new int[]{1, 4}) {
Engine engine = createEngine(128, 110);
IrisDimension dimension = engine.getDimension();
doReturn(step).when(dimension).getCaveDensitySamplingStep();
WriterCapture capture = createWriterCapture(128);
calls.set(0);
new IrisCaveCarver3D(engine, profile).carve(capture.writer, 0, 0,
fullWeights(), 0D, 0D, null, filledHeights(110));
assertFalse(capture.carvedCells.isEmpty());
if (expected == null) {
expected = capture;
expectedSamples = calls.get();
} else {
assertEquals(expectedSamples, calls.get());
assertEquals(expected.carvedCells, capture.carvedCells);
assertEquals(expected.carvedLiquids, capture.carvedLiquids);
}
}
}
@Test
public void verticalDensityPreservesTwoBlockModuleTunnelsWithoutBleedingAcrossBands() throws Exception {
for (int step : new int[]{2, 4, 8}) {
Engine engine = createEngine(128, 110);
IrisDimension dimension = engine.getDimension();
doReturn(step).when(dimension).getCaveDensitySamplingStep();
IrisCaveProfile profile = createProfile(false, false).setDetailWeight(0D).setAllowFluid(false)
.setVerticalEdgeFade(0).setVerticalEdgeFadeStrength(0D).setAllowSurfaceBreak(false)
.setDensityThreshold(new IrisStyledRange(0.25D, 0.25D, new IrisGeneratorStyle(NoiseStyle.FLAT)))
.setModules(new KList<>(new IrisCaveFieldModule(new IrisGeneratorStyle(NoiseStyle.FLAT),
1D, 0D, new IrisRange(5D, 6D), false)));
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile);
CNG base = mock(CNG.class);
doReturn(1D).when(base).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
baseDensityField.set(carver, base);
CNG module = mock(CNG.class);
doReturn(-1D).when(module).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
moduleDensityField.set(((CaveFieldModuleState[]) modulesField.get(carver))[0], module);
WriterCapture capture = createWriterCapture(128);
carver.carve(capture.writer, -1, 2, fullWeights(), 0D, 0D, null, filledHeights(110));
assertEquals(512, capture.carvedCells.size());
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
assertTrue(capture.carvedCells.contains(cellKey(x, 5, z)));
assertTrue(capture.carvedCells.contains(cellKey(x, 6, z)));
assertFalse(capture.carvedCells.contains(cellKey(x, 4, z)));
assertFalse(capture.carvedCells.contains(cellKey(x, 7, z)));
}
}
}
}
@Test
public void verticalDensityMatchesParallelDecisionsAndKeepsSurfaceFluidBarriers() throws Exception {
try (ForkJoinPool pool = new ForkJoinPool(4)) {
for (boolean warp : new boolean[]{false, true}) {
Engine engine = createEngine(128, 110);
IrisDimension dimension = engine.getDimension();
doReturn(4).when(dimension).getCaveDensitySamplingStep();
doReturn(IrisWorld.builder().minHeight(-63).maxHeight(65).build()).when(engine).getWorld();
IrisCaveProfile profile = createProfile(warp, true).setFluidRequiresFloor(false)
.setDensityThreshold(new IrisStyledRange(0.15D, 0.15D, new IrisGeneratorStyle(NoiseStyle.FLAT)));
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile);
long[] boundaries = new long[256];
Arrays.fill(boundaries, SurfaceFluidBoundaryPlan.boundary(
SurfaceFluidBoundaryPlan.NO_BOUNDARY, Integer.MIN_VALUE));
boundaries[16] = SurfaceFluidBoundaryPlan.boundary(61, 64);
WriterCapture serial = createWriterCapture(128);
WriterCapture parallel = createWriterCapture(128);
int[] heights = filledHeights(110);
int expected = carver.carve(serial.writer, -17, 19, fullWeights(), 0D, 0D,
null, heights, boundaries, null, new CaveFluidSupportPlan());
int actual = pool.submit(() -> carver.carve(parallel.writer, -17, 19, fullWeights(), 0D, 0D,
null, heights, boundaries, null, new CaveFluidSupportPlan())).get(20, TimeUnit.SECONDS);
assertTrue(expected > 0);
assertEquals(expected, actual);
assertEquals(serial.carvedCells, parallel.carvedCells);
assertEquals(serial.carvedLiquids, parallel.carvedLiquids);
for (int y = 61; y <= 64; y++) {
assertFalse(serial.carvedCells.contains(cellKey(1, y, 0)));
}
assertTrue(countLiquid(serial, (byte) 2) > 0);
assertTrue(countLiquid(serial, (byte) 0) > 0);
assertTrue(maxY(serial.carvedCells) <= 110);
}
}
}
@Test
public void carvingReleasesSectionReferencesAfterSuccessAndFailure() throws Exception {
for (boolean fail : new boolean[]{false, true}) {
@@ -37,6 +37,10 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class MantleWriterPreObjectJournalTest {
@@ -72,7 +76,7 @@ public class MantleWriterPreObjectJournalTest {
when(engineMantle.getComplex()).thenReturn(mock(IrisComplex.class));
Mantle<Matter> mantle = mock(Mantle.class);
MantleChunk<Matter> chunk = mock(MantleChunk.class);
matter = new IrisMatter(16, 16, 16);
matter = spy(new IrisMatter(16, 16, 16));
when(mantle.getWorldHeight()).thenReturn(32);
when(mantle.getChunk(0, 0)).thenReturn(chunk);
when(chunk.use()).thenReturn(chunk);
@@ -90,6 +94,88 @@ public class MantleWriterPreObjectJournalTest {
IrisPlatforms.unbind();
}
@Test
public void concurrentTransactionsKeepOverlaysPrivateDuringObjectPublication() throws Exception {
NativeBlockState original = mock(NativeBlockState.class);
NativeBlockState published = mock(NativeBlockState.class);
NativeBlockState firstOverlay = mock(NativeBlockState.class);
NativeBlockState secondOverlay = mock(NativeBlockState.class);
matter.<NativeBlockState>slice(NativeBlockState.class).set(X, Y, Z, original);
ExecutorService executor = Executors.newFixedThreadPool(2);
CountDownLatch ready = new CountDownLatch(2);
CountDownLatch start = new CountDownLatch(1);
try {
Future<?> first = executor.submit(() -> verifyTransactionReads(original, firstOverlay, ready, start));
Future<?> second = executor.submit(() -> verifyTransactionReads(original, secondOverlay, ready, start));
assertTrue(ready.await(5, TimeUnit.SECONDS));
start.countDown();
writer.withComponentPriority(2, () -> {
for (int pass = 0; pass < 100; pass++) {
writer.setData(X, Y, Z, published);
}
});
first.get(5, TimeUnit.SECONDS);
second.get(5, TimeUnit.SECONDS);
assertSame(published, writer.getDataIfPresent(X, Y, Z, NativeBlockState.class));
assertSame(original, writer.getPrerequisiteBlock(X, Y, Z));
} finally {
start.countDown();
executor.shutdownNow();
}
}
@Test
public void integerPrerequisitesDoNotReadTheObjectJournal() {
matter.<Integer>slice(Integer.class).set(X, Y, Z, 17);
clearInvocations(matter);
assertEquals(Integer.valueOf(17), writer.getPrerequisiteDataIfPresent(X, Y, Z, Integer.class));
verify(matter, never()).getSlice(PreObjectMatterCell.class);
}
@Test
public void hydrologyPrerequisitesHonorCapturedOriginalsAndNulls() {
HydrologyCaveCell original = HydrologyCaveCell.of(HydrologyCaveAction.DRY_AIR);
HydrologyCaveCell live = HydrologyCaveCell.of(HydrologyCaveAction.SEAL_GUARD);
matter.<HydrologyCaveCell>slice(HydrologyCaveCell.class).set(X, Y, Z, live);
matter.<HydrologyCaveCell>slice(HydrologyCaveCell.class).set(X, Y + 1, Z, live);
matter.<PreObjectMatterCell>slice(PreObjectMatterCell.class)
.set(X, Y, Z, PreObjectMatterCell.hydrology(original));
matter.<PreObjectMatterCell>slice(PreObjectMatterCell.class)
.set(X, Y + 1, Z, PreObjectMatterCell.hydrology(null));
assertSame(original, writer.getPrerequisiteDataIfPresent(X, Y, Z, HydrologyCaveCell.class));
assertNull(writer.getPrerequisiteDataIfPresent(X, Y + 1, Z, HydrologyCaveCell.class));
assertSame(live, writer.getDataIfPresent(X, Y, Z, HydrologyCaveCell.class));
assertSame(live, writer.getDataIfPresent(X, Y + 1, Z, HydrologyCaveCell.class));
}
@Test
public void carvedColumnsHonorCapturedHydrologyBeforeLiveCells() {
HydrologyCaveCell original = HydrologyCaveCell.of(HydrologyCaveAction.DRY_AIR);
HydrologyCaveCell live = HydrologyCaveCell.of(HydrologyCaveAction.SEAL_GUARD);
MatterSlice<HydrologyCaveCell> hydrology = matter.slice(HydrologyCaveCell.class);
MatterSlice<PreObjectMatterCell> journal = matter.slice(PreObjectMatterCell.class);
hydrology.set(X, Y, Z, live);
hydrology.set(X, Y + 1, Z, live);
hydrology.set(X, Y + 2, Z, original);
journal.set(X, Y, Z, PreObjectMatterCell.hydrology(original));
journal.set(X, Y + 1, Z, PreObjectMatterCell.hydrology(null));
journal.set(X, Y + 2, Z, PreObjectMatterCell.hydrology(live));
matter.<MatterCavern>slice(MatterCavern.class)
.set(X, Y + 1, Z, new MatterCavern(true, "cave", (byte) 0));
byte[] carved = writer.getPrerequisiteCarvedColumn(X, Z, 32);
assertEquals(1, carved[Y]);
assertEquals(1, carved[Y + 1]);
assertEquals(0, carved[Y + 2]);
for (int y = 0; y < carved.length; y++) {
assertEquals(writer.isPrerequisiteCarved(X, y, Z), carved[y] != 0);
}
}
@Test
public void firstMutationCapturesEachSupportedOriginalIndependently() {
NativeBlockState originalBlock = mock(NativeBlockState.class);
@@ -207,6 +293,23 @@ public class MantleWriterPreObjectJournalTest {
assertFalse(writer.restorePrerequisiteCell(X + 1, Y, Z));
}
private void verifyTransactionReads(
NativeBlockState original,
NativeBlockState overlay,
CountDownLatch ready,
CountDownLatch start
) {
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, 0, 0);
ready.countDown();
await(start);
for (int pass = 0; pass < 100; pass++) {
assertSame(original, transaction.get(X, Y, Z));
}
transaction.setData(X, Y, Z, overlay);
assertSame(overlay, transaction.get(X, Y, Z));
assertSame(original, writer.getPrerequisiteBlock(X, Y, Z));
}
private void await(CountDownLatch latch) {
try {
assertTrue(latch.await(5, TimeUnit.SECONDS));
@@ -1,6 +1,7 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.integration.Identifier;
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.world.history.BoundaryColumnGeometry;
@@ -27,6 +28,9 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
@@ -47,6 +51,219 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ObjectDestinationTransactionTest {
@Test
public void commonAndGenericMetadataSurviveBlockReplacementAndSourceReplay() {
MantleWriter writer = writer();
NativeBlockState custom = mock(NativeBlockState.class);
NativeBlockState base = mock(NativeBlockState.class);
NativeBlockState replacement = mock(NativeBlockState.class);
Identifier identifier = Identifier.fromString("iris:tree_block");
when(custom.isCustom()).thenReturn(true);
when(custom.deferredPlacementKey()).thenReturn(identifier.toString());
when(custom.placementBaseState()).thenReturn(base);
TreeBlockMaterial firstMaterial = new TreeBlockMaterial("minecraft:oak_log");
TreeBlockMaterial secondMaterial = new TreeBlockMaterial("minecraft:birch_log");
Marker generic = new Marker("generic");
ObjectDestinationTransaction source = new ObjectDestinationTransaction(writer, 0, 0);
int[][] positions = {{-17, 31}, {Integer.MIN_VALUE, Integer.MAX_VALUE}};
for (int[] position : positions) {
int x = position[0];
int z = position[1];
source.setData(x, 7, z, "first");
source.setData(x, 7, z, firstMaterial);
source.setData(x, 7, z, generic);
source.set(x, 7, z, custom);
source.setData(x, 7, z, "second");
source.setData(x, 7, z, secondMaterial);
source.setData(x, 7, z, Integer.valueOf(42));
source.setData(x, 7, z, replacement);
source.setData(x, 7, z, null);
assertNull(source.getDataIfPresent(x, 7, z, Identifier.class));
}
ObjectSourcePlan plan = source.sourcePlanSince(0);
assertEquals(16, source.mutationCheckpoint());
assertEquals(35, plan.mutationWeight());
for (int[] position : positions) {
int x = position[0];
int z = position[1];
ObjectDestinationTransaction destination = new ObjectDestinationTransaction(writer, x >> 4, z >> 4);
destination.apply(plan);
List<ObjectDestinationTransaction.Mutation> sourceMutations = plan.mutationsFor(x >> 4, z >> 4);
List<ObjectDestinationTransaction.Mutation> replayed = destination.sourcePlanSince(0).mutationsFor(x >> 4, z >> 4);
assertEquals(sourceMutations.size(), replayed.size());
for (int index = 0; index < sourceMutations.size(); index++) {
assertSame(sourceMutations.get(index), replayed.get(index));
}
assertEquals(8, destination.mutationCheckpoint());
assertSame(replacement, destination.get(x, 7, z));
assertNull(destination.getDataIfPresent(x, 7, z, Identifier.class));
assertEquals("second", destination.getDataIfPresent(x, 7, z, String.class));
assertSame(secondMaterial, destination.getDataIfPresent(x, 7, z, TreeBlockMaterial.class));
assertSame(generic, destination.getDataIfPresent(x, 7, z, Marker.class));
assertEquals(Integer.valueOf(42), destination.getDataIfPresent(x, 7, z, Integer.class));
destination.commit();
InOrder order = inOrder(writer);
order.verify(writer).setData(x, 7, z, "first");
order.verify(writer).setData(x, 7, z, firstMaterial);
order.verify(writer).setData(x, 7, z, generic);
order.verify(writer).set(x, 7, z, custom);
order.verify(writer).setData(x, 7, z, "second");
order.verify(writer).setData(x, 7, z, secondMaterial);
order.verify(writer).setData(x, 7, z, Integer.valueOf(42));
order.verify(writer).setData(x, 7, z, replacement);
}
}
@Test
public void replayRechecksDestinationProtectionAndHeightBeforeRetainingMutations() {
MantleWriter sourceWriter = writer();
when(sourceWriter.getMantle().getWorldHeight()).thenReturn(128);
NativeBlockState block = mock(NativeBlockState.class);
NativeBlockState custom = mock(NativeBlockState.class);
when(custom.isCustom()).thenReturn(true);
when(custom.deferredPlacementKey()).thenReturn("iris:tree_block");
when(custom.placementBaseState()).thenReturn(block);
MatterCavern cavern = new MatterCavern(true, "", (byte) 0);
TreeBlockMaterial material = new TreeBlockMaterial("minecraft:oak_log");
ObjectDestinationTransaction source = new ObjectDestinationTransaction(sourceWriter, 0, 0);
source.setData(0, 4, 0, block);
source.setData(1, 4, 0, cavern);
source.set(2, 4, 0, custom);
source.setData(3, 0, 0, block);
source.setData(4, 80, 0, block);
source.setData(0, 4, 0, "tree");
source.setData(0, 4, 0, material);
ObjectSourcePlan plan = source.sourcePlanSince(0);
assertEquals(7, plan.mutationsFor(0, 0).size());
MantleWriter destinationWriter = writer();
when(destinationWriter.getEngine().getDimension().isBedrock()).thenReturn(true);
for (int x = 0; x < 3; x++) {
when(destinationWriter.getPrerequisiteDataIfPresent(x, 4, 0, HydrologyCaveCell.class))
.thenReturn(HydrologyCaveCell.of(HydrologyCaveAction.SEAL_GUARD));
}
ObjectDestinationTransaction destination = new ObjectDestinationTransaction(destinationWriter, 0, 0);
destination.apply(plan);
List<ObjectDestinationTransaction.Mutation> replayed = destination.sourcePlanSince(0).mutationsFor(0, 0);
assertEquals(2, replayed.size());
assertSame(plan.mutationsFor(0, 0).get(5), replayed.get(0));
assertSame(plan.mutationsFor(0, 0).get(6), replayed.get(1));
assertNull(destination.getDataIfPresent(2, 4, 0, Identifier.class));
assertNull(destination.getDataIfPresent(1, 4, 0, MatterCavern.class));
assertEquals("tree", destination.getDataIfPresent(0, 4, 0, String.class));
assertSame(material, destination.getDataIfPresent(0, 4, 0, TreeBlockMaterial.class));
destination.commit();
verify(destinationWriter, never()).setData(anyInt(), anyInt(), anyInt(), any(NativeBlockState.class));
verify(destinationWriter, never()).set(anyInt(), anyInt(), anyInt(), any(NativeBlockState.class));
verify(destinationWriter, never()).setData(anyInt(), anyInt(), anyInt(), any(MatterCavern.class));
}
@Test
public void failedCommonMetadataPublicationRestoresOriginalValuesAndMissingTypes() {
MantleWriter writer = writer();
TreeBlockMaterial originalMaterial = new TreeBlockMaterial("minecraft:oak_log");
TreeBlockMaterial replacementMaterial = new TreeBlockMaterial("minecraft:birch_log");
Marker generic = new Marker("generic");
Marker failure = new Marker("failure");
when(writer.getPrerequisiteDataIfPresent(0, 4, 0, String.class)).thenReturn("original");
when(writer.getPrerequisiteDataIfPresent(0, 4, 0, TreeBlockMaterial.class)).thenReturn(originalMaterial);
doThrow(new IllegalStateException("publication failed"))
.when(writer).setData(1, 4, 0, failure);
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, 0, 0);
assertEquals("original", transaction.getDataIfPresent(0, 4, 0, String.class));
assertSame(originalMaterial, transaction.getDataIfPresent(0, 4, 0, TreeBlockMaterial.class));
transaction.setData(0, 4, 0, "replacement");
transaction.setData(0, 4, 0, replacementMaterial);
transaction.setData(0, 4, 0, generic);
transaction.setData(1, 4, 0, failure);
assertThrows(IllegalStateException.class, transaction::commit);
verify(writer).clearData(0, 4, 0, String.class);
verify(writer).setData(0, 4, 0, "original");
verify(writer).clearData(0, 4, 0, TreeBlockMaterial.class);
verify(writer).setData(0, 4, 0, originalMaterial);
verify(writer).clearData(0, 4, 0, Marker.class);
verify(writer).setData(0, 4, 0, generic);
}
@Test
public void sparseOverlayMatchesScalarAndColumnReadsAcrossSignedCoordinates() {
MantleWriter writer = writer();
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, 0, 0);
int[][] columns = {{0, 0}, {-1, -1}, {1, -1}, {-1, 1},
{Integer.MIN_VALUE, Integer.MAX_VALUE}, {Integer.MAX_VALUE, Integer.MIN_VALUE},
{0, 1 << 26}, {1 << 26, 0}};
NativeBlockState solid = mock(NativeBlockState.class);
NativeBlockState air = mock(NativeBlockState.class);
NativeBlockState fluid = mock(NativeBlockState.class);
when(air.isAir()).thenReturn(true);
when(fluid.isFluid()).thenReturn(true);
MatterCavern cavern = new MatterCavern(true, "", (byte) 0);
Object[] values = {solid, air, fluid, cavern,
HydrologyCaveCell.of(HydrologyCaveAction.SEAL_GUARD),
HydrologyCaveCell.of(HydrologyCaveAction.DRY_AIR)};
Map<ObjectDestinationTransaction.DataKey, Object> expected = new HashMap<>();
Random random = new Random(918241L);
for (int pass = 0; pass < 12; pass++) {
for (int write = 0; write < 80; write++) {
int[] column = columns[random.nextInt(columns.length)];
int y = random.nextInt(64);
Object value = values[random.nextInt(values.length)];
transaction.setData(column[0], y, column[1], value);
HydrologyCaveCell hydrology = (HydrologyCaveCell) expected.get(
new ObjectDestinationTransaction.DataKey(column[0], y, column[1], HydrologyCaveCell.class));
if (!(value instanceof HydrologyCaveCell)
&& hydrology != null && hydrology.protectsPlacement()) {
continue;
}
Class<?> type = value instanceof NativeBlockState ? NativeBlockState.class : value.getClass();
expected.put(new ObjectDestinationTransaction.DataKey(column[0], y, column[1], type), value);
}
for (int[] column : columns) {
byte[] carved = new byte[64];
for (int y = 0; y < 64; y++) {
NativeBlockState block = (NativeBlockState) expected.get(
new ObjectDestinationTransaction.DataKey(column[0], y, column[1], NativeBlockState.class));
HydrologyCaveCell hydrology = (HydrologyCaveCell) expected.get(
new ObjectDestinationTransaction.DataKey(column[0], y, column[1], HydrologyCaveCell.class));
MatterCavern expectedCavern = (MatterCavern) expected.get(
new ObjectDestinationTransaction.DataKey(column[0], y, column[1], MatterCavern.class));
boolean isCarved = block != solid && (hydrology == null
? expectedCavern != null : hydrology.carves());
carved[y] = isCarved ? (byte) 1 : 0;
assertSame(block, transaction.get(column[0], y, column[1]));
assertSame(hydrology, transaction.getDataIfPresent(column[0], y, column[1], HydrologyCaveCell.class));
assertSame(expectedCavern, transaction.getDataIfPresent(column[0], y, column[1], MatterCavern.class));
assertEquals(isCarved, transaction.isCarved(column[0], y, column[1]));
}
assertArrayEquals(carved, transaction.getCarvedColumn(column[0], column[1], 64));
}
}
}
@Test
public void overlayReadsLeavePrerequisiteUpdatesVisibleAndReturnedColumnsIndependent() {
MantleWriter writer = writer();
NativeBlockState first = mock(NativeBlockState.class);
NativeBlockState second = mock(NativeBlockState.class);
when(writer.getPrerequisiteBlock(-1, 5, -1)).thenReturn(first, second);
byte[] prerequisite = new byte[64];
prerequisite[5] = 1;
when(writer.getPrerequisiteCarvedColumn(-1, -1, 64)).thenReturn(prerequisite);
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, -1, -1);
assertSame(first, transaction.get(-1, 5, -1));
assertSame(second, transaction.get(-1, 5, -1));
byte[] column = transaction.getCarvedColumn(-1, -1, 64);
column[5] = 0;
assertEquals(1, transaction.getCarvedColumn(-1, -1, 64)[5]);
transaction.setData(-1, 5, -1, second);
assertEquals(0, transaction.getCarvedColumn(-1, -1, 64)[5]);
assertEquals(1, prerequisite[5]);
}
@Test
public void overlayReadsEarlierWritesAndCommitsOnlyDestinationChunk() {
MantleWriter writer = writer();
@@ -122,6 +339,46 @@ public class ObjectDestinationTransactionTest {
verify(writer, never()).setData(-1, 4, 16, foreign);
}
@Test
public void indexedSourceReplayPreservesCustomIdentityAndWriteOrderAtNegativeChunks() {
MantleWriter writer = writer();
NativeBlockState custom = mock(NativeBlockState.class);
NativeBlockState base = mock(NativeBlockState.class);
NativeBlockState replacement = mock(NativeBlockState.class);
Identifier identifier = Identifier.fromString("iris:custom_block");
when(custom.isCustom()).thenReturn(true);
when(custom.deferredPlacementKey()).thenReturn(identifier.toString());
when(custom.placementBaseState()).thenReturn(base);
Marker first = new Marker("first");
Marker second = new Marker("second");
Marker foreign = new Marker("foreign");
ObjectDestinationTransaction source = new ObjectDestinationTransaction(writer, 0, 0);
source.setData(-17, 4, 31, first);
source.set(-17, 4, 31, custom);
source.setData(-16, 4, 31, foreign);
source.setData(-17, 4, 31, replacement);
source.setData(-17, 4, 32, foreign);
source.set(-17, 4, 31, custom);
source.setData(-17, 4, 31, second);
ObjectDestinationTransaction destination = new ObjectDestinationTransaction(writer, -2, 1);
destination.apply(source.sourcePlanSince(0));
assertEquals(5, destination.mutationCheckpoint());
assertSame(base, destination.get(-17, 4, 31));
assertEquals(identifier, destination.getDataIfPresent(-17, 4, 31, Identifier.class));
assertSame(second, destination.getDataIfPresent(-17, 4, 31, Marker.class));
destination.commit();
InOrder order = inOrder(writer);
order.verify(writer).setData(-17, 4, 31, first);
order.verify(writer).set(-17, 4, 31, custom);
order.verify(writer).setData(-17, 4, 31, replacement);
order.verify(writer).set(-17, 4, 31, custom);
order.verify(writer).setData(-17, 4, 31, second);
verify(writer, never()).setData(-16, 4, 31, foreign);
verify(writer, never()).setData(-17, 4, 32, foreign);
}
@Test
public void sourcePlanRejectsAnInvalidCheckpoint() {
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer(), 0, 0);
@@ -340,8 +597,13 @@ public class ObjectDestinationTransactionTest {
transaction.setData(0, 4, 0, block);
assertNull(transaction.getDataIfPresent(0, 4, 0, Identifier.class));
transaction.setData(1, 4, 0, failure);
ObjectSourcePlan plan = transaction.sourcePlanSince(0);
ObjectDestinationTransaction destination = new ObjectDestinationTransaction(writer, 0, 0);
destination.apply(plan);
assertNull(destination.getDataIfPresent(0, 4, 0, Identifier.class));
assertSame(plan.mutationsFor(0, 0).getFirst(), destination.sourcePlanSince(0).mutationsFor(0, 0).getFirst());
assertThrows(IllegalStateException.class, transaction::commit);
assertThrows(IllegalStateException.class, destination::commit);
verify(writer).clearData(0, 4, 0, Identifier.class);
verify(writer).setData(0, 4, 0, originalIdentifier);
}
@@ -1,22 +1,91 @@
package art.arcane.iris.generation.mantle;
import art.arcane.volmlib.util.cache.CacheKey;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class ObjectSourcePlanCacheTest {
@Test
public void defaultCapacityRetainsNeighboringSourceWorkingSetWithoutRebuilding() {
ObjectSourcePlanCache cache = new ObjectSourcePlanCache();
AtomicInteger builds = new AtomicInteger();
ObjectDestinationTransaction.DataKey key = new ObjectDestinationTransaction.DataKey(
0, 4, 0, String.class);
ObjectDestinationTransaction.Mutation mutation = new ObjectDestinationTransaction.SetMutation(key, "marker");
ObjectSourcePlan expected = new ObjectSourcePlan(Collections.nCopies(2048, mutation));
assertTrue(expected.mutationWeight() * 1024L > 1_048_576L);
for (int sweep = 0; sweep < 3; sweep++) {
for (int x = -16; x < 16; x++) {
for (int z = -16; z < 16; z++) {
assertSame(expected, cache.get(x, z, () -> {
builds.incrementAndGet();
return expected;
}));
}
}
assertEquals(1024L, cache.estimatedSize());
assertEquals(1024, builds.get());
}
cache.clear();
assertEquals(0L, cache.estimatedSize());
assertSame(expected, cache.get(-16, -16, () -> {
builds.incrementAndGet();
return expected;
}));
assertEquals(1025, builds.get());
}
@Test
public void unrelatedCoordinatesBuildWhileAnotherPlanIsPending() throws Exception {
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(64L);
ObjectSourcePlan first = planAt(1);
ObjectSourcePlan second = planAt(2);
CountDownLatch firstEntered = new CountDownLatch(1);
CountDownLatch releaseFirst = new CountDownLatch(1);
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
assertEquals(Long.hashCode(CacheKey.mix(CacheKey.key(32113, 37))),
Long.hashCode(CacheKey.mix(CacheKey.key(32667, 37))));
Future<ObjectSourcePlan> firstResult = executor.submit(() -> cache.get(32113, 37, () -> {
firstEntered.countDown();
try {
assertTrue(releaseFirst.await(10L, TimeUnit.SECONDS));
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IllegalStateException(failure);
}
return first;
}));
assertTrue(firstEntered.await(5L, TimeUnit.SECONDS));
Future<ObjectSourcePlan> secondResult = executor.submit(() -> cache.get(32667, 37, () -> second));
assertSame(second, secondResult.get(3L, TimeUnit.SECONDS));
releaseFirst.countDown();
assertSame(first, firstResult.get(5L, TimeUnit.SECONDS));
assertSame(first, cache.get(32113, 37, () -> second));
assertSame(second, cache.get(32667, 37, () -> first));
} finally {
releaseFirst.countDown();
executor.shutdownNow();
}
}
@Test
public void concurrentRequestsBuildOneSourcePlan() throws Exception {
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(64L);
@@ -51,9 +120,9 @@ public class ObjectSourcePlanCacheTest {
@Test
public void cacheEvictsByMutationWeightAndCanBeCleared() {
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(3L);
ObjectSourcePlan first = planAt(0);
ObjectSourcePlan second = planAt(1);
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(first.mutationWeight());
cache.get(0, 0, () -> first);
cache.get(1, 0, () -> second);
@@ -68,6 +137,83 @@ public class ObjectSourcePlanCacheTest {
assertThrows(IllegalArgumentException.class, () -> new ObjectSourcePlanCache(0L));
}
@Test
public void clearingPendingWorkKeepsNewGenerationIsolated() throws Exception {
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(64L);
ObjectSourcePlan old = planAt(1);
ObjectSourcePlan replacement = planAt(2);
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
try (ExecutorService executor = Executors.newSingleThreadExecutor()) {
Future<ObjectSourcePlan> first = executor.submit(() -> cache.get(7, 8, () -> {
entered.countDown();
await(release);
return old;
}));
try {
assertTrue(entered.await(5L, TimeUnit.SECONDS));
cache.clear();
assertSame(replacement, cache.get(7, 8, () -> replacement));
} finally {
release.countDown();
}
assertSame(old, first.get(5L, TimeUnit.SECONDS));
assertSame(replacement, cache.get(7, 8, () -> old));
}
}
@Test
public void waitingForkJoinWorkerAllowsQueuedDependencyToRun() throws Exception {
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(64L);
ObjectSourcePlan expected = planAt(1);
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch waiting = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
try (ExecutorService owner = Executors.newSingleThreadExecutor();
ForkJoinPool pool = new ForkJoinPool(1)) {
Future<ObjectSourcePlan> first = owner.submit(() -> cache.get(7, 8, () -> {
entered.countDown();
await(release);
return expected;
}));
try {
assertTrue(entered.await(5L, TimeUnit.SECONDS));
Future<ObjectSourcePlan> second = pool.submit(() -> {
waiting.countDown();
return cache.get(7, 8, () -> { throw new AssertionError("Duplicate build"); });
});
assertTrue(waiting.await(5L, TimeUnit.SECONDS));
pool.submit(release::countDown).get(5L, TimeUnit.SECONDS);
assertSame(expected, first.get(5L, TimeUnit.SECONDS));
assertSame(expected, second.get(5L, TimeUnit.SECONDS));
} finally {
release.countDown();
}
}
}
@Test
public void failedAndNullBuildsAreRetryableAndRecursionFailsImmediately() {
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(64L);
IllegalArgumentException failure = new IllegalArgumentException("Build failed");
assertSame(failure, assertThrows(IllegalArgumentException.class,
() -> cache.get(7, 8, () -> { throw failure; })));
assertNull(cache.get(7, 8, () -> null));
assertThrows(IllegalStateException.class, () -> cache.get(7, 8,
() -> cache.get(7, 8, () -> planAt(1))));
ObjectSourcePlan expected = planAt(1);
assertSame(expected, cache.get(7, 8, () -> expected));
}
private static void await(CountDownLatch latch) {
try {
assertTrue(latch.await(10L, TimeUnit.SECONDS));
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IllegalStateException(failure);
}
}
private static ObjectSourcePlan planAt(int x) {
ObjectDestinationTransaction.DataKey key = new ObjectDestinationTransaction.DataKey(
x,
@@ -0,0 +1,57 @@
package art.arcane.iris.generation.mantle;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class ObjectSourcePlanTest {
@Test
public void destinationLookupRetainsImmutableIndexedOrder() {
List<ObjectDestinationTransaction.Mutation> mutations = new ArrayList<>();
int[][] positions = {{-1, -1}, {0, 0}, {-16, -16}, {-17, -1},
{Integer.MIN_VALUE, Integer.MAX_VALUE}, {Integer.MAX_VALUE, Integer.MIN_VALUE}};
for (int pass = 0; pass < 3; pass++) {
for (int[] position : positions) {
mutations.add(mutationAt(position[0], position[1]));
}
}
ObjectSourcePlan plan = new ObjectSourcePlan(mutations);
List<ObjectDestinationTransaction.Mutation> expected = List.of(
mutations.get(0), mutations.get(2), mutations.get(6), mutations.get(8), mutations.get(12), mutations.get(14));
mutations.clear();
List<ObjectDestinationTransaction.Mutation> local = plan.mutationsFor(-1, -1);
assertEquals(expected, local);
assertSame(local, plan.mutationsFor(-1, -1));
assertEquals(3, plan.mutationsFor(Integer.MIN_VALUE >> 4, Integer.MAX_VALUE >> 4).size());
assertEquals(3, plan.mutationsFor(Integer.MAX_VALUE >> 4, Integer.MIN_VALUE >> 4).size());
assertTrue(plan.mutationsFor(1, -1).isEmpty());
assertTrue(plan.mutationsFor(-1, 1).isEmpty());
assertThrows(UnsupportedOperationException.class, () -> local.clear());
}
@Test
public void cacheWeightIncludesEachDestinationIndex() {
ObjectDestinationTransaction.Mutation first = mutationAt(0, 0);
ObjectDestinationTransaction.Mutation nearby = mutationAt(1, 0);
ObjectDestinationTransaction.Mutation remote = mutationAt(16, 0);
ObjectSourcePlan compact = new ObjectSourcePlan(List.of(first, nearby));
ObjectSourcePlan scattered = new ObjectSourcePlan(List.of(first, remote));
assertTrue(compact.mutationWeight() > 1 + first.weight() + nearby.weight());
assertTrue(scattered.mutationWeight() > compact.mutationWeight());
assertThrows(NullPointerException.class, () -> new ObjectSourcePlan(Arrays.asList(first, null)));
}
private static ObjectDestinationTransaction.Mutation mutationAt(int x, int z) {
return new ObjectDestinationTransaction.SetMutation(
new ObjectDestinationTransaction.DataKey(x, 4, z, String.class), "marker");
}
}
@@ -34,6 +34,11 @@ import java.nio.file.Path;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
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.AtomicBoolean;
import static org.junit.Assert.assertFalse;
@@ -97,6 +102,128 @@ public class EngineShutdownDrainTest {
}
}
@Test
public void closingRuntimeAllowsItsBackgroundWorkersToOpenCapturedScopes() throws Exception {
ShutdownFixture fixture = new ShutdownFixture();
doCallRealMethod().when(fixture.engine).openGenerationRuntimeScope(any());
IrisEngine.GenerationRuntimeBinding binding = new IrisEngine.GenerationRuntimeBinding(
fixture.engine, fixture.generation);
ExecutorService worker = Executors.newSingleThreadExecutor();
doAnswer(invocation -> {
Future<?> continuation = worker.submit(() -> {
try (IrisEngine.GenerationRuntimeScope ignored = fixture.engine.openGenerationRuntimeScope(binding)) {
assertSame(binding, fixture.engine.generationRuntimeScopes.current());
assertSame(fixture.runtime, fixture.engine.runtime);
assertTrue(fixture.engine.getClosing().get());
assertThrows(IllegalStateException.class, () -> fixture.engine.requireRunning("start generation"));
}
});
continuation.get(5, TimeUnit.SECONDS);
return null;
}).when(fixture.complex).close();
try (MockedStatic<NativeStructureOwnershipStore> ownership = mockStatic(NativeStructureOwnershipStore.class)) {
fixture.shutdown.close();
assertTrue(fixture.engine.closed);
assertThrows(IllegalStateException.class, () -> fixture.engine.openGenerationRuntimeScope(binding));
} finally {
worker.shutdownNow();
assertTrue(worker.awaitTermination(5, TimeUnit.SECONDS));
}
}
@Test
public void concurrentCloseWaitsForTheRunningShutdown() throws Exception {
ShutdownFixture fixture = new ShutdownFixture();
ExecutorService closer = Executors.newSingleThreadExecutor();
CompletableFuture<Future<?>> concurrentClose = new CompletableFuture<>();
CountDownLatch entered = new CountDownLatch(1);
doAnswer(invocation -> {
concurrentClose.complete(closer.submit(() -> {
entered.countDown();
fixture.shutdown.close();
}));
assertTrue(entered.await(5, TimeUnit.SECONDS));
return null;
}).when(fixture.complex).close();
try (MockedStatic<NativeStructureOwnershipStore> ownership = mockStatic(NativeStructureOwnershipStore.class)) {
fixture.shutdown.close();
concurrentClose.get(5, TimeUnit.SECONDS).get(5, TimeUnit.SECONDS);
verify(fixture.complex).close();
verify(fixture.mantle).close();
assertTrue(fixture.engine.closed);
} finally {
closer.shutdownNow();
assertTrue(closer.awaitTermination(5, TimeUnit.SECONDS));
}
}
@Test
public void shutdownWaitsForConcurrentDetachedRetirementWithoutClosingItTwice() throws Exception {
ShutdownFixture fixture = new ShutdownFixture();
GenerationRuntime detached = generation(target(), mock(EngineMantle.class));
IrisComplex detachedComplex = detached.complex();
fixture.engine.detachedGenerationRuntimes.add(detached);
CountDownLatch retirementStarted = new CountDownLatch(1);
CountDownLatch allowRetirement = new CountDownLatch(1);
CountDownLatch backgroundDrained = new CountDownLatch(1);
doAnswer(invocation -> {
retirementStarted.countDown();
assertTrue(allowRetirement.await(5, TimeUnit.SECONDS));
return null;
}).when(detachedComplex).close();
when(fixture.engine.backgroundTasks.drainBackgroundTasks("close")).thenAnswer(invocation -> {
backgroundDrained.countDown();
return new EngineBackgroundTasks.BackgroundTaskDrain(null, true);
});
ExecutorService workers = Executors.newFixedThreadPool(2);
try {
Future<?> retiring = workers.submit(() -> fixture.engine.closeDetachedGenerationRuntime(
new IrisEngine.GenerationRuntimeBinding(fixture.engine, detached)));
assertTrue(retirementStarted.await(5, TimeUnit.SECONDS));
Future<?> shutdown = workers.submit(() -> {
try (MockedStatic<NativeStructureOwnershipStore> ownership = mockStatic(NativeStructureOwnershipStore.class)) {
fixture.shutdown.close();
}
});
assertTrue(backgroundDrained.await(5, TimeUnit.SECONDS));
assertFalse(shutdown.isDone());
verify(fixture.target, never()).close();
allowRetirement.countDown();
retiring.get(5, TimeUnit.SECONDS);
shutdown.get(5, TimeUnit.SECONDS);
verify(detached.complex()).close();
verify(detached.target()).close();
assertTrue(fixture.engine.closed);
} finally {
allowRetirement.countDown();
workers.shutdownNow();
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
}
}
@Test
public void failedDetachedRetirementRetainsOwnershipForShutdownRetry() throws Exception {
ShutdownFixture fixture = new ShutdownFixture();
GenerationRuntime detached = generation(target(), mock(EngineMantle.class));
IrisComplex detachedComplex = detached.complex();
fixture.engine.detachedGenerationRuntimes.add(detached);
doThrow(new IllegalStateException("Background work is still active")).doNothing()
.when(detachedComplex).close();
assertThrows(IllegalStateException.class, () -> fixture.engine.closeDetachedGenerationRuntime(
new IrisEngine.GenerationRuntimeBinding(fixture.engine, detached)));
assertTrue(fixture.engine.detachedGenerationRuntimes.contains(detached));
verify(detached.target(), never()).close();
try (MockedStatic<NativeStructureOwnershipStore> ownership = mockStatic(NativeStructureOwnershipStore.class)) {
fixture.shutdown.close();
}
verify(detached.complex(), times(2)).close();
verify(detached.target()).close();
assertTrue(fixture.engine.closed);
}
@Test
public void settledStructureBootstrapFailureDoesNotPreventEngineCleanup() throws Exception {
ShutdownFixture fixture = new ShutdownFixture();
@@ -726,14 +853,21 @@ public class EngineShutdownDrainTest {
setField(engine, "lifecycleLock", new Object());
setField(engine, "runtimeAssembly", new ThreadLocal<RuntimeAssembly>());
setField(engine, "detachedGenerationRuntimes", ConcurrentHashMap.newKeySet());
when(engine.getClosing()).thenReturn(new AtomicBoolean());
setField(engine, "retiringGenerationRuntimes", ConcurrentHashMap.newKeySet());
setField(engine, "generationRuntimeScopes", new GenerationRuntimeScopeState());
AtomicBoolean closing = new AtomicBoolean();
setField(engine, "closing", closing);
when(engine.getClosing()).thenReturn(closing);
when(engine.getGenerationSessions()).thenReturn(sessions);
when(engine.getPlatformHooks()).thenReturn(mock(EnginePlatformHooks.class));
when(engine.getMantle()).thenReturn(mantle);
IrisWorld world = target.getWorld();
when(engine.getWorld()).thenReturn(world);
when(engine.beginShutdown()).thenReturn(true);
doCallRealMethod().when(engine).beginShutdown();
doCallRealMethod().when(engine).requireRunning(anyString());
doCallRealMethod().when(engine).closeDetachedGenerationRuntime(any());
engine.runtime = runtime;
engine.lifecycleState = IrisEngine.LifecycleState.RUNNING;
engine.publishedTarget = target;
}
}
@@ -19,6 +19,8 @@ import art.arcane.volmlib.util.function.Consumer4;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.stream.ProceduralStream;
import art.arcane.volmlib.util.stream.interpolation.Interpolated;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import org.junit.Test;
@@ -208,6 +210,7 @@ public class IrisCarveWallPaletteScopeTest {
engine.runtime = published;
doReturn(IrisWorld.builder().minHeight(-64).maxHeight(0).build()).when(engine).getWorld();
doReturn(64).when(engine).getHeight();
doReturn(mock(EnginePlatformHooks.class)).when(engine).getPlatformHooks();
doReturn(mock(EngineMetrics.class, RETURNS_DEEP_STUBS)).when(engine).getMetrics();
doReturn(48).when(context).getRoundedHeight(anyInt(), anyInt());
IrisComplex activeComplex = active.complex();
@@ -230,13 +233,15 @@ public class IrisCarveWallPaletteScopeTest {
paletteCalls.incrementAndGet();
return wall;
}).when(palette).get(any(RNG.class), anyDouble(), anyDouble(), anyDouble(), any(IrisData.class));
doAnswer(call -> {
try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(
new IrisEngine.GenerationRuntimeBinding(engine, active))) {
assertSame(engine.runtimeAssembly.get() == null ? active.data() : assemblyData, engine.getData());
}
return biome;
}).when(engine).getCaveBiome(anyInt(), anyInt(), anyInt(), any(IrisDimensionCarvingResolver.State.class));
for (IrisComplex complex : new IrisComplex[]{activeComplex, detachedComplex, assemblyComplex}) {
doReturn(ProceduralStream.of((x, z) -> biome, Interpolated.of(value -> 0D, value -> null)))
.when(complex).getTrueBiomeStream();
doReturn(ProceduralStream.of((x, z) -> scopedBiome(biome), Interpolated.of(value -> 0D, value -> null)))
.when(complex).getCaveBiomeStream();
doReturn(ProceduralStream.of((x, z) -> 48D, Interpolated.DOUBLE))
.when(complex).getHeightStream();
}
doAnswer(call -> scopedBiome(biome)).when(engine).getCaveBiome(anyInt(), anyInt(), anyInt(), any(IrisDimensionCarvingResolver.State.class));
doReturn(engine).when(modifier).getEngine();
doAnswer(call -> {
dataLookups.incrementAndGet();
@@ -246,5 +251,14 @@ public class IrisCarveWallPaletteScopeTest {
field(modifier, IrisCarveModifier.class, "AIR", air);
field(modifier, IrisCarveModifier.class, "LAVA", air);
}
private IrisBiome scopedBiome(IrisBiome biome) {
try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(
new IrisEngine.GenerationRuntimeBinding(engine, active))) {
assertSame(engine.runtimeAssembly.get() == null ? active.data() : assemblyData, engine.getData());
}
return biome;
}
}
}
@@ -84,6 +84,72 @@ public class IrisComplexGridBoundsCacheTest {
}
}
public static class CoarseBounds {
@Test
public void coarseBoundsUseGlobalCornersAcrossNegativeChunkSeams() throws Exception {
for (int step : new int[]{8, 16, 32}) {
IrisComplex complex = createComplex();
setStep(complex, step);
CoordinateInterpolator interpolator = new CoordinateInterpolator();
for (double x : new double[]{-33D, -32D, -17D, -16D, -1D, 0D, 15D, 16D, 31D, 32D}) {
for (double z : new double[]{-17D, -0.25D, 0D, 15.5D, 16D}) {
NoiseBounds actual = invokeGridSampleBounds(complex, gridSampleBoundsMethod(), interpolator, x, z);
assertBoundsBitsEqual(expectedGridSampleBounds(x, z, step), actual);
}
}
}
}
@Test
public void coarseGridReducesDistinctEnvelopeEvaluationsForDenseTerrainNodes() throws Exception {
for (int step : new int[]{4, 8, 16, 32}) {
IrisComplex complex = createComplex();
setStep(complex, step);
CountingInterpolator interpolator = new CountingInterpolator(1D, 9D);
for (int x = 0; x < 32; x += 4) {
for (int z = 0; z < 32; z += 4) {
NoiseBounds actual = invokeGridSampleBounds(complex, gridSampleBoundsMethod(), interpolator, x, z);
assertEquals(1D, actual.min(), 0D);
assertEquals(9D, actual.max(), 0D);
}
}
int side = step == 4 ? 8 : 32 / step + 1;
assertEquals(side * side, interpolator.getInvocations());
}
}
@Test
public void everyInterpolatorGroupKeepsItsOwnEnvelopeAndGenerators() throws Exception {
IrisComplex complex = createComplex();
setStep(complex, 16);
CountingInterpolator first = new CountingInterpolator(1D, 2D);
CountingInterpolator second = new CountingInterpolator(10D, 20D);
SampleGenerator firstGenerator = new SampleGenerator(0.25D);
SampleGenerator secondGenerator = new SampleGenerator(0.75D);
setField(complex, "frozenInterpolators", new IrisInterpolator[]{first, second});
setField(complex, "frozenGenerators", new IrisGenerator[][]{{firstGenerator}, {secondGenerator}});
setField(complex, "biomeBuffetGenerators", new HashMap<>());
Method method = IrisComplex.class.getDeclaredMethod("getInterpolatedHeight", Engine.class,
double.class, double.class, long.class);
method.setAccessible(true);
assertEquals(18.75D, (double) method.invoke(complex, null, -1D, 17D, 31L), 0D);
assertEquals(4, first.getInvocations());
assertEquals(4, second.getInvocations());
assertEquals(1, firstGenerator.invocations);
assertEquals(1, secondGenerator.invocations);
}
private static void setStep(IrisComplex complex, int step) throws Exception {
setField(complex, "biomeBoundsSamplingStep", step);
}
private static void setField(IrisComplex complex, String name, Object value) throws Exception {
Field field = IrisComplex.class.getDeclaredField(name);
field.setAccessible(true);
field.set(complex, value);
}
}
@RunWith(Parameterized.class)
public static class LegacyBilerpParity {
@Parameters(name = "x={0} z={1}")
@@ -121,6 +187,9 @@ public class IrisComplexGridBoundsCacheTest {
private static IrisComplex createComplex() throws Exception {
IrisComplex complex = mock(IrisComplex.class, CALLS_REAL_METHODS);
Field biomeBoundsSamplingStep = IrisComplex.class.getDeclaredField("biomeBoundsSamplingStep");
biomeBoundsSamplingStep.setAccessible(true);
biomeBoundsSamplingStep.setInt(complex, 4);
Field generatorBounds = IrisComplex.class.getDeclaredField("generatorBounds");
generatorBounds.setAccessible(true);
@@ -192,7 +261,10 @@ public class IrisComplexGridBoundsCacheTest {
}
private static NoiseBounds legacyGridSampleBounds(double x, double z) {
int grid = 4;
return expectedGridSampleBounds(x, z, 4);
}
private static NoiseBounds expectedGridSampleBounds(double x, double z, int grid) {
int xi = (int) Math.floor(x);
int zi = (int) Math.floor(z);
int mask = grid - 1;
@@ -262,6 +334,21 @@ public class IrisComplexGridBoundsCacheTest {
}
}
private static final class SampleGenerator extends IrisGenerator {
private final double value;
private int invocations;
private SampleGenerator(double value) {
this.value = value;
}
@Override
public double getHeight(double x, double z, long seed) {
invocations++;
return value;
}
}
private static final class CoordinateInterpolator extends IrisInterpolator {
private final AtomicInteger invocations = new AtomicInteger();
@@ -272,11 +359,11 @@ public class IrisComplexGridBoundsCacheTest {
}
private static double low(double x, double z) {
return x * 0.125D - z * 0.0625D - 7.75D;
return x * 0.125D - z * 0.0625D - 7.75D + x * x * 0.000125D;
}
private static double high(double x, double z) {
return x * -0.03125D + z * 0.1875D + 12.5D;
return x * -0.03125D + z * 0.1875D + 12.5D + z * z * 0.00015625D;
}
private int getInvocations() {
@@ -4,6 +4,7 @@ import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.world.pregen.PregenPerformanceProfile;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.hydrology.HydrologyPlanner;
import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.generation.hydrology.HydrologyTile;
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
import art.arcane.iris.generation.hydrology.HydrologyTileKey;
@@ -335,6 +336,7 @@ public class IrisComplexHydrologyNoiseCacheTest {
private HydrologyTileCache planningCache(HydrologyNoiseCacheBudget budget) {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
when(planner.settings()).thenReturn(HydrologyPlannerSettings.defaults());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(mock(HydrologyTile.class));
HydrologyTileCache cache = new HydrologyTileCache(planner, 4);
cache.setTerrainPreparation(() -> complex.expandHydrologyNoiseCaches(budget));
@@ -1,6 +1,9 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntimeContext;
import art.arcane.volmlib.nativelib.terrain.NativeGenerationScope;
import art.arcane.iris.generation.runtime.GenerationRuntime.BiomeMaxes;
import art.arcane.iris.world.history.SavedBiomeRuntime;
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
@@ -32,6 +35,7 @@ import org.junit.Test;
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;
@@ -47,6 +51,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.IntConsumer;
import java.util.function.Supplier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
@@ -70,6 +77,52 @@ public class IrisEngineGenerationRuntimeScopeTest {
@ClassRule
public static final PlatformBinding PLATFORM = PlatformBinding.mockPlatform();
@Test
@SuppressWarnings("unchecked")
public void hydrologyBasisWorkersRetainHistoricalBindingAndRestoreAmbientContext() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
RuntimeFixture historical = runtime(2, 2D, 2D, 2D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
setField(engine, "runtimeBuilder", new EngineRuntimeBuilder(engine));
IrisHydrologyRuntime hydrology = mock(IrisHydrologyRuntime.class, CALLS_REAL_METHODS);
when(historical.data.getEngine()).thenReturn(engine);
when(historical.complex.getHydrologyRuntime()).thenReturn(hydrology);
IrisHydrologyRuntimeContext context = new IrisHydrologyRuntimeContext(17L, 384, historical.dimension,
historical.data, (x, z, height) -> null, (x, z) -> 80D, (x, z) -> "constant",
(x, z) -> false, footprint -> null, () -> false, () -> {});
Field runtimeContext = IrisHydrologyRuntime.class.getDeclaredField("context");
runtimeContext.setAccessible(true);
runtimeContext.set(hydrology, context);
Method capture = IrisHydrologyRuntime.class.getDeclaredMethod("captureBasisScope");
capture.setAccessible(true);
assertNull(capture.invoke(hydrology));
Supplier<NativeGenerationScope> workerScope;
try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(detachedBinding(engine, historical.runtime));
IrisContext.Scope execution = IrisContext.open(engine, 73L, mock(ChunkContext.class))) {
workerScope = (Supplier<NativeGenerationScope>) capture.invoke(hydrology);
}
ExecutorService worker = Executors.newSingleThreadExecutor();
try {
worker.submit(() -> {
assertSame(active.complex, engine.getComplex());
try (IrisContext.Scope ambient = IrisContext.open(engine, 99L, null)) {
try (NativeGenerationScope selected = workerScope.get()) {
assertSame(historical.complex, engine.getComplex());
assertSame(historical.data, IrisContext.require().getData());
assertEquals(73L, IrisContext.require().getGenerationSessionId());
assertNull(IrisContext.require().getChunkContext());
}
assertSame(active.complex, engine.getComplex());
assertEquals(99L, IrisContext.require().getGenerationSessionId());
}
assertNull(IrisContext.get());
}).get(5, TimeUnit.SECONDS);
} finally {
worker.shutdownNow();
assertTrue(worker.awaitTermination(5, TimeUnit.SECONDS));
}
}
@Test
public void carvingResolverRestoresNestedRuntimeDefinitions() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
@@ -128,7 +128,7 @@ public class IrisCarveModifierBiomeBlendTest {
private static IrisCarveModifier modifier(Engine engine, RNG rng) throws ReflectiveOperationException {
IrisCarveModifier modifier = mock(IrisCarveModifier.class, CALLS_REAL_METHODS);
doReturn(engine).when(modifier).getEngine();
doReturn(mock(IrisComplex.class)).when(modifier).getComplex();
doReturn(mock(IrisComplex.class)).when(engine).getComplex();
Field field = IrisCarveModifier.class.getDeclaredField("rng");
field.setAccessible(true);
field.set(modifier, rng);
@@ -138,7 +138,7 @@ public class IrisCarveModifierBiomeBlendTest {
private static IrisBiome resolve(IrisCarveModifier modifier, int[] coordinate,
Long2ObjectOpenHashMap<IrisBiome> cache) {
return modifier.resolveCaveBoundaryBiome(null, coordinate[0], coordinate[1], coordinate[2],
new IrisDimensionCarvingResolver.State(), cache, new HashMap<>());
new IrisCarveModifier.CaveInputs(modifier.getEngine()), cache, new HashMap<>());
}
private static int[] coordinate(int roll, int sign) {
@@ -50,7 +50,7 @@ public class IrisCarveModifierBoundarySupportTest {
IrisCarveModifier modifier = mock(IrisCarveModifier.class, CALLS_REAL_METHODS);
doReturn(engine).when(modifier).getEngine();
doReturn(mock(IrisComplex.class)).when(modifier).getComplex();
doReturn(mock(IrisComplex.class)).when(engine).getComplex();
Field rng = IrisCarveModifier.class.getDeclaredField("rng");
rng.setAccessible(true);
rng.set(modifier, new RNG(71L));
@@ -73,7 +73,7 @@ public class IrisCarveModifierBoundarySupportTest {
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache = new Long2ObjectOpenHashMap<>();
Map<String, IrisBiome> customBiomeCache = new HashMap<>();
IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State();
IrisCarveModifier.CaveInputs resolverState = new IrisCarveModifier.CaveInputs(engine);
IrisBiome floor = modifier.resolveCaveBoundaryBiome(
mantleChunk, 1, 6, 2, 40, 44, resolverState, caveBiomeCache, customBiomeCache);
@@ -94,11 +94,13 @@ public class IrisCarveModifierBoundarySupportTest {
doReturn(biomes).when(complex).getTrueBiomeStream();
doReturn(surface).when(biomes).get(40, 44);
IrisCarveModifier modifier = mock(IrisCarveModifier.class, CALLS_REAL_METHODS);
doReturn(complex).when(modifier).getComplex();
Engine engine = mock(Engine.class);
doReturn(engine).when(modifier).getEngine();
doReturn(complex).when(engine).getComplex();
IrisBiome biome = modifier.resolveCaveBoundaryBiome(
new MatterCavern(true, "carving/deep", (byte) 0), 40, 42, 44,
new IrisDimensionCarvingResolver.State(), new Long2ObjectOpenHashMap<>(), new HashMap<>());
new IrisCarveModifier.CaveInputs(engine), new Long2ObjectOpenHashMap<>(), new HashMap<>());
assertSame(surface, biome);
}
@@ -0,0 +1,177 @@
package art.arcane.iris.generation.stage;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.context.IrisContext;
import art.arcane.iris.generation.runtime.DimensionStackContext;
import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EnginePlatformHooks;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisDimensionCarvingEntry;
import art.arcane.iris.generation.terrain.IrisDimensionCarvingResolver;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.loading.ResourceLoader;
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.stream.ProceduralStream;
import art.arcane.volmlib.util.stream.interpolation.Interpolated;
import org.junit.Test;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisCarveModifierCaveInputsTest {
@Test
public void capturedColumnsMatchScalarAcrossDepthStackAndCoordinateCollisions() {
try (Fixture fixture = new Fixture()) {
DimensionStackContext stack = mock(DimensionStackContext.class);
when(fixture.engine.getDimensionStackContext()).thenReturn(stack);
when(fixture.scalar.getDimensionStackContext()).thenReturn(stack);
when(stack.getLayout(anyInt(), anyInt())).thenAnswer(call -> {
DimensionStackLayout layout = mock(DimensionStackLayout.class);
DimensionStackLayout.Layer layer = mock(DimensionStackLayout.Layer.class);
when(layer.biome()).thenReturn((int) call.getArgument(0) % 2 == 0 ? fixture.stacked : null);
when(layout.surfaceLayer()).thenReturn(layer);
return layout;
});
IrisCarveModifier.CaveInputs inputs = new IrisCarveModifier.CaveInputs(fixture.engine);
IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State();
for (int x : new int[]{-35, -32, -17, -16, 0, 16, 1024, Integer.MIN_VALUE, Integer.MAX_VALUE}) {
for (int z : new int[]{-35, -16, 0, 3, 1024, Integer.MIN_VALUE, Integer.MAX_VALUE}) {
for (int y : new int[]{0, 30, 32, 33, 34, 39, 40, 41}) {
assertSame(fixture.scalar.getCaveBiome(x, y, z, state), inputs.resolve(x, y, z));
}
}
}
}
}
@Test
public void repeatedHeightsReuseCapturedColumnAndIgnoreAmbientRuntimeReplacement() {
try (Fixture fixture = new Fixture()) {
IrisCarveModifier.CaveInputs inputs = new IrisCarveModifier.CaveInputs(fixture.engine);
IrisComplex replacement = mock(IrisComplex.class);
when(fixture.engine.getComplex()).thenReturn(replacement);
when(fixture.engine.getDimension()).thenReturn(new IrisDimension());
when(fixture.engine.getData()).thenReturn(mock(IrisData.class));
for (int y = 0; y < 40; y++) {
assertSame(y <= 33 ? fixture.cave : fixture.surface, inputs.resolve(-31, y, 6));
}
assertEquals(1, fixture.samples.get());
}
}
@Test
@SuppressWarnings("unchecked")
public void configuredWorldHeightRangeRetainsCapturedPackAndPrecedesFallback() {
try (Fixture fixture = new Fixture()) {
IrisBiome configured = biome("configured", 0);
ResourceLoader<IrisBiome> loader = mock(ResourceLoader.class);
when(fixture.data.getBiomeLoader()).thenReturn(loader);
when(loader.load("configured")).thenReturn(configured);
IrisDimensionCarvingEntry root = new IrisDimensionCarvingEntry();
root.setBiome("configured");
root.setChildRecursionDepth(0);
root.setWorldYRange(new IrisRange(-64, -48));
fixture.dimension.setCarving(new KList<>(List.of(root)));
IrisCarveModifier.CaveInputs inputs = new IrisCarveModifier.CaveInputs(fixture.engine);
when(fixture.engine.getDimension()).thenReturn(new IrisDimension());
when(fixture.engine.getData()).thenReturn(mock(IrisData.class));
assertSame(configured, inputs.resolve(-31, 0, 6));
assertSame(configured, inputs.resolve(-31, 16, 6));
assertEquals(0, fixture.samples.get());
assertSame(fixture.cave, inputs.resolve(-31, 17, 6));
}
}
@Test
public void genericAndMainThreadEnginesRetainCustomBiomeDispatch() {
IrisBiome custom = biome("custom", 0);
Engine generic = mock(Engine.class);
when(generic.getCaveBiome(anyInt(), anyInt(), anyInt(), any(IrisDimensionCarvingResolver.State.class))).thenReturn(custom);
assertSame(custom, new IrisCarveModifier.CaveInputs(generic).resolve(1, 2, 3));
try (Fixture fixture = new Fixture()) {
when(fixture.hooks.isMainThread()).thenReturn(true);
when(fixture.engine.getCaveBiome(anyInt(), anyInt(), anyInt(), any(IrisDimensionCarvingResolver.State.class))).thenReturn(custom);
assertSame(custom, new IrisCarveModifier.CaveInputs(fixture.engine).resolve(1, 2, 3));
assertEquals(0, fixture.samples.get());
}
}
@Test
public void contentStageKeepsCoordinateHistoryDispatch() {
try (Fixture fixture = new Fixture()) {
when(fixture.context.isNaturalTerrain()).thenReturn(false);
IrisBiome historical = biome("historical", 0);
when(fixture.engine.getCaveBiome(anyInt(), anyInt(), anyInt(), any(IrisDimensionCarvingResolver.State.class))).thenReturn(historical);
assertSame(historical, new IrisCarveModifier.CaveInputs(fixture.engine).resolve(-17, 20, 16));
assertEquals(0, fixture.samples.get());
}
}
private static IrisBiome biome(String key, int depth) {
IrisBiome biome = new IrisBiome();
biome.setLoadKey(key);
biome.setCaveMinDepthBelowSurface(depth);
return biome;
}
private static final class Fixture implements AutoCloseable {
private final IrisContext.Scope scope;
private final ChunkContext context = mock(ChunkContext.class);
private final IrisEngine engine = mock(IrisEngine.class);
private final Engine scalar = mock(Engine.class, CALLS_REAL_METHODS);
private final IrisComplex complex = mock(IrisComplex.class);
private final IrisDimension dimension = new IrisDimension();
private final IrisData data = mock(IrisData.class);
private final EnginePlatformHooks hooks = mock(EnginePlatformHooks.class);
private final IrisBiome surface = biome("surface", 3);
private final IrisBiome cave = biome("cave", 7);
private final IrisBiome unkeyed = biome(null, 2);
private final IrisBiome stacked = biome("stacked", 5);
private final AtomicInteger samples = new AtomicInteger();
private Fixture() {
when(context.getComplex()).thenReturn(complex);
when(context.isNaturalTerrain()).thenReturn(true);
scope = IrisContext.open(engine, 1L, context);
when(engine.hasGenerationRuntimeScope()).thenReturn(true);
for (Engine target : new Engine[]{engine, scalar}) {
doReturn(complex).when(target).getComplex();
doReturn(dimension).when(target).getDimension();
doReturn(data).when(target).getData();
doReturn(hooks).when(target).getPlatformHooks();
doReturn(IrisWorld.builder().minHeight(-64).maxHeight(320).build()).when(target).getWorld();
}
when(complex.getTrueBiomeStream()).thenReturn(ProceduralStream.of((x, z) -> surface, Interpolated.of(value -> 0D, value -> null)));
when(complex.getCaveBiomeStream()).thenReturn(ProceduralStream.of((x, z) -> switch (Math.floorMod(x.intValue(), 3)) {
case 0 -> null;
case 1 -> unkeyed;
default -> cave;
}, Interpolated.of(value -> 0D, value -> null)));
when(complex.getHeightStream()).thenReturn(ProceduralStream.of((x, z) -> {
samples.incrementAndGet();
return 40D;
}, Interpolated.DOUBLE));
}
@Override
public void close() {
scope.close();
}
}
}
@@ -64,7 +64,7 @@ public class IrisCarveModifierInferenceIsolationTest {
int.class,
int.class,
int.class,
IrisDimensionCarvingResolver.State.class,
IrisCarveModifier.CaveInputs.class,
Long2ObjectOpenHashMap.class,
Map.class
);
@@ -80,7 +80,7 @@ public class IrisCarveModifierInferenceIsolationTest {
0,
1,
1,
new IrisDimensionCarvingResolver.State(),
new IrisCarveModifier.CaveInputs(engine),
new Long2ObjectOpenHashMap<IrisBiome>(),
customBiomes
);
@@ -1,14 +1,33 @@
package art.arcane.iris.generation.stage;
import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.generation.biome.IrisFloatingChildBiomes;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class IrisFloatingChildBiomeModifierStackTest {
@Test
public void floatingPaletteSeedDependsOnWorldAndBiomeInsteadOfColumnVisitOrder() {
IrisFloatingChildBiomes first = new IrisFloatingChildBiomes().setBiome("magnetics/glass-shard");
IrisFloatingChildBiomes repeated = new IrisFloatingChildBiomes().setBiome("magnetics/glass-shard");
IrisFloatingChildBiomes other = new IrisFloatingChildBiomes().setBiome("other/floating");
RNG world = new RNG(7191L ^ 0x7EB0A73F1DCE514DL);
long expected = IrisFloatingChildBiomeModifier.paletteRng(world, first).getSeed();
for (int column = -256; column < 256; column++) {
world.nextLong();
assertEquals(expected, IrisFloatingChildBiomeModifier.paletteRng(world, repeated).getSeed());
}
assertNotEquals(expected, IrisFloatingChildBiomeModifier.paletteRng(world, other).getSeed());
assertNotEquals(expected, IrisFloatingChildBiomeModifier.paletteRng(new RNG(8191L), first).getSeed());
}
@Test
public void floatingBiomeMarkersSkipStackProtectedCells() {
DimensionStackLayout layout = mock(DimensionStackLayout.class);
@@ -1,6 +1,7 @@
package art.arcane.iris.generation.terrain;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.iris.world.IrisWorld;
@@ -48,6 +49,7 @@ import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
public class IrisDimensionCarvingResolverParityTest {
@@ -59,6 +61,67 @@ public class IrisDimensionCarvingResolverParityTest {
BukkitTestServer.install();
}
@Test
public void scopedSnapshotMatchesScalarRootsAndReadsRuntimeIdentityOnce() {
Fixture fixture = createFixture();
Map<String, IrisBiome> expected = new HashMap<>();
for (int y = -80; y <= 340; y += 13) {
for (int x = -17; x <= 17; x += 17) {
expected.put(x + ":" + y, IrisDimensionCarvingResolver.resolveBiome(
fixture.engine, x, y, -23, new IrisDimensionCarvingResolver.State()));
}
}
clearInvocations(fixture.engine);
IrisDimensionCarvingResolver.Snapshot snapshot = IrisDimensionCarvingResolver.snapshot(fixture.engine);
for (int y = -80; y <= 340; y += 13) {
for (int x = -17; x <= 17; x += 17) {
assertSame(expected.get(x + ":" + y), snapshot.resolveBiome(x, y, -23));
}
}
verify(fixture.engine).getDimension();
verify(fixture.engine).getData();
}
@Test
public void scopedSnapshotsBindReplacementRuntimeWithoutSharingPriorResults() {
Fixture first = createFixture();
Fixture replacement = createMixedDepthFixture();
IrisBiome expectedFirst = IrisDimensionCarvingResolver.resolveBiome(first.engine, -17, 80, 23, null);
IrisBiome expectedReplacement = IrisDimensionCarvingResolver.resolveBiome(replacement.engine, -17, 80, 23, null);
assertSame(expectedFirst, IrisDimensionCarvingResolver.snapshot(first.engine).resolveBiome(-17, 80, 23));
doReturn(replacement.engine.getData()).when(first.engine).getData();
doReturn(replacement.engine.getDimension()).when(first.engine).getDimension();
doReturn(replacement.engine.getSeedManager()).when(first.engine).getSeedManager();
assertSame(expectedReplacement, IrisDimensionCarvingResolver.snapshot(first.engine).resolveBiome(-17, 80, 23));
}
@Test
public void scopedSnapshotsKeepExpressionSamplingPerImplicitCell() {
for (boolean fracture : new boolean[]{false, true}) {
Fixture fixture = createFixture();
IrisDimension dimension = fixture.engine.getDimension();
IrisDimensionCarvingEntry parent = spy(dimension.getCarvingEntryIndex().get("root-low"));
parent.setChildRecursionDepth(1);
if (fracture) {
parent.getChildStyle().setFracture(new IrisGeneratorStyle().setExpression("live-height"));
} else {
parent.getChildStyle().setExpression("live-height");
}
KList<IrisDimensionCarvingEntry> entries = new KList<>(dimension.getCarving());
entries.set(0, parent);
doReturn(entries).when(dimension).getCarving();
dimension.getCarvingEntryIndex().put("root-low", parent);
CNG generator = mock(CNG.class);
doReturn(0.5D, 0D).when(generator).noiseFast2D(-17D, 23D);
doReturn(generator).when(parent).getChildrenGenerator(anyLong(), any(IrisData.class));
IrisDimensionCarvingResolver.Snapshot snapshot = IrisDimensionCarvingResolver.snapshot(fixture.engine);
assertSame(fixture.engine.getData().getBiomeLoader().load("child-a"), snapshot.resolveBiome(-17, 40, 23));
assertSame(fixture.engine.getData().getBiomeLoader().load("root-low"), snapshot.resolveBiome(-17, 41, 23));
verify(generator, times(2)).noiseFast2D(-17D, 23D);
}
}
@Test
public void extremeChildRarityRemainsReachableWithOneNoiseSample() {
Fixture fixture = createFixture();
@@ -84,6 +147,72 @@ public class IrisDimensionCarvingResolverParityTest {
verify(generator, never()).fit2D(anyInt(), anyInt(), anyDouble(), anyDouble());
}
@Test
public void repeatedColumnResolutionSamplesChildrenOnceAcrossHeights() {
Fixture fixture = createFixture();
IrisDimensionCarvingEntry parent = spy(fixture.engine.getDimension().getCarvingEntryIndex().get("root-low"));
parent.setChildRecursionDepth(1);
CNG generator = mock(CNG.class);
doReturn(0.5D).when(generator).noiseFast2D(anyDouble(), anyDouble());
doReturn(generator).when(parent).getChildrenGenerator(anyLong(), any(IrisData.class));
IrisDimensionCarvingEntry child = fixture.engine.getDimension().getCarvingEntryIndex().get("child-a");
IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State();
for (int height = 0; height < 16; height++) {
for (int x = -16; x < 0; x++) {
for (int z = 0; z < 16; z++) {
assertSame(child, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, x, z, state));
}
}
}
verify(generator, times(256)).noiseFast2D(anyDouble(), anyDouble());
}
@Test
public void expressionChildStylesKeepSamplingTheCurrentContext() {
for (boolean fracture : new boolean[]{false, true}) {
Fixture fixture = createFixture();
IrisDimensionCarvingEntry parent = spy(fixture.engine.getDimension().getCarvingEntryIndex().get("root-low"));
parent.setChildRecursionDepth(1);
if (fracture) {
parent.getChildStyle().setFracture(new IrisGeneratorStyle().setExpression("live-height"));
} else {
parent.getChildStyle().setExpression("live-height");
}
CNG generator = mock(CNG.class);
doReturn(0.5D, 0D).when(generator).noiseFast2D(-17D, 23D);
doReturn(generator).when(parent).getChildrenGenerator(anyLong(), any(IrisData.class));
IrisDimensionCarvingEntry child = fixture.engine.getDimension().getCarvingEntryIndex().get("child-a");
IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State();
assertSame(child, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, -17, 23, state));
assertSame(parent, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, -17, 23, state));
}
}
@Test
public void cachedColumnResolutionInvalidatesWithDataAndEngineChanges() {
Fixture fixture = createFixture();
Fixture replacement = createFixture();
IrisDimensionCarvingEntry parent = spy(fixture.engine.getDimension().getCarvingEntryIndex().get("root-low"));
parent.setChildRecursionDepth(1);
CNG generator = mock(CNG.class);
doReturn(0.5D).when(generator).noiseFast2D(anyDouble(), anyDouble());
doReturn(generator).when(parent).getChildrenGenerator(anyLong(), any(IrisData.class));
IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State();
IrisDimensionCarvingEntry firstChild = fixture.engine.getDimension().getCarvingEntryIndex().get("child-a");
IrisDimensionCarvingEntry replacementChild = replacement.engine.getDimension().getCarvingEntryIndex().get("child-a");
assertSame(firstChild, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, -17, 23, state));
doReturn(replacement.engine.getData()).when(fixture.engine).getData();
assertSame(firstChild, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, -17, 23, state));
assertSame(replacementChild, IrisDimensionCarvingResolver.resolveFromRoot(replacement.engine, parent, -17, 23, state));
assertSame(replacementChild, IrisDimensionCarvingResolver.resolveFromRoot(replacement.engine, parent, -17, 23, state));
verify(generator, times(3)).noiseFast2D(-17D, 23D);
}
@Test
public void resolverStatefulOverloadsMatchLegacyResolverAcrossSampleGrid() {
Fixture fixture = createFixture();
@@ -0,0 +1,195 @@
package art.arcane.iris.generation.terrain;
import art.arcane.iris.generation.biome.IrisBiome;
import org.junit.Test;
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 java.util.concurrent.atomic.AtomicInteger;
import java.util.function.DoubleBinaryOperator;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class ProceduralTerrainHeightSamplerTest {
@Test
public void unitStepPreservesOriginalFractionalSamples() {
AtomicInteger calls = new AtomicInteger();
ProceduralTerrainHeightSampler sampler = new ProceduralTerrainHeightSampler((x, z) -> {
calls.incrementAndGet();
return terrain(x, z);
}, 1);
for (double x : new double[]{-17.25D, -4D, -0.5D, 0D, 1.75D, 32D}) {
assertEquals(terrain(x, -2.25D), sampler.sample(x, -2.25D), 0D);
}
assertEquals(6, calls.get());
}
@Test
public void denseTerrainReusesLatticeNodesInsteadOfSamplingEveryColumn() {
AtomicInteger calls = new AtomicInteger();
ProceduralTerrainHeightSampler sampler = new ProceduralTerrainHeightSampler((x, z) -> {
calls.incrementAndGet();
return terrain(x, z);
}, 4);
for (int x = -32; x < 32; x++) {
for (int z = -32; z < 32; z++) {
assertEquals(interpolated(ProceduralTerrainHeightSamplerTest::terrain, x, z, 4),
sampler.sample(x, z), 0D);
}
}
assertTrue("Procedural sample calls: " + calls.get(), calls.get() < 400);
assertTrue(calls.get() >= 289);
}
@Test
public void neighboringRegionsRetainHeightsAcrossRepeatedSweeps() {
AtomicInteger calls = new AtomicInteger();
DoubleBinaryOperator linear = (x, z) -> x * 0.25D - z * 0.125D + 96D;
ProceduralTerrainHeightSampler sampler = new ProceduralTerrainHeightSampler((x, z) -> {
calls.incrementAndGet();
return linear.applyAsDouble(x, z);
}, 4);
int uniqueNodes = 261 * 261;
for (int sweep = 0; sweep < 2; sweep++) {
int before = calls.get();
for (int chunkX = -32; chunkX <= 32; chunkX++) {
for (int chunkZ = -32; chunkZ <= 32; chunkZ++) {
for (int localX = 0; localX < 16; localX++) {
for (int localZ = 0; localZ < 16; localZ++) {
int x = chunkX * 16 + localX;
int z = chunkZ * 16 + localZ;
assertEquals(linear.applyAsDouble(x, z), sampler.sample(x, z), 0D);
}
}
}
}
int sourceCalls = calls.get() - before;
assertTrue("Procedural sample calls in sweep " + sweep + ": " + sourceCalls,
sourceCalls < (sweep == 0 ? uniqueNodes * 1.05D : uniqueNodes * 0.25D));
}
}
@Test
public void allStepsPreserveNodesLinearFieldsAndSignedWorldCoordinates() {
DoubleBinaryOperator linear = (x, z) -> x * 0.25D - z * 0.125D + 96D;
for (int step : new int[]{2, 4, 8}) {
ProceduralTerrainHeightSampler sampler = new ProceduralTerrainHeightSampler(linear, step);
ProceduralTerrainHeightSampler shaped = new ProceduralTerrainHeightSampler(
ProceduralTerrainHeightSamplerTest::terrain, step);
for (double x : new double[]{Integer.MIN_VALUE, -33.5D, -8D, -0.25D, 0D, 8D, 31.75D,
Integer.MAX_VALUE}) {
for (double z : new double[]{-19.5D, -8D, 0D, 8D, 19.5D}) {
assertEquals(linear.applyAsDouble(x, z), sampler.sample(x, z), 0D);
assertEquals(interpolated(ProceduralTerrainHeightSamplerTest::terrain, x, z, step),
shaped.sample(x, z), 0D);
}
}
for (int x = -32; x <= 32; x += step) {
assertEquals(terrain(x, -8), shaped.sample(x, -8), 0D);
}
}
}
@Test
public void concurrentQueriesRemainDeterministicAcrossCacheReplacement() throws Exception {
ProceduralTerrainHeightSampler sampler = new ProceduralTerrainHeightSampler(
ProceduralTerrainHeightSamplerTest::terrain, 4);
ExecutorService executor = Executors.newFixedThreadPool(4);
List<Future<?>> tasks = new ArrayList<>();
try {
for (int worker = 0; worker < 4; worker++) {
int offset = worker * 65_537;
tasks.add(executor.submit(() -> {
for (int index = 0; index < 100_000; index++) {
int x = index * 4 - offset;
int z = -index * 8 + offset;
assertEquals(interpolated(ProceduralTerrainHeightSamplerTest::terrain, x, z, 4),
sampler.sample(x, z), 0D);
}
}));
}
for (Future<?> task : tasks) {
task.get(10, TimeUnit.SECONDS);
}
assertTrue(sampler.cachedNodeCount() > 200_000);
assertTrue(sampler.cachedNodeCount() <= 262_144);
ProceduralTerrainHeightSampler other = new ProceduralTerrainHeightSampler((x, z) -> 42D, 4);
assertEquals(42D, other.sample(0, 0), 0D);
assertEquals(terrain(0, 0), sampler.sample(0, 0), 0D);
} finally {
executor.shutdownNow();
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
}
}
@Test
public void failedNodesCanBeRetriedAndInvalidStepsAreRejected() {
AtomicInteger calls = new AtomicInteger();
ProceduralTerrainHeightSampler sampler = new ProceduralTerrainHeightSampler((x, z) -> {
if (calls.incrementAndGet() == 1) {
throw new IllegalStateException("height failure");
}
return 72D;
}, 4);
assertThrows(IllegalStateException.class, () -> sampler.sample(-4D, 8D));
assertEquals(72D, sampler.sample(-4D, 8D), 0D);
assertEquals(2, calls.get());
for (int step : new int[]{-1, 0, 3, 16}) {
assertThrows(IllegalArgumentException.class,
() -> new ProceduralTerrainHeightSampler((x, z) -> 0D, step));
}
}
@Test
public void sampledBaseHeightsRetainThreeDimensionalLedgesAndAirGaps() {
ProceduralTerrainHeightSampler sampler = new ProceduralTerrainHeightSampler(
(x, z) -> 96D + (Math.floorMod((int) x, 4) == 2 ? 2D : 0D), 4);
IrisBiome biome = new IrisBiome().setTerrain3D(new IrisTerrain3D()
.setAmplitude(32).setHorizontalScale(64).setVerticalScale(64)
.setMinimumSlope(0).setFluidClearance(0).setFluidFade(1));
Terrain3DRuntime.Options options = new Terrain3DRuntime.Options(12, 256, 0, null, true, 4096);
Terrain3DRuntime.NoiseFactory noise = (style, seed) ->
(x, y, z) -> StrictMath.sin(y * StrictMath.PI / 16D);
Terrain3DRuntime sampled = new Terrain3DRuntime(
new Terrain3DRuntime.Sources((x, z) -> sampler.sample(x, z), (x, z) -> biome), options, noise);
Terrain3DRuntime reference = new Terrain3DRuntime(
new Terrain3DRuntime.Sources((x, z) -> 96D, (x, z) -> biome), options, noise);
for (int x = -9; x <= 9; x++) {
Terrain3DColumn column = sampled.column(x, -2);
assertEquals(reference.column(x, -2), column);
assertTrue(column.shaped());
assertTrue(column.spanCount() > 1);
assertTrue(column.isSolid(104));
assertFalse(column.isSolid(84));
assertTrue(column.isSolid(60));
}
}
private static double terrain(double x, double z) {
return 96D + StrictMath.sin(x * 0.13D) * 18D + StrictMath.cos(z * 0.17D) * 12D;
}
private static double interpolated(DoubleBinaryOperator source, double x, double z, int step) {
double lowerX = Math.floor(x / step) * step;
double lowerZ = Math.floor(z / step) * step;
double dx = (x - lowerX) / step;
double dz = (z - lowerZ) / step;
double northWest = source.applyAsDouble(lowerX, lowerZ);
double north = dx == 0D ? northWest
: northWest + (source.applyAsDouble(lowerX + step, lowerZ) - northWest) * dx;
if (dz == 0D) {
return north;
}
double southWest = source.applyAsDouble(lowerX, lowerZ + step);
double south = dx == 0D ? southWest
: southWest + (source.applyAsDouble(lowerX + step, lowerZ + step) - southWest) * dx;
return north + (south - north) * dz;
}
}
@@ -3,15 +3,146 @@ package art.arcane.iris.generation.terrain;
import org.junit.Test;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class Terrain3DFragmentFilterTest {
@Test
public void heightLeavesIrrelevantLowerFragmentsUnclassified() {
Terrain3DColumn center = new Terrain3DColumn(10D, 11, true, new int[]{0, 10, 20, 20, 100, 612});
BiFunction<Integer, Integer, Terrain3DColumn> source = (x, z) -> x == 0 && z == 0
? center : new Terrain3DColumn(10D, 11, true, new int[]{0, 10});
Fixture reference = new Fixture(source);
Fixture selective = new Fixture(source);
Terrain3DColumn expected = reference.filter.column(0, 0);
assertEquals(expected.topY(), selective.filter.height(0, 0), 0D);
assertEquals(1, selective.reads.get());
assertTrue(reference.reads.get() > selective.reads.get());
assertEquals(expected, selective.filter.column(0, 0));
assertEquals(2, expected.spanCount());
}
@Test
public void heightRejectsSmallTopFragmentBeforeKeepingGroundConnectedLedge() {
Fixture fixture = new Fixture((x, z) -> x == 0 && z == 0
? new Terrain3DColumn(10D, 11, true, new int[]{0, 10, 50, 70, 110, 112})
: new Terrain3DColumn(100D, 101, true, new int[]{0, 100}));
assertEquals(70D, fixture.filter.height(0, 0), 0D);
assertEquals(70, fixture.filter.column(0, 0).topY());
assertEquals(2, fixture.filter.column(0, 0).spanCount());
}
@Test
public void heightUsesExactConnectedVolumeThresholdAndSignedCoordinateEdges() {
for (int volume : new int[]{512, 513}) {
BiFunction<Integer, Integer, Terrain3DColumn> source = (x, z) ->
x >= -volume && x < 0 && z == -17
? new Terrain3DColumn(10D, 11, true, new int[]{0, 10, 20, 20})
: new Terrain3DColumn(10D, 11, true, new int[]{0, 10});
Fixture reference = new Fixture(source);
Fixture selective = new Fixture(source);
assertEquals(volume == 512 ? 10D : 20D, selective.filter.height(-volume, -17), 0D);
assertEquals(reference.filter.column(-volume, -17), selective.filter.column(-volume, -17));
}
for (int x : new int[]{Integer.MIN_VALUE, Integer.MAX_VALUE}) {
Fixture fixture = new Fixture((ignoredX, ignoredZ) ->
new Terrain3DColumn(10D, 11, true, new int[]{0, 10, 20, 20}));
assertEquals(20D, fixture.filter.height(x, 7), 0D);
assertEquals(20, fixture.filter.column(x, 7).topY());
}
}
@Test
public void concurrentHeightAndFullColumnQueriesMatchIndependentFullClassification() throws Exception {
Random random = new Random(883719L);
Map<Long, Terrain3DColumn> raw = new HashMap<>();
List<Long> positions = new ArrayList<>();
for (int x = -8; x <= 8; x++) {
for (int z = -8; z <= 8; z++) {
int ground = random.nextInt(5) == 0 ? 36 : 5 + random.nextInt(6);
int[] boundaries = new int[10];
boundaries[0] = 0;
boundaries[1] = ground;
int count = 2;
for (int y = 16; y <= 40; y += 8) {
if (y > ground + 1 && random.nextBoolean()) {
boundaries[count++] = y;
boundaries[count++] = y + random.nextInt(3);
}
}
long key = pack(x, z);
raw.put(key, new Terrain3DColumn(ground + 0.25D, ground + 1, true, Arrays.copyOf(boundaries, count)));
positions.add(key);
}
}
Terrain3DColumn outside = new Terrain3DColumn(8D, 9, true, new int[]{0, 8});
BiFunction<Integer, Integer, Terrain3DColumn> source = (x, z) -> raw.getOrDefault(pack(x, z), outside);
Fixture reference = new Fixture(source);
Map<Long, Terrain3DColumn> expected = new HashMap<>();
for (long key : positions) {
expected.put(key, reference.filter.column((int) (key >> 32), (int) key));
}
Fixture selective = new Fixture(source);
try (ExecutorService workers = Executors.newFixedThreadPool(4)) {
List<Future<?>> tasks = new ArrayList<>();
for (int worker = 0; worker < 4; worker++) {
int shift = worker * 59;
tasks.add(workers.submit(() -> {
for (int index = 0; index < positions.size() * 2; index++) {
long key = positions.get((index + shift) % positions.size());
int x = (int) (key >> 32);
int z = (int) key;
assertEquals(expected.get(key).topY(), selective.filter.height(x, z), 0D);
if ((index & 1) == 0) {
assertEquals(expected.get(key), selective.filter.column(x, z));
}
}
}));
}
for (Future<?> task : tasks) {
task.get(10, TimeUnit.SECONDS);
}
}
for (long key : positions) {
assertEquals(expected.get(key), selective.filter.column((int) (key >> 32), (int) key));
}
}
private static long pack(int x, int z) {
return (long) x << 32 | z & 0xffffffffL;
}
private static final class Fixture {
private final AtomicInteger reads = new AtomicInteger();
private final ConcurrentHashMap<Long, Terrain3DFragmentFilter.DensityColumn> columns = new ConcurrentHashMap<>();
private final Terrain3DFragmentFilter filter;
private Fixture(BiFunction<Integer, Integer, Terrain3DColumn> source) {
filter = new Terrain3DFragmentFilter((x, z) -> {
reads.incrementAndGet();
return columns.computeIfAbsent(pack(x, z), ignored -> new Terrain3DFragmentFilter.DensityColumn(source.apply(x, z)));
});
}
}
@Test
public void perThreadTraversalScratchDoesNotPinTheFilterAfterItIsDropped() throws Exception {
// A hotload retires the runtime that owns the filter; the worker threads that classified
@@ -21,6 +21,35 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class Terrain3DRuntimeTest {
@Test
public void heightQueriesPreserveCompleteDensityColumnsAndFractionalFallback() {
Terrain3DRuntime.NoiseSource noise = (x, y, z) -> StrictMath.sin(y * StrictMath.PI / 16D)
* StrictMath.cos(x * 0.08D) * StrictMath.cos(z * 0.06D);
Terrain3DRuntime reference = runtime(profile(), noise);
Terrain3DRuntime selective = runtime(profile(), noise);
for (int pass = 0; pass < 2; pass++) {
for (int x = -12; x <= 12; x++) {
for (int z = -6; z <= 6; z++) {
Terrain3DColumn expected = reference.column(x, z);
assertEquals(expected.topY(), selective.height(x, z), 0D);
assertEquals(expected, selective.column(x, z));
assertEquals(expected.topY(), selective.height(x, z), 0D);
}
}
selective.clear();
}
Terrain3DRuntime unshaped = new Terrain3DRuntime(
new Terrain3DRuntime.Sources((x, z) -> 300.25D, (x, z) -> new IrisBiome()),
options(true), (style, seed) -> noise);
assertEquals(300.25D, unshaped.height(-17, 23), 0D);
assertEquals(255, unshaped.column(-17, 23).topY());
assertEquals(300.25D, unshaped.height(-17, 23), 0D);
Terrain3DRuntime inactive = new Terrain3DRuntime(
new Terrain3DRuntime.Sources((x, z) -> -12.75D, (x, z) -> null),
options(false), (style, seed) -> noise);
assertEquals(-12.75D, inactive.height(0, 0), 0D);
}
@Test
public void alignedColumnsOnlySampleContributingAnchors() {
for (int x = -8; x <= -7; x++) {
@@ -260,6 +289,138 @@ public class Terrain3DRuntimeTest {
assertEquals(40, water.column(0, 0).topY());
}
@Test
public void anchorRetentionBoundsFullHeightNoiseStorage() {
int shorter = Terrain3DRuntime.anchorCacheCapacity(256, 4096);
int taller = Terrain3DRuntime.anchorCacheCapacity(1024, 4096);
assertTrue(shorter > taller);
assertTrue(taller > 4096 / 4);
for (int height : new int[]{16, 256, 640, 1024, 4096, 16_384}) {
for (int columns : new int[]{16, 4096, Integer.MAX_VALUE}) {
int capacity = Terrain3DRuntime.anchorCacheCapacity(height, columns);
long fullHeightStorage = 160L + 48L * (Math.ceilDiv((long) height, 4) + 1);
assertEquals(0, capacity % 16);
assertTrue(capacity >= 16);
assertTrue(capacity <= 32_768);
assertTrue(capacity * fullHeightStorage <= 64L * 1024 * 1024);
}
}
}
@Test
public void sharedColumnWorkingSetReusesTerrainAcrossChunksAndClearsCompletely() {
AtomicInteger sourceCalls = new AtomicInteger();
AtomicInteger height = new AtomicInteger(96);
IrisBiome biome = new IrisBiome().setTerrain3D(profile());
Terrain3DRuntime runtime = new Terrain3DRuntime(
new Terrain3DRuntime.Sources((x, z) -> {
sourceCalls.incrementAndGet();
return height.get();
}, (x, z) -> biome),
new Terrain3DRuntime.Options(12, 256, 0, null, true, 65_536),
(style, seed) -> (x, y, z) -> 0D);
List<Terrain3DColumn> columns = new ArrayList<>(128 * 128);
for (int x = -64; x < 64; x++) {
for (int z = -64; z < 64; z++) {
Terrain3DColumn column = runtime.column(x, z);
assertTrue(column.shaped());
assertEquals(96, column.topY());
columns.add(column);
}
}
int callsBeforeRevisit = sourceCalls.get();
int index = 0;
for (int x = -64; x < 64; x++) {
for (int z = -64; z < 64; z++) {
assertSame(columns.get(index++), runtime.column(x, z));
}
}
assertEquals(callsBeforeRevisit, sourceCalls.get());
assertEquals(columns.size(), runtime.cachedColumnCount());
height.set(120);
runtime.clear();
assertEquals(0, runtime.cachedColumnCount());
assertEquals(120, runtime.column(-64, -64).topY());
assertTrue(sourceCalls.get() > callsBeforeRevisit);
}
@Test
public void sharedColumnCacheRemainsBoundedAcrossDistantWorkingSets() {
IrisBiome biome = new IrisBiome();
Terrain3DRuntime runtime = new Terrain3DRuntime(
new Terrain3DRuntime.Sources((x, z) -> 96D, (x, z) -> biome),
new Terrain3DRuntime.Options(12, 256, 0, null, true, 65_536),
(style, seed) -> (x, y, z) -> 0D);
for (int x = -65_536; x < 65_536; x++) {
assertEquals(96, runtime.column(x, -1).topY());
}
assertEquals(65_536, runtime.cachedColumnCount());
assertEquals(96, runtime.column(-65_536, -1).topY());
assertEquals(65_536, runtime.cachedColumnCount());
runtime.clear();
assertEquals(0, runtime.cachedColumnCount());
}
@Test
public void evictedAnchorsRecomputeIdenticalColumns() {
AtomicInteger samples = new AtomicInteger();
IrisBiome biome = new IrisBiome().setTerrain3D(profile());
Terrain3DRuntime runtime = new Terrain3DRuntime(
new Terrain3DRuntime.Sources((x, z) -> 96D, (x, z) -> biome),
new Terrain3DRuntime.Options(12, 256, 0, null, true, 16),
(style, seed) -> (x, y, z) -> {
samples.incrementAndGet();
return 0.2D * StrictMath.sin(x + z);
});
Terrain3DColumn original = runtime.column(-4, -4);
for (int index = 0; index < 512; index++) {
runtime.column(index * 4, -16);
}
int samplesBeforeRevisit = samples.get();
assertEquals(original, runtime.column(-4, -4));
assertTrue(samples.get() > samplesBeforeRevisit);
}
@Test
public void anchorNoiseSurvivesColumnCacheTurnover() {
AtomicInteger samples = new AtomicInteger();
AtomicInteger biomes = new AtomicInteger();
IrisBiome biome = new IrisBiome().setTerrain3D(profile());
Terrain3DRuntime runtime = new Terrain3DRuntime(
new Terrain3DRuntime.Sources((x, z) -> 96D, (x, z) -> {
biomes.incrementAndGet();
return biome;
}),
new Terrain3DRuntime.Options(12, 256, 0, null, true, 64),
(style, seed) -> (x, y, z) -> {
samples.incrementAndGet();
return 0.2D * StrictMath.sin(x + z);
});
List<Terrain3DColumn> columns = new ArrayList<>();
for (int x = -32; x < 32; x += 4) {
for (int z = -32; z < 32; z += 4) {
columns.add(runtime.column(x, z));
}
}
int initialSamples = samples.get();
int initialBiomes = biomes.get();
int index = 0;
for (int x = -32; x < 32; x += 4) {
for (int z = -32; z < 32; z += 4) {
assertEquals(columns.get(index++), runtime.column(x, z));
}
}
assertTrue(initialSamples > 0);
assertEquals(initialSamples, samples.get());
assertEquals(initialBiomes, biomes.get());
runtime.clear();
assertEquals(columns.getFirst(), runtime.column(-32, -32));
assertTrue(samples.get() > initialSamples);
assertTrue(biomes.get() > initialBiomes);
}
@Test
public void columnCacheReusesGeometryAndAnchorNoise() {
AtomicInteger samples = new AtomicInteger();
@@ -0,0 +1,66 @@
package art.arcane.iris.pack;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class PackValidatorTerrainSamplingTest {
@Rule
public TemporaryFolder temporary = new TemporaryFolder();
@Test
public void acceptsOmittedAndSupportedSamplingSteps() throws Exception {
assertTrue(validate("terrainSamplingStep", null).isLoadable());
for (String field : new String[]{"terrainSamplingStep", "caveDensitySamplingStep"}) {
for (String value : new String[]{"1", "2", "4", "8"}) {
PackValidationResult result = validate(field, value);
assertTrue(result.getBlockingErrors().toString(), result.isLoadable());
}
}
}
@Test
public void rejectsInvalidSamplingStepsBeforeWorldCreation() throws Exception {
for (String field : new String[]{"terrainSamplingStep", "caveDensitySamplingStep"}) {
for (String value : new String[]{"0", "-1", "3", "16", "4.5", "\"4\"", "null", "true"}) {
PackValidationResult result = validate(field, value);
assertFalse("Accepted " + field + "=" + value, result.isLoadable());
assertTrue(result.getBlockingErrors().contains(
"Dimension 'main' " + field + " must be one of 1, 2, 4, or 8."));
}
}
}
@Test
public void acceptsOnlySupportedBiomeBoundsSamplingSteps() throws Exception {
assertTrue(validate("biomeBoundsSamplingStep", null).isLoadable());
for (String value : new String[]{"4", "8", "16", "32"}) {
PackValidationResult result = validate("biomeBoundsSamplingStep", value);
assertTrue(result.getBlockingErrors().toString(), result.isLoadable());
}
for (String value : new String[]{"0", "-1", "1", "2", "3", "5", "64", "8.5", "\"8\"", "null", "true"}) {
PackValidationResult result = validate("biomeBoundsSamplingStep", value);
assertFalse("Accepted biomeBoundsSamplingStep=" + value, result.isLoadable());
assertTrue(result.getBlockingErrors().contains(
"Dimension 'main' biomeBoundsSamplingStep must be one of 4, 8, 16, or 32."));
}
}
private PackValidationResult validate(String field, String value) throws Exception {
Path pack = temporary.newFolder().toPath();
Files.createDirectories(pack.resolve("dimensions"));
Files.createDirectories(pack.resolve("regions"));
Files.createDirectories(pack.resolve("biomes"));
Files.writeString(pack.resolve("dimensions/main.json"), "{\"regions\":[\"main\"]"
+ (value == null ? "" : ",\"" + field + "\":" + value) + "}");
Files.writeString(pack.resolve("regions/main.json"), "{\"landBiomes\":[\"main\"]}");
Files.writeString(pack.resolve("biomes/main.json"), "{\"name\":\"Main\"}");
return PackValidator.validate(pack.toFile());
}
}
@@ -0,0 +1,46 @@
package art.arcane.iris.pack.schema;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import com.google.gson.Gson;
import org.junit.Test;
import java.lang.reflect.Field;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
public class IrisBiomeBoundsSamplingSchemaTest {
@Test
public void omittedSettingKeepsFourAndExplicitValuesRoundTrip() {
Gson gson = new Gson();
assertEquals(4, gson.fromJson("{}", IrisDimension.class).getBiomeBoundsSamplingStep());
for (int step : new int[]{4, 8, 16, 32}) {
IrisDimension dimension = new IrisDimension().setBiomeBoundsSamplingStep(step);
assertEquals(step, gson.fromJson(gson.toJson(dimension), IrisDimension.class).getBiomeBoundsSamplingStep());
}
for (int step : new int[]{0, 1, 2, 3, 5, 64}) {
assertThrows(IllegalArgumentException.class,
() -> new IrisDimension().setBiomeBoundsSamplingStep(step).getBiomeBoundsSamplingStep());
}
}
@Test
public void schemaListsOnlySupportedIntegerSteps() throws Exception {
SchemaBuilder builder = new SchemaBuilder(IrisDimension.class, null);
for (String name : new String[]{"biomeBoundsSamplingStep", "terrainSamplingStep", "caveDensitySamplingStep"}) {
int minimum = name.equals("biomeBoundsSamplingStep") ? 4 : 1;
Field field = IrisDimension.class.getDeclaredField(name);
JSONObject property = new SchemaPropertyBuilder(builder).buildProperty(field, IrisDimension.class);
assertEquals("integer", property.getString("type"));
assertEquals(minimum, property.getInt("minimum"));
assertEquals(minimum * 8, property.getInt("maximum"));
JSONArray options = property.getJSONArray("enum");
assertEquals(4, options.length());
for (int index = 0; index < options.length(); index++) {
assertEquals(minimum << index, options.getInt(index));
}
}
}
}
@@ -78,7 +78,7 @@ public class SchemaBuilderIdentityTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final String EXPECTED_SCHEMA_DIGEST = "415345947f2c96fc468848aefa064854a085ae5631ea0f780c77c99973bff594";
private static final String EXPECTED_SCHEMA_DIGEST = "9c118c18c2f757f08ffb71d52d8549c53e4dae4939e0e030d36e4e9115794497";
private static final List<Class<?>> SCHEMA_ROOTS = List.of(
IrisDimension.class,
@@ -52,6 +52,59 @@ public class BoundaryColumnGeometryTest {
assertEquals(50, geometry.surfaceOffsetNear(70D));
}
@Test
public void nearestSurfaceMatchesDenseColumnsAcrossRunsProtectionAndTies() {
List<BoundaryColumnGeometry.Voxel> states = List.of(STONE, AIR,
new BoundaryColumnGeometry.Voxel("minecraft:oak_log[axis=y]",
BoundaryColumnGeometry.Phase.SOLID, "", true),
new BoundaryColumnGeometry.Voxel("minecraft:water[level=0]",
BoundaryColumnGeometry.Phase.FLUID, "minecraft:water[level=0]", false));
for (int combination = 0; combination < 4096; combination++) {
int encoded = combination;
ArrayList<BoundaryColumnGeometry.Voxel> voxels = new ArrayList<>(18);
for (int run = 0; run < 6; run++) {
BoundaryColumnGeometry.Voxel voxel = states.get(encoded & 3);
encoded >>= 2;
for (int offset = 0; offset <= run % 3; offset++) {
voxels.add(voxel);
}
}
BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-64, voxels);
for (double expected : new double[]{-10D, 0D, 1.5D, 3D, 5.5D, 8D, 12D, 100D}) {
assertEquals(denseSurface(voxels, expected), geometry.surfaceOffsetNear(expected));
}
}
assertThrows(IllegalArgumentException.class, () -> BoundaryColumnGeometry.empty().surfaceOffsetNear(0D));
BoundaryColumnGeometry solid = BoundaryColumnGeometry.fromVoxels(-64, List.of(STONE));
assertThrows(IllegalArgumentException.class, () -> solid.surfaceOffsetNear(Double.NaN));
assertThrows(IllegalArgumentException.class, () -> solid.surfaceOffsetNear(Double.POSITIVE_INFINITY));
}
private static int denseSurface(List<BoundaryColumnGeometry.Voxel> voxels, double expected) {
int selected = -1;
double distance = Double.POSITIVE_INFINITY;
for (int index = 0; index + 1 < voxels.size(); index++) {
BoundaryColumnGeometry.Voxel current = voxels.get(index);
BoundaryColumnGeometry.Voxel above = voxels.get(index + 1);
if (current.phase() == BoundaryColumnGeometry.Phase.SOLID && !current.protectedContent()
&& (above.phase() != BoundaryColumnGeometry.Phase.SOLID || above.protectedContent())
&& Math.abs(index - expected) < distance) {
selected = index;
distance = Math.abs(index - expected);
}
}
if (selected >= 0) {
return selected;
}
for (int index = voxels.size() - 1; index >= 0; index--) {
BoundaryColumnGeometry.Voxel voxel = voxels.get(index);
if (voxel.phase() == BoundaryColumnGeometry.Phase.SOLID && !voxel.protectedContent()) {
return index;
}
}
return 0;
}
@Test
public void enclosedOpenQueriesIncludeUpperCavesAndExcludeOpenSky() {
BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(-4,
@@ -369,6 +369,10 @@ public final class GenerationHistoryRuntimeRouterRoutingTest extends GenerationH
GenerationHistoryRuntimeRouter.RuntimeRoute route = router.openRoute(4, -3);
assertThrows(IllegalStateException.class, route::claimGeneratedSemantics);
try (GenerationHistoryRuntimeRouter.RuntimeRoute other = router.openRoute(5, -3);
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope ignored = other.openRuntimeScope()) {
assertThrows(IllegalStateException.class, route::claimGeneratedSemantics);
}
try (GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope ignored = route.openRuntimeScope()) {
assertTrue(route.claimGeneratedSemantics());
assertFalse(route.claimGeneratedSemantics());
@@ -3,6 +3,19 @@ package art.arcane.iris.world.history;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EnginePlatformHooks;
import art.arcane.iris.generation.runtime.DimensionStackContext;
import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.volmlib.util.stream.ProceduralStream;
import art.arcane.volmlib.util.stream.interpolation.Interpolated;
import art.arcane.iris.generation.terrain.IrisDimensionCarvingResolver;
import art.arcane.iris.generation.terrain.IrisDimensionCarvingEntry;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.loading.ResourceLoader;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.iris.world.IrisWorld;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.iris.structure.placement.StructurePlacementMarker;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.generation.mantle.EngineMantle;
@@ -23,13 +36,18 @@ import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
@@ -47,6 +65,211 @@ public final class GenerationSemanticCaptureTest {
}
}
@Test
@SuppressWarnings("unchecked")
public void scopedCaptureKeepsExplicitPrecedenceAndScalarFallback() {
Fixture fixture = new Fixture();
IrisBiome configured = new IrisBiome();
configured.setLoadKey("iris:configured");
IrisBiome fallback = new IrisBiome();
fallback.setLoadKey("iris:fallback");
IrisData data = mock(IrisData.class);
ResourceLoader<IrisBiome> loader = mock(ResourceLoader.class);
when(data.getBiomeLoader()).thenReturn(loader);
when(loader.load("iris:configured")).thenReturn(configured);
IrisDimensionCarvingEntry root = new IrisDimensionCarvingEntry();
root.setBiome("iris:configured");
root.setWorldYRange(new IrisRange(-64, -48));
root.setChildRecursionDepth(0);
IrisDimension dimension = new IrisDimension();
dimension.setCarving(new KList<>(List.of(root)));
when(fixture.engine.getDimension()).thenReturn(dimension);
when(fixture.engine.getData()).thenReturn(data);
when(fixture.engine.getWorld()).thenReturn(IrisWorld.builder().minHeight(-64).maxHeight(320).build());
when(fixture.engine.getCaveBiome(anyInt(), anyInt(), anyInt(), any(IrisDimensionCarvingResolver.State.class)))
.thenAnswer(invocation -> (int) invocation.getArgument(1) <= 16 ? configured : fallback);
doAnswer(invocation -> {
Consumer4<Integer, Integer, Integer, MatterCavern> consumer = invocation.getArgument(3);
for (int y = 0; y < 64; y++) {
consumer.accept(1, y, 2, new MatterCavern(true, y == 3 ? "iris:explicit" : null, (byte) 0));
}
return null;
}).when(fixture.mantle).iterateChunk(eq(-2), eq(3), eq(MatterCavern.class), any());
HydrologyCaveCell duplicate = mock(HydrologyCaveCell.class);
when(duplicate.floodedBiomeKey()).thenReturn("iris:duplicate");
when(duplicate.fluidProfileKey()).thenReturn("iris:river");
doAnswer(invocation -> {
Consumer4<Integer, Integer, Integer, HydrologyCaveCell> consumer = invocation.getArgument(3);
consumer.accept(1, 4, 2, duplicate);
return null;
}).when(fixture.mantle).iterateChunk(eq(-2), eq(3), eq(HydrologyCaveCell.class), any());
GenerationHistory.GenerationStage stage = mock(GenerationHistory.GenerationStage.class);
GenerationActivation activation = mock(GenerationActivation.class);
when(activation.activationId()).thenReturn(5L);
when(stage.activation()).thenReturn(activation);
when(stage.chunkX()).thenReturn(-2);
when(stage.chunkZ()).thenReturn(3);
ChunkGenerationSemantics expected = GenerationSemanticCapture.capture(fixture.engine, stage, (x, y, z) -> y != 8);
verify(fixture.engine, times(62)).getCaveBiome(anyInt(), anyInt(), anyInt(), any(IrisDimensionCarvingResolver.State.class));
clearInvocations(fixture.engine);
ChunkGenerationSemantics actual = GenerationSemanticCapture.captureScoped(fixture.engine, stage, (x, y, z) -> y != 8);
assertEquals(expected, actual);
assertEquals(Set.of("iris:configured", "iris:fallback", "iris:explicit"), actual.caveBiomeKeys());
verify(fixture.engine, times(47)).getCaveBiome(anyInt(), anyInt(), anyInt(), any(IrisDimensionCarvingResolver.State.class));
}
@Test
public void scopedFallbackMatchesScalarDepthAndStackBiomesWithOneSamplePerColumn() {
IrisEngine engine = mock(IrisEngine.class);
Fixture fixture = new Fixture(engine);
IrisComplex complex = engine.getComplex();
EnginePlatformHooks hooks = mock(EnginePlatformHooks.class);
when(engine.getPlatformHooks()).thenReturn(hooks);
when(engine.hasGenerationRuntimeScope()).thenReturn(true);
IrisDimension dimension = new IrisDimension();
IrisData data = mock(IrisData.class);
IrisWorld world = IrisWorld.builder().minHeight(-64).maxHeight(320).build();
when(engine.getDimension()).thenReturn(dimension);
when(engine.getData()).thenReturn(data);
when(engine.getWorld()).thenReturn(world);
DimensionStackContext stack = mock(DimensionStackContext.class);
when(engine.getDimensionStackContext()).thenReturn(stack);
IrisBiome[] surfaces = new IrisBiome[256];
IrisBiome[] caves = new IrisBiome[256];
for (int index = 0; index < 256; index++) {
surfaces[index] = biome("surface:" + index, 7);
caves[index] = index % 3 == 0 ? null : biome(index % 3 == 1 ? null : "cave:" + index, 5);
}
when(complex.getTrueBiomeStream()).thenReturn(ProceduralStream.of((x, z) -> surfaces[column(x, z)], Interpolated.of(value -> 0D, value -> null)));
when(complex.getCaveBiomeStream()).thenReturn(ProceduralStream.of((x, z) -> caves[column(x, z)], Interpolated.of(value -> 0D, value -> null)));
AtomicInteger heightSamples = new AtomicInteger();
when(complex.getHeightStream()).thenReturn(ProceduralStream.of((x, z) -> {
heightSamples.incrementAndGet();
return 40D + (column(x, z) % 8);
}, Interpolated.DOUBLE));
when(stack.getLayout(anyInt(), anyInt())).thenAnswer(invocation -> {
int index = column((int) invocation.getArgument(0), (int) invocation.getArgument(1));
DimensionStackLayout layout = mock(DimensionStackLayout.class);
DimensionStackLayout.Layer layer = mock(DimensionStackLayout.Layer.class);
when(layer.biome()).thenReturn(index % 2 == 0 ? biome("stack:" + index, 3) : null);
when(layout.surfaceLayer()).thenReturn(layer);
return layout;
});
Engine scalar = mock(Engine.class, CALLS_REAL_METHODS);
doReturn(complex).when(scalar).getComplex();
doReturn(dimension).when(scalar).getDimension();
doReturn(stack).when(scalar).getDimensionStackContext();
doReturn(data).when(scalar).getData();
doReturn(world).when(scalar).getWorld();
doReturn(hooks).when(scalar).getPlatformHooks();
Set<String> expected = new HashSet<>();
IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State();
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
int index = (x << 4) | z;
int surfaceY = 40 + index % 8;
int y = surfaceY - index % 10;
expected.add(scalar.getCaveBiome(x - 32, y, z + 48, state).getLoadKey());
expected.add(scalar.getCaveBiome(x - 32, y + 1, z + 48, state).getLoadKey());
}
}
heightSamples.set(0);
doAnswer(invocation -> {
Consumer4<Integer, Integer, Integer, MatterCavern> consumer = invocation.getArgument(3);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
int index = (x << 4) | z;
int y = 40 + index % 8 - index % 10;
consumer.accept(x, y, z, new MatterCavern(true, null, (byte) 0));
consumer.accept(x, y + 1, z, new MatterCavern(true, null, (byte) 0));
}
}
return null;
}).when(fixture.mantle).iterateChunk(eq(-2), eq(3), eq(MatterCavern.class), any());
ChunkGenerationSemantics captured = GenerationSemanticCapture.captureScoped(engine, stage(), (x, y, z) -> true);
assertEquals(expected, captured.caveBiomeKeys());
assertEquals(256, heightSamples.get());
verify(engine, never()).getCaveBiome(anyInt(), anyInt(), anyInt(), any(IrisDimensionCarvingResolver.State.class));
}
@Test
public void scopedFallbackRetainsCapturedRuntimeWhenAmbientInputsChange() {
IrisEngine engine = mock(IrisEngine.class);
Fixture fixture = new Fixture(engine);
IrisComplex capturedComplex = engine.getComplex();
EnginePlatformHooks hooks = mock(EnginePlatformHooks.class);
when(engine.getPlatformHooks()).thenReturn(hooks);
when(engine.hasGenerationRuntimeScope()).thenReturn(true);
when(engine.getDimension()).thenReturn(new IrisDimension());
when(engine.getData()).thenReturn(mock(IrisData.class));
when(engine.getWorld()).thenReturn(IrisWorld.builder().minHeight(-64).maxHeight(320).build());
when(capturedComplex.getTrueBiomeStream()).thenReturn(ProceduralStream.of((x, z) -> biome("captured:surface", 0), Interpolated.of(value -> 0D, value -> null)));
when(capturedComplex.getCaveBiomeStream()).thenReturn(ProceduralStream.of((x, z) -> biome("captured:cave", 5), Interpolated.of(value -> 0D, value -> null)));
when(capturedComplex.getHeightStream()).thenReturn(ProceduralStream.of((x, z) -> 40D, Interpolated.DOUBLE));
IrisComplex changedComplex = mock(IrisComplex.class);
doAnswer(invocation -> {
Consumer4<Integer, Integer, Integer, MatterCavern> consumer = invocation.getArgument(3);
consumer.accept(0, 40, 0, new MatterCavern(true, null, (byte) 0));
when(engine.getComplex()).thenReturn(changedComplex);
when(engine.getDimension()).thenReturn(new IrisDimension());
when(engine.getData()).thenReturn(mock(IrisData.class));
consumer.accept(0, 35, 0, new MatterCavern(true, null, (byte) 0));
consumer.accept(15, 20, 15, new MatterCavern(true, null, (byte) 0));
return null;
}).when(fixture.mantle).iterateChunk(eq(-2), eq(3), eq(MatterCavern.class), any());
ChunkGenerationSemantics captured = GenerationSemanticCapture.captureScoped(engine, stage(), (x, y, z) -> true);
assertEquals(Set.of("captured:surface", "captured:cave"), captured.caveBiomeKeys());
verify(changedComplex, never()).getCaveBiomeStream();
verify(changedComplex, never()).getHeightStream();
verify(engine, never()).getCaveBiome(anyInt(), anyInt(), anyInt(), any(IrisDimensionCarvingResolver.State.class));
}
private static IrisBiome biome(String key, int minimumDepth) {
IrisBiome biome = new IrisBiome();
biome.setLoadKey(key);
biome.setCaveMinDepthBelowSurface(minimumDepth);
return biome;
}
private static int column(double x, double z) {
return (((int) x & 15) << 4) | ((int) z & 15);
}
private static GenerationHistory.GenerationStage stage() {
GenerationHistory.GenerationStage stage = mock(GenerationHistory.GenerationStage.class);
GenerationActivation activation = mock(GenerationActivation.class);
when(activation.activationId()).thenReturn(5L);
when(stage.activation()).thenReturn(activation);
when(stage.chunkX()).thenReturn(-2);
when(stage.chunkZ()).thenReturn(3);
return stage;
}
@Test
public void unscopedCapturePreservesCustomThreeArgumentCaveBiomeDispatch() {
Fixture fixture = new Fixture();
IrisBiome custom = biome("custom:cave", 0);
doReturn(custom).when(fixture.engine).getCaveBiome(-31, 20, 50);
doAnswer(invocation -> {
Consumer4<Integer, Integer, Integer, MatterCavern> consumer = invocation.getArgument(3);
consumer.accept(1, 20, 2, new MatterCavern(true, null, (byte) 0));
return null;
}).when(fixture.mantle).iterateChunk(eq(-2), eq(3), eq(MatterCavern.class), any());
clearInvocations(fixture.engine);
ChunkGenerationSemantics captured = GenerationSemanticCapture.capture(fixture.engine, -2, 3, 5L);
assertEquals(Set.of("custom:cave"), captured.caveBiomeKeys());
verify(fixture.engine).getCaveBiome(-31, 20, 50);
verify(fixture.engine, never()).getCaveBiome(anyInt(), anyInt(), anyInt(), any(IrisDimensionCarvingResolver.State.class));
}
@Test
public void captureKeepsWorldPoiCoordinatesAndInternalHeight() {
Fixture fixture = new Fixture();
@@ -103,7 +326,7 @@ public final class GenerationSemanticCaptureTest {
new ChunkGenerationSemantics.BlockPosition(3, 11, 4),
new ChunkGenerationSemantics.BlockPosition(5, 12, 6)), checked);
verify(filled, never()).fluidProfileKey();
verify(fixture.engine, never()).getCaveBiome(anyInt(), anyInt(), anyInt());
verify(fixture.engine, never()).getCaveBiome(anyInt(), anyInt(), anyInt(), any(IrisDimensionCarvingResolver.State.class));
}
@Test
@@ -112,7 +335,7 @@ public final class GenerationSemanticCaptureTest {
List<Integer> heights = List.of(0, 15, 16, 255, 256, 767, 4063);
Set<String> expected = new HashSet<>();
List<ChunkGenerationSemantics.BlockPosition> resolved = new ArrayList<>();
when(fixture.engine.getCaveBiome(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
when(fixture.engine.getCaveBiome(anyInt(), anyInt(), anyInt(), any(IrisDimensionCarvingResolver.State.class))).thenAnswer(invocation -> {
int x = invocation.getArgument(0);
int y = invocation.getArgument(1);
int z = invocation.getArgument(2);
@@ -181,11 +404,19 @@ public final class GenerationSemanticCaptureTest {
}
private static final class Fixture {
private final Engine engine = mock(Engine.class);
private final Engine engine;
@SuppressWarnings("unchecked")
private final Mantle<Matter> mantle = mock(Mantle.class);
private Fixture() {
this(mock(Engine.class));
}
private Fixture(Engine engine) {
this.engine = engine;
IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State();
when(engine.getCaveBiome(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> engine.getCaveBiome(
invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2), state));
IrisComplex complex = mock(IrisComplex.class);
EngineMantle engineMantle = mock(EngineMantle.class);
when(engine.getComplex()).thenReturn(complex);
@@ -250,25 +250,25 @@ public class SavedBiomeStoreBatchTest {
private static List<Future<ClaimResult>> enqueue(SavedBiomeStore store, Path root, BatchControl control,
ExecutorService workers, List<SavedBiomeChunk> chunks) throws Exception {
Object stripe = regionLock(store, 0, 0);
Field writingField = stripe.getClass().getDeclaredField("writing");
Field pendingField = stripe.getClass().getDeclaredField("pending");
writingField.setAccessible(true);
pendingField.setAccessible(true);
Queue<?> pending = (Queue<?>) pendingField.get(stripe);
ArrayList<Future<ClaimResult>> results = new ArrayList<>(chunks.size());
synchronized (stripe) {
writingField.setBoolean(stripe, true);
try {
for (SavedBiomeChunk chunk : chunks) {
results.add(workers.submit(() -> claim(store, root, control, chunk)));
}
Await.reached("every claim to queue behind the region write", Duration.ofSeconds(5L),
() -> pending.size() == chunks.size());
assertEquals(chunks.size(), pending.size());
} finally {
writingField.setBoolean(stripe, false);
int uncachedClaims = 0;
for (SavedBiomeChunk chunk : chunks) {
if (store.cached(chunk.chunkX(), chunk.chunkZ()).isEmpty()) {
uncachedClaims++;
}
}
int expectedQueuedClaims = uncachedClaims;
synchronized (stripe) {
for (SavedBiomeChunk chunk : chunks) {
results.add(workers.submit(() -> claim(store, root, control, chunk)));
}
Await.reached("uncached claims to queue before acquiring the region write lock", Duration.ofSeconds(5L),
() -> pending.size() == expectedQueuedClaims);
assertEquals(expectedQueuedClaims, pending.size());
}
return results;
}
@@ -16,6 +16,24 @@ import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class AsyncPregenMethodConcurrencyCapTest {
@Test
public void pregenWorkerTargetExpandsOnlyOptedInHighHeapJobs() {
assertEquals(16, AsyncPregenMethod.pregenWorkerThreadTarget(16, (16L << 30) - 1L, true));
assertEquals(32, AsyncPregenMethod.pregenWorkerThreadTarget(16, 16L << 30, true));
assertEquals(32, AsyncPregenMethod.pregenWorkerThreadTarget(24, 16L << 30, true));
assertEquals(64, AsyncPregenMethod.pregenWorkerThreadTarget(64, 16L << 30, true));
assertEquals(16, AsyncPregenMethod.pregenWorkerThreadTarget(16, 32L << 30, false));
assertEquals(2, AsyncPregenMethod.pregenWorkerThreadTarget(1, 8L << 30, true));
assertEquals(Integer.MAX_VALUE, AsyncPregenMethod.pregenWorkerThreadTarget(Integer.MAX_VALUE, Long.MAX_VALUE, true));
int target = AsyncPregenMethod.pregenWorkerThreadTarget(16, 16L << 30, true);
int workers = AsyncPregenMethod.resolvePaperLikeConcurrencyWorkerThreads(4, 16, target);
assertEquals(32, workers);
assertEquals(256, AsyncPregenMethod.computePaperLikeRecommendedCap(workers));
assertEquals(256, AsyncPregenMethod.computeInitialInFlightLimit(256, workers));
assertEquals(48, AsyncPregenMethod.resolvePaperLikeConcurrencyWorkerThreads(48, 16, target));
assertEquals(1, AsyncPregenMethod.selectConcurrencyCap(256, true));
}
@Test
public void paperLikeRecommendedCapTracksWorkerThreads() {
assertEquals(16, AsyncPregenMethod.computePaperLikeRecommendedCap(1));
@@ -57,11 +75,16 @@ public class AsyncPregenMethodConcurrencyCapTest {
}
@Test
public void coldStartMatchesAvailableWorkersBeforeAdaptiveGrowth() {
public void coldStartFillsChunkStatusPipelineWithinHardCap() {
assertEquals(1, AsyncPregenMethod.computeInitialInFlightLimit(1, 16));
assertEquals(4, AsyncPregenMethod.computeInitialInFlightLimit(128, 1));
assertEquals(16, AsyncPregenMethod.computeInitialInFlightLimit(128, 16));
assertEquals(8, AsyncPregenMethod.computeInitialInFlightLimit(128, 1));
assertEquals(128, AsyncPregenMethod.computeInitialInFlightLimit(128, 16));
assertEquals(6, AsyncPregenMethod.computeInitialInFlightLimit(6, 16));
assertEquals(64, AsyncPregenMethod.computeInitialInFlightLimit(64, 8));
assertEquals(192, AsyncPregenMethod.computeInitialInFlightLimit(192, 64));
assertEquals(256, AsyncPregenMethod.computeInitialInFlightLimit(256, Integer.MAX_VALUE));
assertEquals(8, AsyncPregenMethod.computeInitialInFlightLimit(128, Integer.MIN_VALUE));
assertEquals(1, AsyncPregenMethod.computeInitialInFlightLimit(0, 16));
}
@Test
@@ -0,0 +1,178 @@
package art.arcane.iris.world.pregen;
import art.arcane.iris.world.lifecycle.CapabilitySnapshot;
import art.arcane.iris.world.lifecycle.WorldLifecycleService;
import art.arcane.volmlib.nativelib.terrain.NativeWorkerPool;
import art.arcane.volmlib.nativelib.terrain.NativeWorldRuntime;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
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.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
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 AsyncPregenWorkerBoostTest {
@Test
public void nestedTargetsEscalateAndRestoreOnlyAfterLastRelease() throws Exception {
try (Fixture fixture = new Fixture(4)) {
AsyncPregenMethod.acquireWorkerThreadBoost(16);
AsyncPregenMethod.acquireWorkerThreadBoost(32);
AsyncPregenMethod.acquireWorkerThreadBoost(16);
assertEquals(32, fixture.threads.get());
AsyncPregenMethod.releaseWorkerThreadBoost();
AsyncPregenMethod.releaseWorkerThreadBoost();
assertEquals(32, fixture.threads.get());
AsyncPregenMethod.releaseWorkerThreadBoost();
assertEquals(4, fixture.threads.get());
AsyncPregenMethod.releaseWorkerThreadBoost();
assertEquals(List.of(16, 32, 4), fixture.adjustments);
AsyncPregenMethod.acquireWorkerThreadBoost(32);
AsyncPregenMethod.releaseWorkerThreadBoost();
assertEquals(List.of(16, 32, 4, 32, 4), fixture.adjustments);
}
}
@Test
public void explicitHigherNativeCountIsNeverReduced() throws Exception {
try (Fixture fixture = new Fixture(48)) {
AsyncPregenMethod.acquireWorkerThreadBoost(32);
AsyncPregenMethod.acquireWorkerThreadBoost(16);
AsyncPregenMethod.releaseWorkerThreadBoost();
AsyncPregenMethod.releaseWorkerThreadBoost();
assertEquals(48, fixture.threads.get());
assertTrue(fixture.adjustments.isEmpty());
assertThrows(IllegalArgumentException.class, () -> AsyncPregenMethod.acquireWorkerThreadBoost(0));
}
}
@Test
public void reacquisitionWaitsForPreviousRestorationAndKeepsItsOwnHold() throws Exception {
try (Fixture fixture = new Fixture(4); ExecutorService executor = Executors.newFixedThreadPool(2)) {
AsyncPregenMethod.acquireWorkerThreadBoost(16);
CountDownLatch restoring = new CountDownLatch(1);
CountDownLatch finishRestore = new CountDownLatch(1);
CountDownLatch acquiring = new CountDownLatch(1);
AtomicBoolean waitOnce = new AtomicBoolean(true);
doAnswer(invocation -> {
int count = invocation.getArgument(0);
if (count == 4 && waitOnce.compareAndSet(true, false)) {
restoring.countDown();
assertTrue(finishRestore.await(5L, TimeUnit.SECONDS));
}
fixture.adjustments.add(count);
fixture.threads.set(count);
return null;
}).when(fixture.pool).adjustThreadCount(anyInt());
Future<?> released = executor.submit(() -> fixture.withBinding(AsyncPregenMethod::releaseWorkerThreadBoost));
try {
assertTrue(restoring.await(5L, TimeUnit.SECONDS));
Future<?> acquired = executor.submit(() -> fixture.withBinding(() -> {
acquiring.countDown();
AsyncPregenMethod.acquireWorkerThreadBoost(32);
}));
assertTrue(acquiring.await(5L, TimeUnit.SECONDS));
assertFalse(acquired.isDone());
finishRestore.countDown();
released.get(5L, TimeUnit.SECONDS);
acquired.get(5L, TimeUnit.SECONDS);
assertEquals(32, fixture.threads.get());
AsyncPregenMethod.releaseWorkerThreadBoost();
assertEquals(4, fixture.threads.get());
} finally {
finishRestore.countDown();
}
}
}
@Test
public void closeRestoresWorkersWhenDrainFailsAndPreservesCancellationInterrupt() throws Exception {
try (Fixture fixture = new Fixture(4)) {
AsyncPregenMethod.acquireWorkerThreadBoost(32);
AsyncPregenMethod method = mock(AsyncPregenMethod.class, CALLS_REAL_METHODS);
PregenAdmissionGate admission = mock(PregenAdmissionGate.class);
IllegalStateException failure = new IllegalStateException("drain failed");
when(admission.awaitDrain(anyLong(), any(TimeUnit.class), any(Runnable.class))).thenThrow(failure);
set(method, "closing", new AtomicBoolean());
set(method, "holdsWorkerBoost", new AtomicBoolean(true));
set(method, "admission", admission);
Thread.currentThread().interrupt();
try {
assertSame(failure, assertThrows(IllegalStateException.class, method::close));
assertEquals(4, fixture.threads.get());
assertTrue(Thread.currentThread().isInterrupted());
} finally {
Thread.interrupted();
}
assertSame(failure, assertThrows(IllegalStateException.class, method::close));
assertEquals(List.of(32, 4), fixture.adjustments);
}
}
private static void set(AsyncPregenMethod method, String name, Object value) throws Exception {
Field field = AsyncPregenMethod.class.getDeclaredField(name);
field.setAccessible(true);
field.set(method, value);
}
private static final class Fixture implements AutoCloseable {
private final WorldLifecycleService service = mock(WorldLifecycleService.class);
private final NativeWorkerPool pool = mock(NativeWorkerPool.class);
private final AtomicInteger threads;
private final List<Integer> adjustments = Collections.synchronizedList(new ArrayList<>());
private final MockedStatic<WorldLifecycleService> lifecycle = mockStatic(WorldLifecycleService.class);
private Fixture(int initialThreads) throws Exception {
threads = new AtomicInteger(initialThreads);
CapabilitySnapshot capabilities = mock(CapabilitySnapshot.class);
NativeWorldRuntime runtime = mock(NativeWorldRuntime.class);
when(service.capabilities()).thenReturn(capabilities);
when(capabilities.nativeRuntime()).thenReturn(runtime);
when(runtime.workers()).thenReturn(pool);
when(pool.threadCount()).thenAnswer(invocation -> threads.get());
doAnswer(invocation -> {
int count = invocation.getArgument(0);
adjustments.add(count);
threads.set(count);
return null;
}).when(pool).adjustThreadCount(anyInt());
lifecycle.when(WorldLifecycleService::get).thenReturn(service);
}
private void withBinding(Runnable action) {
try (MockedStatic<WorldLifecycleService> binding = mockStatic(WorldLifecycleService.class)) {
binding.when(WorldLifecycleService::get).thenReturn(service);
action.run();
}
}
@Override
public void close() {
for (int index = 0; index < 4; index++) {
AsyncPregenMethod.releaseWorkerThreadBoost();
}
lifecycle.close();
}
}
}