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:
Brian Neumann-Fopiano committed 2026-10-21 19:53:23 -04:00
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);