mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 21:40:05 +00:00
Answer server-thread terrain queries from natural terrain
A height or biome query from the tick thread no longer blocks on a cold hydrology plan: it answers from the natural streams and asks the planning pool for the tiles meanwhile. A new world prefetches the tiles around spawn when the generator is injected, and the hydrology pool gets full parallelism instead of a third. Also stops the shared corner-bounds cache from pinning a non-finite sample and fences its two stamp reads.
This commit is contained in:
8 files changed
+115
-9
No files matched your search
+6
@@ -42,6 +42,7 @@ import art.arcane.iris.spi.IrisLogging;
|
|||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
import art.arcane.iris.util.common.scheduling.J;
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.WorldBorder;
|
import org.bukkit.WorldBorder;
|
||||||
|
|
||||||
@@ -112,6 +113,11 @@ public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isMainThread() {
|
||||||
|
return Bukkit.isPrimaryThread();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isPregeneratorActive(Engine engine) {
|
public boolean isPregeneratorActive(Engine engine) {
|
||||||
IrisWorld world = engine.getWorld();
|
IrisWorld world = engine.getWorld();
|
||||||
|
|||||||
@@ -692,6 +692,26 @@ public class IrisComplex implements DataProvider {
|
|||||||
return hydrologyRuntime == null ? null : hydrologyRuntime.sample(x, z).orElse(null);
|
return hydrologyRuntime == null ? null : hydrologyRuntime.sample(x, z).orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether the column's hydrology can be sampled without waiting for a plan (see Engine.answersFromNaturalTerrain). */
|
||||||
|
public boolean isHydrologyPlanned(int x, int z) {
|
||||||
|
return hydrologyRuntime == null || hydrologyRuntime.isPlanned(x, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The natural terrain height of a column, from the natural height stream only: never touches hydrology or its caches. */
|
||||||
|
public int naturalTrueHeight(int x, int z) {
|
||||||
|
return (int) Math.round(naturalHeightStream.getDouble(x, z));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The surface biome a column would have without any river content, from natural streams only. */
|
||||||
|
public IrisBiome naturalSurfaceBiome(int x, int z) {
|
||||||
|
IrisBiome mapped = imageMapRuntime.sampleBiome(x, z);
|
||||||
|
if (mapped != null) {
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
double terrainHeight = naturalHeightStream.getDouble(x, z);
|
||||||
|
return fixBiomeType(terrainHeight, baseBiomeStream.get(x, z), regionStream.get(x, z), (double) x, (double) z, fluidHeight);
|
||||||
|
}
|
||||||
|
|
||||||
private HydrologyColumnLayer surfaceLayer(double x, double z) {
|
private HydrologyColumnLayer surfaceLayer(double x, double z) {
|
||||||
HydrologyColumnSample sample = hydrologySample(x, z);
|
HydrologyColumnSample sample = hydrologySample(x, z);
|
||||||
return sample == null ? null : sample.primarySurfaceLayer().orElse(null);
|
return sample == null ? null : sample.primarySurfaceLayer().orElse(null);
|
||||||
@@ -928,15 +948,14 @@ public class IrisComplex implements DataProvider {
|
|||||||
// catches the same corner reached by another thread or by hydrology planning earlier.
|
// catches the same corner reached by another thread or by hydrology planning earlier.
|
||||||
SharedCornerBounds shared = sharedCornerBounds();
|
SharedCornerBounds shared = sharedCornerBounds();
|
||||||
long sharedKey = SharedCornerBounds.key(gx, gz, interpolatorIndex);
|
long sharedKey = SharedCornerBounds.key(gx, gz, interpolatorIndex);
|
||||||
long packed;
|
long packed = shared.contains(sharedKey) ? shared.get(sharedKey) : Long.MIN_VALUE;
|
||||||
if (shared.contains(sharedKey)) {
|
if (packed == Long.MIN_VALUE) {
|
||||||
packed = shared.get(sharedKey);
|
|
||||||
if (packed == Long.MIN_VALUE) {
|
|
||||||
packed = computePackedBounds(cache, engine, interpolator, generators, gx, gz);
|
|
||||||
shared.put(sharedKey, packed);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
packed = computePackedBounds(cache, engine, interpolator, generators, gx, gz);
|
packed = computePackedBounds(cache, engine, interpolator, generators, gx, gz);
|
||||||
|
if (!Double.isFinite(boundsLow(packed)) || !Double.isFinite(boundsHigh(packed))) {
|
||||||
|
// A non-finite corner is a transient sampling fault, not a fact about the terrain:
|
||||||
|
// hand it back uncached so the next sample recomputes it instead of pinning it.
|
||||||
|
return packed;
|
||||||
|
}
|
||||||
shared.put(sharedKey, packed);
|
shared.put(sharedKey, packed);
|
||||||
}
|
}
|
||||||
cache.gx[slot] = gx;
|
cache.gx[slot] = gx;
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ final class SharedCornerBounds {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
long storedKey = (long) LONGS.getAcquire(keys, slot);
|
long storedKey = (long) LONGS.getAcquire(keys, slot);
|
||||||
|
VarHandle.loadLoadFence();
|
||||||
int after = (int) STAMPS.getAcquire(stamps, slot);
|
int after = (int) STAMPS.getAcquire(stamps, slot);
|
||||||
return before == after && storedKey == key;
|
return before == after && storedKey == key;
|
||||||
}
|
}
|
||||||
@@ -69,6 +70,8 @@ final class SharedCornerBounds {
|
|||||||
}
|
}
|
||||||
long storedKey = (long) LONGS.getAcquire(keys, slot);
|
long storedKey = (long) LONGS.getAcquire(keys, slot);
|
||||||
long value = (long) LONGS.getAcquire(values, slot);
|
long value = (long) LONGS.getAcquire(values, slot);
|
||||||
|
// The data loads above must not drift past the second stamp read, or a torn slot could pass.
|
||||||
|
VarHandle.loadLoadFence();
|
||||||
int after = (int) STAMPS.getAcquire(stamps, slot);
|
int after = (int) STAMPS.getAcquire(stamps, slot);
|
||||||
if (before != after || storedKey != key) {
|
if (before != after || storedKey != key) {
|
||||||
return Long.MIN_VALUE;
|
return Long.MIN_VALUE;
|
||||||
|
|||||||
@@ -333,9 +333,21 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
|||||||
|
|
||||||
@BlockCoordinates
|
@BlockCoordinates
|
||||||
default IrisBiome getSurfaceBiome(int x, int z) {
|
default IrisBiome getSurfaceBiome(int x, int z) {
|
||||||
|
if (answersFromNaturalTerrain(x, z)) {
|
||||||
|
return getComplex().naturalSurfaceBiome(x, z);
|
||||||
|
}
|
||||||
return getComplex().getTrueBiomeStream().get(x, z);
|
return getComplex().getTrueBiomeStream().get(x, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The server thread must never wait for a cold hydrology plan, so a height or biome query from it
|
||||||
|
* on a column whose tiles are not planned yet is answered from natural terrain (the tiles are
|
||||||
|
* requested from the planning pool meanwhile). Generation threads always wait for the real plan.
|
||||||
|
*/
|
||||||
|
default boolean answersFromNaturalTerrain(int x, int z) {
|
||||||
|
return getPlatformHooks().isMainThread() && !getComplex().isHydrologyPlanned(x, z);
|
||||||
|
}
|
||||||
|
|
||||||
@BlockCoordinates
|
@BlockCoordinates
|
||||||
default int getHeight(int x, int z) {
|
default int getHeight(int x, int z) {
|
||||||
return getHeight(x, z, true);
|
return getHeight(x, z, true);
|
||||||
@@ -343,6 +355,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
|||||||
|
|
||||||
@BlockCoordinates
|
@BlockCoordinates
|
||||||
default int getHeight(int x, int z, boolean ignoreFluid) {
|
default int getHeight(int x, int z, boolean ignoreFluid) {
|
||||||
|
if (answersFromNaturalTerrain(x, z)) {
|
||||||
|
int natural = getComplex().naturalTrueHeight(x, z);
|
||||||
|
return ignoreFluid ? natural : Math.max(natural, getMantle().getFluidHeight());
|
||||||
|
}
|
||||||
return getMantle().getHighest(x, z, getData(), ignoreFluid);
|
return getMantle().getHighest(x, z, getData(), ignoreFluid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,14 @@ public interface EnginePlatformHooks {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the calling thread is the server's tick thread. Queries from there must never wait
|
||||||
|
* for hydrology planning, so they answer from natural terrain until the tiles are planned.
|
||||||
|
*/
|
||||||
|
default boolean isMainThread() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
default void shutdownPregenerator(Engine engine) {
|
default void shutdownPregenerator(Engine engine) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
INMS.get().inject(world.getSeed(), engine, world);
|
INMS.get().inject(world.getSeed(), engine, world);
|
||||||
engine.getPlatformHooks().applyWorldBoundary(engine);
|
engine.getPlatformHooks().applyWorldBoundary(engine);
|
||||||
IrisLogging.debug("Injected Iris Biome Source into " + world.getName());
|
IrisLogging.debug("Injected Iris Biome Source into " + world.getName());
|
||||||
|
prefetchSpawnHydrology(engine, world);
|
||||||
if (!studio) {
|
if (!studio) {
|
||||||
J.sfut(() -> updateSpawnLocation(world), 1)
|
J.sfut(() -> updateSpawnLocation(world), 1)
|
||||||
.whenComplete((ignored, failure) -> {
|
.whenComplete((ignored, failure) -> {
|
||||||
@@ -239,6 +240,44 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
return new Location(world, 0.5D, y, 0.5D);
|
return new Location(world, 0.5D, y, 0.5D);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int hydrologyTileSize() {
|
||||||
|
Engine activeEngine = engine;
|
||||||
|
if (activeEngine == null || activeEngine.getComplex() == null || activeEngine.getComplex().getHydrologyRuntime() == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return activeEngine.getComplex().getHydrologyRuntime().settings().routing().tileSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts planning the hydrology tiles around the initial spawn as soon as the generator is injected
|
||||||
|
* into a world whose spawn chunk does not exist yet. The first teleport there makes the chunk system
|
||||||
|
* generate chunks a few hundred blocks around the spawn, and each of those chunks' mantle windows
|
||||||
|
* reaches further still, so the area planned ahead is half a tile in every direction: every tile a
|
||||||
|
* cold entry can touch starts planning at once instead of one generation thread at a time finding
|
||||||
|
* it cold. A world that already generated its spawn (every existing world at boot, a hotloaded live
|
||||||
|
* world) skips it.
|
||||||
|
*/
|
||||||
|
private void prefetchSpawnHydrology(Engine engine, World world) {
|
||||||
|
if (engine.getComplex() == null || engine.getComplex().getHydrologyRuntime() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Location spawn = getInitialSpawnLocation(world);
|
||||||
|
int spawnX = spawn.getBlockX();
|
||||||
|
int spawnZ = spawn.getBlockZ();
|
||||||
|
if (world.isChunkGenerated(spawnX >> 4, spawnZ >> 4)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int reach = Math.max(16, hydrologyTileSize() / 2);
|
||||||
|
engine.getComplex().getHydrologyRuntime().prefetchArea(
|
||||||
|
spawnX - reach,
|
||||||
|
spawnZ - reach,
|
||||||
|
spawnX + reach - 1,
|
||||||
|
spawnZ + reach - 1,
|
||||||
|
spawnX,
|
||||||
|
spawnZ
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private void updateSpawnLocation(World world) {
|
private void updateSpawnLocation(World world) {
|
||||||
try {
|
try {
|
||||||
Location initialSpawn = getInitialSpawnLocation(world);
|
Location initialSpawn = getInitialSpawnLocation(world);
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public class MultiBurst extends MultiBurstSupport {
|
|||||||
* Long-running hydrology tile planning. Kept off {@link #burst} so a handful of twenty-second
|
* Long-running hydrology tile planning. Kept off {@link #burst} so a handful of twenty-second
|
||||||
* plans never pin the workers that chunk generation's short stage and mantle tasks need.
|
* plans never pin the workers that chunk generation's short stage and mantle tasks need.
|
||||||
*/
|
*/
|
||||||
public static final MultiBurst hydrology = new MultiBurst("Iris Hydrology", () -> Math.max(2, IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()) / 3));
|
public static final MultiBurst hydrology = new MultiBurst("Iris Hydrology", () -> Math.max(2, IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism())));
|
||||||
|
|
||||||
public MultiBurst() {
|
public MultiBurst() {
|
||||||
this("Iris");
|
this("Iris");
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import java.util.HashMap;
|
|||||||
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.assertNotEquals;
|
import static org.junit.Assert.assertNotEquals;
|
||||||
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;
|
||||||
@@ -48,6 +49,20 @@ public class IrisComplexGridBoundsCacheTest {
|
|||||||
assertEquals(1, interpolator.getInvocations());
|
assertEquals(1, interpolator.getInvocations());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void nonFiniteCornerBoundsAreNeverCached() throws Exception {
|
||||||
|
IrisComplex complex = createComplex();
|
||||||
|
Method cornerBounds = cornerBoundsMethod();
|
||||||
|
CountingInterpolator interpolator = new CountingInterpolator(Double.NaN, 7.25D);
|
||||||
|
|
||||||
|
long firstPacked = invokeCornerBounds(complex, cornerBounds, interpolator, 128, 96);
|
||||||
|
long secondPacked = invokeCornerBounds(complex, cornerBounds, interpolator, 128, 96);
|
||||||
|
|
||||||
|
assertTrue(Double.isNaN(unpackLow(firstPacked)));
|
||||||
|
assertTrue(Double.isNaN(unpackLow(secondPacked)));
|
||||||
|
assertEquals(2, interpolator.getInvocations());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void alignedGridSampleUsesOnlyTheContributingCorner() throws Exception {
|
public void alignedGridSampleUsesOnlyTheContributingCorner() throws Exception {
|
||||||
IrisComplex complex = createComplex();
|
IrisComplex complex = createComplex();
|
||||||
|
|||||||
Reference in new issue
Block a user