diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index c595b00..216df05 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,3 +1,3 @@ wrapperVersion=3.3.4 distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.14/apache-maven-3.9.14-bin.zip +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip diff --git a/README.md b/README.md index 0519262..59dcd0b 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ Because some errors in user programs can cause the JVM to crash without a meanin 1. Download the `lwjglx-debug-1.0.0.jar` file from [https://www.lwjgl.org/browse/addons/lwjglx-debug](https://www.lwjgl.org/browse/addons/lwjglx-debug) or build this Maven project via the instructions in the 'Build' section below 2. Copy the `lwjglx-debug-1.0.0.jar` to any directory (henceforth called ``) -3. Start your LWJGL3 application with the **JVM/VM argument** (_not_ program argument) `-javaagent:/lwjglx-debug-1.0.0.jar` - 1. when using the command line, it should look like: `java -javaagent:/lwjglx-debug-1.0.0.jar -cp all/the/jars your.main.Class` +3. Start your LWJGL3 application with the **JVM/VM argument** (_not_ program argument) `-javaagent:/lwjglx-debug-1.0.4.jar` + 1. when using the command line, it should look like: `java -javaagent:/lwjglx-debug-1.0.4.jar -cp all/the/jars your.main.Class` 2. when using Eclipse, right-click your class with the `main()` method, goto 'Run As > Run Configurations...' and on the 'Arguments' tab inside the 'VM Arguments:' field enter `-javaagent:/lwjglx-debug-1.0.0.jar` # Configuration @@ -29,13 +29,13 @@ The following configuration properties are available to configure the library: Examples: -* `java -javaagent:lwjglx-debug-1.0.0.jar=t ...` (generate a trace on stderr) -* `java -javaagent:lwjglx-debug-1.0.0.jar=t;o=trace.log` (generate a trace written to file `trace.log`) -* `java -javaagent:lwjglx-debug-1.0.0.jar=t;o=trace.log.zip` (generate a zip archive containing a single `trace.log` file) -* `java -javaagent:lwjglx-debug-1.0.0.jar=tn;o=trace.log` (generate a trace written to file `trace.log` and do not throw on GL errors) -* `java -javaagent:lwjglx-debug-1.0.0.jar=t;e=*GL20*,*GL11.glVertex3f` (generate a trace on stderr and exclude all methods from any class having `GL20` in its name, as well as exclude `glVertex3f` from any class ending with `GL11`) +* `java -javaagent:lwjglx-debug-1.0.4.jar=t ...` (generate a trace on stderr) +* `java -javaagent:lwjglx-debug-1.0.4.jar=t;o=trace.log` (generate a trace written to file `trace.log`) +* `java -javaagent:lwjglx-debug-1.0.4.jar=t;o=trace.log.zip` (generate a zip archive containing a single `trace.log` file) +* `java -javaagent:lwjglx-debug-1.0.4.jar=tn;o=trace.log` (generate a trace written to file `trace.log` and do not throw on GL errors) +* `java -javaagent:lwjglx-debug-1.0.4.jar=t;e=*GL20*,*GL11.glVertex3f` (generate a trace on stderr and exclude all methods from any class having `GL20` in its name, as well as exclude `glVertex3f` from any class ending with `GL11`) # Build 1. `./mvnw package` -2. see target/lwjglx-debug-1.0.0.jar +2. see target/lwjglx-debug-1.0.4.jar diff --git a/pom.xml b/pom.xml index 1c8efaa..3a8c512 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 org.lwjglx lwjglx-debug - 1.0.3 + 1.0.4 LWJGLX/debug Debug LWJGL3 OpenGL applications 2017 @@ -414,6 +414,12 @@ ${lwjgl.version} provided + + org.lwjgl + lwjgl-sdl + ${lwjgl.version} + provided + org.lwjgl lwjgl @@ -435,5 +441,12 @@ natives-${platform} provided + + org.lwjgl + lwjgl-sdl + ${lwjgl.version} + natives-${platform} + provided + diff --git a/src/org/lwjglx/debug/InterceptClassGenerator.java b/src/org/lwjglx/debug/InterceptClassGenerator.java index 5b61d57..d0aed6a 100644 --- a/src/org/lwjglx/debug/InterceptClassGenerator.java +++ b/src/org/lwjglx/debug/InterceptClassGenerator.java @@ -22,27 +22,31 @@ of this software and associated documentation files (the "Software"), to deal */ package org.lwjglx.debug; -import static org.lwjglx.debug.Log.*; -import static org.lwjglx.debug.Properties.*; +import org.lwjglx.debug.ClassMetadata.MethodInfo; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.util.TraceClassVisitor; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; -import org.lwjglx.debug.ClassMetadata.MethodInfo; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; -import org.objectweb.asm.util.TraceClassVisitor; +import static org.lwjglx.debug.Log.debug; +import static org.lwjglx.debug.Properties.DEBUG; +import static org.lwjglx.debug.Properties.TRACE; +import static org.lwjglx.debug.Properties.VALIDATE; class InterceptedCall { private static final AtomicInteger counter = new AtomicInteger(); @@ -130,6 +134,20 @@ class InterceptClassGenerator implements Opcodes { private static final Map> declaredMethods = new ConcurrentHashMap<>(); + private static final Set GLFW_MAIN_THREAD_METHODS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + "glfwInit", "glfwTerminate", "glfwCreateWindow", "glfwDefaultWindowHints", "glfwDestroyWindow", "glfwFocusWindow", + "glfwGetFramebufferSize", "glfwGetWindowAttrib", "glfwGetWindowFrameSize", "glfwGetWindowMonitor", "glfwGetWindowPos", + "glfwGetWindowSize", "glfwHideWindow", "glfwIconifyWindow", "glfwMaximizeWindow", "glfwPollEvents", "glfwRestoreWindow", + "glfwSetFramebufferSizeCallback", "glfwSetWindowAspectRatio", "glfwSetWindowCloseCallback", "glfwSetWindowFocusCallback", + "glfwSetWindowIcon", "glfwSetWindowIconifyCallback", "glfwSetWindowMonitor", "glfwSetWindowPos", "glfwSetWindowPosCallback", + "glfwSetWindowRefreshCallback", "glfwSetWindowSize", "glfwSetWindowSizeCallback", "glfwSetWindowSizeLimits", + "glfwSetWindowTitle", "glfwShowWindow", "glfwWaitEvents", "glfwWaitEventsTimeout", "glfwWindowHint" + ))); + + private static final Set SDL_THREAD_SAFE_METHODS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + "SDL_GL_MakeCurrent", "SDL_GL_GetCurrentContext", "SDL_GL_SwapWindow", "SDL_GL_DestroyContext" + ))); + private static boolean isGLcall(InterceptedCall call) { return (call.name.startsWith("gl") || call.name.startsWith("ngl")) && call.resolvedReceiverInternalName.startsWith("org/lwjgl/opengl/"); } @@ -160,13 +178,10 @@ private static String glCall(InterceptedCall call) { private static boolean isMainThreadMethod(InterceptedCall call) { if (call.resolvedReceiverInternalName.equals("org/lwjgl/glfw/GLFW")) { - return Arrays - .asList("glfwInit", "glfwTerminate", "glfwCreateWindow", "glfwDefaultWindowHints", "glfwDestroyWindow", "glfwFocusWindow", "glfwGetFramebufferSize", "glfwGetWindowAttrib", - "glfwGetWindowFrameSize", "glfwGetWindowMonitor", "glfwGetWindowPos", "glfwGetWindowSize", "glfwHideWindow", "glfwIconifyWindow", "glfwMaximizeWindow", "glfwPollEvents", - "glfwRestoreWindow", "glfwSetFramebufferSizeCallback", "glfwSetWindowAspectRatio", "glfwSetWindowCloseCallback", "glfwSetWindowFocusCallback", "glfwSetWindowIcon", - "glfwSetWindowIconifyCallback", "glfwSetWindowMonitor", "glfwSetWindowPos", "glfwSetWindowPosCallback", "glfwSetWindowRefreshCallback", "glfwSetWindowSize", - "glfwSetWindowSizeCallback", "glfwSetWindowSizeLimits", "glfwSetWindowTitle", "glfwShowWindow", "glfwWaitEvents", "glfwWaitEventsTimeout", "glfwWindowHint") - .contains(call.name); + return GLFW_MAIN_THREAD_METHODS.contains(call.name); + } + if (call.resolvedReceiverInternalName.startsWith("org/lwjgl/sdl/")) { + return call.name.startsWith("SDL_") && !SDL_THREAD_SAFE_METHODS.contains(call.name); } return false; } @@ -178,6 +193,13 @@ private static boolean requiresGlfwInit(InterceptedCall call) { return false; } + private static boolean requiresSdlInit(InterceptedCall call) { + if (call.resolvedReceiverInternalName.startsWith("org/lwjgl/sdl/")) { + return call.name.startsWith("SDL_") && !call.name.equals("SDL_Init") && !call.name.equals("SDL_InitSubSystem") && !call.name.equals("SDL_SetMemoryFunctions"); + } + return false; + } + private static void checkFunctionSupported(MethodVisitor mv, String name) { mv.visitFieldInsn(GETFIELD, "org/lwjgl/opengl/GLCapabilities", name, "J"); mv.visitLdcInsn(name); @@ -249,6 +271,11 @@ public static Class generate(ClassLoader classLoader, String proxyInternalNam mv.visitLdcInsn(call.name); mv.visitMethodInsn(INVOKESTATIC, RT_InternalName, "checkGlfwInitialized", "(Ljava/lang/String;)V", false); } + /* and whether it was an SDL method that requires SDL_Init() to have been called */ + if (requiresSdlInit(call)) { + mv.visitLdcInsn(call.name); + mv.visitMethodInsn(INVOKESTATIC, RT_InternalName, "checkSdlInitialized", "(Ljava/lang/String;)V", false); + } } /* Validate buffer arguments and also load all arguments onto stack */ Type[] paramTypes = Type.getArgumentTypes(call.desc); @@ -458,6 +485,12 @@ private static void generateDefaultTraceBefore(InterceptedCall call, MethodVisit } else if ("GLFWmonitor *".equals(nativeType)) { mv.visitVarInsn(paramType.getOpcode(ILOAD), var); mv.visitMethodInsn(INVOKESTATIC, RT_InternalName, "paramGlfwMonitor", "(" + MethodCall_Desc + paramType.getDescriptor() + ")" + MethodCall_Desc, false); + } else if ("SDL_Window *".equals(nativeType)) { + mv.visitVarInsn(paramType.getOpcode(ILOAD), var); + mv.visitMethodInsn(INVOKESTATIC, RT_InternalName, "paramSdlWindow", "(" + MethodCall_Desc + paramType.getDescriptor() + ")" + MethodCall_Desc, false); + } else if ("SDL_GLContext".equals(nativeType) || "SDL_GLContext *".equals(nativeType)) { + mv.visitVarInsn(paramType.getOpcode(ILOAD), var); + mv.visitMethodInsn(INVOKESTATIC, RT_InternalName, "paramSdlGlContext", "(" + MethodCall_Desc + paramType.getDescriptor() + ")" + MethodCall_Desc, false); } else { mv.visitVarInsn(paramType.getOpcode(ILOAD), var); if (paramType.getSort() == Type.ARRAY || paramType.getSort() == Type.OBJECT) { @@ -488,6 +521,10 @@ private static void generateDefaultTraceAfter(InterceptedCall call, MethodVisito mv.visitMethodInsn(INVOKESTATIC, RT_InternalName, "returnValueGlfwWindow", "(" + retType.getDescriptor() + MethodCall_Desc + ")" + retType.getDescriptor(), false); } else if ("GLFWmonitor *".equals(returnNativeType)) { mv.visitMethodInsn(INVOKESTATIC, RT_InternalName, "returnValueGlfwMonitor", "(" + retType.getDescriptor() + MethodCall_Desc + ")" + retType.getDescriptor(), false); + } else if ("SDL_Window *".equals(returnNativeType)) { + mv.visitMethodInsn(INVOKESTATIC, RT_InternalName, "returnValueSdlWindow", "(" + retType.getDescriptor() + MethodCall_Desc + ")" + retType.getDescriptor(), false); + } else if ("SDL_GLContext".equals(returnNativeType) || "SDL_GLContext *".equals(returnNativeType)) { + mv.visitMethodInsn(INVOKESTATIC, RT_InternalName, "returnValueSdlGlContext", "(" + retType.getDescriptor() + MethodCall_Desc + ")" + retType.getDescriptor(), false); } else { mv.visitMethodInsn(INVOKESTATIC, RT_InternalName, "returnValue", "(" + retType.getDescriptor() + MethodCall_Desc + ")" + retType.getDescriptor(), false); } diff --git a/src/org/lwjglx/debug/RT.java b/src/org/lwjglx/debug/RT.java index 807cc5d..605e263 100644 --- a/src/org/lwjglx/debug/RT.java +++ b/src/org/lwjglx/debug/RT.java @@ -22,9 +22,8 @@ of this software and associated documentation files (the "Software"), to deal */ package org.lwjglx.debug; -import static org.lwjglx.debug.Log.error; -import static org.lwjglx.debug.Log.trace; -import static org.lwjglx.debug.org.lwjgl.opengl.Context.*; +import org.lwjgl.PointerBuffer; +import org.lwjglx.debug.org.lwjgl.opengl.Context; import java.nio.Buffer; import java.nio.ByteBuffer; @@ -37,11 +36,19 @@ of this software and associated documentation files (the "Software"), to deal import java.nio.ShortBuffer; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; -import org.lwjgl.PointerBuffer; -import org.lwjglx.debug.org.lwjgl.opengl.Context; +import static org.lwjglx.debug.Log.error; +import static org.lwjglx.debug.Log.trace; +import static org.lwjglx.debug.org.lwjgl.opengl.Context.CONTEXTS; +import static org.lwjglx.debug.org.lwjgl.opengl.Context.CURRENT_CONTEXT; +import static org.lwjglx.debug.org.lwjgl.opengl.Context.TextureLayer; +import static org.lwjglx.debug.org.lwjgl.opengl.Context.TextureLevel; +import static org.lwjglx.debug.org.lwjgl.opengl.Context.TextureObject; +import static org.lwjglx.debug.org.lwjgl.opengl.Context.checkFramebufferCompleteness; class Command { final List params; @@ -76,6 +83,7 @@ public class RT { public static Thread mainThread; public static boolean glfwInitialized; + public static boolean sdlInitialized; private static void throwIfNotNativeEndianness(ByteOrder order) { if (order != null && order != ByteOrder.nativeOrder()) { @@ -1260,6 +1268,84 @@ public static void checkGlfwWindow(long share) { throwIAEOrLogError("Provided 'share' argument is not a valid GLFW window handle: " + share); } + public static final Map SDL_WINDOWS = Collections.synchronizedMap(new HashMap<>()); + private static final AtomicInteger SDL_WINDOW_COUNTER = new AtomicInteger(1); + + public static long registerSdlWindow(long window) { + if (window != 0L) { + SDL_WINDOWS.put(window, SDL_WINDOW_COUNTER.getAndIncrement()); + } + return window; + } + + public static void destroySdlWindow(long window) { + if (window != 0L) { + SDL_WINDOWS.remove(window); + } + } + + public static MethodCall paramSdlWindow(MethodCall mc, long window) { + Integer counter = SDL_WINDOWS.get(window); + if (counter == null) { + mc.param(window); + } else { + mc.paramEnum("window[" + counter + "]"); + } + return mc; + } + + public static long returnValueSdlWindow(long window, MethodCall mc) { + Integer counter = SDL_WINDOWS.get(window); + if (counter == null) { + mc.returnValue(window); + } else { + mc.returnValueEnum("window[" + counter + "]"); + } + return window; + } + + public static MethodCall paramSdlGlContext(MethodCall mc, long context) { + Context ctx = CONTEXTS.get(context); + if (ctx == null) { + mc.param(context); + } else { + mc.paramEnum("context[" + ctx.counter + "]"); + } + return mc; + } + + public static long returnValueSdlGlContext(long context, MethodCall mc) { + Context ctx = CONTEXTS.get(context); + if (ctx == null) { + mc.returnValue(context); + } else { + mc.returnValueEnum("context[" + ctx.counter + "]"); + } + return context; + } + + public static void checkSdlInitialized(String methodName) { + if (!sdlInitialized) { + throwISEOrLogError("Method " + methodName + " was called before initializing SDL via SDL_Init()."); + } + } + + public static void checkSdlWindow(long window) { + if (window == 0L) + return; + if (!SDL_WINDOWS.containsKey(window)) { + throwIAEOrLogError("Provided 'window' argument is not a valid SDL window handle: " + window); + } + } + + public static void checkSdlGlContext(long context) { + if (context == 0L) + return; + if (!CONTEXTS.containsKey(context)) { + throwIAEOrLogError("Provided 'context' argument is not a valid SDL GL context handle: " + context); + } + } + public static void checkMainMethod(String[] args) { if (args == null) return; diff --git a/src/org/lwjglx/debug/org/lwjgl/opengl/Context.java b/src/org/lwjglx/debug/org/lwjgl/opengl/Context.java index c675485..c88a568 100644 --- a/src/org/lwjglx/debug/org/lwjgl/opengl/Context.java +++ b/src/org/lwjglx/debug/org/lwjgl/opengl/Context.java @@ -22,7 +22,8 @@ of this software and associated documentation files (the "Software"), to deal */ package org.lwjglx.debug.org.lwjgl.opengl; -import static org.lwjglx.debug.Log.*; +import org.lwjglx.debug.Properties; +import org.lwjglx.debug.RT; import java.lang.reflect.Field; import java.lang.reflect.Modifier; @@ -36,7 +37,8 @@ of this software and associated documentation files (the "Software"), to deal import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.atomic.AtomicInteger; -import org.lwjglx.debug.*; +import static org.lwjglx.debug.Log.LineBreakingStringBuilder; +import static org.lwjglx.debug.Log.info; public class Context implements Comparable { public static class ShareGroup { @@ -151,7 +153,7 @@ public static class TimedCodeSection { public static Context currentContext() { Context ctx = CURRENT_CONTEXT.get(); if (ctx == null) { - RT.throwISEOrLogError("No OpenGL context has been made current through recognized API methods (glfwMakeContextCurrent)."); + RT.throwISEOrLogError("No OpenGL context has been made current through recognized API methods (glfwMakeContextCurrent or SDL_GL_MakeCurrent)."); } return ctx; } diff --git a/src/org/lwjglx/debug/org/lwjgl/sdl/SDLInit.java b/src/org/lwjglx/debug/org/lwjgl/sdl/SDLInit.java new file mode 100644 index 0000000..9c30354 --- /dev/null +++ b/src/org/lwjglx/debug/org/lwjgl/sdl/SDLInit.java @@ -0,0 +1,63 @@ +/* + * (C) Copyright 2026 Kai Burjack + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.lwjglx.debug.org.lwjgl.sdl; + +import org.lwjglx.debug.Properties; +import org.lwjglx.debug.RT; + +import static org.lwjglx.debug.Log.error; + +public class SDLInit { + + public static boolean SDL_Init(int flags) { + boolean ret = org.lwjgl.sdl.SDLInit.SDL_Init(flags); + if (ret) { + RT.sdlInitialized = true; + } else { + if (Properties.VALIDATE.enabled) { + error("SDL_Init returned false"); + } + } + return ret; + } + + public static boolean SDL_InitSubSystem(int flags) { + boolean ret = org.lwjgl.sdl.SDLInit.SDL_InitSubSystem(flags); + if (ret) { + RT.sdlInitialized = true; + } else { + if (Properties.VALIDATE.enabled) { + error("SDL_InitSubSystem returned false"); + } + } + return ret; + } + + public static void SDL_Quit() { + org.lwjgl.sdl.SDLInit.SDL_Quit(); + RT.sdlInitialized = false; + } + + public static void SDL_QuitSubSystem(int flags) { + org.lwjgl.sdl.SDLInit.SDL_QuitSubSystem(flags); + } +} diff --git a/src/org/lwjglx/debug/org/lwjgl/sdl/SDLVideo.java b/src/org/lwjglx/debug/org/lwjgl/sdl/SDLVideo.java new file mode 100644 index 0000000..bd5ddc9 --- /dev/null +++ b/src/org/lwjglx/debug/org/lwjgl/sdl/SDLVideo.java @@ -0,0 +1,172 @@ +/* + * (C) Copyright 2026 Kai Burjack + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.lwjglx.debug.org.lwjgl.sdl; + +import org.lwjglx.debug.Properties; +import org.lwjglx.debug.RT; +import org.lwjglx.debug.org.lwjgl.opengl.Context; + +import java.nio.ByteBuffer; +import java.util.Map; + +import static org.lwjglx.debug.Log.error; +import static org.lwjglx.debug.org.lwjgl.opengl.Context.CONTEXTS; +import static org.lwjglx.debug.org.lwjgl.opengl.Context.CURRENT_CONTEXT; + +public class SDLVideo { + + public static long SDL_CreateWindow(CharSequence title, int w, int h, long flags) { + if (Properties.VALIDATE.enabled) { + org.lwjgl.sdl.SDLVideo.SDL_GL_SetAttribute( + org.lwjgl.sdl.SDLVideo.SDL_GL_CONTEXT_FLAGS, + org.lwjgl.sdl.SDLVideo.SDL_GL_CONTEXT_DEBUG_FLAG + ); + } + long window = org.lwjgl.sdl.SDLVideo.SDL_CreateWindow(title, w, h, flags); + if (window != 0L) { + RT.registerSdlWindow(window); + } else { + error("Failed to create SDL window"); + } + return window; + } + + public static long SDL_CreateWindow(ByteBuffer title, int w, int h, long flags) { + if (Properties.VALIDATE.enabled) { + org.lwjgl.sdl.SDLVideo.SDL_GL_SetAttribute( + org.lwjgl.sdl.SDLVideo.SDL_GL_CONTEXT_FLAGS, + org.lwjgl.sdl.SDLVideo.SDL_GL_CONTEXT_DEBUG_FLAG + ); + } + long window = org.lwjgl.sdl.SDLVideo.SDL_CreateWindow(title, w, h, flags); + if (window != 0L) { + RT.registerSdlWindow(window); + } else { + error("Failed to create SDL window"); + } + return window; + } + + public static void SDL_DestroyWindow(long window) { + org.lwjgl.sdl.SDLVideo.SDL_DestroyWindow(window); + if (window == 0L) + return; + RT.destroySdlWindow(window); + Context currentContext = CURRENT_CONTEXT.get(); + if (currentContext != null && currentContext.window == window) { + CURRENT_CONTEXT.remove(); + } + Context context = CONTEXTS.get(window); + if (context != null) { + context.destroy(); + } + CONTEXTS.remove(window); + } + + public static long SDL_GL_CreateContext(long window) { + if (Properties.VALIDATE.enabled) { + RT.checkSdlWindow(window); + } + long context = org.lwjgl.sdl.SDLVideo.SDL_GL_CreateContext(window); + if (context != 0L) { + long shareContextHandle = 0L; + Context current = CURRENT_CONTEXT.get(); + if (current != null) { + for (Map.Entry entry : CONTEXTS.entrySet()) { + if (entry.getValue() == current) { + shareContextHandle = entry.getKey(); + break; + } + } + } + Context.create(context, shareContextHandle); + Context ctx = CONTEXTS.get(context); + if (ctx != null) { + ctx.window = window; + } + } + return context; + } + + public static boolean SDL_GL_MakeCurrent(long window, long context) { + if (Properties.VALIDATE.enabled) { + if (window != 0L) { + RT.checkSdlWindow(window); + } + if (context != 0L) { + RT.checkSdlGlContext(context); + Context ctx = CONTEXTS.get(context); + if (ctx != null && ctx.currentInThread != null && ctx.currentInThread != Thread.currentThread()) { + RT.throwISEOrLogError("Context of context[" + ctx.counter + "] is current in another thread [" + + ctx.currentInThread + "]"); + } + } + } + boolean ret = org.lwjgl.sdl.SDLVideo.SDL_GL_MakeCurrent(window, context); + if (ret) { + if (context == 0L) { + Context ctx = CURRENT_CONTEXT.get(); + CURRENT_CONTEXT.remove(); + if (ctx != null) + ctx.currentInThread = null; + } else { + Context ctx = CONTEXTS.get(context); + if (ctx != null) { + ctx.window = window; + CURRENT_CONTEXT.set(ctx); + ctx.currentInThread = Thread.currentThread(); + } + } + } + return ret; + } + + public static boolean SDL_GL_DestroyContext(long context) { + if (context == 0L) { + return org.lwjgl.sdl.SDLVideo.SDL_GL_DestroyContext(context); + } + Context currentContext = CURRENT_CONTEXT.get(); + Context ctx = CONTEXTS.get(context); + boolean ret = org.lwjgl.sdl.SDLVideo.SDL_GL_DestroyContext(context); + if (ret) { + if (currentContext != null && currentContext == ctx) { + CURRENT_CONTEXT.remove(); + } + if (ctx != null) { + ctx.destroy(); + } + CONTEXTS.remove(context); + } + return ret; + } + + public static boolean SDL_GL_SwapWindow(long window) { + if (Properties.VALIDATE.enabled) { + RT.checkSdlWindow(window); + } + boolean ret = org.lwjgl.sdl.SDLVideo.SDL_GL_SwapWindow(window); + if (ret) { + RT.frame(); + } + return ret; + } +} diff --git a/test/test/DebugSDLIT.java b/test/test/DebugSDLIT.java new file mode 100644 index 0000000..4fcd5d8 --- /dev/null +++ b/test/test/DebugSDLIT.java @@ -0,0 +1,175 @@ +package test; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.lwjglx.debug.Properties; +import org.lwjglx.debug.org.lwjgl.opengl.Context; + +import java.util.concurrent.CountDownLatch; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.lwjgl.opengl.GL.createCapabilities; +import static org.lwjgl.opengl.GL.setCapabilities; +import static org.lwjgl.opengl.GL11.GL_DEPTH_TEST; +import static org.lwjgl.opengl.GL11.GL_VERTEX_ARRAY_POINTER; +import static org.lwjgl.opengl.GL11.glEnable; +import static org.lwjgl.sdl.SDLError.SDL_GetError; +import static org.lwjgl.sdl.SDLInit.SDL_INIT_VIDEO; +import static org.lwjgl.sdl.SDLInit.SDL_Init; +import static org.lwjgl.sdl.SDLInit.SDL_Quit; +import static org.lwjgl.sdl.SDLVideo.SDL_CreateWindow; +import static org.lwjgl.sdl.SDLVideo.SDL_DestroyWindow; +import static org.lwjgl.sdl.SDLVideo.SDL_GL_CONTEXT_MAJOR_VERSION; +import static org.lwjgl.sdl.SDLVideo.SDL_GL_CONTEXT_MINOR_VERSION; +import static org.lwjgl.sdl.SDLVideo.SDL_GL_CONTEXT_PROFILE_CORE; +import static org.lwjgl.sdl.SDLVideo.SDL_GL_CONTEXT_PROFILE_MASK; +import static org.lwjgl.sdl.SDLVideo.SDL_GL_CreateContext; +import static org.lwjgl.sdl.SDLVideo.SDL_GL_DestroyContext; +import static org.lwjgl.sdl.SDLVideo.SDL_GL_MakeCurrent; +import static org.lwjgl.sdl.SDLVideo.SDL_GL_SetAttribute; +import static org.lwjgl.sdl.SDLVideo.SDL_WINDOW_HIDDEN; +import static org.lwjgl.sdl.SDLVideo.SDL_WINDOW_OPENGL; + +public class DebugSDLIT { + + private long window; + private long window2; + private long context; + private long context2; + private boolean isMac; + + static { + Properties.STRICT.enable(); + } + + public static void assertThrowsWithMessage(Class exceptionClass, Runnable r, Object message) { + RuntimeException e = assertThrows(exceptionClass, () -> r.run()); + if (message != null) { + if (message instanceof String) + assertEquals((String) message, e.getMessage()); + else if (message instanceof Pattern) + assertTrue(((Pattern)message).matcher(e.getMessage()).matches(), "Expect Regex [" + message.toString() + "] to match [" + e.getMessage() + "]"); + } + } + + private boolean alreadyTerminated; + + @BeforeEach + public void beforeEach(TestInfo testInfo) { + isMac = System.getProperty("os.name").toLowerCase().contains("mac"); + if (!SDL_Init(SDL_INIT_VIDEO)) { + throw new IllegalStateException("Unable to initialize SDL: " + SDL_GetError()); + } + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); + } + + @AfterEach + public void afterEach() { + if (context != 0L) { + SDL_GL_DestroyContext(context); + context = 0L; + } + if (context2 != 0L) { + SDL_GL_DestroyContext(context2); + context2 = 0L; + } + if (window != 0L) { + SDL_DestroyWindow(window); + window = 0L; + } + if (window2 != 0L) { + SDL_DestroyWindow(window2); + window2 = 0L; + } + setCapabilities(null); + if (!alreadyTerminated) { + SDL_Quit(); + } + } + + @Test + public void testQuitAndInit() { + SDL_Quit(); + alreadyTerminated = true; + assertTrue(SDL_Init(SDL_INIT_VIDEO)); + alreadyTerminated = false; + } + + @Test + public void testWrongWindowArgumentForContext() { + assertThrows(IllegalArgumentException.class, () -> context = SDL_GL_CreateContext(12345L)); + } + + @Test + public void testWrongWindowArgumentForMakeCurrent() { + window = SDL_CreateWindow("Test", 300, 300, SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN); + assertNotNull(window); + context = SDL_GL_CreateContext(window); + assertNotNull(context); + assertThrows(IllegalArgumentException.class, () -> SDL_GL_MakeCurrent(12345L, context)); + } + + @Test + public void testWrongContextArgumentForMakeCurrent() { + window = SDL_CreateWindow("Test", 300, 300, SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN); + assertNotNull(window); + assertThrows(IllegalArgumentException.class, () -> SDL_GL_MakeCurrent(window, 12345L)); + } + + @Test + public void testNoGLCapabilities() { + window = SDL_CreateWindow("Test", 300, 300, SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN); + context = SDL_GL_CreateContext(window); + assertTrue(SDL_GL_MakeCurrent(window, context)); + assertThrows(IllegalStateException.class, () -> glEnable(GL_DEPTH_TEST)); + } + + @Test + public void testNoCurrentContext() { + assertThrowsWithMessage(IllegalStateException.class, () -> Context.currentContext(), + "No OpenGL context has been made current through recognized API methods (glfwMakeContextCurrent or SDL_GL_MakeCurrent)."); + } + + @Test + public void testCurrentInAnotherThread() throws Exception { + CountDownLatch l1 = new CountDownLatch(1); + CountDownLatch l2 = new CountDownLatch(1); + window = SDL_CreateWindow("Test", 300, 300, SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN); + context = SDL_GL_CreateContext(window); + + Thread t = new Thread() { + public void run() { + SDL_GL_MakeCurrent(window, context); + l1.countDown(); + try { + l2.await(); + } catch (InterruptedException e) { + } + } + }; + t.start(); + l1.await(); + + assertThrowsWithMessage(IllegalStateException.class, () -> SDL_GL_MakeCurrent(window, context), + Pattern.compile("Context of context\\[\\d+\\] is current in another thread \\[.*\\]")); + l2.countDown(); + t.join(); + } + + @Test + public void testGLError() { + window = SDL_CreateWindow("Test", 300, 300, SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN); + context = SDL_GL_CreateContext(window); + assertTrue(SDL_GL_MakeCurrent(window, context)); + createCapabilities(); + assertThrowsWithMessage(IllegalStateException.class, () -> glEnable(GL_VERTEX_ARRAY_POINTER), Pattern.compile("glEnable produced error: 1280 \\(GL_INVALID_ENUM\\)")); + } +} diff --git a/test/test/HelloWorldSDL.java b/test/test/HelloWorldSDL.java new file mode 100644 index 0000000..7460430 --- /dev/null +++ b/test/test/HelloWorldSDL.java @@ -0,0 +1,75 @@ +package test; + +import org.lwjgl.Version; +import org.lwjgl.opengl.GL; + +import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT; +import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT; +import static org.lwjgl.opengl.GL11.glClear; +import static org.lwjgl.opengl.GL11.glClearColor; +import static org.lwjgl.sdl.SDLError.SDL_GetError; +import static org.lwjgl.sdl.SDLInit.SDL_INIT_VIDEO; +import static org.lwjgl.sdl.SDLInit.SDL_Init; +import static org.lwjgl.sdl.SDLInit.SDL_Quit; +import static org.lwjgl.sdl.SDLVideo.SDL_CreateWindow; +import static org.lwjgl.sdl.SDLVideo.SDL_DestroyWindow; +import static org.lwjgl.sdl.SDLVideo.SDL_GL_CONTEXT_MAJOR_VERSION; +import static org.lwjgl.sdl.SDLVideo.SDL_GL_CONTEXT_MINOR_VERSION; +import static org.lwjgl.sdl.SDLVideo.SDL_GL_CONTEXT_PROFILE_CORE; +import static org.lwjgl.sdl.SDLVideo.SDL_GL_CONTEXT_PROFILE_MASK; +import static org.lwjgl.sdl.SDLVideo.SDL_GL_CreateContext; +import static org.lwjgl.sdl.SDLVideo.SDL_GL_DestroyContext; +import static org.lwjgl.sdl.SDLVideo.SDL_GL_MakeCurrent; +import static org.lwjgl.sdl.SDLVideo.SDL_GL_SetAttribute; +import static org.lwjgl.sdl.SDLVideo.SDL_GL_SwapWindow; +import static org.lwjgl.sdl.SDLVideo.SDL_WINDOW_OPENGL; +import static org.lwjgl.system.MemoryUtil.NULL; + +public class HelloWorldSDL { + + public void run() { + System.out.println("Hello LWJGL SDL3 " + Version.getVersion() + "!"); + + if (!SDL_Init(SDL_INIT_VIDEO)) { + throw new IllegalStateException("Unable to initialize SDL: " + SDL_GetError()); + } + + // Configure SDL GL attributes + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); + + long window = SDL_CreateWindow("Hello SDL3!", 300, 300, SDL_WINDOW_OPENGL); + if (window == NULL) { + throw new RuntimeException("Failed to create the SDL window: " + SDL_GetError()); + } + + long context = SDL_GL_CreateContext(window); + if (context == NULL) { + throw new RuntimeException("Failed to create the OpenGL context: " + SDL_GetError()); + } + + if (!SDL_GL_MakeCurrent(window, context)) { + throw new RuntimeException("Failed to make OpenGL context current: " + SDL_GetError()); + } + + GL.createCapabilities(); + + // Set the clear color + glClearColor(0.0f, 1.0f, 0.0f, 0.0f); // Green for SDL! + + // Draw a few frames and exit + for (int i = 0; i < 60; i++) { + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + SDL_GL_SwapWindow(window); + } + + SDL_GL_DestroyContext(context); + SDL_DestroyWindow(window); + SDL_Quit(); + } + + public static void main(String[] args) { + new HelloWorldSDL().run(); + } +}