Воксельный движок, дни со второго по шестой
Первый день закончился 16 октября в 18:35: два закрытых плана, версия 0.3.0, пещера с подгрузкой, по которой можно летать и копать, и три невыполненных бюджета. Этот пост — о пяти днях после него, до 21 октября, 11:31: версия 0.24.0, двадцать пять закрытых планов, двадцать одно архитектурное решение, а у движка появились коллизии, пространственный звук с реверберацией под размер комнаты, в которой стоишь, журнал операций, хост WebAssembly для логики игры, префабы, HUD, геймпад, обрушение, который крушит то, сквозь что падает, новый формат кирпича на стороне CPU, а зеркало свою половину ещё ждёт, вид на мир снаружи, сквозь породу, и поверхности, которые больше не похожи на кубики. Из трёх невыполненных бюджетов один выполнен, один выполнен после того, как я поднял планку, а один не выполнен до сих пор, зато теперь известно, почему.
Движок делается вместе с первой игрой, о которой в этом посте речи не будет. Каждое число ниже снято на моём ноутбуке, который в файле бюджетов назван машиной разработки: RTX 3080 Laptop GPU и Ryzen 9. Два слова из первого дня, на которые опирается всё остальное: кирпич — это 8 × 8 × 8 вокселей, а чанк — 8 × 8 × 8 кирпичей.
Вечер 16-го
Пост про первый день писался, пока шли последние планы того дня, и три из них закрылись до полуночи. В каждом была фаза, где был нужен я.
Вид поверхности. План 0005 сравнил четыре способа затенять воксельную поверхность с ориентиром из дорожной карты, собором в Шибенике с иллюстрации Лайне и Карраса: нормали граней, трилинейная смесь восьми соседей, градиент по двадцати семи и контурные плоскости, плоскость на каждый воксель, которая сдвигает точку попадания, плюс процедурный шум детализации поверх любого из них. Числа сняты на двух аналитических сценах: вырезанная сфера, где настоящая нормаль известна в каждой точке, и столб породы, где известен настоящий силуэт.
Последняя фаза была моей: пролететь уровень в каждом режиме и выбрать. В логе осталось, что я сказал. «Больше всего мне нравится gradient27, но тени выглядят неправильно; контуры странные, много артефактов — чёрные линии»; «контуры без теней выглядят лучше всего (если нажать L)»; про детализацию: «похоже на что-то среднее между шумом и тенями, наверное, лучше поменьше». Я назвал двух финалистов, а потом: «Я ошибался, контуры — лучший режим». К тому времени чёрные линии уже были посчитаны. Подсчёт одиночных тёмных пикселей на программном драйвере дал для контуров 646 с тенями и 301 без, и 372 пикселя, которые становились пятнами только с включённой тенью, то есть теневой луч стартовал внутри плоскости, в которую только что попал; у граней таких 174. ADR-0011 в тот же вечер зафиксировал решение: каждый воксель полного кирпича несёт 16-битный атрибут, октаэдральную нормаль в двенадцати битах и смещение плоскости в четырёх, в страницах атрибутов внутри того же пула кирпичей, так что шейдер ничего нового не привязывает. С собственной оговоркой: «Решение нельзя выпускать при радиусе по умолчанию, пока доработка не уменьшит память под атрибуты». Со страницами атрибутов пещера с подгрузкой дошла до 3,3 ГБ живых данных и 4,1 ГБ выделенных на карте с 8 ГБ и умерла с ошибкой нехватки памяти. Доработка стала планом 0013, одобренным в 21:27.
Ещё одна строка из лога того плана, потому что она объясняет пост про первый день: «RTX 3080 Laptop GPU отвалилась от шины PCIe 16 октября в 17:51», с записью ядра «GPU lost from the bus». Первые две фазы пробы сняли на встроенной Radeon, грани за 17,2 мс и градиент за 41,3, и кадры в посте про первый день тоже отрисованы на Radeon, и тот пост её называет. После перезагрузки карта вернулась; фазу 3 снимали уже на ней.
Один и тот же проход в двух видах
Отрисовано headless в 1280 × 720 сборкой версии 0.24.0, ограниченный уровень, сид 1, камера как у заглавной картинки README, налобный фонарь с теневым лучом.
Кадры сделаны маленькой программой-примером, написанной для этого поста поверх публичного API движка: сгенерировать, отзеркалить со страницами атрибутов, прогнать проход атрибутов, пустить лучи, прочитать обратно.
Ходьба. План 0004 дал движку контроллер персонажа: вертикальный цилиндр, плоский с обоих концов, который проводят сквозь воксели целочисленным conservative advancement, с зондом подъёма на уступ. ADR-0005 объясняет, почему цилиндр, а не капсула: «полусферическая стопа не может стоять на краю ступени, пока её ось не зайдёт за край… Каждый выпущенный контроллер обходит это плоским зондом стопы». Моя первая ходьба в окне тем вечером показала, что тело «замирало на ровном полу и у стен или углов, ни на один уступ нельзя было подняться, и только G освобождал его». Четыре причины исправили за следующие два часа, и две из них — ошибки, которые бывают только в целочисленной геометрии. У воксельного бокса меньше чем в единице от тела, на смещении 768,89 единицы от ребра по логу, квадрат расстояния округлился вниз до нуля, и он «останавливал каждое следующее движение, включая падение и шаг прочь». А корень из длины шага округляли вниз, так что остаток, до единицы длиннее самого шага, проходился целиком и заканчивался внутри бокса. В 22:04 я прошёлся снова: «всё хорошо, ощущается естественно. подстроим потом, если понадобится». Теперь это закреплено двумя тестами: 115 248 проводок, из которых ни одна не заканчивается внутри бокса, и 800 случайных прогулок на 56 135 тиков, которые ни разу не замирают.
Уши. План 0006 принёс пространственный звук: передаточную функцию головы из базы SADIE II, объект D2, манекен KEMAR, упакованную в формат движка и свёрнутую во временной области, 128 отсчётов на ухо, в микшере, который работает в колбэке устройства вывода и никогда не выделяет память и не берёт блокировок. Целиком голова весила 9 083 684 байта, втрое больше репозитория, так что оставили каждый третий азимут: 2 950 направлений, 3 044 420 байт. Окклюзия пускает от источника семь зондирующих лучей и считает на них отрезки породы; главный тест — тон 100 Гц в десяти метрах за пятью метрами материала с пропусканием 0,5 и поглощением 0,3 на метр, который должен звучать на уровне 0,0814 от открытого и звучит на 0,081379. В тот вечер я надел наушники и доложил две вещи: левое и правое перепутаны, и щёлкает, когда поворачиваешься или идёшь. Первое было в наушниках: «speaker-test сыграл „Front Left“ в правое ухо», это была системная карта каналов, движок тут ни при чём. У щелчков было три причины, каждая измерена: линейный кроссфейд между направлениями, четвёртая разность которого была в 227 раз больше, чем в покое, заменён на приподнятый косинус; звуковой крейт собирался без оптимизаций в отладочных сборках, 1,25 мс на блок в 256 кадров против 5,33 мс, которые блок длится; и окна упакованной головы по направлениям начинались с разбросом до 23 отсчётов. Я на слух сравнил в движке пакет с единым окном и старый: блоков со скачком фазы больше 0,3 радиана стало 14 из 3 611 вместо 378 из 2 722, «и щелчки на слух пропали».
Хук. В 18:35 появился новый хук, тот, который упомянут в посте про первый день: агенту нельзя просить композитор закрыть окно, потому что дважды за день тот закрывал вместо этого мой редактор. Через час случилось зависание при выходе, которое я не смог воспроизвести: мой полёт в окне не отреагировал на закрытие, и его пришлось убить, а все headless-варианты выходили сразу. Оболочка теперь пишет в лог каждую стадию завершения в фиксированном порядке, так что в следующий раз будет видно, на какой стадии зависло.
Ночь 17-го
Я оставил планы одобренными, с одним указанием на все случаи: «действуй по своим рекомендациям». Оркестрирующая сессия гнала их параллельно в отдельных worktree, и между 00:24 и 06:09 закрылись девять:
| Время | План | Версия |
|---|---|---|
| 00:24 | 0003, стоимость правки: строки-отрезки и таблицы цели | 0.4.1 |
| 00:27 | 0004, коллизии и контроллер персонажа | 0.5.0 |
| 01:12 | 0012, два гейта репозитория из Ritmolux | — |
| 02:06 | 0008, импорт MagicaVoxel и штамповка префабов | 0.6.0 |
| 03:16 | 0014, стоимость правки, второй заход | 0.7.0 |
| 03:43 | 0009, запрос опоры и операции обрушения | 0.8.0 |
| 03:53 | 0010, HUD со статистикой | 0.9.0 |
| 04:04 | 0007, журнал операций, байтовый канал, клиенты | 0.10.0 |
| 06:09 | 0011, хост WebAssembly для логики | 0.11.0 |
Шесть фаз за ночь остановились на своём условии готовности, том условии, которое фаза должна выполнить, чтобы считаться сделанной, и были поправлены на месте, и у каждой поправки в логе стоит «применено оркестратором… владелец может отменить». Клавиша штамповки V совпала с переключателем детализации, так что штамповка теперь P. Журнал операций не мог отличить операцию, которую сделала логика, от операции, поданной напрямую в том же тике, а отвергнутая операция забирала порядковый номер, которого не записывал ни один журнал; поправка — пограничная запись в 13 байт. План по стоимости правки остановил его собственный профиль: работа с отрезками, которую он собирался срезать ниже пяти процентов, занимала тринадцать, и поправка говорит прямо: «Цель nfr.md §4 в 2 мс этим планом не достигается». А хост WebAssembly обнаружил, что после любого прерывания, по топливу, по памяти или по панике, «wasmtime 49.0.3 отказывается входить в экземпляр компонента снова», так что сорвавшийся скрипт оставался остановленным.
В 07:11 сессия написала мне файл decisions-pending.md, а в 09:12 я на него ответил. Решения: базовая карта остаётся GTX 1660, а портативный профиль класса Switch 2 — профиль качества, а не гейт, и «до Switch 1 ray marcher такой формы не дотянется»; полупрозрачные твёрдые тела — воксели, а газ — отдельный объём, потом; прямой свет через ресемплирование по важности, а непрямой — через зонды; сцены под открытым небом — в рамках проекта; а вопрос о стоимости правки переформулирован «вокруг стабильности времени кадра на сложных сценах». Три ночных решения я отменил: контроллер получает выталкивание, тело, которое засыпала правка, поднимают наружу; сорвавшийся скрипт перезапускается со своего сида, а не остаётся остановленным, до восьми раз; а план про гейты получает патч-версию. Планы с 0016 по 0019 были написаны и одобрены к 10:02, план 0020 к 10:36, и я запушил main.
Что построила ночь
Журнал операций. Сервер владеет миром и журналом из (tick, seq, op); клиент либо общий, один мир за Arc, либо собственный, со своим миром, который проигрывает байты и поднимает событие рассинхронизации при первом расхождении. Выкапывание сферой — 47 байт в канале, заполнение коробкой 65, капсула 75. Проиграть 10 000 выкапываний по метру — 279 мс. Фраза из ADR-0007: «Журнал операций — единственный источник порядка. В каком бы порядке потоки ни вызывали submit, воспроизведение идёт в порядке seq журнала». Стенд получил --save и --replay, и хеш мира сохранённой сессии при воспроизведении сходится.
Хост WebAssembly. ADR-0010 в первый день сказал, что логика игры — это один компонент за трейтом, который разрабатывается нативно и поставляется как WebAssembly. План 0011 это доказал: одни и те же 100 входов через сервер с нативной эхо-логикой и со скомпилированным компонентом, «закодированные байты двух журналов операций равны, хеши двух миров равны». Собственная логика копания стенда переехала в отдельный крейт и даёт тот же хеш сессии в обоих вариантах. Каждый механизм детерминизма проверен гостем, написанным специально, чтобы его сломать: канонические биты NaN, часы, которые читают тик, источник случайности, засеянный тиком, никакой файловой системы, гость, который сжигает топливо, гость, который съедает лимит памяти, гость, который пишет по 64 КиБ в stderr на каждый вызов. Число, которое предложение назвало неверно, записано в итоге ADR: «Вызов хоста стоит около 1,4 мкс, а не десятки наносекунд». Бюджет поставили в 2 мкс; тихий замер на отсечке — 1,56. Через три дня план 0019 заставил хост брать плату за работу: за каждый байт списка операций, за каждый воксель в границах правки, за каждый шаг луча, за каждую ячейку, в которую заглянул запрос опоры, из топлива тика, так что подсчёт по кубу в 1 024 вокселя на сторону роняет тик по топливу вместо того, чтобы выполниться.
Префабы, HUD, обрушение. Файл MagicaVoxel импортируется в префаб с картой материалов, а префаб штампуется в мир в любом из 24 поворотов, при генерации или на ходу; свод 16 × 16 × 16 с оболочкой в один воксель, заштампованный в породу, меняет ровно 4 096 вокселей, а тот же свод, поставленный при генерации, даёт мир с тем же хешем. Ревью нашло файл в 70 байт, который выделил бы 3,1 ГиБ, и загрузчик теперь ограничивает модель 256 вокселями по стороне. HUD — это растровый шрифт 8 × 8 из 95 глифов в одном проходе поверх кадра, 0,038 мс на кадр, потому что из библиотеки интерфейса, рендерера текста и собственного шрифта я выбрал вариант без зависимостей; ADR-0009: «неверный бит — это неверный пиксель, и свалить его не на какую библиотеку». Появилось и обрушение, в первом варианте: запрос опоры, заливка по грубой сетке (ячейка на кирпич, та же, по которой считают коллизии и звук) от изменённой ячейки по несущим, с лимитом на размер, и две операции, жёсткое вертикальное падение области на минимальный зазор под ней и удаление, которое оставляет часть щебнем. «Область больше max_region не падает никогда: свод большой каверны „держится“ своим размером».
Вечер 17-го и 18-е
Прослушивание 17-го в 10:00, уже на влитом звуке, прошло: стук с правильной стороны, гул глуше за породой, никаких щелчков при повороте. Одна находка осталась открытой, щелчки при движении слушателя, которые пропадали с выключенными отражениями, значит, дело было в том, как переключался отвод отражения; её передали плану 0020. Вечером я запустил проверку версии 0.12.0 в окне и нашёл две вещи, которых ночные закрытия не нашли. Удалённая тонкая область печатала «removed: {rock: 201}, 0 rubble», потому что четверть каждого столбца округлялась в ноль. А колёсико мыши не меняло радиус копания: я «прокрутил раз сорок, и каждая строка читалась dig radius 0.5 m», дробная часть каждого кадра тоже округлялась в ноль. Третья находка не была ошибкой ни одного из планов: обрушение в пещере почти не случалось, потому что «ячейка 0,8 м с 64 сплошными вокселями из 512 всё ещё несёт нагрузку, так что тонкая оставшаяся перемычка держит навес», а я хотел навесов, которые падают от пролёта и веса, а не только когда их отрезало от породы. Это ушло в веху про обрушение в дорожной карте и стало решением 20-го, о котором ниже. План 0016, исправление всего этого и трёх решений, которые я отменил, прошёл 18-го и закрылся в 22:09 версией 0.13.0; в моей перепроверке колёсико печатает «dig radius 0.4, 0.5, 0.3, 0.2, 0.3 m», а кольцо, прорезанное в тонкой стене, оставляет 134 вокселя щебня из 598, то есть четверть.
Планка правки и кирпич, который её сдвинул
Первый день не уложился в бюджет правки: 100 выкапываний по метру за 4,92 мс против 2. План 0003 довёл до 3,56 на тихой машине, решая отрезки аналитически на каждую строку кирпича. План 0014 сначала профилировал и опроверг собственную арифметику плана: развёртывание кирпича, которое он собирался убрать, занимало пять процентов времени, а запись строк — 54. Он выпустил пул массивов кирпичей и более дешёвую строку капсулы и закрылся на 3,03 мс на прогретой машине с таким расчётом при закрытии: «Убрать решение строки, поиск отрезков и Brick::expand целиком — останется около 2,4 мс. Внутри представления из ADR-0002 до 2 мс не доходит ничто». Ревьюер рекомендовал пересмотреть бюджет. Тем утром я пошёл другим путём: новое представление.
ADR-0012, предложен и принят 17-го: кирпич — это 512-битная маска занятости, палитра на кирпич и столько битовых плоскостей, сколько нужно палитре, одни и те же байты в ОЗУ и в зеркале на GPU. «Время кадра — это пропускная способность памяти, а сегодняшний обход тратит на шаг пять зависимых чтений». Порядок работ в нём такой: сначала иерархия, потом формат. Формат для CPU пришёл 20-го планом 0021, и его первой фазой была перепись настоящих кирпичей: на уровне 383 608 полных кирпичей, из них 275 466 держат один материал и 107 325 два, так что объём падает с 392,8 МБ до 34,6; в пещере с подгрузкой при радиусах по умолчанию — с 3 053 МБ до 411. Бенчмарк ядра, по которому я решал 20-го в 09:12: 505,3 нс на правку кирпича сегодня, 116,8 на плоскостях, отношение 0,231. План закрылся в 13:37 с бенчмарком правки на 1,49 мс на прогретой машине в том же окне, где старый код показывал 2,86.
Для памяти кирпич сделал больше, чем для правок. В файле бюджетов первого дня было сказано: «по ОЗУ запас самый маленький». Первая загрузка уровня в GPU упала с примерно 585 мс до примерно 190, а сброшенный на диск чанк из 512 кирпичей с двумя материалами занимает 37 416 байт вместо 524 840.
Проход лучей и иерархия
Второй невыполненный бюджет — первичные лучи в пещере с подгрузкой: 6,69 мс против 3, тогда как ограниченный уровень тот же шейдер проходил за 1,95. Первая фаза плана 0015 добавила в стенд вид по шагам и показала, в чём дело: «У точки появления в пещере с подгрузкой в среднем 15,55 шага на луч против 10,48 на уровне, в 1,5 раза, а время прохода — в 3,7 раза… Большая часть разницы — это цена шага: пять зависимых чтений в зеркало размером около 1,7 ГБ». Так что план построил иерархию битов занятости, по одному чтению на уровень: 512 битов кирпичей в начале каждой записи чанка, бит в таблице чанков для чанка из сплошного воздуха, 512-битная маска непрозрачности на кирпич вместо 8-байтовой, и обход, который держит свой чанк, биты, запись кирпича и маску от шага к шагу и читает пул только в точке попадания.
Остановилась та фаза, которая меняла выход пропуска из ячейки. В посте про первый день была история про 17 одиночных пикселей, сдвинувшихся с приходом масок 2 × 2 × 2; пропуск 4 × 4 × 4 сдвигал пиксели в каждом эталоне, в каждой из картинок, с которыми CI сравнивает каждую сборку, а правило плана было «бит в бит». Меня спросили, и я решил: переснять эталоны один раз, «у разового переснятия нет порога по пикселям»; каждый эталон перегенерировали единожды, отличающиеся пиксели посчитали, от 58 до 141 на картинку, а координаты выхода у обхода теперь идут в порядке плоскостей, так что «картинка не зависит ни от одного пропуска». План закрылся 19-го в 10:32 с точкой появления пещеры на 1,93–1,98 мс по шести прогонам, «планка на машине разработки выполнена», и с классом полупрозрачных материалов в придачу: второй короткий обход от глаза до непрозрачного попадания, который накладывает перед ним до четырёх полупрозрачных вокселей. Тест плана говорит, что непрозрачному обходу он ничего не стоит; замер в точке появления вырос примерно на 0,1 мс, когда он лёг.
Контуры по умолчанию
У плана 0013 было три задачи: начало теневого луча, память под атрибуты и сила детализации. Начало сдвинули на полвокселя вдоль нормали плоскости, и число одиночных тёмных пикселей упало со 174 у граней до 52. Силу детализации выбирал я, 18-го, из 0,1, 0,15, 0,2 и 0,3: 0,1. Память оказалась самым трудным и закончился вариантом, который план называет (в). 17-го я выбрал как временную меру меньший радиус полной детализации, 14 чанков вместо 16. Тем временем план 0017 заставил зеркало резервировать свои 3 ГБ один раз при старте и сообщать давление, долю занятого, с правилом, которое снимает чанк радиуса полной детализации, когда давление переходит 0,9, и возвращает его ниже 0,7. 20-го, уже с этим, фаза остановилась: при 14 давление показало 0,911, когда всё загрузилось, и правило всё равно уменьшило радиус. Вариант (в) отзывает временную меру и не задаёт нового радиуса: вид по умолчанию укладывается в 3 ГБ одним только правилом давления. Два полёта автопилота установились на 12–13 чанках, от 76,8 до 83,2 м полной детализации, против «около 100 м» из дорожной карты; простой вид держит 16, 102 м. В файле бюджетов это записано как наблюдение, а не как невыполнение: живые байты на пике ниже планки. План, который удержит 16 с контурами, хранение атрибутов только для поверхности, — последний шаг ADR-0012, и он пока не написан. 20-го я его облетел, и в логе стоит: «вид по умолчанию принят; --level — любимый вид владельца, и он плавный».
Ревью при закрытии того плана нашло «настоящую регрессию»: p99 прохода атрибутов на потоке рендера, то есть кадр медленнее 99 из 100, был 167 мс, а вид по умолчанию пролетал на пятую часть меньше кадров, чем простой. Из этого вырос план 0023.
Поток рендера
Гипотеза ADR-0017, подтверждённая профилем 20-го: когда камера пересекает границу чанка, разом выгружаются около 2 000 чанков кольца пониженной детализации, и каждый запускает проверку 488 пограничных кирпичей, под миллион поисков в хеш-таблице за один кадр; бенчмарк насчитал 956 968 посещений кирпичей оболочки на одно пересечение. Три исправления, каждое измерено. Проверка оболочки теперь заходит только в пограничные кирпичи полных соседей, и случай выгрузки превращается в 50 986 поисков соседей и ноль посещений кирпичей, со 170,89 мс до 1,399 на кадре пересечения в бенчмарке. Проход ограничен 8 192 кирпичами на кадр с очередью переноса, а кирпич, который ждёт, получает запись очистки, пишущую «нет плоскости», так что он «показывает нормали граней, а не устаревшие плоскости». А очередь подгрузки, обход резидентности и сдвиг окна стали инкрементальными: худший кадр обновления ушёл с 37,8 мс до 4,75. Фазу, которая перенесла бы сужение палитр при загрузке на рабочие потоки подгрузки, я пропустил 21-го, посмотрев на её входной профиль.
Результат я облетел утром 21-го: «Рывок: „почти исчез“, ни одной ощутимой остановки; частота кадров падала примерно до 40 fps на быстрых проходах… Грани вместо контуров на свежеподгруженной породе: „не бывает“, видимой задержки нет. Выкапывания: контуры в том же кадре». План закрылся в 11:21 версией 0.23.0, с планкой подгрузки, которая «по-прежнему записана как невыполнение», и с причиной, у которой впервые есть число: 41,8 процента отсчётов синхронизации — это копирование в буфер GPU, а сужение — примерно 38. Следующий шаг — кольцо промежуточных буферов, которое ADR отверг как свой вариант Б, так что взять его «можно только новым ADR, который отменит это отклонение». Я поставил его после плана по освещению.
Обрушение, который крушит
20-го я провёл в окне две проверки, вида по умолчанию и плана, который проводит штамповку и падение через сервер, и вторая нашла главную ошибку этих дней. Каждая область, которую запрос опоры называл неподдержанной, печатала «приземлилась через 0 тиков, упала на 0 вокселей». Запрос считает, что ячейка с менее чем 64 сплошными вокселями из 512 ничего не держит; падение же останавливается на любом сплошном вокселе снизу. Область на тонкой прослойке была одновременно неподдержанной и заклиненной, и никогда ничего не падало. Headless-воспроизведение на уровне: первая область, которую отделяют выкапывания по сиду, четыре ячейки и 501 воксель, лежит на ячейках, в которых от 31 до 38 сплошных вокселей, и падает на ноль.
Я выбрал падение, которое крушит то, сквозь что идёт, и ADR-0016 это записал: «Крушить, а не уносить. Раскрошенный объём становится щебнем, ссыпанным в конце падения, через сервер и журнал». Сплошной воксель вне области поддаётся, если в его ячейке сплошных меньше порога, а материал не неразрушимый: поиск зазора проходит сквозь него как сквозь воздух, и падение его крушит. Та область падает по восемь вокселей за тик, девять раз, 72 вокселя, круша по дороге 141, и садится на ячейку, которая держит нагрузку; операция ссыпания кладёт эти 141 обратно поверх неё щебнем. Полный прогон уровня по 40 000 выкапываний по сиду теперь находит одну область там, где заклинивающий код находил шесть, потому что первая падает, а следующие выкапывания, которые отделяли бы её снова, не находят ничего. Два новых вида операций, пятый и шестой, чтобы воспроизведение падало так же; хеш сценария обрушения сдвинулся и закреплён заново, хеш сессии не сдвинулся.
Приземлившийся кусок
Область, которую отделяет тест, после падения на 7,2 м сквозь прослойки: раскрошенные воксели ссыпаны обратно на неё щебнем. Отрисовано headless из собственного мира теста обрушения, лампа приглушена в восемь раз из-за расстояния.
Моя проверка в 22:23: 17 неподдержанных областей, 17 приземлений, ни одного отказа и ни одного заклинивания; «приземлилась через 4 тика, упала на 26 вокселей, раскрошила 159 вокселей, 159 щебня»; «приземлилась через 16 тиков, упала на 121 воксель, раскрошила 17 вокселей, 17 щебня»; 14 из 17 что-то раскрошили, и каждая кучка равна раскрошенному. «всё работает как задумано». Бенчмарки: плита сквозь прослойку 92–95 мкс против предложенных 100, поддающийся куб 7,4–10,6 мс против 20, ссыпание 35–38 мкс против 200.
Пещера, которую слышно
У плана 0020 три фазы с пометкой human, больше, чем у любого другого, потому что проверять их можно только ушами. Прямой звук, окклюзия и ранние отражения были у движка с 16-го. Этот план добавил поздний хвост: веер из 128 лучей от слушателя измеряет объём и площадь стен комнаты, которую он видит, формула Эйринга превращает их во время затухания, зажатое между 50 мс и 8 с, и одна общая сеть обратной связи из шестнадцати линий задержки воспроизводит хвост такой длины, с длинами линий в простых числах около средней длины свободного пробега. Ранние эхо воспроизводятся со стороны своей стены через шестнадцать общих шин направлений, каждая со своим откликом головы, и при движении слушателя скользят, а не переключаются, и это те самые щелчки, которые прослушивание 17-го оставило открытыми. Отражающая грань сохраняет корень пятой степени из единицы минус её поглощение, правило глубины проникновения. А --mic открывает микрофон как голос с заглушённым прямым путём, «чтобы игрок слышал, как его собственный голос возвращается от пещеры». Куб в 12 м, измеренный из центра, даёт затухание 2,249 с.
Две фазы остановились на условиях готовности, которые нельзя выполнить так, как они записаны: тест ходьбы просил меньше полдецибела изменения на блок, а идеальный рендер той же ходьбы без единого щелчка меняется на 5,24; тест шага между комнатами измерял гребень хвоста вместо щелчков. Обе поправили. Потом, 20-го в 17:55, я слушал. Из шести проверок прошли две. «С включённым хвостом теряется направление. С выключенным Y направление стука слышно; с включённым — нет». «Баланс неверный, эхо слишком тихое, а хвост слишком громкий». Комната в 4 м была «короткой и тесной» и прошла; комната в 36 м «не читается как большой зал: владелец ждёт, что голос вернётся поздно и почти в полную силу, как когда кричишь в настоящей пещере». От движения слушателя получалось что-то «вроде Доплера, но без сдвига высоты». А вылет за пределы мира и возвращение в зал давали «громкий хлопок».
Исправление, пять фаз, вошло между 18:40 и 19:44. Хлопок давал веер, пущенный из толщи породы: он сообщал комнату вообще без стен; теперь такой веер отбрасывается, а веер без единого попадания даёт ноль. Смена комнаты передаёт хвост между двумя сетями, когда средняя длина свободного пробега меняется больше чем на пятнадцать процентов, с кроссфейдом; измерено как +2,62 дБ над установившимся уровнем зала против +19,11 при скольжении и +34,10 при старом переключении. Эхо скользят со скоростью самой ходьбы и дотягиваются до комнаты в 100 м. Прямой путь получил необязательную задержку распространения, выключенную по умолчанию. А линии хвоста были ограничены 4 800 кадрами, и комната в 100 м это вскрыла: пятнадцать из шестнадцати линий упёрлись в предел, и хвост показывал −29,9 дБ против −22,6 у комнаты; предел теперь 28 000, а замер −22,56.
Я слушал снова в 22:27, в наушниках, которые названы в логе, и подбирал уровни руками по комнатам: от +30 дБ эха в комнате 4 м до +6 в комнате 100 м, хвост от −15 до 0, а в комнате 100 м с микрофоном «голос возвращается поздно и сильно, потом тянется, „вполне естественно“». Направление стука держалось во всех трёх комнатах на этих настройках. Значит, уровень — функция комнаты, и фаза 13 сделала его функцией: кусочно-линейной кривой по логарифму средней длины свободного пробега с узлами в четырёх комнатах, которые я настроил. ADR-0021 записывает цену: «Уровни больше не физические. В кубе 4 м при настройках по умолчанию ранняя энергия примерно на 34 дБ выше энергии прямого пути». Подтверждающее прослушивание утром 21-го: комната 4 м «хороша и естественна как есть», комната 12 м лучше с эхо на 3 дБ тише, и это уходит архитектору на пересчёт того узла, прогулка по пещере без единого скачка уровня в любом зале, а дрожание при ходьбе «почти отсутствует».
Последнее решение из этого плана — правило для бенчмарков. Парный бенчмарк микшера в фазе 13 показал новый код на +5,80 процента к родительскому при том, что в измеряемом блоке не прибавилось никакой работы. После пересборки с выравниванием циклов на 64 байта «оба дерева показывают одно и то же… Разница в том, куда компоновщик положил цикл». ADR-0020: парные границы бенчмарков снимаются с выровненными циклами, и повторный замер дал +0,08. Абсолютные предложения для микшера всё равно не выполнены, 0,69 мс в покое против 0,6 и 1,14 при повороте против 1,0, на нагруженной машине; я принял нагруженные замеры, чтобы закрыть план 21-го, а тихий замер и оптимизация, если он останется выше, стали доработкой. Версия 0.24.0.
Снаружи мира
Вылет за границы уровня показывал чёрный экран, а хотелось вида, как когда вылетаешь через noclip за пределы карты в Source: уровень как прозрачный кукольный дом из освещённых комнат. Правило плана 0025 — одно предложение в шейдере: луч, который начинается в ячейке не из воздуха, игнорирует породу, пока не войдёт в пустую ячейку резидентного чанка, а потом попадает в следующую поверхность как обычно; чанк вне окна — не воздух, так что порода, за которой нет каверны, — промах. Попадание затеняется фиксированным полусферическим светом, без лампы и теневого луча, а промах — ровное небо. Камеру эталона я отодвинул с 20 м снаружи на 40, потому что с 20 неба было шесть процентов, а с 40 — двадцать четыре. Клавиша X, флаг --cutaway, и HUD показывает режим.
Уровень с 40 м снаружи его западной грани
Сид 1, камера эталона разреза в 1280 × 720 на RTX 3080: дальняя стена каждой каверны видна сквозь оболочку под полусферическим светом; там, где за породой нет каверны, — небо.
Луч снаружи проходит всю оболочку: в среднем 49,5 шага на луч и p99 170 против 0,91 и 1 для той же камеры без разреза. В файле бюджетов это диагностика, без планки.
В тот же вечер появился геймпад, потому что я хочу показывать демо с дивана: оболочка несёт аналоговые оси и кнопки как простые перечисления, ни один крейт движка не зависит от библиотеки геймпадов, а стенд отображает стики DualSense на полёт и взгляд, курки на копание и заполнение, кнопку на переключение разреза. Проверку на диване с настоящим контроллером я ещё не делал.
Бюджеты на отсечке
Четыре планки за эти дни поменял я: планку правки на 2,2 мс, предложение в 5 мкс для проводки на её замер 26,3 мкс, предложение в 0,5 мс для обновления акустики на её замер 1,0 мс, и нагруженные замеры микшера, принятые как невыполнение. Каждая так и записана в файле, с датой.
Что дальше и что открыто
Следующий план — тот, который ADR-0001 назвал самым большим риском: освещение. План 0024 одобрен, и в нём не начато ничего.
flowchart TB hit["проход попаданий: по записи на пиксель (позиция, нормаль, материал, плоскость)"] --> light["проход света: один образец на пиксель, фонарь или светящийся воксель, выбранный ресемплированием; один теневой луч"] mirror["загрузка в зеркало извлекает светящиеся воксели в буфер образцов света"] --> light light --> temporal["временной проход: перепроецировать результат прошлого кадра; стереть историю внутри бокса каждой правки"] temporal --> atrous["проход a-trous: фильтр по дисперсии"] atrous --> comp["сборка: вернуть альбедо, добавить свечение, экспозиция, тональная кривая"]
Довод ADR-0018 — арифметика базовой карты: «Планка освещения в §2, 6,0 мс, покупает на базовой карте примерно два луча на пиксель в полном разрешении или восемь в половинном. Любое семейство, которому нужно больше одного-двух теневых лучей на пиксель за кадр в полном разрешении, в базовую карту не укладывается». Четыре механизма остаются переключаемыми, пока я их не увижу: обход теневого луча, выбор образца света, стирание истории и то, считать ли свет в половинном разрешении. Два из его условий готовности: светящийся куб 4 × 4 × 4 «держит ровно 56 образцов света после синхронизации», пол под светящимся потолком — в пределах пяти процентов от освещённости 1,741. Последняя фаза переснимает заглавную картинку README со светящейся жилой.
Кроме него открыто: кольцо промежуточных буферов для копирования при подгрузке, которому нужен свой ADR; план хранения атрибутов, который удержит 16 чанков контуров в 3 ГБ; доработки звука, тихий замер микшера, дрожание, узел 12 м; три хеша, которые ждут подтверждения от Windows-ветки CI; DualSense на диване; и базовая карта, «которую нужно купить или одолжить до закрытия M2». README отстаёт от движка на три плана: он не знает X, --cutaway, --pad, --room и --mic и по-прежнему пишет, что реверберация «в работе».
Как были организованы пять дней
Харнесс тот же, что в первый день: ADR на настоящий компромисс, план с фазами и условием готовности на каждую, dev строит, ревью при закрытии в свежей сессии, моё «go». За пять дней прибавился масштаб: 20-го одновременно шли четыре агента в четырёх worktree, поэтому почти каждый замер того дня помечен «не тихо» со средней нагрузкой за минуту от 8 до 29, и поэтому тихие повторные замеры существуют как отдельные коммиты. Одиннадцать планов закрылись 19-го, 20-го и 21-го. Между двумя из них прошло обновление зависимости, потому что инструмент проверки уязвимостей упал на двух предупреждениях про wasmtime; патч-версия закрыла оба. А каждую фазу с пометкой human делал я, за клавиатурой или в наушниках, кроме геймпада на диване.