From 2d41c8df5292142c5593c66745558c9febb16b69 Mon Sep 17 00:00:00 2001 From: Pavel Date: Fri, 2 Aug 2024 15:10:06 +0200 Subject: [PATCH] Add a flag to control number of buffers used by Metal. Add a flag to control Vsync on window resize (#968) #### Number of buffers MacOS [doc](https://developer.apple.com/documentation/quartzcore/cametallayer/2938720-maximumdrawablecount?language=objc). As far as I know AWT uses double buffering by default. So we would like to experiment with this setting in Fleet. Also I expect that double buffering should reduce user interaction latency. #### Control VSync on window resize I've noticed that on Linux and Windows we don't wait for VSync when repainting synchronously, but only on macOS we do. I suspect that locking EDT when we are waiting for VSync might lead to problems with app responsivenes. So it would be nice to experiment with this setting. --- .../org/jetbrains/skiko/SkiaLayer.awt.kt | 2 ++ .../skiko/redrawer/Direct3DRedrawer.kt | 2 +- .../skiko/redrawer/LinuxOpenGLRedrawer.kt | 9 +++++++-- .../jetbrains/skiko/redrawer/MetalRedrawer.kt | 20 ++++++++++--------- .../skiko/redrawer/WindowsOpenGLRedrawer.kt | 3 +++ .../awtMain/objectiveC/macos/MetalRedrawer.mm | 6 +++++- .../kotlin/org/jetbrains/skiko/GraphicsApi.kt | 14 +++++++++++++ .../jetbrains/skiko/SkiaLayerProperties.kt | 1 + .../org/jetbrains/skiko/SkikoProperties.kt | 20 +++++++++++++++++++ 9 files changed, 64 insertions(+), 13 deletions(-) diff --git a/skiko/src/awtMain/kotlin/org/jetbrains/skiko/SkiaLayer.awt.kt b/skiko/src/awtMain/kotlin/org/jetbrains/skiko/SkiaLayer.awt.kt index fca23000c..8340fcaf0 100644 --- a/skiko/src/awtMain/kotlin/org/jetbrains/skiko/SkiaLayer.awt.kt +++ b/skiko/src/awtMain/kotlin/org/jetbrains/skiko/SkiaLayer.awt.kt @@ -56,6 +56,7 @@ actual open class SkiaLayer internal constructor( externalAccessibleFactory: ((Component) -> Accessible)? = null, isVsyncEnabled: Boolean = SkikoProperties.vsyncEnabled, isVsyncFramelimitFallbackEnabled: Boolean = SkikoProperties.vsyncFramelimitFallbackEnabled, + frameBuffering: FrameBuffering = SkikoProperties.frameBuffering, renderApi: GraphicsApi = SkikoProperties.renderApi, analytics: SkiaLayerAnalytics = SkiaLayerAnalytics.Empty, pixelGeometry: PixelGeometry = PixelGeometry.UNKNOWN, @@ -64,6 +65,7 @@ actual open class SkiaLayer internal constructor( SkiaLayerProperties( isVsyncEnabled, isVsyncFramelimitFallbackEnabled, + frameBuffering, renderApi ), RenderFactory.Default, diff --git a/skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/Direct3DRedrawer.kt b/skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/Direct3DRedrawer.kt index 1df9fc2f1..58c049b9a 100644 --- a/skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/Direct3DRedrawer.kt +++ b/skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/Direct3DRedrawer.kt @@ -72,7 +72,7 @@ internal class Direct3DRedrawer( check(!isDisposed) { "Direct3DRedrawer is disposed" } inDrawScope { update(System.nanoTime()) - drawAndSwap(withVsync = false) + drawAndSwap(withVsync = SkikoProperties.windowsWaitForVsyncOnRedrawImmediately) } } diff --git a/skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/LinuxOpenGLRedrawer.kt b/skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/LinuxOpenGLRedrawer.kt index a70b30313..2dca0e73c 100644 --- a/skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/LinuxOpenGLRedrawer.kt +++ b/skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/LinuxOpenGLRedrawer.kt @@ -84,10 +84,15 @@ internal class LinuxOpenGLRedrawer( inDrawScope { it.makeCurrent(context) contextHandler.draw() - it.setSwapInterval(0) + val turnOfVsync = properties.isVsyncEnabled && !SkikoProperties.linuxWaitForVsyncOnRedrawImmediately + if (turnOfVsync) { + it.setSwapInterval(0) + } it.swapBuffers() OpenGLApi.instance.glFinish() - it.setSwapInterval(swapInterval) + if (turnOfVsync) { + it.setSwapInterval(swapInterval) + } } } diff --git a/skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/MetalRedrawer.kt b/skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/MetalRedrawer.kt index e33009f2b..8f6e6a8b8 100644 --- a/skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/MetalRedrawer.kt +++ b/skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/MetalRedrawer.kt @@ -67,8 +67,9 @@ internal class MetalRedrawer( init { onDeviceChosen(adapter.name) + val numberOfBuffers = properties.frameBuffering.numberOfBuffers() ?: 0 // zero means default for system val initDevice = layer.backedLayer.useDrawingSurfacePlatformInfo { - MetalDevice(createMetalDevice(layer.windowHandle, layer.transparency, adapter.ptr, it)) + MetalDevice(createMetalDevice(layer.windowHandle, layer.transparency, numberOfBuffers, adapter.ptr, it)) } _device = initDevice contextHandler = MetalContextHandler(layer, initDevice, adapter) @@ -113,7 +114,7 @@ internal class MetalRedrawer( inDrawScope { update(System.nanoTime()) if (!isDisposed) { // Redrawer may be disposed in user code, during `update` - performDraw() + performDraw(waitVsync = SkikoProperties.macOSWaitForPreviousFrameVsyncOnRedrawImmediately) } } } @@ -154,13 +155,14 @@ internal class MetalRedrawer( windowOcclusionStateChannel.trySend(isOccluded) } - private fun performDraw() = synchronized(drawLock) { + private fun performDraw(waitVsync: Boolean = true) = synchronized(drawLock) { if (!isDisposed) { - // Wait for vsync because: - // - macOS drops the second/next drawables if they are sent in the same vsync - // - it makes frames consistent and limits FPS - displayLinkThrottler.waitVSync() - + if (waitVsync) { + // Wait for vsync because: + // - macOS drops the second/next drawables if they are sent in the same vsync + // - it makes frames consistent and limits FPS + displayLinkThrottler.waitVSync() + } autoreleasepool { contextHandler.draw() } @@ -185,7 +187,7 @@ internal class MetalRedrawer( setLayerVisible(device.ptr, isVisible) } - private external fun createMetalDevice(window: Long, transparency: Boolean, adapter: Long, platformInfo: Long): Long + private external fun createMetalDevice(window: Long, transparency: Boolean, frameBuffering: Int, adapter: Long, platformInfo: Long): Long private external fun disposeDevice(device: Long) private external fun resizeLayers(device: Long, x: Int, y: Int, width: Int, height: Int) private external fun setLayerVisible(device: Long, isVisible: Boolean) diff --git a/skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/WindowsOpenGLRedrawer.kt b/skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/WindowsOpenGLRedrawer.kt index 925873eae..293c23a66 100644 --- a/skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/WindowsOpenGLRedrawer.kt +++ b/skiko/src/awtMain/kotlin/org/jetbrains/skiko/redrawer/WindowsOpenGLRedrawer.kt @@ -69,6 +69,9 @@ internal class WindowsOpenGLRedrawer( contextHandler.draw() swapBuffers() OpenGLApi.instance.glFinish() + if (SkikoProperties.windowsWaitForVsyncOnRedrawImmediately) { + dwmFlush() + } } } diff --git a/skiko/src/awtMain/objectiveC/macos/MetalRedrawer.mm b/skiko/src/awtMain/objectiveC/macos/MetalRedrawer.mm index 805fe44b7..39bcba1f0 100644 --- a/skiko/src/awtMain/objectiveC/macos/MetalRedrawer.mm +++ b/skiko/src/awtMain/objectiveC/macos/MetalRedrawer.mm @@ -118,7 +118,7 @@ static jmethodID getOnOcclusionStateChangedMethodID(JNIEnv *env, jobject redrawe { JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_createMetalDevice( - JNIEnv *env, jobject redrawer, jlong windowPtr, jboolean transparency, jlong adapterPtr, jlong platformInfoPtr) + JNIEnv *env, jobject redrawer, jlong windowPtr, jboolean transparency, jint frameBuffering, jlong adapterPtr, jlong platformInfoPtr) { @autoreleasepool { id adapter = (__bridge id) (void *) adapterPtr; @@ -133,6 +133,10 @@ JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_MetalRedrawer_createMe [container setNeedsDisplayOnBoundsChange: YES]; AWTMetalLayer *layer = [AWTMetalLayer new]; + if (frameBuffering == 2 || frameBuffering == 3) { + layer.maximumDrawableCount = frameBuffering; + } + [container addSublayer: layer]; layer.javaRef = env->NewGlobalRef(redrawer); diff --git a/skiko/src/commonMain/kotlin/org/jetbrains/skiko/GraphicsApi.kt b/skiko/src/commonMain/kotlin/org/jetbrains/skiko/GraphicsApi.kt index d7e6019c2..ae45461bc 100644 --- a/skiko/src/commonMain/kotlin/org/jetbrains/skiko/GraphicsApi.kt +++ b/skiko/src/commonMain/kotlin/org/jetbrains/skiko/GraphicsApi.kt @@ -29,3 +29,17 @@ enum class GpuPriority(val value: String) { fun parseOrNull(value: String): GpuPriority? = GpuPriority.values().find { it.value == value } } } + +enum class FrameBuffering { + DEFAULT, + DOUBLE, + TRIPLE +} + +fun FrameBuffering.numberOfBuffers(): Int? { + return when (this) { + FrameBuffering.DEFAULT -> null + FrameBuffering.DOUBLE -> 2 + FrameBuffering.TRIPLE -> 3 + } +} \ No newline at end of file diff --git a/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkiaLayerProperties.kt b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkiaLayerProperties.kt index b3c1d0867..6bed4af7e 100644 --- a/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkiaLayerProperties.kt +++ b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkiaLayerProperties.kt @@ -18,6 +18,7 @@ package org.jetbrains.skiko class SkiaLayerProperties( val isVsyncEnabled: Boolean = SkikoProperties.vsyncEnabled, val isVsyncFramelimitFallbackEnabled: Boolean = SkikoProperties.vsyncFramelimitFallbackEnabled, + val frameBuffering: FrameBuffering = SkikoProperties.frameBuffering, val renderApi: GraphicsApi = SkikoProperties.renderApi, val adapterPriority: GpuPriority = SkikoProperties.gpuPriority, ) { diff --git a/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkikoProperties.kt b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkikoProperties.kt index 621e92c61..1b4ee47fa 100644 --- a/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkikoProperties.kt +++ b/skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkikoProperties.kt @@ -34,6 +34,26 @@ object SkikoProperties { val vsyncEnabled: Boolean get() = getProperty("skiko.vsync.enabled")?.toBoolean() ?: true + val frameBuffering: FrameBuffering get() { + return when (getProperty("skiko.buffering")) { + "DOUBLE" -> FrameBuffering.DOUBLE + "TRIPLE" -> FrameBuffering.TRIPLE + else -> FrameBuffering.DEFAULT + } + } + + val macOSWaitForPreviousFrameVsyncOnRedrawImmediately: Boolean get() { + return getProperty("skiko.rendering.macos.waitForPreviousFrameVsyncOnRedrawImmediately")?.toBoolean() ?: true + } + + val windowsWaitForVsyncOnRedrawImmediately: Boolean get() { + return getProperty("skiko.rendering.windows.waitForFrameVsyncOnRedrawImmediately")?.toBoolean() ?: false + } + + val linuxWaitForVsyncOnRedrawImmediately: Boolean get() { + return getProperty("skiko.rendering.linux.waitForFrameVsyncOnRedrawImmediately")?.toBoolean() ?: false + } + /** * If vsync is enabled, but platform can't support it (Software renderer, Linux with uninstalled drivers), * we enable frame limit by the display refresh rate.