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. * *

Life-cycle

*

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: * *

    *
  1. {@link #configure(Configuration)} It's called before window creation, so only basic application setups are expected.
  2. - *
  3. {@link #initWindow(Configuration)} Method creates application window.
  4. - *
  5. {@link #initImGui(Configuration)} Method initializes Dear ImGui context. Could be used to do Dear ImGui setup as well.
  6. + *
  7. {@link Window#init(Configuration)} The window's {@code init} creates the OS window and triggers + * {@link #initImGui(Configuration)} to set up the Dear ImGui context.
  8. *
  9. {@link #preRun()} Method called once, before application loop.
  10. *
  11. {@link #preProcess()} Method called every frame, before {@link #process()}.
  12. *
  13. {@link #process()} Method is meant to be overridden with user application logic.
  14. *
  15. {@link #postProcess()} Method called every frame, after {@link #process()}.
  16. *
  17. {@link #postRun()} Method called once, after application loop.
  18. *
  19. {@link #disposeImGui()} Destroys Dear ImGui context.
  20. - *
  21. {@link #disposeWindow()} Destroys application window.
  22. + *
  23. {@link Window#dispose()} The window's {@code dispose} releases platform/renderer resources.
  24. *
* *

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 gamepads = new ArrayList<>(); + protected int gamepadMode = GAMEPAD_MODE_AUTO_FIRST; + protected boolean wantUpdateGamepadsList; + } + + protected Data data = null; + + protected ImStrSupplier getClipboardTextFn() { + return new ImStrSupplier() { + @Override + public String get() { + if (SDL_HasClipboardText()) { + final String clipboardString = SDL_GetClipboardText(); + return clipboardString != null ? clipboardString : ""; + } + return null; + } + }; + } + + protected ImStrConsumer setClipboardTextFn() { + return new ImStrConsumer() { + @Override + public void accept(final String text) { + if (text != null) { + SDL_SetClipboardText(text); + } + } + }; + } + + /** + * Initializes the SDL3 platform backend bound to the given native {@code SDL_Window} handle. + * Mirrors {@code ImGui_ImplSDL3_Init}. + * + * @param sdlWindow native {@code SDL_Window*} pointer + * @return {@code true} on success + */ + public boolean init(final long sdlWindow) { + final ImGuiIO io = ImGui.getIO(); + // IM_ASSERT(io.BackendPlatformUserData == NULL && "Already initialized a platform backend!"); + + // Setup backend capabilities flags + io.setBackendPlatformName("imgui_impl_sdl3 (lwjgl)"); + io.addBackendFlags(ImGuiBackendFlags.HasMouseCursors); + io.addBackendFlags(ImGuiBackendFlags.HasSetMousePos); + + data = new Data(); + data.window = sdlWindow; + data.windowID = SDL_GetWindowID(sdlWindow); + data.mouseCanUseGlobalState = false; + data.mouseCaptureMode = MOUSE_CAPTURE_MODE_DISABLED; + + // Gamepad handling + data.gamepadMode = GAMEPAD_MODE_AUTO_FIRST; + data.wantUpdateGamepadsList = true; + + // Load mouse cursors + data.mouseCursors[ImGuiMouseCursor.Arrow] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_DEFAULT); + data.mouseCursors[ImGuiMouseCursor.TextInput] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_TEXT); + data.mouseCursors[ImGuiMouseCursor.ResizeAll] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_MOVE); + data.mouseCursors[ImGuiMouseCursor.ResizeNS] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_NS_RESIZE); + data.mouseCursors[ImGuiMouseCursor.ResizeEW] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_EW_RESIZE); + data.mouseCursors[ImGuiMouseCursor.ResizeNESW] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_NESW_RESIZE); + data.mouseCursors[ImGuiMouseCursor.ResizeNWSE] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_NWSE_RESIZE); + data.mouseCursors[ImGuiMouseCursor.Hand] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_POINTER); + data.mouseCursors[ImGuiMouseCursor.Wait] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_WAIT); + data.mouseCursors[ImGuiMouseCursor.Progress] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_PROGRESS); + data.mouseCursors[ImGuiMouseCursor.NotAllowed] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_NOT_ALLOWED); + + io.setGetClipboardTextFn(getClipboardTextFn()); + io.setSetClipboardTextFn(setClipboardTextFn()); + + // TODO: not ported — Platform_SetImeDataFn / Platform_OpenInShellFn registrations on ImGuiPlatformIO require C function pointers. + + // From 2.0.5: Set SDL hint to receive mouse click events on window focus + SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1"); + // From 2.0.22: Disable auto-capture, this is preventing drag and drop across multiple windows + SDL_SetHint(SDL_HINT_MOUSE_AUTO_CAPTURE, "0"); + // SDL 3.x : see https://github.com/libsdl-org/SDL/issues/6659 + SDL_SetHint("SDL_BORDERLESS_WINDOWED_STYLE", "0"); + + // TODO: not ported — multi-viewport (ImGui_ImplSDL3_InitMultiViewportSupport) requires C function pointers. + + return true; + } + + /** + * Should technically take a {@code SDL_GLContext} but the parameter is ignored on non-OpenGL paths. + */ + public final boolean initForOpenGL(final long sdlWindow, final long sdlGlContext) { + return init(sdlWindow); + } + + public final boolean initForVulkan(final long sdlWindow) { + return init(sdlWindow); + } + + public final boolean initForD3D(final long sdlWindow) { + return init(sdlWindow); + } + + public final boolean initForMetal(final long sdlWindow) { + return init(sdlWindow); + } + + public final boolean initForSDLRenderer(final long sdlWindow, final long sdlRenderer) { + return init(sdlWindow); + } + + public final boolean initForSDLGPU(final long sdlWindow) { + return init(sdlWindow); + } + + public final boolean initForOther(final long sdlWindow) { + return init(sdlWindow); + } + + /** + * Mirrors {@code ImGui_ImplSDL3_Shutdown}. + */ + public void shutdown() { + final ImGuiIO io = ImGui.getIO(); + + // TODO: not ported — multi-viewport shutdown. + + for (int i = 0; i < ImGuiMouseCursor.COUNT; i++) { + if (data.mouseCursors[i] != 0L) { + SDL_DestroyCursor(data.mouseCursors[i]); + data.mouseCursors[i] = 0L; + } + } + closeGamepads(); + + io.setBackendPlatformName(null); + io.removeBackendFlags(ImGuiBackendFlags.HasMouseCursors + | ImGuiBackendFlags.HasSetMousePos + | ImGuiBackendFlags.HasGamepad + | ImGuiBackendFlags.PlatformHasViewports + | ImGuiBackendFlags.HasMouseHoveredViewport + | ImGuiBackendFlags.HasParentViewport); + + data = null; + } + + /** + * Mirrors {@code ImGui_ImplSDL3_SetMouseCaptureMode}. + */ + public void setMouseCaptureMode(final int mode) { + if (mode == MOUSE_CAPTURE_MODE_DISABLED && data.mouseCaptureMode != MOUSE_CAPTURE_MODE_DISABLED) { + SDL_CaptureMouse(false); + } + data.mouseCaptureMode = mode; + } + + /** + * Mirrors {@code ImGui_ImplSDL3_SetGamepadMode}. + * + * @param mode one of {@link #GAMEPAD_MODE_AUTO_FIRST}, {@link #GAMEPAD_MODE_AUTO_ALL}, {@link #GAMEPAD_MODE_MANUAL} + * @param manualGamepads array of gamepad handles when {@code mode == GAMEPAD_MODE_MANUAL}, otherwise {@code null} + */ + public void setGamepadMode(final int mode, final long[] manualGamepads) { + closeGamepads(); + if (mode == GAMEPAD_MODE_MANUAL) { + if (manualGamepads != null) { + for (final long g : manualGamepads) { + data.gamepads.add(g); + } + } + } else { + data.wantUpdateGamepadsList = true; + } + data.gamepadMode = mode; + } + + /** + * Forwards an SDL event into Dear ImGui. Mirrors {@code ImGui_ImplSDL3_ProcessEvent}. + * + * @param sdlEventPtr native {@code SDL_Event*} pointer (from {@code SDL_PollEvent}) + * @return {@code true} when the event was consumed by the backend + */ + public boolean processEvent(final long sdlEventPtr) { + final SDL_Event event = SDL_Event.create(sdlEventPtr); + final ImGuiIO io = ImGui.getIO(); + final int type = event.type(); + + switch (type) { + case SDL_EVENT_MOUSE_MOTION: + return processMouseMotion(event, io); + case SDL_EVENT_MOUSE_WHEEL: + return processMouseWheel(event, io); + case SDL_EVENT_MOUSE_BUTTON_DOWN: + case SDL_EVENT_MOUSE_BUTTON_UP: + return processMouseButton(event, io, type); + case SDL_EVENT_TEXT_INPUT: + return processTextInput(event, io); + case SDL_EVENT_KEY_DOWN: + case SDL_EVENT_KEY_UP: + return processKey(event, io, type); + case SDL_EVENT_DISPLAY_ORIENTATION: + case SDL_EVENT_DISPLAY_ADDED: + case SDL_EVENT_DISPLAY_REMOVED: + case SDL_EVENT_DISPLAY_MOVED: + case SDL_EVENT_DISPLAY_CONTENT_SCALE_CHANGED: + case SDL_EVENT_DISPLAY_USABLE_BOUNDS_CHANGED: + data.wantUpdateMonitors = true; + return true; + case SDL_EVENT_WINDOW_MOUSE_ENTER: + return processWindowMouseEnter(event); + case SDL_EVENT_WINDOW_MOUSE_LEAVE: + return processWindowMouseLeave(event); + case SDL_EVENT_WINDOW_FOCUS_GAINED: + case SDL_EVENT_WINDOW_FOCUS_LOST: + return processWindowFocus(event, io, type); + case SDL_EVENT_WINDOW_CLOSE_REQUESTED: + case SDL_EVENT_WINDOW_MOVED: + case SDL_EVENT_WINDOW_RESIZED: + // Single-viewport: nothing to forward to ImGui; user code reacts via SDL events directly. + // TODO: not ported — viewport->PlatformRequestClose / Move / Resize hooks are multi-viewport only. + return true; + case SDL_EVENT_GAMEPAD_ADDED: + case SDL_EVENT_GAMEPAD_REMOVED: + data.wantUpdateGamepadsList = true; + return true; + default: + return false; + } + } + + private boolean processMouseMotion(final SDL_Event event, final ImGuiIO io) { + final SDL_MouseMotionEvent motion = event.motion(); + if (motion.windowID() != data.windowID) { + return false; + } + float mx = motion.x(); + float my = motion.y(); + if ((io.getConfigFlags() & ImGuiConfigFlags.ViewportsEnable) != 0) { + try (MemoryStack stack = MemoryStack.stackPush()) { + final IntBuffer pwx = stack.mallocInt(1); + final IntBuffer pwy = stack.mallocInt(1); + SDL_GetWindowPosition(data.window, pwx, pwy); + mx += pwx.get(0); + my += pwy.get(0); + } + } + io.addMouseSourceEvent(motion.which() == SDL_TOUCH_MOUSEID ? ImGuiMouseSource.TouchScreen : ImGuiMouseSource.Mouse); + io.addMousePosEvent(mx, my); + return true; + } + + private boolean processMouseWheel(final SDL_Event event, final ImGuiIO io) { + final SDL_MouseWheelEvent wheel = event.wheel(); + if (wheel.windowID() != data.windowID) { + return false; + } + final float wheelX = -wheel.x(); + final float wheelY = wheel.y(); + io.addMouseSourceEvent(wheel.which() == SDL_TOUCH_MOUSEID ? ImGuiMouseSource.TouchScreen : ImGuiMouseSource.Mouse); + io.addMouseWheelEvent(wheelX, wheelY); + return true; + } + + private boolean processMouseButton(final SDL_Event event, final ImGuiIO io, final int type) { + final SDL_MouseButtonEvent button = event.button(); + if (button.windowID() != data.windowID) { + return false; + } + int mouseButton = -1; + final int btn = button.button() & 0xFF; + if (btn == SDL_BUTTON_LEFT) { + mouseButton = 0; + } else if (btn == SDL_BUTTON_RIGHT) { + mouseButton = 1; + } else if (btn == SDL_BUTTON_MIDDLE) { + mouseButton = 2; + } else if (btn == SDL_BUTTON_X1) { + mouseButton = 3; + } else if (btn == SDL_BUTTON_X2) { + mouseButton = 4; + } + if (mouseButton == -1) { + return false; + } + final boolean down = type == SDL_EVENT_MOUSE_BUTTON_DOWN; + io.addMouseSourceEvent(button.which() == SDL_TOUCH_MOUSEID ? ImGuiMouseSource.TouchScreen : ImGuiMouseSource.Mouse); + io.addMouseButtonEvent(mouseButton, down); + if (down) { + data.mouseButtonsDown |= 1 << mouseButton; + } else { + data.mouseButtonsDown &= ~(1 << mouseButton); + } + return true; + } + + private boolean processTextInput(final SDL_Event event, final ImGuiIO io) { + final SDL_TextInputEvent text = event.text(); + if (text.windowID() != data.windowID) { + return false; + } + io.addInputCharactersUTF8(text.textString()); + return true; + } + + private boolean processKey(final SDL_Event event, final ImGuiIO io, final int type) { + final SDL_KeyboardEvent key = event.key(); + if (key.windowID() != data.windowID) { + return false; + } + updateKeyModifiers(key.mod()); + final int imguiKey = keyEventToImGuiKey(key.key(), key.scancode()); + io.addKeyEvent(imguiKey, type == SDL_EVENT_KEY_DOWN); + io.setKeyEventNativeData(imguiKey, key.key(), key.scancode(), key.scancode()); + return true; + } + + private boolean processWindowMouseEnter(final SDL_Event event) { + final SDL_WindowEvent win = event.window(); + if (win.windowID() != data.windowID) { + return false; + } + data.mouseWindowID = win.windowID(); + data.mousePendingLeaveFrame = 0; + return true; + } + + private boolean processWindowMouseLeave(final SDL_Event event) { + final SDL_WindowEvent win = event.window(); + if (win.windowID() != data.windowID) { + return false; + } + data.mousePendingLeaveFrame = ImGui.getFrameCount() + 1; + return true; + } + + private boolean processWindowFocus(final SDL_Event event, final ImGuiIO io, final int type) { + final SDL_WindowEvent win = event.window(); + if (win.windowID() != data.windowID) { + return false; + } + io.addFocusEvent(type == SDL_EVENT_WINDOW_FOCUS_GAINED); + return true; + } + + /** + * Mirrors {@code ImGui_ImplSDL3_NewFrame}. + */ + public void newFrame() { + final ImGuiIO io = ImGui.getIO(); + + // Setup main viewport size (every frame to accommodate for window resizing) + final int[] wh = getWindowSize(); + final float[] fbScale = getFramebufferScale(wh[0], wh[1]); + io.setDisplaySize((float) wh[0], (float) wh[1]); + io.setDisplayFramebufferScale(fbScale[0], fbScale[1]); + + // Setup time step + if (frequency == 0L) { + frequency = SDL_GetPerformanceFrequency(); + } + long currentTime = SDL_GetPerformanceCounter(); + if (currentTime <= data.time) { + currentTime = data.time + 1; + } + io.setDeltaTime(data.time > 0L ? (float) ((double) (currentTime - data.time) / (double) frequency) : (1.0f / 60.0f)); + data.time = currentTime; + + if (data.mousePendingLeaveFrame != 0 && data.mousePendingLeaveFrame >= ImGui.getFrameCount() && data.mouseButtonsDown == 0) { + data.mouseWindowID = 0; + data.mousePendingLeaveFrame = 0; + io.addMousePosEvent(-Float.MAX_VALUE, -Float.MAX_VALUE); + } + + updateMouseData(io); + updateMouseCursor(io); + // TODO: not ported — IME update (UpdateIme) requires platform IO callback wiring. + updateGamepads(io); + } + + private long frequency; + + private int[] getWindowSize() { + final int[] wh = new int[2]; + try (MemoryStack stack = MemoryStack.stackPush()) { + final IntBuffer pw = stack.mallocInt(1); + final IntBuffer ph = stack.mallocInt(1); + SDL_GetWindowSize(data.window, pw, ph); + wh[0] = pw.get(0); + wh[1] = ph.get(0); + } + // SDL_WINDOW_MINIMIZED — when minimized, report 0,0 + if ((SDL_GetWindowFlags(data.window) & SDLWindowFlags.SDL_WINDOW_MINIMIZED) != 0L) { + wh[0] = 0; + wh[1] = 0; + } + return wh; + } + + private float[] getFramebufferScale(final int w, final int h) { + final int dw; + final int dh; + try (MemoryStack stack = MemoryStack.stackPush()) { + final IntBuffer pdw = stack.mallocInt(1); + final IntBuffer pdh = stack.mallocInt(1); + SDL_GetWindowSizeInPixels(data.window, pdw, pdh); + dw = pdw.get(0); + dh = pdh.get(0); + } + final float fx = w > 0 ? (float) dw / (float) w : 1.0f; + final float fy = h > 0 ? (float) dh / (float) h : 1.0f; + return new float[]{fx, fy}; + } + + private void updateMouseData(final ImGuiIO io) { + final boolean isAppFocused = (SDL_GetWindowFlags(data.window) & SDL_WINDOW_INPUT_FOCUS) != 0L; + if (isAppFocused) { + if (io.getWantSetMousePos()) { + SDL_WarpMouseInWindow(data.window, io.getMousePosX(), io.getMousePosY()); + } + // TODO: not ported — global-state fallback (SDL_GetGlobalMouseState) for unhovered focus + // because LWJGL's PointerBuffer/IntBuffer translation is non-trivial and the path + // is only required when mouseCanUseGlobalState is true (multi-viewport regime). + } + } + + private void updateMouseCursor(final ImGuiIO io) { + if ((io.getConfigFlags() & ImGuiConfigFlags.NoMouseCursorChange) != 0) { + return; + } + final int imguiCursor = ImGui.getMouseCursor(); + if (io.getMouseDrawCursor() || imguiCursor == ImGuiMouseCursor.None) { + SDL_HideCursor(); + } else { + final long expected = data.mouseCursors[imguiCursor] != 0L + ? data.mouseCursors[imguiCursor] + : data.mouseCursors[ImGuiMouseCursor.Arrow]; + if (data.mouseLastCursor != expected) { + SDL_SetCursor(expected); + data.mouseLastCursor = expected; + } + SDL_ShowCursor(); + } + } + + private void closeGamepads() { + if (data.gamepadMode != GAMEPAD_MODE_MANUAL) { + for (final Long g : data.gamepads) { + SDL_CloseGamepad(g); + } + } + data.gamepads.clear(); + } + + private void updateGamepads(final ImGuiIO io) { + // Update list of gamepads to use + if (data.wantUpdateGamepadsList && data.gamepadMode != GAMEPAD_MODE_MANUAL) { + closeGamepads(); + final IntBuffer ids = SDL_GetGamepads(); + if (ids != null) { + while (ids.hasRemaining()) { + final int id = ids.get(); + final long gp = SDL_OpenGamepad(id); + if (gp != 0L) { + data.gamepads.add(gp); + if (data.gamepadMode == GAMEPAD_MODE_AUTO_FIRST) { + break; + } + } + } + } + data.wantUpdateGamepadsList = false; + } + + io.removeBackendFlags(ImGuiBackendFlags.HasGamepad); + if (data.gamepads.isEmpty()) { + return; + } + io.addBackendFlags(ImGuiBackendFlags.HasGamepad); + + final int thumbDeadZone = 8000; + updateGamepadButton(io, ImGuiKey.GamepadStart, SDL_GAMEPAD_BUTTON_START); + updateGamepadButton(io, ImGuiKey.GamepadBack, SDL_GAMEPAD_BUTTON_BACK); + updateGamepadButton(io, ImGuiKey.GamepadFaceLeft, SDL_GAMEPAD_BUTTON_WEST); + updateGamepadButton(io, ImGuiKey.GamepadFaceRight, SDL_GAMEPAD_BUTTON_EAST); + updateGamepadButton(io, ImGuiKey.GamepadFaceUp, SDL_GAMEPAD_BUTTON_NORTH); + updateGamepadButton(io, ImGuiKey.GamepadFaceDown, SDL_GAMEPAD_BUTTON_SOUTH); + updateGamepadButton(io, ImGuiKey.GamepadDpadLeft, SDL_GAMEPAD_BUTTON_DPAD_LEFT); + updateGamepadButton(io, ImGuiKey.GamepadDpadRight, SDL_GAMEPAD_BUTTON_DPAD_RIGHT); + updateGamepadButton(io, ImGuiKey.GamepadDpadUp, SDL_GAMEPAD_BUTTON_DPAD_UP); + updateGamepadButton(io, ImGuiKey.GamepadDpadDown, SDL_GAMEPAD_BUTTON_DPAD_DOWN); + updateGamepadButton(io, ImGuiKey.GamepadL1, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER); + updateGamepadButton(io, ImGuiKey.GamepadR1, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER); + updateGamepadAnalog(io, ImGuiKey.GamepadL2, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, 0.0f, 32767.0f); + updateGamepadAnalog(io, ImGuiKey.GamepadR2, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, 0.0f, 32767.0f); + updateGamepadButton(io, ImGuiKey.GamepadL3, SDL_GAMEPAD_BUTTON_LEFT_STICK); + updateGamepadButton(io, ImGuiKey.GamepadR3, SDL_GAMEPAD_BUTTON_RIGHT_STICK); + updateGamepadAnalog(io, ImGuiKey.GamepadLStickLeft, SDL_GAMEPAD_AXIS_LEFTX, -thumbDeadZone, -32768.0f); + updateGamepadAnalog(io, ImGuiKey.GamepadLStickRight, SDL_GAMEPAD_AXIS_LEFTX, thumbDeadZone, 32767.0f); + updateGamepadAnalog(io, ImGuiKey.GamepadLStickUp, SDL_GAMEPAD_AXIS_LEFTY, -thumbDeadZone, -32768.0f); + updateGamepadAnalog(io, ImGuiKey.GamepadLStickDown, SDL_GAMEPAD_AXIS_LEFTY, thumbDeadZone, 32767.0f); + updateGamepadAnalog(io, ImGuiKey.GamepadRStickLeft, SDL_GAMEPAD_AXIS_RIGHTX, -thumbDeadZone, -32768.0f); + updateGamepadAnalog(io, ImGuiKey.GamepadRStickRight, SDL_GAMEPAD_AXIS_RIGHTX, thumbDeadZone, 32767.0f); + updateGamepadAnalog(io, ImGuiKey.GamepadRStickUp, SDL_GAMEPAD_AXIS_RIGHTY, -thumbDeadZone, -32768.0f); + updateGamepadAnalog(io, ImGuiKey.GamepadRStickDown, SDL_GAMEPAD_AXIS_RIGHTY, thumbDeadZone, 32767.0f); + } + + private void updateGamepadButton(final ImGuiIO io, final int key, final int sdlButton) { + boolean merged = false; + for (final Long g : data.gamepads) { + merged |= SDL_GetGamepadButton(g, sdlButton); + } + io.addKeyEvent(key, merged); + } + + private void updateGamepadAnalog(final ImGuiIO io, final int key, final int sdlAxis, final float v0, final float v1) { + float merged = 0.0f; + for (final Long g : data.gamepads) { + final float vn = saturate((SDL_GetGamepadAxis(g, sdlAxis) - v0) / (v1 - v0)); + if (merged < vn) { + merged = vn; + } + } + io.addKeyAnalogEvent(key, merged > 0.1f, merged); + } + + private static float saturate(final float v) { + return v < 0.0f ? 0.0f : (v > 1.0f ? 1.0f : v); + } + + private static void updateKeyModifiers(final int sdlKeyMods) { + final ImGuiIO io = ImGui.getIO(); + io.addKeyEvent(ImGuiKey.ImGuiMod_Ctrl, (sdlKeyMods & SDL_KMOD_CTRL) != 0); + io.addKeyEvent(ImGuiKey.ImGuiMod_Shift, (sdlKeyMods & SDL_KMOD_SHIFT) != 0); + io.addKeyEvent(ImGuiKey.ImGuiMod_Alt, (sdlKeyMods & SDL_KMOD_ALT) != 0); + io.addKeyEvent(ImGuiKey.ImGuiMod_Super, (sdlKeyMods & SDL_KMOD_GUI) != 0); + } + + /** + * Mirrors {@code ImGui_ImplSDL3_KeyEventToImGuiKey}. Public so external code can reuse the mapping. + */ + @SuppressWarnings({"checkstyle:CyclomaticComplexity", "checkstyle:JavaNCSS", "checkstyle:MethodLength"}) + public static int keyEventToImGuiKey(final int keycode, final int scancode) { + // Keypad doesn't have individual key values in SDL3 + switch (scancode) { + case SDL_SCANCODE_KP_0: return ImGuiKey.Keypad0; + case SDL_SCANCODE_KP_1: return ImGuiKey.Keypad1; + case SDL_SCANCODE_KP_2: return ImGuiKey.Keypad2; + case SDL_SCANCODE_KP_3: return ImGuiKey.Keypad3; + case SDL_SCANCODE_KP_4: return ImGuiKey.Keypad4; + case SDL_SCANCODE_KP_5: return ImGuiKey.Keypad5; + case SDL_SCANCODE_KP_6: return ImGuiKey.Keypad6; + case SDL_SCANCODE_KP_7: return ImGuiKey.Keypad7; + case SDL_SCANCODE_KP_8: return ImGuiKey.Keypad8; + case SDL_SCANCODE_KP_9: return ImGuiKey.Keypad9; + case SDL_SCANCODE_KP_PERIOD: return ImGuiKey.KeypadDecimal; + case SDL_SCANCODE_KP_DIVIDE: return ImGuiKey.KeypadDivide; + case SDL_SCANCODE_KP_MULTIPLY: return ImGuiKey.KeypadMultiply; + case SDL_SCANCODE_KP_MINUS: return ImGuiKey.KeypadSubtract; + case SDL_SCANCODE_KP_PLUS: return ImGuiKey.KeypadAdd; + case SDL_SCANCODE_KP_ENTER: return ImGuiKey.KeypadEnter; + case SDL_SCANCODE_KP_EQUALS: return ImGuiKey.KeypadEqual; + default: break; + } + switch (keycode) { + case SDLK_TAB: return ImGuiKey.Tab; + case SDLK_LEFT: return ImGuiKey.LeftArrow; + case SDLK_RIGHT: return ImGuiKey.RightArrow; + case SDLK_UP: return ImGuiKey.UpArrow; + case SDLK_DOWN: return ImGuiKey.DownArrow; + case SDLK_PAGEUP: return ImGuiKey.PageUp; + case SDLK_PAGEDOWN: return ImGuiKey.PageDown; + case SDLK_HOME: return ImGuiKey.Home; + case SDLK_END: return ImGuiKey.End; + case SDLK_INSERT: return ImGuiKey.Insert; + case SDLK_DELETE: return ImGuiKey.Delete; + case SDLK_BACKSPACE: return ImGuiKey.Backspace; + case SDLK_SPACE: return ImGuiKey.Space; + case SDLK_RETURN: return ImGuiKey.Enter; + case SDLK_ESCAPE: return ImGuiKey.Escape; + case SDLK_COMMA: return ImGuiKey.Comma; + case SDLK_PERIOD: return ImGuiKey.Period; + case SDLK_SEMICOLON: return ImGuiKey.Semicolon; + case SDLK_CAPSLOCK: return ImGuiKey.CapsLock; + case SDLK_SCROLLLOCK: return ImGuiKey.ScrollLock; + case SDLK_NUMLOCKCLEAR: return ImGuiKey.NumLock; + case SDLK_PRINTSCREEN: return ImGuiKey.PrintScreen; + case SDLK_PAUSE: return ImGuiKey.Pause; + case SDLK_LCTRL: return ImGuiKey.LeftCtrl; + case SDLK_LSHIFT: return ImGuiKey.LeftShift; + case SDLK_LALT: return ImGuiKey.LeftAlt; + case SDLK_LGUI: return ImGuiKey.LeftSuper; + case SDLK_RCTRL: return ImGuiKey.RightCtrl; + case SDLK_RSHIFT: return ImGuiKey.RightShift; + case SDLK_RALT: return ImGuiKey.RightAlt; + case SDLK_RGUI: return ImGuiKey.RightSuper; + case SDLK_APPLICATION: return ImGuiKey.Menu; + case SDLK_0: return ImGuiKey._0; + case SDLK_1: return ImGuiKey._1; + case SDLK_2: return ImGuiKey._2; + case SDLK_3: return ImGuiKey._3; + case SDLK_4: return ImGuiKey._4; + case SDLK_5: return ImGuiKey._5; + case SDLK_6: return ImGuiKey._6; + case SDLK_7: return ImGuiKey._7; + case SDLK_8: return ImGuiKey._8; + case SDLK_9: return ImGuiKey._9; + case SDLK_A: return ImGuiKey.A; + case SDLK_B: return ImGuiKey.B; + case SDLK_C: return ImGuiKey.C; + case SDLK_D: return ImGuiKey.D; + case SDLK_E: return ImGuiKey.E; + case SDLK_F: return ImGuiKey.F; + case SDLK_G: return ImGuiKey.G; + case SDLK_H: return ImGuiKey.H; + case SDLK_I: return ImGuiKey.I; + case SDLK_J: return ImGuiKey.J; + case SDLK_K: return ImGuiKey.K; + case SDLK_L: return ImGuiKey.L; + case SDLK_M: return ImGuiKey.M; + case SDLK_N: return ImGuiKey.N; + case SDLK_O: return ImGuiKey.O; + case SDLK_P: return ImGuiKey.P; + case SDLK_Q: return ImGuiKey.Q; + case SDLK_R: return ImGuiKey.R; + case SDLK_S: return ImGuiKey.S; + case SDLK_T: return ImGuiKey.T; + case SDLK_U: return ImGuiKey.U; + case SDLK_V: return ImGuiKey.V; + case SDLK_W: return ImGuiKey.W; + case SDLK_X: return ImGuiKey.X; + case SDLK_Y: return ImGuiKey.Y; + case SDLK_Z: return ImGuiKey.Z; + case SDLK_F1: return ImGuiKey.F1; + case SDLK_F2: return ImGuiKey.F2; + case SDLK_F3: return ImGuiKey.F3; + case SDLK_F4: return ImGuiKey.F4; + case SDLK_F5: return ImGuiKey.F5; + case SDLK_F6: return ImGuiKey.F6; + case SDLK_F7: return ImGuiKey.F7; + case SDLK_F8: return ImGuiKey.F8; + case SDLK_F9: return ImGuiKey.F9; + case SDLK_F10: return ImGuiKey.F10; + case SDLK_F11: return ImGuiKey.F11; + case SDLK_F12: return ImGuiKey.F12; + case SDLK_F13: return ImGuiKey.F13; + case SDLK_F14: return ImGuiKey.F14; + case SDLK_F15: return ImGuiKey.F15; + case SDLK_F16: return ImGuiKey.F16; + case SDLK_F17: return ImGuiKey.F17; + case SDLK_F18: return ImGuiKey.F18; + case SDLK_F19: return ImGuiKey.F19; + case SDLK_F20: return ImGuiKey.F20; + case SDLK_F21: return ImGuiKey.F21; + case SDLK_F22: return ImGuiKey.F22; + case SDLK_F23: return ImGuiKey.F23; + case SDLK_F24: return ImGuiKey.F24; + case SDLK_AC_BACK: return ImGuiKey.AppBack; + case SDLK_AC_FORWARD: return ImGuiKey.AppForward; + default: break; + } + // Fallback to scancode + switch (scancode) { + case SDL_SCANCODE_GRAVE: return ImGuiKey.GraveAccent; + case SDL_SCANCODE_MINUS: return ImGuiKey.Minus; + case SDL_SCANCODE_EQUALS: return ImGuiKey.Equal; + case SDL_SCANCODE_LEFTBRACKET: return ImGuiKey.LeftBracket; + case SDL_SCANCODE_RIGHTBRACKET: return ImGuiKey.RightBracket; + case SDL_SCANCODE_NONUSBACKSLASH: return ImGuiKey.Oem102; + case SDL_SCANCODE_BACKSLASH: return ImGuiKey.Backslash; + case SDL_SCANCODE_SEMICOLON: return ImGuiKey.Semicolon; + case SDL_SCANCODE_APOSTROPHE: return ImGuiKey.Apostrophe; + case SDL_SCANCODE_COMMA: return ImGuiKey.Comma; + case SDL_SCANCODE_PERIOD: return ImGuiKey.Period; + case SDL_SCANCODE_SLASH: return ImGuiKey.Slash; + default: break; + } + return ImGuiKey.None; + } + + /** + * Local mirror of {@code SDL_WINDOW_MINIMIZED} to avoid a static import collision; the constant + * is just a long bit-flag from {@link org.lwjgl.sdl.SDLVideo}. + */ + private static final class SDLWindowFlags { + static final long SDL_WINDOW_MINIMIZED = org.lwjgl.sdl.SDLVideo.SDL_WINDOW_MINIMIZED; + + private SDLWindowFlags() { + } + } + + /** + * @return native {@code SDL_Window*} handle this backend was initialized with + */ + public final long getWindow() { + return data.window; + } +} diff --git a/imgui-lwjgl3/src/main/java/imgui/sdl3/ImGuiImplSdlGpu3.java b/imgui-lwjgl3/src/main/java/imgui/sdl3/ImGuiImplSdlGpu3.java new file mode 100644 index 00000000..bb877b57 --- /dev/null +++ b/imgui-lwjgl3/src/main/java/imgui/sdl3/ImGuiImplSdlGpu3.java @@ -0,0 +1,716 @@ +package imgui.sdl3; + +import imgui.ImDrawData; +import imgui.ImFontAtlas; +import imgui.ImGui; +import imgui.ImGuiIO; +import imgui.flag.ImGuiBackendFlags; +import imgui.type.ImInt; +import org.lwjgl.sdl.SDL_GPUBufferBinding; +import org.lwjgl.sdl.SDL_GPUBufferCreateInfo; +import org.lwjgl.sdl.SDL_GPUBufferRegion; +import org.lwjgl.sdl.SDL_GPUColorTargetBlendState; +import org.lwjgl.sdl.SDL_GPUColorTargetDescription; +import org.lwjgl.sdl.SDL_GPUGraphicsPipelineCreateInfo; +import org.lwjgl.sdl.SDL_GPUGraphicsPipelineTargetInfo; +import org.lwjgl.sdl.SDL_GPURasterizerState; +import org.lwjgl.sdl.SDL_GPUSamplerCreateInfo; +import org.lwjgl.sdl.SDL_GPUShaderCreateInfo; +import org.lwjgl.sdl.SDL_GPUTextureCreateInfo; +import org.lwjgl.sdl.SDL_GPUTextureRegion; +import org.lwjgl.sdl.SDL_GPUTextureSamplerBinding; +import org.lwjgl.sdl.SDL_GPUTextureTransferInfo; +import org.lwjgl.sdl.SDL_GPUTransferBufferCreateInfo; +import org.lwjgl.sdl.SDL_GPUTransferBufferLocation; +import org.lwjgl.sdl.SDL_GPUVertexAttribute; +import org.lwjgl.sdl.SDL_GPUVertexBufferDescription; +import org.lwjgl.sdl.SDL_GPUVertexInputState; +import org.lwjgl.sdl.SDL_GPUViewport; +import org.lwjgl.sdl.SDL_Rect; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; + +import static org.lwjgl.sdl.SDLGPU.SDL_BeginGPUCopyPass; +import static org.lwjgl.sdl.SDLGPU.SDL_BindGPUFragmentSamplers; +import static org.lwjgl.sdl.SDLGPU.SDL_BindGPUGraphicsPipeline; +import static org.lwjgl.sdl.SDLGPU.SDL_BindGPUIndexBuffer; +import static org.lwjgl.sdl.SDLGPU.SDL_BindGPUVertexBuffers; +import static org.lwjgl.sdl.SDLGPU.SDL_CreateGPUBuffer; +import static org.lwjgl.sdl.SDLGPU.SDL_CreateGPUGraphicsPipeline; +import static org.lwjgl.sdl.SDLGPU.SDL_CreateGPUSampler; +import static org.lwjgl.sdl.SDLGPU.SDL_CreateGPUShader; +import static org.lwjgl.sdl.SDLGPU.SDL_CreateGPUTexture; +import static org.lwjgl.sdl.SDLGPU.SDL_CreateGPUTransferBuffer; +import static org.lwjgl.sdl.SDLGPU.SDL_DrawGPUIndexedPrimitives; +import static org.lwjgl.sdl.SDLGPU.SDL_EndGPUCopyPass; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_BLENDFACTOR_ONE; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_BLENDFACTOR_SRC_ALPHA; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_BLENDOP_ADD; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_BUFFERUSAGE_INDEX; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_BUFFERUSAGE_VERTEX; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_CULLMODE_NONE; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_FILLMODE_FILL; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_FILTER_LINEAR; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_INDEXELEMENTSIZE_16BIT; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_INDEXELEMENTSIZE_32BIT; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_PRESENTMODE_VSYNC; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_PRIMITIVETYPE_TRIANGLELIST; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_SAMPLECOUNT_1; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_SAMPLERMIPMAPMODE_LINEAR; +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_GPU_SHADERSTAGE_FRAGMENT; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_SHADERSTAGE_VERTEX; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_SWAPCHAINCOMPOSITION_SDR; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_INVALID; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTURETYPE_2D; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREUSAGE_SAMPLER; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_VERTEXELEMENTFORMAT_UBYTE4_NORM; +import static org.lwjgl.sdl.SDLGPU.SDL_GPU_VERTEXINPUTRATE_VERTEX; +import static org.lwjgl.sdl.SDLGPU.SDL_GetGPUShaderFormats; +import static org.lwjgl.sdl.SDLGPU.SDL_MapGPUTransferBuffer; +import static org.lwjgl.sdl.SDLGPU.SDL_PushGPUVertexUniformData; +import static org.lwjgl.sdl.SDLGPU.SDL_ReleaseGPUBuffer; +import static org.lwjgl.sdl.SDLGPU.SDL_ReleaseGPUGraphicsPipeline; +import static org.lwjgl.sdl.SDLGPU.SDL_ReleaseGPUSampler; +import static org.lwjgl.sdl.SDLGPU.SDL_ReleaseGPUShader; +import static org.lwjgl.sdl.SDLGPU.SDL_ReleaseGPUTexture; +import static org.lwjgl.sdl.SDLGPU.SDL_ReleaseGPUTransferBuffer; +import static org.lwjgl.sdl.SDLGPU.SDL_SetGPUScissor; +import static org.lwjgl.sdl.SDLGPU.SDL_SetGPUViewport; +import static org.lwjgl.sdl.SDLGPU.SDL_UnmapGPUTransferBuffer; +import static org.lwjgl.sdl.SDLGPU.SDL_UploadToGPUBuffer; +import static org.lwjgl.sdl.SDLGPU.SDL_UploadToGPUTexture; + +/** + * Java port of upstream {@code imgui_impl_sdlgpu3.cpp}: Dear ImGui renderer backend for SDL_GPU. + * + *

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