From 865c1a0a8ec2aa5a8ba2d04909ccb0562a1fa65f Mon Sep 17 00:00:00 2001 From: Aliaksandr Shcherbin Date: Tue, 18 Aug 2026 21:10:18 +0200 Subject: [PATCH 1/4] Support external OpenGL textures in NanoVG elements --- .../nanovg/NvgExternalTextureRenderer.java | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 spinygui.core.backend.lwjgl.nanovg/src/main/java/com/spinyowl/spinygui/core/backend/renderer/lwjgl/nanovg/NvgExternalTextureRenderer.java diff --git a/spinygui.core.backend.lwjgl.nanovg/src/main/java/com/spinyowl/spinygui/core/backend/renderer/lwjgl/nanovg/NvgExternalTextureRenderer.java b/spinygui.core.backend.lwjgl.nanovg/src/main/java/com/spinyowl/spinygui/core/backend/renderer/lwjgl/nanovg/NvgExternalTextureRenderer.java new file mode 100644 index 00000000..2d0850d4 --- /dev/null +++ b/spinygui.core.backend.lwjgl.nanovg/src/main/java/com/spinyowl/spinygui/core/backend/renderer/lwjgl/nanovg/NvgExternalTextureRenderer.java @@ -0,0 +1,98 @@ +package com.spinyowl.spinygui.core.backend.renderer.lwjgl.nanovg; + +import static org.lwjgl.nanovg.NanoVG.NVG_IMAGE_FLIPY; +import static org.lwjgl.nanovg.NanoVG.NVG_IMAGE_NODELETE; +import static org.lwjgl.nanovg.NanoVG.nvgBeginPath; +import static org.lwjgl.nanovg.NanoVG.nvgDeleteImage; +import static org.lwjgl.nanovg.NanoVG.nvgFill; +import static org.lwjgl.nanovg.NanoVG.nvgFillPaint; +import static org.lwjgl.nanovg.NanoVG.nvgImagePattern; +import static org.lwjgl.nanovg.NanoVG.nvgRect; +import static org.lwjgl.nanovg.NanoVGGL2.nvglCreateImageFromHandle; + +import com.spinyowl.spinygui.core.node.Element; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Objects; +import org.lwjgl.nanovg.NVGPaint; +import org.lwjgl.system.MemoryStack; + +/** Draws externally owned OpenGL textures inside ordinary SpinyGUI elements. */ +final class NvgExternalTextureRenderer { + private final Map bindings = new IdentityHashMap<>(); + private final Map images = new IdentityHashMap<>(); + + void bind(Element element, int textureId, int width, int height) { + Objects.requireNonNull(element, "element"); + if (textureId <= 0) throw new IllegalArgumentException("textureId must be positive"); + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException("texture dimensions must be positive"); + } + bindings.put(element, new TextureBinding(textureId, width, height)); + } + + void clear(Element element, long context) { + bindings.remove(element); + ImageBinding image = images.remove(element); + if (image != null && context != 0) nvgDeleteImage(context, image.imageId()); + } + + void render(Element element, long context, NvgRenderer.Profile profile) { + TextureBinding binding = bindings.get(element); + if (binding == null) return; + + ImageBinding image = images.get(element); + if (image == null || !image.texture().equals(binding)) { + if (image != null) nvgDeleteImage(context, image.imageId()); + int imageId = createImage(context, profile, binding); + if (imageId <= 0) { + throw new IllegalStateException("NanoVG failed to wrap external OpenGL texture"); + } + image = new ImageBinding(binding, imageId); + images.put(element, image); + } + + var position = element.layoutAbsolutePosition(); + var size = element.size(); + if (size.x <= 0 || size.y <= 0) return; + + try (MemoryStack stack = MemoryStack.stackPush()) { + NVGPaint paint = NVGPaint.malloc(stack); + nvgImagePattern( + context, + position.x, + position.y, + size.x, + size.y, + 0f, + image.imageId(), + 1f, + paint); + nvgBeginPath(context); + nvgRect(context, position.x, position.y, size.x, size.y); + nvgFillPaint(context, paint); + nvgFill(context); + } + } + + void release(long context) { + if (context != 0) { + images.values().forEach(image -> nvgDeleteImage(context, image.imageId())); + } + images.clear(); + bindings.clear(); + } + + private int createImage(long context, NvgRenderer.Profile profile, TextureBinding binding) { + int flags = NVG_IMAGE_NODELETE | NVG_IMAGE_FLIPY; + return switch (profile) { + case GL2 -> nvglCreateImageFromHandle( + context, binding.textureId(), binding.width(), binding.height(), flags); + case GL3 -> org.lwjgl.nanovg.NanoVGGL3.nvglCreateImageFromHandle( + context, binding.textureId(), binding.width(), binding.height(), flags); + }; + } + + private record TextureBinding(int textureId, int width, int height) {} + private record ImageBinding(TextureBinding texture, int imageId) {} +} From 7a8712474df3ae439aeb10a91b1c80105bd3f98c Mon Sep 17 00:00:00 2001 From: Aliaksandr Shcherbin Date: Tue, 18 Aug 2026 21:11:28 +0200 Subject: [PATCH 2/4] Select NanoVG texture wrapper from active GL profile --- .../nanovg/NvgExternalTextureRenderer.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/spinygui.core.backend.lwjgl.nanovg/src/main/java/com/spinyowl/spinygui/core/backend/renderer/lwjgl/nanovg/NvgExternalTextureRenderer.java b/spinygui.core.backend.lwjgl.nanovg/src/main/java/com/spinyowl/spinygui/core/backend/renderer/lwjgl/nanovg/NvgExternalTextureRenderer.java index 2d0850d4..e860dc75 100644 --- a/spinygui.core.backend.lwjgl.nanovg/src/main/java/com/spinyowl/spinygui/core/backend/renderer/lwjgl/nanovg/NvgExternalTextureRenderer.java +++ b/spinygui.core.backend.lwjgl.nanovg/src/main/java/com/spinyowl/spinygui/core/backend/renderer/lwjgl/nanovg/NvgExternalTextureRenderer.java @@ -8,13 +8,13 @@ import static org.lwjgl.nanovg.NanoVG.nvgFillPaint; import static org.lwjgl.nanovg.NanoVG.nvgImagePattern; import static org.lwjgl.nanovg.NanoVG.nvgRect; -import static org.lwjgl.nanovg.NanoVGGL2.nvglCreateImageFromHandle; import com.spinyowl.spinygui.core.node.Element; import java.util.IdentityHashMap; import java.util.Map; import java.util.Objects; import org.lwjgl.nanovg.NVGPaint; +import org.lwjgl.opengl.GL; import org.lwjgl.system.MemoryStack; /** Draws externally owned OpenGL textures inside ordinary SpinyGUI elements. */ @@ -37,14 +37,14 @@ void clear(Element element, long context) { if (image != null && context != 0) nvgDeleteImage(context, image.imageId()); } - void render(Element element, long context, NvgRenderer.Profile profile) { + void render(Element element, long context) { TextureBinding binding = bindings.get(element); if (binding == null) return; ImageBinding image = images.get(element); if (image == null || !image.texture().equals(binding)) { if (image != null) nvgDeleteImage(context, image.imageId()); - int imageId = createImage(context, profile, binding); + int imageId = createImage(context, binding); if (imageId <= 0) { throw new IllegalStateException("NanoVG failed to wrap external OpenGL texture"); } @@ -83,14 +83,14 @@ void release(long context) { bindings.clear(); } - private int createImage(long context, NvgRenderer.Profile profile, TextureBinding binding) { + private int createImage(long context, TextureBinding binding) { int flags = NVG_IMAGE_NODELETE | NVG_IMAGE_FLIPY; - return switch (profile) { - case GL2 -> nvglCreateImageFromHandle( + if (GL.getCapabilities().OpenGL30) { + return org.lwjgl.nanovg.NanoVGGL3.nvglCreateImageFromHandle( context, binding.textureId(), binding.width(), binding.height(), flags); - case GL3 -> org.lwjgl.nanovg.NanoVGGL3.nvglCreateImageFromHandle( - context, binding.textureId(), binding.width(), binding.height(), flags); - }; + } + return org.lwjgl.nanovg.NanoVGGL2.nvglCreateImageFromHandle( + context, binding.textureId(), binding.width(), binding.height(), flags); } private record TextureBinding(int textureId, int width, int height) {} From 1bb906e66a060c3e2a1df258f345c18949932ba9 Mon Sep 17 00:00:00 2001 From: Aliaksandr Shcherbin Date: Tue, 18 Aug 2026 21:11:47 +0200 Subject: [PATCH 3/4] Add NanoVG renderer with external texture element bindings --- .../nanovg/ExternalTextureNvgRenderer.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 spinygui.core.backend.lwjgl.nanovg/src/main/java/com/spinyowl/spinygui/core/backend/renderer/lwjgl/nanovg/ExternalTextureNvgRenderer.java diff --git a/spinygui.core.backend.lwjgl.nanovg/src/main/java/com/spinyowl/spinygui/core/backend/renderer/lwjgl/nanovg/ExternalTextureNvgRenderer.java b/spinygui.core.backend.lwjgl.nanovg/src/main/java/com/spinyowl/spinygui/core/backend/renderer/lwjgl/nanovg/ExternalTextureNvgRenderer.java new file mode 100644 index 00000000..9e25e16b --- /dev/null +++ b/spinygui.core.backend.lwjgl.nanovg/src/main/java/com/spinyowl/spinygui/core/backend/renderer/lwjgl/nanovg/ExternalTextureNvgRenderer.java @@ -0,0 +1,41 @@ +package com.spinyowl.spinygui.core.backend.renderer.lwjgl.nanovg; + +import com.spinyowl.spinygui.core.node.Element; +import com.spinyowl.spinygui.core.node.Node; + +/** + * NanoVG renderer variant that can present externally owned OpenGL textures inside regular + * SpinyGUI elements. Texture ownership stays with the caller. + */ +public final class ExternalTextureNvgRenderer extends NvgRenderer { + private final NvgElementRenderer elements = new NvgElementRenderer(); + private final NvgBorderRenderer borders = new NvgBorderRenderer(); + private final NvgExternalTextureRenderer textures = new NvgExternalTextureRenderer(); + + public ExternalTextureNvgRenderer() { + super(); + subtreeContentRenderer(this::renderElementContent); + } + + /** Bind an externally owned OpenGL texture to an element. */ + public void bindExternalTexture(Element element, int textureId, int width, int height) { + textures.bind(element, textureId, width, height); + } + + /** Remove an external texture binding without deleting the OpenGL texture. */ + public void clearExternalTexture(Element element) { + textures.clear(element, contextIdentity()); + } + + private void renderElementContent(Node node, long context) { + elements.render(node, context); + if (node instanceof Element element) textures.render(element, context); + borders.render(node, context); + } + + @Override + public void destroy() { + textures.release(contextIdentity()); + super.destroy(); + } +} From 16a83cce736e2edb33bdf291e2c13584d7e96f22 Mon Sep 17 00:00:00 2001 From: Aliaksandr Shcherbin Date: Tue, 18 Aug 2026 21:17:14 +0200 Subject: [PATCH 4/4] Use backend-specific NanoVG no-delete flag --- .../renderer/lwjgl/nanovg/NvgExternalTextureRenderer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spinygui.core.backend.lwjgl.nanovg/src/main/java/com/spinyowl/spinygui/core/backend/renderer/lwjgl/nanovg/NvgExternalTextureRenderer.java b/spinygui.core.backend.lwjgl.nanovg/src/main/java/com/spinyowl/spinygui/core/backend/renderer/lwjgl/nanovg/NvgExternalTextureRenderer.java index e860dc75..b9a61198 100644 --- a/spinygui.core.backend.lwjgl.nanovg/src/main/java/com/spinyowl/spinygui/core/backend/renderer/lwjgl/nanovg/NvgExternalTextureRenderer.java +++ b/spinygui.core.backend.lwjgl.nanovg/src/main/java/com/spinyowl/spinygui/core/backend/renderer/lwjgl/nanovg/NvgExternalTextureRenderer.java @@ -1,7 +1,6 @@ package com.spinyowl.spinygui.core.backend.renderer.lwjgl.nanovg; import static org.lwjgl.nanovg.NanoVG.NVG_IMAGE_FLIPY; -import static org.lwjgl.nanovg.NanoVG.NVG_IMAGE_NODELETE; import static org.lwjgl.nanovg.NanoVG.nvgBeginPath; import static org.lwjgl.nanovg.NanoVG.nvgDeleteImage; import static org.lwjgl.nanovg.NanoVG.nvgFill; @@ -84,11 +83,12 @@ void release(long context) { } private int createImage(long context, TextureBinding binding) { - int flags = NVG_IMAGE_NODELETE | NVG_IMAGE_FLIPY; if (GL.getCapabilities().OpenGL30) { + int flags = org.lwjgl.nanovg.NanoVGGL3.NVG_IMAGE_NODELETE | NVG_IMAGE_FLIPY; return org.lwjgl.nanovg.NanoVGGL3.nvglCreateImageFromHandle( context, binding.textureId(), binding.width(), binding.height(), flags); } + int flags = org.lwjgl.nanovg.NanoVGGL2.NVG_IMAGE_NODELETE | NVG_IMAGE_FLIPY; return org.lwjgl.nanovg.NanoVGGL2.nvglCreateImageFromHandle( context, binding.textureId(), binding.width(), binding.height(), flags); }