В конце прошлой части я обещал посмотреть, что ещё стоит поверх kcall (Kernel Calls): как программы "разговаривают" друг с другом и с оконным менеджером. Ровно этим мы сейчас и займёмся, на самом наглядном примере, какой у меня есть, на приложении, которое открывает окно.
Сразу главная фишка: оконный менеджер, это тоже всего лишь программа
К этому надо привыкнуть. Во многих системах вся возня с окнами сидит глубоко внутри. У меня нет. Ядро вообще ничего не знает про окна. Оно знает экран и поток, который записывает туда пиксели, и больше ничего. Всё, что выглядит как рабочий стол, окна, заголовки, Z-порядок, фокус, делает оконный менеджер (WindowManager - WM), а он сам, это всего лишь программа в ring 3, ровно как твоё приложение.
Это значит: когда приложению нужно окно, оно "разговаривает" не с ядром (конечно совсем без ядра не обходится). Оно "разговаривает" с другой программой пользовательского пространства, которая по случаю заведует рабочим столом. Два равноправных процесса, которым надо договориться. А техника для этого тебе уже знакома по прошлой части: общая память (SHM) плюс события (Events). Несмотря на то что оба процесса выполняются в ring 3, оконный менеджер (WM) имеет по отношению к ядру немножко больше возможности общения (он знает пароль :-) ). Он является подписанным сервисом (signed service) и ему ядро доверяет больше.
Как возникает соединение
При создании первого окна маленькая клиентская библиотека libwm один раз вызывает display_client_connect(). И это ровно один из opcode'ов kcall из прошлой части. Ядро лишь осуществляет "знакомство" пользовательского приложения и оконного сервиса: создаёт область общей памяти, говорит WM "вот новый клиент" и отдаёт приложению идентификаторы (ID).
int libwm_init(void) {
display_client_connect_result_t res;
display_client_connect(&res); // opcode kcall из прошлой части
void* base = shm_map(res.shm_id, 0); // один SHM, два кольца внутри
g_libwm.req_ring = base; // Ring A: приложение -> WM (запросы)
g_libwm.resp_ring = base + ring_a_size; // Ring B: WM -> приложение (ответы + события)
g_libwm.req_event = res.req_event_id; // будит WM
g_libwm.resp_event = res.resp_event_id; // будит приложение
return 0;
}
После этого приложение и WM висят на одних и тех же двух кольцах. Через Ring A приложение шлёт свои запросы, через Ring B WM отвечает и позже досылает события ввода если необходимо. приложение кладёт что-то в кольцо, сигналит (Event) с помошью соответствующего соответствующего события, чтобы другая сторона проснулась и обработало данные или результат.
Окно появляется на свет
Теперь приложение может запросить своё окно. Оно кладёт сообщение типа WM_MSG_CREATE_WINDOW в Ring A (позиция, размер, заголовок) и будит WM. Тот ищет свободный слот клиента, создаёт окно, резервирует под него буфер пикселей и отвечает:
typedef struct {
uint32_t window_id; // 0 = ошибка
int surface_shm_id; // этот буфер приложение отображает себе
uint32_t surface_width, surface_height, surface_pitch;
uint8_t surface_bpp;
} wm_create_resp_t;
Главное сидит в surface_shm_id. Это поверхность окна, вторая область общей памяти, на этот раз только под пиксели этого одного окна. Приложение отображает её себе и получает прямой доступ к памяти изображения своего окна, ARGB32, пиксель за пикселем.
Весь путь на картинке
Рисование
Когда у приложения есть поверхность, рисование выглядит будничным. Вот настоящий шаблон из моей маленькой демо-программы (набери в терминале wm_demo):
libwm_window_t* win = libwm_create_window(100, 100, 400, 300, "Demo", 0);
uint32_t w = libwm_get_width(win), h = libwm_get_height(win);
for (uint32_t y = 0; y < h; y++)
for (uint32_t x = 0; x < w; x++)
libwm_draw_pixel(win, x, y, make_argb(0xFF, r, g, 0x40)); // прямо в поверхность
libwm_invalidate_rect(win, 0, 0, w, h); // "пожалуйста, вынеси этот прямоугольник на экран заново"
То есть приложение пишет прямо в общий буфер пикселей, а потом лишь сообщает WM "в этом прямоугольнике кое-что изменилось". WM не нужно принимать пиксели, они у него уже есть, они лежат в той же памяти (Zero-Copy).
Кому не хочется делать всё пиксель за пикселем, может слать и готовые команды рисования через Ring A: залитый прямоугольник (FILL_RECT), блок пикселей (BLIT_PIXELS), символ шрифтом WM (DRAW_GLYPH) или сдвинуть блок при прокрутке (SCROLL_REGION). Терминал, например, использует SCROLL_REGION, чтобы при прокрутке вверх не передавать каждую строку заново по отдельности.
И обратно в приложение: события (Events)
Обратное направление идёт по Ring B. Как только ты нажимаешь клавишу или двигаешь мышь над окном, WM досылает событие (Event) приложению. Приложение забирает его в своём цикле:
libwm_event_t ev;
while (running) {
if (libwm_get_event(win, &ev, (uint32_t)-1)) { //блокирующее ожидание на Ring B
if (ev.type == LIBWM_EVENT_CLOSE) break; // нажат крестик окна
if (ev.type == LIBWM_EVENT_KEY_DOWN) handle_key(ev.u.key.ch);
if (ev.type == LIBWM_EVENT_MOUSE_MOVE) move_cursor(ev.u.mouse.x, ev.u.mouse.y);
}
}
libwm_destroy_window(win);
События, которые так приходят, это клавиатура, мышь, просьба о закрытии, "кадр готов" и изменение размера. Для мыши WM отдаёт сразу две позиции : один раз относительно окна (0,0, это верхний левый угол твоего окна) и один раз абсолютно на экране. Так приложению не нужно ничего пересчитывать самому, а вещи вроде перетаскивания (Drag and Drop) или всплывающих окон всё равно работают.
Кто убирает за собой
Одну вещь я узнал только через неприятную ошибку: WM не просто собеседник, он ещё и хозяин над приложениями рабочего стола. Закрой последнее окно приложения, и WM тут же завершает весь процесс. Звучит строго, но это правильный путь: WM открыл окно, значит он же и убирает за собой, вместо того чтобы оставлять приложение без окна висеть в системе как зомби. Пока я не переделал все в такой форме, закрытые программы зависали в системе невидимыми трупами.
От окна на экран
Остаётся вопрос, как из множества отдельных поверхностей окон получается одно изображение. Это работа WM как композитора (compositor): он берёт все видимые поверхности, складывает их в правильном порядке друг на друга, дорисовывает заголовки и рамки и пишет итоговое изображение в ещё одну общую память fb_shm. Оттуда его забирает поток блита в ядре и записывает/копирует на видеокарту только изменившиеся области.
Ядро делает тупое, быстрое проталкивание пикселей через драйвер видеокарты (softrendering или DMA), вся логика рисования живёт в пространстве пользователя в сервисе под названием WindowManager- WM.
Какое то специальное имя для WM я еще пока не придумал, делайте предложения в комментариях.
Правда жизни как обычно
Это ровно тот же приём, что и у кольцевого буфера kcall, уровнем выше: общая память под данные, маленький кольцевой буфер под управление, события (Events) для пробуждения. Только тут друг с другом "говорят" две равноправные (ну почти равноправные) программы, а не программа с ядром. И как и мост из прошлой части, этот канал тоже хрупок, если WM теряет соединение к клиенту (приложению), это быстро становится неприятным, замёрзшие окна, мёртвый ввод. Пару таких историй я уже прошёл. Но в целом оно работает и ты кликаешь по рабочему столу который использует ровно эту "механику".
◀ Предыдущая статья Содержание Следующая статья ▶
*Система не стоит на месте, поэтому в дальнейшем тексты могут не совпадать с реальным положением