mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 05:50:39 +00:00
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:
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.stream.ProvisionalSampling;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class ChunkBakeCache {
|
||||
@@ -10,6 +11,7 @@ public final class ChunkBakeCache {
|
||||
static final int MAXIMUM_CAPACITY = 32_768;
|
||||
static final long BYTES_PER_ENTRY = 13L * 1_024L;
|
||||
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 final PlaneSource source;
|
||||
@@ -52,17 +54,16 @@ public final class ChunkBakeCache {
|
||||
|
||||
public ChunkPlanes.Plane planeIfPresent(BakePlane plane, int chunkX, int chunkZ) {
|
||||
long key = key(chunkX, chunkZ);
|
||||
int slot = Recent.slot(chunkX, chunkZ);
|
||||
Recent last = recent.get();
|
||||
int current = generation;
|
||||
ChunkPlanes planes;
|
||||
if (last.key == key && last.generation == current) {
|
||||
planes = last.planes;
|
||||
} else {
|
||||
ChunkPlanes planes = last.get(key, slot, current);
|
||||
if (planes == null) {
|
||||
planes = entries.get(key);
|
||||
if (planes == null) {
|
||||
return null;
|
||||
}
|
||||
last.remember(key, planes, current);
|
||||
last.remember(key, slot, planes, current);
|
||||
}
|
||||
return planes.planeIfPresent(plane);
|
||||
}
|
||||
@@ -75,6 +76,10 @@ public final class ChunkBakeCache {
|
||||
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() {
|
||||
return SPARSE.get()[0] == 0 && !source.mainThread();
|
||||
}
|
||||
@@ -102,7 +107,7 @@ public final class ChunkBakeCache {
|
||||
private ChunkPlanes remembered(int chunkX, int chunkZ) {
|
||||
int current = generation;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -123,14 +128,25 @@ public final class ChunkBakeCache {
|
||||
}
|
||||
|
||||
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 ChunkPlanes planes;
|
||||
|
||||
private void remember(long key, ChunkPlanes planes, int generation) {
|
||||
this.key = key;
|
||||
this.planes = planes;
|
||||
this.generation = generation;
|
||||
private static int slot(int chunkX, int chunkZ) {
|
||||
return ((chunkZ & 3) << 2) | (chunkX & 3);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
long[] missing = missing();
|
||||
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.context.ChunkContext;
|
||||
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.simd.SimdSupport;
|
||||
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_DZ = new int[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 final Object profileCarverLock = new Object();
|
||||
@@ -81,6 +89,22 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
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) {
|
||||
@@ -128,7 +152,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
|
||||
private void generateLayer(MantleWriter writer, int x, int z, ChunkContext context, BlendScratch blendScratch) {
|
||||
IrisComplex complex = context.getComplex();
|
||||
IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State();
|
||||
IrisDimensionCarvingResolver.State resolverState = blendScratch.resolverState;
|
||||
int[] chunkSurfaceHeights = prepareChunkSurfaceHeights(x, z, context, blendScratch.chunkSurfaceHeights);
|
||||
PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start();
|
||||
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) {
|
||||
int startX = PowerOfTwoCoordinates.chunkToBlock(chunkX) - 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++) {
|
||||
int worldX = startX + fieldX;
|
||||
@@ -519,17 +543,28 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private void prefillProfileFieldSamples(int startX, int startZ, IrisComplex complex, BlendScratch blendScratch) {
|
||||
fillFieldHeights(complex.getNaturalHeightStream(), startX, startZ, blendScratch.fieldSurfaceHeights);
|
||||
Arrays.fill(blendScratch.fieldFluidHeights, complex.getFluidHeight());
|
||||
fillFieldFluidPresence(complex, startX, startZ, blendScratch.fieldSurfaceHeights,
|
||||
blendScratch.fieldFluidHeights, blendScratch.fieldHasFluid);
|
||||
fillFieldObjects(complex.getRegionStream(), startX, startZ, blendScratch.fieldRegions);
|
||||
fillFieldObjects(complex.getNaturalTrueBiomeStream(), startX, startZ, blendScratch.fieldSurfaceBiomes);
|
||||
fillFieldObjects(complex.getCaveBiomeStream(), startX, startZ, blendScratch.fieldCaveBiomes);
|
||||
private void prefillProfileFieldSamples(int chunkX, int chunkZ, int startX, int startZ, IrisComplex complex, BlendScratch blendScratch) {
|
||||
fillFieldHeights(complex.getNaturalHeightStream(), chunkX, chunkZ, startX, startZ, blendScratch);
|
||||
double fluidHeight = complex.getFluidHeight();
|
||||
Arrays.fill(blendScratch.fieldFluidHeights, fluidHeight);
|
||||
fillFieldFluidPresence(complex.getFluidStream(), chunkX, chunkZ, startX, startZ, Math.round(fluidHeight), blendScratch);
|
||||
fillFieldObjects(complex.getRegionStream(), chunkX, chunkZ, startX, startZ, blendScratch.fieldRegions, blendScratch.neighbourObjects);
|
||||
fillFieldObjects(complex.getNaturalTrueBiomeStream(), chunkX, chunkZ, startX, startZ, blendScratch.fieldSurfaceBiomes, blendScratch.neighbourObjects);
|
||||
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++) {
|
||||
int worldX = startX + fieldX;
|
||||
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++) {
|
||||
int worldX = startX + fieldX;
|
||||
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
|
||||
@@ -547,20 +594,41 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private void fillFieldFluidPresence(
|
||||
IrisComplex complex,
|
||||
int startX,
|
||||
int startZ,
|
||||
double[] surfaceHeights,
|
||||
double[] fluidHeights,
|
||||
boolean[] target
|
||||
) {
|
||||
private void fillFieldFluidPresence(ProceduralStream<NativeBlockState> stream, int chunkX, int chunkZ, int startX, int startZ,
|
||||
long roundedFluidHeight, BlendScratch blendScratch) {
|
||||
double[] surfaceHeights = blendScratch.fieldSurfaceHeights;
|
||||
boolean[] target = blendScratch.fieldHasFluid;
|
||||
if (stream instanceof BakedStream<?> baked) {
|
||||
long[][] submerged = blendScratch.neighbourSubmerged;
|
||||
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++) {
|
||||
int worldX = startX + fieldX;
|
||||
for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) {
|
||||
int fieldIndex = (fieldX * FIELD_SIZE) + fieldZ;
|
||||
target[fieldIndex] = B.isFluid(complex.getFluidStream().get(worldX, startZ + fieldZ))
|
||||
&& Math.round(surfaceHeights[fieldIndex]) < Math.round(fluidHeights[fieldIndex]);
|
||||
target[fieldIndex] = B.isFluid(stream.get(worldX, startZ + fieldZ))
|
||||
&& 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 int[] chunkSurfaceHeights = new int[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 boolean inUse;
|
||||
|
||||
@@ -705,6 +777,9 @@ public class MantleCarvingComponent extends IrisMantleComponent {
|
||||
Arrays.fill(fieldRegions, null);
|
||||
Arrays.fill(fieldSurfaceBiomes, null);
|
||||
Arrays.fill(fieldCaveBiomes, null);
|
||||
for (Object[] objects : neighbourObjects) {
|
||||
Arrays.fill(objects, null);
|
||||
}
|
||||
columnProfileWeights.clear();
|
||||
profileOrder.clear();
|
||||
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
|
||||
public void fillChunkRaw(int worldX, int worldZ, Object[] target) {
|
||||
double[] values = new double[256];
|
||||
|
||||
@@ -54,6 +54,21 @@ public final class BakedStream<T> extends BasicStream<T> implements ChunkFillabl
|
||||
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
|
||||
public void fillChunkRaw(int worldX, int worldZ, Object[] target) {
|
||||
int chunkX = worldX >> 4;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package art.arcane.iris.generation.bake;
|
||||
|
||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
@@ -87,6 +89,155 @@ public class ChunkBakeCacheTest {
|
||||
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 {
|
||||
@Override
|
||||
public void fill(BakePlane plane, int chunkX, int chunkZ, long[] missing, Object values) {
|
||||
|
||||
+242
@@ -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) {
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user