OpenGL rendering spinning out of control when hidden


#1

Hi,

I’m using using Juce’s OpenGLContext and attach it to a Component which I also set to be the OpenGLRenderer. I turned OpenGL component painting off. This all works very well and the application is using around 13% of CPU when rendering 30FPS. However, when I hide the application (cmd+h) or have it running fullscreen (OSX fullscreen) and have another virtual screen visible, the rendering suddenly spins out of control and CPU shoots up to 93%. When I unhide the application or switch back to the virtual fullscreen it’s running on, CPU usage goes back down again.

It feels like the OpenGL rendering is entering a tight loop in these situations. Has anyone experienced this? Any tips to work around it?

Thanks,

Geert


#2

Damn… I bet its the GL swap-buffers call failing to wait for the vertical blank when the window’s not there.


#3

Ah, that sounds plausible. Not that I know much about OpenGL, but I’ve seen the vsync stuff when configuring games to run better :slight_smile:

Do you think it will be a difficult thing to fix?


#4

Tricky… Rather than trying to fix the swapbuffer call it might make more sense to just make sure it stops the rendering thread when hidden. Don’t know, will need to ponder this. Ideas welcome.


#5

Yes, my initial attempts were going into the direction of trying to register callbacks from the AppDelegate and stop the rendering thread. The ones I need don’t seem to be accessible through Juce itself though, so it’ll need some hacking on juce_mac_MessageManager.mm. Do you think that an approach worth taking? If you think so, I’ll go ahead with that direction and send you a patch when it’s working.


#6

Yes… that sounds like a sensible plan, would be keen to see what you come up with!


#7

I couldn’t find a way to get notified when a fullscreen app in not the active desktop anymore. Also, the whole OpenGL abstraction is way out of my league for now and I have no time at the moment to dig deeper to learn. So I ended up adding this horrible hack to juce_OpenGLContext.cpp:

        while (! threadShouldExit())
        {
            uint64 before = Time::currentTimeMillis();
            
            if (! renderFrame())
                wait (5); // failed to render, so avoid a tight fail-loop.
            
            // horrible hack below to prevent tight loop when hidden of fullscreen hidden on MacOSX
            uint64 elapsed = Time::currentTimeMillis()-before;
            if (elapsed < 20) {
                wait(20-elapsed);
            }
        }

        shutdownOnThread();

It essentially prevents more than 50FPS, which is fine for my application and ensures that it never spins in a tight loop.


#8

Any update on this or other ways around the high CPU usage when not visible?


#9

This seems to be a stupid OSX bug, and is a bit of a pain to fix, but I’ve just checked-in a workaround that should do the trick - let me know what you reckon.


#10

Thanks Jules, I tested your fix and indeed prevents this from spinning out of control. However it still increases CPU usage 3 times (37% versus 13%) when hidden. When I put my even uglier fix from above back in place, there’s no CPU usage increase.


#11

Yeah, my version is more conservative because I didn’t want to make it sleep unnecessarily when things are happening that could use the extra oomph, like window resizing, etc. Also, hard-coding the time to 20ms is way too high to run smoothly at higher monitor refresh rates.

This is all a hack anyway - a much better fix would be to find out when the window is occluded by other windows, and when that happens, really slow things down a lot, but AFAICT there’s no way to find out when that happens.


#12

I'm using OpenGLRenderer, with setContinuousRepainting(true), and I see what appears to be this issue on OSX still - what I first noticed, is that if the window has been not visible for more than 5-10 seconds, and then I return to it, the FPS-for-the-last-few-seconds that my app displays would be like 100 or so (when normally it is limited to 60) .... I also then noticed that when this is happening the CPU usage is 100% (rather than whatever CPU usage is required to get 60 FPS normally, which is less).

I guess what I'm seeing doesn't conflict with the partial solution you mentioned 'working', as 3x CPU usage puts me up to 100% easily.

The ideal solution for me would probably be if there would somehow be a way to have rendering simply stop when the window is not visible.

 


#13

When you say "not visible", you mean that it's actually been hidden with setVisible (false), or is it just off-screen or behind something else?


#14

It appears to occur whenever the window is completely covered by other windows (i.e. from other apps). So its not been explicitly hidden.


#15

If you look in sleepIfRenderingTooFast() in juce_OpenGL_osx.h you'll see that there's already some code to deal with this exact situation. It seems to work for me, isn't it getting hit on your machine?


#16

Hi, that code is running, however its far too conservative to have much effect, it basically induces a sleep about 1 in every 6 frames, still leaving the drawing going at closer to 200 FPS if it can manage it ....  one could tweak it a little, but it's never going to be great trying to solve this that way

NSWindow has a property occlusion, i.e. bool windowFullyOccluded = !([window occlusionState] & NSWindowOcclusionStateVisible), which you can use to determine that no part of the window is at all visible, and thus that one should completely stop drawing (perhaps also if minimized, tho occlusionState might be true in that case in any case). I would presume/hope that this value being true is precisely matching when the rendering goes into overdrive. There's also a corresponding notification when occlusion changes you can hook up too .... so that looks like the best solution, I'm just not sure exactly how one would best hook it up within juce, but if you could do it or point to me how I could do it to test it, that'd be great ....

 


#17

Ok, I worked out a way to do a quick solution, it's hacky so you'd want to do something cleaner, but it demonstrates that this is generally a solution that works, and this code is behaving perfectly for me - dropping CPU whenever minimized/occluded, and rendering fine when not.

So, I replaced the contents of sleepIfRenderingTooFast() with:

       for (NSWindow* window in [NSApp windows]) {

            // don't sleep tooo long in one go, in case the window becomes un-occluded and we
            // want to resume drawing instantly ....
            for(int repeats=0; repeats<20; repeats++) {
                bool occluded = !([window occlusionState] & NSWindowOcclusionStateVisible);
                if(occluded) {
                    Thread::sleep(50);
                }
                else {
                    break;
                }
            }

            // we're simply assuming here that the first window is the one we're rendering to!
            break;
        }

 


#18

And I moved that code out of there and into juce_OpenGlContext's runJob, so it can break quick if the thread wants to stop - https://github.com/mbevin/JUCE/commit/f90fb75526b186ab78dc96ae8a4140e3935e8fb4 ... anyway, gives you the idea :)

 


#19

Thanks! But several issues with this..

That occlusion property is only available in 10.9 - which is annoying, but not a showstopper as the problem may not even happen on <10.9 versions where it presumably doesn't take occlusion into account.

But you seem to be suggesting that it waits for longer than minSwapTimeMs when the window is occluded. That'd mess things up for people who've called setSwapInterval to specify how often they want their render call to happen, and whose apps will have problems if the render callback just stops happening for unspecified amounts of time.

The original code attempts to do what I think is the correct behaviour here: i.e. to make sure that it waits for at least as long as the swap interval that they specified (and it shoud spin if setSwapInterval (0) was called). You say it's inaccurate, and yes, it deliberately errs on the side of being conservative to avoid glitches that would happen if it waited too long.

Perhaps a good solution would be to use the occlusion property like this:


    void sleepIfRenderingTooFast()
    {
        // When our window is entirely occluded by other windows, the system
        // fails to correctly implement the swap interval time, so the render
        // loop spins at full speed, burning CPU. This hack detects when things
        // are going too fast and slows things down if necessary.

        if (minSwapTimeMs > 0)
        {
            const double now = Time::getMillisecondCounterHiRes();
            const int elapsed = (int) (now - lastSwapTime);
            lastSwapTime = now;

           #if defined (MAC_OS_X_VERSION_10_9) && (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_9)
            if (NSWindow* window = [view window])
            {
                if ([window respondsToSelector: @selector (occlusionState)])
                {
                    if (([window occlusionState] & NSWindowOcclusionStateVisible) == 0)
                        Thread::sleep (minSwapTimeMs - elapsed);

                    return;
                }
            }
           #endif

            if (isPositiveAndBelow (elapsed, minSwapTimeMs - 3))
            {
                if (underrunCounter > 3)
                    Thread::sleep (minSwapTimeMs - elapsed);
                else
                    ++underrunCounter;
            }
            else
            {
                underrunCounter = 0;
            }
        }
    }


#20

Hi, the above suggestion should be fine if the app really needs to maintain its intended FPS while in the background. At least, by having that occluded special case, the code above should work now to achieve exactly the intended while in background (and on 10.9+).

Additionally, I imagine the far more common preference (insofar as the juce-lib-user cares either way), and the preference that works best for the good of the OS's CPU+battery-usage (nothing worse that apps and/or webpages that in the background but still sucking CPU), thus hopefully something that is more the 'default', is that the app should throttle the painting back to something super low (like the 1 or 2 fps I did), or perhaps even just nothing, when included ....  so I'd recommend having that as a configurable option at least.