Вступление
Хабр, привет!
Я — Леонид Забурунов, инженер‑программист компании «БФГ» — стартап по разработке системы виртуализации — в отделе разработки систем виртуализации графических ресурсов.
В рамках работы над доставкой рабочего стола для своей системы виртуализации мы наломали немало дров в самых разных направлениях. Одно из таких направлений — протокол 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 был разработкой внутри стартапа — и почему эта ставка не сработала.
Приглашаю аудиторию в комментарии: готов ответить на вопросы по теме и жажду критических замечаний в адрес содержания и авторского стиля.
До встречи в будущих статьях, счастливо!