Привет! Меня зовут Денис Молодяков, я — тимлид команды графики в KasperskyOS. Мы отвечаем за разработку графического стека полного цикла для микроядерной ОС: от создания низкоуровневых графических драйверов до всего необходимого для фреймворков.
Современная реальность разработки диктует новые правила: удаленка и распределенные команды стали нормой. Моя команда не исключение — ребята базируются в разных регионах. При этом каждому разработчику требуется доступ к аппаратным платформам для сборки, запуска ОС, написания и отладки драйверов. Обеспечение каждого сотрудника полным набором плат сопряжено с серьезными логистическими и финансовыми затратами. В этой статье я хочу рассказать, как мы искали оптимальное решение этой проблемы и почему пришли к использованию методики паравиртуализации.
Какую задачу необходимо решить
Все, кто занимается разработкой ОС или ПО для встраиваемых систем, сталкиваются с необходимостью запуска и отладки на целевом оборудовании. За свою трудовую деятельность я сталкивался с разными проблемами в процессе разработки. Например, «поднимали» ОС на железке и проверяли на минимальном образе, а когда стали делать тяжелые образы с прикладными программами, выяснилось, что ничего не работает. Или перешли на новый загрузчик и вдруг обнаружили, что у нас образ ПО для старого, и снова сидишь медитируешь перед черным экраном. Ну и вишенка — есть несколько железок, на которых надо проверить работоспособность. Например, приложение со сводкой погоды на сегодня, которому, в общем-то, все равно, на каком железе работать. Начинаем шить одну железку — внезапно оказывается, что нужно обновить прошивающую утилиту. Победив эту железку, беремся за вторую — а там логи не работают, нужно выставить загрузчику хитрые опции командной строки, которые описаны где-то в середине мануала мелким шрифтом, и так далее.
Ну а в современных реалиях добавляется еще одна проблема — теперь нужно отправлять этот зоопарк плат еще и в город N специалисту, который будет сопровождать разработку и фиксить баги. И ему тоже все целевые платформы не помешали бы.
А если таких разработчиков пять или семь?
Почесав голову, идем к инфраструктурной команде и говорим, что надо бы добавить еще целевых железок в серверной, чтобы удаленные разработчики могли отлаживаться прямо на CI. А нам отвечают, что идея, конечно, хорошая, но с местами под новое железо в стойках все сложно. И задача обеспечения эффективной разработки уже становится нетривиальной.
Мы у себя столкнулись с похожей ситуацией — когда от продуктовых команд прилетают баги, где по внешним признакам или по результатам экспресс-анализа проблема очевидно не в платформозависимых компонентах — например, на уровне транспорта в Wayland или уровне UI-фреймворка. Проводить «ритуальные обряды» для воспроизведения и отладки на целевом железе в таком случае не очень хочется. Мы решили эту задачу другим путем.
Графический стек KasperskyOS
Сначала скажем несколько слов о нашем графическом стеке. Мы поддерживаем две ветки:
Без аппаратного ускорения. Дисплейные драйверы и бэкенд GUI-фреймворка собственной разработки. Его рассмотрение выходит за рамки настоящей статьи.
С аппаратным ускорением. Весьма условно графический стек можно рассмотреть в вертикальном разрезе, поделив его на «верхнюю» и «нижнюю» части. «Верхняя» часть стека (GUI-фреймворки, Mesa — см. рисунок ниже) портирована из Linux. «Нижняя» часть (настройка аппаратуры), отвечающая за работу с дисплейным контроллером и GPU, реализована по-разному: где-то, где в процессе портирования драйвер можно модифицировать так, чтобы он удовлетворял требованиям безопасности, — делаем порт. Если драйвер очень сложен и модифицировать его дороже, чем написать свой небольшой драйвер, — пишем свой (обычно это касается дисплейных контроллеров).

К слову сказать, «порт» — слишком простое слово, чтобы описать этот процесс. При портировании в драйверах и «верхней» и «нижней» части переделывается все, что касается взаимодействия с ядром Linux или вызывает вопросы с точки зрения безопасности: работа с физической памятью и ее отображением в пользовательское пространство, системные вызовы, шаринг видеопамяти между процессами, работа с прерываниями и так далее. По сути, в драйверах остается лишь чистая логика и работа с регистрами аппаратуры.
Мы рассматривали графические стеки в различных открытых ОС вроде Fuchsia и Android, однако именно Linux-стек продемонстрировал наибольшую зрелость, широкую поддержку бэкендов в сторонних фреймворках и оптимальное соотношение гибкости/производительности.
В рамках этой статьи сосредоточимся на технологии, которая позволила нам отлаживать всю платформонезависимую часть стека, а прикладным командам помогает выполнять работы по созданию UI и отлаживать бизнес-логику.
Анализ платформозависимости и тестового покрытия
Прежде чем выбрать техническое решение, мы провели детальный аудит графического стека. Прежде всего требовалось понять, какая часть стека непосредственно зависит от платформы. В качестве метрики мы взяли количество строчек кода. Метрика максимально грубая и общая, но в этом и ее плюс — она позволяет крупноблочно оценить объемы платформозависимого кода.
Анализ показал, что около 85% кодовой базы не зависит от конкретной аппаратной платформы. Даже при использовании самого объемного драйвера (Intel, ≈ 120 000 строк) лишь 10–13% кода в графическом стеке относятся к работе с регистрами, шейдерными компиляторами и кодировщиками команд для исполнительных ядер видеокарты. Остальная часть — это абстракции, менеджеры памяти, планировщики и платформонезависимые графические примитивы.
На картинке ниже агрегированы основные компоненты графического стека.

На диаграмме все, что окрашено в темно-синий цвет, является платформозависимым.
Анализ тестового набора подтвердил аналогичную картину:
40% тестов — модульные (Unit), проверяющие конкретные подсистемы (memory-менеджер, синхронизацию, фичи драйверов);
60% — интеграционные, запускающие стек целиком от приложения до буферов кадров.
Статистика CI показала у нас около сотни сборок за рабочий день (не считая ночных), то есть больше десятка в час. В каждой сборке для конфигураций с графическим стеком несколько десятков графических тестов. Очевидно, что если все графические тесты выполнять на всех целевых аппаратных платформах, то железо становится узким местом, а масштабирование на нем становится экономически и физически нецелесообразным.
Итого, у нас есть 4/5 части платформонезависимого кода и больше половины графических тестов, которые проверяют не отдельные фичи платформы, а комплексные сценарии. Очевидно, напрашивается решение пересадить как можно больше на что-то виртуальное, что требует конкретных железок под собой, а узкие сценарии проверять уже на аппаратуре.
Решение: паравиртуализация через VirtIO и VirGL
Оптимальным решением стала методика паравиртуализации. Это довольно зрелая технология, хотя на просторах Рунета об опыте ее применения довольно мало информации. Ее суть заключается в осознанном сотрудничестве гостевой ОС и эмулятора: гость знает, что работает в виртуальной среде, эмулятор знает об этом, и обе стороны совместно оптимизируют выполнение задач. В случае KasperskyOS гостем выступает сама ОС, а в роли эмулятора/гипервизора QEMU/KVM.
Данные передаются по стандартизированному интерфейсу — VirtIO. Современная спецификация VirtIO описывает протоколы работы с блочными, сетевыми, звуковыми и графическими устройствами, а также структуры данных, необходимых для реализации интерфейса. Для гостевой ОС VirtIO GPU будет выглядеть как PCI-устройство, где в BAR содержатся MMIO-регионы и адреса так называемых виртуальных очередей — virtqueue. Подробнее про интерфейс можно почитать в спецификации, но принципиально важны три структуры данных, физические адреса которых записываются в MMIO-регион:
таблица дескрипторов (Descriptor Table);
кольцо (кольцевой буфер) доступных буферов (Avail Ring);
кольцо обработанных буферов (Used Ring).
Дескрипторы содержат физические адреса в гостевой памяти, длину, флаги и указатель на следующий элемент, позволяя формировать цепочки буферов. Avail Ring заполняется драйвером гостя и указывает эмулятору, какие данные готовы к обработке. Used Ring заполняется эмулятором и сообщает драйверу, какие буферы уже отработаны и могут быть переиспользованы. То есть концептуально это тот же кольцевой буфер, с указателями, двигаемыми драйвером и эмулируемой железкой, который используется для работы сетевыми картами, видеокартами и прочими устройствами.

«Верхний» уровень стека реализован через VirGL — драйвер, построенный по модели Gallium3D. Он позволяет гостевым приложениям использовать вычислительную мощность хостового GPU без прямого доступа к аппаратуре. Когда приложение вызывает OpenGL-функцию, Gallium-драйвер формирует батч-буфер команд, компилирует шейдеры в промежуточное представление (IR), упаковывает все в DMA-буферы (GEM-объекты) и передает на уровень VirtIO. На стороне хоста VirGLrenderer выполняет обратное декодирование команд и транслирует их в нативные вызовы OpenGL, которые уже исполняются реальным GPU. Результат возвращается в гостевой буфер кадров.
Например, вызов glClear проходит следующий путь: приложение → Gallium-трекер контекста → аппаратно-специфичный pipe-драйвер → кодирование команды в батч-буфер → уровень WinSys (адаптер под платформу) → VirtIO GPU-драйвер → запись в таблицу дескрипторов → сигнал эмулятору → QEMU копирует данные в промежуточный буфер → VirGLrenderer декодирует команду → вызов нативного glClear на хосте → рендеринг → возврат результата. Вся цепочка занимает считанные миллисекунды и полностью прозрачна для разработчика.

То есть в гостевой ОС происходит кодирование команд OpenGL в протокол VIRGL, а на стороне хоста — декодирование и превращение в обычные GL-вызовы.
Для хостового драйвера и видеокарты такая схема выглядит как еще один графический клиент — как калькулятор или экземпляр glmark2, например. Здесь нет ни сложных схем «проброса», ни виртуализации GPU. Вы можете запустить столько экземпляров QEMU с графикой, сколько позволит производительность вашей хостовой видеокарты. Неплохая масштабируемость. А если развернуть это на CI?
Интеграция в CI и масштабируемость
Паравиртуализация была органично вписана в CI-инфраструктуру. Вместо закупки десятков серверных плат мы выделили пул обычных десктопов высокой производительности с дискретными видеокартами. Машины размещены в серверной, не подключены к мониторам, но интегрированы в общую сеть. Агенты помечаются специальной меткой (например, gpu-virtio), позволяющей CI-оркестратору маршрутизировать графические тесты именно на эти ноды.
Разработчик маркирует тест требуемой capability, инфраструктура находит свободного агента, разворачивает образ KasperskyOS в QEMU, запускает тестовый пакет и собирает метрики. На одном десктопе одновременно работает 10–20 виртуальных агентов, что обеспечивает колоссальную масштабируемость: добавление одного физического ПК автоматически увеличивает пропускную способность CI на порядок. При этом нагрузка на основной пул агентов снижается, а время ожидания выполнения тестов сокращается в разы.

Почему не подошли альтернативные подходы?
До внедрения паравиртуализации мы оценили несколько альтернатив:
Программный рендеринг (softpipe, llvmpipe). Softpipe выдавал около 2 FPS на эталонных сценариях. Llvmpipe с KVM-акселератором работал лучше, но не покрывал критически важные части стека: работу с DMA, объекты синхронизации, memory-менеджер. Кроме того, требовался DRM-совместимый дисплейный драйвер для вывода изображения, что усложняло валидацию. Еще можно обратить внимание на встроенные рендереры в UI-фреймворки. Но и там есть подводные камни — например, Qt просто не даст использовать «навороченный» графический эффект без GPU.
DRM-shim (заглушка в Mesa). Позволяет тестировать компиляторы шейдеров и кодировщики, имитируя топологию GPU (слайсы, execution-юниты). Однако визуальная валидация отсутствует: разработчик не видит итоговую картинку, что критично для графического стека.
Прямая виртуализация GPU (SR-IOV, GPU passthrough и прочее). Технологии эффективны, но требуют топ-видеокарт, сложны в настройке, нестабильны на нецелевом железе и экономически нецелесообразны для масштабирования в CI. Passthrough, например, требует двух GPU на хосте, что исключает массовое применение.
Результаты и практическое применение
Внедрение паравиртуализации дало измеримые результаты. На демонстрационных стендах glmark2 показал производительность, сопоставимую с нативным запуском на аналогичном железе. Тестовый запуск Quake 3 стабильно выдавал около 30 FPS при запуске QEMU без KVM, что подтверждает жизнеспособность подхода для реальных нагрузок. Doom3, но уже с KVM, выдавал 30–40 FPS. Игры выбраны не случайно: они, в отличие от синтетических бенчмарков, обеспечивают неравномерное, но максимально приближенное к продакшену распределение нагрузки на стек.
А самое главное — больше не нужно «заводить» очередную железку для отладки прикладной UI-программы, выяснять, какой интерфейс можно использовать в качестве транспорта для GDB, и так далее.
Такое решение может облегчить жизнь следующим заинтересованным лицам:
Командам, отвечающим за графический стек в ОС. Ускорение отладки, устранение зависимости от физических плат, удобная работа из дома.
Продуктовым командам. Быстрое прототипирование бизнес-приложений с аппаратным ускорением.
Инфраструктурным командам. Сокращение затрат на серверное железо, снижение нагрузки на основной CI-пул.
Сообществу. Надеемся, что в скором времени паравиртуализация войдет в KasperskyOS Community Edition, что позволит энтузиастам тестировать графические приложения без закупки специфичных плат.
Преимущества и ограничения
Ключевые плюсы методики очевидны: упрощение цикла разработки, покрытие ≈ 85% кодовой базы без реального железа, экономия на закупке и логистике платформ, линейная масштабируемость CI. Разработчик получает рабочий графический контекст, не отходя от рабочего места, может использовать стандартные тулчейны и не тратить часы на прошивку, загрузчик или настройку транспорта для отладки.
Ограничения также присутствуют.
Платформоспецифичные тесты и финальная валидация на целевом железе остаются необходимыми: паравиртуализация не заменяет, а дополняет реальное тестирование.
Качество рендеринга оценивается принципиально: тонкие особенности сглаживания, блендинга или специфичные артефакты конкретного GPU могут отличаться, так как транслируются через абстракцию VirGL.
Для CI все равно требуется парк десктопов с видеокартами, хотя их стоимость и логистика несопоставимо ниже серверных плат.
Если у кого-то возникнут вопросы по тексту, буду рад ответить на них в комментариях!

