mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 05:50:39 +00:00
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:
1 parent
943c8d67e5
commit
be30285718
5 files changed
+135
-26
No files matched your search
+40
-11
@@ -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();
|
||||
}
|
||||
|
||||
+15
-1
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user