From 113ef3d5a9b9314771884df497e7d97c6d1ddc84 Mon Sep 17 00:00:00 2001 From: Silas Della Contrada Date: Sun, 15 Sep 2024 16:46:57 +0200 Subject: [PATCH 01/16] feat: start working on wgl context support --- build.gradle.kts | 6 +- native/CMakeLists.txt | 7 +- native/src/cpp/windows/WGLContext.cpp | 16 +++ .../gl/context/GLContextProviderFactory.kt | 4 +- .../silenium/compose/gl/context/WGLContext.kt | 111 ++++++++++++++++++ .../compose/gl/surface/GLSurfaceView.kt | 17 ++- src/test/kotlin/Main.kt | 1 + 7 files changed, 153 insertions(+), 9 deletions(-) create mode 100644 native/src/cpp/windows/WGLContext.cpp create mode 100644 src/main/java/dev/silenium/compose/gl/context/WGLContext.kt diff --git a/build.gradle.kts b/build.gradle.kts index f2e821b..cef2bb6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,7 +20,7 @@ val deployNative = (findProperty("deploy.native") as String?)?.toBoolean() ?: tr val deployKotlin = (findProperty("deploy.kotlin") as String?)?.toBoolean() ?: true val lwjglVersion = "3.3.4" -val lwjglNatives = "natives-linux" +val lwjglNatives = arrayOf("natives-linux", "natives-windows") dependencies { implementation(compose.desktop.common) @@ -35,7 +35,9 @@ dependencies { api(libs.lwjgl.egl) libs.bundles.lwjgl.natives.get().forEach { api(it) - runtimeOnly(variantOf(provider { it }) { classifier(lwjglNatives) }) + lwjglNatives.forEach { native -> + runtimeOnly(variantOf(provider { it }) { classifier(native) }) + } } implementation(libs.bundles.kotlinx.coroutines) diff --git a/native/CMakeLists.txt b/native/CMakeLists.txt index 3c327f4..fbf2d3f 100644 --- a/native/CMakeLists.txt +++ b/native/CMakeLists.txt @@ -14,9 +14,7 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON) -set(SOURCES - -) +set(SOURCES) if (CMAKE_SYSTEM_NAME STREQUAL "Linux") if (NOT DEFINED JAVA_HOME) @@ -27,6 +25,9 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Linux") src/cpp/linux/GLXContext.cpp ) elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows") + list(APPEND SOURCES + src/cpp/windows/WGLContext.cpp + ) endif () add_library(${PROJECT_NAME} SHARED ${SOURCES}) diff --git a/native/src/cpp/windows/WGLContext.cpp b/native/src/cpp/windows/WGLContext.cpp new file mode 100644 index 0000000..a2e1d05 --- /dev/null +++ b/native/src/cpp/windows/WGLContext.cpp @@ -0,0 +1,16 @@ +// +// Created by silenium-dev on 2024-09-15. +// + +#include +#include +#include +#include + +extern "C" { +JNIEXPORT jlong JNICALL Java_dev_silenium_compose_gl_context_WGLContextKt_wglCreateContext(JNIEnv *env, jobject thiz, jlong _hdc) { + const auto hdc = reinterpret_cast(_hdc); + const auto ctx = wglCreateContext(hdc); + return reinterpret_cast(ctx); +} +} diff --git a/src/main/java/dev/silenium/compose/gl/context/GLContextProviderFactory.kt b/src/main/java/dev/silenium/compose/gl/context/GLContextProviderFactory.kt index c489a36..8c2014d 100644 --- a/src/main/java/dev/silenium/compose/gl/context/GLContextProviderFactory.kt +++ b/src/main/java/dev/silenium/compose/gl/context/GLContextProviderFactory.kt @@ -21,11 +21,13 @@ object GLContextProviderFactory { private enum class GLContextProviderType(val provider: GLContextProvider<*>) { EGL(EGLContext), - GLX(GLXContext); + GLX(GLXContext), + WGL(WGLContext), } private val osOrder = mapOf( Platform.OS.LINUX to listOf(GLContextProviderType.EGL, GLContextProviderType.GLX), + Platform.OS.WINDOWS to listOf(GLContextProviderType.WGL), ) /** diff --git a/src/main/java/dev/silenium/compose/gl/context/WGLContext.kt b/src/main/java/dev/silenium/compose/gl/context/WGLContext.kt new file mode 100644 index 0000000..aeb7c81 --- /dev/null +++ b/src/main/java/dev/silenium/compose/gl/context/WGLContext.kt @@ -0,0 +1,111 @@ +package dev.silenium.compose.gl.context + +import dev.silenium.libs.jni.NativeLoader +import org.lwjgl.opengl.GL +import org.lwjgl.opengl.GLCapabilities +import org.lwjgl.opengl.WGL +import org.lwjgl.opengl.WGLCapabilities +import java.util.concurrent.ConcurrentHashMap + +data class WGLContext( + val deviceContext: Long, + val renderingContext: Long, +) : GLContext { + @Transient + override val provider: GLContextProvider = Companion + + @Transient + lateinit var wglCapabilities: WGLCapabilities + + @Transient + override lateinit var glCapabilities: GLCapabilities + + override fun unbindCurrent() { + WGL.wglMakeCurrent(0L, 0L) + } + + override fun makeCurrent() { + if (provider.fromCurrent() != this) { + check(WGL.wglMakeCurrent(deviceContext, renderingContext)) { + "Failed to make context current" + } + } + val (wglCap, glCap) = contextCapabilities.compute(this) { key, value -> + value?.let { + it.copy(refCount = it.refCount + 1) + } ?: restorePrevious { + ContextCapabilities(GL.createCapabilitiesWGL(), GL.createCapabilities(), 1) + } + }!! + wglCapabilities = wglCap + glCapabilities = glCap + } + + override fun destroy() { + contextCapabilities.compute(this) { key, value -> + if (value == null) { + WGL.wglMakeCurrent(0L, 0L) + WGL.wglDeleteContext(key.renderingContext) + return@compute null + } + val refCount = value.refCount - 1 + if (refCount == 0) { + WGL.wglMakeCurrent(0L, 0L) + WGL.wglDeleteContext(key.renderingContext) + return@compute null + } + value.copy(refCount = refCount) + } + } + + override fun deriveOffscreenContext() = provider.createOffscreen(this) + + companion object : GLContextProvider { + private data class ContextCapabilities( + val egl: WGLCapabilities, + val gl: GLCapabilities, + val refCount: Int, + ) + + private val contextCapabilities = ConcurrentHashMap() + + init { + NativeLoader.loadLibraryFromClasspath("compose-gl").getOrThrow() + } + + override fun restorePrevious(block: () -> R): R { + val displayContext = WGL.wglGetCurrentDC() + val renderingContext = WGL.wglGetCurrentContext() + return block().also { + WGL.wglMakeCurrent(displayContext, renderingContext) + } + } + + override fun fromCurrent(): WGLContext? { + val deviceContext = WGL.wglGetCurrentDC() + val renderingContext = WGL.wglGetCurrentContext() + return if (deviceContext != 0L && renderingContext != 0L) { + WGLContext(deviceContext, renderingContext) + } else { + null + } + } + + override fun isCurrent(): Boolean { + val deviceContext = WGL.wglGetCurrentDC() + val renderingContext = WGL.wglGetCurrentContext() + return deviceContext != 0L && renderingContext != 0L + } + + override fun createOffscreen(parent: WGLContext): WGLContext { + val deviceContext = parent.deviceContext + val renderingContext = WGL.wglCreateContext(deviceContext) + check(WGL.wglShareLists(parent.renderingContext, renderingContext)) { + "Failed to share context lists" + } + return WGLContext(deviceContext, renderingContext) + } + } +} + +private external fun wglCreateContext(deviceContext: Long): Long diff --git a/src/main/java/dev/silenium/compose/gl/surface/GLSurfaceView.kt b/src/main/java/dev/silenium/compose/gl/surface/GLSurfaceView.kt index b26aa22..85a846e 100644 --- a/src/main/java/dev/silenium/compose/gl/surface/GLSurfaceView.kt +++ b/src/main/java/dev/silenium/compose/gl/surface/GLSurfaceView.kt @@ -117,7 +117,10 @@ fun GLSurfaceView( DisposableEffect(surfaceView) { val job = surfaceView.launch() onDispose { - job.cancel() + runBlocking { + println("Cancelling GLSurfaceView job") + job.cancelAndJoin() + } } } LaunchedEffect(fboSizeOverride) { @@ -168,6 +171,9 @@ class GLSurfaceView internal constructor( internal fun resize(size: IntSize) { if (size == fboPool?.size) return this.size = size + if (renderContext == null) { + renderContext = parentContext.deriveOffscreenContext() + } fboPool?.size = size state.requestUpdate() } @@ -206,7 +212,6 @@ class GLSurfaceView internal constructor( } private fun initialize() { - renderContext = parentContext.deriveOffscreenContext() renderContext!!.makeCurrent() directContext = DirectContext.makeGL() @@ -216,13 +221,14 @@ class GLSurfaceView internal constructor( @OptIn(ExperimentalCoroutinesApi::class) private suspend fun run() = coroutineScope { - while (size == IntSize.Zero && isActive) delay(10.milliseconds) + while (renderContext == null && isActive) delay(10.milliseconds) if (!isActive) return@coroutineScope initialize() var lastFrame: Long? = null while (isActive) { val renderStart = System.nanoTime() val deltaTime = lastFrame?.let { renderStart - it } ?: 0 + println("Rendering frame with delta time $deltaTime") val waitTime = fboPool!!.render(deltaTime.nanoseconds, drawBlock) ?: continue invalidate() val renderEnd = System.nanoTime() @@ -240,12 +246,17 @@ class GLSurfaceView internal constructor( } } cleanupBlock() + println("GLSurfaceView job cancelled") fboPool?.destroy() fboPool = null + println("FBO pool destroyed") directContext?.close() directContext = null + println("Direct context closed") + renderContext?.unbindCurrent() renderContext?.destroy() renderContext = null + println("Render context destroyed") } companion object { diff --git a/src/test/kotlin/Main.kt b/src/test/kotlin/Main.kt index f2c975a..fd84441 100644 --- a/src/test/kotlin/Main.kt +++ b/src/test/kotlin/Main.kt @@ -78,6 +78,7 @@ fun ApplicationScope.App() { } suspend fun main() = awaitApplication { + System.setProperty("skiko.renderApi", "OPENGL") Window(onCloseRequest = ::exitApplication) { App() } From 8866de7d67eff327820a2c517598d2ca6b650f72 Mon Sep 17 00:00:00 2001 From: Silas Della Contrada Date: Sat, 4 Oct 2025 02:28:27 +0200 Subject: [PATCH 02/16] feat: first experiments on directly importing opengl textures as Skia images --- src/test/kotlin/direct_import/Main.kt | 182 ++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 src/test/kotlin/direct_import/Main.kt diff --git a/src/test/kotlin/direct_import/Main.kt b/src/test/kotlin/direct_import/Main.kt new file mode 100644 index 0000000..e74e98f --- /dev/null +++ b/src/test/kotlin/direct_import/Main.kt @@ -0,0 +1,182 @@ +package direct_import + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext +import org.jetbrains.skia.* +import org.jetbrains.skiko.SkiaLayer +import org.lwjgl.BufferUtils +import org.lwjgl.opengl.GL +import org.lwjgl.opengl.GL11 +import org.lwjgl.opengl.GL11.GL_RGBA +import org.lwjgl.opengl.GL11.GL_RGBA8 +import org.lwjgl.opengl.GL11.GL_TEXTURE_2D +import java.awt.Window +import java.io.File +import javax.imageio.ImageIO + +fun main() = application { + System.setProperty("skiko.renderApi", "OPENGL") + var textureId: Int? = null + var textureSize: Pair = 0 to 0 + Window( + ::exitApplication, + title = "Direct Render Test", + ) { + val window = LocalWindow.current!! + var directContext by remember { mutableStateOf(null) } + LaunchedEffect(window) { + withContext(Dispatchers.IO) { + while (isActive) { + window.directContext()?.let { + directContext = it + return@withContext + } + } + } + } + Canvas(Modifier.fillMaxSize()) { + val context = directContext ?: return@Canvas + if (textureId == null) { + val img = "image.png" + val (id, size) = loadTexture(File(img)) + textureId = id + textureSize = size + } + GL11.glClear(GL11.GL_COLOR_BUFFER_BIT) + + GL11.glEnable(GL_TEXTURE_2D) + GL11.glBindTexture(GL_TEXTURE_2D, textureId) + + GL11.glBegin(GL11.GL_QUADS) + GL11.glTexCoord2f(0f, 0f) + GL11.glVertex2f(-1f, -1f) + GL11.glTexCoord2f(1f, 0f) + GL11.glVertex2f(1f, -1f) + GL11.glTexCoord2f(1f, 1f) + GL11.glVertex2f(1f, 1f) + GL11.glTexCoord2f(0f, 1f) + GL11.glVertex2f(-1f, 1f) + GL11.glEnd() + + GL11.glDisable(GL_TEXTURE_2D) + + val backendTex = BackendTexture.makeGL( + width = textureSize.first, + height = textureSize.second, + isMipmapped = false, + textureId = textureId, + textureTarget = GL_TEXTURE_2D, + textureFormat = GL_RGBA8, + ) + try { + val image = Image.adoptTextureFrom( + context, + backendTex, + SurfaceOrigin.BOTTOM_LEFT, + ColorType.RGBA_8888, + ) + drawContext.canvas.nativeCanvas.drawImage(image, 0f, 0f) + } catch (_: Throwable) { + var err = GL11.glGetError() + while (err != GL11.GL_NO_ERROR) { + println("GL Error: $err") + err = GL11.glGetError() + } + throw RuntimeException( + "Failed to create Image from BackendTexture: $backendTex" + ) + } + } + } +} + +@Suppress("UNCHECKED_CAST") +val LocalWindow: CompositionLocal by lazy { + val clazz = Class.forName("androidx.compose.ui.window.LocalWindowKt") + val method = clazz.getMethod("getLocalWindow") + method.invoke(null) as CompositionLocal +} + +fun Window.directContext(): DirectContext? { + fun Any.getFieldValue(fieldName: String): Any? { + val field = this::class.java.getDeclaredField(fieldName) + field.isAccessible = true + return field.get(this) + } + + val composePanel = this.getFieldValue("composePanel")!! + val composeContainer = composePanel.getFieldValue("_composeContainer")!! + val mediator = composeContainer.getFieldValue("mediator")!! + val contentComponent = mediator.let { + val getter = it::class.java.getMethod("getContentComponent") + getter.invoke(it) as SkiaLayer + } + val redrawer = contentComponent.let { + val getter = it::class.java.getMethod("getRedrawer${'$'}skiko") + getter.invoke(it) + } + val contextHandler = redrawer.getFieldValue("contextHandler")!! + val surface = contextHandler.let { + val getter = it::class.java.superclass.superclass.getDeclaredMethod("getSurface") + getter.isAccessible = true + getter.invoke(it) as? Surface + } + return surface?.recordingContext +} + +fun loadTexture(file: File): Pair> { + val image = ImageIO.read(file) + + val width = image.width + val height = image.height + + // Convert image to RGBA + val pixels = IntArray(width * height) + image.getRGB(0, 0, width, height, pixels, 0, width) + + val buffer = BufferUtils.createByteBuffer(width * height * 4) + + // OpenGL expects bottom-to-top, so flip vertically + for (y in height - 1 downTo 0) { + for (x in 0.. Date: Sat, 4 Oct 2025 17:26:20 +0200 Subject: [PATCH 03/16] feat: working direct opengl rendering inside canvas draw call --- .../silenium/compose/gl/CompositionLocals.kt | 68 ++--- .../compose/gl/direct/GLDirectCanvas.kt | 128 ++++++++ .../java/dev/silenium/compose/gl/fbo/FBO.kt | 12 + src/test/kotlin/direct_import/Main.kt | 282 ++++++++++-------- 4 files changed, 331 insertions(+), 159 deletions(-) create mode 100644 src/main/java/dev/silenium/compose/gl/direct/GLDirectCanvas.kt diff --git a/src/main/java/dev/silenium/compose/gl/CompositionLocals.kt b/src/main/java/dev/silenium/compose/gl/CompositionLocals.kt index 4b94fa7..5d736e4 100644 --- a/src/main/java/dev/silenium/compose/gl/CompositionLocals.kt +++ b/src/main/java/dev/silenium/compose/gl/CompositionLocals.kt @@ -2,11 +2,9 @@ package dev.silenium.compose.gl import androidx.compose.runtime.CompositionLocal import org.jetbrains.skia.DirectContext +import org.jetbrains.skia.Surface import org.jetbrains.skiko.SkiaLayer import java.awt.Window -import kotlin.reflect.KProperty1 -import kotlin.reflect.full.memberProperties -import kotlin.reflect.jvm.isAccessible @Suppress("UNCHECKED_CAST") val LocalWindow: CompositionLocal by lazy { @@ -15,43 +13,29 @@ val LocalWindow: CompositionLocal by lazy { method.invoke(null) as CompositionLocal } -@Suppress("UNCHECKED_CAST") -internal fun Window.directContext(): DirectContext? { - val composePanelProp = this::class.memberProperties.first { it.name == "composePanel" } as KProperty1 - composePanelProp.isAccessible = true - val composePanel = composePanelProp.get(this) -// println("composePanel: $composePanel") - val composeContainerProp = - composePanel::class.memberProperties.first { it.name == "_composeContainer" } as KProperty1 - composeContainerProp.isAccessible = true - val composeContainer = composeContainerProp.get(composePanel) -// println("composeContainer: $composeContainer") - val mediatorProp = composeContainer::class.memberProperties.first { it.name == "mediator" } as KProperty1 - mediatorProp.isAccessible = true - val mediator = mediatorProp.get(composeContainer) -// println("mediator: $mediator") - val skiaLayerComponentProp = - mediator::class.memberProperties.first { it.name == "skiaLayerComponent" } as KProperty1 - skiaLayerComponentProp.isAccessible = true - val skiaLayerComponent = skiaLayerComponentProp.get(mediator) -// println("skiaLayerComponent: $skiaLayerComponent") - val contentComponentProp = - skiaLayerComponent::class.memberProperties.first { it.name == "contentComponent" } as KProperty1 - contentComponentProp.isAccessible = true - val contentComponent = contentComponentProp.get(skiaLayerComponent) as SkiaLayer -// println("contentComponent: $contentComponent") - val redrawerProp = contentComponent::class.memberProperties.first { it.name == "redrawer" } as KProperty1 - redrawerProp.isAccessible = true - val redrawer = redrawerProp.get(contentComponent) -// println("redrawer: $redrawer") - val contextHandlerProp = - redrawer::class.memberProperties.first { it.name == "contextHandler" } as KProperty1 - contextHandlerProp.isAccessible = true - val contextHandler = contextHandlerProp.get(redrawer) -// println("contextHandler: $contextHandler") - val contextProp = contextHandler::class.memberProperties.first { it.name == "context" } as KProperty1 - contextProp.isAccessible = true - val context = contextProp.get(contextHandler) as DirectContext? -// println("context: $context") - return context +fun Window.directContext(): DirectContext? { + fun Any.getFieldValue(fieldName: String): Any? { + val field = this::class.java.getDeclaredField(fieldName) + field.isAccessible = true + return field.get(this) + } + + val composePanel = this.getFieldValue("composePanel")!! + val composeContainer = composePanel.getFieldValue("_composeContainer")!! + val mediator = composeContainer.getFieldValue("mediator")!! + val contentComponent = mediator.let { + val getter = it::class.java.getMethod("getContentComponent") + getter.invoke(it) as SkiaLayer + } + val redrawer = contentComponent.let { + val getter = it::class.java.getMethod("getRedrawer${'$'}skiko") + getter.invoke(it) + } + val contextHandler = redrawer.getFieldValue("contextHandler")!! + val surface = contextHandler.let { + val getter = it::class.java.superclass.superclass.getDeclaredMethod("getSurface") + getter.isAccessible = true + getter.invoke(it) as? Surface + } + return surface?.recordingContext } diff --git a/src/main/java/dev/silenium/compose/gl/direct/GLDirectCanvas.kt b/src/main/java/dev/silenium/compose/gl/direct/GLDirectCanvas.kt new file mode 100644 index 0000000..b9fc62b --- /dev/null +++ b/src/main/java/dev/silenium/compose/gl/direct/GLDirectCanvas.kt @@ -0,0 +1,128 @@ +package dev.silenium.compose.gl.direct + +import androidx.compose.foundation.Canvas +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.unit.toIntSize +import dev.silenium.compose.gl.LocalWindow +import dev.silenium.compose.gl.directContext +import dev.silenium.compose.gl.fbo.FBO +import dev.silenium.compose.gl.fbo.FBODrawScope +import dev.silenium.compose.gl.fbo.draw +import dev.silenium.compose.gl.objects.Renderbuffer +import dev.silenium.compose.gl.objects.Texture +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext +import org.jetbrains.skia.BackendTexture +import org.jetbrains.skia.ColorType +import org.jetbrains.skia.DirectContext +import org.jetbrains.skia.Image +import org.jetbrains.skia.SurfaceOrigin +import org.lwjgl.opengl.GL +import org.lwjgl.opengl.GL11 +import org.lwjgl.opengl.GL30 +import org.lwjgl.opengl.GL33 +import org.lwjgl.opengl.GLCapabilities +import org.slf4j.LoggerFactory + +private class GLDirectCanvasState { + var fbo: FBO? = null + var image: Image? = null + var texture: BackendTexture? = null + var initialized = false + lateinit var glCapabilities: GLCapabilities + var directContext: DirectContext? by mutableStateOf(null) + var size: Size = Size.Zero + + fun draw(scope: DrawScope, block: FBODrawScope.() -> Unit) { + val ctx = directContext ?: return log.warn("No direct context") + ctx.submit(syncCpu = true) + ensureInitialized() + ensureFBO(scope.size, ctx) + fbo!!.draw(block) + ctx.resetGLAll() + } + + fun display(scope: DrawScope) { + directContext ?: return log.warn("No direct context") + val img = image ?: return log.warn("No image") + ensureInitialized() + scope.drawContext.canvas.nativeCanvas.drawImage(img, 0f, 0f) + } + + private fun ensureInitialized() { + if (!initialized) { + glCapabilities = GL.createCapabilities() + initialized = true + } + } + + private fun ensureFBO(size: Size, ctx: DirectContext) { + if (size != this.size) { + image?.close() + fbo?.id?.let(GL30::glDeleteFramebuffers) + fbo?.depthStencilAttachment?.destroy() + val fbo = createFBO(size).also { this.fbo = it } + this.size = size + + val texture = BackendTexture.makeGL( + width = fbo.size.width, + height = fbo.size.height, + isMipmapped = false, + textureId = fbo.colorAttachment.id, + textureTarget = fbo.colorAttachment.target, + textureFormat = fbo.colorAttachment.internalFormat, + ).also { this.texture = it } + image = Image.adoptTextureFrom( + context = ctx, + backendTexture = texture, + origin = SurfaceOrigin.BOTTOM_LEFT, + colorType = ColorType.RGBA_8888, + ) + } + } + + companion object { + private val log = LoggerFactory.getLogger(GLDirectCanvasState::class.java) + } +} + +@Composable +fun GLDirectCanvas(modifier: Modifier = Modifier, block: FBODrawScope.() -> Unit) { + val state = remember { GLDirectCanvasState() } + val window = LocalWindow.current ?: throw IllegalStateException("No window") + LaunchedEffect(window) { + withContext(Dispatchers.IO) { + while (isActive) { + window.directContext()?.let { + state.directContext = it + return@withContext + } + } + } + } + Canvas(modifier) { + state.directContext ?: return@Canvas + state.draw(this, block) + state.display(this) + } +} + +private fun createFBO(size: Size): FBO { + val colorAttachment = Texture.create( + target = GL11.GL_TEXTURE_2D, size = size.toIntSize(), internalFormat = GL11.GL_RGBA8, + wrapS = GL33.GL_CLAMP_TO_EDGE, wrapT = GL33.GL_CLAMP_TO_EDGE, + minFilter = GL33.GL_NEAREST, magFilter = GL33.GL_NEAREST, + ) + val depthStencil = Renderbuffer.create(size.toIntSize(), GL33.GL_DEPTH24_STENCIL8) + return FBO.create(colorAttachment, depthStencil) +} diff --git a/src/main/java/dev/silenium/compose/gl/fbo/FBO.kt b/src/main/java/dev/silenium/compose/gl/fbo/FBO.kt index a5dfb94..2fa2431 100644 --- a/src/main/java/dev/silenium/compose/gl/fbo/FBO.kt +++ b/src/main/java/dev/silenium/compose/gl/fbo/FBO.kt @@ -83,3 +83,15 @@ data class FBO( } } } + +data class FBODrawScope(val fbo: FBO) + +inline fun FBO.draw(block: FBODrawScope.() -> T): T { + bind() + try { + val scope = FBODrawScope(this) + return scope.block() + } finally { + unbind() + } +} diff --git a/src/test/kotlin/direct_import/Main.kt b/src/test/kotlin/direct_import/Main.kt index e74e98f..09d5316 100644 --- a/src/test/kotlin/direct_import/Main.kt +++ b/src/test/kotlin/direct_import/Main.kt @@ -1,135 +1,182 @@ package direct_import -import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.* +import androidx.compose.material.Button +import androidx.compose.material.Text +import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.window.Window import androidx.compose.ui.window.application -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.isActive -import kotlinx.coroutines.withContext -import org.jetbrains.skia.* -import org.jetbrains.skiko.SkiaLayer +import dev.silenium.compose.gl.direct.GLDirectCanvas import org.lwjgl.BufferUtils import org.lwjgl.opengl.GL -import org.lwjgl.opengl.GL11 -import org.lwjgl.opengl.GL11.GL_RGBA -import org.lwjgl.opengl.GL11.GL_RGBA8 -import org.lwjgl.opengl.GL11.GL_TEXTURE_2D -import java.awt.Window +import org.lwjgl.opengl.GL33.* import java.io.File import javax.imageio.ImageIO +import kotlin.random.Random +//language=glsl +const val VERTEX_SHADER_SOURCE = """ +#version 330 core + +// Input vertex data, different for all executions of this shader. +layout(location = 0) in vec3 vertexPosition; +layout(location = 1) in vec2 vertexUV; + +// Output data ; will be interpolated for each fragment. +out vec2 UV; + +void main(){ + // Output position of the vertex, in clip space : MVP * position + gl_Position = vec4(vertexPosition,1); + + // UV of the vertex. No special space for this one. + UV = vertexUV; +} +""" + +//language=glsl +const val FRAGMENT_SHADER_SOURCE = """ +#version 330 core + +// Interpolated values from the vertex shaders +in vec2 UV; + +// Output data +out vec4 color; + +// Values that stay constant for the whole mesh. +uniform sampler2D myTextureSampler; + +void main(){ + + // Output color = color of the texture at the specified UV + color = vec4(texture( myTextureSampler, UV ).rgb, 1.0); +// color = vec4(UV.xy, 0.0, 1.0); +} +""" + +@OptIn(ExperimentalUnsignedTypes::class) fun main() = application { System.setProperty("skiko.renderApi", "OPENGL") - var textureId: Int? = null - var textureSize: Pair = 0 to 0 + var textureId = 0 + var initialized = false + var shaderProgram = 0 + var vao = 0 + var vbo = 0 + var ibo = 0 Window( ::exitApplication, title = "Direct Render Test", ) { - val window = LocalWindow.current!! - var directContext by remember { mutableStateOf(null) } - LaunchedEffect(window) { - withContext(Dispatchers.IO) { - while (isActive) { - window.directContext()?.let { - directContext = it - return@withContext - } + Box(Modifier.fillMaxSize()) { + DisposableEffect(Unit) { + onDispose { + glDeleteProgram(shaderProgram) + glDeleteVertexArrays(vao) + glDeleteBuffers(vbo) + glDeleteBuffers(ibo) + glDeleteTextures(textureId) + GL.destroy() + initialized = false + shaderProgram = 0 + vao = 0 + vbo = 0 + ibo = 0 + textureId = 0 + println("Disposed") } } - } - Canvas(Modifier.fillMaxSize()) { - val context = directContext ?: return@Canvas - if (textureId == null) { - val img = "image.png" - val (id, size) = loadTexture(File(img)) - textureId = id - textureSize = size - } - GL11.glClear(GL11.GL_COLOR_BUFFER_BIT) - - GL11.glEnable(GL_TEXTURE_2D) - GL11.glBindTexture(GL_TEXTURE_2D, textureId) - - GL11.glBegin(GL11.GL_QUADS) - GL11.glTexCoord2f(0f, 0f) - GL11.glVertex2f(-1f, -1f) - GL11.glTexCoord2f(1f, 0f) - GL11.glVertex2f(1f, -1f) - GL11.glTexCoord2f(1f, 1f) - GL11.glVertex2f(1f, 1f) - GL11.glTexCoord2f(0f, 1f) - GL11.glVertex2f(-1f, 1f) - GL11.glEnd() - - GL11.glDisable(GL_TEXTURE_2D) - - val backendTex = BackendTexture.makeGL( - width = textureSize.first, - height = textureSize.second, - isMipmapped = false, - textureId = textureId, - textureTarget = GL_TEXTURE_2D, - textureFormat = GL_RGBA8, - ) - try { - val image = Image.adoptTextureFrom( - context, - backendTex, - SurfaceOrigin.BOTTOM_LEFT, - ColorType.RGBA_8888, - ) - drawContext.canvas.nativeCanvas.drawImage(image, 0f, 0f) - } catch (_: Throwable) { - var err = GL11.glGetError() - while (err != GL11.GL_NO_ERROR) { - println("GL Error: $err") - err = GL11.glGetError() + GLDirectCanvas(Modifier.matchParentSize()) { + if (!initialized) { + val img = "image.png" + val (id, size) = loadTexture(File(img)) + textureId = id + + shaderProgram = glCreateProgram() + val vertexShader = glCreateShader(GL_VERTEX_SHADER) + val fragmentShader = glCreateShader(GL_FRAGMENT_SHADER) + glShaderSource(vertexShader, VERTEX_SHADER_SOURCE) + glCompileShader(vertexShader) + if (glGetShaderi(vertexShader, GL_COMPILE_STATUS) == GL_FALSE) { + println("Vertex shader compilation failed: ${glGetShaderInfoLog(vertexShader)}") + } + glShaderSource(fragmentShader, FRAGMENT_SHADER_SOURCE) + glCompileShader(fragmentShader) + if (glGetShaderi(fragmentShader, GL_COMPILE_STATUS) == GL_FALSE) { + println("Fragment shader compilation failed: ${glGetShaderInfoLog(fragmentShader)}") + } + glAttachShader(shaderProgram, vertexShader) + glAttachShader(shaderProgram, fragmentShader) + glLinkProgram(shaderProgram) + if (glGetProgrami(shaderProgram, GL_LINK_STATUS) == GL_FALSE) { + println("Shader program linking failed: ${glGetProgramInfoLog(shaderProgram)}") + } + glUseProgram(shaderProgram) + val loc = glGetUniformLocation(shaderProgram, "myTextureSampler") + glUniform1i(loc, 0) + glUseProgram(0) + + val vertices = floatArrayOf( + // aPosition | aTexCoords + 0.5f, 0.5f, 0.0f, 1.0f, 1.0f, + 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, + -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, + -0.5f, 0.5f, 0.0f, 0.0f, 1.0f + ) + val indices = intArrayOf( + 0, 1, 3, + 1, 2, 3 + ) + vao = glGenVertexArrays() + glBindVertexArray(vao) + + vbo = glGenBuffers() + glBindBuffer(GL_ARRAY_BUFFER, vbo) + glBufferData(GL_ARRAY_BUFFER, vertices, GL_STATIC_DRAW) + + ibo = glGenBuffers() + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo) + glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices, GL_STATIC_DRAW) + + glVertexAttribPointer(0, 3, GL_FLOAT, false, 5 * Float.SIZE_BYTES, 0) + glEnableVertexAttribArray(0) + glVertexAttribPointer(1, 2, GL_FLOAT, false, 5 * Float.SIZE_BYTES, 3L * Float.SIZE_BYTES) + glEnableVertexAttribArray(1) + glBindVertexArray(0) + glBindBuffer(GL_ARRAY_BUFFER, 0) + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0) + + initialized = true } - throw RuntimeException( - "Failed to create Image from BackendTexture: $backendTex" - ) - } - } - } -} -@Suppress("UNCHECKED_CAST") -val LocalWindow: CompositionLocal by lazy { - val clazz = Class.forName("androidx.compose.ui.window.LocalWindowKt") - val method = clazz.getMethod("getLocalWindow") - method.invoke(null) as CompositionLocal -} + println("redrawing (${fbo.size})") + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) -fun Window.directContext(): DirectContext? { - fun Any.getFieldValue(fieldName: String): Any? { - val field = this::class.java.getDeclaredField(fieldName) - field.isAccessible = true - return field.get(this) - } + glClearColor(Random.nextFloat(), Random.nextFloat(), Random.nextFloat(), 1f) + glClear(GL_COLOR_BUFFER_BIT) - val composePanel = this.getFieldValue("composePanel")!! - val composeContainer = composePanel.getFieldValue("_composeContainer")!! - val mediator = composeContainer.getFieldValue("mediator")!! - val contentComponent = mediator.let { - val getter = it::class.java.getMethod("getContentComponent") - getter.invoke(it) as SkiaLayer - } - val redrawer = contentComponent.let { - val getter = it::class.java.getMethod("getRedrawer${'$'}skiko") - getter.invoke(it) - } - val contextHandler = redrawer.getFieldValue("contextHandler")!! - val surface = contextHandler.let { - val getter = it::class.java.superclass.superclass.getDeclaredMethod("getSurface") - getter.isAccessible = true - getter.invoke(it) as? Surface + glBindVertexArray(vao) + glUseProgram(shaderProgram) + glActiveTexture(GL_TEXTURE0) + glBindTexture(GL_TEXTURE_2D, textureId) + + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0) + + glBindTexture(GL_TEXTURE_2D, 0) + glBindVertexArray(0) + glUseProgram(0) + glDisable(GL_BLEND) + + glFinish() + } + Button(onClick = { println("button pressed") }) { + Text("Button") + } + } } - return surface?.recordingContext } fun loadTexture(file: File): Pair> { @@ -158,25 +205,26 @@ fun loadTexture(file: File): Pair> { buffer.flip() GL.createCapabilities() - val textureID = GL11.glGenTextures() - GL11.glBindTexture(GL_TEXTURE_2D, textureID) + val textureID = glGenTextures() + glBindTexture(GL_TEXTURE_2D, textureID) - GL11.glTexParameteri(GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP) - GL11.glTexParameteri(GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP) - GL11.glTexParameteri(GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR) - GL11.glTexParameteri(GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) - GL11.glTexImage2D( + glTexImage2D( GL_TEXTURE_2D, 0, - GL11.GL_RGBA8, + GL_RGBA8, width, height, 0, GL_RGBA, - GL11.GL_UNSIGNED_BYTE, + GL_UNSIGNED_BYTE, buffer ) + glBindTexture(GL_TEXTURE_2D, 0) return textureID to (width to height) } From 838f79e0627961a006fe42e53b000f0cd415dbfb Mon Sep 17 00:00:00 2001 From: Silas Della Contrada Date: Sat, 4 Oct 2025 17:27:59 +0200 Subject: [PATCH 04/16] ci: bump jdk version to 17 --- .github/workflows/build.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index f4fbdc2..b30c489 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -27,7 +27,7 @@ jobs: repo-username: ${{ secrets.REPOSILITE_USERNAME }} repo-password: ${{ secrets.REPOSILITE_PASSWORD }} tests: false - java-version: 11 + java-version: 17 platform: ${{ github.job }} kotlin: runs-on: ubuntu-22.04 @@ -40,4 +40,4 @@ jobs: repo-username: ${{ secrets.REPOSILITE_USERNAME }} repo-password: ${{ secrets.REPOSILITE_PASSWORD }} tests: false - java-version: 11 + java-version: 17 From c9dc51201e932dcc25a1c295b4c74a20424dbb25 Mon Sep 17 00:00:00 2001 From: silenium-dev Date: Sat, 4 Oct 2025 18:29:02 +0200 Subject: [PATCH 05/16] build: add foojay-resolver --- settings.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/settings.gradle.kts b/settings.gradle.kts index c2249a3..d132e06 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,6 +6,9 @@ pluginManagement { mavenCentral() } } +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} rootProject.name = "compose-gl" From b298bc35115dc76d7d015f6c4c3dbcdd26b6ad0a Mon Sep 17 00:00:00 2001 From: silenium-dev Date: Sun, 5 Oct 2025 13:58:28 +0200 Subject: [PATCH 06/16] feat: first working draft of opengl texture in d3d compose --- native/.gitignore | 1 + native/CMakeLists.txt | 43 ++- native/src/cpp/windows/D3DInterop.cpp | 139 ++++++++ .../silenium/compose/gl/CompositionLocals.kt | 62 +++- .../silenium/compose/gl/context/WGLContext.kt | 24 +- .../silenium/compose/gl/interop/Compat.java | 9 + .../silenium/compose/gl/interop/D3DInterop.kt | 42 +++ .../silenium/compose/gl/objects/Texture.kt | 1 + .../gl/util/DoubleDestructionProtection.kt | 14 + .../kotlin/direct_import/GLTextureDrawer.kt | 163 ++++++++++ src/test/kotlin/direct_import/Main.kt | 298 ++++++++++++------ 11 files changed, 668 insertions(+), 128 deletions(-) create mode 100644 native/src/cpp/windows/D3DInterop.cpp create mode 100644 src/main/java/dev/silenium/compose/gl/interop/Compat.java create mode 100644 src/main/java/dev/silenium/compose/gl/interop/D3DInterop.kt create mode 100644 src/test/kotlin/direct_import/GLTextureDrawer.kt diff --git a/native/.gitignore b/native/.gitignore index 957a68f..4027f6e 100644 --- a/native/.gitignore +++ b/native/.gitignore @@ -1,3 +1,4 @@ .idea/ *.iml cmake-build-*/ +third_party/ diff --git a/native/CMakeLists.txt b/native/CMakeLists.txt index fbf2d3f..f6327dd 100644 --- a/native/CMakeLists.txt +++ b/native/CMakeLists.txt @@ -2,6 +2,13 @@ cmake_minimum_required(VERSION 3.16) if (NOT DEFINED PROJECT_NAME) set(PROJECT_NAME "compose-gl") endif () + +if (NOT DEFINED JAVA_HOME) + if (DEFINED ENV{JAVA_HOME}) + set(JAVA_HOME "$ENV{JAVA_HOME}") + endif () +endif () + if (NOT DEFINED JAVA_HOME) message(FATAL_ERROR "JAVA_HOME must be defined") else () @@ -10,11 +17,37 @@ endif () project(${PROJECT_NAME} LANGUAGES CXX) -set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON) -set(SOURCES) +add_library(SKIA INTERFACE) +target_include_directories(SKIA INTERFACE + "${CMAKE_SOURCE_DIR}/third_party/skia/include" + "${CMAKE_SOURCE_DIR}/third_party/skia/modules/svg/include" + "${CMAKE_SOURCE_DIR}/third_party/skia/src" + "${CMAKE_SOURCE_DIR}/third_party/skia" + "${CMAKE_SOURCE_DIR}/third_party/skia/third_party/externals/angle2/include" + "${CMAKE_SOURCE_DIR}/third_party/skia/third_party/externals/freetype/include" + "${CMAKE_SOURCE_DIR}/third_party/skia/third_party/externals/harfbuzz/src" + "${CMAKE_SOURCE_DIR}/third_party/skia/third_party/externals/icu/source/common" + "${CMAKE_SOURCE_DIR}/third_party/skia/third_party/externals/libpng" + "${CMAKE_SOURCE_DIR}/third_party/skia/third_party/externals/libwebp/src" + "${CMAKE_SOURCE_DIR}/third_party/skia/third_party/externals/swiftshader/include" + "${CMAKE_SOURCE_DIR}/third_party/skia/third_party/externals/zlib" + "${CMAKE_SOURCE_DIR}/third_party/skia/third_party/icu" +) +target_link_directories(SKIA INTERFACE "${CMAKE_SOURCE_DIR}/third_party/skia/out/Release-windows-x64") +target_link_libraries(SKIA INTERFACE + bentleyottmann.lib libwebp.lib skottie.lib skshaper.lib svg.lib + d3d12allocator.lib icu.lib libwebp_sse41.lib skparagraph.lib skunicode_core.lib wuffs.lib + expat.lib libjpeg.lib skcms.lib skresources.lib skunicode_icu.lib zlib.lib + harfbuzz.lib libpng.lib skia.lib sksg.lib spirv_cross.lib +) + +set(SOURCES + +) if (CMAKE_SYSTEM_NAME STREQUAL "Linux") if (NOT DEFINED JAVA_HOME) @@ -26,6 +59,7 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Linux") ) elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows") list(APPEND SOURCES + src/cpp/windows/D3DInterop.cpp src/cpp/windows/WGLContext.cpp ) endif () @@ -44,6 +78,9 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Linux") target_include_directories(${PROJECT_NAME} PUBLIC "${JAVA_HOME}/include/linux") elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows") target_compile_definitions(${PROJECT_NAME} PRIVATE -D_WINDOWS) - target_link_libraries(${PROJECT_NAME} PUBLIC opengl32 dxgi d3d11 d3dcompiler) + target_compile_definitions(${PROJECT_NAME} PRIVATE SK_DIRECT3D NOMINMAX WIN32_LEAN_AND_MEAN) + target_compile_options(${PROJECT_NAME} PRIVATE /MT) + target_link_libraries(${PROJECT_NAME} PUBLIC opengl32 dxgi d3d12 d3dcompiler) target_include_directories(${PROJECT_NAME} PUBLIC "${JAVA_HOME}/include/win32") + target_link_libraries(${PROJECT_NAME} PUBLIC SKIA) endif () diff --git a/native/src/cpp/windows/D3DInterop.cpp b/native/src/cpp/windows/D3DInterop.cpp new file mode 100644 index 0000000..382e6ad --- /dev/null +++ b/native/src/cpp/windows/D3DInterop.cpp @@ -0,0 +1,139 @@ +// +// Created by silenium-dev on 2025-10-04. +// + +#include +#include + +#define SK_DIRECT3D // for some reason, the d3d headers undefine this macro +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +constexpr int BuffersCount = 2; + +class DirectXDevice { +public: + HWND hWnd;// Handle of native view. + GrD3DBackendContext backendContext; + gr_cp device; + gr_cp swapChain; + gr_cp queue; + gr_cp buffers[BuffersCount]; + gr_cp fence; + gr_cp dcDevice; + gr_cp dcTarget; + gr_cp dcVisual; + uint64_t fenceValues[BuffersCount]; + HANDLE fenceEvent = nullptr; + unsigned int bufferIndex; +}; + +extern "C" { +JNIEXPORT jlong JNICALL Java_dev_silenium_compose_gl_interop_D3DInteropKt_createD3DTextureN(JNIEnv *env, jobject thiz, const jlong _device, const jint width, const jint height) { + const auto device = reinterpret_cast(_device); + + D3D12_RESOURCE_DESC desc{}; + desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + desc.Width = width; + desc.Height = height; + desc.DepthOrArraySize = 1; + desc.MipLevels = 1; + desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + desc.SampleDesc.Count = 1; + desc.SampleDesc.Quality = 0; + desc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; + desc.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET; + + D3D12_HEAP_PROPERTIES heap{}; + heap.Type = D3D12_HEAP_TYPE_DEFAULT; + heap.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN; + heap.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN; + heap.CreationNodeMask = 1; + heap.VisibleNodeMask = 1; + + D3D12_CLEAR_VALUE clearValue{}; + clearValue.Format = desc.Format; + clearValue.Color[0] = 1; + clearValue.Color[1] = 0; + clearValue.Color[2] = 0; + clearValue.Color[3] = 1; + + ID3D12Resource *resource; + const auto res = device->device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_SHARED, &desc, D3D12_RESOURCE_STATE_COMMON, &clearValue, IID_PPV_ARGS(&resource)); + if (FAILED(res)) { + const _com_error err{res}; + std::cerr << "Failed to create resource: " << err.ErrorMessage() << std::endl; + return 0; + } + + return reinterpret_cast(resource); +} + +JNIEXPORT jlong JNICALL Java_dev_silenium_compose_gl_interop_D3DInteropKt_exportSharedHandleN(JNIEnv *env, jobject thiz, const jlong _device, const jlong _resource) { + const auto resource = reinterpret_cast(_resource); + const auto device = reinterpret_cast(_device); + + HANDLE sharedHandle; + const auto res = device->device->CreateSharedHandle(resource, nullptr, GENERIC_ALL, nullptr, &sharedHandle); + if (FAILED(res)) { + const _com_error err{res}; + std::cerr << "Failed to create shared handle: " << err.ErrorMessage() << std::endl; + return 0; + } + return reinterpret_cast(sharedHandle); +} + +JNIEXPORT jlong JNICALL Java_dev_silenium_compose_gl_interop_D3DInteropKt_makeD3DBackendTextureN(JNIEnv *env, jobject thiz, const jlong _resource) { + const auto resource = reinterpret_cast(_resource); + const auto desc = resource->GetDesc(); + + GrD3DTextureResourceInfo importInfo{}; + importInfo.fResource.retain(resource); + importInfo.fLevelCount = 1; + importInfo.fFormat = DXGI_FORMAT_R8G8B8A8_UNORM; + + const auto texture = new GrBackendTexture(static_cast(desc.Width), static_cast(desc.Height), importInfo); + return reinterpret_cast(texture); +} + +JNIEXPORT void JNICALL Java_dev_silenium_compose_gl_interop_D3DInteropKt_destroyD3DTextureN(JNIEnv *env, jobject thiz, const jlong _resource) { + const auto resource = reinterpret_cast(_resource); + resource->Release(); +} + +JNIEXPORT void JNICALL Java_dev_silenium_compose_gl_interop_D3DInteropKt_closeSharedHandleN(JNIEnv *env, jobject thiz, const jlong _handle) { + const auto handle = reinterpret_cast(_handle); + CloseHandle(handle); +} +JNIEXPORT jstring JNICALL Java_dev_silenium_compose_gl_interop_D3DInteropKt_getDirectContextApiN(JNIEnv *env, jobject thiz, jobject _directContext) { + const auto clazz = env->GetObjectClass(_directContext); + const auto ptrField = env->GetFieldID(clazz, "_ptr", "J"); + const auto ptr = env->GetLongField(_directContext, ptrField); + const auto directContext = reinterpret_cast(ptr); + switch (directContext->backend()) { + case GrBackendApi::kOpenGL: + return env->NewStringUTF("kOpenGL"); + case GrBackendApi::kVulkan: + return env->NewStringUTF("kVulkan"); + case GrBackendApi::kMetal: + return env->NewStringUTF("kMetal"); + case GrBackendApi::kDirect3D: + return env->NewStringUTF("kDirect3D"); + case GrBackendApi::kMock: + return env->NewStringUTF("kMock"); + case GrBackendApi::kUnsupported: + return env->NewStringUTF("kUnsupported"); + default: + return nullptr; + } +} +} diff --git a/src/main/java/dev/silenium/compose/gl/CompositionLocals.kt b/src/main/java/dev/silenium/compose/gl/CompositionLocals.kt index 5d736e4..4531e47 100644 --- a/src/main/java/dev/silenium/compose/gl/CompositionLocals.kt +++ b/src/main/java/dev/silenium/compose/gl/CompositionLocals.kt @@ -3,8 +3,14 @@ package dev.silenium.compose.gl import androidx.compose.runtime.CompositionLocal import org.jetbrains.skia.DirectContext import org.jetbrains.skia.Surface +import org.jetbrains.skia.impl.NativePointer +import org.jetbrains.skiko.GraphicsApi import org.jetbrains.skiko.SkiaLayer +import org.jetbrains.skiko.graphicapi.DirectXOffscreenContext import java.awt.Window +import kotlin.reflect.full.memberProperties +import kotlin.reflect.jvm.isAccessible +import kotlin.reflect.typeOf @Suppress("UNCHECKED_CAST") val LocalWindow: CompositionLocal by lazy { @@ -13,17 +19,47 @@ val LocalWindow: CompositionLocal by lazy { method.invoke(null) as CompositionLocal } +fun Window.directXRedrawer(): Any? { + return contextHandler().let { + val getter = it::class.memberProperties.first { it.name == "directXRedrawer" } + getter.isAccessible = true + getter.call(it) + } +} + +fun Window.directX12Device(): NativePointer? { + return directXRedrawer()?.let { + val getter = it::class.memberProperties.first { it.name == "device" && it.returnType == typeOf() } + getter.isAccessible = true + getter.call(it) as Long? + } +} + fun Window.directContext(): DirectContext? { - fun Any.getFieldValue(fieldName: String): Any? { - val field = this::class.java.getDeclaredField(fieldName) - field.isAccessible = true - return field.get(this) + val surface = contextHandler().let { + val getter = it::class.java.superclass.superclass.getDeclaredMethod("getSurface") + getter.isAccessible = true + getter.invoke(it) as? Surface + } + return surface?.recordingContext +} + +fun Window.graphicsApi(): GraphicsApi { + return mediator().let { + val getter = it::class.memberProperties.first { it.name == "renderApi" && it.returnType == typeOf() } + getter.isAccessible = true + getter.call(it) as GraphicsApi } +} +fun Window.mediator(): Any { val composePanel = this.getFieldValue("composePanel")!! val composeContainer = composePanel.getFieldValue("_composeContainer")!! - val mediator = composeContainer.getFieldValue("mediator")!! - val contentComponent = mediator.let { + return composeContainer.getFieldValue("mediator")!! +} + +fun Window.contextHandler(): Any { + val contentComponent = mediator().let { val getter = it::class.java.getMethod("getContentComponent") getter.invoke(it) as SkiaLayer } @@ -31,11 +67,11 @@ fun Window.directContext(): DirectContext? { val getter = it::class.java.getMethod("getRedrawer${'$'}skiko") getter.invoke(it) } - val contextHandler = redrawer.getFieldValue("contextHandler")!! - val surface = contextHandler.let { - val getter = it::class.java.superclass.superclass.getDeclaredMethod("getSurface") - getter.isAccessible = true - getter.invoke(it) as? Surface - } - return surface?.recordingContext + return redrawer?.getFieldValue("contextHandler")!! +} + +private fun Any.getFieldValue(fieldName: String): Any? { + val field = this::class.java.getDeclaredField(fieldName) + field.isAccessible = true + return field.get(this) } diff --git a/src/main/java/dev/silenium/compose/gl/context/WGLContext.kt b/src/main/java/dev/silenium/compose/gl/context/WGLContext.kt index aeb7c81..162eae9 100644 --- a/src/main/java/dev/silenium/compose/gl/context/WGLContext.kt +++ b/src/main/java/dev/silenium/compose/gl/context/WGLContext.kt @@ -21,12 +21,12 @@ data class WGLContext( override lateinit var glCapabilities: GLCapabilities override fun unbindCurrent() { - WGL.wglMakeCurrent(0L, 0L) + WGL.wglMakeCurrent(null, 0L, 0L) } override fun makeCurrent() { if (provider.fromCurrent() != this) { - check(WGL.wglMakeCurrent(deviceContext, renderingContext)) { + check(WGL.wglMakeCurrent(null, deviceContext, renderingContext)) { "Failed to make context current" } } @@ -44,14 +44,14 @@ data class WGLContext( override fun destroy() { contextCapabilities.compute(this) { key, value -> if (value == null) { - WGL.wglMakeCurrent(0L, 0L) - WGL.wglDeleteContext(key.renderingContext) + WGL.wglMakeCurrent(null, 0L, 0L) + WGL.wglDeleteContext(null, key.renderingContext) return@compute null } val refCount = value.refCount - 1 if (refCount == 0) { - WGL.wglMakeCurrent(0L, 0L) - WGL.wglDeleteContext(key.renderingContext) + WGL.wglMakeCurrent(null, 0L, 0L) + WGL.wglDeleteContext(null, key.renderingContext) return@compute null } value.copy(refCount = refCount) @@ -75,15 +75,15 @@ data class WGLContext( override fun restorePrevious(block: () -> R): R { val displayContext = WGL.wglGetCurrentDC() - val renderingContext = WGL.wglGetCurrentContext() + val renderingContext = WGL.wglGetCurrentContext(null) return block().also { - WGL.wglMakeCurrent(displayContext, renderingContext) + WGL.wglMakeCurrent(null, displayContext, renderingContext) } } override fun fromCurrent(): WGLContext? { val deviceContext = WGL.wglGetCurrentDC() - val renderingContext = WGL.wglGetCurrentContext() + val renderingContext = WGL.wglGetCurrentContext(null) return if (deviceContext != 0L && renderingContext != 0L) { WGLContext(deviceContext, renderingContext) } else { @@ -93,14 +93,14 @@ data class WGLContext( override fun isCurrent(): Boolean { val deviceContext = WGL.wglGetCurrentDC() - val renderingContext = WGL.wglGetCurrentContext() + val renderingContext = WGL.wglGetCurrentContext(null) return deviceContext != 0L && renderingContext != 0L } override fun createOffscreen(parent: WGLContext): WGLContext { val deviceContext = parent.deviceContext - val renderingContext = WGL.wglCreateContext(deviceContext) - check(WGL.wglShareLists(parent.renderingContext, renderingContext)) { + val renderingContext = WGL.wglCreateContext(null, deviceContext) + check(WGL.wglShareLists(null, parent.renderingContext, renderingContext)) { "Failed to share context lists" } return WGLContext(deviceContext, renderingContext) diff --git a/src/main/java/dev/silenium/compose/gl/interop/Compat.java b/src/main/java/dev/silenium/compose/gl/interop/Compat.java new file mode 100644 index 0000000..45a5e79 --- /dev/null +++ b/src/main/java/dev/silenium/compose/gl/interop/Compat.java @@ -0,0 +1,9 @@ +package dev.silenium.compose.gl.interop; + +import org.jetbrains.skia.BackendTexture; + +class Compat { + static BackendTexture create(long nativePtr) { + return new BackendTexture(nativePtr); + } +} diff --git a/src/main/java/dev/silenium/compose/gl/interop/D3DInterop.kt b/src/main/java/dev/silenium/compose/gl/interop/D3DInterop.kt new file mode 100644 index 0000000..6dba7c8 --- /dev/null +++ b/src/main/java/dev/silenium/compose/gl/interop/D3DInterop.kt @@ -0,0 +1,42 @@ +package dev.silenium.compose.gl.interop + +import dev.silenium.compose.gl.directX12Device +import dev.silenium.libs.jni.NativeLoader +import org.jetbrains.skia.BackendTexture +import org.jetbrains.skia.DirectContext +import org.jetbrains.skia.impl.NativePointer +import java.awt.Window + +object D3DInterop { + fun createTexture(window: Window, width: Int, height: Int): NativePointer { + val device = window.directX12Device() ?: throw IllegalStateException("No D3D12 device found") + return createD3DTextureN(device, width, height) + } + fun destroyTexture(texture: NativePointer) { + destroyD3DTextureN(texture) + } + fun exportSharedHandle(window: Window, texture: NativePointer): NativePointer { + val device = window.directX12Device() ?: throw IllegalStateException("No D3D12 device found") + return exportSharedHandleN(device, texture) + } + fun closeSharedHandle(handle: NativePointer) { + closeSharedHandleN(handle) + } + fun makeBackendTexture(texture: NativePointer): BackendTexture { + return Compat.create(makeD3DBackendTextureN(texture)) + } + fun getDirectContextApi(context: DirectContext) { + getDirectContextApiN(context) + } + + init { + NativeLoader.loadLibraryFromClasspath("compose-gl").getOrThrow() + } +} + +private external fun createD3DTextureN(device: NativePointer, width: Int, height: Int): NativePointer +private external fun exportSharedHandleN(device: NativePointer, texture: NativePointer): NativePointer +private external fun makeD3DBackendTextureN(texture: NativePointer): NativePointer +private external fun destroyD3DTextureN(texture: NativePointer) +private external fun closeSharedHandleN(handle: NativePointer) +private external fun getDirectContextApiN(context: DirectContext) diff --git a/src/main/java/dev/silenium/compose/gl/objects/Texture.kt b/src/main/java/dev/silenium/compose/gl/objects/Texture.kt index 7d940b6..e4ad5f1 100644 --- a/src/main/java/dev/silenium/compose/gl/objects/Texture.kt +++ b/src/main/java/dev/silenium/compose/gl/objects/Texture.kt @@ -30,6 +30,7 @@ data class Texture( override fun destroyInternal() { glDeleteTextures(id) + checkGLError("glDeleteTextures") } companion object { diff --git a/src/main/java/dev/silenium/compose/gl/util/DoubleDestructionProtection.kt b/src/main/java/dev/silenium/compose/gl/util/DoubleDestructionProtection.kt index 465bb5c..311ef3e 100644 --- a/src/main/java/dev/silenium/compose/gl/util/DoubleDestructionProtection.kt +++ b/src/main/java/dev/silenium/compose/gl/util/DoubleDestructionProtection.kt @@ -23,6 +23,20 @@ abstract class DoubleDestructionProtection { } } + fun abandon() { + if (destroyed.compareAndSet(false, true)) { + destructionPoint = Exception("Abandoned") + } else { + logger.trace( + "{} {} was already destroyed at: {}", + javaClass.simpleName, + id, + destroyed.get(), + Exception(), + ) + } + } + protected abstract fun destroyInternal() companion object { diff --git a/src/test/kotlin/direct_import/GLTextureDrawer.kt b/src/test/kotlin/direct_import/GLTextureDrawer.kt new file mode 100644 index 0000000..db96b96 --- /dev/null +++ b/src/test/kotlin/direct_import/GLTextureDrawer.kt @@ -0,0 +1,163 @@ +package direct_import + +import org.lwjgl.opengl.GL +import org.lwjgl.opengl.GL11.GL_BLEND +import org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT +import org.lwjgl.opengl.GL11.GL_FALSE +import org.lwjgl.opengl.GL11.GL_FLOAT +import org.lwjgl.opengl.GL11.GL_ONE_MINUS_SRC_ALPHA +import org.lwjgl.opengl.GL11.GL_SRC_ALPHA +import org.lwjgl.opengl.GL11.GL_TEXTURE_2D +import org.lwjgl.opengl.GL11.GL_TRIANGLES +import org.lwjgl.opengl.GL11.GL_UNSIGNED_INT +import org.lwjgl.opengl.GL11.glBindTexture +import org.lwjgl.opengl.GL11.glBlendFunc +import org.lwjgl.opengl.GL11.glClear +import org.lwjgl.opengl.GL11.glClearColor +import org.lwjgl.opengl.GL11.glDeleteTextures +import org.lwjgl.opengl.GL11.glDisable +import org.lwjgl.opengl.GL11.glDrawElements +import org.lwjgl.opengl.GL11.glEnable +import org.lwjgl.opengl.GL13.GL_TEXTURE0 +import org.lwjgl.opengl.GL13.glActiveTexture +import org.lwjgl.opengl.GL15.GL_ARRAY_BUFFER +import org.lwjgl.opengl.GL15.GL_ELEMENT_ARRAY_BUFFER +import org.lwjgl.opengl.GL15.GL_STATIC_DRAW +import org.lwjgl.opengl.GL15.glBindBuffer +import org.lwjgl.opengl.GL15.glBufferData +import org.lwjgl.opengl.GL15.glDeleteBuffers +import org.lwjgl.opengl.GL15.glGenBuffers +import org.lwjgl.opengl.GL20.GL_COMPILE_STATUS +import org.lwjgl.opengl.GL20.GL_FRAGMENT_SHADER +import org.lwjgl.opengl.GL20.GL_LINK_STATUS +import org.lwjgl.opengl.GL20.GL_VERTEX_SHADER +import org.lwjgl.opengl.GL20.glAttachShader +import org.lwjgl.opengl.GL20.glCompileShader +import org.lwjgl.opengl.GL20.glCreateProgram +import org.lwjgl.opengl.GL20.glCreateShader +import org.lwjgl.opengl.GL20.glDeleteProgram +import org.lwjgl.opengl.GL20.glEnableVertexAttribArray +import org.lwjgl.opengl.GL20.glGetProgramInfoLog +import org.lwjgl.opengl.GL20.glGetProgrami +import org.lwjgl.opengl.GL20.glGetShaderInfoLog +import org.lwjgl.opengl.GL20.glGetShaderi +import org.lwjgl.opengl.GL20.glGetUniformLocation +import org.lwjgl.opengl.GL20.glLinkProgram +import org.lwjgl.opengl.GL20.glShaderSource +import org.lwjgl.opengl.GL20.glUniform1i +import org.lwjgl.opengl.GL20.glUseProgram +import org.lwjgl.opengl.GL20.glVertexAttribPointer +import org.lwjgl.opengl.GL30.glBindVertexArray +import org.lwjgl.opengl.GL30.glDeleteVertexArrays +import org.lwjgl.opengl.GL30.glGenVertexArrays +import java.io.File +import kotlin.random.Random + +class GLTextureDrawer { + private var textureId = 0 + private var initialized = false + private var shaderProgram = 0 + private var vao = 0 + private var vbo = 0 + private var ibo = 0 + + fun initialize() { + if (initialized) return + + val img = "image.png" + val (id, size) = loadTexture(File(img)) + textureId = id + + shaderProgram = glCreateProgram() + val vertexShader = glCreateShader(GL_VERTEX_SHADER) + val fragmentShader = glCreateShader(GL_FRAGMENT_SHADER) + glShaderSource(vertexShader, VERTEX_SHADER_SOURCE) + glCompileShader(vertexShader) + if (glGetShaderi(vertexShader, GL_COMPILE_STATUS) == GL_FALSE) { + println("Vertex shader compilation failed: ${glGetShaderInfoLog(vertexShader)}") + } + glShaderSource(fragmentShader, FRAGMENT_SHADER_SOURCE) + glCompileShader(fragmentShader) + if (glGetShaderi(fragmentShader, GL_COMPILE_STATUS) == GL_FALSE) { + println("Fragment shader compilation failed: ${glGetShaderInfoLog(fragmentShader)}") + } + glAttachShader(shaderProgram, vertexShader) + glAttachShader(shaderProgram, fragmentShader) + glLinkProgram(shaderProgram) + if (glGetProgrami(shaderProgram, GL_LINK_STATUS) == GL_FALSE) { + println("Shader program linking failed: ${glGetProgramInfoLog(shaderProgram)}") + } + glUseProgram(shaderProgram) + val loc = glGetUniformLocation(shaderProgram, "myTextureSampler") + glUniform1i(loc, 0) + glUseProgram(0) + + val vertices = floatArrayOf( + // aPosition | aTexCoords + 0.5f, 0.5f, 0.0f, 1.0f, 1.0f, + 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, + -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, + -0.5f, 0.5f, 0.0f, 0.0f, 1.0f + ) + val indices = intArrayOf( + 0, 1, 3, + 1, 2, 3 + ) + vao = glGenVertexArrays() + glBindVertexArray(vao) + + vbo = glGenBuffers() + glBindBuffer(GL_ARRAY_BUFFER, vbo) + glBufferData(GL_ARRAY_BUFFER, vertices, GL_STATIC_DRAW) + + ibo = glGenBuffers() + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo) + glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices, GL_STATIC_DRAW) + + glVertexAttribPointer(0, 3, GL_FLOAT, false, 5 * Float.SIZE_BYTES, 0) + glEnableVertexAttribArray(0) + glVertexAttribPointer(1, 2, GL_FLOAT, false, 5 * Float.SIZE_BYTES, 3L * Float.SIZE_BYTES) + glEnableVertexAttribArray(1) + glBindVertexArray(0) + glBindBuffer(GL_ARRAY_BUFFER, 0) + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0) + + initialized = true + } + + fun render() { + initialize() + + glEnable(GL_BLEND) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + + glClearColor(0f, .5f, .5f, 1f) + glClear(GL_COLOR_BUFFER_BIT) + + glBindVertexArray(vao) + glUseProgram(shaderProgram) + glActiveTexture(GL_TEXTURE0) + glBindTexture(GL_TEXTURE_2D, textureId) + + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0) + + glBindTexture(GL_TEXTURE_2D, 0) + glBindVertexArray(0) + glUseProgram(0) + glDisable(GL_BLEND) + } + + fun destroy() { + glDeleteProgram(shaderProgram) + glDeleteVertexArrays(vao) + glDeleteBuffers(vbo) + glDeleteBuffers(ibo) + glDeleteTextures(textureId) + initialized = false + shaderProgram = 0 + vao = 0 + vbo = 0 + ibo = 0 + textureId = 0 + } +} diff --git a/src/test/kotlin/direct_import/Main.kt b/src/test/kotlin/direct_import/Main.kt index 09d5316..d4ce437 100644 --- a/src/test/kotlin/direct_import/Main.kt +++ b/src/test/kotlin/direct_import/Main.kt @@ -1,20 +1,45 @@ package direct_import -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.* import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface import androidx.compose.material.Text -import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asComposeCanvas +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.scene.PlatformLayersComposeScene +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.toIntSize import androidx.compose.ui.window.Window import androidx.compose.ui.window.application -import dev.silenium.compose.gl.direct.GLDirectCanvas +import dev.silenium.compose.gl.directContext +import dev.silenium.compose.gl.fbo.FBO +import dev.silenium.compose.gl.fbo.draw +import dev.silenium.compose.gl.graphicsApi +import dev.silenium.compose.gl.interop.D3DInterop +import dev.silenium.compose.gl.objects.Renderbuffer +import dev.silenium.compose.gl.objects.Texture +import dev.silenium.compose.gl.util.checkGLError +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext +import org.jetbrains.skia.* import org.lwjgl.BufferUtils +import org.lwjgl.glfw.GLFW +import org.lwjgl.opengl.EXTMemoryObject +import org.lwjgl.opengl.EXTMemoryObject.GL_OPTIMAL_TILING_EXT +import org.lwjgl.opengl.EXTMemoryObject.GL_TEXTURE_TILING_EXT +import org.lwjgl.opengl.EXTMemoryObjectWin32 import org.lwjgl.opengl.GL import org.lwjgl.opengl.GL33.* +import org.lwjgl.system.MemoryUtil import java.io.File import javax.imageio.ImageIO -import kotlin.random.Random //language=glsl const val VERTEX_SHADER_SOURCE = """ @@ -57,15 +82,32 @@ void main(){ } """ -@OptIn(ExperimentalUnsignedTypes::class) +@OptIn(ExperimentalUnsignedTypes::class, InternalComposeUiApi::class) fun main() = application { - System.setProperty("skiko.renderApi", "OPENGL") - var textureId = 0 - var initialized = false - var shaderProgram = 0 - var vao = 0 - var vbo = 0 - var ibo = 0 + System.setProperty("skiko.renderApi", "DIRECT3D") + + var directContext: DirectContext? by mutableStateOf(null) + + var d3dTexture: Long? = null + var sharedHandle: Long? = null + var backendTexture: BackendTexture? = null + var glMemory: Int? = null + var image: Image? = null + var initialized by mutableStateOf(false) + + var glfwWindow = 0L + var fbo: FBO? = null + var glSurface: Surface? = null + var glRenderTarget: BackendRenderTarget? = null + var glDirectContext: DirectContext? = null + + val renderer = GLTextureDrawer() + + val glScene = PlatformLayersComposeScene() + glScene.setContent { + Text("Hello from Skia on OpenGL", style = MaterialTheme.typography.h2) + } + Window( ::exitApplication, title = "Direct Render Test", @@ -73,107 +115,163 @@ fun main() = application { Box(Modifier.fillMaxSize()) { DisposableEffect(Unit) { onDispose { - glDeleteProgram(shaderProgram) - glDeleteVertexArrays(vao) - glDeleteBuffers(vbo) - glDeleteBuffers(ibo) - glDeleteTextures(textureId) - GL.destroy() - initialized = false - shaderProgram = 0 - vao = 0 - vbo = 0 - ibo = 0 - textureId = 0 + GLFW.glfwMakeContextCurrent(glfwWindow) + renderer.destroy() + glSurface?.close() + glRenderTarget?.close() + glDirectContext?.close() + fbo?.destroy() + image?.close() + glMemory?.let(EXTMemoryObject::glDeleteMemoryObjectsEXT) + d3dTexture?.let(D3DInterop::destroyTexture) + sharedHandle?.let(D3DInterop::closeSharedHandle) + GLFW.glfwMakeContextCurrent(MemoryUtil.NULL) + glfwWindow.let(GLFW::glfwDestroyWindow) println("Disposed") } } - GLDirectCanvas(Modifier.matchParentSize()) { - if (!initialized) { - val img = "image.png" - val (id, size) = loadTexture(File(img)) - textureId = id - - shaderProgram = glCreateProgram() - val vertexShader = glCreateShader(GL_VERTEX_SHADER) - val fragmentShader = glCreateShader(GL_FRAGMENT_SHADER) - glShaderSource(vertexShader, VERTEX_SHADER_SOURCE) - glCompileShader(vertexShader) - if (glGetShaderi(vertexShader, GL_COMPILE_STATUS) == GL_FALSE) { - println("Vertex shader compilation failed: ${glGetShaderInfoLog(vertexShader)}") + LaunchedEffect(window) { + withContext(Dispatchers.IO) { + while (directContext == null && isActive) { + directContext = window.directContext() } - glShaderSource(fragmentShader, FRAGMENT_SHADER_SOURCE) - glCompileShader(fragmentShader) - if (glGetShaderi(fragmentShader, GL_COMPILE_STATUS) == GL_FALSE) { - println("Fragment shader compilation failed: ${glGetShaderInfoLog(fragmentShader)}") + } + } + Canvas(Modifier.matchParentSize()) { + if (directContext == null) return@Canvas + if (!initialized) { + GLFW.glfwInitHint(GLFW.GLFW_COCOA_MENUBAR, GLFW.GLFW_FALSE) + if (!GLFW.glfwInit()) { + throw RuntimeException("Failed to initialize GLFW") } - glAttachShader(shaderProgram, vertexShader) - glAttachShader(shaderProgram, fragmentShader) - glLinkProgram(shaderProgram) - if (glGetProgrami(shaderProgram, GL_LINK_STATUS) == GL_FALSE) { - println("Shader program linking failed: ${glGetProgramInfoLog(shaderProgram)}") + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3) + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 2) + GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE) + + glfwWindow = GLFW.glfwCreateWindow(128, 128, "", MemoryUtil.NULL, MemoryUtil.NULL) + println("glfwWindow: $glfwWindow") + if (glfwWindow == MemoryUtil.NULL) { + throw RuntimeException("Failed to create GLFW window") } - glUseProgram(shaderProgram) - val loc = glGetUniformLocation(shaderProgram, "myTextureSampler") - glUniform1i(loc, 0) - glUseProgram(0) - - val vertices = floatArrayOf( - // aPosition | aTexCoords - 0.5f, 0.5f, 0.0f, 1.0f, 1.0f, - 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, - -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, - -0.5f, 0.5f, 0.0f, 0.0f, 1.0f - ) - val indices = intArrayOf( - 0, 1, 3, - 1, 2, 3 - ) - vao = glGenVertexArrays() - glBindVertexArray(vao) - - vbo = glGenBuffers() - glBindBuffer(GL_ARRAY_BUFFER, vbo) - glBufferData(GL_ARRAY_BUFFER, vertices, GL_STATIC_DRAW) - - ibo = glGenBuffers() - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo) - glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices, GL_STATIC_DRAW) - - glVertexAttribPointer(0, 3, GL_FLOAT, false, 5 * Float.SIZE_BYTES, 0) - glEnableVertexAttribArray(0) - glVertexAttribPointer(1, 2, GL_FLOAT, false, 5 * Float.SIZE_BYTES, 3L * Float.SIZE_BYTES) - glEnableVertexAttribArray(1) - glBindVertexArray(0) - glBindBuffer(GL_ARRAY_BUFFER, 0) - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0) - + GLFW.glfwMakeContextCurrent(glfwWindow) + GL.createCapabilities() + GLFW.glfwMakeContextCurrent(MemoryUtil.NULL) initialized = true } + GLFW.glfwMakeContextCurrent(glfwWindow) + if (fbo?.size != size.toIntSize()) { + glMemory?.let(EXTMemoryObject::glDeleteMemoryObjectsEXT) + glMemory = null + sharedHandle?.let(D3DInterop::closeSharedHandle) + sharedHandle = null + d3dTexture?.let(D3DInterop::destroyTexture) + d3dTexture = null + backendTexture?.close() + backendTexture = null + glSurface?.close() + glSurface = null + glRenderTarget?.close() + glRenderTarget = null + glDirectContext?.close() + glDirectContext = null + image?.close() + image = null + fbo?.destroy() + fbo = null + + d3dTexture = D3DInterop.createTexture(window, size.width.toInt(), size.height.toInt()) +// println("d3dTexture: $d3dTexture") + + sharedHandle = D3DInterop.exportSharedHandle(window, d3dTexture!!) +// println("sharedHandle: $sharedHandle") + + val colorAttachment = Texture(glGenTextures(), size.toIntSize(), GL_TEXTURE_2D, GL_RGBA8) + colorAttachment.bind() + glTexParameteri(colorAttachment.target, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + checkGLError("glTexParameteri") + glTexParameteri(colorAttachment.target, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + checkGLError("glTexParameteri") + glTexParameteri(colorAttachment.target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) + checkGLError("glTexParameteri") + glTexParameteri(colorAttachment.target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) + checkGLError("glTexParameteri") + glTexParameteri(colorAttachment.target, GL_TEXTURE_TILING_EXT, GL_OPTIMAL_TILING_EXT) + checkGLError("glTexParameteri") + colorAttachment.unbind() + + glMemory = EXTMemoryObject.glCreateMemoryObjectsEXT() + checkGLError("glCreateMemoryObjectsEXT") +// println("glMemory: $glMemory") + EXTMemoryObjectWin32.glImportMemoryWin32HandleEXT( + glMemory!!, size.width.toInt() * size.height.toInt() * 4 * 2L, + EXTMemoryObjectWin32.GL_HANDLE_TYPE_D3D12_RESOURCE_EXT, sharedHandle!!, + ) + checkGLError("glImportMemoryWin32HandleEXT") - println("redrawing (${fbo.size})") - glEnable(GL_BLEND) - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) - - glClearColor(Random.nextFloat(), Random.nextFloat(), Random.nextFloat(), 1f) - glClear(GL_COLOR_BUFFER_BIT) + EXTMemoryObject.glTextureStorageMem2DEXT( + colorAttachment.id, + 1, GL_RGBA8, + size.width.toInt(), size.height.toInt(), + glMemory!!, 0 + ) + checkGLError("glTextureStorageMem2DEXT") - glBindVertexArray(vao) - glUseProgram(shaderProgram) - glActiveTexture(GL_TEXTURE0) - glBindTexture(GL_TEXTURE_2D, textureId) + val depthStencilAttachment = Renderbuffer.create(size.toIntSize(), GL_DEPTH24_STENCIL8) + fbo = FBO.create(colorAttachment, depthStencilAttachment) - glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0) + backendTexture = D3DInterop.makeBackendTexture(d3dTexture!!) + image = Image.adoptTextureFrom( + directContext!!, backendTexture!!, + SurfaceOrigin.TOP_LEFT, ColorType.RGBA_8888, + ) - glBindTexture(GL_TEXTURE_2D, 0) - glBindVertexArray(0) - glUseProgram(0) - glDisable(GL_BLEND) + glDirectContext = DirectContext.makeGL() + glRenderTarget = BackendRenderTarget.makeGL( + size.width.toInt(), size.height.toInt(), 1, 8, fbo!!.id, GL_RGBA8, + ) + glSurface = Surface.makeFromBackendRenderTarget( + glDirectContext!!, + glRenderTarget!!, + SurfaceOrigin.TOP_LEFT, + SurfaceColorFormat.RGBA_8888, + ColorSpace.sRGB, + ) + } + fbo?.draw { + println("redrawing (${size})") + renderer.render() + } glFinish() + glDirectContext!!.resetGLAll() + with(glSurface!!.canvas) { + drawRect(Rect(100f, 200f, 200f, 300f), Paint().apply { color = Color.RED }) + save() + translate(100f, 300f) + glScene.render(this.asComposeCanvas(), 0L) + restore() + } + glSurface!!.flushAndSubmit(true) +// fbo?.snapshot(Path("rendered.png")) + GLFW.glfwMakeContextCurrent(MemoryUtil.NULL) + + drawContext.canvas.nativeCanvas.drawImage(image!!, 0f, 0f) } - Button(onClick = { println("button pressed") }) { - Text("Button") + Surface( + shape = MaterialTheme.shapes.medium, + color = MaterialTheme.colors.surface, + modifier = Modifier.padding(8.dp).width(200.dp), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.Start, + modifier = Modifier.padding(8.dp), + ) { + Text("Skia Graphics API: ${window.graphicsApi()}") + Button(onClick = { print("button pressed") }) { + Text("Button") + } + } } } } @@ -192,7 +290,7 @@ fun loadTexture(file: File): Pair> { val buffer = BufferUtils.createByteBuffer(width * height * 4) // OpenGL expects bottom-to-top, so flip vertically - for (y in height - 1 downTo 0) { + for (y in 0.. Date: Sun, 5 Oct 2025 16:04:13 +0200 Subject: [PATCH 07/16] build: automate skia-pack download from github --- build.gradle.kts | 1 + native/CMakeLists.txt | 26 +--------- native/cmake/skia.cmake | 111 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 24 deletions(-) create mode 100644 native/cmake/skia.cmake diff --git a/build.gradle.kts b/build.gradle.kts index 519151c..2924efb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -42,6 +42,7 @@ dependencies { } implementation(libs.bundles.kotlinx.coroutines) + implementation("net.java.dev.jna:jna") // api(libs.bundles.skiko) { // version { // strictly(libs.skiko.awt.runtime.linux.x64.get().version!!) diff --git a/native/CMakeLists.txt b/native/CMakeLists.txt index f6327dd..df50bb6 100644 --- a/native/CMakeLists.txt +++ b/native/CMakeLists.txt @@ -21,29 +21,7 @@ set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON) -add_library(SKIA INTERFACE) -target_include_directories(SKIA INTERFACE - "${CMAKE_SOURCE_DIR}/third_party/skia/include" - "${CMAKE_SOURCE_DIR}/third_party/skia/modules/svg/include" - "${CMAKE_SOURCE_DIR}/third_party/skia/src" - "${CMAKE_SOURCE_DIR}/third_party/skia" - "${CMAKE_SOURCE_DIR}/third_party/skia/third_party/externals/angle2/include" - "${CMAKE_SOURCE_DIR}/third_party/skia/third_party/externals/freetype/include" - "${CMAKE_SOURCE_DIR}/third_party/skia/third_party/externals/harfbuzz/src" - "${CMAKE_SOURCE_DIR}/third_party/skia/third_party/externals/icu/source/common" - "${CMAKE_SOURCE_DIR}/third_party/skia/third_party/externals/libpng" - "${CMAKE_SOURCE_DIR}/third_party/skia/third_party/externals/libwebp/src" - "${CMAKE_SOURCE_DIR}/third_party/skia/third_party/externals/swiftshader/include" - "${CMAKE_SOURCE_DIR}/third_party/skia/third_party/externals/zlib" - "${CMAKE_SOURCE_DIR}/third_party/skia/third_party/icu" -) -target_link_directories(SKIA INTERFACE "${CMAKE_SOURCE_DIR}/third_party/skia/out/Release-windows-x64") -target_link_libraries(SKIA INTERFACE - bentleyottmann.lib libwebp.lib skottie.lib skshaper.lib svg.lib - d3d12allocator.lib icu.lib libwebp_sse41.lib skparagraph.lib skunicode_core.lib wuffs.lib - expat.lib libjpeg.lib skcms.lib skresources.lib skunicode_icu.lib zlib.lib - harfbuzz.lib libpng.lib skia.lib sksg.lib spirv_cross.lib -) +include(cmake/skia.cmake) set(SOURCES @@ -82,5 +60,5 @@ elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows") target_compile_options(${PROJECT_NAME} PRIVATE /MT) target_link_libraries(${PROJECT_NAME} PUBLIC opengl32 dxgi d3d12 d3dcompiler) target_include_directories(${PROJECT_NAME} PUBLIC "${JAVA_HOME}/include/win32") - target_link_libraries(${PROJECT_NAME} PUBLIC SKIA) + target_link_libraries(${PROJECT_NAME} PUBLIC Skia) endif () diff --git a/native/cmake/skia.cmake b/native/cmake/skia.cmake new file mode 100644 index 0000000..2ea7ba6 --- /dev/null +++ b/native/cmake/skia.cmake @@ -0,0 +1,111 @@ +set(SKIA_VERSION "m132-a00c390e98-1") +set(SKIA_OS "linux") +set(SKIA_ARCH "x64") +set(SKIA_VARIANT "Debug") + +if (CMAKE_BUILD_TYPE STREQUAL "Debug") + set(SKIA_VARIANT "Debug") +endif () + +if (CMAKE_BUILD_TYPE STREQUAL "Release") + set(SKIA_VARIANT "Release") +endif () + +if (CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") + set(SKIA_VARIANT "Debug") +endif () + +if (CMAKE_BUILD_TYPE STREQUAL "MinSizeRel") + set(SKIA_VARIANT "Release") +endif () + +if (CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(SKIA_OS "windows") +elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(SKIA_OS "linux") +elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set(SKIA_OS "macos") +endif () + +string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" PROCESSOR) + +if (PROCESSOR MATCHES "arm" OR PROCESSOR MATCHES "aarch64") + set(SKIA_ARCH "arm64") +elseif (PROCESSOR MATCHES "x86_64" OR PROCESSOR MATCHES "amd64") + set(SKIA_ARCH "x64") +endif () + +file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/download") + +file(DOWNLOAD "https://api.github.com/repos/JetBrains/skia-pack/releases/tags/${SKIA_VERSION}" "${CMAKE_BINARY_DIR}/download/skia_assets.json" STATUS GH_RELEASE_STATUS SHOW_PROGRESS) +list(GET GH_RELEASE_STATUS 0 GH_RELEASE_STATUS_CODE) +if (GH_RELEASE_STATUS_CODE) + message(FATAL_ERROR "Failed to download GitHub release: ${GH_RELEASE_STATUS_CODE}") + return() +endif () + +file(READ "${CMAKE_BINARY_DIR}/download/skia_assets.json" GH_RELEASE_JSON) + +string(JSON ASSETS_COUNT LENGTH ${GH_RELEASE_JSON} "assets") +math(EXPR ASSETS_COUNT "${ASSETS_COUNT} - 1") + +foreach (assetIdx RANGE 0 ${ASSETS_COUNT} 1) + string(JSON assetName GET ${GH_RELEASE_JSON} "assets" "${assetIdx}" "name") + if (assetName MATCHES "Skia-${SKIA_VERSION}-${SKIA_OS}-${SKIA_VARIANT}-${SKIA_ARCH}.zip") + message(STATUS "Found asset ${assetIdx} of ${ASSETS_COUNT}: ${assetName}") + set(ASSET_IDX ${assetIdx}) + set(ASSET_NAME ${assetName}) + string(JSON ASSET_URL GET ${GH_RELEASE_JSON} "assets" "${assetIdx}" "browser_download_url") + string(JSON ASSET_HASH GET ${GH_RELEASE_JSON} "assets" "${assetIdx}" "digest") + break() + endif () +endforeach () + +message(STATUS "Downloading Skia from ${ASSET_URL} with hash ${ASSET_HASH}") + +set(SKIA_URL "${ASSET_URL}") +string(REPLACE ":" "=" ASSET_HASH "${ASSET_HASH}") + +if (NOT ASSET_HASH) + message(WARNING "Failed to find Skia hash, just checking for the files existence to determine if we need to download Skia again.") + if (NOT EXISTS "${CMAKE_BINARY_DIR}/download/${ASSET_NAME}") + file(DOWNLOAD ${SKIA_URL} "${CMAKE_BINARY_DIR}/download/${ASSET_NAME}" STATUS SKIA_DOWNLOAD_STATUS SHOW_PROGRESS) + endif () +else () + file(DOWNLOAD ${SKIA_URL} "${CMAKE_BINARY_DIR}/download/${ASSET_NAME}" STATUS SKIA_DOWNLOAD_STATUS EXPECTED_HASH "${ASSET_HASH}" SHOW_PROGRESS) +endif () + +list(GET SKIA_DOWNLOAD_STATUS 0 SKIA_DOWNLOAD_STATUS_CODE) +if (SKIA_DOWNLOAD_STATUS_CODE) + message(FATAL_ERROR "Failed to download Skia: ${SKIA_DOWNLOAD_STATUS_CODE}") + return() +endif () + +file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/third_party/skia") +message(STATUS "Unpacking Skia to ${CMAKE_BINARY_DIR}/third_party/skia") +file(REMOVE_RECURSE "${CMAKE_BINARY_DIR}/third_party/skia") +file(ARCHIVE_EXTRACT INPUT "${CMAKE_BINARY_DIR}/download/${ASSET_NAME}" DESTINATION "${CMAKE_BINARY_DIR}/third_party/skia") + +add_library(Skia INTERFACE) +target_include_directories(Skia INTERFACE + "${CMAKE_BINARY_DIR}/third_party/skia/include" + "${CMAKE_BINARY_DIR}/third_party/skia/modules/svg/include" + "${CMAKE_BINARY_DIR}/third_party/skia/src" + "${CMAKE_BINARY_DIR}/third_party/skia" + "${CMAKE_BINARY_DIR}/third_party/skia/third_party/externals/angle2/include" + "${CMAKE_BINARY_DIR}/third_party/skia/third_party/externals/freetype/include" + "${CMAKE_BINARY_DIR}/third_party/skia/third_party/externals/harfbuzz/src" + "${CMAKE_BINARY_DIR}/third_party/skia/third_party/externals/icu/source/common" + "${CMAKE_BINARY_DIR}/third_party/skia/third_party/externals/libpng" + "${CMAKE_BINARY_DIR}/third_party/skia/third_party/externals/libwebp/src" + "${CMAKE_BINARY_DIR}/third_party/skia/third_party/externals/swiftshader/include" + "${CMAKE_BINARY_DIR}/third_party/skia/third_party/externals/zlib" + "${CMAKE_BINARY_DIR}/third_party/skia/third_party/icu" +) +target_link_directories(Skia INTERFACE "${CMAKE_BINARY_DIR}/third_party/skia/out/${SKIA_VARIANT}-${SKIA_OS}-${SKIA_ARCH}") +target_link_libraries(Skia INTERFACE + bentleyottmann.lib libwebp.lib skottie.lib skshaper.lib svg.lib + d3d12allocator.lib icu.lib libwebp_sse41.lib skparagraph.lib skunicode_core.lib wuffs.lib + expat.lib libjpeg.lib skcms.lib skresources.lib skunicode_icu.lib zlib.lib + harfbuzz.lib libpng.lib skia.lib sksg.lib spirv_cross.lib +) \ No newline at end of file From 443011c6fcc88ce3907d984904385e278e5ecafd Mon Sep 17 00:00:00 2001 From: silenium-dev Date: Sun, 5 Oct 2025 16:04:22 +0200 Subject: [PATCH 08/16] chore: cleanup D3DInterop --- native/src/cpp/windows/D3DInterop.cpp | 24 +------------------ .../silenium/compose/gl/interop/D3DInterop.kt | 9 ++++--- 2 files changed, 5 insertions(+), 28 deletions(-) diff --git a/native/src/cpp/windows/D3DInterop.cpp b/native/src/cpp/windows/D3DInterop.cpp index 382e6ad..484a201 100644 --- a/native/src/cpp/windows/D3DInterop.cpp +++ b/native/src/cpp/windows/D3DInterop.cpp @@ -5,7 +5,7 @@ #include #include -#define SK_DIRECT3D // for some reason, the d3d headers undefine this macro +#define SK_DIRECT3D// for some reason, the d3d headers undefine this macro #include #include @@ -114,26 +114,4 @@ JNIEXPORT void JNICALL Java_dev_silenium_compose_gl_interop_D3DInteropKt_closeSh const auto handle = reinterpret_cast(_handle); CloseHandle(handle); } -JNIEXPORT jstring JNICALL Java_dev_silenium_compose_gl_interop_D3DInteropKt_getDirectContextApiN(JNIEnv *env, jobject thiz, jobject _directContext) { - const auto clazz = env->GetObjectClass(_directContext); - const auto ptrField = env->GetFieldID(clazz, "_ptr", "J"); - const auto ptr = env->GetLongField(_directContext, ptrField); - const auto directContext = reinterpret_cast(ptr); - switch (directContext->backend()) { - case GrBackendApi::kOpenGL: - return env->NewStringUTF("kOpenGL"); - case GrBackendApi::kVulkan: - return env->NewStringUTF("kVulkan"); - case GrBackendApi::kMetal: - return env->NewStringUTF("kMetal"); - case GrBackendApi::kDirect3D: - return env->NewStringUTF("kDirect3D"); - case GrBackendApi::kMock: - return env->NewStringUTF("kMock"); - case GrBackendApi::kUnsupported: - return env->NewStringUTF("kUnsupported"); - default: - return nullptr; - } -} } diff --git a/src/main/java/dev/silenium/compose/gl/interop/D3DInterop.kt b/src/main/java/dev/silenium/compose/gl/interop/D3DInterop.kt index 6dba7c8..d87d7d3 100644 --- a/src/main/java/dev/silenium/compose/gl/interop/D3DInterop.kt +++ b/src/main/java/dev/silenium/compose/gl/interop/D3DInterop.kt @@ -3,7 +3,6 @@ package dev.silenium.compose.gl.interop import dev.silenium.compose.gl.directX12Device import dev.silenium.libs.jni.NativeLoader import org.jetbrains.skia.BackendTexture -import org.jetbrains.skia.DirectContext import org.jetbrains.skia.impl.NativePointer import java.awt.Window @@ -12,22 +11,23 @@ object D3DInterop { val device = window.directX12Device() ?: throw IllegalStateException("No D3D12 device found") return createD3DTextureN(device, width, height) } + fun destroyTexture(texture: NativePointer) { destroyD3DTextureN(texture) } + fun exportSharedHandle(window: Window, texture: NativePointer): NativePointer { val device = window.directX12Device() ?: throw IllegalStateException("No D3D12 device found") return exportSharedHandleN(device, texture) } + fun closeSharedHandle(handle: NativePointer) { closeSharedHandleN(handle) } + fun makeBackendTexture(texture: NativePointer): BackendTexture { return Compat.create(makeD3DBackendTextureN(texture)) } - fun getDirectContextApi(context: DirectContext) { - getDirectContextApiN(context) - } init { NativeLoader.loadLibraryFromClasspath("compose-gl").getOrThrow() @@ -39,4 +39,3 @@ private external fun exportSharedHandleN(device: NativePointer, texture: NativeP private external fun makeD3DBackendTextureN(texture: NativePointer): NativePointer private external fun destroyD3DTextureN(texture: NativePointer) private external fun closeSharedHandleN(handle: NativePointer) -private external fun getDirectContextApiN(context: DirectContext) From 64934b69a4c1da9c3c349088ec25dbe63fde6ed4 Mon Sep 17 00:00:00 2001 From: silenium-dev Date: Sun, 5 Oct 2025 16:04:33 +0200 Subject: [PATCH 09/16] feat: display skia and skiko versions --- src/test/kotlin/direct_import/Main.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/test/kotlin/direct_import/Main.kt b/src/test/kotlin/direct_import/Main.kt index d4ce437..5cf7a2f 100644 --- a/src/test/kotlin/direct_import/Main.kt +++ b/src/test/kotlin/direct_import/Main.kt @@ -29,6 +29,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext import org.jetbrains.skia.* +import org.jetbrains.skiko.Version import org.lwjgl.BufferUtils import org.lwjgl.glfw.GLFW import org.lwjgl.opengl.EXTMemoryObject @@ -260,7 +261,7 @@ fun main() = application { Surface( shape = MaterialTheme.shapes.medium, color = MaterialTheme.colors.surface, - modifier = Modifier.padding(8.dp).width(200.dp), + modifier = Modifier.padding(8.dp).wrapContentWidth(), ) { Column( verticalArrangement = Arrangement.spacedBy(4.dp), @@ -268,6 +269,8 @@ fun main() = application { modifier = Modifier.padding(8.dp), ) { Text("Skia Graphics API: ${window.graphicsApi()}") + Text("Skia Version: ${Version.skia}") + Text("Skiko Version: ${Version.skiko}") Button(onClick = { print("button pressed") }) { Text("Button") } From 616b38fd7b98e4dd59e447ed6df44bf5950d7a99 Mon Sep 17 00:00:00 2001 From: silenium-dev Date: Sun, 5 Oct 2025 17:26:40 +0200 Subject: [PATCH 10/16] build: retrieve skia version from skiko dependency and configure it in native build --- build.gradle.kts | 81 ++++++++++++++++----------- gradle/libs.versions.toml | 1 + native/build.gradle.kts | 16 +++--- native/cmake/skia.cmake | 4 +- src/test/kotlin/direct_import/Main.kt | 10 +++- 5 files changed, 69 insertions(+), 43 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 2924efb..9befed2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,11 +1,15 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinVersion +import java.net.URLClassLoader +import kotlin.reflect.full.functions +import kotlin.reflect.jvm.isAccessible plugins { alias(libs.plugins.kotlin) alias(libs.plugins.kotlin.compose) alias(libs.plugins.compose) + alias(libs.plugins.bytebuddy) `maven-publish` } @@ -18,6 +22,27 @@ repositories { google() } +allprojects { + apply() + apply() + + this.group = "dev.silenium.compose.gl" + this.version = findProperty("deploy.version") as String? ?: "0.0.0-SNAPSHOT" + + publishing { + repositories { + val url = System.getenv("MAVEN_REPO_URL") ?: return@repositories + maven(url) { + name = "reposilite" + credentials { + username = System.getenv("MAVEN_REPO_USERNAME") ?: "" + password = System.getenv("MAVEN_REPO_PASSWORD") ?: "" + } + } + } + } +} + val deployNative = (findProperty("deploy.native") as String?)?.toBoolean() ?: true val deployKotlin = (findProperty("deploy.kotlin") as String?)?.toBoolean() ?: true @@ -43,29 +68,13 @@ dependencies { implementation(libs.bundles.kotlinx.coroutines) implementation("net.java.dev.jna:jna") -// api(libs.bundles.skiko) { -// version { -// strictly(libs.skiko.awt.runtime.linux.x64.get().version!!) -// } -// } + api(libs.bundles.skiko) testImplementation(compose.desktop.currentOs) testImplementation(libs.logback.classic) testImplementation("me.saket.telephoto:zoomable:0.14.0") } -compose.desktop { - application { - mainClass = "MainKt" - - nativeDistributions { - targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) - packageName = "compose-gl" - packageVersion = "1.0.0" - } - } -} - java { withSourcesJar() sourceCompatibility = JavaVersion.VERSION_11 @@ -80,23 +89,31 @@ kotlin { } } -allprojects { - apply() - apply() +val skiaVersion = configurations.compileClasspath.map { + it.filter { it.name.matches(Regex("skiko-awt-\\d.+.jar")) }.singleFile +}.get().let { + val loader = URLClassLoader(arrayOf(it.toURI().toURL())) + val clazz = loader.loadClass("org.jetbrains.skiko.Version").kotlin + val getSkikoVersion = clazz.functions.single { it.name == "getSkiko" } + val getSkiaVersion = clazz.functions.single { it.name == "getSkia" } + val constructor = clazz.constructors.single() + constructor.isAccessible = true + val instance = constructor.call() + val skikoVersion = getSkikoVersion.call(instance) + val skiaVersion = getSkiaVersion.call(instance) + println("skiko version: $skikoVersion") + println("skia version: $skiaVersion") + rootProject.ext.set("skia.version", skiaVersion) +} - group = "dev.silenium.compose.gl" - version = findProperty("deploy.version") as String? ?: "0.0.0-SNAPSHOT" +compose.desktop { + application { + mainClass = "MainKt" - publishing { - repositories { - val url = System.getenv("MAVEN_REPO_URL") ?: return@repositories - maven(url) { - name = "reposilite" - credentials { - username = System.getenv("MAVEN_REPO_USERNAME") ?: "" - password = System.getenv("MAVEN_REPO_PASSWORD") ?: "" - } - } + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "compose-gl" + packageVersion = "1.0.0" } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 573e887..36ec595 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,6 +31,7 @@ logback-classic = { group = "ch.qos.logback", name = "logback-classic", version kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } compose = { id = "org.jetbrains.compose", version.ref = "compose" } +bytebuddy = { id = "net.bytebuddy.byte-buddy-gradle-plugin", version = "1.17.7" } [bundles] kotlinx-coroutines = [ diff --git a/native/build.gradle.kts b/native/build.gradle.kts index 7de52ee..5100e50 100644 --- a/native/build.gradle.kts +++ b/native/build.gradle.kts @@ -29,21 +29,20 @@ val platform = platformString?.let(Platform::invoke) ?: NativePlatform.platform( val cmakeExe = findProperty("cmake.executable") as? String ?: "cmake" val generateMakefile = tasks.register("generateMakefile") { workingDir = layout.buildDirectory.dir("cmake").get().asFile.apply { mkdirs() } - val additionalFlags = mutableListOf( - "-DJAVA_HOME=${System.getProperty("java.home")}", - "-DPROJECT_NAME=${libName}", + val additionalFlags = listOfNotNull( + "JAVA_HOME" to System.getProperty("java.home"), + "PROJECT_NAME" to libName, + "CMAKE_BUILD_TYPE" to "Release", + rootProject.ext.get("skia.version")?.let { "SKIA_VERSION" to it }, ) commandLine( cmakeExe, - *additionalFlags.toTypedArray(), + *additionalFlags.map { "-D${it.first}=${it.second}" }.toTypedArray(), layout.projectDirectory.asFile.absolutePath, ) inputs.file(layout.projectDirectory.file("CMakeLists.txt")) - inputs.properties( - "JAVA_HOME" to System.getProperty("java.home"), - "PROJECT_NAME" to libName, - ) + inputs.properties(additionalFlags.toMap()) outputs.dir(workingDir) standardOutput = System.out } @@ -76,6 +75,7 @@ val jar = tasks.register("nativeJar") { val libName = rootProject.name val platformString = findProperty("deploy.platform")?.toString() val platform = platformString?.let(Platform::invoke) ?: NativePlatform.platform() + archiveBaseName.set("$libName-natives-$platform") from(compileNative.get().outputs.files) { rename { diff --git a/native/cmake/skia.cmake b/native/cmake/skia.cmake index 2ea7ba6..1c570e9 100644 --- a/native/cmake/skia.cmake +++ b/native/cmake/skia.cmake @@ -1,4 +1,4 @@ -set(SKIA_VERSION "m132-a00c390e98-1") +set(SKIA_VERSION "m132-a00c390e98-1" CACHE STRING "Skia version") set(SKIA_OS "linux") set(SKIA_ARCH "x64") set(SKIA_VARIANT "Debug") @@ -108,4 +108,4 @@ target_link_libraries(Skia INTERFACE d3d12allocator.lib icu.lib libwebp_sse41.lib skparagraph.lib skunicode_core.lib wuffs.lib expat.lib libjpeg.lib skcms.lib skresources.lib skunicode_icu.lib zlib.lib harfbuzz.lib libpng.lib skia.lib sksg.lib spirv_cross.lib -) \ No newline at end of file +) diff --git a/src/test/kotlin/direct_import/Main.kt b/src/test/kotlin/direct_import/Main.kt index 5cf7a2f..937b7b9 100644 --- a/src/test/kotlin/direct_import/Main.kt +++ b/src/test/kotlin/direct_import/Main.kt @@ -42,6 +42,7 @@ import org.lwjgl.system.MemoryUtil import java.io.File import javax.imageio.ImageIO + //language=glsl const val VERTEX_SHADER_SOURCE = """ #version 330 core @@ -87,6 +88,13 @@ void main(){ fun main() = application { System.setProperty("skiko.renderApi", "DIRECT3D") + val classpath = System.getProperty("java.class.path") + val classPathValues: Array = + classpath.split(File.pathSeparator.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + for (cpv in classPathValues) { + println(cpv) + } + var directContext: DirectContext? by mutableStateOf(null) var d3dTexture: Long? = null @@ -150,7 +158,7 @@ fun main() = application { GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE) glfwWindow = GLFW.glfwCreateWindow(128, 128, "", MemoryUtil.NULL, MemoryUtil.NULL) - println("glfwWindow: $glfwWindow") +// println("glfwWindow: $glfwWindow") if (glfwWindow == MemoryUtil.NULL) { throw RuntimeException("Failed to create GLFW window") } From a57b6421a70816c68fb1c87c59c5f6eea49696f5 Mon Sep 17 00:00:00 2001 From: silenium-dev Date: Sun, 5 Oct 2025 21:26:14 +0200 Subject: [PATCH 11/16] feat: start implementing proper GLDirectCanvas with platform specific canvas interfaces --- build.gradle.kts | 6 +- native/build.gradle.kts | 2 +- .../compose/gl/direct/CanvasInterface.kt | 32 ++++ .../compose/gl/direct/D3DCanvasInterface.kt | 165 ++++++++++++++++++ .../direct/DefaultCanvasInterfaceFactory.kt | 18 ++ .../compose/gl/direct/GLCanvasInterface.kt | 109 ++++++++++++ .../compose/gl/direct/GLDirectCanvas.kt | 118 ++----------- .../silenium/compose/gl/interop/D3DInterop.kt | 2 +- .../interop/{Compat.java => SkikoCompat.java} | 5 +- src/test/kotlin/direct/Main.kt | 59 +++++++ .../kotlin/direct_import/GLTextureDrawer.kt | 55 +----- src/test/kotlin/direct_import/Main.kt | 2 +- 12 files changed, 417 insertions(+), 156 deletions(-) create mode 100644 src/main/java/dev/silenium/compose/gl/direct/CanvasInterface.kt create mode 100644 src/main/java/dev/silenium/compose/gl/direct/D3DCanvasInterface.kt create mode 100644 src/main/java/dev/silenium/compose/gl/direct/DefaultCanvasInterfaceFactory.kt create mode 100644 src/main/java/dev/silenium/compose/gl/direct/GLCanvasInterface.kt rename src/main/java/dev/silenium/compose/gl/interop/{Compat.java => SkikoCompat.java} (71%) create mode 100644 src/test/kotlin/direct/Main.kt diff --git a/build.gradle.kts b/build.gradle.kts index 9befed2..0cccb32 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -68,7 +68,11 @@ dependencies { implementation(libs.bundles.kotlinx.coroutines) implementation("net.java.dev.jna:jna") - api(libs.bundles.skiko) + api(libs.bundles.skiko) { + version { + strictly(libs.versions.skiko.get()) + } + } testImplementation(compose.desktop.currentOs) testImplementation(libs.logback.classic) diff --git a/native/build.gradle.kts b/native/build.gradle.kts index 5100e50..930ceda 100644 --- a/native/build.gradle.kts +++ b/native/build.gradle.kts @@ -32,7 +32,7 @@ val generateMakefile = tasks.register("generateMakefile") { val additionalFlags = listOfNotNull( "JAVA_HOME" to System.getProperty("java.home"), "PROJECT_NAME" to libName, - "CMAKE_BUILD_TYPE" to "Release", + "CMAKE_BUILD_TYPE" to "Debug", rootProject.ext.get("skia.version")?.let { "SKIA_VERSION" to it }, ) commandLine( diff --git a/src/main/java/dev/silenium/compose/gl/direct/CanvasInterface.kt b/src/main/java/dev/silenium/compose/gl/direct/CanvasInterface.kt new file mode 100644 index 0000000..2cb4f74 --- /dev/null +++ b/src/main/java/dev/silenium/compose/gl/direct/CanvasInterface.kt @@ -0,0 +1,32 @@ +package dev.silenium.compose.gl.direct + +import androidx.compose.ui.graphics.drawscope.DrawScope +import dev.silenium.compose.gl.fbo.FBO +import org.jetbrains.skia.DirectContext +import org.lwjgl.opengl.GL11.glFlush +import java.awt.Window + +interface CanvasInterface { + fun setup(directContext: DirectContext) + fun render(scope: DrawScope, block: GLDrawScope.() -> Unit) + fun display(scope: DrawScope) + fun dispose() +} + +fun interface CanvasInterfaceFactory { + fun create(window: Window): T +} + +data class GLDrawScope( + val fbo: FBO, +) + +internal inline fun GLDrawScope.drawGL(block: () -> T): T { + fbo.bind() + try { + return block() + } finally { + fbo.unbind() + glFlush() + } +} diff --git a/src/main/java/dev/silenium/compose/gl/direct/D3DCanvasInterface.kt b/src/main/java/dev/silenium/compose/gl/direct/D3DCanvasInterface.kt new file mode 100644 index 0000000..33ce653 --- /dev/null +++ b/src/main/java/dev/silenium/compose/gl/direct/D3DCanvasInterface.kt @@ -0,0 +1,165 @@ +package dev.silenium.compose.gl.direct + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.toIntSize +import dev.silenium.compose.gl.fbo.FBO +import dev.silenium.compose.gl.interop.D3DInterop +import dev.silenium.compose.gl.objects.Renderbuffer +import dev.silenium.compose.gl.objects.Texture +import dev.silenium.compose.gl.util.checkGLError +import org.jetbrains.skia.* +import org.lwjgl.glfw.GLFW +import org.lwjgl.opengl.EXTMemoryObject +import org.lwjgl.opengl.EXTMemoryObject.GL_OPTIMAL_TILING_EXT +import org.lwjgl.opengl.EXTMemoryObject.GL_TEXTURE_TILING_EXT +import org.lwjgl.opengl.EXTMemoryObjectWin32 +import org.lwjgl.opengl.GL +import org.lwjgl.opengl.GL11.* +import org.lwjgl.opengl.GL12.GL_CLAMP_TO_EDGE +import org.lwjgl.opengl.GL30.GL_DEPTH24_STENCIL8 +import org.lwjgl.opengl.GLCapabilities +import org.lwjgl.system.MemoryUtil +import org.slf4j.LoggerFactory +import java.awt.Window + +class D3DCanvasInterface(private val window: Window) : CanvasInterface { + private var d3dDirectContext: DirectContext? by mutableStateOf(null) + var d3dTexture: Long? = null + var sharedHandle: Long? = null + var backendTexture: BackendTexture? = null + var image: Image? = null + var initialized by mutableStateOf(false) + + var glMemory: Int? = null + var glfwWindow = 0L + var fbo: FBO? = null + var glCaps: GLCapabilities? = null + + override fun setup(directContext: DirectContext) { + d3dDirectContext = directContext + } + + private fun ensureInitialized() { + if (!initialized) { + GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE) + + glfwWindow = GLFW.glfwCreateWindow(128, 128, "", MemoryUtil.NULL, MemoryUtil.NULL) + if (glfwWindow == MemoryUtil.NULL) { + error("Failed to create GLFW window") + } + GLFW.glfwMakeContextCurrent(glfwWindow) + glCaps = GL.createCapabilities() + GLFW.glfwMakeContextCurrent(MemoryUtil.NULL) + initialized = true + } + } + + override fun render( + scope: DrawScope, + block: GLDrawScope.() -> Unit + ) { + val d3dCtx = d3dDirectContext ?: return + ensureInitialized() + GLFW.glfwMakeContextCurrent(glfwWindow) + ensureFBO(scope.size.toIntSize(), d3dCtx) + GLDrawScope(fbo!!).block() + glFlush() + GLFW.glfwMakeContextCurrent(MemoryUtil.NULL) + } + + override fun display(scope: DrawScope) { + d3dDirectContext ?: return + ensureInitialized() + val img = image ?: return log.warn("No image") + scope.drawContext.canvas.nativeCanvas.drawImage(img, 0f, 0f) + } + + override fun dispose() { + GLFW.glfwMakeContextCurrent(glfwWindow) + fbo?.destroy() + image?.close() + glMemory?.let(EXTMemoryObject::glDeleteMemoryObjectsEXT) + d3dTexture?.let(D3DInterop::destroyTexture) + sharedHandle?.let(D3DInterop::closeSharedHandle) + GLFW.glfwMakeContextCurrent(MemoryUtil.NULL) + glfwWindow.let(GLFW::glfwDestroyWindow) + println("Disposed") + } + + private fun ensureFBO(size: IntSize, d3dContext: DirectContext) { + if (fbo?.size != size) { + glMemory?.let(EXTMemoryObject::glDeleteMemoryObjectsEXT) + glMemory = null + sharedHandle?.let(D3DInterop::closeSharedHandle) + sharedHandle = null + d3dTexture?.let(D3DInterop::destroyTexture) + d3dTexture = null + backendTexture?.close() + backendTexture = null + image?.close() + image = null + fbo?.destroy() + fbo = null + + d3dTexture = D3DInterop.createTexture(window, size.width, size.height) + sharedHandle = D3DInterop.exportSharedHandle(window, d3dTexture!!) + + val colorAttachment = Texture(glGenTextures(), size, GL_TEXTURE_2D, GL_RGBA8) + colorAttachment.bind() + glTexParameteri(colorAttachment.target, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + checkGLError("glTexParameteri") + glTexParameteri(colorAttachment.target, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + checkGLError("glTexParameteri") + glTexParameteri(colorAttachment.target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) + checkGLError("glTexParameteri") + glTexParameteri(colorAttachment.target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) + checkGLError("glTexParameteri") + glTexParameteri(colorAttachment.target, GL_TEXTURE_TILING_EXT, GL_OPTIMAL_TILING_EXT) + checkGLError("glTexParameteri") + colorAttachment.unbind() + + glMemory = EXTMemoryObject.glCreateMemoryObjectsEXT() + checkGLError("glCreateMemoryObjectsEXT") + EXTMemoryObjectWin32.glImportMemoryWin32HandleEXT( + glMemory!!, size.width * size.height * 4 * 2L, + EXTMemoryObjectWin32.GL_HANDLE_TYPE_D3D12_RESOURCE_EXT, sharedHandle!!, + ) + checkGLError("glImportMemoryWin32HandleEXT") + + EXTMemoryObject.glTextureStorageMem2DEXT( + colorAttachment.id, + 1, GL_RGBA8, + size.width, size.height, + glMemory!!, 0 + ) + checkGLError("glTextureStorageMem2DEXT") + + val depthStencilAttachment = Renderbuffer.create(size, GL_DEPTH24_STENCIL8) + fbo = FBO.create(colorAttachment, depthStencilAttachment) + + backendTexture = D3DInterop.makeBackendTexture(d3dTexture!!) + image = Image.adoptTextureFrom( + d3dContext, backendTexture!!, + SurfaceOrigin.TOP_LEFT, ColorType.RGBA_8888, + ) + } + } + + companion object : CanvasInterfaceFactory { + private val log = LoggerFactory.getLogger(D3DCanvasInterface::class.java) + + override fun create(window: Window) = D3DCanvasInterface(window) + + init { + GLFW.glfwInitHint(GLFW.GLFW_COCOA_MENUBAR, GLFW.GLFW_FALSE) + if (!GLFW.glfwInit()) { + throw RuntimeException("Failed to initialize GLFW") + } + } + } +} diff --git a/src/main/java/dev/silenium/compose/gl/direct/DefaultCanvasInterfaceFactory.kt b/src/main/java/dev/silenium/compose/gl/direct/DefaultCanvasInterfaceFactory.kt new file mode 100644 index 0000000..7594c94 --- /dev/null +++ b/src/main/java/dev/silenium/compose/gl/direct/DefaultCanvasInterfaceFactory.kt @@ -0,0 +1,18 @@ +package dev.silenium.compose.gl.direct + +import dev.silenium.compose.gl.graphicsApi +import org.jetbrains.skiko.GraphicsApi +import java.awt.Window + +object DefaultCanvasInterfaceFactory : CanvasInterfaceFactory { + override fun create(window: Window): CanvasInterface { + val factory = apiFactories[window.graphicsApi()] + factory ?: throw UnsupportedOperationException("Unsupported graphics api: ${window.graphicsApi()}") + return factory.create(window) + } + + private val apiFactories = mapOf( + GraphicsApi.OPENGL to GLCanvasInterface, + GraphicsApi.DIRECT3D to D3DCanvasInterface, + ) +} diff --git a/src/main/java/dev/silenium/compose/gl/direct/GLCanvasInterface.kt b/src/main/java/dev/silenium/compose/gl/direct/GLCanvasInterface.kt new file mode 100644 index 0000000..64f7f58 --- /dev/null +++ b/src/main/java/dev/silenium/compose/gl/direct/GLCanvasInterface.kt @@ -0,0 +1,109 @@ +package dev.silenium.compose.gl.direct + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.unit.toIntSize +import dev.silenium.compose.gl.fbo.FBO +import dev.silenium.compose.gl.objects.Renderbuffer +import dev.silenium.compose.gl.objects.Texture +import org.jetbrains.skia.* +import org.lwjgl.opengl.GL +import org.lwjgl.opengl.GL11 +import org.lwjgl.opengl.GL11.glFlush +import org.lwjgl.opengl.GL33 +import org.lwjgl.opengl.GLCapabilities +import org.slf4j.LoggerFactory +import java.awt.Window + +class GLCanvasInterface : CanvasInterface { + var fbo: FBO? = null + var image: Image? = null + var texture: BackendTexture? = null + var initialized = false + lateinit var glCapabilities: GLCapabilities + var directContext: DirectContext? by mutableStateOf(null) + var size: Size = Size.Zero + + override fun setup(directContext: DirectContext) { + this.directContext = directContext + } + + private fun ensureInitialized() { + if (!initialized) { + glCapabilities = GL.createCapabilities() + initialized = true + } + } + + override fun dispose() { + texture?.close() + fbo?.destroy() + } + + override fun render( + scope: DrawScope, + block: GLDrawScope.() -> Unit + ) { + val ctx = directContext ?: return + ctx.submit(syncCpu = true) + ensureInitialized() + log.trace("Ensuring FBO") + ensureFBO(scope.size, ctx) + log.trace("Drawing to FBO") + GLDrawScope(fbo!!).block() + log.trace("Flushing") + glFlush() + ctx.resetGLAll() + log.trace("Rendering done") + } + + override fun display(scope: DrawScope) { + directContext ?: return + val img = image ?: return log.warn("No image") + ensureInitialized() + scope.drawContext.canvas.nativeCanvas.drawImage(img, 0f, 0f) + } + + private fun ensureFBO(size: Size, ctx: DirectContext) { + if (size != this.size) { + texture?.close() + fbo?.destroy() + val fbo = createFBO(size).also { this.fbo = it } + this.size = size + + val texture = BackendTexture.makeGL( + width = fbo.size.width, + height = fbo.size.height, + isMipmapped = false, + textureId = fbo.colorAttachment.id, + textureTarget = fbo.colorAttachment.target, + textureFormat = fbo.colorAttachment.internalFormat, + ).also { this.texture = it } + image = Image.adoptTextureFrom( + context = ctx, + backendTexture = texture, + origin = SurfaceOrigin.TOP_LEFT, + colorType = ColorType.RGBA_8888, + ) + } + } + + private fun createFBO(size: Size): FBO { + val colorAttachment = Texture.create( + target = GL11.GL_TEXTURE_2D, size = size.toIntSize(), internalFormat = GL11.GL_RGBA8, + wrapS = GL33.GL_CLAMP_TO_EDGE, wrapT = GL33.GL_CLAMP_TO_EDGE, + minFilter = GL33.GL_NEAREST, magFilter = GL33.GL_NEAREST, + ) + val depthStencil = Renderbuffer.create(size.toIntSize(), GL33.GL_DEPTH24_STENCIL8) + return FBO.create(colorAttachment, depthStencil) + } + + companion object : CanvasInterfaceFactory { + private val log = LoggerFactory.getLogger(GLCanvasInterface::class.java) + override fun create(window: Window) = GLCanvasInterface() + } +} diff --git a/src/main/java/dev/silenium/compose/gl/direct/GLDirectCanvas.kt b/src/main/java/dev/silenium/compose/gl/direct/GLDirectCanvas.kt index b9fc62b..53a0f94 100644 --- a/src/main/java/dev/silenium/compose/gl/direct/GLDirectCanvas.kt +++ b/src/main/java/dev/silenium/compose/gl/direct/GLDirectCanvas.kt @@ -2,127 +2,43 @@ package dev.silenium.compose.gl.direct import androidx.compose.foundation.Canvas import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.graphics.nativeCanvas -import androidx.compose.ui.unit.toIntSize import dev.silenium.compose.gl.LocalWindow import dev.silenium.compose.gl.directContext -import dev.silenium.compose.gl.fbo.FBO -import dev.silenium.compose.gl.fbo.FBODrawScope -import dev.silenium.compose.gl.fbo.draw -import dev.silenium.compose.gl.objects.Renderbuffer -import dev.silenium.compose.gl.objects.Texture import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext -import org.jetbrains.skia.BackendTexture -import org.jetbrains.skia.ColorType -import org.jetbrains.skia.DirectContext -import org.jetbrains.skia.Image -import org.jetbrains.skia.SurfaceOrigin -import org.lwjgl.opengl.GL -import org.lwjgl.opengl.GL11 -import org.lwjgl.opengl.GL30 -import org.lwjgl.opengl.GL33 -import org.lwjgl.opengl.GLCapabilities -import org.slf4j.LoggerFactory - -private class GLDirectCanvasState { - var fbo: FBO? = null - var image: Image? = null - var texture: BackendTexture? = null - var initialized = false - lateinit var glCapabilities: GLCapabilities - var directContext: DirectContext? by mutableStateOf(null) - var size: Size = Size.Zero - - fun draw(scope: DrawScope, block: FBODrawScope.() -> Unit) { - val ctx = directContext ?: return log.warn("No direct context") - ctx.submit(syncCpu = true) - ensureInitialized() - ensureFBO(scope.size, ctx) - fbo!!.draw(block) - ctx.resetGLAll() - } - - fun display(scope: DrawScope) { - directContext ?: return log.warn("No direct context") - val img = image ?: return log.warn("No image") - ensureInitialized() - scope.drawContext.canvas.nativeCanvas.drawImage(img, 0f, 0f) - } - - private fun ensureInitialized() { - if (!initialized) { - glCapabilities = GL.createCapabilities() - initialized = true - } - } - - private fun ensureFBO(size: Size, ctx: DirectContext) { - if (size != this.size) { - image?.close() - fbo?.id?.let(GL30::glDeleteFramebuffers) - fbo?.depthStencilAttachment?.destroy() - val fbo = createFBO(size).also { this.fbo = it } - this.size = size - - val texture = BackendTexture.makeGL( - width = fbo.size.width, - height = fbo.size.height, - isMipmapped = false, - textureId = fbo.colorAttachment.id, - textureTarget = fbo.colorAttachment.target, - textureFormat = fbo.colorAttachment.internalFormat, - ).also { this.texture = it } - image = Image.adoptTextureFrom( - context = ctx, - backendTexture = texture, - origin = SurfaceOrigin.BOTTOM_LEFT, - colorType = ColorType.RGBA_8888, - ) - } - } - - companion object { - private val log = LoggerFactory.getLogger(GLDirectCanvasState::class.java) - } -} @Composable -fun GLDirectCanvas(modifier: Modifier = Modifier, block: FBODrawScope.() -> Unit) { - val state = remember { GLDirectCanvasState() } +fun GLCanvas( + wrapperFactory: CanvasInterfaceFactory<*> = DefaultCanvasInterfaceFactory, + modifier: Modifier = Modifier, + block: GLDrawScope.() -> Unit +) { val window = LocalWindow.current ?: throw IllegalStateException("No window") + val wrapper = remember { wrapperFactory.create(window) } LaunchedEffect(window) { withContext(Dispatchers.IO) { while (isActive) { window.directContext()?.let { - state.directContext = it + wrapper.setup(it) return@withContext } } } } + DisposableEffect(window) { + onDispose { + wrapper.dispose() + } + } Canvas(modifier) { - state.directContext ?: return@Canvas - state.draw(this, block) - state.display(this) + wrapper.render(this) { + drawGL { block() } + } + wrapper.display(this) } } - -private fun createFBO(size: Size): FBO { - val colorAttachment = Texture.create( - target = GL11.GL_TEXTURE_2D, size = size.toIntSize(), internalFormat = GL11.GL_RGBA8, - wrapS = GL33.GL_CLAMP_TO_EDGE, wrapT = GL33.GL_CLAMP_TO_EDGE, - minFilter = GL33.GL_NEAREST, magFilter = GL33.GL_NEAREST, - ) - val depthStencil = Renderbuffer.create(size.toIntSize(), GL33.GL_DEPTH24_STENCIL8) - return FBO.create(colorAttachment, depthStencil) -} diff --git a/src/main/java/dev/silenium/compose/gl/interop/D3DInterop.kt b/src/main/java/dev/silenium/compose/gl/interop/D3DInterop.kt index d87d7d3..4a64e19 100644 --- a/src/main/java/dev/silenium/compose/gl/interop/D3DInterop.kt +++ b/src/main/java/dev/silenium/compose/gl/interop/D3DInterop.kt @@ -26,7 +26,7 @@ object D3DInterop { } fun makeBackendTexture(texture: NativePointer): BackendTexture { - return Compat.create(makeD3DBackendTextureN(texture)) + return SkikoCompat.create(makeD3DBackendTextureN(texture)) } init { diff --git a/src/main/java/dev/silenium/compose/gl/interop/Compat.java b/src/main/java/dev/silenium/compose/gl/interop/SkikoCompat.java similarity index 71% rename from src/main/java/dev/silenium/compose/gl/interop/Compat.java rename to src/main/java/dev/silenium/compose/gl/interop/SkikoCompat.java index 45a5e79..5da82c7 100644 --- a/src/main/java/dev/silenium/compose/gl/interop/Compat.java +++ b/src/main/java/dev/silenium/compose/gl/interop/SkikoCompat.java @@ -2,7 +2,10 @@ import org.jetbrains.skia.BackendTexture; -class Compat { +/** + * Java bridge to access internal Skiko methods + */ +class SkikoCompat { static BackendTexture create(long nativePtr) { return new BackendTexture(nativePtr); } diff --git a/src/test/kotlin/direct/Main.kt b/src/test/kotlin/direct/Main.kt new file mode 100644 index 0000000..236e89b --- /dev/null +++ b/src/test/kotlin/direct/Main.kt @@ -0,0 +1,59 @@ +package direct + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.scene.PlatformLayersComposeScene +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import dev.silenium.compose.gl.direct.GLCanvas +import dev.silenium.compose.gl.graphicsApi +import direct_import.GLTextureDrawer +import org.jetbrains.skiko.Version + +@OptIn(InternalComposeUiApi::class) +fun main() = application { + val glScene = PlatformLayersComposeScene() + glScene.setContent { + Text("Hello from Skia on OpenGL", style = MaterialTheme.typography.h2) + } + + val drawer = GLTextureDrawer() + Window(onCloseRequest = ::exitApplication, title = "Test") { + Box(Modifier.fillMaxSize()) { + DisposableEffect(Unit) { + onDispose { + drawer.destroy() + } + } + GLCanvas(modifier = Modifier.fillMaxSize()) { + drawer.render() + } + Surface( + shape = MaterialTheme.shapes.medium, + color = MaterialTheme.colors.surface, + modifier = Modifier.padding(8.dp).wrapContentWidth(), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.Start, + modifier = Modifier.padding(8.dp), + ) { + Text("Skia Graphics API: ${window.graphicsApi()}") + Text("Skia Version: ${Version.skia}") + Text("Skiko Version: ${Version.skiko}") + Button(onClick = { println("button pressed") }) { + Text("Button") + } + } + } + } + } +} diff --git a/src/test/kotlin/direct_import/GLTextureDrawer.kt b/src/test/kotlin/direct_import/GLTextureDrawer.kt index db96b96..6888384 100644 --- a/src/test/kotlin/direct_import/GLTextureDrawer.kt +++ b/src/test/kotlin/direct_import/GLTextureDrawer.kt @@ -1,57 +1,12 @@ package direct_import -import org.lwjgl.opengl.GL -import org.lwjgl.opengl.GL11.GL_BLEND -import org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT -import org.lwjgl.opengl.GL11.GL_FALSE -import org.lwjgl.opengl.GL11.GL_FLOAT -import org.lwjgl.opengl.GL11.GL_ONE_MINUS_SRC_ALPHA -import org.lwjgl.opengl.GL11.GL_SRC_ALPHA -import org.lwjgl.opengl.GL11.GL_TEXTURE_2D -import org.lwjgl.opengl.GL11.GL_TRIANGLES -import org.lwjgl.opengl.GL11.GL_UNSIGNED_INT -import org.lwjgl.opengl.GL11.glBindTexture -import org.lwjgl.opengl.GL11.glBlendFunc -import org.lwjgl.opengl.GL11.glClear -import org.lwjgl.opengl.GL11.glClearColor -import org.lwjgl.opengl.GL11.glDeleteTextures -import org.lwjgl.opengl.GL11.glDisable -import org.lwjgl.opengl.GL11.glDrawElements -import org.lwjgl.opengl.GL11.glEnable +import org.lwjgl.opengl.GL11.* import org.lwjgl.opengl.GL13.GL_TEXTURE0 import org.lwjgl.opengl.GL13.glActiveTexture -import org.lwjgl.opengl.GL15.GL_ARRAY_BUFFER -import org.lwjgl.opengl.GL15.GL_ELEMENT_ARRAY_BUFFER -import org.lwjgl.opengl.GL15.GL_STATIC_DRAW -import org.lwjgl.opengl.GL15.glBindBuffer -import org.lwjgl.opengl.GL15.glBufferData -import org.lwjgl.opengl.GL15.glDeleteBuffers -import org.lwjgl.opengl.GL15.glGenBuffers -import org.lwjgl.opengl.GL20.GL_COMPILE_STATUS -import org.lwjgl.opengl.GL20.GL_FRAGMENT_SHADER -import org.lwjgl.opengl.GL20.GL_LINK_STATUS -import org.lwjgl.opengl.GL20.GL_VERTEX_SHADER -import org.lwjgl.opengl.GL20.glAttachShader -import org.lwjgl.opengl.GL20.glCompileShader -import org.lwjgl.opengl.GL20.glCreateProgram -import org.lwjgl.opengl.GL20.glCreateShader -import org.lwjgl.opengl.GL20.glDeleteProgram -import org.lwjgl.opengl.GL20.glEnableVertexAttribArray -import org.lwjgl.opengl.GL20.glGetProgramInfoLog -import org.lwjgl.opengl.GL20.glGetProgrami -import org.lwjgl.opengl.GL20.glGetShaderInfoLog -import org.lwjgl.opengl.GL20.glGetShaderi -import org.lwjgl.opengl.GL20.glGetUniformLocation -import org.lwjgl.opengl.GL20.glLinkProgram -import org.lwjgl.opengl.GL20.glShaderSource -import org.lwjgl.opengl.GL20.glUniform1i -import org.lwjgl.opengl.GL20.glUseProgram -import org.lwjgl.opengl.GL20.glVertexAttribPointer -import org.lwjgl.opengl.GL30.glBindVertexArray -import org.lwjgl.opengl.GL30.glDeleteVertexArrays -import org.lwjgl.opengl.GL30.glGenVertexArrays +import org.lwjgl.opengl.GL15.* +import org.lwjgl.opengl.GL20.* +import org.lwjgl.opengl.GL30.* import java.io.File -import kotlin.random.Random class GLTextureDrawer { private var textureId = 0 @@ -65,7 +20,7 @@ class GLTextureDrawer { if (initialized) return val img = "image.png" - val (id, size) = loadTexture(File(img)) + val (id, _) = loadTexture(File(img)) textureId = id shaderProgram = glCreateProgram() diff --git a/src/test/kotlin/direct_import/Main.kt b/src/test/kotlin/direct_import/Main.kt index 937b7b9..fac9365 100644 --- a/src/test/kotlin/direct_import/Main.kt +++ b/src/test/kotlin/direct_import/Main.kt @@ -279,7 +279,7 @@ fun main() = application { Text("Skia Graphics API: ${window.graphicsApi()}") Text("Skia Version: ${Version.skia}") Text("Skiko Version: ${Version.skiko}") - Button(onClick = { print("button pressed") }) { + Button(onClick = { println("button pressed") }) { Text("Button") } } From 0890dd5faff966288c9a1ddb70d0c6dad20501c7 Mon Sep 17 00:00:00 2001 From: silenium-dev Date: Sun, 5 Oct 2025 22:08:46 +0200 Subject: [PATCH 12/16] chore: remove old render-thread-based approach, cleanup demo and improve naming --- .../CanvasDriver.kt} | 16 +- .../D3DCanvasDriver.kt} | 25 +- .../DefaultCanvasDriverFactory.kt} | 10 +- .../GLDirectCanvas.kt => canvas/GLCanvas.kt} | 13 +- .../GLCanvasDriver.kt} | 39 +- .../silenium/compose/gl/context/EGLContext.kt | 148 -------- .../silenium/compose/gl/context/GLContext.kt | 79 ---- .../gl/context/GLContextProviderFactory.kt | 67 ---- .../silenium/compose/gl/context/GLXContext.kt | 126 ------- .../silenium/compose/gl/context/WGLContext.kt | 111 ------ .../compose/gl/fbo/FBOFifoSwapChain.kt | 73 ---- .../compose/gl/fbo/FBOMailboxSwapChain.kt | 59 --- .../dev/silenium/compose/gl/fbo/FBOPool.kt | 170 --------- .../silenium/compose/gl/fbo/FBOSwapChain.kt | 38 -- .../compose/gl/fbo/NoRenderFBOAvailable.kt | 5 - .../compose/gl/surface/GLDisplayScope.kt | 11 - .../compose/gl/surface/GLDrawScope.kt | 46 --- .../silenium/compose/gl/surface/GLSurface.kt | 348 ------------------ .../compose/gl/surface/GLSurfaceState.kt | 123 ------- .../compose/gl/surface/GLSurfaceView.kt | 0 src/test/kotlin/Main.kt | 159 -------- src/test/kotlin/direct/Main.kt | 24 +- .../SampleRenderer.kt} | 96 ++++- src/test/kotlin/direct_import/Main.kt | 339 ----------------- 24 files changed, 163 insertions(+), 1962 deletions(-) rename src/main/java/dev/silenium/compose/gl/{direct/CanvasInterface.kt => canvas/CanvasDriver.kt} (58%) rename src/main/java/dev/silenium/compose/gl/{direct/D3DCanvasInterface.kt => canvas/D3DCanvasDriver.kt} (89%) rename src/main/java/dev/silenium/compose/gl/{direct/DefaultCanvasInterfaceFactory.kt => canvas/DefaultCanvasDriverFactory.kt} (56%) rename src/main/java/dev/silenium/compose/gl/{direct/GLDirectCanvas.kt => canvas/GLCanvas.kt} (74%) rename src/main/java/dev/silenium/compose/gl/{direct/GLCanvasInterface.kt => canvas/GLCanvasDriver.kt} (73%) delete mode 100644 src/main/java/dev/silenium/compose/gl/context/EGLContext.kt delete mode 100644 src/main/java/dev/silenium/compose/gl/context/GLContext.kt delete mode 100644 src/main/java/dev/silenium/compose/gl/context/GLContextProviderFactory.kt delete mode 100644 src/main/java/dev/silenium/compose/gl/context/GLXContext.kt delete mode 100644 src/main/java/dev/silenium/compose/gl/context/WGLContext.kt delete mode 100644 src/main/java/dev/silenium/compose/gl/fbo/FBOFifoSwapChain.kt delete mode 100644 src/main/java/dev/silenium/compose/gl/fbo/FBOMailboxSwapChain.kt delete mode 100644 src/main/java/dev/silenium/compose/gl/fbo/FBOPool.kt delete mode 100644 src/main/java/dev/silenium/compose/gl/fbo/FBOSwapChain.kt delete mode 100644 src/main/java/dev/silenium/compose/gl/fbo/NoRenderFBOAvailable.kt delete mode 100644 src/main/java/dev/silenium/compose/gl/surface/GLDisplayScope.kt delete mode 100644 src/main/java/dev/silenium/compose/gl/surface/GLDrawScope.kt delete mode 100644 src/main/java/dev/silenium/compose/gl/surface/GLSurface.kt delete mode 100644 src/main/java/dev/silenium/compose/gl/surface/GLSurfaceState.kt delete mode 100644 src/main/java/dev/silenium/compose/gl/surface/GLSurfaceView.kt delete mode 100644 src/test/kotlin/Main.kt rename src/test/kotlin/{direct_import/GLTextureDrawer.kt => direct/SampleRenderer.kt} (61%) delete mode 100644 src/test/kotlin/direct_import/Main.kt diff --git a/src/main/java/dev/silenium/compose/gl/direct/CanvasInterface.kt b/src/main/java/dev/silenium/compose/gl/canvas/CanvasDriver.kt similarity index 58% rename from src/main/java/dev/silenium/compose/gl/direct/CanvasInterface.kt rename to src/main/java/dev/silenium/compose/gl/canvas/CanvasDriver.kt index 2cb4f74..e7ef8b0 100644 --- a/src/main/java/dev/silenium/compose/gl/direct/CanvasInterface.kt +++ b/src/main/java/dev/silenium/compose/gl/canvas/CanvasDriver.kt @@ -1,19 +1,25 @@ -package dev.silenium.compose.gl.direct +package dev.silenium.compose.gl.canvas import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.unit.IntSize import dev.silenium.compose.gl.fbo.FBO import org.jetbrains.skia.DirectContext import org.lwjgl.opengl.GL11.glFlush import java.awt.Window -interface CanvasInterface { +interface CanvasDriver { fun setup(directContext: DirectContext) - fun render(scope: DrawScope, block: GLDrawScope.() -> Unit) + fun render( + scope: DrawScope, + userResizeHandler: GLDrawScope.(old: IntSize?, new: IntSize) -> Unit = { _, _ -> }, + block: GLDrawScope.() -> Unit, + ) + fun display(scope: DrawScope) - fun dispose() + fun dispose(userDisposeHandler: () -> Unit) } -fun interface CanvasInterfaceFactory { +fun interface CanvasDriverFactory { fun create(window: Window): T } diff --git a/src/main/java/dev/silenium/compose/gl/direct/D3DCanvasInterface.kt b/src/main/java/dev/silenium/compose/gl/canvas/D3DCanvasDriver.kt similarity index 89% rename from src/main/java/dev/silenium/compose/gl/direct/D3DCanvasInterface.kt rename to src/main/java/dev/silenium/compose/gl/canvas/D3DCanvasDriver.kt index 33ce653..0743186 100644 --- a/src/main/java/dev/silenium/compose/gl/direct/D3DCanvasInterface.kt +++ b/src/main/java/dev/silenium/compose/gl/canvas/D3DCanvasDriver.kt @@ -1,4 +1,4 @@ -package dev.silenium.compose.gl.direct +package dev.silenium.compose.gl.canvas import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -27,7 +27,7 @@ import org.lwjgl.system.MemoryUtil import org.slf4j.LoggerFactory import java.awt.Window -class D3DCanvasInterface(private val window: Window) : CanvasInterface { +class D3DCanvasDriver(private val window: Window) : CanvasDriver { private var d3dDirectContext: DirectContext? by mutableStateOf(null) var d3dTexture: Long? = null var sharedHandle: Long? = null @@ -61,12 +61,21 @@ class D3DCanvasInterface(private val window: Window) : CanvasInterface { override fun render( scope: DrawScope, + userResizeHandler: GLDrawScope.(old: IntSize?, new: IntSize) -> Unit, block: GLDrawScope.() -> Unit ) { val d3dCtx = d3dDirectContext ?: return ensureInitialized() GLFW.glfwMakeContextCurrent(glfwWindow) + + val oldSize = fbo?.size + val newSize = scope.size.toIntSize() ensureFBO(scope.size.toIntSize(), d3dCtx) + + if (oldSize != newSize) { + GLDrawScope(fbo!!).userResizeHandler(oldSize, newSize) + } + GLDrawScope(fbo!!).block() glFlush() GLFW.glfwMakeContextCurrent(MemoryUtil.NULL) @@ -79,8 +88,11 @@ class D3DCanvasInterface(private val window: Window) : CanvasInterface { scope.drawContext.canvas.nativeCanvas.drawImage(img, 0f, 0f) } - override fun dispose() { + override fun dispose(userDisposeHandler: () -> Unit) { GLFW.glfwMakeContextCurrent(glfwWindow) + + userDisposeHandler() + fbo?.destroy() image?.close() glMemory?.let(EXTMemoryObject::glDeleteMemoryObjectsEXT) @@ -88,7 +100,6 @@ class D3DCanvasInterface(private val window: Window) : CanvasInterface { sharedHandle?.let(D3DInterop::closeSharedHandle) GLFW.glfwMakeContextCurrent(MemoryUtil.NULL) glfwWindow.let(GLFW::glfwDestroyWindow) - println("Disposed") } private fun ensureFBO(size: IntSize, d3dContext: DirectContext) { @@ -150,10 +161,10 @@ class D3DCanvasInterface(private val window: Window) : CanvasInterface { } } - companion object : CanvasInterfaceFactory { - private val log = LoggerFactory.getLogger(D3DCanvasInterface::class.java) + companion object : CanvasDriverFactory { + private val log = LoggerFactory.getLogger(D3DCanvasDriver::class.java) - override fun create(window: Window) = D3DCanvasInterface(window) + override fun create(window: Window) = D3DCanvasDriver(window) init { GLFW.glfwInitHint(GLFW.GLFW_COCOA_MENUBAR, GLFW.GLFW_FALSE) diff --git a/src/main/java/dev/silenium/compose/gl/direct/DefaultCanvasInterfaceFactory.kt b/src/main/java/dev/silenium/compose/gl/canvas/DefaultCanvasDriverFactory.kt similarity index 56% rename from src/main/java/dev/silenium/compose/gl/direct/DefaultCanvasInterfaceFactory.kt rename to src/main/java/dev/silenium/compose/gl/canvas/DefaultCanvasDriverFactory.kt index 7594c94..2815086 100644 --- a/src/main/java/dev/silenium/compose/gl/direct/DefaultCanvasInterfaceFactory.kt +++ b/src/main/java/dev/silenium/compose/gl/canvas/DefaultCanvasDriverFactory.kt @@ -1,18 +1,18 @@ -package dev.silenium.compose.gl.direct +package dev.silenium.compose.gl.canvas import dev.silenium.compose.gl.graphicsApi import org.jetbrains.skiko.GraphicsApi import java.awt.Window -object DefaultCanvasInterfaceFactory : CanvasInterfaceFactory { - override fun create(window: Window): CanvasInterface { +object DefaultCanvasDriverFactory : CanvasDriverFactory { + override fun create(window: Window): CanvasDriver { val factory = apiFactories[window.graphicsApi()] factory ?: throw UnsupportedOperationException("Unsupported graphics api: ${window.graphicsApi()}") return factory.create(window) } private val apiFactories = mapOf( - GraphicsApi.OPENGL to GLCanvasInterface, - GraphicsApi.DIRECT3D to D3DCanvasInterface, + GraphicsApi.OPENGL to GLCanvasDriver, + GraphicsApi.DIRECT3D to D3DCanvasDriver, ) } diff --git a/src/main/java/dev/silenium/compose/gl/direct/GLDirectCanvas.kt b/src/main/java/dev/silenium/compose/gl/canvas/GLCanvas.kt similarity index 74% rename from src/main/java/dev/silenium/compose/gl/direct/GLDirectCanvas.kt rename to src/main/java/dev/silenium/compose/gl/canvas/GLCanvas.kt index 53a0f94..d818537 100644 --- a/src/main/java/dev/silenium/compose/gl/direct/GLDirectCanvas.kt +++ b/src/main/java/dev/silenium/compose/gl/canvas/GLCanvas.kt @@ -1,4 +1,4 @@ -package dev.silenium.compose.gl.direct +package dev.silenium.compose.gl.canvas import androidx.compose.foundation.Canvas import androidx.compose.runtime.Composable @@ -6,6 +6,7 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.IntSize import dev.silenium.compose.gl.LocalWindow import dev.silenium.compose.gl.directContext import kotlinx.coroutines.Dispatchers @@ -14,9 +15,11 @@ import kotlinx.coroutines.withContext @Composable fun GLCanvas( - wrapperFactory: CanvasInterfaceFactory<*> = DefaultCanvasInterfaceFactory, + wrapperFactory: CanvasDriverFactory<*> = DefaultCanvasDriverFactory, modifier: Modifier = Modifier, - block: GLDrawScope.() -> Unit + onDispose: () -> Unit = {}, + onResize: GLDrawScope.(old: IntSize?, new: IntSize) -> Unit = { _, _ -> }, + block: GLDrawScope.() -> Unit, ) { val window = LocalWindow.current ?: throw IllegalStateException("No window") val wrapper = remember { wrapperFactory.create(window) } @@ -32,11 +35,11 @@ fun GLCanvas( } DisposableEffect(window) { onDispose { - wrapper.dispose() + wrapper.dispose(onDispose) } } Canvas(modifier) { - wrapper.render(this) { + wrapper.render(this, onResize) { drawGL { block() } } wrapper.display(this) diff --git a/src/main/java/dev/silenium/compose/gl/direct/GLCanvasInterface.kt b/src/main/java/dev/silenium/compose/gl/canvas/GLCanvasDriver.kt similarity index 73% rename from src/main/java/dev/silenium/compose/gl/direct/GLCanvasInterface.kt rename to src/main/java/dev/silenium/compose/gl/canvas/GLCanvasDriver.kt index 64f7f58..f07c16d 100644 --- a/src/main/java/dev/silenium/compose/gl/direct/GLCanvasInterface.kt +++ b/src/main/java/dev/silenium/compose/gl/canvas/GLCanvasDriver.kt @@ -1,11 +1,11 @@ -package dev.silenium.compose.gl.direct +package dev.silenium.compose.gl.canvas import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.toIntSize import dev.silenium.compose.gl.fbo.FBO import dev.silenium.compose.gl.objects.Renderbuffer @@ -19,14 +19,14 @@ import org.lwjgl.opengl.GLCapabilities import org.slf4j.LoggerFactory import java.awt.Window -class GLCanvasInterface : CanvasInterface { +class GLCanvasDriver : CanvasDriver { var fbo: FBO? = null var image: Image? = null var texture: BackendTexture? = null var initialized = false lateinit var glCapabilities: GLCapabilities var directContext: DirectContext? by mutableStateOf(null) - var size: Size = Size.Zero + var size: IntSize = IntSize.Zero override fun setup(directContext: DirectContext) { this.directContext = directContext @@ -39,26 +39,31 @@ class GLCanvasInterface : CanvasInterface { } } - override fun dispose() { + override fun dispose(userDisposeHandler: () -> Unit) { + userDisposeHandler() texture?.close() fbo?.destroy() } override fun render( scope: DrawScope, + userResizeHandler: GLDrawScope.(old: IntSize?, new: IntSize) -> Unit, block: GLDrawScope.() -> Unit ) { val ctx = directContext ?: return ctx.submit(syncCpu = true) ensureInitialized() - log.trace("Ensuring FBO") - ensureFBO(scope.size, ctx) - log.trace("Drawing to FBO") + + val oldSize = fbo?.size + val newSize = scope.size.toIntSize() + ensureFBO(newSize, ctx) + if (oldSize != newSize) { + GLDrawScope(fbo!!).userResizeHandler(oldSize, newSize) + } + GLDrawScope(fbo!!).block() - log.trace("Flushing") glFlush() ctx.resetGLAll() - log.trace("Rendering done") } override fun display(scope: DrawScope) { @@ -68,7 +73,7 @@ class GLCanvasInterface : CanvasInterface { scope.drawContext.canvas.nativeCanvas.drawImage(img, 0f, 0f) } - private fun ensureFBO(size: Size, ctx: DirectContext) { + private fun ensureFBO(size: IntSize, ctx: DirectContext) { if (size != this.size) { texture?.close() fbo?.destroy() @@ -92,18 +97,18 @@ class GLCanvasInterface : CanvasInterface { } } - private fun createFBO(size: Size): FBO { + private fun createFBO(size: IntSize): FBO { val colorAttachment = Texture.create( - target = GL11.GL_TEXTURE_2D, size = size.toIntSize(), internalFormat = GL11.GL_RGBA8, + target = GL11.GL_TEXTURE_2D, size = size, internalFormat = GL11.GL_RGBA8, wrapS = GL33.GL_CLAMP_TO_EDGE, wrapT = GL33.GL_CLAMP_TO_EDGE, minFilter = GL33.GL_NEAREST, magFilter = GL33.GL_NEAREST, ) - val depthStencil = Renderbuffer.create(size.toIntSize(), GL33.GL_DEPTH24_STENCIL8) + val depthStencil = Renderbuffer.create(size, GL33.GL_DEPTH24_STENCIL8) return FBO.create(colorAttachment, depthStencil) } - companion object : CanvasInterfaceFactory { - private val log = LoggerFactory.getLogger(GLCanvasInterface::class.java) - override fun create(window: Window) = GLCanvasInterface() + companion object : CanvasDriverFactory { + private val log = LoggerFactory.getLogger(GLCanvasDriver::class.java) + override fun create(window: Window) = GLCanvasDriver() } } diff --git a/src/main/java/dev/silenium/compose/gl/context/EGLContext.kt b/src/main/java/dev/silenium/compose/gl/context/EGLContext.kt deleted file mode 100644 index e9f800e..0000000 --- a/src/main/java/dev/silenium/compose/gl/context/EGLContext.kt +++ /dev/null @@ -1,148 +0,0 @@ -package dev.silenium.compose.gl.context - -import dev.silenium.libs.jni.NativeLoader -import org.lwjgl.egl.EGL -import org.lwjgl.egl.EGL15.* -import org.lwjgl.egl.EGLCapabilities -import org.lwjgl.opengl.GL -import org.lwjgl.opengl.GLCapabilities -import org.lwjgl.system.MemoryUtil -import java.util.concurrent.ConcurrentHashMap - -data class EGLContext( - val display: Long, - val context: Long, - val surface: Long, -) : GLContext { - @Transient - override val provider = Companion - - @Transient - val eglCapabilities: EGLCapabilities - - @Transient - override val glCapabilities: GLCapabilities - - init { - val (eglCap, glCap) = contextCapabilities.compute(this) { key, value -> - value?.let { - it.copy(refCount = it.refCount + 1) - } ?: restorePrevious { - key.makeCurrent() - ContextCapabilities(EGL.createDisplayCapabilities(key.display), GL.createCapabilities(), 1) - } - }!! - eglCapabilities = eglCap - glCapabilities = glCap - } - - override fun makeCurrent() { - check(eglMakeCurrent(display, surface, surface, context)) { "Failed to make context current" } - } - - override fun unbindCurrent() { - check(eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) { "Failed to unbind context" } - } - - override fun destroy() { - contextCapabilities.compute(this) { _, value -> - if (value == null) { - eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT) - eglDestroyContext(display, context) - eglDestroySurface(display, surface) - return@compute null - } - val refCount = value.refCount - 1 - if (refCount == 0) { - eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT) - eglDestroyContext(display, context) - eglDestroySurface(display, surface) - return@compute null - } - value.copy(refCount = refCount) - } - } - - override fun deriveOffscreenContext() = provider.createOffscreen(this) - - companion object : GLContextProvider { - private data class ContextCapabilities( - val egl: EGLCapabilities, - val gl: GLCapabilities, - val refCount: Int, - ) - - private val contextCapabilities = ConcurrentHashMap() - - init { - NativeLoader.loadLibraryFromClasspath("compose-gl").getOrThrow() - } - - override fun restorePrevious(block: () -> R): R { - val display = eglGetCurrentDisplay() - val context = eglGetCurrentContext() - val surface = eglGetCurrentSurface(EGL_DRAW) - if (display == EGL_NO_DISPLAY || context == EGL_NO_CONTEXT || surface == EGL_NO_SURFACE) { - return block() - } - return block().also { - eglMakeCurrent(display, surface, surface, context) - } - } - - override fun fromCurrent(): EGLContext? { - val display = eglGetCurrentDisplay() - val context = eglGetCurrentContext() - val surface = eglGetCurrentSurface(EGL_DRAW) - if (display == EGL_NO_DISPLAY || context == EGL_NO_CONTEXT || surface == EGL_NO_SURFACE) { - return null - } - return EGLContext(display, context, surface) - } - - override fun isCurrent(): Boolean { - val display = eglGetCurrentDisplay() - val context = eglGetCurrentContext() - val surface = eglGetCurrentSurface(EGL_DRAW) - return display != EGL_NO_DISPLAY && context != EGL_NO_CONTEXT && surface != EGL_NO_SURFACE - } - - override fun createOffscreen(parent: EGLContext): EGLContext { - val display = parent.display - check(eglBindAPI(EGL_OPENGL_API)) { "Failed to bind API: 0x${eglGetError().toString(16).uppercase()}" } - - val attribList = intArrayOf( - EGL_RED_SIZE, - 8, - EGL_GREEN_SIZE, - 8, - EGL_BLUE_SIZE, - 8, - EGL_ALPHA_SIZE, - 8, - EGL_RENDERABLE_TYPE, - EGL_OPENGL_BIT, - EGL_NONE - ) - val config = MemoryUtil.memAllocPointer(1) - val numConfig = IntArray(1) - if (!eglChooseConfig(display, attribList, config, numConfig)) { - error("Failed to choose config: 0x${eglGetError().toString(16).uppercase()}") - } - val context = eglCreateContext( - display, - config.get(0), - parent.context, - intArrayOf( - EGL_CONTEXT_MAJOR_VERSION, 3, - EGL_NONE - ) - ) - check(context != EGL_NO_CONTEXT) { "Failed to create context: 0x${eglGetError().toString(16).uppercase()}" } - val surface = - eglCreatePbufferSurface(display, config.get(0), intArrayOf(EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE)) - check(surface != EGL_NO_SURFACE) { "Failed to create surface: 0x${eglGetError().toString(16).uppercase()}" } - return EGLContext(display, context, surface) - } - } -} diff --git a/src/main/java/dev/silenium/compose/gl/context/GLContext.kt b/src/main/java/dev/silenium/compose/gl/context/GLContext.kt deleted file mode 100644 index a9b61ed..0000000 --- a/src/main/java/dev/silenium/compose/gl/context/GLContext.kt +++ /dev/null @@ -1,79 +0,0 @@ -package dev.silenium.compose.gl.context - -import org.lwjgl.opengl.GLCapabilities - -/** - * Represents an OpenGL context. - * @param C The type of the context. - */ -interface GLContext> { - /** - * The [GLContextProvider] of the context. - */ - val provider: GLContextProvider - - /** - * The [GLCapabilities] of the context. - */ - val glCapabilities: GLCapabilities - - /** - * Makes the context current. - * @throws IllegalStateException If the context cannot be made current. - * @see unbindCurrent - */ - fun makeCurrent() - - /** - * Unbinds the current context. - * @throws IllegalStateException If the context cannot be unbound. - * @see makeCurrent - */ - fun unbindCurrent() - - /** - * Destroys the context. - * @throws IllegalStateException If the context is currently bound in another thread. - * @see makeCurrent - * @see unbindCurrent - */ - fun destroy() - - /** - * Derives an offscreen context from the current context. - * @return The offscreen [GLContext]. - */ - fun deriveOffscreenContext(): C -} - -/** - * Provides a way to create and manage OpenGL contexts. - */ -interface GLContextProvider> { - /** - * Captures the current context, calls the [block] and restores the captured context afterward. - * This is useful when you need to temporarily switch to another context. - * @param block The block to call. - * @return The result of the block. - */ - fun restorePrevious(block: () -> R): R - - /** - * Creates a new [GLContext] instance from the current context. - * @return The new [GLContext] instance. - */ - fun fromCurrent(): C? - - /** - * Checks if there is a context current. - * @return `true` if there is a context current, `false` otherwise. - */ - fun isCurrent(): Boolean - - /** - * Creates an offscreen context for the given [parent] context. - * @param parent The parent [GLContext]. - * @return The offscreen [GLContext]. - */ - fun createOffscreen(parent: C): C -} diff --git a/src/main/java/dev/silenium/compose/gl/context/GLContextProviderFactory.kt b/src/main/java/dev/silenium/compose/gl/context/GLContextProviderFactory.kt deleted file mode 100644 index 2fe2f09..0000000 --- a/src/main/java/dev/silenium/compose/gl/context/GLContextProviderFactory.kt +++ /dev/null @@ -1,67 +0,0 @@ -package dev.silenium.compose.gl.context - -import dev.silenium.libs.jni.NativePlatform -import dev.silenium.libs.jni.Platform -import org.lwjgl.system.Configuration -import org.slf4j.LoggerFactory - -object GLContextProviderFactory { - init { - Configuration.OPENGL_CONTEXT_API.set("native") - Configuration.OPENGLES_CONTEXT_API.set("native") - } - - private val log = LoggerFactory.getLogger(GLContextProviderFactory::class.java) - - private val override by lazy { - System.getProperty("compose.gl.context.provider") - ?.takeIf { it.isNotBlank() } - ?.let { enumValueOf(it) } - } - - /** - * The detected GL context provider. - * @see detect - */ - val detected: GLContextProvider<*> by lazy(::detect) - - private enum class GLContextProviderType(val provider: GLContextProvider<*>) { - EGL(EGLContext), - GLX(GLXContext), - WGL(WGLContext), - } - - private val osOrder = mapOf( - Platform.OS.LINUX to listOf(GLContextProviderType.EGL, GLContextProviderType.GLX), - Platform.OS.WINDOWS to listOf(GLContextProviderType.WGL), - ) - - /** - * Detects the GL context provider. - * - * The provider is detected based on the current platform. - * The detection order is defined by the [osOrder] map. - * - * @return The detected GL context provider. - */ - fun detect(): GLContextProvider<*> { - override?.let { - log.info("Using overridden GL context provider: {}", it) - return it.provider - } - - val platform = NativePlatform.platform() - log.debug("Detecting GL context provider for platform: {}", platform) - val order = osOrder[platform.os] - ?: throw UnsupportedOperationException("Unsupported platform: ${NativePlatform.os}") - for (type in order) { - log.debug("Trying {} context", type) - if (type.provider.fromCurrent() != null) { - log.info("Using {} context", type) - return type.provider - } - } - return osOrder[platform.os]?.firstOrNull()?.provider - ?: throw UnsupportedOperationException("No GL context provider found") - } -} diff --git a/src/main/java/dev/silenium/compose/gl/context/GLXContext.kt b/src/main/java/dev/silenium/compose/gl/context/GLXContext.kt deleted file mode 100644 index a5dfc32..0000000 --- a/src/main/java/dev/silenium/compose/gl/context/GLXContext.kt +++ /dev/null @@ -1,126 +0,0 @@ -package dev.silenium.compose.gl.context - -import dev.silenium.libs.jni.NativeLoader -import org.lwjgl.egl.EGL15.* -import org.lwjgl.opengl.GL -import org.lwjgl.opengl.GLCapabilities -import org.lwjgl.opengl.GLX -import org.lwjgl.opengl.GLXCapabilities -import java.util.concurrent.ConcurrentHashMap - -data class GLXContext( - val display: Long, - val drawable: Long, - val context: Long, - val xDrawable: Long? = null, -) : GLContext { - override val provider = Companion - - @Transient - val glxCapabilities: GLXCapabilities - - @Transient - override val glCapabilities: GLCapabilities - - init { - val (glxCap, glCap) = contextCapabilities.compute(this) { key, value -> - value?.let { - it.copy(refCount = it.refCount + 1) - } ?: restorePrevious { - key.makeCurrent() - ContextCapabilities(GL.createCapabilitiesGLX(key.display), GL.createCapabilities(), 1) - } - }!! - glxCapabilities = glxCap - glCapabilities = glCap - } - - override fun makeCurrent() { - check(GLX.glXMakeCurrent(display, drawable, context)) { - "Failed to make context current" - } - } - - override fun unbindCurrent() { - check(GLX.glXMakeCurrent(display, 0L, 0L)) { - "Failed to unbind context" - } - } - - override fun destroy() { - contextCapabilities.compute(this) { key, value -> - if (value == null) { - unbindCurrent() - GLX.glXDestroyContext(key.display, key.context) - destroyPixmapN(key.display, key.xDrawable ?: 0L, key.drawable) - return@compute null - } - val refCount = value.refCount - 1 - if (refCount == 0) { - unbindCurrent() - GLX.glXDestroyContext(key.display, key.context) - destroyPixmapN(key.display, key.xDrawable ?: 0L, key.drawable) - return@compute null - } - value.copy(refCount = refCount) - } - } - - override fun deriveOffscreenContext() = provider.createOffscreen(this) - - companion object : GLContextProvider { - private data class ContextCapabilities( - val glx: GLXCapabilities, - val gl: GLCapabilities, - val refCount: Int, - ) - - private val contextCapabilities = ConcurrentHashMap() - - init { - NativeLoader.loadLibraryFromClasspath("compose-gl").getOrThrow() - } - - override fun restorePrevious(block: () -> R): R { - val display = getCurrentDisplayN() - val context = getCurrentContextN() - val drawable = getCurrentDrawableN() - if (display == EGL_NO_DISPLAY || context == EGL_NO_CONTEXT || drawable == EGL_NO_SURFACE) { - return block() - } - return block().also { - GLX.glXMakeCurrent(display, drawable, context) - } - } - - override fun fromCurrent(): GLXContext? { - val display = getCurrentDisplayN() - val context = getCurrentContextN() - val drawable = getCurrentDrawableN() - if (display == 0L || context == 0L || drawable == 0L) { - return null - } - return GLXContext(display, drawable, context) - } - - override fun isCurrent(): Boolean { - val display = getCurrentDisplayN() - val context = getCurrentContextN() - val drawable = getCurrentDrawableN() - return display != 0L && context != 0L && drawable != 0L - } - - override fun createOffscreen(parent: GLXContext): GLXContext { - val display = parent.display - val share = parent.context - val (xPixmap, drawable, context) = createContextN(display, share) - return GLXContext(display, drawable, context, xPixmap) - } - } -} - -private external fun getCurrentContextN(): Long -private external fun getCurrentDisplayN(): Long -private external fun getCurrentDrawableN(): Long -private external fun createContextN(display: Long, share: Long): LongArray -private external fun destroyPixmapN(display: Long, xPixmap: Long, pixmap: Long) diff --git a/src/main/java/dev/silenium/compose/gl/context/WGLContext.kt b/src/main/java/dev/silenium/compose/gl/context/WGLContext.kt deleted file mode 100644 index 162eae9..0000000 --- a/src/main/java/dev/silenium/compose/gl/context/WGLContext.kt +++ /dev/null @@ -1,111 +0,0 @@ -package dev.silenium.compose.gl.context - -import dev.silenium.libs.jni.NativeLoader -import org.lwjgl.opengl.GL -import org.lwjgl.opengl.GLCapabilities -import org.lwjgl.opengl.WGL -import org.lwjgl.opengl.WGLCapabilities -import java.util.concurrent.ConcurrentHashMap - -data class WGLContext( - val deviceContext: Long, - val renderingContext: Long, -) : GLContext { - @Transient - override val provider: GLContextProvider = Companion - - @Transient - lateinit var wglCapabilities: WGLCapabilities - - @Transient - override lateinit var glCapabilities: GLCapabilities - - override fun unbindCurrent() { - WGL.wglMakeCurrent(null, 0L, 0L) - } - - override fun makeCurrent() { - if (provider.fromCurrent() != this) { - check(WGL.wglMakeCurrent(null, deviceContext, renderingContext)) { - "Failed to make context current" - } - } - val (wglCap, glCap) = contextCapabilities.compute(this) { key, value -> - value?.let { - it.copy(refCount = it.refCount + 1) - } ?: restorePrevious { - ContextCapabilities(GL.createCapabilitiesWGL(), GL.createCapabilities(), 1) - } - }!! - wglCapabilities = wglCap - glCapabilities = glCap - } - - override fun destroy() { - contextCapabilities.compute(this) { key, value -> - if (value == null) { - WGL.wglMakeCurrent(null, 0L, 0L) - WGL.wglDeleteContext(null, key.renderingContext) - return@compute null - } - val refCount = value.refCount - 1 - if (refCount == 0) { - WGL.wglMakeCurrent(null, 0L, 0L) - WGL.wglDeleteContext(null, key.renderingContext) - return@compute null - } - value.copy(refCount = refCount) - } - } - - override fun deriveOffscreenContext() = provider.createOffscreen(this) - - companion object : GLContextProvider { - private data class ContextCapabilities( - val egl: WGLCapabilities, - val gl: GLCapabilities, - val refCount: Int, - ) - - private val contextCapabilities = ConcurrentHashMap() - - init { - NativeLoader.loadLibraryFromClasspath("compose-gl").getOrThrow() - } - - override fun restorePrevious(block: () -> R): R { - val displayContext = WGL.wglGetCurrentDC() - val renderingContext = WGL.wglGetCurrentContext(null) - return block().also { - WGL.wglMakeCurrent(null, displayContext, renderingContext) - } - } - - override fun fromCurrent(): WGLContext? { - val deviceContext = WGL.wglGetCurrentDC() - val renderingContext = WGL.wglGetCurrentContext(null) - return if (deviceContext != 0L && renderingContext != 0L) { - WGLContext(deviceContext, renderingContext) - } else { - null - } - } - - override fun isCurrent(): Boolean { - val deviceContext = WGL.wglGetCurrentDC() - val renderingContext = WGL.wglGetCurrentContext(null) - return deviceContext != 0L && renderingContext != 0L - } - - override fun createOffscreen(parent: WGLContext): WGLContext { - val deviceContext = parent.deviceContext - val renderingContext = WGL.wglCreateContext(null, deviceContext) - check(WGL.wglShareLists(null, parent.renderingContext, renderingContext)) { - "Failed to share context lists" - } - return WGLContext(deviceContext, renderingContext) - } - } -} - -private external fun wglCreateContext(deviceContext: Long): Long diff --git a/src/main/java/dev/silenium/compose/gl/fbo/FBOFifoSwapChain.kt b/src/main/java/dev/silenium/compose/gl/fbo/FBOFifoSwapChain.kt deleted file mode 100644 index 7a5733e..0000000 --- a/src/main/java/dev/silenium/compose/gl/fbo/FBOFifoSwapChain.kt +++ /dev/null @@ -1,73 +0,0 @@ -package dev.silenium.compose.gl.fbo - -import androidx.compose.ui.unit.IntSize -import java.util.concurrent.ConcurrentLinkedQueue -import java.util.concurrent.atomic.AtomicReference - -class FBOFifoSwapChain(private val capacity: Int, override val fboCreator: (IntSize) -> FBO) : FBOSwapChain() { - override var size: IntSize = IntSize.Zero - private set - private var fbos = AtomicReference?>(null) - private var current: Int = -1 - private val displayQueue = ConcurrentLinkedQueue() - private val renderQueue = ConcurrentLinkedQueue() - - override fun display(block: (FBO) -> R): R? { - val next = displayQueue.poll() - if (next != null && next != -1) { - renderQueue.add(current) - current = next - } - if (current == -1) return null - val fbo = fbos.get()?.get(current) ?: return null - val result = block(fbo) - return result - } - - override fun render(block: (FBO) -> R): R? { - val next = renderQueue.poll() ?: return null - val nextFbos = fbos.updateAndGet { - if (it?.get(next)?.size != size) { - it?.get(next)?.destroy() - it?.set(next, fboCreator(size)) - } - it - } - try { - val nextFbo = nextFbos?.get(next) ?: return null - val result = block(nextFbo) - current = next - return result - } catch (e: Throwable) { - throw e - } - } - - override fun resize(size: IntSize) { - this.size = size - renderQueue.clear() - displayQueue.clear() - fbos.get()?.forEachIndexed { index, fbo -> - if (fbo.size != size && index != current) { - fbo.destroy() - } - } - val currentFbo = fbos.get()?.getOrNull(current) - fbos.set(Array(capacity) { - if (it == current) currentFbo ?: fboCreator(size) - else fboCreator(size) - }.also { - renderQueue.addAll(it.indices - current) - }) - } - - override fun destroyFBOs() { - renderQueue.clear() - displayQueue.clear() - current = -1 - fbos.updateAndGet { - it?.forEach(FBO::destroy) - null - } - } -} diff --git a/src/main/java/dev/silenium/compose/gl/fbo/FBOMailboxSwapChain.kt b/src/main/java/dev/silenium/compose/gl/fbo/FBOMailboxSwapChain.kt deleted file mode 100644 index 05e7da2..0000000 --- a/src/main/java/dev/silenium/compose/gl/fbo/FBOMailboxSwapChain.kt +++ /dev/null @@ -1,59 +0,0 @@ -package dev.silenium.compose.gl.fbo - -import androidx.compose.ui.unit.IntSize -import java.util.concurrent.atomic.AtomicReference - -class FBOMailboxSwapChain(private val capacity: Int, override val fboCreator: (IntSize) -> FBO) : FBOSwapChain() { - override var size: IntSize = IntSize.Zero - private set - private var fbos = AtomicReference?>(null) - private var current: Int = -1 - - override fun display(block: (FBO) -> R): R? { - if (current == -1) return null - val fbo = fbos.get()?.get(current) ?: return null - val result = block(fbo) - return result - } - - override fun render(block: (FBO) -> R): R? { - val next = (current + 1) % capacity - val nextFbos = fbos.updateAndGet { - if (it?.get(next)?.size != size) { - it?.get(next)?.destroy() - it?.set(next, fboCreator(size)) - } - it - } - try { - val nextFbo = nextFbos?.get(next) ?: return null - val result = block(nextFbo) - current = next - return result - } catch (e: Throwable) { - throw e - } - } - - override fun resize(size: IntSize) { - this.size = size - fbos.get()?.forEachIndexed { index, fbo -> - if (fbo.size != size && index != current) { - fbo.destroy() - } - } - val currentFbo = fbos.get()?.getOrNull(current) - fbos.set(Array(capacity) { - if (it == current) currentFbo ?: fboCreator(size) - else fboCreator(size) - }) - } - - override fun destroyFBOs() { - current = -1 - fbos.updateAndGet { - it?.forEach(FBO::destroy) - null - } - } -} diff --git a/src/main/java/dev/silenium/compose/gl/fbo/FBOPool.kt b/src/main/java/dev/silenium/compose/gl/fbo/FBOPool.kt deleted file mode 100644 index 3d2b171..0000000 --- a/src/main/java/dev/silenium/compose/gl/fbo/FBOPool.kt +++ /dev/null @@ -1,170 +0,0 @@ -package dev.silenium.compose.gl.fbo - -import androidx.compose.ui.unit.IntSize -import dev.silenium.compose.gl.context.GLContext -import dev.silenium.compose.gl.objects.Renderbuffer -import dev.silenium.compose.gl.objects.Texture -import dev.silenium.compose.gl.surface.GLDisplayScope -import dev.silenium.compose.gl.surface.GLDisplayScopeImpl -import dev.silenium.compose.gl.surface.GLDrawScope -import dev.silenium.compose.gl.surface.GLDrawScopeImpl -import kotlinx.coroutines.CancellationException -import org.lwjgl.opengl.GL30.* -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract -import kotlin.time.Duration - -/** - * A pool of framebuffers. - * @param render The OpenGL context for rendering. - * @param display The OpenGL context for displaying. - * @param size The initial size of the framebuffers. - * @param swapChain The swap chain for the framebuffers. - */ -class FBOPool( - private val render: GLContext<*>, - private val display: GLContext<*>, - var size: IntSize, - private val swapChain: FBOSwapChain, -) { - - /** - * A pool of framebuffers. - * @param render The OpenGL context for rendering. - * @param display The OpenGL context for displaying. - * @param size The initial size of the framebuffers. - * @param swapChainFactory The factory for creating the swap chain. - * @param swapChainSize The size of the swap chain. - */ - constructor( - render: GLContext<*>, - display: GLContext<*>, - size: IntSize, - swapChainFactory: (Int, (IntSize) -> FBO) -> FBOSwapChain, - swapChainSize: Int = 10, - ) : this(render, display, size, swapChainFactory(swapChainSize, ::createFBO)) - - private enum class ContextType { - RENDER, - DISPLAY, - } - - @OptIn(ExperimentalContracts::class) - private inline fun ensureContext(contextType: ContextType, block: () -> R): R { - contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } - val previousContext = render.provider.fromCurrent() - val nextContext = when (contextType) { - ContextType.RENDER -> render - ContextType.DISPLAY -> display - } - - nextContext.makeCurrent() - try { - return block() - } finally { - previousContext?.makeCurrent() - } - } - - /** - * Initialize the framebuffers, has to be called once in the render context before it can be used. - */ - fun initialize() = ensureContext(ContextType.RENDER) { - swapChain.resize(size) - } - - /** - * Render the next frame, has to be called in the render context. - * @param deltaTime The time since the last frame. - * @param block The block to render the frame. - * @return wait time for the next frame, or null, if there was no frame rendered due to no framebuffers being available. - */ - fun render(deltaTime: Duration, block: GLDrawScope.() -> Unit): Result = try { - ensureContext(ContextType.RENDER) { - if (swapChain.size != size) { - swapChain.resize(size) - } - restoreAfter { - swapChain.render { fbo -> - fbo.bind() - val drawScope = GLDrawScopeImpl(fbo, deltaTime) - drawScope.block() - glFlush() - fbo.unbind() - - if (drawScope.terminate) { - Result.failure(CancellationException("Rendering terminated")) - } else { - Result.success(drawScope.redrawAfter) - } - } - } ?: Result.failure(NoRenderFBOAvailable()) - } - } catch (t: Throwable) { - Result.failure(t) - } - - /** - * Display the next frame, has to be called in the display context. - * @param block The block to display the frame. - */ - fun display(block: GLDisplayScope.() -> Unit) = swapChain.display { fbo -> - ensureContext(ContextType.DISPLAY) { - val displayScope = GLDisplayScopeImpl(fbo) - displayScope.block() - } - } - - /** - * Destroy the framebuffers, has to be called once in the render context after it is no longer needed. - * This will destroy all framebuffers. - * @see [FBOSwapChain.destroyFBOs] - */ - fun destroy() = ensureContext(ContextType.RENDER) { - swapChain.destroyFBOs() - } - - companion object { - private fun createFBO(size: IntSize) = restoreAfter { - val colorAttachment = Texture.create( - GL_TEXTURE_2D, size, GL_RGBA8, - GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE, GL_LINEAR, GL_LINEAR, - ) - val depthStencilAttachment = Renderbuffer.create(size, GL_DEPTH24_STENCIL8) - - FBO.create(colorAttachment, depthStencilAttachment) - } - - @OptIn(ExperimentalContracts::class) - private inline fun restoreAfter(block: () -> R): R { - contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } - - val prevFb = glGetInteger(GL_FRAMEBUFFER_BINDING) - val prevReadFb = glGetInteger(GL_READ_FRAMEBUFFER_BINDING) - val prevDrawFb = glGetInteger(GL_DRAW_FRAMEBUFFER_BINDING) - val prevRb = glGetInteger(GL_RENDERBUFFER_BINDING) - val prevTex = glGetInteger(GL_TEXTURE_BINDING_2D) - val prevViewport = IntArray(4) - glGetIntegerv(GL_VIEWPORT, prevViewport) - val prevScissor = IntArray(4) - glGetIntegerv(GL_SCISSOR_BOX, prevScissor) - val prevDepthTest = glIsEnabled(GL_DEPTH_TEST) - val prevStencilTest = glIsEnabled(GL_STENCIL_TEST) - - try { - return block() - } finally { - glBindFramebuffer(GL_FRAMEBUFFER, prevFb) - glBindFramebuffer(GL_READ_FRAMEBUFFER, prevReadFb) - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, prevDrawFb) - glBindRenderbuffer(GL_RENDERBUFFER, prevRb) - glBindTexture(GL_TEXTURE_2D, prevTex) - glViewport(prevViewport[0], prevViewport[1], prevViewport[2], prevViewport[3]) - glScissor(prevScissor[0], prevScissor[1], prevScissor[2], prevScissor[3]) - if (prevDepthTest) glEnable(GL_DEPTH_TEST) else glDisable(GL_DEPTH_TEST) - if (prevStencilTest) glEnable(GL_STENCIL_TEST) else glDisable(GL_STENCIL_TEST) - } - } - } -} diff --git a/src/main/java/dev/silenium/compose/gl/fbo/FBOSwapChain.kt b/src/main/java/dev/silenium/compose/gl/fbo/FBOSwapChain.kt deleted file mode 100644 index 7bc1422..0000000 --- a/src/main/java/dev/silenium/compose/gl/fbo/FBOSwapChain.kt +++ /dev/null @@ -1,38 +0,0 @@ -package dev.silenium.compose.gl.fbo - -import androidx.compose.ui.unit.IntSize -import java.util.concurrent.ArrayBlockingQueue - -abstract class FBOSwapChain { - /** - * Current size of the swap chain - */ - abstract val size: IntSize - - /** - * The function that creates the FBOs - */ - protected abstract val fboCreator: (IntSize) -> FBO - - /** - * Display the current FBO - * @param block The block to run with the current FBO for display - * @return The result of the block - */ - abstract fun display(block: (FBO) -> R): R? - - /** - * Render a frame - * @param block The block to run with the current FBO for rendering - * @return The result of the block - */ - abstract fun render(block: (FBO) -> R): R? - abstract fun resize(size: IntSize) - abstract fun destroyFBOs() -} - -fun ArrayBlockingQueue.fillRenderQueue(fboCreator: (IntSize) -> FBO, size: IntSize) { - while (remainingCapacity() > 0) { - offer(fboCreator(size)) - } -} diff --git a/src/main/java/dev/silenium/compose/gl/fbo/NoRenderFBOAvailable.kt b/src/main/java/dev/silenium/compose/gl/fbo/NoRenderFBOAvailable.kt deleted file mode 100644 index 9899cd7..0000000 --- a/src/main/java/dev/silenium/compose/gl/fbo/NoRenderFBOAvailable.kt +++ /dev/null @@ -1,5 +0,0 @@ -package dev.silenium.compose.gl.fbo - -class NoRenderFBOAvailable(message: String) : Exception(message) { - constructor() : this("No render FBO available.") -} diff --git a/src/main/java/dev/silenium/compose/gl/surface/GLDisplayScope.kt b/src/main/java/dev/silenium/compose/gl/surface/GLDisplayScope.kt deleted file mode 100644 index 2ef0fe2..0000000 --- a/src/main/java/dev/silenium/compose/gl/surface/GLDisplayScope.kt +++ /dev/null @@ -1,11 +0,0 @@ -package dev.silenium.compose.gl.surface - -import dev.silenium.compose.gl.fbo.FBO - -interface GLDisplayScope { - val fbo: FBO -} - -internal class GLDisplayScopeImpl( - override val fbo: FBO -) : GLDisplayScope diff --git a/src/main/java/dev/silenium/compose/gl/surface/GLDrawScope.kt b/src/main/java/dev/silenium/compose/gl/surface/GLDrawScope.kt deleted file mode 100644 index fc329fb..0000000 --- a/src/main/java/dev/silenium/compose/gl/surface/GLDrawScope.kt +++ /dev/null @@ -1,46 +0,0 @@ -package dev.silenium.compose.gl.surface - -import dev.silenium.compose.gl.fbo.FBO -import kotlin.time.Duration -import kotlin.time.Duration.Companion.milliseconds - -interface GLDrawScope { - /** - * Current FBO. - */ - val fbo: FBO - - /** - * Time since the last frame. - */ - val deltaTime: Duration - - /** - * Redraw after the given duration. - * @param duration The duration to redraw after. If null, a call to [GLSurfaceState.requestUpdate] is required to redraw. - */ - fun redrawAfter(duration: Duration?) - - /** - * Terminate the rendering thread. - */ - fun terminate() -} - -internal class GLDrawScopeImpl( - override val fbo: FBO, - override val deltaTime: Duration, -) : GLDrawScope { - internal var redrawAfter: Duration? = (1000 / 60).milliseconds - private set - internal var terminate = false - private set - - override fun terminate() { - terminate = true - } - - override fun redrawAfter(duration: Duration?) { - redrawAfter = duration - } -} diff --git a/src/main/java/dev/silenium/compose/gl/surface/GLSurface.kt b/src/main/java/dev/silenium/compose/gl/surface/GLSurface.kt deleted file mode 100644 index a6f5340..0000000 --- a/src/main/java/dev/silenium/compose/gl/surface/GLSurface.kt +++ /dev/null @@ -1,348 +0,0 @@ -package dev.silenium.compose.gl.surface - -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.TransformOrigin -import androidx.compose.ui.graphics.drawscope.scale -import androidx.compose.ui.graphics.drawscope.translate -import androidx.compose.ui.graphics.nativeCanvas -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.unit.IntSize -import dev.silenium.compose.gl.LocalWindow -import dev.silenium.compose.gl.context.GLContext -import dev.silenium.compose.gl.context.GLContextProvider -import dev.silenium.compose.gl.context.GLContextProviderFactory -import dev.silenium.compose.gl.directContext -import dev.silenium.compose.gl.fbo.* -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.isActive -import kotlinx.coroutines.withContext -import org.jetbrains.skia.* -import org.lwjgl.opengl.GL -import org.lwjgl.opengl.GL30.GL_RGBA8 -import org.slf4j.LoggerFactory -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicLong -import kotlin.time.Duration.Companion.nanoseconds -import kotlin.time.toJavaDuration - -/** - * Override the size of the FBO. - * @param width The width of the FBO. - * @param height The height of the FBO. - * @param transformOrigin The transform origin for scaling the FBO to the size of the GLSurfaceView (default: [TransformOrigin.Center]). - */ -data class FBOSizeOverride( - val width: Int, - val height: Int, - val transformOrigin: TransformOrigin = TransformOrigin.Center, -) { - val size get() = IntSize(width, height) -} - -/** - * A composable that displays OpenGL content. - * @param state The state of the [GLSurface]. - * @param modifier The modifier to apply to the [GLSurface]. - * @param paint The paint to draw the contents on the compose scene. - * @param glContextProvider The provider of the OpenGL context (default: [GLContextProviderFactory.detected]). - * @param presentMode The present mode of the GLSurfaceView (default: [GLSurface.PresentMode.FIFO]). - * @param swapChainSize The size of the swap chain (default: 10). - * @param fboSizeOverride The size override of the FBO (default: null). - * @param cleanup The cleanup block to run when the [GLSurface] is destroyed (default: {}). - * @param draw The draw block to render the OpenGL content. - * @see [GLDrawScope] - */ -@Composable -fun GLSurfaceView( - state: GLSurfaceState = rememberGLSurfaceState(), - modifier: Modifier = Modifier, - paint: Paint = Paint(), - glContextProvider: GLContextProvider<*> = GLContextProviderFactory.detected, - presentMode: GLSurface.PresentMode = GLSurface.PresentMode.FIFO, - swapChainSize: Int = 10, - fboSizeOverride: FBOSizeOverride? = null, - cleanup: () -> Unit = {}, - draw: GLDrawScope.() -> Unit, -) { - val surfaceView = rememberGLSurface( - state = state, - glContextProvider = glContextProvider, - presentMode = presentMode, - swapChainSize = swapChainSize, - fboSizeOverride = fboSizeOverride, - cleanup = cleanup, - draw = draw, - ) - GLSurfaceView(surfaceView, modifier, paint) -} - -/** - * A composable that remembers a [GLSurface]. - * @param state The state of the [GLSurface]. - * @param glContextProvider The provider of the OpenGL context (default: [GLContextProviderFactory.detected]). - * @param presentMode The present mode of the GLSurfaceView (default: [GLSurface.PresentMode.FIFO]). - * @param swapChainSize The size of the swap chain (default: 10). - * @param fboSizeOverride The size override of the FBO (default: null). - * @param cleanup The cleanup block to run when the [GLSurface] is destroyed (default: {}). - * @param draw The draw block to render the OpenGL content. - */ -@Composable -fun rememberGLSurface( - state: GLSurfaceState = rememberGLSurfaceState(), - glContextProvider: GLContextProvider<*> = GLContextProviderFactory.detected, - presentMode: GLSurface.PresentMode = GLSurface.PresentMode.FIFO, - swapChainSize: Int = 10, - fboSizeOverride: FBOSizeOverride? = null, - cleanup: () -> Unit = {}, - draw: GLDrawScope.() -> Unit, -): GLSurface { - val surfaceView = remember(state, glContextProvider, presentMode, swapChainSize, cleanup, draw) { - val currentContext = glContextProvider.fromCurrent() ?: error("No current EGL context") - GLSurface( - state = state, - parentContext = currentContext, - presentMode = presentMode, - swapChainSize = swapChainSize, - cleanupBlock = cleanup, - drawBlock = draw, - fboSizeOverride = fboSizeOverride, - ) - } - DisposableEffect(surfaceView) { - surfaceView.launch() - onDispose { - surfaceView.interrupt() - } - } - LaunchedEffect(fboSizeOverride) { - surfaceView.fboSizeOverride = fboSizeOverride - } - - return surfaceView -} - -/** - * A composable that displays OpenGL content. - * @param surface The [GLSurface] to display. - * @param modifier The modifier to apply to the [GLSurface]. - * @param paint The paint to draw the contents on the compose scene. - */ -@Composable -fun GLSurfaceView( - surface: GLSurface, - modifier: Modifier = Modifier, - paint: Paint = Paint(), -) { - val window = LocalWindow.current - var directContext by remember { mutableStateOf(null) } - var componentSize by remember { mutableStateOf(IntSize.Zero) } - LaunchedEffect(window) { - withContext(Dispatchers.IO) { - while (isActive) { - window?.directContext()?.let { - directContext = it - return@withContext - } - } - } - } - BoxWithConstraints(modifier) { - Canvas( - modifier = Modifier - .onSizeChanged { - componentSize = it - if (surface.fboSizeOverride == null) { - surface.resize(it) - } - }.let { - val override = surface.fboSizeOverride - if (override != null) { - it.matchParentSize() - .drawWithContent { - val xScale = size.width / override.width - val yScale = size.height / override.height - val scale = minOf(xScale, yScale) - translate( - (size.width - override.width * scale) * override.transformOrigin.pivotFractionX, - (size.height - override.height * scale) * override.transformOrigin.pivotFractionY, - ) { - scale(scale, Offset.Zero) { - this@drawWithContent.drawContent() - } - } - } - } else { - it.matchParentSize() - } - } - ) { - surface.invalidations.let { - directContext?.let { directContext -> - surface.display(drawContext.canvas.nativeCanvas, directContext, paint) - } - } - } - } - LaunchedEffect(surface.fboSizeOverride) { - val fboSizeOverride = surface.fboSizeOverride - if (fboSizeOverride != null) { - surface.resize(fboSizeOverride.size) - } else { - surface.resize(componentSize) - } - } -} - -class GLSurface internal constructor( - private val state: GLSurfaceState, - private val parentContext: GLContext<*>, - private val drawBlock: GLDrawScope.() -> Unit, - private val cleanupBlock: () -> Unit = {}, - private val presentMode: PresentMode = PresentMode.MAILBOX, - private val swapChainSize: Int = 10, - fboSizeOverride: FBOSizeOverride? = null, -) : Thread("GLSurfaceView-${index.getAndIncrement()}") { - enum class PresentMode(internal val impl: (Int, (IntSize) -> FBO) -> FBOSwapChain) { - /** - * Renders the latest frame and discards all the previous frames. - * Results in the lowest latency. - */ - MAILBOX(::FBOMailboxSwapChain), - - /** - * Renders all the frames in the order they were produced. - * Results in a higher latency, but smoother animations. - * Limits the framerate to the display refresh rate. - */ - FIFO(::FBOFifoSwapChain), - } - - private var directContext: DirectContext? = null - private var renderContext: GLContext<*>? = null - private var size: IntSize = IntSize.Zero - private var fboPool: FBOPool? = null - internal var invalidations by mutableStateOf(0L) - internal var fboSizeOverride: FBOSizeOverride? by mutableStateOf(fboSizeOverride) - - internal fun launch() { - GL.createCapabilities() - start() -// return CoroutineScope(executor.asCoroutineDispatcher()).launch { -// run() -// }.also { -// it.invokeOnCompletion { executor.shutdown() } -// } - } - - internal fun resize(size: IntSize) { - if (size == fboPool?.size) return - this.size = size - fboPool?.size = size - state.requestUpdate() - } - - internal fun display(canvas: Canvas, displayContext: DirectContext, paint: Paint) { - val t1 = System.nanoTime() - fboPool?.display { displayImpl(canvas, displayContext, paint) } - invalidations = t1 - val t2 = System.nanoTime() - state.onDisplay(t2, (t2 - t1).nanoseconds) - } - - private fun GLDisplayScope.displayImpl( - canvas: Canvas, - displayContext: DirectContext, - paint: Paint, - ) { - val rt = BackendRenderTarget.makeGL( - fbo.size.width, - fbo.size.height, - 1, - 8, - fbo.id, - GL_RGBA8, - ) - val surface = Surface.makeFromBackendRenderTarget( - displayContext, - rt, - SurfaceOrigin.TOP_LEFT, - SurfaceColorFormat.RGBA_8888, - ColorSpace.sRGB - ) ?: error("Failed to create surface") - surface.draw(canvas, 0, 0, paint) - surface.close() - rt.close() - displayContext.resetGLAll() - } - - private fun initialize() { - renderContext = parentContext.deriveOffscreenContext() - renderContext!!.makeCurrent() - directContext = DirectContext.makeGL() - - fboPool = FBOPool(renderContext!!, parentContext, size, presentMode.impl, swapChainSize) - .apply(FBOPool::initialize) - } - - override fun run() { - while (size == IntSize.Zero && !isInterrupted) sleep(10) - if (isInterrupted) return - initialize() - var lastFrame: Long? = null - while (!isInterrupted) { - val renderStart = System.nanoTime() - val deltaTime = lastFrame?.let { renderStart - it } ?: 0 - val renderResult = fboPool!!.render(deltaTime.nanoseconds, drawBlock) - val e = renderResult.exceptionOrNull() - if (e is NoRenderFBOAvailable) { - try { - sleep(1) - } catch (e: InterruptedException) { - break - } - continue - } else if (e is CancellationException || e is InterruptedException) { - break - } else if (e != null) { - logger.error("Failed to render frame", e) - break - } - val waitTime = renderResult.getOrNull() - val renderEnd = System.nanoTime() - invalidations = renderEnd - state.onRender(renderEnd, (renderEnd - renderStart).nanoseconds) - lastFrame = renderStart - try { - if (waitTime != null) { - val toWait = (waitTime - (System.nanoTime() - renderStart).nanoseconds).toJavaDuration() - if (!toWait.isZero) { - state.updateRequested.poll(toWait.toNanos(), TimeUnit.NANOSECONDS) - } - } else { - state.updateRequested.take() - } - } catch (e: InterruptedException) { - break - } - } - logger.debug("GLSurfaceView stopped") - cleanupBlock() - fboPool?.destroy() - fboPool = null - directContext?.close() - directContext = null - renderContext?.destroy() - renderContext = null - } - - companion object { - private val logger = LoggerFactory.getLogger(GLSurface::class.java) - private val index = AtomicLong(0L) - } -} diff --git a/src/main/java/dev/silenium/compose/gl/surface/GLSurfaceState.kt b/src/main/java/dev/silenium/compose/gl/surface/GLSurfaceState.kt deleted file mode 100644 index 7ec49dd..0000000 --- a/src/main/java/dev/silenium/compose/gl/surface/GLSurfaceState.kt +++ /dev/null @@ -1,123 +0,0 @@ -package dev.silenium.compose.gl.surface - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import java.util.concurrent.ArrayBlockingQueue -import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds - -interface Stats> { - val values: List - val sum: T - val average: T - val min: T - val max: T - val median: T - fun percentile(percentile: Double, direction: Percentile = Percentile.UP): T - - enum class Percentile { - UP, LOWEST - } -} - -data class DurationStats(override val values: List) : Stats { - override val sum by lazy { values.fold(Duration.ZERO) { a, it -> a + it } } - override val average by lazy { sum / values.size } - override val min by lazy { values.minOrNull() ?: Duration.ZERO } - override val max by lazy { values.maxOrNull() ?: Duration.ZERO } - override val median by lazy { - if (values.isEmpty()) return@lazy Duration.ZERO - if (values.size == 1) return@lazy values.first() - val sorted = values.sorted() - val middle = sorted.size / 2 - if (sorted.size % 2 == 0) { - (sorted[middle - 1] + sorted[middle]) / 2 - } else { - sorted[middle] - } - } - - override fun percentile(percentile: Double, direction: Stats.Percentile): Duration { - if (values.isEmpty()) return Duration.ZERO - val sorted = values.sorted() - val index = when(direction) { - Stats.Percentile.UP -> (percentile * sorted.size).toInt() - Stats.Percentile.LOWEST -> sorted.size - (percentile * sorted.size).toInt() - 1 - } - return sorted[index] - } -} - -data class DoubleStats(override val values: List) : Stats { - override val sum by lazy { values.fold(0.0) { a, it -> a + it } } - override val average by lazy { sum / values.size } - override val min by lazy { values.minOrNull() ?: 0.0 } - override val max by lazy { values.maxOrNull() ?: 0.0 } - override val median by lazy { - if (values.isEmpty()) return@lazy 0.0 - if (values.size == 1) return@lazy values.first() - val sorted = values.sorted() - val middle = sorted.size / 2 - if (sorted.size % 2 == 0) { - (sorted[middle - 1] + sorted[middle]) / 2 - } else { - sorted[middle] - } - } - - override fun percentile(percentile: Double, direction: Stats.Percentile): Double { - if (values.isEmpty()) return 0.0 - val sorted = values.sorted() - val index = when(direction) { - Stats.Percentile.UP -> (percentile * sorted.size).toInt() - Stats.Percentile.LOWEST -> sorted.size - (percentile * sorted.size).toInt() - 1 - } - return sorted[index] - } -} - -data class RollingWindowStatistics( - val windowSize: Duration = 5.seconds, - val values: Map = emptyMap(), -) { - val frameTimes by lazy { DurationStats(values.values.toList()) } - val fps by lazy { - DoubleStats( - if (values.size < 2) emptyList() - else values.keys.sorted().zipWithNext().map { (a, b) -> 1_000_000_000.0 / (b - a) } - ) - } - - fun add(nanos: Long, time: Duration): RollingWindowStatistics { - val newValues = values.toMutableMap() - newValues[nanos] = time - return copy(values = newValues.filter { it.key >= nanos - windowSize.inWholeNanoseconds }) - } -} - -class GLSurfaceState { - private val renderStatisticsMutable = MutableStateFlow(RollingWindowStatistics()) - private val displayStatisticsMutable = MutableStateFlow(RollingWindowStatistics()) - internal val updateRequested = ArrayBlockingQueue(1) - - val renderStatistics: StateFlow get() = renderStatisticsMutable.asStateFlow() - val displayStatistics: StateFlow get() = displayStatisticsMutable.asStateFlow() - - fun requestUpdate() { - updateRequested.offer(Unit) - } - - internal fun onDisplay(nanos: Long, frameTime: Duration) { - displayStatisticsMutable.tryEmit(displayStatisticsMutable.value.add(nanos, frameTime)) - } - - internal fun onRender(nanos: Long, frameTime: Duration) { - renderStatisticsMutable.tryEmit(renderStatisticsMutable.value.add(nanos, frameTime)) - } -} - -@Composable -fun rememberGLSurfaceState() = remember { GLSurfaceState() } diff --git a/src/main/java/dev/silenium/compose/gl/surface/GLSurfaceView.kt b/src/main/java/dev/silenium/compose/gl/surface/GLSurfaceView.kt deleted file mode 100644 index e69de29..0000000 diff --git a/src/test/kotlin/Main.kt b/src/test/kotlin/Main.kt deleted file mode 100644 index 86b3c85..0000000 --- a/src/test/kotlin/Main.kt +++ /dev/null @@ -1,159 +0,0 @@ -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.tween -import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.pager.VerticalPager -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.TransformOrigin -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.ApplicationScope -import androidx.compose.ui.window.Window -import androidx.compose.ui.window.awaitApplication -import dev.silenium.compose.gl.surface.* -import kotlinx.coroutines.delay -import me.saket.telephoto.zoomable.ZoomSpec -import me.saket.telephoto.zoomable.rememberZoomableState -import me.saket.telephoto.zoomable.zoomable -import org.jetbrains.skia.Paint -import org.lwjgl.opengl.GL30.* -import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.seconds - -@Composable -@Preview -fun ApplicationScope.App() { - MaterialTheme(lightColors()) { - val state = rememberPagerState { 1000 } - LaunchedEffect(Unit) { -// while (isActive) { -// delay(100.milliseconds) -// state.animateScrollToPage(Random.nextInt(0..999)) -// } - } - VerticalPager(state, modifier = Modifier.fillMaxSize(), beyondViewportPageCount = 1) { - Content() - } - } -} - -@Composable -fun ApplicationScope.Content() { - Box(contentAlignment = Alignment.TopStart, modifier = Modifier.fillMaxSize().background(Color.White)) { - val state = rememberGLSurfaceState() - var targetHue by remember { mutableStateOf(0f) } - val color by animateColorAsState( - Color.hsl(targetHue, 1f, 0.5f, 0.1f), - animationSpec = tween(durationMillis = 200, easing = LinearEasing) - ) - LaunchedEffect(Unit) { - while (true) { - targetHue = (targetHue + 10f) % 360f - delay(200) - } - } - var visible by remember { mutableStateOf(true) } - LaunchedEffect(Unit) { - while (true) { - delay(2.seconds) - visible = !visible - } - } - LaunchedEffect(Unit) { - while (true) { - delay(300.milliseconds) - if (visible) { - state.requestUpdate() - } - } - } - val surfaceView = rememberGLSurface( - state = state, - presentMode = GLSurface.PresentMode.MAILBOX, - fboSizeOverride = FBOSizeOverride(4096, 4096, TransformOrigin.Center), - swapChainSize = 2, - ) { - glClearColor(color.red, color.green, color.blue, color.alpha) - glClear(GL_COLOR_BUFFER_BIT) - try { - Thread.sleep(33, 333) - } catch (e: InterruptedException) { - terminate() - return@rememberGLSurface - } - glBegin(GL_QUADS) - glColor3f(1f, 0f, 0f) - glVertex2f(-1f, 0f) - glColor3f(0f, 1f, 0f) - glVertex2f(0f, -1f) - glColor3f(0f, 0f, 1f) - glVertex2f(1f, 0f) - glColor3f(1f, 1f, 1f) - glVertex2f(0f, 1f) - glEnd() - - redrawAfter(null) - } - val modifier = Modifier - .aspectRatio(1f) - .zoomable(rememberZoomableState(ZoomSpec(6f))) - .align(Alignment.Center) - if (visible) { - GLSurfaceView( - surfaceView, - modifier = modifier, - paint = Paint().apply { - alpha = 128 - } - ) - } else { - Box( - modifier = modifier, - contentAlignment = Alignment.Center, - ) { - Text( - "Surface is not visible", - style = MaterialTheme.typography.h6, - color = MaterialTheme.colors.onBackground, - modifier = Modifier.align(Alignment.Center) - ) - } - } - Surface(modifier = Modifier.align(Alignment.TopStart).padding(4.dp)) { - Column(modifier = Modifier.padding(4.dp).width(400.dp)) { - val display by state.displayStatistics.collectAsState() - Text("Display datapoints: ${display.frameTimes.values.size}") - Text("Display frame time: ${display.frameTimes.median.inWholeMicroseconds / 1000.0} ms") - Text("Display frame time (99th): ${display.frameTimes.percentile(0.99).inWholeMicroseconds / 1000.0} ms") - Text("Display FPS: ${display.fps.median}") - Text("Display FPS (99th): ${display.fps.percentile(0.99, Stats.Percentile.LOWEST)}") - - val render by state.renderStatistics.collectAsState() - Text("Render datapoints: ${render.frameTimes.values.size}") - Text("Render frame time: ${render.frameTimes.median.inWholeMicroseconds / 1000.0} ms") - Text("Render frame time (99th): ${render.frameTimes.percentile(0.99).inWholeMicroseconds / 1000.0} ms") - Text("Render FPS: ${render.fps.median} ms") - Text("Render FPS (99th): ${render.fps.percentile(0.99, Stats.Percentile.LOWEST)}") - } - } - Button( - onClick = ::exitApplication, - modifier = Modifier.align(Alignment.BottomStart).padding(8.dp), - ) { - Text("Exit application") - } - } -} - -suspend fun main() = awaitApplication { - System.setProperty("skiko.renderApi", "OPENGL") - Window(onCloseRequest = ::exitApplication) { - App() - } -} diff --git a/src/test/kotlin/direct/Main.kt b/src/test/kotlin/direct/Main.kt index 236e89b..43755b5 100644 --- a/src/test/kotlin/direct/Main.kt +++ b/src/test/kotlin/direct/Main.kt @@ -5,7 +5,6 @@ import androidx.compose.material.Button import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text -import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Alignment import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.Modifier @@ -13,9 +12,8 @@ import androidx.compose.ui.scene.PlatformLayersComposeScene import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.application -import dev.silenium.compose.gl.direct.GLCanvas +import dev.silenium.compose.gl.canvas.GLCanvas import dev.silenium.compose.gl.graphicsApi -import direct_import.GLTextureDrawer import org.jetbrains.skiko.Version @OptIn(InternalComposeUiApi::class) @@ -25,16 +23,20 @@ fun main() = application { Text("Hello from Skia on OpenGL", style = MaterialTheme.typography.h2) } - val drawer = GLTextureDrawer() + val renderer = SampleRenderer() Window(onCloseRequest = ::exitApplication, title = "Test") { Box(Modifier.fillMaxSize()) { - DisposableEffect(Unit) { - onDispose { - drawer.destroy() - } - } - GLCanvas(modifier = Modifier.fillMaxSize()) { - drawer.render() + GLCanvas( + modifier = Modifier.fillMaxSize(), + onDispose = { + renderer.destroy() + println("Disposed") + }, + onResize = { old, new -> + println("Resized from $old to $new, new fbo: ${fbo.id}") + }, + ) { + renderer.draw() } Surface( shape = MaterialTheme.shapes.medium, diff --git a/src/test/kotlin/direct_import/GLTextureDrawer.kt b/src/test/kotlin/direct/SampleRenderer.kt similarity index 61% rename from src/test/kotlin/direct_import/GLTextureDrawer.kt rename to src/test/kotlin/direct/SampleRenderer.kt index 6888384..a24c929 100644 --- a/src/test/kotlin/direct_import/GLTextureDrawer.kt +++ b/src/test/kotlin/direct/SampleRenderer.kt @@ -1,14 +1,40 @@ -package direct_import +package direct -import org.lwjgl.opengl.GL11.* -import org.lwjgl.opengl.GL13.GL_TEXTURE0 -import org.lwjgl.opengl.GL13.glActiveTexture -import org.lwjgl.opengl.GL15.* -import org.lwjgl.opengl.GL20.* +import org.lwjgl.BufferUtils +import org.lwjgl.opengl.GL import org.lwjgl.opengl.GL30.* import java.io.File +import javax.imageio.ImageIO -class GLTextureDrawer { +//language=glsl +const val VERTEX_SHADER_SOURCE = """#version 330 core + +layout(location = 0) in vec3 vertexPosition; +layout(location = 1) in vec2 vertexUV; + +out vec2 UV; + +void main() { + gl_Position = vec4(vertexPosition, 1); + UV = vertexUV; +} +""" + +//language=glsl +const val FRAGMENT_SHADER_SOURCE = """#version 330 core + +in vec2 UV; +out vec4 color; + +uniform sampler2D myTextureSampler; + +void main() { + color = vec4(texture(myTextureSampler, UV).rgb, 1.0); + //color = vec4(UV.xy, 0.0, 1.0); +} +""" + +class SampleRenderer { private var textureId = 0 private var initialized = false private var shaderProgram = 0 @@ -16,7 +42,7 @@ class GLTextureDrawer { private var vbo = 0 private var ibo = 0 - fun initialize() { + private fun initialize() { if (initialized) return val img = "image.png" @@ -80,13 +106,13 @@ class GLTextureDrawer { initialized = true } - fun render() { + fun draw() { initialize() glEnable(GL_BLEND) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) - glClearColor(0f, .5f, .5f, 1f) + glClearColor(0f, .8f, .4f, 1f) glClear(GL_COLOR_BUFFER_BIT) glBindVertexArray(vao) @@ -116,3 +142,53 @@ class GLTextureDrawer { textureId = 0 } } + +fun loadTexture(file: File): Pair> { + val image = ImageIO.read(file) + + val width = image.width + val height = image.height + + // Convert image to RGBA + val pixels = IntArray(width * height) + image.getRGB(0, 0, width, height, pixels, 0, width) + + val buffer = BufferUtils.createByteBuffer(width * height * 4) + + // OpenGL expects bottom-to-top, so flip vertically + for (y in 0.. = - classpath.split(File.pathSeparator.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - for (cpv in classPathValues) { - println(cpv) - } - - var directContext: DirectContext? by mutableStateOf(null) - - var d3dTexture: Long? = null - var sharedHandle: Long? = null - var backendTexture: BackendTexture? = null - var glMemory: Int? = null - var image: Image? = null - var initialized by mutableStateOf(false) - - var glfwWindow = 0L - var fbo: FBO? = null - var glSurface: Surface? = null - var glRenderTarget: BackendRenderTarget? = null - var glDirectContext: DirectContext? = null - - val renderer = GLTextureDrawer() - - val glScene = PlatformLayersComposeScene() - glScene.setContent { - Text("Hello from Skia on OpenGL", style = MaterialTheme.typography.h2) - } - - Window( - ::exitApplication, - title = "Direct Render Test", - ) { - Box(Modifier.fillMaxSize()) { - DisposableEffect(Unit) { - onDispose { - GLFW.glfwMakeContextCurrent(glfwWindow) - renderer.destroy() - glSurface?.close() - glRenderTarget?.close() - glDirectContext?.close() - fbo?.destroy() - image?.close() - glMemory?.let(EXTMemoryObject::glDeleteMemoryObjectsEXT) - d3dTexture?.let(D3DInterop::destroyTexture) - sharedHandle?.let(D3DInterop::closeSharedHandle) - GLFW.glfwMakeContextCurrent(MemoryUtil.NULL) - glfwWindow.let(GLFW::glfwDestroyWindow) - println("Disposed") - } - } - LaunchedEffect(window) { - withContext(Dispatchers.IO) { - while (directContext == null && isActive) { - directContext = window.directContext() - } - } - } - Canvas(Modifier.matchParentSize()) { - if (directContext == null) return@Canvas - if (!initialized) { - GLFW.glfwInitHint(GLFW.GLFW_COCOA_MENUBAR, GLFW.GLFW_FALSE) - if (!GLFW.glfwInit()) { - throw RuntimeException("Failed to initialize GLFW") - } - GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3) - GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 2) - GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE) - - glfwWindow = GLFW.glfwCreateWindow(128, 128, "", MemoryUtil.NULL, MemoryUtil.NULL) -// println("glfwWindow: $glfwWindow") - if (glfwWindow == MemoryUtil.NULL) { - throw RuntimeException("Failed to create GLFW window") - } - GLFW.glfwMakeContextCurrent(glfwWindow) - GL.createCapabilities() - GLFW.glfwMakeContextCurrent(MemoryUtil.NULL) - initialized = true - } - GLFW.glfwMakeContextCurrent(glfwWindow) - if (fbo?.size != size.toIntSize()) { - glMemory?.let(EXTMemoryObject::glDeleteMemoryObjectsEXT) - glMemory = null - sharedHandle?.let(D3DInterop::closeSharedHandle) - sharedHandle = null - d3dTexture?.let(D3DInterop::destroyTexture) - d3dTexture = null - backendTexture?.close() - backendTexture = null - glSurface?.close() - glSurface = null - glRenderTarget?.close() - glRenderTarget = null - glDirectContext?.close() - glDirectContext = null - image?.close() - image = null - fbo?.destroy() - fbo = null - - d3dTexture = D3DInterop.createTexture(window, size.width.toInt(), size.height.toInt()) -// println("d3dTexture: $d3dTexture") - - sharedHandle = D3DInterop.exportSharedHandle(window, d3dTexture!!) -// println("sharedHandle: $sharedHandle") - - val colorAttachment = Texture(glGenTextures(), size.toIntSize(), GL_TEXTURE_2D, GL_RGBA8) - colorAttachment.bind() - glTexParameteri(colorAttachment.target, GL_TEXTURE_MIN_FILTER, GL_LINEAR) - checkGLError("glTexParameteri") - glTexParameteri(colorAttachment.target, GL_TEXTURE_MAG_FILTER, GL_LINEAR) - checkGLError("glTexParameteri") - glTexParameteri(colorAttachment.target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) - checkGLError("glTexParameteri") - glTexParameteri(colorAttachment.target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) - checkGLError("glTexParameteri") - glTexParameteri(colorAttachment.target, GL_TEXTURE_TILING_EXT, GL_OPTIMAL_TILING_EXT) - checkGLError("glTexParameteri") - colorAttachment.unbind() - - glMemory = EXTMemoryObject.glCreateMemoryObjectsEXT() - checkGLError("glCreateMemoryObjectsEXT") -// println("glMemory: $glMemory") - EXTMemoryObjectWin32.glImportMemoryWin32HandleEXT( - glMemory!!, size.width.toInt() * size.height.toInt() * 4 * 2L, - EXTMemoryObjectWin32.GL_HANDLE_TYPE_D3D12_RESOURCE_EXT, sharedHandle!!, - ) - checkGLError("glImportMemoryWin32HandleEXT") - - EXTMemoryObject.glTextureStorageMem2DEXT( - colorAttachment.id, - 1, GL_RGBA8, - size.width.toInt(), size.height.toInt(), - glMemory!!, 0 - ) - checkGLError("glTextureStorageMem2DEXT") - - val depthStencilAttachment = Renderbuffer.create(size.toIntSize(), GL_DEPTH24_STENCIL8) - fbo = FBO.create(colorAttachment, depthStencilAttachment) - - backendTexture = D3DInterop.makeBackendTexture(d3dTexture!!) - image = Image.adoptTextureFrom( - directContext!!, backendTexture!!, - SurfaceOrigin.TOP_LEFT, ColorType.RGBA_8888, - ) - - glDirectContext = DirectContext.makeGL() - glRenderTarget = BackendRenderTarget.makeGL( - size.width.toInt(), size.height.toInt(), 1, 8, fbo!!.id, GL_RGBA8, - ) - glSurface = Surface.makeFromBackendRenderTarget( - glDirectContext!!, - glRenderTarget!!, - SurfaceOrigin.TOP_LEFT, - SurfaceColorFormat.RGBA_8888, - ColorSpace.sRGB, - ) - } - - fbo?.draw { - println("redrawing (${size})") - renderer.render() - } - glFinish() - glDirectContext!!.resetGLAll() - with(glSurface!!.canvas) { - drawRect(Rect(100f, 200f, 200f, 300f), Paint().apply { color = Color.RED }) - save() - translate(100f, 300f) - glScene.render(this.asComposeCanvas(), 0L) - restore() - } - glSurface!!.flushAndSubmit(true) -// fbo?.snapshot(Path("rendered.png")) - GLFW.glfwMakeContextCurrent(MemoryUtil.NULL) - - drawContext.canvas.nativeCanvas.drawImage(image!!, 0f, 0f) - } - Surface( - shape = MaterialTheme.shapes.medium, - color = MaterialTheme.colors.surface, - modifier = Modifier.padding(8.dp).wrapContentWidth(), - ) { - Column( - verticalArrangement = Arrangement.spacedBy(4.dp), - horizontalAlignment = Alignment.Start, - modifier = Modifier.padding(8.dp), - ) { - Text("Skia Graphics API: ${window.graphicsApi()}") - Text("Skia Version: ${Version.skia}") - Text("Skiko Version: ${Version.skiko}") - Button(onClick = { println("button pressed") }) { - Text("Button") - } - } - } - } - } -} - -fun loadTexture(file: File): Pair> { - val image = ImageIO.read(file) - - val width = image.width - val height = image.height - - // Convert image to RGBA - val pixels = IntArray(width * height) - image.getRGB(0, 0, width, height, pixels, 0, width) - - val buffer = BufferUtils.createByteBuffer(width * height * 4) - - // OpenGL expects bottom-to-top, so flip vertically - for (y in 0.. Date: Sun, 5 Oct 2025 22:37:48 +0200 Subject: [PATCH 13/16] docs: add skia in GLCanvas to sample --- src/test/kotlin/direct/Main.kt | 45 +++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/src/test/kotlin/direct/Main.kt b/src/test/kotlin/direct/Main.kt index 43755b5..462d117 100644 --- a/src/test/kotlin/direct/Main.kt +++ b/src/test/kotlin/direct/Main.kt @@ -1,28 +1,43 @@ package direct +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.Button import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asComposeCanvas import androidx.compose.ui.scene.PlatformLayersComposeScene import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import dev.silenium.compose.gl.canvas.GLCanvas import dev.silenium.compose.gl.graphicsApi +import org.jetbrains.skia.* import org.jetbrains.skiko.Version @OptIn(InternalComposeUiApi::class) fun main() = application { val glScene = PlatformLayersComposeScene() glScene.setContent { - Text("Hello from Skia on OpenGL", style = MaterialTheme.typography.h2) + Text( + "Hello from Skia on OpenGL", + style = MaterialTheme.typography.h2, + modifier = Modifier.background(Color.White.copy(alpha = 0.5f)).padding(10.dp), + ) } + var glSurface: Surface? by mutableStateOf(null) + var glContext: DirectContext? by mutableStateOf(null) + var glRenderTarget: BackendRenderTarget? by mutableStateOf(null) + val renderer = SampleRenderer() Window(onCloseRequest = ::exitApplication, title = "Test") { Box(Modifier.fillMaxSize()) { @@ -34,9 +49,37 @@ fun main() = application { }, onResize = { old, new -> println("Resized from $old to $new, new fbo: ${fbo.id}") + + if (glContext == null) { + glContext = DirectContext.makeGL() + } + glSurface?.close() + glRenderTarget?.close() + glRenderTarget = BackendRenderTarget.makeGL( + width = new.width, + height = new.height, + sampleCnt = 1, + stencilBits = 8, + fbId = fbo.id, + fbFormat = fbo.colorAttachment.internalFormat, + ) + glSurface = Surface.makeFromBackendRenderTarget( + context = glContext!!, rt = glRenderTarget!!, + origin = SurfaceOrigin.TOP_LEFT, + colorFormat = SurfaceColorFormat.RGBA_8888, + colorSpace = ColorSpace.sRGB, + ) }, ) { renderer.draw() + glContext?.resetGLAll() + glSurface?.canvas?.let { + it.save() + it.translate(50f, 200f) + glScene.render(it.asComposeCanvas(), 0L) + it.restore() + } + glSurface?.flushAndSubmit() } Surface( shape = MaterialTheme.shapes.medium, From 25d37104a33d42162ad7971136587a8fd0fc4d09 Mon Sep 17 00:00:00 2001 From: Silas Della Contrada Date: Fri, 10 Oct 2025 16:14:39 +0200 Subject: [PATCH 14/16] chore: remove obsolete cpp code and only download skia for windows build --- native/CMakeLists.txt | 7 +- native/src/cpp/library.cpp | 1 + native/src/cpp/linux/GLXContext.cpp | 172 -------------------------- native/src/cpp/windows/WGLContext.cpp | 16 --- 4 files changed, 3 insertions(+), 193 deletions(-) create mode 100644 native/src/cpp/library.cpp delete mode 100644 native/src/cpp/linux/GLXContext.cpp delete mode 100644 native/src/cpp/windows/WGLContext.cpp diff --git a/native/CMakeLists.txt b/native/CMakeLists.txt index df50bb6..0162d37 100644 --- a/native/CMakeLists.txt +++ b/native/CMakeLists.txt @@ -21,10 +21,8 @@ set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON) -include(cmake/skia.cmake) - set(SOURCES - + src/cpp/library.cpp ) if (CMAKE_SYSTEM_NAME STREQUAL "Linux") @@ -33,12 +31,10 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Linux") endif () list(APPEND SOURCES - src/cpp/linux/GLXContext.cpp ) elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows") list(APPEND SOURCES src/cpp/windows/D3DInterop.cpp - src/cpp/windows/WGLContext.cpp ) endif () @@ -55,6 +51,7 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_libraries(${PROJECT_NAME} PUBLIC PkgConfig::GL) target_include_directories(${PROJECT_NAME} PUBLIC "${JAVA_HOME}/include/linux") elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows") + include(cmake/skia.cmake) target_compile_definitions(${PROJECT_NAME} PRIVATE -D_WINDOWS) target_compile_definitions(${PROJECT_NAME} PRIVATE SK_DIRECT3D NOMINMAX WIN32_LEAN_AND_MEAN) target_compile_options(${PROJECT_NAME} PRIVATE /MT) diff --git a/native/src/cpp/library.cpp b/native/src/cpp/library.cpp new file mode 100644 index 0000000..8b1a393 --- /dev/null +++ b/native/src/cpp/library.cpp @@ -0,0 +1 @@ +// empty diff --git a/native/src/cpp/linux/GLXContext.cpp b/native/src/cpp/linux/GLXContext.cpp deleted file mode 100644 index 3550d0a..0000000 --- a/native/src/cpp/linux/GLXContext.cpp +++ /dev/null @@ -1,172 +0,0 @@ -// -// Created by silenium-dev on 7/28/24. -// - -#include -#include -#include - -#include - -#define GLX_CONTEXT_MAJOR_VERSION_ARB 0x2091 -#define GLX_CONTEXT_MINOR_VERSION_ARB 0x2092 - -typedef GLXContext (*glXCreateContextAttribsARBProc)(Display *, GLXFBConfig, GLXContext, Bool, const int *); - -static int handleError(Display *display, XErrorEvent *event) { - std::cerr << "X Error: " << static_cast(event->error_code) << std::endl; - std::cerr << " Request: " << static_cast(event->request_code) << std::endl; - std::cerr << " Error: " << static_cast(event->error_code) << std::endl; - char buf[256]; - XGetErrorText(display, event->error_code, buf, sizeof(buf)); - std::cerr << " Text: " << buf << std::endl; - return 0; -} - -// Helper to check for extension string presence. Adapted from: -// http://www.opengl.org/resources/features/OGLextensions/ -static bool isExtensionSupported(const char *extList, const char *extension) { - /* Extension names should not have spaces. */ - const char *where = strchr(extension, ' '); - if (where || *extension == '\0') - return false; - - /* It takes a bit of care to be fool-proof about parsing the - OpenGL extensions string. Don't be fooled by sub-strings, - etc. */ - for (const char *start = extList;;) { - where = strstr(start, extension); - - if (!where) - break; - - const char *terminator = where + strlen(extension); - - if (where == start || *(where - 1) == ' ') - if (*terminator == ' ' || *terminator == '\0') - return true; - - start = terminator; - } - - return false; -} - -extern "C" { -JNIEXPORT jlong JNICALL Java_dev_silenium_compose_gl_context_GLXContextKt_getCurrentContextN(JNIEnv *env, jobject thiz) { - return reinterpret_cast(glXGetCurrentContext()); -} - -JNIEXPORT jlong JNICALL Java_dev_silenium_compose_gl_context_GLXContextKt_getCurrentDisplayN(JNIEnv *env, jobject thiz) { - return reinterpret_cast(glXGetCurrentDisplay()); -} - -JNIEXPORT jlong JNICALL -Java_dev_silenium_compose_gl_context_GLXContextKt_getCurrentDrawableN(JNIEnv *env, jobject thiz) { - return static_cast(glXGetCurrentDrawable()); -} - -JNIEXPORT jlongArray JNICALL -Java_dev_silenium_compose_gl_context_GLXContextKt_createContextN(JNIEnv *env, jobject thiz, - const jlong _display, const jlong share) { - auto result = env->NewLongArray(3); - - const auto display = reinterpret_cast(_display); - - constexpr int attribs[]{ - GLX_RED_SIZE, 8, - GLX_GREEN_SIZE, 8, - GLX_BLUE_SIZE, 8, - GLX_ALPHA_SIZE, 8, - GLX_DRAWABLE_TYPE, GLX_PIXMAP_BIT, - GLX_DEPTH_SIZE, 24, - GLX_STENCIL_SIZE, 8, - None - }; - int fbConfigCount{0}; - const auto fbConfig = glXChooseFBConfig(display, DefaultScreen(display), attribs, &fbConfigCount); - if (fbConfig == nullptr || fbConfigCount <= 0) { - std::cerr << "Failed to get FBConfig" << std::endl; - env->DeleteLocalRef(result); - return nullptr; - } - const auto fbc = fbConfig[0]; - XFree(fbConfig); - - int attribs2[]{ - GLX_NONE, - }; - const auto xPixmap = XCreatePixmap(display, RootWindow(display, DefaultScreen(display)), 16, 16, 24); - if (xPixmap == None) { - std::cerr << "Failed to create X Pixmap" << std::endl; - env->DeleteLocalRef(result); - return nullptr; - } - const auto glxPixmap = glXCreatePixmap(display, fbc, xPixmap, attribs2); - if (glxPixmap == None) { - std::cerr << "Failed to create PBuffer" << std::endl; - env->DeleteLocalRef(result); - XFreePixmap(display, xPixmap); - return nullptr; - } - const jlong xPixmapLong = *reinterpret_cast(&xPixmap); - const jlong glxPixmapLong = *reinterpret_cast(&glxPixmap); - env->SetLongArrayRegion(result, 0, 1, &xPixmapLong); - env->SetLongArrayRegion(result, 1, 1, &glxPixmapLong); - - const auto visual = glXGetVisualFromFBConfig(display, fbc); - if (visual == nullptr) { - std::cerr << "Failed to get Visual" << std::endl; - env->DeleteLocalRef(result); - glXDestroyPixmap(display, glxPixmap); - XFreePixmap(display, xPixmap); - return nullptr; - } - - const char *glxExts = glXQueryExtensionsString(display, DefaultScreen(display)); - auto glXCreateContextAttribsARB = reinterpret_cast( - glXGetProcAddressARB(reinterpret_cast("glXCreateContextAttribsARB")) - ); - - GLXContext ctx{nullptr}; - if (!isExtensionSupported(glxExts, "GLX_ARB_create_context") || - !glXCreateContextAttribsARB) { - std::cout << "glXCreateContextAttribsARB() not found ... using old-style GLX context" << std::endl; - ctx = glXCreateNewContext(display, fbc, GLX_RGBA_TYPE, reinterpret_cast(share), True); - } else { - int context_attribs[] = { - GLX_CONTEXT_MAJOR_VERSION_ARB, 3, - GLX_CONTEXT_MINOR_VERSION_ARB, 0, - //GLX_CONTEXT_FLAGS_ARB , GLX_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB, - None - }; - ctx = glXCreateContextAttribsARB(display, fbc, reinterpret_cast(share), True, context_attribs); - } - - if (ctx == nullptr) { - std::cerr << "Failed to create Context" << std::endl; - env->DeleteLocalRef(result); - glXDestroyPixmap(display, glxPixmap); - XFreePixmap(display, xPixmap); - XFree(visual); - return nullptr; - } - const auto ctxLong = reinterpret_cast(ctx); - env->SetLongArrayRegion(result, 2, 1, &ctxLong); - - return result; -} - -JNIEXPORT void JNICALL -Java_dev_silenium_compose_gl_context_GLXContextKt_destroyPixmapN(JNIEnv *env, jobject thiz, - const jlong _display, const jlong xPixmap, - const jlong glxPixmap) { - const auto display = reinterpret_cast(_display); - const auto xPixmap_ = *reinterpret_cast(&xPixmap); - const auto glxPixmap_ = *reinterpret_cast(&glxPixmap); - glXDestroyPixmap(display, glxPixmap_); - if (xPixmap_ != None) { - XFreePixmap(display, xPixmap_); - } -} -} diff --git a/native/src/cpp/windows/WGLContext.cpp b/native/src/cpp/windows/WGLContext.cpp deleted file mode 100644 index a2e1d05..0000000 --- a/native/src/cpp/windows/WGLContext.cpp +++ /dev/null @@ -1,16 +0,0 @@ -// -// Created by silenium-dev on 2024-09-15. -// - -#include -#include -#include -#include - -extern "C" { -JNIEXPORT jlong JNICALL Java_dev_silenium_compose_gl_context_WGLContextKt_wglCreateContext(JNIEnv *env, jobject thiz, jlong _hdc) { - const auto hdc = reinterpret_cast(_hdc); - const auto ctx = wglCreateContext(hdc); - return reinterpret_cast(ctx); -} -} From c12a61a288939ceb8e7639adaa59a0a66d545b39 Mon Sep 17 00:00:00 2001 From: Silas Della Contrada Date: Fri, 10 Oct 2025 16:15:26 +0200 Subject: [PATCH 15/16] ci: add windows build --- .github/workflows/build.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index b30c489..05c29ac 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -29,6 +29,19 @@ jobs: tests: false java-version: 17 platform: ${{ github.job }} + windows-x86_64: + runs-on: windows-latest + steps: + - uses: silenium-dev/actions/jni-natives/windows@main + with: + gradle-cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} + snapshot-repo-url: "https://reposilite.silenium.dev/snapshots" + release-repo-url: "https://reposilite.silenium.dev/releases" + repo-username: ${{ secrets.REPOSILITE_USERNAME }} + repo-password: ${{ secrets.REPOSILITE_PASSWORD }} + tests: false + java-version: 17 + platform: ${{ github.job }} kotlin: runs-on: ubuntu-22.04 steps: From acad856f196cddba5bbbad7ad7c4b789c90b24a4 Mon Sep 17 00:00:00 2001 From: Silas Della Contrada Date: Fri, 10 Oct 2025 16:29:17 +0200 Subject: [PATCH 16/16] chore: remove obsolete link libs for linux build --- native/CMakeLists.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/native/CMakeLists.txt b/native/CMakeLists.txt index 0162d37..e772979 100644 --- a/native/CMakeLists.txt +++ b/native/CMakeLists.txt @@ -47,8 +47,6 @@ target_include_directories(${PROJECT_NAME} PUBLIC "${JAVA_HOME}/include") if (CMAKE_SYSTEM_NAME STREQUAL "Linux") target_compile_definitions(${PROJECT_NAME} PRIVATE -D_LINUX) find_package(PkgConfig REQUIRED) - pkg_check_modules(GL REQUIRED IMPORTED_TARGET gl egl glx) - target_link_libraries(${PROJECT_NAME} PUBLIC PkgConfig::GL) target_include_directories(${PROJECT_NAME} PUBLIC "${JAVA_HOME}/include/linux") elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows") include(cmake/skia.cmake)