Nihil: SPEC
Nihil: SPEC
Section titled “Nihil: SPEC”Wayland-композитор с 3D-миром вместо рабочего стола.
Название: Nihil (сокращение ENW; по духу Ex Nihilo, «из ничего»). Как у LLVM, имя можно не расшифровывать.
Метки в документе:
- ✅ решено тобой
- 💡 предложено в обсуждении, не утверждено (кандидат)
- ❓ открытый вопрос
1. Суть
Section titled “1. Суть”Полноценный Wayland-композитор, написанный с нуля (не плагин и не надстройка над Hyprland). Рабочий стол это 3D-мир, в котором можно ходить как в игре. Окна приложений это объекты в этом мире: их можно двигать, растягивать, фокусироваться на них. Ощущение: «отдельный мир, в котором ты живёшь, а рядом твои окна». Пример сценария: сел в кресло, смотришь YouTube в браузере, встал, пошёл играть.
Вдохновение: Hypr3D, Starling, Apple visionOS.
2. Принятые решения ✅
Section titled “2. Принятые решения ✅”Правило документации (✅, 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 | Слои по глубине (детали не описаны) |
3. Рендер и чёткость
Section titled “3. Рендер и чёткость”Причины мыла и способы лечения (💡):
- Несовпадение масштаба текстуры и экрана. Решение: клиент рендерит в нужном разрешении (
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), конфиг остаётся декларативным.
5. Миры и карты
Section titled “5. Миры и карты”Идея ✅: поддержка карт, ходить по ним.
Кандидаты (💡):
- Мир это ассет (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: запись и воспроизведение траектории камеры (для демо).
7. Открытые вопросы ❓
Section titled “7. Открытые вопросы ❓”Рендерер: GLES или wgpu✅ решено: GLES для M0–M5, wgpu пересматривается на M6.Среда плагинов✅ решено: WASM (wasmtime).Маппинг пикселей клиента на единицы сцены✅ решено: метры + density на окно (см. раздел 2).- Нужен ли Portal Mode.
- Что именно значат Cluster Mode, Ambient Mode, Depth Layer (детали).
- Лицензия проекта.
- Формат манифеста мира.
- Где проходит граница «Lua декларирует / плагин исполняет».
Что делать, если не хватает видеопамяти при большом множителе рендера✅ решено: деградация или отказ загрузки, режим настраивается.- Масштаб мира и дефолт 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).
7.2 Что надо проверить
Section titled “7.2 Что надо проверить”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.
- Лицензии зависимостей.
7.3 Прочее
Section titled “7.3 Прочее”- Репозиторий, 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) не меняется. План:
- Chroix как UI-движок панелей/HUD/drag-оверлеев (nihil-core): Chroix в
headless CPU-бэкенде (он рабочий —
chroix_headless/VelloCpu, проверено аудитом) рисует UI в RGBA-буфер. - Nihil заливает буфер как GL-текстуру и рисует обычным квадом —
существующий путь, ноль новой архитектуры. Это даёт draw-path панелям
(сейчас его НЕТ —
WindowPanel/DragGhost/GridOverlay/SnapGuideживут в логике без рендера) и dogfood Chroix. - Дальше по плану:
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-автомат.
- Плагины.
- Миры/карты.
Не-цели (на старте)
Section titled “Не-цели (на старте)”- Совместимость с конфигами Hyprland.
- Физика окон.
- Все режимы сразу: сначала ядро, потом режимы.
Архитектура (💡)
Section titled “Архитектура (💡)”- Сцена как граф: окно это узел с позицией, поворотом, размером. Растягивание окна меняет размер плоскости и отправляет клиенту 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 самостоятельно работающий.
9. Правила для агентов
Section titled “9. Правила для агентов”- Решения с меткой ✅ не менять без явного согласия. Сомнения записывать в раздел 7.
- Идеи с меткой 💡 это кандидаты: реализовывать только по запросу или в рамках этапа.
- Не добавлять зависимость от Hyprland. База: Smithay.
- Скрытых лимитов быть не должно: опасные значения (например огромный множитель рендера) разрешены, но с предупреждением и подтверждением.
- Работать маленькими шагами, по этапам из раздела 8. Каждый этап закрывать проверяемым критерием приёмки, по возможности с тестами.
- Не рендерить то, чего не видно.
- Документировать API плагинов и формат конфигов по мере появления.
- Если требование неоднозначно, сначала записать вопрос в раздел 7, а не угадывать.
10. Для тебя: с чего начать
Section titled “10. Для тебя: с чего начать”Установить Hypr3D и записать наблюдения— сделано (см. 7.2, дамп~/h3d.dump).Выбрать вопросы 1 и 2 (рендерер и среда плагинов)— решено (см. раздел 2).- Сделать 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).
11. Промпт для агента
Section titled “11. Промпт для агента”Скопируй, подставь текущую задачу и отдай агенту вместе с этим файлом.
Ты работаешь над проектом Nihil: Wayland-композитором на Rust + Smithay,где рабочий стол это 3D-мир, а окна объекты в нём.Весь контекст в SPEC.md. Прочитай его целиком до начала работы.
Правила:1. Решения с меткой ✅ не менять без явного согласия.2. Идеи с меткой 💡 это кандидаты: делай их только если они нужны текущему этапу или есть прямой запрос.3. Зависимости от Hyprland нет. База: Smithay.4. Скрытых лимитов нет: опасные значения разрешены, но с предупреждением и подтверждением.5. Работай по этапам M0-M9 из раздела 8, закрывай каждый критерием приёмки.6. Если что-то неоднозначно, запиши вопрос в раздел 7, не угадывай.7. Не рендери то, чего не видно.8. Перед изменением архитектуры коротко опиши план и дождись подтверждения.
Текущая задача: <вставь этап или задачу>
Формат ответа: что сделано, что проверено, что осталось, новые вопросы.