Add render loop
This commit is contained in:
parent
0b4502c3ce
commit
bbde2e9d2d
7 changed files with 241 additions and 73 deletions
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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 ] ------------------------------------------------ //
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue