mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 13:30:08 +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:
commit
aa3420b4fa
18 files changed
+1506
-75
No files matched your search
@@ -1,8 +1,15 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
package art.arcane.iris.generation.hydrology;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||||
import art.arcane.volmlib.util.cache.CacheKey;
|
import art.arcane.volmlib.util.cache.CacheKey;
|
||||||
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
||||||
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
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 {
|
final class HydrologyBasisCache {
|
||||||
private static final int MAXIMUM_ENTRIES = 32_768;
|
private static final int MAXIMUM_ENTRIES = 32_768;
|
||||||
@@ -39,23 +46,74 @@ final class HydrologyBasisCache {
|
|||||||
seed(samples, full);
|
seed(samples, full);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean findWithoutSlope(long packed, HydrologyTerrainSample[] samples, int index) {
|
HydrologyTerrainSample[] sampleWithoutSlopeBatch(long[] coordinates, int count,
|
||||||
Stripe stripe = stripe(withoutSlope, packed);
|
HydrologyNaturalTerrainSampler sampler) {
|
||||||
synchronized (stripe) {
|
Objects.checkFromIndexSize(0, count, coordinates.length);
|
||||||
samples[index] = stripe.samples.getAndMoveToFirst(packed);
|
if (count > HydrologyTerrainSampler.MAXIMUM_BATCH_SIZE) {
|
||||||
return samples[index] != null || stripe.samples.containsKey(packed);
|
throw new IllegalArgumentException("Terrain basis batch exceeds its sample limit");
|
||||||
}
|
}
|
||||||
}
|
HydrologyTerrainSample[] samples = new HydrologyTerrainSample[count];
|
||||||
|
Pending[] pending = new Pending[count];
|
||||||
HydrologyTerrainSample rememberWithoutSlope(long packed, HydrologyTerrainSample sample) {
|
long[] ownedCoordinates = new long[count];
|
||||||
Stripe stripe = stripe(withoutSlope, packed);
|
Pending[] owned = new Pending[count];
|
||||||
synchronized (stripe) {
|
int ownedCount = count;
|
||||||
if (stripe.samples.containsKey(packed)) {
|
Completion completion = new Completion();
|
||||||
return stripe.samples.getAndMoveToFirst(packed);
|
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);
|
int claimed = 0;
|
||||||
return sample;
|
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) {
|
void seedWithoutSlope(Long2ObjectMap<HydrologyTerrainSample> samples) {
|
||||||
@@ -64,15 +122,104 @@ final class HydrologyBasisCache {
|
|||||||
|
|
||||||
private HydrologyTerrainSample sample(int x, int z, HydrologyNaturalTerrainSampler sampler, boolean omitSlope) {
|
private HydrologyTerrainSample sample(int x, int z, HydrologyNaturalTerrainSampler sampler, boolean omitSlope) {
|
||||||
long packed = RiverFootprint.pack(x, z);
|
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) {
|
synchronized (stripe) {
|
||||||
HydrologyTerrainSample cached = stripe.samples.getAndMoveToFirst(packed);
|
HydrologyTerrainSample cached = stripe.samples.getAndMoveToFirst(packed);
|
||||||
if (cached != null || stripe.samples.containsKey(packed)) {
|
if (cached != null || stripe.samples.containsKey(packed)) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
HydrologyTerrainSample sampled = omitSlope ? sampler.sampleBasisWithoutSlope(x, z) : sampler.sampleBasis(x, z);
|
loading = stripe.pending.get(packed);
|
||||||
stripe.put(packed, sampled);
|
if (loading == null) {
|
||||||
return sampled;
|
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 static final class Stripe {
|
||||||
private final int maximumEntries;
|
private final int maximumEntries;
|
||||||
private final Long2ObjectLinkedOpenHashMap<HydrologyTerrainSample> samples;
|
private final Long2ObjectLinkedOpenHashMap<HydrologyTerrainSample> samples;
|
||||||
|
private final Long2ObjectOpenHashMap<Pending> pending = new Long2ObjectOpenHashMap<>();
|
||||||
|
|
||||||
private Stripe(int maximumEntries) {
|
private Stripe(int maximumEntries) {
|
||||||
this.maximumEntries = maximumEntries;
|
this.maximumEntries = maximumEntries;
|
||||||
@@ -105,4 +253,73 @@ final class HydrologyBasisCache {
|
|||||||
samples.putAndMoveToFirst(packed, value);
|
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);
|
HydrologyTerrainSample cached = terrainBases.get(key);
|
||||||
if (cached != null) {
|
if (cached != null) {
|
||||||
samples[index] = cached;
|
samples[index] = cached;
|
||||||
} else if (sharedBasis != null && sharedBasis.findWithoutSlope(key, samples, index)) {
|
|
||||||
continue;
|
|
||||||
} else if (naturalSampler == null) {
|
} else if (naturalSampler == null) {
|
||||||
samples[index] = sampleTerrainBasis(RiverFootprint.unpackX(key), RiverFootprint.unpackZ(key));
|
samples[index] = sampleTerrainBasis(RiverFootprint.unpackX(key), RiverFootprint.unpackZ(key));
|
||||||
} else {
|
} else {
|
||||||
@@ -956,10 +954,11 @@ final class HydrologyFootprintCompiler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (missingCount > 0) {
|
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++) {
|
for (int index = 0; index < missingCount; index++) {
|
||||||
samples[positions[index]] = sharedBasis == null ? loaded[index]
|
samples[positions[index]] = loaded[index];
|
||||||
: sharedBasis.rememberWithoutSlope(missing[index], loaded[index]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (int index = 0; index < count; index++) {
|
for (int index = 0; index < count; index++) {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
package art.arcane.iris.generation.hydrology;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -91,6 +93,7 @@ public final class HydrologyForkJoin {
|
|||||||
private final CountDownLatch complete = new CountDownLatch(1);
|
private final CountDownLatch complete = new CountDownLatch(1);
|
||||||
private T result;
|
private T result;
|
||||||
private Throwable failure;
|
private Throwable failure;
|
||||||
|
private boolean provisional;
|
||||||
|
|
||||||
Task(Callable<T> work) {
|
Task(Callable<T> work) {
|
||||||
this.work = Objects.requireNonNull(work);
|
this.work = Objects.requireNonNull(work);
|
||||||
@@ -136,6 +139,9 @@ public final class HydrologyForkJoin {
|
|||||||
if (failure != null) {
|
if (failure != null) {
|
||||||
throw unwrap(failure);
|
throw unwrap(failure);
|
||||||
}
|
}
|
||||||
|
if (provisional) {
|
||||||
|
ProvisionalSampling.mark();
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +162,10 @@ public final class HydrologyForkJoin {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
result = call(work);
|
result = ProvisionalSampling.memoizable(() -> call(work));
|
||||||
|
} catch (ProvisionalSampling.Unmemoizable value) {
|
||||||
|
result = value.value();
|
||||||
|
provisional = true;
|
||||||
} catch (RuntimeException | Error failed) {
|
} catch (RuntimeException | Error failed) {
|
||||||
failure = failed;
|
failure = failed;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
+12
@@ -1,6 +1,18 @@
|
|||||||
package art.arcane.iris.generation.hydrology.runtime;
|
package art.arcane.iris.generation.hydrology.runtime;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.hydrology.RiverFootprint;
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface IrisHydrologyNaturalHeightProvider {
|
public interface IrisHydrologyNaturalHeightProvider {
|
||||||
double sample(int blockX, int blockZ);
|
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.volmlib.nativelib.terrain.NativeGenerationScope;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyForkJoin;
|
import art.arcane.iris.generation.hydrology.HydrologyForkJoin;
|
||||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
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.Long2DoubleLinkedOpenHashMap;
|
||||||
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
||||||
|
|
||||||
@@ -98,15 +100,10 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
|||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NaturalClassification sampled = NaturalClassification.LAND;
|
long revision = ProvisionalSampling.revision();
|
||||||
if (oceanClassifier.isOcean(blockX, blockZ)) {
|
NaturalClassification sampled = classifyNatural(stripe, packed, blockX, blockZ);
|
||||||
double height = naturalHeight(stripe, packed, blockX, blockZ);
|
if (sampled == NaturalClassification.UNAVAILABLE || revision != ProvisionalSampling.revision()) {
|
||||||
if (!Double.isFinite(height)) {
|
return sampled;
|
||||||
return NaturalClassification.UNAVAILABLE;
|
|
||||||
}
|
|
||||||
if (physicalOcean(true, height, seaLevel)) {
|
|
||||||
sampled = NaturalClassification.OCEAN;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
synchronized (stripe) {
|
synchronized (stripe) {
|
||||||
TerrainBasis basis = stripe.bases.getAndMoveToLast(packed);
|
TerrainBasis basis = stripe.bases.getAndMoveToLast(packed);
|
||||||
@@ -123,6 +120,17 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
|||||||
return sampled;
|
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
|
@Override
|
||||||
public HydrologyTerrainSample sampleBasis(int blockX, int blockZ) {
|
public HydrologyTerrainSample sampleBasis(int blockX, int blockZ) {
|
||||||
// Point samples carry the forward slope; the aligned grid derives its own from neighbouring bases.
|
// 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()),
|
int workers = Math.min(Math.min(samplingOptions.maximumWorkers(), Runtime.getRuntime().availableProcessors()),
|
||||||
Math.ceilDiv(count, MINIMUM_PARALLEL_BASIS_SAMPLES));
|
Math.ceilDiv(count, MINIMUM_PARALLEL_BASIS_SAMPLES));
|
||||||
Supplier<NativeGenerationScope> scope = workers > 1 ? samplingOptions.batchScopes().capture() : null;
|
Supplier<NativeGenerationScope> scope = workers > 1 ? samplingOptions.batchScopes().capture() : null;
|
||||||
if (scope == null) {
|
|
||||||
return HydrologyNaturalTerrainSampler.super.sampleBasisWithoutSlopeBatch(coordinates, count);
|
|
||||||
}
|
|
||||||
HydrologyTerrainSample[] samples = new HydrologyTerrainSample[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);
|
int perWorker = Math.ceilDiv(count, workers);
|
||||||
ArrayList<Callable<Void>> tasks = new ArrayList<>(workers);
|
ArrayList<Callable<Void>> tasks = new ArrayList<>(workers);
|
||||||
for (int worker = 0; worker < workers; worker++) {
|
for (int worker = 0; worker < workers; worker++) {
|
||||||
@@ -158,12 +171,12 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
|||||||
}
|
}
|
||||||
tasks.add(() -> {
|
tasks.add(() -> {
|
||||||
try (NativeGenerationScope ignored = scope.get()) {
|
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++) {
|
for (int index = start; index < end; index++) {
|
||||||
if (Thread.currentThread().isInterrupted()) {
|
samples[index] = bases[index - start].terrain();
|
||||||
throw new CancellationException("Terrain basis sampling interrupted");
|
|
||||||
}
|
|
||||||
samples[index] = sampleBasisWithoutSlope(RiverFootprint.unpackX(coordinates[index]),
|
|
||||||
RiverFootprint.unpackZ(coordinates[index]));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -183,9 +196,12 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
|||||||
return cached.terrain().ocean() ? Double.NaN : cached.terrain().naturalHeight();
|
return cached.terrain().ocean() ? Double.NaN : cached.terrain().naturalHeight();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
long revision = ProvisionalSampling.revision();
|
||||||
double naturalHeight = naturalHeight(stripe, packed, blockX, blockZ);
|
double naturalHeight = naturalHeight(stripe, packed, blockX, blockZ);
|
||||||
|
boolean memoizable = revision == ProvisionalSampling.revision();
|
||||||
if (!Double.isFinite(naturalHeight)) {
|
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();
|
return basis.terrain().ocean() ? Double.NaN : basis.terrain().naturalHeight();
|
||||||
}
|
}
|
||||||
long roundedHeight = StrictMath.round(naturalHeight);
|
long roundedHeight = StrictMath.round(naturalHeight);
|
||||||
@@ -194,7 +210,8 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
|||||||
}
|
}
|
||||||
NaturalClassification classification = classifyNatural(blockX, blockZ);
|
NaturalClassification classification = classifyNatural(blockX, blockZ);
|
||||||
if (classification == NaturalClassification.UNAVAILABLE) {
|
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 basis.terrain().ocean() ? Double.NaN : basis.terrain().naturalHeight();
|
||||||
}
|
}
|
||||||
return classification == NaturalClassification.OCEAN ? Double.NaN : (int) roundedHeight;
|
return classification == NaturalClassification.OCEAN ? Double.NaN : (int) roundedHeight;
|
||||||
@@ -209,16 +226,28 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
|||||||
return cached;
|
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) {
|
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);
|
TerrainBasis sampled = basisProvider.sample(blockX, blockZ, naturalHeight);
|
||||||
if (sampled == null) {
|
if (sampled == null) {
|
||||||
throw new NullPointerException(
|
throw new NullPointerException(
|
||||||
"Hydrology terrain basis provider returned null at " + blockX + "," + blockZ
|
"Hydrology terrain basis provider returned null at " + blockX + "," + blockZ
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return sampled;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TerrainBasis publishBasis(CacheStripe stripe, long packed, TerrainBasis sampled) {
|
||||||
synchronized (stripe) {
|
synchronized (stripe) {
|
||||||
TerrainBasis existing = stripe.bases.getAndMoveToLast(packed);
|
TerrainBasis existing = stripe.bases.getAndMoveToLast(packed);
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
@@ -349,14 +378,108 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
|||||||
int minimumRow,
|
int minimumRow,
|
||||||
int maximumRow
|
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++) {
|
for (int gridZ = minimumRow; gridZ < maximumRow; gridZ++) {
|
||||||
int z = Math.toIntExact((long) minimumZ + (long) gridZ * spacing);
|
int z = Math.toIntExact((long) minimumZ + (long) gridZ * spacing);
|
||||||
int planeIndex = gridZ * planeWidth;
|
|
||||||
for (int gridX = 0; gridX < planeWidth; gridX++) {
|
for (int gridX = 0; gridX < planeWidth; gridX++) {
|
||||||
int x = Math.toIntExact((long) minimumX + (long) gridX * spacing);
|
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) {
|
private int gridWorkerCount(int planeWidth, int planeSize) {
|
||||||
@@ -384,8 +507,9 @@ final class IrisHydrologyRoutingTerrainSampler implements HydrologyNaturalTerrai
|
|||||||
return cachedBasis.naturalHeight();
|
return cachedBasis.naturalHeight();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
long revision = ProvisionalSampling.revision();
|
||||||
double sampled = heightProvider.sample(blockX, blockZ);
|
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
|
// 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.
|
// later sample of the same column gets a fresh chance instead of the cached failure.
|
||||||
return sampled;
|
return sampled;
|
||||||
|
|||||||
+1
-1
@@ -100,7 +100,7 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
|||||||
HydrologyGeometrySampler geometrySampler = geometrySampler(context, hydrology);
|
HydrologyGeometrySampler geometrySampler = geometrySampler(context, hydrology);
|
||||||
IrisHydrologyRoutingTerrainSampler.Sources terrainSources = new IrisHydrologyRoutingTerrainSampler.Sources(
|
IrisHydrologyRoutingTerrainSampler.Sources terrainSources = new IrisHydrologyRoutingTerrainSampler.Sources(
|
||||||
this::createTerrainBasis,
|
this::createTerrainBasis,
|
||||||
(int x, int z) -> context.naturalHeightProvider().sample(x, z),
|
context.naturalHeightProvider(),
|
||||||
(int x, int z) -> context.naturalOceanClassifier().isOcean(x, z),
|
(int x, int z) -> context.naturalOceanClassifier().isOcean(x, z),
|
||||||
settings.seaLevel()
|
settings.seaLevel()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ import lombok.Getter;
|
|||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.DoubleBinaryOperator;
|
||||||
|
|
||||||
@Accessors(chain = true)
|
@Accessors(chain = true)
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@@ -208,26 +211,106 @@ public class IrisGenerator extends IrisRegistrant {
|
|||||||
if (surfaceDetail == 1D) {
|
if (surfaceDetail == 1D) {
|
||||||
return sampleHeight(rx, rz, superSeed, data, engine);
|
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();
|
SurfaceCacheStripe[] stripes = surfaceCache(data, engine).stripes();
|
||||||
double lowerX = Math.floor(rx / SURFACE_GRID) * SURFACE_GRID;
|
double lowerX = Math.floor(rx / SURFACE_GRID) * SURFACE_GRID;
|
||||||
double lowerZ = Math.floor(rz / SURFACE_GRID) * SURFACE_GRID;
|
double lowerZ = Math.floor(rz / SURFACE_GRID) * SURFACE_GRID;
|
||||||
double dx = (rx - lowerX) / SURFACE_GRID;
|
double dx = (rx - lowerX) / SURFACE_GRID;
|
||||||
double dz = (rz - lowerZ) / 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) {
|
if (dx == 0D && dz == 0D) {
|
||||||
return northWest;
|
return northWest;
|
||||||
}
|
}
|
||||||
double north = dx == 0D ? northWest : IrisInterpolation.lerp(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;
|
double smooth = north;
|
||||||
if (dz != 0D) {
|
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,
|
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);
|
smooth = IrisInterpolation.lerp(north, south, dz);
|
||||||
}
|
}
|
||||||
return surfaceDetail == 0D ? smooth
|
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) {
|
private double sampleHeight(double rx, double rz, long superSeed, IrisData data, Engine engine) {
|
||||||
@@ -267,9 +350,9 @@ public class IrisGenerator extends IrisRegistrant {
|
|||||||
return v;
|
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) {
|
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 key = ((long) (int) x << 32) | ((int) z & 0xffffffffL);
|
||||||
long mixed = key ^ (key >>> 33);
|
long mixed = key ^ (key >>> 33);
|
||||||
@@ -281,7 +364,7 @@ public class IrisGenerator extends IrisRegistrant {
|
|||||||
if (cached != null) {
|
if (cached != null) {
|
||||||
return cached.height();
|
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));
|
stripe.put(slot, new SurfaceSample(key, superSeed, height));
|
||||||
return height;
|
return height;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import lombok.Setter;
|
|||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.function.DoubleBinaryOperator;
|
||||||
|
|
||||||
@Snippet("generator")
|
@Snippet("generator")
|
||||||
@Accessors(chain = true)
|
@Accessors(chain = true)
|
||||||
@@ -176,6 +177,67 @@ public class IrisNoiseGenerator {
|
|||||||
return sample(getGenerator(superSeed, data, engine), x, z);
|
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() {
|
public KList<IrisNoiseGenerator> getAllComposites() {
|
||||||
KList<IrisNoiseGenerator> g = new KList<>();
|
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.HydrologyColumnSnapshot;
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
|
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
|
||||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyNaturalSample;
|
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.IrisHydrologyRuntime;
|
||||||
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntimeContext;
|
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntimeContext;
|
||||||
import art.arcane.iris.generation.image.IrisImageMapApplication;
|
import art.arcane.iris.generation.image.IrisImageMapApplication;
|
||||||
@@ -454,7 +456,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
engine.getDimension(),
|
engine.getDimension(),
|
||||||
data,
|
data,
|
||||||
(x, z, naturalHeight) -> sampleHydrologyNatural(engine, x, z, naturalHeight),
|
(x, z, naturalHeight) -> sampleHydrologyNatural(engine, x, z, naturalHeight),
|
||||||
(x, z) -> naturalHeightStream.getDouble(x, z),
|
new HydrologyHeightProvider(),
|
||||||
(x, z) -> describeNaturalHeight(engine, x, z),
|
(x, z) -> describeNaturalHeight(engine, x, z),
|
||||||
this::sampleNaturalOcean,
|
this::sampleNaturalOcean,
|
||||||
footprint -> new MantleHydrologyCaveVoxelView(engine, this, footprint),
|
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);
|
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) {
|
private double sampleUnblendedNaturalTerrainHeight(double x, double z) {
|
||||||
double proceduralHeight = proceduralTerrainHeight.sample(x, z);
|
double proceduralHeight = proceduralTerrainHeight.sample(x, z);
|
||||||
return imageMapRuntime.sampleTerrainHeight(x, z, proceduralHeight);
|
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
|
@Override
|
||||||
public long getSize() {
|
public long getSize() {
|
||||||
return cache.getSize();
|
return cache.getSize();
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ public final class ProvisionalSampling {
|
|||||||
private ProvisionalSampling() {
|
private ProvisionalSampling() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static long revision() {
|
||||||
|
return TRACKERS.get().revision;
|
||||||
|
}
|
||||||
|
|
||||||
/** Records that the calling thread just answered from inputs that are not final. */
|
/** Records that the calling thread just answered from inputs that are not final. */
|
||||||
public static void mark() {
|
public static void mark() {
|
||||||
TRACKERS.get().revision++;
|
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);
|
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) {
|
public double height(int x, int z) {
|
||||||
if (!active()) {
|
if (!active()) {
|
||||||
return baseHeight(x, z);
|
return baseHeight(x, z);
|
||||||
|
|||||||
+382
-13
@@ -1,37 +1,385 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
package art.arcane.iris.generation.hydrology;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.ForkJoinPool;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
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.assertEquals;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertThrows;
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class HydrologyBasisCacheTest {
|
public class HydrologyBasisCacheTest {
|
||||||
@Test
|
@Test
|
||||||
public void batchLookupPreservesNullAndFirstPublishedSample() {
|
public void batchLookupPreservesNullDuplicatesAndEviction() {
|
||||||
|
CountingSampler sampler = new CountingSampler();
|
||||||
HydrologyBasisCache cache = new HydrologyBasisCache(1);
|
HydrologyBasisCache cache = new HydrologyBasisCache(1);
|
||||||
HydrologyTerrainSample[] samples = new HydrologyTerrainSample[1];
|
long key = RiverFootprint.pack(Integer.MIN_VALUE, Integer.MAX_VALUE);
|
||||||
long key = RiverFootprint.pack(-17, Integer.MAX_VALUE);
|
assertArrayEquals(new HydrologyTerrainSample[2],
|
||||||
assertTrue(!cache.findWithoutSlope(key, samples, 0));
|
cache.sampleWithoutSlopeBatch(new long[]{key, key}, 2, sampler));
|
||||||
assertNull(cache.rememberWithoutSlope(key, null));
|
assertNull(cache.sampleWithoutSlope(Integer.MIN_VALUE, Integer.MAX_VALUE, sampler));
|
||||||
assertTrue(cache.findWithoutSlope(key, samples, 0));
|
assertEquals(1, sampler.slopeFreeCalls.get());
|
||||||
assertNull(samples[0]);
|
long later = RiverFootprint.pack(73, -17);
|
||||||
HydrologyTerrainSample later = HydrologyTerrainSample.openLand(73, 0D, "land");
|
assertEquals(87, cache.sampleWithoutSlopeBatch(new long[]{later}, 1, sampler)[0].naturalHeight());
|
||||||
assertNull(cache.rememberWithoutSlope(key, later));
|
assertNull(cache.sampleWithoutSlopeBatch(new long[]{key}, 1, sampler)[0]);
|
||||||
cache.rememberWithoutSlope(key + 1, later);
|
assertEquals(3, sampler.slopeFreeCalls.get());
|
||||||
assertTrue(!cache.findWithoutSlope(key, samples, 0));
|
}
|
||||||
assertEquals(later, cache.rememberWithoutSlope(key + 1, null));
|
|
||||||
|
@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
|
@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 fullCalls = new AtomicInteger();
|
||||||
private final AtomicInteger slopeFreeCalls = new AtomicInteger();
|
private final AtomicInteger slopeFreeCalls = new AtomicInteger();
|
||||||
private final AtomicInteger failures = new AtomicInteger();
|
private final AtomicInteger failures = new AtomicInteger();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package art.arcane.iris.generation.hydrology;
|
package art.arcane.iris.generation.hydrology;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -23,6 +24,44 @@ import static org.junit.Assert.assertThrows;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class HydrologyForkJoinTest {
|
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
|
@Test
|
||||||
public void blockedGridBatchNeverHelpsUnrelatedOwnerWork() throws Exception {
|
public void blockedGridBatchNeverHelpsUnrelatedOwnerWork() throws Exception {
|
||||||
ForkJoinPool pool = new ForkJoinPool(2, ForkJoinPool.defaultForkJoinWorkerThreadFactory,
|
ForkJoinPool pool = new ForkJoinPool(2, ForkJoinPool.defaultForkJoinWorkerThreadFactory,
|
||||||
|
|||||||
+16
-5
@@ -279,11 +279,22 @@ public class PreparedHydrologyTileStoreTest {
|
|||||||
HydrologyTile original = tile();
|
HydrologyTile original = tile();
|
||||||
PreparedHydrologyTileStore store = store();
|
PreparedHydrologyTileStore store = store();
|
||||||
store.save(original);
|
store.save(original);
|
||||||
HydrologyTile oversized = new HydrologyTile(original.key(), original.worldSeed(),
|
long allocationLimit = Math.max(16L * 1024L * 1024L,
|
||||||
original.settingsFingerprint(), original.tileSize(), original.nodes(), original.edges(),
|
Math.min(512L * 1024L * 1024L, Runtime.getRuntime().maxMemory() / 8L));
|
||||||
original.outlets(), original.courses(), original.regionalCourseIds(), original.cavePlans(),
|
int pointCount = 4096;
|
||||||
Collections.nCopies(600_000, original.localDiagnosticCandidates().getFirst()), original.footprint());
|
int courseCount = Math.toIntExact(allocationLimit / (256L * pointCount) + 1L);
|
||||||
assertThrows(IOException.class, () -> store.save(oversized));
|
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());
|
assertEquals(original, store.load(original.key()).orElseThrow());
|
||||||
try (Stream<Path> files = Files.list(store.file(original.key()).getParent())) {
|
try (Stream<Path> files = Files.list(store.file(original.key()).getParent())) {
|
||||||
assertEquals(List.of(store.file(original.key())), files.toList());
|
assertEquals(List.of(store.file(original.key())), files.toList());
|
||||||
|
|||||||
+127
@@ -1,6 +1,7 @@
|
|||||||
package art.arcane.iris.generation.hydrology.runtime;
|
package art.arcane.iris.generation.hydrology.runtime;
|
||||||
|
|
||||||
import art.arcane.iris.generation.hydrology.HydrologyRoutingTerrainSampler;
|
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.HydrologyTerrainSample;
|
||||||
import art.arcane.iris.generation.hydrology.RiverFootprint;
|
import art.arcane.iris.generation.hydrology.RiverFootprint;
|
||||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||||
@@ -40,6 +41,132 @@ import static org.mockito.Mockito.doReturn;
|
|||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
public class IrisHydrologyRoutingTerrainSamplerTest {
|
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
|
@Test
|
||||||
public void boundedBasisBatchUsesScopedWorkersAndDrainsNestedPool() throws Exception {
|
public void boundedBasisBatchUsesScopedWorkersAndDrainsNestedPool() throws Exception {
|
||||||
ThreadLocal<Integer> scope = new ThreadLocal<>();
|
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