Skip to content

Nihil: SPEC

Wayland-композитор с 3D-миром вместо рабочего стола.

Название: Nihil (сокращение ENW; по духу Ex Nihilo, «из ничего»). Как у LLVM, имя можно не расшифровывать.

Метки в документе:

  • ✅ решено тобой
  • 💡 предложено в обсуждении, не утверждено (кандидат)
  • ❓ открытый вопрос

Полноценный Wayland-композитор, написанный с нуля (не плагин и не надстройка над Hyprland). Рабочий стол это 3D-мир, в котором можно ходить как в игре. Окна приложений это объекты в этом мире: их можно двигать, растягивать, фокусироваться на них. Ощущение: «отдельный мир, в котором ты живёшь, а рядом твои окна». Пример сценария: сел в кресло, смотришь YouTube в браузере, встал, пошёл играть.

Вдохновение: Hypr3D, Starling, Apple visionOS.

Правило документации (✅, 2026-10-16): Starlight-сайт (docs-site/) всегда 1:1 с markdown-доками в репо. Markdown — истина, сайт только её отражает; контента «только на сайте» не существует. Новый док → сначала .md в репо, потом зеркалим в docs-site/src/content/docs/.

Тема Решение
Тип проекта Свой композитор целиком, не плагин к Hyprland
База Rust + Smithay (Wayland, буферы, ввод, вывод, GLES) + Bevy headless (логика, сцена, сеть) — см. «Логика и сцена»
Исследование Сначала поставить и попробовать Hypr3D, затем писать свой аналог
Подход «Как игра»: свободное перемещение, растягивание экранов, плагины для развития
Чёткость Картинка не должна мылиться. В Hypr3D мылится (по её оценке, из-за слабого рендера)
Фокус на одном окне Обязательная возможность (в Hypr3D её нет)
Суперсемплинг Множитель рендера на окно (пример: окно 640x320, x4, рендер 2560x1280). Максимум не ограничивается
Защита от перегруза Не лимит, а предупреждение и подтверждение при загрузке конфига
Фильтрация дальних окон Настраиваемая (в Hypr3D дальние окна сглаживаются билинейно или хуже)
MSAA Рассматривается как опция
Конфиги Lua-подобные, цепочка: .lua → plugin (название окна, regex, прочее) → статический AOT FSM, с AOT-оптимизацией. Всё настраивается через конфиги
Расширяемость Система плагинов в композиторе
Миры Поддержка карт: можно пойти погулять, например по морю
Название Nihil (сокращение ENW)
Рендерер GLES (штатный рендер Smithay) для M0–M5; wgpu пересматривается на M6 (миры, шейдеры). Сцена — pure Rust, renderer-agnostic
Среда плагинов WASM (wasmtime). Lua — только конфиги. API-контракт (WIT) фиксируется рано, реализация — M7. Внутренняя архитектура логики — Bevy plugins (см. «Логика и сцена»)
Логика и сцена Bevy headless (без рендерера): ECS, Transform-иерархии, расписания, плагины — вся «игровая» логика (окна, комнаты, якоря, movers, физика, сеть). Smithay — тонкий Wayland-слой (протоколы, буферы, ввод, вывод, GLES). Рендерер Bevy (wgpu) НЕ используется: GLES-рендер каждый кадр читает Transform/GlobalTransform из ECS. Попапы — дочерние сущности окна (иерархия Bevy решает привязку)
Единицы и модель сцены Мир в метрах: RH, Y-вверх, глаз 1.6 u, vfov 65°, grid-пол y=0 (модель снята с Hypr3D, см. 7.2). Окно: квад_м = logical_px / density. Density — поле окна, задаётся конфигом (regex-правило) или вручную; дефолт — конфиг (вопрос 10). Смена density меняет только квад вокруг центра: logical_px и буфер клиента не трогаются, configure не отправляется. Растягивание мышью меняет logical_px → configure клиенту. Density читается в момент рейкаста (не кэшируется). HiDPI: density считается в логических px, чёткость — буфер-scale и суперсемплингом (M3). «Растянуть в телевизор» = сменить density
Нехватка VRAM при суперсемплинге Деградация (автоснижение множителя + уведомление) или отказ загрузки конфига; режим настраивается в конфиге
Панели/шелл Первоклассные объекты сцены. Якоря: world (в комнате, как окна), head (HUD, едет с камерой), body/wrist (офсет от камеры ~(-0.25, -0.2, -0.5) м, вниз-влево, со сглаживанием следования — ощущение предмета, как wrist-панель в Quest). Показ/скрытие: и по клавише/жесту, и при взгляде в сторону панели — оба сразу. Панели участвуют в hit-test луча (кликабельны), их поза считается до рейкаста в том же кадре. Совместимость: layer-shell (DMS, waybar, eww, quickshell работают без изменений) + свой протокол nihil_shell_v1 для 3D-якорей
Сохранение раскладки Поза каждого окна (center, yaw/pitch/roll, density, якорь) пишется в state-файл; ключ app_id + initial_title (+ дизамбигуация дублей). Атомарная запись (tmp + rename) при отпускании drag, закрытии окна и выходе. Окно без правила появляется перед камерой на ~3 u, лицом к пользователю. Именованные раскладки («работа», «игры»): снимок всех поз, переключение по хоткею
Комнаты Room { name, windows, camera_spawn, bounds, grid }. Окно принадлежит одной комнате или закреплено «видно во всех» (как панель). Переход: портал в пространстве или хоткей. Раскладка = комната, лежит в файле под своим именем. Порталы между комнатами — 💡
Grid Snap Зажатие модификатора во время drag: позиция квантуется по ячейкам, углы — до шага (15°/90°), у стены нормаль берётся от стены; без клавиши — свободно. Призрак-превью целевой ячейки. Размер задаётся в ячейках (3×2), logical_px вычисляется через density (клиенту configure). Два типа сетки: плоскость на стене/полу и кластерная сфера вокруг якоря (как visionOS)
Визуальные эффекты drag При перемещении окна: виртуальная сетка (видимая), направляющие для выравнивания (snap к краям других окон), призрак целевой позиции. Всё отключаемо в конфиге
IPC CLI-интерфейс с первого дня: управление «игрой в композитор» извне (список окон, состояние, команды). Им же проверяем, что окна доходят до сцены
Хоткеи Общий механизм: правила на Lua → компилируются в DFA (единый автомат с правилами окон, раздел 4). Поддержка сложных последовательностей: double-tap, модификаторы, chords (Super+K затем C). По умолчанию: double-Shift → wofi --show drun. Всё настраивается в конфиге
Прозрачность окон RGBA-буферы клиентов с alpha-blending; настраиваемый множитель прозрачности на окно. Отличие от Hypr3D: пустые места — подложка, прицел игнорирует alpha < порога
Контроллеры движения Камера одна, движок подменяемый: трейт Mover { update(input, dt) -> Pose }. Режимы: freecam (WASD+мышь, без коллизий, Shift/Ctrl = ускорение/замедление), физика (скорость, гравитация, прыжок, трение, коллизия пола/стен; окна — не препятствия; coyote time ~100 мс, лёгкий head-bob; камера резкая, без сглаживания поворота), полёт с инерцией, орбита вокруг окна (колесо = радиус), телепорт к окну (список/поиск → плавный пролёт лицом на комфортной дистанции), крюк-рывок (луч цепляет квад и тянет). Уровни по высоте обслуживаются прыжком/крюком/полётом
Попапы xdg_popup привязан к родительскому кваду: рисуется на нём/перед ним с сохранением относительного положения и поворота родителя, не становится отдельным 3D-окном (в Hypr3D попапы отваливаются — у нас это баг, а не поведение)
Fullscreen-запросы Клиент просит fullscreen → переводим его квад в Immersive Focus (True FullScreen = 1px=1px, Framed = с полями), а не ломаемся. Это M2. В Hypr3D это крашит сессию
Прозрачные окна В Hypr3D все окна прозрачные (RGBA без подложки). У нас: если окно не объявило opaque region, оно рендерится поверх подложки (рамка/фон комнаты), а не прозрачным в никуда; прицел игнорирует пиксели с alpha < порога (порог в конфиге)
Захват экрана (Discord/OBS) Первоклассный: ext-image-copy-capture-v1 + свой xdg-desktop-portal бэкенд (PipeWire). Окно = отдельная текстура, поэтому стрим одного окна естественен. Свой протокол (N4) — второй потребитель той же текстуры. ✅ Гранулярность (2026-10-16): локальный захват (Discord/OBS, screencopy/ext-image-copy-capture/portal) — и окно, и весь экран, оба валидны; шаринг по сети (стрим мира другим) — только per-room или per-window, весь экран по сети НЕ шарим
Оверлеи Discord-оверлея на Linux нет, поэтому своё: HUD-якорь панелей + nihil_shell_v1 (любой оверлей поверх мира), layer-shell overlay-слой маппится в 3D
Панель управления окном Дочерняя сущность окна (Bevy ChildOf): заголовок + кнопки (pin, minimize, maximize, close), парящая рядом с окном (сверху/сбоку), настраиваемая (офсет, высота, размер, что показывать). Не привязана жёстко — лёгкое следование за окном, левитация. Кликабельна (участвует в hit-test)
VR-готовность Camera и RaySource — трейты с M0: источник позы головы (мышь/WASD сейчас, OpenXR/Monado 6DoF потом) и источник луча (центр экрана сейчас, контроллер/рука потом) подменяемы. Стереорендер и OpenXR — будущее, за пределами M0–M9
Мультиплеер Общие комнаты между машинами: compositor-agnostic протокол «присутствие + общие окна». Слои протокола: presence (позы/имена, 20–30 Гц, крошечный трафик), surface (описание окна, владелец, права), media (пиксели: приоритет — синхронизация состояния плеера (URL+таймкод+пауза) для «вместе смотреть YouTube» — хост может отойти, видео продолжится; иначе WebRTC/QUIC+AV1/H.264). Права: по умолчанию ничего не расшарено; расшаривание окна явное и по окну; гость видит только расшаренное; ввод гостю — отдельный флаг; у гостей нет доступа к хост-системе вообще, только к потоку. Первый внешний клиент — мод для Minecraft (область в мире = вход в комнату; мод знает только протокол, не Wayland)
Совместимость Протокол и конфиги проектируются расширяемыми с самого начала: версионирование, опциональные поля, capability-negotiation — новые фичи добавляются «как в игру», не ломая старые клиенты
Сетевой стек (N0, решено по исследованию) Транспорт: QUIC (quinn) — reliable streams для handshake/join/leave, unreliable datagrams для поз 20–30 Гц. Схема: FlatBuffers (append-only поля, union как реестр типов, zero-copy чтение; required поля запрещены — проверка в CI). Конверт: {min_version: u16, type: union_tag, payload} + Hello с versions/capabilities на первом стриме (пересечение возможностей, как SDP). Медиа: сначала sync-state (URL+таймкод+пауза, ~100 байт); пиксельный стрим позже — str0m (Sans-IO WebRTC) + GStreamer/VAAPI (H.264, затем AV1). Антипаттерны: serde_cbor (мёртв), TCP для живого видео, версия-на-релиз как в Minecraft

Режимы (твои названия) ✅

Section titled “Режимы (твои названия) ✅”
Режим Описание
Spatial Mode Основной режим: окна расположены в 3D-пространстве
Immersive Focus: True FullScreen Окно на весь монитор
Immersive Focus: Framed FullScreen Полный экран с небольшими полями по бокам, чтобы было видно, что ты в 3D
Focus WorkSpace Виртуальное пространство, в котором есть сфокусированное окно и можно открыть другие
Walk Mode Хождение по миру как в игре
Cluster Mode Группы окон (детали не описаны)
Portal Mode Под вопросом, сомнения в нужности
visionOS-подобный режим Приложения расставлены между собой, камера вращается перед ними
Game Mode Максимальная производительность для игры: True FullScreen + direct scanout, пауза фоновых эффектов (левитация, анимации, размытие), дальние окна без frame callbacks. Интеграция с Feral gamemoded (CPU governor, renice) через DBus. Включение по правилу (regex на app_id) или хоткеем
Grid Snap Привязка окон к сетке
Ambient Mode Окружение/атмосфера (детали не описаны)
Depth Layer Слои по глубине (детали не описаны)

Причины мыла и способы лечения (💡):

  • Несовпадение масштаба текстуры и экрана. Решение: клиент рендерит в нужном разрешении (wp_fractional_scale, wp_viewporter), на стороне композитора включены мипмапы и анизотропная фильтрация.
  • В Immersive Focus плоскость окна выравнивается с экраном без поворота и масштаба: 1 пиксель окна = 1 пиксель монитора.
  • MSAA сглаживает только края геометрии, содержимое текстур не трогает. Для окон основной инструмент это мипмапы + анизотропия + суперсемплинг. MSAA нужен для геометрии миров и краёв/рамок. Фильтр текстур и MSAA настраиваются отдельно.
  • Множитель рендера (суперсемплинг): клиенту объявляется масштаб xN, при показе картинка сжимается хорошим фильтром. Опция автоматического множителя от расстояния и угла, ручной режим как оверрайд.

Цена множителя: память растёт как N². Примеры:

  • 640x320 при x4 даёт 2560x1280, около 13 МБ на буфер.
  • 2560x1440 при x4 даёт 10240x5760, около 236 МБ на буфер.

Диалог подтверждения при загрузке конфига должен показывать такую оценку видеопамяти.

4. Конфиги ✅ (реализация 💡)

Section titled “4. Конфиги ✅ (реализация 💡)”

Цепочка: .lua → plugin (название окна, regex, прочее) → static AOT FSM.

Предлагаемая реализация (💡):

  • Lua выполняется один раз при загрузке и выдаёт декларативные данные (правила, regex, режимы), а не работает на каждое событие.
  • Из данных строится автомат. Regex превращаются в DFA (кандидат: крейт regex-automata, умеет сериализовать готовый автомат). Правила склеиваются в одну таблицу переходов.
  • Матчинг окна по заголовку или app_id это проход по готовой таблице без аллокаций.
  • Матчинг выполняется при мапе окна по app_id и initial title; результат закрепляется за окном до ремапа (живой title со счётчиками — Telegram и т.п. — не перезапускает правила). Матч по живому title — явная опция правила.
  • Горячая перезагрузка: пересборка автомата в фоне, атомарная подмена.
  • Динамическая логика живёт в плагинах (WASM), конфиг остаётся декларативным.

Идея ✅: поддержка карт, ходить по ним.

Кандидаты (💡):

  • Мир это ассет (glTF/GLB) плюс манифест: точка спавна, зоны, посадочные места.
  • Якоря для окон: «экран на стене», «стол», «кресло». Сел, и камера встала перед привязанным окном (это Immersive Focus), встал, и снова ходишь.
  • Ходьба: капсула с коллизиями (кандидат: rapier). Навмеш и физика окон не нужны на старте.
  • Одно окно можно показывать как телевизор в комнате, как плоскость в космосе или как экран монитора на столе.
  • Миры могут поставляться плагинами.

6. Идеи-кандидаты 💡 (не утверждены)

Section titled “6. Идеи-кандидаты 💡 (не утверждены)”
  • Camera Bookmarks: сохранение позы камеры, прыжок по хоткею с плавным пролётом.
  • Overview: вид сверху на всю сцену с миниатюрами.
  • HUD-слой: окна, привязанные к камере, а не к миру (мессенджер, часы, плеер, шелл).
  • Rooms (✅ перешло в раздел 2) и порталы между ними (💡, оправдывает Portal Mode).
  • Head tracking: параллакс по положению головы через вебкамеру.
  • Ambient + глубинная резкость: окна вне фокуса слегка размываются и тускнеют по расстоянию.
  • Density по читаемости: угловой размер окна 2·atan(max(w,h)/2 / dist) ниже ~15° — текст плохо читается; режим правила «читаемо с N метров» сам считает нужный density.
  • Launcher (wofi и подобные) как head-якорь панель по центру экрана с полупрозрачным фоном — «поверх мира», а не квад в пространстве. Требует layer-shell или nihil_shell_v1 (M5+). Проверено вживую: double-Shift → wofi рисуется как обычный toplevel-квад — работает, но выглядит чуждо.
  • Умный рендер: окна вне поля зрения не получают frame callback или обновляются реже; заморозка текстур дальних окон разгружает GPU/CPU при «всё открыто». Влияет на архитектуру цикла отрисовки, закладывать рано.
  • Camera Paths: запись и воспроизведение траектории камеры (для демо).
  1. Рендерер: GLES или wgpu ✅ решено: GLES для M0–M5, wgpu пересматривается на M6.
  2. Среда плагинов ✅ решено: WASM (wasmtime).
  3. Маппинг пикселей клиента на единицы сцены ✅ решено: метры + density на окно (см. раздел 2).
  4. Нужен ли Portal Mode.
  5. Что именно значат Cluster Mode, Ambient Mode, Depth Layer (детали).
  6. Лицензия проекта.
  7. Формат манифеста мира.
  8. Где проходит граница «Lua декларирует / плагин исполняет».
  9. Что делать, если не хватает видеопамяти при большом множителе рендера ✅ решено: деградация или отказ загрузки, режим настраивается.
  10. Масштаб мира и дефолт density: 200 px/u (парити с Hypr3D: глаз 1.6 м, окна 5–14 м, «зал») против 1000–1500 px/u («честные метры»: окно 1920 px ≈ 1.3–1.9 м, комната как комната). Влияет на эргономику Walk Mode и миров (M6). Дефолт задаётся конфигом в любом случае; для M0 — 200 (удобно сравнивать с дампом).

7.4 Предвычислимая динамика конфига 💡 (предложение Ники, 2026-10-14)

Section titled “7.4 Предвычислимая динамика конфига 💡 (предложение Ники, 2026-10-14)”

Вместо рантайм-колбэков пользователь заранее описывает обработку всех событий (или их игнор/прокидывание), и это компилируется AOT в статичную таблицу входа-выхода — машина Мили: (состояние, событие) → (действие, новое состояние). События композитора — конечный enum, пользовательские состояния объявлены явно, поэтому таблица конечна. Приоритеты правил — как в udev: числовой префикс имени (00-early … 99-late); конфликты правил с равным приоритетом и пересекающимися условиями — ошибка компиляции.

AOT-оптимизации: минимизация состояний (Хопкрофт), выкидывание недостижимых, дедуп action-блоков, в пределе dense-матрица state × event → action_id (lookup = одна индексация, ноль аллокаций). Бонус: гарантия завершимости даром — переход не вычисление, конфиг физически не может подвесить композитор.

Оценка (кратко, реализация не сейчас): это естественное развитие M4: hotkey-автомат уже timed-trie, тот же приём расширяется с алфавита «сканкоды» до полного enum событий композитора. Покрывает ~95% кейсов динамики без WASM; когда логики становится слишком много (взрыв состояний) — лечится иерархическими состояниями, капом с ошибкой компиляции «слишком жирно — пиши WASM-плагин (M7)» или явным ходом в WASM. Остаётся вопрос про события с параметрами (заголовок окна не влезает в конечный enum — потребуется ограничить парам-события отдельным классом с матчингом по правилам, а не по таблице). Влияет на дизайн M7: контракт событий плагинов стоит делать сразу совместимым с этим enum-ом.

7.1 Что надо ещё спланировать

Section titled “7.1 Что надо ещё спланировать”
  • Формат и API плагинов: события, доступ к сцене, права, версионирование.
  • Схема конфига: Lua-API для правил окон, режимов, параметров рендера, порогов предупреждений.
  • Модель сцены: граф узлов, единицы измерения (решено: метры + density на окно), система координат (решено: RH, Y-вверх, как Hypr3D), масштаб пикселей клиента (решено: logical_px / density).
  • Маппинг ввода: луч в плоскость в локальные координаты, фокус клавиатуры, захват курсора в Walk Mode.
  • Конечный автомат режимов: переходы между Spatial, Focus, Walk и другими, анимации камеры.
  • Формат мира и манифеста (glTF, зоны, якоря, точка спавна).
  • Структура Rust-воркспейса (например крейты core, render, config, plugin-api, world).
  • Тестирование: headless-бэкенд, скриншот-тесты на резкость.
  • Отладка и профилирование кадра.
  • Детали режимов Cluster, Ambient, Depth Layer и решение по Portal.
  • Формат state-файла раскладок и ключей окон (дизамбигуация дублей: cwd/команда, порядковый номер).
  • Lua-конфиг: черновик API — docs/CONFIG_DRAFT.md (💡). Открытые вопросы по нему — в разделе 9 этого черновика (13 штук: единый автомат или артефакт с несколькими; тайминги double-tap в DFA; приоритет правил при пересечении; reapply = "reload" как поправка к «матчинг только при мапе»; подтверждение при неинтерактивном старте; оценка VRAM по est_size или реальному буферу; политика неизвестных полей; конфликты хоткеев; приоритет state-файл > spawn > дефолт; имена клавиш/жесты; квота DFA; сетевые настройки; формат plugins[].config).
  • Hypr3D: поставить, записать, что именно мылится, какие фильтры используются, чего не хватает (фокус на одном окне) — сделано. Снимок модели снят с живого процесса (RH, Y-вверх, метры, 1u=100 logical px, vfov 65°, квады yaw/pitch/roll, roll не используется, pitch до 45°, 19/22 окон в кадре, дальние 7–20° угловых — причина мыла: текстуры в разы крупнее видимого размера при слабой фильтрации). Дамп: ~/h3d.dump. Вывод: модель Hypr3D берём как базу сцены, их рендер — не берём.
  • Smithay: актуальная версия, как импортируются dmabuf клиентов в GLES-текстуры, поддержка нужных протоколов (fractional-scale, viewporter), XWayland.
  • Мипмапы и анизотропия для часто обновляемых клиентских текстур: цена генерации мипмапов на лету.
  • Direct scanout для окна в True FullScreen при работающем 3D-композиторе.
  • regex-automata: сериализация DFA, ограничения по синтаксису, размер таблиц.
  • Выбор Lua для Rust (например mlua или Luau) и песочница для конфигов.
  • Как клиенты реагируют на увеличенный масштаб (целый buffer scale против дробного), нужен ли обход.
  • Реальные лимиты драйвера на размер текстуры при суперсемплинге.
  • Свободно ли имя Nihil (и ENW) на crates.io и GitHub.
  • Лицензии зависимостей.
  • Репозиторий, CI, структура документации.
  • Выбор лицензии проекта.
  • Список «потом»: идеи из раздела 6, которые не вошли в первые этапы.
  • Баг spawn-env (живое поле, 2026-10-16; ✅ пофикшен 2026-10-17): приложения, запущенные из Nihil (wofi/exec-ветка хоткея), наследовали env хост-сессии — WAYLAND_DISPLAY=wayland-0 указывал на сокет Hyprland, и Steam открылся в Hyprland, а не у нас. Ланчер обязан санитизировать окружение при спавне: подставлять наш WAYLAND_DISPLAY (наш сокет), вычищать чужие сессионные переменные (XDG_SESSION_* и т.п.). (Причина была шире одного WAYLAND_DISPLAY: set_var в main.rs подставлял наше имя, но DISPLAY (XWayland хоста — X11-приложения вроде Steam по умолчанию шли туда), WAYLAND_SOCKET (либвейленд предпочитает fd имени), и идентичность сессии XDG_SESSION_*/XDG_CURRENT_DESKTOP/HYPRLAND_* всё ещё утекали в ребёнка.) Решение ✅: чистая функция child_env(parent, socket_name) в nihil/src/spawn_env.rs: WAYLAND_DISPLAY заменяется на голое имя нашего сокета (state.socket_name — задаётся в Nihil::new при создании слушающего сокета; голое имя, не путь: клиенты резолвят его сами через XDG_RUNTIME_DIR); вычищаются WAYLAND_SOCKET, DISPLAY, XDG_SEAT, XDG_VTNR, XDG_CURRENT_DESKTOP, DESKTOP_SESSION и префикс-семейства XDG_SESSION_* / HYPRLAND_* (префиксы ловят и ещё не виденные переменные); остальное проходит байт-в-байт (PATH, HOME, TERM, XDG_RUNTIME_DIR, DBUS_*…). Спавн-сайт (hotkey exec в input.rs) применяет её как ПОЛНОЕ окружение ребёнка — env_clear() + envs(...): единственный источник правды о том, что наследуют дети, а не процесс-глобальный env. Юнит-тесты покрывают все ветки (замена, каждый вычищаемый var, pass-through, пустой env, не-UTF8 имена).

7.5 Chroix в Nihil ✅ (решение Ники, 2026-10-17; реализация — этап после M2)

Section titled “7.5 Chroix в Nihil ✅ (решение Ники, 2026-10-17; реализация — этап после M2)”

Решение ✅: Chroix встраивается в Nihil УЖЕ СЕЙЧАС — но как 2D UI-слой, не как рендерер окон. Квад-пайплайн окон (raw-GLES поверх Smithay, ✅ раздел 2) не меняется. План:

  1. Chroix как UI-движок панелей/HUD/drag-оверлеев (nihil-core): Chroix в headless CPU-бэкенде (он рабочий — chroix_headless/VelloCpu, проверено аудитом) рисует UI в RGBA-буфер.
  2. Nihil заливает буфер как GL-текстуру и рисует обычным квадом — существующий путь, ноль новой архитектуры. Это даёт draw-path панелям (сейчас его НЕТ — WindowPanel/DragGhost/GridOverlay/SnapGuide живут в логике без рендера) и dogfood Chroix.
  3. Дальше по плану: vello_gpu с external textures для zero-copy (движок это умеет: TextureBindings, рендер в чужой TextureView), потом Chroix получит 3D и станет полным рендерером.

Дополнение ✅ (2026-10-17): возможен бэкенд «Chroix-raw» — рендерит всё (GPU/CPU) и отдаёт результат куда скажем (буфер/текстура/куда угодно). Интерфейс подачи результата — ОБСУЖДАЕМЫЙ, не зашитый: встройка в Nihil должна принимать и буфер, и текстуру.

Сопутствующее ✅: формализовать backend-agnostic render-трейт — draw_3d сегодня единственный call-site рендера (state/winit.rs, Redraw-арм); GLES — первый бэкенд, Chroix — второй. Трейт описан требованиями: ~(windows: &[WindowDraw], camera: &CameraPose, viewport, breakdown) -> damage rects + readback для IPC-скриншота + телеметрия.

Разведка (2026-10-17, read-only аудиты): ~/.hermes/cache/scratch/chroix-port/chroix-audit.md (что умеет/не умеет Chroix: 3D/внешние текстуры/шейдеры — нет на уровне схемы; CPU-headless и vello_cpu — да; зрелость pre-alpha) и nihil-render-requirements.md (полный контракт текущего рендера: GL-фичи, б-в-текстуры, damage/скриншот/телеметрия, 9 точек сопряжения). Migрация рендера окон на Chroix остаётся 💡-кандидатом (критерий: пиксель-в-пиксель по IPC-дампу); встройка 2D UI-слоя — ✅, этап после M1/M2.

7.6 Привязка переключения Immersive Focus ✅ (решение Ники, 2026-10-17; реализовано)

Section titled “7.6 Привязка переключения Immersive Focus ✅ (решение Ники, 2026-10-17; реализовано)”

Решение ✅ (вариант 4 из предложенных): конфиг-рул в Lua через DFA-компилятор, бинд переназначаемый, дефолт SUPER+F, цикл True → Framed → off (с заворотом).

Реализация: секция immersive = { hotkey = "super+f", double_tap_ms = 600, cycle = { "true", "framed", "off" } } в конфиге; HotkeyEngine::compile добавляет её как обычное правило (immersive = true вместо exec — виды действий взаимно исключают) в ЕДИНСТВЕННЫЙ DFA (один автомат, одна политика конфликтов first-declared-wins). Буквы a–z разбираются в Key::parse по QWERTY-строкам evdev (f = 33, НЕ по алфавиту). Неизвестная запись цикла — ошибка конфига (опечатки не проходят молча). Компрессор: cycle_immersive() — целевое окно (фокус клавиатуры, иначе первое живое) → текущий режим → следующий по immersive.cycle. IPC immersive cycle — тот же путь для тестов (инъекция клавиш во вложенный композитор из хоста недоступна: wtype без virtual-keyboard, hyprctl dispatch сломан в 0.56.2). Проверено живьём: True (контент на все края, split в точности посередине) → Framed (поля фона ~4% сверху/снизу, 96% высоты) → off (квад вернулся) → True.

  • Работающий Wayland-композитор с 3D-сценой, хождением, растягиваемыми окнами.
  • Чёткая картинка: Immersive Focus без потерь, суперсемплинг, настраиваемая фильтрация.
  • Режимы из раздела 2.
  • Конфиги Lua → статический AOT-автомат.
  • Плагины.
  • Миры/карты.
  • Совместимость с конфигами Hyprland.
  • Физика окон.
  • Все режимы сразу: сначала ядро, потом режимы.
  • Сцена как граф: окно это узел с позицией, поворотом, размером. Растягивание окна меняет размер плоскости и отправляет клиенту configure с новыми размерами в пикселях.
  • Режимы фокуса это состояния камеры: камера плавно переезжает в нужную позу.
  • True FullScreen: плоскость заполняет экран; можно отдать клиенту direct scanout.
  • Разработка во вложенном режиме (winit-бэкенд), DRM/libinput-бэкенд позже.
  • Camera и RaySource — трейты с первого дня: мониторная реализация сейчас, VR (OpenXR/Monado, стерео по глазам) позже без переписывания сцены.
  • Мультиплеер — композитор-независимый протокол «presence + surfaces»: Nihil и Minecraft-мод общаются как равноправные клиенты. Транспорт и схему сообщений выбрать исследованием (WebRTC/QUIC, FlatBuffers/Cap’n Proto/Protobuf). Права: явное расшаривание окна, гость видит только расшаренное, ввод гостю — отдельный флаг. Порядок: presence → общее окно (синхронизация плеера) → Minecraft-мод → произвольный стрим окон.

Этапы и критерии приёмки (💡)

Section titled “Этапы и критерии приёмки (💡)”
# Этап Приёмка
M0 Вложенный композитор, одно Wayland-окно как текстура в 3D Обычный клиент (например терминал) виден на плоскости
M1 ✅ (2026-10-18) Камера + лучевой пик мыши ВЫПОЛНЕНО: клик/ввод попадают в нужное окно (рейкаст читает density в момент луча; зеркало-X закона UV совпадает в рендере и px-рейкасте, b46b682); попапы рисуются суб-квадами на плоскости родителя (configure-протокол → экранный рект совпал с математикой mirror-x, дампы shot27–31)
M2 ✅ (2026-10-18) Immersive Focus (True и Framed) + обработка fullscreen-запросов ВЫПОЛНЕНО: True пиксель-в-пиксель (logical_px=[960,540] → screen_rect [0,0,959,539], split точно по центру, дампы shot32–36); Framed = 96% покрытия (измерено); fullscreen-запрос → Immersive Focus (a75c147), не крашит; хоткей-цикл через DFA (§7.6, b472832)
M2A ✅ (2026-10-18) Chroix 2D UI-слой (§7.5): панели через Chroix headless → RGBA → GL-текстура → квад (d8b7643); backend-agnostic SceneBackend-трейт, draw_3d — единственный call-site (987290b) ВЫПОЛНЕНО: панель рисуется Chroix-контентом (дамп: грифельная полоса + 4 кнопки пиксель-в-пиксель); ChroixBackend компилируется за GlesBackend и явно отказывает до vello_gpu. Drag-оверлеи (ghost/grid/guides) — следующим срезом того же пути
M3 ✅ (2026-10-18) Суперсемплинг, мипмапы, анизотропия, настраиваемая фильтрация (ea50b62) ВЫПОЛНЕНО: множитель на окно работает — GTK4-клиенту объявлен preferred_scale(240) через wp-fractional-scale, буфер пришёл 900×700 при render_scale 2.00 (собственный IPC-дамп), маркер рендерится зеркально-корректно; VRAM-предупреждение при загрузке конфига (render_scale 5 > 4 → оценка ~396 МБ, NIHIL_UNSAFE_CONFIG глушит). Мипмапы работают на shm; на dmabuf (EGLImage) принудительно выключены с одноразовым warn — генерация цепочки на single-level текстуре оставляет её incomplete → чёрное окно (измерено по матрице: shm+mip ✓, dmabuf+mip ✗, dmabuf−mip ✓). Правило матча окон — одноразовое, но отложенное до первого коммита с identity (GTK шлёт app_id/title ПОСЛЕ map)
M4 Конфиги: Lua → правила → DFA Матчинг окон по regex через автомат; горячая перезагрузка без пропуска кадра
M5 Focus WorkSpace, Walk Mode, Grid Snap, Cluster Mode Ходьба с коллизиями, фокус-пространство открывает несколько окон
M6 Миры: glTF + манифест, якоря, «кресло» Сел в кресло, камера встала перед окном; встал, ходишь
M7 Плагины: API и загрузка Плагин добавляет мир или правило без пересборки композитора
M8 DRM/libinput бэкенд, мультимонитор, XWayland, захват экрана (ext-image-copy-capture + portal/PipeWire) Запуск как реальной сессии; Discord/OBS видят и стримят окна и экран
M9 visionOS-режим, Ambient, Depth Layer, Portal (если нужен), Game Mode По описаниям режимов; Game Mode: direct scanout + пауза фоновых эффектов + gamemoded
N0 Транспорт и схема протокола Выбор медиа-канала (WebRTC/QUIC/QUIC-datagram), presence-канала и формата сообщений (FlatBuffers/Cap’n Proto/Protobuf) с forward/backward совместимостью; зафиксировать спеку до кода
N1 Presence + общая комната Два клиента (или композитор + тестовый клиент) видят позы и имена друг друга в общей комнате
N2 Общее окно (синхронизация плеера) Совместный просмотр YouTube: синхронизация URL/таймкода/паузы, без пиксельного стрима
N3 Minecraft-мод Вход в область мира = регистрация в комнате; окно как квад с динамической текстурой; позы игроков видны в обе стороны
N4 Стрим произвольных окон Любое окно композитора стримится в общую комнату (str0m/WebRTC + VAAPI), права гранулярные: view / seek / control. Обратный ввод гостя → приложение хоста по флагу

Умный рендер (frame callbacks по видимости) закладывать уже в M0-M1.

Сеть: спеку протокола (N0) зафиксировать до написания клиентов; каждый этап N1–N4 самостоятельно работающий.

  1. Решения с меткой ✅ не менять без явного согласия. Сомнения записывать в раздел 7.
  2. Идеи с меткой 💡 это кандидаты: реализовывать только по запросу или в рамках этапа.
  3. Не добавлять зависимость от Hyprland. База: Smithay.
  4. Скрытых лимитов быть не должно: опасные значения (например огромный множитель рендера) разрешены, но с предупреждением и подтверждением.
  5. Работать маленькими шагами, по этапам из раздела 8. Каждый этап закрывать проверяемым критерием приёмки, по возможности с тестами.
  6. Не рендерить то, чего не видно.
  7. Документировать API плагинов и формат конфигов по мере появления.
  8. Если требование неоднозначно, сначала записать вопрос в раздел 7, а не угадывать.

10. Для тебя: с чего начать

Section titled “10. Для тебя: с чего начать”
  1. Установить Hypr3D и записать наблюдения — сделано (см. 7.2, дамп ~/h3d.dump).
  2. Выбрать вопросы 1 и 2 (рендерер и среда плагинов) — решено (см. раздел 2).
  3. Сделать M0 и M1. Следующий шаг.

10.1 Большой сдвиг (сессия 2026-10-13)

Section titled “10.1 Большой сдвиг (сессия 2026-10-13)”

Проект расширен: Nihil перестаёт быть только «композитором с 3D» и становится «композитором как игрой» с сетевым присутствием и общими комнатами между машинами. Ключевые решения:

  • Плагины и миры остаются, но рядом появляется compositor-agnostic протокол (presence + общие окна), чтобы можно было встречаться в комнате независимо от того, что у каждого стоит (Nihil, Minecraft-мод, другой шелл).
  • Для «вместе смотреть YouTube» приоритет — синхронизация состояния плеера, а не стрим пикселей.
  • Совместимость по протоколу и конфигам — базовое требование: всё проектируется расширяемым, фичи добавляются «как в игру» без поломки старых клиентов.
  • Bevy рассматривался как движок для мира, но по фактуре: импорт чужих dmabuf в wgpu-hal — unsafe-обход без гарантий, поэтому композитор остаётся на Smithay + GLES, а «игровые» штуки (комнаты, движение, физика, сеть) живут в сцене/логике поверх, а не в отдельном движке. Моды/совместное присутствие — через протокол, не через движок.
  • Главный риск проекта — объём: фактически три проекта в одном (композитор, протокол, мод). Ответ: спека протокола раньше кода и маленькие этапы с критериями приёмки (раздел 8).

Скопируй, подставь текущую задачу и отдай агенту вместе с этим файлом.

Ты работаешь над проектом Nihil: Wayland-композитором на Rust + Smithay,
где рабочий стол это 3D-мир, а окна объекты в нём.
Весь контекст в SPEC.md. Прочитай его целиком до начала работы.
Правила:
1. Решения с меткой ✅ не менять без явного согласия.
2. Идеи с меткой 💡 это кандидаты: делай их только если они нужны
текущему этапу или есть прямой запрос.
3. Зависимости от Hyprland нет. База: Smithay.
4. Скрытых лимитов нет: опасные значения разрешены, но с предупреждением
и подтверждением.
5. Работай по этапам M0-M9 из раздела 8, закрывай каждый критерием приёмки.
6. Если что-то неоднозначно, запиши вопрос в раздел 7, не угадывай.
7. Не рендери то, чего не видно.
8. Перед изменением архитектуры коротко опиши план и дождись подтверждения.
Текущая задача: <вставь этап или задачу>
Формат ответа: что сделано, что проверено, что осталось, новые вопросы.