Lock-free scopes and caches

- History coordinate scopes read the router and routing flag lock-free
- Subterrain planner shares bounded plan and chunk candidate caches
- Cave blend roll uses a stateless hash identical to the parallel RNG
- Custom biome resource keys are cached per dimension and biome identity
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-21 19:15:01 -04:00
1 parent e2430500cf
commit 7282a46a26
10 files changed
+395 -81

No files matched your search

@@ -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,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);
}
}