Commit 0499e314 authored by Igor Demin's avatar Igor Demin

Merge remote-tracking branch 'origin/master' into refactor_jni

# Conflicts:
#	skiko/src/jvmMain/kotlin/org/jetbrains/skiko/redrawer/MacOsOpenGLRedrawer.kt
parents 3b530c04 7a24e47c
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="SkijaInjectSample (show long frames) " type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$/samples/SkijaInjectSample" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="-Dskiko.fps.longFrames.show=true" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="run" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2">
<option name="Gradle.BeforeRunTask" enabled="false" tasks="publishToMavenLocal" externalProjectPath="$PROJECT_DIR$/skiko" vmOptions="" scriptParameters="" />
</method>
</configuration>
</component>
\ No newline at end of file
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
kotlin("jvm") version "1.3.72" kotlin("jvm") version "1.3.72"
application application
...@@ -36,6 +38,7 @@ dependencies { ...@@ -36,6 +38,7 @@ dependencies {
implementation(platform("org.jetbrains.kotlin:kotlin-bom")) implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.4.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.4.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.1")
implementation("org.jetbrains.skiko:skiko-jvm-runtime-$target:$version") implementation("org.jetbrains.skiko:skiko-jvm-runtime-$target:$version")
testImplementation("org.jetbrains.kotlin:kotlin-test") testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit") testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
...@@ -45,7 +48,9 @@ application { ...@@ -45,7 +48,9 @@ application {
mainClass.set("SkijaInjectSample.AppKt") mainClass.set("SkijaInjectSample.AppKt")
} }
tasks.named<JavaExec>("run") { val additionalArguments = mutableMapOf<String, String>()
val casualRun = tasks.named<JavaExec>("run") {
systemProperty("skiko.fps.enabled", "true") systemProperty("skiko.fps.enabled", "true")
System.getProperties().entries System.getProperties().entries
.associate { .associate {
...@@ -53,6 +58,12 @@ tasks.named<JavaExec>("run") { ...@@ -53,6 +58,12 @@ tasks.named<JavaExec>("run") {
} }
.filterKeys { it.startsWith("skiko.") } .filterKeys { it.startsWith("skiko.") }
.forEach { systemProperty(it.key, it.value) } .forEach { systemProperty(it.key, it.value) }
additionalArguments.forEach { systemProperty(it.key, it.value) }
}
tasks.register("runSoftware") {
additionalArguments += mapOf("skiko.renderApi" to "SOFTWARE")
dependsOn(casualRun)
} }
tasks.withType<Test> { tasks.withType<Test> {
...@@ -65,3 +76,7 @@ tasks.withType<Test> { ...@@ -65,3 +76,7 @@ tasks.withType<Test> {
systemProperty("sun.java2d.uiScale", "1") systemProperty("sun.java2d.uiScale", "1")
} }
} }
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
}
\ No newline at end of file
package org.jetbrains.skiko
import kotlinx.coroutines.*
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Assert.assertEquals
import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
class FrameDispatcherTest {
private var frameCount = 0
@Test
fun `shouldn't call onFrame after the creating`() = test {
FrameDispatcher(scope = this) {
frameCount++
}
advanceUntilIdle()
assertEquals(0, frameCount)
}
@Test
fun `scheduleFrame after creating`() = test {
val frameDispatcher = FrameDispatcher(scope = this) {
frameCount++
}
frameDispatcher.scheduleFrame()
advanceUntilIdle()
assertEquals(1, frameCount)
}
@Test
fun `scheduleFrame multiple times after creating`() = test {
val frameDispatcher = FrameDispatcher(scope = this) {
frameCount++
}
frameDispatcher.scheduleFrame()
frameDispatcher.scheduleFrame()
frameDispatcher.scheduleFrame()
frameDispatcher.scheduleFrame()
advanceUntilIdle()
assertEquals(1, frameCount)
}
@Test
fun `scheduleFrame second time after first onFrame`() = test {
val frameDispatcher = FrameDispatcher(scope = this) {
frameCount++
}
frameDispatcher.scheduleFrame()
advanceUntilIdle()
frameDispatcher.scheduleFrame()
advanceUntilIdle()
assertEquals(2, frameCount)
}
@Test
fun `scheduleFrame second time twice after first onFrame`() = test {
val frameDispatcher = FrameDispatcher(scope = this) {
frameCount++
}
frameDispatcher.scheduleFrame()
advanceUntilIdle()
frameDispatcher.scheduleFrame()
frameDispatcher.scheduleFrame()
advanceUntilIdle()
assertEquals(2, frameCount)
}
@Suppress("JoinDeclarationAndAssignment")
@Test
fun `scheduleFrame during onFrame`() = test {
lateinit var frameDispatcher: FrameDispatcher
frameDispatcher = FrameDispatcher(scope = this) {
frameCount++
frameDispatcher.scheduleFrame()
}
frameDispatcher.scheduleFrame()
yield()
assertEquals(1, frameCount)
yield()
assertEquals(2, frameCount)
yield()
assertEquals(3, frameCount)
yield()
assertEquals(4, frameCount)
}
@Suppress("JoinDeclarationAndAssignment")
@Test
fun `scheduleFrame multiple times during onFrame`() = test {
lateinit var frameDispatcher: FrameDispatcher
frameDispatcher = FrameDispatcher(scope = this) {
frameCount++
frameDispatcher.scheduleFrame()
frameDispatcher.scheduleFrame()
frameDispatcher.scheduleFrame()
}
frameDispatcher.scheduleFrame()
yield()
assertEquals(1, frameCount)
yield()
assertEquals(2, frameCount)
yield()
assertEquals(3, frameCount)
yield()
assertEquals(4, frameCount)
}
@Suppress("JoinDeclarationAndAssignment")
@Test
fun `cancel coroutine scope`() = test {
val scope = CoroutineScope(coroutineContext)
lateinit var frameDispatcher: FrameDispatcher
frameDispatcher = FrameDispatcher(scope) {
frameCount++
frameDispatcher.scheduleFrame()
}
frameDispatcher.scheduleFrame()
yield()
assertEquals(1, frameCount)
yield()
assertEquals(2, frameCount)
yield()
assertEquals(3, frameCount)
scope.cancel()
yield()
assertEquals(3, frameCount)
advanceUntilIdle()
assertEquals(3, frameCount)
}
private fun test(
block: suspend TestCoroutineScope.() -> Unit
) = runBlockingTest {
pauseDispatcher()
val job = Job()
TestCoroutineScope(coroutineContext + job).block()
job.cancel()
}
}
\ No newline at end of file
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
kotlin.code.style=official kotlin.code.style=official
deploy.version=0.0.0 deploy.version=0.0.0
dependencies.skija.git.commit=1ff7ab54457cb0171075f72dcc586b552373f281 dependencies.skija.git.commit=b85e3e7a70204281cf0036d5595a8eed68bacd2c
dependencies.skia.windows-x64=m89-15595ea39c/Skia-m89-15595ea39c-windows-Release-x64 dependencies.skia.windows-x64=m89-15595ea39c/Skia-m89-15595ea39c-windows-Release-x64
dependencies.skia.linux-x64=m89-15595ea39c/Skia-m89-15595ea39c-linux-Release-x64 dependencies.skia.linux-x64=m89-15595ea39c/Skia-m89-15595ea39c-linux-Release-x64
dependencies.skia.macos-x64=m89-15595ea39c/Skia-m89-15595ea39c-macos-Release-x64 dependencies.skia.macos-x64=m89-15595ea39c/Skia-m89-15595ea39c-macos-Release-x64
......
package org.jetbrains.skiko package org.jetbrains.skiko
import java.util.* import java.awt.Component
import kotlin.math.roundToInt import kotlin.math.roundToInt
internal class FPSCounter( internal class FPSCounter(
private val count: Int, private val periodSeconds: Double,
private val probability: Double private val showLongFrames: Boolean,
private val getLongFrameMillis: () -> Double
) { ) {
private var i = 0 private val times = mutableListOf<Long>()
private val times = LinkedList<Double>() private var lastLogTime = System.nanoTime()
private var t1 = System.nanoTime() private var lastTime = System.nanoTime()
/**
* [value] 0.0 - min, 1.0 - max, 0.5 - median
*/
private fun MutableList<Double>.quantile(value: Double) : Double {
val index = (value * (size - 1)).toInt()
return sorted()[index]
}
fun tick() { fun tick() {
val t2 = System.nanoTime() val time = System.nanoTime()
val frameTime = (t2 - t1) / 1E6 val timestamp = time.nanosToMillis().toLong()
t1 = t2 val frameTime = time - lastTime
lastTime = time
i++
times.add(frameTime) times.add(frameTime)
if (times.size > count) { if (showLongFrames && frameTime > getLongFrameMillis().millisToNanos()) {
times.removeFirst() println("[%d] Long frame %.2f ms".format(timestamp, frameTime.nanosToMillis()))
} }
if (i % count == 0) { if ((time - lastLogTime) > periodSeconds.secondsToNanos()) {
val quantile = (1 - probability) / 2.0 val average = (nanosPerSecond / times.average()).roundToInt()
val average = (1000.0 / times.average()).roundToInt() val min = (nanosPerSecond / times.max()!!).roundToInt()
val min = (1000.0 / times.quantile(1 - quantile)).roundToInt() val max = (nanosPerSecond / times.min()!!).roundToInt()
val max = (1000.0 / times.quantile(quantile)).roundToInt() println("[$timestamp] FPS $average ($min-$max)")
val probability = (100 * probability).roundToInt() times.clear()
println("FPS $average ($min-$max $probability%)") lastLogTime = time
}
} }
private val nanosPerMillis = 1_000_000.0
private val nanosPerSecond = 1_000_000_000.0
private fun Long.nanosToMillis(): Double = this / nanosPerMillis
private fun Double.millisToNanos(): Long = (this * nanosPerMillis).toLong()
private fun Double.secondsToNanos(): Long = (this * nanosPerSecond).toLong()
}
internal fun defaultFPSCounter(
component: Component
): FPSCounter? = with(SkikoProperties) {
if (!SkikoProperties.fpsEnabled) return@with null
// it is slow on Linux (100ms), so we cache it. Also refreshRate available only after window is visible
val refreshRate by lazy { component.graphicsConfiguration.device.displayMode.refreshRate }
FPSCounter(
periodSeconds = fpsPeriodSeconds,
showLongFrames = fpsLongFramesShow,
getLongFrameMillis = {
fpsLongFramesMillis ?: 1.5 * 1000 / refreshRate
} }
)
} }
\ No newline at end of file
package org.jetbrains.skiko package org.jetbrains.skiko
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
/** /**
* Dispatch frame after call of [scheduleFrame] * Dispatch frame after call of [scheduleFrame].
*
* After the creating there should be no scheduled frame in the frame loop.
*/ */
class FrameDispatcher( class FrameDispatcher(
context: CoroutineContext, scope: CoroutineScope,
private val onFrame: suspend () -> Unit private val onFrame: suspend () -> Unit
) { ) {
constructor(
context: CoroutineContext,
onFrame: suspend () -> Unit
) : this(
CoroutineScope(context),
onFrame
)
private var needFrame = CompletableDeferred<Unit>() private var needFrame = CompletableDeferred<Unit>()
private val job = GlobalScope.launch(context) { private val job = scope.launch {
while (true) { while (true) {
needFrame.await() needFrame.await()
needFrame = CompletableDeferred() needFrame = CompletableDeferred()
...@@ -28,6 +38,14 @@ class FrameDispatcher( ...@@ -28,6 +38,14 @@ class FrameDispatcher(
job.cancel() job.cancel()
} }
/**
* Schedule next frame to render in the frame loop.
*
* Multiple calls of scheduleFrame before beginning of the frame will cause only one onFrame.
*
* Multiple calls of scheduleFrame after beginning of the frame but before its ending
* will schedule next single onFrame after the current one.
*/
fun scheduleFrame() { fun scheduleFrame() {
needFrame.complete(Unit) needFrame.complete(Unit)
} }
......
package org.jetbrains.skiko package org.jetbrains.skiko
import org.jetbrains.skija.* import org.jetbrains.skija.Canvas
import org.jetbrains.skiko.redrawer.Redrawer import org.jetbrains.skija.ClipMode
import org.jetbrains.skiko.redrawer.RasterRedrawer import org.jetbrains.skija.Picture
import org.jetbrains.skiko.context.createContextHandler import org.jetbrains.skija.PictureRecorder
import org.jetbrains.skija.Rect
import org.jetbrains.skiko.context.SoftwareContextHandler import org.jetbrains.skiko.context.SoftwareContextHandler
import org.jetbrains.skiko.context.createContextHandler
import org.jetbrains.skiko.redrawer.RasterRedrawer
import org.jetbrains.skiko.redrawer.Redrawer
import java.awt.Graphics import java.awt.Graphics
import javax.swing.SwingUtilities.isEventDispatchThread import javax.swing.SwingUtilities.isEventDispatchThread
...@@ -64,18 +68,14 @@ open class SkiaLayer : HardwareLayer() { ...@@ -64,18 +68,14 @@ open class SkiaLayer : HardwareLayer() {
redrawer?.needRedraw() redrawer?.needRedraw()
} }
private val fpsCounter = FPSCounter( @Suppress("LeakingThis")
count = SkikoProperties.fpsCount, private val fpsCounter = defaultFPSCounter(this)
probability = SkikoProperties.fpsProbability
)
override fun update(nanoTime: Long) { override fun update(nanoTime: Long) {
check(!isDisposed) check(!isDisposed)
check(isEventDispatchThread()) check(isEventDispatchThread())
if (SkikoProperties.fpsEnabled) { fpsCounter?.tick()
fpsCounter.tick()
}
val pictureWidth = (width * contentScale).toInt().coerceAtLeast(0) val pictureWidth = (width * contentScale).toInt().coerceAtLeast(0)
val pictureHeight = (height * contentScale).toInt().coerceAtLeast(0) val pictureHeight = (height * contentScale).toInt().coerceAtLeast(0)
......
...@@ -5,8 +5,15 @@ internal object SkikoProperties { ...@@ -5,8 +5,15 @@ internal object SkikoProperties {
val vsyncEnabled: Boolean by property("skiko.vsync.enabled", default = true) val vsyncEnabled: Boolean by property("skiko.vsync.enabled", default = true)
val fpsEnabled: Boolean by property("skiko.fps.enabled", default = false) val fpsEnabled: Boolean by property("skiko.fps.enabled", default = false)
val fpsCount: Int by property("skiko.fps.count", default = 300) val fpsPeriodSeconds: Double by property("skiko.fps.periodSeconds", default = 2.0)
val fpsProbability: Double by property("skiko.fps.probability", default = 0.97)
/**
* Show long frames which is longer than [fpsLongFramesMillis].
* If [fpsLongFramesMillis] isn't defined will show frames longer than 1.5 * (1000 / displayRefreshRate)
*/
val fpsLongFramesShow: Boolean by property("skiko.fps.longFrames.show", default = false)
val fpsLongFramesMillis: Double? by property("skiko.fps.longFrames.millis", default = null)
val renderApi: GraphicsApi by lazy { val renderApi: GraphicsApi by lazy {
val environment = System.getenv("SKIKO_RENDER_API") val environment = System.getenv("SKIKO_RENDER_API")
...@@ -30,11 +37,11 @@ internal object SkikoProperties { ...@@ -30,11 +37,11 @@ internal object SkikoProperties {
System.getProperty(name)?.toBoolean() ?: default System.getProperty(name)?.toBoolean() ?: default
} }
private fun property(name: String, default: Int) = lazy { private fun property(name: String, default: Double) = lazy {
System.getProperty(name)?.toInt() ?: default System.getProperty(name)?.toDouble() ?: default
} }
private fun property(name: String, default: Double) = lazy { private fun property(name: String, default: Double?) = lazy {
System.getProperty(name)?.toDouble() ?: default System.getProperty(name)?.toDouble() ?: default
} }
} }
\ 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
...@@ -8,6 +7,7 @@ import org.jetbrains.skiko.HardwareLayer ...@@ -8,6 +7,7 @@ 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.useDrawingSurfacePlatformInfo import org.jetbrains.skiko.useDrawingSurfacePlatformInfo
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
...@@ -31,7 +31,7 @@ internal class MacOsOpenGLRedrawer( ...@@ -31,7 +31,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
...@@ -42,21 +42,17 @@ internal class MacOsOpenGLRedrawer( ...@@ -42,21 +42,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
// //
...@@ -69,17 +65,26 @@ internal class MacOsOpenGLRedrawer( ...@@ -69,17 +65,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()
} }
} }
...@@ -112,10 +117,12 @@ internal class MacOsOpenGLRedrawer( ...@@ -112,10 +117,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())
} }
...@@ -129,11 +136,25 @@ private open class AWTGLLayer(private val containerPtr: Long, setNeedsDisplayOnB ...@@ -129,11 +136,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)
......
...@@ -59,7 +59,7 @@ JavaVM *jvm = NULL; ...@@ -59,7 +59,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