From 13ce46a0867e348f7b47fa476d6c0aade5c62c4b Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Mon, 4 Sep 2023 16:14:33 -0700 Subject: [PATCH] Optimise BitSetDiscreteVoxelShape#forAllBoxes This should speed up general shape joining, as VoxelShape#optimize() uses this method when performing an uncached toAabbs() --- .../moonrise/common/util/FlatBitsetUtil.java | 109 ++++++++++++++++ .../common/util/MixinWorkarounds.java | 10 ++ .../BitSetDiscreteVoxelShapeMixin.java | 122 ++++++++++++++++++ .../moonrise/mixin/collisions/BlockMixin.java | 2 +- .../collisions/DiscreteVoxelShapeMixin.java | 32 +++-- .../moonrise/mixin/collisions/LevelMixin.java | 5 +- .../mixin/collisions/ShapesMixin.java | 7 +- .../mixin/collisions/VoxelShapeMixin.java | 1 + .../mixin/hopper/HopperBlockEntityMixin.java | 2 +- .../patches/render/VisibilityGraph.java | 7 +- src/main/resources/moonrise.mixins.json | 1 + 11 files changed, 278 insertions(+), 20 deletions(-) create mode 100644 src/main/java/ca/spottedleaf/moonrise/common/util/FlatBitsetUtil.java create mode 100644 src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java create mode 100644 src/main/java/ca/spottedleaf/moonrise/mixin/collisions/BitSetDiscreteVoxelShapeMixin.java diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/FlatBitsetUtil.java b/src/main/java/ca/spottedleaf/moonrise/common/util/FlatBitsetUtil.java new file mode 100644 index 0000000..24fcd3e --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/util/FlatBitsetUtil.java @@ -0,0 +1,109 @@ +package ca.spottedleaf.moonrise.common.util; + +import java.util.Objects; + +public final class FlatBitsetUtil { + + private static final int LOG2_LONG = 6; + private static final long ALL_SET = -1L; + private static final int BITS_PER_LONG = Long.SIZE; + + // from inclusive + // to exclusive + public static int firstSet(final long[] bitset, final int from, final int to) { + if ((from | to | (to - from)) < 0) { + throw new IndexOutOfBoundsException(); + } + + int bitsetIdx = from >>> LOG2_LONG; + int bitIdx = from & ~(BITS_PER_LONG - 1); + + long tmp = bitset[bitsetIdx] & (ALL_SET << from); + for (;;) { + if (tmp != 0L) { + final int ret = bitIdx | Long.numberOfTrailingZeros(tmp); + return ret >= to ? -1 : ret; + } + + bitIdx += BITS_PER_LONG; + + if (bitIdx >= to) { + return -1; + } + + tmp = bitset[++bitsetIdx]; + } + } + + // from inclusive + // to exclusive + public static int firstClear(final long[] bitset, final int from, final int to) { + if ((from | to | (to - from)) < 0) { + throw new IndexOutOfBoundsException(); + } + // like firstSet, but invert the bitset + + int bitsetIdx = from >>> LOG2_LONG; + int bitIdx = from & ~(BITS_PER_LONG - 1); + + long tmp = (~bitset[bitsetIdx]) & (ALL_SET << from); + for (;;) { + if (tmp != 0L) { + final int ret = bitIdx | Long.numberOfTrailingZeros(tmp); + return ret >= to ? -1 : ret; + } + + bitIdx += BITS_PER_LONG; + + if (bitIdx >= to) { + return -1; + } + + tmp = ~bitset[++bitsetIdx]; + } + } + + // from inclusive + // to exclusive + public static void clearRange(final long[] bitset, final int from, int to) { + if ((from | to | (to - from)) < 0) { + throw new IndexOutOfBoundsException(); + } + + if (from == to) { + return; + } + + --to; + + final int fromBitsetIdx = from >>> LOG2_LONG; + final int toBitsetIdx = to >>> LOG2_LONG; + + final long keepFirst = ALL_SET >>> ((BITS_PER_LONG - 1) ^ from); + final long keepLast = ALL_SET << to; + + Objects.checkFromToIndex(fromBitsetIdx, toBitsetIdx, bitset.length); + + if (fromBitsetIdx == toBitsetIdx) { + // special case: need to keep both first and last + bitset[fromBitsetIdx] &= (keepFirst | keepLast); + } else { + bitset[fromBitsetIdx] &= keepFirst; + + for (int i = fromBitsetIdx + 1; i < toBitsetIdx; ++i) { + bitset[toBitsetIdx] = 0L; + } + + bitset[toBitsetIdx] &= keepLast; + } + } + + // from inclusive + // to exclusive + public static boolean isRangeSet(final long[] bitset, final int from, final int to) { + return firstClear(bitset, from, to) == -1; + } + + + private FlatBitsetUtil() {} +} diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java new file mode 100644 index 0000000..ac6f284 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MixinWorkarounds.java @@ -0,0 +1,10 @@ +package ca.spottedleaf.moonrise.common.util; + +public final class MixinWorkarounds { + + // mixins tries to find the owner of the clone() method, which doesn't exist and NPEs + public static long[] clone(final long[] values) { + return values.clone(); + } + +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/BitSetDiscreteVoxelShapeMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/BitSetDiscreteVoxelShapeMixin.java new file mode 100644 index 0000000..2d85b72 --- /dev/null +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/BitSetDiscreteVoxelShapeMixin.java @@ -0,0 +1,122 @@ +package ca.spottedleaf.moonrise.mixin.collisions; + +import ca.spottedleaf.moonrise.common.util.FlatBitsetUtil; +import ca.spottedleaf.moonrise.common.util.MixinWorkarounds; +import ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData; +import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape; +import net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape; +import net.minecraft.world.phys.shapes.DiscreteVoxelShape; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; + +@Mixin(BitSetDiscreteVoxelShape.class) +public abstract class BitSetDiscreteVoxelShapeMixin extends DiscreteVoxelShape { + + protected BitSetDiscreteVoxelShapeMixin(int i, int j, int k) { + super(i, j, k); + } + + /** + * @reason avoid creating temporary shape, interact directly with the bitset + * @author Spottedleaf + */ + @Overwrite + public static void forAllBoxes(final DiscreteVoxelShape shape, final DiscreteVoxelShape.IntLineConsumer consumer, final boolean mergeAdjacent) { + // called with the shape of a VoxelShape, so we can expect the cache to exist + final CachedShapeData cache = ((CollisionDiscreteVoxelShape)shape).getOrCreateCachedShapeData(); + + final int sizeX = cache.sizeX(); + final int sizeY = cache.sizeY(); + final int sizeZ = cache.sizeZ(); + + int indexX; + int indexY = 0; + int indexZ; + + int incY = sizeZ; + int incX = sizeZ*sizeY; + + long[] bitset = cache.voxelSet(); + + // index = z + y*size_z + x*(size_z*size_y) + + if (!mergeAdjacent) { + // due to the odd selection of loop order (which does affect behavior, unfortunately) we can't simply + // increment an index in the Z loop, and have to perform this trash (keeping track of 3 counters) to avoid + // the multiplication + for (int y = 0; y < sizeY; ++y, indexY += incY) { + indexX = indexY; + for (int x = 0; x < sizeX; ++x, indexX += incX) { + indexZ = indexX; + for (int z = 0; z < sizeZ; ++z, ++indexZ) { + if ((bitset[indexZ >>> 6] & (1L << indexZ)) != 0L) { + consumer.consume(x, y, z, x + 1, y + 1, z + 1); + } + } + } + } + } else { + // same notes about loop order as the above + // this branch is actually important to optimise, as it affects uncached toAabbs() (which affects optimize()) + + // only clone when we may write to it + bitset = MixinWorkarounds.clone(bitset); + + for (int y = 0; y < sizeY; ++y, indexY += incY) { + indexX = indexY; + for (int x = 0; x < sizeX; ++x, indexX += incX) { + final int startIndex = indexX; + final int endIndex = indexX + sizeZ; + + final int firstSetZ = FlatBitsetUtil.firstSet(bitset, startIndex, endIndex); + + if (firstSetZ == -1) { + continue; + } + + int lastSetZ = FlatBitsetUtil.firstClear(bitset, firstSetZ, endIndex); + if (lastSetZ == -1) { + lastSetZ = endIndex; + } + + FlatBitsetUtil.clearRange(bitset, firstSetZ, lastSetZ); + + // try to merge neighbouring on the X axis + int endX = x + 1; // exclusive + for (int neighbourIdxStart = firstSetZ + incX, neighbourIdxEnd = lastSetZ + incX; + endX < sizeX && FlatBitsetUtil.isRangeSet(bitset, neighbourIdxStart, neighbourIdxEnd); + neighbourIdxStart += incX, neighbourIdxEnd += incX) { + + ++endX; + FlatBitsetUtil.clearRange(bitset, neighbourIdxStart, neighbourIdxEnd); + } + + // try to merge neighbouring on the Y axis + + int endY; // exclusive + int firstSetZY, lastSetZY; + y_merge: + for (endY = y + 1, firstSetZY = firstSetZ + incY, lastSetZY = lastSetZ + incY; endY < sizeY; + ++endY, firstSetZY += incY, lastSetZY += incY) { + + // test the whole XZ range + for (int testX = x, start = firstSetZY, end = lastSetZY; testX < endX; + ++testX, start += incX, end += incX) { + if (!FlatBitsetUtil.isRangeSet(bitset, start, end)) { + break y_merge; + } + } + + // passed, so we can clear it + for (int testX = x, start = firstSetZY, end = lastSetZY; testX < endX; + ++testX, start += incX, end += incX) { + FlatBitsetUtil.clearRange(bitset, start, end); + } + } + + consumer.consume(x, y, firstSetZ - startIndex, endX, endY, lastSetZ - startIndex); + } + } + } + } +} diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/BlockMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/BlockMixin.java index f947531..a6ab664 100644 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/BlockMixin.java +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/BlockMixin.java @@ -10,7 +10,7 @@ import org.spongepowered.asm.mixin.Overwrite; public abstract class BlockMixin { /** - * @reason Replace with an implementation that does not use join AND one that caches the result + * @reason Replace with an implementation that does not use join AND one that caches the result per VoxelShape instance * @author Spottedleaf */ @Overwrite diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/DiscreteVoxelShapeMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/DiscreteVoxelShapeMixin.java index 6834f9e..d601c9e 100644 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/DiscreteVoxelShapeMixin.java +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/DiscreteVoxelShapeMixin.java @@ -3,9 +3,11 @@ package ca.spottedleaf.moonrise.mixin.collisions; import ca.spottedleaf.moonrise.patches.collisions.shape.CachedShapeData; import ca.spottedleaf.moonrise.patches.collisions.shape.CollisionDiscreteVoxelShape; import net.minecraft.core.Direction; +import net.minecraft.world.phys.shapes.BitSetDiscreteVoxelShape; import net.minecraft.world.phys.shapes.DiscreteVoxelShape; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; +import java.util.Arrays; @Mixin(DiscreteVoxelShape.class) public abstract class DiscreteVoxelShapeMixin implements CollisionDiscreteVoxelShape { @@ -29,19 +31,29 @@ public abstract class DiscreteVoxelShapeMixin implements CollisionDiscreteVoxelS final int maxIndex = sizeX * sizeY * sizeZ; // exclusive final int longsRequired = (maxIndex + (Long.SIZE - 1)) >>> 6; - final long[] voxelSet = new long[longsRequired]; + long[] voxelSet; final boolean isEmpty = discreteVoxelShape.isEmpty(); - if (!isEmpty) { - final int mulX = sizeZ * sizeY; - for (int x = 0; x < sizeX; ++x) { - for (int y = 0; y < sizeY; ++y) { - for (int z = 0; z < sizeZ; ++z) { - if (discreteVoxelShape.isFull(x, y, z)) { - // index = z + y*size_z + x*(size_z*size_y) - final int index = z + y*sizeZ + x*mulX; - voxelSet[index >>> 6] |= 1L << index; + if (discreteVoxelShape instanceof BitSetDiscreteVoxelShape bitsetShape) { + voxelSet = bitsetShape.storage.toLongArray(); + if (voxelSet.length < longsRequired) { + // happens when the later long values are 0L, so we need to resize + voxelSet = Arrays.copyOf(voxelSet, longsRequired); + } + } else { + voxelSet = new long[longsRequired]; + if (!isEmpty) { + final int mulX = sizeZ * sizeY; + for (int x = 0; x < sizeX; ++x) { + for (int y = 0; y < sizeY; ++y) { + for (int z = 0; z < sizeZ; ++z) { + if (discreteVoxelShape.isFull(x, y, z)) { + // index = z + y*size_z + x*(size_z*size_y) + final int index = z + y * sizeZ + x * mulX; + + voxelSet[index >>> 6] |= 1L << index; + } } } } diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/LevelMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/LevelMixin.java index be681ec..50c7647 100644 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/LevelMixin.java +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/LevelMixin.java @@ -421,7 +421,7 @@ public abstract class LevelMixin implements CollisionLevel, CollisionEntityGette } /** - * @reason Use optimised merge strategy, avoid streams + * @reason Use optimised OR operator join strategy, avoid streams * @author Spottedleaf */ @Override @@ -464,7 +464,8 @@ public abstract class LevelMixin implements CollisionLevel, CollisionEntityGette final VoxelShape joined = Shapes.or(first, rest); // find free space - final VoxelShape freeSpace = Shapes.join(boundsShape, joined, BooleanOp.ONLY_FIRST); + // don't need optimized join, as closestPointTo uses toAabbs() + final VoxelShape freeSpace = Shapes.joinUnoptimized(boundsShape, joined, BooleanOp.ONLY_FIRST); return freeSpace.closestPointTo(fromPosition); } diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/ShapesMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/ShapesMixin.java index 0580a0c..11a469f 100644 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/ShapesMixin.java +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/ShapesMixin.java @@ -168,7 +168,7 @@ public abstract class ShapesMixin { final VoxelShape first = tmp[i]; final VoxelShape second = tmp[next]; - tmp[newSize++] = Shapes.join(first, second, BooleanOp.OR); + tmp[newSize++] = Shapes.or(first, second); } } size = newSize; @@ -269,10 +269,11 @@ public abstract class ShapesMixin { final AABB bounds2 = shape2.bounds(); final double minX = Math.min(bounds1.minX, bounds2.minX); - final double maxX = Math.max(bounds1.maxX, bounds2.maxX); final double minY = Math.min(bounds1.minY, bounds2.minY); - final double maxY = Math.max(bounds1.maxY, bounds2.maxY); final double minZ = Math.min(bounds1.minZ, bounds2.minZ); + + final double maxX = Math.max(bounds1.maxX, bounds2.maxX); + final double maxY = Math.max(bounds1.maxY, bounds2.maxY); final double maxZ = Math.max(bounds1.maxZ, bounds2.maxZ); return (minX <= CollisionUtil.COLLISION_EPSILON && maxX >= (1 - CollisionUtil.COLLISION_EPSILON)) && diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/VoxelShapeMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/VoxelShapeMixin.java index dfa6c74..a20ebce 100644 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/VoxelShapeMixin.java +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/collisions/VoxelShapeMixin.java @@ -657,6 +657,7 @@ public abstract class VoxelShapeMixin implements CollisionVoxelShape { } if (this.singleAABBRepresentation != null) { + // note: the isFullBlock() is fuzzy, and Shapes.create() is also fuzzy which would return block() return this.isFullBlock() ? Shapes.block() : (VoxelShape)(Object)this; } diff --git a/src/main/java/ca/spottedleaf/moonrise/mixin/hopper/HopperBlockEntityMixin.java b/src/main/java/ca/spottedleaf/moonrise/mixin/hopper/HopperBlockEntityMixin.java index d63bea8..6d7e9ea 100644 --- a/src/main/java/ca/spottedleaf/moonrise/mixin/hopper/HopperBlockEntityMixin.java +++ b/src/main/java/ca/spottedleaf/moonrise/mixin/hopper/HopperBlockEntityMixin.java @@ -283,7 +283,7 @@ public abstract class HopperBlockEntityMixin extends RandomizableContainerBlockE } if (changed) { - ((HopperBlockEntityMixin)(Object)hopperBlockEntity).setCooldown(8); + ((HopperBlockEntityMixin)(Object)hopperBlockEntity).setCooldown(HopperBlockEntity.MOVE_ITEM_SPEED); setChanged(level, blockPos, blockState); return true; } diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/render/VisibilityGraph.java b/src/main/java/ca/spottedleaf/moonrise/patches/render/VisibilityGraph.java index f16bcb6..f7dd41f 100644 --- a/src/main/java/ca/spottedleaf/moonrise/patches/render/VisibilityGraph.java +++ b/src/main/java/ca/spottedleaf/moonrise/patches/render/VisibilityGraph.java @@ -175,7 +175,7 @@ public final class VisibilityGraph { ret.setAll(true); return ret; } - if (this.opaqueEdgeCount >= (TOTAL_BLOCKS_FACES - 1)) { + if (this.opaqueEdgeCount == TOTAL_BLOCKS_FACES) { // impossible for there to be any visibility from one edge to another ret.setAll(false); return ret; @@ -218,12 +218,13 @@ public final class VisibilityGraph { final int directionOrdinal = Integer.numberOfTrailingZeros(negatedBitset); final int add = INDEX_ADD_BY_DIRECTION_ORDINAL[directionOrdinal]; - negatedBitset ^= -negatedBitset & negatedBitset; // remove trailing bit - final int neighbourIndex = add + queuedIndex; final long bitsetValue = opaque[neighbourIndex >>> LOG2_LONG]; final int directionBitsetForNeighbour = DIRECTIONS_BITSET_BY_INDEX[neighbourIndex]; + + negatedBitset ^= -negatedBitset & negatedBitset; // remove trailing bit + final long neighbourMask = 1L << neighbourIndex; final long newBitsetValue = bitsetValue | neighbourMask; diff --git a/src/main/resources/moonrise.mixins.json b/src/main/resources/moonrise.mixins.json index 7bc318b..714b4b5 100644 --- a/src/main/resources/moonrise.mixins.json +++ b/src/main/resources/moonrise.mixins.json @@ -10,6 +10,7 @@ "chunk_getblock.LevelChunkMixin", "collisions.ArmorStandMixin", "collisions.ArrayVoxelShapeMixin", + "collisions.BitSetDiscreteVoxelShapeMixin", "collisions.BlockMixin", "collisions.BlockStateBaseMixin", "collisions.CollisionGetterMixin",