Reject deposit clumps early

- Ellipsoid footprint dry run replays the build draws without building
- Terrain-scoped clumps above every column limit are dropped before building
- Surface biome filters no chunk column passes skip the deposit
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-28 11:35:56 -04:00
1 parent b47c7ae6aa
commit ec1984949b
4 files changed
+237 -13

No files matched your search

@@ -53,6 +53,8 @@ import lombok.experimental.Accessors;
@Description("Creates ore & other block deposits underground") @Description("Creates ore & other block deposits underground")
@Data @Data
public class IrisDepositGenerator { public class IrisDepositGenerator {
private static final int FOOTPRINT_MAXIMUM_SIZE = 64;
private static final ThreadLocal<FootprintCells> FOOTPRINT_CELLS = ThreadLocal.withInitial(FootprintCells::new);
private static final long FNV_OFFSET_BASIS = 0xcbf29ce484222325L; private static final long FNV_OFFSET_BASIS = 0xcbf29ce484222325L;
private static final long FNV_PRIME = 0x100000001b3L; private static final long FNV_PRIME = 0x100000001b3L;
@@ -267,6 +269,42 @@ public class IrisDepositGenerator {
return new IrisObject(1, 1, 1); return new IrisObject(1, 1, 1);
} }
KSet<BlockPosition> cells = new KSet<>();
vanillaEllipsoidCells(rng, size, (x, y, z) -> cells.add(new BlockPosition(x, y, z)));
return objectFromCells(cells, rng, rdata);
}
/**
* Advances {@code rng} exactly as {@link #getClump(Engine, RNG, IrisData)} would and reports the clump's width
* and lowest block offset without building it, so a caller can reject the clump first and replay the build
* only for clumps it keeps. Returns null without drawing when the shape has no footprint path.
*/
public ClumpFootprint sampleClumpFootprint(RNG rng, IrisData rdata) {
if (shape != IrisDepositShape.VANILLA_ELLIPSOID || minSize > FOOTPRINT_MAXIMUM_SIZE
|| maxSize > FOOTPRINT_MAXIMUM_SIZE) {
return null;
}
int paletteSize = getBlockData(rdata).size();
if (paletteSize == 0) {
return null;
}
int size = rng.i(minSize, maxSize + 1);
if (size <= 0) {
return ClumpFootprint.EMPTY;
}
FootprintCells cells = FOOTPRINT_CELLS.get();
cells.reset();
vanillaEllipsoidCells(rng, size, cells);
if (cells.count == 0) {
return ClumpFootprint.EMPTY;
}
for (int cell = 0; cell < cells.count; cell++) {
rng.i(0, paletteSize);
}
return new ClumpFootprint(cells.extentX * 2 + 1, cells.minY, false);
}
private static void vanillaEllipsoidCells(RNG rng, int size, CellSink sink) {
float angle = rng.nextFloat() * (float) Math.PI; float angle = rng.nextFloat() * (float) Math.PI;
float reach = size / 8F; float reach = size / 8F;
double startX = Math.sin(angle) * reach; double startX = Math.sin(angle) * reach;
@@ -309,7 +347,6 @@ public class IrisDepositGenerator {
} }
} }
KSet<BlockPosition> cells = new KSet<>();
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
double radius = nodes[i * 4 + 3]; double radius = nodes[i * 4 + 3];
if (radius < 0D) { if (radius < 0D) {
@@ -338,14 +375,12 @@ public class IrisDepositGenerator {
for (int z = minZ; z <= maxZ; z++) { for (int z = minZ; z <= maxZ; z++) {
double nz = (z + 0.5D - centerZ) / radius; double nz = (z + 0.5D - centerZ) / radius;
if (nx * nx + ny * ny + nz * nz < 1D) { if (nx * nx + ny * ny + nz * nz < 1D) {
cells.add(new BlockPosition(x, y, z)); sink.add(x, y, z);
} }
} }
} }
} }
} }
return objectFromCells(cells, rng, rdata);
} }
IrisObject generateVanillaScattered(RNG rng, IrisData rdata, int size) { IrisObject generateVanillaScattered(RNG rng, IrisData rdata, int size) {
@@ -564,4 +599,52 @@ public class IrisDepositGenerator {
record ClumpCacheKey(long depositSeed, int minSize, int maxSize) { record ClumpCacheKey(long depositSeed, int minSize, int maxSize) {
} }
@FunctionalInterface
private interface CellSink {
void add(int x, int y, int z);
}
public record ClumpFootprint(int width, int minY, boolean empty) {
static final ClumpFootprint EMPTY = new ClumpFootprint(1, 0, true);
}
private static final class FootprintCells implements CellSink {
private static final int OFFSET = 32;
private static final int AXIS = 64;
private final long[] occupied = new long[AXIS * AXIS * AXIS / 64];
private final int[] touched = new int[AXIS * AXIS * AXIS / 64];
private int touchedCount;
private int count;
private int extentX;
private int minY;
private void reset() {
for (int index = 0; index < touchedCount; index++) {
occupied[touched[index]] = 0L;
}
touchedCount = 0;
count = 0;
extentX = 0;
minY = Integer.MAX_VALUE;
}
@Override
public void add(int x, int y, int z) {
int index = ((x + OFFSET) * AXIS + (y + OFFSET)) * AXIS + (z + OFFSET);
int word = index >>> 6;
long bit = 1L << (index & 63);
long current = occupied[word];
if ((current & bit) != 0L) {
return;
}
if (current == 0L) {
touched[touchedCount++] = word;
}
occupied[word] = current | bit;
count++;
extentX = Math.max(extentX, Math.abs(x));
minY = Math.min(minY, y);
}
}
} }
@@ -23,6 +23,7 @@ import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.EngineAssignedModifier; import art.arcane.iris.generation.runtime.EngineAssignedModifier;
import art.arcane.iris.generation.runtime.DimensionStackLayout; import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.decoration.IrisDepositBiomeScope;
import art.arcane.iris.generation.decoration.IrisDepositGenerator; import art.arcane.iris.generation.decoration.IrisDepositGenerator;
import art.arcane.iris.generation.decoration.IrisDepositHeightDistribution; import art.arcane.iris.generation.decoration.IrisDepositHeightDistribution;
import art.arcane.iris.generation.decoration.IrisDepositPlacementScope; import art.arcane.iris.generation.decoration.IrisDepositPlacementScope;
@@ -96,7 +97,7 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
Throwable callerFailure = null; Throwable callerFailure = null;
try { try {
for (int i = 0; i < generators.size(); i++) { for (int i = 0; i < generators.size(); i++) {
DepositPlan plan = plan(generators.get(i), rng.nextParallelRNG(seed * (i + 1L))); DepositPlan plan = plan(generators.get(i), rng.nextParallelRNG(seed * (i + 1L)), context);
for (int first = 0; first < plan.attempts(); first += CLUMPS_PER_BATCH) { for (int first = 0; first < plan.attempts(); first += CLUMPS_PER_BATCH) {
int firstAttempt = first; int firstAttempt = first;
int limit = Math.min(plan.attempts(), first + CLUMPS_PER_BATCH); int limit = Math.min(plan.attempts(), first + CLUMPS_PER_BATCH);
@@ -168,20 +169,48 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
} }
public void generate(IrisDepositGenerator k, MantleChunk chunk, Hunk<NativeBlockState> data, RNG rng, int cx, int cz, boolean safe, HeightMap he, ChunkContext context) { public void generate(IrisDepositGenerator k, MantleChunk chunk, Hunk<NativeBlockState> data, RNG rng, int cx, int cz, boolean safe, HeightMap he, ChunkContext context) {
DepositPlan plan = plan(k, rng); DepositPlan plan = plan(k, rng, context);
for (int first = 0; first < plan.attempts(); first += CLUMPS_PER_BATCH) { for (int first = 0; first < plan.attempts(); first += CLUMPS_PER_BATCH) {
int limit = Math.min(plan.attempts(), first + CLUMPS_PER_BATCH); int limit = Math.min(plan.attempts(), first + CLUMPS_PER_BATCH);
place(prepare(plan, first, limit, cx, cz, he, context), chunk, data, cx, cz, he, context); place(prepare(plan, first, limit, cx, cz, he, context), chunk, data, cx, cz, he, context);
} }
} }
private DepositPlan plan(IrisDepositGenerator generator, RNG rng) { private DepositPlan plan(IrisDepositGenerator generator, RNG rng, ChunkContext context) {
if (generator.getSpawnChance() < rng.d()) { if (generator.getSpawnChance() < rng.d()) {
return new DepositPlan(generator, rng.getSeed(), false, 0); return new DepositPlan(generator, rng.getSeed(), false, 0);
} }
boolean ore = generator.isOre(getData()); boolean ore = generator.isOre(getData());
int attempts = rng.i(generator.getMinPerChunk(), generator.getMaxPerChunk() + 1); int attempts = rng.i(generator.getMinPerChunk(), generator.getMaxPerChunk() + 1);
return new DepositPlan(generator, rng.getSeed(), ore, attempts); return new DepositPlan(generator, rng.getSeed(), ore,
attempts == 0 || surfaceBiomeMayMatch(generator, context) ? attempts : 0);
}
/**
* A surface-scoped biome filter reads the surface biome of the clump's own column, so when no column of the
* chunk passes it every clump is rejected at that check. Each clump draws from its own seeded RNG, so skipping
* them all up front changes nothing else.
*/
private static boolean surfaceBiomeMayMatch(IrisDepositGenerator generator, ChunkContext context) {
if (generator.getBiomeScope() != IrisDepositBiomeScope.SURFACE
|| (generator.getIncludedBiomes() == null || generator.getIncludedBiomes().isEmpty())
&& (generator.getExcludedBiomes() == null || generator.getExcludedBiomes().isEmpty())) {
return true;
}
IrisBiome checked = null;
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
IrisBiome surface = context.getBiome().get(x, z);
if (surface == checked) {
continue;
}
if (generator.matchesBiome(surface, null)) {
return true;
}
checked = surface;
}
}
return false;
} }
private PreparedDeposit prepare(DepositPlan plan, int first, int limit, int cx, int cz, HeightMap he, ChunkContext context) { private PreparedDeposit prepare(DepositPlan plan, int first, int limit, int cx, int cz, HeightMap he, ChunkContext context) {
@@ -190,15 +219,18 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
boolean needsCaveBiome = oreDeposit || k.usesCaveBiomeFilter(); boolean needsCaveBiome = oreDeposit || k.usesCaveBiomeFilter();
IrisDimensionCarvingResolver.State carvingState = needsCaveBiome ? new IrisDimensionCarvingResolver.State() : null; IrisDimensionCarvingResolver.State carvingState = needsCaveBiome ? new IrisDimensionCarvingResolver.State() : null;
List<PreparedClump> clumps = new ArrayList<>(limit - first); List<PreparedClump> clumps = new ArrayList<>(limit - first);
int terrainLimit = Integer.MIN_VALUE;
for (int l = first; l < limit; l++) { for (int l = first; l < limit; l++) {
RNG clumpRng = new RNG(clumpSeed(plan.seed(), l)); long clumpSeed = clumpSeed(plan.seed(), l);
RNG clumpRng = new RNG(clumpSeed);
if (k.getPerClumpSpawnChance() < clumpRng.d()) { if (k.getPerClumpSpawnChance() < clumpRng.d()) {
continue; continue;
} }
IrisObject clump = k.getClump(getEngine(), clumpRng, getData()); IrisDepositGenerator.ClumpFootprint footprint = k.sampleClumpFootprint(clumpRng, getData());
IrisObject clump = footprint == null ? k.getClump(getEngine(), clumpRng, getData()) : null;
int dim = clump.getW(); int dim = footprint == null ? clump.getW() : footprint.width();
int min = dim / 2; int min = dim / 2;
int max = (int) (16D - dim / 2D); int max = (int) (16D - dim / 2D);
@@ -262,6 +294,20 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
} }
} }
if (clump == null) {
if (k.getPlacementScope() == IrisDepositPlacementScope.TERRAIN && !footprint.empty()) {
if (terrainLimit == Integer.MIN_VALUE) {
terrainLimit = chunkTerrainLimit(cx, cz, he, context, k.getSurfaceClearance());
}
if (y + footprint.minY() > terrainLimit) {
continue;
}
}
RNG replay = new RNG(clumpSeed);
replay.d();
clump = k.getClump(getEngine(), replay, getData());
}
clumps.add(new PreparedClump(clump, x, y, z, clumpRng)); clumps.add(new PreparedClump(clump, x, y, z, clumpRng));
} }
return new PreparedDeposit(k, oreDeposit, clumps); return new PreparedDeposit(k, oreDeposit, clumps);
@@ -350,6 +396,20 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
: context.getRoundedHeight(localX, localZ); : context.getRoundedHeight(localX, localZ);
} }
/**
* The highest block a TERRAIN-scoped deposit may place anywhere in the chunk; a clump whose lowest block sits
* above it would have every block rejected by {@link #placementSurfaceAllows}.
*/
private int chunkTerrainLimit(int cx, int cz, HeightMap he, ChunkContext context, int surfaceClearance) {
int highest = Integer.MIN_VALUE;
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
highest = Math.max(highest, getDepositTerrainSurface(cx, cz, x, z, he, context));
}
}
return highest - Math.max(0, surfaceClearance);
}
static int depositSurfaceLimit(int surfaceY) { static int depositSurfaceLimit(int surfaceY) {
return depositSurfaceLimit(surfaceY, 7); return depositSurfaceLimit(surfaceY, 7);
} }
@@ -2,6 +2,8 @@ package art.arcane.iris.generation.decoration;
import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.block.IrisBlockData; import art.arcane.iris.generation.block.IrisBlockData;
import art.arcane.iris.generation.block.VectorMap;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.loading.IrisData;
import art.arcane.volmlib.util.documentation.Description; import art.arcane.volmlib.util.documentation.Description;
@@ -224,6 +226,38 @@ public class IrisDepositTuningTest {
assertNotEquals(17L * samples, largeBlocks); assertNotEquals(17L * samples, largeBlocks);
} }
@Test
public void ellipsoidFootprintConsumesTheBuildDrawsAndMatchesItsGeometry() {
IrisData data = mock(IrisData.class);
IrisDepositGenerator generator = generatorWithState(data, true);
generator.setShape(IrisDepositShape.VANILLA_ELLIPSOID);
for (int size : new int[]{0, 1, 3, 4, 9, 17, 25, 64}) {
generator.setMinSize(size).setMaxSize(size);
for (long seed = 0; seed < 300; seed++) {
RNG built = new RNG(seed);
IrisObject clump = generator.getClump(null, built, data);
RNG sampled = new RNG(seed);
IrisDepositGenerator.ClumpFootprint footprint = generator.sampleClumpFootprint(sampled, data);
assertNotNull(footprint);
assertEquals(clump.getW(), footprint.width());
assertEquals(clump.getBlocks().size() == 0, footprint.empty());
if (!footprint.empty()) {
int minimumY = Integer.MAX_VALUE;
VectorMap<NativeBlockState>.Cursor cursor = clump.getBlocks().cursor();
while (cursor.next()) {
minimumY = Math.min(minimumY, cursor.key().getBlockY());
}
assertEquals(minimumY, footprint.minY());
}
assertEquals(built.nextLong(), sampled.nextLong());
}
}
generator.setMinSize(65).setMaxSize(65);
assertNull(generator.sampleClumpFootprint(new RNG(1L), data));
generator.setShape(IrisDepositShape.VANILLA_SCATTERED).setMinSize(4).setMaxSize(4);
assertNull(generator.sampleClumpFootprint(new RNG(1L), data));
}
@Test @Test
public void vanillaScatteredSizeIsAnUpperCandidateBound() { public void vanillaScatteredSizeIsAnUpperCandidateBound() {
IrisData data = mock(IrisData.class); IrisData data = mock(IrisData.class);
@@ -5,12 +5,14 @@ import art.arcane.iris.testsupport.KeyedBlockState;
import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.decoration.IrisDepositBiomeScope;
import art.arcane.iris.generation.decoration.IrisDepositGenerator; import art.arcane.iris.generation.decoration.IrisDepositGenerator;
import art.arcane.iris.generation.decoration.IrisDepositHeightDistribution; import art.arcane.iris.generation.decoration.IrisDepositHeightDistribution;
import art.arcane.iris.generation.decoration.IrisDepositShape; import art.arcane.iris.generation.decoration.IrisDepositShape;
import art.arcane.iris.generation.decoration.IrisDepositVariant; import art.arcane.iris.generation.decoration.IrisDepositVariant;
import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion; import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.structure.object.IrisObject;
import art.arcane.iris.spi.IrisPlatform; import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
@@ -25,6 +27,7 @@ import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern; import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.math.RNG;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.ClassRule; import org.junit.ClassRule;
@@ -99,13 +102,26 @@ public class IrisDepositModifierParityTest {
} }
} }
@Test
public void unmatchableSurfaceBiomeFiltersSkipClumpsWithoutChangingOutput() {
long[] expected = {-4609692219192155345L, 784L, 2118L, 53L, 67L, 11L};
AtomicInteger clumps = new AtomicInteger();
assertArrayEquals(expected, generateConfigured(101L, false, true, false, new AtomicInteger(), clumps));
assertEquals(0, clumps.get());
}
private long[] generateConfigured(long seed, boolean multicore) { private long[] generateConfigured(long seed, boolean multicore) {
return generateConfigured(seed, multicore, true, false, new AtomicInteger()); return generateConfigured(seed, multicore, true, false, new AtomicInteger());
} }
@SuppressWarnings("unchecked")
private long[] generateConfigured(long seed, boolean multicore, boolean caveVariant, boolean enumerablePack, private long[] generateConfigured(long seed, boolean multicore, boolean caveVariant, boolean enumerablePack,
AtomicInteger caveBiomeLookups) { AtomicInteger caveBiomeLookups) {
return generateConfigured(seed, multicore, caveVariant, enumerablePack, caveBiomeLookups, null);
}
@SuppressWarnings("unchecked")
private long[] generateConfigured(long seed, boolean multicore, boolean caveVariant, boolean enumerablePack,
AtomicInteger caveBiomeLookups, AtomicInteger unmatchableClumps) {
Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS); Engine engine = mock(Engine.class, RETURNS_DEEP_STUBS);
ChunkContext context = mock(ChunkContext.class, RETURNS_DEEP_STUBS); ChunkContext context = mock(ChunkContext.class, RETURNS_DEEP_STUBS);
IrisDimension dimension = new IrisDimension(); IrisDimension dimension = new IrisDimension();
@@ -136,6 +152,16 @@ public class IrisDepositModifierParityTest {
gold.setExcludedBiomes(new KList<>("allowed")); gold.setExcludedBiomes(new KList<>("allowed"));
gold.setDiscardChanceOnAirExposure(0.3D); gold.setDiscardChanceOnAirExposure(0.3D);
surface.getDeposits().add(gold); surface.getDeposits().add(gold);
if (unmatchableClumps != null) {
IrisDepositGenerator unmatchable = new CountingPaletteGenerator(state("emerald_ore"), unmatchableClumps)
.setShape(IrisDepositShape.VANILLA_ELLIPSOID).setMinSize(9).setMaxSize(9)
.setMinPerChunk(90).setMaxPerChunk(90).setMinHeight(-4).setMaxHeight(44)
.setHeightDistribution(IrisDepositHeightDistribution.UNIFORM).setSurfaceClearance(0)
.setReplaceableBlocks(new KList<>("stone"))
.setBiomeScope(IrisDepositBiomeScope.SURFACE)
.setIncludedBiomes(new KList<>("elsewhere"));
surface.getDeposits().add(unmatchable);
}
when(engine.getHeight()).thenReturn(48); when(engine.getHeight()).thenReturn(48);
when(engine.getMinHeight()).thenReturn(-64); when(engine.getMinHeight()).thenReturn(-64);
when(engine.getSeedManager().getDeposit()).thenReturn(seed); when(engine.getSeedManager().getDeposit()).thenReturn(seed);
@@ -404,7 +430,7 @@ public class IrisDepositModifierParityTest {
} }
} }
private static final class PaletteGenerator extends IrisDepositGenerator { private static class PaletteGenerator extends IrisDepositGenerator {
private final KList<NativeBlockState> blocks; private final KList<NativeBlockState> blocks;
private PaletteGenerator(NativeBlockState block) { private PaletteGenerator(NativeBlockState block) {
@@ -416,4 +442,25 @@ public class IrisDepositModifierParityTest {
return blocks; return blocks;
} }
} }
private static final class CountingPaletteGenerator extends PaletteGenerator {
private final AtomicInteger clumps;
private CountingPaletteGenerator(NativeBlockState block, AtomicInteger clumps) {
super(block);
this.clumps = clumps;
}
@Override
public IrisObject getClump(Engine engine, RNG rng, IrisData data) {
clumps.incrementAndGet();
return super.getClump(engine, rng, data);
}
@Override
public ClumpFootprint sampleClumpFootprint(RNG rng, IrisData data) {
clumps.incrementAndGet();
return super.sampleClumpFootprint(rng, data);
}
}
} }