diff --git a/.gitignore b/.gitignore index 7904a980..aee6e667 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ logs/ build/ out/ !gradle/wrapper/*.jar + +# Dear ImGui runtime layout file (written by ImGui on exit) +imgui.ini diff --git a/README.md b/README.md index e1224968..ec3fcdd2 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,8 @@ consuming them as a dependency only needs JDK 8+ at runtime. git clone --recurse-submodules https://github.com/SpaiR/imgui-java.git cd imgui-java ./gradlew :example:run +# SDL3 + SDL_GPU backend smoke test: +./gradlew :example:run -PmainClass=MainSdl ``` `example/src/main/java/` contains a runnable showcase for every widget and extension — start with `Main.java`, then @@ -64,6 +66,30 @@ Pick a module based on how much control you need: On Apple Silicon nothing extra is needed: the published macOS native is a universal binary covering both `x86_64` and `aarch64`. +### SDL3 backend (imgui-app) + +`imgui-app` now supports two backends via `Configuration#setBackend(...)`: + +- `Backend.GLFW` (default) +- `Backend.SDL` (SDL3 + SDL_GPU) + +Use SDL3 by switching backend in `configure(...)`: + +``` +@Override +protected void configure(final Configuration config) { + config.setBackend(Backend.SDL); +} +``` + +For a ready smoke-test entry point, run `MainSdl`: + +``` +./gradlew :example:run -PmainClass=MainSdl +``` + +At the moment, SDL backend in `imgui-app` is focused on single-window flow (multi-viewport is not supported). + ## Application If you don't care about OpenGL and other low-level stuff, then you can use application layer from `imgui-app` module. diff --git a/example/build.gradle b/example/build.gradle index f668338f..ac5002d8 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -9,7 +9,7 @@ java { } application { - mainClass = 'Main' + mainClass = (findProperty('mainClass') ?: 'Main') as String try { if (libPath) { applicationDefaultJvmArgs = ["-Dimgui.library.path=$libPath"] @@ -23,7 +23,7 @@ dependencies { implementation project(':imgui-app') } -run { +tasks.withType(JavaExec).configureEach { if (org.gradle.internal.os.OperatingSystem.current().isMacOsX()) { jvmArgs += ['-XstartOnFirstThread', '-Djava.awt.headless=true'] } diff --git a/example/src/main/java/MainSdl.java b/example/src/main/java/MainSdl.java new file mode 100644 index 00000000..4ba282a0 --- /dev/null +++ b/example/src/main/java/MainSdl.java @@ -0,0 +1,29 @@ +import imgui.ImGui; +import imgui.app.Application; +import imgui.app.Backend; +import imgui.app.Configuration; + +/** + * Smoke-test entry point for the SDL3 + SDL_GPU backend. Run with: + *
+ * ./gradlew :example:run -PmainClass=MainSdl + *+ */ +public class MainSdl extends Application { + @Override + protected void configure(final Configuration config) { + config.setTitle("Example Application — SDL3 + SDL_GPU"); + config.setBackend(Backend.SDL); + } + + @Override + public void process() { + ImGui.begin("SDL3 + SDL_GPU"); + ImGui.text("Hello from the SDL backend!"); + ImGui.end(); + } + + public static void main(final String[] args) { + launch(new MainSdl()); + } +} diff --git a/imgui-app/build.gradle b/imgui-app/build.gradle index 8b2ef74e..726f0c09 100644 --- a/imgui-app/build.gradle +++ b/imgui-app/build.gradle @@ -22,11 +22,13 @@ dependencies { api 'org.lwjgl:lwjgl' api 'org.lwjgl:lwjgl-glfw' api 'org.lwjgl:lwjgl-opengl' + api 'org.lwjgl:lwjgl-sdl' ['natives-linux', 'natives-windows', 'natives-macos', 'natives-macos-arm64'].each { api "org.lwjgl:lwjgl::$it" api "org.lwjgl:lwjgl-glfw::$it" api "org.lwjgl:lwjgl-opengl::$it" + api "org.lwjgl:lwjgl-sdl::$it" } api project(':imgui-binding') diff --git a/imgui-app/src/main/java/imgui/app/Application.java b/imgui-app/src/main/java/imgui/app/Application.java index 37d641d3..d135726d 100644 --- a/imgui-app/src/main/java/imgui/app/Application.java +++ b/imgui-app/src/main/java/imgui/app/Application.java @@ -1,24 +1,28 @@ package imgui.app; +import imgui.ImGui; + /** * Application class from which ImGui applications extend. * Serves as an abstraction layer to hide all low-level details about window creation and rendering routine. * *
The entry point for ImGui applications is the Application class and {@link #launch(Application)} method. - * It initializes application instance and starts the main application loop. + * It builds a {@link Configuration}, lets the user customize it (including selecting a {@link Backend}), then + * instantiates the matching {@link Window} subclass ({@link WindowGlfw} or {@link WindowSdl}) and drives the + * application loop: * *
As it could be seen, ImGui application differs from the classic one in the way of its life-cycle flow. @@ -52,7 +56,9 @@ * For example, large list of computations could be separated between application ticks. {@link #process()} method is called constantly. * Use that wisely and remember that all GUI should be in the main thread. */ -public abstract class Application extends Window { +public abstract class Application { + private Window window; + /** * Method called before window creation. Could be used to provide basic window information, like title name etc. * @@ -61,6 +67,32 @@ public abstract class Application extends Window { protected void configure(final Configuration config) { } + /** + * Method to initialize Dear ImGui context. Could be overridden to do custom Dear ImGui setup before application start. + * + * @param config configuration object with basic window information + */ + protected void initImGui(final Configuration config) { + ImGui.createContext(); + } + + /** + * Method called every frame, before calling {@link #process()} method. + */ + protected void preProcess() { + } + + /** + * Method called every frame, after calling {@link #process()} method. + */ + protected void postProcess() { + } + + /** + * Method to be overridden by user to provide main application logic. + */ + public abstract void process(); + /** * Method called once, before application run loop. */ @@ -73,22 +105,41 @@ protected void preRun() { protected void postRun() { } + /** + * Method to destroy Dear ImGui context. + */ + protected void disposeImGui() { + ImGui.destroyContext(); + } + + /** + * @return the {@link Window} backing this application + */ + public final Window getWindow() { + return window; + } + + /** + * @return {@link Color} instance, which represents background color for the window + */ + public final Color getColorBg() { + return window.getColorBg(); + } + /** * Entry point of any ImGui application. Use it to start the application loop. * * @param app application instance to run */ public static void launch(final Application app) { - initialize(app); - app.preRun(); - app.run(); - app.postRun(); - app.dispose(); - } - - private static void initialize(final Application app) { final Configuration config = new Configuration(); app.configure(config); - app.init(config); + app.window = (config.getBackend() == Backend.SDL) ? new WindowSdl() : new WindowGlfw(); + app.window.setOwner(app); + app.window.init(config); + app.preRun(); + app.window.run(); + app.postRun(); + app.window.dispose(); } } diff --git a/imgui-app/src/main/java/imgui/app/Backend.java b/imgui-app/src/main/java/imgui/app/Backend.java new file mode 100644 index 00000000..9eb69aa5 --- /dev/null +++ b/imgui-app/src/main/java/imgui/app/Backend.java @@ -0,0 +1,15 @@ +package imgui.app; + +/** + * Backend selection used by {@link Configuration} to choose which {@link Window} subclass {@link Application} instantiates. + */ +public enum Backend { + /** + * GLFW platform + OpenGL 3 renderer. Default. + */ + GLFW, + /** + * SDL3 platform + SDL_GPU renderer. + */ + SDL +} diff --git a/imgui-app/src/main/java/imgui/app/Configuration.java b/imgui-app/src/main/java/imgui/app/Configuration.java index 4c52a0eb..c0576174 100644 --- a/imgui-app/src/main/java/imgui/app/Configuration.java +++ b/imgui-app/src/main/java/imgui/app/Configuration.java @@ -20,6 +20,10 @@ public class Configuration { * When true, application will be maximized by default. */ private boolean fullScreen = false; + /** + * Backend to use for window/platform and rendering. + */ + private Backend backend = Backend.GLFW; public String getTitle() { return title; @@ -52,4 +56,12 @@ public boolean isFullScreen() { public void setFullScreen(final boolean fullScreen) { this.fullScreen = fullScreen; } + + public Backend getBackend() { + return backend; + } + + public void setBackend(final Backend backend) { + this.backend = backend; + } } diff --git a/imgui-app/src/main/java/imgui/app/Window.java b/imgui-app/src/main/java/imgui/app/Window.java index 2ba8b357..4f66cb84 100644 --- a/imgui-app/src/main/java/imgui/app/Window.java +++ b/imgui-app/src/main/java/imgui/app/Window.java @@ -1,250 +1,60 @@ package imgui.app; -import imgui.ImGui; -import imgui.flag.ImGuiConfigFlags; -import imgui.gl3.ImGuiImplGl3; -import imgui.glfw.ImGuiImplGlfw; -import org.lwjgl.glfw.Callbacks; -import org.lwjgl.glfw.GLFW; -import org.lwjgl.glfw.GLFWErrorCallback; -import org.lwjgl.glfw.GLFWVidMode; -import org.lwjgl.glfw.GLFWWindowSizeCallback; -import org.lwjgl.opengl.GL; -import org.lwjgl.opengl.GL32; -import org.lwjgl.system.MemoryStack; -import org.lwjgl.system.MemoryUtil; - -import java.nio.IntBuffer; -import java.util.Objects; - /** - * Low-level abstraction, which creates application window and starts the main loop. - * It's recommended to use {@link Application}, but this class could be extended directly as well. - * When extended, life-cycle methods should be called manually. + * Abstract base for an application window. Subclasses bind a specific platform/renderer pair + * (see {@link WindowGlfw}, {@link WindowSdl}) and forward lifecycle hooks back to the owning + * {@link Application} via {@link #owner}. + * + *
Direct subclassing is supported but the supported integration path is to extend + * {@link Application} and select a backend through {@link Configuration#setBackend(Backend)}. */ public abstract class Window { - protected ImGuiImplGlfw imGuiGlfw = new ImGuiImplGlfw(); - protected ImGuiImplGl3 imGuiGl3 = new ImGuiImplGl3(); - - private String glslVersion = null; - - /** - * Pointer to the native GLFW window. - */ - protected long handle; - /** * Background color of the window. */ protected final Color colorBg = new Color(.5f, .5f, .5f, 1); /** - * Method to initialize application. - * - * @param config configuration object with basic window information - */ - protected void init(final Configuration config) { - initWindow(config); - initImGui(config); - imGuiGlfw.init(handle, true); - imGuiGl3.init(glslVersion); - } - - /** - * Method to dispose all used application resources and destroy its window. + * Application owning this window. Set by {@link Application#launch(Application)} prior to + * {@link #init(Configuration)}. Concrete windows invoke its lifecycle hooks + * ({@link Application#initImGui(Configuration)}, {@link Application#preProcess()}, + * {@link Application#process()}, {@link Application#postProcess()}, + * {@link Application#disposeImGui()}). */ - protected void dispose() { - imGuiGl3.shutdown(); - imGuiGlfw.shutdown(); - disposeImGui(); - disposeWindow(); - } + protected Application owner; /** - * Method to create and initialize GLFW window. + * Wires the owning application. Package-private; called by {@link Application#launch(Application)}. * - * @param config configuration object with basic window information + * @param owner application driving this window */ - protected void initWindow(final Configuration config) { - GLFWErrorCallback.createPrint(System.err).set(); - - if (!GLFW.glfwInit()) { - throw new IllegalStateException("Unable to initialize GLFW"); - } - - decideGlGlslVersions(); - - GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE); - handle = GLFW.glfwCreateWindow(config.getWidth(), config.getHeight(), config.getTitle(), MemoryUtil.NULL, MemoryUtil.NULL); - - if (handle == MemoryUtil.NULL) { - throw new RuntimeException("Failed to create the GLFW window"); - } - - try (MemoryStack stack = MemoryStack.stackPush()) { - final IntBuffer pWidth = stack.mallocInt(1); // int* - final IntBuffer pHeight = stack.mallocInt(1); // int* - - GLFW.glfwGetWindowSize(handle, pWidth, pHeight); - final GLFWVidMode vidmode = Objects.requireNonNull(GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor())); - GLFW.glfwSetWindowPos(handle, (vidmode.width() - pWidth.get(0)) / 2, (vidmode.height() - pHeight.get(0)) / 2); - } - - GLFW.glfwMakeContextCurrent(handle); - - GL.createCapabilities(); - - GLFW.glfwSwapInterval(GLFW.GLFW_TRUE); - - if (config.isFullScreen()) { - GLFW.glfwMaximizeWindow(handle); - } else { - GLFW.glfwShowWindow(handle); - } - - clearBuffer(); - renderBuffer(); - - GLFW.glfwSetWindowSizeCallback(handle, new GLFWWindowSizeCallback() { - @Override - public void invoke(final long window, final int width, final int height) { - runFrame(); - } - }); - } - - private void decideGlGlslVersions() { - final boolean isMac = System.getProperty("os.name").toLowerCase().contains("mac"); - if (isMac) { - glslVersion = "#version 150"; - GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3); - GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 2); - GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE); // 3.2+ only - GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GLFW.GLFW_TRUE); // Required on Mac - } else { - glslVersion = "#version 130"; - GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3); - GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 0); - } + final void setOwner(final Application owner) { + this.owner = owner; } /** - * Method to initialize Dear ImGui context. Could be overridden to do custom Dear ImGui setup before application start. + * Initializes the window and its platform/renderer backends. * * @param config configuration object with basic window information */ - protected void initImGui(final Configuration config) { - ImGui.createContext(); - } - - /** - * Method called every frame, before calling {@link #process()} method. - */ - protected void preProcess() { - } - - /** - * Method called every frame, after calling {@link #process()} method. - */ - protected void postProcess() { - } - - /** - * Main application loop. - */ - protected void run() { - while (!GLFW.glfwWindowShouldClose(handle)) { - runFrame(); - } - } - - /** - * Method used to run the next frame. - */ - protected void runFrame() { - startFrame(); - preProcess(); - process(); - postProcess(); - endFrame(); - } + protected abstract void init(Configuration config); /** - * Method to be overridden by user to provide main application logic. + * Releases all resources owned by the window and its backends. */ - public abstract void process(); + protected abstract void dispose(); /** - * Method used to clear the OpenGL buffer. + * Main application loop. Drives {@link #runFrame()} until the window is asked to close. */ - private void clearBuffer() { - GL32.glClearColor(colorBg.getRed(), colorBg.getGreen(), colorBg.getBlue(), colorBg.getAlpha()); - GL32.glClear(GL32.GL_COLOR_BUFFER_BIT | GL32.GL_DEPTH_BUFFER_BIT); - } + protected abstract void run(); /** - * Method called at the beginning of the main cycle. - * It clears OpenGL buffer and starts an ImGui frame. + * Renders a single frame: pumps platform events, calls back into {@link #owner}'s + * {@link Application#preProcess()} / {@link Application#process()} / {@link Application#postProcess()} + * hooks, and submits the frame to the renderer. */ - protected void startFrame() { - clearBuffer(); - imGuiGl3.newFrame(); - imGuiGlfw.newFrame(); - ImGui.newFrame(); - } - - /** - * Method called in the end of the main cycle. - * It renders ImGui and swaps GLFW buffers to show an updated frame. - */ - protected void endFrame() { - ImGui.render(); - imGuiGl3.renderDrawData(ImGui.getDrawData()); - - // Update and Render additional Platform Windows - // (Platform functions may change the current OpenGL context, so we save/restore it to make it easier to paste this code elsewhere. - // For this specific demo app we could also call glfwMakeContextCurrent(window) directly) - if (ImGui.getIO().hasConfigFlags(ImGuiConfigFlags.ViewportsEnable)) { - final long backupCurrentContext = GLFW.glfwGetCurrentContext(); - ImGui.updatePlatformWindows(); - ImGui.renderPlatformWindowsDefault(); - GLFW.glfwMakeContextCurrent(backupCurrentContext); - } - - renderBuffer(); - } - - /** - * Method to render the OpenGL buffer and poll window events. - */ - private void renderBuffer() { - GLFW.glfwSwapBuffers(handle); - GLFW.glfwPollEvents(); - } - - /** - * Method to destroy Dear ImGui context. - */ - protected void disposeImGui() { - ImGui.destroyContext(); - } - - /** - * Method to destroy GLFW window. - */ - protected void disposeWindow() { - Callbacks.glfwFreeCallbacks(handle); - GLFW.glfwDestroyWindow(handle); - GLFW.glfwTerminate(); - Objects.requireNonNull(GLFW.glfwSetErrorCallback(null)).free(); - } - - /** - * @return pointer to the native GLFW window - */ - public final long getHandle() { - return handle; - } + protected abstract void runFrame(); /** * @return {@link Color} instance, which represents background color for the window diff --git a/imgui-app/src/main/java/imgui/app/WindowGlfw.java b/imgui-app/src/main/java/imgui/app/WindowGlfw.java new file mode 100644 index 00000000..88ab16ae --- /dev/null +++ b/imgui-app/src/main/java/imgui/app/WindowGlfw.java @@ -0,0 +1,227 @@ +package imgui.app; + +import imgui.ImGui; +import imgui.flag.ImGuiConfigFlags; +import imgui.gl3.ImGuiImplGl3; +import imgui.glfw.ImGuiImplGlfw; +import org.lwjgl.glfw.Callbacks; +import org.lwjgl.glfw.GLFWErrorCallback; +import org.lwjgl.glfw.GLFWVidMode; +import org.lwjgl.glfw.GLFWWindowSizeCallback; +import org.lwjgl.opengl.GL; +import org.lwjgl.opengl.GL32; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; + +import java.nio.IntBuffer; +import java.util.Objects; + +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_FORWARD_COMPAT; +import static org.lwjgl.glfw.GLFW.GLFW_OPENGL_PROFILE; +import static org.lwjgl.glfw.GLFW.GLFW_TRUE; +import static org.lwjgl.glfw.GLFW.GLFW_VISIBLE; +import static org.lwjgl.glfw.GLFW.glfwCreateWindow; +import static org.lwjgl.glfw.GLFW.glfwDestroyWindow; +import static org.lwjgl.glfw.GLFW.glfwGetCurrentContext; +import static org.lwjgl.glfw.GLFW.glfwGetPrimaryMonitor; +import static org.lwjgl.glfw.GLFW.glfwGetVideoMode; +import static org.lwjgl.glfw.GLFW.glfwGetWindowSize; +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.glfwSetErrorCallback; +import static org.lwjgl.glfw.GLFW.glfwSetWindowPos; +import static org.lwjgl.glfw.GLFW.glfwSetWindowSizeCallback; +import static org.lwjgl.glfw.GLFW.glfwShowWindow; +import static org.lwjgl.glfw.GLFW.glfwSwapBuffers; +import static org.lwjgl.glfw.GLFW.glfwSwapInterval; +import static org.lwjgl.glfw.GLFW.glfwTerminate; +import static org.lwjgl.glfw.GLFW.glfwWindowHint; +import static org.lwjgl.glfw.GLFW.glfwWindowShouldClose; + +/** + * GLFW + OpenGL 3 implementation of {@link Window}. Mirrors the historical (pre-SDL) behavior of + * {@code Window}: GLFW window creation with GL 3.0 / 3.2 context, GL clear, GLFW poll/swap, + * and multi-viewport handling via {@link ImGuiConfigFlags#ViewportsEnable}. + */ +public class WindowGlfw extends Window { + private final ImGuiImplGlfw imGuiGlfw = new ImGuiImplGlfw(); + private final ImGuiImplGl3 imGuiGl3 = new ImGuiImplGl3(); + + private String glslVersion = null; + + /** + * Pointer to the native GLFW window. + */ + private long handle; + + @Override + protected void init(final Configuration config) { + initWindow(config); + owner.initImGui(config); + imGuiGlfw.init(handle, true); + imGuiGl3.init(glslVersion); + } + + @Override + protected void dispose() { + imGuiGl3.shutdown(); + imGuiGlfw.shutdown(); + owner.disposeImGui(); + disposeWindow(); + } + + /** + * Method to create and initialize GLFW window. + * + * @param config configuration object with basic window information + */ + protected void initWindow(final Configuration config) { + GLFWErrorCallback.createPrint(System.err).set(); + + if (!glfwInit()) { + throw new IllegalStateException("Unable to initialize GLFW"); + } + + decideGlGlslVersions(); + + glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); + handle = glfwCreateWindow(config.getWidth(), config.getHeight(), config.getTitle(), MemoryUtil.NULL, MemoryUtil.NULL); + + if (handle == MemoryUtil.NULL) { + throw new RuntimeException("Failed to create the GLFW window"); + } + + try (MemoryStack stack = MemoryStack.stackPush()) { + final IntBuffer pWidth = stack.mallocInt(1); // int* + final IntBuffer pHeight = stack.mallocInt(1); // int* + + glfwGetWindowSize(handle, pWidth, pHeight); + final GLFWVidMode vidmode = Objects.requireNonNull(glfwGetVideoMode(glfwGetPrimaryMonitor())); + glfwSetWindowPos(handle, (vidmode.width() - pWidth.get(0)) / 2, (vidmode.height() - pHeight.get(0)) / 2); + } + + glfwMakeContextCurrent(handle); + + GL.createCapabilities(); + + glfwSwapInterval(GLFW_TRUE); + + if (config.isFullScreen()) { + glfwMaximizeWindow(handle); + } else { + glfwShowWindow(handle); + } + + clearBuffer(); + renderBuffer(); + + glfwSetWindowSizeCallback(handle, new GLFWWindowSizeCallback() { + @Override + public void invoke(final long window, final int width, final int height) { + runFrame(); + } + }); + } + + private void decideGlGlslVersions() { + final boolean isMac = System.getProperty("os.name").toLowerCase().contains("mac"); + if (isMac) { + glslVersion = "#version 150"; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 3.2+ only + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE); // Required on Mac + } else { + glslVersion = "#version 130"; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); + } + } + + @Override + protected void run() { + while (!glfwWindowShouldClose(handle)) { + runFrame(); + } + } + + @Override + protected void runFrame() { + startFrame(); + owner.preProcess(); + owner.process(); + owner.postProcess(); + endFrame(); + } + + /** + * Method used to clear the OpenGL buffer. + */ + private void clearBuffer() { + GL32.glClearColor(colorBg.getRed(), colorBg.getGreen(), colorBg.getBlue(), colorBg.getAlpha()); + GL32.glClear(GL32.GL_COLOR_BUFFER_BIT | GL32.GL_DEPTH_BUFFER_BIT); + } + + /** + * Method called at the beginning of the main cycle. + * It clears OpenGL buffer and starts an ImGui frame. + */ + protected void startFrame() { + clearBuffer(); + imGuiGl3.newFrame(); + imGuiGlfw.newFrame(); + ImGui.newFrame(); + } + + /** + * Method called in the end of the main cycle. + * It renders ImGui and swaps GLFW buffers to show an updated frame. + */ + protected void endFrame() { + ImGui.render(); + imGuiGl3.renderDrawData(ImGui.getDrawData()); + + // Update and Render additional Platform Windows + // (Platform functions may change the current OpenGL context, so we save/restore it to make it easier to paste this code elsewhere. + // For this specific demo app we could also call glfwMakeContextCurrent(window) directly) + if (ImGui.getIO().hasConfigFlags(ImGuiConfigFlags.ViewportsEnable)) { + final long backupCurrentContext = glfwGetCurrentContext(); + ImGui.updatePlatformWindows(); + ImGui.renderPlatformWindowsDefault(); + glfwMakeContextCurrent(backupCurrentContext); + } + + renderBuffer(); + } + + /** + * Method to render the OpenGL buffer and poll window events. + */ + private void renderBuffer() { + glfwSwapBuffers(handle); + glfwPollEvents(); + } + + /** + * Method to destroy GLFW window. + */ + protected void disposeWindow() { + Callbacks.glfwFreeCallbacks(handle); + glfwDestroyWindow(handle); + glfwTerminate(); + Objects.requireNonNull(glfwSetErrorCallback(null)).free(); + } + + /** + * @return pointer to the native GLFW window + */ + public final long getHandle() { + return handle; + } +} diff --git a/imgui-app/src/main/java/imgui/app/WindowSdl.java b/imgui-app/src/main/java/imgui/app/WindowSdl.java new file mode 100644 index 00000000..23ebbf9c --- /dev/null +++ b/imgui-app/src/main/java/imgui/app/WindowSdl.java @@ -0,0 +1,188 @@ +package imgui.app; + +import imgui.ImGui; +import imgui.sdl3.ImGuiImplSdl3; +import imgui.sdl3.ImGuiImplSdlGpu3; +import org.lwjgl.PointerBuffer; +import org.lwjgl.sdl.SDL_Event; +import org.lwjgl.sdl.SDL_GPUColorTargetInfo; +import org.lwjgl.system.MemoryStack; + +import java.nio.IntBuffer; + +import static org.lwjgl.sdl.SDLError.SDL_GetError; +import static org.lwjgl.sdl.SDLEvents.SDL_EVENT_QUIT; +import static org.lwjgl.sdl.SDLEvents.SDL_EVENT_WINDOW_CLOSE_REQUESTED; +import static org.lwjgl.sdl.SDLEvents.SDL_PollEvent; +import static org.lwjgl.sdl.SDLGPU.SDL_AcquireGPUCommandBuffer; +import static org.lwjgl.sdl.SDLGPU.SDL_BeginGPURenderPass; +import static org.lwjgl.sdl.SDLGPU.SDL_ClaimWindowForGPUDevice; +import static org.lwjgl.sdl.SDLGPU.SDL_CreateGPUDevice; +import static org.lwjgl.sdl.SDLGPU.SDL_DestroyGPUDevice; +import static org.lwjgl.sdl.SDLGPU.SDL_EndGPURenderPass; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_LOADOP_CLEAR; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_STOREOP_STORE; +import static org.lwjgl.sdl.SDLGPU.SDL_WaitAndAcquireGPUSwapchainTexture; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_SHADERFORMAT_DXBC; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_SHADERFORMAT_METALLIB; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_SHADERFORMAT_SPIRV; +import static org.lwjgl.sdl.SDLGPU.SDL_GetGPUSwapchainTextureFormat; +import static org.lwjgl.sdl.SDLGPU.SDL_ReleaseWindowFromGPUDevice; +import static org.lwjgl.sdl.SDLGPU.SDL_SubmitGPUCommandBuffer; +import static org.lwjgl.sdl.SDLInit.SDL_INIT_GAMEPAD; +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_MaximizeWindow; +import static org.lwjgl.sdl.SDLVideo.SDL_WINDOW_HIGH_PIXEL_DENSITY; +import static org.lwjgl.sdl.SDLVideo.SDL_WINDOW_RESIZABLE; + +/** + * SDL3 + SDL_GPU implementation of {@link Window}. + * + *
Pumps SDL events through {@link ImGuiImplSdl3#processEvent(long)} each frame, acquires an + * {@code SDL_GPU} swapchain texture, and submits a clear-load render pass through + * {@link ImGuiImplSdlGpu3#prepareDrawData(long, long)} + + * {@link ImGuiImplSdlGpu3#renderDrawData(long, long, long)}. Multi-viewport is not supported + * (skipped per task scope). + */ +public class WindowSdl extends Window { + private final ImGuiImplSdl3 imGuiSdl3 = new ImGuiImplSdl3(); + private final ImGuiImplSdlGpu3 imGuiSdlGpu3 = new ImGuiImplSdlGpu3(); + + private long handle; + private long gpuDevice; + private boolean shouldClose; + + @Override + protected void init(final Configuration config) { + if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD)) { + throw new IllegalStateException("Unable to initialize SDL3: " + SDL_GetError()); + } + + final long flags = SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_RESIZABLE; + handle = SDL_CreateWindow(config.getTitle(), config.getWidth(), config.getHeight(), flags); + if (handle == 0L) { + throw new RuntimeException("Failed to create the SDL window: " + SDL_GetError()); + } + if (config.isFullScreen()) { + SDL_MaximizeWindow(handle); + } + + // Create SDL_GPU device matching whatever shader format we ship. + final int shaderFormats = SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_DXBC | SDL_GPU_SHADERFORMAT_METALLIB; + gpuDevice = SDL_CreateGPUDevice(shaderFormats, false, (java.nio.ByteBuffer) null); + if (gpuDevice == 0L) { + throw new RuntimeException("Failed to create the SDL_GPU device: " + SDL_GetError()); + } + if (!SDL_ClaimWindowForGPUDevice(gpuDevice, handle)) { + throw new RuntimeException("Failed to claim window for SDL_GPU device: " + SDL_GetError()); + } + + owner.initImGui(config); + + imGuiSdl3.initForSDLGPU(handle); + + final ImGuiImplSdlGpu3.InitInfo initInfo = new ImGuiImplSdlGpu3.InitInfo() + .setDevice(gpuDevice) + .setColorTargetFormat(SDL_GetGPUSwapchainTextureFormat(gpuDevice, handle)); + imGuiSdlGpu3.init(initInfo); + } + + @Override + protected void dispose() { + imGuiSdlGpu3.shutdown(); + imGuiSdl3.shutdown(); + owner.disposeImGui(); + + if (gpuDevice != 0L && handle != 0L) { + SDL_ReleaseWindowFromGPUDevice(gpuDevice, handle); + } + if (gpuDevice != 0L) { + SDL_DestroyGPUDevice(gpuDevice); + gpuDevice = 0L; + } + if (handle != 0L) { + SDL_DestroyWindow(handle); + handle = 0L; + } + SDL_Quit(); + } + + @Override + protected void run() { + while (!shouldClose) { + runFrame(); + } + } + + @Override + protected void runFrame() { + // Pump events + try (SDL_Event event = SDL_Event.calloc()) { + while (SDL_PollEvent(event)) { + final int type = event.type(); + imGuiSdl3.processEvent(event.address()); + if (type == SDL_EVENT_QUIT || type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) { + shouldClose = true; + } + } + } + + // Begin a Dear ImGui frame + imGuiSdlGpu3.newFrame(); + imGuiSdl3.newFrame(); + ImGui.newFrame(); + + owner.preProcess(); + owner.process(); + owner.postProcess(); + + ImGui.render(); + + final long commandBuffer = SDL_AcquireGPUCommandBuffer(gpuDevice); + if (commandBuffer != 0L) { + imGuiSdlGpu3.prepareDrawData(ImGui.getDrawData().ptr, commandBuffer); + + try (MemoryStack stack = MemoryStack.stackPush()) { + final PointerBuffer pSwapchain = stack.callocPointer(1); + final IntBuffer pW = stack.mallocInt(1); + final IntBuffer pH = stack.mallocInt(1); + final boolean acquired = SDL_WaitAndAcquireGPUSwapchainTexture(commandBuffer, handle, pSwapchain, pW, pH); + final long swapchainTexture = pSwapchain.get(0); + if (acquired && swapchainTexture != 0L) { + final SDL_GPUColorTargetInfo.Buffer colorTargets = SDL_GPUColorTargetInfo.calloc(1, stack); + final SDL_GPUColorTargetInfo target = colorTargets.get(0); + target.texture(swapchainTexture); + target.load_op(SDL_GPU_LOADOP_CLEAR); + target.store_op(SDL_GPU_STOREOP_STORE); + target.clear_color().set(colorBg.getRed(), colorBg.getGreen(), colorBg.getBlue(), colorBg.getAlpha()); + + final long renderPass = SDL_BeginGPURenderPass(commandBuffer, colorTargets, null); + if (renderPass != 0L) { + imGuiSdlGpu3.renderDrawData(ImGui.getDrawData().ptr, commandBuffer, renderPass); + SDL_EndGPURenderPass(renderPass); + } + } + } + + SDL_SubmitGPUCommandBuffer(commandBuffer); + } + } + + /** + * @return native {@code SDL_Window*} handle + */ + public final long getHandle() { + return handle; + } + + /** + * @return native {@code SDL_GPUDevice*} handle + */ + public final long getGpuDevice() { + return gpuDevice; + } +} diff --git a/imgui-lwjgl3/build.gradle b/imgui-lwjgl3/build.gradle index c026a2c8..8c3d3840 100644 --- a/imgui-lwjgl3/build.gradle +++ b/imgui-lwjgl3/build.gradle @@ -21,6 +21,7 @@ dependencies { implementation 'org.lwjgl:lwjgl' implementation 'org.lwjgl:lwjgl-glfw' implementation 'org.lwjgl:lwjgl-opengl' + implementation 'org.lwjgl:lwjgl-sdl' implementation project(':imgui-binding') } diff --git a/imgui-lwjgl3/src/main/java/imgui/glfw/ImGuiImplGlfw.java b/imgui-lwjgl3/src/main/java/imgui/glfw/ImGuiImplGlfw.java index f59ea669..31db5e34 100644 --- a/imgui-lwjgl3/src/main/java/imgui/glfw/ImGuiImplGlfw.java +++ b/imgui-lwjgl3/src/main/java/imgui/glfw/ImGuiImplGlfw.java @@ -233,6 +233,7 @@ import static org.lwjgl.glfw.GLFW.glfwGetMonitorWorkarea; import static org.lwjgl.glfw.GLFW.glfwGetMonitors; import static org.lwjgl.glfw.GLFW.glfwGetTime; +import static org.lwjgl.glfw.GLFW.glfwGetVersion; import static org.lwjgl.glfw.GLFW.glfwGetVideoMode; import static org.lwjgl.glfw.GLFW.glfwGetWindowAttrib; import static org.lwjgl.glfw.GLFW.glfwGetWindowContentScale; @@ -898,7 +899,7 @@ private boolean initImpl(final long window, final boolean installCallbacks, fina final IntBuffer verMinor = MemoryUtil.memAllocInt(1); final IntBuffer verRev = MemoryUtil.memAllocInt(1); try { - org.lwjgl.glfw.GLFW.glfwGetVersion(verMajor, verMinor, verRev); + glfwGetVersion(verMajor, verMinor, verRev); final int versionCombined = verMajor.get(0) * 1000 + verMinor.get(0) * 100 + verRev.get(0); io.setBackendPlatformName("imgui_impl_glfw (" + versionCombined + ")"); } finally { diff --git a/imgui-lwjgl3/src/main/java/imgui/sdl3/ImGuiImplSdl3.java b/imgui-lwjgl3/src/main/java/imgui/sdl3/ImGuiImplSdl3.java new file mode 100644 index 00000000..6a092498 --- /dev/null +++ b/imgui-lwjgl3/src/main/java/imgui/sdl3/ImGuiImplSdl3.java @@ -0,0 +1,989 @@ +package imgui.sdl3; + +import imgui.ImGui; +import imgui.ImGuiIO; +import imgui.callback.ImStrConsumer; +import imgui.callback.ImStrSupplier; +import imgui.flag.ImGuiBackendFlags; +import imgui.flag.ImGuiConfigFlags; +import imgui.flag.ImGuiKey; +import imgui.flag.ImGuiMouseCursor; +import imgui.flag.ImGuiMouseSource; +import org.lwjgl.sdl.SDL_Event; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.sdl.SDL_KeyboardEvent; +import org.lwjgl.sdl.SDL_MouseButtonEvent; +import org.lwjgl.sdl.SDL_MouseMotionEvent; +import org.lwjgl.sdl.SDL_MouseWheelEvent; +import org.lwjgl.sdl.SDL_TextInputEvent; +import org.lwjgl.sdl.SDL_WindowEvent; + +import static org.lwjgl.sdl.SDLClipboard.SDL_GetClipboardText; +import static org.lwjgl.sdl.SDLClipboard.SDL_HasClipboardText; +import static org.lwjgl.sdl.SDLClipboard.SDL_SetClipboardText; +import static org.lwjgl.sdl.SDLEvents.SDL_EVENT_DISPLAY_ADDED; +import static org.lwjgl.sdl.SDLEvents.SDL_EVENT_DISPLAY_CONTENT_SCALE_CHANGED; +import static org.lwjgl.sdl.SDLEvents.SDL_EVENT_DISPLAY_MOVED; +import static org.lwjgl.sdl.SDLEvents.SDL_EVENT_DISPLAY_ORIENTATION; +import static org.lwjgl.sdl.SDLEvents.SDL_EVENT_DISPLAY_REMOVED; +import static org.lwjgl.sdl.SDLEvents.SDL_EVENT_DISPLAY_USABLE_BOUNDS_CHANGED; +import static org.lwjgl.sdl.SDLEvents.SDL_EVENT_GAMEPAD_ADDED; +import static org.lwjgl.sdl.SDLEvents.SDL_EVENT_GAMEPAD_REMOVED; +import static org.lwjgl.sdl.SDLEvents.SDL_EVENT_KEY_DOWN; +import static org.lwjgl.sdl.SDLEvents.SDL_EVENT_KEY_UP; +import static org.lwjgl.sdl.SDLEvents.SDL_EVENT_MOUSE_BUTTON_DOWN; +import static org.lwjgl.sdl.SDLEvents.SDL_EVENT_MOUSE_BUTTON_UP; +import static org.lwjgl.sdl.SDLEvents.SDL_EVENT_MOUSE_MOTION; +import static org.lwjgl.sdl.SDLEvents.SDL_EVENT_MOUSE_WHEEL; +import static org.lwjgl.sdl.SDLEvents.SDL_EVENT_TEXT_INPUT; +import static org.lwjgl.sdl.SDLEvents.SDL_EVENT_WINDOW_CLOSE_REQUESTED; +import static org.lwjgl.sdl.SDLEvents.SDL_EVENT_WINDOW_FOCUS_GAINED; +import static org.lwjgl.sdl.SDLEvents.SDL_EVENT_WINDOW_FOCUS_LOST; +import static org.lwjgl.sdl.SDLEvents.SDL_EVENT_WINDOW_MOUSE_ENTER; +import static org.lwjgl.sdl.SDLEvents.SDL_EVENT_WINDOW_MOUSE_LEAVE; +import static org.lwjgl.sdl.SDLEvents.SDL_EVENT_WINDOW_MOVED; +import static org.lwjgl.sdl.SDLEvents.SDL_EVENT_WINDOW_RESIZED; +import static org.lwjgl.sdl.SDLGamepad.SDL_CloseGamepad; +import static org.lwjgl.sdl.SDLGamepad.SDL_GAMEPAD_AXIS_LEFTX; +import static org.lwjgl.sdl.SDLGamepad.SDL_GAMEPAD_AXIS_LEFTY; +import static org.lwjgl.sdl.SDLGamepad.SDL_GAMEPAD_AXIS_LEFT_TRIGGER; +import static org.lwjgl.sdl.SDLGamepad.SDL_GAMEPAD_AXIS_RIGHTX; +import static org.lwjgl.sdl.SDLGamepad.SDL_GAMEPAD_AXIS_RIGHTY; +import static org.lwjgl.sdl.SDLGamepad.SDL_GAMEPAD_AXIS_RIGHT_TRIGGER; +import static org.lwjgl.sdl.SDLGamepad.SDL_GAMEPAD_BUTTON_BACK; +import static org.lwjgl.sdl.SDLGamepad.SDL_GAMEPAD_BUTTON_DPAD_DOWN; +import static org.lwjgl.sdl.SDLGamepad.SDL_GAMEPAD_BUTTON_DPAD_LEFT; +import static org.lwjgl.sdl.SDLGamepad.SDL_GAMEPAD_BUTTON_DPAD_RIGHT; +import static org.lwjgl.sdl.SDLGamepad.SDL_GAMEPAD_BUTTON_DPAD_UP; +import static org.lwjgl.sdl.SDLGamepad.SDL_GAMEPAD_BUTTON_EAST; +import static org.lwjgl.sdl.SDLGamepad.SDL_GAMEPAD_BUTTON_LEFT_SHOULDER; +import static org.lwjgl.sdl.SDLGamepad.SDL_GAMEPAD_BUTTON_LEFT_STICK; +import static org.lwjgl.sdl.SDLGamepad.SDL_GAMEPAD_BUTTON_NORTH; +import static org.lwjgl.sdl.SDLGamepad.SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER; +import static org.lwjgl.sdl.SDLGamepad.SDL_GAMEPAD_BUTTON_RIGHT_STICK; +import static org.lwjgl.sdl.SDLGamepad.SDL_GAMEPAD_BUTTON_SOUTH; +import static org.lwjgl.sdl.SDLGamepad.SDL_GAMEPAD_BUTTON_START; +import static org.lwjgl.sdl.SDLGamepad.SDL_GAMEPAD_BUTTON_WEST; +import static org.lwjgl.sdl.SDLGamepad.SDL_GetGamepadAxis; +import static org.lwjgl.sdl.SDLGamepad.SDL_GetGamepadButton; +import static org.lwjgl.sdl.SDLGamepad.SDL_GetGamepads; +import static org.lwjgl.sdl.SDLGamepad.SDL_OpenGamepad; +import static org.lwjgl.sdl.SDLHints.SDL_HINT_MOUSE_AUTO_CAPTURE; +import static org.lwjgl.sdl.SDLHints.SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH; +import static org.lwjgl.sdl.SDLHints.SDL_SetHint; +import static org.lwjgl.sdl.SDLKeycode.SDLK_0; +import static org.lwjgl.sdl.SDLKeycode.SDLK_1; +import static org.lwjgl.sdl.SDLKeycode.SDLK_2; +import static org.lwjgl.sdl.SDLKeycode.SDLK_3; +import static org.lwjgl.sdl.SDLKeycode.SDLK_4; +import static org.lwjgl.sdl.SDLKeycode.SDLK_5; +import static org.lwjgl.sdl.SDLKeycode.SDLK_6; +import static org.lwjgl.sdl.SDLKeycode.SDLK_7; +import static org.lwjgl.sdl.SDLKeycode.SDLK_8; +import static org.lwjgl.sdl.SDLKeycode.SDLK_9; +import static org.lwjgl.sdl.SDLKeycode.SDLK_A; +import static org.lwjgl.sdl.SDLKeycode.SDLK_AC_BACK; +import static org.lwjgl.sdl.SDLKeycode.SDLK_AC_FORWARD; +import static org.lwjgl.sdl.SDLKeycode.SDLK_APPLICATION; +import static org.lwjgl.sdl.SDLKeycode.SDLK_B; +import static org.lwjgl.sdl.SDLKeycode.SDLK_BACKSPACE; +import static org.lwjgl.sdl.SDLKeycode.SDLK_C; +import static org.lwjgl.sdl.SDLKeycode.SDLK_CAPSLOCK; +import static org.lwjgl.sdl.SDLKeycode.SDLK_COMMA; +import static org.lwjgl.sdl.SDLKeycode.SDLK_D; +import static org.lwjgl.sdl.SDLKeycode.SDLK_DELETE; +import static org.lwjgl.sdl.SDLKeycode.SDLK_DOWN; +import static org.lwjgl.sdl.SDLKeycode.SDLK_E; +import static org.lwjgl.sdl.SDLKeycode.SDLK_END; +import static org.lwjgl.sdl.SDLKeycode.SDLK_ESCAPE; +import static org.lwjgl.sdl.SDLKeycode.SDLK_F; +import static org.lwjgl.sdl.SDLKeycode.SDLK_F1; +import static org.lwjgl.sdl.SDLKeycode.SDLK_F10; +import static org.lwjgl.sdl.SDLKeycode.SDLK_F11; +import static org.lwjgl.sdl.SDLKeycode.SDLK_F12; +import static org.lwjgl.sdl.SDLKeycode.SDLK_F13; +import static org.lwjgl.sdl.SDLKeycode.SDLK_F14; +import static org.lwjgl.sdl.SDLKeycode.SDLK_F15; +import static org.lwjgl.sdl.SDLKeycode.SDLK_F16; +import static org.lwjgl.sdl.SDLKeycode.SDLK_F17; +import static org.lwjgl.sdl.SDLKeycode.SDLK_F18; +import static org.lwjgl.sdl.SDLKeycode.SDLK_F19; +import static org.lwjgl.sdl.SDLKeycode.SDLK_F2; +import static org.lwjgl.sdl.SDLKeycode.SDLK_F20; +import static org.lwjgl.sdl.SDLKeycode.SDLK_F21; +import static org.lwjgl.sdl.SDLKeycode.SDLK_F22; +import static org.lwjgl.sdl.SDLKeycode.SDLK_F23; +import static org.lwjgl.sdl.SDLKeycode.SDLK_F24; +import static org.lwjgl.sdl.SDLKeycode.SDLK_F3; +import static org.lwjgl.sdl.SDLKeycode.SDLK_F4; +import static org.lwjgl.sdl.SDLKeycode.SDLK_F5; +import static org.lwjgl.sdl.SDLKeycode.SDLK_F6; +import static org.lwjgl.sdl.SDLKeycode.SDLK_F7; +import static org.lwjgl.sdl.SDLKeycode.SDLK_F8; +import static org.lwjgl.sdl.SDLKeycode.SDLK_F9; +import static org.lwjgl.sdl.SDLKeycode.SDLK_G; +import static org.lwjgl.sdl.SDLKeycode.SDLK_H; +import static org.lwjgl.sdl.SDLKeycode.SDLK_HOME; +import static org.lwjgl.sdl.SDLKeycode.SDLK_I; +import static org.lwjgl.sdl.SDLKeycode.SDLK_INSERT; +import static org.lwjgl.sdl.SDLKeycode.SDLK_J; +import static org.lwjgl.sdl.SDLKeycode.SDLK_K; +import static org.lwjgl.sdl.SDLKeycode.SDLK_L; +import static org.lwjgl.sdl.SDLKeycode.SDLK_LALT; +import static org.lwjgl.sdl.SDLKeycode.SDLK_LCTRL; +import static org.lwjgl.sdl.SDLKeycode.SDLK_LEFT; +import static org.lwjgl.sdl.SDLKeycode.SDLK_LGUI; +import static org.lwjgl.sdl.SDLKeycode.SDLK_LSHIFT; +import static org.lwjgl.sdl.SDLKeycode.SDLK_M; +import static org.lwjgl.sdl.SDLKeycode.SDLK_N; +import static org.lwjgl.sdl.SDLKeycode.SDLK_NUMLOCKCLEAR; +import static org.lwjgl.sdl.SDLKeycode.SDLK_O; +import static org.lwjgl.sdl.SDLKeycode.SDLK_P; +import static org.lwjgl.sdl.SDLKeycode.SDLK_PAGEDOWN; +import static org.lwjgl.sdl.SDLKeycode.SDLK_PAGEUP; +import static org.lwjgl.sdl.SDLKeycode.SDLK_PAUSE; +import static org.lwjgl.sdl.SDLKeycode.SDLK_PERIOD; +import static org.lwjgl.sdl.SDLKeycode.SDLK_PRINTSCREEN; +import static org.lwjgl.sdl.SDLKeycode.SDLK_Q; +import static org.lwjgl.sdl.SDLKeycode.SDLK_R; +import static org.lwjgl.sdl.SDLKeycode.SDLK_RALT; +import static org.lwjgl.sdl.SDLKeycode.SDLK_RCTRL; +import static org.lwjgl.sdl.SDLKeycode.SDLK_RETURN; +import static org.lwjgl.sdl.SDLKeycode.SDLK_RGUI; +import static org.lwjgl.sdl.SDLKeycode.SDLK_RIGHT; +import static org.lwjgl.sdl.SDLKeycode.SDLK_RSHIFT; +import static org.lwjgl.sdl.SDLKeycode.SDLK_S; +import static org.lwjgl.sdl.SDLKeycode.SDLK_SCROLLLOCK; +import static org.lwjgl.sdl.SDLKeycode.SDLK_SEMICOLON; +import static org.lwjgl.sdl.SDLKeycode.SDLK_SPACE; +import static org.lwjgl.sdl.SDLKeycode.SDLK_T; +import static org.lwjgl.sdl.SDLKeycode.SDLK_TAB; +import static org.lwjgl.sdl.SDLKeycode.SDLK_U; +import static org.lwjgl.sdl.SDLKeycode.SDLK_UP; +import static org.lwjgl.sdl.SDLKeycode.SDLK_V; +import static org.lwjgl.sdl.SDLKeycode.SDLK_W; +import static org.lwjgl.sdl.SDLKeycode.SDLK_X; +import static org.lwjgl.sdl.SDLKeycode.SDLK_Y; +import static org.lwjgl.sdl.SDLKeycode.SDLK_Z; +import static org.lwjgl.sdl.SDLKeycode.SDL_KMOD_ALT; +import static org.lwjgl.sdl.SDLKeycode.SDL_KMOD_CTRL; +import static org.lwjgl.sdl.SDLKeycode.SDL_KMOD_GUI; +import static org.lwjgl.sdl.SDLKeycode.SDL_KMOD_SHIFT; +import static org.lwjgl.sdl.SDLMouse.SDL_BUTTON_LEFT; +import static org.lwjgl.sdl.SDLMouse.SDL_BUTTON_MIDDLE; +import static org.lwjgl.sdl.SDLMouse.SDL_BUTTON_RIGHT; +import static org.lwjgl.sdl.SDLMouse.SDL_BUTTON_X1; +import static org.lwjgl.sdl.SDLMouse.SDL_BUTTON_X2; +import static org.lwjgl.sdl.SDLMouse.SDL_CaptureMouse; +import static org.lwjgl.sdl.SDLMouse.SDL_CreateSystemCursor; +import static org.lwjgl.sdl.SDLMouse.SDL_DestroyCursor; +import static org.lwjgl.sdl.SDLMouse.SDL_HideCursor; +import static org.lwjgl.sdl.SDLMouse.SDL_SYSTEM_CURSOR_DEFAULT; +import static org.lwjgl.sdl.SDLMouse.SDL_SYSTEM_CURSOR_EW_RESIZE; +import static org.lwjgl.sdl.SDLMouse.SDL_SYSTEM_CURSOR_MOVE; +import static org.lwjgl.sdl.SDLMouse.SDL_SYSTEM_CURSOR_NESW_RESIZE; +import static org.lwjgl.sdl.SDLMouse.SDL_SYSTEM_CURSOR_NOT_ALLOWED; +import static org.lwjgl.sdl.SDLMouse.SDL_SYSTEM_CURSOR_NS_RESIZE; +import static org.lwjgl.sdl.SDLMouse.SDL_SYSTEM_CURSOR_NWSE_RESIZE; +import static org.lwjgl.sdl.SDLMouse.SDL_SYSTEM_CURSOR_POINTER; +import static org.lwjgl.sdl.SDLMouse.SDL_SYSTEM_CURSOR_PROGRESS; +import static org.lwjgl.sdl.SDLMouse.SDL_SYSTEM_CURSOR_TEXT; +import static org.lwjgl.sdl.SDLMouse.SDL_SYSTEM_CURSOR_WAIT; +import static org.lwjgl.sdl.SDLMouse.SDL_SetCursor; +import static org.lwjgl.sdl.SDLMouse.SDL_ShowCursor; +import static org.lwjgl.sdl.SDLMouse.SDL_WarpMouseInWindow; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_APOSTROPHE; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_BACKSLASH; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_COMMA; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_EQUALS; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_GRAVE; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_KP_0; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_KP_1; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_KP_2; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_KP_3; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_KP_4; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_KP_5; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_KP_6; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_KP_7; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_KP_8; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_KP_9; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_KP_DIVIDE; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_KP_ENTER; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_KP_EQUALS; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_KP_MINUS; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_KP_MULTIPLY; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_KP_PERIOD; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_KP_PLUS; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_LEFTBRACKET; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_MINUS; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_NONUSBACKSLASH; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_PERIOD; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_RIGHTBRACKET; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_SEMICOLON; +import static org.lwjgl.sdl.SDLScancode.SDL_SCANCODE_SLASH; +import static org.lwjgl.sdl.SDLTimer.SDL_GetPerformanceCounter; +import static org.lwjgl.sdl.SDLTimer.SDL_GetPerformanceFrequency; +import static org.lwjgl.sdl.SDLTouch.SDL_TOUCH_MOUSEID; +import static org.lwjgl.sdl.SDLVideo.SDL_GetWindowFlags; +import static org.lwjgl.sdl.SDLVideo.SDL_GetWindowID; +import static org.lwjgl.sdl.SDLVideo.SDL_GetWindowPosition; +import static org.lwjgl.sdl.SDLVideo.SDL_GetWindowSize; +import static org.lwjgl.sdl.SDLVideo.SDL_GetWindowSizeInPixels; +import static org.lwjgl.sdl.SDLVideo.SDL_WINDOW_INPUT_FOCUS; + +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * Java 1-to-1 port of upstream {@code imgui_impl_sdl3.cpp}: Dear ImGui platform backend for SDL3. + * + *
Public lifecycle ({@link #init(long)}, {@link #shutdown()}, {@link #newFrame()}) and event + * forwarding ({@link #processEvent(long)}) mirror the upstream C functions. Unlike the GLFW port, + * SDL has an explicit poll loop, so callers feed events via {@link #processEvent(long)} after + * {@code SDL_PollEvent} rather than installing window callbacks. + * + *
Multi-viewport, IME callbacks, and the {@code Platform_*Fn} function-pointer registrations on
+ * {@code ImGuiPlatformIO} are not ported — they require C function pointers (see
+ * {@code // NOTE: not ported} markers below). Single-viewport mode covers the visual smoke test.
+ */
+@SuppressWarnings("checkstyle:DesignForExtension")
+public class ImGuiImplSdl3 {
+ /**
+ * Manual gamepad list mode: {@link #setGamepadMode(int, long[])} accepts gamepad handles directly.
+ */
+ public static final int GAMEPAD_MODE_AUTO_FIRST = 0;
+ /**
+ * Auto-discover and forward inputs from all connected gamepads merged.
+ */
+ public static final int GAMEPAD_MODE_AUTO_ALL = 1;
+ /**
+ * User-managed list of gamepads (call {@link #setGamepadMode(int, long[])} with the array).
+ */
+ public static final int GAMEPAD_MODE_MANUAL = 2;
+
+ /**
+ * Mouse capture disabled.
+ */
+ public static final int MOUSE_CAPTURE_MODE_DISABLED = 0;
+ /**
+ * Mouse capture starts on mouse-down. Default on most platforms.
+ */
+ public static final int MOUSE_CAPTURE_MODE_ENABLED = 1;
+ /**
+ * Mouse capture starts only after a drag is detected. Default on X11 to mitigate debugger issues.
+ */
+ public static final int MOUSE_CAPTURE_MODE_ENABLED_AFTER_DRAG = 2;
+
+ protected static class Data {
+ protected long window;
+ protected int windowID;
+ protected long time;
+ protected boolean wantUpdateMonitors;
+
+ // Mouse handling
+ protected int mouseWindowID;
+ protected int mouseButtonsDown;
+ protected final long[] mouseCursors = new long[ImGuiMouseCursor.COUNT];
+ protected long mouseLastCursor;
+ protected int mousePendingLeaveFrame;
+ protected boolean mouseCanUseGlobalState;
+ protected int mouseCaptureMode;
+
+ // Gamepad handling
+ protected final List Public lifecycle ({@link #init(InitInfo)}, {@link #shutdown()}, {@link #newFrame()},
+ * {@link #prepareDrawData(long, long)}, {@link #renderDrawData(long, long, long)}) mirrors the
+ * upstream API. Note the upstream-mandated split between {@code prepareDrawData} (called outside
+ * any render pass — uploads vertex / index buffers and any pending texture data) and
+ * {@code renderDrawData} (called inside an active render pass — issues the draw commands).
+ *
+ * Multi-viewport renderer hooks and the per-frame {@code ImTextureData} upload path are not
+ * exposed: this binding still uses the legacy {@code ImFontAtlas#setTexID} flow (see
+ * {@link #createFontsTexture(long)}).
+ */
+public class ImGuiImplSdlGpu3 {
+ /**
+ * Mirrors {@code ImGui_ImplSDLGPU3_InitInfo}. Default values match the upstream C++ struct.
+ */
+ public static final class InitInfo {
+ public long device;
+ public int colorTargetFormat = SDL_GPU_TEXTUREFORMAT_INVALID;
+ public int msaaSamples = SDL_GPU_SAMPLECOUNT_1;
+ public int swapchainComposition = SDL_GPU_SWAPCHAINCOMPOSITION_SDR;
+ public int presentMode = SDL_GPU_PRESENTMODE_VSYNC;
+
+ public InitInfo() {
+ }
+
+ public InitInfo setDevice(final long device) {
+ this.device = device;
+ return this;
+ }
+
+ public InitInfo setColorTargetFormat(final int colorTargetFormat) {
+ this.colorTargetFormat = colorTargetFormat;
+ return this;
+ }
+
+ public InitInfo setMsaaSamples(final int msaaSamples) {
+ this.msaaSamples = msaaSamples;
+ return this;
+ }
+
+ public InitInfo setSwapchainComposition(final int swapchainComposition) {
+ this.swapchainComposition = swapchainComposition;
+ return this;
+ }
+
+ public InitInfo setPresentMode(final int presentMode) {
+ this.presentMode = presentMode;
+ return this;
+ }
+ }
+
+ private InitInfo initInfo;
+
+ // GPU resources
+ private long vertexShader;
+ private long fragmentShader;
+ private long pipeline;
+ private long samplerLinear;
+ private long fontTexture;
+
+ // Per-frame buffers, reused frame-to-frame (resized on demand).
+ private long vertexBuffer;
+ private long indexBuffer;
+ private long vertexTransferBuffer;
+ private long indexTransferBuffer;
+ private int vertexBufferSize;
+ private int indexBufferSize;
+
+ public final boolean init(final InitInfo info) {
+ if (info == null || info.device == 0L) {
+ throw new IllegalArgumentException("ImGuiImplSdlGpu3.init: InitInfo.device must be a valid SDL_GPUDevice handle");
+ }
+ this.initInfo = info;
+
+ final ImGuiIO io = ImGui.getIO();
+ io.setBackendRendererName("imgui_impl_sdlgpu3 (lwjgl)");
+ io.addBackendFlags(ImGuiBackendFlags.RendererHasVtxOffset);
+
+ createDeviceObjects();
+ return true;
+ }
+
+ public final void shutdown() {
+ destroyDeviceObjects();
+ final ImGuiIO io = ImGui.getIO();
+ io.setBackendRendererName(null);
+ io.removeBackendFlags(ImGuiBackendFlags.RendererHasVtxOffset | ImGuiBackendFlags.RendererHasViewports);
+ this.initInfo = null;
+ }
+
+ public final void newFrame() {
+ if (initInfo == null) {
+ return;
+ }
+ if (pipeline == 0L) {
+ createDeviceObjects();
+ }
+ if (fontTexture == 0L) {
+ createFontsTexture(0L);
+ }
+ }
+
+ public final void createDeviceObjects() {
+ if (initInfo == null) {
+ return;
+ }
+ final long device = initInfo.device;
+
+ // Pick shader format
+ final int formats = SDL_GetGPUShaderFormats(device);
+ final byte[] vertexBytes;
+ final byte[] fragmentBytes;
+ final int chosenFormat;
+ if ((formats & SDL_GPU_SHADERFORMAT_SPIRV) != 0) {
+ vertexBytes = ImGuiImplSdlGpu3Shaders.SPIRV_VERTEX;
+ fragmentBytes = ImGuiImplSdlGpu3Shaders.SPIRV_FRAGMENT;
+ chosenFormat = SDL_GPU_SHADERFORMAT_SPIRV;
+ } else if ((formats & SDL_GPU_SHADERFORMAT_DXBC) != 0) {
+ vertexBytes = ImGuiImplSdlGpu3Shaders.DXBC_VERTEX;
+ fragmentBytes = ImGuiImplSdlGpu3Shaders.DXBC_FRAGMENT;
+ chosenFormat = SDL_GPU_SHADERFORMAT_DXBC;
+ } else if ((formats & SDL_GPU_SHADERFORMAT_METALLIB) != 0) {
+ vertexBytes = ImGuiImplSdlGpu3Shaders.METALLIB_VERTEX;
+ fragmentBytes = ImGuiImplSdlGpu3Shaders.METALLIB_FRAGMENT;
+ chosenFormat = SDL_GPU_SHADERFORMAT_METALLIB;
+ } else {
+ throw new IllegalStateException("ImGuiImplSdlGpu3: SDL_GPU device does not support any shader format known to the backend");
+ }
+
+ // Create shaders. Metal (METALLIB) requires the "main0" entrypoint per upstream;
+ // SPIR-V / DXBC use the standard "main".
+ final String entrypointName = chosenFormat == SDL_GPU_SHADERFORMAT_METALLIB ? "main0" : "main";
+ vertexShader = createShader(device, vertexBytes, chosenFormat, SDL_GPU_SHADERSTAGE_VERTEX, 0, 1, entrypointName);
+ fragmentShader = createShader(device, fragmentBytes, chosenFormat, SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0, entrypointName);
+
+ // Create pipeline
+ try (MemoryStack stack = MemoryStack.stackPush()) {
+ final SDL_GPUVertexBufferDescription.Buffer vertexBufferDescriptions = SDL_GPUVertexBufferDescription.calloc(1, stack);
+ vertexBufferDescriptions.get(0)
+ .slot(0)
+ .pitch(ImDrawData.sizeOfImDrawVert())
+ .input_rate(SDL_GPU_VERTEXINPUTRATE_VERTEX)
+ .instance_step_rate(0);
+
+ final SDL_GPUVertexAttribute.Buffer vertexAttributes = SDL_GPUVertexAttribute.calloc(3, stack);
+ vertexAttributes.get(0).location(0).buffer_slot(0).format(SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2).offset(0);
+ vertexAttributes.get(1).location(1).buffer_slot(0).format(SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2).offset(8);
+ vertexAttributes.get(2).location(2).buffer_slot(0).format(SDL_GPU_VERTEXELEMENTFORMAT_UBYTE4_NORM).offset(16);
+
+ final SDL_GPUColorTargetBlendState blendState = SDL_GPUColorTargetBlendState.calloc(stack)
+ .enable_blend(true)
+ .src_color_blendfactor(SDL_GPU_BLENDFACTOR_SRC_ALPHA)
+ .dst_color_blendfactor(SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA)
+ .color_blend_op(SDL_GPU_BLENDOP_ADD)
+ .src_alpha_blendfactor(SDL_GPU_BLENDFACTOR_ONE)
+ .dst_alpha_blendfactor(SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA)
+ .alpha_blend_op(SDL_GPU_BLENDOP_ADD)
+ .color_write_mask((byte) 0xF)
+ .enable_color_write_mask(false);
+
+ final SDL_GPUColorTargetDescription.Buffer colorTargets = SDL_GPUColorTargetDescription.calloc(1, stack);
+ colorTargets.get(0).format(initInfo.colorTargetFormat).blend_state(blendState);
+
+ final SDL_GPUGraphicsPipelineCreateInfo info = SDL_GPUGraphicsPipelineCreateInfo.calloc(stack)
+ .vertex_shader(vertexShader)
+ .fragment_shader(fragmentShader)
+ .primitive_type(SDL_GPU_PRIMITIVETYPE_TRIANGLELIST)
+ .props(0);
+
+ final SDL_GPUVertexInputState vertexInputState = info.vertex_input_state();
+ vertexInputState.vertex_buffer_descriptions(vertexBufferDescriptions);
+ vertexInputState.num_vertex_buffers(1);
+ vertexInputState.vertex_attributes(vertexAttributes);
+ vertexInputState.num_vertex_attributes(3);
+
+ final SDL_GPURasterizerState rasterizer = info.rasterizer_state();
+ rasterizer.fill_mode(SDL_GPU_FILLMODE_FILL);
+ rasterizer.cull_mode(SDL_GPU_CULLMODE_NONE);
+ rasterizer.front_face(SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE);
+ rasterizer.enable_depth_bias(false);
+ rasterizer.enable_depth_clip(false);
+
+ info.multisample_state().sample_count(initInfo.msaaSamples);
+
+ // depth_stencil_state left zeroed (no depth/stencil)
+
+ final SDL_GPUGraphicsPipelineTargetInfo targetInfo = info.target_info();
+ targetInfo.color_target_descriptions(colorTargets);
+ targetInfo.num_color_targets(1);
+ targetInfo.has_depth_stencil_target(false);
+
+ pipeline = SDL_CreateGPUGraphicsPipeline(device, info);
+ if (pipeline == 0L) {
+ throw new IllegalStateException("SDL_CreateGPUGraphicsPipeline failed");
+ }
+
+ // Sampler
+ final SDL_GPUSamplerCreateInfo samplerInfo = SDL_GPUSamplerCreateInfo.calloc(stack)
+ .min_filter(SDL_GPU_FILTER_LINEAR)
+ .mag_filter(SDL_GPU_FILTER_LINEAR)
+ .mipmap_mode(SDL_GPU_SAMPLERMIPMAPMODE_LINEAR)
+ .address_mode_u(SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE)
+ .address_mode_v(SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE)
+ .address_mode_w(SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE)
+ .mip_lod_bias(0.0f)
+ .min_lod(-1000.0f)
+ .max_lod(1000.0f)
+ .enable_anisotropy(false)
+ .enable_compare(false);
+ samplerLinear = SDL_CreateGPUSampler(device, samplerInfo);
+ }
+ }
+
+ private long createShader(final long device, final byte[] code, final int format, final int stage,
+ final int numSamplers, final int numUniformBuffers, final String entrypointName) {
+ final ByteBuffer codeBuffer = MemoryUtil.memAlloc(code.length);
+ try {
+ codeBuffer.put(code).flip();
+ try (MemoryStack stack = MemoryStack.stackPush()) {
+ final ByteBuffer entrypoint = stack.UTF8(entrypointName);
+ final SDL_GPUShaderCreateInfo info = SDL_GPUShaderCreateInfo.calloc(stack)
+ .code(codeBuffer)
+ .entrypoint(entrypoint)
+ .format(format)
+ .stage(stage)
+ .num_samplers(numSamplers)
+ .num_storage_textures(0)
+ .num_storage_buffers(0)
+ .num_uniform_buffers(numUniformBuffers)
+ .props(0);
+ final long handle = SDL_CreateGPUShader(device, info);
+ if (handle == 0L) {
+ throw new IllegalStateException("SDL_CreateGPUShader failed (stage=" + stage + ")");
+ }
+ return handle;
+ }
+ } finally {
+ MemoryUtil.memFree(codeBuffer);
+ }
+ }
+
+ /**
+ * Builds the font atlas texture and stores its handle on the atlas via {@link ImFontAtlas#setTexID(long)}.
+ *
+ * @param uploadCommandBuffer optional command buffer used to upload pixels; pass {@code 0} to acquire one internally
+ */
+ public void createFontsTexture(final long uploadCommandBuffer) {
+ if (initInfo == null) {
+ return;
+ }
+ final long device = initInfo.device;
+ final ImFontAtlas atlas = ImGui.getIO().getFonts();
+
+ final ImInt width = new ImInt();
+ final ImInt height = new ImInt();
+ final ByteBuffer pixels = atlas.getTexDataAsRGBA32(width, height);
+ final int w = width.get();
+ final int h = height.get();
+ final int sizeInBytes = w * h * 4;
+
+ try (MemoryStack stack = MemoryStack.stackPush()) {
+ final SDL_GPUTextureCreateInfo texInfo = SDL_GPUTextureCreateInfo.calloc(stack)
+ .type(SDL_GPU_TEXTURETYPE_2D)
+ .format(SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM)
+ .usage(SDL_GPU_TEXTUREUSAGE_SAMPLER)
+ .width(w)
+ .height(h)
+ .layer_count_or_depth(1)
+ .num_levels(1)
+ .sample_count(SDL_GPU_SAMPLECOUNT_1)
+ .props(0);
+ fontTexture = SDL_CreateGPUTexture(device, texInfo);
+ if (fontTexture == 0L) {
+ throw new IllegalStateException("SDL_CreateGPUTexture failed for font atlas");
+ }
+
+ final SDL_GPUTransferBufferCreateInfo tbInfo = SDL_GPUTransferBufferCreateInfo.calloc(stack)
+ .usage(SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD)
+ .size(sizeInBytes)
+ .props(0);
+ final long transferBuffer = SDL_CreateGPUTransferBuffer(device, tbInfo);
+ if (transferBuffer == 0L) {
+ throw new IllegalStateException("SDL_CreateGPUTransferBuffer failed for font atlas");
+ }
+
+ final ByteBuffer mapped = SDL_MapGPUTransferBuffer(device, transferBuffer, false, sizeInBytes);
+ if (mapped == null) {
+ SDL_ReleaseGPUTransferBuffer(device, transferBuffer);
+ throw new IllegalStateException("SDL_MapGPUTransferBuffer failed for font atlas");
+ }
+ final ByteBuffer src = pixels.duplicate();
+ src.limit(Math.min(src.capacity(), sizeInBytes));
+ mapped.put(src);
+ SDL_UnmapGPUTransferBuffer(device, transferBuffer);
+
+ long cmd = uploadCommandBuffer;
+ final boolean ownsCommandBuffer = cmd == 0L;
+ if (ownsCommandBuffer) {
+ cmd = org.lwjgl.sdl.SDLGPU.SDL_AcquireGPUCommandBuffer(device);
+ }
+
+ final long copyPass = SDL_BeginGPUCopyPass(cmd);
+
+ final SDL_GPUTextureTransferInfo srcInfo = SDL_GPUTextureTransferInfo.calloc(stack)
+ .transfer_buffer(transferBuffer)
+ .offset(0)
+ .pixels_per_row(w)
+ .rows_per_layer(h);
+ final SDL_GPUTextureRegion dstRegion = SDL_GPUTextureRegion.calloc(stack)
+ .texture(fontTexture)
+ .mip_level(0)
+ .layer(0)
+ .x(0)
+ .y(0)
+ .z(0)
+ .w(w)
+ .h(h)
+ .d(1);
+ SDL_UploadToGPUTexture(copyPass, srcInfo, dstRegion, false);
+
+ SDL_EndGPUCopyPass(copyPass);
+
+ if (ownsCommandBuffer) {
+ org.lwjgl.sdl.SDLGPU.SDL_SubmitGPUCommandBuffer(cmd);
+ }
+
+ SDL_ReleaseGPUTransferBuffer(device, transferBuffer);
+ }
+
+ atlas.setTexID(fontTexture);
+ }
+
+ public final void destroyDeviceObjects() {
+ if (initInfo == null) {
+ return;
+ }
+ final long device = initInfo.device;
+ if (fontTexture != 0L) {
+ SDL_ReleaseGPUTexture(device, fontTexture);
+ fontTexture = 0L;
+ ImGui.getIO().getFonts().setTexID(0);
+ }
+ if (vertexBuffer != 0L) {
+ SDL_ReleaseGPUBuffer(device, vertexBuffer);
+ vertexBuffer = 0L;
+ }
+ if (indexBuffer != 0L) {
+ SDL_ReleaseGPUBuffer(device, indexBuffer);
+ indexBuffer = 0L;
+ }
+ if (vertexTransferBuffer != 0L) {
+ SDL_ReleaseGPUTransferBuffer(device, vertexTransferBuffer);
+ vertexTransferBuffer = 0L;
+ }
+ if (indexTransferBuffer != 0L) {
+ SDL_ReleaseGPUTransferBuffer(device, indexTransferBuffer);
+ indexTransferBuffer = 0L;
+ }
+ if (samplerLinear != 0L) {
+ SDL_ReleaseGPUSampler(device, samplerLinear);
+ samplerLinear = 0L;
+ }
+ if (pipeline != 0L) {
+ SDL_ReleaseGPUGraphicsPipeline(device, pipeline);
+ pipeline = 0L;
+ }
+ if (vertexShader != 0L) {
+ SDL_ReleaseGPUShader(device, vertexShader);
+ vertexShader = 0L;
+ }
+ if (fragmentShader != 0L) {
+ SDL_ReleaseGPUShader(device, fragmentShader);
+ fragmentShader = 0L;
+ }
+ vertexBufferSize = 0;
+ indexBufferSize = 0;
+ }
+
+ /**
+ * Mirrors {@code ImGui_ImplSDLGPU3_PrepareDrawData}. Must be called outside any active render pass.
+ */
+ public void prepareDrawData(final long drawDataPtr, final long commandBufferPtr) {
+ if (drawDataPtr == 0L || commandBufferPtr == 0L || initInfo == null) {
+ return;
+ }
+ final ImDrawData drawData = new ImDrawData(drawDataPtr);
+ if (drawData.getCmdListsCount() <= 0 || drawData.getTotalVtxCount() <= 0) {
+ return;
+ }
+
+ final long device = initInfo.device;
+ final int vertexSize = drawData.getTotalVtxCount() * ImDrawData.sizeOfImDrawVert();
+ final int indexSize = drawData.getTotalIdxCount() * ImDrawData.sizeOfImDrawIdx();
+
+ // Resize buffers if needed (grow only)
+ ensureVertexBuffer(device, vertexSize);
+ ensureIndexBuffer(device, indexSize);
+
+ // Map transfer buffers and copy interleaved cmd-list vertex/index data.
+ final ByteBuffer vMapped = SDL_MapGPUTransferBuffer(device, vertexTransferBuffer, true, vertexBufferSize);
+ final ByteBuffer iMapped = SDL_MapGPUTransferBuffer(device, indexTransferBuffer, true, indexBufferSize);
+ if (vMapped == null || iMapped == null) {
+ if (vMapped != null) {
+ SDL_UnmapGPUTransferBuffer(device, vertexTransferBuffer);
+ }
+ if (iMapped != null) {
+ SDL_UnmapGPUTransferBuffer(device, indexTransferBuffer);
+ }
+ return;
+ }
+ // NOTE: ImDrawData#getCmdListVtxBufferData and #getCmdListIdxBufferData reuse
+ // a single shared ByteBuffer. Each call overwrites the previous content, so the
+ // vertex copy must complete before the next index call (and vice versa). Read+copy
+ // one at a time per cmd-list rather than reading both then copying both.
+ int vOffsetBytes = 0;
+ int iOffsetBytes = 0;
+ for (int n = 0; n < drawData.getCmdListsCount(); n++) {
+ final ByteBuffer vtx = drawData.getCmdListVtxBufferData(n);
+ final int vlen = vtx.remaining();
+ vMapped.position(vOffsetBytes);
+ vMapped.put(vtx);
+ vOffsetBytes += vlen;
+
+ final ByteBuffer idx = drawData.getCmdListIdxBufferData(n);
+ final int ilen = idx.remaining();
+ iMapped.position(iOffsetBytes);
+ iMapped.put(idx);
+ iOffsetBytes += ilen;
+ }
+ SDL_UnmapGPUTransferBuffer(device, vertexTransferBuffer);
+ SDL_UnmapGPUTransferBuffer(device, indexTransferBuffer);
+
+ // Copy pass: transfer → device-local buffers
+ final long copyPass = SDL_BeginGPUCopyPass(commandBufferPtr);
+ try (MemoryStack stack = MemoryStack.stackPush()) {
+ final SDL_GPUTransferBufferLocation vSrc = SDL_GPUTransferBufferLocation.calloc(stack)
+ .transfer_buffer(vertexTransferBuffer)
+ .offset(0);
+ final SDL_GPUBufferRegion vDst = SDL_GPUBufferRegion.calloc(stack)
+ .buffer(vertexBuffer)
+ .offset(0)
+ .size(vertexSize);
+ SDL_UploadToGPUBuffer(copyPass, vSrc, vDst, true);
+
+ final SDL_GPUTransferBufferLocation iSrc = SDL_GPUTransferBufferLocation.calloc(stack)
+ .transfer_buffer(indexTransferBuffer)
+ .offset(0);
+ final SDL_GPUBufferRegion iDst = SDL_GPUBufferRegion.calloc(stack)
+ .buffer(indexBuffer)
+ .offset(0)
+ .size(indexSize);
+ SDL_UploadToGPUBuffer(copyPass, iSrc, iDst, true);
+ }
+ SDL_EndGPUCopyPass(copyPass);
+ }
+
+ private void ensureVertexBuffer(final long device, final int sizeNeeded) {
+ if (vertexBufferSize >= sizeNeeded && vertexBuffer != 0L) {
+ return;
+ }
+ final int newSize = Math.max(sizeNeeded, vertexBufferSize == 0 ? 16 * 1024 : vertexBufferSize * 2);
+ if (vertexBuffer != 0L) {
+ SDL_ReleaseGPUBuffer(device, vertexBuffer);
+ }
+ if (vertexTransferBuffer != 0L) {
+ SDL_ReleaseGPUTransferBuffer(device, vertexTransferBuffer);
+ }
+ try (MemoryStack stack = MemoryStack.stackPush()) {
+ vertexBuffer = SDL_CreateGPUBuffer(device, SDL_GPUBufferCreateInfo.calloc(stack)
+ .usage(SDL_GPU_BUFFERUSAGE_VERTEX)
+ .size(newSize)
+ .props(0));
+ vertexTransferBuffer = SDL_CreateGPUTransferBuffer(device, SDL_GPUTransferBufferCreateInfo.calloc(stack)
+ .usage(SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD)
+ .size(newSize)
+ .props(0));
+ }
+ vertexBufferSize = newSize;
+ }
+
+ private void ensureIndexBuffer(final long device, final int sizeNeeded) {
+ if (indexBufferSize >= sizeNeeded && indexBuffer != 0L) {
+ return;
+ }
+ final int newSize = Math.max(sizeNeeded, indexBufferSize == 0 ? 16 * 1024 : indexBufferSize * 2);
+ if (indexBuffer != 0L) {
+ SDL_ReleaseGPUBuffer(device, indexBuffer);
+ }
+ if (indexTransferBuffer != 0L) {
+ SDL_ReleaseGPUTransferBuffer(device, indexTransferBuffer);
+ }
+ try (MemoryStack stack = MemoryStack.stackPush()) {
+ indexBuffer = SDL_CreateGPUBuffer(device, SDL_GPUBufferCreateInfo.calloc(stack)
+ .usage(SDL_GPU_BUFFERUSAGE_INDEX)
+ .size(newSize)
+ .props(0));
+ indexTransferBuffer = SDL_CreateGPUTransferBuffer(device, SDL_GPUTransferBufferCreateInfo.calloc(stack)
+ .usage(SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD)
+ .size(newSize)
+ .props(0));
+ }
+ indexBufferSize = newSize;
+ }
+
+ public final void renderDrawData(final long drawDataPtr, final long commandBufferPtr, final long renderPassPtr) {
+ renderDrawData(drawDataPtr, commandBufferPtr, renderPassPtr, 0L);
+ }
+
+ public final void renderDrawData(final long drawDataPtr, final long commandBufferPtr, final long renderPassPtr, final long pipelineOverride) {
+ if (drawDataPtr == 0L || commandBufferPtr == 0L || renderPassPtr == 0L || initInfo == null) {
+ return;
+ }
+ final ImDrawData drawData = new ImDrawData(drawDataPtr);
+ if (drawData.getCmdListsCount() <= 0 || drawData.getTotalVtxCount() <= 0) {
+ return;
+ }
+
+ final int fbWidth = (int) (drawData.getDisplaySizeX() * drawData.getFramebufferScaleX());
+ final int fbHeight = (int) (drawData.getDisplaySizeY() * drawData.getFramebufferScaleY());
+ if (fbWidth <= 0 || fbHeight <= 0) {
+ return;
+ }
+
+ final long usePipeline = pipelineOverride != 0L ? pipelineOverride : pipeline;
+
+ try (MemoryStack stack = MemoryStack.stackPush()) {
+ // Viewport
+ final SDL_GPUViewport viewport = SDL_GPUViewport.calloc(stack)
+ .x(0.0f).y(0.0f).w(fbWidth).h(fbHeight).min_depth(0.0f).max_depth(1.0f);
+ SDL_SetGPUViewport(renderPassPtr, viewport);
+
+ // Pipeline
+ SDL_BindGPUGraphicsPipeline(renderPassPtr, usePipeline);
+
+ // Vertex / index buffers
+ final SDL_GPUBufferBinding.Buffer vBindings = SDL_GPUBufferBinding.calloc(1, stack);
+ vBindings.get(0).buffer(vertexBuffer).offset(0);
+ SDL_BindGPUVertexBuffers(renderPassPtr, 0, vBindings);
+
+ final SDL_GPUBufferBinding iBinding = SDL_GPUBufferBinding.calloc(stack)
+ .buffer(indexBuffer).offset(0);
+ final int indexElementSize = ImDrawData.sizeOfImDrawIdx() == 2
+ ? SDL_GPU_INDEXELEMENTSIZE_16BIT
+ : SDL_GPU_INDEXELEMENTSIZE_32BIT;
+ SDL_BindGPUIndexBuffer(renderPassPtr, iBinding, indexElementSize);
+
+ // Vertex uniform: { vec2 scale, vec2 translation } — matches upstream UBO layout.
+ final float scaleX = 2.0f / drawData.getDisplaySizeX();
+ final float scaleY = 2.0f / drawData.getDisplaySizeY();
+ final FloatBuffer ubo = stack.mallocFloat(4);
+ ubo.put(0, scaleX);
+ ubo.put(1, scaleY);
+ ubo.put(2, -1.0f - drawData.getDisplayPosX() * scaleX);
+ ubo.put(3, -1.0f - drawData.getDisplayPosY() * scaleY);
+ SDL_PushGPUVertexUniformData(commandBufferPtr, 0, ubo);
+
+ // Will project scissor/clipping rectangles into framebuffer space
+ final float clipOffX = drawData.getDisplayPosX();
+ final float clipOffY = drawData.getDisplayPosY();
+ final float clipScaleX = drawData.getFramebufferScaleX();
+ final float clipScaleY = drawData.getFramebufferScaleY();
+
+ int globalVtxOffset = 0;
+ int globalIdxOffset = 0;
+ final SDL_GPUTextureSamplerBinding.Buffer samplerBinding = SDL_GPUTextureSamplerBinding.calloc(1, stack);
+ final SDL_Rect scissorRect = SDL_Rect.calloc(stack);
+ final imgui.ImVec4 clipRect = new imgui.ImVec4();
+
+ for (int n = 0; n < drawData.getCmdListsCount(); n++) {
+ final int cmdCount = drawData.getCmdListCmdBufferSize(n);
+ for (int cmdIdx = 0; cmdIdx < cmdCount; cmdIdx++) {
+ drawData.getCmdListCmdBufferClipRect(clipRect, n, cmdIdx);
+ final float clipMinX = (clipRect.x - clipOffX) * clipScaleX;
+ final float clipMinY = (clipRect.y - clipOffY) * clipScaleY;
+ final float clipMaxXf = (clipRect.z - clipOffX) * clipScaleX;
+ final float clipMaxYf = (clipRect.w - clipOffY) * clipScaleY;
+ final float clipMinXc = Math.max(0.0f, clipMinX);
+ final float clipMinYc = Math.max(0.0f, clipMinY);
+ final float clipMaxXc = Math.min(fbWidth, clipMaxXf);
+ final float clipMaxYc = Math.min(fbHeight, clipMaxYf);
+ if (clipMaxXc <= clipMinXc || clipMaxYc <= clipMinYc) {
+ continue;
+ }
+ scissorRect.x((int) clipMinXc).y((int) clipMinYc)
+ .w((int) (clipMaxXc - clipMinXc)).h((int) (clipMaxYc - clipMinYc));
+ SDL_SetGPUScissor(renderPassPtr, scissorRect);
+
+ final long texId = drawData.getCmdListCmdBufferTextureId(n, cmdIdx);
+ samplerBinding.get(0).texture(texId == 0L ? fontTexture : texId).sampler(samplerLinear);
+ SDL_BindGPUFragmentSamplers(renderPassPtr, 0, samplerBinding);
+
+ final int elemCount = drawData.getCmdListCmdBufferElemCount(n, cmdIdx);
+ final int idxOffset = drawData.getCmdListCmdBufferIdxOffset(n, cmdIdx);
+ final int vtxOffset = drawData.getCmdListCmdBufferVtxOffset(n, cmdIdx);
+ SDL_DrawGPUIndexedPrimitives(renderPassPtr,
+ elemCount, 1,
+ globalIdxOffset + idxOffset,
+ globalVtxOffset + vtxOffset,
+ 0);
+ }
+ globalVtxOffset += drawData.getCmdListVtxBufferSize(n);
+ globalIdxOffset += drawData.getCmdListIdxBufferSize(n);
+ }
+ }
+ }
+
+ /**
+ * Not used in this binding (the legacy {@link #createFontsTexture(long)} flow is used instead since the binding does
+ * not yet expose {@code ImTextureData}).
+ *
+ * @param imTextureDataPtr unused
+ */
+ public void updateTexture(final long imTextureDataPtr) {
+ // TODO: not ported — ImTextureData is not exposed in the imgui-binding yet.
+ }
+
+ public final InitInfo getInitInfo() {
+ return initInfo;
+ }
+}
diff --git a/imgui-lwjgl3/src/main/java/imgui/sdl3/ImGuiImplSdlGpu3Shaders.java b/imgui-lwjgl3/src/main/java/imgui/sdl3/ImGuiImplSdlGpu3Shaders.java
new file mode 100644
index 00000000..d270d62e
--- /dev/null
+++ b/imgui-lwjgl3/src/main/java/imgui/sdl3/ImGuiImplSdlGpu3Shaders.java
@@ -0,0 +1,65 @@
+package imgui.sdl3;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Precompiled shader blobs ported 1-to-1 from upstream {@code imgui_impl_sdlgpu3_shaders.h}.
+ *
+ * The byte content of each blob matches the upstream {@code const uint8_t name[len]} array
+ * byte-for-byte. Blobs are bundled as classpath resources under
+ * {@code imgui/sdl3/shaders/*.bin} and lazily loaded on first access — direct {@code byte[]}
+ * literals would exceed the 64 KB Java method-code limit on a single static initializer.
+ *
+ * Only the desktop variants (SPIR-V / DXBC / macOS Metal) are exposed here. The upstream
+ * iOS / iOS-Simulator Metal blobs are not ported (desktop LWJGL only).
+ */
+public final class ImGuiImplSdlGpu3Shaders {
+ /**
+ * Upstream {@code spirv_vertex[1732]}.
+ */
+ public static final byte[] SPIRV_VERTEX = load("spirv_vertex.bin");
+ /**
+ * Upstream {@code spirv_fragment[844]}.
+ */
+ public static final byte[] SPIRV_FRAGMENT = load("spirv_fragment.bin");
+ /**
+ * Upstream {@code dxbc_vertex[1064]}.
+ */
+ public static final byte[] DXBC_VERTEX = load("dxbc_vertex.bin");
+ /**
+ * Upstream {@code dxbc_fragment[744]}.
+ */
+ public static final byte[] DXBC_FRAGMENT = load("dxbc_fragment.bin");
+ /**
+ * Upstream macOS {@code metallib_vertex[3892]}.
+ */
+ public static final byte[] METALLIB_VERTEX = load("metallib_vertex.bin");
+ /**
+ * Upstream macOS {@code metallib_fragment[3787]}.
+ */
+ public static final byte[] METALLIB_FRAGMENT = load("metallib_fragment.bin");
+
+ private ImGuiImplSdlGpu3Shaders() {
+ }
+
+ private static byte[] load(final String name) {
+ final String resource = "/imgui/sdl3/shaders/" + name;
+ try (InputStream in = ImGuiImplSdlGpu3Shaders.class.getResourceAsStream(resource)) {
+ if (in == null) {
+ throw new IllegalStateException("Missing classpath resource: " + resource);
+ }
+ try (ByteArrayOutputStream buf = new ByteArrayOutputStream()) {
+ final byte[] chunk = new byte[4096];
+ int n;
+ while ((n = in.read(chunk)) > 0) {
+ buf.write(chunk, 0, n);
+ }
+ return buf.toByteArray();
+ }
+ } catch (IOException ex) {
+ throw new IllegalStateException("Failed to load shader resource: " + resource, ex);
+ }
+ }
+}
diff --git a/imgui-lwjgl3/src/main/resources/imgui/sdl3/shaders/dxbc_fragment.bin b/imgui-lwjgl3/src/main/resources/imgui/sdl3/shaders/dxbc_fragment.bin
new file mode 100644
index 00000000..005ebbc7
Binary files /dev/null and b/imgui-lwjgl3/src/main/resources/imgui/sdl3/shaders/dxbc_fragment.bin differ
diff --git a/imgui-lwjgl3/src/main/resources/imgui/sdl3/shaders/dxbc_vertex.bin b/imgui-lwjgl3/src/main/resources/imgui/sdl3/shaders/dxbc_vertex.bin
new file mode 100644
index 00000000..0270f723
Binary files /dev/null and b/imgui-lwjgl3/src/main/resources/imgui/sdl3/shaders/dxbc_vertex.bin differ
diff --git a/imgui-lwjgl3/src/main/resources/imgui/sdl3/shaders/metallib_fragment.bin b/imgui-lwjgl3/src/main/resources/imgui/sdl3/shaders/metallib_fragment.bin
new file mode 100644
index 00000000..57a4c81e
Binary files /dev/null and b/imgui-lwjgl3/src/main/resources/imgui/sdl3/shaders/metallib_fragment.bin differ
diff --git a/imgui-lwjgl3/src/main/resources/imgui/sdl3/shaders/metallib_vertex.bin b/imgui-lwjgl3/src/main/resources/imgui/sdl3/shaders/metallib_vertex.bin
new file mode 100644
index 00000000..f798c8c2
Binary files /dev/null and b/imgui-lwjgl3/src/main/resources/imgui/sdl3/shaders/metallib_vertex.bin differ
diff --git a/imgui-lwjgl3/src/main/resources/imgui/sdl3/shaders/spirv_fragment.bin b/imgui-lwjgl3/src/main/resources/imgui/sdl3/shaders/spirv_fragment.bin
new file mode 100644
index 00000000..d3456140
Binary files /dev/null and b/imgui-lwjgl3/src/main/resources/imgui/sdl3/shaders/spirv_fragment.bin differ
diff --git a/imgui-lwjgl3/src/main/resources/imgui/sdl3/shaders/spirv_vertex.bin b/imgui-lwjgl3/src/main/resources/imgui/sdl3/shaders/spirv_vertex.bin
new file mode 100644
index 00000000..35421bfa
Binary files /dev/null and b/imgui-lwjgl3/src/main/resources/imgui/sdl3/shaders/spirv_vertex.bin differ