[Перевод] Как написать драйвер GPU в open source без реального оборудования

[Перевод] Как написать драйвер GPU в open source без реального оборудования

Спустя шесть месяцев реверс-инжиниринга новые GPU Arm «Valhall» (Mali-G57, Mali-G78) получили свободные и опенсорсные драйверы Panfrost. Благодаря новому компилятору, патчам драйверов и хакингу ядра эти новые GPU почти готовы к переносу вверх по потоку.

В 2021 году не существовало устройств Valhall, способных запускать основную ветвь Linux. Хотя отсутствие устройств являлось очевидным препятствием для разработки драйверов устройств, нет более подходящего времени для написания драйверов, чем момент, когда оборудование ещё не добралось до конечных пользователей. Разработка и распространение драйверов уровня качества продакшена требует времени, а мы не хотели, чтобы пользователям пришлось полагаться на блобы с закрытыми исходниками. Если разработку не начать до того, как устройство попадёт на прилавки магазинов, то к выпуску готовых открытых драйверов это устройство может уже достигнуть конца срока своей жизни. Но имея преимущество по времени, мы можем выпустить драйверы уже к моменту, когда устройства попадут к конечным пользователям.

В статье я расскажу, как нам это удалось.

Реверс-инжиниринг без рута

Летом прошлого года Collabora приобрела телефон с Android на Mali-G78. В телефоне не было рута, поэтому мы не могли заменить графические драйверы на собственные. Мы могли перевести телефон в режим разработчика, чтобы запускать тестовые приложения с проприетарным графическим драйвером и выполнять инъекции нашего кода с помощью LD_PRELOAD, что позволяло нам исследовать подготовленную проприетарным драйвером графическую память и «пассивно» реверс-инжинирить оборудование. Эта память содержала скомпилированные двоичные файлы шейдеров в наборе команд Valhall, а также структуры данных Valhall, контролирующие такое графическое состояние, как текстуры, смешение и усечение.

«Активный» реверс-инжиниринг тоже был возможен. Мы могли модифицировать скомпилированные шейдеры и структуры данных GPU, что позволяло нам экспериментировать с отдельными частями. Мы могли пойти ещё дальше, собирая собственные шейдеры и структуры данных, проверяя их работу на оборудовании.

В качестве примера этой методики рассмотрим реверс-инжиниринг «дескриптора буфера» Valhall. Эта новая структура данных описывает буфер памяти, доступ к которому выполняется новой командой «load buffer» (LD_BUFFER). Угадав структуру дескриптора буфера и кодировку LD_BUFFER, мы можем создать собственный дескриптор буфера и написать шейдер при помощи LD_BUFFER, чтобы подтвердить правильность нашей догадки и исследовать низкоуровневую семантику.

При реверс-инжиниринге новых структур данных Valhall мы могли ориентироваться на легаси. Хотя Valhall реорганизует свои структуры данных, чтобы снизить излишнюю трату ресурсов драйверов Vulkan, содержимое на битовом уровне напоминает старые GPU Mali. Если мы найдём «контуры» новых структур данных, то сможем заполнить недостающую информацию, выполняя сравнение со старым оборудованием.

В процессе исследования структур данных мы документировали свои находки в формальном XML-описании оборудования. Этот файл имеет тот же формат, что и XML для старых архитектур Mali, которые уже поддерживаются драйвером Panfrost. Так как структуры данных Valhall являются потомками этих старых архитектур, мы можем форкнуть старый XML Mali, что сэкономит нам время на ввод и позволит сохранить согласованность наименований.

После достаточного объёма реверс-инжиниринга мы смогли вставить наш XML в Panfrost, автоматически генерирующий код для упаковки и распаковки структур данных. Благодаря неустанному труду сотрудника Collabora Бориса Брезиллона критически важный для производительности код Panfrost специализируется на этапе компиляции под целевую архитектуру, что позволяет нам добавлять новые архитектуры без дополнительных издержек старого оборудования. Итак, получив файл XML, мы были готовы к написанию драйвера Valhall.

Пишем драйвер без оборудования

Ноябрь 2021 года. Я написала компилятор Valhall. Я провела достаточный для написания драйвера объём реверс-инжиниринга. Но у меня всё ещё не было Linux-оборудования для тестирования кода.

Это стало основным препятствием.

К счастью, я знала, как его обойти.

Мы можем разработать драйвер на любой машине с Linux, не тестируя его на реальном оборудовании. Чтобы реализовать это, необходимо юнит-тестирование. Без оборудования мы не можем выполнять комплексное тестирование, однако юнит-тесты можно запускать на любом оборудовании. В случае компилятора Valhall я написала юнит-тесты для всего, от упаковки команд до оптимизации. Хотя покрытие было неполным, тесты ещё на ранних этапах начали выявлять множество багов.

Однако тут был изъян: юнит-тестирование не говорит нам, верны ли наши ожидания относительно оборудования. Однако, оно может подтвердить, что наш код соответствует нашим ожиданиям. Если наш реверс-инжиниринг будет доскональным, то ожидания окажутся верными.

Но даже в этом случае одного юнит-тестирования было бы недостаточно.

На сцене появляется drm-shim.

drm-shim

Драйверы Mesa, в том числе и Panfrost, могут имитировать тестируемое оборудование при помощи drm-shim — небольшой библиотеки, создающей заглушки системных вызовов, используемых графическими драйверами пользовательского пространства для общения с ядром. Благодаря drm-shim немодифицированные драйверы пользовательского пространства считают, что работают на реальном оборудовании, в том числе и на оборудовании Valhall.

Гуру графики Эмма Анхолт спроектировала drm-shim, чтобы запускать компиляторы Mesa как кросс-компиляторы для использования в continuous integration (CI). Кроме CI drm-shim позволяет тестировать компиляторы на наших машинах для разработки, что может быть значительно быстрее, чем на целевых встроенных устройствах. Но дело не ограничивается компиляторами; мы можем выполнять под drm-shim целые наборы тестов, «кросс-тестируя» код для любого оборудования. Тесты не проходятся, поскольку drm-shim не выполняет рендеринга; это прокладка, а не эмулятор. Однако она позволяет нам исполнять кодовые пути новых драйверов без ограничений реального оборудования.

Так как drm-shim работает на любой машине с Linux, я захотела использовать самую быструю машину с Linux, которая у меня есть: Apple M1. Как ни странно, drm-shim не заработала на моём M1 с Linux, хотя работала на всех остальных компьютерах. Требовалась отладка.

Немного изучив код, я наткнулась на виновный в проблеме фрагмент:

bo->addr = util_vma_heap_alloc(&heap, size, 4096);
mmap(NULL, ..., bo->addr);

Этот код распределяет блок памяти, выровненный по странице, и использует его адрес как смещение в вызове mmap. В моей системе вызов mmap завершается неудачей, поэтому я обратилась к странице man mmap:

offset должно быть кратно размеру страницы, возвращаемому sysconf(_SC_PAGE_SIZE).

mmap в drm-shim работает, потому что размер страницы в Linux равен 4096 байтам (4 КБ)…

Но так бывает не всегда.

Блок управления ввода-вывода памяти Apple использует страницы большего размера, 16384 байта (16 КБ). Следовательно, когда мы запускаем Linux bare metal на платформах Apple, чтобы не усложнять жизнь, мы конфигурируем Linux на использование страниц по 16 КБ. Это значит, что на платформах Apple с Linux sysconf(_SC_PAGE_SIZE) возвращает 16384, поэтому mmap завершается неудачей. Исправить это легко:

bo->addr = util_vma_heap_alloc(&heap, size, sysconf(_SC_PAGE_SIZE));
mmap(NULL, ..., bo->addr);

Благодаря этому drm-shim работает в системах с размером страниц больше 4 КБ, в том числе и на моём M1. Это значит, что я могу с помощью компилятора Valhall компилировать тысячи шейдеров в секунду — гораздо больше, чем на любой системе с GPU Mali. Также я могу запускать Khronos OpenGL ES Conformance Test Suite:

PAN_MESA_DEBUG=valhall,trace LIBGL_DRIVERS_PATH=~/lib/dri/ LD_PRELOAD=~/mesa/build/src/panfrost/drm-shim/libpanfrost_noop_drm_shim.so PAN_GPU_ID=9091 EGL_PLATFORM=surfaceless ./deqp-gles31 --deqp-surface-type=pbuffer --deqp-gl-config-name=rgba8888d24s8ms0 --deqp-surface-width=256 --deqp-surface-height=256'

Подобные длинные команды запускают тесты и создают красиво сформатированные дампы памяти GPU, готовые для изучения вручную. Если дампы похожи на дампы из проприетарного драйвера, есть большая вероятность того, что тесты будут пройдены и на реальном оборудовании.

Общий код

Так как Valhall схож с его предшественниками, благодаря тому, что мы потратили годы на совершенствование Panfrost, нам достаточно модифицировать драйвер только в тех местах, где Valhall вносит существенные изменения.

Например, набор команд Valhall напоминает старый набор команд «Bifrost», поэтому мы можем встроить компилятор Valhall в качестве дополнительного бэкенда в уже имеющийся компилятор Bifrost. Общие проходы компилятора, например, этапы выбора команд и распределения регистров просто работают в Valhall, даже несмотря на то, что разрабатывались и отлаживались они для Bifrost.

После адаптации Panfrost под Valhall мы получили совместимый и производительный готовый драйвер.

… Теоретически.

Реальное оборудование и реальная боль

Я не могла провести тесты на реальном оборудовании Valhall до начала января, когда раздобыла Chromebook с SOC MediaTek MT8192 и соответствующим последовательным кабелем. MT8192 включает в себя GPU Valhall «Mali-G57», совместимый с Mali-G78, реверс-инжинирингом которого я занимаюсь. Поддержка MT8192 в основной ветви ядра минимальна, однако Linux загружается. Благодаря патчам других сотрудников Collabora работает и USB. Этого достаточно для работы на GPU. Да, дисплей не работает, но кому он нужен?

Мы начали с того, что стали обучать Linux обнаруживать GPU. На десктопах определять всё подключенное оборудование операционным системам помогают ACPI и UEFI. Хотя эти стандарты существуют и для Arm, на практике системы Arm требуют дерева устройств, описывающего оборудование: какие комплектующие присутствуют, какие регистры и тактовые генераторы они используют, как они подключены. Мы знаем не очень много о MT8192, но его поддерживает ChromeOS, поэтому у ChromeOS есть полное дерево устройств. Адаптировав это дерево устройств под основную ветку, мы вскоре начали наблюдать признаки жизни:

[  1.942843] panfrost 13000000.gpu: unknown id 0x9093 major 0x0 minor 0x0 status 0x0

Ядро не может идентифицировать подключенный GPU Mali, но это было ожидаемо — в конце концов, оно никогда раньше не видео Mali-G57. Нам нужно добавить отображение из аппаратного ID Mali-G57 в его название, список функций и список аппаратных багов. После этого драйвер загружается.

[  1.942843] panfrost 13000000.gpu: mali-g57 id 0x9093 major 0x0 minor 0x0 status 0x0
[  1.982322] [drm] Initialized panfrost 1.2.0 20180908 for 13000000.gpu on minor 0

Благодаря модулю ядра вниз по потоку, выпущенному компанией Arm, мы знали, какие части Valhall, относящиеся к ядру, обратно совместимы с GPU Mali, выпущенными десяток лет назад. Panfrost поддерживает старое оборудование Mali, поэтому теоретически мы могли прямо сейчас протестировать Mali-G57.

Однако когда дело касается оборудования, теория и практика никогда не согласуются.

Давайте попробуем отправить оборудованию «null job» — простую задачу, которая совершенно ничего не делает:

struct mali_job_descriptor_header job = {
    .job_type = MALI_JOB_TYPE_NULL,
    .job_index = 1
};

Во всей структуре данных задано всего 2 бита. Мы даже можем прописать эту задачу в ядре и передавать её сразу после включения питания оборудования. Так как задача корректна, оборудование выполнит её без проблем.

[   2.094748] panfrost 13000000.gpu: js fault, js=1, status=DATA_INVALID_FAULT, head=0x6087000, tail=0x6087000

Что? Оборудование утверждает, что задача недопустима, несмотря на то, что её допустимость очевидна. Наверно, оборудование считывает из памяти не то, что мы написали.

Этот симптом нам пугающе знаком. Когда мы с сотрудником Collabora Томе Визосо два года назад добавили поддержку Mali-G52, то наблюдали те же симптомы у SOC Amlogic. Виновником оказалась специфичная для Amlogic проблема когерентности кэша. Решение той проблемы здесь неприменимо, поэтому настало время охоты за специфичными багами MediaTek.

Пробираясь через код ChromeOS, я выяснила, что MediaTek реализовала необъяснённое изменение в драйвере GPU, задав один бит, относящийся к тактовому генератору MT8192, чтобы «отключить ACP» и устранить проблемы с шиной. Это изменение является воплощением «решающего все проблемы» магического бита, о котором ходили только слухи; это настоящий кошмар для реверс-инженера.

… Однако при задании этого бита в нашем ядре null job выполняется успешно.

… Что-что?

Оказывается, ACP — это «Accelerator Coherency Port», отвечающий за управление когерентностью кэша между CPU и GPU. Очевидно, ACP не должен быть включён в MT8192, но из-за аппаратного бага случайно был включён. Чтобы обойти эту проблему, ядро должно установить этот бит, чтобы отключить ACP.

Что?

Мы можем создать ту же null job из пользовательского пространства. Для оборудования пространство ядра и пользовательское пространство одинаковы, поэтому это должно сработать.

Но не сработало.

Таймаут задачи истекает до завершения. Изучая лог ядра, мы обратили внимание на более ранний таймаут, ожидающий пробуждения GPU после сброса.

Добавив в ядро множество printk, мы всё-таки выяснили, что при запуске Linux GPU отключен и что мы ничего не можем сделать для включения его питания. Неудивительно, что всё завершается таймаутом.

Решить эту проблему нам помог мастер по работе с ядром Хейко Стубнер. Хейко предположил, что Linux может отключать питание GPU. Для экономии энергии Linux отключает неиспользуемые тактовые генераторы и домены питания. Если Linux не знает, что тактовый генератор или домен питания используется GPU, то непреднамеренно отключит GPU.

Для отладки мы можем отключить этот механизм, задав аргументы ядра clk_ignore_unused pd_ignore_unused. Благодаря этому наши тесты пользовательского пространства начинают работать.

Иногда простейшее решение находится прямо перед глазами.

Какова первопричина? У MediaTek есть сложная иерархия тактовых генераторов и доменов питания, и мы упустили некоторые из них в нашем дереве устройств. Для правильного решения проблемы нам нужно дополнить код, чтобы Linux узнал о дополнительных тактовых генераторах и доменах питания.

Как бы то ни было, теперь мы можем тестировать наш драйвер на реальном оборудовании. Начало было непростым: первая отправленная задача вернула Data Invalid Fault. Экспериментируя, мы выяснили, что Valhall требуется бОльшая согласованность указателей структур данных, чем в Bifrost. Повышение согласованности распределения устраняет сбои, а повторное снижение позволяет нам определить наименьшую требуемую согласованность. Эта информация доступна только когда мы запускаем код на оборудовании, но недоступна при изучении оборудования in vitro. Реверс-инжиниринг и разработку драйверов лучше выполнять вместе.

Наконец-то успех

После этих исправлений мы наконец получили первый пройденный тест, запущенный на реальном оборудовании, со структурами данных, подготовленными нашим опенсорсным драйвером Mesa и шейдерами, скомпилированными нашим компилятором Valhall. Ура!

После получения оборудования и последовательного кабеля мне потребовалось всего несколько дней, чтобы провести сотни тестов на новой архитектуре. Многие месяцы умозрительной разработки драйвера полностью оправдали себя.

Похоже, мы наконец вовремя выпустили драйверы Valhall для конечных пользователей.

 

Источник

Читайте также