Handle MC running at 0 FPS

This commit is contained in:
James Seibel
2026-01-31 10:22:23 -06:00
parent dfed2c8966
commit 00be9a3c4f
4 changed files with 66 additions and 7 deletions
@@ -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)
{
@@ -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<Runnable> 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
@@ -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). <br><br>
*
* Any tasks submitted here will be run on the render thread.
*
* @see GLProxy#queueRunningOnRenderThread(Runnable)
*/
void executeOnRenderThread(Runnable runnable);
//=============//
@@ -57,6 +57,8 @@ public interface IMinecraftRenderWrapper extends IBindable
/** Measured in chunks */
int getRenderDistance();
int getFrameLimit();
boolean mcRendersToFrameBuffer();
boolean runningLegacyOpenGL();