mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 14:30:14 +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.iris.generation.block.B;
|
||||||
import art.arcane.volmlib.util.collection.KList;
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||||
|
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
||||||
private static final int CACHE_MISS = Integer.MIN_VALUE;
|
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 IObjectPlacer delegate;
|
||||||
private final Engine engine;
|
private final Engine engine;
|
||||||
@@ -46,11 +47,14 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
private final int minDepthBelowSurface;
|
private final int minDepthBelowSurface;
|
||||||
private final int worldHeight;
|
private final int worldHeight;
|
||||||
private final KList<BufferedMutation> mutations;
|
private final KList<BufferedMutation> mutations;
|
||||||
private final Map<PositionKey, NativeBlockState> bufferedBlocks;
|
private final Long2ObjectOpenHashMap<NativeBlockState> bufferedBlocks;
|
||||||
private final Long2IntOpenHashMap surfaceHeights;
|
private final Long2IntOpenHashMap surfaceHeights;
|
||||||
private final Long2IntOpenHashMap caveCeilings;
|
private final Long2IntOpenHashMap caveCeilings;
|
||||||
private int blockWrites;
|
private int blockWrites;
|
||||||
private BlockPosition canopyPosition;
|
private BlockPosition canopyPosition;
|
||||||
|
private boolean originKnown;
|
||||||
|
private int originX;
|
||||||
|
private int originZ;
|
||||||
|
|
||||||
CaveObjectPlacementTransaction(IObjectPlacer delegate, int anchorY, int minDepthBelowSurface) {
|
CaveObjectPlacementTransaction(IObjectPlacer delegate, int anchorY, int minDepthBelowSurface) {
|
||||||
this.delegate = delegate;
|
this.delegate = delegate;
|
||||||
@@ -59,7 +63,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
this.minDepthBelowSurface = Math.max(0, minDepthBelowSurface);
|
this.minDepthBelowSurface = Math.max(0, minDepthBelowSurface);
|
||||||
this.worldHeight = engine == null ? 0 : engine.getHeight();
|
this.worldHeight = engine == null ? 0 : engine.getHeight();
|
||||||
this.mutations = new KList<>();
|
this.mutations = new KList<>();
|
||||||
this.bufferedBlocks = new HashMap<>();
|
this.bufferedBlocks = new Long2ObjectOpenHashMap<>();
|
||||||
this.surfaceHeights = new Long2IntOpenHashMap();
|
this.surfaceHeights = new Long2IntOpenHashMap();
|
||||||
this.caveCeilings = new Long2IntOpenHashMap();
|
this.caveCeilings = new Long2IntOpenHashMap();
|
||||||
this.surfaceHeights.defaultReturnValue(CACHE_MISS);
|
this.surfaceHeights.defaultReturnValue(CACHE_MISS);
|
||||||
@@ -73,7 +77,14 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
IrisComplex complex = engine == null ? null : engine.getComplex();
|
IrisComplex complex = engine == null ? null : engine.getComplex();
|
||||||
|
boolean previousChecked = false;
|
||||||
|
int previousX = 0;
|
||||||
|
int previousY = 0;
|
||||||
|
int previousZ = 0;
|
||||||
for (BufferedMutation mutation : mutations) {
|
for (BufferedMutation mutation : mutations) {
|
||||||
|
if (previousChecked && mutation.x() == previousX && mutation.y() == previousY && mutation.z() == previousZ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (complex != null
|
if (complex != null
|
||||||
&& !complex.allowsNewDiscreteContentAt(mutation.x(), mutation.z())) {
|
&& !complex.allowsNewDiscreteContentAt(mutation.x(), mutation.z())) {
|
||||||
discard();
|
discard();
|
||||||
@@ -95,6 +106,10 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
discard();
|
discard();
|
||||||
return CommitResult.REJECTED_HYDROLOGY;
|
return CommitResult.REJECTED_HYDROLOGY;
|
||||||
}
|
}
|
||||||
|
previousChecked = true;
|
||||||
|
previousX = mutation.x();
|
||||||
|
previousY = mutation.y();
|
||||||
|
previousZ = mutation.z();
|
||||||
}
|
}
|
||||||
|
|
||||||
applyMutations();
|
applyMutations();
|
||||||
@@ -165,7 +180,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mutations.add(new BlockMutation(x, y, z, state, isCanopy(x, y, z)));
|
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++;
|
blockWrites++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +205,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NativeBlockState get(int x, int y, int z) {
|
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;
|
return buffered == null ? delegate.get(x, y, z) : buffered;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +226,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isSolid(int x, int y, int z) {
|
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);
|
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)));
|
mutations.add(new DataMutation(x, y, z, data, isCanopy(x, y, z)));
|
||||||
if (data instanceof NativeBlockState state) {
|
if (data instanceof NativeBlockState state) {
|
||||||
bufferedBlocks.put(new PositionKey(x, y, z), state);
|
bufferedBlocks.put(bufferKey(x, y, z), state);
|
||||||
blockWrites++;
|
blockWrites++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,6 +293,22 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
|
|||||||
return engine;
|
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) {
|
private boolean isWithinBounds(int x, int y, int z) {
|
||||||
if (engine == null || y < 0 || y >= worldHeight) {
|
if (engine == null || y < 0 || y >= worldHeight) {
|
||||||
return false;
|
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 Map<String, CaveRejectLogState> CAVE_REJECT_LOG_STATE = new ConcurrentHashMap<>();
|
||||||
private static final Set<String> MISSING_LOAD_KEY_WARNED = ConcurrentHashMap.newKeySet();
|
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 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 Object collisionRuleLock;
|
||||||
private final ObjectSourcePlanCache sourcePlans;
|
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) {
|
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
|
@Override
|
||||||
@@ -157,19 +166,23 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
|||||||
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, x, z);
|
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, x, z);
|
||||||
int radius = getRadius();
|
int radius = getRadius();
|
||||||
int sourceChunkRadius = sourceChunkRadius(radius);
|
int sourceChunkRadius = sourceChunkRadius(radius);
|
||||||
replaySourceChunks(
|
int side = (sourceChunkRadius * 2) + 1;
|
||||||
x,
|
ObjectSourcePlan[] plans = new ObjectSourcePlan[side * side];
|
||||||
z,
|
int[] slot = new int[1];
|
||||||
radius,
|
replaySourceChunks(x, z, radius, (sourceX, sourceZ) -> plans[slot[0]++] = sourcePlans.tryAcquire(
|
||||||
(sourceX, sourceZ) -> {
|
sourceX, sourceZ, x, z, sourceChunkRadius, () -> buildSourcePlan(writer, sourceX, sourceZ, context)));
|
||||||
ObjectSourcePlan plan = sourcePlans.acquire(sourceX, sourceZ, x, z, sourceChunkRadius,
|
slot[0] = 0;
|
||||||
() -> buildSourcePlan(writer, sourceX, sourceZ, context));
|
replaySourceChunks(x, z, radius, (sourceX, sourceZ) -> {
|
||||||
if (plan != null) {
|
ObjectSourcePlan plan = plans[slot[0]++];
|
||||||
transaction.apply(plan);
|
if (plan == ObjectSourcePlanCache.BUSY) {
|
||||||
plan.persistContinuations(writer, sourceX, sourceZ, x, z);
|
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();
|
transaction.commit();
|
||||||
logSourcePlanStats();
|
logSourcePlanStats();
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-1
@@ -40,6 +40,11 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
|||||||
private int hydrologyGateChunkX;
|
private int hydrologyGateChunkX;
|
||||||
private int hydrologyGateChunkZ;
|
private int hydrologyGateChunkZ;
|
||||||
private boolean hydrologyGateOpen;
|
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) {
|
ObjectDestinationTransaction(MantleWriter writer, int destinationChunkX, int destinationChunkZ) {
|
||||||
this.writer = writer;
|
this.writer = writer;
|
||||||
@@ -396,8 +401,17 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean protectsSubterrain(int x, int y, int z) {
|
private boolean protectsSubterrain(int x, int y, int z) {
|
||||||
|
if (subterrainKnown && x == subterrainX && y == subterrainY && z == subterrainZ) {
|
||||||
|
return subterrainProtects;
|
||||||
|
}
|
||||||
Engine engine = getEngine();
|
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) {
|
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 com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
import art.arcane.volmlib.util.cache.CacheKey;
|
import art.arcane.volmlib.util.cache.CacheKey;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CompletionException;
|
import java.util.concurrent.CompletionException;
|
||||||
@@ -14,6 +15,7 @@ import java.util.function.Supplier;
|
|||||||
import static art.arcane.iris.generation.cache.Cache.key;
|
import static art.arcane.iris.generation.cache.Cache.key;
|
||||||
|
|
||||||
final class ObjectSourcePlanCache {
|
final class ObjectSourcePlanCache {
|
||||||
|
static final ObjectSourcePlan BUSY = new ObjectSourcePlan(List.of());
|
||||||
private static final long MAXIMUM_RETAINED_BYTES = 512L * 1024L * 1024L;
|
private static final long MAXIMUM_RETAINED_BYTES = 512L * 1024L * 1024L;
|
||||||
private static final int ENTRY_BYTES = 64;
|
private static final int ENTRY_BYTES = 64;
|
||||||
|
|
||||||
@@ -68,6 +70,19 @@ final class ObjectSourcePlanCache {
|
|||||||
return plan;
|
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,
|
private ObjectSourcePlan claim(State current, long source, Entry entry, int destinationChunkX, int destinationChunkZ,
|
||||||
Supplier<ObjectSourcePlan> builder) {
|
Supplier<ObjectSourcePlan> builder) {
|
||||||
int slot = entry.slot(destinationChunkX, destinationChunkZ);
|
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
|
@Test
|
||||||
public void concurrentDestinationsShareOneSourceBuild() throws Exception {
|
public void concurrentDestinationsShareOneSourceBuild() throws Exception {
|
||||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(1024L * 1024L);
|
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(1024L * 1024L);
|
||||||
|
|||||||
Reference in new issue
Block a user