Skip to content

Commit

Permalink
Add a flag to control number of buffers used by Metal. Add a flag to …
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
SergeevPavel authored Aug 2, 2024
1 parent c8147b1 commit 2d41c8d
Show file tree
Hide file tree
Showing 9 changed files with 64 additions and 13 deletions.
2 changes: 2 additions & 0 deletions skiko/src/awtMain/kotlin/org/jetbrains/skiko/SkiaLayer.awt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -64,6 +65,7 @@ actual open class SkiaLayer internal constructor(
SkiaLayerProperties(
isVsyncEnabled,
isVsyncFramelimitFallbackEnabled,
frameBuffering,
renderApi
),
RenderFactory.Default,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ internal class Direct3DRedrawer(
check(!isDisposed) { "Direct3DRedrawer is disposed" }
inDrawScope {
update(System.nanoTime())
drawAndSwap(withVsync = false)
drawAndSwap(withVsync = SkikoProperties.windowsWaitForVsyncOnRedrawImmediately)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
}
}
Expand Down Expand Up @@ -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()
}
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ internal class WindowsOpenGLRedrawer(
contextHandler.draw()
swapBuffers()
OpenGLApi.instance.glFinish()
if (SkikoProperties.windowsWaitForVsyncOnRedrawImmediately) {
dwmFlush()
}
}
}

Expand Down
6 changes: 5 additions & 1 deletion skiko/src/awtMain/objectiveC/macos/MetalRedrawer.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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<MTLDevice> adapter = (__bridge id<MTLDevice>) (void *) adapterPtr;
Expand All @@ -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);

Expand Down
14 changes: 14 additions & 0 deletions skiko/src/commonMain/kotlin/org/jetbrains/skiko/GraphicsApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
) {
Expand Down
20 changes: 20 additions & 0 deletions skiko/src/jvmMain/kotlin/org/jetbrains/skiko/SkikoProperties.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 2d41c8d

Please sign in to comment.