feat(mod): Stage 3 (partial) — RenderBackendOps interface + 3 GL mixins
Per mod/docs/optimize-layer-design.md Stage 3 (partial). This commit ships the simplest GL mixins and the strategy interface that splits GL/Vulkan backends. SkinAtlas/AnimFreeze/HudCache/FrameFence deferred to a later pass because they're more complex (vertex-format / glCopyImageSubData / RenderPipeline+RenderPass). RenderBackendOps interface (features/.../optimize/gl/RenderBackendOps.java): - Backend-agnostic surface for lazy clear, TBO cache, RenderTypes cache. - current() resolves the active backend — returns GlRenderBackendOps on GL, null on Vulkan (so all WrapOperations fall through to the vanilla call). - Stage 4 will add a VulkanRenderBackendOps no-op implementation. GlRenderBackendOps (features/.../optimize/gl/GlRenderBackendOps.java): - Concrete GL implementation. Three caches ported from ZakoOpt: * Lazy clear (from zako.opt.mixin.gl.GlCommandEncoderClearMixin). * TBO cache (from zako.opt.gl.TexBufferCache + the TexBufferMixin). * RenderTypes cache (from zako.opt.mixin.gl.RenderTypesMixin). - All three gated on OptimizeToggles.microOpts() (the Stage 1 'micro_opts' toggle wired in Optimize.java). - Includes test helpers (resetForTesting, boundTboTextureForTesting, cachedRenderTypeCountForTesting). Three new mixins (mixins/optimize/): - RenderTypesMixin: WrapOperation on RenderTypes.entityTranslucent's BiFunction.apply — cache by Identifier when flag=true (the common case). Drops the per-call Objects.hash(Pair) hit. - GlCommandEncoderClearMixin: WrapOperation on GlCommandEncoder's clearColorAndDepthTextures → GlStateManager._glBindFramebuffer — skip the bind-framebuffer-0 because the next render pass binds its own target anyway and GlStateManager's cache already skips redundant binds. - GlCommandEncoderTexBufferMixin: WrapOperation on GlCommandEncoder's trySetup → glBindTexture (track bound) and glTexBuffer (skip re-attach when the same (format, buffer) pair is already attached). Re-attaching makes the driver revalidate the texture (clouds: ~2% of a frame). lovisual.mixins.json: - Register all three mixins + OptimizeDebugHudMixin in the 'client' array, alphabetically under optimize.* (before accessors.*). OptimizeDebugHudMixin (mixins/optimize/OptimizeDebugHudMixin.java): - F3 overlay for the Optimize layer — Stage 2 of the design doc. - Adds three blocks of lines to F3 (after the existing LoVisual Engine block): * 'LoVisual Optimize' — toggles summary + budget mode + scale + frame time. * 'frame:' — EMA-averaged per-section timings (entities/particles/HUD/effects/text). * 'props:' — when any -Dlovisual.opt.* is set, list which ones. - Mirrors the existing hud.DebugHudMixin pattern (ModifyArg on extractLines). Tests (JUnit 5): 11 new tests in GlRenderBackendOpsTest — pure-logic pieces of RenderBackendOps + Identifier equality/hash. Total suite: 849 tests, 0 failures. checkFolderLimit: OK (0 folders >4).
This commit is contained in:
parent
64f27ee9e3
commit
e88718a32d
8 changed files with 707 additions and 0 deletions
|
|
@ -0,0 +1,140 @@
|
||||||
|
package dev.loki.lovisual.features.module.modules.misc.optimize.gl;
|
||||||
|
|
||||||
|
import dev.loki.lovisual.render.engine.optimize.OptimizeToggles;
|
||||||
|
import it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GL backend implementation of {@link RenderBackendOps}.
|
||||||
|
*
|
||||||
|
* <p>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).
|
||||||
|
*
|
||||||
|
* <p>Ported from ZakoOpt's three GL caches:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code zako.opt.mixin.gl.GlCommandEncoderClearMixin} (lazy clear) — wrapped in {@link #skipBindFramebufferZero(int)}.</li>
|
||||||
|
* <li>{@code zako.opt.gl.TexBufferCache} + {@code zako.opt.mixin.gl.GlCommandEncoderTexBufferMixin} — wrapped in the TBO-cache methods.</li>
|
||||||
|
* <li>{@code zako.opt.mixin.gl.RenderTypesMixin} — wrapped in the RenderTypes-cache methods.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>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<Identifier, Object> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
*
|
||||||
|
* <p>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.
|
||||||
|
*
|
||||||
|
* <p>Stage 3 ships:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #lazyClearSupported()} / {@link #skipBindFramebufferZero(int)} — lazy clear</li>
|
||||||
|
* <li>{@link #tboCacheSupported()} / {@link #tboAlreadyAttached(int, int)} /
|
||||||
|
* {@link #markTboAttached(int, int)} / {@link #trackBoundTboTexture(int)} /
|
||||||
|
* {@link #forgetTboCache()} — TBO cache</li>
|
||||||
|
* <li>{@link #renderTypesCacheSupported()} / {@link #renderTypeFor(Identifier)} /
|
||||||
|
* {@link #cacheRenderType(Identifier, Object)} — RenderTypes cache</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
*
|
||||||
|
* <p>Ported from {@code zako.opt.mixin.gl.GlCommandEncoderClearMixin} (ZakoOpt,
|
||||||
|
* LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
|
||||||
|
*
|
||||||
|
* <p>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<Void> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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}).
|
||||||
|
*
|
||||||
|
* <p>Ported from {@code zako.opt.mixin.gl.GlCommandEncoderTexBufferMixin} (ZakoOpt,
|
||||||
|
* LGPL-3.0). Original author: Zako — https://t.me/StarikZako.
|
||||||
|
*
|
||||||
|
* <p>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<Void> 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<Void> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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}.
|
||||||
|
*
|
||||||
|
* <p>Injects three groups of lines into the F3 text list, prepended before
|
||||||
|
* the existing LoVisual engine block:
|
||||||
|
* <ul>
|
||||||
|
* <li>Optimize status — which toggles are on, plus the budget mode + target FPS.</li>
|
||||||
|
* <li>Frame profiler — EMA-averaged per-section timings (entities/particles/HUD/effects/text).</li>
|
||||||
|
* <li>Property overrides — when any {@code -Dlovisual.opt.*} is set, list which ones.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>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<String> lovisual$addOptimizeInfo(List<String> list) {
|
||||||
|
if (list == null) return list;
|
||||||
|
Optimize module = Modules.get(Optimize.class);
|
||||||
|
if (module == null || !module.isEnabled()) {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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}.
|
||||||
|
*
|
||||||
|
* <p>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).
|
||||||
|
*
|
||||||
|
* <p>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<Object, Object, Object> memo, Object id, Object flag, Operation<Object> original) {
|
||||||
|
RenderBackendOps backend = RenderBackendOps.current();
|
||||||
|
if (backend == null || !backend.renderTypesCacheSupported()) {
|
||||||
|
return original.call(memo, id, flag);
|
||||||
|
}
|
||||||
|
// Only cache when the flag is true — the false case depends on the flag too.
|
||||||
|
if (!(Boolean) flag) {
|
||||||
|
return original.call(memo, id, flag);
|
||||||
|
}
|
||||||
|
Identifier mcId = (Identifier) id;
|
||||||
|
RenderBackendOps.Identifier cacheKey = new RenderBackendOps.Identifier(mcId.getNamespace(), mcId.getPath());
|
||||||
|
Object cached = backend.renderTypeFor(cacheKey);
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
Object result = original.call(memo, id, flag);
|
||||||
|
if (result instanceof RenderType) {
|
||||||
|
backend.cacheRenderType(cacheKey, result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -33,6 +33,10 @@
|
||||||
"world.level.LevelMixin"
|
"world.level.LevelMixin"
|
||||||
],
|
],
|
||||||
"client": [
|
"client": [
|
||||||
|
"optimize.GlCommandEncoderClearMixin",
|
||||||
|
"optimize.GlCommandEncoderTexBufferMixin",
|
||||||
|
"optimize.OptimizeDebugHudMixin",
|
||||||
|
"optimize.RenderTypesMixin",
|
||||||
"accessors.blaze3d.GlTextureInvoker",
|
"accessors.blaze3d.GlTextureInvoker",
|
||||||
"accessors.blaze3d.GpuDeviceAccessor",
|
"accessors.blaze3d.GpuDeviceAccessor",
|
||||||
"accessors.blaze3d.ShaderLoaderDefinitionsAccessor",
|
"accessors.blaze3d.ShaderLoaderDefinitionsAccessor",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,185 @@
|
||||||
|
package dev.loki.lovisual.features.module.modules.misc.optimize.gl;
|
||||||
|
|
||||||
|
import dev.loki.lovisual.render.engine.optimize.OptimizeToggles;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies {@link GlRenderBackendOps} — the GL backend implementation of
|
||||||
|
* {@link RenderBackendOps}. Pure-logic pieces only (no GL device lookup).
|
||||||
|
*/
|
||||||
|
public final class GlRenderBackendOpsTest {
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void reset() {
|
||||||
|
OptimizeToggles.reset();
|
||||||
|
GlRenderBackendOps.resetForTesting();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void currentReturnsGlInstance() {
|
||||||
|
RenderBackendOps ops = RenderBackendOps.current();
|
||||||
|
assertTrue(ops instanceof GlRenderBackendOps,
|
||||||
|
"Stage 3-prep: current() returns the GL singleton");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void lazyClearDisabledWhenMicroOptsOff() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
RenderBackendOps ops = RenderBackendOps.current();
|
||||||
|
assertFalse(ops.lazyClearSupported());
|
||||||
|
assertFalse(ops.skipBindFramebufferZero(0));
|
||||||
|
assertFalse(ops.skipBindFramebufferZero(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void lazyClearSkipsBindZeroWhenMicroOptsOn() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, true);
|
||||||
|
RenderBackendOps ops = RenderBackendOps.current();
|
||||||
|
assertTrue(ops.lazyClearSupported());
|
||||||
|
assertTrue(ops.skipBindFramebufferZero(0));
|
||||||
|
assertFalse(ops.skipBindFramebufferZero(5),
|
||||||
|
"Non-zero framebuffer should not be skipped");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tboCacheDisabledWhenMicroOptsOff() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
RenderBackendOps ops = RenderBackendOps.current();
|
||||||
|
assertFalse(ops.tboCacheSupported());
|
||||||
|
// Even after marking attached, lookup should be a no-op (returns false because not supported).
|
||||||
|
ops.trackBoundTboTexture(42);
|
||||||
|
assertFalse(ops.tboAlreadyAttached(0x8229, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tboCacheAttachedWhenSameFormatBuffer() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, true);
|
||||||
|
RenderBackendOps ops = RenderBackendOps.current();
|
||||||
|
ops.trackBoundTboTexture(42);
|
||||||
|
assertFalse(ops.tboAlreadyAttached(0x8229, 100),
|
||||||
|
"First lookup should be a miss");
|
||||||
|
ops.markTboAttached(0x8229, 100);
|
||||||
|
assertTrue(ops.tboAlreadyAttached(0x8229, 100),
|
||||||
|
"Same (format, buffer) pair should be cached as attached");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tboCacheMissOnDifferentFormatOrBuffer() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, true);
|
||||||
|
RenderBackendOps ops = RenderBackendOps.current();
|
||||||
|
ops.trackBoundTboTexture(42);
|
||||||
|
ops.markTboAttached(0x8229, 100);
|
||||||
|
assertFalse(ops.tboAlreadyAttached(0x8230, 100),
|
||||||
|
"Different format should be a miss");
|
||||||
|
assertFalse(ops.tboAlreadyAttached(0x8229, 101),
|
||||||
|
"Different buffer should be a miss");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tboCacheSeparatesPerBoundTexture() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, true);
|
||||||
|
RenderBackendOps ops = RenderBackendOps.current();
|
||||||
|
ops.trackBoundTboTexture(42);
|
||||||
|
ops.markTboAttached(0x8229, 100);
|
||||||
|
ops.trackBoundTboTexture(43);
|
||||||
|
assertFalse(ops.tboAlreadyAttached(0x8229, 100),
|
||||||
|
"Different bound texture should be a miss — cache is per-texture");
|
||||||
|
ops.trackBoundTboTexture(42);
|
||||||
|
assertTrue(ops.tboAlreadyAttached(0x8229, 100),
|
||||||
|
"Switching back to the first texture should still hit the cache");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void forgetTboCacheClearsEverything() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, true);
|
||||||
|
RenderBackendOps ops = RenderBackendOps.current();
|
||||||
|
ops.trackBoundTboTexture(42);
|
||||||
|
ops.markTboAttached(0x8229, 100);
|
||||||
|
ops.trackBoundTboTexture(43);
|
||||||
|
ops.markTboAttached(0x8230, 200);
|
||||||
|
ops.forgetTboCache();
|
||||||
|
ops.trackBoundTboTexture(42);
|
||||||
|
assertFalse(ops.tboAlreadyAttached(0x8229, 100));
|
||||||
|
ops.trackBoundTboTexture(43);
|
||||||
|
assertFalse(ops.tboAlreadyAttached(0x8230, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void renderTypesCacheLookup() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, true);
|
||||||
|
RenderBackendOps ops = RenderBackendOps.current();
|
||||||
|
RenderBackendOps.Identifier id = new RenderBackendOps.Identifier("minecraft", "textures/entity/skin.png");
|
||||||
|
assertNull(ops.renderTypeFor(id), "Fresh cache should miss");
|
||||||
|
Object type = new Object();
|
||||||
|
ops.cacheRenderType(id, type);
|
||||||
|
assertSame(type, ops.renderTypeFor(id),
|
||||||
|
"Cache should return the same instance we stored");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void renderTypesCacheDisabledWhenMicroOptsOff() {
|
||||||
|
OptimizeToggles.apply(
|
||||||
|
false, 0,
|
||||||
|
false, false, false,
|
||||||
|
false, 0,
|
||||||
|
false, false, false, false);
|
||||||
|
RenderBackendOps ops = RenderBackendOps.current();
|
||||||
|
assertFalse(ops.renderTypesCacheSupported());
|
||||||
|
RenderBackendOps.Identifier id = new RenderBackendOps.Identifier("minecraft", "test");
|
||||||
|
// cacheRenderType is a no-op when disabled.
|
||||||
|
ops.cacheRenderType(id, new Object());
|
||||||
|
assertNull(ops.renderTypeFor(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void identifierEqualityAndHash() {
|
||||||
|
RenderBackendOps.Identifier a = new RenderBackendOps.Identifier("mc", "path");
|
||||||
|
RenderBackendOps.Identifier b = new RenderBackendOps.Identifier("mc", "path");
|
||||||
|
RenderBackendOps.Identifier c = new RenderBackendOps.Identifier("mc", "other");
|
||||||
|
RenderBackendOps.Identifier d = new RenderBackendOps.Identifier("other", "path");
|
||||||
|
assertEquals(a, b);
|
||||||
|
assertEquals(a.hashCode(), b.hashCode());
|
||||||
|
assertFalse(a.equals(c));
|
||||||
|
assertFalse(a.equals(d));
|
||||||
|
assertEquals("mc:path", a.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue