mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-19 23:14:10 +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:
+6
@@ -42,6 +42,7 @@ import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.WorldBorder;
|
||||
|
||||
@@ -112,6 +113,11 @@ public final class BukkitEnginePlatformHooks implements EnginePlatformHooks {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMainThread() {
|
||||
return Bukkit.isPrimaryThread();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPregeneratorActive(Engine engine) {
|
||||
IrisWorld world = engine.getWorld();
|
||||
|
||||
@@ -692,6 +692,26 @@ public class IrisComplex implements DataProvider {
|
||||
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) {
|
||||
HydrologyColumnSample sample = hydrologySample(x, z);
|
||||
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.
|
||||
SharedCornerBounds shared = sharedCornerBounds();
|
||||
long sharedKey = SharedCornerBounds.key(gx, gz, interpolatorIndex);
|
||||
long packed;
|
||||
if (shared.contains(sharedKey)) {
|
||||
packed = shared.get(sharedKey);
|
||||
if (packed == Long.MIN_VALUE) {
|
||||
packed = computePackedBounds(cache, engine, interpolator, generators, gx, gz);
|
||||
shared.put(sharedKey, packed);
|
||||
}
|
||||
} else {
|
||||
long packed = shared.contains(sharedKey) ? shared.get(sharedKey) : Long.MIN_VALUE;
|
||||
if (packed == Long.MIN_VALUE) {
|
||||
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);
|
||||
}
|
||||
cache.gx[slot] = gx;
|
||||
|
||||
@@ -52,6 +52,7 @@ final class SharedCornerBounds {
|
||||
return false;
|
||||
}
|
||||
long storedKey = (long) LONGS.getAcquire(keys, slot);
|
||||
VarHandle.loadLoadFence();
|
||||
int after = (int) STAMPS.getAcquire(stamps, slot);
|
||||
return before == after && storedKey == key;
|
||||
}
|
||||
@@ -69,6 +70,8 @@ final class SharedCornerBounds {
|
||||
}
|
||||
long storedKey = (long) LONGS.getAcquire(keys, 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);
|
||||
if (before != after || storedKey != key) {
|
||||
return Long.MIN_VALUE;
|
||||
|
||||
@@ -333,9 +333,21 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
@BlockCoordinates
|
||||
default IrisBiome getSurfaceBiome(int x, int z) {
|
||||
if (answersFromNaturalTerrain(x, z)) {
|
||||
return getComplex().naturalSurfaceBiome(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
|
||||
default int getHeight(int x, int z) {
|
||||
return getHeight(x, z, true);
|
||||
@@ -343,6 +355,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
||||
|
||||
@BlockCoordinates
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,14 @@ public interface EnginePlatformHooks {
|
||||
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) {
|
||||
}
|
||||
|
||||
|
||||
@@ -194,6 +194,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
INMS.get().inject(world.getSeed(), engine, world);
|
||||
engine.getPlatformHooks().applyWorldBoundary(engine);
|
||||
IrisLogging.debug("Injected Iris Biome Source into " + world.getName());
|
||||
prefetchSpawnHydrology(engine, world);
|
||||
if (!studio) {
|
||||
J.sfut(() -> updateSpawnLocation(world), 1)
|
||||
.whenComplete((ignored, failure) -> {
|
||||
@@ -239,6 +240,44 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
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) {
|
||||
try {
|
||||
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
|
||||
* 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() {
|
||||
this("Iris");
|
||||
|
||||
@@ -14,6 +14,7 @@ import java.util.HashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.mockito.Answers.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -48,6 +49,20 @@ public class IrisComplexGridBoundsCacheTest {
|
||||
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
|
||||
public void alignedGridSampleUsesOnlyTheContributingCorner() throws Exception {
|
||||
IrisComplex complex = createComplex();
|
||||
|
||||
Reference in New Issue
Block a user