Толпа на максималках: flow fields, GPU и десятки тысяч юнитов в кадре
https://dev-math.ru/articles/crowd-gpu/
Помните прошлую статью про толпу? Я остановился на тысяче юнитов и оставил четыре вещи «на потом» — flow fields, симуляцию на GPU, indirect draw и LOD AI. Обещал продолжение, да и вы за него проголосовали — и вот оно 🙂 Четыре интерактива (один — честная 3D-сцена на three.js, с реальным счётчиком draw call'ов), и снова получилось так объёмно, что впору сшивать в книжку. Жду огоньков и репостов друзьям 🔥
Тысяча юнитов на CPU — это разминка. Десятки тысяч в кадре (Париж в AC Unity, битвы Total War, орды нежити) — уже другая архитектура из четырёх слоёв. Глобальный путь перестаёт быть тысячей независимых A* и становится одним flow field — сеткой направлений на всю толпу за O(1). Симуляция агентов уезжает с ядер CPU на тысячи дорожек GPU (compute shaders + StructuredBuffer). Рендер перестаёт быть draw call'ом на агента и становится indirect draw, где команды отрисовки генерит сам GPU. А то, что не видно вблизи, не считается честно — LOD AI и recycling. В финале — разбор под капотом Planetary Annihilation, Supreme Commander 2, AC Unity, Total War, They Are Billions и Helldivers 2.
Первая часть, если пропустили: https://dev-math.ru/articles/crowd/ Думаю на этом тема толп в играх раскрыта окончательно :)
#мат_геймдев #МатРазбор #algorithms #GPU
https://dev-math.ru/articles/crowd-gpu/
Помните прошлую статью про толпу? Я остановился на тысяче юнитов и оставил четыре вещи «на потом» — flow fields, симуляцию на GPU, indirect draw и LOD AI. Обещал продолжение, да и вы за него проголосовали — и вот оно 🙂 Четыре интерактива (один — честная 3D-сцена на three.js, с реальным счётчиком draw call'ов), и снова получилось так объёмно, что впору сшивать в книжку. Жду огоньков и репостов друзьям 🔥
Тысяча юнитов на CPU — это разминка. Десятки тысяч в кадре (Париж в AC Unity, битвы Total War, орды нежити) — уже другая архитектура из четырёх слоёв. Глобальный путь перестаёт быть тысячей независимых A* и становится одним flow field — сеткой направлений на всю толпу за O(1). Симуляция агентов уезжает с ядер CPU на тысячи дорожек GPU (compute shaders + StructuredBuffer). Рендер перестаёт быть draw call'ом на агента и становится indirect draw, где команды отрисовки генерит сам GPU. А то, что не видно вблизи, не считается честно — LOD AI и recycling. В финале — разбор под капотом Planetary Annihilation, Supreme Commander 2, AC Unity, Total War, They Are Billions и Helldivers 2.
Первая часть, если пропустили: https://dev-math.ru/articles/crowd/ Думаю на этом тема толп в играх раскрыта окончательно :)
#мат_геймдев #МатРазбор #algorithms #GPU
🔥18
Лоу-поли, а тормозит: кто съел ваш FPS?
Поговорим о дроуколлах. А то про графику мы зашли сразу с дитеринга, давайте пойдёмся по базе. Так сказать с самого начала. Думаю за 2-3 месяца можно рассказать интересно о том, как рендер работает в современном мире в деталях.
Собираете уровень из лоу-поли-пака: деревня, лес, заборы, камни — по паре сотен треугольников на модель. Геометрия копеечная, а кадр проседает. Открываете профайлер и видите странное: видеокарта простаивает, весь кадр съел процессор, а счётчик батчей в статистике рендера ушёл в тысячи. Та же история в другом костюме — клон Minecraft: мир из кубов по 12 треугольников, каждый куб — отдельный объект, и уже десяток чанков кладёт игру. Как так? Ведь треугольников мало и причем тут процессор?
Процессор не рисует — он раздаёт команды. Draw call — это команда «нарисуй вот этот меш вот этим материалом», которую CPU через графический драйвер отправляет видеокарте. И перед каждой такой командой — бумажная работа: собрать состояние (какой шейдер, какие текстуры, какие буферы, как смешивать), драйвер всё это проверит, переведёт на язык GPU и положит в командный буфер. Только после этого видеокарта доберётся до треугольников.
Собственно, ключевое свойство: цена этой бумажной работы от количества треугольников почти не зависит. Это как заказы в интернет-магазине: оформить один заказ на тысячу ручек — пара минут, оформить тысячу заказов по одной ручке — вечер. Товар тот же, вся разница — в накладных расходах на каждый заказ.
Лоу-поли-пак экономит то, что и так ничего не стоит. Миллион треугольников одним вызовом GPU прожуёт легче, чем деревню из тысячи заборов — тысячей вызовов. Поэтому важно следить за числом дроуколлов.
Статья этой недели «Путь одного draw call»: пройдём всю дорогу команды от вызова в коде до пикселей на экране.
#геймдев #графика #математика #gpu #drawcall #оптимизация
Поговорим о дроуколлах. А то про графику мы зашли сразу с дитеринга, давайте пойдёмся по базе. Так сказать с самого начала. Думаю за 2-3 месяца можно рассказать интересно о том, как рендер работает в современном мире в деталях.
Собираете уровень из лоу-поли-пака: деревня, лес, заборы, камни — по паре сотен треугольников на модель. Геометрия копеечная, а кадр проседает. Открываете профайлер и видите странное: видеокарта простаивает, весь кадр съел процессор, а счётчик батчей в статистике рендера ушёл в тысячи. Та же история в другом костюме — клон Minecraft: мир из кубов по 12 треугольников, каждый куб — отдельный объект, и уже десяток чанков кладёт игру. Как так? Ведь треугольников мало и причем тут процессор?
Процессор не рисует — он раздаёт команды. Draw call — это команда «нарисуй вот этот меш вот этим материалом», которую CPU через графический драйвер отправляет видеокарте. И перед каждой такой командой — бумажная работа: собрать состояние (какой шейдер, какие текстуры, какие буферы, как смешивать), драйвер всё это проверит, переведёт на язык GPU и положит в командный буфер. Только после этого видеокарта доберётся до треугольников.
Собственно, ключевое свойство: цена этой бумажной работы от количества треугольников почти не зависит. Это как заказы в интернет-магазине: оформить один заказ на тысячу ручек — пара минут, оформить тысячу заказов по одной ручке — вечер. Товар тот же, вся разница — в накладных расходах на каждый заказ.
Лоу-поли-пак экономит то, что и так ничего не стоит. Миллион треугольников одним вызовом GPU прожуёт легче, чем деревню из тысячи заборов — тысячей вызовов. Поэтому важно следить за числом дроуколлов.
Статья этой недели «Путь одного draw call»: пройдём всю дорогу команды от вызова в коде до пикселей на экране.
#геймдев #графика #математика #gpu #drawcall #оптимизация
🔥20❤5❤🔥1
Поговорим про шину
Вы взяли ту же геометрию, тот же свет, те же материалы — просто заменили текстуры с 1K на 4K, чтобы было почетче. Ни одной новой строки в шейдере. А fps просел, и на телефоне куда сильнее, чем на компе. Профайлер показывает знакомое: ядра GPU недогружены — видеокарта не считает, она ждёт. Ждёт данные.
Дело в том, что у видеокарты две очень разные функции. Считать — умножать, складывать, гонять шейдерную арифметику — она умеет чудовищно быстро, тысячами потоков разом. А доставать данные из памяти — текстуру, вершины, буфер — заметно медленнее: байты лежат в видеопамяти далеко от вычислительных ядер, и их надо провезти по шине через кэши к тому месту, где над ними наконец сделают пару операций. Bandwidth (пропускная способность) — это сколько байт в секунду успевает проехать по этой дороге.
Собственно, ключевое свойство: на современном GPU довезти байт до вычисления дороже, чем само вычисление сделать. 4K-текстура — это вчетверо больше пикселей по каждой стороне, то есть в шестнадцать раз больше байт, которые тащатся по той же шине через тот же кэш. Шейдер не поменялся, а данных, которые надо подвезти, стало в разы больше — и подвоз не поспевает.
Вот почему безобидная замена текстур чего-то стоит, и не только текстур. Похожий механизм — дешёвый на вид полноэкранный эффект: блюр или bloom заняты не арифметикой, а выборкой текстур и чтением-записью целого кадрового буфера — и проходов таких много. На телефоне часто это буквально гоняет кадр во внешнюю память и обратно.
По сути, у кадра две точки для ключевой оптимизации. Одна — сколько GPU может посчитать, другая — сколько данных можно провезти. Уперлись в первый — оптимизируйте шейдеры. Уперлись во второй — арифметику хоть в два раза ускорьте, быстрее не станет; лечить надо другое: мельче текстуры, мипмапы, сжатие, меньше проходов по полноэкранному буферу. Мипмапы, кстати, дают fps не только за счёт сглаживания — уменьшенная копия лежит в памяти компактно и попадает в кэш, а полноразмерная текстура на далёком объекте читается вразнобой и впустую гоняет данные по шине.
А в пятницу в статье «Путь одного draw call» проследим всю дорогу команды — от вызова в коде через шину и стадии GPU до пикселя.
#геймдев #графика #математика #gpu #bandwidth #оптимизация
Вы взяли ту же геометрию, тот же свет, те же материалы — просто заменили текстуры с 1K на 4K, чтобы было почетче. Ни одной новой строки в шейдере. А fps просел, и на телефоне куда сильнее, чем на компе. Профайлер показывает знакомое: ядра GPU недогружены — видеокарта не считает, она ждёт. Ждёт данные.
Дело в том, что у видеокарты две очень разные функции. Считать — умножать, складывать, гонять шейдерную арифметику — она умеет чудовищно быстро, тысячами потоков разом. А доставать данные из памяти — текстуру, вершины, буфер — заметно медленнее: байты лежат в видеопамяти далеко от вычислительных ядер, и их надо провезти по шине через кэши к тому месту, где над ними наконец сделают пару операций. Bandwidth (пропускная способность) — это сколько байт в секунду успевает проехать по этой дороге.
Собственно, ключевое свойство: на современном GPU довезти байт до вычисления дороже, чем само вычисление сделать. 4K-текстура — это вчетверо больше пикселей по каждой стороне, то есть в шестнадцать раз больше байт, которые тащатся по той же шине через тот же кэш. Шейдер не поменялся, а данных, которые надо подвезти, стало в разы больше — и подвоз не поспевает.
Вот почему безобидная замена текстур чего-то стоит, и не только текстур. Похожий механизм — дешёвый на вид полноэкранный эффект: блюр или bloom заняты не арифметикой, а выборкой текстур и чтением-записью целого кадрового буфера — и проходов таких много. На телефоне часто это буквально гоняет кадр во внешнюю память и обратно.
По сути, у кадра две точки для ключевой оптимизации. Одна — сколько GPU может посчитать, другая — сколько данных можно провезти. Уперлись в первый — оптимизируйте шейдеры. Уперлись во второй — арифметику хоть в два раза ускорьте, быстрее не станет; лечить надо другое: мельче текстуры, мипмапы, сжатие, меньше проходов по полноэкранному буферу. Мипмапы, кстати, дают fps не только за счёт сглаживания — уменьшенная копия лежит в памяти компактно и попадает в кэш, а полноразмерная текстура на далёком объекте читается вразнобой и впустую гоняет данные по шине.
А в пятницу в статье «Путь одного draw call» проследим всю дорогу команды — от вызова в коде через шину и стадии GPU до пикселя.
#геймдев #графика #математика #gpu #bandwidth #оптимизация
🔥21👍5❤🔥2
Обернул шейдер в if, чтобы считать реже. А быстрее не стало
Вы нашли в шейдере тяжёлый кусок — дорогой цикл, лишний проход, ветку с кучей математики — и обернули его в if: мол, для половины пикселей эту дорогую ветку пропустим и сэкономим. Пропустили. А кадр не разгрузился ни на сколько.
Видеокарта прячет задержку памяти тем, что держит не один поток, а тысячи. Один встал на выборке из памяти — планировщик тут же пускает считать другой, у которого данные уже приехали; пока первый ждёт свою порцию, вычислители заняты чужой работой. Вот зачем на GPU тысячи потоков. Мало потоков— прятать нечем, и тогда видеокарта реально встаёт на сотни тактов ожидания.
Но тут и зарыта плата. Одно ядро GPU — это не маленький процессор, а простая и небыстрая линия: ни хитрого предсказания ветвлений, ни глубоких кэшей, один поток на нём медленнее, чем на CPU. Сила — в количестве. И эти линии не независимы: они идут строем, группами (у NVIDIA — по 32), и вся группа исполняет одну и ту же инструкцию разом, каждая над своими данными. Это SIMT — одна команда, много потоков. Представьте не тысячу гонцов, у каждого свой маршрут, а колонну, марширующую в ногу: шаг у всех общий.
Собственно, вот и разгадка вашего if. У этого есть имя — дивергенция ветвлений (branch divergence, ещё говорят «расхождение варпа»). Когда потоки одной группы на развилке расходятся, железу приходится прогнать обе ветки по очереди, отключая на каждой те линии, которых там нет. Группа идёт так же долго, как все пути её потоков вместе взятые. Ваш «пропустить дорогой код» экономит, только если его пропускает вся колонна разом. Один поток свернул в тяжёлую ветку — и остальные тридцать один стоят и ждут его.
По сути, ветвления тут не под запретом — платите вы не за if, а за расхождение внутри группы. Противоположность расхождения — когерентность: это когда соседние потоки заняты одним и тем же, все сворачивают в одну сторону и шагают в ногу. Такая когерентная ветка почти бесплатна — вторая сторона просто не исполняется.
Пример. Материал с enum-режимом и switch (_Mode) в шейдере (классический ubershader, куда свалили все варианты освещения). Если режим один на весь материал — uniform-параметр, — весь варп читает одно значение и идёт одной веткой, остальные просто не исполняются: расхождения нет, дёшево. Но стоит выбирать режим попиксельно — из маски материалов, material-ID в G-буфере, слоёв терреина — и соседние пиксели одного варпа уходят в разные ветки: на стыках материалов варп прогоняет все режимы по очереди.
И та же боль в raymarching и SDF: часть лучей упирается в поверхность за пару шагов, часть марширует до предела — и вся группа топчется в цикле, пока не закончит самый глубокий луч. Ранний выход по одному лучу ничего не даёт: время группы держит самый медленный поток.
В четверг — почему переключение шейдера или материала сбивает весь этот разгон: смена стейта и флаш конвейера, и чем тут спасают батчинг с инстансингом.
#devmath #графика #математика #gpu #simt #оптимизация
Вы нашли в шейдере тяжёлый кусок — дорогой цикл, лишний проход, ветку с кучей математики — и обернули его в if: мол, для половины пикселей эту дорогую ветку пропустим и сэкономим. Пропустили. А кадр не разгрузился ни на сколько.
Видеокарта прячет задержку памяти тем, что держит не один поток, а тысячи. Один встал на выборке из памяти — планировщик тут же пускает считать другой, у которого данные уже приехали; пока первый ждёт свою порцию, вычислители заняты чужой работой. Вот зачем на GPU тысячи потоков. Мало потоков— прятать нечем, и тогда видеокарта реально встаёт на сотни тактов ожидания.
Но тут и зарыта плата. Одно ядро GPU — это не маленький процессор, а простая и небыстрая линия: ни хитрого предсказания ветвлений, ни глубоких кэшей, один поток на нём медленнее, чем на CPU. Сила — в количестве. И эти линии не независимы: они идут строем, группами (у NVIDIA — по 32), и вся группа исполняет одну и ту же инструкцию разом, каждая над своими данными. Это SIMT — одна команда, много потоков. Представьте не тысячу гонцов, у каждого свой маршрут, а колонну, марширующую в ногу: шаг у всех общий.
Собственно, вот и разгадка вашего if. У этого есть имя — дивергенция ветвлений (branch divergence, ещё говорят «расхождение варпа»). Когда потоки одной группы на развилке расходятся, железу приходится прогнать обе ветки по очереди, отключая на каждой те линии, которых там нет. Группа идёт так же долго, как все пути её потоков вместе взятые. Ваш «пропустить дорогой код» экономит, только если его пропускает вся колонна разом. Один поток свернул в тяжёлую ветку — и остальные тридцать один стоят и ждут его.
По сути, ветвления тут не под запретом — платите вы не за if, а за расхождение внутри группы. Противоположность расхождения — когерентность: это когда соседние потоки заняты одним и тем же, все сворачивают в одну сторону и шагают в ногу. Такая когерентная ветка почти бесплатна — вторая сторона просто не исполняется.
Пример. Материал с enum-режимом и switch (_Mode) в шейдере (классический ubershader, куда свалили все варианты освещения). Если режим один на весь материал — uniform-параметр, — весь варп читает одно значение и идёт одной веткой, остальные просто не исполняются: расхождения нет, дёшево. Но стоит выбирать режим попиксельно — из маски материалов, material-ID в G-буфере, слоёв терреина — и соседние пиксели одного варпа уходят в разные ветки: на стыках материалов варп прогоняет все режимы по очереди.
И та же боль в raymarching и SDF: часть лучей упирается в поверхность за пару шагов, часть марширует до предела — и вся группа топчется в цикле, пока не закончит самый глубокий луч. Ранний выход по одному лучу ничего не даёт: время группы держит самый медленный поток.
В четверг — почему переключение шейдера или материала сбивает весь этот разгон: смена стейта и флаш конвейера, и чем тут спасают батчинг с инстансингом.
#devmath #графика #математика #gpu #simt #оптимизация
🔥13👍1
Гонитесь за числом дроуколлов, а тормозит другое
Допустим в оптимизации вы гонитесь за числом draw calls: объединяете меши, чистите объекты — и счётчик Batches ползёт вниз. Дело конечно хорошее. А кадр работает всё так же медленно, особенно на мобилке. Смотрите профайлер внимательнее: draw calls упали, а вторая цифра рядом, SetPass calls, — нет. Что это за цифра?
Дело в том, что это две разные вещи. Batch (draw call) — это просто команда «нарисуй» на текущем состоянии. А SetPass случается, только когда состояние надо СМЕНИТЬ: другой шейдер, другая текстура, другой блендинг, другой Z-тест. Сто вызовов на одном материале — это один SetPass и куча дешёвых команд следом. А смена материала между вызовами — это новый SetPass: перенастройка конвейера, и в тяжёлом случае видеокарта сливает всё, что уже запустила, прежде чем переключиться. Это и есть флаш конвейера (pipeline flush).
Собственно, вот тема дня: цену кадру набивает не сколько вы рисуете, а сколько раз между вызовами меняете стейт. Поэтому один материал со статик-батчингом летит — сотни мешей идут одним SetPass'ом; а десяток материалов вперемешку кладёт кадр, хотя вы не добавили ни полигона — вы добавили переключений контекста. Жёстче всего флаш на смене render target: у мобильных тайловых GPU это выгрузка целого тайла в память,.
Техника дальше весьма логичная. Меньше разных материалов и шейдеров, свести похожее в один материал и атлас, сортировать по стейту, чтобы объекты с одним состоянием шли подряд. Батчинг, инстансинг и SRP Batcher по-разному делают одно и то же — держат стейт на месте, чтобы конвейер переставлять приходилось как можно реже.
Подробнее разберу завтра.
#devmath #графика #математика #gpu #батчинг #оптимизация
Допустим в оптимизации вы гонитесь за числом draw calls: объединяете меши, чистите объекты — и счётчик Batches ползёт вниз. Дело конечно хорошее. А кадр работает всё так же медленно, особенно на мобилке. Смотрите профайлер внимательнее: draw calls упали, а вторая цифра рядом, SetPass calls, — нет. Что это за цифра?
Дело в том, что это две разные вещи. Batch (draw call) — это просто команда «нарисуй» на текущем состоянии. А SetPass случается, только когда состояние надо СМЕНИТЬ: другой шейдер, другая текстура, другой блендинг, другой Z-тест. Сто вызовов на одном материале — это один SetPass и куча дешёвых команд следом. А смена материала между вызовами — это новый SetPass: перенастройка конвейера, и в тяжёлом случае видеокарта сливает всё, что уже запустила, прежде чем переключиться. Это и есть флаш конвейера (pipeline flush).
Собственно, вот тема дня: цену кадру набивает не сколько вы рисуете, а сколько раз между вызовами меняете стейт. Поэтому один материал со статик-батчингом летит — сотни мешей идут одним SetPass'ом; а десяток материалов вперемешку кладёт кадр, хотя вы не добавили ни полигона — вы добавили переключений контекста. Жёстче всего флаш на смене render target: у мобильных тайловых GPU это выгрузка целого тайла в память,.
Техника дальше весьма логичная. Меньше разных материалов и шейдеров, свести похожее в один материал и атлас, сортировать по стейту, чтобы объекты с одним состоянием шли подряд. Батчинг, инстансинг и SRP Batcher по-разному делают одно и то же — держат стейт на месте, чтобы конвейер переставлять приходилось как можно реже.
Подробнее разберу завтра.
#devmath #графика #математика #gpu #батчинг #оптимизация
🔥10✍1
Путь одного draw call
Всю неделю мы считали, чем платим за кадр: в понедельник — командами, во вторник — байтами на шине, в среду — когерентностью варпов, в четверг — сменами состояния. Обещанная статья готова: «Путь одного draw call» — проходим дорогу команды рисования целиком, от строчки в вашем коде до пикселя на экране, со всеми четырьмя валютами на местах.
В общем я довольно подробно описал разные части графического конвейера, и не как стандартную схему и так далее, а более практично в контексте движка.
Читать: https://dev-math.ru/articles/drawcall/
#devmath #графика #математика #gpu #drawcall #оптимизация
Всю неделю мы считали, чем платим за кадр: в понедельник — командами, во вторник — байтами на шине, в среду — когерентностью варпов, в четверг — сменами состояния. Обещанная статья готова: «Путь одного draw call» — проходим дорогу команды рисования целиком, от строчки в вашем коде до пикселя на экране, со всеми четырьмя валютами на местах.
В общем я довольно подробно описал разные части графического конвейера, и не как стандартную схему и так далее, а более практично в контексте движка.
Читать: https://dev-math.ru/articles/drawcall/
#devmath #графика #математика #gpu #drawcall #оптимизация
🔥13
Шина видеокарты и fps
https://youtube.com/shorts/39vq_Y3LPEs?si=kHte0rGuj7kpavSu
Продолжаю экспериментировать с форматом коротких видео.
Видеокарта считает тысячами потоков разом — а байты к этим ядрам ещё надо провезти по шине через кэши.
Разбор целиком — в статье про путь одного draw call.
#компьютернаяграфика #оптимизация #gpu #геймдев #devmath
https://youtube.com/shorts/39vq_Y3LPEs?si=kHte0rGuj7kpavSu
Продолжаю экспериментировать с форматом коротких видео.
Видеокарта считает тысячами потоков разом — а байты к этим ядрам ещё надо провезти по шине через кэши.
Разбор целиком — в статье про путь одного draw call.
#компьютернаяграфика #оптимизация #gpu #геймдев #devmath
YouTube
Шина видеокарты и fps
Видеокарта считает тысячами потоков разом — а байты к этим ядрам ещё надо провезти по шине через кэши.
Разбор целиком — в статье про путь одного draw call, ссылка в профиле.
#компьютернаяграфика #оптимизация #gpu #геймдев #devmath
Разбор целиком — в статье про путь одного draw call, ссылка в профиле.
#компьютернаяграфика #оптимизация #gpu #геймдев #devmath
🔥3❤1👎1