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 x 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.

Комментарии (15)


  1. thousandsofthem
    13.12.2018 21:32
    +1

    Есть какие-то определенные планы по поддержке Vulkan?


    1. rokuz Автор
      13.12.2018 23:18

      Определенными планами порадовать не можем, но все может быть)


  1. makufulai
    13.12.2018 23:39
    +2

    Пользуюсь приложением ещё с тех времён, когда оно называлось Maps with me, до сих пор для себя альтернатив в путешествиях не нашел.
    А уж быстродействие на стареньком iPhone 6 Plus просто фантастическое.

    И может я сейчас выскажусь не по теме, но не могли бы вы прояснить пару вопросов:
    1) Планируется ли полноценная запись GPS треков? С возможностью импорта и экспорта.
    2) Будет ли онлайн личный кабинет? С возможностью редактирования меток и создания собственных маршрутов. Например при подготовке поездки, очень неудобно набивать 20-30 меток на дисплее телефона. А уж отсортировать 200-300 меток на телефоне вообще не реально.


    1. terrakok
      14.12.2018 00:58

      Полностью поддерживаю предыдущего оратора!
      P.S.: для составления маршрутов и просмотра их в Maps.Me использую это: share.mapbbcode.org Не знаю, что за добрый человек это сделал, но очень помогает!


      1. ALPINE63rus
        14.12.2018 01:15
        +1

        Этого доброго человека зовут Илья Зверев Zverik.


        1. ooprizrakoo
          14.12.2018 01:44
          +2

          … который помимо многих хороших штук ещё и три года делал Maps.me )))


    1. rokuz Автор
      14.12.2018 06:35

      По пункту 1 ничего конкретного сказать не могу, а вот по пункту 2 уже в самых ближайших релизах появится одна очень крутая фича. Следите за обновлениями :)


    1. StallinHrusch
      14.12.2018 16:52

      по поводу пункта 2: я это решал путем составления маршрута с метками на гугл мэпс с сохранением там своей карты, потом экспортировал в KML/KMZ там же и отправлял почтой на телефон, затем открывал этот файл через maps.me (приложение). И он создает новую коллекцию закладок со всеми метками и даже построенными маршрутами.
      Да, воркэраунд, но цель достигается и есть свои плюсы. Например у мэпс ми ужасный поиск (и с каждым релизом он хуже и медленее). Плюс в некоторых странах он строит очень странные автомобильные маршруты (было такое что выводил на несуществующую дорогу или на встречку (в мексике))


  1. FreeNickname
    14.12.2018 00:56
    +2

    Спасибо, очень интересный опыт!

    Скажите, правильно ли я понимаю, что теперь, если добавить поддержку Vulkan, можно будет сравнительно малыми усилиями прикрутить эксперимента ради MoltenVK и сравнить по производительности с чистым Metal?

    Вдогонку:

    Разница и/или ошибки в реализации компиляторов шейдеров приводят иногда к фантастическим багам, особенно, на Android-устройствах китайских брендов.

    А можно пару самых фееричных? :)


    1. rokuz Автор
      14.12.2018 07:04

      Добавить MoltenVk будет точно легче, чем раньше, особую сложность будут представлять шейдеры на spir-v. Ну, и в целом это далеко не 5 минут.
      Про баги. Когда вышел чип Mali-G72 в одном из устройств (не буду называть бренд и модель) мы очень радовались производительности приложения на нем, но был неприятный нюанс: приложение падало в режиме навигации где-то в недрах видеодрайвера. Все было странно, так как в других девайсах с этим чипом все работало как часы. А выяснилось следующее. У нас использовался общий вершинный буфер для рисования маршрута и навигационных стрелок. Рисовался он, очевидно, разными шейдерами, которые использовали разные атрибуты вершин. В свою очередь разработчики компилятора шейдеров перемудрили с оптимизацией и решили что помимо неиспользуемых uniform при компиляции шейдера можно выкинуть и неиспользумые атрибуты вершин, но забыли про смещение :) В итоге шейдер считал, что размер вершины меньше, чем на самом деле, и уже вторая вершина в буфере мапилась некорректно. У видеодрайвера срывало башню, так как со стороны API OpenGL все валидации тоже были пройдены, а размер вершины был другой, и он аварийно завершал приложение.


  1. pronvit
    14.12.2018 04:19

    Практически нет приложений, по плавности отрисовки и управления дотягивающих до родной Maps.app. У Galileo получилось, у вас не очень, но гораздо лучше, чем, например, у Waze, конечно.


  1. Mr_Boshi
    14.12.2018 12:52

    Сейчас сравнил плавность отрисовки при зуме и перемещении по карте на iPad 2018 и Galaxy S9. Версия под iOS сильно проигрывала в плавности и была очень резкой. Ввел в поиск ?metal, перезапустил приложение, сравнил еще раз. Разница есть, она заметна, стало значительно лучше. Подгрузка карты из-за края экрана при сдвиге быстра настолько, что ее не видно (на Android видно). Учитывая, что экран планшета (а следовательно и количество отображаемых элементов) примерно вчетверо больше, чем у телефона, нельзя не сказать спасибо за работу)

    Maps.me вообще выглядит довольно хорошо. На Google Maps в режиме навигации смотреть невозможно. Все дома пропадают, направление определить невозможно. По его мнению я постоянно иду вдоль маршрута боком. И калибровка компаса помогает не надолго. Maps.me в этом плане замечателен.
    На мой взгляд, главная проблема Maps.me — отвратительный поиск. Он довольно плохо работает, например, при попытках ввести точный адрес. Скопировал ты его откуда-то, а тебе «ничего не найдено». Оказывается, что вместо «корп.» надо написать «корпус», или «стр.», или просто через слэш после номера дома — тогда найдется. Наверное, это проблема базы данных, но опция «не требовать точного совпадения» при поиске была бы очень к месту. А то приходится искать где дом находится на G.Maps, а потом строить маршрут туда в Maps.me.
    А еще не хватает навигации на заблокированном экране)


  1. Darkxiv
    14.12.2018 15:06

    Статья интересная, спасибо.

    А какие именно группы состояний кэшируются? Т.е., грубо говоря кэшируется уникальная комбинация RasterizerState, DepthStencilState, BlendState? Или туда ещё добавляются состояния сэмплеров или ещё какие-нибудь?


    1. rokuz Автор
      14.12.2018 15:46

      Есть кэши MTLDepthStencilState, MTLRenderPipelineState и MTLSamplerState
      Подробнее здесь: github.com/mapsme/omim/blob/master/drape/metal/metal_states.hpp


  1. AdventurerRussia
    15.12.2018 19:04

    Виден большой прирост производительности на iPad Air. Этот старичик немного подтупливал при прокрутке карты, а сейчас он все стало очень шустро. Все отрисовывается очень быстро и плавно.