From 2f8d3b1d1a76a6f7be723e0b78402c8e5a217036 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Mon, 28 Sep 2026 11:22:51 -0400 Subject: [PATCH] Memoize object block states - Cache vanilla withProperty variants per state - Cache rotated states per rotation and quantized spin - Hoist the stilt dirt state and rotate only target-layer stilt blocks --- .../platform/bukkit/BukkitBlockState.java | 14 ++++- .../iris/structure/object/IrisObject.java | 6 ++ .../object/IrisObjectPlacementRunner.java | 5 +- .../structure/object/IrisObjectRotation.java | 21 ++++++- .../bukkit/BukkitSpiConformanceTest.java | 18 ++++++ .../IrisObjectRotationStateMemoTest.java | 58 +++++++++++++++++++ 6 files changed, 115 insertions(+), 7 deletions(-) create mode 100644 core/src/test/java/art/arcane/iris/structure/object/IrisObjectRotationStateMemoTest.java diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBlockState.java b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBlockState.java index 9ccbda4f5..09d25445a 100644 --- a/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBlockState.java +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBlockState.java @@ -80,6 +80,7 @@ public final class BukkitBlockState implements NativeBlockState { private volatile Boolean deepSlate; private volatile Boolean vineBlock; private volatile Boolean tileEntity; + private volatile ConcurrentHashMap> propertyVariants; private BukkitBlockState(BlockData data, String key) { this.data = data; @@ -428,7 +429,18 @@ public final class BukkitBlockState implements NativeBlockState { BlockData resolved = Bukkit.createBlockData(merged); return of(IrisCustomData.of(resolved, custom.getCustom())); } - return of(Bukkit.createBlockData(BlockStateKey.withProperty(key, name, value))); + ConcurrentHashMap> variants = propertyVariants; + if (variants == null) { + variants = new ConcurrentHashMap<>(4); + propertyVariants = variants; + } + ConcurrentHashMap values = variants.computeIfAbsent(name, (String ignored) -> new ConcurrentHashMap<>(4)); + NativeBlockState variant = values.get(value); + if (variant == null) { + variant = of(Bukkit.createBlockData(BlockStateKey.withProperty(key, name, value))); + values.putIfAbsent(value, variant); + } + return variant; } @Override diff --git a/core/src/main/java/art/arcane/iris/structure/object/IrisObject.java b/core/src/main/java/art/arcane/iris/structure/object/IrisObject.java index 79bd3550d..54c99ec6a 100644 --- a/core/src/main/java/art/arcane/iris/structure/object/IrisObject.java +++ b/core/src/main/java/art/arcane/iris/structure/object/IrisObject.java @@ -86,6 +86,7 @@ public class IrisObject extends IrisRegistrant { NativeBlockState stone, NativeBlockState vair, NativeBlockState vairDebug, + NativeBlockState dirt, NativeBlockState[] snowLayers) { } @@ -102,6 +103,7 @@ public class IrisObject extends IrisRegistrant { B.getState("STONE"), B.getState("VOID_AIR"), B.getState("COBWEB"), + B.getState("minecraft:dirt"), new NativeBlockState[]{B.getState("minecraft:snow[layers=1]"), B.getState("minecraft:snow[layers=2]"), B.getState("minecraft:snow[layers=3]"), B.getState("minecraft:snow[layers=4]"), B.getState("minecraft:snow[layers=5]"), B.getState("minecraft:snow[layers=6]"), B.getState("minecraft:snow[layers=7]"), B.getState("minecraft:snow[layers=8]")}); bound = resolved; return resolved; @@ -123,6 +125,10 @@ public class IrisObject extends IrisRegistrant { return bound().vairDebug(); } + static NativeBlockState dirt() { + return bound().dirt(); + } + static NativeBlockState snowLayer(int layerIndex) { return bound().snowLayers()[layerIndex]; } diff --git a/core/src/main/java/art/arcane/iris/structure/object/IrisObjectPlacementRunner.java b/core/src/main/java/art/arcane/iris/structure/object/IrisObjectPlacementRunner.java index e203e009d..c26f36951 100644 --- a/core/src/main/java/art/arcane/iris/structure/object/IrisObjectPlacementRunner.java +++ b/core/src/main/java/art/arcane/iris/structure/object/IrisObjectPlacementRunner.java @@ -733,7 +733,7 @@ final class IrisObjectPlacementRunner { } else { String mat = IrisObjectShaping.materialKey(d); if (mat.equals("minecraft:grass_block") || mat.equals("minecraft:mycelium") || mat.equals("minecraft:podzol") || mat.equals("minecraft:dirt_path")) { - d = B.getState("minecraft:dirt"); + d = IrisObject.States.dirt(); } } @@ -745,11 +745,10 @@ final class IrisObjectPlacementRunner { if (translating) { i.add(translateOffset); } - d = config.getRotation().rotate(d, spinx, spiny, spinz); - int targetLayer = ceilingHang ? topLayer : lowest; if (i.getBlockY() != targetLayer) continue; + d = config.getRotation().rotate(d, spinx, spiny, spinz); if (hasEdits) { for (IrisObjectReplace j : config.getEdit()) { diff --git a/core/src/main/java/art/arcane/iris/structure/object/IrisObjectRotation.java b/core/src/main/java/art/arcane/iris/structure/object/IrisObjectRotation.java index 6d08b0af8..4c51dbb7c 100644 --- a/core/src/main/java/art/arcane/iris/structure/object/IrisObjectRotation.java +++ b/core/src/main/java/art/arcane/iris/structure/object/IrisObjectRotation.java @@ -50,6 +50,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; @Snippet("object-rotator") @Accessors(chain = true) @@ -59,6 +60,8 @@ import java.util.Set; @Data public class IrisObjectRotation { private static volatile StateRotator PLATFORM_ROTATOR = null; + private static final Object ROTATED_AWAY = new Object(); + private static final int SPIN_STEPS = 5; public interface StateRotator { NativeBlockState rotate(IrisObjectRotation rotation, NativeBlockState state, int spinx, int spiny, int spinz); @@ -91,6 +94,8 @@ public class IrisObjectRotation { @Description("The z axis rotation") private IrisAxisRotationClamp zAxis = new IrisAxisRotationClamp(); + private final transient ConcurrentHashMap stateRotations = new ConcurrentHashMap<>(); + public static IrisObjectRotation xFlip180() { IrisObjectRotation rt = new IrisObjectRotation(); IrisAxisRotationClamp rtx = new IrisAxisRotationClamp(); @@ -316,9 +321,19 @@ public class IrisObjectRotation { return state; } - BlockData raw = original.clone(); - BlockData rotated = rotate(raw, spinx, spiny, spinz); - return rotated == null ? null : BukkitBlockState.of(rotated); + Object[] rotations = stateRotations.computeIfAbsent(state, (NativeBlockState ignored) -> new Object[SPIN_STEPS * SPIN_STEPS * SPIN_STEPS]); + int index = (spinStep(spinx) * SPIN_STEPS + spinStep(spiny)) * SPIN_STEPS + spinStep(spinz); + Object memo = rotations[index]; + if (memo == null) { + BlockData rotated = rotate(original.clone(), spinx, spiny, spinz); + memo = rotated == null ? ROTATED_AWAY : BukkitBlockState.of(rotated); + rotations[index] = memo; + } + return memo == ROTATED_AWAY ? null : (NativeBlockState) memo; + } + + private static int spinStep(int spin) { + return (int) Math.ceil(Math.abs((spin % 360D) / 90D)); } private static boolean canRotateBlockData(BlockData data) { diff --git a/core/src/test/java/art/arcane/iris/platform/bukkit/BukkitSpiConformanceTest.java b/core/src/test/java/art/arcane/iris/platform/bukkit/BukkitSpiConformanceTest.java index acd31474e..deb835294 100644 --- a/core/src/test/java/art/arcane/iris/platform/bukkit/BukkitSpiConformanceTest.java +++ b/core/src/test/java/art/arcane/iris/platform/bukkit/BukkitSpiConformanceTest.java @@ -54,6 +54,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockingDetails; public class BukkitSpiConformanceTest { @BeforeClass @@ -181,6 +182,23 @@ public class BukkitSpiConformanceTest { assertEquals("iristest:append_block[axis=y,lit=true]", merged.key()); } + @Test + public void withPropertyParsesEachVariantOnce() { + String merged = "iristest:memo_leaves[distance=1,persistent=true]"; + NativeBlockState state = BukkitBlockState.of(blockData("iristest:memo_leaves[distance=1,persistent=false]")); + + NativeBlockState first = state.withProperty("persistent", "true"); + NativeBlockState second = state.withProperty("persistent", "true"); + + assertSame(first, second); + assertEquals(merged, first.key()); + long parses = mockingDetails(Bukkit.getServer()).getInvocations().stream() + .filter(invocation -> invocation.getMethod().getName().equals("createBlockData") + && merged.equals(invocation.getArgument(0))) + .count(); + assertEquals(1L, parses); + } + @Test public void withPropertyAddsBracketSectionWhenAbsent() { BlockData data = blockData("iristest:bare_block"); diff --git a/core/src/test/java/art/arcane/iris/structure/object/IrisObjectRotationStateMemoTest.java b/core/src/test/java/art/arcane/iris/structure/object/IrisObjectRotationStateMemoTest.java new file mode 100644 index 000000000..95a84ef58 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/structure/object/IrisObjectRotationStateMemoTest.java @@ -0,0 +1,58 @@ +package art.arcane.iris.structure.object; + +import art.arcane.iris.platform.bukkit.BukkitBlockState; +import art.arcane.iris.testsupport.BukkitTestServer; +import art.arcane.volmlib.nativelib.terrain.NativeBlockState; +import org.bukkit.Axis; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.Orientable; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.withSettings; + +public class IrisObjectRotationStateMemoTest { + @BeforeClass + public static void installServer() { + BukkitTestServer.install(); + } + + @Test + public void rotatedStatesAreComputedOncePerQuantizedSpin() { + AtomicInteger clones = new AtomicInteger(); + BlockData original = log("iristest:memo_log[axis=x]", Axis.X); + doAnswer(invocation -> { + clones.incrementAndGet(); + return log("iristest:memo_log[axis=x]", Axis.X); + }).when(original).clone(); + NativeBlockState state = BukkitBlockState.of(original); + IrisObjectRotation rotation = IrisObjectRotation.of(0, 90, 0); + + NativeBlockState first = rotation.rotate(state, 0, 0, 0); + NativeBlockState repeated = rotation.rotate(state, 0, 0, 0); + rotation.rotate(state, 0, 30, 0); + rotation.rotate(state, 0, 90, 0); + + assertSame(first, repeated); + assertEquals(2, clones.get()); + assertEquals("iristest:memo_log[axis=x]", first.key()); + } + + private static BlockData log(String key, Axis axis) { + BlockData data = mock(BlockData.class, withSettings().extraInterfaces(Orientable.class)); + doReturn(key).when(data).getAsString(); + doReturn(key).when(data).getAsString(anyBoolean()); + doReturn(axis).when((Orientable) data).getAxis(); + doReturn(Set.of(Axis.X, Axis.Y, Axis.Z)).when((Orientable) data).getAxes(); + return data; + } +}