mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 14:00:26 +00:00
Merged carve field reads
- Batched baked carve field reads, per-thread bake chunk memory, terminal carving roots return early
This commit is contained in:
commit
9d4c55d11a
9 files changed
+644
-44
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;
|
||||||
|
|||||||
+21
-10
@@ -120,7 +120,7 @@ public final class IrisDimensionCarvingResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int remainingDepth = clampDepth(rootEntry.getChildRecursionDepth());
|
int remainingDepth = clampDepth(rootEntry.getChildRecursionDepth());
|
||||||
if (remainingDepth <= 0) {
|
if (remainingDepth <= 0 || terminal(engine, data, rootEntry, rootBiome, resolvedState)) {
|
||||||
return rootEntry;
|
return rootEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,13 +131,12 @@ public final class IrisDimensionCarvingResolver {
|
|||||||
return columns.entries[slot];
|
return columns.entries[slot];
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, IrisDimensionCarvingEntry> entryIndex = resolveEntryIndex(engine, resolvedState);
|
|
||||||
IrisDimensionCarvingEntry current = rootEntry;
|
IrisDimensionCarvingEntry current = rootEntry;
|
||||||
int depth = remainingDepth;
|
int depth = remainingDepth;
|
||||||
boolean cacheable = true;
|
boolean cacheable = true;
|
||||||
while (depth > 0) {
|
while (depth > 0) {
|
||||||
cacheable &= fixedChildStyle(current);
|
cacheable &= fixedChildStyle(current);
|
||||||
IrisDimensionCarvingEntry selected = selectChild(engine, data, current, worldX, worldZ, entryIndex, resolvedState);
|
IrisDimensionCarvingEntry selected = selectChild(engine, data, current, worldX, worldZ, resolvedState);
|
||||||
if (selected == null || selected == current || selected.getWorldYRange() != null
|
if (selected == null || selected == current || selected.getWorldYRange() != null
|
||||||
&& !selected.getWorldYRange().contains(worldY)) {
|
&& !selected.getWorldYRange().contains(worldY)) {
|
||||||
break;
|
break;
|
||||||
@@ -164,6 +163,24 @@ public final class IrisDimensionCarvingResolver {
|
|||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean terminal(Engine engine, IrisData data, IrisDimensionCarvingEntry entry, IrisBiome biome, State state) {
|
||||||
|
KList<String> children = entry.getChildren();
|
||||||
|
if (children == null || children.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return selectionPlan(engine, data, entry, biome, state).size() <= 1L;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IrisRaritySelection<CarvingChoice> selectionPlan(Engine engine, IrisData data, IrisDimensionCarvingEntry parent,
|
||||||
|
IrisBiome parentBiome, State state) {
|
||||||
|
IrisRaritySelection<CarvingChoice> selectionPlan = state.selectionPlans.get(parent);
|
||||||
|
if (selectionPlan == null) {
|
||||||
|
selectionPlan = buildSelectionPlan(data, parent, parentBiome, resolveEntryIndex(engine, state), state);
|
||||||
|
state.selectionPlans.put(parent, selectionPlan);
|
||||||
|
}
|
||||||
|
return selectionPlan;
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean fixedChildStyle(IrisDimensionCarvingEntry entry) {
|
private static boolean fixedChildStyle(IrisDimensionCarvingEntry entry) {
|
||||||
for (IrisGeneratorStyle style = entry.getChildStyle(); style != null; style = style.getFracture()) {
|
for (IrisGeneratorStyle style = entry.getChildStyle(); style != null; style = style.getFracture()) {
|
||||||
if (style.getExpression() != null) {
|
if (style.getExpression() != null) {
|
||||||
@@ -202,7 +219,6 @@ public final class IrisDimensionCarvingResolver {
|
|||||||
IrisDimensionCarvingEntry parent,
|
IrisDimensionCarvingEntry parent,
|
||||||
int worldX,
|
int worldX,
|
||||||
int worldZ,
|
int worldZ,
|
||||||
Map<String, IrisDimensionCarvingEntry> entryIndex,
|
|
||||||
State state
|
State state
|
||||||
) {
|
) {
|
||||||
KList<String> children = parent.getChildren();
|
KList<String> children = parent.getChildren();
|
||||||
@@ -215,12 +231,7 @@ public final class IrisDimensionCarvingResolver {
|
|||||||
return parent;
|
return parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
IrisRaritySelection<CarvingChoice> selectionPlan = state.selectionPlans.get(parent);
|
IrisRaritySelection<CarvingChoice> selectionPlan = selectionPlan(engine, data, parent, parentBiome, state);
|
||||||
if (selectionPlan == null) {
|
|
||||||
selectionPlan = buildSelectionPlan(data, parent, parentBiome, entryIndex, state);
|
|
||||||
state.selectionPlans.put(parent, selectionPlan);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectionPlan.size() <= 1L) {
|
if (selectionPlan.size() <= 1L) {
|
||||||
return parent;
|
return parent;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
+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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
@@ -166,6 +166,32 @@ public class IrisDimensionCarvingResolverParityTest {
|
|||||||
assertSame(child, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, 7, 0, 9, state));
|
assertSame(child, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, 7, 0, 9, state));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rootsWithoutSelectableChildrenResolveToThemselvesWithoutSampling() {
|
||||||
|
for (List<String> children : List.of(List.<String>of(), List.of("missing-child"))) {
|
||||||
|
Fixture fixture = createFixture();
|
||||||
|
IrisDimensionCarvingEntry parent = spy(fixture.engine.getDimension().getCarvingEntryIndex().get("root-low"));
|
||||||
|
parent.setChildRecursionDepth(3);
|
||||||
|
parent.setChildren(new KList<>(children));
|
||||||
|
CNG generator = mock(CNG.class);
|
||||||
|
doReturn(0.5D).when(generator).noiseFast2D(anyDouble(), anyDouble());
|
||||||
|
doReturn(generator).when(parent).getChildrenGenerator(anyLong(), any(IrisData.class));
|
||||||
|
IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State();
|
||||||
|
|
||||||
|
for (int worldY = -80; worldY < 140; worldY += 13) {
|
||||||
|
for (int x = -20; x < 20; x += 3) {
|
||||||
|
for (int z = -20; z < 20; z += 7) {
|
||||||
|
assertSame(legacyResolveFromRoot(fixture.engine, parent, x, worldY, z),
|
||||||
|
IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, x, worldY, z, state));
|
||||||
|
assertSame(parent, IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, parent, x, worldY, z, state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(generator, never()).noiseFast2D(anyDouble(), anyDouble());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void repeatedColumnResolutionSamplesChildrenOnceAcrossHeights() {
|
public void repeatedColumnResolutionSamplesChildrenOnceAcrossHeights() {
|
||||||
Fixture fixture = createFixture();
|
Fixture fixture = createFixture();
|
||||||
|
|||||||
Reference in new issue
Block a user