From d15d1a75afff565fd474df9c8b5e2241272a192d Mon Sep 17 00:00:00 2001 From: Halbear Date: Thu, 22 Jan 2026 11:08:52 +0000 Subject: [PATCH] Engine architecture + Multithreading --- .../Terrain4J/EngineCore/Display/Window.java | 3 +- .../EngineCore/Logic/EngineConfig.java | 22 +++-- .../EngineCore/Main/PrimaryRuntime.java | 63 +++++++------ .../EngineCore/Threads/FastTickThread.java | 4 + .../EngineCore/Threads/MainThread.java | 89 +++++++++++++++++++ .../EngineCore/Threads/RenderThread.java | 65 ++++++++++++++ .../resources/DefaultEngineConfig.properties | 12 +-- terrain4j.properties | 7 +- 8 files changed, 223 insertions(+), 42 deletions(-) create mode 100644 src/main/java/net/halbear/Terrain4J/EngineCore/Threads/FastTickThread.java create mode 100644 src/main/java/net/halbear/Terrain4J/EngineCore/Threads/MainThread.java create mode 100644 src/main/java/net/halbear/Terrain4J/EngineCore/Threads/RenderThread.java diff --git a/src/main/java/net/halbear/Terrain4J/EngineCore/Display/Window.java b/src/main/java/net/halbear/Terrain4J/EngineCore/Display/Window.java index 95e6ed0..3522d51 100644 --- a/src/main/java/net/halbear/Terrain4J/EngineCore/Display/Window.java +++ b/src/main/java/net/halbear/Terrain4J/EngineCore/Display/Window.java @@ -57,6 +57,7 @@ public class Window { glfwFreeCallbacks(handle); glfwDestroyWindow(handle); glfwTerminate(); + Logger.debug("Destroyed Window"); } public long getHandle(){ return handle; @@ -78,7 +79,7 @@ public class Window { EngineConfig.RENDERFRAMES++; } public void UpdateWindowTitle(){ - if (displayFPS) glfwSetWindowTitle(handle, EngineConfig.SOFTWARE_TITLE + " - FPS: " + EngineConfig.FrameRate_PRIMARYTHREAD + " TPS: " + EngineConfig.TPS + " RenderFPS: " + EngineConfig.FrameRate_RENDER + " VSYNC: " + EngineConfig.VSYNC); + if (displayFPS) glfwSetWindowTitle(handle, EngineConfig.SOFTWARE_TITLE + " - FPS: " + EngineConfig.FrameRate_PRIMARYTHREAD + " TPS: " + EngineConfig.TPS + " RenderFPS: " + EngineConfig.FrameRate_RENDERTHREAD + " VSYNC: " + EngineConfig.VSYNC); } public void SetVsync(){ Logger.info("Vsync Swap"); diff --git a/src/main/java/net/halbear/Terrain4J/EngineCore/Logic/EngineConfig.java b/src/main/java/net/halbear/Terrain4J/EngineCore/Logic/EngineConfig.java index 9d99c18..73a3122 100644 --- a/src/main/java/net/halbear/Terrain4J/EngineCore/Logic/EngineConfig.java +++ b/src/main/java/net/halbear/Terrain4J/EngineCore/Logic/EngineConfig.java @@ -1,7 +1,7 @@ package net.halbear.Terrain4J.EngineCore.Logic; +import net.halbear.Terrain4J.EngineCore.Main.PrimaryRuntime; import org.tinylog.Logger; -import org.tinylog.core.TinylogLoggingConfiguration; import java.io.*; import java.nio.file.Path; @@ -18,18 +18,21 @@ public class EngineConfig { private static EngineConfig instance; private long accuracy; private int TickRate; + private int MainTickRate; + private int FastTickRate; + private boolean CapMainToTickRate; private int Width; private int Height; public static int FrameRate_PRIMARYTHREAD =0; public static int TPS =0; public static int FrameRate_MAINENGINETHREAD =0; - public static int FrameRate_RENDER =0; + public static int FrameRate_RENDERTHREAD =0; + public static int FrameRate_WINDOW =0; public static int RENDERFRAMES =0; public static boolean VSYNC = false; private boolean vkValidated; private String PhysicalDeviceName; - private EngineConfig() { var EngineConfigVar = new Properties(); Path path = Paths.get(""); @@ -68,10 +71,13 @@ public class EngineConfig { } } if(SuccessfulLoad) { + CapMainToTickRate = Boolean.parseBoolean(EngineConfigVar.getOrDefault("cap_main_to_tickrate", "false").toString()); + MainTickRate = Integer.parseInt(EngineConfigVar.getOrDefault("main_thread_tickrate", "120").toString()); + FastTickRate = Integer.parseInt(EngineConfigVar.getOrDefault("fast_thread_tickrate", "240").toString()); SOFTWARE_TITLE = EngineConfigVar.getOrDefault("Software_Title", "Terrain4J").toString(); PhysicalDeviceName = EngineConfigVar.getOrDefault("PhysicalDeviceName", "").toString(); accuracy = Long.parseLong(EngineConfigVar.getOrDefault("frame_accuracy", DEFAULT_ACCURACY).toString()); - TickRate = Integer.parseInt(EngineConfigVar.getOrDefault("main_tick_rate", DEFAULT_TICKRATE).toString()); + TickRate = Integer.parseInt(EngineConfigVar.getOrDefault("master_tick_rate", DEFAULT_TICKRATE).toString()); Width = Integer.parseInt(EngineConfigVar.getOrDefault("display_width", "1280").toString()); Height = Integer.parseInt(EngineConfigVar.getOrDefault("display_height", "720").toString()); VSYNC = Boolean.parseBoolean(EngineConfigVar.getOrDefault("vsync", false).toString()); @@ -80,10 +86,13 @@ public class EngineConfig { } else if(!ConfigFile.exists()){ try { + EngineConfigVar.setProperty("cap_main_to_tickrate","false"); + EngineConfigVar.setProperty("main_thread_tickrate","120"); + EngineConfigVar.setProperty("fast_thread_tickrate","240"); EngineConfigVar.setProperty("Software_Title","Terrain4J Software"); EngineConfigVar.setProperty("PhysicalDeviceName",null); EngineConfigVar.setProperty("frame_accuracy", String.valueOf(DEFAULT_ACCURACY)); - EngineConfigVar.setProperty("main_tick_rate", String.valueOf(DEFAULT_TICKRATE)); + EngineConfigVar.setProperty("master_tick_rate", String.valueOf(DEFAULT_TICKRATE)); EngineConfigVar.setProperty("display_width","1280"); EngineConfigVar.setProperty("display_height","720"); EngineConfigVar.setProperty("vsync","true"); @@ -115,6 +124,9 @@ public class EngineConfig { public int getHeight(){ return Height; } + public boolean GetMainTickCap(){return CapMainToTickRate;} + public int GetMainTickrate(){return MainTickRate;} + public int GetFastTickrate(){return FastTickRate;} public long getAccuracy() { return accuracy; } diff --git a/src/main/java/net/halbear/Terrain4J/EngineCore/Main/PrimaryRuntime.java b/src/main/java/net/halbear/Terrain4J/EngineCore/Main/PrimaryRuntime.java index 7432bb2..5498c9e 100644 --- a/src/main/java/net/halbear/Terrain4J/EngineCore/Main/PrimaryRuntime.java +++ b/src/main/java/net/halbear/Terrain4J/EngineCore/Main/PrimaryRuntime.java @@ -1,36 +1,30 @@ package net.halbear.Terrain4J.EngineCore.Main; -import net.halbear.Terrain4J.EngineCore.Display.Render; import net.halbear.Terrain4J.EngineCore.Display.Window; import net.halbear.Terrain4J.EngineCore.Logic.EngineConfig; import net.halbear.Terrain4J.EngineCore.Logic.EngineInstance; import net.halbear.Terrain4J.EngineCore.Logic.GameLogic; +import net.halbear.Terrain4J.EngineCore.Threads.MainThread; +import net.halbear.Terrain4J.EngineCore.Threads.RenderThread; import org.tinylog.Logger; public class PrimaryRuntime { - private final EngineInstance engineInstance; - private final GameLogic gameLogic; - private final Render render; - private Window window; private int Frames = 0; private int TickFrames = 0; private double TickRate = 0; + private static MainThread PrimaryThread; + private static RenderThread DrawingThread; + private static boolean running = true; + private static Window window; + private static EngineInstance engineInstance; public PrimaryRuntime(String windowTitle, GameLogic appLogic) { - this.gameLogic = appLogic; window = new Window(windowTitle); engineInstance = new EngineInstance(window, new Scene(window)); - render = new Render(engineInstance); - gameLogic.Initialise(engineInstance); - } - private void cleanup() { - gameLogic.cleanup(); - render.cleanup(); - engineInstance.cleanup(); - } - public EngineInstance getEngineInstance() { - return engineInstance; + PrimaryThread = new MainThread(engineInstance, appLogic); + DrawingThread = new RenderThread(window, engineInstance); } + public void UpdateTickRate(){ TickRate = (double) 1000000000 / (double) EngineConfig.getInstance().getTickRate(); } @@ -40,41 +34,56 @@ public class PrimaryRuntime { double FrameDiff =0; double TickDiff =0; double SecondCounter =0; - double tickDiff = 0; + double cycleDiff = 0; + PrimaryThread.StartMainThread(); + DrawingThread.StartRenderThread(); TickRate = (double) 1000000000 / (double) EngineConfig.getInstance().getTickRate(); + long FrameAccuracy = EngineConfig.getInstance().getAccuracy(); + Logger.debug("Primary Thread Started"); while (!window.shouldClose()){ long now = System.nanoTime(); - tickDiff = now - InitialTime; - FrameDiff += tickDiff; - TickDiff += tickDiff; - SecondCounter += tickDiff; - if (FrameDiff >= EngineConfig.getInstance().getAccuracy()){ + cycleDiff = now - InitialTime; + FrameDiff += cycleDiff; + TickDiff += cycleDiff; + SecondCounter += cycleDiff; + if (FrameDiff >= FrameAccuracy){ window.pollEvents(); - gameLogic.Input(engineInstance, (now - InitialTime)); Frames++; if (TickDiff >= TickRate) { TickFrames++; long nsTimeDiff = now - updateTime; - gameLogic.Update(engineInstance, nsTimeDiff); updateTime = now; TickDiff = 0; } if (SecondCounter >= 1000000000){ SecondCounter = 0; + EngineConfig.FrameRate_WINDOW = EngineConfig.RENDERFRAMES; EngineConfig.FrameRate_PRIMARYTHREAD = Frames; - EngineConfig.FrameRate_RENDER = EngineConfig.RENDERFRAMES; EngineConfig.TPS = TickFrames; + Logger.info("Primary Thread FPS: " + Frames + " TPS: " + TickFrames + " Window Draw FPS: " + EngineConfig.RENDERFRAMES); Frames = 0; TickFrames = 0; EngineConfig.RENDERFRAMES = 0; - Logger.info("FPS: " + EngineConfig.FrameRate_PRIMARYTHREAD + " TPS: " + EngineConfig.TPS + " RenderFPS: " + EngineConfig.FrameRate_RENDER); } - render.render(engineInstance); FrameDiff = 0; } InitialTime = now; } + PrimaryThread.KillThread(); + DrawingThread.KillThread(); cleanup(); } + + public static EngineInstance GetEngineInstance(){ + return engineInstance; + } + public static void CloseRuntime(){ + running = false; + } + private void cleanup() { + engineInstance.cleanup(); + window.cleanup(); + Logger.debug("Closing Primary Thread"); + } } diff --git a/src/main/java/net/halbear/Terrain4J/EngineCore/Threads/FastTickThread.java b/src/main/java/net/halbear/Terrain4J/EngineCore/Threads/FastTickThread.java new file mode 100644 index 0000000..a181f1a --- /dev/null +++ b/src/main/java/net/halbear/Terrain4J/EngineCore/Threads/FastTickThread.java @@ -0,0 +1,4 @@ +package net.halbear.Terrain4J.EngineCore.Threads; + +public class FastTickThread { +} diff --git a/src/main/java/net/halbear/Terrain4J/EngineCore/Threads/MainThread.java b/src/main/java/net/halbear/Terrain4J/EngineCore/Threads/MainThread.java new file mode 100644 index 0000000..f84c525 --- /dev/null +++ b/src/main/java/net/halbear/Terrain4J/EngineCore/Threads/MainThread.java @@ -0,0 +1,89 @@ +package net.halbear.Terrain4J.EngineCore.Threads; + +import net.halbear.Terrain4J.EngineCore.Display.Window; +import net.halbear.Terrain4J.EngineCore.Logic.EngineConfig; +import net.halbear.Terrain4J.EngineCore.Logic.EngineInstance; +import net.halbear.Terrain4J.EngineCore.Logic.GameLogic; +import net.halbear.Terrain4J.EngineCore.Main.PrimaryRuntime; +import net.halbear.Terrain4J.EngineCore.Main.Scene; +import org.tinylog.Logger; + +public class MainThread implements Runnable { + private static boolean CappedToTickRate; + private static int TickRate; + private static double TickThreshold; + private static boolean running = true; + private final GameLogic gameLogic; + private int Frames = 0; + private int TickFrames = 0; + private final Thread mainThread; + + public void KillThread(){ + running = false; + } + public void cleanup(){ + gameLogic.cleanup(); + Logger.debug("Closing Main Thread"); + } + public Thread FetchMainThread(){ + return mainThread; + } + public void StartMainThread(){ + mainThread.start(); + } + public MainThread(EngineInstance engineInstance, GameLogic appLogic){ + this.gameLogic = appLogic; + gameLogic.Initialise(engineInstance); + Logger.debug("Starting Main Thread"); + UpdateTickRateThreshold(); + mainThread = new Thread(this); + } + public void UpdateTickRateThreshold(){ + CappedToTickRate = EngineConfig.getInstance().GetMainTickCap(); + TickRate = EngineConfig.getInstance().GetMainTickrate(); + if (CappedToTickRate){ + TickThreshold = (double) 1000000000 / (double) TickRate; + } else{ + TickThreshold = 0; + } + } + @Override + public void run() { + long InitialTime = System.nanoTime(); + long updateTime = InitialTime; + double FrameDiff =0; + double TickDiff =0; + double SecondCounter =0; + double cycleDiff = 0; + Logger.debug("Main Thread Started"); + long EngineAccuracy = EngineConfig.getInstance().getAccuracy(); + while (running){ + long now = System.nanoTime(); + cycleDiff = now - InitialTime; + FrameDiff += cycleDiff; + TickDiff += cycleDiff; + SecondCounter += cycleDiff; + if (FrameDiff >= EngineAccuracy){ + gameLogic.Input(PrimaryRuntime.GetEngineInstance(), (now - InitialTime)); + Frames++; + if (TickDiff >= TickThreshold) { + TickFrames++; + long nsTimeDiff = now - updateTime; + gameLogic.Update(PrimaryRuntime.GetEngineInstance(), nsTimeDiff); + updateTime = now; + TickDiff = 0; + } + if (SecondCounter >= 1000000000){ + SecondCounter = 0; + EngineConfig.FrameRate_MAINENGINETHREAD = Frames; + Logger.info("Main Thread FPS: " + EngineConfig.FrameRate_MAINENGINETHREAD + " TPS: " + TickFrames); + Frames = 0; + TickFrames = 0; + } + FrameDiff = 0; + } + InitialTime = now; + } + cleanup(); + } +} diff --git a/src/main/java/net/halbear/Terrain4J/EngineCore/Threads/RenderThread.java b/src/main/java/net/halbear/Terrain4J/EngineCore/Threads/RenderThread.java new file mode 100644 index 0000000..b6185b4 --- /dev/null +++ b/src/main/java/net/halbear/Terrain4J/EngineCore/Threads/RenderThread.java @@ -0,0 +1,65 @@ +package net.halbear.Terrain4J.EngineCore.Threads; + +import net.halbear.Terrain4J.EngineCore.Display.Render; +import net.halbear.Terrain4J.EngineCore.Display.Window; +import net.halbear.Terrain4J.EngineCore.Logic.EngineConfig; +import net.halbear.Terrain4J.EngineCore.Logic.EngineInstance; +import net.halbear.Terrain4J.EngineCore.Main.PrimaryRuntime; +import org.tinylog.Logger; + +public class RenderThread implements Runnable { + private static boolean running = true; + private int Frames = 0; + private final Thread renderThread; + private final Render render; + private Window window; + + public RenderThread(Window window,EngineInstance engineInstance) { + //this.window = window; + render = new Render(engineInstance); + renderThread = new Thread(this); + } + + public void KillThread(){ + running = false; + } + public Thread FetchRenderThread(){ + return renderThread; + } + public void StartRenderThread(){ + renderThread.start(); + } + private void cleanup() { + render.cleanup(); + Logger.debug("Closing Render Thread"); + } + @Override + public void run() { + long InitialTime = System.nanoTime(); + double FrameDiff =0; + double SecondCounter =0; + double cycleDiff = 0; + long FrameAccuracy = EngineConfig.getInstance().getAccuracy(); + Logger.debug("Render Thread Started"); + while (running){ + long now = System.nanoTime(); + cycleDiff = now - InitialTime; + FrameDiff += cycleDiff; + SecondCounter += cycleDiff; + if (FrameDiff >= FrameAccuracy){ + Frames++; + if (SecondCounter >= 1000000000){ + SecondCounter = 0; + EngineConfig.FrameRate_RENDERTHREAD = Frames; + Logger.info("Render FPS: " + Frames); + Frames = 0; + } + render.render(PrimaryRuntime.GetEngineInstance()); + FrameDiff = 0; + } + InitialTime = now; + } + //PrimaryRuntime.CloseRuntime(); + cleanup(); + } +} diff --git a/src/main/resources/DefaultEngineConfig.properties b/src/main/resources/DefaultEngineConfig.properties index 2aada15..c3dae9c 100644 --- a/src/main/resources/DefaultEngineConfig.properties +++ b/src/main/resources/DefaultEngineConfig.properties @@ -1,18 +1,18 @@ -#created new properties file -#Tue Jan 20 08:16:32 GMT 2026 #Default Window dimensions display_height=480 display_width=640 #the precision of frames, set this to 16000 and it will limit the whole engine to 64FPS frame_accuracy=0 #event tick rate for game process -main_tick_rate=120 +master_tick_rate=120 +main_thread_tickrate=120 +fast_thread_tickrate=240 +cap_main_to_tickrate=false #Vulkan Debugging toggle vkValidated=true -vsync=false +vsync=true #This is the name of the GPU you want the engine to use PhysicalDeviceName= #Title that shows up on app window -Software_Title=Terrain4J Game Engine - +Software_Title=Terrain4J Game Engine \ No newline at end of file diff --git a/terrain4j.properties b/terrain4j.properties index 7167f8b..2f62165 100644 --- a/terrain4j.properties +++ b/terrain4j.properties @@ -1,12 +1,13 @@ -#created new properties file -#Tue Jan 20 08:16:32 GMT 2026 #Default Window dimensions display_height=480 display_width=640 #the precision of frames, set this to 16000 and it will limit the whole engine to 64FPS frame_accuracy=0 #event tick rate for game process -main_tick_rate=120 +master_tick_rate=120 +main_thread_tickrate=120 +fast_thread_tickrate=240 +cap_main_to_tickrate=true #Vulkan Debugging toggle vkValidated=true