Как сделать асинхронный вывод в контекст GLGit 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;

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

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:

  • Каждая задача должна устанавливать контекст перед выполнением любых команд OpenGL ES.
    • Хорошо. Я устанавливаю контекст. Это указатели на объекты Objective-C, у которых в любом случае время жизни больше, чем у блока, поэтому они должны закрываться слишком хорошо. Кроме того, я могу проверить их в отладчике, и они в порядке. Кроме того, когда я рисую из dispatch_sync, это работает. Так что это не проблема.
  • Две задачи, которые обращаются к одному и тому же контексту, могут никогда не выполняться одновременно.
    • Единственный код, обращающийся к контексту GL после его настройки, - это код в этом методе, который, в свою очередь, находится в этом блоке. Так как это последовательная очередь, только один экземпляр этого должен когда-либо рисоваться за раз в любом случае. Далее, если я добавлю synchronized(self.context){} блок, это ничего не исправляет. Кроме того, в другом коде с очень медленной прорисовкой я добавил семафор, чтобы пропустить добавление блоков в очередь, когда предыдущий еще не закончил и он удалял кадры нормально (в соответствии с NSLog() сообщения, которые он выплевывал), но это не исправило рисунок. ОДНАКО, есть вероятность, что часть кода GLKit, который я не вижу, манипулирует контекстом способами, которые я не понимаю из основного потока. Сейчас это моя вторая по рейтингу теория, несмотря на то, что synchronized() не меняет проблему, а OpenGL Profiler не показывает никаких конфликтов потоков.
  • Каждая задача должна очищать контекст потока перед выходом.
    • Я не совсем понимаю, что это значит. Контекст GCD-потока? Все в порядке. Мы ничего не добавляем в контекст очереди, поэтому нечего очищать. EAGLContext, к которому мы рисуем? Я не знаю, что еще мы могли бы сделать. Конечно, на самом деле не блестит, это просто сотрет все. Также в Molecules Сансет-Лейк есть некоторый код, который выглядит так:

Код:

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. Я делаю концептуальную ошибку о взаимодействии между блоками, GCD и OpenGL ES. 2. GLKit's GLKViewController или же GLKView рисовать или манипулировать EAGLContext между звонками drawInRect, Пока мой drawInRect над блоками работают, это случается, все портит. 3. Тот факт, что я полагаюсь на - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect Метод сам по себе проблема. Я думаю об этом методе как: "Эй, у вас автоматически CADisplayLink настроен, и каждый раз, когда он хочет кадр, он будет попадать в этот метод. Делай, что хочешь, черт возьми. Я имею в виду, что в обычном коде вы просто запускаете команды glDrawArrays. Это не так, как будто я возвращаю объект framebuffer или 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 просто планируются в цикле выполнения потока установки. Так что это связано с потоками.

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

1 ответ

Решение

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

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

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

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

Другие вопросы по тегам