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
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.paragraph.FontCollection
import org.jetbrains.skija.paragraph.ParagraphBuilder
......@@ -8,6 +12,7 @@ import org.jetbrains.skija.paragraph.TextStyle
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.SkiaRenderer
import org.jetbrains.skiko.SkiaWindow
import java.awt.Dimension
import java.awt.Toolkit
import java.awt.event.*
import javax.swing.*
......@@ -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 mouseY = 0
......@@ -74,9 +79,11 @@ fun createWindow(title: String) = SwingUtilities.invokeLater {
}
})
// MANDATORY: set window size before calling setVisible(true)
window.setSize(800, 600)
window.setVisible(true)
// MANDATORY: set window preferred size before calling pack()
window.preferredSize = Dimension(800, 600)
window.pack()
window.layer.awaitRedraw()
window.isVisible = true
}
class Renderer(
......
......@@ -18,6 +18,7 @@ import org.junit.Assume.assumeTrue
import org.junit.Rule
import org.junit.Test
import java.awt.Color
import java.awt.Dimension
import java.awt.Robot
import java.awt.event.WindowEvent
import javax.swing.JFrame
......@@ -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
fun `resize window`() = swingTest {
val window = SkiaWindow()
......
......@@ -3,8 +3,11 @@ package org.jetbrains.skiko
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.yield
import kotlin.coroutines.Continuation
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume
/**
* Dispatch frame after call of [scheduleFrame].
......@@ -31,6 +34,7 @@ class FrameDispatcher(
frameChannel.receive()
frameScheduled = false
onFrame()
resumeFrameAwaiters(isActive = true)
// As per `yield()` documentation:
//
// For other dispatchers (not == Unconfined) , this function calls [CoroutineDispatcher.dispatch] and
......@@ -41,6 +45,12 @@ class FrameDispatcher(
}
}
init {
job.invokeOnCompletion {
resumeFrameAwaiters(isActive = false)
}
}
fun cancel() {
job.cancel()
}
......@@ -59,4 +69,35 @@ class FrameDispatcher(
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
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.ClipMode
import org.jetbrains.skija.Picture
......@@ -53,17 +57,18 @@ open class SkiaLayer(
add(backedLayer)
@Suppress("LeakingThis")
backedLayer.addHierarchyListener {
if (it.changeFlags and HierarchyEvent.SHOWING_CHANGED.toLong() != 0L) {
checkIsShowing()
if (it.changeFlags and HierarchyEvent.DISPLAYABILITY_CHANGED.toLong() != 0L) {
checkInit()
}
}
}
private var isInited = false
private val onInit = CompletableDeferred<Unit>()
private val isInited get() = onInit.isCompleted
private var isRendering = false
private fun checkIsShowing() {
if (!isInited && isShowing) {
private fun checkInit() {
if (!isInited && isDisplayable) {
backedLayer.defineContentScale()
init()
}
......@@ -113,7 +118,7 @@ open class SkiaLayer(
renderApi = fallbackRenderApiQueue.removeAt(0)
contextHandler = createContextHandler(this, renderApi)
redrawer = platformOperations.createRedrawer(this, renderApi, properties)
isInited = true
onInit.complete(Unit)
}
private val stateHandlers =
......@@ -252,6 +257,20 @@ open class SkiaLayer(
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")
private val fpsCounter = defaultFPSCounter(this)
......
......@@ -32,6 +32,10 @@ internal class AngleRedrawer(
frameDispatcher.scheduleFrame()
}
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() {
check(!isDisposed)
update(System.nanoTime())
......
......@@ -3,8 +3,8 @@ package org.jetbrains.skiko.redrawer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext
import org.jetbrains.skija.Surface
import org.jetbrains.skija.DirectContext
import org.jetbrains.skija.Surface
import org.jetbrains.skiko.FrameDispatcher
import org.jetbrains.skiko.GpuPriority
import org.jetbrains.skiko.SkiaLayer
......@@ -33,6 +33,10 @@ internal class Direct3DRedrawer(
frameDispatcher.scheduleFrame()
}
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() {
check(!isDisposed)
// 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
import kotlinx.coroutines.swing.Swing
import org.jetbrains.skiko.DrawingSurface
import org.jetbrains.skiko.FrameDispatcher
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.HardwareLayer
import org.jetbrains.skiko.OpenGLApi
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.SkiaLayerProperties
import org.jetbrains.skiko.getDrawingSurface
......@@ -34,6 +34,10 @@ internal class LinuxOpenGLRedrawer(
frameDispatcher.scheduleFrame()
}
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() = layer.backedLayer.lockDrawingSurface {
check(!isDisposed)
update(System.nanoTime())
......
......@@ -3,8 +3,8 @@ package org.jetbrains.skiko.redrawer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.swing.Swing
import org.jetbrains.skiko.FrameDispatcher
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.OpenGLApi
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.SkiaLayerProperties
import org.jetbrains.skiko.Task
import org.jetbrains.skiko.useDrawingSurfacePlatformInfo
......@@ -121,6 +121,10 @@ internal class MacOsOpenGLRedrawer(
frameDispatcher.scheduleFrame()
}
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() {
layer.update(System.nanoTime())
......
package org.jetbrains.skiko.redrawer
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext
import org.jetbrains.skija.BackendRenderTarget
import org.jetbrains.skija.DirectContext
import org.jetbrains.skiko.FrameDispatcher
......@@ -39,6 +41,10 @@ internal class MetalRedrawer(
frameDispatcher.scheduleFrame()
}
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() {
check(!isDisposed)
// TODO: now we wait until previous `layer.draw` is finished. it ends only on the next vsync.
......
......@@ -4,5 +4,6 @@ interface Redrawer {
fun dispose()
fun needRedraw()
fun redrawImmediately()
suspend fun awaitRedraw(): Boolean
fun syncSize() = Unit
}
\ No newline at end of file
......@@ -24,6 +24,10 @@ internal class SoftwareRedrawer(
frameDispatcher.scheduleFrame()
}
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() {
layer.update(System.nanoTime())
if (layer.prepareDrawContext()) {
......
......@@ -5,8 +5,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.withContext
import org.jetbrains.skiko.FrameDispatcher
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.OpenGLApi
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.SkiaLayerProperties
import org.jetbrains.skiko.useDrawingSurfacePlatformInfo
......@@ -39,6 +39,10 @@ internal class WindowsOpenGLRedrawer(
frameDispatcher.scheduleFrame()
}
override suspend fun awaitRedraw(): Boolean {
return frameDispatcher.awaitFrame()
}
override fun redrawImmediately() {
check(!isDisposed)
update(System.nanoTime())
......
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.runBlockingTest
import kotlinx.coroutines.yield
import org.junit.Assert.assertEquals
import org.junit.Test
import java.util.concurrent.Executors
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext
import kotlin.test.assertFalse
import kotlin.test.assertTrue
@OptIn(ExperimentalCoroutinesApi::class)
class FrameDispatcherTest {
......@@ -185,6 +197,75 @@ class FrameDispatcherTest {
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(
block: suspend TestCoroutineScope.() -> Unit
) = 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