Commit c5bc301e authored by Igor Demin's avatar Igor Demin

MacOsOpenGLRedrawer. Refactor Task, use Channel instead of CompletableDeferred

parent a306eae8
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
...@@ -165,26 +165,3 @@ private external fun initContainer(layer: HardwareLayer): Long ...@@ -165,26 +165,3 @@ private external fun initContainer(layer: HardwareLayer): Long
private external fun setContentScale(layerNativePtr: Long, contentScale: Float) private external fun setContentScale(layerNativePtr: Long, contentScale: Float)
private external fun initAWTGLLayer(containerPtr: Long, layer: AWTGLLayer, setNeedsDisplayOnBoundsChange: Boolean): Long private external fun initAWTGLLayer(containerPtr: Long, layer: AWTGLLayer, setNeedsDisplayOnBoundsChange: Boolean): Long
private external fun disposeAWTGLLayer(ptr: Long) private external fun disposeAWTGLLayer(ptr: Long)
\ No newline at end of file
internal class Task {
@Volatile
private var onFinish: CompletableDeferred<Unit>? = null
/**
* Run task and await its finishing (i.e. calling of [finish])
*/
suspend fun runAndAwait(run: suspend () -> Unit) {
check(onFinish == null)
onFinish = CompletableDeferred()
run()
onFinish!!.await()
onFinish = null
}
/**
* Finish running task. If there is no running task, do nothing.
*/
fun finish() {
onFinish?.complete(Unit)
}
}
\ No newline at end of file
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