Enforce seed cohesion

- Scope cached generation fields to their seed and context.
- Warm decorators and ores with their canonical generation seeds.
- Stabilize object traversal and procedural placement choices.
- Isolate smart-bore variants from shared source objects.
- Pin noise and full-engine output across generation orders.
- Verify same-jar generation through cold server restarts.
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-02 14:43:48 -04:00
1 parent 6b82ea99e2
commit 4ef6fc7cc6
50 files changed
+1734 -259

No files matched your search

@@ -51,7 +51,7 @@ public final class FloatingIslandSample {
private static final double CARVE_MAX_VERTICAL_RUN_FRACTION = 0.18D;
private static final ThreadLocal<int[]> LAST_REJECT = ThreadLocal.withInitial(() -> new int[1]);
private static final ThreadLocal<double[]> LAST_DENSITY = ThreadLocal.withInitial(() -> new double[2]);
private static final ThreadLocal<HashMap<Long, FloatingIslandSample>> CHUNK_MEMO = ThreadLocal.withInitial(HashMap::new);
private static final ThreadLocal<ChunkMemo> CHUNK_MEMO = ThreadLocal.withInitial(ChunkMemo::new);
private static final ThreadLocal<IdentityHashMap<CNG, Long2DoubleOpenHashMap>> FOOTPRINT_MEMO = ThreadLocal.withInitial(IdentityHashMap::new);
private static final AtomicBoolean NULL_CNG_WARNED = new AtomicBoolean(false);
@@ -68,21 +68,32 @@ public final class FloatingIslandSample {
}
public static void clearThreadCaches() {
CHUNK_MEMO.remove();
FOOTPRINT_MEMO.remove();
LAST_REJECT.remove();
LAST_DENSITY.remove();
}
public static void clearChunkMemo() {
CHUNK_MEMO.get().clear();
CHUNK_MEMO.get().samples.clear();
FOOTPRINT_MEMO.get().clear();
}
public static FloatingIslandSample sampleMemoized(IrisBiome parent, int wx, int wz, int chunkHeight, long baseSeed, IrisData data, Engine engine, FloatingIslandBoundarySampler boundarySampler) {
long key = (((long) wx) << 32) ^ (wz & 0xFFFFFFFFL);
HashMap<Long, FloatingIslandSample> memo = CHUNK_MEMO.get();
if (memo.containsKey(key)) {
return memo.get(key);
ChunkMemo memo = CHUNK_MEMO.get();
if (memo.context == null || !memo.context.matches(wx >> 4, wz >> 4, chunkHeight, baseSeed,
data, engine, boundarySampler)) {
memo.samples.clear();
FOOTPRINT_MEMO.get().clear();
memo.context = new MemoContext(wx >> 4, wz >> 4, chunkHeight, baseSeed, data, engine, boundarySampler);
}
MemoizedSample cached = memo.samples.get(key);
if (cached != null && cached.parent == parent) {
return cached.sample;
}
FloatingIslandSample result = sample(parent, wx, wz, chunkHeight, baseSeed, data, engine, boundarySampler);
memo.put(key, result);
memo.samples.put(key, new MemoizedSample(parent, result));
return result;
}
@@ -799,4 +810,21 @@ public final class FloatingIslandSample {
}
};
}
private static final class ChunkMemo {
private final HashMap<Long, MemoizedSample> samples = new HashMap<>(256);
private MemoContext context;
}
private record MemoContext(int chunkX, int chunkZ, int height, long seed, IrisData data, Engine engine,
FloatingIslandBoundarySampler boundarySampler) {
private boolean matches(int chunkX, int chunkZ, int height, long seed, IrisData data, Engine engine,
FloatingIslandBoundarySampler boundarySampler) {
return this.chunkX == chunkX && this.chunkZ == chunkZ && this.height == height && this.seed == seed
&& this.data == data && this.engine == engine && this.boundarySampler == boundarySampler;
}
}
private record MemoizedSample(IrisBiome parent, FloatingIslandSample sample) {
}
}
@@ -105,7 +105,12 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
private final transient AtomicCache<Color> cacheColorDecoratorLoad = new AtomicCache<>();
private final transient AtomicCache<Color> cacheColorLayerLoad = new AtomicCache<>();
private final transient AtomicCache<Color> cacheColorDepositLoad = new AtomicCache<>();
private final transient AtomicCache<CNG> childrenCell = new AtomicCache<>();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient LazyBoundedCache<ChildGeneratorKey, CNG> childrenGenerators = new LazyBoundedCache<>(8);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient volatile CachedChildGenerator recentChildGenerator;
@Getter(AccessLevel.NONE)
private final transient LazyBoundedCache<Long, CNG> biomeGenerators =
new LazyBoundedCache<>(BIOME_GENERATOR_CACHE_SIZE);
@@ -512,11 +517,18 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
}
public CNG getChildrenGenerator(RNG random, int sig, double scale) {
return childrenCell.aquire(() -> createChildrenGenerator(random, sig, scale));
}
private CNG createChildrenGenerator(RNG random, int sig, double scale) {
return getChildStyle().createScaledGenerator(deriveChildStyleSeed(random, sig), getLoader(), scale);
long seed = deriveChildStyleSeed(random, sig).getSeed();
IrisData data = getLoader();
Engine engine = data == null ? null : data.getEngine();
CachedChildGenerator recent = recentChildGenerator;
if (recent != null && recent.key.matches(seed, scale, data, engine)) {
return recent.generator;
}
ChildGeneratorKey key = new ChildGeneratorKey(seed, scale, data, engine);
CNG generator = childrenGenerators.computeIfAbsent(key,
ignored -> getChildStyle().createScaledGenerator(new RNG(seed), data, scale));
recentChildGenerator = new CachedChildGenerator(key, generator);
return generator;
}
static RNG deriveChildStyleSeed(RNG random, int signature) {
@@ -698,4 +710,27 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
this.generator = generator;
}
}
private record ChildGeneratorKey(long seed, double scale, IrisData data, Engine engine) {
private boolean matches(long seed, double scale, IrisData data, Engine engine) {
return this.seed == seed && Double.doubleToLongBits(this.scale) == Double.doubleToLongBits(scale)
&& this.data == data && this.engine == engine;
}
@Override
public boolean equals(Object object) {
return object instanceof ChildGeneratorKey other && matches(other.seed, other.scale, other.data, other.engine);
}
@Override
public int hashCode() {
int hash = Long.hashCode(seed);
hash = 31 * hash + Double.hashCode(scale);
hash = 31 * hash + System.identityHashCode(data);
return 31 * hash + System.identityHashCode(engine);
}
}
private record CachedChildGenerator(ChildGeneratorKey key, CNG generator) {
}
}
@@ -33,6 +33,10 @@ import art.arcane.volmlib.util.math.Rarity;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.volmlib.util.cache.AtomicCache;
import art.arcane.iris.generation.cache.LazyBoundedCache;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.pack.schema.annotation.ArrayType;
import art.arcane.volmlib.util.documentation.Description;
@@ -43,7 +47,6 @@ import art.arcane.iris.pack.schema.annotation.RegistryListResource;
import art.arcane.iris.pack.schema.annotation.Snippet;
import art.arcane.volmlib.util.noise.CNG;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -59,48 +62,59 @@ import lombok.experimental.Accessors;
@Data
public class IrisFloatingChildBiomes implements Rarity {
private final transient AtomicCache<IrisBiome> resolvedBiome = new AtomicCache<>();
private final transient AtomicCache<CNG> footprintCache = new AtomicCache<>();
private final transient AtomicCache<FloatingIslandEdgeProfile> edgeTaperProfileCache = new AtomicCache<>();
private final transient AtomicCache<CNG> pickerCache = new AtomicCache<>();
private final transient AtomicCache<CNG> altitudeCache = new AtomicCache<>();
private final transient AtomicCache<CNG> topShapeCache = new AtomicCache<>();
private final transient AtomicCache<CNG> bottomCache = new AtomicCache<>();
private final transient AtomicCache<CNG> wallWarpCache = new AtomicCache<>();
private final transient AtomicCache<CNG> carveCache = new AtomicCache<>();
private final transient AtomicCache<IrisCaveProfileSampler> carvingProfileSamplerCache = new AtomicCache<>(true);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient LazyBoundedCache<RuntimeKey, FloatingIslandEdgeProfile> edgeTaperProfiles = new LazyBoundedCache<>(8);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient volatile CachedEdgeProfile recentEdgeProfile;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient LazyBoundedCache<RuntimeKey, IrisCaveProfileSampler> carvingProfileSamplers = new LazyBoundedCache<>(8);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient volatile CachedCarvingProfile recentCarvingProfile;
private final transient AtomicCache<IrisObjectScale> shrinkScaleCache = new AtomicCache<>();
public CNG getFootprintCng(long baseSeed, IrisData data) {
return footprintCache.aquire(() -> getFootprintStyle().create(new RNG(baseSeed ^ 0xF007B17DL), data));
return getFootprintStyle().create(baseSeed ^ 0xF007B17DL, data);
}
public FloatingIslandEdgeProfile getEdgeTaperProfile(long baseSeed, IrisData data) {
return edgeTaperProfileCache.aquire(() -> {
Engine engine = data == null ? null : data.getEngine();
CachedEdgeProfile recent = recentEdgeProfile;
if (recent != null && recent.key.matches(baseSeed, data, engine)) {
return recent.profile;
}
RuntimeKey key = new RuntimeKey(baseSeed, data, engine);
FloatingIslandEdgeProfile profile = edgeTaperProfiles.computeIfAbsent(key, ignored -> {
int width = FloatingIslandEdgeProfile.clampWidth(getEdgeTaperWidth());
double amplitude = FloatingIslandEdgeProfile.clampVariationAmplitude(
getEdgeTaperVariationAmplitude(), width);
IrisGeneratorStyle style = getEdgeTaperVariationStyle();
CNG variation = amplitude > 0.0D && style != null
? style.create(new RNG(baseSeed ^ 0xED6E7A9E5L), data)
? style.create(baseSeed ^ 0xED6E7A9E5L, data)
: null;
return new FloatingIslandEdgeProfile(width, getEdgeTaperExponent(), amplitude, variation);
});
recentEdgeProfile = new CachedEdgeProfile(key, profile);
return profile;
}
public CNG getPickerCng(long baseSeed, IrisData data) {
return pickerCache.aquire(() -> getPickerStyle().create(new RNG(baseSeed ^ 0x91C4E72DL), data));
return getPickerStyle().create(baseSeed ^ 0x91C4E72DL, data);
}
public CNG getAltitudeCng(long baseSeed, IrisData data) {
return altitudeCache.aquire(() -> getAltitudeStyle().create(new RNG(baseSeed ^ 0xA17DEBBL), data));
return getAltitudeStyle().create(baseSeed ^ 0xA17DEBBL, data);
}
public CNG getTopShapeCng(long baseSeed, IrisData data) {
return topShapeCache.aquire(() -> getTopShapeStyle().create(new RNG(baseSeed ^ 0x70970601DEFL), data));
return getTopShapeStyle().create(baseSeed ^ 0x70970601DEFL, data);
}
public CNG getBottomCng(long baseSeed, IrisData data) {
return bottomCache.aquire(() -> getBottomStyle().create(new RNG(baseSeed ^ 0xB0770075CAFEL), data));
return getBottomStyle().create(baseSeed ^ 0xB0770075CAFEL, data);
}
public CNG getWallWarpCng(long baseSeed, IrisData data) {
@@ -108,7 +122,7 @@ public class IrisFloatingChildBiomes implements Rarity {
if (style == null) {
return null;
}
return wallWarpCache.aquire(() -> style.create(new RNG(baseSeed ^ 0xA117BA17E0FL), data));
return style.create(baseSeed ^ 0xA117BA17E0FL, data);
}
public CNG getCarveCng(long baseSeed, IrisData data) {
@@ -116,7 +130,7 @@ public class IrisFloatingChildBiomes implements Rarity {
if (style == null) {
return null;
}
return carveCache.aquire(() -> style.create(new RNG(baseSeed ^ 0xCA5EC1EE5EL), data));
return style.create(baseSeed ^ 0xCA5EC1EE5EL, data);
}
public IrisCaveProfileSampler getCarvingProfileSampler(Engine engine, IrisData data) {
@@ -124,7 +138,12 @@ public class IrisFloatingChildBiomes implements Rarity {
return null;
}
return carvingProfileSamplerCache.aquire(() -> {
CachedCarvingProfile recent = recentCarvingProfile;
if (recent != null && recent.key.matches(0L, data, engine)) {
return recent.sampler;
}
RuntimeKey key = new RuntimeKey(0L, data, engine);
IrisCaveProfileSampler sampler = carvingProfileSamplers.computeIfAbsent(key, ignored -> {
IrisBiome resolved = resolveCarvingBiome(carving, engine, data);
if (resolved == null || resolved.getCaveProfile() == null || !resolved.getCaveProfile().isEnabled()) {
return null;
@@ -132,6 +151,8 @@ public class IrisFloatingChildBiomes implements Rarity {
return new IrisCaveProfileSampler(engine, resolved.getCaveProfile());
});
recentCarvingProfile = new CachedCarvingProfile(key, sampler);
return sampler;
}
public boolean hasCarvingReference() {
@@ -379,4 +400,28 @@ public class IrisFloatingChildBiomes implements Rarity {
return loaded;
});
}
private record RuntimeKey(long seed, IrisData data, Engine engine) {
private boolean matches(long seed, IrisData data, Engine engine) {
return this.seed == seed && this.data == data && this.engine == engine;
}
@Override
public boolean equals(Object object) {
return object instanceof RuntimeKey other && matches(other.seed, other.data, other.engine);
}
@Override
public int hashCode() {
int hash = Long.hashCode(seed);
hash = 31 * hash + System.identityHashCode(data);
return 31 * hash + System.identityHashCode(engine);
}
}
private record CachedEdgeProfile(RuntimeKey key, FloatingIslandEdgeProfile profile) {
}
private record CachedCarvingProfile(RuntimeKey key, IrisCaveProfileSampler sampler) {
}
}
@@ -6,18 +6,28 @@ import art.arcane.iris.generation.geometry.IrisBlockVector;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiConsumer;
import java.util.function.Function;
public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
private final Map<Key, Map<Key, T>> map = new KMap<>();
private transient volatile long modificationRevision;
private final AtomicLong modificationRevision = new AtomicLong();
private transient volatile OrderedSnapshot<T> orderedSnapshot;
public long modificationRevision() {
return modificationRevision;
return modificationRevision.get();
}
private void changed() {
modificationRevision.incrementAndGet();
orderedSnapshot = null;
}
public int size() {
@@ -30,7 +40,7 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
public boolean containsKey(@NonNull IrisBlockVector vector) {
if (map.isEmpty()) return false;
var chunk = map.get(chunk(vector));
Map<Key, T> chunk = map.get(chunk(vector));
return chunk != null && chunk.containsKey(relative(vector));
}
@@ -40,14 +50,14 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
public @Nullable T get(@NonNull IrisBlockVector vector) {
if (map.isEmpty()) return null;
var chunk = map.get(chunk(vector));
Map<Key, T> chunk = map.get(chunk(vector));
return chunk == null ? null : chunk.get(relative(vector));
}
public @Nullable T put(@NonNull IrisBlockVector vector, @NonNull T value) {
T previous = map.computeIfAbsent(chunk(vector), k -> new KMap<>())
.put(relative(vector), value);
modificationRevision++;
changed();
return previous;
}
@@ -59,7 +69,7 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
return mappingFunction.apply(vector);
});
if (inserted[0]) {
modificationRevision++;
changed();
}
return value;
}
@@ -77,7 +87,7 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
});
if (removed[0] != null) {
modificationRevision++;
changed();
}
return (T) removed[0];
@@ -89,22 +99,47 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
public void clear() {
if (!map.isEmpty()) {
modificationRevision++;
map.clear();
changed();
}
map.clear();
}
public void forEach(@NonNull BiConsumer<@NonNull IrisBlockVector, @NonNull T> consumer) {
map.forEach((chunk, values) -> {
int rX = chunk.x << 10;
int rY = chunk.y << 10;
int rZ = chunk.z << 10;
Cursor cursor = cursor();
while (cursor.next()) {
consumer.accept(cursor.key().clone(), cursor.value());
}
}
values.forEach((relative, value) -> consumer.accept(
relative.resolve(rX, rY, rZ),
value
));
});
private List<OrderedEntry<T>> orderedEntries() {
long revision = modificationRevision.get();
OrderedSnapshot<T> cached = orderedSnapshot;
if (cached != null && cached.revision() == revision) {
return cached.entries();
}
synchronized (this) {
revision = modificationRevision.get();
cached = orderedSnapshot;
if (cached != null && cached.revision() == revision) {
return cached.entries();
}
List<OrderedEntry<T>> entries = new ArrayList<>();
map.forEach((chunk, values) -> values.forEach((relative, value) -> entries.add(
new OrderedEntry<>((chunk.x << 10) + relative.x, (chunk.y << 10) + relative.y,
(chunk.z << 10) + relative.z, value))));
entries.sort(Comparator.comparingInt((OrderedEntry<T> entry) -> entry.x())
.thenComparingInt(OrderedEntry::y).thenComparingInt(OrderedEntry::z));
if (modificationRevision.get() == revision) {
orderedSnapshot = new OrderedSnapshot<>(revision, entries);
}
return entries;
}
}
private record OrderedEntry<T>(int x, int y, int z, T value) {
}
private record OrderedSnapshot<T>(long revision, List<OrderedEntry<T>> entries) {
}
private static Key chunk(IrisBlockVector vector) {
@@ -139,32 +174,21 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
}
public final class Cursor {
private final Iterator<Map.Entry<Key, Map<Key, T>>> chunkIterator = map.entrySet().iterator();
private final List<OrderedEntry<T>> entries = orderedEntries();
private final IrisBlockVector position = new IrisBlockVector(0, 0, 0);
private Iterator<Map.Entry<Key, T>> relativeIterator;
private int rX, rY, rZ;
private int index;
private T value;
public boolean next() {
while (relativeIterator == null || !relativeIterator.hasNext()) {
if (!chunkIterator.hasNext()) {
value = null;
return false;
}
Map.Entry<Key, Map<Key, T>> chunk = chunkIterator.next();
rX = chunk.getKey().x << 10;
rY = chunk.getKey().y << 10;
rZ = chunk.getKey().z << 10;
relativeIterator = chunk.getValue().entrySet().iterator();
if (index >= entries.size()) {
value = null;
return false;
}
Map.Entry<Key, T> entry = relativeIterator.next();
Key relative = entry.getKey();
position.setX(rX + relative.x);
position.setY(rY + relative.y);
position.setZ(rZ + relative.z);
value = entry.getValue();
OrderedEntry<T> entry = entries.get(index++);
position.setX(entry.x());
position.setY(entry.y());
position.setZ(entry.z());
value = entry.value();
return true;
}
@@ -181,79 +205,62 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
}
public class EntryIterator implements Iterator<Map.Entry<IrisBlockVector, T>> {
private final Iterator<Map.Entry<Key, Map<Key, T>>> chunkIterator = map.entrySet().iterator();
private Iterator<Map.Entry<Key, T>> relativeIterator;
private int rX, rY, rZ;
private final List<OrderedEntry<T>> entries = orderedEntries();
private int index;
private OrderedEntry<T> last;
@Override
public boolean hasNext() {
return advance();
return index < entries.size();
}
@Override
public Map.Entry<IrisBlockVector, T> next() {
if (!advance()) throw new NoSuchElementException();
var entry = relativeIterator.next();
return Map.entry(entry.getKey().resolve(rX, rY, rZ), entry.getValue());
}
private boolean advance() {
while (relativeIterator == null || !relativeIterator.hasNext()) {
if (!chunkIterator.hasNext()) return false;
var chunk = chunkIterator.next();
rX = chunk.getKey().x << 10;
rY = chunk.getKey().y << 10;
rZ = chunk.getKey().z << 10;
relativeIterator = chunk.getValue().entrySet().iterator();
if (!hasNext()) {
throw new NoSuchElementException();
}
return true;
OrderedEntry<T> entry = entries.get(index++);
last = entry;
return Map.entry(new IrisBlockVector(entry.x(), entry.y(), entry.z()), entry.value());
}
@Override
public void remove() {
if (relativeIterator == null) throw new IllegalStateException("No element to remove");
relativeIterator.remove();
modificationRevision++;
if (last == null) {
throw new IllegalStateException("No element to remove");
}
VectorMap.this.remove(new IrisBlockVector(last.x(), last.y(), last.z()));
last = null;
}
}
public class KeyIterator implements Iterator<IrisBlockVector>, Iterable<IrisBlockVector> {
private final Iterator<Map.Entry<Key, Map<Key, T>>> chunkIterator = map.entrySet().iterator();
private Iterator<Key> relativeIterator;
private int rX, rY, rZ;
private final List<OrderedEntry<T>> entries = orderedEntries();
private int index;
private OrderedEntry<T> last;
@Override
public boolean hasNext() {
return advance();
return index < entries.size();
}
@Override
public IrisBlockVector next() {
if (!advance()) throw new NoSuchElementException();
return relativeIterator.next().resolve(rX, rY, rZ);
}
private boolean advance() {
while (relativeIterator == null || !relativeIterator.hasNext()) {
if (!chunkIterator.hasNext()) return false;
var chunk = chunkIterator.next();
rX = chunk.getKey().x << 10;
rY = chunk.getKey().y << 10;
rZ = chunk.getKey().z << 10;
relativeIterator = chunk.getValue().keySet().iterator();
if (!hasNext()) {
throw new NoSuchElementException();
}
return true;
OrderedEntry<T> entry = entries.get(index++);
last = entry;
return new IrisBlockVector(entry.x(), entry.y(), entry.z());
}
@Override
public void remove() {
if (relativeIterator == null) throw new IllegalStateException("No element to remove");
relativeIterator.remove();
modificationRevision++;
if (last == null) {
throw new IllegalStateException("No element to remove");
}
VectorMap.this.remove(new IrisBlockVector(last.x(), last.y(), last.z()));
last = null;
}
@Override
@@ -263,35 +270,31 @@ public class VectorMap<T> implements Iterable<Map.Entry<IrisBlockVector, T>> {
}
public class ValueIterator implements Iterator<T>, Iterable<T> {
private final Iterator<Map<Key, T>> chunkIterator = map.values().iterator();
private Iterator<T> relativeIterator;
private final List<OrderedEntry<T>> entries = orderedEntries();
private int index;
private OrderedEntry<T> last;
@Override
public boolean hasNext() {
return advance();
return index < entries.size();
}
@Override
public T next() {
if (!advance()) throw new NoSuchElementException();
return relativeIterator.next();
}
private boolean advance() {
while (relativeIterator == null || !relativeIterator.hasNext()) {
if (!chunkIterator.hasNext()) return false;
relativeIterator = chunkIterator.next().values().iterator();
if (!hasNext()) {
throw new NoSuchElementException();
}
return true;
last = entries.get(index++);
return last.value();
}
@Override
public void remove() {
if (relativeIterator == null) throw new IllegalStateException("No element to remove");
relativeIterator.remove();
modificationRevision++;
if (last == null) {
throw new IllegalStateException("No element to remove");
}
VectorMap.this.remove(new IrisBlockVector(last.x(), last.y(), last.z()));
last = null;
}
@Override
@@ -35,7 +35,7 @@ public class FloatingDecorator {
int xf, int zf, int realX, int realZ,
int height, int max, Hunk<NativeBlockState> data, RNG rng,
Runnable candidatesNullCallback) {
RNG gRNG = new RNG(DecoratorCore.partSeed(engine.getSeedManager().getDecorator(), part));
RNG gRNG = new RNG(DecoratorCore.partSeed(engine.getSeedManager().getComponent(), part));
IrisDecorator decorator = DecoratorCore.pickDecorator(target, part, gRNG, rng, engine.getData(), realX, realZ);
if (decorator == null) {
@@ -45,11 +45,11 @@ public class FloatingDecorator {
if (!decorator.isStacking()) {
DecoratorCore.placeFloatingSimple(decorator, xf, zf, realX, realZ, height, max, data,
rng, engine.getData(), engine.getMantle());
gRNG, engine.getData(), engine.getMantle());
return max > 1 ? 1 : 0;
}
return DecoratorCore.placeFloatingStacked(decorator, xf, zf, realX, realZ, height, max, data,
rng, engine.getData(), engine.getMantle());
gRNG, engine.getData(), engine.getMantle());
}
}
@@ -60,13 +60,13 @@ public class IrisCeilingDecorator extends IrisEngineDecorator {
return;
}
}
DecoratorCore.placeSingleAt(decorator, x, z, realX, height, realZ, data, rng, getData(), true, getEngine().getMantle());
DecoratorCore.placeSingleAt(decorator, x, z, realX, height, realZ, data, partRNG, getData(), true, getEngine().getMantle());
return;
}
DecoratorCore.PlaceOpts opts = DecoratorCore.SCRATCH_OPTS.get();
opts.reset();
opts.caveSkipFluid = caveSkipFluid;
DecoratorCore.placeStackDown(decorator, x, z, realX, realZ, height, 0, data, rng, getData(), max, opts, getEngine().getMantle());
DecoratorCore.placeStackDown(decorator, x, z, realX, realZ, height, 0, data, partRNG, getData(), max, opts, getEngine().getMantle());
}
}
@@ -19,6 +19,8 @@
package art.arcane.iris.generation.decoration;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.cache.LazyBoundedCache;
import art.arcane.iris.generation.block.IrisBlockData;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.NoiseStyle;
@@ -50,9 +52,8 @@ import lombok.experimental.Accessors;
@Description("A biome decorator is used for placing flowers, grass, cacti and so on")
@Data
public class IrisDecorator {
private final transient AtomicCache<CNG> layerGenerator = new AtomicCache<>();
private final transient AtomicCache<CNG> varianceGenerator = new AtomicCache<>();
private final transient AtomicCache<CNG> heightGenerator = new AtomicCache<>();
private final transient LazyBoundedCache<GeneratorKey, Generators> generators = new LazyBoundedCache<>(32);
private transient volatile Generators recentGenerators;
private final transient AtomicCache<KList<NativeBlockState>> blockData = new AtomicCache<>();
private final transient AtomicCache<KList<NativeBlockState>> blockDataTops = new AtomicCache<>();
private final transient AtomicCache<NativeBlockState[]> blockDataArray = new AtomicCache<>();
@@ -129,33 +130,34 @@ public class IrisDecorator {
}
public CNG getHeightGenerator(RNG rng, IrisData data) {
CNG cached = heightGenerator.getIfPresent();
if (cached != null) {
return cached;
}
return heightGenerator.aquire(() ->
AtomicCache<CNG> cache = generators(rng, data).height;
CNG cached = cache.getIfPresent();
return cached != null ? cached : cache.aquire(() ->
heightVariance.create(rng.nextParallelRNG(getBlockData(data).size() + stackMax + stackMin), data));
}
public CNG getGenerator(RNG rng, IrisData data) {
CNG cached = layerGenerator.getIfPresent();
if (cached != null) {
return cached;
}
return layerGenerator.aquire(() -> style.create(rng.nextParallelRNG(getBlockData(data).size()), data));
AtomicCache<CNG> cache = generators(rng, data).layer;
CNG cached = cache.getIfPresent();
return cached != null ? cached : cache.aquire(() -> style.create(rng.nextParallelRNG(getBlockData(data).size()), data));
}
public CNG getVarianceGenerator(RNG rng, IrisData data) {
CNG cached = varianceGenerator.getIfPresent();
return cached != null ? cached : buildVarianceGeneratorOnce(rng, data);
AtomicCache<CNG> cache = generators(rng, data).variance;
CNG cached = cache.getIfPresent();
return cached != null ? cached : cache.aquire(() -> createVarianceGenerator(rng, data));
}
private CNG buildVarianceGeneratorOnce(RNG rng, IrisData data) {
return varianceGenerator.aquire(() -> createVarianceGenerator(rng, data));
private Generators generators(RNG rng, IrisData data) {
long seed = rng.getSeed();
Engine engine = data == null ? null : data.getEngine();
Generators recent = recentGenerators;
if (recent != null && recent.key.seed == seed && recent.key.data == data && recent.key.engine == engine) {
return recent;
}
Generators selected = generators.computeIfAbsent(new GeneratorKey(seed, data, engine), Generators::new);
recentGenerators = selected;
return selected;
}
private CNG createVarianceGenerator(RNG rng, IrisData data) {
@@ -358,4 +360,29 @@ public class IrisDecorator {
public boolean isStacking() {
return getStackMax() > 1;
}
private static final class Generators {
private final GeneratorKey key;
private final AtomicCache<CNG> layer = new AtomicCache<>();
private final AtomicCache<CNG> variance = new AtomicCache<>();
private final AtomicCache<CNG> height = new AtomicCache<>();
private Generators(GeneratorKey key) {
this.key = key;
}
}
private record GeneratorKey(long seed, IrisData data, Engine engine) {
@Override
public boolean equals(Object other) {
return other instanceof GeneratorKey key
&& seed == key.seed && data == key.data && engine == key.engine;
}
@Override
public int hashCode() {
return 31 * (31 * Long.hashCode(seed) + System.identityHashCode(data))
+ System.identityHashCode(engine);
}
}
}
@@ -39,6 +39,8 @@ import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.math.BlockPosition;
import art.arcane.volmlib.util.math.RNG;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import lombok.AllArgsConstructor;
@@ -269,7 +271,7 @@ public class IrisDepositGenerator {
return new IrisObject(1, 1, 1);
}
KSet<BlockPosition> cells = new KSet<>();
Set<BlockPosition> cells = new LinkedHashSet<>();
vanillaEllipsoidCells(rng, size, (x, y, z) -> cells.add(new BlockPosition(x, y, z)));
return objectFromCells(cells, rng, rdata);
}
@@ -407,7 +409,7 @@ public class IrisDepositGenerator {
}
IrisObject generateVanillaScattered(RNG rng, IrisData rdata, int size) {
KSet<BlockPosition> cells = new KSet<>();
Set<BlockPosition> cells = new LinkedHashSet<>();
int candidates = rng.nextInt(Math.max(0, size) + 1);
for (int i = 0; i < candidates; i++) {
int magnitude = Math.min(i, 7);
@@ -419,7 +421,7 @@ public class IrisDepositGenerator {
return objectFromCells(cells, rng, rdata);
}
private IrisObject objectFromCells(KSet<BlockPosition> cells, RNG rng, IrisData rdata) {
private IrisObject objectFromCells(Set<BlockPosition> cells, RNG rng, IrisData rdata) {
if (cells.isEmpty()) {
return new IrisObject(1, 1, 1);
}
@@ -34,6 +34,10 @@ public abstract class IrisEngineDecorator extends EngineAssignedComponent implem
this.part = part;
}
public static long seedForPart(long baseSeed, IrisDecorationPart part) {
return DecoratorCore.partSeed(baseSeed, part);
}
@BlockCoordinates
protected RNG getRNG(int x, int z) {
long seed = DecoratorCore.partSeed(getSeed(), part);
@@ -24,7 +24,8 @@ import art.arcane.iris.generation.terrain.IrisMaterialPalette;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.volmlib.util.cache.AtomicCache;
import art.arcane.iris.generation.cache.LazyBoundedCache;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.volmlib.util.documentation.Description;
import art.arcane.iris.pack.schema.annotation.MaxNumber;
import art.arcane.iris.pack.schema.annotation.MinNumber;
@@ -55,10 +56,11 @@ public class IrisOreGenerator {
@Description("Vertical band (min, max) this ore can generate in, in engine-local Y where 0 is the bottom of the dimension, not world Y.")
private IrisRange range = new IrisRange(30, 80);
private transient AtomicCache<OreChance> chanceCache = new AtomicCache<>();
private final transient LazyBoundedCache<ChanceKey, OreChance> chanceCache = new LazyBoundedCache<>(32);
private transient volatile CachedChance recentChance;
public void warm(RNG rng, IrisData data) {
chanceCache.aquire(() -> OreChance.of(chanceStyle.create(rng, data)));
chance(rng, data);
palette.getLayerGenerator(rng, data);
}
@@ -71,18 +73,43 @@ public class IrisOreGenerator {
return null;
}
OreChance chance = chanceCache.getIfPresent();
if (chance == null) {
chance = chanceCache.aquire(() -> OreChance.of(chanceStyle.create(rng, data)));
}
if (chance.sample(x, y, z) > threshold) {
if (chance(rng, data).sample(x, y, z) > threshold) {
return null;
}
return palette.get(rng, x, y, z, data);
}
private OreChance chance(RNG rng, IrisData data) {
long seed = rng.getSeed();
Engine engine = data == null ? null : data.getEngine();
CachedChance recent = recentChance;
if (recent != null && recent.key.seed == seed && recent.key.data == data && recent.key.engine == engine) {
return recent.chance;
}
ChanceKey key = new ChanceKey(seed, data, engine);
OreChance selected = chanceCache.computeIfAbsent(key, ignored -> OreChance.of(chanceStyle.create(rng, data)));
recentChance = new CachedChance(key, selected);
return selected;
}
private record ChanceKey(long seed, IrisData data, Engine engine) {
@Override
public boolean equals(Object other) {
return other instanceof ChanceKey key
&& seed == key.seed && data == key.data && engine == key.engine;
}
@Override
public int hashCode() {
return 31 * (31 * Long.hashCode(seed) + System.identityHashCode(data))
+ System.identityHashCode(engine);
}
}
private record CachedChance(ChanceKey key, OreChance chance) {
}
/**
* The chance field, with its single value resolved once when it is constant (a FLAT style),
* which bands such as a full deepslate layer sample at every block.
@@ -53,7 +53,7 @@ public class IrisSeaFloorDecorator extends IrisEngineDecorator {
&& !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) {
return;
}
NativeBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData());
NativeBlockState block = decorator.getBlockData100(biome, partRNG, realX, height, realZ, getData());
if (block != null) {
data.set(x, height, z, block);
}
@@ -61,13 +61,13 @@ public class IrisSeaFloorDecorator extends IrisEngineDecorator {
}
int stack = Math.min(limit - height,
DecoratorCore.computeStack(decorator, rng, realX, realZ, getData(), max - height));
DecoratorCore.computeStack(decorator, partRNG, realX, realZ, getData(), max - height));
for (int i = 0; i < stack; i++) {
int h = height + i;
double threshold = stack == 1 ? 1.0 : ((double) i) / (stack - 1);
NativeBlockState block = threshold >= decorator.getTopThreshold()
? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData())
: decorator.getBlockData100(biome, rng, realX, h, realZ, getData());
? decorator.getBlockDataForTop(biome, partRNG, realX, h, realZ, getData())
: decorator.getBlockData100(biome, partRNG, realX, h, realZ, getData());
if (block == null) {
break;
}
@@ -51,7 +51,7 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator {
if (!decorator.isStacking()) {
int targetY = height + 1;
if (DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
NativeBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData());
NativeBlockState block = decorator.getBlockData100(biome, partRNG, realX, height, realZ, getData());
if (block != null) {
data.set(x, targetY, z, block);
}
@@ -60,7 +60,7 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator {
}
int stack = Math.min(maxBaseY - height,
DecoratorCore.computeStack(decorator, rng, realX, realZ, getData(), max - height));
DecoratorCore.computeStack(decorator, partRNG, realX, realZ, getData(), max - height));
for (int i = 0; i < stack; i++) {
int h = height + i;
int targetY = h + 1;
@@ -69,8 +69,8 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator {
}
double threshold = stack == 1 ? 1.0 : ((double) i) / (stack - 1);
NativeBlockState block = threshold >= decorator.getTopThreshold()
? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData())
: decorator.getBlockData100(biome, rng, realX, h, realZ, getData());
? decorator.getBlockDataForTop(biome, partRNG, realX, h, realZ, getData())
: decorator.getBlockData100(biome, partRNG, realX, h, realZ, getData());
if (block == null) {
break;
}
@@ -102,7 +102,7 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
|| !DecoratorCore.canReplaceStackTarget(data.get(x, targetY, z), false)) {
return;
}
NativeBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData());
NativeBlockState block = decorator.getBlockData100(biome, partRNG, realX, height, realZ, getData());
if (block != null && DecoratorCore.isValidShorelineSupport(decorator, block, support)
&& IrisSugarCane.canPlace(block, data, x, targetY, z, realX, realZ, getEngine())) {
data.set(x, targetY, z, block);
@@ -111,7 +111,7 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
return;
}
int stack = decorator.getHeight(rng, realX, realZ, getData());
int stack = decorator.getHeight(partRNG, realX, realZ, getData());
if (decorator.isScaleStack()) {
stack = (int) Math.ceil((double) (max - height) * ((double) stack / 100));
} else {
@@ -124,7 +124,7 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
return;
}
NativeBlockState block = decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData());
NativeBlockState block = decorator.getBlockDataForTop(biome, partRNG, realX, height, realZ, getData());
if (block != null && DecoratorCore.isValidShorelineSupport(decorator, block, support)
&& IrisSugarCane.canPlace(block, data, x, targetY, z, realX, realZ, getEngine())) {
data.set(x, targetY, z, block);
@@ -142,8 +142,8 @@ public class IrisShoreLineDecorator extends IrisEngineDecorator {
}
double threshold = ((double) i) / (stack - 1);
NativeBlockState block = threshold >= decorator.getTopThreshold()
? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData())
: decorator.getBlockData100(biome, rng, realX, h, realZ, getData());
? decorator.getBlockDataForTop(biome, partRNG, realX, h, realZ, getData())
: decorator.getBlockData100(biome, partRNG, realX, h, realZ, getData());
if (block == null) {
break;
}
@@ -86,13 +86,13 @@ public class IrisSurfaceDecorator extends IrisEngineDecorator {
opts.fluidHeight = fluidHeight;
opts.caveSkipFluid = caveSkipFluid;
opts.mantle = getEngine().getMantle();
DecoratorCore.placeStackUp(decorator, x, z, realX, realZ, height, max, data, rng, getData(), opts);
DecoratorCore.placeStackUp(decorator, x, z, realX, realZ, height, max, data, partRNG, getData(), opts);
aquaticSnapshot.restoreIfUnsupported(data, x, z);
return;
}
DecoratorCore.placeSurfaceSingle(decorator, x, z, realX, height, realZ,
data, rng, getData(), underwater, caveSkipFluid, getEngine().getMantle());
data, partRNG, getData(), underwater, caveSkipFluid, getEngine().getMantle());
aquaticSnapshot.restoreIfUnsupported(data, x, z);
}
@@ -25,6 +25,7 @@ import art.arcane.volmlib.util.math.Vector3i;
import art.arcane.volmlib.util.math.RNG;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -42,6 +43,8 @@ final class RuinDecoratorApplier {
case BASE_SCATTER -> baseScatterCandidates(canvas, Math.max(1, decorator.getScatterRadius()));
};
candidates.sort(Comparator.comparingInt(Vector3i::getX)
.thenComparingInt(Vector3i::getY).thenComparingInt(Vector3i::getZ));
RNG paletteRng = new RNG(seed);
for (Vector3i v : candidates) {
if (!rng.chance(decorator.getChance())) {
@@ -24,7 +24,15 @@ import java.util.Map;
import java.util.Set;
public class TreeBlockCanvas {
public record Vec(int x, int y, int z) {
public record Vec(int x, int y, int z) implements Comparable<Vec> {
@Override
public int compareTo(Vec other) {
int comparison = Integer.compare(x, other.x);
if (comparison == 0) {
comparison = Integer.compare(y, other.y);
}
return comparison == 0 ? Integer.compare(z, other.z) : comparison;
}
}
public enum Role {
@@ -23,6 +23,9 @@ import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.RNG;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
@@ -90,7 +93,7 @@ public final class TreeDecoratorApplier {
private static void trunkSurface(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) {
int[][] sides = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
String[] facings = {"east", "west", "south", "north"};
for (TreeBlockCanvas.Vec v : new KList<>(canvas.getTrunk())) {
for (TreeBlockCanvas.Vec v : sortedPositions(canvas.getTrunk())) {
for (int i = 0; i < sides.length; i++) {
int nx = v.x() + sides[i][0];
int nz = v.z() + sides[i][1];
@@ -105,7 +108,7 @@ public final class TreeDecoratorApplier {
private static void leafSurface(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) {
int[][] sides = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
String[] facings = {"east", "west", "south", "north"};
for (TreeBlockCanvas.Vec v : new KList<>(canvas.getLeaf())) {
for (TreeBlockCanvas.Vec v : sortedPositions(canvas.getLeaf())) {
for (int i = 0; i < sides.length; i++) {
int nx = v.x() + sides[i][0];
int nz = v.z() + sides[i][1];
@@ -118,7 +121,7 @@ public final class TreeDecoratorApplier {
}
private static void branchSurface(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) {
for (TreeBlockCanvas.Vec v : new KList<>(canvas.getTrunk())) {
for (TreeBlockCanvas.Vec v : sortedPositions(canvas.getTrunk())) {
int ay = v.y() + 1;
if (canvas.has(v.x(), ay, v.z()) || rng.nextDouble() > dec.getChance()) {
continue;
@@ -136,7 +139,7 @@ public final class TreeDecoratorApplier {
colTop.put(key, v.y());
}
}
for (Map.Entry<Long, Integer> e : colTop.entrySet()) {
for (Map.Entry<Long, Integer> e : sortedColumns(colTop)) {
int x = unpackX(e.getKey());
int z = unpackZ(e.getKey());
int aboveY = e.getValue() + 1;
@@ -149,7 +152,7 @@ public final class TreeDecoratorApplier {
private static void canopyBottom(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) {
KMap<Long, Integer> colBottom = columnLeafBottoms(canvas);
for (Map.Entry<Long, Integer> e : colBottom.entrySet()) {
for (Map.Entry<Long, Integer> e : sortedColumns(colBottom)) {
int x = unpackX(e.getKey());
int z = unpackZ(e.getKey());
int belowY = e.getValue() - 1;
@@ -163,7 +166,7 @@ public final class TreeDecoratorApplier {
private static void canopyHang(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) {
int maxLen = Math.max(1, dec.getLength());
KMap<Long, Integer> colBottom = columnLeafBottoms(canvas);
for (Map.Entry<Long, Integer> e : colBottom.entrySet()) {
for (Map.Entry<Long, Integer> e : sortedColumns(colBottom)) {
if (rng.nextDouble() > dec.getChance()) {
continue;
}
@@ -182,7 +185,7 @@ public final class TreeDecoratorApplier {
private static void trunkBase(TreeBlockCanvas canvas, IrisTreeDecorator dec, int idx, RNG rng) {
int[][] sides = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
for (TreeBlockCanvas.Vec v : new KList<>(canvas.getTrunk())) {
for (TreeBlockCanvas.Vec v : sortedPositions(canvas.getTrunk())) {
if (v.y() != 0) {
continue;
}
@@ -205,7 +208,7 @@ public final class TreeDecoratorApplier {
if (maxY == Integer.MIN_VALUE) {
return;
}
for (TreeBlockCanvas.Vec v : new KList<>(canvas.getTrunk())) {
for (TreeBlockCanvas.Vec v : sortedPositions(canvas.getTrunk())) {
if (v.y() != maxY) {
continue;
}
@@ -269,6 +272,18 @@ public final class TreeDecoratorApplier {
return colBottom;
}
private static List<TreeBlockCanvas.Vec> sortedPositions(Collection<TreeBlockCanvas.Vec> positions) {
List<TreeBlockCanvas.Vec> sorted = new ArrayList<>(positions);
sorted.sort(null);
return sorted;
}
private static List<Map.Entry<Long, Integer>> sortedColumns(Map<Long, Integer> columns) {
List<Map.Entry<Long, Integer>> sorted = new ArrayList<>(columns.entrySet());
sorted.sort(Comparator.comparingLong(Map.Entry::getKey));
return sorted;
}
private static String facingAway(int cx, int cz, int x, int z) {
int dx = x - cx;
int dz = z - cz;
@@ -44,15 +44,13 @@ public final class TreeSupport {
int best = Integer.MAX_VALUE;
for (TreeBlockCanvas.Vec wood : canvas.getTrunk()) {
int cd = chebyshev(leaf, wood);
if (cd < best) {
if (cd < best || cd == best && nearest != null && wood.compareTo(nearest) < 0) {
best = cd;
nearest = wood;
if (cd <= 1) {
break;
}
}
}
if (nearest != null && best > worstGap) {
if (nearest != null && (best > worstGap
|| best == worstGap && worstLeaf != null && leaf.compareTo(worstLeaf) < 0)) {
worstGap = best;
worstLeaf = leaf;
worstWood = nearest;
@@ -24,6 +24,10 @@ import com.dfsek.paralithic.eval.parser.Parser;
import com.dfsek.paralithic.eval.parser.Scope;
import art.arcane.iris.pack.loading.IrisRegistrant;
import art.arcane.volmlib.util.cache.AtomicCache;
import art.arcane.iris.generation.cache.LazyBoundedCache;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import art.arcane.iris.generation.noise.IrisExpressionFunction.FunctionContext;
import art.arcane.iris.pack.schema.annotation.ArrayType;
import art.arcane.volmlib.util.documentation.Description;
@@ -58,7 +62,9 @@ public class IrisExpression extends IrisRegistrant {
private String expression;
private transient AtomicCache<Expression> expressionCache = new AtomicCache<>();
private transient AtomicCache<ProceduralStream<Double>> streamCache = new AtomicCache<>();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient LazyBoundedCache<Long, ProceduralStream<Double>> streams = new LazyBoundedCache<>(8);
private Expression expression() {
return expressionCache.aquire(() -> {
@@ -94,8 +100,11 @@ public class IrisExpression extends IrisRegistrant {
}
public ProceduralStream<Double> stream(RNG rng) {
return streamCache.aquire(() -> ProceduralStream.of((x, z) -> evaluate(rng, x, z),
(x, y, z) -> evaluate(rng, x, y, z), Interpolated.DOUBLE));
return streams.computeIfAbsent(rng.getSeed(), seed -> {
RNG streamRng = new RNG(seed);
return ProceduralStream.of((x, z) -> evaluate(streamRng, x, z),
(x, y, z) -> evaluate(streamRng, x, y, z), Interpolated.DOUBLE);
});
}
public double evaluate(RNG rng, double x, double z) {
@@ -10,7 +10,11 @@ import art.arcane.volmlib.util.documentation.Description;
import art.arcane.iris.pack.schema.annotation.MinNumber;
import art.arcane.iris.pack.schema.annotation.Required;
import art.arcane.iris.pack.schema.annotation.Snippet;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.iris.generation.cache.LazyBoundedCache;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.volmlib.util.stream.ProceduralStream;
import java.util.Objects;
import art.arcane.volmlib.util.math.RNG;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
@@ -48,7 +52,10 @@ public class IrisExpressionFunction implements DynamicFunction {
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient final KMap<FunctionContext, Provider> cache = new KMap<>();
private transient final LazyBoundedCache<FunctionKey, Provider> providers = new LazyBoundedCache<>(8);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient volatile CachedProvider recentProvider;
private transient IrisData data;
public boolean isValid() {
@@ -74,19 +81,53 @@ public class IrisExpressionFunction implements DynamicFunction {
@Override
public double eval(@Nullable Context raw, double... args) {
return cache.computeIfAbsent((FunctionContext) raw, context -> {
assert context != null;
if (engineStreamValue != null) {
var stream = engineStreamValue.get(data.getEngine());
return d -> stream.get(d[0], d[1]);
}
FunctionContext context = Objects.requireNonNull((FunctionContext) raw, "Expression context");
IrisData currentData = data;
Engine engine = currentData == null ? null : currentData.getEngine();
long seed = context.rng.getSeed();
CachedProvider recent = recentProvider;
if (recent != null && recent.key.matches(seed, currentData, engine)) {
return recent.provider.eval(args);
}
FunctionKey key = new FunctionKey(seed, currentData, engine);
Provider provider = providers.computeIfAbsent(key, ignored -> createProvider(key));
recentProvider = new CachedProvider(key, provider);
return provider.eval(args);
}
if (styleValue != null) {
return styleValue.createNoCache(context.rng, data)::noise;
private Provider createProvider(FunctionKey key) {
if (engineStreamValue != null) {
if (key.engine == null) {
throw new IllegalStateException("Expression function '" + name + "' requires an active Iris engine.");
}
ProceduralStream<Double> stream = engineStreamValue.get(key.engine);
return coordinates -> stream.get(coordinates[0], coordinates[1]);
}
if (styleValue != null) {
return styleValue.create(new RNG(key.seed), key.data, key.engine)::noise;
}
return coordinates -> Double.NaN;
}
return d -> Double.NaN;
}).eval(args);
private record FunctionKey(long seed, IrisData data, Engine engine) {
private boolean matches(long seed, IrisData data, Engine engine) {
return this.seed == seed && this.data == data && this.engine == engine;
}
@Override
public boolean equals(Object object) {
return object instanceof FunctionKey other && matches(other.seed, other.data, other.engine);
}
@Override
public int hashCode() {
int hash = Long.hashCode(seed);
hash = 31 * hash + System.identityHashCode(data);
return 31 * hash + System.identityHashCode(engine);
}
}
private record CachedProvider(FunctionKey key, Provider provider) {
}
public record FunctionContext(@NonNull RNG rng) implements Context {
@@ -19,7 +19,7 @@
package art.arcane.iris.generation.noise;
import art.arcane.iris.pack.loading.IrisRegistrant;
import art.arcane.volmlib.util.cache.AtomicCache;
import art.arcane.iris.generation.cache.LazyBoundedCache;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.schema.annotation.ArrayType;
@@ -54,7 +54,12 @@ public class IrisGenerator extends IrisRegistrant {
private static final int SURFACE_CACHE_STRIPES = 16;
private static final int SURFACE_CACHE_STRIPE_SIZE = 1024;
private final transient AtomicCache<CellGenerator> cellGen = new AtomicCache<>();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient LazyBoundedCache<Long, CellGenerator> cellGenerators = new LazyBoundedCache<>(8);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient volatile CachedCellGenerator recentCellGenerator;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient volatile SurfaceCache surfaceSamples;
@@ -115,7 +120,17 @@ public class IrisGenerator extends IrisRegistrant {
}
public CellGenerator getCellGenerator(long seed) {
return cellGen.aquire(() -> new CellGenerator(new RNG(seed + 239466)));
CachedCellGenerator recent = recentCellGenerator;
if (recent != null && recent.seed == seed) {
return recent.generator;
}
CellGenerator generator = cellGenerators.computeIfAbsent(seed, ignored -> {
CellGenerator created = new CellGenerator(new RNG(seed + 239466));
created.setShuffle(getCellFractureShuffle());
return created;
});
recentCellGenerator = new CachedCellGenerator(seed, generator);
return generator;
}
public <T> T fit(T[] v, long superSeed, double rx, double rz) {
@@ -296,7 +311,6 @@ public class IrisGenerator extends IrisRegistrant {
}
public double cell(double rx, double rz, double v, double superSeed) {
getCellGenerator(getSeed() + 46222).setShuffle(getCellFractureShuffle());
double fractureX = rx / getCellFractureZoom();
double fractureZ = rz / getCellFractureZoom();
return getCellGenerator(getSeed() + 46222).getDistance(fractureX, fractureZ) > getCellPercentSize() ? (v * getCellFractureHeight()) : v;
@@ -363,4 +377,7 @@ public class IrisGenerator extends IrisRegistrant {
entries.set(slot, sample);
}
}
private record CachedCellGenerator(long seed, CellGenerator generator) {
}
}
@@ -58,6 +58,9 @@ public class IrisGeneratorStyle {
@Setter(AccessLevel.NONE)
private final transient LazyBoundedCache<GeneratorCacheKey, CNG> generatorCache =
new LazyBoundedCache<>(GENERATOR_CACHE_SIZE);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient volatile CachedGenerator recentGenerator;
@Description("The base noise style. Used when neither expression nor imageMap is set; a failed expression also falls back to this style.")
private NoiseStyle style = NoiseStyle.FLAT;
@@ -124,7 +127,7 @@ public class IrisGeneratorStyle {
}
private int hash() {
return Objects.hash(expression, imageMapHash(), multiplier, fracture != null ? fracture.hash() : 0, exponent, cacheSize, zoom, cellularZoom, cellularFrequency, style);
return Objects.hash(expression, imageMapHash(), multiplier, fracture != null ? fracture.hash() : 0, exponent, cacheSize, zoom, cellularZoom, cellularFrequency, style == null ? null : style.name());
}
public int prebakeSignature() {
@@ -233,10 +236,24 @@ public class IrisGeneratorStyle {
return create(rng, data, resolveEngine(data));
}
public CNG create(long seed, IrisData data) {
return cachedGenerator(seed, data, resolveEngine(data));
}
public CNG create(RNG rng, IrisData data, Engine engine) {
GeneratorCacheKey key = new GeneratorCacheKey(data, engine, rng.getSeed());
return generatorCache.computeIfAbsent(key,
ignored -> createNoCache(rng, data, 1, 0, false, engine));
return cachedGenerator(rng.getSeed(), data, engine);
}
private CNG cachedGenerator(long seed, IrisData data, Engine engine) {
CachedGenerator recent = recentGenerator;
if (recent != null && recent.key.matches(data, engine, seed)) {
return recent.generator;
}
GeneratorCacheKey key = new GeneratorCacheKey(data, engine, seed);
CNG generator = generatorCache.computeIfAbsent(key,
ignored -> createNoCache(new RNG(seed), data, 1, 0, false, engine));
recentGenerator = new CachedGenerator(key, generator);
return generator;
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
@@ -295,6 +312,10 @@ public class IrisGeneratorStyle {
this.seed = seed;
}
private boolean matches(IrisData data, Engine engine, long seed) {
return this.data == data && this.engine == engine && this.seed == seed;
}
@Override
public boolean equals(Object other) {
if (this == other) {
@@ -313,4 +334,7 @@ public class IrisGeneratorStyle {
return 31 * result + Long.hashCode(seed);
}
}
private record CachedGenerator(GeneratorCacheKey key, CNG generator) {
}
}
@@ -21,6 +21,7 @@ package art.arcane.iris.generation.runtime;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.decoration.IrisDecorator;
import art.arcane.iris.generation.decoration.IrisEngineDecorator;
import art.arcane.iris.structure.object.IrisObjectPlacement;
import art.arcane.iris.generation.decoration.IrisOreGenerator;
import art.arcane.iris.generation.decoration.IrisProceduralObjects;
@@ -47,8 +48,8 @@ public final class GenerationCacheWarmer {
biomes.sort(Comparator.comparing(IrisBiome::getLoadKey));
for (IrisBiome biome : biomes) {
warmPlacements(biome.getObjects(), root, counter, data, engine);
warmDecorators(biome.getDecorators(), root, counter, data);
warmOres(biome.getOres(), root, counter, data);
warmDecorators(biome.getDecorators(), engine.getSeedManager().getComponent(), counter, data);
warmOres(biome.getOres(), engine.getSeedManager().getTerrain(), counter, data);
warmProcedural(biome.getProceduralObjects(), root, counter, data);
}
@@ -56,11 +57,11 @@ public final class GenerationCacheWarmer {
regions.sort(Comparator.comparing(IrisRegion::getLoadKey));
for (IrisRegion region : regions) {
warmPlacements(region.getObjects(), root, counter, data, engine);
warmOres(region.getOres(), root, counter, data);
warmOres(region.getOres(), engine.getSeedManager().getTerrain(), counter, data);
warmProcedural(region.getProceduralObjects(), root, counter, data);
}
warmOres(engine.getDimension().getOres(), root, counter, data);
warmOres(engine.getDimension().getOres(), engine.getSeedManager().getTerrain(), counter, data);
IrisLogging.debug("[IrisEngine timing] cache warm " + counter[0] + " configs=" + (M.ms() - start) + "ms");
}
@@ -80,7 +81,7 @@ public final class GenerationCacheWarmer {
}
}
private static void warmDecorators(KList<IrisDecorator> decorators, RNG root, int[] counter, IrisData data) {
private static void warmDecorators(KList<IrisDecorator> decorators, long componentSeed, int[] counter, IrisData data) {
if (decorators == null) {
return;
}
@@ -88,14 +89,15 @@ public final class GenerationCacheWarmer {
if (decorator == null) {
continue;
}
RNG rng = root.nextParallelRNG(counter[0]++);
counter[0]++;
RNG rng = new RNG(IrisEngineDecorator.seedForPart(componentSeed, decorator.getPartOf()));
decorator.getHeightGenerator(rng, data);
decorator.getGenerator(rng, data);
decorator.getVarianceGenerator(rng, data);
}
}
private static void warmOres(KList<IrisOreGenerator> ores, RNG root, int[] counter, IrisData data) {
private static void warmOres(KList<IrisOreGenerator> ores, long terrainSeed, int[] counter, IrisData data) {
if (ores == null) {
return;
}
@@ -103,7 +105,8 @@ public final class GenerationCacheWarmer {
if (ore == null) {
continue;
}
ore.warm(root.nextParallelRNG(counter[0]++), data);
counter[0]++;
ore.warm(new RNG(terrainSeed), data);
}
}
@@ -67,6 +67,7 @@ import art.arcane.iris.pack.datapack.DataVersion;
import art.arcane.iris.pack.datapack.IDataFixer;
import art.arcane.iris.pack.datapack.IDataFixer.Dimension;
import art.arcane.volmlib.util.cache.AtomicCache;
import art.arcane.iris.generation.cache.LazyBoundedCache;
import art.arcane.iris.world.history.GenerationRegistryContractFactory;
import art.arcane.iris.pack.schema.annotation.ArrayType;
import art.arcane.volmlib.util.documentation.Description;
@@ -89,6 +90,9 @@ import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.noise.CNG;
import lombok.Data;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
@@ -130,7 +134,12 @@ public class IrisDimension extends IrisRegistrant {
private final transient AtomicCache<IrisStaticObjectLayer> staticObjectLayer = new AtomicCache<>();
private final transient AtomicCache<CNG> rockLayerGenerator = new AtomicCache<>();
private final transient AtomicCache<CNG> fluidLayerGenerator = new AtomicCache<>();
private final transient AtomicCache<CNG> coordFracture = new AtomicCache<>();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient LazyBoundedCache<Long, CNG> coordinateFractures = new LazyBoundedCache<>(8);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private transient volatile CachedCoordinateFracture recentCoordinateFracture;
private final transient AtomicCache<Double> sinr = new AtomicCache<>();
private final transient AtomicCache<Double> cosr = new AtomicCache<>();
private final transient AtomicCache<Double> rad = new AtomicCache<>();
@@ -537,12 +546,15 @@ public class IrisDimension extends IrisRegistrant {
}
public CNG getCoordFracture(RNG rng, int signature) {
return coordFracture.aquire(() ->
{
CNG coordFracture = CNG.signature(rng.nextParallelRNG(signature));
coordFracture.scale(0.012 / coordFractureZoom);
return coordFracture;
});
long seed = rng.getSeed() + signature;
CachedCoordinateFracture recent = recentCoordinateFracture;
if (recent != null && recent.seed == seed) {
return recent.generator;
}
CNG generator = coordinateFractures.computeIfAbsent(seed, ignored ->
CNG.signature(new RNG(seed)).scale(0.012 / coordFractureZoom));
recentCoordinateFracture = new CachedCoordinateFracture(seed, generator);
return generator;
}
public double getDimensionAngle() {
@@ -1301,4 +1313,7 @@ public class IrisDimension extends IrisRegistrant {
IO.writeAll(dimTypeVanilla, json);
}
}
private record CachedCoordinateFracture(long seed, CNG generator) {
}
}
@@ -67,7 +67,6 @@ public class IrisMaterialPalette implements StructurePalette {
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient AtomicReference<CachedLayerGenerator> recentLayerGenerator = new AtomicReference<>();
private final transient AtomicCache<CNG> heightGenerator = new AtomicCache<>();
@Description("The style of noise")
private IrisGeneratorStyle style = NoiseStyle.STATIC.style();
@MinNumber(0.0001)
@@ -138,6 +138,7 @@ public class IrisObject extends IrisRegistrant {
@Getter
@Setter
protected transient volatile boolean smartBored = false;
transient volatile IrisObjectShaping.SmartBoreVariant smartBoreVariant;
@Setter
protected transient AtomicCache<AxisAlignedBB> aabb = new AtomicCache<>();
transient final AtomicCache<KList<IrisBlockVector>> surfaceSupportOffsets = new AtomicCache<>();
@@ -335,7 +336,8 @@ public class IrisObject extends IrisRegistrant {
}
public int place(int x, int yv, int z, IObjectPlacer oplacer, IrisObjectPlacement config, RNG rng, BiConsumer<BlockPosition, NativeBlockState> listener, CarveResult c, IrisData rdata) {
return new IrisObjectPlacementRunner(this).place(x, yv, z, oplacer, config, rng, listener, c, rdata);
IrisObject placedObject = config.isSmartBore() ? IrisObjectShaping.smartBoredVariant(this) : this;
return new IrisObjectPlacementRunner(placedObject).place(x, yv, z, oplacer, config, rng, listener, c, rdata);
}
KList<IrisBlockVector> getSurfaceSupportOffsets() {
@@ -49,6 +49,7 @@ import art.arcane.volmlib.util.math.Position2;
import art.arcane.volmlib.util.math.RNG;
import art.arcane.volmlib.util.matter.MatterMarker;
import java.util.Comparator;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@@ -123,10 +124,6 @@ final class IrisObjectPlacementRunner {
}
}
if (config.isSmartBore()) {
IrisObjectShaping.ensureSmartBored(self);
}
boolean warped = !config.getWarp().isFlat();
// Placement-invariant hoists off the per-voxel loop: the warp CNG resolution went
// through a keyed cache probe per block, and the two boolean gates below cut per-block
@@ -902,7 +899,9 @@ final class IrisObjectPlacementRunner {
if (heightmap != null) {
RNG rngx = rng.nextParallelRNG(3468854);
for (Position2 i : heightmap.k()) {
KList<Position2> snowColumns = heightmap.k();
snowColumns.sort(Comparator.comparingInt(Position2::getX).thenComparingInt(Position2::getZ));
for (Position2 i : snowColumns) {
int vx = i.getX();
int vy = heightmap.get(i);
int vz = i.getZ();
@@ -39,6 +39,41 @@ final class IrisObjectShaping {
private IrisObjectShaping() {
}
static IrisObject smartBoredVariant(IrisObject self) {
SmartBoreVariant cached = self.smartBoreVariant;
if (cached != null && cached.matches(self)) {
return cached.object();
}
self.writeLock.lock();
try {
cached = self.smartBoreVariant;
if (cached != null && cached.matches(self)) {
return cached.object();
}
IrisObject variant = self.copy();
ensureSmartBored(variant);
self.smartBoreVariant = new SmartBoreVariant(self.blocks, self.states,
self.blocks.modificationRevision(), self.states.modificationRevision(),
self.w, self.h, self.d, self.center.getX(), self.center.getY(), self.center.getZ(), variant);
return variant;
} finally {
self.writeLock.unlock();
}
}
record SmartBoreVariant(VectorMap<NativeBlockState> blocks, VectorMap<TileData> states,
long blockRevision, long stateRevision, int width, int height, int depth,
int centerX, int centerY, int centerZ, IrisObject object) {
boolean matches(IrisObject source) {
return blocks == source.blocks && states == source.states
&& blockRevision == source.blocks.modificationRevision()
&& stateRevision == source.states.modificationRevision()
&& width == source.w && height == source.h && depth == source.d
&& centerX == source.center.getX() && centerY == source.center.getY()
&& centerZ == source.center.getZ();
}
}
static void ensureSmartBored(IrisObject self) {
if (self.smartBored) {
return;