Commit f970ae3d authored by Igor Demin's avatar Igor Demin

Draw into picture, refactor SkiaLayer

1. Split onRender into performUpdate and performDraw.
performUpdate will be called from the Swing thread in upcoming commits (to avoid race conditions)

2. Remove canvas.scale by default

Motivation: automatic scaling is harmful.
Example:
- skiko returns us 100x100 canvas size.
- We load image from disk and reduce its size to 100x100.
- We draw this image onto canvas, and it is scaled 2x with artifacts.
If skiko returns us 200x200 then we reduce image to 200x200 and draw it onto canvas without scaling.

3. Cache contentScale
It is expensive operation on Linux to do it in every frame (we read database)

4. Remove onInit, onReshape, onDispose removed from SkiaRenderer.
It is hard to implement it in the right thread (on macOs we have AppKit and AWT thread). Besides we don't use them in Compose.
If they will be needed then we can implement them later.
parent b6624360
package SkijaInjectSample
import org.jetbrains.skiko.SkiaWindow
import java.awt.event.MouseEvent
import javax.swing.WindowConstants
import javax.swing.event.MouseInputAdapter
import org.jetbrains.skija.*
import org.jetbrains.skiko.SkiaRenderer
import java.awt.event.MouseMotionAdapter
import kotlin.math.cos
import kotlin.math.sin
import org.jetbrains.skija.paragraph.FontCollection
import org.jetbrains.skija.paragraph.ParagraphBuilder
import org.jetbrains.skija.paragraph.ParagraphStyle
import org.jetbrains.skija.paragraph.TextStyle
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import java.awt.event.KeyEvent
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.SkiaRenderer
import org.jetbrains.skiko.SkiaWindow
import java.awt.Toolkit
import javax.swing.JFrame
import javax.swing.JMenu
import javax.swing.JMenuBar
import javax.swing.JMenuItem
import javax.swing.JOptionPane
import javax.swing.KeyStroke
import java.awt.event.*
import javax.swing.*
import kotlin.math.cos
import kotlin.math.sin
fun main(args: Array<String>) {
createWindow("First window")
}
fun createWindow(title: String) {
fun createWindow(title: String) = SwingUtilities.invokeLater {
var mouseX = 0
var mouseY = 0
......@@ -68,8 +57,8 @@ fun createWindow(title: String) {
val state = State()
state.text = title
window.layer.renderer = Renderer {
renderer, w, h -> displayScene(renderer, w, h, mouseX, mouseY, state)
window.layer.renderer = Renderer(window.layer) {
renderer, w, h, nanoTime -> displayScene(renderer, w, h, nanoTime, mouseX, mouseY, state)
}
window.layer.addMouseMotionListener(object : MouseMotionAdapter() {
......@@ -85,7 +74,10 @@ fun createWindow(title: String) {
window.setVisible(true)
}
class Renderer(val displayScene: (Renderer, Int, Int) -> Unit): SkiaRenderer {
class Renderer(
val layer: SkiaLayer,
val displayScene: (Renderer, Int, Int, Long) -> Unit
): SkiaRenderer {
val typeface = Typeface.makeFromFile("fonts/JetBrainsMono-Regular.ttf")
val font = Font(typeface, 40f)
val paint = Paint().apply {
......@@ -96,18 +88,11 @@ class Renderer(val displayScene: (Renderer, Int, Int) -> Unit): SkiaRenderer {
var canvas: Canvas? = null
override fun onInit() {
}
override fun onDispose() {
}
override fun onReshape(width: Int, height: Int) {
}
override fun onRender(canvas: Canvas, width: Int, height: Int) {
override suspend fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
this.canvas = canvas
displayScene(this, width, height)
val contentScale = layer.contentScale
canvas.scale(contentScale, contentScale)
displayScene(this, (width / contentScale).toInt(), (height / contentScale).toInt(), nanoTime)
}
}
......@@ -116,7 +101,10 @@ class State {
var text: String = "Hello Skija"
}
fun displayScene(renderer: Renderer, width: Int, height: Int, xpos: Int, ypos: Int, state: State) {
private val fontCollection = FontCollection()
.setDefaultFontManager(FontMgr.getDefault())
fun displayScene(renderer: Renderer, width: Int, height: Int, nanoTime: Long, xpos: Int, ypos: Int, state: State) {
val canvas = renderer.canvas!!
val watchFill = Paint().setColor(0xFFFFFFFF.toInt())
val watchStroke = Paint().setColor(0xFF000000.toInt()).setMode(PaintMode.STROKE).setStrokeWidth(1f)
......@@ -140,7 +128,7 @@ fun displayScene(renderer: Renderer, width: Int, height: Int, xpos: Int, ypos: I
)
angle += (2.0 * Math.PI / 12.0).toFloat()
}
val time = System.currentTimeMillis() % 60000 +
val time = (nanoTime / 1E6) % 60000 +
(x.toFloat() / width * 5000).toLong() +
(y.toFloat() / width * 5000).toLong()
......@@ -160,8 +148,6 @@ fun displayScene(renderer: Renderer, width: Int, height: Int, xpos: Int, ypos: I
val text = "${state.text} ${state.frame++}!"
canvas.drawString(text, xpos.toFloat(), ypos.toFloat(), renderer.font, renderer.paint)
val fontCollection = FontCollection()
.setDefaultFontManager(FontMgr.getDefault())
val style = ParagraphStyle()
val paragraph = ParagraphBuilder(style, fontCollection)
.pushStyle(TextStyle().setColor(0xFF000000.toInt()))
......
......@@ -63,7 +63,7 @@ fun SwingSkia() {
}
})
panel.layer.renderer = Renderer { renderer, w, h -> displayScene(renderer, w, h, mouseX, mouseY, state) }
panel.layer.renderer = Renderer(panel.layer) { renderer, w, h, nanoTime -> displayScene(renderer, w, h, nanoTime, mouseX, mouseY, state) }
panel.layer.addMouseMotionListener(object : MouseMotionAdapter() {
override fun mouseMoved(event: MouseEvent) {
......
package org.jetbrains.skiko
import java.awt.Graphics
import java.awt.Canvas
import java.awt.Graphics
import java.awt.event.HierarchyEvent
import javax.swing.SwingUtilities.convertPoint
import javax.swing.SwingUtilities.getRootPane
abstract class HardwareLayer : Canvas(), Drawable {
companion object {
init {
Library.load()
}
}
// getDpiScale is expensive operation on some platforms, so we cache it
private var _contentScale: Float? = null
private var isInit = false
init {
@Suppress("LeakingThis")
addHierarchyListener {
if (it.changeFlags and HierarchyEvent.SHOWING_CHANGED.toLong() != 0L) {
checkIsShowing()
}
}
}
private fun checkIsShowing() {
if (!isInit && isShowing) {
_contentScale = platformOperations.getDpiScale(this)
isInit = true
}
}
protected open fun contentScaleChanged() = Unit
override fun paint(g: Graphics) {
val contentScale = platformOperations.getDpiScale(this)
if (contentScale != _contentScale) {
_contentScale = contentScale
contentScaleChanged()
}
display()
}
......@@ -34,7 +61,7 @@ abstract class HardwareLayer : Canvas(), Drawable {
external get
override val contentScale: Float
get() = platformOperations.getDpiScale(this)
get() = _contentScale!!
val absoluteX: Int
get() = convertPoint(this, x, y, getRootPane(this)).x
......
package org.jetbrains.skiko
import java.awt.Component
import org.jetbrains.skija.BackendRenderTarget
import org.jetbrains.skija.Canvas
import org.jetbrains.skija.ColorSpace
import org.jetbrains.skija.DirectContext
import org.jetbrains.skija.FramebufferFormat
import org.jetbrains.skija.Rect
import org.jetbrains.skija.Surface
import org.jetbrains.skija.SurfaceColorFormat
import org.jetbrains.skija.SurfaceOrigin
import org.jetbrains.skija.ClipMode
import kotlinx.coroutines.runBlocking
import org.jetbrains.skija.*
import javax.swing.SwingUtilities.isEventDispatchThread
private class SkijaState {
val bleachConstant = if (hostOs == OS.MacOS) 0 else -1
......@@ -26,28 +18,34 @@ private class SkijaState {
}
interface SkiaRenderer {
fun onInit()
fun onRender(canvas: Canvas, width: Int, height: Int)
fun onReshape(width: Int, height: Int)
fun onDispose()
suspend fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long)
}
open class SkiaLayer() : HardwareLayer() {
private class PictureHolder(val instance: Picture, val width: Int, val height: Int)
open class SkiaLayer : HardwareLayer() {
open val api: GraphicsApi = GraphicsApi.OPENGL
var renderer: SkiaRenderer? = null
val clipComponets = mutableListOf<ClipRectangle>()
val clipComponents = mutableListOf<ClipRectangle>()
private val skijaState = SkijaState()
protected var inited: Boolean = false
fun reinit() {
inited = false
}
@Volatile
private var isDisposed = false
@Volatile
private var picture: PictureHolder? = null
private val pictureRecorder = PictureRecorder()
private val pictureLock = Any()
override fun disposeLayer() {
check(!isDisposed)
check(isEventDispatchThread())
picture?.instance?.close()
pictureRecorder.close()
isDisposed = true
super.disposeLayer()
renderer?.onDispose()
}
private val fpsCounter = FPSCounter(
......@@ -56,11 +54,43 @@ open class SkiaLayer() : HardwareLayer() {
)
override fun draw() {
runBlocking {
performUpdate(System.nanoTime())
}
performDraw()
}
private suspend fun performUpdate(nanoTime: Long) {
check(!isDisposed)
//check(isEventDispatchThread()) // TODO run from AWT Thread
if (System.getProperty("skiko.fps.enabled") == "true") {
fpsCounter.tick()
}
if (!inited) {
val pictureWidth = (width * contentScale).toInt().coerceAtLeast(0)
val pictureHeight = (height * contentScale).toInt().coerceAtLeast(0)
val bounds = Rect.makeWH(pictureWidth.toFloat(), pictureHeight.toFloat())!!
val canvas = pictureRecorder.beginRecording(bounds)!!
// clipping
for (component in clipComponents) {
canvas.clipRectBy(component)
}
renderer?.onRender(canvas, pictureWidth, pictureHeight, nanoTime)
synchronized(pictureLock) {
picture?.instance?.close()
val picture = pictureRecorder.finishRecordingAsPicture()
this.picture = PictureHolder(picture, pictureWidth, pictureHeight)
}
}
private fun performDraw() {
check(!isDisposed)
if (skijaState.context == null) {
skijaState.context = when (api) {
GraphicsApi.OPENGL -> makeGLContext()
......@@ -68,27 +98,23 @@ open class SkiaLayer() : HardwareLayer() {
else -> TODO("Unsupported yet")
}
}
renderer?.onInit()
inited = true
renderer?.onReshape(width, height)
}
initSkija()
skijaState.apply {
canvas!!.clear(bleachConstant)
// cliping
for (component in clipComponets) {
clipRectBy(component)
synchronized(pictureLock) {
val picture = picture
if (picture != null) {
canvas!!.drawPicture(picture.instance)
}
}
renderer?.onRender(canvas!!, width, height)
context!!.flush()
}
}
private fun clipRectBy(rectangle: ClipRectangle) {
skijaState.apply {
canvas!!.clipRect(
private fun Canvas.clipRectBy(rectangle: ClipRectangle) {
clipRect(
Rect.makeLTRB(
rectangle.x,
rectangle.y,
......@@ -99,25 +125,25 @@ open class SkiaLayer() : HardwareLayer() {
true
)
}
}
private fun initSkija() {
val dpi = contentScale
initRenderTarget(dpi)
initRenderTarget()
initSurface()
scaleCanvas(dpi)
}
private fun initRenderTarget(dpi: Float) {
private fun initRenderTarget() {
skijaState.apply {
clear()
val dpi = contentScale
val width = (width * dpi).toInt().coerceAtLeast(0)
val height = (height * dpi).toInt().coerceAtLeast(0)
renderTarget = when (api) {
GraphicsApi.OPENGL -> {
val gl = OpenGLApi.instance
val fbId = gl.glGetIntegerv(gl.GL_DRAW_FRAMEBUFFER_BINDING)
makeGLRenderTarget(
(width * dpi).toInt(),
(height * dpi).toInt(),
width,
height,
0,
8,
fbId,
......@@ -125,8 +151,8 @@ open class SkiaLayer() : HardwareLayer() {
)
}
GraphicsApi.METAL -> makeMetalRenderTarget(
(width * dpi).toInt(),
(height * dpi).toInt(),
width,
height,
0
)
else -> TODO("Unsupported yet")
......@@ -146,10 +172,4 @@ open class SkiaLayer() : HardwareLayer() {
canvas = surface!!.canvas
}
}
protected open fun scaleCanvas(dpi: Float) {
skijaState.apply {
canvas!!.scale(dpi, dpi)
}
}
}
......@@ -5,7 +5,6 @@ import java.awt.Component
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import javax.swing.JLayeredPane
import org.jetbrains.skija.Rect
open class SkiaPanel: JLayeredPane {
val layer = SkiaLayer()
......@@ -16,7 +15,7 @@ open class SkiaPanel: JLayeredPane {
}
override fun add(component: Component): Component {
layer.clipComponets.add(ClipComponent(component))
layer.clipComponents.add(ClipComponent(component))
return super.add(component, Integer.valueOf(0))
}
......@@ -26,7 +25,6 @@ open class SkiaPanel: JLayeredPane {
addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) {
layer.reinit()
layer.setSize(width, height)
}
})
......
......@@ -9,12 +9,6 @@ open class SkiaWindow : JFrame() {
init {
contentPane.add(layer)
addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) {
layer.reinit()
}
})
}
fun display() {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment