From 8ef46e4560a9378f6b4e967d707f95174b109466 Mon Sep 17 00:00:00 2001 From: LoVisual AI Date: Sat, 10 Oct 2026 16:51:40 +0000 Subject: [PATCH] =?UTF-8?q?feat(mod):=20Stage=203=20(continued=203)=20?= =?UTF-8?q?=E2=80=94=20ParticleMixin=20+=20BlockEntityRenderStateMixin=20+?= =?UTF-8?q?=20ChestRendererMixin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per mod/docs/optimize-layer-design.md Stage 3 (continued). Three more mixin'ов that consume the Stage 3-continued-2 contract mixins (CachedLight, TickCached) on actual renderers, plus the ParticleMixin that adds its own per-particle light cache + far-particle physics skip. mixins/optimize/particle/ParticleMixin.java: - @Inject HEAD/RETURN on Particle.getLightCoords — caches the per-particle light lookup by game tick (light only changes on tick boundaries). Gated on the Stage 1 'particleLight' toggle. - @Inject HEAD/RETURN on Particle.move — temporarily sets hasPhysics=false for far particles (>16 blocks from camera) and three always-skip subclasses (BaseAshSmokeParticle, CampfireSmokeParticle, BubbleColumnUpParticle). Gated on the 'particlePhysics' toggle. - Ported from zako.opt.mixin.particle.ParticleMixin. features/.../optimize/particle/camera/CameraPosition.java (new helper): - Single point where the camera position is sampled. ParticleMixin reads it instead of re-walking Minecraft.getInstance().gameRenderer.mainCamera() per particle per tick. Ported from zako.opt.particle.CameraPosition. - Moved into particle/camera/ subfolder so optimize/particle/ stays under the ≤4 .java-per-folder limit after adding ParticleMixin. mixins/optimize/block/BlockEntityRenderStateMixin.java: - WrapOperation on BlockEntityRenderState.extractBase's LightCoordsUtil.getLightCoords — consults the per-tick light cache slot on the BlockEntity (via the CachedLight mixin from Stage 3-continued-2) before falling through to the original. Gated on 'blockEntityCache'. - Ported from zako.opt.mixin.block.BlockEntityRenderStateMixin. mixins/optimize/block/ChestRendererMixin.java: - Two WrapOperations on ChestRenderer.extractRenderState: * ChestBlock.combine — caches the double-chest neighbor-lookup result by game tick (via the TickCached mixin). Gated on 'blockEntityCache'. * Int2IntFunction.applyAsInt — caches the second (pair) light lookup done for double chests (via the CachedLight.lovisual$pairLight slot). Gated on 'microOpts'. - Ported from zako.opt.mixin.block.ChestRendererMixin. lovisual.mixins.json: three new entries in the 'client' array: optimize.block.BlockEntityRenderStateMixin optimize.block.ChestRendererMixin optimize.particle.ParticleMixin Tests: no new tests in this batch (mixins are pure side-effect field-bags / WrapOperation wrappers without unit-testable logic). Existing suite still green. Total: 866 tests, 0 failures. checkFolderLimit: OK (0 папок >4). --- .../particle/camera/CameraPosition.java | 32 +++++ .../block/BlockEntityRenderStateMixin.java | 57 +++++++++ .../optimize/block/ChestRendererMixin.java | 92 +++++++++++++++ .../optimize/particle/ParticleMixin.java | 111 ++++++++++++++++++ mod/src/main/resources/lovisual.mixins.json | 3 + 5 files changed, 295 insertions(+) create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/camera/CameraPosition.java create mode 100644 mod/src/main/java/dev/loki/lovisual/mixins/optimize/block/BlockEntityRenderStateMixin.java create mode 100644 mod/src/main/java/dev/loki/lovisual/mixins/optimize/block/ChestRendererMixin.java create mode 100644 mod/src/main/java/dev/loki/lovisual/mixins/optimize/particle/ParticleMixin.java 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}: + *

+ * + *

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): + *

+ * + *

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",