Columnar object mutation log

- Log transaction writes in parallel arrays instead of mutation records
- Replay source plans straight from their packed cells without decoding records
- Build source plans from a log slice
- Journal commit originals in a packed-key set instead of a linked map
- Buffer cave object writes in parallel arrays
- Drop the now-unused import in the runner
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-22 01:04:22 -04:00
1 parent fd47240db9
commit ad73c33480
6 files changed
+410 -191

No files matched your search

@@ -30,11 +30,12 @@ import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.math.BlockPosition; 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 it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
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_BITS = 21;
@@ -46,7 +47,16 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
private final int anchorY; private final int anchorY;
private final int minDepthBelowSurface; private final int minDepthBelowSurface;
private final int worldHeight; private final int worldHeight;
private final KList<BufferedMutation> mutations; private static final byte BLOCK = 0;
private static final byte TILE = 1;
private static final byte DATA = 2;
private int[] mutationX = new int[64];
private int[] mutationY = new int[64];
private int[] mutationZ = new int[64];
private Object[] mutationValues = new Object[64];
private byte[] mutationKinds = new byte[64];
private boolean[] mutationCanopies = new boolean[64];
private int mutationCount;
private final Long2ObjectOpenHashMap<NativeBlockState> bufferedBlocks; private final Long2ObjectOpenHashMap<NativeBlockState> bufferedBlocks;
private final Long2IntOpenHashMap surfaceHeights; private final Long2IntOpenHashMap surfaceHeights;
private final Long2IntOpenHashMap caveCeilings; private final Long2IntOpenHashMap caveCeilings;
@@ -62,7 +72,6 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
this.anchorY = anchorY; this.anchorY = anchorY;
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.bufferedBlocks = new Long2ObjectOpenHashMap<>(); this.bufferedBlocks = new Long2ObjectOpenHashMap<>();
this.surfaceHeights = new Long2IntOpenHashMap(); this.surfaceHeights = new Long2IntOpenHashMap();
this.caveCeilings = new Long2IntOpenHashMap(); this.caveCeilings = new Long2IntOpenHashMap();
@@ -81,35 +90,37 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
int previousX = 0; int previousX = 0;
int previousY = 0; int previousY = 0;
int previousZ = 0; int previousZ = 0;
for (BufferedMutation mutation : mutations) { for (int index = 0; index < mutationCount; index++) {
if (previousChecked && mutation.x() == previousX && mutation.y() == previousY && mutation.z() == previousZ) { int x = mutationX[index];
int y = mutationY[index];
int z = mutationZ[index];
if (previousChecked && x == previousX && y == previousY && z == previousZ) {
continue; continue;
} }
if (complex != null if (complex != null
&& !complex.allowsNewDiscreteContentAt(mutation.x(), mutation.z())) { && !complex.allowsNewDiscreteContentAt(x, z)) {
discard(); discard();
return CommitResult.REJECTED_TRANSITION; return CommitResult.REJECTED_TRANSITION;
} }
if (!isWithinBounds(mutation.x(), mutation.y(), mutation.z())) { if (!isWithinBounds(x, y, z)) {
discard(); discard();
return CommitResult.REJECTED_BOUNDS; return CommitResult.REJECTED_BOUNDS;
} }
SubterrainCell feature = engine.getSubterrainCell(mutation.x(), mutation.y(), mutation.z()); SubterrainCell feature = engine.getSubterrainCell(x, y, z);
if (SubterrainRasterizer.protectsPlacement(feature) if (SubterrainRasterizer.protectsPlacement(feature)
|| feature != null && feature.solid() && delegate.isSolid(mutation.x(), mutation.y(), mutation.z())) { || feature != null && feature.solid() && delegate.isSolid(x, y, z)) {
discard(); discard();
return CommitResult.REJECTED_SUBTERRAIN; return CommitResult.REJECTED_SUBTERRAIN;
} }
HydrologyCaveCell hydrology = delegate.getData( HydrologyCaveCell hydrology = delegate.getData(x, y, z, HydrologyCaveCell.class);
mutation.x(), mutation.y(), mutation.z(), HydrologyCaveCell.class);
if (hydrology != null && hydrology.protectsPlacement()) { if (hydrology != null && hydrology.protectsPlacement()) {
discard(); discard();
return CommitResult.REJECTED_HYDROLOGY; return CommitResult.REJECTED_HYDROLOGY;
} }
previousChecked = true; previousChecked = true;
previousX = mutation.x(); previousX = x;
previousY = mutation.y(); previousY = y;
previousZ = mutation.z(); previousZ = z;
} }
applyMutations(); applyMutations();
@@ -121,18 +132,26 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
boolean canopyOpen = false; boolean canopyOpen = false;
boolean skipCanopy = false; boolean skipCanopy = false;
try { try {
for (BufferedMutation mutation : mutations) { for (int index = 0; index < mutationCount; index++) {
if (mutation.value() instanceof NativeBlockState) { int x = mutationX[index];
int y = mutationY[index];
int z = mutationZ[index];
Object value = mutationValues[index];
boolean canopy = mutationCanopies[index];
if (value instanceof NativeBlockState) {
if (canopyOpen) { if (canopyOpen) {
delegate.endTreeCanopy(); delegate.endTreeCanopy();
canopyOpen = false; canopyOpen = false;
} }
skipCanopy = mutation.canopy() skipCanopy = canopy && !delegate.beginTreeCanopy(new BlockPosition(x, y, z));
&& !delegate.beginTreeCanopy(new BlockPosition(mutation.x(), mutation.y(), mutation.z())); canopyOpen = canopy && !skipCanopy;
canopyOpen = mutation.canopy() && !skipCanopy;
} }
if (!mutation.canopy() || !skipCanopy) { if (!canopy || !skipCanopy) {
mutation.apply(delegate); switch (mutationKinds[index]) {
case BLOCK -> delegate.set(x, y, z, (NativeBlockState) value);
case TILE -> delegate.setTile(x, y, z, (TileData) value);
default -> delegate.setData(x, y, z, value);
}
} }
} }
} finally { } finally {
@@ -143,11 +162,31 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
} }
void discard() { void discard() {
mutations.clear(); Arrays.fill(mutationValues, 0, mutationCount, null);
mutationCount = 0;
bufferedBlocks.clear(); bufferedBlocks.clear();
blockWrites = 0; blockWrites = 0;
} }
private void buffer(byte kind, int x, int y, int z, Object value) {
if (mutationCount == mutationValues.length) {
int capacity = mutationCount * 2;
mutationX = Arrays.copyOf(mutationX, capacity);
mutationY = Arrays.copyOf(mutationY, capacity);
mutationZ = Arrays.copyOf(mutationZ, capacity);
mutationValues = Arrays.copyOf(mutationValues, capacity);
mutationKinds = Arrays.copyOf(mutationKinds, capacity);
mutationCanopies = Arrays.copyOf(mutationCanopies, capacity);
}
mutationX[mutationCount] = x;
mutationY[mutationCount] = y;
mutationZ[mutationCount] = z;
mutationValues[mutationCount] = value;
mutationKinds[mutationCount] = kind;
mutationCanopies[mutationCount] = isCanopy(x, y, z);
mutationCount++;
}
int getCaveCeiling(int x, int z) { int getCaveCeiling(int x, int z) {
long key = Cache.key(x, z); long key = Cache.key(x, z);
int cached = caveCeilings.get(key); int cached = caveCeilings.get(key);
@@ -179,7 +218,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
if (state == null) { if (state == null) {
return; return;
} }
mutations.add(new BlockMutation(x, y, z, state, isCanopy(x, y, z))); buffer(BLOCK, x, y, z, state);
bufferedBlocks.put(bufferKey(x, y, z), state); bufferedBlocks.put(bufferKey(x, y, z), state);
blockWrites++; blockWrites++;
} }
@@ -257,7 +296,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
@Override @Override
public void setTile(int x, int y, int z, TileData tile) { public void setTile(int x, int y, int z, TileData tile) {
if (tile != null) { if (tile != null) {
mutations.add(new TileMutation(x, y, z, tile, isCanopy(x, y, z))); buffer(TILE, x, y, z, tile);
} }
} }
@@ -266,7 +305,7 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
if (data == null) { if (data == null) {
return; return;
} }
mutations.add(new DataMutation(x, y, z, data, isCanopy(x, y, z))); buffer(DATA, x, y, z, data);
if (data instanceof NativeBlockState state) { if (data instanceof NativeBlockState state) {
bufferedBlocks.put(bufferKey(x, y, z), state); bufferedBlocks.put(bufferKey(x, y, z), state);
blockWrites++; blockWrites++;
@@ -275,12 +314,11 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
@Override @Override
public <T> @Nullable T getData(int x, int y, int z, Class<T> type) { public <T> @Nullable T getData(int x, int y, int z, Class<T> type) {
for (int i = mutations.size() - 1; i >= 0; i--) { for (int index = mutationCount - 1; index >= 0; index--) {
BufferedMutation mutation = mutations.get(i); if (mutationX[index] != x || mutationY[index] != y || mutationZ[index] != z) {
if (mutation.x() != x || mutation.y() != y || mutation.z() != z) {
continue; continue;
} }
Object value = mutation.value(); Object value = mutationValues[index];
if (type.isInstance(value)) { if (type.isInstance(value)) {
return type.cast(value); return type.cast(value);
} }
@@ -356,40 +394,4 @@ final class CaveObjectPlacementTransaction implements IObjectPlacer {
REJECTED_HYDROLOGY, REJECTED_HYDROLOGY,
REJECTED_SUBTERRAIN REJECTED_SUBTERRAIN
} }
private interface BufferedMutation {
int x();
int y();
int z();
Object value();
boolean canopy();
void apply(IObjectPlacer placer);
}
private record BlockMutation(int x, int y, int z, NativeBlockState value, boolean canopy) implements BufferedMutation {
@Override
public void apply(IObjectPlacer placer) {
placer.set(x, y, z, value);
}
}
private record TileMutation(int x, int y, int z, TileData value, boolean canopy) implements BufferedMutation {
@Override
public void apply(IObjectPlacer placer) {
placer.setTile(x, y, z, value);
}
}
private record DataMutation(int x, int y, int z, Object value, boolean canopy) implements BufferedMutation {
@Override
public void apply(IObjectPlacer placer) {
placer.setData(x, y, z, value);
}
}
} }
@@ -0,0 +1,89 @@
package art.arcane.iris.generation.mantle;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
final class MutationLog {
private static final byte CUSTOM = 1;
private static final byte CANOPY = 2;
private int size;
private int[] x = new int[32];
private int[] y = new int[32];
private int[] z = new int[32];
private Object[] values = new Object[32];
private byte[] flags = new byte[32];
static MutationLog of(List<ObjectDestinationTransaction.Mutation> mutations) {
MutationLog log = new MutationLog();
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
Objects.requireNonNull(mutation, "mutation");
ObjectDestinationTransaction.DataKey key = mutation.key();
switch (mutation) {
case ObjectDestinationTransaction.SetMutation set -> {
Object value = Objects.requireNonNull(set.value(), "mutation value");
if (ObjectSourcePlan.typeOf(value) != key.type()) {
throw new IllegalArgumentException("Object mutation type " + key.type().getName()
+ " does not match its value " + value.getClass().getName());
}
log.add(key.x(), key.y(), key.z(), value, false, set.canopy());
}
case ObjectDestinationTransaction.CustomBlockMutation block -> log.add(key.x(), key.y(), key.z(),
Objects.requireNonNull(block.state(), "custom block state"), true, block.canopy());
}
}
return log;
}
void add(int blockX, int blockY, int blockZ, Object value, boolean custom, boolean canopy) {
if (size == values.length) {
int capacity = size * 2;
x = Arrays.copyOf(x, capacity);
y = Arrays.copyOf(y, capacity);
z = Arrays.copyOf(z, capacity);
values = Arrays.copyOf(values, capacity);
flags = Arrays.copyOf(flags, capacity);
}
x[size] = blockX;
y[size] = blockY;
z[size] = blockZ;
values[size] = value;
flags[size] = (byte) ((custom ? CUSTOM : 0) | (canopy ? CANOPY : 0));
size++;
}
int size() {
return size;
}
int x(int index) {
return x[index];
}
int y(int index) {
return y[index];
}
int z(int index) {
return z[index];
}
Object value(int index) {
return values[index];
}
boolean custom(int index) {
return (flags[index] & CUSTOM) != 0;
}
boolean canopy(int index) {
return (flags[index] & CANOPY) != 0;
}
Class<?> type(int index) {
return custom(index) ? NativeBlockState.class : ObjectSourcePlan.typeOf(values[index]);
}
}
@@ -8,7 +8,6 @@ import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell; import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.generation.subterrain.SubterrainRasterizer; import art.arcane.iris.generation.subterrain.SubterrainRasterizer;
import art.arcane.iris.generation.block.TileData; import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.structure.object.IObjectPlacer;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.math.BlockPosition; import art.arcane.volmlib.util.math.BlockPosition;
import art.arcane.iris.generation.block.B; import art.arcane.iris.generation.block.B;
@@ -18,10 +17,11 @@ import org.jetbrains.annotations.Nullable;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -32,7 +32,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
private final int destinationChunkX; private final int destinationChunkX;
private final int destinationChunkZ; private final int destinationChunkZ;
private final int worldHeight; private final int worldHeight;
private final List<Mutation> mutations; private final MutationLog log = new MutationLog();
private final List<PlacementRange> placements = new ArrayList<>(); private final List<PlacementRange> placements = new ArrayList<>();
private final Long2ObjectOpenHashMap<Int2ObjectOpenHashMap<OverlayCell>> overlay; private final Long2ObjectOpenHashMap<Int2ObjectOpenHashMap<OverlayCell>> overlay;
private BlockPosition canopyPosition; private BlockPosition canopyPosition;
@@ -51,7 +51,6 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
this.destinationChunkX = destinationChunkX; this.destinationChunkX = destinationChunkX;
this.destinationChunkZ = destinationChunkZ; this.destinationChunkZ = destinationChunkZ;
this.worldHeight = writer.getMantle().getWorldHeight(); this.worldHeight = writer.getMantle().getWorldHeight();
this.mutations = new ArrayList<>();
this.overlay = new Long2ObjectOpenHashMap<>(); this.overlay = new Long2ObjectOpenHashMap<>();
} }
@@ -60,11 +59,11 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
} }
int mutationCheckpoint() { int mutationCheckpoint() {
return mutations.size(); return log.size();
} }
ObjectSourcePlan sourcePlanSince(int checkpoint) { ObjectSourcePlan sourcePlanSince(int checkpoint) {
if (checkpoint < 0 || checkpoint > mutations.size()) { if (checkpoint < 0 || checkpoint > log.size()) {
throw new IllegalArgumentException("Mutation checkpoint is outside the transaction"); throw new IllegalArgumentException("Mutation checkpoint is outside the transaction");
} }
ArrayList<PlacementRange> accepted = new ArrayList<>(); ArrayList<PlacementRange> accepted = new ArrayList<>();
@@ -73,65 +72,91 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
accepted.add(new PlacementRange(placement.start() - checkpoint, placement.end() - checkpoint)); accepted.add(new PlacementRange(placement.start() - checkpoint, placement.end() - checkpoint));
} }
} }
return new ObjectSourcePlan(mutations.subList(checkpoint, mutations.size()), accepted); return new ObjectSourcePlan(log, checkpoint, accepted);
} }
@Override @Override
public int beginObjectPlacement() { public int beginObjectPlacement() {
return mutations.size(); return log.size();
} }
@Override @Override
public void endObjectPlacement(int checkpoint) { public void endObjectPlacement(int checkpoint) {
if (checkpoint >= 0 && checkpoint < mutations.size()) { if (checkpoint >= 0 && checkpoint < log.size()) {
placements.add(new PlacementRange(checkpoint, mutations.size())); placements.add(new PlacementRange(checkpoint, log.size()));
} }
} }
void apply(ObjectSourcePlan plan) { void apply(ObjectSourcePlan plan) {
int slot = plan.destinationSlot(destinationChunkX, destinationChunkZ);
if (slot < 0) {
return;
}
int baseX = destinationChunkX << 4;
int baseZ = destinationChunkZ << 4;
boolean skipCanopy = false; boolean skipCanopy = false;
for (Mutation mutation : plan.mutationsFor(destinationChunkX, destinationChunkZ)) { int end = plan.slotEnd(slot);
if (mutation.key().type() == NativeBlockState.class) { for (int index = plan.slotStart(slot); index < end; index++) {
NativeBlockState existing = mutation.canopy() ? get(mutation.key().x(), mutation.key().y(), mutation.key().z()) : null; int cell = plan.cellAt(index);
int x = baseX | ObjectSourcePlan.cellX(cell);
int y = ObjectSourcePlan.cellY(cell);
int z = baseZ | ObjectSourcePlan.cellZ(cell);
Object value = plan.valueAt(index);
boolean custom = ObjectSourcePlan.isCustom(cell);
boolean canopy = ObjectSourcePlan.isCanopy(cell);
if (custom || value instanceof NativeBlockState) {
NativeBlockState existing = canopy ? get(x, y, z) : null;
skipCanopy = existing != null && existing.isSolid(); skipCanopy = existing != null && existing.isSolid();
} }
if (mutation.canopy() && skipCanopy) { if (canopy && skipCanopy) {
continue; continue;
} }
switch (mutation) { if (custom) {
case SetMutation set -> setData(set.key().x(), set.key().y(), set.key().z(), set.value(), set); set(x, y, z, (NativeBlockState) value, true, canopy);
case CustomBlockMutation custom -> set(custom.key().x(), custom.key().y(), custom.key().z(), custom.state(), custom); } else {
setData(x, y, z, value, true, canopy);
} }
} }
} }
private void commitFenced() { private void commitFenced() {
LinkedHashMap<DataKey, Object> originals = new LinkedHashMap<>(); CommitJournal journal = new CommitJournal();
try { try {
for (Mutation mutation : mutations) { for (int position = 0; position < log.size(); position++) {
if (!isDestination(mutation.x(), mutation.z())) { int x = log.x(position);
int z = log.z(position);
if (!isDestination(x, z)) {
continue; continue;
} }
captureOriginal(originals, mutation.key()); int y = log.y(position);
if (mutation.key().type() == NativeBlockState.class) { Object value = log.value(position);
TileWrapper tile = writer.getDataIfPresent( boolean custom = log.custom(position);
mutation.key().x(), mutation.key().y(), mutation.key().z(), TileWrapper.class); Class<?> type = log.type(position);
if (tile != null) { if (!journal.captured(x, y, z, type)) {
originals.putIfAbsent(new DataKey( journal.capture(x, y, z, type, publishedOrCleared(x, y, z, type));
mutation.key().x(), mutation.key().y(), mutation.key().z(), TileWrapper.class), tile); }
if (type == NativeBlockState.class) {
if (!journal.captured(x, y, z, TileWrapper.class)) {
TileWrapper tile = writer.getDataIfPresent(x, y, z, TileWrapper.class);
if (tile != null) {
journal.capture(x, y, z, TileWrapper.class, tile);
}
} }
Identifier identifier = writer.getDataIfPresent( if (!journal.captured(x, y, z, Identifier.class)) {
mutation.key().x(), mutation.key().y(), mutation.key().z(), Identifier.class); Identifier identifier = writer.getDataIfPresent(x, y, z, Identifier.class);
if (identifier != null || mutation instanceof CustomBlockMutation) { if (identifier != null || custom) {
originals.putIfAbsent(new DataKey( journal.capture(x, y, z, Identifier.class, identifier == null ? CLEARED : identifier);
mutation.key().x(), mutation.key().y(), mutation.key().z(), Identifier.class), }
identifier == null ? CLEARED : identifier);
} }
} }
mutation.apply(writer); if (custom) {
writer.set(x, y, z, (NativeBlockState) value);
} else {
writer.setData(x, y, z, value);
}
} }
} catch (Throwable failure) { } catch (Throwable failure) {
rollback(originals, failure); journal.rollback(failure);
if (failure instanceof RuntimeException runtimeException) { if (failure instanceof RuntimeException runtimeException) {
throw runtimeException; throw runtimeException;
} }
@@ -154,10 +179,10 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
@Override @Override
public void set(int x, int y, int z, NativeBlockState state) { public void set(int x, int y, int z, NativeBlockState state) {
set(x, y, z, state, null); set(x, y, z, state, false, false);
} }
private void set(int x, int y, int z, NativeBlockState state, CustomBlockMutation replayed) { private void set(int x, int y, int z, NativeBlockState state, boolean replayed, boolean replayedCanopy) {
if (state == null) { if (state == null) {
return; return;
} }
@@ -172,8 +197,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
cell.block = baseState; cell.block = baseState;
cell.tile = CLEARED; cell.tile = CLEARED;
cell.identifier = identifier; cell.identifier = identifier;
mutations.add(replayed != null log.add(x, y, z, state, true, replayed ? replayedCanopy : isCanopy(x, y, z));
? replayed : new CustomBlockMutation(new DataKey(x, y, z, NativeBlockState.class), state, isCanopy(x, y, z)));
return; return;
} }
setData(x, y, z, state); setData(x, y, z, state);
@@ -267,10 +291,10 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
@Override @Override
public <T> void setData(int x, int y, int z, T data) { public <T> void setData(int x, int y, int z, T data) {
setData(x, y, z, data, null); setData(x, y, z, data, false, false);
} }
private void setData(int x, int y, int z, Object data, SetMutation replayed) { private void setData(int x, int y, int z, Object data, boolean replayed, boolean replayedCanopy) {
if (data == null || y < 0 || y >= worldHeight if (data == null || y < 0 || y >= worldHeight
|| protectsSubterrain(x, y, z)) { || protectsSubterrain(x, y, z)) {
return; return;
@@ -290,8 +314,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
} else { } else {
cell.put(type, data); cell.put(type, data);
} }
mutations.add(replayed != null log.add(x, y, z, data, false, replayed ? replayedCanopy : isCanopy(x, y, z));
? replayed : new SetMutation(new DataKey(x, y, z, type), data, isCanopy(x, y, z)));
} }
@Override @Override
@@ -373,18 +396,6 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
return value == null ? CLEARED : value; return value == null ? CLEARED : value;
} }
private void captureOriginal(LinkedHashMap<DataKey, Object> originals, DataKey key) {
originals.computeIfAbsent(
key,
candidate -> publishedOrCleared(
candidate.x(),
candidate.y(),
candidate.z(),
candidate.type()
)
);
}
private boolean isDestination(int x, int z) { private boolean isDestination(int x, int z) {
return (x >> 4) == destinationChunkX && (z >> 4) == destinationChunkZ; return (x >> 4) == destinationChunkX && (z >> 4) == destinationChunkZ;
} }
@@ -433,22 +444,44 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
return hydrologyGateOpen; return hydrologyGateOpen;
} }
private void rollback(LinkedHashMap<DataKey, Object> originals, Throwable failure) { private final class CommitJournal {
ArrayList<Map.Entry<DataKey, Object>> entries = new ArrayList<>(originals.entrySet()); private final Reference2IntOpenHashMap<Class<?>> typeIds = new Reference2IntOpenHashMap<>();
for (int index = entries.size() - 1; index >= 0; index--) { private final LongOpenHashSet keys = new LongOpenHashSet();
Map.Entry<DataKey, Object> entry = entries.get(index); private final ArrayList<Original> originals = new ArrayList<>();
DataKey key = entry.getKey();
try { private long key(int x, int y, int z, Class<?> type) {
writer.restoreData(key.x(), key.y(), key.z(), key.type(), int typeId = typeIds.computeIfAbsent(type, (Class<?> ignored) -> typeIds.size());
entry.getValue() == CLEARED ? null : entry.getValue()); return ((long) typeId << 32) | ((long) y << 8) | ((long) (z & 15) << 4) | (x & 15);
} catch (Throwable rollbackFailure) { }
if (rollbackFailure != failure) {
failure.addSuppressed(rollbackFailure); private boolean captured(int x, int y, int z, Class<?> type) {
return keys.contains(key(x, y, z, type));
}
private void capture(int x, int y, int z, Class<?> type, Object value) {
if (keys.add(key(x, y, z, type))) {
originals.add(new Original(x, y, z, type, value));
}
}
private void rollback(Throwable failure) {
for (int index = originals.size() - 1; index >= 0; index--) {
Original original = originals.get(index);
try {
writer.restoreData(original.x(), original.y(), original.z(), original.type(),
original.value() == CLEARED ? null : original.value());
} catch (Throwable rollbackFailure) {
if (rollbackFailure != failure) {
failure.addSuppressed(rollbackFailure);
}
} }
} }
} }
} }
private record Original(int x, int y, int z, Class<?> type, Object value) {
}
private static final class OverlayCell { private static final class OverlayCell {
private Object block; private Object block;
private Object hydrology; private Object hydrology;
@@ -520,8 +553,6 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
DataKey key(); DataKey key();
boolean canopy(); boolean canopy();
void apply(IObjectPlacer placer);
} }
record SetMutation(DataKey key, Object value, boolean canopy) implements Mutation { record SetMutation(DataKey key, Object value, boolean canopy) implements Mutation {
@@ -534,11 +565,6 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
public int z() { public int z() {
return key.z(); return key.z();
} }
@Override
public void apply(IObjectPlacer placer) {
placer.setData(key.x(), key.y(), key.z(), value);
}
} }
record CustomBlockMutation(DataKey key, NativeBlockState state, boolean canopy) implements Mutation { record CustomBlockMutation(DataKey key, NativeBlockState state, boolean canopy) implements Mutation {
@@ -551,10 +577,5 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
public int z() { public int z() {
return key.z(); return key.z();
} }
@Override
public void apply(IObjectPlacer placer) {
placer.set(key.x(), key.y(), key.z(), state);
}
} }
} }
@@ -20,7 +20,6 @@ import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Objects;
final class ObjectSourcePlan { final class ObjectSourcePlan {
static final ObjectSourcePlan EMPTY = new ObjectSourcePlan(List.of()); static final ObjectSourcePlan EMPTY = new ObjectSourcePlan(List.of());
@@ -45,18 +44,21 @@ final class ObjectSourcePlan {
private final int estimatedRetainedBytes; private final int estimatedRetainedBytes;
ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations) { ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations) {
this(mutations, List.of()); this(MutationLog.of(mutations), 0, List.of());
} }
ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations, List<ObjectDestinationTransaction.PlacementRange> accepted) { ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations, List<ObjectDestinationTransaction.PlacementRange> accepted) {
int count = mutations.size(); this(MutationLog.of(mutations), 0, accepted);
}
ObjectSourcePlan(MutationLog log, int from, List<ObjectDestinationTransaction.PlacementRange> accepted) {
int count = log.size() - from;
long[] mutationDestinations = new long[count]; long[] mutationDestinations = new long[count];
Long2IntOpenHashMap destinationSlots = new Long2IntOpenHashMap(); Long2IntOpenHashMap destinationSlots = new Long2IntOpenHashMap();
destinationSlots.defaultReturnValue(-1); destinationSlots.defaultReturnValue(-1);
LongArrayList firstSeen = new LongArrayList(); LongArrayList firstSeen = new LongArrayList();
for (int index = 0; index < count; index++) { for (int index = 0; index < count; index++) {
ObjectDestinationTransaction.Mutation mutation = Objects.requireNonNull(mutations.get(index), "mutation"); long destination = destinationKey(log.x(from + index) >> 4, log.z(from + index) >> 4);
long destination = destinationKey(mutation.x() >> 4, mutation.z() >> 4);
mutationDestinations[index] = destination; mutationDestinations[index] = destination;
if (destinationSlots.putIfAbsent(destination, firstSeen.size()) == -1) { if (destinationSlots.putIfAbsent(destination, firstSeen.size()) == -1) {
firstSeen.add(destination); firstSeen.add(destination);
@@ -80,38 +82,23 @@ final class ObjectSourcePlan {
valueIndexes = new int[count]; valueIndexes = new int[count];
ValueTable table = new ValueTable(); ValueTable table = new ValueTable();
for (int index = 0; index < count; index++) { for (int index = 0; index < count; index++) {
ObjectDestinationTransaction.Mutation mutation = mutations.get(index); int logged = from + index;
int position = cursor[destinationSlots.get(mutationDestinations[index])]++; int position = cursor[destinationSlots.get(mutationDestinations[index])]++;
positions[index] = position; positions[index] = position;
ObjectDestinationTransaction.DataKey key = mutation.key(); int y = log.y(logged);
if (key.y() < 0 || key.y() > MAXIMUM_Y) { if (y < 0 || y > MAXIMUM_Y) {
throw new IllegalArgumentException("Object mutation height is outside the plan range: " + key.y()); throw new IllegalArgumentException("Object mutation height is outside the plan range: " + y);
} }
Object value; cells[position] = (y << Y_SHIFT) | (log.custom(logged) ? CUSTOM_BIT : 0) | (log.canopy(logged) ? CANOPY_BIT : 0)
int custom; | ((log.z(logged) & 15) << 4) | (log.x(logged) & 15);
switch (mutation) { valueIndexes[position] = table.indexOf(log.value(logged));
case ObjectDestinationTransaction.SetMutation set -> {
value = Objects.requireNonNull(set.value(), "mutation value");
if (typeOf(value) != key.type()) {
throw new IllegalArgumentException("Object mutation type " + key.type().getName()
+ " does not match its value " + value.getClass().getName());
}
custom = 0;
}
case ObjectDestinationTransaction.CustomBlockMutation block -> {
value = Objects.requireNonNull(block.state(), "custom block state");
custom = CUSTOM_BIT;
}
}
cells[position] = (key.y() << Y_SHIFT) | custom | (mutation.canopy() ? CANOPY_BIT : 0) | ((key.z() & 15) << 4) | (key.x() & 15);
valueIndexes[position] = table.indexOf(value);
} }
values = table.values.toArray(); values = table.values.toArray();
ArrayList<Placement> crossing = new ArrayList<>(); ArrayList<Placement> crossing = new ArrayList<>();
for (int ordinal = 0; ordinal < accepted.size(); ordinal++) { for (int ordinal = 0; ordinal < accepted.size(); ordinal++) {
ObjectDestinationTransaction.PlacementRange range = accepted.get(ordinal); ObjectDestinationTransaction.PlacementRange range = accepted.get(ordinal);
Placement placement = crossingPlacement(ordinal, range.start(), range.end(), mutations, mutationDestinations, destinationSlots, positions); Placement placement = crossingPlacement(ordinal, range.start(), range.end(), log, from, mutationDestinations, destinationSlots, positions);
if (placement != null) { if (placement != null) {
crossing.add(placement); crossing.add(placement);
} }
@@ -147,12 +134,13 @@ final class ObjectSourcePlan {
int ordinal, int ordinal,
int start, int start,
int end, int end,
List<ObjectDestinationTransaction.Mutation> mutations, MutationLog log,
int from,
long[] mutationDestinations, long[] mutationDestinations,
Long2IntOpenHashMap destinationSlots, Long2IntOpenHashMap destinationSlots,
int[] positions int[] positions
) { ) {
if (start < 0 || end > mutations.size() || start >= end) { if (start < 0 || end > log.size() - from || start >= end) {
throw new IllegalArgumentException("Object placement range is outside the plan"); throw new IllegalArgumentException("Object placement range is outside the plan");
} }
long first = mutationDestinations[start]; long first = mutationDestinations[start];
@@ -172,11 +160,12 @@ final class ObjectSourcePlan {
int maximumX = Integer.MIN_VALUE; int maximumX = Integer.MIN_VALUE;
int maximumZ = Integer.MIN_VALUE; int maximumZ = Integer.MIN_VALUE;
for (int mutationIndex = start; mutationIndex < end; mutationIndex++) { for (int mutationIndex = start; mutationIndex < end; mutationIndex++) {
ObjectDestinationTransaction.Mutation mutation = mutations.get(mutationIndex); int mutationX = log.x(from + mutationIndex);
minimumX = Math.min(minimumX, mutation.x()); int mutationZ = log.z(from + mutationIndex);
minimumZ = Math.min(minimumZ, mutation.z()); minimumX = Math.min(minimumX, mutationX);
maximumX = Math.max(maximumX, mutation.x()); minimumZ = Math.min(minimumZ, mutationZ);
maximumZ = Math.max(maximumZ, mutation.z()); maximumX = Math.max(maximumX, mutationX);
maximumZ = Math.max(maximumZ, mutationZ);
long destination = mutationDestinations[mutationIndex]; long destination = mutationDestinations[mutationIndex];
int touchedIndex = touchedIndexes.get(destination); int touchedIndex = touchedIndexes.get(destination);
if (touchedIndex < 0) { if (touchedIndex < 0) {
@@ -216,6 +205,55 @@ final class ObjectSourcePlan {
return destinations.length == 0; return destinations.length == 0;
} }
int slotStart(int slot) {
return offsets[slot];
}
int slotEnd(int slot) {
return offsets[slot + 1];
}
int cellAt(int index) {
return cells[index];
}
Object valueAt(int index) {
return values[valueIndexes[index]];
}
static int cellX(int cell) {
return cell & 15;
}
static int cellZ(int cell) {
return (cell >>> 4) & 15;
}
static int cellY(int cell) {
return cell >>> Y_SHIFT;
}
static boolean isCustom(int cell) {
return (cell & CUSTOM_BIT) != 0;
}
static boolean isCanopy(int cell) {
return (cell & CANOPY_BIT) != 0;
}
ObjectDestinationTransaction.Mutation mutationAt(int chunkX, int chunkZ, int index) {
int cell = cells[index];
int x = (chunkX << 4) | cellX(cell);
int z = (chunkZ << 4) | cellZ(cell);
int y = cellY(cell);
Object value = valueAt(index);
return isCustom(cell)
? new ObjectDestinationTransaction.CustomBlockMutation(
new ObjectDestinationTransaction.DataKey(x, y, z, NativeBlockState.class), (NativeBlockState) value, isCanopy(cell))
: new ObjectDestinationTransaction.SetMutation(
new ObjectDestinationTransaction.DataKey(x, y, z, typeOf(value)), value, isCanopy(cell));
}
List<ObjectDestinationTransaction.Mutation> mutationsFor(int chunkX, int chunkZ) { List<ObjectDestinationTransaction.Mutation> mutationsFor(int chunkX, int chunkZ) {
int slot = destinationSlot(chunkX, chunkZ); int slot = destinationSlot(chunkX, chunkZ);
return slot < 0 ? List.of() : decode(chunkX, chunkZ, offsets[slot], offsets[slot + 1] - offsets[slot]); return slot < 0 ? List.of() : decode(chunkX, chunkZ, offsets[slot], offsets[slot + 1] - offsets[slot]);
@@ -266,19 +304,8 @@ final class ObjectSourcePlan {
private List<ObjectDestinationTransaction.Mutation> decode(int chunkX, int chunkZ, int start, int length) { private List<ObjectDestinationTransaction.Mutation> decode(int chunkX, int chunkZ, int start, int length) {
ObjectDestinationTransaction.Mutation[] decoded = new ObjectDestinationTransaction.Mutation[length]; ObjectDestinationTransaction.Mutation[] decoded = new ObjectDestinationTransaction.Mutation[length];
int baseX = chunkX << 4;
int baseZ = chunkZ << 4;
for (int index = 0; index < length; index++) { for (int index = 0; index < length; index++) {
int cell = cells[start + index]; decoded[index] = mutationAt(chunkX, chunkZ, start + index);
int x = baseX | (cell & 15);
int z = baseZ | ((cell >>> 4) & 15);
int y = cell >>> Y_SHIFT;
Object value = values[valueIndexes[start + index]];
decoded[index] = (cell & CUSTOM_BIT) != 0
? new ObjectDestinationTransaction.CustomBlockMutation(
new ObjectDestinationTransaction.DataKey(x, y, z, NativeBlockState.class), (NativeBlockState) value, (cell & CANOPY_BIT) != 0)
: new ObjectDestinationTransaction.SetMutation(
new ObjectDestinationTransaction.DataKey(x, y, z, typeOf(value)), value, (cell & CANOPY_BIT) != 0);
} }
return List.of(decoded); return List.of(decoded);
} }
@@ -30,7 +30,6 @@ import art.arcane.iris.generation.decoration.IrisVacuumSettings;
import art.arcane.iris.world.entity.IrisMarker; import art.arcane.iris.world.entity.IrisMarker;
import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.GenerationFailures;
import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume; import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
@@ -0,0 +1,81 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.testsupport.KeyedBlockState;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class MutationLogTest {
@Test
public void logKeepsPositionsValuesAndFlagsInOrder() {
NativeBlockState state = new KeyedBlockState("minecraft:stone");
MutationLog log = MutationLog.of(List.of(
new ObjectDestinationTransaction.SetMutation(new ObjectDestinationTransaction.DataKey(3, 70, -2, String.class), "marker", true),
new ObjectDestinationTransaction.CustomBlockMutation(new ObjectDestinationTransaction.DataKey(-17, 4, 40, NativeBlockState.class), state, false)));
for (int index = 0; index < 100; index++) {
log.add(index, index + 1, -index, "grow" + index, (index & 1) == 0, (index & 2) == 0);
}
assertEquals(102, log.size());
assertEquals(3, log.x(0));
assertEquals(70, log.y(0));
assertEquals(-2, log.z(0));
assertEquals("marker", log.value(0));
assertFalse(log.custom(0));
assertTrue(log.canopy(0));
assertSame(String.class, log.type(0));
assertSame(state, log.value(1));
assertTrue(log.custom(1));
assertFalse(log.canopy(1));
assertSame(NativeBlockState.class, log.type(1));
assertEquals(99, log.x(101));
assertEquals("grow99", log.value(101));
assertFalse(log.custom(101));
assertTrue(log.canopy(98));
assertFalse(log.canopy(100));
}
@Test
public void mismatchedMutationTypesAreRejected() {
assertThrows(IllegalArgumentException.class, () -> MutationLog.of(List.of(
new ObjectDestinationTransaction.SetMutation(new ObjectDestinationTransaction.DataKey(0, 4, 0, Integer.class), "text", false))));
}
@Test
public void planSlicesMatchThePlanBuiltFromTheSameMutations() {
MutationLog log = new MutationLog();
List<ObjectDestinationTransaction.Mutation> tail = new ArrayList<>();
for (int index = 0; index < 64; index++) {
int x = (index * 7) - 30;
int y = 4 + index;
int z = (index * 5) - 20;
String value = "value" + (index % 9);
boolean canopy = index % 4 == 0;
log.add(x, y, z, value, false, canopy);
if (index >= 10) {
tail.add(new ObjectDestinationTransaction.SetMutation(new ObjectDestinationTransaction.DataKey(x, y, z, String.class), value, canopy));
}
}
ObjectSourcePlan sliced = new ObjectSourcePlan(log, 10, List.of());
ObjectSourcePlan expected = new ObjectSourcePlan(tail);
assertEquals(expected.destinationCount(), sliced.destinationCount());
int total = 0;
for (int chunkX = -3; chunkX <= 30; chunkX++) {
for (int chunkZ = -3; chunkZ <= 30; chunkZ++) {
List<ObjectDestinationTransaction.Mutation> mutations = sliced.mutationsFor(chunkX, chunkZ);
assertEquals(expected.mutationsFor(chunkX, chunkZ), mutations);
total += mutations.size();
}
}
assertEquals(54, total);
}
}