Improved cold generation

- Flat authoring studios skip terrain and native structure preparation
- Normal Studio entry prepares the exact hydrology column asynchronously
- Hydrology prefetch admits the complete frontier before stale queued work
- Concurrent hydrology loads avoid cache monitor contention
- Cave density tasks drain failures before releasing chunk state
- Terrain and interpolation queries reuse exact bounded samples
- Pregeneration counts requested chunks directly from its bounds
This commit is contained in:
Brian Neumann-Fopiano
2026-09-05 11:57:01 -04:00
parent 20ad49b9d2
commit 73d640943c
43 changed files with 4934 additions and 384 deletions
@@ -17,6 +17,7 @@ import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeCustom;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.RNG;
@@ -64,6 +65,7 @@ public class CustomBiomeSource extends BiomeSource {
private final long seed;
private final Engine engine;
private final BukkitChunkGenerator platformGenerator;
private final Registry<Biome> biomeCustomRegistry;
private final Registry<Biome> biomeRegistry;
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
@@ -76,6 +78,9 @@ public class CustomBiomeSource extends BiomeSource {
public CustomBiomeSource(long seed, Engine engine, World world) {
this.engine = engine;
this.platformGenerator = world.getGenerator() instanceof BukkitChunkGenerator generator
? generator
: null;
this.seed = seed;
this.biomeCustomRegistry = registry().lookup(Registries.BIOME).orElse(null);
this.biomeRegistry = ((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())).lookup(Registries.BIOME).orElse(null);
@@ -353,6 +358,9 @@ public class CustomBiomeSource extends BiomeSource {
if (!isRuntimeAvailable()) {
return null;
}
if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) {
return fallbackBiome;
}
runtimeBiomeState();
int quartX = QuartPos.fromBlock(blockX);
int quartZ = QuartPos.fromBlock(blockZ);
@@ -405,6 +413,9 @@ public class CustomBiomeSource extends BiomeSource {
}
private Holder<Biome> getStructureNoiseBiomeWithActiveGenerationLease(int x, int y, int z) {
if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) {
return fallbackBiome;
}
if (isGuaranteedSurfaceBiome(y)) {
return getSurfaceStructureBiomeHolder(x, z);
}
@@ -731,6 +742,9 @@ public class CustomBiomeSource extends BiomeSource {
IrisDimensionCarvingResolver.State resolverState,
boolean cacheable
) {
if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) {
return fallbackBiome;
}
RuntimeNoiseKey cacheKey = new RuntimeNoiseKey(
engine.getCacheID(), packNoiseKey(x, y, z));
if (cacheable) {
@@ -13,6 +13,7 @@ import art.arcane.iris.engine.framework.NativeStructureGenerationPolicy;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
import art.arcane.iris.engine.platform.BukkitChunkGenerator;
import art.arcane.iris.engine.platform.studio.generators.JigsawStudioGenerator;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
import art.arcane.iris.engine.object.IrisMaterialPalette;
@@ -123,6 +124,13 @@ import java.util.function.IntBinaryOperator;
import java.util.function.Predicate;
public class IrisChunkGenerator extends CustomChunkGenerator {
private static final Set<Heightmap.Types> AUTHORING_HEIGHTMAPS = Set.of(
Heightmap.Types.WORLD_SURFACE_WG,
Heightmap.Types.OCEAN_FLOOR_WG,
Heightmap.Types.WORLD_SURFACE,
Heightmap.Types.OCEAN_FLOOR,
Heightmap.Types.MOTION_BLOCKING,
Heightmap.Types.MOTION_BLOCKING_NO_LEAVES);
private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
private static final WrappedReturningMethod<Heightmap, Object> SET_HEIGHT;
private static final Runnable NO_OP = () -> {
@@ -854,9 +862,17 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
}
private void primeWorldgenHeightmaps(ChunkAccess chunkAccess) {
if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) {
primeAuthoringHeightmaps(chunkAccess);
return;
}
WorldgenTerrainHeightmaps.primeTerrain(chunkAccess, worldgenSurfaceHeight(), worldgenFloorHeight());
}
static void primeAuthoringHeightmaps(ChunkAccess chunkAccess) {
Heightmap.primeHeightmaps(chunkAccess, AUTHORING_HEIGHTMAPS);
}
private IntBinaryOperator worldgenSurfaceHeight() {
return (x, z) -> engine.getHeight(x, z, false) + runtimeMinY + 1;
}
@@ -934,15 +950,20 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
GenerationSessionLease lease = requireGenerationLease("bukkit_nms_biome_decoration");
IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) {
if (allowsRoutedDiscreteGeneration(ichunkaccess, ChunkStatus.FEATURES)) {
// Bind-time equivalent for Bukkit: the table is built on the first decorated chunk, which is where a
// feature-order cycle is reported once and degraded to features-off.
importedFeatures.prepare(generatoraccessseed);
boolean flatStudioTerrain = platformGenerator != null && platformGenerator.usesFlatStudioTerrain();
if (!flatStudioTerrain) {
// Bind-time equivalent for Bukkit: the table is built on the first decorated chunk, which is where a
// feature-order cycle is reported once and degraded to features-off.
importedFeatures.prepare(generatoraccessseed);
}
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
// Vanilla's placed-feature pass, on THIS thread. The delegate is still called with
// addVanillaDecorations=false below, so the vanilla half never runs twice. Inert unless the
// dimension set importedFeatures.enabled.
importedFeatures.run(generatoraccessseed, ichunkaccess, this);
if (!flatStudioTerrain) {
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
// Vanilla's placed-feature pass, on THIS thread. The delegate is still called with
// addVanillaDecorations=false below, so the vanilla half never runs twice. Inert unless the
// dimension set importedFeatures.enabled.
importedFeatures.run(generatoraccessseed, ichunkaccess, this);
}
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);
}
claimGeneratedSemantics(route, "bukkit_nms_biome_decoration");
@@ -1219,6 +1240,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
BlockPos blockPos = sectionPos.origin();
primeWorldgenHeightmaps(chunkAccess);
if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) {
return;
}
Heightmap motion = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING);
Heightmap motionNoLeaves = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES);
@@ -1310,6 +1334,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) {
return platformGenerator.getAuthoringBaseHeight();
}
try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope(
i, j, "bukkit_nms_base_height");
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_height");
@@ -1327,6 +1354,19 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
@Override
public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
if (platformGenerator != null && platformGenerator.usesFlatStudioTerrain()) {
BlockState[] column = new BlockState[levelheightaccessor.getHeight()];
int floorIndex = platformGenerator.getAuthoringFloorY() - levelheightaccessor.getMinY();
BlockState floor = platformGenerator.isJigsawStudioActive() && JigsawStudioGenerator.isLightFloor(i, j)
? Blocks.SMOOTH_STONE.defaultBlockState()
: Blocks.POLISHED_DEEPSLATE.defaultBlockState();
for (int index = 0; index < column.length; index++) {
column[index] = index == floorIndex
? floor
: Blocks.AIR.defaultBlockState();
}
return new NoiseColumn(levelheightaccessor.getMinY(), column);
}
try (GenerationHistoryRuntimeRouter.CoordinateScope route = openHistoryCoordinateScope(
i, j, "bukkit_nms_base_column");
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_nms_base_column");
@@ -1279,9 +1279,9 @@ public class NMSBinding implements INMSBinding {
: null;
boolean studioBootstrap = platformGenerator != null
&& platformGenerator.isStudioEntryBootstrapActive();
boolean jigsawStudio = platformGenerator != null
&& platformGenerator.isJigsawStudioActive();
if (jigsawStudio) {
boolean authoringStudio = platformGenerator != null
&& platformGenerator.isAuthoringStudio();
if (authoringStudio) {
requireIrisGenerator(generator);
if (currentState.possibleStructureSets().isEmpty()) {
currentState.ensureStructuresGenerated();
@@ -24,6 +24,31 @@ public class CustomBiomeSourceStructureContractTest {
Bootstrap.bootStrap();
}
@Test
public void authoringBiomesBypassTerrainResolutionBeforeDestinationChunksGenerate() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
int visibleStart = source.indexOf("private Holder<Biome> getVisibleNoiseBiomeWithActiveGenerationLease(");
int visibleEnd = source.indexOf("private boolean isBiomeCacheable(", visibleStart);
String visible = source.substring(visibleStart, visibleEnd);
int constantBiome = visible.indexOf("platformGenerator.usesFlatStudioTerrain()");
int terrainResolution = visible.indexOf("resolveVisibleBiomeHolder(");
assertTrue(source.contains("platformGenerator.usesFlatStudioTerrain()"));
assertTrue(constantBiome >= 0);
assertTrue(visible.indexOf("return fallbackBiome;", constantBiome) < terrainResolution);
assertTrue(terrainResolution > constantBiome);
int structureStart = source.indexOf("private Holder<Biome> getStructureNoiseBiomeWithActiveGenerationLease(");
int structureEnd = source.indexOf("boolean cacheable", structureStart);
assertTrue(source.substring(structureStart, structureEnd).contains("platformGenerator.usesFlatStudioTerrain()"));
String generator = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
int createBiomes = generator.indexOf("public CompletableFuture<ChunkAccess> createBiomes(");
int buildSurface = generator.indexOf("public void buildSurface(", createBiomes);
assertTrue(generator.substring(createBiomes, buildSurface)
.contains("customBiomeSource.getVisibleNoiseBiomeWithActiveGenerationLease("));
}
@Test
public void nativeStructuresUseTerrainSafeDerivativeAtEveryBiomeBoundary() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.customBiomeSource")));
@@ -221,6 +221,49 @@ public class IrisChunkGeneratorFailureContractTest {
assertTrue(source.contains("int minY = chunk.getMinY() + 1;"));
}
@Test
public void authoringHeightmapStagesReturnBeforeAnyTerrainHeightResolver() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
int primeStart = source.indexOf("private void primeWorldgenHeightmaps(");
int primeEnd = source.indexOf("private IntBinaryOperator worldgenSurfaceHeight()", primeStart);
String prime = source.substring(primeStart, primeEnd);
assertBefore(prime, "platformGenerator.usesFlatStudioTerrain()", "worldgenSurfaceHeight()");
assertBefore(prime, "primeAuthoringHeightmaps(chunkAccess);", "return;");
assertBefore(prime, "return;", "WorldgenTerrainHeightmaps.primeTerrain(");
int decorationStart = source.indexOf("public void addVanillaDecorations(");
int decorationEnd = source.indexOf("public void spawnOriginalMobs(", decorationStart);
String decorations = source.substring(decorationStart, decorationEnd);
assertBefore(decorations, "primeWorldgenHeightmaps(chunkAccess);", "platformGenerator.usesFlatStudioTerrain()");
assertBefore(decorations, "platformGenerator.usesFlatStudioTerrain()", "return;");
assertBefore(decorations, "return;", "engine.getHeight(");
}
@Test
public void authoringDecorationSkipsNativeFeaturesButRetainsHeightmapsAndBukkitPopulators() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")));
String decoration = method(source,
"public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla)",
"public BiomeGenerationSettings getBiomeGenerationSettings");
int preparationGuard = decoration.indexOf("if (!flatStudioTerrain)");
int preparationEnd = decoration.indexOf("\n }", preparationGuard);
String preparation = decoration.substring(preparationGuard, preparationEnd);
assertTrue(preparation.contains("importedFeatures.prepare(generatoraccessseed);"));
int placementGuard = decoration.indexOf("if (!flatStudioTerrain)", preparationEnd);
int placementEnd = decoration.indexOf("\n }", placementGuard);
String placement = decoration.substring(placementGuard, placementEnd);
assertTrue(placement.contains("placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);"));
assertTrue(placement.contains("importedFeatures.run(generatoraccessseed, ichunkaccess, this);"));
int heightmaps = decoration.indexOf("addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);");
int populators = decoration.indexOf("delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);");
assertTrue(heightmaps > preparationEnd && heightmaps < placementGuard);
assertTrue(populators > placementEnd);
assertTrue(decoration.indexOf("claimGeneratedSemantics(route,") > populators);
assertTrue(decoration.contains("requireGenerationStage(\"bukkit_nms_biome_decoration\")"));
assertTrue(decoration.contains("requireGenerationLease(\"bukkit_nms_biome_decoration\")"));
}
@Test
public void everyChunkUsesTheProductionGenerationStages() throws IOException {
String source = Files.readString(Path.of(System.getProperty("iris.nmsChunkGeneratorSource")))
@@ -352,7 +352,7 @@ public class NMSBindingDatapackStructureScopeTest {
}
@Test
public void standardPublishesOneDeferredStateWhileJigsawPublishesInitializedEmptyState() throws IOException {
public void standardPublishesOneDeferredStateWhileAuthoringPublishesInitializedEmptyState() throws IOException {
Path chunkGeneratorSource = Path.of(System.getProperty("iris.nmsChunkGeneratorSource"));
String source = Files.readString(chunkGeneratorSource.resolveSibling("NMSBinding.java")).replace("\r\n", "\n");
int methodStart = source.indexOf("public DatapackStructureScopeResult scopeDatapackStructures(");
@@ -365,8 +365,8 @@ public class NMSBindingDatapackStructureScopeTest {
String method = source.substring(methodStart, methodEnd);
int filteredState = method.indexOf("possibleSetsField.set(scopedState, selection.structureSets());");
int jigsawMode = method.indexOf(
"boolean jigsawStudio = platformGenerator != null");
int jigsawOnly = method.indexOf("if (jigsawStudio)");
"boolean authoringStudio = platformGenerator != null");
int jigsawOnly = method.indexOf("if (authoringStudio)");
int emptyCreation = method.indexOf("ChunkGeneratorStructureState bootstrapState = createStructureState(");
int emptyFiltering = method.indexOf("bootstrapSetsField.set(bootstrapState, List.of());");
int emptyInitialization = method.indexOf("bootstrapState.ensureStructuresGenerated();");
@@ -64,6 +64,21 @@ public class WorldgenTerrainHeightmapsTest {
Bootstrap.bootStrap();
}
@Test
public void authoringHeightmapsReadPlacedObjectsAndFluidsWithoutTerrainResolvers() {
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
IrisChunkGenerator.primeAuthoringHeightmaps(chunk);
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, LAND_X, LAND_Z));
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, LAND_X, LAND_Z));
assertEquals(LAND_CANOPY_TOP, chunk.getHeight(Heightmap.Types.MOTION_BLOCKING, LAND_X, LAND_Z));
assertEquals(LAND_TERRAIN_TOP + 6,
chunk.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, LAND_X, LAND_Z));
assertEquals(OCEAN_FLUID_TOP, chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, OCEAN_X, OCEAN_Z));
assertEquals(OCEAN_FLOOR_TOP, chunk.getHeight(Heightmap.Types.OCEAN_FLOOR_WG, OCEAN_X, OCEAN_Z));
}
@Test
public void primeTerrainWritesEngineSurfaceAndOceanFloorHeights() {
ProtoChunk chunk = terrainChunk(new ChunkPos(0, 0));
+19 -19
View File
@@ -16,11 +16,11 @@ scope core/src/main/java
scope lombok.config
scope spi/src/main/java
exclude core/src/main/java/art/arcane/iris/engine/kernels
source adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSource.java a8987a308db171229d6379bae2698ad827ee58b6f86eee4164f3e594aeaf522f
source adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/CustomBiomeSource.java d195942b064c0f3d694f824870a44883043e5c47b19099e81072a871fbe0a9a5
source adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/DatapackStructureStateFilter.java b1ae6b3ab4e0ed1d0292681e90a875dbbf449fef29cc7af281ec01a34100fa14
source adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/ImportedFeatureStage.java 8eafc6ddd5dc3023e80fd97a718183e142c58998444502a835a8f8331a53ff5a
source adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java 145cadbccef9253ca8b29ba441c164a27d2c50e6a7c2177b18d1b999c0391ef7
source adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NMSBinding.java 0b0823ddc0b03c7788ec33678f70acda0a26979158bbefd3809058cbf898a9b5
source adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/IrisChunkGenerator.java bdf785a793ba615f45d4a375984ab695f5abc56d54e360623df94a6eddfa048d
source adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NMSBinding.java baeb4056e11f72108d920b1013eb05a66b9c08c0c8f384f0c7ee73335d5ca557
source adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NativeStructureWorldgenAccess.java a33a05dfcd1467a7b27a3245b819d21d9dac6024306901bcaafc43ff0f61bebc
source adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/NmsGenerationRegistry.java 8e7aa89be6166c40801edbe3c76bc7ff767e6a80874bdfc454c98ac7615da297
source adapters/bukkit/nms/v26_2_R1/src/main/java/art/arcane/iris/core/nms/v26_2_R1/VanillaStructureBiomes.java bc9ef399119dd325e9c7256ffb9c9d070022f30af7c23c3711aeb2f736ba724b
@@ -462,7 +462,7 @@ source core/src/main/java/art/arcane/iris/core/pack/PackValidationResult.java c3
source core/src/main/java/art/arcane/iris/core/pack/PackValidator.java b51b82898fe5dc618880ff0af5553e21f82b41499eb2d2dc13fe7c3c1295be14
source core/src/main/java/art/arcane/iris/core/pack/StructureGraphPackValidator.java 2e88ed567e1f477f1b77450932983ebf5c9e2fae866a66ad4713879f1f233d11
source core/src/main/java/art/arcane/iris/core/pack/StructurePackageClosure.java 6d0f68a637ef260729d5e67da6b2211d204c78fb2d07866467923740e9f9ffb6
source core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java f06951398de67eef6d2ea60c607673113353991b3c0db838ea4ce33d73d35be2
source core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java 2965a14213a6a463ab8c9a2b2bc47ef68ba37ede248263bdcc5bb2144bac35ab
source core/src/main/java/art/arcane/iris/core/pregenerator/MantleHeapPressure.java 8a01dfff59d07b75f90edffef9599d9a0ae44b3a33cced65af190fd9576690fb
source core/src/main/java/art/arcane/iris/core/pregenerator/PregenApiPhase.java aa6fca878aedd70c394c034cf62b6b7a0051875275682b154805121422da00d3
source core/src/main/java/art/arcane/iris/core/pregenerator/PregenApiSink.java bdf3ba2e9e2ccd4b34d272635bd615de4be7a09166ed01798811c0574b1d24d2
@@ -472,7 +472,7 @@ source core/src/main/java/art/arcane/iris/core/pregenerator/PregenPerformancePro
source core/src/main/java/art/arcane/iris/core/pregenerator/PregenPhaseTracker.java d57637b5a9e7c8e09c58e71fc8a1ff23eb295435c9fcd411746d5e8176a9e015
source core/src/main/java/art/arcane/iris/core/pregenerator/PregenRateTracker.java 53304e35af5b0e32b86dd75bdb1d4052630150ace2751fc8ba86ca38e94ae527
source core/src/main/java/art/arcane/iris/core/pregenerator/PregenRates.java d348acd7898d701e71bda83da20eb9ba09bad4f6779966ad87aa8a60522c3c8d
source core/src/main/java/art/arcane/iris/core/pregenerator/PregenTask.java a52c1ff8be510f5357bc721b88e735b6bf14c389956f50e249f74f8970af2320
source core/src/main/java/art/arcane/iris/core/pregenerator/PregenTask.java 04c174899d60a48e4ec9ed2bf0e72e4376b1f020bac7049f52766a59207855f3
source core/src/main/java/art/arcane/iris/core/pregenerator/PregeneratorMethod.java 6b259a5b14fd513c441f26c053e5713a646b7aac88542dc5c4e53f6aa4c008af
source core/src/main/java/art/arcane/iris/core/pregenerator/cache/PregenCache.java e6af20dfaeea6967ee0d561631f612e0b2dae7e22182773001844d3522bd79de
source core/src/main/java/art/arcane/iris/core/pregenerator/cache/PregenCacheImpl.java 2dafaaeb0b5bc8e65db9b0cf25ed2786cfa0f561c6712b7b5817568c239a9eb6
@@ -512,7 +512,7 @@ source core/src/main/java/art/arcane/iris/core/runtime/InPlaceChunkRegenerator.j
source core/src/main/java/art/arcane/iris/core/runtime/ObjectStudioActivation.java b3fa731a267ace9e052f1d4cbc437ecc1b6102c2c0ae030c21c9262c41069386
source core/src/main/java/art/arcane/iris/core/runtime/ObjectStudioLayout.java b78868e5adefca27c3f1f9544256317b1949fe208f02ccee7dbffc61c389e129
source core/src/main/java/art/arcane/iris/core/runtime/PaperLikeRuntimeControlBackend.java 443047b33624db675576a189c843952854fb8852c9df979eca044e8f0e3c23e6
source core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java b501913f1704a3e59bcf6982d6683110565c549725d00311061a7dbb7dc0ef88
source core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java e8ad851c680934278f1cefd954ecb40d48b0d4c1e1213dca9e6dc31a8c648946
source core/src/main/java/art/arcane/iris/core/runtime/TransientWorldCleanupSupport.java 54fe5614808ffa4cef920e6428f6a1fd70c23d01e3ab16d75fac862271dd2e97
source core/src/main/java/art/arcane/iris/core/runtime/WorldDeletionQueue.java 4e77be2cb6c1bde9abc38253208c4a86bae0e575d8b6ddd7defdada414248ac3
source core/src/main/java/art/arcane/iris/core/runtime/WorldRuntimeControlBackend.java 0827d2b8412ffdc33ab35f1abb732584f85935dac3e882e9e04994b0cfd3fcce
@@ -656,8 +656,8 @@ source core/src/main/java/art/arcane/iris/engine/EngineTickRegistry.java 9ae20d3
source core/src/main/java/art/arcane/iris/engine/GenerationCacheWarmer.java 821455b42549511328dfd90e5447dffcdf8ea1311490f1b0ada2e65ca1a311d2
source core/src/main/java/art/arcane/iris/engine/GenerationRuntime.java 09e250e72e1ac63c38c29dc0860c951955ad71a7f9ec7a36ac27d7d27839b11c
source core/src/main/java/art/arcane/iris/engine/GenerationRuntimeScopeState.java a05de937d2d8ed1b5017549f1083e5f6592d944fa5a136dcfb5e9bc33eb78f2d
source core/src/main/java/art/arcane/iris/engine/IrisComplex.java 257916da58a83632887123debc9f0dea9a63087de61391550774982f5c613fb6
source core/src/main/java/art/arcane/iris/engine/IrisEngine.java c64aabd254a8e75d0a52c5c4b7b43f06013d6762c7a1cac4bb1c324e9447db80
source core/src/main/java/art/arcane/iris/engine/IrisComplex.java 5ae7ffc3ee932fd140100d48a9127db8c1216f950c8c0c20c8f27d537d4683a4
source core/src/main/java/art/arcane/iris/engine/IrisEngine.java 739b16fd1c283f6499328138406b4ffdbcb1be08c6bc696e4dc9f0cee372258d
source core/src/main/java/art/arcane/iris/engine/IrisEngineEffects.java 67cf96aed6c213dbd6d6cb81866afd29cd30b555cd4e88712395bc089e50f43c
source core/src/main/java/art/arcane/iris/engine/IrisEngineMantle.java 2c4251a6ad59b5435488fc3b83191c3753d5e356bc5c58832bba2c56a3a5aae9
source core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java 5f441c4ba85045b4c273e934599e65df0122586d97cb23ddae3ed76c9471c759
@@ -823,14 +823,14 @@ source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyDiagnosticRe
source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyFeatureQuery.java 60e6478e03dd339f57e7d6633c02bb6308d2de27ea86a634b26745b697121708
source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyFeatureRef.java 6e02a66c7ec7c9ee4732176eea248e4ba3174c66c164da02c12dee7e229cb0ee
source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyFeatureType.java aa574734891b581bd140320cd23707d9f61bd9b3afa851909d4c8fe6e20dc1e4
source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyFootprintCompiler.java 076eb55abe053524cd1990c62458e007092fcba69118c620592edfef0fbad4f2
source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyForkJoin.java 25005438b669b33bad5344ad6dd08127d0e928a3d383c2bb25610acb385322f1
source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyFootprintCompiler.java b8e486b2c8ff0769737c21120a582acef7520d47c760ba8c8558f482093f1857
source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyForkJoin.java 50d933ca908f7c3322e34b866fb422baa353c8c4741df2ddecb1fb4c76e803ca
source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyGeometrySampler.java 77ce4ed588dd7238ae448e8997a688df8cc3a21ed4fc8f829d593b9bfbfdd7c5
source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyHash.java 41771991828e67f219c36985ffc8dd3f65e9664b0a51f4e30be16b13b55e9c1d
source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyNaturalTerrainSampler.java 2ddc53c8961e1db153ac36f7f5f4e71742a22316bb644187e02c52740dbcf41d
source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyObservedPlannedSurface.java eac4e7f4ba41814d01c2fc4bade4653c89e53db7bd8ef498e0c54ad22179fb15
source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyOceanBoundaryRefiner.java 7f6aa7fa674a4f0293cbd42de1808347d0c101874ba0f5c870117b9ede5a562f
source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlanner.java b3c98015b4623049027effae6a2f8dccc5c82c97787304e2de1cac6338e62f51
source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlanner.java c31a0312508c178a0e8e41cae7debb87dff32aa90458a1b441d62891ea22cf95
source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPlannerSettings.java 5165f5b6faf5ab79156d93f94687c8f7249f16726a63592fb51c46c1b1ddb75c
source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyPoint.java 015a50924678d09c8447e0d66fa24c93da6a23178bc819bc0b8c320b9d82e7dd
source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyRenderSample.java 03587336c3d95369ba7c2dba1e17346b637c5cda75b6ce93e1aa3761bc64815c
@@ -839,7 +839,7 @@ source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyTerrainCaveV
source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyTerrainSample.java f1ea65b71befeb5d0cdb6d3c8402f0023f925fb13c16cb0c07ff1610f4079cd6
source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyTerrainSampler.java 190fd761a0be72700e05f5bd26626dcb51a87e59e280e9861ad3d79d5c86c494
source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyTile.java 33ee976cdba3b051d31738ab3acc6d5c4eea01c7043fa4d2ed24cb094b0cd704
source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyTileCache.java 0f39c34037f39945f99fbf17e27327d7cbc890956aab578a19129030f28f0da1
source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyTileCache.java e86e564915bc0f19b678d3d6a23fae3782ea339258febe91d2a132b6917bf985
source core/src/main/java/art/arcane/iris/engine/hydrology/HydrologyTileKey.java b806c0df952d005f0f0543225b18dc1990ed6f9a85f68a9489c96011b3ef5a5c
source core/src/main/java/art/arcane/iris/engine/hydrology/RiverCourse.java f919c7ce0953a08a65e015d84195e6a43980d9c409bbf5d3dd9cde86a9307e7f
source core/src/main/java/art/arcane/iris/engine/hydrology/RiverCourseType.java 7bbb28a498f0d7705caf5e278e68922bacdec16a16fa75643e14feca721ebb40
@@ -919,7 +919,7 @@ source core/src/main/java/art/arcane/iris/engine/mantle/components/CaveObjectPla
source core/src/main/java/art/arcane/iris/engine/mantle/components/FloatingIslandSampleResolver.java b40d3996c47afc832467567c7f5b00739398c76821a7bf6e2fb5172213a83af6
source core/src/main/java/art/arcane/iris/engine/mantle/components/FloatingObjectPlacementTransaction.java d3ae16cddedf58673d4ca50d4058b6d13500c1c6d85dbd7acf5a93fa72383ca1
source core/src/main/java/art/arcane/iris/engine/mantle/components/GoldenDebugObjectPlacer.java fd1230b323182e5e44b1810a3ff4d2934c1d98704ae93ec8275e73c0be940634
source core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java 2a767b374403b6285a9b010821ac17eb1163f2296134c1493ba76fe23c271ce6
source core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java bea4ecf5ce08814e5fa56f94d6b20156bb16332b67a0261eeaabd322e1df55af
source core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java 9449a3e411e818d5e6e47dcc7eff357fa6735fe6b67236bc1d45f5c7762ccdf9
source core/src/main/java/art/arcane/iris/engine/mantle/components/IslandObjectPlacer.java 617a452172d46289a1a9464ae32ca19996db8fc6fef31cf0819cecb3ee12b094
source core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java 4462fbbe6d904be0eff1a1b9a8b2cb753709d60be63c135d0410b7a247a3b4fb
@@ -1009,7 +1009,7 @@ source core/src/main/java/art/arcane/iris/engine/object/IrisDepositHeightDistrib
source core/src/main/java/art/arcane/iris/engine/object/IrisDepositPlacementScope.java 0b1b15482a85d473eac3b9d58be5b681b8e210846900ab5a2b4e945bf3873105
source core/src/main/java/art/arcane/iris/engine/object/IrisDepositShape.java 07647e01b010a338e1cf01f5372f8dfbccf34f3915742032baecacccda5293fc
source core/src/main/java/art/arcane/iris/engine/object/IrisDepositVariant.java 0234c4caa8c723047be7293fea0452df2221450b05ef3102bbc152170a5e5038
source core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java 806dcdc5b7c57b766bff0e48655c1bc9381854f9ba83d08a22cca37409ae218d
source core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java 5e9f5942a92248c216844f7840e39b0ede97afef9734bb737bfa95e2576a014d
source core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingEntry.java a0c5971a455b2ff2e4c82c7a0387110ab4f1e0dd27238fbcd57b7a0b3b8899dc
source core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolver.java 51ecf9f7f108311951baa621614f33085a569ffa88f7d314daf0fcdaf39e44b2
source core/src/main/java/art/arcane/iris/engine/object/IrisDimensionContractException.java 6d64f24dd2ff4b759ade515a2f6bff218fa7b59a68cd856f7e82cf2ae7d0fb65
@@ -1274,7 +1274,7 @@ source core/src/main/java/art/arcane/iris/engine/object/tree/TreeProfiles.java d
source core/src/main/java/art/arcane/iris/engine/object/tree/TreeRootBuilder.java df4248d6dd1508b4856508a2a8363b7cffae0d8db66a10495ffe9593b8127f68
source core/src/main/java/art/arcane/iris/engine/object/tree/TreeSupport.java 11c72c83a632d34945e3032d68ba89f0727ff3e7af3fae64988d17a8e9bf0fd5
source core/src/main/java/art/arcane/iris/engine/object/tree/TreeTrunkBuilder.java 856d0b46117a485df832cfadd2c31b491fde8609b43799b060110246524527d1
source core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java 6b036e6e3246df6a8e850344c67b0b5b1f8cc5e36914d304b5e9eaeb3a214243
source core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java 8f4521b6bb462e0a6afeaccb5112698ba1ca0cd493c0fe13cc432dcbeee0fbae
source core/src/main/java/art/arcane/iris/engine/platform/DummyBiomeProvider.java 560036f370d550f7916e6ed4b79df907fc0787567c27eabc49df1ef108cf5b13
source core/src/main/java/art/arcane/iris/engine/platform/DummyChunkGenerator.java 12ecd07d0189672a7c161734bce487f6cb6b4797a71df3692f29fa7ccc34cf32
source core/src/main/java/art/arcane/iris/engine/platform/EngineBukkitOps.java bee55e2704b0e2405b631408fa42ca4c7baef8c83c0b00af8c1dc98e8f3792f0
@@ -1282,8 +1282,8 @@ source core/src/main/java/art/arcane/iris/engine/platform/PlatformChunkGenerator
source core/src/main/java/art/arcane/iris/engine/platform/studio/EnginedStudioGenerator.java 5e279de24da76030fdfaf8b037bebf375fd35bd3e388fbaa2c4a24942ec1cedb
source core/src/main/java/art/arcane/iris/engine/platform/studio/StudioGenerator.java a94909957b4a6c60b9ac995312a72ef4208d5fd8862c4c03c2525693e9ebe69f
source core/src/main/java/art/arcane/iris/engine/platform/studio/generators/BiomeBuffetGenerator.java 5d169970dcd15011a6dfb9a8ddf76b0c03de2bef09b0f7c8ffbe3ce6787194f9
source core/src/main/java/art/arcane/iris/engine/platform/studio/generators/JigsawStudioGenerator.java 65b7b09536bcc9a92b8588f06f0d1e94d1301fd19be8e8e26a17b79ff67e3e86
source core/src/main/java/art/arcane/iris/engine/platform/studio/generators/ObjectStudioGenerator.java 93d9b05fbbcb579ac898d83aadc4bddc084aa05ffaebd84f72c3e51b5f4115b3
source core/src/main/java/art/arcane/iris/engine/platform/studio/generators/JigsawStudioGenerator.java 8b7e18180158951ebb7c09fdea310d31721b56f2ffb2056a36f2a05f07d9ceec
source core/src/main/java/art/arcane/iris/engine/platform/studio/generators/ObjectStudioGenerator.java 581a56e0b482a44b14f403b8ffd5dc9f40c8a119eb09def3684615ef6601870d
source core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBiome.java 4d6cbcac0788b593616e063ed102dbbd93db57f9354f8b5294f19b8660748889
source core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBiomeWriter.java 8c37ba79a8299a4b05f5e561380b03a54a10cc2f32f7d79b25ffd544211a7d54
source core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBlockResolution.java a032f34aa55fb67d8f8435689f75ae2323edcb80204c93716a8b5fd8cf86b784
@@ -1415,10 +1415,10 @@ source core/src/main/java/art/arcane/iris/util/project/interpolation/Interpolati
source core/src/main/java/art/arcane/iris/util/project/interpolation/InterpolationMethod.java 8948ead4aeb36e734b32dd7c247c0546c59e41d7792bf913053ee4f5154793e2
source core/src/main/java/art/arcane/iris/util/project/interpolation/InterpolationMethod3D.java 4d5831fc15fe506432ec0f747ccd8cadc1f4976c561812d18a53fea3af121e2e
source core/src/main/java/art/arcane/iris/util/project/interpolation/InterpolationType.java 50266ebcd1552f781fee3409f01ff24259f6a33dd7c1e9876075b7462f5bca2e
source core/src/main/java/art/arcane/iris/util/project/interpolation/IrisInterpolation.java b217803f57df1bcff429bbc9319598294ec3f1c68bab3e34b7bc4fd722136aef
source core/src/main/java/art/arcane/iris/util/project/interpolation/IrisInterpolation.java cafad8d3698cf733f6432a1f1d40db7494e89cb82e3d781f7cfcb2437f920d72
source core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBounds.java 484d465f6e7ee86c567570a3d044bf871d716728b936ea1909fcc5b6fb8834d8
source core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBoundsProvider.java 5da0eaf2db701a21db2fd3009660449b5c2ee306ac3bfaf0522b15f82890008a
source core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBoundsSampleCache2D.java 7a8928a4e3649c7120c5da17e11eab45bd1f39ed9ebfa4b3a58ff42c5a0f60c5
source core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseBoundsSampleCache2D.java a114e3c7997f81a1cdbc31a1d861a45475f46369461b77d54fc80cf9dbe05395
source core/src/main/java/art/arcane/iris/util/project/interpolation/NoiseSampleCache2D.java 2a7c13310ec82c1211b73e4925bd0262fab32fc7e474ab4c41c9f803e18e107b
source core/src/main/java/art/arcane/iris/util/project/matter/IrisMatterContext.java b8eca540d775aac3ff015b12e2d1c56967443c186ea8e28678c210ce5a67c0be
source core/src/main/java/art/arcane/iris/util/project/matter/IrisMatterSupport.java 0ed6c5fceaed9c5f88a6f431e5264707f402a63fc95307b1fee175fea6d50354
@@ -203,7 +203,7 @@ public class IrisPregenerator {
// fires — leaving a phantom job that suppresses spawns and blocks future pregens.
try {
init();
task.iterateAllChunks((_a, _b) -> totalChunks.incrementAndGet());
totalChunks.set(task.chunkCount());
long startedAt = M.ms();
startTime.set(startedAt);
rateTracker.reset(startedAt, generated.get());
@@ -215,6 +215,11 @@ public class PregenTask {
return new int[]{bound.minX(), bound.minZ(), bound.maxX(), bound.maxZ()};
}
public long chunkCount() {
Bound bound = bounds.chunk();
return (long) bound.sizeX() * bound.sizeZ();
}
@FunctionalInterface
public interface InterleavedChunkConsumer {
boolean on(int regionX, int regionZ, int chunkX, int chunkZ, boolean firstChunkInRegion, boolean lastChunkInRegion);
@@ -952,7 +952,7 @@ public final class StudioOpenCoordinator {
}
public boolean prepareGeneratorState() {
return this != JIGSAW;
return this == STANDARD || this == FORCED_STANDARD;
}
public IrisCreator.DatapackPreparation datapackPreparation() {
@@ -38,6 +38,9 @@ import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.IrisGeneratorStyle;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.engine.object.IrisInterpolator;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisRegion;
@@ -67,6 +70,7 @@ import lombok.Getter;
import lombok.ToString;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
@@ -133,6 +137,8 @@ public class IrisComplex implements DataProvider {
private ProceduralStream<IrisBiome> seaBiomeStream;
private ProceduralStream<IrisBiome> shoreBiomeStream;
private ProceduralStream<IrisBiome> baseBiomeStream;
@Getter(AccessLevel.NONE)
private final transient boolean reuseNaturalBaseBiome;
private ProceduralStream<UUID> baseBiomeIDStream;
private ProceduralStream<IrisBiome> naturalTrueBiomeStream;
private ProceduralStream<IrisBiome> unblendedNaturalTrueBiomeStream;
@@ -228,6 +234,7 @@ public class IrisComplex implements DataProvider {
}
imageMapRuntime.getMappedBiomes().forEach(this::registerGenerators);
}
reuseNaturalBaseBiome = hasFixedNaturalBiomeNoise(engine.getDimension(), generatorBiomes);
inferredBiomeStreams = compileInferredBiomeStreams(
preparedRegions,
(region, inferredType) -> compileInferredBiomeStream(engine, region, inferredType, emptyBiome)
@@ -519,16 +526,19 @@ public class IrisComplex implements DataProvider {
InferredType inferredType,
IrisRegion region,
double naturalHeight,
double x,
double z
int x,
int z
) {
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
if (mapped != null) {
return mapped;
}
IrisBiome baseBiome = implode(sampleInferredBiome(region, inferredType, x, z), x, z);
IrisContext context = IrisContext.get();
IrisBiome baseBiome = reuseNaturalBaseBiome && (context == null || context.getChunkContext() == null)
? baseBiomeStream.get(x, z)
: implode(sampleInferredBiome(region, inferredType, x, z), (double) x, (double) z);
IrisBiome resolved = resolveNaturalSurfaceBiome(naturalHeight, baseBiome, region, x, z);
return resolved == baseBiome ? baseBiome : implode(resolved, x, z);
return resolved == baseBiome ? baseBiome : implode(resolved, (double) x, (double) z);
}
private IrisBiome resolveNaturalSurfaceBiome(
@@ -1371,6 +1381,12 @@ public class IrisComplex implements DataProvider {
return 0D;
}
double height = 0D;
if (low == high && Double.isFinite(low)) {
for (int i = 0; i < generators.length; i++) {
height += low;
}
return height / generators.length;
}
for (IrisGenerator generator : generators) {
height += M.lerp(low, high, generator.getHeight(x, z, seed));
}
@@ -1455,9 +1471,34 @@ public class IrisComplex implements DataProvider {
CNG childCell = b.getChildrenGenerator(rng, 123, b.getChildShrinkFactor());
ChildSelectionPlan childSelectionPlan = resolveChildSelectionPlan(b);
IrisBiome biome = childSelectionPlan.select(childCell, x, z).withInferredType(b.getInferredType());
if (biome == b && hasFixedNoise(b.getChildStyle())) {
return biome;
}
return implode(biome, x, z, max - 1);
}
static boolean hasFixedNaturalBiomeNoise(IrisDimension dimension, Iterable<IrisBiome> biomes) {
if (!hasFixedNoise(dimension.getRegionStyle()) || !hasFixedNoise(dimension.getContinentalStyle())
|| !hasFixedNoise(dimension.getLandBiomeStyle()) || !hasFixedNoise(dimension.getSeaBiomeStyle())) {
return false;
}
for (IrisBiome biome : biomes) {
if (!biome.getChildren().isEmpty() && !hasFixedNoise(biome.getChildStyle())) {
return false;
}
}
return true;
}
private static boolean hasFixedNoise(IrisGeneratorStyle style) {
for (IrisGeneratorStyle current = style; current != null; current = current.getFracture()) {
if (current.getExpression() != null) {
return false;
}
}
return true;
}
private ChildSelectionPlan resolveChildSelectionPlan(IrisBiome biome) {
ChildSelectionPlan cachedPlan = childSelectionPlans.get(biome);
if (cachedPlan != null) {
@@ -1517,14 +1558,15 @@ public class IrisComplex implements DataProvider {
/**
* Reusable per-thread bounds provider. Holds the biome memo and the lazily computed generator
* bounds for one sampleBoundsRaw pass so the pass allocates nothing; {@link #bind}
* resets both, giving each pass the same empty-scratch semantics a fresh allocation had.
* resets generator bounds while retaining bounded coordinate samples for the same biome stream.
* <p>
* Single threaded and non reentrant by contract, matching the thread local sample caches in
* IrisInterpolation that this provider is invoked through. If a nested pass ever does appear,
* {@link #isInUse()} makes the caller fall back to a freshly allocated sampler.
*/
private static final class BoundsSampler implements NoiseBoundsProvider {
private final CoordinateBiomeCache sampleCache = new CoordinateBiomeCache(64);
private final CoordinateBiomeCache sampleCache = new CoordinateBiomeCache(4096);
private WeakReference<ProceduralStream<IrisBiome>> sampleSource = new WeakReference<>(null);
private final IdentityHashMap<IrisBiome, GeneratorBounds> localBounds = new IdentityHashMap<>(8);
private IrisComplex complex;
private Engine engine;
@@ -1542,7 +1584,10 @@ public class IrisComplex implements DataProvider {
this.generators = generators;
this.cachedBounds = cachedBounds;
this.inUse = true;
sampleCache.clear();
if (sampleSource.get() != complex.baseBiomeStream) {
sampleCache.clear();
sampleSource = new WeakReference<>(complex.baseBiomeStream);
}
if (!localBounds.isEmpty()) {
localBounds.clear();
@@ -1585,73 +1630,40 @@ public class IrisComplex implements DataProvider {
}
/**
* Open addressed biome memo keyed on the packed coordinate bits, replacing a linear scan that was
* Bounded biome memo keyed on the packed coordinate bits, replacing a linear scan that was
* quadratic in the number of columns a wide starcast touches. Single threaded by contract.
*/
private static final class CoordinateBiomeCache {
private long[] xBits;
private long[] zBits;
private IrisBiome[] values;
private byte[] states;
private int mask;
private int resizeThreshold;
private int size;
private final long[] xBits;
private final long[] zBits;
private final IrisBiome[] values;
private final int mask;
private CoordinateBiomeCache(int initialCapacity) {
int minimumCapacity = Math.max(8, initialCapacity);
int tableSize = tableSizeFor((minimumCapacity << 1) + minimumCapacity);
xBits = new long[tableSize];
zBits = new long[tableSize];
values = new IrisBiome[tableSize];
states = new byte[tableSize];
mask = tableSize - 1;
resizeThreshold = Math.max(1, (tableSize * 3) >> 2);
size = 0;
private CoordinateBiomeCache(int capacity) {
xBits = new long[capacity];
zBits = new long[capacity];
values = new IrisBiome[capacity];
mask = capacity - 1;
}
private void clear() {
if (size == 0) {
return;
}
Arrays.fill(states, (byte) 0);
size = 0;
Arrays.fill(values, null);
}
private IrisBiome get(double x, double z) {
int slot = findSlot(Double.doubleToLongBits(x), Double.doubleToLongBits(z));
return states[slot] == 0 ? null : values[slot];
long xb = Double.doubleToLongBits(x);
long zb = Double.doubleToLongBits(z);
int slot = mix(xb, zb) & mask;
return xBits[slot] == xb && zBits[slot] == zb ? values[slot] : null;
}
private void put(double x, double z, IrisBiome biome) {
long xb = Double.doubleToLongBits(x);
long zb = Double.doubleToLongBits(z);
int slot = findSlot(xb, zb);
boolean occupied = states[slot] != 0;
int slot = mix(xb, zb) & mask;
xBits[slot] = xb;
zBits[slot] = zb;
values[slot] = biome;
states[slot] = 1;
if (occupied) {
return;
}
size++;
if (size >= resizeThreshold) {
grow();
}
}
private int findSlot(long xb, long zb) {
int slot = mix(xb, zb) & mask;
while (states[slot] != 0) {
if (xBits[slot] == xb && zBits[slot] == zb) {
break;
}
slot = (slot + 1) & mask;
}
return slot;
}
private int mix(long xb, long zb) {
@@ -1662,49 +1674,6 @@ public class IrisComplex implements DataProvider {
hash ^= (hash >>> 33);
return (int) hash;
}
private void grow() {
long[] previousXBits = xBits;
long[] previousZBits = zBits;
IrisBiome[] previousValues = values;
byte[] previousStates = states;
int nextLength = previousXBits.length << 1;
xBits = new long[nextLength];
zBits = new long[nextLength];
values = new IrisBiome[nextLength];
states = new byte[nextLength];
mask = nextLength - 1;
resizeThreshold = Math.max(1, (nextLength * 3) >> 2);
size = 0;
for (int i = 0; i < previousStates.length; i++) {
if (previousStates[i] == 0) {
continue;
}
int slot = findSlot(previousXBits[i], previousZBits[i]);
xBits[slot] = previousXBits[i];
zBits[slot] = previousZBits[i];
values[slot] = previousValues[i];
states[slot] = 1;
size++;
}
}
private int tableSizeFor(int value) {
int n = value - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
int tableSize = n + 1;
if (tableSize < 8) {
return 8;
}
return tableSize;
}
}
static final class ChildSelectionPlan {
@@ -447,6 +447,36 @@ public class IrisEngine implements Engine {
return packRoot.resolve(".iris").resolve("studio-hydrology");
}
public void startStudioEntryHydrology(int blockX, int blockZ) {
GenerationRuntimeBinding binding = getActiveGenerationRuntimeBinding();
if (!backgroundTasks.scheduleTrackedTask(() -> prepareStudioEntryHydrology(binding, blockX, blockZ))) {
throw new IllegalStateException("Iris background task admission closed before Studio entry preparation.");
}
}
private void prepareStudioEntryHydrology(GenerationRuntimeBinding binding, int blockX, int blockZ) {
if (closing.get()) {
return;
}
GenerationSessionLease lease;
try {
lease = acquireGenerationLease("studio_entry_hydrology");
} catch (GenerationSessionException failure) {
if (closing.get()) {
return;
}
throw new IllegalStateException("Studio entry hydrology preparation could not acquire its generation session.", failure);
}
try (lease;
GenerationRuntimeScope runtimeScope = generationRuntimeScopes.open(binding);
IrisContext.Scope context = IrisContext.open(this, lease.sessionId(), null)) {
getComplex().getHydrologyRuntime().prepareChunkColumns(blockX, blockZ);
} catch (Exception failure) {
throw new IllegalStateException("Studio entry hydrology preparation failed at "
+ blockX + "," + blockZ + ".", failure);
}
}
private void startGenerationCacheWarm(long phaseStartedAtNanos) {
if (!backgroundTasks.scheduleTrackedTask(() -> warmGenerationCaches(phaseStartedAtNanos))) {
throw new IllegalStateException("Iris background task admission closed before generation cache warming.");
@@ -1466,6 +1496,7 @@ public class IrisEngine implements Engine {
public enum InitializationMode {
RUNTIME(false, true),
STUDIO(true, true),
OBJECT_STUDIO(true, false),
JIGSAW_STUDIO(true, false);
private final boolean studio;
@@ -1694,7 +1694,7 @@ final class HydrologyFootprintCompiler {
if (cached != null) {
return cached;
}
HydrologyTerrainSample sampled = naturalSampler.sampleBasis(x, z);
HydrologyTerrainSample sampled = naturalSampler.sampleBasisWithoutSlope(x, z);
if (sampled != null) {
terrainBases.put(packed, sampled);
}
@@ -4,18 +4,17 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinWorkerThread;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Runs a batch of independent planning tasks in parallel without ever parking a pool worker on work
* that lives in another pool. On a ForkJoin worker the tasks are forked into that worker's own pool
* and joined with work helping, so a saturated pool still finishes them on the joining thread; on any
* other thread they run on the fallback executor, or inline when there is none. Results keep the
* task order and a task's unchecked exception is rethrown as is.
* Runs independent planning tasks and returns results in request order, using the current pool or fallback.
* A worker claims its own queued tasks before waiting, and never helps unrelated work while holding
* an owner draft. Every started task finishes before the batch returns or throws.
*/
public final class HydrologyForkJoin {
private HydrologyForkJoin() {
@@ -23,48 +22,45 @@ public final class HydrologyForkJoin {
public static <T> List<T> invokeAll(List<Callable<T>> tasks, Executor fallback) {
Objects.requireNonNull(tasks, "tasks");
if (tasks.size() < 2 || fallback == null && !(Thread.currentThread() instanceof ForkJoinWorkerThread)) {
return runInline(tasks);
}
if (Thread.currentThread() instanceof ForkJoinWorkerThread) {
return forkJoin(tasks);
}
return runOn(tasks, fallback);
}
private static <T> List<T> runInline(List<Callable<T>> tasks) {
ArrayList<T> results = new ArrayList<>(tasks.size());
for (Callable<T> task : tasks) {
results.add(call(task));
}
return results;
}
private static <T> List<T> forkJoin(List<Callable<T>> tasks) {
ArrayList<ForkJoinTask<T>> forked = new ArrayList<>(tasks.size());
for (Callable<T> task : tasks) {
forked.add(ForkJoinTask.adapt(task));
}
ForkJoinTask.invokeAll(forked);
ArrayList<T> results = new ArrayList<>(tasks.size());
for (ForkJoinTask<T> task : forked) {
results.add(task.join());
}
return results;
}
private static <T> List<T> runOn(List<Callable<T>> tasks, Executor executor) {
ArrayList<CompletableFuture<T>> futures = new ArrayList<>(tasks.size());
for (Callable<T> task : tasks) {
futures.add(CompletableFuture.supplyAsync(() -> call(task), executor));
}
ArrayList<T> results = new ArrayList<>(tasks.size());
for (CompletableFuture<T> future : futures) {
try {
results.add(future.join());
} catch (CompletionException failure) {
throw unwrap(failure.getCause());
boolean callerRuns = Thread.currentThread() instanceof ForkJoinWorkerThread;
if (tasks.size() < 2 || fallback == null && !callerRuns) {
ArrayList<T> results = new ArrayList<>(tasks.size());
for (Callable<T> task : tasks) {
results.add(call(task));
}
return results;
}
Executor executor = callerRuns ? ((ForkJoinWorkerThread) Thread.currentThread()).getPool() : fallback;
ArrayList<Task<T>> pending = new ArrayList<>(tasks.size());
for (Callable<T> task : tasks) {
pending.add(new Task<>(task));
}
Throwable failure = null;
int submitted = callerRuns ? 1 : 0;
for (int index = callerRuns ? 1 : 0; index < pending.size(); index++) {
try {
executor.execute(pending.get(index));
submitted = index + 1;
} catch (RuntimeException | Error rejected) {
failure = rejected;
break;
}
}
ArrayList<T> results = new ArrayList<>(tasks.size());
int drainCount = callerRuns ? pending.size() : submitted;
for (int index = 0; index < drainCount; index++) {
try {
results.add(pending.get(index).await(callerRuns));
} catch (RuntimeException | Error failed) {
if (failure == null) {
failure = failed;
} else if (failure != failed) {
failure.addSuppressed(failed);
}
}
}
if (failure != null) {
throw unwrap(failure);
}
return results;
}
@@ -80,12 +76,92 @@ public final class HydrologyForkJoin {
}
private static RuntimeException unwrap(Throwable cause) {
if (cause instanceof RuntimeException runtimeException) {
return runtimeException;
}
if (cause instanceof Error error) {
throw error;
}
if (cause instanceof RuntimeException runtime) {
return runtime;
}
return new IllegalStateException("Hydrology planning task failed.", cause);
}
static final class Task<T> implements Runnable, ForkJoinPool.ManagedBlocker {
private final Callable<T> work;
private final AtomicBoolean claimed = new AtomicBoolean();
private final CountDownLatch complete = new CountDownLatch(1);
private T result;
private Throwable failure;
Task(Callable<T> work) {
this.work = Objects.requireNonNull(work);
}
T await() {
return await(true);
}
private T await(boolean claimPending) {
if (claimPending) {
run();
}
boolean interrupted = false;
boolean managed = true;
Throwable blockingFailure = null;
while (!isReleasable()) {
try {
if (managed) {
ForkJoinPool.managedBlock(this);
} else {
complete.await();
}
} catch (InterruptedException interruption) {
interrupted = true;
managed = false;
} catch (RejectedExecutionException unavailable) {
managed = false;
} catch (RuntimeException | Error failed) {
blockingFailure = failed;
managed = false;
}
}
if (interrupted) {
Thread.currentThread().interrupt();
}
if (blockingFailure != null) {
if (failure != null && failure != blockingFailure) {
blockingFailure.addSuppressed(failure);
}
throw unwrap(blockingFailure);
}
if (failure != null) {
throw unwrap(failure);
}
return result;
}
@Override
public boolean isReleasable() {
return complete.getCount() == 0;
}
@Override
public boolean block() throws InterruptedException {
complete.await();
return true;
}
@Override
public void run() {
if (!claimed.compareAndSet(false, true)) {
return;
}
try {
result = call(work);
} catch (RuntimeException | Error failed) {
failure = failed;
} finally {
complete.countDown();
}
}
}
}
@@ -27,6 +27,7 @@ import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -39,6 +40,7 @@ import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ForkJoinWorkerThread;
import java.util.function.IntPredicate;
public final class HydrologyPlanner {
@@ -124,6 +126,7 @@ public final class HydrologyPlanner {
private final int minimumY;
private final HydrologyCaveVoxelViewFactory caveViewFactory;
private final Cache<HydrologyTileKey, SourceRoutingContext> routingContexts;
private final ConcurrentHashMap<HydrologyTileKey, RoutingContextLoad> resolvingRoutingContexts;
private final Cache<HydrologyTileKey, CrossTileResolvedOwner> resolvedOwners;
private final Cache<RefinedEdgeKey, List<HydrologyPoint>> refinedEdgeCache;
private final ConcurrentHashMap<HydrologyTileKey, CompletableFuture<CrossTileResolvedOwner>> resolvingOwners;
@@ -237,6 +240,7 @@ public final class HydrologyPlanner {
this.routingContexts = Caffeine.newBuilder()
.maximumSize(ROUTING_CONTEXT_CACHE_SIZE)
.build();
this.resolvingRoutingContexts = new ConcurrentHashMap<>();
this.resolvedOwners = Caffeine.newBuilder()
.maximumSize(RESOLVED_OWNER_CACHE_SIZE)
.build();
@@ -252,7 +256,7 @@ public final class HydrologyPlanner {
this.routeWormDetail = new SimplexNoise(HydrologyHash.mix(worldSeed, ROUTE_WORM_DETAIL_SALT));
this.surfaceCourseBuilder = new SurfaceCourseBuilder(
settings.surface(),
this::sampleBasis,
this::sampleBasisWithoutSlope,
geometrySampler,
settings.seaLevel(),
settings.routing().minimumSurfaceCourseLength()
@@ -260,7 +264,7 @@ public final class HydrologyPlanner {
// A tributary only has to be a real reach before it joins its stem: half the course minimum.
this.tributaryCourseBuilder = new SurfaceCourseBuilder(
settings.surface(),
this::sampleBasis,
this::sampleBasisWithoutSlope,
geometrySampler,
settings.seaLevel(),
settings.routing().minimumSurfaceCourseLength() / 2
@@ -418,6 +422,10 @@ public final class HydrologyPlanner {
sourceRoutingContexts
);
phaseStarted = profile.record(DraftPhase.SELECT, phaseStarted);
if (crossTileAdmission != null && (!surfaceSelection.selectedCandidateIndices.isEmpty()
|| !undergroundSelection.selectedCandidateIndices.isEmpty())) {
crossTileAdmission.prepare();
}
}
HydrologyFootprintCompiler footprintCompiler = new HydrologyFootprintCompiler(
settings,
@@ -780,15 +788,24 @@ public final class HydrologyPlanner {
try {
int ownerRank = ownerColorRank(key);
ColorRankedDraftAdmission admission = new ColorRankedDraftAdmission(key, ownerRank, context);
HydrologyOwnerDraft draft = compileOwnerDraft(
key,
admission,
key.equals(context.root())
);
HydrologyOwnerDraft draft;
try (admission) {
try {
draft = compileOwnerDraft(
key,
admission,
key.equals(context.root())
);
} catch (RuntimeException | Error failure) {
admission.primaryFailure = failure;
throw failure;
}
}
CrossTileResolvedOwner resolved = new CrossTileResolvedOwner(draft, admission.observedRejections());
context.remember(key, resolved);
resolvedOwners.put(key, resolved.withoutFootprintCompiler());
owned.complete(resolved);
CrossTileResolvedOwner shared = resolved.withoutFootprintCompiler();
resolvedOwners.put(key, shared);
owned.complete(shared);
return resolved;
} catch (Throwable failure) {
owned.completeExceptionally(failure);
@@ -803,7 +820,8 @@ public final class HydrologyPlanner {
HydrologyTileKey ownerKey,
HydrologyCaveCourseFilter.Result result,
int ownerRank,
CrossTileResolutionContext context
CrossTileResolutionContext context,
Map<HydrologyTileKey, HydrologyForkJoin.Task<CrossTileResolvedOwner>> preparedOwners
) {
ArrayList<HydrologyCrossTileCaveAdmission.RankedClaim> blockers = new ArrayList<>();
ArrayList<HydrologyCrossTileSurfaceAdmission.RankedClaim> surfaceBlockers = new ArrayList<>();
@@ -822,7 +840,7 @@ public final class HydrologyPlanner {
candidateKeys.add(candidateKey);
}
long dependenciesStarted = System.nanoTime();
List<CrossTileResolvedOwner> resolvedCandidates = resolveLowerRankOwners(candidateKeys, context);
List<CrossTileResolvedOwner> resolvedCandidates = resolveLowerRankOwners(candidateKeys, context, preparedOwners);
currentDraftProfile().recordDependencies(candidateKeys.size(), dependenciesStarted);
for (int index = 0; index < candidateKeys.size(); index++) {
HydrologyTileKey candidateKey = candidateKeys.get(index);
@@ -848,20 +866,26 @@ public final class HydrologyPlanner {
private List<CrossTileResolvedOwner> resolveLowerRankOwners(
List<HydrologyTileKey> candidateKeys,
CrossTileResolutionContext context
CrossTileResolutionContext context,
Map<HydrologyTileKey, HydrologyForkJoin.Task<CrossTileResolvedOwner>> preparedOwners
) {
if (candidateKeys.size() < 2 || !IrisPlatforms.isBound()) {
ArrayList<CrossTileResolvedOwner> resolved = new ArrayList<>(candidateKeys.size());
for (HydrologyTileKey candidateKey : candidateKeys) {
resolved.add(resolveCrossTileOwner(candidateKey, context));
HydrologyForkJoin.Task<CrossTileResolvedOwner> prepared = preparedOwners.get(candidateKey);
CrossTileResolvedOwner owner = prepared == null
? resolveCrossTileOwner(candidateKey, context) : prepared.await();
context.remember(candidateKey, owner);
resolved.add(owner);
}
return List.copyOf(resolved);
}
// Neighbour drafts fork into the pool this thread belongs to (or the burst pool from outside
// any pool) and are joined with work helping, so a saturated pool never deadlocks on them.
// Neighbour drafts use the current pool (or the burst pool from outside any pool).
// Waiting workers claim their own tasks without helping unrelated owner drafts.
ArrayList<Callable<CrossTileResolvedOwner>> tasks = new ArrayList<>(candidateKeys.size());
for (HydrologyTileKey candidateKey : candidateKeys) {
tasks.add(() -> resolveIndependentOwner(candidateKey));
HydrologyForkJoin.Task<CrossTileResolvedOwner> prepared = preparedOwners.get(candidateKey);
tasks.add(prepared == null ? () -> resolveIndependentOwner(candidateKey) : prepared::await);
}
List<CrossTileResolvedOwner> owners = HydrologyForkJoin.invokeAll(tasks, MultiBurst.burst);
ArrayList<CrossTileResolvedOwner> resolved = new ArrayList<>(candidateKeys.size());
@@ -1227,7 +1251,11 @@ public final class HydrologyPlanner {
if (includeDeepFluids && grid != null) {
compileSeaCaves(grid, courses, diagnostics);
}
DraftProfile profile = currentDraftProfile();
long rasterStarted = System.nanoTime();
HydrologyFootprintCompiler.ValidationRaster validation = footprintCompiler.compileValidation(courses);
profile.rasterNanos += System.nanoTime() - rasterStarted;
profile.rasterCalls++;
HydrologyObservedPlannedSurface plannedSurface = new HydrologyObservedPlannedSurface(
validation.plannedSurface()
);
@@ -1243,6 +1271,7 @@ public final class HydrologyPlanner {
samples == null ? null : samples.caveCandidates;
HydrologyCaveContainmentPlanner.ValidationCache validationCache =
samples == null ? null : samples.caveValidations;
long filterStarted = System.nanoTime();
HydrologyCaveCourseFilter.Result containment = new HydrologyCaveCourseFilter(
caveView,
new HydrologyCaveCourseFilter.Options(
@@ -1261,6 +1290,8 @@ public final class HydrologyPlanner {
validation,
diagnostics
);
profile.filterNanos += System.nanoTime() - filterStarted;
profile.filterCalls++;
return new PublicationAttempt(containment, diagnostics);
}
@@ -1650,7 +1681,7 @@ public final class HydrologyPlanner {
settled = true;
break;
}
int floor = sampleBasis(point.x(), point.z()).naturalHeight() - maximumIncision;
int floor = sampleBasisWithoutSlope(point.x(), point.z()).naturalHeight() - maximumIncision;
int lowered = Math.min(point.y(), Math.max(Math.max(allowed, floor), downstreamHead));
if (lowered < point.y()) {
centerline.set(pointIndex, new HydrologyPoint(point.x(), lowered, point.z()));
@@ -3031,7 +3062,36 @@ public final class HydrologyPlanner {
}
private SourceRoutingContext sourceRoutingContext(HydrologyTileKey key) {
return routingContexts.get(key, this::compileSourceRoutingContext);
SourceRoutingContext cached = routingContexts.getIfPresent(key);
if (cached != null) {
return cached;
}
RoutingContextLoad owned = new RoutingContextLoad(Thread.currentThread(), new CompletableFuture<>());
RoutingContextLoad existing = resolvingRoutingContexts.putIfAbsent(key, owned);
if (existing != null) {
if (existing.owner() == Thread.currentThread()) {
throw new IllegalStateException("Hydrology routing context cannot recursively load its own tile.");
}
try {
return existing.future().join();
} catch (CompletionException failure) {
throw propagateOwnerFailure(failure.getCause());
}
}
try {
SourceRoutingContext resolved = routingContexts.getIfPresent(key);
if (resolved == null) {
resolved = compileSourceRoutingContext(key);
routingContexts.put(key, resolved);
}
owned.future().complete(resolved);
return resolved;
} catch (Throwable failure) {
owned.future().completeExceptionally(failure);
throw propagateOwnerFailure(failure);
} finally {
resolvingRoutingContexts.remove(key, owned);
}
}
private SourceRoutingContext compileSourceRoutingContext(HydrologyTileKey key) {
@@ -3487,16 +3547,19 @@ public final class HydrologyPlanner {
int desiredZ,
double maximumOffsetRatio
) {
HydrologyTerrainSample terrain = sampleLandBasis(x, z);
double distanceFromNode = StrictMath.hypot(x - node.x(), z - node.z());
if (distanceFromNode > settings.routing().sampleSpacing() * maximumOffsetRatio) {
return Double.POSITIVE_INFINITY;
}
HydrologyTerrainSample terrain = sampleLandBasisWithoutSlope(x, z);
if (terrain == null || !terrain.transitAllowed() || !withinConfines(terrain, node.terrain().confinesKey())) {
return Double.POSITIVE_INFINITY;
}
double distanceFromDesired = StrictMath.hypot(x - desiredX, z - desiredZ);
double distanceFromNode = StrictMath.hypot(x - node.x(), z - node.z());
if (distanceFromNode > settings.routing().sampleSpacing() * maximumOffsetRatio
|| !traversableHop(node.naturalPoint(), new HydrologyPoint(x, terrain.naturalHeight(), z))) {
if (!traversableHop(node.naturalPoint(), new HydrologyPoint(x, terrain.naturalHeight(), z))) {
return Double.POSITIVE_INFINITY;
}
terrain = sampleLandBasis(x, z);
double distanceFromDesired = StrictMath.hypot(x - desiredX, z - desiredZ);
double score = terrain.naturalHeight() * settings.routing().valleyPreference()
+ terrain.slope() * settings.routing().slopePenalty()
+ terrain.routingCost() * terrain.routingMultiplier()
@@ -3637,7 +3700,7 @@ public final class HydrologyPlanner {
ArrayList<HydrologyPoint> points = new ArrayList<>(direct.size());
for (int index = 0; index < direct.size(); index++) {
HydrologyPoint point = direct.get(index);
HydrologyTerrainSample terrain = sampleLandBasis(point.x(), point.z());
HydrologyTerrainSample terrain = sampleLandBasisWithoutSlope(point.x(), point.z());
if (terrain == null || !terrain.transitAllowed()) {
return List.of();
}
@@ -3661,7 +3724,7 @@ public final class HydrologyPlanner {
for (int direction : List.of(-1, 1)) {
int x = (int) StrictMath.round(point.x() + perpendicularX * refinement * direction);
int z = (int) StrictMath.round(point.z() + perpendicularZ * refinement * direction);
HydrologyTerrainSample terrain = sampleLandBasis(x, z);
HydrologyTerrainSample terrain = sampleLandBasisWithoutSlope(x, z);
if (terrain != null && terrain.transitAllowed()) {
return true;
}
@@ -3827,11 +3890,13 @@ public final class HydrologyPlanner {
candidates.put(packed, candidate);
}
}
if (!candidates.isEmpty()) {
return List.copyOf(candidates.values());
}
int fallbackX = (int) StrictMath.round(nominal.baseX());
int fallbackZ = (int) StrictMath.round(nominal.baseZ());
HydrologyTerrainSample fallbackTerrain = sampleLandBasis(fallbackX, fallbackZ);
if (candidates.isEmpty()
&& fallbackTerrain != null
if (fallbackTerrain != null
&& fallbackTerrain.transitAllowed()
&& withinConfines(fallbackTerrain, confines)) {
long packed = RiverFootprint.pack(fallbackX, fallbackZ);
@@ -4141,6 +4206,7 @@ public final class HydrologyPlanner {
}
double[][] localPenalties = new double[layerCount][];
double[][][] transitionCosts = new double[layerCount][][];
double[][][][] turnCosts = new double[layerCount][][][];
for (int layerIndex = 0; layerIndex < layerCount; layerIndex++) {
int layerSize = layers.get(layerIndex).size();
localPenalties[layerIndex] = new double[layerSize];
@@ -4163,7 +4229,8 @@ public final class HydrologyPlanner {
maximumTurn,
turnCost,
localPenalties,
transitionCosts
transitionCosts,
turnCosts
);
if (selection.route().length == 0) {
return selection.route();
@@ -4187,7 +4254,8 @@ public final class HydrologyPlanner {
double maximumTurn,
double turnCost,
double[][] localPenalties,
double[][][] transitionCosts
double[][][] transitionCosts,
double[][][][] turnCosts
) {
int layerCount = layers.size();
double[][][] costs = new double[layerCount][][];
@@ -4222,6 +4290,9 @@ public final class HydrologyPlanner {
List<RouteCandidate> currentLayer = layers.get(layerIndex);
costs[layerIndex] = new double[previousLayer.size()][currentLayer.size()];
predecessors[layerIndex] = new int[previousLayer.size()][currentLayer.size()];
if (turnCosts[layerIndex] == null) {
turnCosts[layerIndex] = new double[previousLayer.size()][currentLayer.size()][];
}
for (int previousIndex = 0; previousIndex < previousLayer.size(); previousIndex++) {
Arrays.fill(costs[layerIndex][previousIndex], Double.POSITIVE_INFINITY);
Arrays.fill(predecessors[layerIndex][previousIndex], -1);
@@ -4243,20 +4314,32 @@ public final class HydrologyPlanner {
if (!Double.isFinite(previousCost)) {
continue;
}
RouteCandidate before = beforeLayer.get(beforeIndex);
double turn = routeTurnDegrees(
before.point(),
previous.point(),
current.point()
);
if (turn > maximumTurn) {
double[] candidateTurns = turnCosts[layerIndex][previousIndex][currentIndex];
if (candidateTurns == null) {
candidateTurns = new double[beforeLayer.size()];
Arrays.fill(candidateTurns, Double.NaN);
turnCosts[layerIndex][previousIndex][currentIndex] = candidateTurns;
}
double turnPenalty = candidateTurns[beforeIndex];
if (Double.isNaN(turnPenalty)) {
double turn = routeTurnDegrees(
beforeLayer.get(beforeIndex).point(),
previous.point(),
current.point()
);
turnPenalty = turn > maximumTurn
? Double.POSITIVE_INFINITY
: curvatureCost(turn, maximumTurn, turnCost);
candidateTurns[beforeIndex] = turnPenalty;
}
if (turnPenalty == Double.POSITIVE_INFINITY) {
continue;
}
double cost = previousCost
+ current.localScore()
+ localPenalty
+ transition
+ curvatureCost(turn, maximumTurn, turnCost);
+ turnPenalty;
if (cost < costs[layerIndex][previousIndex][currentIndex]) {
costs[layerIndex][previousIndex][currentIndex] = cost;
predecessors[layerIndex][previousIndex][currentIndex] = beforeIndex;
@@ -4480,8 +4563,8 @@ public final class HydrologyPlanner {
if (offsetX == 0 && offsetZ == 0) {
return false;
}
HydrologyTerrainSample before = sampleLandBasis(point.x() - offsetX, point.z() - offsetZ);
HydrologyTerrainSample after = sampleLandBasis(point.x() + offsetX, point.z() + offsetZ);
HydrologyTerrainSample before = sampleLandBasisWithoutSlope(point.x() - offsetX, point.z() - offsetZ);
HydrologyTerrainSample after = sampleLandBasisWithoutSlope(point.x() + offsetX, point.z() + offsetZ);
int threshold = settings.hydraulics().waterfallMinimumDrop();
return before != null
&& after != null
@@ -4504,7 +4587,7 @@ public final class HydrologyPlanner {
HydrologyPoint next = current.get(pointIndex + 1);
int x = (int) StrictMath.round(previous.x() * 0.25D + point.x() * 0.5D + next.x() * 0.25D);
int z = (int) StrictMath.round(previous.z() * 0.25D + point.z() * 0.5D + next.z() * 0.25D);
HydrologyTerrainSample terrain = sampleLandBasis(x, z);
HydrologyTerrainSample terrain = sampleLandBasisWithoutSlope(x, z);
smoothed.add(terrain == null || !terrain.transitAllowed()
? point
: new HydrologyPoint(x, terrain.naturalHeight(), z));
@@ -4661,8 +4744,8 @@ public final class HydrologyPlanner {
if (crossing.size() < 3) {
return false;
}
HydrologyTerrainSample startTerrain = sampleLandBasis(start.x(), start.z());
HydrologyTerrainSample endTerrain = sampleLandBasis(end.x(), end.z());
HydrologyTerrainSample startTerrain = sampleLandBasisWithoutSlope(start.x(), start.z());
HydrologyTerrainSample endTerrain = sampleLandBasisWithoutSlope(end.x(), end.z());
if (startTerrain == null || endTerrain == null) {
return true;
}
@@ -4670,7 +4753,7 @@ public final class HydrologyPlanner {
int threshold = settings.hydraulics().waterfallMinimumDrop();
for (int pointIndex = 1; pointIndex < crossing.size() - 1; pointIndex++) {
HydrologyPoint point = crossing.get(pointIndex);
HydrologyTerrainSample terrain = sampleLandBasis(point.x(), point.z());
HydrologyTerrainSample terrain = sampleLandBasisWithoutSlope(point.x(), point.z());
if (terrain == null || boundaryHeight - terrain.naturalHeight() >= threshold) {
return true;
}
@@ -5275,7 +5358,7 @@ public final class HydrologyPlanner {
preferredHeads[0] = initialHead;
for (int index = 0; index < pairCount; index++) {
HydrologyPoint point = path.points().get(index);
HydrologyTerrainSample terrain = sampleBasis(point.x(), point.z());
HydrologyTerrainSample terrain = sampleBasisWithoutSlope(point.x(), point.z());
int discharge = Math.max(1, path.pairEdges().get(index).contributingUndergroundSources());
widths[index] = scaledDimension(
sampleGeometry(
@@ -5978,8 +6061,8 @@ public final class HydrologyPlanner {
for (int pointIndex = 0; pointIndex < raster.size() - 1; pointIndex++) {
HydrologyPoint current = raster.get(pointIndex);
HydrologyPoint downstream = raster.get(pointIndex + 1);
HydrologyTerrainSample currentTerrain = sampleBasis(current.x(), current.z());
HydrologyTerrainSample downstreamTerrain = sampleBasis(downstream.x(), downstream.z());
HydrologyTerrainSample currentTerrain = sampleBasisWithoutSlope(current.x(), current.z());
HydrologyTerrainSample downstreamTerrain = sampleBasisWithoutSlope(downstream.x(), downstream.z());
long decline = (long) currentTerrain.naturalHeight() - downstreamTerrain.naturalHeight();
if (decline > strongestDecline) {
strongestDecline = decline;
@@ -6078,11 +6161,29 @@ public final class HydrologyPlanner {
edges.getFirst().upstreamNodeId(),
outlet.id()
);
List<HydrologyPoint> points = surfaceCourseCenterline(
Objects.requireNonNull(grid, "Surface course routing grid is required"),
surfaceGuide,
geometryId
);
Objects.requireNonNull(grid, "Surface course routing grid is required");
PlanningSamples samples = planningSamples.get();
DraftProfile profile = currentDraftProfile();
profile.routeCalls++;
List<HydrologyPoint> points;
if (samples == null) {
profile.routeSolves++;
long solveStarted = System.nanoTime();
points = surfaceCourseCenterline(grid, surfaceGuide, geometryId);
profile.routeSolveNanos += System.nanoTime() - solveStarted;
} else {
HashMap<SurfaceRouteKey, List<HydrologyPoint>> routes = samples.surfaceRoutes.computeIfAbsent(
grid, ignored -> new HashMap<>()
);
SurfaceRouteKey routeKey = new SurfaceRouteKey(surfaceGuide, geometryId);
points = routes.computeIfAbsent(routeKey, ignored -> {
profile.routeSolves++;
long solveStarted = System.nanoTime();
List<HydrologyPoint> solved = surfaceCourseCenterline(grid, surfaceGuide, geometryId);
profile.routeSolveNanos += System.nanoTime() - solveStarted;
return solved;
});
}
if (points.size() < 2) {
return null;
}
@@ -7865,6 +7966,7 @@ public final class HydrologyPlanner {
private final HashMap<Integer, List<GridOffset>> radialOffsets;
private final HashMap<UndergroundSegmentCapKey, Integer> undergroundSegmentCaps;
private final HashMap<Long, HydrologyPoint> routeAnchors;
private final IdentityHashMap<SampledGrid, HashMap<SurfaceRouteKey, List<HydrologyPoint>>> surfaceRoutes;
private final HydrologyCaveCourseFilter.CandidateCache caveCandidates;
private final HydrologyCaveContainmentPlanner.ValidationCache caveValidations;
@@ -7878,6 +7980,7 @@ public final class HydrologyPlanner {
this.radialOffsets = new HashMap<>();
this.undergroundSegmentCaps = new HashMap<>();
this.routeAnchors = new HashMap<>();
this.surfaceRoutes = new IdentityHashMap<>();
this.caveCandidates = new HydrologyCaveCourseFilter.CandidateCache();
this.caveValidations = new HydrologyCaveContainmentPlanner.ValidationCache();
}
@@ -7924,6 +8027,9 @@ public final class HydrologyPlanner {
private record RoutePosition(double x, double z, double baseX, double baseZ, Direction tangent) {
}
private record SurfaceRouteKey(List<HydrologyPoint> guide, long geometryId) {
}
private record RouteCandidate(
HydrologyPoint point,
double offset,
@@ -8307,6 +8413,9 @@ public final class HydrologyPlanner {
) {
}
private record RoutingContextLoad(Thread owner, CompletableFuture<SourceRoutingContext> future) {
}
private record SourceRoutingContext(
SampledGrid grid,
RoutingPlan surfaceRouting,
@@ -8385,16 +8494,20 @@ public final class HydrologyPlanner {
}
}
@FunctionalInterface
private interface CrossTileDraftAdmission {
void prepare();
CrossTilePublicationAdmission admit(HydrologyCaveCourseFilter.Result result);
}
private final class ColorRankedDraftAdmission implements CrossTileDraftAdmission {
private final class ColorRankedDraftAdmission implements CrossTileDraftAdmission, AutoCloseable {
private final HydrologyTileKey ownerKey;
private final int ownerRank;
private final CrossTileResolutionContext context;
private final Set<Long> rejectedCourseIds;
private final Map<HydrologyTileKey, HydrologyForkJoin.Task<CrossTileResolvedOwner>> preparedOwners = new LinkedHashMap<>();
private final Set<HydrologyTileKey> demandedOwners = new HashSet<>();
private Throwable primaryFailure;
private final LinkedHashMap<CrossTileRejectionKey, CrossTileRejectedCourse> observedRejections;
private ColorRankedDraftAdmission(
@@ -8409,6 +8522,77 @@ public final class HydrologyPlanner {
this.observedRejections = new LinkedHashMap<>();
}
@Override
public void prepare() {
if (ownerRank == 0 || settings.crossTileColorPeriod() != 2 || !hasRoutedSourceSearch()
|| !(Thread.currentThread() instanceof ForkJoinWorkerThread worker)) {
return;
}
int radius = settings.crossTileColorPeriod() - 1;
ArrayList<HydrologyTileKey> candidates = new ArrayList<>();
for (long tileZ = (long) ownerKey.tileZ() - radius; tileZ <= (long) ownerKey.tileZ() + radius; tileZ++) {
for (long tileX = (long) ownerKey.tileX() - radius; tileX <= (long) ownerKey.tileX() + radius; tileX++) {
if (tileX < Integer.MIN_VALUE || tileX > Integer.MAX_VALUE
|| tileZ < Integer.MIN_VALUE || tileZ > Integer.MAX_VALUE) {
continue;
}
HydrologyTileKey key = new HydrologyTileKey((int) tileX, (int) tileZ);
if (ownerColorRank(key) >= ownerRank || preparedOwners.containsKey(key)
|| resolvedOwners.getIfPresent(key) != null || resolvingOwners.containsKey(key)) {
continue;
}
candidates.add(key);
}
}
candidates.sort(Comparator.comparingInt(HydrologyPlanner.this::ownerColorRank).reversed()
.thenComparing(HydrologyTileKey::compareTo));
for (HydrologyTileKey key : candidates) {
if (resolvedOwners.getIfPresent(key) != null || resolvingOwners.containsKey(key)) {
continue;
}
HydrologyForkJoin.Task<CrossTileResolvedOwner> task = new HydrologyForkJoin.Task<>(() -> resolveIndependentOwner(key));
preparedOwners.put(key, task);
try {
worker.getPool().execute(task);
} catch (RuntimeException | Error failure) {
preparedOwners.remove(key);
throw failure;
}
currentDraftProfile().earlyOwners++;
}
}
@Override
public void close() {
Error failure = null;
Map<HydrologyTileKey, RuntimeException> unusedFailures = new LinkedHashMap<>();
for (Map.Entry<HydrologyTileKey, HydrologyForkJoin.Task<CrossTileResolvedOwner>> entry : preparedOwners.entrySet()) {
try {
entry.getValue().await();
} catch (RuntimeException unusedFailure) {
if (!demandedOwners.contains(entry.getKey())) {
unusedFailures.put(entry.getKey(), unusedFailure);
}
} catch (Error failed) {
if (failed == primaryFailure) {
continue;
}
if (failure == null) {
failure = failed;
} else if (failure != failed) {
failure.addSuppressed(failed);
}
}
}
for (Map.Entry<HydrologyTileKey, RuntimeException> entry : unusedFailures.entrySet()) {
IrisLogging.reportError("Unused early hydrology owner " + entry.getKey().tileX()
+ "," + entry.getKey().tileZ() + " failed to prepare.", entry.getValue());
}
if (failure != null) {
throw failure;
}
}
@Override
public CrossTilePublicationAdmission admit(HydrologyCaveCourseFilter.Result result) {
context.recordIteration();
@@ -8416,11 +8600,13 @@ public final class HydrologyPlanner {
result,
rejectedCourseIds
);
demandedOwners.addAll(conflictOwnerKeys(ownerKey, current));
CrossTileBlockers blockers = lowerRankBlockers(
ownerKey,
current,
ownerRank,
context
context,
preparedOwners
);
HydrologyCrossTileSurfaceAdmission.Result surfaceAdmission =
HydrologyCrossTileSurfaceAdmission.admit(
@@ -8536,6 +8722,14 @@ public final class HydrologyPlanner {
private long dependencyNanos;
private int dependencies;
private int admissions;
private int earlyOwners;
private int routeCalls;
private int routeSolves;
private long routeSolveNanos;
private int rasterCalls;
private long rasterNanos;
private int filterCalls;
private long filterNanos;
private long record(DraftPhase phase, long started) {
long now = System.nanoTime();
@@ -8551,7 +8745,7 @@ public final class HydrologyPlanner {
private void log(HydrologyTileKey key, int rank, long totalNanos) {
IrisLogging.debug(
"Hydrology owner %d,%d rank=%d drafted in %dms: context=%dms select=%dms settle=%dms publish=%dms deps=%d wait=%dms admissions=%d on %s",
"Hydrology owner %d,%d rank=%d drafted in %dms: context=%dms select=%dms settle=%dms publish=%dms deps=%d wait=%dms admissions=%d earlyOwners=%d routes=%d reuses=%d routeSolve=%dms rasters=%d raster=%dms filters=%d filter=%dms on %s",
key.tileX(),
key.tileZ(),
rank,
@@ -8563,6 +8757,14 @@ public final class HydrologyPlanner {
dependencies,
dependencyNanos / 1_000_000L,
admissions,
earlyOwners,
routeCalls,
routeCalls - routeSolves,
routeSolveNanos / 1_000_000L,
rasterCalls,
rasterNanos / 1_000_000L,
filterCalls,
filterNanos / 1_000_000L,
Thread.currentThread().getName()
);
}
@@ -10,6 +10,7 @@ import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;
@@ -48,12 +49,17 @@ public final class HydrologyTileCache implements AutoCloseable {
private final Executor prefetchExecutor;
private final ConcurrentLinkedQueue<HydrologyTileKey> prefetchQueue;
private final Set<HydrologyTileKey> queuedPrefetches;
private final Set<CacheLoadKey<HydrologyTileKey>> demandedPlans;
private final AtomicBoolean prefetchActive;
private final ConcurrentHashMap<HydrologyTileKey, CompletableFuture<HydrologyTile>> planning;
private final ConcurrentHashMap<CacheLoadKey<HydrologyTileKey>, CompletableFuture<HydrologyTile>> planning;
private final ConcurrentHashMap<CacheLoadKey<HydrologyTileKey>, PendingLoad<HydrologyTile>> loading;
private final ConcurrentHashMap<CacheLoadKey<Long>, PendingLoad<ChunkColumns>> composing;
private final Object publicationLock = new Object();
private final BooleanSupplier waitingForbidden;
private volatile SharedCacheScope sharedCacheScope;
private volatile StudioHydrologyTileStore persistentStore;
private volatile boolean neighbourPrefetchEnabled;
private int demandBatches;
public HydrologyTileCache(HydrologyPlanner planner) {
this(planner, DEFAULT_MAXIMUM_ENTRIES, null, null, null);
@@ -96,12 +102,15 @@ public final class HydrologyTileCache implements AutoCloseable {
this.sharedCacheScope = sharedCacheScope;
this.persistentStore = null;
this.planning = new ConcurrentHashMap<>();
this.loading = new ConcurrentHashMap<>();
this.composing = new ConcurrentHashMap<>();
if (maximumEntries < 1) {
throw new IllegalArgumentException("maximumEntries must be positive.");
}
this.maximumEntries = maximumEntries;
this.prefetchQueue = new ConcurrentLinkedQueue<>();
this.queuedPrefetches = ConcurrentHashMap.newKeySet();
this.demandedPlans = new HashSet<>();
this.prefetchActive = new AtomicBoolean();
this.tiles = Caffeine.newBuilder()
.maximumSize(maximumEntries)
@@ -122,10 +131,13 @@ public final class HydrologyTileCache implements AutoCloseable {
*/
public HydrologyTile get(HydrologyTileKey key) {
Objects.requireNonNull(key, "key");
if (prefetchExecutor == null || plansInline()) {
return planInline(key);
if (prefetchExecutor == null) {
return planInline(key, cacheEpoch.get());
}
return awaitTile(planDemandAsync(key));
if (plansInline()) {
return planDemandInline(key);
}
return awaitPlan(planDemandAsync(key));
}
private boolean plansInline() {
@@ -137,12 +149,33 @@ public final class HydrologyTileCache implements AutoCloseable {
&& worker.getPool() == pool;
}
private HydrologyTile planInline(HydrologyTileKey key) {
if (tiles.getIfPresent(key) == null && planning.containsKey(key)) {
IrisLogging.debug("Hydrology tile %d,%d awaited by %s while the planning pool plans it",
key.tileX(), key.tileZ(), Thread.currentThread().getName());
private HydrologyTile planInline(HydrologyTileKey key, long epoch) {
HydrologyTile present = tiles.getIfPresent(key);
if (present != null) {
return present;
}
CacheLoadKey<HydrologyTileKey> loadKey = new CacheLoadKey<>(epoch, key);
PendingLoad<HydrologyTile> owned = new PendingLoad<>(Thread.currentThread(), new CompletableFuture<>());
PendingLoad<HydrologyTile> existing = loading.putIfAbsent(loadKey, owned);
if (existing != null) {
return awaitLoad(existing);
}
try {
present = tiles.getIfPresent(key);
HydrologyTile tile = present == null ? planOrEmpty(key, epoch) : present;
synchronized (publicationLock) {
if (!closed.get() && cacheEpoch.get() == epoch) {
tiles.put(key, tile);
}
}
owned.future().complete(tile);
return tile;
} catch (Throwable failure) {
owned.future().completeExceptionally(failure);
throw failure;
} finally {
loading.remove(loadKey, owned);
}
return tiles.get(key, this::planOrEmpty);
}
/**
@@ -152,8 +185,7 @@ public final class HydrologyTileCache implements AutoCloseable {
* stays consistent. A plan that was interrupted is not a bad tile: it is rethrown without being
* cached so the next request plans it again.
*/
private HydrologyTile planOrEmpty(HydrologyTileKey key) {
long planningEpoch = cacheEpoch.get();
private HydrologyTile planOrEmpty(HydrologyTileKey key, long planningEpoch) {
SharedTileKey sharedKey = sharedCacheScope == null ? null : new SharedTileKey(sharedCacheScope, key);
HydrologyTile shared = sharedKey == null ? null : SHARED_TILES.getIfPresent(sharedKey);
if (shared != null && validSharedTile(shared, sharedKey)) {
@@ -176,10 +208,12 @@ public final class HydrologyTileCache implements AutoCloseable {
}
try {
HydrologyTile planned = planner.plan(key);
if (sharedKey != null && !closed.get() && planningEpoch == cacheEpoch.get()) {
HydrologyTile existing = SHARED_TILES.asMap().putIfAbsent(sharedKey, planned);
if (existing == null) {
IrisLogging.debug("Published shared Studio hydrology tile %d,%d", key.tileX(), key.tileZ());
synchronized (publicationLock) {
if (sharedKey != null && !closed.get() && planningEpoch == cacheEpoch.get()) {
HydrologyTile existing = SHARED_TILES.asMap().putIfAbsent(sharedKey, planned);
if (existing == null) {
IrisLogging.debug("Published shared Studio hydrology tile %d,%d", key.tileX(), key.tileZ());
}
}
}
if (store != null && persistentStudioKey(key) && !closed.get() && planningEpoch == cacheEpoch.get()) {
@@ -277,14 +311,18 @@ public final class HydrologyTileCache implements AutoCloseable {
}
public void clear() {
cacheEpoch.incrementAndGet();
planner.clearOwnerDrafts();
synchronized (prefetchQueue) {
synchronized (publicationLock) {
cacheEpoch.incrementAndGet();
planner.clearOwnerDrafts();
tiles.invalidateAll();
composedChunks.invalidateAll();
}
prefetchQueue.clear();
queuedPrefetches.clear();
demandedPlans.clear();
demandBatches = 0;
}
tiles.invalidateAll();
composedChunks.invalidateAll();
localChunkColumns.remove();
}
@@ -302,16 +340,19 @@ public final class HydrologyTileCache implements AutoCloseable {
}
public void preparePregeneration(int centerBlockX, int centerBlockZ) {
neighbourPrefetchEnabled = true;
int tileSize = planner.settings().routing().tileSize();
prefetchArea(
Math.subtractExact(centerBlockX, tileSize),
Math.subtractExact(centerBlockZ, tileSize),
Math.addExact(centerBlockX, tileSize),
Math.addExact(centerBlockZ, tileSize),
int minimumBlockX = Math.subtractExact(centerBlockX, tileSize);
int minimumBlockZ = Math.subtractExact(centerBlockZ, tileSize);
int maximumBlockX = Math.addExact(centerBlockX, tileSize);
int maximumBlockZ = Math.addExact(centerBlockZ, tileSize);
enqueuePrefetchArea(prefetchAreaKeys(
minimumBlockX,
minimumBlockZ,
maximumBlockX,
maximumBlockZ,
centerBlockX,
centerBlockZ
);
), true);
}
public void enableSharedCache(SharedCacheScope scope, Path persistentRoot) {
@@ -366,14 +407,41 @@ public final class HydrologyTileCache implements AutoCloseable {
// The empty result is never cached, so the next query composes the real columns.
return ChunkColumns.empty(chunkX, chunkZ);
}
ChunkColumns columns = composedChunks.get(
packedChunk,
ignored -> composeChunkColumns(chunkX, chunkZ)
);
ChunkColumns columns = loadChunkColumns(new CacheLoadKey<>(epoch, packedChunk), chunkX, chunkZ);
localChunkColumns.set(new LocalChunkColumns(epoch, columns));
return columns;
}
private ChunkColumns loadChunkColumns(CacheLoadKey<Long> loadKey, int chunkX, int chunkZ) {
long epoch = loadKey.epoch();
long packedChunk = loadKey.key();
ChunkColumns present = composedChunks.getIfPresent(packedChunk);
if (present != null) {
return present;
}
PendingLoad<ChunkColumns> owned = new PendingLoad<>(Thread.currentThread(), new CompletableFuture<>());
PendingLoad<ChunkColumns> existing = composing.putIfAbsent(loadKey, owned);
if (existing != null) {
return awaitLoad(existing);
}
try {
present = composedChunks.getIfPresent(packedChunk);
ChunkColumns columns = present == null ? composeChunkColumns(chunkX, chunkZ) : present;
synchronized (publicationLock) {
if (!closed.get() && cacheEpoch.get() == epoch) {
composedChunks.put(packedChunk, columns);
}
}
owned.future().complete(columns);
return columns;
} catch (Throwable failure) {
owned.future().completeExceptionally(failure);
throw failure;
} finally {
composing.remove(loadKey, owned);
}
}
/** The tiles a chunk's columns compose from: every tile within the publication radius of the chunk. */
private ArrayList<HydrologyTileKey> relevantKeys(int chunkX, int chunkZ) {
int minimumBlockX = Math.multiplyExact(chunkX, CHUNK_SIZE);
@@ -447,6 +515,12 @@ public final class HydrologyTileCache implements AutoCloseable {
if (prefetchExecutor == null) {
return;
}
enqueuePrefetchArea(prefetchAreaKeys(minimumBlockX, minimumBlockZ, maximumBlockX, maximumBlockZ,
centreBlockX, centreBlockZ), false);
}
private List<HydrologyTileKey> prefetchAreaKeys(int minimumBlockX, int minimumBlockZ, int maximumBlockX,
int maximumBlockZ, int centreBlockX, int centreBlockZ) {
int tileSize = planner.settings().routing().tileSize();
int publicationRadius = planner.settings().publicationRadius();
int minimumTileX = tileCoordinate((long) minimumBlockX - publicationRadius, tileSize);
@@ -464,31 +538,66 @@ public final class HydrologyTileCache implements AutoCloseable {
keys.sort(Comparator.comparingInt((HydrologyTileKey key) -> Math.max(Math.abs(key.tileX() - centreTileX), Math.abs(key.tileZ() - centreTileZ)))
.thenComparingInt(HydrologyTileKey::tileZ)
.thenComparingInt(HydrologyTileKey::tileX));
for (HydrologyTileKey key : keys) {
if (!prefetch(key) && speculativeEntryCount() >= maximumEntries) {
break;
return keys;
}
private void enqueuePrefetchArea(List<HydrologyTileKey> keys, boolean discardOutsideArea) {
Set<HydrologyTileKey> area = discardOutsideArea ? new HashSet<>(keys) : Set.of();
tiles.cleanUp();
synchronized (prefetchQueue) {
if (discardOutsideArea) {
prefetchQueue.removeIf(key -> {
if (!area.contains(key)) {
queuedPrefetches.remove(key);
return true;
}
return false;
});
neighbourPrefetchEnabled = true;
}
if (prefetchExecutor == null) {
return;
}
for (HydrologyTileKey key : keys) {
if (!enqueuePrefetch(key) && speculativeEntryCount() >= maximumEntries) {
break;
}
}
}
pumpPrefetch();
}
private boolean prefetch(HydrologyTileKey key) {
if (closed.get()
|| tiles.getIfPresent(key) != null
|| planning.containsKey(new CacheLoadKey<>(cacheEpoch.get(), key))
|| queuedPrefetches.contains(key)) {
return false;
}
tiles.cleanUp();
synchronized (prefetchQueue) {
if (closed.get()
|| tiles.getIfPresent(key) != null
|| planning.containsKey(key)
|| queuedPrefetches.contains(key)
|| speculativeEntryCount() >= maximumEntries) {
if (!enqueuePrefetch(key)) {
return false;
}
queuedPrefetches.add(key);
prefetchQueue.add(key);
}
pumpPrefetch();
return true;
}
private boolean enqueuePrefetch(HydrologyTileKey key) {
if (closed.get()
|| tiles.getIfPresent(key) != null
|| planning.containsKey(new CacheLoadKey<>(cacheEpoch.get(), key))
|| queuedPrefetches.contains(key)
|| speculativeEntryCount() >= maximumEntries) {
return false;
}
queuedPrefetches.add(key);
prefetchQueue.add(key);
return true;
}
private int speculativeEntryCount() {
tiles.cleanUp();
return Math.toIntExact(Math.min(
maximumEntries,
tiles.estimatedSize() + planning.size() + queuedPrefetches.size()
@@ -496,22 +605,30 @@ public final class HydrologyTileCache implements AutoCloseable {
}
private void pumpPrefetch() {
if (prefetchExecutor == null || closed.get() || !prefetchActive.compareAndSet(false, true)) {
return;
synchronized (prefetchQueue) {
if (prefetchExecutor == null || closed.get() || demandBatches != 0 || !demandedPlans.isEmpty()
|| !prefetchActive.compareAndSet(false, true)) {
return;
}
}
scheduleNextPrefetch();
}
private void scheduleNextPrefetch() {
HydrologyTileKey key = pollPrefetch();
if (key == null || closed.get()) {
prefetchActive.set(false);
if (!prefetchQueue.isEmpty()) {
pumpPrefetch();
CacheLoadKey<HydrologyTileKey> loadKey;
synchronized (prefetchQueue) {
if (closed.get() || demandBatches != 0 || !demandedPlans.isEmpty()) {
prefetchActive.set(false);
return;
}
return;
HydrologyTileKey key = pollPrefetch();
if (key == null) {
prefetchActive.set(false);
return;
}
loadKey = new CacheLoadKey<>(cacheEpoch.get(), key);
}
planAsync(key).whenComplete((tile, failure) -> scheduleNextPrefetch());
planAsync(loadKey).whenComplete((tile, failure) -> scheduleNextPrefetch());
}
private HydrologyTileKey pollPrefetch() {
@@ -536,65 +653,154 @@ public final class HydrologyTileCache implements AutoCloseable {
public List<HydrologyTile> tiles(List<HydrologyTileKey> keys) {
Objects.requireNonNull(keys, "keys");
HydrologyTile[] loaded = new HydrologyTile[keys.size()];
if (prefetchExecutor == null || plansInline()) {
boolean missing = false;
for (int index = 0; index < loaded.length; index++) {
loaded[index] = tiles.getIfPresent(keys.get(index));
missing |= loaded[index] == null;
}
if (!missing) {
return List.of(loaded);
}
if (prefetchExecutor == null) {
for (int index = 0; index < loaded.length; index++) {
loaded[index] = planInline(keys.get(index));
if (loaded[index] == null) {
loaded[index] = get(keys.get(index));
}
}
return List.of(loaded);
}
int batch = Math.max(1, maximumEntries / 2);
ArrayList<CompletableFuture<HydrologyTile>> futures = new ArrayList<>(Math.min(batch, loaded.length));
for (int start = 0; start < loaded.length; start += batch) {
int end = Math.min(loaded.length, start + batch);
futures.clear();
for (int index = start; index < end; index++) {
futures.add(planDemandAsync(keys.get(index)));
long demandEpoch = beginDemandBatch();
try {
if (plansInline()) {
for (int index = 0; index < loaded.length; index++) {
if (loaded[index] == null) {
loaded[index] = get(keys.get(index));
}
}
return List.of(loaded);
}
for (int index = start; index < end; index++) {
loaded[index] = awaitTile(futures.get(index - start));
int batch = Math.max(1, maximumEntries / 2);
ArrayList<CompletableFuture<HydrologyTile>> futures = new ArrayList<>(Math.min(batch, loaded.length));
for (int start = 0; start < loaded.length; start += batch) {
int end = Math.min(loaded.length, start + batch);
futures.clear();
for (int index = start; index < end; index++) {
futures.add(planDemandAsync(keys.get(index)));
}
for (int index = start; index < end; index++) {
loaded[index] = awaitPlan(futures.get(index - start));
}
}
return List.of(loaded);
} finally {
finishDemandBatch(demandEpoch);
}
}
private long beginDemandBatch() {
synchronized (prefetchQueue) {
demandBatches++;
return cacheEpoch.get();
}
}
private void finishDemandBatch(long epoch) {
synchronized (prefetchQueue) {
if (epoch == cacheEpoch.get()) {
demandBatches--;
}
}
return List.of(loaded);
pumpPrefetch();
}
/**
* The tile, planned on the prefetch executor unless it is cached or already being planned, in which
* case the caller shares that plan instead of occupying a second executor thread with it.
*/
private CompletableFuture<HydrologyTile> planAsync(HydrologyTileKey key) {
private CompletableFuture<HydrologyTile> planAsync(CacheLoadKey<HydrologyTileKey> loadKey) {
HydrologyTileKey key = loadKey.key();
HydrologyTile present = tiles.getIfPresent(key);
if (present != null) {
return CompletableFuture.completedFuture(present);
}
CompletableFuture<HydrologyTile> future = new CompletableFuture<>();
CompletableFuture<HydrologyTile> existing = planning.putIfAbsent(key, future);
CompletableFuture<HydrologyTile> existing = planning.putIfAbsent(loadKey, future);
if (existing != null) {
return existing;
}
try {
prefetchExecutor.execute(() -> {
try {
future.complete(planInline(key));
future.complete(planInline(key, loadKey.epoch()));
} catch (Throwable failure) {
future.completeExceptionally(failure);
IrisLogging.reportError(failure);
} finally {
planning.remove(key, future);
planning.remove(loadKey, future);
}
});
} catch (RuntimeException rejected) {
planning.remove(key, future);
planning.remove(loadKey, future);
future.completeExceptionally(rejected);
}
return future;
}
private CompletableFuture<HydrologyTile> planDemandAsync(HydrologyTileKey key) {
queuedPrefetches.remove(key);
return planAsync(key);
HydrologyTile present = tiles.getIfPresent(key);
if (present != null) {
return CompletableFuture.completedFuture(present);
}
CacheLoadKey<HydrologyTileKey> loadKey = new CacheLoadKey<>(cacheEpoch.get(), key);
boolean tracked = beginDemand(loadKey);
CompletableFuture<HydrologyTile> future = planAsync(loadKey);
if (tracked) {
future.whenComplete((tile, failure) -> finishDemand(loadKey));
}
return future;
}
private static HydrologyTile awaitTile(CompletableFuture<HydrologyTile> future) {
private HydrologyTile planDemandInline(HydrologyTileKey key) {
HydrologyTile present = tiles.getIfPresent(key);
if (present != null) {
return present;
}
CacheLoadKey<HydrologyTileKey> loadKey = new CacheLoadKey<>(cacheEpoch.get(), key);
boolean tracked = beginDemand(loadKey);
try {
return planInline(key, loadKey.epoch());
} finally {
if (tracked) {
finishDemand(loadKey);
}
}
}
private boolean beginDemand(CacheLoadKey<HydrologyTileKey> loadKey) {
synchronized (prefetchQueue) {
if (loadKey.epoch() != cacheEpoch.get()) {
return false;
}
queuedPrefetches.remove(loadKey.key());
return demandedPlans.add(loadKey);
}
}
private void finishDemand(CacheLoadKey<HydrologyTileKey> loadKey) {
synchronized (prefetchQueue) {
demandedPlans.remove(loadKey);
}
pumpPrefetch();
}
private static <T> T awaitLoad(PendingLoad<T> load) {
if (load.owner() == Thread.currentThread()) {
throw new IllegalStateException("Recursive hydrology cache load.");
}
return awaitPlan(load.future());
}
private static <T> T awaitPlan(CompletableFuture<T> future) {
try {
return future.join();
} catch (CompletionException failure) {
@@ -697,6 +903,12 @@ public final class HydrologyTileCache implements AutoCloseable {
private record LocalChunkColumns(long epoch, ChunkColumns columns) {
}
private record CacheLoadKey<K>(long epoch, K key) {
}
private record PendingLoad<T>(Thread owner, CompletableFuture<T> future) {
}
public record SharedCacheScope(
String runtimeIdentity,
long worldSeed,
@@ -24,6 +24,7 @@ import art.arcane.iris.engine.mantle.MantleWriter;
import art.arcane.iris.engine.object.IrisCaveFieldModule;
import art.arcane.iris.engine.object.IrisCaveProfile;
import art.arcane.iris.engine.object.IrisRange;
import art.arcane.iris.engine.object.IrisGeneratorStyle;
import art.arcane.iris.engine.object.IrisStyledRange;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
@@ -38,6 +39,11 @@ import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinWorkerThread;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
public class IrisCaveCarver3D {
private static final byte LIQUID_AIR = 0;
@@ -51,6 +57,12 @@ public class IrisCaveCarver3D {
private static final int SURFACE_CEILING_FADE_DEPTH = 12;
private static final double SURFACE_CEILING_SOLID_EPSILON = 0.000001D;
private static final byte DENSITY_CARVE = 1;
private static final byte DENSITY_FLUID = 2;
private static final int MINIMUM_PARALLEL_DENSITY_CELLS = 16_384;
private static final int MAXIMUM_DENSITY_WORKERS = 4;
private static final int[] LATTICE_TILE_OFFSETS = {0, 1, 16, 17};
private final Engine engine;
private final IrisData data;
private final IrisCaveProfile profile;
@@ -79,6 +91,8 @@ public class IrisCaveCarver3D {
private final int fluidMinDepthBelowSurface;
private final int fluidHeight;
private final int aquiferCeilingY;
private final boolean parallelDensity;
private final ThreadLocal<Boolean> carving = ThreadLocal.withInitial(() -> false);
private final ThreadLocal<CaveCarveScratch> scratchCache = ThreadLocal.withInitial(CaveCarveScratch::new);
public IrisCaveCarver3D(Engine engine, IrisCaveProfile profile) {
@@ -125,6 +139,7 @@ public class IrisCaveCarver3D {
hasModules = modules.length > 0;
detailMinContribution = -detailWeight;
detailMaxContribution = detailWeight;
parallelDensity = fixedDensityStyles(profile);
}
public int carve(MantleWriter writer, int chunkX, int chunkZ) {
@@ -204,6 +219,12 @@ public class IrisCaveCarver3D {
CaveFluidSupportPlan fluidSupportPlan
) {
PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start();
boolean nested = carving.get();
CaveCarveScratch previousScratch = scratchCache.get();
if (nested) {
scratchCache.set(new CaveCarveScratch());
}
carving.set(true);
try {
CaveCarveScratch scratch = scratchCache.get();
if (columnWeights == null || columnWeights.length < 256) {
@@ -415,10 +436,289 @@ public class IrisCaveCarver3D {
return carved;
} finally {
carving.set(nested);
if (nested) {
scratchCache.set(previousScratch);
}
engine.getMetrics().getCarveApply().put(applyStopwatch.getMilliseconds());
}
}
private static boolean fixedDensityStyles(IrisCaveProfile profile) {
if (!fixedStyle(profile.getBaseDensityStyle()) || !fixedStyle(profile.getDetailDensityStyle())
|| !fixedStyle(profile.getWarpStyle()) || !fixedStyle(profile.getSurfaceBreakStyle())
|| !fixedStyle(profile.getDensityThreshold().getStyle())) {
return false;
}
for (IrisCaveFieldModule module : profile.getModules()) {
if (!fixedStyle(module.getStyle())) {
return false;
}
}
return true;
}
private static boolean fixedStyle(IrisGeneratorStyle style) {
for (IrisGeneratorStyle current = style; current != null; current = current.getFracture()) {
if (current.getExpression() != null) {
return false;
}
}
return true;
}
private byte[] classifyPassParallel(DensityPass pass) {
int height = pass.maximumY() - pass.minimumY() + 1;
if (!parallelDensity || height < 64
|| (long) height * pass.columnCount() < MINIMUM_PARALLEL_DENSITY_CELLS
|| !(Thread.currentThread() instanceof ForkJoinWorkerThread worker)) {
return null;
}
ForkJoinPool pool = worker.getPool();
int workers = Math.min(MAXIMUM_DENSITY_WORKERS, pool.getParallelism());
if (workers < 2) {
return null;
}
byte[] classified = new byte[Math.multiplyExact(height, 256)];
int rows = Math.ceilDiv(height, workers);
ArrayList<Runnable> tasks = new ArrayList<>(workers);
for (int index = 0; index < workers; index++) {
int minimumY = pass.minimumY() + index * rows;
int maximumY = Math.min(pass.maximumY(), minimumY + rows - 1);
if (minimumY <= maximumY) {
tasks.add(() -> classifyRows(pass, minimumY, maximumY, classified));
}
}
runDensityTasks(tasks, pool);
return classified;
}
private void classifyRows(DensityPass pass, int minimumY, int maximumY, byte[] classified) {
CaveCarveScratch scratch = new CaveCarveScratch();
for (int y = minimumY; y <= maximumY; y++) {
int count = 0;
for (int index = 0; index < pass.columnCount(); index++) {
int column = pass.columns()[index];
if (pass.columnTops()[index] < y
|| SurfaceFluidBoundaryPlan.protects(pass.fluidBoundaries(), column, y)) {
continue;
}
double threshold = pass.thresholds()[column];
if (pass.surfaceBreakColumns()[column] && y >= pass.surfaceBreakFloors()[column]) {
threshold += pass.surfaceBreakBoost();
}
threshold -= pass.verticalFade()[y - pass.minimumY()];
threshold = applySurfaceCeilingFade(pass.sourceScratch(), threshold, column, y, pass.minimumY());
scratch.planeColumnIndices[count] = column;
scratch.planeThresholdLimit[count++] = threshold * normalizationFactor;
}
if (count == 0) {
continue;
}
if (pass.adaptiveStep() == 0) {
classifyDensityPlane(scratch, pass.x(), pass.z(), y, scratch.planeColumnIndices,
scratch.planeThresholdLimit, count, scratch.planeCarve);
} else {
classifyDensityPlaneAdaptive(scratch, pass.x(), pass.z(), y, scratch.planeColumnIndices,
scratch.planeThresholdLimit, count, scratch.planeCarve,
pass.adaptiveStep(), pass.adaptiveMargin());
}
int offset = (y - pass.minimumY()) * 256;
for (int index = 0; index < count; index++) {
if (!scratch.planeCarve[index]) {
continue;
}
int column = scratch.planeColumnIndices[index];
double threshold = scratch.planeThresholdLimit[index] * inverseNormalization;
MatterCavern matter = resolveMatter(scratch, pass.matterByY()[y - pass.minimumY()],
pass.x() + PowerOfTwoCoordinates.unpackLocal16X(column), y, pass.z() + (column & 15),
column, pass.fluidTops(), threshold);
classified[offset + column] = matter == carveFluid
? DENSITY_CARVE | DENSITY_FLUID : DENSITY_CARVE;
}
}
}
private double[] sampleLatticeParallel(
int x, int z, int minimumY, int maximumY, int step, double[] thresholds, int[] tops
) {
int rows = (maximumY - minimumY) / step + 1;
if (!parallelDensity || rows < 64
|| !(Thread.currentThread() instanceof ForkJoinWorkerThread worker)) {
return null;
}
ForkJoinPool pool = worker.getPool();
int workers = Math.min(MAXIMUM_DENSITY_WORKERS, pool.getParallelism());
if (workers < 2) {
return null;
}
int[] tileTops = new int[64];
int samples = 0;
for (int tile = 0; tile < 64; tile++) {
int column = (tile >> 3) * 32 + (tile & 7) * 2;
int top = Integer.MIN_VALUE;
for (int offset : LATTICE_TILE_OFFSETS) {
if (!Double.isNaN(thresholds[column + offset])) {
top = Math.max(top, tops[column + offset]);
}
}
tileTops[tile] = top;
if (top >= minimumY) {
samples += (top - minimumY) / step + 1;
}
}
if (samples < 4096) {
return null;
}
double[] densities = new double[Math.multiplyExact(rows, 64)];
LatticePass pass = new LatticePass(x, z, minimumY, step, rows, tileTops);
int tilesPerWorker = Math.ceilDiv(64, workers);
ArrayList<Runnable> tasks = new ArrayList<>(workers);
for (int index = 0; index < workers; index++) {
int firstTile = index * tilesPerWorker;
int lastTile = Math.min(64, firstTile + tilesPerWorker);
tasks.add(() -> sampleLatticeTiles(pass, firstTile, lastTile, densities));
}
runDensityTasks(tasks, pool);
return densities;
}
private void sampleLatticeTiles(LatticePass pass, int firstTile, int lastTile, double[] densities) {
CaveCarveScratch scratch = new CaveCarveScratch();
for (int tile = firstTile; tile < lastTile; tile++) {
int blockX = pass.x() + (tile >> 3) * 2;
int blockZ = pass.z() + (tile & 7) * 2;
int offset = tile * pass.rows();
for (int y = pass.minimumY(); y <= pass.tileTops()[tile]; y += pass.step()) {
densities[offset++] = sampleDensityOptimized(scratch, blockX, y, blockZ);
}
}
}
private static void copyClassifiedPlane(byte[] classified, int row, int[] columns, int count, boolean[] result) {
int offset = row * 256;
for (int index = 0; index < count; index++) {
result[index] = (classified[offset + columns[index]] & DENSITY_CARVE) != 0;
}
}
static void runDensityTasks(List<Runnable> work, ForkJoinPool pool) {
ArrayList<DensityTask> tasks = new ArrayList<>(work.size());
for (Runnable runnable : work) {
tasks.add(new DensityTask(runnable));
}
Throwable failure = null;
for (int index = 1; index < tasks.size(); index++) {
try {
pool.execute(tasks.get(index));
} catch (RuntimeException | Error rejected) {
failure = rejected;
break;
}
}
for (DensityTask task : tasks) {
task.run();
}
boolean interrupted = Thread.interrupted();
for (DensityTask task : tasks) {
boolean complete = false;
boolean managed = true;
while (!complete) {
try {
if (managed) {
ForkJoinPool.managedBlock(task);
} else {
task.complete.await();
}
complete = true;
} catch (InterruptedException interruption) {
interrupted = true;
managed = false;
} catch (RejectedExecutionException unavailable) {
managed = false;
} catch (RuntimeException | Error blockingFailure) {
if (failure == null) {
failure = blockingFailure;
} else if (failure != blockingFailure) {
failure.addSuppressed(blockingFailure);
}
managed = false;
}
}
if (task.failure != null) {
if (failure == null) {
failure = task.failure;
} else if (failure != task.failure) {
failure.addSuppressed(task.failure);
}
}
}
interrupted |= Thread.interrupted();
if (interrupted) {
Thread.currentThread().interrupt();
InterruptedException interruption = new InterruptedException("Cave density classification interrupted.");
if (failure == null) {
failure = new IllegalStateException(interruption);
} else {
failure.addSuppressed(interruption);
}
}
if (failure instanceof Error error) {
throw error;
}
if (failure instanceof RuntimeException runtimeFailure) {
throw runtimeFailure;
}
}
private record LatticePass(int x, int z, int minimumY, int step, int rows, int[] tileTops) {
}
private record DensityPass(
int x, int z, int minimumY, int maximumY,
int[] columns, int[] columnTops, int columnCount, double[] thresholds,
long[] fluidBoundaries, boolean[] surfaceBreakColumns, int[] surfaceBreakFloors,
double surfaceBreakBoost, double[] verticalFade, CaveCarveScratch sourceScratch,
int adaptiveStep, double adaptiveMargin, int[] fluidTops, MatterCavern[] matterByY
) {
}
private static final class DensityTask implements Runnable, ForkJoinPool.ManagedBlocker {
private final Runnable work;
private final AtomicBoolean claimed = new AtomicBoolean();
private final CountDownLatch complete = new CountDownLatch(1);
private Throwable failure;
private DensityTask(Runnable work) {
this.work = work;
}
@Override
public boolean isReleasable() {
return complete.getCount() == 0;
}
@Override
public boolean block() throws InterruptedException {
complete.await();
return true;
}
@Override
public void run() {
if (!claimed.compareAndSet(false, true)) {
return;
}
try {
work.run();
} catch (RuntimeException | Error failed) {
failure = failed;
} finally {
complete.countDown();
}
}
}
private int carvePassExact(
MantleChunk<Matter> chunk,
int x0,
@@ -474,6 +774,11 @@ public class IrisCaveCarver3D {
int passMaxY = Math.min(maxY, activeMaxY);
int maxSection = PowerOfTwoCoordinates.floorDivPow2(passMaxY, 4);
byte[] classified = skipExistingCarved ? null : classifyPassParallel(new DensityPass(
x0, z0, minY, passMaxY, activeColumnIndices, activeColumnTopY, activeColumnCount,
passThreshold, surfaceFluidBoundaries, surfaceBreakColumn, surfaceBreakFloorY,
surfaceBreakThresholdBoost, verticalEdgeFade, scratch, 0, 0D, fluidMaxY, matterByY));
for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) {
int sectionMinY = Math.max(minY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex));
int sectionMaxY = Math.min(passMaxY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex) + 15);
@@ -505,7 +810,11 @@ public class IrisCaveCarver3D {
continue;
}
classifyDensityPlane(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve);
if (classified == null) {
classifyDensityPlane(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve);
} else {
copyClassifiedPlane(classified, y - minY, planeColumnIndices, planeCount, planeCarve);
}
int fadeIndex = y - minY;
int localY = y & 15;
MatterCavern verticalMatter = matterByY[fadeIndex];
@@ -541,8 +850,11 @@ public class IrisCaveCarver3D {
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
int localZ = columnIndex & 15;
double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization;
MatterCavern matter = resolveMatter(scratch, verticalMatter, x0 + localX, y, z0 + localZ,
columnIndex, fluidMaxY, localThreshold);
MatterCavern matter = classified == null
? resolveMatter(scratch, verticalMatter, x0 + localX, y, z0 + localZ,
columnIndex, fluidMaxY, localThreshold)
: (classified[fadeIndex * 256 + columnIndex] & DENSITY_FLUID) != 0
? carveFluid : verticalMatter;
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
carved++;
}
@@ -615,6 +927,12 @@ public class IrisCaveCarver3D {
effectiveAdaptiveSampleStep
);
byte[] classified = skipExistingCarved ? null : classifyPassParallel(new DensityPass(
x0, z0, minY, passMaxY, activeColumnIndices, activeColumnTopY, activeColumnCount,
passThreshold, surfaceFluidBoundaries, surfaceBreakColumn, surfaceBreakFloorY,
surfaceBreakThresholdBoost, verticalEdgeFade, scratch, effectiveAdaptiveSampleStep, effectiveAdaptiveThresholdMargin,
fluidMaxY, matterByY));
for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) {
int sectionMinY = Math.max(minY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex));
int sectionMaxY = Math.min(passMaxY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex) + 15);
@@ -646,18 +964,22 @@ public class IrisCaveCarver3D {
continue;
}
classifyDensityPlaneAdaptive(
scratch,
x0,
z0,
y,
planeColumnIndices,
planeThresholdLimit,
planeCount,
planeCarve,
effectiveAdaptiveSampleStep,
effectiveAdaptiveThresholdMargin
);
if (classified == null) {
classifyDensityPlaneAdaptive(
scratch,
x0,
z0,
y,
planeColumnIndices,
planeThresholdLimit,
planeCount,
planeCarve,
effectiveAdaptiveSampleStep,
effectiveAdaptiveThresholdMargin
);
} else {
copyClassifiedPlane(classified, y - minY, planeColumnIndices, planeCount, planeCarve);
}
int fadeIndex = y - minY;
int localY = y & 15;
MatterCavern verticalMatter = matterByY[fadeIndex];
@@ -693,8 +1015,11 @@ public class IrisCaveCarver3D {
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
int localZ = columnIndex & 15;
double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization;
MatterCavern matter = resolveMatter(scratch, verticalMatter, x0 + localX, y, z0 + localZ,
columnIndex, fluidMaxY, localThreshold);
MatterCavern matter = classified == null
? resolveMatter(scratch, verticalMatter, x0 + localX, y, z0 + localZ,
columnIndex, fluidMaxY, localThreshold)
: (classified[fadeIndex * 256 + columnIndex] & DENSITY_FLUID) != 0
? carveFluid : verticalMatter;
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
carved++;
}
@@ -756,6 +1081,9 @@ public class IrisCaveCarver3D {
passThreshold[index] = columnThreshold[index] + thresholdBoost - ((1D - columnWeight) * thresholdPenalty);
}
double[] latticeDensity = sampleLatticeParallel(x0, z0, minY, maxY, latticeStep, passThreshold, columnMaxY);
int latticeRows = (maxY - minY) / latticeStep + 1;
for (int lx = 0; lx < 16; lx += 2) {
int x = x0 + lx;
int lx1 = lx + 1;
@@ -815,7 +1143,9 @@ public class IrisCaveCarver3D {
}
for (int y = minY; y <= tileMaxY; y += latticeStep) {
double density = sampleDensityOptimized(scratch, x, y, z);
double density = latticeDensity == null
? sampleDensityOptimized(scratch, x, y, z)
: latticeDensity[((lx >> 1) * 8 + (lz >> 1)) * latticeRows + (y - minY) / latticeStep];
int stampMaxY = Math.min(maxY, y + 1);
for (int yy = y; yy <= stampMaxY; yy++) {
MatterCavern verticalMatter = matterByY[yy - minY];
@@ -1018,11 +1018,17 @@ public class IrisDimension extends IrisRegistrant {
}
private static String sanitizeRegistryNamespace(String value) {
if (isCanonicalRegistryValue(value, false)) {
return value;
}
String sanitized = value.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9_.-]", "_");
return sanitized.isBlank() ? "dimension" : sanitized;
}
private static String sanitizeRegistryPath(String value, String fallback) {
if (isCanonicalRegistryValue(value, true)) {
return value;
}
if (value == null || value.isBlank()) {
return fallback;
}
@@ -1037,6 +1043,29 @@ public class IrisDimension extends IrisRegistrant {
return sanitized.isBlank() ? fallback : sanitized;
}
private static boolean isCanonicalRegistryValue(String value, boolean path) {
if (value == null || value.isEmpty()) {
return false;
}
char previous = 0;
for (int i = 0; i < value.length(); i++) {
char current = value.charAt(i);
if (current == '/') {
if (!path || i == 0 || i == value.length() - 1 || previous == '/') {
return false;
}
} else if (!((current >= 'a' && current <= 'z') || (current >= '0' && current <= '9')
|| current == '_' || current == '-' || current == '.')) {
return false;
}
if (path && current == '.' && previous == '.') {
return false;
}
previous = current;
}
return true;
}
public IrisDimensionType getDimensionType() {
IrisDimensionTypeOptions options = getDimensionOptions();
if (fullbright) {
@@ -33,6 +33,7 @@ import art.arcane.iris.core.gui.PregeneratorJob;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.runtime.ObjectStudioActivation;
import art.arcane.iris.core.runtime.ObjectStudioLayout;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession;
import art.arcane.iris.core.service.StudioSVC;
@@ -138,6 +139,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
private volatile StudioMode lastMode;
private volatile UUID lastJigsawStudioRequestId;
private volatile boolean jigsawStudioActive;
private volatile boolean objectStudioActive;
private volatile DummyBiomeProvider dummyBiomeProvider;
private volatile Throwable initializationFailure;
private volatile IrisDimension validatedDimension;
@@ -258,6 +260,9 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
/** Starts nearest-first hydrology planning around a new world's initial spawn. */
private void prefetchSpawnHydrology(Engine engine, World world) {
if (usesFlatStudioTerrain()) {
return;
}
if (engine.getComplex() == null || engine.getComplex().getHydrologyRuntime() == null) {
return;
}
@@ -267,6 +272,11 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
if (world.isChunkGenerated(spawnX >> 4, spawnZ >> 4)) {
return;
}
if (studio && engine.getDimension().getStudioMode() == StudioMode.NORMAL
&& engine instanceof IrisEngine irisEngine) {
irisEngine.startStudioEntryHydrology(spawnX, spawnZ);
return;
}
int reach = Math.max(16, hydrologyTileSize() / 2);
engine.getComplex().getHydrologyRuntime().prefetchArea(
spawnX - reach,
@@ -401,6 +411,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
jigsawStudioActive = request != null
&& session != null
&& session.sessionId().equals(request.requestId());
objectStudioActive = studio && ObjectStudioActivation.isActive(packKey);
IrisEngine createdEngine = createEngine(engineTarget);
if (generationHistory != null) {
long initialActivationId = generationHistory.activeActivation().activationId();
@@ -433,7 +444,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
}
}
createdEngine.setNativeStructureVolumeQueriesEnabled(shouldGenerateNativeStructures(
jigsawStudioActive,
isAuthoringStudio(),
studioEntryBootstrapActive.get(),
initializationFailure != null));
engine = createdEngine;
@@ -471,7 +482,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
}
private IrisEngine createEngine(EngineTarget engineTarget) {
IrisEngine.InitializationMode mode = selectInitializationMode(studio, jigsawStudioActive);
IrisEngine.InitializationMode mode = selectInitializationMode(studio, jigsawStudioActive, objectStudioActive);
if (generationHistory == null) {
return new IrisEngine(engineTarget, mode);
}
@@ -501,12 +512,14 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
return new IllegalStateException("Unable to attach Iris generation history.", failure);
}
static IrisEngine.InitializationMode selectInitializationMode(boolean studio, boolean jigsawStudioActive) {
static IrisEngine.InitializationMode selectInitializationMode(boolean studio, boolean jigsawStudioActive, boolean objectStudioActive) {
if (!studio) {
return IrisEngine.InitializationMode.RUNTIME;
}
return jigsawStudioActive
? IrisEngine.InitializationMode.JIGSAW_STUDIO
: objectStudioActive
? IrisEngine.InitializationMode.OBJECT_STUDIO
: IrisEngine.InitializationMode.STUDIO;
}
@@ -721,7 +734,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
Engine activeEngine = engine;
if (activeEngine instanceof IrisEngine irisEngine) {
irisEngine.setNativeStructureVolumeQueriesEnabled(shouldGenerateNativeStructures(
jigsawStudioActive,
isAuthoringStudio(),
false,
initializationFailure != null));
}
@@ -736,6 +749,33 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
return jigsawStudioActive;
}
public boolean isAuthoringStudio() {
return jigsawStudioActive || objectStudioActive;
}
public boolean usesFlatStudioTerrain() {
Engine activeEngine = engine;
return usesFlatStudioTerrain(studio, isAuthoringStudio(),
activeEngine == null ? null : activeEngine.getDimension().getStudioMode());
}
static boolean usesFlatStudioTerrain(boolean studio, boolean authoringStudio, StudioMode mode) {
return studio && (authoringStudio || mode == StudioMode.OBJECT_BUFFET);
}
public int getAuthoringFloorY() {
return Math.max(engine.getMinHeight(), ObjectStudioLayout.FLOOR_Y);
}
public int getAuthoringBaseHeight() {
return authoringBaseHeight(engine.getMinHeight(), engine.getMaxHeight());
}
static int authoringBaseHeight(int minimumY, int maximumY) {
int floorY = Math.max(minimumY, ObjectStudioLayout.FLOOR_Y);
return floorY < maximumY ? floorY + 1 : minimumY;
}
@Override
public void hotload() {
if (!shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)) {
@@ -1087,6 +1127,9 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
@Override
public int getBaseHeight(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z, @NotNull HeightMap heightMap) {
Engine currentEngine = getEngine(worldInfo);
if (usesFlatStudioTerrain()) {
return getAuthoringBaseHeight();
}
boolean ignoreFluid = switch (heightMap) {
case OCEAN_FLOOR, OCEAN_FLOOR_WG -> true;
@@ -1163,17 +1206,17 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
@Override
public boolean shouldGenerateStructures() {
return shouldGenerateNativeStructures(
jigsawStudioActive,
isAuthoringStudio(),
studioEntryBootstrapActive.get(),
initializationFailure != null);
}
static boolean shouldGenerateNativeStructures(
boolean jigsawStudioActive,
boolean authoringStudio,
boolean studioEntryBootstrapActive,
boolean initializationFailed
) {
return !jigsawStudioActive && !studioEntryBootstrapActive && !initializationFailed;
return !authoringStudio && !studioEntryBootstrapActive && !initializationFailed;
}
@Override
@@ -62,6 +62,12 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
private final ConcurrentMap<RenderKey, RenderedBay> renderedBays = new ConcurrentHashMap<>();
private final ConcurrentMap<String, PlatformBlockState> jigsawStates = new ConcurrentHashMap<>();
public static boolean isLightFloor(int blockX, int blockZ) {
int tileX = Math.floorDiv(blockX, CHECKER_SIZE);
int tileZ = Math.floorDiv(blockZ, CHECKER_SIZE);
return ((tileX + tileZ) & 1) == 0;
}
public JigsawStudioGenerator(
Engine engine,
JigsawStudioActivation.Request request,
@@ -262,9 +268,7 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
for (int localZ = 0; localZ < 16; localZ++) {
int worldX = chunkWorldX + localX;
int worldZ = chunkWorldZ + localZ;
int tileX = Math.floorDiv(worldX, CHECKER_SIZE);
int tileZ = Math.floorDiv(worldZ, CHECKER_SIZE);
PlatformBlockState block = ((tileX + tileZ) & 1) == 0 ? lightFloor : darkFloor;
PlatformBlockState block = isLightFloor(worldX, worldZ) ? lightFloor : darkFloor;
terrainChunk.setBlock(localX, floorY, localZ, block);
}
}
@@ -93,9 +93,11 @@ public class ObjectStudioGenerator extends EnginedStudioGenerator {
}
private void generateChunkWithinSession(Engine engine, TerrainChunk tc, int x, int z) {
ensureLayout(engine);
int floorY = Math.max(engine.getMinHeight(), ObjectStudioLayout.FLOOR_Y);
if (floorY >= tc.getMaxHeight()) {
return;
}
ensureLayout(engine);
for (int bx = 0; bx < 16; bx++) {
for (int bz = 0; bz < 16; bz++) {
tc.setBiome(bx, floorY, bz, DEFAULT_BIOME);
@@ -667,13 +667,21 @@ public class IrisInterpolation {
// outer pass's table — its entries belong to a different bound provider.
cache = new NoiseBoundsSampleCache2D(64);
}
NoiseSampleCache2D scalarCache = usesSampleCache(method) ? NOISE_SAMPLE_CACHE_2D.get() : null;
boolean ownsScalarCache = scalarCache != null && !scalarCache.isInUse();
cache.beginUse();
NoiseBoundsProvider previous = cache.bindProvider(noise);
if (ownsScalarCache) {
scalarCache.beginUse();
}
try {
double min = getNoise(method, x, z, h, cache.minView());
double max = getNoise(method, x, z, h, cache.maxView());
double min = dispatch(method, x, z, h, cache.minView());
double max = dispatch(method, x, z, h, cache.replayMaxView());
return new NoiseBounds(min, max);
} finally {
if (ownsScalarCache) {
scalarCache.endUse();
}
cache.bindProvider(previous);
cache.endUse();
}
@@ -24,15 +24,15 @@ import java.util.Arrays;
/**
* Open-addressed memo table that samples a {@link NoiseBoundsProvider} once per column and serves
* both the min and max side from the same entry. Single threaded by contract; instances are held in
* the minimum while recording maxima for an identical second interpolation pass. Single threaded; held in
* thread locals.
* <p>
* The min/max {@link NoiseProvider} views are allocated once per cache instance and read the
* currently bound provider, so a bounds interpolation pass allocates no lambdas per column.
* The minimum view samples the bound provider; the maximum view replays the same sampling sequence.
* Both views are allocated once per cache instance.
*/
final class NoiseBoundsSampleCache2D {
private final NoiseProvider minView = this::sampleMin;
private final NoiseProvider maxView = this::sampleMax;
private final NoiseProvider replayMaxView = this::replayMax;
private NoiseBoundsProvider boundProvider;
private long[] xBits;
private long[] zBits;
@@ -43,6 +43,9 @@ final class NoiseBoundsSampleCache2D {
private int resizeThreshold;
private int size;
private boolean inUse;
private double[] sampledMaximums = new double[64];
private int sampledMaximumCount;
private int maximumReplayIndex;
public NoiseBoundsSampleCache2D(int initialCapacity) {
int minimumCapacity = Math.max(8, initialCapacity);
@@ -75,6 +78,8 @@ final class NoiseBoundsSampleCache2D {
public void beginUse() {
inUse = true;
sampledMaximumCount = 0;
maximumReplayIndex = 0;
clear();
}
@@ -83,7 +88,7 @@ final class NoiseBoundsSampleCache2D {
}
/**
* Binds the provider that {@link #minView()} and {@link #maxView()} delegate to, returning the
* Binds the provider that {@link #minView()} samples, returning the
* previously bound provider so callers can restore it.
*/
public NoiseBoundsProvider bindProvider(NoiseBoundsProvider provider) {
@@ -96,44 +101,34 @@ final class NoiseBoundsSampleCache2D {
return minView;
}
public NoiseProvider maxView() {
return maxView;
public NoiseProvider replayMaxView() {
return replayMaxView;
}
private double sampleMin(double sampleX, double sampleZ) {
return getOrSampleMin(sampleX, sampleZ, boundProvider);
}
private double sampleMax(double sampleX, double sampleZ) {
return getOrSampleMax(sampleX, sampleZ, boundProvider);
}
public double getOrSampleMin(double sampleX, double sampleZ, NoiseBoundsProvider provider) {
long xBitsValue = Double.doubleToLongBits(sampleX);
long zBitsValue = Double.doubleToLongBits(sampleZ);
int slot = findSlot(xBitsValue, zBitsValue);
long sampleXBits = Double.doubleToLongBits(sampleX);
long sampleZBits = Double.doubleToLongBits(sampleZ);
int slot = findSlot(sampleXBits, sampleZBits);
double minimum;
double maximum;
if (states[slot] != 0) {
return minValues[slot];
minimum = minValues[slot];
maximum = maxValues[slot];
} else {
NoiseBounds bounds = boundProvider.noise(sampleX, sampleZ);
minimum = bounds.min();
maximum = bounds.max();
insert(findSlot(sampleXBits, sampleZBits), sampleXBits, sampleZBits, minimum, maximum);
}
NoiseBounds bounds = provider.noise(sampleX, sampleZ);
// Recompute the slot: the provider call can mutate the table (clear/grow) via nesting.
insert(findSlot(xBitsValue, zBitsValue), xBitsValue, zBitsValue, bounds.min(), bounds.max());
return bounds.min();
if (sampledMaximumCount == sampledMaximums.length) {
sampledMaximums = Arrays.copyOf(sampledMaximums, sampledMaximums.length << 1);
}
sampledMaximums[sampledMaximumCount++] = maximum;
return minimum;
}
public double getOrSampleMax(double sampleX, double sampleZ, NoiseBoundsProvider provider) {
long xBitsValue = Double.doubleToLongBits(sampleX);
long zBitsValue = Double.doubleToLongBits(sampleZ);
int slot = findSlot(xBitsValue, zBitsValue);
if (states[slot] != 0) {
return maxValues[slot];
}
NoiseBounds bounds = provider.noise(sampleX, sampleZ);
// Recompute the slot: the provider call can mutate the table (clear/grow) via nesting.
insert(findSlot(xBitsValue, zBitsValue), xBitsValue, zBitsValue, bounds.min(), bounds.max());
return bounds.max();
private double replayMax(double sampleX, double sampleZ) {
return sampledMaximums[maximumReplayIndex++];
}
private int findSlot(long xb, long zb) {
@@ -0,0 +1,46 @@
package art.arcane.iris.core.pregenerator;
import art.arcane.volmlib.util.math.Position2;
import org.junit.Test;
import java.util.concurrent.atomic.AtomicLong;
import static org.junit.Assert.assertEquals;
public class PregenTaskChunkCountTest {
@Test
public void countMatchesTraversalAcrossChunkAndRegionBoundaries() {
int[][] areas = {
{0, 0, 1, 1},
{0, 0, 16, 32},
{17, -17, 15, 33},
{-16, -32, 16, 16},
{-513, 511, 32, 17},
{512, -512, 512, 256},
{PregenTask.MAX_WORLD_BLOCK - 32, 0, 32, 1},
{0, -PregenTask.MAX_WORLD_BLOCK + 32, 1, 32}
};
for (int[] area : areas) {
PregenTask task = PregenTask.builder()
.center(new Position2(area[0], area[1]))
.radiusX(area[2])
.radiusZ(area[3])
.build();
AtomicLong visited = new AtomicLong();
task.iterateAllChunks((x, z) -> visited.incrementAndGet());
assertEquals("center=" + area[0] + "," + area[1], visited.get(), task.chunkCount());
}
}
@Test(timeout = 2_000L)
public void entireWorldCountUsesLongArithmeticWithoutTraversal() {
PregenTask task = PregenTask.builder()
.radiusX(PregenTask.MAX_WORLD_BLOCK)
.radiusZ(PregenTask.MAX_WORLD_BLOCK)
.build();
assertEquals(14_062_492_500_001L, task.chunkCount());
}
}
@@ -56,7 +56,7 @@ public class StudioOpenCoordinatorOpenKindTest {
assertTrue(kind.openWorkspace());
assertFalse(kind.teleportThroughStandardEntry());
assertTrue(kind.prepareGeneratorState());
assertFalse(kind.prepareGeneratorState());
}
@Test
@@ -0,0 +1,188 @@
package art.arcane.iris.engine;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.IrisInterpolator;
import art.arcane.iris.util.project.interpolation.NoiseBounds;
import art.arcane.iris.util.project.interpolation.NoiseBoundsProvider;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Answers.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
public class IrisComplexBoundsBiomeCacheTest {
@Test
public void adjacentPassesReuseSamplesWithoutChangingInterpolationBits() throws Exception {
IrisBiome first = new IrisBiome();
IrisBiome second = new IrisBiome();
AtomicInteger reads = new AtomicInteger();
ProceduralStream<IrisBiome> stream = ProceduralStream.of((x, z) -> {
reads.incrementAndGet();
return x + z < 0D ? first : second;
}, Interpolated.of(value -> 0D, value -> first));
IrisInterpolator interpolator = new IrisInterpolator();
Fixture fixture = new Fixture(stream);
fixture.setBounds(interpolator, first, -15.25D, 63.5D);
fixture.setBounds(interpolator, second, 7.75D, 127.25D);
int uncachedReads = 0;
for (int z = -32; z <= 32; z += 4) {
for (int x = -32; x <= 32; x += 4) {
AtomicInteger expectedReads = new AtomicInteger();
NoiseBounds expected = interpolator.interpolateBounds(x, z, (xx, zz) -> {
expectedReads.incrementAndGet();
return xx + zz < 0D ? new NoiseBounds(-15.25D, 63.5D) : new NoiseBounds(7.75D, 127.25D);
});
assertBits(expected, fixture.sample(interpolator, x, z));
uncachedReads += expectedReads.get();
}
}
assertTrue(reads.get() < uncachedReads / 4);
}
@Test
public void replacingBiomeStreamInvalidatesRetainedSamples() throws Exception {
IrisBiome first = new IrisBiome();
IrisBiome second = new IrisBiome();
Fixture fixture = new Fixture(constant(first));
IrisInterpolator interpolator = new IrisInterpolator();
fixture.setBounds(interpolator, first, -10D, 20D);
fixture.setBounds(interpolator, second, 50D, 80D);
assertBits(new NoiseBounds(-10D, 20D), fixture.sample(interpolator, 0, 0));
fixture.complex.setBaseBiomeStream(constant(second));
assertBits(new NoiseBounds(50D, 80D), fixture.sample(interpolator, 0, 0));
}
@Test
public void newComplexDoesNotReusePreviousComplexSamples() throws Exception {
IrisBiome first = new IrisBiome();
IrisBiome second = new IrisBiome();
Fixture firstFixture = new Fixture(constant(first));
Fixture secondFixture = new Fixture(constant(second));
IrisInterpolator interpolator = new IrisInterpolator();
firstFixture.setBounds(interpolator, first, -10D, 20D);
secondFixture.setBounds(interpolator, second, 50D, 80D);
assertBits(new NoiseBounds(-10D, 20D), firstFixture.sample(interpolator, 0, 0));
assertBits(new NoiseBounds(50D, 80D), secondFixture.sample(interpolator, 0, 0));
assertBits(new NoiseBounds(-10D, 20D), firstFixture.sample(interpolator, 0, 0));
}
@Test
public void nestedPassDoesNotReplaceOuterGeneratorBounds() throws Exception {
IrisBiome biome = new IrisBiome();
Fixture fixture = new Fixture(constant(biome));
IrisInterpolator nested = new IrisInterpolator().setHorizontalScale(19D);
fixture.setBounds(nested, biome, 50D, 80D);
IrisInterpolator outer = new IrisInterpolator() {
@Override
public NoiseBounds interpolateBounds(double x, double z, NoiseBoundsProvider provider) {
NoiseBounds before = provider.noise(x, z);
try {
assertBits(new NoiseBounds(50D, 80D), fixture.sample(nested, x, z));
} catch (ReflectiveOperationException e) {
throw new IllegalStateException(e);
}
assertBits(before, provider.noise(x, z));
return before;
}
};
fixture.setBounds(outer, biome, -10D, 20D);
assertBits(new NoiseBounds(-10D, 20D), fixture.sample(outer, 0D, 0D));
assertBits(new NoiseBounds(-10D, 20D), fixture.sample(outer, 0D, 0D));
}
@Test
public void coordinateMemoKeepsExactKeysAndBoundedStorage() throws Exception {
Class<?> type = Class.forName("art.arcane.iris.engine.IrisComplex$CoordinateBiomeCache");
Constructor<?> constructor = type.getDeclaredConstructor(int.class);
constructor.setAccessible(true);
Object cache = constructor.newInstance(4096);
Method get = type.getDeclaredMethod("get", double.class, double.class);
Method put = type.getDeclaredMethod("put", double.class, double.class, IrisBiome.class);
get.setAccessible(true);
put.setAccessible(true);
IrisBiome first = new IrisBiome();
IrisBiome second = new IrisBiome();
double[][] coordinates = {{0D, -0D}, {-0D, 0D}, {-0.25D, 0.75D}, {-0.75D, 0.25D},
{0.125D, 0.125D}, {Math.nextUp(0.125D), 0.125D}, {1e12D, -1e12D}};
for (double[] coordinate : coordinates) {
put.invoke(cache, coordinate[0], coordinate[1], first);
assertSame(first, get.invoke(cache, coordinate[0], coordinate[1]));
assertNull(get.invoke(cache, Math.nextUp(coordinate[0]), coordinate[1]));
}
for (int i = 0; i < 100_000; i++) {
put.invoke(cache, (double) i, (double) -i, second);
assertSame(second, get.invoke(cache, (double) i, (double) -i));
}
for (double[] coordinate : coordinates) {
Object result = get.invoke(cache, coordinate[0], coordinate[1]);
assertTrue(result == null || result == first);
}
Field values = type.getDeclaredField("values");
values.setAccessible(true);
assertEquals(4096, ((IrisBiome[]) values.get(cache)).length);
}
private static ProceduralStream<IrisBiome> constant(IrisBiome biome) {
return ProceduralStream.of((x, z) -> biome, Interpolated.of(value -> 0D, value -> biome));
}
private static void assertBits(NoiseBounds expected, NoiseBounds actual) {
assertEquals(Double.doubleToLongBits(expected.min()), Double.doubleToLongBits(actual.min()));
assertEquals(Double.doubleToLongBits(expected.max()), Double.doubleToLongBits(actual.max()));
}
private static final class Fixture {
private final IrisComplex complex = mock(IrisComplex.class, CALLS_REAL_METHODS);
private final Map<IrisInterpolator, IdentityHashMap<IrisBiome, Object>> bounds = new HashMap<>();
private final Object cache;
private final Method sample;
private final Constructor<?> boundsConstructor;
private Fixture(ProceduralStream<IrisBiome> stream) throws Exception {
complex.setBaseBiomeStream(stream);
Field field = IrisComplex.class.getDeclaredField("generatorBounds");
field.setAccessible(true);
field.set(complex, bounds);
Class<?> cacheClass = Class.forName("art.arcane.iris.engine.IrisComplex$GridBoundsCache");
Constructor<?> constructor = cacheClass.getDeclaredConstructor();
constructor.setAccessible(true);
cache = constructor.newInstance();
sample = IrisComplex.class.getDeclaredMethod("sampleBoundsRaw", cacheClass, Engine.class,
IrisInterpolator.class, IrisGenerator[].class, double.class, double.class);
sample.setAccessible(true);
boundsConstructor = Class.forName("art.arcane.iris.engine.IrisComplex$GeneratorBounds")
.getDeclaredConstructor(double.class, double.class);
boundsConstructor.setAccessible(true);
}
private void setBounds(IrisInterpolator interpolator, IrisBiome biome, double min, double max) throws Exception {
bounds.computeIfAbsent(interpolator, ignored -> new IdentityHashMap<>())
.put(biome, boundsConstructor.newInstance(min, max));
}
private NoiseBounds sample(IrisInterpolator interpolator, double x, double z) throws ReflectiveOperationException {
return (NoiseBounds) sample.invoke(complex, cache, null, interpolator, new IrisGenerator[0], x, z);
}
}
}
@@ -0,0 +1,86 @@
package art.arcane.iris.engine;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.volmlib.util.math.M;
import org.junit.Test;
import java.util.Arrays;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class IrisComplexConstantGeneratorBoundsTest {
@Test
public void constantBoundsPreserveFiniteArithmeticAndSkipNoise() {
double[] bounds = {0D, -0D, 0.1D, -0.1D, Double.MIN_VALUE, -Double.MIN_VALUE,
Double.MAX_VALUE, -Double.MAX_VALUE};
double[] noises = {0D, -0D, 0.3D, -1D, Double.MAX_VALUE, -Double.MAX_VALUE};
int[] sizes = {1, 2, 3, 11};
for (double low : bounds) {
for (double high : bounds) {
if (low != high) {
continue;
}
for (double noise : noises) {
for (int size : sizes) {
CountingGenerator generator = new CountingGenerator(noise);
IrisGenerator[] generators = new IrisGenerator[size];
Arrays.fill(generators, generator);
double expected = 0D;
for (int i = 0; i < size; i++) {
expected += M.lerp(low, high, noise);
}
expected /= size;
assertBits(expected, IrisComplex.averageGeneratorHeights(generators, low, high, 12D, -9D, 7331L));
assertEquals(0, generator.calls);
}
}
}
}
}
@Test
public void inactiveInvalidNoiseMatchesExistingConstantFitContract() {
for (double noise : new double[]{Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY}) {
CountingGenerator generator = new CountingGenerator(noise);
assertBits(generator.fitDouble(0D, 0D, 7331L, 12D, -9D),
IrisComplex.averageGeneratorHeights(new IrisGenerator[]{generator}, 0D, 0D, 12D, -9D, 7331L));
assertEquals(0, generator.calls);
assertTrue(Double.isNaN(M.lerp(0D, 0D, noise)));
}
}
@Test
public void nonconstantAndNonfiniteBoundsRetainNoiseEvaluation() {
double[][] bounds = {{0D, 1D}, {-9D, 12D}, {Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY},
{Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY}, {Double.NaN, Double.NaN}};
for (double[] pair : bounds) {
for (double noise : new double[]{0.3D, Double.NaN, Double.POSITIVE_INFINITY}) {
CountingGenerator generator = new CountingGenerator(noise);
double expected = (0D + M.lerp(pair[0], pair[1], noise)) / 1;
assertBits(expected, IrisComplex.averageGeneratorHeights(new IrisGenerator[]{generator},
pair[0], pair[1], 12D, -9D, 7331L));
assertEquals(1, generator.calls);
}
}
}
private static void assertBits(double expected, double actual) {
assertEquals(Double.doubleToLongBits(expected), Double.doubleToLongBits(actual));
}
private static final class CountingGenerator extends IrisGenerator {
private final double value;
private int calls;
private CountingGenerator(double value) {
this.value = value;
}
@Override
public double getHeight(double x, double z, long seed) {
calls++;
return value;
}
}
}
@@ -0,0 +1,137 @@
package art.arcane.iris.engine;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.image.IrisImageMapRuntime;
import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisGeneratorStyle;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.iris.util.project.stream.interpolation.Interpolated;
import art.arcane.volmlib.util.collection.KList;
import org.junit.Test;
import java.lang.reflect.Method;
import java.lang.reflect.Field;
import java.util.List;
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.assertTrue;
import static org.mockito.Answers.CALLS_REAL_METHODS;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
public class IrisComplexNaturalBiomeReuseTest {
@Test
public void reusesBaseSelectionAndRetainsHeightDependentShoreSelection() throws Exception {
Fixture fixture = new Fixture();
assertSame(fixture.land, fixture.sample(90D));
assertEquals(1, fixture.cachedCalls.get());
assertEquals(0, fixture.directCalls.get());
assertSame(fixture.shore, fixture.sample(63D));
assertEquals(1, fixture.directCalls.get());
assertSame(fixture.sea, fixture.sample(40D));
assertEquals(2, fixture.directCalls.get());
}
@Test
public void mappedBiomesBypassBothSelections() throws Exception {
Fixture fixture = new Fixture();
IrisBiome mapped = new IrisBiome();
doReturn(mapped).when(fixture.images).sampleBiome(anyDouble(), anyDouble());
assertSame(mapped, fixture.sample(90D));
assertEquals(0, fixture.cachedCalls.get());
assertEquals(0, fixture.directCalls.get());
}
@Test
public void disabledReuseAndBoundChunkContextsKeepDirectSampling() throws Exception {
Fixture fixture = new Fixture();
fixture.setReuse(false);
assertSame(fixture.land, fixture.sample(90D));
fixture.setReuse(true);
try (IrisContext.Scope ignored = IrisContext.open(mock(Engine.class), 1L, mock(ChunkContext.class))) {
assertSame(fixture.land, fixture.sample(90D));
}
assertEquals(0, fixture.cachedCalls.get());
assertEquals(2, fixture.directCalls.get());
try (IrisContext.Scope ignored = IrisContext.open(mock(Engine.class), 1L, null)) {
assertSame(fixture.land, fixture.sample(90D));
}
assertEquals(1, fixture.cachedCalls.get());
}
@Test
public void allActiveSelectionExpressionsAndFracturesDisableReuse() {
IrisDimension dimension = new IrisDimension();
IrisBiome leaf = new IrisBiome();
assertTrue(IrisComplex.hasFixedNaturalBiomeNoise(dimension, List.of(leaf)));
for (IrisGeneratorStyle style : List.of(dimension.getRegionStyle(), dimension.getContinentalStyle(),
dimension.getLandBiomeStyle(), dimension.getSeaBiomeStyle())) {
style.setFracture(new IrisGeneratorStyle().setExpression("context"));
assertFalse(IrisComplex.hasFixedNaturalBiomeNoise(dimension, List.of(leaf)));
style.setFracture(null);
}
leaf.setChildStyle(new IrisGeneratorStyle().setExpression("context"));
assertTrue(IrisComplex.hasFixedNaturalBiomeNoise(dimension, List.of(leaf)));
leaf.setChildren(new KList<>("child"));
assertFalse(IrisComplex.hasFixedNaturalBiomeNoise(dimension, List.of(leaf)));
leaf.setChildStyle(new IrisGeneratorStyle().setFracture(new IrisGeneratorStyle().setExpression("context")));
assertFalse(IrisComplex.hasFixedNaturalBiomeNoise(dimension, List.of(leaf)));
}
private static final class Fixture {
private final IrisComplex complex = mock(IrisComplex.class, CALLS_REAL_METHODS);
private final IrisImageMapRuntime images = mock(IrisImageMapRuntime.class);
private final IrisRegion region = mock(IrisRegion.class);
private final IrisBiome land = new IrisBiome().setInferredType(InferredType.LAND);
private final IrisBiome sea = new IrisBiome().setInferredType(InferredType.SEA);
private final IrisBiome shore = new IrisBiome().setInferredType(InferredType.SHORE);
private final AtomicInteger cachedCalls = new AtomicInteger();
private final AtomicInteger directCalls = new AtomicInteger();
private final Method sample;
private Fixture() throws Exception {
complex.setImageMapRuntime(images);
complex.setFluidHeight(63D);
setReuse(true);
complex.setBaseBiomeStream(counted(land, cachedCalls));
doReturn(2D).when(region).getShoreHeight(anyDouble(), anyDouble());
Field streams = IrisComplex.class.getDeclaredField("inferredBiomeStreams");
streams.setAccessible(true);
streams.set(complex, IrisComplex.compileInferredBiomeStreams(List.of(region),
(ignored, type) -> counted(switch (type) {
case SEA -> sea;
case SHORE -> shore;
default -> land;
}, directCalls)));
sample = IrisComplex.class.getDeclaredMethod("sampleNaturalBiome", InferredType.class,
IrisRegion.class, double.class, int.class, int.class);
sample.setAccessible(true);
}
private void setReuse(boolean enabled) throws Exception {
Field reuse = IrisComplex.class.getDeclaredField("reuseNaturalBaseBiome");
reuse.setAccessible(true);
reuse.setBoolean(complex, enabled);
}
private IrisBiome sample(double height) throws Exception {
return (IrisBiome) sample.invoke(complex, InferredType.LAND, region, height, -17, 262144);
}
private static ProceduralStream<IrisBiome> counted(IrisBiome biome, AtomicInteger calls) {
return ProceduralStream.of((x, z) -> {
calls.incrementAndGet();
return biome;
}, Interpolated.of(value -> 0D, value -> biome));
}
}
}
@@ -0,0 +1,127 @@
package art.arcane.iris.engine;
import art.arcane.iris.engine.object.InferredType;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisGeneratorStyle;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.IdentityHashMap;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.mockito.Answers.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
public class IrisComplexSelfSelectionTest {
@Test
public void parentSelectionStopsAfterOneIdenticalNoiseSample() throws Exception {
Fixture fixture = new Fixture();
CountingBiome parent = new CountingBiome();
fixture.select(parent, parent);
assertSame(parent, fixture.implode(parent, 3));
assertEquals(1, parent.noise.calls);
}
@Test
public void cyclicChildrenRetainDepthLimit() throws Exception {
Fixture fixture = new Fixture();
CountingBiome first = new CountingBiome();
CountingBiome second = new CountingBiome();
fixture.select(first, second);
fixture.select(second, first);
assertSame(first, fixture.implode(first, 3));
assertEquals(2, first.noise.calls);
assertEquals(2, second.noise.calls);
}
@Test
public void exhaustedDepthAndLeafDoNotSelectChildren() throws Exception {
Fixture fixture = new Fixture();
CountingBiome parent = new CountingBiome();
fixture.select(parent, parent);
assertSame(parent, fixture.implode(parent, -1));
assertEquals(0, parent.noise.calls);
parent.getChildren().clear();
assertSame(parent, fixture.implode(parent, 3));
assertEquals(0, parent.noise.calls);
}
@Test
public void expressionsRetainRepeatedEvaluationIncludingFractureLayers() throws Exception {
for (int fractureDepth = 0; fractureDepth < 3; fractureDepth++) {
Fixture fixture = new Fixture();
CountingBiome parent = new CountingBiome();
IrisGeneratorStyle style = parent.getChildStyle();
for (int level = 0; level < fractureDepth; level++) {
IrisGeneratorStyle fracture = new IrisGeneratorStyle();
style.setFracture(fracture);
style = fracture;
}
style.setExpression("contextual-expression");
fixture.select(parent, parent);
assertSame(parent, fixture.implode(parent, 3));
assertEquals(4, parent.noise.calls);
}
}
private static final class Fixture {
private final IrisComplex complex = mock(IrisComplex.class, CALLS_REAL_METHODS);
private final IdentityHashMap<IrisBiome, IrisComplex.ChildSelectionPlan> plans = new IdentityHashMap<>();
private final Method implode;
private Fixture() throws Exception {
complex.setRng(new RNG(7331L));
Field field = IrisComplex.class.getDeclaredField("childSelectionPlans");
field.setAccessible(true);
field.set(complex, plans);
implode = IrisComplex.class.getDeclaredMethod("implode", IrisBiome.class, Double.class, Double.class, int.class);
implode.setAccessible(true);
}
private void select(IrisBiome parent, IrisBiome selected) {
IrisBiome alternative = new IrisBiome();
plans.put(parent, IrisComplex.ChildSelectionPlan.create(new KList<>(alternative, selected)));
}
private IrisBiome implode(IrisBiome parent, int depth) throws Exception {
return (IrisBiome) implode.invoke(complex, parent, -17.25D, 8.5D, depth);
}
}
private static final class CountingBiome extends IrisBiome {
private final CountingNoise noise = new CountingNoise();
private CountingBiome() {
setChildren(new KList<>("child"));
setInferredType(InferredType.LAND);
}
@Override
public CNG getChildrenGenerator(RNG rng, int signature, double scale) {
return noise;
}
}
private static final class CountingNoise extends CNG {
private int calls;
private CountingNoise() {
super(new RNG(7331L));
}
@Override
public int fit2D(int minimum, int maximum, double x, double z) {
calls++;
return 0;
}
}
}
@@ -0,0 +1,337 @@
package art.arcane.iris.engine;
import art.arcane.iris.engine.framework.GenerationSessionLease;
import art.arcane.iris.engine.framework.GenerationSessionManager;
import art.arcane.iris.engine.framework.GenerationSessionException;
import art.arcane.iris.engine.hydrology.HydrologyPlanner;
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.engine.hydrology.HydrologyTile;
import art.arcane.iris.engine.hydrology.HydrologyTileCache;
import art.arcane.iris.engine.hydrology.HydrologyTileKey;
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.iris.util.project.context.IrisContext;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Field;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
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.FutureTask;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
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.Mockito.doAnswer;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class IrisEngineStudioEntryHydrologyTest {
@Test
@SuppressWarnings("unchecked")
public void queuesScopedEntryDemandWithoutBlockingInitialization() throws Exception {
EngineBackgroundTasks tasks = new EngineBackgroundTasks();
tasks.openBackgroundTaskAdmission();
IrisEngine engine = engine(tasks);
IrisEngine.GenerationRuntimeBinding binding = mock(IrisEngine.GenerationRuntimeBinding.class);
GenerationSessionLease lease = mock(GenerationSessionLease.class);
IrisHydrologyRuntime hydrology = hydrology(engine);
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(binding);
when(engine.acquireGenerationLease("studio_entry_hydrology")).thenReturn(lease);
when(lease.sessionId()).thenReturn(93L);
doAnswer(invocation -> {
assertSame(engine, IrisContext.require().getEngine());
assertEquals(93L, IrisContext.require().getGenerationSessionId());
assertSame(binding, engine.generationRuntimeScopes.current());
return null;
}).when(hydrology).prepareChunkColumns(0, 0);
AtomicReference<Callable<Void>> queued = new AtomicReference<>();
try (MockedStatic<J> scheduler = scheduler(queued)) {
engine.startStudioEntryHydrology(0, 0);
assertNotNull(queued.get());
verifyNoInteractions(hydrology, lease);
verify(engine, never()).acquireGenerationLease(any());
queued.get().call();
verify(hydrology).prepareChunkColumns(0, 0);
verify(lease).close();
assertNull(IrisContext.get());
assertNull(engine.generationRuntimeScopes.current());
tasks.drainBackgroundTasks("test").requireComplete("test");
}
}
@Test
public void planningFailureStillLogsIfTransitionStartsDuringPlanning() throws Exception {
EngineBackgroundTasks tasks = new EngineBackgroundTasks();
tasks.openBackgroundTaskAdmission();
IrisEngine engine = engine(tasks);
GenerationSessionLease lease = mock(GenerationSessionLease.class);
when(engine.acquireGenerationLease("studio_entry_hydrology")).thenReturn(lease);
IrisHydrologyRuntime hydrology = hydrology(engine);
IllegalStateException cause = new IllegalStateException("planning failed");
doAnswer(invocation -> {
closing(engine).set(true);
throw cause;
}).when(hydrology).prepareChunkColumns(0, 0);
AtomicReference<Callable<Void>> queued = new AtomicReference<>();
try (MockedStatic<J> scheduler = scheduler(queued);
MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
engine.startStudioEntryHydrology(0, 0);
IllegalStateException failure = assertThrows(IllegalStateException.class, () -> queued.get().call());
assertSame(cause, failure.getCause());
assertTrue(failure.getMessage().contains("Studio entry hydrology preparation failed at 0,0"));
logging.verify(() -> IrisLogging.reportError(failure));
verify(lease).close();
}
}
@Test
public void closedBackgroundAdmissionAndSchedulerFailureAcquireNoLease() throws Exception {
EngineBackgroundTasks tasks = new EngineBackgroundTasks();
IrisEngine engine = engine(tasks);
IllegalStateException failure = assertThrows(IllegalStateException.class,
() -> engine.startStudioEntryHydrology(0, 0));
assertTrue(failure.getMessage().contains("admission closed"));
tasks.openBackgroundTaskAdmission();
RejectedExecutionException rejected = new RejectedExecutionException("scheduler closed");
try (MockedStatic<J> scheduler = mockStatic(J.class)) {
scheduler.when(() -> J.a(any(Callable.class))).thenThrow(rejected);
assertSame(rejected, assertThrows(RejectedExecutionException.class,
() -> engine.startStudioEntryHydrology(0, 0)));
}
verify(engine, never()).acquireGenerationLease(any());
}
@Test
public void queuedEntrySkipsClosedOrHotloadingSessionAndDrainsWithoutErrors() throws Exception {
for (boolean teardown : new boolean[]{false, true}) {
EngineBackgroundTasks tasks = new EngineBackgroundTasks();
tasks.openBackgroundTaskAdmission();
IrisEngine engine = engine(tasks);
GenerationSessionManager sessions = new GenerationSessionManager();
when(engine.acquireGenerationLease(any())).thenAnswer(invocation -> sessions.acquire("entry"));
AtomicReference<Callable<Void>> queued = new AtomicReference<>();
try (MockedStatic<J> scheduler = scheduler(queued);
MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
engine.startStudioEntryHydrology(0, 0);
assertEquals(0, sessions.activeLeases());
closing(engine).set(true);
tasks.closeBackgroundTaskAdmission();
sessions.sealAndAwait("transition", 0L, teardown);
queued.get().call();
tasks.drainBackgroundTasks("transition").requireComplete("transition");
assertEquals(0, sessions.activeLeases());
verify(engine, never()).acquireGenerationLease(any());
logging.verifyNoInteractions();
}
}
}
@Test
public void admissionRaceCancelsOnlyWhenClosing() throws Exception {
for (boolean transition : new boolean[]{false, true}) {
EngineBackgroundTasks tasks = new EngineBackgroundTasks();
tasks.openBackgroundTaskAdmission();
IrisEngine engine = engine(tasks);
GenerationSessionException rejected = new GenerationSessionException("sealed", false);
when(engine.acquireGenerationLease(any())).thenAnswer(invocation -> {
closing(engine).set(transition);
throw rejected;
});
AtomicReference<Callable<Void>> queued = new AtomicReference<>();
try (MockedStatic<J> scheduler = scheduler(queued);
MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
engine.startStudioEntryHydrology(0, 0);
if (transition) {
queued.get().call();
logging.verifyNoInteractions();
} else {
IllegalStateException failure = assertThrows(IllegalStateException.class, () -> queued.get().call());
assertSame(rejected, failure.getCause());
logging.verify(() -> IrisLogging.reportError(failure));
}
}
}
}
@Test
@SuppressWarnings("unchecked")
public void canceledFutureBeforeStartOwnsNoGenerationLease() throws Exception {
EngineBackgroundTasks tasks = new EngineBackgroundTasks();
tasks.openBackgroundTaskAdmission();
IrisEngine engine = engine(tasks);
GenerationSessionManager sessions = new GenerationSessionManager();
when(engine.acquireGenerationLease(any())).thenAnswer(invocation -> sessions.acquire("entry"));
AtomicReference<FutureTask<Void>> queued = new AtomicReference<>();
try (MockedStatic<J> scheduler = mockStatic(J.class)) {
scheduler.when(() -> J.a(any(Callable.class))).thenAnswer(invocation -> {
FutureTask<Void> future = new FutureTask<>(invocation.<Callable<Void>>getArgument(0));
queued.set(future);
return future;
});
engine.startStudioEntryHydrology(0, 0);
assertTrue(queued.get().cancel(false));
queued.get().run();
sessions.sealAndAwait("close", 0L, true);
assertEquals(0, sessions.activeLeases());
verify(engine, never()).acquireGenerationLease(any());
}
}
@Test
public void activeLeaseFinishesWhileTransitionHoldsLifecycleLockAndDrains() throws Exception {
EngineBackgroundTasks tasks = new EngineBackgroundTasks();
tasks.openBackgroundTaskAdmission();
IrisEngine engine = engine(tasks);
IrisHydrologyRuntime hydrology = hydrology(engine);
GenerationSessionManager sessions = new GenerationSessionManager();
CountDownLatch admitted = new CountDownLatch(1);
CountDownLatch proceed = new CountDownLatch(1);
when(engine.acquireGenerationLease(any())).thenAnswer(invocation -> {
GenerationSessionLease lease = sessions.acquire("entry");
admitted.countDown();
assertTrue(proceed.await(5, TimeUnit.SECONDS));
return lease;
});
AtomicReference<Callable<Void>> queued = new AtomicReference<>();
try (MockedStatic<J> scheduler = scheduler(queued);
ExecutorService executor = Executors.newFixedThreadPool(2)) {
engine.startStudioEntryHydrology(0, 0);
Future<Void> worker = executor.submit(queued.get());
assertTrue(admitted.await(5, TimeUnit.SECONDS));
Future<?> drain = executor.submit(() -> {
synchronized (engine.lifecycleLock) {
closing(engine).set(true);
tasks.closeBackgroundTaskAdmission();
sessions.sealAndAwait("hotload", 5000L);
tasks.drainBackgroundTasks("hotload").requireComplete("hotload");
}
return null;
});
try {
awaitSealed(sessions);
assertFalse(drain.isDone());
} finally {
proceed.countDown();
}
worker.get(5, TimeUnit.SECONDS);
drain.get(5, TimeUnit.SECONDS);
verify(hydrology).prepareChunkColumns(0, 0);
assertEquals(0, sessions.activeLeases());
}
}
private void awaitSealed(GenerationSessionManager sessions) throws Exception {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
while (System.nanoTime() < deadline) {
try (GenerationSessionLease probe = sessions.acquire("seal probe")) {
Thread.yield();
} catch (GenerationSessionException sealed) {
return;
}
}
throw new AssertionError("Session admission did not seal");
}
@SuppressWarnings("unchecked")
private MockedStatic<J> scheduler(AtomicReference<Callable<Void>> queued) {
MockedStatic<J> scheduler = mockStatic(J.class);
scheduler.when(() -> J.a(any(Callable.class))).thenAnswer(invocation -> {
queued.set(invocation.getArgument(0));
return new CompletableFuture<Void>();
});
return scheduler;
}
private IrisHydrologyRuntime hydrology(IrisEngine engine) {
IrisComplex complex = mock(IrisComplex.class);
IrisHydrologyRuntime hydrology = mock(IrisHydrologyRuntime.class);
when(engine.getComplex()).thenReturn(complex);
when(complex.getHydrologyRuntime()).thenReturn(hydrology);
return hydrology;
}
private AtomicBoolean closing(IrisEngine engine) throws Exception {
Field field = IrisEngine.class.getDeclaredField("closing");
field.setAccessible(true);
return (AtomicBoolean) field.get(engine);
}
@Test
public void exactOriginChunkDemandsAllFourTilesConcurrently() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyPlannerSettings settings = mock(HydrologyPlannerSettings.class);
when(settings.routing()).thenReturn(HydrologyPlannerSettings.defaults().routing());
when(settings.publicationRadius()).thenReturn(64);
when(planner.settings()).thenReturn(settings);
CountDownLatch started = new CountDownLatch(4);
CountDownLatch release = new CountDownLatch(1);
Set<HydrologyTileKey> keys = ConcurrentHashMap.newKeySet();
when(planner.plan(any())).thenAnswer(invocation -> {
assertTrue(keys.add(invocation.getArgument(0)));
started.countDown();
assertTrue(release.await(5, TimeUnit.SECONDS));
HydrologyTile tile = mock(HydrologyTile.class);
when(tile.columnAt(anyInt(), anyInt())).thenReturn(Optional.empty());
return tile;
});
try (ExecutorService planning = Executors.newFixedThreadPool(4);
ExecutorService caller = Executors.newSingleThreadExecutor()) {
HydrologyTileCache cache = new HydrologyTileCache(planner, 16, planning);
cache.setNeighbourPrefetchEnabled(false);
Future<?> preparation = caller.submit(() -> cache.prepareChunkColumns(0, 0));
try {
assertTrue(started.await(5, TimeUnit.SECONDS));
assertEquals(Set.of(new HydrologyTileKey(-1, -1), new HydrologyTileKey(0, -1),
new HydrologyTileKey(-1, 0), new HydrologyTileKey(0, 0)), keys);
} finally {
release.countDown();
}
preparation.get(5, TimeUnit.SECONDS);
cache.prepareChunkColumns(0, 0);
assertEquals(4, keys.size());
cache.close();
}
}
private IrisEngine engine(EngineBackgroundTasks tasks) throws Exception {
IrisEngine engine = mock(IrisEngine.class);
Field field = IrisEngine.class.getDeclaredField("backgroundTasks");
field.setAccessible(true);
field.set(engine, tasks);
Field closing = IrisEngine.class.getDeclaredField("closing");
closing.setAccessible(true);
closing.set(engine, new AtomicBoolean());
Field scopes = IrisEngine.class.getDeclaredField("generationRuntimeScopes");
scopes.setAccessible(true);
scopes.set(engine, new GenerationRuntimeScopeState());
Field lifecycle = IrisEngine.class.getDeclaredField("lifecycleLock");
lifecycle.setAccessible(true);
lifecycle.set(engine, new Object());
doCallRealMethod().when(engine).startStudioEntryHydrology(anyInt(), anyInt());
return engine;
}
}
@@ -67,6 +67,51 @@ public class HydrologyFootprintCompilerTest {
return maximum;
}
@Test
public void slopeFreeBasisPreservesCompleteSurfaceAndCaveFootprints() {
List<HydrologyFeatureType> types = List.of(
HydrologyFeatureType.SURFACE_POOL,
HydrologyFeatureType.MOUTH,
HydrologyFeatureType.UNDERGROUND_POOL,
HydrologyFeatureType.COASTAL_GROTTO
);
for (HydrologyFeatureType type : types) {
int head = type == HydrologyFeatureType.MOUTH || type == HydrologyFeatureType.COASTAL_GROTTO
? 63 : 70;
HydraulicSegment segment = new HydraulicSegment(
9001L, 9000L, type, head, head, 6, 3, false, false,
List.of(new HydrologyPoint(-12, head, -3), new HydrologyPoint(32, head, 5)));
RiverCourse course = course(9000L,
type.isSurface() ? RiverCourseType.SURFACE : RiverCourseType.UNDERGROUND, segment);
SlopeCountingSampler original = new SlopeCountingSampler(false);
SlopeCountingSampler slopeFree = new SlopeCountingSampler(true);
HydrologyFootprintCompiler baseline = new HydrologyFootprintCompiler(
HydrologyPlannerSettings.defaults(),
new HydrologyFootprintCompiler.Sampling(original, request -> request.minimum(), original));
HydrologyFootprintCompiler optimized = new HydrologyFootprintCompiler(
HydrologyPlannerSettings.defaults(),
new HydrologyFootprintCompiler.Sampling(slopeFree, request -> request.minimum(), slopeFree));
HydrologyFootprintCompiler.ValidationRaster expectedValidation = baseline.compileValidation(List.of(course));
HydrologyFootprintCompiler.ValidationRaster actualValidation = optimized.compileValidation(List.of(course));
RiverFootprint expected = baseline.compile(List.of(course));
RiverFootprint actual = optimized.compile(List.of(course));
assertFalse(type.toString(), expected.isEmpty());
assertEquals(type.toString(), expected, actual);
assertEquals(type.toString(), expectedValidation.columns(), actualValidation.columns());
for (HydrologyColumnSample column : expected.columns().values()) {
assertEquals(type.toString(),
expectedValidation.plannedSurface().resolve(column.x(), column.z(), column.naturalHeight()),
actualValidation.plannedSurface().resolve(column.x(), column.z(), column.naturalHeight()));
}
assertTrue(type.toString(), original.basisCalls > 0);
assertEquals(type.toString(), 0, slopeFree.basisCalls);
assertEquals(type.toString(), original.basisCalls, slopeFree.slopeFreeCalls);
assertEquals(type.toString(), original.detailedCalls, slopeFree.detailedCalls);
}
}
@Test
public void surfaceChannelNeverRaisesTerrainOrPublishesUnsupportedFluid() {
HydraulicSegment segment = new HydraulicSegment(
@@ -2290,6 +2335,56 @@ public class HydrologyFootprintCompilerTest {
return visited.size();
}
private static final class SlopeCountingSampler implements HydrologyNaturalTerrainSampler, HydrologyTerrainSampler {
private final boolean omitSlope;
private int basisCalls;
private int slopeFreeCalls;
private int detailedCalls;
private SlopeCountingSampler(boolean omitSlope) {
this.omitSlope = omitSlope;
}
@Override
public HydrologyTerrainSample sample(int x, int z) {
detailedCalls++;
return terrain(x, z);
}
@Override
public HydrologyTerrainSample sampleBasis(int x, int z) {
basisCalls++;
return terrain(x, z);
}
@Override
public HydrologyTerrainSample sampleBasisWithoutSlope(int x, int z) {
if (!omitSlope) {
return sampleBasis(x, z);
}
slopeFreeCalls++;
return terrain(x, z).withSlope(0D);
}
@Override
public HydrologyTerrainSample[] sampleGrid(GridRequest request) {
throw new UnsupportedOperationException();
}
@Override
public NaturalClassification classifyNatural(int x, int z) {
return x >= 24 ? NaturalClassification.OCEAN : NaturalClassification.LAND;
}
private HydrologyTerrainSample terrain(int x, int z) {
double slope = 1D + Math.floorMod(x * 13 - z * 7, 101) * 0.5D;
int relief = Math.floorMod(x * 3 + z * 5, 7);
return x >= 24
? HydrologyTerrainSample.ocean(52 + relief, "ocean").withSlope(slope)
: HydrologyTerrainSample.openLand(80 + relief, slope, z < 0 ? "north" : "south");
}
}
private static final class CountingNaturalSampler implements HydrologyNaturalTerrainSampler {
private final HydrologyTerrainSample basis;
private final NaturalClassification classification;
@@ -6,16 +6,195 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
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 HydrologyForkJoinTest {
@Test
public void blockedGridBatchNeverHelpsUnrelatedOwnerWork() throws Exception {
ForkJoinPool pool = new ForkJoinPool(2, ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, false, 2, 2, 2, null, 60, TimeUnit.SECONDS);
CountDownLatch childStarted = new CountDownLatch(1);
CountDownLatch releaseChild = new CountDownLatch(1);
CountDownLatch unrelatedRan = new CountDownLatch(1);
try {
Future<List<Integer>> result = pool.submit(() -> {
return HydrologyForkJoin.invokeAll(List.of(() -> {
await(childStarted);
return 1;
}, () -> {
pool.execute(() -> {
unrelatedRan.countDown();
});
childStarted.countDown();
await(releaseChild);
return 2;
}), null);
});
assertTrue(childStarted.await(5, TimeUnit.SECONDS));
assertFalse(unrelatedRan.await(100, TimeUnit.MILLISECONDS));
releaseChild.countDown();
assertEquals(List.of(1, 2), result.get(5, TimeUnit.SECONDS));
assertTrue(unrelatedRan.await(5, TimeUnit.SECONDS));
} finally {
releaseChild.countDown();
pool.shutdownNow();
}
}
@Test
public void failedCallerDrainsAnAlreadyStartedGridRow() throws Exception {
ForkJoinPool pool = new ForkJoinPool(2);
CountDownLatch started = new CountDownLatch(1);
CountDownLatch failed = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
AtomicInteger completed = new AtomicInteger();
IllegalStateException expected = new IllegalStateException("grid failure");
try {
Future<?> result = pool.submit(() -> HydrologyForkJoin.invokeAll(List.of(() -> {
await(started);
failed.countDown();
throw expected;
}, () -> {
started.countDown();
await(release);
completed.incrementAndGet();
return 2;
}), null));
assertTrue(failed.await(5, TimeUnit.SECONDS));
assertFalse(result.isDone());
release.countDown();
assertThrows(ExecutionException.class, () -> result.get(5, TimeUnit.SECONDS));
assertEquals(1, completed.get());
} finally {
release.countDown();
pool.shutdownNow();
}
}
@Test
public void rejectedFallbackSchedulingNeverRunsUnsubmittedTasks() {
AtomicInteger completed = new AtomicInteger();
RejectedExecutionException expected = new RejectedExecutionException("closed executor");
RejectedExecutionException actual = assertThrows(RejectedExecutionException.class,
() -> HydrologyForkJoin.invokeAll(List.of(completed::incrementAndGet, completed::incrementAndGet),
runnable -> { throw expected; }));
assertEquals(expected, actual);
assertEquals(0, completed.get());
}
@Test
public void partiallyRejectedFallbackDrainsAcceptedWorkOnItsExecutor() throws Exception {
ExecutorService fallback = Executors.newSingleThreadExecutor(runnable -> new Thread(runnable, "fallback-executor"));
ExecutorService caller = Executors.newSingleThreadExecutor();
CountDownLatch started = new CountDownLatch(1);
CountDownLatch rejected = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
AtomicInteger submissions = new AtomicInteger();
AtomicInteger unsubmittedCalls = new AtomicInteger();
AtomicReference<String> callbackThread = new AtomicReference<>();
RejectedExecutionException expected = new RejectedExecutionException("closed executor");
try {
Future<RejectedExecutionException> result = caller.submit(() -> assertThrows(RejectedExecutionException.class,
() -> HydrologyForkJoin.invokeAll(List.of(() -> {
callbackThread.set(Thread.currentThread().getName());
started.countDown();
await(release);
return 1;
}, unsubmittedCalls::incrementAndGet), runnable -> {
if (submissions.getAndIncrement() == 0) {
fallback.execute(runnable);
} else {
rejected.countDown();
throw expected;
}
})));
assertTrue(started.await(5, TimeUnit.SECONDS));
assertTrue(rejected.await(5, TimeUnit.SECONDS));
assertFalse(result.isDone());
release.countDown();
assertEquals(expected, result.get(5, TimeUnit.SECONDS));
assertEquals("fallback-executor", callbackThread.get());
assertEquals(0, unsubmittedCalls.get());
} finally {
release.countDown();
caller.shutdownNow();
fallback.shutdownNow();
}
}
@Test
public void repeatedDemandClaimsAQueuedOwnerOnlyOnceAndRestoresInterrupts() throws Exception {
AtomicInteger calls = new AtomicInteger();
HydrologyForkJoin.Task<Integer> task = new HydrologyForkJoin.Task<>(calls::incrementAndGet);
Thread.currentThread().interrupt();
try {
assertEquals(Integer.valueOf(1), task.await());
assertEquals(Integer.valueOf(1), task.await());
task.run();
assertEquals(1, calls.get());
assertTrue(Thread.currentThread().isInterrupted());
} finally {
Thread.interrupted();
}
}
@Test
public void interruptWhileWaitingStillDrainsTheStartedTask() throws Exception {
ForkJoinPool pool = new ForkJoinPool(2);
CountDownLatch started = new CountDownLatch(1);
CountDownLatch waiting = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
AtomicReference<Thread> waiter = new AtomicReference<>();
HydrologyForkJoin.Task<Integer> task = new HydrologyForkJoin.Task<>(() -> {
started.countDown();
await(release);
return 1;
});
try {
pool.execute(task);
assertTrue(started.await(5, TimeUnit.SECONDS));
Future<Boolean> result = pool.submit(() -> {
waiter.set(Thread.currentThread());
waiting.countDown();
assertEquals(Integer.valueOf(1), task.await());
return Thread.currentThread().isInterrupted();
});
assertTrue(waiting.await(5, TimeUnit.SECONDS));
waiter.get().interrupt();
assertFalse(result.isDone());
release.countDown();
assertTrue(result.get(5, TimeUnit.SECONDS));
} finally {
release.countDown();
pool.shutdownNow();
}
}
private static void await(CountDownLatch latch) {
try {
if (!latch.await(5, TimeUnit.SECONDS)) {
throw new AssertionError("Timed out waiting for planning task");
}
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new AssertionError(failure);
}
}
@Test
public void nestedFanOutCompletesOnASaturatedForkJoinPool() throws Exception {
ForkJoinPool pool = new ForkJoinPool(1);
@@ -5,25 +5,625 @@ import art.arcane.iris.engine.hydrology.cave.CaveVoxel;
import art.arcane.iris.engine.hydrology.cave.CaveVoxelView;
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveAction;
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
import com.github.benmanes.caffeine.cache.Cache;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Consumer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class HydrologyPlannerTest {
private static final HydrologyTileKey TILE = new HydrologyTileKey(0, 0);
@Test
public void sharedOwnerFutureExcludesMutableCompilerWhileCreatorRetainsIt() throws Exception {
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
HydrologyPlanner planner = routingContextPlanner(request -> {
entered.countDown();
awaitContext(release);
});
Method resolve = HydrologyPlanner.class.getDeclaredMethod("resolveCrossTileOwner", HydrologyTileKey.class);
resolve.setAccessible(true);
ExecutorService caller = Executors.newSingleThreadExecutor();
try {
Future<Object> creator = caller.submit(() -> resolve.invoke(planner, TILE));
assertTrue(entered.await(5, TimeUnit.SECONDS));
CompletableFuture<?> shared = runningOwners(planner).get(TILE);
assertNotNull(shared);
release.countDown();
Object creatorDraft = ownerDraft(creator.get(5, TimeUnit.SECONDS));
Object sharedDraft = ownerDraft(shared.get(5, TimeUnit.SECONDS));
Method compiler = creatorDraft.getClass().getDeclaredMethod("footprintCompiler");
compiler.setAccessible(true);
assertNotNull(compiler.invoke(creatorDraft));
assertNull(compiler.invoke(sharedDraft));
Method withoutCompiler = creatorDraft.getClass().getDeclaredMethod("withoutFootprintCompiler");
withoutCompiler.setAccessible(true);
assertEquals(withoutCompiler.invoke(creatorDraft), sharedDraft);
assertEquals(sharedDraft, ownerDraft(resolve.invoke(planner, TILE)));
} finally {
release.countDown();
caller.shutdownNow();
}
}
private static Object ownerDraft(Object resolution) throws Exception {
Method draft = resolution.getClass().getDeclaredMethod("draft");
draft.setAccessible(true);
return draft.invoke(resolution);
}
@Test
public void collidingRoutingContextKeysCompileIndependently() throws Exception {
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
AtomicInteger grids = new AtomicInteger();
HydrologyPlanner planner = routingContextPlanner(request -> {
grids.incrementAndGet();
if (request.minimumZ() == -32) {
entered.countDown();
awaitContext(release);
}
});
ExecutorService callers = Executors.newFixedThreadPool(2);
try {
Future<Object> first = callers.submit(() -> routingContext(planner, new HydrologyTileKey(0, 0)));
assertTrue(entered.await(5, TimeUnit.SECONDS));
Future<Object> second = callers.submit(() -> routingContext(planner, new HydrologyTileKey(0, -1)));
assertNotNull(second.get(5, TimeUnit.SECONDS));
assertFalse(first.isDone());
assertEquals(2, grids.get());
release.countDown();
assertNotNull(first.get(5, TimeUnit.SECONDS));
} finally {
release.countDown();
callers.shutdownNow();
}
}
@Test
public void concurrentSameKeyRoutingContextsShareOneCompilation() throws Exception {
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch secondEntered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
AtomicInteger grids = new AtomicInteger();
HydrologyPlanner planner = routingContextPlanner(request -> {
grids.incrementAndGet();
entered.countDown();
awaitContext(release);
});
ExecutorService callers = Executors.newFixedThreadPool(2);
try {
Future<Object> first = callers.submit(() -> routingContext(planner, TILE));
assertTrue(entered.await(5, TimeUnit.SECONDS));
Future<Object> second = callers.submit(() -> {
secondEntered.countDown();
return routingContext(planner, TILE);
});
assertTrue(secondEntered.await(5, TimeUnit.SECONDS));
assertThrows(TimeoutException.class, () -> second.get(50, TimeUnit.MILLISECONDS));
assertEquals(1, grids.get());
release.countDown();
Object expected = first.get(5, TimeUnit.SECONDS);
assertSame(expected, second.get(5, TimeUnit.SECONDS));
assertSame(expected, routingContext(planner, TILE));
assertEquals(1, grids.get());
} finally {
release.countDown();
callers.shutdownNow();
}
}
@Test
public void failedRoutingContextCanRetryWithoutRetainingFailedWork() throws Exception {
AtomicInteger grids = new AtomicInteger();
IllegalStateException expected = new IllegalStateException("routing input failed");
HydrologyPlanner planner = routingContextPlanner(request -> {
if (grids.incrementAndGet() == 1) {
throw expected;
}
});
InvocationTargetException failure = assertThrows(InvocationTargetException.class,
() -> routingContext(planner, TILE));
assertSame(expected, failure.getCause());
Object retried = routingContext(planner, TILE);
assertSame(retried, routingContext(planner, TILE));
assertEquals(2, grids.get());
Field pending = HydrologyPlanner.class.getDeclaredField("resolvingRoutingContexts");
pending.setAccessible(true);
assertTrue(((Map<?, ?>) pending.get(planner)).isEmpty());
}
@Test(timeout = 5000)
public void recursiveRoutingContextFailsImmediatelyAndCanRetry() throws Exception {
AtomicInteger grids = new AtomicInteger();
AtomicReference<HydrologyPlanner> reference = new AtomicReference<>();
HydrologyPlanner planner = routingContextPlanner(request -> {
if (grids.incrementAndGet() != 1) {
return;
}
try {
routingContext(reference.get(), TILE);
} catch (InvocationTargetException failure) {
throw (RuntimeException) failure.getCause();
} catch (Exception failure) {
throw new AssertionError(failure);
}
});
reference.set(planner);
InvocationTargetException failure = assertThrows(InvocationTargetException.class,
() -> routingContext(planner, TILE));
assertTrue(failure.getCause() instanceof IllegalStateException);
assertTrue(failure.getCause().getMessage().contains("recursively load"));
Object retried = routingContext(planner, TILE);
assertSame(retried, routingContext(planner, TILE));
assertEquals(2, grids.get());
}
private HydrologyPlanner routingContextPlanner(Consumer<HydrologyRoutingTerrainSampler.GridRequest> beforeGrid) {
HydrologyTerrainSample sample = blockedTerrain();
HydrologyTerrainSampler terrain = (x, z) -> sample;
HydrologyPlannerSettings settings = standardSettings(4D, 2D, true, false, List.of());
return new HydrologyPlanner(77L, settings, terrain, new ContextRoutingSampler(sample, beforeGrid),
HydrologyGeometrySampler.deterministic(terrain), -4096,
footprint -> new HydrologyTerrainCaveVoxelView(terrain, settings.seaLevel(), -4096, 4096));
}
private static Object routingContext(HydrologyPlanner planner, HydrologyTileKey key) throws Exception {
Method method = HydrologyPlanner.class.getDeclaredMethod("sourceRoutingContext", HydrologyTileKey.class);
method.setAccessible(true);
return method.invoke(planner, key);
}
private static void awaitContext(CountDownLatch latch) {
try {
if (!latch.await(5, TimeUnit.SECONDS)) {
throw new AssertionError("Routing context did not release");
}
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new AssertionError(failure);
}
}
private static final class ContextRoutingSampler implements HydrologyRoutingTerrainSampler {
private final HydrologyTerrainSample sample;
private final Consumer<GridRequest> beforeGrid;
private ContextRoutingSampler(HydrologyTerrainSample sample, Consumer<GridRequest> beforeGrid) {
this.sample = sample;
this.beforeGrid = beforeGrid;
}
@Override
public HydrologyTerrainSample[] sampleGrid(GridRequest request) {
beforeGrid.accept(request);
HydrologyTerrainSample[] samples = new HydrologyTerrainSample[request.width() * request.width()];
Arrays.fill(samples, sample);
return samples;
}
@Override
public NaturalClassification classifyNatural(int blockX, int blockZ) {
return NaturalClassification.UNAVAILABLE;
}
}
@Test
public void earlyOwnersPrioritizeHigherRanksAndSkipRunningOwners() throws Exception {
HydrologyPlannerSettings settings = withPeriodTwo(standardSettings(4D, 2D, true, false, List.of()));
HydrologyPlanner planner = new HydrologyPlanner(77L, settings, (x, z) -> blockedTerrain());
AutoCloseable admission = earlyAdmission(planner, new HydrologyTileKey(-1, -1));
Map<HydrologyTileKey, HydrologyForkJoin.Task<?>> tasks = earlyTasks(admission);
Map<HydrologyTileKey, CompletableFuture<?>> running = runningOwners(planner);
HydrologyTileKey alreadyRunning = new HydrologyTileKey(0, -1);
CompletableFuture<?> existing = new CompletableFuture<>();
running.put(alreadyRunning, existing);
Method prepare = admission.getClass().getDeclaredMethod("prepare");
prepare.setAccessible(true);
CapturingHydrologyPool pool = new CapturingHydrologyPool();
try {
pool.submit(() -> {
prepare.invoke(admission);
prepare.invoke(admission);
return null;
}).get(5, TimeUnit.SECONDS);
assertEquals(List.of(new HydrologyTileKey(-2, -1),
new HydrologyTileKey(-1, -2), new HydrologyTileKey(-1, 0),
new HydrologyTileKey(-2, -2), new HydrologyTileKey(-2, 0),
new HydrologyTileKey(0, -2), new HydrologyTileKey(0, 0)), new ArrayList<>(tasks.keySet()));
assertEquals(new ArrayList<>(tasks.values()), pool.submitted);
assertEquals(existing, running.get(alreadyRunning));
} finally {
running.remove(alreadyRunning, existing);
admission.close();
pool.shutdownNow();
}
}
private static final class CapturingHydrologyPool extends ForkJoinPool {
private final ArrayList<Runnable> submitted = new ArrayList<>();
private CapturingHydrologyPool() {
super(1);
}
@Override
public void execute(Runnable task) {
submitted.add(task);
}
}
@SuppressWarnings("unchecked")
private static Map<HydrologyTileKey, CompletableFuture<?>> runningOwners(HydrologyPlanner planner) throws Exception {
Field field = HydrologyPlanner.class.getDeclaredField("resolvingOwners");
field.setAccessible(true);
return (Map<HydrologyTileKey, CompletableFuture<?>>) field.get(planner);
}
@Test
public void earlyNeighborOwnersPreserveCompleteColdOwnerOutputs() throws Exception {
HydrologyTileKey key = new HydrologyTileKey(-1, -1);
Set<Thread> parallelThreads = ConcurrentHashMap.newKeySet();
boolean observedParallelOwner = false;
ForkJoinPool pool = new ForkJoinPool(4);
try {
for (boolean organic : new boolean[]{false, true}) {
HydrologyPlannerSettings settings = organic ? organicShapeSettings()
: standardSettings(4D, 2D, true, false, List.of());
settings = withPeriodTwo(settings);
assertEquals(2, settings.crossTileColorPeriod());
HydrologyTerrainSampler original = organic ? organicShapeTerrain() : rollingCoast(112);
int tileSize = settings.routing().tileSize();
HydrologyTerrainSampler terrain = (x, z) -> original.sample(x + tileSize, z + tileSize);
HydrologyTerrainSampler counted = (x, z) -> {
parallelThreads.add(Thread.currentThread());
return terrain.sample(x, z);
};
for (long seed : new long[]{19L, 77L, 642L}) {
HydrologyTile expected = new HydrologyPlanner(seed, settings, terrain).plan(key);
HydrologyPlanner candidate = new HydrologyPlanner(seed, settings, counted);
parallelThreads.clear();
HydrologyTile actual = pool.submit(() -> candidate.plan(key)).get(30, TimeUnit.SECONDS);
observedParallelOwner |= parallelThreads.size() > 1;
assertTileContentsEqual(expected, actual);
assertEquals(expected.courses(), actual.courses());
assertEquals(expected.diagnosticCandidates(), actual.diagnosticCandidates());
}
}
} finally {
pool.shutdownNow();
}
assertTrue("Expected selected sources to prepare lower-rank owners", observedParallelOwner);
}
@Test
public void earlyOwnersCompleteWithOneWorkerAndSaturatedRootCallers() throws Exception {
HydrologyTileKey key = new HydrologyTileKey(-1, -1);
HydrologyPlannerSettings settings = standardSettings(4D, 2D, true, false, List.of());
settings = withPeriodTwo(settings);
int tileSize = settings.routing().tileSize();
HydrologyTerrainSampler base = rollingCoast(112);
HydrologyTerrainSampler terrain = (x, z) -> base.sample(x + tileSize, z + tileSize);
HydrologyTile expected = new HydrologyPlanner(77L, settings, terrain).plan(key);
for (int workers : new int[]{1, 2}) {
ForkJoinPool pool = new ForkJoinPool(workers);
CountDownLatch started = new CountDownLatch(workers);
ArrayList<Future<HydrologyTile>> results = new ArrayList<>();
try {
HydrologyPlanner shared = new HydrologyPlanner(77L, settings, terrain);
for (int index = 0; index < workers; index++) {
results.add(pool.submit(() -> {
started.countDown();
if (!started.await(5, TimeUnit.SECONDS)) {
throw new AssertionError("Owner callers did not start");
}
return shared.plan(key);
}));
}
for (Future<HydrologyTile> result : results) {
assertTileContentsEqual(expected, result.get(30, TimeUnit.SECONDS));
}
} finally {
pool.shutdownNow();
}
}
}
private static void assertTileContentsEqual(HydrologyTile expected, HydrologyTile actual) {
assertEquals("nodes", expected.nodes(), actual.nodes());
assertEquals("edges", expected.edges(), actual.edges());
assertEquals("outlets", expected.outlets(), actual.outlets());
assertEquals("courses", expected.courses(), actual.courses());
assertEquals("cave plans", expected.cavePlans(), actual.cavePlans());
assertEquals("diagnostics", expected.diagnosticCandidates(), actual.diagnosticCandidates());
assertEquals("footprint column count", expected.footprint().columns().size(), actual.footprint().columns().size());
for (Map.Entry<Long, HydrologyColumnSample> entry : expected.footprint().columns().entrySet()) {
assertEquals("footprint column " + entry.getKey(), entry.getValue(), actual.footprint().columns().get(entry.getKey()));
}
assertEquals(expected, actual);
}
@Test
public void emptySelectionsAndLargerColorPeriodsDoNotExpandOwnerWork() throws Exception {
HydrologyPlannerSettings base = standardSettings(4D, 2D, true, false, List.of());
HydrologyPlannerSettings periodTwo = withPeriodTwo(base);
HydrologyTileKey key = new HydrologyTileKey(-1, -1);
ForkJoinPool pool = new ForkJoinPool(4);
try {
HydrologyPlanner empty = new HydrologyPlanner(77L, periodTwo, (x, z) -> blockedTerrain());
HydrologyTile expectedEmpty = new HydrologyPlanner(77L, periodTwo, (x, z) -> blockedTerrain()).plan(key);
assertEquals(expectedEmpty, pool.submit(() -> empty.plan(key)).get(30, TimeUnit.SECONDS));
assertEquals(Set.of(key), cachedOwnerKeys(empty));
assertTrue(base.crossTileColorPeriod() > 2);
HydrologyPlanner larger = new HydrologyPlanner(77L, base, rollingCoast(112));
HydrologyPlanner serial = new HydrologyPlanner(77L, base, rollingCoast(112));
HydrologyTile expected = serial.plan(key);
assertEquals(expected, pool.submit(() -> larger.plan(key)).get(30, TimeUnit.SECONDS));
assertEquals(cachedOwnerKeys(serial), cachedOwnerKeys(larger));
} finally {
pool.shutdownNow();
}
}
private static HydrologyPlannerSettings withPeriodTwo(HydrologyPlannerSettings settings) {
HydrologyPlannerSettings.Routing routing = settings.routing();
int spacing = routing.sampleSpacing();
int tileSize = (2 * (settings.publicationRadius() + 1) / spacing + 1) * spacing;
int latticeWidth = tileSize / spacing + 5;
return new HydrologyPlannerSettings(settings.seaLevel(),
new HydrologyPlannerSettings.Routing(tileSize, spacing, Math.max(routing.maximumRouteNodes(), latticeWidth * latticeWidth),
routing.maximumRouteLength(), routing.minimumSurfaceCourseLength(), routing.minimumUndergroundCourseLength(),
routing.valleyPreference(), routing.uphillPenalty(), routing.slopePenalty(),
routing.confluenceAttraction(), routing.lengthPreference(), routing.tributaries()),
settings.surface(), settings.hydraulics(), settings.underground(), settings.outlets(), settings.geometry(),
settings.deepFluids(), settings.surfacePools(), settings.widestShoreBiomeWidth(), settings.seaCaves());
}
private static Set<?> cachedOwnerKeys(HydrologyPlanner planner) throws Exception {
Field cacheField = HydrologyPlanner.class.getDeclaredField("resolvedOwners");
cacheField.setAccessible(true);
Cache<?, ?> cache = (Cache<?, ?>) cacheField.get(planner);
return Set.copyOf(cache.asMap().keySet());
}
@Test
public void earlyOwnerFailureDrainsRemainingTasksAndPreservesParentFailure() throws Exception {
HydrologyPlanner planner = new HydrologyPlanner(19L,
standardSettings(4D, 2D, true, false, List.of()), rollingCoast(112));
HydrologyTileKey key = new HydrologyTileKey(-1, -1);
AutoCloseable admission = earlyAdmission(planner, key);
Map<HydrologyTileKey, HydrologyForkJoin.Task<?>> tasks = earlyTasks(admission);
CountDownLatch started = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
CountDownLatch parentFailed = new CountDownLatch(1);
AtomicInteger completed = new AtomicInteger();
AssertionError childFailure = new AssertionError("owner failure");
IllegalArgumentException parentFailure = new IllegalArgumentException("parent failure");
try (ForkJoinPool pool = new ForkJoinPool(3)) {
HydrologyForkJoin.Task<?> failedTask = new HydrologyForkJoin.Task<>(() -> { throw childFailure; });
tasks.put(new HydrologyTileKey(-2, -2), failedTask);
pool.execute(failedTask);
HydrologyForkJoin.Task<?> delayedTask = new HydrologyForkJoin.Task<>(() -> {
started.countDown();
try {
if (!release.await(5, TimeUnit.SECONDS)) {
throw new AssertionError("Owner did not release");
}
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new AssertionError(failure);
}
completed.incrementAndGet();
return null;
});
tasks.put(new HydrologyTileKey(-2, -1), delayedTask);
pool.execute(delayedTask);
assertTrue(started.await(5, TimeUnit.SECONDS));
Future<?> parent = pool.submit(() -> {
try (admission) {
parentFailed.countDown();
throw parentFailure;
}
});
try {
assertTrue(parentFailed.await(5, TimeUnit.SECONDS));
assertFalse(parent.isDone());
} finally {
release.countDown();
}
ExecutionException failure = assertThrows(ExecutionException.class,
() -> parent.get(5, TimeUnit.SECONDS));
Throwable cause = failure.getCause();
if (cause != parentFailure) {
cause = cause.getCause();
}
assertEquals(parentFailure, cause);
assertEquals(1, parentFailure.getSuppressed().length);
assertEquals(1, completed.get());
}
}
@Test
public void unusedOwnerFailureDoesNotFailPublicationButDemandUsesItsOriginalFailure() throws Exception {
HydrologyPlanner planner = new HydrologyPlanner(19L,
standardSettings(4D, 2D, true, false, List.of()), rollingCoast(112));
HydrologyTileKey key = new HydrologyTileKey(-1, -1);
HydrologyTileKey dependency = new HydrologyTileKey(-2, -2);
AutoCloseable admission = earlyAdmission(planner, key);
Map<HydrologyTileKey, HydrologyForkJoin.Task<?>> tasks = earlyTasks(admission);
AtomicInteger attempts = new AtomicInteger();
IllegalStateException expected = new IllegalStateException("unused owner failure");
HydrologyForkJoin.Task<?> failed = new HydrologyForkJoin.Task<>(() -> {
attempts.incrementAndGet();
throw expected;
});
tasks.put(dependency, failed);
assertThrows(IllegalStateException.class, failed::await);
Field contextField = admission.getClass().getDeclaredField("context");
contextField.setAccessible(true);
Object context = contextField.get(admission);
Method resolve = HydrologyPlanner.class.getDeclaredMethod("resolveLowerRankOwners",
List.class, context.getClass(), Map.class);
resolve.setAccessible(true);
for (int demand = 0; demand < 2; demand++) {
InvocationTargetException failure = assertThrows(
InvocationTargetException.class,
() -> resolve.invoke(planner, List.of(dependency), context, tasks));
assertEquals(expected, failure.getCause());
}
assertEquals(1, attempts.get());
admission.close();
}
@Test
public void demandedOwnerErrorIsNotSuppressedOntoItselfDuringDrain() throws Exception {
HydrologyPlanner planner = new HydrologyPlanner(19L,
standardSettings(4D, 2D, true, false, List.of()), rollingCoast(112));
AutoCloseable admission = earlyAdmission(planner, new HydrologyTileKey(-1, -1));
AssertionError expected = new AssertionError("demanded owner failure");
HydrologyForkJoin.Task<?> failed = new HydrologyForkJoin.Task<>(() -> { throw expected; });
earlyTasks(admission).put(new HydrologyTileKey(-2, -2), failed);
assertThrows(AssertionError.class, failed::await);
Field primary = admission.getClass().getDeclaredField("primaryFailure");
primary.setAccessible(true);
primary.set(admission, expected);
AssertionError actual = assertThrows(AssertionError.class, () -> {
try (admission) {
throw expected;
}
});
assertEquals(expected, actual);
assertEquals(0, actual.getSuppressed().length);
}
private static AutoCloseable earlyAdmission(HydrologyPlanner planner, HydrologyTileKey key) throws Exception {
Class<?> contextType = Class.forName(HydrologyPlanner.class.getName() + "$CrossTileResolutionContext");
Constructor<?> contextConstructor = contextType.getDeclaredConstructor(HydrologyTileKey.class, long.class, int.class);
contextConstructor.setAccessible(true);
Object context = contextConstructor.newInstance(key, 64L, 4096);
Class<?> admissionType = Class.forName(HydrologyPlanner.class.getName() + "$ColorRankedDraftAdmission");
Constructor<?> admissionConstructor = admissionType.getDeclaredConstructor(
HydrologyPlanner.class, HydrologyTileKey.class, int.class, contextType);
admissionConstructor.setAccessible(true);
return (AutoCloseable) admissionConstructor.newInstance(planner, key, 3, context);
}
@SuppressWarnings("unchecked")
private static Map<HydrologyTileKey, HydrologyForkJoin.Task<?>> earlyTasks(AutoCloseable admission) throws Exception {
Field prepared = admission.getClass().getDeclaredField("preparedOwners");
prepared.setAccessible(true);
return (Map<HydrologyTileKey, HydrologyForkJoin.Task<?>>) prepared.get(admission);
}
@Test
public void anchorRadiusAndAdmissionRejectBeforeSlopeSampling() throws Exception {
HydrologyTerrainSample land = terrain(100, 7D, false, false, false, false, false, false);
CountingNaturalSampler outside = new CountingNaturalSampler((x, z) -> land);
assertEquals(Double.POSITIVE_INFINITY, anchorScore(countedPlanner(outside), land, 20, 0), 0D);
assertEquals(0, outside.basisCalls);
assertEquals(0, outside.slopeCalls);
CountingNaturalSampler blocked = new CountingNaturalSampler((x, z) -> blockedTerrain());
assertEquals(Double.POSITIVE_INFINITY, anchorScore(countedPlanner(blocked), land, 2, 0), 0D);
assertEquals(1, blocked.basisCalls);
assertEquals(0, blocked.slopeCalls);
CountingNaturalSampler crossing = new CountingNaturalSampler((x, z) -> x == 1 ? blockedTerrain() : land);
assertEquals(Double.POSITIVE_INFINITY, anchorScore(countedPlanner(crossing), land, 2, 0), 0D);
assertTrue(crossing.basisCalls > 1);
assertEquals(0, crossing.slopeCalls);
CountingNaturalSampler admitted = new CountingNaturalSampler((x, z) -> land);
HydrologyPlannerSettings.Routing routing = standardSettings(4D, 0D, true, false, List.of()).routing();
double expected = land.naturalHeight() * routing.valleyPreference()
+ land.slope() * routing.slopePenalty()
+ land.routingCost() * land.routingMultiplier() + 2D * 2.4D + 2D * 0.08D;
expected += HydrologyHash.unit(HydrologyHash.mix(91L, 719L, 2, 0)) * 1.0E-6D;
assertEquals(Double.doubleToLongBits(expected),
Double.doubleToLongBits(anchorScore(countedPlanner(admitted), land, 2, 0)));
assertEquals(1, admitted.slopeCalls);
}
@Test
public void refinedRouteSamplesFallbackOnlyWhenAllCandidatesFailAdmission() throws Exception {
HydrologyTerrainSample land = terrain(100, 7D, false, false, false, false, false, false);
AtomicInteger fallbackCalls = new AtomicInteger();
CountingNaturalSampler admitted = new CountingNaturalSampler((x, z) -> {
if (x == 100) {
fallbackCalls.incrementAndGet();
}
return land;
});
assertEquals(3, routeCandidates(countedPlanner(admitted)).size());
assertEquals(0, fallbackCalls.get());
CountingNaturalSampler rejected = new CountingNaturalSampler((x, z) -> {
if (x == 100) {
fallbackCalls.incrementAndGet();
return land;
}
return blockedTerrain();
});
assertEquals(1, routeCandidates(countedPlanner(rejected)).size());
assertEquals(1, fallbackCalls.get());
}
@Test
public void surfaceRouteMemoPreservesCompleteOwnerOutputAcrossPublicationPasses() throws Exception {
for (boolean organic : new boolean[]{false, true}) {
HydrologyPlannerSettings settings = organic ? organicShapeSettings()
: standardSettings(4D, 2D, true, false, List.of());
HydrologyTerrainSampler terrain = organic ? organicShapeTerrain() : rollingCoast(112);
SurfaceRouteMemos uncached = new SurfaceRouteMemos(false);
SurfaceRouteMemos cached = new SurfaceRouteMemos(true);
HydrologyTile expected = planWithSurfaceRouteMemos(new HydrologyPlanner(642L, settings, terrain), uncached);
HydrologyTile actual = planWithSurfaceRouteMemos(new HydrologyPlanner(642L, settings, terrain), cached);
assertEquals(expected, actual);
assertEquals(expected.courses(), actual.courses());
assertEquals(expected.diagnosticCandidates(), actual.diagnosticCandidates());
assertTrue(cached.computations <= uncached.computations);
if (organic) {
assertTrue("hits=" + cached.hits, cached.hits > 0);
assertTrue("uncached=" + uncached.computations + " cached=" + cached.computations,
cached.computations < uncached.computations);
}
}
}
@Test
public void biomeIncisionMultiplierCannotExceedTheConfiguredSurfaceMaximum() {
assertEquals(6, HydrologyPlanner.permittedSurfaceIncision(6, 2D));
@@ -2761,4 +3361,124 @@ public class HydrologyPlannerTest {
}
return false;
}
@SuppressWarnings("unchecked")
private HydrologyTile planWithSurfaceRouteMemos(HydrologyPlanner planner, SurfaceRouteMemos memos) throws Exception {
Class<?> samplesClass = Class.forName(HydrologyPlanner.class.getName() + "$PlanningSamples");
Constructor<?> constructor = samplesClass.getDeclaredConstructor();
constructor.setAccessible(true);
Object samples = constructor.newInstance();
Field routes = samplesClass.getDeclaredField("surfaceRoutes");
routes.setAccessible(true);
routes.set(samples, memos);
Field scope = HydrologyPlanner.class.getDeclaredField("planningSamples");
scope.setAccessible(true);
ThreadLocal<Object> local = (ThreadLocal<Object>) scope.get(planner);
local.set(samples);
try {
return planner.plan(TILE);
} finally {
local.remove();
}
}
private static final class SurfaceRouteMemos extends IdentityHashMap<Object, HashMap<Object, List<HydrologyPoint>>> {
private final boolean retain;
private int hits;
private int computations;
private SurfaceRouteMemos(boolean retain) {
this.retain = retain;
}
@Override
public HashMap<Object, List<HydrologyPoint>> computeIfAbsent(Object key,
Function<? super Object, ? extends HashMap<Object, List<HydrologyPoint>>> ignored) {
return super.computeIfAbsent(key, unused -> new HashMap<>() {
@Override
public List<HydrologyPoint> computeIfAbsent(Object route,
Function<? super Object, ? extends List<HydrologyPoint>> computation) {
if (!retain) {
clear();
}
if (containsKey(route)) {
hits++;
}
return super.computeIfAbsent(route, missing -> {
computations++;
return computation.apply(missing);
});
}
});
}
}
private HydrologyPlanner countedPlanner(CountingNaturalSampler sampler) {
return new HydrologyPlanner(91L, standardSettings(4D, 0D, true, false, List.of()),
sampler.delegate, sampler, HydrologyGeometrySampler.deterministic(sampler.delegate),
-4096, footprint -> solidCaveView());
}
private double anchorScore(HydrologyPlanner planner, HydrologyTerrainSample terrain, int x, int z) throws Exception {
Class<?> nodeType = Class.forName(HydrologyPlanner.class.getName() + "$GridNode");
Constructor<?> constructor = nodeType.getDeclaredConstructor(int.class, int.class, int.class,
int.class, int.class, long.class, HydrologyTerrainSample.class);
constructor.setAccessible(true);
Object node = constructor.newInstance(0, 0, 0, 0, 0, 719L, terrain);
Method method = HydrologyPlanner.class.getDeclaredMethod("anchorScore", nodeType,
int.class, int.class, int.class, int.class, double.class);
method.setAccessible(true);
return (double) method.invoke(planner, node, x, z, 0, 0, 0.3D);
}
private List<?> routeCandidates(HydrologyPlanner planner) throws Exception {
Class<?> directionType = Class.forName(HydrologyPlanner.class.getName() + "$Direction");
Constructor<?> directionConstructor = directionType.getDeclaredConstructor(double.class, double.class);
directionConstructor.setAccessible(true);
Object direction = directionConstructor.newInstance(1D, 0D);
Class<?> positionType = Class.forName(HydrologyPlanner.class.getName() + "$RoutePosition");
Constructor<?> positionConstructor = positionType.getDeclaredConstructor(double.class, double.class,
double.class, double.class, directionType);
positionConstructor.setAccessible(true);
Object position = positionConstructor.newInstance(0D, 0D, 100D, 0D, direction);
Method method = HydrologyPlanner.class.getDeclaredMethod("routeCandidates", long.class, long.class,
positionType, double.class, int.class, int.class, String.class);
method.setAccessible(true);
return (List<?>) method.invoke(planner, 1L, 2L, position, 0.5D, 4, 1, null);
}
private static final class CountingNaturalSampler implements HydrologyNaturalTerrainSampler {
private final HydrologyTerrainSampler delegate;
private int basisCalls;
private int slopeCalls;
private CountingNaturalSampler(HydrologyTerrainSampler delegate) {
this.delegate = delegate;
}
@Override
public HydrologyTerrainSample sampleBasis(int x, int z) {
slopeCalls++;
return delegate.sample(x, z);
}
@Override
public HydrologyTerrainSample sampleBasisWithoutSlope(int x, int z) {
basisCalls++;
HydrologyTerrainSample sample = delegate.sample(x, z);
return sample == null ? null : sample.withSlope(0D);
}
@Override
public HydrologyTerrainSample[] sampleGrid(GridRequest request) {
throw new AssertionError("Point sampling fixture does not request a grid.");
}
@Override
public NaturalClassification classifyNatural(int x, int z) {
HydrologyTerrainSample sample = delegate.sample(x, z);
return sample == null ? NaturalClassification.UNAVAILABLE
: sample.ocean() ? NaturalClassification.OCEAN : NaturalClassification.LAND;
}
}
}
@@ -13,6 +13,39 @@ import static org.junit.Assert.assertTrue;
public class HydrologyPlannerTributaryTest {
private static final HydrologyTileKey TILE = new HydrologyTileKey(0, 0);
@Test
public void heightOnlySamplingPreservesSurfacePlansWithFewerSlopeSamples() {
HydrologyPlannerSettings settings = settings(1);
HydrologyTerrainSampler terrain = (int x, int z) -> valley(x, z).withSlope(0.5D);
CountingNaturalSampler referenceSampler = new CountingNaturalSampler(terrain, false);
CountingNaturalSampler optimizedSampler = new CountingNaturalSampler(terrain, true);
HydrologyTile reference = planner(settings, terrain, referenceSampler).plan(TILE);
HydrologyTile optimized = planner(settings, terrain, optimizedSampler).plan(TILE);
assertFalse(reference.courses().isEmpty());
assertEquals(reference, optimized);
assertTrue(optimizedSampler.heightOnlyCalls > 0);
assertTrue("reference=" + referenceSampler.slopeCalls + ", optimized=" + optimizedSampler.slopeCalls,
optimizedSampler.slopeCalls < referenceSampler.slopeCalls);
}
private static HydrologyPlanner planner(
HydrologyPlannerSettings settings,
HydrologyTerrainSampler terrain,
HydrologyNaturalTerrainSampler naturalSampler
) {
return new HydrologyPlanner(
19L,
settings,
terrain,
naturalSampler,
HydrologyGeometrySampler.deterministic(terrain),
-4096,
footprint -> new HydrologyTerrainCaveVoxelView(terrain, settings.seaLevel(), -4096, 4096)
);
}
@Test
public void aSecondSourceJoinsTheMainStemAsATributaryWhenAllowed() {
HydrologyTerrainSampler terrain = HydrologyPlannerTributaryTest::valley;
@@ -419,4 +452,49 @@ public class HydrologyPlannerTributaryTest {
HydrologyPlannerSettings.SeaCaves.disabled()
);
}
private static final class CountingNaturalSampler implements HydrologyNaturalTerrainSampler {
private final HydrologyTerrainSampler terrain;
private final boolean omitUnusedSlope;
private int slopeCalls;
private int heightOnlyCalls;
private CountingNaturalSampler(HydrologyTerrainSampler terrain, boolean omitUnusedSlope) {
this.terrain = terrain;
this.omitUnusedSlope = omitUnusedSlope;
}
@Override
public HydrologyTerrainSample[] sampleGrid(GridRequest request) {
HydrologyTerrainSample[] samples = new HydrologyTerrainSample[request.width() * request.width()];
for (int gridZ = 0; gridZ < request.width(); gridZ++) {
for (int gridX = 0; gridX < request.width(); gridX++) {
int x = request.minimumX() + gridX * request.spacing();
int z = request.minimumZ() + gridZ * request.spacing();
samples[gridZ * request.width() + gridX] = terrain.sample(x, z);
}
}
return samples;
}
@Override
public NaturalClassification classifyNatural(int blockX, int blockZ) {
return terrain.sample(blockX, blockZ).ocean() ? NaturalClassification.OCEAN : NaturalClassification.LAND;
}
@Override
public HydrologyTerrainSample sampleBasis(int blockX, int blockZ) {
slopeCalls++;
return terrain.sample(blockX, blockZ);
}
@Override
public HydrologyTerrainSample sampleBasisWithoutSlope(int blockX, int blockZ) {
if (!omitUnusedSlope) {
return sampleBasis(blockX, blockZ);
}
heightOnlyCalls++;
return terrain.sample(blockX, blockZ).withSlope(0D);
}
}
}
@@ -0,0 +1,233 @@
package art.arcane.iris.engine.hydrology;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
public class HydrologyRouteTurnCacheTest {
@Test
public void repeatedRefinementMatchesExhaustiveUncachedRoutes() throws Exception {
Random random = new Random(712319L);
Solver solver = new Solver();
for (int scenario = 0; scenario < 40; scenario++) {
int count = 4 + random.nextInt(4);
HydrologyPoint[][] points = new HydrologyPoint[count][];
for (int layer = 0; layer < count; layer++) {
points[layer] = new HydrologyPoint[layer == 0 || layer == count - 1 ? 1 : 3];
for (int candidate = 0; candidate < points[layer].length; candidate++) {
points[layer][candidate] = new HydrologyPoint(layer * 12 + random.nextInt(5), 80,
candidate * 9 + random.nextInt(7));
}
}
double maximumTurn = scenario % 2 == 0 ? 35D : 115D;
double[][][][] cache = new double[count][][][];
for (int refinement = 0; refinement < 8; refinement++) {
Fixture fixture = fixture(points, random);
assertArrayEquals("scenario=" + scenario + " refinement=" + refinement,
exhaustive(fixture, maximumTurn), solver.solve(fixture, maximumTurn, cache));
assertCacheGeometry(fixture, maximumTurn, cache);
}
}
}
@Test
public void straightTiesAndDegenerateSegmentsKeepOriginalFirstChoice() throws Exception {
HydrologyPoint[][] points = {
{new HydrologyPoint(0, 80, 0)},
{new HydrologyPoint(0, 90, 0), new HydrologyPoint(0, 70, 0)},
{new HydrologyPoint(12, 80, 0), new HydrologyPoint(12, 80, 0)},
{new HydrologyPoint(24, 80, 0)}
};
Fixture fixture = fixture(points, new Random(2L));
for (double[] scores : fixture.scores()) {
Arrays.fill(scores, 0D);
}
for (double[] penalties : fixture.penalties()) {
Arrays.fill(penalties, 0D);
}
for (double[][] transitions : fixture.transitions()) {
if (transitions != null) {
for (double[] row : transitions) {
Arrays.fill(row, 0D);
}
}
}
assertArrayEquals(new int[]{0, 0, 0, 0}, new Solver().solve(fixture, 75D, new double[4][][][]));
}
private static Fixture fixture(HydrologyPoint[][] coordinates, Random random) {
int count = coordinates.length;
HydrologyPoint[][] points = new HydrologyPoint[count][];
double[][] scores = new double[count][];
double[][] penalties = new double[count][];
double[][][] transitions = new double[count][][];
for (int layer = 0; layer < count; layer++) {
int size = coordinates[layer].length;
points[layer] = new HydrologyPoint[size];
scores[layer] = new double[size];
penalties[layer] = new double[size];
for (int candidate = 0; candidate < size; candidate++) {
HydrologyPoint point = coordinates[layer][candidate];
points[layer][candidate] = new HydrologyPoint(point.x(), random.nextInt(200), point.z());
scores[layer][candidate] = random.nextDouble() * 20D;
penalties[layer][candidate] = random.nextDouble() * 10D;
}
if (layer > 0) {
transitions[layer] = new double[coordinates[layer - 1].length][size];
for (double[] row : transitions[layer]) {
for (int current = 0; current < size; current++) {
row[current] = random.nextInt(8) == 0 ? Double.POSITIVE_INFINITY : random.nextDouble() * 30D;
}
}
}
}
return new Fixture(points, scores, penalties, transitions);
}
private static int[] exhaustive(Fixture fixture, double maximumTurn) {
ArrayList<int[]> routes = new ArrayList<>();
enumerate(fixture.points(), new int[fixture.points().length], 0, routes);
int[] selected = new int[0];
double selectedCost = Double.POSITIVE_INFINITY;
for (int[] route : routes) {
double cost = fixture.scores()[0][route[0]] + fixture.scores()[1][route[1]]
+ fixture.penalties()[1][route[1]] + fixture.transitions()[1][route[0]][route[1]];
for (int layer = 2; layer < route.length && Double.isFinite(cost); layer++) {
double turn = turn(fixture.points()[layer - 2][route[layer - 2]],
fixture.points()[layer - 1][route[layer - 1]], fixture.points()[layer][route[layer]]);
if (turn > maximumTurn) {
cost = Double.POSITIVE_INFINITY;
break;
}
double excess = Math.max(0D, turn - 8D);
cost = cost + fixture.scores()[layer][route[layer]] + fixture.penalties()[layer][route[layer]]
+ fixture.transitions()[layer][route[layer - 1]][route[layer]]
+ excess * excess * 12D / maximumTurn;
}
if (cost < selectedCost || Double.isFinite(cost) && cost == selectedCost && precedes(route, selected)) {
selectedCost = cost;
selected = route;
}
}
return selected;
}
private static boolean precedes(int[] candidate, int[] selected) {
int last = candidate.length - 1;
if (candidate[last - 1] != selected[last - 1]) {
return candidate[last - 1] < selected[last - 1];
}
if (candidate[last] != selected[last]) {
return candidate[last] < selected[last];
}
for (int index = last - 2; index >= 0; index--) {
if (candidate[index] != selected[index]) {
return candidate[index] < selected[index];
}
}
return false;
}
private static void enumerate(HydrologyPoint[][] points, int[] route, int layer, List<int[]> routes) {
if (layer == route.length) {
routes.add(route.clone());
return;
}
for (int candidate = 0; candidate < points[layer].length; candidate++) {
route[layer] = candidate;
enumerate(points, route, layer + 1, routes);
}
}
private static void assertCacheGeometry(Fixture fixture, double maximumTurn, double[][][][] cache) {
int populated = 0;
for (int layer = 2; layer < cache.length; layer++) {
for (int previous = 0; previous < cache[layer].length; previous++) {
for (int current = 0; current < cache[layer][previous].length; current++) {
double[] costs = cache[layer][previous][current];
if (costs == null) {
continue;
}
for (int before = 0; before < costs.length; before++) {
if (Double.isNaN(costs[before])) {
continue;
}
double turn = turn(fixture.points()[layer - 2][before],
fixture.points()[layer - 1][previous], fixture.points()[layer][current]);
double excess = Math.max(0D, turn - 8D);
double expected = turn > maximumTurn ? Double.POSITIVE_INFINITY : excess * excess * 12D / maximumTurn;
assertEquals(Double.doubleToLongBits(expected), Double.doubleToLongBits(costs[before]));
populated++;
}
}
}
}
assertTrue(populated > 0);
}
private static double turn(HydrologyPoint before, HydrologyPoint previous, HydrologyPoint current) {
double incomingX = previous.x() - before.x();
double incomingZ = previous.z() - before.z();
double outgoingX = current.x() - previous.x();
double outgoingZ = current.z() - previous.z();
double incomingLength = StrictMath.hypot(incomingX, incomingZ);
double outgoingLength = StrictMath.hypot(outgoingX, outgoingZ);
if (incomingLength == 0D || outgoingLength == 0D) {
return 0D;
}
return StrictMath.toDegrees(StrictMath.acos(Math.max(-1D, Math.min(1D,
(incomingX * outgoingX + incomingZ * outgoingZ) / (incomingLength * outgoingLength)))));
}
private record Fixture(HydrologyPoint[][] points, double[][] scores, double[][] penalties, double[][][] transitions) {
}
private static final class Solver {
private final HydrologyPlanner planner = mock(HydrologyPlanner.class, CALLS_REAL_METHODS);
private final Constructor<?> candidateConstructor;
private final Object tangent;
private final Method solve;
private final Method indices;
private Solver() throws Exception {
Class<?> direction = Class.forName(HydrologyPlanner.class.getName() + "$Direction");
Constructor<?> directionConstructor = direction.getDeclaredConstructor(double.class, double.class);
directionConstructor.setAccessible(true);
tangent = directionConstructor.newInstance(1D, 0D);
Class<?> candidate = Class.forName(HydrologyPlanner.class.getName() + "$RouteCandidate");
candidateConstructor = candidate.getDeclaredConstructor(HydrologyPoint.class, double.class, double.class,
double.class, direction, boolean.class);
candidateConstructor.setAccessible(true);
solve = HydrologyPlanner.class.getDeclaredMethod("selectCurvatureAwareTerrainRoute", List.class,
double.class, double.class, double.class, double[][].class, double[][][].class, double[][][][].class);
solve.setAccessible(true);
indices = solve.getReturnType().getDeclaredMethod("indices");
indices.setAccessible(true);
}
private int[] solve(Fixture fixture, double maximumTurn, double[][][][] cache) throws Exception {
ArrayList<List<Object>> layers = new ArrayList<>();
for (int layer = 0; layer < fixture.points().length; layer++) {
ArrayList<Object> candidates = new ArrayList<>();
for (int candidate = 0; candidate < fixture.points()[layer].length; candidate++) {
candidates.add(candidateConstructor.newInstance(fixture.points()[layer][candidate], 0D,
fixture.scores()[layer][candidate], 0D, tangent, true));
}
layers.add(candidates);
}
return (int[]) indices.invoke(solve.invoke(planner, layers, 12D, maximumTurn, 12D,
fixture.penalties(), fixture.transitions(), cache));
}
}
}
@@ -1,20 +1,27 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.volmlib.util.cache.CacheKey;
import org.junit.Test;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -23,6 +30,8 @@ import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -34,6 +43,183 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.when;
public class HydrologyTileCacheTest {
@Test
public void explicitAreaIsFullyQueuedBeforeExecutorCallbacksCanAddOldWork() throws Exception {
for (boolean pregeneration : new boolean[]{false, true}) {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyPlannerSettings settings = stalePrefetchSettings();
when(planner.settings()).thenReturn(settings);
ArrayList<HydrologyTileKey> planned = new ArrayList<>();
AtomicReference<HydrologyTileCache> cacheReference = new AtomicReference<>();
AtomicReference<Object> monitor = new AtomicReference<>();
AtomicBoolean injected = new AtomicBoolean();
doAnswer(invocation -> {
assertFalse(Thread.holdsLock(monitor.get()));
planned.add(invocation.getArgument(0));
return tile;
}).when(planner).plan(any(HydrologyTileKey.class));
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, task -> {
assertFalse(Thread.holdsLock(monitor.get()));
if (injected.compareAndSet(false, true)) {
cacheReference.get().prefetchArea(0, 0, 1023, 0, 0, 0);
}
task.run();
});
cacheReference.set(cache);
Field queue = HydrologyTileCache.class.getDeclaredField("prefetchQueue");
queue.setAccessible(true);
monitor.set(queue.get(cache));
if (pregeneration) {
cache.preparePregeneration(262144, 262144);
} else {
cache.prefetchArea(261120, 261120, 263168, 263168, 262144, 262144);
}
assertEquals(new HydrologyTileKey(256, 256), planned.getFirst());
assertEquals(prefetchRectangle(254, 257, 254, 257), new HashSet<>(planned.subList(0, 16)));
assertEquals(List.of(new HydrologyTileKey(255, 255), new HydrologyTileKey(256, 255),
new HydrologyTileKey(257, 255)), planned.subList(1, 4));
assertEquals(prefetchRectangle(-1, 1, -1, 0), new HashSet<>(planned.subList(16, planned.size())));
assertEquals(22, planned.size());
}
}
@Test
public void pregenDiscardsStaleQueueWithoutCancelingActivePlansOrDemand() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyPlannerSettings settings = stalePrefetchSettings();
when(planner.settings()).thenReturn(settings);
List<HydrologyTileKey> planned = Collections.synchronizedList(new ArrayList<>());
HydrologyTileKey activeKey = new HydrologyTileKey(0, 0);
HydrologyTileKey demandedKey = new HydrologyTileKey(-1, 0);
CountDownLatch started = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
doAnswer(invocation -> {
HydrologyTileKey key = invocation.getArgument(0);
planned.add(key);
if (key.equals(activeKey)) {
started.countDown();
assertTrue(release.await(5, TimeUnit.SECONDS));
}
return tile;
}).when(planner).plan(any(HydrologyTileKey.class));
LinkedBlockingQueue<Runnable> queued = new LinkedBlockingQueue<>();
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, queued::add);
ExecutorService callers = Executors.newFixedThreadPool(2);
try {
cache.prefetchArea(0, 0, 1023, 0, 0, 0);
Runnable initial = queued.poll(5, TimeUnit.SECONDS);
assertNotNull(initial);
Future<?> active = callers.submit(initial);
assertTrue(started.await(5, TimeUnit.SECONDS));
Future<HydrologyTile> demanded = callers.submit(() -> cache.get(demandedKey));
Runnable demand = queued.poll(5, TimeUnit.SECONDS);
assertNotNull(demand);
cache.preparePregeneration(262144, 262144);
assertFalse(active.isDone());
assertFalse(demanded.isDone());
release.countDown();
active.get(5, TimeUnit.SECONDS);
assertTrue(queued.isEmpty());
demand.run();
assertSame(tile, demanded.get(5, TimeUnit.SECONDS));
drainPrefetchTasks(queued);
Set<HydrologyTileKey> expected = new HashSet<>(prefetchRectangle(254, 257, 254, 257));
expected.add(activeKey);
expected.add(demandedKey);
assertEquals(expected, new HashSet<>(planned));
assertEquals(18, planned.size());
assertSame(tile, cache.get(activeKey));
verify(planner, times(1)).plan(activeKey);
verify(planner, times(1)).plan(demandedKey);
} finally {
release.countDown();
callers.shutdownNow();
}
}
@Test
public void repeatedPregenPreparationRetainsTheFullCurrentLookaheadAndClearsCleanly() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyPlannerSettings settings = stalePrefetchSettings();
when(planner.settings()).thenReturn(settings);
ArrayList<HydrologyTileKey> planned = new ArrayList<>();
doAnswer(invocation -> {
planned.add(invocation.getArgument(0));
return tile;
}).when(planner).plan(any(HydrologyTileKey.class));
LinkedBlockingQueue<Runnable> queued = new LinkedBlockingQueue<>();
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, queued::add);
cache.prefetchArea(0, 0, 1023, 0, 0, 0);
cache.preparePregeneration(262144, 262144);
cache.preparePregeneration(263168, 262144);
cache.preparePregeneration(263168, 262144);
drainPrefetchTasks(queued);
Set<HydrologyTileKey> expected = new HashSet<>(prefetchRectangle(255, 258, 254, 257));
expected.add(new HydrologyTileKey(0, 0));
assertEquals(expected, new HashSet<>(planned));
assertEquals(17, planned.size());
cache.clear();
planned.clear();
cache.preparePregeneration(263168, 262144);
drainPrefetchTasks(queued);
assertEquals(prefetchRectangle(255, 258, 254, 257), new HashSet<>(planned));
assertEquals(16, planned.size());
}
@Test
public void invalidPregenBoundsLeaveQueuedPrefetchIntact() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
HydrologyPlannerSettings settings = stalePrefetchSettings();
when(planner.settings()).thenReturn(settings);
ArrayList<HydrologyTileKey> planned = new ArrayList<>();
doAnswer(invocation -> {
planned.add(invocation.getArgument(0));
return tile;
}).when(planner).plan(any(HydrologyTileKey.class));
LinkedBlockingQueue<Runnable> queued = new LinkedBlockingQueue<>();
HydrologyTileCache cache = new HydrologyTileCache(planner, 128, queued::add);
cache.prefetchArea(0, 0, 1023, 0, 0, 0);
assertThrows(ArithmeticException.class, () -> cache.preparePregeneration(Integer.MAX_VALUE, 0));
assertThrows(ArithmeticException.class, () -> cache.preparePregeneration(0, Integer.MIN_VALUE));
drainPrefetchTasks(queued);
assertEquals(prefetchRectangle(-1, 1, -1, 0), new HashSet<>(planned));
assertEquals(6, planned.size());
}
private static HydrologyPlannerSettings stalePrefetchSettings() {
HydrologyPlannerSettings settings = mock(HydrologyPlannerSettings.class);
when(settings.routing()).thenReturn(new HydrologyPlannerSettings.Routing(
1024, 64, 4096, 2048, 0, 0, 0D, 0D, 0D, 0D, 1D, 0));
when(settings.publicationRadius()).thenReturn(364);
return settings;
}
private static Set<HydrologyTileKey> prefetchRectangle(int minX, int maxX, int minZ, int maxZ) {
HashSet<HydrologyTileKey> keys = new HashSet<>();
for (int z = minZ; z <= maxZ; z++) {
for (int x = minX; x <= maxX; x++) {
keys.add(new HydrologyTileKey(x, z));
}
}
return keys;
}
private static void drainPrefetchTasks(LinkedBlockingQueue<Runnable> queued) {
Runnable task;
while ((task = queued.poll()) != null) {
task.run();
}
}
@Test
public void equivalentStudioRuntimesReuseCompletedTiles() {
HydrologyPlanner firstPlanner = mock(HydrologyPlanner.class);
@@ -108,6 +294,7 @@ public class HydrologyTileCacheTest {
closing.close();
allowCompletion.countDown();
assertSame(lateTile, late.get(10L, TimeUnit.SECONDS));
assertEquals(0, closing.size());
assertSame(plannedTile, succeeding.get(key));
verify(succeedingPlanner, times(1)).plan(key);
} finally {
@@ -612,6 +799,233 @@ public class HydrologyTileCacheTest {
}
}
@Test
public void collidingTileHashesDoNotSerializePlanning() throws Exception {
List<HydrologyTileKey> keys = List.of(new HydrologyTileKey(0, 31), new HydrologyTileKey(1, 0));
assertEquals(keys.getFirst().hashCode(), keys.getLast().hashCode());
assertConcurrentTilePlanning(keys);
}
@Test
public void diagonalColdTilesDoNotSerializePlanning() throws Exception {
assertConcurrentTilePlanning(List.of(new HydrologyTileKey(127, 127), new HydrologyTileKey(128, 128)));
}
private void assertConcurrentTilePlanning(List<HydrologyTileKey> keys) throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
CountDownLatch started = new CountDownLatch(keys.size());
CountDownLatch release = new CountDownLatch(1);
doAnswer(invocation -> {
started.countDown();
assertTrue(release.await(5, TimeUnit.SECONDS));
return tile;
}).when(planner).plan(any(HydrologyTileKey.class));
ExecutorService executor = Executors.newFixedThreadPool(keys.size());
ExecutorService caller = Executors.newSingleThreadExecutor();
try {
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, executor);
Future<List<HydrologyTile>> result = caller.submit(() -> cache.tiles(keys));
assertTrue("colliding keys must start before either plan completes", started.await(5, TimeUnit.SECONDS));
release.countDown();
assertEquals(keys.size(), result.get(5, TimeUnit.SECONDS).size());
for (HydrologyTileKey key : keys) {
verify(planner, times(1)).plan(key);
}
} finally {
release.countDown();
caller.shutdownNow();
executor.shutdownNow();
}
}
@Test
public void collidingChunkCacheBinsDoNotSerializeComposition() throws Exception {
int secondChunkX = 207;
int firstHash = Long.hashCode(CacheKey.mix(RiverFootprint.pack(0, 0)));
int secondHash = Long.hashCode(CacheKey.mix(RiverFootprint.pack(secondChunkX, 0)));
assertEquals((firstHash ^ (firstHash >>> 16)) & 127, (secondHash ^ (secondHash >>> 16)) & 127);
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
CountDownLatch started = new CountDownLatch(2);
CountDownLatch release = new CountDownLatch(1);
doAnswer(invocation -> {
int blockX = invocation.getArgument(0);
int blockZ = invocation.getArgument(1);
if ((blockX == 0 || blockX == secondChunkX * 16) && blockZ == 0) {
started.countDown();
assertTrue(release.await(5, TimeUnit.SECONDS));
}
return Optional.empty();
}).when(tile).columnAt(anyInt(), anyInt());
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
HydrologyTileCache cache = new HydrologyTileCache(planner, 64);
Future<?> first = executor.submit(() -> cache.prepareChunkColumns(0, 0));
Future<?> second = executor.submit(() -> cache.prepareChunkColumns(secondChunkX * 16, 0));
assertTrue("colliding chunks must compose before either finishes", started.await(5, TimeUnit.SECONDS));
release.countDown();
first.get(5, TimeUnit.SECONDS);
second.get(5, TimeUnit.SECONDS);
} finally {
release.countDown();
executor.shutdownNow();
}
}
@Test
public void concurrentSameChunkRequestsComposeOnlyOnce() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
when(tile.columnAt(anyInt(), anyInt())).thenReturn(Optional.empty());
ExecutorService executor = Executors.newFixedThreadPool(8);
try {
HydrologyTileCache cache = new HydrologyTileCache(planner, 64);
List<Callable<Optional<HydrologyColumnSample>>> calls = new ArrayList<>();
for (int index = 0; index < 32; index++) {
calls.add(() -> cache.columnAt(0, 0));
}
for (Future<Optional<HydrologyColumnSample>> result : executor.invokeAll(calls, 5, TimeUnit.SECONDS)) {
assertTrue(result.get(5, TimeUnit.SECONDS).isEmpty());
}
verify(tile, times(256)).columnAt(anyInt(), anyInt());
verify(planner, times(1)).plan(new HydrologyTileKey(0, 0));
} finally {
executor.shutdownNow();
}
}
@Test
public void inlineWorkerCanClaimItsQueuedPrefetch() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
ForkJoinPool executor = new ForkJoinPool(1);
try {
HydrologyTileCache cache = new HydrologyTileCache(planner, 8, executor);
HydrologyTileKey key = new HydrologyTileKey(0, 0);
HydrologyTile result = executor.submit(() -> {
cache.prefetchArea(0, 0, 0, 0, 0, 0);
return cache.get(key);
}).get(5, TimeUnit.SECONDS);
assertSame(tile, result);
assertTrue(executor.awaitQuiescence(5, TimeUnit.SECONDS));
verify(planner, times(1)).plan(key);
} finally {
executor.shutdownNow();
}
}
@Test
public void clearDoesNotJoinOrPublishAnOldTilePlan() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile oldTile = mock(HydrologyTile.class);
HydrologyTile newTile = mock(HydrologyTile.class);
HydrologyTileKey key = new HydrologyTileKey(0, 0);
CountDownLatch started = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
AtomicInteger calls = new AtomicInteger();
doAnswer(invocation -> {
if (calls.incrementAndGet() == 1) {
started.countDown();
assertTrue(release.await(5, TimeUnit.SECONDS));
return oldTile;
}
return newTile;
}).when(planner).plan(key);
ExecutorService executor = Executors.newFixedThreadPool(2);
ExecutorService callers = Executors.newFixedThreadPool(2);
try {
HydrologyTileCache cache = new HydrologyTileCache(planner, 8, executor);
Future<HydrologyTile> oldResult = callers.submit(() -> cache.get(key));
assertTrue(started.await(5, TimeUnit.SECONDS));
cache.clear();
assertSame(newTile, callers.submit(() -> cache.get(key)).get(5, TimeUnit.SECONDS));
release.countDown();
assertSame(oldTile, oldResult.get(5, TimeUnit.SECONDS));
assertSame(newTile, cache.get(key));
assertEquals(2, calls.get());
} finally {
release.countDown();
callers.shutdownNow();
executor.shutdownNow();
}
}
@Test
public void queuedPrefetchCannotPublishAcrossClear() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
List<Runnable> tasks = new ArrayList<>();
HydrologyTileCache cache = new HydrologyTileCache(planner, 8, tasks::add);
cache.prefetchArea(0, 0, 0, 0, 0, 0);
cache.clear();
tasks.removeFirst().run();
assertEquals(0, cache.size());
cache.prefetchArea(0, 0, 0, 0, 0, 0);
tasks.removeFirst().run();
assertEquals(1, cache.size());
verify(planner, times(2)).plan(new HydrologyTileKey(0, 0));
}
@Test
public void clearDoesNotJoinOrPublishOldChunkColumns() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile oldTile = mock(HydrologyTile.class);
HydrologyTile newTile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(oldTile, newTile);
CountDownLatch started = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
doAnswer(invocation -> {
int blockX = invocation.getArgument(0);
int blockZ = invocation.getArgument(1);
if (blockX == 0 && blockZ == 0) {
started.countDown();
assertTrue(release.await(5, TimeUnit.SECONDS));
}
return Optional.empty();
}).when(oldTile).columnAt(anyInt(), anyInt());
HydrologyColumnSample sample = new HydrologyColumnSample(0, 0, 90, 63, false, "parent", List.of());
when(newTile.columnAt(anyInt(), anyInt())).thenReturn(Optional.of(sample));
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
HydrologyTileCache cache = new HydrologyTileCache(planner, 8);
Future<Optional<HydrologyColumnSample>> oldResult = executor.submit(() -> cache.columnAt(0, 0));
assertTrue(started.await(5, TimeUnit.SECONDS));
cache.clear();
assertEquals(Optional.of(sample), executor.submit(() -> cache.columnAt(0, 0)).get(5, TimeUnit.SECONDS));
release.countDown();
assertTrue(oldResult.get(5, TimeUnit.SECONDS).isEmpty());
assertEquals(Optional.of(sample), cache.columnAt(0, 0));
verify(newTile, times(256)).columnAt(anyInt(), anyInt());
} finally {
release.countDown();
executor.shutdownNow();
}
}
@Test(timeout = 5000)
public void recursiveChunkCompositionFailsInsteadOfJoiningItself() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
HydrologyTileCache cache = new HydrologyTileCache(planner, 8);
doAnswer(invocation -> cache.columnAt(0, 0)).when(tile).columnAt(anyInt(), anyInt());
org.junit.Assert.assertThrows(IllegalStateException.class, () -> cache.columnAt(0, 0));
}
@Test
public void tileBatchesNeverKeepMoreThanHalfTheCacheBoundInFlight() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
@@ -852,6 +1266,199 @@ public class HydrologyTileCacheTest {
assertEquals(4, cache.size());
verify(planner, times(4)).plan(any(HydrologyTileKey.class));
}
@Test
public void speculativeQueueWaitsForEveryDemandAndSharesQueuedPlans() throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
LinkedBlockingQueue<Runnable> queued = new LinkedBlockingQueue<>();
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, queued::add);
int tileSize = emptySettings().routing().tileSize();
cache.prefetchArea(0, 0, tileSize * 4 - 1, 0, 0, 0);
Runnable initialPrefetch = queued.poll(5, TimeUnit.SECONDS);
assertNotNull(initialPrefetch);
ExecutorService caller = Executors.newSingleThreadExecutor();
try {
List<HydrologyTileKey> keys = List.of(
new HydrologyTileKey(0, 0), new HydrologyTileKey(1, 0), new HydrologyTileKey(2, 0));
Future<List<HydrologyTile>> result = caller.submit(() -> cache.tiles(keys));
Runnable firstDemand = queued.poll(5, TimeUnit.SECONDS);
Runnable secondDemand = queued.poll(5, TimeUnit.SECONDS);
assertNotNull(firstDemand);
assertNotNull(secondDemand);
initialPrefetch.run();
assertTrue(queued.isEmpty());
assertSame(tile, cache.get(new HydrologyTileKey(0, 0)));
assertTrue(queued.isEmpty());
firstDemand.run();
assertTrue(queued.isEmpty());
secondDemand.run();
assertEquals(3, result.get(5, TimeUnit.SECONDS).size());
Runnable resumedPrefetch = queued.poll(5, TimeUnit.SECONDS);
assertNotNull(resumedPrefetch);
resumedPrefetch.run();
assertTrue(queued.isEmpty());
for (int index = 0; index < 4; index++) {
verify(planner, times(1)).plan(new HydrologyTileKey(index, 0));
}
} finally {
caller.shutdownNow();
}
}
@Test
public void demandCompletingBeforeActivePrefetchDoesNotLoseResume() throws Exception {
assertDemandPrefetchResume(true, false);
}
@Test
public void immediateAsyncBatchRegistersAllDemandBeforePrefetchResumes() throws Exception {
assertBatchDemandOrder(false);
}
@Test
public void inlineBatchRegistersAllDemandBeforePrefetchResumes() throws Exception {
assertBatchDemandOrder(true);
}
private void assertBatchDemandOrder(boolean inline) throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
ArrayList<HydrologyTileKey> order = new ArrayList<>();
AtomicReference<HydrologyTileCache> cacheReference = new AtomicReference<>();
int tileSize = emptySettings().routing().tileSize();
doAnswer(invocation -> {
HydrologyTileKey key = invocation.getArgument(0);
order.add(key);
if (key.tileX() == 1) {
cacheReference.get().prefetchArea(0, 0, tileSize * 4 - 1, 0, 0, 0);
}
return tile;
}).when(planner).plan(any(HydrologyTileKey.class));
ForkJoinPool pool = new ForkJoinPool(1) {
@Override
public void execute(Runnable task) {
task.run();
}
};
try {
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, inline ? pool : Runnable::run);
cacheReference.set(cache);
List<HydrologyTileKey> keys = List.of(new HydrologyTileKey(1, 0), new HydrologyTileKey(2, 0));
if (inline) {
assertEquals(2, pool.submit(() -> cache.tiles(keys)).get(5, TimeUnit.SECONDS).size());
} else {
assertEquals(2, cache.tiles(keys).size());
}
assertEquals(List.of(new HydrologyTileKey(1, 0), new HydrologyTileKey(2, 0),
new HydrologyTileKey(0, 0), new HydrologyTileKey(3, 0)), order);
} finally {
pool.shutdownNow();
}
}
@Test
public void failedDemandResumesPausedPrefetch() throws Exception {
assertDemandPrefetchResume(false, true);
}
private void assertDemandPrefetchResume(boolean demandFirst, boolean failDemand) throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
HydrologyTileKey demandKey = new HydrologyTileKey(1, 0);
if (failDemand) {
when(planner.plan(demandKey)).thenThrow(new IllegalStateException("Interrupted", new InterruptedException()));
}
LinkedBlockingQueue<Runnable> queued = new LinkedBlockingQueue<>();
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, queued::add);
int tileSize = emptySettings().routing().tileSize();
cache.prefetchArea(0, 0, tileSize * 3 - 1, 0, 0, 0);
Runnable initialPrefetch = queued.poll(5, TimeUnit.SECONDS);
assertNotNull(initialPrefetch);
ExecutorService caller = Executors.newSingleThreadExecutor();
try {
Future<HydrologyTile> result = caller.submit(() -> cache.get(demandKey));
Runnable demand = queued.poll(5, TimeUnit.SECONDS);
assertNotNull(demand);
if (demandFirst) {
demand.run();
assertTrue(queued.isEmpty());
initialPrefetch.run();
} else {
initialPrefetch.run();
assertTrue(queued.isEmpty());
demand.run();
}
if (failDemand) {
assertThrows(ExecutionException.class, () -> result.get(5, TimeUnit.SECONDS));
} else {
assertSame(tile, result.get(5, TimeUnit.SECONDS));
}
Runnable resumed = queued.poll(5, TimeUnit.SECONDS);
assertNotNull(resumed);
resumed.run();
assertTrue(queued.isEmpty());
verify(planner, times(1)).plan(new HydrologyTileKey(2, 0));
} finally {
caller.shutdownNow();
}
}
@Test
public void clearDiscardsOldDemandPauseAndSpeculativeQueue() throws Exception {
assertDemandPrefetchInvalidation(false);
}
@Test
public void closePreventsOldDemandFromResumingPrefetch() throws Exception {
assertDemandPrefetchInvalidation(true);
}
private void assertDemandPrefetchInvalidation(boolean close) throws Exception {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
HydrologyTile tile = mock(HydrologyTile.class);
when(planner.settings()).thenReturn(emptySettings());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(tile);
LinkedBlockingQueue<Runnable> queued = new LinkedBlockingQueue<>();
HydrologyTileCache cache = new HydrologyTileCache(planner, 64, queued::add);
int tileSize = emptySettings().routing().tileSize();
cache.prefetchArea(0, 0, tileSize * 3 - 1, 0, 0, 0);
Runnable initialPrefetch = queued.poll(5, TimeUnit.SECONDS);
assertNotNull(initialPrefetch);
ExecutorService caller = Executors.newSingleThreadExecutor();
try {
Future<HydrologyTile> result = caller.submit(() -> cache.tiles(List.of(new HydrologyTileKey(1, 0))).getFirst());
Runnable oldDemand = queued.poll(5, TimeUnit.SECONDS);
assertNotNull(oldDemand);
initialPrefetch.run();
assertTrue(queued.isEmpty());
if (close) {
cache.close();
} else {
cache.clear();
}
cache.prefetchArea(tileSize * 10, 0, tileSize * 10, 0, tileSize * 10, 0);
if (!close) {
Runnable freshPrefetch = queued.poll(5, TimeUnit.SECONDS);
assertNotNull(freshPrefetch);
freshPrefetch.run();
}
oldDemand.run();
assertSame(tile, result.get(5, TimeUnit.SECONDS));
assertTrue(queued.isEmpty());
assertEquals(close ? 0 : 1, cache.size());
verify(planner, never()).plan(new HydrologyTileKey(2, 0));
} finally {
caller.shutdownNow();
}
}
@Test
public void plannedCheckNeverPlansItselfButAsksThePrefetchExecutorForMissingTiles() {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
@@ -927,4 +1534,5 @@ public class HydrologyTileCacheTest {
verify(planner, org.mockito.Mockito.atLeastOnce()).plan(any(HydrologyTileKey.class));
assertTrue(cache.isPlanned(40, 40));
}
}
@@ -0,0 +1,211 @@
package art.arcane.iris.engine.mantle.components;
import org.junit.Test;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class CaveDensityTasksTest {
@Test
public void saturatedParentsClaimQueuedWorkWithoutHelpingOtherLocks() throws Exception {
try (ForkJoinPool pool = new ForkJoinPool(2)) {
CountDownLatch parents = new CountDownLatch(2);
AtomicInteger executions = new AtomicInteger();
Runnable parent = () -> {
synchronized (new Object()) {
parents.countDown();
await(parents);
IrisCaveCarver3D.runDensityTasks(List.of(
executions::incrementAndGet, executions::incrementAndGet,
executions::incrementAndGet, executions::incrementAndGet), pool);
}
};
Future<?> first = pool.submit(parent);
Future<?> second = pool.submit(parent);
first.get(5, TimeUnit.SECONDS);
second.get(5, TimeUnit.SECONDS);
assertEquals(8, executions.get());
}
}
@Test
public void failedCallerDrainsStartedWorkerBeforeThrowing() throws Exception {
try (ForkJoinPool pool = new ForkJoinPool(2)) {
CountDownLatch started = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
CountDownLatch failed = new CountDownLatch(1);
AtomicInteger completed = new AtomicInteger();
IllegalStateException failure = new IllegalStateException("density failure");
Future<?> parent = pool.submit(() -> IrisCaveCarver3D.runDensityTasks(List.of(() -> {
await(started);
failed.countDown();
throw failure;
}, () -> {
started.countDown();
await(release);
completed.incrementAndGet();
}), pool));
try {
assertTrue(failed.await(5, TimeUnit.SECONDS));
assertFalse(parent.isDone());
} finally {
release.countDown();
}
try {
parent.get(5, TimeUnit.SECONDS);
fail("Expected density failure");
} catch (ExecutionException expected) {
Throwable cause = expected.getCause();
assertTrue(cause == failure || cause.getCause() == failure);
}
assertEquals(1, completed.get());
}
}
@Test
public void interruptionDrainsWorkerAndRestoresInterruptStatus() throws Exception {
try (ForkJoinPool pool = new ForkJoinPool(2)) {
CountDownLatch started = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
CountDownLatch awaiting = new CountDownLatch(1);
AtomicReference<Thread> parentThread = new AtomicReference<>();
AtomicInteger completed = new AtomicInteger();
Future<Boolean> parent = pool.submit(() -> {
parentThread.set(Thread.currentThread());
try {
IrisCaveCarver3D.runDensityTasks(List.of(() -> {
await(started);
awaiting.countDown();
}, () -> {
started.countDown();
await(release);
completed.incrementAndGet();
}), pool);
return false;
} catch (IllegalStateException expected) {
return Thread.currentThread().isInterrupted();
}
});
try {
assertTrue(awaiting.await(5, TimeUnit.SECONDS));
parentThread.get().interrupt();
assertFalse(parent.isDone());
} finally {
release.countDown();
}
assertTrue(parent.get(5, TimeUnit.SECONDS));
assertEquals(1, completed.get());
}
}
@Test
public void rejectedSubmissionClaimsAllWorkBeforeFailureReturns() {
ForkJoinPool pool = new ForkJoinPool(2);
pool.shutdown();
AtomicInteger completed = new AtomicInteger();
try {
IrisCaveCarver3D.runDensityTasks(List.of(completed::incrementAndGet, completed::incrementAndGet), pool);
fail("Expected rejected submission");
} catch (RejectedExecutionException expected) {
assertEquals(2, completed.get());
}
}
@Test
public void managedDrainCompensatesWithoutRunningUnrelatedWorkOnParent() throws Exception {
try (ForkJoinPool pool = new ForkJoinPool(2, ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, false, 2, 3, 2, null, 60, TimeUnit.SECONDS)) {
CountDownLatch started = new CountDownLatch(1);
CountDownLatch awaiting = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
AtomicReference<Thread> owner = new AtomicReference<>();
Object chunkFlag = new Object();
Future<?> parent = pool.submit(() -> {
synchronized (chunkFlag) {
owner.set(Thread.currentThread());
IrisCaveCarver3D.runDensityTasks(List.of(() -> {
await(started);
awaiting.countDown();
}, () -> {
started.countDown();
await(release);
}), pool);
}
});
try {
assertTrue(awaiting.await(5, TimeUnit.SECONDS));
Thread other = pool.submit(Thread::currentThread).get(5, TimeUnit.SECONDS);
assertFalse(other == owner.get());
assertFalse(parent.isDone());
} finally {
release.countDown();
}
parent.get(5, TimeUnit.SECONDS);
}
}
@Test
public void unavailableCompensationStillDrainsStartedWork() throws Exception {
try (ForkJoinPool pool = new ForkJoinPool(2, ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, false, 2, 2, 2, null, 60, TimeUnit.SECONDS)) {
CountDownLatch started = new CountDownLatch(1);
CountDownLatch awaiting = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
AtomicInteger completed = new AtomicInteger();
Future<?> parent = pool.submit(() -> IrisCaveCarver3D.runDensityTasks(List.of(() -> {
await(started);
awaiting.countDown();
}, () -> {
started.countDown();
await(release);
completed.incrementAndGet();
}), pool));
try {
assertTrue(awaiting.await(5, TimeUnit.SECONDS));
assertFalse(parent.isDone());
} finally {
release.countDown();
}
parent.get(5, TimeUnit.SECONDS);
assertEquals(1, completed.get());
}
}
@Test
public void completedTasksStillPropagateAnExistingInterrupt() {
try (ForkJoinPool pool = new ForkJoinPool(1)) {
Thread.currentThread().interrupt();
try {
IrisCaveCarver3D.runDensityTasks(List.of(() -> {}), pool);
fail("Expected interrupted density batch");
} catch (IllegalStateException expected) {
assertTrue(Thread.currentThread().isInterrupted());
} finally {
Thread.interrupted();
}
}
}
private static void await(CountDownLatch latch) {
try {
if (!latch.await(5, TimeUnit.SECONDS)) {
throw new AssertionError("Timed out waiting for density task");
}
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new AssertionError(failure);
}
}
}
@@ -34,6 +34,9 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -41,6 +44,8 @@ import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -108,6 +113,157 @@ public class IrisCaveCarver3DNearParityTest {
carveForcedAirField.setAccessible(true);
}
@Test
public void parallelDensityPreservesExactAndAdaptiveCavernsAndLiquids() throws Exception {
try (ForkJoinPool pool = new ForkJoinPool(4)) {
for (boolean adaptive : new boolean[]{false, true}) {
for (boolean warp : new boolean[]{false, true}) {
for (boolean modules : new boolean[]{false, true}) {
Engine engine = createEngine(128, 110);
IrisCaveProfile profile = createProfile(warp, modules).setAdaptiveSampling(adaptive)
.setDensityThreshold(new IrisStyledRange(0.08D, 0.08D, new IrisGeneratorStyle(NoiseStyle.FLAT)))
.setFluidRequiresFloor(true);
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile);
WriterCapture serial = createWriterCapture(128);
WriterCapture parallel = createWriterCapture(128);
int[] heights = filledHeights(110);
double[] weights = fullWeights();
long[] boundaries = new long[256];
Arrays.fill(boundaries, SurfaceFluidBoundaryPlan.boundary(
SurfaceFluidBoundaryPlan.NO_BOUNDARY, Integer.MIN_VALUE));
for (int column = 0; column < 256; column += 11) {
heights[column] = 80;
weights[column] = column % 3 == 0 ? 0D : 0.6D;
boundaries[column] = SurfaceFluidBoundaryPlan.boundary(56, 64);
}
int expected = carver.carve(serial.writer, -17, 19, weights, 0.1D, 0.15D,
null, heights, boundaries, null, new CaveFluidSupportPlan());
int actual = pool.submit(() -> carver.carve(parallel.writer, -17, 19, weights, 0.1D, 0.15D,
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);
}
}
}
}
}
@Test
public void rejectedParallelVoxelsDoNotSampleAquifers() throws Exception {
Engine engine = createEngine(128, 110);
IrisCaveProfile profile = createProfile(false, false).setAdaptiveSampling(false)
.setBaseDensityStyle(new IrisGeneratorStyle(NoiseStyle.FLAT))
.setDetailWeight(0D)
.setDensityThreshold(new IrisStyledRange(-2D, -2D, new IrisGeneratorStyle(NoiseStyle.FLAT)));
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile);
CNG detail = mock(CNG.class);
doAnswer(invocation -> {
throw new AssertionError("Rejected voxel sampled an aquifer");
}).when(detail).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
Field detailField = IrisCaveCarver3D.class.getDeclaredField("detailDensity");
detailField.setAccessible(true);
detailField.set(carver, detail);
WriterCapture capture = createWriterCapture(128);
try (ForkJoinPool pool = new ForkJoinPool(4)) {
assertEquals(0, pool.submit(() -> carver.carve(capture.writer, 0, 0, fullWeights(),
0D, 0D, null, filledHeights(110))).get(20, TimeUnit.SECONDS).intValue());
}
assertTrue(capture.carvedCells.isEmpty());
}
@Test
public void parallelMaterialDecisionsPreserveLavaFluidAndForcedAir() throws Exception {
try (ForkJoinPool pool = new ForkJoinPool(4)) {
for (boolean lava : new boolean[]{false, true}) {
Engine engine = createEngine(128, 110);
IrisCaveProfile profile = createProfile(true, true).setAdaptiveSampling(true)
.setAllowLava(lava).setFluidRequiresFloor(false)
.setDensityThreshold(new IrisStyledRange(2D, 2D, new IrisGeneratorStyle(NoiseStyle.FLAT)));
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile);
WriterCapture serial = createWriterCapture(128);
WriterCapture parallel = createWriterCapture(128);
int[] heights = filledHeights(110);
int expected = carver.carve(serial.writer, 7, -3, fullWeights(), 0D, 0D, null, heights);
int actual = pool.submit(() -> carver.carve(parallel.writer, 7, -3, fullWeights(), 0D, 0D,
null, heights)).get(20, TimeUnit.SECONDS);
assertEquals(expected, actual);
assertEquals(serial.carvedCells, parallel.carvedCells);
assertEquals(serial.carvedLiquids, parallel.carvedLiquids);
assertTrue(countLiquid(serial, (byte) 0) > 0);
assertTrue(countLiquid(serial, (byte) 1) > 0);
assertTrue(countLiquid(serial, lava ? (byte) 2 : (byte) 3) > 0);
}
}
}
@Test
public void parallelLatticePreservesCavernsAndResolvedFluidSupport() throws Exception {
try (ForkJoinPool pool = new ForkJoinPool(4)) {
for (int step : new int[]{3, 5}) {
Engine engine = createEngine(384, 360);
IrisCaveProfile profile = createProfile(true, true).setSampleStep(step)
.setVerticalRange(new IrisRange(6, 350))
.setDensityThreshold(new IrisStyledRange(0.08D, 0.08D, new IrisGeneratorStyle(NoiseStyle.FLAT)))
.setFluidRequiresFloor(true);
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile);
WriterCapture serial = createWriterCapture(384);
WriterCapture parallel = createWriterCapture(384);
int[] heights = filledHeights(340);
double[] weights = fullWeights();
for (int column = 0; column < 256; column += 7) {
heights[column] = 80;
weights[column] = column % 3 == 0 ? 0D : 0.6D;
}
int expected = carver.carve(serial.writer, 9, -7, weights, 0.1D, 0.15D, null, heights);
int actual = pool.submit(() -> carver.carve(parallel.writer, 9, -7, weights, 0.1D, 0.15D,
null, heights)).get(20, TimeUnit.SECONDS);
assertTrue(expected > 0);
assertEquals(expected, actual);
assertEquals(serial.carvedCells, parallel.carvedCells);
assertEquals(serial.carvedLiquids, parallel.carvedLiquids);
}
}
}
@Test
public void expressionsInDensityFracturesDisableParallelClassification() throws Exception {
Method fixedStyles = IrisCaveCarver3D.class.getDeclaredMethod("fixedDensityStyles", IrisCaveProfile.class);
fixedStyles.setAccessible(true);
IrisCaveProfile profile = createProfile(true, true);
assertEquals(true, fixedStyles.invoke(null, profile));
profile.getWarpStyle().setFracture(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).setExpression("live-height"));
assertEquals(false, fixedStyles.invoke(null, profile));
profile = createProfile(true, true);
profile.getModules().get(0).getStyle().setExpression("live-height");
assertEquals(false, fixedStyles.invoke(null, profile));
}
@Test
public void nestedCarvingPreservesOuterScratch() {
Engine engine = createEngine(128, 110);
doReturn(110).when(engine).getHeight(anyInt(), anyInt(), anyBoolean());
IrisCaveProfile profile = createProfile(true, true).setAdaptiveSampling(false);
WriterCapture expected = createWriterCapture(128);
new IrisCaveCarver3D(engine, profile).carve(expected.writer, 2, 3);
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile);
WriterCapture nested = createWriterCapture(128);
AtomicBoolean entered = new AtomicBoolean();
doAnswer(invocation -> {
if (entered.compareAndSet(false, true)) {
carver.carve(nested.writer, -9, -11, fullWeights(), 0D, 0D, null, filledHeights(80));
}
return 110;
}).when(engine).getHeight(anyInt(), anyInt(), anyBoolean());
WriterCapture actual = createWriterCapture(128);
carver.carve(actual.writer, 2, 3);
assertTrue(entered.get());
assertFalse(nested.carvedCells.isEmpty());
assertEquals(expected.carvedCells, actual.carvedCells);
assertEquals(expected.carvedLiquids, actual.carvedLiquids);
}
@Test
public void genericFluidContractKeepsOverworldDefaults() {
IrisCaveProfile profile = new IrisCaveProfile();
@@ -2,6 +2,9 @@ package art.arcane.iris.engine.object;
import org.junit.Test;
import java.util.Locale;
import java.util.Random;
import static org.junit.Assert.assertEquals;
public class IrisDimensionTypeKeyTest {
@@ -36,4 +39,65 @@ public class IrisDimensionTypeKeyTest {
assertEquals("layers:sky/aurora", dimension.getCustomBiomeKey("Aurora"));
}
@Test
public void customBiomeKeysPreserveSanitizerBoundaryBehavior() {
String[] values = {null, "", " ", "\t\n", "a", "0", "_", "-", ".", "..", "...", "....",
"/", "//", "/a/", "a//b", "a/./b", "a/../b", "a..b", "a...b", "a\\b", " a/b ",
"A_Z-9.ext/path", "İ", "ΣΟΣ", "K", "ß", "é", "a\u2003b", "\u2003", "\u00a0",
"\u0000a\u001f", "a\u0000b", "\ud83d\ude00", "\ud800", "\udc00"};
for (String dimension : values) {
for (String biome : values) {
assertEquals(legacyCustomBiomeKey(dimension, biome), IrisDimension.customBiomeKey(dimension, biome));
}
}
}
@Test
public void customBiomeKeysMatchPriorSanitizerForMixedUnicodeInputs() {
Random random = new Random(7331L);
String alphabet = "abcdefghijklmnopqrstuvwxyz0123456789_-.//\\ ABCXYZ\t\nİΣKé\u2003\u0000\ud83d\ude00";
for (int sample = 0; sample < 3000; sample++) {
String dimension = randomValue(random, alphabet);
String biome = randomValue(random, alphabet);
assertEquals(legacyCustomBiomeKey(dimension, biome), IrisDimension.customBiomeKey(dimension, biome));
}
}
private static String randomValue(Random random, String alphabet) {
int length = random.nextInt(65);
StringBuilder value = new StringBuilder(length);
for (int i = 0; i < length; i++) {
value.append(random.nextInt(8) == 0 ? (char) random.nextInt(65536)
: alphabet.charAt(random.nextInt(alphabet.length())));
}
return value.toString();
}
private static String legacyCustomBiomeKey(String dimension, String biome) {
String dimensionPath = legacyRegistryPath(dimension, "dimension");
int separator = dimensionPath.indexOf('/');
String namespace = separator < 0 ? dimensionPath : dimensionPath.substring(0, separator);
namespace = namespace.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9_.-]", "_");
if (namespace.isBlank()) {
namespace = "dimension";
}
return namespace + ":" + (separator < 0 ? "" : dimensionPath.substring(separator + 1) + "/")
+ legacyRegistryPath(biome, "biome");
}
private static String legacyRegistryPath(String value, String fallback) {
if (value == null || value.isBlank()) {
return fallback;
}
String sanitized = value.trim().toLowerCase(Locale.ROOT).replace("\\", "/");
sanitized = sanitized.replaceAll("[^a-z0-9/._-]", "_");
sanitized = sanitized.replaceAll("/+", "/");
sanitized = sanitized.replaceAll("^/+", "");
sanitized = sanitized.replaceAll("/+$", "");
while (sanitized.contains("..")) {
sanitized = sanitized.replace("..", "_");
}
return sanitized.isBlank() ? fallback : sanitized;
}
}
@@ -1,19 +1,119 @@
package art.arcane.iris.engine.platform;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.hydrology.runtime.IrisHydrologyRuntime;
import org.bukkit.World;
import art.arcane.iris.engine.object.StudioMode;
import art.arcane.iris.engine.object.IrisDimension;
import org.junit.Test;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
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.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static org.mockito.ArgumentMatchers.anyInt;
public class BukkitChunkGeneratorInitializationModeTest {
@Test
public void normalStudioDemandsItsActualEntryAndAuthoringDoesNotPlanTerrain() throws Exception {
BukkitChunkGenerator generator = mock(BukkitChunkGenerator.class, CALLS_REAL_METHODS);
Field studio = BukkitChunkGenerator.class.getDeclaredField("studio");
studio.setAccessible(true);
studio.setBoolean(generator, true);
doReturn(false).when(generator).usesFlatStudioTerrain();
World world = mock(World.class);
when(world.getMinHeight()).thenReturn(-64);
when(world.getMaxHeight()).thenReturn(320);
IrisEngine engine = mock(IrisEngine.class);
IrisDimension dimension = mock(IrisDimension.class);
when(engine.getDimension()).thenReturn(dimension);
when(dimension.getStudioMode()).thenReturn(StudioMode.NORMAL);
IrisComplex complex = mock(IrisComplex.class);
IrisHydrologyRuntime hydrology = mock(IrisHydrologyRuntime.class);
when(engine.getComplex()).thenReturn(complex);
when(complex.getHydrologyRuntime()).thenReturn(hydrology);
Method prefetch = BukkitChunkGenerator.class.getDeclaredMethod(
"prefetchSpawnHydrology", Engine.class, World.class);
prefetch.setAccessible(true);
prefetch.invoke(generator, engine, world);
verify(engine).startStudioEntryHydrology(0, 0);
verifyNoInteractions(hydrology);
assertEquals(0.5D, generator.getInitialSpawnLocation(world).getX(), 0D);
assertEquals(96D, generator.getInitialSpawnLocation(world).getY(), 0D);
assertEquals(0.5D, generator.getInitialSpawnLocation(world).getZ(), 0D);
when(dimension.getStudioMode()).thenReturn(StudioMode.BIOME_BUFFET_1x1);
prefetch.invoke(generator, engine, world);
verify(engine, times(1)).startStudioEntryHydrology(0, 0);
verify(hydrology).prefetchArea(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt());
when(dimension.getStudioMode()).thenReturn(StudioMode.NORMAL);
IrisEngine authoring = mock(IrisEngine.class);
doReturn(true).when(generator).usesFlatStudioTerrain();
prefetch.invoke(generator, authoring, world);
verifyNoInteractions(authoring);
IrisEngine generated = mock(IrisEngine.class);
when(generated.getComplex()).thenReturn(complex);
doReturn(false).when(generator).usesFlatStudioTerrain();
when(world.isChunkGenerated(0, 0)).thenReturn(true);
prefetch.invoke(generator, generated, world);
verify(generated, never()).startStudioEntryHydrology(anyInt(), anyInt());
}
@Test
public void standardStudioTerrainModeTracksHotloadedDimension() {
assertFalse(BukkitChunkGenerator.usesFlatStudioTerrain(true, false, StudioMode.NORMAL));
assertTrue(BukkitChunkGenerator.usesFlatStudioTerrain(true, false, StudioMode.OBJECT_BUFFET));
assertFalse(BukkitChunkGenerator.usesFlatStudioTerrain(true, false, StudioMode.NORMAL));
assertTrue(BukkitChunkGenerator.usesFlatStudioTerrain(true, true, StudioMode.NORMAL));
assertFalse(BukkitChunkGenerator.usesFlatStudioTerrain(false, false, StudioMode.OBJECT_BUFFET));
}
@Test
public void authoringBaseHeightMatchesTheFloorAndEmptyShortWorlds() {
assertEquals(65, BukkitChunkGenerator.authoringBaseHeight(-64, 320));
assertEquals(129, BukkitChunkGenerator.authoringBaseHeight(128, 256));
assertEquals(-64, BukkitChunkGenerator.authoringBaseHeight(-64, 64));
assertEquals(-64, BukkitChunkGenerator.authoringBaseHeight(-64, 0));
}
@Test
public void authoringStartupDoesNotPrefetchTerrainHydrology() throws IOException {
String source = Files.readString(Path.of(
"src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java"));
int start = source.indexOf("private void prefetchSpawnHydrology(");
int end = source.indexOf("private int hydrologyTileSize()", start);
String prefetch = source.substring(start, end);
assertTrue(prefetch.indexOf("if (usesFlatStudioTerrain())") >= 0);
assertTrue(prefetch.indexOf("if (usesFlatStudioTerrain())")
< prefetch.indexOf("engine.getComplex()"));
}
@Test
public void runtimeAndOrdinaryStudioWarmGenerationCaches() {
IrisEngine.InitializationMode runtime =
BukkitChunkGenerator.selectInitializationMode(false, false);
BukkitChunkGenerator.selectInitializationMode(false, false, false);
IrisEngine.InitializationMode studio =
BukkitChunkGenerator.selectInitializationMode(true, false);
BukkitChunkGenerator.selectInitializationMode(true, false, false);
assertEquals(IrisEngine.InitializationMode.RUNTIME, runtime);
assertFalse(runtime.studio());
@@ -26,13 +126,25 @@ public class BukkitChunkGeneratorInitializationModeTest {
@Test
public void activeJigsawStudioSkipsGenerationCacheWarm() {
IrisEngine.InitializationMode mode =
BukkitChunkGenerator.selectInitializationMode(true, true);
BukkitChunkGenerator.selectInitializationMode(true, true, false);
assertEquals(IrisEngine.InitializationMode.JIGSAW_STUDIO, mode);
assertTrue(mode.studio());
assertFalse(mode.warmGenerationCaches());
}
@Test
public void activeObjectStudioSkipsGenerationCacheWarm() {
IrisEngine.InitializationMode mode =
BukkitChunkGenerator.selectInitializationMode(true, false, true);
assertEquals(IrisEngine.InitializationMode.OBJECT_STUDIO, mode);
assertTrue(mode.studio());
assertFalse(mode.warmGenerationCaches());
assertEquals(IrisEngine.InitializationMode.RUNTIME,
BukkitChunkGenerator.selectInitializationMode(false, false, true));
}
@Test
public void jigsawBootstrapAndInitializationFailureSkipNativeStructureGeneration() {
assertFalse(BukkitChunkGenerator.shouldGenerateNativeStructures(true, false, false));
@@ -58,6 +58,15 @@ import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class JigsawStudioGeneratorTest {
@Test
public void checkerboardFloorUsesTheSameFourBlockTilesAtNegativeCoordinates() {
assertTrue(JigsawStudioGenerator.isLightFloor(0, 0));
assertTrue(JigsawStudioGenerator.isLightFloor(3, 3));
assertFalse(JigsawStudioGenerator.isLightFloor(4, 0));
assertFalse(JigsawStudioGenerator.isLightFloor(-1, 0));
assertTrue(JigsawStudioGenerator.isLightFloor(-1, -1));
}
@Test
@SuppressWarnings("unchecked")
public void connectorBlocksAreHiddenByDefaultAndCanBeShownPerWorkcell() {
@@ -0,0 +1,121 @@
package art.arcane.iris.util.project.interpolation;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
public class IrisInterpolationBoundsTest {
@Test
public void everyMethodPreservesSeparatePassValuesAndSamplesEachCoordinateOnce() {
Random random = new Random(19053);
for (InterpolationMethod method : InterpolationMethod.values()) {
for (double scale : new double[]{1D, 7D, 12D, 23D, 200D, 7.75D, 8192D}) {
for (int index = 0; index < 24; index++) {
int x = random.nextInt();
int z = random.nextInt();
Map<Coordinate, NoiseBounds> expectedSamples = new HashMap<>();
NoiseBounds expected = separatePasses(method, x, z, scale,
IrisInterpolationBoundsTest::asymmetricBounds, expectedSamples);
Map<Coordinate, Integer> actualSamples = new HashMap<>();
NoiseBounds actual = IrisInterpolation.getNoiseBounds(method, x, z, scale, (sx, sz) -> {
actualSamples.merge(new Coordinate(sx, sz), 1, Integer::sum);
return asymmetricBounds(sx, sz);
});
assertBounds(expected, actual);
assertEquals(expectedSamples.keySet(), actualSamples.keySet());
for (int calls : actualSamples.values()) {
assertEquals(1, calls);
}
}
}
}
}
@Test
public void nestedProvidersKeepTheirOwnSamplingSequences() {
for (InterpolationMethod method : InterpolationMethod.values()) {
NoiseBoundsProvider expectedProvider = (x, z) -> separatePasses(
InterpolationMethod.HERMITE_STARCAST_12, (int) x + 103, (int) z - 79, 13.5D,
IrisInterpolationBoundsTest::asymmetricBounds, new HashMap<>());
NoiseBoundsProvider actualProvider = (x, z) -> IrisInterpolation.getNoiseBounds(
InterpolationMethod.HERMITE_STARCAST_12, (int) x + 103, (int) z - 79, 13.5D,
IrisInterpolationBoundsTest::asymmetricBounds);
NoiseBounds expected = separatePasses(method, -93, 201, 23D, expectedProvider, new HashMap<>());
NoiseBounds actual = IrisInterpolation.getNoiseBounds(method, -93, 201, 23D, actualProvider);
assertBounds(expected, actual);
}
}
@Test
public void nestedScalarProvidersPreserveTheirOriginalCallbackOrder() {
for (InterpolationMethod method : InterpolationMethod.values()) {
AtomicInteger expectedCalls = new AtomicInteger();
AtomicInteger actualCalls = new AtomicInteger();
NoiseBoundsProvider expectedProvider = (x, z) -> scalarBounds(x, z, expectedCalls);
NoiseBoundsProvider actualProvider = (x, z) -> scalarBounds(x, z, actualCalls);
NoiseBounds expected = separatePasses(method, -93, 201, 23D, expectedProvider, new HashMap<>());
NoiseBounds actual = IrisInterpolation.getNoiseBounds(method, -93, 201, 23D, actualProvider);
assertBounds(expected, actual);
assertEquals(expectedCalls.get(), actualCalls.get());
}
}
private static NoiseBounds scalarBounds(double x, double z, AtomicInteger calls) {
double value = IrisInterpolation.getNoise(InterpolationMethod.HERMITE_STARCAST_12,
(int) x + 103, (int) z - 79, 13.5D, (sx, sz) -> calls.incrementAndGet());
return new NoiseBounds(value, value + 17D);
}
@Test
public void failedProviderDoesNotLeavePartialSamplesForTheNextPass() {
AtomicInteger calls = new AtomicInteger();
IllegalStateException failure = new IllegalStateException("provider failure");
try {
IrisInterpolation.getNoiseBounds(InterpolationMethod.HERMITE_STARCAST_12, 71, -209, 7D,
(x, z) -> {
if (calls.incrementAndGet() == 5) {
throw failure;
}
return asymmetricBounds(x, z);
});
fail("Expected provider failure");
} catch (IllegalStateException expected) {
assertEquals(failure, expected);
}
NoiseBounds expected = separatePasses(InterpolationMethod.HERMITE_STARCAST_12, 71, -209, 7D,
IrisInterpolationBoundsTest::asymmetricBounds, new HashMap<>());
NoiseBounds actual = IrisInterpolation.getNoiseBounds(InterpolationMethod.HERMITE_STARCAST_12,
71, -209, 7D, IrisInterpolationBoundsTest::asymmetricBounds);
assertBounds(expected, actual);
}
private static NoiseBounds separatePasses(InterpolationMethod method, int x, int z, double scale,
NoiseBoundsProvider provider, Map<Coordinate, NoiseBounds> samples) {
double minimum = IrisInterpolation.getNoise(method, x, z, scale,
(sx, sz) -> samples.computeIfAbsent(new Coordinate(sx, sz),
coordinate -> provider.noise(sx, sz)).min());
double maximum = IrisInterpolation.getNoise(method, x, z, scale,
(sx, sz) -> samples.computeIfAbsent(new Coordinate(sx, sz),
coordinate -> provider.noise(sx, sz)).max());
return new NoiseBounds(minimum, maximum);
}
private static NoiseBounds asymmetricBounds(double x, double z) {
double minimum = StrictMath.sin(x * 0.0031D) * 35D - StrictMath.cos(z * 0.0087D) * 7D;
return new NoiseBounds(minimum, minimum + 17D + StrictMath.cos(x * 0.0123D - z * 0.019D) * 11D);
}
private static void assertBounds(NoiseBounds expected, NoiseBounds actual) {
assertEquals(Double.doubleToLongBits(expected.min()), Double.doubleToLongBits(actual.min()));
assertEquals(Double.doubleToLongBits(expected.max()), Double.doubleToLongBits(actual.max()));
}
private record Coordinate(double x, double z) {
}
}