Search code examples
androidgame-engineonresumeglsurfaceviewonpause

Android Game - how correctly handle onPause and onResume if game runs in GLSurfaceView's video thread


I work on c++ game which runs in GLSurfaceView's video thread (game's loop is loop of GLSurfaceView, because it runs in continuos mode). I have problems how to correctly handle Activity.onPause/onResume together with nativePause of my game. In nativePause i release opengl resources and various big data. I don't have nativeResume, because this is handled by Android when I call GLSurfaceView.onResume() which calls again methods onSurfaceCreated/onSurfaceChanged , in which I allocate again my resources.

Here is how I do it now :

OnPause

Activity in java handles onPause and runs custom nativePause method of glSurfaceView:

@Override
protected void onPause() {
    super.onPause();

    glSurfaceView.nativePause();
}

nativePause sends asynchronous request to game's video loop. In video loop is handled and various resources are released. Next, is send another message to main thread with information that nativePause is finished and I do GLSurfaceView.onPause(), this stops video thread.

onResume

this method has simple implementation, it only starts surfaceview's video thread with onResume()

@Override
protected void onResume() {
    super.onResume();

    glSurfaceView.onResume();
}

But problem is, onPause does asynchronous calls to video thread and back to main thread. Activity.onResume is often called sooner before whole pausing mechanism is finished and then it crashes or hangs. How should I handle onPause/onResume correctly if game runs in video thread?

EDIT :

Java side :

public class RendererWrapper implements Renderer {
    public native void onSurfaceCreated();
    public native void onSurfaceChanged(int width, int height);
    public native void onDrawFrame();
....
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        onSurfaceCreated();
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        onSurfaceChanged(width, height);
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        onDrawFrame();
    }
....
}

public class VideoSurface extends GLSurfaceView {
    public VideoSurface(Context context) {
        super(context);

        this.setEGLContextClientVersion(2);
        this.renderer = new RendererWrapper();
        this.setEGLConfigChooser(8, 8, 8, 8, 16, 0);
        this.setRenderer(renderer);
        this.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
    }

    public native void nativePause();
}

Native onDrawFrame() in RendererWrapper is main game loop.

C++ side

void nativePause() {
    InputEvent *event = inputQueue.getWriteEvent();
    event->type = InputEvent::PAUSE;

    inputQueue.incWriteIndex();
}

void onDrawFrame() {
    if (isPaused) {
        return;
    }

    InputEvent *event = inputQueue.getReadEvent();
    if (event) {
        inputQueue.incReadIndex();

        ....
        if (event->type == InputEvent::PAUSE) {
            release();
            return;
        }
    }

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glClearColor(1.0f, 0.0f, 0.0f, 0.0f);

    game->draw();
}

EDIT2 :

class EventQueue {
public:
    static const int size = 256;
    volatile int readIndex;
    volatile int writeIndex;
    InputEvent *events;

    EventQueue() {
        readIndex = 0;
        writeIndex = 0;
        events = new InputEvent[size];
    }

    InputEvent* getReadEvent() {
        if (writeIndex == readIndex) {
            return 0; // queue empty
        }
        return events + readIndex;
    }

    InputEvent* getWriteEvent() {
        if (((writeIndex + 2) & (size - 1)) == readIndex) {
            return 0; // queue full
        }
        return events + writeIndex;
    }

    void incReadIndex() {
        readIndex = (readIndex + 1) & (size - 1);
    }

    void incWriteIndex() {
        writeIndex = (writeIndex + 1) & (size - 1);
    }
};

Solution

  • Careful with that volatile trick. In many cases, it doesn't do what you think it does. If it's worked so far, well, that's probably due to luck.

    Since the InputQueue class isn't really suitable for this, I'll just show you how to solve the problem with a condition variable (code not tested):

    #include <pthread.h>
    
    pthread_cond_t cond;
    pthread_mutex_t mutex;
    bool released = false;
    
    ...
    
    pthread_cond_init(&cond, NULL);    //TODO: check return value
    pthread_mutex_init(&mutex, NULL);    //TODO: check return value
    
    ...
    
    void nativePause() {
        InputEvent *event = inputQueue.getWriteEvent();
        event->type = InputEvent::PAUSE;
    
        inputQueue.incWriteIndex();
    
        //Wait for the OpenGL thread to accomplish the release():
        pthread_mutex_lock(&mutex);
        while(!released) {
            pthread_cond_wait(&cond, &mutex);   //Expected to always return 0.
        }
        pthread_mutex_unlock(&mutex);
    }
    
    void onDrawFrame() {
    
        ...
    
        if (event) {
            inputQueue.incReadIndex();
    
            ....
            if (event->type == InputEvent::PAUSE) {
                release();
    
                pthread_mutex_lock(&mutex);
                released = true;
                pthread_cond_broadcast(&cond);    //Notifies the nativePause() thread, which is supposed to be blocking in the condition loop, at this point.
                pthread_mutex_unlock(&mutex);
    
                return;
            }
        }
    
        ...    
    }
    
    ...
    
    void nativeCleanup()
    {
        pthread_cond_destroy(&cond);    //Expected to return 0.
        pthread_mutex_destroy(&mutex);    //Expected to return 0.
    }
    

    At least, that should work. Code assumes the OpenGL thread is guaranteed to exist until after onPause() returns. I guess that's true; I really don't remember.