mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-11-01 00:40:01 +00:00
Pregen optimization pass
- Hydrology height lattice stores samples in tiles with least-recently-used eviction - Evicted lattice cells are no longer sampled twice - Pond rim columns go through the lattice column cache - Window planning reads its coarse nodes from the fine lattice - Prefetch stays inside the pregeneration area while a pregeneration runs - Sea inlet splat compares squared distances - Bounds sampling answers a column from region constants when every reachable biome shares the bounds - Base terrain heights are shared through a per-column memo - Chunk water rasters use per-chunk palettes and a larger cache - Raster channel lookups use a 4-block segment grid - Chunks with no water in reach return the empty raster directly - Riffle spreads stop at cells that cannot reach the chunk - Cave pool terrain is built only for chunks with a pool site - Concurrent raster builds for one chunk share a single build - Section buffers resolve air once and support bulk column reads - Cave decoration, perfection and transition capture read bounded column windows - Generation history claims and saved biome writes elect one writer per batch
This commit is contained in:
commit
6ceda7c53b
55 files changed
+1968
-481
No files matched your search
+5
@@ -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<long[]> regions) {
|
||||
if (!closing.get()) {
|
||||
|
||||
@@ -20,7 +20,7 @@ package art.arcane.iris.generation.chunk;
|
||||
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
|
||||
public final class ColumnExtentListeningHunk<T> implements Hunk<T>, ColumnExtent, ColumnRunHunk<T> {
|
||||
public final class ColumnExtentListeningHunk<T> implements Hunk<T>, ColumnExtent, ColumnRunHunk<T>, ColumnReadHunk<T> {
|
||||
private final Hunk<T> source;
|
||||
private final Listener<T> listener;
|
||||
|
||||
@@ -46,6 +46,11 @@ public final class ColumnExtentListeningHunk<T> implements Hunk<T>, 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();
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.generation.chunk;
|
||||
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
|
||||
public interface ColumnReadHunk<T> {
|
||||
void readColumn(int x, int z, int yFromInclusive, int yToInclusive, T[] target, int targetOffset);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T> void readColumn(Hunk<T> hunk, int x, int z, int yFromInclusive, int yToInclusive, T[] target,
|
||||
int targetOffset) {
|
||||
if (hunk instanceof ColumnReadHunk<?> reads) {
|
||||
((ColumnReadHunk<T>) 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,8 @@ import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.VarHandle;
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class SectionBuffer implements Hunk<NativeBlockState>, NativeSectionVolume, ColumnExtent, ColumnRunHunk<NativeBlockState> {
|
||||
public final class SectionBuffer implements Hunk<NativeBlockState>, NativeSectionVolume, ColumnExtent, ColumnRunHunk<NativeBlockState>,
|
||||
ColumnReadHunk<NativeBlockState> {
|
||||
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<NativeBlockState>, 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<NativeBlockState>, 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<NativeBlockState>, 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<NativeBlockState>, NativeSectio
|
||||
return state(id);
|
||||
}
|
||||
}
|
||||
return AIR.get();
|
||||
return air();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -126,6 +134,32 @@ public final class SectionBuffer implements Hunk<NativeBlockState>, 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<NativeBlockState>, 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) {
|
||||
|
||||
@@ -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<IrisContext> CONTEXT = new ThreadLocal<>();
|
||||
private static final int SLOTS = 512;
|
||||
private static final AtomicReferenceArray<Binding> BINDINGS = new AtomicReferenceArray<>(SLOTS);
|
||||
private static final ThreadLocal<Binding> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
+3
-5
@@ -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);
|
||||
}
|
||||
}
|
||||
+6
@@ -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) {
|
||||
|
||||
+93
-3
@@ -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<LastRaster> last;
|
||||
private final Set<Long> queued;
|
||||
private final ConcurrentHashMap<Long, CompletableFuture<ChunkWaterRaster>> building;
|
||||
private final RasterFailures failures;
|
||||
private final RasterFailures windowPreviewFailures;
|
||||
private final RasterFailures regionPreviewFailures;
|
||||
private final Set<Object> previews;
|
||||
private final LinkedHashMap<WindowKey, WindowPrefetch> prefetches;
|
||||
private final Map<Object, PregenArea> pregenAreas;
|
||||
private final HydrologyBinding binding;
|
||||
private final Function<PreparedHydrologyCacheIdentity, HydrologyPlanStore> 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<ChunkWaterRaster> claim = new CompletableFuture<>();
|
||||
CompletableFuture<ChunkWaterRaster> 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<ChunkWaterRaster> 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<WindowKey> windowKeys = neighbourhood(chunkX, chunkZ);
|
||||
@@ -667,7 +729,7 @@ public final class HydrologyRuntime {
|
||||
&& (!known.prepared().isDone() || known.evictions() == evictions)) {
|
||||
return;
|
||||
}
|
||||
CompletableFuture<Void> prepared = cache.prepareWindowsAround(chunkX, chunkZ, chunkX, chunkZ, 1 + settings.prefetchRings());
|
||||
CompletableFuture<Void> 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<Void> 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<WindowKey> neighbourhood(int chunkX, int chunkZ) {
|
||||
WindowKey own = WindowKey.containing(chunkX << 4, chunkZ << 4);
|
||||
List<WindowKey> keys = new ArrayList<>(9);
|
||||
@@ -754,6 +834,16 @@ public final class HydrologyRuntime {
|
||||
private record WindowPrefetch(CompletableFuture<Void> 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;
|
||||
|
||||
@@ -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<Void> 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<Long, Slot> cells;
|
||||
private final ConcurrentLinkedQueue<Long> insertionOrder;
|
||||
private final int tileShift;
|
||||
private final int tileMask;
|
||||
private final int tileCells;
|
||||
private final int capacityTiles;
|
||||
private final ConcurrentHashMap<Long, Tile> 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<HydrologySampleBuffer> 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<HydrologySampleBuffer> claimAndSample(Nodes nodes, Slot[] slots) {
|
||||
CompletableFuture<HydrologySampleBuffer> 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<HydrologySampleBuffer> 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<HydrologySampleBuffer> 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<HydrologySampleBuffer> own, Slot[] slots) {
|
||||
int unresolved = 0;
|
||||
for (int index = 0; index < slots.length; index++) {
|
||||
CompletableFuture<HydrologySampleBuffer> batch = slots[index].batch();
|
||||
if (batch != own && !await(batch)) {
|
||||
slots[index] = null;
|
||||
unresolved++;
|
||||
}
|
||||
}
|
||||
return unresolved;
|
||||
}
|
||||
|
||||
private static boolean await(CompletableFuture<HydrologySampleBuffer> 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<Map.Entry<Long, Tile>> idle = new ArrayList<>(tiles.size());
|
||||
for (Map.Entry<Long, Tile> 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<Long, Tile> 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<HydrologySampleBuffer> batch, int index) {
|
||||
private static boolean await(CompletableFuture<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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<Void> 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<CompletableFuture<Void>> 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 {
|
||||
|
||||
+6
-6
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Pond> lakes = regionLakes(plans.values());
|
||||
|
||||
@@ -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<Pond> ponds;
|
||||
private final List<Pond> lakes;
|
||||
private final List<SeaInlet> 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<Reach> reaches, List<Pond> ponds, List<Pond> lakes, List<SeaInlet> inlets) {
|
||||
private ChunkFeatures(List<Reach> reaches, List<Pond> ponds, List<Pond> lakes, List<SeaInlet> 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<Reach> reaches = new ArrayList<>();
|
||||
List<Pond> ponds = new ArrayList<>();
|
||||
List<SeaInlet> 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<Pond> 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<WindowKey, IndexedWindow> windowsByKey(int chunkX, int chunkZ, List<IndexedWindow> windows) {
|
||||
|
||||
@@ -37,7 +37,7 @@ public final class ChunkRasterizer {
|
||||
List<RegionPlan> 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);
|
||||
|
||||
+78
-18
@@ -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<HydrologyTier> tiers, LongPalette features, Palette<String> 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<T> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-15
@@ -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<HydrologyTier> tierOut = new ChunkWaterRaster.Palette<>(new HydrologyTier[columns]);
|
||||
ChunkWaterRaster.LongPalette featureOut = new ChunkWaterRaster.LongPalette();
|
||||
ChunkWaterRaster.Palette<String> 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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<Field> 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);
|
||||
}
|
||||
|
||||
+6
-1
@@ -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<? extends CarvedTerrain> 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);
|
||||
|
||||
+18
-3
@@ -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<Integer> path) {
|
||||
float length = 0f;
|
||||
for (int station = 1; station < path.size(); station++) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<TerrainBatchScratch> BATCH_SCRATCH = ThreadLocal.withInitial(TerrainBatchScratch::new);
|
||||
private static final ThreadLocal<HydrologyBaseHeights> 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<IrisInterpolator, IdentityHashMap<IrisBiome, GeneratorBounds>> generatorBounds;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient RegionBounds regionBounds;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient ProceduralStream<IrisRegion> contextRegionStream;
|
||||
private Set<IrisBiome> 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<IrisBiome> 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<IrisBiome, GeneratorBounds> 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<IrisBiome> picks(IrisRegion region, InferredType type) {
|
||||
return loadInferredBiomes(biomeKeys(region, type), type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<IrisBiome> descendants(IrisBiome biome) {
|
||||
Set<IrisBiome> reached = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
ArrayDeque<IrisBiome> 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<IrisBiome, GeneratorBounds> localBounds = new IdentityHashMap<>(8);
|
||||
private IrisComplex complex;
|
||||
private Engine engine;
|
||||
private int group;
|
||||
private IrisGenerator[] generators;
|
||||
private IdentityHashMap<IrisBiome, GeneratorBounds> cachedBounds;
|
||||
private SharedBoundsBiomes sharedBiomes;
|
||||
private BakedStream<IrisBiome> 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<IrisBiome, GeneratorBounds> cachedBounds) {
|
||||
private void bind(IrisComplex complex, Engine engine, int group, IrisGenerator[] generators,
|
||||
IdentityHashMap<IrisBiome, GeneratorBounds> 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<IrisBiome> 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> 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) {
|
||||
}
|
||||
}
|
||||
@@ -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<IrisRegion, Constants> regions;
|
||||
private final Map<IrisBiome, NoiseBounds[]> picks;
|
||||
|
||||
private RegionBounds(Map<IrisRegion, Constants> regions, Map<IrisBiome, NoiseBounds[]> picks) {
|
||||
this.regions = regions;
|
||||
this.picks = picks;
|
||||
}
|
||||
|
||||
static RegionBounds compile(Collection<IrisRegion> regions, int groups, Reach reach, GroupBounds bounds) {
|
||||
IdentityHashMap<IrisRegion, Constants> compiled = new IdentityHashMap<>(Math.max(16, regions.size() * 2));
|
||||
IdentityHashMap<IrisBiome, NoiseBounds[]> 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<IrisBiome> picked, int groups, Reach reach, GroupBounds bounds,
|
||||
Map<IrisBiome, NoiseBounds[]> picks) {
|
||||
if (picked.isEmpty()) {
|
||||
return new NoiseBounds[groups];
|
||||
}
|
||||
Set<IrisBiome> reached = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
for (IrisBiome biome : picked) {
|
||||
Collection<IrisBiome> 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<IrisBiome> 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<IrisBiome> 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<IrisBiome> picks(IrisRegion region, InferredType type);
|
||||
|
||||
Collection<IrisBiome> descendants(IrisBiome biome);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface GroupBounds {
|
||||
NoiseBounds of(int group, IrisBiome biome);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<NativeBlockState>
|
||||
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<NativeBlockState>
|
||||
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<NativeBlockState>
|
||||
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<NativeBlockState> output;
|
||||
private final NativeBlockState[] states;
|
||||
private int x;
|
||||
private int z;
|
||||
private int limit;
|
||||
private int from;
|
||||
private int to;
|
||||
|
||||
private ColumnWindow(Hunk<NativeBlockState> 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;
|
||||
|
||||
@@ -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<NativeBlockSt
|
||||
StateFlags states = new StateFlags();
|
||||
NativeBlockState air = AIR.get();
|
||||
NativeBlockState water = WATER.get();
|
||||
NativeBlockState[] column = new NativeBlockState[output.getHeight()];
|
||||
for (int x = firstSlice; x < lastSlice; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
perfectColumn(output, x, z, surfaces, states, air, water);
|
||||
perfectColumn(output, x, z, surfaces, states, air, water, column);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void perfectColumn(Hunk<NativeBlockState> 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<NativeBlockSt
|
||||
if ((flags & SPIKE) != 0) {
|
||||
block = normalizeSpike(block, output, x, z, y, air, water);
|
||||
flags = block == null ? 0 : states.of(block);
|
||||
columnRead = false;
|
||||
}
|
||||
}
|
||||
boolean now = (flags & SOLID) != 0;
|
||||
@@ -233,11 +240,11 @@ public class IrisPerfectionModifier extends EngineAssignedModifier<NativeBlockSt
|
||||
burst.complete();
|
||||
}
|
||||
|
||||
private static int getHeight(Hunk<NativeBlockState> 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;
|
||||
}
|
||||
|
||||
+12
-3
@@ -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<NativeBlockState, BoundaryColumnGeometry.Voxel> 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;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,13 @@ public final class BakedStream<T> extends BasicStream<T> 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);
|
||||
|
||||
@@ -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<Long, TransitionGenerationPlan> transitionPlanCache;
|
||||
private final ArrayBlockingQueue<PendingSemanticClaim> 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 {
|
||||
|
||||
@@ -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<Matter> 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;
|
||||
|
||||
@@ -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<Deflater> 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<Long, List<PendingWrite>> batches = new LinkedHashMap<>();
|
||||
List<PendingWrite> 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<PendingWrite> 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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<long[]> regions) {
|
||||
if (!closing.get()) {
|
||||
|
||||
@@ -13,6 +13,7 @@ public final class PregenHydrology implements AutoCloseable {
|
||||
private final Supplier<Engine> engine;
|
||||
private HydrologyRuntime owner;
|
||||
private HydrologyPregeneration epoch;
|
||||
private int[] bounds;
|
||||
private boolean closed;
|
||||
|
||||
public PregenHydrology(Supplier<Engine> 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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<NativeBlockState> 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<NativeBlockState> 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();
|
||||
|
||||
@@ -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<Throwable> failure = new AtomicReference<>();
|
||||
List<Thread> 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);
|
||||
|
||||
-80
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+58
@@ -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<ChunkWaterRaster> first = new AtomicReference<>();
|
||||
AtomicReference<ChunkWaterRaster> 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<int[]> 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());
|
||||
|
||||
+2
-3
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+1
-1
@@ -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));
|
||||
}
|
||||
|
||||
+1
-1
@@ -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());
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Reach> 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<Reach> 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);
|
||||
}
|
||||
}
|
||||
+26
-6
@@ -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<Long> 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<Long, HydrologyCaveCell> fill(HydrologySettings settings, VoxelWorld world, int chunkX, int chunkZ) {
|
||||
Map<Long, HydrologyCaveCell> 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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())) {
|
||||
|
||||
+4
-4
@@ -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<IrisBiome> 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 {
|
||||
|
||||
@@ -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<IrisBiome, NoiseBounds[]> 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<IrisBiome, NoiseBounds> 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<IrisBiome, NoiseBounds> 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<IrisBiome, List<IrisBiome>> descendants(IrisBiome first, List<IrisBiome> firstDescendants,
|
||||
IrisBiome second, List<IrisBiome> secondDescendants) {
|
||||
Map<IrisBiome, List<IrisBiome>> 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<InferredType, List<IrisBiome>> picks, Map<IrisBiome, List<IrisBiome>> descendants)
|
||||
implements RegionBounds.Reach {
|
||||
@Override
|
||||
public List<IrisBiome> picks(IrisRegion region, InferredType type) {
|
||||
return picks.get(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<IrisBiome> descendants(IrisBiome biome) {
|
||||
List<IrisBiome> known = descendants.get(biome);
|
||||
return known == null ? List.of(biome) : known;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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]) {
|
||||
|
||||
+17
-11
@@ -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<Thread> 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<Thread> 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<Thread> workers) {
|
||||
int blocked = 0;
|
||||
private static int queuedOnHistory(GenerationHistory history, List<Thread> 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,
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
Reference in new issue
Block a user