diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenMethod.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenMethod.java index 46080f526..11aa965f3 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenMethod.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenMethod.java @@ -348,6 +348,11 @@ public final class ModdedPregenMethod implements PregeneratorMethod { return false; } + @Override + public void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) { + hydrology.bound(minRegionX, minRegionZ, maxRegionX, maxRegionZ); + } + @Override public void onRegionsAhead(List regions) { if (!closing.get()) { diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/engine/HydrologyPregeneration.java b/core/src/main/java/art/arcane/iris/generation/hydrology/engine/HydrologyPregeneration.java index 7e17ebc22..94f29f4e0 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/engine/HydrologyPregeneration.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/engine/HydrologyPregeneration.java @@ -18,6 +18,12 @@ public final class HydrologyPregeneration implements AutoCloseable { } } + public synchronized void bound(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) { + if (!closed) { + runtime.boundPregeneration(epoch, minRegionX, minRegionZ, maxRegionX, maxRegionZ); + } + } + @Override public synchronized void close() { if (!closed) { diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/engine/HydrologyRuntime.java b/core/src/main/java/art/arcane/iris/generation/hydrology/engine/HydrologyRuntime.java index 2f525c3d9..8c38c8c2f 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/engine/HydrologyRuntime.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/engine/HydrologyRuntime.java @@ -36,6 +36,7 @@ import java.io.UncheckedIOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -77,6 +78,7 @@ public final class HydrologyRuntime { private final RasterFailures regionPreviewFailures; private final Set previews; private final LinkedHashMap prefetches; + private final Map pregenAreas; private final HydrologyBinding binding; private final Function stores; private final boolean caveFluidPolicies; @@ -99,6 +101,7 @@ public final class HydrologyRuntime { this.regionPreviewFailures = null; this.previews = null; this.prefetches = null; + this.pregenAreas = null; this.binding = HydrologyBinding.UNBOUND; this.stores = identity -> null; this.caveFluidPolicies = false; @@ -125,6 +128,7 @@ public final class HydrologyRuntime { this.regionPreviewFailures = new RasterFailures(ticker); this.previews = new HashSet<>(); this.prefetches = new LinkedHashMap<>(PREFETCH_CAPACITY * 2, 0.75f, true); + this.pregenAreas = new HashMap<>(); this.binding = Objects.requireNonNull(binding, "binding"); this.stores = Objects.requireNonNull(stores, "stores"); this.caveFluidPolicies = caveFluidPolicies; @@ -354,8 +358,28 @@ public final class HydrologyRuntime { } } + void boundPregeneration(Object epoch, int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) { + Objects.requireNonNull(epoch, "epoch"); + if (maxRegionX < minRegionX || maxRegionZ < minRegionZ) { + throw new IllegalArgumentException("Invalid pregeneration bounds " + minRegionX + "," + minRegionZ + " to " + maxRegionX + "," + + maxRegionZ); + } + if (cache == null) { + return; + } + synchronized (prefetches) { + pregenAreas.put(epoch, new PregenArea(minRegionX - 1, minRegionZ - 1, maxRegionX + 1, maxRegionZ + 1)); + prefetches.clear(); + } + } + void cancelPregeneration(Object epoch) { if (cache != null) { + synchronized (prefetches) { + if (pregenAreas.remove(epoch) != null) { + prefetches.clear(); + } + } cache.cancel(epoch); } } @@ -667,7 +691,7 @@ public final class HydrologyRuntime { && (!known.prepared().isDone() || known.evictions() == evictions)) { return; } - CompletableFuture prepared = cache.prepareWindowsAround(chunkX, chunkZ, chunkX, chunkZ, 1 + settings.prefetchRings()); + CompletableFuture prepared = prepareAround(chunkX, chunkZ, owner, 1 + settings.prefetchRings()); prefetches.put(owner, new WindowPrefetch(prepared, evictions)); if (prefetches.size() > PREFETCH_CAPACITY) { prefetches.pollFirstEntry(); @@ -675,6 +699,24 @@ public final class HydrologyRuntime { } } + private CompletableFuture prepareAround(int chunkX, int chunkZ, WindowKey owner, int rings) { + PregenArea covering = null; + for (PregenArea area : pregenAreas.values()) { + if (area.contains(owner)) { + covering = covering == null ? area : covering.union(area); + } + } + if (covering == null) { + return cache.prepareWindowsAround(chunkX, chunkZ, chunkX, chunkZ, rings); + } + int minX = Math.max(owner.wx() - rings, covering.minX()); + int minZ = Math.max(owner.wz() - rings, covering.minZ()); + int maxX = Math.min(owner.wx() + rings, covering.maxX()); + int maxZ = Math.min(owner.wz() + rings, covering.maxZ()); + return cache.prepareWindowsAround(minX * CHUNKS_PER_WINDOW, minZ * CHUNKS_PER_WINDOW, (maxX + 1) * CHUNKS_PER_WINDOW - 1, + (maxZ + 1) * CHUNKS_PER_WINDOW - 1, 0); + } + private static List neighbourhood(int chunkX, int chunkZ) { WindowKey own = WindowKey.containing(chunkX << 4, chunkZ << 4); List keys = new ArrayList<>(9); @@ -754,6 +796,16 @@ public final class HydrologyRuntime { private record WindowPrefetch(CompletableFuture prepared, long evictions) { } + private record PregenArea(int minX, int minZ, int maxX, int maxZ) { + private boolean contains(WindowKey key) { + return key.wx() > minX && key.wx() < maxX && key.wz() > minZ && key.wz() < maxZ; + } + + private PregenArea union(PregenArea other) { + return new PregenArea(Math.min(minX, other.minX), Math.min(minZ, other.minZ), Math.max(maxX, other.maxX), Math.max(maxZ, other.maxZ)); + } + } + private static final class LastRaster { private long key; private ChunkWaterRaster raster; diff --git a/core/src/main/java/art/arcane/iris/world/pregen/AsyncPregenMethod.java b/core/src/main/java/art/arcane/iris/world/pregen/AsyncPregenMethod.java index fe1fab2df..4b728610a 100644 --- a/core/src/main/java/art/arcane/iris/world/pregen/AsyncPregenMethod.java +++ b/core/src/main/java/art/arcane/iris/world/pregen/AsyncPregenMethod.java @@ -268,6 +268,7 @@ public class AsyncPregenMethod implements PregeneratorMethod { @Override public void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) { retention = createRetention(minRegionX, minRegionZ, maxRegionX, maxRegionZ); + hydrology.bound(minRegionX, minRegionZ, maxRegionX, maxRegionZ); } private PregenChunkRetention createRetention(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) { diff --git a/core/src/main/java/art/arcane/iris/world/pregen/MedievalPregenMethod.java b/core/src/main/java/art/arcane/iris/world/pregen/MedievalPregenMethod.java index 4646ecf74..ae8f5be1a 100644 --- a/core/src/main/java/art/arcane/iris/world/pregen/MedievalPregenMethod.java +++ b/core/src/main/java/art/arcane/iris/world/pregen/MedievalPregenMethod.java @@ -219,6 +219,11 @@ public class MedievalPregenMethod implements PregeneratorMethod { return false; } + @Override + public void onRegionBounds(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) { + hydrology.bound(minRegionX, minRegionZ, maxRegionX, maxRegionZ); + } + @Override public void onRegionsAhead(List regions) { if (!closing.get()) { diff --git a/core/src/main/java/art/arcane/iris/world/pregen/PregenHydrology.java b/core/src/main/java/art/arcane/iris/world/pregen/PregenHydrology.java index 28d5921e2..2c0cc3489 100644 --- a/core/src/main/java/art/arcane/iris/world/pregen/PregenHydrology.java +++ b/core/src/main/java/art/arcane/iris/world/pregen/PregenHydrology.java @@ -13,6 +13,7 @@ public final class PregenHydrology implements AutoCloseable { private final Supplier engine; private HydrologyRuntime owner; private HydrologyPregeneration epoch; + private int[] bounds; private boolean closed; public PregenHydrology(Supplier engine) { @@ -28,12 +29,21 @@ public final class PregenHydrology implements AutoCloseable { withdraw(); owner = runtime; epoch = runtime == null ? null : runtime.openPregeneration(); + applyBounds(); } if (epoch != null) { epoch.prepare(regions); } } + public synchronized void bound(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) { + if (closed) { + return; + } + bounds = new int[]{minRegionX, minRegionZ, maxRegionX, maxRegionZ}; + applyBounds(); + } + @Override public synchronized void close() { closed = true; @@ -41,6 +51,12 @@ public final class PregenHydrology implements AutoCloseable { owner = null; } + private void applyBounds() { + if (epoch != null && bounds != null) { + epoch.bound(bounds[0], bounds[1], bounds[2], bounds[3]); + } + } + private void withdraw() { if (epoch != null) { epoch.close(); diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/engine/HydrologyRuntimeTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/engine/HydrologyRuntimeTest.java index a97fa21dc..7e38638fa 100644 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/engine/HydrologyRuntimeTest.java +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/engine/HydrologyRuntimeTest.java @@ -67,6 +67,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.BooleanSupplier; import java.util.stream.Stream; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -414,6 +415,35 @@ public class HydrologyRuntimeTest { assertEquals(3, prefetches.get()); } + @Test(timeout = 180_000L) + public void boundedPregenerationPrefetchesOnlyWindowsItsChunksNeed() { + FakePlanners planners = new FakePlanners(settings()); + HydrologyPlanCache cache = spy(new HydrologyPlanCache(planners.settings, planners::region, planners::window, + pool(planners, 2), () -> false)); + List prepared = Collections.synchronizedList(new ArrayList<>()); + doAnswer(invocation -> { + prepared.add(new int[]{invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2), invocation.getArgument(3), + invocation.getArgument(4)}); + return CompletableFuture.completedFuture(null); + }).when(cache).prepareWindowsAround(anyInt(), anyInt(), anyInt(), anyInt(), anyInt()); + HydrologyRuntime runtime = new HydrologyRuntime(planners.settings, cache, planners::natural, () -> false, planners.ticker::get); + runtimes.add(runtime); + int chunks = WindowKey.SIZE >> 4; + HydrologyPregeneration pregeneration = runtime.openPregeneration(); + pregeneration.bound(0, 0, 1, 1); + + runtime.raster(0, 0); + assertArrayEquals(new int[]{-chunks, -chunks, 3 * chunks - 1, 3 * chunks - 1, 0}, prepared.getLast()); + runtime.raster(chunks + 5, chunks + 5); + assertArrayEquals(new int[]{-chunks, -chunks, 3 * chunks - 1, 3 * chunks - 1, 0}, prepared.getLast()); + runtime.raster(-3 * chunks, 0); + assertArrayEquals(new int[]{-3 * chunks, 0, -3 * chunks, 0, 2}, prepared.getLast()); + + pregeneration.close(); + runtime.raster(1, 1); + assertArrayEquals(new int[]{1, 1, 1, 1, 2}, prepared.getLast()); + } + @Test(timeout = 180_000L) public void preparePregenerationPlansRegionsInOrder() throws Exception { FakePlanners planners = new FakePlanners(settings());