Архитектура и внутреннее устройство Polynya
В этом документе описана архитектура и реализация проекта: как устроен рендер без фреймворков, почему для окна выбрана поверхность DirectComposition вместо обычного swap chain, как именно снимаются цвет и текстура панели задач, и какие нетривиальные проблемы Windows пришлось обойти.
1. Многопроцессная схема и потоки
Polynya разделена на независимое ядро и изолированные процессы модулей — падение любого модуля не роняет сам остров:
| Процесс | Время жизни | Связь с ядром |
|---|---|---|
polynya.exe (ядро) |
Постоянно | — |
polynya-start.exe ("Пуск", в разработке) |
Постоянный модуль (resident) |
stdin / stdout (JSON-строки) |
polynya-notify.exe (уведомления Windows) |
Постоянный, активируется через sparse-пакет | Именованный канал (proto::send) |
polynya-agents.exe (хуки ИИ-агентов) |
Запускается на событие хука и сразу выходит | Именованный канал |
polynya-picker.exe (пипетка) |
Запускается своей командой из поиска, завершается после выбора цвета | stdin / stdout, чипы через именованный канал |
devtools (main.js на Node.js, в разработке) |
Запускается первой командой или сочетанием клавиш, продолжает работать | stdin / stdout (JSON-строки) |
polynya.exe push/... (клиент CLI) |
Одно сообщение и выход | Именованный канал |
Потоки внутри ядра (polynya.exe)
- UI-поток: окно, цикл сообщений
GetMessage, таймеры и весь рендер Direct2D / DirectComposition. ipc: сервер именованного канала\\.\pipe\Polynya-<SessionId>.media: подписка на системные медиасессии (GSMTC) и дочитывание запаздывающих обложек.audio: захват звука процесса плеера через WASAPI process loopback и расчет спектра.appsиicons: фоновый обходshell:AppsFolderи извлечение иконок оболочки (IShellItemImageFactory).- Хук мыши: поднимается в отдельном потоке только пока открыта выпадашка (иначе глобальный хук в UI-потоке давал бы микро-лаг курсора во всей системе).
module-<id>: чтениеstdoutиstderrпроцесса модуля и отслеживание его завершения.- Погода и Everything IPC: фоновые запросы к Open-Meteo / Nominatim и обмен через
WM_COPYDATAс Everything.
2. Окно и рендер (Direct2D + DirectComposition)
- Окно без перенаправления битовой карты: создается со стилями
WS_EX_NOREDIRECTIONBITMAP | TOPMOST | TOOLWINDOW | NOACTIVATE. Владельцем окна назначается сама панель задач (Shell_TrayWnd) — в Win32 принадлежащее окно гарантированно рисуется поверх владельца, поэтому панель задач никогда не перекроет остров при всплытии (после перезапуска проводника поTaskbarCreatedпривязка восстанавливается). - Почему поверхность DirectComposition, а не DXGI Swap Chain: на встроенной графике Intel первая же цепочка буферов (swap chain) любого размера резервирует ~50 МБ служебной памяти драйвера. Переход на чистую поверхность DirectComposition (
IDCompositionSurface) снизил потребление памяти ядра в покое с 67 МБ до 17-26 МБ. - Рендер на встроенной графике (iGPU): в системах с двумя видеокартами ядро намеренно выбирает интегрированный адаптер. На нем же работает системный композитор Windows (DWM) и рисуется рабочий стол, поэтому текстуры не гоняются между разными GPU, а дискретная видеокарта не будится ради интерфейсной утилиты и спокойно уходит в глубокий сон.
- Прозрачность для кликов и регион окна (
SetWindowRgn): само окно растянуто на всю ширину панели задач, поэтому без ограничений оно бы перехватывало все клики по системному трею и иконкам программ. Чтобы пустые места пропускали мышь насквозь, для окна задается фигурная маска (SetWindowRgn) ровно по контуру видимых блоков. При открытии выпадашки эта маска расширяется заранее (доCommitкадра) и сразу на финальный размер панели с запасом под тень и скругления - иначе во время пружинной анимации края картинки упирались бы в границы еще не выросшего региона и обрезались. При этом сам хит-тест мыши внутри окна считает точный вогнутый контур выпадашки. - Рендер только при изменениях: в покое главный поток полностью спит в
GetMessage(0,00% CPU). Анимации раскрытия идут с частотой обновления монитора (Commit+DwmFlush), а фоновые анимации (визуализатор, бегущие строки) ограничены таймером ровно в 30 к/с на любом мониторе (на 144 Гц ожиданиеDwmFlushбез таймера выдавало бы лишние 72 к/с). - Аппаратная бегущая строка силами DWM: длинное название трека рисуется один раз на отдельную поверхность композиции (
scroller.rs), а саму прокрутку ведет системный композитор Windows черезIDCompositionAnimationс интерполяцией ближайшим соседом (буквы едут по целым пикселям и не мылятся). Нагрузка CPU при прокрутке упала с 0,36% до 0,03%. Так как две поверхности одного устройства нельзя рисовать одновременно (SURFACE_BEING_RENDERED), слои скроллера отрисовываются строго после закрытия основного кадра и отправляются одним общимCommit. - Прогрев шейдеров на старте: первое применение эффектов Direct2D (
Turbulence,DisplacementMap,Shadow) требует компиляции шейдеров (~290 мс), что давало бы фриз при первом открытии выпадашки. Поэтому при старте ядро вхолостую прогоняет эти эффекты на буфере 16x16 — первое реальное открытие занимает 2,1 мс.
3. Адаптация под панель задач и цветовая модель
Как снимается вид панели задач (look.rs)
Захват окна панели через Windows Graphics Capture (WGC) не подходит — в нем нет размытой подложки DWM. Поэтому делается BitBlt полосы экрана, где лежит панель задач. На время снимка (23–46 мс) окно самого острова помечается флагом WDA_EXCLUDEFROMCAPTURE, чтобы не сфотографировать само себя.
Из снимка извлекаются:
- Светлая или темная тема панели и системный акцент;
- Строка кромки (цвет пикселей у края панели по столбцам с медианным фильтром 48 px и сглаживанием 8 px — чтобы иконки или кнопка "Пуск" не давали цветовых пятен на стыке);
- Строка обводки (тонкая линия вдоль края панели, если разница светлоты ≥ 0,02);
- Зерно (шум) текстуры панели задач.
Никакого поллинга по таймеру нет — пересъемка срабатывает только по событиям (склеиваются дебаунсом в 1 с): старт системы (плюс контрольные досъёмки через 2 / 5 / 15 / 30 с, так как проводник дорисовывает подложку с задержкой), смена обоев, смена темы (ImmersiveColorSet), смена акцента (WM_DWMCOLORIZATIONCOLORCHANGED), высокий контраст, перезапуск проводника (TaskbarCreated), разблокировка и выход из сна.
Цветовое пространство OKLCH и контраст APCA (tone.rs)
- правило цвета: все, что видно на экране постоянно (плашки времени и поиска, кнопки, включенные тумблеры), красится в тон самой панели задач, а не в системный акцент (акцент часто спорит с обоями). Системный акцент используется только для выделения строки в момент действия пользователя (наведение или выбор стрелками).
- Плашки (
tone::plate): берут оттенок (Hue) панели в пространстве OKLCH, а светлота подбирается под целевой перцептивный контраст APCA (Lc 60для основного текста,Lc 45для приглушенного) при насыщенности≤ 0,06и прозрачности 20–45%.
Выпадашки: стык с панелью и переход в Mica
- Геометрия стыка (
shape.rs): выпадашка растет из панели через вогнутые скругления, привязанные к целым физическим пикселям экрана (на дробных координатах края мерцали при анимации). - Бесшовное продолжение обводки: родная 1-пиксельная обводка панели задач под открытой выпадашкой стирается и заменяется контуром выпадашки с прямыми горизонтальными "хвостами" по 2 DIP с каждой стороны (сшить сглаженную дугу Direct2D встык с пиксельной линией без темной точки на кончике иначе нельзя).
- Переход в Mica: у самого стыка фон выпадашки равен цвету кромки панели, а через 56 DIP плавно переходит в Mica (текущие обои рабочего стола, размытые один раз в разрешении 1/8 экрана и тонированные цветом панели до коэффициента 0,65 по разнице светлоты OKLab). Сверху накладывается виньетка с турбулентным краем (
Turbulence+DisplacementMap) и плитка зерна 128x128. Mica и затенение сшиваются в один слой, чтобы две отдельные альфа-маски не давали светлую кайму от двойного сглаживания.
4. Нетривиальные инженерные решения
Текст и шрифты
- Вшитый шрифт без расхода RAM: подмножество шрифта Inter (180 КБ вместо 410 КБ) вшито прямо в
.exe. СтандартныйCreateInMemoryFontFileReferenceв DirectWrite копирует буфер шрифта в частную кучу процесса; вместо этого реализован свой загрузчик с передачей объекта-владельца — DirectWrite читает глифы напрямую из отображенного в память образа.exeс диска (0 байт в private working set). - Свес глифов и табличные цифры: ширина текста считается с учетом overhang-метрик (чтобы дата не наезжала на наклонные или жирные цифры), а моноширинные цифры перемеряются строго после применения OpenType-фичи
tnum. При прокрутке минут строка раскладывается целиком и режется по ячейке символа, чтобы цифры не двоились.
Ввод, фокус и перехват клавиш
- Лаунчер и фокус: в покое у окна стоит
WS_EX_NOACTIVATE+ возвратMA_NOACTIVATEпо клику, чтобы клик по плееру не уводил фокус из активного окна пользователя. Когда открывается поиск или меню с вводом, стильWS_EX_NOACTIVATEвременно снимается, а по нажатиюEscфокус возвращается тому окну, где был. - Одиночный
Altв поиске: в Win32 нажатиеAltпереводит окно в режим системного меню, и следующие буквы летят какWM_SYSCHARсо звуком ошибки. В лаунчереSC_KEYMENUгасится, аWM_SYSCHARбез зажатогоAltобрабатываются как обычный ввод. - Наследование режима IME: чтобы при каждом открытии поиска китайская/японская раскладка не сбрасывалась на латиницу, состояние IME и режим конверсии копируются у окна, из которого пришел пользователь (читаются через его окно IME сообщением
WM_IME_CONTROL). - Одиночный
Winдля своего "Пуска" (в разработке): модуль перехватывает только отпускание клавишиWin, если между нажатием и отпусканием не нажимались другие кнопки. Чтобы сама Windows не открыла стандартный "Пуск", перед отпусканиемWinв систему синтезируется нажатие пустой клавиши-маски0xE8(прием из AutoHotkey — система считает это сочетанием клавиш и молчит). А чтобы всплывающая системная подсказка кнопки "Пуск" (XAML-попапexplorer.exe) не перекрывала наше меню, модуль отслеживает появление топмост-окон проводника над координатами кнопки и временно скрывает их.
Звук, терминал и пипетка
- Как найти процесс, который реально играет звук: у современных браузеров звук выводит дочерний аудио-процесс. Ядро берет
AppUserModelIdиз медиасессии GSMTC, находит окно с этим идентификатором (или сопоставляет путь к.exe), берет его PID и запускает WASAPI process loopback сразу на все дерево дочерних процессов — без хардкода списков вроде"spotify.exe". - Отслеживание команд в ConPTY: Windows Terminal и VS Code работают через псевдоконсоль ConPTY, поэтому обычные консольные события оттуда не приходят. Остров включает слежение в момент, когда пользователь уходит из окна терминала: находит в дереве процессов терминала запущенные оболочки и их дочерние задачи и вешает ожидание завершения через
RegisterWaitForSingleObject. - Живая пипетка без клика сквозь окно: в Windows полностью прозрачное окно (
alpha = 0) пропускает клики мыши сквозь себя, поэтому экранная ловушка пипетки заливается альфой1/255, а сама лупа исключается из захвата черезWDA_EXCLUDEFROMCAPTURE+CAPTUREBLT, чтобы не увеличивать саму себя. Выход насыщенности за пределы охвата sRGB при генерации палитры OKLCH отсекается бинарным поиском.
5. Карта исходного кода
src/
main.rs окно, цикл сообщений, таймеры, фокус, связывание подсистем
island/ отрисовка и поведение острова
mod.rs раскладка, выпадашки, хит-тест, всплытие уведомлений
kit.rs кисти, тема, текст (кэш раскладок), Frame, бегущая строка
tone.rs математика цвета OKLab/OKLCH, контраст APCA, плашки, vivid
zone.rs правая зона уведомлений: стек, очередь 3 с, серии, стопка "+N"
logo.rs знак Polynya, глиф Claude, иконки уведомлений и приложений
scroller.rs отдельная DComp-поверхность для аппаратной бегущей строки
segments/ сегменты капсулы: плеер, время, погода
panels/ экраны выпадашек: плеер, погода, уведомление, стопка, лаунчер, модули
launcher/ поиск: приложения, файлы (Everything), калькулятор, валюты, нечеткий поиск, недавние
look.rs снимок панели задач (BitBlt + медианный фильтр кромки + шум)
taskbar.rs геометрия, ориентация и отслеживание панели задач
wallpaper.rs слежение за сменой обоев и генерация подложки Mica
media.rs работа с GSMTC медиасессиями
audio.rs WASAPI process loopback и расчет 5 полос спектра
weather.rs клиент Open-Meteo, системная геолокация и обратный геокодинг Nominatim
terminal.rs отслеживание долгих команд в дереве процессов терминала
locks.rs индикация Caps/Num Lock через Raw Input
ipc.rs сервер именованного канала
modules.rs менеджер модулей: разбор module.toml, запуск, stdin/stdout, пакетные модули
package.rs установка и проверка sparse-пакета Polynya.Notify
settings.rs встроенные экраны настроек
config.rs чтение и запись config.ini
i18n.rs таблицы локализации интерфейса (ru, en, zh, ja)
install.rs перенос бинарника и модулей в %LOCALAPPDATA% (кроме портативной версии)
autostart.rs управление ключом автозапуска в реестре
gfx.rs инициализация D3D11 / D2D1 / DComp, эффекты и прогрев шейдеров
shape.rs геометрия вогнутого выступа выпадашки и хвостов обводки
spring.rs физика пружинных анимаций
icons.rs встроенные векторные иконки и парсер SVG-путей в Direct2D
cli.rs разбор аргументов командной строки (push, dismiss, refresh, quit, uninstall и др.)
proto/ общий крейт протокола (MIT): типы сообщений, клиент пайпа, атомарный логгер
modules/ исходники встроенных модулей: agents, notify, picker, start
tools/ скрипты сборки пакета (package.ps1) и генерации логотипов (logo.py, ascii.py)
build.rs генерация VERSIONINFO из git describe и вшивание ресурсов в exe