Baked carve field reads

- ChunkBakeCache.fillColumns batches the missing requested columns of one chunk plane and publishes them
- BakedDoubleStream and BakedStream expose fillColumns with the per-column fallback when memoization is not allowed
- Carve profile field reads heights, regions, surface and cave biomes from the nine overlapped chunk planes
- Fluid presence samples the fluid plane only for columns below the fluid height
- Carving reuses one dimension carving resolver state per thread scratch
- Bake recent-entry memo holds a 16-slot per-thread chunk table instead of one entry
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-22 07:14:09 -04:00
1 parent af90765a92
commit 67fd9046ba
7 files changed
+597 -34

No files matched your search

@@ -3,6 +3,7 @@ package art.arcane.iris.generation.bake;
import art.arcane.iris.generation.cache.ConcurrentClockCache; import art.arcane.iris.generation.cache.ConcurrentClockCache;
import art.arcane.iris.generation.stream.ProvisionalSampling; import art.arcane.iris.generation.stream.ProvisionalSampling;
import java.util.Arrays;
import java.util.Objects; import java.util.Objects;
public final class ChunkBakeCache { public final class ChunkBakeCache {
@@ -10,6 +11,7 @@ public final class ChunkBakeCache {
static final int MAXIMUM_CAPACITY = 32_768; static final int MAXIMUM_CAPACITY = 32_768;
static final long BYTES_PER_ENTRY = 13L * 1_024L; static final long BYTES_PER_ENTRY = 13L * 1_024L;
static final int HEAP_SHARE = 32; static final int HEAP_SHARE = 32;
private static final int RECENT_SLOTS = 16;
private static final ThreadLocal<int[]> SPARSE = ThreadLocal.withInitial(() -> new int[1]); private static final ThreadLocal<int[]> SPARSE = ThreadLocal.withInitial(() -> new int[1]);
private final PlaneSource source; private final PlaneSource source;
@@ -52,17 +54,16 @@ public final class ChunkBakeCache {
public ChunkPlanes.Plane planeIfPresent(BakePlane plane, int chunkX, int chunkZ) { public ChunkPlanes.Plane planeIfPresent(BakePlane plane, int chunkX, int chunkZ) {
long key = key(chunkX, chunkZ); long key = key(chunkX, chunkZ);
int slot = Recent.slot(chunkX, chunkZ);
Recent last = recent.get(); Recent last = recent.get();
int current = generation; int current = generation;
ChunkPlanes planes; ChunkPlanes planes = last.get(key, slot, current);
if (last.key == key && last.generation == current) { if (planes == null) {
planes = last.planes;
} else {
planes = entries.get(key); planes = entries.get(key);
if (planes == null) { if (planes == null) {
return null; return null;
} }
last.remember(key, planes, current); last.remember(key, slot, planes, current);
} }
return planes.planeIfPresent(plane); return planes.planeIfPresent(plane);
} }
@@ -75,6 +76,10 @@ public final class ChunkBakeCache {
return remembered(chunkX, chunkZ).complete(plane, source); return remembered(chunkX, chunkZ).complete(plane, source);
} }
public void fillColumns(BakePlane plane, int chunkX, int chunkZ, long[] columns, Object target) {
remembered(chunkX, chunkZ).plane(plane).fillColumns(source, chunkX, chunkZ, columns, target);
}
public boolean memoAllowed() { public boolean memoAllowed() {
return SPARSE.get()[0] == 0 && !source.mainThread(); return SPARSE.get()[0] == 0 && !source.mainThread();
} }
@@ -102,7 +107,7 @@ public final class ChunkBakeCache {
private ChunkPlanes remembered(int chunkX, int chunkZ) { private ChunkPlanes remembered(int chunkX, int chunkZ) {
int current = generation; int current = generation;
ChunkPlanes planes = planes(chunkX, chunkZ); ChunkPlanes planes = planes(chunkX, chunkZ);
recent.get().remember(key(chunkX, chunkZ), planes, current); recent.get().remember(key(chunkX, chunkZ), Recent.slot(chunkX, chunkZ), planes, current);
return planes; return planes;
} }
@@ -123,14 +128,25 @@ public final class ChunkBakeCache {
} }
private static final class Recent { private static final class Recent {
private long key; private final long[] keys = new long[RECENT_SLOTS];
private final ChunkPlanes[] planes = new ChunkPlanes[RECENT_SLOTS];
private int generation = -1; private int generation = -1;
private ChunkPlanes planes;
private void remember(long key, ChunkPlanes planes, int generation) { private static int slot(int chunkX, int chunkZ) {
this.key = key; return ((chunkZ & 3) << 2) | (chunkX & 3);
this.planes = planes; }
this.generation = generation;
private ChunkPlanes get(long key, int slot, int generation) {
return this.generation == generation && keys[slot] == key ? planes[slot] : null;
}
private void remember(long key, int slot, ChunkPlanes value, int generation) {
if (this.generation != generation) {
Arrays.fill(planes, null);
this.generation = generation;
}
keys[slot] = key;
planes[slot] = value;
} }
} }
} }
@@ -145,6 +145,55 @@ public final class ChunkPlanes {
return values; return values;
} }
void fillColumns(PlaneSource source, int chunkX, int chunkZ, long[] columns, Object target) {
long[] missing = new long[4];
boolean any = false;
if (!complete) {
for (int word = 0; word < 4; word++) {
missing[word] = columns[word] & ~(long) READY.getAcquire(ready, word);
any |= missing[word] != 0L;
}
}
if (!any) {
copyColumns(columns, values, target);
return;
}
long revision = ProvisionalSampling.revision();
source.fill(kind, chunkX, chunkZ, missing, target);
long[] known = new long[4];
for (int word = 0; word < 4; word++) {
known[word] = columns[word] & ~missing[word];
}
copyColumns(known, values, target);
if (revision == ProvisionalSampling.revision()) {
publish(missing, target);
}
}
private void publish(long[] missing, Object computed) {
copyColumns(missing, computed, values);
for (int word = 0; word < 4; word++) {
if (missing[word] != 0L) {
READY.getAndBitwiseOrRelease(ready, word, missing[word]);
}
}
}
private void copyColumns(long[] columns, Object from, Object to) {
for (int word = 0; word < 4; word++) {
long bits = columns[word];
while (bits != 0L) {
int index = (word << 6) | Long.numberOfTrailingZeros(bits);
if (kind.doubles()) {
((double[]) to)[index] = ((double[]) from)[index];
} else {
((Object[]) to)[index] = ((Object[]) from)[index];
}
bits &= bits - 1L;
}
}
}
private Object unpublishedFill(PlaneSource source, int chunkX, int chunkZ) { private Object unpublishedFill(PlaneSource source, int chunkX, int chunkZ) {
long[] missing = missing(); long[] missing = missing();
Object computed = kind.doubles() ? new double[256] : new Object[256]; Object computed = kind.doubles() ? new double[256] : new Object[256];
@@ -34,6 +34,8 @@ import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.generation.block.B; import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.context.ChunkContext; import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.volmlib.util.stream.ProceduralStream; import art.arcane.volmlib.util.stream.ProceduralStream;
import art.arcane.iris.generation.stream.BakedDoubleStream;
import art.arcane.iris.generation.stream.BakedStream;
import art.arcane.iris.generation.stream.ChunkFillableDoubleStream2D; import art.arcane.iris.generation.stream.ChunkFillableDoubleStream2D;
import art.arcane.iris.generation.simd.SimdSupport; import art.arcane.iris.generation.simd.SimdSupport;
import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.documentation.ChunkCoordinates;
@@ -64,6 +66,12 @@ public class MantleCarvingComponent extends IrisMantleComponent {
private static final int[] KERNEL_DX = new int[KERNEL_SIZE]; private static final int[] KERNEL_DX = new int[KERNEL_SIZE];
private static final int[] KERNEL_DZ = new int[KERNEL_SIZE]; private static final int[] KERNEL_DZ = new int[KERNEL_SIZE];
private static final double[] KERNEL_WEIGHT = new double[KERNEL_SIZE]; private static final double[] KERNEL_WEIGHT = new double[KERNEL_SIZE];
private static final int NEIGHBOURS = 9;
private static final int[] NEIGHBOUR_DX = new int[NEIGHBOURS];
private static final int[] NEIGHBOUR_DZ = new int[NEIGHBOURS];
private static final long[][] NEIGHBOUR_COLUMNS = new long[NEIGHBOURS][4];
private static final int[] FIELD_NEIGHBOUR = new int[FIELD_SIZE * FIELD_SIZE];
private static final int[] FIELD_COLUMN = new int[FIELD_SIZE * FIELD_SIZE];
private static final ThreadLocal<BlendScratch> BLEND_SCRATCH = ThreadLocal.withInitial(BlendScratch::new); private static final ThreadLocal<BlendScratch> BLEND_SCRATCH = ThreadLocal.withInitial(BlendScratch::new);
private final Object profileCarverLock = new Object(); private final Object profileCarverLock = new Object();
@@ -81,6 +89,22 @@ public class MantleCarvingComponent extends IrisMantleComponent {
kernelIndex++; kernelIndex++;
} }
} }
for (int neighbour = 0; neighbour < NEIGHBOURS; neighbour++) {
NEIGHBOUR_DX[neighbour] = (neighbour / 3) - 1;
NEIGHBOUR_DZ[neighbour] = (neighbour % 3) - 1;
}
for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) {
int offsetX = fieldX - BLEND_RADIUS;
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
int offsetZ = fieldZ - BLEND_RADIUS;
int neighbour = ((Math.floorDiv(offsetX, CHUNK_SIZE) + 1) * 3) + Math.floorDiv(offsetZ, CHUNK_SIZE) + 1;
int column = ((offsetZ & (CHUNK_SIZE - 1)) << 4) | (offsetX & (CHUNK_SIZE - 1));
int fieldIndex = (fieldX * FIELD_SIZE) + fieldZ;
FIELD_NEIGHBOUR[fieldIndex] = neighbour;
FIELD_COLUMN[fieldIndex] = column;
NEIGHBOUR_COLUMNS[neighbour][column >>> 6] |= 1L << (column & 63);
}
}
} }
public MantleCarvingComponent(EngineMantle engineMantle) { public MantleCarvingComponent(EngineMantle engineMantle) {
@@ -128,7 +152,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
private void generateLayer(MantleWriter writer, int x, int z, ChunkContext context, BlendScratch blendScratch) { private void generateLayer(MantleWriter writer, int x, int z, ChunkContext context, BlendScratch blendScratch) {
IrisComplex complex = context.getComplex(); IrisComplex complex = context.getComplex();
IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State(); IrisDimensionCarvingResolver.State resolverState = blendScratch.resolverState;
int[] chunkSurfaceHeights = prepareChunkSurfaceHeights(x, z, context, blendScratch.chunkSurfaceHeights); int[] chunkSurfaceHeights = prepareChunkSurfaceHeights(x, z, context, blendScratch.chunkSurfaceHeights);
PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start(); PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start();
List<WeightedProfile> weightedProfiles = resolveWeightedProfiles(x, z, complex, resolverState); List<WeightedProfile> weightedProfiles = resolveWeightedProfiles(x, z, complex, resolverState);
@@ -422,7 +446,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
private void fillProfileField(IrisCaveProfile[] profileField, int chunkX, int chunkZ, IrisComplex complex, IrisDimensionCarvingResolver.State resolverState, BlendScratch blendScratch) { private void fillProfileField(IrisCaveProfile[] profileField, int chunkX, int chunkZ, IrisComplex complex, IrisDimensionCarvingResolver.State resolverState, BlendScratch blendScratch) {
int startX = PowerOfTwoCoordinates.chunkToBlock(chunkX) - BLEND_RADIUS; int startX = PowerOfTwoCoordinates.chunkToBlock(chunkX) - BLEND_RADIUS;
int startZ = PowerOfTwoCoordinates.chunkToBlock(chunkZ) - BLEND_RADIUS; int startZ = PowerOfTwoCoordinates.chunkToBlock(chunkZ) - BLEND_RADIUS;
prefillProfileFieldSamples(startX, startZ, complex, blendScratch); prefillProfileFieldSamples(chunkX, chunkZ, startX, startZ, complex, blendScratch);
for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) { for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) {
int worldX = startX + fieldX; int worldX = startX + fieldX;
@@ -519,17 +543,28 @@ public class MantleCarvingComponent extends IrisMantleComponent {
return resolved; return resolved;
} }
private void prefillProfileFieldSamples(int startX, int startZ, IrisComplex complex, BlendScratch blendScratch) { private void prefillProfileFieldSamples(int chunkX, int chunkZ, int startX, int startZ, IrisComplex complex, BlendScratch blendScratch) {
fillFieldHeights(complex.getNaturalHeightStream(), startX, startZ, blendScratch.fieldSurfaceHeights); fillFieldHeights(complex.getNaturalHeightStream(), chunkX, chunkZ, startX, startZ, blendScratch);
Arrays.fill(blendScratch.fieldFluidHeights, complex.getFluidHeight()); double fluidHeight = complex.getFluidHeight();
fillFieldFluidPresence(complex, startX, startZ, blendScratch.fieldSurfaceHeights, Arrays.fill(blendScratch.fieldFluidHeights, fluidHeight);
blendScratch.fieldFluidHeights, blendScratch.fieldHasFluid); fillFieldFluidPresence(complex.getFluidStream(), chunkX, chunkZ, startX, startZ, Math.round(fluidHeight), blendScratch);
fillFieldObjects(complex.getRegionStream(), startX, startZ, blendScratch.fieldRegions); fillFieldObjects(complex.getRegionStream(), chunkX, chunkZ, startX, startZ, blendScratch.fieldRegions, blendScratch.neighbourObjects);
fillFieldObjects(complex.getNaturalTrueBiomeStream(), startX, startZ, blendScratch.fieldSurfaceBiomes); fillFieldObjects(complex.getNaturalTrueBiomeStream(), chunkX, chunkZ, startX, startZ, blendScratch.fieldSurfaceBiomes, blendScratch.neighbourObjects);
fillFieldObjects(complex.getCaveBiomeStream(), startX, startZ, blendScratch.fieldCaveBiomes); fillFieldObjects(complex.getCaveBiomeStream(), chunkX, chunkZ, startX, startZ, blendScratch.fieldCaveBiomes, blendScratch.neighbourObjects);
} }
private <T> void fillFieldObjects(ProceduralStream<T> stream, int startX, int startZ, T[] target) { @SuppressWarnings("unchecked")
private <T> void fillFieldObjects(ProceduralStream<T> stream, int chunkX, int chunkZ, int startX, int startZ, T[] target, Object[][] neighbourObjects) {
if (stream instanceof BakedStream<?> baked) {
for (int neighbour = 0; neighbour < NEIGHBOURS; neighbour++) {
baked.fillColumns(chunkX + NEIGHBOUR_DX[neighbour], chunkZ + NEIGHBOUR_DZ[neighbour],
NEIGHBOUR_COLUMNS[neighbour], neighbourObjects[neighbour]);
}
for (int fieldIndex = 0; fieldIndex < target.length; fieldIndex++) {
target[fieldIndex] = (T) neighbourObjects[FIELD_NEIGHBOUR[fieldIndex]][FIELD_COLUMN[fieldIndex]];
}
return;
}
for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) { for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) {
int worldX = startX + fieldX; int worldX = startX + fieldX;
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) { for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
@@ -538,7 +573,19 @@ public class MantleCarvingComponent extends IrisMantleComponent {
} }
} }
private void fillFieldHeights(ProceduralStream<Double> stream, int startX, int startZ, double[] target) { private void fillFieldHeights(ProceduralStream<Double> stream, int chunkX, int chunkZ, int startX, int startZ, BlendScratch blendScratch) {
double[] target = blendScratch.fieldSurfaceHeights;
if (stream instanceof BakedDoubleStream baked) {
double[][] neighbourHeights = blendScratch.neighbourHeights;
for (int neighbour = 0; neighbour < NEIGHBOURS; neighbour++) {
baked.fillColumns(chunkX + NEIGHBOUR_DX[neighbour], chunkZ + NEIGHBOUR_DZ[neighbour],
NEIGHBOUR_COLUMNS[neighbour], neighbourHeights[neighbour]);
}
for (int fieldIndex = 0; fieldIndex < target.length; fieldIndex++) {
target[fieldIndex] = neighbourHeights[FIELD_NEIGHBOUR[fieldIndex]][FIELD_COLUMN[fieldIndex]];
}
return;
}
for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) { for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) {
int worldX = startX + fieldX; int worldX = startX + fieldX;
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) { for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
@@ -547,20 +594,41 @@ public class MantleCarvingComponent extends IrisMantleComponent {
} }
} }
private void fillFieldFluidPresence( private void fillFieldFluidPresence(ProceduralStream<NativeBlockState> stream, int chunkX, int chunkZ, int startX, int startZ,
IrisComplex complex, long roundedFluidHeight, BlendScratch blendScratch) {
int startX, double[] surfaceHeights = blendScratch.fieldSurfaceHeights;
int startZ, boolean[] target = blendScratch.fieldHasFluid;
double[] surfaceHeights, if (stream instanceof BakedStream<?> baked) {
double[] fluidHeights, long[][] submerged = blendScratch.neighbourSubmerged;
boolean[] target for (long[] columns : submerged) {
) { Arrays.fill(columns, 0L);
}
for (int fieldIndex = 0; fieldIndex < target.length; fieldIndex++) {
if (Math.round(surfaceHeights[fieldIndex]) < roundedFluidHeight) {
int column = FIELD_COLUMN[fieldIndex];
submerged[FIELD_NEIGHBOUR[fieldIndex]][column >>> 6] |= 1L << (column & 63);
}
}
Object[][] neighbourObjects = blendScratch.neighbourObjects;
for (int neighbour = 0; neighbour < NEIGHBOURS; neighbour++) {
long[] columns = submerged[neighbour];
if ((columns[0] | columns[1] | columns[2] | columns[3]) != 0L) {
baked.fillColumns(chunkX + NEIGHBOUR_DX[neighbour], chunkZ + NEIGHBOUR_DZ[neighbour],
columns, neighbourObjects[neighbour]);
}
}
for (int fieldIndex = 0; fieldIndex < target.length; fieldIndex++) {
target[fieldIndex] = Math.round(surfaceHeights[fieldIndex]) < roundedFluidHeight
&& B.isFluid((NativeBlockState) neighbourObjects[FIELD_NEIGHBOUR[fieldIndex]][FIELD_COLUMN[fieldIndex]]);
}
return;
}
for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) { for (int fieldX = 0; fieldX < FIELD_SIZE; fieldX++) {
int worldX = startX + fieldX; int worldX = startX + fieldX;
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) { for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
int fieldIndex = (fieldX * FIELD_SIZE) + fieldZ; int fieldIndex = (fieldX * FIELD_SIZE) + fieldZ;
target[fieldIndex] = B.isFluid(complex.getFluidStream().get(worldX, startZ + fieldZ)) target[fieldIndex] = B.isFluid(stream.get(worldX, startZ + fieldZ))
&& Math.round(surfaceHeights[fieldIndex]) < Math.round(fluidHeights[fieldIndex]); && Math.round(surfaceHeights[fieldIndex]) < roundedFluidHeight;
} }
} }
} }
@@ -682,6 +750,10 @@ public class MantleCarvingComponent extends IrisMantleComponent {
private final IrisBiome[] fieldCaveBiomes = new IrisBiome[FIELD_SIZE * FIELD_SIZE]; private final IrisBiome[] fieldCaveBiomes = new IrisBiome[FIELD_SIZE * FIELD_SIZE];
private final int[] chunkSurfaceHeights = new int[CHUNK_AREA]; private final int[] chunkSurfaceHeights = new int[CHUNK_AREA];
private final double[] chunkSurfaceHeightSamples = new double[CHUNK_AREA]; private final double[] chunkSurfaceHeightSamples = new double[CHUNK_AREA];
private final double[][] neighbourHeights = new double[NEIGHBOURS][CHUNK_AREA];
private final Object[][] neighbourObjects = new Object[NEIGHBOURS][CHUNK_AREA];
private final long[][] neighbourSubmerged = new long[NEIGHBOURS][4];
private final IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State();
private int nextWeightBuffer; private int nextWeightBuffer;
private boolean inUse; private boolean inUse;
@@ -705,6 +777,9 @@ public class MantleCarvingComponent extends IrisMantleComponent {
Arrays.fill(fieldRegions, null); Arrays.fill(fieldRegions, null);
Arrays.fill(fieldSurfaceBiomes, null); Arrays.fill(fieldSurfaceBiomes, null);
Arrays.fill(fieldCaveBiomes, null); Arrays.fill(fieldCaveBiomes, null);
for (Object[] objects : neighbourObjects) {
Arrays.fill(objects, null);
}
columnProfileWeights.clear(); columnProfileWeights.clear();
profileOrder.clear(); profileOrder.clear();
nextWeightBuffer = 0; nextWeightBuffer = 0;
@@ -81,6 +81,21 @@ public final class BakedDoubleStream extends BasicStream<Double> implements Chun
} }
} }
public void fillColumns(int chunkX, int chunkZ, long[] columns, double[] target) {
if (cache.memoAllowed()) {
cache.fillColumns(plane, chunkX, chunkZ, columns, target);
return;
}
int originX = chunkX << 4;
int originZ = chunkZ << 4;
for (int word = 0; word < 4; word++) {
for (long bits = columns[word]; bits != 0L; bits &= bits - 1L) {
int index = (word << 6) | Long.numberOfTrailingZeros(bits);
target[index] = getDouble(originX + (index & 15), originZ + (index >> 4));
}
}
}
@Override @Override
public void fillChunkRaw(int worldX, int worldZ, Object[] target) { public void fillChunkRaw(int worldX, int worldZ, Object[] target) {
double[] values = new double[256]; double[] values = new double[256];
@@ -54,6 +54,21 @@ public final class BakedStream<T> extends BasicStream<T> implements ChunkFillabl
return scalar.fromDouble(d); return scalar.fromDouble(d);
} }
public void fillColumns(int chunkX, int chunkZ, long[] columns, Object[] target) {
if (cache.memoAllowed()) {
cache.fillColumns(plane, chunkX, chunkZ, columns, target);
return;
}
int originX = chunkX << 4;
int originZ = chunkZ << 4;
for (int word = 0; word < 4; word++) {
for (long bits = columns[word]; bits != 0L; bits &= bits - 1L) {
int index = (word << 6) | Long.numberOfTrailingZeros(bits);
target[index] = get(originX + (index & 15), originZ + (index >> 4));
}
}
}
@Override @Override
public void fillChunkRaw(int worldX, int worldZ, Object[] target) { public void fillChunkRaw(int worldX, int worldZ, Object[] target) {
int chunkX = worldX >> 4; int chunkX = worldX >> 4;
@@ -1,7 +1,9 @@
package art.arcane.iris.generation.bake; package art.arcane.iris.generation.bake;
import art.arcane.iris.generation.stream.ProvisionalSampling;
import org.junit.Test; import org.junit.Test;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNotSame;
@@ -87,6 +89,155 @@ public class ChunkBakeCacheTest {
assertEquals(64, cache.capacity()); assertEquals(64, cache.capacity());
} }
@Test
public void rememberedPlanesAnswerOnlyForTheirOwnChunkAndOnlyUntilClear() {
ChunkBakeCache cache = new ChunkBakeCache(new ConstantSource(), 256);
int[][] chunks = {{0, 0}, {1, 0}, {0, 1}, {4, 0}, {0, 4}, {-4, -4}, {3, 3}, {7, 7}};
for (int[] chunk : chunks) {
cache.complete(BakePlane.BASE_HEIGHT, chunk[0], chunk[1]);
}
for (int round = 0; round < 2; round++) {
for (int[] chunk : chunks) {
ChunkPlanes.Plane plane = cache.planeIfPresent(BakePlane.BASE_HEIGHT, chunk[0], chunk[1]);
assertEquals(chunk[0] * 1_000D + chunk[1] + 5D, plane.doubleAt(5), 0D);
}
}
assertNull(cache.planeIfPresent(BakePlane.BASE_HEIGHT, 8, 8));
cache.clear();
for (int[] chunk : chunks) {
assertNull(cache.planeIfPresent(BakePlane.BASE_HEIGHT, chunk[0], chunk[1]));
}
}
@Test
public void fillColumnsComputesOnlyTheMissingRequestedColumnsAndPublishesThem() {
RecordingSource source = new RecordingSource();
ChunkBakeCache cache = new ChunkBakeCache(source, 64);
long[] strip = columns(13, 0, 3, 16);
double[] target = new double[256];
cache.fillColumns(BakePlane.NATURAL_HEIGHT, 2, -1, strip, target);
assertEquals(1, source.calls);
assertArrayEquals(strip, source.lastMissing);
ChunkPlanes.Plane plane = cache.planeIfPresent(BakePlane.NATURAL_HEIGHT, 2, -1);
for (int index = 0; index < 256; index++) {
boolean requested = (strip[index >>> 6] & (1L << (index & 63))) != 0L;
assertEquals(requested, plane.ready(index));
if (requested) {
assertEquals(RecordingSource.value(2, -1, index), target[index], 0D);
}
}
long[] corner = columns(13, 13, 3, 3);
long[] wider = columns(10, 13, 6, 3);
double[] second = new double[256];
cache.fillColumns(BakePlane.NATURAL_HEIGHT, 2, -1, wider, second);
assertEquals(2, source.calls);
long[] expected = new long[4];
for (int word = 0; word < 4; word++) {
expected[word] = wider[word] & ~corner[word];
}
assertArrayEquals(expected, source.lastMissing);
for (int index = 0; index < 256; index++) {
if ((wider[index >>> 6] & (1L << (index & 63))) != 0L) {
assertEquals(RecordingSource.value(2, -1, index), second[index], 0D);
}
}
cache.fillColumns(BakePlane.NATURAL_HEIGHT, 2, -1, corner, new double[256]);
assertEquals(2, source.calls);
assertArrayEquals(columns(13, 0, 3, 16), strip);
assertArrayEquals(columns(10, 13, 6, 3), wider);
}
@Test
public void fillColumnsReadsCompletedPlanesWithoutRecomputing() {
RecordingSource source = new RecordingSource();
ChunkBakeCache cache = new ChunkBakeCache(source, 64);
cache.complete(BakePlane.REGION, -4, 7);
Object[] target = new Object[256];
cache.fillColumns(BakePlane.REGION, -4, 7, columns(0, 0, 16, 3), target);
assertEquals(1, source.calls);
assertEquals(RecordingSource.label(-4, 7, 37), target[37]);
assertNull(target[3 * 16]);
}
@Test
public void fillColumnsHandsBackProvisionalValuesWithoutPublishingThem() {
RecordingSource source = new RecordingSource();
source.provisional = true;
ChunkBakeCache cache = new ChunkBakeCache(source, 64);
long[] block = columns(4, 4, 2, 2);
double[] target = new double[256];
cache.fillColumns(BakePlane.NATURAL_HEIGHT, 0, 0, block, target);
int index = (4 << 4) | 4;
assertEquals(RecordingSource.value(0, 0, index), target[index], 0D);
assertTrue(cache.planeIfPresent(BakePlane.NATURAL_HEIGHT, 0, 0) == null
|| !cache.planeIfPresent(BakePlane.NATURAL_HEIGHT, 0, 0).ready(index));
source.provisional = false;
cache.fillColumns(BakePlane.NATURAL_HEIGHT, 0, 0, block, new double[256]);
assertEquals(2, source.calls);
assertTrue(cache.planeIfPresent(BakePlane.NATURAL_HEIGHT, 0, 0).ready(index));
}
private static long[] columns(int x0, int z0, int width, int depth) {
long[] mask = new long[4];
for (int x = x0; x < x0 + width; x++) {
for (int z = z0; z < z0 + depth; z++) {
int index = (z << 4) | x;
mask[index >>> 6] |= 1L << (index & 63);
}
}
return mask;
}
private static final class RecordingSource implements PlaneSource {
private int calls;
private long[] lastMissing;
private boolean provisional;
private static double value(int chunkX, int chunkZ, int index) {
return chunkX * 10_000D + chunkZ * 1_000D + index + 0.25D;
}
private static String label(int chunkX, int chunkZ, int index) {
return chunkX + ":" + chunkZ + ":" + index;
}
@Override
public void fill(BakePlane plane, int chunkX, int chunkZ, long[] missing, Object values) {
calls++;
lastMissing = missing.clone();
if (provisional) {
ProvisionalSampling.mark();
}
for (int index = 0; index < 256; index++) {
if ((missing[index >>> 6] & (1L << (index & 63))) == 0L) {
continue;
}
if (values instanceof double[] doubles) {
doubles[index] = value(chunkX, chunkZ, index);
} else {
((Object[]) values)[index] = label(chunkX, chunkZ, index);
}
}
}
@Override
public boolean mainThread() {
return false;
}
}
private static final class ConstantSource implements PlaneSource { private static final class ConstantSource implements PlaneSource {
@Override @Override
public void fill(BakePlane plane, int chunkX, int chunkZ, long[] missing, Object values) { public void fill(BakePlane plane, int chunkX, int chunkZ, long[] missing, Object values) {
@@ -0,0 +1,242 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.bake.BakePlane;
import art.arcane.iris.generation.bake.ChunkBakeCache;
import art.arcane.iris.generation.bake.PlaneSource;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.cave.IrisCaveProfile;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EngineMetrics;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.stream.BakedDoubleStream;
import art.arcane.iris.generation.stream.BakedStream;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.iris.world.IrisWorld;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.stream.ProceduralStream;
import org.junit.Test;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class MantleCarvingComponentBakedFieldTest {
private static final double FLUID_HEIGHT = 63D;
private static final int[][] ORDER = {
{1, 1}, {-2, 0}, {0, 0}, {2, -1}, {-1, -1}, {0, 1}, {1, -2}, {-2, 2}, {2, 2}, {-1, 1}, {0, -1}, {1, 0}
};
private final IrisCaveProfile dimensionProfile = new IrisCaveProfile().setEnabled(true);
private final IrisCaveProfile regionProfile = new IrisCaveProfile().setEnabled(true);
private final IrisCaveProfile surfaceProfile = new IrisCaveProfile().setEnabled(true);
private final IrisCaveProfile shallowCaveProfile = new IrisCaveProfile().setEnabled(true);
private final IrisCaveProfile deepCaveProfile = new IrisCaveProfile().setEnabled(true);
private final IrisRegion profiledRegion = new IrisRegion().setCaveProfile(regionProfile);
private final IrisRegion plainRegion = new IrisRegion();
private final IrisBiome profiledSurface = new IrisBiome().setCaveProfile(surfaceProfile);
private final IrisBiome plainSurface = new IrisBiome();
private final IrisBiome shallowCave = new IrisBiome().setCaveProfile(shallowCaveProfile);
private final IrisBiome deepCave = new IrisBiome().setCaveProfile(deepCaveProfile).setCaveMinDepthBelowSurface(60);
private final IrisBiome plainCave = new IrisBiome();
private final NativeBlockState water = fluid(true);
private final NativeBlockState air = fluid(false);
@Test
public void bakedFieldReadsMatchPerColumnStreamReads() throws Exception {
List<String> plain = run(false, false);
List<String> baked = run(true, false);
List<String> warmed = run(true, true);
assertTrue(plain.size() > ORDER.length);
assertEquals(plain, baked);
assertEquals(plain, warmed);
}
@Test
public void apronHeightsAreComputedThroughThePlaneSourceAndNeverPerColumn() throws Exception {
AtomicInteger scalarHeights = new AtomicInteger();
Harness harness = harness(true, scalarHeights);
for (int[] chunk : ORDER) {
harness.component.generateLayer(harness.writer, chunk[0], chunk[1], harness.context);
}
assertEquals(0, scalarHeights.get());
}
private List<String> run(boolean baked, boolean warm) throws Exception {
Harness harness = harness(baked, new AtomicInteger());
if (warm) {
for (int x = -40; x < 56; x += 3) {
for (int z = -40; z < 56; z += 5) {
harness.complex.getNaturalHeightStream().getDouble(x, z);
harness.complex.getCaveBiomeStream().get(x, z);
harness.complex.getFluidStream().get(x, z);
}
}
}
for (int[] chunk : ORDER) {
harness.component.generateLayer(harness.writer, chunk[0], chunk[1], harness.context);
}
return harness.calls;
}
@SuppressWarnings("unchecked")
private Harness harness(boolean baked, AtomicInteger scalarHeights) throws ReflectiveOperationException {
Engine engine = mock(Engine.class);
EngineMantle engineMantle = mock(EngineMantle.class);
when(engineMantle.getEngine()).thenReturn(engine);
MantleCarvingComponent component = new MantleCarvingComponent(engineMantle);
IrisDimension dimension = new IrisDimension();
dimension.setCaveProfile(dimensionProfile);
when(engine.getDimension()).thenReturn(dimension);
when(engine.getWorld()).thenReturn(IrisWorld.builder().minHeight(0).maxHeight(128).build());
when(engine.getMetrics()).thenReturn(new EngineMetrics(16));
IrisComplex complex = mock(IrisComplex.class);
when(complex.allowsMantleChunkWrite(anyInt(), anyInt())).thenReturn(true);
when(complex.getFluidHeight()).thenReturn(FLUID_HEIGHT);
ProceduralStream<Double> heights = mock(ProceduralStream.class);
when(heights.getDouble(anyDouble(), anyDouble())).thenAnswer(call -> {
scalarHeights.incrementAndGet();
return height((int) (double) call.getArgument(0), (int) (double) call.getArgument(1));
});
ProceduralStream<IrisRegion> regions = objects(this::region);
ProceduralStream<IrisBiome> surfaces = objects(this::surface);
ProceduralStream<IrisBiome> caves = objects(this::cave);
ProceduralStream<NativeBlockState> fluids = objects(this::fluidAt);
if (baked) {
ChunkBakeCache cache = new ChunkBakeCache(new FunctionSource(), 256);
when(complex.getNaturalHeightStream()).thenReturn(new BakedDoubleStream(cache, BakePlane.NATURAL_HEIGHT, heights));
when(complex.getRegionStream()).thenReturn(new BakedStream<>(cache, BakePlane.REGION, regions));
when(complex.getNaturalTrueBiomeStream()).thenReturn(new BakedStream<>(cache, BakePlane.NATURAL_TRUE_BIOME, surfaces));
when(complex.getCaveBiomeStream()).thenReturn(new BakedStream<>(cache, BakePlane.CAVE_BIOME, caves));
when(complex.getFluidStream()).thenReturn(new BakedStream<>(cache, BakePlane.FLUID, fluids));
} else {
when(complex.getNaturalHeightStream()).thenReturn(heights);
when(complex.getRegionStream()).thenReturn(regions);
when(complex.getNaturalTrueBiomeStream()).thenReturn(surfaces);
when(complex.getCaveBiomeStream()).thenReturn(caves);
when(complex.getFluidStream()).thenReturn(fluids);
}
ChunkContext context = mock(ChunkContext.class);
when(context.getComplex()).thenReturn(complex);
MantleWriter writer = mock(MantleWriter.class);
Mantle<Matter> storage = mock(Mantle.class);
when(storage.getWorldHeight()).thenReturn(128);
when(writer.getMantle()).thenReturn(storage);
List<String> calls = new ArrayList<>();
Map<IrisCaveProfile, IrisCaveCarver3D> carvers = new IdentityHashMap<>();
IrisCaveProfile[] profiles = {dimensionProfile, regionProfile, surfaceProfile, shallowCaveProfile, deepCaveProfile};
for (int index = 0; index < profiles.length; index++) {
int profileIndex = index;
IrisCaveCarver3D carver = mock(IrisCaveCarver3D.class);
doAnswer(call -> {
IrisRange range = call.getArgument(6);
calls.add(call.getArgument(1) + "," + call.getArgument(2) + " profile=" + profileIndex
+ " range=" + (range == null ? "-" : range.getMin() + ".." + range.getMax())
+ " weights=" + Arrays.toString((double[]) call.getArgument(3))
+ " heights=" + Arrays.toString((int[]) call.getArgument(7))
+ " boundaries=" + Arrays.toString((long[]) call.getArgument(8)));
return 0;
}).when(carver).carve(any(), anyInt(), anyInt(), any(), anyDouble(), anyDouble(),
any(), any(), any(), any(), any());
carvers.put(profiles[index], carver);
}
Field profileCarvers = MantleCarvingComponent.class.getDeclaredField("profileCarvers");
profileCarvers.setAccessible(true);
profileCarvers.set(component, carvers);
return new Harness(component, complex, context, writer, calls);
}
@SuppressWarnings("unchecked")
private static <T> ProceduralStream<T> objects(ColumnFunction<T> function) {
ProceduralStream<T> stream = mock(ProceduralStream.class);
when(stream.get(anyDouble(), anyDouble())).thenAnswer(call ->
function.at((int) (double) call.getArgument(0), (int) (double) call.getArgument(1)));
return stream;
}
private static double height(int x, int z) {
return 64D + 9.5D * Math.sin(x * 0.37D) * Math.cos(z * 0.23D) + Math.floorMod(x * 31 + z * 17, 7) * 0.31D;
}
private IrisRegion region(int x, int z) {
return Math.floorMod((x >> 3) + (z >> 3), 3) == 0 ? profiledRegion : plainRegion;
}
private IrisBiome surface(int x, int z) {
return Math.floorMod(x * 7 + z * 3, 11) < 4 ? profiledSurface : plainSurface;
}
private IrisBiome cave(int x, int z) {
return switch (Math.floorMod((x >> 2) ^ (z >> 2), 5)) {
case 0 -> shallowCave;
case 1, 2 -> deepCave;
case 3 -> plainCave;
default -> null;
};
}
private NativeBlockState fluidAt(int x, int z) {
return Math.floorMod(x + z, 5) == 0 ? air : water;
}
private static NativeBlockState fluid(boolean fluid) {
NativeBlockState state = mock(NativeBlockState.class);
when(state.isFluid()).thenReturn(fluid);
return state;
}
private interface ColumnFunction<T> {
T at(int x, int z);
}
private final class FunctionSource implements PlaneSource {
@Override
public void fill(BakePlane plane, int chunkX, int chunkZ, long[] missing, Object values) {
for (int index = 0; index < 256; index++) {
if ((missing[index >>> 6] & (1L << (index & 63))) == 0L) {
continue;
}
int x = (chunkX << 4) + (index & 15);
int z = (chunkZ << 4) + (index >> 4);
switch (plane) {
case NATURAL_HEIGHT -> ((double[]) values)[index] = height(x, z);
case REGION -> ((Object[]) values)[index] = region(x, z);
case NATURAL_TRUE_BIOME -> ((Object[]) values)[index] = surface(x, z);
case CAVE_BIOME -> ((Object[]) values)[index] = cave(x, z);
case FLUID -> ((Object[]) values)[index] = fluidAt(x, z);
default -> throw new IllegalStateException("Unexpected plane " + plane);
}
}
}
@Override
public boolean mainThread() {
return false;
}
}
private record Harness(MantleCarvingComponent component, IrisComplex complex, ChunkContext context,
MantleWriter writer, List<String> calls) {
}
}