Lwjgl3: Screen tearing on Mac with vsync

Created on 16 Sep 2019  ยท  15Comments  ยท  Source: LWJGL/lwjgl3

Environment

  • LWJGL version: 3.2.4-SNAPSHOT
  • LWJGL build #: 20190903.201424-1
  • Java version: 12.0.2
  • Platform: macOS 10.14.6
  • Module: not sure

Description

OpenGL windows on my Mac have screen tearing near the bottom of the screen, even with vsync on. It looks like this is a regression starting with 3.2.2. When I use version 3.2.1, the problem isn't there. Also, the problem isn't there on Windows with the latest LWJGL version.

Example that shows the problem based on the SimpleDrawElements demo from lwjgl3-demos.

When I run this example on 3.2.2 or newer, the screen tearing starts after a few seconds (before this, the animation is slow but doesn't tear). The screen tearing is only there when the window is near the bottom of the screen.

Using 3.2.1, the animation is always fast and smooth (no screen tearing).

Bug 3rd party

Most helpful comment

Ok, after spending quite some time with lldb I've finally figured this out ๐Ÿ˜Ž

It is actually a macOS + OpenJDK java executable issue:
This is not an issue with LWJGL and it is not an issue with GLFW. It is an issue with macOS (Catalina 10.15, probably already with Mojave 10.14) and the macOS version that the java executable is being build on (I only checked OpenJDK Java 12 which is build using High Sierra, 10.13 but suspect other Java versions are effected as well).

Reason:
Somewhere deep inside AppKit a NSView is being connected with a NSCGLSurface. This happens somewhere inside [NSOpenGLContext setView:]. According to some logic in there it's either NSOpenGLContextAttachOffScreenViewSurface or NSOpenGLContextAttachOnScreenViewSurface. Only the NSOpenGLContextAttachOffScreenViewSurface path leads to NSCGLSurfaceFlush being called later in glSwap_Exec (that is the good case as in tearing.c).

The method that seems to decide which way to go is called _NSViewAllowsRootLayerBackingDefaultValueFunction and it looks like this (disassembled using hopper):

int _NSViewAllowsRootLayerBackingDefaultValueFunction(int arg0, int arg1) {
    rsi = arg1;
    rdi = arg0;
    if (*_NSViewLinkedOnLiberty.onceToken != 0xffffffffffffffff) {
            rdi = _NSViewLinkedOnLiberty.onceToken;
            rsi = ^ {/* block implemented at ___NSViewLinkedOnLiberty_block_invoke */ } };
            rax = dispatch_once(rdi, rsi);
    }
    if (*(int8_t *)_NSViewLinkedOnLiberty.isLinkedOnLiberty != 0x0) {
            rax = 0x1;
            rbp = stack[-8];
            rsp = rsp + 0x8;
    }
    else {
            if (_NSRequiresAquaSystemAppearance(rdi, rsi) != 0x0) {
                    rdi = @"com.autodesk.Maya.2016";
                    xmm0 = intrinsic_movsd(xmm0, *double_value_minus_1);
                    if (_CFAppVersionLessThan() == 0x0) {
                            rdi = @"com.autodesk.Maya.2016";
                            rdi = @"com.autodesk.Maya.2017";
                            xmm0 = intrinsic_movsd(xmm0, *double_value_minus_1);
                            if (_CFAppVersionLessThan() == 0x0) {
                                    rdi = @"com.autodesk.Maya.2017";
                                    rdi = @"com.autodesk.Maya.2018";
                                    xmm0 = intrinsic_movsd(xmm0, *double_value_minus_1);
                                    if (_CFAppVersionLessThan() == 0x0) {
                                            rdi = @"com.autodesk.Maya.2018";
                                            rdi = @"com.autodesk.Maya.2019";
                                            xmm0 = intrinsic_movsd(xmm0, *double_value_minus_1);
                                            rax = _CFAppVersionLessThan();
                                            rcx = 0x0;
                                            rax = rax != 0x0 ? 0x1 : 0x0;
                                            rbp = stack[-8];
                                            rsp = rsp + 0x8;
                                    }
                                    else {
                                            rax = 0x1;
                                            rbp = stack[-8];
                                            rsp = rsp + 0x8;
                                    }
                            }
                            else {
                                    rax = 0x1;
                                    rbp = stack[-8];
                                    rsp = rsp + 0x8;
                            }
                    }
                    else {
                            rax = 0x1;
                            rbp = stack[-8];
                            rsp = rsp + 0x8;
                    }
            }
            else {
                    rax = 0x1;
                    rbp = stack[-8];
                    rsp = rsp + 0x8;
            }
    }
    return rax;
}

Please note a couple of interesting things in here:

  1. It first checks if the executable (!) isLinkedOnLiberty. According to Wikipedia this means "macOS Mojave 10.14 โ€“ Liberty".
  2. The OpenJDK java 12 executable which I checked is linked with 10.13 since: otool -v -l /Library/Java/JavaVirtualMachines/jdk-12.0.2.jdk/Contents/Home/bin/java | grep sdk returns "sdk 10.13".
  3. So when running with the java executable isLinkedOnLiberty returns 0 which makes the method continue.
  4. Some NSRequiresAquaSystemAppearance default setting is checked and if this is not "No" (= 0x00) it checks for "com.autodesk.Maya." - kinda interesting that Maya is getting a work around ...
  5. The end result is that in the java executable case we'll never return with 0x01 from this method and later end up with NSOpenGLContextAttachOnScreenViewSurface which will create slow performance and the tearing problems.

Workaround (maybe):
I've asked google about the NSRequiresAquaSystemAppearance and couldn't really find out what it means. However, it seems to have something to do with the Light and Dark MenuBar in macOS Mojave and how to use this setting as a workaround, f.i. this link: How to Use Light Theme with Dark Menu Bar and Dock in MacOS Mojave is using it for this purpose.

So what if we just force this to 0x00 using:

defaults write -g NSRequiresAquaSystemAppearance -bool No

and then run our tearing.java?

Yep, we'll get equal performance in FPS in the java port of tearing.c as in the original and the tearing is gone as well!

Tadda! ๐ŸŽ‰ ๐ŸŽˆ ๐Ÿฐ

What I've tested:

  • defaults write -g NSRequiresAquaSystemAppearance -bool Yes -> tearing
  • defaults write -g NSRequiresAquaSystemAppearance -bool No -> no tearing
  • defaults delete -g NSRequiresAquaSystemAppearance -> tearing (this is probably the default value).

Real solution
Realistically speaking I'm not sure if that workaround really counts since I don't know what NSRequiresAquaSystemAppearance really does and if one really wants to set it to No. I'm not sure about side effects and so on.

So the actual real solution would be:

  • building the OpenJDK java executable with MacOS Mojave, 10.14 or higher or
  • if we find a way to embed the java executable into some 10.14+ build executable wrapper or
  • macOS is changing this again.

But this is not an LWJGL issue at all and I don't think it can be fixed or workaround in GLFW.

All 15 comments

Hey @marshallsa,

GLFW 3.3 has switched to CVDisplayLink synchronization because vsync was broken in macOS 10.14. It is a known issue that this causes screen tearing. The glfw build in LWJGL will be updated immediately when a fix is available.

I do agree that this is an issue in GLFW but something is weird here.

I'm experiencing screen tearing with LWJGL version 3.2.3 and 3.2.4-SNAPSHOT as well. There is none with 3.2.1. However, I'm on macOS 10.15 Catalina.

So far, the explanation that this is GLFW 3.3 related totally makes sense.

However, when I build GLFW 3.3 from source using the command line tools that come with Xcode 10.3, I don't see any tearing when I run the tearing example of GLFW tearing.c. Everything is super smooth when setting the swap interval to 1 by using the cursor up key when running their example.

Then I just did a quick port of the tearing.c example to LWJGL Tearing.java and it does show tearing when I run it with LWJGL 3.2.2+ and with swap interval set to 1. There is no tearing in this example in Java when using LWJGL 3.2.1.

So now I'm a bit puzzled ๐Ÿ˜•

What's the difference between GLFW 3.3 build from source by myself and the one included in LWJGL 3.2.3? ๐Ÿ˜ณ

What's the difference between GLFW 3.3 build from source by myself and the one included in LWJGL 3.2.3?

See LWJGL-CI/glfw. The LWJGL build uses -DCMAKE_OSX_DEPLOYMENT_TARGET=10.9, but I'm not sure how that would explain the difference (CVDisplayLink is supported on 10.4+).

I quickly build GLFW the way LWJGL-CL does it (I had to leave out -DGLFW_BUILD_TESTS=OFF so that it builds the tearing test though):

rm -rf *
cmake -DBUILD_SHARED_LIBS=ON -DGLFW_BUILD_EXAMPLES=OFF \
-DGLFW_BUILD_DOCS=OFF -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_OSX_DEPLOYMENT_TARGET=10.9 ../glfw
cmake --build .
strip -u -r src/libglfw.dylib

open tests/tearing.app/

but didn't see any difference. Still no tearing. Still weird. I tried the GLFW 3.3 and master branch.

The only other difference I see is the osx_image: xcode11.1 which is different from my 10.3.
I'll try it with Xcode 11.1 next ... when the download finishes someday :wink:

I have successesfully build GLFW with Xcode 11.1 and still cannot reproduce any screen tearing with the original GLFW tearing.c test (macOS 10.15 Catalina on a 13 Inch 2016 Macbook Pro). I even tried slowing the tearing test down by rendering the white bar a couple hundred times but nothing. No tearing at all.

At least for my current macOS + hardware configuration I cannot confirm screen tearing using GLFW 3.3.

However, I still get the screen tearing when using the current LWJGL version using my Tearing.java which I've updated to be even closer to the original tearing.c. Additionally, I even built LWJGL from source and used the libglfw.dylib that I've build locally. Still, the tearing remains.

I'm still puzzled about that. There seems to be some weird interaction between Java, LWJGL and the CVDisplayLink synchronization going on that I'm not able to pinpoint.

Reproduced. Not only is there tearing in the Java port, the frame rate is roughly half of the native version. Even if I remove everything from the render loop except glfwSwapBuffers. Do you see this too?

Quick test: Change the native GLFW executable's name to java.exe and see whether it exhibits the same behaviour as the Java port. Wouldn't be the first time that a driver alters behaviour based on the executable's name...

@Spasi Yes, same here! The original tearing.c runs with about 1900 FPS while tearing.java runs with about 700 FPS and is way more unstable too (I see a lot of big changes in FPS while tearing.c is pretty steady). This is running with swap interval 0.

Additionally if I attach a secondary display things get even more strange: with the original tearing.c everything stays the same as expected: no tearing, the same FPS and if I drag the window to the external display I get twice the amount of FPS (approx. 3000). So far so good.

Doing the same with tearing.java has some unexpected results:

  • The FPS are now locked to about 60 FPS even with swap interval set to zero and setting swap interval to 1 is not changing anything.
    - I don't have tearing now! ๐Ÿ˜ณ
    - If I move the window around between screens and resize it then sometimes I see the tearing again but only for a short moment. Somehow it catches up and the tearing is gone again after a while.

Edit: Tearing is still there! I was using LWJGL 3.2.1, sorry .. With LWJGL 3.2.3 there is still tearing. The 60 FPS lock even with swap interval set to 0 remains though.

Weird! ๐ŸŽƒ

@httpdigest Renaming "tearing" to "java" did not change anything.

Since this is still annoying me - a lot actual :wink: I did more research.

I've used instruments time profiler to profile both, the tearing.c and the tearing.java and there are indeed a couple of differences:

  1. While tearing.c utilizing constantly about 70% CPU in the Main Thread, tearing.java is using way less. If I check the Main Thread in detail it seems to do nothing for a couple of ms o_O while in tearing.c it is constantly busy.
  2. When I take a closer look at the stacktraces then in tearing.c NSCGLSurfaceFlush is called somewhere. This is not the case for tearing.java! It is calling gIdPresentFramebufferData but not NSCGLSurfaceFlush. I did confirm the latter using lldb with a breakpoint on NSCGLSurfaceFlush (tearing.c is stopping there, tearing.java is not)

I have a screenshot explaining this a little bit graphical:
strange

I checked the pixel formats of both, tearing.c and tearing.java using OpenGL Profiler and they are identical. However, for tearing.c I see two OpenGLContexts listed under pixel formats in OpenGL Profiler - one without a pixel format and one with a pixel format. For tearing.java I only see one. That's a bit odd as well.

Maybe this is all a coincidence but somewhere there has to be a difference that in the end makes Apple's graphic driver to change path somewhere.

What is visible in the stacktrace is, that the java version is using a CFRunLoop (not in the screenshot unfortunately) while tearing.c is using just the plain main method that is calling into GL. Maybe this leads to the Main Thread not being constanly busy?

Any macOS runtime or OpenGL Gurus listening that have any idea explaining the difference?

Ok, after spending quite some time with lldb I've finally figured this out ๐Ÿ˜Ž

It is actually a macOS + OpenJDK java executable issue:
This is not an issue with LWJGL and it is not an issue with GLFW. It is an issue with macOS (Catalina 10.15, probably already with Mojave 10.14) and the macOS version that the java executable is being build on (I only checked OpenJDK Java 12 which is build using High Sierra, 10.13 but suspect other Java versions are effected as well).

Reason:
Somewhere deep inside AppKit a NSView is being connected with a NSCGLSurface. This happens somewhere inside [NSOpenGLContext setView:]. According to some logic in there it's either NSOpenGLContextAttachOffScreenViewSurface or NSOpenGLContextAttachOnScreenViewSurface. Only the NSOpenGLContextAttachOffScreenViewSurface path leads to NSCGLSurfaceFlush being called later in glSwap_Exec (that is the good case as in tearing.c).

The method that seems to decide which way to go is called _NSViewAllowsRootLayerBackingDefaultValueFunction and it looks like this (disassembled using hopper):

int _NSViewAllowsRootLayerBackingDefaultValueFunction(int arg0, int arg1) {
    rsi = arg1;
    rdi = arg0;
    if (*_NSViewLinkedOnLiberty.onceToken != 0xffffffffffffffff) {
            rdi = _NSViewLinkedOnLiberty.onceToken;
            rsi = ^ {/* block implemented at ___NSViewLinkedOnLiberty_block_invoke */ } };
            rax = dispatch_once(rdi, rsi);
    }
    if (*(int8_t *)_NSViewLinkedOnLiberty.isLinkedOnLiberty != 0x0) {
            rax = 0x1;
            rbp = stack[-8];
            rsp = rsp + 0x8;
    }
    else {
            if (_NSRequiresAquaSystemAppearance(rdi, rsi) != 0x0) {
                    rdi = @"com.autodesk.Maya.2016";
                    xmm0 = intrinsic_movsd(xmm0, *double_value_minus_1);
                    if (_CFAppVersionLessThan() == 0x0) {
                            rdi = @"com.autodesk.Maya.2016";
                            rdi = @"com.autodesk.Maya.2017";
                            xmm0 = intrinsic_movsd(xmm0, *double_value_minus_1);
                            if (_CFAppVersionLessThan() == 0x0) {
                                    rdi = @"com.autodesk.Maya.2017";
                                    rdi = @"com.autodesk.Maya.2018";
                                    xmm0 = intrinsic_movsd(xmm0, *double_value_minus_1);
                                    if (_CFAppVersionLessThan() == 0x0) {
                                            rdi = @"com.autodesk.Maya.2018";
                                            rdi = @"com.autodesk.Maya.2019";
                                            xmm0 = intrinsic_movsd(xmm0, *double_value_minus_1);
                                            rax = _CFAppVersionLessThan();
                                            rcx = 0x0;
                                            rax = rax != 0x0 ? 0x1 : 0x0;
                                            rbp = stack[-8];
                                            rsp = rsp + 0x8;
                                    }
                                    else {
                                            rax = 0x1;
                                            rbp = stack[-8];
                                            rsp = rsp + 0x8;
                                    }
                            }
                            else {
                                    rax = 0x1;
                                    rbp = stack[-8];
                                    rsp = rsp + 0x8;
                            }
                    }
                    else {
                            rax = 0x1;
                            rbp = stack[-8];
                            rsp = rsp + 0x8;
                    }
            }
            else {
                    rax = 0x1;
                    rbp = stack[-8];
                    rsp = rsp + 0x8;
            }
    }
    return rax;
}

Please note a couple of interesting things in here:

  1. It first checks if the executable (!) isLinkedOnLiberty. According to Wikipedia this means "macOS Mojave 10.14 โ€“ Liberty".
  2. The OpenJDK java 12 executable which I checked is linked with 10.13 since: otool -v -l /Library/Java/JavaVirtualMachines/jdk-12.0.2.jdk/Contents/Home/bin/java | grep sdk returns "sdk 10.13".
  3. So when running with the java executable isLinkedOnLiberty returns 0 which makes the method continue.
  4. Some NSRequiresAquaSystemAppearance default setting is checked and if this is not "No" (= 0x00) it checks for "com.autodesk.Maya." - kinda interesting that Maya is getting a work around ...
  5. The end result is that in the java executable case we'll never return with 0x01 from this method and later end up with NSOpenGLContextAttachOnScreenViewSurface which will create slow performance and the tearing problems.

Workaround (maybe):
I've asked google about the NSRequiresAquaSystemAppearance and couldn't really find out what it means. However, it seems to have something to do with the Light and Dark MenuBar in macOS Mojave and how to use this setting as a workaround, f.i. this link: How to Use Light Theme with Dark Menu Bar and Dock in MacOS Mojave is using it for this purpose.

So what if we just force this to 0x00 using:

defaults write -g NSRequiresAquaSystemAppearance -bool No

and then run our tearing.java?

Yep, we'll get equal performance in FPS in the java port of tearing.c as in the original and the tearing is gone as well!

Tadda! ๐ŸŽ‰ ๐ŸŽˆ ๐Ÿฐ

What I've tested:

  • defaults write -g NSRequiresAquaSystemAppearance -bool Yes -> tearing
  • defaults write -g NSRequiresAquaSystemAppearance -bool No -> no tearing
  • defaults delete -g NSRequiresAquaSystemAppearance -> tearing (this is probably the default value).

Real solution
Realistically speaking I'm not sure if that workaround really counts since I don't know what NSRequiresAquaSystemAppearance really does and if one really wants to set it to No. I'm not sure about side effects and so on.

So the actual real solution would be:

  • building the OpenJDK java executable with MacOS Mojave, 10.14 or higher or
  • if we find a way to embed the java executable into some 10.14+ build executable wrapper or
  • macOS is changing this again.

But this is not an LWJGL issue at all and I don't think it can be fixed or workaround in GLFW.

Just. Wow.

Can confirm that, by just recompiling the packr executable on 10.15 Catalina, screen tearing is gone on my MacBook Pro.

Great work @void256!

Have you tried with NSGL: Remove problematic swap interval workaround?

I did now and have build their "ci" branch. With this branch we're back at LWJGL 3.2.1 behaviour:

tearing.java:

  • glfwSwapInterval = 0: lots of tearing and half the fps of tearing.c (about 400 fps)
  • glfwSwapInterval = 1: no tearing and 60 fps
  • glfwSwapInterval = 2: no tearing and 60 fps

for comparision:

tearing.c:

  • glfwSwapInterval = 0: no tearing and about 800 fps
  • glfwSwapInterval = 1: no tearing and 60 fps
  • glfwSwapInterval = 2: no tearing and 60 fps (this really should be 30 and was with the CVDisplayLink patch applied as it is currently still the case in the master branch of GLFW)

Applying defaults write -g NSRequiresAquaSystemAppearance -bool No to the java executable I'll get the same behaviour as in tearing.c, about 800 fps and no tearing with glfwSwapInterval = 0.

So far everything is as I would expected it.

However, in general it seems that with the CVDisplayLink patch applied to GLFW we get a little better performance in the tearing.c and tearing.java + workaround example (about 1400 fps instead of 800) and we get the correct behaviour for glfwSwapInterval = 2 which is 30 fps.

Speaking only for my system (MacBook Pro and macOS catalina) I'd prefer the version of GLFW with CVDisplayLink and a java executable build with macOS Mojava for best performance ๐Ÿ˜Ž

I think at some point the latter part is solving itself when the OpenJDK mac build is changed one day to Mojava instead of High Sierra.

I've now checked the OpenJDK 13 build and it is build with macOS Mojave (10.14) already and guess what? The problems are gone when using OpenJDK 13 ๐Ÿ˜Ž

If you can, use LWJGL 3.2.3+ with java 13+ and enjoy tearing free, equal to c :wink: fps performance!

๐ŸŽ‰ ๐Ÿš€ ๐Ÿฐ

This should be fixed in the latest snapshot (LWJGL 3.2.4 build 4).

Was this page helpful?
0 / 5 - 0 ratings