diff --git a/core/src/main/java/art/arcane/iris/engine/actuator/IrisTerrainNormalActuator.java b/core/src/main/java/art/arcane/iris/engine/actuator/IrisTerrainNormalActuator.java index 6d31bff52..67b5cb45b 100644 --- a/core/src/main/java/art/arcane/iris/engine/actuator/IrisTerrainNormalActuator.java +++ b/core/src/main/java/art/arcane/iris/engine/actuator/IrisTerrainNormalActuator.java @@ -22,6 +22,7 @@ import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.EngineAssignedActuator; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.util.project.stream.ProceduralStream; import art.arcane.iris.engine.UpperDimensionContext; import art.arcane.iris.engine.hydrology.HydrologyColumnLayer; import art.arcane.iris.engine.hydrology.HydrologyColumnSample; @@ -84,6 +85,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator riverWaterSurfaceStream = complex.getRiverWaterSurfaceStream(); RNG localRng = rng; boolean bedrockEnabled = dimension.isBedrock(); boolean hideOres = dimension.isHideOresForHiddenOre(); @@ -117,7 +119,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator primarySurfaceLayer() { - return selectSurfaceLayer(false); + return Optional.ofNullable(selectSurfaceLayer(false)); } public Optional primarySurfaceFluidLayer() { + return Optional.ofNullable(selectSurfaceLayer(true)); + } + + public HydrologyColumnLayer primarySurfaceLayerOrNull() { + return selectSurfaceLayer(false); + } + + public HydrologyColumnLayer primarySurfaceFluidLayerOrNull() { return selectSurfaceLayer(true); } @@ -120,11 +128,10 @@ public record HydrologyColumnSample( } public int terrainHeight() { - Optional layer = primarySurfaceLayer(); - if (layer.isEmpty()) { + HydrologyColumnLayer primary = selectSurfaceLayer(false); + if (primary == null) { return naturalHeight; } - HydrologyColumnLayer primary = layer.get(); if (primary.channel()) { return primary.bedY(); } @@ -147,7 +154,7 @@ public record HydrologyColumnSample( return new HydrologyRenderSample(x, z, features); } - private Optional selectSurfaceLayer(boolean fluidOnly) { + private HydrologyColumnLayer selectSurfaceLayer(boolean fluidOnly) { HydrologyColumnLayer selected = null; for (HydrologyColumnLayer layer : layers) { if (layer.oceanApron() || !layer.feature().type().isSurface()) { @@ -164,7 +171,7 @@ public record HydrologyColumnSample( selected = layer; } } - return Optional.ofNullable(selected); + return selected; } private static int surfaceRolePriority(HydrologyColumnLayer layer) { diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java b/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java index 8c8c5b7b4..1c3a92817 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java @@ -23,6 +23,7 @@ import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.util.project.matter.TileWrapper; import art.arcane.iris.util.project.matter.PreObjectMatterCell; import com.google.common.collect.ImmutableList; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.link.Identifier; import art.arcane.iris.core.tools.WorldMaintenance; @@ -175,7 +176,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { } } - private static Set getBallooned(Set vset, double radius) { + static Set getBallooned(Set vset, double radius) { Set returnset = new HashSet<>(); int ceilrad = (int) Math.ceil(radius); double r2 = Math.pow(radius, 2); @@ -198,30 +199,83 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { return returnset; } - private static Set getHollowed(Set vset) { + static Set getHollowed(Set vset) { + PositionLookup lookup = PositionLookup.of(vset); Set returnset = new KSet<>(); for (IrisPosition v : vset) { - double x = v.getX(); - double y = v.getY(); - double z = v.getZ(); - if (!(vset.contains(new IrisPosition(x + 1, y, z)) - && vset.contains(new IrisPosition(x - 1, y, z)) - && vset.contains(new IrisPosition(x, y + 1, z)) - && vset.contains(new IrisPosition(x, y - 1, z)) - && vset.contains(new IrisPosition(x, y, z + 1)) - && vset.contains(new IrisPosition(x, y, z - 1)))) { + int x = v.getX(); + int y = v.getY(); + int z = v.getZ(); + if (!(lookup.contains(x + 1, y, z) + && lookup.contains(x - 1, y, z) + && lookup.contains(x, y + 1, z) + && lookup.contains(x, y - 1, z) + && lookup.contains(x, y, z + 1) + && lookup.contains(x, y, z - 1))) { returnset.add(v); } } return returnset; } - private static double hypot(double... pars) { - double sum = 0; - for (double d : pars) { - sum += Math.pow(d, 2); + private static double hypot(double x, double y, double z) { + return Math.pow(x, 2) + Math.pow(y, 2) + Math.pow(z, 2); + } + + record PositionLookup(LongOpenHashSet keys, Set positions, int originX, int originY, int originZ) { + private static final int AXIS_BITS = 21; + private static final long AXIS_LIMIT = 1L << AXIS_BITS; + + static PositionLookup of(Set positions) { + if (positions.isEmpty()) { + return new PositionLookup(new LongOpenHashSet(), positions, 0, 0, 0); + } + + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + int minZ = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxY = Integer.MIN_VALUE; + int maxZ = Integer.MIN_VALUE; + for (IrisPosition position : positions) { + minX = Math.min(minX, position.getX()); + minY = Math.min(minY, position.getY()); + minZ = Math.min(minZ, position.getZ()); + maxX = Math.max(maxX, position.getX()); + maxY = Math.max(maxY, position.getY()); + maxZ = Math.max(maxZ, position.getZ()); + } + + if ((long) maxX - minX >= AXIS_LIMIT - 2L + || (long) maxY - minY >= AXIS_LIMIT - 2L + || (long) maxZ - minZ >= AXIS_LIMIT - 2L) { + return new PositionLookup(null, positions, 0, 0, 0); + } + + LongOpenHashSet keys = new LongOpenHashSet(positions.size()); + for (IrisPosition position : positions) { + keys.add(key(position.getX() - minX, position.getY() - minY, position.getZ() - minZ)); + } + return new PositionLookup(keys, positions, minX, minY, minZ); + } + + boolean contains(int x, int y, int z) { + if (keys == null) { + return positions.contains(new IrisPosition(x, y, z)); + } + + long dx = (long) x - originX; + long dy = (long) y - originY; + long dz = (long) z - originZ; + if ((dx | dy | dz) < 0L || dx >= AXIS_LIMIT || dy >= AXIS_LIMIT || dz >= AXIS_LIMIT) { + return false; + } + return keys.contains(key(dx, dy, dz)); + } + + private static long key(long x, long y, long z) { + return (x << (AXIS_BITS * 2)) | (y << AXIS_BITS) | z; } - return sum; } private static double lengthSq(double x, double y, double z) { @@ -294,9 +348,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { if (hasProtectedHydrology(matter, x, y, z)) { return false; } - MatterSlice cavernSlice = matter.hasSlice(MatterCavern.class) - ? matter.getSlice(MatterCavern.class) - : null; + MatterSlice cavernSlice = matter.getSlice(MatterCavern.class); MatterCavern existing = cavernSlice == null ? null : cavernSlice.get(x & 15, y & 15, z & 15); @@ -328,15 +380,13 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { if (hasProtectedHydrology(matter, x, y, z)) { return false; } - if (matter.hasSlice(PlatformBlockState.class)) { + MatterSlice blockSlice = matter.getSlice(PlatformBlockState.class); + if (blockSlice != null) { capturePreObjectOriginal(matter, x, y, z, PlatformBlockState.class); - matter.getSlice(PlatformBlockState.class) - .set(x & 15, y & 15, z & 15, null); + blockSlice.set(x & 15, y & 15, z & 15, null); } clearDeferredPlacement(matter, x, y, z); - MatterSlice cavernSlice = matter.hasSlice(MatterCavern.class) - ? matter.getSlice(MatterCavern.class) - : null; + MatterSlice cavernSlice = matter.getSlice(MatterCavern.class); if (cavernSlice != null && cavernSlice.get(x & 15, y & 15, z & 15) != null) { return false; } @@ -363,10 +413,10 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { if (hasProtectedHydrology(matter, x, y, z)) { return; } - if (matter.hasSlice(PlatformBlockState.class)) { + MatterSlice blockSlice = matter.getSlice(PlatformBlockState.class); + if (blockSlice != null) { capturePreObjectOriginal(matter, x, y, z, PlatformBlockState.class); - matter.getSlice(PlatformBlockState.class) - .set(x & 15, y & 15, z & 15, null); + blockSlice.set(x & 15, y & 15, z & 15, null); } clearDeferredPlacement(matter, x, y, z); capturePreObjectOriginal(matter, x, y, z, MatterCavern.class); @@ -394,10 +444,10 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { if (hasProtectedHydrology(matter, x, y, z)) { return; } - if (matter.hasSlice(PlatformBlockState.class)) { + MatterSlice blockSlice = matter.getSlice(PlatformBlockState.class); + if (blockSlice != null) { capturePreObjectOriginal(matter, x, y, z, PlatformBlockState.class); - matter.getSlice(PlatformBlockState.class) - .set(x & 15, y & 15, z & 15, null); + blockSlice.set(x & 15, y & 15, z & 15, null); } clearDeferredPlacement(matter, x, y, z); } @@ -651,17 +701,18 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { } private static boolean hasProtectedHydrology(Matter matter, int x, int y, int z) { - if (!matter.hasSlice(HydrologyCaveCell.class)) { + MatterSlice slice = matter.getSlice(HydrologyCaveCell.class); + if (slice == null) { return false; } - HydrologyCaveCell hydrology = matter.getSlice(HydrologyCaveCell.class) - .get(x & 15, y & 15, z & 15); + HydrologyCaveCell hydrology = slice.get(x & 15, y & 15, z & 15); return hydrology != null && hydrology.protectsPlacement(); } private static void clearDeferredPlacement(Matter matter, int x, int y, int z) { - if (matter.hasSlice(Identifier.class)) { - matter.getSlice(Identifier.class).set(x & 15, y & 15, z & 15, null); + MatterSlice slice = matter.getSlice(Identifier.class); + if (slice != null) { + slice.set(x & 15, y & 15, z & 15, null); } } @@ -728,11 +779,11 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { } private static PreObjectMatterCell preObjectCell(Matter matter, int x, int y, int z) { - if (matter == null || !matter.hasSlice(PreObjectMatterCell.class)) { + if (matter == null) { return null; } - return matter.getSlice(PreObjectMatterCell.class) - .get(x & 15, y & 15, z & 15); + MatterSlice slice = matter.getSlice(PreObjectMatterCell.class); + return slice == null ? null : slice.get(x & 15, y & 15, z & 15); } private static T prerequisiteValue(Matter matter, int x, int y, int z, Class type) { @@ -740,17 +791,13 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { if (cell != null && cell.captures(type)) { return cell.original(type); } - if (!matter.hasSlice(type)) { - return null; - } - return matter.getSlice(type).get(x & 15, y & 15, z & 15); + MatterSlice slice = matter.getSlice(type); + return slice == null ? null : slice.get(x & 15, y & 15, z & 15); } private static Object rawValue(Matter matter, int x, int y, int z, Class type) { - if (!matter.hasSlice(type)) { - return null; - } - return matter.getSlice(type).get(x, y, z); + MatterSlice slice = matter.getSlice(type); + return slice == null ? null : slice.get(x, y, z); } private boolean allowsWrite(int blockX, int blockZ) { @@ -762,10 +809,10 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { private static void restoreRaw(Matter matter, int x, int y, int z, Class type, Object value) { MatterSlice slice; if (value == null) { - if (!matter.hasSlice(type)) { + slice = (MatterSlice) matter.getSlice(type); + if (slice == null) { return; } - slice = (MatterSlice) matter.getSlice(type); } else { slice = (MatterSlice) matter.slice(type); } @@ -951,9 +998,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { continue; } - MatterSlice cavernSlice = matter.hasSlice(MatterCavern.class) - ? matter.getSlice(MatterCavern.class) - : null; + MatterSlice cavernSlice = matter.getSlice(MatterCavern.class); MatterSlice hydrologySlice = matter.hasSlice(HydrologyCaveCell.class) ? matter.getSlice(HydrologyCaveCell.class) : null; @@ -1277,10 +1322,11 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { setConsumer(vset, data); } - private static Set getMasked(Set vectors, Set masks, double radius) { + static Set getMasked(Set vectors, Set masks, double radius) { Set vset = new KSet<>(); int ceil = (int) Math.ceil(radius); double r2 = Math.pow(radius, 2); + PositionLookup maskLookup = PositionLookup.of(masks); for (IrisPosition v : vectors) { int tipX = v.getX(); @@ -1290,7 +1336,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { for (int x = -ceil; x <= ceil; x++) { for (int y = -ceil; y <= ceil; y++) { for (int z = -ceil; z <= ceil; z++) { - if (hypot(x, y, z) > r2 || !masks.contains(new IrisPosition(x, y, z))) + if (hypot(x, y, z) > r2 || !maskLookup.contains(x, y, z)) continue; vset.add(new IrisPosition(tipX + x, tipY + y, tipZ + z)); } diff --git a/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyColumnSampleAccessorTest.java b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyColumnSampleAccessorTest.java new file mode 100644 index 000000000..23f1af2cc --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/hydrology/HydrologyColumnSampleAccessorTest.java @@ -0,0 +1,153 @@ +package art.arcane.iris.engine.hydrology; + +import org.junit.Assume; +import org.junit.Test; + +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class HydrologyColumnSampleAccessorTest { + private static final int WARMUP = 20_000; + private static final int ROUNDS = 20_000; + private static final int PASSES = 5; + + @Test + public void theNullableSurfaceAccessorsAgreeWithTheOptionalOnes() { + HydrologyColumnSample sample = sample(); + + assertSame(sample.primarySurfaceLayer().orElse(null), sample.primarySurfaceLayerOrNull()); + assertSame(sample.primarySurfaceFluidLayer().orElse(null), sample.primarySurfaceFluidLayerOrNull()); + assertNotNull(sample.primarySurfaceLayerOrNull()); + assertNotNull(sample.primarySurfaceFluidLayerOrNull()); + } + + @Test + public void aColumnWithoutSurfaceLayersReportsNothingOnBothAccessors() { + HydrologyColumnSample empty = new HydrologyColumnSample(0, 0, 70, 62, false, "overworld/plains", List.of()); + + assertNull(empty.primarySurfaceLayerOrNull()); + assertNull(empty.primarySurfaceFluidLayerOrNull()); + assertTrue(empty.primarySurfaceLayer().isEmpty()); + assertTrue(empty.primarySurfaceFluidLayer().isEmpty()); + assertEquals(70, empty.terrainHeight()); + } + + @Test + public void terrainHeightStillFollowsThePrimarySurfaceLayer() { + HydrologyColumnSample sample = sample(); + + assertEquals(sample.primarySurfaceLayerOrNull().bedY(), sample.terrainHeight()); + } + + @Test + public void theNullableAccessorNeverAllocatesMoreThanTheOptionalOne() { + HydrologyColumnSample sample = sample(); + + Measurement nullable = best(() -> sample.primarySurfaceLayerOrNull()); + Measurement optional = best(() -> sample.primarySurfaceLayer().orElse(null)); + + System.out.println("[bench] primarySurfaceLayer nullable=" + nullable + " optional=" + optional); + + Assume.assumeTrue("per-thread allocation counters are unavailable on this JVM", nullable.bytes() >= 0L); + assertTrue("the nullable accessor allocated " + nullable.bytes() + + " bytes against " + optional.bytes() + " for the Optional accessor", + nullable.bytes() <= optional.bytes()); + } + + private static HydrologyColumnSample sample() { + List layers = new ArrayList<>(); + layers.add(layer(1L, HydrologyFeatureType.RIFFLE, 66, 68, true, false, true, true)); + layers.add(layer(2L, HydrologyFeatureType.SURFACE_POOL, 67, 68, false, true, false, false)); + layers.add(layer(3L, HydrologyFeatureType.MOUTH, 65, 67, true, false, true, true)); + return new HydrologyColumnSample(4, 9, 74, 62, false, "overworld/river", layers); + } + + private static HydrologyColumnLayer layer( + long id, + HydrologyFeatureType type, + int bedY, + int fluidHeadY, + boolean channel, + boolean shore, + boolean connectedFluid, + boolean fluidOwned + ) { + return new HydrologyColumnLayer( + new HydrologyFeatureRef(id, type, id, id, 4, fluidHeadY, 9, 1, 0, false), + bedY, + fluidHeadY, + fluidHeadY, + channel, + shore, + false, + connectedFluid, + false, + false, + true, + fluidOwned, + false, + "overworld/river", + "overworld/river-surface", + "overworld/river-mouth", + "overworld/river-shore", + "overworld/river-bank", + "overworld/river-cave"); + } + + private static Measurement best(Supplier work) { + for (int round = 0; round < WARMUP; round++) { + work.get(); + } + + long bytes = Long.MAX_VALUE; + long nanos = Long.MAX_VALUE; + for (int pass = 0; pass < PASSES; pass++) { + Measurement measured = measure(work); + if (measured.bytes() < 0L) { + return measured; + } + bytes = Math.min(bytes, measured.bytes()); + nanos = Math.min(nanos, measured.nanos()); + } + return new Measurement(bytes, nanos); + } + + private static Measurement measure(Supplier work) { + HydrologyColumnLayer sink = null; + long startBytes = allocatedBytes(); + long startNanos = System.nanoTime(); + for (int round = 0; round < ROUNDS; round++) { + sink = work.get(); + } + long elapsedNanos = System.nanoTime() - startNanos; + long endBytes = allocatedBytes(); + assertNotNull(sink); + long bytes = startBytes < 0L || endBytes < 0L ? -1L : (endBytes - startBytes) / ROUNDS; + + return new Measurement(bytes, elapsedNanos / ROUNDS); + } + + private static long allocatedBytes() { + ThreadMXBean threads = ManagementFactory.getThreadMXBean(); + if (!(threads instanceof com.sun.management.ThreadMXBean hotspot) || !hotspot.isThreadAllocatedMemoryEnabled()) { + return -1L; + } + return hotspot.getThreadAllocatedBytes(Thread.currentThread().threadId()); + } + + private record Measurement(long bytes, long nanos) { + @Override + public String toString() { + return "{bytes/op=" + bytes + ", ns/op=" + nanos + "}"; + } + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/MantleWriterNeighborhoodBenchmarkTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/MantleWriterNeighborhoodBenchmarkTest.java new file mode 100644 index 000000000..d7e5aea6f --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/mantle/MantleWriterNeighborhoodBenchmarkTest.java @@ -0,0 +1,125 @@ +package art.arcane.iris.engine.mantle; + +import art.arcane.iris.engine.object.IrisPosition; +import org.junit.Assume; +import org.junit.Test; + +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.function.Supplier; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class MantleWriterNeighborhoodBenchmarkTest { + private static final int WARMUP = 400; + private static final int ROUNDS = 40; + private static final int PASSES = 6; + private static final double RADIUS = 5.0D; + + @Test + public void hollowingAllocatesLessThanThePositionProbeImplementation() { + Set ballooned = MantleWriter.getBallooned(line(), RADIUS); + assertTrue("benchmark input is too small to be meaningful: " + ballooned.size(), ballooned.size() > 4_000); + + Comparison comparison = compare( + () -> MantleWriter.getHollowed(ballooned), + () -> MantleWriterNeighborhoodTest.referenceHollowed(ballooned)); + + System.out.println("[bench] getHollowed voxels=" + ballooned.size() + + " primitiveKeys=" + comparison.candidate() + + " positionProbes=" + comparison.reference()); + + assertEquals(comparison.reference().result(), comparison.candidate().result()); + + Assume.assumeTrue("per-thread allocation counters are unavailable on this JVM", + comparison.candidate().bytes() >= 0L); + assertTrue("primitive-keyed hollowing allocated " + comparison.candidate().bytes() + + " bytes against " + comparison.reference().bytes() + " for the position-probe implementation", + comparison.candidate().bytes() < comparison.reference().bytes()); + } + + @Test + public void ballooningMatchesTheVarargsDistanceImplementationExactly() { + Set seeds = line(); + + Comparison comparison = compare( + () -> MantleWriter.getBallooned(seeds, RADIUS), + () -> MantleWriterNeighborhoodTest.referenceBallooned(seeds, RADIUS)); + + System.out.println("[bench] getBallooned seeds=" + seeds.size() + + " fixedArityDistance=" + comparison.candidate() + + " varargsDistance=" + comparison.reference()); + + assertEquals(comparison.reference().result(), comparison.candidate().result()); + } + + private static Set line() { + Set positions = new LinkedHashSet<>(); + for (int step = 0; step <= 48; step++) { + positions.add(new IrisPosition(step, 60 + (step % 5), step / 2)); + } + return positions; + } + + private static Comparison compare(Supplier> candidate, Supplier> reference) { + for (int round = 0; round < WARMUP; round++) { + candidate.get(); + reference.get(); + } + + Measurement bestCandidate = null; + Measurement bestReference = null; + for (int pass = 0; pass < PASSES; pass++) { + bestCandidate = best(bestCandidate, measure(candidate)); + bestReference = best(bestReference, measure(reference)); + } + + return new Comparison(bestCandidate, bestReference); + } + + private static Measurement best(Measurement current, Measurement measured) { + if (current == null) { + return measured; + } + + return new Measurement( + measured.result(), + current.bytes() < 0L || measured.bytes() < 0L ? -1L : Math.min(current.bytes(), measured.bytes()), + Math.min(current.nanos(), measured.nanos())); + } + + private static Measurement measure(Supplier> work) { + Set result = null; + long startBytes = allocatedBytes(); + long startNanos = System.nanoTime(); + for (int round = 0; round < ROUNDS; round++) { + result = work.get(); + } + long elapsedNanos = System.nanoTime() - startNanos; + long endBytes = allocatedBytes(); + long bytes = startBytes < 0L || endBytes < 0L ? -1L : (endBytes - startBytes) / ROUNDS; + + return new Measurement(result, bytes, elapsedNanos / ROUNDS); + } + + private static long allocatedBytes() { + ThreadMXBean threads = ManagementFactory.getThreadMXBean(); + if (!(threads instanceof com.sun.management.ThreadMXBean hotspot) || !hotspot.isThreadAllocatedMemoryEnabled()) { + return -1L; + } + return hotspot.getThreadAllocatedBytes(Thread.currentThread().threadId()); + } + + private record Comparison(Measurement candidate, Measurement reference) { + } + + private record Measurement(Set result, long bytes, long nanos) { + @Override + public String toString() { + return "{bytes/op=" + bytes + ", ns/op=" + nanos + ", voxels=" + result.size() + "}"; + } + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/MantleWriterNeighborhoodTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/MantleWriterNeighborhoodTest.java new file mode 100644 index 000000000..c9b18dbfb --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/mantle/MantleWriterNeighborhoodTest.java @@ -0,0 +1,184 @@ +package art.arcane.iris.engine.mantle; + +import art.arcane.iris.engine.object.IrisPosition; +import org.junit.Test; + +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class MantleWriterNeighborhoodTest { + @Test + public void balloonedVoxelsMatchTheReferenceSphereForFractionalRadii() { + for (double radius : new double[]{0.0D, 0.5D, 1.0D, 1.5D, 2.0D, 3.7D, 5.0D}) { + Set seeds = line(-3, 60, 7, 4, 63, 9); + assertEquals("radius " + radius, referenceBallooned(seeds, radius), MantleWriter.getBallooned(seeds, radius)); + } + } + + @Test + public void hollowedVoxelsMatchTheReferenceSurfaceExtraction() { + Set ballooned = MantleWriter.getBallooned(line(0, 40, 0, 6, 44, 3), 4.0D); + + assertEquals(referenceHollowed(ballooned), MantleWriter.getHollowed(ballooned)); + } + + @Test + public void hollowingASolidBlockKeepsOnlyItsShell() { + Set solid = new LinkedHashSet<>(); + for (int x = 0; x < 5; x++) { + for (int y = 0; y < 5; y++) { + for (int z = 0; z < 5; z++) { + solid.add(new IrisPosition(x, y, z)); + } + } + } + + Set hollow = MantleWriter.getHollowed(solid); + + assertEquals(98, hollow.size()); + assertFalse(hollow.contains(new IrisPosition(2, 2, 2))); + assertTrue(hollow.contains(new IrisPosition(0, 2, 2))); + } + + @Test + public void aSetTooWideToPackFallsBackToPositionEqualityWithoutChangingTheResult() { + Set spread = new LinkedHashSet<>(); + spread.add(new IrisPosition(-20_000_000, 60, 0)); + spread.add(new IrisPosition(20_000_000, 60, 0)); + spread.add(new IrisPosition(20_000_001, 60, 0)); + + assertEquals(referenceHollowed(spread), MantleWriter.getHollowed(spread)); + assertEquals(spread, MantleWriter.getHollowed(spread)); + } + + @Test + public void maskedVoxelsMatchTheReferenceMaskExpansion() { + Set seeds = line(0, 70, 0, 3, 70, 4); + Set masks = new LinkedHashSet<>(); + masks.add(new IrisPosition(0, 0, 0)); + masks.add(new IrisPosition(1, 0, 0)); + masks.add(new IrisPosition(0, 1, 0)); + masks.add(new IrisPosition(-1, 0, 1)); + masks.add(new IrisPosition(0, 0, -2)); + + assertEquals(referenceMasked(seeds, masks, 3.0D), MantleWriter.getMasked(seeds, masks, 3.0D)); + } + + @Test + public void anEmptyMaskProducesNoVoxels() { + assertTrue(MantleWriter.getMasked(line(0, 70, 0, 2, 70, 2), new LinkedHashSet<>(), 2.0D).isEmpty()); + } + + @Test + public void aPositionLookupReportsMembershipOutsideItsBoundingBoxAsAbsent() { + Set positions = new LinkedHashSet<>(); + positions.add(new IrisPosition(10, 20, 30)); + MantleWriter.PositionLookup lookup = MantleWriter.PositionLookup.of(positions); + + assertTrue(lookup.contains(10, 20, 30)); + assertFalse(lookup.contains(9, 20, 30)); + assertFalse(lookup.contains(10, 20, 31)); + assertFalse(lookup.contains(Integer.MIN_VALUE, 20, 30)); + assertFalse(lookup.contains(Integer.MAX_VALUE, 20, 30)); + } + + @Test + public void anEmptyPositionLookupContainsNothing() { + MantleWriter.PositionLookup lookup = MantleWriter.PositionLookup.of(new LinkedHashSet<>()); + + assertFalse(lookup.contains(0, 0, 0)); + assertFalse(lookup.contains(-1, -1, -1)); + } + + private static Set line(int x1, int y1, int z1, int x2, int y2, int z2) { + Set positions = new LinkedHashSet<>(); + int steps = Math.max(Math.max(Math.abs(x2 - x1), Math.abs(y2 - y1)), Math.abs(z2 - z1)); + for (int step = 0; step <= steps; step++) { + double t = steps == 0 ? 0D : (double) step / steps; + positions.add(new IrisPosition( + (int) Math.round(x1 + t * (x2 - x1)), + (int) Math.round(y1 + t * (y2 - y1)), + (int) Math.round(z1 + t * (z2 - z1)))); + } + return positions; + } + + static Set referenceBallooned(Set vset, double radius) { + Set returnset = new HashSet<>(); + int ceilrad = (int) Math.ceil(radius); + double r2 = Math.pow(radius, 2); + + for (IrisPosition v : vset) { + int tipx = v.getX(); + int tipy = v.getY(); + int tipz = v.getZ(); + + for (int loopx = tipx - ceilrad; loopx <= tipx + ceilrad; loopx++) { + for (int loopy = tipy - ceilrad; loopy <= tipy + ceilrad; loopy++) { + for (int loopz = tipz - ceilrad; loopz <= tipz + ceilrad; loopz++) { + if (referenceHypot(loopx - tipx, loopy - tipy, loopz - tipz) <= r2) { + returnset.add(new IrisPosition(loopx, loopy, loopz)); + } + } + } + } + } + return returnset; + } + + static Set referenceHollowed(Set vset) { + Set returnset = new LinkedHashSet<>(); + for (IrisPosition v : vset) { + double x = v.getX(); + double y = v.getY(); + double z = v.getZ(); + if (!(vset.contains(new IrisPosition(x + 1, y, z)) + && vset.contains(new IrisPosition(x - 1, y, z)) + && vset.contains(new IrisPosition(x, y + 1, z)) + && vset.contains(new IrisPosition(x, y - 1, z)) + && vset.contains(new IrisPosition(x, y, z + 1)) + && vset.contains(new IrisPosition(x, y, z - 1)))) { + returnset.add(v); + } + } + return returnset; + } + + static Set referenceMasked(Set vectors, Set masks, double radius) { + Set vset = new LinkedHashSet<>(); + int ceil = (int) Math.ceil(radius); + double r2 = Math.pow(radius, 2); + + for (IrisPosition v : vectors) { + int tipX = v.getX(); + int tipY = v.getY(); + int tipZ = v.getZ(); + + for (int x = -ceil; x <= ceil; x++) { + for (int y = -ceil; y <= ceil; y++) { + for (int z = -ceil; z <= ceil; z++) { + if (referenceHypot(x, y, z) > r2 || !masks.contains(new IrisPosition(x, y, z))) { + continue; + } + vset.add(new IrisPosition(tipX + x, tipY + y, tipZ + z)); + } + } + } + } + + return vset; + } + + private static double referenceHypot(double... pars) { + double sum = 0; + for (double d : pars) { + sum += Math.pow(d, 2); + } + return sum; + } +}