Все про композитинг в X11 | Linux

Введение

Приветствую, читатели хабра! Вы когда-нибудь задумывались над тем, что скрывается за красотой и динамичностью вашего рабочего стола? Какие компоненты реализовывают анимации и различные графические эффекты: блюр, тень, скругления, прозрачность? В X11 есть одна очень интересная программная единица — композитор. В этой статье мы получим общее понимание того, что это такое, как реализовано и для чего используется. Также, немного поговорим о том, как Xorg хранит информацию для отрисовки, затронем front/back буферы и узнаем, как компоненты рабочего стола обмениваются информацией между собой.

Оглавление

  1. Что такое композитинг

  2. Какие бывают композиторы

  3. Как работает композитинг

  4. Xrender

  5. GLX

  6. Атомы

  7. Заключение

  8. Полезные материалы

Что такое композитинг

Для того, чтобы осознать, какую роль композитор играет в Linux, остановимся на общем определении термина. Композитинг — процесс сочетания множества изображений в единый кадр. Те, кто когда-либо работал в программе photoshop, понимают, как устроен композитинг: различные изображения размещаются на выделенных слоях, редактируются, объединяются и экспортируются, как единое целое. В Linux и других операционных системах похожий алгоритм применим к преображению внешнего вида окон.

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

Все это потому, что дисплей сервер Xorg довольно ограничен по части графической обработки изображений. Если рассмотреть тот же эффект прозрачности, то без композитинга его воспроизвести не получится, так как для Xorg каждый пиксель на экране относится только к одному окну — в его памяти отсутствует информация о регионах, которые перекрыты и не видны пользователю ⇒ мы не можем получить доступ к красным пикселям, а они нужны для такой процедуры, как blending, чтобы достичь эффекта неполной прозрачности.

источник: xplainрендер окон Xorg (источник: xplain)

рендер окон Xorg (источник: xplain)

На примере этого чрезвычайно милого видео можно приблизительно понять, как Xorg работает с изображениями: он, при формирование кадра на вывод, исключает из обработки те части окон, которых не видно и заполняет изображением только те регионы, которые ничем не перекрыты и видны пользователю.

Перед тем, как идти дальше, хочется напомнить о том, что в самом примитивном виде, каждый элемент рабочего стола — дочернее для root окно. Иконки рабочего стола могут состоять из 2 toplevel окон, панель задач также является окном. Наверняка, это не самый оптимальный путь организации рабочего стола, но в X11 штатным способом отслеживать события ввода можно только на окнах. KDE-Plasma в этом плане хорош: он для всех иконок создает одно, дочернее для root, toplevel окно, внутри которого, как ему угодно, с ними работает. В этом подходе все бы хорошо, но сторонний композитор о наличии иконок ничего знать не будет и не сможет с ними работать, как с отдельными сущностями.

Какие бывают композиторы

Многие композиторы, из соображений различных оптимизаций и совместимости, являются частью оконных менеджеров: gnome-shell использует библиотеку mutter для оконного менеджмента и композитинга, kwin самостоятельно реализовывает композитинг, compiz запускается только если его использовать в качестве wm.

композитный оконный менеджер compiz

композитный оконный менеджер compiz

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

  1. Добавить строку »exec xfwm4 --compositor=off» в файл »~/.xinitrc». (xfwm4)

  2. Выполнить команду »qdbus org.kde.KWin /Compositor suspend» в терминале. (kwin)

Из тех немногих хороших композиторов, которые можно использовать в связке с чистым оконным менеджером, сейчас можно выделить, наверное, только picom и несколько его форков. Picom — это форк старого композитора compton. Полная цепочка зависимости выглядит следующим образом: picom → compton → xcompmgr-dana → xcompmgr. У этого композитора есть постоянный разработчик и широкая группа контрибьютеров. Композитный менеджер шустро развивается и регулярно дорабатывается. Из форков самым интересным сегодня является версия разработчика Ft-Labs: в проекте реализованы анимации + он периодически синхронизируется с мейнстримом. Сейчас анимации в процессе разработки и для оригинального проекта, но, пока все это на стадии майлстоуна для следующей версии (picom v12).

композитор picom

композитор picom

Как работает композитинг

Первым делом, для того, чтобы начать создавать комплексные графические эффекты, нужно как-то получить возможность свободно распоряжаться содержимым каждого окна и перестать использовать Xorg для компоновки окон внутри итогового кадра, который впоследствии будет выведен на экран. Технология, которая позволяет это сделать, называется »redirection». С помощью перенаправления можно указать серверу на то, что изображения каждого окна необходимо вывести в специальную область памяти, содержимое которой не предназначено для прямого отображения на экране — такой буфер в народе называют offscreen-buffer. Перенаправлять можно как все дочерние для таргета окна, так и какие-то определенные (кроме root).

Ниже вы можете наглядно увидеть то, как работает перенаправления. Xorg сервером зарегистрировано 4 окна, из которых видно 3: kitten1, root, inspector button. Где окно kitten2? Оно перенаправлено, что значит Xorg не отрисовал его напрямую в front-buffer, а перевел в так называемый »off-screen storage» — »backing pixmap». То, что мы не видим изображения, не говорит о том, что окна не существует. Мы все так же можем его перемещать, изменять его размер, в общем, работать, как обычно.

результат перенаправления (источник: xplain)

результат перенаправления (источник: xplain)

структура буферов в памяти Xorg

структура буферов в памяти Xorg

Сейчас может возникнуть вопрос, почему «root image» занимает весь »front-buffer»? Потому что область памяти, куда Xorg отрисовывает содержимое root, является буфером, который впоследствии будет выведен на экран — именно в этот блок памяти Xorg сервер записывает графическую информацию для всех видимых регионов наших окон — тот самый «front-buffer».

На программном уровне, реализовать перенаправление позволяет расширение XComposite, которое дополняет стандартные возможности взаимодействия с Xorg. В результате вызова для root »xcb_composite_redirect_subwindows», для каждого потомка в памяти выделяется буфер, содержимое которого регулярно обновляется, но подтвердить этого пока не получится, т.к все, что можно увидеть на экране монитора после данного вызова — корневое окно с наложенным изображением — фоном рабочего стола.

Так уж устроено расширение, что корневое окно нельзя перенаправить. Как тогда получить его содержимое? Получить информацию о фоне рабочего стола можно за счет трудов специального X11 клиента, в задачи которого входит работа с наполнением для root. Таким клиентом может быть оконный менеджер или менеджер обоев — он сохраняет фон рабочего стола в offscreen и присваивает корневому окну свойство »_XROOTPMAP_ID», чтобы внешнее приложение смогло по его значению получить доступ к изображению.

Перед тем, как переходить к демонстрационному коду, остановлюсь на том, что такое pixmap. Pixmap — сущность, через которую можно попросить Xorg сохранить графическую информацию в отдельный offscreen блок памяти, вместо того, чтобы сразу выводить на экран. Сам по себе, pixmap хранит идентификатор, который Xorg привязывает к данным в памяти. Через этот ID различные клиенты могут обращаться к графической информации, которая за ним находится.

Код ниже относится к композитору picom и реализовывает функционал, отвечающий за получение pixmap id буфера с изображением бэкграунда, которое позже будет переведено в текстуру и наложено на область, размеры и расположение которой соответствуют root.

static const char *background_props_str[] = {
    "_XROOTPMAP_ID",
    "_XSETROOT_ID",
    0,
};

xcb_pixmap_t
x_get_root_back_pixmap(xcb_connection_t *c, xcb_window_t root, struct atom *atoms) 
{
	 xcb_pixmap_t pixmap = XCB_NONE;
	 // Get the values of background attributes
	 for (int p = 0; background_props_str[p]; p++) 
	 {
		xcb_atom_t prop_atom = get_atom(atoms, background_props_str[p]);
		winprop_t prop = x_get_prop(c, root, prop_atom, 1, XCB_ATOM_PIXMAP, 32);
		if (prop.nitems) 
		{
    		pixmap = (xcb_pixmap_t)*prop.p32;
			free_winprop(&prop);
			break;
		}
		free_winprop(&prop);
	 }

	return pixmap;
}

В похожем ключе композитор будет получать pixmap для каждого окна, зарегистрированного сервером. Отличие в том, что другие приложения не обязаны создавать offscreen буфер для своих окон, поэтому, после того, как мы явно все перенаправили, композитор будет сам создавать идентификаторы и просить Xorg подвязывать к ним буферы с графическим представлением окон.

Код далее также относится к picom и занимается тем, что генерирует свободное id для pixmap элемента, закрепляет за ним буфер с данными окна, которые перенаправлены и находятся в offscreen. Необходимый буфер он находит за счет передачи id целевого окна в вызов »xcb_composite_name_window_pixmap». После того, как pixmap привязан к содержимому в памяти, композитор вызывает метод »bind_pixmap» для того, чтобы вытянуть из буфера информацию об изображении и сохранить ее в структуру бэкенда.

static 
inline bool win_bind_pixmap(struct backend_base *b, struct managed_win *w) 
{
    auto pixmap = x_new_id(b->c);
    xcb_composite_name_window_pixmap(b->c, w->base.id, pixmap)
 
    w->win_image = b->ops->bind_pixmap(b, pixmap, 
                                     x_get_visual_info(b->c, w->a.visual), 
                                     true);
    if (!w->win_image) 
    {
    	log_error("Failed to bind pixmap");
    	win_set_flags(w, WIN_FLAGS_IMAGE_ERROR);
    	return false;
    }

    win_clear_flags(w, WIN_FLAGS_PIXMAP_NONE);
    return true;
}

Следующим шагом является создание контекста, внутри которого будет отрисовываться составленный композитором кадр. Таким контекстом является overlay окно, которое занимает всю область монитора, пропускает через себя все события ввода и располагается поверх всех окон в системе, кроме «screen saver». Данное окно не будет перенаправлено и его не получится получить, как дочернее для root. Таким образом, оконный менеджер и другие X11 клиенты ничего об этом окне знать не будут и единственным способом получить его XID можно через команду расширения XComposite »CompositeGetOverlayWindow» (сигнатура вызова зависит от API: для xcb это «xcb_composite_get_overlay_window»).

Остановлюсь подробнее на том, почему нам важно, чтобы overlay пропускал все события ввода. Дело в том, что окна все еще находятся под контролем Xorg сервера и оконного менеджера. Когда мы перемещаем окно или изменяем его размер, Xorg генерирует события, изменяет атрибуты окон и предоставляет композитору всю необходимую информацию. Композитор, в свою очередь, может проигнорировать истинные параметры, где-угодно отрисовать примитив и как угодно наложить на него текстуру, из-за чего может возникнуть ситуация, когда вы перемещаете невидимое окно в одном месте и видите результат совершенно в другом. Отключаем композитинг, все на своих местах, в соотвествии с информацией, которой располагает Xorg.

демонстрация отношения данных Xorg и Picom

демонстрация отношения данных Xorg и Picom

Теперь, после того, как контекст создан, пришло время выбрать бэкенд, который будет пыхтеть над тем, чтобы алгоритм блюра выполнялся быстро и результат радовал пользователя. Бэкенд — это кодовая база, имплементирующая рендер всего, что мы видим на экране. Если попробовать провести ассоциацию с тем же фотошопом, то бэкенд — это умелый дизайнер, который берет от заказчика (Xorg) исходный материал (текстуры окон), подгоняет его под ТЗ (размеры, координаты расположения на экране), накладывает эффекты, располагает все на разных слоях и, когда все готово, экспортирует результат работы единым изображением (рендерит кадр внутри контекста). Конечно, не существует каких-либо нормативных требований под то, как композитор должен реализовывать бэкенд — у каждого свой подход.

Бэкенды бывают разные: egl, vulkan, xrender, glx и т.д. Различаются они набором используемых библиотек, клиентским интерфейсом и способом организации рендер пайплайна. Давайте копнем глубже и рассмотрим 2 самых популярных: xrender и glx.

Xrender

Xrender использует для отрисовки различные расширения из окружения X: xcomposite, xrender, xpresent, xsync, xcb, xshape, которые, грубо говоря, расширяют графический потенциал стандартного xlib API: позволяют гибко управлять трансформацией окон, использовать antialiasing и alpha blending. Этот бэкенд все еще сильно ограничен по функционалу, как минимум из-за того, что отсутствуют шейдеры, которые позволяют буквально контролировать состояние каждого пикселя. Из-за отсутствия развития расширений, бэкенд потихоньку выпиливается из композиторов. Из того же kwin его убрали, в picom он все еще используется (какие-то серьезные проблемы стараются допиливать, но основной приоритет стоит на glx). Сейчас бэкенд является фоллбэком и используется либо со слабым видеускорением, которое не может нормально тянуть OpenGL рендер, либо с драйверами, которые реализовывают устаревшую версию OpenGL, либо с software rendering драйверами, такими как llvmpipe, softpipe или swrast.

GLX

GLX бэкенд реализовывает рендер через связку OpenGL + GLX. Если кто-то не в курсе, то OpenGL представляет собой графическое API, реализация которого содержится либо в открытых, либо в проприетарных драйверах, которые переводят вызовы API в понятные железу команды. Если сильно облегчать, то OpenGL, так или иначе, используется для отрисовки графики в «framebuffer». Создание контекста позволяет инициализировать OpenGL: предоставить дефолтный буфер для отрисовки, установить контакт с драйверами и оконной системой. Создание и управление контекстом является платформозависимым — каждая оконная система предоставляет свое расширение WGL (Windows), GLX (X11). Таким образом, расширение GLX связывает оконную систему X и OpenGL, что позволяет установить соединение с Xorg сервером и рендерить графику в окне.

С появлением расширения GLX, узел, связывающий разработчикам руки, начал немного послабляться: возможностей для реализации идей стало больше, но в тоже время появилась необходимость постоянно следить за производительностью и совместимостью реализованных решений с широким спектром драйверов. В отличии от xrender, glx бэкенду свойственно в процессе выполнения подстраивать систему рендера под параметры той или иной системы. Если говорить о kwin, то в нем реализован целый уровень абстракции для проверки графических модулей системы.

В picom тоже есть подвижки в эту сторону, но пока какого-то глубокого анализа не происходит. Поэтому, современный glx бэкенд работает криво на драйверах программного рендеринга и не запускается с OpenGL < 3.3. Проблема в том, что не реализован никакой механизм фоллбэка на нормально работающую реализацию - пользователь должен сам перебирать параметры и разбираться, почему что-то не работает.

Сейчас, на примере picom, чуть подробнее разберем, для чего может быть нужен GLX в прикладных приложениях. Благодаря тому, что это расширение позволяет нам тесно взаимодействовать с оконной системой, становится возможным преобразовать «offscreen pixmap» в текстуру, с которой OpenGL сможет работать.

// pixmap - идентификатор, по которому лежит "offscreen" изображение окна
// glxpixmap->glpixmap - GLX специфичный id, который ссылается на данные pixmap
// inner->texture - OpenGL текстура
// glXBindTexImageEXT - преобразование данных glpixmap в текстуру
// Впоследствии, обновление содержимого, на которое указывает pixmap/glpixmap 
// приведет к обновлению inner->texture
glxpixmap->glpixmap = glXCreatePixmap(base->dpy, fbconfig->cfg, pixmap, attrs);
glBindTexture(GL_TEXTURE_2D, inner->texture);
glXBindTexImageEXT(gd->display, glxpixmap->glpixmap, GLX_FRONT_LEFT_EXT, NULL);

Как говорилось ранее, GLX предоставляет OpenGL дефолтный буфер для offscreen рендера, в который будет отрисовываться графика при вызове команды «glDrawElements». Необходимый буфер можно явно указать указать с помощью «glBindFramebuffer». Если все время рендерить графику в сторонний буфер, который не является стандартным для контекста, результата компоновки видно не будет, т.к »glXSwapBuffers» выводит на экран содержимое именно того буфера, который определен расширением GLX. Для того, чтобы кадр все-таки отобразился в окне, его нужно перенести из «back-buffer» в «front-buffer». Расширение, как раз, предоставляет функционал замены кадров — вызов «glXSwapBuffers». Сокращенный процесс рендера графики с последующим выводом на экран выглядит следующим образом:

// Активируем итоговую текстуру, которая является конечным результатом компановки
// Рендерим итоговый кадр в "back-buffer" (пользователь еще ничего не видит)
// Переносим "back-buffer" в "front-buffer", что приводит к отображению кадра
// внутри overlay окна
glBindTexture(GL_TEXTURE_2D, gd->back_texture);
glDrawElements(GL_TRIANGLES, nrects * 6, GL_UNSIGNED_INT, NULL);
glXSwapBuffers(gd->display, gd->target_win);

Еще одним примером, показывающим важность расширений GLX, WGL и т.д., является возможность вертикальной синхронизации. Как вы понимаете, OpenGL используется для того, чтобы рисовать. За то, с какой периодичностью показывать результат, он уже не отвечает. GLX предоставляет ряд «расширений», поддержка которых зависит от реализации. К vsync относятся следующие: glXSwapIntervalEXT, glXSwapIntervalSGI, glXSwapIntervalMESA.

Атомы

Можно сказать, что Xorg реализовывает что-то вроде IPC (Inter process communication). Каждому окну можно присвоить ряд свойств со строковым либо числовым значением: 8s (строка), 32a/32i (знаковое 32-битное число), 32с (беззнаковое 32-битное число) и т.д. Эти свойства могут быть установлены одним X11 клиентом и считаны другим, за счет чего тот же композитор сможет определить необходимые действия по отношению к целевому окну. Список стандартных свойств, с которыми работает Xorg, можно посмотреть в X11/Xatom.h: некоторые из них могут неявно использоваться в библиотечных вызовах xlib/xcb. Например, вызов XStoreName выставляет значение для WM_NAME. Никто вам не запрещает создать произвольное свойство, которое не определено в заголовочном файле. Список всех атомов для целевого окна можно посмотреть с помощью команды xprop. Эту же команду можно использовать для того, чтобы присвоить свойство:

xprop -id  -format   -set  
xprop -id 0x8000a8 -format WM_NAME 8s -set WM_NAME "My Application"

Программа xprop написана на xlib и, в упрощенном виде, использует следующие вызовы для определения свойств:

Atom prop = XInternAtom(display, "CUSTOM_TEXT_PROPERTY", False);
const char *prop_val = "Property value";
XChangeProperty(display, window, prop, XA_STRING, 8, PropModeReplace, 
								(const unsigned char *)prop_val, 
                                strlen(prop_val));

Существует, так называемая, EWMH спецификация, которая трактует правила для построения качественного взаимодействия между различными компонентами окружения рабочего стола. В том числе, она указывает на то, какие свойства каким процессом должны быть выставлены для каких целей. Композитинг спецификация не обошла стороной: X11 клиент может указать свойство »_NET_WM_BYPASS_COMPOSITOR» в случае, если окно не нужно перенаправлять и прогонять через цикл компоновки. Как вы понимаете, приложения не обязаны следовать спецификации и в точности реализовывать то, что в ней написано, поэтому, в picom какое-то время отсутствовала поддержка данного свойства. Сейчас, если на каком-то окне обнаружен _NET_WM_BYPASS_COMPOSITOR=1, picom запускает »unredirect» для всех окон и возвращает бразды правления серверу.

Заключение

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

С этой статьей запускается в работу мой ТГ-канал, который будет регулярно наполняться интересным IT материалом (Linux, Computer Science, разработка, графика). Подписывайтесь, чтобы не пропустить интересных анонсов, всегда быть на связи и с каждым новым днем становиться лучше себя вчерашнего. Полный список ресурсов, которые использовались при написании статьи, будет закреплен до следующей публикации. До встречи в грядущих уроках!

Полезные материалы

  1. Введение в композитинг: reparenting-redirecting-composition-rendering / xplain-composite-tutorial / x11-composite-tutorial / layered compositing / the compositor is evil

  2. OpenGL/GLX: OpenGL Context / extensions, protocols and IPC standards / Double Buffering / EGLAndGLXAndOpenGL

  3. Графические подсистемы Linux: The Linux Graphics Stack / How does Linux’s display work? / The Real Story Behind Wayland and X / Direct Rendering Manager / Direct Rendering Infrastructure

© Habrahabr.ru