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(); + } +} 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..b9a61198 --- /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.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 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. */ +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) { + 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, 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, TextureBinding binding) { + 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); + } + + private record TextureBinding(int textureId, int width, int height) {} + private record ImageBinding(TextureBinding texture, int imageId) {} +}