Add RenderThreadTaskHandler

This commit is contained in:
James Seibel
2026-03-09 16:30:45 -05:00
parent c84dbfceaf
commit 82c832a4af
5 changed files with 149 additions and 210 deletions
@@ -31,6 +31,8 @@ import com.seibel.distanthorizons.core.logging.f3.F3Screen;
import com.seibel.distanthorizons.core.network.messages.MessageRegistry;
import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.render.DhApiRenderProxy;
import com.seibel.distanthorizons.core.render.RenderParams;
import com.seibel.distanthorizons.core.render.RenderThreadTaskHandler;
import com.seibel.distanthorizons.core.render.renderer.*;
import com.seibel.distanthorizons.core.util.TimerUtil;
import com.seibel.distanthorizons.core.util.math.Vec3d;
@@ -49,7 +51,6 @@ import com.seibel.distanthorizons.api.enums.rendering.EDhApiDebugRendering;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel;
import com.seibel.distanthorizons.core.render.glObject.GLProxy;
import com.seibel.distanthorizons.core.world.AbstractDhWorld;
import com.seibel.distanthorizons.core.world.DhClientWorld;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
@@ -464,11 +465,8 @@ public class ClientApi
try
{
// make sure the GLProxy is created for future use
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();
RenderThreadTaskHandler.INSTANCE.runRenderThreadTasks();
}
catch (Exception e)
{
@@ -9,7 +9,7 @@ import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.network.event.internal.CloseInternalEvent;
import com.seibel.distanthorizons.core.network.messages.base.LevelInitMessage;
import com.seibel.distanthorizons.core.network.session.NetworkSession;
import com.seibel.distanthorizons.core.render.glObject.GLProxy;
import com.seibel.distanthorizons.core.render.RenderThreadTaskHandler;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import org.jetbrains.annotations.NotNull;
@@ -90,7 +90,7 @@ public class ClientPluginChannelApi
LOGGER.info("Server level key received: [" + msg.levelKey + "].");
GLProxy.queueRunningOnRenderThread(() ->
RenderThreadTaskHandler.INSTANCE.queueRunningOnRenderThread(() ->
{
IClientLevelWrapper clientLevel = MC.getWrappedClientLevel(true);
IServerKeyedClientLevel existingKeyedClientLevel = KEYED_CLIENT_LEVEL_MANAGER.getServerKeyedLevel();
@@ -1,196 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.config.gui;
import com.seibel.distanthorizons.core.jar.EPlatform;
import org.jetbrains.annotations.NotNull;
import org.lwjgl.system.jawt.JAWT;
import org.lwjgl.system.macosx.*;
import java.awt.*;
import java.lang.reflect.*;
import java.util.regex.*;
import static org.lwjgl.glfw.GLFWNativeCocoa.*;
import static org.lwjgl.glfw.GLFWNativeWin32.*;
import static org.lwjgl.glfw.GLFWNativeX11.*;
import static org.lwjgl.system.JNI.*;
import static org.lwjgl.system.jawt.JAWTFunctions.*;
import static org.lwjgl.system.macosx.ObjCRuntime.*;
// Some of the code is from https://github.com/LWJGL/lwjgl3/blob/master/modules/samples/src/test/java/org/lwjgl/demo/system/jawt/EmbeddedFrameUtil.java
// which is licensed under https://www.lwjgl.org/license
/**
* Some utils for embedding awt and swing items into lwjgl windows
*
* @author Ran
* @author coolGi
*/
public final class EmbeddedFrameUtil
{
private static final int JAVA_VERSION;
private static final JAWT awt;
static
{
Pattern p = Pattern.compile("^(?:1[.])?([1-9][0-9]*)[.-]");
Matcher m = p.matcher(System.getProperty("java.version"));
if (!m.find())
{
throw new IllegalStateException("Failed to parse java.version");
}
JAVA_VERSION = Integer.parseInt(m.group(1));
awt = JAWT.calloc();
awt.version(JAVA_VERSION < 9 ? JAWT_VERSION_1_4 : JAWT_VERSION_9);
if (!JAWT_GetAWT(awt))
{
throw new RuntimeException("GetAWT failed");
}
}
private static String getEmbeddedFrameImpl()
{
switch (EPlatform.get())
{
case LINUX:
return "sun.awt.X11.XEmbeddedFrame";
case WINDOWS:
return "sun.awt.windows.WEmbeddedFrame";
case MACOS:
return "sun.lwawt.macosx.CViewEmbeddedFrame";
default:
throw new IllegalStateException();
}
}
private static long getEmbeddedFrameHandle(long window)
{
switch (EPlatform.get())
{
case LINUX:
return glfwGetX11Window(window);
case WINDOWS:
return glfwGetWin32Window(window);
case MACOS:
long objc_msgSend = ObjCRuntime.getLibrary().getFunctionAddress("objc_msgSend");
return invokePPP(glfwGetCocoaWindow(window), sel_getUid("contentView"), objc_msgSend);
default:
throw new IllegalStateException();
}
}
public static Frame embeddedFrameCreate(long window)
{
if (JAVA_VERSION < 9)
{
try
{
@SuppressWarnings("unchecked")
Class<? extends Frame> EmdeddedFrame = (Class<? extends Frame>) Class.forName(getEmbeddedFrameImpl());
Constructor<? extends Frame> c = EmdeddedFrame.getConstructor(long.class);
return c.newInstance(getEmbeddedFrameHandle(window));
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
else
{
return nJAWT_CreateEmbeddedFrame(getEmbeddedFrameHandle(window), awt.CreateEmbeddedFrame());
}
}
static void embeddedFrameSynthesizeWindowActivation(Frame embeddedFrame, boolean doActivate)
{
if (JAVA_VERSION < 9)
{
try
{
embeddedFrame
.getClass()
.getMethod("synthesizeWindowActivation", boolean.class)
.invoke(embeddedFrame, doActivate);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
else
{
JAWT_SynthesizeWindowActivation(embeddedFrame, doActivate, awt.SynthesizeWindowActivation());
}
}
public static void embeddedFrameSetBounds(Frame embeddedFrame, int x, int y, int width, int height)
{
if (JAVA_VERSION < 9)
{
try
{
Method setLocationPrivate = embeddedFrame
.getClass()
.getSuperclass()
.getDeclaredMethod("setBoundsPrivate", int.class, int.class, int.class, int.class);
setLocationPrivate.setAccessible(true);
setLocationPrivate.invoke(embeddedFrame, x, y, width, height);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
else
{
JAWT_SetBounds(embeddedFrame, x, y, width, height, awt.SetBounds());
}
}
public static void hideFrame(@NotNull Frame embeddedFrame)
{
embeddedFrame.setVisible(false);
embeddedFrameSynthesizeWindowActivation(embeddedFrame, false);
}
public static void showFrame(@NotNull Frame embeddedFrame)
{
embeddedFrameSynthesizeWindowActivation(embeddedFrame, true);
embeddedFrame.setVisible(true);
}
public static void placeAtCenter(Frame embeddedFrame, int windowWidth, int windowHeight, int frameWidth, int frameHeight, float scale)
{
float scaleFactor = (100.0F - scale) / 100.0F;
float newWidth = frameWidth * scaleFactor;
float newHeight = frameHeight * scaleFactor;
float newX = (windowWidth - newWidth) / 2F;
float newY = (windowHeight - newHeight) / 2F;
embeddedFrameSetBounds(embeddedFrame, Math.round(newX), Math.round(newY), Math.round(newWidth), Math.round(newHeight));
}
}
@@ -150,13 +150,6 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
/** @return true if the upload started, false if it wasn't able to for any reason */
public synchronized boolean uploadRenderDataToGpuAsync()
{
if (!GLProxy.hasInstance())
{
// it's possible to try uploading buffers before the GLProxy has been initialized
// which would cause the system to crash
return false;
}
if (this.getAndBuildRenderDataFutureRef.get() != null)
{
// don't accidentally queue multiple uploads at the same time
@@ -0,0 +1,144 @@
package com.seibel.distanthorizons.core.render;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
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.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper;
import java.util.Timer;
import java.util.concurrent.ConcurrentLinkedQueue;
public class RenderThreadTaskHandler
{
public static final DhLogger LOGGER = new DhLoggerBuilder()
.fileLevelConfig(Config.Common.Logging.logRendererGLEventToFile)
.chatLevelConfig(Config.Common.Logging.logRendererGLEventToChat)
.build();
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;
public static final RenderThreadTaskHandler INSTANCE = new RenderThreadTaskHandler();
private long msSinceGlTasksRun = System.currentTimeMillis();
//=============//
// constructor //
//=============//
//region
private RenderThreadTaskHandler() { TIMER.scheduleAtFixedRate(TimerUtil.createTimerTask(this::manualCleanupTick), MS_BETWEEN_CLEANUP_TICKS, MS_BETWEEN_CLEANUP_TICKS); }
//endregion
//==============//
// task queuing //
//==============//
//region
public void queueRunningOnRenderThread(Runnable renderCall)
{
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
RENDER_THREAD_RUNNABLE_QUEUE.add(() -> this.createRenderThreadRunnable(renderCall, stackTrace));
}
private void createRenderThreadRunnable(Runnable renderCall, StackTraceElement[] stackTrace)
{
try
{
renderCall.run();
}
catch (Exception e)
{
RuntimeException error = new RuntimeException("Uncaught Exception during GL call execution:", e);
error.setStackTrace(stackTrace);
LOGGER.error("[" + Thread.currentThread().getName() + "] ran into an unexpected error running a GL call, Error: ["+ e.getMessage() +"].", error);
}
}
//endregion
//===========//
// run tasks //
//===========//
//region
/**
* Doesn't do any thread/GL Context validation.
* Running this outside of the render thread may cause crashes or other issues.
*/
public void runRenderThreadTasks()
{
IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class);
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)
{
runnable.run();
// only try running for 4ms (240 FPS) at a time to prevent random lag spikes
long currentTimeMs = System.currentTimeMillis();
long runDuration = currentTimeMs - startTimeMs;
if (runDuration > msMaxRunTime)
{
break;
}
runnable = RENDER_THREAD_RUNNABLE_QUEUE.poll();
}
}
/**
* 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 manualCleanupTick()
{
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).
IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
MC.executeOnRenderThread(() -> this.runRenderThreadTasks(1_000));
}
//end region
}