Add render loop

This commit is contained in:
JeremyStar™ 2024-07-25 19:46:50 +02:00
parent 0b4502c3ce
commit bbde2e9d2d
Signed by: JeremyStarTM
GPG key ID: E366BAEF67E4704D
7 changed files with 241 additions and 73 deletions

View file

@ -0,0 +1,117 @@
/*
* STAROPENSOURCE ENGINE SOURCE FILE
* Copyright (c) 2024 The StarOpenSource Engine Contributors
* Licensed under the GNU Affero General Public License v3
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.staropensource.sosengine.graphics.glfw.classes;
import de.staropensource.sosengine.base.logging.LoggerInstance;
import de.staropensource.sosengine.base.types.CodePart;
import de.staropensource.sosengine.base.types.logging.LogIssuer;
import de.staropensource.sosengine.base.utility.Miscellaneous;
import de.staropensource.sosengine.graphics.classes.ApiManagementClass;
import de.staropensource.sosengine.graphics.classes.Window;
import de.staropensource.sosengine.graphics.glfw.exceptions.NotOnMainThreadException;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.OptionalDouble;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static org.lwjgl.glfw.GLFW.*;
@Getter
public abstract class GlfwManagementClass implements ApiManagementClass {
/**
* Contains the {@link LoggerInstance} for this class.
*
* @since v1-alpha2
*/
protected LoggerInstance logger = new LoggerInstance(new LogIssuer(getClass(), CodePart.ENGINE));
/** {@inheritDoc} */
@Override
public boolean mustRunOnMainThread() {
return true;
}
/** {@inheritDoc} */
@NotNull
@Override
public LinkedHashMap<@NotNull Window, @NotNull Throwable> runRenderLoop() {
// Ensure running on the main thread
if (!Miscellaneous.onMainThread())
throw new NotOnMainThreadException();
LinkedHashMap<@NotNull Window, @NotNull Throwable> throwables = new LinkedHashMap<>();
for (Window window : Window.getWindows()) {
try {
window.updateState();
window.render();
} catch (Throwable throwable) {
logger.error("Rendering window " + window + " failed: Threw throwable " + throwable.getClass().getName() + (throwable.getMessage() == null ? "" : ": " + throwable.getMessage()));
throwables.put(window, throwable);
}
}
glfwPollEvents();
return throwables;
}
/** {@inheritDoc} */
@NotNull
@Override
public LinkedHashMap<@NotNull Window, @NotNull Throwable> runRenderLoopContinuously(@NotNull Runnable frameCode) {
// Ensure running on the main thread
if (!Miscellaneous.onMainThread())
throw new NotOnMainThreadException();
long renderTime = 0L;
LinkedList<Long> renderTimes = new LinkedList<>();
AtomicReference<LinkedHashMap<@NotNull Window, @NotNull Throwable>> output = new AtomicReference<>(new LinkedHashMap<>());
// Run while the output linked map of runRenderLoop is empty
while (output.get().isEmpty()) {
while (renderTimes.size() >= 10)
renderTimes.removeLast();
renderTimes.add(Miscellaneous.measureExecutionTime(() -> {
output.set(runRenderLoop());
frameCode.run();
}));
renderTime += renderTimes.getLast();
// Report
if (renderTime >= 1000) {
OptionalDouble average = renderTimes.stream().mapToDouble(a -> a).average();
if (average.isPresent())
logger.info("Delta average: " + average.getAsDouble() + " | Frames/s: " + 1000 / average.getAsDouble());
else
logger.error("Unable to get delta average and FPS");
renderTimes.clear();
renderTime = 0L;
}
}
return output.get();
}
}

View file

@ -23,7 +23,6 @@ import de.staropensource.sosengine.base.exceptions.UnexpectedThrowableException;
import de.staropensource.sosengine.base.types.Tristate;
import de.staropensource.sosengine.base.types.vectors.Vec2i;
import de.staropensource.sosengine.base.utility.Miscellaneous;
import de.staropensource.sosengine.graphics.GraphicsSubsystemConfiguration;
import de.staropensource.sosengine.graphics.classes.Window;
import de.staropensource.sosengine.graphics.events.GraphicsApiErrorEvent;
import de.staropensource.sosengine.graphics.events.InputEvent;
@ -144,7 +143,6 @@ public abstract class GlfwWindow extends Window {
glfwWindowHint(GLFW_CENTER_CURSOR, 0);
glfwWindowHint(GLFW_FOCUSED, Miscellaneous.getIntegerizedBoolean(focused));
glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, Miscellaneous.getIntegerizedBoolean(isTransparent()));
glfwWindowHint(GLFW_COCOA_GRAPHICS_SWITCHING, Miscellaneous.getIntegerizedBoolean(GraphicsSubsystemConfiguration.getInstance().isDisallowIntegratedGraphics()));
glfwWindowHintString(GLFW_WAYLAND_APP_ID, getName());
glfwWindowHintString(GLFW_X11_CLASS_NAME, getName());
glfwWindowHintString(GLFW_X11_INSTANCE_NAME, getName());
@ -163,8 +161,8 @@ public abstract class GlfwWindow extends Window {
// Own context
ownContext();
// Set swap interval based on isDisallowTearing setting
glfwSwapInterval(Miscellaneous.getIntegerizedBoolean(GraphicsSubsystemConfiguration.getInstance().isDisallowTearing()));
// Set swap interval based on V-Sync mode setting
glfwSwapInterval(getVsyncMode() == VsyncMode.ON ? 1 : 0);
// Create callbacks
keyCallback = GLFWKeyCallback.create(new KeyCallback(this));
@ -228,12 +226,6 @@ public abstract class GlfwWindow extends Window {
if (!Miscellaneous.onMainThread())
throw new NotOnMainThreadException();
// Own context
ownContext();
// Set swap interval based on isDisallowTearing setting
glfwSwapInterval(Miscellaneous.getIntegerizedBoolean(GraphicsSubsystemConfiguration.getInstance().isDisallowTearing()));
try (MemoryStack stack = MemoryStack.stackPush()) {
IntBuffer width = stack.mallocInt(2);
IntBuffer height = stack.mallocInt(2);
@ -279,13 +271,15 @@ public abstract class GlfwWindow extends Window {
// Ensure the window is not terminated
if (isTerminated())
return;
// Ensure rendering is enabled
if (!isRendering())
return;
// Ensure running on the main thread
if (!Miscellaneous.onMainThread())
throw new NotOnMainThreadException();
glfwSwapBuffers(identifierLong);
glfwPollEvents();
}
// ------------------------------------------------ [ GLFW handling ] ------------------------------------------------ //

View file

@ -19,7 +19,7 @@
package de.staropensource.sosengine.graphics.opengl;
import de.staropensource.sosengine.graphics.classes.ApiManagementClass;
import de.staropensource.sosengine.graphics.glfw.classes.GlfwManagementClass;
/**
* The OpenGL API management class.
@ -27,15 +27,11 @@ import de.staropensource.sosengine.graphics.classes.ApiManagementClass;
* @since v1-alpha0
*/
@SuppressWarnings({ "unused" })
public final class OpenGlManagement implements ApiManagementClass {
public final class OpenGlManagement extends GlfwManagementClass {
/**
* Constructs this class.
*
* @since v1-alpha2
*/
public OpenGlManagement() {}
/** {@inheritDoc} */
@Override
public boolean mustRunOnMainThread() {
return true;
}
}

View file

@ -97,6 +97,21 @@ public final class GraphicsSubsystemConfiguration implements SubsystemConfigurat
*/
private boolean debugInput;
/**
* If enabled, will log the delta time average and FPS count
* to the console every second.
*
* @since v1-alpha2
*
* -- GETTER --
* Gets the value for {@link #debugFrames}.
*
* @return variable value
* @see #debugFrames
* @since v1-alpha2
*/
private boolean debugFrames;
/**
* If enabled, graphical errors thrown by GLFW will be printed to the log by the subsystem.
*
@ -113,34 +128,21 @@ public final class GraphicsSubsystemConfiguration implements SubsystemConfigurat
private boolean errorGraphicsError;
/**
* If enabled, will make the Graphics API try to prevent tearing from happening.
*
* @since v1-alpha2
*
* -- GETTER --
* Gets the value for {@link #disallowTearing}.
*
* @return variable value
* @see GraphicsSubsystemConfiguration#disallowTearing
* @since v1-alpha2
*/
private boolean disallowTearing;
/**
* If enabled, will make the Graphics API try to prevent using the integrated graphics card in your computer.
* Determines how many frames can be rendered max per second.
* <p>
* Will have no effect if no integrated or discrete graphics card is installed in the system.
* This value will have no effect on windows with V-Sync enabled.
* Set to {@code 0} for no limit.
*
* @since v1-alpha2
*
* -- GETTER --
* Gets the value for {@link #disallowIntegratedGraphics}.
* Gets the value for {@link #maximumFramesPerSecond}
*
* @return variable value
* @see GraphicsSubsystemConfiguration#disallowIntegratedGraphics
* @see #maximumFramesPerSecond
* @since v1-alpha2
*/
private boolean disallowIntegratedGraphics;
private int maximumFramesPerSecond;
/**
* Constructs this class.
@ -175,17 +177,20 @@ public final class GraphicsSubsystemConfiguration implements SubsystemConfigurat
switch (property) {
case "debug" -> debug = parser.getBoolean(group + property);
case "debugInput" -> debugInput = parser.getBoolean(group + property);
case "debugFrames" -> debugFrames = parser.getBoolean(group + property);
case "errorGraphicsError" -> errorGraphicsError = parser.getBoolean(group + property);
case "disallowTearing" -> disallowTearing = parser.getBoolean(group + property);
case "maximumFramesPerSecond" -> maximumFramesPerSecond = parser.getInteger(group + property, true);
}
} catch (NullPointerException ignored) {}
}
// Disable all debug options if 'debug' is disabled
if (!debug)
if (!debug) {
debugInput = false;
debugFrames = false;
}
}
/** {@inheritDoc} */
@ -197,10 +202,11 @@ public final class GraphicsSubsystemConfiguration implements SubsystemConfigurat
public void loadDefaultConfiguration() {
debug = false;
debugInput = false;
debugFrames = false;
errorGraphicsError = true;
disallowTearing = false;
maximumFramesPerSecond = 60;
}
/** {@inheritDoc} */
@ -209,10 +215,11 @@ public final class GraphicsSubsystemConfiguration implements SubsystemConfigurat
switch (setting) {
case "debug" -> { return debug; }
case "debugInput" -> { return debugInput; }
case "debugFrames" -> { return debugFrames; }
case "errorGraphicsError" -> { return errorGraphicsError; }
case "disallowTearing" -> { return disallowTearing; }
case "maximumFramesPerSecond" -> { return maximumFramesPerSecond; }
default -> { return null; }
}
}

View file

@ -19,6 +19,10 @@
package de.staropensource.sosengine.graphics.classes;
import org.jetbrains.annotations.NotNull;
import java.util.LinkedHashMap;
/**
* The interface for Graphics API management classes.
*
@ -33,4 +37,25 @@ public interface ApiManagementClass {
* @since v1-alpha2
*/
boolean mustRunOnMainThread();
/**
* Runs the render loop <b>once</b>.
* To run the render loop continuously, see {@link #runRenderLoopContinuously()}.
*
* @return map of windows and their thrown throwables
* @since v1-alpha2
*/
LinkedHashMap<@NotNull Window, @NotNull Throwable> runRenderLoop();
/**
* Runs the render loop <b>for ever</b>.
* To run the render loop only once, see {@link #runRenderLoop()}.
* <p>
* Immediately returns with when a {@link #runRenderLoop()} call fails.
*
* @param frameCode code that should be invoked on during a frame. will be counted to frame time
* @return see {@link #runRenderLoop()}
* @since v1-alpha2
*/
LinkedHashMap<@NotNull Window, @NotNull Throwable> runRenderLoopContinuously(@NotNull Runnable frameCode);
}

View file

@ -494,8 +494,8 @@ public abstract class Window implements AutoCloseable {
* @since v1-alpha2
*/
@NotNull
public static ImmutableHashSet<@NotNull Window> getWindows() {
return new ImmutableHashSet<>(windows);
public static HashSet<@NotNull Window> getWindows() {
return new HashSet<>(windows);
}
/**

View file

@ -27,13 +27,22 @@ import de.staropensource.sosengine.base.types.CodePart;
import de.staropensource.sosengine.base.types.logging.LogIssuer;
import de.staropensource.sosengine.base.types.vectors.Vec2i;
import de.staropensource.sosengine.base.utility.Miscellaneous;
import de.staropensource.sosengine.base.utility.parser.StackTraceParser;
import de.staropensource.sosengine.graphics.GraphicsSubsystem;
import de.staropensource.sosengine.graphics.classes.ApiMainClass;
import de.staropensource.sosengine.graphics.classes.ApiManagementClass;
import de.staropensource.sosengine.graphics.classes.Window;
import de.staropensource.sosengine.graphics.events.InputEvent;
import de.staropensource.sosengine.graphics.types.input.Key;
import de.staropensource.sosengine.graphics.types.input.KeyState;
import de.staropensource.sosengine.graphics.types.window.VsyncMode;
import lombok.Getter;
import lombok.SneakyThrows;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.LinkedHashMap;
import java.util.LinkedList;
/**
* The initialization class for sos!engine's development application.
@ -75,6 +84,13 @@ public class Main {
*/
private final LoggerInstance logger = new LoggerInstance(new LogIssuer(getClass(), CodePart.APPLICATION));
/**
* Used for terminating the render loop.
*
* @since v1-alpha2
*/
private boolean shutdown;
/**
* Constructs this class.
*/
@ -123,40 +139,38 @@ public class Main {
.setTitle("test application window")
.setSize(new Vec2i(960, 540))
.setPosition(new Vec2i(10, 10))
.setVsyncMode(VsyncMode.OFF)
.build();
} catch (Throwable throwable) {
logger.crash("Window.Builder#build() failed", throwable);
return;
}
// Sleep for 2.5 seconds
logger.diag("Sleeping for 2.5s");
try {
Thread.sleep(2500);
} catch (InterruptedException exception) {
logger.crash("Was unable to sleep for 2500ms", exception);
LinkedHashMap<@NotNull Window, @NotNull Throwable> renderLoopFailures = GraphicsSubsystem
.getInstance()
.getApi()
.getManagement()
.runRenderLoopContinuously(() -> {
if (shutdown || window.isClosureRequested())
Engine.getInstance().shutdown();
});
StringBuilder message = new StringBuilder();
message.append("Render loop failed on some windows:\n");
for (Window windowFailed : renderLoopFailures.keySet()) {
StackTraceParser parser = new StackTraceParser(renderLoopFailures.get(windowFailed));
message
.append("-> ")
.append(window)
.append(": ")
.append(parser.getHeader())
.append("\n")
.append(parser.getStackTrace())
.append("\n");
}
// Update state and render window
try {
logger.diag("Updating state");
window.updateState();
logger.diag("Rendering");
window.render();
} catch (Throwable throwable) {
logger.crash("Window updating or rendering failed", throwable);
}
// Sleep for five seconds
logger.diag("Sleeping for 2.5s");
try {
Thread.sleep(2500);
} catch (InterruptedException exception) {
logger.crash("Was unable to sleep for 2500ms", exception);
}
// Shutdown
Engine.getInstance().shutdown();
logger.crash(message.toString());
}, "mainThread");
}
@ -167,9 +181,24 @@ public class Main {
* @since v1-alpha2
*/
@EventListener(event = ThrowableCatchEvent.class)
public static void onThrowable(@NotNull Throwable throwable, @NotNull String identifier) {
if (identifier.equals("mainThread")) {
private static void onThrowable(@NotNull Throwable throwable, @NotNull String identifier) {
if (identifier.equals("mainThread") && instance != null)
instance.logger.crash("The main thread threw an exception", throwable);
}
/**
* Handles input events.
*
* @param window origin window
* @param key key pressed
* @param state key state
* @since v1-alpha2
*/
@EventListener(event = InputEvent.class)
private static void onInput(@Nullable Window window, @NotNull Key key, @NotNull KeyState state) {
if (key == Key.ESCAPE && instance != null) {
instance.logger.diag("ESC pressed, setting shutdown flag");
instance.shutdown = true;
}
}
}