Создание эффекта волн на GPU с использованием нитей

Опубликовано мая 2, 2018 в Технология CUDA

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

Мы снова воспользуемся вычислительной мощью GPU для программной генерации картинок. Но чтобы было интереснее, мы их еще и анимируем. Не пугайтесь, весь не относящийся к делу код анимации мы вынесли во вспомогательные функции, так что владения компьютерной графикой или техникой анимации от вас не потребуется.

Наиболее сложная часть main () скрыта во вспомогательном классе. Как и раньше, мы выделяем память с помощью cudaMalloc (), исполняем на устройстве код, в котором используется выделенная память, а затем освобождаем память, вызывая cudaFree (). Эта последовательность теперь должна казаться вам рутинной.

В этом примере мы немного усложнили способ выполнения среднего шага, «исполнение на устройстве кода, в котором используется выделенная память».
Мы передаем методу anim_and_exit () указатель на функцию generate_frame (). Класс будет вызывать ее всякий раз, как понадобится сгенерировать новый кадр анимации.

В четырех строчках этой функции заключены важные концепции CUDA С. Во-первых, мы объявляем двумерные переменные blocks и threads. Понятно, что blocks — это число параллельных блоков в запускаемой сетке, a threads — число нитей в одном блоке. Так как мы собираемся генерировать изображение, то используем двумерную индексацию, так что у каждой нити будет уникальный индекс (х, у), которому легко поставить в соответствие пиксель в создаваемом изображении. Мы решили, что блоки будут содержать массив нитей размером 16 х 16. Если размер изображения составляет DIM х DIM пикселей, то, для того чтобы на каждый пиксель приходилась одна нить, нужно будет запустить DIM/16 х DIM/16 блоков. На рисунке показано, как выглядит такая конфигурация блоков и нитей для изображения шириной 48 пиксель и высотой 32 пикселя — до смешного маленького.

Создание эффекта волн на GPU с использованием нитей

Если вам доводилось заниматься многопоточным программированием для CPU, то, наверное, вас удивляет, зачем так много нитей. Ведь чтобы выполнить анимацию высокой четкости на экране размером 1920 х 1080, будет создано более двух миллионов нитей. На GPU создание и планирование такого гигантского количества нитей — обычное дело, тогда как на CPU никто об этом и помышлять бы не стал. Поскольку управление нитями на CPU осуществляется программно, он просто не может справиться с таким количеством нитей, как GPU. Но раз мы в состоянии создать по одной нити для каждого обрабатываемого элемента, то параллельное программирование для GPU оказывается гораздо проще, чем для CPU.

Объявив переменные, в которых будут храниться параметры параллельного исполнения, мы просто запускаем ядро, вычисляющее значения пикселей.

Ядру необходимо знать две вещи, которые мы передаем в виде параметров. Во-первых, нужен указатель на область памяти устройства, в котором будут храниться вычисленные пиксели. Эта память выделена в функции main (), и указатель на нее помещен в глобальную переменную. Однако переменная «глобальна» лишь в коде, исполняемом CPU, поэтому ее необходимо передавать в параметре, что-бы исполняющая среда CUD А могла сделать ее доступной в коде, исполняемом устройством.

Во-вторых, ядро должно знать текущее время анимации, чтобы сгенерировать правильный кадр. Это время, ticks, передается функции generate_frame () из инфраструктурного кода в классе CPUAnimBitmap, поэтому мы можем переправить его ядру без изменения.

Здесь каждая нить определяет свой индекс в блоке, а также индекс своего блока в сетке и преобразует их в уникальный индекс (х, у) пикселя в изображении. Таким образом, когда нить с индексом (3, 5) в блоке (12, 8) начинает выполнение, она знает, что слева от нее есть 12 полных блоков, а сверху — 8 полных блоков. Внутри же блока слева от нити с индексом (3, 5) находятся три нити, а над ней -пять нитей. Поскольку всего в каждом блоке 16 нитей, то получается, что слева от рассматриваемой нити имеются 3 нити + 12 блоков * 16 нитей/блок = 195 нитей, а над ней — 5 нитей + 8 блоков * 16 нитей/блок =128 нитей.

Это и есть вычисление х и у в первых двух строчках; именно так мы отображаем индексы нити и блока на координаты пикселя в изображении. Далее мы просто линеаризуем эти значения х и у и получаем смещение пикселя от начала выходного буфера. Все это аналогично вычислениям, проделанным в разделах •«Сложение более длинных векторов на GPU» и «Сложение векторов произвольной длины на GPU».

Поскольку мы знаем пиксель (х, у), который должна обсчитывать нить, и время, когда она должна вычислять его значение, то можем вычислить произвольную функцию от (x.y.t) и поместить результат в выходной буфер. В данном случае функция порождает синусоидальную «рябь».

Мы рекомендуем не задерживаться слишком долго на вычислении переменной grey. По сути дела, это простая двумерная функция от времени, которая при анимации порождает симпатичный эффект волн. На рис. 5.3 показан снимок одного кадра анимации.