Почему мобильная игра не держит 60 FPS?
На Samsung Galaxy A52 (Adreno 618) игра выдаёт 28–32 FPS при целевых 60. На Xiaomi Redmi Note 11 с Helio G96 и Mali-G57 — стабильные 55–60 FPS. Разная производительность на похожих по цене устройствах — типичная ситуация в мобильном геймдеве. Причина почти всегда в том, что оптимизация проводилась на флагмане, а mid-range GPU с другой архитектурой даёт совсем другую картину. Наша оптимизация рендеринга мобильной игры начинается с анализа bottleneck на конкретных чипах. Мы помогли заказчикам сократить затраты на GPU вдвое, избежав дорогих переделок.
Мы специализируемся на кросс-платформенной оптимизации: работаем с Unity, Unreal Engine и собственных движках. За плечами — более 15 проектов, где мы доводили FPS до стабильных 60 на целевых устройствах. В результате оптимизации рендеринга мобильной игры мы добивались прироста в 20–30% без потери качества. Ключ к стабильной производительности — понимание архитектуры мобильных GPU. В отличие от настольных, они используют tile-based rendering, что накладывает особые требования к шейдерам и батчингу.
Как определить bottleneck на мобильном GPU?
Прежде чем оптимизировать — понять что является bottleneck. В Unity: Frame Debugger + Profiler. Включаем Profile GPU в Android Player Settings и смотрим Profiler → GPU. Если GPU-время на кадр близко к 16ms (60 FPS) а CPU-время значительно меньше — GPU-bound. Если наоборот — CPU-bound.
На Unreal Engine: stat GPU в консоли, ProfileGPU команда, RenderDoc для захвата кадра. r.ScreenPercentage 50 — быстрый тест: если FPS резко вырос при снижении разрешения вдвое — точно GPU-bound. Команды stat unit, stat drawcalls, r.ShowFlag.Rendering 1 дают breakdown по CPU/GPU и количеству draw calls.
Почему draw calls критичны для mid-range устройств?
На мобильных GPU draw call overhead выше чем на консолях/PC. 500+ draw calls в кадре — красная зона для mid-range Android. Каждый уникальный материал = отдельный draw call. Каждый MeshRenderer с уникальным материалом — ещё один.
Static batching (Unity): объекты с одинаковым материалом объединяются в один mesh. Требование — одинаковый Material asset (не просто одинаковые настройки). Mark as Static в Inspector. Работает автоматически при сборке.
GPU Instancing: для повторяющихся объектов (трава, деревья, враги одного типа):
// Материал должен поддерживать instancing material.enableInstancing = true; // Рисуем 1000 экземпляров одним draw call Graphics.DrawMeshInstanced(mesh, 0, material, matrices, 1000); SRP Batcher (Unity URP/HDRP): автоматически батчит объекты с разными материалами но одинаковым шейдером. Включается в URP Asset → SRP Batcher = enabled. Самый простой способ сократить draw calls без ручного batching.
Как мы оптимизируем рендеринг
Шейдеры под tile-based GPU
Мобильные GPU (Adreno, Mali, PowerVR, Apple) используют Tile-Based Immediate Mode Rendering. Экран делится на тайлы, каждый рендерится целиком в быстром on-chip памяти. Это означает:
- Framebuffer fetch — чтение из текущего framebuffer внутри тайла — практически бесплатно. Используем для deferred lighting:
gl_LastFragDataв GLSL (GLES extensionEXT_shader_framebuffer_fetch). - Depth pre-pass на мобиле — часто лишний overhead, TBIMR и так эффективно работает с depth test внутри тайла.
- Discard в фрагментном шейдере (alpha-test, clip) — на tile-based GPU убивает ранний depth test для целого тайла. Заменяем alpha-blend или alpha-to-coverage там где возможно.
Precision qualifiers в GLSL/Metal
// МЕДЛЕННО — highp везде по умолчанию uniform highp mat4 ModelMatrix; varying highp vec2 TexCoord; // БЫСТРО — минимально необходимая точность uniform highp mat4 ModelMatrix; // матрицы — highp обязательно varying mediump vec2 TexCoord; // UV-координаты — mediump достаточно varying lowp vec4 VertexColor; // цвет — lowp На Mali GPU переход с highp на mediump для texture samplers — от 10 до 25% прирост производительности фрагментного шейдера.
ALU vs Texture Fetch
На большинстве мобильных GPU texture fetch дешевле чем тяжёлые ALU-вычисления (sin, pow, sqrt). Предварительно запечённые lookup таблицы в текстуре быстрее чем вычисление в шейдере:
// Медленно: вычисляем fresnel в шейдере float fresnel = pow(1.0 - dot(viewDir, normal), 5.0); // Быстро: lookup texture float fresnel = texture2D(fresnelLUT, vec2(dot(viewDir, normal), roughness)).r; Совет по профилированию на Mali
Для Mali GPU используйте инструмент Streamline Performance Analyzer (ARM DS-5) или AGI (Android GPU Inspector). Обращайте внимание на контроллеры: Fragment ALU cycles, Fragment texture cycles, и Memory bandwidth. Это поможет точно определить, узкое место — ALU, текстуры или пропускная способность.Dynamic resolution и adaptive performance
Рендерить в нативном разрешении iPhone 15 Pro (2556×1179) — избыточно для мобильной игры с интенсивным рендерингом. Стандартная практика — render scale 0.7–0.85 от нативного с последующим upscale.
Unity URP Dynamic Resolution:
ScalableBufferManager.ResizeBuffers(0.75f, 0.75f); // 75% от нативного Unreal Mobile Super Resolution (MSR) — встроенный temporal upscaler для мобильных платформ с Unreal 5.1+. r.Mobile.TemporalAA 1. Даёт качество близкое к нативному при значительно меньшей GPU-нагрузке.
Adaptive Performance (Samsung Game SDK + Unity): автоматически снижает нагрузку при перегреве. Доступны метрики теплового статуса и производительности через UnityEngine.AdaptivePerformance.
Кейс: 40 → 58 FPS на Adreno 618
Из нашей практики: runner-игра на Galaxy A52 — 40 FPS. Профилирование через AGI показало: Fragment ALU 87%, fragment bandwidth — перегружен. Три изменения:
- Шейдер воды: заменили
pow(fresnel, 5.0)на LUT-текстуру → -8ms GPU -
highp→mediumpдля всех texture samplers → -4ms GPU - Dynamic resolution 0.80 вместо нативного → -6ms GPU
Итого: с 40 до 58 FPS без изменения визуального стиля. На Pro-устройствах — без изменений, они держали 60 FPS с запасом.
| Метрика | До | После | Снижение |
|---|---|---|---|
| FPS | 40 | 58 | +18 (45%) |
| GPU time (ms) | 25 | 16.5 | 34% |
| Draw calls | 780 | 210 | 73% |
Процесс работы
- Аналитика: сбор логов, профилирование на целевых устройствах, определение bottleneck.
- Проектирование: выбор методов оптимизации (batching, шейдеры, dynamic resolution).
- Реализация: внедрение изменений в код и ассеты.
- Тестирование: прогон на 5+ различных устройствах, сравнение FPS и качества.
- Деплой: подготовка релизной сборки, настройка Adaptive Performance.
Сроки: профилирование и анализ занимают 2–3 дня. Оптимизация шейдеров, batching, dynamic resolution — от 1 до 3 недель в зависимости от состояния проекта.
Что вы получите
- Подробный отчёт с анализом рендеринга и узкими местами
- Оптимизированные шейдеры с минимальной точностью (mediump/lowp)
- Настроенный automatic batching (static batching, SRP Batcher, instancing)
- Конфигурацию Dynamic Resolution с учётом целевых устройств
- Инструкцию по сборке и деплою
- Гарантию стабильности результата на согласованном пуле устройств
| Bottleneck | Симптомы | Инструменты диагностики | Типичные решения |
|---|---|---|---|
| CPU-bound | CPU time > 16ms, много физики, скриптов | Unity Profiler, Unreal stat unit | Оптимизация кода, сжатие ассетов |
| GPU-bound | GPU time > 16ms, высокий fill rate | GPU Profiler, RenderDoc | Снижение разрешения, шейдеры, LOD |
| Draw calls | >500 draw calls, высокая batch count | Frame Debugger, stat drawcalls | Static batching, GPU instancing, SRP |
| Bandwidth | Высокое использование memory bandwidth | GPU counters (Mali, Adreno) | Сжатие текстур, mipmaps, alpha |
Свяжитесь с нами для консультации по вашему проекту. Закажите аудит рендеринга и получите план оптимизации под ваше целевое железо. Узнайте, как сэкономить ресурсы на ранних этапах разработки.







