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
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-22 00:03:34 -04:00
1 parent 943c8d67e5
commit be30285718
5 files changed
+135 -26

No files matched your search

@@ -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<BufferedMutation> mutations;
private final Map<PositionKey, NativeBlockState> bufferedBlocks;
private final Long2ObjectOpenHashMap<NativeBlockState> 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) {
}
}
@@ -88,6 +88,8 @@ public class MantleObjectComponent extends IrisMantleComponent {
private static final Map<String, CaveRejectLogState> CAVE_REJECT_LOG_STATE = new ConcurrentHashMap<>();
private static final Set<String> 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<NativeBlockState, TreeBlockMaterial> 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();
}
@@ -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) {
@@ -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<ObjectSourcePlan> 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<ObjectSourcePlan> builder) {
int slot = entry.slot(destinationChunkX, destinationChunkZ);
@@ -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<ObjectSourcePlan> 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);