mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 21:40:05 +00:00
Merged height sampling changes
- Height prefill reuses chunk caches and mapped biomes skip unused sampling - Shared terrain corner storage allocates only when sampled
This commit is contained in:
commit
ac026343c1
4 files changed
+431
-21
No files matched your search
@@ -125,7 +125,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
private static final int GRID_BOUNDS_CACHE_SIZE = 8192;
|
private static final int GRID_BOUNDS_CACHE_SIZE = 8192;
|
||||||
private static final int STUDIO_NOISE_CACHE_SIZE = 32_768;
|
private static final int STUDIO_NOISE_CACHE_SIZE = 32_768;
|
||||||
private static final int TERRAIN_COLUMN_CACHE_SIZE = 524_288;
|
private static final int TERRAIN_COLUMN_CACHE_SIZE = 524_288;
|
||||||
/** One million corners: about 16 MB, roughly a 4000 by 4000 block area at the 4-block grid. */
|
/** One million corners: about 20 MB, roughly a 4000 by 4000 block area at the 4-block grid. */
|
||||||
private static final int SHARED_CORNER_BOUNDS_CAPACITY = 1 << 20;
|
private static final int SHARED_CORNER_BOUNDS_CAPACITY = 1 << 20;
|
||||||
/** The slope streams measure the rise across a run of this many blocks, so gradient is slope over run. */
|
/** The slope streams measure the rise across a run of this many blocks, so gradient is slope over run. */
|
||||||
private static final double SLOPE_RUN = 3D;
|
private static final double SLOPE_RUN = 3D;
|
||||||
@@ -148,7 +148,7 @@ public class IrisComplex implements DataProvider {
|
|||||||
private final transient ThreadLocal<GridBoundsCache> gridBoundsCache = ThreadLocal.withInitial(GridBoundsCache::new);
|
private final transient ThreadLocal<GridBoundsCache> gridBoundsCache = ThreadLocal.withInitial(GridBoundsCache::new);
|
||||||
@Getter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE)
|
||||||
private final int biomeBoundsSamplingStep;
|
private final int biomeBoundsSamplingStep;
|
||||||
private transient volatile SharedCornerBounds sharedCornerBounds = new SharedCornerBounds(SHARED_CORNER_BOUNDS_CAPACITY);
|
private transient volatile SharedCornerBounds sharedCornerBounds;
|
||||||
@Getter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE)
|
||||||
private final transient IrisInterpolator[] frozenInterpolators;
|
private final transient IrisInterpolator[] frozenInterpolators;
|
||||||
@Getter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE)
|
||||||
@@ -407,11 +407,13 @@ public class IrisComplex implements DataProvider {
|
|||||||
GenerationStreams.cache2D(bridgeStream.convertAware2D((t, x, z) -> inferredStreams.get(t).get(x, z))
|
GenerationStreams.cache2D(bridgeStream.convertAware2D((t, x, z) -> inferredStreams.get(t).get(x, z))
|
||||||
.convertAware2D(this::implode), "baseBiomeStream", engine, cacheSize);
|
.convertAware2D(this::implode), "baseBiomeStream", engine, cacheSize);
|
||||||
baseBiomeStream = focusedBiomes != null || !imageMapRuntime.has(IrisImageMapApplication.BIOME)
|
baseBiomeStream = focusedBiomes != null || !imageMapRuntime.has(IrisImageMapApplication.BIOME)
|
||||||
? proceduralBaseBiomeStream : GenerationStreams.cache2D(proceduralBaseBiomeStream
|
? proceduralBaseBiomeStream : GenerationStreams.cache2D(ProceduralStream.of((x, z) -> {
|
||||||
.convertAware2D((biome, x, z) -> {
|
|
||||||
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
||||||
return mapped == null ? biome : mapped;
|
return mapped == null ? proceduralBaseBiomeStream.get(x, z) : mapped;
|
||||||
}), "imageMappedBaseBiomeStream", engine, cacheSize);
|
}, (x, y, z) -> {
|
||||||
|
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
||||||
|
return mapped == null ? proceduralBaseBiomeStream.get(x, y, z) : mapped;
|
||||||
|
}, proceduralBaseBiomeStream), "imageMappedBaseBiomeStream", engine, cacheSize);
|
||||||
proceduralTerrainHeight = new ProceduralTerrainHeightSampler(
|
proceduralTerrainHeight = new ProceduralTerrainHeightSampler(
|
||||||
(x, z) -> getHeight(engine, x, z, engine.getSeedManager().getHeight()),
|
(x, z) -> getHeight(engine, x, z, engine.getSeedManager().getHeight()),
|
||||||
engine.getDimension().getTerrainSamplingStep());
|
engine.getDimension().getTerrainSamplingStep());
|
||||||
@@ -441,14 +443,9 @@ public class IrisComplex implements DataProvider {
|
|||||||
: GenerationStreams.cache2DDouble(ProceduralStream.ofDouble((x, z) -> transitionDisplacement.height(
|
: GenerationStreams.cache2DDouble(ProceduralStream.ofDouble((x, z) -> transitionDisplacement.height(
|
||||||
blockCoordinate(x), blockCoordinate(z), unblendedNaturalHeightStream.getDouble(x, z))),
|
blockCoordinate(x), blockCoordinate(z), unblendedNaturalHeightStream.getDouble(x, z))),
|
||||||
"naturalHeightStream", engine, cacheSize);
|
"naturalHeightStream", engine, cacheSize);
|
||||||
naturalTrueBiomeStream = focusedBiomes != null ? GenerationStreams.cache2D(focusedBiomes, "naturalTrueBiomeStream-focus", engine, cacheSize) : GenerationStreams.cache2D(naturalHeightStream
|
naturalTrueBiomeStream = focusedBiomes != null ? GenerationStreams.cache2D(focusedBiomes, "naturalTrueBiomeStream-focus", engine, cacheSize)
|
||||||
.convertAware2D((h, x, z) -> {
|
: GenerationStreams.cache2D(ProceduralStream.of(this::sampleNaturalTrueBiome, this::sampleNaturalTrueBiome,
|
||||||
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
Interpolated.of(value -> 0D, value -> null)), "naturalTrueBiomeStream", engine, cacheSize);
|
||||||
return mapped == null
|
|
||||||
? fixBiomeType(h, baseBiomeStream.get(x, z), regionStream.get(x, z), x, z,
|
|
||||||
transitionFluidHeight(x, z, fluidHeight))
|
|
||||||
: mapped;
|
|
||||||
}), "naturalTrueBiomeStream", engine, cacheSize);
|
|
||||||
IrisHydrology configuredHydrology = engine.getDimension().getHydrology();
|
IrisHydrology configuredHydrology = engine.getDimension().getHydrology();
|
||||||
if (hydrologyActive(configuredHydrology)) {
|
if (hydrologyActive(configuredHydrology)) {
|
||||||
hydrologyRuntime = new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext(
|
hydrologyRuntime = new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext(
|
||||||
@@ -477,10 +474,8 @@ public class IrisComplex implements DataProvider {
|
|||||||
Math.max(4096, cacheSize), data, banks3D));
|
Math.max(4096, cacheSize), data, banks3D));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cachedHeightStream = new CachedDoubleStream2D("heightStream", engine,
|
cachedHeightStream = new TerrainHeightStream(engine, cacheSize);
|
||||||
ProceduralStream.ofDouble(this::resolveHydrologyTerrainHeight), cacheSize);
|
heightStream = cachedHeightStream;
|
||||||
heightStream = ProceduralStream.ofDouble((x, z) -> terrainEngine.getPlatformHooks().isMainThread()
|
|
||||||
? nonblockingTerrainHeight(x, z) : cachedHeightStream.getDouble(x, z));
|
|
||||||
roundedHeighteightStream = GenerationStreams.contextInjecting(heightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
roundedHeighteightStream = GenerationStreams.contextInjecting(heightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||||
.round();
|
.round();
|
||||||
slopeStream = GenerationStreams.contextInjecting(heightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
slopeStream = GenerationStreams.contextInjecting(heightStream, engine, (c, x, z) -> c.getHeight().getDouble(x, z))
|
||||||
@@ -1038,6 +1033,22 @@ public class IrisComplex implements DataProvider {
|
|||||||
return sample.terrainHeight();
|
return sample.terrainHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IrisBiome sampleNaturalTrueBiome(double x, double z) {
|
||||||
|
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
||||||
|
return mapped == null
|
||||||
|
? fixBiomeType(naturalHeightStream.getDouble(x, z), baseBiomeStream.get(x, z), regionStream.get(x, z), x, z,
|
||||||
|
transitionFluidHeight(x, z, fluidHeight))
|
||||||
|
: mapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IrisBiome sampleNaturalTrueBiome(double x, double y, double z) {
|
||||||
|
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
||||||
|
return mapped == null
|
||||||
|
? fixBiomeType(naturalHeightStream.getDouble(x, y, z), baseBiomeStream.get(x, z), regionStream.get(x, z), x, z,
|
||||||
|
transitionFluidHeight(x, z, fluidHeight))
|
||||||
|
: mapped;
|
||||||
|
}
|
||||||
|
|
||||||
private double nonblockingTerrainHeight(double x, double z) {
|
private double nonblockingTerrainHeight(double x, double z) {
|
||||||
HydrologyColumnSnapshot snapshot = sampleHydrologySnapshot(blockCoordinate(x), blockCoordinate(z));
|
HydrologyColumnSnapshot snapshot = sampleHydrologySnapshot(blockCoordinate(x), blockCoordinate(z));
|
||||||
if (!snapshot.available() || snapshot.column() == null) {
|
if (!snapshot.available() || snapshot.column() == null) {
|
||||||
@@ -1693,12 +1704,17 @@ public class IrisComplex implements DataProvider {
|
|||||||
return packed;
|
return packed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The field initializer covers every real complex; an instance built without it gets one here. */
|
/** Coarse bounds allocate their shared table on the first corner sample. */
|
||||||
private SharedCornerBounds sharedCornerBounds() {
|
private SharedCornerBounds sharedCornerBounds() {
|
||||||
SharedCornerBounds shared = sharedCornerBounds;
|
SharedCornerBounds shared = sharedCornerBounds;
|
||||||
if (shared == null) {
|
if (shared == null) {
|
||||||
shared = new SharedCornerBounds(SHARED_CORNER_BOUNDS_CAPACITY);
|
synchronized (this) {
|
||||||
sharedCornerBounds = shared;
|
shared = sharedCornerBounds;
|
||||||
|
if (shared == null) {
|
||||||
|
shared = new SharedCornerBounds(SHARED_CORNER_BOUNDS_CAPACITY);
|
||||||
|
sharedCornerBounds = shared;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return shared;
|
return shared;
|
||||||
}
|
}
|
||||||
@@ -2214,6 +2230,62 @@ public class IrisComplex implements DataProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class TerrainHeightStream extends CachedDoubleStream2D {
|
||||||
|
private TerrainHeightStream(Engine engine, int cacheSize) {
|
||||||
|
super("heightStream", engine, ProceduralStream.ofDouble(IrisComplex.this::resolveHydrologyTerrainHeight), cacheSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double getDouble(double x, double z) {
|
||||||
|
return terrainEngine.getPlatformHooks().isMainThread()
|
||||||
|
? nonblockingTerrainHeight(x, z) : super.getDouble(x, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Double get(double x, double y, double z) {
|
||||||
|
return getDouble(x, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double getDouble(double x, double y, double z) {
|
||||||
|
return getDouble(x, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fillChunkDoubles(int worldX, int worldZ, double[] target) {
|
||||||
|
boolean mainThread = terrainEngine.getPlatformHooks().isMainThread();
|
||||||
|
if (!mainThread && (worldX & 15) == 0 && (worldZ & 15) == 0) {
|
||||||
|
super.fillChunkDoubles(worldX, worldZ, target);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (target == null || target.length != 256) {
|
||||||
|
throw new IllegalArgumentException("Expected a 16x16 target array.");
|
||||||
|
}
|
||||||
|
for (int index = 0; index < 256; index++) {
|
||||||
|
int x = worldX + (index & 15);
|
||||||
|
int z = worldZ + (index >> 4);
|
||||||
|
target[index] = mainThread ? nonblockingTerrainHeight(x, z) : super.getDouble(x, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void fillChunkRaw(int worldX, int worldZ, Object[] target) {
|
||||||
|
boolean mainThread = terrainEngine.getPlatformHooks().isMainThread();
|
||||||
|
if (!mainThread && (worldX & 15) == 0 && (worldZ & 15) == 0) {
|
||||||
|
super.fillChunkRaw(worldX, worldZ, target);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (target == null || target.length != 256) {
|
||||||
|
throw new IllegalArgumentException("Expected a 16x16 target array.");
|
||||||
|
}
|
||||||
|
for (int index = 0; index < 256; index++) {
|
||||||
|
int x = worldX + (index & 15);
|
||||||
|
int z = worldZ + (index >> 4);
|
||||||
|
target[index] = mainThread ? nonblockingTerrainHeight(x, z) : super.getDouble(x, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static final class ChildSelectionPlan {
|
static final class ChildSelectionPlan {
|
||||||
private final IrisRaritySelection<IrisBiome> selection;
|
private final IrisRaritySelection<IrisBiome> selection;
|
||||||
|
|
||||||
|
|||||||
@@ -84,12 +84,14 @@ public class IrisComplexBiomeBuffetTest {
|
|||||||
IrisComplex complex = new IrisComplex(engine);
|
IrisComplex complex = new IrisComplex(engine);
|
||||||
doReturn(complex).when(engine).getComplex();
|
doReturn(complex).when(engine).getComplex();
|
||||||
try {
|
try {
|
||||||
|
assertNull(complex.getSharedCornerBounds());
|
||||||
assertEquals("alpha", complex.getTrueBiomeStream().get(15, 0).getLoadKey());
|
assertEquals("alpha", complex.getTrueBiomeStream().get(15, 0).getLoadKey());
|
||||||
assertEquals("beta", complex.getTrueBiomeStream().get(16, 0).getLoadKey());
|
assertEquals("beta", complex.getTrueBiomeStream().get(16, 0).getLoadKey());
|
||||||
assertEquals("gamma", complex.getTrueBiomeStream().get(0, 16).getLoadKey());
|
assertEquals("gamma", complex.getTrueBiomeStream().get(0, 16).getLoadKey());
|
||||||
assertEquals(137D, complex.getBaseTerrainHeightStream().getDouble(15, 0), 0D);
|
assertEquals(137D, complex.getBaseTerrainHeightStream().getDouble(15, 0), 0D);
|
||||||
assertEquals(147D, complex.getBaseTerrainHeightStream().getDouble(16, 0), 0D);
|
assertEquals(147D, complex.getBaseTerrainHeightStream().getDouble(16, 0), 0D);
|
||||||
assertEquals(157D, complex.getBaseTerrainHeightStream().getDouble(0, 16), 0D);
|
assertEquals(157D, complex.getBaseTerrainHeightStream().getDouble(0, 16), 0D);
|
||||||
|
assertNull(complex.getSharedCornerBounds());
|
||||||
assertEquals("owned", complex.getRegionStream().get(16, 0).getLoadKey());
|
assertEquals("owned", complex.getRegionStream().get(16, 0).getLoadKey());
|
||||||
assertEquals("iris-focus/", complex.getRegionStream().get(0, 16).getLoadKey().substring(0, 11));
|
assertEquals("iris-focus/", complex.getRegionStream().get(0, 16).getLoadKey().substring(0, 11));
|
||||||
assertEquals("alpha", dimension.getFocus());
|
assertEquals("alpha", dimension.getFocus());
|
||||||
|
|||||||
+32
@@ -13,20 +13,52 @@ import org.junit.runners.Parameterized.Parameters;
|
|||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.junit.Assert.assertNotEquals;
|
import static org.junit.Assert.assertNotEquals;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
import static org.mockito.Answers.CALLS_REAL_METHODS;
|
import static org.mockito.Answers.CALLS_REAL_METHODS;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
@RunWith(Enclosed.class)
|
@RunWith(Enclosed.class)
|
||||||
public class IrisComplexGridBoundsCacheTest {
|
public class IrisComplexGridBoundsCacheTest {
|
||||||
public static class CornerCache {
|
public static class CornerCache {
|
||||||
|
@Test(timeout = 10_000L)
|
||||||
|
public void concurrentFirstSamplesShareOneCornerTable() throws Exception {
|
||||||
|
IrisComplex complex = createComplex();
|
||||||
|
Method sharedCache = IrisComplex.class.getDeclaredMethod("sharedCornerBounds");
|
||||||
|
sharedCache.setAccessible(true);
|
||||||
|
CountDownLatch entered = new CountDownLatch(4);
|
||||||
|
CountDownLatch release = new CountDownLatch(1);
|
||||||
|
try (ExecutorService readers = Executors.newFixedThreadPool(4)) {
|
||||||
|
List<Future<SharedCornerBounds>> results = new ArrayList<>();
|
||||||
|
for (int index = 0; index < 4; index++) {
|
||||||
|
results.add(readers.submit(() -> {
|
||||||
|
entered.countDown();
|
||||||
|
assertTrue(release.await(5L, TimeUnit.SECONDS));
|
||||||
|
return (SharedCornerBounds) sharedCache.invoke(complex);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
assertTrue(entered.await(5L, TimeUnit.SECONDS));
|
||||||
|
release.countDown();
|
||||||
|
SharedCornerBounds expected = results.getFirst().get(5L, TimeUnit.SECONDS);
|
||||||
|
for (Future<SharedCornerBounds> result : results) {
|
||||||
|
assertSame(expected, result.get(5L, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void gridBoundsCacheIsIsolatedPerComplex() throws Exception {
|
public void gridBoundsCacheIsIsolatedPerComplex() throws Exception {
|
||||||
IrisComplex first = createComplex();
|
IrisComplex first = createComplex();
|
||||||
|
|||||||
+304
@@ -0,0 +1,304 @@
|
|||||||
|
package art.arcane.iris.generation.runtime;
|
||||||
|
|
||||||
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
|
import art.arcane.iris.generation.block.B;
|
||||||
|
import art.arcane.iris.generation.context.ChunkedDoubleDataCache;
|
||||||
|
import art.arcane.iris.generation.hydrology.HydrologyColumnSample;
|
||||||
|
import art.arcane.iris.generation.hydrology.HydrologyColumnSnapshot;
|
||||||
|
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
|
||||||
|
import art.arcane.iris.generation.image.IrisImageMapApplication;
|
||||||
|
import art.arcane.iris.generation.image.IrisImageMapRuntime;
|
||||||
|
import art.arcane.iris.generation.stream.ChunkFillableDoubleStream2D;
|
||||||
|
import art.arcane.iris.generation.stream.ChunkFillableStream2D;
|
||||||
|
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
|
import art.arcane.iris.pack.loading.IrisData;
|
||||||
|
import art.arcane.iris.spi.IrisPlatform;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
|
import art.arcane.iris.spi.IrisServices;
|
||||||
|
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||||
|
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
|
||||||
|
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.mockito.Answers.CALLS_REAL_METHODS;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.clearInvocations;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class IrisComplexHeightStreamTest {
|
||||||
|
@ClassRule public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||||
|
@Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
private IrisSettings previousSettings;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void prepareRuntime() {
|
||||||
|
previousSettings = IrisSettings.settings;
|
||||||
|
IrisSettings.settings = new IrisSettings();
|
||||||
|
IrisServices.register(PreservationRegistry.class, mock(PreservationRegistry.class));
|
||||||
|
IrisPlatform platform = mock(IrisPlatform.class);
|
||||||
|
when(platform.platformName()).thenReturn("bukkit");
|
||||||
|
IrisPlatforms.bind(platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void releaseRuntime() {
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
IrisServices.remove(PreservationRegistry.class);
|
||||||
|
IrisSettings.settings = previousSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sharedCornersRemainUnallocatedUntilTerrainSamplesThem() throws Exception {
|
||||||
|
try (Fixture fixture = new Fixture(pack())) {
|
||||||
|
assertNull(fixture.complex.getSharedCornerBounds());
|
||||||
|
fixture.complex.getBaseTerrainHeightStream().getDouble(0, 0);
|
||||||
|
assertNotNull(fixture.complex.getSharedCornerBounds());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mappedBiomesSkipProceduralSelectionAndNaturalHeight() throws Exception {
|
||||||
|
try (Fixture fixture = new Fixture(pack())) {
|
||||||
|
IrisBiome mapped = new IrisBiome();
|
||||||
|
when(fixture.images.sampleBiome(anyDouble(), anyDouble())).thenReturn(mapped);
|
||||||
|
MeteredCache bridge = (MeteredCache) fixture.complex.getBridgeStream();
|
||||||
|
MeteredCache regions = (MeteredCache) fixture.complex.getRegionStream();
|
||||||
|
MeteredCache heights = (MeteredCache) fixture.complex.getNaturalHeightStream();
|
||||||
|
assertSame(mapped, fixture.complex.getBaseBiomeStream().get(-16, 32));
|
||||||
|
assertSame(mapped, fixture.complex.getBaseBiomeStream().get(-16.75, 10, 32.5));
|
||||||
|
assertSame(mapped, fixture.complex.getNaturalTrueBiomeStream().get(-16, 32));
|
||||||
|
assertSame(mapped, fixture.complex.getNaturalTrueBiomeStream().get(-16.75, 10, 32.5));
|
||||||
|
assertEquals(0L, bridge.getSize());
|
||||||
|
assertEquals(0L, regions.getSize());
|
||||||
|
assertEquals(0L, heights.getSize());
|
||||||
|
assertNull(fixture.complex.getSharedCornerBounds());
|
||||||
|
|
||||||
|
when(fixture.images.sampleBiome(anyDouble(), anyDouble())).thenReturn(null);
|
||||||
|
assertEquals("plain", fixture.complex.getBaseBiomeStream().get(16, 32).getLoadKey());
|
||||||
|
assertEquals("plain", fixture.complex.getNaturalTrueBiomeStream().get(16, 32).getLoadKey());
|
||||||
|
assertNotNull(fixture.complex.getSharedCornerBounds());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void workerPrefillUsesOneThreadCheckAndRetainsRoundedValues() throws Exception {
|
||||||
|
try (Fixture fixture = new Fixture(pack())) {
|
||||||
|
AtomicInteger samples = fixture.installNaturalHeight();
|
||||||
|
ChunkedDoubleDataCache cache = new ChunkedDoubleDataCache(fixture.complex.getRawHeightStream(), -16, 32);
|
||||||
|
int[] rounded = new int[256];
|
||||||
|
clearInvocations(fixture.hooks);
|
||||||
|
cache.fillRounded(rounded);
|
||||||
|
verify(fixture.hooks, times(1)).isMainThread();
|
||||||
|
assertEquals(256, samples.get());
|
||||||
|
for (int index = 0; index < 256; index++) {
|
||||||
|
int x = index & 15;
|
||||||
|
int z = index >> 4;
|
||||||
|
double expected = height(-16 + x, 32 + z);
|
||||||
|
assertBits(expected, cache.getDouble(x, z));
|
||||||
|
assertEquals((int) Math.round(expected), rounded[index]);
|
||||||
|
}
|
||||||
|
clearInvocations(fixture.hooks);
|
||||||
|
cache.fill();
|
||||||
|
verify(fixture.hooks, times(1)).isMainThread();
|
||||||
|
assertEquals(256, samples.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mainThreadPrefillNeverWaitsOrConsumesCachedHydrology() throws Exception {
|
||||||
|
try (Fixture fixture = new Fixture(pack())) {
|
||||||
|
fixture.installNaturalHeight();
|
||||||
|
IrisHydrologyRuntime hydrology = mock(IrisHydrologyRuntime.class);
|
||||||
|
HydrologyColumnSample planned = mock(HydrologyColumnSample.class);
|
||||||
|
when(planned.terrainHeight()).thenReturn(100);
|
||||||
|
when(hydrology.sample(anyDouble(), anyDouble())).thenAnswer(invocation -> {
|
||||||
|
if (fixture.mainThread.get()) {
|
||||||
|
throw new AssertionError("Main-thread sampling entered blocking hydrology");
|
||||||
|
}
|
||||||
|
return Optional.of(planned);
|
||||||
|
});
|
||||||
|
when(hydrology.sampleSnapshot(anyInt(), anyInt())).thenAnswer(invocation -> {
|
||||||
|
ProvisionalSampling.mark();
|
||||||
|
return HydrologyColumnSnapshot.unavailable();
|
||||||
|
});
|
||||||
|
fixture.complex.setHydrologyRuntime(hydrology);
|
||||||
|
ProceduralStream<Double> stream = fixture.complex.getRawHeightStream();
|
||||||
|
ChunkFillableDoubleStream2D bulk = (ChunkFillableDoubleStream2D) stream;
|
||||||
|
double[] values = new double[256];
|
||||||
|
fixture.mainThread.set(true);
|
||||||
|
assertThrows(ProvisionalSampling.Unmemoizable.class, () -> ProvisionalSampling.memoizable(() -> {
|
||||||
|
bulk.fillChunkDoubles(-16, 32, values);
|
||||||
|
return values[0];
|
||||||
|
}));
|
||||||
|
assertBits(height(-16, 32), values[0]);
|
||||||
|
fixture.mainThread.set(false);
|
||||||
|
bulk.fillChunkDoubles(-16, 32, values);
|
||||||
|
assertBits(100D, values[0]);
|
||||||
|
assertBits(100D, values[255]);
|
||||||
|
verify(hydrology, times(256)).sample(anyDouble(), anyDouble());
|
||||||
|
|
||||||
|
fixture.mainThread.set(true);
|
||||||
|
bulk.fillChunkDoubles(-16, 32, values);
|
||||||
|
assertBits(height(-16, 32), values[0]);
|
||||||
|
assertBits(height(-16.75, 32.5), stream.getDouble(-16.75, 10, 32.5));
|
||||||
|
assertBits(height(-16.75, 32.5), stream.get(-16.75, 10, 32.5));
|
||||||
|
Object[] boxed = new Object[256];
|
||||||
|
((ChunkFillableStream2D) stream).fillChunkRaw(-16, 32, boxed);
|
||||||
|
assertBits(height(-1, 47), (Double) boxed[255]);
|
||||||
|
verify(hydrology, times(256)).sample(anyDouble(), anyDouble());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unalignedPrefillAndThreeDimensionalReadsRetainColumnCoordinates() throws Exception {
|
||||||
|
try (Fixture fixture = new Fixture(pack())) {
|
||||||
|
fixture.installNaturalHeight();
|
||||||
|
ProceduralStream<Double> stream = fixture.complex.getRawHeightStream();
|
||||||
|
double[] values = new double[256];
|
||||||
|
((ChunkFillableDoubleStream2D) stream).fillChunkDoubles(-17, 31, values);
|
||||||
|
for (int index = 0; index < 256; index++) {
|
||||||
|
assertBits(height(-17 + (index & 15), 31 + (index >> 4)), values[index]);
|
||||||
|
}
|
||||||
|
assertBits(height(-16, 32), stream.getDouble(-16.75, 10, 32.5));
|
||||||
|
assertBits(height(-16, 32), stream.get(-16.75, 10, 32.5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void workerPrefillDoesNotMemoizeProvisionalNaturalValues() throws Exception {
|
||||||
|
try (Fixture fixture = new Fixture(pack())) {
|
||||||
|
AtomicBoolean ready = new AtomicBoolean();
|
||||||
|
AtomicInteger samples = new AtomicInteger();
|
||||||
|
fixture.complex.setNaturalHeightStream(ProceduralStream.ofDouble((x, z) -> {
|
||||||
|
samples.incrementAndGet();
|
||||||
|
if (!ready.get()) {
|
||||||
|
ProvisionalSampling.mark();
|
||||||
|
return -1D;
|
||||||
|
}
|
||||||
|
return height(x, z);
|
||||||
|
}));
|
||||||
|
ChunkFillableDoubleStream2D stream = (ChunkFillableDoubleStream2D) fixture.complex.getRawHeightStream();
|
||||||
|
double[] values = new double[256];
|
||||||
|
stream.fillChunkDoubles(-16, 32, values);
|
||||||
|
for (double value : values) {
|
||||||
|
assertBits(-1D, value);
|
||||||
|
}
|
||||||
|
ready.set(true);
|
||||||
|
int before = samples.get();
|
||||||
|
stream.fillChunkDoubles(-16, 32, values);
|
||||||
|
assertEquals(before + 256, samples.get());
|
||||||
|
for (int index = 0; index < 256; index++) {
|
||||||
|
assertBits(height(-16 + (index & 15), 32 + (index >> 4)), values[index]);
|
||||||
|
}
|
||||||
|
stream.fillChunkDoubles(-16, 32, values);
|
||||||
|
assertEquals(before + 256, samples.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path pack() throws Exception {
|
||||||
|
Path pack = temporaryFolder.newFolder().toPath();
|
||||||
|
Files.createDirectories(pack.resolve("dimensions"));
|
||||||
|
Files.createDirectories(pack.resolve("regions"));
|
||||||
|
Files.createDirectories(pack.resolve("biomes"));
|
||||||
|
Files.createDirectories(pack.resolve("generators"));
|
||||||
|
Files.writeString(pack.resolve("dimensions/main.json"), """
|
||||||
|
{"regions":["plain"],"landChance":1,
|
||||||
|
"carvingEnabled":false,"hydrology":{"rivers":{"enabled":false}}}
|
||||||
|
""");
|
||||||
|
Files.writeString(pack.resolve("regions/plain.json"), "{\"landBiomes\":[\"plain\"]}");
|
||||||
|
Files.writeString(pack.resolve("generators/flat.json"), "{\"composite\":[]}");
|
||||||
|
Files.writeString(pack.resolve("biomes/plain.json"),
|
||||||
|
"{\"generators\":[{\"generator\":\"flat\",\"min\":10,\"max\":10}]}");
|
||||||
|
return pack;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double height(double x, double z) {
|
||||||
|
return x * 0.125D - z * 0.03125D;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertBits(double expected, double actual) {
|
||||||
|
assertEquals(Double.doubleToRawLongBits(expected), Double.doubleToRawLongBits(actual));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Fixture implements AutoCloseable {
|
||||||
|
private final Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
|
||||||
|
private final EnginePlatformHooks hooks = mock(EnginePlatformHooks.class);
|
||||||
|
private final AtomicBoolean mainThread = new AtomicBoolean();
|
||||||
|
private final MockedStatic<B> blocks = mockStatic(B.class);
|
||||||
|
private final MockedStatic<IrisImageMapRuntime> imageRuntimes = mockStatic(IrisImageMapRuntime.class);
|
||||||
|
private final IrisImageMapRuntime images = mock(IrisImageMapRuntime.class);
|
||||||
|
private final IrisData data;
|
||||||
|
private final IrisComplex complex;
|
||||||
|
|
||||||
|
private Fixture(Path pack) {
|
||||||
|
NativeBlockState block = mock(NativeBlockState.class);
|
||||||
|
when(block.isFluid()).thenReturn(true);
|
||||||
|
blocks.when(() -> B.getState(anyString())).thenReturn(block);
|
||||||
|
blocks.when(() -> B.getStateOrNull(anyString(), eq(false))).thenReturn(block);
|
||||||
|
imageRuntimes.when(() -> IrisImageMapRuntime.compile(engine)).thenReturn(images);
|
||||||
|
when(images.has(IrisImageMapApplication.BIOME)).thenReturn(true);
|
||||||
|
when(images.sampleTerrainHeight(anyDouble(), anyDouble(), anyDouble()))
|
||||||
|
.thenAnswer(invocation -> invocation.<Double>getArgument(2));
|
||||||
|
data = IrisData.openRuntime(pack.toFile());
|
||||||
|
IrisDimension dimension = data.getDimensionLoader().load("main");
|
||||||
|
doReturn(data).when(engine).getData();
|
||||||
|
doReturn(dimension).when(engine).getDimension();
|
||||||
|
doReturn(384).when(engine).getHeight();
|
||||||
|
doReturn(-64).when(engine).getMinHeight();
|
||||||
|
doReturn(320).when(engine).getMaxHeight();
|
||||||
|
when(hooks.isMainThread()).thenAnswer(ignored -> mainThread.get());
|
||||||
|
doReturn(hooks).when(engine).getPlatformHooks();
|
||||||
|
doReturn(new SeedManager(1337L)).when(engine).getSeedManager();
|
||||||
|
complex = new IrisComplex(engine);
|
||||||
|
doReturn(complex).when(engine).getComplex();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AtomicInteger installNaturalHeight() {
|
||||||
|
AtomicInteger samples = new AtomicInteger();
|
||||||
|
complex.setNaturalHeightStream(ProceduralStream.ofDouble((x, z) -> {
|
||||||
|
samples.incrementAndGet();
|
||||||
|
return height(x, z);
|
||||||
|
}));
|
||||||
|
return samples;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
try {
|
||||||
|
complex.close();
|
||||||
|
data.close();
|
||||||
|
} finally {
|
||||||
|
imageRuntimes.close();
|
||||||
|
blocks.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user