Merged terrain assembly improvements

- Ceiling layer thickness resolves independently from surface layers
- Locked strata repeat correctly above the internal height reference
- Cave scratch releases generation references after each pass
- Terrain actuators omit redundant setup and checks
This commit is contained in:
Brian Neumann-Fopiano
2026-09-06 03:16:51 -04:00
16 changed files with 446 additions and 223 deletions
@@ -22,24 +22,15 @@ import art.arcane.volmlib.util.json.JSONArray;
import art.arcane.volmlib.util.json.JSONObject;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* Validates biome layer stacks. caveCeilingLayers reuses the height generators built from layers,
* so a biome with more ceiling entries than surface entries has no generator for the extras; the
* engine skips them, and this validator surfaces the mistake to the author at validate time.
*/
final class PackBiomeLayerValidator {
private static final int DEFAULT_LAYER_COUNT = 1;
private PackBiomeLayerValidator() {
}
static List<String> validateCeilingLayerCounts(File biomesFolder) {
static List<String> validateLayers(File biomesFolder) {
List<String> blockingErrors = new ArrayList<>();
if (biomesFolder == null || !biomesFolder.isDirectory()) {
return blockingErrors;
@@ -49,24 +40,13 @@ final class PackBiomeLayerValidator {
biomeFiles.sort(Comparator.comparing(File::getPath));
for (File biomeFile : biomeFiles) {
String biomeKey = PackValidationIo.deriveKey(biomesFolder, biomeFile);
JSONObject biome;
try {
biome = new JSONObject(Files.readString(biomeFile.toPath(), StandardCharsets.UTF_8));
} catch (Throwable e) {
// Invalid JSON is reported by the graph validators; layer counts have nothing to add.
JSONObject biome = PackValidationIo.readJson(biomeFile);
if (biome == null) {
continue;
}
Integer layers = arrayLength(biome, "layers", biomeKey, DEFAULT_LAYER_COUNT, blockingErrors);
Integer ceiling = arrayLength(biome, "caveCeilingLayers", biomeKey, 0, blockingErrors);
if (layers == null || ceiling == null) {
continue;
}
if (ceiling > layers) {
blockingErrors.add("Biome '" + biomeKey + "' declares " + ceiling + " caveCeilingLayers but only "
+ layers + " layers. caveCeilingLayers reuses the layers height generators and must not have more entries.");
}
validateLayerArray(biome, "layers", biomeKey, blockingErrors);
validateLayerArray(biome, "caveCeilingLayers", biomeKey, blockingErrors);
}
return blockingErrors;
}
@@ -126,17 +106,15 @@ final class PackBiomeLayerValidator {
}
}
private static Integer arrayLength(JSONObject biome, String field, String biomeKey, int defaultCount,
private static void validateLayerArray(JSONObject biome, String field, String biomeKey,
List<String> blockingErrors) {
if (!biome.has(field) || biome.isNull(field)) {
return defaultCount;
return;
}
JSONArray array = biome.optJSONArray(field);
if (array == null) {
blockingErrors.add("Biome '" + biomeKey + "' " + field + " must be an array.");
return null;
}
return array.length();
}
}
@@ -150,7 +150,7 @@ public final class PackValidator {
new File(packFolder, "spawners"), new File(packFolder, "entities")));
blockingErrors.addAll(PackSpawnValidator.validateCustomBiomeSpawns(
new File(packFolder, "biomes"), PackSpawnValidator::resolveEntitySpawnCategory));
blockingErrors.addAll(PackBiomeLayerValidator.validateCeilingLayerCounts(new File(packFolder, "biomes")));
blockingErrors.addAll(PackBiomeLayerValidator.validateLayers(new File(packFolder, "biomes")));
blockingErrors.addAll(PackBiomeLayerValidator.validateDecoratorPalettes(
new File(packFolder, "biomes"), new File(packFolder, "snippet/decorator")));
PackStyledRangeDefaultValidator.Validation styledRanges = PackStyledRangeDefaultValidator.validate(packFolder);
@@ -41,7 +41,6 @@ import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterBiomeInject;
import art.arcane.volmlib.util.matter.slices.BiomeInjectMatter;
import art.arcane.volmlib.util.scheduling.ChronoLatch;
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import java.util.List;
@@ -49,7 +48,6 @@ import java.util.Objects;
public class IrisBiomeActuator extends EngineAssignedActuator<PlatformBiome> {
private final RNG rng;
private final ChronoLatch cl = new ChronoLatch(5000);
private final KMap<String, ResolvedBiome> resolvedBiomes = new KMap<>();
private final KMap<String, ResolvedBiome> resolvedPhysicalBiomes = new KMap<>();
@@ -46,11 +46,7 @@ import art.arcane.iris.util.common.data.B;
import lombok.Getter;
public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBlockState> {
private static final PlatformBlockState AIR = B.getState("AIR");
private static final PlatformBlockState BEDROCK = B.getState("BEDROCK");
private static final PlatformBlockState LAVA = B.getState("LAVA");
private static final PlatformBlockState GLASS = B.getState("GLASS");
private static final PlatformBlockState CAVE_AIR = B.getState("CAVE_AIR");
@Getter
private final RNG rng;
@Getter
@@ -83,11 +79,6 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<PlatformBl
*/
@BlockCoordinates
public void terrainSliver(int x, int z, int xf, Hunk<PlatformBlockState> h, ChunkContext context) {
terrainSliverOptimized(x, z, xf, h, context);
}
@BlockCoordinates
private void terrainSliverOptimized(int x, int z, int xf, Hunk<PlatformBlockState> h, ChunkContext context) {
int chunkHeight = h.getHeight();
int chunkDepth = h.getDepth();
IrisDimension dimension = getDimension();
@@ -42,9 +42,7 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
return;
}
GeometrySource source = new GeometrySource(blocks, biomes, getEngine().getMinHeight(), context);
if (plan != null && plan.hasTransitionAtChunk(x >> 4, z >> 4)) {
blendGeometry(x, z, blocks, context, source, plan);
}
blendGeometry(x, z, blocks, context, source, plan);
}
public static SavedTerrainChunk capture(int x, int z, Hunk<PlatformBlockState> blocks,
@@ -22,6 +22,8 @@ import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterSlice;
import java.util.Arrays;
final class CaveCarveScratch {
final int[] columnMaxY = new int[256];
final int[] fluidMaxY = new int[256];
@@ -70,4 +72,9 @@ final class CaveCarveScratch {
int adaptiveGeometryStep = -1;
int adaptiveGeometryAxisCells = -1;
boolean fullWeightsInitialized;
void releaseSections() {
Arrays.fill(sectionMatter, null);
Arrays.fill(sectionSlices, null);
}
}
@@ -436,6 +436,7 @@ public class IrisCaveCarver3D {
return carved;
} finally {
scratchCache.get().releaseSections();
carving.set(nested);
if (nested) {
scratchCache.set(previousScratch);
@@ -2695,10 +2696,6 @@ public class IrisCaveCarver3D {
return weight;
}
private double signed(double value) {
return (value * 2D) - 1D;
}
private double applySurfaceCeilingFade(
CaveCarveScratch scratch,
double threshold,
@@ -19,6 +19,7 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.UpperDimensionContext;
import art.arcane.iris.engine.mantle.MatterGenerationPhase;
import art.arcane.iris.engine.mantle.ComponentFlag;
@@ -36,7 +37,6 @@ import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.iris.util.project.stream.utility.ChunkFillableDoubleStream2D;
import art.arcane.iris.util.simd.SimdKernels;
import art.arcane.iris.util.simd.SimdSupport;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
@@ -58,6 +58,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
private static final double MIN_WEIGHT = 0.08D;
private static final double THRESHOLD_PENALTY = 0.24D;
private static final int MAX_BLENDED_PROFILE_PASSES = 2;
private static final int MAX_POOLED_WEIGHT_BUFFERS = 64;
private static final int KERNEL_WIDTH = (BLEND_RADIUS * 2) + 1;
private static final int KERNEL_SIZE = KERNEL_WIDTH * KERNEL_WIDTH;
private static final int[] KERNEL_DX = new int[KERNEL_SIZE];
@@ -107,8 +108,25 @@ public class MantleCarvingComponent extends IrisMantleComponent {
if (!complex.allowsMantleChunkWrite(x, z)) {
return;
}
BlendScratch previous = BLEND_SCRATCH.get();
BlendScratch scratch = previous.inUse ? new BlendScratch() : previous;
if (scratch != previous) {
BLEND_SCRATCH.set(scratch);
}
scratch.inUse = true;
try {
generateLayer(writer, x, z, context, scratch);
} finally {
scratch.release();
if (scratch != previous) {
BLEND_SCRATCH.set(previous);
}
}
}
private void generateLayer(MantleWriter writer, int x, int z, ChunkContext context, BlendScratch blendScratch) {
IrisComplex complex = context.getComplex();
IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State();
BlendScratch blendScratch = BLEND_SCRATCH.get();
int[] chunkSurfaceHeights = prepareChunkSurfaceHeights(x, z, context, blendScratch.chunkSurfaceHeights);
PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start();
List<WeightedProfile> weightedProfiles = resolveWeightedProfiles(x, z, complex, resolverState);
@@ -217,12 +235,12 @@ public class MantleCarvingComponent extends IrisMantleComponent {
BlendScratch blendScratch = BLEND_SCRATCH.get();
IrisCaveProfile[] profileField = blendScratch.profileField;
Map<IrisCaveProfile, double[]> columnProfileWeights = blendScratch.columnProfileWeights;
IdentityHashMap<IrisCaveProfile, Boolean> activeProfiles = blendScratch.activeProfiles;
List<IrisCaveProfile> profileOrder = blendScratch.profileOrder;
IrisCaveProfile[] kernelProfiles = blendScratch.kernelProfiles;
double[] kernelProfileWeights = blendScratch.kernelProfileWeights;
activeProfiles.clear();
columnProfileWeights.clear();
profileOrder.clear();
blendScratch.nextWeightBuffer = 0;
fillProfileField(profileField, chunkX, chunkZ, complex, resolverState, blendScratch);
for (int localX = 0; localX < CHUNK_SIZE; localX++) {
@@ -270,12 +288,8 @@ public class MantleCarvingComponent extends IrisMantleComponent {
double[] weights = columnProfileWeights.get(profile);
if (weights == null) {
weights = new double[CHUNK_AREA];
weights = blendScratch.acquireWeights();
columnProfileWeights.put(profile, weights);
} else if (!activeProfiles.containsKey(profile)) {
Arrays.fill(weights, 0D);
}
if (activeProfiles.put(profile, Boolean.TRUE) == null) {
profileOrder.add(profile);
}
weights[columnIndex] = columnWeight;
@@ -316,28 +330,27 @@ public class MantleCarvingComponent extends IrisMantleComponent {
return weightedProfiles;
}
Map<IrisDimensionCarvingEntry, IrisDimensionCarvingEntry[]> dimensionColumnPlans = blendScratch.dimensionColumnPlans;
dimensionColumnPlans.clear();
Engine engine = getEngineMantle().getEngine();
int baseX = PowerOfTwoCoordinates.chunkToBlock(chunkX);
int baseZ = PowerOfTwoCoordinates.chunkToBlock(chunkZ);
for (IrisDimensionCarvingEntry entry : entries) {
if (entry == null || !entry.isEnabled()) {
continue;
}
IrisBiome rootBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), entry, resolverState);
IrisBiome rootBiome = IrisDimensionCarvingResolver.resolveEntryBiome(engine, entry, resolverState);
if (rootBiome == null) {
continue;
}
IrisDimensionCarvingEntry[] columnPlan = dimensionColumnPlans.computeIfAbsent(entry, key -> new IrisDimensionCarvingEntry[CHUNK_AREA]);
buildDimensionColumnPlan(columnPlan, chunkX, chunkZ, entry, resolverState);
Map<IrisCaveProfile, double[]> rootProfileColumnWeights = new IdentityHashMap<>();
List<IrisCaveProfile> rootProfileOrder = new ArrayList<>();
IrisRange worldYRange = entry.getWorldYRange();
for (int columnIndex = 0; columnIndex < CHUNK_AREA; columnIndex++) {
IrisDimensionCarvingEntry resolvedEntry = columnPlan[columnIndex];
IrisBiome resolvedBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), resolvedEntry, resolverState);
IrisDimensionCarvingEntry resolvedEntry = IrisDimensionCarvingResolver.resolveFromRoot(
engine, entry, baseX + (columnIndex >> 4), baseZ + (columnIndex & 15), resolverState);
IrisBiome resolvedBiome = IrisDimensionCarvingResolver.resolveEntryBiome(engine, resolvedEntry, resolverState);
if (resolvedBiome == null) {
continue;
}
@@ -349,7 +362,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
double[] columnWeights = rootProfileColumnWeights.get(profile);
if (columnWeights == null) {
columnWeights = new double[CHUNK_AREA];
columnWeights = blendScratch.acquireWeights();
rootProfileColumnWeights.put(profile, columnWeights);
rootProfileOrder.add(profile);
}
@@ -364,19 +377,6 @@ public class MantleCarvingComponent extends IrisMantleComponent {
return weightedProfiles;
}
private void buildDimensionColumnPlan(IrisDimensionCarvingEntry[] columnPlan, int chunkX, int chunkZ, IrisDimensionCarvingEntry entry, IrisDimensionCarvingResolver.State resolverState) {
int baseX = PowerOfTwoCoordinates.chunkToBlock(chunkX);
int baseZ = PowerOfTwoCoordinates.chunkToBlock(chunkZ);
for (int localX = 0; localX < CHUNK_SIZE; localX++) {
int worldX = baseX + localX;
for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) {
int worldZ = baseZ + localZ;
int columnIndex = PowerOfTwoCoordinates.packLocal16(localX, localZ);
columnPlan[columnIndex] = IrisDimensionCarvingResolver.resolveFromRoot(getEngineMantle().getEngine(), entry, worldX, worldZ, resolverState);
}
}
}
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;
@@ -698,8 +698,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
private final IrisCaveProfile[] kernelProfiles = new IrisCaveProfile[KERNEL_SIZE];
private final double[] kernelProfileWeights = new double[KERNEL_SIZE];
private final IdentityHashMap<IrisCaveProfile, double[]> columnProfileWeights = new IdentityHashMap<>();
private final IdentityHashMap<IrisDimensionCarvingEntry, IrisDimensionCarvingEntry[]> dimensionColumnPlans = new IdentityHashMap<>();
private final IdentityHashMap<IrisCaveProfile, Boolean> activeProfiles = new IdentityHashMap<>();
private final List<double[]> weightBuffers = new ArrayList<>();
private final List<IrisCaveProfile> profileOrder = new ArrayList<>();
private final double[] fieldSurfaceHeights = new double[FIELD_SIZE * FIELD_SIZE];
private final double[] fieldFluidHeights = new double[FIELD_SIZE * FIELD_SIZE];
@@ -710,5 +709,33 @@ 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 int nextWeightBuffer;
private boolean inUse;
private double[] acquireWeights() {
if (nextWeightBuffer < weightBuffers.size()) {
double[] weights = weightBuffers.get(nextWeightBuffer++);
Arrays.fill(weights, 0D);
return weights;
}
double[] weights = new double[CHUNK_AREA];
if (weightBuffers.size() < MAX_POOLED_WEIGHT_BUFFERS) {
weightBuffers.add(weights);
nextWeightBuffer++;
}
return weights;
}
private void release() {
Arrays.fill(profileField, null);
Arrays.fill(kernelProfiles, null);
Arrays.fill(fieldRegions, null);
Arrays.fill(fieldSurfaceBiomes, null);
Arrays.fill(fieldCaveBiomes, null);
columnProfileWeights.clear();
profileOrder.clear();
nextWeightBuffer = 0;
inUse = false;
}
}
}
@@ -92,6 +92,7 @@ public class IrisBiome extends IrisRegistrant implements IRare {
private final transient AtomicCache<IrisBiome> realCarveBiome = new AtomicCache<>();
private final transient AtomicCache<KList<IrisBiome>> realChildren = new AtomicCache<>();
private final transient AtomicCache<KList<CNG>> layerHeightGenerators = new AtomicCache<>();
private final transient AtomicCache<KList<CNG>> layerCeilingHeightGenerators = new AtomicCache<>();
private final transient AtomicCache<KList<CNG>> layerSeaHeightGenerators = new AtomicCache<>();
private final transient AtomicCache<KList<IrisOreGenerator>> surfaceOreCache = new AtomicCache<>();
private final transient AtomicCache<KList<IrisOreGenerator>> undergroundOreCache = new AtomicCache<>();
@@ -74,7 +74,7 @@ final class IrisBiomeLayerGenerator {
IrisBiomePaletteLayer layer = layers.get(i);
double zoom = layer.getZoom();
CNG hgen = heightGenerators.get(i);
double d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom);
int d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom);
IrisSlopeClip sc = layer.getSlopeCondition();
@@ -88,17 +88,7 @@ final class IrisBiomeLayerGenerator {
continue;
}
for (int j = 0; j < d; j++) {
if (data.size() >= maxDepth) {
break;
}
try {
data.add(layer.get(random, i + j, (wx + j) / zoom, j, (wz - j) / zoom, rdata));
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
appendLayer(data, layer, i, d, wx, wz, random, maxDepth, rdata);
if (data.size() >= maxDepth) {
break;
@@ -132,31 +122,20 @@ final class IrisBiomeLayerGenerator {
return data;
}
KList<CNG> heightGenerators = getLayerHeightGenerators(biome, random, rdata);
// Ceiling layers reuse the surface-layer height generators, so entries beyond layers.size() have no generator; skip them.
int usableLayers = Math.min(layerCount, heightGenerators.size());
KList<CNG> heightGenerators = getHeightGenerators(
biome.getLayerCeilingHeightGenerators(), ceilingLayers, 7235, random, rdata);
for (int i = 0; i < usableLayers; i++) {
for (int i = 0; i < layerCount; i++) {
IrisBiomePaletteLayer layer = ceilingLayers.get(i);
double zoom = layer.getZoom();
CNG hgen = heightGenerators.get(i);
double d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom);
int d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom);
if (d <= 0) {
continue;
}
for (int j = 0; j < d; j++) {
if (data.size() >= maxDepth) {
break;
}
try {
data.add(layer.get(random, i + j, (wx + j) / zoom, j, (wz - j) / zoom, rdata));
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
appendLayer(data, layer, i, d, wx, wz, random, maxDepth, rdata);
if (data.size() >= maxDepth) {
break;
@@ -198,7 +177,7 @@ final class IrisBiomeLayerGenerator {
IrisBiomePaletteLayer layer = layers.get(i);
double zoom = layer.getZoom();
CNG hgen = heightGenerators.get(i);
double d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom);
int d = hgen.fit(layer.getMinHeight(), layer.getMaxHeight(), wx / zoom, wz / zoom);
IrisSlopeClip sc = layer.getSlopeCondition();
@@ -212,13 +191,7 @@ final class IrisBiomeLayerGenerator {
continue;
}
for (int j = 0; j < d; j++) {
try {
data.add(layer.get(random, i + j, (wx + j) / zoom, j, (wz - j) / zoom, rdata));
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
appendLayer(data, layer, i, d, wx, wz, random, Integer.MAX_VALUE, rdata);
}
}
@@ -227,9 +200,8 @@ final class IrisBiomeLayerGenerator {
}
for (int i = 0; i < maxDepth; i++) {
int offset = (512 - height) - i;
int index = offset % data.size();
real.add(data.get(Math.max(index, 0)));
long offset = 512L - height - i;
real.add(data.get(Math.floorMod(offset, data.size())));
}
return real;
@@ -264,17 +236,7 @@ final class IrisBiomeLayerGenerator {
continue;
}
for (int j = 0; j < d; j++) {
if (data.size() >= maxDepth) {
break;
}
try {
data.add(layer.get(random, i + j, (wx + j) / zoom, j, (wz - j) / zoom, rdata));
} catch (Throwable e) {
IrisLogging.reportError(e);
}
}
appendLayer(data, layer, i, d, wx, wz, random, maxDepth, rdata);
if (data.size() >= maxDepth) {
break;
@@ -285,7 +247,16 @@ final class IrisBiomeLayerGenerator {
}
static KList<CNG> getLayerHeightGenerators(IrisBiome biome, RNG rng, IrisData rdata) {
AtomicCache<KList<CNG>> cache = biome.getLayerHeightGenerators();
return getHeightGenerators(biome.getLayerHeightGenerators(), biome.getLayers(), 7235, rng, rdata);
}
static KList<CNG> getLayerSeaHeightGenerators(IrisBiome biome, RNG rng, IrisData data) {
return getHeightGenerators(biome.getLayerSeaHeightGenerators(), biome.getSeaLayers(), 7735, rng, data);
}
private static KList<CNG> getHeightGenerators(AtomicCache<KList<CNG>> cache,
KList<IrisBiomePaletteLayer> layers,
int seedOffset, RNG rng, IrisData data) {
KList<CNG> cached = cache.getIfPresent();
if (cached != null) {
@@ -296,36 +267,28 @@ final class IrisBiomeLayerGenerator {
{
KList<CNG> layerHeightGenerators = new KList<>();
int m = 7235;
int m = seedOffset;
for (IrisBiomePaletteLayer i : biome.getLayers()) {
layerHeightGenerators.add(i.getHeightGenerator(rng.nextParallelRNG((m++) * m * m * m), rdata));
for (IrisBiomePaletteLayer layer : layers) {
layerHeightGenerators.add(layer.getHeightGenerator(rng.nextParallelRNG((m++) * m * m * m), data));
}
return layerHeightGenerators;
});
}
static KList<CNG> getLayerSeaHeightGenerators(IrisBiome biome, RNG rng, IrisData data) {
AtomicCache<KList<CNG>> cache = biome.getLayerSeaHeightGenerators();
KList<CNG> cached = cache.getIfPresent();
if (cached != null) {
return cached;
}
return cache.aquire(() ->
{
KList<CNG> layerSeaHeightGenerators = new KList<>();
int m = 7735;
for (IrisBiomePaletteLayer i : biome.getSeaLayers()) {
layerSeaHeightGenerators.add(i.getHeightGenerator(rng.nextParallelRNG((m++) * m * m * m), data));
private static void appendLayer(KList<PlatformBlockState> blocks, IrisBiomePaletteLayer layer,
int layerIndex, int thickness, double x, double z,
RNG random, int maxDepth, IrisData data) {
double zoom = layer.getZoom();
for (int offset = 0; offset < thickness && blocks.size() < maxDepth; offset++) {
try {
blocks.add(layer.get(random, layerIndex + offset,
(x + offset) / zoom, offset, (z - offset) / zoom, data));
} catch (Throwable error) {
IrisLogging.reportError(error);
}
return layerSeaHeightGenerators;
});
}
}
static PlatformBlockState getSurfaceBlock(IrisBiome biome, int x, int z, RNG rng, IrisData idm) {
@@ -11,7 +11,6 @@ import java.nio.file.Path;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class PackBiomeLayerValidatorTest {
@@ -19,14 +18,12 @@ public class PackBiomeLayerValidatorTest {
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void rejectsMoreCeilingLayersThanLayers() throws Exception {
public void acceptsMoreCeilingLayersThanSurfaceLayers() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "biomes/nested/cave.json",
"{\"layers\":[{}],\"caveCeilingLayers\":[{},{},{}]}");
assertEquals(List.of(
"Biome 'nested/cave' declares 3 caveCeilingLayers but only 1 layers. caveCeilingLayers reuses the layers height generators and must not have more entries."
), PackBiomeLayerValidator.validateCeilingLayerCounts(new File(pack, "biomes")));
assertTrue(PackBiomeLayerValidator.validateLayers(new File(pack, "biomes")).isEmpty());
}
@Test
@@ -35,9 +32,7 @@ public class PackBiomeLayerValidatorTest {
write(pack, "biomes/defaulted.json", "{\"name\":\"Defaulted\"}");
write(pack, "biomes/implicit.json", "{\"caveCeilingLayers\":[{},{}]}");
assertEquals(List.of(
"Biome 'implicit' declares 2 caveCeilingLayers but only 1 layers. caveCeilingLayers reuses the layers height generators and must not have more entries."
), PackBiomeLayerValidator.validateCeilingLayerCounts(new File(pack, "biomes")));
assertTrue(PackBiomeLayerValidator.validateLayers(new File(pack, "biomes")).isEmpty());
}
@Test
@@ -45,7 +40,7 @@ public class PackBiomeLayerValidatorTest {
File pack = temporaryFolder.newFolder("pack");
write(pack, "biomes/ok.json", "{\"layers\":[{},{},{}],\"caveCeilingLayers\":[{},{}]}");
assertTrue(PackBiomeLayerValidator.validateCeilingLayerCounts(new File(pack, "biomes")).isEmpty());
assertTrue(PackBiomeLayerValidator.validateLayers(new File(pack, "biomes")).isEmpty());
}
@Test
@@ -56,11 +51,11 @@ public class PackBiomeLayerValidatorTest {
assertEquals(List.of(
"Biome 'bad' layers must be an array.",
"Biome 'bad' caveCeilingLayers must be an array."
), PackBiomeLayerValidator.validateCeilingLayerCounts(new File(pack, "biomes")));
), PackBiomeLayerValidator.validateLayers(new File(pack, "biomes")));
}
@Test
public void ceilingLayerOverflowBlocksFullPackValidation() throws Exception {
public void independentCeilingLayersPassFullPackValidation() throws Exception {
File pack = temporaryFolder.newFolder("pack");
write(pack, "dimensions/main.json", "{\"regions\":[\"region\"]}");
write(pack, "regions/region.json", "{\"landBiomes\":[\"biome\"]}");
@@ -68,9 +63,7 @@ public class PackBiomeLayerValidatorTest {
PackValidationResult result = PackValidator.validate(pack);
assertFalse(result.isLoadable());
assertTrue(result.getBlockingErrors().contains(
"Biome 'biome' declares 2 caveCeilingLayers but only 1 layers. caveCeilingLayers reuses the layers height generators and must not have more entries."));
assertTrue(result.getBlockingErrors().toString(), result.isLoadable());
}
@Test
@@ -35,12 +35,15 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -114,6 +117,45 @@ public class IrisCaveCarver3DNearParityTest {
carveForcedAirField.setAccessible(true);
}
@Test
public void carvingReleasesSectionReferencesAfterSuccessAndFailure() throws Exception {
for (boolean fail : new boolean[]{false, true}) {
Engine engine = createEngine(128, 110);
IrisCaveProfile profile = createProfile(false, false).setAdaptiveSampling(false)
.setAllowFluid(false).setDetailWeight(0D)
.setDensityThreshold(new IrisStyledRange(2D, 2D, new IrisGeneratorStyle(NoiseStyle.FLAT)));
IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, profile);
WriterCapture capture = createWriterCapture(128);
if (fail) {
AtomicInteger samples = new AtomicInteger();
CNG density = mock(CNG.class);
doAnswer(invocation -> {
if (samples.incrementAndGet() > 300) {
throw new IllegalStateException("Density sample failed");
}
return -1D;
}).when(density).noiseFastSigned3D(anyDouble(), anyDouble(), anyDouble());
baseDensityField.set(carver, density);
assertThrows(IllegalStateException.class, () -> carver.carve(capture.writer, 0, 0,
fullWeights(), 0D, 0D, null, filledHeights(110)));
} else {
carver.carve(capture.writer, 0, 0, fullWeights(), 0D, 0D, null, filledHeights(110));
}
assertFalse(capture.carvedCells.isEmpty());
Field scratchField = IrisCaveCarver3D.class.getDeclaredField("scratchCache");
scratchField.setAccessible(true);
ThreadLocal<?> scratchCache = (ThreadLocal<?>) scratchField.get(carver);
CaveCarveScratch scratch = (CaveCarveScratch) scratchCache.get();
assertTrue(scratch.sectionMatter.length > 0);
for (Matter matter : scratch.sectionMatter) {
assertNull(matter);
}
for (MatterSlice<?> slice : scratch.sectionSlices) {
assertNull(slice);
}
}
}
@Test
public void parallelDensityPreservesExactAndAdaptiveCavernsAndLiquids() throws Exception {
try (ForkJoinPool pool = new ForkJoinPool(4)) {
@@ -0,0 +1,170 @@
package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineMetrics;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.mantle.MantleWriter;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisCaveProfile;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.matter.Matter;
import org.junit.Before;
import org.junit.Test;
import java.lang.reflect.Field;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
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 MantleCarvingComponentScratchTest {
private MantleCarvingComponent component;
private IrisDimension dimension;
private ChunkContext context;
private MantleWriter writer;
@Before
@SuppressWarnings("unchecked")
public void prepare() {
Engine engine = mock(Engine.class);
EngineMantle mantle = mock(EngineMantle.class);
when(mantle.getEngine()).thenReturn(engine);
component = new MantleCarvingComponent(mantle);
dimension = new IrisDimension();
when(engine.getDimension()).thenReturn(dimension);
when(engine.getWorld()).thenReturn(mock(IrisWorld.class));
when(engine.getMetrics()).thenReturn(new EngineMetrics(16));
IrisComplex complex = mock(IrisComplex.class);
when(complex.allowsMantleChunkWrite(anyInt(), anyInt())).thenReturn(true);
ProceduralStream<Double> heights = mock(ProceduralStream.class);
when(heights.getDouble(anyDouble(), anyDouble())).thenAnswer(invocation -> {
double x = invocation.getArgument(0);
return x < 0D ? 90D : 70D;
});
when(complex.getNaturalHeightStream()).thenReturn(heights);
ProceduralStream<IrisRegion> regions = mock(ProceduralStream.class);
ProceduralStream<IrisBiome> biomes = mock(ProceduralStream.class);
ProceduralStream<PlatformBlockState> fluids = mock(ProceduralStream.class);
when(regions.get(anyDouble(), anyDouble())).thenReturn(new IrisRegion());
when(biomes.get(anyDouble(), anyDouble())).thenReturn(new IrisBiome());
when(complex.getRegionStream()).thenReturn(regions);
when(complex.getNaturalTrueBiomeStream()).thenReturn(biomes);
when(complex.getCaveBiomeStream()).thenReturn(biomes);
when(complex.getFluidStream()).thenReturn(fluids);
context = mock(ChunkContext.class);
when(context.getComplex()).thenReturn(complex);
writer = mock(MantleWriter.class);
Mantle<Matter> storage = mock(Mantle.class);
when(storage.getWorldHeight()).thenReturn(128);
when(writer.getMantle()).thenReturn(storage);
}
@Test
public void nestedGenerationPreservesOuterColumnHeights() throws Exception {
IrisCaveCarver3D carver = installProfile();
AtomicBoolean entered = new AtomicBoolean();
AtomicInteger calls = new AtomicInteger();
doAnswer(invocation -> {
int chunkX = invocation.getArgument(1);
int[] heights = invocation.getArgument(7);
int expectedHeight = chunkX < 0 ? 90 : 70;
assertEquals(expectedHeight, heights[0]);
calls.incrementAndGet();
if (entered.compareAndSet(false, true)) {
component.generateLayer(writer, -3, -3, context);
}
for (int height : heights) {
assertEquals(expectedHeight, height);
}
return 0;
}).when(carver).carve(any(), anyInt(), anyInt(), any(), anyDouble(), anyDouble(),
any(), any(), any(), any(), any());
component.generateLayer(writer, 2, 2, context);
assertEquals(2, calls.get());
assertReleased();
}
@Test
public void generationReleasesConfigurationReferencesWhenCarvingFails() throws Exception {
IrisCaveCarver3D carver = installProfile();
doAnswer(invocation -> {
throw new IllegalStateException("Carving failed");
}).when(carver).carve(any(), anyInt(), anyInt(), any(), anyDouble(), anyDouble(),
any(), any(), any(), any(), any());
assertThrows(IllegalStateException.class, () -> component.generateLayer(writer, 2, 2, context));
assertReleased();
}
@Test
public void changedProfilesReuseNumericBuffersWithoutKeepingProfileKeys() throws Exception {
Object firstBuffer = null;
for (int index = 0; index < 4; index++) {
installProfile();
component.generateLayer(writer, 2, 2, context);
Object scratch = assertReleased();
List<?> buffers = (List<?>) field(scratch, "weightBuffers");
assertEquals(1, buffers.size());
if (firstBuffer == null) {
firstBuffer = buffers.get(0);
} else {
assertSame(firstBuffer, buffers.get(0));
}
}
}
private IrisCaveCarver3D installProfile() throws Exception {
IrisCaveProfile profile = new IrisCaveProfile().setEnabled(true);
dimension.setCaveProfile(profile);
IrisCaveCarver3D carver = mock(IrisCaveCarver3D.class);
Map<IrisCaveProfile, IrisCaveCarver3D> carvers = new IdentityHashMap<>();
carvers.put(profile, carver);
Field cache = MantleCarvingComponent.class.getDeclaredField("profileCarvers");
cache.setAccessible(true);
cache.set(component, carvers);
return carver;
}
private Object assertReleased() throws Exception {
Field scratchField = MantleCarvingComponent.class.getDeclaredField("BLEND_SCRATCH");
scratchField.setAccessible(true);
ThreadLocal<?> scratchCache = (ThreadLocal<?>) scratchField.get(null);
Object scratch = scratchCache.get();
for (String name : List.of("profileField", "kernelProfiles", "fieldRegions", "fieldSurfaceBiomes", "fieldCaveBiomes")) {
for (Object value : (Object[]) field(scratch, name)) {
assertNull(value);
}
}
assertEquals(0, ((Map<?, ?>) field(scratch, "columnProfileWeights")).size());
assertEquals(0, ((List<?>) field(scratch, "profileOrder")).size());
assertFalse((boolean) field(scratch, "inUse"));
return scratch;
}
private Object field(Object object, String name) throws Exception {
Field field = object.getClass().getDeclaredField(name);
field.setAccessible(true);
return field.get(object);
}
}
@@ -1,55 +0,0 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
public class IrisBiomeCeilingLayerGuardTest {
private static IrisBiome biome(int layers, int ceilingLayers) {
IrisBiome biome = new IrisBiome();
KList<IrisBiomePaletteLayer> surface = new KList<>();
for (int i = 0; i < layers; i++) {
surface.add(new IrisBiomePaletteLayer().zero());
}
KList<IrisBiomePaletteLayer> ceiling = new KList<>();
for (int i = 0; i < ceilingLayers; i++) {
IrisBiomePaletteLayer layer = new IrisBiomePaletteLayer().zero();
layer.setMinHeight(0);
layer.setMaxHeight(0);
ceiling.add(layer);
}
biome.setLayers(surface);
biome.setCaveCeilingLayers(ceiling);
return biome;
}
@Test
public void moreCeilingLayersThanLayersMustNotThrow() {
IrisBiome biome = biome(1, 3);
KList<PlatformBlockState> result = biome.generateCeilingLayers(
new IrisDimension(), 0, 0, new RNG(1), 8, 64, mock(IrisData.class), null);
assertTrue("extra ceiling layers are skipped, not crashed on", result.isEmpty());
}
@Test
public void inRangeCeilingLayersKeepWorking() {
IrisBiome biome = biome(3, 2);
KList<PlatformBlockState> result = biome.generateCeilingLayers(
new IrisDimension(), 0, 0, new RNG(1), 8, 64, mock(IrisData.class), null);
assertTrue("zeroed palettes with zero heights produce no blocks", result.isEmpty());
}
@Test
public void omittedCeilingLayersLeaveCavesUnchanged() {
assertTrue(new IrisBiome().getCaveCeilingLayers().isEmpty());
}
}
@@ -0,0 +1,78 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
public class IrisBiomeCeilingLayerTest {
@Test
public void emitsEveryCeilingLayerWithoutSurfaceLayers() {
PlatformBlockState first = mock(PlatformBlockState.class);
PlatformBlockState second = mock(PlatformBlockState.class);
PlatformBlockState third = mock(PlatformBlockState.class);
IrisBiome biome = new IrisBiome().setLayers(new KList<>()).setCaveCeilingLayers(
new KList<>(layer(first, 2), layer(second, 1), layer(third, 2)));
KList<PlatformBlockState> result = biome.generateCeilingLayers(
new IrisDimension(), 13, -27, new RNG(1), 8, 64, null, null);
assertEquals(List.of(first, first, second, third, third), result);
assertNull(biome.getLayerHeightGenerators().getIfPresent());
}
@Test
public void ceilingThicknessDoesNotDependOnSurfaceDefinitionOrWarmOrder() {
IrisBiome first = variableBiome(1, 1);
IrisBiome second = variableBiome(7, 31);
RNG rng = new RNG(37);
second.getLayerHeightGenerators(rng, null);
for (int x = -200; x <= 200; x += 7) {
KList<PlatformBlockState> firstCeiling = first.generateCeilingLayers(
new IrisDimension(), x, -x, rng, 100, 120, null, null);
KList<PlatformBlockState> secondCeiling = second.generateCeilingLayers(
new IrisDimension(), x, -x, rng, 100, 120, null, null);
assertEquals(firstCeiling.size(), secondCeiling.size());
}
first.getLayerHeightGenerators(rng, null);
assertNotSame(first.getLayerHeightGenerators().getIfPresent().get(0),
first.getLayerCeilingHeightGenerators().getIfPresent().get(0));
}
@Test
public void capsCeilingLayersAtAvailableDepth() {
PlatformBlockState first = mock(PlatformBlockState.class);
PlatformBlockState second = mock(PlatformBlockState.class);
IrisBiome biome = new IrisBiome().setCaveCeilingLayers(new KList<>(layer(first, 2), layer(second, 3)));
assertEquals(List.of(first, first, second), biome.generateCeilingLayers(
new IrisDimension(), 0, 0, new RNG(1), 3, 64, null, null));
assertTrue(biome.generateCeilingLayers(
new IrisDimension(), 0, 0, new RNG(1), 0, 64, null, null).isEmpty());
}
@Test
public void omittedCeilingLayersLeaveCavesUnchanged() {
assertTrue(new IrisBiome().getCaveCeilingLayers().isEmpty());
}
static IrisBiomePaletteLayer layer(PlatformBlockState block, int thickness) {
IrisBiomePaletteLayer layer = new IrisBiomePaletteLayer().setMinHeight(thickness).setMaxHeight(thickness);
layer.getBlockData().aquire(() -> new KList<>(block));
return layer;
}
private static IrisBiome variableBiome(int surfaceMinimum, int surfaceMaximum) {
PlatformBlockState block = mock(PlatformBlockState.class);
return new IrisBiome()
.setLayers(new KList<>(layer(block, surfaceMinimum).setMaxHeight(surfaceMaximum)))
.setCaveCeilingLayers(new KList<>(layer(block, 1).setMaxHeight(9)));
}
}
@@ -0,0 +1,35 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
public class IrisBiomeLockedLayerTest {
@Test
public void repeatsLockedLayersAcrossTheReferenceHeight() {
PlatformBlockState first = mock(PlatformBlockState.class);
PlatformBlockState second = mock(PlatformBlockState.class);
PlatformBlockState third = mock(PlatformBlockState.class);
IrisBiome biome = new IrisBiome().setLockLayers(true).setLockLayersMax(8).setLayers(new KList<>(
IrisBiomeCeilingLayerTest.layer(first, 1),
IrisBiomeCeilingLayerTest.layer(second, 1),
IrisBiomeCeilingLayerTest.layer(third, 1)));
List<PlatformBlockState> cycle = List.of(first, second, third);
int[] heights = {1, 509, 512, 513, 1000, Integer.MAX_VALUE};
int[][] expectedCycles = {{1, 0, 2}, {0, 2, 1}, {0, 2, 1}, {2, 1, 0}, {1, 0, 2}, {1, 0, 2}};
for (int index = 0; index < heights.length; index++) {
KList<PlatformBlockState> blocks = biome.generateLockedLayers(
13, -27, new RNG(37), 8, heights[index], null, null);
assertEquals(8, blocks.size());
for (int depth = 0; depth < blocks.size(); depth++) {
assertEquals(cycle.get(expectedCycles[index][depth % 3]), blocks.get(depth));
}
}
}
}