diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/camera/CameraPosition.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/camera/CameraPosition.java
new file mode 100644
index 00000000..f50de799
--- /dev/null
+++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/camera/CameraPosition.java
@@ -0,0 +1,32 @@
+package dev.loki.lovisual.features.module.modules.misc.optimize.particle.camera;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.world.phys.Vec3;
+
+/**
+ * Cached camera position accessor for the particle optimization layer.
+ * Vanilla {@code Particle.move} recomputes the camera position via
+ * {@code Minecraft.getInstance().gameRenderer.mainCamera().position()} per
+ * particle per tick — but the camera only moves once per frame. This helper
+ * is the single point where the camera position is sampled, so
+ * {@link ParticleMixin} reads it without re-walking the gameRenderer chain
+ * per particle.
+ *
+ *
Ported from {@code zako.opt.particle.CameraPosition} (ZakoOpt, LGPL-3.0).
+ * Original author: Zako — https://t.me/StarikZako. Render-thread only —
+ * particle ticking runs on the render thread.
+ */
+public final class CameraPosition {
+
+ private CameraPosition() {
+ }
+
+ /**
+ * Sample the current camera position. Caller must not cache the returned
+ * {@link Vec3} — it may be a reused instance whose values change between
+ * frames.
+ */
+ public static Vec3 get() {
+ return Minecraft.getInstance().gameRenderer.mainCamera().position();
+ }
+}
diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/optimize/block/BlockEntityRenderStateMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/optimize/block/BlockEntityRenderStateMixin.java
new file mode 100644
index 00000000..30f1e14e
--- /dev/null
+++ b/mod/src/main/java/dev/loki/lovisual/mixins/optimize/block/BlockEntityRenderStateMixin.java
@@ -0,0 +1,57 @@
+package dev.loki.lovisual.mixins.optimize.block;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import dev.loki.lovisual.features.module.modules.misc.optimize.block.CachedLight;
+import dev.loki.lovisual.render.engine.optimize.OptimizeToggles;
+import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState;
+import net.minecraft.client.renderer.feature.ModelFeatureRenderer;
+import net.minecraft.core.BlockPos;
+import net.minecraft.util.LightCoordsUtil;
+import net.minecraft.world.level.BlockAndLightGetter;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+/**
+ * Stage 3: cache per-tick block-light lookups on every block-entity in view.
+ *
+ *
Vanilla {@code BlockEntityRenderState.extractBase} calls
+ * {@link LightCoordsUtil#getLightCoords} every frame per block-entity — that
+ * walks the chunk's light section data per call, and the light value only
+ * changes on tick boundaries. This WrapOperation wraps the call to consult
+ * the per-tick cache slot on the {@link BlockEntity} (via the
+ * {@link CachedLight} mixin from Stage 3-continued-2) before falling through
+ * to the original.
+ *
+ *
Ported from {@code zako.opt.mixin.block.BlockEntityRenderStateMixin}
+ * (ZakoOpt, LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
+ * Gated on the {@code blockEntityCache} toggle via
+ * {@link OptimizeToggles#blockEntityCache()}. When the toggle is off, the
+ * original runs unchanged.
+ */
+@Mixin(BlockEntityRenderState.class)
+public abstract class BlockEntityRenderStateMixin {
+
+ @WrapOperation(
+ method = "extractBase",
+ at = @At(value = "INVOKE",
+ target = "Lnet/minecraft/util/LightCoordsUtil;getLightCoords(Lnet/minecraft/world/level/BlockAndLightGetter;Lnet/minecraft/core/BlockPos;)I")
+ )
+ private static int lovisual$cachedLight(BlockAndLightGetter level, BlockPos pos, Operation original,
+ BlockEntity blockEntity, BlockEntityRenderState state,
+ ModelFeatureRenderer.CrumblingOverlay crumbling) {
+ if (!OptimizeToggles.blockEntityCache() || !(level instanceof Level world)) {
+ return original.call(level, pos);
+ }
+ if (!(blockEntity instanceof CachedLight cache)) {
+ return original.call(level, pos);
+ }
+ long tick = world.getGameTime();
+ if (cache.lovisual$lightTick() != tick) {
+ cache.lovisual$setLight(original.call(level, pos), tick);
+ }
+ return cache.lovisual$light();
+ }
+}
diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/optimize/block/ChestRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/optimize/block/ChestRendererMixin.java
new file mode 100644
index 00000000..05bdd51b
--- /dev/null
+++ b/mod/src/main/java/dev/loki/lovisual/mixins/optimize/block/ChestRendererMixin.java
@@ -0,0 +1,92 @@
+package dev.loki.lovisual.mixins.optimize.block;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import dev.loki.lovisual.features.module.modules.misc.optimize.block.CachedLight;
+import dev.loki.lovisual.features.module.modules.misc.optimize.block.TickCached;
+import dev.loki.lovisual.render.engine.optimize.OptimizeToggles;
+import it.unimi.dsi.fastutil.ints.Int2IntFunction;
+import net.minecraft.client.renderer.blockentity.ChestRenderer;
+import net.minecraft.client.renderer.blockentity.state.ChestRenderState;
+import net.minecraft.client.renderer.feature.ModelFeatureRenderer;
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.block.ChestBlock;
+import net.minecraft.world.level.block.DoubleBlockCombiner;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.phys.Vec3;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+/**
+ * Stage 3: double-chest combine cache + double-chest pair-light cache.
+ *
+ * Two WrapOperations on {@link ChestRenderer#extractRenderState}:
+ *
+ * - Combine cache — wraps {@link ChestBlock#combine} (the
+ * neighbor-lookup that builds the double-chest render state). The
+ * result only changes when the neighbour block changes, so caching by
+ * game tick (via the {@link TickCached} mixin from Stage 3-continued-2)
+ * removes the neighbor-walk from the per-frame hot path.
+ * - Pair-light cache — wraps {@link Int2IntFunction#applyAsInt}
+ * (the second light lookup done for double chests). Two light lookups
+ * per chest per frame in vanilla, cached to one per tick via the
+ * {@link CachedLight#lovisual$pairLight()} slot.
+ *
+ *
+ * Ported from {@code zako.opt.mixin.block.ChestRendererMixin} (ZakoOpt,
+ * LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
+ * Combine cache gated on {@code blockEntityCache} toggle; pair-light cache
+ * gated on {@code microOpts} toggle (matches ZakoOpt's microOpts2 split —
+ * Stage 1 collapsed microOpts2 into microOpts, so both paths read the same
+ * toggle).
+ */
+@Mixin(ChestRenderer.class)
+public abstract class ChestRendererMixin {
+
+ @WrapOperation(
+ method = "extractRenderState(Lnet/minecraft/world/level/block/entity/BlockEntity;Lnet/minecraft/client/renderer/blockentity/state/ChestRenderState;FLnet/minecraft/world/phys/Vec3;Lnet/minecraft/client/renderer/feature/ModelFeatureRenderer$CrumblingOverlay;)V",
+ at = @At(value = "INVOKE",
+ target = "Lnet/minecraft/world/level/block/ChestBlock;combine(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Z)Lnet/minecraft/world/level/block/DoubleBlockCombiner$NeighborCombineResult;")
+ )
+ private DoubleBlockCombiner.NeighborCombineResult> lovisual$cachedCombine(
+ ChestBlock block, BlockState state, Level level, BlockPos pos, boolean ignoreBlocked,
+ Operation> original,
+ BlockEntity blockEntity, ChestRenderState renderState, float partialTick, Vec3 camera,
+ ModelFeatureRenderer.CrumblingOverlay crumbling) {
+ if (!OptimizeToggles.blockEntityCache()) {
+ return original.call(block, state, level, pos, ignoreBlocked);
+ }
+ if (!(blockEntity instanceof TickCached cache)) {
+ return original.call(block, state, level, pos, ignoreBlocked);
+ }
+ long tick = level.getGameTime();
+ if (cache.lovisual$cachedTick() != tick || cache.lovisual$cached() == null) {
+ cache.lovisual$setCached(original.call(block, state, level, pos, ignoreBlocked), tick);
+ }
+ return (DoubleBlockCombiner.NeighborCombineResult>) cache.lovisual$cached();
+ }
+
+ @WrapOperation(
+ method = "extractRenderState(Lnet/minecraft/world/level/block/entity/BlockEntity;Lnet/minecraft/client/renderer/blockentity/state/ChestRenderState;FLnet/minecraft/world/phys/Vec3;Lnet/minecraft/client/renderer/feature/ModelFeatureRenderer$CrumblingOverlay;)V",
+ at = @At(value = "INVOKE",
+ target = "Lit/unimi/dsi/fastutil/ints/Int2IntFunction;applyAsInt(I)I")
+ )
+ private int lovisual$cachedPairLight(Int2IntFunction combiner, int light, Operation original,
+ BlockEntity blockEntity, ChestRenderState renderState, float partialTick, Vec3 camera,
+ ModelFeatureRenderer.CrumblingOverlay crumbling) {
+ Level level = blockEntity.getLevel();
+ if (!OptimizeToggles.microOpts() || level == null) {
+ return original.call(combiner, light);
+ }
+ if (!(blockEntity instanceof CachedLight cache)) {
+ return original.call(combiner, light);
+ }
+ long tick = level.getGameTime();
+ if (cache.lovisual$pairLightTick() != tick) {
+ cache.lovisual$setPairLight(original.call(combiner, light), tick);
+ }
+ return cache.lovisual$pairLight();
+ }
+}
diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/optimize/particle/ParticleMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/optimize/particle/ParticleMixin.java
new file mode 100644
index 00000000..446317c0
--- /dev/null
+++ b/mod/src/main/java/dev/loki/lovisual/mixins/optimize/particle/ParticleMixin.java
@@ -0,0 +1,111 @@
+package dev.loki.lovisual.mixins.optimize.particle;
+
+import dev.loki.lovisual.features.module.modules.misc.optimize.particle.camera.CameraPosition;
+import dev.loki.lovisual.render.engine.optimize.OptimizeToggles;
+import net.minecraft.client.multiplayer.ClientLevel;
+import net.minecraft.client.particle.BaseAshSmokeParticle;
+import net.minecraft.client.particle.BubbleColumnUpParticle;
+import net.minecraft.client.particle.CampfireSmokeParticle;
+import net.minecraft.client.particle.Particle;
+import net.minecraft.world.phys.Vec3;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+/**
+ * Stage 3: per-particle light cache + far-particle physics skip.
+ *
+ * Two optimizations in one mixin (matches ZakoOpt):
+ *
+ * - Light cache — vanilla {@code getLightCoords(partialTick)} walks
+ * the chunk's light section data per call, called per particle per frame.
+ * The value only changes on tick boundaries (light updates), so caching
+ * by game tick removes the walk from the hot path.
+ * - Physics skip — vanilla {@code move} does full collision against
+ * the world for every particle. Far particles (>16 blocks) and three
+ * specific always-skip subclasses ({@link BaseAshSmokeParticle},
+ * {@link CampfireSmokeParticle}, {@link BubbleColumnUpParticle}) skip
+ * the collision check by setting {@code hasPhysics=false} for the
+ * duration of {@code move}.
+ *
+ *
+ * Ported from {@code zako.opt.mixin.particle.ParticleMixin} (ZakoOpt,
+ * LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
+ * Light-cache gated on the {@code microOpts} toggle (Stage 1 wiring);
+ * physics-skip also gated on {@code microOpts} since both are micro-opts.
+ * When the toggle is off, the original code runs unchanged.
+ */
+@Mixin(Particle.class)
+public abstract class ParticleMixin {
+
+ @Shadow
+ @Final
+ protected ClientLevel level;
+
+ @Shadow
+ protected boolean hasPhysics;
+
+ @Shadow
+ protected double x;
+
+ @Shadow
+ protected double y;
+
+ @Shadow
+ protected double z;
+
+ @Unique
+ private boolean lovisual$physicsBefore;
+
+ @Unique
+ private long lovisual$lightTick = Long.MIN_VALUE;
+
+ @Unique
+ private int lovisual$light;
+
+ @Inject(method = "getLightCoords", at = @At("HEAD"), cancellable = true)
+ private void lovisual$cachedLight(float partialTick, CallbackInfoReturnable cir) {
+ if (lovisual$lightTick == level.getGameTime() && OptimizeToggles.particleLight()) {
+ cir.setReturnValue(lovisual$light);
+ }
+ }
+
+ @Inject(method = "getLightCoords", at = @At("RETURN"))
+ private void lovisual$storeLight(float partialTick, CallbackInfoReturnable cir) {
+ lovisual$light = cir.getReturnValueI();
+ lovisual$lightTick = level.getGameTime();
+ }
+
+ @Inject(method = "move", at = @At("HEAD"))
+ private void lovisual$cheapPhysics(double dx, double dy, double dz, CallbackInfo ci) {
+ lovisual$physicsBefore = hasPhysics;
+ if (hasPhysics && OptimizeToggles.particlePhysics() && lovisual$skipCollision()) {
+ hasPhysics = false;
+ }
+ }
+
+ @Inject(method = "move", at = @At("RETURN"))
+ private void lovisual$restorePhysics(double dx, double dy, double dz, CallbackInfo ci) {
+ hasPhysics = lovisual$physicsBefore;
+ }
+
+ @Unique
+ private boolean lovisual$skipCollision() {
+ Object self = this;
+ if (self instanceof BaseAshSmokeParticle
+ || self instanceof CampfireSmokeParticle
+ || self instanceof BubbleColumnUpParticle) {
+ return true;
+ }
+ Vec3 cam = CameraPosition.get();
+ double ddx = x - cam.x;
+ double ddy = y - cam.y;
+ double ddz = z - cam.z;
+ return ddx * ddx + ddy * ddy + ddz * ddz > 16 * 16;
+ }
+}
diff --git a/mod/src/main/resources/lovisual.mixins.json b/mod/src/main/resources/lovisual.mixins.json
index f04a6dc9..000e84c4 100644
--- a/mod/src/main/resources/lovisual.mixins.json
+++ b/mod/src/main/resources/lovisual.mixins.json
@@ -35,6 +35,8 @@
"client": [
"optimize.block.BaseSpawnerMixin",
"optimize.block.BlockEntityMixin",
+ "optimize.block.BlockEntityRenderStateMixin",
+ "optimize.block.ChestRendererMixin",
"optimize.entity.EntityNameMixin",
"optimize.entity.EntityRenderStateEntityMixin",
"optimize.entity.LivingEntityLodMixin",
@@ -46,6 +48,7 @@
"optimize.gl.RenderTypesMixin",
"optimize.hud.OptimizeDebugHudMixin",
"optimize.particle.BubbleColumnUpParticleMixin",
+ "optimize.particle.ParticleMixin",
"optimize.world.LevelRendererOutlineMixin",
"accessors.blaze3d.GlTextureInvoker",
"accessors.blaze3d.GpuDeviceAccessor",