0

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);
    }
};
user1063364
  • 791
  • 6
  • 21
  • If it were me, I'd make `nativePause()` block until it completed. I believe calling `GLSurfaceView.onPause()` after `Activity.onPause()` has already returned is illegal, and you're probably finding out why. It should be fairly easy to make `nativePause()` synchronous; you can still count on your OpenGL thread being present; I assume you do not rely on any intervening calls to `onDrawFrame()` in order to clean up; you ought to just be able to block until your OpenGL thread notifies a condition variable, or something, that the `nativePause()` actions have been accomplished. – greeble31 Nov 14 '19 at 15:59
  • Thank you, do you mean with simple Thread.sleep()? – user1063364 Nov 15 '19 at 07:23
  • No, I mean using an established thread communication technique. Either 1.) pass a message from thread A to thread B, have thread B do the cleanup, then pass a message to thread A (which then unblocks and returns from `nativePause()`), or 2.) Have thread A grab a lock, do the cleanup, release the lock, and return from `nativePause()`. `pthread_mutex_lock` or `pthread_cond_wait/pthread_cond_signal` are probably what you want to use, here. If you need help, best to add all relevant code for `nativePause()` to your question. – greeble31 Nov 15 '19 at 13:28
  • Thank you, I added code how nativePause works now. It creates input event and puts it to input queue. This queue is read in draw thread's loop – user1063364 Nov 15 '19 at 19:47
  • It looks like you're already using inter-thread communication, so why don't we just stick with what you're already comfortable with. I can't tell exactly what type of object `inputQueue` is. Do you have some documentation? Does it have a _blocking_ version of the getReadEvent() method? Also, I assume the whole point is getting the OpenGL thread to call `release()` (whatever that is), otherwise things will break? – greeble31 Nov 15 '19 at 20:16
  • Exactly, release() is what must be executed before is finished onPause() on java side. inputQueue is my class, I updated question with definition, I don't use mutex for locking, because in my case is exactly one thread writer and second thread reader, no necessary to have mutex – user1063364 Nov 16 '19 at 08:21

1 Answers1

1

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.

greeble31
  • 4,894
  • 2
  • 16
  • 30
  • Wow! I just tested it in emulator and it works perfectly! I will test it today on real device and let you know, but seems this is exactly what I need :) – user1063364 Nov 17 '19 at 07:45
  • So, as far as I can tell - it works. Really many thanks! :) Right now I have well working pause/resume mechanism. I can release big data, save settings and release database connection. My last question is about whole application exit. I call Activity.finish on button press. This calls Activity.onPause in which (in this case) I run nativeDestroy (destroys allocated objects in video thread). However android can send Activity.onDestroy event if is my application long time paused. I think I should run nativeDestroy in this event too, but I don't know how, because video thread is already paused... – user1063364 Nov 17 '19 at 12:06
  • @user1063364 You're welcome, don't forget to accept this answer! You're right that `onDestroy()` may get sent eventually, but it will always be after an `onPause()`, and an `onCreate()/onResume()/onSurfaceCreated()` cycle will follow it if the `Activity` is to be brought back. So, what I'm saying is, I think you need to call `nativeDestroy()` in `onPause()`, too, _if_ it requires the OpenGL thread. (I haven't seen the docs for `nativeDestroy()`, so I don't know.) And then you need to be able to reconstruct everything in `onResume()/onSurfaceCreated()`. – greeble31 Nov 17 '19 at 14:21
  • Problem is, opengl context may be not lost after onResume and onSurfaceCreated() may be not called. But onDraw will be called, probably I should recreate all here. – user1063364 Nov 17 '19 at 14:58
  • @user1063364 I think you may be right. Maybe you could just track it with a variable; if `onSurfaceCreated()` is called a second time, you can infer that the context was lost, so do a quick `nativeDestroy()`, then a `nativeCreate()` (or whatever) to get yourself back on track. Just an idea. – greeble31 Nov 17 '19 at 15:08
  • Yes, many thanks for your help, you really helped me, still do not understand why is this not in android docs. – user1063364 Nov 17 '19 at 18:07