hrxdev
Русский

№1 Детерминированное ядро, которое охраняет компилятор

· Nick

Смотреть пока не на что, поэтому первая запись посвящена части, которую никто не увидит: ядру симуляции. Игра должна тянуть до 1000 заключённых и сотрудников при 20 тиках в секунду и объяснять, почему кто-либо что-либо сделал. Это возможно, только если одинаковые входные данные всегда дают один и тот же мир. Я решил с первого дня сделать это свойством, которое проверяет машина.

Где мы сейчас

Я на первом этапе, каркасе ядра. Симуляция – обычная библиотека .NET без кода Godot, а клиент на Godot 4 пока открывает пустую 3D-сцену. Модели мира, клеток, персонажей и сохранений нет. Бюджет в 1000 персонажей – это цель; в таком масштабе я пока ничего не измерял.

Тик

Время в ядре – целое число тиков, 20 в секунду. Времени с плавающей точкой нет. Тик проходит три фазы: применить внешние команды, запустить системы в порядке, записанном в одном месте, затем зафиксировать отложенные изменения и увеличить счётчик. Если из тика вырывается ошибка, мир помечается как сбойный и останавливается. Сейчас список систем пуст; цикл существует для того, чтобы у первой системы было место.

Команды и журнал

Мир меняется только командами. Команда – небольшая запись со стабильным идентификатором типа и написанным вручную бинарным кодированием. Для каждой мир кодирует её, ставит отметку с номером тика и порядковым номером, проверяет (только чтение), записывает в журнал и только потом применяет. Отклонённые команды тоже попадают в журнал, что должно помочь с баг-репортами. Сид плюс журнал должны точно воспроизводить прогон. Формат файла повтора пока не написан.

Случайные числа

Генераторы случайных чисел я написал сам (SplitMix64 и xoshiro256**) и запретил в ядре стандартный Random. У мира четыре именованных потока (команды, потребности, ИИ, события), они выводятся из сида и хранятся в состоянии мира, поэтому сохраняются и входят в хеш. Для работы, где порядок не должен влиять на результат, есть генератор без состояния с ключом из домена, сущности и тика. Выходные значения для сида 42 закреплены, потому что по сути это и есть формат повторов: поменяешь генератор – сломаются все старые повторы.

Хеш мира

Небольшой хешер, чувствительный к порядку, сворачивает сид, тик, случайные потоки и дайджест журнала команд в 64 бита. Два прогона, которые должны совпадать, можно сравнить одним числом. Пока хеш покрывает только эти части, потому что в мире больше ничего нет. Следующий шаг – сравнивать хеши каждые N тиков во время прогона.

Недетерминизм как ошибка сборки

Я не хотел полагаться на то, что буду помнить правила. Ядро собирается с анализаторами запрещённых API: системное время, случайные числа без сида, случайные GUID, рандомизированные хеши строк, потоки и параллельные помощники, SIMD и строковые API, зависящие от культуры, – всё это ломает сборку. Дополнительные проверки следят, чтобы не было изменяемого статического состояния, и просматривают скомпилированный код на предмет всего, что проскочило. У меня на машине русская локаль с десятичной запятой, а это хороший повод запретить API, зависящие от культуры.

Потом я пошёл дальше и запретил в ядре стандартные хеш-коллекции (Dictionary, HashSet и им подобные), поскольку порядок их обхода – классический источник расхождений. Ядру достались массивы, списки и две небольшие собственные коллекции с задокументированным порядком. Защита не герметична: API, возвращающий хеш-коллекцию через обобщённый интерфейс, всё ещё может проскочить, а часть защиты работает только в тестах.

Как это построено

Инструменты вокруг кода тоже автоматизированы, и одни и те же скрипты работают в Windows и macOS. Общий подход описан на странице Как это сделано.

Что дальше

На этом этапе остаются: модель мира с этажами, файлы сохранений и повторов, тест детерминизма и безголовый запуск сценариев с базовой линией бенчмарка. После этого – режим строительства на Godot. См. дорожную карту.

← Все записи девлога