Игра для Android — как правильно обрабатывать onPause и onResume, если игра запускается в видеопотоке GLSurfaceView

Я работаю над игрой на С++, которая запускается в видеопотоке GLSurfaceView (цикл игры - это цикл GLSurfaceView, потому что он работает в непрерывном режиме). У меня проблемы с корректной обработкой Activity.onPause/onResume вместе с nativePause моей игры. В nativePause я выпускаю ресурсы opengl и разные большие данные. У меня нет nativeResume, потому что это обрабатывается Android, когда я вызываю GLSurfaceView.onResume(), который снова вызывает методы onSurfaceCreated/onSurfaceChanged, в которых я снова выделяю свои ресурсы.

Вот как я это делаю сейчас:

при паузе

Активность в java обрабатывает onPause и запускает пользовательский метод nativePause glSurfaceView:

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

    glSurfaceView.nativePause();
}

nativePause отправляет асинхронный запрос видеоциклу игры. В цикле видео обрабатывается и высвобождаются различные ресурсы. Затем я отправляю еще одно сообщение в основной поток с информацией о том, что nativePause завершен, и я делаю GLSurfaceView.onPause(), это останавливает видеопоток.

onResume

этот метод имеет простую реализацию, он только запускает видеопоток SurfaceView с помощью onResume()

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

    glSurfaceView.onResume();
}

Но проблема в том, что onPause выполняет асинхронные вызовы видеопотока и обратно в основной поток. Activity.onResume часто вызывается раньше, чем весь механизм приостановки завершен, а затем происходит сбой или зависание. Как правильно обрабатывать onPause/onResume, если игра запускается в видеопотоке?

РЕДАКТИРОВАТЬ :

Сторона Java:

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();
}

Нативная функция onDrawFrame() в RendererWrapper является основным игровым циклом.

Сторона С++

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();
}

РЕДАКТИРОВАТЬ2:

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);
    }
};

person user1063364    schedule 14.11.2019    source источник
comment
Если бы это был я, я бы сделал nativePause() блок, пока он не завершится. Я считаю, что вызов GLSurfaceView.onPause() после того, как Activity.onPause() уже вернулся, незаконен, и вы, вероятно, понимаете, почему. Должно быть довольно легко сделать nativePause() синхронным; вы по-прежнему можете рассчитывать на наличие вашего потока OpenGL; Я предполагаю, что вы не полагаетесь на какие-либо промежуточные вызовы onDrawFrame() для очистки; вы должны просто иметь возможность блокироваться до тех пор, пока ваш поток OpenGL не уведомит переменную условия или что-то еще о том, что действия nativePause() были выполнены.   -  person greeble31    schedule 14.11.2019
comment
Спасибо, вы имеете в виду простой Thread.sleep()?   -  person user1063364    schedule 15.11.2019
comment
Нет, я имею в виду использование устоявшейся техники связи между потоками. Либо 1.) передать сообщение из потока A в поток B, дать потоку B выполнить очистку, затем передать сообщение потоку A (который затем разблокируется и вернуться из nativePause()), либо 2.) Попросить поток A захватить блокировку, сделать очистку, снимите блокировку и вернитесь из nativePause(). pthread_mutex_lock или pthread_cond_wait/pthread_cond_signal, вероятно, то, что вы хотите использовать здесь. Если вам нужна помощь, лучше всего добавить весь соответствующий код для nativePause() к вашему вопросу.   -  person greeble31    schedule 15.11.2019
comment
Спасибо, я добавил код, как теперь работает nativePause. Он создает событие ввода и помещает его в очередь ввода. Эта очередь читается в цикле потока рисования   -  person user1063364    schedule 15.11.2019
comment
Похоже, вы уже используете межпотоковое взаимодействие, так почему бы нам просто не придерживаться того, что вам уже удобно. Я не могу точно сказать, что это за объект inputQueue. У вас есть документация? Есть ли в нем блокирующая версия метода getReadEvent()? Кроме того, я предполагаю, что весь смысл в том, чтобы поток OpenGL вызывал release() (что бы это ни было), иначе все сломается?   -  person greeble31    schedule 15.11.2019
comment
Точно, release() - это то, что должно быть выполнено до завершения onPause() на стороне java. inputQueue - это мой класс, я обновил вопрос с определением, я не использую мьютекс для блокировки, потому что в моем случае это ровно один писатель потока и второй читатель потока, нет необходимости иметь мьютекс   -  person user1063364    schedule 16.11.2019


Ответы (1)


Осторожнее с этим изменчивым трюком. Во многих случаях он делает не то, что вы думаете. Если это сработало до сих пор, ну, это, вероятно, из-за удачи.

Поскольку класс InputQueue для этого не очень подходит, я просто покажу, как решить задачу с условной переменной (код не тестировался):

#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.
}

По крайней мере, это должно сработать. Код предполагает, что поток OpenGL гарантированно существует до тех пор, пока не вернется onPause(). Я думаю, это правда; Я действительно не помню.

person greeble31    schedule 16.11.2019
comment
Вау! Я только что проверил его в эмуляторе, и он отлично работает! Я протестирую это сегодня на реальном устройстве и дам вам знать, но, похоже, это именно то, что мне нужно :) - person user1063364; 17.11.2019
comment
Так что, насколько я могу судить - это работает. Действительно большое спасибо! :) Прямо сейчас у меня есть хорошо работающий механизм паузы/возобновления. Я могу публиковать большие данные, сохранять настройки и освобождать соединение с базой данных. Мой последний вопрос касается выхода всего приложения. Я вызываю Activity.finish при нажатии кнопки. Это вызывает Activity.onPause, в котором (в данном случае) я запускаю nativeDestroy (уничтожает выделенные объекты в видеопотоке). Однако Android может отправить событие Activity.onDestroy, если мое приложение долгое время приостановлено. Я думаю, что я должен запустить nativeDestroy и в этом событии, но я не знаю, как это сделать, потому что видеопоток уже приостановлен... - person user1063364; 17.11.2019
comment
@ user1063364 Пожалуйста, не забудьте принять этот ответ! Вы правы, что onDestroy() может быть отправлено в конце концов, но это всегда будет после onPause(), и за ним последует цикл onCreate()/onResume()/onSurfaceCreated(), если Activity нужно вернуть. Итак, я хочу сказать, что вам нужно вызывать nativeDestroy() и в onPause(), если для этого требуется поток OpenGL. (Я не видел документы для nativeDestroy(), поэтому не знаю.) И тогда вам нужно иметь возможность реконструировать все в onResume()/onSurfaceCreated(). - person greeble31; 17.11.2019
comment
Проблема в том, что контекст opengl не может быть потерян после того, как onResume и onSurfaceCreated() могут не вызываться. Но будет вызываться onDraw, видимо мне придется тут все пересоздавать. - person user1063364; 17.11.2019
comment
@user1063364 user1063364 Думаю, ты прав. Может быть, вы могли бы просто отслеживать это с помощью переменной; если onSurfaceCreated() вызывается во второй раз, вы можете сделать вывод, что контекст был потерян, поэтому сделайте быстрый nativeDestroy(), затем nativeCreate() (или что-то еще), чтобы вернуться в нужное русло. Просто идея. - person greeble31; 17.11.2019
comment
Да, большое спасибо за помощь, вы мне очень помогли, до сих пор не понимаю, почему этого нет в документации по андроиду. - person user1063364; 17.11.2019