From 00be9a3c4fa12fa9ce56ed8f9635985ecd4977e9 Mon Sep 17 00:00:00 2001 From: James Seibel Date: Sat, 31 Jan 2026 10:22:23 -0600 Subject: [PATCH] Handle MC running at 0 FPS --- .../core/api/internal/ClientApi.java | 4 +- .../core/render/glObject/GLProxy.java | 55 +++++++++++++++++-- .../minecraft/IMinecraftClientWrapper.java | 12 ++++ .../minecraft/IMinecraftRenderWrapper.java | 2 + 4 files changed, 66 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java index 6943e3a7a..16f179e8a 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java @@ -437,10 +437,10 @@ public class ClientApi try { // make sure the GLProxy is created for future use - GLProxy.getInstance(); + GLProxy glProxy = GLProxy.getInstance(); // these tasks always need to be called, regardless of whether the renderer is enabled or not to prevent memory leaks - GLProxy.runRenderThreadTasks(); + glProxy.runRenderThreadTasks(); } catch (Exception e) { diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/glObject/GLProxy.java b/core/src/main/java/com/seibel/distanthorizons/core/render/glObject/GLProxy.java index 147ff3811..e3740c34d 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/glObject/GLProxy.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/glObject/GLProxy.java @@ -26,8 +26,10 @@ import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.jar.EPlatform; import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +import com.seibel.distanthorizons.core.util.TimerUtil; import com.seibel.distanthorizons.core.util.objects.GLMessages.*; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper; import com.seibel.distanthorizons.coreapi.ModInfo; import org.lwjgl.glfw.GLFW; import org.lwjgl.opengl.GL; @@ -38,6 +40,7 @@ import org.lwjgl.opengl.GLUtil; import java.io.PrintStream; import java.util.Collections; import java.util.Set; +import java.util.Timer; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; @@ -48,6 +51,7 @@ import java.util.concurrent.ConcurrentLinkedQueue; public class GLProxy { private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); + private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class); public static final DhLogger LOGGER = new DhLoggerBuilder() .fileLevelConfig(Config.Common.Logging.logRendererGLEventToFile) @@ -58,6 +62,10 @@ public class GLProxy private static final ConcurrentLinkedQueue RENDER_THREAD_RUNNABLE_QUEUE = new ConcurrentLinkedQueue<>(); + private static final Timer TIMER = TimerUtil.CreateTimer("Cleanup timer"); + private static final long MS_BETWEEN_CLEANUP_TICKS = 1_000L; + private static final long MS_BEFORE_RUN_CLEANUP_TIMER = 1_000L; + private static GLProxy instance = null; @@ -98,6 +106,8 @@ public class GLProxy null ); + private long msSinceGlTasksRun = System.currentTimeMillis(); + //=============// @@ -194,6 +204,8 @@ public class GLProxy LOGGER.info("GPU Vendor [" + vendor + "] with OS [" + EPlatform.get().getName() + "], Preferred upload method is [" + this.preferredUploadMethod + "]."); + TIMER.scheduleAtFixedRate(TimerUtil.createTimerTask(this::manualGlCleanupTick), MS_BETWEEN_CLEANUP_TICKS, MS_BETWEEN_CLEANUP_TICKS); + //==========// // clean up // @@ -273,9 +285,22 @@ public class GLProxy * Doesn't do any thread/GL Context validation. * Running this outside of the render thread may cause crashes or other issues. */ - public static void runRenderThreadTasks() + public void runRenderThreadTasks() { - long startTime = System.nanoTime(); + int frameLimit = MC_RENDER.getFrameLimit(); + if (frameLimit <= 1) + { + frameLimit = 4; // 240 FPS + } + + // https://fpstoms.com/ + int msPerFrame = 1000 / frameLimit; + this.runRenderThreadTasks(msPerFrame); + } + private void runRenderThreadTasks(long msMaxRunTime) + { + long startTimeMs = System.currentTimeMillis(); + this.msSinceGlTasksRun = startTimeMs; Runnable runnable = RENDER_THREAD_RUNNABLE_QUEUE.poll(); while(runnable != null) @@ -283,9 +308,9 @@ public class GLProxy runnable.run(); // only try running for 4ms (240 FPS) at a time to prevent random lag spikes - long currentTime = System.nanoTime(); - long runDuration = currentTime - startTime; - if (runDuration > 4_000_000) + long currentTimeMs = System.currentTimeMillis(); + long runDuration = currentTimeMs - startTimeMs; + if (runDuration > msMaxRunTime) { break; } @@ -294,6 +319,26 @@ public class GLProxy } } + /** + * Should only be called if our render code isn't being hit for some reason. + * Normally this only happens if there's a mod that limits MC's framerate to 0. + */ + private void manualGlCleanupTick() + { + long nowMs = System.currentTimeMillis(); + long msSinceLast = nowMs - this.msSinceGlTasksRun; + if (msSinceLast > MS_BEFORE_RUN_CLEANUP_TIMER) + { + return; + } + + // We haven't gotten a frame for a while, + // this means we could have GL jobs building up. + // Run the queued tasks on MC's executor (hopefully this should always run, + // even if DH's render code isn't being hit). + MC.executeOnRenderThread(() -> this.runRenderThreadTasks(1_000)); + } + //endregion diff --git a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftClientWrapper.java b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftClientWrapper.java index f717a9204..c7e56fbb7 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftClientWrapper.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftClientWrapper.java @@ -120,6 +120,18 @@ public interface IMinecraftClientWrapper extends IBindable */ void crashMinecraft(String errorMessage, Throwable exception); + /** + * This is only designed to be used internally by {@link GLProxy} + * since it handles task frame limiting (reducing/preventing stuttering) + * whereas this method causes the task to be run whenever MC decides to + * (likely all at once the next frame).

+ * + * Any tasks submitted here will be run on the render thread. + * + * @see GLProxy#queueRunningOnRenderThread(Runnable) + */ + void executeOnRenderThread(Runnable runnable); + //=============// diff --git a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftRenderWrapper.java b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftRenderWrapper.java index b8101302f..2a92fdbb5 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftRenderWrapper.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/minecraft/IMinecraftRenderWrapper.java @@ -57,6 +57,8 @@ public interface IMinecraftRenderWrapper extends IBindable /** Measured in chunks */ int getRenderDistance(); + int getFrameLimit(); + boolean mcRendersToFrameBuffer(); boolean runningLegacyOpenGL();