From 52864cb701e59a1d847fd5586245519eb5e3b3bc Mon Sep 17 00:00:00 2001 From: Elizabeth Hunt Date: Fri, 23 Jan 2026 20:22:30 -0800 Subject: Move code over --- .../abstractionengine/lwjgl3/Lwjgl3Launcher.java | 63 ++++++ .../abstractionengine/lwjgl3/StartupHelper.java | 218 +++++++++++++++++++++ 2 files changed, 281 insertions(+) create mode 100644 lwjgl3/src/main/java/coffee/liz/abstractionengine/lwjgl3/Lwjgl3Launcher.java create mode 100644 lwjgl3/src/main/java/coffee/liz/abstractionengine/lwjgl3/StartupHelper.java (limited to 'lwjgl3/src/main/java/coffee') diff --git a/lwjgl3/src/main/java/coffee/liz/abstractionengine/lwjgl3/Lwjgl3Launcher.java b/lwjgl3/src/main/java/coffee/liz/abstractionengine/lwjgl3/Lwjgl3Launcher.java new file mode 100644 index 0000000..1c67c62 --- /dev/null +++ b/lwjgl3/src/main/java/coffee/liz/abstractionengine/lwjgl3/Lwjgl3Launcher.java @@ -0,0 +1,63 @@ +package coffee.liz.abstractionengine.lwjgl3; + +import coffee.liz.abstractionengine.app.AbstractionEngineGame; + +import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application; +import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration; +import com.badlogic.gdx.graphics.glutils.HdpiMode; + +/** Launches the desktop (LWJGL3) application. */ +public class Lwjgl3Launcher { + public static void main(final String[] args) { + if (StartupHelper.startNewJvmIfRequired()) + return; // This handles macOS support and helps on Windows. + createApplication(); + } + + private static Lwjgl3Application createApplication() { + return new Lwjgl3Application(new AbstractionEngineGame(), getDefaultConfiguration()); + } + + private static Lwjgl3ApplicationConfiguration getDefaultConfiguration() { + final Lwjgl3ApplicationConfiguration configuration = new Lwjgl3ApplicationConfiguration(); + configuration.setTitle("The Abstraction Engine"); + //// Vsync limits the frames per second to what your hardware can display, and + //// helps eliminate + //// screen tearing. This setting doesn't always work on Linux, so the line + //// after is a safeguard. + configuration.useVsync(true); + //// Limits FPS to the refresh rate of the currently active monitor, plus 1 to + //// try to match fractional + //// refresh rates. The Vsync setting above should limit the actual FPS to match + //// the monitor. + configuration.setForegroundFPS(Lwjgl3ApplicationConfiguration.getDisplayMode().refreshRate + 1); + //// If you remove the above line and set Vsync to false, you can get unlimited + //// FPS, which can be + //// useful for testing performance, but can also be very stressful to some + //// hardware. + //// You may also need to configure GPU drivers to fully disable Vsync; this can + //// cause screen tearing. + + configuration.setHdpiMode(HdpiMode.Logical); + + configuration.setWindowedMode(1024, 1024); + configuration.setDecorated(true); + //// You can change these files; they are in lwjgl3/src/main/resources/ . + //// They can also be loaded from the root of assets/ . + configuration.setWindowIcon("libgdx128.png", "libgdx64.png", "libgdx32.png", "libgdx16.png"); + + //// This should improve compatibility with Windows machines with buggy OpenGL + //// drivers, Macs + //// with Apple Silicon that have to emulate compatibility with OpenGL anyway, + //// and more. + //// This uses the dependency `com.badlogicgames.gdx:gdx-lwjgl3-angle` to + //// function. + //// You can choose to remove the following line and the mentioned dependency if + //// you want; they + //// are not intended for games that use GL30 (which is compatibility with + //// OpenGL ES 3.0). + configuration.setOpenGLEmulation(Lwjgl3ApplicationConfiguration.GLEmulation.ANGLE_GLES20, 0, 0); + + return configuration; + } +} diff --git a/lwjgl3/src/main/java/coffee/liz/abstractionengine/lwjgl3/StartupHelper.java b/lwjgl3/src/main/java/coffee/liz/abstractionengine/lwjgl3/StartupHelper.java new file mode 100644 index 0000000..21520f9 --- /dev/null +++ b/lwjgl3/src/main/java/coffee/liz/abstractionengine/lwjgl3/StartupHelper.java @@ -0,0 +1,218 @@ +/* + * Copyright 2020 damios + * + * 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: + * https://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. + */ +//Note, the above license and copyright applies to this file only. + +package coffee.liz.abstractionengine.lwjgl3; + +import com.badlogic.gdx.Version; +import com.badlogic.gdx.backends.lwjgl3.Lwjgl3NativesLoader; +import org.lwjgl.system.macosx.LibC; +import org.lwjgl.system.macosx.ObjCRuntime; + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStreamReader; +import java.lang.management.ManagementFactory; +import java.util.ArrayList; + +import static org.lwjgl.system.JNI.invokePPP; +import static org.lwjgl.system.JNI.invokePPZ; +import static org.lwjgl.system.macosx.ObjCRuntime.objc_getClass; +import static org.lwjgl.system.macosx.ObjCRuntime.sel_getUid; + +/** + * Adds some utilities to ensure that the JVM was started with the + * {@code -XstartOnFirstThread} argument, which is required on macOS for LWJGL 3 + * to function. Also helps on Windows when users have names with characters from + * outside the Latin alphabet, a common cause of startup crashes.
+ * Based + * on this java-gaming.org post by kappa + * + * @author damios + */ +public class StartupHelper { + + private static final String JVM_RESTARTED_ARG = "jvmIsRestarted"; + + private StartupHelper() { + throw new UnsupportedOperationException(); + } + + /** + * Starts a new JVM if the application was started on macOS without the + * {@code -XstartOnFirstThread} argument. This also includes some code for + * Windows, for the case where the user's home directory includes certain + * non-Latin-alphabet characters (without this code, most LWJGL3 apps fail + * immediately for those users). Returns whether a new JVM was started and thus + * no code should be executed. + *

+ * Usage: + * + *

+	 * 
+	 * public static void main(String... args) {
+	 * 	if (StartupHelper.startNewJvmIfRequired(true)) return; // This handles macOS support and helps on Windows.
+	 * 	// after this is the actual main method code
+	 * }
+	 * 
+	 * 
+ * + * @param redirectOutput + * whether the output of the new JVM should be rerouted to the old + * JVM, so it can be accessed in the same place; keeps the old JVM + * running if enabled + * @return whether a new JVM was started and thus no code should be executed in + * this one + */ + public static boolean startNewJvmIfRequired(boolean redirectOutput) { + String osName = System.getProperty("os.name").toLowerCase(java.util.Locale.ROOT); + if (!osName.contains("mac")) { + if (osName.contains("windows")) { + // Here, we are trying to work around an issue with how LWJGL3 loads its + // extracted .dll files. + // By default, LWJGL3 extracts to the directory specified by "java.io.tmpdir", + // which is usually the user's home. + // If the user's name has non-ASCII (or some non-alphanumeric) characters in it, + // that would fail. + // By extracting to the relevant "ProgramData" folder, which is usually + // "C:\ProgramData", we avoid this. + // We also temporarily change the "user.name" property to one without any chars + // that would be invalid. + // We revert our changes immediately after loading LWJGL3 natives. + String programData = System.getenv("ProgramData"); + if (programData == null) + programData = "C:\\Temp\\"; // if ProgramData isn't set, try some fallback. + String prevTmpDir = System.getProperty("java.io.tmpdir", programData); + String prevUser = System.getProperty("user.name", "libGDX_User"); + System.setProperty("java.io.tmpdir", programData + "/libGDX-temp"); + System.setProperty("user.name", + ("User_" + prevUser.hashCode() + "_GDX" + Version.VERSION).replace('.', '_')); + Lwjgl3NativesLoader.load(); + System.setProperty("java.io.tmpdir", prevTmpDir); + System.setProperty("user.name", prevUser); + } + return false; + } + + // There is no need for -XstartOnFirstThread on Graal native image + if (!System.getProperty("org.graalvm.nativeimage.imagecode", "").isEmpty()) { + return false; + } + + // Checks if we are already on the main thread, such as from running via + // Construo. + long objc_msgSend = ObjCRuntime.getLibrary().getFunctionAddress("objc_msgSend"); + long NSThread = objc_getClass("NSThread"); + long currentThread = invokePPP(NSThread, sel_getUid("currentThread"), objc_msgSend); + boolean isMainThread = invokePPZ(currentThread, sel_getUid("isMainThread"), objc_msgSend); + if (isMainThread) + return false; + + long pid = LibC.getpid(); + + // check whether -XstartOnFirstThread is enabled + if ("1".equals(System.getenv("JAVA_STARTED_ON_FIRST_THREAD_" + pid))) { + return false; + } + + // check whether the JVM was previously restarted + // avoids looping, but most certainly leads to a crash + if ("true".equals(System.getProperty(JVM_RESTARTED_ARG))) { + System.err.println( + "There was a problem evaluating whether the JVM was started with the -XstartOnFirstThread argument."); + return false; + } + + // Restart the JVM with -XstartOnFirstThread + ArrayList jvmArgs = new ArrayList<>(); + String separator = System.getProperty("file.separator", "/"); + // The following line is used assuming you target Java 8, the minimum for + // LWJGL3. + String javaExecPath = System.getProperty("java.home") + separator + "bin" + separator + "java"; + // If targeting Java 9 or higher, you could use the following instead of the + // above line: + // String javaExecPath = ProcessHandle.current().info().command().orElseThrow(); + + if (!(new File(javaExecPath)).exists()) { + System.err.println( + "A Java installation could not be found. If you are distributing this app with a bundled JRE, be sure to set the -XstartOnFirstThread argument manually!"); + return false; + } + + jvmArgs.add(javaExecPath); + jvmArgs.add("-XstartOnFirstThread"); + jvmArgs.add("-D" + JVM_RESTARTED_ARG + "=true"); + jvmArgs.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments()); + jvmArgs.add("-cp"); + jvmArgs.add(System.getProperty("java.class.path")); + String mainClass = System.getenv("JAVA_MAIN_CLASS_" + pid); + if (mainClass == null) { + StackTraceElement[] trace = Thread.currentThread().getStackTrace(); + if (trace.length > 0) { + mainClass = trace[trace.length - 1].getClassName(); + } else { + System.err.println("The main class could not be determined."); + return false; + } + } + jvmArgs.add(mainClass); + + try { + if (!redirectOutput) { + ProcessBuilder processBuilder = new ProcessBuilder(jvmArgs); + processBuilder.start(); + } else { + Process process = (new ProcessBuilder(jvmArgs)).redirectErrorStream(true).start(); + BufferedReader processOutput = new BufferedReader(new InputStreamReader(process.getInputStream())); + String line; + + while ((line = processOutput.readLine()) != null) { + System.out.println(line); + } + + process.waitFor(); + } + } catch (Exception e) { + System.err.println("There was a problem restarting the JVM"); + e.printStackTrace(); + } + + return true; + } + + /** + * Starts a new JVM if the application was started on macOS without the + * {@code -XstartOnFirstThread} argument. Returns whether a new JVM was started + * and thus no code should be executed. Redirects the output of the new JVM to + * the old one. + *

+ * Usage: + * + *

+	 * public static void main(String... args) {
+	 * 	if (StartupHelper.startNewJvmIfRequired())
+	 * 		return; // This handles macOS support and helps on Windows.
+	 * 	// the actual main method code
+	 * }
+	 * 
+ * + * @return whether a new JVM was started and thus no code should be executed in + * this one + */ + public static boolean startNewJvmIfRequired() { + return startNewJvmIfRequired(true); + } +} \ No newline at end of file -- cgit v1.2.3-70-g09d2