В прошлой части я сказал, что крупнейший потребитель ULES, это оконный менеджер WindowManager. Теперь я взгляну на него изнутри, ведь это место где сходится почти всё что я описывал до сих пор - разделяемая память, IPC-каналы, дисплейный прокси, события, загрузчик ELF, что его вообще запускает. Когда ты видишь на экране окна что накладываются друг на друга, курсор мыши, что скользит поверх, строку состояния с тикающими часами, всё это, это работа одной-единственной программы.
И вот что в этой архитектуре радует меня больше всего, это и правда лишь одна программа. Оконный менеджер работает не в ядре, а как обычный сервис в ring 3 /bin/window_manager, загруженный как любой другой. Само ядро ничего не знает об окнах.
Где живёт рабочий стол
Вспомни систему дисплея. Ядро даёт лишь самый нижний слой, аппаратную абстракцию и тонкий дисплейный прокси по имени KDP. Всё, что выше, это userspace. Оконный менеджер сидит ровно в этом промежутке.
Ядро лишь пропускает данные через себя. Его поток блита берёт готовое изображение из разделяемой памяти fb_shm и шлёт изменённые области на железо примерно шестьдесят раз в секунду. Ввод идёт обратным путём, прерывания от клавиатуры и мыши PS/2 ложатся событиями в кольцо, что читает WM. Ядро не знает ни окна, ни z-order, ни фокуса. Оно знает пиксели и прямоугольники, не более.
Зачем эти хлопоты, почему не положить всё в ядро? Потому что падение оконного менеджера тогда не утянет за собой ядро. Я могу перезапустить его, перестроить, отладить теми же инструментами, что и любую программу. Ядро остаётся маленьким и ограничивается тем, чему действительно нужны привилегии. Вся политика рабочего стола, какое окно впереди, кто получает клавиатуру, как толковать клик, всё это, это обычный код в ring 3.
Складывать поверхности, рисовать лишь разницу
Так как же из нескольких окон складывается изображение? Базовая единица для этого, это поверхность, самостоятельный буфер пикселей. У каждого окна их даже две - одна для оформления с заголовком и рамкой и одна для содержимого в которое рисует приложение. Рабочий стол тоже поверхность, в самом низу, а курсор мыши, в самом верху.
Порядок, в котором эти поверхности лежат друг на друге, это z-order. Это просто число на поверхность - у стола z = 0, обычные окна лежат выше, строка состояния (statusbar) на z = 1000, а курсор на самом верху на z = 0x7FFF. Поднять окно вперёд значит лишь дать ему z_index больше нынешнего максимума. На этом окне висит фокус и клавиатура всегда достается лишь верхнему окну с фокусом.
Складыванием занимается компоновщик, и он ленив намеренно. Перерисовывай он весь экран каждый кадр, это было бы чистым расточительством. Вместо этого каждая поверхность помнит в сетке, какие маленькие ячейки изменились с прошлого раза. Это и есть dirty-rects. На каждый изменённый прямоугольник компоновщик идёт снизу вверх через поверхности и смешивает пиксели, причём полностью прозрачные просто пропускаются. Итоговая картинка сперва ложится в backbuffer в RAM, а потом одним блоком в fb_shm, откуда его забирает поток блита в ядре.
Эффект ощутим. Если тикают только часы, заново перерисовывается лишь маленький прямоугольник часов, а не весь стол. Когда движется мышь, компоновщик возвращает старый прямоугольник с фоном и рисует курсор на новом месте. Так поверхность остаётся плавной, даже без большого аппаратного ускорения.
Как приложение получает своё окно
Остаётся вопрос, как приложение вообще получает окно и рисует в него. Ответ снова - это разделяемая память, а библиотека, что её прячет, зовётся libwm.
При старте каждое приложение получает от WM канал, то есть пару колец в разделяемой памяти, одно для запросов от приложения к WM, одно для ответов и событий обратно. Когда приложение запрашивает окно, WM создаёт окно вместе с поверхностью и шлёт обратно две вещи: ID окна и идентификатор буфера SHM для пикселей.
libwm_window_t* win = libwm_create_window(x, y, w, h, "заголовок", flags);
Этот буфер приложение отображает к себе как свою память и пишет прямо в него, пиксель за пикселем, в формате ARGB. Без вызова копирования, без системного вызова на каждый пиксель.
uint8_t* px = libwm_get_surface_pixels(win);
px[y * pitch + x * 4] = color;
libwm_invalidate_rect(win, x, y, w, h); // сказать, что изменилось
invalidate это единственное сообщение, что ещё нужно. Оно говорит WM какая область новая. WM забирает пиксели в поверхность окна, помечает их dirty и на следующем проходе компоновщика они появляются на экране. Ввод идёт обратным путём. Когда приходит клик мышью, WM находит по hit-test верхнее окно под курсором, поднимает его вперёд и кладёт событие в его очередь, что приложение читает через libwm_wait_event.
Честное место
Оконный менеджер, это та часть системы, где баги преследовали меня все время от начала и преследуют до сих пор.
Худшим был use-after-free при закрытии. Когда приложение закрывает последнее окно, WM это владелец жизненного цикла, он завершает процесс через SIGKILL. Но поначалу он убирал связанного клиента лишь с задержкой. В промежутке между этим он продолжал читать из кольца запросов этого клиента, пока Thread reaper уже освобождал разделяемую память. Итогом был page fault посреди главного потока WM и с этим вставал весь рабочий стол. Переделал на - кто прибивает окошко тот должен и сразу прибраться. Клиент теперь убирается из списка в тот самый миг, когда прилетает сигнал, а не позже.
Сродни этому была проблема с зомби-приложениями. Некоторые приложения выгребали из своей очереди лишь одно событие за кадр. Если та наполнялась событиями мыши, событие закрытия окна выпадало и приложение работало дальше невидимо, хотя его окно давно исчезло. Та же ловушка таится при переносе окна, если окно уничтожить посреди переноса, компоновщику нельзя дальше обращаться к старому указателю. Поэтому вставил преднамеренное ожидание, пока текущий проход компоновщика не завершится, прежде чем освободить призрачный контур.
И наконец классика с двумя потоками компоновщика: один поток пишет пиксели, другой читает dirty-флаг. Без порядка блокировки памяти пиксели могли бы ещё застрять в кэше, пока флаг уже сообщает "готово". Решение, это пометки ACQUIRE и RELEASE, что гарантируют - пиксели будут записаны прежде, чем флаг их отпустит. Это маленькая дисциплина, но без неё изображение мерцает как неоновая лампа :-) .
Еще одна честная история - я не собирался вообще делать графику!
На тот момент мне казалось, что это какая то черная дыра, адронный колайдер, ракетная техника. Система должна была остаться текстовой, серверной. Но случилось чудо. В дзене повстречал статью а затем прошелся по этой серии статей об окнах, графических элементах, z ордерах и понял, что не все так страшно. Сделал, что сделал.
Что дальше
Итак, оконный менеджер рисует рамку а содержимое окна приложение рисует само с помощью маленьких библиотечек. libwm я уже называл, рядом есть libgfx для рисования, libterm для отображения терминала и собственная маленькая libc под ними. Эти библиотеки userspace, это то, на чём стоит каждое графическое приложение и они это следующая часть опуса.
Было бы интересно увидеть ваши комментарии и улучшить статьи.
◀ Предыдущая статья Содержание Следующая статья ▶
*Система не стоит на месте, поэтому в дальнейшем тексты могут не совпадать с реальным положением