Engine architecture + Multithreading

This commit is contained in:
Harrison Corlett 2026-01-22 11:08:52 +00:00
parent 44845f781d
commit d15d1a75af
8 changed files with 223 additions and 42 deletions

View file

@ -57,6 +57,7 @@ public class Window {
glfwFreeCallbacks(handle); glfwFreeCallbacks(handle);
glfwDestroyWindow(handle); glfwDestroyWindow(handle);
glfwTerminate(); glfwTerminate();
Logger.debug("Destroyed Window");
} }
public long getHandle(){ public long getHandle(){
return handle; return handle;
@ -78,7 +79,7 @@ public class Window {
EngineConfig.RENDERFRAMES++; EngineConfig.RENDERFRAMES++;
} }
public void UpdateWindowTitle(){ 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(){ public void SetVsync(){
Logger.info("Vsync Swap"); Logger.info("Vsync Swap");

View file

@ -1,7 +1,7 @@
package net.halbear.Terrain4J.EngineCore.Logic; package net.halbear.Terrain4J.EngineCore.Logic;
import net.halbear.Terrain4J.EngineCore.Main.PrimaryRuntime;
import org.tinylog.Logger; import org.tinylog.Logger;
import org.tinylog.core.TinylogLoggingConfiguration;
import java.io.*; import java.io.*;
import java.nio.file.Path; import java.nio.file.Path;
@ -18,18 +18,21 @@ public class EngineConfig {
private static EngineConfig instance; private static EngineConfig instance;
private long accuracy; private long accuracy;
private int TickRate; private int TickRate;
private int MainTickRate;
private int FastTickRate;
private boolean CapMainToTickRate;
private int Width; private int Width;
private int Height; private int Height;
public static int FrameRate_PRIMARYTHREAD =0; public static int FrameRate_PRIMARYTHREAD =0;
public static int TPS =0; public static int TPS =0;
public static int FrameRate_MAINENGINETHREAD =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 int RENDERFRAMES =0;
public static boolean VSYNC = false; public static boolean VSYNC = false;
private boolean vkValidated; private boolean vkValidated;
private String PhysicalDeviceName; private String PhysicalDeviceName;
private EngineConfig() { private EngineConfig() {
var EngineConfigVar = new Properties(); var EngineConfigVar = new Properties();
Path path = Paths.get(""); Path path = Paths.get("");
@ -68,10 +71,13 @@ public class EngineConfig {
} }
} }
if(SuccessfulLoad) { 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(); SOFTWARE_TITLE = EngineConfigVar.getOrDefault("Software_Title", "Terrain4J").toString();
PhysicalDeviceName = EngineConfigVar.getOrDefault("PhysicalDeviceName", "").toString(); PhysicalDeviceName = EngineConfigVar.getOrDefault("PhysicalDeviceName", "").toString();
accuracy = Long.parseLong(EngineConfigVar.getOrDefault("frame_accuracy", DEFAULT_ACCURACY).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()); Width = Integer.parseInt(EngineConfigVar.getOrDefault("display_width", "1280").toString());
Height = Integer.parseInt(EngineConfigVar.getOrDefault("display_height", "720").toString()); Height = Integer.parseInt(EngineConfigVar.getOrDefault("display_height", "720").toString());
VSYNC = Boolean.parseBoolean(EngineConfigVar.getOrDefault("vsync", false).toString()); VSYNC = Boolean.parseBoolean(EngineConfigVar.getOrDefault("vsync", false).toString());
@ -80,10 +86,13 @@ public class EngineConfig {
} }
else if(!ConfigFile.exists()){ else if(!ConfigFile.exists()){
try { 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("Software_Title","Terrain4J Software");
EngineConfigVar.setProperty("PhysicalDeviceName",null); EngineConfigVar.setProperty("PhysicalDeviceName",null);
EngineConfigVar.setProperty("frame_accuracy", String.valueOf(DEFAULT_ACCURACY)); 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_width","1280");
EngineConfigVar.setProperty("display_height","720"); EngineConfigVar.setProperty("display_height","720");
EngineConfigVar.setProperty("vsync","true"); EngineConfigVar.setProperty("vsync","true");
@ -115,6 +124,9 @@ public class EngineConfig {
public int getHeight(){ public int getHeight(){
return Height; return Height;
} }
public boolean GetMainTickCap(){return CapMainToTickRate;}
public int GetMainTickrate(){return MainTickRate;}
public int GetFastTickrate(){return FastTickRate;}
public long getAccuracy() { public long getAccuracy() {
return accuracy; return accuracy;
} }

View file

@ -1,36 +1,30 @@
package net.halbear.Terrain4J.EngineCore.Main; 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.Display.Window;
import net.halbear.Terrain4J.EngineCore.Logic.EngineConfig; import net.halbear.Terrain4J.EngineCore.Logic.EngineConfig;
import net.halbear.Terrain4J.EngineCore.Logic.EngineInstance; import net.halbear.Terrain4J.EngineCore.Logic.EngineInstance;
import net.halbear.Terrain4J.EngineCore.Logic.GameLogic; 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; import org.tinylog.Logger;
public class PrimaryRuntime { public class PrimaryRuntime {
private final EngineInstance engineInstance;
private final GameLogic gameLogic;
private final Render render;
private Window window;
private int Frames = 0; private int Frames = 0;
private int TickFrames = 0; private int TickFrames = 0;
private double TickRate = 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) { public PrimaryRuntime(String windowTitle, GameLogic appLogic) {
this.gameLogic = appLogic;
window = new Window(windowTitle); window = new Window(windowTitle);
engineInstance = new EngineInstance(window, new Scene(window)); engineInstance = new EngineInstance(window, new Scene(window));
render = new Render(engineInstance); PrimaryThread = new MainThread(engineInstance, appLogic);
gameLogic.Initialise(engineInstance); DrawingThread = new RenderThread(window, engineInstance);
}
private void cleanup() {
gameLogic.cleanup();
render.cleanup();
engineInstance.cleanup();
}
public EngineInstance getEngineInstance() {
return engineInstance;
} }
public void UpdateTickRate(){ public void UpdateTickRate(){
TickRate = (double) 1000000000 / (double) EngineConfig.getInstance().getTickRate(); TickRate = (double) 1000000000 / (double) EngineConfig.getInstance().getTickRate();
} }
@ -40,41 +34,56 @@ public class PrimaryRuntime {
double FrameDiff =0; double FrameDiff =0;
double TickDiff =0; double TickDiff =0;
double SecondCounter =0; double SecondCounter =0;
double tickDiff = 0; double cycleDiff = 0;
PrimaryThread.StartMainThread();
DrawingThread.StartRenderThread();
TickRate = (double) 1000000000 / (double) EngineConfig.getInstance().getTickRate(); TickRate = (double) 1000000000 / (double) EngineConfig.getInstance().getTickRate();
long FrameAccuracy = EngineConfig.getInstance().getAccuracy();
Logger.debug("Primary Thread Started");
while (!window.shouldClose()){ while (!window.shouldClose()){
long now = System.nanoTime(); long now = System.nanoTime();
tickDiff = now - InitialTime; cycleDiff = now - InitialTime;
FrameDiff += tickDiff; FrameDiff += cycleDiff;
TickDiff += tickDiff; TickDiff += cycleDiff;
SecondCounter += tickDiff; SecondCounter += cycleDiff;
if (FrameDiff >= EngineConfig.getInstance().getAccuracy()){ if (FrameDiff >= FrameAccuracy){
window.pollEvents(); window.pollEvents();
gameLogic.Input(engineInstance, (now - InitialTime));
Frames++; Frames++;
if (TickDiff >= TickRate) { if (TickDiff >= TickRate) {
TickFrames++; TickFrames++;
long nsTimeDiff = now - updateTime; long nsTimeDiff = now - updateTime;
gameLogic.Update(engineInstance, nsTimeDiff);
updateTime = now; updateTime = now;
TickDiff = 0; TickDiff = 0;
} }
if (SecondCounter >= 1000000000){ if (SecondCounter >= 1000000000){
SecondCounter = 0; SecondCounter = 0;
EngineConfig.FrameRate_WINDOW = EngineConfig.RENDERFRAMES;
EngineConfig.FrameRate_PRIMARYTHREAD = Frames; EngineConfig.FrameRate_PRIMARYTHREAD = Frames;
EngineConfig.FrameRate_RENDER = EngineConfig.RENDERFRAMES;
EngineConfig.TPS = TickFrames; EngineConfig.TPS = TickFrames;
Logger.info("Primary Thread FPS: " + Frames + " TPS: " + TickFrames + " Window Draw FPS: " + EngineConfig.RENDERFRAMES);
Frames = 0; Frames = 0;
TickFrames = 0; TickFrames = 0;
EngineConfig.RENDERFRAMES = 0; EngineConfig.RENDERFRAMES = 0;
Logger.info("FPS: " + EngineConfig.FrameRate_PRIMARYTHREAD + " TPS: " + EngineConfig.TPS + " RenderFPS: " + EngineConfig.FrameRate_RENDER);
} }
render.render(engineInstance);
FrameDiff = 0; FrameDiff = 0;
} }
InitialTime = now; InitialTime = now;
} }
PrimaryThread.KillThread();
DrawingThread.KillThread();
cleanup(); 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");
}
} }

View file

@ -0,0 +1,4 @@
package net.halbear.Terrain4J.EngineCore.Threads;
public class FastTickThread {
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -1,18 +1,18 @@
#created new properties file
#Tue Jan 20 08:16:32 GMT 2026
#Default Window dimensions #Default Window dimensions
display_height=480 display_height=480
display_width=640 display_width=640
#the precision of frames, set this to 16000 and it will limit the whole engine to 64FPS #the precision of frames, set this to 16000 and it will limit the whole engine to 64FPS
frame_accuracy=0 frame_accuracy=0
#event tick rate for game process #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 #Vulkan Debugging toggle
vkValidated=true vkValidated=true
vsync=false vsync=true
#This is the name of the GPU you want the engine to use #This is the name of the GPU you want the engine to use
PhysicalDeviceName= PhysicalDeviceName=
#Title that shows up on app window #Title that shows up on app window
Software_Title=Terrain4J Game Engine Software_Title=Terrain4J Game Engine

View file

@ -1,12 +1,13 @@
#created new properties file
#Tue Jan 20 08:16:32 GMT 2026
#Default Window dimensions #Default Window dimensions
display_height=480 display_height=480
display_width=640 display_width=640
#the precision of frames, set this to 16000 and it will limit the whole engine to 64FPS #the precision of frames, set this to 16000 and it will limit the whole engine to 64FPS
frame_accuracy=0 frame_accuracy=0
#event tick rate for game process #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 #Vulkan Debugging toggle
vkValidated=true vkValidated=true