Оптимизация CPU-рендеринга Godot: как профилирование превратило два узких места в серьёзный прирост
Public- 15 Sep, 2026

Со стороны оптимизация рендеринга часто выглядит как чёрная магия, но на деле процесс куда методичнее, чем кажется. Разработчики Godot подробно рассказали, как они оптимизируют CPU-код в рендерере движка, на конкретных примерах за последний год. Вся работа опирается на один ключевой принцип: любому движку приходится балансировать нагрузку между CPU и GPU, потому что итоговую производительность всегда определяет более медленный из них. Никакая настройка CPU не скомпенсирует неэффективные шейдеры. Команда движка постоянно идёт на такие компромиссы — например, 2D-батчинг слегка ухудшает производительность GPU, зато сильно улучшает производительность CPU, а поскольку 2D-игры обычно первыми упираются в CPU, батчинг почти всегда даёт чистую выгоду. 3D-игры, наоборот, чаще всего ограничены GPU, поэтому Godot выполняет occlusion culling на CPU, чтобы разгрузить GPU. К тому же любая оптимизация экономит заряд батареи на платформах с ограниченным питанием.
Основной цикл оптимизации CPU прост: найти узкое место, понять, как работает система, внести изменение и заново измерить. Замерять заново важно, потому что оптимизации часто ведут себя неожиданно. Добавить ветвление внутри цикла, чтобы пропускать лишнюю работу, кажется быстрее, но оно может заблокировать векторизацию компилятора и всё замедлить. Точно так же кэширование значения ради отказа от лишних вычислений может дать обратный эффект, если настоящее узкое место — чтение из памяти: в этом случае сделать работу дважды оказалось бы быстрее. Найти виновника — самый трудный шаг, и начинается он с профайлера. В Godot их встроено два: один для GDScript, другой для рендерера. А вот для самого кода движка разработчикам нужен внешний инструмент. В статье используется Superluminal — сэмплирующий профайлер, который подключается к запущенному экземпляру Godot, записывает данные по мере работы движка, показывает, какой код когда выполняется, и недавно обзавёлся поддержкой Linux. Godot также поддерживает сборку с Tracy для профилирования на основе трассировки.
Первый пример появился из разговора с разработчиком игры Heidi's Legacy: Mountains Calling, который обнаружил, что код анимации в Godot настолько медленный, что контент пришлось ограничить примерно двадцатью анимированными персонажами. Схема включала AnimationPlayer, анимирующий вершины Polygon2D, который отображался на Viewport поверх Sprite3D — приём необычный, но сам по себе не дорогой. Профилирование минимального воспроизведения (проверено на Ryzen 5 9600X) сразу выявило виновника. Класс Polygon2D почти всё время тратил на то, чтобы каждый кадр освобождать прежнюю поверхность меша и выделять новую, ведь данные вершин на CPU меняются каждый кадр. Граф вызовов и вкладка «Source and Disassembly» указали на точные строки, пересобирающие внутренний меш, подтвердив лишнюю возню.
Исправление задействовало низкоуровневый API Godot для ручного обновления данных вершин на месте. Когда число вершин не меняется, освобождать и заново выделять меш не нужно — достаточно загрузить новые данные вершин в уже существующий меш. Большая часть pull request заключалась в том, чтобы решить, в каких случаях безопасно обновлять на месте, а в каких действительно нужно пересоздавать. В итоге производительность анимированных Polygon2D выросла примерно втрое: время кадра сократилось с 35 мс до 13 мс, а сцена поднялась с 28 FPS до 83 FPS. После изменения на рендеринг приходилось около 50% времени кадра, на анимацию — около 30%, а на обновление Polygon2D — 20%. Автор отметил, что возможен и дальнейший прирост — перезагружать только изменившиеся области меша вместо целого меша, — но такие частичные обновления требуют реального тестового сценария, который бы ими руководствовался, а поскольку эта игра анимирует почти каждую вершину в каждом кадре, отслеживание частичных изменений, скорее всего, даст чистый минус.
Второй случай касался конвейера компиляции шейдеров D3D12 в Godot, который взяли в работу после жалоб на медленную загрузку. Самым медленным шагом конвейера была транспиляция, и переход с Vulkan на DXIL давал заметный штраф ко времени загрузки, потому что Vulkan умеет потреблять SPIR-V напрямую. Трассировка в Superluminal показала причину: плотная многопоточность с большими провалами, где активны лишь пара потоков. Приближение показало, что провалы возникали, когда все потоки одновременно запрашивали у ОС больше кучи памяти — они простаивали в ожидании выделения из единой глобальной кучи. Исправление дало каждому потоку собственную кучу вместо постоянных обращений к общей глобальной куче, убрав простои. Это сэкономило 11 секунд времени загрузки в демо TPS — время, в течение которого CPU полностью простаивал. Оба примера подтверждают один урок: с правильным инструментом решение становится очевидным, как только проблема правильно определена.