mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-18 22:44:09 +00:00
Merged terrain sampling
- Concurrent fallback queries share pending terrain samples - Pure height generators reuse noise contexts across batches - Storage allocation tests account for the configured heap budget
This commit is contained in:
@@ -1,8 +1,15 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||
import art.arcane.volmlib.util.cache.CacheKey;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
|
||||
final class HydrologyBasisCache {
|
||||
private static final int MAXIMUM_ENTRIES = 32_768;
|
||||
@@ -39,23 +46,74 @@ final class HydrologyBasisCache {
|
||||
seed(samples, full);
|
||||
}
|
||||
|
||||
boolean findWithoutSlope(long packed, HydrologyTerrainSample[] samples, int index) {
|
||||
Stripe stripe = stripe(withoutSlope, packed);
|
||||
synchronized (stripe) {
|
||||
samples[index] = stripe.samples.getAndMoveToFirst(packed);
|
||||
return samples[index] != null || stripe.samples.containsKey(packed);
|
||||
HydrologyTerrainSample[] sampleWithoutSlopeBatch(long[] coordinates, int count,
|
||||
HydrologyNaturalTerrainSampler sampler) {
|
||||
Objects.checkFromIndexSize(0, count, coordinates.length);
|
||||
if (count > HydrologyTerrainSampler.MAXIMUM_BATCH_SIZE) {
|
||||
throw new IllegalArgumentException("Terrain basis batch exceeds its sample limit");
|
||||
}
|
||||
}
|
||||
|
||||
HydrologyTerrainSample rememberWithoutSlope(long packed, HydrologyTerrainSample sample) {
|
||||
Stripe stripe = stripe(withoutSlope, packed);
|
||||
synchronized (stripe) {
|
||||
if (stripe.samples.containsKey(packed)) {
|
||||
return stripe.samples.getAndMoveToFirst(packed);
|
||||
HydrologyTerrainSample[] samples = new HydrologyTerrainSample[count];
|
||||
Pending[] pending = new Pending[count];
|
||||
long[] ownedCoordinates = new long[count];
|
||||
Pending[] owned = new Pending[count];
|
||||
int ownedCount = count;
|
||||
Completion completion = new Completion();
|
||||
int[] next = new int[count];
|
||||
int[] heads = groupByStripe(coordinates, count, next);
|
||||
try {
|
||||
for (int stripeIndex = 0; stripeIndex < withoutSlope.length; stripeIndex++) {
|
||||
if (heads[stripeIndex] == 0) {
|
||||
continue;
|
||||
}
|
||||
Stripe stripe = withoutSlope[stripeIndex];
|
||||
synchronized (stripe) {
|
||||
for (int position = heads[stripeIndex]; position != 0; position = next[position - 1]) {
|
||||
int index = position - 1;
|
||||
long packed = coordinates[index];
|
||||
HydrologyTerrainSample cached = stripe.samples.getAndMoveToFirst(packed);
|
||||
if (cached != null || stripe.samples.containsKey(packed)) {
|
||||
samples[index] = cached;
|
||||
continue;
|
||||
}
|
||||
Pending loading = stripe.pending.get(packed);
|
||||
if (loading == null) {
|
||||
loading = new Pending(completion);
|
||||
stripe.pending.put(packed, loading);
|
||||
ownedCoordinates[index] = packed;
|
||||
owned[index] = loading;
|
||||
}
|
||||
pending[index] = loading;
|
||||
}
|
||||
}
|
||||
}
|
||||
stripe.put(packed, sample);
|
||||
return sample;
|
||||
int claimed = 0;
|
||||
for (int index = 0; index < count; index++) {
|
||||
if (owned[index] != null) {
|
||||
ownedCoordinates[claimed] = ownedCoordinates[index];
|
||||
owned[claimed++] = owned[index];
|
||||
}
|
||||
}
|
||||
ownedCount = claimed;
|
||||
if (ownedCount > 0) {
|
||||
loadBatch(ownedCoordinates, owned, ownedCount, sampler);
|
||||
}
|
||||
} catch (RuntimeException | Error failure) {
|
||||
for (int index = 0; index < ownedCount; index++) {
|
||||
if (owned[index] != null) {
|
||||
fail(withoutSlope, ownedCoordinates[index], owned[index], failure);
|
||||
}
|
||||
}
|
||||
completion.failure = failure;
|
||||
throw failure;
|
||||
} finally {
|
||||
completion.complete.countDown();
|
||||
}
|
||||
for (int index = 0; index < count; index++) {
|
||||
if (pending[index] != null) {
|
||||
samples[index] = pending[index].await();
|
||||
}
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
void seedWithoutSlope(Long2ObjectMap<HydrologyTerrainSample> samples) {
|
||||
@@ -64,15 +122,104 @@ final class HydrologyBasisCache {
|
||||
|
||||
private HydrologyTerrainSample sample(int x, int z, HydrologyNaturalTerrainSampler sampler, boolean omitSlope) {
|
||||
long packed = RiverFootprint.pack(x, z);
|
||||
Stripe stripe = stripe(omitSlope ? withoutSlope : full, packed);
|
||||
Stripe[] stripes = omitSlope ? withoutSlope : full;
|
||||
Stripe stripe = stripe(stripes, packed);
|
||||
Pending loading;
|
||||
boolean owner = false;
|
||||
synchronized (stripe) {
|
||||
HydrologyTerrainSample cached = stripe.samples.getAndMoveToFirst(packed);
|
||||
if (cached != null || stripe.samples.containsKey(packed)) {
|
||||
return cached;
|
||||
}
|
||||
HydrologyTerrainSample sampled = omitSlope ? sampler.sampleBasisWithoutSlope(x, z) : sampler.sampleBasis(x, z);
|
||||
stripe.put(packed, sampled);
|
||||
return sampled;
|
||||
loading = stripe.pending.get(packed);
|
||||
if (loading == null) {
|
||||
loading = new Pending(new Completion());
|
||||
stripe.pending.put(packed, loading);
|
||||
owner = true;
|
||||
}
|
||||
}
|
||||
if (owner) {
|
||||
try {
|
||||
HydrologyTerrainSample sampled = ProvisionalSampling.memoizable(() -> omitSlope
|
||||
? sampler.sampleBasisWithoutSlope(x, z) : sampler.sampleBasis(x, z));
|
||||
complete(stripes, packed, loading, sampled, false);
|
||||
} catch (ProvisionalSampling.Unmemoizable provisional) {
|
||||
complete(stripes, packed, loading, provisional.value(), true);
|
||||
} catch (RuntimeException | Error failure) {
|
||||
fail(stripes, packed, loading, failure);
|
||||
throw failure;
|
||||
} finally {
|
||||
loading.completion.complete.countDown();
|
||||
}
|
||||
}
|
||||
return loading.await();
|
||||
}
|
||||
|
||||
private void loadBatch(long[] coordinates, Pending[] pending, int count, HydrologyNaturalTerrainSampler sampler) {
|
||||
HydrologyTerrainSample[] samples;
|
||||
boolean provisional = false;
|
||||
try {
|
||||
samples = ProvisionalSampling.memoizable(() -> sampler.sampleBasisWithoutSlopeBatch(coordinates, count));
|
||||
} catch (ProvisionalSampling.Unmemoizable value) {
|
||||
samples = value.value();
|
||||
provisional = true;
|
||||
}
|
||||
if (samples.length < count) {
|
||||
throw new IllegalStateException("Terrain basis batch returned too few samples");
|
||||
}
|
||||
int[] next = new int[count];
|
||||
int[] heads = groupByStripe(coordinates, count, next);
|
||||
for (int stripeIndex = 0; stripeIndex < withoutSlope.length; stripeIndex++) {
|
||||
if (heads[stripeIndex] == 0) {
|
||||
continue;
|
||||
}
|
||||
Stripe stripe = withoutSlope[stripeIndex];
|
||||
synchronized (stripe) {
|
||||
for (int position = heads[stripeIndex]; position != 0; position = next[position - 1]) {
|
||||
int index = position - 1;
|
||||
complete(stripe, coordinates[index], pending[index], samples[index], provisional);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int[] groupByStripe(long[] coordinates, int count, int[] next) {
|
||||
int[] heads = new int[withoutSlope.length];
|
||||
for (int index = count - 1; index >= 0; index--) {
|
||||
int stripeIndex = (int) CacheKey.mix(coordinates[index]) & (withoutSlope.length - 1);
|
||||
next[index] = heads[stripeIndex];
|
||||
heads[stripeIndex] = index + 1;
|
||||
}
|
||||
return heads;
|
||||
}
|
||||
|
||||
private void complete(Stripe[] stripes, long packed, Pending pending, HydrologyTerrainSample sample,
|
||||
boolean provisional) {
|
||||
Stripe stripe = stripe(stripes, packed);
|
||||
synchronized (stripe) {
|
||||
complete(stripe, packed, pending, sample, provisional);
|
||||
}
|
||||
}
|
||||
|
||||
private void complete(Stripe stripe, long packed, Pending pending, HydrologyTerrainSample sample,
|
||||
boolean provisional) {
|
||||
if (stripe.samples.containsKey(packed)) {
|
||||
sample = stripe.samples.getAndMoveToFirst(packed);
|
||||
provisional = false;
|
||||
} else if (!provisional) {
|
||||
stripe.put(packed, sample);
|
||||
}
|
||||
stripe.pending.remove(packed);
|
||||
pending.sample = sample;
|
||||
pending.provisional = provisional;
|
||||
}
|
||||
|
||||
private void fail(Stripe[] stripes, long packed, Pending pending, Throwable failure) {
|
||||
Stripe stripe = stripe(stripes, packed);
|
||||
synchronized (stripe) {
|
||||
if (stripe.pending.remove(packed, pending)) {
|
||||
pending.completion.failure = failure;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +239,7 @@ final class HydrologyBasisCache {
|
||||
private static final class Stripe {
|
||||
private final int maximumEntries;
|
||||
private final Long2ObjectLinkedOpenHashMap<HydrologyTerrainSample> samples;
|
||||
private final Long2ObjectOpenHashMap<Pending> pending = new Long2ObjectOpenHashMap<>();
|
||||
|
||||
private Stripe(int maximumEntries) {
|
||||
this.maximumEntries = maximumEntries;
|
||||
@@ -105,4 +253,73 @@ final class HydrologyBasisCache {
|
||||
samples.putAndMoveToFirst(packed, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Pending {
|
||||
private final Completion completion;
|
||||
private HydrologyTerrainSample sample;
|
||||
private boolean provisional;
|
||||
|
||||
private Pending(Completion completion) {
|
||||
this.completion = completion;
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample await() {
|
||||
completion.await();
|
||||
if (provisional) {
|
||||
ProvisionalSampling.mark();
|
||||
}
|
||||
return sample;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Completion implements ForkJoinPool.ManagedBlocker {
|
||||
private final Thread owner = Thread.currentThread();
|
||||
private final CountDownLatch complete = new CountDownLatch(1);
|
||||
private Throwable failure;
|
||||
|
||||
private void await() {
|
||||
if (!isReleasable() && owner == Thread.currentThread()) {
|
||||
throw new IllegalStateException("Recursive terrain basis sampling");
|
||||
}
|
||||
boolean interrupted = false;
|
||||
boolean managed = true;
|
||||
try {
|
||||
while (!isReleasable()) {
|
||||
try {
|
||||
if (managed) {
|
||||
ForkJoinPool.managedBlock(this);
|
||||
} else {
|
||||
complete.await();
|
||||
}
|
||||
} catch (InterruptedException interruption) {
|
||||
interrupted = true;
|
||||
managed = false;
|
||||
} catch (RejectedExecutionException unavailable) {
|
||||
managed = false;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
if (failure instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
if (failure instanceof RuntimeException runtime) {
|
||||
throw runtime;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean block() throws InterruptedException {
|
||||
complete.await();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReleasable() {
|
||||
return complete.getCount() == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-5
@@ -946,8 +946,6 @@ final class HydrologyFootprintCompiler {
|
||||
HydrologyTerrainSample cached = terrainBases.get(key);
|
||||
if (cached != null) {
|
||||
samples[index] = cached;
|
||||
} else if (sharedBasis != null && sharedBasis.findWithoutSlope(key, samples, index)) {
|
||||
continue;
|
||||
} else if (naturalSampler == null) {
|
||||
samples[index] = sampleTerrainBasis(RiverFootprint.unpackX(key), RiverFootprint.unpackZ(key));
|
||||
} else {
|
||||
@@ -956,10 +954,11 @@ final class HydrologyFootprintCompiler {
|
||||
}
|
||||
}
|
||||
if (missingCount > 0) {
|
||||
HydrologyTerrainSample[] loaded = naturalSampler.sampleBasisWithoutSlopeBatch(missing, missingCount);
|
||||
HydrologyTerrainSample[] loaded = sharedBasis == null
|
||||
? naturalSampler.sampleBasisWithoutSlopeBatch(missing, missingCount)
|
||||
: sharedBasis.sampleWithoutSlopeBatch(missing, missingCount, naturalSampler);
|
||||
for (int index = 0; index < missingCount; index++) {
|
||||
samples[positions[index]] = sharedBasis == null ? loaded[index]
|
||||
: sharedBasis.rememberWithoutSlope(missing[index], loaded[index]);
|
||||
samples[positions[index]] = loaded[index];
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < count; index++) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
@@ -91,6 +93,7 @@ public final class HydrologyForkJoin {
|
||||
private final CountDownLatch complete = new CountDownLatch(1);
|
||||
private T result;
|
||||
private Throwable failure;
|
||||
private boolean provisional;
|
||||
|
||||
Task(Callable<T> work) {
|
||||
this.work = Objects.requireNonNull(work);
|
||||
@@ -136,6 +139,9 @@ public final class HydrologyForkJoin {
|
||||
if (failure != null) {
|
||||
throw unwrap(failure);
|
||||
}
|
||||
if (provisional) {
|
||||
ProvisionalSampling.mark();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -156,7 +162,10 @@ public final class HydrologyForkJoin {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
result = call(work);
|
||||
result = ProvisionalSampling.memoizable(() -> call(work));
|
||||
} catch (ProvisionalSampling.Unmemoizable value) {
|
||||
result = value.value();
|
||||
provisional = true;
|
||||
} catch (RuntimeException | Error failed) {
|
||||
failure = failed;
|
||||
} finally {
|
||||
|
||||
+12
@@ -1,6 +1,18 @@
|
||||
package art.arcane.iris.generation.hydrology.runtime;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.RiverFootprint;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface IrisHydrologyNaturalHeightProvider {
|
||||
double sample(int blockX, int blockZ);
|
||||
|
||||
default boolean supportsBatch() {
|
||||
return false;
|
||||
}
|
||||
|
||||
default void sampleBatch(long[] coordinates, int count, double[] heights) {
|
||||
for (int index = 0; index < count; index++) {
|
||||
heights[index] = sample(RiverFootprint.unpackX(coordinates[index]), RiverFootprint.unpackZ(coordinates[index]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+147
-23
@@ -10,6 +10,8 @@ import art.arcane.iris.generation.hydrology.RiverFootprint;
|
||||
import art.arcane.volmlib.nativelib.terrain.NativeGenerationScope;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyForkJoin;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2DoubleLinkedOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
||||
|
||||
@@ -98,15 +100,10 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
NaturalClassification sampled = NaturalClassification.LAND;
|
||||
if (oceanClassifier.isOcean(blockX, blockZ)) {
|
||||
double height = naturalHeight(stripe, packed, blockX, blockZ);
|
||||
if (!Double.isFinite(height)) {
|
||||
return NaturalClassification.UNAVAILABLE;
|
||||
}
|
||||
if (physicalOcean(true, height, seaLevel)) {
|
||||
sampled = NaturalClassification.OCEAN;
|
||||
}
|
||||
long revision = ProvisionalSampling.revision();
|
||||
NaturalClassification sampled = classifyNatural(stripe, packed, blockX, blockZ);
|
||||
if (sampled == NaturalClassification.UNAVAILABLE || revision != ProvisionalSampling.revision()) {
|
||||
return sampled;
|
||||
}
|
||||
synchronized (stripe) {
|
||||
TerrainBasis basis = stripe.bases.getAndMoveToLast(packed);
|
||||
@@ -123,6 +120,17 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
return sampled;
|
||||
}
|
||||
|
||||
private NaturalClassification classifyNatural(CacheStripe stripe, long packed, int blockX, int blockZ) {
|
||||
if (!oceanClassifier.isOcean(blockX, blockZ)) {
|
||||
return NaturalClassification.LAND;
|
||||
}
|
||||
double height = naturalHeight(stripe, packed, blockX, blockZ);
|
||||
if (!Double.isFinite(height)) {
|
||||
return NaturalClassification.UNAVAILABLE;
|
||||
}
|
||||
return physicalOcean(true, height, seaLevel) ? NaturalClassification.OCEAN : NaturalClassification.LAND;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HydrologyTerrainSample sampleBasis(int blockX, int blockZ) {
|
||||
// Point samples carry the forward slope; the aligned grid derives its own from neighbouring bases.
|
||||
@@ -144,10 +152,15 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
int workers = Math.min(Math.min(samplingOptions.maximumWorkers(), Runtime.getRuntime().availableProcessors()),
|
||||
Math.ceilDiv(count, MINIMUM_PARALLEL_BASIS_SAMPLES));
|
||||
Supplier<NativeGenerationScope> scope = workers > 1 ? samplingOptions.batchScopes().capture() : null;
|
||||
if (scope == null) {
|
||||
return HydrologyNaturalTerrainSampler.super.sampleBasisWithoutSlopeBatch(coordinates, count);
|
||||
}
|
||||
HydrologyTerrainSample[] samples = new HydrologyTerrainSample[count];
|
||||
if (scope == null) {
|
||||
TerrainBasis[] bases = new TerrainBasis[count];
|
||||
fillBasisBatch(coordinates, count, bases, 0);
|
||||
for (int index = 0; index < count; index++) {
|
||||
samples[index] = bases[index].terrain();
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
int perWorker = Math.ceilDiv(count, workers);
|
||||
ArrayList<Callable<Void>> tasks = new ArrayList<>(workers);
|
||||
for (int worker = 0; worker < workers; worker++) {
|
||||
@@ -158,12 +171,12 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
}
|
||||
tasks.add(() -> {
|
||||
try (NativeGenerationScope ignored = scope.get()) {
|
||||
long[] slice = new long[end - start];
|
||||
System.arraycopy(coordinates, start, slice, 0, slice.length);
|
||||
TerrainBasis[] bases = new TerrainBasis[slice.length];
|
||||
fillBasisBatch(slice, slice.length, bases, 0);
|
||||
for (int index = start; index < end; index++) {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
throw new CancellationException("Terrain basis sampling interrupted");
|
||||
}
|
||||
samples[index] = sampleBasisWithoutSlope(RiverFootprint.unpackX(coordinates[index]),
|
||||
RiverFootprint.unpackZ(coordinates[index]));
|
||||
samples[index] = bases[index - start].terrain();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -183,9 +196,12 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
return cached.terrain().ocean() ? Double.NaN : cached.terrain().naturalHeight();
|
||||
}
|
||||
}
|
||||
long revision = ProvisionalSampling.revision();
|
||||
double naturalHeight = naturalHeight(stripe, packed, blockX, blockZ);
|
||||
boolean memoizable = revision == ProvisionalSampling.revision();
|
||||
if (!Double.isFinite(naturalHeight)) {
|
||||
TerrainBasis basis = loadBasis(blockX, blockZ, stripe, packed, naturalHeight);
|
||||
TerrainBasis basis = memoizable ? loadBasis(blockX, blockZ, stripe, packed, naturalHeight)
|
||||
: sampleBasis(blockX, blockZ, naturalHeight);
|
||||
return basis.terrain().ocean() ? Double.NaN : basis.terrain().naturalHeight();
|
||||
}
|
||||
long roundedHeight = StrictMath.round(naturalHeight);
|
||||
@@ -194,7 +210,8 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
}
|
||||
NaturalClassification classification = classifyNatural(blockX, blockZ);
|
||||
if (classification == NaturalClassification.UNAVAILABLE) {
|
||||
TerrainBasis basis = loadBasis(blockX, blockZ, stripe, packed, naturalHeight);
|
||||
TerrainBasis basis = memoizable ? loadBasis(blockX, blockZ, stripe, packed, naturalHeight)
|
||||
: sampleBasis(blockX, blockZ, naturalHeight);
|
||||
return basis.terrain().ocean() ? Double.NaN : basis.terrain().naturalHeight();
|
||||
}
|
||||
return classification == NaturalClassification.OCEAN ? Double.NaN : (int) roundedHeight;
|
||||
@@ -209,16 +226,28 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
return loadBasis(blockX, blockZ, stripe, packed, naturalHeight(stripe, packed, blockX, blockZ));
|
||||
long revision = ProvisionalSampling.revision();
|
||||
TerrainBasis sampled = sampleBasis(blockX, blockZ, naturalHeight(stripe, packed, blockX, blockZ));
|
||||
return revision == ProvisionalSampling.revision() ? publishBasis(stripe, packed, sampled) : sampled;
|
||||
}
|
||||
|
||||
private TerrainBasis loadBasis(int blockX, int blockZ, CacheStripe stripe, long packed, double naturalHeight) {
|
||||
long revision = ProvisionalSampling.revision();
|
||||
TerrainBasis sampled = sampleBasis(blockX, blockZ, naturalHeight);
|
||||
return revision == ProvisionalSampling.revision() ? publishBasis(stripe, packed, sampled) : sampled;
|
||||
}
|
||||
|
||||
private TerrainBasis sampleBasis(int blockX, int blockZ, double naturalHeight) {
|
||||
TerrainBasis sampled = basisProvider.sample(blockX, blockZ, naturalHeight);
|
||||
if (sampled == null) {
|
||||
throw new NullPointerException(
|
||||
"Hydrology terrain basis provider returned null at " + blockX + "," + blockZ
|
||||
);
|
||||
}
|
||||
return sampled;
|
||||
}
|
||||
|
||||
private TerrainBasis publishBasis(CacheStripe stripe, long packed, TerrainBasis sampled) {
|
||||
synchronized (stripe) {
|
||||
TerrainBasis existing = stripe.bases.getAndMoveToLast(packed);
|
||||
if (existing != null) {
|
||||
@@ -349,14 +378,108 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
int minimumRow,
|
||||
int maximumRow
|
||||
) {
|
||||
int capacity = Math.min(HydrologyTerrainSampler.MAXIMUM_BATCH_SIZE,
|
||||
Math.multiplyExact(maximumRow - minimumRow, planeWidth));
|
||||
long[] coordinates = new long[capacity];
|
||||
int count = 0;
|
||||
int destination = minimumRow * planeWidth;
|
||||
for (int gridZ = minimumRow; gridZ < maximumRow; gridZ++) {
|
||||
int z = Math.toIntExact((long) minimumZ + (long) gridZ * spacing);
|
||||
int planeIndex = gridZ * planeWidth;
|
||||
for (int gridX = 0; gridX < planeWidth; gridX++) {
|
||||
int x = Math.toIntExact((long) minimumX + (long) gridX * spacing);
|
||||
plane[planeIndex++] = basis(x, z);
|
||||
coordinates[count++] = RiverFootprint.pack(x, z);
|
||||
if (count == coordinates.length) {
|
||||
fillBasisBatch(coordinates, count, plane, destination);
|
||||
destination += count;
|
||||
count = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (count > 0) {
|
||||
fillBasisBatch(coordinates, count, plane, destination);
|
||||
}
|
||||
}
|
||||
|
||||
private void fillBasisBatch(long[] coordinates, int count, TerrainBasis[] target, int offset) {
|
||||
if (!heightProvider.supportsBatch()) {
|
||||
for (int index = 0; index < count; index++) {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
throw new CancellationException("Terrain basis sampling interrupted");
|
||||
}
|
||||
target[offset + index] = basis(RiverFootprint.unpackX(coordinates[index]),
|
||||
RiverFootprint.unpackZ(coordinates[index]));
|
||||
}
|
||||
return;
|
||||
}
|
||||
long[] missing = new long[count];
|
||||
int[] slots = new int[count];
|
||||
double[] heights = new double[count];
|
||||
Long2IntOpenHashMap indices = new Long2IntOpenHashMap(count);
|
||||
indices.defaultReturnValue(-1);
|
||||
int missingCount = 0;
|
||||
for (int index = 0; index < count; index++) {
|
||||
int x = RiverFootprint.unpackX(coordinates[index]);
|
||||
int z = RiverFootprint.unpackZ(coordinates[index]);
|
||||
long packed = pack(x, z);
|
||||
CacheStripe stripe = stripe(packed);
|
||||
synchronized (stripe) {
|
||||
TerrainBasis basis = stripe.bases.getAndMoveToLast(packed);
|
||||
if (basis != null) {
|
||||
target[offset + index] = basis;
|
||||
slots[index] = -1;
|
||||
continue;
|
||||
}
|
||||
heights[index] = stripe.naturalHeights.getAndMoveToLast(packed);
|
||||
}
|
||||
if (!Double.isNaN(heights[index])) {
|
||||
slots[index] = -1;
|
||||
continue;
|
||||
}
|
||||
int slot = indices.get(coordinates[index]);
|
||||
if (slot < 0) {
|
||||
slot = missingCount++;
|
||||
indices.put(coordinates[index], slot);
|
||||
missing[slot] = coordinates[index];
|
||||
}
|
||||
slots[index] = slot;
|
||||
}
|
||||
double[] sampled = new double[missingCount];
|
||||
boolean memoizable = true;
|
||||
if (missingCount > 0) {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
throw new CancellationException("Terrain basis sampling interrupted");
|
||||
}
|
||||
long revision = ProvisionalSampling.revision();
|
||||
heightProvider.sampleBatch(missing, missingCount, sampled);
|
||||
memoizable = revision == ProvisionalSampling.revision();
|
||||
for (int index = 0; memoizable && index < missingCount; index++) {
|
||||
if (!Double.isFinite(sampled[index])) {
|
||||
continue;
|
||||
}
|
||||
long packed = pack(RiverFootprint.unpackX(missing[index]), RiverFootprint.unpackZ(missing[index]));
|
||||
CacheStripe stripe = stripe(packed);
|
||||
synchronized (stripe) {
|
||||
double existing = stripe.naturalHeights.getAndMoveToLast(packed);
|
||||
if (Double.isNaN(existing)) {
|
||||
stripe.naturalHeights.putAndMoveToLast(packed, sampled[index]);
|
||||
stripe.evictOldest(stripe.naturalHeights);
|
||||
} else {
|
||||
sampled[index] = existing;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < count; index++) {
|
||||
if (target[offset + index] != null) {
|
||||
continue;
|
||||
}
|
||||
int x = RiverFootprint.unpackX(coordinates[index]);
|
||||
int z = RiverFootprint.unpackZ(coordinates[index]);
|
||||
long packed = pack(x, z);
|
||||
double height = slots[index] < 0 ? heights[index] : sampled[slots[index]];
|
||||
target[offset + index] = memoizable ? loadBasis(x, z, stripe(packed), packed, height)
|
||||
: sampleBasis(x, z, height);
|
||||
}
|
||||
}
|
||||
|
||||
private int gridWorkerCount(int planeWidth, int planeSize) {
|
||||
@@ -384,8 +507,9 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
||||
return cachedBasis.naturalHeight();
|
||||
}
|
||||
}
|
||||
long revision = ProvisionalSampling.revision();
|
||||
double sampled = heightProvider.sample(blockX, blockZ);
|
||||
if (!Double.isFinite(sampled)) {
|
||||
if (!Double.isFinite(sampled) || revision != ProvisionalSampling.revision()) {
|
||||
// Never memoize a broken sample: the basis provider rejects it with a diagnostic and a
|
||||
// later sample of the same column gets a fresh chance instead of the cached failure.
|
||||
return sampled;
|
||||
|
||||
+1
-1
@@ -100,7 +100,7 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
HydrologyGeometrySampler geometrySampler = geometrySampler(context, hydrology);
|
||||
IrisHydrologyRoutingTerrainSampler.Sources terrainSources = new IrisHydrologyRoutingTerrainSampler.Sources(
|
||||
this::createTerrainBasis,
|
||||
(int x, int z) -> context.naturalHeightProvider().sample(x, z),
|
||||
context.naturalHeightProvider(),
|
||||
(int x, int z) -> context.naturalOceanClassifier().isOcean(x, z),
|
||||
settings.seaLevel()
|
||||
);
|
||||
|
||||
@@ -41,7 +41,10 @@ import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.DoubleBinaryOperator;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@@ -208,26 +211,106 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
if (surfaceDetail == 1D) {
|
||||
return sampleHeight(rx, rz, superSeed, data, engine);
|
||||
}
|
||||
return surfaceHeight(rx, rz, superSeed, data, engine, null);
|
||||
}
|
||||
|
||||
public boolean supportsBatch() {
|
||||
if (getClass() != IrisGenerator.class || !Double.isFinite(surfaceDetail)
|
||||
|| surfaceDetail < 0D || surfaceDetail > 1D) {
|
||||
return false;
|
||||
}
|
||||
for (IrisNoiseGenerator layer : composite) {
|
||||
if (!layer.supportsBatch()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return !hasCliffs() || cliffHeightGenerator.supportsBatch();
|
||||
}
|
||||
|
||||
public void getHeights(double[] xs, double[] zs, long superSeed, double[] target, int count) {
|
||||
Objects.checkFromIndexSize(0, count, xs.length);
|
||||
Objects.checkFromIndexSize(0, count, zs.length);
|
||||
Objects.checkFromIndexSize(0, count, target.length);
|
||||
if (count > 4096) {
|
||||
throw new IllegalArgumentException("Generator batch exceeds 4096 samples");
|
||||
}
|
||||
if (count < 8 || !supportsBatch()) {
|
||||
for (int index = 0; index < count; index++) {
|
||||
target[index] = getHeight(xs[index], zs[index], superSeed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (composite.isEmpty()) {
|
||||
Arrays.fill(target, 0, count, 0D);
|
||||
return;
|
||||
}
|
||||
IrisData data = getLoader();
|
||||
Engine engine = data == null ? null : data.getEngine();
|
||||
DoubleBinaryOperator prepared = prepareHeight(superSeed, data, engine);
|
||||
for (int index = 0; index < count; index++) {
|
||||
target[index] = surfaceDetail == 1D ? prepared.applyAsDouble(xs[index], zs[index])
|
||||
: surfaceHeight(xs[index], zs[index], superSeed, data, engine, prepared);
|
||||
}
|
||||
}
|
||||
|
||||
private DoubleBinaryOperator prepareHeight(long superSeed, IrisData data, Engine engine) {
|
||||
int hc = (int) ((cliffHeightMin * 10) + 10 + cliffHeightMax * getSeed() + offsetX + offsetZ);
|
||||
DoubleBinaryOperator[] layers = new DoubleBinaryOperator[composite.size()];
|
||||
double totalOpacity = 0D;
|
||||
for (int index = 0; index < layers.length; index++) {
|
||||
IrisNoiseGenerator layer = composite.get(index);
|
||||
layers[index] = layer.prepareBatch(getSeed() + superSeed + hc, data, engine);
|
||||
if (!multiplicitive) {
|
||||
totalOpacity += layer.getOpacity();
|
||||
}
|
||||
}
|
||||
double sumOpacity = totalOpacity;
|
||||
double cliffSeed = (double) (superSeed + 294596 + hc) - 34857;
|
||||
DoubleBinaryOperator cliffs = hasCliffs() ? cliffHeightGenerator.prepareBatch(
|
||||
(long) (getSeed() + cliffSeed + hc), data, engine) : null;
|
||||
return (rx, rz) -> {
|
||||
double sampleX = (rx + offsetX) / zoom;
|
||||
double sampleZ = (rz + offsetZ) / zoom;
|
||||
double h = multiplicitive ? 1D : 0D;
|
||||
for (DoubleBinaryOperator layer : layers) {
|
||||
double sampled = layer.applyAsDouble(sampleX, sampleZ);
|
||||
h = multiplicitive ? h * sampled : h + sampled;
|
||||
}
|
||||
double value = multiplicitive ? h * opacity : (h / sumOpacity) * opacity;
|
||||
if (Double.isNaN(value)) {
|
||||
value = 0D;
|
||||
}
|
||||
if (cliffs != null) {
|
||||
double height = IrisInterpolation.lerp(cliffHeightMin, cliffHeightMax,
|
||||
cliffs.applyAsDouble(sampleX, sampleZ));
|
||||
value = (Math.round((value * 255D) / height) * height) / 255D;
|
||||
}
|
||||
return hasCellCracks() ? cell(rx, rz, value, superSeed + 48622 + hc) : value;
|
||||
};
|
||||
}
|
||||
|
||||
private double surfaceHeight(double rx, double rz, long superSeed, IrisData data, Engine engine,
|
||||
DoubleBinaryOperator prepared) {
|
||||
SurfaceCacheStripe[] stripes = surfaceCache(data, engine).stripes();
|
||||
double lowerX = Math.floor(rx / SURFACE_GRID) * SURFACE_GRID;
|
||||
double lowerZ = Math.floor(rz / SURFACE_GRID) * SURFACE_GRID;
|
||||
double dx = (rx - lowerX) / SURFACE_GRID;
|
||||
double dz = (rz - lowerZ) / SURFACE_GRID;
|
||||
double northWest = cachedSurfaceHeight(lowerX, lowerZ, superSeed, stripes, data, engine);
|
||||
double northWest = cachedSurfaceHeight(lowerX, lowerZ, superSeed, stripes, data, engine, prepared);
|
||||
if (dx == 0D && dz == 0D) {
|
||||
return northWest;
|
||||
}
|
||||
double north = dx == 0D ? northWest : IrisInterpolation.lerp(northWest,
|
||||
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ, superSeed, stripes, data, engine), dx);
|
||||
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ, superSeed, stripes, data, engine, prepared), dx);
|
||||
double smooth = north;
|
||||
if (dz != 0D) {
|
||||
double southWest = cachedSurfaceHeight(lowerX, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine);
|
||||
double southWest = cachedSurfaceHeight(lowerX, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine, prepared);
|
||||
double south = dx == 0D ? southWest : IrisInterpolation.lerp(southWest,
|
||||
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine), dx);
|
||||
cachedSurfaceHeight(lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID, superSeed, stripes, data, engine, prepared), dx);
|
||||
smooth = IrisInterpolation.lerp(north, south, dz);
|
||||
}
|
||||
return surfaceDetail == 0D ? smooth
|
||||
: smooth + (sampleHeight(rx, rz, superSeed, data, engine) - smooth) * surfaceDetail;
|
||||
: smooth + ((prepared == null ? sampleHeight(rx, rz, superSeed, data, engine) : prepared.applyAsDouble(rx, rz)) - smooth) * surfaceDetail;
|
||||
}
|
||||
|
||||
private double sampleHeight(double rx, double rz, long superSeed, IrisData data, Engine engine) {
|
||||
@@ -267,9 +350,9 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
return v;
|
||||
}
|
||||
|
||||
private double cachedSurfaceHeight(double x, double z, long superSeed, SurfaceCacheStripe[] stripes, IrisData data, Engine engine) {
|
||||
private double cachedSurfaceHeight(double x, double z, long superSeed, SurfaceCacheStripe[] stripes, IrisData data, Engine engine, DoubleBinaryOperator prepared) {
|
||||
if (x != (int) x || z != (int) z) {
|
||||
return sampleHeight(x, z, superSeed, data, engine);
|
||||
return prepared == null ? sampleHeight(x, z, superSeed, data, engine) : prepared.applyAsDouble(x, z);
|
||||
}
|
||||
long key = ((long) (int) x << 32) | ((int) z & 0xffffffffL);
|
||||
long mixed = key ^ (key >>> 33);
|
||||
@@ -281,7 +364,7 @@ public class IrisGenerator extends IrisRegistrant {
|
||||
if (cached != null) {
|
||||
return cached.height();
|
||||
}
|
||||
double height = sampleHeight(x, z, superSeed, data, engine);
|
||||
double height = prepared == null ? sampleHeight(x, z, superSeed, data, engine) : prepared.applyAsDouble(x, z);
|
||||
stripe.put(slot, new SurfaceSample(key, superSeed, height));
|
||||
return height;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import lombok.Setter;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.DoubleBinaryOperator;
|
||||
|
||||
@Snippet("generator")
|
||||
@Accessors(chain = true)
|
||||
@@ -176,6 +177,67 @@ public class IrisNoiseGenerator {
|
||||
return sample(getGenerator(superSeed, data, engine), x, z);
|
||||
}
|
||||
|
||||
boolean supportsBatch() {
|
||||
if (getClass() != IrisNoiseGenerator.class) {
|
||||
return false;
|
||||
}
|
||||
if (!enabled) {
|
||||
return true;
|
||||
}
|
||||
for (IrisGeneratorStyle current = style; current != null; current = current.getFracture()) {
|
||||
if (current.getClass() != IrisGeneratorStyle.class || current.getExpression() != null
|
||||
|| current.getImageMap() != null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (IrisNoiseGenerator child : fracture) {
|
||||
if (!child.supportsBatch()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
DoubleBinaryOperator prepareBatch(long superSeed, IrisData data, Engine engine) {
|
||||
if (!enabled) {
|
||||
double value = offsetY;
|
||||
return (x, z) -> value;
|
||||
}
|
||||
if (hasDeepFracture()) {
|
||||
FractureSamples samples = new FractureSamples();
|
||||
if (samples.prepare(this, superSeed, data, engine)) {
|
||||
return samples::sample;
|
||||
}
|
||||
}
|
||||
DoubleBinaryOperator[] children = new DoubleBinaryOperator[fracture.size()];
|
||||
double[] offsets = new double[fracture.size()];
|
||||
int salt = 33;
|
||||
for (int index = 0; index < children.length; index++) {
|
||||
IrisNoiseGenerator child = fracture.get(index);
|
||||
if (child.isEnabled()) {
|
||||
children[index] = child.prepareBatch(superSeed + seed + salt, data, engine);
|
||||
offsets[index] = child.getOpacity() / 2D;
|
||||
}
|
||||
salt += 819;
|
||||
}
|
||||
CNG generator = getGenerator(superSeed, data, engine);
|
||||
return (x, z) -> samplePrepared(generator, children, offsets, x, z);
|
||||
}
|
||||
|
||||
private double samplePrepared(CNG generator, DoubleBinaryOperator[] children, double[] offsets,
|
||||
double xv, double zv) {
|
||||
double x = xv;
|
||||
double z = zv;
|
||||
for (int index = 0; index < children.length; index++) {
|
||||
DoubleBinaryOperator child = children[index];
|
||||
if (child != null) {
|
||||
x += child.applyAsDouble(xv, zv) - offsets[index];
|
||||
z += child.applyAsDouble(zv, xv) - offsets[index];
|
||||
}
|
||||
}
|
||||
return sample(generator, x, z);
|
||||
}
|
||||
|
||||
public KList<IrisNoiseGenerator> getAllComposites() {
|
||||
KList<IrisNoiseGenerator> g = new KList<>();
|
||||
|
||||
|
||||
@@ -39,6 +39,8 @@ import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyColumnSnapshot;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyNaturalSample;
|
||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyNaturalHeightProvider;
|
||||
import art.arcane.iris.generation.hydrology.RiverFootprint;
|
||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntimeContext;
|
||||
import art.arcane.iris.generation.image.IrisImageMapApplication;
|
||||
@@ -454,7 +456,7 @@ public class IrisComplex implements DataProvider {
|
||||
engine.getDimension(),
|
||||
data,
|
||||
(x, z, naturalHeight) -> sampleHydrologyNatural(engine, x, z, naturalHeight),
|
||||
(x, z) -> naturalHeightStream.getDouble(x, z),
|
||||
new HydrologyHeightProvider(),
|
||||
(x, z) -> describeNaturalHeight(engine, x, z),
|
||||
this::sampleNaturalOcean,
|
||||
footprint -> new MantleHydrologyCaveVoxelView(engine, this, footprint),
|
||||
@@ -791,6 +793,154 @@ public class IrisComplex implements DataProvider {
|
||||
return column == null ? getHeightStream().getDouble(x, z) : column.nearestSurfaceY(surfaceY);
|
||||
}
|
||||
|
||||
private boolean supportsHeightBatch() {
|
||||
if (terrainEngine.getDimension().getTerrainSamplingStep() != 1 || !biomeBuffetGenerators.isEmpty()
|
||||
|| !reuseNaturalBaseBiome) {
|
||||
return false;
|
||||
}
|
||||
for (IrisGenerator[] group : frozenGenerators) {
|
||||
for (IrisGenerator generator : group) {
|
||||
if (!generator.supportsBatch()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (IrisShapedGeneratorStyle overlay : terrainEngine.getDimension().getOverlayNoise()) {
|
||||
if (overlay.getClass() != IrisShapedGeneratorStyle.class) {
|
||||
return false;
|
||||
}
|
||||
for (IrisGeneratorStyle style = overlay.getGenerator(); style != null; style = style.getFracture()) {
|
||||
if (style.getClass() != IrisGeneratorStyle.class || style.getExpression() != null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void sampleBaseHeightBatch(long[] coordinates, int count, double[] target) {
|
||||
((CachedDoubleStream2D) baseTerrainHeightStream).getBatch(coordinates, count, target, this::resolveBaseHeightBatch);
|
||||
}
|
||||
|
||||
private void resolveBaseHeightBatch(long[] missing, int size, double[] values) {
|
||||
double[] xs = new double[size];
|
||||
double[] zs = new double[size];
|
||||
double[] lows = new double[size];
|
||||
double[] highs = new double[size];
|
||||
double[] activeXs = new double[size];
|
||||
double[] activeZs = new double[size];
|
||||
double[] noise = new double[size];
|
||||
double[] groupHeights = new double[size];
|
||||
int[] active = new int[size];
|
||||
for (int index = 0; index < size; index++) {
|
||||
xs[index] = RiverFootprint.unpackX(missing[index]);
|
||||
zs[index] = RiverFootprint.unpackZ(missing[index]);
|
||||
}
|
||||
long seed = terrainEngine.getSeedManager().getHeight() + 239945;
|
||||
for (int group = 0; group < frozenInterpolators.length; group++) {
|
||||
IrisGenerator[] generators = frozenGenerators[group];
|
||||
if (generators.length == 0) {
|
||||
continue;
|
||||
}
|
||||
int activeCount = 0;
|
||||
Arrays.fill(groupHeights, 0D);
|
||||
for (int index = 0; index < size; index++) {
|
||||
NoiseBounds bounds = gridSampleBounds(terrainEngine, frozenInterpolators[group], group,
|
||||
generators, xs[index], zs[index]);
|
||||
lows[index] = bounds.min();
|
||||
highs[index] = bounds.max();
|
||||
if (!Double.isFinite(lows[index]) || !Double.isFinite(highs[index])) {
|
||||
averageGeneratorHeights(generators, lows[index], highs[index], xs[index], zs[index], seed);
|
||||
}
|
||||
if (lows[index] != highs[index]) {
|
||||
active[activeCount] = index;
|
||||
activeXs[activeCount] = xs[index];
|
||||
activeZs[activeCount++] = zs[index];
|
||||
} else {
|
||||
for (int generator = 0; generator < generators.length; generator++) {
|
||||
groupHeights[index] += lows[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
for (IrisGenerator generator : generators) {
|
||||
if (activeCount == 0) {
|
||||
break;
|
||||
}
|
||||
generator.getHeights(activeXs, activeZs, seed, noise, activeCount);
|
||||
for (int sample = 0; sample < activeCount; sample++) {
|
||||
int index = active[sample];
|
||||
if (!Double.isFinite(noise[sample])) {
|
||||
throw new IllegalStateException("Terrain generator noise was not finite at " + xs[index] + "," + zs[index]
|
||||
+ " (generator=" + generator.getLoadKey() + ", noise=" + noise[sample]
|
||||
+ ", bounds=" + lows[index] + ".." + highs[index] + ").");
|
||||
}
|
||||
groupHeights[index] += M.lerp(lows[index], highs[index], noise[sample]);
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < size; index++) {
|
||||
values[index] += groupHeights[index] / generators.length;
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < size; index++) {
|
||||
double height = Math.max(Math.min(values[index] + fluidHeight + overlayStream.get(xs[index], zs[index]),
|
||||
terrainEngine.getHeight()), 0);
|
||||
values[index] = imageMapRuntime.sampleTerrainHeight(xs[index], zs[index], height);
|
||||
}
|
||||
}
|
||||
|
||||
private void sampleUnblendedHeightBatch(long[] coordinates, int count, double[] target) {
|
||||
if (terrain3D == null) {
|
||||
sampleBaseHeightBatch(coordinates, count, target);
|
||||
return;
|
||||
}
|
||||
((CachedDoubleStream2D) unblendedNaturalHeightStream).getBatch(coordinates, count, target, (missing, size, values) -> {
|
||||
long[] required = new long[size];
|
||||
int requiredCount = 0;
|
||||
for (int index = 0; index < size; index++) {
|
||||
if (terrain3D.requiresBaseHeight(RiverFootprint.unpackX(missing[index]), RiverFootprint.unpackZ(missing[index]))) {
|
||||
required[requiredCount++] = missing[index];
|
||||
}
|
||||
}
|
||||
if (requiredCount > 0) {
|
||||
sampleBaseHeightBatch(required, requiredCount, new double[requiredCount]);
|
||||
}
|
||||
for (int index = 0; index < size; index++) {
|
||||
values[index] = sampleTerrain3DHeight(RiverFootprint.unpackX(missing[index]), RiverFootprint.unpackZ(missing[index]));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private final class HydrologyHeightProvider implements IrisHydrologyNaturalHeightProvider {
|
||||
private final boolean batch = supportsHeightBatch();
|
||||
|
||||
@Override
|
||||
public double sample(int blockX, int blockZ) {
|
||||
return naturalHeightStream.getDouble(blockX, blockZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsBatch() {
|
||||
return batch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sampleBatch(long[] coordinates, int count, double[] heights) {
|
||||
if (!batch) {
|
||||
IrisHydrologyNaturalHeightProvider.super.sampleBatch(coordinates, count, heights);
|
||||
} else if (transitionDisplacement == null) {
|
||||
sampleUnblendedHeightBatch(coordinates, count, heights);
|
||||
} else {
|
||||
((CachedDoubleStream2D) naturalHeightStream).getBatch(coordinates, count, heights, (missing, size, values) -> {
|
||||
sampleUnblendedHeightBatch(missing, size, values);
|
||||
for (int index = 0; index < size; index++) {
|
||||
values[index] = transitionDisplacement.height(RiverFootprint.unpackX(missing[index]),
|
||||
RiverFootprint.unpackZ(missing[index]), values[index]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private double sampleUnblendedNaturalTerrainHeight(double x, double z) {
|
||||
double proceduralHeight = proceduralTerrainHeight.sample(x, z);
|
||||
return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight);
|
||||
|
||||
@@ -50,6 +50,20 @@ public class CachedDoubleStream2D extends BasicStream<Double> implements Procedu
|
||||
}
|
||||
}
|
||||
|
||||
public void getBatch(long[] coordinates, int count, double[] target, WorldCache2DDouble.BatchResolver resolver) {
|
||||
try {
|
||||
cache.getBatch(coordinates, count, target, (missing, size, values) ->
|
||||
ProvisionalSampling.memoizable(() -> {
|
||||
resolver.resolve(missing, size, values);
|
||||
return values;
|
||||
}));
|
||||
} catch (ProvisionalSampling.Unmemoizable provisional) {
|
||||
for (int index = 0; index < count; index++) {
|
||||
target[index] = getDouble((int) (coordinates[index] >> 32), (int) coordinates[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSize() {
|
||||
return cache.getSize();
|
||||
|
||||
@@ -16,6 +16,10 @@ public final class ProvisionalSampling {
|
||||
private ProvisionalSampling() {
|
||||
}
|
||||
|
||||
public static long revision() {
|
||||
return TRACKERS.get().revision;
|
||||
}
|
||||
|
||||
/** Records that the calling thread just answered from inputs that are not final. */
|
||||
public static void mark() {
|
||||
TRACKERS.get().revision++;
|
||||
|
||||
@@ -63,6 +63,10 @@ public final class Terrain3DRuntime {
|
||||
return Math.max(ANCHOR_BLOCK_SIZE, (int) (maximumEntries / ANCHOR_BLOCK_SIZE) * ANCHOR_BLOCK_SIZE);
|
||||
}
|
||||
|
||||
public boolean requiresBaseHeight(int x, int z) {
|
||||
return !active() || columns.get(x, z) == null;
|
||||
}
|
||||
|
||||
public double height(int x, int z) {
|
||||
if (!active()) {
|
||||
return baseHeight(x, z);
|
||||
|
||||
+382
-13
@@ -1,37 +1,385 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyBasisCacheTest {
|
||||
@Test
|
||||
public void batchLookupPreservesNullAndFirstPublishedSample() {
|
||||
public void batchLookupPreservesNullDuplicatesAndEviction() {
|
||||
CountingSampler sampler = new CountingSampler();
|
||||
HydrologyBasisCache cache = new HydrologyBasisCache(1);
|
||||
HydrologyTerrainSample[] samples = new HydrologyTerrainSample[1];
|
||||
long key = RiverFootprint.pack(-17, Integer.MAX_VALUE);
|
||||
assertTrue(!cache.findWithoutSlope(key, samples, 0));
|
||||
assertNull(cache.rememberWithoutSlope(key, null));
|
||||
assertTrue(cache.findWithoutSlope(key, samples, 0));
|
||||
assertNull(samples[0]);
|
||||
HydrologyTerrainSample later = HydrologyTerrainSample.openLand(73, 0D, "land");
|
||||
assertNull(cache.rememberWithoutSlope(key, later));
|
||||
cache.rememberWithoutSlope(key + 1, later);
|
||||
assertTrue(!cache.findWithoutSlope(key, samples, 0));
|
||||
assertEquals(later, cache.rememberWithoutSlope(key + 1, null));
|
||||
long key = RiverFootprint.pack(Integer.MIN_VALUE, Integer.MAX_VALUE);
|
||||
assertArrayEquals(new HydrologyTerrainSample[2],
|
||||
cache.sampleWithoutSlopeBatch(new long[]{key, key}, 2, sampler));
|
||||
assertNull(cache.sampleWithoutSlope(Integer.MIN_VALUE, Integer.MAX_VALUE, sampler));
|
||||
assertEquals(1, sampler.slopeFreeCalls.get());
|
||||
long later = RiverFootprint.pack(73, -17);
|
||||
assertEquals(87, cache.sampleWithoutSlopeBatch(new long[]{later}, 1, sampler)[0].naturalHeight());
|
||||
assertNull(cache.sampleWithoutSlopeBatch(new long[]{key}, 1, sampler)[0]);
|
||||
assertEquals(3, sampler.slopeFreeCalls.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void batchOwnershipKeepsProviderOrderAcrossStripes() {
|
||||
HydrologyBasisCache cache = new HydrologyBasisCache();
|
||||
CountingSampler sampler = new CountingSampler() {
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleBasisWithoutSlopeBatch(long[] coordinates, int count) {
|
||||
assertArrayEquals(new long[]{RiverFootprint.pack(7, 0), RiverFootprint.pack(-9, 0),
|
||||
RiverFootprint.pack(-17, 0)}, Arrays.copyOf(coordinates, count));
|
||||
return super.sampleBasisWithoutSlopeBatch(coordinates, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HydrologyTerrainSample sampleBasisWithoutSlope(int x, int z) {
|
||||
return HydrologyTerrainSample.openLand(63 + x, 0D, "land");
|
||||
}
|
||||
};
|
||||
HydrologyTerrainSample seeded = cache.sampleWithoutSlope(4, 0, sampler);
|
||||
HydrologyTerrainSample[] samples = cache.sampleWithoutSlopeBatch(new long[]{
|
||||
RiverFootprint.pack(7, 0), RiverFootprint.pack(-9, 0), RiverFootprint.pack(7, 0),
|
||||
RiverFootprint.pack(4, 0), RiverFootprint.pack(-17, 0), RiverFootprint.pack(4, 0)}, 6, sampler);
|
||||
assertEquals(70, samples[0].naturalHeight());
|
||||
assertEquals(54, samples[1].naturalHeight());
|
||||
assertSame(samples[0], samples[2]);
|
||||
assertSame(seeded, samples[3]);
|
||||
assertEquals(46, samples[4].naturalHeight());
|
||||
assertSame(seeded, samples[5]);
|
||||
}
|
||||
|
||||
@Test(timeout = 10_000)
|
||||
public void overlappingBatchesDispatchOnlyTheirOwnedCoordinates() throws Exception {
|
||||
HydrologyBasisCache cache = new HydrologyBasisCache();
|
||||
CountDownLatch firstStarted = new CountDownLatch(1);
|
||||
CountDownLatch secondStarted = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
AtomicInteger batchCalls = new AtomicInteger();
|
||||
CountingSampler sampler = new CountingSampler() {
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleBasisWithoutSlopeBatch(long[] coordinates, int count) {
|
||||
if (batchCalls.getAndIncrement() == 0) {
|
||||
assertEquals(2, count);
|
||||
firstStarted.countDown();
|
||||
} else {
|
||||
assertEquals(1, count);
|
||||
assertEquals(RiverFootprint.pack(3, 0), coordinates[0]);
|
||||
secondStarted.countDown();
|
||||
}
|
||||
await(release);
|
||||
return super.sampleBasisWithoutSlopeBatch(coordinates, count);
|
||||
}
|
||||
};
|
||||
ExecutorService workers = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<HydrologyTerrainSample[]> first = workers.submit(() -> cache.sampleWithoutSlopeBatch(
|
||||
new long[]{RiverFootprint.pack(1, 0), RiverFootprint.pack(2, 0)}, 2, sampler));
|
||||
assertTrue(firstStarted.await(5, TimeUnit.SECONDS));
|
||||
Future<HydrologyTerrainSample[]> second = workers.submit(() -> cache.sampleWithoutSlopeBatch(
|
||||
new long[]{RiverFootprint.pack(2, 0), RiverFootprint.pack(3, 0)}, 2, sampler));
|
||||
assertTrue(secondStarted.await(5, TimeUnit.SECONDS));
|
||||
release.countDown();
|
||||
assertSame(first.get(5, TimeUnit.SECONDS)[1], second.get(5, TimeUnit.SECONDS)[0]);
|
||||
assertEquals(3, sampler.slopeFreeCalls.get());
|
||||
assertEquals(2, batchCalls.get());
|
||||
} finally {
|
||||
release.countDown();
|
||||
workers.shutdownNow();
|
||||
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 10_000)
|
||||
public void pointAndBatchSharePendingSamplesWithoutHoldingStripeLocks() throws Exception {
|
||||
HydrologyBasisCache cache = new HydrologyBasisCache(1);
|
||||
CountDownLatch pointStarted = new CountDownLatch(1);
|
||||
CountDownLatch batchStarted = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
CountingSampler sampler = new CountingSampler() {
|
||||
@Override
|
||||
public HydrologyTerrainSample sampleBasisWithoutSlope(int x, int z) {
|
||||
if (x == 1) {
|
||||
pointStarted.countDown();
|
||||
await(release);
|
||||
}
|
||||
return super.sampleBasisWithoutSlope(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleBasisWithoutSlopeBatch(long[] coordinates, int count) {
|
||||
assertEquals(1, count);
|
||||
assertEquals(RiverFootprint.pack(2, 0), coordinates[0]);
|
||||
batchStarted.countDown();
|
||||
return super.sampleBasisWithoutSlopeBatch(coordinates, count);
|
||||
}
|
||||
};
|
||||
ExecutorService workers = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<HydrologyTerrainSample> point = workers.submit(() -> cache.sampleWithoutSlope(1, 0, sampler));
|
||||
assertTrue(pointStarted.await(5, TimeUnit.SECONDS));
|
||||
Future<HydrologyTerrainSample[]> batch = workers.submit(() -> cache.sampleWithoutSlopeBatch(
|
||||
new long[]{RiverFootprint.pack(1, 0), RiverFootprint.pack(2, 0)}, 2, sampler));
|
||||
assertTrue(batchStarted.await(5, TimeUnit.SECONDS));
|
||||
release.countDown();
|
||||
assertSame(point.get(5, TimeUnit.SECONDS), batch.get(5, TimeUnit.SECONDS)[0]);
|
||||
assertEquals(2, sampler.slopeFreeCalls.get());
|
||||
} finally {
|
||||
release.countDown();
|
||||
workers.shutdownNow();
|
||||
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 10_000)
|
||||
public void nestedBatchesFinishOnSingleWorkerPool() throws Exception {
|
||||
HydrologyBasisCache cache = new HydrologyBasisCache();
|
||||
ForkJoinPool workers = new ForkJoinPool(1);
|
||||
CountingSampler sampler = new CountingSampler() {
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleBasisWithoutSlopeBatch(long[] coordinates, int count) {
|
||||
List<HydrologyTerrainSample> values = HydrologyForkJoin.invokeAll(List.of(
|
||||
() -> sampleBasisWithoutSlope(RiverFootprint.unpackX(coordinates[0]), 0),
|
||||
() -> sampleBasisWithoutSlope(RiverFootprint.unpackX(coordinates[1]), 0)), workers);
|
||||
return values.toArray(HydrologyTerrainSample[]::new);
|
||||
}
|
||||
};
|
||||
try {
|
||||
workers.submit(() -> HydrologyForkJoin.invokeAll(List.of(
|
||||
() -> cache.sampleWithoutSlopeBatch(new long[]{RiverFootprint.pack(1, 0), RiverFootprint.pack(2, 0)}, 2, sampler),
|
||||
() -> cache.sampleWithoutSlopeBatch(new long[]{RiverFootprint.pack(2, 0), RiverFootprint.pack(1, 0)}, 2, sampler)), workers))
|
||||
.get(5, TimeUnit.SECONDS);
|
||||
assertEquals(2, sampler.slopeFreeCalls.get());
|
||||
} finally {
|
||||
workers.shutdownNow();
|
||||
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedMalformedAndRecursiveBatchesReleaseEveryReservation() {
|
||||
HydrologyBasisCache cache = new HydrologyBasisCache();
|
||||
long[] coordinates = {RiverFootprint.pack(1, 0), RiverFootprint.pack(2, 0)};
|
||||
CountingSampler failed = new CountingSampler() {
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleBasisWithoutSlopeBatch(long[] keys, int count) {
|
||||
throw new IllegalArgumentException("failure");
|
||||
}
|
||||
};
|
||||
assertThrows(IllegalArgumentException.class, () -> cache.sampleWithoutSlopeBatch(coordinates, 2, failed));
|
||||
CountingSampler malformed = new CountingSampler() {
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleBasisWithoutSlopeBatch(long[] keys, int count) {
|
||||
return new HydrologyTerrainSample[1];
|
||||
}
|
||||
};
|
||||
assertThrows(IllegalStateException.class, () -> cache.sampleWithoutSlopeBatch(coordinates, 2, malformed));
|
||||
CountingSampler absent = new CountingSampler() {
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleBasisWithoutSlopeBatch(long[] keys, int count) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
assertThrows(NullPointerException.class, () -> cache.sampleWithoutSlopeBatch(coordinates, 2, absent));
|
||||
CountingSampler recursive = new CountingSampler() {
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleBasisWithoutSlopeBatch(long[] keys, int count) {
|
||||
cache.sampleWithoutSlope(2, 0, this);
|
||||
throw new AssertionError("Recursive sampling must not wait on itself");
|
||||
}
|
||||
};
|
||||
assertThrows(IllegalStateException.class, () -> cache.sampleWithoutSlopeBatch(coordinates, 2, recursive));
|
||||
CountingSampler successful = new CountingSampler();
|
||||
assertEquals(2, cache.sampleWithoutSlopeBatch(coordinates, 2, successful).length);
|
||||
assertEquals(2, successful.slopeFreeCalls.get());
|
||||
}
|
||||
|
||||
@Test(timeout = 10_000)
|
||||
public void interruptedPointWaiterReceivesBatchResultAndRestoresInterrupt() throws Exception {
|
||||
HydrologyBasisCache cache = new HydrologyBasisCache();
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
AtomicReference<Thread> waiting = new AtomicReference<>();
|
||||
CountingSampler sampler = new CountingSampler() {
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleBasisWithoutSlopeBatch(long[] coordinates, int count) {
|
||||
started.countDown();
|
||||
await(release);
|
||||
return super.sampleBasisWithoutSlopeBatch(coordinates, count);
|
||||
}
|
||||
};
|
||||
ExecutorService workers = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<HydrologyTerrainSample[]> batch = workers.submit(() -> cache.sampleWithoutSlopeBatch(
|
||||
new long[]{RiverFootprint.pack(1, 0)}, 1, sampler));
|
||||
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||
Future<HydrologyTerrainSample> point = workers.submit(() -> {
|
||||
waiting.set(Thread.currentThread());
|
||||
HydrologyTerrainSample sample = cache.sampleWithoutSlope(1, 0, sampler);
|
||||
assertTrue(Thread.interrupted());
|
||||
return sample;
|
||||
});
|
||||
awaitWaiting(waiting);
|
||||
waiting.get().interrupt();
|
||||
release.countDown();
|
||||
assertSame(batch.get(5, TimeUnit.SECONDS)[0], point.get(5, TimeUnit.SECONDS));
|
||||
assertEquals(1, sampler.slopeFreeCalls.get());
|
||||
} finally {
|
||||
release.countDown();
|
||||
workers.shutdownNow();
|
||||
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 10_000)
|
||||
public void batchFailureReachesPointWaiterAndAllowsAllCoordinatesToRetry() throws Exception {
|
||||
HydrologyBasisCache cache = new HydrologyBasisCache();
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
AtomicReference<Thread> waiting = new AtomicReference<>();
|
||||
IllegalArgumentException failure = new IllegalArgumentException("retry");
|
||||
CountingSampler sampler = new CountingSampler() {
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleBasisWithoutSlopeBatch(long[] coordinates, int count) {
|
||||
started.countDown();
|
||||
await(release);
|
||||
throw failure;
|
||||
}
|
||||
};
|
||||
long[] coordinates = {RiverFootprint.pack(1, 0), RiverFootprint.pack(2, 0)};
|
||||
ExecutorService workers = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
Future<IllegalArgumentException> batch = workers.submit(() -> assertThrows(IllegalArgumentException.class,
|
||||
() -> cache.sampleWithoutSlopeBatch(coordinates, 2, sampler)));
|
||||
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||
Future<IllegalArgumentException> point = workers.submit(() -> {
|
||||
waiting.set(Thread.currentThread());
|
||||
return assertThrows(IllegalArgumentException.class, () -> cache.sampleWithoutSlope(2, 0, sampler));
|
||||
});
|
||||
awaitWaiting(waiting);
|
||||
release.countDown();
|
||||
assertSame(failure, batch.get(5, TimeUnit.SECONDS));
|
||||
assertSame(failure, point.get(5, TimeUnit.SECONDS));
|
||||
CountingSampler successful = new CountingSampler();
|
||||
assertEquals(2, cache.sampleWithoutSlopeBatch(coordinates, 2, successful).length);
|
||||
assertEquals(2, successful.slopeFreeCalls.get());
|
||||
} finally {
|
||||
release.countDown();
|
||||
workers.shutdownNow();
|
||||
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void provisionalResultsAreNotRetained() {
|
||||
HydrologyBasisCache cache = new HydrologyBasisCache();
|
||||
CountingSampler sampler = new CountingSampler() {
|
||||
@Override
|
||||
public HydrologyTerrainSample sampleBasisWithoutSlope(int x, int z) {
|
||||
ProvisionalSampling.mark();
|
||||
return super.sampleBasisWithoutSlope(x, z);
|
||||
}
|
||||
};
|
||||
long[] coordinates = {RiverFootprint.pack(1, 0), RiverFootprint.pack(2, 0)};
|
||||
cache.sampleWithoutSlopeBatch(coordinates, 2, sampler);
|
||||
cache.sampleWithoutSlopeBatch(coordinates, 2, sampler);
|
||||
cache.sampleWithoutSlope(1, 0, sampler);
|
||||
cache.sampleWithoutSlope(1, 0, sampler);
|
||||
assertEquals(6, sampler.slopeFreeCalls.get());
|
||||
}
|
||||
|
||||
@Test(timeout = 10_000)
|
||||
public void provisionalWorkerResultMarksSharedCacheWaitersAndIsNotRetained() throws Exception {
|
||||
HydrologyBasisCache cache = new HydrologyBasisCache();
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
AtomicReference<Thread> waiting = new AtomicReference<>();
|
||||
ExecutorService providerWorkers = Executors.newFixedThreadPool(2);
|
||||
CountingSampler sampler = new CountingSampler() {
|
||||
@Override
|
||||
public HydrologyTerrainSample[] sampleBasisWithoutSlopeBatch(long[] coordinates, int count) {
|
||||
List<HydrologyTerrainSample> values = HydrologyForkJoin.invokeAll(List.of(() -> {
|
||||
started.countDown();
|
||||
await(release);
|
||||
ProvisionalSampling.mark();
|
||||
return sampleBasisWithoutSlope(RiverFootprint.unpackX(coordinates[0]), 0);
|
||||
}, () -> sampleBasisWithoutSlope(RiverFootprint.unpackX(coordinates[1]), 0)), providerWorkers);
|
||||
return values.toArray(HydrologyTerrainSample[]::new);
|
||||
}
|
||||
};
|
||||
ExecutorService callers = Executors.newFixedThreadPool(2);
|
||||
long[] coordinates = {RiverFootprint.pack(1, 0), RiverFootprint.pack(2, 0)};
|
||||
try {
|
||||
Future<HydrologyTerrainSample[]> batch = callers.submit(() -> cache.sampleWithoutSlopeBatch(coordinates, 2, sampler));
|
||||
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||
Future<ProvisionalSampling.Unmemoizable> point = callers.submit(() -> {
|
||||
waiting.set(Thread.currentThread());
|
||||
return assertThrows(ProvisionalSampling.Unmemoizable.class,
|
||||
() -> ProvisionalSampling.memoizable(() -> cache.sampleWithoutSlope(1, 0, sampler)));
|
||||
});
|
||||
awaitWaiting(waiting);
|
||||
release.countDown();
|
||||
assertSame(batch.get(5, TimeUnit.SECONDS)[0], point.get(5, TimeUnit.SECONDS).value());
|
||||
cache.sampleWithoutSlopeBatch(coordinates, 2, sampler);
|
||||
assertEquals(4, sampler.slopeFreeCalls.get());
|
||||
} finally {
|
||||
release.countDown();
|
||||
callers.shutdownNow();
|
||||
providerWorkers.shutdownNow();
|
||||
assertTrue(callers.awaitTermination(5, TimeUnit.SECONDS));
|
||||
assertTrue(providerWorkers.awaitTermination(5, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 10_000)
|
||||
public void seedWinsPendingSample() throws Exception {
|
||||
HydrologyBasisCache cache = new HydrologyBasisCache();
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
CountingSampler sampler = new CountingSampler() {
|
||||
@Override
|
||||
public HydrologyTerrainSample sampleBasisWithoutSlope(int x, int z) {
|
||||
started.countDown();
|
||||
await(release);
|
||||
return super.sampleBasisWithoutSlope(x, z);
|
||||
}
|
||||
};
|
||||
ExecutorService workers = Executors.newSingleThreadExecutor();
|
||||
try {
|
||||
Future<HydrologyTerrainSample[]> result = workers.submit(() -> cache.sampleWithoutSlopeBatch(
|
||||
new long[]{RiverFootprint.pack(1, 0)}, 1, sampler));
|
||||
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||
HydrologyTerrainSample seeded = HydrologyTerrainSample.openLand(73, 0D, "seed");
|
||||
Long2ObjectOpenHashMap<HydrologyTerrainSample> seeds = new Long2ObjectOpenHashMap<>();
|
||||
seeds.put(RiverFootprint.pack(1, 0), seeded);
|
||||
cache.seedWithoutSlope(seeds);
|
||||
release.countDown();
|
||||
assertSame(seeded, result.get(5, TimeUnit.SECONDS)[0]);
|
||||
assertSame(seeded, cache.sampleWithoutSlope(1, 0, sampler));
|
||||
} finally {
|
||||
release.countDown();
|
||||
workers.shutdownNow();
|
||||
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -122,7 +470,28 @@ public class HydrologyBasisCacheTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CountingSampler implements HydrologyNaturalTerrainSampler {
|
||||
private static void awaitWaiting(AtomicReference<Thread> thread) {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
|
||||
while (System.nanoTime() < deadline) {
|
||||
Thread current = thread.get();
|
||||
if (current != null && current.getState() == Thread.State.WAITING) {
|
||||
return;
|
||||
}
|
||||
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
|
||||
}
|
||||
throw new AssertionError("Cache waiter did not wait for its owner");
|
||||
}
|
||||
|
||||
private static void await(CountDownLatch latch) {
|
||||
try {
|
||||
assertTrue(latch.await(5, TimeUnit.SECONDS));
|
||||
} catch (InterruptedException interruption) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new AssertionError(interruption);
|
||||
}
|
||||
}
|
||||
|
||||
private static class CountingSampler implements HydrologyNaturalTerrainSampler {
|
||||
private final AtomicInteger fullCalls = new AtomicInteger();
|
||||
private final AtomicInteger slopeFreeCalls = new AtomicInteger();
|
||||
private final AtomicInteger failures = new AtomicInteger();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -23,6 +24,44 @@ import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyForkJoinTest {
|
||||
@Test(timeout = 10_000)
|
||||
public void workerProvisionalAnswersReachEveryAwaitingCaller() throws Exception {
|
||||
HydrologyForkJoin.Task<Integer> task = new HydrologyForkJoin.Task<>(() -> {
|
||||
ProvisionalSampling.mark();
|
||||
return 73;
|
||||
});
|
||||
ExecutorService workers = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
workers.submit(task).get(5, TimeUnit.SECONDS);
|
||||
for (int attempt = 0; attempt < 2; attempt++) {
|
||||
ProvisionalSampling.Unmemoizable result = workers.submit(() -> assertThrows(
|
||||
ProvisionalSampling.Unmemoizable.class,
|
||||
() -> ProvisionalSampling.memoizable(task::await))).get(5, TimeUnit.SECONDS);
|
||||
assertEquals(Integer.valueOf(73), result.value());
|
||||
}
|
||||
} finally {
|
||||
workers.shutdownNow();
|
||||
assertTrue(workers.awaitTermination(5, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 10_000)
|
||||
public void provisionalWorkerMarksTheBatchCaller() {
|
||||
ExecutorService workers = Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
ProvisionalSampling.Unmemoizable result = assertThrows(ProvisionalSampling.Unmemoizable.class,
|
||||
() -> ProvisionalSampling.memoizable(() -> HydrologyForkJoin.invokeAll(List.of(
|
||||
() -> 1,
|
||||
() -> {
|
||||
ProvisionalSampling.mark();
|
||||
return 2;
|
||||
}), workers)));
|
||||
assertEquals(List.of(1, 2), result.value());
|
||||
} finally {
|
||||
workers.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blockedGridBatchNeverHelpsUnrelatedOwnerWork() throws Exception {
|
||||
ForkJoinPool pool = new ForkJoinPool(2, ForkJoinPool.defaultForkJoinWorkerThreadFactory,
|
||||
|
||||
+16
-5
@@ -279,11 +279,22 @@ public class PreparedHydrologyTileStoreTest {
|
||||
HydrologyTile original = tile();
|
||||
PreparedHydrologyTileStore store = store();
|
||||
store.save(original);
|
||||
HydrologyTile oversized = new HydrologyTile(original.key(), original.worldSeed(),
|
||||
original.settingsFingerprint(), original.tileSize(), original.nodes(), original.edges(),
|
||||
original.outlets(), original.courses(), original.regionalCourseIds(), original.cavePlans(),
|
||||
Collections.nCopies(600_000, original.localDiagnosticCandidates().getFirst()), original.footprint());
|
||||
assertThrows(IOException.class, () -> store.save(oversized));
|
||||
long allocationLimit = Math.max(16L * 1024L * 1024L,
|
||||
Math.min(512L * 1024L * 1024L, Runtime.getRuntime().maxMemory() / 8L));
|
||||
int pointCount = 4096;
|
||||
int courseCount = Math.toIntExact(allocationLimit / (256L * pointCount) + 1L);
|
||||
HydraulicSegment segment = new HydraulicSegment(45L, 44L, HydrologyFeatureType.SURFACE_POOL,
|
||||
72, 72, 4, 2, false, false, Collections.nCopies(pointCount, new HydrologyPoint(0, 72, 0)),
|
||||
HydraulicChannelProfile.uniform(4D, 2D));
|
||||
RiverCourse course = new RiverCourse(44L, RiverCourseType.SURFACE, OptionalLong.of(1L),
|
||||
OptionalLong.of(2L), "default", 1, List.of(), List.of(segment));
|
||||
HydrologyOwnerDraft draft = new HydrologyOwnerDraft(original.key(),
|
||||
new HydrologyCaveCourseFilter.Result(original.nodes(), original.edges(), original.outlets(),
|
||||
original.courses(), original.cavePlans()), original.localDiagnosticCandidates(), null);
|
||||
HydrologyTile oversized = original.withResolvedOwner(new CrossTileResolvedOwner(draft,
|
||||
Collections.nCopies(courseCount, new CrossTileRejectedCourse(course, 4L))));
|
||||
IOException failure = assertThrows(IOException.class, () -> store.save(oversized));
|
||||
assertEquals("Prepared hydrology tile exceeds its allocation budget.", failure.getMessage());
|
||||
assertEquals(original, store.load(original.key()).orElseThrow());
|
||||
try (Stream<Path> files = Files.list(store.file(original.key()).getParent())) {
|
||||
assertEquals(List.of(store.file(original.key())), files.toList());
|
||||
|
||||
+127
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.generation.hydrology.runtime;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.HydrologyRoutingTerrainSampler;
|
||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||
import art.arcane.iris.generation.hydrology.HydrologyTerrainSample;
|
||||
import art.arcane.iris.generation.hydrology.RiverFootprint;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
@@ -40,6 +41,132 @@ import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
public class IrisHydrologyRoutingTerrainSamplerTest {
|
||||
@Test
|
||||
public void provisionalOceanClassificationIsReevaluated() {
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
IrisHydrologyRoutingTerrainSampler sampler = new IrisHydrologyRoutingTerrainSampler(
|
||||
new IrisHydrologyRoutingTerrainSampler.Sources((x, z, height) -> {
|
||||
throw new AssertionError("Classification must not load a basis");
|
||||
}, (x, z) -> {
|
||||
if (calls.incrementAndGet() == 1) {
|
||||
ProvisionalSampling.mark();
|
||||
return 100D;
|
||||
}
|
||||
return 40D;
|
||||
}, (x, z) -> true, 60), IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(16));
|
||||
assertEquals(IrisHydrologyRoutingTerrainSampler.NaturalClassification.LAND, sampler.classifyNatural(2, 3));
|
||||
assertEquals(0, sampler.oceanClassificationCacheSize());
|
||||
assertEquals(IrisHydrologyRoutingTerrainSampler.NaturalClassification.OCEAN, sampler.classifyNatural(2, 3));
|
||||
assertEquals(1, sampler.oceanClassificationCacheSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bulkHeightProviderReceivesOnlyUniqueUncachedCoordinates() {
|
||||
ArrayList<Long> requested = new ArrayList<>();
|
||||
IrisHydrologyNaturalHeightProvider heights = new IrisHydrologyNaturalHeightProvider() {
|
||||
@Override
|
||||
public double sample(int x, int z) {
|
||||
return 100D + Math.floorMod(x + z, 17);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsBatch() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sampleBatch(long[] coordinates, int count, double[] target) {
|
||||
for (int index = 0; index < count; index++) {
|
||||
requested.add(coordinates[index]);
|
||||
target[index] = sample(RiverFootprint.unpackX(coordinates[index]), RiverFootprint.unpackZ(coordinates[index]));
|
||||
}
|
||||
}
|
||||
};
|
||||
IrisHydrologyRoutingTerrainSampler sampler = new IrisHydrologyRoutingTerrainSampler(
|
||||
new IrisHydrologyRoutingTerrainSampler.Sources((x, z, height) ->
|
||||
new IrisHydrologyRoutingTerrainSampler.TerrainBasis(height,
|
||||
HydrologyTerrainSample.openLand((int) height, 0D, "land")), heights, (x, z) -> false, 60),
|
||||
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(8192));
|
||||
sampler.sampleBasisWithoutSlope(-1, 5);
|
||||
sampler.sampleLandHeight(6, -7);
|
||||
long[] coordinates = {RiverFootprint.pack(-1, 5), RiverFootprint.pack(6, -7),
|
||||
RiverFootprint.pack(-9, 19), RiverFootprint.pack(-9, 19),
|
||||
RiverFootprint.pack(Integer.MAX_VALUE, Integer.MIN_VALUE)};
|
||||
for (int repeat = 0; repeat < 2; repeat++) {
|
||||
HydrologyTerrainSample[] samples = sampler.sampleBasisWithoutSlopeBatch(coordinates, coordinates.length);
|
||||
for (int index = 0; index < coordinates.length; index++) {
|
||||
assertEquals((int) heights.sample(RiverFootprint.unpackX(coordinates[index]),
|
||||
RiverFootprint.unpackZ(coordinates[index])), samples[index].naturalHeight());
|
||||
}
|
||||
}
|
||||
assertEquals(List.of(coordinates[2], coordinates[4]), requested);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void gridUsesBoundedHeightSlices() {
|
||||
ArrayList<Integer> sizes = new ArrayList<>();
|
||||
IrisHydrologyNaturalHeightProvider heights = new IrisHydrologyNaturalHeightProvider() {
|
||||
@Override
|
||||
public double sample(int x, int z) {
|
||||
throw new AssertionError("Expected bulk height sampling");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsBatch() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sampleBatch(long[] coordinates, int count, double[] target) {
|
||||
sizes.add(count);
|
||||
for (int index = 0; index < count; index++) {
|
||||
target[index] = 100D;
|
||||
}
|
||||
}
|
||||
};
|
||||
IrisHydrologyRoutingTerrainSampler sampler = new IrisHydrologyRoutingTerrainSampler(
|
||||
new IrisHydrologyRoutingTerrainSampler.Sources((x, z, height) ->
|
||||
new IrisHydrologyRoutingTerrainSampler.TerrainBasis(height,
|
||||
HydrologyTerrainSample.openLand((int) height, 0D, "land")), heights, (x, z) -> false, 60),
|
||||
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(8192));
|
||||
HydrologyTerrainSample[] samples = sampler.sampleGrid(new HydrologyRoutingTerrainSampler.GridRequest(-1024, -4096, 65, 8));
|
||||
assertEquals(65 * 65, samples.length);
|
||||
assertEquals(List.of(4096, 260), sizes);
|
||||
for (HydrologyTerrainSample sample : samples) {
|
||||
assertEquals(100, sample.naturalHeight());
|
||||
assertEquals(0D, sample.slope(), 0D);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void provisionalBulkAndScalarHeightsNeverBecomeRoutingCacheEntries() {
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
IrisHydrologyNaturalHeightProvider heights = new IrisHydrologyNaturalHeightProvider() {
|
||||
@Override
|
||||
public double sample(int x, int z) {
|
||||
ProvisionalSampling.mark();
|
||||
return 100D + calls.incrementAndGet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsBatch() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
IrisHydrologyRoutingTerrainSampler sampler = new IrisHydrologyRoutingTerrainSampler(
|
||||
new IrisHydrologyRoutingTerrainSampler.Sources((x, z, height) ->
|
||||
new IrisHydrologyRoutingTerrainSampler.TerrainBasis(height,
|
||||
HydrologyTerrainSample.openLand((int) height, 0D, "land")), heights, (x, z) -> false, 60),
|
||||
IrisHydrologyRoutingTerrainSampler.SamplingOptions.serial(16));
|
||||
long[] coordinates = {RiverFootprint.pack(4, -3)};
|
||||
assertEquals(101, sampler.sampleBasisWithoutSlopeBatch(coordinates, 1)[0].naturalHeight());
|
||||
assertEquals(102, sampler.sampleBasisWithoutSlopeBatch(coordinates, 1)[0].naturalHeight());
|
||||
assertEquals(103, sampler.sampleBasisWithoutSlope(4, -3).naturalHeight());
|
||||
assertEquals(104, sampler.sampleBasisWithoutSlope(4, -3).naturalHeight());
|
||||
assertEquals(0, sampler.basisCacheSize());
|
||||
assertEquals(0, sampler.naturalHeightCacheSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void boundedBasisBatchUsesScopedWorkersAndDrainsNestedPool() throws Exception {
|
||||
ThreadLocal<Integer> scope = new ThreadLocal<>();
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package art.arcane.iris.generation.noise;
|
||||
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.clearInvocations;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
public class IrisGeneratorBatchTest {
|
||||
private static final double[] XS = {-0D, 0D, -12D, -5.75D, 18D, 18D, 8191.25D, -29999999.5D, 2147483647D};
|
||||
private static final double[] ZS = {0D, -0D, 6D, 2.5D, -6D, -6D, -4096.75D, 29999998D, -2147483648D};
|
||||
|
||||
@Test
|
||||
public void batchPreservesRawBitsAcrossStylesSeedsAndSurfaceDetails() {
|
||||
NoiseStyle[] styles = {NoiseStyle.SIMPLEX, NoiseStyle.IRIS, NoiseStyle.FRACTAL_SMOKE,
|
||||
NoiseStyle.CLOVER, NoiseStyle.CELLULAR, NoiseStyle.FLAT};
|
||||
long[] seeds = {0L, -1337L, Long.MAX_VALUE - 37L, Long.MIN_VALUE + 23L};
|
||||
for (NoiseStyle style : styles) {
|
||||
for (double detail : new double[]{0D, 0.5D, 1D}) {
|
||||
for (boolean multiply : new boolean[]{false, true}) {
|
||||
IrisGenerator scalar = generator(style, detail, multiply);
|
||||
IrisGenerator batch = generator(style, detail, multiply);
|
||||
assertTrue(batch.supportsBatch());
|
||||
double[] sampled = new double[XS.length];
|
||||
for (long seed : seeds) {
|
||||
batch.getHeights(XS, ZS, seed, sampled, sampled.length);
|
||||
for (int index = 0; index < sampled.length; index++) {
|
||||
assertEquals(style + " detail=" + detail + " seed=" + seed + " index=" + index,
|
||||
Double.doubleToRawLongBits(scalar.getHeight(XS[index], ZS[index], seed)),
|
||||
Double.doubleToRawLongBits(sampled[index]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unroundedCompositeBatchRetainsEveryNoiseBit() {
|
||||
for (NoiseStyle style : new NoiseStyle[]{NoiseStyle.SIMPLEX, NoiseStyle.IRIS, NoiseStyle.CLOVER}) {
|
||||
for (double detail : new double[]{0D, 0.5D, 1D}) {
|
||||
IrisGenerator scalar = generator(style, detail, false).setCliffHeightMax(0D).setCellFractureHeight(0D);
|
||||
IrisGenerator batch = generator(style, detail, false).setCliffHeightMax(0D).setCellFractureHeight(0D);
|
||||
double[] values = new double[XS.length];
|
||||
for (long seed : new long[]{-1337L, Long.MAX_VALUE - 37L}) {
|
||||
batch.getHeights(XS, ZS, seed, values, XS.length);
|
||||
for (int index = 0; index < XS.length; index++) {
|
||||
assertEquals(Double.doubleToRawLongBits(scalar.getHeight(XS[index], ZS[index], seed)),
|
||||
Double.doubleToRawLongBits(values[index]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolvesLayerContextOncePerBatch() {
|
||||
IrisNoiseGenerator layer = spy(new IrisNoiseGenerator().setStyle(NoiseStyle.SIMPLEX.style()));
|
||||
IrisGenerator generator = new IrisGenerator().setComposite(new KList<>(layer));
|
||||
double[] sampled = new double[XS.length];
|
||||
generator.getHeights(XS, ZS, 99L, sampled, XS.length);
|
||||
verify(layer, times(1)).getGenerator(anyLong(), isNull(), isNull());
|
||||
clearInvocations(layer);
|
||||
for (int index = 0; index < XS.length; index++) {
|
||||
generator.getHeight(XS[index], ZS[index], 99L);
|
||||
}
|
||||
verify(layer, times(XS.length)).getGenerator(anyLong(), isNull(), isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customGeneratorFallbackKeepsCoordinateOrder() {
|
||||
List<Double> sampled = new ArrayList<>();
|
||||
IrisGenerator generator = new IrisGenerator() {
|
||||
@Override
|
||||
public double getHeight(double x, double z, long seed) {
|
||||
sampled.add(x);
|
||||
return sampled.size();
|
||||
}
|
||||
};
|
||||
assertFalse(generator.supportsBatch());
|
||||
double[] values = new double[XS.length];
|
||||
generator.getHeights(XS, ZS, 0L, values, values.length);
|
||||
for (int index = 0; index < values.length; index++) {
|
||||
assertEquals(Double.valueOf(XS[index]), sampled.get(index));
|
||||
assertEquals(index + 1D, values[index], 0D);
|
||||
}
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> generator.getHeights(new double[4097], new double[4097], 0L, new double[4097], 4097));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expressionsAndCustomStylesKeepScalarSampling() {
|
||||
assertFalse(new IrisGenerator().setComposite(new KList<>(new IrisNoiseGenerator()
|
||||
.setStyle(new IrisGeneratorStyle().setExpression("height")))).supportsBatch());
|
||||
assertFalse(new IrisGenerator().setComposite(new KList<>(new IrisNoiseGenerator()
|
||||
.setStyle(new IrisGeneratorStyle() {}))).supportsBatch());
|
||||
}
|
||||
|
||||
private static IrisGenerator generator(NoiseStyle style, double detail, boolean multiply) {
|
||||
IrisNoiseGenerator layer = new IrisNoiseGenerator().setStyle(style.style()).setSeed(-831L)
|
||||
.setZoom(0.73D).setOffsetX(3.25D).setOffsetZ(-1.75D).setOpacity(0.7D)
|
||||
.setNegative(true).setExponent(1.37D).setOffsetY(0.03D).setParametric(true);
|
||||
IrisNoiseGenerator node = layer;
|
||||
for (int depth = 0; depth < 5; depth++) {
|
||||
IrisNoiseGenerator child = new IrisNoiseGenerator().setStyle(NoiseStyle.SIMPLEX.style())
|
||||
.setSeed(depth * 7L).setOpacity(3.1D).setZoom(1.7D);
|
||||
node.setFracture(new KList<>(child));
|
||||
node = child;
|
||||
}
|
||||
return new IrisGenerator().setComposite(new KList<>(layer,
|
||||
new IrisNoiseGenerator().setStyle(NoiseStyle.SIMPLEX.style()).setOpacity(0.2D)))
|
||||
.setSurfaceDetail(detail).setMultiplicitive(multiply).setSeed(-93L).setZoom(1.3D)
|
||||
.setOffsetX(7.25D).setOffsetZ(-9.125D).setOpacity(0.93D)
|
||||
.setCliffHeightMin(3.5D).setCliffHeightMax(12.25D)
|
||||
.setCliffHeightGenerator(new IrisNoiseGenerator().setStyle(NoiseStyle.SIMPLEX.style()))
|
||||
.setCellFractureHeight(0.25D);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.generation.noise.IrisGenerator;
|
||||
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||
import art.arcane.iris.generation.noise.IrisNoiseGenerator;
|
||||
import art.arcane.iris.generation.noise.IrisShapedGeneratorStyle;
|
||||
import art.arcane.iris.generation.noise.NoiseStyle;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisComplexHeightBatchTest {
|
||||
@Test
|
||||
public void standardSurfaceGraphsCanBatchAndCoarseSamplingKeepsScalarPath() throws Exception {
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
IrisGenerator generator = new IrisGenerator().setSurfaceDetail(0.5D)
|
||||
.setComposite(new KList<>(new IrisNoiseGenerator().setStyle(NoiseStyle.FRACTAL_SMOKE.style())
|
||||
.setFracture(new KList<>(new IrisNoiseGenerator().setStyle(NoiseStyle.IRIS.style())))));
|
||||
IrisComplex complex = complex(dimension, generator);
|
||||
assertTrue(eligible(complex));
|
||||
for (int step : new int[]{2, 4, 8}) {
|
||||
dimension.setTerrainSamplingStep(step);
|
||||
assertFalse(eligible(complex));
|
||||
}
|
||||
dimension.setTerrainSamplingStep(1);
|
||||
assertTrue(eligible(complex));
|
||||
generator.getComposite().getFirst().setStyle(new IrisGeneratorStyle().setExpression("height"));
|
||||
assertFalse(eligible(complex));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void statefulBiomeAndOverlaySourcesKeepScalarPath() throws Exception {
|
||||
IrisDimension dimension = new IrisDimension();
|
||||
IrisComplex complex = complex(dimension, new IrisGenerator()
|
||||
.setComposite(new KList<>(new IrisNoiseGenerator().setStyle(NoiseStyle.SIMPLEX.style()))));
|
||||
set(complex, "reuseNaturalBaseBiome", false);
|
||||
assertFalse(eligible(complex));
|
||||
set(complex, "reuseNaturalBaseBiome", true);
|
||||
dimension.setOverlayNoise(new KList<>(new IrisShapedGeneratorStyle()
|
||||
.setGenerator(new IrisGeneratorStyle() {})));
|
||||
assertFalse(eligible(complex));
|
||||
dimension.getOverlayNoise().getFirst().setGenerator(NoiseStyle.SIMPLEX.style());
|
||||
assertTrue(eligible(complex));
|
||||
}
|
||||
|
||||
private static IrisComplex complex(IrisDimension dimension, IrisGenerator generator) throws Exception {
|
||||
Engine engine = mock(Engine.class);
|
||||
when(engine.getDimension()).thenReturn(dimension);
|
||||
IrisComplex complex = mock(IrisComplex.class, CALLS_REAL_METHODS);
|
||||
set(complex, "terrainEngine", engine);
|
||||
set(complex, "biomeBuffetGenerators", Map.of());
|
||||
set(complex, "reuseNaturalBaseBiome", true);
|
||||
set(complex, "frozenGenerators", new IrisGenerator[][]{{generator}});
|
||||
return complex;
|
||||
}
|
||||
|
||||
private static boolean eligible(IrisComplex complex) throws Exception {
|
||||
Method method = IrisComplex.class.getDeclaredMethod("supportsHeightBatch");
|
||||
method.setAccessible(true);
|
||||
return (boolean) method.invoke(complex);
|
||||
}
|
||||
|
||||
private static void set(IrisComplex complex, String name, Object value) throws Exception {
|
||||
Field field = IrisComplex.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(complex, value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user