Merged generation improvements

- Cold startup and hydrology changes integrate with terrain history
- Flat authoring worlds skip native features under the history write guards
- Native decoration checks retain recorded structure and terrain ownership
- Automatic build revisions replace manual source seals
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-05 12:05:14 -04:00
commit 965606bbcc
42 files changed
+4921 -366

No files matched your search

@@ -203,7 +203,7 @@ public class IrisPregenerator {
// fires — leaving a phantom job that suppresses spawns and blocks future pregens.
try {
init();
task.iterateAllChunks((_a, _b) -> totalChunks.incrementAndGet());
totalChunks.set(task.chunkCount());
long startedAt = M.ms();
startTime.set(startedAt);
rateTracker.reset(startedAt, generated.get());
@@ -215,6 +215,11 @@ public class PregenTask {
return new int[]{bound.minX(), bound.minZ(), bound.maxX(), bound.maxZ()};
}
public long chunkCount() {
Bound bound = bounds.chunk();
return (long) bound.sizeX() * bound.sizeZ();
}
@FunctionalInterface
public interface InterleavedChunkConsumer {
boolean on(int regionX, int regionZ, int chunkX, int chunkZ, boolean firstChunkInRegion, boolean lastChunkInRegion);
@@ -952,7 +952,7 @@ public final class StudioOpenCoordinator {
}
public boolean prepareGeneratorState() {
return this != JIGSAW;
return this == STANDARD || this == FORCED_STANDARD;
}
public IrisCreator.DatapackPreparation datapackPreparation() {
@@ -18,7 +18,6 @@
package art.arcane.iris.engine;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.loader.IrisData;
@@ -41,6 +40,9 @@ import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisDecorationPart;
import art.arcane.iris.engine.object.IrisDecorator;
import art.arcane.iris.engine.object.IrisGenerator;
import art.arcane.iris.engine.object.IrisGeneratorStyle;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.engine.object.IrisInterpolator;
import art.arcane.iris.engine.object.IrisMaterialPalette;
import art.arcane.iris.engine.object.IrisRegion;
@@ -70,6 +72,7 @@ import lombok.Getter;
import lombok.ToString;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
@@ -137,6 +140,8 @@ public class IrisComplex implements DataProvider {
private ProceduralStream<IrisBiome> seaBiomeStream;
private ProceduralStream<IrisBiome> shoreBiomeStream;
private ProceduralStream<IrisBiome> baseBiomeStream;
@Getter(AccessLevel.NONE)
private final transient boolean reuseNaturalBaseBiome;
private ProceduralStream<UUID> baseBiomeIDStream;
private ProceduralStream<IrisBiome> naturalTrueBiomeStream;
private ProceduralStream<IrisBiome> unblendedNaturalTrueBiomeStream;
@@ -235,6 +240,7 @@ public class IrisComplex implements DataProvider {
}
imageMapRuntime.getMappedBiomes().forEach(this::registerGenerators);
}
reuseNaturalBaseBiome = hasFixedNaturalBiomeNoise(engine.getDimension(), generatorBiomes);
inferredBiomeStreams = compileInferredBiomeStreams(
preparedRegions,
(region, inferredType) -> compileInferredBiomeStream(engine, region, inferredType, emptyBiome)
@@ -502,16 +508,19 @@ public class IrisComplex implements DataProvider {
InferredType inferredType,
IrisRegion region,
double naturalHeight,
double x,
double z
int x,
int z
) {
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
if (mapped != null) {
return mapped;
}
IrisBiome baseBiome = implode(sampleInferredBiome(region, inferredType, x, z), x, z);
IrisContext context = IrisContext.get();
IrisBiome baseBiome = reuseNaturalBaseBiome && (context == null || context.getChunkContext() == null)
? baseBiomeStream.get(x, z)
: implode(sampleInferredBiome(region, inferredType, x, z), (double) x, (double) z);
IrisBiome resolved = resolveNaturalSurfaceBiome(naturalHeight, baseBiome, region, x, z);
return resolved == baseBiome ? baseBiome : implode(resolved, x, z);
return resolved == baseBiome ? baseBiome : implode(resolved, (double) x, (double) z);
}
private IrisBiome resolveNaturalSurfaceBiome(
@@ -1393,6 +1402,12 @@ public class IrisComplex implements DataProvider {
return 0D;
}
double height = 0D;
if (low == high && Double.isFinite(low)) {
for (int i = 0; i < generators.length; i++) {
height += low;
}
return height / generators.length;
}
for (IrisGenerator generator : generators) {
height += M.lerp(low, high, generator.getHeight(x, z, seed));
}
@@ -1477,9 +1492,34 @@ public class IrisComplex implements DataProvider {
CNG childCell = b.getChildrenGenerator(rng, 123, b.getChildShrinkFactor());
ChildSelectionPlan childSelectionPlan = resolveChildSelectionPlan(b);
IrisBiome biome = childSelectionPlan.select(childCell, x, z).withInferredType(b.getInferredType());
if (biome == b && hasFixedNoise(b.getChildStyle())) {
return biome;
}
return implode(biome, x, z, max - 1);
}
static boolean hasFixedNaturalBiomeNoise(IrisDimension dimension, Iterable<IrisBiome> biomes) {
if (!hasFixedNoise(dimension.getRegionStyle()) || !hasFixedNoise(dimension.getContinentalStyle())
|| !hasFixedNoise(dimension.getLandBiomeStyle()) || !hasFixedNoise(dimension.getSeaBiomeStyle())) {
return false;
}
for (IrisBiome biome : biomes) {
if (!biome.getChildren().isEmpty() && !hasFixedNoise(biome.getChildStyle())) {
return false;
}
}
return true;
}
private static boolean hasFixedNoise(IrisGeneratorStyle style) {
for (IrisGeneratorStyle current = style; current != null; current = current.getFracture()) {
if (current.getExpression() != null) {
return false;
}
}
return true;
}
private ChildSelectionPlan resolveChildSelectionPlan(IrisBiome biome) {
ChildSelectionPlan cachedPlan = childSelectionPlans.get(biome);
if (cachedPlan != null) {
@@ -1539,14 +1579,15 @@ public class IrisComplex implements DataProvider {
/**
* Reusable per-thread bounds provider. Holds the biome memo and the lazily computed generator
* bounds for one sampleBoundsRaw pass so the pass allocates nothing; {@link #bind}
* resets both, giving each pass the same empty-scratch semantics a fresh allocation had.
* resets generator bounds while retaining bounded coordinate samples for the same biome stream.
* <p>
* Single threaded and non reentrant by contract, matching the thread local sample caches in
* IrisInterpolation that this provider is invoked through. If a nested pass ever does appear,
* {@link #isInUse()} makes the caller fall back to a freshly allocated sampler.
*/
private static final class BoundsSampler implements NoiseBoundsProvider {
private final CoordinateBiomeCache sampleCache = new CoordinateBiomeCache(64);
private final CoordinateBiomeCache sampleCache = new CoordinateBiomeCache(4096);
private WeakReference<ProceduralStream<IrisBiome>> sampleSource = new WeakReference<>(null);
private final IdentityHashMap<IrisBiome, GeneratorBounds> localBounds = new IdentityHashMap<>(8);
private IrisComplex complex;
private Engine engine;
@@ -1564,7 +1605,10 @@ public class IrisComplex implements DataProvider {
this.generators = generators;
this.cachedBounds = cachedBounds;
this.inUse = true;
sampleCache.clear();
if (sampleSource.get() != complex.baseBiomeStream) {
sampleCache.clear();
sampleSource = new WeakReference<>(complex.baseBiomeStream);
}
if (!localBounds.isEmpty()) {
localBounds.clear();
@@ -1607,73 +1651,40 @@ public class IrisComplex implements DataProvider {
}
/**
* Open addressed biome memo keyed on the packed coordinate bits, replacing a linear scan that was
* Bounded biome memo keyed on the packed coordinate bits, replacing a linear scan that was
* quadratic in the number of columns a wide starcast touches. Single threaded by contract.
*/
private static final class CoordinateBiomeCache {
private long[] xBits;
private long[] zBits;
private IrisBiome[] values;
private byte[] states;
private int mask;
private int resizeThreshold;
private int size;
private final long[] xBits;
private final long[] zBits;
private final IrisBiome[] values;
private final int mask;
private CoordinateBiomeCache(int initialCapacity) {
int minimumCapacity = Math.max(8, initialCapacity);
int tableSize = tableSizeFor((minimumCapacity << 1) + minimumCapacity);
xBits = new long[tableSize];
zBits = new long[tableSize];
values = new IrisBiome[tableSize];
states = new byte[tableSize];
mask = tableSize - 1;
resizeThreshold = Math.max(1, (tableSize * 3) >> 2);
size = 0;
private CoordinateBiomeCache(int capacity) {
xBits = new long[capacity];
zBits = new long[capacity];
values = new IrisBiome[capacity];
mask = capacity - 1;
}
private void clear() {
if (size == 0) {
return;
}
Arrays.fill(states, (byte) 0);
size = 0;
Arrays.fill(values, null);
}
private IrisBiome get(double x, double z) {
int slot = findSlot(Double.doubleToLongBits(x), Double.doubleToLongBits(z));
return states[slot] == 0 ? null : values[slot];
long xb = Double.doubleToLongBits(x);
long zb = Double.doubleToLongBits(z);
int slot = mix(xb, zb) & mask;
return xBits[slot] == xb && zBits[slot] == zb ? values[slot] : null;
}
private void put(double x, double z, IrisBiome biome) {
long xb = Double.doubleToLongBits(x);
long zb = Double.doubleToLongBits(z);
int slot = findSlot(xb, zb);
boolean occupied = states[slot] != 0;
int slot = mix(xb, zb) & mask;
xBits[slot] = xb;
zBits[slot] = zb;
values[slot] = biome;
states[slot] = 1;
if (occupied) {
return;
}
size++;
if (size >= resizeThreshold) {
grow();
}
}
private int findSlot(long xb, long zb) {
int slot = mix(xb, zb) & mask;
while (states[slot] != 0) {
if (xBits[slot] == xb && zBits[slot] == zb) {
break;
}
slot = (slot + 1) & mask;
}
return slot;
}
private int mix(long xb, long zb) {
@@ -1684,49 +1695,6 @@ public class IrisComplex implements DataProvider {
hash ^= (hash >>> 33);
return (int) hash;
}
private void grow() {
long[] previousXBits = xBits;
long[] previousZBits = zBits;
IrisBiome[] previousValues = values;
byte[] previousStates = states;
int nextLength = previousXBits.length << 1;
xBits = new long[nextLength];
zBits = new long[nextLength];
values = new IrisBiome[nextLength];
states = new byte[nextLength];
mask = nextLength - 1;
resizeThreshold = Math.max(1, (nextLength * 3) >> 2);
size = 0;
for (int i = 0; i < previousStates.length; i++) {
if (previousStates[i] == 0) {
continue;
}
int slot = findSlot(previousXBits[i], previousZBits[i]);
xBits[slot] = previousXBits[i];
zBits[slot] = previousZBits[i];
values[slot] = previousValues[i];
states[slot] = 1;
size++;
}
}
private int tableSizeFor(int value) {
int n = value - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
int tableSize = n + 1;
if (tableSize < 8) {
return 8;
}
return tableSize;
}
}
static final class ChildSelectionPlan {
@@ -406,6 +406,36 @@ public class IrisEngine implements Engine {
return packRoot.resolve(".iris").resolve("studio-hydrology");
}
public void startStudioEntryHydrology(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.");
}
}
private void prepareStudioEntryHydrology(GenerationRuntimeBinding binding, int blockX, int blockZ) {
if (closing.get()) {
return;
}
GenerationSessionLease lease;
try {
lease = acquireGenerationLease("studio_entry_hydrology");
} catch (GenerationSessionException failure) {
if (closing.get()) {
return;
}
throw new IllegalStateException("Studio 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 "
+ blockX + "," + blockZ + ".", failure);
}
}
private void startGenerationCacheWarm(long phaseStartedAtNanos) {
if (!backgroundTasks.scheduleTrackedTask(() -> warmGenerationCaches(phaseStartedAtNanos))) {
throw new IllegalStateException("Iris background task admission closed before generation cache warming.");
@@ -1491,6 +1521,7 @@ public class IrisEngine implements Engine {
public enum InitializationMode {
RUNTIME(false, true),
STUDIO(true, true),
OBJECT_STUDIO(true, false),
JIGSAW_STUDIO(true, false);
private final boolean studio;
@@ -1694,7 +1694,7 @@ final class HydrologyFootprintCompiler {
if (cached != null) {
return cached;
}
HydrologyTerrainSample sampled = naturalSampler.sampleBasis(x, z);
HydrologyTerrainSample sampled = naturalSampler.sampleBasisWithoutSlope(x, z);
if (sampled != null) {
terrainBases.put(packed, sampled);
}
@@ -4,18 +4,17 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinWorkerThread;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Runs a batch of independent planning tasks in parallel without ever parking a pool worker on work
* that lives in another pool. On a ForkJoin worker the tasks are forked into that worker's own pool
* and joined with work helping, so a saturated pool still finishes them on the joining thread; on any
* other thread they run on the fallback executor, or inline when there is none. Results keep the
* task order and a task's unchecked exception is rethrown as is.
* Runs independent planning tasks and returns results in request order, using the current pool or fallback.
* A worker claims its own queued tasks before waiting, and never helps unrelated work while holding
* an owner draft. Every started task finishes before the batch returns or throws.
*/
public final class HydrologyForkJoin {
private HydrologyForkJoin() {
@@ -23,48 +22,45 @@ public final class HydrologyForkJoin {
public static <T> List<T> invokeAll(List<Callable<T>> tasks, Executor fallback) {
Objects.requireNonNull(tasks, "tasks");
if (tasks.size() < 2 || fallback == null && !(Thread.currentThread() instanceof ForkJoinWorkerThread)) {
return runInline(tasks);
}
if (Thread.currentThread() instanceof ForkJoinWorkerThread) {
return forkJoin(tasks);
}
return runOn(tasks, fallback);
}
private static <T> List<T> runInline(List<Callable<T>> tasks) {
ArrayList<T> results = new ArrayList<>(tasks.size());
for (Callable<T> task : tasks) {
results.add(call(task));
}
return results;
}
private static <T> List<T> forkJoin(List<Callable<T>> tasks) {
ArrayList<ForkJoinTask<T>> forked = new ArrayList<>(tasks.size());
for (Callable<T> task : tasks) {
forked.add(ForkJoinTask.adapt(task));
}
ForkJoinTask.invokeAll(forked);
ArrayList<T> results = new ArrayList<>(tasks.size());
for (ForkJoinTask<T> task : forked) {
results.add(task.join());
}
return results;
}
private static <T> List<T> runOn(List<Callable<T>> tasks, Executor executor) {
ArrayList<CompletableFuture<T>> futures = new ArrayList<>(tasks.size());
for (Callable<T> task : tasks) {
futures.add(CompletableFuture.supplyAsync(() -> call(task), executor));
}
ArrayList<T> results = new ArrayList<>(tasks.size());
for (CompletableFuture<T> future : futures) {
try {
results.add(future.join());
} catch (CompletionException failure) {
throw unwrap(failure.getCause());
boolean callerRuns = Thread.currentThread() instanceof ForkJoinWorkerThread;
if (tasks.size() < 2 || fallback == null && !callerRuns) {
ArrayList<T> results = new ArrayList<>(tasks.size());
for (Callable<T> task : tasks) {
results.add(call(task));
}
return results;
}
Executor executor = callerRuns ? ((ForkJoinWorkerThread) Thread.currentThread()).getPool() : fallback;
ArrayList<Task<T>> pending = new ArrayList<>(tasks.size());
for (Callable<T> task : tasks) {
pending.add(new Task<>(task));
}
Throwable failure = null;
int submitted = callerRuns ? 1 : 0;
for (int index = callerRuns ? 1 : 0; index < pending.size(); index++) {
try {
executor.execute(pending.get(index));
submitted = index + 1;
} catch (RuntimeException | Error rejected) {
failure = rejected;
break;
}
}
ArrayList<T> results = new ArrayList<>(tasks.size());
int drainCount = callerRuns ? pending.size() : submitted;
for (int index = 0; index < drainCount; index++) {
try {
results.add(pending.get(index).await(callerRuns));
} catch (RuntimeException | Error failed) {
if (failure == null) {
failure = failed;
} else if (failure != failed) {
failure.addSuppressed(failed);
}
}
}
if (failure != null) {
throw unwrap(failure);
}
return results;
}
@@ -80,12 +76,92 @@ public final class HydrologyForkJoin {
}
private static RuntimeException unwrap(Throwable cause) {
if (cause instanceof RuntimeException runtimeException) {
return runtimeException;
}
if (cause instanceof Error error) {
throw error;
}
if (cause instanceof RuntimeException runtime) {
return runtime;
}
return new IllegalStateException("Hydrology planning task failed.", cause);
}
static final class Task<T> implements Runnable, ForkJoinPool.ManagedBlocker {
private final Callable<T> work;
private final AtomicBoolean claimed = new AtomicBoolean();
private final CountDownLatch complete = new CountDownLatch(1);
private T result;
private Throwable failure;
Task(Callable<T> work) {
this.work = Objects.requireNonNull(work);
}
T await() {
return await(true);
}
private T await(boolean claimPending) {
if (claimPending) {
run();
}
boolean interrupted = false;
boolean managed = true;
Throwable blockingFailure = null;
while (!isReleasable()) {
try {
if (managed) {
ForkJoinPool.managedBlock(this);
} else {
complete.await();
}
} catch (InterruptedException interruption) {
interrupted = true;
managed = false;
} catch (RejectedExecutionException unavailable) {
managed = false;
} catch (RuntimeException | Error failed) {
blockingFailure = failed;
managed = false;
}
}
if (interrupted) {
Thread.currentThread().interrupt();
}
if (blockingFailure != null) {
if (failure != null && failure != blockingFailure) {
blockingFailure.addSuppressed(failure);
}
throw unwrap(blockingFailure);
}
if (failure != null) {
throw unwrap(failure);
}
return result;
}
@Override
public boolean isReleasable() {
return complete.getCount() == 0;
}
@Override
public boolean block() throws InterruptedException {
complete.await();
return true;
}
@Override
public void run() {
if (!claimed.compareAndSet(false, true)) {
return;
}
try {
result = call(work);
} catch (RuntimeException | Error failed) {
failure = failed;
} finally {
complete.countDown();
}
}
}
}
@@ -27,6 +27,7 @@ import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -39,6 +40,7 @@ import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ForkJoinWorkerThread;
import java.util.function.IntPredicate;
public final class HydrologyPlanner {
@@ -124,6 +126,7 @@ public final class HydrologyPlanner {
private final int minimumY;
private final HydrologyCaveVoxelViewFactory caveViewFactory;
private final Cache<HydrologyTileKey, SourceRoutingContext> routingContexts;
private final ConcurrentHashMap<HydrologyTileKey, RoutingContextLoad> resolvingRoutingContexts;
private final Cache<HydrologyTileKey, CrossTileResolvedOwner> resolvedOwners;
private final Cache<RefinedEdgeKey, List<HydrologyPoint>> refinedEdgeCache;
private final ConcurrentHashMap<HydrologyTileKey, CompletableFuture<CrossTileResolvedOwner>> resolvingOwners;
@@ -237,6 +240,7 @@ public final class HydrologyPlanner {
this.routingContexts = Caffeine.newBuilder()
.maximumSize(ROUTING_CONTEXT_CACHE_SIZE)
.build();
this.resolvingRoutingContexts = new ConcurrentHashMap<>();
this.resolvedOwners = Caffeine.newBuilder()
.maximumSize(RESOLVED_OWNER_CACHE_SIZE)
.build();
@@ -252,7 +256,7 @@ public final class HydrologyPlanner {
this.routeWormDetail = new SimplexNoise(HydrologyHash.mix(worldSeed, ROUTE_WORM_DETAIL_SALT));
this.surfaceCourseBuilder = new SurfaceCourseBuilder(
settings.surface(),
this::sampleBasis,
this::sampleBasisWithoutSlope,
geometrySampler,
settings.seaLevel(),
settings.routing().minimumSurfaceCourseLength()
@@ -260,7 +264,7 @@ public final class HydrologyPlanner {
// A tributary only has to be a real reach before it joins its stem: half the course minimum.
this.tributaryCourseBuilder = new SurfaceCourseBuilder(
settings.surface(),
this::sampleBasis,
this::sampleBasisWithoutSlope,
geometrySampler,
settings.seaLevel(),
settings.routing().minimumSurfaceCourseLength() / 2
@@ -418,6 +422,10 @@ public final class HydrologyPlanner {
sourceRoutingContexts
);
phaseStarted = profile.record(DraftPhase.SELECT, phaseStarted);
if (crossTileAdmission != null && (!surfaceSelection.selectedCandidateIndices.isEmpty()
|| !undergroundSelection.selectedCandidateIndices.isEmpty())) {
crossTileAdmission.prepare();
}
}
HydrologyFootprintCompiler footprintCompiler = new HydrologyFootprintCompiler(
settings,
@@ -780,15 +788,24 @@ public final class HydrologyPlanner {
try {
int ownerRank = ownerColorRank(key);
ColorRankedDraftAdmission admission = new ColorRankedDraftAdmission(key, ownerRank, context);
HydrologyOwnerDraft draft = compileOwnerDraft(
key,
admission,
key.equals(context.root())
);
HydrologyOwnerDraft draft;
try (admission) {
try {
draft = compileOwnerDraft(
key,
admission,
key.equals(context.root())
);
} catch (RuntimeException | Error failure) {
admission.primaryFailure = failure;
throw failure;
}
}
CrossTileResolvedOwner resolved = new CrossTileResolvedOwner(draft, admission.observedRejections());
context.remember(key, resolved);
resolvedOwners.put(key, resolved.withoutFootprintCompiler());
owned.complete(resolved);
CrossTileResolvedOwner shared = resolved.withoutFootprintCompiler();
resolvedOwners.put(key, shared);
owned.complete(shared);
return resolved;
} catch (Throwable failure) {
owned.completeExceptionally(failure);
@@ -803,7 +820,8 @@ public final class HydrologyPlanner {
HydrologyTileKey ownerKey,
HydrologyCaveCourseFilter.Result result,
int ownerRank,
CrossTileResolutionContext context
CrossTileResolutionContext context,
Map<HydrologyTileKey, HydrologyForkJoin.Task<CrossTileResolvedOwner>> preparedOwners
) {
ArrayList<HydrologyCrossTileCaveAdmission.RankedClaim> blockers = new ArrayList<>();
ArrayList<HydrologyCrossTileSurfaceAdmission.RankedClaim> surfaceBlockers = new ArrayList<>();
@@ -822,7 +840,7 @@ public final class HydrologyPlanner {
candidateKeys.add(candidateKey);
}
long dependenciesStarted = System.nanoTime();
List<CrossTileResolvedOwner> resolvedCandidates = resolveLowerRankOwners(candidateKeys, context);
List<CrossTileResolvedOwner> resolvedCandidates = resolveLowerRankOwners(candidateKeys, context, preparedOwners);
currentDraftProfile().recordDependencies(candidateKeys.size(), dependenciesStarted);
for (int index = 0; index < candidateKeys.size(); index++) {
HydrologyTileKey candidateKey = candidateKeys.get(index);
@@ -848,20 +866,26 @@ public final class HydrologyPlanner {
private List<CrossTileResolvedOwner> resolveLowerRankOwners(
List<HydrologyTileKey> candidateKeys,
CrossTileResolutionContext context
CrossTileResolutionContext context,
Map<HydrologyTileKey, HydrologyForkJoin.Task<CrossTileResolvedOwner>> preparedOwners
) {
if (candidateKeys.size() < 2 || !IrisPlatforms.isBound()) {
ArrayList<CrossTileResolvedOwner> resolved = new ArrayList<>(candidateKeys.size());
for (HydrologyTileKey candidateKey : candidateKeys) {
resolved.add(resolveCrossTileOwner(candidateKey, context));
HydrologyForkJoin.Task<CrossTileResolvedOwner> prepared = preparedOwners.get(candidateKey);
CrossTileResolvedOwner owner = prepared == null
? resolveCrossTileOwner(candidateKey, context) : prepared.await();
context.remember(candidateKey, owner);
resolved.add(owner);
}
return List.copyOf(resolved);
}
// Neighbour drafts fork into the pool this thread belongs to (or the burst pool from outside
// any pool) and are joined with work helping, so a saturated pool never deadlocks on them.
// Neighbour drafts use the current pool (or the burst pool from outside any pool).
// Waiting workers claim their own tasks without helping unrelated owner drafts.
ArrayList<Callable<CrossTileResolvedOwner>> tasks = new ArrayList<>(candidateKeys.size());
for (HydrologyTileKey candidateKey : candidateKeys) {
tasks.add(() -> resolveIndependentOwner(candidateKey));
HydrologyForkJoin.Task<CrossTileResolvedOwner> prepared = preparedOwners.get(candidateKey);
tasks.add(prepared == null ? () -> resolveIndependentOwner(candidateKey) : prepared::await);
}
List<CrossTileResolvedOwner> owners = HydrologyForkJoin.invokeAll(tasks, MultiBurst.burst);
ArrayList<CrossTileResolvedOwner> resolved = new ArrayList<>(candidateKeys.size());
@@ -1227,7 +1251,11 @@ public final class HydrologyPlanner {
if (includeDeepFluids && grid != null) {
compileSeaCaves(grid, courses, diagnostics);
}
DraftProfile profile = currentDraftProfile();
long rasterStarted = System.nanoTime();
HydrologyFootprintCompiler.ValidationRaster validation = footprintCompiler.compileValidation(courses);
profile.rasterNanos += System.nanoTime() - rasterStarted;
profile.rasterCalls++;
HydrologyObservedPlannedSurface plannedSurface = new HydrologyObservedPlannedSurface(
validation.plannedSurface()
);
@@ -1243,6 +1271,7 @@ public final class HydrologyPlanner {
samples == null ? null : samples.caveCandidates;
HydrologyCaveContainmentPlanner.ValidationCache validationCache =
samples == null ? null : samples.caveValidations;
long filterStarted = System.nanoTime();
HydrologyCaveCourseFilter.Result containment = new HydrologyCaveCourseFilter(
caveView,
new HydrologyCaveCourseFilter.Options(
@@ -1261,6 +1290,8 @@ public final class HydrologyPlanner {
validation,
diagnostics
);
profile.filterNanos += System.nanoTime() - filterStarted;
profile.filterCalls++;
return new PublicationAttempt(containment, diagnostics);
}
@@ -1650,7 +1681,7 @@ public final class HydrologyPlanner {
settled = true;
break;
}
int floor = sampleBasis(point.x(), point.z()).naturalHeight() - maximumIncision;
int floor = sampleBasisWithoutSlope(point.x(), point.z()).naturalHeight() - maximumIncision;
int lowered = Math.min(point.y(), Math.max(Math.max(allowed, floor), downstreamHead));
if (lowered < point.y()) {
centerline.set(pointIndex, new HydrologyPoint(point.x(), lowered, point.z()));
@@ -3031,7 +3062,36 @@ public final class HydrologyPlanner {
}
private SourceRoutingContext sourceRoutingContext(HydrologyTileKey key) {
return routingContexts.get(key, this::compileSourceRoutingContext);
SourceRoutingContext cached = routingContexts.getIfPresent(key);
if (cached != null) {
return cached;
}
RoutingContextLoad owned = new RoutingContextLoad(Thread.currentThread(), new CompletableFuture<>());
RoutingContextLoad existing = resolvingRoutingContexts.putIfAbsent(key, owned);
if (existing != null) {
if (existing.owner() == Thread.currentThread()) {
throw new IllegalStateException("Hydrology routing context cannot recursively load its own tile.");
}
try {
return existing.future().join();
} catch (CompletionException failure) {
throw propagateOwnerFailure(failure.getCause());
}
}
try {
SourceRoutingContext resolved = routingContexts.getIfPresent(key);
if (resolved == null) {
resolved = compileSourceRoutingContext(key);
routingContexts.put(key, resolved);
}
owned.future().complete(resolved);
return resolved;
} catch (Throwable failure) {
owned.future().completeExceptionally(failure);
throw propagateOwnerFailure(failure);
} finally {
resolvingRoutingContexts.remove(key, owned);
}
}
private SourceRoutingContext compileSourceRoutingContext(HydrologyTileKey key) {
@@ -3487,16 +3547,19 @@ public final class HydrologyPlanner {
int desiredZ,
double maximumOffsetRatio
) {
HydrologyTerrainSample terrain = sampleLandBasis(x, z);
double distanceFromNode = StrictMath.hypot(x - node.x(), z - node.z());
if (distanceFromNode > settings.routing().sampleSpacing() * maximumOffsetRatio) {
return Double.POSITIVE_INFINITY;
}
HydrologyTerrainSample terrain = sampleLandBasisWithoutSlope(x, z);
if (terrain == null || !terrain.transitAllowed() || !withinConfines(terrain, node.terrain().confinesKey())) {
return Double.POSITIVE_INFINITY;
}
double distanceFromDesired = StrictMath.hypot(x - desiredX, z - desiredZ);
double distanceFromNode = StrictMath.hypot(x - node.x(), z - node.z());
if (distanceFromNode > settings.routing().sampleSpacing() * maximumOffsetRatio
|| !traversableHop(node.naturalPoint(), new HydrologyPoint(x, terrain.naturalHeight(), z))) {
if (!traversableHop(node.naturalPoint(), new HydrologyPoint(x, terrain.naturalHeight(), z))) {
return Double.POSITIVE_INFINITY;
}
terrain = sampleLandBasis(x, z);
double distanceFromDesired = StrictMath.hypot(x - desiredX, z - desiredZ);
double score = terrain.naturalHeight() * settings.routing().valleyPreference()
+ terrain.slope() * settings.routing().slopePenalty()
+ terrain.routingCost() * terrain.routingMultiplier()
@@ -3637,7 +3700,7 @@ public final class HydrologyPlanner {
ArrayList<HydrologyPoint> points = new ArrayList<>(direct.size());
for (int index = 0; index < direct.size(); index++) {
HydrologyPoint point = direct.get(index);
HydrologyTerrainSample terrain = sampleLandBasis(point.x(), point.z());
HydrologyTerrainSample terrain = sampleLandBasisWithoutSlope(point.x(), point.z());
if (terrain == null || !terrain.transitAllowed()) {
return List.of();
}
@@ -3661,7 +3724,7 @@ public final class HydrologyPlanner {
for (int direction : List.of(-1, 1)) {
int x = (int) StrictMath.round(point.x() + perpendicularX * refinement * direction);
int z = (int) StrictMath.round(point.z() + perpendicularZ * refinement * direction);
HydrologyTerrainSample terrain = sampleLandBasis(x, z);
HydrologyTerrainSample terrain = sampleLandBasisWithoutSlope(x, z);
if (terrain != null && terrain.transitAllowed()) {
return true;
}
@@ -3827,11 +3890,13 @@ public final class HydrologyPlanner {
candidates.put(packed, candidate);
}
}
if (!candidates.isEmpty()) {
return List.copyOf(candidates.values());
}
int fallbackX = (int) StrictMath.round(nominal.baseX());
int fallbackZ = (int) StrictMath.round(nominal.baseZ());
HydrologyTerrainSample fallbackTerrain = sampleLandBasis(fallbackX, fallbackZ);
if (candidates.isEmpty()
&& fallbackTerrain != null
if (fallbackTerrain != null
&& fallbackTerrain.transitAllowed()
&& withinConfines(fallbackTerrain, confines)) {
long packed = RiverFootprint.pack(fallbackX, fallbackZ);
@@ -4141,6 +4206,7 @@ public final class HydrologyPlanner {
}
double[][] localPenalties = new double[layerCount][];
double[][][] transitionCosts = new double[layerCount][][];
double[][][][] turnCosts = new double[layerCount][][][];
for (int layerIndex = 0; layerIndex < layerCount; layerIndex++) {
int layerSize = layers.get(layerIndex).size();
localPenalties[layerIndex] = new double[layerSize];
@@ -4163,7 +4229,8 @@ public final class HydrologyPlanner {
maximumTurn,
turnCost,
localPenalties,
transitionCosts
transitionCosts,
turnCosts
);
if (selection.route().length == 0) {
return selection.route();
@@ -4187,7 +4254,8 @@ public final class HydrologyPlanner {
double maximumTurn,
double turnCost,
double[][] localPenalties,
double[][][] transitionCosts
double[][][] transitionCosts,
double[][][][] turnCosts
) {
int layerCount = layers.size();
double[][][] costs = new double[layerCount][][];
@@ -4222,6 +4290,9 @@ public final class HydrologyPlanner {
List<RouteCandidate> currentLayer = layers.get(layerIndex);
costs[layerIndex] = new double[previousLayer.size()][currentLayer.size()];
predecessors[layerIndex] = new int[previousLayer.size()][currentLayer.size()];
if (turnCosts[layerIndex] == null) {
turnCosts[layerIndex] = new double[previousLayer.size()][currentLayer.size()][];
}
for (int previousIndex = 0; previousIndex < previousLayer.size(); previousIndex++) {
Arrays.fill(costs[layerIndex][previousIndex], Double.POSITIVE_INFINITY);
Arrays.fill(predecessors[layerIndex][previousIndex], -1);
@@ -4243,20 +4314,32 @@ public final class HydrologyPlanner {
if (!Double.isFinite(previousCost)) {
continue;
}
RouteCandidate before = beforeLayer.get(beforeIndex);
double turn = routeTurnDegrees(
before.point(),
previous.point(),
current.point()
);
if (turn > maximumTurn) {
double[] candidateTurns = turnCosts[layerIndex][previousIndex][currentIndex];
if (candidateTurns == null) {
candidateTurns = new double[beforeLayer.size()];
Arrays.fill(candidateTurns, Double.NaN);
turnCosts[layerIndex][previousIndex][currentIndex] = candidateTurns;
}
double turnPenalty = candidateTurns[beforeIndex];
if (Double.isNaN(turnPenalty)) {
double turn = routeTurnDegrees(
beforeLayer.get(beforeIndex).point(),
previous.point(),
current.point()
);
turnPenalty = turn > maximumTurn
? Double.POSITIVE_INFINITY
: curvatureCost(turn, maximumTurn, turnCost);
candidateTurns[beforeIndex] = turnPenalty;
}
if (turnPenalty == Double.POSITIVE_INFINITY) {
continue;
}
double cost = previousCost
+ current.localScore()
+ localPenalty
+ transition
+ curvatureCost(turn, maximumTurn, turnCost);
+ turnPenalty;
if (cost < costs[layerIndex][previousIndex][currentIndex]) {
costs[layerIndex][previousIndex][currentIndex] = cost;
predecessors[layerIndex][previousIndex][currentIndex] = beforeIndex;
@@ -4480,8 +4563,8 @@ public final class HydrologyPlanner {
if (offsetX == 0 && offsetZ == 0) {
return false;
}
HydrologyTerrainSample before = sampleLandBasis(point.x() - offsetX, point.z() - offsetZ);
HydrologyTerrainSample after = sampleLandBasis(point.x() + offsetX, point.z() + offsetZ);
HydrologyTerrainSample before = sampleLandBasisWithoutSlope(point.x() - offsetX, point.z() - offsetZ);
HydrologyTerrainSample after = sampleLandBasisWithoutSlope(point.x() + offsetX, point.z() + offsetZ);
int threshold = settings.hydraulics().waterfallMinimumDrop();
return before != null
&& after != null
@@ -4504,7 +4587,7 @@ public final class HydrologyPlanner {
HydrologyPoint next = current.get(pointIndex + 1);
int x = (int) StrictMath.round(previous.x() * 0.25D + point.x() * 0.5D + next.x() * 0.25D);
int z = (int) StrictMath.round(previous.z() * 0.25D + point.z() * 0.5D + next.z() * 0.25D);
HydrologyTerrainSample terrain = sampleLandBasis(x, z);
HydrologyTerrainSample terrain = sampleLandBasisWithoutSlope(x, z);
smoothed.add(terrain == null || !terrain.transitAllowed()
? point
: new HydrologyPoint(x, terrain.naturalHeight(), z));
@@ -4661,8 +4744,8 @@ public final class HydrologyPlanner {
if (crossing.size() < 3) {
return false;
}
HydrologyTerrainSample startTerrain = sampleLandBasis(start.x(), start.z());
HydrologyTerrainSample endTerrain = sampleLandBasis(end.x(), end.z());
HydrologyTerrainSample startTerrain = sampleLandBasisWithoutSlope(start.x(), start.z());
HydrologyTerrainSample endTerrain = sampleLandBasisWithoutSlope(end.x(), end.z());
if (startTerrain == null || endTerrain == null) {
return true;
}
@@ -4670,7 +4753,7 @@ public final class HydrologyPlanner {
int threshold = settings.hydraulics().waterfallMinimumDrop();
for (int pointIndex = 1; pointIndex < crossing.size() - 1; pointIndex++) {
HydrologyPoint point = crossing.get(pointIndex);
HydrologyTerrainSample terrain = sampleLandBasis(point.x(), point.z());
HydrologyTerrainSample terrain = sampleLandBasisWithoutSlope(point.x(), point.z());
if (terrain == null || boundaryHeight - terrain.naturalHeight() >= threshold) {
return true;
}
@@ -5275,7 +5358,7 @@ public final class HydrologyPlanner {
preferredHeads[0] = initialHead;
for (int index = 0; index < pairCount; index++) {
HydrologyPoint point = path.points().get(index);
HydrologyTerrainSample terrain = sampleBasis(point.x(), point.z());
HydrologyTerrainSample terrain = sampleBasisWithoutSlope(point.x(), point.z());
int discharge = Math.max(1, path.pairEdges().get(index).contributingUndergroundSources());
widths[index] = scaledDimension(
sampleGeometry(
@@ -5978,8 +6061,8 @@ public final class HydrologyPlanner {
for (int pointIndex = 0; pointIndex < raster.size() - 1; pointIndex++) {
HydrologyPoint current = raster.get(pointIndex);
HydrologyPoint downstream = raster.get(pointIndex + 1);
HydrologyTerrainSample currentTerrain = sampleBasis(current.x(), current.z());
HydrologyTerrainSample downstreamTerrain = sampleBasis(downstream.x(), downstream.z());
HydrologyTerrainSample currentTerrain = sampleBasisWithoutSlope(current.x(), current.z());
HydrologyTerrainSample downstreamTerrain = sampleBasisWithoutSlope(downstream.x(), downstream.z());
long decline = (long) currentTerrain.naturalHeight() - downstreamTerrain.naturalHeight();
if (decline > strongestDecline) {
strongestDecline = decline;
@@ -6078,11 +6161,29 @@ public final class HydrologyPlanner {
edges.getFirst().upstreamNodeId(),
outlet.id()
);
List<HydrologyPoint> points = surfaceCourseCenterline(
Objects.requireNonNull(grid, "Surface course routing grid is required"),
surfaceGuide,
geometryId
);
Objects.requireNonNull(grid, "Surface course routing grid is required");
PlanningSamples samples = planningSamples.get();
DraftProfile profile = currentDraftProfile();
profile.routeCalls++;
List<HydrologyPoint> points;
if (samples == null) {
profile.routeSolves++;
long solveStarted = System.nanoTime();
points = surfaceCourseCenterline(grid, surfaceGuide, geometryId);
profile.routeSolveNanos += System.nanoTime() - solveStarted;
} else {
HashMap<SurfaceRouteKey, List<HydrologyPoint>> routes = samples.surfaceRoutes.computeIfAbsent(
grid, ignored -> new HashMap<>()
);
SurfaceRouteKey routeKey = new SurfaceRouteKey(surfaceGuide, geometryId);
points = routes.computeIfAbsent(routeKey, ignored -> {
profile.routeSolves++;
long solveStarted = System.nanoTime();
List<HydrologyPoint> solved = surfaceCourseCenterline(grid, surfaceGuide, geometryId);
profile.routeSolveNanos += System.nanoTime() - solveStarted;
return solved;
});
}
if (points.size() < 2) {
return null;
}
@@ -7865,6 +7966,7 @@ public final class HydrologyPlanner {
private final HashMap<Integer, List<GridOffset>> radialOffsets;
private final HashMap<UndergroundSegmentCapKey, Integer> undergroundSegmentCaps;
private final HashMap<Long, HydrologyPoint> routeAnchors;
private final IdentityHashMap<SampledGrid, HashMap<SurfaceRouteKey, List<HydrologyPoint>>> surfaceRoutes;
private final HydrologyCaveCourseFilter.CandidateCache caveCandidates;
private final HydrologyCaveContainmentPlanner.ValidationCache caveValidations;
@@ -7878,6 +7980,7 @@ public final class HydrologyPlanner {
this.radialOffsets = new HashMap<>();
this.undergroundSegmentCaps = new HashMap<>();
this.routeAnchors = new HashMap<>();
this.surfaceRoutes = new IdentityHashMap<>();
this.caveCandidates = new HydrologyCaveCourseFilter.CandidateCache();
this.caveValidations = new HydrologyCaveContainmentPlanner.ValidationCache();
}
@@ -7924,6 +8027,9 @@ public final class HydrologyPlanner {
private record RoutePosition(double x, double z, double baseX, double baseZ, Direction tangent) {
}
private record SurfaceRouteKey(List<HydrologyPoint> guide, long geometryId) {
}
private record RouteCandidate(
HydrologyPoint point,
double offset,
@@ -8307,6 +8413,9 @@ public final class HydrologyPlanner {
) {
}
private record RoutingContextLoad(Thread owner, CompletableFuture<SourceRoutingContext> future) {
}
private record SourceRoutingContext(
SampledGrid grid,
RoutingPlan surfaceRouting,
@@ -8385,16 +8494,20 @@ public final class HydrologyPlanner {
}
}
@FunctionalInterface
private interface CrossTileDraftAdmission {
void prepare();
CrossTilePublicationAdmission admit(HydrologyCaveCourseFilter.Result result);
}
private final class ColorRankedDraftAdmission implements CrossTileDraftAdmission {
private final class ColorRankedDraftAdmission implements CrossTileDraftAdmission, AutoCloseable {
private final HydrologyTileKey ownerKey;
private final int ownerRank;
private final CrossTileResolutionContext context;
private final Set<Long> rejectedCourseIds;
private final Map<HydrologyTileKey, HydrologyForkJoin.Task<CrossTileResolvedOwner>> preparedOwners = new LinkedHashMap<>();
private final Set<HydrologyTileKey> demandedOwners = new HashSet<>();
private Throwable primaryFailure;
private final LinkedHashMap<CrossTileRejectionKey, CrossTileRejectedCourse> observedRejections;
private ColorRankedDraftAdmission(
@@ -8409,6 +8522,77 @@ public final class HydrologyPlanner {
this.observedRejections = new LinkedHashMap<>();
}
@Override
public void prepare() {
if (ownerRank == 0 || settings.crossTileColorPeriod() != 2 || !hasRoutedSourceSearch()
|| !(Thread.currentThread() instanceof ForkJoinWorkerThread worker)) {
return;
}
int radius = settings.crossTileColorPeriod() - 1;
ArrayList<HydrologyTileKey> candidates = new ArrayList<>();
for (long tileZ = (long) ownerKey.tileZ() - radius; tileZ <= (long) ownerKey.tileZ() + radius; tileZ++) {
for (long tileX = (long) ownerKey.tileX() - radius; tileX <= (long) ownerKey.tileX() + radius; tileX++) {
if (tileX < Integer.MIN_VALUE || tileX > Integer.MAX_VALUE
|| tileZ < Integer.MIN_VALUE || tileZ > Integer.MAX_VALUE) {
continue;
}
HydrologyTileKey key = new HydrologyTileKey((int) tileX, (int) tileZ);
if (ownerColorRank(key) >= ownerRank || preparedOwners.containsKey(key)
|| resolvedOwners.getIfPresent(key) != null || resolvingOwners.containsKey(key)) {
continue;
}
candidates.add(key);
}
}
candidates.sort(Comparator.comparingInt(HydrologyPlanner.this::ownerColorRank).reversed()
.thenComparing(HydrologyTileKey::compareTo));
for (HydrologyTileKey key : candidates) {
if (resolvedOwners.getIfPresent(key) != null || resolvingOwners.containsKey(key)) {
continue;
}
HydrologyForkJoin.Task<CrossTileResolvedOwner> task = new HydrologyForkJoin.Task<>(() -> resolveIndependentOwner(key));
preparedOwners.put(key, task);
try {
worker.getPool().execute(task);
} catch (RuntimeException | Error failure) {
preparedOwners.remove(key);
throw failure;
}
currentDraftProfile().earlyOwners++;
}
}
@Override
public void close() {
Error failure = null;
Map<HydrologyTileKey, RuntimeException> unusedFailures = new LinkedHashMap<>();
for (Map.Entry<HydrologyTileKey, HydrologyForkJoin.Task<CrossTileResolvedOwner>> entry : preparedOwners.entrySet()) {
try {
entry.getValue().await();
} catch (RuntimeException unusedFailure) {
if (!demandedOwners.contains(entry.getKey())) {
unusedFailures.put(entry.getKey(), unusedFailure);
}
} catch (Error failed) {
if (failed == primaryFailure) {
continue;
}
if (failure == null) {
failure = failed;
} else if (failure != failed) {
failure.addSuppressed(failed);
}
}
}
for (Map.Entry<HydrologyTileKey, RuntimeException> entry : unusedFailures.entrySet()) {
IrisLogging.reportError("Unused early hydrology owner " + entry.getKey().tileX()
+ "," + entry.getKey().tileZ() + " failed to prepare.", entry.getValue());
}
if (failure != null) {
throw failure;
}
}
@Override
public CrossTilePublicationAdmission admit(HydrologyCaveCourseFilter.Result result) {
context.recordIteration();
@@ -8416,11 +8600,13 @@ public final class HydrologyPlanner {
result,
rejectedCourseIds
);
demandedOwners.addAll(conflictOwnerKeys(ownerKey, current));
CrossTileBlockers blockers = lowerRankBlockers(
ownerKey,
current,
ownerRank,
context
context,
preparedOwners
);
HydrologyCrossTileSurfaceAdmission.Result surfaceAdmission =
HydrologyCrossTileSurfaceAdmission.admit(
@@ -8536,6 +8722,14 @@ public final class HydrologyPlanner {
private long dependencyNanos;
private int dependencies;
private int admissions;
private int earlyOwners;
private int routeCalls;
private int routeSolves;
private long routeSolveNanos;
private int rasterCalls;
private long rasterNanos;
private int filterCalls;
private long filterNanos;
private long record(DraftPhase phase, long started) {
long now = System.nanoTime();
@@ -8551,7 +8745,7 @@ public final class HydrologyPlanner {
private void log(HydrologyTileKey key, int rank, long totalNanos) {
IrisLogging.debug(
"Hydrology owner %d,%d rank=%d drafted in %dms: context=%dms select=%dms settle=%dms publish=%dms deps=%d wait=%dms admissions=%d on %s",
"Hydrology owner %d,%d rank=%d drafted in %dms: context=%dms select=%dms settle=%dms publish=%dms deps=%d wait=%dms admissions=%d earlyOwners=%d routes=%d reuses=%d routeSolve=%dms rasters=%d raster=%dms filters=%d filter=%dms on %s",
key.tileX(),
key.tileZ(),
rank,
@@ -8563,6 +8757,14 @@ public final class HydrologyPlanner {
dependencies,
dependencyNanos / 1_000_000L,
admissions,
earlyOwners,
routeCalls,
routeCalls - routeSolves,
routeSolveNanos / 1_000_000L,
rasterCalls,
rasterNanos / 1_000_000L,
filterCalls,
filterNanos / 1_000_000L,
Thread.currentThread().getName()
);
}
@@ -10,6 +10,7 @@ import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;
@@ -48,12 +49,17 @@ public final class HydrologyTileCache implements AutoCloseable {
private final Executor prefetchExecutor;
private final ConcurrentLinkedQueue<HydrologyTileKey> prefetchQueue;
private final Set<HydrologyTileKey> queuedPrefetches;
private final Set<CacheLoadKey<HydrologyTileKey>> demandedPlans;
private final AtomicBoolean prefetchActive;
private final ConcurrentHashMap<HydrologyTileKey, CompletableFuture<HydrologyTile>> planning;
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 Object publicationLock = new Object();
private final BooleanSupplier waitingForbidden;
private volatile SharedCacheScope sharedCacheScope;
private volatile StudioHydrologyTileStore persistentStore;
private volatile boolean neighbourPrefetchEnabled;
private int demandBatches;
public HydrologyTileCache(HydrologyPlanner planner) {
this(planner, DEFAULT_MAXIMUM_ENTRIES, null, null, null);
@@ -96,12 +102,15 @@ public final class HydrologyTileCache implements AutoCloseable {
this.sharedCacheScope = sharedCacheScope;
this.persistentStore = null;
this.planning = new ConcurrentHashMap<>();
this.loading = new ConcurrentHashMap<>();
this.composing = new ConcurrentHashMap<>();
if (maximumEntries < 1) {
throw new IllegalArgumentException("maximumEntries must be positive.");
}
this.maximumEntries = maximumEntries;
this.prefetchQueue = new ConcurrentLinkedQueue<>();
this.queuedPrefetches = ConcurrentHashMap.newKeySet();
this.demandedPlans = new HashSet<>();
this.prefetchActive = new AtomicBoolean();
this.tiles = Caffeine.newBuilder()
.maximumSize(maximumEntries)
@@ -122,10 +131,13 @@ public final class HydrologyTileCache implements AutoCloseable {
*/
public HydrologyTile get(HydrologyTileKey key) {
Objects.requireNonNull(key, "key");
if (prefetchExecutor == null || plansInline()) {
return planInline(key);
if (prefetchExecutor == null) {
return planInline(key, cacheEpoch.get());
}
return awaitTile(planDemandAsync(key));
if (plansInline()) {
return planDemandInline(key);
}
return awaitPlan(planDemandAsync(key));
}
private boolean plansInline() {
@@ -137,12 +149,33 @@ public final class HydrologyTileCache implements AutoCloseable {
&& worker.getPool() == pool;
}
private HydrologyTile planInline(HydrologyTileKey key) {
if (tiles.getIfPresent(key) == null && planning.containsKey(key)) {
IrisLogging.debug("Hydrology tile %d,%d awaited by %s while the planning pool plans it",
key.tileX(), key.tileZ(), Thread.currentThread().getName());
private HydrologyTile planInline(HydrologyTileKey key, long epoch) {
HydrologyTile present = tiles.getIfPresent(key);
if (present != null) {
return present;
}
CacheLoadKey<HydrologyTileKey> loadKey = new CacheLoadKey<>(epoch, key);
PendingLoad<HydrologyTile> owned = new PendingLoad<>(Thread.currentThread(), new CompletableFuture<>());
PendingLoad<HydrologyTile> existing = loading.putIfAbsent(loadKey, owned);
if (existing != null) {
return awaitLoad(existing);
}
try {
present = tiles.getIfPresent(key);
HydrologyTile tile = present == null ? planOrEmpty(key, epoch) : present;
synchronized (publicationLock) {
if (!closed.get() && cacheEpoch.get() == epoch) {
tiles.put(key, tile);
}
}
owned.future().complete(tile);
return tile;
} catch (Throwable failure) {
owned.future().completeExceptionally(failure);
throw failure;
} finally {
loading.remove(loadKey, owned);
}
return tiles.get(key, this::planOrEmpty);
}
/**
@@ -152,8 +185,7 @@ public final class HydrologyTileCache implements AutoCloseable {
* stays consistent. A plan that was interrupted is not a bad tile: it is rethrown without being
* cached so the next request plans it again.
*/
private HydrologyTile planOrEmpty(HydrologyTileKey key) {
long planningEpoch = cacheEpoch.get();
private HydrologyTile planOrEmpty(HydrologyTileKey key, long planningEpoch) {
SharedTileKey sharedKey = sharedCacheScope == null ? null : new SharedTileKey(sharedCacheScope, key);
HydrologyTile shared = sharedKey == null ? null : SHARED_TILES.getIfPresent(sharedKey);
if (shared != null && validSharedTile(shared, sharedKey)) {
@@ -176,10 +208,12 @@ public final class HydrologyTileCache implements AutoCloseable {
}
try {
HydrologyTile planned = planner.plan(key);
if (sharedKey != null && !closed.get() && planningEpoch == cacheEpoch.get()) {
HydrologyTile existing = SHARED_TILES.asMap().putIfAbsent(sharedKey, planned);
if (existing == null) {
IrisLogging.debug("Published shared Studio hydrology tile %d,%d", key.tileX(), key.tileZ());
synchronized (publicationLock) {
if (sharedKey != null && !closed.get() && planningEpoch == cacheEpoch.get()) {
HydrologyTile existing = SHARED_TILES.asMap().putIfAbsent(sharedKey, planned);
if (existing == null) {
IrisLogging.debug("Published shared Studio hydrology tile %d,%d", key.tileX(), key.tileZ());
}
}
}
if (store != null && persistentStudioKey(key) && !closed.get() && planningEpoch == cacheEpoch.get()) {
@@ -277,14 +311,18 @@ public final class HydrologyTileCache implements AutoCloseable {
}
public void clear() {
cacheEpoch.incrementAndGet();
planner.clearOwnerDrafts();
synchronized (prefetchQueue) {
synchronized (publicationLock) {
cacheEpoch.incrementAndGet();
planner.clearOwnerDrafts();
tiles.invalidateAll();
composedChunks.invalidateAll();
}
prefetchQueue.clear();
queuedPrefetches.clear();
demandedPlans.clear();
demandBatches = 0;
}
tiles.invalidateAll();
composedChunks.invalidateAll();
localChunkColumns.remove();
}
@@ -302,16 +340,19 @@ public final class HydrologyTileCache implements AutoCloseable {
}
public void preparePregeneration(int centerBlockX, int centerBlockZ) {
neighbourPrefetchEnabled = true;
int tileSize = planner.settings().routing().tileSize();
prefetchArea(
Math.subtractExact(centerBlockX, tileSize),
Math.subtractExact(centerBlockZ, tileSize),
Math.addExact(centerBlockX, tileSize),
Math.addExact(centerBlockZ, tileSize),
int minimumBlockX = Math.subtractExact(centerBlockX, tileSize);
int minimumBlockZ = Math.subtractExact(centerBlockZ, tileSize);
int maximumBlockX = Math.addExact(centerBlockX, tileSize);
int maximumBlockZ = Math.addExact(centerBlockZ, tileSize);
enqueuePrefetchArea(prefetchAreaKeys(
minimumBlockX,
minimumBlockZ,
maximumBlockX,
maximumBlockZ,
centerBlockX,
centerBlockZ
);
), true);
}
public void enableSharedCache(SharedCacheScope scope, Path persistentRoot) {
@@ -366,14 +407,41 @@ public final class HydrologyTileCache implements AutoCloseable {
// The empty result is never cached, so the next query composes the real columns.
return ChunkColumns.empty(chunkX, chunkZ);
}
ChunkColumns columns = composedChunks.get(
packedChunk,
ignored -> composeChunkColumns(chunkX, chunkZ)
);
ChunkColumns columns = loadChunkColumns(new CacheLoadKey<>(epoch, packedChunk), chunkX, chunkZ);
localChunkColumns.set(new LocalChunkColumns(epoch, columns));
return columns;
}
private ChunkColumns loadChunkColumns(CacheLoadKey<Long> loadKey, int chunkX, int chunkZ) {
long epoch = loadKey.epoch();
long packedChunk = loadKey.key();
ChunkColumns present = composedChunks.getIfPresent(packedChunk);
if (present != null) {
return present;
}
PendingLoad<ChunkColumns> owned = new PendingLoad<>(Thread.currentThread(), new CompletableFuture<>());
PendingLoad<ChunkColumns> existing = composing.putIfAbsent(loadKey, owned);
if (existing != null) {
return awaitLoad(existing);
}
try {
present = composedChunks.getIfPresent(packedChunk);
ChunkColumns columns = present == null ? composeChunkColumns(chunkX, chunkZ) : present;
synchronized (publicationLock) {
if (!closed.get() && cacheEpoch.get() == epoch) {
composedChunks.put(packedChunk, columns);
}
}
owned.future().complete(columns);
return columns;
} catch (Throwable failure) {
owned.future().completeExceptionally(failure);
throw failure;
} finally {
composing.remove(loadKey, owned);
}
}
/** The tiles a chunk's columns compose from: every tile within the publication radius of the chunk. */
private ArrayList<HydrologyTileKey> relevantKeys(int chunkX, int chunkZ) {
int minimumBlockX = Math.multiplyExact(chunkX, CHUNK_SIZE);
@@ -447,6 +515,12 @@ public final class HydrologyTileCache implements AutoCloseable {
if (prefetchExecutor == null) {
return;
}
enqueuePrefetchArea(prefetchAreaKeys(minimumBlockX, minimumBlockZ, maximumBlockX, maximumBlockZ,
centreBlockX, centreBlockZ), false);
}
private List<HydrologyTileKey> prefetchAreaKeys(int minimumBlockX, int minimumBlockZ, int maximumBlockX,
int maximumBlockZ, int centreBlockX, int centreBlockZ) {
int tileSize = planner.settings().routing().tileSize();
int publicationRadius = planner.settings().publicationRadius();
int minimumTileX = tileCoordinate((long) minimumBlockX - publicationRadius, tileSize);
@@ -464,31 +538,66 @@ public final class HydrologyTileCache implements AutoCloseable {
keys.sort(Comparator.comparingInt((HydrologyTileKey key) -> Math.max(Math.abs(key.tileX() - centreTileX), Math.abs(key.tileZ() - centreTileZ)))
.thenComparingInt(HydrologyTileKey::tileZ)
.thenComparingInt(HydrologyTileKey::tileX));
for (HydrologyTileKey key : keys) {
if (!prefetch(key) && speculativeEntryCount() >= maximumEntries) {
break;
return keys;
}
private void enqueuePrefetchArea(List<HydrologyTileKey> keys, boolean discardOutsideArea) {
Set<HydrologyTileKey> area = discardOutsideArea ? new HashSet<>(keys) : Set.of();
tiles.cleanUp();
synchronized (prefetchQueue) {
if (discardOutsideArea) {
prefetchQueue.removeIf(key -> {
if (!area.contains(key)) {
queuedPrefetches.remove(key);
return true;
}
return false;
});
neighbourPrefetchEnabled = true;
}
if (prefetchExecutor == null) {
return;
}
for (HydrologyTileKey key : keys) {
if (!enqueuePrefetch(key) && speculativeEntryCount() >= maximumEntries) {
break;
}
}
}
pumpPrefetch();
}
private boolean prefetch(HydrologyTileKey key) {
if (closed.get()
|| tiles.getIfPresent(key) != null
|| planning.containsKey(new CacheLoadKey<>(cacheEpoch.get(), key))
|| queuedPrefetches.contains(key)) {
return false;
}
tiles.cleanUp();
synchronized (prefetchQueue) {
if (closed.get()
|| tiles.getIfPresent(key) != null
|| planning.containsKey(key)
|| queuedPrefetches.contains(key)
|| speculativeEntryCount() >= maximumEntries) {
if (!enqueuePrefetch(key)) {
return false;
}
queuedPrefetches.add(key);
prefetchQueue.add(key);
}
pumpPrefetch();
return true;
}
private boolean enqueuePrefetch(HydrologyTileKey key) {
if (closed.get()
|| tiles.getIfPresent(key) != null
|| planning.containsKey(new CacheLoadKey<>(cacheEpoch.get(), key))
|| queuedPrefetches.contains(key)
|| speculativeEntryCount() >= maximumEntries) {
return false;
}
queuedPrefetches.add(key);
prefetchQueue.add(key);
return true;
}
private int speculativeEntryCount() {
tiles.cleanUp();
return Math.toIntExact(Math.min(
maximumEntries,
tiles.estimatedSize() + planning.size() + queuedPrefetches.size()
@@ -496,22 +605,30 @@ public final class HydrologyTileCache implements AutoCloseable {
}
private void pumpPrefetch() {
if (prefetchExecutor == null || closed.get() || !prefetchActive.compareAndSet(false, true)) {
return;
synchronized (prefetchQueue) {
if (prefetchExecutor == null || closed.get() || demandBatches != 0 || !demandedPlans.isEmpty()
|| !prefetchActive.compareAndSet(false, true)) {
return;
}
}
scheduleNextPrefetch();
}
private void scheduleNextPrefetch() {
HydrologyTileKey key = pollPrefetch();
if (key == null || closed.get()) {
prefetchActive.set(false);
if (!prefetchQueue.isEmpty()) {
pumpPrefetch();
CacheLoadKey<HydrologyTileKey> loadKey;
synchronized (prefetchQueue) {
if (closed.get() || demandBatches != 0 || !demandedPlans.isEmpty()) {
prefetchActive.set(false);
return;
}
return;
HydrologyTileKey key = pollPrefetch();
if (key == null) {
prefetchActive.set(false);
return;
}
loadKey = new CacheLoadKey<>(cacheEpoch.get(), key);
}
planAsync(key).whenComplete((tile, failure) -> scheduleNextPrefetch());
planAsync(loadKey).whenComplete((tile, failure) -> scheduleNextPrefetch());
}
private HydrologyTileKey pollPrefetch() {
@@ -536,65 +653,154 @@ public final class HydrologyTileCache implements AutoCloseable {
public List<HydrologyTile> tiles(List<HydrologyTileKey> keys) {
Objects.requireNonNull(keys, "keys");
HydrologyTile[] loaded = new HydrologyTile[keys.size()];
if (prefetchExecutor == null || plansInline()) {
boolean missing = false;
for (int index = 0; index < loaded.length; index++) {
loaded[index] = tiles.getIfPresent(keys.get(index));
missing |= loaded[index] == null;
}
if (!missing) {
return List.of(loaded);
}
if (prefetchExecutor == null) {
for (int index = 0; index < loaded.length; index++) {
loaded[index] = planInline(keys.get(index));
if (loaded[index] == null) {
loaded[index] = get(keys.get(index));
}
}
return List.of(loaded);
}
int batch = Math.max(1, maximumEntries / 2);
ArrayList<CompletableFuture<HydrologyTile>> futures = new ArrayList<>(Math.min(batch, loaded.length));
for (int start = 0; start < loaded.length; start += batch) {
int end = Math.min(loaded.length, start + batch);
futures.clear();
for (int index = start; index < end; index++) {
futures.add(planDemandAsync(keys.get(index)));
long demandEpoch = beginDemandBatch();
try {
if (plansInline()) {
for (int index = 0; index < loaded.length; index++) {
if (loaded[index] == null) {
loaded[index] = get(keys.get(index));
}
}
return List.of(loaded);
}
for (int index = start; index < end; index++) {
loaded[index] = awaitTile(futures.get(index - start));
int batch = Math.max(1, maximumEntries / 2);
ArrayList<CompletableFuture<HydrologyTile>> futures = new ArrayList<>(Math.min(batch, loaded.length));
for (int start = 0; start < loaded.length; start += batch) {
int end = Math.min(loaded.length, start + batch);
futures.clear();
for (int index = start; index < end; index++) {
futures.add(planDemandAsync(keys.get(index)));
}
for (int index = start; index < end; index++) {
loaded[index] = awaitPlan(futures.get(index - start));
}
}
return List.of(loaded);
} finally {
finishDemandBatch(demandEpoch);
}
}
private long beginDemandBatch() {
synchronized (prefetchQueue) {
demandBatches++;
return cacheEpoch.get();
}
}
private void finishDemandBatch(long epoch) {
synchronized (prefetchQueue) {
if (epoch == cacheEpoch.get()) {
demandBatches--;
}
}
return List.of(loaded);
pumpPrefetch();
}
/**
* The tile, planned on the prefetch executor unless it is cached or already being planned, in which
* case the caller shares that plan instead of occupying a second executor thread with it.
*/
private CompletableFuture<HydrologyTile> planAsync(HydrologyTileKey key) {
private CompletableFuture<HydrologyTile> planAsync(CacheLoadKey<HydrologyTileKey> loadKey) {
HydrologyTileKey key = loadKey.key();
HydrologyTile present = tiles.getIfPresent(key);
if (present != null) {
return CompletableFuture.completedFuture(present);
}
CompletableFuture<HydrologyTile> future = new CompletableFuture<>();
CompletableFuture<HydrologyTile> existing = planning.putIfAbsent(key, future);
CompletableFuture<HydrologyTile> existing = planning.putIfAbsent(loadKey, future);
if (existing != null) {
return existing;
}
try {
prefetchExecutor.execute(() -> {
try {
future.complete(planInline(key));
future.complete(planInline(key, loadKey.epoch()));
} catch (Throwable failure) {
future.completeExceptionally(failure);
IrisLogging.reportError(failure);
} finally {
planning.remove(key, future);
planning.remove(loadKey, future);
}
});
} catch (RuntimeException rejected) {
planning.remove(key, future);
planning.remove(loadKey, future);
future.completeExceptionally(rejected);
}
return future;
}
private CompletableFuture<HydrologyTile> planDemandAsync(HydrologyTileKey key) {
queuedPrefetches.remove(key);
return planAsync(key);
HydrologyTile present = tiles.getIfPresent(key);
if (present != null) {
return CompletableFuture.completedFuture(present);
}
CacheLoadKey<HydrologyTileKey> loadKey = new CacheLoadKey<>(cacheEpoch.get(), key);
boolean tracked = beginDemand(loadKey);
CompletableFuture<HydrologyTile> future = planAsync(loadKey);
if (tracked) {
future.whenComplete((tile, failure) -> finishDemand(loadKey));
}
return future;
}
private static HydrologyTile awaitTile(CompletableFuture<HydrologyTile> future) {
private HydrologyTile planDemandInline(HydrologyTileKey key) {
HydrologyTile present = tiles.getIfPresent(key);
if (present != null) {
return present;
}
CacheLoadKey<HydrologyTileKey> loadKey = new CacheLoadKey<>(cacheEpoch.get(), key);
boolean tracked = beginDemand(loadKey);
try {
return planInline(key, loadKey.epoch());
} finally {
if (tracked) {
finishDemand(loadKey);
}
}
}
private boolean beginDemand(CacheLoadKey<HydrologyTileKey> loadKey) {
synchronized (prefetchQueue) {
if (loadKey.epoch() != cacheEpoch.get()) {
return false;
}
queuedPrefetches.remove(loadKey.key());
return demandedPlans.add(loadKey);
}
}
private void finishDemand(CacheLoadKey<HydrologyTileKey> loadKey) {
synchronized (prefetchQueue) {
demandedPlans.remove(loadKey);
}
pumpPrefetch();
}
private static <T> T awaitLoad(PendingLoad<T> load) {
if (load.owner() == Thread.currentThread()) {
throw new IllegalStateException("Recursive hydrology cache load.");
}
return awaitPlan(load.future());
}
private static <T> T awaitPlan(CompletableFuture<T> future) {
try {
return future.join();
} catch (CompletionException failure) {
@@ -697,6 +903,12 @@ public final class HydrologyTileCache implements AutoCloseable {
private record LocalChunkColumns(long epoch, ChunkColumns columns) {
}
private record CacheLoadKey<K>(long epoch, K key) {
}
private record PendingLoad<T>(Thread owner, CompletableFuture<T> future) {
}
public record SharedCacheScope(
String runtimeIdentity,
long worldSeed,
@@ -24,6 +24,7 @@ import art.arcane.iris.engine.mantle.MantleWriter;
import art.arcane.iris.engine.object.IrisCaveFieldModule;
import art.arcane.iris.engine.object.IrisCaveProfile;
import art.arcane.iris.engine.object.IrisRange;
import art.arcane.iris.engine.object.IrisGeneratorStyle;
import art.arcane.iris.engine.object.IrisStyledRange;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
@@ -38,6 +39,11 @@ import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinWorkerThread;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
public class IrisCaveCarver3D {
private static final byte LIQUID_AIR = 0;
@@ -51,6 +57,12 @@ public class IrisCaveCarver3D {
private static final int SURFACE_CEILING_FADE_DEPTH = 12;
private static final double SURFACE_CEILING_SOLID_EPSILON = 0.000001D;
private static final byte DENSITY_CARVE = 1;
private static final byte DENSITY_FLUID = 2;
private static final int MINIMUM_PARALLEL_DENSITY_CELLS = 16_384;
private static final int MAXIMUM_DENSITY_WORKERS = 4;
private static final int[] LATTICE_TILE_OFFSETS = {0, 1, 16, 17};
private final Engine engine;
private final IrisData data;
private final IrisCaveProfile profile;
@@ -79,6 +91,8 @@ public class IrisCaveCarver3D {
private final int fluidMinDepthBelowSurface;
private final int fluidHeight;
private final int aquiferCeilingY;
private final boolean parallelDensity;
private final ThreadLocal<Boolean> carving = ThreadLocal.withInitial(() -> false);
private final ThreadLocal<CaveCarveScratch> scratchCache = ThreadLocal.withInitial(CaveCarveScratch::new);
public IrisCaveCarver3D(Engine engine, IrisCaveProfile profile) {
@@ -125,6 +139,7 @@ public class IrisCaveCarver3D {
hasModules = modules.length > 0;
detailMinContribution = -detailWeight;
detailMaxContribution = detailWeight;
parallelDensity = fixedDensityStyles(profile);
}
public int carve(MantleWriter writer, int chunkX, int chunkZ) {
@@ -204,6 +219,12 @@ public class IrisCaveCarver3D {
CaveFluidSupportPlan fluidSupportPlan
) {
PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start();
boolean nested = carving.get();
CaveCarveScratch previousScratch = scratchCache.get();
if (nested) {
scratchCache.set(new CaveCarveScratch());
}
carving.set(true);
try {
CaveCarveScratch scratch = scratchCache.get();
if (columnWeights == null || columnWeights.length < 256) {
@@ -415,10 +436,289 @@ public class IrisCaveCarver3D {
return carved;
} finally {
carving.set(nested);
if (nested) {
scratchCache.set(previousScratch);
}
engine.getMetrics().getCarveApply().put(applyStopwatch.getMilliseconds());
}
}
private static boolean fixedDensityStyles(IrisCaveProfile profile) {
if (!fixedStyle(profile.getBaseDensityStyle()) || !fixedStyle(profile.getDetailDensityStyle())
|| !fixedStyle(profile.getWarpStyle()) || !fixedStyle(profile.getSurfaceBreakStyle())
|| !fixedStyle(profile.getDensityThreshold().getStyle())) {
return false;
}
for (IrisCaveFieldModule module : profile.getModules()) {
if (!fixedStyle(module.getStyle())) {
return false;
}
}
return true;
}
private static boolean fixedStyle(IrisGeneratorStyle style) {
for (IrisGeneratorStyle current = style; current != null; current = current.getFracture()) {
if (current.getExpression() != null) {
return false;
}
}
return true;
}
private byte[] classifyPassParallel(DensityPass pass) {
int height = pass.maximumY() - pass.minimumY() + 1;
if (!parallelDensity || height < 64
|| (long) height * pass.columnCount() < MINIMUM_PARALLEL_DENSITY_CELLS
|| !(Thread.currentThread() instanceof ForkJoinWorkerThread worker)) {
return null;
}
ForkJoinPool pool = worker.getPool();
int workers = Math.min(MAXIMUM_DENSITY_WORKERS, pool.getParallelism());
if (workers < 2) {
return null;
}
byte[] classified = new byte[Math.multiplyExact(height, 256)];
int rows = Math.ceilDiv(height, workers);
ArrayList<Runnable> tasks = new ArrayList<>(workers);
for (int index = 0; index < workers; index++) {
int minimumY = pass.minimumY() + index * rows;
int maximumY = Math.min(pass.maximumY(), minimumY + rows - 1);
if (minimumY <= maximumY) {
tasks.add(() -> classifyRows(pass, minimumY, maximumY, classified));
}
}
runDensityTasks(tasks, pool);
return classified;
}
private void classifyRows(DensityPass pass, int minimumY, int maximumY, byte[] classified) {
CaveCarveScratch scratch = new CaveCarveScratch();
for (int y = minimumY; y <= maximumY; y++) {
int count = 0;
for (int index = 0; index < pass.columnCount(); index++) {
int column = pass.columns()[index];
if (pass.columnTops()[index] < y
|| SurfaceFluidBoundaryPlan.protects(pass.fluidBoundaries(), column, y)) {
continue;
}
double threshold = pass.thresholds()[column];
if (pass.surfaceBreakColumns()[column] && y >= pass.surfaceBreakFloors()[column]) {
threshold += pass.surfaceBreakBoost();
}
threshold -= pass.verticalFade()[y - pass.minimumY()];
threshold = applySurfaceCeilingFade(pass.sourceScratch(), threshold, column, y, pass.minimumY());
scratch.planeColumnIndices[count] = column;
scratch.planeThresholdLimit[count++] = threshold * normalizationFactor;
}
if (count == 0) {
continue;
}
if (pass.adaptiveStep() == 0) {
classifyDensityPlane(scratch, pass.x(), pass.z(), y, scratch.planeColumnIndices,
scratch.planeThresholdLimit, count, scratch.planeCarve);
} else {
classifyDensityPlaneAdaptive(scratch, pass.x(), pass.z(), y, scratch.planeColumnIndices,
scratch.planeThresholdLimit, count, scratch.planeCarve,
pass.adaptiveStep(), pass.adaptiveMargin());
}
int offset = (y - pass.minimumY()) * 256;
for (int index = 0; index < count; index++) {
if (!scratch.planeCarve[index]) {
continue;
}
int column = scratch.planeColumnIndices[index];
double threshold = scratch.planeThresholdLimit[index] * inverseNormalization;
MatterCavern matter = resolveMatter(scratch, pass.matterByY()[y - pass.minimumY()],
pass.x() + PowerOfTwoCoordinates.unpackLocal16X(column), y, pass.z() + (column & 15),
column, pass.fluidTops(), threshold);
classified[offset + column] = matter == carveFluid
? DENSITY_CARVE | DENSITY_FLUID : DENSITY_CARVE;
}
}
}
private double[] sampleLatticeParallel(
int x, int z, int minimumY, int maximumY, int step, double[] thresholds, int[] tops
) {
int rows = (maximumY - minimumY) / step + 1;
if (!parallelDensity || rows < 64
|| !(Thread.currentThread() instanceof ForkJoinWorkerThread worker)) {
return null;
}
ForkJoinPool pool = worker.getPool();
int workers = Math.min(MAXIMUM_DENSITY_WORKERS, pool.getParallelism());
if (workers < 2) {
return null;
}
int[] tileTops = new int[64];
int samples = 0;
for (int tile = 0; tile < 64; tile++) {
int column = (tile >> 3) * 32 + (tile & 7) * 2;
int top = Integer.MIN_VALUE;
for (int offset : LATTICE_TILE_OFFSETS) {
if (!Double.isNaN(thresholds[column + offset])) {
top = Math.max(top, tops[column + offset]);
}
}
tileTops[tile] = top;
if (top >= minimumY) {
samples += (top - minimumY) / step + 1;
}
}
if (samples < 4096) {
return null;
}
double[] densities = new double[Math.multiplyExact(rows, 64)];
LatticePass pass = new LatticePass(x, z, minimumY, step, rows, tileTops);
int tilesPerWorker = Math.ceilDiv(64, workers);
ArrayList<Runnable> tasks = new ArrayList<>(workers);
for (int index = 0; index < workers; index++) {
int firstTile = index * tilesPerWorker;
int lastTile = Math.min(64, firstTile + tilesPerWorker);
tasks.add(() -> sampleLatticeTiles(pass, firstTile, lastTile, densities));
}
runDensityTasks(tasks, pool);
return densities;
}
private void sampleLatticeTiles(LatticePass pass, int firstTile, int lastTile, double[] densities) {
CaveCarveScratch scratch = new CaveCarveScratch();
for (int tile = firstTile; tile < lastTile; tile++) {
int blockX = pass.x() + (tile >> 3) * 2;
int blockZ = pass.z() + (tile & 7) * 2;
int offset = tile * pass.rows();
for (int y = pass.minimumY(); y <= pass.tileTops()[tile]; y += pass.step()) {
densities[offset++] = sampleDensityOptimized(scratch, blockX, y, blockZ);
}
}
}
private static void copyClassifiedPlane(byte[] classified, int row, int[] columns, int count, boolean[] result) {
int offset = row * 256;
for (int index = 0; index < count; index++) {
result[index] = (classified[offset + columns[index]] & DENSITY_CARVE) != 0;
}
}
static void runDensityTasks(List<Runnable> work, ForkJoinPool pool) {
ArrayList<DensityTask> tasks = new ArrayList<>(work.size());
for (Runnable runnable : work) {
tasks.add(new DensityTask(runnable));
}
Throwable failure = null;
for (int index = 1; index < tasks.size(); index++) {
try {
pool.execute(tasks.get(index));
} catch (RuntimeException | Error rejected) {
failure = rejected;
break;
}
}
for (DensityTask task : tasks) {
task.run();
}
boolean interrupted = Thread.interrupted();
for (DensityTask task : tasks) {
boolean complete = false;
boolean managed = true;
while (!complete) {
try {
if (managed) {
ForkJoinPool.managedBlock(task);
} else {
task.complete.await();
}
complete = true;
} catch (InterruptedException interruption) {
interrupted = true;
managed = false;
} catch (RejectedExecutionException unavailable) {
managed = false;
} catch (RuntimeException | Error blockingFailure) {
if (failure == null) {
failure = blockingFailure;
} else if (failure != blockingFailure) {
failure.addSuppressed(blockingFailure);
}
managed = false;
}
}
if (task.failure != null) {
if (failure == null) {
failure = task.failure;
} else if (failure != task.failure) {
failure.addSuppressed(task.failure);
}
}
}
interrupted |= Thread.interrupted();
if (interrupted) {
Thread.currentThread().interrupt();
InterruptedException interruption = new InterruptedException("Cave density classification interrupted.");
if (failure == null) {
failure = new IllegalStateException(interruption);
} else {
failure.addSuppressed(interruption);
}
}
if (failure instanceof Error error) {
throw error;
}
if (failure instanceof RuntimeException runtimeFailure) {
throw runtimeFailure;
}
}
private record LatticePass(int x, int z, int minimumY, int step, int rows, int[] tileTops) {
}
private record DensityPass(
int x, int z, int minimumY, int maximumY,
int[] columns, int[] columnTops, int columnCount, double[] thresholds,
long[] fluidBoundaries, boolean[] surfaceBreakColumns, int[] surfaceBreakFloors,
double surfaceBreakBoost, double[] verticalFade, CaveCarveScratch sourceScratch,
int adaptiveStep, double adaptiveMargin, int[] fluidTops, MatterCavern[] matterByY
) {
}
private static final class DensityTask implements Runnable, ForkJoinPool.ManagedBlocker {
private final Runnable work;
private final AtomicBoolean claimed = new AtomicBoolean();
private final CountDownLatch complete = new CountDownLatch(1);
private Throwable failure;
private DensityTask(Runnable work) {
this.work = work;
}
@Override
public boolean isReleasable() {
return complete.getCount() == 0;
}
@Override
public boolean block() throws InterruptedException {
complete.await();
return true;
}
@Override
public void run() {
if (!claimed.compareAndSet(false, true)) {
return;
}
try {
work.run();
} catch (RuntimeException | Error failed) {
failure = failed;
} finally {
complete.countDown();
}
}
}
private int carvePassExact(
MantleChunk<Matter> chunk,
int x0,
@@ -474,6 +774,11 @@ public class IrisCaveCarver3D {
int passMaxY = Math.min(maxY, activeMaxY);
int maxSection = PowerOfTwoCoordinates.floorDivPow2(passMaxY, 4);
byte[] classified = skipExistingCarved ? null : classifyPassParallel(new DensityPass(
x0, z0, minY, passMaxY, activeColumnIndices, activeColumnTopY, activeColumnCount,
passThreshold, surfaceFluidBoundaries, surfaceBreakColumn, surfaceBreakFloorY,
surfaceBreakThresholdBoost, verticalEdgeFade, scratch, 0, 0D, fluidMaxY, matterByY));
for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) {
int sectionMinY = Math.max(minY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex));
int sectionMaxY = Math.min(passMaxY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex) + 15);
@@ -505,7 +810,11 @@ public class IrisCaveCarver3D {
continue;
}
classifyDensityPlane(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve);
if (classified == null) {
classifyDensityPlane(scratch, x0, z0, y, planeColumnIndices, planeThresholdLimit, planeCount, planeCarve);
} else {
copyClassifiedPlane(classified, y - minY, planeColumnIndices, planeCount, planeCarve);
}
int fadeIndex = y - minY;
int localY = y & 15;
MatterCavern verticalMatter = matterByY[fadeIndex];
@@ -541,8 +850,11 @@ public class IrisCaveCarver3D {
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
int localZ = columnIndex & 15;
double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization;
MatterCavern matter = resolveMatter(scratch, verticalMatter, x0 + localX, y, z0 + localZ,
columnIndex, fluidMaxY, localThreshold);
MatterCavern matter = classified == null
? resolveMatter(scratch, verticalMatter, x0 + localX, y, z0 + localZ,
columnIndex, fluidMaxY, localThreshold)
: (classified[fadeIndex * 256 + columnIndex] & DENSITY_FLUID) != 0
? carveFluid : verticalMatter;
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
carved++;
}
@@ -615,6 +927,12 @@ public class IrisCaveCarver3D {
effectiveAdaptiveSampleStep
);
byte[] classified = skipExistingCarved ? null : classifyPassParallel(new DensityPass(
x0, z0, minY, passMaxY, activeColumnIndices, activeColumnTopY, activeColumnCount,
passThreshold, surfaceFluidBoundaries, surfaceBreakColumn, surfaceBreakFloorY,
surfaceBreakThresholdBoost, verticalEdgeFade, scratch, effectiveAdaptiveSampleStep, effectiveAdaptiveThresholdMargin,
fluidMaxY, matterByY));
for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) {
int sectionMinY = Math.max(minY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex));
int sectionMaxY = Math.min(passMaxY, PowerOfTwoCoordinates.chunkToBlock(sectionIndex) + 15);
@@ -646,18 +964,22 @@ public class IrisCaveCarver3D {
continue;
}
classifyDensityPlaneAdaptive(
scratch,
x0,
z0,
y,
planeColumnIndices,
planeThresholdLimit,
planeCount,
planeCarve,
effectiveAdaptiveSampleStep,
effectiveAdaptiveThresholdMargin
);
if (classified == null) {
classifyDensityPlaneAdaptive(
scratch,
x0,
z0,
y,
planeColumnIndices,
planeThresholdLimit,
planeCount,
planeCarve,
effectiveAdaptiveSampleStep,
effectiveAdaptiveThresholdMargin
);
} else {
copyClassifiedPlane(classified, y - minY, planeColumnIndices, planeCount, planeCarve);
}
int fadeIndex = y - minY;
int localY = y & 15;
MatterCavern verticalMatter = matterByY[fadeIndex];
@@ -693,8 +1015,11 @@ public class IrisCaveCarver3D {
int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex);
int localZ = columnIndex & 15;
double localThreshold = planeThresholdLimit[planeIndex] * inverseNormalization;
MatterCavern matter = resolveMatter(scratch, verticalMatter, x0 + localX, y, z0 + localZ,
columnIndex, fluidMaxY, localThreshold);
MatterCavern matter = classified == null
? resolveMatter(scratch, verticalMatter, x0 + localX, y, z0 + localZ,
columnIndex, fluidMaxY, localThreshold)
: (classified[fadeIndex * 256 + columnIndex] & DENSITY_FLUID) != 0
? carveFluid : verticalMatter;
writeCavern(cavernSlice, localX, y, localZ, matter, fluidSupportPlan);
carved++;
}
@@ -756,6 +1081,9 @@ public class IrisCaveCarver3D {
passThreshold[index] = columnThreshold[index] + thresholdBoost - ((1D - columnWeight) * thresholdPenalty);
}
double[] latticeDensity = sampleLatticeParallel(x0, z0, minY, maxY, latticeStep, passThreshold, columnMaxY);
int latticeRows = (maxY - minY) / latticeStep + 1;
for (int lx = 0; lx < 16; lx += 2) {
int x = x0 + lx;
int lx1 = lx + 1;
@@ -815,7 +1143,9 @@ public class IrisCaveCarver3D {
}
for (int y = minY; y <= tileMaxY; y += latticeStep) {
double density = sampleDensityOptimized(scratch, x, y, z);
double density = latticeDensity == null
? sampleDensityOptimized(scratch, x, y, z)
: latticeDensity[((lx >> 1) * 8 + (lz >> 1)) * latticeRows + (y - minY) / latticeStep];
int stampMaxY = Math.min(maxY, y + 1);
for (int yy = y; yy <= stampMaxY; yy++) {
MatterCavern verticalMatter = matterByY[yy - minY];
@@ -1018,11 +1018,17 @@ public class IrisDimension extends IrisRegistrant {
}
private static String sanitizeRegistryNamespace(String value) {
if (isCanonicalRegistryValue(value, false)) {
return value;
}
String sanitized = value.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9_.-]", "_");
return sanitized.isBlank() ? "dimension" : sanitized;
}
private static String sanitizeRegistryPath(String value, String fallback) {
if (isCanonicalRegistryValue(value, true)) {
return value;
}
if (value == null || value.isBlank()) {
return fallback;
}
@@ -1037,6 +1043,29 @@ public class IrisDimension extends IrisRegistrant {
return sanitized.isBlank() ? fallback : sanitized;
}
private static boolean isCanonicalRegistryValue(String value, boolean path) {
if (value == null || value.isEmpty()) {
return false;
}
char previous = 0;
for (int i = 0; i < value.length(); i++) {
char current = value.charAt(i);
if (current == '/') {
if (!path || i == 0 || i == value.length() - 1 || previous == '/') {
return false;
}
} else if (!((current >= 'a' && current <= 'z') || (current >= '0' && current <= '9')
|| current == '_' || current == '-' || current == '.')) {
return false;
}
if (path && current == '.' && previous == '.') {
return false;
}
previous = current;
}
return true;
}
public IrisDimensionType getDimensionType() {
IrisDimensionTypeOptions options = getDimensionOptions();
if (fullbright) {
@@ -32,6 +32,7 @@ import art.arcane.iris.core.IrisWorlds;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.runtime.ObjectStudioActivation;
import art.arcane.iris.core.runtime.ObjectStudioLayout;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioActivation;
import art.arcane.iris.core.runtime.jigsaw.JigsawStudioSession;
import art.arcane.iris.core.service.StudioSVC;
@@ -136,6 +137,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
private volatile StudioMode lastMode;
private volatile UUID lastJigsawStudioRequestId;
private volatile boolean jigsawStudioActive;
private volatile boolean objectStudioActive;
private volatile DummyBiomeProvider dummyBiomeProvider;
private volatile Throwable initializationFailure;
private volatile IrisDimension validatedDimension;
@@ -256,6 +258,9 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
/** Starts nearest-first hydrology planning around a new world's initial spawn. */
private void prefetchSpawnHydrology(Engine engine, World world) {
if (usesFlatStudioTerrain()) {
return;
}
if (engine.getComplex() == null || engine.getComplex().getHydrologyRuntime() == null) {
return;
}
@@ -265,6 +270,11 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
if (world.isChunkGenerated(spawnX >> 4, spawnZ >> 4)) {
return;
}
if (studio && engine.getDimension().getStudioMode() == StudioMode.NORMAL
&& engine instanceof IrisEngine irisEngine) {
irisEngine.startStudioEntryHydrology(spawnX, spawnZ);
return;
}
int reach = Math.max(16, hydrologyTileSize() / 2);
engine.getComplex().getHydrologyRuntime().prefetchArea(
spawnX - reach,
@@ -407,6 +417,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
jigsawStudioActive = request != null
&& session != null
&& session.sessionId().equals(request.requestId());
objectStudioActive = studio && ObjectStudioActivation.isActive(packKey);
IrisEngine createdEngine = createEngine(engineTarget);
if (generationHistory != null) {
long initialActivationId = generationHistory.activeActivation().activationId();
@@ -439,7 +450,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
}
}
createdEngine.setNativeStructureVolumeQueriesEnabled(shouldGenerateNativeStructures(
jigsawStudioActive,
isAuthoringStudio(),
studioEntryBootstrapActive.get(),
initializationFailure != null));
engine = createdEngine;
@@ -477,7 +488,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
}
private IrisEngine createEngine(EngineTarget engineTarget) {
IrisEngine.InitializationMode mode = selectInitializationMode(studio, jigsawStudioActive);
IrisEngine.InitializationMode mode = selectInitializationMode(studio, jigsawStudioActive, objectStudioActive);
if (generationHistory == null) {
return new IrisEngine(engineTarget, mode);
}
@@ -511,12 +522,14 @@ 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) {
static IrisEngine.InitializationMode selectInitializationMode(boolean studio, boolean jigsawStudioActive, boolean objectStudioActive) {
if (!studio) {
return IrisEngine.InitializationMode.RUNTIME;
}
return jigsawStudioActive
? IrisEngine.InitializationMode.JIGSAW_STUDIO
: objectStudioActive
? IrisEngine.InitializationMode.OBJECT_STUDIO
: IrisEngine.InitializationMode.STUDIO;
}
@@ -738,7 +751,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
Engine activeEngine = engine;
if (activeEngine instanceof IrisEngine irisEngine) {
irisEngine.setNativeStructureVolumeQueriesEnabled(shouldGenerateNativeStructures(
jigsawStudioActive,
isAuthoringStudio(),
false,
initializationFailure != null));
}
@@ -753,6 +766,33 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
return jigsawStudioActive;
}
public boolean isAuthoringStudio() {
return jigsawStudioActive || objectStudioActive;
}
public boolean usesFlatStudioTerrain() {
Engine activeEngine = engine;
return usesFlatStudioTerrain(studio, isAuthoringStudio(),
activeEngine == null ? null : activeEngine.getDimension().getStudioMode());
}
static boolean usesFlatStudioTerrain(boolean studio, boolean authoringStudio, StudioMode mode) {
return studio && (authoringStudio || mode == StudioMode.OBJECT_BUFFET);
}
public int getAuthoringFloorY() {
return Math.max(engine.getMinHeight(), ObjectStudioLayout.FLOOR_Y);
}
public int getAuthoringBaseHeight() {
return authoringBaseHeight(engine.getMinHeight(), engine.getMaxHeight());
}
static int authoringBaseHeight(int minimumY, int maximumY) {
int floorY = Math.max(minimumY, ObjectStudioLayout.FLOOR_Y);
return floorY < maximumY ? floorY + 1 : minimumY;
}
@Override
public void hotload() {
if (!shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)) {
@@ -1089,6 +1129,9 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
@Override
public int getBaseHeight(@NotNull WorldInfo worldInfo, @NotNull Random random, int x, int z, @NotNull HeightMap heightMap) {
Engine currentEngine = getEngine(worldInfo);
if (usesFlatStudioTerrain()) {
return getAuthoringBaseHeight();
}
boolean ignoreFluid = switch (heightMap) {
case OCEAN_FLOOR, OCEAN_FLOOR_WG -> true;
@@ -1165,17 +1208,17 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
@Override
public boolean shouldGenerateStructures() {
return shouldGenerateNativeStructures(
jigsawStudioActive,
isAuthoringStudio(),
studioEntryBootstrapActive.get(),
initializationFailure != null);
}
static boolean shouldGenerateNativeStructures(
boolean jigsawStudioActive,
boolean authoringStudio,
boolean studioEntryBootstrapActive,
boolean initializationFailed
) {
return !jigsawStudioActive && !studioEntryBootstrapActive && !initializationFailed;
return !authoringStudio && !studioEntryBootstrapActive && !initializationFailed;
}
@Override
@@ -62,6 +62,12 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
private final ConcurrentMap<RenderKey, RenderedBay> renderedBays = new ConcurrentHashMap<>();
private final ConcurrentMap<String, PlatformBlockState> jigsawStates = new ConcurrentHashMap<>();
public static boolean isLightFloor(int blockX, int blockZ) {
int tileX = Math.floorDiv(blockX, CHECKER_SIZE);
int tileZ = Math.floorDiv(blockZ, CHECKER_SIZE);
return ((tileX + tileZ) & 1) == 0;
}
public JigsawStudioGenerator(
Engine engine,
JigsawStudioActivation.Request request,
@@ -262,9 +268,7 @@ public final class JigsawStudioGenerator extends EnginedStudioGenerator {
for (int localZ = 0; localZ < 16; localZ++) {
int worldX = chunkWorldX + localX;
int worldZ = chunkWorldZ + localZ;
int tileX = Math.floorDiv(worldX, CHECKER_SIZE);
int tileZ = Math.floorDiv(worldZ, CHECKER_SIZE);
PlatformBlockState block = ((tileX + tileZ) & 1) == 0 ? lightFloor : darkFloor;
PlatformBlockState block = isLightFloor(worldX, worldZ) ? lightFloor : darkFloor;
terrainChunk.setBlock(localX, floorY, localZ, block);
}
}
@@ -93,9 +93,11 @@ public class ObjectStudioGenerator extends EnginedStudioGenerator {
}
private void generateChunkWithinSession(Engine engine, TerrainChunk tc, int x, int z) {
ensureLayout(engine);
int floorY = Math.max(engine.getMinHeight(), ObjectStudioLayout.FLOOR_Y);
if (floorY >= tc.getMaxHeight()) {
return;
}
ensureLayout(engine);
for (int bx = 0; bx < 16; bx++) {
for (int bz = 0; bz < 16; bz++) {
tc.setBiome(bx, floorY, bz, DEFAULT_BIOME);
@@ -667,13 +667,21 @@ public class IrisInterpolation {
// outer pass's table — its entries belong to a different bound provider.
cache = new NoiseBoundsSampleCache2D(64);
}
NoiseSampleCache2D scalarCache = usesSampleCache(method) ? NOISE_SAMPLE_CACHE_2D.get() : null;
boolean ownsScalarCache = scalarCache != null && !scalarCache.isInUse();
cache.beginUse();
NoiseBoundsProvider previous = cache.bindProvider(noise);
if (ownsScalarCache) {
scalarCache.beginUse();
}
try {
double min = getNoise(method, x, z, h, cache.minView());
double max = getNoise(method, x, z, h, cache.maxView());
double min = dispatch(method, x, z, h, cache.minView());
double max = dispatch(method, x, z, h, cache.replayMaxView());
return new NoiseBounds(min, max);
} finally {
if (ownsScalarCache) {
scalarCache.endUse();
}
cache.bindProvider(previous);
cache.endUse();
}
@@ -24,15 +24,15 @@ import java.util.Arrays;
/**
* Open-addressed memo table that samples a {@link NoiseBoundsProvider} once per column and serves
* both the min and max side from the same entry. Single threaded by contract; instances are held in
* the minimum while recording maxima for an identical second interpolation pass. Single threaded; held in
* thread locals.
* <p>
* The min/max {@link NoiseProvider} views are allocated once per cache instance and read the
* currently bound provider, so a bounds interpolation pass allocates no lambdas per column.
* The minimum view samples the bound provider; the maximum view replays the same sampling sequence.
* Both views are allocated once per cache instance.
*/
final class NoiseBoundsSampleCache2D {
private final NoiseProvider minView = this::sampleMin;
private final NoiseProvider maxView = this::sampleMax;
private final NoiseProvider replayMaxView = this::replayMax;
private NoiseBoundsProvider boundProvider;
private long[] xBits;
private long[] zBits;
@@ -43,6 +43,9 @@ final class NoiseBoundsSampleCache2D {
private int resizeThreshold;
private int size;
private boolean inUse;
private double[] sampledMaximums = new double[64];
private int sampledMaximumCount;
private int maximumReplayIndex;
public NoiseBoundsSampleCache2D(int initialCapacity) {
int minimumCapacity = Math.max(8, initialCapacity);
@@ -75,6 +78,8 @@ final class NoiseBoundsSampleCache2D {
public void beginUse() {
inUse = true;
sampledMaximumCount = 0;
maximumReplayIndex = 0;
clear();
}
@@ -83,7 +88,7 @@ final class NoiseBoundsSampleCache2D {
}
/**
* Binds the provider that {@link #minView()} and {@link #maxView()} delegate to, returning the
* Binds the provider that {@link #minView()} samples, returning the
* previously bound provider so callers can restore it.
*/
public NoiseBoundsProvider bindProvider(NoiseBoundsProvider provider) {
@@ -96,44 +101,34 @@ final class NoiseBoundsSampleCache2D {
return minView;
}
public NoiseProvider maxView() {
return maxView;
public NoiseProvider replayMaxView() {
return replayMaxView;
}
private double sampleMin(double sampleX, double sampleZ) {
return getOrSampleMin(sampleX, sampleZ, boundProvider);
}
private double sampleMax(double sampleX, double sampleZ) {
return getOrSampleMax(sampleX, sampleZ, boundProvider);
}
public double getOrSampleMin(double sampleX, double sampleZ, NoiseBoundsProvider provider) {
long xBitsValue = Double.doubleToLongBits(sampleX);
long zBitsValue = Double.doubleToLongBits(sampleZ);
int slot = findSlot(xBitsValue, zBitsValue);
long sampleXBits = Double.doubleToLongBits(sampleX);
long sampleZBits = Double.doubleToLongBits(sampleZ);
int slot = findSlot(sampleXBits, sampleZBits);
double minimum;
double maximum;
if (states[slot] != 0) {
return minValues[slot];
minimum = minValues[slot];
maximum = maxValues[slot];
} else {
NoiseBounds bounds = boundProvider.noise(sampleX, sampleZ);
minimum = bounds.min();
maximum = bounds.max();
insert(findSlot(sampleXBits, sampleZBits), sampleXBits, sampleZBits, minimum, maximum);
}
NoiseBounds bounds = provider.noise(sampleX, sampleZ);
// Recompute the slot: the provider call can mutate the table (clear/grow) via nesting.
insert(findSlot(xBitsValue, zBitsValue), xBitsValue, zBitsValue, bounds.min(), bounds.max());
return bounds.min();
if (sampledMaximumCount == sampledMaximums.length) {
sampledMaximums = Arrays.copyOf(sampledMaximums, sampledMaximums.length << 1);
}
sampledMaximums[sampledMaximumCount++] = maximum;
return minimum;
}
public double getOrSampleMax(double sampleX, double sampleZ, NoiseBoundsProvider provider) {
long xBitsValue = Double.doubleToLongBits(sampleX);
long zBitsValue = Double.doubleToLongBits(sampleZ);
int slot = findSlot(xBitsValue, zBitsValue);
if (states[slot] != 0) {
return maxValues[slot];
}
NoiseBounds bounds = provider.noise(sampleX, sampleZ);
// Recompute the slot: the provider call can mutate the table (clear/grow) via nesting.
insert(findSlot(xBitsValue, zBitsValue), xBitsValue, zBitsValue, bounds.min(), bounds.max());
return bounds.max();
private double replayMax(double sampleX, double sampleZ) {
return sampledMaximums[maximumReplayIndex++];
}
private int findSlot(long xb, long zb) {