Resolves the current GL device via {@code GlBackendAccess.current()} —
+ * returns {@code null} on Vulkan so all mixins short-circuit to the vanilla
+ * call. The cache state is static and render-thread-only (ZakoOpt does the
+ * same — the GL command encoder runs single-threaded).
+ *
+ *
Ported from ZakoOpt's three GL caches:
+ *
+ *
{@code zako.opt.mixin.gl.GlCommandEncoderClearMixin} (lazy clear) — wrapped in {@link #skipBindFramebufferZero(int)}.
+ *
{@code zako.opt.gl.TexBufferCache} + {@code zako.opt.mixin.gl.GlCommandEncoderTexBufferMixin} — wrapped in the TBO-cache methods.
+ *
{@code zako.opt.mixin.gl.RenderTypesMixin} — wrapped in the RenderTypes-cache methods.
+ *
+ *
+ *
Each toggle is gated by {@link OptimizeToggles#microOpts()} (the Stage 1
+ * "micro_opts" toggle wired into {@code Optimize.java}); ZakoOpt splits this
+ * into {@code lookupCaches}, {@code microOpts}, {@code tboCache} — Stage 1
+ * collapses them, Stage 2 may split if profiling warrants.
+ */
+public final class GlRenderBackendOps implements RenderBackendOps {
+
+ private static final GlRenderBackendOps INSTANCE = new GlRenderBackendOps();
+
+ // --- TBO cache (ported from zako.opt.gl.TexBufferCache) ---
+ /** Maps bound-texture-id -> packed (format << 32) | (buffer & 0xFFFFFFFFL). -1 = nothing attached. */
+ private final Int2LongOpenHashMap tboAttached = new Int2LongOpenHashMap();
+ private int boundTboTexture;
+
+ // --- RenderTypes cache (ported from zako.opt.mixin.gl.RenderTypesMixin) ---
+ private final Map translucentCache = new ConcurrentHashMap<>();
+
+ private GlRenderBackendOps() {
+ tboAttached.defaultReturnValue(-1L);
+ }
+
+ /**
+ * @return the singleton GL backend instance, or {@code null} if the
+ * current device is not GL (e.g. Vulkan backend active).
+ */
+ public static RenderBackendOps current() {
+ // Stage 3-prep: we don't yet wire the GL device check (would require
+ // the GL device accessor mixin). Return INSTANCE unconditionally for
+ // now; the actual device-type check is added alongside the first GL
+ // mixin that needs it (RenderTypesMixin — Stage 3b).
+ return INSTANCE;
+ }
+
+ // --- Lazy clear ---
+
+ @Override
+ public boolean lazyClearSupported() {
+ return OptimizeToggles.microOpts();
+ }
+
+ @Override
+ public boolean skipBindFramebufferZero(int framebuffer) {
+ if (!lazyClearSupported()) return false;
+ return framebuffer == 0;
+ }
+
+ // --- TBO cache ---
+
+ @Override
+ public boolean tboCacheSupported() {
+ return OptimizeToggles.microOpts();
+ }
+
+ @Override
+ public void trackBoundTboTexture(int texture) {
+ boundTboTexture = texture;
+ }
+
+ @Override
+ public boolean tboAlreadyAttached(int format, int buffer) {
+ if (!tboCacheSupported()) return false;
+ return tboAttached.getOrDefault(boundTboTexture, -1L) == packTboKey(format, buffer);
+ }
+
+ @Override
+ public void markTboAttached(int format, int buffer) {
+ if (!tboCacheSupported()) return;
+ tboAttached.put(boundTboTexture, packTboKey(format, buffer));
+ }
+
+ @Override
+ public void forgetTboCache() {
+ tboAttached.clear();
+ }
+
+ private static long packTboKey(int format, int buffer) {
+ return ((long) format << 32) | (buffer & 0xFFFFFFFFL);
+ }
+
+ // --- RenderTypes cache ---
+
+ @Override
+ public boolean renderTypesCacheSupported() {
+ return OptimizeToggles.microOpts();
+ }
+
+ @Override
+ public Object renderTypeFor(Identifier id) {
+ if (!renderTypesCacheSupported()) return null;
+ return translucentCache.get(id);
+ }
+
+ @Override
+ public void cacheRenderType(Identifier id, Object type) {
+ if (!renderTypesCacheSupported()) return;
+ translucentCache.put(id, type);
+ }
+
+ // --- Test helpers ---
+
+ /** Test-only: clear all caches. */
+ static void resetForTesting() {
+ INSTANCE.tboAttached.clear();
+ INSTANCE.boundTboTexture = 0;
+ INSTANCE.translucentCache.clear();
+ }
+
+ /** Test-only: peek the bound TBO texture. */
+ static int boundTboTextureForTesting() {
+ return INSTANCE.boundTboTexture;
+ }
+
+ /** Test-only: count of cached render types. */
+ static int cachedRenderTypeCountForTesting() {
+ return INSTANCE.translucentCache.size();
+ }
+}
diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/optimize/gl/RenderBackendOps.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/optimize/gl/RenderBackendOps.java
new file mode 100644
index 00000000..b0a2c59b
--- /dev/null
+++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/optimize/gl/RenderBackendOps.java
@@ -0,0 +1,118 @@
+package dev.loki.lovisual.features.module.modules.misc.optimize.gl;
+
+/**
+ * Backend-agnostic surface for the optimization layer's GL/Vulkan hooks.
+ *
+ *
Stage 3 of {@code mod/docs/optimize-layer-design.md} splits the engine
+ * into "what to do" (this interface) and "how to do it on this backend"
+ * ({@link GlRenderBackendOps} for OpenGL, a future {@code VulkanRenderBackendOps}
+ * for Vulkan). The Optimize mixins call {@link #current()} once and dispatch
+ * through this surface; the Vulkan implementation returns {@code false} from
+ * every {@code supported*} method so the mixin's {@code WrapOperation} falls
+ * through to the original call.
+ *
+ *
Stage 4 will add Vulkan-specific paths ({@code vkCmdCopyImage},
+ * {@code VkPipelineCache}) by implementing this surface with a no-op strategy
+ * for the GL-only methods.
+ */
+public interface RenderBackendOps {
+
+ /** Resolve the current backend — GL backend if GL is active, otherwise {@code null}. */
+ static RenderBackendOps current() {
+ return GlRenderBackendOps.current();
+ }
+
+ // --- Lazy clear ---
+
+ /** True when this backend can skip the redundant bind-framebuffer-0 after a clear. */
+ boolean lazyClearSupported();
+
+ /**
+ * Called from {@code GlCommandEncoder.clearColorAndDepthTextures} on the
+ * {@code GlStateManager._glBindFramebuffer(target, framebuffer)} call.
+ *
+ * @param framebuffer the framebuffer GlStateManager is about to bind
+ * @return {@code true} to skip the bind (lazy clear — next pass binds its
+ * own target anyway, the GlStateManager cache skips redundant binds)
+ */
+ boolean skipBindFramebufferZero(int framebuffer);
+
+ // --- TBO cache ---
+
+ /** True when this backend can cache {@code glTexBuffer} attachments per-texture. */
+ boolean tboCacheSupported();
+
+ /** Track the most recent {@code glBindTexture} target's texture ID. */
+ void trackBoundTboTexture(int texture);
+
+ /** True when the {@code (format, buffer)} pair is already attached to the bound texture. */
+ boolean tboAlreadyAttached(int format, int buffer);
+
+ /** Mark the {@code (format, buffer)} pair as attached to the bound texture. */
+ void markTboAttached(int format, int buffer);
+
+ /** Forget all TBO attachments — called when a {@code GlBuffer.Direct} is closed. */
+ void forgetTboCache();
+
+ // --- RenderTypes cache ---
+
+ /** True when this backend can cache {@code RenderTypes.entityTranslucent(id, flag=true)}. */
+ boolean renderTypesCacheSupported();
+
+ /**
+ * Look up a cached render type for the given identifier.
+ *
+ * @param id the texture identifier
+ * @return the cached render type, or {@code null} if not cached
+ */
+ Object renderTypeFor(Identifier id);
+
+ /**
+ * Cache a render type for the given identifier.
+ *
+ * @param id the texture identifier
+ * @param type the render type returned by the original lookup
+ */
+ void cacheRenderType(Identifier id, Object type);
+
+ /** Tiny value-class carrying a texture identifier — kept out of the public API of {@link RenderBackendOps}. */
+ final class Identifier {
+ private final String namespace;
+ private final String path;
+
+ public Identifier(String namespace, String path) {
+ this.namespace = namespace;
+ this.path = path;
+ }
+
+ public String namespace() { return namespace; }
+ public String path() { return path; }
+
+ @Override
+ public int hashCode() {
+ return namespace.hashCode() * 31 + path.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return o instanceof Identifier i
+ && i.namespace.equals(namespace)
+ && i.path.equals(path);
+ }
+
+ @Override
+ public String toString() {
+ return namespace + ":" + path;
+ }
+ }
+}
diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/optimize/GlCommandEncoderClearMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/optimize/GlCommandEncoderClearMixin.java
new file mode 100644
index 00000000..29ea2bd2
--- /dev/null
+++ b/mod/src/main/java/dev/loki/lovisual/mixins/optimize/GlCommandEncoderClearMixin.java
@@ -0,0 +1,40 @@
+package dev.loki.lovisual.mixins.optimize;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import com.mojang.blaze3d.opengl.GlStateManager;
+import dev.loki.lovisual.features.module.modules.misc.optimize.gl.RenderBackendOps;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+/**
+ * Stage 3c: lazy clear — after {@code clearColorAndDepthTextures}, vanilla
+ * binds framebuffer 0 (the default framebuffer) before the next pass runs.
+ * The next pass binds its own target anyway, and {@code GlStateManager}'s
+ * cache skips the redundant bind. This mixin drops the explicit bind-0 so
+ * we don't pay for a {@code glBindFramebuffer(GL_FRAMEBUFFER, 0)} that's
+ * immediately overwritten.
+ *
+ *
Ported from {@code zako.opt.mixin.gl.GlCommandEncoderClearMixin} (ZakoOpt,
+ * LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
+ *
+ *
Gated on the {@code microOpts} toggle via {@link RenderBackendOps#skipBindFramebufferZero(int)}.
+ * When the toggle is off, the original call runs unchanged.
+ */
+@Mixin(targets = "com.mojang.blaze3d.opengl.GlCommandEncoder")
+public abstract class GlCommandEncoderClearMixin {
+
+ @WrapOperation(
+ method = "clearColorAndDepthTextures",
+ at = @At(value = "INVOKE",
+ target = "Lcom/mojang/blaze3d/opengl/GlStateManager;_glBindFramebuffer(II)V")
+ )
+ private void lovisual$skipBindZero(int target, int framebuffer, Operation original) {
+ RenderBackendOps backend = RenderBackendOps.current();
+ if (backend != null && backend.skipBindFramebufferZero(framebuffer)) {
+ // Skip the bind — the next render pass binds its own target anyway.
+ return;
+ }
+ original.call(target, framebuffer);
+ }
+}
diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/optimize/GlCommandEncoderTexBufferMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/optimize/GlCommandEncoderTexBufferMixin.java
new file mode 100644
index 00000000..ce313052
--- /dev/null
+++ b/mod/src/main/java/dev/loki/lovisual/mixins/optimize/GlCommandEncoderTexBufferMixin.java
@@ -0,0 +1,57 @@
+package dev.loki.lovisual.mixins.optimize;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import dev.loki.lovisual.features.module.modules.misc.optimize.gl.RenderBackendOps;
+import org.lwjgl.opengl.GL33C;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+/**
+ * Stage 3d: TBO cache — when {@code GlCommandEncoder.trySetup} binds a
+ * texel-buffer texture via {@code glBindTexture}, we record the texture ID.
+ * When {@code glTexBuffer(target, format, buffer)} is called for the same
+ * {@code (format, buffer)} pair that's already attached, we skip the call —
+ * re-attaching makes the driver revalidate the texture (clouds: ~2% of a
+ * frame in {@code glDrawElements}).
+ *
+ *
Ported from {@code zako.opt.mixin.gl.GlCommandEncoderTexBufferMixin} (ZakoOpt,
+ * LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
+ *
+ *
Gated on the {@code microOpts} toggle via
+ * {@link RenderBackendOps#tboCacheSupported()}. When the toggle is off, both
+ * {@code WrapOperation}s call the original.
+ */
+@Mixin(targets = "com.mojang.blaze3d.opengl.GlCommandEncoder")
+public abstract class GlCommandEncoderTexBufferMixin {
+
+ @WrapOperation(
+ method = "trySetup",
+ at = @At(value = "INVOKE",
+ target = "Lorg/lwjgl/opengl/GL33C;glBindTexture(II)V")
+ )
+ private void lovisual$trackBoundTboTexture(int target, int texture, Operation original) {
+ RenderBackendOps backend = RenderBackendOps.current();
+ if (backend != null && backend.tboCacheSupported()) {
+ backend.trackBoundTboTexture(texture);
+ }
+ original.call(target, texture);
+ }
+
+ @WrapOperation(
+ method = "trySetup",
+ at = @At(value = "INVOKE",
+ target = "Lorg/lwjgl/opengl/GL33C;glTexBuffer(III)V")
+ )
+ private void lovisual$texBufferOnce(int target, int format, int buffer, Operation original) {
+ RenderBackendOps backend = RenderBackendOps.current();
+ if (backend != null && backend.tboAlreadyAttached(format, buffer)) {
+ // Same (format, buffer) pair already attached to the bound texture — skip the re-attach.
+ return;
+ }
+ original.call(target, format, buffer);
+ if (backend != null) {
+ backend.markTboAttached(format, buffer);
+ }
+ }
+}
diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/optimize/OptimizeDebugHudMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/optimize/OptimizeDebugHudMixin.java
new file mode 100644
index 00000000..d42cf8f3
--- /dev/null
+++ b/mod/src/main/java/dev/loki/lovisual/mixins/optimize/OptimizeDebugHudMixin.java
@@ -0,0 +1,101 @@
+package dev.loki.lovisual.mixins.optimize;
+
+import dev.loki.lovisual.features.module.lifecycle.Modules;
+import dev.loki.lovisual.features.module.modules.misc.optimize.Optimize;
+import dev.loki.lovisual.render.engine.optimize.profile.FrameBudget;
+import dev.loki.lovisual.render.engine.optimize.profile.FrameProfiler;
+import dev.loki.lovisual.render.engine.optimize.profile.OptimizePropertyBridge;
+import dev.loki.lovisual.render.engine.optimize.OptimizeToggles;
+import net.minecraft.client.gui.components.DebugScreenOverlay;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.ModifyArg;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * F3 debug overlay for the Optimize layer — Stage 2 of
+ * {@code mod/docs/optimize-layer-design.md}.
+ *
+ *
Injects three groups of lines into the F3 text list, prepended before
+ * the existing LoVisual engine block:
+ *
+ *
Optimize status — which toggles are on, plus the budget mode + target FPS.
Property overrides — when any {@code -Dlovisual.opt.*} is set, list which ones.
+ *
+ *
+ *
The hook mirrors the existing {@code hud.DebugHudMixin}'s pattern: it
+ * {@code ModifyArg}s the {@code extractLines} call inside
+ * {@code DebugScreenOverlay.extractRenderState} and replaces the text list.
+ * Both mixins return a new list with their own lines prepended; whichever runs
+ * last sees the previous one's output in its input list, so both blocks appear
+ * in F3.
+ */
+@Mixin(DebugScreenOverlay.class)
+public abstract class OptimizeDebugHudMixin {
+
+ @ModifyArg(
+ method = "extractRenderState",
+ at = @At(
+ value = "INVOKE",
+ target = "Lnet/minecraft/client/gui/components/DebugScreenOverlay;extractLines(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Ljava/util/List;Z)V",
+ ordinal = 0
+ ),
+ index = 1
+ )
+ private List lovisual$addOptimizeInfo(List list) {
+ if (list == null) return list;
+ Optimize module = Modules.get(Optimize.class);
+ if (module == null || !module.isEnabled()) {
+ return list;
+ }
+
+ List out = new ArrayList<>(list.size() + 16);
+ out.add("§cLoVisual Optimize");
+ out.add(String.format("toggles: %s", togglesSummary()));
+ out.add(String.format("budget: %s @ %d fps (scale %.2f)",
+ FrameBudget.mode(),
+ FrameBudget.targetFps(),
+ FrameBudget.scale()));
+
+ FrameProfiler.Snapshot snap = FrameProfiler.snapshot();
+ if (snap.frameMs() > 0) {
+ out.add(String.format("frame: %.2f ms (%.0f fps)",
+ snap.frameMs(),
+ 1000.0 / Math.max(0.01, snap.frameMs())));
+ out.add(String.format(" entities=%.2fms particles=%.2fms hud=%.2fms effects=%.2fms text=%.2fms",
+ snap.entityMs(),
+ snap.particleMs(),
+ snap.hudMs(),
+ snap.effectMs(),
+ snap.textMs()));
+ }
+
+ if (OptimizePropertyBridge.hasOverrides()) {
+ out.add("props: " + OptimizePropertyBridge.lastResolved());
+ }
+
+ out.add("");
+ out.addAll(list);
+ return out;
+ }
+
+ private static String togglesSummary() {
+ StringBuilder sb = new StringBuilder();
+ if (OptimizeToggles.entityLod()) append(sb, "entity_lod@" + OptimizeToggles.entityLodDistance());
+ if (OptimizeToggles.particleLod()) append(sb, "particle_lod");
+ if (OptimizeToggles.spawnerCull()) append(sb, "spawner_cull@" + OptimizeToggles.spawnerDistance());
+ if (OptimizeToggles.spawnerTickCache()) append(sb, "spawner_tick_cache");
+ if (OptimizeToggles.blockEntityCache()) append(sb, "block_entity_cache");
+ if (OptimizeToggles.preparedTextCache()) append(sb, "prepared_text_cache");
+ if (OptimizeToggles.microOpts()) append(sb, "micro_opts");
+ return sb.length() == 0 ? "none" : sb.toString();
+ }
+
+ private static void append(StringBuilder out, String knob) {
+ if (out.length() > 0) out.append(", ");
+ out.append(knob);
+ }
+}
diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/optimize/RenderTypesMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/optimize/RenderTypesMixin.java
new file mode 100644
index 00000000..701211e1
--- /dev/null
+++ b/mod/src/main/java/dev/loki/lovisual/mixins/optimize/RenderTypesMixin.java
@@ -0,0 +1,62 @@
+package dev.loki.lovisual.mixins.optimize;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import dev.loki.lovisual.features.module.modules.misc.optimize.gl.RenderBackendOps;
+import net.minecraft.client.renderer.rendertype.RenderType;
+import net.minecraft.client.renderer.rendertype.RenderTypes;
+import net.minecraft.resources.Identifier;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+import java.util.function.BiFunction;
+
+/**
+ * Stage 3b: cache {@code RenderTypes.entityTranslucent(id, flag=true)} by
+ * identifier — vanilla uses {@code Util.memoize} keyed on {@code Pair(id, flag)},
+ * which {@code Objects.hash}-es the pair on every call, per entity per frame.
+ * When the flag is {@code true} (the common case — translucent skins, held
+ * items, etc.), the result depends only on the identifier, so we cache it
+ * directly in a {@code ConcurrentHashMap}.
+ *
+ *
Ported from {@code zako.opt.mixin.gl.RenderTypesMixin} (ZakoOpt, LGPL-3.0).
+ * Original author: Zako — https://t.me/StarikZako. The original cache logic
+ * is wrapped in {@link RenderBackendOps}; LoVisual's GL implementation is
+ * {@code GlRenderBackendOps}, which gates on {@code microOpts} (Stage 1 toggle).
+ *
+ *
When the toggle is off, the WrapOperation calls the original — vanilla
+ * behavior preserved. When the toggle is on and the flag is {@code true},
+ * the cache is consulted; on a miss, the original is called and the result
+ * is stored.
+ */
+@Mixin(RenderTypes.class)
+public abstract class RenderTypesMixin {
+
+ @WrapOperation(
+ method = "entityTranslucent(Lnet/minecraft/resources/Identifier;Z)Lnet/minecraft/client/renderer/rendertype/RenderType;",
+ at = @At(value = "INVOKE",
+ target = "Ljava/util/function/BiFunction;apply(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;")
+ )
+ private static Object lovisual$cacheTranslucent(
+ BiFunction