From 1f41e195cfc4964ecf731ed3df4a0b67d5fdaf83 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Wed, 18 Feb 2026 23:44:23 -0500 Subject: [PATCH] p1 --- .../art/arcane/iris/engine/IrisComplex.java | 161 +++++++++++++--- .../engine/object/IrisGeneratorStyle.java | 74 +++++++- .../object/IrisShapedGeneratorStyle.java | 8 + .../interpolation/IrisInterpolation.java | 54 +++++- .../arcane/iris/util/project/noise/CNG.java | 176 +++++++++++++++++- .../util/project/context/ChunkedDataCache.kt | 11 +- 6 files changed, 450 insertions(+), 34 deletions(-) diff --git a/core/src/main/java/art/arcane/iris/engine/IrisComplex.java b/core/src/main/java/art/arcane/iris/engine/IrisComplex.java index d46465e23..942910da4 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisComplex.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisComplex.java @@ -27,7 +27,6 @@ import art.arcane.iris.engine.object.*; import art.arcane.volmlib.util.collection.KList; import art.arcane.iris.util.project.context.IrisContext; import art.arcane.iris.util.common.data.DataProvider; -import art.arcane.iris.util.project.interpolation.IrisInterpolation.NoiseKey; import art.arcane.volmlib.util.math.M; import art.arcane.volmlib.util.math.RNG; import art.arcane.iris.util.project.noise.CNG; @@ -83,6 +82,8 @@ public class IrisComplex implements DataProvider { private ProceduralStream fluidStream; private IrisBiome focusBiome; private IrisRegion focusRegion; + private Map> generatorBounds; + private Set generatorBiomes; public IrisComplex(Engine engine) { this(engine, false); @@ -97,6 +98,7 @@ public class IrisComplex implements DataProvider { double height = engine.getMaxHeight(); fluidHeight = engine.getDimension().getFluidHeight(); generators = new HashMap<>(); + generatorBiomes = Collections.newSetFromMap(new IdentityHashMap<>()); focusBiome = engine.getFocus(); focusRegion = engine.getFocusRegion(); Map> inferredStreams = new HashMap<>(); @@ -116,9 +118,20 @@ public class IrisComplex implements DataProvider { .getAllBiomes(this) .forEach(this::registerGenerators)); } + generatorBounds = buildGeneratorBounds(engine); boolean legacy = engine.getDimension().isLegacyRarity(); - overlayStream = ProceduralStream.ofDouble((x, z) -> 0.0D).waste("Overlay Stream"); - engine.getDimension().getOverlayNoise().forEach(i -> overlayStream = overlayStream.add((x, z) -> i.get(rng, getData(), x, z))); + KList overlayNoise = engine.getDimension().getOverlayNoise(); + overlayStream = overlayNoise.isEmpty() + ? ProceduralStream.ofDouble((x, z) -> 0.0D).waste("Overlay Stream") + : ProceduralStream.ofDouble((x, z) -> { + double value = 0D; + + for (IrisShapedGeneratorStyle style : overlayNoise) { + value += style.get(rng, getData(), x, z); + } + + return value; + }).waste("Overlay Stream"); rockStream = engine.getDimension().getRockPalette().getLayerGenerator(rng.nextParallelRNG(45), data).stream() .select(engine.getDimension().getRockPalette().getBlockData(data)).waste("Rock Stream"); fluidStream = engine.getDimension().getFluidPalette().getLayerGenerator(rng.nextParallelRNG(78), data).stream() @@ -306,18 +319,27 @@ public class IrisComplex implements DataProvider { return 0; } - HashMap cache = new HashMap<>(64); + CoordinateBiomeCache sampleCache = new CoordinateBiomeCache(64); + IdentityHashMap cachedBounds = generatorBounds.get(interpolator); + IdentityHashMap localBounds = new IdentityHashMap<>(8); double hi = interpolator.interpolate(x, z, (xx, zz) -> { try { - IrisBiome bx = baseBiomeStream.get(xx, zz); - cache.put(new NoiseKey(xx, zz), bx); - double b = 0; - - for (IrisGenerator gen : generators) { - b += bx.getGenLinkMax(gen.getLoadKey(), engine); + IrisBiome bx = sampleCache.get(xx, zz); + if (bx == null) { + bx = baseBiomeStream.get(xx, zz); + sampleCache.put(xx, zz, bx); } - return b; + GeneratorBounds bounds = cachedBounds == null ? null : cachedBounds.get(bx); + if (bounds == null) { + bounds = localBounds.get(bx); + if (bounds == null) { + bounds = computeGeneratorBounds(engine, generators, bx); + localBounds.put(bx, bounds); + } + } + + return bounds.max; } catch (Throwable e) { Iris.reportError(e); e.printStackTrace(); @@ -329,18 +351,22 @@ public class IrisComplex implements DataProvider { double lo = interpolator.interpolate(x, z, (xx, zz) -> { try { - IrisBiome bx = cache.get(new NoiseKey(xx, zz)); + IrisBiome bx = sampleCache.get(xx, zz); if (bx == null) { bx = baseBiomeStream.get(xx, zz); - cache.put(new NoiseKey(xx, zz), bx); - } - double b = 0; - - for (IrisGenerator gen : generators) { - b += bx.getGenLinkMin(gen.getLoadKey(), engine); + sampleCache.put(xx, zz, bx); } - return b; + GeneratorBounds bounds = cachedBounds == null ? null : cachedBounds.get(bx); + if (bounds == null) { + bounds = localBounds.get(bx); + if (bounds == null) { + bounds = computeGeneratorBounds(engine, generators, bx); + localBounds.put(bx, bounds); + } + } + + return bounds.min; } catch (Throwable e) { Iris.reportError(e); e.printStackTrace(); @@ -362,8 +388,8 @@ public class IrisComplex implements DataProvider { private double getInterpolatedHeight(Engine engine, double x, double z, long seed) { double h = 0; - for (IrisInterpolator i : generators.keySet()) { - h += interpolateGenerators(engine, i, generators.get(i), x, z, seed); + for (Map.Entry> entry : generators.entrySet()) { + h += interpolateGenerators(engine, entry.getKey(), entry.getValue(), x, z, seed); } return h; @@ -374,6 +400,7 @@ public class IrisComplex implements DataProvider { } private void registerGenerators(IrisBiome biome) { + generatorBiomes.add(biome); biome.getGenerators().forEach(c -> registerGenerator(c.getCachedGenerator(this))); } @@ -381,6 +408,38 @@ public class IrisComplex implements DataProvider { generators.computeIfAbsent(cachedGenerator.getInterpolator(), (k) -> new HashSet<>()).add(cachedGenerator); } + private Map> buildGeneratorBounds(Engine engine) { + Map> bounds = new HashMap<>(); + KList allBiomes = new KList<>(generatorBiomes); + + if (focusBiome != null && !allBiomes.contains(focusBiome)) { + allBiomes.add(focusBiome); + } + + for (Map.Entry> entry : generators.entrySet()) { + IdentityHashMap interpolatorBounds = new IdentityHashMap<>(Math.max(allBiomes.size(), 16)); + for (IrisBiome biome : allBiomes) { + interpolatorBounds.put(biome, computeGeneratorBounds(engine, entry.getValue(), biome)); + } + bounds.put(entry.getKey(), interpolatorBounds); + } + + return bounds; + } + + private GeneratorBounds computeGeneratorBounds(Engine engine, Set generators, IrisBiome biome) { + double min = 0D; + double max = 0D; + + for (IrisGenerator gen : generators) { + String key = gen.getLoadKey(); + max += biome.getGenLinkMax(key, engine); + min += biome.getGenLinkMin(key, engine); + } + + return new GeneratorBounds(min, max); + } + private IrisBiome implode(IrisBiome b, Double x, Double z) { if (b.getChildren().isEmpty()) { return b; @@ -406,6 +465,66 @@ public class IrisComplex implements DataProvider { return implode(biome, x, z, max - 1); } + private static class GeneratorBounds { + private final double min; + private final double max; + + private GeneratorBounds(double min, double max) { + this.min = min; + this.max = max; + } + } + + private static class CoordinateBiomeCache { + private long[] xBits; + private long[] zBits; + private IrisBiome[] values; + private int size; + + private CoordinateBiomeCache(int initialSize) { + xBits = new long[initialSize]; + zBits = new long[initialSize]; + values = new IrisBiome[initialSize]; + size = 0; + } + + private IrisBiome get(double x, double z) { + long xb = Double.doubleToLongBits(x); + long zb = Double.doubleToLongBits(z); + for (int i = 0; i < size; i++) { + if (xBits[i] == xb && zBits[i] == zb) { + return values[i]; + } + } + + return null; + } + + private void put(double x, double z, IrisBiome biome) { + if (size >= xBits.length) { + grow(); + } + + xBits[size] = Double.doubleToLongBits(x); + zBits[size] = Double.doubleToLongBits(z); + values[size] = biome; + size++; + } + + private void grow() { + int nextSize = xBits.length << 1; + long[] nx = new long[nextSize]; + long[] nz = new long[nextSize]; + IrisBiome[] nv = new IrisBiome[nextSize]; + System.arraycopy(xBits, 0, nx, 0, size); + System.arraycopy(zBits, 0, nz, 0, size); + System.arraycopy(values, 0, nv, 0, size); + xBits = nx; + zBits = nz; + values = nv; + } + } + public void close() { } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisGeneratorStyle.java b/core/src/main/java/art/arcane/iris/engine/object/IrisGeneratorStyle.java index feca3a06f..afd3b3742 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisGeneratorStyle.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisGeneratorStyle.java @@ -32,6 +32,7 @@ import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; +import java.io.File; import java.util.Objects; @Snippet("style") @@ -91,26 +92,85 @@ public class IrisGeneratorStyle { } + private int imageMapHash() { + if (imageMap == null) { + return 0; + } + + return Objects.hash(imageMap.getImage(), + imageMap.getCoordinateScale(), + imageMap.getInterpolationMethod(), + imageMap.getChannel(), + imageMap.isInverted(), + imageMap.isTiled(), + imageMap.isCentered()); + } + private int hash() { - return Objects.hash(expression, imageMap, multiplier, axialFracturing, fracture != null ? fracture.hash() : 0, exponent, cacheSize, zoom, cellularZoom, cellularFrequency, style); + return Objects.hash(expression, imageMapHash(), script, multiplier, axialFracturing, fracture != null ? fracture.hash() : 0, exponent, cacheSize, zoom, cellularZoom, cellularFrequency, style); + } + + private String cachePrefix(RNG rng) { + return "style-" + Integer.toUnsignedString(hash()) + + "-seed-" + Long.toUnsignedString(rng.getSeed()) + + "-src-"; + } + + private String cacheKey(RNG rng, long sourceStamp) { + return cachePrefix(rng) + Long.toUnsignedString(sourceStamp); + } + + private long scriptStamp(IrisData data) { + if (getScript() == null) { + return 0L; + } + + File scriptFile = data.getScriptLoader().findFile(getScript()); + if (scriptFile == null) { + return Integer.toUnsignedLong(getScript().hashCode()); + } + + return Integer.toUnsignedLong(Objects.hash(getScript(), scriptFile.lastModified(), scriptFile.length())); + } + + private void clearStaleCacheEntries(IrisData data, String prefix, String key) { + File cacheFolder = new File(data.getDataFolder(), ".cache"); + File[] files = cacheFolder.listFiles((dir, name) -> name.endsWith(".cnm") && name.startsWith(prefix)); + if (files == null) { + return; + } + + String keepFile = key + ".cnm"; + for (File file : files) { + if (keepFile.equals(file.getName())) { + continue; + } + + if (!file.delete()) { + Iris.debug("Unable to delete stale noise cache " + file.getName()); + } + } } public CNG createNoCache(RNG rng, IrisData data, boolean actuallyCached) { - String cacheKey = hash() + ""; - CNG cng = null; + long sourceStamp = 0L; if (getExpression() != null) { IrisExpression e = data.getExpressionLoader().load(getExpression()); if (e != null) { cng = new CNG(rng, new ExpressionNoise(rng, e), 1D, 1).bake(); + sourceStamp = Integer.toUnsignedLong(Objects.hash(getExpression(), + e.getLoadFile() == null ? 0L : e.getLoadFile().lastModified())); } } else if (getImageMap() != null) { cng = new CNG(rng, new ImageNoise(data, getImageMap()), 1D, 1).bake(); + sourceStamp = Integer.toUnsignedLong(imageMapHash()); } else if (getScript() != null) { Object result = data.getEnvironment().createNoise(getScript(), rng); if (result == null) Iris.warn("Failed to create noise from script: " + getScript()); if (result instanceof NoiseGenerator generator) { cng = new CNG(rng, generator, 1D, 1).bake(); + sourceStamp = scriptStamp(data); } } @@ -126,7 +186,13 @@ public class IrisGeneratorStyle { } if (cellularFrequency > 0) { - return cng.cellularize(rng.nextParallelRNG(884466), cellularFrequency).scale(1D / cellularZoom).bake(); + cng = cng.cellularize(rng.nextParallelRNG(884466), cellularFrequency).scale(1D / cellularZoom).bake(); + } + + if (cacheSize > 0) { + String key = cacheKey(rng, sourceStamp); + clearStaleCacheEntries(data, cachePrefix(rng), key); + cng = cng.cached(cacheSize, key, data.getDataFolder()); } return cng; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisShapedGeneratorStyle.java b/core/src/main/java/art/arcane/iris/engine/object/IrisShapedGeneratorStyle.java index f01f278c3..c755d6f53 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisShapedGeneratorStyle.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisShapedGeneratorStyle.java @@ -63,6 +63,14 @@ public class IrisShapedGeneratorStyle { return generator.create(rng, data).fitDouble(min, max, dim); } + public double get(RNG rng, IrisData data, double x, double z) { + return generator.create(rng, data).fitDouble(min, max, x, z); + } + + public double get(RNG rng, IrisData data, double x, double y, double z) { + return generator.create(rng, data).fitDouble(min, max, x, y, z); + } + public boolean isFlat() { return min == max || getGenerator().isFlat(); } diff --git a/core/src/main/java/art/arcane/iris/util/project/interpolation/IrisInterpolation.java b/core/src/main/java/art/arcane/iris/util/project/interpolation/IrisInterpolation.java index 03619bba4..64ee9c471 100644 --- a/core/src/main/java/art/arcane/iris/util/project/interpolation/IrisInterpolation.java +++ b/core/src/main/java/art/arcane/iris/util/project/interpolation/IrisInterpolation.java @@ -999,8 +999,8 @@ public class IrisInterpolation { } public static double getNoise(InterpolationMethod method, int x, int z, double h, NoiseProvider noise) { - HashMap cache = new HashMap<>(64); - NoiseProvider n = (x1, z1) -> cache.computeIfAbsent(new NoiseKey(x1 - x, z1 - z), k -> noise.noise(x1, z1)); + NoiseSampleCache2D cache = new NoiseSampleCache2D(64); + NoiseProvider n = (x1, z1) -> cache.getOrSample(x1 - x, z1 - z, x1, z1, noise); if (method.equals(InterpolationMethod.BILINEAR)) { return getBilinearNoise(x, z, h, n); @@ -1059,6 +1059,56 @@ public class IrisInterpolation { return n.noise(x, z); } + private static class NoiseSampleCache2D { + private long[] xBits; + private long[] zBits; + private double[] values; + private int size; + + public NoiseSampleCache2D(int initialCapacity) { + xBits = new long[initialCapacity]; + zBits = new long[initialCapacity]; + values = new double[initialCapacity]; + size = 0; + } + + public double getOrSample(double relativeX, double relativeZ, double sampleX, double sampleZ, NoiseProvider provider) { + long rx = Double.doubleToLongBits(relativeX); + long rz = Double.doubleToLongBits(relativeZ); + + for (int i = 0; i < size; i++) { + if (xBits[i] == rx && zBits[i] == rz) { + return values[i]; + } + } + + double value = provider.noise(sampleX, sampleZ); + if (size >= xBits.length) { + grow(); + } + + xBits[size] = rx; + zBits[size] = rz; + values[size] = value; + size++; + + return value; + } + + private void grow() { + int nextLength = xBits.length << 1; + long[] nextXBits = new long[nextLength]; + long[] nextZBits = new long[nextLength]; + double[] nextValues = new double[nextLength]; + System.arraycopy(xBits, 0, nextXBits, 0, size); + System.arraycopy(zBits, 0, nextZBits, 0, size); + System.arraycopy(values, 0, nextValues, 0, size); + xBits = nextXBits; + zBits = nextZBits; + values = nextValues; + } + } + public static double rangeScale(double amin, double amax, double bmin, double bmax, double b) { return amin + ((amax - amin) * ((b - bmin) / (bmax - bmin))); } diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/CNG.java b/core/src/main/java/art/arcane/iris/util/project/noise/CNG.java index bc23ad9b8..ebbb01b8a 100644 --- a/core/src/main/java/art/arcane/iris/util/project/noise/CNG.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/CNG.java @@ -425,6 +425,26 @@ public class CNG { return (int) Math.round(IrisInterpolation.lerp(min, max, noise)); } + public int fit(int min, int max, double x, double z) { + if (min == max) { + return min; + } + + double noise = noise(x, z); + + return (int) Math.round(IrisInterpolation.lerp(min, max, noise)); + } + + public int fit(int min, int max, double x, double y, double z) { + if (min == max) { + return min; + } + + double noise = noise(x, y, z); + + return (int) Math.round(IrisInterpolation.lerp(min, max, noise)); + } + public int fit(double min, double max, double... dim) { if (min == max) { return (int) Math.round(min); @@ -435,6 +455,26 @@ public class CNG { return (int) Math.round(IrisInterpolation.lerp(min, max, noise)); } + public int fit(double min, double max, double x, double z) { + if (min == max) { + return (int) Math.round(min); + } + + double noise = noise(x, z); + + return (int) Math.round(IrisInterpolation.lerp(min, max, noise)); + } + + public int fit(double min, double max, double x, double y, double z) { + if (min == max) { + return (int) Math.round(min); + } + + double noise = noise(x, y, z); + + return (int) Math.round(IrisInterpolation.lerp(min, max, noise)); + } + public double fitDouble(double min, double max, double... dim) { if (min == max) { return min; @@ -445,6 +485,26 @@ public class CNG { return IrisInterpolation.lerp(min, max, noise); } + public double fitDouble(double min, double max, double x, double z) { + if (min == max) { + return min; + } + + double noise = noise(x, z); + + return IrisInterpolation.lerp(min, max, noise); + } + + public double fitDouble(double min, double max, double x, double y, double z) { + if (min == max) { + return min; + } + + double noise = noise(x, y, z); + + return IrisInterpolation.lerp(min, max, noise); + } + private double getNoise(double... dim) { double scale = noscale ? 1 : this.bakedScale * this.scale; @@ -481,6 +541,42 @@ public class CNG { return generator.noise(x * scale, y * scale, z * scale) * opacity; } + private double getNoise(double x) { + double scl = noscale ? 1 : this.bakedScale * this.scale; + + if (fracture == null || noscale) { + return generator.noise(x * scl, 0D, 0D) * opacity; + } + + double fx = x + ((fracture.noise(x) - 0.5D) * fscale); + return generator.noise(fx * scl, 0D, 0D) * opacity; + } + + private double getNoise(double x, double z) { + double scl = noscale ? 1 : this.bakedScale * this.scale; + + if (fracture == null || noscale) { + return generator.noise(x * scl, z * scl, 0D) * opacity; + } + + double fx = x + ((fracture.noise(x, z) - 0.5D) * fscale); + double fz = z + ((fracture.noise(z, x) - 0.5D) * fscale); + return generator.noise(fx * scl, fz * scl, 0D) * opacity; + } + + private double getNoise(double x, double y, double z) { + double scl = noscale ? 1 : this.bakedScale * this.scale; + + if (fracture == null || noscale) { + return generator.noise(x * scl, y * scl, z * scl) * opacity; + } + + double fx = x + ((fracture.noise(x, y, z) - 0.5D) * fscale); + double fy = y + ((fracture.noise(y, x) - 0.5D) * fscale); + double fz = z + ((fracture.noise(z, x, y) - 0.5D) * fscale); + return generator.noise(fx * scl, fy * scl, fz * scl) * opacity; + } + public double invertNoise(double... dim) { if (dim.length == 1) { return noise(-dim[0]); @@ -497,9 +593,69 @@ public class CNG { return (noise(dim) * 2) - 1; } + private static boolean isWholeCoordinate(double value) { + return value == Math.rint(value); + } + + private double applyPost(double n, double x) { + n = power != 1D ? (n < 0 ? -Math.pow(Math.abs(n), power) : Math.pow(n, power)) : n; + double m = 1; + hits += oct; + + if (children != null) { + for (CNG i : children) { + double[] r = injector.combine(n, i.noise(x)); + n = r[0]; + m += r[1]; + } + } + + return ((n / m) - down + up) * patch; + } + + private double applyPost(double n, double x, double z) { + n = power != 1D ? (n < 0 ? -Math.pow(Math.abs(n), power) : Math.pow(n, power)) : n; + double m = 1; + hits += oct; + + if (children != null) { + for (CNG i : children) { + double[] r = injector.combine(n, i.noise(x, z)); + n = r[0]; + m += r[1]; + } + } + + return ((n / m) - down + up) * patch; + } + + private double applyPost(double n, double x, double y, double z) { + n = power != 1D ? (n < 0 ? -Math.pow(Math.abs(n), power) : Math.pow(n, power)) : n; + double m = 1; + hits += oct; + + if (children != null) { + for (CNG i : children) { + double[] r = injector.combine(n, i.noise(x, y, z)); + n = r[0]; + m += r[1]; + } + } + + return ((n / m) - down + up) * patch; + } + public double noise(double... dim) { - if (cache != null && dim.length == 2) { - return cache.get((int) dim[0], (int) dim[1]); + if (dim.length == 1) { + return noise(dim[0]); + } + + if (dim.length == 2) { + return noise(dim[0], dim[1]); + } + + if (dim.length == 3) { + return noise(dim[0], dim[1], dim[2]); } double n = getNoise(dim); @@ -519,6 +675,22 @@ public class CNG { return ((n / m) - down + up) * patch; } + public double noise(double x) { + return applyPost(getNoise(x), x); + } + + public double noise(double x, double z) { + if (cache != null && isWholeCoordinate(x) && isWholeCoordinate(z)) { + return cache.get((int) x, (int) z); + } + + return applyPost(getNoise(x, z), x, z); + } + + public double noise(double x, double y, double z) { + return applyPost(getNoise(x, y, z), x, y, z); + } + public CNG pow(double power) { this.power = power; return this; diff --git a/core/src/main/kotlin/art/arcane/iris/util/project/context/ChunkedDataCache.kt b/core/src/main/kotlin/art/arcane/iris/util/project/context/ChunkedDataCache.kt index e28c63fde..a098cacd8 100644 --- a/core/src/main/kotlin/art/arcane/iris/util/project/context/ChunkedDataCache.kt +++ b/core/src/main/kotlin/art/arcane/iris/util/project/context/ChunkedDataCache.kt @@ -23,11 +23,12 @@ class ChunkedDataCache private constructor( if (!cache) return supervisorScope { - for (i in 0 until 16) { - for (j in 0 until 16) { - launch(context) { - val t = stream.get((x + i).toDouble(), (z + j).toDouble()) - data[(j * 16) + i] = t + for (j in 0 until 16) { + launch(context) { + val rowOffset = j * 16 + val zz = (z + j).toDouble() + for (i in 0 until 16) { + data[rowOffset + i] = stream.get((x + i).toDouble(), zz) } } }