feat(mod): Stage 3 (continued 3) — ParticleMixin + BlockEntityRenderStateMixin + ChestRendererMixin
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).
This commit is contained in:
parent
135628c2f1
commit
8ef46e4560
5 changed files with 295 additions and 0 deletions
|
|
@ -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.
|
||||||
|
*
|
||||||
|
* <p>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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
*
|
||||||
|
* <p>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.
|
||||||
|
*
|
||||||
|
* <p>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<Integer> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
*
|
||||||
|
* <p>Two WrapOperations on {@link ChestRenderer#extractRenderState}:
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Combine cache</b> — 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.</li>
|
||||||
|
* <li><b>Pair-light cache</b> — 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.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>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<DoubleBlockCombiner.NeighborCombineResult<?>> 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<Integer> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
*
|
||||||
|
* <p>Two optimizations in one mixin (matches ZakoOpt):
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Light cache</b> — 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.</li>
|
||||||
|
* <li><b>Physics skip</b> — 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}.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>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<Integer> 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<Integer> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -35,6 +35,8 @@
|
||||||
"client": [
|
"client": [
|
||||||
"optimize.block.BaseSpawnerMixin",
|
"optimize.block.BaseSpawnerMixin",
|
||||||
"optimize.block.BlockEntityMixin",
|
"optimize.block.BlockEntityMixin",
|
||||||
|
"optimize.block.BlockEntityRenderStateMixin",
|
||||||
|
"optimize.block.ChestRendererMixin",
|
||||||
"optimize.entity.EntityNameMixin",
|
"optimize.entity.EntityNameMixin",
|
||||||
"optimize.entity.EntityRenderStateEntityMixin",
|
"optimize.entity.EntityRenderStateEntityMixin",
|
||||||
"optimize.entity.LivingEntityLodMixin",
|
"optimize.entity.LivingEntityLodMixin",
|
||||||
|
|
@ -46,6 +48,7 @@
|
||||||
"optimize.gl.RenderTypesMixin",
|
"optimize.gl.RenderTypesMixin",
|
||||||
"optimize.hud.OptimizeDebugHudMixin",
|
"optimize.hud.OptimizeDebugHudMixin",
|
||||||
"optimize.particle.BubbleColumnUpParticleMixin",
|
"optimize.particle.BubbleColumnUpParticleMixin",
|
||||||
|
"optimize.particle.ParticleMixin",
|
||||||
"optimize.world.LevelRendererOutlineMixin",
|
"optimize.world.LevelRendererOutlineMixin",
|
||||||
"accessors.blaze3d.GlTextureInvoker",
|
"accessors.blaze3d.GlTextureInvoker",
|
||||||
"accessors.blaze3d.GpuDeviceAccessor",
|
"accessors.blaze3d.GpuDeviceAccessor",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue