Добавить в корзинуПозвонить
Найти в Дзене
Триалогия

Пишем операционную систему Триалогия - Оконный менеджер изнутри: компоновщик, поверхности и z-order

В прошлой части я сказал, что крупнейший потребитель ULES, это оконный менеджер WindowManager. Теперь я взгляну на него изнутри, ведь это место где сходится почти всё что я описывал до сих пор - разделяемая память, IPC-каналы, дисплейный прокси, события, загрузчик ELF, что его вообще запускает. Когда ты видишь на экране окна что накладываются друг на друга, курсор мыши, что скользит поверх, строку состояния с тикающими часами, всё это, это работа одной-единственной программы. И вот что в этой архитектуре радует меня больше всего, это и правда лишь одна программа. Оконный менеджер работает не в ядре, а как обычный сервис в ring 3 /bin/window_manager, загруженный как любой другой. Само ядро ничего не знает об окнах. Вспомни систему дисплея. Ядро даёт лишь самый нижний слой, аппаратную абстракцию и тонкий дисплейный прокси по имени KDP. Всё, что выше, это userspace. Оконный менеджер сидит ровно в этом промежутке. Ядро лишь пропускает данные через себя. Его поток блита берёт готовое изобр
Оглавление

В прошлой части я сказал, что крупнейший потребитель ULES, это оконный менеджер WindowManager. Теперь я взгляну на него изнутри, ведь это место где сходится почти всё что я описывал до сих пор - разделяемая память, IPC-каналы, дисплейный прокси, события, загрузчик ELF, что его вообще запускает. Когда ты видишь на экране окна что накладываются друг на друга, курсор мыши, что скользит поверх, строку состояния с тикающими часами, всё это, это работа одной-единственной программы.

И вот что в этой архитектуре радует меня больше всего, это и правда лишь одна программа. Оконный менеджер работает не в ядре, а как обычный сервис в ring 3 /bin/window_manager, загруженный как любой другой. Само ядро ничего не знает об окнах.

Где живёт рабочий стол

Вспомни систему дисплея. Ядро даёт лишь самый нижний слой, аппаратную абстракцию и тонкий дисплейный прокси по имени KDP. Всё, что выше, это userspace. Оконный менеджер сидит ровно в этом промежутке.

Слои: WM в ring 3 над прокси ядра
Слои: WM в ring 3 над прокси ядра

Ядро лишь пропускает данные через себя. Его поток блита берёт готовое изображение из разделяемой памяти fb_shm и шлёт изменённые области на железо примерно шестьдесят раз в секунду. Ввод идёт обратным путём, прерывания от клавиатуры и мыши PS/2 ложатся событиями в кольцо, что читает WM. Ядро не знает ни окна, ни z-order, ни фокуса. Оно знает пиксели и прямоугольники, не более.

Зачем эти хлопоты, почему не положить всё в ядро? Потому что падение оконного менеджера тогда не утянет за собой ядро. Я могу перезапустить его, перестроить, отладить теми же инструментами, что и любую программу. Ядро остаётся маленьким и ограничивается тем, чему действительно нужны привилегии. Вся политика рабочего стола, какое окно впереди, кто получает клавиатуру, как толковать клик, всё это, это обычный код в ring 3.

Складывать поверхности, рисовать лишь разницу

Так как же из нескольких окон складывается изображение? Базовая единица для этого, это поверхность, самостоятельный буфер пикселей. У каждого окна их даже две - одна для оформления с заголовком и рамкой и одна для содержимого в которое рисует приложение. Рабочий стол тоже поверхность, в самом низу, а курсор мыши, в самом верху.

Поверхности, z-order и компоновщик на основе dirty
Поверхности, z-order и компоновщик на основе dirty

Порядок, в котором эти поверхности лежат друг на друге, это 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, это то, на чём стоит каждое графическое приложение и они это следующая часть опуса.

Было бы интересно увидеть ваши комментарии и улучшить статьи.

Предыдущая статья Содержание Следующая статья ▶

*Система не стоит на месте, поэтому в дальнейшем тексты могут не совпадать с реальным положением