mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 06:20:02 +00:00
Hydrology Fix, and V+
This commit is contained in:
1 parent
dfb3748059
commit
1e1eaa96aa
137 files changed
+8842
-711
No files matched your search
@@ -206,6 +206,8 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
|
||||
@ArrayType(type = IrisBiomePaletteLayer.class)
|
||||
@Description("This defines the layers of materials in this biome. Each layer has a palette and min/max height and some other properties. Usually a grassy/sandy layer then a dirt layer then a stone layer. Iris will fill in the remaining blocks below your layers with stone.")
|
||||
private KList<IrisBiomePaletteLayer> layers = new KList<IrisBiomePaletteLayer>().qadd(new IrisBiomePaletteLayer());
|
||||
@Description("What this biome's columns surface with when every layer is rejected, usually because all of them are slope gated and the column is too steep. Omit to inherit the dimension setting.")
|
||||
private IrisSurfaceLayerFallback surfaceLayerFallback = null;
|
||||
@ArrayType(type = IrisBiomePaletteLayer.class)
|
||||
@Description("Layers of materials placed on cave ceilings in this biome, indexed upward from the ceiling surface. Must not have more entries than layers, whose height generators it reuses. Omitting this leaves cave ceilings unchanged.")
|
||||
private KList<IrisBiomePaletteLayer> caveCeilingLayers = new KList<>();
|
||||
|
||||
@@ -56,7 +56,9 @@ final class IrisBiomeLayerGenerator {
|
||||
|
||||
private static KList<PlatformBlockState> generateLayers(IrisBiome biome, IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex, ProceduralStream<Double> slopeStream) {
|
||||
if (biome.isLockLayers()) {
|
||||
return generateLockedLayers(biome, wx, wz, random, maxDepth, height, rdata, complex, slopeStream);
|
||||
KList<PlatformBlockState> data = generateLockedLayers(biome, wx, wz, random, maxDepth, height, rdata, complex, slopeStream);
|
||||
appendSurfaceFallback(data, biome, dim, wx, wz, random, Math.min(maxDepth, biome.getLockLayersMax()), rdata);
|
||||
return data;
|
||||
}
|
||||
|
||||
KList<PlatformBlockState> data = new KList<>();
|
||||
@@ -114,9 +116,24 @@ final class IrisBiomeLayerGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
appendSurfaceFallback(data, biome, dim, wx, wz, random, maxDepth, rdata);
|
||||
return data;
|
||||
}
|
||||
|
||||
private static void appendSurfaceFallback(KList<PlatformBlockState> data, IrisBiome biome, IrisDimension dim,
|
||||
double wx, double wz, RNG random, int maxDepth, IrisData rdata) {
|
||||
if (!data.isEmpty() || maxDepth <= 0 || biome.getLayers().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
IrisSurfaceLayerFallback fallback = biome.getSurfaceLayerFallback();
|
||||
if (fallback == null) {
|
||||
fallback = dim.getSurfaceLayerFallback();
|
||||
}
|
||||
if (fallback == IrisSurfaceLayerFallback.TOP_LAYER) {
|
||||
appendLayer(data, biome.getLayers().get(0), 0, 1, wx, wz, random, maxDepth, rdata);
|
||||
}
|
||||
}
|
||||
|
||||
static KList<PlatformBlockState> generateCeilingLayers(IrisBiome biome, IrisDimension dim, double wx, double wz, RNG random, int maxDepth, int height, IrisData rdata, IrisComplex complex) {
|
||||
KList<PlatformBlockState> data = new KList<>();
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.generation.biome;
|
||||
|
||||
import art.arcane.volmlib.util.documentation.Description;
|
||||
|
||||
@Description("What a column surfaces with when every biome layer is rejected, usually because all of them are slope gated and the column is too steep.")
|
||||
public enum IrisSurfaceLayerFallback {
|
||||
@Description("Leave the column without layers so it surfaces with the dimension rock palette. Use this for biomes meant to be bare rock where they get steep.")
|
||||
ROCK,
|
||||
|
||||
@Description("Cap the column with a single block of the first layer's palette, ignoring that layer's slope condition. Keeps steep coastlines and cliff faces from turning into bare stone.")
|
||||
TOP_LAYER
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.ForkJoinWorkerThread;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.IntSupplier;
|
||||
|
||||
@@ -21,7 +22,7 @@ public class MultiBurst extends MultiBurstSupport {
|
||||
* Long-running hydrology tile planning. Kept off {@link #burst} so a handful of twenty-second
|
||||
* plans never pin the workers that chunk generation's short stage and mantle tasks need.
|
||||
*/
|
||||
public static final MultiBurst hydrology = new MultiBurst("Iris Hydrology", () -> Math.max(2, IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism())));
|
||||
public static final MultiBurst hydrology = new HydrologyBurst();
|
||||
private final AtomicInteger parallelismBaseline = new AtomicInteger();
|
||||
|
||||
public MultiBurst() {
|
||||
@@ -136,4 +137,21 @@ public class MultiBurst extends MultiBurstSupport {
|
||||
public static void close(ExecutorService service) {
|
||||
MultiBurstSupport.close(service, M::ms, IrisLogging::info, IrisLogging::warn, IrisLogging::reportError, TIMEOUT);
|
||||
}
|
||||
|
||||
static final class HydrologyBurst extends MultiBurst {
|
||||
HydrologyBurst() {
|
||||
super("Iris Hydrology", () -> Math.max(2,
|
||||
IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism())));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ForkJoinPool createPool(
|
||||
int parallelism,
|
||||
ForkJoinPool.ForkJoinWorkerThreadFactory factory,
|
||||
Thread.UncaughtExceptionHandler handler
|
||||
) {
|
||||
return new ForkJoinPool(parallelism, factory, handler, true,
|
||||
0, Integer.MAX_VALUE, parallelism, null, 60L, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,9 @@ final class HydrologyChannelGeometry {
|
||||
compiler.settings.surface().banks().maximumBlendWidth()
|
||||
);
|
||||
if (type == HydrologyFeatureType.WATERFALL) {
|
||||
// A plunge has no floodplain: the shore bench is cut level with the bank top, so a wide one
|
||||
// terraces the cliff the water falls down. Deliberate saturation, paired with the grading cap
|
||||
// below - shoreWidth stops tracking the knob past 2.5.
|
||||
shoreWidth = Math.min(2.5D, shoreWidth);
|
||||
gradingWidth = Math.min(gradingWidth, Math.max(8D, channelRadius * 4D));
|
||||
}
|
||||
|
||||
+134
-34
@@ -1,5 +1,7 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.generation.hydrology.cave.CavePosition;
|
||||
import art.arcane.iris.generation.hydrology.cave.CaveVoxelView;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCandidate;
|
||||
@@ -17,8 +19,10 @@ import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
final class HydrologyCrossTileResolver {
|
||||
private static final int FALLBACK_TRIAL_PARALLELISM = 16;
|
||||
private final HydrologyPlanner planner;
|
||||
|
||||
HydrologyCrossTileResolver(HydrologyPlanner planner) {
|
||||
@@ -114,7 +118,6 @@ final class HydrologyCrossTileResolver {
|
||||
)
|
||||
);
|
||||
seedRegionalSurface(key, footprintCompiler);
|
||||
diagnostics.addAll(footprintCompiler.regionalNetwork.diagnostics());
|
||||
HashMap<Long, List<HydrologyPoint>> refinedEdges = new HashMap<>();
|
||||
HashMap<SourceCompilationKey, SourceCompilation> sourceCompilations = new HashMap<>();
|
||||
settleSelection(
|
||||
@@ -147,6 +150,8 @@ final class HydrologyCrossTileResolver {
|
||||
? List.of()
|
||||
: planner.outletPlanner.resolveSurfaceFallbackOutlets(grid);
|
||||
int fallbackOutletIndex = 0;
|
||||
int fallbackBatchStart = 0;
|
||||
List<FallbackTrial> fallbackTrials = List.of();
|
||||
while (true) {
|
||||
int maximumPublicationPasses = Math.addExact(
|
||||
1,
|
||||
@@ -225,41 +230,27 @@ final class HydrologyCrossTileResolver {
|
||||
}
|
||||
boolean fallbackSelected = false;
|
||||
while (fallbackOutletIndex < fallbackOutlets.size()) {
|
||||
OutletCandidate fallbackOutlet = fallbackOutlets.get(fallbackOutletIndex++);
|
||||
HydrologyRoutingPlan fallbackRouting = planner.sourcePlanner.requireOrganicSurface(planner.sourcePlanner.buildRouting(grid, List.of(fallbackOutlet), true));
|
||||
HashMap<HydrologyTileKey, SourceRoutingContext> fallbackContexts = new HashMap<>();
|
||||
fallbackContexts.put(key, new SourceRoutingContext(
|
||||
grid,
|
||||
fallbackRouting,
|
||||
undergroundRouting,
|
||||
List.of()
|
||||
));
|
||||
SourceSelection fallbackSelection = planner.sourcePlanner.selectSources(
|
||||
key,
|
||||
grid,
|
||||
fallbackRouting,
|
||||
true,
|
||||
false,
|
||||
diagnostics,
|
||||
fallbackContexts
|
||||
);
|
||||
settleSelection(
|
||||
key,
|
||||
grid,
|
||||
fallbackRouting,
|
||||
undergroundRouting,
|
||||
fallbackSelection,
|
||||
true,
|
||||
footprintCompiler,
|
||||
refinedEdges,
|
||||
sourceCompilations,
|
||||
diagnostics
|
||||
);
|
||||
if (!fallbackSelection.hasAcceptedSelection()) {
|
||||
if (fallbackOutletIndex >= fallbackBatchStart + fallbackTrials.size()) {
|
||||
fallbackBatchStart = fallbackOutletIndex;
|
||||
fallbackTrials = compileFallbackTrials(new FallbackContext(
|
||||
key, grid, undergroundRouting, footprintCompiler, refinedEdges, sourceCompilations
|
||||
), fallbackOutlets, fallbackBatchStart);
|
||||
}
|
||||
FallbackTrial trial = fallbackTrials.get(fallbackOutletIndex++ - fallbackBatchStart);
|
||||
if (trial.failure() != null) {
|
||||
throw HydrologyPlanner.propagateOwnerFailure(trial.failure());
|
||||
}
|
||||
refinedEdges.putAll(trial.refinedEdges());
|
||||
sourceCompilations.putAll(trial.sourceCompilations());
|
||||
if (!trial.selection().hasAcceptedSelection()) {
|
||||
continue;
|
||||
}
|
||||
surfaceRouting = fallbackRouting;
|
||||
surfaceSelection = fallbackSelection;
|
||||
// Every trial re-selects over the whole owned lattice, so a discarded one invents a full set of
|
||||
// per-node rejections for a plan that was never published. Only the adopted trial may report.
|
||||
diagnostics.addAll(trial.diagnostics());
|
||||
surfaceRouting = trial.routing();
|
||||
surfaceSelection = trial.selection();
|
||||
footprintCompiler = trial.footprintCompiler();
|
||||
fallbackSelected = true;
|
||||
break;
|
||||
}
|
||||
@@ -287,6 +278,104 @@ final class HydrologyCrossTileResolver {
|
||||
return draft;
|
||||
}
|
||||
|
||||
private List<FallbackTrial> compileFallbackTrials(
|
||||
FallbackContext context,
|
||||
List<OutletCandidate> outlets,
|
||||
int start
|
||||
) {
|
||||
int end = Math.min(outlets.size(), start + FALLBACK_TRIAL_PARALLELISM);
|
||||
ArrayList<Callable<FallbackTrial>> tasks = new ArrayList<>(end - start);
|
||||
for (int index = start; index < end; index++) {
|
||||
OutletCandidate outlet = outlets.get(index);
|
||||
tasks.add(() -> compileFallbackTrial(context, outlet));
|
||||
}
|
||||
List<FallbackTrial> trials = HydrologyForkJoin.invokeAll(tasks,
|
||||
IrisPlatforms.isBound() ? MultiBurst.hydrology : null);
|
||||
DraftProfile ownerProfile = planner.currentDraftProfile();
|
||||
for (FallbackTrial trial : trials) {
|
||||
DraftProfile trialProfile = trial.profile();
|
||||
ownerProfile.routeCalls += trialProfile.routeCalls;
|
||||
ownerProfile.routeSolves += trialProfile.routeSolves;
|
||||
ownerProfile.routeSolveNanos += trialProfile.routeSolveNanos;
|
||||
ownerProfile.rasterCalls += trialProfile.rasterCalls;
|
||||
ownerProfile.rasterNanos += trialProfile.rasterNanos;
|
||||
ownerProfile.filterCalls += trialProfile.filterCalls;
|
||||
ownerProfile.filterNanos += trialProfile.filterNanos;
|
||||
}
|
||||
return trials;
|
||||
}
|
||||
|
||||
private FallbackTrial compileFallbackTrial(FallbackContext context, OutletCandidate outlet) {
|
||||
HydrologyPlanner.PlanningSamples previousSamples = planner.planningSamples.get();
|
||||
DraftProfile previousProfile = planner.draftProfiles.get();
|
||||
DraftProfile profile = new DraftProfile();
|
||||
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
|
||||
HashMap<Long, List<HydrologyPoint>> refinedEdges = new HashMap<>(context.refinedEdges());
|
||||
HashMap<SourceCompilationKey, SourceCompilation> sourceCompilations = new HashMap<>(context.sourceCompilations());
|
||||
HydrologyFootprintCompiler footprints = null;
|
||||
try {
|
||||
planner.planningSamples.set(new HydrologyPlanner.PlanningSamples());
|
||||
planner.draftProfiles.set(profile);
|
||||
footprints = new HydrologyFootprintCompiler(
|
||||
planner.settings,
|
||||
new HydrologyFootprintCompiler.Sampling(planner.sampler, planner.geometrySampler, planner.naturalSampler)
|
||||
);
|
||||
footprints.regionalNetwork = context.footprintCompiler().regionalNetwork;
|
||||
footprints.regionalSurfaceFootprints.putAll(context.footprintCompiler().regionalSurfaceFootprints);
|
||||
footprints.regionalDropRasters.putAll(context.footprintCompiler().regionalDropRasters);
|
||||
HydrologyRoutingPlan routing = planner.sourcePlanner.requireOrganicSurface(
|
||||
planner.sourcePlanner.buildRouting(context.grid(), List.of(outlet), true)
|
||||
);
|
||||
HashMap<HydrologyTileKey, SourceRoutingContext> routingContexts = new HashMap<>();
|
||||
routingContexts.put(context.key(), new SourceRoutingContext(
|
||||
context.grid(), routing, context.undergroundRouting(), List.of()
|
||||
));
|
||||
SourceSelection selection = planner.sourcePlanner.selectSources(
|
||||
context.key(), context.grid(), routing, true, false, diagnostics, routingContexts
|
||||
);
|
||||
settleSelection(context.key(), context.grid(), routing, context.undergroundRouting(), selection,
|
||||
true, footprints, refinedEdges, sourceCompilations, diagnostics);
|
||||
return new FallbackTrial(routing, selection, List.copyOf(diagnostics), footprints,
|
||||
refinedEdges, sourceCompilations, profile, null);
|
||||
} catch (Throwable failure) {
|
||||
return new FallbackTrial(null, null, List.copyOf(diagnostics), footprints,
|
||||
refinedEdges, sourceCompilations, profile, failure);
|
||||
} finally {
|
||||
if (previousSamples == null) {
|
||||
planner.planningSamples.remove();
|
||||
} else {
|
||||
planner.planningSamples.set(previousSamples);
|
||||
}
|
||||
if (previousProfile == null) {
|
||||
planner.draftProfiles.remove();
|
||||
} else {
|
||||
planner.draftProfiles.set(previousProfile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record FallbackContext(
|
||||
HydrologyTileKey key,
|
||||
HydrologySampledGrid grid,
|
||||
HydrologyRoutingPlan undergroundRouting,
|
||||
HydrologyFootprintCompiler footprintCompiler,
|
||||
Map<Long, List<HydrologyPoint>> refinedEdges,
|
||||
Map<SourceCompilationKey, SourceCompilation> sourceCompilations
|
||||
) {
|
||||
}
|
||||
|
||||
private record FallbackTrial(
|
||||
HydrologyRoutingPlan routing,
|
||||
SourceSelection selection,
|
||||
List<HydrologyDiagnosticCandidate> diagnostics,
|
||||
HydrologyFootprintCompiler footprintCompiler,
|
||||
Map<Long, List<HydrologyPoint>> refinedEdges,
|
||||
Map<SourceCompilationKey, SourceCompilation> sourceCompilations,
|
||||
DraftProfile profile,
|
||||
Throwable failure
|
||||
) {
|
||||
}
|
||||
|
||||
HydrologyTile materializeAcceptedTile(CrossTileResolution resolution) {
|
||||
HydrologyCaveCourseFilter.Result result = resolution.draft().result();
|
||||
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>(resolution.draft().diagnostics());
|
||||
@@ -324,6 +413,16 @@ final class HydrologyCrossTileResolver {
|
||||
if (footprintCompiler.fullMaterializationCount() < 1) {
|
||||
throw new IllegalStateException("Hydrology publication did not materialize its full footprint.");
|
||||
}
|
||||
HashSet<Long> regionalIds = new HashSet<>();
|
||||
for (RiverCourse course : footprintCompiler.regionalNetwork.courses()) {
|
||||
regionalIds.add(course.id());
|
||||
}
|
||||
HashSet<Long> publishedRegionalIds = new HashSet<>();
|
||||
for (RiverCourse course : result.courses()) {
|
||||
if (regionalIds.contains(course.id())) {
|
||||
publishedRegionalIds.add(course.id());
|
||||
}
|
||||
}
|
||||
return new HydrologyTile(
|
||||
resolution.draft().key(),
|
||||
planner.worldSeed,
|
||||
@@ -333,6 +432,7 @@ final class HydrologyCrossTileResolver {
|
||||
result.edges(),
|
||||
result.outlets(),
|
||||
result.courses(),
|
||||
publishedRegionalIds,
|
||||
result.cavePlans(),
|
||||
planner.sourcePlanner.uniqueDiagnostics(diagnostics),
|
||||
footprint
|
||||
|
||||
+93
-35
@@ -1,16 +1,21 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.iris.generation.hydrology.policy.SurfaceRiverPolicy;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import art.arcane.iris.generation.hydrology.policy.SurfaceRiverPolicy;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
final class HydrologyOutletPlanner {
|
||||
private static final int REGIONAL_SHORELINE_WINDOW = 4;
|
||||
private final HydrologyPlanner planner;
|
||||
|
||||
HydrologyOutletPlanner(HydrologyPlanner planner) {
|
||||
@@ -133,45 +138,95 @@ final class HydrologyOutletPlanner {
|
||||
ArrayList<OutletCandidate> oceanOutletCandidates(HydrologySampledGrid grid, boolean surface) {
|
||||
ArrayList<OutletCandidate> oceanCandidates = new ArrayList<>();
|
||||
for (HydrologyGridNode land : grid.nodes()) {
|
||||
if (land.terrain().ocean() || !land.terrain().transitAllowed() || !land.terrain().outletAllowed()) {
|
||||
Shoreline shoreline = shoreline(grid, land, surface);
|
||||
if (shoreline == null) {
|
||||
continue;
|
||||
}
|
||||
HydrologyGridNode ocean = firstOceanNeighbor(grid, land, surface);
|
||||
if (ocean == null || !land.terrain().drainsInto(ocean.terrain())) {
|
||||
// A confined shore only reaches a sea that belongs to its own area.
|
||||
continue;
|
||||
OutletCandidate candidate = oceanOutletCandidate(shoreline.land(), shoreline.ocean(), surface);
|
||||
if (candidate != null) {
|
||||
oceanCandidates.add(candidate);
|
||||
}
|
||||
HydrologyOceanBoundaryRefiner.Result boundary = refineOceanBoundary(land, ocean);
|
||||
if (boundary == null) {
|
||||
continue;
|
||||
}
|
||||
HydrologyTerrainSample receiving = planner.sampleBasisWithoutSlope(boundary.oceanPoint().x(), boundary.oceanPoint().z());
|
||||
if (receiving == null || !boundary.landwardTerrain().drainsInto(receiving)
|
||||
|| !boundary.landwardTerrain().transitAllowed()
|
||||
|| !boundary.landwardTerrain().outletAllowed()
|
||||
|| surface && !HydrologySurfaceProfiles.sharesProfile(boundary.landwardTerrain(), receiving)) {
|
||||
continue;
|
||||
}
|
||||
HydrologyFeatureType type = coastalOutletType(boundary.landwardTerrain());
|
||||
if (type == null) {
|
||||
continue;
|
||||
}
|
||||
long outletId = HydrologyHash.mix(planner.worldSeed, OUTLET_SALT, land.id(), ocean.id(), type.ordinal());
|
||||
int seaLevel = planner.settingsSeaLevel(ocean.terrain());
|
||||
RiverOutlet outlet = new RiverOutlet(
|
||||
outletId,
|
||||
type,
|
||||
land.id(),
|
||||
HydrologyPlanner.withY(boundary.landwardPoint(), seaLevel),
|
||||
HydrologyPlanner.withY(boundary.oceanPoint(), seaLevel),
|
||||
seaLevel,
|
||||
true
|
||||
);
|
||||
oceanCandidates.add(new OutletCandidate(land.index(), ocean.index(), outlet));
|
||||
}
|
||||
return oceanCandidates;
|
||||
}
|
||||
|
||||
ArrayList<OutletCandidate> regionalOceanOutletCandidates(HydrologySampledGrid grid) {
|
||||
ArrayList<OutletCandidate> oceanCandidates = new ArrayList<>();
|
||||
int index = 0;
|
||||
while (index < grid.nodes().size()) {
|
||||
ArrayList<Callable<OutletCandidate>> tasks = new ArrayList<>(REGIONAL_SHORELINE_WINDOW);
|
||||
while (index < grid.nodes().size() && tasks.size() < REGIONAL_SHORELINE_WINDOW) {
|
||||
Shoreline shoreline = shoreline(grid, grid.nodes().get(index++), true);
|
||||
if (shoreline != null) {
|
||||
tasks.add(() -> regionalOceanOutletCandidate(shoreline));
|
||||
}
|
||||
}
|
||||
for (OutletCandidate candidate : HydrologyForkJoin.invokeAll(tasks,
|
||||
IrisPlatforms.isBound() ? MultiBurst.hydrology : null)) {
|
||||
if (candidate != null) {
|
||||
oceanCandidates.add(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
return oceanCandidates;
|
||||
}
|
||||
|
||||
private OutletCandidate regionalOceanOutletCandidate(Shoreline shoreline) {
|
||||
HydrologyPlanner.PlanningSamples previous = planner.planningSamples.get();
|
||||
planner.planningSamples.set(new HydrologyPlanner.PlanningSamples());
|
||||
try {
|
||||
return oceanOutletCandidate(shoreline.land(), shoreline.ocean(), true);
|
||||
} finally {
|
||||
if (previous == null) {
|
||||
planner.planningSamples.remove();
|
||||
} else {
|
||||
planner.planningSamples.set(previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Shoreline shoreline(HydrologySampledGrid grid, HydrologyGridNode land, boolean surface) {
|
||||
if (land.terrain().ocean() || !land.terrain().transitAllowed() || !land.terrain().outletAllowed()) {
|
||||
return null;
|
||||
}
|
||||
HydrologyGridNode ocean = firstOceanNeighbor(grid, land, surface);
|
||||
if (ocean == null || !land.terrain().drainsInto(ocean.terrain())) {
|
||||
// A confined shore only reaches a sea that belongs to its own area.
|
||||
return null;
|
||||
}
|
||||
return new Shoreline(land, ocean);
|
||||
}
|
||||
|
||||
private OutletCandidate oceanOutletCandidate(HydrologyGridNode land, HydrologyGridNode ocean, boolean surface) {
|
||||
HydrologyOceanBoundaryRefiner.Result boundary = refineOceanBoundary(land, ocean);
|
||||
if (boundary == null) {
|
||||
return null;
|
||||
}
|
||||
HydrologyTerrainSample receiving = planner.sampleBasisWithoutSlope(boundary.oceanPoint().x(), boundary.oceanPoint().z());
|
||||
if (receiving == null || !boundary.landwardTerrain().drainsInto(receiving)
|
||||
|| !boundary.landwardTerrain().transitAllowed()
|
||||
|| !boundary.landwardTerrain().outletAllowed()
|
||||
|| surface && !HydrologySurfaceProfiles.sharesProfile(boundary.landwardTerrain(), receiving)) {
|
||||
return null;
|
||||
}
|
||||
HydrologyFeatureType type = coastalOutletType(boundary.landwardTerrain());
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
long outletId = HydrologyHash.mix(planner.worldSeed, OUTLET_SALT, land.id(), ocean.id(), type.ordinal());
|
||||
int seaLevel = planner.settingsSeaLevel(ocean.terrain());
|
||||
RiverOutlet outlet = new RiverOutlet(
|
||||
outletId,
|
||||
type,
|
||||
land.id(),
|
||||
HydrologyPlanner.withY(boundary.landwardPoint(), seaLevel),
|
||||
HydrologyPlanner.withY(boundary.oceanPoint(), seaLevel),
|
||||
seaLevel,
|
||||
true
|
||||
);
|
||||
return new OutletCandidate(land.index(), ocean.index(), outlet);
|
||||
}
|
||||
|
||||
List<OutletCandidate> resolveSurfaceFallbackOutlets(HydrologySampledGrid grid) {
|
||||
ArrayList<OutletCandidate> oceanCandidates = oceanOutletCandidates(grid, true);
|
||||
ArrayList<OutletCandidate> inlandCandidates = new ArrayList<>();
|
||||
@@ -728,4 +783,7 @@ final class HydrologyOutletPlanner {
|
||||
|
||||
record InlandConnection(HydrologyPoint point, HydrologyTerrainSample terrain) {
|
||||
}
|
||||
|
||||
private record Shoreline(HydrologyGridNode land, HydrologyGridNode ocean) {
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.cave.CaveVoxelView;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlan;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveContainmentPlanner;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceCourseBuilder;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
@@ -220,6 +221,7 @@ public final class HydrologyPlanner {
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(),
|
||||
Set.of(),
|
||||
List.of(),
|
||||
List.of(),
|
||||
RiverFootprint.empty()
|
||||
@@ -246,6 +248,15 @@ public final class HydrologyPlanner {
|
||||
return tile;
|
||||
}
|
||||
|
||||
public List<HydrologyDiagnosticCandidate> diagnosticCandidates(HydrologyTile tile) {
|
||||
Objects.requireNonNull(tile, "tile");
|
||||
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>(tile.localDiagnosticCandidates());
|
||||
diagnostics.addAll(regional.diagnosticsIn(regional.ownerBounds(tile.key())));
|
||||
ArrayList<HydrologyDiagnosticCandidate> unique = new ArrayList<>(sourcePlanner.uniqueDiagnostics(diagnostics));
|
||||
unique.sort(Comparator.comparingLong(HydrologyDiagnosticCandidate::id));
|
||||
return List.copyOf(unique);
|
||||
}
|
||||
|
||||
void clearOwnerDrafts() {
|
||||
resolvedOwners.invalidateAll();
|
||||
regional.clear();
|
||||
@@ -258,18 +269,29 @@ public final class HydrologyPlanner {
|
||||
|| tile.tileSize() != settings.routing().tileSize()) {
|
||||
throw new IllegalArgumentException("Hydrology tile does not match this planner.");
|
||||
}
|
||||
ArrayList<RiverCourse> localCourses = new ArrayList<>(tile.courses().size());
|
||||
for (RiverCourse course : tile.courses()) {
|
||||
if (!tile.regionalCourseIds().contains(course.id())) {
|
||||
localCourses.add(course);
|
||||
}
|
||||
}
|
||||
ArrayList<HydrologyCavePlan> localPlans = new ArrayList<>(tile.cavePlans().size());
|
||||
for (HydrologyCavePlan plan : tile.cavePlans()) {
|
||||
if (!tile.regionalCourseIds().contains(plan.source().sourceId())) {
|
||||
localPlans.add(plan);
|
||||
}
|
||||
}
|
||||
HydrologyCaveCourseFilter.Result result = new HydrologyCaveCourseFilter.Result(
|
||||
tile.nodes(),
|
||||
tile.edges(),
|
||||
tile.outlets(),
|
||||
tile.courses(),
|
||||
tile.cavePlans()
|
||||
localCourses,
|
||||
localPlans
|
||||
);
|
||||
result = regional.withoutRegional(result, tile.key());
|
||||
HydrologyOwnerDraft draft = new HydrologyOwnerDraft(
|
||||
tile.key(),
|
||||
result,
|
||||
tile.diagnosticCandidates(),
|
||||
tile.localDiagnosticCandidates(),
|
||||
null
|
||||
);
|
||||
resolvedOwners.put(tile.key(), new CrossTileResolvedOwner(draft, List.of()));
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceBounds;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
record HydrologyRegionalCoarsePlan(
|
||||
HydrologySampledGrid grid,
|
||||
List<OutletCandidate> outlets,
|
||||
HydrologyRegionalGraph.Tree tree,
|
||||
List<Integer> sources,
|
||||
List<HydrologyRegionalEnvelope> envelopes,
|
||||
double longest,
|
||||
boolean coastal,
|
||||
int weight
|
||||
) {
|
||||
HydrologyRegionalCoarsePlan {
|
||||
outlets = List.copyOf(outlets);
|
||||
sources = List.copyOf(sources);
|
||||
envelopes = List.copyOf(envelopes);
|
||||
}
|
||||
|
||||
boolean intersects(SurfaceBounds bounds) {
|
||||
if (coastal) {
|
||||
return true;
|
||||
}
|
||||
for (HydrologyRegionalEnvelope envelope : envelopes) {
|
||||
if (envelope.intersects(bounds)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceBounds;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
record HydrologyRegionalEnvelope(long minimumX, long minimumZ, long maximumX, long maximumZ) {
|
||||
private static final HydrologyRegionalEnvelope UNBOUNDED = new HydrologyRegionalEnvelope(
|
||||
Long.MIN_VALUE, Long.MIN_VALUE, Long.MAX_VALUE, Long.MAX_VALUE);
|
||||
|
||||
static HydrologyRegionalEnvelope of(List<HydrologyPoint> guide, HydrologyPlannerSettings settings, int footprintRadius) {
|
||||
long minimumX = Long.MAX_VALUE;
|
||||
long minimumZ = Long.MAX_VALUE;
|
||||
long maximumX = Long.MIN_VALUE;
|
||||
long maximumZ = Long.MIN_VALUE;
|
||||
for (HydrologyPoint point : guide) {
|
||||
minimumX = Math.min(minimumX, point.x());
|
||||
minimumZ = Math.min(minimumZ, point.z());
|
||||
maximumX = Math.max(maximumX, point.x());
|
||||
maximumZ = Math.max(maximumZ, point.z());
|
||||
}
|
||||
if (guide.isEmpty()) {
|
||||
return UNBOUNDED;
|
||||
}
|
||||
int spacing = settings.routing().regional().sampleSpacing();
|
||||
double commonPadding = spacing + spacing * settings.geometry().meanders().maximumOffsetRatio()
|
||||
+ 256D + 2D + Math.max(footprintRadius, (long) settings.publicationRadius() + 1L);
|
||||
double paddingX = StrictMath.ceil(commonPadding + (maximumX - minimumX + 2D * spacing) / 8D);
|
||||
double paddingZ = StrictMath.ceil(commonPadding + (maximumZ - minimumZ + 2D * spacing) / 8D);
|
||||
if (!Double.isFinite(paddingX) || !Double.isFinite(paddingZ)
|
||||
|| minimumX - paddingX < Integer.MIN_VALUE || maximumX + paddingX > Integer.MAX_VALUE
|
||||
|| minimumZ - paddingZ < Integer.MIN_VALUE || maximumZ + paddingZ > Integer.MAX_VALUE) {
|
||||
return UNBOUNDED;
|
||||
}
|
||||
return new HydrologyRegionalEnvelope(minimumX - (long) paddingX, minimumZ - (long) paddingZ,
|
||||
maximumX + (long) paddingX, maximumZ + (long) paddingZ);
|
||||
}
|
||||
|
||||
boolean intersects(SurfaceBounds bounds) {
|
||||
return maximumX >= bounds.minimumX() && minimumX <= bounds.maximumX()
|
||||
&& maximumZ >= bounds.minimumZ() && minimumZ <= bounds.maximumZ();
|
||||
}
|
||||
}
|
||||
+254
-74
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlan;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceBounds;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceCenterline;
|
||||
@@ -10,11 +11,13 @@ import art.arcane.iris.generation.hydrology.surface.SurfaceFootprint;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceFootprintCompiler;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceLayerColumn;
|
||||
import art.arcane.iris.generation.hydrology.surface.SurfaceTerminal;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -22,12 +25,14 @@ import java.util.Map;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
final class HydrologyRegionalPlanner {
|
||||
private static final long BASIN_SALT = 0x524547424153494eL;
|
||||
private static final long COURSE_SALT = 0x524547434f555253L;
|
||||
private static final long NODE_SALT = 0x5245474e4f444553L;
|
||||
private static final int MAXIMUM_SOURCE_TRIALS = 24;
|
||||
private static final int SOURCE_TRIAL_WINDOW = 16;
|
||||
private static final int MAXIMUM_DIAGNOSTICS = 64;
|
||||
|
||||
private final HydrologyPlanner planner;
|
||||
@@ -35,6 +40,11 @@ final class HydrologyRegionalPlanner {
|
||||
private final HydrologyRegionalGraph graph;
|
||||
private final HydrologyRegionalRoute routes;
|
||||
private final Cache<HydrologyTileKey, HydrologyRegionalNetwork> drafts;
|
||||
private final Cache<HydrologyTileKey, HydrologyRegionalCoarsePlan> coarsePlans;
|
||||
private final Map<HydrologyTileKey, HydrologyForkJoin.Task<HydrologyRegionalCoarsePlan>> preparingCoarsePlans = new HashMap<>();
|
||||
private final Map<HydrologyTileKey, HydrologyForkJoin.Task<HydrologyRegionalNetwork>> compilingDrafts = new HashMap<>();
|
||||
private final Object draftPublicationLock = new Object();
|
||||
private long draftEpoch;
|
||||
|
||||
HydrologyRegionalPlanner(HydrologyPlanner planner) {
|
||||
this.planner = planner;
|
||||
@@ -46,6 +56,10 @@ final class HydrologyRegionalPlanner {
|
||||
.maximumWeight(settings.maximumCachedStations())
|
||||
.weigher((HydrologyTileKey key, HydrologyRegionalNetwork network) -> Math.max(minimumWeight, network.stations()))
|
||||
.build();
|
||||
this.coarsePlans = Caffeine.newBuilder()
|
||||
.maximumWeight(settings.maximumCachedStations())
|
||||
.weigher((HydrologyTileKey key, HydrologyRegionalCoarsePlan plan) -> Math.max(minimumWeight, plan.weight()))
|
||||
.build();
|
||||
}
|
||||
|
||||
boolean enabled() {
|
||||
@@ -55,8 +69,14 @@ final class HydrologyRegionalPlanner {
|
||||
}
|
||||
|
||||
void clear() {
|
||||
drafts.invalidateAll();
|
||||
routes.clear();
|
||||
synchronized (draftPublicationLock) {
|
||||
draftEpoch++;
|
||||
compilingDrafts.clear();
|
||||
preparingCoarsePlans.clear();
|
||||
coarsePlans.invalidateAll();
|
||||
drafts.invalidateAll();
|
||||
routes.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample sample(int x, int z) {
|
||||
@@ -73,30 +93,56 @@ final class HydrologyRegionalPlanner {
|
||||
int maximumX = Math.floorDiv(bounds.maximumX() + reach, basinSize);
|
||||
int minimumZ = Math.floorDiv(bounds.minimumZ() - reach, basinSize);
|
||||
int maximumZ = Math.floorDiv(bounds.maximumZ() + reach, basinSize);
|
||||
HydrologyTileKey primary = HydrologyTileKey.fromBlock(
|
||||
(int) (((long) bounds.minimumX() + bounds.maximumX()) / 2L),
|
||||
(int) (((long) bounds.minimumZ() + bounds.maximumZ()) / 2L), basinSize);
|
||||
ArrayList<HydrologyRegionalNetwork> networks = new ArrayList<>();
|
||||
ArrayList<HydrologyTileKey> keys = new ArrayList<>();
|
||||
ArrayList<Callable<HydrologyRegionalNetwork>> tasks = new ArrayList<>();
|
||||
for (int z = minimumZ; z <= maximumZ; z++) {
|
||||
for (int x = minimumX; x <= maximumX; x++) {
|
||||
HydrologyTileKey key = new HydrologyTileKey(x, z);
|
||||
HydrologyRegionalNetwork draft = draft(key);
|
||||
ArrayList<RiverCourse> accepted = new ArrayList<>();
|
||||
for (RiverCourse course : draft.courses()) {
|
||||
if (intersects(course, bounds, footprintRadius()) && admitted(key, course)) {
|
||||
accepted.add(course);
|
||||
}
|
||||
}
|
||||
if (!accepted.isEmpty() || key.equals(primary)) {
|
||||
HydrologyRegionalNetwork selected = select(draft, accepted);
|
||||
networks.add(new HydrologyRegionalNetwork(selected.nodes(), selected.edges(), selected.outlets(),
|
||||
selected.courses(), key.equals(primary) ? draft.diagnostics() : List.of(), selected.cavePlans()));
|
||||
keys.add(key);
|
||||
tasks.add(() -> demandedDraft(key, bounds));
|
||||
}
|
||||
}
|
||||
List<HydrologyRegionalNetwork> loaded = HydrologyForkJoin.invokeAll(tasks,
|
||||
IrisPlatforms.isBound() ? MultiBurst.hydrology : null);
|
||||
ArrayList<HydrologyRegionalNetwork> networks = new ArrayList<>(loaded.size());
|
||||
for (int index = 0; index < loaded.size(); index++) {
|
||||
HydrologyTileKey key = keys.get(index);
|
||||
HydrologyRegionalNetwork draft = loaded.get(index);
|
||||
ArrayList<RiverCourse> accepted = new ArrayList<>();
|
||||
for (RiverCourse course : draft.courses()) {
|
||||
if (intersects(course, bounds, footprintRadius()) && admitted(key, course)) {
|
||||
accepted.add(course);
|
||||
}
|
||||
}
|
||||
if (!accepted.isEmpty()) {
|
||||
HydrologyRegionalNetwork selected = select(draft, accepted);
|
||||
networks.add(new HydrologyRegionalNetwork(selected.nodes(), selected.edges(), selected.outlets(),
|
||||
selected.courses(), List.of(), selected.cavePlans()));
|
||||
}
|
||||
}
|
||||
return merge(networks);
|
||||
}
|
||||
|
||||
List<HydrologyDiagnosticCandidate> diagnosticsIn(SurfaceBounds bounds) {
|
||||
if (!enabled()) {
|
||||
return List.of();
|
||||
}
|
||||
int basinSize = settings.basinSize(planner.settings.routing());
|
||||
HydrologyTileKey primary = HydrologyTileKey.fromBlock(
|
||||
(int) (((long) bounds.minimumX() + bounds.maximumX()) / 2L),
|
||||
(int) (((long) bounds.minimumZ() + bounds.maximumZ()) / 2L), basinSize);
|
||||
return draft(primary).diagnostics();
|
||||
}
|
||||
|
||||
private HydrologyRegionalNetwork demandedDraft(HydrologyTileKey key, SurfaceBounds bounds) {
|
||||
HydrologyRegionalNetwork cached = drafts.getIfPresent(key);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
PreparedCoarse prepared = prepareCoarse(key);
|
||||
return prepared.plan().intersects(bounds) ? draft(key, prepared) : HydrologyRegionalNetwork.EMPTY;
|
||||
}
|
||||
|
||||
RiverFootprint materialize(HydrologyRegionalNetwork network, SurfaceBounds bounds) {
|
||||
if (network.courses().isEmpty()) {
|
||||
return RiverFootprint.empty();
|
||||
@@ -125,24 +171,142 @@ final class HydrologyRegionalPlanner {
|
||||
}
|
||||
|
||||
HydrologyRegionalNetwork draft(HydrologyTileKey key) {
|
||||
return drafts.get(key, this::compile);
|
||||
HydrologyRegionalNetwork cached = drafts.getIfPresent(key);
|
||||
return cached != null ? cached : draft(key, prepareCoarse(key));
|
||||
}
|
||||
|
||||
private HydrologyRegionalNetwork compile(HydrologyTileKey key) {
|
||||
private HydrologyRegionalNetwork draft(HydrologyTileKey key, PreparedCoarse prepared) {
|
||||
HydrologyForkJoin.Task<HydrologyRegionalNetwork> task;
|
||||
synchronized (draftPublicationLock) {
|
||||
boolean current = prepared.epoch() == draftEpoch;
|
||||
HydrologyRegionalNetwork cached = current ? drafts.getIfPresent(key) : null;
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
task = current ? compilingDrafts.get(key) : null;
|
||||
if (task == null) {
|
||||
task = new HydrologyForkJoin.Task<>(() -> compileDraft(key, prepared));
|
||||
if (current) {
|
||||
compilingDrafts.put(key, task);
|
||||
}
|
||||
}
|
||||
}
|
||||
return task.await();
|
||||
}
|
||||
|
||||
private PreparedCoarse prepareCoarse(HydrologyTileKey key) {
|
||||
long epoch;
|
||||
HydrologyForkJoin.Task<HydrologyRegionalCoarsePlan> task;
|
||||
synchronized (draftPublicationLock) {
|
||||
epoch = draftEpoch;
|
||||
HydrologyRegionalCoarsePlan cached = coarsePlans.getIfPresent(key);
|
||||
if (cached != null) {
|
||||
return new PreparedCoarse(epoch, cached);
|
||||
}
|
||||
task = preparingCoarsePlans.get(key);
|
||||
if (task == null) {
|
||||
task = new HydrologyForkJoin.Task<>(() -> compileCoarse(key, epoch));
|
||||
preparingCoarsePlans.put(key, task);
|
||||
}
|
||||
}
|
||||
return new PreparedCoarse(epoch, task.await());
|
||||
}
|
||||
|
||||
private HydrologyRegionalCoarsePlan compileCoarse(HydrologyTileKey key, long epoch) {
|
||||
try {
|
||||
HydrologyRegionalCoarsePlan plan = coarseSetup(key);
|
||||
synchronized (draftPublicationLock) {
|
||||
if (epoch == draftEpoch) {
|
||||
coarsePlans.put(key, plan);
|
||||
}
|
||||
}
|
||||
return plan;
|
||||
} finally {
|
||||
synchronized (draftPublicationLock) {
|
||||
if (epoch == draftEpoch) {
|
||||
preparingCoarsePlans.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private HydrologyRegionalNetwork compileDraft(HydrologyTileKey key, PreparedCoarse prepared) {
|
||||
try {
|
||||
HydrologyRegionalNetwork network = compile(key, prepared.plan());
|
||||
synchronized (draftPublicationLock) {
|
||||
if (prepared.epoch() == draftEpoch) {
|
||||
drafts.put(key, network);
|
||||
}
|
||||
}
|
||||
return network;
|
||||
} finally {
|
||||
synchronized (draftPublicationLock) {
|
||||
if (prepared.epoch() == draftEpoch) {
|
||||
compilingDrafts.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private HydrologyRegionalCoarsePlan coarseSetup(HydrologyTileKey key) {
|
||||
HydrologySampledGrid grid = sample(key);
|
||||
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
|
||||
HydrologyGridNode center = grid.nodes().get(grid.nodes().size() / 2);
|
||||
ArrayList<OutletCandidate> outlets = planner.outletPlanner.oceanOutletCandidates(grid, true);
|
||||
ArrayList<OutletCandidate> outlets = planner.outletPlanner.regionalOceanOutletCandidates(grid);
|
||||
outlets.removeIf(outlet -> outlet.outlet().type() != HydrologyFeatureType.MOUTH);
|
||||
outlets.sort(Comparator.comparingLong(outlet -> HydrologyHash.mix(planner.worldSeed, BASIN_SALT,
|
||||
key.tileX(), key.tileZ(), outlet.outlet().id())));
|
||||
long basinSeed = HydrologyHash.mix(planner.worldSeed, BASIN_SALT, key.tileX(), key.tileZ());
|
||||
boolean coastal = settings.coastalChannels() && HydrologyHash.unit(basinSeed) < settings.coastalChannelChance();
|
||||
if (outlets.isEmpty() || coastal) {
|
||||
return new HydrologyRegionalCoarsePlan(grid, outlets, null, List.of(), List.of(), 0D, coastal, grid.nodes().size());
|
||||
}
|
||||
return routeCoarse(grid, outlets);
|
||||
}
|
||||
|
||||
private HydrologyRegionalCoarsePlan routeCoarse(HydrologySampledGrid grid, List<OutletCandidate> outlets) {
|
||||
ArrayList<OutletCandidate> roots = spacedOutlets(outlets, settings.maximumTrunks() * 2);
|
||||
HydrologyRegionalGraph.Tree tree = graph.route(grid, roots, false);
|
||||
ArrayList<Integer> sources = new ArrayList<>();
|
||||
ArrayList<HydrologyRegionalEnvelope> envelopes = new ArrayList<>();
|
||||
double longest = 0D;
|
||||
for (HydrologyGridNode node : grid.nodes()) {
|
||||
if (!grid.owns(node.x(), node.z()) || tree.root(node.index()) < 0) {
|
||||
continue;
|
||||
}
|
||||
longest = Math.max(longest, tree.length(node.index()));
|
||||
if (tree.length(node.index()) < settings.minimumLength()
|
||||
|| !planner.sourcePlanner.rawSourceEligible(node.terrain(), planner.settings.surface().sources(), true)) {
|
||||
continue;
|
||||
}
|
||||
sources.add(node.index());
|
||||
ArrayList<HydrologyPoint> guide = new ArrayList<>();
|
||||
for (int index : graph.path(tree, node.index())) {
|
||||
guide.add(grid.node(index).naturalPoint());
|
||||
}
|
||||
RiverOutlet outlet = tree.outlets().get(tree.root(node.index())).outlet();
|
||||
guide.add(outlet.landwardPoint());
|
||||
guide.add(outlet.connectionPoint());
|
||||
envelopes.add(HydrologyRegionalEnvelope.of(guide, planner.settings, footprintRadius()));
|
||||
}
|
||||
int weight = Math.addExact(Math.addExact(grid.nodes().size(), tree.selected().length),
|
||||
Math.addExact(Math.ceilDiv(tree.accumulation().length, 16), envelopes.size()));
|
||||
return new HydrologyRegionalCoarsePlan(grid, outlets, tree, sources, envelopes, longest, false, weight);
|
||||
}
|
||||
|
||||
private record PreparedCoarse(long epoch, HydrologyRegionalCoarsePlan plan) {
|
||||
}
|
||||
|
||||
private HydrologyRegionalNetwork compile(HydrologyTileKey key, HydrologyRegionalCoarsePlan coarse) {
|
||||
HydrologySampledGrid grid = coarse.grid();
|
||||
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
|
||||
HydrologyGridNode center = grid.nodes().get(grid.nodes().size() / 2);
|
||||
List<OutletCandidate> outlets = coarse.outlets();
|
||||
if (outlets.isEmpty()) {
|
||||
diagnostic(diagnostics, center.id(), center.naturalPoint(), false, HydrologyCandidateRejection.NO_LEGAL_OUTLET, 0);
|
||||
return new HydrologyRegionalNetwork(List.of(), List.of(), List.of(), List.of(), diagnostics, List.of());
|
||||
}
|
||||
ArrayList<HydrologyRegionalNetwork> accepted = new ArrayList<>();
|
||||
long basinSeed = HydrologyHash.mix(planner.worldSeed, BASIN_SALT, key.tileX(), key.tileZ());
|
||||
if (settings.coastalChannels() && HydrologyHash.unit(basinSeed) < settings.coastalChannelChance()) {
|
||||
if (coarse.coastal()) {
|
||||
ArrayList<HydrologyDiagnosticCandidate> coastalDiagnostics = new ArrayList<>();
|
||||
HydrologyRegionalNetwork coastal = coastalChannel(grid, outlets, basinSeed, coastalDiagnostics);
|
||||
diagnostics.addAll(coastalDiagnostics.subList(0, Math.min(16, coastalDiagnostics.size())));
|
||||
@@ -150,23 +314,11 @@ final class HydrologyRegionalPlanner {
|
||||
accepted.add(coastal);
|
||||
}
|
||||
}
|
||||
ArrayList<OutletCandidate> roots = spacedOutlets(outlets, settings.maximumTrunks() * 2);
|
||||
HydrologyRegionalGraph.Tree tree = graph.route(grid, roots, false);
|
||||
ArrayList<Integer> sources = new ArrayList<>();
|
||||
for (HydrologyGridNode node : grid.nodes()) {
|
||||
if (grid.owns(node.x(), node.z()) && tree.root(node.index()) >= 0
|
||||
&& tree.length(node.index()) >= settings.minimumLength()
|
||||
&& planner.sourcePlanner.rawSourceEligible(node.terrain(), planner.settings.surface().sources(), true)) {
|
||||
sources.add(node.index());
|
||||
}
|
||||
}
|
||||
HydrologyRegionalCoarsePlan routed = coarse.tree() == null ? routeCoarse(grid, outlets) : coarse;
|
||||
HydrologyRegionalGraph.Tree tree = routed.tree();
|
||||
List<Integer> sources = routed.sources();
|
||||
if (sources.isEmpty()) {
|
||||
double longest = 0D;
|
||||
for (HydrologyGridNode node : grid.nodes()) {
|
||||
if (grid.owns(node.x(), node.z()) && tree.root(node.index()) >= 0) {
|
||||
longest = Math.max(longest, tree.length(node.index()));
|
||||
}
|
||||
}
|
||||
double longest = routed.longest();
|
||||
diagnostic(diagnostics, center.id(), center.naturalPoint(), false,
|
||||
longest == 0D ? HydrologyCandidateRejection.NO_DRAINAGE_PATH
|
||||
: longest < settings.minimumLength() ? HydrologyCandidateRejection.COURSE_TOO_SHORT
|
||||
@@ -179,26 +331,70 @@ final class HydrologyRegionalPlanner {
|
||||
}
|
||||
}
|
||||
HydrologyRegionalSourceTrials sourceTrials = new HydrologyRegionalSourceTrials(grid, tree, sources);
|
||||
for (int trials = 0; trials < MAXIMUM_SOURCE_TRIALS; trials++) {
|
||||
if (accepted.size() >= settings.maximumTrunks()) {
|
||||
SourceBuildContext sourceContext = new SourceBuildContext(grid, tree, basinSeed);
|
||||
int trials = 0;
|
||||
while (trials < MAXIMUM_SOURCE_TRIALS && accepted.size() < settings.maximumTrunks()) {
|
||||
int window = Math.min(SOURCE_TRIAL_WINDOW, MAXIMUM_SOURCE_TRIALS - trials);
|
||||
ArrayList<Callable<SourceTrial>> tasks = new ArrayList<>(window);
|
||||
for (int index = 0; index < window; index++) {
|
||||
OptionalInt selected = sourceTrials.next(usedOutlets);
|
||||
if (selected.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
int source = selected.getAsInt();
|
||||
tasks.add(() -> buildSourceTrial(sourceContext, source));
|
||||
}
|
||||
if (tasks.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
OptionalInt selected = sourceTrials.next(usedOutlets);
|
||||
if (selected.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
int source = selected.getAsInt();
|
||||
OutletCandidate outlet = tree.outlets().get(tree.root(source));
|
||||
HydrologyRegionalNetwork candidate = build(grid, tree, source, null, basinSeed, diagnostics);
|
||||
if (!candidate.courses().isEmpty() && !conflicts(candidate.courses().getFirst(), merge(accepted).courses())) {
|
||||
accepted.add(candidate);
|
||||
usedOutlets.add(outlet.outlet().id());
|
||||
List<SourceTrial> completed = HydrologyForkJoin.invokeAll(tasks,
|
||||
IrisPlatforms.isBound() ? MultiBurst.hydrology : null);
|
||||
for (SourceTrial trial : completed) {
|
||||
if (accepted.size() >= settings.maximumTrunks()) {
|
||||
break;
|
||||
}
|
||||
if (usedOutlets.contains(trial.outletId())) {
|
||||
continue;
|
||||
}
|
||||
trials++;
|
||||
if (trial.failure() instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
if (trial.failure() instanceof RuntimeException failure) {
|
||||
throw failure;
|
||||
}
|
||||
int retained = Math.min(MAXIMUM_DIAGNOSTICS - diagnostics.size(), trial.diagnostics().size());
|
||||
diagnostics.addAll(trial.diagnostics().subList(0, retained));
|
||||
HydrologyRegionalNetwork candidate = trial.network();
|
||||
if (!candidate.courses().isEmpty() && !conflicts(candidate.courses().getFirst(), merge(accepted).courses())) {
|
||||
accepted.add(candidate);
|
||||
usedOutlets.add(trial.outletId());
|
||||
}
|
||||
}
|
||||
}
|
||||
HydrologyRegionalNetwork network = merge(accepted);
|
||||
return new HydrologyRegionalNetwork(network.nodes(), network.edges(), network.outlets(), network.courses(), diagnostics, network.cavePlans());
|
||||
}
|
||||
|
||||
private SourceTrial buildSourceTrial(SourceBuildContext context, int source) {
|
||||
long outletId = context.tree().outlets().get(context.tree().root(source)).outlet().id();
|
||||
ArrayList<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>();
|
||||
HydrologyPlanner.PlanningSamples previous = planner.planningSamples.get();
|
||||
planner.planningSamples.set(new HydrologyPlanner.PlanningSamples());
|
||||
try {
|
||||
HydrologyRegionalNetwork network = build(context.grid(), context.tree(), source, null, context.basinSeed(), diagnostics);
|
||||
return new SourceTrial(outletId, network, List.copyOf(diagnostics), null);
|
||||
} catch (RuntimeException | Error failure) {
|
||||
return new SourceTrial(outletId, null, List.of(), failure);
|
||||
} finally {
|
||||
if (previous == null) {
|
||||
planner.planningSamples.remove();
|
||||
} else {
|
||||
planner.planningSamples.set(previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private HydrologyRegionalNetwork coastalChannel(HydrologySampledGrid grid, List<OutletCandidate> outlets,
|
||||
long basinSeed, List<HydrologyDiagnosticCandidate> diagnostics) {
|
||||
int maximum = Math.min(8, outlets.size());
|
||||
@@ -536,29 +732,6 @@ final class HydrologyRegionalPlanner {
|
||||
combined.courses(), combined.cavePlans());
|
||||
}
|
||||
|
||||
HydrologyCaveCourseFilter.Result withoutRegional(HydrologyCaveCourseFilter.Result result, HydrologyTileKey key) {
|
||||
if (!enabled()) {
|
||||
return result;
|
||||
}
|
||||
Set<Long> regionalIds = new HashSet<>();
|
||||
for (RiverCourse course : coursesIn(ownerBounds(key)).courses()) {
|
||||
regionalIds.add(course.id());
|
||||
}
|
||||
ArrayList<RiverCourse> local = new ArrayList<>();
|
||||
for (RiverCourse course : result.courses()) {
|
||||
if (!regionalIds.contains(course.id())) {
|
||||
local.add(course);
|
||||
}
|
||||
}
|
||||
ArrayList<HydrologyCavePlan> plans = new ArrayList<>();
|
||||
for (HydrologyCavePlan plan : result.cavePlans()) {
|
||||
if (!regionalIds.contains(plan.source().sourceId())) {
|
||||
plans.add(plan);
|
||||
}
|
||||
}
|
||||
return new HydrologyCaveCourseFilter.Result(result.nodes(), result.edges(), result.outlets(), local, plans);
|
||||
}
|
||||
|
||||
private int footprintRadius() {
|
||||
return (int) StrictMath.ceil(planner.settings.surface().maximumWidth() * 2D
|
||||
+ Math.max(planner.settings.surface().shoreWidth(), planner.settings.widestShoreBiomeWidth())
|
||||
@@ -749,6 +922,13 @@ final class HydrologyRegionalPlanner {
|
||||
new ArrayList<>(outlets.values()), new ArrayList<>(courses.values()), new ArrayList<>(diagnostics.values()), new ArrayList<>(plans.values()));
|
||||
}
|
||||
|
||||
private record SourceBuildContext(HydrologySampledGrid grid, HydrologyRegionalGraph.Tree tree, long basinSeed) {
|
||||
}
|
||||
|
||||
private record SourceTrial(long outletId, HydrologyRegionalNetwork network,
|
||||
List<HydrologyDiagnosticCandidate> diagnostics, Throwable failure) {
|
||||
}
|
||||
|
||||
private record BuildContext(HydrologyGridNode source, OutletCandidate origin, long basinSeed, RiverOutlet outlet,
|
||||
String profile, HydrologyRegionalFlow flow, HydrologyRegionalMorphology morphology) {
|
||||
}
|
||||
|
||||
+84
-1
@@ -3,14 +3,19 @@ package art.arcane.iris.generation.hydrology;
|
||||
import art.arcane.volmlib.util.noise.SimplexNoise;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ForkJoinWorkerThread;
|
||||
|
||||
final class HydrologyRegionalRoute {
|
||||
private static final double SAMPLE_SPACING = 4D;
|
||||
private static final double SIMPLEX_COORDINATE_SCALE = 100D;
|
||||
private static final int MAXIMUM_RECEIVER_SAMPLES = 65536;
|
||||
private static final int CORRIDOR_SAMPLE_BATCH = 128;
|
||||
private static final int MAXIMUM_CORRIDOR_WORKERS = 8;
|
||||
|
||||
private final HydrologyPlanner planner;
|
||||
private final HydrologyRegionalTerrainRefiner terrainRefiner;
|
||||
@@ -333,6 +338,7 @@ final class HydrologyRegionalRoute {
|
||||
}
|
||||
|
||||
private Refinement validateCorridor(List<HydrologyPoint> points, String profile, boolean coastal) {
|
||||
CorridorSamples samples = new CorridorSamples(points);
|
||||
Set<Long> visited = new HashSet<>();
|
||||
HydrologyTerrainSample previousTerrain = null;
|
||||
HydrologyPoint previousPoint = null;
|
||||
@@ -345,7 +351,7 @@ final class HydrologyRegionalRoute {
|
||||
double progress = steps == 0 ? 0D : step / (double) steps;
|
||||
int x = (int) StrictMath.round(first.x() + (second.x() - first.x()) * progress);
|
||||
int z = (int) StrictMath.round(first.z() + (second.z() - first.z()) * progress);
|
||||
HydrologyTerrainSample terrain = terrainRefiner.sample(x, z);
|
||||
HydrologyTerrainSample terrain = samples.next(index, step, x, z);
|
||||
HydrologyPoint failure = new HydrologyPoint(x, terrain == null ? first.y() : terrain.naturalHeight(), z);
|
||||
if (!visited.add(RiverFootprint.pack(x, z))) {
|
||||
return new Refinement(List.of(), failure, HydrologyCandidateRejection.SURFACE_SHAPE_UNSUPPORTED, 2, null);
|
||||
@@ -568,6 +574,83 @@ final class HydrologyRegionalRoute {
|
||||
+ (-before + 3D * start - 3D * end + after) * progress * progress * progress);
|
||||
}
|
||||
|
||||
private final class CorridorSamples {
|
||||
private final List<HydrologyPoint> points;
|
||||
private final int workers;
|
||||
private final int[] xs;
|
||||
private final int[] zs;
|
||||
private final HydrologyTerrainSample[] terrain;
|
||||
private final RuntimeException[] failures;
|
||||
private int count;
|
||||
private int cursor;
|
||||
|
||||
private CorridorSamples(List<HydrologyPoint> points) {
|
||||
this.points = points;
|
||||
workers = Thread.currentThread() instanceof ForkJoinWorkerThread worker
|
||||
? Math.min(MAXIMUM_CORRIDOR_WORKERS, worker.getPool().getParallelism()) : 1;
|
||||
xs = workers > 1 ? new int[CORRIDOR_SAMPLE_BATCH] : null;
|
||||
zs = workers > 1 ? new int[CORRIDOR_SAMPLE_BATCH] : null;
|
||||
terrain = workers > 1 ? new HydrologyTerrainSample[CORRIDOR_SAMPLE_BATCH] : null;
|
||||
failures = workers > 1 ? new RuntimeException[CORRIDOR_SAMPLE_BATCH] : null;
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample next(int segment, int step, int x, int z) {
|
||||
if (workers == 1) {
|
||||
return terrainRefiner.sample(x, z);
|
||||
}
|
||||
if (cursor == count) {
|
||||
fill(segment, step);
|
||||
}
|
||||
int index = cursor++;
|
||||
if (failures[index] != null) {
|
||||
throw failures[index];
|
||||
}
|
||||
return terrain[index];
|
||||
}
|
||||
|
||||
private void fill(int segment, int firstStep) {
|
||||
count = 0;
|
||||
cursor = 0;
|
||||
Arrays.fill(failures, null);
|
||||
for (int index = segment; index + 1 < points.size() && count < CORRIDOR_SAMPLE_BATCH; index++) {
|
||||
HydrologyPoint first = points.get(index);
|
||||
HydrologyPoint second = points.get(index + 1);
|
||||
int steps = Math.max(Math.abs(second.x() - first.x()), Math.abs(second.z() - first.z()));
|
||||
for (int step = index == segment ? firstStep : 1; step <= steps && count < CORRIDOR_SAMPLE_BATCH; step++) {
|
||||
double progress = steps == 0 ? 0D : step / (double) steps;
|
||||
xs[count] = (int) StrictMath.round(first.x() + (second.x() - first.x()) * progress);
|
||||
zs[count++] = (int) StrictMath.round(first.z() + (second.z() - first.z()) * progress);
|
||||
}
|
||||
}
|
||||
int taskCount = Math.min(workers, Math.ceilDiv(count, 16));
|
||||
if (taskCount < 2) {
|
||||
sampleRange(0, count);
|
||||
return;
|
||||
}
|
||||
int width = Math.ceilDiv(count, taskCount);
|
||||
ArrayList<Callable<Void>> tasks = new ArrayList<>(taskCount);
|
||||
for (int index = 0; index < taskCount; index++) {
|
||||
int start = index * width;
|
||||
int end = Math.min(count, start + width);
|
||||
tasks.add(() -> {
|
||||
sampleRange(start, end);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
HydrologyForkJoin.invokeAll(tasks, null);
|
||||
}
|
||||
|
||||
private void sampleRange(int start, int end) {
|
||||
for (int index = start; index < end; index++) {
|
||||
try {
|
||||
terrain[index] = terrainRefiner.sample(xs[index], zs[index]);
|
||||
} catch (RuntimeException failure) {
|
||||
failures[index] = failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class CandidateSelection {
|
||||
private final CandidateAdmission admission;
|
||||
private final OceanEntry resolved;
|
||||
|
||||
+108
-44
@@ -1,43 +1,54 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.volmlib.util.cache.CacheKey;
|
||||
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.Long2IntOpenHashMap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.PriorityQueue;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
final class HydrologyRegionalTerrainRefiner {
|
||||
private static final int GRID_SPACING = 16;
|
||||
private static final int MAXIMUM_EXPANSIONS = 4096;
|
||||
private static final int MAXIMUM_SAMPLES = 65536;
|
||||
private static final int MAXIMUM_CACHED_SAMPLES = 4194304;
|
||||
private static final int MAXIMUM_SKIPPED_WAYPOINTS = 2;
|
||||
private static final int[][] NEIGHBOURS = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}, {1, 1}, {-1, 1}, {-1, -1}, {1, -1}};
|
||||
|
||||
private final HydrologyPlanner planner;
|
||||
private final Cache<Edge, Reach> edges;
|
||||
private final HydrologyRegionalHydraulics hydraulics;
|
||||
private final Cache<Long, Terrain> terrain;
|
||||
private volatile CacheState caches;
|
||||
|
||||
HydrologyRegionalTerrainRefiner(HydrologyPlanner planner) {
|
||||
this.planner = planner;
|
||||
this.hydraulics = new HydrologyRegionalHydraulics(planner.settings);
|
||||
this.edges = Caffeine.newBuilder().maximumSize(128).build();
|
||||
this.terrain = Caffeine.newBuilder().maximumSize(MAXIMUM_SAMPLES).build();
|
||||
this.caches = new CacheState();
|
||||
}
|
||||
|
||||
void clear() {
|
||||
edges.invalidateAll();
|
||||
terrain.invalidateAll();
|
||||
caches = new CacheState();
|
||||
}
|
||||
|
||||
HydrologyTerrainSample sample(int x, int z) {
|
||||
return terrain.get(RiverFootprint.pack(x, z), key -> new Terrain(planner.naturalSampler == null
|
||||
? planner.sampler.sample(x, z) : planner.naturalSampler.sampleBasisWithoutSlope(x, z))).sample();
|
||||
return sample(caches.terrain, x, z);
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample sample(Cache<Long, Terrain> cache, int x, int z) {
|
||||
long key = CacheKey.mix(RiverFootprint.pack(x, z));
|
||||
Terrain cached = cache.getIfPresent(key);
|
||||
if (cached != null) {
|
||||
return cached.sample();
|
||||
}
|
||||
Terrain sampled = new Terrain(planner.naturalSampler == null
|
||||
? planner.sampler.sample(x, z) : planner.naturalSampler.sampleBasisWithoutSlope(x, z));
|
||||
Terrain existing = cache.asMap().putIfAbsent(key, sampled);
|
||||
return (existing == null ? sampled : existing).sample();
|
||||
}
|
||||
|
||||
List<HydrologyPoint> refine(List<HydrologyPoint> guide, String profile, boolean coastal, HydrologyTerrainSampler receiver) {
|
||||
@@ -61,7 +72,8 @@ final class HydrologyRegionalTerrainRefiner {
|
||||
double length = 0D;
|
||||
ArrayList<HydrologyPoint> refined = new ArrayList<>();
|
||||
refined.add(guide.getFirst());
|
||||
Map<Long, Integer> stations = new HashMap<>();
|
||||
Long2IntOpenHashMap stations = new Long2IntOpenHashMap();
|
||||
stations.defaultReturnValue(-1);
|
||||
stations.put(RiverFootprint.pack(guide.getFirst().x(), guide.getFirst().z()), 0);
|
||||
for (int index = 1; index < guide.size(); index++) {
|
||||
int start = index - 1;
|
||||
@@ -70,7 +82,7 @@ final class HydrologyRegionalTerrainRefiner {
|
||||
for (int candidate = index; candidate < guide.size() && candidate <= index + MAXIMUM_SKIPPED_WAYPOINTS; candidate++) {
|
||||
Edge edge = new Edge(guide.get(start), guide.get(candidate), profile, coastal,
|
||||
requiredHead[candidate], availableHead, terminal);
|
||||
reach = edges.get(edge, this::route);
|
||||
reach = edge(edge);
|
||||
if (!reach.points().isEmpty()) {
|
||||
selected = candidate;
|
||||
break;
|
||||
@@ -88,8 +100,8 @@ final class HydrologyRegionalTerrainRefiner {
|
||||
for (int point = 1; point < points.size(); point++) {
|
||||
HydrologyPoint next = points.get(point);
|
||||
long key = RiverFootprint.pack(next.x(), next.z());
|
||||
Integer previous = stations.get(key);
|
||||
if (previous != null) {
|
||||
int previous = stations.get(key);
|
||||
if (previous >= 0) {
|
||||
while (refined.size() > previous + 1) {
|
||||
HydrologyPoint removed = refined.removeLast();
|
||||
stations.remove(RiverFootprint.pack(removed.x(), removed.z()));
|
||||
@@ -158,7 +170,7 @@ final class HydrologyRegionalTerrainRefiner {
|
||||
|
||||
private List<HydrologyPoint> simplify(List<HydrologyPoint> points, String profile, boolean coastal, Terminal terminal) {
|
||||
double maximumReach = Math.min(512D, planner.settings.routing().regional().sampleSpacing() * 2D);
|
||||
Map<Long, HydrologyTerrainSample> samples = new HashMap<>();
|
||||
Samples samples = new Samples();
|
||||
ArrayList<HydrologyPoint> simplified = new ArrayList<>();
|
||||
int current = 0;
|
||||
int[] requiredHead = requiredHeads(points, terminal, 1);
|
||||
@@ -203,7 +215,7 @@ final class HydrologyRegionalTerrainRefiner {
|
||||
}
|
||||
|
||||
private Reach route(Edge edge) {
|
||||
Map<Long, HydrologyTerrainSample> samples = new HashMap<>();
|
||||
Samples samples = new Samples();
|
||||
Traversal direct = traverse(edge.start(), edge.end(), edge, samples, edge.maximumHead());
|
||||
if (direct.cost() == 0D) {
|
||||
return new Reach(List.of(edge.start(), edge.end()), direct.availableHead());
|
||||
@@ -213,7 +225,7 @@ final class HydrologyRegionalTerrainRefiner {
|
||||
int maximumX = Math.max(edge.start().x(), edge.end().x()) + margin;
|
||||
int minimumZ = Math.min(edge.start().z(), edge.end().z()) - margin;
|
||||
int maximumZ = Math.max(edge.start().z(), edge.end().z()) + margin;
|
||||
Map<Long, List<Node>> visited = new HashMap<>();
|
||||
Long2ObjectOpenHashMap<List<Node>> visited = new Long2ObjectOpenHashMap<>();
|
||||
PriorityQueue<Pending> pending = new PriorityQueue<>(Comparator.comparingDouble(Pending::estimate)
|
||||
.thenComparingLong(Pending::key)
|
||||
.thenComparing(Comparator.comparingInt(Pending::availableHead).reversed()));
|
||||
@@ -249,10 +261,35 @@ final class HydrologyRegionalTerrainRefiner {
|
||||
return new Reach(List.of(), edge.maximumHead());
|
||||
}
|
||||
|
||||
private Reach edge(Edge edge) {
|
||||
CacheState state = caches;
|
||||
Reach cached = state.edges.getIfPresent(edge);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
HydrologyForkJoin.Task<Reach> task = new HydrologyForkJoin.Task<>(() -> {
|
||||
try {
|
||||
Reach present = state.edges.getIfPresent(edge);
|
||||
Reach result = present == null ? route(edge) : present;
|
||||
state.edges.put(edge, result);
|
||||
return result;
|
||||
} finally {
|
||||
state.loading.remove(edge);
|
||||
}
|
||||
});
|
||||
HydrologyForkJoin.Task<Reach> existing = state.loading.putIfAbsent(edge, task);
|
||||
return (existing == null ? task : existing).await();
|
||||
}
|
||||
|
||||
private void offer(Search search, Node current, HydrologyPoint target) {
|
||||
HydrologyTerrainSample terrain = sample(target.x(), target.z(), search.samples());
|
||||
applyOffer(search, current, evaluateOffer(search, current, target));
|
||||
}
|
||||
|
||||
private Offer evaluateOffer(Search search, Node current, HydrologyPoint target) {
|
||||
Samples samples = search.samples();
|
||||
HydrologyTerrainSample terrain = samples.sample(target.x(), target.z());
|
||||
if (!allowed(terrain, search.edge())) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
HydrologyPoint point = new HydrologyPoint(target.x(), terrain.naturalHeight(), target.z());
|
||||
double cost = current.cost() + distance(current.point(), point)
|
||||
@@ -261,30 +298,39 @@ final class HydrologyRegionalTerrainRefiner {
|
||||
long key = RiverFootprint.pack(point.x(), point.z());
|
||||
List<Node> labels = search.visited().get(key);
|
||||
if (dominated(labels, cost, current.availableHead())) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
Traversal traversal = traverse(current.point(), point, search.edge(), search.samples(), current.availableHead());
|
||||
Traversal traversal = traverse(current.point(), point, search.edge(), samples, current.availableHead());
|
||||
cost += traversal.cost();
|
||||
if (!Double.isFinite(cost) || dominated(labels, cost, traversal.availableHead())) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
cost += clearanceCost(point, search.edge(), search.samples());
|
||||
cost += clearanceCost(point, search.edge(), samples);
|
||||
if (dominated(labels, cost, traversal.availableHead())) {
|
||||
return null;
|
||||
}
|
||||
return new Offer(point, cost, traversal.availableHead());
|
||||
}
|
||||
|
||||
private void applyOffer(Search search, Node current, Offer offer) {
|
||||
if (offer == null) {
|
||||
return;
|
||||
}
|
||||
long key = RiverFootprint.pack(offer.point().x(), offer.point().z());
|
||||
List<Node> labels = search.visited().get(key);
|
||||
if (labels == null) {
|
||||
labels = new ArrayList<>(2);
|
||||
search.visited().put(key, labels);
|
||||
}
|
||||
for (int index = labels.size() - 1; index >= 0; index--) {
|
||||
Node previous = labels.get(index);
|
||||
if (cost <= previous.cost() && traversal.availableHead() >= previous.availableHead()) {
|
||||
if (offer.cost() <= previous.cost() && offer.availableHead() >= previous.availableHead()) {
|
||||
labels.remove(index);
|
||||
}
|
||||
}
|
||||
Node retained = new Node(point, cost, traversal.availableHead(), current);
|
||||
Node retained = new Node(offer.point(), offer.cost(), offer.availableHead(), current);
|
||||
labels.add(retained);
|
||||
search.pending().add(new Pending(key, retained, cost + distance(point, search.edge().end())));
|
||||
search.pending().add(new Pending(key, retained, offer.cost() + distance(offer.point(), search.edge().end())));
|
||||
}
|
||||
|
||||
private static boolean dominated(List<Node> labels, double cost, int availableHead) {
|
||||
@@ -309,26 +355,25 @@ final class HydrologyRegionalTerrainRefiner {
|
||||
}
|
||||
|
||||
private Traversal traverse(HydrologyPoint start, HydrologyPoint end, Edge edge,
|
||||
Map<Long, HydrologyTerrainSample> samples, int availableHead) {
|
||||
Samples samples, int availableHead) {
|
||||
int steps = Math.max(Math.abs(end.x() - start.x()), Math.abs(end.z() - start.z()));
|
||||
HydrologyTerrainSample previous = null;
|
||||
double distance = distance(start, end);
|
||||
double tangentX = distance == 0D ? 1D : (end.x() - start.x()) / distance;
|
||||
double tangentZ = distance == 0D ? 0D : (end.z() - start.z()) / distance;
|
||||
HydrologyTerrainSampler bankSampler = (x, z) -> sample(x, z, samples);
|
||||
double cost = 0D;
|
||||
for (int step = 0; step <= steps; step++) {
|
||||
double progress = steps == 0 ? 0D : step / (double) steps;
|
||||
int x = (int) StrictMath.round(start.x() + (end.x() - start.x()) * progress);
|
||||
int z = (int) StrictMath.round(start.z() + (end.z() - start.z()) * progress);
|
||||
HydrologyTerrainSample terrain = sample(x, z, samples);
|
||||
HydrologyTerrainSample terrain = samples.sample(x, z);
|
||||
if (!allowed(terrain, edge) || previous != null && (!previous.drainsInto(terrain)
|
||||
|| edge.coastal() && !terrain.drainsInto(previous))) {
|
||||
return new Traversal(Double.POSITIVE_INFINITY, availableHead);
|
||||
}
|
||||
if (!edge.coastal()) {
|
||||
availableHead = hydraulics.supportedHead(new HydrologyRegionalHydraulics.HeadStation(
|
||||
x, z, tangentX, tangentZ, terrain, availableHead), bankSampler);
|
||||
x, z, tangentX, tangentZ, terrain, availableHead), samples);
|
||||
if (availableHead < Math.max(edge.minimumHead(), minimumHead(terrain, x, z, edge.terminal()))) {
|
||||
return new Traversal(Double.POSITIVE_INFINITY, availableHead);
|
||||
}
|
||||
@@ -356,7 +401,7 @@ final class HydrologyRegionalTerrainRefiner {
|
||||
return Math.max(0D, excess) * Math.max(1D, planner.settings.routing().uphillPenalty());
|
||||
}
|
||||
|
||||
private double clearanceCost(HydrologyPoint point, Edge edge, Map<Long, HydrologyTerrainSample> samples) {
|
||||
private double clearanceCost(HydrologyPoint point, Edge edge, Samples samples) {
|
||||
double radius = planner.settings.surface().maximumWidth() / 2D
|
||||
* (1D + planner.settings.surface().banks().roughness()) + 1D;
|
||||
int blocked = 0;
|
||||
@@ -364,7 +409,7 @@ final class HydrologyRegionalTerrainRefiner {
|
||||
double scale = offset[0] != 0 && offset[1] != 0 ? radius / StrictMath.sqrt(2D) : radius;
|
||||
int x = point.x() + (int) StrictMath.round(offset[0] * scale);
|
||||
int z = point.z() + (int) StrictMath.round(offset[1] * scale);
|
||||
if (!allowed(sample(x, z, samples), edge)) {
|
||||
if (!allowed(samples.sample(x, z), edge)) {
|
||||
blocked++;
|
||||
}
|
||||
}
|
||||
@@ -379,17 +424,6 @@ final class HydrologyRegionalTerrainRefiner {
|
||||
<= planner.settings.routing().regional().maximumCoastalIncision());
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample sample(int x, int z, Map<Long, HydrologyTerrainSample> samples) {
|
||||
long key = RiverFootprint.pack(x, z);
|
||||
HydrologyTerrainSample sampled = samples.get(key);
|
||||
if (sampled != null || samples.containsKey(key) || samples.size() >= MAXIMUM_SAMPLES) {
|
||||
return sampled;
|
||||
}
|
||||
sampled = sample(x, z);
|
||||
samples.put(key, sampled);
|
||||
return sampled;
|
||||
}
|
||||
|
||||
private static List<HydrologyPoint> path(Node current) {
|
||||
ArrayList<HydrologyPoint> points = new ArrayList<>();
|
||||
while (current != null) {
|
||||
@@ -404,6 +438,36 @@ final class HydrologyRegionalTerrainRefiner {
|
||||
return StrictMath.sqrt(first.distanceSquared2D(second));
|
||||
}
|
||||
|
||||
final class Samples implements HydrologyTerrainSampler {
|
||||
private final Long2ObjectOpenHashMap<HydrologyTerrainSample> values = new Long2ObjectOpenHashMap<>();
|
||||
private final Cache<Long, Terrain> terrainCache = caches.terrain;
|
||||
|
||||
@Override
|
||||
public HydrologyTerrainSample sample(int x, int z) {
|
||||
long key = RiverFootprint.pack(x, z);
|
||||
HydrologyTerrainSample sampled = values.get(key);
|
||||
if (sampled != null || values.containsKey(key)) {
|
||||
return sampled;
|
||||
}
|
||||
if (values.size() >= MAXIMUM_SAMPLES) {
|
||||
return null;
|
||||
}
|
||||
sampled = HydrologyRegionalTerrainRefiner.this.sample(terrainCache, x, z);
|
||||
values.put(key, sampled);
|
||||
return sampled;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CacheState {
|
||||
private final Cache<Edge, Reach> edges = Caffeine.newBuilder().maximumSize(128).build();
|
||||
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 record Offer(HydrologyPoint point, double cost, int availableHead) {
|
||||
}
|
||||
|
||||
private record Edge(HydrologyPoint start, HydrologyPoint end, String profile, boolean coastal,
|
||||
int minimumHead, int maximumHead, Terminal terminal) {
|
||||
}
|
||||
@@ -420,7 +484,7 @@ final class HydrologyRegionalTerrainRefiner {
|
||||
private record Terrain(HydrologyTerrainSample sample) {
|
||||
}
|
||||
|
||||
private record Search(Edge edge, Map<Long, HydrologyTerrainSample> samples, Map<Long, List<Node>> visited,
|
||||
private record Search(Edge edge, Samples samples, Long2ObjectOpenHashMap<List<Node>> visited,
|
||||
PriorityQueue<Pending> pending) {
|
||||
}
|
||||
|
||||
|
||||
@@ -1077,7 +1077,6 @@ final class HydrologyRouteGeometry {
|
||||
RouteCandidate start,
|
||||
RouteCandidate end
|
||||
) {
|
||||
int minimumBankDistance = planner.surfaceCourses.surfaceBankDistance(planner.settings.surface().minimumWidth());
|
||||
int maximumBankDistance = planner.surfaceCourses.surfaceBankDistance(planner.settings.surface().maximumWidth());
|
||||
RouteDirection tangent = planner.routePaths.direction(start.point().x(), start.point().z(), end.point().x(), end.point().z());
|
||||
int endpointCeiling = Math.max(start.point().y(), end.point().y());
|
||||
@@ -1103,10 +1102,9 @@ final class HydrologyRouteGeometry {
|
||||
terrain.naturalHeight(),
|
||||
planner.settings.surface().banks().sink()
|
||||
);
|
||||
penalty += planner.surfaceCourses.surfaceRouteBankPenalty(
|
||||
penalty += planner.surfaceCourses.surfaceRouteBankBandPenalty(
|
||||
point,
|
||||
tangent,
|
||||
minimumBankDistance,
|
||||
maximumBankDistance,
|
||||
head,
|
||||
planner.surfaceCourses.maximumSurfaceBankRise(terrain)
|
||||
@@ -1129,10 +1127,9 @@ final class HydrologyRouteGeometry {
|
||||
terrain.naturalHeight(),
|
||||
planner.settings.surface().banks().sink()
|
||||
);
|
||||
penalty += planner.surfaceCourses.surfaceRouteBankPenalty(
|
||||
penalty += planner.surfaceCourses.surfaceRouteBankBandPenalty(
|
||||
point,
|
||||
tangent,
|
||||
minimumBankDistance,
|
||||
maximumBankDistance,
|
||||
head,
|
||||
planner.surfaceCourses.maximumSurfaceBankRise(terrain)
|
||||
|
||||
+118
-9
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
@@ -281,8 +282,10 @@ final class HydrologySourcePlanner {
|
||||
candidates.add(new SourceCandidate(node.index(), stable, score, required));
|
||||
hasRequiredCandidate |= required;
|
||||
}
|
||||
HashMap<Long, Boolean> admissions = new HashMap<>();
|
||||
if (surfaceBudgets != null && surfaceBudgets.overridden()) {
|
||||
return selectLocalSurfaceSources(key, grid, routing, candidates, surfaceBudgets, enforceGlobalSpacing, routingContexts);
|
||||
return selectLocalSurfaceSources(key, grid, routing, candidates, surfaceBudgets, enforceGlobalSpacing,
|
||||
routingContexts, admissions);
|
||||
}
|
||||
int target = expectedCount(sourceSettings.density(), HydrologyHash.mix(
|
||||
planner.worldSeed,
|
||||
@@ -330,7 +333,8 @@ final class HydrologySourcePlanner {
|
||||
sourceSettings,
|
||||
sourceSalt,
|
||||
surface,
|
||||
routingContexts
|
||||
routingContexts,
|
||||
admissions
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -358,7 +362,8 @@ final class HydrologySourcePlanner {
|
||||
List<SourceCandidate> candidates,
|
||||
HydrologySurfaceBudgets budgets,
|
||||
boolean enforceGlobalSpacing,
|
||||
Map<HydrologyTileKey, SourceRoutingContext> routingContexts
|
||||
Map<HydrologyTileKey, SourceRoutingContext> routingContexts,
|
||||
Map<Long, Boolean> admissions
|
||||
) {
|
||||
candidates.sort(Comparator.comparing(SourceCandidate::required).reversed()
|
||||
.thenComparing(Comparator.comparingDouble(SourceCandidate::score).reversed())
|
||||
@@ -404,7 +409,7 @@ final class HydrologySourcePlanner {
|
||||
boolean[] evaluated = new boolean[count];
|
||||
IntPredicate admitted = candidateIndex -> !enforceGlobalSpacing || globallyAdmittedSource(
|
||||
grid.node(candidates.get(candidateIndex).nodeIndex()), planner.settings.surface().sources(),
|
||||
SURFACE_SOURCE_SALT, true, routingContexts);
|
||||
SURFACE_SOURCE_SALT, true, routingContexts, admissions);
|
||||
ArrayList<Integer> selectedIndices = new ArrayList<>(Math.min(count, target));
|
||||
int[] selectedByArea = new int[areas.size()];
|
||||
int[] selectedRequiredByArea = new int[areas.size()];
|
||||
@@ -585,7 +590,73 @@ final class HydrologySourcePlanner {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Greedy min-separation admission over the lattice. A candidate yields its slot only to a higher-priority
|
||||
* neighbour that is itself admitted, so a contender that loses its own contest costs nobody a source; under the
|
||||
* old strict local maximum a doomed contender still suppressed everything beneath it. Contenders outrank the
|
||||
* node they contest, so the walk is acyclic; {@code admissions} memoizes every decision by packed source
|
||||
* coordinate and belongs to one surface or underground selection pass.
|
||||
*/
|
||||
boolean globallyAdmittedSource(
|
||||
HydrologyGridNode candidate,
|
||||
HydrologyPlannerSettings.Source sourceSettings,
|
||||
long sourceSalt,
|
||||
boolean surface,
|
||||
Map<HydrologyTileKey, SourceRoutingContext> routingContexts,
|
||||
Map<Long, Boolean> admissions
|
||||
) {
|
||||
long candidateKey = RiverFootprint.pack(candidate.x(), candidate.z());
|
||||
Boolean decided = admissions.get(candidateKey);
|
||||
if (decided != null) {
|
||||
return decided;
|
||||
}
|
||||
ArrayDeque<SourceContest> contests = new ArrayDeque<>();
|
||||
contests.push(sourceContest(candidate, sourceSettings, sourceSalt, surface, routingContexts));
|
||||
while (!contests.isEmpty()) {
|
||||
SourceContest contest = contests.peek();
|
||||
boolean descended = false;
|
||||
boolean suppressed = false;
|
||||
while (contest.cursor < contest.contenders.size()) {
|
||||
HydrologyGridNode contender = contest.contenders.get(contest.cursor);
|
||||
Boolean contenderAdmitted = admissions.get(RiverFootprint.pack(contender.x(), contender.z()));
|
||||
if (contenderAdmitted == null) {
|
||||
contests.push(sourceContest(contender, sourceSettings, sourceSalt, surface, routingContexts));
|
||||
descended = true;
|
||||
break;
|
||||
}
|
||||
contest.cursor++;
|
||||
if (contenderAdmitted) {
|
||||
suppressed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (descended) {
|
||||
continue;
|
||||
}
|
||||
admissions.put(contest.key, !suppressed);
|
||||
contests.pop();
|
||||
}
|
||||
return admissions.get(candidateKey);
|
||||
}
|
||||
|
||||
private SourceContest sourceContest(
|
||||
HydrologyGridNode node,
|
||||
HydrologyPlannerSettings.Source sourceSettings,
|
||||
long sourceSalt,
|
||||
boolean surface,
|
||||
Map<HydrologyTileKey, SourceRoutingContext> routingContexts
|
||||
) {
|
||||
return new SourceContest(
|
||||
RiverFootprint.pack(node.x(), node.z()),
|
||||
higherPrioritySources(node, sourceSettings, sourceSalt, surface, routingContexts)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Every eligible lattice source inside {@code candidate}'s spacing that outranks it, best first. Required heads
|
||||
* and unspaced underground sources contest nothing and so are admitted outright.
|
||||
*/
|
||||
private List<HydrologyGridNode> higherPrioritySources(
|
||||
HydrologyGridNode candidate,
|
||||
HydrologyPlannerSettings.Source sourceSettings,
|
||||
long sourceSalt,
|
||||
@@ -599,12 +670,13 @@ final class HydrologySourcePlanner {
|
||||
? candidate.terrain().surfaceSourceRequired()
|
||||
: candidate.terrain().undergroundSourceRequired();
|
||||
if ((!surface && minimumSpacing <= 0) || required) {
|
||||
return true;
|
||||
return List.of();
|
||||
}
|
||||
SourcePriority candidatePriority = sourcePriority(candidate.x(), candidate.z(), candidate.terrain(), sourceSalt, surface);
|
||||
int sampleSpacing = planner.settings.routing().sampleSpacing();
|
||||
int searchSpacing = surface ? Math.max(minimumSpacing, planner.settings.maximumSurfaceSourceSpacing()) : minimumSpacing;
|
||||
int latticeRadius = (int) StrictMath.ceil(searchSpacing / (double) sampleSpacing);
|
||||
ArrayList<SourceContender> contenders = new ArrayList<>();
|
||||
for (int offsetZ = -latticeRadius; offsetZ <= latticeRadius; offsetZ++) {
|
||||
for (int offsetX = -latticeRadius; offsetX <= latticeRadius; offsetX++) {
|
||||
if (offsetX == 0 && offsetZ == 0) {
|
||||
@@ -624,8 +696,7 @@ final class HydrologySourcePlanner {
|
||||
}
|
||||
int x = (int) neighborX;
|
||||
int z = (int) neighborZ;
|
||||
HydrologyTileKey owner = HydrologyTileKey.fromBlock(x, z, planner.settings.routing().tileSize());
|
||||
SourceRoutingContext context = routingContexts.computeIfAbsent(owner, planner::sourceRoutingContext);
|
||||
SourceRoutingContext context = sourceContext(x, z, routingContexts);
|
||||
HydrologyGridNode neighbor = context.grid().nodeAtWorld(x, z);
|
||||
if (neighbor == null) {
|
||||
throw new IllegalStateException("Source coordinate is absent from its owner routing lattice.");
|
||||
@@ -645,11 +716,28 @@ final class HydrologySourcePlanner {
|
||||
}
|
||||
SourcePriority neighborPriority = sourcePriority(x, z, terrain, sourceSalt, surface);
|
||||
if (compareSourcePriority(neighborPriority, candidatePriority) > 0) {
|
||||
return false;
|
||||
contenders.add(new SourceContender(neighbor, neighborPriority));
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
// Best first: the strongest contender is the likeliest to hold its own slot and end the walk at depth one.
|
||||
contenders.sort((first, second) -> compareSourcePriority(second.priority(), first.priority()));
|
||||
ArrayList<HydrologyGridNode> ordered = new ArrayList<>(contenders.size());
|
||||
for (SourceContender contender : contenders) {
|
||||
ordered.add(contender.node());
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
|
||||
private SourceRoutingContext sourceContext(
|
||||
int x,
|
||||
int z,
|
||||
Map<HydrologyTileKey, SourceRoutingContext> routingContexts
|
||||
) {
|
||||
return routingContexts.computeIfAbsent(
|
||||
HydrologyTileKey.fromBlock(x, z, planner.settings.routing().tileSize()),
|
||||
planner::sourceRoutingContext
|
||||
);
|
||||
}
|
||||
|
||||
SourceRoutingContext compileSourceRoutingContext(HydrologyTileKey key) {
|
||||
@@ -720,6 +808,12 @@ final class HydrologySourcePlanner {
|
||||
return required || weight > 0D;
|
||||
}
|
||||
|
||||
/**
|
||||
* Terrain only, never the routing plan: route length and potential are measured against the outlets of the
|
||||
* tile that owns the node, so scoring the contest with them ranks neighbours either side of a tile seam
|
||||
* against different outlet sets and the winner flips with ownership. Ordering ends in x then z, so distinct
|
||||
* lattice nodes never tie and the contest graph stays acyclic.
|
||||
*/
|
||||
SourcePriority sourcePriority(
|
||||
int x,
|
||||
int z,
|
||||
@@ -1028,4 +1122,19 @@ final class HydrologySourcePlanner {
|
||||
int z
|
||||
) {
|
||||
}
|
||||
|
||||
private record SourceContender(HydrologyGridNode node, SourcePriority priority) {
|
||||
}
|
||||
|
||||
/** One node's unresolved spacing contest: its contenders, and how many of them already answered. */
|
||||
private static final class SourceContest {
|
||||
private final long key;
|
||||
private final List<HydrologyGridNode> contenders;
|
||||
private int cursor;
|
||||
|
||||
private SourceContest(long key, List<HydrologyGridNode> contenders) {
|
||||
this.key = key;
|
||||
this.contenders = contenders;
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
-6
@@ -653,7 +653,6 @@ final class HydrologySurfaceCoursePlanner {
|
||||
}
|
||||
|
||||
double surfaceRouteCandidateBankPenalty(RouteCandidate candidate) {
|
||||
int minimumBankDistance = surfaceBankDistance(planner.settings.surface().minimumWidth());
|
||||
int maximumBankDistance = surfaceBankDistance(planner.settings.surface().maximumWidth());
|
||||
HydrologyPoint point = candidate.point();
|
||||
HydrologyTerrainSample terrain = planner.sampleLandBasis(point.x(), point.z());
|
||||
@@ -667,10 +666,9 @@ final class HydrologySurfaceCoursePlanner {
|
||||
terrain.naturalHeight(),
|
||||
planner.settings.surface().banks().sink()
|
||||
);
|
||||
return surfaceRouteBankPenalty(
|
||||
return surfaceRouteBankBandPenalty(
|
||||
point,
|
||||
candidate.tangent(),
|
||||
minimumBankDistance,
|
||||
maximumBankDistance,
|
||||
head,
|
||||
maximumSurfaceBankRise(terrain)
|
||||
@@ -684,16 +682,16 @@ final class HydrologySurfaceCoursePlanner {
|
||||
);
|
||||
}
|
||||
|
||||
double surfaceRouteBankPenalty(
|
||||
double surfaceRouteBankBandPenalty(
|
||||
HydrologyPoint point,
|
||||
RouteDirection tangent,
|
||||
int minimumDistance,
|
||||
int maximumDistance,
|
||||
int head,
|
||||
int maximumBankRise
|
||||
) {
|
||||
int innerDistance = surfaceBankInnerDistance();
|
||||
double penalty = 0D;
|
||||
for (int distance = minimumDistance; distance <= maximumDistance; distance++) {
|
||||
for (int distance = innerDistance; distance <= maximumDistance; distance++) {
|
||||
penalty += surfaceRouteBankPenalty(point, tangent, distance, head, maximumBankRise);
|
||||
penalty += surfaceRouteBankPenalty(point, tangent, -distance, head, maximumBankRise);
|
||||
}
|
||||
@@ -720,6 +718,25 @@ final class HydrologySurfaceCoursePlanner {
|
||||
return (double) excess * excess * 128D;
|
||||
}
|
||||
|
||||
/**
|
||||
* Innermost probed ring, in blocks out from the centerline. It reads {@code surface.maximumWidth()} and
|
||||
* nothing else - no {@code shoreWidth}, no candidate, no terrain - so raising the shore knob widens the
|
||||
* probe band instead of sliding it. That is the whole guarantee.
|
||||
*
|
||||
* <p>The ring sits one block outside half the nominal maximum width, which is the widest channel the
|
||||
* width sampler will draw, not the widest channel that gets cut: {@code ChannelProfileBuilder} clamps a
|
||||
* station to {@code maximumWidth * 2} after the terrain width multiplier, then scales it again by
|
||||
* {@code banks().springWidthRatio()} over the spring reach and {@code banks().mouthFlareRatio()} through
|
||||
* the inlet flare. Routing runs before any profile exists, so no ring can promise to clear the channel.
|
||||
*/
|
||||
int surfaceBankInnerDistance() {
|
||||
return (int) StrictMath.ceil(planner.settings.surface().maximumWidth() / 2D) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Outermost probed ring: where the bank of the widest channel is expected once the shore is cut. It
|
||||
* gains a ring per block of {@code shoreWidth}, which is what widens the band walked by the penalty.
|
||||
*/
|
||||
int surfaceBankDistance(int width) {
|
||||
int channelRadius = (int) StrictMath.ceil(width / 2D);
|
||||
return Math.max(
|
||||
|
||||
@@ -26,8 +26,9 @@ public final class HydrologyTile {
|
||||
private final List<DrainageEdge> edges;
|
||||
private final List<RiverOutlet> outlets;
|
||||
private final List<RiverCourse> courses;
|
||||
private final Set<Long> regionalCourseIds;
|
||||
private final List<HydrologyCavePlan> cavePlans;
|
||||
private final List<HydrologyDiagnosticCandidate> diagnosticCandidates;
|
||||
private final List<HydrologyDiagnosticCandidate> localDiagnosticCandidates;
|
||||
private final RiverFootprint footprint;
|
||||
private final Map<Long, DrainageNode> nodesById;
|
||||
private final Map<Long, RiverOutlet> outletsById;
|
||||
@@ -43,8 +44,9 @@ public final class HydrologyTile {
|
||||
List<DrainageEdge> edges,
|
||||
List<RiverOutlet> outlets,
|
||||
List<RiverCourse> courses,
|
||||
Set<Long> regionalCourseIds,
|
||||
List<HydrologyCavePlan> cavePlans,
|
||||
List<HydrologyDiagnosticCandidate> diagnosticCandidates,
|
||||
List<HydrologyDiagnosticCandidate> localDiagnosticCandidates,
|
||||
RiverFootprint footprint
|
||||
) {
|
||||
this.key = Objects.requireNonNull(key, "key");
|
||||
@@ -58,12 +60,13 @@ public final class HydrologyTile {
|
||||
this.edges = sortedCopy(edges, Comparator.comparingLong(DrainageEdge::id));
|
||||
this.outlets = sortedCopy(outlets, Comparator.comparingLong(RiverOutlet::id));
|
||||
this.courses = sortedCopy(courses, Comparator.comparingLong(RiverCourse::id));
|
||||
this.regionalCourseIds = Set.copyOf(Objects.requireNonNull(regionalCourseIds, "regionalCourseIds"));
|
||||
this.cavePlans = sortedCopy(
|
||||
cavePlans,
|
||||
Comparator.comparingLong((HydrologyCavePlan plan) -> plan.source().sourceId())
|
||||
);
|
||||
this.diagnosticCandidates = sortedCopy(
|
||||
diagnosticCandidates,
|
||||
this.localDiagnosticCandidates = sortedCopy(
|
||||
localDiagnosticCandidates,
|
||||
Comparator.comparingLong(HydrologyDiagnosticCandidate::id)
|
||||
);
|
||||
this.footprint = Objects.requireNonNull(footprint, "footprint");
|
||||
@@ -107,12 +110,16 @@ public final class HydrologyTile {
|
||||
return courses;
|
||||
}
|
||||
|
||||
public Set<Long> regionalCourseIds() {
|
||||
return regionalCourseIds;
|
||||
}
|
||||
|
||||
public List<HydrologyCavePlan> cavePlans() {
|
||||
return cavePlans;
|
||||
}
|
||||
|
||||
public List<HydrologyDiagnosticCandidate> diagnosticCandidates() {
|
||||
return diagnosticCandidates;
|
||||
public List<HydrologyDiagnosticCandidate> localDiagnosticCandidates() {
|
||||
return localDiagnosticCandidates;
|
||||
}
|
||||
|
||||
public RiverFootprint footprint() {
|
||||
@@ -139,13 +146,13 @@ public final class HydrologyTile {
|
||||
return footprint.renderSample(x, z);
|
||||
}
|
||||
|
||||
public HydrologyDiagnosticRenderSample diagnosticRenderAt(int x, int z, int maximumDistance) {
|
||||
public HydrologyDiagnosticRenderSample localDiagnosticRenderAt(int x, int z, int maximumDistance) {
|
||||
if (maximumDistance < 0) {
|
||||
throw new IllegalArgumentException("maximumDistance cannot be negative.");
|
||||
}
|
||||
long maximumDistanceSquared = (long) maximumDistance * maximumDistance;
|
||||
ArrayList<HydrologyDiagnosticCandidate> selected = new ArrayList<>();
|
||||
for (HydrologyDiagnosticCandidate candidate : diagnosticCandidates) {
|
||||
for (HydrologyDiagnosticCandidate candidate : localDiagnosticCandidates) {
|
||||
long distance = candidate.point().distanceSquared2D(new HydrologyPoint(x, candidate.point().y(), z));
|
||||
if (distance <= maximumDistanceSquared) {
|
||||
selected.add(candidate);
|
||||
@@ -266,6 +273,12 @@ public final class HydrologyTile {
|
||||
throw new IllegalArgumentException("Every cave-bearing course must have one accepted containment plan.");
|
||||
}
|
||||
}
|
||||
for (long courseId : regionalCourseIds) {
|
||||
RiverCourse course = coursesById.get(courseId);
|
||||
if (course == null || course.type() != RiverCourseType.SURFACE) {
|
||||
throw new IllegalArgumentException("Regional ownership must reference an accepted surface course.");
|
||||
}
|
||||
}
|
||||
validateFootprintCourses(coursesById);
|
||||
}
|
||||
|
||||
@@ -614,8 +627,9 @@ public final class HydrologyTile {
|
||||
&& edges.equals(tile.edges)
|
||||
&& outlets.equals(tile.outlets)
|
||||
&& courses.equals(tile.courses)
|
||||
&& regionalCourseIds.equals(tile.regionalCourseIds)
|
||||
&& cavePlans.equals(tile.cavePlans)
|
||||
&& diagnosticCandidates.equals(tile.diagnosticCandidates)
|
||||
&& localDiagnosticCandidates.equals(tile.localDiagnosticCandidates)
|
||||
&& footprint.equals(tile.footprint);
|
||||
}
|
||||
|
||||
@@ -630,8 +644,9 @@ public final class HydrologyTile {
|
||||
edges,
|
||||
outlets,
|
||||
courses,
|
||||
regionalCourseIds,
|
||||
cavePlans,
|
||||
diagnosticCandidates,
|
||||
localDiagnosticCandidates,
|
||||
footprint
|
||||
);
|
||||
}
|
||||
@@ -639,6 +654,6 @@ public final class HydrologyTile {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HydrologyTile[key=" + key + ", courses=" + courses.size() + ", candidates="
|
||||
+ diagnosticCandidates.size() + ", columns=" + footprint.size() + "]";
|
||||
+ localDiagnosticCandidates.size() + ", columns=" + footprint.size() + "]";
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
private final int maximumEntries;
|
||||
private final Cache<HydrologyTileKey, HydrologyTile> tiles;
|
||||
private final Cache<Long, ChunkColumns> composedChunks;
|
||||
private final Cache<HydrologyTileKey, List<HydrologyDiagnosticCandidate>> diagnostics;
|
||||
private final AtomicLong cacheEpoch;
|
||||
private final AtomicBoolean closed;
|
||||
private final ThreadLocal<LocalChunkColumns> localChunkColumns;
|
||||
@@ -55,11 +56,14 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
private final ConcurrentHashMap<CacheLoadKey<HydrologyTileKey>, CompletableFuture<HydrologyTile>> planning;
|
||||
private final ConcurrentHashMap<CacheLoadKey<HydrologyTileKey>, PendingLoad<HydrologyTile>> loading;
|
||||
private final ConcurrentHashMap<CacheLoadKey<Long>, PendingLoad<ChunkColumns>> composing;
|
||||
private final ConcurrentHashMap<CacheLoadKey<HydrologyTileKey>, PendingLoad<List<HydrologyDiagnosticCandidate>>> diagnosing;
|
||||
private final ConcurrentHashMap<CacheLoadKey<HydrologyTileKey>, PendingLoad<List<HydrologyDiagnosticCandidate>>> loadingDiagnostics;
|
||||
private final Object publicationLock = new Object();
|
||||
private final BooleanSupplier waitingForbidden;
|
||||
private volatile SharedCacheScope sharedCacheScope;
|
||||
private volatile StudioHydrologyTileStore persistentStore;
|
||||
private volatile boolean neighbourPrefetchEnabled;
|
||||
private volatile Runnable terrainPreparation;
|
||||
private int demandBatches;
|
||||
|
||||
public HydrologyTileCache(HydrologyPlanner planner) {
|
||||
@@ -105,6 +109,8 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
this.planning = new ConcurrentHashMap<>();
|
||||
this.loading = new ConcurrentHashMap<>();
|
||||
this.composing = new ConcurrentHashMap<>();
|
||||
this.diagnosing = new ConcurrentHashMap<>();
|
||||
this.loadingDiagnostics = new ConcurrentHashMap<>();
|
||||
if (maximumEntries < 1) {
|
||||
throw new IllegalArgumentException("maximumEntries must be positive.");
|
||||
}
|
||||
@@ -119,6 +125,9 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
this.composedChunks = Caffeine.newBuilder()
|
||||
.maximumSize(MAXIMUM_COMPOSED_CHUNKS)
|
||||
.build();
|
||||
this.diagnostics = Caffeine.newBuilder()
|
||||
.maximumSize(maximumEntries)
|
||||
.build();
|
||||
this.cacheEpoch = new AtomicLong();
|
||||
this.closed = new AtomicBoolean();
|
||||
this.localChunkColumns = new ThreadLocal<>();
|
||||
@@ -142,6 +151,123 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
return awaitPlan(planDemandAsync(key));
|
||||
}
|
||||
|
||||
public List<HydrologyDiagnosticCandidate> diagnosticCandidates(HydrologyTileKey key) {
|
||||
Objects.requireNonNull(key, "key");
|
||||
requireOpen();
|
||||
List<HydrologyDiagnosticCandidate> present = diagnostics.getIfPresent(key);
|
||||
if (present != null) {
|
||||
return present;
|
||||
}
|
||||
if (waitsForbidden()) {
|
||||
throw new IllegalStateException("Cold hydrology diagnostics cannot run on a thread that may not wait.");
|
||||
}
|
||||
long epoch = beginDemandBatch();
|
||||
if (prefetchExecutor == null || plansInline()) {
|
||||
try {
|
||||
return loadDiagnostics(new CacheLoadKey<>(epoch, key));
|
||||
} finally {
|
||||
finishDemandBatch(epoch);
|
||||
}
|
||||
}
|
||||
CompletableFuture<List<HydrologyDiagnosticCandidate>> future;
|
||||
try {
|
||||
future = diagnoseAsync(new CacheLoadKey<>(epoch, key));
|
||||
} catch (Throwable failure) {
|
||||
finishDemandBatch(epoch);
|
||||
throw failure;
|
||||
}
|
||||
future.whenComplete((result, failure) -> finishDemandBatch(epoch));
|
||||
return awaitPlan(future);
|
||||
}
|
||||
|
||||
private List<HydrologyDiagnosticCandidate> loadDiagnostics(CacheLoadKey<HydrologyTileKey> loadKey) {
|
||||
HydrologyTileKey key = loadKey.key();
|
||||
List<HydrologyDiagnosticCandidate> present = diagnostics.getIfPresent(key);
|
||||
if (present != null) {
|
||||
return present;
|
||||
}
|
||||
PendingLoad<List<HydrologyDiagnosticCandidate>> owned =
|
||||
new PendingLoad<>(Thread.currentThread(), new CompletableFuture<>());
|
||||
PendingLoad<List<HydrologyDiagnosticCandidate>> existing;
|
||||
synchronized (publicationLock) {
|
||||
requireOpen();
|
||||
existing = loadingDiagnostics.putIfAbsent(loadKey, owned);
|
||||
}
|
||||
if (existing != null) {
|
||||
return awaitLoad(existing);
|
||||
}
|
||||
try {
|
||||
present = diagnostics.getIfPresent(key);
|
||||
if (present == null) {
|
||||
prepareTerrain();
|
||||
}
|
||||
List<HydrologyDiagnosticCandidate> result = present == null
|
||||
? List.copyOf(planner.diagnosticCandidates(planInline(key, loadKey.epoch()))) : present;
|
||||
synchronized (publicationLock) {
|
||||
if (!closed.get() && cacheEpoch.get() == loadKey.epoch()) {
|
||||
diagnostics.put(key, result);
|
||||
}
|
||||
}
|
||||
owned.future().complete(result);
|
||||
return result;
|
||||
} catch (Throwable failure) {
|
||||
owned.future().completeExceptionally(failure);
|
||||
throw failure;
|
||||
} finally {
|
||||
loadingDiagnostics.remove(loadKey, owned);
|
||||
}
|
||||
}
|
||||
|
||||
private CompletableFuture<List<HydrologyDiagnosticCandidate>> diagnoseAsync(CacheLoadKey<HydrologyTileKey> loadKey) {
|
||||
List<HydrologyDiagnosticCandidate> present = diagnostics.getIfPresent(loadKey.key());
|
||||
if (present != null) {
|
||||
return CompletableFuture.completedFuture(present);
|
||||
}
|
||||
PendingLoad<List<HydrologyDiagnosticCandidate>> queued = new PendingLoad<>(null, new CompletableFuture<>());
|
||||
PendingLoad<List<HydrologyDiagnosticCandidate>> existing;
|
||||
synchronized (publicationLock) {
|
||||
if (closed.get()) {
|
||||
return CompletableFuture.failedFuture(new CancellationException("Hydrology tile cache is closed."));
|
||||
}
|
||||
existing = diagnosing.putIfAbsent(loadKey, queued);
|
||||
}
|
||||
if (existing != null) {
|
||||
return existing.future();
|
||||
}
|
||||
try {
|
||||
prefetchExecutor.execute(() -> runDiagnostics(loadKey, queued));
|
||||
} catch (RuntimeException | Error rejected) {
|
||||
diagnosing.remove(loadKey, queued);
|
||||
queued.future().completeExceptionally(rejected);
|
||||
}
|
||||
return queued.future();
|
||||
}
|
||||
|
||||
private void runDiagnostics(CacheLoadKey<HydrologyTileKey> loadKey,
|
||||
PendingLoad<List<HydrologyDiagnosticCandidate>> queued) {
|
||||
PendingLoad<List<HydrologyDiagnosticCandidate>> active =
|
||||
new PendingLoad<>(Thread.currentThread(), queued.future());
|
||||
boolean started;
|
||||
synchronized (publicationLock) {
|
||||
started = !closed.get() && diagnosing.replace(loadKey, queued, active);
|
||||
}
|
||||
if (!started) {
|
||||
queued.future().completeExceptionally(new CancellationException("Hydrology tile cache is closed."));
|
||||
diagnosing.remove(loadKey, queued);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
active.future().complete(loadDiagnostics(loadKey));
|
||||
} catch (Throwable failure) {
|
||||
active.future().completeExceptionally(failure);
|
||||
if (!(failure instanceof CancellationException) || !closed.get()) {
|
||||
IrisLogging.reportError(failure);
|
||||
}
|
||||
} finally {
|
||||
diagnosing.remove(loadKey, active);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean plansInline() {
|
||||
if (prefetchExecutor instanceof MultiBurst burst) {
|
||||
return burst.ownsCurrentThread();
|
||||
@@ -212,6 +338,7 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
return persisted;
|
||||
}
|
||||
}
|
||||
prepareTerrain();
|
||||
try {
|
||||
HydrologyTile planned = planner.plan(key);
|
||||
synchronized (publicationLock) {
|
||||
@@ -352,6 +479,7 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
planner.clearOwnerDrafts();
|
||||
tiles.invalidateAll();
|
||||
composedChunks.invalidateAll();
|
||||
diagnostics.invalidateAll();
|
||||
}
|
||||
prefetchQueue.clear();
|
||||
queuedPrefetches.clear();
|
||||
@@ -412,7 +540,7 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
@Override
|
||||
public void close() {
|
||||
ArrayList<CompletableFuture<?>> active = new ArrayList<>();
|
||||
ArrayList<CompletableFuture<HydrologyTile>> queued = new ArrayList<>();
|
||||
ArrayList<CompletableFuture<?>> queued = new ArrayList<>();
|
||||
synchronized (publicationLock) {
|
||||
for (PendingLoad<HydrologyTile> load : loading.values()) {
|
||||
collectActiveLoad(active, load);
|
||||
@@ -420,6 +548,16 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
for (PendingLoad<ChunkColumns> load : composing.values()) {
|
||||
collectActiveLoad(active, load);
|
||||
}
|
||||
for (PendingLoad<List<HydrologyDiagnosticCandidate>> load : loadingDiagnostics.values()) {
|
||||
collectActiveLoad(active, load);
|
||||
}
|
||||
for (PendingLoad<List<HydrologyDiagnosticCandidate>> load : diagnosing.values()) {
|
||||
if (load.owner() == null) {
|
||||
queued.add(load.future());
|
||||
} else {
|
||||
collectActiveLoad(active, load);
|
||||
}
|
||||
}
|
||||
closed.set(true);
|
||||
planning.forEach((key, future) -> {
|
||||
if (!loading.containsKey(key)) {
|
||||
@@ -427,16 +565,28 @@ public final class HydrologyTileCache implements AutoCloseable {
|
||||
}
|
||||
});
|
||||
}
|
||||
for (CompletableFuture<HydrologyTile> future : queued) {
|
||||
for (CompletableFuture<?> future : queued) {
|
||||
future.completeExceptionally(new CancellationException("Hydrology tile cache is closed."));
|
||||
}
|
||||
CompletableFuture.allOf(active.toArray(CompletableFuture<?>[]::new))
|
||||
.handle((ignored, failure) -> null)
|
||||
.join();
|
||||
planning.clear();
|
||||
diagnosing.clear();
|
||||
clear();
|
||||
}
|
||||
|
||||
public void setTerrainPreparation(Runnable preparation) {
|
||||
terrainPreparation = Objects.requireNonNull(preparation, "terrainPreparation");
|
||||
}
|
||||
|
||||
private void prepareTerrain() {
|
||||
Runnable preparation = terrainPreparation;
|
||||
if (preparation != null) {
|
||||
preparation.run();
|
||||
}
|
||||
}
|
||||
|
||||
private static void collectActiveLoad(List<CompletableFuture<?>> active, PendingLoad<?> load) {
|
||||
if (load.owner() == Thread.currentThread()) {
|
||||
throw new IllegalStateException("Hydrology tile cache cannot close from an active load.");
|
||||
|
||||
+12
-5
@@ -37,11 +37,12 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.Set;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
final class StudioHydrologyTileStore {
|
||||
private static final int SCHEMA_VERSION = 3;
|
||||
private static final int SCHEMA_VERSION = 5;
|
||||
private static final long MAXIMUM_COMPRESSED_BYTES = 128L * 1024L * 1024L;
|
||||
private static final long MAXIMUM_DECOMPRESSED_BYTES = 512L * 1024L * 1024L;
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
@@ -168,8 +169,9 @@ final class StudioHydrologyTileStore {
|
||||
List<DrainageEdge> edges,
|
||||
List<RiverOutlet> outlets,
|
||||
List<PersistedCourse> courses,
|
||||
List<Long> regionalCourseIds,
|
||||
List<PersistedCavePlan> cavePlans,
|
||||
List<HydrologyDiagnosticCandidate> diagnosticCandidates,
|
||||
List<HydrologyDiagnosticCandidate> localDiagnosticCandidates,
|
||||
List<HydrologyColumnSample> columns
|
||||
) {
|
||||
private static PersistedTile from(HydrologyTile tile) {
|
||||
@@ -177,6 +179,8 @@ final class StudioHydrologyTileStore {
|
||||
for (RiverCourse course : tile.courses()) {
|
||||
courses.add(PersistedCourse.from(course));
|
||||
}
|
||||
ArrayList<Long> regionalCourseIds = new ArrayList<>(tile.regionalCourseIds());
|
||||
regionalCourseIds.sort(Long::compareTo);
|
||||
ArrayList<PersistedCavePlan> cavePlans = new ArrayList<>(tile.cavePlans().size());
|
||||
for (HydrologyCavePlan cavePlan : tile.cavePlans()) {
|
||||
cavePlans.add(PersistedCavePlan.from(cavePlan));
|
||||
@@ -191,8 +195,9 @@ final class StudioHydrologyTileStore {
|
||||
tile.edges(),
|
||||
tile.outlets(),
|
||||
courses,
|
||||
regionalCourseIds,
|
||||
cavePlans,
|
||||
tile.diagnosticCandidates(),
|
||||
tile.localDiagnosticCandidates(),
|
||||
new ArrayList<>(tile.footprint().columns().values())
|
||||
);
|
||||
}
|
||||
@@ -202,8 +207,9 @@ final class StudioHydrologyTileStore {
|
||||
requireCollection(edges, "edges");
|
||||
requireCollection(outlets, "outlets");
|
||||
requireCollection(courses, "courses");
|
||||
requireCollection(regionalCourseIds, "regionalCourseIds");
|
||||
requireCollection(cavePlans, "cavePlans");
|
||||
requireCollection(diagnosticCandidates, "diagnosticCandidates");
|
||||
requireCollection(localDiagnosticCandidates, "localDiagnosticCandidates");
|
||||
requireCollection(columns, "columns");
|
||||
ArrayList<RiverCourse> restoredCourses = new ArrayList<>(courses.size());
|
||||
for (PersistedCourse course : courses) {
|
||||
@@ -231,8 +237,9 @@ final class StudioHydrologyTileStore {
|
||||
edges,
|
||||
outlets,
|
||||
restoredCourses,
|
||||
Set.copyOf(regionalCourseIds),
|
||||
restoredCavePlans,
|
||||
diagnosticCandidates,
|
||||
localDiagnosticCandidates,
|
||||
new RiverFootprint(restoredColumns)
|
||||
);
|
||||
}
|
||||
|
||||
+99
-62
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.generation.hydrology.runtime;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.volmlib.util.cache.CacheKey;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyNaturalTerrainSampler;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyRoutingTerrainSampler;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTerrainSample;
|
||||
@@ -18,17 +19,15 @@ import java.util.concurrent.Executor;
|
||||
final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrainSampler, AutoCloseable {
|
||||
private static final int MINIMUM_PARALLEL_BASIS_SAMPLES = 64;
|
||||
private static final int COORDINATE_MIX = 0x9E3779B9;
|
||||
private static final int CACHE_STRIPES = 16;
|
||||
private static final int MINIMUM_STRIPE_ENTRIES = 256;
|
||||
|
||||
private final BasisProvider basisProvider;
|
||||
private final IrisHydrologyNaturalHeightProvider heightProvider;
|
||||
private final IrisHydrologyNaturalOceanClassifier oceanClassifier;
|
||||
private final int seaLevel;
|
||||
private final int maximumEntries;
|
||||
private final SamplingOptions samplingOptions;
|
||||
private final Object lock;
|
||||
private final Long2ObjectLinkedOpenHashMap<TerrainBasis> bases;
|
||||
private final Long2DoubleLinkedOpenHashMap naturalHeights;
|
||||
private final Long2ObjectLinkedOpenHashMap<NaturalClassification> oceanClassifications;
|
||||
private final CacheStripe[] stripes;
|
||||
|
||||
IrisHydrologyRoutingTerrainSampler(Sources sources, SamplingOptions samplingOptions) {
|
||||
Objects.requireNonNull(sources, "sources");
|
||||
@@ -38,12 +37,12 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
this.oceanClassifier = Objects.requireNonNull(sources.oceanClassifier(), "oceanClassifier");
|
||||
this.seaLevel = sources.seaLevel();
|
||||
int maximumEntries = samplingOptions.maximumEntries();
|
||||
this.maximumEntries = maximumEntries;
|
||||
this.lock = new Object();
|
||||
this.bases = new Long2ObjectLinkedOpenHashMap<>(maximumEntries);
|
||||
this.naturalHeights = new Long2DoubleLinkedOpenHashMap(maximumEntries);
|
||||
this.naturalHeights.defaultReturnValue(Double.NaN);
|
||||
this.oceanClassifications = new Long2ObjectLinkedOpenHashMap<>(maximumEntries);
|
||||
int stripeCount = Math.min(CACHE_STRIPES, Integer.highestOneBit(Math.max(1, maximumEntries / MINIMUM_STRIPE_ENTRIES)));
|
||||
this.stripes = new CacheStripe[stripeCount];
|
||||
for (int index = 0; index < stripeCount; index++) {
|
||||
int capacity = maximumEntries / stripeCount + (index < maximumEntries % stripeCount ? 1 : 0);
|
||||
stripes[index] = new CacheStripe(capacity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -78,19 +77,20 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
@Override
|
||||
public NaturalClassification classifyNatural(int blockX, int blockZ) {
|
||||
long packed = pack(blockX, blockZ);
|
||||
synchronized (lock) {
|
||||
TerrainBasis basis = bases.getAndMoveToLast(packed);
|
||||
CacheStripe stripe = stripe(packed);
|
||||
synchronized (stripe) {
|
||||
TerrainBasis basis = stripe.bases.getAndMoveToLast(packed);
|
||||
if (basis != null) {
|
||||
return basis.terrain().ocean() ? NaturalClassification.OCEAN : NaturalClassification.LAND;
|
||||
}
|
||||
NaturalClassification cached = oceanClassifications.getAndMoveToLast(packed);
|
||||
NaturalClassification cached = stripe.oceanClassifications.getAndMoveToLast(packed);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
NaturalClassification sampled = NaturalClassification.LAND;
|
||||
if (oceanClassifier.isOcean(blockX, blockZ)) {
|
||||
double height = naturalHeight(packed, blockX, blockZ);
|
||||
double height = naturalHeight(stripe, packed, blockX, blockZ);
|
||||
if (!Double.isFinite(height)) {
|
||||
return NaturalClassification.UNAVAILABLE;
|
||||
}
|
||||
@@ -98,17 +98,17 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
sampled = NaturalClassification.OCEAN;
|
||||
}
|
||||
}
|
||||
synchronized (lock) {
|
||||
TerrainBasis basis = bases.getAndMoveToLast(packed);
|
||||
synchronized (stripe) {
|
||||
TerrainBasis basis = stripe.bases.getAndMoveToLast(packed);
|
||||
if (basis != null) {
|
||||
return basis.terrain().ocean() ? NaturalClassification.OCEAN : NaturalClassification.LAND;
|
||||
}
|
||||
NaturalClassification existing = oceanClassifications.getAndMoveToLast(packed);
|
||||
NaturalClassification existing = stripe.oceanClassifications.getAndMoveToLast(packed);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
oceanClassifications.putAndMoveToLast(packed, sampled);
|
||||
evictOldest(oceanClassifications);
|
||||
stripe.oceanClassifications.putAndMoveToLast(packed, sampled);
|
||||
stripe.evictOldest(stripe.oceanClassifications);
|
||||
}
|
||||
return sampled;
|
||||
}
|
||||
@@ -127,55 +127,70 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
|
||||
TerrainBasis basis(int blockX, int blockZ) {
|
||||
long packed = pack(blockX, blockZ);
|
||||
synchronized (lock) {
|
||||
TerrainBasis cached = bases.getAndMoveToLast(packed);
|
||||
CacheStripe stripe = stripe(packed);
|
||||
synchronized (stripe) {
|
||||
TerrainBasis cached = stripe.bases.getAndMoveToLast(packed);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
double naturalHeight = naturalHeight(packed, blockX, blockZ);
|
||||
double naturalHeight = naturalHeight(stripe, packed, blockX, blockZ);
|
||||
TerrainBasis sampled = basisProvider.sample(blockX, blockZ, naturalHeight);
|
||||
if (sampled == null) {
|
||||
throw new NullPointerException(
|
||||
"Hydrology terrain basis provider returned null at " + blockX + "," + blockZ
|
||||
);
|
||||
}
|
||||
synchronized (lock) {
|
||||
TerrainBasis existing = bases.getAndMoveToLast(packed);
|
||||
synchronized (stripe) {
|
||||
TerrainBasis existing = stripe.bases.getAndMoveToLast(packed);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
bases.putAndMoveToLast(packed, sampled);
|
||||
oceanClassifications.remove(packed);
|
||||
evictOldest(bases);
|
||||
stripe.bases.putAndMoveToLast(packed, sampled);
|
||||
stripe.oceanClassifications.remove(packed);
|
||||
stripe.evictOldest(stripe.bases);
|
||||
}
|
||||
return sampled;
|
||||
}
|
||||
|
||||
int basisCacheSize() {
|
||||
synchronized (lock) {
|
||||
return bases.size();
|
||||
int size = 0;
|
||||
for (CacheStripe stripe : stripes) {
|
||||
synchronized (stripe) {
|
||||
size += stripe.bases.size();
|
||||
}
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
int naturalHeightCacheSize() {
|
||||
synchronized (lock) {
|
||||
return naturalHeights.size();
|
||||
int size = 0;
|
||||
for (CacheStripe stripe : stripes) {
|
||||
synchronized (stripe) {
|
||||
size += stripe.naturalHeights.size();
|
||||
}
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
int oceanClassificationCacheSize() {
|
||||
synchronized (lock) {
|
||||
return oceanClassifications.size();
|
||||
int size = 0;
|
||||
for (CacheStripe stripe : stripes) {
|
||||
synchronized (stripe) {
|
||||
size += stripe.oceanClassifications.size();
|
||||
}
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
synchronized (lock) {
|
||||
bases.clear();
|
||||
naturalHeights.clear();
|
||||
oceanClassifications.clear();
|
||||
for (CacheStripe stripe : stripes) {
|
||||
synchronized (stripe) {
|
||||
stripe.bases.clear();
|
||||
stripe.naturalHeights.clear();
|
||||
stripe.oceanClassifications.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,30 +290,21 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
}
|
||||
|
||||
|
||||
private void evictOldest(Long2ObjectLinkedOpenHashMap<?> cache) {
|
||||
if (cache.size() <= maximumEntries) {
|
||||
return;
|
||||
}
|
||||
cache.removeFirst();
|
||||
}
|
||||
|
||||
private void evictOldest(Long2DoubleLinkedOpenHashMap cache) {
|
||||
if (cache.size() <= maximumEntries) {
|
||||
return;
|
||||
}
|
||||
cache.removeFirstDouble();
|
||||
}
|
||||
|
||||
private double naturalHeight(int blockX, int blockZ) {
|
||||
return naturalHeight(pack(blockX, blockZ), blockX, blockZ);
|
||||
long packed = pack(blockX, blockZ);
|
||||
return naturalHeight(stripe(packed), packed, blockX, blockZ);
|
||||
}
|
||||
|
||||
private double naturalHeight(long packed, int blockX, int blockZ) {
|
||||
synchronized (lock) {
|
||||
double cached = naturalHeights.getAndMoveToLast(packed);
|
||||
if (!Double.isNaN(cached) || naturalHeights.containsKey(packed)) {
|
||||
private double naturalHeight(CacheStripe stripe, long packed, int blockX, int blockZ) {
|
||||
synchronized (stripe) {
|
||||
double cached = stripe.naturalHeights.getAndMoveToLast(packed);
|
||||
if (!Double.isNaN(cached) || stripe.naturalHeights.containsKey(packed)) {
|
||||
return cached;
|
||||
}
|
||||
TerrainBasis cachedBasis = stripe.bases.getAndMoveToLast(packed);
|
||||
if (cachedBasis != null) {
|
||||
return cachedBasis.naturalHeight();
|
||||
}
|
||||
}
|
||||
double sampled = heightProvider.sample(blockX, blockZ);
|
||||
if (!Double.isFinite(sampled)) {
|
||||
@@ -306,22 +312,53 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
// later sample of the same column gets a fresh chance instead of the cached failure.
|
||||
return sampled;
|
||||
}
|
||||
synchronized (lock) {
|
||||
double existing = naturalHeights.getAndMoveToLast(packed);
|
||||
if (!Double.isNaN(existing) || naturalHeights.containsKey(packed)) {
|
||||
synchronized (stripe) {
|
||||
double existing = stripe.naturalHeights.getAndMoveToLast(packed);
|
||||
if (!Double.isNaN(existing) || stripe.naturalHeights.containsKey(packed)) {
|
||||
return existing;
|
||||
}
|
||||
naturalHeights.putAndMoveToLast(packed, sampled);
|
||||
evictOldest(naturalHeights);
|
||||
stripe.naturalHeights.putAndMoveToLast(packed, sampled);
|
||||
stripe.evictOldest(stripe.naturalHeights);
|
||||
}
|
||||
return sampled;
|
||||
}
|
||||
|
||||
private CacheStripe stripe(long packed) {
|
||||
return stripes[(int) CacheKey.mix(packed) & (stripes.length - 1)];
|
||||
}
|
||||
|
||||
private static long pack(int blockX, int blockZ) {
|
||||
int mixedZ = blockZ ^ Integer.rotateLeft(blockX * COORDINATE_MIX, 16);
|
||||
return ((long) blockX << 32) ^ (mixedZ & 0xffffffffL);
|
||||
}
|
||||
|
||||
private static final class CacheStripe {
|
||||
private final int maximumEntries;
|
||||
private final Long2ObjectLinkedOpenHashMap<TerrainBasis> bases;
|
||||
private final Long2DoubleLinkedOpenHashMap naturalHeights;
|
||||
private final Long2ObjectLinkedOpenHashMap<NaturalClassification> oceanClassifications;
|
||||
|
||||
private CacheStripe(int maximumEntries) {
|
||||
this.maximumEntries = maximumEntries;
|
||||
this.bases = new Long2ObjectLinkedOpenHashMap<>(maximumEntries);
|
||||
this.naturalHeights = new Long2DoubleLinkedOpenHashMap(maximumEntries);
|
||||
this.naturalHeights.defaultReturnValue(Double.NaN);
|
||||
this.oceanClassifications = new Long2ObjectLinkedOpenHashMap<>(maximumEntries);
|
||||
}
|
||||
|
||||
private void evictOldest(Long2ObjectLinkedOpenHashMap<?> cache) {
|
||||
if (cache.size() > maximumEntries) {
|
||||
cache.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
private void evictOldest(Long2DoubleLinkedOpenHashMap cache) {
|
||||
if (cache.size() > maximumEntries) {
|
||||
cache.removeFirstDouble();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface BasisProvider {
|
||||
TerrainBasis sample(int blockX, int blockZ, double naturalHeight);
|
||||
|
||||
+7
-2
@@ -119,6 +119,7 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
IrisPlatforms.isBound() ? MultiBurst.hydrology : null,
|
||||
context.waitingForbidden()
|
||||
);
|
||||
this.cache.setTerrainPreparation(context.terrainPreparation());
|
||||
IrisLogging.debug("Hydrology runtime: tileSize=%d publicationRadius=%d planningThreads=%d",
|
||||
settings.routing().tileSize(), settings.publicationRadius(), MultiBurst.hydrology.parallelism());
|
||||
}
|
||||
@@ -131,6 +132,10 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
return cache.get(key);
|
||||
}
|
||||
|
||||
public List<HydrologyDiagnosticCandidate> diagnosticCandidates(HydrologyTileKey key) {
|
||||
return cache.diagnosticCandidates(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the column can be sampled without planning: true when its tiles are cached. Otherwise the
|
||||
* planning pool is asked for them and the caller should answer from natural terrain for now.
|
||||
@@ -319,8 +324,8 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
LinkedHashMap<Long, HydrologyDiagnosticCandidate> candidates = new LinkedHashMap<>();
|
||||
for (int tileZ = minimumTileZ; tileZ <= maximumTileZ; tileZ++) {
|
||||
for (int tileX = minimumTileX; tileX <= maximumTileX; tileX++) {
|
||||
HydrologyTile tile = cache.get(new HydrologyTileKey(tileX, tileZ));
|
||||
for (HydrologyDiagnosticCandidate candidate : tile.diagnosticCandidates()) {
|
||||
HydrologyTileKey key = new HydrologyTileKey(tileX, tileZ);
|
||||
for (HydrologyDiagnosticCandidate candidate : diagnosticCandidates(key)) {
|
||||
if (candidate.point().x() < minX || candidate.point().x() >= maxX
|
||||
|| candidate.point().z() < minZ || candidate.point().z() >= maxZ) {
|
||||
continue;
|
||||
|
||||
+3
-1
@@ -17,7 +17,8 @@ public record IrisHydrologyRuntimeContext(
|
||||
IrisHydrologyNaturalHeightDescriber naturalHeightDescriber,
|
||||
IrisHydrologyNaturalOceanClassifier naturalOceanClassifier,
|
||||
HydrologyCaveVoxelViewFactory caveViewFactory,
|
||||
BooleanSupplier waitingForbidden
|
||||
BooleanSupplier waitingForbidden,
|
||||
Runnable terrainPreparation
|
||||
) {
|
||||
/**
|
||||
* {@code waitingForbidden} is true on a thread that must never wait for a cold hydrology plan,
|
||||
@@ -36,5 +37,6 @@ public record IrisHydrologyRuntimeContext(
|
||||
Objects.requireNonNull(naturalOceanClassifier);
|
||||
Objects.requireNonNull(caveViewFactory);
|
||||
Objects.requireNonNull(waitingForbidden);
|
||||
Objects.requireNonNull(terrainPreparation);
|
||||
}
|
||||
}
|
||||
@@ -324,6 +324,9 @@ public final class MantleHydrologyComponent extends IrisMantleComponent {
|
||||
CavePosition position = new CavePosition(sample.x(), y, sample.z());
|
||||
HydrologyCaveAction action = plan.actions().get(position);
|
||||
if (action == null || action == HydrologyCaveAction.SEAL_GUARD) {
|
||||
if (!isCaveLayer(layer)) {
|
||||
continue;
|
||||
}
|
||||
throw new IllegalStateException("Accepted cave layer is absent from its containment plan at "
|
||||
+ position + ".");
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.interpolation.IrisInterpolation;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.noise.CellGenerator;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
||||
import java.util.concurrent.atomic.AtomicReferenceArray;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
@@ -262,12 +262,13 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
mixed *= 0xff51afd7ed558ccdL;
|
||||
mixed ^= mixed >>> 33;
|
||||
SurfaceCacheStripe stripe = stripes[(int) mixed & (SURFACE_CACHE_STRIPES - 1)];
|
||||
SurfaceSample cached = stripe.get(key, superSeed);
|
||||
int slot = (int) (mixed >>> 4) & (SURFACE_CACHE_STRIPE_SIZE - 1);
|
||||
SurfaceSample cached = stripe.get(key, superSeed, slot);
|
||||
if (cached != null) {
|
||||
return cached.height();
|
||||
}
|
||||
double height = sampleHeight(x, z, superSeed);
|
||||
stripe.put(key, new SurfaceSample(superSeed, height));
|
||||
stripe.put(slot, new SurfaceSample(key, superSeed, height));
|
||||
return height;
|
||||
}
|
||||
|
||||
@@ -347,22 +348,19 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
private record SurfaceCache(IrisData data, Engine engine, SurfaceCacheStripe[] stripes) {
|
||||
}
|
||||
|
||||
private record SurfaceSample(long seed, double height) {
|
||||
private record SurfaceSample(long key, long seed, double height) {
|
||||
}
|
||||
|
||||
private static final class SurfaceCacheStripe {
|
||||
private final Long2ObjectLinkedOpenHashMap<SurfaceSample> entries = new Long2ObjectLinkedOpenHashMap<>(16);
|
||||
private final AtomicReferenceArray<SurfaceSample> entries = new AtomicReferenceArray<>(SURFACE_CACHE_STRIPE_SIZE);
|
||||
|
||||
private synchronized SurfaceSample get(long key, long seed) {
|
||||
SurfaceSample sample = entries.getAndMoveToLast(key);
|
||||
return sample != null && sample.seed() == seed ? sample : null;
|
||||
private SurfaceSample get(long key, long seed, int slot) {
|
||||
SurfaceSample sample = entries.get(slot);
|
||||
return sample != null && sample.key() == key && sample.seed() == seed ? sample : null;
|
||||
}
|
||||
|
||||
private synchronized void put(long key, SurfaceSample sample) {
|
||||
entries.putAndMoveToLast(key, sample);
|
||||
if (entries.size() > SURFACE_CACHE_STRIPE_SIZE) {
|
||||
entries.removeFirst();
|
||||
}
|
||||
private void put(int slot, SurfaceSample sample) {
|
||||
entries.set(slot, sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -354,6 +354,8 @@ public final class DimensionTerrainContext implements DataProvider {
|
||||
);
|
||||
return mappedTerrainHeight(imageMapRuntime, proceduralHeight, x, z);
|
||||
}, Interpolated.DOUBLE), cachePrefix + "ImageMappedHeightStream", engine, cacheSize);
|
||||
// heightStream is cached and never resolves the surface biome, so the shore band can read its slope.
|
||||
ProceduralStream<Double> shoreSlopeStream = heightStream.slope(3);
|
||||
ProceduralStream<IrisBiome> finalBiomeStream = focusBiome == null
|
||||
? GenerationStreams.cache2D(heightStream.convertAware2D((height, x, z) -> {
|
||||
IrisBiome mappedBiome = imageMapRuntime.sampleBiome(x, z);
|
||||
@@ -370,7 +372,8 @@ public final class DimensionTerrainContext implements DataProvider {
|
||||
fluidHeight,
|
||||
landBiomeStream,
|
||||
seaBiomeStream,
|
||||
shoreBiomeStream
|
||||
shoreBiomeStream,
|
||||
shoreSlopeStream
|
||||
);
|
||||
return resolvedBiome == baseBiome
|
||||
? baseBiome
|
||||
|
||||
@@ -43,30 +43,25 @@ public final class GenerationCacheWarmer {
|
||||
RNG root = new RNG(engine.getSeedManager().getComponent() + 7777L);
|
||||
int[] counter = {0};
|
||||
|
||||
try {
|
||||
KList<IrisBiome> biomes = engine.getAllBiomes();
|
||||
biomes.sort(Comparator.comparing(IrisBiome::getLoadKey));
|
||||
for (IrisBiome biome : biomes) {
|
||||
warmPlacements(biome.getObjects(), root, counter, data, engine);
|
||||
warmDecorators(biome.getDecorators(), root, counter, data);
|
||||
warmOres(biome.getOres(), root, counter, data);
|
||||
warmProcedural(biome.getProceduralObjects(), root, counter, data);
|
||||
}
|
||||
|
||||
KList<IrisRegion> regions = engine.getDimension().getAllRegions(engine);
|
||||
regions.sort(Comparator.comparing(IrisRegion::getLoadKey));
|
||||
for (IrisRegion region : regions) {
|
||||
warmPlacements(region.getObjects(), root, counter, data, engine);
|
||||
warmOres(region.getOres(), root, counter, data);
|
||||
warmProcedural(region.getProceduralObjects(), root, counter, data);
|
||||
}
|
||||
|
||||
warmOres(engine.getDimension().getOres(), root, counter, data);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
IrisLogging.warn("Generation cache warm pass failed: " + e.getMessage());
|
||||
KList<IrisBiome> biomes = engine.getAllBiomes();
|
||||
biomes.sort(Comparator.comparing(IrisBiome::getLoadKey));
|
||||
for (IrisBiome biome : biomes) {
|
||||
warmPlacements(biome.getObjects(), root, counter, data, engine);
|
||||
warmDecorators(biome.getDecorators(), root, counter, data);
|
||||
warmOres(biome.getOres(), root, counter, data);
|
||||
warmProcedural(biome.getProceduralObjects(), root, counter, data);
|
||||
}
|
||||
|
||||
KList<IrisRegion> regions = engine.getDimension().getAllRegions(engine);
|
||||
regions.sort(Comparator.comparing(IrisRegion::getLoadKey));
|
||||
for (IrisRegion region : regions) {
|
||||
warmPlacements(region.getObjects(), root, counter, data, engine);
|
||||
warmOres(region.getOres(), root, counter, data);
|
||||
warmProcedural(region.getProceduralObjects(), root, counter, data);
|
||||
}
|
||||
|
||||
warmOres(engine.getDimension().getOres(), root, counter, data);
|
||||
|
||||
IrisLogging.debug("[IrisEngine timing] cache warm " + counter[0] + " configs=" + (M.ms() - start) + "ms");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
final class HydrologyNoiseCacheBudget {
|
||||
static final int MAXIMUM_CHUNKS = 32_768;
|
||||
static final int BYTES_PER_CHUNK = 4_096;
|
||||
static final HydrologyNoiseCacheBudget SHARED = new HydrologyNoiseCacheBudget(Runtime.getRuntime().maxMemory() / 8L);
|
||||
|
||||
private final long maximumBytes;
|
||||
private long reservedBytes;
|
||||
|
||||
HydrologyNoiseCacheBudget(long maximumBytes) {
|
||||
if (maximumBytes < 0L) {
|
||||
throw new IllegalArgumentException("Cache budget must not be negative.");
|
||||
}
|
||||
this.maximumBytes = maximumBytes;
|
||||
}
|
||||
|
||||
synchronized Reservation reserve(int cacheCount, int requestedAdditionalChunks) {
|
||||
if (cacheCount <= 0 || requestedAdditionalChunks < 0 || requestedAdditionalChunks > MAXIMUM_CHUNKS) {
|
||||
throw new IllegalArgumentException("Invalid hydrology cache reservation.");
|
||||
}
|
||||
long bytesPerIncrement = (long) BYTES_PER_CHUNK * cacheCount;
|
||||
int additionalChunks = (int) Math.min(requestedAdditionalChunks, (maximumBytes - reservedBytes) / bytesPerIncrement);
|
||||
long bytes = additionalChunks * bytesPerIncrement;
|
||||
reservedBytes += bytes;
|
||||
return new Reservation(this, additionalChunks, bytes);
|
||||
}
|
||||
|
||||
synchronized long reservedBytes() {
|
||||
return reservedBytes;
|
||||
}
|
||||
|
||||
private synchronized void release(Reservation reservation) {
|
||||
if (!reservation.released) {
|
||||
reservedBytes -= reservation.bytes;
|
||||
reservation.released = true;
|
||||
}
|
||||
}
|
||||
|
||||
static final class Reservation implements AutoCloseable {
|
||||
private final HydrologyNoiseCacheBudget budget;
|
||||
private final int additionalChunks;
|
||||
private final long bytes;
|
||||
private boolean released;
|
||||
|
||||
private Reservation(HydrologyNoiseCacheBudget budget, int additionalChunks, long bytes) {
|
||||
this.budget = budget;
|
||||
this.additionalChunks = additionalChunks;
|
||||
this.bytes = bytes;
|
||||
}
|
||||
|
||||
int additionalChunks() {
|
||||
return additionalChunks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
budget.release(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.generation.stream.GenerationStreams;
|
||||
import art.arcane.iris.generation.stream.CachedDoubleStream2D;
|
||||
import art.arcane.iris.generation.stream.CachedStream2D;
|
||||
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
@@ -36,6 +38,7 @@ import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyNaturalSample;
|
||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntimeContext;
|
||||
import art.arcane.iris.generation.image.IrisImageMapApplication;
|
||||
import art.arcane.iris.generation.image.IrisImageMapRuntime;
|
||||
import art.arcane.iris.generation.mantle.MantleHydrologyCaveVoxelView;
|
||||
import art.arcane.iris.generation.terrain.InferredType;
|
||||
@@ -78,6 +81,7 @@ import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
@@ -90,6 +94,7 @@ import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
@@ -111,6 +116,10 @@ public class IrisComplex implements DataProvider {
|
||||
/** One million corners: about 16 MB, roughly a 4000 by 4000 block area at the 4-block grid. */
|
||||
private static final int SHARED_CORNER_BOUNDS_CAPACITY = 1 << 20;
|
||||
private static final int HEIGHT_BOUNDS_GRID = 4;
|
||||
/** The slope streams measure the rise across a run of this many blocks, so gradient is slope over run. */
|
||||
private static final double SLOPE_RUN = 3D;
|
||||
/** A sheer coast would let shoreMinimumWidth buy unbounded height, so the shore band stops climbing here. */
|
||||
private static final double MAX_SHORE_BAND_GRADIENT = 3D;
|
||||
private static final Comparator<IrisInterpolator> INTERPOLATOR_ORDER = Comparator
|
||||
.comparing((IrisInterpolator interpolator) -> interpolator.getFunction().name())
|
||||
.thenComparingDouble(IrisInterpolator::getHorizontalScale);
|
||||
@@ -134,6 +143,17 @@ public class IrisComplex implements DataProvider {
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient Map<IrisRegion, Map<InferredType, ProceduralStream<IrisBiome>>> inferredBiomeStreams;
|
||||
private final TransitionGenerationPlan transitionGenerationPlan;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final transient boolean detached;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private transient List<NoiseCacheCapacity> hydrologyNoiseCaches = List.of();
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private transient HydrologyNoiseCacheBudget.Reservation hydrologyNoiseCacheReservation;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private transient boolean hydrologyNoiseCachesClosed;
|
||||
private RNG rng;
|
||||
private double fluidHeight;
|
||||
private IrisData data;
|
||||
@@ -166,12 +186,16 @@ public class IrisComplex implements DataProvider {
|
||||
private final ResolvedTerrainProvider resolvedTerrain;
|
||||
private ProceduralStream<Double> placementHeightStream;
|
||||
private ProceduralStream<Double> heightStream;
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Setter(AccessLevel.NONE)
|
||||
private CachedDoubleStream2D cachedHeightStream;
|
||||
private ProceduralStream<Integer> roundedHeighteightStream;
|
||||
private ProceduralStream<Double> maxHeightStream;
|
||||
private ProceduralStream<Double> overlayStream;
|
||||
private ProceduralStream<Double> heightFluidStream;
|
||||
private ProceduralStream<Double> slopeStream;
|
||||
private ProceduralStream<Double> naturalSlopeStream;
|
||||
private ProceduralStream<Double> shoreSlopeStream;
|
||||
private ProceduralStream<Double> riverDistanceStream;
|
||||
private ProceduralStream<Double> riverFlowStream;
|
||||
private ProceduralStream<Double> riverCarveWeightStream;
|
||||
@@ -216,6 +240,7 @@ public class IrisComplex implements DataProvider {
|
||||
|
||||
IrisComplex(Engine engine, boolean simple, TransitionGenerationPlan transitionGenerationPlan, boolean detached) {
|
||||
terrainEngine = engine;
|
||||
this.detached = detached;
|
||||
this.transitionGenerationPlan = transitionGenerationPlan;
|
||||
this.resolvedTerrain = new ResolvedTerrainProvider(engine);
|
||||
int cacheSize = noiseCacheSize(engine, IrisSettings.get().getPerformance().getNoiseCacheSize(), detached);
|
||||
@@ -317,7 +342,8 @@ public class IrisComplex implements DataProvider {
|
||||
ProceduralStream<IrisRegion> proceduralRegionStream = focusedRegions != null ? focusedRegions
|
||||
: GenerationStreams.cache2D(regionStyleStream
|
||||
.selectRarity(compatRegionPool(engine)), "regionStream", engine, cacheSize);
|
||||
regionStream = focusedRegions != null ? proceduralRegionStream : GenerationStreams.cache2D(proceduralRegionStream
|
||||
regionStream = focusedRegions != null || !imageMapRuntime.has(IrisImageMapApplication.REGION)
|
||||
? proceduralRegionStream : GenerationStreams.cache2D(proceduralRegionStream
|
||||
.convertAware2D((region, x, z) -> {
|
||||
IrisRegion mapped = imageMapRuntime.sampleRegion(x, z);
|
||||
return mapped == null ? region : mapped;
|
||||
@@ -347,7 +373,8 @@ public class IrisComplex implements DataProvider {
|
||||
ProceduralStream<IrisBiome> proceduralBaseBiomeStream = focusedBiomes != null ? focusedBiomes :
|
||||
GenerationStreams.cache2D(bridgeStream.convertAware2D((t, x, z) -> inferredStreams.get(t).get(x, z))
|
||||
.convertAware2D(this::implode), "baseBiomeStream", engine, cacheSize);
|
||||
baseBiomeStream = focusedBiomes != null ? proceduralBaseBiomeStream : GenerationStreams.cache2D(proceduralBaseBiomeStream
|
||||
baseBiomeStream = focusedBiomes != null || !imageMapRuntime.has(IrisImageMapApplication.BIOME)
|
||||
? proceduralBaseBiomeStream : GenerationStreams.cache2D(proceduralBaseBiomeStream
|
||||
.convertAware2D((biome, x, z) -> {
|
||||
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
||||
return mapped == null ? biome : mapped;
|
||||
@@ -400,7 +427,8 @@ public class IrisComplex implements DataProvider {
|
||||
(x, z) -> describeNaturalHeight(engine, x, z),
|
||||
this::sampleNaturalOcean,
|
||||
footprint -> new MantleHydrologyCaveVoxelView(engine, this, footprint),
|
||||
() -> engine.getPlatformHooks().isMainThread()
|
||||
() -> engine.getPlatformHooks().isMainThread(),
|
||||
this::expandHydrologyNoiseCaches
|
||||
));
|
||||
hydrologyRuntime.setNeighbourPrefetchEnabled(!engine.isStudio());
|
||||
IrisRiverBank3DConfig banks3D = configuredHydrology.getRivers().getGeometry().getBanks3D();
|
||||
@@ -415,7 +443,8 @@ public class IrisComplex implements DataProvider {
|
||||
Math.max(4096, cacheSize), data, banks3D));
|
||||
}
|
||||
}
|
||||
ProceduralStream<Double> cachedHeightStream = GenerationStreams.cache2DDouble(ProceduralStream.ofDouble((x, z) -> resolveHydrologyTerrainHeight(x, z)), "heightStream", engine, cacheSize);
|
||||
cachedHeightStream = new CachedDoubleStream2D("heightStream", engine,
|
||||
ProceduralStream.ofDouble(this::resolveHydrologyTerrainHeight), cacheSize);
|
||||
heightStream = ProceduralStream.ofDouble((x, z) -> terrainEngine.getPlatformHooks().isMainThread()
|
||||
? nonblockingTerrainHeight(x, z) : cachedHeightStream.getDouble(x, z));
|
||||
placementHeightStream = ProceduralStream.ofDouble(this::samplePlacementHeight);
|
||||
@@ -424,6 +453,8 @@ public class IrisComplex implements DataProvider {
|
||||
slopeStream = GenerationStreams.contextInjecting(placementHeightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||
.slope(3);
|
||||
naturalSlopeStream = GenerationStreams.cache2DDouble(naturalHeightStream.slope(3), "naturalSlopeStream", engine, cacheSize);
|
||||
// Terrain3D column heights resolve the surface biome, so the shore band reads the pre-terrain3D slope or it recurses.
|
||||
shoreSlopeStream = terrain3DEnabled ? baseTerrainHeightStream.slope(3) : naturalSlopeStream;
|
||||
trueBiomeStream = focusedBiomes != null ? GenerationStreams.cache2D(focusedBiomes, "trueBiomeStream-focus", engine, cacheSize) : GenerationStreams.cache2D(heightStream
|
||||
.convertAware2D((terrainHeight, x, z) -> resolveHydrologySurfaceBiome(terrainHeight, x, z)), "trueBiomeStream", engine, cacheSize);
|
||||
trueBiomeDerivativeStream = GenerationStreams.cache2D(GenerationStreams.contextInjecting(trueBiomeStream, engine, (c, x, z) -> c.getBiome().get(x, z))
|
||||
@@ -469,6 +500,66 @@ public class IrisComplex implements DataProvider {
|
||||
return engine.isStudio() && !detached ? Math.max(configuredSize, STUDIO_NOISE_CACHE_SIZE) : configuredSize;
|
||||
}
|
||||
|
||||
public synchronized void ensureTerrainNoiseCacheSize(int minimumChunks) {
|
||||
if (minimumChunks <= 0) {
|
||||
throw new IllegalArgumentException("Terrain noise cache size must be positive");
|
||||
}
|
||||
CachedDoubleStream2D naturalCache = (CachedDoubleStream2D) naturalHeightStream;
|
||||
if (naturalCache.getMaxSize() < minimumChunks * 256L) {
|
||||
naturalCache.setMaximumChunks(minimumChunks);
|
||||
}
|
||||
if (cachedHeightStream.getMaxSize() < minimumChunks * 256L) {
|
||||
cachedHeightStream.setMaximumChunks(minimumChunks);
|
||||
}
|
||||
}
|
||||
|
||||
void expandHydrologyNoiseCaches() {
|
||||
expandHydrologyNoiseCaches(HydrologyNoiseCacheBudget.SHARED);
|
||||
}
|
||||
|
||||
synchronized void expandHydrologyNoiseCaches(HydrologyNoiseCacheBudget budget) {
|
||||
if (detached || hydrologyRuntime == null || hydrologyNoiseCachesClosed || hydrologyNoiseCacheReservation != null) {
|
||||
return;
|
||||
}
|
||||
IdentityHashMap<ProceduralStream<?>, Boolean> selected = new IdentityHashMap<>();
|
||||
List<NoiseCacheCapacity> capacities = new ArrayList<>(5);
|
||||
int additionalChunks = HydrologyNoiseCacheBudget.MAXIMUM_CHUNKS;
|
||||
for (ProceduralStream<?> stream : new ProceduralStream<?>[]{
|
||||
baseBiomeStream, baseTerrainHeightStream, unblendedNaturalHeightStream, regionStream, bridgeStream}) {
|
||||
if (!(stream instanceof CachedStream2D<?> || stream instanceof CachedDoubleStream2D)
|
||||
|| selected.put(stream, Boolean.TRUE) != null) {
|
||||
continue;
|
||||
}
|
||||
int originalChunks = Math.toIntExact(((MeteredCache) stream).getMaxSize() / 256L);
|
||||
if (originalChunks >= HydrologyNoiseCacheBudget.MAXIMUM_CHUNKS) {
|
||||
continue;
|
||||
}
|
||||
capacities.add(new NoiseCacheCapacity(stream, originalChunks));
|
||||
additionalChunks = Math.min(additionalChunks, HydrologyNoiseCacheBudget.MAXIMUM_CHUNKS - originalChunks);
|
||||
}
|
||||
if (capacities.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
hydrologyNoiseCaches = List.copyOf(capacities);
|
||||
hydrologyNoiseCacheReservation = budget.reserve(capacities.size(), additionalChunks);
|
||||
for (NoiseCacheCapacity capacity : hydrologyNoiseCaches) {
|
||||
capacity.resize(capacity.originalChunks() + hydrologyNoiseCacheReservation.additionalChunks());
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void releaseHydrologyNoiseCaches() {
|
||||
hydrologyNoiseCachesClosed = true;
|
||||
if (hydrologyNoiseCacheReservation == null) {
|
||||
return;
|
||||
}
|
||||
for (NoiseCacheCapacity capacity : hydrologyNoiseCaches) {
|
||||
capacity.resize(capacity.originalChunks());
|
||||
}
|
||||
hydrologyNoiseCacheReservation.close();
|
||||
hydrologyNoiseCacheReservation = null;
|
||||
hydrologyNoiseCaches = List.of();
|
||||
}
|
||||
|
||||
void enableStudioHydrologyCache(String runtimeIdentity, Path persistentRoot) {
|
||||
if (hydrologyRuntime != null) {
|
||||
hydrologyRuntime.enableSharedCache(runtimeIdentity, persistentRoot);
|
||||
@@ -684,11 +775,11 @@ public class IrisComplex implements DataProvider {
|
||||
if (biome == null || region == null) {
|
||||
return biome;
|
||||
}
|
||||
double shoreHeight = region.getShoreHeight(x, z);
|
||||
if (height >= fluidHeight - 1 && height <= fluidHeight + shoreHeight && !biome.isShore()) {
|
||||
double shoreTop = shoreBandTop(height, region, x, z, fluidHeight, shoreSlopeStream);
|
||||
if (height >= fluidHeight - 1 && height <= shoreTop && !biome.isShore()) {
|
||||
return sampleInferredBiome(region, InferredType.SHORE, x, z);
|
||||
}
|
||||
if (height > fluidHeight + shoreHeight && !biome.isLand()) {
|
||||
if (height > shoreTop && !biome.isLand()) {
|
||||
return sampleInferredBiome(region, InferredType.LAND, x, z);
|
||||
}
|
||||
if (height < fluidHeight && !biome.isAquatic()) {
|
||||
@@ -1264,10 +1355,38 @@ public class IrisComplex implements DataProvider {
|
||||
fluidHeight,
|
||||
landBiomeStream,
|
||||
seaBiomeStream,
|
||||
shoreBiomeStream);
|
||||
shoreBiomeStream,
|
||||
shoreSlopeStream);
|
||||
return resolved == biome ? biome : implode(resolved, x, z);
|
||||
}
|
||||
|
||||
/**
|
||||
* The highest column the shore band still covers. Shore height alone is a vertical window, so a steep
|
||||
* coast gets a beach only a fraction of a block wide; shoreMinimumWidth trades the local gradient back into
|
||||
* height so the beach keeps that width across the ground. Zero width never reads the slope, and neither
|
||||
* does a column that is already inside the vertical window or too high for any widening to reach.
|
||||
*/
|
||||
static double shoreBandTop(
|
||||
double height,
|
||||
IrisRegion region,
|
||||
double x,
|
||||
double z,
|
||||
double fluidHeight,
|
||||
ProceduralStream<Double> shoreSlopes
|
||||
) {
|
||||
double bandTop = fluidHeight + region.getShoreHeight(x, z);
|
||||
double shoreMinimumWidth = region.getShoreMinimumWidth();
|
||||
|
||||
if (shoreMinimumWidth <= 0
|
||||
|| height <= bandTop
|
||||
|| height < fluidHeight - 1
|
||||
|| height > bandTop + shoreMinimumWidth * MAX_SHORE_BAND_GRADIENT) {
|
||||
return bandTop;
|
||||
}
|
||||
|
||||
return bandTop + shoreMinimumWidth * Math.min(shoreSlopes.getDouble(x, z) / SLOPE_RUN, MAX_SHORE_BAND_GRADIENT);
|
||||
}
|
||||
|
||||
static IrisBiome resolveSurfaceBiome(
|
||||
double height,
|
||||
IrisBiome biome,
|
||||
@@ -1277,18 +1396,19 @@ public class IrisComplex implements DataProvider {
|
||||
double fluidHeight,
|
||||
ProceduralStream<IrisBiome> landBiomes,
|
||||
ProceduralStream<IrisBiome> seaBiomes,
|
||||
ProceduralStream<IrisBiome> shoreBiomes
|
||||
ProceduralStream<IrisBiome> shoreBiomes,
|
||||
ProceduralStream<Double> shoreSlopes
|
||||
) {
|
||||
if (biome == null || region == null) {
|
||||
return biome;
|
||||
}
|
||||
double sh = region.getShoreHeight(x, z);
|
||||
double shoreTop = shoreBandTop(height, region, x, z, fluidHeight, shoreSlopes);
|
||||
|
||||
if (height >= fluidHeight - 1 && height <= fluidHeight + sh && !biome.isShore()) {
|
||||
if (height >= fluidHeight - 1 && height <= shoreTop && !biome.isShore()) {
|
||||
return shoreBiomes.get(x, z);
|
||||
}
|
||||
|
||||
if (height > fluidHeight + sh && !biome.isLand()) {
|
||||
if (height > shoreTop && !biome.isLand()) {
|
||||
return landBiomes.get(x, z);
|
||||
}
|
||||
|
||||
@@ -1944,6 +2064,16 @@ public class IrisComplex implements DataProvider {
|
||||
record GeneratorGroup(IrisInterpolator interpolator, IrisGenerator[] generators) {
|
||||
}
|
||||
|
||||
private record NoiseCacheCapacity(ProceduralStream<?> stream, int originalChunks) {
|
||||
private void resize(int chunks) {
|
||||
if (stream instanceof CachedDoubleStream2D doubles) {
|
||||
doubles.setMaximumChunks(chunks);
|
||||
} else if (stream instanceof CachedStream2D<?> objects) {
|
||||
objects.setMaximumChunks(chunks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (hydrologyBanks3D != null) {
|
||||
hydrologyBanks3D.clear();
|
||||
@@ -1954,6 +2084,7 @@ public class IrisComplex implements DataProvider {
|
||||
if (hydrologyRuntime != null) {
|
||||
hydrologyRuntime.close();
|
||||
}
|
||||
releaseHydrologyNoiseCaches();
|
||||
resolvedTerrain.clear();
|
||||
}
|
||||
}
|
||||
@@ -322,16 +322,7 @@ public class IrisEngine implements Engine {
|
||||
diagnostics.logPackCompatSummary();
|
||||
diagnostics.logStudioInitializationPhase("build_runtime", phaseStartedAt, false);
|
||||
phaseStartedAt = System.nanoTime();
|
||||
if (requiredMode.warmGenerationCaches()) {
|
||||
if (requiredMode.studio()) {
|
||||
startGenerationCacheWarm(phaseStartedAt);
|
||||
} else {
|
||||
warmGenerationCaches(phaseStartedAt);
|
||||
}
|
||||
} else {
|
||||
generationCacheWarm.complete(null);
|
||||
diagnostics.logStudioInitializationPhase("generation_cache_warm", phaseStartedAt, true);
|
||||
}
|
||||
prepareGenerationCaches(requiredMode, phaseStartedAt);
|
||||
EngineTickRegistry.registerTicking(this);
|
||||
} catch (Throwable e) {
|
||||
shutdownSequence.cleanupFailedConstruction(e);
|
||||
@@ -355,6 +346,19 @@ public class IrisEngine implements Engine {
|
||||
return !generationCacheWarm.isDone();
|
||||
}
|
||||
|
||||
void prepareGenerationCaches(InitializationMode mode, long phaseStartedAt) {
|
||||
if (!mode.warmGenerationCaches()) {
|
||||
generationCacheWarm.complete(null);
|
||||
diagnostics.logStudioInitializationPhase("generation_cache_warm", phaseStartedAt, true);
|
||||
return;
|
||||
}
|
||||
if (mode == InitializationMode.RUNTIME) {
|
||||
warmGenerationCaches(phaseStartedAt);
|
||||
} else {
|
||||
startGenerationCacheWarm(phaseStartedAt);
|
||||
}
|
||||
}
|
||||
|
||||
private String currentStudioCacheIdentity() {
|
||||
if (initializationMode != InitializationMode.STUDIO || !IrisPlatforms.isBound()) {
|
||||
return "";
|
||||
@@ -396,32 +400,32 @@ public class IrisEngine implements Engine {
|
||||
return packRoot.resolve(".iris").resolve("studio-hydrology");
|
||||
}
|
||||
|
||||
public void startStudioEntryHydrology(int blockX, int blockZ) {
|
||||
public void startEntryHydrology(int blockX, int blockZ) {
|
||||
GenerationRuntimeBinding binding = getActiveGenerationRuntimeBinding();
|
||||
if (!backgroundTasks.scheduleTrackedTask(() -> prepareStudioEntryHydrology(binding, blockX, blockZ))) {
|
||||
throw new IllegalStateException("Iris background task admission closed before Studio entry preparation.");
|
||||
if (!backgroundTasks.scheduleTrackedTask(() -> prepareEntryHydrology(binding, blockX, blockZ))) {
|
||||
throw new IllegalStateException("Iris background task admission closed before world entry preparation.");
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareStudioEntryHydrology(GenerationRuntimeBinding binding, int blockX, int blockZ) {
|
||||
private void prepareEntryHydrology(GenerationRuntimeBinding binding, int blockX, int blockZ) {
|
||||
if (closing.get()) {
|
||||
return;
|
||||
}
|
||||
GenerationSessionLease lease;
|
||||
try {
|
||||
lease = acquireGenerationLease("studio_entry_hydrology");
|
||||
lease = acquireGenerationLease("initial_entry_hydrology");
|
||||
} catch (GenerationSessionException failure) {
|
||||
if (closing.get()) {
|
||||
return;
|
||||
}
|
||||
throw new IllegalStateException("Studio entry hydrology preparation could not acquire its generation session.", failure);
|
||||
throw new IllegalStateException("World entry hydrology preparation could not acquire its generation session.", failure);
|
||||
}
|
||||
try (lease;
|
||||
GenerationRuntimeScope runtimeScope = generationRuntimeScopes.open(binding);
|
||||
IrisContext.Scope context = IrisContext.open(this, lease.sessionId(), null)) {
|
||||
getComplex().getHydrologyRuntime().prepareChunkColumns(blockX, blockZ);
|
||||
} catch (Exception failure) {
|
||||
throw new IllegalStateException("Studio entry hydrology preparation failed at "
|
||||
throw new IllegalStateException("World entry hydrology preparation failed at "
|
||||
+ blockX + "," + blockZ + ".", failure);
|
||||
}
|
||||
}
|
||||
@@ -1685,6 +1689,7 @@ public class IrisEngine implements Engine {
|
||||
|
||||
public enum InitializationMode {
|
||||
RUNTIME(false, true),
|
||||
WORLD_CREATION(false, true),
|
||||
STUDIO(true, true),
|
||||
OBJECT_STUDIO(true, false),
|
||||
JIGSAW_STUDIO(true, false);
|
||||
|
||||
@@ -27,14 +27,17 @@ import art.arcane.volmlib.util.scheduling.Looper;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class PreservationSVC implements IrisService, PreservationRegistry {
|
||||
private static final long SHUTDOWN_TIMEOUT_MILLIS = 10_000L;
|
||||
private final List<Thread> threads = new CopyOnWriteArrayList<>();
|
||||
private final List<ExecutorService> services = new CopyOnWriteArrayList<>();
|
||||
private final List<WeakReference<MeteredCache>> caches = new CopyOnWriteArrayList<>();
|
||||
@@ -114,27 +117,64 @@ public class PreservationSVC implements IrisService, PreservationRegistry {
|
||||
}
|
||||
dereference();
|
||||
|
||||
postShutdown(() -> {
|
||||
for (Thread i : threads) {
|
||||
if (i.isAlive()) {
|
||||
try {
|
||||
i.interrupt();
|
||||
IrisLogging.debug("Shutdown Thread " + i.getName());
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
postShutdown(() -> shutdownResources(SHUTDOWN_TIMEOUT_MILLIS));
|
||||
}
|
||||
|
||||
for (ExecutorService i : services) {
|
||||
void shutdownResources(long timeoutMillis) {
|
||||
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMillis);
|
||||
List<Thread> ownedThreads = new ArrayList<>(threads);
|
||||
if (dereferencer != null && !ownedThreads.contains(dereferencer)) {
|
||||
ownedThreads.add(dereferencer);
|
||||
}
|
||||
List<ExecutorService> ownedServices = List.copyOf(services);
|
||||
for (Thread thread : ownedThreads) {
|
||||
if (thread.isAlive() && thread != Thread.currentThread()) {
|
||||
try {
|
||||
i.shutdownNow();
|
||||
IrisLogging.debug("Shutdown Executor Service " + i);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
thread.interrupt();
|
||||
} catch (Throwable failure) {
|
||||
IrisLogging.reportError("Failed to interrupt owned thread " + thread.getName(), failure);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
for (ExecutorService executor : ownedServices) {
|
||||
try {
|
||||
executor.shutdownNow();
|
||||
} catch (Throwable failure) {
|
||||
IrisLogging.reportError("Failed to stop owned executor " + executor, failure);
|
||||
}
|
||||
}
|
||||
try {
|
||||
for (Thread thread : ownedThreads) {
|
||||
long remaining = deadline - System.nanoTime();
|
||||
if (thread != Thread.currentThread() && thread.isAlive() && remaining > 0L) {
|
||||
TimeUnit.NANOSECONDS.timedJoin(thread, remaining);
|
||||
}
|
||||
}
|
||||
for (ExecutorService executor : ownedServices) {
|
||||
long remaining = deadline - System.nanoTime();
|
||||
if (!executor.isTerminated() && remaining > 0L) {
|
||||
executor.awaitTermination(remaining, TimeUnit.NANOSECONDS);
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException failure) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("Interrupted while draining Iris background resources", failure);
|
||||
}
|
||||
List<String> activeResources = new ArrayList<>();
|
||||
for (Thread thread : ownedThreads) {
|
||||
if (thread.isAlive() && thread != Thread.currentThread()) {
|
||||
activeResources.add("thread " + thread.getName() + " (" + thread.getState() + ")");
|
||||
}
|
||||
}
|
||||
for (ExecutorService executor : ownedServices) {
|
||||
if (!executor.isTerminated()) {
|
||||
activeResources.add("executor " + executor);
|
||||
}
|
||||
}
|
||||
if (!activeResources.isEmpty()) {
|
||||
throw new IllegalStateException("Iris background resources did not terminate within "
|
||||
+ timeoutMillis + " ms: " + String.join(", ", activeResources));
|
||||
}
|
||||
}
|
||||
|
||||
public void updateCaches() {
|
||||
|
||||
@@ -195,7 +195,9 @@ public class IrisPostModifier extends EngineAssignedModifier<PlatformBlockState>
|
||||
// Wall Patcher
|
||||
IrisBiome biome = context.getBiome().get(currentPostX, currentPostZ);
|
||||
|
||||
if (walls && !river) {
|
||||
// Neighbour heights come from trueHeight, which excludes fluid, so every coastal column reads a
|
||||
// seaward drop and would have its beach surface replaced with the wall palette.
|
||||
if (walls && !river && !biome.isShore()) {
|
||||
if (!biome.getWall().getPalette().isEmpty()) {
|
||||
if (ha < h - 2 || hb < h - 2 || hc < h - 2 || hd < h - 2) {
|
||||
boolean brokeGround = false;
|
||||
|
||||
@@ -246,8 +246,9 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
continue;
|
||||
}
|
||||
|
||||
int strataIndex = strataIndex(depth, cut, blocks.size());
|
||||
PlatformBlockState layerBlock = paintHydrologyMaterial(
|
||||
blocks.hasIndex(depth + cut) ? blocks.get(depth + cut) : null,
|
||||
blocks.hasIndex(strataIndex) ? blocks.get(strataIndex) : null,
|
||||
roleMaterial, depth, localRng, realX, i, realZ, data);
|
||||
if (layerBlock != null) {
|
||||
if (riverOwned && depth <= riverBed.getPadding() && IrisProceduralBlocks.isGravityAffected(layerBlock)) {
|
||||
@@ -324,6 +325,16 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The palette index for a column that erosion cut {@code cut} blocks down into. The offset is
|
||||
* clamped to the deepest authored layer so an exposed bank shows soil rather than raw rock,
|
||||
* while a depth past the authored soil still runs off the end and falls through to rock. A
|
||||
* cut of zero collapses to {@code depth}, leaving every uncut column untouched.
|
||||
*/
|
||||
static int strataIndex(int depth, int cut, int paletteSize) {
|
||||
return depth + Math.min(cut, Math.max(0, paletteSize - 1 - depth));
|
||||
}
|
||||
|
||||
/**
|
||||
* The river material that owns this column's top layers, or null when the biome layers stand.
|
||||
* Roles are exclusive: a channel bed, then the shore strip, then the eroded bank.
|
||||
|
||||
@@ -75,6 +75,10 @@ public class CachedStream2D<T> extends BasicStream<T> implements ProceduralStrea
|
||||
return cache.getMaxSize();
|
||||
}
|
||||
|
||||
public void setMaximumChunks(int maximumChunks) {
|
||||
cache.setMaximumChunks(maximumChunks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClosed() {
|
||||
return engine.isClosed();
|
||||
|
||||
@@ -22,6 +22,7 @@ import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.biome.IrisBiomeCustom;
|
||||
import art.arcane.iris.generation.biome.IrisCustomBiomeAliasResolver;
|
||||
import art.arcane.iris.generation.biome.IrisFloatingChildBiomes;
|
||||
import art.arcane.iris.generation.biome.IrisSurfaceLayerFallback;
|
||||
import art.arcane.iris.generation.cave.IrisCaveProfile;
|
||||
import art.arcane.iris.generation.decoration.IrisDepositGenerator;
|
||||
import art.arcane.iris.generation.decoration.IrisDepositVariant;
|
||||
@@ -323,6 +324,8 @@ public class IrisDimension extends IrisRegistrant {
|
||||
private KList<IrisShapedGeneratorStyle> overlayNoise = new KList<>();
|
||||
@Description("The palette of blocks for 'stone'")
|
||||
private IrisMaterialPalette rockPalette = new IrisMaterialPalette().qclear().qadd("stone");
|
||||
@Description("What a column surfaces with when every biome layer is rejected. Biomes may override this.")
|
||||
private IrisSurfaceLayerFallback surfaceLayerFallback = IrisSurfaceLayerFallback.ROCK;
|
||||
@Description("The dimension fluid block palette used for ocean columns and cave aquifers.")
|
||||
private IrisMaterialPalette fluidPalette = new IrisMaterialPalette().qclear().qadd("water");
|
||||
@Description("Collection of ores to be generated")
|
||||
|
||||
@@ -127,6 +127,10 @@ public class IrisRegion extends IrisRegistrant implements Rarity {
|
||||
@MinNumber(0.0001)
|
||||
@Description("The varience of the shore height")
|
||||
private double shoreHeightZoom = 3.14;
|
||||
@MinNumber(0)
|
||||
@MaxNumber(64)
|
||||
@Description("The minimum width of the shore in blocks. Shore height alone is a vertical window, so a steep coast gets almost no beach; above zero the shore band also climbs with the local slope to stay this wide across the ground.")
|
||||
private double shoreMinimumWidth = 0;
|
||||
@MinNumber(0.0001)
|
||||
@Description("How large land biomes are in this region")
|
||||
private double landBiomeZoom = 1;
|
||||
|
||||
@@ -899,7 +899,7 @@ public class ServerConfigurator {
|
||||
try (Stream<Path> children = Files.list(root)) {
|
||||
for (Path child : children.toList()) {
|
||||
String childName = child.getFileName().toString();
|
||||
if (PackDirectoryResolver.isHiddenName(childName) || isGeneratedPackFile(childName)) {
|
||||
if (PackDirectoryResolver.isHiddenName(childName) || isNonContentPackFile(childName)) {
|
||||
continue;
|
||||
}
|
||||
if (Files.isSymbolicLink(child)) {
|
||||
@@ -953,7 +953,7 @@ public class ServerConfigurator {
|
||||
String fileName = file.getFileName().toString();
|
||||
if ((treeRoot.relativize(file).getNameCount() == 1
|
||||
&& PackDirectoryResolver.isHiddenName(fileName))
|
||||
|| isGeneratedPackFile(fileName)) {
|
||||
|| isNonContentPackFile(fileName)) {
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
if (attributes.isSymbolicLink() || Files.isSymbolicLink(file)) {
|
||||
@@ -981,8 +981,8 @@ public class ServerConfigurator {
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean isGeneratedPackFile(String name) {
|
||||
return name != null && name.endsWith(CODE_WORKSPACE_SUFFIX);
|
||||
private static boolean isNonContentPackFile(String name) {
|
||||
return name != null && (name.equals(".DS_Store") || name.endsWith(CODE_WORKSPACE_SUFFIX));
|
||||
}
|
||||
|
||||
private record FingerprintEntry(
|
||||
|
||||
@@ -77,6 +77,7 @@ import art.arcane.iris.generation.context.IrisContext;
|
||||
import art.arcane.volmlib.util.io.ReactiveFolder;
|
||||
import art.arcane.volmlib.util.scheduling.ChronoLatch;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.iris.world.runtime.WorldRuntimeControlService;
|
||||
import art.arcane.volmlib.util.scheduling.Looper;
|
||||
import io.papermc.lib.PaperLib;
|
||||
import lombok.Data;
|
||||
@@ -98,6 +99,7 @@ import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -121,6 +123,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
private static final int LOAD_LOCKS = Runtime.getRuntime().availableProcessors() * 4;
|
||||
private static final long HOTLOAD_LOOP_DELAY_MS = 250L;
|
||||
private static final long HOTLOAD_MAINTENANCE_DELAY_MS = 4000L;
|
||||
private static final int PLAYER_ENTRY_MOVE_PADDING = 3;
|
||||
private final GenerationStageGate loadLock;
|
||||
private final IrisWorld world;
|
||||
private final File dataLocation;
|
||||
@@ -153,6 +156,8 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
private volatile CompletableFuture<Void> startupContentReady = CompletableFuture.completedFuture(null);
|
||||
private volatile CompletableFuture<Void> startupReady = CompletableFuture.completedFuture(null);
|
||||
private volatile boolean closing;
|
||||
private volatile boolean initialEntryPending;
|
||||
private volatile boolean initialPlayerEntryPending;
|
||||
@Setter
|
||||
private volatile StudioGenerator studioGenerator;
|
||||
|
||||
@@ -391,9 +396,9 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
}
|
||||
int spawnX = spawn.getBlockX();
|
||||
int spawnZ = spawn.getBlockZ();
|
||||
if (studio && expectedEngine.getDimension().getStudioMode() == StudioMode.NORMAL
|
||||
if ((initialEntryPending || studio && expectedEngine.getDimension().getStudioMode() == StudioMode.NORMAL)
|
||||
&& expectedEngine instanceof IrisEngine irisEngine) {
|
||||
irisEngine.startStudioEntryHydrology(spawnX, spawnZ);
|
||||
irisEngine.startEntryHydrology(spawnX, spawnZ);
|
||||
return;
|
||||
}
|
||||
int reach = Math.max(16, hydrology.settings().routing().tileSize() / 2);
|
||||
@@ -411,9 +416,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
private void updateSpawnLocation(World world) {
|
||||
try {
|
||||
Location initialSpawn = getInitialSpawnLocation(world);
|
||||
int chunkX = initialSpawn.getBlockX() >> 4;
|
||||
int chunkZ = initialSpawn.getBlockZ() >> 4;
|
||||
CompletableFuture<Chunk> chunkFuture = requestChunkAsync(world, chunkX, chunkZ, true);
|
||||
CompletableFuture<Chunk> chunkFuture = requestInitialSpawnChunks(world, initialSpawn);
|
||||
if (chunkFuture == null) {
|
||||
initialSpawnReady.completeExceptionally(new IllegalStateException(
|
||||
"Initial spawn chunk request returned no completion future for world \""
|
||||
@@ -454,6 +457,72 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
}
|
||||
}
|
||||
|
||||
private CompletableFuture<Chunk> requestInitialSpawnChunks(World world, Location initialSpawn) {
|
||||
int chunkX = initialSpawn.getBlockX() >> 4;
|
||||
int chunkZ = initialSpawn.getBlockZ() >> 4;
|
||||
if (!initialEntryPending) {
|
||||
return requestChunkAsync(world, chunkX, chunkZ, true);
|
||||
}
|
||||
WorldRuntimeControlService runtime = WorldRuntimeControlService.get();
|
||||
CompletableFuture<Chunk> canonical = runtime.requestChunkAsync(world, chunkX, chunkZ, true, true);
|
||||
if (canonical == null || !initialPlayerEntryPending) {
|
||||
return canonical;
|
||||
}
|
||||
List<CompletableFuture<Chunk>> requests = new ArrayList<>(4);
|
||||
requests.add(canonical);
|
||||
int minimumX = (initialSpawn.getBlockX() - PLAYER_ENTRY_MOVE_PADDING) >> 4;
|
||||
int maximumX = (initialSpawn.getBlockX() + PLAYER_ENTRY_MOVE_PADDING) >> 4;
|
||||
int minimumZ = (initialSpawn.getBlockZ() - PLAYER_ENTRY_MOVE_PADDING) >> 4;
|
||||
int maximumZ = (initialSpawn.getBlockZ() + PLAYER_ENTRY_MOVE_PADDING) >> 4;
|
||||
for (int x = minimumX; x <= maximumX; x++) {
|
||||
for (int z = minimumZ; z <= maximumZ; z++) {
|
||||
if (x == chunkX && z == chunkZ) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
CompletableFuture<Chunk> request = runtime.requestChunkAsync(world, x, z, true, true);
|
||||
if (request == null) {
|
||||
throw new IllegalStateException("Initial player entry chunk request returned no completion future for world \""
|
||||
+ world.getName() + "\" at " + x + "," + z + ".");
|
||||
}
|
||||
requests.add(request.thenApply(chunk -> Objects.requireNonNull(chunk,
|
||||
"Initial player entry chunk request completed without a chunk.")));
|
||||
} catch (RuntimeException | Error failure) {
|
||||
requests.add(CompletableFuture.failedFuture(failure));
|
||||
}
|
||||
}
|
||||
}
|
||||
CompletableFuture<Chunk> prepared = new CompletableFuture<>();
|
||||
CompletableFuture.allOf(requests.toArray(CompletableFuture<?>[]::new)).whenComplete((ignored, failure) -> {
|
||||
if (failure == null) {
|
||||
prepared.complete(canonical.getNow(null));
|
||||
return;
|
||||
}
|
||||
Throwable firstFailure = null;
|
||||
IdentityHashMap<Throwable, Boolean> failures = new IdentityHashMap<>();
|
||||
for (CompletableFuture<Chunk> request : requests) {
|
||||
Throwable requestFailure = request.handle((chunk, cause) -> cause).getNow(null);
|
||||
if (requestFailure == null) {
|
||||
continue;
|
||||
}
|
||||
Throwable distinctCause = requestFailure;
|
||||
while (distinctCause instanceof CompletionException && distinctCause.getCause() != null) {
|
||||
distinctCause = distinctCause.getCause();
|
||||
}
|
||||
if (failures.put(distinctCause, Boolean.TRUE) != null) {
|
||||
continue;
|
||||
}
|
||||
if (firstFailure == null) {
|
||||
firstFailure = request == canonical ? requestFailure : distinctCause;
|
||||
} else {
|
||||
firstFailure.addSuppressed(distinctCause);
|
||||
}
|
||||
}
|
||||
prepared.completeExceptionally(firstFailure);
|
||||
});
|
||||
return prepared;
|
||||
}
|
||||
|
||||
private void completeSpawnLocation(World world, Location initialSpawn) {
|
||||
try {
|
||||
applySpawnLocation(world, initialSpawn);
|
||||
@@ -515,8 +584,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
lastJigsawStudioRequestId = null;
|
||||
if (generationHistory != null) {
|
||||
try {
|
||||
generationHistory.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
|
||||
targetCache.reset();
|
||||
prepareGenerationHistoryTarget(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Unable to prepare saved Iris terrain for the current generator.", failure);
|
||||
}
|
||||
@@ -568,12 +636,41 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
isAuthoringStudio(),
|
||||
studioEntryBootstrapActive.get(),
|
||||
initializationFailure != null));
|
||||
engine = createdEngine;
|
||||
publishInitializedEngine(createdEngine);
|
||||
populators.clear();
|
||||
targetCache.reset();
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void publishInitializedEngine(IrisEngine initializedEngine) {
|
||||
if (initialEntryPending) {
|
||||
setInitialEntryNeighbourPrefetch(initializedEngine, false);
|
||||
}
|
||||
engine = initializedEngine;
|
||||
}
|
||||
|
||||
void prepareGenerationHistoryTarget(int transitionWidthBlocks) throws IOException {
|
||||
generationHistory.prepareCurrentGenerator(transitionWidthBlocks);
|
||||
EngineTarget cachedTarget = targetCache.getIfPresent();
|
||||
if (cachedTarget == null) {
|
||||
return;
|
||||
}
|
||||
Path activePackRoot = generationHistory.activePackRoot();
|
||||
IrisData cachedData = cachedTarget.getData();
|
||||
if (!cachedData.isClosed() && cachedData.getDataFolder().toPath().equals(activePackRoot)) {
|
||||
return;
|
||||
}
|
||||
targetCache.reset();
|
||||
EngineTarget pendingTarget = startupTarget;
|
||||
Engine activeEngine = engine;
|
||||
if (!cachedData.isClosed()
|
||||
&& cachedData.getEngines().isEmpty()
|
||||
&& (pendingTarget == null || pendingTarget.getData() != cachedData)
|
||||
&& (activeEngine == null || activeEngine.getData() != cachedData)) {
|
||||
cachedData.close();
|
||||
}
|
||||
}
|
||||
|
||||
private EngineTarget loadActiveGenerationHistoryTarget() throws IOException {
|
||||
Path packRoot = generationHistory.activePackRoot();
|
||||
IrisData data = IrisData.openRuntime(packRoot.toFile());
|
||||
@@ -604,7 +701,8 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
}
|
||||
|
||||
private IrisEngine createEngine(EngineTarget engineTarget) {
|
||||
IrisEngine.InitializationMode mode = selectInitializationMode(studio, jigsawStudioActive, objectStudioActive);
|
||||
IrisEngine.InitializationMode mode = selectInitializationMode(studio, jigsawStudioActive, objectStudioActive,
|
||||
initialEntryPending);
|
||||
if (generationHistory == null) {
|
||||
return new IrisEngine(engineTarget, mode);
|
||||
}
|
||||
@@ -638,9 +736,10 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
return new IllegalStateException("Unable to attach Iris generation history.", failure);
|
||||
}
|
||||
|
||||
static IrisEngine.InitializationMode selectInitializationMode(boolean studio, boolean jigsawStudioActive, boolean objectStudioActive) {
|
||||
static IrisEngine.InitializationMode selectInitializationMode(boolean studio, boolean jigsawStudioActive,
|
||||
boolean objectStudioActive, boolean initialEntry) {
|
||||
if (!studio) {
|
||||
return IrisEngine.InitializationMode.RUNTIME;
|
||||
return initialEntry ? IrisEngine.InitializationMode.WORLD_CREATION : IrisEngine.InitializationMode.RUNTIME;
|
||||
}
|
||||
return jigsawStudioActive
|
||||
? IrisEngine.InitializationMode.JIGSAW_STUDIO
|
||||
@@ -864,12 +963,14 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
// reject generation for a world that may still be loaded.
|
||||
closing = false;
|
||||
}
|
||||
completeInitialEntry();
|
||||
closeFuture.compareAndSet(future, null);
|
||||
future.completeExceptionally(throwable);
|
||||
return future;
|
||||
}
|
||||
|
||||
operation.whenComplete((ignored, throwable) -> {
|
||||
completeInitialEntry();
|
||||
if (throwable == null) {
|
||||
future.complete(null);
|
||||
} else {
|
||||
@@ -898,6 +999,36 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
return studio;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void beginInitialEntry(boolean playerEntry) {
|
||||
if (studio || closing || engine != null) {
|
||||
throw new IllegalStateException("Initial world entry must begin before a normal generator initializes.");
|
||||
}
|
||||
initialEntryPending = true;
|
||||
initialPlayerEntryPending = playerEntry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void completeInitialEntry() {
|
||||
if (!initialEntryPending) {
|
||||
return;
|
||||
}
|
||||
initialEntryPending = false;
|
||||
initialPlayerEntryPending = false;
|
||||
setInitialEntryNeighbourPrefetch(engine, true);
|
||||
}
|
||||
|
||||
private void setInitialEntryNeighbourPrefetch(Engine activeEngine, boolean enabled) {
|
||||
if (activeEngine == null || activeEngine.isClosed()) {
|
||||
return;
|
||||
}
|
||||
IrisComplex complex = activeEngine.getComplex();
|
||||
IrisHydrologyRuntime hydrology = complex == null ? null : complex.getHydrologyRuntime();
|
||||
if (hydrology != null) {
|
||||
hydrology.setNeighbourPrefetchEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
public void endStudioEntryBootstrap() {
|
||||
Engine activeEngine = engine;
|
||||
if (activeEngine instanceof IrisEngine irisEngine) {
|
||||
|
||||
@@ -77,6 +77,10 @@ public interface PlatformChunkGenerator extends Hotloadable, DataProvider {
|
||||
|
||||
CompletableFuture<Integer> getSpawnChunks();
|
||||
|
||||
void beginInitialEntry(boolean playerEntry);
|
||||
|
||||
void completeInitialEntry();
|
||||
|
||||
default CompletableFuture<Void> getInitialSpawnReady() {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ public class StudioSVC implements IrisService {
|
||||
queueStudioWorldDeletionOnStartup(worldNamesToDelete);
|
||||
}
|
||||
|
||||
public IrisDimension installIntoWorld(
|
||||
public GenerationPublication installIntoWorld(
|
||||
VolmitSender sender,
|
||||
IrisDimension dimension,
|
||||
File dimensionRoot,
|
||||
@@ -220,7 +220,8 @@ public class StudioSVC implements IrisService {
|
||||
File dimensionRoot,
|
||||
long worldSeed
|
||||
) {
|
||||
return publishGenerationHistory(sender, dimension, dimensionRoot, worldSeed, true);
|
||||
GenerationPublication publication = publishGenerationHistory(sender, dimension, dimensionRoot, worldSeed, true);
|
||||
return publication == null ? null : publication.dimension();
|
||||
}
|
||||
|
||||
public IrisDimension installIntoTransientWorld(
|
||||
@@ -235,7 +236,7 @@ public class StudioSVC implements IrisService {
|
||||
return installIntoDirectory(sender, dimension, folder, true);
|
||||
}
|
||||
|
||||
private IrisDimension publishGenerationHistory(
|
||||
private GenerationPublication publishGenerationHistory(
|
||||
VolmitSender sender,
|
||||
IrisDimension dimension,
|
||||
File dimensionRoot,
|
||||
@@ -288,11 +289,11 @@ public class StudioSVC implements IrisService {
|
||||
Path installedPack = stageUpdate && history.pendingActivation().isPresent()
|
||||
? history.packRoot(history.pendingActivation().orElseThrow().activationId())
|
||||
: history.activePackRoot();
|
||||
IrisDimension installed = loadInstalledDimension(installedPack, dimensionKey);
|
||||
IrisDimension installed = loadInstalledDimension(installedPack, source, dimensionKey);
|
||||
if (restartRequired) {
|
||||
ServerConfigurator.restart("An Iris generation epoch update is pending activation.");
|
||||
}
|
||||
return installed;
|
||||
return new GenerationPublication(installed, history);
|
||||
} catch (Throwable failure) {
|
||||
IrisLogging.reportError("Failed to publish generation history for dimension '"
|
||||
+ dimensionKey + "' into " + dimensionRoot.getPath(), failure);
|
||||
@@ -383,8 +384,9 @@ public class StudioSVC implements IrisService {
|
||||
}
|
||||
}
|
||||
|
||||
private static IrisDimension loadInstalledDimension(Path packRoot, String dimensionKey) throws IOException {
|
||||
PackValidationResult validation = validatePublishedPack(packRoot);
|
||||
static IrisDimension loadInstalledDimension(Path packRoot, Path validatedSource, String dimensionKey) throws IOException {
|
||||
String copiedFingerprint = ServerConfigurator.computePackTreeFingerprint(packRoot.toFile());
|
||||
PackValidationResult validation = validatePublishedPack(packRoot, validatedSource, copiedFingerprint);
|
||||
if (!validation.isLoadable()) {
|
||||
throw new BrokenPackException(packRoot.toString(), validation.getBlockingErrors());
|
||||
}
|
||||
@@ -1755,6 +1757,13 @@ public class StudioSVC implements IrisService {
|
||||
RESTART
|
||||
}
|
||||
|
||||
public record GenerationPublication(IrisDimension dimension, GenerationHistory history) {
|
||||
public GenerationPublication {
|
||||
Objects.requireNonNull(dimension, "Published dimension");
|
||||
Objects.requireNonNull(history, "Published generation history");
|
||||
}
|
||||
}
|
||||
|
||||
private record GenerationCandidate(
|
||||
String packFingerprint,
|
||||
GenerationEpoch.DimensionContract dimensionContract,
|
||||
|
||||
@@ -20,6 +20,7 @@ package art.arcane.iris.world;
|
||||
|
||||
import art.arcane.iris.world.runtime.TransientWorldCleanupSupport;
|
||||
import art.arcane.iris.world.runtime.WorldRuntimeControlService;
|
||||
import art.arcane.iris.world.history.GenerationHistory;
|
||||
import com.google.common.util.concurrent.AtomicDouble;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
@@ -125,7 +126,7 @@ public class IrisCreator {
|
||||
private boolean benchmark = false;
|
||||
private BiConsumer<Double, String> studioProgressConsumer;
|
||||
private BiConsumer<String, Long> studioTimingConsumer;
|
||||
private DatapackPreparation datapackPreparation = DatapackPreparation.INSTALL_IF_CHANGED;
|
||||
private DatapackPreparation datapackPreparation = DatapackPreparation.REUSE_LOADED_RUNTIME_IF_READY;
|
||||
|
||||
public static boolean removeFromBukkitYml(NamespacedKey worldKey) throws IOException {
|
||||
return BukkitWorldConfiguration.remove(
|
||||
@@ -258,9 +259,20 @@ public class IrisCreator {
|
||||
|
||||
reportCreationProgress(creationReporter, 0.26D, "prepare_world_pack");
|
||||
StudioSVC studioService = IrisServices.get(StudioSVC.class);
|
||||
IrisDimension installedDimension = benchmark && !studio
|
||||
? studioService.installIntoTransientWorld(sender, resolvedDimension, dimensionRoot)
|
||||
: studioService.installIntoWorld(sender, resolvedDimension, dimensionRoot, seed);
|
||||
long packPreparationStart = System.nanoTime();
|
||||
GenerationHistory publishedHistory = null;
|
||||
IrisDimension installedDimension;
|
||||
if (benchmark && !studio) {
|
||||
installedDimension = studioService.installIntoTransientWorld(sender, resolvedDimension, dimensionRoot);
|
||||
} else {
|
||||
StudioSVC.GenerationPublication publication = studioService.installIntoWorld(
|
||||
sender, resolvedDimension, dimensionRoot, seed);
|
||||
installedDimension = publication == null ? null : publication.dimension();
|
||||
if (publication != null) {
|
||||
publishedHistory = publication.history();
|
||||
}
|
||||
}
|
||||
reportStudioTiming("prepare_world_pack", packPreparationStart);
|
||||
if (installedDimension == null) {
|
||||
throw new IrisException("Failed to install dimension pack for " + dimension());
|
||||
}
|
||||
@@ -276,14 +288,17 @@ public class IrisCreator {
|
||||
AtomicDouble pp = new AtomicDouble(0);
|
||||
AtomicBoolean done = new AtomicBoolean(false);
|
||||
long generatorPrepareStart = System.nanoTime();
|
||||
WorldCreator wc = new IrisWorldCreator()
|
||||
IrisWorldCreator worldCreator = new IrisWorldCreator()
|
||||
.dimension(installedDimension)
|
||||
.studioPackSource(resolvedDimension.getLoader().getDataFolder())
|
||||
.name(name)
|
||||
.seed(seed)
|
||||
.studio(studio)
|
||||
.persistent(!studio && !benchmark)
|
||||
.create();
|
||||
.persistent(!studio && !benchmark);
|
||||
if (publishedHistory != null) {
|
||||
worldCreator.generationHistory(publishedHistory);
|
||||
}
|
||||
WorldCreator wc = worldCreator.create();
|
||||
reportStudioTiming("prepare_studio_generator", generatorPrepareStart);
|
||||
reportStudioProgress(0.40D, "install_datapacks");
|
||||
|
||||
@@ -292,6 +307,9 @@ public class IrisCreator {
|
||||
throw new IrisException("Access is null. Something bad happened.");
|
||||
}
|
||||
stagedGenerator = access;
|
||||
if (!studio && !benchmark) {
|
||||
access.beginInitialEntry(sender != null && sender.isPlayer());
|
||||
}
|
||||
AtomicInteger createProgressTask = startCreateProgressReporter(access, done, creationReporter);
|
||||
|
||||
reportStudioProgress(0.46D, "create_world");
|
||||
@@ -366,6 +384,7 @@ public class IrisCreator {
|
||||
}
|
||||
reportCreationProgress(creationReporter, 0.92D, "teleport_player");
|
||||
awaitSenderTeleport(world);
|
||||
access.completeInitialEntry();
|
||||
|
||||
if (pregen != null) {
|
||||
CompletableFuture<Boolean> ff = new CompletableFuture<>();
|
||||
@@ -401,6 +420,10 @@ public class IrisCreator {
|
||||
throw irisException;
|
||||
}
|
||||
throw new IrisException("Failed to create world \"" + name + "\".", failure);
|
||||
} finally {
|
||||
if (stagedGenerator != null) {
|
||||
stagedGenerator.completeInitialEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.bukkit.generator.ChunkGenerator;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
public class IrisWorldCreator {
|
||||
@@ -40,6 +41,7 @@ public class IrisWorldCreator {
|
||||
private long seed = 1337;
|
||||
private boolean persistent;
|
||||
private File studioPackSource;
|
||||
private GenerationHistory generationHistory;
|
||||
|
||||
public IrisWorldCreator() {
|
||||
|
||||
@@ -87,6 +89,11 @@ public class IrisWorldCreator {
|
||||
return this;
|
||||
}
|
||||
|
||||
public IrisWorldCreator generationHistory(GenerationHistory history) {
|
||||
this.generationHistory = Objects.requireNonNull(history, "Generation history");
|
||||
return this;
|
||||
}
|
||||
|
||||
public WorldCreator create() {
|
||||
IrisDimension dim = dimension == null ? IrisData.loadAnyDimension(dimensionName, null) : dimension;
|
||||
NamespacedKey worldKey = IrisWorldStorage.keyFromName(name);
|
||||
@@ -106,42 +113,46 @@ public class IrisWorldCreator {
|
||||
.seed(seed)
|
||||
.worldFolder(worldFolder)
|
||||
.build();
|
||||
GenerationHistory generationHistory = persistent || studio
|
||||
? requireGenerationHistory(w.worldFolder(), seed)
|
||||
: null;
|
||||
GenerationHistory history = resolveGenerationHistory(w.worldFolder());
|
||||
File packRoot = studio
|
||||
? Objects.requireNonNull(studioPackSource, "Studio authoring source")
|
||||
: generationHistory != null
|
||||
? requireActivePack(generationHistory)
|
||||
: history != null
|
||||
? history.paths().packRoot(history.activeEpoch().epochId()).toFile()
|
||||
: new File(w.worldFolder(), "iris/pack");
|
||||
ChunkGenerator g = new BukkitChunkGenerator(
|
||||
w,
|
||||
studio,
|
||||
packRoot,
|
||||
dimensionName,
|
||||
generationHistory);
|
||||
history);
|
||||
|
||||
return creator.environment(environment)
|
||||
.generateStructures(true)
|
||||
.generator(g).seed(seed);
|
||||
}
|
||||
|
||||
private static GenerationHistory requireGenerationHistory(File dimensionRoot, long seed) {
|
||||
GenerationHistory resolveGenerationHistory(File dimensionRoot) {
|
||||
if (!persistent && !studio) {
|
||||
if (generationHistory != null) {
|
||||
throw new IllegalStateException("Generation history requires a persistent or Studio world.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Path root = Objects.requireNonNull(dimensionRoot, "Dimension root").toPath().toAbsolutePath().normalize();
|
||||
try {
|
||||
return GenerationHistory.open(dimensionRoot.toPath(), seed);
|
||||
if (generationHistory == null) {
|
||||
return GenerationHistory.open(root, seed);
|
||||
}
|
||||
if (!generationHistory.paths().dimensionRoot().equals(root)) {
|
||||
throw new IllegalArgumentException("Generation history belongs to a different dimension root.");
|
||||
}
|
||||
generationHistory.requireWorldSeed(seed);
|
||||
return generationHistory;
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Iris generation history is unusable at " + dimensionRoot + ".", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static File requireActivePack(GenerationHistory history) {
|
||||
try {
|
||||
return history.activePackRoot().toFile();
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Iris active generation pack is unusable.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
private World.Environment findEnvironment() {
|
||||
IrisDimension dim = dimension == null ? IrisData.loadAnyDimension(dimensionName, null) : dimension;
|
||||
return BukkitEnvironment.from(dim == null ? null : dim.getEnvironment());
|
||||
|
||||
@@ -488,7 +488,8 @@ public final class IrisWorldStorage {
|
||||
.toAbsolutePath()
|
||||
.normalize();
|
||||
try {
|
||||
return GenerationHistory.open(root).activePackRoot().toFile();
|
||||
GenerationHistory history = GenerationHistory.open(root);
|
||||
return history.paths().packRoot(history.activeEpoch().epochId()).toFile();
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Iris generation history is unusable at " + root + ".", failure);
|
||||
}
|
||||
@@ -500,7 +501,8 @@ public final class IrisWorldStorage {
|
||||
.toAbsolutePath()
|
||||
.normalize();
|
||||
try {
|
||||
return GenerationHistory.open(root, expectedWorldSeed).activePackRoot().toFile();
|
||||
GenerationHistory history = GenerationHistory.open(root, expectedWorldSeed);
|
||||
return history.paths().packRoot(history.activeEpoch().epochId()).toFile();
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Iris generation history is unusable at " + root + ".", failure);
|
||||
}
|
||||
|
||||
@@ -180,19 +180,17 @@ public class IrisWorlds {
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops registry entries whose pack snapshot no longer backs them.
|
||||
* Drops registry entries whose canonical world directory no longer exists.
|
||||
* <p>
|
||||
* A world whose storage is present but unusable is kept and reported instead of dropped: the entry is
|
||||
* what lets {@code /iris remove} find the world, and dropping it would not stop the server from loading
|
||||
* the folder anyway. It is excluded from {@link #getDimensions()} so one broken world cannot fail the
|
||||
* whole registry - that failure used to propagate out of the constructor and null out {@link #get()}.
|
||||
* the folder anyway. Pack history and dimension definitions are validated by {@link #getDimensions()}
|
||||
* when callers load them, rather than while reading or saving the identity registry.
|
||||
*/
|
||||
public synchronized void clean() {
|
||||
boolean removed = worlds.entrySet().removeIf(entry -> {
|
||||
try {
|
||||
Optional<File> packRoot = packRoot(entry.getKey());
|
||||
return packRoot.isEmpty()
|
||||
|| !new File(packRoot.get(), "dimensions/" + entry.getValue() + ".json").exists();
|
||||
return dimensionRoot(entry.getKey()).isEmpty();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return true;
|
||||
} catch (IllegalStateException e) {
|
||||
@@ -353,7 +351,7 @@ public class IrisWorlds {
|
||||
return null;
|
||||
}
|
||||
|
||||
private Optional<File> packRoot(String worldIdentity) {
|
||||
private Optional<File> dimensionRoot(String worldIdentity) {
|
||||
NamespacedKey worldKey = WorldIdentity.parse(worldIdentity);
|
||||
Path worldContainer = levelRoot.getParent();
|
||||
if (worldContainer == null) {
|
||||
@@ -363,13 +361,12 @@ public class IrisWorlds {
|
||||
worldKey,
|
||||
levelRoot.getFileName().toString()
|
||||
);
|
||||
Optional<File> dimensionRoot = IrisWorldStorage.frozenDimensionRoot(
|
||||
return IrisWorldStorage.frozenDimensionRoot(
|
||||
worldContainer.toFile(),
|
||||
levelRoot.toFile(),
|
||||
configuredWorldName,
|
||||
worldKey
|
||||
);
|
||||
return dimensionRoot.map(IrisWorldStorage::requireActiveGenerationPackRoot);
|
||||
}
|
||||
|
||||
private static void warnUnusableStorage(String worldIdentity, IllegalStateException failure) {
|
||||
@@ -385,7 +382,9 @@ public class IrisWorlds {
|
||||
private IrisDimension loadDimension(String worldIdentity, String id) {
|
||||
File pack;
|
||||
try {
|
||||
pack = packRoot(worldIdentity).orElse(null);
|
||||
pack = dimensionRoot(worldIdentity)
|
||||
.map(IrisWorldStorage::requireActiveGenerationPackRoot)
|
||||
.orElse(null);
|
||||
} catch (IllegalStateException unusableStorage) {
|
||||
warnUnusableStorage(worldIdentity, unusableStorage);
|
||||
return null;
|
||||
|
||||
@@ -18,7 +18,6 @@ import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -26,7 +25,6 @@ import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class GenerationHistory {
|
||||
private static final int MAXIMUM_CACHED_BOUNDARIES = 4;
|
||||
@@ -54,7 +52,8 @@ public final class GenerationHistory {
|
||||
GenerationPackRepository packs,
|
||||
GenerationHistoryStore store,
|
||||
ChunkGenerationOwnership ownership,
|
||||
GenerationKernelRegistry kernels
|
||||
GenerationKernelRegistry kernels,
|
||||
Optional<FreshPublication> publication
|
||||
) throws IOException {
|
||||
this.paths = Objects.requireNonNull(paths, "paths");
|
||||
this.packs = Objects.requireNonNull(packs, "packs");
|
||||
@@ -68,7 +67,7 @@ public final class GenerationHistory {
|
||||
this.admission = new GenerationAdmission(paths.dimensionRoot());
|
||||
this.boundaryCache = boundedCache(MAXIMUM_CACHED_BOUNDARIES);
|
||||
this.terrainSignatureCache = boundedCache(MAXIMUM_CACHED_TERRAIN_SIGNATURES);
|
||||
validateReferencedState();
|
||||
validateReferencedState(publication);
|
||||
}
|
||||
|
||||
public GenerationAdmission.RuntimeLease retainRuntime() {
|
||||
@@ -132,7 +131,7 @@ public final class GenerationHistory {
|
||||
);
|
||||
requireSourceFingerprint(packSource, epoch.packFingerprint(), epoch.packFingerprintVersion());
|
||||
GenerationPackRepository packs = new GenerationPackRepository(paths.dimensionRoot());
|
||||
packs.publish(
|
||||
Path publishedPack = packs.publish(
|
||||
epoch.epochId(),
|
||||
epoch.packFingerprint(),
|
||||
epoch.packFingerprintVersion(),
|
||||
@@ -141,7 +140,8 @@ public final class GenerationHistory {
|
||||
forceEpochPublication(paths, epoch.epochId());
|
||||
GenerationSemanticIndex.initialize(paths.dimensionRoot());
|
||||
GenerationHistoryStore store = GenerationHistoryStore.initialize(paths.generationRoot(), epoch);
|
||||
return new GenerationHistory(paths, packs, store, ownership, kernels);
|
||||
return new GenerationHistory(paths, packs, store, ownership, kernels,
|
||||
Optional.of(new FreshPublication(publishedPack, epoch)));
|
||||
}
|
||||
|
||||
public static GenerationHistory open(Path dimensionRoot) throws IOException {
|
||||
@@ -163,7 +163,7 @@ public final class GenerationHistory {
|
||||
GenerationPackRepository packs = new GenerationPackRepository(paths.dimensionRoot());
|
||||
GenerationHistoryStore store = GenerationHistoryStore.open(paths.generationRoot());
|
||||
ChunkGenerationOwnership ownership = ChunkGenerationOwnership.load(paths.ownershipRoot());
|
||||
return new GenerationHistory(paths, packs, store, ownership, kernels);
|
||||
return new GenerationHistory(paths, packs, store, ownership, kernels, Optional.empty());
|
||||
}
|
||||
|
||||
public static Optional<GenerationHistory> openIfPresent(Path dimensionRoot) throws IOException {
|
||||
@@ -436,7 +436,7 @@ public final class GenerationHistory {
|
||||
BoundarySignatureCapture signatureCapture
|
||||
) throws IOException {
|
||||
WorldChunkInventory inventory = recoverUnstoredClaims();
|
||||
validateReferencedState();
|
||||
validateReferencedState(Optional.empty());
|
||||
Optional<GenerationActivation> pending = store.pendingActivation();
|
||||
if (pending.isEmpty()) {
|
||||
return store.activeActivation();
|
||||
@@ -470,15 +470,15 @@ public final class GenerationHistory {
|
||||
return paths;
|
||||
}
|
||||
|
||||
public synchronized GenerationManifest manifest() {
|
||||
public GenerationManifest manifest() {
|
||||
return store.manifest();
|
||||
}
|
||||
|
||||
public synchronized GenerationActivation activeActivation() {
|
||||
public GenerationActivation activeActivation() {
|
||||
return store.activeActivation();
|
||||
}
|
||||
|
||||
public synchronized GenerationEpoch activeEpoch() {
|
||||
public GenerationEpoch activeEpoch() {
|
||||
return store.activeEpoch();
|
||||
}
|
||||
|
||||
@@ -513,7 +513,7 @@ public final class GenerationHistory {
|
||||
synchronized (this) {
|
||||
if (store.pendingActivation().isEmpty() && usesCurrentGenerator()) {
|
||||
recoverUnstoredClaims();
|
||||
validateReferencedState();
|
||||
validateReferencedState(Optional.empty());
|
||||
} else {
|
||||
if (store.pendingActivation().isEmpty()) {
|
||||
stageCurrentKernel(transitionWidthBlocks);
|
||||
@@ -808,10 +808,14 @@ public final class GenerationHistory {
|
||||
return ownership.explicitChunkCount();
|
||||
}
|
||||
|
||||
private void validateReferencedState() throws IOException {
|
||||
private void validateReferencedState(Optional<FreshPublication> publication) throws IOException {
|
||||
validateRuntimeVersions();
|
||||
GenerationEpoch active = store.activeEpoch();
|
||||
packs.requireExactPack(active.epochId(), active.packFingerprint(), active.packFingerprintVersion());
|
||||
if (publication.isEmpty()
|
||||
|| !publication.get().epoch().equals(active)
|
||||
|| !publication.get().pack().equals(paths.packRoot(active.epochId()))) {
|
||||
packs.requireExactPack(active.epochId(), active.packFingerprint(), active.packFingerprintVersion());
|
||||
}
|
||||
if (store.pendingEpoch().isPresent()) {
|
||||
GenerationEpoch pending = store.pendingEpoch().orElseThrow();
|
||||
packs.requireExactPack(pending.epochId(), pending.packFingerprint(), pending.packFingerprintVersion());
|
||||
@@ -1138,15 +1142,8 @@ public final class GenerationHistory {
|
||||
throw new IOException("Generation epoch is not a safe directory: " + epochRoot);
|
||||
}
|
||||
if (File.separatorChar != '\\') {
|
||||
List<Path> directories;
|
||||
try (Stream<Path> entries = Files.walk(epochRoot)) {
|
||||
directories = entries
|
||||
.filter(path -> Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS))
|
||||
.sorted(Comparator.comparingInt(Path::getNameCount).reversed())
|
||||
.toList();
|
||||
}
|
||||
for (Path directory : directories) {
|
||||
forceDirectory(directory);
|
||||
if (Durability.enabled()) {
|
||||
GenerationPackRepository.forceDirectoryTree(epochRoot, GenerationHistory::forceDirectory);
|
||||
}
|
||||
forceDirectory(paths.epochsRoot());
|
||||
forceDirectory(paths.generationRoot());
|
||||
@@ -1178,6 +1175,9 @@ public final class GenerationHistory {
|
||||
};
|
||||
}
|
||||
|
||||
private record FreshPublication(Path pack, GenerationEpoch epoch) {
|
||||
}
|
||||
|
||||
private static final class PendingSemanticClaim {
|
||||
private final GenerationStage stage;
|
||||
private final GenerationSemanticIndex.Claim claim;
|
||||
|
||||
@@ -18,24 +18,35 @@ import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
public final class GenerationPackFingerprint {
|
||||
public static final int CURRENT_VERSION = 2;
|
||||
private static final int BUFFER_BYTES = 64 * 1_024;
|
||||
private static final int PREFETCH_FILE_BYTES = 256 * 1_024;
|
||||
private static final int PREFETCH_BATCH_FILES = 64;
|
||||
private static final int MAXIMUM_PREFETCH_WORKERS = 4;
|
||||
|
||||
private GenerationPackFingerprint() {
|
||||
}
|
||||
|
||||
public static String compute(Path packRoot, int version) throws IOException {
|
||||
return computeWithInput(packRoot, version, GenerationPackFingerprint::openInput);
|
||||
}
|
||||
|
||||
static String computeWithInput(Path packRoot, int version, InputSource inputSource) throws IOException {
|
||||
requireSupported(version);
|
||||
return switch (version) {
|
||||
case 1 -> computeVersionOne(packRoot);
|
||||
case 2 -> computeTree(packRoot, true);
|
||||
default -> throw new IOException("Unsupported Iris generation pack fingerprint version " + version + ".");
|
||||
};
|
||||
return computeTree(packRoot, version == 2, Objects.requireNonNull(inputSource, "Fingerprint input source"));
|
||||
}
|
||||
|
||||
public static void requireSupported(int version) throws IOException {
|
||||
@@ -44,11 +55,7 @@ public final class GenerationPackFingerprint {
|
||||
}
|
||||
}
|
||||
|
||||
private static String computeVersionOne(Path packRoot) throws IOException {
|
||||
return computeTree(packRoot, false);
|
||||
}
|
||||
|
||||
private static String computeTree(Path packRoot, boolean ignoreFinderMetadata) throws IOException {
|
||||
private static String computeTree(Path packRoot, boolean ignoreFinderMetadata, InputSource inputSource) throws IOException {
|
||||
Path root = Objects.requireNonNull(packRoot, "packRoot").toAbsolutePath().normalize();
|
||||
if (!Files.isDirectory(root, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(root)) {
|
||||
throw new IOException("Generation pack fingerprint root is missing or unsafe: " + root);
|
||||
@@ -59,30 +66,160 @@ public final class GenerationPackFingerprint {
|
||||
entries.sort(Comparator.comparing(FingerprintEntry::relativePath));
|
||||
MessageDigest digest = sha256();
|
||||
byte[] buffer = new byte[BUFFER_BYTES];
|
||||
for (FingerprintEntry entry : entries) {
|
||||
updateEntry(digest, entry.relativePath(), entry.size());
|
||||
long readBytes = 0L;
|
||||
try (InputStream input = Files.newInputStream(
|
||||
entry.source(),
|
||||
StandardOpenOption.READ,
|
||||
LinkOption.NOFOLLOW_LINKS
|
||||
)) {
|
||||
int read;
|
||||
while ((read = input.read(buffer)) >= 0) {
|
||||
if (read > 0) {
|
||||
digest.update(buffer, 0, read);
|
||||
readBytes += read;
|
||||
int workers = Math.min(MAXIMUM_PREFETCH_WORKERS, Runtime.getRuntime().availableProcessors());
|
||||
if (workers <= 1 || !hasPrefetchBatch(entries)) {
|
||||
for (FingerprintEntry entry : entries) {
|
||||
updateEntry(digest, entry.relativePath(), entry.size());
|
||||
streamEntry(digest, entry, buffer, inputSource);
|
||||
}
|
||||
} else {
|
||||
try (ExecutorService readers = Executors.newFixedThreadPool(workers,
|
||||
Thread.ofPlatform().name("Iris Pack Fingerprint-", 0L).factory())) {
|
||||
for (int start = 0; start < entries.size(); start += PREFETCH_BATCH_FILES) {
|
||||
List<FingerprintEntry> batch = entries.subList(start,
|
||||
Math.min(entries.size(), start + PREFETCH_BATCH_FILES));
|
||||
PrefetchedBatch prefetched = prefetchBatch(batch, inputSource, readers, workers, root);
|
||||
for (int index = 0; index < batch.size(); index++) {
|
||||
FingerprintEntry entry = batch.get(index);
|
||||
try {
|
||||
updateEntry(digest, entry.relativePath(), entry.size());
|
||||
requireSuccessfulRead(prefetched.failures()[index]);
|
||||
byte[] bytes = prefetched.contents()[index];
|
||||
if (bytes == null) {
|
||||
streamEntry(digest, entry, buffer, inputSource);
|
||||
} else {
|
||||
digest.update(bytes);
|
||||
}
|
||||
} catch (IOException | RuntimeException | Error failure) {
|
||||
suppressReadFailures(failure, prefetched.failures());
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (readBytes != entry.size()) {
|
||||
throw new IOException("Iris generation pack changed while fingerprinting: "
|
||||
+ entry.source());
|
||||
}
|
||||
}
|
||||
return HexFormat.of().formatHex(digest.digest());
|
||||
}
|
||||
|
||||
private static boolean hasPrefetchBatch(List<FingerprintEntry> entries) {
|
||||
int smallFiles = 0;
|
||||
for (FingerprintEntry entry : entries) {
|
||||
if (entry.size() <= PREFETCH_FILE_BYTES && ++smallFiles >= PREFETCH_BATCH_FILES) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static PrefetchedBatch prefetchBatch(List<FingerprintEntry> entries, InputSource inputSource,
|
||||
ExecutorService readers, int workers, Path root) throws IOException {
|
||||
PrefetchedBatch batch = new PrefetchedBatch(new byte[entries.size()][], new Throwable[entries.size()]);
|
||||
List<Callable<Void>> tasks = new ArrayList<>(workers);
|
||||
for (int worker = 0; worker < workers; worker++) {
|
||||
int first = worker;
|
||||
tasks.add(() -> {
|
||||
readBatch(entries, batch, inputSource, first, workers);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
try {
|
||||
for (Future<Void> task : readers.invokeAll(tasks)) {
|
||||
task.get();
|
||||
}
|
||||
} catch (InterruptedException failure) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("Interrupted while fingerprinting " + root, failure);
|
||||
} catch (ExecutionException failure) {
|
||||
throw new IOException("Unable to prefetch generation pack " + root, failure.getCause());
|
||||
}
|
||||
return batch;
|
||||
}
|
||||
|
||||
private static void readBatch(List<FingerprintEntry> entries, PrefetchedBatch batch,
|
||||
InputSource inputSource, int first, int workers) {
|
||||
for (int index = first; index < entries.size(); index += workers) {
|
||||
FingerprintEntry entry = entries.get(index);
|
||||
if (entry.size() > PREFETCH_FILE_BYTES) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
batch.contents()[index] = readSmall(entry, inputSource);
|
||||
} catch (Throwable failure) {
|
||||
batch.failures()[index] = failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] readSmall(FingerprintEntry entry, InputSource inputSource) throws IOException {
|
||||
byte[] bytes = new byte[(int) entry.size()];
|
||||
int offset = 0;
|
||||
try (InputStream input = inputSource.open(entry.source())) {
|
||||
while (offset < bytes.length) {
|
||||
int read = input.read(bytes, offset, bytes.length - offset);
|
||||
if (read < 0) {
|
||||
throw changed(entry);
|
||||
}
|
||||
offset += read;
|
||||
}
|
||||
if (input.read() != -1) {
|
||||
throw changed(entry);
|
||||
}
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static void streamEntry(MessageDigest digest, FingerprintEntry entry, byte[] buffer,
|
||||
InputSource inputSource) throws IOException {
|
||||
long readBytes = 0L;
|
||||
try (InputStream input = inputSource.open(entry.source())) {
|
||||
int read;
|
||||
while ((read = input.read(buffer)) >= 0) {
|
||||
if (read > 0) {
|
||||
digest.update(buffer, 0, read);
|
||||
readBytes += read;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (readBytes != entry.size()) {
|
||||
throw changed(entry);
|
||||
}
|
||||
}
|
||||
|
||||
static InputStream openInput(Path source) throws IOException {
|
||||
return Files.newInputStream(source, StandardOpenOption.READ, LinkOption.NOFOLLOW_LINKS);
|
||||
}
|
||||
|
||||
private static void requireSuccessfulRead(Throwable failure) throws IOException {
|
||||
if (failure == null) {
|
||||
return;
|
||||
}
|
||||
if (failure instanceof IOException ioFailure) {
|
||||
throw ioFailure;
|
||||
}
|
||||
if (failure instanceof RuntimeException runtimeFailure) {
|
||||
throw runtimeFailure;
|
||||
}
|
||||
if (failure instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
throw new IOException("Unable to read generation pack entry", failure);
|
||||
}
|
||||
|
||||
private static void suppressReadFailures(Throwable primary, Throwable[] failures) {
|
||||
Set<Throwable> seen = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
seen.add(primary);
|
||||
Collections.addAll(seen, primary.getSuppressed());
|
||||
for (Throwable failure : failures) {
|
||||
if (failure != null && seen.add(failure)) {
|
||||
primary.addSuppressed(failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IOException changed(FingerprintEntry entry) {
|
||||
return new IOException("Iris generation pack changed while fingerprinting: " + entry.source());
|
||||
}
|
||||
|
||||
private static void collectTree(
|
||||
Path root,
|
||||
List<FingerprintEntry> entries,
|
||||
@@ -150,6 +287,14 @@ public final class GenerationPackFingerprint {
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface InputSource {
|
||||
InputStream open(Path source) throws IOException;
|
||||
}
|
||||
|
||||
private record PrefetchedBatch(byte[][] contents, Throwable[] failures) {
|
||||
}
|
||||
|
||||
private record FingerprintEntry(Path source, String relativePath, long size) {
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package art.arcane.iris.world.history;
|
||||
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.iris.pack.AtomicDirectoryPublisher;
|
||||
import art.arcane.iris.pack.PackDirectoryResolver;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -16,12 +18,23 @@ import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.RecursiveAction;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class GenerationPackRepository {
|
||||
private static final Pattern SHA_256 = Pattern.compile("[0-9a-f]{64}");
|
||||
private static final int MAXIMUM_COPY_WORKERS = 4;
|
||||
|
||||
private final Path dimensionRoot;
|
||||
private final Path generationRoot;
|
||||
@@ -90,7 +103,9 @@ public final class GenerationPackRepository {
|
||||
return requireExactPack(epochId, packFingerprint, packFingerprintVersion);
|
||||
}
|
||||
forceDirectory(epochDirectory);
|
||||
return requireExactPack(epochId, packFingerprint, packFingerprintVersion);
|
||||
validateExistingAncestors(epochId);
|
||||
requireSafeDirectory(target, "Generation epoch pack is missing or unsafe");
|
||||
return target;
|
||||
} finally {
|
||||
if (Files.exists(stage, LinkOption.NOFOLLOW_LINKS)) {
|
||||
AtomicDirectoryPublisher.deleteTree(stage);
|
||||
@@ -182,6 +197,16 @@ public final class GenerationPackRepository {
|
||||
}
|
||||
|
||||
public static void copyPackTree(Path source, Path target) throws IOException {
|
||||
Executor executor = IrisPlatforms.isBound() ? MultiBurst.ioBurst : ForkJoinPool.commonPool();
|
||||
int workers = Math.max(1, Math.min(MAXIMUM_COPY_WORKERS, Runtime.getRuntime().availableProcessors() / 2));
|
||||
copyPackTree(source, target, executor, workers);
|
||||
}
|
||||
|
||||
static void copyPackTree(Path source, Path target, Executor executor, int workers) throws IOException {
|
||||
Objects.requireNonNull(executor, "executor");
|
||||
if (workers < 1 || workers > MAXIMUM_COPY_WORKERS) {
|
||||
throw new IllegalArgumentException("Pack copy workers must be between 1 and " + MAXIMUM_COPY_WORKERS + ".");
|
||||
}
|
||||
Path sourcePath = Objects.requireNonNull(source, "source").toAbsolutePath().normalize();
|
||||
if (Files.isSymbolicLink(sourcePath)) {
|
||||
throw new IOException("Pack source is a symbolic link: " + sourcePath);
|
||||
@@ -199,7 +224,7 @@ public final class GenerationPackRepository {
|
||||
+ normalizedSource + " and " + normalizedTarget);
|
||||
}
|
||||
|
||||
List<Path> copiedFiles = new ArrayList<>();
|
||||
List<Path> sourceFiles = new ArrayList<>();
|
||||
Files.walkFileTree(normalizedSource, new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attributes) throws IOException {
|
||||
@@ -229,9 +254,7 @@ public final class GenerationPackRepository {
|
||||
|| fileName.endsWith(".code-workspace")) {
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
Path destination = copyDestination(normalizedSource, normalizedTarget, file);
|
||||
Files.copy(file, destination, StandardCopyOption.COPY_ATTRIBUTES);
|
||||
copiedFiles.add(destination);
|
||||
sourceFiles.add(file);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@@ -240,10 +263,103 @@ public final class GenerationPackRepository {
|
||||
throw new IOException("Unable to copy pack entry: " + file, failure);
|
||||
}
|
||||
});
|
||||
for (Path copiedFile : copiedFiles) {
|
||||
try (FileChannel channel = FileChannel.open(copiedFile, StandardOpenOption.WRITE)) {
|
||||
channel.force(true);
|
||||
executeFilePhase(sourceFiles, executor, workers, sourceFile ->
|
||||
Files.copy(sourceFile, copyDestination(normalizedSource, normalizedTarget, sourceFile),
|
||||
StandardCopyOption.COPY_ATTRIBUTES));
|
||||
executeFilePhase(sourceFiles, executor, workers, sourceFile ->
|
||||
forceFile(copyDestination(normalizedSource, normalizedTarget, sourceFile)));
|
||||
}
|
||||
|
||||
static void forceDirectoryTree(Path root, FileAction force) throws IOException {
|
||||
Executor executor = IrisPlatforms.isBound() ? MultiBurst.ioBurst : ForkJoinPool.commonPool();
|
||||
int workers = Math.max(1, Math.min(MAXIMUM_COPY_WORKERS, Runtime.getRuntime().availableProcessors() / 2));
|
||||
forceDirectoryTree(root, executor, workers, force);
|
||||
}
|
||||
|
||||
static void forceDirectoryTree(Path root, Executor executor, int workers, FileAction force) throws IOException {
|
||||
Objects.requireNonNull(executor, "executor");
|
||||
Objects.requireNonNull(force, "force");
|
||||
if (workers < 1 || workers > MAXIMUM_COPY_WORKERS) {
|
||||
throw new IllegalArgumentException("Directory sync workers must be between 1 and " + MAXIMUM_COPY_WORKERS + ".");
|
||||
}
|
||||
List<Path> directories;
|
||||
try (Stream<Path> entries = Files.walk(root)) {
|
||||
directories = entries
|
||||
.filter(path -> Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS))
|
||||
.sorted(Comparator.comparingInt(Path::getNameCount).reversed())
|
||||
.toList();
|
||||
}
|
||||
int start = 0;
|
||||
while (start < directories.size()) {
|
||||
int end = start + 1;
|
||||
int depth = directories.get(start).getNameCount();
|
||||
while (end < directories.size() && directories.get(end).getNameCount() == depth) {
|
||||
end++;
|
||||
}
|
||||
executeFilePhase(directories.subList(start, end), executor, workers, force);
|
||||
start = end;
|
||||
}
|
||||
}
|
||||
|
||||
private static void executeFilePhase(
|
||||
List<Path> files, Executor executor, int workers, FileAction action
|
||||
) throws IOException {
|
||||
FileWork work = new FileWork(files, action, new AtomicInteger(), new AtomicBoolean());
|
||||
int workerCount = Math.min(workers, files.size());
|
||||
List<CompletableFuture<Void>> copies = new ArrayList<>(workerCount);
|
||||
for (int worker = 0; worker < workerCount; worker++) {
|
||||
FileTask task = new FileTask(work);
|
||||
copies.add(task.finished);
|
||||
try {
|
||||
executor.execute(task);
|
||||
} catch (RuntimeException | Error failure) {
|
||||
task.reject(failure);
|
||||
break;
|
||||
}
|
||||
}
|
||||
awaitFileTasks(copies);
|
||||
}
|
||||
|
||||
private static void processFiles(FileWork work) throws IOException {
|
||||
int index;
|
||||
while (!work.failed().get() && (index = work.next().getAndIncrement()) < work.files().size()) {
|
||||
work.action().run(work.files().get(index));
|
||||
}
|
||||
}
|
||||
|
||||
private static void forceFile(Path file) throws IOException {
|
||||
try (FileChannel channel = FileChannel.open(file, StandardOpenOption.WRITE)) {
|
||||
channel.force(true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void awaitFileTasks(List<CompletableFuture<Void>> copies) throws IOException {
|
||||
CompletableFuture.allOf(copies.toArray(CompletableFuture[]::new))
|
||||
.handle((ignored, failure) -> null).join();
|
||||
Throwable failure = null;
|
||||
for (CompletableFuture<Void> copy : copies) {
|
||||
try {
|
||||
copy.join();
|
||||
} catch (CompletionException | CancellationException exception) {
|
||||
Throwable cause = exception instanceof CompletionException ? exception.getCause() : exception;
|
||||
if (failure == null) {
|
||||
failure = cause;
|
||||
} else if (failure != cause) {
|
||||
failure.addSuppressed(cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failure instanceof IOException exception) {
|
||||
throw exception;
|
||||
}
|
||||
if (failure instanceof RuntimeException exception) {
|
||||
throw exception;
|
||||
}
|
||||
if (failure instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
if (failure != null) {
|
||||
throw new IOException("Generation pack file operation failed.", failure);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,4 +390,79 @@ public final class GenerationPackRepository {
|
||||
}
|
||||
return requiredValue;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface FileAction {
|
||||
void run(Path file) throws IOException;
|
||||
}
|
||||
|
||||
private record FileWork(List<Path> files, FileAction action, AtomicInteger next, AtomicBoolean failed) {
|
||||
}
|
||||
|
||||
private static final class FileTask extends RecursiveAction implements Runnable {
|
||||
private final FileWork work;
|
||||
private final CompletableFuture<Void> finished = new CompletableFuture<>();
|
||||
private boolean started;
|
||||
private Throwable failure;
|
||||
|
||||
private FileTask(FileWork work) {
|
||||
this.work = work;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
quietlyInvoke();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean cancel(boolean mayInterruptIfRunning) {
|
||||
if (finished.isDone() || !super.cancel(mayInterruptIfRunning)) {
|
||||
return false;
|
||||
}
|
||||
reject(new CancellationException("Generation pack file worker was cancelled."));
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void compute() {
|
||||
synchronized (this) {
|
||||
if (started || failure != null) {
|
||||
return;
|
||||
}
|
||||
started = true;
|
||||
}
|
||||
Throwable fileFailure = null;
|
||||
try {
|
||||
processFiles(work);
|
||||
} catch (Throwable caught) {
|
||||
fileFailure = caught;
|
||||
} finally {
|
||||
finish(fileFailure);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void reject(Throwable failure) {
|
||||
work.failed().set(true);
|
||||
if (this.failure == null) {
|
||||
this.failure = failure;
|
||||
} else if (this.failure != failure) {
|
||||
this.failure.addSuppressed(failure);
|
||||
}
|
||||
if (!started) {
|
||||
finished.completeExceptionally(this.failure);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void finish(Throwable fileFailure) {
|
||||
if (fileFailure != null) {
|
||||
reject(fileFailure);
|
||||
}
|
||||
if (failure == null) {
|
||||
finished.complete(null);
|
||||
} else {
|
||||
super.completeExceptionally(failure);
|
||||
finished.completeExceptionally(failure);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -554,8 +554,13 @@ public final class SavedBiomeStore {
|
||||
}
|
||||
|
||||
private WriteStripe regionLock(int chunkX, int chunkZ) {
|
||||
long key = key(chunkX >> 5, chunkZ >> 5);
|
||||
return regionLocks[(int) (key ^ (key >>> 32)) & (regionLocks.length - 1)];
|
||||
long mixed = key(chunkX >> 5, chunkZ >> 5);
|
||||
mixed ^= mixed >>> 33;
|
||||
mixed *= 0xff51afd7ed558ccdL;
|
||||
mixed ^= mixed >>> 33;
|
||||
mixed *= 0xc4ceb9fe1a85ec53L;
|
||||
mixed ^= mixed >>> 33;
|
||||
return regionLocks[(int) (mixed ^ (mixed >>> 32)) & (regionLocks.length - 1)];
|
||||
}
|
||||
|
||||
private static long key(int x, int z) {
|
||||
|
||||
@@ -24,8 +24,6 @@ import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import art.arcane.iris.generation.stream.CachedDoubleStream2D;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@@ -76,9 +74,11 @@ public final class PregenPerformanceProfile {
|
||||
}
|
||||
|
||||
public static void apply(Engine engine) {
|
||||
apply();
|
||||
if (engine != null) {
|
||||
prepareNoiseCacheInPlace(engine);
|
||||
}
|
||||
apply();
|
||||
if (engine != null) {
|
||||
IrisLogging.info("Pregen profile applied: noiseCacheSize="
|
||||
+ IrisSettings.get().getPerformance().getNoiseCacheSize());
|
||||
}
|
||||
@@ -94,15 +94,7 @@ public final class PregenPerformanceProfile {
|
||||
|
||||
private static void prepareNoiseCacheInPlace(Engine engine) {
|
||||
IrisComplex complex = Objects.requireNonNull(engine.getComplex(), "Pregeneration requires an active biome complex");
|
||||
int configuredChunks = IrisSettings.get().getPerformance().getNoiseCacheSize();
|
||||
resizeNoiseCache(complex.getNaturalHeightStream(), configuredChunks);
|
||||
resizeNoiseCache(complex.getRawHeightStream(), configuredChunks);
|
||||
}
|
||||
|
||||
private static void resizeNoiseCache(ProceduralStream<Double> stream, int configuredChunks) {
|
||||
if (!(stream instanceof CachedDoubleStream2D cachedStream)) {
|
||||
throw new IllegalStateException("Pregeneration requires a mutable double terrain cache");
|
||||
}
|
||||
cachedStream.setMaximumChunks(configuredChunks);
|
||||
int configuredChunks = Math.max(IrisSettings.get().getPerformance().getNoiseCacheSize(), 4_096);
|
||||
complex.ensureTerrainNoiseCacheSize(configuredChunks);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -42,7 +42,7 @@ final class PaperLikeRuntimeControlBackend implements WorldRuntimeControlBackend
|
||||
}
|
||||
|
||||
TimeAccessStrategy strategy = resolveTimeAccessStrategy(world);
|
||||
if (strategy == null) {
|
||||
if (strategy == null || strategy.handleMethod() == null || strategy.readMethod() == null) {
|
||||
return OptionalLong.empty();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user