This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-21 10:04:22 -04:00
1 parent 8e3ef1b651
commit 197d2c2461
73 files changed
+4586 -424

No files matched your search

@@ -0,0 +1,75 @@
package art.arcane.iris.generation.hydrology;
final class HydrologyCacheBudget {
private static final long MEBIBYTE = 1024L * 1024L;
private static final HydrologyCacheBudget REGIONAL_RUNTIME = forHeap(Runtime.getRuntime().maxMemory(), true);
private static final HydrologyCacheBudget LOCAL_RUNTIME = forHeap(Runtime.getRuntime().maxMemory(), false);
private final long plannerBytes;
private final long publicationBytes;
private final boolean regionalEnabled;
private HydrologyCacheBudget(long retainedBytes, boolean regionalEnabled) {
plannerBytes = retainedBytes / 2L;
publicationBytes = retainedBytes - plannerBytes;
this.regionalEnabled = regionalEnabled;
}
static HydrologyCacheBudget runtime() {
return REGIONAL_RUNTIME;
}
static HydrologyCacheBudget runtime(boolean regionalEnabled) {
return regionalEnabled ? REGIONAL_RUNTIME : LOCAL_RUNTIME;
}
static HydrologyCacheBudget forHeap(long maximumHeapBytes, boolean regionalEnabled) {
if (maximumHeapBytes <= 0L) {
throw new IllegalArgumentException("Maximum heap size must be positive.");
}
return new HydrologyCacheBudget(Math.clamp(maximumHeapBytes / 8L, 8L * MEBIBYTE, 256L * MEBIBYTE), regionalEnabled);
}
static long sharedStudioBytes() {
return Math.clamp(Runtime.getRuntime().maxMemory() / 128L, MEBIBYTE, 16L * MEBIBYTE);
}
long tileBytes() {
return publicationBytes * 3L / 4L;
}
long columnBytes() {
return publicationBytes / 8L;
}
long diagnosticBytes() {
return publicationBytes / 8L;
}
long routingBytes() {
return regionalEnabled ? plannerBytes / 8L : plannerBytes * 3L / 16L;
}
long ownerBytes() {
return regionalEnabled ? plannerBytes / 4L : plannerBytes * 3L / 4L;
}
long edgeBytes() {
return plannerBytes / 16L;
}
long regionalDraftBytes() {
return regionalEnabled ? plannerBytes / 8L : 0L;
}
long regionalCoarseBytes() {
return regionalEnabled ? plannerBytes / 8L : 0L;
}
long regionalTerrainBytes() {
return regionalEnabled ? plannerBytes / 4L : 0L;
}
long regionalReachBytes() {
return regionalEnabled ? plannerBytes / 16L : 0L;
}
}
@@ -0,0 +1,123 @@
package art.arcane.iris.generation.hydrology;
import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlan;
import java.util.List;
final class HydrologyCacheWeights {
private static final long ENTRY_BYTES = 192L;
private static final long TERRAIN_BYTES = 256L;
private static final long POINT_BYTES = 40L;
private HydrologyCacheWeights() {
}
static int bounded(long estimatedBytes, long budget, int maximumEntries) {
return saturated(Math.max(estimatedBytes, Math.ceilDiv(budget, maximumEntries)));
}
static int bounded(long estimatedBytes, long budget, int maximumEntries, int units, int maximumUnits) {
return bounded(Math.max(estimatedBytes, Math.ceilDiv((long) units * budget, maximumUnits)), budget, maximumEntries);
}
static int tile(HydrologyTile tile) {
long bytes = network(tile.nodes(), tile.edges(), tile.outlets(), tile.courses(), tile.cavePlans());
bytes += diagnostics(tile.localDiagnosticCandidates());
bytes += (long) tile.regionalCourseIds().size() * 64L;
RiverFootprint footprint = tile.footprint();
if (footprint != null) {
bytes += (long) footprint.size() * 64L;
for (HydrologyColumnSample sample : footprint.columns().values()) {
bytes += column(sample);
}
}
return saturated(bytes);
}
static int column(HydrologyColumnSample sample) {
return saturated(96L + (long) sample.layers().size() * 192L);
}
static int diagnostics(List<HydrologyDiagnosticCandidate> diagnostics) {
return saturated(ENTRY_BYTES + (long) diagnostics.size() * 112L);
}
static int routing(SourceRoutingContext context) {
return saturated(ENTRY_BYTES + grid(context.grid()) + routingPlan(context.surfaceRouting())
+ routingPlan(context.undergroundRouting()) + diagnostics(context.diagnostics()));
}
static int owner(CrossTileResolvedOwner owner) {
HydrologyCaveCourseFilter.Result result = owner.draft().result();
long bytes = network(result.nodes(), result.edges(), result.outlets(), result.courses(), result.cavePlans())
+ diagnostics(owner.draft().diagnostics());
for (CrossTileRejectedCourse rejection : owner.observedRejections()) {
bytes += 48L + course(rejection.course());
}
return saturated(bytes);
}
static int points(List<HydrologyPoint> points) {
return saturated(ENTRY_BYTES + (long) points.size() * POINT_BYTES);
}
static int regionalNetwork(HydrologyRegionalNetwork network) {
return saturated(network(network.nodes(), network.edges(), network.outlets(), network.courses(), network.cavePlans())
+ diagnostics(network.diagnostics()));
}
static int regionalCoarse(HydrologyRegionalCoarsePlan plan) {
HydrologyRegionalGraph.Tree tree = plan.tree();
long treeBytes = tree == null ? 0L : 96L + (long) tree.selected().length * 8L
+ (long) tree.accumulation().length * 4L + tree.retainedLabelCount() * 96L;
return saturated(ENTRY_BYTES + grid(plan.grid()) + treeBytes + (long) plan.outlets().size() * 224L
+ (long) plan.sources().size() * 32L + (long) plan.envelopes().size() * 64L);
}
static int terrain(HydrologyTerrainSample terrain) {
return saturated(128L + (terrain == null ? 0L : TERRAIN_BYTES));
}
private static long grid(HydrologySampledGrid grid) {
return grid == null ? 0L : 96L + (long) grid.nodes().size() * (64L + TERRAIN_BYTES);
}
private static long routingPlan(HydrologyRoutingPlan plan) {
if (plan == null) {
return 0L;
}
return 128L + (long) plan.potential().length * 8L
+ ((long) plan.parent().length + plan.outletIndex().length + plan.routeLengths().length) * 4L
+ (long) plan.outlets().size() * 224L;
}
private static long network(List<DrainageNode> nodes, List<DrainageEdge> edges, List<RiverOutlet> outlets,
List<RiverCourse> courses, List<HydrologyCavePlan> cavePlans) {
long bytes = ENTRY_BYTES + (long) nodes.size() * (96L + TERRAIN_BYTES) + (long) outlets.size() * 224L;
for (DrainageEdge edge : edges) {
bytes += 96L + points(edge.centerline());
}
for (RiverCourse course : courses) {
bytes += course(course);
}
for (HydrologyCavePlan plan : cavePlans) {
bytes += plan.estimatedRetainedBytes();
}
return bytes;
}
private static long course(RiverCourse course) {
long bytes = 128L;
for (DrainageEdge edge : course.drainageEdges()) {
bytes += 96L + points(edge.centerline());
}
for (HydraulicSegment segment : course.segments()) {
bytes += 128L + points(segment.centerline()) + (long) segment.channelProfile().size() * 16L;
}
return bytes;
}
private static int saturated(long bytes) {
return (int) Math.clamp(bytes, 1L, Integer.MAX_VALUE);
}
}
@@ -20,6 +20,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.ForkJoinWorkerThread;
final class HydrologyCrossTileResolver {
private static final int FALLBACK_TRIAL_PARALLELISM = 16;
@@ -283,11 +284,20 @@ final class HydrologyCrossTileResolver {
List<OutletCandidate> outlets,
int start
) {
int end = Math.min(outlets.size(), start + FALLBACK_TRIAL_PARALLELISM);
int parallelism = HydrologyPlanningAdmission.effectiveParallelism(
Thread.currentThread() instanceof ForkJoinWorkerThread worker
? worker.getPool().getParallelism()
: IrisPlatforms.isBound() ? MultiBurst.hydrology.parallelism() : 1);
int batchSize = parallelism <= 2 ? 1 : Math.max(1, Math.min(HydrologyPlanningAdmission.maximumTrials(),
Math.min(FALLBACK_TRIAL_PARALLELISM, parallelism)));
int end = Math.min(outlets.size(), start + batchSize);
ArrayList<Callable<FallbackTrial>> tasks = new ArrayList<>(end - start);
HydrologyPlanner.PlanningSamples ownerSamples = planner.planningSamples.get();
HydrologyLandHeightCache sharedLandHeights = planner.naturalSampler == null ? null
: ownerSamples == null ? new HydrologyLandHeightCache() : ownerSamples.fallbackLandHeights();
for (int index = start; index < end; index++) {
OutletCandidate outlet = outlets.get(index);
tasks.add(() -> compileFallbackTrial(context, outlet));
tasks.add(() -> compileFallbackTrial(context, outlet, sharedLandHeights));
}
List<FallbackTrial> trials = HydrologyForkJoin.invokeAll(tasks,
IrisPlatforms.isBound() ? MultiBurst.hydrology : null);
@@ -305,7 +315,11 @@ final class HydrologyCrossTileResolver {
return trials;
}
private FallbackTrial compileFallbackTrial(FallbackContext context, OutletCandidate outlet) {
private FallbackTrial compileFallbackTrial(
FallbackContext context,
OutletCandidate outlet,
HydrologyLandHeightCache sharedLandHeights
) {
HydrologyPlanner.PlanningSamples previousSamples = planner.planningSamples.get();
DraftProfile previousProfile = planner.draftProfiles.get();
DraftProfile profile = new DraftProfile();
@@ -314,7 +328,7 @@ final class HydrologyCrossTileResolver {
HashMap<SourceCompilationKey, SourceCompilation> sourceCompilations = new HashMap<>(context.sourceCompilations());
HydrologyFootprintCompiler footprints = null;
try {
planner.planningSamples.set(new HydrologyPlanner.PlanningSamples());
planner.planningSamples.set(new HydrologyPlanner.PlanningSamples(sharedLandHeights));
planner.draftProfiles.set(profile);
footprints = new HydrologyFootprintCompiler(
planner.settings,
@@ -391,7 +391,7 @@ final class HydrologyFootprintCompiler {
}
RiverFootprint build(Long2ObjectLinkedOpenHashMap<FootprintMutableColumn> columns) {
LinkedHashMap<Long, HydrologyColumnSample> immutable = new LinkedHashMap<>();
Long2ObjectOpenHashMap<HydrologyColumnSample> immutable = new Long2ObjectOpenHashMap<>(columns.size());
for (Long2ObjectMap.Entry<FootprintMutableColumn> entry : columns.long2ObjectEntrySet()) {
immutable.put(entry.getLongKey(), entry.getValue().build());
}
@@ -0,0 +1,71 @@
package art.arcane.iris.generation.hydrology;
import art.arcane.volmlib.util.cache.CacheKey;
import it.unimi.dsi.fastutil.longs.Long2DoubleLinkedOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2DoubleMap;
final class HydrologyLandHeightCache {
private static final int MAXIMUM_ENTRIES = 131_072;
private static final int MAXIMUM_STRIPES = 64;
private final Stripe[] stripes;
HydrologyLandHeightCache() {
this(MAXIMUM_ENTRIES);
}
HydrologyLandHeightCache(int maximumEntries) {
if (maximumEntries < 1 || Integer.bitCount(maximumEntries) != 1) {
throw new IllegalArgumentException("Land height cache size must be a positive power of two.");
}
int stripeCount = Math.min(MAXIMUM_STRIPES, maximumEntries);
stripes = new Stripe[stripeCount];
for (int index = 0; index < stripeCount; index++) {
stripes[index] = new Stripe(maximumEntries / stripeCount);
}
}
double sample(int blockX, int blockZ, HydrologyNaturalTerrainSampler sampler) {
long packed = RiverFootprint.pack(blockX, blockZ);
Stripe stripe = stripe(packed);
synchronized (stripe) {
double cached = stripe.heights.getAndMoveToFirst(packed);
if (!Double.isNaN(cached) || stripe.heights.containsKey(packed)) {
return cached;
}
double sampled = sampler.sampleLandHeight(blockX, blockZ);
stripe.put(packed, sampled);
return sampled;
}
}
void seed(Long2DoubleMap samples) {
for (Long2DoubleMap.Entry entry : samples.long2DoubleEntrySet()) {
Stripe stripe = stripe(entry.getLongKey());
synchronized (stripe) {
stripe.put(entry.getLongKey(), entry.getDoubleValue());
}
}
}
private Stripe stripe(long packed) {
return stripes[(int) CacheKey.mix(packed) & (stripes.length - 1)];
}
private static final class Stripe {
private final int maximumEntries;
private final Long2DoubleLinkedOpenHashMap heights;
private Stripe(int maximumEntries) {
this.maximumEntries = maximumEntries;
heights = new Long2DoubleLinkedOpenHashMap(maximumEntries);
heights.defaultReturnValue(Double.NaN);
}
private void put(long packed, double height) {
if (heights.size() >= maximumEntries && !heights.containsKey(packed)) {
heights.removeLastDouble();
}
heights.putAndMoveToFirst(packed, height);
}
}
}
@@ -6,4 +6,9 @@ public interface HydrologyNaturalTerrainSampler extends HydrologyRoutingTerrainS
default HydrologyTerrainSample sampleBasisWithoutSlope(int blockX, int blockZ) {
return sampleBasis(blockX, blockZ);
}
default double sampleLandHeight(int blockX, int blockZ) {
HydrologyTerrainSample terrain = sampleBasisWithoutSlope(blockX, blockZ);
return terrain == null || terrain.ocean() ? Double.NaN : terrain.naturalHeight();
}
}
@@ -11,6 +11,7 @@ import art.arcane.volmlib.util.noise.SimplexNoise;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2DoubleOpenHashMap;
import java.util.ArrayList;
import java.util.Comparator;
@@ -162,15 +163,22 @@ public final class HydrologyPlanner {
this.geometrySampler = Objects.requireNonNull(geometrySampler, "geometrySampler");
this.minimumY = minimumY;
this.caveViewFactory = Objects.requireNonNull(caveViewFactory, "caveViewFactory");
HydrologyCacheBudget cacheBudget = HydrologyCacheBudget.runtime(settings.routing().regional().enabled());
this.routingContexts = Caffeine.newBuilder()
.maximumSize(ROUTING_CONTEXT_CACHE_SIZE)
.maximumWeight(cacheBudget.routingBytes())
.weigher((HydrologyTileKey key, SourceRoutingContext context) -> HydrologyCacheWeights.bounded(
HydrologyCacheWeights.routing(context), cacheBudget.routingBytes(), ROUTING_CONTEXT_CACHE_SIZE))
.build();
this.resolvingRoutingContexts = new ConcurrentHashMap<>();
this.resolvedOwners = Caffeine.newBuilder()
.maximumSize(RESOLVED_OWNER_CACHE_SIZE)
.maximumWeight(cacheBudget.ownerBytes())
.weigher((HydrologyTileKey key, CrossTileResolvedOwner owner) -> HydrologyCacheWeights.bounded(
HydrologyCacheWeights.owner(owner), cacheBudget.ownerBytes(), RESOLVED_OWNER_CACHE_SIZE))
.build();
this.refinedEdgeCache = Caffeine.newBuilder()
.maximumSize(REFINED_EDGE_CACHE_SIZE)
.maximumWeight(cacheBudget.edgeBytes())
.weigher((RefinedEdgeKey key, List<HydrologyPoint> points) -> HydrologyCacheWeights.bounded(
HydrologyCacheWeights.points(points), cacheBudget.edgeBytes(), REFINED_EDGE_CACHE_SIZE))
.build();
this.resolvingOwners = new ConcurrentHashMap<>();
this.planningSamples = new ThreadLocal<>();
@@ -304,7 +312,7 @@ public final class HydrologyPlanner {
maximumCrossTileDependencyOffset(key),
maximumCrossTileDependencyOwners(key)
);
CrossTileResolvedOwner resolved = resolveCrossTileOwner(key, context);
CrossTileResolvedOwner resolved = resolveCrossTileOwner(key, context, true);
return new CrossTileResolution(
resolved.draft(),
resolved.observedRejections(),
@@ -316,7 +324,8 @@ public final class HydrologyPlanner {
CrossTileResolvedOwner resolveCrossTileOwner(
HydrologyTileKey key,
CrossTileResolutionContext context
CrossTileResolutionContext context,
boolean retainFootprintCompiler
) {
CrossTileResolvedOwner local = context.resolved(key);
if (local != null) {
@@ -347,7 +356,7 @@ public final class HydrologyPlanner {
draft = crossTile.compileOwnerDraft(
key,
admission,
key.equals(context.root())
retainFootprintCompiler
);
} catch (RuntimeException | Error failure) {
admission.primaryFailure = failure;
@@ -379,7 +388,7 @@ public final class HydrologyPlanner {
for (HydrologyTileKey candidateKey : candidateKeys) {
HydrologyForkJoin.Task<CrossTileResolvedOwner> prepared = preparedOwners.get(candidateKey);
CrossTileResolvedOwner owner = prepared == null
? resolveCrossTileOwner(candidateKey, context) : prepared.await();
? resolveCrossTileOwner(candidateKey, context, false) : prepared.await();
context.remember(candidateKey, owner);
resolved.add(owner);
}
@@ -408,7 +417,7 @@ public final class HydrologyPlanner {
maximumCrossTileDependencyOffset(key),
maximumCrossTileDependencyOwners(key)
);
return resolveCrossTileOwner(key, context);
return resolveCrossTileOwner(key, context, false);
}
static CrossTileResolvedOwner awaitResolvedOwner(
@@ -578,6 +587,35 @@ public final class HydrologyPlanner {
return terrain == null || terrain.ocean() ? null : terrain;
}
double sampleLandHeight(int blockX, int blockZ) {
if (naturalSampler == null) {
HydrologyTerrainSample terrain = sampleLandBasisWithoutSlope(blockX, blockZ);
return terrain == null ? Double.NaN : terrain.naturalHeight();
}
PlanningSamples samples = planningSamples.get();
if (samples == null) {
return naturalSampler.sampleLandHeight(blockX, blockZ);
}
long packed = RiverFootprint.pack(blockX, blockZ);
HydrologyTerrainSample terrain = samples.basis.get(packed);
if (terrain != null || samples.basis.containsKey(packed)) {
return terrain == null || terrain.ocean() ? Double.NaN : terrain.naturalHeight();
}
terrain = samples.basisWithoutSlope.get(packed);
if (terrain != null || samples.basisWithoutSlope.containsKey(packed)) {
return terrain == null || terrain.ocean() ? Double.NaN : terrain.naturalHeight();
}
double cached = samples.landHeights.get(packed);
if (!Double.isNaN(cached) || samples.landHeights.containsKey(packed)) {
return cached;
}
double sampled = samples.sharedLandHeights == null
? naturalSampler.sampleLandHeight(blockX, blockZ)
: samples.sharedLandHeights.sample(blockX, blockZ, naturalSampler);
samples.landHeights.put(packed, sampled);
return sampled;
}
HydrologyTerrainSample sampleDetailed(int blockX, int blockZ) {
PlanningSamples samples = planningSamples.get();
if (samples == null) {
@@ -618,6 +656,11 @@ public final class HydrologyPlanner {
this.sampler = Objects.requireNonNull(sampler, "sampler");
}
@Override
public boolean supportsSharedGridSamples() {
return true;
}
@Override
public HydrologyTerrainSample[] sampleGrid(GridRequest request) {
int minimumX = request.minimumX();
@@ -648,6 +691,8 @@ public final class HydrologyPlanner {
static final class PlanningSamples {
private final Long2ObjectOpenHashMap<HydrologyTerrainSample> basis;
private final Long2ObjectOpenHashMap<HydrologyTerrainSample> basisWithoutSlope;
private final Long2DoubleOpenHashMap landHeights;
private HydrologyLandHeightCache sharedLandHeights;
final Long2ObjectOpenHashMap<HydrologyTerrainSample> detailed;
private final Long2ObjectOpenHashMap<HydrologyRoutingTerrainSampler.NaturalClassification> classifications;
private final HashMap<HydrologyGeometrySampler.Request, Integer> geometry;
@@ -661,8 +706,15 @@ public final class HydrologyPlanner {
final HydrologyCaveContainmentPlanner.ValidationCache caveValidations;
PlanningSamples() {
this(null);
}
PlanningSamples(HydrologyLandHeightCache sharedLandHeights) {
this.sharedLandHeights = sharedLandHeights;
this.basis = new Long2ObjectOpenHashMap<>();
this.basisWithoutSlope = new Long2ObjectOpenHashMap<>();
this.landHeights = new Long2DoubleOpenHashMap();
this.landHeights.defaultReturnValue(Double.NaN);
this.detailed = new Long2ObjectOpenHashMap<>();
this.classifications = new Long2ObjectOpenHashMap<>();
this.geometry = new HashMap<>();
@@ -675,6 +727,14 @@ public final class HydrologyPlanner {
this.caveCandidates = new HydrologyCaveCourseFilter.CandidateCache();
this.caveValidations = new HydrologyCaveContainmentPlanner.ValidationCache();
}
HydrologyLandHeightCache fallbackLandHeights() {
if (sharedLandHeights == null) {
sharedLandHeights = new HydrologyLandHeightCache();
sharedLandHeights.seed(landHeights);
}
return sharedLandHeights;
}
}
final class ColorRankedDraftAdmission implements CrossTileDraftAdmission, AutoCloseable {
@@ -702,7 +762,8 @@ public final class HydrologyPlanner {
@Override
public void prepare() {
if (ownerRank == 0 || settings.crossTileColorPeriod() != 2 || !sourcePlanner.hasRoutedSourceSearch()
|| !(Thread.currentThread() instanceof ForkJoinWorkerThread worker)) {
|| !(Thread.currentThread() instanceof ForkJoinWorkerThread worker)
|| HydrologyPlanningAdmission.effectiveParallelism(worker.getPool().getParallelism()) <= 2) {
return;
}
int radius = settings.crossTileColorPeriod() - 1;
@@ -0,0 +1,134 @@
package art.arcane.iris.generation.hydrology;
import java.util.Objects;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BooleanSupplier;
final class HydrologyPlanningAdmission {
private static final long BYTES_PER_ROOT = 2L * 1024L * 1024L * 1024L;
private static final int MAXIMUM_ROOTS = rootLimit(
Runtime.getRuntime().maxMemory(), Runtime.getRuntime().availableProcessors());
private static final int MAXIMUM_TRIALS = trialLimit(Runtime.getRuntime().maxMemory(), MAXIMUM_ROOTS);
private static final HydrologyPlanningAdmission ROOTS = new HydrologyPlanningAdmission(MAXIMUM_ROOTS);
private final Semaphore permits;
HydrologyPlanningAdmission(int maximumRoots) {
if (maximumRoots < 1) {
throw new IllegalArgumentException("Maximum hydrology roots must be positive.");
}
permits = new Semaphore(maximumRoots, true);
}
static Permit acquireRoot(BooleanSupplier cancelled) {
return ROOTS.acquire(cancelled);
}
static int maximumRoots() {
return MAXIMUM_ROOTS;
}
static int maximumTrials() {
return MAXIMUM_TRIALS;
}
static int effectiveParallelism(int poolParallelism) {
return effectiveParallelism(poolParallelism, Runtime.getRuntime().availableProcessors());
}
static int effectiveParallelism(int poolParallelism, int availableProcessors) {
return Math.max(1, Math.min(poolParallelism, availableProcessors));
}
static int trialLimit(long maximumHeap, int maximumRoots) {
if (maximumRoots < 1) {
throw new IllegalArgumentException("Maximum hydrology roots must be positive.");
}
return (int) Math.clamp(maximumHeap / maximumRoots / (1024L * 1024L * 1024L), 1L, 16L);
}
static int rootLimit(long maximumHeap, int processors) {
return (int) Math.max(1L, Math.min(Math.min(8L, processors), maximumHeap / BYTES_PER_ROOT));
}
Permit acquire(BooleanSupplier cancelled) {
AdmissionWait wait = new AdmissionWait(Objects.requireNonNull(cancelled));
try {
ForkJoinPool.managedBlock(wait);
wait.requireActive();
return new Permit(permits);
} catch (InterruptedException interruption) {
Thread.currentThread().interrupt();
if (wait.acquired) {
permits.release();
}
throw new CancellationException("Hydrology planning admission interrupted.");
} catch (RuntimeException | Error failure) {
if (wait.acquired) {
permits.release();
}
throw failure;
}
}
static final class Permit implements AutoCloseable {
private final Semaphore permits;
private final AtomicBoolean released = new AtomicBoolean();
private Permit(Semaphore permits) {
this.permits = permits;
}
@Override
public void close() {
if (released.compareAndSet(false, true)) {
permits.release();
}
}
}
private final class AdmissionWait implements ForkJoinPool.ManagedBlocker {
private final BooleanSupplier cancelled;
private boolean acquired;
private AdmissionWait(BooleanSupplier cancelled) {
this.cancelled = cancelled;
}
@Override
public boolean isReleasable() {
requireActive();
if (!acquired) {
try {
acquired = permits.tryAcquire(0L, TimeUnit.NANOSECONDS);
} catch (InterruptedException interruption) {
Thread.currentThread().interrupt();
throw new CancellationException("Hydrology planning admission interrupted.");
}
}
return acquired;
}
@Override
public boolean block() throws InterruptedException {
requireActive();
if (!acquired) {
acquired = permits.tryAcquire(100L, TimeUnit.MILLISECONDS);
}
return acquired;
}
private void requireActive() {
if (Thread.currentThread().isInterrupted()) {
throw new CancellationException("Hydrology planning admission interrupted.");
}
if (cancelled.getAsBoolean()) {
throw new CancellationException("Hydrology tile cache is closed.");
}
}
}
}
@@ -1,6 +1,7 @@
package art.arcane.iris.generation.hydrology;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;
@@ -160,6 +161,20 @@ final class HydrologyRegionalGraph {
}
record Tree(Label[] selected, int[] accumulation, List<OutletCandidate> outlets) {
int retainedLabelCount() {
BitSet retained = new BitSet(accumulation.length);
for (Label label : selected) {
for (Label current = label; current != null; current = current.downstream) {
int id = current.state.id();
if (retained.get(id)) {
break;
}
retained.set(id);
}
}
return retained.cardinality();
}
int root(int source) {
return selected[source] == null ? -1 : selected[source].state.root();
}
@@ -51,14 +51,18 @@ final class HydrologyRegionalPlanner {
this.settings = planner.settings.routing().regional();
this.graph = new HydrologyRegionalGraph(planner);
this.routes = new HydrologyRegionalRoute(planner);
int minimumWeight = Math.max(1, Math.ceilDiv(settings.maximumCachedStations(), settings.maximumCachedBasins()));
HydrologyCacheBudget cacheBudget = HydrologyCacheBudget.runtime(settings.enabled());
this.drafts = Caffeine.newBuilder()
.maximumWeight(settings.maximumCachedStations())
.weigher((HydrologyTileKey key, HydrologyRegionalNetwork network) -> Math.max(minimumWeight, network.stations()))
.maximumWeight(cacheBudget.regionalDraftBytes())
.weigher((HydrologyTileKey key, HydrologyRegionalNetwork network) -> HydrologyCacheWeights.bounded(
HydrologyCacheWeights.regionalNetwork(network), cacheBudget.regionalDraftBytes(),
settings.maximumCachedBasins(), network.stations(), settings.maximumCachedStations()))
.build();
this.coarsePlans = Caffeine.newBuilder()
.maximumWeight(settings.maximumCachedStations())
.weigher((HydrologyTileKey key, HydrologyRegionalCoarsePlan plan) -> Math.max(minimumWeight, plan.weight()))
.maximumWeight(cacheBudget.regionalCoarseBytes())
.weigher((HydrologyTileKey key, HydrologyRegionalCoarsePlan plan) -> HydrologyCacheWeights.bounded(
HydrologyCacheWeights.regionalCoarse(plan), cacheBudget.regionalCoarseBytes(),
settings.maximumCachedBasins(), plan.weight(), settings.maximumCachedStations()))
.build();
}
@@ -347,8 +351,13 @@ final class HydrologyRegionalPlanner {
if (tasks.isEmpty()) {
break;
}
List<SourceTrial> completed = HydrologyForkJoin.invokeAll(tasks,
IrisPlatforms.isBound() ? MultiBurst.hydrology : null);
ArrayList<SourceTrial> completed = new ArrayList<>(tasks.size());
int batchSize = HydrologyPlanningAdmission.maximumTrials();
for (int start = 0; start < tasks.size(); start += batchSize) {
int end = Math.min(tasks.size(), start + batchSize);
completed.addAll(HydrologyForkJoin.invokeAll(tasks.subList(start, end),
IrisPlatforms.isBound() ? MultiBurst.hydrology : null));
}
for (SourceTrial trial : completed) {
if (accepted.size() >= settings.maximumTrunks()) {
break;
@@ -642,8 +651,9 @@ final class HydrologyRegionalPlanner {
int[] excavation = new int[centerline.size()];
HydrologyRegionalConnectivity connectivity = new HydrologyRegionalConnectivity();
HydrologyTerrainSampler receiver = HydrologyOceanReceiver.forCourse(planner.settings, this::sample, course);
SurfaceFootprintCompiler.PreparedCourse prepared = compiler.prepare(course);
for (SurfaceBounds bounds : courseWindows(course)) {
SurfaceFootprint footprint = compiler.compile(course, bounds);
SurfaceFootprint footprint = compiler.compile(prepared, bounds);
if (!footprint.accepted()) {
diagnostic(diagnostics, course.id(), source, coastal, footprint.rejection(), footprint.rejectionDetail());
return false;
@@ -23,16 +23,18 @@ final class HydrologyRegionalTerrainRefiner {
private final HydrologyPlanner planner;
private final HydrologyRegionalHydraulics hydraulics;
private final HydrologyCacheBudget cacheBudget;
private volatile CacheState caches;
HydrologyRegionalTerrainRefiner(HydrologyPlanner planner) {
this.planner = planner;
this.hydraulics = new HydrologyRegionalHydraulics(planner.settings);
this.caches = new CacheState();
this.cacheBudget = HydrologyCacheBudget.runtime(planner.settings.routing().regional().enabled());
this.caches = new CacheState(cacheBudget);
}
void clear() {
caches = new CacheState();
caches = new CacheState(cacheBudget);
}
HydrologyTerrainSample sample(int x, int z) {
@@ -459,10 +461,23 @@ final class HydrologyRegionalTerrainRefiner {
}
private static final class CacheState {
private final Cache<Edge, Reach> edges = Caffeine.newBuilder().maximumSize(128).build();
private final Cache<Edge, Reach> edges;
private final ConcurrentHashMap<Edge, HydrologyForkJoin.Task<Reach>> loading = new ConcurrentHashMap<>();
private final Cache<Long, Terrain> terrain = Caffeine.newBuilder().maximumSize(Math.max(MAXIMUM_SAMPLES,
Math.min(MAXIMUM_CACHED_SAMPLES, Runtime.getRuntime().maxMemory() / 8192L))).build();
private final Cache<Long, Terrain> terrain;
private CacheState(HydrologyCacheBudget budget) {
edges = Caffeine.newBuilder()
.maximumWeight(budget.regionalReachBytes())
.weigher((Edge edge, Reach reach) -> HydrologyCacheWeights.bounded(
HydrologyCacheWeights.points(reach.points()), budget.regionalReachBytes(), 128))
.build();
terrain = Caffeine.newBuilder()
.maximumWeight(budget.regionalTerrainBytes())
.weigher((Long key, Terrain terrain) -> HydrologyCacheWeights.bounded(
HydrologyCacheWeights.terrain(terrain.sample()), budget.regionalTerrainBytes(),
(int) Math.max(MAXIMUM_SAMPLES, Math.min(MAXIMUM_CACHED_SAMPLES, Runtime.getRuntime().maxMemory() / 8192L))))
.build();
}
}
private record Offer(HydrologyPoint point, double cost, int availableHead) {
@@ -657,11 +657,11 @@ final class HydrologyRouteGeometry {
int lowest = center.naturalHeight();
for (double distance = reach / 2D; distance <= reach; distance += reach / 2D) {
for (double direction = -1D; direction <= 1D; direction += 2D) {
HydrologyTerrainSample side = planner.sampleLandBasisWithoutSlope(
double side = planner.sampleLandHeight(
(int) StrictMath.round(point.x() + normalX * distance * direction),
(int) StrictMath.round(point.z() + normalZ * distance * direction));
if (side != null) {
lowest = Math.min(lowest, side.naturalHeight());
if (!Double.isNaN(side)) {
lowest = Math.min(lowest, (int) side);
}
}
}
@@ -819,6 +819,7 @@ final class HydrologyRouteGeometry {
Arrays.fill(row, Double.NaN);
}
}
CurvatureWorkspace workspace = new CurvatureWorkspace(layers);
while (true) {
CurvatureRouteSelection selection = selectCurvatureAwareTerrainRoute(
layers,
@@ -827,7 +828,8 @@ final class HydrologyRouteGeometry {
turnCost,
localPenalties,
transitionCosts,
turnCosts
turnCosts,
workspace
);
if (selection.route().length == 0) {
return selection.route();
@@ -852,17 +854,17 @@ final class HydrologyRouteGeometry {
double turnCost,
double[][] localPenalties,
double[][][] transitionCosts,
double[][][][] turnCosts
double[][][][] turnCosts,
CurvatureWorkspace workspace
) {
int layerCount = layers.size();
double[][][] costs = new double[layerCount][][];
int[][][] predecessors = new int[layerCount][][];
double[][] previousCosts = workspace.firstCosts;
double[][] currentCosts = workspace.secondCosts;
int[][][] predecessors = workspace.predecessors;
List<RouteCandidate> firstLayer = layers.getFirst();
List<RouteCandidate> secondLayer = layers.get(1);
costs[1] = new double[firstLayer.size()][secondLayer.size()];
predecessors[1] = new int[firstLayer.size()][secondLayer.size()];
for (int firstIndex = 0; firstIndex < firstLayer.size(); firstIndex++) {
Arrays.fill(costs[1][firstIndex], Double.POSITIVE_INFINITY);
Arrays.fill(previousCosts[firstIndex], Double.POSITIVE_INFINITY);
Arrays.fill(predecessors[1][firstIndex], -1);
for (int secondIndex = 0; secondIndex < secondLayer.size(); secondIndex++) {
RouteCandidate first = firstLayer.get(firstIndex);
@@ -871,10 +873,14 @@ final class HydrologyRouteGeometry {
first,
second,
targetStepLength,
transitionCosts[1][firstIndex][secondIndex]
transitionCosts[1][firstIndex][secondIndex],
workspace,
1,
firstIndex,
secondIndex
);
if (Double.isFinite(transition)) {
costs[1][firstIndex][secondIndex] = first.localScore()
previousCosts[firstIndex][secondIndex] = first.localScore()
+ second.localScore()
+ resolvedLocalPenalty(localPenalties[1][secondIndex])
+ transition;
@@ -885,13 +891,11 @@ final class HydrologyRouteGeometry {
List<RouteCandidate> beforeLayer = layers.get(layerIndex - 2);
List<RouteCandidate> previousLayer = layers.get(layerIndex - 1);
List<RouteCandidate> currentLayer = layers.get(layerIndex);
costs[layerIndex] = new double[previousLayer.size()][currentLayer.size()];
predecessors[layerIndex] = new int[previousLayer.size()][currentLayer.size()];
if (turnCosts[layerIndex] == null) {
turnCosts[layerIndex] = new double[previousLayer.size()][currentLayer.size()][];
}
for (int previousIndex = 0; previousIndex < previousLayer.size(); previousIndex++) {
Arrays.fill(costs[layerIndex][previousIndex], Double.POSITIVE_INFINITY);
Arrays.fill(currentCosts[previousIndex], Double.POSITIVE_INFINITY);
Arrays.fill(predecessors[layerIndex][previousIndex], -1);
RouteCandidate previous = previousLayer.get(previousIndex);
for (int currentIndex = 0; currentIndex < currentLayer.size(); currentIndex++) {
@@ -900,14 +904,18 @@ final class HydrologyRouteGeometry {
previous,
current,
targetStepLength,
transitionCosts[layerIndex][previousIndex][currentIndex]
transitionCosts[layerIndex][previousIndex][currentIndex],
workspace,
layerIndex,
previousIndex,
currentIndex
);
if (!Double.isFinite(transition)) {
continue;
}
double localPenalty = resolvedLocalPenalty(localPenalties[layerIndex][currentIndex]);
for (int beforeIndex = 0; beforeIndex < beforeLayer.size(); beforeIndex++) {
double previousCost = costs[layerIndex - 1][beforeIndex][previousIndex];
double previousCost = previousCosts[beforeIndex][previousIndex];
if (!Double.isFinite(previousCost)) {
continue;
}
@@ -937,24 +945,27 @@ final class HydrologyRouteGeometry {
+ localPenalty
+ transition
+ turnPenalty;
if (cost < costs[layerIndex][previousIndex][currentIndex]) {
costs[layerIndex][previousIndex][currentIndex] = cost;
if (cost < currentCosts[previousIndex][currentIndex]) {
currentCosts[previousIndex][currentIndex] = cost;
predecessors[layerIndex][previousIndex][currentIndex] = beforeIndex;
}
}
}
}
double[][] completedCosts = currentCosts;
currentCosts = previousCosts;
previousCosts = completedCosts;
}
int lastLayerIndex = layerCount - 1;
int selectedPreviousIndex = -1;
int selectedCurrentIndex = -1;
double selectedCost = Double.POSITIVE_INFINITY;
for (int previousIndex = 0; previousIndex < costs[lastLayerIndex].length; previousIndex++) {
for (int previousIndex = 0; previousIndex < layers.get(lastLayerIndex - 1).size(); previousIndex++) {
for (int currentIndex = 0;
currentIndex < costs[lastLayerIndex][previousIndex].length;
currentIndex < layers.get(lastLayerIndex).size();
currentIndex++) {
if (costs[lastLayerIndex][previousIndex][currentIndex] < selectedCost) {
selectedCost = costs[lastLayerIndex][previousIndex][currentIndex];
if (previousCosts[previousIndex][currentIndex] < selectedCost) {
selectedCost = previousCosts[previousIndex][currentIndex];
selectedPreviousIndex = previousIndex;
selectedCurrentIndex = currentIndex;
}
@@ -1048,11 +1059,24 @@ final class HydrologyRouteGeometry {
RouteCandidate previous,
RouteCandidate current,
double targetStepLength,
double exactCost
double exactCost,
CurvatureWorkspace workspace,
int layerIndex,
int previousIndex,
int currentIndex
) {
return Double.isNaN(exactCost)
? planner.routePaths.routeTransitionGeometryCost(previous, current, targetStepLength)
: exactCost;
if (!Double.isNaN(exactCost)) {
return exactCost;
}
long heights = ((long) previous.point().y() << 32) | (current.point().y() & 0xffffffffL);
if (!workspace.geometryKnown[layerIndex][previousIndex][currentIndex]
|| workspace.geometryHeights[layerIndex][previousIndex][currentIndex] != heights) {
workspace.geometryCosts[layerIndex][previousIndex][currentIndex] =
planner.routePaths.routeTransitionGeometryCost(previous, current, targetStepLength);
workspace.geometryHeights[layerIndex][previousIndex][currentIndex] = heights;
workspace.geometryKnown[layerIndex][previousIndex][currentIndex] = true;
}
return workspace.geometryCosts[layerIndex][previousIndex][currentIndex];
}
double curvatureCost(double turn, double maximumTurn, double turnCost) {
@@ -1139,6 +1163,36 @@ final class HydrologyRouteGeometry {
return penalty;
}
static final class CurvatureWorkspace {
private final double[][] firstCosts;
private final double[][] secondCosts;
private final int[][][] predecessors;
private final double[][][] geometryCosts;
private final long[][][] geometryHeights;
private final boolean[][][] geometryKnown;
CurvatureWorkspace(List<? extends List<?>> layers) {
int maximumCandidates = 0;
for (List<?> layer : layers) {
maximumCandidates = Math.max(maximumCandidates, layer.size());
}
firstCosts = new double[maximumCandidates][maximumCandidates];
secondCosts = new double[maximumCandidates][maximumCandidates];
predecessors = new int[layers.size()][][];
geometryCosts = new double[layers.size()][][];
geometryHeights = new long[layers.size()][][];
geometryKnown = new boolean[layers.size()][][];
for (int layer = 1; layer < layers.size(); layer++) {
int previousSize = layers.get(layer - 1).size();
int currentSize = layers.get(layer).size();
predecessors[layer] = new int[previousSize][currentSize];
geometryCosts[layer] = new double[previousSize][currentSize];
geometryHeights[layer] = new long[previousSize][currentSize];
geometryKnown[layer] = new boolean[previousSize][currentSize];
}
}
}
record CurvatureRouteSelection(RouteCandidate[] route, int[] indices) {
private static CurvatureRouteSelection empty() {
return new CurvatureRouteSelection(new RouteCandidate[0], new int[0]);
@@ -63,13 +63,13 @@ final class HydrologyRoutePath {
if (offsetX == 0 && offsetZ == 0) {
return false;
}
HydrologyTerrainSample before = planner.sampleLandBasisWithoutSlope(point.x() - offsetX, point.z() - offsetZ);
HydrologyTerrainSample after = planner.sampleLandBasisWithoutSlope(point.x() + offsetX, point.z() + offsetZ);
double before = planner.sampleLandHeight(point.x() - offsetX, point.z() - offsetZ);
double after = planner.sampleLandHeight(point.x() + offsetX, point.z() + offsetZ);
int threshold = planner.settings.hydraulics().waterfallMinimumDrop();
return before != null
&& after != null
&& before.naturalHeight() - point.y() >= threshold
&& after.naturalHeight() - point.y() >= threshold;
return !Double.isNaN(before)
&& !Double.isNaN(after)
&& (int) before - point.y() >= threshold
&& (int) after - point.y() >= threshold;
}
List<HydrologyPoint> smoothTerrainRoute(List<HydrologyPoint> route) {
@@ -220,12 +220,12 @@ final class HydrologyRoutePath {
boolean traversableTerrainTransition(HydrologyPoint start, HydrologyPoint end) {
List<HydrologyPoint> crossing = planner.segments.rasterLine(start, end);
HydrologyTerrainSample startTerrain = planner.sampleLandBasisWithoutSlope(start.x(), start.z());
HydrologyTerrainSample endTerrain = planner.sampleLandBasisWithoutSlope(end.x(), end.z());
if (startTerrain == null || endTerrain == null) {
double startHeight = planner.sampleLandHeight(start.x(), start.z());
double endHeight = planner.sampleLandHeight(end.x(), end.z());
if (Double.isNaN(startHeight) || Double.isNaN(endHeight)) {
return false;
}
int boundaryHeight = Math.min(startTerrain.naturalHeight(), endTerrain.naturalHeight());
int boundaryHeight = Math.min((int) startHeight, (int) endHeight);
int threshold = planner.settings.hydraulics().waterfallMinimumDrop();
for (int pointIndex = 1; pointIndex < crossing.size() - 1; pointIndex++) {
HydrologyPoint point = crossing.get(pointIndex);
@@ -244,17 +244,17 @@ final class HydrologyRoutePath {
if (crossing.size() < 3) {
return false;
}
HydrologyTerrainSample startTerrain = planner.sampleLandBasisWithoutSlope(start.x(), start.z());
HydrologyTerrainSample endTerrain = planner.sampleLandBasisWithoutSlope(end.x(), end.z());
if (startTerrain == null || endTerrain == null) {
double startHeight = planner.sampleLandHeight(start.x(), start.z());
double endHeight = planner.sampleLandHeight(end.x(), end.z());
if (Double.isNaN(startHeight) || Double.isNaN(endHeight)) {
return true;
}
int boundaryHeight = Math.min(startTerrain.naturalHeight(), endTerrain.naturalHeight());
int boundaryHeight = Math.min((int) startHeight, (int) endHeight);
int threshold = planner.settings.hydraulics().waterfallMinimumDrop();
for (int pointIndex = 1; pointIndex < crossing.size() - 1; pointIndex++) {
HydrologyPoint point = crossing.get(pointIndex);
HydrologyTerrainSample terrain = planner.sampleLandBasisWithoutSlope(point.x(), point.z());
if (terrain == null || boundaryHeight - terrain.naturalHeight() >= threshold) {
double height = planner.sampleLandHeight(point.x(), point.z());
if (Double.isNaN(height) || boundaryHeight - (int) height >= threshold) {
return true;
}
}
@@ -5,6 +5,10 @@ public interface HydrologyRoutingTerrainSampler {
NaturalClassification classifyNatural(int blockX, int blockZ);
default boolean supportsSharedGridSamples() {
return false;
}
enum NaturalClassification {
LAND,
OCEAN,
@@ -696,8 +696,15 @@ final class HydrologySourcePlanner {
}
int x = (int) neighborX;
int z = (int) neighborZ;
SourceRoutingContext context = sourceContext(x, z, routingContexts);
HydrologyGridNode neighbor = context.grid().nodeAtWorld(x, z);
HydrologyTileKey owner = HydrologyTileKey.fromBlock(x, z, planner.settings.routing().tileSize());
SourceRoutingContext context = routingContexts.get(owner);
HydrologyGridNode neighbor = context == null
? sampledSourceNode(x, z, routingContexts)
: context.grid().nodeAtWorld(x, z);
if (neighbor == null) {
context = sourceContext(x, z, routingContexts);
neighbor = context.grid().nodeAtWorld(x, z);
}
if (neighbor == null) {
throw new IllegalStateException("Source coordinate is absent from its owner routing lattice.");
}
@@ -711,11 +718,18 @@ final class HydrologySourcePlanner {
if (!rawSourceEligible(terrain, sourceSettings, surface)) {
continue;
}
if (!routeViable(neighbor, context, surface)) {
SourcePriority neighborPriority = sourcePriority(x, z, terrain, sourceSalt, surface);
if (compareSourcePriority(neighborPriority, candidatePriority) <= 0) {
continue;
}
SourcePriority neighborPriority = sourcePriority(x, z, terrain, sourceSalt, surface);
if (compareSourcePriority(neighborPriority, candidatePriority) > 0) {
if (context == null) {
context = sourceContext(x, z, routingContexts);
neighbor = context.grid().nodeAtWorld(x, z);
if (neighbor == null) {
throw new IllegalStateException("Source coordinate is absent from its owner routing lattice.");
}
}
if (routeViable(neighbor, context, surface)) {
contenders.add(new SourceContender(neighbor, neighborPriority));
}
}
@@ -729,6 +743,23 @@ final class HydrologySourcePlanner {
return ordered;
}
private HydrologyGridNode sampledSourceNode(int x, int z,
Map<HydrologyTileKey, SourceRoutingContext> routingContexts) {
if (!planner.routingSampler.supportsSharedGridSamples()) {
return null;
}
for (SourceRoutingContext context : routingContexts.values()) {
if (context.grid().spacing() != planner.settings.routing().sampleSpacing()) {
continue;
}
HydrologyGridNode node = context.grid().nodeAtWorld(x, z);
if (node != null) {
return node;
}
}
return null;
}
private SourceRoutingContext sourceContext(
int x,
int z,
@@ -705,11 +705,13 @@ final class HydrologySurfaceCoursePlanner {
int head,
int maximumBankRise
) {
HydrologyTerrainSample bank = surfaceBankTerrain(point, tangent, signedDistance);
if (bank == null) {
int bankX = (int) StrictMath.round(point.x() - tangent.z() * signedDistance);
int bankZ = (int) StrictMath.round(point.z() + tangent.x() * signedDistance);
double bankHeight = planner.sampleLandHeight(bankX, bankZ);
if (Double.isNaN(bankHeight)) {
return 1.0E12D;
}
int rise = bank.naturalHeight() - head;
int rise = (int) bankHeight - head;
int shortfall = HydrologyRouteGeometry.SURFACE_MINIMUM_NATURAL_BANK_RISE - rise;
if (shortfall > 0) {
return (double) shortfall * shortfall * 4096D;
@@ -922,17 +924,6 @@ final class HydrologySurfaceCoursePlanner {
return pointX * pointX + pointZ * pointZ;
}
HydrologyTerrainSample surfaceBankTerrain(
HydrologyPoint point,
RouteDirection tangent,
int signedDistance
) {
HydrologyPoint bankPoint = surfaceBankPoint(point, tangent, signedDistance);
int bankX = bankPoint.x();
int bankZ = bankPoint.z();
return planner.sampleLandBasisWithoutSlope(bankX, bankZ);
}
HydrologyPoint surfaceBankPoint(HydrologyPoint point, RouteDirection tangent, int signedDistance) {
return new HydrologyPoint(
(int) StrictMath.round(point.x() - tangent.z() * signedDistance),
@@ -12,6 +12,7 @@ import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -36,7 +37,9 @@ public final class HydrologyTileCache implements AutoCloseable {
private static final int CHUNK_SIZE = 16;
private static final int CHUNK_COLUMN_COUNT = CHUNK_SIZE * CHUNK_SIZE;
private static final Cache<SharedTileKey, HydrologyTile> SHARED_TILES = Caffeine.newBuilder()
.maximumSize(MAXIMUM_SHARED_ENTRIES)
.maximumWeight(HydrologyCacheBudget.sharedStudioBytes())
.weigher((SharedTileKey key, HydrologyTile tile) -> HydrologyCacheWeights.bounded(
HydrologyCacheWeights.tile(tile), HydrologyCacheBudget.sharedStudioBytes(), MAXIMUM_SHARED_ENTRIES))
.expireAfterAccess(10, TimeUnit.MINUTES)
.build();
@@ -51,6 +54,7 @@ public final class HydrologyTileCache implements AutoCloseable {
private final Executor prefetchExecutor;
private final ConcurrentLinkedQueue<HydrologyTileKey> prefetchQueue;
private final Set<HydrologyTileKey> queuedPrefetches;
private final LinkedHashSet<ChunkCoordinate> queriedChunks;
private final Set<CacheLoadKey<HydrologyTileKey>> demandedPlans;
private final AtomicBoolean prefetchActive;
private final ConcurrentHashMap<CacheLoadKey<HydrologyTileKey>, CompletableFuture<HydrologyTile>> planning;
@@ -117,16 +121,24 @@ public final class HydrologyTileCache implements AutoCloseable {
this.maximumEntries = maximumEntries;
this.prefetchQueue = new ConcurrentLinkedQueue<>();
this.queuedPrefetches = ConcurrentHashMap.newKeySet();
this.queriedChunks = new LinkedHashSet<>();
this.demandedPlans = new HashSet<>();
this.prefetchActive = new AtomicBoolean();
HydrologyCacheBudget budget = HydrologyCacheBudget.runtime();
this.tiles = Caffeine.newBuilder()
.maximumSize(maximumEntries)
.maximumWeight(budget.tileBytes())
.weigher((HydrologyTileKey key, HydrologyTile tile) -> HydrologyCacheWeights.bounded(
HydrologyCacheWeights.tile(tile), budget.tileBytes(), maximumEntries))
.build();
this.composedChunks = Caffeine.newBuilder()
.maximumSize(MAXIMUM_COMPOSED_CHUNKS)
.maximumWeight(budget.columnBytes())
.weigher((Long key, ChunkColumns columns) -> HydrologyCacheWeights.bounded(
columns.estimatedBytes(), budget.columnBytes(), MAXIMUM_COMPOSED_CHUNKS))
.build();
this.diagnostics = Caffeine.newBuilder()
.maximumSize(maximumEntries)
.maximumWeight(budget.diagnosticBytes())
.weigher((HydrologyTileKey key, List<HydrologyDiagnosticCandidate> candidates) -> HydrologyCacheWeights.bounded(
HydrologyCacheWeights.diagnostics(candidates), budget.diagnosticBytes(), maximumEntries))
.build();
this.cacheEpoch = new AtomicLong();
this.closed = new AtomicBoolean();
@@ -198,11 +210,8 @@ public final class HydrologyTileCache implements AutoCloseable {
}
try {
present = diagnostics.getIfPresent(key);
if (present == null) {
prepareTerrain();
}
List<HydrologyDiagnosticCandidate> result = present == null
? List.copyOf(planner.diagnosticCandidates(planInline(key, loadKey.epoch()))) : present;
? planDiagnostics(key, loadKey.epoch()) : present;
synchronized (publicationLock) {
if (!closed.get() && cacheEpoch.get() == loadKey.epoch()) {
diagnostics.put(key, result);
@@ -218,6 +227,14 @@ public final class HydrologyTileCache implements AutoCloseable {
}
}
private List<HydrologyDiagnosticCandidate> planDiagnostics(HydrologyTileKey key, long epoch) {
HydrologyTile tile = planInline(key, epoch);
try (HydrologyPlanningAdmission.Permit ignored = HydrologyPlanningAdmission.acquireRoot(closed::get)) {
prepareTerrain();
return List.copyOf(planner.diagnosticCandidates(tile));
}
}
private CompletableFuture<List<HydrologyDiagnosticCandidate>> diagnoseAsync(CacheLoadKey<HydrologyTileKey> loadKey) {
List<HydrologyDiagnosticCandidate> present = diagnostics.getIfPresent(loadKey.key());
if (present != null) {
@@ -329,6 +346,13 @@ public final class HydrologyTileCache implements AutoCloseable {
SHARED_TILES.invalidate(sharedKey);
}
StudioHydrologyTileStore store = persistentStore;
try (HydrologyPlanningAdmission.Permit ignored = HydrologyPlanningAdmission.acquireRoot(closed::get)) {
return planAdmitted(key, planningEpoch, sharedKey, store);
}
}
private HydrologyTile planAdmitted(HydrologyTileKey key, long planningEpoch,
SharedTileKey sharedKey, StudioHydrologyTileStore store) {
if (store != null && persistentStudioKey(key)) {
HydrologyTile persisted = store.load(key).orElse(null);
if (persisted != null) {
@@ -360,7 +384,7 @@ public final class HydrologyTileCache implements AutoCloseable {
}
return planned;
} catch (RuntimeException failure) {
if (interrupted(failure)) {
if (failure instanceof CancellationException || interrupted(failure)) {
throw failure;
}
IrisLogging.error("Hydrology tile " + key.tileX() + "," + key.tileZ()
@@ -449,13 +473,24 @@ public final class HydrologyTileCache implements AutoCloseable {
continue;
}
missing = true;
if (prefetchExecutor != null) {
prefetch(key);
}
break;
}
if (missing) {
requestChunk(chunkX, chunkZ);
}
return missing;
}
private void requestChunk(int chunkX, int chunkZ) {
synchronized (prefetchQueue) {
if (prefetchExecutor == null || closed.get() || queriedChunks.size() >= maximumEntries) {
return;
}
queriedChunks.add(new ChunkCoordinate(chunkX, chunkZ));
}
pumpPrefetch();
}
/** Whether the calling thread must never wait for a cold hydrology plan. */
private boolean waitsForbidden() {
return waitingForbidden != null && waitingForbidden.getAsBoolean();
@@ -473,6 +508,7 @@ public final class HydrologyTileCache implements AutoCloseable {
}
public void clear() {
ArrayList<CompletableFuture<ChunkColumns>> cancelledQueries = new ArrayList<>();
synchronized (prefetchQueue) {
synchronized (publicationLock) {
cacheEpoch.incrementAndGet();
@@ -480,13 +516,22 @@ public final class HydrologyTileCache implements AutoCloseable {
tiles.invalidateAll();
composedChunks.invalidateAll();
diagnostics.invalidateAll();
composing.forEach((key, load) -> {
if (load.owner() == null && composing.remove(key, load)) {
cancelledQueries.add(load.future());
}
});
}
prefetchQueue.clear();
queuedPrefetches.clear();
queriedChunks.clear();
demandedPlans.clear();
demandBatches = 0;
}
localChunkColumns.remove();
for (CompletableFuture<ChunkColumns> future : cancelledQueries) {
future.completeExceptionally(new CancellationException("Hydrology query was invalidated."));
}
}
public int size() {
@@ -546,7 +591,11 @@ public final class HydrologyTileCache implements AutoCloseable {
collectActiveLoad(active, load);
}
for (PendingLoad<ChunkColumns> load : composing.values()) {
collectActiveLoad(active, load);
if (load.owner() == null) {
queued.add(load.future());
} else {
collectActiveLoad(active, load);
}
}
for (PendingLoad<List<HydrologyDiagnosticCandidate>> load : loadingDiagnostics.values()) {
collectActiveLoad(active, load);
@@ -620,13 +669,29 @@ public final class HydrologyTileCache implements AutoCloseable {
return local.columns();
}
long packedChunk = CacheKey.mix(RiverFootprint.pack(chunkX, chunkZ));
if (composedChunks.getIfPresent(packedChunk) == null
&& waitsForbidden()
&& requestUnplanned(chunkX, chunkZ)) {
// A thread that must never wait (the server thread answering a query) gets a riverless
// answer while the tiles it would have waited for are planned on the prefetch executor.
// The empty result is never cached, so the next query composes the real columns.
return ChunkColumns.empty(chunkX, chunkZ);
if (waitsForbidden()) {
ChunkColumns present = composedChunks.getIfPresent(packedChunk);
if (present != null) {
return present;
}
ArrayList<HydrologyTileKey> keys = relevantKeys(chunkX, chunkZ);
ArrayList<HydrologyTile> snapshots = new ArrayList<>(keys.size());
for (HydrologyTileKey key : keys) {
HydrologyTile tile = tiles.getIfPresent(key);
if (tile == null) {
requestChunk(chunkX, chunkZ);
return ChunkColumns.empty(chunkX, chunkZ);
}
snapshots.add(tile);
}
ChunkColumns columns = composeChunkColumns(chunkX, chunkZ, keys, snapshots);
synchronized (publicationLock) {
if (!closed.get() && cacheEpoch.get() == epoch) {
composedChunks.put(packedChunk, columns);
}
}
localChunkColumns.set(new LocalChunkColumns(epoch, columns));
return columns;
}
ChunkColumns columns = loadChunkColumns(new CacheLoadKey<>(epoch, packedChunk), chunkX, chunkZ);
localChunkColumns.set(new LocalChunkColumns(epoch, columns));
@@ -642,16 +707,29 @@ public final class HydrologyTileCache implements AutoCloseable {
}
PendingLoad<ChunkColumns> owned = new PendingLoad<>(Thread.currentThread(), new CompletableFuture<>());
PendingLoad<ChunkColumns> existing;
boolean claimedQuery = false;
synchronized (publicationLock) {
requireOpen();
existing = composing.putIfAbsent(loadKey, owned);
if (existing != null && existing.owner() == null && !existing.future().isDone()) {
PendingLoad<ChunkColumns> claimed = new PendingLoad<>(Thread.currentThread(), existing.future());
if (composing.replace(loadKey, existing, claimed)) {
owned = claimed;
existing = null;
claimedQuery = true;
}
}
}
if (existing != null) {
return awaitLoad(existing);
}
try {
present = composedChunks.getIfPresent(packedChunk);
ChunkColumns columns = present == null ? composeChunkColumns(chunkX, chunkZ) : present;
ChunkColumns columns = present;
if (columns == null) {
columns = claimedQuery ? composeChunkColumnsInline(chunkX, chunkZ, epoch)
: composeChunkColumns(chunkX, chunkZ);
}
synchronized (publicationLock) {
if (!closed.get() && cacheEpoch.get() == epoch) {
composedChunks.put(packedChunk, columns);
@@ -689,14 +767,28 @@ public final class HydrologyTileCache implements AutoCloseable {
}
private ChunkColumns composeChunkColumns(int chunkX, int chunkZ) {
ArrayList<HydrologyTileKey> relevantKeys = relevantKeys(chunkX, chunkZ);
return composeChunkColumns(chunkX, chunkZ, relevantKeys, tiles(relevantKeys));
}
private ChunkColumns composeChunkColumnsInline(int chunkX, int chunkZ, long epoch) {
ArrayList<HydrologyTileKey> keys = relevantKeys(chunkX, chunkZ);
ArrayList<HydrologyTile> snapshots = new ArrayList<>(keys.size());
for (HydrologyTileKey key : keys) {
snapshots.add(planInline(key, epoch));
}
return composeChunkColumns(chunkX, chunkZ, keys, snapshots);
}
private ChunkColumns composeChunkColumns(int chunkX, int chunkZ,
List<HydrologyTileKey> relevantKeys,
List<HydrologyTile> relevantTiles) {
int minimumBlockX = Math.multiplyExact(chunkX, CHUNK_SIZE);
int minimumBlockZ = Math.multiplyExact(chunkZ, CHUNK_SIZE);
ArrayList<HydrologyTileKey> relevantKeys = relevantKeys(chunkX, chunkZ);
int minimumTileX = relevantKeys.getFirst().tileX();
int maximumTileX = relevantKeys.getLast().tileX();
int minimumTileZ = relevantKeys.getFirst().tileZ();
int maximumTileZ = relevantKeys.getLast().tileZ();
List<HydrologyTile> relevantTiles = tiles(relevantKeys);
prefetchNeighbours(minimumTileX, maximumTileX, minimumTileZ, maximumTileZ);
HydrologyColumnSample[] columns = new HydrologyColumnSample[CHUNK_COLUMN_COUNT];
for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) {
@@ -823,12 +915,22 @@ public final class HydrologyTileCache implements AutoCloseable {
}
private int speculativeEntryCount() {
long entries = tiles.estimatedSize();
long budget = HydrologyCacheBudget.runtime().tileBytes();
long weight = tiles.policy().eviction().orElseThrow().weightedSize().orElse(0L);
long available = speculativeCapacity(entries, budget, weight);
return Math.toIntExact(Math.min(
maximumEntries,
tiles.estimatedSize() + planning.size() + queuedPrefetches.size()
Math.max(entries, maximumEntries - available) + planning.size() + queuedPrefetches.size()
));
}
private long speculativeCapacity(long entries, long budget, long weight) {
long expectedWeight = Math.max(Math.ceilDiv(budget, maximumEntries),
Math.ceilDiv(weight, Math.max(1L, entries)));
return Math.max(0L, budget - weight) / expectedWeight;
}
private void pumpPrefetch() {
synchronized (prefetchQueue) {
if (prefetchExecutor == null || closed.get() || demandBatches != 0 || !demandedPlans.isEmpty()
@@ -840,23 +942,115 @@ public final class HydrologyTileCache implements AutoCloseable {
}
private void scheduleNextPrefetch() {
CacheLoadKey<HydrologyTileKey> loadKey;
CacheLoadKey<HydrologyTileKey> loadKey = null;
ChunkCoordinate chunk;
long epoch;
synchronized (prefetchQueue) {
if (closed.get() || demandBatches != 0 || !demandedPlans.isEmpty()) {
prefetchActive.set(false);
return;
}
HydrologyTileKey key = pollPrefetch();
if (key == null) {
prefetchActive.set(false);
return;
epoch = cacheEpoch.get();
chunk = pollQuery();
if (chunk != null) {
demandBatches++;
} else {
HydrologyTileKey key = pollPrefetch();
if (key == null) {
prefetchActive.set(false);
return;
}
loadKey = new CacheLoadKey<>(epoch, key);
}
loadKey = new CacheLoadKey<>(cacheEpoch.get(), key);
}
planAsync(loadKey).whenComplete((tile, failure) -> scheduleNextPrefetch());
if (chunk != null) {
composeAsync(chunk, epoch).whenComplete((columns, failure) -> {
synchronized (prefetchQueue) {
if (epoch == cacheEpoch.get()) {
queriedChunks.remove(chunk);
}
}
finishDemandBatch(epoch);
scheduleNextPrefetch();
});
} else {
planAsync(loadKey).whenComplete((tile, failure) -> scheduleNextPrefetch());
}
}
private ChunkCoordinate pollQuery() {
while (!queriedChunks.isEmpty()) {
ChunkCoordinate chunk = queriedChunks.getFirst();
if (composedChunks.getIfPresent(chunk.packed()) == null) {
return chunk;
}
queriedChunks.removeFirst();
}
return null;
}
private CompletableFuture<ChunkColumns> composeAsync(ChunkCoordinate chunk, long epoch) {
CacheLoadKey<Long> loadKey = new CacheLoadKey<>(epoch, chunk.packed());
PendingLoad<ChunkColumns> queued = new PendingLoad<>(null, new CompletableFuture<>());
synchronized (publicationLock) {
if (closed.get() || epoch != cacheEpoch.get()) {
return CompletableFuture.failedFuture(new CancellationException("Hydrology query was invalidated."));
}
PendingLoad<ChunkColumns> existing = composing.putIfAbsent(loadKey, queued);
if (existing != null) {
return existing.future();
}
}
try {
prefetchExecutor.execute(() -> runComposition(loadKey, chunk, queued));
} catch (RuntimeException | Error rejected) {
if (composing.remove(loadKey, queued)) {
queued.future().completeExceptionally(rejected);
}
}
return queued.future();
}
private void runComposition(CacheLoadKey<Long> loadKey, ChunkCoordinate chunk,
PendingLoad<ChunkColumns> queued) {
PendingLoad<ChunkColumns> active = new PendingLoad<>(Thread.currentThread(), queued.future());
boolean started;
synchronized (publicationLock) {
started = !closed.get() && loadKey.epoch() == cacheEpoch.get() && !queued.future().isDone()
&& composing.replace(loadKey, queued, active);
}
if (!started) {
if (composing.remove(loadKey, queued)) {
queued.future().completeExceptionally(new CancellationException("Hydrology query was invalidated."));
}
return;
}
try {
ChunkColumns columns = composeChunkColumnsInline(chunk.x(), chunk.z(), loadKey.epoch());
synchronized (publicationLock) {
if (!closed.get() && cacheEpoch.get() == loadKey.epoch()) {
composedChunks.put(loadKey.key(), columns);
}
}
active.future().complete(columns);
} catch (Throwable failure) {
active.future().completeExceptionally(failure);
if (!(failure instanceof CancellationException)) {
IrisLogging.reportError(failure);
}
} finally {
composing.remove(loadKey, active);
}
}
private HydrologyTileKey pollPrefetch() {
tiles.cleanUp();
long weight = tiles.policy().eviction().orElseThrow().weightedSize().orElse(0L);
if (speculativeCapacity(tiles.estimatedSize(), HydrologyCacheBudget.runtime().tileBytes(), weight) == 0L) {
prefetchQueue.clear();
queuedPrefetches.clear();
return null;
}
HydrologyTileKey key;
while ((key = prefetchQueue.poll()) != null) {
if (!queuedPrefetches.remove(key)) {
@@ -905,7 +1099,7 @@ public final class HydrologyTileCache implements AutoCloseable {
}
return List.of(loaded);
}
int batch = Math.max(1, maximumEntries / 2);
int batch = Math.max(1, Math.min(maximumEntries / 2, HydrologyPlanningAdmission.maximumRoots()));
ArrayList<CompletableFuture<HydrologyTile>> futures = new ArrayList<>(Math.min(batch, loaded.length));
for (int start = 0; start < loaded.length; start += batch) {
int end = Math.min(loaded.length, start + batch);
@@ -1129,6 +1323,16 @@ public final class HydrologyTileCache implements AutoCloseable {
}
private record ChunkColumns(int chunkX, int chunkZ, HydrologyColumnSample[] columns) {
private long estimatedBytes() {
long bytes = 64L + 8L * columns.length;
for (HydrologyColumnSample column : columns) {
if (column != null) {
bytes += HydrologyCacheWeights.column(column);
}
}
return bytes;
}
/** A chunk with no hydrology at all, the answer for a caller that may not wait for its plan. */
private static ChunkColumns empty(int chunkX, int chunkZ) {
return new ChunkColumns(chunkX, chunkZ, new HydrologyColumnSample[CHUNK_COLUMN_COUNT]);
@@ -1144,6 +1348,12 @@ public final class HydrologyTileCache implements AutoCloseable {
private record LocalChunkColumns(long epoch, ChunkColumns columns) {
}
private record ChunkCoordinate(int x, int z) {
private long packed() {
return CacheKey.mix(RiverFootprint.pack(x, z));
}
}
private record CacheLoadKey<K>(long epoch, K key) {
}
@@ -1,15 +1,16 @@
package art.arcane.iris.generation.hydrology.cave;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.HashCommon;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import java.util.AbstractCollection;
import java.util.AbstractList;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
@@ -18,6 +19,8 @@ import java.util.OptionalLong;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiPredicate;
import java.util.function.IntConsumer;
import java.util.function.IntPredicate;
public record HydrologyCavePlan(
HydrologyCaveSource source,
@@ -56,22 +59,10 @@ public record HydrologyCavePlan(
actions = Map.of();
baselinePreconditions = Map.of();
} else {
LinkedHashMap<CavePosition, Byte> packed = new LinkedHashMap<>(requestedPreconditions.size());
for (Map.Entry<CavePosition, HydrologyCaveAction> entry : requestedActions.entrySet()) {
CaveVoxelPrecondition precondition = requestedPreconditions.get(entry.getKey());
packed.put(entry.getKey(), encode(entry.getValue(), precondition));
}
for (Map.Entry<CavePosition, CaveVoxelPrecondition> entry : requestedPreconditions.entrySet()) {
packed.putIfAbsent(entry.getKey(), encode(null, entry.getValue()));
}
Map<CavePosition, Byte> immutablePacked = Collections.unmodifiableMap(packed);
PositionSpatialIndex spatialIndex = new PositionSpatialIndex(immutablePacked);
actions = new ActionMap(immutablePacked, requestedActions.size(), spatialIndex);
baselinePreconditions = new PreconditionMap(
immutablePacked,
List.copyOf(requestedPreconditions.keySet()),
spatialIndex
);
PackedPositions packed = new PackedPositions(requestedActions, requestedPreconditions);
PositionSpatialIndex spatialIndex = new PositionSpatialIndex(packed);
actions = new ActionMap(packed, requestedActions.size(), spatialIndex);
baselinePreconditions = new PreconditionMap(packed, packed.preconditionOrder(requestedPreconditions), spatialIndex);
}
}
@@ -79,6 +70,27 @@ public record HydrologyCavePlan(
return rejection == HydrologyCaveRejection.NONE;
}
public long estimatedRetainedBytes() {
if (!(baselinePreconditions instanceof PreconditionMap preconditions)) {
return 192L;
}
PackedPositions packed = preconditions.packed;
PositionSpatialIndex spatialIndex = preconditions.spatialIndex;
long spatialCapacity = (long) HashCommon.arraySize(spatialIndex.positionsByChunk.size(), 0.75F) + 1L;
long bytes = 512L + arrayBytes((long) packed.x.length * Integer.BYTES) * 3L
+ arrayBytes(packed.flags.length) + arrayBytes((long) packed.slots.length * Integer.BYTES)
+ arrayBytes((long) preconditions.order.rows.length * Integer.BYTES)
+ arrayBytes(spatialCapacity * Long.BYTES) * 2L;
for (int[] rows : spatialIndex.positionsByChunk.values()) {
bytes += arrayBytes((long) rows.length * Integer.BYTES);
}
return bytes;
}
private static long arrayBytes(long elementsBytes) {
return (elementsBytes + 23L) & ~7L;
}
public void forEachAction(BiConsumer<CavePosition, HydrologyCaveAction> consumer) {
actions.forEach(consumer);
}
@@ -183,12 +195,129 @@ public record HydrologyCavePlan(
return preconditions;
}
private static final class PackedPositions {
private final int[] x;
private final int[] y;
private final int[] z;
private final byte[] flags;
private final int[] slots;
private final int mask;
private int count;
private PackedPositions(Map<CavePosition, HydrologyCaveAction> actions,
Map<CavePosition, CaveVoxelPrecondition> preconditions) {
int size = preconditions.size();
x = new int[size];
y = new int[size];
z = new int[size];
flags = new byte[size];
long required = Math.max(2L, ((long) size * 4L + 2L) / 3L);
if (required > 1 << 30) {
throw new IllegalArgumentException("Too many cave positions");
}
int capacity = Integer.highestOneBit((int) required - 1) << 1;
slots = new int[capacity];
mask = capacity - 1;
for (Map.Entry<CavePosition, HydrologyCaveAction> entry : actions.entrySet()) {
append(entry.getKey(), encode(entry.getValue(), preconditions.get(entry.getKey())));
}
for (Map.Entry<CavePosition, CaveVoxelPrecondition> entry : preconditions.entrySet()) {
if (row(entry.getKey()) < 0) {
append(entry.getKey(), encode(null, entry.getValue()));
}
}
}
private void append(CavePosition position, byte value) {
int slot = slot(position);
x[count] = position.x();
y[count] = position.y();
z[count] = position.z();
flags[count] = value;
slots[slot] = ++count;
}
private int slot(CavePosition position) {
int hash = 0x811C9DC5;
hash = (hash ^ position.x()) * 0x01000193;
hash = (hash ^ position.y()) * 0x01000193;
hash = (hash ^ position.z()) * 0x01000193;
hash ^= hash >>> 16;
hash *= 0x7FEB352D;
hash ^= hash >>> 15;
hash *= 0x846CA68B;
hash ^= hash >>> 16;
int slot = hash & mask;
while (slots[slot] != 0) {
int row = slots[slot] - 1;
if (x[row] == position.x() && y[row] == position.y() && z[row] == position.z()) {
return slot;
}
slot = slot + 1 & mask;
}
return slot;
}
private int row(CavePosition position) {
return slots[slot(position)] - 1;
}
private CavePosition position(int row) {
return new CavePosition(x[row], y[row], z[row]);
}
private PositionOrder preconditionOrder(Map<CavePosition, CaveVoxelPrecondition> preconditions) {
int[] order = new int[count];
int index = 0;
for (CavePosition position : preconditions.keySet()) {
order[index++] = row(position);
}
return new PositionOrder(this, order);
}
public Byte get(Object key) {
if (!(key instanceof CavePosition position)) {
return null;
}
int row = row(position);
return row < 0 ? null : flags[row];
}
public boolean containsKey(Object key) {
return key instanceof CavePosition position && row(position) >= 0;
}
public int size() {
return count;
}
}
private static final class PositionOrder extends AbstractList<CavePosition> {
private final PackedPositions packed;
private final int[] rows;
private PositionOrder(PackedPositions packed, int[] rows) {
this.packed = packed;
this.rows = rows;
}
@Override
public CavePosition get(int index) {
return packed.position(rows[index]);
}
@Override
public int size() {
return rows.length;
}
}
private static final class ActionMap extends AbstractMap<CavePosition, HydrologyCaveAction> {
private final Map<CavePosition, Byte> packed;
private final PackedPositions packed;
private final int size;
private final PositionSpatialIndex spatialIndex;
private ActionMap(Map<CavePosition, Byte> packed, int size, PositionSpatialIndex spatialIndex) {
private ActionMap(PackedPositions packed, int size, PositionSpatialIndex spatialIndex) {
this.packed = packed;
this.size = size;
this.spatialIndex = spatialIndex;
@@ -229,16 +358,16 @@ public record HydrologyCavePlan(
@Override
public void forEach(BiConsumer<? super CavePosition, ? super HydrologyCaveAction> consumer) {
Objects.requireNonNull(consumer);
for (Map.Entry<CavePosition, Byte> entry : packed.entrySet()) {
if (hasAction(entry.getValue())) {
consumer.accept(entry.getKey(), decodeAction(entry.getValue()));
for (int row = 0; row < packed.size(); row++) {
if (hasAction(packed.flags[row])) {
consumer.accept(packed.position(row), decodeAction(packed.flags[row]));
}
}
}
private boolean intersects(int minimumX, int minimumZ, int maximumX, int maximumZ) {
return spatialIndex.anyIn(minimumX, minimumZ, maximumX, maximumZ,
(CavePosition position) -> hasAction(packed.get(position)));
(int row) -> hasAction(packed.flags[row]));
}
private void forEachIn(
@@ -248,23 +377,23 @@ public record HydrologyCavePlan(
int maximumZ,
BiConsumer<CavePosition, HydrologyCaveAction> consumer
) {
spatialIndex.forEachIn(minimumX, minimumZ, maximumX, maximumZ, (CavePosition position) -> {
byte value = packed.get(position);
spatialIndex.forEachIn(minimumX, minimumZ, maximumX, maximumZ, (int row) -> {
byte value = packed.flags[row];
if (hasAction(value)) {
consumer.accept(position, decodeAction(value));
consumer.accept(packed.position(row), decodeAction(value));
}
});
}
}
private static final class PreconditionMap extends AbstractMap<CavePosition, CaveVoxelPrecondition> {
private final Map<CavePosition, Byte> packed;
private final List<CavePosition> order;
private final PackedPositions packed;
private final PositionOrder order;
private final PositionSpatialIndex spatialIndex;
private PreconditionMap(
Map<CavePosition, Byte> packed,
List<CavePosition> order,
PackedPositions packed,
PositionOrder order,
PositionSpatialIndex spatialIndex
) {
this.packed = packed;
@@ -306,8 +435,8 @@ public record HydrologyCavePlan(
@Override
public void forEach(BiConsumer<? super CavePosition, ? super CaveVoxelPrecondition> consumer) {
Objects.requireNonNull(consumer);
for (CavePosition position : order) {
consumer.accept(position, decodePrecondition(packed.get(position)));
for (int row : order.rows) {
consumer.accept(packed.position(row), decodePrecondition(packed.flags[row]));
}
}
@@ -319,26 +448,32 @@ public record HydrologyCavePlan(
BiPredicate<CavePosition, CaveVoxelPrecondition> predicate
) {
return spatialIndex.allIn(minimumX, minimumZ, maximumX, maximumZ,
(CavePosition position) -> predicate.test(position, decodePrecondition(packed.get(position))));
(int row) -> predicate.test(packed.position(row), decodePrecondition(packed.flags[row])));
}
}
private static final class PositionSpatialIndex {
private static final int CHUNK_SHIFT = 4;
private final Map<Long, List<CavePosition>> positionsByChunk;
private final PackedPositions packed;
private final Long2ObjectOpenHashMap<int[]> positionsByChunk;
private PositionSpatialIndex(Map<CavePosition, Byte> packed) {
HashMap<Long, ArrayList<CavePosition>> mutable = new HashMap<>();
for (CavePosition position : packed.keySet()) {
long chunkKey = packChunk(position.x() >> CHUNK_SHIFT, position.z() >> CHUNK_SHIFT);
mutable.computeIfAbsent(chunkKey, ignored -> new ArrayList<>()).add(position);
private PositionSpatialIndex(PackedPositions packed) {
this.packed = packed;
Long2ObjectOpenHashMap<IntArrayList> mutable = new Long2ObjectOpenHashMap<>();
for (int row = 0; row < packed.size(); row++) {
long chunkKey = packChunk(packed.x[row] >> CHUNK_SHIFT, packed.z[row] >> CHUNK_SHIFT);
IntArrayList rows = mutable.get(chunkKey);
if (rows == null) {
rows = new IntArrayList();
mutable.put(chunkKey, rows);
}
rows.add(row);
}
HashMap<Long, List<CavePosition>> immutable = HashMap.newHashMap(mutable.size());
for (Map.Entry<Long, ArrayList<CavePosition>> entry : mutable.entrySet()) {
immutable.put(entry.getKey(), List.copyOf(entry.getValue()));
positionsByChunk = new Long2ObjectOpenHashMap<>(mutable.size());
for (Long2ObjectMap.Entry<IntArrayList> entry : mutable.long2ObjectEntrySet()) {
positionsByChunk.put(entry.getLongKey(), entry.getValue().toIntArray());
}
positionsByChunk = Map.copyOf(immutable);
}
private boolean anyIn(
@@ -346,7 +481,7 @@ public record HydrologyCavePlan(
int minimumZ,
int maximumX,
int maximumZ,
java.util.function.Predicate<CavePosition> predicate
IntPredicate predicate
) {
int minimumChunkX = minimumX >> CHUNK_SHIFT;
int maximumChunkX = maximumX - 1 >> CHUNK_SHIFT;
@@ -354,13 +489,13 @@ public record HydrologyCavePlan(
int maximumChunkZ = maximumZ - 1 >> CHUNK_SHIFT;
for (int chunkZ = minimumChunkZ; chunkZ <= maximumChunkZ; chunkZ++) {
for (int chunkX = minimumChunkX; chunkX <= maximumChunkX; chunkX++) {
List<CavePosition> positions = positionsByChunk.get(packChunk(chunkX, chunkZ));
int[] positions = positionsByChunk.get(packChunk(chunkX, chunkZ));
if (positions == null) {
continue;
}
for (CavePosition position : positions) {
if (inside(position, minimumX, minimumZ, maximumX, maximumZ)
&& predicate.test(position)) {
for (int row : positions) {
if (inside(row, minimumX, minimumZ, maximumX, maximumZ)
&& predicate.test(row)) {
return true;
}
}
@@ -374,7 +509,7 @@ public record HydrologyCavePlan(
int minimumZ,
int maximumX,
int maximumZ,
java.util.function.Predicate<CavePosition> predicate
IntPredicate predicate
) {
int minimumChunkX = minimumX >> CHUNK_SHIFT;
int maximumChunkX = maximumX - 1 >> CHUNK_SHIFT;
@@ -382,13 +517,13 @@ public record HydrologyCavePlan(
int maximumChunkZ = maximumZ - 1 >> CHUNK_SHIFT;
for (int chunkZ = minimumChunkZ; chunkZ <= maximumChunkZ; chunkZ++) {
for (int chunkX = minimumChunkX; chunkX <= maximumChunkX; chunkX++) {
List<CavePosition> positions = positionsByChunk.get(packChunk(chunkX, chunkZ));
int[] positions = positionsByChunk.get(packChunk(chunkX, chunkZ));
if (positions == null) {
continue;
}
for (CavePosition position : positions) {
if (inside(position, minimumX, minimumZ, maximumX, maximumZ)
&& !predicate.test(position)) {
for (int row : positions) {
if (inside(row, minimumX, minimumZ, maximumX, maximumZ)
&& !predicate.test(row)) {
return false;
}
}
@@ -402,7 +537,7 @@ public record HydrologyCavePlan(
int minimumZ,
int maximumX,
int maximumZ,
java.util.function.Consumer<CavePosition> consumer
IntConsumer consumer
) {
int minimumChunkX = minimumX >> CHUNK_SHIFT;
int maximumChunkX = maximumX - 1 >> CHUNK_SHIFT;
@@ -410,28 +545,28 @@ public record HydrologyCavePlan(
int maximumChunkZ = maximumZ - 1 >> CHUNK_SHIFT;
for (int chunkZ = minimumChunkZ; chunkZ <= maximumChunkZ; chunkZ++) {
for (int chunkX = minimumChunkX; chunkX <= maximumChunkX; chunkX++) {
List<CavePosition> positions = positionsByChunk.get(packChunk(chunkX, chunkZ));
int[] positions = positionsByChunk.get(packChunk(chunkX, chunkZ));
if (positions == null) {
continue;
}
for (CavePosition position : positions) {
if (inside(position, minimumX, minimumZ, maximumX, maximumZ)) {
consumer.accept(position);
for (int row : positions) {
if (inside(row, minimumX, minimumZ, maximumX, maximumZ)) {
consumer.accept(row);
}
}
}
}
}
private static boolean inside(
CavePosition position,
private boolean inside(
int row,
int minimumX,
int minimumZ,
int maximumX,
int maximumZ
) {
return position.x() >= minimumX && position.x() < maximumX
&& position.z() >= minimumZ && position.z() < maximumZ;
return packed.x[row] >= minimumX && packed.x[row] < maximumX
&& packed.z[row] >= minimumZ && packed.z[row] < maximumZ;
}
private static long packChunk(int chunkX, int chunkZ) {
@@ -440,17 +575,17 @@ public record HydrologyCavePlan(
}
private static final class ActionKeySet extends AbstractSet<CavePosition> {
private final Map<CavePosition, Byte> packed;
private final PackedPositions packed;
private final int size;
private ActionKeySet(Map<CavePosition, Byte> packed, int size) {
private ActionKeySet(PackedPositions packed, int size) {
this.packed = packed;
this.size = size;
}
@Override
public Iterator<CavePosition> iterator() {
return new ActionKeyIterator(packed.entrySet().iterator());
return new ActionKeyIterator(packed);
}
@Override
@@ -466,17 +601,17 @@ public record HydrologyCavePlan(
}
private static final class ActionEntrySet extends AbstractSet<Entry<CavePosition, HydrologyCaveAction>> {
private final Map<CavePosition, Byte> packed;
private final PackedPositions packed;
private final int size;
private ActionEntrySet(Map<CavePosition, Byte> packed, int size) {
private ActionEntrySet(PackedPositions packed, int size) {
this.packed = packed;
this.size = size;
}
@Override
public Iterator<Entry<CavePosition, HydrologyCaveAction>> iterator() {
return new ActionEntryIterator(packed.entrySet().iterator());
return new ActionEntryIterator(packed);
}
@Override
@@ -487,17 +622,17 @@ public record HydrologyCavePlan(
private static final class PreconditionEntrySet
extends AbstractSet<Entry<CavePosition, CaveVoxelPrecondition>> {
private final Map<CavePosition, Byte> packed;
private final List<CavePosition> order;
private final PackedPositions packed;
private final PositionOrder order;
private PreconditionEntrySet(Map<CavePosition, Byte> packed, List<CavePosition> order) {
private PreconditionEntrySet(PackedPositions packed, PositionOrder order) {
this.packed = packed;
this.order = order;
}
@Override
public Iterator<Entry<CavePosition, CaveVoxelPrecondition>> iterator() {
return new PreconditionEntryIterator(packed, order.iterator());
return new PreconditionEntryIterator(packed, order.rows);
}
@Override
@@ -507,10 +642,10 @@ public record HydrologyCavePlan(
}
private static final class PreconditionKeySet extends AbstractSet<CavePosition> {
private final Map<CavePosition, Byte> packed;
private final List<CavePosition> order;
private final PackedPositions packed;
private final PositionOrder order;
private PreconditionKeySet(Map<CavePosition, Byte> packed, List<CavePosition> order) {
private PreconditionKeySet(PackedPositions packed, PositionOrder order) {
this.packed = packed;
this.order = order;
}
@@ -532,17 +667,17 @@ public record HydrologyCavePlan(
}
private static final class ActionValues extends AbstractCollection<HydrologyCaveAction> {
private final Map<CavePosition, Byte> packed;
private final PackedPositions packed;
private final int size;
private ActionValues(Map<CavePosition, Byte> packed, int size) {
private ActionValues(PackedPositions packed, int size) {
this.packed = packed;
this.size = size;
}
@Override
public Iterator<HydrologyCaveAction> iterator() {
return new ActionValueIterator(packed.entrySet().iterator());
return new ActionValueIterator(packed);
}
@Override
@@ -552,17 +687,17 @@ public record HydrologyCavePlan(
}
private static final class PreconditionValues extends AbstractCollection<CaveVoxelPrecondition> {
private final Map<CavePosition, Byte> packed;
private final List<CavePosition> order;
private final PackedPositions packed;
private final PositionOrder order;
private PreconditionValues(Map<CavePosition, Byte> packed, List<CavePosition> order) {
private PreconditionValues(PackedPositions packed, PositionOrder order) {
this.packed = packed;
this.order = order;
}
@Override
public Iterator<CaveVoxelPrecondition> iterator() {
return new PreconditionValueIterator(packed, order.iterator());
return new PreconditionValueIterator(packed, order.rows);
}
@Override
@@ -572,121 +707,123 @@ public record HydrologyCavePlan(
}
private abstract static class FilteredActionIterator<T> implements Iterator<T> {
private final Iterator<Map.Entry<CavePosition, Byte>> entries;
private Map.Entry<CavePosition, Byte> next;
protected final PackedPositions packed;
private int next;
private FilteredActionIterator(Iterator<Map.Entry<CavePosition, Byte>> entries) {
this.entries = entries;
private FilteredActionIterator(PackedPositions packed) {
this.packed = packed;
}
@Override
public boolean hasNext() {
advance();
return next != null;
return next < packed.size();
}
@Override
public T next() {
advance();
if (next == null) {
if (!hasNext()) {
throw new NoSuchElementException();
}
Map.Entry<CavePosition, Byte> selected = next;
next = null;
return map(selected);
return map(next++);
}
protected abstract T map(Map.Entry<CavePosition, Byte> entry);
protected abstract T map(int row);
private void advance() {
while (next == null && entries.hasNext()) {
Map.Entry<CavePosition, Byte> candidate = entries.next();
if (hasAction(candidate.getValue())) {
next = candidate;
}
while (next < packed.size() && !hasAction(packed.flags[next])) {
next++;
}
}
}
private static final class ActionKeyIterator extends FilteredActionIterator<CavePosition> {
private ActionKeyIterator(Iterator<Map.Entry<CavePosition, Byte>> entries) {
super(entries);
private ActionKeyIterator(PackedPositions packed) {
super(packed);
}
@Override
protected CavePosition map(Map.Entry<CavePosition, Byte> entry) {
return entry.getKey();
protected CavePosition map(int row) {
return packed.position(row);
}
}
private static final class ActionEntryIterator
extends FilteredActionIterator<Entry<CavePosition, HydrologyCaveAction>> {
private ActionEntryIterator(Iterator<Map.Entry<CavePosition, Byte>> entries) {
super(entries);
private ActionEntryIterator(PackedPositions packed) {
super(packed);
}
@Override
protected Entry<CavePosition, HydrologyCaveAction> map(Map.Entry<CavePosition, Byte> entry) {
return Map.entry(entry.getKey(), decodeAction(entry.getValue()));
protected Entry<CavePosition, HydrologyCaveAction> map(int row) {
return Map.entry(packed.position(row), decodeAction(packed.flags[row]));
}
}
private static final class ActionValueIterator extends FilteredActionIterator<HydrologyCaveAction> {
private ActionValueIterator(Iterator<Map.Entry<CavePosition, Byte>> entries) {
super(entries);
private ActionValueIterator(PackedPositions packed) {
super(packed);
}
@Override
protected HydrologyCaveAction map(Map.Entry<CavePosition, Byte> entry) {
return decodeAction(entry.getValue());
protected HydrologyCaveAction map(int row) {
return decodeAction(packed.flags[row]);
}
}
private static final class PreconditionEntryIterator
implements Iterator<Entry<CavePosition, CaveVoxelPrecondition>> {
private final Map<CavePosition, Byte> packed;
private final Iterator<CavePosition> positions;
private final PackedPositions packed;
private final int[] rows;
private int index;
private PreconditionEntryIterator(
Map<CavePosition, Byte> packed,
Iterator<CavePosition> positions
PackedPositions packed,
int[] rows
) {
this.packed = packed;
this.positions = positions;
this.rows = rows;
}
@Override
public boolean hasNext() {
return positions.hasNext();
return index < rows.length;
}
@Override
public Entry<CavePosition, CaveVoxelPrecondition> next() {
CavePosition position = positions.next();
return Map.entry(position, decodePrecondition(packed.get(position)));
if (!hasNext()) {
throw new NoSuchElementException();
}
int row = rows[index++];
return Map.entry(packed.position(row), decodePrecondition(packed.flags[row]));
}
}
private static final class PreconditionValueIterator implements Iterator<CaveVoxelPrecondition> {
private final Map<CavePosition, Byte> packed;
private final Iterator<CavePosition> positions;
private final PackedPositions packed;
private final int[] rows;
private int index;
private PreconditionValueIterator(
Map<CavePosition, Byte> packed,
Iterator<CavePosition> positions
PackedPositions packed,
int[] rows
) {
this.packed = packed;
this.positions = positions;
this.rows = rows;
}
@Override
public boolean hasNext() {
return positions.hasNext();
return index < rows.length;
}
@Override
public CaveVoxelPrecondition next() {
return decodePrecondition(packed.get(positions.next()));
if (!hasNext()) {
throw new NoSuchElementException();
}
return decodePrecondition(packed.flags[rows[index++]]);
}
}
}
@@ -45,6 +45,11 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
}
}
@Override
public boolean supportsSharedGridSamples() {
return true;
}
@Override
public HydrologyTerrainSample[] sampleGrid(GridRequest request) {
int minimumX = request.minimumX();
@@ -125,6 +130,33 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
return basis(blockX, blockZ).terrain();
}
@Override
public double sampleLandHeight(int blockX, int blockZ) {
long packed = pack(blockX, blockZ);
CacheStripe stripe = stripe(packed);
synchronized (stripe) {
TerrainBasis cached = stripe.bases.getAndMoveToLast(packed);
if (cached != null) {
return cached.terrain().ocean() ? Double.NaN : cached.terrain().naturalHeight();
}
}
double naturalHeight = naturalHeight(stripe, packed, blockX, blockZ);
if (!Double.isFinite(naturalHeight)) {
TerrainBasis basis = loadBasis(blockX, blockZ, stripe, packed, naturalHeight);
return basis.terrain().ocean() ? Double.NaN : basis.terrain().naturalHeight();
}
long roundedHeight = StrictMath.round(naturalHeight);
if (roundedHeight >= seaLevel) {
return (int) roundedHeight;
}
NaturalClassification classification = classifyNatural(blockX, blockZ);
if (classification == NaturalClassification.UNAVAILABLE) {
TerrainBasis basis = loadBasis(blockX, blockZ, stripe, packed, naturalHeight);
return basis.terrain().ocean() ? Double.NaN : basis.terrain().naturalHeight();
}
return classification == NaturalClassification.OCEAN ? Double.NaN : (int) roundedHeight;
}
TerrainBasis basis(int blockX, int blockZ) {
long packed = pack(blockX, blockZ);
CacheStripe stripe = stripe(packed);
@@ -134,7 +166,10 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
return cached;
}
}
double naturalHeight = naturalHeight(stripe, packed, blockX, blockZ);
return loadBasis(blockX, blockZ, stripe, packed, naturalHeight(stripe, packed, blockX, blockZ));
}
private TerrainBasis loadBasis(int blockX, int blockZ, CacheStripe stripe, long packed, double naturalHeight) {
TerrainBasis sampled = basisProvider.sample(blockX, blockZ, naturalHeight);
if (sampled == null) {
throw new NullPointerException(
@@ -79,6 +79,32 @@ public final class ErosionFieldCompiler {
HydrologyPlannerSettings.Ponds ponds,
SurfaceRasterContext context
) {
return compile(courseSeed, centerline, channel, valley, terminal, apronLimit, ponds, context,
prepare(centerline, channel, valley, terminal));
}
PreparedField prepare(SurfaceCenterline centerline, ChannelProfile channel, ValleyProfile valley,
SurfaceTerminal terminal) {
int count = terminal == SurfaceTerminal.OCEAN_MOUTH ? centerline.size() : valley.exposedStations();
boolean[] basin = basins(valley, channel, count);
double[] bendOffsets = SurfaceBendProfile.offsets(centerline, channel);
double[][] blendWidth = blendWidths(centerline, channel, valley, count);
int[] radii = new int[count];
for (int station = 0; station < count; station++) {
double widest = Math.max(blendWidth[station][0], blendWidth[station][1]);
// The shore biome band a policy asks for can be wider than the geometric shore and the
// eroded valley; the search radius has to reach it so those columns get a shore role.
double stationBand = stationShoreBiomeBand(centerline, station, surface.shoreWidth());
double stationShore = stationShoreWidth(centerline, station, surface.shoreWidth());
radii[station] = (int) StrictMath.ceil(channel.halfWidth()[station] * (1D + surface.banks().roughness())
+ Math.max(stationShore + widest, stationBand)) + 1;
}
return new PreparedField(count, basin, bendOffsets, blendWidth, radii);
}
ErosionField compile(long courseSeed, SurfaceCenterline centerline, ChannelProfile channel, ValleyProfile valley,
SurfaceTerminal terminal, int apronLimit, HydrologyPlannerSettings.Ponds ponds,
SurfaceRasterContext context, PreparedField prepared) {
Objects.requireNonNull(context, "context");
SurfaceBounds bounds = context.bounds();
SurfaceBounds samplingBounds = bounds == null ? null : bounds.expand(2);
@@ -87,24 +113,19 @@ public final class ErosionFieldCompiler {
HydrologyPlannerSettings.Erosion erosion = banks.erosion();
HydrologyPlannerSettings.Channel channelSettings = banks.channel();
HydrologyPlannerSettings.Flow flow = banks.flow();
int count = terminal == SurfaceTerminal.OCEAN_MOUTH ? centerline.size() : valley.exposedStations();
int count = prepared.count();
double roughness = banks.roughness();
double shoreWidth = surface.shoreWidth();
double shoreRise = erosion.shoreRise();
int sink = banks.sink();
boolean[] basin = basins(valley, channel, count);
double[] bendOffsets = SurfaceBendProfile.offsets(centerline, channel);
double[][] blendWidth = blendWidths(centerline, channel, valley, count);
boolean[] basin = prepared.basin();
double[] bendOffsets = prepared.bendOffsets();
double[][] blendWidth = prepared.blendWidth();
Long2IntOpenHashMap nearest = new Long2IntOpenHashMap();
nearest.defaultReturnValue(-1);
Long2DoubleOpenHashMap distance = new Long2DoubleOpenHashMap();
for (int station = 0; station < count; station++) {
double widest = Math.max(blendWidth[station][0], blendWidth[station][1]);
// The shore biome band a policy asks for can be wider than the geometric shore and the
// eroded valley; the search radius has to reach it so those columns get a shore role.
double stationBand = stationShoreBiomeBand(centerline, station, shoreWidth);
double stationShore = stationShoreWidth(centerline, station, shoreWidth);
int radius = (int) StrictMath.ceil(channel.halfWidth()[station] * (1D + roughness) + Math.max(stationShore + widest, stationBand)) + 1;
int radius = prepared.radii()[station];
int stationX = centerline.x()[station];
int stationZ = centerline.z()[station];
if (samplingBounds != null && !samplingBounds.intersects(stationX, stationZ, radius)) {
@@ -743,4 +764,7 @@ public final class ErosionFieldCompiler {
double deltaZ = cellZ - centerline.z()[station];
return deltaX * centerline.normalX(station) + deltaZ * centerline.normalZ(station);
}
record PreparedField(int count, boolean[] basin, double[] bendOffsets, double[][] blendWidth, int[] radii) {
}
}
@@ -51,14 +51,30 @@ public final class SurfaceFootprintCompiler {
}
public SurfaceFootprint compile(RiverCourse course, SurfaceBounds bounds) {
return compile(prepare(course), bounds);
}
public PreparedCourse prepare(RiverCourse course) {
return new PreparedCourse(this, Objects.requireNonNull(course, "course"));
}
public SurfaceFootprint compile(PreparedCourse prepared, SurfaceBounds bounds) {
if (prepared.compiler != this) {
throw new IllegalArgumentException("Prepared course belongs to another surface compiler");
}
RiverCourse course = prepared.course;
boolean pool = course.type() == RiverCourseType.SURFACE_POOL;
if (course.type() != RiverCourseType.SURFACE && !pool) {
return SurfaceFootprint.empty();
}
HydrologyTerrainSampler receivingSampler = HydrologyOceanReceiver.forCourse(settings, sampler, course);
if (prepared.receivingSampler == null) {
prepared.receivingSampler = HydrologyOceanReceiver.forCourse(settings, sampler, course);
prepared.offsets = stationOffsets(course.segments());
}
HydrologyTerrainSampler receivingSampler = prepared.receivingSampler;
HydrologySurfaceDropRaster drops = HydrologySurfaceDropRaster.compile(settings, receivingSampler, geometry, course,
bounds == null ? null : bounds.expand(2));
int[] offsets = stationOffsets(course.segments());
int[] offsets = prepared.offsets;
ArrayList<SurfaceLayerColumn> columns = new ArrayList<>();
int uncontained = 0;
long excavation = 0L;
@@ -74,7 +90,7 @@ public final class SurfaceFootprintCompiler {
while (after < course.segments().size() && exposedSegment(course.segments().get(after))) {
after++;
}
SurfaceFootprint run = compileRun(course, first, after, offsets[first], bounds, drops, pool, receivingSampler);
SurfaceFootprint run = compileRun(prepared, first, after, offsets[first], bounds, drops, pool, receivingSampler);
columns.addAll(run.columns());
uncontained += run.uncontainedWetCells();
excavation += run.bankExcavation();
@@ -106,14 +122,10 @@ public final class SurfaceFootprintCompiler {
return new SurfaceFootprint(columns, uncontained, rejection, detail, excavation);
}
private SurfaceFootprint compileRun(RiverCourse course, int first, int after, int stationOffset,
SurfaceBounds bounds, HydrologySurfaceDropRaster drops, boolean pool,
HydrologyTerrainSampler receivingSampler) {
private PreparedRun prepareRun(RiverCourse course, int first, int after, boolean pool,
HydrologyTerrainSampler receivingSampler) {
List<HydraulicSegment> exposed = course.segments().subList(first, after);
Stations stations = stations(exposed);
if (stations.count() < 1) {
return SurfaceFootprint.empty();
}
boolean fallingEnd = after < course.segments().size() && course.segments().get(after).fallingFluid();
SurfaceTerminal terminal = pool ? SurfaceTerminal.SINKHOLE
: fallingEnd ? SurfaceTerminal.TRIBUTARY : terminal(course, exposed);
@@ -129,9 +141,29 @@ public final class SurfaceFootprintCompiler {
: new HydrologyPlannerSettings.Ponds(first == 0 ? settings.surface().banks().ponds().source() : disabled.source(),
fallingEnd ? disabled.terminal() : settings.surface().banks().ponds().terminal());
SurfaceRunBoundary boundary = runBoundary(course, first, after, centerline, channel);
ErosionField field = new ErosionFieldCompiler(settings.surface(), receivingSampler, settings.seaLevel()).compile(
HydrologyHash.mix(course.id(), COURSE_SEED_SALT), centerline, channel, valley, terminal,
settings.outlets().maximumOceanApron(), ponds, new SurfaceRasterContext(bounds, drops, boundary));
ErosionFieldCompiler erosion = new ErosionFieldCompiler(settings.surface(), receivingSampler, settings.seaLevel());
return new PreparedRun(exposed, stations, coastalChannel, centerline, channel, poolBiome, valley,
terminal, ponds, boundary, erosion, erosion.prepare(centerline, channel, valley, terminal));
}
private SurfaceFootprint compileRun(PreparedCourse prepared, int first, int after, int stationOffset,
SurfaceBounds bounds, HydrologySurfaceDropRaster drops, boolean pool,
HydrologyTerrainSampler receivingSampler) {
RiverCourse course = prepared.course;
PreparedRun run = prepared.runs[first];
if (run == null) {
run = prepareRun(course, first, after, pool, receivingSampler);
prepared.runs[first] = run;
}
List<HydraulicSegment> exposed = run.exposed();
Stations stations = run.stations();
SurfaceCenterline centerline = run.centerline();
boolean coastalChannel = run.coastalChannel();
String poolBiome = run.poolBiome();
ErosionField field = run.erosion().compile(
HydrologyHash.mix(course.id(), COURSE_SEED_SALT), centerline, run.channel(), run.valley(), run.terminal(),
settings.outlets().maximumOceanApron(), run.ponds(), new SurfaceRasterContext(bounds, drops, run.boundary()),
run.field());
ArrayList<SurfaceColumn> ordered = new ArrayList<>(field.columns().values());
ordered.sort(Comparator.comparingInt(SurfaceColumn::station)
.thenComparingLong((SurfaceColumn column) -> RiverFootprint.pack(column.x(), column.z())));
@@ -367,8 +399,25 @@ public final class SurfaceFootprintCompiler {
private record Stations(List<HydrologyPoint> points, int[] segmentIndex, int[] head, double[] width,
double[] depth, int exposedStations) {
private int count() {
return points.size();
}
public static final class PreparedCourse {
private final SurfaceFootprintCompiler compiler;
private final RiverCourse course;
private final PreparedRun[] runs;
private HydrologyTerrainSampler receivingSampler;
private int[] offsets;
private PreparedCourse(SurfaceFootprintCompiler compiler, RiverCourse course) {
this.compiler = compiler;
this.course = course;
this.runs = new PreparedRun[course.segments().size()];
}
}
private record PreparedRun(List<HydraulicSegment> exposed, Stations stations, boolean coastalChannel,
SurfaceCenterline centerline, ChannelProfile channel, String poolBiome, ValleyProfile valley,
SurfaceTerminal terminal, HydrologyPlannerSettings.Ponds ponds, SurfaceRunBoundary boundary,
ErosionFieldCompiler erosion, ErosionFieldCompiler.PreparedField field) {
}
}
@@ -356,7 +356,11 @@ public interface EngineMantle extends MatterGenerator {
}
default boolean cleanupCoveredChunk(int x, int z, boolean force) {
MantleChunk<Matter> chunk = getMantle().getChunk(x, z).use();
return cleanupCoveredChunk(getMantle().getChunk(x, z), force);
}
default boolean cleanupCoveredChunk(MantleChunk<Matter> chunk, boolean force) {
chunk.use();
try {
synchronized (chunk) {
if (chunk.isFlagged(MantleFlag.CLEANED)) {
@@ -599,7 +599,7 @@ public class IrisComplex implements DataProvider {
}
private boolean sampleNaturalOcean(int x, int z) {
return resolveNaturalInferredType(bridgeStream, focusBiome, x, z) == InferredType.SEA;
return resolveNaturalInferredType(bridgeStream, focusedBiomeAt(x, z), x, z) == InferredType.SEA;
}
/**
@@ -138,11 +138,13 @@ public class IrisEngineMantle implements EngineMantle {
int candidateX = newRealX + offsetX;
for (int offsetZ = -radius; offsetZ <= radius; offsetZ++) {
int candidateZ = newRealZ + offsetZ;
if (getMantle().hasFlag(candidateX, candidateZ, MantleFlag.CLEANED)
if (!isCoverageResident(candidateX, candidateZ, radius)
|| getMantle().hasLoadedFlag(candidateX, candidateZ, MantleFlag.CLEANED)
|| !isCovered(candidateX, candidateZ, missingChunk)) {
continue;
}
if (cleanupCoveredChunk(candidateX, candidateZ, force)) {
if (getMantle().withLoadedChunk(candidateX, candidateZ,
chunk -> cleanupCoveredChunk(chunk, force))) {
callback.onChunkCleaned(candidateX, candidateZ);
}
}
@@ -246,12 +248,23 @@ public class IrisEngineMantle implements EngineMantle {
return Objects.requireNonNull(storageDirectory, "mantle storage directory").toAbsolutePath().normalize();
}
private boolean isCoverageResident(int x, int z, int radius) {
for (int regionX = (x - radius) >> 5; regionX <= (x + radius) >> 5; regionX++) {
for (int regionZ = (z - radius) >> 5; regionZ <= (z + radius) >> 5; regionZ++) {
if (!getMantle().isChunkLoaded(regionX << 5, regionZ << 5)) {
return false;
}
}
}
return true;
}
private boolean isCovered(int x, int z, MissingRealChunk missingChunk) {
int radius = Math.max(getRadius(), getRealRadius());
if (missingChunk != null && missingChunk.known
&& Math.abs((long) missingChunk.x - x) <= radius
&& Math.abs((long) missingChunk.z - z) <= radius) {
if (!getMantle().hasFlag(missingChunk.x, missingChunk.z, MantleFlag.REAL)) {
if (!getMantle().hasLoadedFlag(missingChunk.x, missingChunk.z, MantleFlag.REAL)) {
return false;
}
missingChunk.known = false;
@@ -260,7 +273,7 @@ public class IrisEngineMantle implements EngineMantle {
for (int offsetZ = -radius; offsetZ <= radius; offsetZ++) {
int chunkX = x + offsetX;
int chunkZ = z + offsetZ;
if (!getMantle().hasFlag(chunkX, chunkZ, MantleFlag.REAL)) {
if (!getMantle().hasLoadedFlag(chunkX, chunkZ, MantleFlag.REAL)) {
if (missingChunk != null) {
missingChunk.x = chunkX;
missingChunk.z = chunkZ;
@@ -352,14 +365,16 @@ public class IrisEngineMantle implements EngineMantle {
@Override
public <T> void remove(Matter section, int x, int y, int z, Class<T> type) {
MatterSlice<T> slice = section.slice(type);
slice.set(x, y, z, null);
MatterSlice<T> slice = section.getSlice(type);
if (slice != null) {
slice.set(x, y, z, null);
}
}
@Override
public <T> T get(Matter section, int x, int y, int z, Class<T> type) {
MatterSlice<T> slice = section.slice(type);
return slice.get(x, y, z);
MatterSlice<T> slice = section.getSlice(type);
return slice == null ? null : slice.get(x, y, z);
}
@Override
@@ -223,6 +223,10 @@ public final class BukkitBlockResolution {
return BASE.getNoCompat(bdxf);
}
public static BlockData decodeBlockData(String key) {
return BASE.decodeBlockData(key);
}
public static BlockData get(String bdxf) {
return BASE.get(bdxf);
}
@@ -60,6 +60,11 @@ public final class BukkitRegistries implements PlatformRegistries {
return INMS.get().generationRegistry();
}
@Override
public NativeBlockState decodeBlockState(String key) {
return BukkitBlockState.of(BukkitBlockResolution.decodeBlockData(key));
}
@Override
public NativeBlockState block(String key) {
BlockData data = BukkitBlockResolution.get(key);
@@ -345,7 +345,7 @@ public class IrisPregenerator {
int previousLoaded = loadedBefore;
int stagnantRounds = 0;
for (int attempt = 0; attempt < 24 && stagnantRounds < 4; attempt++) {
mantle.trim(0, 0);
mantle.trim(0L);
mantle.unloadTectonicPlate(0);
int loaded = mantle.getLoadedRegionCount();
if (loaded <= 0) {
@@ -441,8 +441,7 @@ public class IrisPregenerator {
try {
Mantle mantle = getMantle();
if (mantle != null) {
mantle.trim(0, 0);
mantle.unloadTectonicPlate(0);
mantle.saveOldestIdleTectonicPlate();
}
} catch (Throwable e) {
IrisLogging.reportError(e);
@@ -34,7 +34,7 @@ public final class MantleHeapPressure {
private static final long MAXIMUM_HYSTERESIS_MS = 60_000L;
private static final ObjectName DIAGNOSTIC_COMMAND = diagnosticCommandName();
private static final PanicGcReclaimer PANIC_RECLAIMER = new PanicGcReclaimer(
new PanicGcPolicy(10_000L, 60_000L, 60_000L, 15 * 60_000L),
new PanicGcPolicy(250L, 5_000L, 10_000L, 60_000L, 60_000L, 15 * 60_000L),
new PanicGcActions(
System::currentTimeMillis,
System::gc,
@@ -85,7 +85,8 @@ public final class MantleHeapPressure {
}
public static void requestPanicReclaim() {
PANIC_RECLAIMER.request(usedFraction());
double fraction = usedFraction();
PANIC_RECLAIMER.request(fraction, PRESSURE_GATE.update(fraction));
}
static void invokeHotSpotDiagnosticGc(MBeanServer server) throws Exception {
@@ -108,13 +109,17 @@ public final class MantleHeapPressure {
}
record PanicGcPolicy(
long explicitGraceMs,
long explicitCooldownMs,
long diagnosticDelayMs,
long diagnosticCooldownMs,
long initialFailureBackoffMs,
long maximumFailureBackoffMs
) {
PanicGcPolicy {
if (diagnosticDelayMs < 0L
if (explicitGraceMs < 0L
|| explicitCooldownMs < 0L
|| diagnosticDelayMs < 0L
|| diagnosticCooldownMs < 0L
|| initialFailureBackoffMs < 0L
|| maximumFailureBackoffMs < initialFailureBackoffMs) {
@@ -138,15 +143,13 @@ public final class MantleHeapPressure {
}
static final class HeapPressureGate {
private static final long NOT_BELOW_HIGH_WATER = Long.MIN_VALUE;
private final double highWater;
private final double lowWater;
private final long maximumHysteresisMs;
private final LongSupplier clock;
private final Runnable releaseAction;
private boolean engaged;
private long belowHighWaterSince;
private long episodeStartedAt;
HeapPressureGate(double highWater, double lowWater, long maximumHysteresisMs, LongSupplier clock, Runnable releaseAction) {
if (!Double.isFinite(highWater)
@@ -161,7 +164,6 @@ public final class MantleHeapPressure {
this.maximumHysteresisMs = maximumHysteresisMs;
this.clock = clock;
this.releaseAction = releaseAction;
this.belowHighWaterSince = NOT_BELOW_HIGH_WATER;
}
synchronized boolean update(double fraction) {
@@ -171,7 +173,7 @@ public final class MantleHeapPressure {
if (!engaged) {
if (fraction >= highWater) {
engaged = true;
belowHighWaterSince = NOT_BELOW_HIGH_WATER;
episodeStartedAt = clock.getAsLong();
}
return engaged;
}
@@ -180,16 +182,11 @@ public final class MantleHeapPressure {
return false;
}
if (fraction >= highWater) {
belowHighWaterSince = NOT_BELOW_HIGH_WATER;
return true;
}
long now = clock.getAsLong();
if (belowHighWaterSince == NOT_BELOW_HIGH_WATER) {
belowHighWaterSince = now;
return true;
}
if (elapsed(now, belowHighWaterSince) < maximumHysteresisMs) {
if (elapsed(now, episodeStartedAt) < maximumHysteresisMs) {
return true;
}
@@ -199,7 +196,7 @@ public final class MantleHeapPressure {
private void release() {
engaged = false;
belowHighWaterSince = NOT_BELOW_HIGH_WATER;
episodeStartedAt = 0L;
releaseAction.run();
}
@@ -212,7 +209,10 @@ public final class MantleHeapPressure {
private final PanicGcPolicy policy;
private final PanicGcActions actions;
private boolean panicEpisode;
private boolean explicitAttempted;
private long episodeStartedAt;
private long explicitAttemptAt;
private long nextExplicitAllowedAt = Long.MIN_VALUE;
private long nextDiagnosticAllowedAt;
private long failureBackoffMs;
@@ -222,17 +222,29 @@ public final class MantleHeapPressure {
this.failureBackoffMs = policy.initialFailureBackoffMs();
}
synchronized void request(double fraction) {
if (!Double.isFinite(fraction) || fraction <= LOW_WATER) {
synchronized void request(double fraction, boolean pressureEngaged) {
if (!Double.isFinite(fraction)) {
return;
}
if (fraction <= LOW_WATER) {
resetEpisode();
return;
}
long now = actions.clock().getAsLong();
if (!panicEpisode) {
if (fraction < HIGH_WATER) {
if (fraction < HIGH_WATER && !pressureEngaged) {
return;
}
beginEpisode(now);
panicEpisode = true;
episodeStartedAt = now;
}
if (!explicitAttempted) {
if (elapsed(now, episodeStartedAt) < policy.explicitGraceMs()
|| now < nextExplicitAllowedAt) {
return;
}
attemptExplicitGc(now);
return;
}
if (elapsed(now, explicitAttemptAt) < policy.diagnosticDelayMs()
@@ -256,12 +268,15 @@ public final class MantleHeapPressure {
synchronized void resetEpisode() {
panicEpisode = false;
explicitAttempted = false;
episodeStartedAt = 0L;
explicitAttemptAt = 0L;
}
private void beginEpisode(long now) {
panicEpisode = true;
private void attemptExplicitGc(long now) {
explicitAttempted = true;
explicitAttemptAt = now;
nextExplicitAllowedAt = deadline(now, policy.explicitCooldownMs());
try {
actions.explicitGc().run();
} catch (RuntimeException failure) {
@@ -84,7 +84,7 @@ public final class PregenMantleBackpressure {
int freed;
int resident;
try {
mantle.trim(0L, 0);
mantle.trim(0L);
freed = mantle.unloadTectonicPlate(0);
resident = mantle.getLoadedRegionCount();
} catch (Throwable e) {
@@ -120,24 +120,27 @@ public final class PregenMantleBackpressure {
}
public void awaitHeapHeadroom() {
awaitHeapHeadroom(MantleHeapPressure::overHighWater, MantleHeapPressure::requestPanicReclaim);
}
void awaitHeapHeadroom(BooleanSupplier heapPressure, Runnable panicReclaim) {
Mantle mantle = resolveMantle();
long waitStart = M.ms();
long lastLog = 0L;
while (MantleHeapPressure.overHighWater()) {
while (heapPressure.getAsBoolean()) {
if (isCancelled()) {
return;
}
try {
if (mantle != null && mantle.getLoadedRegionCount() > maxResidentTectonicPlates) {
mantle.trim(0L, 0);
mantle.unloadTectonicPlate(0);
if (mantle != null) {
mantle.saveOldestIdleTectonicPlate();
}
} catch (Throwable e) {
IrisLogging.reportError(e);
}
MantleHeapPressure.requestPanicReclaim();
panicReclaim.run();
long elapsed = M.ms() - waitStart;
if (elapsed >= timeoutMs) {
@@ -2,6 +2,7 @@ package art.arcane.iris.world.storage.matter;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.util.data.palette.Palette;
import art.arcane.volmlib.util.matter.Sliced;
import art.arcane.volmlib.util.matter.slices.RawMatter;
@@ -33,7 +34,7 @@ public class PlatformBlockMatter extends RawMatter<NativeBlockState> {
@Override
public NativeBlockState readNode(DataInputStream din) throws IOException {
NativeBlockState state = B.getState(din.readUTF());
NativeBlockState state = IrisPlatforms.get().registries().decodeBlockState(din.readUTF());
return state == null ? B.getAirState() : state;
}
}
@@ -2,7 +2,7 @@ package art.arcane.iris.world.storage.matter;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.util.data.palette.Palette;
import art.arcane.volmlib.util.function.Consumer4;
import art.arcane.volmlib.util.function.Consumer4IO;
@@ -114,7 +114,7 @@ public final class PreObjectMatter extends MappedHunk<PreObjectMatterCell>
public PreObjectMatterCell readNode(DataInputStream input) throws IOException {
int flags = input.readUnsignedByte();
validateFlags(flags);
NativeBlockState block = (flags & BLOCK_PRESENT) == 0 ? null : B.getState(input.readUTF());
NativeBlockState block = (flags & BLOCK_PRESENT) == 0 ? null : IrisPlatforms.get().registries().decodeBlockState(input.readUTF());
String string = (flags & STRING_PRESENT) == 0 ? null : input.readUTF();
MatterCavern cavern = (flags & CAVERN_PRESENT) == 0
? null