diff --git a/src/com/badlogic/gdx/backends/lwjgl3/Lwjgl3Application.java b/src/com/badlogic/gdx/backends/lwjgl3/Lwjgl3Application.java new file mode 100644 index 000000000..377991265 --- /dev/null +++ b/src/com/badlogic/gdx/backends/lwjgl3/Lwjgl3Application.java @@ -0,0 +1,676 @@ +/******************************************************************************* + * Copyright 2011 See AUTHORS file. + * + * 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 com.badlogic.gdx.backends.lwjgl3; + +import java.io.File; +import java.io.PrintStream; +import java.lang.reflect.Method; +import java.nio.IntBuffer; + +import com.badlogic.gdx.ApplicationLogger; +import com.badlogic.gdx.backends.lwjgl3.audio.Lwjgl3Audio; +import com.badlogic.gdx.backends.lwjgl3.audio.OpenALLwjgl3Audio; +import com.badlogic.gdx.graphics.glutils.GLVersion; + +import org.lwjgl.BufferUtils; +import org.lwjgl.glfw.GLFW; +import org.lwjgl.glfw.GLFWErrorCallback; +import org.lwjgl.opengl.AMDDebugOutput; +import org.lwjgl.opengl.ARBDebugOutput; +import org.lwjgl.opengl.GL; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL43; +import org.lwjgl.opengl.GLCapabilities; +import org.lwjgl.opengl.GLUtil; +import org.lwjgl.opengl.KHRDebug; +import org.lwjgl.system.Callback; + +import com.badlogic.gdx.Application; +import com.badlogic.gdx.ApplicationListener; +import com.badlogic.gdx.Audio; +import com.badlogic.gdx.Files; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Graphics; +import com.badlogic.gdx.Input; +import com.badlogic.gdx.LifecycleListener; +import com.badlogic.gdx.Net; +import com.badlogic.gdx.Preferences; +import com.badlogic.gdx.backends.lwjgl3.audio.mock.MockAudio; +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.Clipboard; +import com.badlogic.gdx.utils.GdxRuntimeException; +import com.badlogic.gdx.utils.ObjectMap; +import com.badlogic.gdx.utils.SharedLibraryLoader; +import org.lwjgl.system.Configuration; + +/** + * This version of code is based on GDX 1.12.0 and was modified for the Terrarum project. + * + * Created by minjaesong on 2023-11-08. + */ +public class Lwjgl3Application implements Lwjgl3ApplicationBase { + private final Lwjgl3ApplicationConfiguration config; + final Array windows = new Array(); + private volatile Lwjgl3Window currentWindow; + private Lwjgl3Audio audio; + private final Files files; + private final Net net; + private final ObjectMap preferences = new ObjectMap(); + private final Lwjgl3Clipboard clipboard; + private int logLevel = LOG_INFO; + private ApplicationLogger applicationLogger; + private volatile boolean running = true; + private final Array runnables = new Array(); + private final Array executedRunnables = new Array(); + private final Array lifecycleListeners = new Array(); + private static GLFWErrorCallback errorCallback; + private static GLVersion glVersion; + private static Callback glDebugCallback; + private final Sync sync; + + static void initializeGlfw () { + if (errorCallback == null) { + if (SharedLibraryLoader.isMac) loadGlfwAwtMacos(); + Lwjgl3NativesLoader.load(); + errorCallback = GLFWErrorCallback.createPrint(Lwjgl3ApplicationConfiguration.errorStream); + GLFW.glfwSetErrorCallback(errorCallback); + if (SharedLibraryLoader.isMac) GLFW.glfwInitHint(GLFW.GLFW_ANGLE_PLATFORM_TYPE, GLFW.GLFW_ANGLE_PLATFORM_TYPE_METAL); + GLFW.glfwInitHint(GLFW.GLFW_JOYSTICK_HAT_BUTTONS, GLFW.GLFW_FALSE); + if (!GLFW.glfwInit()) { + throw new GdxRuntimeException("Unable to initialize GLFW"); + } + } + } + + static void loadANGLE () { + try { + Class angleLoader = Class.forName("com.badlogic.gdx.backends.lwjgl3.angle.ANGLELoader"); + Method load = angleLoader.getMethod("load"); + load.invoke(angleLoader); + } catch (ClassNotFoundException t) { + return; + } catch (Throwable t) { + throw new GdxRuntimeException("Couldn't load ANGLE.", t); + } + } + + static void postLoadANGLE () { + try { + Class angleLoader = Class.forName("com.badlogic.gdx.backends.lwjgl3.angle.ANGLELoader"); + Method load = angleLoader.getMethod("postGlfwInit"); + load.invoke(angleLoader); + } catch (ClassNotFoundException t) { + return; + } catch (Throwable t) { + throw new GdxRuntimeException("Couldn't load ANGLE.", t); + } + } + + static void loadGlfwAwtMacos () { + try { + Class loader = Class.forName("com.badlogic.gdx.backends.lwjgl3.awt.GlfwAWTLoader"); + Method load = loader.getMethod("load"); + File sharedLib = (File)load.invoke(loader); + Configuration.GLFW_LIBRARY_NAME.set(sharedLib.getAbsolutePath()); + Configuration.GLFW_CHECK_THREAD0.set(false); + } catch (ClassNotFoundException t) { + return; + } catch (Throwable t) { + throw new GdxRuntimeException("Couldn't load GLFW AWT for macOS.", t); + } + } + + public Lwjgl3Application (ApplicationListener listener) { + this(listener, new Lwjgl3ApplicationConfiguration()); + } + + public Lwjgl3Application (ApplicationListener listener, Lwjgl3ApplicationConfiguration config) { + if (config.glEmulation == Lwjgl3ApplicationConfiguration.GLEmulation.ANGLE_GLES20) loadANGLE(); + initializeGlfw(); + setApplicationLogger(new Lwjgl3ApplicationLogger()); + + this.config = config = Lwjgl3ApplicationConfiguration.copy(config); + if (config.title == null) config.title = listener.getClass().getSimpleName(); + + Gdx.app = this; + if (!config.disableAudio) { + try { + this.audio = createAudio(config); + } catch (Throwable t) { + log("Lwjgl3Application", "Couldn't initialize audio, disabling audio", t); + this.audio = new MockAudio(); + } + } else { + this.audio = new MockAudio(); + } + Gdx.audio = audio; + this.files = Gdx.files = createFiles(); + this.net = Gdx.net = new Lwjgl3Net(config); + this.clipboard = new Lwjgl3Clipboard(); + + this.sync = new Sync(); + + Lwjgl3Window window = createWindow(config, listener, 0); + if (config.glEmulation == Lwjgl3ApplicationConfiguration.GLEmulation.ANGLE_GLES20) postLoadANGLE(); + windows.add(window); + try { + loop(); + cleanupWindows(); + } catch (Throwable t) { + if (t instanceof RuntimeException) + throw (RuntimeException)t; + else + throw new GdxRuntimeException(t); + } finally { + cleanup(); + } + } + + protected void loop () { + Array closedWindows = new Array(); + while (running && windows.size > 0) { +// System.out.println("aaaaaaaaaaaaaaaaaaaaa"); + +// audio.update(); // is handled on net.torvald.terrarum.AudioManager.update(float) + + boolean haveWindowsRendered = false; + closedWindows.clear(); + int targetFramerate = -2; + for (Lwjgl3Window window : windows) { + window.makeCurrent(); + currentWindow = window; + if (targetFramerate == -2) targetFramerate = window.getConfig().foregroundFPS; + synchronized (lifecycleListeners) { + haveWindowsRendered |= window.update(); + } + if (window.shouldClose()) { + closedWindows.add(window); + } + } + GLFW.glfwPollEvents(); + + boolean shouldRequestRendering; + synchronized (runnables) { + shouldRequestRendering = runnables.size > 0; + executedRunnables.clear(); + executedRunnables.addAll(runnables); + runnables.clear(); + } + for (Runnable runnable : executedRunnables) { + runnable.run(); + } + if (shouldRequestRendering) { + // Must follow Runnables execution so changes done by Runnables are reflected + // in the following render. + for (Lwjgl3Window window : windows) { + if (!window.getGraphics().isContinuousRendering()) window.requestRendering(); + } + } + + for (Lwjgl3Window closedWindow : closedWindows) { + if (windows.size == 1) { + // Lifecycle listener methods have to be called before ApplicationListener methods. The + // application will be disposed when _all_ windows have been disposed, which is the case, + // when there is only 1 window left, which is in the process of being disposed. + for (int i = lifecycleListeners.size - 1; i >= 0; i--) { + LifecycleListener l = lifecycleListeners.get(i); + l.pause(); + l.dispose(); + } + lifecycleListeners.clear(); + } + closedWindow.dispose(); + + windows.removeValue(closedWindow, false); + } + + if (!haveWindowsRendered) { + // Sleep a few milliseconds in case no rendering was requested + // with continuous rendering disabled. + try { + Thread.sleep(1000 / config.idleFPS); + } catch (InterruptedException e) { + // ignore + } + } else if (targetFramerate > 0) { + sync.sync(targetFramerate); // sleep as needed to meet the target framerate + } + } + } + + protected void cleanupWindows () { + synchronized (lifecycleListeners) { + for (LifecycleListener lifecycleListener : lifecycleListeners) { + lifecycleListener.pause(); + lifecycleListener.dispose(); + } + } + for (Lwjgl3Window window : windows) { + window.dispose(); + } + windows.clear(); + } + + protected void cleanup () { + Lwjgl3Cursor.disposeSystemCursors(); + audio.dispose(); + errorCallback.free(); + errorCallback = null; + if (glDebugCallback != null) { + glDebugCallback.free(); + glDebugCallback = null; + } + GLFW.glfwTerminate(); + } + + @Override + public ApplicationListener getApplicationListener () { + return currentWindow.getListener(); + } + + @Override + public Graphics getGraphics () { + return currentWindow.getGraphics(); + } + + @Override + public Audio getAudio () { + return audio; + } + + @Override + public Input getInput () { + return currentWindow.getInput(); + } + + @Override + public Files getFiles () { + return files; + } + + @Override + public Net getNet () { + return net; + } + + @Override + public void debug (String tag, String message) { + if (logLevel >= LOG_DEBUG) getApplicationLogger().debug(tag, message); + } + + @Override + public void debug (String tag, String message, Throwable exception) { + if (logLevel >= LOG_DEBUG) getApplicationLogger().debug(tag, message, exception); + } + + @Override + public void log (String tag, String message) { + if (logLevel >= LOG_INFO) getApplicationLogger().log(tag, message); + } + + @Override + public void log (String tag, String message, Throwable exception) { + if (logLevel >= LOG_INFO) getApplicationLogger().log(tag, message, exception); + } + + @Override + public void error (String tag, String message) { + if (logLevel >= LOG_ERROR) getApplicationLogger().error(tag, message); + } + + @Override + public void error (String tag, String message, Throwable exception) { + if (logLevel >= LOG_ERROR) getApplicationLogger().error(tag, message, exception); + } + + @Override + public void setLogLevel (int logLevel) { + this.logLevel = logLevel; + } + + @Override + public int getLogLevel () { + return logLevel; + } + + @Override + public void setApplicationLogger (ApplicationLogger applicationLogger) { + this.applicationLogger = applicationLogger; + } + + @Override + public ApplicationLogger getApplicationLogger () { + return applicationLogger; + } + + @Override + public ApplicationType getType () { + return ApplicationType.Desktop; + } + + @Override + public int getVersion () { + return 0; + } + + @Override + public long getJavaHeap () { + return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + } + + @Override + public long getNativeHeap () { + return getJavaHeap(); + } + + @Override + public Preferences getPreferences (String name) { + if (preferences.containsKey(name)) { + return preferences.get(name); + } else { + Preferences prefs = new Lwjgl3Preferences( + new Lwjgl3FileHandle(new File(config.preferencesDirectory, name), config.preferencesFileType)); + preferences.put(name, prefs); + return prefs; + } + } + + @Override + public Clipboard getClipboard () { + return clipboard; + } + + @Override + public void postRunnable (Runnable runnable) { + synchronized (runnables) { + runnables.add(runnable); + } + } + + @Override + public void exit () { + running = false; + } + + @Override + public void addLifecycleListener (LifecycleListener listener) { + synchronized (lifecycleListeners) { + lifecycleListeners.add(listener); + } + } + + @Override + public void removeLifecycleListener (LifecycleListener listener) { + synchronized (lifecycleListeners) { + lifecycleListeners.removeValue(listener, true); + } + } + + @Override + public Lwjgl3Audio createAudio (Lwjgl3ApplicationConfiguration config) { + return new OpenALLwjgl3Audio(config.audioDeviceSimultaneousSources, config.audioDeviceBufferCount, + config.audioDeviceBufferSize); + } + + @Override + public Lwjgl3Input createInput (Lwjgl3Window window) { + return new DefaultLwjgl3Input(window); + } + + protected Files createFiles () { + return new Lwjgl3Files(); + } + + /** Creates a new {@link Lwjgl3Window} using the provided listener and {@link Lwjgl3WindowConfiguration}. + * + * This function only just instantiates a {@link Lwjgl3Window} and returns immediately. The actual window creation is postponed + * with {@link Application#postRunnable(Runnable)} until after all existing windows are updated. */ + public Lwjgl3Window newWindow (ApplicationListener listener, Lwjgl3WindowConfiguration config) { + Lwjgl3ApplicationConfiguration appConfig = Lwjgl3ApplicationConfiguration.copy(this.config); + appConfig.setWindowConfiguration(config); + if (appConfig.title == null) appConfig.title = listener.getClass().getSimpleName(); + return createWindow(appConfig, listener, windows.get(0).getWindowHandle()); + } + + private Lwjgl3Window createWindow (final Lwjgl3ApplicationConfiguration config, ApplicationListener listener, + final long sharedContext) { + final Lwjgl3Window window = new Lwjgl3Window(listener, config, this); + if (sharedContext == 0) { + // the main window is created immediately + createWindow(window, config, sharedContext); + } else { + // creation of additional windows is deferred to avoid GL context trouble + postRunnable(new Runnable() { + public void run () { + createWindow(window, config, sharedContext); + windows.add(window); + } + }); + } + return window; + } + + void createWindow (Lwjgl3Window window, Lwjgl3ApplicationConfiguration config, long sharedContext) { + long windowHandle = createGlfwWindow(config, sharedContext); + window.create(windowHandle); + window.setVisible(config.initialVisible); + + for (int i = 0; i < 2; i++) { + GL11.glClearColor(config.initialBackgroundColor.r, config.initialBackgroundColor.g, config.initialBackgroundColor.b, + config.initialBackgroundColor.a); + GL11.glClear(GL11.GL_COLOR_BUFFER_BIT); + GLFW.glfwSwapBuffers(windowHandle); + } + } + + static long createGlfwWindow (Lwjgl3ApplicationConfiguration config, long sharedContextWindow) { + GLFW.glfwDefaultWindowHints(); + GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE); + GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, config.windowResizable ? GLFW.GLFW_TRUE : GLFW.GLFW_FALSE); + GLFW.glfwWindowHint(GLFW.GLFW_MAXIMIZED, config.windowMaximized ? GLFW.GLFW_TRUE : GLFW.GLFW_FALSE); + GLFW.glfwWindowHint(GLFW.GLFW_AUTO_ICONIFY, config.autoIconify ? GLFW.GLFW_TRUE : GLFW.GLFW_FALSE); + + GLFW.glfwWindowHint(GLFW.GLFW_RED_BITS, config.r); + GLFW.glfwWindowHint(GLFW.GLFW_GREEN_BITS, config.g); + GLFW.glfwWindowHint(GLFW.GLFW_BLUE_BITS, config.b); + GLFW.glfwWindowHint(GLFW.GLFW_ALPHA_BITS, config.a); + GLFW.glfwWindowHint(GLFW.GLFW_STENCIL_BITS, config.stencil); + GLFW.glfwWindowHint(GLFW.GLFW_DEPTH_BITS, config.depth); + GLFW.glfwWindowHint(GLFW.GLFW_SAMPLES, config.samples); + + if (config.glEmulation == Lwjgl3ApplicationConfiguration.GLEmulation.GL30 + || config.glEmulation == Lwjgl3ApplicationConfiguration.GLEmulation.GL31 + || config.glEmulation == Lwjgl3ApplicationConfiguration.GLEmulation.GL32) { + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, config.gles30ContextMajorVersion); + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, config.gles30ContextMinorVersion); + if (SharedLibraryLoader.isMac) { + // hints mandatory on OS X for GL 3.2+ context creation, but fail on Windows if the + // WGL_ARB_create_context extension is not available + // see: http://www.glfw.org/docs/latest/compat.html + GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GLFW.GLFW_TRUE); + GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE); + } + } else { + if (config.glEmulation == Lwjgl3ApplicationConfiguration.GLEmulation.ANGLE_GLES20) { + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_CREATION_API, GLFW.GLFW_EGL_CONTEXT_API); + GLFW.glfwWindowHint(GLFW.GLFW_CLIENT_API, GLFW.GLFW_OPENGL_ES_API); + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 2); + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 0); + } + } + + if (config.transparentFramebuffer) { + GLFW.glfwWindowHint(GLFW.GLFW_TRANSPARENT_FRAMEBUFFER, GLFW.GLFW_TRUE); + } + + if (config.debug) { + GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_DEBUG_CONTEXT, GLFW.GLFW_TRUE); + } + + long windowHandle = 0; + + if (config.fullscreenMode != null) { + GLFW.glfwWindowHint(GLFW.GLFW_REFRESH_RATE, config.fullscreenMode.refreshRate); + windowHandle = GLFW.glfwCreateWindow(config.fullscreenMode.width, config.fullscreenMode.height, config.title, + config.fullscreenMode.getMonitor(), sharedContextWindow); + } else { + GLFW.glfwWindowHint(GLFW.GLFW_DECORATED, config.windowDecorated ? GLFW.GLFW_TRUE : GLFW.GLFW_FALSE); + windowHandle = GLFW.glfwCreateWindow(config.windowWidth, config.windowHeight, config.title, 0, sharedContextWindow); + } + if (windowHandle == 0) { + throw new GdxRuntimeException("Couldn't create window"); + } + Lwjgl3Window.setSizeLimits(windowHandle, config.windowMinWidth, config.windowMinHeight, config.windowMaxWidth, + config.windowMaxHeight); + if (config.fullscreenMode == null) { + if (config.windowX == -1 && config.windowY == -1) { + int windowWidth = Math.max(config.windowWidth, config.windowMinWidth); + int windowHeight = Math.max(config.windowHeight, config.windowMinHeight); + if (config.windowMaxWidth > -1) windowWidth = Math.min(windowWidth, config.windowMaxWidth); + if (config.windowMaxHeight > -1) windowHeight = Math.min(windowHeight, config.windowMaxHeight); + + long monitorHandle = GLFW.glfwGetPrimaryMonitor(); + if (config.windowMaximized && config.maximizedMonitor != null) { + monitorHandle = config.maximizedMonitor.monitorHandle; + } + + IntBuffer areaXPos = BufferUtils.createIntBuffer(1); + IntBuffer areaYPos = BufferUtils.createIntBuffer(1); + IntBuffer areaWidth = BufferUtils.createIntBuffer(1); + IntBuffer areaHeight = BufferUtils.createIntBuffer(1); + GLFW.glfwGetMonitorWorkarea(monitorHandle, areaXPos, areaYPos, areaWidth, areaHeight); + + GLFW.glfwSetWindowPos(windowHandle, Math.max(0, areaXPos.get(0) + areaWidth.get(0) / 2 - windowWidth / 2), + Math.max(0, areaYPos.get(0) + areaHeight.get(0) / 2 - windowHeight / 2)); + } else { + GLFW.glfwSetWindowPos(windowHandle, config.windowX, config.windowY); + } + + if (config.windowMaximized) { + GLFW.glfwMaximizeWindow(windowHandle); + } + } + if (config.windowIconPaths != null) { + Lwjgl3Window.setIcon(windowHandle, config.windowIconPaths, config.windowIconFileType); + } + GLFW.glfwMakeContextCurrent(windowHandle); + GLFW.glfwSwapInterval(config.vSyncEnabled ? 1 : 0); + if (config.glEmulation == Lwjgl3ApplicationConfiguration.GLEmulation.ANGLE_GLES20) { + try { + Class gles = Class.forName("org.lwjgl.opengles.GLES"); + gles.getMethod("createCapabilities").invoke(gles); + } catch (Throwable e) { + throw new GdxRuntimeException("Couldn't initialize GLES", e); + } + } else { + GL.createCapabilities(); + } + + initiateGL(config.glEmulation == Lwjgl3ApplicationConfiguration.GLEmulation.ANGLE_GLES20); + if (!glVersion.isVersionEqualToOrHigher(2, 0)) + throw new GdxRuntimeException("OpenGL 2.0 or higher with the FBO extension is required. OpenGL version: " + + GL11.glGetString(GL11.GL_VERSION) + "\n" + glVersion.getDebugVersionString()); + + if (config.glEmulation != Lwjgl3ApplicationConfiguration.GLEmulation.ANGLE_GLES20 && !supportsFBO()) { + throw new GdxRuntimeException("OpenGL 2.0 or higher with the FBO extension is required. OpenGL version: " + + GL11.glGetString(GL11.GL_VERSION) + ", FBO extension: false\n" + glVersion.getDebugVersionString()); + } + + if (config.debug) { + glDebugCallback = GLUtil.setupDebugMessageCallback(config.debugStream); + setGLDebugMessageControl(GLDebugMessageSeverity.NOTIFICATION, false); + } + + return windowHandle; + } + + private static void initiateGL (boolean useGLES20) { + if (!useGLES20) { + String versionString = GL11.glGetString(GL11.GL_VERSION); + String vendorString = GL11.glGetString(GL11.GL_VENDOR); + String rendererString = GL11.glGetString(GL11.GL_RENDERER); + glVersion = new GLVersion(Application.ApplicationType.Desktop, versionString, vendorString, rendererString); + } else { + try { + Class gles = Class.forName("org.lwjgl.opengles.GLES20"); + Method getString = gles.getMethod("glGetString", int.class); + String versionString = (String)getString.invoke(gles, GL11.GL_VERSION); + String vendorString = (String)getString.invoke(gles, GL11.GL_VENDOR); + String rendererString = (String)getString.invoke(gles, GL11.GL_RENDERER); + glVersion = new GLVersion(Application.ApplicationType.Desktop, versionString, vendorString, rendererString); + } catch (Throwable e) { + throw new GdxRuntimeException("Couldn't get GLES version string.", e); + } + } + } + + private static boolean supportsFBO () { + // FBO is in core since OpenGL 3.0, see https://www.opengl.org/wiki/Framebuffer_Object + return glVersion.isVersionEqualToOrHigher(3, 0) || GLFW.glfwExtensionSupported("GL_EXT_framebuffer_object") + || GLFW.glfwExtensionSupported("GL_ARB_framebuffer_object"); + } + + public enum GLDebugMessageSeverity { + HIGH(GL43.GL_DEBUG_SEVERITY_HIGH, KHRDebug.GL_DEBUG_SEVERITY_HIGH, ARBDebugOutput.GL_DEBUG_SEVERITY_HIGH_ARB, + AMDDebugOutput.GL_DEBUG_SEVERITY_HIGH_AMD), MEDIUM(GL43.GL_DEBUG_SEVERITY_MEDIUM, KHRDebug.GL_DEBUG_SEVERITY_MEDIUM, + ARBDebugOutput.GL_DEBUG_SEVERITY_MEDIUM_ARB, AMDDebugOutput.GL_DEBUG_SEVERITY_MEDIUM_AMD), LOW( + GL43.GL_DEBUG_SEVERITY_LOW, KHRDebug.GL_DEBUG_SEVERITY_LOW, ARBDebugOutput.GL_DEBUG_SEVERITY_LOW_ARB, + AMDDebugOutput.GL_DEBUG_SEVERITY_LOW_AMD), NOTIFICATION(GL43.GL_DEBUG_SEVERITY_NOTIFICATION, + KHRDebug.GL_DEBUG_SEVERITY_NOTIFICATION, -1, -1); + + final int gl43, khr, arb, amd; + + GLDebugMessageSeverity (int gl43, int khr, int arb, int amd) { + this.gl43 = gl43; + this.khr = khr; + this.arb = arb; + this.amd = amd; + } + } + + /** Enables or disables GL debug messages for the specified severity level. Returns false if the severity level could not be + * set (e.g. the NOTIFICATION level is not supported by the ARB and AMD extensions). + * + * See {@link Lwjgl3ApplicationConfiguration#enableGLDebugOutput(boolean, PrintStream)} */ + public static boolean setGLDebugMessageControl (GLDebugMessageSeverity severity, boolean enabled) { + GLCapabilities caps = GL.getCapabilities(); + final int GL_DONT_CARE = 0x1100; // not defined anywhere yet + + if (caps.OpenGL43) { + GL43.glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, severity.gl43, (IntBuffer)null, enabled); + return true; + } + + if (caps.GL_KHR_debug) { + KHRDebug.glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, severity.khr, (IntBuffer)null, enabled); + return true; + } + + if (caps.GL_ARB_debug_output && severity.arb != -1) { + ARBDebugOutput.glDebugMessageControlARB(GL_DONT_CARE, GL_DONT_CARE, severity.arb, (IntBuffer)null, enabled); + return true; + } + + if (caps.GL_AMD_debug_output && severity.amd != -1) { + AMDDebugOutput.glDebugMessageEnableAMD(GL_DONT_CARE, severity.amd, (IntBuffer)null, enabled); + return true; + } + + return false; + } + +} diff --git a/src/net/torvald/terrarum/App.java b/src/net/torvald/terrarum/App.java index a67007669..6d651fe7a 100644 --- a/src/net/torvald/terrarum/App.java +++ b/src/net/torvald/terrarum/App.java @@ -74,6 +74,11 @@ public class App implements ApplicationListener { (VERSION_TAG.isBlank() ? "" : "-"+VERSION_TAG) + (snap == null ? "" : (" (" + snap + ")")); } + public static final String getVERSION_STRING_WITHOUT_SNAPSHOT() { + return String.format("%d.%d.%d", VERSION_RAW >>> 48, (VERSION_RAW & 0xffff000000L) >>> 24, VERSION_RAW & 0xffffffL) + + (VERSION_TAG.isBlank() ? "" : "-"+VERSION_TAG); + } + /** * when FALSE, some assertion and print code will not execute */ @@ -1182,7 +1187,6 @@ public class App implements ApplicationListener { AudioManager.INSTANCE.getMasterVolume(); audioManagerThread = new Thread(new AudioManagerRunnable(), "TerrarumAudioManager"); - audioManagerThread.setPriority(2); audioManagerThread.start(); Terrarum.initialise(); diff --git a/src/net/torvald/terrarum/AudioManager.kt b/src/net/torvald/terrarum/AudioManager.kt index 5c95e8784..c29e9f212 100644 --- a/src/net/torvald/terrarum/AudioManager.kt +++ b/src/net/torvald/terrarum/AudioManager.kt @@ -1,12 +1,17 @@ package net.torvald.terrarum +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.backends.lwjgl3.audio.Lwjgl3Audio import net.torvald.terrarum.App.printdbg import net.torvald.terrarum.modulebasegame.MusicContainer /** + * Any audio reference fed into this manager will get lost; you must manually store and dispose of them on your own. + * * Created by minjaesong on 2023-11-07. */ object AudioManager { + const val DEFAULT_FADEOUT_LEN = 2.4f /** Returns a master volume */ val masterVolume: Float @@ -27,25 +32,28 @@ object AudioManager { private var nextMusic: MusicContainer? = null private var fadeAkku = 0f - private var fadeLength = 1f + private var fadeLength = DEFAULT_FADEOUT_LEN private var fadeoutFired = false private var fadeinFired = false fun update(delta: Float) { + (Gdx.audio as? Lwjgl3Audio)?.update() + + if (fadeoutFired) { fadeAkku += delta - currentMusic?.gdxMusic?.volume = musicVolume * (1f - (fadeAkku / fadeLength)).coerceIn(0f, 1f) + currentMusic?.gdxMusic?.volume = (musicVolume * (1f - (fadeAkku / fadeLength))).coerceIn(0f, 1f) - printdbg(this, "Fadeout fired - akku: $fadeAkku; volume: ${currentMusic?.gdxMusic?.volume}") +// printdbg(this, "Fadeout fired - akku: $fadeAkku; volume: ${currentMusic?.gdxMusic?.volume}") if (fadeAkku >= fadeLength) { fadeoutFired = false currentMusic?.gdxMusic?.volume = 0f - currentMusic?.gdxMusic?.pause() +// currentMusic?.gdxMusic?.pause() currentMusic = null - printdbg(this, "Fadeout end") +// printdbg(this, "Fadeout end") } } // process fadein request @@ -53,24 +61,24 @@ object AudioManager { fadeAkku += delta currentMusic?.gdxMusic?.volume = (musicVolume * (fadeAkku / fadeLength)).coerceIn(0f, 1f) - printdbg(this, "Fadein fired - akku: $fadeAkku; volume: ${currentMusic?.gdxMusic?.volume}") +// printdbg(this, "Fadein fired - akku: $fadeAkku; volume: ${currentMusic?.gdxMusic?.volume}") if (currentMusic?.gdxMusic?.isPlaying == false) { currentMusic?.gdxMusic?.play() - printdbg(this, "Fadein starting music ${currentMusic?.name}") +// printdbg(this, "Fadein starting music ${currentMusic?.name}") } if (fadeAkku >= fadeLength) { currentMusic?.gdxMusic?.volume = musicVolume fadeinFired = false - printdbg(this, "Fadein end") +// printdbg(this, "Fadein end") } } if (currentMusic?.gdxMusic?.isPlaying != true && nextMusic != null) { - printdbg(this, "Playing next music: ${nextMusic!!.name}") +// printdbg(this, "Playing next music: ${nextMusic!!.name}") currentMusic = nextMusic nextMusic = null currentMusic!!.gdxMusic.volume = musicVolume @@ -80,13 +88,13 @@ object AudioManager { fun startMusic(song: MusicContainer) { if (currentMusic?.gdxMusic?.isPlaying == true) { - requestFadeOut(1f) + requestFadeOut(DEFAULT_FADEOUT_LEN) } nextMusic = song } fun stopMusic() { - requestFadeOut(1f) + requestFadeOut(DEFAULT_FADEOUT_LEN) } fun requestFadeOut(length: Float) { diff --git a/src/net/torvald/terrarum/AudioManagerRunnable.kt b/src/net/torvald/terrarum/AudioManagerRunnable.kt index 11a2f97ab..f881172ea 100644 --- a/src/net/torvald/terrarum/AudioManagerRunnable.kt +++ b/src/net/torvald/terrarum/AudioManagerRunnable.kt @@ -18,7 +18,8 @@ class AudioManagerRunnable : Runnable { dT = (T - oldT) / 1000000000f oldT = T; AudioManager.update(dT) - Thread.sleep(20L) +// println("AudioManagerRunnable dT = ${dT * 1000f} ms") + Thread.sleep(30L) } catch (e: InterruptedException) { break diff --git a/src/net/torvald/terrarum/CheckUpdate.kt b/src/net/torvald/terrarum/CheckUpdate.kt index f6aabc37b..86eba7604 100644 --- a/src/net/torvald/terrarum/CheckUpdate.kt +++ b/src/net/torvald/terrarum/CheckUpdate.kt @@ -17,7 +17,7 @@ import java.net.http.HttpResponse */ object CheckUpdate { - private val versionNumFull = App.getVERSION_STRING() + private val versionNumFull = App.getVERSION_STRING_WITHOUT_SNAPSHOT() private val versionNumOnly = String.format( "%d.%d.%d", App.VERSION_RAW ushr 48, diff --git a/src/net/torvald/terrarum/SavegameCollection.kt b/src/net/torvald/terrarum/SavegameCollection.kt index a070cd48e..f0effda41 100644 --- a/src/net/torvald/terrarum/SavegameCollection.kt +++ b/src/net/torvald/terrarum/SavegameCollection.kt @@ -40,7 +40,7 @@ class SavegameCollection(files0: List, prefiltered: Boolean) { val manualSaves = files.filter { !it.diskFile.extension.matches(Regex("[a-z]")) } init { - printdbg(this, "Rebuilding skimmers (${files.size})") +// printdbg(this, "Rebuilding skimmers (${files.size})") // files.forEach { it.rebuild() } } diff --git a/src/net/torvald/terrarum/Terrarum.kt b/src/net/torvald/terrarum/Terrarum.kt index d96ed6cec..b53213499 100644 --- a/src/net/torvald/terrarum/Terrarum.kt +++ b/src/net/torvald/terrarum/Terrarum.kt @@ -794,7 +794,7 @@ fun AppUpdateListOfSavegames() { // create list of worlds - printdbg("ListSavegames", "Listing saved worlds...") +// printdbg("ListSavegames", "Listing saved worlds...") val worldsDirLs = File(worldsDir).listFiles().filter { !it.isDirectory && !it.name.contains('.') }.mapNotNull { file -> try { DiskSkimmer(file, true) @@ -820,16 +820,16 @@ fun AppUpdateListOfSavegames() { } filteringResults.forEachIndexed { index, list -> val it = list.first() - printdbg("ListSavegames", " ${index+1}.\t${it.diskFile.absolutePath}") +// printdbg("ListSavegames", " ${index+1}.\t${it.diskFile.absolutePath}") - printdbg("ListSavegames", " collecting...") +// printdbg("ListSavegames", " collecting...") val collection = SavegameCollection.collectFromBaseFilename(list, it.diskFile.name) - printdbg("ListSavegames", " disk rebuilding...") +// printdbg("ListSavegames", " disk rebuilding...") collection.rebuildLoadable() - printdbg("ListSavegames", " get UUID...") +// printdbg("ListSavegames", " get UUID...") val worldUUID = collection.getUUID() - printdbg("ListSavegames", " registration...") +// printdbg("ListSavegames", " registration...") // if multiple valid savegames with same UUID exist, only the most recent one is retained if (!App.savegameWorlds.contains(worldUUID)) { App.savegameWorlds[worldUUID] = collection @@ -840,7 +840,7 @@ fun AppUpdateListOfSavegames() { // create list of players - printdbg("ListSavegames", "Listing saved players...") +// printdbg("ListSavegames", "Listing saved players...") val playersDirLs = File(playersDir).listFiles().filter { !it.isDirectory && !it.name.contains('.') }.mapNotNull { file -> try { DiskSkimmer(file, true) @@ -866,16 +866,16 @@ fun AppUpdateListOfSavegames() { } filteringResults2.forEachIndexed { index, list -> val it = list.first() - printdbg("ListSavegames", " ${index+1}.\t${it.diskFile.absolutePath}") +// printdbg("ListSavegames", " ${index+1}.\t${it.diskFile.absolutePath}") - printdbg("ListSavegames", " collecting...") +// printdbg("ListSavegames", " collecting...") val collection = SavegameCollection.collectFromBaseFilename(list, it.diskFile.name) - printdbg("ListSavegames", " disk rebuilding...") +// printdbg("ListSavegames", " disk rebuilding...") collection.rebuildLoadable() - printdbg("ListSavegames", " get UUID...") +// printdbg("ListSavegames", " get UUID...") val playerUUID = collection.getUUID() - printdbg("ListSavegames", " registration...") +// printdbg("ListSavegames", " registration...") // if multiple valid savegames with same UUID exist, only the most recent one is retained if (!App.savegamePlayers.contains(playerUUID)) { App.savegamePlayers[playerUUID] = collection diff --git a/src/net/torvald/terrarum/modulebasegame/TerrarumMusicGovernor.kt b/src/net/torvald/terrarum/modulebasegame/TerrarumMusicGovernor.kt index 0137f35c1..421966a01 100644 --- a/src/net/torvald/terrarum/modulebasegame/TerrarumMusicGovernor.kt +++ b/src/net/torvald/terrarum/modulebasegame/TerrarumMusicGovernor.kt @@ -25,7 +25,11 @@ class TerrarumMusicGovernor : MusicGovernor() { MusicContainer( it.nameWithoutExtension.replace('_', ' ').split(" ").map { it.capitalize() }.joinToString(" "), it, - Gdx.audio.newMusic(Gdx.files.absolute(it.absolutePath)) + Gdx.audio.newMusic(Gdx.files.absolute(it.absolutePath)).also { + it.setOnCompletionListener { + stopMusic() + } + } ) } catch (e: GdxRuntimeException) { @@ -98,9 +102,7 @@ class TerrarumMusicGovernor : MusicGovernor() { } } STATE_PLAYING -> { - if (AudioManager.currentMusic?.gdxMusic?.isPlaying == false) { -// stopMusic() - } + // stopMusic() will be called when the music finishes; it's on the setOnCompletionListener } STATE_INTERMISSION -> { intermissionAkku += delta