From be30285718345a194ec95f87a56f16a6220f1950 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Thu, 22 Oct 2026 00:03:34 -0400 Subject: [PATCH] Leaner object transactions - Memoize the last subterrain protection verdict in object destination transactions - Key buffered cave object blocks by packed positions and check each position once at commit - Build every free source plan before waiting on plans other threads are building - Reuse TreeBlockMaterial values per block state --- .../CaveObjectPlacementTransaction.java | 51 +++++++++++++++---- .../mantle/MantleObjectComponent.java | 41 ++++++++++----- .../mantle/ObjectDestinationTransaction.java | 16 +++++- .../mantle/ObjectSourcePlanCache.java | 15 ++++++ .../mantle/ObjectSourcePlanCacheTest.java | 38 ++++++++++++++ 5 files changed, 135 insertions(+), 26 deletions(-) diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/CaveObjectPlacementTransaction.java b/core/src/main/java/art/arcane/iris/generation/mantle/CaveObjectPlacementTransaction.java index b53862f49..35123610a 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/CaveObjectPlacementTransaction.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/CaveObjectPlacementTransaction.java @@ -32,13 +32,14 @@ import art.arcane.volmlib.util.math.BlockPosition; import art.arcane.iris.generation.block.B; import art.arcane.volmlib.util.collection.KList; import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import org.jetbrains.annotations.Nullable; -import java.util.HashMap; -import java.util.Map; - final class CaveObjectPlacementTransaction implements IObjectPlacer { private static final int CACHE_MISS = Integer.MIN_VALUE; + private static final int POSITION_BITS = 21; + private static final int POSITION_LIMIT = 1 << (POSITION_BITS - 1); + private static final long POSITION_MASK = (1L << POSITION_BITS) - 1L; private final IObjectPlacer delegate; private final Engine engine; @@ -46,11 +47,14 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer { private final int minDepthBelowSurface; private final int worldHeight; private final KList mutations; - private final Map bufferedBlocks; + private final Long2ObjectOpenHashMap bufferedBlocks; private final Long2IntOpenHashMap surfaceHeights; private final Long2IntOpenHashMap caveCeilings; private int blockWrites; private BlockPosition canopyPosition; + private boolean originKnown; + private int originX; + private int originZ; CaveObjectPlacementTransaction(IObjectPlacer delegate, int anchorY, int minDepthBelowSurface) { this.delegate = delegate; @@ -59,7 +63,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer { this.minDepthBelowSurface = Math.max(0, minDepthBelowSurface); this.worldHeight = engine == null ? 0 : engine.getHeight(); this.mutations = new KList<>(); - this.bufferedBlocks = new HashMap<>(); + this.bufferedBlocks = new Long2ObjectOpenHashMap<>(); this.surfaceHeights = new Long2IntOpenHashMap(); this.caveCeilings = new Long2IntOpenHashMap(); this.surfaceHeights.defaultReturnValue(CACHE_MISS); @@ -73,7 +77,14 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer { } IrisComplex complex = engine == null ? null : engine.getComplex(); + boolean previousChecked = false; + int previousX = 0; + int previousY = 0; + int previousZ = 0; for (BufferedMutation mutation : mutations) { + if (previousChecked && mutation.x() == previousX && mutation.y() == previousY && mutation.z() == previousZ) { + continue; + } if (complex != null && !complex.allowsNewDiscreteContentAt(mutation.x(), mutation.z())) { discard(); @@ -95,6 +106,10 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer { discard(); return CommitResult.REJECTED_HYDROLOGY; } + previousChecked = true; + previousX = mutation.x(); + previousY = mutation.y(); + previousZ = mutation.z(); } applyMutations(); @@ -165,7 +180,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer { return; } mutations.add(new BlockMutation(x, y, z, state, isCanopy(x, y, z))); - bufferedBlocks.put(new PositionKey(x, y, z), state); + bufferedBlocks.put(bufferKey(x, y, z), state); blockWrites++; } @@ -190,7 +205,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer { @Override public NativeBlockState get(int x, int y, int z) { - NativeBlockState buffered = bufferedBlocks.get(new PositionKey(x, y, z)); + NativeBlockState buffered = bufferedBlocks.isEmpty() ? null : bufferedBlocks.get(bufferKey(x, y, z)); return buffered == null ? delegate.get(x, y, z) : buffered; } @@ -211,7 +226,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer { @Override public boolean isSolid(int x, int y, int z) { - NativeBlockState buffered = bufferedBlocks.get(new PositionKey(x, y, z)); + NativeBlockState buffered = bufferedBlocks.isEmpty() ? null : bufferedBlocks.get(bufferKey(x, y, z)); return buffered == null ? delegate.isSolid(x, y, z) : B.isSolid(buffered); } @@ -253,7 +268,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer { } mutations.add(new DataMutation(x, y, z, data, isCanopy(x, y, z))); if (data instanceof NativeBlockState state) { - bufferedBlocks.put(new PositionKey(x, y, z), state); + bufferedBlocks.put(bufferKey(x, y, z), state); blockWrites++; } } @@ -278,6 +293,22 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer { return engine; } + private long bufferKey(int x, int y, int z) { + if (!originKnown) { + originX = x; + originZ = z; + originKnown = true; + } + int dx = x - originX; + int dz = z - originZ; + if (dx < -POSITION_LIMIT || dx >= POSITION_LIMIT || dz < -POSITION_LIMIT || dz >= POSITION_LIMIT + || y < -POSITION_LIMIT || y >= POSITION_LIMIT) { + throw new IllegalStateException("Cave object write at " + x + "," + y + "," + z + + " is too far from its first write at " + originX + "," + originZ); + } + return ((dx & POSITION_MASK) << (POSITION_BITS * 2)) | ((dz & POSITION_MASK) << POSITION_BITS) | (y & POSITION_MASK); + } + private boolean isWithinBounds(int x, int y, int z) { if (engine == null || y < 0 || y >= worldHeight) { return false; @@ -361,6 +392,4 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer { } } - private record PositionKey(int x, int y, int z) { - } } diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/MantleObjectComponent.java b/core/src/main/java/art/arcane/iris/generation/mantle/MantleObjectComponent.java index 913c726f8..3d452b5ab 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/MantleObjectComponent.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/MantleObjectComponent.java @@ -88,6 +88,8 @@ public class MantleObjectComponent extends IrisMantleComponent { private static final Map CAVE_REJECT_LOG_STATE = new ConcurrentHashMap<>(); private static final Set MISSING_LOAD_KEY_WARNED = ConcurrentHashMap.newKeySet(); private static final long SOURCE_PLAN_STATS_INTERVAL_MS = 30_000L; + private static final int MAXIMUM_TREE_MATERIALS = 8192; + private static final Map TREE_MATERIALS = new ConcurrentHashMap<>(); private final Object collisionRuleLock; private final ObjectSourcePlanCache sourcePlans; @@ -124,7 +126,14 @@ public class MantleObjectComponent extends IrisMantleComponent { } private static void writeTreeMaterial(IObjectPlacer placer, int x, int y, int z, NativeBlockState data) { - placer.setData(x, y, z, TreeBlockMaterial.of(data)); + TreeBlockMaterial material = TREE_MATERIALS.get(data); + if (material == null) { + material = TreeBlockMaterial.of(data); + if (TREE_MATERIALS.size() < MAXIMUM_TREE_MATERIALS) { + TREE_MATERIALS.putIfAbsent(data, material); + } + } + placer.setData(x, y, z, material); } @Override @@ -157,19 +166,23 @@ public class MantleObjectComponent extends IrisMantleComponent { ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, x, z); int radius = getRadius(); int sourceChunkRadius = sourceChunkRadius(radius); - replaySourceChunks( - x, - z, - radius, - (sourceX, sourceZ) -> { - ObjectSourcePlan plan = sourcePlans.acquire(sourceX, sourceZ, x, z, sourceChunkRadius, - () -> buildSourcePlan(writer, sourceX, sourceZ, context)); - if (plan != null) { - transaction.apply(plan); - plan.persistContinuations(writer, sourceX, sourceZ, x, z); - } - } - ); + int side = (sourceChunkRadius * 2) + 1; + ObjectSourcePlan[] plans = new ObjectSourcePlan[side * side]; + int[] slot = new int[1]; + replaySourceChunks(x, z, radius, (sourceX, sourceZ) -> plans[slot[0]++] = sourcePlans.tryAcquire( + sourceX, sourceZ, x, z, sourceChunkRadius, () -> buildSourcePlan(writer, sourceX, sourceZ, context))); + slot[0] = 0; + replaySourceChunks(x, z, radius, (sourceX, sourceZ) -> { + ObjectSourcePlan plan = plans[slot[0]++]; + if (plan == ObjectSourcePlanCache.BUSY) { + plan = sourcePlans.acquire(sourceX, sourceZ, x, z, sourceChunkRadius, + () -> buildSourcePlan(writer, sourceX, sourceZ, context)); + } + if (plan != null) { + transaction.apply(plan); + plan.persistContinuations(writer, sourceX, sourceZ, x, z); + } + }); transaction.commit(); logSourcePlanStats(); } diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/ObjectDestinationTransaction.java b/core/src/main/java/art/arcane/iris/generation/mantle/ObjectDestinationTransaction.java index f1012ec84..0622f536b 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/ObjectDestinationTransaction.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/ObjectDestinationTransaction.java @@ -40,6 +40,11 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer { private int hydrologyGateChunkX; private int hydrologyGateChunkZ; private boolean hydrologyGateOpen; + private boolean subterrainKnown; + private int subterrainX; + private int subterrainY; + private int subterrainZ; + private boolean subterrainProtects; ObjectDestinationTransaction(MantleWriter writer, int destinationChunkX, int destinationChunkZ) { this.writer = writer; @@ -396,8 +401,17 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer { } private boolean protectsSubterrain(int x, int y, int z) { + if (subterrainKnown && x == subterrainX && y == subterrainY && z == subterrainZ) { + return subterrainProtects; + } Engine engine = getEngine(); - return engine != null && SubterrainRasterizer.protectsPlacement(engine.getSubterrainCell(x, y, z)); + boolean protects = engine != null && SubterrainRasterizer.protectsPlacement(engine.getSubterrainCell(x, y, z)); + subterrainX = x; + subterrainY = y; + subterrainZ = z; + subterrainProtects = protects; + subterrainKnown = true; + return protects; } private boolean hasProtectedHydrology(int x, int y, int z) { diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/ObjectSourcePlanCache.java b/core/src/main/java/art/arcane/iris/generation/mantle/ObjectSourcePlanCache.java index 4b73e6dce..daa1a1580 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/ObjectSourcePlanCache.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/ObjectSourcePlanCache.java @@ -4,6 +4,7 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import art.arcane.volmlib.util.cache.CacheKey; +import java.util.List; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -14,6 +15,7 @@ import java.util.function.Supplier; import static art.arcane.iris.generation.cache.Cache.key; final class ObjectSourcePlanCache { + static final ObjectSourcePlan BUSY = new ObjectSourcePlan(List.of()); private static final long MAXIMUM_RETAINED_BYTES = 512L * 1024L * 1024L; private static final int ENTRY_BYTES = 64; @@ -68,6 +70,19 @@ final class ObjectSourcePlanCache { return plan; } + ObjectSourcePlan tryAcquire(int sourceChunkX, int sourceChunkZ, int destinationChunkX, int destinationChunkZ, + int sourceChunkRadius, Supplier builder) { + State current = state; + long source = CacheKey.mix(key(sourceChunkX, sourceChunkZ)); + if (current.plans.getIfPresent(source) == null) { + Pending pending = current.pending.get(source); + if (pending != null && pending.owner != Thread.currentThread()) { + return BUSY; + } + } + return acquire(sourceChunkX, sourceChunkZ, destinationChunkX, destinationChunkZ, sourceChunkRadius, builder); + } + private ObjectSourcePlan claim(State current, long source, Entry entry, int destinationChunkX, int destinationChunkZ, Supplier builder) { int slot = entry.slot(destinationChunkX, destinationChunkZ); diff --git a/core/src/test/java/art/arcane/iris/generation/mantle/ObjectSourcePlanCacheTest.java b/core/src/test/java/art/arcane/iris/generation/mantle/ObjectSourcePlanCacheTest.java index 25d05d0b2..9c88b387e 100644 --- a/core/src/test/java/art/arcane/iris/generation/mantle/ObjectSourcePlanCacheTest.java +++ b/core/src/test/java/art/arcane/iris/generation/mantle/ObjectSourcePlanCacheTest.java @@ -163,6 +163,44 @@ public class ObjectSourcePlanCacheTest { } } + @Test + public void tryAcquireReportsBusySourcesWithoutConsumingThem() throws Exception { + ObjectSourcePlanCache cache = new ObjectSourcePlanCache(1024L * 1024L); + ObjectSourcePlan plan = planTouching(new int[]{0, 0}, new int[]{1, 0}); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + AtomicInteger builds = new AtomicInteger(); + ExecutorService executor = Executors.newFixedThreadPool(1); + try { + Future owner = executor.submit(() -> cache.acquire(0, 0, 0, 0, 7, () -> { + builds.incrementAndGet(); + entered.countDown(); + await(release); + return plan; + })); + assertTrue(entered.await(5L, TimeUnit.SECONDS)); + + assertSame(ObjectSourcePlanCache.BUSY, cache.tryAcquire(0, 0, 1, 0, 7, () -> { + builds.incrementAndGet(); + return plan; + })); + ObjectSourcePlan unrelated = planTouching(new int[]{5, 5}); + assertSame(unrelated, cache.tryAcquire(5, 5, 5, 5, 7, () -> unrelated)); + release.countDown(); + assertSame(plan, owner.get(5L, TimeUnit.SECONDS)); + + assertSame(plan, cache.tryAcquire(0, 0, 1, 0, 7, () -> { + builds.incrementAndGet(); + return plan; + })); + assertEquals(1, builds.get()); + assertEquals(2L, cache.stats().drained()); + } finally { + release.countDown(); + executor.shutdownNow(); + } + } + @Test public void concurrentDestinationsShareOneSourceBuild() throws Exception { ObjectSourcePlanCache cache = new ObjectSourcePlanCache(1024L * 1024L);