From 1a2c8a88458d27274cdfc0bba5191bdcf1d6ce77 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Sun, 6 Sep 2026 03:43:25 -0400 Subject: [PATCH] Simplified object smart boring - Smart boring runs direct axis passes under the existing write lock - Negative object bounds stop at the last occupied coordinate - Bore insertion avoids redundant map callbacks - Regression tests cover ordered fills and repeated calls --- .../iris/engine/object/IrisObjectShaping.java | 161 +++++++----------- .../engine/object/IrisObjectShapingTest.java | 127 ++++++++++++++ 2 files changed, 192 insertions(+), 96 deletions(-) create mode 100644 core/src/test/java/art/arcane/iris/engine/object/IrisObjectShapingTest.java diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectShaping.java b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectShaping.java index 5b31f2bf4..61cba47f9 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisObjectShaping.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisObjectShaping.java @@ -23,13 +23,9 @@ import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.util.common.data.VectorMap; import art.arcane.iris.util.common.math.IrisBlockVector; import art.arcane.iris.util.common.math.Vector3i; -import art.arcane.iris.util.common.parallel.BurstExecutor; -import art.arcane.iris.util.common.parallel.MultiBurst; import art.arcane.volmlib.util.format.Form; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; -import java.util.concurrent.atomic.AtomicInteger; - /** * Volume shaping for {@link IrisObject}: smart boring, shrinkwrapping, compaction and the shared block * classification helpers used by placement. @@ -59,30 +55,23 @@ final class IrisObjectShaping { private static void ensureSmartBoredLocked(IrisObject self) { PrecisionStopwatch p = PrecisionStopwatch.start(); PlatformBlockState vair = IrisObject.States.VAIR; - AtomicInteger applied = new AtomicInteger(); - IrisBlockVector max = new IrisBlockVector(Double.MIN_VALUE, Double.MIN_VALUE, Double.MIN_VALUE); - IrisBlockVector min = new IrisBlockVector(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE); - VectorMap source; - self.readLock.lock(); - try { - if (self.blocks.isEmpty()) { - IrisLogging.warn("Cannot Smart Bore " + self.getLoadKey() + " because it has 0 blocks in it."); - self.smartBored = true; - return; - } + int applied = 0; + IrisBlockVector max = new IrisBlockVector(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE); + IrisBlockVector min = new IrisBlockVector(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); + VectorMap source = self.blocks; + if (source.isEmpty()) { + IrisLogging.warn("Cannot Smart Bore " + self.getLoadKey() + " because it has 0 blocks in it."); + self.smartBored = true; + return; + } - source = self.blocks; - - for (IrisBlockVector i : self.blocks.keys()) { - max.setX(Math.max(i.getX(), max.getX())); - min.setX(Math.min(i.getX(), min.getX())); - max.setY(Math.max(i.getY(), max.getY())); - min.setY(Math.min(i.getY(), min.getY())); - max.setZ(Math.max(i.getZ(), max.getZ())); - min.setZ(Math.min(i.getZ(), min.getZ())); - } - } finally { - self.readLock.unlock(); + for (IrisBlockVector i : source.keys()) { + max.setX(Math.max(i.getX(), max.getX())); + min.setX(Math.min(i.getX(), min.getX())); + max.setY(Math.max(i.getY(), max.getY())); + min.setY(Math.min(i.getY(), min.getY())); + max.setZ(Math.max(i.getZ(), max.getZ())); + min.setZ(Math.min(i.getZ(), min.getZ())); } VectorMap bore = new VectorMap<>(); @@ -90,114 +79,94 @@ final class IrisObjectShaping { // bored volume must come from a fixed X->Y->Z order on the calling (lock-owning) // thread, not from pool scheduling. This matches what burst-worker callers (studio, // generation threads) already produced, so output is unchanged where it was stable. - BurstExecutor burst = MultiBurst.burst.burst(); - burst.setMulticore(false); // Smash X for (int rayY = min.getBlockY(); rayY <= max.getBlockY(); rayY++) { - int finalRayY = rayY; - burst.queue(() -> { - for (int rayZ = min.getBlockZ(); rayZ <= max.getBlockZ(); rayZ++) { - int start = Integer.MAX_VALUE; - int end = Integer.MIN_VALUE; + for (int rayZ = min.getBlockZ(); rayZ <= max.getBlockZ(); rayZ++) { + int start = Integer.MAX_VALUE; + int end = Integer.MIN_VALUE; - for (int ray = min.getBlockX(); ray <= max.getBlockX(); ray++) { - if (boreContains(source, bore, new IrisBlockVector(ray, finalRayY, rayZ))) { - start = Math.min(ray, start); - end = Math.max(ray, end); - } - } - - if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) { - for (int i = start; i <= end; i++) { - boreCell(source, bore, new IrisBlockVector(i, finalRayY, rayZ), vair, applied); - } + for (int ray = min.getBlockX(); ray <= max.getBlockX(); ray++) { + if (boreContains(source, bore, new IrisBlockVector(ray, rayY, rayZ))) { + start = Math.min(ray, start); + end = Math.max(ray, end); } } - }); + + if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) { + for (int i = start; i <= end; i++) { + applied += boreCell(source, bore, new IrisBlockVector(i, rayY, rayZ), vair); + } + } + } } // Smash Y for (int rayX = min.getBlockX(); rayX <= max.getBlockX(); rayX++) { - int finalRayX = rayX; - burst.queue(() -> { - for (int rayZ = min.getBlockZ(); rayZ <= max.getBlockZ(); rayZ++) { - int start = Integer.MAX_VALUE; - int end = Integer.MIN_VALUE; + for (int rayZ = min.getBlockZ(); rayZ <= max.getBlockZ(); rayZ++) { + int start = Integer.MAX_VALUE; + int end = Integer.MIN_VALUE; - for (int ray = min.getBlockY(); ray <= max.getBlockY(); ray++) { - if (boreContains(source, bore, new IrisBlockVector(finalRayX, ray, rayZ))) { - start = Math.min(ray, start); - end = Math.max(ray, end); - } - } - - if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) { - for (int i = start; i <= end; i++) { - boreCell(source, bore, new IrisBlockVector(finalRayX, i, rayZ), vair, applied); - } + for (int ray = min.getBlockY(); ray <= max.getBlockY(); ray++) { + if (boreContains(source, bore, new IrisBlockVector(rayX, ray, rayZ))) { + start = Math.min(ray, start); + end = Math.max(ray, end); } } - }); + + if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) { + for (int i = start; i <= end; i++) { + applied += boreCell(source, bore, new IrisBlockVector(rayX, i, rayZ), vair); + } + } + } } // Smash Z for (int rayX = min.getBlockX(); rayX <= max.getBlockX(); rayX++) { - int finalRayX = rayX; - burst.queue(() -> { - for (int rayY = min.getBlockY(); rayY <= max.getBlockY(); rayY++) { - int start = Integer.MAX_VALUE; - int end = Integer.MIN_VALUE; + for (int rayY = min.getBlockY(); rayY <= max.getBlockY(); rayY++) { + int start = Integer.MAX_VALUE; + int end = Integer.MIN_VALUE; - for (int ray = min.getBlockZ(); ray <= max.getBlockZ(); ray++) { - if (boreContains(source, bore, new IrisBlockVector(finalRayX, rayY, ray))) { - start = Math.min(ray, start); - end = Math.max(ray, end); - } - } - - if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) { - for (int i = start; i <= end; i++) { - boreCell(source, bore, new IrisBlockVector(finalRayX, rayY, i), vair, applied); - } + for (int ray = min.getBlockZ(); ray <= max.getBlockZ(); ray++) { + if (boreContains(source, bore, new IrisBlockVector(rayX, rayY, ray))) { + start = Math.min(ray, start); + end = Math.max(ray, end); } } - }); - } - burst.complete(); - - self.writeLock.lock(); - try { - if (!self.smartBored) { - bore.forEach((v, s) -> self.blocks.computeIfAbsent(v, (vv) -> s)); - self.smartBored = true; + if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) { + for (int i = start; i <= end; i++) { + applied += boreCell(source, bore, new IrisBlockVector(rayX, rayY, i), vair); + } + } } - } finally { - self.writeLock.unlock(); } - IrisLogging.debug("Smart Bore: " + self.getLoadKey() + " in " + Form.duration(p.getMilliseconds(), 2) + " (" + Form.f(applied.get()) + ")"); + source.putAll(bore); + self.smartBored = true; + + IrisLogging.debug("Smart Bore: " + self.getLoadKey() + " in " + Form.duration(p.getMilliseconds(), 2) + " (" + Form.f(applied) + ")"); } private static boolean boreContains(VectorMap source, VectorMap bore, IrisBlockVector v) { return source.containsKey(v) || bore.containsKey(v); } - private static void boreCell(VectorMap source, VectorMap bore, IrisBlockVector v, PlatformBlockState vair, AtomicInteger applied) { + private static int boreCell(VectorMap source, VectorMap bore, IrisBlockVector v, PlatformBlockState vair) { PlatformBlockState existing = source.get(v); if (existing == null) { if (vair.equals(bore.get(v))) { - return; + return 0; } - bore.computeIfAbsent(v, (vv) -> vair); + bore.put(v, vair); } else if (vair.equals(existing)) { - return; + return 0; } - applied.getAndIncrement(); + return 1; } static void shrinkwrap(IrisObject self) { diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisObjectShapingTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisObjectShapingTest.java new file mode 100644 index 000000000..713adfdd8 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisObjectShapingTest.java @@ -0,0 +1,127 @@ +package art.arcane.iris.engine.object; + +import art.arcane.iris.spi.IrisPlatform; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.spi.PlatformRegistries; +import art.arcane.iris.util.common.math.IrisBlockVector; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class IrisObjectShapingTest { + @Before + public void bindPlatform() { + IrisPlatforms.unbind(); + PlatformRegistries registries = mock(PlatformRegistries.class); + Map states = new HashMap<>(); + when(registries.block(anyString())).thenAnswer(invocation -> states.computeIfAbsent( + invocation.getArgument(0), key -> mock(PlatformBlockState.class))); + IrisPlatform platform = mock(IrisPlatform.class); + when(platform.registries()).thenReturn(registries); + IrisPlatforms.bind(platform); + } + + @After + public void unbindPlatform() { + IrisPlatforms.unbind(); + } + + @Test + public void smartBoreMatchesOrderedAxisClosureAndPreservesOriginalBlocks() { + Random random = new Random(617431L); + PlatformBlockState stone = mock(PlatformBlockState.class); + for (int fixture = 0; fixture < 24; fixture++) { + int offset = switch (fixture % 3) { + case 0 -> -1050; + case 1 -> 1020; + default -> -3; + }; + IrisObject object = new IrisObject(6, 6, 6); + Map expected = new HashMap<>(); + for (int index = 0; index < 36; index++) { + Cell cell = new Cell(offset + random.nextInt(6), offset + random.nextInt(6), offset + random.nextInt(6)); + PlatformBlockState state = switch (index % 3) { + case 0 -> IrisObject.States.AIR; + case 1 -> IrisObject.States.VAIR; + default -> stone; + }; + object.getBlocks().put(cell.vector(), state); + expected.put(cell, state); + } + for (int axis = 0; axis < 3; axis++) { + closeAxis(expected, axis); + } + + IrisObjectShaping.ensureSmartBored(object); + + assertTrue(object.isSmartBored()); + assertEquals("fixture " + fixture, expected.size(), object.getBlocks().size()); + for (Map.Entry entry : expected.entrySet()) { + assertSame(entry.getValue(), object.getBlocks().get(entry.getKey().vector())); + } + long revision = object.getBlocks().modificationRevision(); + IrisObjectShaping.ensureSmartBored(object); + assertEquals(revision, object.getBlocks().modificationRevision()); + } + } + + @Test + public void emptySmartBoreCompletesWithoutAddingBlocks() { + IrisObject object = new IrisObject(1, 1, 1); + + IrisObjectShaping.ensureSmartBored(object); + + assertTrue(object.isSmartBored()); + assertTrue(object.getBlocks().isEmpty()); + } + + private static void closeAxis(Map blocks, int axis) { + List occupied = new ArrayList<>(blocks.keySet()); + for (Cell start : occupied) { + for (Cell end : occupied) { + if (!start.with(axis, 0).equals(end.with(axis, 0))) { + continue; + } + for (int position = start.coordinate(axis); position <= end.coordinate(axis); position++) { + blocks.putIfAbsent(start.with(axis, position), IrisObject.States.VAIR); + } + } + } + } + + private record Cell(int x, int y, int z) { + private int coordinate(int axis) { + return switch (axis) { + case 0 -> x; + case 1 -> y; + default -> z; + }; + } + + private Cell with(int axis, int coordinate) { + return switch (axis) { + case 0 -> new Cell(coordinate, y, z); + case 1 -> new Cell(x, coordinate, z); + default -> new Cell(x, y, coordinate); + }; + } + + private IrisBlockVector vector() { + return new IrisBlockVector(x, y, z); + } + } +}