Apple Metal в MAPS.ME

imageВсем привет!

В мире существует огромное количество приложений на OpenGL, и, кажется, Apple c этим не вполне согласна. Начиная с iOS 12 и MacOS Mojave, OpenGL переведен в статус устаревшего. Мы интегрировали Apple Metal в MAPS.ME и готовы поделиться своим опытом и результатами. Расскажем, как рефакторили наш графический движок, с какими трудностями пришлось столкнуться и, самое главное, сколько у нас теперь FPS.

Всех, кто заинтересовался или раздумывает над добавлением поддержки Apple Metal в графический движок, приглашаем под кат.

Проблематика


Наш графический движок проектировался как кроссплатформенный, и так как OpenGL является, по сути, единственным кроссплатформенным графическим API для интересующего нас набора платформ (iOS, Android, MacOS и Linux), то выбрали его в качестве основы. Мы не сделали дополнительный уровень абстракции, который скрывал бы характерные для OpenGL особенности, но, к счастью, оставили потенциальную возможность его внедрения.

С появлением графических API нового поколения Apple Metal и Vulkan, мы, разумеется, рассматривали возможность их появления в нашем приложении, однако, нас останавливало следующее:

  1. Vulkan мог работать только на Android и Linux, а Apple Metal — только на iOS и MacOS. Мы не хотели терять кроссплатформенность на уровне графического API, это усложнило бы процессы разработки и отладки, увеличило бы объем работы.
  2. Приложение на Apple Metal не может быть собрано и запущено на iOS-симуляторе (кстати, до сих пор), что также усложнило бы нам разработку и не позволило бы окончательно избавиться от OpenGL.
  3. Qt Framework, который мы используем для создания внутренних инструментов, поддерживал только OpenGL (сейчас поддерживается Vulkan).
  4. Apple Metal не имел и не имеет C++ API, что заставило бы нас придумывать абстракции не только для этапа выполнения, но и для этапа сборки приложения, когда часть движка компилируется на Objective C++, а другая, существенно большая, на C++.
  5. Мы не были готовы делать отдельный движок или отдельную ветку кода специально для iOS.
  6. Внедрение оценивалось, как минимум, в полгода работы одного графического разработчика.


Когда весной 2018 года Apple объявила о переводе OpenGL в статус deprecated, стало понятно, что откладывать больше нельзя, и вышеописанные проблемы необходимо тем или иным способом решить. Кроме того, мы давно уже работали над оптимизацией как скорости работы приложения, так и энергопотребления, и Apple Metal, казалось, мог в этом помочь.

Выбор решения


Почти сразу мы обратили внимание на MoltenVK. Этот фреймворк эмулирует Vulkan API при помощи Apple Metal, к тому же его исходный код был не так давно открыт. Использование MoltenVK, казалось, позволило бы заменить OpenGL на Vulkan, и вообще не заниматься отдельной интеграцией Apple Metal. Кроме того, разработчики Qt отказались от отдельной поддержки рендеринга на Apple Metal в пользу MoltenVK. Однако, нас остановили:

  • необходимость поддерживать Android-устройства, на которых Vulkan недоступен;
  • невозможность запуститься на iOS-симуляторе без наличия fallback на OpenGL;
  • невозможность использовать инструменты Apple для отладки, профилирования и прекомпиляции шейдеров, так как MoltenVK формирует шейдеры для Apple Metal в реальном времени из исходных кодов на SPIR-V или GLSL;
  • необходимость ожидания обновлений и багфиксов MoltenVK при выходе новых версий Metal;
  • невозможность тонкой оптимизации, специфичной для Metal, но не специфичной или не существующей для Vulkan.


Получалось, что OpenGL нам необходимо сохранить, а значит не обойтись без абстрагирования движка от графического API. Apple Metal, OpenGL ES, а в будущем и Vulkan, будут использованы при создании независимых внутренних компонентов графического движка, которые смогут быть полностью взаимозаменяемыми. OpenGL будет играть роль fallback-варианта в тех случаях, когда Metal или Vulkan по той или иной причине недоступны.

План реализации был такой:

  1. Рефакторинг графического движка, чтобы абстрагировать используемый графический API.
  2. Сделать рендеринг на Apple Metal для iOS-версии приложения.
  3. Сделать соответствующие бенчмарки скорости рендеринга и энергопотребления, чтобы понять, смогут ли современные, более низкоуровневые графические API принести пользу продукту.


Ключевые различия между OpenGL и Metal


Чтобы понять, как именно абстрагировать графический API, давайте сначала определим, какие ключевые концептуальные различия есть между OpenGL и Metal.

  1. Считается, и небезосновательно, что Metal является более низкоуровневым API. Однако, это не означает, что вам придется писать на ассемблере или самому реализовывать растеризацию. Metal можно назвать низкоуровневым API в том смысле, что он выполняет очень малое количество неявных действий, то есть почти все действия необходимо прописывать самому программисту. OpenGL очень многое делает неявно, начиная от поддержки неявной ссылки на контекст OpenGL и связи этого контекста с потоком, в котором он был создан.
  2. В Metal «отсутствует» realtime-валидация команд. В режиме отладки валидация, конечно, существует и сделана существенно лучше, чем во многих других API, во многом благодаря тесной интеграции с XCode. А вот когда программа отправляется пользователю, то никакой валидации уже нет, программа просто аварийно завершается на первой же ошибке. Стоит ли говорить, что OpenGL падает только в самых крайних случаях. Самая распространенная практика: проигнорировать ошибку и продолжать работу.
  3. Metal умеет прекомпилировать шейдеры и формировать из них библиотеки. В OpenGL шейдеры компилируются из исходников в процессе работы программы, за это отвечает конкретная низкоуровневая реализация OpenGL на конкретном устройстве. Разница и/или ошибки в реализации компиляторов шейдеров приводят иногда к фантастическим багам, особенно, на Android-устройствах китайских брендов.
  4. OpenGL активно использует машину состояний, что добавляет побочные эффекты практически в каждую функцию. Таким образом, функции OpenGL не являются чистыми (pure) функциями, и часто важен порядок и история вызовов. Metal не использует состояния неявно и не сохраняет их дольше, чем это необходимо для рендеринга. Состояния существуют в виде предварительно созданных и провалидированных объектов.


Рефакторинг графического движка и встраивание Metal


Процесс рефакторинга графического движка, в основном, заключался в поиске лучшего решения для избавления от особенностей OpenGL, которыми активно пользовался наш движок. Встраивание Metal, начиная с какого-то из этапов, шло параллельно.

  • Как уже было замечено, в API OpenGL есть неявная сущность, называемая контекстом. Контекст связывается с конкретным потоком, и функция OpenGL, вызванная в этом потоке, сама находит и использует этот контекст. Metal, Vulkan (да, и другие API, например, Direct3D) так не работают, у них существуют аналогичные явные объекты, называемые device или instance. Пользователь сам создает эти объекты и отвечает за их передачу разным подсистемам. Именно через эти объекты осуществляются все вызовы графических команд.

    Наш абстрактный объект мы назвали графическим контекстом, и в случае OpenGL он просто декорирует вызовы OpenGL-команд, а в случае Metal — содержит корневой интерфейс MTLDevice, через который вызываются команды Metal.

    Разумеется, пришлось распространить этот объект (а так как рендеринг у нас многопоточный, то даже несколько таких объектов) по всех подсистемам.

    Создание очередей команд, кодировщиков (encoders) и управление ими мы скрыли внутри графического контекста, чтобы не распространять по движку сущности, которых просто не существует в OpenGL.

  • Перспектива исчезновения валидации графических команд на устройствах пользователей нас откровенно не радовала. Широкий спектр устройств и версий ОС не мог быть полностью покрыт нашим отделом QA. Поэтому пришлось дописывать развернутые логи там, где раньше мы получали осмысленную ошибку от графического API. Безусловно, эта валидация была добавлена только в потенциально опасные и критически важные места графического движка, так как покрытие диагностическим кодом всего движка практически невозможно и вообще вредно для производительности. Новая реальность такова, что тестирование на пользователях и отладка при помощи логов теперь в прошлом, по крайней мере, в отношении рендеринга.
  • Наша предыдущая система шейдеров оказалась непригодной для рефакторинга, пришлось её полностью переписать. Дело здесь не только в прекомпиляции шейдеров и их валидации на этапе сборки проекта. В OpenGL для передачи параметров в шейдеры используются так называемые uniform-переменные. Передача структурированных данных доступна только с OpenGL ES 3.0, а так как мы по-прежнему поддерживаем OpenGL ES 2.0, то этот способ мы просто не использовали. Metal заставил нас использовать структуры данных для передачи параметров, а для OpenGL пришлось придумывать mapping полей структуры на uniform-переменные. Кроме того, пришлось заново написать каждый из шейдеров на Metal Shading Language.
  • При использовании объектов состояний нам пришлось пойти на хитрость. В OpenGL все состояния, как правило, выставляются непосредственно перед рендерингом, а в Metal это должен быть предварительно созданный и прошедший валидацию объект. Наш движок, очевидно, использовал подход OpenGL, а рефакторинг с предварительным созданием объектов состояний был соизмерим с полным переписыванием движка. Чтобы разрубить этот узел, мы создали внутри графического контекста кэш состояний. В первый раз, когда формируется уникальная комбинация параметров состояний, в Metal создается объект состояния и помещается в кэш. Во второй и последующие разы объект просто извлекается из кэша. Это работает у нас в картах, так как количество разных комбинаций параметров состояний не слишком велико (порядка 20–30). Для сложного игрового графического движка такой способ вряд ли подойдет.


В итоге, примерно через 5 месяцев работ мы смогли в первый раз запустить MAPS.ME с полноценным рендерингом на Apple Metal. Пора было узнать, что же у нас получилось.

Тестирование скорости рендеринга


Методика эксперимента


Мы использовали в эксперименте устройства Apple разных поколений. Все они были обновлены до iOS 12. На всех исполнялся одинаковый пользовательский сценарий — навигация по карте (перемещение и масштабирование). Сценарий был заскриптован, чтобы гарантировать почти полную идентичность процессов внутри приложения при каждом запуске на каждом из устройств. В качестве тестовой локации выбрали район Лос-Анджелеса — одна из самых высоконагруженных областей в MAPS.ME.

Сначала сценарий исполнялся с рендерингом на OpenGL ES 3.0, затем на том же устройстве с рендерингом на Apple Metal. Между запусками приложение полностью выгружалось из памяти.
Измерялись следующие показатели:

  • FPS (frames per second) для кадра целиком;
  • FPS для части кадра, которая занимается только рендерингом, исключая подготовку данных и прочие покадровые операции;
  • Процент медленных кадров (меньше 30 мс), т.е. тех, которые человеческий глаз может воспринимать как рывки.


При измерении FPS исключалось рисование непосредственно на экране устройства, так как вертикальная синхронизация с частотой обновления экрана не позволяет получить достоверные результаты. Поэтому кадр рисовался в текстуру в памяти. Для синхронизации CPU и GPU в OpenGL использовался дополнительный вызов команды glFinish, в Apple Metal — waitUntilCompleted для MTLFrameCommandBuffer.

iPhone 6s iPhone 7+ iPhone 8
OpenGL Metal OpenGL Metal OpenGL Metal
FPS 106 160 159 221 196 298
FPS (только рендеринг) 157 596 247 597 271 833
Доля медленных кадров (< 30 fps) 4,13% 1,25% 5,45% 0,76% 1,5% 0,29%


iPhone X iPad Pro 12.9'
OpenGL Metal OpenGL Metal
FPS 145 210 104 137
FPS (только рендеринг) 248 705 147 463
Доля медленных кадров (< 30 fps) 0,15% 0,15% 17,52% 4,46%


iPhone 6s iPhone 7+ iPhone 8 iPhone X iPad Pro 12.9'
Ускорение кадра на Metal (в N раз) 1,5 1,39 1,52 1,45 1,32
Ускорение рендеринга на Metal (в N раз) 3,78 2,41 3,07 2,84 3,15
Улучшение по медленным кадрам (в N раз) 3,3 7,17 5,17 1 3,93


Анализ результатов


В среднем, прирост производительности кадра при использовании Apple Metal составил 43%. Минимальное значение зафиксировано на iPad Pro 12.9» — 32%, максимальное — 52% на iPhone 8. Просматривается зависимость: чем меньше разрешение экрана, тем больше Apple Metal превосходит OpenGL ES 3.0.

Если оценивать часть кадра, ответственную непосредственно за рендеринг, то в среднем скорость рендеринга на Apple Metal выросла в 3 раза. Это говорит о существенно лучшей организации, и, как следствие, эффективности Apple Metal API по сравнению с OpenGL ES 3.0.

Количество медленных кадров (меньше 30 мс), на Apple Metal сократилось примерно в 4 раза. Это означает, что восприятие анимаций и перемещения по карте стало более плавным. Наихудший результат зафиксирован на iPad Pro 12.9» с разрешением 2732×2048 пикселей: OpenGL ES 3.0 дает примерно 17,5% медленных кадров, тогда как Apple Metal — только 4,5%.

Тестирование энергопотребления


Методика эксперимента


Энергопотребление тестировалось на iPhone 8 на iOS 12. Исполнялся одинаковый пользовательский сценарий — навигация по карте (перемещение и масштабирование) в течение 1 часа. Сценарий был заскриптован, чтобы гарантировать почти полную идентичность процессов внутри приложения на каждом запуске. В качестве тестовой локации был также выбран район Лос-Анджелеса.

Мы использовали следующий подход к измерению энергопотребления. Устройство не подключено к зарядке. В настройках разработчика включено логирование энергопотребления. Перед началом эксперимента устройство полностью заряжено. Конец эксперимента наступает по завершению сценария. В конце эксперимента фиксировалось состояние заряда батареи, а логи энергопотребления импортировались в утилиту для профилирования батареи в XCode. Мы регистрировали, какая часть заряда была потрачена на работу GPU. Кроме того, здесь мы дополнительно утяжелили рендеринг, включив отображение схемы метро и полноэкранный антиалиасинг.

Яркость экрана не менялась во всех случаях. Никаких других процессов, кроме системных и MAPS.ME, не исполнялось. Был включен авиарежим, выключены Wi-Fi и GPS. Дополнительно проводилось несколько контрольных измерений.

В итоге, для каждого из показателей формировалось сравнение Metal с OpenGL, а затем коэффициенты отношения усреднялись, чтобы получить одну агрегированную оценку.

OpenGL Metal Прирост
Потраченный заряд батареи 32% 28% 12,5%
Профилирование Battery Usage в XCode 1,95% 1,83% 6,16%


Анализ результатов


В среднем, энергопотребление версии с рендерингом на Apple Metal незначительно улучшилось. На энергопотребление нашего приложения GPU оказывает не слишком большое влияние, порядка 2%, потому что MAPS.ME нельзя назвать высоконагруженным с точки зрения использования GPU. Небольшой выигрыш достигается, вероятно, за счет уменьшения вычислительных затрат при подготовке команд для GPU на CPU, что, к сожалению, нельзя выделить при помощи инструментов профилирования.

Итоги


Встраивание Metal обошлось нам в 5 месяцев разработки. Этим занимались два разработчика, правда, почти всегда по очереди. Мы, очевидно, значительно выиграли по производительности рендеринга, немного выиграли по энергопотреблению. Кроме того, мы получили возможность встраивать новые графические API, в частности, Vulkan, куда меньшими усилиями. Почти целиком «перебрали» графический движок, в результате нашли и исправили несколько старых багов и проблем с производительностью.

На вопрос, действительно ли нашему проекту нужен рендеринг на Apple Metal, мы готовы ответить утвердительно. Дело не столько в том, что мы любим инновации, или в том, что Apple может окончательно отказаться от OpenGL. Просто на дворе 2018 год, а OpenGL появился в далеком 1997-м, давно пора сделать следующий шаг.

P.S. Пока мы не запустили фичу на всех iOS-устройствах. Для ручного включения напишите в строке поиска команду ?metal и перезапустите приложение. Чтобы вернуть рендеринг на OpenGL, введите команду ?gl и перезапустите приложение.

P.P. S. MAPS.ME — это open-source проект. С исходными кодами вы можете ознакомиться на github.

© Habrahabr.ru