Polynya

Активная разработка

Меню документации

Архитектура и внутреннее устройство 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, чтобы не сфотографировать само себя.

Из снимка извлекаются:

  1. Светлая или темная тема панели и системный акцент;
  2. Строка кромки (цвет пикселей у края панели по столбцам с медианным фильтром 48 px и сглаживанием 8 px — чтобы иконки или кнопка "Пуск" не давали цветовых пятен на стыке);
  3. Строка обводки (тонкая линия вдоль края панели, если разница светлоты ≥ 0,02);
  4. Зерно (шум) текстуры панели задач.

Никакого поллинга по таймеру нет — пересъемка срабатывает только по событиям (склеиваются дебаунсом в 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