iOS crash on suspend

Bit stumped on this problem, hoping someone out there has seen something similar or can suggest where to look.

When my iOS app tries to go into the background (when home button pressed) I get a crash. From the call stack, the app crashes while trying to swap buffers, so presumably it's still trying to draw normally and iOS isn't letting it. 

I've been studying the Juce demo which seems to handle going into the background just fine, but I can't find where I might have gone wrong here. I'm doing basically what the demo is doing (openGL auto redraw is on, I'm not manually trying to call to render, etc). Any pointers here would be appreciated!

thanks, 

Nick

oh, and this problem looks like it's only just reproducing on an actual device. In the simulator I have no problem going to the home screen without the app crashing, and it seems to resume where it left off. 

Frustrating. Seems i'm finding lots of weird little problems that don't happen in the simulator but happen on a device. Drawing performance on the latest Juce demo looks fantastic in the simulator but grinds to 5 fps on a retina iPad. Hi Res timer flat out does not work. Jules we really need to get you something to test on! I will happily trade you a 2nd hand iPad for a commercial license :).

 

 

sorry for the self-conversation, just wanted to update with a clue:

I added the suspend() method to my main application so that I get a callback when iOS tries to suspend the app and did a quick test. 

My app successfully goes into the background in the simulator, however the renderOpenGL callback still gets hit 4 more times after the app is suspended. 

There is an additional method that should probably be implemented in the Juce app delegate:

- (void)applicationWillResignActive:(UIApplication *)application

{

    // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.

    // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.

}

 

I can add that callback, but I'm unclear about what it could do.. Are you saying you think the problem is because the openGL thread doesn't slow down when the app becomes inactive?

What I'm saying is it Apple says you need to use this other callback to"throttle down" (presumably stop rendering) openGL ES before the app goes into the background.

What it seems like from here is that Juce tries to keep rendering for a few frames after it's no longer allowed to, which in certain cases (iPad Retina, but apparently not in simulator) causes a crash. 

make sense?

 

Ok.. Well, I've just added that callback so that now Process::isForegroundProcess() should work correctly on iOS, and that could be checked in the GL call to see whether it's active or not.

I'm really not sure what the GL code should do if it's not active though.. The GL thread just spins in a loop, should it just do a short sleep() instead of rendering while it's not active? If you want to experiment, I'd be interested to hear what happens.

Ok, great, I will take a look and see what I can find. Maybe it's enough to just check whether i'm the foreground process. 

unfortunately checking if I'm the foreground process is not enough. the context is still trying to swap buffers automatically which is causing the crash. So maybe the thread should sleep? 

Honestly I'm a bit out of my depth rummaging around the threading code. But perhaps you tell the thread to wait() with an indefinite timeout when the app is being suspended and then notify() when coming back? What do you think?

 

I really don't know - this kind of thing generally involves experimenting to see what happens..

ok. well, if I do this in my render callback, the app will go into the background without crashing. So i'd imagine you need to do the equivalent behind the scenes (telling the thread to wait indefinitely, right?). Then do the opposite when coming back. 

    if (!Process::isForegroundProcess())  //if in the background stop rendering!

    {

        openGLContext.setContinuousRepainting(false);

    }

Ok... Can you give this a try, in juce_OpenGLContext.cpp, line 335:

        while (! threadShouldExit())
        {
            if (! renderFrame())
                wait (5); // failed to render, so avoid a tight fail-loop.
           #if JUCE_IOS
            else if (! (context.continuousRepaint && Process::isForegroundProcess()))
           #else
            else if (! context.continuousRepaint)
           #endif
                wait (-1);
        }

works just fine for me!

well, looks like I spoke too soon! There's another related case that will cause a crash, and a case where rendering will not restart properly:

1.

If openGL renderer is being used, any components trying to redraw will also crash the app while it's in the background. The case I'm seeing this in is with the MidiKeyboardComponent. Basically if you try to play an external keyboard while the app is in the background, it will crash on noteOn. I can also reproduce this crash with the Juce Demo. If i set it to use core graphics renderer, the crash does not occur. 

Not sure I fully understand why components need to manually redraw in the first place if the openGL context is auto-redrawing at a high frame rate.  This is also causing a lot of choppiness in animations when the controls are adjusted. It is not really noticable on the desktop, but on the iPad it is. 

2.

If you reveal the iOS menus from the bottom or top of the screen, rendering will pause (expected behavior). But when you hide the menu, rendering does not resume. 

Have you got a stack trace for the crash?

for some reason, even in a debug build, I cannot get a crash report from the device with readable Juce functions, so it's hard to give you anything useful for the external midi device crash. however i did get this with Xcode attached while using the opengl renderer:

Thread 1 Juce Message Thread, Queue : com.apple.main-thread
#0 0x3a3d3f64 in __psynch_mutexdrop ()
#1 0x3a439e08 in pthread_mutex_unlock ()
#2 0x00229548 in juce::WaitableEvent::signal() const at /Users/nhdika/Code/JUCE/modules/juce_core/native/juce_posix_SharedCode.h:124
#3 0x002bfc92 in juce::MessageManagerLock::BlockingMessage::messageCallback() at /Users/nhdika/Code/JUCE/modules/juce_events/messages/juce_MessageManager.cpp:231
#4 0x002be2c6 in juce::MessageQueue::deliverNextMessage() at /Users/nhdika/Code/JUCE/modules/juce_events/native/juce_osx_MessageQueue.h:79
#5 0x002be206 in juce::MessageQueue::runLoopCallback() at /Users/nhdika/Code/JUCE/modules/juce_events/native/juce_osx_MessageQueue.h:90
#6 0x002be1da in juce::MessageQueue::runLoopSourceCallback(void*) at /Users/nhdika/Code/JUCE/modules/juce_events/native/juce_osx_MessageQueue.h:99
#7 0x2f30ffe6 in __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ ()
#8 0x2f30f4ae in __CFRunLoopDoSources0 ()
#9 0x2f30dc9e in __CFRunLoopRun ()
#10 0x2f2787a8 in CFRunLoopRunSpecific ()
#11 0x2f27858a in CFRunLoopRunInMode ()
#12 0x341b26d2 in GSEventRunModal ()
#13 0x31bd7890 in UIApplicationMain ()
#14 0x003e9392 in juce::juce_iOSMain(int, char const**) at /Users/nhdika/Code/JUCE/modules/juce_gui_basics/native/juce_ios_Windowing.mm:95
#15 0x002b82e8 in juce::JUCEApplicationBase::main(int, char const**) at /Users/nhdika/Code/JUCE/modules/juce_events/messages/juce_ApplicationBase.cpp:211
#16 0x00063a16 in main at /Users/nhdika/Code/JUCE/extras/Demo/Source/Main.cpp:91
#17 0x3a31dab6 in start ()
Thread 8 Juce Timer, Queue : (null)
#0 0x3a3d3f2c in __psynch_cvwait ()
#1 0x3a43af66 in _pthread_cond_wait ()
#2 0x3a43bde0 in pthread_cond_timedwait ()
#3 0x00229232 in juce::WaitableEvent::wait(int) const at /Users/nhdika/Code/JUCE/modules/juce_core/native/juce_posix_SharedCode.h:97
#4 0x0022a8ec in juce::Thread::wait(int) const at /Users/nhdika/Code/JUCE/modules/juce_core/threads/juce_Thread.cpp:246
#5 0x002bf1c4 in juce::Timer::TimerThread::run() at /Users/nhdika/Code/JUCE/modules/juce_events/timers/juce_Timer.cpp:89
#6 0x00229fa0 in juce::Thread::threadEntryPoint() at /Users/nhdika/Code/JUCE/modules/juce_core/threads/juce_Thread.cpp:101
#7 0x0022a2c2 in juce::juce_threadEntryPoint(void*) at /Users/nhdika/Code/JUCE/modules/juce_core/threads/juce_Thread.cpp:113
#8 0x0023cbba in threadEntryProc at /Users/nhdika/Code/JUCE/modules/juce_core/native/juce_posix_SharedCode.h:846
#9 0x3a43b958 in _pthread_body ()
#10 0x3a43b8ca in _pthread_start ()
Thread 9 OpenGL Rendering, Queue : (null)
#0 0x33e01932 in gpus_ReturnNotPermittedKillClient ()
#1 0x33e023d0 in gpusSubmitDataBuffers ()
#2 0x2e0bb29a in <redacted> ()
#3 0x315eca6c in gliPresentViewES_Exec ()
#4 0x315ec97e in gliPresentViewES ()
#5 0x315f70ca in -[EAGLContext presentRenderbuffer:] ()
#6 0x010d72d6 in EAGLContext_presentRenderbuffer(EAGLContext*, objc_selector*, unsigned int) ()
#7 0x004afc36 in juce::OpenGLContext::NativeContext::swapBuffers() at /Users/nhdika/Code/JUCE/modules/juce_opengl/native/juce_OpenGL_ios.h:146
#8 0x004ae000 in juce::OpenGLContext::swapBuffers() at /Users/nhdika/Code/JUCE/modules/juce_opengl/opengl/juce_OpenGLContext.cpp:716
#9 0x004b59f8 in juce::OpenGLContext::CachedImage::renderFrame() at /Users/nhdika/Code/JUCE/modules/juce_opengl/opengl/juce_OpenGLContext.cpp:194
#10 0x004b55de in juce::OpenGLContext::CachedImage::run() at /Users/nhdika/Code/JUCE/modules/juce_opengl/opengl/juce_OpenGLContext.cpp:337
#11 0x004b5688 in non-virtual thunk to juce::OpenGLContext::CachedImage::run() at /Users/nhdika/Code/JUCE/modules/juce_opengl/juce_opengl.mm:348
#12 0x00229fa0 in juce::Thread::threadEntryPoint() at /Users/nhdika/Code/JUCE/modules/juce_core/threads/juce_Thread.cpp:101
#13 0x0022a2c2 in juce::juce_threadEntryPoint(void*) at /Users/nhdika/Code/JUCE/modules/juce_core/threads/juce_Thread.cpp:113
#14 0x0023cbba in threadEntryProc at /Users/nhdika/Code/JUCE/modules/juce_core/native/juce_posix_SharedCode.h:846
#15 0x3a43b958 in _pthread_body ()
#16 0x3a43b8ca in _pthread_start ()
Thread 11, Queue : (null)
#0 0x00072504 in LiveScrollingAudioDisplay::audioDeviceIOCallback(float const**, int, float**, int, int) at /Users/nhdika/Code/JUCE/extras/Demo/Source/Demos/AudioLiveScrollingDisplay.h:71
#1 0x000725fc in non-virtual thunk to LiveScrollingAudioDisplay::audioDeviceIOCallback(float const**, int, float**, int, int) at /Users/nhdika/Code/JUCE/extras/Demo/Source/Demos/AudioLatencyDemo.cpp:80
#2 0x0015b56a in juce::AudioDeviceManager::audioDeviceIOCallbackInt(float const**, int, float**, int, int) at /Users/nhdika/Code/JUCE/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp:727
#3 0x0016fb88 in juce::AudioDeviceManager::CallbackHandler::audioDeviceIOCallback(float const**, int, float**, int, int) at /Users/nhdika/Code/JUCE/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp:56
#4 0x0016aa1c in juce::iOSAudioIODevice::process(unsigned long*, AudioTimeStamp const*, unsigned long, AudioBufferList*) at /Users/nhdika/Code/JUCE/modules/juce_audio_devices/native/juce_ios_Audio.cpp:283
#5 0x0016a7c6 in juce::iOSAudioIODevice::processStatic(void*, unsigned long*, AudioTimeStamp const*, unsigned long, unsigned long, AudioBufferList*) at /Users/nhdika/Code/JUCE/modules/juce_audio_devices/native/juce_ios_Audio.cpp:458
#6 0x2ed0443c in AUInputElement::PullInput(unsigned long&, AudioTimeStamp const&, unsigned long, unsigned long) ()
#7 0x2ed00ff0 in AUInputFormatConverter2::InputProc(OpaqueAudioConverter*, unsigned long*, AudioBufferList*, AudioStreamPacketDescription**, void*) ()
#8 0x2ec32e74 in AudioConverterChain::CallInputProc(unsigned long) ()
#9 0x2ec32be8 in AudioConverterChain::FillBufferFromInputProc(unsigned long*, CABufferList*) ()
#10 0x2ec32b7e in BufferedAudioConverter::GetInputBytes(unsigned long, unsigned long&, CABufferList const*&) ()
#11 0x2ec32a8a in CBRConverter::RenderOutput(CABufferList*, unsigned long, unsigned long&, AudioStreamPacketDescription*) ()
#12 0x2ec3292c in BufferedAudioConverter::FillBuffer(unsigned long&, AudioBufferList&, AudioStreamPacketDescription*) ()
#13 0x2ec32a00 in AudioConverterChain::RenderOutput(CABufferList*, unsigned long, unsigned long&, AudioStreamPacketDescription*) ()
#14 0x2ec3292c in BufferedAudioConverter::FillBuffer(unsigned long&, AudioBufferList&, AudioStreamPacketDescription*) ()
#15 0x2ec41240 in AudioConverterFillComplexBuffer ()
#16 0x2ed00ed0 in AUInputFormatConverter2::PullAndConvertInput(AudioTimeStamp const&, unsigned long&, AudioBufferList&, AudioStreamPacketDescription*, bool&) ()
#17 0x2ed00a78 in AUConverterBase::RenderBus(unsigned long&, AudioTimeStamp const&, unsigned long, unsigned long) ()
#18 0x2ec348f6 in AUBase::DoRenderBus(unsigned long&, AudioTimeStamp const&, unsigned long, AUOutputElement*, unsigned long, AudioBufferList&) ()
#19 0x2ec34748 in AUBase::DoRender(unsigned long&, AudioTimeStamp const&, unsigned long, unsigned long, AudioBufferList&) ()
#20 0x2ecf0970 in AURemoteIO::PerformIO(unsigned long, unsigned int, unsigned int, AudioTimeStamp const&, AudioTimeStamp const&, AudioBufferList const*, AudioBufferList*, int&) ()
#21 0x2ecf19bc in AURIOCallbackReceiver_PerformIO ()
#22 0x2ece80b6 in _XPerformIO ()
#23 0x2ec5564c in mshMIGPerform ()
#24 0x2ecc4b2c in MSHMIGDispatchMessage ()
#25 0x2ecf0be8 in AURemoteIO::IOThread::Run() ()
#26 0x2ecf3cc8 in AURemoteIO::IOThread::Entry(void*) ()
#27 0x2ec30b3a in CAPThread::Entry(CAPThread*) ()
#28 0x3a43b958 in _pthread_body ()
#29 0x3a43b8ca in _pthread_start ()
 

Well, looks like it's the swapBuffers call that's crashing if called when the app is in the background. (Seems a bit harsh to crash when that happens rather than just failing!)

Presumably it could be worked around like this:

    void swapBuffers()
    {
        if (Process::isForegroundProcess())
        {
            glBindRenderbuffer (GL_RENDERBUFFER, colorBufferHandle);
            [context presentRenderbuffer: GL_RENDERBUFFER];

            if (needToRebuildBuffers)
            {
                needToRebuildBuffers = false;
                freeGLBuffers();
                createGLBuffers();
                makeActive();
            }
        }
    }

adding that results in the following:

 


Thread 1 Juce Message Thread, Queue : com.apple.main-thread
#0    0x3a3d3f64 in __psynch_mutexdrop ()
#1    0x3a439e08 in pthread_mutex_unlock ()
#2    0x00209518 in juce::WaitableEvent::signal() const at /Users/nhdika/Code/JUCE/modules/juce_core/native/juce_posix_SharedCode.h:124
#3    0x0029fbf2 in juce::MessageManagerLock::BlockingMessage::messageCallback() at /Users/nhdika/Code/JUCE/modules/juce_events/messages/juce_MessageManager.cpp:231
#4    0x0029e226 in juce::MessageQueue::deliverNextMessage() at /Users/nhdika/Code/JUCE/modules/juce_events/native/juce_osx_MessageQueue.h:79
#5    0x0029e166 in juce::MessageQueue::runLoopCallback() at /Users/nhdika/Code/JUCE/modules/juce_events/native/juce_osx_MessageQueue.h:90
#6    0x0029e13a in juce::MessageQueue::runLoopSourceCallback(void*) at /Users/nhdika/Code/JUCE/modules/juce_events/native/juce_osx_MessageQueue.h:99
#7    0x2f30ffe6 in __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ ()
#8    0x2f30f4ae in __CFRunLoopDoSources0 ()
#9    0x2f30dc9e in __CFRunLoopRun ()
#10    0x2f2787a8 in CFRunLoopRunSpecific ()
#11    0x2f27858a in CFRunLoopRunInMode ()
#12    0x341b26d2 in GSEventRunModal ()
#13    0x31bd7890 in UIApplicationMain ()
#14    0x003c9302 in juce::juce_iOSMain(int, char const**) at /Users/nhdika/Code/JUCE/modules/juce_gui_basics/native/juce_ios_Windowing.mm:95
#15    0x00298248 in juce::JUCEApplicationBase::main(int, char const**) at /Users/nhdika/Code/JUCE/modules/juce_events/messages/juce_ApplicationBase.cpp:211
#16    0x000439fe in main at /Users/nhdika/Code/JUCE/extras/Demo/Source/Main.cpp:91
Thread 7 Juce Timer, Queue : (null)
#0    0x3a3d3f2c in __psynch_cvwait ()
#1    0x3a43af66 in _pthread_cond_wait ()
#2    0x3a43bde0 in pthread_cond_timedwait ()
#3    0x00209202 in juce::WaitableEvent::wait(int) const at /Users/nhdika/Code/JUCE/modules/juce_core/native/juce_posix_SharedCode.h:97
#4    0x0020a8bc in juce::Thread::wait(int) const at /Users/nhdika/Code/JUCE/modules/juce_core/threads/juce_Thread.cpp:246
#5    0x0029f186 in juce::Timer::TimerThread::run() at /Users/nhdika/Code/JUCE/modules/juce_events/timers/juce_Timer.cpp:106
#6    0x00209f70 in juce::Thread::threadEntryPoint() at /Users/nhdika/Code/JUCE/modules/juce_core/threads/juce_Thread.cpp:101
#7    0x0020a292 in juce::juce_threadEntryPoint(void*) at /Users/nhdika/Code/JUCE/modules/juce_core/threads/juce_Thread.cpp:113
#8    0x0021cb8a in threadEntryProc at /Users/nhdika/Code/JUCE/modules/juce_core/native/juce_posix_SharedCode.h:846
#9    0x3a43b958 in _pthread_body ()
#10    0x3a43b8ca in _pthread_start ()
Thread 8 OpenGL Rendering, Queue : (null)
#0    0x33e01932 in gpus_ReturnNotPermittedKillClient ()
#1    0x33e023d0 in gpusSubmitDataBuffers ()
#2    0x2e0d65ac in <redacted> ()
#3    0x2e0d6834 in <redacted> ()
#4    0x3156ecde in glDrawArrays_ACC_ES2Exec ()
#5    0x010457fa in draw_arrays(__GLIContextRec*, unsigned int, int, int) ()
#6    0x0048ab5e in juce::OpenGLContext::copyTexture(juce::Rectangle<int> const&, juce::Rectangle<int> const&, int, int, bool) at /Users/nhdika/Code/JUCE/modules/juce_opengl/opengl/juce_OpenGLContext.cpp:913
#7    0x0049603a in juce::OpenGLContext::CachedImage::drawComponentBuffer() at /Users/nhdika/Code/JUCE/modules/juce_opengl/opengl/juce_OpenGLContext.cpp:266
#8    0x004959e6 in juce::OpenGLContext::CachedImage::renderFrame() at /Users/nhdika/Code/JUCE/modules/juce_opengl/opengl/juce_OpenGLContext.cpp:191
#9    0x004955de in juce::OpenGLContext::CachedImage::run() at /Users/nhdika/Code/JUCE/modules/juce_opengl/opengl/juce_OpenGLContext.cpp:337
#10    0x00495688 in non-virtual thunk to juce::OpenGLContext::CachedImage::run() at /Users/nhdika/Code/JUCE/modules/juce_opengl/juce_opengl.mm:348
#11    0x00209f70 in juce::Thread::threadEntryPoint() at /Users/nhdika/Code/JUCE/modules/juce_core/threads/juce_Thread.cpp:101
#12    0x0020a292 in juce::juce_threadEntryPoint(void*) at /Users/nhdika/Code/JUCE/modules/juce_core/threads/juce_Thread.cpp:113
#13    0x0021cb8a in threadEntryProc at /Users/nhdika/Code/JUCE/modules/juce_core/native/juce_posix_SharedCode.h:846
#14    0x3a43b958 in _pthread_body ()
#15    0x3a43b8ca in _pthread_start ()

Oh FFS.. so all GL calls look like they're failing when it's in the background. Ok, forget that last one, how about just disabling the entire GL rendering loop like this:

    void run() override
    {
        {
            // Allow the message thread to finish setting-up the context before using it..
            MessageManagerLock mml (this);
            if (! mml.lockWasGained())
                return;
        }

        initialiseOnThread();

        hasInitialised = true;

        while (! threadShouldExit())
        {
           #if JUCE_IOS
            if (! Process::isForegroundProcess())
            {
                wait (500);
                continue;
            }
           #endif

            if (! renderFrame())
                wait (5); // failed to render, so avoid a tight fail-loop.
            else if (! context.continuousRepaint)
                wait (-1);
        }

        shutdownOnThread();
    }

Whew! That looks like it's working. And the crash when using the midi controller also goes away. 

From my experience it seems prudent to add the check in the swapBuffer call as well. My application is getting through the initial check in OpenGLContext::CachedImage::run() but (presumably due to the time spent in the render call) still manages to crash at the presentRenderbuffer call.  Seems to be a whole lot more resilient with both checks in place.