diff --git a/core/src/main/java/art/arcane/iris/generation/cache/ConcurrentClockCache.java b/core/src/main/java/art/arcane/iris/generation/cache/ConcurrentClockCache.java new file mode 100644 index 000000000..a999fbb97 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/cache/ConcurrentClockCache.java @@ -0,0 +1,128 @@ +package art.arcane.iris.generation.cache; + +import art.arcane.volmlib.util.cache.CacheKey; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReferenceArray; +import java.util.function.Consumer; + +/** + * A bounded, lock-free, lossy long-keyed cache for memoized values. Keys hash into 4-way sets and a full set + * evicts with CLOCK (second chance), so reads never write shared state except to set a reference bit once. + * Concurrent inserts of the same key may both compute; the first published value wins. + */ +public final class ConcurrentClockCache { + private static final int WAYS = 4; + + private final AtomicReferenceArray> slots; + private final long sets; + + public ConcurrentClockCache(int capacity) { + if (capacity <= 0) { + throw new IllegalArgumentException("Cache capacity must be positive"); + } + int setCount = Math.ceilDiv(capacity, WAYS); + sets = setCount; + slots = new AtomicReferenceArray<>(Math.multiplyExact(setCount, WAYS)); + } + + public int capacity() { + return slots.length(); + } + + public V get(long key) { + long hash = CacheKey.mix(key); + int base = base(hash); + for (int way = 0; way < WAYS; way++) { + Entry entry = slots.getAcquire(base + way); + if (entry != null && entry.key == key) { + entry.touch(); + return entry.value; + } + } + return null; + } + + public V putIfAbsent(long key, V value) { + Objects.requireNonNull(value, "Cached value"); + long hash = CacheKey.mix(key); + int base = base(hash); + int hand = (int) (hash >>> 60) & (WAYS - 1); + while (true) { + int empty = -1; + for (int way = 0; way < WAYS; way++) { + Entry entry = slots.getAcquire(base + way); + if (entry == null) { + if (empty < 0) { + empty = way; + } + } else if (entry.key == key) { + entry.touch(); + return entry.value; + } + } + Entry created = new Entry<>(key, value); + if (empty >= 0) { + if (slots.compareAndSet(base + empty, null, created)) { + return value; + } + continue; + } + int victim = -1; + Entry victimEntry = null; + for (int step = 0; step < WAYS * 2 && victim < 0; step++) { + int way = (hand + step) & (WAYS - 1); + Entry entry = slots.getAcquire(base + way); + if (entry == null || !entry.referenced) { + victim = way; + victimEntry = entry; + } else { + entry.referenced = false; + } + } + if (victim < 0) { + victim = hand; + victimEntry = slots.getAcquire(base + victim); + } + if (slots.compareAndSet(base + victim, victimEntry, created)) { + return value; + } + } + } + + public void forEach(Consumer action) { + for (int index = 0; index < slots.length(); index++) { + Entry entry = slots.getAcquire(index); + if (entry != null) { + action.accept(entry.value); + } + } + } + + public void clear() { + for (int index = 0; index < slots.length(); index++) { + slots.setRelease(index, null); + } + } + + private int base(long hash) { + return (int) (((hash & 0xffffffffL) * sets) >>> 32) * WAYS; + } + + private static final class Entry { + private final long key; + private final V value; + private boolean referenced; + + private Entry(long key, V value) { + this.key = key; + this.value = value; + } + + private void touch() { + if (!referenced) { + referenced = true; + } + } + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/decoration/IrisDepositGenerator.java b/core/src/main/java/art/arcane/iris/generation/decoration/IrisDepositGenerator.java index bb73646a6..80ff26a4f 100644 --- a/core/src/main/java/art/arcane/iris/generation/decoration/IrisDepositGenerator.java +++ b/core/src/main/java/art/arcane/iris/generation/decoration/IrisDepositGenerator.java @@ -53,6 +53,8 @@ import lombok.experimental.Accessors; @Description("Creates ore & other block deposits underground") @Data public class IrisDepositGenerator { + private static final int FOOTPRINT_MAXIMUM_SIZE = 64; + private static final ThreadLocal FOOTPRINT_CELLS = ThreadLocal.withInitial(FootprintCells::new); private static final long FNV_OFFSET_BASIS = 0xcbf29ce484222325L; private static final long FNV_PRIME = 0x100000001b3L; @@ -267,6 +269,42 @@ public class IrisDepositGenerator { return new IrisObject(1, 1, 1); } + KSet 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 reach = size / 8F; double startX = Math.sin(angle) * reach; @@ -309,7 +347,6 @@ public class IrisDepositGenerator { } } - KSet cells = new KSet<>(); for (int i = 0; i < size; i++) { double radius = nodes[i * 4 + 3]; if (radius < 0D) { @@ -338,14 +375,12 @@ public class IrisDepositGenerator { for (int z = minZ; z <= maxZ; z++) { double nz = (z + 0.5D - centerZ) / radius; 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) { @@ -564,4 +599,52 @@ public class IrisDepositGenerator { 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); + } + } } diff --git a/core/src/main/java/art/arcane/iris/generation/decoration/IrisOreGenerator.java b/core/src/main/java/art/arcane/iris/generation/decoration/IrisOreGenerator.java index aeacc660b..b4e7dd711 100644 --- a/core/src/main/java/art/arcane/iris/generation/decoration/IrisOreGenerator.java +++ b/core/src/main/java/art/arcane/iris/generation/decoration/IrisOreGenerator.java @@ -71,7 +71,10 @@ public class IrisOreGenerator { return null; } - CNG chance = chanceCache.aquire(() -> chanceStyle.create(rng, data)); + CNG chance = chanceCache.getIfPresent(); + if (chance == null) { + chance = chanceCache.aquire(() -> chanceStyle.create(rng, data)); + } if (chance.noise(x, y, z) > threshold) { return null; diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/CaveDensityGrid.java b/core/src/main/java/art/arcane/iris/generation/mantle/CaveDensityGrid.java new file mode 100644 index 000000000..b0064d1a2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/mantle/CaveDensityGrid.java @@ -0,0 +1,84 @@ +package art.arcane.iris.generation.mantle; + +import art.arcane.iris.generation.cache.ConcurrentClockCache; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReferenceArray; + +/** + * Shares the adaptive cave planes' 8-block density lattice between neighbouring chunks. Every lattice column on a + * chunk edge is sampled by two chunks and every corner by four, with identical coordinates and carver noise, so a + * value computed once is reused bit for bit. Racing writers store the same value. + */ +final class CaveDensityGrid { + static final int LATTICE_SHIFT = 3; + private static final int SEGMENT_SHIFT = 5; + private static final int SEGMENT_SIZE = 1 << SEGMENT_SHIFT; + private static final long COORDINATE_MASK = 0xFFFFFFL; + private static final VarHandle VALUES = MethodHandles.arrayElementVarHandle(double[].class); + private static final AtomicInteger OWNERS = new AtomicInteger(); + + private final ConcurrentClockCache columns; + + CaveDensityGrid(int capacity) { + columns = new ConcurrentClockCache<>(capacity); + } + + static int nextOwner() { + return OWNERS.getAndIncrement(); + } + + Column column(Object owner, int ownerId, int height, int latticeX, int latticeZ) { + long key = (long) (ownerId & 0xFFFF) << 48 | (latticeX & COORDINATE_MASK) << 24 | (latticeZ & COORDINATE_MASK); + Column cached = columns.get(key); + if (cached == null) { + cached = columns.putIfAbsent(key, new Column(owner, latticeX, latticeZ, height)); + } + return cached.owner == owner && cached.latticeX == latticeX && cached.latticeZ == latticeZ ? cached : null; + } + + void clear() { + columns.clear(); + } + + static final class Column { + private final Object owner; + private final int latticeX; + private final int latticeZ; + private final AtomicReferenceArray segments; + + private Column(Object owner, int latticeX, int latticeZ, int height) { + this.owner = owner; + this.latticeX = latticeX; + this.latticeZ = latticeZ; + segments = new AtomicReferenceArray<>(Math.ceilDiv(Math.max(1, height), SEGMENT_SIZE)); + } + + double get(int y) { + int segment = y >> SEGMENT_SHIFT; + if (segment >= segments.length()) { + return Double.NaN; + } + double[] values = segments.getAcquire(segment); + return values == null ? Double.NaN : (double) VALUES.getOpaque(values, y & (SEGMENT_SIZE - 1)); + } + + void set(int y, double value) { + int segment = y >> SEGMENT_SHIFT; + if (segment >= segments.length()) { + return; + } + double[] values = segments.getAcquire(segment); + if (values == null) { + double[] created = new double[SEGMENT_SIZE]; + Arrays.fill(created, Double.NaN); + double[] published = segments.compareAndExchange(segment, null, created); + values = published == null ? created : published; + } + VALUES.setOpaque(values, y & (SEGMENT_SIZE - 1), value); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/IrisCaveCarver3D.java b/core/src/main/java/art/arcane/iris/generation/mantle/IrisCaveCarver3D.java index d3059fdba..0f6858c6e 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/IrisCaveCarver3D.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/IrisCaveCarver3D.java @@ -61,6 +61,7 @@ public class IrisCaveCarver3D { private static final int MINIMUM_PARALLEL_DENSITY_CELLS = 16_384; private static final int MAXIMUM_DENSITY_WORKERS = 4; private static final int[] LATTICE_TILE_OFFSETS = {0, 1, 16, 17}; + private static final int STANDALONE_DENSITY_GRID_COLUMNS = 1024; private final Engine engine; private final IrisData data; @@ -96,9 +97,16 @@ public class IrisCaveCarver3D { private final int densityMaximumY; private final ThreadLocal carving = ThreadLocal.withInitial(() -> false); private final ThreadLocal scratchCache = ThreadLocal.withInitial(CaveCarveScratch::new); + private final CaveDensityGrid densityGrid; + private final int densityGridOwner = CaveDensityGrid.nextOwner(); public IrisCaveCarver3D(Engine engine, IrisCaveProfile profile) { + this(engine, profile, new CaveDensityGrid(STANDALONE_DENSITY_GRID_COLUMNS)); + } + + IrisCaveCarver3D(Engine engine, IrisCaveProfile profile, CaveDensityGrid densityGrid) { this.engine = engine; + this.densityGrid = densityGrid; this.data = engine.getData(); this.profile = profile; this.carveAir = new MatterCavern(true, "", LIQUID_AIR); @@ -1542,7 +1550,7 @@ public class IrisCaveCarver3D { int rowOffset = sampleXIndex * axisSamples; for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) { int sampleLocalZ = Math.min(sampleZIndex * adaptiveSampleStep, 16); - adaptivePlaneDensity[rowOffset + sampleZIndex] = sampleDensityNoWarpNoModules(x, y, z0 + sampleLocalZ); + adaptivePlaneDensity[rowOffset + sampleZIndex] = latticeDensity(scratch, x, y, z0 + sampleLocalZ); } } @@ -1595,7 +1603,7 @@ public class IrisCaveCarver3D { int rowOffset = sampleXIndex * axisSamples; for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) { int sampleLocalZ = Math.min(sampleZIndex * adaptiveSampleStep, 16); - adaptivePlaneDensity[rowOffset + sampleZIndex] = sampleDensityNoWarpNoModules(x, y, z0 + sampleLocalZ); + adaptivePlaneDensity[rowOffset + sampleZIndex] = latticeDensity(scratch, x, y, z0 + sampleLocalZ); } } @@ -1643,7 +1651,7 @@ public class IrisCaveCarver3D { int rowOffset = sampleXIndex * axisSamples; for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) { int sampleLocalZ = Math.min(sampleZIndex * adaptiveSampleStep, 16); - adaptivePlaneDensity[rowOffset + sampleZIndex] = sampleDensityWarpOnly(scratch, x, y, z0 + sampleLocalZ); + adaptivePlaneDensity[rowOffset + sampleZIndex] = latticeDensity(scratch, x, y, z0 + sampleLocalZ); } } @@ -1696,14 +1704,7 @@ public class IrisCaveCarver3D { int rowOffset = sampleXIndex * axisSamples; for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) { int sampleLocalZ = Math.min(sampleZIndex * adaptiveSampleStep, 16); - adaptivePlaneDensity[rowOffset + sampleZIndex] = sampleDensityWarpModules( - scratch, - x, - y, - z0 + sampleLocalZ, - localModules, - activeModuleCount - ); + adaptivePlaneDensity[rowOffset + sampleZIndex] = latticeDensity(scratch, x, y, z0 + sampleLocalZ); } } @@ -2463,6 +2464,33 @@ public class IrisCaveCarver3D { scratch.adaptiveGeometryAxisCells = axisCells; } + private double latticeDensity(CaveCarveScratch scratch, int x, int y, int z) { + int latticeMask = (1 << CaveDensityGrid.LATTICE_SHIFT) - 1; + CaveDensityGrid.Column column = ((x | z) & latticeMask) != 0 ? null : densityGrid.column(this, densityGridOwner, + densityMaximumY + 1, x >> CaveDensityGrid.LATTICE_SHIFT, z >> CaveDensityGrid.LATTICE_SHIFT); + if (column == null) { + return samplePlaneDensity(scratch, x, y, z); + } + double cached = column.get(y); + if (!Double.isNaN(cached)) { + return cached; + } + double density = samplePlaneDensity(scratch, x, y, z); + column.set(y, density); + return density; + } + + private double samplePlaneDensity(CaveCarveScratch scratch, int x, int y, int z) { + if (!hasWarp) { + return sampleDensityNoWarpNoModules(x, y, z); + } + int activeModuleCount = hasModules ? prepareActiveModules(scratch, y) : 0; + if (activeModuleCount == 0) { + return sampleDensityWarpOnly(scratch, x, y, z); + } + return sampleDensityWarpModules(scratch, x, y, z, scratch.activeModules, activeModuleCount); + } + private double sampleDensityNoWarpNoModules(int x, int y, int z) { double density = baseDensity.noiseFastSigned3D(x, y, z) * baseWeight; density += detailDensity.noiseFastSigned3D(x, y, z) * detailWeight; diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/MantleCarvingComponent.java b/core/src/main/java/art/arcane/iris/generation/mantle/MantleCarvingComponent.java index 237ff2041..9c8dbf454 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/MantleCarvingComponent.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/MantleCarvingComponent.java @@ -54,6 +54,8 @@ public class MantleCarvingComponent extends IrisMantleComponent { 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; + /** Lattice columns kept for neighbouring chunks: about 3 KB each on land, roughly 50 MB when full. */ + private static final int DENSITY_GRID_COLUMNS = 16_384; 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]; @@ -62,6 +64,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { private static final ThreadLocal BLEND_SCRATCH = ThreadLocal.withInitial(BlendScratch::new); private final Object profileCarverLock = new Object(); + private final CaveDensityGrid densityGrid = new CaveDensityGrid(DENSITY_GRID_COLUMNS); private volatile Map profileCarvers = new IdentityHashMap<>(); static { @@ -94,6 +97,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { // Carvers rebuild deterministically from the carve seed, so output is unchanged. synchronized (profileCarverLock) { profileCarvers = new IdentityHashMap<>(); + densityGrid.clear(); } } @@ -524,7 +528,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { return carver; } - IrisCaveCarver3D createdCarver = new IrisCaveCarver3D(getEngineMantle().getEngine(), profile); + IrisCaveCarver3D createdCarver = new IrisCaveCarver3D(getEngineMantle().getEngine(), profile, densityGrid); synchronized (profileCarverLock) { IrisCaveCarver3D published = profileCarvers.get(profile); if (published != null) { diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java b/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java index 0b501194e..cd5d1a2af 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java @@ -115,7 +115,7 @@ public class IrisComplex implements DataProvider { private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L); private static final int GRID_BOUNDS_CACHE_SIZE = 8192; private static final int STUDIO_NOISE_CACHE_SIZE = 32_768; - private static final int TERRAIN_COLUMN_CACHE_SIZE = 65_536; + private static final int TERRAIN_COLUMN_CACHE_SIZE = 524_288; /** One million corners: about 16 MB, roughly a 4000 by 4000 block area at the 4-block grid. */ private static final int SHARED_CORNER_BOUNDS_CAPACITY = 1 << 20; /** The slope streams measure the rise across a run of this many blocks, so gradient is slope over run. */ @@ -386,10 +386,9 @@ public class IrisComplex implements DataProvider { proceduralTerrainHeight = new ProceduralTerrainHeightSampler( (x, z) -> getHeight(engine, x, z, engine.getSeedManager().getHeight()), engine.getDimension().getTerrainSamplingStep()); - baseTerrainHeightStream = GenerationStreams.cache2DDouble(ProceduralStream.of( - this::sampleUnblendedNaturalTerrainHeight, - Interpolated.DOUBLE - ), "baseTerrainHeightStream", engine, cacheSize); + baseTerrainHeightStream = GenerationStreams.cache2DDouble( + ProceduralStream.ofDouble(this::sampleUnblendedNaturalTerrainHeight), + "baseTerrainHeightStream", engine, cacheSize); boolean terrain3DEnabled = false; for (IrisBiome biome : generatorBiomes) { if (biome.getTerrain3D() != null) { diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisDepositModifier.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisDepositModifier.java index a317b9c57..afd4ee15b 100644 --- a/core/src/main/java/art/arcane/iris/generation/stage/IrisDepositModifier.java +++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisDepositModifier.java @@ -23,6 +23,7 @@ import art.arcane.iris.generation.runtime.IrisEngine; import art.arcane.iris.generation.runtime.EngineAssignedModifier; import art.arcane.iris.generation.runtime.DimensionStackLayout; 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.IrisDepositHeightDistribution; import art.arcane.iris.generation.decoration.IrisDepositPlacementScope; @@ -45,15 +46,20 @@ import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.iris.generation.block.B; import art.arcane.iris.generation.block.VectorMap; +import art.arcane.iris.pack.loading.IrisData; +import art.arcane.iris.pack.loading.IrisRegistrant; +import art.arcane.iris.pack.loading.ResourceLoader; import java.util.ArrayList; import java.util.List; +import java.util.function.Function; public class IrisDepositModifier extends EngineAssignedModifier { private static final int CLUMPS_PER_BATCH = 8; private static final int PREPARATION_BATCH_COUNT = 4; private final RNG rng; + private volatile DepositVariantScan variantScan; public IrisDepositModifier(Engine engine) { super(engine, "Deposit"); @@ -91,7 +97,7 @@ public class IrisDepositModifier extends EngineAssignedModifier 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) { 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); } } - private DepositPlan plan(IrisDepositGenerator generator, RNG rng) { + private DepositPlan plan(IrisDepositGenerator generator, RNG rng, ChunkContext context) { if (generator.getSpawnChance() < rng.d()) { return new DepositPlan(generator, rng.getSeed(), false, 0); } boolean ore = generator.isOre(getData()); 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) { @@ -185,15 +219,18 @@ public class IrisDepositModifier extends EngineAssignedModifier clumps = new ArrayList<>(limit - first); + int terrainLimit = Integer.MIN_VALUE; 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()) { 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 max = (int) (16D - dim / 2D); @@ -257,6 +294,20 @@ public class IrisDepositModifier extends EngineAssignedModifier terrainLimit) { + continue; + } + } + RNG replay = new RNG(clumpSeed); + replay.d(); + clump = k.getClump(getEngine(), replay, getData()); + } + clumps.add(new PreparedClump(clump, x, y, z, clumpRng)); } return new PreparedDeposit(k, oreDeposit, clumps); @@ -271,6 +322,7 @@ public class IrisDepositModifier extends EngineAssignedModifier Boolean declaresVariants(ResourceLoader loader, + Function> variants) { + String[] keys = loader == null ? null : loader.getPossibleKeys(); + if (keys == null) { + return null; + } + for (String key : keys) { + T loaded = loader.load(key); + if (loaded == null) { + continue; + } + List declared = variants.apply(loaded); + if (declared == null || !declared.isEmpty()) { + return true; + } + } + return false; + } + private NativeBlockState resolveDepositVariant(int cx, int cz, int nx, int localY, int nz, NativeBlockState ore, IrisDimension dimension, ChunkContext context, IrisDimensionCarvingResolver.State carvingState) { int worldX = (cx << 4) + nx; int worldZ = (cz << 4) + nz; @@ -548,6 +659,9 @@ public class IrisDepositModifier extends EngineAssignedModifier clumps) { } diff --git a/core/src/main/java/art/arcane/iris/generation/terrain/Terrain3DRuntime.java b/core/src/main/java/art/arcane/iris/generation/terrain/Terrain3DRuntime.java index ac32ee03b..5998da6ec 100644 --- a/core/src/main/java/art/arcane/iris/generation/terrain/Terrain3DRuntime.java +++ b/core/src/main/java/art/arcane/iris/generation/terrain/Terrain3DRuntime.java @@ -1,12 +1,11 @@ package art.arcane.iris.generation.terrain; +import art.arcane.iris.generation.cache.ConcurrentClockCache; import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.noise.IrisGeneratorStyle; import art.arcane.volmlib.util.noise.CNG; -import art.arcane.volmlib.util.cache.CacheKey; import art.arcane.volmlib.util.math.RNG; -import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; import java.util.Arrays; import java.util.IdentityHashMap; @@ -16,9 +15,10 @@ import java.util.concurrent.atomic.AtomicReferenceArray; public final class Terrain3DRuntime { private static final int STEP = 4; - private static final int CACHE_STRIPES = 16; + private static final int MINIMUM_COLUMNS = 16; + private static final int ANCHOR_BLOCK_SIZE = 16; private static final int MAXIMUM_ANCHORS = 32_768; - private static final long ANCHOR_CACHE_BYTES = 64L * 1024 * 1024; + private static final long ANCHOR_CACHE_BYTES = 128L * 1024 * 1024; private static final long DENSITY_SALT = 0x536E4A11C924B3D7L; private static final long CRACK_SALT = 0x7839A16DC4052EFBL; private static final NodeSample EMPTY_SAMPLE = new NodeSample(0D, 0D, 0D); @@ -28,10 +28,11 @@ public final class Terrain3DRuntime { private final Sources sources; private final Options options; private final NoiseFactory noiseFactory; - private final BoundedCache columns; - private final BoundedCache anchors; + private final BlockCache columns; + private final BlockCache anchors; private final Terrain3DFragmentFilter fragments; - private final Map profiles = new IdentityHashMap<>(); + private final Object profileLock = new Object(); + private volatile Map profiles = new IdentityHashMap<>(); private final ThreadLocal localColumn = ThreadLocal.withInitial(ColumnMemo::new); private volatile Object cacheGeneration = new Object(); @@ -46,8 +47,8 @@ public final class Terrain3DRuntime { this.sources = Objects.requireNonNull(sources, "Terrain sources"); this.options = Objects.requireNonNull(options, "Terrain options"); this.noiseFactory = Objects.requireNonNull(noiseFactory, "Terrain noise factory"); - columns = new BoundedCache<>(options.maximumColumns()); - anchors = new BoundedCache<>(anchorCacheCapacity(options.height(), options.maximumColumns())); + columns = new BlockCache<>(options.maximumColumns(), 0); + anchors = new BlockCache<>(anchorCacheCapacity(options.height(), options.maximumColumns()), 2); fragments = new Terrain3DFragmentFilter(this::rawColumn); } @@ -59,7 +60,7 @@ public final class Terrain3DRuntime { long bytesPerAnchor = 160L + 48L * (Math.ceilDiv((long) height, STEP) + 1); long maximumEntries = Math.min(MAXIMUM_ANCHORS, Math.min((long) maximumColumns * 8, ANCHOR_CACHE_BYTES / bytesPerAnchor)); - return Math.max(CACHE_STRIPES, (int) (maximumEntries / CACHE_STRIPES) * CACHE_STRIPES); + return Math.max(ANCHOR_BLOCK_SIZE, (int) (maximumEntries / ANCHOR_BLOCK_SIZE) * ANCHOR_BLOCK_SIZE); } public double height(int x, int z) { @@ -94,8 +95,8 @@ public final class Terrain3DRuntime { columns.clear(); anchors.clear(); fragments.clear(); - synchronized (profiles) { - profiles.clear(); + synchronized (profileLock) { + profiles = new IdentityHashMap<>(); } cacheGeneration = new Object(); localColumn.remove(); @@ -106,9 +107,8 @@ public final class Terrain3DRuntime { } private Terrain3DFragmentFilter.DensityColumn rawColumn(int x, int z) { - long key = pack(x, z); - Terrain3DFragmentFilter.DensityColumn cached = columns.get(key); - return cached == null ? columns.putIfAbsent(key, + Terrain3DFragmentFilter.DensityColumn cached = columns.get(x, z); + return cached == null ? columns.putIfAbsent(x, z, new Terrain3DFragmentFilter.DensityColumn(createColumn(x, z))) : cached; } @@ -196,9 +196,8 @@ public final class Terrain3DRuntime { } private Anchor anchor(int x, int z) { - long key = pack(x, z); - Anchor cached = anchors.get(key); - return cached == null ? anchors.putIfAbsent(key, createAnchor(x, z)) : cached; + Anchor cached = anchors.get(x, z); + return cached == null ? anchors.putIfAbsent(x, z, createAnchor(x, z)) : cached; } private Anchor createAnchor(int x, int z) { @@ -227,19 +226,19 @@ public final class Terrain3DRuntime { if (config == null || !config.isEnabled()) { return DISABLED; } - synchronized (profiles) { - CompiledProfile cached = profiles.get(config); - if (cached != null) { - return cached; - } + CompiledProfile cached = profiles.get(config); + if (cached != null) { + return cached; } CompiledProfile compiled = compile(config); - synchronized (profiles) { + synchronized (profileLock) { CompiledProfile existing = profiles.get(config); if (existing != null) { return existing; } - profiles.put(config, compiled); + Map updated = new IdentityHashMap<>(profiles); + updated.put(config, compiled); + profiles = updated; return compiled; } } @@ -314,7 +313,7 @@ public final class Terrain3DRuntime { boolean enabled, int maximumColumns) { public Options { if (height < 2 || height > 4096 || !Double.isFinite(fluidHeight) - || maximumColumns < CACHE_STRIPES) { + || maximumColumns < MINIMUM_COLUMNS) { throw new IllegalArgumentException("Invalid volumetric terrain runtime options"); } } @@ -408,74 +407,61 @@ public final class Terrain3DRuntime { double fluidFade, NoiseSource density, NoiseSource cracks) { } - private static final class BoundedCache { - private final CacheStripe[] stripes; + /** + * Groups the 16x16 block area of one chunk into a single cache entry, so neighbouring lookups share one + * lock-free set probe and a working set of whole chunks is evicted together. + */ + private static final class BlockCache { + private final ConcurrentClockCache> blocks; + private final int cellShift; + private final int axisMask; + private final int axisBits; - @SuppressWarnings("unchecked") - private BoundedCache(int maximumSize) { - stripes = (CacheStripe[]) new CacheStripe[CACHE_STRIPES]; - for (int index = 0; index < stripes.length; index++) { - stripes[index] = new CacheStripe<>(Math.ceilDiv(maximumSize, CACHE_STRIPES)); + private BlockCache(int maximumEntries, int cellShift) { + this.cellShift = cellShift; + axisBits = 4 - cellShift; + axisMask = (1 << axisBits) - 1; + int blockSize = 1 << (axisBits * 2); + blocks = new ConcurrentClockCache<>(Math.ceilDiv(maximumEntries, blockSize)); + } + + private T get(int x, int z) { + AtomicReferenceArray block = blocks.get(blockKey(x, z)); + return block == null ? null : block.getAcquire(slot(x, z)); + } + + private T putIfAbsent(int x, int z, T value) { + long key = blockKey(x, z); + AtomicReferenceArray block = blocks.get(key); + if (block == null) { + block = blocks.putIfAbsent(key, new AtomicReferenceArray<>(1 << (axisBits * 2))); } - } - - private T get(long key) { - return stripe(key).get(key); - } - - private T putIfAbsent(long key, T value) { - return stripe(key).putIfAbsent(key, value); + T existing = block.compareAndExchange(slot(x, z), null, value); + return existing == null ? value : existing; } private void clear() { - for (CacheStripe stripe : stripes) { - stripe.clear(); - } + blocks.clear(); } private int size() { - int size = 0; - for (CacheStripe stripe : stripes) { - size += stripe.size(); - } - return size; + int[] size = new int[1]; + blocks.forEach(block -> { + for (int index = 0; index < block.length(); index++) { + if (block.getAcquire(index) != null) { + size[0]++; + } + } + }); + return size[0]; } - private CacheStripe stripe(long key) { - return stripes[(int) CacheKey.mix(key) & (CACHE_STRIPES - 1)]; - } - } - - private static final class CacheStripe { - private final int maximumSize; - private final Long2ObjectLinkedOpenHashMap entries = new Long2ObjectLinkedOpenHashMap<>(16); - - private CacheStripe(int maximumSize) { - this.maximumSize = maximumSize; + private static long blockKey(int x, int z) { + return pack(x >> 4, z >> 4); } - private synchronized T get(long key) { - return entries.getAndMoveToLast(key); - } - - private synchronized T putIfAbsent(long key, T value) { - T existing = entries.getAndMoveToLast(key); - if (existing != null) { - return existing; - } - entries.putAndMoveToLast(key, value); - if (entries.size() > maximumSize) { - entries.removeFirst(); - } - return value; - } - - private synchronized void clear() { - entries.clear(); - } - - private synchronized int size() { - return entries.size(); + private int slot(int x, int z) { + return ((x >> cellShift) & axisMask) << axisBits | ((z >> cellShift) & axisMask); } } } diff --git a/core/src/test/java/art/arcane/iris/generation/cache/ConcurrentClockCacheTest.java b/core/src/test/java/art/arcane/iris/generation/cache/ConcurrentClockCacheTest.java new file mode 100644 index 000000000..276396f0d --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/cache/ConcurrentClockCacheTest.java @@ -0,0 +1,84 @@ +package art.arcane.iris.generation.cache; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class ConcurrentClockCacheTest { + @Test + public void firstPublishedValueWins() { + ConcurrentClockCache cache = new ConcurrentClockCache<>(16); + Object first = new Object(); + assertNull(cache.get(7L)); + assertSame(first, cache.putIfAbsent(7L, first)); + assertSame(first, cache.putIfAbsent(7L, new Object())); + assertSame(first, cache.get(7L)); + cache.clear(); + assertNull(cache.get(7L)); + } + + @Test + public void capacityBoundsRetainedEntries() { + ConcurrentClockCache cache = new ConcurrentClockCache<>(64); + for (long key = 0; key < 10_000; key++) { + assertEquals(Long.valueOf(key), cache.putIfAbsent(key, key)); + } + AtomicInteger retained = new AtomicInteger(); + cache.forEach(value -> { + retained.incrementAndGet(); + assertEquals(value, cache.get(value)); + }); + assertEquals(cache.capacity(), retained.get()); + assertTrue(cache.capacity() >= 64); + } + + @Test + public void referencedEntriesSurviveAStreamOfOneTimeKeys() { + ConcurrentClockCache cache = new ConcurrentClockCache<>(4); + cache.putIfAbsent(-1L, -1L); + for (long key = 0; key < 1_000; key++) { + assertEquals(Long.valueOf(-1L), cache.get(-1L)); + cache.putIfAbsent(key, key); + } + assertEquals(Long.valueOf(-1L), cache.get(-1L)); + } + + @Test + public void concurrentWritersAgreeOnOneValuePerKey() throws Exception { + ConcurrentClockCache cache = new ConcurrentClockCache<>(1 << 12); + List> results = new ArrayList<>(); + try (ExecutorService executor = Executors.newFixedThreadPool(8)) { + for (int thread = 0; thread < 8; thread++) { + results.add(executor.submit(() -> { + Object[] seen = new Object[256]; + for (int key = 0; key < seen.length; key++) { + seen[key] = cache.putIfAbsent(key, new Object()); + } + return seen; + })); + } + Object[] reference = results.getFirst().get(10, TimeUnit.SECONDS); + for (Future result : results) { + Object[] seen = result.get(10, TimeUnit.SECONDS); + for (int key = 0; key < seen.length; key++) { + Object cached = cache.get(key); + if (cached != null) { + assertSame(cached, seen[key]); + assertSame(reference[key], seen[key]); + } + } + } + } + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/decoration/IrisDepositTuningTest.java b/core/src/test/java/art/arcane/iris/generation/decoration/IrisDepositTuningTest.java index 4221c7b59..839e18e77 100644 --- a/core/src/test/java/art/arcane/iris/generation/decoration/IrisDepositTuningTest.java +++ b/core/src/test/java/art/arcane/iris/generation/decoration/IrisDepositTuningTest.java @@ -2,6 +2,8 @@ package art.arcane.iris.generation.decoration; import art.arcane.iris.generation.biome.IrisBiome; 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.volmlib.util.documentation.Description; @@ -224,6 +226,38 @@ public class IrisDepositTuningTest { 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.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 public void vanillaScatteredSizeIsAnUpperCandidateBound() { IrisData data = mock(IrisData.class); diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DNearParityTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DNearParityTest.java index 21c277cb0..2bb001781 100644 --- a/core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DNearParityTest.java +++ b/core/src/test/java/art/arcane/iris/generation/mantle/IrisCaveCarver3DNearParityTest.java @@ -1038,6 +1038,40 @@ public class IrisCaveCarver3DNearParityTest { largestPlane < 192); } + @Test + public void sharedDensityLatticeReproducesIsolatedCarvesAcrossNeighbourChunks() throws Exception { + Engine engine = createEngine(96, 90); + for (boolean warp : new boolean[]{false, true}) { + for (boolean modules : new boolean[]{false, true}) { + AtomicLong sharedSamples = new AtomicLong(); + AtomicLong isolatedSamples = new AtomicLong(); + IrisCaveCarver3D shared = new IrisCaveCarver3D(engine, + createProfile(warp, modules).setAdaptiveSampling(true), new CaveDensityGrid(4096)); + baseDensityField.set(shared, new CountingCNG((CNG) baseDensityField.get(shared), sharedSamples)); + for (int chunkX = -1; chunkX <= 1; chunkX++) { + for (int chunkZ = -1; chunkZ <= 1; chunkZ++) { + WriterCapture sharedCapture = createWriterCapture(96); + int sharedCarved = shared.carve(sharedCapture.writer, chunkX, chunkZ, fullWeights(), 0D, 0D, + null, filledHeights(90)); + IrisCaveCarver3D isolated = new IrisCaveCarver3D(engine, + createProfile(warp, modules).setAdaptiveSampling(true)); + baseDensityField.set(isolated, new CountingCNG((CNG) baseDensityField.get(isolated), isolatedSamples)); + WriterCapture isolatedCapture = createWriterCapture(96); + int isolatedCarved = isolated.carve(isolatedCapture.writer, chunkX, chunkZ, fullWeights(), 0D, 0D, + null, filledHeights(90)); + assertTrue(isolatedCarved > 0); + assertEquals(isolatedCarved, sharedCarved); + assertEquals(isolatedCapture.carvedCells, sharedCapture.carvedCells); + assertEquals(isolatedCapture.carvedLiquids, sharedCapture.carvedLiquids); + } + } + assertTrue("warp=" + warp + " modules=" + modules + " shared=" + sharedSamples.get() + + " isolated=" + isolatedSamples.get(), + sharedSamples.get() < isolatedSamples.get() * 3 / 4); + } + } + } + private void assertExactParity(boolean warp, boolean modules, boolean adaptiveSampling) throws Exception { Engine engine = createEngine(96, 90); double[] columnWeights = fullWeights(); diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisDepositModifierParityTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisDepositModifierParityTest.java index d9a7f6fce..45c47dede 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisDepositModifierParityTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisDepositModifierParityTest.java @@ -5,12 +5,14 @@ import art.arcane.iris.testsupport.KeyedBlockState; import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.generation.runtime.Engine; 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.IrisDepositHeightDistribution; import art.arcane.iris.generation.decoration.IrisDepositShape; import art.arcane.iris.generation.decoration.IrisDepositVariant; import art.arcane.iris.generation.terrain.IrisDimension; 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.IrisPlatforms; import art.arcane.volmlib.nativelib.terrain.NativeBlockState; @@ -25,17 +27,21 @@ import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.volmlib.util.math.RNG; import org.junit.After; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import org.mockito.MockedStatic; +import art.arcane.iris.pack.loading.ResourceLoader; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -84,8 +90,38 @@ public class IrisDepositModifierParityTest { } } - @SuppressWarnings("unchecked") + @Test + public void variantFreePacksSkipPerBlockCaveBiomeLookupsWithIdenticalOutput() { + for (long seed : new long[]{101L, -73L, 90181L}) { + AtomicInteger scannedLookups = new AtomicInteger(); + AtomicInteger unscannedLookups = new AtomicInteger(); + long[] scanned = generateConfigured(seed, false, false, true, scannedLookups); + long[] unscanned = generateConfigured(seed, false, false, false, unscannedLookups); + assertArrayEquals("seed=" + seed, unscanned, scanned); + assertTrue(scannedLookups.get() < unscannedLookups.get()); + } + } + + @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) { + return generateConfigured(seed, multicore, true, false, new AtomicInteger()); + } + + private long[] generateConfigured(long seed, boolean multicore, boolean caveVariant, boolean enumerablePack, + 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); ChunkContext context = mock(ChunkContext.class, RETURNS_DEEP_STUBS); IrisDimension dimension = new IrisDimension(); @@ -99,7 +135,9 @@ public class IrisDepositModifierParityTest { denied.setLoadKey("denied"); IrisDepositVariant variant = new IrisDepositVariant().setMinHeight(-64).setMaxHeight(64); variant.getRemap().put("minecraft:gold_ore", "minecraft:redstone_ore"); - cave.getDepositVariants().add(variant); + if (caveVariant) { + cave.getDepositVariants().add(variant); + } dimension.getDeposits().add(generator("granite", IrisDepositShape.IRIS, 25, 70, IrisDepositHeightDistribution.CLIPPED_UNIFORM, "stone", "deepslate")); region.getDeposits().add(generator("iron_ore", IrisDepositShape.VANILLA_ELLIPSOID, 20, 110, @@ -114,11 +152,37 @@ public class IrisDepositModifierParityTest { gold.setExcludedBiomes(new KList<>("allowed")); gold.setDiscardChanceOnAirExposure(0.3D); 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.getMinHeight()).thenReturn(-64); when(engine.getSeedManager().getDeposit()).thenReturn(seed); when(engine.getDimension()).thenReturn(dimension); + if (enumerablePack) { + IrisData data = mock(IrisData.class); + ResourceLoader biomes = mock(ResourceLoader.class); + ResourceLoader regions = mock(ResourceLoader.class); + when(data.getBiomeLoader()).thenReturn(biomes); + when(data.getRegionLoader()).thenReturn(regions); + when(biomes.getPossibleKeys()).thenReturn(new String[]{"allowed", "denied", "surface"}); + when(biomes.load("allowed")).thenReturn(cave); + when(biomes.load("denied")).thenReturn(denied); + when(biomes.load("surface")).thenReturn(surface); + when(regions.getPossibleKeys()).thenReturn(new String[]{"region"}); + when(regions.load("region")).thenReturn(region); + when(engine.getData()).thenReturn(data); + when(engine.getDimensionStackContext()).thenReturn(null); + } when(engine.getCaveBiome(anyInt(), anyInt(), anyInt(), any())).thenAnswer(invocation -> { + caveBiomeLookups.incrementAndGet(); int x = invocation.getArgument(0); int y = invocation.getArgument(1); int z = invocation.getArgument(2); @@ -366,7 +430,7 @@ public class IrisDepositModifierParityTest { } } - private static final class PaletteGenerator extends IrisDepositGenerator { + private static class PaletteGenerator extends IrisDepositGenerator { private final KList blocks; private PaletteGenerator(NativeBlockState block) { @@ -378,4 +442,25 @@ public class IrisDepositModifierParityTest { 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); + } + } } diff --git a/core/src/test/java/art/arcane/iris/generation/terrain/Terrain3DRuntimeTest.java b/core/src/test/java/art/arcane/iris/generation/terrain/Terrain3DRuntimeTest.java index f183c9884..37a68c632 100644 --- a/core/src/test/java/art/arcane/iris/generation/terrain/Terrain3DRuntimeTest.java +++ b/core/src/test/java/art/arcane/iris/generation/terrain/Terrain3DRuntimeTest.java @@ -302,7 +302,7 @@ public class Terrain3DRuntimeTest { assertEquals(0, capacity % 16); assertTrue(capacity >= 16); assertTrue(capacity <= 32_768); - assertTrue(capacity * fullHeightStorage <= 64L * 1024 * 1024); + assertTrue(capacity * fullHeightStorage <= 128L * 1024 * 1024); } } } @@ -354,9 +354,10 @@ public class Terrain3DRuntimeTest { for (int x = -65_536; x < 65_536; x++) { assertEquals(96, runtime.column(x, -1).topY()); } - assertEquals(65_536, runtime.cachedColumnCount()); + assertTrue(runtime.cachedColumnCount() > 0); + assertTrue(runtime.cachedColumnCount() <= 65_536); assertEquals(96, runtime.column(-65_536, -1).topY()); - assertEquals(65_536, runtime.cachedColumnCount()); + assertTrue(runtime.cachedColumnCount() <= 65_536); runtime.clear(); assertEquals(0, runtime.cachedColumnCount()); }