Как сделать асинхронный вывод в контекст 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;
Затем настройте их в 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:
- Каждая задача должна устанавливать контекст перед выполнением любых команд OpenGL ES.
- Хорошо. Я устанавливаю контекст. Это указатели на объекты Objective-C, у которых в любом случае время жизни больше, чем у блока, поэтому они должны закрываться слишком хорошо. Кроме того, я могу проверить их в отладчике, и они в порядке. Кроме того, когда я рисую из dispatch_sync, это работает. Так что это не проблема.
- Две задачи, которые обращаются к одному и тому же контексту, могут никогда не выполняться одновременно.
- Единственный код, обращающийся к контексту GL после его настройки, - это код в этом методе, который, в свою очередь, находится в этом блоке. Так как это последовательная очередь, только один экземпляр этого должен когда-либо рисоваться за раз в любом случае. Далее, если я добавлю
synchronized(self.context){}
блок, это ничего не исправляет. Кроме того, в другом коде с очень медленной прорисовкой я добавил семафор, чтобы пропустить добавление блоков в очередь, когда предыдущий еще не закончил и он удалял кадры нормально (в соответствии сNSLog()
сообщения, которые он выплевывал), но это не исправило рисунок. ОДНАКО, есть вероятность, что часть кода GLKit, который я не вижу, манипулирует контекстом способами, которые я не понимаю из основного потока. Сейчас это моя вторая по рейтингу теория, несмотря на то, что synchronized() не меняет проблему, а OpenGL Profiler не показывает никаких конфликтов потоков.
- Единственный код, обращающийся к контексту GL после его настройки, - это код в этом методе, который, в свою очередь, находится в этом блоке. Так как это последовательная очередь, только один экземпляр этого должен когда-либо рисоваться за раз в любом случае. Далее, если я добавлю
- Каждая задача должна очищать контекст потока перед выходом.
- Я не совсем понимаю, что это значит. Контекст GCD-потока? Все в порядке. Мы ничего не добавляем в контекст очереди, поэтому нечего очищать. EAGLContext, к которому мы рисуем? Я не знаю, что еще мы могли бы сделать. Конечно, на самом деле не блестит, это просто сотрет все. Также в Molecules Сансет-Лейк есть некоторый код, который выглядит так:
Код:
dispatch_async(openGLESContextQueue, ^{
[EAGLContext setCurrentContext:context];
GLfloat currentModelViewMatrix[9];
[self convert3DTransform:¤tCalculatedMatrix 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 было действительным. Когда вы выполняете эту диспетчеризацию, вы по существу исключаете выполнение этого кода чертежа из области действия этого метода.
Я не понимаю, почему это должно быть правдой. Но:
- Я только закрываю ссылки на вещи в блоке, которые переживут блок, и это такие вещи, как указатели Objective-C из области видимости объекта.
- Я могу проверить их в отладчике, они выглядят хорошо.
- Я вставил вызов getGLError() после каждой операции GL, и он никогда не возвращает ничего, кроме нуля.
- Рисование из блока с dispatch_sync работает.
- Я попытался подумать, где в
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 в главном потоке и выполнять "другие вещи" из основного потока.