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

Merge pull request #60 from JetBrains/macos-update

macOs. call update only once per draw if vsync is disabled
parents 50223f6b c5bc301e
import de.undercouch.gradle.tasks.download.Download import de.undercouch.gradle.tasks.download.Download
import org.gradle.crypto.checksum.Checksum import org.gradle.crypto.checksum.Checksum
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.kotlin.gradle.tasks.KotlinTest
plugins { plugins {
kotlin("multiplatform") version "1.3.72" kotlin("multiplatform") version "1.3.72"
...@@ -9,6 +11,8 @@ plugins { ...@@ -9,6 +11,8 @@ plugins {
id("de.undercouch.download") version "4.1.1" id("de.undercouch.download") version "4.1.1"
} }
val coroutinesVersion = "1.4.1"
buildscript { buildscript {
dependencies { dependencies {
classpath("org.kohsuke:github-api:1.116") classpath("org.kohsuke:github-api:1.116")
...@@ -166,7 +170,7 @@ kotlin { ...@@ -166,7 +170,7 @@ kotlin {
kotlin.srcDirs(skijaSrcDir) kotlin.srcDirs(skijaSrcDir)
dependencies { dependencies {
implementation(kotlin("stdlib-jdk8")) implementation(kotlin("stdlib-jdk8"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.4.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:$coroutinesVersion")
compileOnly(lombok) compileOnly(lombok)
compileOnly(jetbrainsAnnotations) compileOnly(jetbrainsAnnotations)
} }
...@@ -174,6 +178,7 @@ kotlin { ...@@ -174,6 +178,7 @@ kotlin {
} }
val jvmTest by getting { val jvmTest by getting {
dependencies { dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
implementation(kotlin("test-junit")) implementation(kotlin("test-junit"))
} }
} }
...@@ -565,3 +570,9 @@ publishing { ...@@ -565,3 +570,9 @@ publishing {
} }
} }
} }
tasks.withType<KotlinCompile>().configureEach {
if (name == "compileTestKotlinJvm") {
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
}
}
\ No newline at end of file
package org.jetbrains.skiko
import kotlinx.coroutines.channels.Channel
import java.util.concurrent.atomic.AtomicBoolean
internal class Task {
private val onFinish = Channel<Unit>(1)
private var done = AtomicBoolean(true)
/**
* Run task and await its finishing (i.e. calling of [finish])
*/
suspend fun runAndAwait(run: suspend () -> Unit) {
done.set(false)
run()
onFinish.receive()
}
/**
* Finish running task. If there is no running task, do nothing.
*/
fun finish() {
if (!done.getAndSet(true)) {
onFinish.offer(Unit)
}
}
}
\ No newline at end of file
package org.jetbrains.skiko.redrawer package org.jetbrains.skiko.redrawer
import kotlinx.coroutines.CompletableDeferred
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.HardwareLayer import org.jetbrains.skiko.HardwareLayer
import org.jetbrains.skiko.OpenGLApi import org.jetbrains.skiko.OpenGLApi
import org.jetbrains.skiko.SkikoProperties import org.jetbrains.skiko.SkikoProperties
import org.jetbrains.skiko.Task
import javax.swing.SwingUtilities.convertPoint import javax.swing.SwingUtilities.convertPoint
import javax.swing.SwingUtilities.getRootPane import javax.swing.SwingUtilities.getRootPane
...@@ -30,7 +30,7 @@ internal class MacOsOpenGLRedrawer( ...@@ -30,7 +30,7 @@ internal class MacOsOpenGLRedrawer(
// AWT has a method to avoid dead locks but it is internal (sun.lwawt.macosx.LWCToolkit.invokeAndWait) // AWT has a method to avoid dead locks but it is internal (sun.lwawt.macosx.LWCToolkit.invokeAndWait)
private val vsyncLayer = object : AWTGLLayer(containerLayerPtr, setNeedsDisplayOnBoundsChange = false) { private val vsyncLayer = object : AWTGLLayer(containerLayerPtr, setNeedsDisplayOnBoundsChange = false) {
@Volatile @Volatile
private var needDraw: CompletableDeferred<Unit>? = null private var canDraw = false
init { init {
setFrame(0, 0, 1, 1) // if frame has zero size then it will be not drawn at all setFrame(0, 0, 1, 1) // if frame has zero size then it will be not drawn at all
...@@ -41,21 +41,17 @@ internal class MacOsOpenGLRedrawer( ...@@ -41,21 +41,17 @@ internal class MacOsOpenGLRedrawer(
val opengl = OpenGLApi.instance val opengl = OpenGLApi.instance
opengl.glClearColor(0f, 0f, 0f, 0f) opengl.glClearColor(0f, 0f, 0f, 0f)
opengl.glClear(opengl.GL_COLOR_BUFFER_BIT) opengl.glClear(opengl.GL_COLOR_BUFFER_BIT)
needDraw?.complete(Unit)
} }
override fun canDraw(): Boolean { override fun canDraw(): Boolean {
val canDraw = needDraw != null val canDraw = canDraw
if (!canDraw) { if (!canDraw) {
isAsynchronous = false // stop asynchronous mode so we don't waste CPU cycles isAsynchronous = false // stop asynchronous mode so we don't waste CPU cycles
} }
return canDraw return canDraw
} }
suspend fun sync() { override fun setNeedsDisplay() {
check(needDraw == null)
needDraw = CompletableDeferred()
// Use asynchronous mode instead of just setNeedsDisplay, // Use asynchronous mode instead of just setNeedsDisplay,
// so Core Animation will wait for the next frame in vsync signal // so Core Animation will wait for the next frame in vsync signal
// //
...@@ -68,17 +64,26 @@ internal class MacOsOpenGLRedrawer( ...@@ -68,17 +64,26 @@ internal class MacOsOpenGLRedrawer(
isAsynchronous = true isAsynchronous = true
super.setNeedsDisplay() super.setNeedsDisplay()
} }
}
needDraw!!.await() suspend fun sync() {
needDraw = null canDraw = true
display()
canDraw = false
} }
} }
private val frameDispatcher = FrameDispatcher(Dispatchers.Swing) { private val frameDispatcher = FrameDispatcher(Dispatchers.Swing) {
synchronized(drawLock) {
layer.update(System.nanoTime()) layer.update(System.nanoTime())
drawLayer.setNeedsDisplay() }
if (SkikoProperties.vsyncEnabled) { if (SkikoProperties.vsyncEnabled) {
drawLayer.setNeedsDisplay()
vsyncLayer.sync() vsyncLayer.sync()
} else {
// If vsync is disabled we should await the drawing to end.
// Otherwise we will call 'update' multiple times.
drawLayer.display()
} }
} }
...@@ -111,10 +116,12 @@ internal class MacOsOpenGLRedrawer( ...@@ -111,10 +116,12 @@ internal class MacOsOpenGLRedrawer(
} }
} }
private open class AWTGLLayer(private val containerPtr: Long, setNeedsDisplayOnBoundsChange: Boolean) { private abstract class AWTGLLayer(private val containerPtr: Long, setNeedsDisplayOnBoundsChange: Boolean) {
@Suppress("LeakingThis") @Suppress("LeakingThis")
val ptr = initAWTGLLayer(containerPtr, this, setNeedsDisplayOnBoundsChange) val ptr = initAWTGLLayer(containerPtr, this, setNeedsDisplayOnBoundsChange)
private val display = Task()
fun setFrame(x: Int, y: Int, width: Int, height: Int) { fun setFrame(x: Int, y: Int, width: Int, height: Int) {
setFrame(containerPtr, ptr, x.toFloat(), y.toFloat(), width.toFloat(), height.toFloat()) setFrame(containerPtr, ptr, x.toFloat(), y.toFloat(), width.toFloat(), height.toFloat())
} }
...@@ -128,11 +135,25 @@ private open class AWTGLLayer(private val containerPtr: Long, setNeedsDisplayOnB ...@@ -128,11 +135,25 @@ private open class AWTGLLayer(private val containerPtr: Long, setNeedsDisplayOnB
open fun setNeedsDisplay() = setNeedsDisplayOnMainThread(ptr) open fun setNeedsDisplay() = setNeedsDisplayOnMainThread(ptr)
suspend fun display() = display.runAndAwait {
setNeedsDisplay()
}
// Called in AppKit Thread // Called in AppKit Thread
protected open fun canDraw() = true protected open fun canDraw() = true
@Suppress("unused") // called from native code
private fun performDraw() {
try {
draw()
} catch (e: Throwable) {
e.printStackTrace()
}
display.finish()
}
// Called in AppKit Thread // Called in AppKit Thread
protected open fun draw() = Unit protected abstract fun draw()
private external fun isAsynchronous(ptr: Long): Boolean private external fun isAsynchronous(ptr: Long): Boolean
private external fun setAsynchronous(ptr: Long, isAsynchronous: Boolean) private external fun setAsynchronous(ptr: Long, isAsynchronous: Boolean)
......
...@@ -61,7 +61,7 @@ JavaVM *jvm = NULL; ...@@ -61,7 +61,7 @@ JavaVM *jvm = NULL;
static jclass cls = NULL; static jclass cls = NULL;
static jmethodID method = NULL; static jmethodID method = NULL;
if (!cls) cls = (*env)->GetObjectClass(env, self.javaRef); if (!cls) cls = (*env)->GetObjectClass(env, self.javaRef);
if (!method) method = (*env)->GetMethodID(env, cls, "draw", "()V"); if (!method) method = (*env)->GetMethodID(env, cls, "performDraw", "()V");
(*env)->CallVoidMethod(env, self.javaRef, method); (*env)->CallVoidMethod(env, self.javaRef, method);
[super drawInCGLContext:ctx pixelFormat:pf forLayerTime:t displayTime:ts]; [super drawInCGLContext:ctx pixelFormat:pf forLayerTime:t displayTime:ts];
......
package org.jetbrains.skiko
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.isActive
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.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import java.util.concurrent.Executors.newSingleThreadExecutor
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.random.Random
@OptIn(ExperimentalCoroutinesApi::class)
internal class TaskTest {
@Test
fun `runAndAwait with finish`() = test {
val task = Task()
val job = launch {
task.runAndAwait {}
}
advanceUntilIdle()
task.finish()
advanceUntilIdle()
assertTrue(job.isCompleted)
}
@Test
fun `runAndAwait without finish`() = test {
val task = Task()
val job = launch {
task.runAndAwait {}
}
advanceUntilIdle()
assertFalse(job.isCompleted)
}
@Test
fun `finish inside runAndAwait`() = test {
val task = Task()
val job = launch {
task.runAndAwait {
task.finish()
}
}
advanceUntilIdle()
assertTrue(job.isCompleted)
}
@Test
fun `finish before runAndAwait`() = test {
val task = Task()
val job = launch {
task.finish()
task.runAndAwait {}
}
advanceUntilIdle()
assertFalse(job.isCompleted)
}
@Test(timeout = 5000)
fun `finish in another thread`() {
val task = Task()
runBlocking {
repeat(1000) {
task.runAndAwait {
launch(Dispatchers.IO) {
task.finish()
}
}
}
}
}
@Test(timeout = 5000)
fun `simulate MacOs layer`() {
runBlocking {
val job = Job()
val layer = object : MacOsSimulatedLayer(scope = CoroutineScope(coroutineContext + job)) {
val draw = Task()
override fun draw() {
draw.finish()
}
suspend fun display() {
draw.runAndAwait {
setNeedsDisplay()
}
}
}
repeat(10000) {
layer.display()
}
job.cancel()
}
}
@Test(timeout = 5000)
fun `simulate MacOs layer with another renderings`() {
runBlocking {
val job = Job()
val layer = object : MacOsSimulatedLayer(scope = CoroutineScope(coroutineContext + job)) {
val draw = Task()
override fun draw() {
draw.finish()
}
suspend fun display() {
draw.runAndAwait {
setNeedsDisplay()
}
}
}
val random = Random(42)
val anotherRenderingsJob1 = launch(newSingleThreadExecutor().asCoroutineDispatcher()) {
while (true) {
repeat(random.nextInt(4)) {
layer.setNeedsDisplay()
}
yield()
}
}
val anotherRenderingsJob2 = launch(newSingleThreadExecutor().asCoroutineDispatcher()) {
while (true) {
repeat(random.nextInt(4)) {
layer.setNeedsDisplay()
}
yield()
}
}
repeat(10000) {
layer.display()
}
job.cancel()
anotherRenderingsJob1.cancel()
anotherRenderingsJob2.cancel()
}
}
private abstract class MacOsSimulatedLayer(scope: CoroutineScope) {
private val dispatcher = newSingleThreadExecutor().asCoroutineDispatcher()
private var needsDisplay = AtomicBoolean(false)
init {
scope.launch(dispatcher) {
while (isActive) {
if (needsDisplay.getAndSet(false)) {
draw()
}
yield()
}
}
}
abstract fun draw()
fun setNeedsDisplay() {
needsDisplay.set(true)
}
}
private fun test(
block: suspend TestCoroutineScope.() -> Unit
) = runBlockingTest {
pauseDispatcher()
val job = Job()
TestCoroutineScope(coroutineContext + job).block()
job.cancel()
}
}
\ No newline at end of file
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