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/chunk/ColumnExtentListeningHunk.java b/core/src/main/java/art/arcane/iris/generation/chunk/ColumnExtentListeningHunk.java index 0498d0973..9c7aa0b30 100644 --- a/core/src/main/java/art/arcane/iris/generation/chunk/ColumnExtentListeningHunk.java +++ b/core/src/main/java/art/arcane/iris/generation/chunk/ColumnExtentListeningHunk.java @@ -20,7 +20,7 @@ package art.arcane.iris.generation.chunk; import art.arcane.volmlib.util.hunk.Hunk; -public final class ColumnExtentListeningHunk implements Hunk, ColumnExtent, ColumnRunHunk { +public final class ColumnExtentListeningHunk implements Hunk, ColumnExtent, ColumnRunHunk, ColumnReadHunk { private final Hunk source; private final Listener listener; @@ -46,6 +46,11 @@ public final class ColumnExtentListeningHunk implements Hunk, ColumnExtent return source.getRaw(x, y, z); } + @Override + public void readColumn(int x, int z, int yFromInclusive, int yToInclusive, T[] target, int targetOffset) { + ColumnReadHunk.readColumn(source, x, z, yFromInclusive, yToInclusive, target, targetOffset); + } + @Override public int getWidth() { return source.getWidth(); diff --git a/core/src/main/java/art/arcane/iris/generation/chunk/ColumnReadHunk.java b/core/src/main/java/art/arcane/iris/generation/chunk/ColumnReadHunk.java new file mode 100644 index 000000000..dbc90ce0d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/chunk/ColumnReadHunk.java @@ -0,0 +1,37 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.generation.chunk; + +import art.arcane.volmlib.util.hunk.Hunk; + +public interface ColumnReadHunk { + void readColumn(int x, int z, int yFromInclusive, int yToInclusive, T[] target, int targetOffset); + + @SuppressWarnings("unchecked") + static void readColumn(Hunk hunk, int x, int z, int yFromInclusive, int yToInclusive, T[] target, + int targetOffset) { + if (hunk instanceof ColumnReadHunk reads) { + ((ColumnReadHunk) reads).readColumn(x, z, yFromInclusive, yToInclusive, target, targetOffset); + return; + } + for (int y = yFromInclusive; y <= yToInclusive; y++) { + target[targetOffset + y - yFromInclusive] = hunk.getRaw(x, y, z); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/chunk/SectionBuffer.java b/core/src/main/java/art/arcane/iris/generation/chunk/SectionBuffer.java index 8a6898911..01e9710e7 100644 --- a/core/src/main/java/art/arcane/iris/generation/chunk/SectionBuffer.java +++ b/core/src/main/java/art/arcane/iris/generation/chunk/SectionBuffer.java @@ -27,7 +27,8 @@ import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; import java.util.Arrays; -public final class SectionBuffer implements Hunk, NativeSectionVolume, ColumnExtent, ColumnRunHunk { +public final class SectionBuffer implements Hunk, NativeSectionVolume, ColumnExtent, ColumnRunHunk, + ColumnReadHunk { private static final BoundBlockState AIR = BoundBlockState.of("AIR"); private static final VarHandle SECTIONS = MethodHandles.arrayElementVarHandle(short[][].class); private static final VarHandle COLUMN_TOPS = MethodHandles.arrayElementVarHandle(int[].class); @@ -43,6 +44,8 @@ public final class SectionBuffer implements Hunk, NativeSectio private PaletteEntry[] slots; private NativeBlockState[] states; private int paletteSize; + private NativeBlockState air; + private PaletteEntry lastEntry; public SectionBuffer(int height) { if (height <= 0) { @@ -76,12 +79,17 @@ public final class SectionBuffer implements Hunk, NativeSectio if (state == null) { return 0; } + PaletteEntry last = lastEntry; + if (last != null && last.state == state) { + return last.id; + } PaletteEntry[] table = slots; int mask = table.length - 1; int slot = System.identityHashCode(state) & mask; PaletteEntry entry; while ((entry = table[slot]) != null) { if (entry.state == state) { + lastEntry = entry; return entry.id; } slot = (slot + 1) & mask; @@ -101,7 +109,7 @@ public final class SectionBuffer implements Hunk, NativeSectio @Override public NativeBlockState getRaw(int x, int y, int z) { if (y < 0 || y >= height) { - return AIR.get(); + return air(); } short[] section = sections[y >> 4]; if (section != null) { @@ -110,7 +118,7 @@ public final class SectionBuffer implements Hunk, NativeSectio return state(id); } } - return AIR.get(); + return air(); } @Override @@ -126,6 +134,32 @@ public final class SectionBuffer implements Hunk, NativeSectio return id == 0 ? null : state(id); } + @Override + public void readColumn(int x, int z, int yFromInclusive, int yToInclusive, NativeBlockState[] target, int targetOffset) { + int column = (z << 4) | x; + int shift = targetOffset - yFromInclusive; + int y = yFromInclusive; + while (y <= yToInclusive) { + if (y < 0 || y >= height) { + target[shift + y] = air(); + y++; + continue; + } + int sectionIndex = y >> 4; + int last = Math.min(Math.min(yToInclusive, height - 1), (sectionIndex << 4) | 15); + short[] section = sections[sectionIndex]; + if (section == null) { + Arrays.fill(target, shift + y, shift + last + 1, air()); + } else { + for (int row = y; row <= last; row++) { + int id = section[((row & 15) << 8) | column] & 0xFFFF; + target[shift + row] = id == 0 ? air() : state(id); + } + } + y = last + 1; + } + } + @Override public void fillColumnRun(int x, int z, int yFromInclusive, int yToInclusive, NativeBlockState value) { if (value == null || yToInclusive < yFromInclusive) { @@ -176,6 +210,15 @@ public final class SectionBuffer implements Hunk, NativeSectio } } + private NativeBlockState air() { + NativeBlockState resolved = air; + if (resolved == null) { + resolved = AIR.get(); + air = resolved; + } + return resolved; + } + private short[] writableSection(int sectionIndex) { short[] section = sections[sectionIndex]; if (section != null) { diff --git a/core/src/main/java/art/arcane/iris/generation/context/IrisContext.java b/core/src/main/java/art/arcane/iris/generation/context/IrisContext.java index 2131b4609..54c00606d 100644 --- a/core/src/main/java/art/arcane/iris/generation/context/IrisContext.java +++ b/core/src/main/java/art/arcane/iris/generation/context/IrisContext.java @@ -28,9 +28,14 @@ import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.volmlib.util.collection.KMap; import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReferenceArray; public final class IrisContext { - private static final ThreadLocal CONTEXT = new ThreadLocal<>(); + private static final int SLOTS = 512; + private static final AtomicReferenceArray BINDINGS = new AtomicReferenceArray<>(SLOTS); + private static final ThreadLocal OVERFLOW = new ThreadLocal<>(); + private static final AtomicInteger OVERFLOW_BINDINGS = new AtomicInteger(); private final Engine engine; private final ChunkContext chunkContext; private final long generationSessionId; @@ -42,7 +47,8 @@ public final class IrisContext { } public static IrisContext get() { - return CONTEXT.get(); + Binding binding = binding(Thread.currentThread()); + return binding == null ? null : binding.context; } public static IrisContext require() { @@ -55,9 +61,10 @@ public final class IrisContext { public static Scope open(Engine engine, long generationSessionId, ChunkContext chunkContext) { Thread thread = Thread.currentThread(); - IrisContext previous = CONTEXT.get(); + Binding binding = bind(thread); + IrisContext previous = binding.context; IrisContext installed = new IrisContext(engine, generationSessionId, chunkContext); - CONTEXT.set(installed); + binding.context = installed; return new Scope(thread, previous, installed); } @@ -99,6 +106,42 @@ public final class IrisContext { .qput("queued", mantle.getUnloadRegionCount())); } + private static Binding binding(Thread thread) { + Binding binding = BINDINGS.getPlain(slot(thread)); + if (binding != null && binding.owner == thread) { + return binding; + } + return OVERFLOW_BINDINGS.get() == 0 ? null : OVERFLOW.get(); + } + + private static Binding bind(Thread thread) { + Binding binding = binding(thread); + if (binding != null) { + return binding; + } + binding = new Binding(thread); + if (!BINDINGS.compareAndSet(slot(thread), null, binding)) { + binding.overflow = true; + OVERFLOW.set(binding); + OVERFLOW_BINDINGS.incrementAndGet(); + } + return binding; + } + + private static void release(Binding binding) { + binding.context = null; + if (binding.overflow) { + OVERFLOW.remove(); + OVERFLOW_BINDINGS.decrementAndGet(); + return; + } + BINDINGS.compareAndSet(slot(binding.owner), binding, null); + } + + private static int slot(Thread thread) { + return (int) thread.threadId() & (SLOTS - 1); + } + public static final class Scope implements NativeGenerationScope { private final Thread owner; private final IrisContext previous; @@ -119,16 +162,26 @@ public final class IrisContext { if (Thread.currentThread() != owner) { throw new IllegalStateException("Iris execution context scope closed from a different thread."); } - IrisContext current = CONTEXT.get(); - if (current != installed) { + Binding binding = binding(owner); + if (binding == null || binding.context != installed) { throw new IllegalStateException("Iris execution context scopes must close in LIFO order."); } if (previous == null) { - CONTEXT.remove(); + release(binding); } else { - CONTEXT.set(previous); + binding.context = previous; } closed = true; } } + + private static final class Binding { + private final Thread owner; + private boolean overflow; + private IrisContext context; + + private Binding(Thread owner) { + this.owner = owner; + } + } } diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/engine/FineLatticeSource.java b/core/src/main/java/art/arcane/iris/generation/hydrology/engine/FineLatticeSource.java deleted file mode 100644 index 73db7475d..000000000 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/engine/FineLatticeSource.java +++ /dev/null @@ -1,53 +0,0 @@ -package art.arcane.iris.generation.hydrology.engine; - -import art.arcane.iris.generation.hydrology.grid.HeightLattice; -import art.arcane.iris.generation.hydrology.grid.HydrologySampleBuffer; -import art.arcane.iris.generation.hydrology.grid.HydrologyTerrainSource; - -import java.util.Objects; - -final class FineLatticeSource implements HydrologyTerrainSource { - private final HeightLattice fine; - private final HydrologyTerrainSource terrain; - - FineLatticeSource(HeightLattice fine, HydrologyTerrainSource terrain) { - this.fine = Objects.requireNonNull(fine, "fine"); - this.terrain = Objects.requireNonNull(terrain, "terrain"); - } - - @Override - public void sample(int[] xs, int[] zs, HydrologySampleBuffer out, boolean baseOnly) { - if (baseOnly) { - throw new IllegalArgumentException("The fine lattice holds natural heights only"); - } - if (xs.length != zs.length) { - throw new IllegalArgumentException("Fine lattice read has " + xs.length + " x and " + zs.length + " z coordinates"); - } - int cell = fine.cell(); - int[] cellXs = new int[xs.length]; - int[] cellZs = new int[zs.length]; - for (int index = 0; index < xs.length; index++) { - if (Math.floorMod(xs[index], cell) != 0 || Math.floorMod(zs[index], cell) != 0) { - throw new IllegalArgumentException("Column " + xs[index] + "," + zs[index] + " is not a node of the " + cell + "-block fine lattice"); - } - cellXs[index] = Math.floorDiv(xs[index], cell); - cellZs[index] = Math.floorDiv(zs[index], cell); - } - fine.fill(cellXs, cellZs, out); - } - - @Override - public int seaLevel() { - return terrain.seaLevel(); - } - - @Override - public int minY() { - return terrain.minY(); - } - - @Override - public int maxY() { - return terrain.maxY(); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/engine/HydrologyPlanners.java b/core/src/main/java/art/arcane/iris/generation/hydrology/engine/HydrologyPlanners.java index 7ff67fb26..88bea3a5e 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/engine/HydrologyPlanners.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/engine/HydrologyPlanners.java @@ -11,23 +11,21 @@ import art.arcane.iris.generation.hydrology.water.ReachShaper; import java.util.Objects; -public record HydrologyPlanners(RegionPlanner regions, WindowPlanner windows, HeightLattice windowLattice) { +public record HydrologyPlanners(RegionPlanner regions, WindowPlanner windows, HeightLattice fineLattice) { public HydrologyPlanners { Objects.requireNonNull(regions, "regions"); Objects.requireNonNull(windows, "windows"); - Objects.requireNonNull(windowLattice, "windowLattice"); + Objects.requireNonNull(fineLattice, "fineLattice"); } public static HydrologyPlanners create(HydrologySettings settings, HydrologyTerrainSource source) { Objects.requireNonNull(settings, "settings"); Objects.requireNonNull(source, "source"); HeightLattice fine = new HeightLattice(source, WindowPlanner.FINE_CELL, false, HydrologyPlanCache.FINE_LATTICE_CELLS); - HeightLattice window = new HeightLattice(new FineLatticeSource(fine, source), WindowPlanner.CELL, false, - HydrologyPlanCache.WINDOW_LATTICE_CELLS); PondPlanner ponds = new PondPlanner(settings); ReachShaper shaper = new ReachShaper(settings); RegionPlanner regions = new RegionPlanner(settings, new HeightLattice(source, RegionPlanner.CELL, true, HydrologyPlanCache.REGION_LATTICE_CELLS), fine, ponds, shaper); - return new HydrologyPlanners(regions, new WindowPlanner(settings, window, fine, ponds, shaper), window); + return new HydrologyPlanners(regions, new WindowPlanner(settings, fine, ponds, shaper), fine); } } 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..c2536f5b8 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; @@ -56,7 +57,7 @@ import java.util.function.Predicate; import java.util.function.Supplier; public final class HydrologyRuntime { - private static final int RASTER_CAPACITY = 1024; + private static final int RASTER_CAPACITY = 8192; private static final int KEEP_SCOPES = 2; private static final int CHUNKS_PER_WINDOW = WindowKey.SIZE >> 4; public static final int MAX_PREGENERATION_REGIONS = 4096; @@ -72,11 +73,13 @@ public final class HydrologyRuntime { private final RasterLru rasters; private final ThreadLocal last; private final Set queued; + private final ConcurrentHashMap> building; private final RasterFailures failures; private final RasterFailures windowPreviewFailures; 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; @@ -94,11 +97,13 @@ public final class HydrologyRuntime { this.rasters = null; this.last = null; this.queued = null; + this.building = null; this.failures = null; this.windowPreviewFailures = null; this.regionPreviewFailures = null; this.previews = null; this.prefetches = null; + this.pregenAreas = null; this.binding = HydrologyBinding.UNBOUND; this.stores = identity -> null; this.caveFluidPolicies = false; @@ -120,11 +125,13 @@ public final class HydrologyRuntime { this.rasters = new RasterLru(); this.last = ThreadLocal.withInitial(LastRaster::new); this.queued = ConcurrentHashMap.newKeySet(); + this.building = new ConcurrentHashMap<>(); this.failures = new RasterFailures(ticker); this.windowPreviewFailures = new RasterFailures(ticker); 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; @@ -234,7 +241,7 @@ public final class HydrologyRuntime { requireOpen(); long key = key(chunkX, chunkZ); ChunkWaterRaster known = remembered(key, slot); - return known != null ? known : remember(key, planned(chunkX, chunkZ, key)); + return known != null ? known : build(chunkX, chunkZ, key, slot); } public ChunkWaterRaster rasterSnapshot(int chunkX, int chunkZ) { @@ -354,8 +361,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); } } @@ -501,6 +528,41 @@ public final class HydrologyRuntime { } } + private ChunkWaterRaster build(int chunkX, int chunkZ, long key, LastRaster slot) { + CompletableFuture claim = new CompletableFuture<>(); + CompletableFuture running = building.putIfAbsent(key, claim); + if (running != null) { + ChunkWaterRaster built = built(running); + slot.remember(key, built); + return built; + } + try { + ChunkWaterRaster known = remembered(key, slot); + ChunkWaterRaster raster = known != null ? known : remember(key, planned(chunkX, chunkZ, key)); + claim.complete(raster); + return raster; + } catch (RuntimeException | Error failure) { + claim.completeExceptionally(failure); + throw failure; + } finally { + building.remove(key, claim); + } + } + + private static ChunkWaterRaster built(CompletableFuture running) { + try { + return running.join(); + } catch (CompletionException failure) { + if (failure.getCause() instanceof RuntimeException cause) { + throw cause; + } + if (failure.getCause() instanceof Error cause) { + throw cause; + } + throw failure; + } + } + private ChunkWaterRaster planned(int chunkX, int chunkZ, long chunk) { try { List windowKeys = neighbourhood(chunkX, chunkZ); @@ -667,7 +729,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 +737,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 +834,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/generation/hydrology/grid/HeightLattice.java b/core/src/main/java/art/arcane/iris/generation/hydrology/grid/HeightLattice.java index 1f1d5c7f4..cc1804606 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/grid/HeightLattice.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/grid/HeightLattice.java @@ -1,34 +1,59 @@ package art.arcane.iris.generation.hydrology.grid; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReferenceArray; public final class HeightLattice { + private static final int TILE_SHIFT = 5; + private static final int COLUMN_TILE_SHIFT = 3; + private static final int COLUMN_CELLS = 1 << 20; + private static final CompletableFuture READY = CompletableFuture.completedFuture(null); + private static final int[] NO_NODES = new int[0]; + private static final Tile[] NO_TILES = new Tile[0]; + private final HydrologyTerrainSource source; private final int cell; private final boolean baseOnly; - private final int capacityCells; - private final ConcurrentHashMap cells; - private final ConcurrentLinkedQueue insertionOrder; + private final int tileShift; + private final int tileMask; + private final int tileCells; + private final int capacityTiles; + private final ConcurrentHashMap tiles; + private final AtomicLong clock; private final AtomicBoolean evicting; + private final HeightLattice columns; public HeightLattice(HydrologyTerrainSource source, int cell, boolean baseOnly, int capacityCells) { + this(source, cell, baseOnly, capacityCells, TILE_SHIFT, new HeightLattice(source, 1, baseOnly, COLUMN_CELLS, COLUMN_TILE_SHIFT, null)); + } + + private HeightLattice(HydrologyTerrainSource source, int cell, boolean baseOnly, int capacityCells, int tileShift, HeightLattice columns) { if (cell <= 0 || capacityCells <= 0) { throw new IllegalArgumentException("Invalid lattice cell=" + cell + " capacityCells=" + capacityCells); } this.source = Objects.requireNonNull(source, "source"); this.cell = cell; this.baseOnly = baseOnly; - this.capacityCells = capacityCells; - this.cells = new ConcurrentHashMap<>(); - this.insertionOrder = new ConcurrentLinkedQueue<>(); + this.tileShift = tileShift; + this.tileMask = (1 << tileShift) - 1; + this.tileCells = 1 << (2 * tileShift); + this.capacityTiles = Math.ceilDiv(capacityCells, tileCells); + this.tiles = new ConcurrentHashMap<>(); + this.clock = new AtomicLong(); this.evicting = new AtomicBoolean(); + this.columns = columns; } public int cell() { @@ -67,19 +92,24 @@ public final class HeightLattice { throw new IllegalArgumentException("Column sample has " + xs.length + " x and " + zs.length + " z coordinates"); } requireCapacity(out, xs.length); - if (xs.length > 0) { - source.sample(xs, zs, out, baseOnly); + if (xs.length == 0) { + return; } + if (columns == null) { + source.sample(xs, zs, out, baseOnly); + return; + } + columns.resolve(new Scatter(xs, zs), xs.length, out); } private void resolve(Nodes nodes, int count, HydrologySampleBuffer out) { - Slot[] slots = new Slot[count]; - int unresolved = count; - while (unresolved > 0) { - CompletableFuture batch = claimAndSample(nodes, slots); - unresolved = awaitForeign(batch, slots); + Pass pass = new Pass(nodes, out, clock.incrementAndGet()); + int[] open = null; + int remaining = count; + while (remaining > 0) { + open = pass.run(open, remaining); + remaining = open == null ? 0 : open.length; } - copy(slots, out); evictOverflow(); } @@ -94,130 +124,33 @@ public final class HeightLattice { Math.multiplyExact(Math.addExact(cell0, Math.multiplyExact(extent - 1, stride)), cell); } - private CompletableFuture claimAndSample(Nodes nodes, Slot[] slots) { - CompletableFuture batch = new CompletableFuture<>(); - try { - int[] xs = new int[slots.length]; - int[] zs = new int[slots.length]; - int claimed = claim(batch, nodes, slots, xs, zs); - if (claimed > 0) { - HydrologySampleBuffer samples = new HydrologySampleBuffer(claimed); - source.sample(Arrays.copyOf(xs, claimed), Arrays.copyOf(zs, claimed), samples, baseOnly); - batch.complete(samples); - } - } catch (Throwable failure) { - try { - release(batch, nodes, slots); - } finally { - batch.completeExceptionally(failure); - } - throw failure; - } - return batch; - } - - private int claim(CompletableFuture batch, Nodes nodes, Slot[] slots, int[] xs, int[] zs) { - int claimed = 0; - for (int index = 0; index < slots.length; index++) { - if (slots[index] != null) { - continue; - } - int cellX = nodes.cellX(index); - int cellZ = nodes.cellZ(index); - Long key = key(cellX, cellZ); - Slot existing = cells.get(key); - if (existing == null) { - Slot fresh = new Slot(batch, claimed); - existing = cells.putIfAbsent(key, fresh); - if (existing == null) { - slots[index] = fresh; - insertionOrder.offer(key); - xs[claimed] = cellX * cell; - zs[claimed] = cellZ * cell; - claimed++; - continue; - } - } - slots[index] = existing; - } - return claimed; - } - - private void release(CompletableFuture batch, Nodes nodes, Slot[] slots) { - for (int index = 0; index < slots.length; index++) { - Slot slot = slots[index]; - if (slot != null && slot.batch() == batch) { - cells.remove(key(nodes.cellX(index), nodes.cellZ(index)), slot); - } - } - } - - private static int awaitForeign(CompletableFuture own, Slot[] slots) { - int unresolved = 0; - for (int index = 0; index < slots.length; index++) { - CompletableFuture batch = slots[index].batch(); - if (batch != own && !await(batch)) { - slots[index] = null; - unresolved++; - } - } - return unresolved; - } - - private static boolean await(CompletableFuture batch) { - if (!batch.isDone()) { - try { - batch.get(); - } catch (ExecutionException | CancellationException failure) { - return false; - } catch (InterruptedException interruption) { - Thread.currentThread().interrupt(); - throw new CancellationException("Hydrology lattice wait interrupted."); - } - } - return !batch.isCompletedExceptionally(); - } - - private static void copy(Slot[] slots, HydrologySampleBuffer out) { - float[] height = out.height(); - boolean[] ocean = out.ocean(); - float[] rainfall = out.rainfall(); - float[] pondDensity = out.pondDensity(); - byte[] permission = out.permission(); - String[] fluidProfile = out.fluidProfile(); - for (int index = 0; index < slots.length; index++) { - Slot slot = slots[index]; - HydrologySampleBuffer samples = slot.batch().join(); - int sample = slot.index(); - height[index] = samples.height()[sample]; - ocean[index] = samples.ocean()[sample]; - rainfall[index] = samples.rainfall()[sample]; - pondDensity[index] = samples.pondDensity()[sample]; - permission[index] = samples.permission()[sample]; - fluidProfile[index] = samples.fluidProfile()[sample]; + private Tile tile(int tileX, int tileZ) { + Long key = key(tileX, tileZ); + Tile known = tiles.get(key); + if (known != null) { + return known; } + return tiles.computeIfAbsent(key, ignored -> new Tile(tileCells)); } private void evictOverflow() { - if (cells.size() <= capacityCells || !evicting.compareAndSet(false, true)) { + if (tiles.size() <= capacityTiles || !evicting.compareAndSet(false, true)) { return; } try { - int excess = cells.size() - capacityCells; - while (excess > 0) { - Long key = insertionOrder.poll(); - if (key == null) { - return; + List> idle = new ArrayList<>(tiles.size()); + for (Map.Entry entry : tiles.entrySet()) { + if (entry.getValue().pending.get() == 0) { + idle.add(entry); } - Slot slot = cells.get(key); - if (slot == null) { - continue; - } - if (!slot.batch().isDone()) { - insertionOrder.offer(key); - excess--; - } else if (cells.remove(key, slot)) { - excess--; + } + idle.sort(Comparator.comparingLong(entry -> entry.getValue().used)); + int target = capacityTiles - (capacityTiles >> 3); + for (int index = 0; index < idle.size() && tiles.size() > target; index++) { + Map.Entry entry = idle.get(index); + Tile tile = entry.getValue(); + if (tile.pending.get() == 0) { + tiles.remove(entry.getKey(), tile); } } } finally { @@ -232,7 +165,219 @@ public final class HeightLattice { return mixed ^ (mixed >>> 31); } - private record Slot(CompletableFuture batch, int index) { + private static boolean await(CompletableFuture batch) { + if (!batch.isDone()) { + try { + batch.get(); + } catch (ExecutionException | CancellationException failure) { + return false; + } catch (InterruptedException interruption) { + Thread.currentThread().interrupt(); + throw new CancellationException("Hydrology lattice wait interrupted."); + } + } + return !batch.isCompletedExceptionally(); + } + + private static void copy(HydrologySampleBuffer samples, int sample, HydrologySampleBuffer out, int index) { + out.height()[index] = samples.height()[sample]; + out.ocean()[index] = samples.ocean()[sample]; + out.rainfall()[index] = samples.rainfall()[sample]; + out.pondDensity()[index] = samples.pondDensity()[sample]; + out.permission()[index] = samples.permission()[sample]; + out.fluidProfile()[index] = samples.fluidProfile()[sample]; + } + + private final class Pass { + private final Nodes nodes; + private final HydrologySampleBuffer out; + private final long stamp; + private int[] claimedIndex = NO_NODES; + private Tile[] claimedTile = NO_TILES; + private int[] claimedLocal = NO_NODES; + private int claimed; + private int[] waitingIndex = NO_NODES; + private Tile[] waitingTile = NO_TILES; + private int[] waitingLocal = NO_NODES; + private int waiting; + private Tile current; + private int currentX; + private int currentZ; + + private Pass(Nodes nodes, HydrologySampleBuffer out, long stamp) { + this.nodes = nodes; + this.out = out; + this.stamp = stamp; + } + + private int[] run(int[] open, int remaining) { + claimed = 0; + waiting = 0; + CompletableFuture batch = new CompletableFuture<>(); + for (int position = 0; position < remaining; position++) { + visit(open == null ? position : open[position], batch); + } + if (claimed > 0) { + sample(batch); + } + return awaitWaiting(); + } + + private void visit(int index, CompletableFuture batch) { + int cellX = nodes.cellX(index); + int cellZ = nodes.cellZ(index); + Tile tile = tileOf(cellX >> tileShift, cellZ >> tileShift); + int local = ((cellZ & tileMask) << tileShift) | (cellX & tileMask); + while (true) { + CompletableFuture mark = tile.marks.get(local); + if (mark == READY) { + tile.copy(local, out, index); + return; + } + if (mark != null) { + addWaiting(index, tile, local); + return; + } + if (tile.marks.compareAndSet(local, null, batch)) { + tile.pending.incrementAndGet(); + addClaimed(index, tile, local); + return; + } + } + } + + private Tile tileOf(int tileX, int tileZ) { + if (current == null || currentX != tileX || currentZ != tileZ) { + current = tile(tileX, tileZ); + currentX = tileX; + currentZ = tileZ; + current.used = stamp; + } + return current; + } + + private void sample(CompletableFuture batch) { + try { + int[] xs = new int[claimed]; + int[] zs = new int[claimed]; + for (int position = 0; position < claimed; position++) { + xs[position] = nodes.cellX(claimedIndex[position]) * cell; + zs[position] = nodes.cellZ(claimedIndex[position]) * cell; + } + HydrologySampleBuffer samples = new HydrologySampleBuffer(claimed); + source.sample(xs, zs, samples, baseOnly); + for (int position = 0; position < claimed; position++) { + Tile tile = claimedTile[position]; + int local = claimedLocal[position]; + tile.store(local, samples, position); + tile.marks.set(local, READY); + tile.pending.decrementAndGet(); + copy(samples, position, out, claimedIndex[position]); + } + batch.complete(null); + } catch (Throwable failure) { + try { + release(batch); + } finally { + batch.completeExceptionally(failure); + } + throw failure; + } + } + + private void release(CompletableFuture batch) { + for (int position = 0; position < claimed; position++) { + Tile tile = claimedTile[position]; + if (tile.marks.compareAndSet(claimedLocal[position], batch, null)) { + tile.pending.decrementAndGet(); + } + } + } + + private int[] awaitWaiting() { + int[] open = null; + int unresolved = 0; + for (int position = 0; position < waiting; position++) { + Tile tile = waitingTile[position]; + int local = waitingLocal[position]; + CompletableFuture mark = tile.marks.get(local); + if (mark != READY && (mark == null || !await(mark) || tile.marks.get(local) != READY)) { + if (open == null) { + open = new int[waiting - position]; + } + open[unresolved++] = waitingIndex[position]; + continue; + } + tile.copy(local, out, waitingIndex[position]); + } + return open == null ? null : Arrays.copyOf(open, unresolved); + } + + private void addClaimed(int index, Tile tile, int local) { + if (claimed == claimedIndex.length) { + int grown = Math.max(16, claimed * 2); + claimedIndex = Arrays.copyOf(claimedIndex, grown); + claimedTile = Arrays.copyOf(claimedTile, grown); + claimedLocal = Arrays.copyOf(claimedLocal, grown); + } + claimedIndex[claimed] = index; + claimedTile[claimed] = tile; + claimedLocal[claimed] = local; + claimed++; + } + + private void addWaiting(int index, Tile tile, int local) { + if (waiting == waitingIndex.length) { + int grown = Math.max(16, waiting * 2); + waitingIndex = Arrays.copyOf(waitingIndex, grown); + waitingTile = Arrays.copyOf(waitingTile, grown); + waitingLocal = Arrays.copyOf(waitingLocal, grown); + } + waitingIndex[waiting] = index; + waitingTile[waiting] = tile; + waitingLocal[waiting] = local; + waiting++; + } + } + + private static final class Tile { + private final float[] height; + private final boolean[] ocean; + private final float[] rainfall; + private final float[] pondDensity; + private final byte[] permission; + private final String[] fluidProfile; + private final AtomicReferenceArray> marks; + private final AtomicInteger pending = new AtomicInteger(); + private volatile long used; + + private Tile(int cells) { + this.height = new float[cells]; + this.ocean = new boolean[cells]; + this.rainfall = new float[cells]; + this.pondDensity = new float[cells]; + this.permission = new byte[cells]; + this.fluidProfile = new String[cells]; + this.marks = new AtomicReferenceArray<>(cells); + } + + private void store(int local, HydrologySampleBuffer samples, int sample) { + height[local] = samples.height()[sample]; + ocean[local] = samples.ocean()[sample]; + rainfall[local] = samples.rainfall()[sample]; + pondDensity[local] = samples.pondDensity()[sample]; + permission[local] = samples.permission()[sample]; + fluidProfile[local] = samples.fluidProfile()[sample]; + } + + private void copy(int local, HydrologySampleBuffer out, int index) { + out.height()[index] = height[local]; + out.ocean()[index] = ocean[local]; + out.rainfall()[index] = rainfall[local]; + out.pondDensity()[index] = pondDensity[local]; + out.permission()[index] = permission[local]; + out.fluidProfile()[index] = fluidProfile[local]; + } } private interface Nodes { diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/plan/HydrologyPlanCache.java b/core/src/main/java/art/arcane/iris/generation/hydrology/plan/HydrologyPlanCache.java index ec2365dc8..4269e145b 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/plan/HydrologyPlanCache.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/plan/HydrologyPlanCache.java @@ -32,10 +32,10 @@ import java.util.function.Consumer; import java.util.function.Function; public final class HydrologyPlanCache { - private static final int FRONT = 3; - public static final int WINDOW_LATTICE_CELLS = frontCells(WindowKey.SIZE, WindowPlanner.CELL); - public static final int FINE_LATTICE_CELLS = frontCells(WindowKey.SIZE, WindowPlanner.FINE_CELL); - public static final int REGION_LATTICE_CELLS = frontCells(RegionKey.SIZE, RegionPlanner.CELL); + private static final int FINE_FRONT = 9; + private static final int REGION_FRONT = 5; + public static final int FINE_LATTICE_CELLS = frontCells(FINE_FRONT, WindowKey.SIZE, WindowPlanner.FINE_CELL); + public static final int REGION_LATTICE_CELLS = frontCells(REGION_FRONT, RegionKey.SIZE, RegionPlanner.CELL); private static final int WINDOW_CAPACITY = 128; private static final int REGION_CAPACITY = 64; private static final long DEMAND = Long.MIN_VALUE; @@ -312,8 +312,8 @@ public final class HydrologyPlanCache { return Math.max(1, parallelism / 2); } - private static int frontCells(int unit, int cell) { - int cells = (FRONT + 2) * unit / cell; + private static int frontCells(int front, int unit, int cell) { + int cells = (front + 2) * unit / cell; return cells * cells; } diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/plan/WindowPlanner.java b/core/src/main/java/art/arcane/iris/generation/hydrology/plan/WindowPlanner.java index 82df753de..07238e748 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/plan/WindowPlanner.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/plan/WindowPlanner.java @@ -36,6 +36,7 @@ public final class WindowPlanner { public static final int CELL = 8; public static final int FINE_CELL = 4; private static final int WINDOW_CELLS = WindowKey.SIZE / CELL; + private static final int FINE_STRIDE = CELL / FINE_CELL; private static final int SPAN = 3 * WINDOW_CELLS; private static final int REACH_OVERHANG = 256; private static final float BASIN_DEPTH = 0.5f; @@ -43,22 +44,17 @@ public final class WindowPlanner { private static final long SALT_HEAD = 0x5354524541484dL; private final HydrologySettings settings; - private final HeightLattice lattice; private final HeightLattice fine; private final PondPlanner ponds; private final ReachShaper shaper; private final SeaInletPlanner inlets; - public WindowPlanner(HydrologySettings settings, HeightLattice lattice, HeightLattice fine, PondPlanner ponds, ReachShaper shaper) { + public WindowPlanner(HydrologySettings settings, HeightLattice fine, PondPlanner ponds, ReachShaper shaper) { this.settings = Objects.requireNonNull(settings, "settings"); - this.lattice = Objects.requireNonNull(lattice, "lattice"); this.fine = Objects.requireNonNull(fine, "fine"); this.ponds = Objects.requireNonNull(ponds, "ponds"); this.shaper = Objects.requireNonNull(shaper, "shaper"); this.inlets = new SeaInletPlanner(settings); - if (lattice.cell() != CELL) { - throw new IllegalArgumentException("Window lattice cell " + lattice.cell() + " must be " + CELL); - } if (fine.cell() != FINE_CELL) { throw new IllegalArgumentException("Window fine lattice cell " + fine.cell() + " must be " + FINE_CELL); } @@ -87,7 +83,7 @@ public final class WindowPlanner { int cellX = Math.multiplyExact(key.wx() - 1, WINDOW_CELLS); int cellZ = Math.multiplyExact(key.wz() - 1, WINDOW_CELLS); HydrologySampleBuffer samples = new HydrologySampleBuffer(SPAN * SPAN); - lattice.fill(cellX, cellZ, SPAN, SPAN, samples); + fine.fill(cellX * FINE_STRIDE, cellZ * FINE_STRIDE, SPAN, SPAN, FINE_STRIDE, samples); DemGrid raw = new DemGrid(cellX * CELL, cellZ * CELL, CELL, SPAN, SPAN); System.arraycopy(samples.height(), 0, raw.values(), 0, SPAN * SPAN); List lakes = regionLakes(plans.values()); diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkFeatures.java b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkFeatures.java index 3a0eb132b..11fd7ba6c 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkFeatures.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkFeatures.java @@ -12,6 +12,7 @@ import art.arcane.iris.generation.hydrology.water.PondMask; import art.arcane.iris.generation.hydrology.water.Reach; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -28,17 +29,20 @@ final class ChunkFeatures { private final List ponds; private final List lakes; private final List inlets; + private final int[] inletBoxes; private final int[] lakeBoxes; private final ReachSegments segments = new ReachSegments(SEGMENT_CAPACITY); private final MaskEdges pondShores = new MaskEdges(MaskEdges.Kind.WATER); private final MaskEdges pondBanks = new MaskEdges(MaskEdges.Kind.BANK); private final MaskEdges lakeReach = new MaskEdges(MaskEdges.Kind.GROWN_WATER); + private boolean inert; - private ChunkFeatures(List reaches, List ponds, List lakes, List inlets) { + private ChunkFeatures(List reaches, List ponds, List lakes, List inlets, int[] inletBoxes) { this.reaches = reaches; this.ponds = ponds; this.lakes = lakes; this.inlets = inlets; + this.inletBoxes = inletBoxes; this.lakeBoxes = new int[4 * lakes.size()]; } @@ -48,12 +52,15 @@ final class ChunkFeatures { List reaches = new ArrayList<>(); List ponds = new ArrayList<>(); List inlets = new ArrayList<>(); + int[] inletBoxes = new int[0]; for (IndexedWindow window : windows) { for (SeaInlet inlet : window.plan().inlets()) { int[] bounds = SeaInletRasterizer.bounds(inlet); int padding = RiffleProfile.RADIUS + 1; if (bounds[0] <= chunkX * CHUNK + CHUNK - 1 + padding && bounds[2] >= chunkX * CHUNK - padding && bounds[1] <= chunkZ * CHUNK + CHUNK - 1 + padding && bounds[3] >= chunkZ * CHUNK - padding) { + inletBoxes = Arrays.copyOf(inletBoxes, inletBoxes.length + 4); + System.arraycopy(bounds, 0, inletBoxes, 4 * inlets.size(), 4); inlets.add(inlet); } } @@ -64,17 +71,22 @@ final class ChunkFeatures { } } List lakes = lakes(chunkX, chunkZ, halo, lakeRegionPlans, lakeRegions); - ChunkFeatures features = new ChunkFeatures(reaches, ponds, lakes, inlets); + ChunkFeatures features = new ChunkFeatures(reaches, ponds, lakes, inlets, inletBoxes); features.index(settings, chunkX, chunkZ); return features; } - boolean empty() { - return reaches.isEmpty() && ponds.isEmpty() && lakes.isEmpty() && inlets.isEmpty(); + boolean inert() { + return inert; } SeaInlet openInletAt(int x, int z, int[] column) { - for (SeaInlet inlet : inlets) { + for (int index = 0; index < inlets.size(); index++) { + int box = 4 * index; + if (x < inletBoxes[box] || z < inletBoxes[box + 1] || x > inletBoxes[box + 2] || z > inletBoxes[box + 3]) { + continue; + } + SeaInlet inlet = inlets.get(index); if (SeaInletRasterizer.column(inlet, x, z, column) && column[1] == SeaInlet.OPEN) { return inlet; } @@ -179,6 +191,8 @@ final class ChunkFeatures { for (Reach reach : reaches) { segments.addNear(reach, minX - 1f, minZ - 1f, maxX + 1f, maxZ + 1f, outward); } + int riffle = RiffleProfile.RADIUS + 1; + segments.coverChannels(minX - riffle, minZ - riffle, maxX + riffle, maxZ + riffle); HydrologySettings.PondShore shore = settings.ponds().shore(); int reach = Math.max(shore.taper(), ChunkWaterRaster.MAX_SHORE_DISTANCE) + 1; int shelf = shore.shelf() + 1; @@ -192,6 +206,39 @@ final class ChunkFeatures { lakeReach.collect(mask, minX - shoreReach, minZ - shoreReach, maxX + shoreReach, maxZ + shoreReach); System.arraycopy(grownBox(mask), 0, lakeBoxes, 4 * index, 4); } + inert = segments.isEmpty() && !inletsTouch(minX - 1, minZ - 1, maxX + 1, maxZ + 1) + && !pondsTouch(minX - 1 - reach, minZ - 1 - reach, maxX + 1 + reach, maxZ + 1 + reach) + && !lakesTouch(minX - 2 - shoreReach, minZ - 2 - shoreReach, maxX + 2 + shoreReach, maxZ + 2 + shoreReach); + } + + private boolean inletsTouch(int minX, int minZ, int maxX, int maxZ) { + for (int box = 0; box < inletBoxes.length; box += 4) { + if (inletBoxes[box] <= maxX && inletBoxes[box + 2] >= minX && inletBoxes[box + 1] <= maxZ && inletBoxes[box + 3] >= minZ) { + return true; + } + } + return false; + } + + private boolean pondsTouch(int minX, int minZ, int maxX, int maxZ) { + for (Pond pond : ponds) { + PondMask mask = pond.mask(); + int fromX = mask.originX() - (mask.step() >> 1); + int fromZ = mask.originZ() - (mask.step() >> 1); + if (fromX <= maxX && fromX + mask.width() * mask.step() > minX && fromZ <= maxZ && fromZ + mask.depth() * mask.step() > minZ) { + return true; + } + } + return false; + } + + private boolean lakesTouch(int minX, int minZ, int maxX, int maxZ) { + for (int box = 0; box < lakeBoxes.length; box += 4) { + if (lakeBoxes[box] <= maxX && lakeBoxes[box + 2] > minX && lakeBoxes[box + 1] <= maxZ && lakeBoxes[box + 3] > minZ) { + return true; + } + } + return false; } private static Map windowsByKey(int chunkX, int chunkZ, List windows) { diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkRasterizer.java b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkRasterizer.java index 369be027d..9a27928ad 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkRasterizer.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkRasterizer.java @@ -37,7 +37,7 @@ public final class ChunkRasterizer { List lakeRegionPlans) { Objects.requireNonNull(naturalHeight, "naturalHeight"); ChunkFeatures features = ChunkFeatures.gather(settings, chunkX, chunkZ, halo, windows, lakeRegionPlans, lakeRegions(chunkX, chunkZ)); - if (features.empty()) { + if (features.inert()) { return ChunkWaterRaster.EMPTY; } return new RasterComposer(settings, features, chunkX, chunkZ).compose(naturalHeight); diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkWaterRaster.java b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkWaterRaster.java index 524bcef74..02c963754 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkWaterRaster.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkWaterRaster.java @@ -2,6 +2,8 @@ package art.arcane.iris.generation.hydrology.raster; import art.arcane.iris.generation.hydrology.water.HydrologyTier; +import java.util.Arrays; + public final class ChunkWaterRaster { public static final int MAX_SHORE_DISTANCE = 32; public static final ChunkWaterRaster PROVISIONAL = new ChunkWaterRaster(true); @@ -13,10 +15,13 @@ public final class ChunkWaterRaster { private final int[] terrain; private final int[] head; private final byte[] role; - private final HydrologyTier[] tier; - private final long[] featureId; - private final String[] fluid; - private final int[] caveCeiling; + private final byte[] tier; + private final HydrologyTier[] tiers; + private final byte[] feature; + private final long[] features; + private final byte[] fluid; + private final String[] fluids; + private final boolean caveMask; private final byte[] shoreDistance; private ChunkWaterRaster(boolean provisional) { @@ -25,26 +30,32 @@ public final class ChunkWaterRaster { this.head = null; this.role = null; this.tier = null; - this.featureId = null; + this.tiers = null; + this.feature = null; + this.features = null; this.fluid = null; - this.caveCeiling = null; + this.fluids = null; + this.caveMask = false; this.shoreDistance = null; } - ChunkWaterRaster(int[] terrain, int[] head, byte[] role, HydrologyTier[] tier, long[] featureId, String[] fluid, int[] caveCeiling, - byte[] shoreDistance) { - if (terrain.length != COLUMNS || head.length != COLUMNS || role.length != COLUMNS || tier.length != COLUMNS - || featureId.length != COLUMNS || fluid.length != COLUMNS || caveCeiling.length != COLUMNS || shoreDistance.length != COLUMNS) { + ChunkWaterRaster(int[] terrain, int[] head, byte[] role, Palette tiers, LongPalette features, Palette fluids, + boolean caveMask, byte[] shoreDistance) { + if (terrain.length != COLUMNS || head.length != COLUMNS || role.length != COLUMNS || tiers.indices.length != COLUMNS + || features.indices.length != COLUMNS || fluids.indices.length != COLUMNS || shoreDistance.length != COLUMNS) { throw new IllegalArgumentException("A chunk water raster needs " + COLUMNS + " entries per array"); } this.provisional = false; this.terrain = terrain; this.head = head; this.role = role; - this.tier = tier; - this.featureId = featureId; - this.fluid = fluid; - this.caveCeiling = caveCeiling; + this.tier = tiers.indices; + this.tiers = tiers.values(); + this.feature = features.indices; + this.features = features.values(); + this.fluid = fluids.indices; + this.fluids = fluids.values(); + this.caveMask = caveMask; this.shoreDistance = shoreDistance; } @@ -68,22 +79,22 @@ public final class ChunkWaterRaster { public HydrologyTier tier(int lx, int lz) { int index = index(lx, lz); - return tier == null ? null : tier[index]; + return tier == null ? null : tiers[tier[index] & 0xFF]; } public long featureId(int lx, int lz) { int index = index(lx, lz); - return featureId == null ? 0L : featureId[index]; + return feature == null ? 0L : features[feature[index] & 0xFF]; } public String fluid(int lx, int lz) { int index = index(lx, lz); - return fluid == null ? null : fluid[index]; + return fluid == null ? null : fluids[fluid[index] & 0xFF]; } public int caveCeiling(int lx, int lz) { int index = index(lx, lz); - return caveCeiling == null ? Integer.MAX_VALUE : caveCeiling[index]; + return caveMask && head[index] != Integer.MIN_VALUE ? terrain[index] - 1 : Integer.MAX_VALUE; } public int shoreDistance(int lx, int lz) { @@ -105,4 +116,53 @@ public final class ChunkWaterRaster { } return lz << 4 | lx; } + + static final class Palette { + private final byte[] indices = new byte[COLUMNS]; + private final T[] seen; + private int size; + + Palette(T[] seen) { + if (seen.length != COLUMNS) { + throw new IllegalArgumentException("A raster palette needs room for " + COLUMNS + " values"); + } + this.seen = seen; + } + + void set(int column, T value) { + int found = 0; + while (found < size && seen[found] != value) { + found++; + } + if (found == size) { + seen[size++] = value; + } + indices[column] = (byte) found; + } + + private T[] values() { + return Arrays.copyOf(seen, size); + } + } + + static final class LongPalette { + private final byte[] indices = new byte[COLUMNS]; + private final long[] seen = new long[COLUMNS]; + private int size; + + void set(int column, long value) { + int found = 0; + while (found < size && seen[found] != value) { + found++; + } + if (found == size) { + seen[size++] = value; + } + indices[column] = (byte) found; + } + + private long[] values() { + return Arrays.copyOf(seen, size); + } + } } diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/RasterComposer.java b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/RasterComposer.java index 36dec7012..bb377ec85 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/RasterComposer.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/RasterComposer.java @@ -385,34 +385,37 @@ final class RasterComposer { private ChunkWaterRaster pack() { int columns = ChunkWaterRaster.COLUMNS; + boolean affected = false; + for (int lz = 0; lz < CHUNK && !affected; lz++) { + for (int lx = 0; lx < CHUNK && !affected; lx++) { + int cell = (lz + 1) * GRID + lx + 1; + affected = role[cell] != WaterRole.NONE || terrain[cell] != natural[cell] || shore(cell) < ChunkWaterRaster.MAX_SHORE_DISTANCE; + } + } + if (!affected) { + return ChunkWaterRaster.EMPTY; + } int[] terrainOut = new int[columns]; int[] headOut = new int[columns]; byte[] roleOut = new byte[columns]; - HydrologyTier[] tierOut = new HydrologyTier[columns]; - long[] featureOut = new long[columns]; - String[] fluidOut = new String[columns]; - int[] caveOut = new int[columns]; + ChunkWaterRaster.Palette tierOut = new ChunkWaterRaster.Palette<>(new HydrologyTier[columns]); + ChunkWaterRaster.LongPalette featureOut = new ChunkWaterRaster.LongPalette(); + ChunkWaterRaster.Palette fluidOut = new ChunkWaterRaster.Palette<>(new String[columns]); byte[] shoreOut = new byte[columns]; - boolean caveMask = settings.deformation().caveMask(); - boolean affected = false; for (int lz = 0; lz < CHUNK; lz++) { for (int lx = 0; lx < CHUNK; lx++) { int cell = (lz + 1) * GRID + lx + 1; int index = lz << 4 | lx; - int shore = shore(cell); terrainOut[index] = terrain[cell]; headOut[index] = head[cell]; roleOut[index] = (byte) role[cell].ordinal(); - tierOut[index] = tier[cell]; - featureOut[index] = feature[cell]; - fluidOut[index] = fluid[cell]; - caveOut[index] = caveMask && head[cell] != Integer.MIN_VALUE ? terrain[cell] - 1 : Integer.MAX_VALUE; - shoreOut[index] = (byte) shore; - affected |= role[cell] != WaterRole.NONE || terrain[cell] != natural[cell] || shore < ChunkWaterRaster.MAX_SHORE_DISTANCE; + tierOut.set(index, tier[cell]); + featureOut.set(index, feature[cell]); + fluidOut.set(index, fluid[cell]); + shoreOut[index] = (byte) shore(cell); } } - return affected ? new ChunkWaterRaster(terrainOut, headOut, roleOut, tierOut, featureOut, fluidOut, caveOut, shoreOut) - : ChunkWaterRaster.EMPTY; + return new ChunkWaterRaster(terrainOut, headOut, roleOut, tierOut, featureOut, fluidOut, settings.deformation().caveMask(), shoreOut); } private int shore(int cell) { diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ReachSegments.java b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ReachSegments.java index 7661088e5..60bebde2d 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ReachSegments.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ReachSegments.java @@ -6,6 +6,8 @@ import java.util.Arrays; final class ReachSegments { private static final int RUN = 16; + private static final int COVER_SHIFT = 2; + private static final float COVER_MARGIN = 1f; private Reach[] reaches; private int[] segments; @@ -24,6 +26,13 @@ final class ReachSegments { private int[] runBands = new int[8]; private int runs; private boolean sealed; + private boolean covered; + private int coverX; + private int coverZ; + private int coverWidth; + private int coverDepth; + private int[] coverStarts; + private int[] coverEntries; ReachSegments(int capacity) { int initial = Math.max(1, capacity); @@ -71,6 +80,19 @@ final class ReachSegments { } } + boolean isEmpty() { + return size == 0; + } + + void coverChannels(int minX, int minZ, int maxX, int maxZ) { + covered = true; + coverX = minX; + coverZ = minZ; + coverWidth = ((maxX - minX) >> COVER_SHIFT) + 1; + coverDepth = ((maxZ - minZ) >> COVER_SHIFT) + 1; + sealed = false; + } + void nearest(double x, double z, Scope scope, ReachCursor cursor) { if (!sealed) { seal(); @@ -89,6 +111,16 @@ final class ReachSegments { hint(channelHint, x, z, unbounded, cursor); hint(bankHint, x, z, unbounded, cursor); hint(edgeHint, x, z, unbounded, cursor); + int cell = scope == Scope.CHANNEL ? coveredCell(x, z) : -1; + if (cell >= 0) { + for (int entry = coverStarts[cell]; entry < coverStarts[cell + 1]; entry++) { + int index = coverEntries[entry]; + if (index != channelHint && index != bankHint && index != edgeHint && !beyondSegment(index, x, z, scope, cursor)) { + evaluate(index, x, z, false, cursor); + } + } + return; + } int first = 0; for (int run = 0; run < runs; run++) { int end = runEnds[run]; @@ -99,9 +131,7 @@ final class ReachSegments { continue; } for (int index = first; index < end; index++) { - if (!beyond(x, z, Math.min(startX[index], endX[index]), Math.min(startZ[index], endZ[index]), Math.max(startX[index], endX[index]), - Math.max(startZ[index], endZ[index]), limit(radius[index], bands[index], scope, cursor)) - && index != channelHint && index != bankHint && index != edgeHint) { + if (!beyondSegment(index, x, z, scope, cursor) && index != channelHint && index != bankHint && index != edgeHint) { evaluate(index, x, z, unbounded, cursor); } } @@ -109,6 +139,23 @@ final class ReachSegments { } } + private int coveredCell(double x, double z) { + if (coverStarts == null) { + return -1; + } + int cellX = ((int) Math.floor(x) - coverX) >> COVER_SHIFT; + int cellZ = ((int) Math.floor(z) - coverZ) >> COVER_SHIFT; + if (cellX < 0 || cellZ < 0 || cellX >= coverWidth || cellZ >= coverDepth) { + return -1; + } + return cellZ * coverWidth + cellX; + } + + private boolean beyondSegment(int index, double x, double z, Scope scope, ReachCursor cursor) { + return beyond(x, z, Math.min(startX[index], endX[index]), Math.min(startZ[index], endZ[index]), Math.max(startX[index], endX[index]), + Math.max(startZ[index], endZ[index]), limit(radius[index], bands[index], scope, cursor)); + } + private void hint(int index, double x, double z, boolean unbounded, ReachCursor cursor) { if (index >= 0 && index < size) { evaluate(index, x, z, unbounded, cursor); @@ -171,9 +218,56 @@ final class ReachSegments { addRun(start, end); start = end; } + if (covered) { + cover(); + } sealed = true; } + private void cover() { + int cells = coverWidth * coverDepth; + int[] starts = new int[cells + 1]; + int[] range = new int[4]; + for (int index = 0; index < size; index++) { + if (coverRange(index, range)) { + for (int cellZ = range[1]; cellZ <= range[3]; cellZ++) { + for (int cellX = range[0]; cellX <= range[2]; cellX++) { + starts[cellZ * coverWidth + cellX + 1]++; + } + } + } + } + for (int cell = 0; cell < cells; cell++) { + starts[cell + 1] += starts[cell]; + } + int[] entries = new int[starts[cells]]; + int[] fill = Arrays.copyOf(starts, cells); + for (int index = 0; index < size; index++) { + if (coverRange(index, range)) { + for (int cellZ = range[1]; cellZ <= range[3]; cellZ++) { + for (int cellX = range[0]; cellX <= range[2]; cellX++) { + entries[fill[cellZ * coverWidth + cellX]++] = index; + } + } + } + } + coverStarts = starts; + coverEntries = entries; + } + + private boolean coverRange(int index, int[] range) { + float reach = radius[index] + COVER_MARGIN; + range[0] = Math.max(0, coverCell(Math.min(startX[index], endX[index]) - reach, coverX)); + range[1] = Math.max(0, coverCell(Math.min(startZ[index], endZ[index]) - reach, coverZ)); + range[2] = Math.min(coverWidth - 1, coverCell(Math.max(startX[index], endX[index]) + reach, coverX)); + range[3] = Math.min(coverDepth - 1, coverCell(Math.max(startZ[index], endZ[index]) + reach, coverZ)); + return range[0] <= range[2] && range[1] <= range[3]; + } + + private static int coverCell(float block, int origin) { + return ((int) Math.floor(block) - origin) >> COVER_SHIFT; + } + private void addRun(int start, int end) { if (runs == runEnds.length) { runEnds = Arrays.copyOf(runEnds, 2 * runs); diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/RiffleProfile.java b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/RiffleProfile.java index 75587a877..d56581969 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/RiffleProfile.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/RiffleProfile.java @@ -15,6 +15,7 @@ final class RiffleProfile { private static final int LIMIT = 10 * RADIUS; private static final int DRY = Integer.MIN_VALUE; private static final int[] SIDES = {1, -1, GRID, -GRID}; + private static final int[] REMAINING = remaining(); private final List fields = new ArrayList<>(); @@ -85,6 +86,20 @@ final class RiffleProfile { return (int) Math.max(original, Math.min(head - 1L, Math.round(raised))); } + private static int[] remaining() { + int[] remaining = new int[GRID * GRID]; + int low = PADDING - 1; + int high = PADDING + CHUNK; + for (int z = 0; z < GRID; z++) { + for (int x = 0; x < GRID; x++) { + int outsideX = Math.max(0, Math.max(low - x, x - high)); + int outsideZ = Math.max(0, Math.max(low - z, z - high)); + remaining[z * GRID + x] = 10 * Math.max(outsideX, outsideZ); + } + } + return remaining; + } + private record Field(int crest, int[] distances) { } @@ -107,6 +122,9 @@ final class RiffleProfile { } private void offer(int cell, int distance) { + if (distance + REMAINING[cell] >= LIMIT) { + return; + } if (distances[cell] < LIMIT) { remove(cell); } diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/underground/CavePoolFiller.java b/core/src/main/java/art/arcane/iris/generation/hydrology/underground/CavePoolFiller.java index d1bf23062..fb303b98c 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/underground/CavePoolFiller.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/underground/CavePoolFiller.java @@ -10,6 +10,7 @@ import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import java.util.Arrays; import java.util.Set; +import java.util.function.Supplier; public final class CavePoolFiller { public static final int REACH = HydrologySettings.MAX_CAVE_POOL_RADIUS + 1; @@ -40,15 +41,19 @@ public final class CavePoolFiller { this.density = settings.density(); } - public void fill(int chunkX, int chunkZ, CarvedTerrain carved, MantleWriter writer, long seed) { + public void fill(int chunkX, int chunkZ, Supplier terrain, MantleWriter writer, long seed) { if (!pools.enabled() || density == 0f) { return; } int attempts = attempts(seed, chunkX, chunkZ); + CarvedTerrain carved = null; for (int attempt = 0; attempt < attempts; attempt++) { if (density < 1f && CellHash.unit(CellHash.mix(seed, SALT_DENSITY + attempt, chunkX, chunkZ)) >= density) { continue; } + if (carved == null) { + carved = terrain.get(); + } long column = CellHash.mix(seed, SALT_COLUMN + attempt, chunkX, chunkZ); int x = (chunkX << 4) + (int) (column & 15); int z = (chunkZ << 4) + (int) ((column >>> 4) & 15); diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/underground/SeaInletPlanner.java b/core/src/main/java/art/arcane/iris/generation/hydrology/underground/SeaInletPlanner.java index 1ad8dea8a..be18dcdbe 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/underground/SeaInletPlanner.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/underground/SeaInletPlanner.java @@ -33,6 +33,7 @@ public final class SeaInletPlanner { private static final long SALT_HEADROOM = 0x53454148454144L; private static final long SALT_DEPTH = 0x53454144455054L; private static final long SALT_DENSITY = 0x53454144454e53L; + private static final double SQUARED_MARGIN = 1e-9D; private final HydrologySettings settings; @@ -403,22 +404,36 @@ public final class SeaInletPlanner { double dx = bx - ax; double dz = bz - az; double span = dx * dx + dz * dz; + double outside = radius > 0f ? squaredAbove(radius) : Double.POSITIVE_INFINITY; + double inside = radius > 0f ? squaredBelow(radius) : Double.NEGATIVE_INFINITY; + double wetOutside = wetRadius > 0f ? squaredAbove(wetRadius) : Double.POSITIVE_INFINITY; + double wetInside = wetRadius > 0f ? squaredBelow(wetRadius) : Double.NEGATIVE_INFINITY; for (int j = minJ; j <= maxJ; j++) { double pz = grid.originZ() + (double) j * cell; for (int i = minI; i <= maxI; i++) { double px = grid.originX() + (double) i * cell; double t = span <= 0D ? 0D : Math.clamp(((px - ax) * dx + (pz - az) * dz) / span, 0D, 1D); - double distance = Math.hypot(px - (ax + t * dx), pz - (az + t * dz)); - if (distance > radius) { + double ex = px - (ax + t * dx); + double ez = pz - (az + t * dz); + double squared = ex * ex + ez * ez; + if (squared > outside || !(squared < inside) && Math.hypot(ex, ez) > radius) { continue; } int index = j * grid.width() + i; cover[index] = Math.min(cover[index], bed); - wet[index] |= distance <= wetRadius; + wet[index] |= squared < wetInside || !(squared > wetOutside) && Math.hypot(ex, ez) <= wetRadius; } } } + private static double squaredAbove(float radius) { + return (double) radius * radius * (1D + SQUARED_MARGIN); + } + + private static double squaredBelow(float radius) { + return (double) radius * radius * (1D - SQUARED_MARGIN); + } + private static float pathLength(DemGrid grid, List path) { float length = 0f; for (int station = 1; station < path.size(); station++) { diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/MantleHydrologyComponent.java b/core/src/main/java/art/arcane/iris/generation/mantle/MantleHydrologyComponent.java index 5c3397042..4ac0b4c3d 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/MantleHydrologyComponent.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/MantleHydrologyComponent.java @@ -85,8 +85,9 @@ public final class MantleHydrologyComponent extends IrisMantleComponent { generateFalls(writer, complex, runtime, own, chunkX, chunkZ); if (poolsActive()) { HydrologySettings settings = runtime.settings(); - new CavePoolFiller(settings).fill(chunkX, chunkZ, - new MantleCarvedTerrain(writer, complex, getEngineMantle().getEngine(), chunkX, chunkZ), writer, settings.seed()); + Engine engine = getEngineMantle().getEngine(); + new CavePoolFiller(settings).fill(chunkX, chunkZ, () -> new MantleCarvedTerrain(writer, complex, engine, chunkX, chunkZ), writer, + settings.seed()); } } diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java b/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java index 4bb51ddaa..acec89aa9 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java @@ -91,7 +91,6 @@ import art.arcane.volmlib.util.interpolation.NoiseBoundsProvider; import art.arcane.volmlib.util.noise.CNG; import art.arcane.volmlib.util.stream.ProceduralStream; import art.arcane.volmlib.util.stream.interpolation.Interpolated; -import it.unimi.dsi.fastutil.longs.Long2DoubleOpenHashMap; import lombok.AccessLevel; import lombok.Data; import lombok.EqualsAndHashCode; @@ -120,10 +119,11 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiFunction; @Data -@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "sharedBoundsBiomes", "terrainProgram", "bakeCache", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyRuntime", "shoreBiomes", "waterVegetation", "effectiveChunks"}) -@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "sharedBoundsBiomes", "terrainProgram", "bakeCache", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyRuntime", "shoreBiomes", "waterVegetation", "effectiveChunks"}) +@EqualsAndHashCode(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "sharedBaseHeights", "sharedBoundsBiomes", "regionBounds", "contextRegionStream", "terrainProgram", "bakeCache", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyRuntime", "shoreBiomes", "waterVegetation", "effectiveChunks"}) +@ToString(exclude = {"data", "gridBoundsCache", "sharedCornerBounds", "sharedBaseHeights", "sharedBoundsBiomes", "regionBounds", "contextRegionStream", "terrainProgram", "bakeCache", "regionSource", "baseBiomeSource", "caveSource", "rockSource", "fluidSource", "childStyleSeed", "inferredByType", "naturalHeightSource", "naturalTrueBiomeSource", "trueBiomeSource", "frozenInterpolators", "frozenGenerators", "inferredBiomeStreams", "imageMapRuntime", "terrainEngine", "terrain3D", "hydrologyRuntime", "shoreBiomes", "waterVegetation", "effectiveChunks"}) public class IrisComplex implements DataProvider { private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D); + private static final long NEGATIVE_ZERO_BITS = Double.doubleToRawLongBits(-0D); private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L); private static final int GRID_BOUNDS_CACHE_SIZE = 8192; private static final int STUDIO_NOISE_CACHE_SIZE = 32_768; @@ -146,7 +146,6 @@ public class IrisComplex implements DataProvider { Comparator.nullsFirst(Comparator.naturalOrder()) ); private static final ThreadLocal BATCH_SCRATCH = ThreadLocal.withInitial(TerrainBatchScratch::new); - private static final ThreadLocal HYDROLOGY_BASE_HEIGHTS = new ThreadLocal<>(); private static final InferredType[] INFERRED_BIOME_PREPARATION_ORDER = { InferredType.LAND, InferredType.CAVE, @@ -160,6 +159,7 @@ public class IrisComplex implements DataProvider { @Getter(AccessLevel.NONE) private final int biomeBoundsSamplingStep; private transient volatile SharedCornerBounds sharedCornerBounds; + private transient volatile SharedBaseHeights sharedBaseHeights; private transient volatile SharedBoundsBiomes sharedBoundsBiomes; @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) @@ -259,6 +259,10 @@ public class IrisComplex implements DataProvider { private IrisBiome focusBiome; private IrisRegion focusRegion; private Map> generatorBounds; + @Getter(AccessLevel.NONE) + private final transient RegionBounds regionBounds; + @Getter(AccessLevel.NONE) + private final transient ProceduralStream contextRegionStream; private Set generatorBiomes; @Setter(AccessLevel.NONE) private final transient HydrologyRuntime hydrologyRuntime; @@ -386,6 +390,9 @@ public class IrisComplex implements DataProvider { frozenGenerators[frozenIndex] = generatorGroups[frozenIndex].generators(); } generatorBounds = buildGeneratorBounds(engine); + regionBounds = focusedBiomes == null && !imageMapRuntime.has(IrisImageMapApplication.BIOME) + ? RegionBounds.compile(preparedRegions, frozenInterpolators.length, new BoundsReach(), this::groupBounds) + : null; biomeBuffetGenerators = compileBuffetGeneratorGroups(); chunkHeightBatch = engine.getDimension().getTerrainSamplingStep() == 1 && biomeBuffetGenerators.isEmpty() && (biomeBuffet == null || biomeBuffet.cells().isEmpty()); @@ -417,20 +424,21 @@ public class IrisComplex implements DataProvider { : RegionSelection.select(new RegionSelection.Options( engine.getDimension(), data, regionStyleStream, imageMapRuntime, null)); regionStream = focusedRegions != null ? focusedRegions : new BakedStream<>(bakeCache, BakePlane.REGION, regionSource); - caveSource = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)) + contextRegionStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)); + caveSource = contextRegionStream .convert((r) -> createInferredBiomeStream(r, InferredType.CAVE)) .convertAware2D(ProceduralStream::get); caveBiomeStream = new BakedStream<>(bakeCache, BakePlane.CAVE_BIOME, caveSource); inferredStreams.put(InferredType.CAVE, caveBiomeStream); - landBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)) + landBiomeStream = contextRegionStream .convert((r) -> createInferredBiomeStream(r, InferredType.LAND)) .convertAware2D(ProceduralStream::get); inferredStreams.put(InferredType.LAND, landBiomeStream); - seaBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)) + seaBiomeStream = contextRegionStream .convert((r) -> createInferredBiomeStream(r, InferredType.SEA)) .convertAware2D(ProceduralStream::get); inferredStreams.put(InferredType.SEA, seaBiomeStream); - shoreBiomeStream = GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)) + shoreBiomeStream = contextRegionStream .convert((r) -> createInferredBiomeStream(r, InferredType.SHORE)) .convertAware2D(ProceduralStream::get); inferredStreams.put(InferredType.SHORE, shoreBiomeStream); @@ -812,17 +820,35 @@ public class IrisComplex implements DataProvider { } return; } + SharedBaseHeights memo = sharedBaseHeights(); TerrainBatchScratch scratch = BATCH_SCRATCH.get(); int mark = scratch.mark(); int indexMark = scratch.indexMark(); try { double[] batchXs = scratch.buffer(count); double[] batchZs = scratch.buffer(count); + double[] computed = scratch.buffer(count); + int[] slots = scratch.indices(count); + int missing = 0; for (int index = 0; index < count; index++) { - batchXs[index] = xs[from + index]; - batchZs[index] = zs[from + index]; + int x = xs[from + index]; + int z = zs[from + index]; + double known = memo.get(x, z); + if (Double.isNaN(known)) { + batchXs[missing] = x; + batchZs[missing] = z; + slots[missing++] = index; + } else { + heights[index] = known; + } + } + if (missing > 0) { + batchBaseHeights(scratch, batchXs, batchZs, missing, computed); + for (int index = 0; index < missing; index++) { + heights[slots[index]] = computed[index]; + memo.put((int) batchXs[index], (int) batchZs[index], computed[index]); + } } - batchBaseHeights(scratch, batchXs, batchZs, count, heights); } finally { scratch.reset(mark); scratch.resetIndices(indexMark); @@ -830,23 +856,9 @@ public class IrisComplex implements DataProvider { if (baseOnly || terrain3D == null && transitionDisplacement == null) { return; } - Long2DoubleOpenHashMap known = new Long2DoubleOpenHashMap(count); - known.defaultReturnValue(Double.NaN); for (int index = 0; index < count; index++) { - known.put(hydrologyColumnKey(xs[from + index], zs[from + index]), heights[index]); + heights[index] = naturalHeightStream.getDouble(xs[from + index], zs[from + index]); } - HYDROLOGY_BASE_HEIGHTS.set(new HydrologyBaseHeights(this, known)); - try { - for (int index = 0; index < count; index++) { - heights[index] = naturalHeightStream.getDouble(xs[from + index], zs[from + index]); - } - } finally { - HYDROLOGY_BASE_HEIGHTS.remove(); - } - } - - private static long hydrologyColumnKey(int x, int z) { - return ((long) x << 32) | (z & 0xFFFFFFFFL); } private void bakeBaseHeights(int chunkX, int chunkZ, long[] missing, double[] values) { @@ -1082,15 +1094,21 @@ public class IrisComplex implements DataProvider { } private double sampleUnblendedNaturalTerrainHeight(double x, double z) { - HydrologyBaseHeights batched = HYDROLOGY_BASE_HEIGHTS.get(); - if (batched != null && batched.owner() == this && (int) x == x && (int) z == z) { - double known = batched.heights().get(hydrologyColumnKey((int) x, (int) z)); - if (!Double.isNaN(known)) { - return known; - } + if (!memoColumn(x) || !memoColumn(z)) { + return imageMapRuntime.sampleTerrainHeight(x, z, proceduralTerrainHeight.sample(x, z)); } - double proceduralHeight = proceduralTerrainHeight.sample(x, z); - return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight); + SharedBaseHeights memo = sharedBaseHeights(); + double known = memo.get((int) x, (int) z); + if (!Double.isNaN(known)) { + return known; + } + double height = imageMapRuntime.sampleTerrainHeight(x, z, proceduralTerrainHeight.sample(x, z)); + memo.put((int) x, (int) z, height); + return height; + } + + private static boolean memoColumn(double coordinate) { + return (int) coordinate == coordinate && Double.doubleToRawLongBits(coordinate) != NEGATIVE_ZERO_BITS; } static double calculateNaturalSlope(double naturalHeight, double easternHeight, double southernHeight) { @@ -1885,7 +1903,7 @@ public class IrisComplex implements DataProvider { int grid = biomeBoundsSamplingStep; GridBoundsCache cache = gridBoundsCache.get(); if (grid <= 1) { - return sampleBoundsRaw(cache, engine, interpolator, generators, x, z); + return sampleBoundsRaw(cache, engine, interpolator, interpolatorIndex, generators, x, z); } int xi = (int) Math.floor(x); @@ -1941,7 +1959,7 @@ public class IrisComplex implements DataProvider { if (GenerationCounter.ENABLED) { GenerationCounter.CORNER_STARCASTS.increment(); } - packed = computePackedBounds(cache, engine, interpolator, generators, gx, gz); + packed = computePackedBounds(cache, engine, interpolator, interpolatorIndex, generators, gx, gz); if (!Double.isFinite(boundsLow(packed)) || !Double.isFinite(boundsHigh(packed))) { // A non-finite corner is a transient sampling fault, not a fact about the terrain: // hand it back uncached so the next sample recomputes it instead of pinning it. @@ -1972,6 +1990,20 @@ public class IrisComplex implements DataProvider { return shared; } + private SharedBaseHeights sharedBaseHeights() { + SharedBaseHeights shared = sharedBaseHeights; + if (shared == null) { + synchronized (this) { + shared = sharedBaseHeights; + if (shared == null) { + shared = new SharedBaseHeights(SharedBaseHeights.capacityFor(Runtime.getRuntime().maxMemory())); + sharedBaseHeights = shared; + } + } + } + return shared; + } + private SharedBoundsBiomes sharedBoundsBiomes(ProceduralStream source) { SharedBoundsBiomes shared = sharedBoundsBiomes; if (shared != null && shared.source() == source) { @@ -1987,8 +2019,9 @@ public class IrisComplex implements DataProvider { } } - private long computePackedBounds(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, IrisGenerator[] generators, int gx, int gz) { - NoiseBounds bounds = sampleBoundsRaw(cache, engine, interpolator, generators, gx, gz); + private long computePackedBounds(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, int interpolatorIndex, + IrisGenerator[] generators, int gx, int gz) { + NoiseBounds bounds = sampleBoundsRaw(cache, engine, interpolator, interpolatorIndex, generators, gx, gz); return (((long) Float.floatToRawIntBits((float) bounds.min())) << 32) | (Float.floatToRawIntBits((float) bounds.max()) & 0xFFFFFFFFL); } @@ -2006,10 +2039,11 @@ public class IrisComplex implements DataProvider { return a + ((b - a) * fz); } - private NoiseBounds sampleBoundsRaw(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, IrisGenerator[] generators, double x, double z) { + private NoiseBounds sampleBoundsRaw(GridBoundsCache cache, Engine engine, IrisInterpolator interpolator, int interpolatorIndex, + IrisGenerator[] generators, double x, double z) { IdentityHashMap cachedBounds = generatorBounds.get(interpolator); BoundsSampler sampler = cache.sampler.isInUse() ? new BoundsSampler() : cache.sampler; - sampler.bind(this, engine, generators, cachedBounds); + sampler.bind(this, engine, interpolatorIndex, generators, cachedBounds); try { return interpolator.interpolateBounds(x, z, sampler); @@ -2297,6 +2331,11 @@ public class IrisComplex implements DataProvider { return reached; } + private NoiseBounds groupBounds(int group, IrisBiome biome) { + GeneratorBounds cached = generatorBounds.get(frozenInterpolators[group]).get(biome); + return (cached == null ? computeGeneratorBounds(frozenGenerators[group], biome) : cached).noiseBounds; + } + static double averageGeneratorHeights( IrisGenerator[] generators, double low, @@ -2604,6 +2643,32 @@ public class IrisComplex implements DataProvider { } } + private final class BoundsReach implements RegionBounds.Reach { + @Override + public List picks(IrisRegion region, InferredType type) { + return loadInferredBiomes(biomeKeys(region, type), type); + } + + @Override + public Collection descendants(IrisBiome biome) { + Set reached = Collections.newSetFromMap(new IdentityHashMap<>()); + ArrayDeque pending = new ArrayDeque<>(); + pending.add(biome); + while (!pending.isEmpty()) { + IrisBiome next = pending.poll(); + if (!reached.add(next)) { + continue; + } + for (IrisBiome child : next.getRealChildren(IrisComplex.this)) { + if (child != null) { + pending.add(child); + } + } + } + return reached; + } + } + /** * 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} @@ -2619,9 +2684,12 @@ public class IrisComplex implements DataProvider { private final IdentityHashMap localBounds = new IdentityHashMap<>(8); private IrisComplex complex; private Engine engine; + private int group; private IrisGenerator[] generators; private IdentityHashMap cachedBounds; private SharedBoundsBiomes sharedBiomes; + private BakedStream bakedBiomes; + private RegionBounds regional; private boolean integerCoordinates; private boolean inUse; @@ -2629,14 +2697,19 @@ public class IrisComplex implements DataProvider { return inUse; } - private void bind(IrisComplex complex, Engine engine, IrisGenerator[] generators, IdentityHashMap cachedBounds) { + private void bind(IrisComplex complex, Engine engine, int group, IrisGenerator[] generators, + IdentityHashMap cachedBounds) { this.complex = complex; this.engine = engine; + this.group = group; this.generators = generators; this.cachedBounds = cachedBounds; this.integerCoordinates = complex.baseBiomeStream instanceof BakedStream || complex.baseBiomeStream instanceof CachedStream2D; this.sharedBiomes = integerCoordinates ? complex.sharedBoundsBiomes(complex.baseBiomeStream) : null; + this.bakedBiomes = complex.regionBounds != null && complex.baseBiomeStream instanceof BakedStream baked + ? baked : null; + this.regional = bakedBiomes == null ? null : complex.regionBounds; this.inUse = true; if (sampleSource.get() != complex.baseBiomeStream) { sampleCache.clear(); @@ -2654,6 +2727,8 @@ public class IrisComplex implements DataProvider { generators = null; cachedBounds = null; sharedBiomes = null; + bakedBiomes = null; + regional = null; inUse = false; } @@ -2662,25 +2737,23 @@ public class IrisComplex implements DataProvider { try { double sampleX = integerCoordinates ? (int) xx : xx; double sampleZ = integerCoordinates ? (int) zz : zz; - IrisBiome bx = sampleCache.get(sampleX, sampleZ); - if (bx == null) { - bx = sharedBiomes == null ? null : sharedBiomes.get((int) sampleX, (int) sampleZ); - if (bx == null) { - ChunkBakeCache.enterSparse(); - try { - bx = complex.baseBiomeStream.get(xx, zz); - } finally { - ChunkBakeCache.exitSparse(); - } - if (sharedBiomes != null) { - sharedBiomes.put((int) sampleX, (int) sampleZ, bx); - } + Object known = sampleCache.get(sampleX, sampleZ); + if (known == null && sharedBiomes != null) { + known = sharedBiomes.get((int) sampleX, (int) sampleZ); + if (known != null) { + sampleCache.put(sampleX, sampleZ, known); } - sampleCache.put(sampleX, sampleZ, bx); } - - GeneratorBounds bounds = complex.resolveGeneratorBounds(engine, generators, bx, cachedBounds, localBounds); - return bounds.noiseBounds; + if (known instanceof IrisBiome biome) { + return bounds(biome); + } + ChunkBakeCache.enterSparse(); + try { + return regional == null ? bounds(remember(sampleX, sampleZ, complex.baseBiomeStream.get(xx, zz))) + : regionalBounds(known, sampleX, sampleZ); + } finally { + ChunkBakeCache.exitSparse(); + } } catch (Throwable e) { GenerationFailures.rethrowEngineFailure(e); long now = System.currentTimeMillis(); @@ -2696,6 +2769,49 @@ public class IrisComplex implements DataProvider { return ZERO_NOISE_BOUNDS; } + + private NoiseBounds regionalBounds(Object known, double x, double z) { + RegionBounds.Pick pick = known instanceof RegionBounds.Pick picked ? picked : null; + IrisRegion region = pick != null ? pick.region() : known instanceof IrisRegion cached ? cached : null; + if (region == null) { + IrisBiome baked = bakedBiomes.baked((int) x, (int) z); + if (baked != null) { + return bounds(remember(x, z, baked)); + } + region = remember(x, z, complex.contextRegionStream.get(x, z)); + } + RegionBounds.Constants constants = regional.of(region); + NoiseBounds constant = constants == null ? null : constants.any(group); + if (constant != null) { + return constant; + } + InferredType type = pick != null ? pick.type() : complex.bridgeStream.get(x, z); + constant = constants == null ? null : constants.typed(type, group); + if (constant != null) { + return constant; + } + IrisBiome picked = pick != null ? pick.biome() : complex.createInferredBiomeStream(region, type).get(x, z); + constant = regional.pick(picked, group); + if (constant != null) { + if (pick == null) { + remember(x, z, new RegionBounds.Pick(region, type, picked)); + } + return constant; + } + return bounds(remember(x, z, complex.implode(picked, x, z))); + } + + private NoiseBounds bounds(IrisBiome biome) { + return complex.resolveGeneratorBounds(engine, generators, biome, cachedBounds, localBounds).noiseBounds; + } + + private T remember(double x, double z, T sample) { + if (sharedBiomes != null) { + sharedBiomes.put((int) x, (int) z, sample); + } + sampleCache.put(x, z, sample); + return sample; + } } /** @@ -2705,13 +2821,13 @@ public class IrisComplex implements DataProvider { private static final class CoordinateBiomeCache { private final long[] xBits; private final long[] zBits; - private final IrisBiome[] values; + private final Object[] values; private final int mask; private CoordinateBiomeCache(int capacity) { xBits = new long[capacity]; zBits = new long[capacity]; - values = new IrisBiome[capacity]; + values = new Object[capacity]; mask = capacity - 1; } @@ -2719,20 +2835,20 @@ public class IrisComplex implements DataProvider { Arrays.fill(values, null); } - private IrisBiome get(double x, double z) { + private Object get(double x, double z) { 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) { + private void put(double x, double z, Object sample) { long xb = Double.doubleToLongBits(x); long zb = Double.doubleToLongBits(z); int slot = mix(xb, zb) & mask; xBits[slot] = xb; zBits[slot] = zb; - values[slot] = biome; + values[slot] = sample; } private int mix(long xb, long zb) { @@ -2784,7 +2900,4 @@ public class IrisComplex implements DataProvider { transitionDisplacement.clear(); } } - - private record HydrologyBaseHeights(IrisComplex owner, Long2DoubleOpenHashMap heights) { - } } diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/RegionBounds.java b/core/src/main/java/art/arcane/iris/generation/runtime/RegionBounds.java new file mode 100644 index 000000000..4fff46022 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/runtime/RegionBounds.java @@ -0,0 +1,120 @@ +package art.arcane.iris.generation.runtime; + +import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.iris.generation.terrain.InferredType; +import art.arcane.iris.generation.terrain.IrisRegion; +import art.arcane.volmlib.util.interpolation.NoiseBounds; + +import java.util.Collection; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +final class RegionBounds { + private final Map regions; + private final Map picks; + + private RegionBounds(Map regions, Map picks) { + this.regions = regions; + this.picks = picks; + } + + static RegionBounds compile(Collection regions, int groups, Reach reach, GroupBounds bounds) { + IdentityHashMap compiled = new IdentityHashMap<>(Math.max(16, regions.size() * 2)); + IdentityHashMap picks = new IdentityHashMap<>(); + for (IrisRegion region : regions) { + if (compiled.containsKey(region)) { + continue; + } + NoiseBounds[] land = typeConstants(reach.picks(region, InferredType.LAND), groups, reach, bounds, picks); + NoiseBounds[] sea = typeConstants(reach.picks(region, InferredType.SEA), groups, reach, bounds, picks); + NoiseBounds[] any = new NoiseBounds[groups]; + for (int group = 0; group < groups; group++) { + any[group] = land[group] != null && sea[group] != null && sameBits(land[group], sea[group]) ? land[group] : null; + } + compiled.put(region, new Constants(any, land, sea)); + } + return new RegionBounds(compiled, picks); + } + + Constants of(IrisRegion region) { + return regions.get(region); + } + + NoiseBounds pick(IrisBiome picked, int group) { + NoiseBounds[] constants = picks.get(picked); + return constants == null ? null : constants[group]; + } + + private static NoiseBounds[] typeConstants(List picked, int groups, Reach reach, GroupBounds bounds, + Map picks) { + if (picked.isEmpty()) { + return new NoiseBounds[groups]; + } + Set reached = Collections.newSetFromMap(new IdentityHashMap<>()); + for (IrisBiome biome : picked) { + Collection descendants = reach.descendants(biome); + reached.addAll(descendants); + if (!picks.containsKey(biome)) { + picks.put(biome, constants(descendants, groups, bounds)); + } + } + return constants(reached, groups, bounds); + } + + private static NoiseBounds[] constants(Collection biomes, int groups, GroupBounds bounds) { + NoiseBounds[] constants = new NoiseBounds[groups]; + for (int group = 0; group < groups; group++) { + constants[group] = constant(biomes, group, bounds); + } + return constants; + } + + private static NoiseBounds constant(Collection biomes, int group, GroupBounds bounds) { + NoiseBounds first = null; + for (IrisBiome biome : biomes) { + NoiseBounds next = bounds.of(group, biome); + if (first == null) { + first = next; + } else if (!sameBits(first, next)) { + return null; + } + } + return first; + } + + private static boolean sameBits(NoiseBounds first, NoiseBounds second) { + return Double.doubleToRawLongBits(first.min()) == Double.doubleToRawLongBits(second.min()) + && Double.doubleToRawLongBits(first.max()) == Double.doubleToRawLongBits(second.max()); + } + + record Constants(NoiseBounds[] any, NoiseBounds[] land, NoiseBounds[] sea) { + NoiseBounds any(int group) { + return any[group]; + } + + NoiseBounds typed(InferredType type, int group) { + return switch (type) { + case LAND -> land[group]; + case SEA -> sea[group]; + case SHORE, CAVE -> null; + }; + } + } + + record Pick(IrisRegion region, InferredType type, IrisBiome biome) { + } + + interface Reach { + List picks(IrisRegion region, InferredType type); + + Collection descendants(IrisBiome biome); + } + + @FunctionalInterface + interface GroupBounds { + NoiseBounds of(int group, IrisBiome biome); + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/SharedBaseHeights.java b/core/src/main/java/art/arcane/iris/generation/runtime/SharedBaseHeights.java new file mode 100644 index 000000000..9ee1db996 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/runtime/SharedBaseHeights.java @@ -0,0 +1,76 @@ +package art.arcane.iris.generation.runtime; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; + +final class SharedBaseHeights { + static final int MINIMUM_CAPACITY = 1 << 16; + static final int MAXIMUM_CAPACITY = 1 << 21; + static final long BYTES_PER_ENTRY = 20L; + static final long HEAP_SHARE = 256L; + private static final VarHandle INTS = MethodHandles.arrayElementVarHandle(int[].class); + private static final VarHandle LONGS = MethodHandles.arrayElementVarHandle(long[].class); + + private final long[] keys; + private final long[] values; + private final int[] stamps; + private final int mask; + + SharedBaseHeights(int capacity) { + if (capacity < 2 || Integer.bitCount(capacity) != 1) { + throw new IllegalArgumentException("capacity must be a power of two of at least 2"); + } + this.keys = new long[capacity]; + this.values = new long[capacity]; + this.stamps = new int[capacity]; + this.mask = capacity - 1; + } + + static int capacityFor(long maxMemory) { + long entries = Math.max(0L, maxMemory) / HEAP_SHARE / BYTES_PER_ENTRY; + long bounded = Math.max(MINIMUM_CAPACITY, Math.min(MAXIMUM_CAPACITY, entries)); + return Integer.highestOneBit((int) bounded); + } + + double get(int x, int z) { + long key = key(x, z); + int slot = slot(key); + int before = (int) INTS.getAcquire(stamps, slot); + if (before == 0 || (before & 1) != 0) { + return Double.NaN; + } + long storedKey = (long) LONGS.getAcquire(keys, slot); + long value = (long) LONGS.getAcquire(values, slot); + VarHandle.loadLoadFence(); + int after = (int) INTS.getAcquire(stamps, slot); + if (before != after || storedKey != key) { + return Double.NaN; + } + return Double.longBitsToDouble(value); + } + + void put(int x, int z, double height) { + if (!Double.isFinite(height)) { + return; + } + long key = key(x, z); + int slot = slot(key); + int stamp = (int) INTS.getAcquire(stamps, slot); + if ((stamp & 1) != 0 || !INTS.compareAndSet(stamps, slot, stamp, stamp + 1)) { + return; + } + LONGS.setRelease(keys, slot, key); + LONGS.setRelease(values, slot, Double.doubleToRawLongBits(height)); + INTS.setRelease(stamps, slot, stamp + 2); + } + + private int slot(long key) { + long mixed = (key ^ (key >>> 30)) * 0xBF58476D1CE4E5B9L; + mixed = (mixed ^ (mixed >>> 27)) * 0x94D049BB133111EBL; + return (int) ((mixed ^ (mixed >>> 31)) & mask); + } + + private static long key(int x, int z) { + return ((long) x << 32) | (z & 0xFFFFFFFFL); + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/SharedBoundsBiomes.java b/core/src/main/java/art/arcane/iris/generation/runtime/SharedBoundsBiomes.java index 3c3fa5ee4..2ebcc725a 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/SharedBoundsBiomes.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/SharedBoundsBiomes.java @@ -1,7 +1,5 @@ package art.arcane.iris.generation.runtime; -import art.arcane.iris.generation.biome.IrisBiome; - import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; import java.util.Objects; @@ -49,7 +47,7 @@ final class SharedBoundsBiomes { return (int) ((mixed ^ (mixed >>> 31)) & mask); } - IrisBiome get(int x, int z) { + Object get(int x, int z) { long key = key(x, z); int slot = slot(x, z); int before = (int) INTS.getAcquire(stamps, slot); @@ -63,11 +61,11 @@ final class SharedBoundsBiomes { if (before != after || storedKey != key) { return null; } - return (IrisBiome) value; + return value; } - void put(int x, int z, IrisBiome biome) { - if (biome == null) { + void put(int x, int z, Object sample) { + if (sample == null) { return; } int slot = slot(x, z); @@ -76,7 +74,7 @@ final class SharedBoundsBiomes { return; } LONGS.setRelease(keys, slot, key(x, z)); - OBJECTS.setRelease(values, slot, biome); + OBJECTS.setRelease(values, slot, sample); INTS.setRelease(stamps, slot, stamp + 2); } diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java index 62bed2da5..dc69b9b06 100644 --- a/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java +++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java @@ -49,6 +49,7 @@ import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell; import art.arcane.iris.generation.hydrology.cave.HydrologyCaveStorage; import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster; import art.arcane.iris.generation.chunk.ColumnExtent; +import art.arcane.iris.generation.chunk.ColumnReadHunk; import art.arcane.iris.generation.context.ChunkContext; import art.arcane.iris.generation.context.IrisContext; import art.arcane.iris.generation.block.B; @@ -1086,19 +1087,21 @@ public class IrisCarveModifier extends EngineAssignedModifier int height = output.getHeight(); ColumnExtent extent = output instanceof ColumnExtent columnExtent ? columnExtent : null; SolidMemo solids = new SolidMemo(); + ColumnWindow column = new ColumnWindow(output, height); for (int localX = 0; localX < width; localX++) { for (int localZ = 0; localZ < depth; localZ++) { int worldX = blockX + localX; int worldZ = blockZ + localZ; int floor = -1; int top = extent == null ? height - 1 : extent.highestStoredY(localX, localZ); + column.begin(localX, localZ, top); boolean previousKnown = false; boolean previousSolid = false; for (int y = 1; y < height; y++) { if (y > top) { break; } - boolean solid = solids.test(output.getRaw(localX, y, localZ)); + boolean solid = solids.test(column.get(y)); boolean decorated = false; if (solid) { if (floor >= 0) { @@ -1122,11 +1125,12 @@ public class IrisCarveModifier extends EngineAssignedModifier if (extent != null) { top = extent.highestStoredY(localX, localZ); } + column.begin(localX, localZ, top); } floor = -1; } } else if (floor < 0 && (previousKnown ? previousSolid - : solids.test(output.getRaw(localX, y - 1, localZ)))) { + : solids.test(column.get(y - 1)))) { floor = y; } previousSolid = solid; @@ -1472,6 +1476,43 @@ public class IrisCarveModifier extends EngineAssignedModifier private record CaveColumn(long key, IrisBiome surface, IrisBiome cave, int surfaceY, int minimumDepth) { } + private static final class ColumnWindow { + private static final int ROWS = 16; + + private final Hunk output; + private final NativeBlockState[] states; + private int x; + private int z; + private int limit; + private int from; + private int to; + + private ColumnWindow(Hunk output, int height) { + this.output = output; + this.states = new NativeBlockState[height]; + } + + private void begin(int localX, int localZ, int top) { + x = localX; + z = localZ; + limit = Math.min(top, states.length - 1); + from = 0; + to = -1; + } + + private NativeBlockState get(int y) { + if (y < from || y > to) { + if (y > limit) { + return output.getRaw(x, y, z); + } + from = y; + to = Math.min(limit, y + ROWS - 1); + ColumnReadHunk.readColumn(output, x, z, from, to, states, from); + } + return states[y]; + } + } + private static final class SolidMemo { private NativeBlockState state; private boolean solid; diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisPerfectionModifier.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisPerfectionModifier.java index 9cf870cc3..53d3aa6b8 100644 --- a/core/src/main/java/art/arcane/iris/generation/stage/IrisPerfectionModifier.java +++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisPerfectionModifier.java @@ -19,6 +19,7 @@ package art.arcane.iris.generation.stage; import art.arcane.iris.generation.chunk.ColumnExtent; +import art.arcane.iris.generation.chunk.ColumnReadHunk; import art.arcane.iris.generation.decoration.IrisSpeleothems; import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.EngineAssignedModifier; @@ -105,24 +106,29 @@ public class IrisPerfectionModifier extends EngineAssignedModifier output, int x, int z, IntArrayList surfaces, - StateFlags states, NativeBlockState air, NativeBlockState water) { + StateFlags states, NativeBlockState air, NativeBlockState water, + NativeBlockState[] column) { do { surfaces.clear(); - int top = getHeight(output, x, z, states); + int highest = Math.min(output.getHeight() - 1, ColumnExtent.highestStoredY(output, x, z)); + ColumnReadHunk.readColumn(output, x, z, 0, Math.max(0, highest), column, 0); + int top = getHeight(column, highest, states); boolean inside = true; surfaces.add(top); NativeBlockState classified = null; int classifiedFlags = 0; + boolean columnRead = true; for (int y = top; y >= 0; y--) { - NativeBlockState block = output.get(x, y, z); + NativeBlockState block = columnRead ? column[y] : output.get(x, y, z); int flags = 0; if (block != null) { if (block != classified) { @@ -133,6 +139,7 @@ public class IrisPerfectionModifier extends EngineAssignedModifier output, int x, int z, StateFlags states) { + private static int getHeight(NativeBlockState[] column, int highest, StateFlags states) { NativeBlockState classified = null; int classifiedFlags = 0; - for (int i = Math.min(output.getHeight() - 1, ColumnExtent.highestStoredY(output, x, z)); i >= 0; i--) { - NativeBlockState state = output.get(x, i, z); + for (int i = highest; i >= 0; i--) { + NativeBlockState state = column[i]; if (state == null) { continue; } diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisTransitionGeometryActuator.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisTransitionGeometryActuator.java index a7c1c4fa4..227b3a8be 100644 --- a/core/src/main/java/art/arcane/iris/generation/stage/IrisTransitionGeometryActuator.java +++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisTransitionGeometryActuator.java @@ -1,5 +1,6 @@ package art.arcane.iris.generation.stage; +import art.arcane.iris.generation.chunk.ColumnReadHunk; import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.EngineAssignedComponent; import art.arcane.iris.generation.runtime.EngineStage; @@ -111,6 +112,7 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen private final ChunkContext context; private final Map voxels = new IdentityHashMap<>(); private NativeBlockState air; + private NativeBlockState[] column = new NativeBlockState[0]; private NativeBlockState lastState; private BoundaryColumnGeometry.Voxel lastVoxel; @@ -132,10 +134,14 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen return; } int base = minimumY - this.minimumY; - NativeBlockState runState = stateAt(localX, base, localZ); + if (column.length < height) { + column = new NativeBlockState[height]; + } + ColumnReadHunk.readColumn(blocks, localX, localZ, base, base + height - 1, column, 0); + NativeBlockState runState = orAir(column[0]); int start = 0; for (int offset = 1; offset < height; offset++) { - NativeBlockState state = stateAt(localX, base + offset, localZ); + NativeBlockState state = orAir(column[offset]); if (state != runState) { runs.add(start, offset, voxelOf(runState)); start = offset; @@ -146,7 +152,10 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen } private NativeBlockState stateAt(int localX, int offset, int localZ) { - NativeBlockState state = blocks.getRaw(localX, offset, localZ); + return orAir(blocks.getRaw(localX, offset, localZ)); + } + + private NativeBlockState orAir(NativeBlockState state) { if (state != null) { return state; } diff --git a/core/src/main/java/art/arcane/iris/generation/stream/BakedStream.java b/core/src/main/java/art/arcane/iris/generation/stream/BakedStream.java index 9f0f922ed..1b528c4c8 100644 --- a/core/src/main/java/art/arcane/iris/generation/stream/BakedStream.java +++ b/core/src/main/java/art/arcane/iris/generation/stream/BakedStream.java @@ -39,6 +39,13 @@ public final class BakedStream extends BasicStream implements ChunkFillabl return value; } + @SuppressWarnings("unchecked") + public T baked(int blockX, int blockZ) { + int index = ((blockZ & 15) << 4) | (blockX & 15); + ChunkPlanes.Plane values = cache.planeIfPresent(plane, blockX >> 4, blockZ >> 4); + return values != null && values.ready(index) ? (T) values.objectAt(index) : null; + } + @Override public T get(double x, double y, double z) { return scalar.get(x, y, z); diff --git a/core/src/main/java/art/arcane/iris/world/history/GenerationHistory.java b/core/src/main/java/art/arcane/iris/world/history/GenerationHistory.java index 83857db64..7731c1774 100644 --- a/core/src/main/java/art/arcane/iris/world/history/GenerationHistory.java +++ b/core/src/main/java/art/arcane/iris/world/history/GenerationHistory.java @@ -26,6 +26,8 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.LockSupport; import java.util.function.Predicate; public final class GenerationHistory { @@ -34,6 +36,7 @@ public final class GenerationHistory { private static final int MAXIMUM_CACHED_TRANSITION_PLANS = 4; private static final int MAXIMUM_SEMANTIC_BATCH = 32; private static final int MAXIMUM_PENDING_SEMANTIC_CLAIMS = 128; + private static final long SEMANTIC_CLAIM_PARK_NANOS = 10_000_000L; private final GenerationHistoryPaths paths; private final GenerationPackRepository packs; @@ -50,6 +53,7 @@ public final class GenerationHistory { private final Map transitionPlanCache; private final ArrayBlockingQueue pendingSemanticClaims = new ArrayBlockingQueue<>(MAXIMUM_PENDING_SEMANTIC_CLAIMS); + private final AtomicBoolean semanticWriter = new AtomicBoolean(); private GenerationHistory( GenerationHistoryPaths paths, @@ -776,12 +780,26 @@ public final class GenerationHistory { persistPendingSemanticClaims(); } } - synchronized (this) { - while (!pending.claim.completed) { - persistPendingSemanticClaims(); + while (!pending.settled) { + if (!semanticWriter.compareAndSet(false, true)) { + LockSupport.parkNanos(pending, SEMANTIC_CLAIM_PARK_NANOS); + continue; + } + try { + synchronized (this) { + while (!pending.claim.completed) { + persistPendingSemanticClaims(); + } + } + } finally { + semanticWriter.set(false); + PendingSemanticClaim next = pendingSemanticClaims.peek(); + if (next != null) { + LockSupport.unpark(next.owner); + } } - return pending.claim.result(); } + return pending.claim.result(); } private void persistPendingSemanticClaims() { @@ -814,10 +832,15 @@ public final class GenerationHistory { } while ((current = pendingSemanticClaims.poll()) != null) { reported |= current.claim.fail(failure); + current.settle(); } if (!reported) { throw failure; } + } finally { + for (PendingSemanticClaim pending : batch) { + pending.settle(); + } } } @@ -1354,11 +1377,21 @@ public final class GenerationHistory { private static final class PendingSemanticClaim { private final GenerationStage stage; private final GenerationSemanticIndex.Claim claim; + private final Thread owner = Thread.currentThread(); + private volatile boolean settled; private PendingSemanticClaim(GenerationStage stage, ChunkGenerationSemantics update) { this.stage = stage; this.claim = new GenerationSemanticIndex.Claim(update); } + + private void settle() { + if (!claim.completed || settled) { + return; + } + settled = true; + LockSupport.unpark(owner); + } } public static final class GenerationStage implements AutoCloseable { diff --git a/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticCapture.java b/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticCapture.java index 673cd4c79..b6ae00d61 100644 --- a/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticCapture.java +++ b/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticCapture.java @@ -35,7 +35,7 @@ import art.arcane.volmlib.util.matter.MatterCavern; import art.arcane.volmlib.util.matter.MatterStructurePOI; import java.util.ArrayDeque; -import java.util.BitSet; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -238,7 +238,7 @@ public final class GenerationSemanticCapture { Mantle mantle = engine.getMantle().getMantle(); int minimumX = Math.multiplyExact(chunkX, CHUNK_SIZE); int minimumZ = Math.multiplyExact(chunkZ, CHUNK_SIZE); - BitSet resolvedCaves = new BitSet(); + CavePositions resolvedCaves = new CavePositions(); CaveBiomes caveBiomes = new CaveBiomes(engine, scoped); mantle.iterateChunk(chunkX, chunkZ, MatterCavern.class, (localX, y, localZ, cavern) -> { if (!caveSpace.test(localX, y, localZ)) { @@ -370,17 +370,15 @@ public final class GenerationSemanticCapture { private static void captureCave( CaveBiomes caveBiomes, ChunkGenerationSemantics.Builder semantics, - BitSet resolvedCaves, + CavePositions resolvedCaves, int blockX, int y, int blockZ, String explicitBiomeKey ) { - int position = (y << 8) | ((blockX & 15) << 4) | (blockZ & 15); - if (resolvedCaves.get(position)) { + if (!resolvedCaves.add((y << 8) | ((blockX & 15) << 4) | (blockZ & 15))) { return; } - resolvedCaves.set(position); if (explicitBiomeKey != null && !explicitBiomeKey.isBlank()) { caveBiomes.add(semantics, explicitBiomeKey); return; @@ -493,6 +491,25 @@ public final class GenerationSemanticCapture { } } + private static final class CavePositions { + private static final int INITIAL_WORDS = 1536; + + private long[] words = new long[INITIAL_WORDS]; + + private boolean add(int position) { + int word = position >>> 6; + if (word >= words.length) { + words = Arrays.copyOf(words, Math.max(word + 1, words.length << 1)); + } + long bit = 1L << position; + if ((words[word] & bit) != 0L) { + return false; + } + words[word] |= bit; + return true; + } + } + private static final class CaveBiomes { private final Engine engine; private final boolean scoped; diff --git a/core/src/main/java/art/arcane/iris/world/history/SavedBiomeStore.java b/core/src/main/java/art/arcane/iris/world/history/SavedBiomeStore.java index 22d0240e8..23a6520e1 100644 --- a/core/src/main/java/art/arcane/iris/world/history/SavedBiomeStore.java +++ b/core/src/main/java/art/arcane/iris/world/history/SavedBiomeStore.java @@ -29,6 +29,8 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.LockSupport; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.CRC32; @@ -49,6 +51,7 @@ public final class SavedBiomeStore { private static final int MAXIMUM_CACHED_CHUNKS = 128; private static final long MAXIMUM_CACHED_BYTES = 64L * 1024L * 1024L; private static final int MAXIMUM_WRITE_BATCH = 64; + private static final long WRITE_PARK_NANOS = 10_000_000L; private static final ThreadLocal DEFLATERS = ThreadLocal.withInitial(() -> new Deflater(Deflater.BEST_SPEED)); private final Path dimensionRoot; @@ -132,19 +135,34 @@ public final class SavedBiomeStore { .putInt(body.length).put(body).putInt(checksum(body)).array(); PendingWrite pending = new PendingWrite(required, record); lock.pending.add(pending); - synchronized (lock) { - while (!pending.completed) { - persistPending(lock); + while (!pending.settled) { + if (!lock.writer.compareAndSet(false, true)) { + LockSupport.parkNanos(pending, WRITE_PARK_NANOS); + continue; } - if (pending.failure != null) { - throw pending.failure; + try { + synchronized (lock) { + while (!pending.completed) { + persistPending(lock); + } + } + } finally { + lock.writer.set(false); + PendingWrite next = lock.pending.peek(); + if (next != null) { + LockSupport.unpark(next.owner); + } } - return pending.persisted; } + if (pending.failure != null) { + throw pending.failure; + } + return pending.persisted; } private void persistPending(WriteStripe stripe) { Map> batches = new LinkedHashMap<>(); + List polled = new ArrayList<>(MAXIMUM_WRITE_BATCH); PendingWrite grouping = null; try { for (int count = 0; count < MAXIMUM_WRITE_BATCH; count++) { @@ -152,6 +170,7 @@ public final class SavedBiomeStore { if (grouping == null) { break; } + polled.add(grouping); long regionKey = key(grouping.chunk.chunkX() >> 5, grouping.chunk.chunkZ() >> 5); batches.computeIfAbsent(regionKey, ignored -> new ArrayList<>()).add(grouping); grouping = null; @@ -175,6 +194,10 @@ public final class SavedBiomeStore { } } throw failure; + } finally { + for (PendingWrite pending : polled) { + pending.settle(); + } } } @@ -822,19 +845,30 @@ public final class SavedBiomeStore { private static final class WriteStripe { private final ConcurrentLinkedQueue pending = new ConcurrentLinkedQueue<>(); + private final AtomicBoolean writer = new AtomicBoolean(); } private static final class PendingWrite { private final SavedBiomeChunk chunk; private final byte[] record; + private final Thread owner = Thread.currentThread(); private boolean completed; private boolean persisted; private IOException failure; + private volatile boolean settled; private PendingWrite(SavedBiomeChunk chunk, byte[] record) { this.chunk = chunk; this.record = record; } + + private void settle() { + if (!completed || settled) { + return; + } + settled = true; + LockSupport.unpark(owner); + } } private static final class DecodeBudget { 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/block/PlatformStateRebindTest.java b/core/src/test/java/art/arcane/iris/generation/block/PlatformStateRebindTest.java index f149111e8..e4ef5651c 100644 --- a/core/src/test/java/art/arcane/iris/generation/block/PlatformStateRebindTest.java +++ b/core/src/test/java/art/arcane/iris/generation/block/PlatformStateRebindTest.java @@ -44,7 +44,7 @@ public class PlatformStateRebindTest { } @Test - public void chunkDataAirIsResolvedAgainstTheCurrentlyBoundPlatform() { + public void chunkDataAirIsResolvedAgainstThePlatformBoundWhenTheBufferFirstReadsIt() { NativeBlockState first = bindPlatform("first"); SectionBuffer holder = new SectionBuffer(384); @@ -53,7 +53,8 @@ public class PlatformStateRebindTest { IrisPlatforms.unbind(); NativeBlockState second = bindPlatform("second"); - assertSame(second, holder.getRaw(0, -1, 0)); + assertSame(first, holder.getRaw(0, -1, 0)); + assertSame(second, new SectionBuffer(384).getRaw(0, -1, 0)); } @Test diff --git a/core/src/test/java/art/arcane/iris/generation/chunk/SectionBufferTest.java b/core/src/test/java/art/arcane/iris/generation/chunk/SectionBufferTest.java index 75ab6a014..e2e212840 100644 --- a/core/src/test/java/art/arcane/iris/generation/chunk/SectionBufferTest.java +++ b/core/src/test/java/art/arcane/iris/generation/chunk/SectionBufferTest.java @@ -264,6 +264,42 @@ public class SectionBufferTest { assertEquals(List.of(9, 8, 7), order); } + @Test + public void columnReadsMatchSingleReadsAcrossEmptySectionsAndBufferEdges() { + SectionBuffer buffer = new SectionBuffer(40); + NativeBlockState stone = mock(NativeBlockState.class); + NativeBlockState dirt = mock(NativeBlockState.class); + buffer.setRaw(3, 2, 9, stone); + buffer.setRaw(3, 33, 9, dirt); + buffer.setRaw(3, 39, 9, stone); + ColumnExtentListeningHunk listening = new ColumnExtentListeningHunk<>(buffer, (x, y, z, state) -> { + }); + NativeBlockState[] column = new NativeBlockState[52]; + + ColumnReadHunk.readColumn(listening, 3, 9, -3, 44, column, 4); + + for (int y = -3; y <= 44; y++) { + assertSame("y=" + y, buffer.getRaw(3, y, 9), column[4 + y + 3]); + } + assertNull(column[0]); + assertSame(dirt, column[4 + 33 + 3]); + } + + @Test + public void plainHunksAnswerColumnReadsWithSingleReads() { + Hunk array = Hunk.newArrayHunk(16, 32, 16); + NativeBlockState stone = mock(NativeBlockState.class); + array.setRaw(5, 7, 1, stone); + NativeBlockState[] column = new NativeBlockState[10]; + + ColumnReadHunk.readColumn(array, 5, 1, 6, 8, column, 2); + + assertNull(column[2]); + assertSame(stone, column[3]); + assertNull(column[4]); + assertNull(column[5]); + } + private static void assertSameContents(SectionBuffer expected, SectionBuffer actual) { assertEquals(expected.sectionCount(), actual.sectionCount()); NativeBlockState[] expectedPalette = expected.palette(); diff --git a/core/src/test/java/art/arcane/iris/generation/context/IrisContextIsolationTest.java b/core/src/test/java/art/arcane/iris/generation/context/IrisContextIsolationTest.java index 02530c7c3..8085c02a3 100644 --- a/core/src/test/java/art/arcane/iris/generation/context/IrisContextIsolationTest.java +++ b/core/src/test/java/art/arcane/iris/generation/context/IrisContextIsolationTest.java @@ -3,6 +3,8 @@ package art.arcane.iris.generation.context; import art.arcane.iris.generation.runtime.Engine; import org.junit.Test; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.TimeUnit; @@ -50,6 +52,42 @@ public class IrisContextIsolationTest { assertNull(rightAfterClose.get()); } + @Test + public void contextsStayIsolatedWhenThreadsOutnumberTheBindingSlots() throws Exception { + Engine engine = mock(Engine.class); + int threads = 640; + CyclicBarrier allBound = new CyclicBarrier(threads); + AtomicReference failure = new AtomicReference<>(); + List workers = new ArrayList<>(threads); + for (int index = 0; index < threads; index++) { + long session = index + 1L; + Thread worker = new Thread(() -> { + try (IrisContext.Scope ignored = IrisContext.open(engine, session, null)) { + allBound.await(30L, TimeUnit.SECONDS); + if (IrisContext.get().getGenerationSessionId() != session) { + failure.compareAndSet(null, new AssertionError("Thread observed session " + + IrisContext.get().getGenerationSessionId() + " instead of " + session)); + } + allBound.await(30L, TimeUnit.SECONDS); + } catch (Throwable thrown) { + failure.compareAndSet(null, thrown); + } + if (IrisContext.get() != null) { + failure.compareAndSet(null, new AssertionError("Context survived its scope")); + } + }, "iris-context-crowd-" + index); + workers.add(worker); + worker.start(); + } + for (Thread worker : workers) { + worker.join(60000L); + assertFalse("Context worker did not finish.", worker.isAlive()); + } + + assertNull(failure.get()); + assertNull(IrisContext.get()); + } + @Test public void scopeRebindsWorkerWhenEngineChanges() throws Exception { Engine firstEngine = mock(Engine.class); diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/engine/FineLatticeSourceTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/engine/FineLatticeSourceTest.java deleted file mode 100644 index b22468d07..000000000 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/engine/FineLatticeSourceTest.java +++ /dev/null @@ -1,80 +0,0 @@ -package art.arcane.iris.generation.hydrology.engine; - -import art.arcane.iris.generation.hydrology.grid.HeightLattice; -import art.arcane.iris.generation.hydrology.grid.HydrologySampleBuffer; -import art.arcane.iris.generation.hydrology.grid.HydrologyTerrainSource; -import org.junit.Test; - -import java.util.concurrent.atomic.AtomicLong; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; - -public class FineLatticeSourceTest { - @Test - public void windowLatticeReadsItsNodesFromTheFineLattice() { - CountingTerrain terrain = new CountingTerrain(); - HeightLattice fine = new HeightLattice(terrain, 4, false, 1 << 16); - HeightLattice window = new HeightLattice(new FineLatticeSource(fine, terrain), 8, false, 1 << 16); - - fine.fill(0, 0, 64, 64, new HydrologySampleBuffer(64 * 64)); - assertEquals(4096L, terrain.samples.get()); - HydrologySampleBuffer held = new HydrologySampleBuffer(32 * 32); - window.fill(0, 0, 32, 32, held); - assertEquals(4096L, terrain.samples.get()); - assertHeights(held, 0, 0, 32); - - HydrologySampleBuffer fresh = new HydrologySampleBuffer(16 * 16); - window.fill(32, 0, 16, 16, fresh); - assertEquals(4096L + 256L, terrain.samples.get()); - assertHeights(fresh, 32, 0, 16); - fine.fill(64, 0, 32, 32, new HydrologySampleBuffer(32 * 32)); - assertEquals(4096L + 1024L, terrain.samples.get()); - - FineLatticeSource source = new FineLatticeSource(fine, terrain); - assertThrows(IllegalArgumentException.class, () -> source.sample(new int[]{8}, new int[]{8}, new HydrologySampleBuffer(1), true)); - assertThrows(IllegalArgumentException.class, () -> source.sample(new int[]{6}, new int[]{8}, new HydrologySampleBuffer(1), false)); - } - - private static void assertHeights(HydrologySampleBuffer samples, int cellX0, int cellZ0, int side) { - for (int index = 0; index < side * side; index++) { - int x = (cellX0 + index % side) * 8; - int z = (cellZ0 + index / side) * 8; - assertEquals(x + "," + z, Float.floatToRawIntBits(CountingTerrain.height(x, z)), Float.floatToRawIntBits(samples.height()[index])); - } - } - - private static final class CountingTerrain implements HydrologyTerrainSource { - private final AtomicLong samples = new AtomicLong(); - - private static float height(int x, int z) { - return 64f + (float) Math.sin(x * 0.013D) * 9f + (float) Math.cos(z * 0.021D) * 7f; - } - - @Override - public void sample(int[] xs, int[] zs, HydrologySampleBuffer out, boolean baseOnly) { - samples.addAndGet(xs.length); - for (int index = 0; index < xs.length; index++) { - out.height()[index] = height(xs[index], zs[index]); - out.permission()[index] = HydrologySampleBuffer.ALLOW_STREAMS | HydrologySampleBuffer.ALLOW_PONDS; - out.rainfall()[index] = 1f; - out.pondDensity()[index] = 1f; - } - } - - @Override - public int seaLevel() { - return 40; - } - - @Override - public int minY() { - return 0; - } - - @Override - public int maxY() { - return 384; - } - } -} 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..cbf79be3f 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; @@ -215,6 +216,34 @@ public class HydrologyRuntimeTest { assertEquals(planned + 1L, runtime.previewRevision()); } + @Test(timeout = 180_000L) + public void concurrentDemandsShareOneRasterBuild() throws Exception { + FakePlanners planners = new FakePlanners(settings(0, false)); + HydrologyRuntime runtime = runtime(planners, () -> false); + runtime.prepareWindowsAround(0, 0, 0, 0, 1).get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + CountDownLatch gate = new CountDownLatch(1); + planners.naturalGate = gate; + AtomicReference first = new AtomicReference<>(); + AtomicReference second = new AtomicReference<>(); + Thread leader = new Thread(() -> first.set(runtime.raster(0, 0))); + Thread follower = new Thread(() -> second.set(runtime.raster(0, 0))); + try { + leader.start(); + await(() -> planners.naturalSamples.get() > 0, "the first demand starts sampling"); + follower.start(); + await(() -> follower.getState() == Thread.State.WAITING || follower.getState() == Thread.State.TIMED_WAITING, + "the second demand waits"); + assertEquals(0, planners.naturalSamplesOn(follower)); + } finally { + gate.countDown(); + leader.join(TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS)); + follower.join(TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS)); + } + assertFalse(first.get().provisional()); + assertSame(first.get(), second.get()); + assertEquals(0, planners.naturalSamplesOn(follower)); + } + @Test(timeout = 180_000L) public void failedRasterDoesNotAdvancePreviewRevision() throws Exception { FakePlanners planners = new FakePlanners(settings(0, false)); @@ -414,6 +443,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()); diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/engine/IrisHydrologyTerrainSourceTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/engine/IrisHydrologyTerrainSourceTest.java index 552d4bcbb..b411a91a2 100644 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/engine/IrisHydrologyTerrainSourceTest.java +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/engine/IrisHydrologyTerrainSourceTest.java @@ -154,10 +154,9 @@ public class IrisHydrologyTerrainSourceTest { HydrologySampleBuffer rect = new HydrologySampleBuffer(32 * 32); fine.fill(-600, 400, 32, 32, rect); assertMatchesScalar(reference, "fine rect", rect, nodes(-600, 400, 32, 1, 4), false); - HeightLattice window = new HeightLattice(new FineLatticeSource(fine, source), 8, false, 1 << 16); HydrologySampleBuffer strided = new HydrologySampleBuffer(16 * 16); - window.fill(-150, 300, 16, 16, 3, strided); - assertMatchesScalar(reference, "window strided", strided, nodes(-150, 300, 16, 3, 8), false); + fine.fill(-300, 600, 16, 16, 6, strided); + assertMatchesScalar(reference, "fine strided", strided, nodes(-300, 600, 16, 6, 4), false); HeightLattice coarse = new HeightLattice(source, 64, true, 1 << 12); HydrologySampleBuffer coarseBase = new HydrologySampleBuffer(16 * 16); coarse.fill(-8, -8, 16, 16, coarseBase); diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/grid/HeightLatticeTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/grid/HeightLatticeTest.java index 0016c469a..5e4da3c2d 100644 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/grid/HeightLatticeTest.java +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/grid/HeightLatticeTest.java @@ -287,6 +287,31 @@ public class HeightLatticeTest { assertThrows(IllegalArgumentException.class, () -> lattice.fill(cellXs, cellZs, new HydrologySampleBuffer(5))); } + @Test + public void sampledColumnsAreReadOnceAndMatchTheSource() { + CountingSource source = new CountingSource(); + HeightLattice lattice = new HeightLattice(source, 4, false, 1 << 16); + int[] xs = {5, -13, 5, 1027, -13, 6}; + int[] zs = {-2, 77, -2, 3, 77, -2}; + HydrologySampleBuffer first = new HydrologySampleBuffer(xs.length); + lattice.sampleColumns(xs, zs, first); + assertEquals(4, source.samples()); + HydrologySampleBuffer second = new HydrologySampleBuffer(xs.length); + lattice.sampleColumns(xs, zs, second); + assertEquals(4, source.samples()); + HydrologySampleBuffer expected = new HydrologySampleBuffer(xs.length); + new CountingSource().sample(xs, zs, expected, false); + for (int index = 0; index < xs.length; index++) { + assertEquals(Float.floatToIntBits(expected.height()[index]), Float.floatToIntBits(first.height()[index])); + assertEquals(Float.floatToIntBits(expected.height()[index]), Float.floatToIntBits(second.height()[index])); + assertEquals(expected.ocean()[index], second.ocean()[index]); + assertEquals(expected.permission()[index], second.permission()[index]); + assertEquals(expected.fluidProfile()[index], second.fluidProfile()[index]); + } + lattice.sampleColumns(new int[0], new int[0], new HydrologySampleBuffer(0)); + assertEquals(1, source.calls()); + } + private static void fillWindow(HeightLattice lattice, int wx, int wz, int cell, boolean baseOnly) { assertFill(lattice, cell, baseOnly, (wx - 1) * WINDOW, (wz - 1) * WINDOW, AREA, AREA); } diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/plan/HydrologyPlanCacheTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/plan/HydrologyPlanCacheTest.java index 040a6bfa4..63c7275ff 100644 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/plan/HydrologyPlanCacheTest.java +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/plan/HydrologyPlanCacheTest.java @@ -1045,7 +1045,7 @@ public class HydrologyPlanCacheTest { } private static WindowPlanner windowPlanner(HydrologySettings settings, SyntheticTerrain terrain) { - return new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.CELL, false, HydrologyPlanCache.WINDOW_LATTICE_CELLS), + return new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.FINE_CELL, false, HydrologyPlanCache.FINE_LATTICE_CELLS), new PondPlanner(settings), new ReachShaper(settings)); } diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/plan/HydrologyPlanCodecTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/plan/HydrologyPlanCodecTest.java index dba3ff4a1..274a2ffce 100644 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/plan/HydrologyPlanCodecTest.java +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/plan/HydrologyPlanCodecTest.java @@ -81,7 +81,7 @@ public class HydrologyPlanCodecTest { assertEquals(planned, HydrologyPlanCodec.decodeRegion(settings, HydrologyPlanCodec.encode(settings, planned)).orElseThrow()); regions.add(planned); } - WindowPlan planned = new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.CELL, false, 1 << 18), + WindowPlan planned = new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.FINE_CELL, false, 1 << 21), new PondPlanner(settings), new ReachShaper(settings)) .plan(LAND, regions); assertFalse(planned.reaches().isEmpty()); diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/plan/WindowPlannerTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/plan/WindowPlannerTest.java index a1829ff59..1efdfb02f 100644 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/plan/WindowPlannerTest.java +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/plan/WindowPlannerTest.java @@ -83,7 +83,7 @@ public class WindowPlannerTest { new int[]{head, head}, new float[]{2f, 2f}, new float[]{2f, 2f}, new ReachStyle[]{ReachStyle.RUN, ReachStyle.RUN}, 1000f); return new ShapedNetwork(List.of(outlet), List.of(pond.withOutletReachId(outlet.id()))); }).when(shaper).shape(any(), any(), any(), any(), any(), any(), anyLong()); - WindowPlanner planner = new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.CELL, false, 1 << 18), + WindowPlanner planner = new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.FINE_CELL, false, 1 << 18), ponds, shaper); WindowComputation settled = planner.compute(ORIGIN, noRegions(ORIGIN)); @@ -587,7 +587,7 @@ public class WindowPlannerTest { } return network; }).when(shaper).shape(any(), any(), any(), any(), any(), any(), anyLong()); - return new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.CELL, false, 1 << 18), + return new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.FINE_CELL, false, 1 << 21), new PondPlanner(settings), shaper); } @@ -715,7 +715,7 @@ public class WindowPlannerTest { } private static WindowPlanner planner(HydrologySettings settings, SyntheticTerrain terrain) { - return new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.CELL, false, 1 << 18), + return new WindowPlanner(settings, new HeightLattice(terrain, WindowPlanner.FINE_CELL, false, 1 << 21), new PondPlanner(settings), new ReachShaper(settings)); } diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/raster/ReachSegmentsTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/raster/ReachSegmentsTest.java new file mode 100644 index 000000000..299c4178d --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/raster/ReachSegmentsTest.java @@ -0,0 +1,95 @@ +package art.arcane.iris.generation.hydrology.raster; + +import art.arcane.iris.generation.hydrology.config.IrisBankStyle; +import art.arcane.iris.generation.hydrology.config.IrisHydrologyMaterials; +import art.arcane.iris.generation.hydrology.water.HydrologyTier; +import art.arcane.iris.generation.hydrology.water.Reach; +import art.arcane.iris.generation.hydrology.water.ReachStyle; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.SplittableRandom; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class ReachSegmentsTest { + private static final HydrologyTier TIER = new HydrologyTier("test", 0f, 1f, 4f, 1f, 0.4f, 1, true, 6, 4, IrisBankStyle.SMOOTH, + false, new IrisHydrologyMaterials(), "water", List.of()); + + @Test + public void coveredChannelLookupsMatchTheFullScan() { + SplittableRandom random = new SplittableRandom(1337L); + int hits = 0; + for (int trial = 0; trial < 40; trial++) { + int originX = random.nextInt(-2000, 2000) << 4; + int originZ = random.nextInt(-2000, 2000) << 4; + List reaches = new ArrayList<>(); + int count = 1 + random.nextInt(12); + for (int walk = 0; walk < count; walk++) { + reaches.add(randomWalk(trial * 100L + walk, originX + 8, originZ + 8, random)); + } + ReachSegments scanned = segments(reaches, originX, originZ); + ReachSegments covered = segments(reaches, originX, originZ); + covered.coverChannels(originX - 17, originZ - 17, originX + 32, originZ + 32); + ReachCursor scan = new ReachCursor(); + ReachCursor cover = new ReachCursor(); + for (int z = originZ - 20; z <= originZ + 35; z++) { + for (int x = originX - 20; x <= originX + 35; x++) { + ReachSegments.Scope scope = random.nextInt(4) == 0 ? ReachSegments.Scope.SHORE : ReachSegments.Scope.CHANNEL; + scanned.nearest(x, z, scope, scan); + covered.nearest(x, z, scope, cover); + String at = scope + " at " + x + "," + z; + assertSameChoice(at + " channel", scan.channel, cover.channel); + assertSameChoice(at + " bank", scan.bank, cover.bank); + assertSameChoice(at + " edge", scan.edge, cover.edge); + if (scan.channel.reach != null) { + hits++; + } + } + } + } + assertTrue("random walks cross the covered area, hits=" + hits, hits > 1000); + } + + private static void assertSameChoice(String what, ReachCursor.Choice expected, ReachCursor.Choice actual) { + assertSame(what, expected.reach, actual.reach); + assertEquals(what, expected.segment, actual.segment); + assertEquals(what, expected.index, actual.index); + assertEquals(what, Double.doubleToLongBits(expected.t), Double.doubleToLongBits(actual.t)); + assertEquals(what, Double.doubleToLongBits(expected.distance), Double.doubleToLongBits(actual.distance)); + } + + private static ReachSegments segments(List reaches, int originX, int originZ) { + ReachSegments segments = new ReachSegments(16); + for (Reach reach : reaches) { + segments.addNear(reach, originX - 1f, originZ - 1f, originX + 16f, originZ + 16f, ChunkWaterRaster.MAX_SHORE_DISTANCE + 1f); + } + return segments; + } + + private static Reach randomWalk(long id, int centerX, int centerZ, SplittableRandom random) { + int points = 2 + random.nextInt(30); + float[] xs = new float[points]; + float[] zs = new float[points]; + float[] halfWidth = new float[points]; + xs[0] = centerX + (float) random.nextDouble(-60D, 60D); + zs[0] = centerZ + (float) random.nextDouble(-60D, 60D); + halfWidth[0] = (float) random.nextDouble(0.5D, 9D); + for (int point = 1; point < points; point++) { + xs[point] = xs[point - 1] + (float) random.nextDouble(-12D, 12D); + zs[point] = zs[point - 1] + (float) random.nextDouble(-12D, 12D); + halfWidth[point] = random.nextInt(8) == 0 ? halfWidth[point - 1] : (float) random.nextDouble(0.5D, 9D); + } + int[] grade = new int[points]; + float[] depth = new float[points]; + ReachStyle[] style = new ReachStyle[points]; + Arrays.fill(grade, 64); + Arrays.fill(depth, 1f); + Arrays.fill(style, ReachStyle.RUN); + return new Reach(id, TIER, -1L, xs, zs, grade, halfWidth, depth, style, 100f); + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/underground/CavePoolFillerTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/underground/CavePoolFillerTest.java index 2d6b4cc30..860215a9a 100644 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/underground/CavePoolFillerTest.java +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/underground/CavePoolFillerTest.java @@ -15,6 +15,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.IntFunction; @@ -50,18 +51,37 @@ public class CavePoolFillerTest { return 0; }); for (int chunkX = 0; chunkX < 1024; chunkX++) { - filler.fill(chunkX, 0, carved, writer, SEED); + filler.fill(chunkX, 0, () -> carved, writer, SEED); } assertTrue("half density samples approximately half the candidate columns", columns.size() > 3600 && columns.size() < 4500); Set repeated = new HashSet<>(columns); columns.clear(); for (int chunkX = 1023; chunkX >= 0; chunkX--) { - filler.fill(chunkX, 0, carved, writer, SEED); + filler.fill(chunkX, 0, () -> carved, writer, SEED); } assertEquals(repeated, columns); verifyNoInteractions(writer); } + @Test + public void chunksWithoutAPoolSiteNeverOpenTheirCaves() { + IrisHydrology config = new IrisHydrology().setDensity(0.5D); + config.getUnderground().getCavePools().setDensity(0.05D); + CavePoolFiller filler = new CavePoolFiller(HydrologySettings.compile(config, SEED, 63, 0, 384)); + CarvedTerrain carved = mock(CarvedTerrain.class); + MantleWriter writer = mock(MantleWriter.class); + AtomicInteger opened = new AtomicInteger(); + int chunks = 1024; + for (int chunkX = 0; chunkX < chunks; chunkX++) { + filler.fill(chunkX, 0, () -> { + opened.incrementAndGet(); + return carved; + }, writer, SEED); + } + assertTrue("only chunks drawing a pool site open their caves, opened " + opened.get(), opened.get() > 0 && opened.get() < chunks / 8); + verifyNoInteractions(writer); + } + @Test public void zeroGlobalDensityDoesNotReadCaves() { IrisHydrology config = new IrisHydrology().setDensity(0D); @@ -69,7 +89,7 @@ public class CavePoolFillerTest { CavePoolFiller filler = new CavePoolFiller(HydrologySettings.compile(config, SEED, 63, 0, 384)); CarvedTerrain carved = mock(CarvedTerrain.class); MantleWriter writer = mock(MantleWriter.class); - filler.fill(0, 0, carved, writer, SEED); + filler.fill(0, 0, () -> carved, writer, SEED); verifyNoInteractions(carved, writer); } @@ -143,10 +163,10 @@ public class CavePoolFillerTest { MantleWriter forwardWriter = writer(forward); MantleWriter reverseWriter = writer(reverse); for (int index = 0; index < 9; index++) { - new CavePoolFiller(settings).fill(index % 3 - 1, index / 3 - 1, world, forwardWriter, SEED); + new CavePoolFiller(settings).fill(index % 3 - 1, index / 3 - 1, () -> world, forwardWriter, SEED); } for (int index = 8; index >= 0; index--) { - new CavePoolFiller(settings).fill(index % 3 - 1, index / 3 - 1, world, reverseWriter, SEED); + new CavePoolFiller(settings).fill(index % 3 - 1, index / 3 - 1, () -> world, reverseWriter, SEED); } assertFalse(forward.isEmpty()); @@ -284,7 +304,7 @@ public class CavePoolFillerTest { private static Map fill(HydrologySettings settings, VoxelWorld world, int chunkX, int chunkZ) { Map written = new HashMap<>(); - new CavePoolFiller(settings).fill(chunkX, chunkZ, world, writer(written), SEED); + new CavePoolFiller(settings).fill(chunkX, chunkZ, () -> world, writer(written), SEED); return written; } diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/MantleCarvedTerrainTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/MantleCarvedTerrainTest.java index 22603f411..de5db777c 100644 --- a/core/src/test/java/art/arcane/iris/generation/mantle/MantleCarvedTerrainTest.java +++ b/core/src/test/java/art/arcane/iris/generation/mantle/MantleCarvedTerrainTest.java @@ -324,7 +324,7 @@ public class MantleCarvedTerrainTest { for (SeaInlet inlet : complex.getHydrologyRuntime().inlets(0, chunkZ)) { rasterizer.write(inlet, 0, chunkZ, writer, HydrologyCaveCell.SEA_FLUID, "", SeaInletRasterizer.SubterrainFluid.NOWHERE); } - new CavePoolFiller(settings).fill(0, chunkZ, new MantleCarvedTerrain(writer, complex, engine, 0, chunkZ), writer, SEED); + new CavePoolFiller(settings).fill(0, chunkZ, () -> new MantleCarvedTerrain(writer, complex, engine, 0, chunkZ), writer, SEED); } return cells; } diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexBakeParityTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexBakeParityTest.java index f455cd6a5..7f143089c 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexBakeParityTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexBakeParityTest.java @@ -22,11 +22,13 @@ import org.junit.rules.TemporaryFolder; import org.mockito.MockedStatic; import org.mockito.Mockito; +import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Path; import java.util.SplittableRandom; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.Answers.CALLS_REAL_METHODS; import static org.mockito.ArgumentMatchers.any; @@ -81,6 +83,24 @@ public class IrisComplexBakeParityTest { } } + @Test + public void regionConstantBoundsEqualFullBiomeBoundsBitForBit() throws Exception { + try (Statics ignored = new Statics(); Fixture shortcut = new Fixture(pack()); Fixture full = new Fixture(pack())) { + Field regionBounds = IrisComplex.class.getDeclaredField("regionBounds"); + regionBounds.setAccessible(true); + assertNotNull(regionBounds.get(shortcut.complex)); + regionBounds.set(full.complex, null); + SplittableRandom random = new SplittableRandom(0x5EEDL); + for (int column = 0; column < 2048; column++) { + int x = random.nextInt(-6400, 6400); + int z = random.nextInt(-6400, 6400); + assertEquals("column " + x + "," + z, + Double.doubleToRawLongBits(full.complex.getBaseTerrainHeightStream().getDouble(x, z)), + Double.doubleToRawLongBits(shortcut.complex.getBaseTerrainHeightStream().getDouble(x, z))); + } + } + } + @Test public void batchedChunksEvaluateExactlyTheScalarGeneratorSet() throws Exception { try (Statics ignored = new Statics(); Fixture batched = new Fixture(pack()); Fixture scalar = new Fixture(pack())) { diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexBoundsBiomeCacheTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexBoundsBiomeCacheTest.java index 401c1b533..6cf44f763 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexBoundsBiomeCacheTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexBoundsBiomeCacheTest.java @@ -243,7 +243,7 @@ public class IrisComplexBoundsBiomeCacheTest { 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); + Method put = type.getDeclaredMethod("put", double.class, double.class, Object.class); get.setAccessible(true); put.setAccessible(true); IrisBiome first = new IrisBiome(); @@ -266,7 +266,7 @@ public class IrisComplexBoundsBiomeCacheTest { } Field values = type.getDeclaredField("values"); values.setAccessible(true); - assertEquals(4096, ((IrisBiome[]) values.get(cache)).length); + assertEquals(4096, ((Object[]) values.get(cache)).length); } private static ProceduralStream constant(IrisBiome biome) { @@ -295,7 +295,7 @@ public class IrisComplexBoundsBiomeCacheTest { constructor.setAccessible(true); cache = constructor.newInstance(); sample = IrisComplex.class.getDeclaredMethod("sampleBoundsRaw", cacheClass, Engine.class, - IrisInterpolator.class, IrisGenerator[].class, double.class, double.class); + IrisInterpolator.class, int.class, IrisGenerator[].class, double.class, double.class); sample.setAccessible(true); boundsConstructor = Class.forName("art.arcane.iris.generation.runtime.IrisComplex$GeneratorBounds") .getDeclaredConstructor(double.class, double.class); @@ -313,7 +313,7 @@ public class IrisComplexBoundsBiomeCacheTest { private NoiseBounds sample(Object gridCache, IrisInterpolator interpolator, double x, double z) throws ReflectiveOperationException { - return (NoiseBounds) sample.invoke(complex, gridCache, null, interpolator, new IrisGenerator[0], x, z); + return (NoiseBounds) sample.invoke(complex, gridCache, null, interpolator, 0, new IrisGenerator[0], x, z); } private Object newGridCache() throws ReflectiveOperationException { diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/RegionBoundsTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/RegionBoundsTest.java new file mode 100644 index 000000000..f4168db0d --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/runtime/RegionBoundsTest.java @@ -0,0 +1,136 @@ +package art.arcane.iris.generation.runtime; + +import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.iris.generation.terrain.InferredType; +import art.arcane.iris.generation.terrain.IrisRegion; +import art.arcane.volmlib.util.interpolation.NoiseBounds; +import org.junit.Test; + +import java.util.Collection; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +public class RegionBoundsTest { + private static final NoiseBounds FLAT = new NoiseBounds(0D, 0D); + private static final NoiseBounds HILLS = new NoiseBounds(4D, 30D); + + @Test + public void groupsEveryReachableBiomeAgreesOnAreConstantForTheRegion() { + IrisBiome plains = new IrisBiome(); + IrisBiome forest = new IrisBiome(); + IrisBiome ocean = new IrisBiome(); + IrisRegion region = new IrisRegion(); + Map bounds = new IdentityHashMap<>(); + bounds.put(plains, new NoiseBounds[]{FLAT, HILLS}); + bounds.put(forest, new NoiseBounds[]{new NoiseBounds(0D, 0D), new NoiseBounds(2D, 30D)}); + bounds.put(ocean, new NoiseBounds[]{FLAT, HILLS}); + + RegionBounds compiled = RegionBounds.compile(List.of(region), 2, + new Fixed(Map.of(InferredType.LAND, List.of(plains, forest), InferredType.SEA, List.of(ocean)), Map.of()), + (group, biome) -> bounds.get(biome)[group]); + RegionBounds.Constants constants = compiled.of(region); + + assertBits(FLAT, constants.any(0)); + assertBits(FLAT, constants.typed(InferredType.LAND, 0)); + assertBits(FLAT, constants.typed(InferredType.SEA, 0)); + assertNull(constants.any(1)); + assertNull(constants.typed(InferredType.LAND, 1)); + assertSame(HILLS, constants.typed(InferredType.SEA, 1)); + assertSame(HILLS, compiled.pick(plains, 1)); + assertBits(new NoiseBounds(2D, 30D), compiled.pick(forest, 1)); + } + + @Test + public void picksAreConstantOnlyWhenEveryDescendantAgrees() { + IrisBiome steppe = new IrisBiome(); + IrisBiome meadow = new IrisBiome(); + IrisBiome mesa = new IrisBiome(); + IrisBiome canyon = new IrisBiome(); + IrisRegion region = new IrisRegion(); + Map bounds = new IdentityHashMap<>(); + bounds.put(steppe, FLAT); + bounds.put(meadow, new NoiseBounds(0D, 0D)); + bounds.put(mesa, FLAT); + bounds.put(canyon, HILLS); + + RegionBounds compiled = RegionBounds.compile(List.of(region), 1, + new Fixed(Map.of(InferredType.LAND, List.of(steppe, mesa), InferredType.SEA, List.of()), + descendants(steppe, List.of(steppe, meadow), mesa, List.of(mesa, canyon))), + (group, biome) -> bounds.get(biome)); + + assertNull(compiled.of(region).typed(InferredType.LAND, 0)); + assertSame(FLAT, compiled.pick(steppe, 0)); + assertNull(compiled.pick(mesa, 0)); + assertNull(compiled.pick(canyon, 0)); + } + + @Test + public void landAndSeaConstantsOnlyMergeWhenTheirBitsMatch() { + IrisBiome beach = new IrisBiome(); + IrisBiome reef = new IrisBiome(); + IrisRegion region = new IrisRegion(); + Map bounds = new IdentityHashMap<>(); + bounds.put(beach, new NoiseBounds(0D, 0D)); + bounds.put(reef, new NoiseBounds(-0D, 0D)); + + RegionBounds.Constants constants = RegionBounds.compile(List.of(region), 1, + new Fixed(Map.of(InferredType.LAND, List.of(beach), InferredType.SEA, List.of(reef)), Map.of()), + (group, biome) -> bounds.get(biome)).of(region); + + assertNull(constants.any(0)); + assertSame(bounds.get(beach), constants.typed(InferredType.LAND, 0)); + assertSame(bounds.get(reef), constants.typed(InferredType.SEA, 0)); + } + + @Test + public void emptyBiomeListsAndUnknownRegionsHaveNoConstant() { + IrisRegion region = new IrisRegion(); + IrisBiome only = new IrisBiome(); + RegionBounds compiled = RegionBounds.compile(List.of(region), 1, + new Fixed(Map.of(InferredType.LAND, List.of(only), InferredType.SEA, List.of()), Map.of()), + (group, biome) -> FLAT); + RegionBounds.Constants constants = compiled.of(region); + + assertSame(FLAT, constants.typed(InferredType.LAND, 0)); + assertNull(constants.typed(InferredType.SEA, 0)); + assertNull(constants.any(0)); + assertNull(constants.typed(InferredType.SHORE, 0)); + assertNull(constants.typed(InferredType.CAVE, 0)); + assertNull(compiled.of(new IrisRegion())); + assertNull(compiled.of(null)); + assertNull(compiled.pick(new IrisBiome(), 0)); + assertNull(compiled.pick(null, 0)); + } + + private static Map> descendants(IrisBiome first, List firstDescendants, + IrisBiome second, List secondDescendants) { + Map> descendants = new IdentityHashMap<>(); + descendants.put(first, firstDescendants); + descendants.put(second, secondDescendants); + return descendants; + } + + private static void assertBits(NoiseBounds expected, NoiseBounds actual) { + assertEquals(Double.doubleToRawLongBits(expected.min()), Double.doubleToRawLongBits(actual.min())); + assertEquals(Double.doubleToRawLongBits(expected.max()), Double.doubleToRawLongBits(actual.max())); + } + + private record Fixed(Map> picks, Map> descendants) + implements RegionBounds.Reach { + @Override + public List picks(IrisRegion region, InferredType type) { + return picks.get(type); + } + + @Override + public Collection descendants(IrisBiome biome) { + List known = descendants.get(biome); + return known == null ? List.of(biome) : known; + } + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/SharedBaseHeightsTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/SharedBaseHeightsTest.java new file mode 100644 index 000000000..d9914c18c --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/runtime/SharedBaseHeightsTest.java @@ -0,0 +1,87 @@ +package art.arcane.iris.generation.runtime; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class SharedBaseHeightsTest { + @Test + public void storedColumnsComeBackWithTheirExactBits() { + SharedBaseHeights cache = new SharedBaseHeights(1 << 10); + double height = Math.nextUp(71.25D); + + assertTrue(Double.isNaN(cache.get(12, -40))); + cache.put(12, -40, height); + + assertEquals(Double.doubleToRawLongBits(height), Double.doubleToRawLongBits(cache.get(12, -40))); + assertTrue(Double.isNaN(cache.get(-40, 12))); + } + + @Test + public void originMissesUntilItIsStored() { + SharedBaseHeights cache = new SharedBaseHeights(2); + + assertTrue(Double.isNaN(cache.get(0, 0))); + cache.put(0, 0, -0D); + assertEquals(Double.doubleToRawLongBits(-0D), Double.doubleToRawLongBits(cache.get(0, 0))); + } + + @Test + public void nonFiniteHeightsAreNeverStored() { + SharedBaseHeights cache = new SharedBaseHeights(2); + cache.put(3, 4, 64D); + cache.put(3, 4, Double.NaN); + cache.put(3, 4, Double.POSITIVE_INFINITY); + + assertEquals(64D, cache.get(3, 4), 0D); + } + + @Test + public void fullCoordinateDomainKeepsDistinctColumns() { + int[] coordinates = {0, 1, -1, 1 << 21, -(1 << 21), 30_000_000, -30_000_000, + Integer.MIN_VALUE, Integer.MAX_VALUE}; + for (int x : coordinates) { + for (int z : coordinates) { + SharedBaseHeights cache = new SharedBaseHeights(2); + cache.put(x, z, 12D); + for (int otherX : coordinates) { + for (int otherZ : coordinates) { + if (x == otherX && z == otherZ) { + assertEquals(12D, cache.get(otherX, otherZ), 0D); + } else { + assertTrue(Double.isNaN(cache.get(otherX, otherZ))); + } + } + } + } + } + } + + @Test + public void aCollidingColumnReplacesTheOldOne() { + SharedBaseHeights cache = new SharedBaseHeights(2); + cache.put(5, 9, 40D); + cache.put(6, 9, 41D); + cache.put(7, 9, 42D); + + int stored = 0; + for (int x = 5; x <= 7; x++) { + double value = cache.get(x, 9); + if (!Double.isNaN(value)) { + assertEquals(35D + x, value, 0D); + stored++; + } + } + assertTrue(stored >= 1 && stored <= 2); + } + + @Test + public void capacityFollowsTheHeapWithinBounds() { + assertEquals(SharedBaseHeights.MINIMUM_CAPACITY, SharedBaseHeights.capacityFor(0L)); + assertEquals(SharedBaseHeights.MAXIMUM_CAPACITY, SharedBaseHeights.capacityFor(Long.MAX_VALUE)); + int capacity = SharedBaseHeights.capacityFor(6L << 30); + assertEquals(1, Integer.bitCount(capacity)); + assertTrue(capacity >= SharedBaseHeights.MINIMUM_CAPACITY && capacity <= SharedBaseHeights.MAXIMUM_CAPACITY); + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/SharedBoundsBiomesTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/SharedBoundsBiomesTest.java index b1f152f5c..3b9c21fb1 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/SharedBoundsBiomesTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/SharedBoundsBiomesTest.java @@ -146,7 +146,7 @@ public class SharedBoundsBiomesTest { int xi = (int) ((state & Long.MAX_VALUE) % coordinates.length); int zi = (int) ((state >>> 10) % coordinates.length); cache.put(coordinates[xi], coordinates[zi], biomes[xi][zi]); - IrisBiome actual = cache.get(coordinates[xi], coordinates[zi]); + Object actual = cache.get(coordinates[xi], coordinates[zi]); if (actual != null) { hits.incrementAndGet(); if (actual != biomes[xi][zi]) { diff --git a/core/src/test/java/art/arcane/iris/world/history/GenerationHistoryAdmissionTest.java b/core/src/test/java/art/arcane/iris/world/history/GenerationHistoryAdmissionTest.java index 15455769d..1cdbada60 100644 --- a/core/src/test/java/art/arcane/iris/world/history/GenerationHistoryAdmissionTest.java +++ b/core/src/test/java/art/arcane/iris/world/history/GenerationHistoryAdmissionTest.java @@ -250,7 +250,7 @@ public class GenerationHistoryAdmissionTest { .addSurfaceBiome("iris:conflict").seal().build() : claim(stage); results.add(executor.submit(() -> persistCounting(history, stage, update, appends, forces))); - awaitBlockedOnHistory(history, workers, index + 1); + awaitQueuedOnHistory(history, workers, index + 1); } stages.get(5).close(); } @@ -340,7 +340,7 @@ public class GenerationHistoryAdmissionTest { for (int index = 0; index < stages.size(); index++) { GenerationHistory.GenerationStage stage = stages.get(index); results.add(executor.submit(() -> persistFailingRegion(history, stage, failure))); - awaitBlockedOnHistory(history, workers, index + 1); + awaitQueuedOnHistory(history, workers, index + 1); } } assertTrue(results.getFirst().get(5, TimeUnit.SECONDS)); @@ -404,21 +404,27 @@ public class GenerationHistoryAdmissionTest { } } - private static void awaitBlockedOnHistory(GenerationHistory history, List workers, int count) { - Await.until("semantic claim callers to queue behind the History monitor", Duration.ofSeconds(5L), - () -> blockedOnHistory(history, workers) == count); + private static void awaitQueuedOnHistory(GenerationHistory history, List workers, int count) { + Await.until("semantic claim callers to queue behind the History writer", Duration.ofSeconds(5L), + () -> queuedOnHistory(history, workers) == count); } - private static int blockedOnHistory(GenerationHistory history, List workers) { - int blocked = 0; + private static int queuedOnHistory(GenerationHistory history, List workers) { + int queued = 0; for (Thread worker : workers) { ThreadInfo info = ManagementFactory.getThreadMXBean().getThreadInfo(worker.threadId()); - if (info != null && info.getThreadState() == Thread.State.BLOCKED - && info.getLockInfo().getIdentityHashCode() == System.identityHashCode(history)) { - blocked++; + if (info == null || info.getLockInfo() == null) { + continue; + } + boolean writer = info.getThreadState() == Thread.State.BLOCKED + && info.getLockInfo().getIdentityHashCode() == System.identityHashCode(history); + boolean follower = info.getThreadState() == Thread.State.TIMED_WAITING + && info.getLockInfo().getClassName().endsWith("GenerationHistory$PendingSemanticClaim"); + if (writer || follower) { + queued++; } } - return blocked; + return queued; } private static boolean persistPaused(GenerationHistory history, GenerationHistory.GenerationStage stage, diff --git a/probe/src/main/java/art/arcane/iris/probe/HydrologyPlanHarness.java b/probe/src/main/java/art/arcane/iris/probe/HydrologyPlanHarness.java index 89d776000..ddc37a008 100644 --- a/probe/src/main/java/art/arcane/iris/probe/HydrologyPlanHarness.java +++ b/probe/src/main/java/art/arcane/iris/probe/HydrologyPlanHarness.java @@ -195,7 +195,8 @@ final class HydrologyPlanHarness { double landTiles(WindowKey key) { int cells = WindowKey.SIZE / WindowPlanner.CELL; HydrologySampleBuffer samples = new HydrologySampleBuffer(cells * cells); - planners.windowLattice().fill(key.wx() * cells, key.wz() * cells, cells, cells, samples); + int stride = WindowPlanner.CELL / WindowPlanner.FINE_CELL; + planners.fineLattice().fill(key.wx() * cells * stride, key.wz() * cells * stride, cells, cells, stride, samples); int land = 0; for (int cell = 0; cell < cells * cells; cell++) { if (!samples.ocean()[cell]) {