libsdl-org / SDL

Simple Directmedia Layer
https://libsdl.org
zlib License
10.16k stars 1.86k forks source link

macOS 12 Monterey - odd/broken OpenGL VSync behavior #4918

Closed directmusic closed 2 years ago

directmusic commented 3 years ago

As of macOS 12 Monterey VSync when using OpenGL behaves differently on different machines or just outright fails.

M1 Mac Mini - VSync gets 2x the monitor's refresh rate. (tested at 60hz, 120hz, 144hz) M1 MacBook Air - Same as above. Intel MacBook Pro 2017 - VSync (silently) fails completely unlocking the FPS. M1 Max MacBook Pro 2021 - VSync works as expected on internal monitor. I can't test an external due to it being reported by a user of my software.

Here is a cpp file that demonstrates the failure: monterey vsync failure.zip

However, I have downloaded and tested the GLEssentials example from these old Apple OpenGL examples and it does not seem to exhibit this behavior: https://github.com/lmdsp/samples_apple_gl

-- I do not believe this is an issue with SDL specifically as I have also observed it in GLFW so it may be a Monterey issue all together (it is clear something changed on Apple's end). I have reported the issue to Apple in the beta feedback app, but I believe this is worth reporting here as well in case there is a workaround.

directmusic commented 3 years ago

I just tried this patch mentioned here and it has fixed the issue. This appears to be a regression on Apple's part.

icculus commented 3 years ago

I just tried this patch mentioned here and it has fixed the issue.

Fwiw, we backed that out because it causes other issues, and a later Mojave update fixed Apple's bug in their OpenGL implementation.

My hope is Apple will also fix the new issue, but we all nervously await the time when OpenGL stops getting new fixes on macOS.

leonstyhre commented 3 years ago

FYI, here's an ugly hack that at least makes my application usable on Monterey:

const auto beforeSwap = std::chrono::system_clock::now();
SDL_GL_SwapWindow(getSDLWindow());
const auto afterSwap = std::chrono::system_clock::now();

if (std::chrono::duration_cast<std::chrono::milliseconds>(afterSwap - beforeSwap).count() < 3.0)
    SDL_Delay(10);

I make this code optional via a menu option so it's controllable by the user. I really hope Apple will fix this properly.

dmitshur commented 2 years ago

In case it's helpful here, reports in https://github.com/glfw/glfw/issues/1990 suggest this issue may be fixed in macOS 12.1 (21C52), released today.

whabilly commented 2 years ago

On both my Mac mini M1 and iMac 5K 27" with Intel i5 running macOS 12.1 (21C52), OpenGL VSync appears to be fixed only at 60Hz... and no other frame rates supported by my display (50, 100, 120 and 144Hz). Can anyone else confirm???

peppy commented 2 years ago

Can confirm that 144hz is not working correctly. Looks to hover around 70-80fps for me.

DctrNoob commented 2 years ago

Having no vsync under macOS is fixed for me with the latest macOS patch 12.1.

directmusic commented 2 years ago

I am also only seeing VSync at 60hz on macOS 12.1. Higher refresh rates show an inconsistent FPS as some lower value (120hz is ~80hz)

DanielGibson commented 2 years ago

VSync capping to 80Hz instead of 120Hz sounds like the problem #4839 is supposed to fix

whabilly commented 2 years ago

Does not seem to be fixed in the just released macOS 12.2...

muswolf432 commented 2 years ago

I am also having this issue in Heroes of the Storm. M1 MacBook Air (8GB, 256GB), macOS 12.2.

dortamiguel commented 2 years ago

@directmusic I get the same issue as you

dortamiguel commented 2 years ago

I upgraded to latest macos 12.3 and the issue is still there, I get 80fps when using vsync instead of the 120fps that promotion needs.

HeySora commented 2 years ago

For what it's worth, it's still not fixed in macOS 12.4 (21F5048e). Running a monitor up to 100Hz does not exhibit any issue, but going over that seems to be making one vsync wait out of two take twice as long, resulting in a 80Hz refresh on 120Hz, and ~95Hz refresh on 144Hz

dortamiguel commented 2 years ago

so the only solution is just to wait for apple to fix it?

icculus commented 2 years ago

Putting this in the 2.26 milestone to ping our Apple contacts about it.

tycho commented 2 years ago

I've observed similar behavior in Metal or MoltenVK apps. I suspect it may be the same issue as seen in OpenGL applications.

In my case, I've only seen this happen in fullscreen mode, and it appears to be tied to the "direct-to-display" feature that is intended to be faster than going through the compositor.

Even just running the Vulkan SDK "vkcube" sample will replicate the issue (shown below with the macOS 13 and later MTL_HUD_ENABLED=1 environment variable):

inconsistent nextDrawable performance - direct-to-display

The blue line graph in the overlay on the upper right is the frame present time, and is where things are wildly inconsistent. Even though the display is 120Hz (the built-in display in the 14" MacBook Pro), the average framerate is only ~87FPS due to the present timing.

In my case, I found that if I can bring up the "Force Quit Applications" dialog (Command+Option+Esc) and leave it on top of the app's fullscreen window, the present timings become consistently good as they're forced to go through the compositor, illustrated here:

good nextDrawable performance - composited

I would be interested to hear if the same hack makes ordinary OpenGL applications behave. If you want to try this, you'll need to either use the SDL_WINDOW_FULLSCREEN_DESKTOP mode or use a resizable window and hit the green 'full screen' button in the upper left corner of the window. Note that if you use an SDL_WINDOW_FULLSCREEN window, then Command+Option+Esc will just kill the application instead of showing the "Force Quit Applications" dialog.

In the Metal/MoltenVK case, it appears what happens is that while direct-to-display is enabled, CAMetalLayer's nextDrawable sometimes takes much longer than it should (maybe the driver is not releasing presented drawables when it should?). I have an open Feedback Assistant report with Apple about this (FB11424542) but haven't heard anything back yet.

leonstyhre commented 2 years ago

I just upgraded my Mac Mini M1 to Ventura and my application is now screwed up again with VSync apparently not working any longer. It was exactly the same issue when installing Monterey until Apple fixed it with an OS update (probably 12.1, can't remember). After that update it was fully stable on Monterey. It's very encouraging to hear that Metal applications are also broken with similar issues as it means Apple will hopefully spend some effort on fixing the problem.

Edit: I just tested with running my application in windowed mode and it's exactly the same problem, VSync doesn't work there either. I can't recall if that was the case when Monterey was originally released but I would guess so.

ptxmac commented 2 years ago

This is also an issue in the latest macos 13.1 beta

genericptr commented 2 years ago

I'm using SDL 2.24.1 and the vsync is all over the place. It's supposed to be 120FPS on my system I believe (see below) but it only hits that at times and fluctuates wildly. I forgot when this started happening but it was in the last 6 months.

Screenshot 2022-11-11 at 8 26 29 AM
vkedwardli commented 2 years ago

@genericptr 14" MacBook Pro with M1 Pro has a ProMotion (dynamic refresh rate) enabled monitor (Not sure about the ProMotion support in SDL2)

You may try to change your refresh rate to fixed 60Hz first, and the check if the VSync is still fluctuating?

genericptr commented 2 years ago

@genericptr 14" MacBook Pro with M1 Pro has a ProMotion (dynamic refresh rate) enabled monitor (Not sure about the ProMotion support in SDL2)

You may try to change your refresh rate to fixed 60Hz first, and the check if the VSync is still fluctuating?

Thanks, I did not know that. There's also a bug in os 13 though. I was seeing jittering dragging around windows and just now I had to restart my Mac and the frame rate has stabilized back to 120 fps where it was getting like 110-118 before and dropping to 80 even randomly. Something is wrong for sure with the new OS and not due to SDL I now believe.

EDIT: just after typing this it's back to around ~108 again! :) Ok so maybe it is the monitor settings. Makes no sense why you want this though. On my external display it was doing strange things also but is it related to SDL?

EDIT 2: just tried changing to a fixed 60Hz which makes the OS feel sluggish moving around windows but I don't see a different FPS coming from SDL. What mean?

vkedwardli commented 2 years ago

Thanks for verifying, I don't own a ProMotion machine.

The OpenGL on Apple Silicon is implemented on top of Metal (pretty much like MoltenVK) So it may be even harder to come up with a proper workaround for OpenGL if the bug is from the Metal layer

genericptr commented 2 years ago

What really is the problem? As you mentioned OpenGL doesn't really exist any macOS anymore except for the API itself but this doesn't seem related to OpenGL anyways. I'm seeing chunky window dragging on my external display (60Hz) in the OS itself and I swear this just started happening with macOS 13 but I could be wrong. Apple constantly breaks things with updates these so I assume we just need to wait for them to fix it. At most SDL could maybe use another API for vsync (if that exists) or review their implementation in extreme detail to see if Apple broke something that can be worked around. 🤷

leonstyhre commented 2 years ago

I think VSync is simply broken in Ventura. As mentioned earlier the exact same thing happened when Monterey was released and I think it was fixed in the 12.1 update (and it has been working fine ever since on this OS). It's the exact same behavior now in Ventura. Fullscreen or windowed mode does not make any difference, both are broken. I have only tested with OpenGL though, not sure if there are similar issues with Vulkan or Metal applications.

Does anyone know if Apple is aware of the problem and if work is ongoing to fix it?

directmusic commented 2 years ago

It does appear that VSync is simply broken now on Ventura. I tested a MacBook with a 60hz screen and it exhibits the same exact ~80hz behavior as it does on my 120hz MacBook.

However, I have a version of my application that gets a Metal context with SDL (rather than an OpenGL one) and then uses Metal commands to do all of the rendering and it appears to VSync correctly on Ventura.

This might be worth using to test: Minimal C SDL2 Metal example

genericptr commented 2 years ago

I just tested my app with another platform layer I made which uses NSOpenGLContext and CVDisplayLink API for vsync and I'm getting a solid 120 fps which means something is wrong with SDL actually. Which API is SDL using for vsync?

icculus commented 2 years ago

We had a CVDisplayLink version (the last time this broke in macOS), and it caused several other problems. I'll follow up with Apple.

genericptr commented 2 years ago

Thanks. I don't know either but last time I checked (a long time ago!) this was the preferred and lowest-level method. Looks like all I'm doing is using a semaphore to unblock the main thread in swap buffers when the display link callback is invoked. No idea if this is correct but I'm getting a solid 120 FPS on my system.

icculus commented 2 years ago

So one of the reasons we removed this is because it didn't deal with different displays at different refresh rates, but I can see from the original patch (13869f194c5b975aefac6517e58998f11108d722) that we set the display at startup and never change it, so that is probably fixable. I can't remember what the other problems were or if it was just "this is more code to go wrong and Apple fixed their bug anyhow."

@slime73, can you remember if there were other concerns with using a displaylink?

tycho commented 2 years ago

I tested out using CVDisplayLink on my M1 Max with the built-in 120Hz display and it seemed to make things worse, though I'm using Metal rather than GL. I think the CAMetalLayer uses a CVDisplayLink under the hood when displaySyncEnabled is true, but I get even worse results (significant tearing, stuttering) with displaySyncEnabled off + CVDisplayLink than I do with displaySyncEnabled on and no explicit CVDisplayLink.

I might have the implementation wrong though, not sure if I need to do the draw+present within the displaylink callback (or even just the present?) before returning or if the semaphore/condvar signaling to block/unblock the thread doing draw+present is actually sufficient.

genericptr commented 2 years ago

For reference I'm using NSOpenGLContext and simply calling NSView.setNeedsDisplay after the semaphore unlocks due to the display link firing (during the swap buffer call). If a CALayer is involved my approach may not be relevant.

icculus commented 2 years ago

My understanding is that the display link callback is just a high-priority thread that wakes up at probably-reliable intervals, which is to say it is just guessing when you should draw your next frame and not actually attached to the GPU at all (the documentation says it can estimate the frame time incorrectly).

We only used it to signal a condition variable, where, if we were trying to sync to the swap interval, would unblock a call to SDL_GL_SwapBuffers...so OpenGL, in theory, could queue up all its work to that point, and submit it as soon as the DisplayLink callback ran.

genericptr commented 2 years ago

Unblocking a call to SDL_GL_SwapBuffers sounds like what I'm doing also and it's giving consistent FPS. Since I'm using NSOpenGLContext and NSView I used NSView.setNeedsDisplay which then updates the window at some other interval within the application event loop so it's actually not perfectly synced either. Between those 2 things I get 120 fps but maybe if I knew how to test I could see tearing artifacts under some situations. Not sure about that though because it looks ok to me.

icculus commented 2 years ago

Okay, I've reintroduced the CVDisplayLink code, and added a fix for when the window's display refresh rate changes, which was an unfixed problem from the display link code before.

Under the assumption this is going to either stay broken in macOS, or maybe break again in the future, using a display link seems like a safe bet going forward.

Note this only applies to OpenGL contexts! We don't have this wired up to the Metal renderer in the 2D renderer API (but the OpenGL 2D renderer API will use it).

icculus commented 2 years ago

(Note this also doesn't apply to MoltenVK's Vulkan, which is built on top of Metal.)

slime73 commented 2 years ago

@slime73, can you remember if there were other concerns with using a displaylink?

Good question, my memory of the change/revert isn't great but I'll see about doing some testing of the new change / research into DisplayLink soon.

leonstyhre commented 2 years ago

Thanks a lot for adding this workaround! I can confirm that it works fine on my Mac Mini M1, the framerate is now a steady 60 fps on my monitor in both fullscreen and windowed mode.

slouken commented 2 years ago

Testing works well here too.

directmusic commented 2 years ago

I just tested the latest build of SDL within my application and I do appear to be getting 120hz on my "ProMotion" MacBook Pro which is great news!

Granted I am likely doing something wrong in this scenario but if I call SDL_GL_SwapWindow() on multiple windows in one draw loop the framerate is half for two windows, and 1/3rd for three windows and so on. Is there a better way to handle this on my end?

JetSetIlly commented 1 year ago

The changes related to this issue were successful and fixed the VSYNC issue. However, as of MacOS 13.5.2, I'm getting reports that the problem with VSYNC has returned. I don't know if it's the same underlying cause but the symptoms are the same.

nitrologic commented 9 months ago

Hi all,

I realise this issue is closed but I thought I would share the code I am using that keeps my SDL3 MacOS OpenGL 3.2 project frame synced when dragging and full-screening between my MacBook 120Hz promotion screen and external 4K 60hz external monitor.

- (void)movedToNewScreen
{
    if (self->displayLink) {
        SDL_CocoaWindowData *windowData = (__bridge SDL_CocoaWindowData *)self->window->driverdata;
        NSScreen *screen = [[windowData nswindow] screen];
        const CGDirectDisplayID displayID = [[screen.deviceDescription objectForKey:@"NSScreenNumber"] unsignedIntValue];

        int success = CVDisplayLinkSetCurrentCGDisplay(self->displayLink, displayID);
        if( success == kCVReturnSuccess ){
            NSLog(@"movedToNewScreen CVDisplayLinkSetCurrentCGDisplay Success");
        }else{
            NSLog(@"movedToNewScreen CVDisplayLinkSetCurrentCGDisplay Fail");
        }

}