From 4d0c067f8a3dc83a53d1d87fc6a4e81afa2b608f Mon Sep 17 00:00:00 2001 From: Emanuel Rabina Date: Fri, 24 May 2024 22:01:40 +1200 Subject: [PATCH] Extract render stats and fix any lingering resources --- .../redhorizon/engine/Application.groovy | 23 ++--- .../redhorizon/engine/EngineStats.groovy | 87 +++++++++++++++++++ .../engine/audio/AudioSystem.groovy | 2 + .../engine/graphics/GraphicsSystem.groovy | 10 +-- .../engine/graphics/imgui/DebugOverlay.groovy | 77 +++------------- .../graphics/opengl/OpenGLRenderer.groovy | 5 +- .../graphics/pipeline/RenderPass.groovy | 15 +--- .../graphics/pipeline/RenderPipeline.groovy | 12 ++- .../redhorizon/engine/scenegraph/Node.groovy | 2 +- .../engine/scenegraph/nodes/Animation.groovy | 1 + .../engine/scenegraph/nodes/Sound.groovy | 5 +- 11 files changed, 129 insertions(+), 110 deletions(-) create mode 100644 redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/EngineStats.groovy diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/Application.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/Application.groovy index 05910580..3070a7db 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/Application.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/Application.groovy @@ -74,7 +74,6 @@ class Application implements EventTarget { private ApplicationEventHandler applicationStop private boolean applicationStopping private Semaphore applicationStoppingSemaphore = new Semaphore(1) - private DebugOverlay debugOverlay /** * Add the audio system to this application. The audio system will run on its @@ -82,7 +81,8 @@ class Application implements EventTarget { */ Application addAudioSystem(AudioConfiguration config = new AudioConfiguration()) { - engine << new AudioSystem(config) + var audioSystem = new AudioSystem(config) + engine << audioSystem return this } @@ -99,15 +99,9 @@ class Application implements EventTarget { inputEventStream.addInputSource(event.window) } graphicsSystem.on(SystemReadyEvent) { event -> - var audioSystem = engine.systems.find { it instanceof AudioSystem } as AudioSystem - debugOverlay = new DebugOverlay(config.debug) - .addAudioRenderer(audioSystem.renderer) - .addGraphicsRenderer(graphicsSystem.renderer) - .toggleWith(inputEventStream, GLFW_KEY_D) - graphicsSystem.imGuiLayer.addOverlay(debugOverlay) + graphicsSystem.imGuiLayer.addOverlay(new DebugOverlay(config.debug).toggleWith(inputEventStream, GLFW_KEY_D)) graphicsSystem.imGuiLayer.addOverlay(new ControlsOverlay(inputEventStream).toggleWith(inputEventStream, GLFW_KEY_C)) graphicsSystem.imGuiLayer.addUiElement(new LogPanel(config.debug)) - uiElements.each { overlayRenderPass -> graphicsSystem.imGuiLayer.addUiElement(overlayRenderPass) } @@ -176,11 +170,12 @@ class Application implements EventTarget { logger.warn("Not all ${resourceName} closed, {} remaining", resourceCount) } } - check(debugOverlay.activeFramebuffers.get(), 'framebuffers') - check(debugOverlay.activeMeshes.get(), 'meshes') - check(debugOverlay.activeTextures.get(), 'textures') - check(debugOverlay.activeSources.get(), 'sources') - check(debugOverlay.activeBuffers.get(), 'buffers') + var engineStats = EngineStats.instance + check(engineStats.activeFramebuffers.get(), 'framebuffers') + check(engineStats.activeMeshes.get(), 'meshes') + check(engineStats.activeTextures.get(), 'textures') + check(engineStats.activeSources.get(), 'sources') + check(engineStats.activeBuffers.get(), 'buffers') } /** diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/EngineStats.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/EngineStats.groovy new file mode 100644 index 00000000..0886c027 --- /dev/null +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/EngineStats.groovy @@ -0,0 +1,87 @@ +/* + * Copyright 2024, Emanuel Rabina (http://www.ultraq.net.nz/) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nz.net.ultraq.redhorizon.engine + +import nz.net.ultraq.redhorizon.engine.audio.AudioRenderer +import nz.net.ultraq.redhorizon.engine.audio.AudioRendererEvent +import nz.net.ultraq.redhorizon.engine.audio.BufferCreatedEvent +import nz.net.ultraq.redhorizon.engine.audio.BufferDeletedEvent +import nz.net.ultraq.redhorizon.engine.audio.SourceCreatedEvent +import nz.net.ultraq.redhorizon.engine.audio.SourceDeletedEvent +import nz.net.ultraq.redhorizon.engine.graphics.DrawEvent +import nz.net.ultraq.redhorizon.engine.graphics.FramebufferCreatedEvent +import nz.net.ultraq.redhorizon.engine.graphics.FramebufferDeletedEvent +import nz.net.ultraq.redhorizon.engine.graphics.GraphicsRenderer +import nz.net.ultraq.redhorizon.engine.graphics.GraphicsRendererEvent +import nz.net.ultraq.redhorizon.engine.graphics.MeshCreatedEvent +import nz.net.ultraq.redhorizon.engine.graphics.MeshDeletedEvent +import nz.net.ultraq.redhorizon.engine.graphics.TextureCreatedEvent +import nz.net.ultraq.redhorizon.engine.graphics.TextureDeletedEvent + +import java.util.concurrent.atomic.AtomicInteger + +/** + * A record of several stats that we want to track for debugging purposes. + * + * @author Emanuel Rabina + */ +@Singleton +class EngineStats { + + final AtomicInteger drawCalls = new AtomicInteger() + final AtomicInteger activeFramebuffers = new AtomicInteger() + final AtomicInteger activeMeshes = new AtomicInteger() + final AtomicInteger activeTextures = new AtomicInteger() + + final AtomicInteger activeSources = new AtomicInteger() + final AtomicInteger activeBuffers = new AtomicInteger() + + /** + * Add the audio renderer to get stats on audio sources, buffers, etc. + */ + EngineStats attachAudioRenderer(AudioRenderer audioRenderer) { + + audioRenderer.on(AudioRendererEvent) { event -> + switch (event) { + case BufferCreatedEvent -> activeBuffers.incrementAndGet() + case BufferDeletedEvent -> activeBuffers.decrementAndGet() + case SourceCreatedEvent -> activeSources.incrementAndGet() + case SourceDeletedEvent -> activeSources.decrementAndGet() + } + } + return this + } + + /** + * Add the graphics renderer to get stats on draws, textures, etc. + */ + EngineStats attachGraphicsRenderer(GraphicsRenderer graphicsRenderer) { + + graphicsRenderer.on(GraphicsRendererEvent) { event -> + switch (event) { + case DrawEvent -> drawCalls.incrementAndGet() + case FramebufferCreatedEvent -> activeFramebuffers.incrementAndGet() + case FramebufferDeletedEvent -> activeFramebuffers.decrementAndGet() + case MeshCreatedEvent -> activeMeshes.incrementAndGet() + case MeshDeletedEvent -> activeMeshes.decrementAndGet() + case TextureCreatedEvent -> activeTextures.incrementAndGet() + case TextureDeletedEvent -> activeTextures.decrementAndGet() + } + } + return this + } +} diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/audio/AudioSystem.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/audio/AudioSystem.groovy index ed7344cc..a7b703e6 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/audio/AudioSystem.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/audio/AudioSystem.groovy @@ -16,6 +16,7 @@ package nz.net.ultraq.redhorizon.engine.audio +import nz.net.ultraq.redhorizon.engine.EngineStats import nz.net.ultraq.redhorizon.engine.EngineSystem import nz.net.ultraq.redhorizon.engine.SystemReadyEvent import nz.net.ultraq.redhorizon.engine.SystemStoppedEvent @@ -144,6 +145,7 @@ class AudioSystem extends EngineSystem implements AudioRequests { context.withCurrent { -> renderer = new OpenALRenderer(config) logger.debug(renderer.toString()) + EngineStats.instance.attachAudioRenderer(renderer) listener = new OpenALListener(config.volume) diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsSystem.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsSystem.groovy index 1e0ac3c1..7a9821be 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsSystem.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsSystem.groovy @@ -16,6 +16,7 @@ package nz.net.ultraq.redhorizon.engine.graphics +import nz.net.ultraq.redhorizon.engine.EngineStats import nz.net.ultraq.redhorizon.engine.EngineSystem import nz.net.ultraq.redhorizon.engine.SystemReadyEvent import nz.net.ultraq.redhorizon.engine.SystemStoppedEvent @@ -118,14 +119,6 @@ class GraphicsSystem extends EngineSystem implements GraphicsRequests { return renderer } - /** - * Return the rendering pipeline. - */ - RenderPipeline getRenderPipeline() { - - return renderPipeline - } - /** * Run through and complete any registered deletions, returning whether or not * there were items to process. @@ -230,6 +223,7 @@ class GraphicsSystem extends EngineSystem implements GraphicsRequests { renderer = new OpenGLRenderer(config, window) renderer.withCloseable { renderer -> + EngineStats.instance.attachGraphicsRenderer(renderer) camera = new OpenGLCamera(window.renderResolution) camera.withCloseable { camera -> diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/imgui/DebugOverlay.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/imgui/DebugOverlay.groovy index 73902643..821fbd19 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/imgui/DebugOverlay.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/imgui/DebugOverlay.groovy @@ -16,29 +16,13 @@ package nz.net.ultraq.redhorizon.engine.graphics.imgui -import nz.net.ultraq.redhorizon.engine.audio.AudioRenderer -import nz.net.ultraq.redhorizon.engine.audio.AudioRendererEvent -import nz.net.ultraq.redhorizon.engine.audio.BufferCreatedEvent -import nz.net.ultraq.redhorizon.engine.audio.BufferDeletedEvent -import nz.net.ultraq.redhorizon.engine.audio.SourceCreatedEvent -import nz.net.ultraq.redhorizon.engine.audio.SourceDeletedEvent -import nz.net.ultraq.redhorizon.engine.graphics.DrawEvent +import nz.net.ultraq.redhorizon.engine.EngineStats import nz.net.ultraq.redhorizon.engine.graphics.Framebuffer -import nz.net.ultraq.redhorizon.engine.graphics.FramebufferCreatedEvent -import nz.net.ultraq.redhorizon.engine.graphics.FramebufferDeletedEvent -import nz.net.ultraq.redhorizon.engine.graphics.GraphicsRenderer -import nz.net.ultraq.redhorizon.engine.graphics.GraphicsRendererEvent -import nz.net.ultraq.redhorizon.engine.graphics.MeshCreatedEvent -import nz.net.ultraq.redhorizon.engine.graphics.MeshDeletedEvent -import nz.net.ultraq.redhorizon.engine.graphics.TextureCreatedEvent -import nz.net.ultraq.redhorizon.engine.graphics.TextureDeletedEvent import imgui.ImGui import imgui.type.ImBoolean import static imgui.flag.ImGuiWindowFlags.* -import java.util.concurrent.atomic.AtomicInteger - /** * An overlay rendering pass for displaying debug information about the game. * @@ -48,16 +32,9 @@ class DebugOverlay implements ImGuiElement { // Debug information private final Map persistentLines = [:] - private AtomicInteger drawCalls = new AtomicInteger() private int debugWindowSizeX = 350 private int debugWindowSizeY = 200 - - // TODO: These stats can probably live outside of this object? - final AtomicInteger activeFramebuffers = new AtomicInteger() - final AtomicInteger activeMeshes = new AtomicInteger() - final AtomicInteger activeTextures = new AtomicInteger() - final AtomicInteger activeSources = new AtomicInteger() - final AtomicInteger activeBuffers = new AtomicInteger() + private final EngineStats engineStats /** * Constructor, create a new blank overlay. This is made more useful by @@ -71,45 +48,11 @@ class DebugOverlay implements ImGuiElement { persistentLines[event.persistentKey] = event.message } } + engineStats = EngineStats.instance this.enabled = enabled } - /** - * Add the audio renderer to get stats on audio sources, buffers, etc. - */ - DebugOverlay addAudioRenderer(AudioRenderer audioRenderer) { - - audioRenderer.on(AudioRendererEvent) { event -> - switch (event) { - case BufferCreatedEvent -> activeBuffers.incrementAndGet() - case BufferDeletedEvent -> activeBuffers.decrementAndGet() - case SourceCreatedEvent -> activeSources.incrementAndGet() - case SourceDeletedEvent -> activeSources.decrementAndGet() - } - } - return this - } - - /** - * Add the graphics renderer to get status on draws, textures, etc. - */ - DebugOverlay addGraphicsRenderer(GraphicsRenderer graphicsRenderer) { - - graphicsRenderer.on(GraphicsRendererEvent) { event -> - switch (event) { - case DrawEvent -> drawCalls.incrementAndGet() - case FramebufferCreatedEvent -> activeFramebuffers.incrementAndGet() - case FramebufferDeletedEvent -> activeFramebuffers.decrementAndGet() - case MeshCreatedEvent -> activeMeshes.incrementAndGet() - case MeshDeletedEvent -> activeMeshes.decrementAndGet() - case TextureCreatedEvent -> activeTextures.incrementAndGet() - case TextureDeletedEvent -> activeTextures.decrementAndGet() - } - } - return this - } - @Override void render(int dockspaceId, Framebuffer sceneFramebufferResult) { @@ -123,13 +66,13 @@ class DebugOverlay implements ImGuiElement { debugWindowSizeY = ImGui.getWindowSizeY() as int ImGui.text("Framerate: ${sprintf('%.1f', ImGui.getIO().framerate)}fps, Frametime: ${sprintf('%.1f', 1000 / ImGui.getIO().framerate)}ms") - ImGui.text("Draw calls: ${drawCalls}") - ImGui.text("Active meshes: ${activeMeshes}") - ImGui.text("Active textures: ${activeTextures}") - ImGui.text("Active framebuffers: ${activeFramebuffers}") - ImGui.text("Active sources: ${activeSources}") - ImGui.text("Active buffers: ${activeBuffers}") - drawCalls.set(0) + ImGui.text("Draw calls: ${engineStats.drawCalls}") + ImGui.text("Active meshes: ${engineStats.activeMeshes}") + ImGui.text("Active textures: ${engineStats.activeTextures}") + ImGui.text("Active framebuffers: ${engineStats.activeFramebuffers}") + ImGui.text("Active sources: ${engineStats.activeSources}") + ImGui.text("Active buffers: ${engineStats.activeBuffers}") + engineStats.drawCalls.set(0) ImGui.separator() persistentLines.keySet().sort().each { key -> diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/opengl/OpenGLRenderer.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/opengl/OpenGLRenderer.groovy index a0be7802..440c31a1 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/opengl/OpenGLRenderer.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/opengl/OpenGLRenderer.groovy @@ -251,7 +251,10 @@ class OpenGLRenderer implements GraphicsRenderer { if (resource) { resource.close() switch (resource) { - case Framebuffer -> trigger(new FramebufferDeletedEvent(resource)) + case Framebuffer -> { + trigger(new TextureDeletedEvent(resource.texture)) + trigger(new FramebufferDeletedEvent(resource)) + } case Mesh -> trigger(new MeshDeletedEvent(resource)) case Texture -> trigger(new TextureDeletedEvent(resource)) case SpriteSheet -> trigger(new TextureDeletedEvent(resource.texture)) diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/pipeline/RenderPass.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/pipeline/RenderPass.groovy index 368688a7..e3c8a434 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/pipeline/RenderPass.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/pipeline/RenderPass.groovy @@ -27,29 +27,16 @@ import nz.net.ultraq.redhorizon.engine.graphics.Switch * The expected type of input data from a prior rendering pass. * @author Emanuel Rabina */ -interface RenderPass extends Switch> { - - /** - * Perform any cleanup for this render pass. - * - * @param renderer - */ - default void delete(GraphicsRenderer renderer) { - } +interface RenderPass extends Switch>, AutoCloseable { /** * Return the target framebuffer for this rendering pass. - * - * @return */ Framebuffer getFramebuffer() /** * Perform the render pass, using the expected result of any previous render * pass. - * - * @param renderer - * @param previous */ void render(GraphicsRenderer renderer, T previous) } diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/pipeline/RenderPipeline.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/pipeline/RenderPipeline.groovy index a391c504..e6647e62 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/pipeline/RenderPipeline.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/pipeline/RenderPipeline.groovy @@ -88,7 +88,7 @@ class RenderPipeline implements Closeable { @Override void close() { - renderPasses*.delete(renderer) + renderPasses*.close() renderer.delete(fullScreenQuad) } @@ -179,6 +179,12 @@ class RenderPipeline implements Closeable { this.enabled = true } + @Override + void close() { + + renderer.delete(framebuffer) + } + @Override void render(GraphicsRenderer renderer, Void unused) { @@ -225,7 +231,7 @@ class RenderPipeline implements Closeable { } @Override - void delete(GraphicsRenderer renderer) { + void close() { renderer.delete(framebuffer) } @@ -287,7 +293,7 @@ class RenderPipeline implements Closeable { } @Override - void delete(GraphicsRenderer renderer) { + void close() { } @Override diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/Node.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/Node.groovy index 1f062d39..6a42033b 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/Node.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/Node.groovy @@ -83,7 +83,7 @@ class Node implements SceneEvents, Scriptable, Visitable { */ protected Vector3f getGlobalPosition() { - return globalTransform.getTranslation(globalPosition) + return getGlobalTransform().getTranslation(globalPosition) } /** diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/Animation.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/Animation.groovy index a65fc5c2..b17b45f9 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/Animation.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/Animation.groovy @@ -108,6 +108,7 @@ class Animation extends Node implements GraphicsElement, Playable, Te @Override CompletableFuture onSceneRemoved(Scene scene) { + stop() return CompletableFuture.allOf( animationSource.onSceneRemoved(scene), scene.requestDelete(mesh) diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/Sound.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/Sound.groovy index 939eb862..de1d52ab 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/Sound.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/Sound.groovy @@ -89,6 +89,7 @@ class Sound extends Node implements AudioElement, Playable, Temporal { @Override CompletableFuture onSceneRemoved(Scene scene) { + stop() return CompletableFuture.allOf( soundSource.onSceneRemoved(scene), scene.requestDelete(source) @@ -101,7 +102,7 @@ class Sound extends Node implements AudioElement, Playable, Temporal { // TODO: A lot of this should occur in an audio equivalent of the update() method if (source) { soundSource.prepareSource(renderer, source) - renderer.updateSource(source, globalPosition) + renderer.updateSource(source, getGlobalPosition()) // Control playback if (playing) { @@ -230,7 +231,7 @@ class Sound extends Node implements AudioElement, Playable, Temporal { CompletableFuture onSceneRemoved(Scene scene) { streamingDecoder.cancel(true) - return scene.requestDelete(*streamedBuffers.drain()) + return scene.requestDelete(streamedBuffers.drain().toArray(new Buffer[0])) } @Override