mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 06:20:02 +00:00
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:
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,
|
||||
|
||||
+348
-18
@@ -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
|
||||
|
||||
+7
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -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);
|
||||
|
||||
+10
-2
@@ -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();
|
||||
}
|
||||
|
||||
+31
-36
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user