From ed3b00b74512902a0ec5c8242587cf2ccf2d66b0 Mon Sep 17 00:00:00 2001 From: Igor Demin Date: Tue, 16 Feb 2021 15:19:15 +0300 Subject: [PATCH 1/2] Ability to change isVsyncEnabled through constructor of SkiaLayer --- .../org/jetbrains/skiko/PlatformOperations.kt | 14 +++++++------- .../kotlin/org/jetbrains/skiko/SkiaLayer.kt | 6 ++++-- .../org/jetbrains/skiko/SkiaLayerProperties.kt | 5 +++++ .../kotlin/org/jetbrains/skiko/SkiaWindow.kt | 6 ++++-- .../skiko/redrawer/LinuxOpenGLRedrawer.kt | 9 ++++++--- .../skiko/redrawer/MacOsOpenGLRedrawer.kt | 9 +++++---- .../skiko/redrawer/WindowsOpenGLRedrawer.kt | 8 +++++--- 7 files changed, 36 insertions(+), 21 deletions(-) create mode 100644 skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkiaLayerProperties.kt diff --git a/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/PlatformOperations.kt b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/PlatformOperations.kt index 854811bca..4300940c7 100644 --- a/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/PlatformOperations.kt +++ b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/PlatformOperations.kt @@ -14,7 +14,7 @@ internal interface PlatformOperations { fun isFullscreen(component: Component): Boolean fun setFullscreen(component: Component, value: Boolean) fun getDpiScale(component: Component): Float - fun createRedrawer(layer: HardwareLayer): Redrawer + fun createRedrawer(layer: HardwareLayer, properties: SkiaLayerProperties): Redrawer } internal val platformOperations: PlatformOperations by lazy { @@ -32,9 +32,9 @@ internal val platformOperations: PlatformOperations by lazy { return component.graphicsConfiguration.defaultTransform.scaleX.toFloat() } - override fun createRedrawer(layer: HardwareLayer) = when(renderApi) { + override fun createRedrawer(layer: HardwareLayer, properties: SkiaLayerProperties) = when(renderApi) { GraphicsApi.SOFTWARE -> RasterRedrawer(layer) - else -> MacOsOpenGLRedrawer(layer) + else -> MacOsOpenGLRedrawer(layer, properties) } } OS.Windows -> { @@ -55,9 +55,9 @@ internal val platformOperations: PlatformOperations by lazy { return component.graphicsConfiguration.defaultTransform.scaleX.toFloat() } - override fun createRedrawer(layer: HardwareLayer) = when(renderApi) { + override fun createRedrawer(layer: HardwareLayer, properties: SkiaLayerProperties) = when(renderApi) { GraphicsApi.SOFTWARE -> RasterRedrawer(layer) - else -> WindowsOpenGLRedrawer(layer) + else -> WindowsOpenGLRedrawer(layer, properties) } } } @@ -91,9 +91,9 @@ internal val platformOperations: PlatformOperations by lazy { // return component.useDrawingSurfacePlatformInfo(::linuxGetDpiScaleNative) } - override fun createRedrawer(layer: HardwareLayer) = when(renderApi) { + override fun createRedrawer(layer: HardwareLayer, properties: SkiaLayerProperties) = when(renderApi) { GraphicsApi.SOFTWARE -> RasterRedrawer(layer) - else -> LinuxOpenGLRedrawer(layer) + else -> LinuxOpenGLRedrawer(layer, properties) } } } diff --git a/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkiaLayer.kt b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkiaLayer.kt index 1574a0df3..1df630342 100644 --- a/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkiaLayer.kt +++ b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkiaLayer.kt @@ -18,7 +18,9 @@ interface SkiaRenderer { private class PictureHolder(val instance: Picture, val width: Int, val height: Int) -open class SkiaLayer : HardwareLayer() { +open class SkiaLayer( + private val properties: SkiaLayerProperties = SkiaLayerProperties() +) : HardwareLayer() { var renderer: SkiaRenderer? = null val clipComponents = mutableListOf() @@ -35,7 +37,7 @@ open class SkiaLayer : HardwareLayer() { override fun init() { super.init() - redrawer = platformOperations.createRedrawer(this) + redrawer = platformOperations.createRedrawer(this, properties) redrawer?.syncSize() redrawer?.redrawImmediately() } diff --git a/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkiaLayerProperties.kt b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkiaLayerProperties.kt new file mode 100644 index 000000000..1fd4e2cd4 --- /dev/null +++ b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkiaLayerProperties.kt @@ -0,0 +1,5 @@ +package org.jetbrains.skiko + +class SkiaLayerProperties( + val isVsyncEnabled: Boolean = SkikoProperties.vsyncEnabled +) \ No newline at end of file diff --git a/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkiaWindow.kt b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkiaWindow.kt index 494131375..dec8970c4 100644 --- a/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkiaWindow.kt +++ b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkiaWindow.kt @@ -2,8 +2,10 @@ package org.jetbrains.skiko import javax.swing.JFrame -open class SkiaWindow : JFrame() { - val layer = SkiaLayer() +open class SkiaWindow( + properties: SkiaLayerProperties = SkiaLayerProperties() +) : JFrame() { + val layer = SkiaLayer(properties) init { contentPane.add(layer) diff --git a/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/redrawer/LinuxOpenGLRedrawer.kt b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/redrawer/LinuxOpenGLRedrawer.kt index 5b7f683ea..531439f6b 100644 --- a/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/redrawer/LinuxOpenGLRedrawer.kt +++ b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/redrawer/LinuxOpenGLRedrawer.kt @@ -7,11 +7,12 @@ import org.jetbrains.skiko.DrawingSurface import org.jetbrains.skiko.FrameDispatcher import org.jetbrains.skiko.HardwareLayer import org.jetbrains.skiko.OpenGLApi -import org.jetbrains.skiko.SkikoProperties +import org.jetbrains.skiko.SkiaLayerProperties import org.jetbrains.skiko.getDrawingSurface internal class LinuxOpenGLRedrawer( - private val layer: HardwareLayer + private val layer: HardwareLayer, + private val properties: SkiaLayerProperties ) : Redrawer { private val context = layer.lockDrawingSurface { it.createContext() @@ -70,6 +71,8 @@ internal class LinuxOpenGLRedrawer( } } + val isVsyncEnabled = toRedrawAlive.all { it.properties.isVsyncEnabled } + val drawingSurfaces = toRedrawAlive.map { lockDrawingSurface(it.layer) }.toList() try { toRedrawAlive.forEachIndexed { index, redrawer -> @@ -79,7 +82,7 @@ internal class LinuxOpenGLRedrawer( toRedrawAlive.forEachIndexed { index, _ -> // it is ok to set swap interval every frame, there is no performance overhead - drawingSurfaces[index].setSwapInterval(if (SkikoProperties.vsyncEnabled) 1 else 0) + drawingSurfaces[index].setSwapInterval(if (isVsyncEnabled) 1 else 0) drawingSurfaces[index].swapBuffers() } diff --git a/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/redrawer/MacOsOpenGLRedrawer.kt b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/redrawer/MacOsOpenGLRedrawer.kt index 205e82944..d2f27848c 100644 --- a/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/redrawer/MacOsOpenGLRedrawer.kt +++ b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/redrawer/MacOsOpenGLRedrawer.kt @@ -5,14 +5,15 @@ import kotlinx.coroutines.swing.Swing import org.jetbrains.skiko.FrameDispatcher import org.jetbrains.skiko.HardwareLayer import org.jetbrains.skiko.OpenGLApi -import org.jetbrains.skiko.SkikoProperties -import org.jetbrains.skiko.useDrawingSurfacePlatformInfo +import org.jetbrains.skiko.SkiaLayerProperties import org.jetbrains.skiko.Task +import org.jetbrains.skiko.useDrawingSurfacePlatformInfo import javax.swing.SwingUtilities.convertPoint import javax.swing.SwingUtilities.getRootPane internal class MacOsOpenGLRedrawer( - private val layer: HardwareLayer + private val layer: HardwareLayer, + private val properties: SkiaLayerProperties ) : Redrawer { private val containerLayerPtr = layer.useDrawingSurfacePlatformInfo(::initContainer) private val drawLock = Any() @@ -78,7 +79,7 @@ internal class MacOsOpenGLRedrawer( synchronized(drawLock) { layer.update(System.nanoTime()) } - if (SkikoProperties.vsyncEnabled) { + if (properties.isVsyncEnabled) { drawLayer.setNeedsDisplay() vsyncLayer.sync() } else { diff --git a/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/redrawer/WindowsOpenGLRedrawer.kt b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/redrawer/WindowsOpenGLRedrawer.kt index 88c57faa2..78201c0a4 100644 --- a/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/redrawer/WindowsOpenGLRedrawer.kt +++ b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/redrawer/WindowsOpenGLRedrawer.kt @@ -7,11 +7,12 @@ import kotlinx.coroutines.withContext import org.jetbrains.skiko.FrameDispatcher import org.jetbrains.skiko.HardwareLayer import org.jetbrains.skiko.OpenGLApi -import org.jetbrains.skiko.SkikoProperties +import org.jetbrains.skiko.SkiaLayerProperties import org.jetbrains.skiko.useDrawingSurfacePlatformInfo internal class WindowsOpenGLRedrawer( - private val layer: HardwareLayer + private val layer: HardwareLayer, + private val properties: SkiaLayerProperties ) : Redrawer { private val device = layer.useDrawingSurfacePlatformInfo(::getDevice) private val context = createContext(device) @@ -92,7 +93,8 @@ internal class WindowsOpenGLRedrawer( OpenGLApi.instance.glFinish() } - if (SkikoProperties.vsyncEnabled) { + val isVsyncEnabled = toRedrawAlive.all { it.properties.isVsyncEnabled } + if (isVsyncEnabled) { withContext(Dispatchers.IO) { dwmFlush() // wait for vsync } From 529dc3fd64e64c363351eb97e5154381cae2d1a2 Mon Sep 17 00:00:00 2001 From: Igor Demin Date: Tue, 16 Feb 2021 15:20:21 +0300 Subject: [PATCH 2/2] Fix macOs freeze when we render very fast on resize. Disable drawing without vsync. Drawing without vsync is disabled, because we cannot guarantee that we will draw with high FPS and don't block drawing loop. Even with FPS limit = 300 we can do that (when we minimize/restore window) --- .../org/jetbrains/skiko/SkiaWindowTest.kt | 39 +++++++++++++ .../skiko/redrawer/MacOsOpenGLRedrawer.kt | 55 +++++++++++++++---- 2 files changed, 84 insertions(+), 10 deletions(-) diff --git a/samples/SkijaInjectSample/src/test/kotlin/org/jetbrains/skiko/SkiaWindowTest.kt b/samples/SkijaInjectSample/src/test/kotlin/org/jetbrains/skiko/SkiaWindowTest.kt index 79d33c778..692b8476f 100644 --- a/samples/SkijaInjectSample/src/test/kotlin/org/jetbrains/skiko/SkiaWindowTest.kt +++ b/samples/SkijaInjectSample/src/test/kotlin/org/jetbrains/skiko/SkiaWindowTest.kt @@ -1,5 +1,6 @@ package org.jetbrains.skiko +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -204,6 +205,44 @@ class SkiaWindowTest { delay(5000) } + @Test(timeout = 20000) + fun `render continuously empty content without vsync`() = swingTest { + assumeTrue(hostOs != OS.MacOS) // TODO remove when we will support drawing without vsync on macOs + + val targetDrawCount = 500 + var drawCount = 0 + val onDrawCompleted = CompletableDeferred() + + val window = SkiaWindow( + properties = SkiaLayerProperties( + isVsyncEnabled = false + ) + ) + + try { + window.setLocation(200, 200) + window.setSize(400, 200) + window.defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE + window.layer.renderer = object : SkiaRenderer { + override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) { + drawCount++ + + if (drawCount < targetDrawCount) { + window.layer.needRedraw() + } else { + onDrawCompleted.complete(Unit) + } + } + } + window.isUndecorated = true + window.isVisible = true + + onDrawCompleted.await() + } finally { + window.close() + } + } + @Test fun `render text (Windows)`() { testRenderText(OS.Windows) diff --git a/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/redrawer/MacOsOpenGLRedrawer.kt b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/redrawer/MacOsOpenGLRedrawer.kt index d2f27848c..bae0e7f74 100644 --- a/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/redrawer/MacOsOpenGLRedrawer.kt +++ b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/redrawer/MacOsOpenGLRedrawer.kt @@ -1,6 +1,7 @@ package org.jetbrains.skiko.redrawer import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.swing.Swing import org.jetbrains.skiko.FrameDispatcher import org.jetbrains.skiko.HardwareLayer @@ -10,6 +11,14 @@ import org.jetbrains.skiko.Task import org.jetbrains.skiko.useDrawingSurfacePlatformInfo import javax.swing.SwingUtilities.convertPoint import javax.swing.SwingUtilities.getRootPane +import kotlin.system.measureNanoTime + +// Current implementation is fragile (it works in all tested cases, but we can't test everything) +// +// We should investigate can we implement our own CAOpenGLLayer, without its restrictions. +// (see, for example https://github.com/gnustep/libs-gui/blob/master/Source/NSOpenGLView.m) +// +// P.S. MacOsOpenGLRedrawer will not be used by default in the future, because we will support Metal. internal class MacOsOpenGLRedrawer( private val layer: HardwareLayer, @@ -25,6 +34,8 @@ internal class MacOsOpenGLRedrawer( layer.draw() } } + + suspend fun display() = display(::setNeedsDisplay) } // use a separate layer for vsync, because with single layer we cannot asynchronously update layer @@ -53,7 +64,7 @@ internal class MacOsOpenGLRedrawer( return canDraw } - override fun setNeedsDisplay() { + private fun requestAsyncDisplay() { // Use asynchronous mode instead of just setNeedsDisplay, // so Core Animation will wait for the next frame in vsync signal // @@ -64,13 +75,13 @@ internal class MacOsOpenGLRedrawer( // https://chromium.googlesource.com/chromium/chromium/+/0489078bf98350b00876070cf2fdce230905f47e/content/browser/renderer_host/compositing_iosurface_layer_mac.mm#57 if (!isAsynchronous) { isAsynchronous = true - super.setNeedsDisplay() + setNeedsDisplay() } } suspend fun sync() { canDraw = true - display() + display(::requestAsyncDisplay) canDraw = false } } @@ -83,9 +94,7 @@ internal class MacOsOpenGLRedrawer( drawLayer.setNeedsDisplay() vsyncLayer.sync() } else { - // If vsync is disabled we should await the drawing to end. - // Otherwise we will call 'update' multiple times. - drawLayer.display() + error("Drawing without vsync isn't supported on macOs with OpenGL") } } @@ -114,7 +123,16 @@ internal class MacOsOpenGLRedrawer( override fun redrawImmediately() { layer.update(System.nanoTime()) - drawLayer.setNeedsDisplay() + + // macOs will call 'draw' itself because of 'setNeedsDisplayOnBoundsChange=true'. + // But we schedule new frame after vsync anyway. + // Because 'redrawImmediately' can be called after 'draw', + // and we need at least one 'draw' after 'redrawImmediately'. + // + // We don't use setNeedsDisplay, because frequent calls of it are unreliable. + // 'setNeedsDisplayOnBoundsChange=true' with combination of 'scheduleFrame' is enough + // to not see the white bars on resize. + frameDispatcher.scheduleFrame() } } @@ -135,15 +153,32 @@ private abstract class AWTGLLayer(private val containerPtr: Long, setNeedsDispla get() = isAsynchronous(ptr) set(value) = setAsynchronous(ptr, value) - open fun setNeedsDisplay() = setNeedsDisplayOnMainThread(ptr) + /** + * Schedule next [draw] as soon as possible (not waiting for vsync) + * + * WARNING!!! + * + * CAOpenGLLayer will not call [draw] if we call [setNeedsDisplay] too often. + * + * Experimentally we found out that after 15 draw's between two vsync's (900 FPS on 60 Hz display) will cause + * setNeedsDisplay to not schedule the next draw at all. + * + * Only after the next vsync, [setNeedsDisplay] will be working again. + */ + fun setNeedsDisplay() { + setNeedsDisplayOnMainThread(ptr) + } - suspend fun display() = display.runAndAwait { - setNeedsDisplay() + protected suspend fun display( + startDisplay: () -> Unit + ) = display.runAndAwait { + startDisplay() } // Called in AppKit Thread protected open fun canDraw() = true + // Called in AppKit Thread @Suppress("unused") // called from native code private fun performDraw() { try {