mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +00:00
Tuning
This commit is contained in:
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);
|
||||
}
|
||||
}
|
||||
+18
-4
@@ -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,
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
@@ -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;
|
||||
|
||||
+134
@@ -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();
|
||||
}
|
||||
|
||||
+18
-8
@@ -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;
|
||||
|
||||
+20
-5
@@ -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) {
|
||||
|
||||
+81
-27
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -5,6 +5,10 @@ public interface HydrologyRoutingTerrainSampler {
|
||||
|
||||
NaturalClassification classifyNatural(int blockX, int blockZ);
|
||||
|
||||
default boolean supportsSharedGridSamples() {
|
||||
return false;
|
||||
}
|
||||
|
||||
enum NaturalClassification {
|
||||
LAND,
|
||||
OCEAN,
|
||||
|
||||
+36
-5
@@ -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,
|
||||
|
||||
+5
-14
@@ -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) {
|
||||
}
|
||||
|
||||
|
||||
+266
-129
@@ -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++]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
-1
@@ -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(
|
||||
|
||||
+34
-10
@@ -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) {
|
||||
}
|
||||
}
|
||||
+63
-14
@@ -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
|
||||
|
||||
Reference in new issue
Block a user