Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
[versions]
kotlinx-coroutines = "1.10.2"
lwjgl = "3.3.6"
skiko = "0.9.22.2"
jni-utils = "0.1.6"
compose = "1.9.0"
compose = "1.10.0-alpha02"
kotlin = "2.2.20"

[libraries]
Expand All @@ -19,8 +18,7 @@ lwjgl-core = { group = "org.lwjgl", name = "lwjgl", version.ref = "lwjgl" }
lwjgl-glfw = { group = "org.lwjgl", name = "lwjgl-glfw", version.ref = "lwjgl" }
lwjgl-opengl = { group = "org.lwjgl", name = "lwjgl-opengl", version.ref = "lwjgl" }

skiko-awt = { group = "org.jetbrains.skiko", name = "skiko-awt", version.ref = "skiko" }
skiko-awt-runtime-linux-x64 = { group = "org.jetbrains.skiko", name = "skiko-awt-runtime-linux-x64", version.ref = "skiko" }
skiko-awt = { group = "org.jetbrains.skiko", name = "skiko-awt" }

jni-utils = { group = "dev.silenium.libs.jni", name = "jni-utils", version.ref = "jni-utils" }
slf4j-api = { group = "org.slf4j", name = "slf4j-api", version = "2.0.17" }
Expand All @@ -44,7 +42,6 @@ kotlinx-coroutines = [

skiko = [
"skiko-awt",
"skiko-awt-runtime-linux-x64",
]

lwjgl = [
Expand Down
6 changes: 4 additions & 2 deletions native/cmake/skia.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,17 @@ endforeach ()
message(STATUS "Downloading Skia from ${ASSET_URL} with hash ${ASSET_HASH}")

set(SKIA_URL "${ASSET_URL}")
string(REPLACE ":" "=" ASSET_HASH "${ASSET_HASH}")
string(REGEX REPLACE ":.*" "" ASSET_HASH_ALGO "${ASSET_HASH}")
string(REGEX REPLACE ".*:" "" ASSET_HASH_VALUE "${ASSET_HASH}")
string(TOUPPER "${ASSET_HASH_ALGO}" ASSET_HASH_ALGO)

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)
file(DOWNLOAD ${SKIA_URL} "${CMAKE_BINARY_DIR}/download/${ASSET_NAME}" STATUS SKIA_DOWNLOAD_STATUS EXPECTED_HASH "${ASSET_HASH_ALGO}=${ASSET_HASH_VALUE}" SHOW_PROGRESS)
endif ()

list(GET SKIA_DOWNLOAD_STATUS 0 SKIA_DOWNLOAD_STATUS_CODE)
Expand Down
105 changes: 62 additions & 43 deletions src/main/java/dev/silenium/compose/gl/CompositionLocals.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@ 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.Container
import java.awt.Window
import java.util.*
import javax.swing.JComponent
import kotlin.reflect.KClass
import kotlin.reflect.KProperty1
import kotlin.reflect.KType
import kotlin.reflect.full.createType
import kotlin.reflect.full.isSubtypeOf
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.superclasses
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.typeOf

Expand All @@ -19,59 +26,71 @@ val LocalWindow: CompositionLocal<Window?> by lazy {
method.invoke(null) as CompositionLocal<Window?>
}

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 findSkiaLayer()?.let { layer ->
if (layer.graphicsApi() != GraphicsApi.DIRECT3D) return null
layer.redrawer().let { redrawer ->
val getter = redrawer::class.memberProperties.first {
it.name == "device" && it.returnType == typeOf<Long>()
}
getter.isAccessible = true
getter.call(redrawer) as Long?
}
}
}

fun Window.directX12Device(): NativePointer? {
return directXRedrawer()?.let {
val getter = it::class.memberProperties.first { it.name == "device" && it.returnType == typeOf<Long>() }
getter.isAccessible = true
getter.call(it) as Long?
}
fun SkiaLayer.directContext(): DirectContext? {
return contextHandler()?.findProperty<DirectContext?>()
}

fun Window.directContext(): DirectContext? {
val surface = contextHandler().let {
val getter = it::class.java.superclass.superclass.getDeclaredMethod("getSurface")
getter.isAccessible = true
getter.invoke(it) as? Surface
}
return surface?.recordingContext
inline fun <reified T> Any.findProperty(): T? {
return findProperty(typeOf<T>()) as T?
}

fun Window.graphicsApi(): GraphicsApi {
return mediator().let {
val getter = it::class.memberProperties.first { it.name == "renderApi" && it.returnType == typeOf<GraphicsApi>() }
getter.isAccessible = true
getter.call(it) as GraphicsApi
fun Any.findProperty(type: KType): Any? {
val supertypes = LinkedList<KClass<*>>()
supertypes.add(this::class)
var getter: KProperty1<*, *>? = null
while (getter == null && supertypes.isNotEmpty()) {
val klass = supertypes.pop()
for (prop in klass.memberProperties) {
if (prop.returnType.isSubtypeOf(type)) {
getter = prop
break
}
}
klass.superclasses.let(supertypes::addAll)
}
getter?.isAccessible = true
return getter?.call(this)
}

fun Window.mediator(): Any {
val composePanel = this.getFieldValue("composePanel")!!
val composeContainer = composePanel.getFieldValue("_composeContainer")!!
return composeContainer.getFieldValue("mediator")!!
fun SkiaLayer.graphicsApi(): GraphicsApi {
return findProperty<GraphicsApi>() ?: GraphicsApi.UNKNOWN
}

fun Window.contextHandler(): Any {
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)
}
return redrawer?.getFieldValue("contextHandler")!!
fun SkiaLayer.contextHandler(): Any? {
val propType = Class.forName("org.jetbrains.skiko.context.ContextHandler")
return redrawer().findProperty(propType.kotlin.createType())
}

private fun Any.getFieldValue(fieldName: String): Any? {
val field = this::class.java.getDeclaredField(fieldName)
field.isAccessible = true
return field.get(this)
fun SkiaLayer.redrawer(): Any = SkikoCompat.getRedrawer(this)

fun Window.findSkiaLayer() = findComponent<SkiaLayer>()

private fun <T : JComponent> findComponent(
container: Container,
klass: Class<T>,
): T? {
val componentSequence = container.components.asSequence()
return componentSequence
.filter { klass.isInstance(it) }
.ifEmpty {
componentSequence
.filterIsInstance<Container>()
.mapNotNull { findComponent(it, klass) }
}.map { klass.cast(it) }
.firstOrNull()
}

private inline fun <reified T : JComponent> Container.findComponent() = findComponent(this, T::class.java)
9 changes: 9 additions & 0 deletions src/main/java/dev/silenium/compose/gl/SkikoCompat.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package dev.silenium.compose.gl;

import org.jetbrains.skiko.SkiaLayer;

class SkikoCompat {
public static Object getRedrawer(SkiaLayer layer) {
return layer.getRedrawer$skiko();
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package dev.silenium.compose.gl.canvas

import dev.silenium.compose.gl.findSkiaLayer
import dev.silenium.compose.gl.graphicsApi
import org.jetbrains.skiko.GraphicsApi
import java.awt.Window

object DefaultCanvasDriverFactory : CanvasDriverFactory<CanvasDriver> {
override fun create(window: Window): CanvasDriver {
val factory = apiFactories[window.graphicsApi()]
factory ?: throw UnsupportedOperationException("Unsupported graphics api: ${window.graphicsApi()}")
val factory = apiFactories[window.findSkiaLayer()?.graphicsApi()]
factory ?: throw UnsupportedOperationException(
"Unsupported graphics api: ${
window.findSkiaLayer()?.graphicsApi()
}"
)
return factory.create(window)
}

Expand Down
5 changes: 4 additions & 1 deletion src/main/java/dev/silenium/compose/gl/canvas/GLCanvas.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.unit.IntSize
import dev.silenium.compose.gl.LocalWindow
import dev.silenium.compose.gl.directContext
import dev.silenium.compose.gl.findSkiaLayer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
Expand All @@ -26,7 +28,7 @@ fun GLCanvas(
LaunchedEffect(window) {
withContext(Dispatchers.IO) {
while (isActive) {
window.directContext()?.let {
window.findSkiaLayer()?.directContext()?.let {
wrapper.setup(it)
return@withContext
}
Expand All @@ -39,6 +41,7 @@ fun GLCanvas(
}
}
Canvas(modifier) {
drawContext.canvas.nativeCanvas
wrapper.render(this, onResize) {
drawGL { block() }
}
Expand Down
5 changes: 4 additions & 1 deletion src/test/kotlin/direct/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ 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.LocalWindow
import dev.silenium.compose.gl.canvas.GLCanvas
import dev.silenium.compose.gl.findSkiaLayer
import dev.silenium.compose.gl.graphicsApi
import org.jetbrains.skia.*
import org.jetbrains.skiko.Version
Expand All @@ -41,6 +43,7 @@ fun main() = application {
val renderer = SampleRenderer()
Window(onCloseRequest = ::exitApplication, title = "Test") {
Box(Modifier.fillMaxSize()) {
println("skia layer: ${LocalWindow.current?.findSkiaLayer()}")
GLCanvas(
modifier = Modifier.fillMaxSize(),
onDispose = {
Expand Down Expand Up @@ -91,7 +94,7 @@ fun main() = application {
horizontalAlignment = Alignment.Start,
modifier = Modifier.padding(8.dp),
) {
Text("Skia Graphics API: ${window.graphicsApi()}")
Text("Skia Graphics API: ${window.findSkiaLayer()?.graphicsApi()}")
Text("Skia Version: ${Version.skia}")
Text("Skiko Version: ${Version.skiko}")
Button(onClick = { println("button pressed") }) {
Expand Down