From 534c844a7da9836dfa1c907468e200ca2969c1f2 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Sun, 6 Sep 2026 03:24:46 -0400 Subject: [PATCH] Compacted biome selection - Biome and carving children share compact rarity selection - Weighted choices preserve alternating insertion order - Rarity totals use long counts without expanded lists - Natural child selection preserves its existing noise calls - Differential tests cover normal and extreme rarity values --- .../art/arcane/iris/engine/IrisComplex.java | 70 ++------- .../object/IrisDimensionCarvingResolver.java | 87 +---------- .../engine/object/IrisRaritySelection.java | 60 ++++++++ .../engine/IrisComplexImplodeParityTest.java | 25 ++++ ...risDimensionCarvingResolverParityTest.java | 30 ++++ .../object/IrisRaritySelectionTest.java | 141 ++++++++++++++++++ 6 files changed, 273 insertions(+), 140 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/engine/object/IrisRaritySelection.java create mode 100644 core/src/test/java/art/arcane/iris/engine/object/IrisRaritySelectionTest.java 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 3e5565b6c..6784ec9e3 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisComplex.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisComplex.java @@ -37,6 +37,7 @@ import art.arcane.iris.engine.image.IrisImageMapRuntime; import art.arcane.iris.engine.mantle.components.MantleHydrologyCaveVoxelView; import art.arcane.iris.engine.object.InferredType; import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisRaritySelection; import art.arcane.iris.engine.object.IrisDecorationPart; import art.arcane.iris.engine.object.IrisDecorator; import art.arcane.iris.engine.object.IrisGenerator; @@ -1708,74 +1709,25 @@ public class IrisComplex implements DataProvider { } static final class ChildSelectionPlan { - private final IrisBiome[] mappedBiomes; - private final int maxIndex; + private final IrisRaritySelection selection; - private ChildSelectionPlan(IrisBiome[] mappedBiomes) { - this.mappedBiomes = mappedBiomes; - this.maxIndex = mappedBiomes.length - 1; + private ChildSelectionPlan(IrisRaritySelection selection) { + this.selection = selection; } static ChildSelectionPlan create(KList options) { - if (options.isEmpty()) { - return new ChildSelectionPlan(new IrisBiome[0]); - } - - int maxRarity = 1; - for (IrisBiome biome : options) { - if (biome != null && biome.getRarity() > maxRarity) { - maxRarity = biome.getRarity(); - } - } - - int rarityMax = maxRarity + 1; - boolean flip = false; - KList mapped = new KList<>(); - for (IrisBiome biome : options) { - if (biome == null) { - continue; - } - - int rarity = Math.max(1, biome.getRarity()); - int count = rarityMax - rarity; - for (int index = 0; index < count; index++) { - flip = !flip; - if (flip) { - mapped.add(biome); - } else { - mapped.add(0, biome); - } - } - } - - if (mapped.isEmpty()) { - IrisBiome[] fallback = new IrisBiome[]{options.get(0)}; - return new ChildSelectionPlan(fallback); - } - - IrisBiome[] mappedBiomes = mapped.toArray(new IrisBiome[0]); - return new ChildSelectionPlan(mappedBiomes); + return new ChildSelectionPlan(IrisRaritySelection.create(options)); } IrisBiome select(CNG childCell, double x, double z) { - if (mappedBiomes.length == 0) { - return null; + long maximumIndex = selection.size() - 1L; + if (maximumIndex <= 0L) { + return selection.get(0L); } - - if (mappedBiomes.length == 1) { - return mappedBiomes[0]; + if (maximumIndex <= Integer.MAX_VALUE) { + return selection.get(childCell.fit2D(0, (int) maximumIndex, x, z)); } - - int selectedIndex = childCell.fit2D(0, maxIndex, x, z); - if (selectedIndex < 0) { - return mappedBiomes[0]; - } - - if (selectedIndex > maxIndex) { - return mappedBiomes[maxIndex]; - } - - return mappedBiomes[selectedIndex]; + return selection.select(childCell.noiseFast2D(x, z)); } } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolver.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolver.java index 6a4c2aa79..65ecaa3d8 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolver.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolver.java @@ -2,7 +2,6 @@ package art.arcane.iris.engine.object; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.util.project.interpolation.IrisInterpolation; import art.arcane.volmlib.util.collection.KList; import art.arcane.iris.util.project.noise.CNG; @@ -149,21 +148,20 @@ public final class IrisDimensionCarvingResolver { return parent; } - ParentSelectionPlan selectionPlan = state.selectionPlans.get(parent); + IrisRaritySelection selectionPlan = state.selectionPlans.get(parent); if (selectionPlan == null) { selectionPlan = buildSelectionPlan(engine, parent, parentBiome, entryIndex, state); state.selectionPlans.put(parent, selectionPlan); } - if (selectionPlan.parentOnly) { + if (selectionPlan.size() <= 1L) { return parent; } long seed = resolveChildSeed(engine, state); CNG childGenerator = parent.getChildrenGenerator(seed, engine.getData()); double sample = childGenerator.noiseFast2D(worldX, worldZ); - int selectedIndex = (int) Math.round(IrisInterpolation.lerp(0, selectionPlan.maxIndex, sample)); - CarvingChoice selected = selectionPlan.get(selectedIndex); + CarvingChoice selected = selectionPlan.select(sample); if (selected == null || selected.entry == null) { return parent; } @@ -171,7 +169,7 @@ public final class IrisDimensionCarvingResolver { return selected.entry; } - private static ParentSelectionPlan buildSelectionPlan( + private static IrisRaritySelection buildSelectionPlan( Engine engine, IrisDimensionCarvingEntry parent, IrisBiome parentBiome, @@ -201,42 +199,7 @@ public final class IrisDimensionCarvingResolver { } options.add(new CarvingChoice(parent, rarity(parentBiome))); - if (options.size() <= 1) { - return ParentSelectionPlan.parentOnly(); - } - - CarvingChoice[] mappedChoices = buildRarityMappedChoices(options); - if (mappedChoices.length == 0) { - return ParentSelectionPlan.parentOnly(); - } - - return new ParentSelectionPlan(mappedChoices); - } - - private static CarvingChoice[] buildRarityMappedChoices(List choices) { - int max = 1; - for (CarvingChoice choice : choices) { - if (choice.rarity > max) { - max = choice.rarity; - } - } - - max++; - List mapped = new ArrayList<>(); - boolean flip = false; - for (CarvingChoice choice : choices) { - int count = max - choice.rarity; - for (int index = 0; index < count; index++) { - flip = !flip; - if (flip) { - mapped.add(choice); - } else { - mapped.add(0, choice); - } - } - } - - return mapped.toArray(new CarvingChoice[0]); + return IrisRaritySelection.create(options); } private static int rarity(IrisBiome biome) { @@ -285,7 +248,7 @@ public final class IrisDimensionCarvingResolver { public static final class State { private final Map rootEntriesByWorldY = new HashMap<>(); - private final Map selectionPlans = new IdentityHashMap<>(); + private final Map> selectionPlans = new IdentityHashMap<>(); private final Map biomeCache = new IdentityHashMap<>(); private WeakReference engineIdentity; private WeakReference dimensionIdentity; @@ -316,44 +279,6 @@ public final class IrisDimensionCarvingResolver { } } - private static final class ParentSelectionPlan { - private final CarvingChoice[] mappedChoices; - private final int maxIndex; - private final boolean parentOnly; - - private ParentSelectionPlan(CarvingChoice[] mappedChoices) { - this.mappedChoices = mappedChoices; - this.maxIndex = mappedChoices.length - 1; - this.parentOnly = false; - } - - private ParentSelectionPlan() { - this.mappedChoices = null; - this.maxIndex = -1; - this.parentOnly = true; - } - - private static ParentSelectionPlan parentOnly() { - return new ParentSelectionPlan(); - } - - private CarvingChoice get(int index) { - if (mappedChoices == null || mappedChoices.length == 0) { - return null; - } - - if (index < 0) { - return mappedChoices[0]; - } - - if (index >= mappedChoices.length) { - return mappedChoices[mappedChoices.length - 1]; - } - - return mappedChoices[index]; - } - } - private static final class CarvingChoice implements IRare { private final IrisDimensionCarvingEntry entry; private final int rarity; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRaritySelection.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRaritySelection.java new file mode 100644 index 000000000..967a05b59 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisRaritySelection.java @@ -0,0 +1,60 @@ +package art.arcane.iris.engine.object; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public final class IrisRaritySelection { + private final List choices; + private final long[] cumulativeCounts; + + private IrisRaritySelection(List choices, long[] cumulativeCounts) { + this.choices = choices; + this.cumulativeCounts = cumulativeCounts; + } + + public static IrisRaritySelection create(List options) { + ArrayList choices = new ArrayList<>(options.size()); + long maximumRarity = 1L; + for (T option : options) { + if (option != null) { + choices.add(option); + maximumRarity = Math.max(maximumRarity, option.getRarity()); + } + } + long[] cumulativeCounts = new long[choices.size()]; + long total = 0L; + for (int index = 0; index < choices.size(); index++) { + total += maximumRarity + 1L - Math.max(1, choices.get(index).getRarity()); + cumulativeCounts[index] = total; + } + return new IrisRaritySelection<>(List.copyOf(choices), cumulativeCounts); + } + + public long size() { + return cumulativeCounts.length == 0 ? 0L : cumulativeCounts[cumulativeCounts.length - 1]; + } + + public T select(double noiseValue) { + long maximumIndex = size() - 1L; + long selectedIndex = Math.round(noiseValue * maximumIndex); + return get(maximumIndex <= Integer.MAX_VALUE ? (int) selectedIndex : selectedIndex); + } + + public T get(long index) { + long size = size(); + if (size == 0L) { + return null; + } + if (choices.size() == 1) { + return choices.getFirst(); + } + long selectedIndex = Math.max(0L, Math.min(size - 1L, index)); + long frontSize = size / 2L; + long insertionIndex = selectedIndex < frontSize + ? 2L * (frontSize - selectedIndex) - 1L + : 2L * (selectedIndex - frontSize); + int choiceIndex = Arrays.binarySearch(cumulativeCounts, insertionIndex + 1L); + return choices.get(choiceIndex < 0 ? -choiceIndex - 1 : choiceIndex); + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/IrisComplexImplodeParityTest.java b/core/src/test/java/art/arcane/iris/engine/IrisComplexImplodeParityTest.java index 7c22081d0..4b7f3828a 100644 --- a/core/src/test/java/art/arcane/iris/engine/IrisComplexImplodeParityTest.java +++ b/core/src/test/java/art/arcane/iris/engine/IrisComplexImplodeParityTest.java @@ -13,8 +13,12 @@ import java.util.List; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; public class IrisComplexImplodeParityTest { private static Method childSelectionCreateMethod; @@ -48,6 +52,27 @@ public class IrisComplexImplodeParityTest { } } + @Test + public void extremeCountsKeepTheRareBiomeReachableAcrossTheIntIndexBoundary() { + IrisBiome rare = createBiome(Integer.MAX_VALUE); + IrisBiome common = createBiome(1); + CNG generator = mock(CNG.class); + doReturn(1_073_741_824).when(generator).fit2D(0, Integer.MAX_VALUE, 12D, -32D); + IrisComplex.ChildSelectionPlan pair = IrisComplex.ChildSelectionPlan.create(new KList<>(rare, common)); + + assertSame(rare, pair.select(generator, 12D, -32D)); + verify(generator).fit2D(0, Integer.MAX_VALUE, 12D, -32D); + verify(generator, never()).noiseFast2D(anyDouble(), anyDouble()); + + CNG largeGenerator = mock(CNG.class); + doReturn(0.5D).when(largeGenerator).noiseFast2D(12D, -32D); + IrisComplex.ChildSelectionPlan large = IrisComplex.ChildSelectionPlan.create( + new KList<>(rare, common, createBiome(2))); + assertSame(rare, large.select(largeGenerator, 12D, -32D)); + verify(largeGenerator).noiseFast2D(12D, -32D); + verify(largeGenerator, never()).fit2D(anyInt(), anyInt(), anyDouble(), anyDouble()); + } + @Test public void emptySelectionPlanMatchesLegacyEmptyBehavior() throws Exception { KList options = new KList<>(); diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolverParityTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolverParityTest.java index e7217db79..edb1e2fa6 100644 --- a/core/src/test/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolverParityTest.java +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolverParityTest.java @@ -35,11 +35,16 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.Answers.CALLS_REAL_METHODS; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyDouble; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; public class IrisDimensionCarvingResolverParityTest { private static final int MAX_CHILD_DEPTH = 32; @@ -71,6 +76,31 @@ public class IrisDimensionCarvingResolverParityTest { return data; } + @Test + public void extremeChildRarityRemainsReachableWithOneNoiseSample() { + Fixture fixture = createFixture(); + Engine engine = fixture.engine; + IrisData data = engine.getData(); + IrisBiome rare = data.getBiomeLoader().load("child-a"); + IrisBiome common = data.getBiomeLoader().load("child-b"); + IrisBiome parentBiome = data.getBiomeLoader().load("root-low"); + doReturn(Integer.MAX_VALUE).when(rare).getRarity(); + doReturn(1).when(common).getRarity(); + doReturn(2).when(parentBiome).getRarity(); + IrisDimensionCarvingEntry parent = spy(engine.getDimension().getCarvingEntryIndex().get("root-low")); + parent.setChildRecursionDepth(1); + CNG generator = mock(CNG.class); + doReturn(0.5D).when(generator).noiseFast2D(-17D, 23D); + doReturn(generator).when(parent).getChildrenGenerator(anyLong(), any(IrisData.class)); + + IrisDimensionCarvingEntry selected = IrisDimensionCarvingResolver.resolveFromRoot( + engine, parent, -17, 23, new IrisDimensionCarvingResolver.State()); + + assertSame(engine.getDimension().getCarvingEntryIndex().get("child-a"), selected); + verify(generator).noiseFast2D(-17D, 23D); + verify(generator, never()).fit2D(anyInt(), anyInt(), anyDouble(), anyDouble()); + } + @Test public void resolverStatefulOverloadsMatchLegacyResolverAcrossSampleGrid() { Fixture fixture = createFixture(); diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisRaritySelectionTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisRaritySelectionTest.java new file mode 100644 index 000000000..a273533c3 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisRaritySelectionTest.java @@ -0,0 +1,141 @@ +package art.arcane.iris.engine.object; + +import com.google.gson.Gson; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +public class IrisRaritySelectionTest { + @Test + public void everyIndexMatchesExpandedInsertionOrderForRandomizedChoices() { + Random random = new Random(718938L); + for (int scenario = 0; scenario < 500; scenario++) { + ArrayList choices = new ArrayList<>(); + int count = random.nextInt(12); + for (int index = 0; index < count; index++) { + choices.add(random.nextInt(7) == 0 ? null : new Choice(index, random.nextInt(35) - 3)); + } + List expanded = expand(choices); + IrisRaritySelection selection = IrisRaritySelection.create(choices); + assertEquals(expanded.size(), selection.size()); + if (expanded.isEmpty()) { + assertNull(selection.get(0)); + continue; + } + for (int index = 0; index < expanded.size(); index++) { + assertSame(expanded.get(index), selection.get(index)); + } + assertSame(expanded.getFirst(), selection.get(Long.MIN_VALUE)); + assertSame(expanded.getLast(), selection.get(Long.MAX_VALUE)); + for (double sample : new double[]{-3D, -0D, 0.125D, 0.5D, 0.875D, 1D, 3D, + Double.NaN, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY}) { + int index = (int) Math.round(sample * (expanded.size() - 1)); + assertSame(expanded.get(Math.max(0, Math.min(expanded.size() - 1, index))), selection.select(sample)); + } + } + } + + @Test(timeout = 1000) + public void maximumRarityCountsStayPositiveWithoutExpandingBillionsOfEntries() { + Choice rare = new Choice(0, Integer.MAX_VALUE); + Choice common = new Choice(1, 1); + Choice next = new Choice(2, 2); + IrisRaritySelection selection = IrisRaritySelection.create(List.of(rare, common, next)); + + assertEquals(4_294_967_294L, selection.size()); + assertSame(next, selection.get(0L)); + assertSame(common, selection.get(2_147_483_646L)); + assertSame(rare, selection.get(2_147_483_647L)); + assertSame(common, selection.get(2_147_483_648L)); + assertSame(next, selection.get(selection.size() - 1L)); + assertSame(rare, selection.select(0.5D)); + assertSame(next, selection.select(0D)); + assertSame(next, selection.select(1D)); + } + + @Test + public void equalMaximumRaritiesRetainEveryChoice() { + Choice first = new Choice(0, Integer.MAX_VALUE); + Choice second = new Choice(1, Integer.MAX_VALUE); + Choice third = new Choice(2, Integer.MAX_VALUE); + IrisRaritySelection selection = IrisRaritySelection.create(List.of(first, second, third)); + + assertEquals(3L, selection.size()); + assertSame(second, selection.get(0L)); + assertSame(first, selection.get(1L)); + assertSame(third, selection.get(2L)); + } + + @Test + public void duplicateChoicesKeepTheirSeparateInsertionPositions() { + Choice repeated = new Choice(0, 1); + Choice other = new Choice(1, 3); + List choices = List.of(repeated, other, repeated); + IrisRaritySelection selection = IrisRaritySelection.create(choices); + List expanded = expand(choices); + for (int index = 0; index < expanded.size(); index++) { + assertSame(expanded.get(index), selection.get(index)); + } + } + + @Test + public void deserializedRuntimeRarityValuesUseTheExistingMinimumClamp() { + Gson gson = new Gson(); + IrisBiome zero = gson.fromJson("{\"rarity\":0}", IrisBiome.class); + IrisBiome negative = gson.fromJson("{\"rarity\":-2147483648}", IrisBiome.class); + IrisBiome maximum = gson.fromJson("{\"rarity\":2147483647}", IrisBiome.class); + IrisRaritySelection selection = IrisRaritySelection.create(List.of(zero, negative, maximum)); + + assertEquals(4_294_967_295L, selection.size()); + assertSame(zero, selection.get(2_147_483_647L)); + assertSame(maximum, selection.get(selection.size() - 1L)); + assertEquals(0, zero.getRarity()); + assertEquals(Integer.MIN_VALUE, negative.getRarity()); + } + + @Test + public void nullAndEmptyOptionsDoNotProduceCandidates() { + assertNull(IrisRaritySelection.create(List.of()).select(0.5D)); + assertNull(IrisRaritySelection.create(Arrays.asList(null, null)).get(0L)); + } + + private static List expand(List choices) { + int maximum = 1; + for (Choice choice : choices) { + if (choice != null) { + maximum = Math.max(maximum, choice.rarity()); + } + } + ArrayList expanded = new ArrayList<>(); + boolean append = false; + for (Choice choice : choices) { + if (choice == null) { + continue; + } + int count = maximum + 1 - Math.max(1, choice.rarity()); + for (int index = 0; index < count; index++) { + append = !append; + if (append) { + expanded.add(choice); + } else { + expanded.add(0, choice); + } + } + } + return expanded; + } + + private record Choice(int id, int rarity) implements IRare { + @Override + public int getRarity() { + return rarity; + } + } +}