feat(mod): Stage 3 (continued 2) — entity + block + Sodium contract mixins

Per mod/docs/optimize-layer-design.md Stage 3 (continued). This batch
implements the Stage 1 mixin-contract interfaces on their target MC classes
so CachedName, StateEntity, TickCached, CachedLight, CachedValidity and
CachedStack actually get used. Seven new mixin'ов; no new helpers (the
helpers were all written in Stage 1 already).

Entity-contract mixins (mixins/optimize/entity/):
- EntityNameMixin: implements CachedName on Entity — adds the per-tick
  cached name-tag fields (Component + tick). Vanilla Entity.getDisplayName()
  rebuilds the team prefix + name + suffix on every call.
- EntityRenderStateEntityMixin: implements StateEntity on EntityRenderState —
  adds the back-reference field to the source Entity. Render-states are
  rebuilt every frame so they can't keep their own pointer; this is captured
  by EntityRendererStateMixin.
- EntityRendererStateMixin: Inject at RETURN on EntityRenderer.createRenderState
  — captures the source Entity on the freshly-built state via the StateEntity
  contract. ZakoOpt used @ModifyReturnValue; we use plain @Inject because the
  MixinExtras ModifyReturnValue transformer is more fragile across MC versions.
- EntityRendererNameMixin: WrapOperation on EntityRenderer.getNameTag's
  Entity.getDisplayName call — caches by game tick so the per-frame cost is
  one identity check + read instead of a full Component rebuild.
- SodiumEntityRendererMixin: WrapOperation on Sodium EntityRenderer.renderCuboid's
  MemoryStack.stackPush — uses CachedStack (Stage 1) to skip the ThreadLocal
  lookup on the render thread. Registered as optional via LoVisualMixinPlugin
  (only applied when Sodium is loaded); remap = false because the target is
  a Sodium class, not a Mojang one.

BlockEntity-contract mixins (mixins/optimize/block/):
- BlockEntityMixin: implements TickCached + CachedLight + CachedValidity on
  BlockEntity — adds the per-tick cache slot, per-tick block-light lookups
  (single + pair for double-chests), per-tick should-render distance check,
  and per-state validity cache. All four are read by the (Stage 3, future)
  ChestRendererMixin / BlockEntityRenderStateMixin / BlockEntityRenderDispatcherMixin.
- BaseSpawnerMixin: implements TickCached on BaseSpawner — adds the per-tick
  cache slot that SpawnerCache (Stage 1) reads back.

Folder limit (≤4 .java per folder):
- mixins/optimize/entity/ hit 6 files after this batch — split into
  mixins/optimize/entity/renderer/ for EntityRendererStateMixin +
  EntityRendererNameMixin (the two that target EntityRenderer rather than
  Entity/EntityRenderState directly). The other 4 stay in entity/.

lovisual.mixins.json: seven new entries in the 'client' array, alphabetical:
  optimize.block.BaseSpawnerMixin
  optimize.block.BlockEntityMixin
  optimize.entity.EntityNameMixin
  optimize.entity.EntityRenderStateEntityMixin
  optimize.entity.SodiumEntityRendererMixin
  optimize.entity.renderer.EntityRendererNameMixin
  optimize.entity.renderer.EntityRendererStateMixin

LoVisualMixinPlugin: added SodiumEntityRendererMixin to OPTIONAL_SODIUM_MIXINS
so the mixin is only applied when Sodium is loaded (matches the existing
convention for sodium.* mixins).

Tests: no new tests in this batch (the new mixins are pure side-effect
field-bags — they don't have unit-testable logic). Existing suite still green.
Total: 866 tests, 0 failures. checkFolderLimit: OK (0 папок >4).
This commit is contained in:
LoVisual AI 2026-10-10 16:43:02 +00:00 • committed by loki5512344
parent 15f070af6d
commit 135628c2f1
Signed by: boba
GPG key ID: 253067914055423B
9 changed files with 415 additions and 1 deletions

View file

@ -21,7 +21,8 @@ public final class LoVisualMixinPlugin implements IMixinConfigPlugin {
"dev.loki.lovisual.mixins.sodium.terrain.SodiumShaderChunkRendererMixin",
"dev.loki.lovisual.mixins.sodium.vertex.SodiumVertexConsumerTrackerMixin",
"dev.loki.lovisual.mixins.sodium.placement.SodiumPlaceCullMixin",
"dev.loki.lovisual.mixins.sodium.placement.SodiumPlaceHideMixin"
"dev.loki.lovisual.mixins.sodium.placement.SodiumPlaceHideMixin",
"dev.loki.lovisual.mixins.optimize.entity.SodiumEntityRendererMixin"
);
private static final Set<String> OPTIONAL_IRIS_MIXINS = Set.of(
"dev.loki.lovisual.mixins.iris.render.IrisCommonUniformsMixin",

View file

@ -0,0 +1,47 @@
package dev.loki.lovisual.mixins.optimize.block;
import dev.loki.lovisual.features.module.modules.misc.optimize.block.TickCached;
import net.minecraft.world.level.BaseSpawner;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
/**
* Stage 3: implements {@link TickCached} on {@link BaseSpawner} — adds the
* per-tick cache slot that {@link dev.loki.lovisual.features.module.modules.misc.optimize.block.SpawnerCache}
* reads back to skip the per-frame {@code extractEntity} rebuild of the
* spawner's display mob.
*
* <p>The vanilla {@code SpawnerRenderer.extractRenderState} rebuilds the
* display entity's render state every frame for every spawner in view;
* the display entity only changes on tick boundaries (spin speed, mob
* rotation, etc.), so caching by tick removes that rebuild from the hot path.
*
* <p>Ported from {@code zako.opt.mixin.block.BaseSpawnerMixin} (ZakoOpt,
* LGPL-3.0). Original author: Zako — https://t.me/StarikZako. Field/method
* prefix changed from {@code zakoopt$} to {@code lovisual$}.
*/
@Mixin(BaseSpawner.class)
public abstract class BaseSpawnerMixin implements TickCached {
@Unique
private Object lovisual$cached;
@Unique
private long lovisual$cachedTick = Long.MIN_VALUE;
@Override
public Object lovisual$cached() {
return lovisual$cached;
}
@Override
public long lovisual$cachedTick() {
return lovisual$cachedTick;
}
@Override
public void lovisual$setCached(Object value, long tick) {
lovisual$cached = value;
lovisual$cachedTick = tick;
}
}

View file

@ -0,0 +1,142 @@
package dev.loki.lovisual.mixins.optimize.block;
import dev.loki.lovisual.features.module.modules.misc.optimize.block.CachedLight;
import dev.loki.lovisual.features.module.modules.misc.optimize.block.CachedValidity;
import dev.loki.lovisual.features.module.modules.misc.optimize.block.TickCached;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
/**
* Stage 3: implements {@link TickCached}, {@link CachedLight} and
* {@link CachedValidity} on {@link BlockEntity} — adds the per-tick cache
* slot (object + tick), the per-tick block-light lookups, the per-tick
* should-render distance check, and the per-state validity cache.
*
* <p>The vanilla {@code BlockEntityType.isValid(state)} is called every
* frame per block-entity in view but only changes on a state transition;
* {@code LightCoordsUtil.getLightCoords(level, pos)} walks the chunk's light
* section data per call; {@code BlockEntityRenderer.shouldRender} does a
* distance check that only changes when the entity moves across the cull
* boundary. All three are cached via the interface fields implemented here.
*
* <p>Ported from {@code zako.opt.mixin.block.BlockEntityMixin} (ZakoOpt,
* LGPL-3.0). Original author: Zako — https://t.me/StarikZako. Field/method
* prefix changed from {@code zakoopt$} to {@code lovisual$}.
*/
@Mixin(BlockEntity.class)
public abstract class BlockEntityMixin implements TickCached, CachedLight, CachedValidity {
// --- TickCached ---
@Unique
private Object lovisual$cached;
@Unique
private long lovisual$cachedTick = Long.MIN_VALUE;
// --- CachedLight ---
@Unique
private int lovisual$light;
@Unique
private long lovisual$lightTick = Long.MIN_VALUE;
@Unique
private int lovisual$pairLight;
@Unique
private long lovisual$pairLightTick = Long.MIN_VALUE;
// --- CachedValidity ---
@Unique
private BlockState lovisual$validState;
@Unique
private boolean lovisual$valid;
@Unique
private long lovisual$distanceTick = Long.MIN_VALUE;
@Unique
private boolean lovisual$inDistance;
// --- TickCached ---
@Override
public Object lovisual$cached() {
return lovisual$cached;
}
@Override
public long lovisual$cachedTick() {
return lovisual$cachedTick;
}
@Override
public void lovisual$setCached(Object value, long tick) {
lovisual$cached = value;
lovisual$cachedTick = tick;
}
// --- CachedLight ---
@Override
public int lovisual$light() {
return lovisual$light;
}
@Override
public long lovisual$lightTick() {
return lovisual$lightTick;
}
@Override
public void lovisual$setLight(int light, long tick) {
lovisual$light = light;
lovisual$lightTick = tick;
}
@Override
public int lovisual$pairLight() {
return lovisual$pairLight;
}
@Override
public long lovisual$pairLightTick() {
return lovisual$pairLightTick;
}
@Override
public void lovisual$setPairLight(int light, long tick) {
lovisual$pairLight = light;
lovisual$pairLightTick = tick;
}
// --- CachedValidity ---
@Override
public BlockState lovisual$validState() {
return lovisual$validState;
}
@Override
public boolean lovisual$valid() {
return lovisual$valid;
}
@Override
public void lovisual$setValid(BlockState state, boolean valid) {
lovisual$validState = state;
lovisual$valid = valid;
}
@Override
public long lovisual$distanceTick() {
return lovisual$distanceTick;
}
@Override
public boolean lovisual$inDistance() {
return lovisual$inDistance;
}
@Override
public void lovisual$setInDistance(boolean inDistance, long tick) {
lovisual$inDistance = inDistance;
lovisual$distanceTick = tick;
}
}

View file

@ -0,0 +1,44 @@
package dev.loki.lovisual.mixins.optimize.entity;
import dev.loki.lovisual.features.module.modules.misc.optimize.entity.CachedName;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
/**
* Stage 3: implements {@link CachedName} on {@link Entity} — adds the cached
* name-tag fields (name + tick) that {@link EntityRendererNameMixin} populates
* and reads back. Vanilla {@code Entity.getDisplayName()} rebuilds the team
* prefix + name + suffix on every call; renderers call it once per frame per
* entity in view, so caching by tick cuts the per-frame cost.
*
* <p>Ported from {@code zako.opt.mixin.entity.EntityNameMixin} (ZakoOpt,
* LGPL-3.0). Original author: Zako — https://t.me/StarikZako. Field/method
* prefix changed from {@code zakoopt$} to {@code lovisual$}.
*/
@Mixin(Entity.class)
public abstract class EntityNameMixin implements CachedName {
@Unique
private Component lovisual$name;
@Unique
private long lovisual$nameTick = Long.MIN_VALUE;
@Override
public Component lovisual$name() {
return lovisual$name;
}
@Override
public long lovisual$nameTick() {
return lovisual$nameTick;
}
@Override
public void lovisual$setName(Component name, long tick) {
lovisual$name = name;
lovisual$nameTick = tick;
}
}

View file

@ -0,0 +1,35 @@
package dev.loki.lovisual.mixins.optimize.entity;
import dev.loki.lovisual.features.module.modules.misc.optimize.entity.StateEntity;
import net.minecraft.client.renderer.entity.state.EntityRenderState;
import net.minecraft.world.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
/**
* Stage 3: implements {@link StateEntity} on {@link EntityRenderState} — adds
* the back-reference field to the source {@link Entity}. Render-states are
* rebuilt every frame, so they can't keep their own pointer — the
* {@link EntityRendererStateMixin} captures it once per state construction
* via {@code createRenderState} and downstream renderers read it back.
*
* <p>Ported from {@code zako.opt.mixin.entity.EntityRenderStateEntityMixin}
* (ZakoOpt, LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
* Prefix changed from {@code zakoopt$} to {@code lovisual$}.
*/
@Mixin(EntityRenderState.class)
public abstract class EntityRenderStateEntityMixin implements StateEntity {
@Unique
private Entity lovisual$entity;
@Override
public Entity lovisual$entity() {
return lovisual$entity;
}
@Override
public void lovisual$entity(Entity entity) {
lovisual$entity = entity;
}
}

View file

@ -0,0 +1,44 @@
package dev.loki.lovisual.mixins.optimize.entity;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import dev.loki.lovisual.features.module.modules.misc.optimize.entity.CachedStack;
import dev.loki.lovisual.render.engine.optimize.OptimizeToggles;
import org.lwjgl.system.MemoryStack;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
/**
* Stage 3: hot-path helper for Sodium's entity cuboid renderer — wraps
* {@code MemoryStack.stackPush()} so the render thread's stack is cached by
* identity instead of being looked up via ThreadLocal on every cuboid. Sodium
* calls this once per cuboid per frame (thousands of times for dense entity
* scenes), so the cached lookup is a measurable win on the hot path.
*
* <p>Uses {@code WrapOperation} rather than {@code @Redirect} because
* MixinExtras 0.5.4 (which Fabric Loader 0.19.3 ships with) crashes on a
* static {@code @Redirect} here — pinned to 0.5.5 in {@code build.gradle},
* but the convention is kept for safety. The mixin is registered with
* {@code remap = false} because the target is a Sodium class, not a Mojang one.
*
* <p>Ported from {@code zako.opt.mixin.entity.SodiumEntityRendererMixin}
* (ZakoOpt, LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
* Gated on the {@code microOpts} toggle via {@link OptimizeToggles#microOpts()}.
* When the toggle is off, the original runs unchanged.
*
* <p>Registered conditionally via {@code LoVisualMixinPlugin} — only applied
* when Sodium is loaded. See {@code lovisual.mixins.json} entry in the
* {@code client} array.
*/
@Mixin(targets = "net.caffeinemc.mods.sodium.client.render.immediate.model.EntityRenderer", remap = false)
public abstract class SodiumEntityRendererMixin {
@WrapOperation(
method = "renderCuboid",
at = @At(value = "INVOKE",
target = "Lorg/lwjgl/system/MemoryStack;stackPush()Lorg/lwjgl/system/MemoryStack;")
)
private static MemoryStack lovisual$cachedStack(Operation<MemoryStack> original) {
return OptimizeToggles.microOpts() ? CachedStack.push() : original.call();
}
}

View file

@ -0,0 +1,51 @@
package dev.loki.lovisual.mixins.optimize.entity.renderer;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import dev.loki.lovisual.features.module.modules.misc.optimize.entity.CachedName;
import dev.loki.lovisual.render.engine.optimize.OptimizeToggles;
import net.minecraft.client.renderer.entity.EntityRenderer;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
/**
* Stage 3: caches {@code Entity.getDisplayName()} per tick. The vanilla
* implementation rebuilds a {@link Component} (team prefix + name + suffix)
* every call; renderers call it once per frame per entity in view. Wrapping
* the call here keeps the per-frame cost to one identity check + read,
* instead of a full Component rebuild + style lookup.
*
* <p>The cache is keyed by game tick — name parts only change on tick
* boundaries (team membership, name change). On the first call of a tick the
* original runs and the result is stored; subsequent calls in the same tick
* return the cached instance.
*
* <p>Ported from {@code zako.opt.mixin.entity.EntityRendererNameMixin}
* (ZakoOpt, LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
* Gated on the {@code microOpts} toggle via {@link OptimizeToggles#microOpts()}.
* When the toggle is off, the original runs unchanged.
*/
@Mixin(EntityRenderer.class)
public abstract class EntityRendererNameMixin {
@WrapOperation(
method = "getNameTag",
at = @At(value = "INVOKE",
target = "Lnet/minecraft/world/entity/Entity;getDisplayName()Lnet/minecraft/network/chat/Component;")
)
private Component lovisual$cachedName(Entity entity, Operation<Component> original) {
if (!OptimizeToggles.microOpts()) {
return original.call(entity);
}
if (!(entity instanceof CachedName cache)) {
return original.call(entity);
}
long tick = entity.level().getGameTime();
if (cache.lovisual$nameTick() != tick) {
cache.lovisual$setName(original.call(entity), tick);
}
return cache.lovisual$name();
}
}

View file

@ -0,0 +1,43 @@
package dev.loki.lovisual.mixins.optimize.entity.renderer;
import com.llamalad7.mixinextras.sugar.Local;
import dev.loki.lovisual.features.module.modules.misc.optimize.entity.StateEntity;
import net.minecraft.client.renderer.entity.EntityRenderer;
import net.minecraft.client.renderer.entity.state.EntityRenderState;
import net.minecraft.world.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/**
* Stage 3: captures the source {@link Entity} on every freshly-built
* {@link EntityRenderState}. Vanilla's {@code createRenderState(entity, partial)}
* returns a state without back-reference; this inject at RETURN stores the
* entity on the state via the {@link StateEntity} contract so downstream
* renderers (model feature renderer, item feature renderer, future LOD mixins)
* can reach the live entity.
*
* <p>Ported from {@code zako.opt.mixin.entity.EntityRendererStateMixin}
* (ZakoOpt, LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
* The original used {@code @ModifyReturnValue}; LoVisual uses {@code @Inject}
* at RETURN with {@code CallbackInfoReturnable} so the mixin stays simple and
* doesn't rely on the (more fragile across versions) MixinExtras return-value
* transformer — the captured state is set as a side effect, not returned.
*/
@Mixin(EntityRenderer.class)
public abstract class EntityRendererStateMixin {
@Inject(
method = "createRenderState(Lnet/minecraft/world/entity/Entity;F)Lnet/minecraft/client/renderer/entity/state/EntityRenderState;",
at = @At("RETURN")
)
private void lovisual$rememberEntity(Entity entity, float partialTick,
CallbackInfoReturnable<EntityRenderState> cir,
@Local(argsOnly = true) Entity localEntity) {
EntityRenderState state = cir.getReturnValue();
if (state instanceof StateEntity stateEntity) {
stateEntity.lovisual$entity(localEntity);
}
}
}

View file

@ -33,7 +33,14 @@
"world.level.LevelMixin"
],
"client": [
"optimize.block.BaseSpawnerMixin",
"optimize.block.BlockEntityMixin",
"optimize.entity.EntityNameMixin",
"optimize.entity.EntityRenderStateEntityMixin",
"optimize.entity.LivingEntityLodMixin",
"optimize.entity.SodiumEntityRendererMixin",
"optimize.entity.renderer.EntityRendererNameMixin",
"optimize.entity.renderer.EntityRendererStateMixin",
"optimize.gl.GlCommandEncoderClearMixin",
"optimize.gl.GlCommandEncoderTexBufferMixin",
"optimize.gl.RenderTypesMixin",