Когда вы запускаете глобальную стратегию, обычно ожидаете умеренную нагрузку на видеокарту — такие проекты редко поражают детализированной графикой. Однако из-за ряда упрощений в разработке наблюдаются ощутимые просадки fps. Сталкиваясь с низкой производительностью, я решил выяснить, какие именно этапы рендеринга потребляют максимум ресурсов.
Для анализа использовалась версия 1.0.0 с основным Vulkan-рендером. Съёмка GPU-профиля выполнена через Nvidia Nsight при разрешении 2560×1440.
Основы рендеринга
Напомню пять ключевых этапов отрисовки непрозрачных объектов: привязка меша, вершинный шейдер, сборка примитивов и растеризатор, тест глубины/трафарета и пиксельный шейдер. Без LOD производительность вершинного шейдера и сборки примитивов неизменна независимо от видимой площади объекта.
Растеризатор преобразует треугольники в фрагменты — для каждого семпла (центра пикселя или нескольких точек при MSAA) проверяется попадание в полигон и сравнивается глубина. Ближайший к камере треугольник запускает пиксельный шейдер. При этом трансформации всех треугольников могут оказаться напрасными, если позже пиксель перекроет другой объект.
Затем выполняется тест глубины. Если фрагмент ближе камеры, пиксельный шейдер запускается для блока 2×2 пикселей, так как для выбора уровня детализации текстур используются операции dFdx и dFdy, вычисляющие производные по соседним фрагментам.
-
Нельзя задать конкретных соседей для операций dFdx/dFdy: шейдер сравнивает произвольные точки внутри блока 2×2 (обычно верхний левый с остальными).
-
При треугольниках размером в один пиксель формулы dFdx/dFdy даёт неточное значение, что ухудшает качество текстур, несмотря на рост числа полигонов.
-
«Однопиксельный» треугольник всё равно запускает пиксельный шейдер четыре раза, отбрасывая лишние фрагменты. Стоимость рендеринга полигона из одного пикселя почти равна полигону из трёх пикселей.
Детализированная 3D-карта

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


Далее рендерится террейн. Учитывая объём шейдера, следовало бы сначала рисовать ближайшие объекты (front-to-back), а потом ландшафт. Вместо этого террейн отрисовывается глобально, а остальные вызовы распределены без оптимизации.
Paradox отказалась от разделения мира на регионы, применяя «убершейдер» для всего: в нём задействованы 145 текстурных слоёв террейна BC3 по 512×512. Многие из них имеют низкую детализацию.

Константные буферы содержат 69 индексов материалов и данные для 199 биомов — до 15 материалов на биом. Затем идут глобальные текстуры: ProvinceColorIndirectionTexture_Texture (R8G8_UNORM, 16384×8192, 256 MB VRAM) и атлас высот VirtualHeightmapPhysicalTexture (R16_UNORM, 8192×8192, 128 MB, заполнено лишь наполовину). В сумме все «глобальные» текстуры занимают 838,3 MB VRAM. Шейдер — 4253 строки SPIR-V или 1282 GLSL.


Речные меши насчитывают 2–4 тыс. вершин, особенно много лишней геометрии на прямых участках.


Система LOD не работает должным образом: вызовы для детализированных и упрощённых домов следуют без порядка. Даже «низкий» LOD содержит больше вершин, чем число пикселей, которые он занимает.



Крупнейшая нагрузка приходится на деревья с фрагментным шейдером на 2578 SPIR-V строк (779 GLSL) и встроенной логикой «тумана войны». Вместо отдельного прохода каждый объект рассчитывает туман индивидуально.

Первый vkCmdDrawIndexed рендерит 1979 инстансов, но это лишь часть леса. Всего задействовано 11 draw-вызовов, по 2–4 тыс. мешей в каждом.

Деревья стоят плотно, без предкастинга глубины, что приводит к катастрофическому overdraw: GPU многократно обрабатывает одни и те же пиксели. Из-за тяжёлого шейдера лесная зона снижает fps примерно вдвое по сравнению с пустыней.
Для дизеринга используется статичный Bayer4×4, который не меняется между кадрами и усиливает паттерн при использовании DLSS в режиме «производительность».


После леса рисуются животные: олени, лоси, коровы, овцы, волки и даже бобры.


Даже если фрагментный шейдер пропускается тестом глубины, вершинный шейдер работает для всех вершин: читает две высотные карты, текстуру шума и туман войны. У бобра 256 строк GLSL или 805 SPIR-V, плюс анимация.
Спойлер

Использовано 485 вершин — более 10 вершин на пиксель. Напоминает случай с Cities: Skylines 2, где у персонажей были прорисованы зубы.
Глобальная карта
Глобальная карта работает заметно быстрее, поэтому её оптимизация менее критична.


Первый проход — лёгкий шейдер (447 GLSL, 1625 SPIR-V) и один квад на весь фон, что эффективно при большом покрытии пикселей.

Затем выполняются сотни вызовов для текста (по одному на название страны), после чего границы рисуются мешами — по одному мешу на страну.


Контуры Валахии содержат 4621 треугольник. В wireframe видно лишние полигоны, сделанные с запасом для масштабирования.

При резких поворотах камеры треугольники пересекаются, добавляя лишнюю геометрию, как и у рек.



Граница — зона между двумя государствами, поэтому пиксели рисуются дважды и более.

Шейдер карты рисует границы через SDF-текстуру BorderDistanceFieldTexture_Texture (R8_UNORM, 4096×2048, 8 MB). Такая карта хорошо подходит для чётких линий, но её разрешения недостаточно для Империи Римской.


Стоит разделить мир на регионы (например, 8) и дать каждому собственную SDF-текстуру. Хотя вершинные буферы занимают ~76,84 MB и индексы ~4,8 MB, это снизит нагрузку на растеризатор и сборку примитивов — узкое место GPU.
Заключение
Paradox упростила логику рендеринга ради ускорения разработки: один «убершейдер» вместо набора специализированных, отсутствие сортировки front-to-back, неэффективный overdraw, примитивная обработка линий, недоработанный LOD и неограниченные шэдоукастеры.
В результате — тяжёлые пиксельные шейдеры, избыточная геометрия, большие требования к VRAM и лишние вершинные операции для крошечных или невидимых объектов. Цена — сниженная производительность в реальном времени.
Плюсы — лёгкий режим политической карты и плавные переходы между видами. Ключевые улучшения: специализированные шейдеры, front-to-back сортировка, исправление LOD и жёсткая фильтрация мелких шэдоукастеров.
Если ваше железо слабое, избегайте детализированной карты и особенно лесных зон.


