Unverified Commit dfe58606 authored by Igor Demin's avatar Igor Demin Committed by GitHub

Add ability to render the first frame before window showing (#109)

```
window.preferredSize = Dimension(800, 600)
window.pack()
window.layer.awaitRedraw()
window.isVisible = true
```
parent aba0903d
package SkijaInjectSample package SkijaInjectSample
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.yield
import org.jetbrains.skija.* import org.jetbrains.skija.*
import org.jetbrains.skija.paragraph.FontCollection import org.jetbrains.skija.paragraph.FontCollection
import org.jetbrains.skija.paragraph.ParagraphBuilder import org.jetbrains.skija.paragraph.ParagraphBuilder
...@@ -8,6 +12,7 @@ import org.jetbrains.skija.paragraph.TextStyle ...@@ -8,6 +12,7 @@ import org.jetbrains.skija.paragraph.TextStyle
import org.jetbrains.skiko.SkiaLayer import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.SkiaRenderer import org.jetbrains.skiko.SkiaRenderer
import org.jetbrains.skiko.SkiaWindow import org.jetbrains.skiko.SkiaWindow
import java.awt.Dimension
import java.awt.Toolkit import java.awt.Toolkit
import java.awt.event.* import java.awt.event.*
import javax.swing.* import javax.swing.*
...@@ -20,7 +25,7 @@ fun main(args: Array<String>) { ...@@ -20,7 +25,7 @@ fun main(args: Array<String>) {
} }
} }
fun createWindow(title: String) = SwingUtilities.invokeLater { fun createWindow(title: String) = runBlocking(Dispatchers.Swing) {
var mouseX = 0 var mouseX = 0
var mouseY = 0 var mouseY = 0
...@@ -74,9 +79,11 @@ fun createWindow(title: String) = SwingUtilities.invokeLater { ...@@ -74,9 +79,11 @@ fun createWindow(title: String) = SwingUtilities.invokeLater {
} }
}) })
// MANDATORY: set window size before calling setVisible(true) // MANDATORY: set window preferred size before calling pack()
window.setSize(800, 600) window.preferredSize = Dimension(800, 600)
window.setVisible(true) window.pack()
window.layer.awaitRedraw()
window.isVisible = true
} }
class Renderer( class Renderer(
......
...@@ -18,6 +18,7 @@ import org.junit.Assume.assumeTrue ...@@ -18,6 +18,7 @@ import org.junit.Assume.assumeTrue
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import java.awt.Color import java.awt.Color
import java.awt.Dimension
import java.awt.Robot import java.awt.Robot
import java.awt.event.WindowEvent import java.awt.event.WindowEvent
import javax.swing.JFrame import javax.swing.JFrame
...@@ -72,6 +73,32 @@ class SkiaWindowTest { ...@@ -72,6 +73,32 @@ class SkiaWindowTest {
} }
} }
@Test
fun `render single window before window show`() = swingTest {
val window = SkiaWindow()
try {
window.setLocation(200, 200)
window.preferredSize = Dimension(400, 200)
window.defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
val renderer = RectRenderer(window.layer, 200, 100, Color.RED)
window.layer.renderer = renderer
window.isUndecorated = true
window.pack()
window.layer.awaitRedraw()
window.isVisible = true
delay(1000)
screenshots.assert(window.bounds, "frame1")
renderer.rectWidth = 100
window.layer.needRedraw()
delay(1000)
screenshots.assert(window.bounds, "frame2")
} finally {
window.close()
}
}
@Test @Test
fun `resize window`() = swingTest { fun `resize window`() = swingTest {
val window = SkiaWindow() val window = SkiaWindow()
......
...@@ -3,8 +3,11 @@ package org.jetbrains.skiko ...@@ -3,8 +3,11 @@ package org.jetbrains.skiko
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import kotlin.coroutines.Continuation
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume
/** /**
* Dispatch frame after call of [scheduleFrame]. * Dispatch frame after call of [scheduleFrame].
...@@ -31,6 +34,7 @@ class FrameDispatcher( ...@@ -31,6 +34,7 @@ class FrameDispatcher(
frameChannel.receive() frameChannel.receive()
frameScheduled = false frameScheduled = false
onFrame() onFrame()
resumeFrameAwaiters(isActive = true)
// As per `yield()` documentation: // As per `yield()` documentation:
// //
// For other dispatchers (not == Unconfined) , this function calls [CoroutineDispatcher.dispatch] and // For other dispatchers (not == Unconfined) , this function calls [CoroutineDispatcher.dispatch] and
...@@ -41,6 +45,12 @@ class FrameDispatcher( ...@@ -41,6 +45,12 @@ class FrameDispatcher(
} }
} }
init {
job.invokeOnCompletion {
resumeFrameAwaiters(isActive = false)
}
}
fun cancel() { fun cancel() {
job.cancel() job.cancel()
} }
...@@ -59,4 +69,35 @@ class FrameDispatcher( ...@@ -59,4 +69,35 @@ class FrameDispatcher(
frameChannel.offer(Unit) frameChannel.offer(Unit)
} }
} }
private val frameAwaiters = mutableListOf<Continuation<Boolean>>()
/**
* Schedule next frame to render in the frame loop, and wait it to finish.
*
* If frame loop was completed (cancelled or there was an exception inside it) then don't wait and immediately continue execution.
*
* @return true if frame loop is active, false if it was completed.
*/
suspend fun awaitFrame(): Boolean {
return if (job.isActive) {
suspendCancellableCoroutine { continuation ->
synchronized(frameAwaiters) {
frameAwaiters.add(continuation)
}
scheduleFrame()
}
} else {
false
}
}
private fun resumeFrameAwaiters(isActive: Boolean) {
synchronized(frameAwaiters) {
for (frameAwaiter in frameAwaiters) {
frameAwaiter.resume(isActive)
}
frameAwaiters.clear()
}
}
} }
\ No newline at end of file
package org.jetbrains.skiko package org.jetbrains.skiko
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext
import org.jetbrains.skija.Canvas import org.jetbrains.skija.Canvas
import org.jetbrains.skija.ClipMode import org.jetbrains.skija.ClipMode
import org.jetbrains.skija.Picture import org.jetbrains.skija.Picture
...@@ -53,17 +57,18 @@ open class SkiaLayer( ...@@ -53,17 +57,18 @@ open class SkiaLayer(
add(backedLayer) add(backedLayer)
@Suppress("LeakingThis") @Suppress("LeakingThis")
backedLayer.addHierarchyListener { backedLayer.addHierarchyListener {
if (it.changeFlags and HierarchyEvent.SHOWING_CHANGED.toLong() != 0L) { if (it.changeFlags and HierarchyEvent.DISPLAYABILITY_CHANGED.toLong() != 0L) {
checkIsShowing() checkInit()
} }
} }
} }
private var isInited = false private val onInit = CompletableDeferred<Unit>()
private val isInited get() = onInit.isCompleted
private var isRendering = false private var isRendering = false
private fun checkIsShowing() { private fun checkInit() {
if (!isInited && isShowing) { if (!isInited && isDisplayable) {
backedLayer.defineContentScale() backedLayer.defineContentScale()
init() init()
} }
...@@ -113,7 +118,7 @@ open class SkiaLayer( ...@@ -113,7 +118,7 @@ open class SkiaLayer(
renderApi = fallbackRenderApiQueue.removeAt(0) renderApi = fallbackRenderApiQueue.removeAt(0)
contextHandler = createContextHandler(this, renderApi) contextHandler = createContextHandler(this, renderApi)
redrawer = platformOperations.createRedrawer(this, renderApi, properties) redrawer = platformOperations.createRedrawer(this, renderApi, properties)
isInited = true onInit.complete(Unit)
} }
private val stateHandlers = private val stateHandlers =
...@@ -252,6 +257,20 @@ open class SkiaLayer( ...@@ -252,6 +257,20 @@ open class SkiaLayer(
redrawer?.needRedraw() redrawer?.needRedraw()
} }
/**
* Redraw on the next animation Frame (on vsync signal if vsync is enabled),
* and wait the frame to finish.
*
* @return true if frame was rendered, false if rendering loop was completed (cancelled or there was an exception inside it)
*/
suspend fun awaitRedraw(): Boolean {
return withContext(Dispatchers.Swing) {
check(!isDisposed)
onInit.await()
redrawer!!.awaitRedraw()
}
}
@Suppress("LeakingThis") @Suppress("LeakingThis")
private val fpsCounter = defaultFPSCounter(this) private val fpsCounter = defaultFPSCounter(this)
......
...@@ -32,6 +32,10 @@ internal class AngleRedrawer( ...@@ -32,6 +32,10 @@ internal class AngleRedrawer(
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() { override fun redrawImmediately() {
check(!isDisposed) check(!isDisposed)
update(System.nanoTime()) update(System.nanoTime())
......
...@@ -3,8 +3,8 @@ package org.jetbrains.skiko.redrawer ...@@ -3,8 +3,8 @@ package org.jetbrains.skiko.redrawer
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jetbrains.skija.Surface
import org.jetbrains.skija.DirectContext import org.jetbrains.skija.DirectContext
import org.jetbrains.skija.Surface
import org.jetbrains.skiko.FrameDispatcher import org.jetbrains.skiko.FrameDispatcher
import org.jetbrains.skiko.GpuPriority import org.jetbrains.skiko.GpuPriority
import org.jetbrains.skiko.SkiaLayer import org.jetbrains.skiko.SkiaLayer
...@@ -33,6 +33,10 @@ internal class Direct3DRedrawer( ...@@ -33,6 +33,10 @@ internal class Direct3DRedrawer(
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() { override fun redrawImmediately() {
check(!isDisposed) check(!isDisposed)
// TODO now we wait until previous layer.draw is finished. it ends only on the next vsync. // TODO now we wait until previous layer.draw is finished. it ends only on the next vsync.
......
...@@ -5,9 +5,9 @@ import kotlinx.coroutines.Dispatchers ...@@ -5,9 +5,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.swing.Swing
import org.jetbrains.skiko.DrawingSurface import org.jetbrains.skiko.DrawingSurface
import org.jetbrains.skiko.FrameDispatcher import org.jetbrains.skiko.FrameDispatcher
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.HardwareLayer import org.jetbrains.skiko.HardwareLayer
import org.jetbrains.skiko.OpenGLApi import org.jetbrains.skiko.OpenGLApi
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.SkiaLayerProperties import org.jetbrains.skiko.SkiaLayerProperties
import org.jetbrains.skiko.getDrawingSurface import org.jetbrains.skiko.getDrawingSurface
...@@ -34,6 +34,10 @@ internal class LinuxOpenGLRedrawer( ...@@ -34,6 +34,10 @@ internal class LinuxOpenGLRedrawer(
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() = layer.backedLayer.lockDrawingSurface { override fun redrawImmediately() = layer.backedLayer.lockDrawingSurface {
check(!isDisposed) check(!isDisposed)
update(System.nanoTime()) update(System.nanoTime())
......
...@@ -3,8 +3,8 @@ package org.jetbrains.skiko.redrawer ...@@ -3,8 +3,8 @@ package org.jetbrains.skiko.redrawer
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.swing.Swing
import org.jetbrains.skiko.FrameDispatcher import org.jetbrains.skiko.FrameDispatcher
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.OpenGLApi import org.jetbrains.skiko.OpenGLApi
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.SkiaLayerProperties import org.jetbrains.skiko.SkiaLayerProperties
import org.jetbrains.skiko.Task import org.jetbrains.skiko.Task
import org.jetbrains.skiko.useDrawingSurfacePlatformInfo import org.jetbrains.skiko.useDrawingSurfacePlatformInfo
...@@ -121,6 +121,10 @@ internal class MacOsOpenGLRedrawer( ...@@ -121,6 +121,10 @@ internal class MacOsOpenGLRedrawer(
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() { override fun redrawImmediately() {
layer.update(System.nanoTime()) layer.update(System.nanoTime())
......
package org.jetbrains.skiko.redrawer package org.jetbrains.skiko.redrawer
import kotlinx.coroutines.* import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext
import org.jetbrains.skija.BackendRenderTarget import org.jetbrains.skija.BackendRenderTarget
import org.jetbrains.skija.DirectContext import org.jetbrains.skija.DirectContext
import org.jetbrains.skiko.FrameDispatcher import org.jetbrains.skiko.FrameDispatcher
...@@ -39,6 +41,10 @@ internal class MetalRedrawer( ...@@ -39,6 +41,10 @@ internal class MetalRedrawer(
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() { override fun redrawImmediately() {
check(!isDisposed) check(!isDisposed)
// TODO: now we wait until previous `layer.draw` is finished. it ends only on the next vsync. // TODO: now we wait until previous `layer.draw` is finished. it ends only on the next vsync.
......
...@@ -4,5 +4,6 @@ interface Redrawer { ...@@ -4,5 +4,6 @@ interface Redrawer {
fun dispose() fun dispose()
fun needRedraw() fun needRedraw()
fun redrawImmediately() fun redrawImmediately()
suspend fun awaitRedraw(): Boolean
fun syncSize() = Unit fun syncSize() = Unit
} }
\ No newline at end of file
...@@ -24,6 +24,10 @@ internal class SoftwareRedrawer( ...@@ -24,6 +24,10 @@ internal class SoftwareRedrawer(
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() { override fun redrawImmediately() {
layer.update(System.nanoTime()) layer.update(System.nanoTime())
if (layer.prepareDrawContext()) { if (layer.prepareDrawContext()) {
......
...@@ -5,8 +5,8 @@ import kotlinx.coroutines.Dispatchers ...@@ -5,8 +5,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.swing.Swing import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jetbrains.skiko.FrameDispatcher import org.jetbrains.skiko.FrameDispatcher
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.OpenGLApi import org.jetbrains.skiko.OpenGLApi
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.SkiaLayerProperties import org.jetbrains.skiko.SkiaLayerProperties
import org.jetbrains.skiko.useDrawingSurfacePlatformInfo import org.jetbrains.skiko.useDrawingSurfacePlatformInfo
...@@ -39,6 +39,10 @@ internal class WindowsOpenGLRedrawer( ...@@ -39,6 +39,10 @@ internal class WindowsOpenGLRedrawer(
frameDispatcher.scheduleFrame() frameDispatcher.scheduleFrame()
} }
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() { override fun redrawImmediately() {
check(!isDisposed) check(!isDisposed)
update(System.nanoTime()) update(System.nanoTime())
......
package org.jetbrains.skiko package org.jetbrains.skiko
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineScope import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.yield
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext
import kotlin.test.assertFalse
import kotlin.test.assertTrue
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class FrameDispatcherTest { class FrameDispatcherTest {
...@@ -185,6 +197,75 @@ class FrameDispatcherTest { ...@@ -185,6 +197,75 @@ class FrameDispatcherTest {
assertEquals(listOf("frame0", "task", "frame1"), history) assertEquals(listOf("frame0", "task", "frame1"), history)
} }
@Test
fun `await frame`() = test {
val scope = CoroutineScope(coroutineContext)
val frameDispatcher = FrameDispatcher(scope = scope) {
frameCount++
}
frameDispatcher.awaitFrame()
assertEquals(1, frameCount)
frameDispatcher.awaitFrame()
assertEquals(2, frameCount)
frameDispatcher.awaitFrame()
assertEquals(3, frameCount)
repeat(100) {
yield()
}
assertEquals(3, frameCount)
}
@Test
fun `await active frame`() = test {
val scope = CoroutineScope(coroutineContext)
val frameDispatcher = FrameDispatcher(scope = scope) {}
val isActive = frameDispatcher.awaitFrame()
assertTrue(isActive)
}
@Test
fun `await failed frame`() = test {
val ignoreExceptionHandler = object :
AbstractCoroutineContextElement(CoroutineExceptionHandler),
CoroutineExceptionHandler {
override fun handleException(context: CoroutineContext, exception: Throwable) = Unit
}
val scope = CoroutineScope(coroutineContext + ignoreExceptionHandler)
val frameDispatcher = FrameDispatcher(scope = scope) {
throw RuntimeException()
}
val isActive = frameDispatcher.awaitFrame()
assertFalse(isActive)
}
@Test
fun `cancel dispatcher before awaitFrame`() = test {
val scope = CoroutineScope(coroutineContext)
val frameDispatcher = FrameDispatcher(scope = scope) {}
frameDispatcher.cancel()
val isActive = frameDispatcher.awaitFrame()
assertFalse(isActive)
}
@Test
fun `cancel scope before awaitFrame`() = test {
val scope = CoroutineScope(coroutineContext)
val frameDispatcher = FrameDispatcher(scope = scope) {}
scope.cancel()
val isActive = frameDispatcher.awaitFrame()
assertFalse(isActive)
}
private fun test( private fun test(
block: suspend TestCoroutineScope.() -> Unit block: suspend TestCoroutineScope.() -> Unit
) = runBlockingTest { ) = runBlockingTest {
......
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