Оптимізація CPU-рендерингу Godot: як профілювання перетворило два вузькі місця на серйозний приріст

  • PublicPublic
  • 15 Sep, 2026
Оптимізація CPU-рендерингу Godot: як профілювання перетворило два вузькі місця на серйозний приріст

Збоку оптимізація рендерингу часто виглядає як чорна магія, але насправді процес куди методичніший, ніж здається. Розробники 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 повністю простоював. Обидва приклади підтверджують один урок: з правильним інструментом рішення стає очевидним, щойно проблему правильно визначено.