mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 14:00:26 +00:00
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:
1 parent
e2430500cf
commit
7282a46a26
10 files changed
+395
-81
No files matched your search
+15
-43
@@ -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,
|
||||
|
||||
+3
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -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);
|
||||
|
||||
+53
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user