Оглавление

Вступление

Хабр, привет!

Я - Леонид Забурунов, инженер-программист компании “БФГ” - стартап по разработке системы виртуализации - в отделе разработки систем виртуализации графических ресурсов.

В рамках работы над доставкой рабочего стола для своей системы виртуализации мы наломали немало дров в самых разных направлениях. Одно из таких направлений - протокол SPICE как интереснейший объект для экспериментов. Этой серией статьей мы подводим своеобразный итог своим исследованиям прошлого и делимся с сообществом своими знаниями, частично - наработками. Своего рода дневники разработчиков.

Пример “кода”: документация к функции на полтора экрана, за кадром - реализация ещё на четыре экрана. Высота монитора - 1080 пикселей. Обхохочешься!
Пример “кода”: документация к функции на полтора экрана, за кадром - реализация ещё на четыре экрана. Высота монитора - 1080 пикселей. Обхохочешься!

Мы разберём самую объёмную часть протокола SPICE - доставку изображения удалённого рабочего стола. Документации и любой другой информации для столь объёмного инженерного решения катастрофически мало, а исходный код порой отбирает волю к жизни заставляет хорошенько почесаться - всё как мы любим!

Наша цель - научить виртуальные машины на ОС Windows, запущенные под SPICE-сервером, транслировать картинку в режиме стриминга. Хотим мы этого добиться потому, что встроенный QXL-адаптер не подходит для высокой нагрузки с частым обновлением экрана (это будет показано и обосновано далее). Необходимым условием для обозначенного предприятия является понимание предметной области, а потому план следующий:

  1. Разобраться с устройством протокола SPICE в части графики. Это мы делаем прямо сейчас;

  2. Разобраться с тем, какими “проводами” всё это хозяйство подключается к ОС Windows. Это мы сделаем в следующей части цикла;

  3. Разобраться с тем, что мы можем сделать для более плавной картинки и понижения битрейта - читай для более комфортной работы. Это и все следующие пункты мы будем делать ещё дальше, начиная с третьей части;

  4. Написать анонсированный стриминговый агент. Точнее, его минимальную реализацию.

  5. Зафиксировать итоги работы и обсудить, что можно было бы сделать на следующих этапах для приближения к стадии Enterprise-решения.

Что ж, начинаем погружение в бездну…

А зачем нам вообще разбираться в SPICE и стриминге?

Забегу далеко вперёд специально для тех, кто глубоко в теме и хочет сперва ответить на вопрос, ради чего ковыряться палкой в сабже и устраивать сыр-бор.

Виртуальная машина после работы в конфигураторе Virtual Machine Manager
Виртуальная машина после работы в конфигураторе Virtual Machine Manager

Недавно я в личных целях развернул локальную виртуальную машину на Windows 10. Настроил всё через virt-manager, подключился через remote-viewer. Это стандартный сетап для виртуализации со SPICE: virt-manager создаёт libvirt-конфиг для запуска машины через QEMU (с libspice-server.so), а remote-viewer вызывает spice-gtk - каноническую реализацию SPICE-клиента - как виджет из библиотеки libspice-gtk-3.0.so, а вокруг дорисовывает минимальный UI.

Из праздного интереса попробовал простейшие задачи: порисовать в Paint, поработать в PowerShell, помедитировать в реестре. Пока исполнял перечисленные экзерсисы, понадобилось зайти на ютуб и открыть туториал. Открываю видео и вижу чудесную картину:

Попытка посмотреть видео через SPICE-клиент
Попытка посмотреть видео через SPICE-клиент

Ещё раз. Настроил всё по инструкции, подключился стандартным клиентом - и не работает видео. А не работает оно потому, что клиент дропает все фреймы стрима - это я позже узнал в логах. И дело тут не в видео: скролл терминала приводил к тому же результату. Оно просто не работает. А если видеокодек переставить с MJPEG на H.264, так и вообще на старте видео ВМ крашится. Шеф, всё пропало.

Для чистоты эксперимента

Отмечу, что попытка повторить всё то же самое на удалённом сервере обернулась провалом - видео заработало. Перепроверил несколько раз и на локалхосте, и на удалённом сервере - результат в обоих случаях неизменен. Как бы то ни было, разве так дело делается?..

В части стриминга SPICE продвинулся скудно - при том, что всё необходимое для работы в протоколе уже имеется, только допиливай. Ну а про часть с QXL я буду рассказывать в этой и следующих статьях - там всё печально. SPICE оказался в сложной ситуации и стал забытой технологией древности. С одной стороны притесняют корпоративные протоколы, которые ушли далеко вперёд: Citrix ICA/HDX, VMWare PCoIP/Blast, Microsoft RDP, AnyDesk, ParSec, другие. С другой подкрались альтернативные открытые протоколы: VNC, FreeRDP, X2GO, X11 Forwarding (да, я причислил сюда и это; да, спорно), другие. SPICE - хороший боевой протокол со своей областью применимости, в репозиториях есть множество интересных инженерных решений - и всё это оказалось перечёркнуто нежеланием Red Hat развивать проект дальше. Оно, конечно, во многом обосновано, но в своё время я посчитал это незаслуженным и пошёл разбираться.

Ну и, в конце концов, как ещё развиваться, если не идти в том направлении, где поля непаханные?

Доставка рабочего стола средствами SPICE: задумка

Протокол SPICE - тема очень большая. Поскольку серверная часть работает в связке с QEMU/KVM, а клиентская написана на GTK+ 3.0 (экосистема GNOME), мы сразу же затрагиваем очень большую часть мира Linux.

Разве только Linux?

На самом деле, QEMU и SPICE существуют также в сборках для Windows и macOS, но этой темы я касаться не буду. Я такие упражнения не проводил, да и в целом на этих ОС есть собственные решения для виртуализации, а потому практического смысла в таких исследованиях я не нахожу.

Я буду очень ёмко описывать те части протокола, которые имеют отношение к теме - настолько ёмко, насколько нахожу возможным. Историю разработки пропущу - как и общие моменты архитектуры. Если вам интересен SPICE как таковой, можно изучить оригинальную документацию протокола: она не блещет полнотой и (местами) актуальностью, но то что есть - написано качественно. Подробные разборы протокола - не только в части графики - существуют и на Хабре, см. серию статей. Уверен, что есть много других заслуживающих внимание материалов. Ну и приглашаю в комментарии, само собой.

Итак, QEMU/KVM. Поддержка SPICE реализована в QEMU и переключается опцией сборки. В качестве аргументов запуска QEMU можно указать множество параметров, которые влияют на работу SPICE-сервера и на клиентскую сессию.

Когда виртуальная машина стартует, инициализация SPICE и выставление настроек происходит по методам из специального интерфейса spice-server.h: тут тебе и настройки TLS, и кодеки, и все остальные доступные настройки. Если в ВМ выбран видеоадаптер QXL, то настраивается и он (в соответствии с версией протокола адаптера). “Если” - потому что можно выбрать и другой, в том числе дубовый фреймбуффер VGA, который превратит SPICE в ещё одну реализацию VNC.

Интерфейс?

Уход в жаргон ООП и окликание заголовка интерфейсом - действие осознанное. На мой взгляд, так лучше раскрывается функциональное назначение. Если Вас это смущает, то подчёркиваю отдельно: это самый обычный C-заголовок без каких-либо магических функций.

QXL - это паравиртуальный адаптер. Слово “паравиртуальный” можно определять несколько по-разному, я же дам своё видение, которое не замкнуто на самом QXL: паравиртуальный адаптер - это устройство, отображающееся в виртуальной машине как настоящее “железное” (с поддержкой актуальных API), но на деле являющееся интерфейсным слоем, передающим эти команды далее в гипервизор для исполнения на реальном железе. В случае с QXL это означает следующую задумку: вместо того, чтобы исполнять графические команды GDI/X11/OpenGL/choose-your-fighter софтварным рендерингом (по определению медленным и унылым), эти команды просто прокидываются дальше - туда, где есть рендеринг аппаратный, - а затем оттуда же прилетает результат. Эта мысль выгодно отличает SPICE от более примитивных подходов к проблеме в стиле remote framebuffer (так, например, устроен VNC), где по задумке по каналу гуляют только пиксели (сжатые через кодек, конечно).

Графическая архитектура SPICE при использовании QXL-адаптера (из официальной документации)
Графическая архитектура SPICE при использовании QXL-адаптера (из официальной документации)

Ладно, к QXL мы ещё вернёмся. Сейчас я хочу остановиться на доставке рабочего стола в принципе; осторожно погрузимся в проблематику протоколов удалённого доступа…

Потоки данных при удалённой работе

В процессе работы с виртуальной машиной вы совершаете действия, которые приводят к изменению на дисплее:

  • Передвинули мышь - появилась всплывающая подсказка. Это должно отобразиться на экране.

  • Нажали кнопку мыши - окно перешло в состояние фокуса и сменило цвет рамки. Это должно отобразиться на экране.

  • Нажали кнопку на клавиатуре - приложение отреагировало или же запустилось какое-либо системное действие. Это должно…что? Правильно - отобразиться на экране!

В случае с физическим компьютером эти изменения внутри вашей ОС посчитались и из неё же ушли на видеовыход монитора. А в рассматриваемом нами случае всё иначе: результат должен отобразиться хрен знает где на удалённом дисплее, соединённом с дата-центром по каналу связи произвольного качества - и вполне возможно, что именно канал связи будет главным препятствием для работы.

Вы когда-нибудь задумывались о том, какое количество данных проходит внутри Вашего рабочего компьютера, например, при просмотре видео? Какую пропускную способность мы бы заняли, если бы нам потребовалось копировать данные с экрана?

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

Ну, давайте считать - и считать для плохого случая, потому что оптимистичные сценарии себя быстро исчерпывают. Вот у вас FullHD монитор - это 1920х1080, или что-то около 2 миллионов пикселей. Современные дисплеи имеют де-факто стандарт глубины цвета 8-бит, картинка отдаётся в формате RGBA, по 8 бит на канал, 32 бита на пиксель. Или 4 байта. 2 миллиона пикселей на 4 байта - 8 мегабайт. Каждый кадр. Монитор 60 Гц - и получаем 480 мегабайт в секунду.

RGBA? Прозрачный экран? Мы что, AR-очки обсуждаем?

Конечно, мониторы непрозрачные и лицо соседа по опенспейсу через него не увидишь (если у вас нет раздвоения личности). Но несмотря на это, современные графические системы как правило отдают изображения в формате именно R8G8B8A8: красный, зелёный, синий, альфа каналы - по 8 бит на каждый.

С одной стороны, очевидна избыточность данных: 25% можно смело выбросить. С другой, снимается множество проблем совместимости data layout с различными библиотеками обработки изображений: там зачастую альфа-канал учитывается, а вот форматы типа R8G8B8 встречаются далеко не всегда. Ну и имеются всякие соображения на тему выравнивания данных и удобства использования в векторных операциях и/или на SIMD-процессорах.

По итогу, проще смириться с избыточностью данных, проходящих через системную шину, чем нарываться на несовместимость с внешним миром. Поэтому обычно альфа-канал отдаётся даже для заведомо непрозрачных изображений и заполняется значениями 255 (или 1.0, если мы говорим не о целочисленном представлении). Такова практика.

Таких цифр на реальных сетевых каналах вы не найдёте, здесь вам не тут локальная PCIE-шина. И это важно усвоить тем, кто не привык погружаться в детали удалённого доступа: интернет - слабое звено, поэтому львиную долю эффективности протокола составляют эффективность передачи данных и минимизация задержек.

Иллюстрация работы протокола доставки рабочего стола и его кодеков (Gemini)
Иллюстрация работы протокола доставки рабочего стола и его кодеков (Gemini)

Способов сжать данные, передаваемые в виде изображения или видео, довольно много. Именно с видео всё достаточно понятно: можно взять H264 и всё. Я сейчас не хочу сказать, что кроме H264 не существует кодеков. Я к тому, что технология сжатия видео в сущности одна, а выбираем мы зачастую не технологию, а тип лицензирования. В любом современном видеокодеке активно используются энтропийное кодирование, межкадровую разницу, манипуляции с цветовым представлением и другие приёмы. Если интересно подробнее - отсылаю к хорошей вводной статье про H264.

А вот с изолированными изображениями интереснее. Разницу между соседними кадрами никто не учитывает, однако разные типы данных на дисплее требуют разных подходов. Для отображения текста важнее чёткость символов, для отображения фото - визуальная целостность. Во многих случаях выигрышным оказывается комбинированный подход. Например, RDP использует несколько кодеков под разные задачи. Кроме того, в зависимости от ситуации можно использовать сжатие без потерь (lossless) или же с потерями (lossy). Конечно, хочется видеть идеальную картинку, но в реальных условиях (и уж тем более на слабых каналах) это неосуществимо.

Текстовые данные на экране, как правило, очень хорошо поддаются сжатию - буквы одного цвета, фон другого. И специализированные кодеки этим пользуются как предварительным знанием - картинка имеет небогатую цветовую палитру, никаких преобразований Фурье нам не нужно, только индексы на пикселях расставь. Это по сути архивация данных через хэш-таблицы. Если есть сложности с пониманием этого момента - посмотрите на широкоизвестный формат PNG. Разумеется, самые крутые кодеки используют и другие приёмы.

Но UI-ориентирванный кодек - это быстро по скорости, но прожорливо по пропускной способности сети. Это не недостаток, это закон физики: UI по свойствам изображения имеет мало общего с насыщенной иллюстрацией. В последнем случае визуальной резкостью приходится пожертвовать, потому что тогда изображение экрана рискует занять существенную часть канала и стать источником лагов.

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

Ну и кодеки для видео. По большому счёту, это развитие той же идеи, только с добавлением предварительного знания о том, что дельта между соседними изображениями будет невысокой.

Когда вы качаете с торрента двух-трёхчасовой экшн-фильм в H265, он весит не 200 гигабайт, а 5-6: техники работы с межкадровым движением в наше время отрабатывают очень хорошо. Но это потому, что фильм обычно идёт в 24 или 30 кадров в секунду - если вы, конечно, не Илья Найшуллер. Если вы попробуете взять тот же фильм (или, например, фотосессию) и в случайном порядке переставить его кадры, то результат будет весить уже во много раз больше. Не говоря уж о качестве содержания...

Всё это ни разу не магия, а выверенное применение инженерных решений. Благослови прогресс.

Сжатие данных в протоколе SPICE

Иллюстрация работы разных типов кодеков в протоколе SPICE (Gemini)
Иллюстрация работы разных типов кодеков в протоколе SPICE (Gemini)

SPICE поступает примерно так же. Если говорить про lossless, то выбор богатый: есть самописный кодек QUIC (не путать с одноимённым сетевым протоколом от Google!), который может замылить текст, но хорошо справляется с фото на экране; есть сжатие через самописную реализацию LZSS, через самописный LZ с глобальным словарём или же через LZ4 (на удивление, не самописный). Технологий здесь было опробовано массово, а с учётом того, что написано всё это было лет 20-25 назад, когда люди нейросетей не нюхали и дубиной размахивали, самостоятельность реализации не удивляет. Любой из этих кодеков можно выбрать на старте виртуальной машины, а для сложных сценариев есть комбинированный подход - автоматическое переключение между QUIC и LZ (или GLZ), производимое на основе эвристического анализа свойств изображения на дисплее.

Это я всё говорил про сжатие без потерь. Для сжатия c потерями используется старый-добрый JPEG. В документации это обозначено как WAN optimization. По задумке авторов (надо сказать, весьма здравой), lossless-сжатия будет достаточно для локальных сетей, но при использовании в сетях на большие расстояния потребуются дополнительные ухищрения. С учётом того, насколько жаден в вопросах гарантии доставки сетевой протокол TCP, который используется в SPICE безальтернативно, на издержки транспортного слоя может уйти слишком много ресурса - и без дополнительного сжатия трафика нам не обойтись.

Про дополнительное сжатие трафика

Это относится, кстати сказать, не только к дисплею. Почти каждый SPICE-канал имеет опцию сжатия трафика через LZ4. Исключение здесь, пожалуй, одно - звуковые данные, для которых используется Opus.

Вообще говоря, оптимизация под плохие каналы связи - это тема не на одну диссертацию. Если Вы когда-либо видели, каким образом Citrix умудряется работать на канале со скоростью меньше мегабита, вопросов у вас больше не останется - только челюсть подбирать. И дело там не только в JPEG, в ход пойдут все доступные средства. Поговоривают, что ранние версии Citrix даже подменяли виндовые DLL-библиотеки…

Про оптимизацию доставки рабочего стола в слабых сетях

Речь, конечно, не только о кодеках. Возможности любого кодека не беграничны: чем больше данных на входа, тем больше данных на выходе. Против физики не попрёшь. Что можно сделать помимо улучшения кодека, так это сократить объём данных на входе. Для общего развития я перечислю лишь некоторые из приёмов, которые мне известны:

  1. Можно понизить глубину цвета дисплея. А то и вообще перейти в ЧБ;

  2. Можно заменить обои рабочего стола на монохромный фон (обычно чёрный). Если у вас есть полупрозрачные окна или же вы часто переключаетесь между приложением и рабочим столом, это сильно сократит объём передаваемой информации. Кстати сказать, дело не только в обоях рабочего стола: любая навороченная визуальная тема в приложении приведёт к таким же последствиям.

  3. Можно отключить в гостевой ОС сглаживания шрифтов. Буквы и засечки станут, конечно, более резкими, зато уйдёт весь полупрозрачный ореол, превращающий с помощью операции alpha blend пиксели монотонного фона в разноцветные. (Если интересно подробнее, почитайте про растеризацию в 3D, там принцип такой же).

  4. Можно заставить среду рабочего стола вести себя более аскетично, отключив системные анимации. В современном Windows, изобилующем всякими разноцветными гирляндами, это как никогда актуально. Ровно как и для оболочки GNOME.

  5. В момент перетаскивания окна можно не рисовать его промежуточные положения, а дождаться, пока вы отпустите курсор.

Как и всегда, целая наука! Обмолвлюсь, что чем больше версия Windows, тем меньше это помогает - и дело в архитектуре DWM. К этому вернёмся в следующих статьях.

Но вернёмся к препарированному SPICE и к JPEG. Управляется он с помощью отдельного параметра QEMU по следующей схеме:

  • Можно отключить совсем (off);

  • Можно держать включённым всегда (always);

  • Можно настроить на принятие решения об активации непосредственно во время пользовательской сессии (auto). Звучит заманчиво, но работает очень просто: если замер скорости на старте сессии показал меньше 10 Мбит/с, то включаем. С постоянным качеством. И всё, дальше не дёргаемся. Сильно лучше двух предыдущих вариантов, но для энтерпрайза никуда не годится. В этом отношении SPICE сильно недоработан, огромный потенциал во многом не реализован.

А ещё там для тех же целей добавлен zlib… Ладно, пора остановиться.

Для молодых и дерзких

Если вы в рамках профессионального развития хотите поупражняться в реализации фичей для сложной программной системы (коей SPICE безусловно является), я даю вам три идеи:

  1. Добавить в SPICE сжатие с помощью LZAV или zstd. Это кодеки для быстрого сжатия без потерь, и они отлично впишутся вместо устаревших кодеков, которые представлены в протоколе сейчас.

  2. Добавить в SPICE кодек JPEG-XL. Это кодек сжатия с потерями, современная альтернатива обычному JPEG, который в протоколе уже представлен.

  3. Добавить в SPICE переменное качество для JPEG-энкодера. Логика простая: чем больше мы потребляем данных, тем ниже качество картинки.


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

Заинтересовались самой идеей добавить что-то своё, но не нравятся конкретные варианты? То же самое, найдите (или найдём вместе) что-то, что придётся по душе.

Короче говоря, количество и объём инженерных решений, использованных для того, чтобы можно было лампово сидеть за удалённым рабочим столом в SPICE-клиенте, неподготовленного человека могут поразить. Но и это ещё не всё…

Графические команды QXL

А вот теперь вернёмся конкретно к QXL. Большую часть предыдущих разделов я посвятил работе с готовыми битмапами. Ещё предусмотрен вариант, где вместо битмапы посылается сырая графическая команда, и битмапа рендерится сразу на клиенте.

Что, только на клиенте?

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

На самом деле, SPICE хранит у себя на сервере зеркальную копию содержимого экрана. Да, прямо в синхронном режиме обновляет состояние. Работает это с помощью управляющих команд QXL, а используется для дополнительной синхронизации состояния с клиентом путём досылки областей экрана.

Разбор команд, как и назначение всего этого механизма позволю себе опустить. Если я начну рассказывать обстоятельно ещё и про такие второстепенные моменты, то, боюсь, цикл статей превратится в учебник для студентов. Мне кажется, оно того не стоит, но если у вас другое мнение - сигнализируйте в комментариях.

Из того, что мы успели обсудить, можно сделать вывод, что SPICE+QXL - это удалённый графический сервер! Да, попахивает X11 Forwarding или VirtualGL. Это не уникальная идея, но проверенная и гарантированно работающая: вместо того, чтобы присылать пиксели, я лучше расскажу, как эти пиксели получить. В межчеловеческой коммуникации такой подход не годится (эйчары комиссуют за плохие soft skills), зато для межмашинной - очень даже!

Вот какие команды мы обнаруживаем в протоколе:

enum {
  QXL_DRAW_NOP,
  QXL_DRAW_FILL,
  QXL_DRAW_OPAQUE,
  QXL_DRAW_COPY,
  QXL_COPY_BITS,
  QXL_DRAW_BLEND,
  QXL_DRAW_BLACKNESS,
  QXL_DRAW_WHITENESS,
  QXL_DRAW_INVERS,
  QXL_DRAW_ROP3,
  QXL_DRAW_STROKE,
  QXL_DRAW_TEXT,
  QXL_DRAW_TRANSPARENT,
  QXL_DRAW_ALPHA_BLEND,
  QXL_DRAW_COMPOSITE
};

Пойдём по порядку:

  1. QXL_DRAW_NOP - обычный no operation. Скорее как служебное значение

  2. QXL_DRAW_FILL - заливка (одним цветом, градиентом и т. д.). Параметры - область и кисть с параметрами заливки.

  3. QXL_DRAW_OPAQUE - применение битмапы с модификацией. Параметры - битмапа, область (в т. ч. можно изменить масштаб), тип логической операции и кисть как второй операнд.

  4. QXL_DRAW_COPY - применение битмапы. Параметры - битмапа и область.

  5. QXL_COPY_BITS - перемещение области уже отрендеренной битмапы (например, для скролла). Параметры - предыдущая область, новая область.

  6. QXL_DRAW_BLEND - наложение битмапы на текущее состояние. Параметры - битмапа, область и тип операции.

  7. QXL_DRAW_BLACKNESS - заливка области чёрным цветом. Параметры - область.

  8. QXL_DRAW_WHITENESS - то же самое, но белым цветом.

  9. QXL_DRAW_INVERS - Инверсия всех пикселей (читай - логическая операция “НЕ”). Параметры - область.

  10. QXL_DRAW_ROP3 - смешивание битмапы с модификацией (что-то вроде OPAQUE + COPY). Параметры - Битмапа, область, тип тернарной логической операции и кисть как третий операнд.

  11. QXL_DRAW_STROKE - отрисовка контура. Параметры - контур, кисть с параметрами заливки, тип логической операции.

  12. QXL_DRAW_TEXT - вывод текста. Параметры - строка, область, две кисти с параметрами заливки (текст и фон), типы логических операций для каждой кисти.

  13. QXL_DRAW_TRANSPARENT - применение битмапы с фильтрацией пикселей по значению альфа-канала. Параметры - битмапа, область, значение для фильтрации.

  14. QXL_DRAW_ALPHA_BLEND - наложение битмапы на текущее состояние. Параметры - битмапа, область

  15. QXL_DRAW_COMPOSITE - наложение битмапы c маской и матрицей трансформации. Параметры - битмапа, положение стартового пикселя (0; 0), маска, матрица.

Про QXL_DRAW_ALPHA_BLEND

Операция alpha blend для полупрозрачных поверхностей довольно распространена в графических конвейерах, в том числе современных аппаратных. Для последних от операций с альфа-каналом рекомендуют отказываться в силу повышенной - в сравнении с “непрозрачными” операциями - сложности расчёта, но это отдельная тема.

В документации SPICE дана формула:

color' = (source_color * alpha) / 255;
alpha' = (source_alpha * alpha) / 255;
new_color = color' + ((255 - alpha' ) * destination_color) / 255;

Про QXL_DRAW_COMPOSITE

На этой операции можно хоть игровой 2D-движок написать. Матрица трансформации - 3х3, может описывать смещение (translation), вращение (rotation), масштабирование (scale). Всё вместе - TRS-матрица, важная часть компьютерной графики как предметной области в целом. Если интересно подробнее, отсылаю к статьям из цикла OpenGL-Tutorial (на английском и на русском) либо из цикла LearnOpenGL (на английском - раз и два, на русском - раз и два).

Выглядит как руководство к графическому редактору :) И так и есть!

Я много раз сказал про логические операции. Они определены в протоколе вот так:

typedef enum SpiceRopd {
  SPICE_ROPD_INVERS_SRC = (1 << 0),
  SPICE_ROPD_INVERS_BRUSH = (1 << 1),
  SPICE_ROPD_INVERS_DEST = (1 << 2),
  SPICE_ROPD_OP_PUT = (1 << 3), // Считай NOP - источник без изменений
  SPICE_ROPD_OP_OR = (1 << 4),
  SPICE_ROPD_OP_AND = (1 << 5),
  SPICE_ROPD_OP_XOR = (1 << 6),
  SPICE_ROPD_OP_BLACKNESS = (1 << 7),
  SPICE_ROPD_OP_WHITENESS = (1 << 8),
  SPICE_ROPD_OP_INVERS = (1 << 9),
  SPICE_ROPD_INVERS_RES = (1 << 10),
  SPICE_ROPD_MASK = 0x7ff
} SpiceRopd;

Здесь объяснять подробно смысла не вижу. Загляните в учебник по схемотехнике ;)

Описанный набор команд достаточно полный, чтобы покрыть много типовых ситуаций, сэкономив время и пропускную способность на передачу готовых данных. И это должно позволить использовать SPICE в жёстких условиях со слабыми и/или нестабильными каналами. Должно…

Реализация команд в QXL-драйверах

Красиво стелит, как говорится. Но вот суровая реальность: потенциал команд QXL сегодня не используется. Совсем. SPICE - в части работы с дисплеем - выродился в обычный remote framebuffer а-ля VNC.

Объясняю. Для того, чтобы вся магия заработала, нужен QXL-драйвер. Драйвер одним концом воткнут в графическую подсистему гостевой ОС, другим - в SPICE-сервер. Он перехватывает действия гостевой ОС и транслирует на язык, понятный SPICE-серверу (и заодно SPICE-клиенту).

Иллюстрация работы QXL-драйвера в виртуальной машине (Gemini)
Иллюстрация работы QXL-драйвера в виртуальной машине (Gemini)

В настоящий момент этих драйверов существует три:

  1. qxl - для устаревшей графической модели Windows XPDM (о ней будет рассказано в следующей серии);

  2. qxl-wddm-dod - для актуальной графической модели Windows WDDM (о ней тоже будет рассказано в следующей серии);

  3. xf86-video-qxl - для X-сессий в Linux.

Чтобы не погружать вас в детали реализации, я срежу углы и поступлю следующим образом: отыщу все использования перечисления QXL_DRAW с помощью команды grep, после чего объясню, что это означает.

Начнём с конца - с драйвера для X11:

# Ищем использование графических команд QXL в драйвере для X11
zaburunovleonid@fedora:~/develop$ grep -R "QXL_DRAW_" --include="*.c*" ./xf86-video-qxl/
./xf86-video-qxl/src/qxl_mem.c:    else if (is_drawable && drawable->type == QXL_DRAW_COPY)
./xf86-video-qxl/src/qxl_mem.c:    else if (is_drawable && drawable->type == QXL_DRAW_COMPOSITE)
./xf86-video-qxl/src/qxl_surface.c:    drawable_bo = make_drawable (qxl, surf, QXL_DRAW_FILL, rect);
./xf86-video-qxl/src/qxl_surface.c:    drawable_bo = make_drawable (qxl, surface, QXL_DRAW_COPY, &rect);
./xf86-video-qxl/src/qxl_surface.c:    drawable_bo = make_drawable (qxl, qxl->primary, QXL_DRAW_COPY, &rect);
./xf86-video-qxl/src/qxl_surface.c:     drawable_bo = make_drawable (qxl, dest, QXL_DRAW_COPY, &qrect);
./xf86-video-qxl/src/qxl_surface.c:    drawable_bo = make_drawable (qxl, dest, QXL_DRAW_COMPOSITE, &rect);
./xf86-video-qxl/src/qxl_surface.c:    drawable_bo = make_drawable (qxl, dest, QXL_DRAW_COPY, &rect);

QXL_DRAW_COPY, QXL_DRAW_FILL - базовый минимум. QXL_DRAW_COMPOSITE - уже поинтереснее. Про QXL_COPY_BITS тоже не забываем:

zaburunovleonid@fedora:~/develop$ grep -R "QXL_COPY_BITS" --include="*.c" ./xf86-video-qxl/
./xf86-video-qxl/src/qxl_surface.c:	drawable_bo = make_drawable (qxl, dest, QXL_COPY_BITS, &qrect);

Что мы видим? Если не считать QXL_DRAW_COMPOSITE, то драйвер поддерживает минимальный набор команд - копирование области кадрового буфера и заливка области одним цветом. Не густо. На серьёзные оптимизации пропускной способности рассчитывать не приходится.

Теперь переходим к современному драйверу для Windows:

# Ищем использование графических команд QXL в драйвере WDDM
# (устанавливается по умолчанию с современными версиями virtio-guest-tools)
zaburunovleonid@fedora:~/develop$ grep -R "QXL_DRAW_" --include="*.c*" ./qxl-wddm-dod/*
./qxl-wddm-dod/qxldod/QxlDod.cpp:    if (!(drawable = Drawable(QXL_DRAW_COPY, pRects, NULL, 0))) {
./qxl-wddm-dod/qxldod/QxlDod.cpp:    if (!(drawable = Drawable(QXL_DRAW_FILL, &Rect, NULL, 0)))
# И теперь QXL_COPY_BITS
zaburunovleonid@fedora:~/develop$ grep -R "QXL_COPY_" --include="*.c*" ./qxl-wddm-dod/
./qxl-wddm-dod/qxldod/QxlDod.cpp:    if (!(drawable = Drawable(QXL_COPY_BITS, &rect, NULL, 0))) {

Нужно ещё раз объяснять? Можно сказать, что драйвер не умеет ничего кроме работы в режиме Remote Framebuffer.

Ну и наконец сведём олдскулы и проверим устаревший драйвер для Windows:

# Ищем использование графических команд QXL в драйвере XPDM
# (поддержка прекращена начиная с Windows 8)
zaburunovleonid@fedora:~/develop$ grep -R "QXL_DRAW_" --include="*.c*" ./qxl/*
./qxl/xddm/display/driver.c:    if (!(drawable = Drawable(pdev, QXL_DRAW_STROKE, &area, clip, GetSurfaceId(surf)))) {
./qxl/xddm/display/rop.c:    if (!(drawable = Drawable(pdev, QXL_DRAW_FILL, area, clip, surface_id))) {
./qxl/xddm/display/rop.c:    if (!(drawable = Drawable(pdev, QXL_DRAW_OPAQUE, area, clip, surface_id))) {
./qxl/xddm/display/rop.c:    if (!(drawable = Drawable(pdev, QXL_DRAW_COPY, &clip_area, NULL, surface_id))) {
./qxl/xddm/display/rop.c:    if (!(drawable = Drawable(pdev, QXL_DRAW_COPY, area, clip, surface_id))) {
./qxl/xddm/display/rop.c:    if (!(drawable = Drawable(pdev, QXL_DRAW_BLEND, area, clip, surface_id))) {
./qxl/xddm/display/rop.c:    if (!(drawable = Drawable(pdev, QXL_DRAW_BLACKNESS, area, clip, surface_id))) {
./qxl/xddm/display/rop.c:    if (!(drawable = Drawable(pdev, QXL_DRAW_WHITENESS, area, clip, surface_id))) {
./qxl/xddm/display/rop.c:    if (!(drawable = Drawable(pdev, QXL_DRAW_INVERS, area, clip, surface_id))) {
./qxl/xddm/display/rop.c:    if (!(drawable = Drawable(pdev, QXL_DRAW_ROP3, area, clip, surface_id))) {
./qxl/xddm/display/rop.c:    if (!(drawable = Drawable(pdev, QXL_DRAW_ALPHA_BLEND, &area, clip, GetSurfaceId(dest)))) {
./qxl/xddm/display/rop.c:    if (!(drawable = Drawable(pdev, QXL_DRAW_TRANSPARENT, &area, clip, GetSurfaceId(dest)))) { 
./qxl/xddm/display/text.c:    if (!(drawable = Drawable(pdev, QXL_DRAW_TEXT, &area, clip, surface_id))) {
# И теперь QXL_COPY_BITS
zaburunovleonid@fedora:~/develop$ grep -R "QXL_COPY_" --include="*.c*" ./qxl/
./qxl/xddm/display/rop.c:    if (!(drawable = Drawable(pdev, QXL_COPY_BITS, area, clip, surface_id))) {

Вот это да! Весь enum перед глазами пролетел! Вот это пацанский драйвер!

Вывод

И вот мы подошли к кликбейтному выводу, который я разместил в начале. Лучшая поддержка и лучшая оптимизация у QXL-адаптера присутствует для ОС Windows! Да, для безнадёжно устаревших, но ведь я вас не обманул ;)

Ну а для Linux всегда всё было по принципу “SpiceVNC”. Шутка. Или нет…

Про одно занятное дополнительное подтверждение

Если присмотреться, то даже поиск графических команд не нужен, чтобы заподозрить что-то неладное. Обратите внимание на названия драйверов. Каноническое имя qxl досталось виндовому ;)

Ну и самое-то главное: QXL как паравиртуальный адаптер с дисплеем занимает собой вакантное место в списке устройств. Если подходить к вопросу так, как это делается в VNC, то по большому счёту реализовать удалённый доступ можно поверх любого драйвера дисплея. А у нас так не прокатит: либо вставлять в ВМ два видеоадаптера - и QXL, и “настоящую”, - и городить свои вариации на тему NVIDIA Optimus, либо оставаться вообще без видеоадаптера. А софтварным OpenGL 3, который даёт QXL, в наше время никого не удивишь. Ну и есть третий вариант: оставить только паравиртуальный адаптер, но другой. Именно так рекомендуют запускать QEMU + SPICE для локальных конфигураций: вместо QXL установить VirGL.

(Забегу вперёд: именно поэтому в нашем будущем стриминговом агенте не будет никакой привязки к драйверам и вся работа будет сосредоточена на уровне API гостевой ОС.)

До кучи я ещё могу отметить, что в ранних версиях SPICE использовал для своих нужд OpenGL Canvas, то есть графические команды транслировались в OpenGL прямиком на клиенте. Но из-за сложностей в поддержке лет 10 назад это было убрано. Точно такая же участь постигла и специализированный GDI Canvas для Windows. В итоге остался всего один вариант - использовать канвас, реализованный через Cairo и Pixman. Если будете использовать SPICE-клиент, обратите внимание на нагрузку на CPU - скушать целое ядро при использовании удалённого рабочего стола для этого бэкенда вообще не проблема. Даже "спасибо" не скажет.

Для тех, кто почувствовал окончательное разочарование

Если вам на мгновение стало обидно и даже показалось, что протокол никудышный - не спешите! На тему вынесения исполнения графических команд в отдельный домен, в том числе удалённый, есть много интересных проектов. Мир протоколов удалённого доступа, доставки рабочего стола и доставки приложений безграничен!

Да и я вас всех тут собрал, чтобы показать альтернативу ;)

Имеем что имеем. Как так вышло в случае с Windows - мы будем разбираться в следующей серии. Про Linux где-то по ходу цикла тоже сделаю отступление, но это не точно.

Заключение

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

В данной статье мы познакомились с протоколом SPICE в части работы с дисплеем. Оценили масштаб работы конкретно над SPICE и сложность технологий удалённого доступа в целом. Заметили, что богатство протокола оказалось сброшено с парохода современности. И пока что только задались вопросом “А как так вышло?”.

Самое главное, что нужно усвоить - устройство потока графических команд. На стороне гостевой ОС генерируются исходные сигналы, на стороне клиента происходит обновление виджета, а всё что между - транспортная обвязка. Во всяком случае, до тех пор, пока мы не говорим о более сложных случаях по типу стриминга - а этой темы мы пока не касаемся. Но обязательно коснёмся!

В следующих частях мы заглянем с обратной стороны “портала для фреймов”: прицельно рассмотрим архитектуру графической подсистемы ОС Windows. И заодно увидим, на что сделали ставку Qumranet при разработке QXL-драйвера в те времена, когда SPICE был разработкой внутри стартапа - и почему эта ставка не сработала.

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

До встречи в будущих статьях, счастливо!

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