From 82c832a4af4fdc88454d6924c2e1a1dc33cf33a3 Mon Sep 17 00:00:00 2001 From: James Seibel Date: Mon, 9 Mar 2026 16:30:45 -0500 Subject: [PATCH] Add RenderThreadTaskHandler --- .../core/api/internal/ClientApi.java | 8 +- .../api/internal/ClientPluginChannelApi.java | 4 +- .../core/config/gui/EmbeddedFrameUtil.java | 196 ------------------ .../render/QuadTree/LodRenderSection.java | 7 - .../core/render/RenderThreadTaskHandler.java | 144 +++++++++++++ 5 files changed, 149 insertions(+), 210 deletions(-) delete mode 100644 core/src/main/java/com/seibel/distanthorizons/core/config/gui/EmbeddedFrameUtil.java create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/render/RenderThreadTaskHandler.java 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 2a9eb17ad..132598e0a 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 @@ -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) { diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientPluginChannelApi.java b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientPluginChannelApi.java index afb1f05e8..90a3a356f 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientPluginChannelApi.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientPluginChannelApi.java @@ -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(); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/config/gui/EmbeddedFrameUtil.java b/core/src/main/java/com/seibel/distanthorizons/core/config/gui/EmbeddedFrameUtil.java deleted file mode 100644 index 49bf35b04..000000000 --- a/core/src/main/java/com/seibel/distanthorizons/core/config/gui/EmbeddedFrameUtil.java +++ /dev/null @@ -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 . - */ - -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 EmdeddedFrame = (Class) Class.forName(getEmbeddedFrameImpl()); - Constructor 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)); - } - -} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/QuadTree/LodRenderSection.java b/core/src/main/java/com/seibel/distanthorizons/core/render/QuadTree/LodRenderSection.java index 0c88cb5bf..39f3c217e 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/QuadTree/LodRenderSection.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/QuadTree/LodRenderSection.java @@ -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 diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/RenderThreadTaskHandler.java b/core/src/main/java/com/seibel/distanthorizons/core/render/RenderThreadTaskHandler.java new file mode 100644 index 000000000..dc068a552 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/RenderThreadTaskHandler.java @@ -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 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 + + + +}