diff --git a/README.md b/README.md index 360ad212..36d56c63 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,8 @@ cd imgui-java ./gradlew :example:run # SDL3 + SDL_GPU backend smoke test: ./gradlew :example:run -PmainClass=MainSdl +# SDL3 + SDL_Renderer backend smoke test: +./gradlew :example:run -PmainClass=MainSdlRenderer ``` `example/src/main/java/` contains a runnable showcase for every widget and extension — start with `Main.java`, then @@ -68,10 +70,11 @@ On Apple Silicon nothing extra is needed: the published macOS native is a univer ### SDL3 backend (imgui-app) -`imgui-app` now supports two backends via `Configuration#setBackend(...)`: +`imgui-app` now supports three backends via `Configuration#setBackend(...)`: - `Backend.GLFW` (default) - `Backend.SDL` (SDL3 + SDL_GPU) +- `Backend.SDL_RENDERER` (SDL3 + `SDL_Renderer`) Use SDL3 by switching backend in `configure(...)`: @@ -88,7 +91,13 @@ 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). +The SDL_Renderer variant is available through `Backend.SDL_RENDERER` and the `MainSdlRenderer` smoke test: + +``` +./gradlew :example:run -PmainClass=MainSdlRenderer +``` + +At the moment, SDL backends in `imgui-app` are focused on single-window flow (multi-viewport is not supported). ## Application diff --git a/example/src/main/java/MainSdlRenderer.java b/example/src/main/java/MainSdlRenderer.java new file mode 100644 index 00000000..fc3c36c7 --- /dev/null +++ b/example/src/main/java/MainSdlRenderer.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_Renderer backend. Run with: + *
+ * ./gradlew :example:run -PmainClass=MainSdlRenderer + *+ */ +public class MainSdlRenderer extends Application { + @Override + protected void configure(final Configuration config) { + config.setTitle("Example Application — SDL3 + SDL_Renderer"); + config.setBackend(Backend.SDL_RENDERER); + } + + @Override + public void process() { + ImGui.begin("SDL3 + SDL_Renderer"); + ImGui.text("Hello from the SDL_Renderer backend!"); + ImGui.end(); + } + + public static void main(final String[] args) { + launch(new MainSdlRenderer()); + } +} diff --git a/imgui-app/src/main/java/imgui/app/Application.java b/imgui-app/src/main/java/imgui/app/Application.java index d135726d..7b69c3b9 100644 --- a/imgui-app/src/main/java/imgui/app/Application.java +++ b/imgui-app/src/main/java/imgui/app/Application.java @@ -9,7 +9,8 @@ *
The entry point for ImGui applications is the Application class and {@link #launch(Application)} method. * 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 + * instantiates the matching {@link Window} subclass ({@link WindowGlfw}, {@link WindowSdl} or + * {@link WindowSdlRenderer}) and drives the * application loop: * *
Direct subclassing is supported but the supported integration path is to extend diff --git a/imgui-app/src/main/java/imgui/app/WindowSdlRenderer.java b/imgui-app/src/main/java/imgui/app/WindowSdlRenderer.java new file mode 100644 index 00000000..ee38376f --- /dev/null +++ b/imgui-app/src/main/java/imgui/app/WindowSdlRenderer.java @@ -0,0 +1,139 @@ +package imgui.app; + +import imgui.ImGui; +import imgui.sdl3.ImGuiImplSdl3; +import imgui.sdl3.ImGuiImplSdlRenderer3; +import org.lwjgl.sdl.SDL_Event; + +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.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.SDLRender.SDL_DestroyRenderer; +import static org.lwjgl.sdl.SDLRender.SDL_RenderClear; +import static org.lwjgl.sdl.SDLRender.SDL_RenderPresent; +import static org.lwjgl.sdl.SDLRender.SDL_SetRenderDrawColorFloat; +import static org.lwjgl.sdl.SDLRender.SDL_SetRenderScale; +import static org.lwjgl.sdl.SDLRender.SDL_SetRenderVSync; +import static org.lwjgl.sdl.SDLRender.nSDL_CreateRenderer; +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 + {@code SDL_Renderer} implementation of {@link Window}. + * + *
Pumps SDL events through {@link ImGuiImplSdl3#processEvent(long)} each frame and renders through + * {@link ImGuiImplSdlRenderer3#renderDrawData(long, long)}. Multi-viewport is not supported. + */ +public class WindowSdlRenderer extends Window { + private final ImGuiImplSdl3 imGuiSdl3 = new ImGuiImplSdl3(); + private final ImGuiImplSdlRenderer3 imGuiSdlRenderer3 = new ImGuiImplSdlRenderer3(); + + private long handle; + private long renderer; + 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); + } + + // Null driver name lets SDL pick the best available renderer. + renderer = nSDL_CreateRenderer(handle, 0L); + if (renderer == 0L) { + throw new RuntimeException("Failed to create the SDL renderer: " + SDL_GetError()); + } + SDL_SetRenderVSync(renderer, 1); + + owner.initImGui(config); + + imGuiSdl3.initForSDLRenderer(handle, renderer); + imGuiSdlRenderer3.init(renderer); + } + + @Override + protected void dispose() { + imGuiSdlRenderer3.shutdown(); + imGuiSdl3.shutdown(); + owner.disposeImGui(); + + if (renderer != 0L) { + SDL_DestroyRenderer(renderer); + renderer = 0L; + } + if (handle != 0L) { + SDL_DestroyWindow(handle); + handle = 0L; + } + SDL_Quit(); + } + + @Override + protected void run() { + while (!shouldClose) { + runFrame(); + } + } + + @Override + protected void runFrame() { + 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; + } + } + } + + imGuiSdlRenderer3.newFrame(); + imGuiSdl3.newFrame(); + ImGui.newFrame(); + + owner.preProcess(); + owner.process(); + owner.postProcess(); + + ImGui.render(); + + // ImGui works in window coordinates (points) while SDL_Renderer draws in output pixels. + // Scale here so the geometry, clip rects and mouse coordinates all share the same space. + SDL_SetRenderScale(renderer, ImGui.getIO().getDisplayFramebufferScaleX(), ImGui.getIO().getDisplayFramebufferScaleY()); + SDL_SetRenderDrawColorFloat(renderer, colorBg.getRed(), colorBg.getGreen(), colorBg.getBlue(), colorBg.getAlpha()); + SDL_RenderClear(renderer); + imGuiSdlRenderer3.renderDrawData(ImGui.getDrawData()); + SDL_RenderPresent(renderer); + } + + /** + * @return native {@code SDL_Window*} handle + */ + public final long getHandle() { + return handle; + } + + /** + * @return native {@code SDL_Renderer*} handle + */ + public final long getRenderer() { + return renderer; + } +} diff --git a/imgui-lwjgl3/src/main/java/imgui/sdl3/ImGuiImplSdlRenderer3.java b/imgui-lwjgl3/src/main/java/imgui/sdl3/ImGuiImplSdlRenderer3.java new file mode 100644 index 00000000..74f44659 --- /dev/null +++ b/imgui-lwjgl3/src/main/java/imgui/sdl3/ImGuiImplSdlRenderer3.java @@ -0,0 +1,334 @@ +package imgui.sdl3; + +import imgui.ImDrawData; +import imgui.ImFontAtlas; +import imgui.ImGui; +import imgui.ImGuiIO; +import imgui.ImVec4; +import imgui.flag.ImGuiBackendFlags; +import imgui.type.ImInt; +import org.lwjgl.sdl.SDL_FColor; +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.SDLBlendMode.SDL_BLENDMODE_BLEND; +import static org.lwjgl.sdl.SDLPixels.SDL_PIXELFORMAT_RGBA32; +import static org.lwjgl.sdl.SDLRender.SDL_GetRenderClipRect; +import static org.lwjgl.sdl.SDLRender.SDL_GetRenderScale; +import static org.lwjgl.sdl.SDLRender.SDL_GetRenderViewport; +import static org.lwjgl.sdl.SDLRender.SDL_RenderClipEnabled; +import static org.lwjgl.sdl.SDLRender.SDL_RenderViewportSet; +import static org.lwjgl.sdl.SDLRender.SDL_SetRenderClipRect; +import static org.lwjgl.sdl.SDLRender.SDL_SetRenderViewport; +import static org.lwjgl.sdl.SDLRender.SDL_TEXTUREACCESS_STATIC; +import static org.lwjgl.sdl.SDLRender.nSDL_CreateTexture; +import static org.lwjgl.sdl.SDLRender.nSDL_DestroyTexture; +import static org.lwjgl.sdl.SDLRender.nSDL_RenderGeometryRaw; +import static org.lwjgl.sdl.SDLRender.nSDL_SetTextureBlendMode; +import static org.lwjgl.sdl.SDLRender.nSDL_SetTextureScaleMode; +import static org.lwjgl.sdl.SDLRender.nSDL_UpdateTexture; +import static org.lwjgl.sdl.SDLSurface.SDL_SCALEMODE_LINEAR; + +/** + * Java port of upstream {@code imgui_impl_sdlrenderer3.cpp}: Dear ImGui renderer backend for {@code SDL_Renderer}. + * + *
The backend uses {@code SDL_RenderGeometryRaw} and supports user textures, large meshes through + * {@link ImGuiBackendFlags#RendererHasVtxOffset} and draw-call clipping. Multi-viewport is not supported. + * + *
The binding does not expose {@code ImTextureData} yet, so the legacy {@link ImFontAtlas#setTexID(long)} flow + * is used for the font atlas: {@link #newFrame()} creates the texture lazily and {@link #destroyDeviceObjects()} + * destroys it. Dynamic atlas updates are therefore not honored. + * + *
As in upstream, when the renderer output is scaled (HiDPI), the caller must set the renderer scale to + * {@code ImGuiIO#DisplayFramebufferScale} before rendering. {@code imgui.app.WindowSdlRenderer} does this + * automatically; custom integrations should call {@code SDL_SetRenderScale} (or set a logical presentation) + * accordingly. + * + *
User callbacks registered through {@code ImDrawList#AddCallback} are not exposed by the binding and cannot be + * dispatched by this port. + */ +public class ImGuiImplSdlRenderer3 { + private static final int VERTEX_COLOR_OFFSET = 16; + + private long renderer; + private long fontTexture; + + // CPU staging buffers. ImDrawData#getCmdListVtxBufferData and #getCmdListIdxBufferData share one static buffer, + // so both are copied out before the draw commands are issued. + private ByteBuffer vertexBuffer; + private ByteBuffer indexBuffer; + private FloatBuffer colorBuffer; + + /** + * Initializes the renderer backend. Mirrors {@code ImGui_ImplSDLRenderer3_Init}. + * + * @param sdlRenderer native {@code SDL_Renderer*} pointer + * @return {@code true} on success + */ + public final boolean init(final long sdlRenderer) { + if (sdlRenderer == 0L) { + throw new IllegalArgumentException("ImGuiImplSdlRenderer3.init: SDL_Renderer handle must not be null"); + } + final ImGuiIO io = ImGui.getIO(); + io.setBackendRendererName("imgui_impl_sdlrenderer3 (lwjgl)"); + io.addBackendFlags(ImGuiBackendFlags.RendererHasVtxOffset); + renderer = sdlRenderer; + return true; + } + + /** + * Mirrors {@code ImGui_ImplSDLRenderer3_Shutdown}. + */ + public final void shutdown() { + destroyDeviceObjects(); + final ImGuiIO io = ImGui.getIO(); + io.setBackendRendererName(null); + io.removeBackendFlags(ImGuiBackendFlags.RendererHasVtxOffset); + renderer = 0L; + + if (vertexBuffer != null) { + MemoryUtil.memFree(vertexBuffer); + vertexBuffer = null; + } + if (indexBuffer != null) { + MemoryUtil.memFree(indexBuffer); + indexBuffer = null; + } + if (colorBuffer != null) { + MemoryUtil.memFree(colorBuffer); + colorBuffer = null; + } + } + + /** + * Mirrors {@code ImGui_ImplSDLRenderer3_NewFrame}. Creates the legacy font texture when needed. + */ + public final void newFrame() { + if (renderer == 0L) { + return; + } + if (fontTexture == 0L) { + createFontsTexture(); + } + } + + /** + * Builds the font atlas texture and stores its handle on the atlas via {@link ImFontAtlas#setTexID(long)}. + * + *
This method exists because the binding uses the legacy texture flow instead of upstream's + * {@code ImTextureData}-driven updates. + */ + public final void createFontsTexture() { + if (renderer == 0L) { + return; + } + 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 long texture = nSDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STATIC, w, h); + if (texture == 0L) { + throw new IllegalStateException("ImGuiImplSdlRenderer3: SDL_CreateTexture failed for the font atlas"); + } + final ByteBuffer src = pixels.duplicate(); + src.position(0); + nSDL_UpdateTexture(texture, 0L, MemoryUtil.memAddress(src), w * 4); + nSDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND); + nSDL_SetTextureScaleMode(texture, SDL_SCALEMODE_LINEAR); + + fontTexture = texture; + atlas.setTexID(texture); + } + + /** + * Mirrors {@code ImGui_ImplSDLRenderer3_CreateDeviceObjects}. Upstream creates no persistent device objects here; + * the font texture is created lazily by {@link #newFrame()}. + */ + public final void createDeviceObjects() { + // Intentionally empty: upstream has no persistent device objects for SDL_Renderer. + } + + /** + * Mirrors {@code ImGui_ImplSDLRenderer3_DestroyDeviceObjects}. + */ + public final void destroyDeviceObjects() { + if (fontTexture != 0L) { + nSDL_DestroyTexture(fontTexture); + fontTexture = 0L; + ImGui.getIO().getFonts().setTexID(0L); + } + } + + /** + * Renders the given draw data using the renderer passed to {@link #init(long)}. + * + * @param drawData draw data to render + */ + public final void renderDrawData(final ImDrawData drawData) { + renderDrawData(drawData, renderer); + } + + /** + * Renders the given draw data using the given renderer. Mirrors {@code ImGui_ImplSDLRenderer3_RenderDrawData}. + * + * @param drawDataPtr native {@code ImDrawData*} pointer + * @param sdlRenderer native {@code SDL_Renderer*} pointer + */ + public final void renderDrawData(final long drawDataPtr, final long sdlRenderer) { + if (drawDataPtr == 0L) { + return; + } + renderDrawData(new ImDrawData(drawDataPtr), sdlRenderer); + } + + /** + * Renders the given draw data using the given renderer. Mirrors {@code ImGui_ImplSDLRenderer3_RenderDrawData}. + * + * @param drawData draw data to render + * @param sdlRenderer native {@code SDL_Renderer*} pointer + */ + public final void renderDrawData(final ImDrawData drawData, final long sdlRenderer) { + if (drawData == null || sdlRenderer == 0L || drawData.getCmdListsCount() <= 0) { + return; + } + if (fontTexture == 0L) { + createFontsTexture(); + } + + final int vertexStride = ImDrawData.sizeOfImDrawVert(); + final int indexStride = ImDrawData.sizeOfImDrawIdx(); + final ImVec4 clipRect = new ImVec4(); + + try (MemoryStack stack = MemoryStack.stackPush()) { + final FloatBuffer renderScaleXBuffer = stack.mallocFloat(1); + final FloatBuffer renderScaleYBuffer = stack.mallocFloat(1); + SDL_GetRenderScale(sdlRenderer, renderScaleXBuffer, renderScaleYBuffer); + + // If the user already set a scale through SDL_RenderSetScale, SDL applies it to the geometry itself. + final float renderScaleX = renderScaleXBuffer.get(0) == 1.0f ? drawData.getFramebufferScaleX() : 1.0f; + final float renderScaleY = renderScaleYBuffer.get(0) == 1.0f ? drawData.getFramebufferScaleY() : 1.0f; + + final int fbWidth = (int) (drawData.getDisplaySizeX() * renderScaleX); + final int fbHeight = (int) (drawData.getDisplaySizeY() * renderScaleY); + if (fbWidth == 0 || fbHeight == 0) { + return; + } + + final boolean viewportEnabled = SDL_RenderViewportSet(sdlRenderer); + final boolean clipEnabled = SDL_RenderClipEnabled(sdlRenderer); + final SDL_Rect oldViewport = SDL_Rect.calloc(stack); + final SDL_Rect oldClipRect = SDL_Rect.calloc(stack); + SDL_GetRenderViewport(sdlRenderer, oldViewport); + SDL_GetRenderClipRect(sdlRenderer, oldClipRect); + + setupRenderState(sdlRenderer); + + final float clipOffX = drawData.getDisplayPosX(); + final float clipOffY = drawData.getDisplayPosY(); + final SDL_Rect scissorRect = SDL_Rect.calloc(stack); + + for (int n = 0; n < drawData.getCmdListsCount(); n++) { + // Copy the shared static buffers one at a time: fetching both without copying would leave the + // vertex buffer pointing at index data. + vertexBuffer = copyTo(vertexBuffer, drawData.getCmdListVtxBufferData(n)); + indexBuffer = copyTo(indexBuffer, drawData.getCmdListIdxBufferData(n)); + + final int vertexCount = drawData.getCmdListVtxBufferSize(n); + final long vertexAddress = MemoryUtil.memAddress(vertexBuffer); + final long indexAddress = MemoryUtil.memAddress(indexBuffer); + final int cmdCount = drawData.getCmdListCmdBufferSize(n); + + for (int cmd = 0; cmd < cmdCount; cmd++) { + drawData.getCmdListCmdBufferClipRect(clipRect, n, cmd); + + float clipMinX = (clipRect.x - clipOffX) * renderScaleX; + float clipMinY = (clipRect.y - clipOffY) * renderScaleY; + float clipMaxX = (clipRect.z - clipOffX) * renderScaleX; + float clipMaxY = (clipRect.w - clipOffY) * renderScaleY; + clipMinX = Math.max(0.0f, clipMinX); + clipMinY = Math.max(0.0f, clipMinY); + clipMaxX = Math.min(fbWidth, clipMaxX); + clipMaxY = Math.min(fbHeight, clipMaxY); + if (clipMaxX <= clipMinX || clipMaxY <= clipMinY) { + continue; + } + + scissorRect.x((int) clipMinX).y((int) clipMinY) + .w((int) (clipMaxX - clipMinX)).h((int) (clipMaxY - clipMinY)); + SDL_SetRenderClipRect(sdlRenderer, scissorRect); + + final int elementCount = drawData.getCmdListCmdBufferElemCount(n, cmd); + final int vertexOffset = drawData.getCmdListCmdBufferVtxOffset(n, cmd); + final int indexOffset = drawData.getCmdListCmdBufferIdxOffset(n, cmd); + final int numVertices = vertexCount - vertexOffset; + if (elementCount == 0 || numVertices <= 0) { + continue; + } + + final FloatBuffer colors = ensureColorBuffer(numVertices * 4); + colors.clear(); + for (int i = 0; i < numVertices; i++) { + final int offset = (vertexOffset + i) * vertexStride + VERTEX_COLOR_OFFSET; + colors.put((vertexBuffer.get(offset) & 0xFF) / 255.0f); + colors.put((vertexBuffer.get(offset + 1) & 0xFF) / 255.0f); + colors.put((vertexBuffer.get(offset + 2) & 0xFF) / 255.0f); + colors.put((vertexBuffer.get(offset + 3) & 0xFF) / 255.0f); + } + colors.flip(); + + final long textureId = drawData.getCmdListCmdBufferTextureId(n, cmd); + final long xyAddress = vertexAddress + (long) vertexOffset * vertexStride; + nSDL_RenderGeometryRaw( + sdlRenderer, + textureId, + xyAddress, vertexStride, + MemoryUtil.memAddress(colors), SDL_FColor.SIZEOF, + xyAddress + 8, vertexStride, + numVertices, + indexAddress + (long) indexOffset * indexStride, elementCount, indexStride); + } + } + + SDL_SetRenderViewport(sdlRenderer, viewportEnabled ? oldViewport : null); + SDL_SetRenderClipRect(sdlRenderer, clipEnabled ? oldClipRect : null); + } + } + + private static void setupRenderState(final long sdlRenderer) { + // Clear out any viewports and clip rects set by the user. + SDL_SetRenderViewport(sdlRenderer, null); + SDL_SetRenderClipRect(sdlRenderer, null); + } + + private static ByteBuffer copyTo(final ByteBuffer destination, final ByteBuffer source) { + final int size = source.remaining(); + ByteBuffer result = destination; + if (result == null || result.capacity() < size) { + if (result != null) { + MemoryUtil.memFree(result); + } + result = MemoryUtil.memAlloc(size); + } + result.clear(); + result.put(source.duplicate()); + result.flip(); + return result; + } + + private FloatBuffer ensureColorBuffer(final int floats) { + if (colorBuffer == null || colorBuffer.capacity() < floats) { + if (colorBuffer != null) { + MemoryUtil.memFree(colorBuffer); + } + colorBuffer = MemoryUtil.memAllocFloat(floats); + } + return colorBuffer; + } +}