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 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.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.FontCollection
import org.jetbrains.skija.paragraph.ParagraphBuilder import org.jetbrains.skija.paragraph.ParagraphBuilder
import org.jetbrains.skija.paragraph.ParagraphStyle import org.jetbrains.skija.paragraph.ParagraphStyle
import org.jetbrains.skija.paragraph.TextStyle import org.jetbrains.skija.paragraph.TextStyle
import java.awt.event.ActionEvent import org.jetbrains.skiko.SkiaLayer
import java.awt.event.ActionListener import org.jetbrains.skiko.SkiaRenderer
import java.awt.event.KeyEvent import org.jetbrains.skiko.SkiaWindow
import java.awt.Toolkit import java.awt.Toolkit
import javax.swing.JFrame import java.awt.event.*
import javax.swing.JMenu import javax.swing.*
import javax.swing.JMenuBar import kotlin.math.cos
import javax.swing.JMenuItem import kotlin.math.sin
import javax.swing.JOptionPane
import javax.swing.KeyStroke
fun main(args: Array<String>) { fun main(args: Array<String>) {
createWindow("First window") createWindow("First window")
} }
fun createWindow(title: String) { fun createWindow(title: String) = SwingUtilities.invokeLater {
var mouseX = 0 var mouseX = 0
var mouseY = 0 var mouseY = 0
...@@ -68,8 +57,8 @@ fun createWindow(title: String) { ...@@ -68,8 +57,8 @@ fun createWindow(title: String) {
val state = State() val state = State()
state.text = title state.text = title
window.layer.renderer = Renderer { window.layer.renderer = Renderer(window.layer) {
renderer, w, h -> displayScene(renderer, w, h, mouseX, mouseY, state) renderer, w, h, nanoTime -> displayScene(renderer, w, h, nanoTime, mouseX, mouseY, state)
} }
window.layer.addMouseMotionListener(object : MouseMotionAdapter() { window.layer.addMouseMotionListener(object : MouseMotionAdapter() {
...@@ -85,7 +74,10 @@ fun createWindow(title: String) { ...@@ -85,7 +74,10 @@ fun createWindow(title: String) {
window.setVisible(true) 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 typeface = Typeface.makeFromFile("fonts/JetBrainsMono-Regular.ttf")
val font = Font(typeface, 40f) val font = Font(typeface, 40f)
val paint = Paint().apply { val paint = Paint().apply {
...@@ -96,18 +88,11 @@ class Renderer(val displayScene: (Renderer, Int, Int) -> Unit): SkiaRenderer { ...@@ -96,18 +88,11 @@ class Renderer(val displayScene: (Renderer, Int, Int) -> Unit): SkiaRenderer {
var canvas: Canvas? = null var canvas: Canvas? = null
override fun onInit() { override suspend fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
}
override fun onDispose() {
}
override fun onReshape(width: Int, height: Int) {
}
override fun onRender(canvas: Canvas, width: Int, height: Int) {
this.canvas = canvas 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 { ...@@ -116,7 +101,10 @@ class State {
var text: String = "Hello Skija" 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 canvas = renderer.canvas!!
val watchFill = Paint().setColor(0xFFFFFFFF.toInt()) val watchFill = Paint().setColor(0xFFFFFFFF.toInt())
val watchStroke = Paint().setColor(0xFF000000.toInt()).setMode(PaintMode.STROKE).setStrokeWidth(1f) 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 ...@@ -140,7 +128,7 @@ fun displayScene(renderer: Renderer, width: Int, height: Int, xpos: Int, ypos: I
) )
angle += (2.0 * Math.PI / 12.0).toFloat() angle += (2.0 * Math.PI / 12.0).toFloat()
} }
val time = System.currentTimeMillis() % 60000 + val time = (nanoTime / 1E6) % 60000 +
(x.toFloat() / width * 5000).toLong() + (x.toFloat() / width * 5000).toLong() +
(y.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 ...@@ -160,8 +148,6 @@ fun displayScene(renderer: Renderer, width: Int, height: Int, xpos: Int, ypos: I
val text = "${state.text} ${state.frame++}!" val text = "${state.text} ${state.frame++}!"
canvas.drawString(text, xpos.toFloat(), ypos.toFloat(), renderer.font, renderer.paint) canvas.drawString(text, xpos.toFloat(), ypos.toFloat(), renderer.font, renderer.paint)
val fontCollection = FontCollection()
.setDefaultFontManager(FontMgr.getDefault())
val style = ParagraphStyle() val style = ParagraphStyle()
val paragraph = ParagraphBuilder(style, fontCollection) val paragraph = ParagraphBuilder(style, fontCollection)
.pushStyle(TextStyle().setColor(0xFF000000.toInt())) .pushStyle(TextStyle().setColor(0xFF000000.toInt()))
......
...@@ -63,7 +63,7 @@ fun SwingSkia() { ...@@ -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() { panel.layer.addMouseMotionListener(object : MouseMotionAdapter() {
override fun mouseMoved(event: MouseEvent) { override fun mouseMoved(event: MouseEvent) {
......
package org.jetbrains.skiko package org.jetbrains.skiko
import java.awt.Graphics
import java.awt.Canvas import java.awt.Canvas
import java.awt.Graphics
import java.awt.event.HierarchyEvent
import javax.swing.SwingUtilities.convertPoint import javax.swing.SwingUtilities.convertPoint
import javax.swing.SwingUtilities.getRootPane import javax.swing.SwingUtilities.getRootPane
abstract class HardwareLayer : Canvas(), Drawable { abstract class HardwareLayer : Canvas(), Drawable {
companion object { companion object {
init { init {
Library.load() 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) { override fun paint(g: Graphics) {
val contentScale = platformOperations.getDpiScale(this)
if (contentScale != _contentScale) {
_contentScale = contentScale
contentScaleChanged()
}
display() display()
} }
...@@ -34,7 +61,7 @@ abstract class HardwareLayer : Canvas(), Drawable { ...@@ -34,7 +61,7 @@ abstract class HardwareLayer : Canvas(), Drawable {
external get external get
override val contentScale: Float override val contentScale: Float
get() = platformOperations.getDpiScale(this) get() = _contentScale!!
val absoluteX: Int val absoluteX: Int
get() = convertPoint(this, x, y, getRootPane(this)).x get() = convertPoint(this, x, y, getRootPane(this)).x
......
package org.jetbrains.skiko package org.jetbrains.skiko
import java.awt.Component import kotlinx.coroutines.runBlocking
import org.jetbrains.skija.BackendRenderTarget import org.jetbrains.skija.*
import org.jetbrains.skija.Canvas import javax.swing.SwingUtilities.isEventDispatchThread
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
private class SkijaState { private class SkijaState {
val bleachConstant = if (hostOs == OS.MacOS) 0 else -1 val bleachConstant = if (hostOs == OS.MacOS) 0 else -1
...@@ -26,28 +18,34 @@ private class SkijaState { ...@@ -26,28 +18,34 @@ private class SkijaState {
} }
interface SkiaRenderer { interface SkiaRenderer {
fun onInit() suspend fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long)
fun onRender(canvas: Canvas, width: Int, height: Int)
fun onReshape(width: Int, height: Int)
fun onDispose()
} }
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 open val api: GraphicsApi = GraphicsApi.OPENGL
var renderer: SkiaRenderer? = null var renderer: SkiaRenderer? = null
val clipComponets = mutableListOf<ClipRectangle>() val clipComponents = mutableListOf<ClipRectangle>()
private val skijaState = SkijaState() private val skijaState = SkijaState()
protected var inited: Boolean = false
fun reinit() { @Volatile
inited = false private var isDisposed = false
}
@Volatile
private var picture: PictureHolder? = null
private val pictureRecorder = PictureRecorder()
private val pictureLock = Any()
override fun disposeLayer() { override fun disposeLayer() {
check(!isDisposed)
check(isEventDispatchThread())
picture?.instance?.close()
pictureRecorder.close()
isDisposed = true
super.disposeLayer() super.disposeLayer()
renderer?.onDispose()
} }
private val fpsCounter = FPSCounter( private val fpsCounter = FPSCounter(
...@@ -56,11 +54,43 @@ open class SkiaLayer() : HardwareLayer() { ...@@ -56,11 +54,43 @@ open class SkiaLayer() : HardwareLayer() {
) )
override fun draw() { 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") { if (System.getProperty("skiko.fps.enabled") == "true") {
fpsCounter.tick() 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) { if (skijaState.context == null) {
skijaState.context = when (api) { skijaState.context = when (api) {
GraphicsApi.OPENGL -> makeGLContext() GraphicsApi.OPENGL -> makeGLContext()
...@@ -68,27 +98,23 @@ open class SkiaLayer() : HardwareLayer() { ...@@ -68,27 +98,23 @@ open class SkiaLayer() : HardwareLayer() {
else -> TODO("Unsupported yet") else -> TODO("Unsupported yet")
} }
} }
renderer?.onInit()
inited = true
renderer?.onReshape(width, height)
}
initSkija() initSkija()
skijaState.apply { skijaState.apply {
canvas!!.clear(bleachConstant) canvas!!.clear(bleachConstant)
synchronized(pictureLock) {
// cliping val picture = picture
for (component in clipComponets) { if (picture != null) {
clipRectBy(component) canvas!!.drawPicture(picture.instance)
}
} }
renderer?.onRender(canvas!!, width, height)
context!!.flush() context!!.flush()
} }
} }
private fun clipRectBy(rectangle: ClipRectangle) { private fun Canvas.clipRectBy(rectangle: ClipRectangle) {
skijaState.apply { clipRect(
canvas!!.clipRect(
Rect.makeLTRB( Rect.makeLTRB(
rectangle.x, rectangle.x,
rectangle.y, rectangle.y,
...@@ -99,25 +125,25 @@ open class SkiaLayer() : HardwareLayer() { ...@@ -99,25 +125,25 @@ open class SkiaLayer() : HardwareLayer() {
true true
) )
} }
}
private fun initSkija() { private fun initSkija() {
val dpi = contentScale initRenderTarget()
initRenderTarget(dpi)
initSurface() initSurface()
scaleCanvas(dpi)
} }
private fun initRenderTarget(dpi: Float) { private fun initRenderTarget() {
skijaState.apply { skijaState.apply {
clear() clear()
val dpi = contentScale
val width = (width * dpi).toInt().coerceAtLeast(0)
val height = (height * dpi).toInt().coerceAtLeast(0)
renderTarget = when (api) { renderTarget = when (api) {
GraphicsApi.OPENGL -> { GraphicsApi.OPENGL -> {
val gl = OpenGLApi.instance val gl = OpenGLApi.instance
val fbId = gl.glGetIntegerv(gl.GL_DRAW_FRAMEBUFFER_BINDING) val fbId = gl.glGetIntegerv(gl.GL_DRAW_FRAMEBUFFER_BINDING)
makeGLRenderTarget( makeGLRenderTarget(
(width * dpi).toInt(), width,
(height * dpi).toInt(), height,
0, 0,
8, 8,
fbId, fbId,
...@@ -125,8 +151,8 @@ open class SkiaLayer() : HardwareLayer() { ...@@ -125,8 +151,8 @@ open class SkiaLayer() : HardwareLayer() {
) )
} }
GraphicsApi.METAL -> makeMetalRenderTarget( GraphicsApi.METAL -> makeMetalRenderTarget(
(width * dpi).toInt(), width,
(height * dpi).toInt(), height,
0 0
) )
else -> TODO("Unsupported yet") else -> TODO("Unsupported yet")
...@@ -146,10 +172,4 @@ open class SkiaLayer() : HardwareLayer() { ...@@ -146,10 +172,4 @@ open class SkiaLayer() : HardwareLayer() {
canvas = surface!!.canvas canvas = surface!!.canvas
} }
} }
protected open fun scaleCanvas(dpi: Float) {
skijaState.apply {
canvas!!.scale(dpi, dpi)
}
}
} }
...@@ -5,7 +5,6 @@ import java.awt.Component ...@@ -5,7 +5,6 @@ import java.awt.Component
import java.awt.event.ComponentAdapter import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent import java.awt.event.ComponentEvent
import javax.swing.JLayeredPane import javax.swing.JLayeredPane
import org.jetbrains.skija.Rect
open class SkiaPanel: JLayeredPane { open class SkiaPanel: JLayeredPane {
val layer = SkiaLayer() val layer = SkiaLayer()
...@@ -16,7 +15,7 @@ open class SkiaPanel: JLayeredPane { ...@@ -16,7 +15,7 @@ open class SkiaPanel: JLayeredPane {
} }
override fun add(component: Component): Component { override fun add(component: Component): Component {
layer.clipComponets.add(ClipComponent(component)) layer.clipComponents.add(ClipComponent(component))
return super.add(component, Integer.valueOf(0)) return super.add(component, Integer.valueOf(0))
} }
...@@ -26,7 +25,6 @@ open class SkiaPanel: JLayeredPane { ...@@ -26,7 +25,6 @@ open class SkiaPanel: JLayeredPane {
addComponentListener(object : ComponentAdapter() { addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) { override fun componentResized(e: ComponentEvent) {
layer.reinit()
layer.setSize(width, height) layer.setSize(width, height)
} }
}) })
......
...@@ -9,12 +9,6 @@ open class SkiaWindow : JFrame() { ...@@ -9,12 +9,6 @@ open class SkiaWindow : JFrame() {
init { init {
contentPane.add(layer) contentPane.add(layer)
addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent) {
layer.reinit()
}
})
} }
fun display() { 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