diff --git a/graphics/glfw/src/main/java/de/staropensource/sosengine/graphics/glfw/classes/GlfwManagementClass.java b/graphics/glfw/src/main/java/de/staropensource/sosengine/graphics/glfw/classes/GlfwManagementClass.java new file mode 100644 index 00000000..39853559 --- /dev/null +++ b/graphics/glfw/src/main/java/de/staropensource/sosengine/graphics/glfw/classes/GlfwManagementClass.java @@ -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 . + */ + +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 renderTimes = new LinkedList<>(); + AtomicReference> 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(); + } +} diff --git a/graphics/glfw/src/main/java/de/staropensource/sosengine/graphics/glfw/classes/GlfwWindow.java b/graphics/glfw/src/main/java/de/staropensource/sosengine/graphics/glfw/classes/GlfwWindow.java index 83828953..7b6a55d7 100644 --- a/graphics/glfw/src/main/java/de/staropensource/sosengine/graphics/glfw/classes/GlfwWindow.java +++ b/graphics/glfw/src/main/java/de/staropensource/sosengine/graphics/glfw/classes/GlfwWindow.java @@ -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 ] ------------------------------------------------ // diff --git a/graphics/opengl/src/main/java/de/staropensource/sosengine/graphics/opengl/OpenGlManagement.java b/graphics/opengl/src/main/java/de/staropensource/sosengine/graphics/opengl/OpenGlManagement.java index 3a0714cc..9846cd7b 100644 --- a/graphics/opengl/src/main/java/de/staropensource/sosengine/graphics/opengl/OpenGlManagement.java +++ b/graphics/opengl/src/main/java/de/staropensource/sosengine/graphics/opengl/OpenGlManagement.java @@ -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; - } } diff --git a/graphics/src/main/java/de/staropensource/sosengine/graphics/GraphicsSubsystemConfiguration.java b/graphics/src/main/java/de/staropensource/sosengine/graphics/GraphicsSubsystemConfiguration.java index 6d6c7b52..a1918120 100644 --- a/graphics/src/main/java/de/staropensource/sosengine/graphics/GraphicsSubsystemConfiguration.java +++ b/graphics/src/main/java/de/staropensource/sosengine/graphics/GraphicsSubsystemConfiguration.java @@ -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. *

- * 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; } } } diff --git a/graphics/src/main/java/de/staropensource/sosengine/graphics/classes/ApiManagementClass.java b/graphics/src/main/java/de/staropensource/sosengine/graphics/classes/ApiManagementClass.java index 3194b096..950fe3fd 100644 --- a/graphics/src/main/java/de/staropensource/sosengine/graphics/classes/ApiManagementClass.java +++ b/graphics/src/main/java/de/staropensource/sosengine/graphics/classes/ApiManagementClass.java @@ -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 once. + * 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 for ever. + * To run the render loop only once, see {@link #runRenderLoop()}. + *

+ * 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); } diff --git a/graphics/src/main/java/de/staropensource/sosengine/graphics/classes/Window.java b/graphics/src/main/java/de/staropensource/sosengine/graphics/classes/Window.java index f106af7b..0c15e6d6 100644 --- a/graphics/src/main/java/de/staropensource/sosengine/graphics/classes/Window.java +++ b/graphics/src/main/java/de/staropensource/sosengine/graphics/classes/Window.java @@ -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); } /** diff --git a/testapp/src/main/java/de/staropensource/sosengine/testapp/Main.java b/testapp/src/main/java/de/staropensource/sosengine/testapp/Main.java index 5378a79f..7baad6f2 100644 --- a/testapp/src/main/java/de/staropensource/sosengine/testapp/Main.java +++ b/testapp/src/main/java/de/staropensource/sosengine/testapp/Main.java @@ -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; } } }