Merged locks and caches

- Lock-free history scope, shared subterrain plan caches, stateless cave biome hash, custom biome key cache
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-21 21:15:07 -04:00
commit 6c7896f2a4
12 files changed
+402 -82

No files matched your search

@@ -177,7 +177,7 @@ public class IrisEngine implements Engine {
volatile GenerationHistoryRuntimeRouter generationHistoryRuntimeRouter;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private boolean generationHistoryRoutingRequired;
private volatile boolean generationHistoryRoutingRequired;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
volatile EngineTarget publishedTarget;
@@ -1011,13 +1011,7 @@ public class IrisEngine implements Engine {
}
public Optional<GenerationHistoryRuntimeRouter> getGenerationHistoryRuntimeRouter() {
GenerationHistoryRuntimeRouter router = generationHistoryRuntimeRouter;
if (router != null) {
return Optional.of(router);
}
synchronized (generationHistoryRuntimeRouterLock) {
return Optional.ofNullable(generationHistoryRuntimeRouter);
}
return Optional.ofNullable(generationHistoryRuntimeRouter);
}
public GenerationHistoryRuntimeRouter.CoordinateScope openGenerationHistoryCoordinateScope(
@@ -1028,13 +1022,14 @@ public class IrisEngine implements Engine {
if (router != null) {
return router.openCoordinateScope(blockX, blockZ);
}
synchronized (generationHistoryRuntimeRouterLock) {
router = generationHistoryRuntimeRouter;
if (router == null && generationHistoryRoutingRequired) {
throw new IllegalStateException("Iris generation-history runtime router is detached.");
}
if (!generationHistoryRoutingRequired) {
return null;
}
return router == null ? null : router.openCoordinateScope(blockX, blockZ);
router = generationHistoryRuntimeRouter;
if (router == null) {
throw new IllegalStateException("Iris generation-history runtime router is detached.");
}
return router.openCoordinateScope(blockX, blockZ);
}
public boolean hasGenerationRuntimeScope() {
@@ -79,15 +79,20 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
private static final int CAVE_BIOME_BLEND_CENTER_WEIGHT = 4;
private static final int CAVE_BIOME_BLEND_TOTAL_WEIGHT = 8;
private static final int SUBMERGED_FLOOR_SUBSTRATE_DEPTH = 8;
private static final long RANDOM_MULTIPLIER = 0x5DEECE66DL;
private static final long RANDOM_ADDEND = 0xBL;
private static final long RANDOM_MASK = (1L << 48) - 1;
private static final MatterCavern BASIC_CAVERN = new MatterCavern(true, "", (byte) 0);
private final RNG rng;
private final long caveBlendSeed;
private final NativeBlockState AIR = B.getState("CAVE_AIR");
private final NativeBlockState LAVA = B.getState("LAVA");
private final IrisDecorantActuator decorant;
public IrisCarveModifier(Engine engine) {
super(engine, "Carve");
rng = new RNG(getEngine().getSeedManager().getCarve());
caveBlendSeed = getEngine().getSeedManager().getCarve();
rng = new RNG(caveBlendSeed);
decorant = new IrisDecorantActuator(engine);
}
@@ -97,9 +102,9 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
PrecisionStopwatch caveStopwatch = PrecisionStopwatch.start();
Mantle<Matter> mantle = getEngine().getMantle().getMantle();
CaveInputs resolverState = new CaveInputs(getEngine());
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache = new Long2ObjectOpenHashMap<>(2048);
IrisCarveScratch scratch = SCRATCH.get();
scratch.reset();
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache = scratch.caveBiomeCache;
CarveWallBuffer walls = scratch.walls;
CarveColumnMask[] columnMasks = scratch.columnMasks;
CarveColumnMask[] boundaryMasks = scratch.boundaryMasks;
@@ -1231,7 +1236,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
return null;
}
int roll = Math.floorMod(rng.nextParallelRNG(BlockPosition.toLong(x, y, z)).nextInt(), CAVE_BIOME_BLEND_TOTAL_WEIGHT);
int roll = Math.floorMod(caveBlendRandom(caveBlendSeed, x, y, z), CAVE_BIOME_BLEND_TOTAL_WEIGHT);
if (roll < CAVE_BIOME_BLEND_CENTER_WEIGHT) {
return center;
}
@@ -1245,6 +1250,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
return neighbor != null ? neighbor : center;
}
static int caveBlendRandom(long seed, int x, int y, int z) {
long scrambled = ((seed + BlockPosition.toLong(x, y, z)) ^ RANDOM_MULTIPLIER) & RANDOM_MASK;
return (int) (((scrambled * RANDOM_MULTIPLIER + RANDOM_ADDEND) & RANDOM_MASK) >>> 16);
}
private IrisBiome sampleCaveBiome(Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, int x, int y, int z, CaveInputs resolverState) {
long key = BlockPosition.toLong(x, y, z);
IrisBiome cachedBiome = caveBiomeCache.get(key);
@@ -21,6 +21,7 @@ package art.arcane.iris.generation.stage;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.math.PowerOfTwoCoordinates;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import java.util.Arrays;
import java.util.HashMap;
@@ -32,6 +33,7 @@ final class IrisCarveScratch {
final int[] surfaceHeights = new int[256];
final CarveWallBuffer walls = new CarveWallBuffer(512);
final Map<String, IrisBiome> customBiomeCache = new HashMap<>();
final Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache = new Long2ObjectOpenHashMap<>(4096);
int[] upperSurfaceHeights;
boolean customCaveBiomePresent;
@@ -56,6 +58,7 @@ final class IrisCarveScratch {
}
walls.clear();
customBiomeCache.clear();
caveBiomeCache.clear();
customCaveBiomePresent = false;
}
}
@@ -6,14 +6,21 @@ import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReferenceArray;
import java.util.function.Predicate;
public final class SubterrainPlanner {
private static final int PLAN_CACHE_SLOTS = 4096;
private static final int TILE_CACHE_SLOTS = 1024;
private static final Comparator<SubterrainPlan> PLAN_ORDER =
Comparator.comparingInt(SubterrainPlan::priority).reversed().thenComparing(SubterrainPlan::id);
private final List<Definition> definitions;
private final long seed;
private final int minY;
private final int maxYExclusive;
private final ThreadLocal<CandidateTile> sampleTile = new ThreadLocal<>();
private final AtomicReferenceArray<CachedPlan> planCache = new AtomicReferenceArray<>(PLAN_CACHE_SLOTS);
private final AtomicReferenceArray<CandidateTile> tileCache = new AtomicReferenceArray<>(TILE_CACHE_SLOTS);
private final Predicate<SubterrainBounds> allowedBounds;
private final RegionResolver regionResolver;
@@ -70,8 +77,9 @@ public final class SubterrainPlanner {
if (minX > maxX || minZ > maxZ) {
throw new IllegalArgumentException("Subterrain query bounds are inverted");
}
List<SubterrainPlan> result = new ArrayList<>();
for (Definition definition : definitions) {
List<SubterrainPlan> result = null;
for (int definitionIndex = 0; definitionIndex < definitions.size(); definitionIndex++) {
Definition definition = definitions.get(definitionIndex);
long firstX = Math.floorDiv(minX, definition.spacing()) - 1L;
long lastX = Math.floorDiv(maxX, definition.spacing()) + 1L;
long firstZ = Math.floorDiv(minZ, definition.spacing()) - 1L;
@@ -81,14 +89,20 @@ public final class SubterrainPlanner {
}
for (long gridX = firstX; gridX <= lastX; gridX++) {
for (long gridZ = firstZ; gridZ <= lastZ; gridZ++) {
SubterrainPlan plan = create(definition, gridX, gridZ);
SubterrainPlan plan = plan(definitionIndex, definition, gridX, gridZ);
if (plan != null && plan.bounds().intersects(minX, minZ, maxX, maxZ)) {
if (result == null) {
result = new ArrayList<>();
}
result.add(plan);
}
}
}
}
result.sort(Comparator.comparingInt(SubterrainPlan::priority).reversed().thenComparing(SubterrainPlan::id));
if (result == null) {
return List.of();
}
result.sort(PLAN_ORDER);
return List.copyOf(result);
}
@@ -96,20 +110,7 @@ public final class SubterrainPlanner {
if (isEmpty() || y < minY || y >= maxYExclusive) {
return SubterrainCell.OUTSIDE;
}
int chunkX = Math.floorDiv(x, 16);
int chunkZ = Math.floorDiv(z, 16);
CandidateTile tile = sampleTile.get();
if (tile == null || tile.chunkX() != chunkX || tile.chunkZ() != chunkZ) {
long baseX = (long) chunkX * 16;
long baseZ = (long) chunkZ * 16;
tile = new CandidateTile(chunkX, chunkZ, plansForBounds(
(int) Math.max(Integer.MIN_VALUE, baseX - 1),
(int) Math.max(Integer.MIN_VALUE, baseZ - 1),
(int) Math.min(Integer.MAX_VALUE, baseX + 16),
(int) Math.min(Integer.MAX_VALUE, baseZ + 16)));
sampleTile.set(tile);
}
return sample(tile.plans(), x, y, z);
return sample(candidates(Math.floorDiv(x, 16), Math.floorDiv(z, 16)), x, y, z);
}
public SubterrainCell sample(List<SubterrainPlan> candidates, int x, int y, int z) {
@@ -127,10 +128,38 @@ public final class SubterrainPlanner {
return selected;
}
private List<SubterrainPlan> candidates(int chunkX, int chunkZ) {
int slot = (int) mix(((long) chunkX << 32) ^ (chunkZ & 0xffffffffL)) & (TILE_CACHE_SLOTS - 1);
CandidateTile tile = tileCache.getAcquire(slot);
if (tile != null && tile.chunkX() == chunkX && tile.chunkZ() == chunkZ) {
return tile.plans();
}
long baseX = (long) chunkX * 16;
long baseZ = (long) chunkZ * 16;
List<SubterrainPlan> plans = plansForBounds(
(int) Math.max(Integer.MIN_VALUE, baseX - 1),
(int) Math.max(Integer.MIN_VALUE, baseZ - 1),
(int) Math.min(Integer.MAX_VALUE, baseX + 16),
(int) Math.min(Integer.MAX_VALUE, baseZ + 16));
tileCache.setRelease(slot, new CandidateTile(chunkX, chunkZ, plans));
return plans;
}
private SubterrainPlan plan(int definitionIndex, Definition definition, long gridX, long gridZ) {
int slot = (int) mix(mix(gridX ^ definition.seedSalt()) ^ Long.rotateLeft(gridZ, 29)) & (PLAN_CACHE_SLOTS - 1);
CachedPlan cached = planCache.getAcquire(slot);
if (cached != null && cached.definition() == definitionIndex && cached.gridX() == gridX && cached.gridZ() == gridZ) {
return cached.plan();
}
SubterrainPlan plan = create(definition, gridX, gridZ);
planCache.setRelease(slot, new CachedPlan(definitionIndex, gridX, gridZ, plan));
return plan;
}
private SubterrainCell raw(List<SubterrainPlan> candidates, int x, int y, int z) {
SubterrainCell selected = SubterrainCell.OUTSIDE;
for (SubterrainPlan plan : candidates) {
SubterrainCell cell = plan.sample(x, y, z);
for (int index = 0; index < candidates.size(); index++) {
SubterrainCell cell = candidates.get(index).sample(x, y, z);
if (cell.solid() && cell.room().reservedSolid()) {
return cell;
}
@@ -212,6 +241,9 @@ public final class SubterrainPlanner {
private record CandidateTile(int chunkX, int chunkZ, List<SubterrainPlan> plans) {
}
private record CachedPlan(int definition, long gridX, long gridZ, SubterrainPlan plan) {
}
record Definition(String id, IrisSubterrainFamily family, IrisSubterrainFluid fluid, String biome, int spacing, double probability,
int length, int radius, int height, double shapeWarp, int fluidDepth, int chimneyHeight, int terraceCount,
int pillarSpacing, double formationFraction, String solid, int priority,
@@ -109,6 +109,7 @@ import java.util.concurrent.ConcurrentHashMap;
@Data
public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
private static final int CUSTOM_BIOME_KEY_CACHE_LIMIT = 4096;
private static final Map<File, IrisData> dataLoaders = new ConcurrentHashMap<>();
// Loaders (cached or detached) that currently have registered engines; see hasActiveEngines.
// Identity-keyed on purpose: Lombok's @Data hashCode over this class's mutable loader state
@@ -159,6 +160,10 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
@Setter(AccessLevel.NONE)
@EqualsAndHashCode.Exclude
private transient volatile GenerationRegistryContractFactory.CustomBiomeResourceResolver customBiomeResourceResolver;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
@EqualsAndHashCode.Exclude
private transient volatile CustomBiomeKeyCache customBiomeKeyCache;
private transient volatile Engine soleEngine;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
@@ -234,6 +239,32 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
IrisDimension requiredDimension = Objects.requireNonNull(dimension, "dimension");
IrisBiomeCustom requiredBiome = Objects.requireNonNull(customBiome, "customBiome");
GenerationRegistryContractFactory.CustomBiomeResourceResolver resolver = customBiomeResourceResolver;
ContentGate gate = resolver == null ? getContentGate() : null;
Object owner = resolver == null ? gate : resolver;
CustomBiomeKeyCache cache = customBiomeKeyCache;
if (cache == null || cache.owner() != owner) {
cache = new CustomBiomeKeyCache(owner, new ConcurrentHashMap<>());
customBiomeKeyCache = cache;
}
CustomBiomeKey key = new CustomBiomeKey(requiredDimension.getLoadKey(), requiredBiome);
String cached = cache.keys().get(key);
if (cached != null) {
return cached;
}
String resolved = resolveCustomBiomeResourceKey(requiredDimension, requiredBiome, resolver, gate);
if (cache.keys().size() >= CUSTOM_BIOME_KEY_CACHE_LIMIT) {
cache.keys().clear();
}
cache.keys().put(key, resolved);
return resolved;
}
private String resolveCustomBiomeResourceKey(
IrisDimension requiredDimension,
IrisBiomeCustom requiredBiome,
GenerationRegistryContractFactory.CustomBiomeResourceResolver resolver,
ContentGate gate
) {
if (resolver != null) {
try {
return resolver.resolve(
@@ -248,7 +279,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
return GenerationRegistryContractFactory.customBiomeResourceKey(
requiredDimension.getLoadKey(),
requiredBiome,
getContentGate()
gate
);
}
@@ -1035,4 +1066,29 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
return immutable;
}
}
private record CustomBiomeKeyCache(Object owner, ConcurrentHashMap<CustomBiomeKey, String> keys) {
}
private static final class CustomBiomeKey {
private final String dimensionKey;
private final IrisBiomeCustom biome;
private final int hash;
private CustomBiomeKey(String dimensionKey, IrisBiomeCustom biome) {
this.dimensionKey = dimensionKey;
this.biome = biome;
hash = 31 * Objects.hashCode(dimensionKey) + System.identityHashCode(biome);
}
@Override
public boolean equals(Object other) {
return other instanceof CustomBiomeKey key && key.biome == biome && Objects.equals(key.dimensionKey, dimensionKey);
}
@Override
public int hashCode() {
return hash;
}
}
}
@@ -8,7 +8,6 @@ import art.arcane.iris.generation.runtime.GenerationRuntime.BiomeMaxes;
import art.arcane.iris.world.history.SavedBiomeRuntime;
import art.arcane.iris.world.history.SavedBiomeUnavailableException;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.testsupport.Await;
import art.arcane.iris.testsupport.PlatformBinding;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@@ -37,13 +36,11 @@ import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
@@ -680,30 +677,19 @@ public class IrisEngineGenerationRuntimeScopeTest {
}
@Test
public void missingRouterReadWaitsForAttachmentPublication() throws Exception {
public void missingRouterReadsDoNotWaitForTheAttachmentMonitor() throws Exception {
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
GenerationHistoryRuntimeRouter router = historyRouter(engine);
GenerationHistoryRuntimeRouter.CoordinateScope scope = mock(GenerationHistoryRuntimeRouter.CoordinateScope.class);
when(router.openCoordinateScope(-17, 32)).thenReturn(scope);
CountDownLatch started = new CountDownLatch(1);
FutureTask<GenerationHistoryRuntimeRouter.CoordinateScope> read = new FutureTask<>(() -> {
started.countDown();
return engine.openGenerationHistoryCoordinateScope(-17, 32);
});
Thread reader = new Thread(read, "Pending history attachment");
ExecutorService reader = Executors.newSingleThreadExecutor();
try {
synchronized (engine.generationHistoryRuntimeRouterLock) {
reader.start();
assertTrue(started.await(5, TimeUnit.SECONDS));
awaitBlocked(reader);
engine.attachGenerationHistoryRuntimeRouter(router);
assertNull(reader.submit(() -> engine.openGenerationHistoryCoordinateScope(-17, 32))
.get(5, TimeUnit.SECONDS));
assertTrue(reader.submit(engine::getGenerationHistoryRuntimeRouter).get(5, TimeUnit.SECONDS).isEmpty());
}
assertSame(scope, read.get(5, TimeUnit.SECONDS));
} finally {
reader.join(5_000L);
assertTrue("Pending router read did not finish", !reader.isAlive());
reader.shutdownNow();
assertTrue(reader.awaitTermination(5, TimeUnit.SECONDS));
}
}
@@ -713,27 +699,19 @@ public class IrisEngineGenerationRuntimeScopeTest {
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
GenerationHistoryRuntimeRouter router = historyRouter(engine);
engine.attachGenerationHistoryRuntimeRouter(router);
CountDownLatch started = new CountDownLatch(1);
FutureTask<GenerationHistoryRuntimeRouter.CoordinateScope> read = new FutureTask<>(() -> {
started.countDown();
return engine.openGenerationHistoryCoordinateScope(-17, 32);
});
Thread reader = new Thread(read, "Detached history read");
engine.detachGenerationHistoryRuntimeRouter(router);
ExecutorService reader = Executors.newSingleThreadExecutor();
try {
synchronized (engine.generationHistoryRuntimeRouterLock) {
engine.detachGenerationHistoryRuntimeRouter(router);
reader.start();
assertTrue(started.await(5, TimeUnit.SECONDS));
awaitBlocked(reader);
ExecutionException failure = assertThrows(ExecutionException.class, () -> reader.submit(
() -> engine.openGenerationHistoryCoordinateScope(-17, 32)).get(5, TimeUnit.SECONDS));
assertTrue(failure.getCause() instanceof IllegalStateException);
assertEquals("Iris generation-history runtime router is detached.", failure.getCause().getMessage());
}
ExecutionException failure = assertThrows(ExecutionException.class, () -> read.get(5, TimeUnit.SECONDS));
assertTrue(failure.getCause() instanceof IllegalStateException);
assertEquals("Iris generation-history runtime router is detached.", failure.getCause().getMessage());
assertTrue(engine.getGenerationHistoryRuntimeRouter().isEmpty());
} finally {
reader.join(5_000L);
assertTrue("Detached router read did not finish", !reader.isAlive());
reader.shutdownNow();
assertTrue(reader.awaitTermination(5, TimeUnit.SECONDS));
}
verify(router, never()).openCoordinateScope(anyInt(), anyInt());
}
@@ -909,12 +887,6 @@ public class IrisEngineGenerationRuntimeScopeTest {
return router;
}
private static void awaitBlocked(Thread thread) {
Await.reached("the reader to block on the engine lifecycle lock", Duration.ofSeconds(5L),
() -> thread.getState() == Thread.State.BLOCKED || !thread.isAlive());
assertEquals(Thread.State.BLOCKED, thread.getState());
}
private static IrisEngine engine(
GenerationRuntime generationRuntime,
EngineEffects effects,
@@ -132,6 +132,9 @@ public class IrisCarveModifierBiomeBlendTest {
Field field = IrisCarveModifier.class.getDeclaredField("rng");
field.setAccessible(true);
field.set(modifier, rng);
Field seed = IrisCarveModifier.class.getDeclaredField("caveBlendSeed");
seed.setAccessible(true);
seed.setLong(modifier, SEED);
return modifier;
}
@@ -56,6 +56,9 @@ public class IrisCarveModifierBoundarySupportTest {
Field rng = IrisCarveModifier.class.getDeclaredField("rng");
rng.setAccessible(true);
rng.set(modifier, new RNG(71L));
Field seed = IrisCarveModifier.class.getDeclaredField("caveBlendSeed");
seed.setAccessible(true);
seed.setLong(modifier, 71L);
IrisMatterSupport.ensureRegistered();
Matter floorMatter = new IrisMatter(16, 16, 16);
@@ -0,0 +1,53 @@
package art.arcane.iris.generation.stage;
import art.arcane.volmlib.util.math.BlockPosition;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import java.util.SplittableRandom;
import static org.junit.Assert.assertEquals;
public class IrisCarveModifierCaveBlendRollTest {
private static final int SAMPLES = 1_000_000;
@Test
public void statelessBlendRandomMatchesParallelRngForOneMillionPoints() {
SplittableRandom random = new SplittableRandom(0x5eed_cafe_f00dL);
long[] seeds = {0L, 1L, -1L, 1337L, Long.MIN_VALUE, Long.MAX_VALUE, 0x5DEECE66DL, random.nextLong()};
for (int sample = 0; sample < SAMPLES; sample++) {
long seed = sample < seeds.length * 64 ? seeds[sample % seeds.length] : random.nextLong();
int x = coordinate(random, sample);
int y = sample % 7 == 0 ? random.nextInt() : random.nextInt(-2048, 2048);
int z = coordinate(random, sample + 3);
assertEquals(seed + " @ " + x + "," + y + "," + z,
new RNG(seed).nextParallelRNG(BlockPosition.toLong(x, y, z)).nextInt(),
IrisCarveModifier.caveBlendRandom(seed, x, y, z));
}
}
@Test
public void statelessBlendRandomMatchesParallelRngAtCoordinateExtremes() {
int[] edges = {Integer.MIN_VALUE, Integer.MIN_VALUE + 1, -33_554_433, -33_554_432, -1, 0, 1,
33_554_431, 33_554_432, Integer.MAX_VALUE - 1, Integer.MAX_VALUE};
long[] seeds = {0L, -1L, 1337L, Long.MIN_VALUE, Long.MAX_VALUE};
for (long seed : seeds) {
for (int x : edges) {
for (int y : edges) {
for (int z : edges) {
assertEquals(new RNG(seed).nextParallelRNG(BlockPosition.toLong(x, y, z)).nextInt(),
IrisCarveModifier.caveBlendRandom(seed, x, y, z));
}
}
}
}
}
private static int coordinate(SplittableRandom random, int sample) {
return switch (sample % 4) {
case 0 -> random.nextInt();
case 1 -> random.nextInt(-30_000_000, 30_000_000);
default -> random.nextInt(-65_536, 65_536);
};
}
}
@@ -1,5 +1,6 @@
package art.arcane.iris.generation.stage;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.volmlib.util.matter.MatterCavern;
import org.junit.Test;
@@ -49,6 +50,7 @@ public class IrisCarveScratchTest {
scratch.boundaryMasks[0].add(13);
scratch.walls.put(1, 12, 2, cavern);
scratch.customBiomeCache.put("cave", null);
scratch.caveBiomeCache.put(42L, new IrisBiome());
scratch.customCaveBiomePresent = true;
scratch.reset();
@@ -56,6 +58,7 @@ public class IrisCarveScratchTest {
assertTrue(scratch.columnMasks[0].isEmpty());
assertTrue(scratch.boundaryMasks[0].isEmpty());
assertTrue(scratch.customBiomeCache.isEmpty());
assertTrue(scratch.caveBiomeCache.isEmpty());
assertFalse(scratch.customCaveBiomePresent);
int[] wallCount = new int[1];
scratch.walls.forEach((x, y, z, value) -> wallCount[0]++);
@@ -1,6 +1,7 @@
package art.arcane.iris.generation.subterrain;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.volmlib.util.collection.KList;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import org.junit.Test;
@@ -10,6 +11,11 @@ import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.HashSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -441,6 +447,74 @@ public class SubterrainPlannerTest {
}
}
@Test
public void alternatingChunkSamplingResolvesEachCandidateCellOnce() {
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE).setAllowedRegions(new KList<>("test-region"));
AtomicInteger regionQueries = new AtomicInteger();
SubterrainPlanner planner = new SubterrainPlanner(new SubterrainPlanner.Options(List.of(feature), 7123, -64, 97,
(x, z) -> {
regionQueries.incrementAndGet();
return "test-region";
}));
for (int round = 0; round < 500; round++) {
planner.sample(7, 10, 7);
planner.sample(23, 10, 7);
planner.sample(7, 10, 23);
planner.sample(-9, 10, -9);
}
int firstRound = regionQueries.get();
assertTrue("candidate cells resolved " + firstRound + " times", firstRound <= 16);
for (int round = 0; round < 500; round++) {
planner.plansForBounds(-1, -1, 16, 16);
planner.plansForBounds(15, 15, 32, 32);
}
assertEquals(firstRound, regionQueries.get());
}
@Test
public void concurrentCrossChunkSamplingMatchesAFreshPlanner() throws Exception {
List<IrisSubterrainFeature> features = List.of(compactFeature(IrisSubterrainFamily.CENOTE).setSpacing(96),
compactFeature(IrisSubterrainFamily.TRAVERTINE_TERRACES).setSpacing(160));
SubterrainPlanner shared = new SubterrainPlanner(new SubterrainPlanner.Options(features, 4242, -64, 97, (x, z) -> "test-region"));
SubterrainPlanner reference = new SubterrainPlanner(new SubterrainPlanner.Options(features, 4242, -64, 97, (x, z) -> "test-region"));
int threads = 8;
ExecutorService pool = Executors.newFixedThreadPool(threads);
try {
List<Future<Integer>> results = new ArrayList<>();
for (int thread = 0; thread < threads; thread++) {
int offset = thread;
results.add(pool.submit(() -> {
int occupied = 0;
for (int step = 0; step < 6000; step++) {
int x = Math.floorMod(step * 37 + offset * 101, 2400) - 1200;
int z = Math.floorMod(step * 53 + offset * 211, 2400) - 1200;
int y = Math.floorMod(step * 11 + offset, 150) - 60;
int chunkX = Math.floorDiv(x, 16) * 16;
int chunkZ = Math.floorDiv(z, 16) * 16;
SubterrainCell expected;
synchronized (reference) {
expected = reference.sample(reference.plansForBounds(chunkX - 1, chunkZ - 1, chunkX + 16, chunkZ + 16), x, y, z);
}
SubterrainCell actual = shared.sample(x, y, z);
assertEquals(expected, actual);
if (actual.occupied()) {
occupied++;
}
}
return occupied;
}));
}
int occupied = 0;
for (Future<Integer> result : results) {
occupied += result.get(60, TimeUnit.SECONDS);
}
assertTrue(occupied > 0);
} finally {
pool.shutdownNow();
assertTrue(pool.awaitTermination(10, TimeUnit.SECONDS));
}
}
@Test
public void immutableSnapshotsAndSeedsControlPlans() {
IrisSubterrainFeature feature = feature(IrisSubterrainFamily.CENOTE);
@@ -0,0 +1,116 @@
package art.arcane.iris.pack.loading;
import art.arcane.iris.generation.biome.IrisBiomeCustom;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.testsupport.DurabilityMode;
import art.arcane.iris.world.history.GenerationRegistryContractFactory;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
public class IrisDataCustomBiomeKeyTest {
@ClassRule
public static final DurabilityMode DURABILITY = DurabilityMode.relaxed();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void repeatedConcurrentLookupsMatchTheContentAddressedKeyPerDimension() throws Exception {
Path pack = temporaryFolder.newFolder("custom-keys").toPath().resolve("iris/pack");
writePack(pack);
IrisData data = IrisData.openDatapackCompiler(pack.toFile());
ExecutorService pool = Executors.newFixedThreadPool(8);
try {
IrisDimension overworld = data.getDimensionLoader().load("overworld");
IrisDimension second = data.getDimensionLoader().load("second");
List<IrisBiomeCustom> customs = data.getBiomeLoader().load("test").getCustomDerivitives();
assertEquals(2, customs.size());
List<String> expected = new ArrayList<>();
for (IrisDimension dimension : List.of(overworld, second)) {
for (IrisBiomeCustom custom : customs) {
expected.add(GenerationRegistryContractFactory.customBiomeResourceKey(
dimension.getLoadKey(), custom, data.getContentGate()));
}
}
assertEquals(4, expected.stream().distinct().count());
List<Future<List<String>>> results = new ArrayList<>();
for (int thread = 0; thread < 8; thread++) {
results.add(pool.submit(() -> {
List<String> seen = new ArrayList<>();
for (int round = 0; round < 200; round++) {
seen.clear();
for (IrisDimension dimension : List.of(overworld, second)) {
for (IrisBiomeCustom custom : customs) {
seen.add(data.customBiomeResourceKey(dimension, custom));
}
}
}
return List.copyOf(seen);
}));
}
for (Future<List<String>> result : results) {
assertEquals(expected, result.get(60, TimeUnit.SECONDS));
}
IrisBiomeCustom equalCopy = new IrisBiomeCustom().setId(customs.getFirst().getId());
assertEquals(GenerationRegistryContractFactory.customBiomeResourceKey(
overworld.getLoadKey(), equalCopy, data.getContentGate()),
data.customBiomeResourceKey(overworld, equalCopy));
assertNotEquals(data.customBiomeResourceKey(overworld, customs.getFirst()),
data.customBiomeResourceKey(second, customs.getFirst()));
} finally {
pool.shutdownNow();
assertTrue(pool.awaitTermination(10, TimeUnit.SECONDS));
data.close();
}
}
private static void writePack(Path root) throws Exception {
Files.createDirectories(root.resolve("dimensions"));
Files.createDirectories(root.resolve("biomes"));
for (String dimension : List.of("overworld", "second")) {
Files.writeString(root.resolve("dimensions/" + dimension + ".json"), """
{
"name": "Test Dimension",
"environment": "NORMAL",
"logicalHeight": 256,
"dimensionHeight": {
"min": -64,
"max": 320
}
}
""", StandardCharsets.UTF_8);
}
Files.writeString(root.resolve("biomes/test.json"), """
{
"name": "Test Biome",
"derivative": "minecraft:plains",
"vanillaDerivative": "minecraft:plains",
"customDerivitives": [
{
"id": "first_custom"
},
{
"id": "second_custom",
"temperature": 1.5
}
]
}
""", StandardCharsets.UTF_8);
}
}