mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 14:00:26 +00:00
Streamlined chunk prefill
- Read interpolator corner bounds through a per-thread chunk block and skip groups whose cell corners are all zero - Prefilled chunk context caches inline on generation threads with heights ahead of biomes - Kept the burst-pool prefill only for the platform main thread, with heights and biomes in one task - Painted surface fluid from the chunk context cache instead of re-sampling the fluid stream
This commit is contained in:
1 parent
c1732d50be
commit
b03f1c90e1
9 files changed
+528
-55
No files matched your search
@@ -17,8 +17,6 @@ import art.arcane.volmlib.nativelib.terrain.NativeBiome;
|
||||
import art.arcane.volmlib.util.hunk.Hunk;
|
||||
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class ChunkContext {
|
||||
@@ -95,38 +93,11 @@ public class ChunkContext {
|
||||
if (hydrologyRuntime != null && (resolvedPlan.height || resolvedPlan.biome)) {
|
||||
hydrologyRuntime.prepareChunkColumns(x, z);
|
||||
}
|
||||
List<Runnable> fillTasks = new ArrayList<>(6);
|
||||
if (resolvedPlan.height) {
|
||||
fillTasks.add(() -> height.fillRounded(roundedHeight));
|
||||
}
|
||||
if (resolvedPlan.biome) {
|
||||
fillTasks.add(new PrefillFillTask(biome));
|
||||
}
|
||||
if (resolvedPlan.rock) {
|
||||
fillTasks.add(new PrefillFillTask(rock));
|
||||
}
|
||||
if (resolvedPlan.fluid) {
|
||||
fillTasks.add(new PrefillFillTask(fluid));
|
||||
}
|
||||
if (resolvedPlan.region) {
|
||||
fillTasks.add(new PrefillFillTask(region));
|
||||
}
|
||||
if (resolvedPlan.cave) {
|
||||
fillTasks.add(new PrefillFillTask(cave));
|
||||
}
|
||||
|
||||
if (!shouldPrefillAsync(resolvedPlan, fillTasks.size())) {
|
||||
for (Runnable fillTask : fillTasks) {
|
||||
fillTask.run();
|
||||
}
|
||||
if (shouldPrefillAsync(resolvedPlan, complex.isPlatformMainThread())) {
|
||||
prefillAsync(resolvedPlan);
|
||||
} else {
|
||||
List<CompletableFuture<Void>> futures = new ArrayList<>(fillTasks.size());
|
||||
for (Runnable fillTask : fillTasks) {
|
||||
futures.add(CompletableFuture.runAsync(fillTask, MultiBurst.burst));
|
||||
}
|
||||
for (CompletableFuture<Void> future : futures) {
|
||||
future.join();
|
||||
}
|
||||
prefillSurface(resolvedPlan);
|
||||
prefillPalettes(resolvedPlan);
|
||||
}
|
||||
if (capturePrefillMetric) {
|
||||
metrics.getContextPrefill().put((System.nanoTime() - totalStartNanos) / 1_000_000D);
|
||||
@@ -134,14 +105,46 @@ public class ChunkContext {
|
||||
}
|
||||
}
|
||||
|
||||
static boolean shouldPrefillAsync(PrefillPlan prefillPlan, int fillTaskCount) {
|
||||
if (prefillPlan == PrefillPlan.NATURAL_TERRAIN || fillTaskCount <= 1 || !IrisPlatforms.isBound()) {
|
||||
static boolean shouldPrefillAsync(PrefillPlan prefillPlan, boolean platformMainThread) {
|
||||
if (!platformMainThread || prefillPlan == PrefillPlan.NATURAL_TERRAIN || prefillPlan == PrefillPlan.NONE
|
||||
|| !IrisPlatforms.isBound()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !MultiBurst.burst.ownsCurrentThread();
|
||||
}
|
||||
|
||||
private void prefillSurface(PrefillPlan prefillPlan) {
|
||||
if (prefillPlan.height) {
|
||||
height.fillRounded(roundedHeight);
|
||||
}
|
||||
if (prefillPlan.biome) {
|
||||
biome.fill();
|
||||
}
|
||||
}
|
||||
|
||||
private void prefillPalettes(PrefillPlan prefillPlan) {
|
||||
if (prefillPlan.rock) {
|
||||
rock.fill();
|
||||
}
|
||||
if (prefillPlan.fluid) {
|
||||
fluid.fill();
|
||||
}
|
||||
if (prefillPlan.region) {
|
||||
region.fill();
|
||||
}
|
||||
if (prefillPlan.cave) {
|
||||
cave.fill();
|
||||
}
|
||||
}
|
||||
|
||||
private void prefillAsync(PrefillPlan prefillPlan) {
|
||||
CompletableFuture<Void> surface = CompletableFuture.runAsync(() -> prefillSurface(prefillPlan), MultiBurst.burst);
|
||||
CompletableFuture<Void> palettes = CompletableFuture.runAsync(() -> prefillPalettes(prefillPlan), MultiBurst.burst);
|
||||
surface.join();
|
||||
palettes.join();
|
||||
}
|
||||
|
||||
public void setTerrainBiomeOutput(Hunk<NativeBiome> terrainBiomes) {
|
||||
this.terrainBiomes = terrainBiomes;
|
||||
}
|
||||
@@ -295,17 +298,4 @@ public class ChunkContext {
|
||||
|
||||
public record TransitionTerrainColumn(Terrain3DColumn solids, int fluidHeight) {
|
||||
}
|
||||
|
||||
private static final class PrefillFillTask implements Runnable {
|
||||
private final ChunkedDataCache<?> dataCache;
|
||||
|
||||
private PrefillFillTask(ChunkedDataCache<?> dataCache) {
|
||||
this.dataCache = dataCache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
dataCache.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1384,6 +1384,10 @@ public class IrisComplex implements DataProvider {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isPlatformMainThread() {
|
||||
return terrainEngine.getPlatformHooks().isMainThread();
|
||||
}
|
||||
|
||||
public ProceduralStream<Double> getRawHeightStream() {
|
||||
return heightStream;
|
||||
}
|
||||
@@ -1891,13 +1895,82 @@ public class IrisComplex implements DataProvider {
|
||||
return h;
|
||||
}
|
||||
|
||||
int grid = biomeBoundsSamplingStep;
|
||||
if (!ChunkCorners.supports(grid)) {
|
||||
for (int interpolatorIndex = 0; interpolatorIndex < frozenInterpolators.length; interpolatorIndex++) {
|
||||
h += interpolateGenerators(engine, frozenInterpolators[interpolatorIndex], interpolatorIndex, frozenGenerators[interpolatorIndex], x, z, seed);
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
GridBoundsCache cache = gridBoundsCache.get();
|
||||
int xi = (int) Math.floor(x);
|
||||
int zi = (int) Math.floor(z);
|
||||
int gx = xi & ~(grid - 1);
|
||||
int gz = zi & ~(grid - 1);
|
||||
double fx = (x - gx) / grid;
|
||||
double fz = (z - gz) / grid;
|
||||
ChunkCorners corners = cache.chunkCorners(xi >> 4, zi >> 4, frozenInterpolators.length, grid);
|
||||
int cornerX = (gx - corners.originX) / grid;
|
||||
int cornerZ = (gz - corners.originZ) / grid;
|
||||
int cell = cornerZ * corners.cellsPerAxis + cornerX;
|
||||
long heightSeed = seed + 239945;
|
||||
for (int interpolatorIndex = 0; interpolatorIndex < frozenInterpolators.length; interpolatorIndex++) {
|
||||
h += interpolateGenerators(engine, frozenInterpolators[interpolatorIndex], interpolatorIndex, frozenGenerators[interpolatorIndex], x, z, seed);
|
||||
IrisGenerator[] generators = frozenGenerators[interpolatorIndex];
|
||||
if (generators.length == 0) {
|
||||
continue;
|
||||
}
|
||||
int state = corners.cellState(interpolatorIndex, cell);
|
||||
if (state == ChunkCorners.SILENT) {
|
||||
continue;
|
||||
}
|
||||
long b00 = cornerAt(cache, engine, corners, interpolatorIndex, cornerX, cornerZ);
|
||||
long b10 = fx == 0D ? 0L : cornerAt(cache, engine, corners, interpolatorIndex, cornerX + 1, cornerZ);
|
||||
long b01 = fz == 0D ? 0L : cornerAt(cache, engine, corners, interpolatorIndex, cornerX, cornerZ + 1);
|
||||
long b11 = fx == 0D || fz == 0D ? 0L : cornerAt(cache, engine, corners, interpolatorIndex, cornerX + 1, cornerZ + 1);
|
||||
if (state == ChunkCorners.UNKNOWN) {
|
||||
corners.classify(interpolatorIndex, cornerX, cornerZ, cell);
|
||||
if (silentBounds(b00) && silentBounds(b10) && silentBounds(b01) && silentBounds(b11)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
double lo;
|
||||
double hi;
|
||||
if (fx == 0D && fz == 0D) {
|
||||
lo = boundsLow(b00);
|
||||
hi = boundsHigh(b00);
|
||||
} else if (fz == 0D) {
|
||||
lo = biLerp(boundsLow(b00), boundsLow(b10), boundsLow(b00), boundsLow(b10), fx, fz);
|
||||
hi = biLerp(boundsHigh(b00), boundsHigh(b10), boundsHigh(b00), boundsHigh(b10), fx, fz);
|
||||
} else if (fx == 0D) {
|
||||
lo = biLerp(boundsLow(b00), boundsLow(b00), boundsLow(b01), boundsLow(b01), fx, fz);
|
||||
hi = biLerp(boundsHigh(b00), boundsHigh(b00), boundsHigh(b01), boundsHigh(b01), fx, fz);
|
||||
} else {
|
||||
lo = biLerp(boundsLow(b00), boundsLow(b10), boundsLow(b01), boundsLow(b11), fx, fz);
|
||||
hi = biLerp(boundsHigh(b00), boundsHigh(b10), boundsHigh(b01), boundsHigh(b11), fx, fz);
|
||||
}
|
||||
h += averageGeneratorHeights(generators, lo, hi, x, z, heightSeed);
|
||||
}
|
||||
|
||||
return h;
|
||||
}
|
||||
|
||||
private long cornerAt(GridBoundsCache cache, Engine engine, ChunkCorners corners, int interpolatorIndex, int column, int row) {
|
||||
if (corners.known(interpolatorIndex, column, row)) {
|
||||
return corners.corner(interpolatorIndex, column, row);
|
||||
}
|
||||
long packed = cornerBounds(cache, engine, frozenInterpolators[interpolatorIndex], interpolatorIndex,
|
||||
frozenGenerators[interpolatorIndex], corners.originX + column * corners.grid, corners.originZ + row * corners.grid);
|
||||
if (Double.isFinite(boundsLow(packed)) && Double.isFinite(boundsHigh(packed))) {
|
||||
corners.remember(interpolatorIndex, column, row, packed);
|
||||
}
|
||||
return packed;
|
||||
}
|
||||
|
||||
private static boolean silentBounds(long packed) {
|
||||
return (packed & 0x7FFFFFFF7FFFFFFFL) == 0L;
|
||||
}
|
||||
|
||||
private NoiseBounds naturalHeightBounds(
|
||||
Engine engine,
|
||||
KList<IrisShapedGeneratorStyle> overlayNoise,
|
||||
@@ -2268,6 +2341,26 @@ public class IrisComplex implements DataProvider {
|
||||
private final long[] packed = new long[GRID_BOUNDS_CACHE_SIZE];
|
||||
private final boolean[] valid = new boolean[GRID_BOUNDS_CACHE_SIZE];
|
||||
private final BoundsSampler sampler = new BoundsSampler();
|
||||
private ChunkCorners recentCorners;
|
||||
private ChunkCorners previousCorners;
|
||||
|
||||
private ChunkCorners chunkCorners(int chunkX, int chunkZ, int groups, int grid) {
|
||||
ChunkCorners recent = recentCorners;
|
||||
if (recent != null && recent.matches(chunkX, chunkZ, groups, grid)) {
|
||||
return recent;
|
||||
}
|
||||
ChunkCorners previous = previousCorners;
|
||||
if (previous != null && previous.matches(chunkX, chunkZ, groups, grid)) {
|
||||
previousCorners = recent;
|
||||
recentCorners = previous;
|
||||
return previous;
|
||||
}
|
||||
ChunkCorners reused = previous != null && previous.fits(groups, grid) ? previous : new ChunkCorners(groups, grid);
|
||||
reused.reset(chunkX, chunkZ);
|
||||
previousCorners = recent;
|
||||
recentCorners = reused;
|
||||
return reused;
|
||||
}
|
||||
|
||||
private int slot(int cornerX, int cornerZ, int interpolatorIndex) {
|
||||
long h = (cornerX * 0x9E3779B97F4A7C15L) ^ (cornerZ * 0xC2B2AE3D27D4EB4FL) ^ (interpolatorIndex * 0x165667B19E3779F9L);
|
||||
@@ -2276,6 +2369,92 @@ public class IrisComplex implements DataProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ChunkCorners {
|
||||
private static final int UNKNOWN = 0;
|
||||
private static final int SILENT = 1;
|
||||
private static final int ACTIVE = 2;
|
||||
|
||||
private final int groups;
|
||||
private final int grid;
|
||||
private final int cornersPerAxis;
|
||||
private final int cellsPerAxis;
|
||||
private final long[] packed;
|
||||
private final boolean[] known;
|
||||
private final byte[] cellStates;
|
||||
private int chunkX;
|
||||
private int chunkZ;
|
||||
private int originX;
|
||||
private int originZ;
|
||||
|
||||
private ChunkCorners(int groups, int grid) {
|
||||
this.groups = groups;
|
||||
this.grid = grid;
|
||||
cellsPerAxis = 16 / grid;
|
||||
cornersPerAxis = cellsPerAxis + 1;
|
||||
packed = new long[groups * cornersPerAxis * cornersPerAxis];
|
||||
known = new boolean[packed.length];
|
||||
cellStates = new byte[groups * cellsPerAxis * cellsPerAxis];
|
||||
}
|
||||
|
||||
private static boolean supports(int grid) {
|
||||
return grid >= 2 && grid <= 16 && Integer.bitCount(grid) == 1;
|
||||
}
|
||||
|
||||
private boolean fits(int groups, int grid) {
|
||||
return this.groups == groups && this.grid == grid;
|
||||
}
|
||||
|
||||
private boolean matches(int chunkX, int chunkZ, int groups, int grid) {
|
||||
return this.chunkX == chunkX && this.chunkZ == chunkZ && fits(groups, grid);
|
||||
}
|
||||
|
||||
private void reset(int chunkX, int chunkZ) {
|
||||
this.chunkX = chunkX;
|
||||
this.chunkZ = chunkZ;
|
||||
originX = chunkX << 4;
|
||||
originZ = chunkZ << 4;
|
||||
Arrays.fill(known, false);
|
||||
Arrays.fill(cellStates, (byte) UNKNOWN);
|
||||
}
|
||||
|
||||
private int cellState(int group, int cell) {
|
||||
return cellStates[group * cellsPerAxis * cellsPerAxis + cell];
|
||||
}
|
||||
|
||||
private void setCellState(int group, int cell, int state) {
|
||||
cellStates[group * cellsPerAxis * cellsPerAxis + cell] = (byte) state;
|
||||
}
|
||||
|
||||
private boolean known(int group, int column, int row) {
|
||||
return known[index(group, column, row)];
|
||||
}
|
||||
|
||||
private long corner(int group, int column, int row) {
|
||||
return packed[index(group, column, row)];
|
||||
}
|
||||
|
||||
private void remember(int group, int column, int row, long value) {
|
||||
int index = index(group, column, row);
|
||||
packed[index] = value;
|
||||
known[index] = true;
|
||||
}
|
||||
|
||||
private void classify(int group, int column, int row, int cell) {
|
||||
int first = index(group, column, row);
|
||||
int second = first + cornersPerAxis;
|
||||
if (!known[first] || !known[first + 1] || !known[second] || !known[second + 1]) {
|
||||
return;
|
||||
}
|
||||
boolean silent = silentBounds(packed[first]) && silentBounds(packed[first + 1])
|
||||
&& silentBounds(packed[second]) && silentBounds(packed[second + 1]);
|
||||
setCellState(group, cell, silent ? SILENT : ACTIVE);
|
||||
}
|
||||
|
||||
private int index(int group, int column, int row) {
|
||||
return (group * cornersPerAxis + row) * cornersPerAxis + column;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable per-thread bounds provider. Holds the biome memo and the lazily computed generator
|
||||
* bounds for one sampleBoundsRaw pass so the pass allocates nothing; {@link #bind}
|
||||
|
||||
@@ -160,6 +160,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
private final ChunkedDataCache<IrisBiome> biomeCache;
|
||||
private final ChunkedDataCache<IrisRegion> regionCache;
|
||||
private final ChunkedDataCache<NativeBlockState> rockCache;
|
||||
private final ChunkedDataCache<NativeBlockState> fluidCache;
|
||||
private final UpperDimensionContext upperContext;
|
||||
private final IrisOreBands dimensionSurfaceOres;
|
||||
private final IrisOreBands dimensionUndergroundOres;
|
||||
@@ -192,6 +193,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
biomeCache = context.getBiome();
|
||||
regionCache = context.getRegion();
|
||||
rockCache = context.getRock();
|
||||
fluidCache = context.getFluid();
|
||||
upperContext = getEngine().getUpperContext();
|
||||
dimensionSurfaceOres = hideOres ? IrisOreBands.EMPTY : dimension.getSurfaceOreBands();
|
||||
dimensionUndergroundOres = hideOres ? IrisOreBands.EMPTY : dimension.getUndergroundOreBands();
|
||||
@@ -247,8 +249,10 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
|
||||
IrisRiverMaterialConfig roleMaterial = hydrologyRoleMaterial(
|
||||
hydrologyTerrain, bedMaterial, shoreMaterial, bankMaterial);
|
||||
int paintDepth = roleMaterial == null ? 0 : roleMaterial.getDepth();
|
||||
NativeBlockState fluid = hydrologyFluid == null || transitionDisplaced
|
||||
NativeBlockState fluid = transitionDisplaced
|
||||
? complex.resolveSurfaceFluid(realX, realZ)
|
||||
: hydrologyFluid == null
|
||||
? fluidCache.get(xf, zf)
|
||||
: complex.resolveHydrologyFluid(hydrologyFluid.profileKey(), realX, realZ);
|
||||
NativeBlockState rock = rockCache.get(xf, zf);
|
||||
NativeBlockState mappedSurfaceBlock = complex.getImageMapRuntime().sampleSurfaceBlock(realX, realZ);
|
||||
|
||||
+54
-4
@@ -95,9 +95,42 @@ public class ChunkContextPrefillPlanTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleOrNoFillTaskPrefillsInline() {
|
||||
assertFalse(ChunkContext.shouldPrefillAsync(ChunkContext.PrefillPlan.NO_CAVE, 0));
|
||||
assertFalse(ChunkContext.shouldPrefillAsync(ChunkContext.PrefillPlan.NO_CAVE, 1));
|
||||
public void generationWorkersPrefillInlineWithoutConsultingSharedExecutor() {
|
||||
try (MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class)) {
|
||||
platforms.when(IrisPlatforms::isBound).thenReturn(true);
|
||||
assertFalse(ChunkContext.shouldPrefillAsync(ChunkContext.PrefillPlan.NO_CAVE, false));
|
||||
assertFalse(ChunkContext.shouldPrefillAsync(ChunkContext.PrefillPlan.ALL, false));
|
||||
platforms.verifyNoInteractions();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void platformMainThreadKeepsBlockingPrefillOffTheTickThread() {
|
||||
try (MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class)) {
|
||||
platforms.when(IrisPlatforms::isBound).thenReturn(true);
|
||||
assertTrue(ChunkContext.shouldPrefillAsync(ChunkContext.PrefillPlan.NO_CAVE, true));
|
||||
assertFalse(ChunkContext.shouldPrefillAsync(ChunkContext.PrefillPlan.NONE, true));
|
||||
assertFalse(ChunkContext.shouldPrefillAsync(ChunkContext.PrefillPlan.NATURAL_TERRAIN, true));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inlinePrefillFinishesHeightsBeforeTheBiomePassStarts() {
|
||||
AtomicInteger caveCalls = new AtomicInteger();
|
||||
AtomicInteger heightCalls = new AtomicInteger();
|
||||
AtomicInteger biomeCalls = new AtomicInteger();
|
||||
AtomicInteger rockCalls = new AtomicInteger();
|
||||
AtomicInteger fluidCalls = new AtomicInteger();
|
||||
AtomicInteger regionCalls = new AtomicInteger();
|
||||
AtomicInteger heightsSeenByFirstBiome = new AtomicInteger(-1);
|
||||
try (MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class)) {
|
||||
platforms.when(IrisPlatforms::isBound).thenReturn(true);
|
||||
createContext(ChunkContext.PrefillPlan.NO_CAVE, caveCalls, heightCalls, biomeCalls, rockCalls,
|
||||
fluidCalls, regionCalls, null, () -> heightsSeenByFirstBiome.compareAndSet(-1, heightCalls.get()));
|
||||
}
|
||||
|
||||
assertEquals(256, heightsSeenByFirstBiome.get());
|
||||
assertEquals(256, biomeCalls.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -110,7 +143,7 @@ public class ChunkContextPrefillPlanTest {
|
||||
AtomicInteger regionCalls = new AtomicInteger();
|
||||
try (MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class)) {
|
||||
platforms.when(IrisPlatforms::isBound).thenReturn(true);
|
||||
assertFalse(ChunkContext.shouldPrefillAsync(ChunkContext.PrefillPlan.NATURAL_TERRAIN, 5));
|
||||
assertFalse(ChunkContext.shouldPrefillAsync(ChunkContext.PrefillPlan.NATURAL_TERRAIN, true));
|
||||
ChunkContext context = createContext(ChunkContext.PrefillPlan.NATURAL_TERRAIN,
|
||||
caveCalls, heightCalls, biomeCalls, rockCalls, fluidCalls, regionCalls);
|
||||
|
||||
@@ -228,6 +261,22 @@ public class ChunkContextPrefillPlanTest {
|
||||
AtomicInteger fluidCalls,
|
||||
AtomicInteger regionCalls,
|
||||
IrisHydrologyRuntime runtime
|
||||
) {
|
||||
return createContext(prefillPlan, caveCalls, heightCalls, biomeCalls, rockCalls, fluidCalls, regionCalls,
|
||||
runtime, () -> {
|
||||
});
|
||||
}
|
||||
|
||||
private ChunkContext createContext(
|
||||
ChunkContext.PrefillPlan prefillPlan,
|
||||
AtomicInteger caveCalls,
|
||||
AtomicInteger heightCalls,
|
||||
AtomicInteger biomeCalls,
|
||||
AtomicInteger rockCalls,
|
||||
AtomicInteger fluidCalls,
|
||||
AtomicInteger regionCalls,
|
||||
IrisHydrologyRuntime runtime,
|
||||
Runnable biomeObserver
|
||||
) {
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
|
||||
@@ -244,6 +293,7 @@ public class ChunkContextPrefillPlanTest {
|
||||
ProceduralStream<IrisBiome> biomeStream = mock(ProceduralStream.class);
|
||||
IrisBiome biome = mock(IrisBiome.class);
|
||||
doAnswer(invocation -> {
|
||||
biomeObserver.run();
|
||||
biomeCalls.incrementAndGet();
|
||||
return biome;
|
||||
}).when(biomeStream).get(anyDouble(), anyDouble());
|
||||
|
||||
+1
@@ -351,6 +351,7 @@ public class DimensionTerrain3DCompositionTest {
|
||||
when(context.getBiome()).thenReturn(biomes);
|
||||
when(context.getRegion()).thenReturn(regions);
|
||||
when(context.getRock()).thenReturn(rocks);
|
||||
when(context.getFluid()).thenReturn(mock(ChunkedDataCache.class));
|
||||
when(context.getRoundedHeight(0, 0)).thenReturn(rootHeight);
|
||||
Hunk<NativeBlockState> blocks = Hunk.newArrayHunk(1, 64, 1);
|
||||
new IrisTerrainNormalActuator(engine).paint(0, 0, blocks, context);
|
||||
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.generation.noise.IrisGenerator;
|
||||
import art.arcane.iris.generation.noise.IrisInterpolator;
|
||||
import art.arcane.volmlib.util.interpolation.NoiseBounds;
|
||||
import art.arcane.volmlib.util.interpolation.NoiseBoundsProvider;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.SplittableRandom;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Answers.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
public class IrisComplexChunkCornersTest {
|
||||
@Test
|
||||
public void chunkCornerHeightsMatchPerGroupGridSamplingBitForBit() throws Exception {
|
||||
for (int step : new int[]{2, 4, 8, 16}) {
|
||||
IrisInterpolator[] interpolators = {
|
||||
new PatchInterpolator(0, 1D),
|
||||
new PatchInterpolator(1, -3D),
|
||||
new ZeroInterpolator(true),
|
||||
new PatchInterpolator(2, 0.5D),
|
||||
new ZeroInterpolator(false)
|
||||
};
|
||||
IrisGenerator[][] generators = {
|
||||
{new WaveGenerator(1D), new WaveGenerator(0.5D)},
|
||||
{new WaveGenerator(-0.25D)},
|
||||
{new WaveGenerator(2D)},
|
||||
{new WaveGenerator(0.75D), new WaveGenerator(-1D), new WaveGenerator(0.125D)},
|
||||
{new WaveGenerator(4D)}
|
||||
};
|
||||
IrisComplex candidate = createComplex(step, interpolators, generators);
|
||||
IrisComplex reference = createComplex(step, interpolators, generators);
|
||||
SplittableRandom random = new SplittableRandom(step * 31L);
|
||||
for (int sample = 0; sample < 60_000; sample++) {
|
||||
double x = coordinate(random);
|
||||
double z = coordinate(random);
|
||||
assertEquals("step " + step + " at " + x + "," + z,
|
||||
Double.doubleToRawLongBits(expectedHeight(reference, interpolators, generators, x, z)),
|
||||
Double.doubleToRawLongBits(interpolatedHeight(candidate, x, z)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void silentCellsNeverEvaluateTheirGenerators() throws Exception {
|
||||
WaveGenerator quiet = new WaveGenerator(1D);
|
||||
WaveGenerator loud = new WaveGenerator(1D);
|
||||
IrisInterpolator[] interpolators = {new ZeroInterpolator(true), new PatchInterpolator(0, 2D)};
|
||||
IrisGenerator[][] generators = {{quiet}, {loud}};
|
||||
IrisComplex complex = createComplex(4, interpolators, generators);
|
||||
|
||||
for (int x = 0; x < 64; x++) {
|
||||
for (int z = 0; z < 64; z++) {
|
||||
interpolatedHeight(complex, x, z);
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(0, quiet.invocations);
|
||||
assertTrue(loud.invocations > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sparseSamplesFetchOnlyTheCornersTheirColumnsUse() throws Exception {
|
||||
CountingInterpolator counting = new CountingInterpolator();
|
||||
IrisInterpolator[] interpolators = {counting};
|
||||
IrisGenerator[][] generators = {{new WaveGenerator(1D)}};
|
||||
IrisComplex complex = createComplex(4, interpolators, generators);
|
||||
|
||||
for (int sample = 0; sample < 32; sample++) {
|
||||
interpolatedHeight(complex, sample * 64D, sample * -64D);
|
||||
}
|
||||
assertEquals(32, counting.calls);
|
||||
|
||||
interpolatedHeight(complex, 2048D + 1D, 0D);
|
||||
assertEquals(34, counting.calls);
|
||||
interpolatedHeight(complex, 4096D, 1D);
|
||||
assertEquals(36, counting.calls);
|
||||
interpolatedHeight(complex, 8192D + 1D, 1D);
|
||||
assertEquals(40, counting.calls);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonFiniteCornersFailTheSameWayAndAreRetried() throws Exception {
|
||||
NaNCornerInterpolator interpolator = new NaNCornerInterpolator();
|
||||
IrisInterpolator[] interpolators = {interpolator};
|
||||
IrisGenerator[][] generators = {{new WaveGenerator(1D)}};
|
||||
IrisComplex complex = createComplex(4, interpolators, generators);
|
||||
|
||||
for (int attempt = 0; attempt < 2; attempt++) {
|
||||
try {
|
||||
interpolatedHeight(complex, 2D, 2D);
|
||||
fail("A non-finite corner must fail the column");
|
||||
} catch (InvocationTargetException expected) {
|
||||
assertTrue(expected.getCause() instanceof IllegalStateException);
|
||||
}
|
||||
}
|
||||
assertEquals(2, interpolator.nanCalls);
|
||||
IrisComplex reference = createComplex(4, interpolators, generators);
|
||||
assertEquals(Double.doubleToRawLongBits(expectedHeight(reference, interpolators, generators, 0D, 0D)),
|
||||
Double.doubleToRawLongBits(interpolatedHeight(complex, 0D, 0D)));
|
||||
}
|
||||
|
||||
private static double coordinate(SplittableRandom random) {
|
||||
int base = random.nextInt(-80, 80);
|
||||
return switch (random.nextInt(4)) {
|
||||
case 0 -> base;
|
||||
case 1 -> base + random.nextInt(4) * 0.25D;
|
||||
case 2 -> base + random.nextDouble();
|
||||
default -> base * 1031 + random.nextInt(-3, 3);
|
||||
};
|
||||
}
|
||||
|
||||
private static double expectedHeight(
|
||||
IrisComplex complex,
|
||||
IrisInterpolator[] interpolators,
|
||||
IrisGenerator[][] generators,
|
||||
double x,
|
||||
double z
|
||||
) throws Exception {
|
||||
Method interpolate = IrisComplex.class.getDeclaredMethod("interpolateGenerators", Engine.class,
|
||||
IrisInterpolator.class, int.class, IrisGenerator[].class, double.class, double.class, long.class);
|
||||
interpolate.setAccessible(true);
|
||||
double height = 0;
|
||||
for (int group = 0; group < interpolators.length; group++) {
|
||||
height += (double) interpolate.invoke(complex, null, interpolators[group], group, generators[group], x, z, 31L);
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
private static double interpolatedHeight(IrisComplex complex, double x, double z) throws Exception {
|
||||
Method method = IrisComplex.class.getDeclaredMethod("getInterpolatedHeight", Engine.class,
|
||||
double.class, double.class, long.class);
|
||||
method.setAccessible(true);
|
||||
return (double) method.invoke(complex, null, x, z, 31L);
|
||||
}
|
||||
|
||||
private static IrisComplex createComplex(int step, IrisInterpolator[] interpolators, IrisGenerator[][] generators) throws Exception {
|
||||
IrisComplex complex = mock(IrisComplex.class, CALLS_REAL_METHODS);
|
||||
setField(complex, "biomeBoundsSamplingStep", step);
|
||||
setField(complex, "generatorBounds", new HashMap<>());
|
||||
setField(complex, "biomeBuffetGenerators", new HashMap<>());
|
||||
setField(complex, "frozenInterpolators", interpolators);
|
||||
setField(complex, "frozenGenerators", generators);
|
||||
Class<?> cacheClass = Class.forName("art.arcane.iris.generation.runtime.IrisComplex$GridBoundsCache");
|
||||
Constructor<?> cacheConstructor = cacheClass.getDeclaredConstructor();
|
||||
cacheConstructor.setAccessible(true);
|
||||
setField(complex, "gridBoundsCache", ThreadLocal.withInitial(() -> newCache(cacheConstructor)));
|
||||
return complex;
|
||||
}
|
||||
|
||||
private static Object newCache(Constructor<?> constructor) {
|
||||
try {
|
||||
return constructor.newInstance();
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setField(IrisComplex complex, String name, Object value) throws Exception {
|
||||
Field field = IrisComplex.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(complex, value);
|
||||
}
|
||||
|
||||
private static final class PatchInterpolator extends IrisInterpolator {
|
||||
private final int salt;
|
||||
private final double scale;
|
||||
|
||||
private PatchInterpolator(int salt, double scale) {
|
||||
this.salt = salt;
|
||||
this.scale = scale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NoiseBounds interpolateBounds(double x, double z, NoiseBoundsProvider provider) {
|
||||
long cell = Math.floorDiv((long) x, 24L) * 73_856_093L ^ Math.floorDiv((long) z, 24L) * 19_349_663L ^ salt * 83_492_791L;
|
||||
if (Math.floorMod(cell, 3L) != 0L) {
|
||||
return new NoiseBounds(0D, 0D);
|
||||
}
|
||||
double low = (x * 0.0625D - z * 0.03125D) * scale;
|
||||
return new NoiseBounds(low, low + 3.75D * scale + Math.floorMod(cell, 5L));
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ZeroInterpolator extends IrisInterpolator {
|
||||
private final boolean negative;
|
||||
|
||||
private ZeroInterpolator(boolean negative) {
|
||||
this.negative = negative;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NoiseBounds interpolateBounds(double x, double z, NoiseBoundsProvider provider) {
|
||||
return negative ? new NoiseBounds(-0D, 0D) : new NoiseBounds(0D, -0D);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CountingInterpolator extends IrisInterpolator {
|
||||
private int calls;
|
||||
|
||||
@Override
|
||||
public NoiseBounds interpolateBounds(double x, double z, NoiseBoundsProvider provider) {
|
||||
calls++;
|
||||
return new NoiseBounds(x * 0.001D, x * 0.001D + 2D);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class NaNCornerInterpolator extends IrisInterpolator {
|
||||
private int nanCalls;
|
||||
|
||||
@Override
|
||||
public NoiseBounds interpolateBounds(double x, double z, NoiseBoundsProvider provider) {
|
||||
if (x == 4D && z == 4D) {
|
||||
nanCalls++;
|
||||
return new NoiseBounds(Double.NaN, 1D);
|
||||
}
|
||||
return new NoiseBounds(0.25D, 0.5D);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class WaveGenerator extends IrisGenerator {
|
||||
private final double amplitude;
|
||||
private int invocations;
|
||||
|
||||
private WaveGenerator(double amplitude) {
|
||||
this.amplitude = amplitude;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getHeight(double x, double z, long seed) {
|
||||
invocations++;
|
||||
return Math.sin(x * 0.37D + seed) * Math.cos(z * 0.21D) * amplitude;
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -175,7 +175,6 @@ public class IrisTerrainNormalActuatorColumnParityTest {
|
||||
when(complex.terrainColumn(eq(X), eq(Z), hydrology == null ? isNull() : any(HydrologyColumnSample.class)))
|
||||
.thenReturn(terrainColumn);
|
||||
when(complex.naturalTerrainColumn(X, Z)).thenReturn(terrainColumn);
|
||||
when(complex.resolveSurfaceFluid(anyDouble(), anyDouble())).thenReturn(fluid);
|
||||
when(context.getRoundedHeight(0, 0)).thenReturn(he);
|
||||
when(context.getBiome()).thenReturn(new ChunkedDataCache<>(
|
||||
ProceduralStream.of((x, z) -> biome, Interpolated.of(value -> 0D, value -> biome)), X, Z, false));
|
||||
@@ -183,6 +182,8 @@ public class IrisTerrainNormalActuatorColumnParityTest {
|
||||
ProceduralStream.of((x, z) -> region, Interpolated.of(value -> 0D, value -> region)), X, Z, false));
|
||||
when(context.getRock()).thenReturn(new ChunkedDataCache<>(
|
||||
ProceduralStream.of((x, z) -> rock, Interpolated.of(value -> 0D, value -> rock)), X, Z, false));
|
||||
when(context.getFluid()).thenReturn(new ChunkedDataCache<>(
|
||||
ProceduralStream.of((x, z) -> fluid, Interpolated.of(value -> 0D, value -> fluid)), X, Z, false));
|
||||
when(biome.generateLayers(eq(dimension), anyDouble(), anyDouble(), any(RNG.class),
|
||||
anyInt(), anyInt(), eq(data), eq(complex))).thenAnswer(invocation -> layers("layer", layerCount));
|
||||
when(biome.generateSeaLayers(anyDouble(), anyDouble(), any(RNG.class), anyInt(), eq(data)))
|
||||
|
||||
+2
@@ -259,6 +259,8 @@ public class IrisTerrainNormalActuatorHydrologyTest {
|
||||
ProceduralStream.of((x, z) -> region, Interpolated.of(value -> 0D, value -> region)), 11, -4, false));
|
||||
when(context.getRock()).thenReturn(new ChunkedDataCache<>(
|
||||
ProceduralStream.of((x, z) -> rock, Interpolated.of(value -> 0D, value -> rock)), 11, -4, false));
|
||||
when(context.getFluid()).thenReturn(new ChunkedDataCache<>(
|
||||
ProceduralStream.of((x, z) -> null, Interpolated.of(value -> 0D, value -> null)), 11, -4, false));
|
||||
when(biome.generateLayers(eq(dimension), anyDouble(), anyDouble(), any(RNG.class),
|
||||
anyInt(), anyInt(), eq(data), eq(complex))).thenReturn(new KList<>(List.of(grass)));
|
||||
Hunk<NativeBlockState> output = Hunk.newArrayHunk(1, 64, 1);
|
||||
|
||||
+2
@@ -155,6 +155,8 @@ public class IrisTerrainNormalActuatorStrataCutTest {
|
||||
ProceduralStream.of((x, z) -> region, Interpolated.of(value -> 0D, value -> region)), 11, -4, false));
|
||||
when(context.getRock()).thenReturn(new ChunkedDataCache<>(
|
||||
ProceduralStream.of((x, z) -> rock, Interpolated.of(value -> 0D, value -> rock)), 11, -4, false));
|
||||
when(context.getFluid()).thenReturn(new ChunkedDataCache<>(
|
||||
ProceduralStream.of((x, z) -> null, Interpolated.of(value -> 0D, value -> null)), 11, -4, false));
|
||||
when(biome.generateLayers(eq(dimension), anyDouble(), anyDouble(), any(RNG.class),
|
||||
anyInt(), anyInt(), eq(data), eq(complex))).thenReturn(new KList<>(List.of(grass, dirt)));
|
||||
when(biome.generateCeilingLayers(eq(dimension), anyDouble(), anyDouble(), any(RNG.class),
|
||||
|
||||
Reference in new issue
Block a user