瀏覽代碼

New: Deduplicate voxel shapes in blockstate caches

malte0811 4 年之前
父節點
當前提交
875bbe2e0d

+ 1 - 1
build.gradle

@@ -23,7 +23,7 @@ version = "${mod_version}"
 group = "malte0811.${modid}" // http://maven.apache.org/guides/mini/guide-naming-conventions.html
 archivesBaseName = "${modid}"
 
-def mixinConfigs = ["predicates", "fastmap", "nopropertymap", "mrl", "dedupmultipart"].stream()
+def mixinConfigs = ["predicates", "fastmap", "nopropertymap", "mrl", "dedupmultipart", "blockstatecache"].stream()
         .map({s -> archivesBaseName+"."+s+".mixin.json"})
         .collect(Collectors.toList())
 

+ 1 - 1
gradle.properties

@@ -2,7 +2,7 @@
 # This is required to provide enough memory for the Minecraft decompilation process.
 org.gradle.jvmargs=-Xmx3G
 org.gradle.daemon=false
-mod_version=1.1.1
+mod_version=1.2
 modid=ferritecore
 mc_version=1.16.5
 forge_version=36.0.0

+ 45 - 0
src/main/java/malte0811/ferritecore/hash/VoxelShapeArrayHash.java

@@ -0,0 +1,45 @@
+package malte0811.ferritecore.hash;
+
+import it.unimi.dsi.fastutil.Hash;
+import malte0811.ferritecore.mixin.blockstatecache.VSArrayAccess;
+import malte0811.ferritecore.mixin.blockstatecache.VoxelShapeAccess;
+import net.minecraft.util.math.shapes.VoxelShape;
+import net.minecraft.util.math.shapes.VoxelShapeArray;
+import net.minecraft.util.math.shapes.VoxelShapePart;
+
+import java.util.Objects;
+
+public class VoxelShapeArrayHash implements Hash.Strategy<VoxelShapeArray> {
+    public static final VoxelShapeArrayHash INSTANCE = new VoxelShapeArrayHash();
+
+    @Override
+    public int hashCode(VoxelShapeArray o) {
+        VSArrayAccess access = access(o);
+        return 31 * Objects.hash(access.getXPoints(), access.getYPoints(), access.getZPoints())
+                + VoxelShapePartHash.INSTANCE.hashCode(getPart(o));
+    }
+
+    @Override
+    public boolean equals(VoxelShapeArray a, VoxelShapeArray b) {
+        if (a == b) {
+            return true;
+        } else if (a == null || b == null) {
+            return false;
+        }
+        VSArrayAccess accessA = access(a);
+        VSArrayAccess accessB = access(b);
+        return Objects.equals(accessA.getXPoints(), accessB.getXPoints()) &&
+                Objects.equals(accessA.getYPoints(), accessB.getYPoints()) &&
+                Objects.equals(accessA.getZPoints(), accessB.getZPoints()) &&
+                VoxelShapePartHash.INSTANCE.equals(getPart(a), getPart(b));
+    }
+
+    @SuppressWarnings("ConstantConditions")
+    private static VSArrayAccess access(VoxelShapeArray a) {
+        return (VSArrayAccess) (Object) a;
+    }
+
+    private static VoxelShapePart getPart(VoxelShape a) {
+        return ((VoxelShapeAccess) a).getPart();
+    }
+}

+ 48 - 0
src/main/java/malte0811/ferritecore/hash/VoxelShapeHash.java

@@ -0,0 +1,48 @@
+package malte0811.ferritecore.hash;
+
+import it.unimi.dsi.fastutil.Hash;
+import malte0811.ferritecore.mixin.blockstatecache.VoxelShapeAccess;
+import net.minecraft.util.math.shapes.SplitVoxelShape;
+import net.minecraft.util.math.shapes.VoxelShape;
+import net.minecraft.util.math.shapes.VoxelShapeArray;
+import net.minecraft.util.math.shapes.VoxelShapeCube;
+
+public class VoxelShapeHash implements Hash.Strategy<VoxelShape> {
+    public static final VoxelShapeHash INSTANCE = new VoxelShapeHash();
+
+    @Override
+    public int hashCode(VoxelShape o) {
+        if (o instanceof SplitVoxelShape) {
+            return VoxelShapeSplitHash.INSTANCE.hashCode((SplitVoxelShape) o);
+        } else if (o instanceof VoxelShapeArray) {
+            return VoxelShapeArrayHash.INSTANCE.hashCode((VoxelShapeArray) o);
+        } else if (o instanceof VoxelShapeCube) {
+            return VoxelShapePartHash.INSTANCE.hashCode(((VoxelShapeAccess) o).getPart());
+        } else {
+            //TODO VSCube?
+            return o.hashCode();
+        }
+    }
+
+    @Override
+    public boolean equals(VoxelShape a, VoxelShape b) {
+        if (a == b) {
+            return true;
+        } else if (a == null || b == null) {
+            return false;
+        } else if (a.getClass() != b.getClass()) {
+            return false;
+        } else if (a instanceof SplitVoxelShape) {
+            return VoxelShapeSplitHash.INSTANCE.equals((SplitVoxelShape) a, (SplitVoxelShape) b);
+        } else if (a instanceof VoxelShapeArray) {
+            return VoxelShapeArrayHash.INSTANCE.equals((VoxelShapeArray) a, (VoxelShapeArray) b);
+        } else if (a instanceof VoxelShapeCube) {
+            return VoxelShapePartHash.INSTANCE.equals(
+                    ((VoxelShapeAccess) a).getPart(),
+                    ((VoxelShapeAccess) b).getPart()
+            );
+        } else {
+            return a.equals(b);
+        }
+    }
+}

+ 84 - 0
src/main/java/malte0811/ferritecore/hash/VoxelShapePartHash.java

@@ -0,0 +1,84 @@
+package malte0811.ferritecore.hash;
+
+import it.unimi.dsi.fastutil.Hash;
+import malte0811.ferritecore.mixin.blockstatecache.VSPBitSetAccess;
+import malte0811.ferritecore.mixin.blockstatecache.VSPSplitAccess;
+import net.minecraft.util.math.shapes.BitSetVoxelShapePart;
+import net.minecraft.util.math.shapes.PartSplitVoxelShape;
+import net.minecraft.util.math.shapes.VoxelShapePart;
+
+import java.util.Objects;
+
+public class VoxelShapePartHash implements Hash.Strategy<VoxelShapePart> {
+    public static final VoxelShapePartHash INSTANCE = new VoxelShapePartHash();
+
+    @Override
+    public int hashCode(VoxelShapePart o) {
+        if (o instanceof PartSplitVoxelShape) {
+            VSPSplitAccess access = access((PartSplitVoxelShape) o);
+            int result = access.getStartX();
+            result = 31 * result + access.getStartY();
+            result = 31 * result + access.getStartZ();
+            result = 31 * result + access.getEndX();
+            result = 31 * result + access.getEndY();
+            result = 31 * result + access.getEndZ();
+            result = 31 * result + hashCode(access.getPart());
+            return result;
+        } else if (o instanceof BitSetVoxelShapePart) {
+            VSPBitSetAccess access = access((BitSetVoxelShapePart) o);
+            int result = access.getStartX();
+            result = 31 * result + access.getStartY();
+            result = 31 * result + access.getStartZ();
+            result = 31 * result + access.getEndX();
+            result = 31 * result + access.getEndY();
+            result = 31 * result + access.getEndZ();
+            result = 31 * result + Objects.hashCode(access.getBitSet());
+            return result;
+        } else {
+            return Objects.hashCode(o);
+        }
+    }
+
+    @Override
+    public boolean equals(VoxelShapePart a, VoxelShapePart b) {
+        if (a == b) {
+            return true;
+        } else if (a == null || b == null) {
+            return false;
+        } else if (a.getClass() != b.getClass()) {
+            return false;
+        } else if (a instanceof PartSplitVoxelShape) {
+            VSPSplitAccess accessA = access((PartSplitVoxelShape) a);
+            VSPSplitAccess accessB = access((PartSplitVoxelShape) b);
+            return accessA.getEndX() == accessB.getEndX() &&
+                    accessA.getEndY() == accessB.getEndY() &&
+                    accessA.getEndZ() == accessB.getEndZ() &&
+                    accessA.getStartX() == accessB.getStartX() &&
+                    accessA.getStartY() == accessB.getStartY() &&
+                    accessA.getStartZ() == accessB.getStartZ() &&
+                    equals(accessA.getPart(), accessB.getPart());
+        } else if (a instanceof BitSetVoxelShapePart) {
+            VSPBitSetAccess accessA = access((BitSetVoxelShapePart) a);
+            VSPBitSetAccess accessB = access((BitSetVoxelShapePart) b);
+            return accessA.getEndX() == accessB.getEndX() &&
+                    accessA.getEndY() == accessB.getEndY() &&
+                    accessA.getEndZ() == accessB.getEndZ() &&
+                    accessA.getStartX() == accessB.getStartX() &&
+                    accessA.getStartY() == accessB.getStartY() &&
+                    accessA.getStartZ() == accessB.getStartZ() &&
+                    accessA.getBitSet().equals(accessB.getBitSet());
+        } else {
+            return a.equals(b);
+        }
+    }
+
+    @SuppressWarnings("ConstantConditions")
+    private static VSPSplitAccess access(PartSplitVoxelShape part) {
+        return (VSPSplitAccess) (Object) part;
+    }
+
+    @SuppressWarnings("ConstantConditions")
+    private static VSPBitSetAccess access(BitSetVoxelShapePart part) {
+        return (VSPBitSetAccess) (Object) part;
+    }
+}

+ 45 - 0
src/main/java/malte0811/ferritecore/hash/VoxelShapeSplitHash.java

@@ -0,0 +1,45 @@
+package malte0811.ferritecore.hash;
+
+import it.unimi.dsi.fastutil.Hash;
+import malte0811.ferritecore.mixin.blockstatecache.VSSplitAccess;
+import malte0811.ferritecore.mixin.blockstatecache.VoxelShapeAccess;
+import net.minecraft.util.math.shapes.SplitVoxelShape;
+import net.minecraft.util.math.shapes.VoxelShape;
+import net.minecraft.util.math.shapes.VoxelShapePart;
+
+import java.util.Objects;
+
+public class VoxelShapeSplitHash implements Hash.Strategy<SplitVoxelShape> {
+    public static final VoxelShapeSplitHash INSTANCE = new VoxelShapeSplitHash();
+
+    @Override
+    public int hashCode(SplitVoxelShape o) {
+        VSSplitAccess access = access(o);
+        int result = Objects.hashCode(access.getAxis());
+        result = 31 * result + VoxelShapePartHash.INSTANCE.hashCode(getPart(o));
+        result = 31 * result + VoxelShapeHash.INSTANCE.hashCode(access.getShape());
+        return result;
+    }
+
+    @Override
+    public boolean equals(SplitVoxelShape a, SplitVoxelShape b) {
+        if (a == b) {
+            return true;
+        } else if (a == null || b == null) {
+            return false;
+        }
+        VSSplitAccess accessA = access(a);
+        VSSplitAccess accessB = access(b);
+        return Objects.equals(accessA.getAxis(), accessB.getAxis()) &&
+                VoxelShapeHash.INSTANCE.equals(accessA.getShape(), accessB.getShape()) &&
+                VoxelShapePartHash.INSTANCE.equals(getPart(a), getPart(b));
+    }
+
+    private static VSSplitAccess access(SplitVoxelShape a) {
+        return (VSSplitAccess) a;
+    }
+
+    private static VoxelShapePart getPart(VoxelShape a) {
+        return ((VoxelShapeAccess) a).getPart();
+    }
+}

+ 119 - 0
src/main/java/malte0811/ferritecore/impl/BlockStateCacheImpl.java

@@ -0,0 +1,119 @@
+package malte0811.ferritecore.impl;
+
+import com.mojang.datafixers.util.Pair;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenCustomHashMap;
+import malte0811.ferritecore.ModMain;
+import malte0811.ferritecore.hash.VoxelShapeArrayHash;
+import malte0811.ferritecore.hash.VoxelShapeHash;
+import malte0811.ferritecore.mixin.blockstatecache.VSArrayAccess;
+import malte0811.ferritecore.mixin.blockstatecache.VoxelShapeAccess;
+import malte0811.ferritecore.util.LastAccessedCache;
+import net.minecraft.block.Block;
+import net.minecraft.block.BlockState;
+import net.minecraft.util.Direction;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.shapes.ISelectionContext;
+import net.minecraft.util.math.shapes.VoxelShape;
+import net.minecraft.util.math.shapes.VoxelShapeArray;
+import net.minecraft.util.math.shapes.VoxelShapes;
+import net.minecraft.world.IBlockReader;
+import net.minecraftforge.event.TagsUpdatedEvent;
+import net.minecraftforge.eventbus.api.SubscribeEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.event.lifecycle.FMLModIdMappingEvent;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.function.Function;
+
+@Mod.EventBusSubscriber(modid = ModMain.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE)
+public class BlockStateCacheImpl {
+    private static final Direction[] DIRECTIONS = Direction.values();
+    public static final Object2ObjectOpenCustomHashMap<VoxelShapeArray, VoxelShapeArray> CACHE_COLLIDE =
+            new Object2ObjectOpenCustomHashMap<>(VoxelShapeArrayHash.INSTANCE);
+    public static final LastAccessedCache<VoxelShape, VoxelShape[]> CACHE_PROJECT = new LastAccessedCache<>(
+            VoxelShapeHash.INSTANCE, vs -> {
+        VoxelShape[] result = new VoxelShape[DIRECTIONS.length];
+        for (Direction d : DIRECTIONS) {
+            result[d.ordinal()] = VoxelShapes.getFaceShape(vs, d);
+        }
+        return result;
+    }
+    );
+    public static int collideCalls = 0;
+    public static int projectCalls = 0;
+
+    // Caches are populated in two places: a) In ITagCollectionSupplier#updateTags (which triggers this event)
+    @SubscribeEvent
+    public static void onTagReloadVanilla(TagsUpdatedEvent.VanillaTagTypes ignored) {
+        resetCaches();
+    }
+
+    // b) Via ForgeRegistry#bake, which usually triggers this event
+    @SubscribeEvent
+    public static void onModIdMapping(FMLModIdMappingEvent ignored) {
+        resetCaches();
+    }
+
+    private static void resetCaches() {
+        //TODO remove
+        Logger logger = LogManager.getLogger();
+        logger.info("Collide stats: Cache size: {}, calls: {}", CACHE_COLLIDE.size(), collideCalls);
+        logger.info("Project stats: Cache size: {}, calls: {}", CACHE_PROJECT.size(), projectCalls);
+
+        CACHE_COLLIDE.clear();
+        CACHE_COLLIDE.trim();
+        collideCalls = 0;
+        CACHE_PROJECT.clear();
+        projectCalls = 0;
+    }
+
+    public static VoxelShape redirectGetCollisionShape(
+            Block block, BlockState state, IBlockReader worldIn, BlockPos pos, ISelectionContext context
+    ) {
+        VoxelShape baseResult = block.getCollisionShape(state, worldIn, pos, context);
+        if (!(baseResult instanceof VoxelShapeArray)) {
+            return baseResult;
+        }
+        VoxelShapeArray baseArray = (VoxelShapeArray) baseResult;
+        ++collideCalls;
+        VoxelShapeArray resultArray = CACHE_COLLIDE.computeIfAbsent(baseArray, Function.identity());
+        replaceInternals(resultArray, baseArray);
+        return resultArray;
+    }
+
+    public static VoxelShape redirectFaceShape(VoxelShape shape, Direction face) {
+        ++projectCalls;
+        Pair<VoxelShape, VoxelShape[]> sides = CACHE_PROJECT.get(shape);
+        if (sides.getFirst() instanceof VoxelShapeArray && shape instanceof VoxelShapeArray) {
+            replaceInternals((VoxelShapeArray) sides.getFirst(), (VoxelShapeArray) shape);
+        }
+        return sides.getSecond()[face.ordinal()];
+    }
+
+    public static void replaceInternals(VoxelShapeArray toKeep, VoxelShapeArray toReplace) {
+        if (toKeep == toReplace) {
+            return;
+        }
+        // Mods have a tendency to keep their shapes in a custom cache, in addition to the blockstate cache. So removing
+        // duplicate shapes from the cache only fixes part of the problem. The proper fix would be to deduplicate the
+        // mod caches as well (or convince people to get rid of the larger ones), but that's not feasible. So: Accept
+        // that we can't do anything about shallow size and replace the internals with those used in the cache. This is
+        // not theoretically 100% safe since VSs can technically be modified after they are created, but handing out VSs
+        // that will be modified is unsafe in any case since a lot of vanilla code relies on VSs being immutable.
+        access(toReplace).setXPoints(access(toKeep).getXPoints());
+        access(toReplace).setYPoints(access(toKeep).getYPoints());
+        access(toReplace).setZPoints(access(toKeep).getZPoints());
+        accessVS(toReplace).setProjectionCache(accessVS(toKeep).getProjectionCache());
+        accessVS(toReplace).setPart(accessVS(toKeep).getPart());
+    }
+
+    @SuppressWarnings("ConstantConditions")
+    private static VSArrayAccess access(VoxelShapeArray a) {
+        return (VSArrayAccess) (Object) a;
+    }
+
+    private static VoxelShapeAccess accessVS(VoxelShape a) {
+        return (VoxelShapeAccess) a;
+    }
+}

+ 41 - 0
src/main/java/malte0811/ferritecore/mixin/blockstatecache/BlockStateCacheMixin.java

@@ -0,0 +1,41 @@
+package malte0811.ferritecore.mixin.blockstatecache;
+
+import malte0811.ferritecore.impl.BlockStateCacheImpl;
+import net.minecraft.block.Block;
+import net.minecraft.block.BlockState;
+import net.minecraft.util.Direction;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.shapes.ISelectionContext;
+import net.minecraft.util.math.shapes.VoxelShape;
+import net.minecraft.world.IBlockReader;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Redirect;
+
+@Mixin(targets = "net.minecraft.block.AbstractBlock$AbstractBlockState$Cache")
+public class BlockStateCacheMixin {
+    @Redirect(
+            method = "<init>(Lnet/minecraft/block/BlockState;)V",
+            at = @At(
+                    value = "INVOKE",
+                    target = "Lnet/minecraft/util/math/shapes/VoxelShapes;getFaceShape(Lnet/minecraft/util/math/shapes/VoxelShape;Lnet/minecraft/util/Direction;)Lnet/minecraft/util/math/shapes/VoxelShape;"
+            )
+    )
+    private VoxelShape redirectFaceShape(VoxelShape shape, Direction face) {
+        return BlockStateCacheImpl.redirectFaceShape(shape, face);
+    }
+
+    @Redirect(
+            method = "<init>(Lnet/minecraft/block/BlockState;)V",
+            at = @At(
+                    value = "INVOKE",
+                    args = "debug = true",
+                    target = "Lnet/minecraft/block/Block;getCollisionShape(Lnet/minecraft/block/BlockState;Lnet/minecraft/world/IBlockReader;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/util/math/shapes/ISelectionContext;)Lnet/minecraft/util/math/shapes/VoxelShape;"
+            )
+    )
+    private VoxelShape redirectGetCollisionShape(
+            Block block, BlockState state, IBlockReader worldIn, BlockPos pos, ISelectionContext context
+    ) {
+        return BlockStateCacheImpl.redirectGetCollisionShape(block, state, worldIn, pos, context);
+    }
+}

+ 22 - 0
src/main/java/malte0811/ferritecore/mixin/blockstatecache/Config.java

@@ -0,0 +1,22 @@
+package malte0811.ferritecore.mixin.blockstatecache;
+
+import com.google.common.collect.ImmutableList;
+import malte0811.ferritecore.mixin.config.FerriteConfig;
+import malte0811.ferritecore.mixin.config.FerriteMixinConfig;
+
+import java.util.List;
+
+public class Config extends FerriteMixinConfig {
+    @Override
+    protected List<String> getAllMixins() {
+        return ImmutableList.of(
+                "BlockStateCacheMixin", "VoxelShapeAccess", "VSArrayAccess", "VSPBitSetAccess", "VSPSplitAccess",
+                "VSSplitAccess"
+        );
+    }
+
+    @Override
+    protected boolean isEnabled(String mixin) {
+        return FerriteConfig.DEDUP_BLOCKSTATE_CACHE.isEnabled();
+    }
+}

+ 27 - 0
src/main/java/malte0811/ferritecore/mixin/blockstatecache/VSArrayAccess.java

@@ -0,0 +1,27 @@
+package malte0811.ferritecore.mixin.blockstatecache;
+
+import it.unimi.dsi.fastutil.doubles.DoubleList;
+import net.minecraft.util.math.shapes.VoxelShapeArray;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+@Mixin(VoxelShapeArray.class)
+public interface VSArrayAccess {
+    @Accessor
+    void setXPoints(DoubleList newPoints);
+
+    @Accessor
+    void setYPoints(DoubleList newPoints);
+
+    @Accessor
+    void setZPoints(DoubleList newPoints);
+
+    @Accessor
+    DoubleList getXPoints();
+
+    @Accessor
+    DoubleList getYPoints();
+
+    @Accessor
+    DoubleList getZPoints();
+}

+ 31 - 0
src/main/java/malte0811/ferritecore/mixin/blockstatecache/VSPBitSetAccess.java

@@ -0,0 +1,31 @@
+package malte0811.ferritecore.mixin.blockstatecache;
+
+import net.minecraft.util.math.shapes.BitSetVoxelShapePart;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+import java.util.BitSet;
+
+@Mixin(BitSetVoxelShapePart.class)
+public interface VSPBitSetAccess {
+    @Accessor
+    BitSet getBitSet();
+
+    @Accessor
+    int getStartX();
+
+    @Accessor
+    int getStartY();
+
+    @Accessor
+    int getStartZ();
+
+    @Accessor
+    int getEndX();
+
+    @Accessor
+    int getEndY();
+
+    @Accessor
+    int getEndZ();
+}

+ 30 - 0
src/main/java/malte0811/ferritecore/mixin/blockstatecache/VSPSplitAccess.java

@@ -0,0 +1,30 @@
+package malte0811.ferritecore.mixin.blockstatecache;
+
+import net.minecraft.util.math.shapes.PartSplitVoxelShape;
+import net.minecraft.util.math.shapes.VoxelShapePart;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+@Mixin(PartSplitVoxelShape.class)
+public interface VSPSplitAccess {
+    @Accessor
+    VoxelShapePart getPart();
+
+    @Accessor
+    int getStartX();
+
+    @Accessor
+    int getStartY();
+
+    @Accessor
+    int getStartZ();
+
+    @Accessor
+    int getEndX();
+
+    @Accessor
+    int getEndY();
+
+    @Accessor
+    int getEndZ();
+}

+ 21 - 0
src/main/java/malte0811/ferritecore/mixin/blockstatecache/VSSplitAccess.java

@@ -0,0 +1,21 @@
+package malte0811.ferritecore.mixin.blockstatecache;
+
+import net.minecraft.util.Direction;
+import net.minecraft.util.math.shapes.SplitVoxelShape;
+import net.minecraft.util.math.shapes.VoxelShape;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Mutable;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+@Mixin(SplitVoxelShape.class)
+public interface VSSplitAccess {
+    @Accessor
+    @Mutable
+    void setShape(VoxelShape newShape);
+
+    @Accessor
+    VoxelShape getShape();
+
+    @Accessor
+    Direction.Axis getAxis();
+}

+ 25 - 0
src/main/java/malte0811/ferritecore/mixin/blockstatecache/VoxelShapeAccess.java

@@ -0,0 +1,25 @@
+package malte0811.ferritecore.mixin.blockstatecache;
+
+import net.minecraft.util.math.shapes.VoxelShape;
+import net.minecraft.util.math.shapes.VoxelShapePart;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Mutable;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+import javax.annotation.Nullable;
+
+@Mixin(VoxelShape.class)
+public interface VoxelShapeAccess {
+    @Accessor
+    VoxelShapePart getPart();
+
+    @Accessor
+    VoxelShape[] getProjectionCache();
+
+    @Accessor
+    @Mutable
+    void setPart(VoxelShapePart newPart);
+
+    @Accessor
+    void setProjectionCache(@Nullable VoxelShape[] newCache);
+}

+ 5 - 0
src/main/java/malte0811/ferritecore/mixin/config/FerriteConfig.java

@@ -19,6 +19,7 @@ public class FerriteConfig {
     public static final Option PREDICATES;
     public static final Option MRL_CACHE;
     public static final Option DEDUP_MULTIPART;
+    public static final Option DEDUP_BLOCKSTATE_CACHE;
 
     static {
         ConfigBuilder builder = new ConfigBuilder();
@@ -43,6 +44,10 @@ public class FerriteConfig {
                         "model. Requires " + PREDICATES.getName() + " to be enabled",
                 PREDICATES
         );
+        DEDUP_BLOCKSTATE_CACHE = builder.createOption(
+                "blockstateCacheDeduplication",
+                "Deduplicate cached data for blockstates, most importantly collision and render shapes"
+        );
         builder.finish();
     }
 

+ 40 - 0
src/main/java/malte0811/ferritecore/util/LastAccessedCache.java

@@ -0,0 +1,40 @@
+package malte0811.ferritecore.util;
+
+import com.mojang.datafixers.util.Pair;
+import it.unimi.dsi.fastutil.Hash;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenCustomHashMap;
+
+import java.util.function.Function;
+
+public class LastAccessedCache<K, V> {
+    private final Object2ObjectOpenCustomHashMap<K, V> mainMap;
+    private final Function<K, V> createValue;
+    private final Hash.Strategy<K> strategy;
+    private Pair<K, V> lastAccessed;
+
+    public LastAccessedCache(Hash.Strategy<K> strategy, Function<K, V> createValue) {
+        this.strategy = strategy;
+        this.mainMap = new Object2ObjectOpenCustomHashMap<>(strategy);
+        this.createValue = createValue;
+    }
+
+    public Pair<K, V> get(K key) {
+        final Pair<K, V> last = lastAccessed;
+        if (last != null && strategy.equals(last.getFirst(), key)) {
+            return last;
+        } else {
+            final V result = mainMap.computeIfAbsent(key, createValue);
+            return lastAccessed = Pair.of(key, result);
+        }
+    }
+
+    public void clear() {
+        lastAccessed = null;
+        mainMap.clear();
+        mainMap.trim();
+    }
+
+    public int size() {
+        return mainMap.size();
+    }
+}

+ 21 - 0
src/main/resources/ferritecore.blockstatecache.mixin.json

@@ -0,0 +1,21 @@
+{
+  "required": true,
+  "package": "malte0811.ferritecore.mixin.blockstatecache",
+  "compatibilityLevel": "JAVA_8",
+  "refmap": "ferritecore.refmap.json",
+  "client": [
+  ],
+  "injectors": {
+    "defaultRequire": 1
+  },
+  "minVersion": "0.8",
+  "plugin": "malte0811.ferritecore.mixin.blockstatecache.Config",
+  "mixins": [
+    "BlockStateCacheMixin",
+    "VoxelShapeAccess",
+    "VSArrayAccess",
+    "VSPBitSetAccess",
+    "VSPSplitAccess",
+    "VSSplitAccess"
+  ]
+}