From 6202ae5d06ed7809ad9295432d6e00871e23fd18 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Mon, 28 Sep 2026 15:54:22 -0400 Subject: [PATCH 1/2] Cheaper ellipsoid deposit cells - squared y/z offsets computed once per sphere row, same evaluation order - test pins cells, order and RNG draws against the per-cell form --- .../decoration/IrisDepositGenerator.java | 43 +++++-- .../IrisDepositEllipsoidCellsTest.java | 113 ++++++++++++++++++ 2 files changed, 146 insertions(+), 10 deletions(-) create mode 100644 core/src/test/java/art/arcane/iris/generation/decoration/IrisDepositEllipsoidCellsTest.java 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 80ff26a4f..ffc7dd39b 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 @@ -304,7 +304,12 @@ public class IrisDepositGenerator { return new ClumpFootprint(cells.extentX * 2 + 1, cells.minY, false); } - private static void vanillaEllipsoidCells(RNG rng, int size, CellSink sink) { + /** + * Emits the clump's cells sphere by sphere in x, y, z order, duplicates included. The squared y and z + * offsets are computed once per sphere row; the sums keep the per-cell evaluation order, so every cell + * test is bit-identical to evaluating {@code nx * nx + ny * ny + nz * nz} in place. + */ + 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; @@ -347,6 +352,8 @@ public class IrisDepositGenerator { } } + double[] squaredY = new double[16]; + double[] squaredZ = new double[16]; for (int i = 0; i < size; i++) { double radius = nodes[i * 4 + 3]; if (radius < 0D) { @@ -361,21 +368,37 @@ public class IrisDepositGenerator { int maxY = Math.max((int) Math.floor(centerY + radius), minY); int minZ = (int) Math.floor(centerZ - radius); int maxZ = Math.max((int) Math.floor(centerZ + radius), minZ); + int spanY = maxY - minY + 1; + int spanZ = maxZ - minZ + 1; + if (spanY > squaredY.length) { + squaredY = new double[spanY]; + } + if (spanZ > squaredZ.length) { + squaredZ = new double[spanZ]; + } + for (int y = 0; y < spanY; y++) { + double ny = (minY + y + 0.5D - centerY) / radius; + squaredY[y] = ny * ny; + } + for (int z = 0; z < spanZ; z++) { + double nz = (minZ + z + 0.5D - centerZ) / radius; + squaredZ[z] = nz * nz; + } for (int x = minX; x <= maxX; x++) { double nx = (x + 0.5D - centerX) / radius; - if (nx * nx >= 1D) { + double squaredX = nx * nx; + if (squaredX >= 1D) { continue; } - for (int y = minY; y <= maxY; y++) { - double ny = (y + 0.5D - centerY) / radius; - if (nx * nx + ny * ny >= 1D) { + for (int y = 0; y < spanY; y++) { + double squaredXY = squaredX + squaredY[y]; + if (squaredXY >= 1D) { continue; } - for (int z = minZ; z <= maxZ; z++) { - double nz = (z + 0.5D - centerZ) / radius; - if (nx * nx + ny * ny + nz * nz < 1D) { - sink.add(x, y, z); + for (int z = 0; z < spanZ; z++) { + if (squaredXY + squaredZ[z] < 1D) { + sink.add(x, minY + y, minZ + z); } } } @@ -601,7 +624,7 @@ public class IrisDepositGenerator { } @FunctionalInterface - private interface CellSink { + interface CellSink { void add(int x, int y, int z); } diff --git a/core/src/test/java/art/arcane/iris/generation/decoration/IrisDepositEllipsoidCellsTest.java b/core/src/test/java/art/arcane/iris/generation/decoration/IrisDepositEllipsoidCellsTest.java new file mode 100644 index 000000000..180138991 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/decoration/IrisDepositEllipsoidCellsTest.java @@ -0,0 +1,113 @@ +package art.arcane.iris.generation.decoration; + +import art.arcane.volmlib.util.math.RNG; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class IrisDepositEllipsoidCellsTest { + @Test + public void cellsAndDrawsMatchThePerCellEvaluation() { + int[] sizes = {1, 2, 3, 4, 5, 7, 8, 9, 12, 16, 17, 24, 32, 33, 48, 64, 96, 200}; + for (int size : sizes) { + long seeds = size <= 64 ? 300L : 8L; + for (long seed = 0L; seed < seeds; seed++) { + long mixedSeed = seed * 0x9E3779B97F4A7C15L + size; + RNG expectedRng = new RNG(mixedSeed); + RNG actualRng = new RNG(mixedSeed); + List expected = new ArrayList<>(); + List actual = new ArrayList<>(); + referenceCells(expectedRng, size, (x, y, z) -> expected.add(pack(x, y, z))); + IrisDepositGenerator.vanillaEllipsoidCells(actualRng, size, (x, y, z) -> actual.add(pack(x, y, z))); + assertEquals("size " + size + " seed " + seed, expected, actual); + assertEquals("size " + size + " seed " + seed, expectedRng.nextLong(), actualRng.nextLong()); + } + } + } + + private static long pack(int x, int y, int z) { + return ((long) (x & 0x1FFFFF) << 42) | ((long) (y & 0x1FFFFF) << 21) | (z & 0x1FFFFF); + } + + private static void referenceCells(RNG rng, int size, IrisDepositGenerator.CellSink sink) { + float angle = rng.nextFloat() * (float) Math.PI; + float reach = size / 8F; + double startX = Math.sin(angle) * reach; + double endX = -Math.sin(angle) * reach; + double startZ = Math.cos(angle) * reach; + double endZ = -Math.cos(angle) * reach; + double startY = rng.nextInt(3) - 2; + double endY = rng.nextInt(3) - 2; + double[] nodes = new double[size * 4]; + + for (int i = 0; i < size; i++) { + float progress = (float) i / size; + double radiusNoise = rng.nextDouble() * size / 16D; + nodes[i * 4] = startX + (endX - startX) * progress; + nodes[i * 4 + 1] = startY + (endY - startY) * progress; + nodes[i * 4 + 2] = startZ + (endZ - startZ) * progress; + nodes[i * 4 + 3] = ((Math.sin(Math.PI * progress) + 1D) * radiusNoise + 1D) / 2D; + } + + for (int i = 0; i < size - 1; i++) { + if (nodes[i * 4 + 3] <= 0D) { + continue; + } + for (int j = i + 1; j < size; j++) { + if (nodes[j * 4 + 3] <= 0D) { + continue; + } + double dx = nodes[i * 4] - nodes[j * 4]; + double dy = nodes[i * 4 + 1] - nodes[j * 4 + 1]; + double dz = nodes[i * 4 + 2] - nodes[j * 4 + 2]; + double dr = nodes[i * 4 + 3] - nodes[j * 4 + 3]; + if (dr * dr <= dx * dx + dy * dy + dz * dz) { + continue; + } + if (dr > 0D) { + nodes[j * 4 + 3] = -1D; + } else { + nodes[i * 4 + 3] = -1D; + } + } + } + + for (int i = 0; i < size; i++) { + double radius = nodes[i * 4 + 3]; + if (radius < 0D) { + continue; + } + double centerX = nodes[i * 4]; + double centerY = nodes[i * 4 + 1]; + double centerZ = nodes[i * 4 + 2]; + int minX = (int) Math.floor(centerX - radius); + int maxX = Math.max((int) Math.floor(centerX + radius), minX); + int minY = (int) Math.floor(centerY - radius); + int maxY = Math.max((int) Math.floor(centerY + radius), minY); + int minZ = (int) Math.floor(centerZ - radius); + int maxZ = Math.max((int) Math.floor(centerZ + radius), minZ); + + for (int x = minX; x <= maxX; x++) { + double nx = (x + 0.5D - centerX) / radius; + if (nx * nx >= 1D) { + continue; + } + for (int y = minY; y <= maxY; y++) { + double ny = (y + 0.5D - centerY) / radius; + if (nx * nx + ny * ny >= 1D) { + continue; + } + for (int z = minZ; z <= maxZ; z++) { + double nz = (z + 0.5D - centerZ) / radius; + if (nx * nx + ny * ny + nz * nz < 1D) { + sink.add(x, y, z); + } + } + } + } + } + } +} From 7ac5ffdb81e0d668719a6390f1f561c617bc107e Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Mon, 28 Sep 2026 15:54:23 -0400 Subject: [PATCH 2/2] Per-chunk hydrology samples - terrain painter reads the chunk's column array once instead of 256 cache lookups - test pins chunk samples against per-column lookups on a real plan --- .../hydrology/HydrologyTileCache.java | 8 ++++++ .../runtime/IrisHydrologyRuntime.java | 5 ++++ .../iris/generation/runtime/IrisComplex.java | 25 +++++++++++++++++ .../stage/IrisTerrainNormalActuator.java | 4 ++- .../hydrology/HydrologyTileCacheTest.java | 28 +++++++++++++++++++ 5 files changed, 69 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyTileCache.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyTileCache.java index ffccce3ab..8be2cf179 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyTileCache.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyTileCache.java @@ -537,6 +537,14 @@ public final class HydrologyTileCache implements AutoCloseable { chunkColumns(blockX, blockZ); } + /** + * Every column of the block's chunk, indexed {@code localZ * 16 + localX}; a column without hydrology + * is null. The array is shared with the cache and must not be modified. + */ + public HydrologyColumnSample[] chunkSamples(int blockX, int blockZ) { + return chunkColumns(blockX, blockZ).columns(); + } + /** * Whether the column's chunk can be answered without planning: every tile it composes from is * already planned. A caller that must not wait (the server thread answering a height or biome diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/runtime/IrisHydrologyRuntime.java b/core/src/main/java/art/arcane/iris/generation/hydrology/runtime/IrisHydrologyRuntime.java index 2cff804f9..789b63b75 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/runtime/IrisHydrologyRuntime.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/runtime/IrisHydrologyRuntime.java @@ -195,6 +195,11 @@ public final class IrisHydrologyRuntime implements AutoCloseable { cache.prepareChunkColumns(blockX, blockZ); } + /** The block's chunk columns indexed {@code localZ * 16 + localX}; shared, never modified by callers. */ + public HydrologyColumnSample[] chunkSamples(int blockX, int blockZ) { + return cache.chunkSamples(blockX, blockZ); + } + /** Whether the chunk or one of its eight neighbours holds a planned footprint column. */ public boolean hasColumnsAround(int chunkX, int chunkZ) { return cache.hasColumnsAround(chunkX, chunkZ); 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 4c04aa714..286bf2a19 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 @@ -1062,6 +1062,31 @@ public class IrisComplex implements DataProvider { return transitionHydrologySample(sample, hydrologyWeight, transitionDisplacement); } + /** + * The raw hydrology columns of the chunk holding the block, for a caller that samples every column of + * one chunk through {@link #sampleHydrologyColumn(HydrologyColumnSample[], int, int)}; null without + * hydrology. + */ + public HydrologyColumnSample[] hydrologyChunkSamples(int x, int z) { + return hydrologyRuntime == null ? null : hydrologyRuntime.chunkSamples(x, z); + } + + /** {@link #sampleHydrologyColumn(double, double)} for a column of the chunk {@code chunkSamples} came from. */ + public HydrologyColumnSample sampleHydrologyColumn(HydrologyColumnSample[] chunkSamples, int x, int z) { + if (chunkSamples == null) { + return null; + } + HydrologyColumnSample sample = chunkSamples[(z & 15) << 4 | (x & 15)]; + if (transitionGenerationPlan == null) { + return sample; + } + double hydrologyWeight = transitionHydrologyWeight(x, z); + if (sample == null) { + return sample; + } + return transitionHydrologySample(sample, hydrologyWeight, transitionDisplacement); + } + private HydrologyColumnSnapshot sampleHydrologySnapshot(int x, int z) { if (hydrologyRuntime == null) { return HydrologyColumnSnapshot.ready(null); diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuator.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuator.java index 9e5bf6754..7da845d0d 100644 --- a/core/src/main/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuator.java +++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuator.java @@ -154,6 +154,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator riverWaterSurfaceStream; + private final HydrologyColumnSample[] hydrologySamples; private final boolean bedrockEnabled; private final boolean hideOres; private final boolean transitionDisplaced; @@ -186,6 +187,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator 0); + } + @Test public void singleColumnContributionReusesItsImmutableSample() { HydrologyColumnSample sample = compositionSample(90, List.of(compositionLayer(11L, 80)));