Skip to content

Instantly share code, notes, and snippets.

@brahmkshatriya
Last active September 25, 2025 22:58
Show Gist options
  • Select an option

  • Save brahmkshatriya/a955b6f5c056d2692e06cb423706f059 to your computer and use it in GitHub Desktop.

Select an option

Save brahmkshatriya/a955b6f5c056d2692e06cb423706f059 to your computer and use it in GitHub Desktop.
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.CompositionLocal
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.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 loadTexture
import org.jetbrains.skia.BackendTexture
import org.jetbrains.skia.ColorType
import org.jetbrains.skia.DirectContext
import org.jetbrains.skia.Image
import org.jetbrains.skia.Surface
import org.jetbrains.skia.SurfaceOrigin
import org.jetbrains.skiko.SkiaLayer
import org.lwjgl.opengl.GL11
import org.lwjgl.opengl.GL11.GL_RGBA
import org.lwjgl.opengl.GL11.GL_TEXTURE_2D
import java.awt.Window
import java.io.File
fun main() = application {
System.setProperty("skiko.renderApi", "OPENGL")
var textureId: Int? = null
Window(
::exitApplication,
title = "OpenGl Test",
) {
val window = LocalWindow.current!!
var directContext by remember { mutableStateOf<DirectContext?>(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"
textureId = loadTexture(File(img))
}
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT)
GL11.glEnable(GL11.GL_TEXTURE_2D)
GL11.glBindTexture(GL11.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(GL11.GL_TEXTURE_2D)
val backendTex = BackendTexture.makeGL(
width = size.width.toInt(),
height = size.height.toInt(),
isMipmapped = false,
textureId = textureId,
textureTarget = GL_TEXTURE_2D,
textureFormat = GL_RGBA
)
val image = Image.adoptTextureFrom(
context,
backendTex,
SurfaceOrigin.TOP_LEFT,
ColorType.RGBA_8888,
)
drawContext.canvas.nativeCanvas.drawImage(image, 0f, 0f)
}
}
}
@Suppress("UNCHECKED_CAST")
val LocalWindow: CompositionLocal<Window?> by lazy {
val clazz = Class.forName("androidx.compose.ui.window.LocalWindowKt")
val method = clazz.getMethod("getLocalWindow")
method.invoke(null) as CompositionLocal<Window?>
}
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): Int {
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..<width) {
val pixel = pixels[y * width + x]
buffer.put(((pixel shr 16) and 0xFF).toByte()) // Red
buffer.put(((pixel shr 8) and 0xFF).toByte()) // Green
buffer.put((pixel and 0xFF).toByte()) // Blue
buffer.put(((pixel shr 24) and 0xFF).toByte()) // Alpha
}
}
buffer.flip()
GL.createCapabilities()
val textureID = GL11.glGenTextures()
GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureID)
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP)
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP)
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR)
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR)
GL11.glTexImage2D(
GL11.GL_TEXTURE_2D,
0,
GL11.GL_RGBA8,
width,
height,
0,
GL11.GL_RGBA,
GL11.GL_UNSIGNED_BYTE,
buffer
)
return textureID
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment