Оглавление
Вступление
Хабр, привет!
Я - Леонид Забурунов, инженер-программист компании “БФГ” - стартап по разработке системы виртуализации - в отделе разработки систем виртуализации графических ресурсов.
В рамках работы над доставкой рабочего стола для своей системы виртуализации мы наломали немало дров в самых разных направлениях. Одно из таких направлений - протокол SPICE как интереснейший объект для экспериментов. Этой серией статьей мы подводим своеобразный итог своим исследованиям прошлого и делимся с сообществом своими знаниями, частично - наработками. Своего рода дневники разработчиков.

Мы разберём самую объёмную часть протокола SPICE - доставку изображения удалённого рабочего стола. Документации и любой другой информации для столь объёмного инженерного решения катастрофически мало, а исходный код порой отбирает волю к жизни заставляет хорошенько почесаться - всё как мы любим!
Наша цель - научить виртуальные машины на ОС Windows, запущенные под SPICE-сервером, транслировать картинку в режиме стриминга. Хотим мы этого добиться потому, что встроенный QXL-адаптер не подходит для высокой нагрузки с частым обновлением экрана (это будет показано и обосновано далее). Необходимым условием для обозначенного предприятия является понимание предметной области, а потому план следующий:
Разобраться с устройством протокола SPICE в части графики. Это мы делаем прямо сейчас;
Разобраться с тем, какими “проводами” всё это хозяйство подключается к ОС Windows. Это мы сделаем в следующей части цикла;
Разобраться с тем, что мы можем сделать для более плавной картинки и понижения битрейта - читай для более комфортной работы. Это и все следующие пункты мы будем делать ещё дальше, начиная с третьей части;
Написать анонсированный стриминговый агент. Точнее, его минимальную реализацию.
Зафиксировать итоги работы и обсудить, что можно было бы сделать на следующих этапах для приближения к стадии Enterprise-решения.
Что ж, начинаем погружение в бездну…
А зачем нам вообще разбираться в SPICE и стриминге?
Забегу далеко вперёд специально для тех, кто глубоко в теме и хочет сперва ответить на вопрос, ради чего ковыряться палкой в сабже и устраивать сыр-бор.

Недавно я в личных целях развернул локальную виртуальную машину на 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, помедитировать в реестре. Пока исполнял перечисленные экзерсисы, понадобилось зайти на ютуб и открыть туториал. Открываю видео и вижу чудесную картину:

Ещё раз. Настроил всё по инструкции, подключился стандартным клиентом - и не работает видео. А не работает оно потому, что клиент дропает все фреймы стрима - это я позже узнал в логах. И дело тут не в видео: скролл терминала приводил к тому же результату. Оно просто не работает. А если видеокодек переставить с 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), где по задумке по каналу гуляют только пиксели (сжатые через кодек, конечно).

Ладно, к 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-шина. И это важно усвоить тем, кто не привык погружаться в детали удалённого доступа: интернет - слабое звено, поэтому львиную долю эффективности протокола составляют эффективность передачи данных и минимизация задержек.

Способов сжать данные, передаваемые в виде изображения или видео, довольно много. Именно с видео всё достаточно понятно: можно взять H264 и всё. Я сейчас не хочу сказать, что кроме H264 не существует кодеков. Я к тому, что технология сжатия видео в сущности одна, а выбираем мы зачастую не технологию, а тип лицензирования. В любом современном видеокодеке активно используются энтропийное кодирование, межкадровую разницу, манипуляции с цветовым представлением и другие приёмы. Если интересно подробнее - отсылаю к хорошей вводной статье про H264.
А вот с изолированными изображениями интереснее. Разницу между соседними кадрами никто не учитывает, однако разные типы данных на дисплее требуют разных подходов. Для отображения текста важнее чёткость символов, для отображения фото - визуальная целостность. Во многих случаях выигрышным оказывается комбинированный подход. Например, RDP использует несколько кодеков под разные задачи. Кроме того, в зависимости от ситуации можно использовать сжатие без потерь (lossless) или же с потерями (lossy). Конечно, хочется видеть идеальную картинку, но в реальных условиях (и уж тем более на слабых каналах) это неосуществимо.
Текстовые данные на экране, как правило, очень хорошо поддаются сжатию - буквы одного цвета, фон другого. И специализированные кодеки этим пользуются как предварительным знанием - картинка имеет небогатую цветовую палитру, никаких преобразований Фурье нам не нужно, только индексы на пикселях расставь. Это по сути архивация данных через хэш-таблицы. Если есть сложности с пониманием этого момента - посмотрите на широкоизвестный формат PNG. Разумеется, самые крутые кодеки используют и другие приёмы.
Но UI-ориентирванный кодек - это быстро по скорости, но прожорливо по пропускной способности сети. Это не недостаток, это закон физики: UI по свойствам изображения имеет мало общего с насыщенной иллюстрацией. В последнем случае визуальной резкостью приходится пожертвовать, потому что тогда изображение экрана рискует занять существенную часть канала и стать источником лагов.
И тогда на сцену выходят кодеки для изображений. Более того, большинство изображений, которое мы используем в повседневной жизни, уже и так сжато через JPEG (и скорее всего это не Lossless JPEG).
Ну и кодеки для видео. По большому счёту, это развитие той же идеи, только с добавлением предварительного знания о том, что дельта между соседними изображениями будет невысокой.
Когда вы качаете с торрента двух-трёхчасовой экшн-фильм в H265, он весит не 200 гигабайт, а 5-6: техники работы с межкадровым движением в наше время отрабатывают очень хорошо. Но это потому, что фильм обычно идёт в 24 или 30 кадров в секунду - если вы, конечно, не Илья Найшуллер. Если вы попробуете взять тот же фильм (или, например, фотосессию) и в случайном порядке переставить его кадры, то результат будет весить уже во много раз больше. Не говоря уж о качестве содержания...
Всё это ни разу не магия, а выверенное применение инженерных решений. Благослови прогресс.
Сжатие данных в протоколе SPICE

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-библиотеки…
Про оптимизацию доставки рабочего стола в слабых сетях
Речь, конечно, не только о кодеках. Возможности любого кодека не беграничны: чем больше данных на входа, тем больше данных на выходе. Против физики не попрёшь. Что можно сделать помимо улучшения кодека, так это сократить объём данных на входе. Для общего развития я перечислю лишь некоторые из приёмов, которые мне известны:
Можно понизить глубину цвета дисплея. А то и вообще перейти в ЧБ;
Можно заменить обои рабочего стола на монохромный фон (обычно чёрный). Если у вас есть полупрозрачные окна или же вы часто переключаетесь между приложением и рабочим столом, это сильно сократит объём передаваемой информации. Кстати сказать, дело не только в обоях рабочего стола: любая навороченная визуальная тема в приложении приведёт к таким же последствиям.
Можно отключить в гостевой ОС сглаживания шрифтов. Буквы и засечки станут, конечно, более резкими, зато уйдёт весь полупрозрачный ореол, превращающий с помощью операции alpha blend пиксели монотонного фона в разноцветные. (Если интересно подробнее, почитайте про растеризацию в 3D, там принцип такой же).
Можно заставить среду рабочего стола вести себя более аскетично, отключив системные анимации. В современном Windows, изобилующем всякими разноцветными гирляндами, это как никогда актуально. Ровно как и для оболочки GNOME.
В момент перетаскивания окна можно не рисовать его промежуточные положения, а дождаться, пока вы отпустите курсор.
Как и всегда, целая наука! Обмолвлюсь, что чем больше версия Windows, тем меньше это помогает - и дело в архитектуре DWM. К этому вернёмся в следующих статьях.
Но вернёмся к препарированному SPICE и к JPEG. Управляется он с помощью отдельного параметра QEMU по следующей схеме:
Можно отключить совсем (
off);Можно держать включённым всегда (
always);Можно настроить на принятие решения об активации непосредственно во время пользовательской сессии (
auto). Звучит заманчиво, но работает очень просто: если замер скорости на старте сессии показал меньше 10 Мбит/с, то включаем. С постоянным качеством. И всё, дальше не дёргаемся. Сильно лучше двух предыдущих вариантов, но для энтерпрайза никуда не годится. В этом отношении SPICE сильно недоработан, огромный потенциал во многом не реализован.
А ещё там для тех же целей добавлен zlib… Ладно, пора остановиться.
Для молодых и дерзких
Если вы в рамках профессионального развития хотите поупражняться в реализации фичей для сложной программной системы (коей SPICE безусловно является), я даю вам три идеи:
Добавить в SPICE сжатие с помощью LZAV или zstd. Это кодеки для быстрого сжатия без потерь, и они отлично впишутся вместо устаревших кодеков, которые представлены в протоколе сейчас.
Добавить в SPICE кодек JPEG-XL. Это кодек сжатия с потерями, современная альтернатива обычному JPEG, который в протоколе уже представлен.
Добавить в 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 };
Пойдём по порядку:
QXL_DRAW_NOP- обычный no operation. Скорее как служебное значение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- смешивание битмапы с модификацией (что-то вродеOPAQUE+COPY). Параметры - Битмапа, область, тип тернарной логической операции и кисть как третий операнд.QXL_DRAW_STROKE- отрисовка контура. Параметры - контур, кисть с параметрами заливки, тип логической операции.QXL_DRAW_TEXT- вывод текста. Параметры - строка, область, две кисти с параметрами заливки (текст и фон), типы логических операций для каждой кисти.QXL_DRAW_TRANSPARENT- применение битмапы с фильтрацией пикселей по значению альфа-канала. Параметры - битмапа, область, значение для фильтрации.QXL_DRAW_ALPHA_BLEND- наложение битмапы на текущее состояние. Параметры - битмапа, область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 - для устаревшей графической модели Windows XPDM (о ней будет рассказано в следующей серии);
qxl-wddm-dod - для актуальной графической модели Windows WDDM (о ней тоже будет рассказано в следующей серии);
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 был разработкой внутри стартапа - и почему эта ставка не сработала.
Приглашаю аудиторию в комментарии: готов ответить на вопросы по теме и жажду критических замечаний в адрес содержания и авторского стиля.
До встречи в будущих статьях, счастливо!