Как асинхронно рисовать контекст GLKit OpenGL ES из очереди отправки Grand Central на iOS

Я пытаюсь переместить длительные операции рисования OpenGL в очередь GCD, чтобы я мог заниматься другими делами, пока GPU работает. Я бы гораздо лучше сделать это с помощью GCD, а не добавлять в свое приложение настоящую многопоточность. Буквально все, что я хочу сделать, это иметь возможность

  • Не блокировать вызов glDrawArrays(), чтобы остальная часть пользовательского интерфейса могла оставаться отзывчивой, когда рендеринг GL становится очень медленным.
  • Отбрасывать вызовы glDrawArrays(), когда мы их все равно не заканчиваем (не создавайте очередь кадров, которая только растет и растет)

На веб-сайте Apple в документах говорится:

Объекты GCD и NSOperationQueue могут выполнять ваши задачи в потоке по своему выбору. Они могут создать поток специально для этой задачи или повторно использовать существующий поток. Но в любом случае вы не можете гарантировать, какой поток выполняет задачу. Для приложения OpenGL ES это означает:

  • Каждая задача должна установить контекст перед выполнением любых команд OpenGL ES.
  • Две задачи, обращающиеся к одному и тому же контексту, никогда не могут выполняться одновременно.
  • Каждая задача должна очищать контекст потока перед выходом.

Звучит довольно просто.

Для простоты в этом вопросе я начну с новой версии шаблона Apple, которая появляется в диалоговом окне «Новый проект» для игры «OpenGL ES». Когда вы создадите его, скомпилируете и запустите, вы должны увидеть два вращающихся куба на сером поле.

К этому коду я добавил очередь GCD. Начиная с раздела интерфейса ViewController.m:

dispatch_queue_t openGLESDrawQueue;

Затем настройте их в ViewController viewDidLoad:

openGLESDrawQueue = dispatch_queue_create("GLDRAWINGQUEUE", NULL);

Наконец, я делаю эти очень небольшие изменения в методе drawInRect, который в конечном итоге запускает CADisplayLink:

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
void (^glDrawBlock)(void) = ^{
    [EAGLContext setCurrentContext:self.context];
    glClearColor(0.65f, 0.65f, 0.65f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glBindVertexArrayOES(_vertexArray);

    // Render the object with GLKit
    [self.effect prepareToDraw];

    glDrawArrays(GL_TRIANGLES, 0, 36);

    // Render the object again with ES2
    glUseProgram(_program);

    glUniformMatrix4fv(uniforms[UNIFORM_MODELVIEWPROJECTION_MATRIX], 1, 0, _modelViewProjectionMatrix.m);
    glUniformMatrix3fv(uniforms[UNIFORM_NORMAL_MATRIX], 1, 0, _normalMatrix.m);

    glDrawArrays(GL_TRIANGLES, 0, 36);
};
dispatch_async(openGLESDrawQueue, glDrawBlock);
}

Это не работает. Рисунок сходит с ума. Однако рисование тем же блоком с dispatch_sync() работает нормально.

Давайте дважды проверим список Apple:

  • Each task must set the context before executing any OpenGL ES commands.
    • Ok. I'm setting the context. It's Objective-C object pointers that have a lifetime longer than the block anyway, so they should get closed over fine. Also, I can check them in the debugger and they are fine. Also, when I draw from dispatch_sync, it works. So this does not appear to be the problem.
  • Two tasks that access the same context may never execute simultaneously.
    • The only code accessing the GL context once it's set up is the code in this method, which is in turn, in this block. Since this a serial queue, only one instance of this should ever be drawing at a time anyway. Further, if I add a synchronized(self.context){} block, it doesn't fix anything. Also, in other code with very slow drawing I added a semaphore to skipping adding blocks to the queue when the previous one hadn't finished and it dropped frames fine (according to the NSLog() messages it was spitting out), but it didn't fix the drawing. HOWEVER, there is the possibility that some of the GLKit code that I can't see manipulates the context in ways I don't understand from the main thread. This is my second-highest rated theory right now, despite the fact that synchronized() doesn't change the problem and OpenGL Profiler doesn't show any thread conflicts.
  • Each task should clear the thread’s context before exiting.
    • I'm not totally clear on what this means. The GCD thread's context? That's fine. We're not adding anything to the queue's context so there's nothing to clean up. The EAGLContext that we're drawing to? I don't know what else we could do. Certainly not actually glClear it, that will just erase everything. Also, there's some code in Sunset Lake's Molecules that renders that looks like this:

Код:

dispatch_async(openGLESContextQueue, ^{
    [EAGLContext setCurrentContext:context];        
    GLfloat currentModelViewMatrix[9];
    [self convert3DTransform:&currentCalculatedMatrix to3x3Matrix:currentModelViewMatrix];
    CATransform3D inverseMatrix = CATransform3DInvert(currentCalculatedMatrix);
    GLfloat inverseModelViewMatrix[9];
    [self convert3DTransform:&inverseMatrix to3x3Matrix:inverseModelViewMatrix];

    GLfloat currentTranslation[3];
    currentTranslation[0] = accumulatedModelTranslation[0];
    currentTranslation[1] = accumulatedModelTranslation[1];
    currentTranslation[2] = accumulatedModelTranslation[2];

    GLfloat currentScaleFactor = currentModelScaleFactor;

    [self precalculateAOLookupTextureForInverseMatrix:inverseModelViewMatrix];
    [self renderDepthTextureForModelViewMatrix:currentModelViewMatrix translation:currentTranslation scale:currentScaleFactor];
    [self renderRaytracedSceneForModelViewMatrix:currentModelViewMatrix inverseMatrix:inverseModelViewMatrix translation:currentTranslation scale:currentScaleFactor];

    const GLenum discards[]  = {GL_DEPTH_ATTACHMENT};
    glDiscardFramebufferEXT(GL_FRAMEBUFFER, 1, discards);

    [self presentRenderBuffer];

    dispatch_semaphore_signal(frameRenderingSemaphore);
});

Этот код работает, и я не вижу никакой дополнительной очистки. Я не могу понять, что этот код делает иначе, чем мой. Отличие заключается в том, что буквально все, что касается контекста GL, выполняется из одной и той же очереди отправки GCD. Однако, когда я делаю свой код таким, он ничего не исправляет.

Последнее отличие состоит в том, что этот код не использует GLKit. Приведенный выше код (вместе с кодом, который меня действительно интересует) действительно использует GLKit.

На данный момент у меня есть три теории по поводу этой проблемы: 1. Я допускаю концептуальную ошибку в отношении взаимодействия между блоками, НОД и OpenGL ES. 2. GLKViewController или GLKView GLKit выполняет некоторое рисование или манипулирование EAGLContext между вызовами drawInRect. Пока мои блоки drawInRect обрабатываются, это происходит, и все портится. 3. Тот факт, что я полагаюсь на метод - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect, сам по себе является проблемой. Я думаю об этом методе так: «Эй, у вас автоматически настроен CADisplayLink, и каждый раз, когда ему нужен кадр, он будет использовать этот метод. Это не значит, что я возвращаю объект кадрового буфера или объект CGImageRef, содержащий то, что я хочу видеть на экране. Я даю команды GL. ОДНАКО это может быть неправильно. Возможно, вы просто не можете отложить рисование в этот метод в любом случае не вызвал проблем.Чтобы проверить эту теорию, я переместил весь код отрисовки в метод с именем drawStuff, а затем заменил тело метода drawRect на:

[NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(drawStuff) userInfo:nil repeats:NO];

Приложение появляется, отображает цвет, к которому представление glClear в течение десяти секунд, а затем рисует как обычно. Так что эта теория тоже не выглядит слишком убедительной.

Здесь опубликован похожий вопрос, на который есть один ответ: проголосовали и приняли:

Код в блоке отправки не будет работать. К тому времени, когда он будет выполнен, все состояние OpenGL для этого кадра уже давно будет уничтожено. Если бы вы поместили вызов glGetError() в этот блок, я уверен, что он сказал бы вам то же самое. Вы должны убедиться, что весь ваш код рисования выполнен в этом методе glkView, чтобы состояние OpenGL было действительным. Когда вы выполняете эту отправку, вы, по сути, выводите выполнение этого кода рисования за рамки этого метода.

Я не понимаю, почему это должно быть правдой. Но:

  1. Я закрываю только ссылки на элементы в блоке, которые должны пережить блок, и это такие элементы, как указатели Objective-C из области охвата объекта.
  2. Я могу проверить их в отладчике, они выглядят нормально.
  3. Я вставил вызов getGLError() после каждой операции GL, и он никогда не возвращает ничего, кроме нуля.
  4. Отрисовка из блока с помощью dispatch_sync работает.
  5. Я попытался подумать, где в методе drawInRect я сохраняю блок в ivar, а затем устанавливаю NSTimer для вызова drawStuff. В drawStuff я просто вызываю блок. Рисует нормально.

Случай NSTimer отрисовывается асинхронно, но он не включает отрисовку из другого потока, поскольку вызовы AFAIK NSTimer просто планируются в цикле выполнения потока настройки. Так что это связано с потоками.

Может ли кто-нибудь подсказать мне, что мне здесь не хватает?


person Jeb    schedule 08.11.2013    source источник
comment
Просто предположение... возможно, GLKit представляет буфер рендеринга после возврата метода drawRect:? Это будет означать, что до завершения рендеринга.   -  person borrrden    schedule 08.11.2013
comment
Проблема с этой теорией, ИМХО, заключается в том, что если я сохраню блок в drawRect и запланирую его запуск с таймером на секунды позже, все будет нормально. Прежде чем какой-либо из блоков будет фактически вызван, представление просто окрашивается в цвет glClear(). Весь цикл отрисовки просто отодвигается на несколько секунд, но проходит плавно. Я имею в виду, вы должны быть на стадионе?   -  person Jeb    schedule 08.11.2013
comment
Этот вопрос и ответ на него великолепны. У меня такая же проблема. Было бы здорово, если бы GLKViewController можно было настроить для работы с фоновыми потоками.   -  person Ricardo Sanchez-Saez    schedule 01.04.2015


Ответы (1)


Это не работает, потому что, как говорит borrrden, GLKit вызывает presentRenderbuffer: сразу после завершения - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect.

Это работает в вашем случае использования таймера, потому что метод drawStuff вызывается в основном потоке в начале цикла рисования. Ваш - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect фактически ничего не делает, кроме планирования повторения этого в основном потоке еще через 10 секунд, а ранее запланированный вызов отрисовки затем отображается в конце метода drawInRect:. Это ничего не делает, кроме задержки рисования на 10 секунд, все по-прежнему происходит в основном потоке.

Если вы хотите пойти по пути рендеринга вне основного потока, GLKit не подойдет. Вам нужно будет настроить свой собственный поток с циклом выполнения, подключить CADisplayLink к этому циклу выполнения, а затем выполнить рендеринг из этой очереди. GLKViewController настроен на использование для этого основного цикла выполнения и всегда будет отображать буфер рендеринга в конце каждого кадра, что вызовет хаос во всем, что вы делаете в другом потоке.

В зависимости от ваших потребностей GL вам может оказаться проще выполнять все вещи GL в основном потоке и выполнять «другие вещи» вне основного потока.

person Tark    schedule 09.11.2013
comment
Небольшой комментарий: согласно developer.apple.com/ library/ios/documentation/3DDrawing/ допустимо просто использовать очередь последовательной отправки для содержания контекста; вам не нужен собственный поток и цикл выполнения, если у вас есть место, куда можно прикрепить ссылку на отображение или таймер, из которого можно публиковать активность в очереди. - person Tommy; 01.03.2016