From a7c8571afa8d13d00b41c93657647f8d8ab2dd8b Mon Sep 17 00:00:00 2001 From: austin Date: Sun, 15 Sep 2024 13:04:08 -0400 Subject: [PATCH] testing fix --- .../creatures/individual/skeletonprogress.md | 4 +- docs/src/progress/renderertodo.md | 1 + .../java/electrosphere/engine/Globals.java | 20 +++- .../engine/threads/ThreadManager.java | 17 ++- .../electrosphere/renderer/OpenGLState.java | 12 ++- .../renderer/RenderingEngine.java | 71 ++++++------- .../renderer/framebuffer/Framebuffer.java | 20 +++- .../renderer/texture/Texture.java | 28 ++++- .../renderer/ui/UIExtensionTests.java | 100 ++++++++++++++++++ .../renderer/ui/elements/WindowTest.java | 7 +- .../test/template/UITestTemplate.java | 13 +++ .../StateCleanupCheckerExtension.java | 4 + .../test/template/extensions/UIExtension.java | 3 + .../test/testutils/Assertions.java | 5 + .../test/testutils/TestEngineUtils.java | 4 + .../renderer/ui/elements/ui-test.png | Bin .../renderer/ui/test_Screencapture_Blank.png | Bin 0 -> 10111 bytes .../renderer/ui/test_Screencapture_Match.png | Bin 0 -> 28071 bytes 18 files changed, 246 insertions(+), 63 deletions(-) create mode 100644 src/test/java/electrosphere/renderer/ui/UIExtensionTests.java rename test/java/{electrosphere => }/renderer/ui/elements/ui-test.png (100%) create mode 100644 test/java/renderer/ui/test_Screencapture_Blank.png create mode 100644 test/java/renderer/ui/test_Screencapture_Match.png diff --git a/docs/src/highlevel-design/creatures/individual/skeletonprogress.md b/docs/src/highlevel-design/creatures/individual/skeletonprogress.md index 46f72939..dfb90898 100644 --- a/docs/src/highlevel-design/creatures/individual/skeletonprogress.md +++ b/docs/src/highlevel-design/creatures/individual/skeletonprogress.md @@ -1,6 +1,6 @@ -@page humanprogress Human +@page skeletonprogres Skeleton -Progress on the human creature +Progress on the skeleton creature ## Third Person Model - [X] Meshed diff --git a/docs/src/progress/renderertodo.md b/docs/src/progress/renderertodo.md index adfe928f..1fe18ae7 100644 --- a/docs/src/progress/renderertodo.md +++ b/docs/src/progress/renderertodo.md @@ -765,6 +765,7 @@ Fix movement packet timing bug Fix all items spawning above player head Fix items falling below terrain Fix gridded data cell manager saving attached items on realm save +Fix render signals caching between frames (not reseting global flags per usual) # TODO diff --git a/src/main/java/electrosphere/engine/Globals.java b/src/main/java/electrosphere/engine/Globals.java index 1edade3e..5058fcff 100644 --- a/src/main/java/electrosphere/engine/Globals.java +++ b/src/main/java/electrosphere/engine/Globals.java @@ -240,8 +240,8 @@ public class Globals { //OpenGL - Other // - public static int WINDOW_WIDTH = 1920; - public static int WINDOW_HEIGHT = 1080; + public static int WINDOW_WIDTH; + public static int WINDOW_HEIGHT; public static boolean WINDOW_DECORATED = true; //used to control whether the window is created with decorations or not (ie for testing) public static boolean WINDOW_FULLSCREEN = false; //used to control whether the window is created fullscreen or not (ie for testing) @@ -444,10 +444,22 @@ public class Globals { Globals.javaPID = ManagementFactory.getRuntimeMXBean().getName(); } //load user settings + Globals.WINDOW_WIDTH = 1920; + Globals.WINDOW_HEIGHT = 1080; UserSettings.loadUserSettings(); //timekeeper timekeeper = new Timekeeper(); - threadManager = new ThreadManager(); + Globals.threadManager = new ThreadManager(); + Globals.threadManager.init(); + + //render flags + RENDER_FLAG_RENDER_SHADOW_MAP = false; + RENDER_FLAG_RENDER_SCREEN_FRAMEBUFFER_CONTENT = false; + RENDER_FLAG_RENDER_SCREEN_FRAMEBUFFER = false; + RENDER_FLAG_RENDER_BLACK_BACKGROUND = true; + RENDER_FLAG_RENDER_WHITE_BACKGROUND = false; + RENDER_FLAG_RENDER_UI = true; + RENDER_FLAG_RENDER_UI_BOUNDS = false; //load in default texture map textureMapDefault = TextureMap.construct("Textures/default_texture_map.json"); //load model pretransforms @@ -667,6 +679,7 @@ public class Globals { Globals.clientScene = null; Globals.audioEngine = null; Globals.renderingEngine = null; + Globals.threadManager = null; Globals.signalSystem = null; Globals.serviceManager = null; Globals.clientConnection = null; @@ -681,6 +694,7 @@ public class Globals { Globals.RENDER_FLAG_RENDER_UI = false; Globals.RENDER_FLAG_RENDER_BLACK_BACKGROUND = false; Globals.RENDER_FLAG_RENDER_WHITE_BACKGROUND = false; + Globals.window = -1; LoggerInterface.destroyLoggers(); } diff --git a/src/main/java/electrosphere/engine/threads/ThreadManager.java b/src/main/java/electrosphere/engine/threads/ThreadManager.java index 4d643f33..23a189b3 100644 --- a/src/main/java/electrosphere/engine/threads/ThreadManager.java +++ b/src/main/java/electrosphere/engine/threads/ThreadManager.java @@ -17,18 +17,27 @@ import electrosphere.util.CodeUtils; public class ThreadManager { //Threadsafes the manager - Semaphore threadLock = new Semaphore(1); + Semaphore threadLock; //All threads that are actively running - private List activeThreads = new LinkedList(); + private List activeThreads; //All loading threads that are actively running - private List loadingThreads = new LinkedList(); + private List loadingThreads; //Used by main thread to alert other threads whether they should keep running or not - private boolean shouldKeepRunning = true; + private boolean shouldKeepRunning; + /** + * Initializes the thread manager + */ + public void init(){ + threadLock = new Semaphore(1); + activeThreads = new LinkedList(); + loadingThreads = new LinkedList(); + shouldKeepRunning = true; + } /** * Updates what threads are being tracked diff --git a/src/main/java/electrosphere/renderer/OpenGLState.java b/src/main/java/electrosphere/renderer/OpenGLState.java index 15adc76c..3d0ab4f8 100644 --- a/src/main/java/electrosphere/renderer/OpenGLState.java +++ b/src/main/java/electrosphere/renderer/OpenGLState.java @@ -20,7 +20,7 @@ import electrosphere.renderer.shader.ShaderProgram; public class OpenGLState { //tracks whether caching should be used or not (to deduplicate opengl calls) - private static final boolean DISABLE_CACHING = false; + private static final boolean DISABLE_CACHING = true; //the max texture allowed by the current environment int MAX_TEXTURE_WIDTH; @@ -67,14 +67,18 @@ public class OpenGLState { */ public void init(){ this.MAX_TEXTURE_WIDTH = 0; - this.viewport = new Vector2i(0,0); + this.viewport = new Vector2i(Globals.WINDOW_WIDTH,Globals.WINDOW_HEIGHT); + GL45.glViewport(0, 0, Globals.WINDOW_WIDTH, Globals.WINDOW_HEIGHT); this.depthTest = false; + GL45.glDisable(GL45.GL_DEPTH_TEST); this.depthFunction = -1; this.blendTest = false; + GL45.glDisable(GL45.GL_BLEND); this.blendFuncMap = new HashMap(); - activeTexture = 0; - framebufferType = 0; + activeTexture = -1; + framebufferType = GL45.GL_FRAMEBUFFER; framebufferPointer = 0; + GL45.glBindFramebuffer(this.framebufferType, this.framebufferPointer); activeShader = null; this.unitToPointerMap = new HashMap(); this.indexBlockMap = new HashMap(); diff --git a/src/main/java/electrosphere/renderer/RenderingEngine.java b/src/main/java/electrosphere/renderer/RenderingEngine.java index 36b6eb7d..f5c70533 100644 --- a/src/main/java/electrosphere/renderer/RenderingEngine.java +++ b/src/main/java/electrosphere/renderer/RenderingEngine.java @@ -1,20 +1,6 @@ package electrosphere.renderer; import static electrosphere.renderer.RenderUtils.createScreenTextureVAO; -import static org.lwjgl.glfw.GLFW.GLFW_CONTEXT_VERSION_MAJOR; -import static org.lwjgl.glfw.GLFW.GLFW_CONTEXT_VERSION_MINOR; -import static org.lwjgl.glfw.GLFW.GLFW_FALSE; -import static org.lwjgl.glfw.GLFW.GLFW_OPENGL_CORE_PROFILE; -import static org.lwjgl.glfw.GLFW.GLFW_OPENGL_PROFILE; -import static org.lwjgl.glfw.GLFW.glfwCreateWindow; -import static org.lwjgl.glfw.GLFW.glfwGetPrimaryMonitor; -import static org.lwjgl.glfw.GLFW.glfwInit; -import static org.lwjgl.glfw.GLFW.glfwMakeContextCurrent; -import static org.lwjgl.glfw.GLFW.glfwMaximizeWindow; -import static org.lwjgl.glfw.GLFW.glfwPollEvents; -import static org.lwjgl.glfw.GLFW.glfwSwapBuffers; -import static org.lwjgl.glfw.GLFW.glfwTerminate; -import static org.lwjgl.glfw.GLFW.glfwWindowHint; import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT; import static org.lwjgl.opengl.GL11.GL_DEPTH_TEST; import static org.lwjgl.opengl.GL11.GL_ONE_MINUS_SRC_ALPHA; @@ -190,47 +176,52 @@ public class RenderingEngine { // //set error callback - // GLFWErrorCallback + // GLFW.glfwSetErrorCallback((int error, long descriptionPtr) -> { String description = GLFWErrorCallback.getDescription(descriptionPtr); System.err.println(description); }); //Initializes opengl - boolean glfwInited = glfwInit(); + boolean glfwInited = GLFW.glfwInit(); if(!glfwInited){ String message = "Failed to initialize glfw!\n" + "Error code: " + this.getGLFWErrorMessage(this.getGLFWError()); throw new IllegalStateException(message); } //Gives hints to glfw to control how opengl will be used - glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); - glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); - glslVersion = "#version 430"; - glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 4); + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 5); + glslVersion = "#version 450"; + GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE); //headless option if(Globals.RUN_HIDDEN){ - glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE); + GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE); } if(!Globals.WINDOW_DECORATED){ - glfwWindowHint(GLFW.GLFW_DECORATED, GLFW_FALSE); + GLFW.glfwWindowHint(GLFW.GLFW_DECORATED, GLFW.GLFW_FALSE); + } + if(Globals.ENGINE_DEBUG){ + GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_DEBUG_CONTEXT, GLFW.GLFW_TRUE); + } + + if(Globals.userSettings.getDisplayWidth() <= 0 || Globals.userSettings.getDisplayHeight() <= 0){ + throw new Error("Trying to create window with width or height less than 1! " + Globals.userSettings.getDisplayWidth() + " " + Globals.userSettings.getDisplayHeight()); } -// glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, GLFW_TRUE); Allows you to make the background transparent -// glfwWindowHint(GLFW_OPACITY, 23); //Creates the window reference object if(Globals.userSettings.displayFullscreen() || Globals.WINDOW_FULLSCREEN){ //below line is for fullscreen - Globals.window = glfwCreateWindow(Globals.WINDOW_WIDTH, Globals.WINDOW_HEIGHT, "ORPG", glfwGetPrimaryMonitor(), NULL); + Globals.window = GLFW.glfwCreateWindow(Globals.userSettings.getDisplayWidth(), Globals.userSettings.getDisplayHeight(), "ORPG", GLFW.glfwGetPrimaryMonitor(), NULL); } else { - Globals.window = glfwCreateWindow(Globals.WINDOW_WIDTH, Globals.WINDOW_HEIGHT, "ORPG", NULL, NULL); + Globals.window = GLFW.glfwCreateWindow(Globals.userSettings.getDisplayWidth(), Globals.userSettings.getDisplayHeight(), "ORPG", NULL, NULL); } // Errors for failure to create window (IE: No GUI mode on linux ?) if (Globals.window == NULL) { String message = "Failed to create window!\n" + "Error code: " + this.getGLFWErrorMessage(this.getGLFWError()); ; - LoggerInterface.loggerEngine.ERROR(new Exception(message)); - glfwTerminate(); + GLFW.glfwTerminate(); + throw new Error(message); } //set resize callback @@ -239,9 +230,9 @@ public class RenderingEngine { Globals.WINDOW_WIDTH = width; }); //Makes the window that was just created the current OS-level window context - glfwMakeContextCurrent(Globals.window); + GLFW.glfwMakeContextCurrent(Globals.window); //Maximize it - glfwMaximizeWindow(Globals.window); + GLFW.glfwMaximizeWindow(Globals.window); GLFW.glfwPollEvents(); //grab actual framebuffer IntBuffer xBuffer = BufferUtils.createIntBuffer(1); @@ -254,10 +245,11 @@ public class RenderingEngine { //get title bar size Globals.WINDOW_TITLE_BAR_HEIGHT = Globals.WINDOW_HEIGHT - bufferHeight; -// System.out.println(Globals.WINDOW_TITLE_BAR_HEIGHT); - Globals.WINDOW_WIDTH = bufferWidth; Globals.WINDOW_HEIGHT = bufferHeight; + if(bufferWidth == 0 || bufferHeight == 0){ + throw new Error("Failed to get width or height! " + Globals.WINDOW_WIDTH + " " + Globals.WINDOW_HEIGHT); + } // // Attach controls callbacks @@ -289,7 +281,7 @@ public class RenderingEngine { imGuiPipeline = new ImGuiPipeline(Globals.window, glslVersion); //This enables Z-buffering so that farther-back polygons are not drawn over nearer ones - glEnable(GL_DEPTH_TEST); + openGLState.glDepthTest(true); // Support for transparency openGLState.glBlend(true); @@ -300,6 +292,10 @@ public class RenderingEngine { if(!Globals.userSettings.graphicsPerformanceEnableVSync()){ GLFW.glfwSwapInterval(0); } + + //clear screen + GL45.glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + GL45.glClear(GL45.GL_COLOR_BUFFER_BIT | GL45.GL_DEPTH_BUFFER_BIT); // //Hide the cursor and capture it // glfwSetInputMode(Globals.window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); @@ -314,7 +310,6 @@ public class RenderingEngine { //default framebuffer defaultFramebuffer = new Framebuffer(GL_DEFAULT_FRAMEBUFFER); - defaultFramebuffer.bind(openGLState); //generate framebuffers Texture screenTextureColor = FramebufferUtils.generateScreenTextureColorAlpha(openGLState, Globals.WINDOW_WIDTH, Globals.WINDOW_HEIGHT); @@ -557,9 +552,9 @@ public class RenderingEngine { //check and call events and swap the buffers LoggerInterface.loggerRenderer.DEBUG_LOOP("GLFW Swap buffers"); - glfwSwapBuffers(Globals.window); + GLFW.glfwSwapBuffers(Globals.window); LoggerInterface.loggerRenderer.DEBUG_LOOP("GLFW Poll Events"); - glfwPollEvents(); + GLFW.glfwPollEvents(); LoggerInterface.loggerRenderer.DEBUG_LOOP("Check OpenGL Errors"); checkError(); } @@ -668,9 +663,9 @@ public class RenderingEngine { public static void recaptureIfNecessary(){ if(Globals.controlHandler.shouldRecapture()){ //Makes the window that was just created the current OS-level window context - glfwMakeContextCurrent(Globals.window); + GLFW.glfwMakeContextCurrent(Globals.window); // //Maximize it - glfwMaximizeWindow(Globals.window); + GLFW.glfwMaximizeWindow(Globals.window); //grab focus GLFW.glfwFocusWindow(Globals.window); //apply mouse controls state diff --git a/src/main/java/electrosphere/renderer/framebuffer/Framebuffer.java b/src/main/java/electrosphere/renderer/framebuffer/Framebuffer.java index 499d5d68..e8b1b08e 100644 --- a/src/main/java/electrosphere/renderer/framebuffer/Framebuffer.java +++ b/src/main/java/electrosphere/renderer/framebuffer/Framebuffer.java @@ -13,9 +13,9 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; +import org.lwjgl.BufferUtils; import org.lwjgl.opengl.GL40; import org.lwjgl.opengl.GL45; -import org.lwjgl.system.MemoryUtil; import static org.lwjgl.opengl.GL11.GL_NONE; import static org.lwjgl.opengl.GL11.GL_TEXTURE; @@ -291,7 +291,7 @@ public class Framebuffer { int type = GL40.GL_UNSIGNED_BYTE; - bind(openGLState); + this.bind(openGLState); GL40.glReadBuffer(GL40.GL_COLOR_ATTACHMENT0); if(this.framebufferPointer == 0){ //this is the default framebuffer, read from backbuffer because it is default @@ -303,11 +303,23 @@ public class Framebuffer { } else { LoggerInterface.loggerRenderer.ERROR(new IllegalStateException("Tried to get pixels from a framebuffer that does not have a texture attached to attachment point 0.")); } + + //error check + if(width < 1){ + throw new Error("Invalid width! " + width); + } + if(height < 1){ + throw new Error("Invalid height! " + height); + } + //get pixel data try { int bytesPerPixel = pixelFormatToBytes(pixelFormat,type); int bufferSize = width * height * bytesPerPixel; - ByteBuffer buffer = MemoryUtil.memAlloc(bufferSize); + ByteBuffer buffer = BufferUtils.createByteBuffer(bufferSize); + if(buffer == null || buffer.limit() < bufferSize){ + throw new Error("Failed to create buffer!"); + } openGLState.glViewport(width, height); GL40.glReadPixels(offsetX, offsetY, width, height, pixelFormat, type, buffer); Globals.renderingEngine.checkError(); @@ -326,8 +338,6 @@ public class Framebuffer { rVal.setRGB(x, height - (y + 1), (alpha << 24) | (red << 16) | (green << 8) | blue); } } - //memory management - MemoryUtil.memFree(buffer); } catch (OutOfMemoryError e){ LoggerInterface.loggerRenderer.ERROR(new IllegalStateException(e.getMessage())); } diff --git a/src/main/java/electrosphere/renderer/texture/Texture.java b/src/main/java/electrosphere/renderer/texture/Texture.java index 899bf669..3d8f986d 100644 --- a/src/main/java/electrosphere/renderer/texture/Texture.java +++ b/src/main/java/electrosphere/renderer/texture/Texture.java @@ -385,6 +385,12 @@ public class Texture { * @param datatype The data type of a single component of a pixel (ie GL_BYTE, GL_UNSIGNED_INT, etc) */ public void glTexImage2D(OpenGLState openGLState, int width, int height, int format, int datatype){ + if(width < 1){ + throw new Error("Invalid texture width " + width); + } + if(height < 1){ + throw new Error("Invalid texture height " + height); + } //store provided values this.width = width; this.height = height; @@ -420,6 +426,12 @@ public class Texture { * @param datatype The data type of a single component of a pixel (ie GL_BYTE, GL_UNSIGNED_INT, etc) */ public void glTextureStorage2D(OpenGLState openGLState, int width, int height, int format, int datatype){ + if(width < 1){ + throw new Error("Invalid texture width " + width); + } + if(height < 1){ + throw new Error("Invalid texture height " + height); + } //store provided values this.width = width; this.height = height; @@ -437,6 +449,12 @@ public class Texture { * @param data The data to populate the image with */ public void glTexImage2D(OpenGLState openGLState, int width, int height, int format, int datatype, ByteBuffer data){ + if(width < 1){ + throw new Error("Invalid texture width " + width); + } + if(height < 1){ + throw new Error("Invalid texture height " + height); + } //store provided values this.width = width; this.height = height; @@ -461,6 +479,12 @@ public class Texture { * @param data The data to populate the image with */ public void glTexImage2D(OpenGLState openGLState, int internalFormat, int width, int height, int format, int datatype, ByteBuffer data){ + if(width < 1){ + throw new Error("Invalid texture width " + width); + } + if(height < 1){ + throw new Error("Invalid texture height " + height); + } //store provided values this.width = width; this.height = height; @@ -480,7 +504,7 @@ public class Texture { * @return The width */ public int getWidth(){ - if(pixelFormat == -1){ + if(width == -1){ throw new IllegalStateException( "The width of the texture you are trying to query from has not been set yet." + " The texture was likely constructed by passing the opengl texture pointer into the texture object." @@ -494,7 +518,7 @@ public class Texture { * @return The height */ public int getHeight(){ - if(pixelFormat == -1){ + if(height == -1){ throw new IllegalStateException( "The height of the texture you are trying to query from has not been set yet." + " The texture was likely constructed by passing the opengl texture pointer into the texture object." diff --git a/src/test/java/electrosphere/renderer/ui/UIExtensionTests.java b/src/test/java/electrosphere/renderer/ui/UIExtensionTests.java new file mode 100644 index 00000000..ce577b8b --- /dev/null +++ b/src/test/java/electrosphere/renderer/ui/UIExtensionTests.java @@ -0,0 +1,100 @@ +package electrosphere.renderer.ui; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.File; + +import org.junit.jupiter.api.extension.ExtendWith; + +import static electrosphere.test.testutils.Assertions.*; + +import electrosphere.engine.Globals; +import electrosphere.engine.Main; +import electrosphere.menu.WindowUtils; +import electrosphere.renderer.ui.elements.Div; +import electrosphere.test.annotations.IntegrationTest; +import electrosphere.test.template.extensions.StateCleanupCheckerExtension; +import electrosphere.test.testutils.EngineInit; +import electrosphere.test.testutils.TestEngineUtils; +import electrosphere.test.testutils.TestRenderingUtils; + +/** + * Tests to verify the ui test template (we're testing our own testing framework woooooo) + */ +@ExtendWith(StateCleanupCheckerExtension.class) +public class UIExtensionTests { + + @IntegrationTest + public void test_StartupShutdown_NoThrow(){ + assertDoesNotThrow(() -> { + Globals.WINDOW_DECORATED = false; + Globals.WINDOW_FULLSCREEN = true; + Globals.RUN_AUDIO = false; + Globals.RUN_SCRIPTS = false; + Globals.WINDOW_WIDTH = 1920; + Globals.WINDOW_HEIGHT = 1080; + EngineInit.initGraphicalEngine(); + TestEngineUtils.flush(); + + Main.shutdown(); + }); + } + + @IntegrationTest + public void test_Screencapture_Match(){ + Globals.WINDOW_DECORATED = false; + Globals.WINDOW_FULLSCREEN = true; + Globals.RUN_AUDIO = false; + Globals.RUN_SCRIPTS = false; + Globals.WINDOW_WIDTH = 1920; + Globals.WINDOW_HEIGHT = 1080; + EngineInit.initGraphicalEngine(); + TestEngineUtils.flush(); + + TestEngineUtils.simulateFrames(3); + + String canonicalName = this.getClass().getCanonicalName(); + //check the render + assertEqualsRender("./test/java/renderer/ui/test_Screencapture_Match.png", () -> { + + //on failure, save the failed render + String failureSavePath = "./.testcache/" + canonicalName + "-test_Screencapture_Match.png"; + File saveFile = new File(failureSavePath); + System.err.println("[[ATTACHMENT|" + saveFile.getAbsolutePath() + "]]"); + TestRenderingUtils.saveTestRender(failureSavePath); + }); + Main.shutdown(); + } + + @IntegrationTest + public void test_Screencapture_Blank_Match(){ + Globals.WINDOW_DECORATED = false; + Globals.WINDOW_FULLSCREEN = true; + Globals.RUN_AUDIO = false; + Globals.RUN_SCRIPTS = false; + Globals.WINDOW_WIDTH = 1920; + Globals.WINDOW_HEIGHT = 1080; + EngineInit.initGraphicalEngine(); + TestEngineUtils.flush(); + + TestEngineUtils.simulateFrames(3); + + WindowUtils.replaceMainMenuContents(Div.createDiv()); + TestEngineUtils.flush(); + TestEngineUtils.simulateFrames(2); + + String canonicalName = this.getClass().getCanonicalName(); + //check the render + assertEqualsRender("./test/java/renderer/ui/test_Screencapture_Blank.png", () -> { + + //on failure, save the failed render + String failureSavePath = "./.testcache/" + canonicalName + "-test_Screencapture_Blank.png"; + File saveFile = new File(failureSavePath); + System.err.println("[[ATTACHMENT|" + saveFile.getAbsolutePath() + "]]"); + TestRenderingUtils.saveTestRender(failureSavePath); + }); + + Main.shutdown(); + } + +} diff --git a/src/test/java/electrosphere/renderer/ui/elements/WindowTest.java b/src/test/java/electrosphere/renderer/ui/elements/WindowTest.java index 40c49e27..44c52feb 100644 --- a/src/test/java/electrosphere/renderer/ui/elements/WindowTest.java +++ b/src/test/java/electrosphere/renderer/ui/elements/WindowTest.java @@ -2,8 +2,6 @@ package electrosphere.renderer.ui.elements; import electrosphere.test.annotations.IntegrationTest; -import org.junit.jupiter.api.Disabled; - import electrosphere.menu.WindowUtils; import electrosphere.menu.mainmenu.MenuGeneratorsUITesting; import electrosphere.test.template.UITestTemplate; @@ -17,7 +15,6 @@ public class WindowTest extends UITestTemplate { /** * Tests creating a window */ - @Disabled @IntegrationTest public void testCreateWindow(){ //create ui testing window @@ -29,8 +26,8 @@ public class WindowTest extends UITestTemplate { TestEngineUtils.simulateFrames(60); - // TestRenderingUtils.saveTestRender("./test/java/electrosphere/renderer/ui/elements/window.png"); - this.checkRender("Basic", "./test/java/electrosphere/renderer/ui/elements/ui-test.png"); + // TestRenderingUtils.saveTestRender("./test/java/renderer/ui/elements/window.png"); + this.checkRender("Basic", "./test/java/renderer/ui/elements/ui-test.png"); } } diff --git a/src/test/java/electrosphere/test/template/UITestTemplate.java b/src/test/java/electrosphere/test/template/UITestTemplate.java index 5b833b82..487e7aca 100644 --- a/src/test/java/electrosphere/test/template/UITestTemplate.java +++ b/src/test/java/electrosphere/test/template/UITestTemplate.java @@ -5,10 +5,14 @@ import java.io.File; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.extension.ExtendWith; +import electrosphere.menu.WindowUtils; +import electrosphere.renderer.ui.elements.Div; import electrosphere.test.template.extensions.StateCleanupCheckerExtension; import electrosphere.test.template.extensions.UIExtension; import static electrosphere.test.testutils.Assertions.*; + +import electrosphere.test.testutils.TestEngineUtils; import electrosphere.test.testutils.TestRenderingUtils; /** @@ -40,4 +44,13 @@ public class UITestTemplate { }); } + /** + * Sets up a blank view + */ + public void setupBlankView(){ + WindowUtils.replaceMainMenuContents(Div.createDiv()); + TestEngineUtils.flush(); + TestEngineUtils.simulateFrames(2); + } + } diff --git a/src/test/java/electrosphere/test/template/extensions/StateCleanupCheckerExtension.java b/src/test/java/electrosphere/test/template/extensions/StateCleanupCheckerExtension.java index 8eaf674d..44777fde 100644 --- a/src/test/java/electrosphere/test/template/extensions/StateCleanupCheckerExtension.java +++ b/src/test/java/electrosphere/test/template/extensions/StateCleanupCheckerExtension.java @@ -19,6 +19,7 @@ public class StateCleanupCheckerExtension implements AfterEachCallback { Globals.clientSceneWrapper, Globals.clientScene, Globals.signalSystem, + Globals.threadManager, Globals.renderingEngine, Globals.audioEngine, Globals.javaPID, @@ -35,6 +36,9 @@ public class StateCleanupCheckerExtension implements AfterEachCallback { throw new Exception("Failed to cleanup state after test! " + object.toString()); } } + if(Globals.window != -1){ + throw new Exception("Failed to cleanup global window pointer!"); + } } } diff --git a/src/test/java/electrosphere/test/template/extensions/UIExtension.java b/src/test/java/electrosphere/test/template/extensions/UIExtension.java index b7cd9738..bce2cfff 100644 --- a/src/test/java/electrosphere/test/template/extensions/UIExtension.java +++ b/src/test/java/electrosphere/test/template/extensions/UIExtension.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.extension.ExtensionContext; import electrosphere.engine.Globals; import electrosphere.engine.Main; import electrosphere.test.testutils.EngineInit; +import electrosphere.test.testutils.TestEngineUtils; /** * Spins up an tears down generic ui environment @@ -18,9 +19,11 @@ public class UIExtension implements BeforeEachCallback, AfterEachCallback { Globals.WINDOW_DECORATED = false; Globals.WINDOW_FULLSCREEN = true; Globals.RUN_AUDIO = false; + Globals.RUN_SCRIPTS = false; Globals.WINDOW_WIDTH = 1920; Globals.WINDOW_HEIGHT = 1080; EngineInit.initGraphicalEngine(); + TestEngineUtils.flush(); } @Override diff --git a/src/test/java/electrosphere/test/testutils/Assertions.java b/src/test/java/electrosphere/test/testutils/Assertions.java index 4285c872..b82a536a 100644 --- a/src/test/java/electrosphere/test/testutils/Assertions.java +++ b/src/test/java/electrosphere/test/testutils/Assertions.java @@ -10,6 +10,7 @@ import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import electrosphere.engine.Globals; +import electrosphere.engine.Main; /** * Custom assertion macros @@ -35,6 +36,7 @@ public class Assertions { try { testData = ImageIO.read(new File(existingRenderPath)); } catch (IOException e){ + Main.shutdown(); fail("Failed to read existing image path " + existingRenderPath); } BufferedImage screenshot = Globals.renderingEngine.defaultFramebuffer.getPixels(Globals.renderingEngine.getOpenGLState()); @@ -43,6 +45,7 @@ public class Assertions { //width if(testData.getWidth() != screenshot.getWidth()){ onFailure.run(); + Main.shutdown(); } assertEquals(testData.getWidth(), screenshot.getWidth()); @@ -50,6 +53,7 @@ public class Assertions { //height if(testData.getHeight() != screenshot.getHeight()){ onFailure.run(); + Main.shutdown(); } assertEquals(testData.getHeight(), screenshot.getHeight()); @@ -79,6 +83,7 @@ public class Assertions { ){ onFailure.run(); + Main.shutdown(); String failMessage = "Colors aren't approximately the same!\n" + "Color from disk: " + sourceRed + "," + sourceGreen + "," + sourceBlue + "," + sourceAlpha + "\n" + "Color from render: " + renderRed + "," + renderGreen + "," + renderBlue + "," + renderAlpha + "\n" diff --git a/src/test/java/electrosphere/test/testutils/TestEngineUtils.java b/src/test/java/electrosphere/test/testutils/TestEngineUtils.java index 6eb2f4f7..4f7b3381 100644 --- a/src/test/java/electrosphere/test/testutils/TestEngineUtils.java +++ b/src/test/java/electrosphere/test/testutils/TestEngineUtils.java @@ -114,6 +114,10 @@ public class TestEngineUtils { } } + if(frames == 0){ + TestEngineUtils.simulateFrames(1); + } + while( Globals.elementService.getSignalQueueCount() > 0 ){ diff --git a/test/java/electrosphere/renderer/ui/elements/ui-test.png b/test/java/renderer/ui/elements/ui-test.png similarity index 100% rename from test/java/electrosphere/renderer/ui/elements/ui-test.png rename to test/java/renderer/ui/elements/ui-test.png diff --git a/test/java/renderer/ui/test_Screencapture_Blank.png b/test/java/renderer/ui/test_Screencapture_Blank.png new file mode 100644 index 0000000000000000000000000000000000000000..de9dd9a43d08b95f4ad8f9138bcb0f43d728f843 GIT binary patch literal 10111 zcmeAS@N?(olHy`uVBq!ia0y~yU~gbxV6os}1B$%3e9)PJLEX*M#WAEJ?#&59!2=Eg z%o`v3|6)!Mv{>xa`|h0_P#Fm9p8%m55`vi^tOj3BsF0Z|lyPIqsG`vz7)=EXqd_nl z1Pr4=Fd78F`f#)?7!88aR4|$mKuzP(vS2g_MpMCPMi?y%Kp`+%7K{eL5KRRSlHL0< zfo_*z`&n+w7`maQnH`j%AHWC4{9%J*cCf**AFz?3QAMNSG@4XKbIWLf0SbZ9vS2g_ zMpMCPMgWDtXjw2C1f!{7G$VjQV6-e44T8~BFq#oSAuw7N4Ba3|czeQ-7Z`OI^ literal 0 HcmV?d00001 diff --git a/test/java/renderer/ui/test_Screencapture_Match.png b/test/java/renderer/ui/test_Screencapture_Match.png new file mode 100644 index 0000000000000000000000000000000000000000..1055fc38ea50ad24dc6c0fe1b5b711ab15d1ff21 GIT binary patch literal 28071 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