Metal — это низкопроизводительный и высокопроизводительный API для выполнения графических и вычислительных операций на графическом процессоре. Обычная задача графического процессора — отрисовывать геометрию, а фундаментальные принципы проектирования Metal направлены на то, чтобы помочь приложениям очень быстро рисовать геометрию.
Геометрия рисования выполняется с помощью вызовов отрисовки на графическом процессоре. Вызов отрисовки — это набор графических команд и состояний, которые создают визуальный результат на экране; для каждого вызова отрисовки требуется свой собственный вектор графического состояния, то есть требуется явно указать шейдеры, графические состояния, буферы данных, текстуры и цели рендеринга, используемые для выполнения рисования. Во всех аппаратных графических API предыдущего поколения, таких как OpenGL ES, изменение векторов состояния является очень дорогостоящей операцией, поскольку все команды API должны быть преобразованы в соответствующие аппаратные команды. Затраты на это обычно полностью ложатся на центральный процессор, тот, кто отвечает за выполнение такого перевода, и все команды API должны быть переведены до того, как графический процессор сможет начать выполнять какую-либо работу. На следующем рисунке показана типичная последовательность вызовов отрисовки и поток выполнения от приложения (со стороны ЦП) к графическому процессору.

Принципы проектирования и архитектура
Металл построен вокруг 6 ключевых принципов дизайна:
- Максимально тонкий API, что означает сокращение объема кода, выполняемого между приложением и графическим процессором.
- Разработан для обеспечения полной поддержки всех современных аппаратных функций графического процессора.
- Делайте дорогостоящие операции реже.
- Обеспечьте предсказуемую производительность.
- Обеспечьте явный контроль над отправкой команд.
- Оптимизировано для поведения процессора.
Почти все современные мобильные игры, как правило, управляют рабочей нагрузкой ЦП и ГП, ориентируясь на определенную частоту кадров, и в большинстве случаев эта цель составляет 60 кадров в секунду (fps), а в других случаях — 30 кадров в секунду. На следующем рисунке показан типичный случай игры, которая пытается оптимизировать нагрузку на ЦП и ГП, поддерживая стабильные 30 кадров в секунду: ЦП подготавливает команды рендеринга для определенного кадра, а ГП использует эти команды в течение следующего кадра.

Когда все работает так, как ожидалось, эта установка может обеспечить идеальный и хорошо сбалансированный параллелизм, но это идеальная ситуация, потому что в реальной жизни большую часть времени процессору может потребоваться больше времени для генерации команд рендеринга, чем графическому процессору для их использования, и это оставляет GPU бездействует часть кадра. Рассмотрев немного подробнее работу, которую должен выполнять ЦП, мы можем разделить ее на 2 части: время, затрачиваемое ЦП на выполнение логики приложения, и время, затрачиваемое ЦП на подготовку команд API рендеринга; обычно последний занимает большую часть доступного времени кадра. Как видно на следующем рисунке, ЦП не смог преобразовать все команды API в пределах целевого времени кадра, и это может привести к пропуску кадра графическим процессором.

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

Чтобы лучше понять, как Metal API может достичь такого результата, важно понять, почему программирование GPU так дорого обходится ЦП.
Есть 3 основные причины:
- Проверка состояния: каждый раз, когда приложение вызывает API рендеринга, реализация API рендеринга должна проверять правильность выполнения вызова: приложение использует правильное количество и тип параметров, а аппаратный контекст перейти в допустимое состояние после завершения вызова. Но это еще не все! при вызове API реализация также должна кодировать состояния API в соответствующие аппаратные состояния и снова проверять другие аппаратные состояния, чтобы выяснить, как объединить их все вместе, чтобы переместить глобальный контекст в новый.
- Компиляция шейдеров исходный код всех шейдеров должен быть скомпилирован для генерации машинного кода графического процессора, и это обычно происходит во время выполнения. Часто состояние и код шейдера описываются не так, как на самом деле ожидает аппаратное обеспечение, и поэтому, когда приложение меняет определенные состояния, может случиться так, что сгенерированный машинный код придется перекомпилировать.
- Состояния отправки работы графического процессора и код шейдеров могут запрашивать ресурсы, которые не являются резидентными на стороне графического процессора, поэтому они должны быть перемещены в память в место, где графический процессор может получить к ним доступ.
Поскольку все эти действия, которые делают все игры, заключаются в объединении вместе операций, использующих схожие состояния и ресурсы, с целью снижения рабочей нагрузки и повышения эффективности, мы обычно называем этот процесс пакетными командами… но пакетные команды требуют выполнения большей логики на компьютере. ЦП для создания этих пакетов. Таким образом, конечным результатом является то, что всегда существует постоянная работа по балансировке между расписанием и правильным объемом работы для ЦП, чтобы создать рабочую нагрузку, которая будет поддерживать загруженность ГП в течение всего кадра и выполнять всю эту работу в пределах целевого времени кадра.
Причина, по которой Metal отличается, заключается в том, что принцип конструкции позволяет реже выполнять дорогостоящие операции.
Во всех API рендеринга до Metal, особенно OpenGL ES, проверка состояния, компиляция шейдеров и отправка работы GPU происходили во время рисования кадра, что делало управление временем кадра ограниченным вещами, не находящимися под непосредственным контролем приложения. Metal поддерживает автономную компиляцию шейдеров и проверку состояния при создании объекта рендеринга, и это оставляет приложению только заботу о передаче работы графическому процессору и ничего больше.
Чтобы лучше понять все это, давайте подробно рассмотрим всю объектную часть Metal API; давайте пройдемся по всем из них:
- Устройство (MTLDevice): это абстракция физического графического процессора, это то, что будет потреблять команды рендеринга и вычислений; это также объект, который можно использовать для любых действий в Metal, поскольку все объекты, с которыми взаимодействует приложение, исходят из этого объекта.
- Очередь команд (MTLCommandQueue): этот объект хранит все команды и позволяет приложению контролировать порядок выполнения всех команд.
- Буфер команд (MTLCommandBuffer): этот объект хранит переведенные аппаратные команды, готовые для использования графическим процессором.
- Кодер команд (MTLCommandEncoder): этот объект отвечает за преобразование команд рендеринга и вычислений в аппаратные команды.
- Состояния: состояние графического процессора описывается рядом объектов состояния: конфигурация буферов кадра, тип смешивания, функция глубины, различные сэмплы, используемые при работе с текстурами. хранятся в объектах.
- Код: представляет собой исходный код всех вершинных и фрагментных шейдеров, объявленных и используемых приложением.
- Ресурсы: это объекты, которые хранят в памяти данные, представляющие ресурсы, такие как буферы вершин, текстуры или набор констант шейдера.
Как показано на рисунке выше, из экземпляра MTLDevice приложение создает объект MTLCommandQueue; обычно приложение создает одну или несколько очередей команд при инициализации, а затем сохраняет эти очереди на протяжении всего своего жизненного цикла. Используя экземпляр объекта MTLCommandQueue, приложение создает один или несколько объектов MTLCommandBuffer для хранения аппаратных команд, которые будут отправлены в объект MTLCommandEncoder. . Чтобы сгенерировать команды, необходимо указать некоторую информацию для объекта MTLCommandEncoder, и это делается путем присоединения различных объектов, прежде чем их можно будет использовать. Для создания объектов ресурсов Metal предоставляет механизм, основанный на структурах данных, называемых дескрипторами. Дескрипторы позволяют приложению указать все необходимые состояния, необходимые для создания определенного объекта ресурса. Та же концепция применима к объектам состояния: API предоставляет дескрипторы, которые приложение должно использовать для их создания. На приведенной выше диаграмме можно увидеть два наиболее часто используемых объекта состояния: объект состояния конвейера рендеринга и объект состояния трафарета глубины, соответственно созданные с помощью MTLRenderPipelineDescriptor и MTLDepthStencilDescriptor; эти два объекта позволяют приложению настраивать различные состояния рендеринга графического процессора. Последний важный объект состояния — это объект Render Pass, который можно создать с помощью MTLRenderPassDescriptor, и этот объект описывает, как приложение будет выводить геометрию.
Причина, по которой Metal разделяет состояние между дескриптором и объектами состояния/ресурса, заключается в том, что после того, как приложение создало все и объявило все различные комбинации состояний, Metal запекает все это в небольшом количестве объектов состояний, уже переведенных в их аппаратный формат с уже шейдерами. компилируется и проходит государственную валидацию; Таким образом, остается только отправить и выполнить команды, что означает, что вызовы отрисовки выполняются очень быстро!
Модель отправки команд
Как уже говорилось, кодировщики команд хранят команды API как аппаратные команды внутри объектов командного буфера. Объекты командного буфера — чрезвычайно легкие объекты, и обычно приложения создают их большое количество при выполнении фрейма; Объекты командного буфера потокобезопасны, поэтому довольно часто их готовят параллельно, используя несколько потоков, и когда они все готовы, отправляют все сразу, чтобы контролировать порядок, в котором они будут выполняться графическим процессором. Что делает Command Encoder таким эффективным, так это тот факт, что они не просто сохраняют некоторую работу, которая позже должна быть использована ЦП перед выполнением, они генерируют команду немедленно без отложенной проверки состояния… это похоже на прямой вызов драйвера графического процессора.
Модель обновления ресурсов
Модель ресурсов в Metal предназначена для поддержки единой системы памяти, что означает, что ЦП и ГП используют одно и то же пространство для хранения данных для обмена данными, что устраняет необходимость выполнения неявных копий, чтобы ГП мог видеть данные, обрабатываемые ЦП, и наоборот. наоборот Metal также обеспечивает поддержку модели автоматической когерентности кэша и гарантирует, что ЦП и ГП будут соблюдать границы выполнения буфера команд при обращении к памяти; единственное, что требуется приложению, — это гарантировать, что работа по рендерингу запланирована таким образом, чтобы ЦП и ГП не выполняли одновременную запись в один и тот же участок памяти. Делегируя планирование и синхронизацию работы по рендерингу приложению, он освобождает API от выполнения любого внутреннего и/или неявного дополнительного блока синхронизации, что значительно повышает производительность.
Говоря о ресурсах, ключевой концепцией, которую следует выделить, является то, что структура ресурса неизменяема, однажды созданная, она не может измениться; это позволяет избежать дорогостоящей проверки ресурсов при их использовании. Конечно, содержимое ресурсов может измениться в любое время, и поскольку модель построена на концепции единой системы, а синхронизация чтения/записи находится под контролем приложения, нет необходимости иметь API блокировки для доступа к данные ресурса; поэтому в Metal для обновления ресурсов требуется получить указатель в памяти и прочитать/записать данные.