Воксельный движок, день первый
Это новый проект, и этот пост — его первый день. Последние четыре поста в этом блоге были о том, как читать Diablo II из её исполняемого файла. А этот пост о новом проекте: воксельный движок на Rust, где мир — маленькие кубики, сквозь которые можно копать, а рисуется он проходом лучей по вокселям (ray marching) в compute-шейдерах.
Движок начался 16 октября в 08:51 с пустого репозитория: лицензия, дорожная карта и список бюджетов. К 15:20 был закрыт первый план: ограниченный мир из вокселей, детерминированные правки, которые сообщают, что изменили, и окно с пещерой, освещённой налобным фонарём, по которой можно летать и в которой можно копать. К 17:29 был закрыт второй план: пещера теперь не имеет края и подгружается вокруг камеры, а за сферой полной детализации идёт кольцо пониженной. По ходу дела измерили каждый бюджет, до которого код дня мог дотянуться, на моём ноутбуке, а не на той карте, под которую бюджеты написаны, и три из них не выполнены, и так и записано: стоимость правки, время CPU на подгрузку и первичные лучи, как только пещера стала подгружаться.
Требования до движка
Движок делается вместе с первой игрой, о которой в этом посте речи не будет. Важно другое: требования игры написаны до движка, чтобы ранние решения движка не перекрыли ей дорогу, и они задают масштаб. Уровни примерно 128 × 128 × 48 метров при вокселе 10 см, в масштабе Teardown, это около 1280 × 1280 × 480 ячеек на уровень. Правки сферой, коробкой, капсулой и инструментом вдоль луча. И одно требование документ называет не подлежащим обсуждению: каждая правка возвращает игре число убранных вокселей по каждому материалу и где именно. Ещё там сказано, что генерация должна быть чистой функцией сида, что арифметика выкапывания должна быть целочисленной, чтобы сервер с клиентом сходились бит в бит, и что любой игровой запрос должен идти по грубой сетке, которая никогда не зависит от лучей рендера или звука.
Решения до кода
Движок — это Cargo workspace из библиотечных крейтов, и README прямо оговаривает, чем движок не является: «Нет ECS, нет редактора сцен и нет конвейера ассетов, кроме импорта воксельных моделей. Игра собирает крейты, которые ей нужны, и сама владеет своим циклом и моделью сущностей». Крейты на сегодня:
| Крейт | За что отвечает |
|---|---|
vxe-world | Хранение вокселей, материалы, операции правки и их результаты, пространственные запросы, подгрузка. Headless и детерминированно: ни GPU, ни часов, ни ГСЧ без сида. |
vxe-render | Загрузка в wgpu, ray marching в compute-шейдерах, освещение. Весь WGSL живёт здесь. |
vxe-app | Тонкая, необязательная оболочка на winit: окно, ввод, поверхность, цикл кадра. |
apps/flythrough | Собственный стенд движка: летать, копать, читать результаты правок. |
Первое архитектурное решение, ADR-0001, самое важное: Rust, wgpu и ray marching в compute-шейдерах без аппаратного ray tracing. Лучи — это проходы DDA, обход сетки по одной ячейке вдоль луча, по карте кирпичей. Довод такой: RT-ядра ускоряют иерархии треугольников, а воксели — не треугольники: «Воксели и аппаратный ray tracing плохо подходят друг другу». Четыре альтернативы отвергнуты и записаны. Нативный Vulkan через ash с аппаратным RT: вокселям от него мало пользы. Плагины Bevy: «модель приложения и расписания Bevy воюет с детерминированным авторитетным серверным циклом». Мир только ограниченного размера: если добавлять подгрузку к ограниченной карте кирпичей потом, придётся переписывать адресацию чанков. И C++: инструменты, харнесс и опыт продакшена у меня в Rust. В ADR названа и цена: ни DLSS, ни вендорского шумоподавителя, и это «самый большой технический риск в дорожной карте».
Rust и wgpu — не прыжок в неизвестность. Ritmolux, музыкальный визуализатор, работает на Rust, wgpu и winit на трёх платформах, а харнесс из планов и ADR, по которому живёт этот репозиторий, — копия того, по которому живёт тот. Два плана, написанных этим вечером, копируют код из Ritmolux напрямую: таймер GPU-проходов и кольцо статистики кадров для HUD стенда, и два гейта репозитория. В плане так и написано: та же лицензия, тот же владелец, «так что его код копируется без церемоний».
Два решения ушли в дорожную карту, а не в ADR. Вид поверхности: гладкая, естественная порода, а не видимые кубики, а ориентир — собор в Шибенике на иллюстрации из статьи Лайне и Карраса 2010 года про разреженные воксельные октодеревья. Дорожная карта честно говорит, что это за картинка: «воксели 5 мм с уникальным цветом и предрассчитанным ambient occlusion на каждый воксель, 2,7 ГБ на GPU: в двадцать раз больше нашего разрешения и данные для затенения, которых мы не храним». Как к этому прийти, сглаженными нормалями, контурными плоскостями или процедурной детализацией, решит измерение, в пробе, которая теперь называется план 0005. Второе решение — первая веха: полёт по пещере с копанием, построенный на headless-крейте мира, который тестируется без окна. Словами самого плана: «владелец хочет пиксели до того, как начнутся споры о механизмах (сглаживание, контуры)».
Потом бюджеты, в файле nfr.md. Базовое железо — настольный компьютер среднего класса 2019 года: карта уровня GTX 1660 или RX 5600 с 6 ГБ, четыре ядра, 16 ГБ памяти, Vulkan 1.2 и никакого RT-железа. Этой карты у меня пока нет; в файле сказано, что её нужно купить или одолжить до закрытия вехи по освещению, а до тех пор числа для базовой карты — оценки. Над каждым числом в файле одно правило: «предложение, пока его не заменит измерение». Бюджет кадра делит 16,6 мс при 1080p на подгрузку и загрузку в GPU 1,0 мс, первичные лучи 3,0 мс, освещение 6,0 мс, шумоподавление 3,0 мс, апскейл и пост 1,5 мс и 2,1 мс запаса. Память: 3 ГБ видеопамяти на мир, 4 ГБ ОЗУ. Правки: 100 выкапываний радиусом в метр за 2 мс процессорного времени. Подгрузка: чанк становится резидентным за 500 мс после того, как понадобился. Старт: первый кадр быстрее 3 с.
План 0001: ограниченный мир и пещера в окне
У первого плана восемь фаз и одна главная строка: «сфера радиусом 10 вокселей, вырезанная из сплошного материала 3, возвращает {3: 4169}». Это вся идея крейта мира в одном assert. Правка — это форма, режим и инструмент; обратно приходит счётчик по каждому материалу, список затронутых чанков и ограничивающий бокс. Все числа целые.
Устройство хранения описано в ADR-0002. Воксель — 16-битный идентификатор материала, ноль для воздуха. Кирпич — 8 × 8 × 8 вокселей, и он либо Uniform(material), либо Full(Box<[u16; 512]>), и полный кирпич схлопывается обратно в однородный, когда правка делает его таким. Чанк — 8 × 8 × 8 кирпичей, то есть 64 × 64 × 64 вокселя, с маской изменённых по биту на кирпич. Позиции внутри правки — с фиксированной точкой, 1/256 вокселя. Хранилище — карта чанков под блокировками, и правка блокирует затронутые чанки по одному в порядке возрастания координат, предварительно проверив, что все они резидентны, то есть загружены в память, так что правка либо проходит целиком, либо целиком отвергается. Отвергнутые альтернативы: разреженное воксельное октодерево или DAG, потому что правка перестраивает поддерево; неизменяемые чанки с копированием при записи, потому что сто правок за кадр копируют мегабайты ещё до начала обхода; и один поток-владелец с сообщениями, потому что тогда каждый звуковой зонд — это запрос в чужой поток и ожидание ответа.
Пещеру генерирует сам стенд; движок ландшафта не поставляет. Это целочисленный value noise: каверны на решётке в 64 вокселя плюс детали на решётке в 16 вокселей с весом в одну восьмую, порог делит на породу и воздух, третий шум даёт рудные жилы, два вокселя неразрушимой оболочки внутри каждой грани уровня и карман воздуха в три метра у точки появления. Каждое значение — чистая целочисленная функция сида и координаты, так что чанки выходят одинаковыми в любом порядке, в любом потоке и на любой платформе. Решётки выровнены по кирпичам, и это даёт дешёвый трюк: внутри одного кирпича каждая октава трилинейна в одной ячейке, так что если все восемь углов кирпича лежат по одну сторону порога, кирпич однородный и выдаётся без вычисления своих 512 вокселей.
Всё это закрепляет тестовый вектор детерминизма. Двадцать четыре правки на пещере по сиду, каждая форма и каждый режим, одна режет оболочку, одна отвергается, потому что чанк не резидентен, одна с отрицательным радиусом, и хеш над миром до, каждым результатом и миром после: 0x2b6b_6a0d_1f9b_8d75. В тесте написано: «Закреплён на Linux x86_64. Никогда не ослабляется, чтобы прошла другая платформа», и CI гоняет его на Linux и Windows. Второй тест ищет в исходниках пещеры и шума f32, f64, sin, exp, powf и любой литерал вида цифра-точка-цифра. В крейте мира в генерации вообще нет плавающей точки, а clippy.toml вдобавок запрещает системное время.
Обход
Рендерер зеркалит мир в буферы GPU: пул кирпичей, 64-битная маска занятости на кирпич, по биту на блок 2 × 2 × 2, по записи на каждый чанк и таблица чанков. Compute-шейдер ведёт луч через ячейки четырёх размеров:
flowchart TB
ray["луч входит в чанк"] --> c{"чанка нет или он однородный воздух?"}
c -->|да| skip64["перешагнуть 64^3 как одну ячейку"]
c -->|нет| b{"кирпич однородный?"}
b -->|воздух| skip8["перешагнуть 8^3 как одну ячейку"]
b -->|сплошной| hit1["попадание там, где луч входит в кирпич"]
b -->|полный| m{"блок 2^3 пуст по маске?"}
m -->|да| skip2["перешагнуть 2^3 как одну ячейку"]
m -->|нет| vox["шагать по вокселям блока по одному"]
vox --> hit2["первый воксель не из воздуха — попадание"]
Маски занятости пришли последними, фазой 8, и именно они делают первичные лучи дешёвыми: при 1080p из точки появления проход лучей занимал от 1,99 до 2,01 мс без них и от 1,58 до 1,59 мс с ними. В списке литературы, собранном в дорожной карте тем утром, около тридцати источников, и первый вывод из них такой: пустоту пропускать по битовым маскам. Второй: упираемся в пропускную способность памяти.
Освещение — один налобный фонарь. Лампа сидит на 25 см правее и на 15 см ниже глаза, с конусом и затуханием по обратному квадрату, и один теневой луч на пиксель от точки в полвокселя над гранью попадания к лампе. Эталонная картинка 320 × 180, в проекте она называется golden, отрисованная на программном драйвере Vulkan из Mesa, lavapipe, закрепляет результат. CI сравнивает с ней каждую сборку на Linux, и не больше одного пикселя из тысячи может отличаться больше чем на 2/255 в любом канале.
Пещера в конце первого дня
Отрисовано headless в 1280 × 720 на Radeon сборкой плана 0002, ограниченный уровень, сид 1, налобный фонарь с теневым лучом. Тёмное кольцо — край конуса лампы; кроме лампы пещеру ничто не освещает.
Кадры сделаны маленькой программой-примером, написанной для этого поста поверх публичного API движка: сгенерировать, синхронизировать зеркало, пустить лучи, прочитать текстуру обратно. Единственный снимок кадра, который делает сам движок, — эталонный тест 320 × 180.
Две остановки и чему они научили
Две фазы остановились на своём условии готовности, том условии, которое фаза должна выполнить, прежде чем считаться сделанной. Обе есть в логе плана, потому что правило лога такое: «Наблюдения, никогда не выводы: ни списка пройденного, ни самооценки. Об отклонениях и невыполненных условиях готовности сообщается всегда».
Фаза 6, теневой луч. Условие было: не меньше одного процента пикселей становятся ярче, когда теневой луч выключают. Измерение дало 0,31 процента. В логе объяснено, почему: стены в 4,4–5,7 метра от глаза, и смещение лампы на 29 см на таком расстоянии почти не даёт параллакса. То есть условие измеряло сцену, а не тень. Поправка оставляет правило, что без теней ни один пиксель не может стать темнее, и переносит доказательство «тени существуют» в построенную сцену: воздух, стена из породы на x от 44 до 46, плита на x от 28 до 30, и камера, поставленная так, что ближняя грань плиты затеняет на стене полосу в форме буквы L. Размер полосы посчитан из геометрии: «От 820 до 840 пикселей (830 по расчёту)». Измерение дало 823. В поправке добавлено: «Если число выйдет за эти пределы, теневой луч неверен. Остановиться и доложить; диапазон не расширять».
Фаза 8, маски занятости. Условие было: эталоны с масками остаются бит в бит. Не остались: 17 пикселей сдвинулись на одной картинке, 15 на другой (в заметке об остановке стояло 19 и 23, но её собственные проценты дают 17 и 15), все одиночные, без скоплений. Диагноз в логе: пропуск блока доходит до границы другим путём в плавающей точке, чем шаги по вокселям внутри него, так что в углу, где оба пути дают одно и то же расстояние, округление может уйти в другую сторону. Поправка заменила правило «бит в бит» тремя: с выключенными масками эталоны фазы 7 должны воспроизводиться байт в байт (воспроизвелись, 0 пикселей); с включёнными могут отличаться не больше 57 пикселей; и никакие два отличающихся пикселя не могут быть соседями по восьми направлениям, потому что настоящая ошибка в маске открывает дыру размером с блок, а не с пиксель. Из тех 17 и 15 соседних не было. В логе записана и контрольная проверка: нарочно переставленный индекс бита дал 6 604 отличающихся пикселя, и тест упал.
Копание: операции правки стенда
Каждая операция вернула свои счётчики: туннель убрал 54 469 вокселей породы и 135 руды в 16 чанках; полость — 53 805 и 65; колонна заполнила 2 900 вокселей воздуха.
Числа при закрытии
Ревью при закрытии плана идёт в свежей сессии и читает каждый assert, который план назвал. Что оно записало:
- Правки. Бенчмарк, 100 выкапываний радиусом в метр, в каждом ровно 4 169 вокселей: 4,92 мс процессорного времени против бюджета в 2 мс. Не выполнено. Бюджет остаётся, это моё решение при закрытии, и план, как до него дойти, написан тем же вечером.
- Первичные лучи при 1080p: 1,59 мс с масками против 3,0 мс. Уложились.
- Хранение пещеры размером с уровень, 20 × 20 × 8 чанков, 128 × 128 × 51 метр: 3 200 чанков, 383 608 полных кирпичей и 1 254 792 однородных. Доля полных — 23,4 процента; дорожная карта предполагала меньше десяти. 400 МиБ в ОЗУ и 384 МиБ в зеркале на GPU. Уровень генерируется за полсекунды.
- Первый кадр: 2,2–2,3 с на холодном старте, 1,1–1,2 с на тёплом, против 3 с. Одно только создание устройства занимает из них 1,8–1,9 с.
Ограничения адаптера оказались важнее, чем ожидалось. Lavapipe не даёт привязать буфер хранения больше 128 МиБ, а пул кирпичей уровня при 16 битах — около 375 МиБ, так что ничто на lavapipe, а именно он есть у CI, уровень вместить не могло. Из этого выросла работа второго плана над палитрами и шардами.
План 0002: пещера без края
Второй план делает мир неограниченным. Пещера подгружается вокруг камеры, полная детализация вблизи, пониженная дальше, и то, что вы выкопали, остаётся выкопанным, когда вы возвращаетесь.
flowchart TB
f["точка фокуса: чанк камеры"] --> full["полный ярус: чанки в пределах r_full = 16, все воксели резидентны (102 м)"]
f --> lod["ярус LOD: чанки в пределах r_lod = 24, резидентны только мипы (154 м)"]
f --> out["дальше r_lod + 1: выгружается"]
full --> gen["пул рабочих потоков генерирует или загружает чанки, ближние первыми"]
lod --> gen
out --> keep{"его правили?"}
keep -->|да| ram["хранится в ОЗУ; сверх лимита сбрасывается в файл с контрольной суммой"]
keep -->|нет| drop["выбрасывается; источник сгенерирует заново"]
ADR-0003 выбрал для резидентности сферы, а не кубы, в целых единицах чанков с гистерезисом в один чанк, и приводит арифметику: «Сферы держат pi / 6, около 52 %, чанков описанного куба». Чанк только с LOD хранит три собственных мипа, тот же чанк в половинном, четвертном и восьмом разрешении, 32³, 16³ и 8³ ячеек, построенных по правилу большинства, где при равенстве берётся меньший идентификатор материала, а любой сплошной потомок делает ячейку сплошной, так что мип никогда не открывает дыру в стене. Мипы — того же типа Brick, что и воксели, так что шейдер обхода идёт по ним тем же кодом с увеличенным размером ячейки. Держать полные воксели кольца в ОЗУ отвергли, посчитав: около 4,8 ГБ.
Сторону GPU описывает ADR-0004. Полный кирпич в зеркале — больше не 512 16-битных значений, а 512 однобайтовых индексов в палитру чанка до 255 материалов, а чанку, которому нужно больше материалов, достаётся запасной широкий кирпич из двух слотов. Зеркало уровня ушло с 384 МиБ на 198 МиБ, а поиск по палитре не стоил ничего, что было бы видно сквозь шум: «от 1,854 до 1,874 мс до сужения и от 1,869 до 1,889 мс после». Пул разбит на шарды, до четырёх привязок буферов хранения, размер которых берётся из того, что предлагает адаптер, и так уровень теперь помещается на lavapipe, в два шарда. Один из тестов рисует одну и ту же сцену с пулом в одном шарде и разбитым на два-четыре и проверяет, что в картинке не меняется ни один байт.
Кольцо мипов
Та же каверна, что выше, где каждый чанк, кроме чанка камеры, обходится по мипам принудительно заданного уровня, чтобы потеря деталей была видна вблизи. Правило в пещере с подгрузкой: уровень 1 до 2 × r_full, уровень 2 до 4 × r_full, дальше уровень 3; при радиусах по умолчанию кольцо кончается на 24 чанках, так что обходится только уровень 1, а два других ждут более широкого кольца.
Ячейка мипа сплошная, если сплошной хоть один воксель под ней, так что чанк из мипов никогда не бывает более открытым, чем настоящий.
Полёт на автопилоте
Для измерений стенд получил автопилот, потому что человек за клавиатурой не пролетает один и тот же путь дважды. Он ждёт полной резидентности, вырезает метр породы в первой стене в пределах 30 м вдоль +x, хеширует затронутые чанки, летит 300 м наружу со скоростью 10 м/с, далеко за радиус выгрузки в 160 м, летит обратно, ждёт, пока выкопанные чанки снова станут резидентными, и сравнивает хеш. Оба записанных полёта заканчиваются строкой return: 2 chunks, hash match. Выкопанное пережило собственную выгрузку. За полёт он считает кадры, в которых чанк, нужный в полной детализации около камеры, ещё не был резидентен и отзеркален: 0 из 2 775 и 0 из 2 772.
Первый же полёт нашёл ошибку: полная резидентность заняла 29,3 с, потому что пул рос почти на каждой синхронизации, и каждый раз копировался целый шард. Когда пул стали растить в 1,5 раза и никогда не ужимать, стало 11,2–12,8 с. В логе признано и то, что единственный полёт в окне «поймал случайные клики с рабочего стола (пять заполнений около точки появления до взлёта)», и что дисплей большую часть сессии был выключен.
В показаниях — невыполненные бюджеты:
Время CPU на подгрузку. Линия — 1 мс на кадр на обновление, опрос и синхронизацию. Два полёта дали медиану, p50, 7,01 и 6,97 мс; p99, то есть кадр медленнее 99 из 100, 57,8 и 60,7 мс; и максимумы 83 и 106 мс. Промах большой. В заметке об итогах самого ADR сказано, что линию срывает работа с резидентностью в потоке рендера; план на неё уже в списке.
Первичные лучи в пещере с подгрузкой. В покое, при 1080p, проход лучей в пещере с подгрузкой занимает 6,69 мс, тогда как тот же коммит проходит ограниченный уровень за 1,95 мс. Шейдер тот же; окно — 51 чанк по стороне, и обход длиннее. В ревью при закрытии назначена диагностическая проба: «цена лучей с MAX_STEPS = 4096 через окно в 51 чанк». Обе линии остаются, это моё решение при закрытии.
Задержка резидентности. От «нужен» до «отзеркален» p95 был 455 и 462 мс на примерно 73 000 чанках, в пределах предложенных 500 мс. Максимум — 1,8 и 2,4 с, за пределами. Бюджет теперь записан как p95, и в файле так и сказано.
Память. Резидентность по умолчанию, полная до 16 чанков и мипы до 24, держит 17 077 полных чанков и 40 700 только с LOD: 2 916 МиБ в ОЗУ и 1 530 МиБ в зеркале. В движении выделенные байты зеркала дошли до 2,43 ГБ, а весь процесс — до 3,87 ГБ пикового RSS, его пиковой памяти в том виде, как её считает ядро, против линии в 4 ГБ. Словами самого файла бюджетов: «по ОЗУ запас самый маленький».
Скорость генерации под подгрузкой была 6,96 × 10⁷ вокселей в секунду на рабочий поток при 15 потоках, против 1,03–1,12 × 10⁸ на поток для ограниченного уровня. Headless-пещера с подгрузкой показывает первый кадр через 0,5–1,5 с и доходит до полной резидентности за 11,7–12,1 с после исправления.
Что открыто
В дорожной карте девять вех после начальной, и первая закрыта; три невыполненных бюджета записаны. Что написано и одобрено под следующие, всё этим вечером:
- План 0003, стоимость правок ниже 2 мс: заполнение отрезками вместо записи по вокселям, таблица целей по операциям, и хеш детерминизма не должен сдвинуться.
- План 0005, проба вида поверхности, которая решит, как порода перестанет выглядеть кубиками, с иллюстрацией собора как планкой и измеренной базой: внутри вырезанной сферы, где настоящее направление поверхности известно, грани кубиков отклоняются от него в среднем примерно на 32 градуса. Последняя фаза моя: посмотреть на три режима и выбрать.
- План 0009, обрушение: ограниченная заливка опоры по грубой сетке и падение вертикально, единым твёрдым куском. Это решение я принял этим вечером вопреки прежней формулировке «только скриптовое обрушение»: «воксельная динамика, а не движок твёрдых тел. Обрушение возникает само, и решает его авторитетная сторона, сервер». ADR называет предел: область больше лимита никогда не падает, так что свод большой каверны держится своим размером.
- План 0011, хост WebAssembly для логики игры. ADR-0010 — самое новое решение, и оно меняет формулировку из основания проекта: вся серверная логика игры — один компонент за трейтом
Logic, разрабатывается нативно и поставляется как компонентwasmtimeс каноническими NaN, без часов и с лимитом топлива (fuel) на тик, а Lua отложен, потому что его модель чисел — плавающая точка. Довод ADR идёт прямо из работы над Diablo II: «Каждая система, которую документирует проект рекомпиляции, — это целочисленная арифметика на собственном сиде каждой вещи». Такой код бит в бит одинаков в WebAssembly и нативно. Через границу ходят планировки уровней; воксели — никогда.
Остальные шесть меньше, и истории у них пока нет: коллизии как вертикальный цилиндр, который двигают целочисленным conservative advancement, с контроллером персонажа и грубой сеткой для навигации; пространственный звук от одной измеренной головы (HRTF), микшируемый в аудиопотоке; журнал операций сервера и байтовый канал между сервером и клиентом даже в одиночной игре, где вырез сферой в канале — 47 байт; импорт MagicaVoxel и штамповка префабов; HUD со статистикой на встроенном растровом шрифте без библиотеки интерфейса, вариант без зависимостей из трёх, которые мне предложили; и два гейта репозитория, скопированные из Ritmolux, которые уже находят в этом дереве два нарушения. Четыре из двенадцати уже шли в своих ветках, когда запись дня заканчивается.
Две вещи открыты, и ни один план их не закрывает. Базовой карты, от которой зависит, значат ли что-нибудь числа кадра выше, пока нет. И веха по освещению, M2, куда ADR положил самый большой риск: без вендорского шумоподавителя, налобный фонарь, скользящий по выкопанной стене, и светящиеся воксели, которые должны освещать пещеру сами. VR тоже получил решение этим вечером: виды как асимметричные усечённые пирамиды и освещение, общее между ними, чтобы ничто не запекалось под один вид, а настоящая поддержка VR не запланирована и на базовой карте не обещается вовсе.
Как шёл день
Репозиторий живёт так же, как репозиторий Ritmolux. Проектный вопрос с настоящим компромиссом становится ADR, ADR становится планом с фазами, у каждой фазы один владелец, dev или human, и поведенческое условие готовности, а моё «go» — это одобрение. Архитектор проектирует и никогда не пишет рабочий код; dev строит и правит в плане только строку статуса и лог реализации. Ревью при закрытии идёт в свежей сессии, «потому что ревью изнутри той же сессии, что писала код, ничего не стоит». Хуки запрещают широкий git add, переписывание истории и атрибуцию агента в сообщениях коммитов, и у них есть свои тесты. Сегодня хуки менялись дважды. Утром разрешили обычный git push, и в коммите написано, что я пробую разрешить агенту пушить этот репозиторий. Вечером появился хук, который запрещает закрывать окна через композитор. Причина в коммите: дважды за день агент-разработчик просил Hyprland закрыть окно стенда, один раз по адресу и один раз по заголовку, и оба раза композитор закрывал вместо него окно VS Code в фокусе, со всеми сессиями внутри. С моей стороны это было просто внезапное закрытие, неожиданное и досадное.
Единственное, чего разделы выше не покрывают из записей, — первый полёт. Перед тем как план 0001 можно было закрыть, в 14:41, я сам полетал по пещере и покопал в ней, и в логе записано, что сработала каждая кнопка.
В README до сих пор написано, что код движка ещё не появился. В 08:58 так и было.