From 0ee0f62b9d2786933e8e711b2c24c10dc4baa430 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Mon, 28 Sep 2026 13:58:58 -0400 Subject: [PATCH] Limit early owner drafts in pregen - Pregeneration scope limits the lower-rank neighbour drafts an owner starts early to tiles the area composes from - Undemanded ring drafts (and the early neighbours they started) are no longer computed; demanded ones still resolve on demand - r2000 tile set: 80 -> 54 drafts, planning CPU -32%, plans identical --- .../hydrology/HydrologyPlanner.java | 15 ++++++- .../hydrology/HydrologyTileCache.java | 13 +++++- .../hydrology/HydrologyPlannerTest.java | 42 +++++++++++++++++++ .../hydrology/HydrologyTileCacheTest.java | 25 +++++++++++ 4 files changed, 92 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyPlanner.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyPlanner.java index 042ca17b1..aad0c461b 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyPlanner.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyPlanner.java @@ -28,6 +28,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ForkJoinWorkerThread; +import java.util.function.Predicate; public final class HydrologyPlanner { static final int ROUTING_CONTEXT_CACHE_SIZE = 64; @@ -64,6 +65,7 @@ public final class HydrologyPlanner { final HydrologySegmentBuilder segments; final HydrologyFeatureSitePlanner featureSites; final HydrologyRegionalPlanner regional; + private volatile Predicate earlyOwnerScope; public HydrologyPlanner(long worldSeed, HydrologyPlannerSettings settings, HydrologyTerrainSampler sampler) { this( @@ -244,6 +246,15 @@ public final class HydrologyPlanner { return List.copyOf(unique); } + /** + * Limits the lower-rank neighbour drafts an owner starts before its admission demands them to the + * tiles the scope accepts; null starts all of them. A neighbour that no admission demands and + * nothing plans is a draft computed for nothing, and so are the early neighbours it starts itself. + */ + void limitEarlyOwners(Predicate scope) { + earlyOwnerScope = scope; + } + void clearOwnerDrafts() { resolvedOwners.invalidateAll(); regional.clear(); @@ -751,6 +762,7 @@ public final class HydrologyPlanner { return; } int radius = settings.crossTileColorPeriod() - 1; + Predicate scope = earlyOwnerScope; ArrayList 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++) { @@ -759,7 +771,8 @@ public final class HydrologyPlanner { continue; } HydrologyTileKey key = new HydrologyTileKey((int) tileX, (int) tileZ); - if (crossTile.ownerColorRank(key) >= ownerRank || preparedOwners.containsKey(key) + if (crossTile.ownerColorRank(key) >= ownerRank || scope != null && !scope.test(key) + || preparedOwners.containsKey(key) || resolvedOwners.getIfPresent(key) != null || resolvingOwners.containsKey(key)) { continue; } diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyTileCache.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyTileCache.java index 6429e5cd7..865614566 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyTileCache.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyTileCache.java @@ -690,7 +690,7 @@ public final class HydrologyTileCache implements AutoCloseable { tileCoordinate(Math.addExact(area.maximumBlockZ(), publicationRadius), tileSize)); List keys = nearestFirst(required, tileCoordinate(area.centerBlockX(), tileSize), tileCoordinate(area.centerBlockZ(), tileSize), MAXIMUM_PREGENERATION_TILES); - PregenerationScope scope = new PregenerationScope(bounds); + PregenerationScope scope = new PregenerationScope(bounds, required); try { enqueuePrefetchArea(keys, scope); return scope; @@ -1082,6 +1082,7 @@ public final class HydrologyTileCache implements AutoCloseable { scope.previousNeighbourPrefetch = pregenerationScope == null ? neighbourPrefetchEnabled : pregenerationScope.previousNeighbourPrefetch; pregenerationScope = scope; + planner.limitEarlyOwners(scope.required::contains); prefetchQueue.removeIf(key -> { if (!area.contains(key)) { queuedPrefetches.remove(key); @@ -1619,12 +1620,19 @@ public final class HydrologyTileCache implements AutoCloseable { } } + /** + * Speculative planning during a pregeneration: tiles inside {@code bounds} may be planned ahead of + * generation, and owner drafts start their lower-rank neighbours early only inside {@code required}, + * the tiles the area composes from. + */ public final class PregenerationScope implements AutoCloseable { private final TileBounds bounds; + private final TileBounds required; private boolean previousNeighbourPrefetch; - private PregenerationScope(TileBounds bounds) { + private PregenerationScope(TileBounds bounds, TileBounds required) { this.bounds = bounds; + this.required = required; } @Override @@ -1634,6 +1642,7 @@ public final class HydrologyTileCache implements AutoCloseable { return; } pregenerationScope = null; + planner.limitEarlyOwners(null); neighbourPrefetchEnabled = previousNeighbourPrefetch; prefetchQueue.clear(); queuedPrefetches.clear(); diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyPlannerTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyPlannerTest.java index 17fd5d818..a9fafc889 100644 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyPlannerTest.java +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyPlannerTest.java @@ -381,6 +381,48 @@ public class HydrologyPlannerTest { } } + @Test + public void earlyOwnersStayInsideTheirScope() throws Exception { + org.junit.Assume.assumeTrue(Runtime.getRuntime().availableProcessors() > 2); + HydrologyPlannerSettings settings = withPeriodTwo(standardSettings(4D, 2D, true, false, List.of())); + HydrologyPlanner planner = new HydrologyPlanner(77L, settings, (x, z) -> blockedTerrain()); + planner.limitEarlyOwners(key -> key.tileX() >= -1); + AutoCloseable admission = earlyAdmission(planner, new HydrologyTileKey(-1, -1)); + Map> tasks = earlyTasks(admission); + Method prepare = admission.getClass().getDeclaredMethod("prepare"); + prepare.setAccessible(true); + CapturingHydrologyPool pool = new CapturingHydrologyPool(); + try { + pool.submit(() -> { + prepare.invoke(admission); + return null; + }).get(5, TimeUnit.SECONDS); + assertEquals(List.of(new HydrologyTileKey(0, -1), + new HydrologyTileKey(-1, -2), new HydrologyTileKey(-1, 0), + new HydrologyTileKey(0, -2), new HydrologyTileKey(0, 0)), new ArrayList<>(tasks.keySet())); + assertEquals(new ArrayList<>(tasks.values()), pool.submitted); + } finally { + tasks.clear(); + admission.close(); + pool.shutdownNow(); + } + planner.limitEarlyOwners(null); + AutoCloseable unlimited = earlyAdmission(planner, new HydrologyTileKey(-1, -1)); + Map> unlimitedTasks = earlyTasks(unlimited); + CapturingHydrologyPool unlimitedPool = new CapturingHydrologyPool(); + try { + unlimitedPool.submit(() -> { + prepare.invoke(unlimited); + return null; + }).get(5, TimeUnit.SECONDS); + assertEquals(8, unlimitedTasks.size()); + } finally { + unlimitedTasks.clear(); + unlimited.close(); + unlimitedPool.shutdownNow(); + } + } + private static final class CapturingHydrologyPool extends ForkJoinPool { private final ArrayList submitted = new ArrayList<>(); diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyTileCacheTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyTileCacheTest.java index 01996c70b..9c72114fc 100644 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyTileCacheTest.java +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyTileCacheTest.java @@ -38,6 +38,8 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; +import org.mockito.ArgumentCaptor; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -590,6 +592,29 @@ public class HydrologyTileCacheTest { assertEquals(16, planned.size()); } + @Test + @SuppressWarnings("unchecked") + public void pregenScopeLimitsEarlyOwnersToTheTilesTheAreaComposesFrom() { + HydrologyPlanner planner = mock(HydrologyPlanner.class); + HydrologyPlannerSettings settings = stalePrefetchSettings(); + when(planner.settings()).thenReturn(settings); + when(planner.plan(any(HydrologyTileKey.class))).thenReturn(mock(HydrologyTile.class)); + HydrologyTileCache cache = new HydrologyTileCache(planner, 128, new LinkedBlockingQueue()::add); + ArgumentCaptor> scope = ArgumentCaptor.forClass(Predicate.class); + + HydrologyTileCache.PregenerationScope pregen = cache.preparePregeneration( + new HydrologyTileCache.PregenerationArea(0, 0, -1536, -1536, 2047, 2047)); + verify(planner).limitEarlyOwners(scope.capture()); + for (HydrologyTileKey key : prefetchRectangle(-2, 2, -2, 2)) { + assertTrue(key.toString(), scope.getValue().test(key)); + } + assertFalse(scope.getValue().test(new HydrologyTileKey(-3, 0))); + assertFalse(scope.getValue().test(new HydrologyTileKey(0, 3))); + + pregen.close(); + verify(planner).limitEarlyOwners(null); + } + @Test public void invalidPregenBoundsLeaveQueuedPrefetchIntact() { HydrologyPlanner planner = mock(HydrologyPlanner.class);